##// END OF EJS Templates
_adjustlinkrev: reuse ancestors set during rename detection (issue4514)...
Pierre-Yves David -
r23980:c1ce5442 stable
parent child Browse files
Show More
@@ -1,1848 +1,1857
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 mdiff, error, util, scmutil, subrepo, patch, encoding, phases
11 11 import match as matchmod
12 12 import os, errno, stat
13 13 import obsolete as obsmod
14 14 import repoview
15 15 import fileset
16 16 import revlog
17 17
18 18 propertycache = util.propertycache
19 19
20 20 # Phony node value to stand-in for new files in some uses of
21 21 # manifests. Manifests support 21-byte hashes for nodes which are
22 22 # dirty in the working copy.
23 23 _newnode = '!' * 21
24 24
25 25 class basectx(object):
26 26 """A basectx object represents the common logic for its children:
27 27 changectx: read-only context that is already present in the repo,
28 28 workingctx: a context that represents the working directory and can
29 29 be committed,
30 30 memctx: a context that represents changes in-memory and can also
31 31 be committed."""
32 32 def __new__(cls, repo, changeid='', *args, **kwargs):
33 33 if isinstance(changeid, basectx):
34 34 return changeid
35 35
36 36 o = super(basectx, cls).__new__(cls)
37 37
38 38 o._repo = repo
39 39 o._rev = nullrev
40 40 o._node = nullid
41 41
42 42 return o
43 43
44 44 def __str__(self):
45 45 return short(self.node())
46 46
47 47 def __int__(self):
48 48 return self.rev()
49 49
50 50 def __repr__(self):
51 51 return "<%s %s>" % (type(self).__name__, str(self))
52 52
53 53 def __eq__(self, other):
54 54 try:
55 55 return type(self) == type(other) and self._rev == other._rev
56 56 except AttributeError:
57 57 return False
58 58
59 59 def __ne__(self, other):
60 60 return not (self == other)
61 61
62 62 def __contains__(self, key):
63 63 return key in self._manifest
64 64
65 65 def __getitem__(self, key):
66 66 return self.filectx(key)
67 67
68 68 def __iter__(self):
69 69 for f in sorted(self._manifest):
70 70 yield f
71 71
72 72 def _manifestmatches(self, match, s):
73 73 """generate a new manifest filtered by the match argument
74 74
75 75 This method is for internal use only and mainly exists to provide an
76 76 object oriented way for other contexts to customize the manifest
77 77 generation.
78 78 """
79 79 return self.manifest().matches(match)
80 80
81 81 def _matchstatus(self, other, match):
82 82 """return match.always if match is none
83 83
84 84 This internal method provides a way for child objects to override the
85 85 match operator.
86 86 """
87 87 return match or matchmod.always(self._repo.root, self._repo.getcwd())
88 88
89 89 def _buildstatus(self, other, s, match, listignored, listclean,
90 90 listunknown):
91 91 """build a status with respect to another context"""
92 92 # Load earliest manifest first for caching reasons. More specifically,
93 93 # if you have revisions 1000 and 1001, 1001 is probably stored as a
94 94 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
95 95 # 1000 and cache it so that when you read 1001, we just need to apply a
96 96 # delta to what's in the cache. So that's one full reconstruction + one
97 97 # delta application.
98 98 if self.rev() is not None and self.rev() < other.rev():
99 99 self.manifest()
100 100 mf1 = other._manifestmatches(match, s)
101 101 mf2 = self._manifestmatches(match, s)
102 102
103 103 modified, added = [], []
104 104 removed = []
105 105 clean = []
106 106 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
107 107 deletedset = set(deleted)
108 108 d = mf1.diff(mf2, clean=listclean)
109 109 for fn, value in d.iteritems():
110 110 if fn in deletedset:
111 111 continue
112 112 if value is None:
113 113 clean.append(fn)
114 114 continue
115 115 (node1, flag1), (node2, flag2) = value
116 116 if node1 is None:
117 117 added.append(fn)
118 118 elif node2 is None:
119 119 removed.append(fn)
120 120 elif node2 != _newnode:
121 121 # The file was not a new file in mf2, so an entry
122 122 # from diff is really a difference.
123 123 modified.append(fn)
124 124 elif self[fn].cmp(other[fn]):
125 125 # node2 was newnode, but the working file doesn't
126 126 # match the one in mf1.
127 127 modified.append(fn)
128 128 else:
129 129 clean.append(fn)
130 130
131 131 if removed:
132 132 # need to filter files if they are already reported as removed
133 133 unknown = [fn for fn in unknown if fn not in mf1]
134 134 ignored = [fn for fn in ignored if fn not in mf1]
135 135 # if they're deleted, don't report them as removed
136 136 removed = [fn for fn in removed if fn not in deletedset]
137 137
138 138 return scmutil.status(modified, added, removed, deleted, unknown,
139 139 ignored, clean)
140 140
141 141 @propertycache
142 142 def substate(self):
143 143 return subrepo.state(self, self._repo.ui)
144 144
145 145 def subrev(self, subpath):
146 146 return self.substate[subpath][1]
147 147
148 148 def rev(self):
149 149 return self._rev
150 150 def node(self):
151 151 return self._node
152 152 def hex(self):
153 153 return hex(self.node())
154 154 def manifest(self):
155 155 return self._manifest
156 156 def phasestr(self):
157 157 return phases.phasenames[self.phase()]
158 158 def mutable(self):
159 159 return self.phase() > phases.public
160 160
161 161 def getfileset(self, expr):
162 162 return fileset.getfileset(self, expr)
163 163
164 164 def obsolete(self):
165 165 """True if the changeset is obsolete"""
166 166 return self.rev() in obsmod.getrevs(self._repo, 'obsolete')
167 167
168 168 def extinct(self):
169 169 """True if the changeset is extinct"""
170 170 return self.rev() in obsmod.getrevs(self._repo, 'extinct')
171 171
172 172 def unstable(self):
173 173 """True if the changeset is not obsolete but it's ancestor are"""
174 174 return self.rev() in obsmod.getrevs(self._repo, 'unstable')
175 175
176 176 def bumped(self):
177 177 """True if the changeset try to be a successor of a public changeset
178 178
179 179 Only non-public and non-obsolete changesets may be bumped.
180 180 """
181 181 return self.rev() in obsmod.getrevs(self._repo, 'bumped')
182 182
183 183 def divergent(self):
184 184 """Is a successors of a changeset with multiple possible successors set
185 185
186 186 Only non-public and non-obsolete changesets may be divergent.
187 187 """
188 188 return self.rev() in obsmod.getrevs(self._repo, 'divergent')
189 189
190 190 def troubled(self):
191 191 """True if the changeset is either unstable, bumped or divergent"""
192 192 return self.unstable() or self.bumped() or self.divergent()
193 193
194 194 def troubles(self):
195 195 """return the list of troubles affecting this changesets.
196 196
197 197 Troubles are returned as strings. possible values are:
198 198 - unstable,
199 199 - bumped,
200 200 - divergent.
201 201 """
202 202 troubles = []
203 203 if self.unstable():
204 204 troubles.append('unstable')
205 205 if self.bumped():
206 206 troubles.append('bumped')
207 207 if self.divergent():
208 208 troubles.append('divergent')
209 209 return troubles
210 210
211 211 def parents(self):
212 212 """return contexts for each parent changeset"""
213 213 return self._parents
214 214
215 215 def p1(self):
216 216 return self._parents[0]
217 217
218 218 def p2(self):
219 219 if len(self._parents) == 2:
220 220 return self._parents[1]
221 221 return changectx(self._repo, -1)
222 222
223 223 def _fileinfo(self, path):
224 224 if '_manifest' in self.__dict__:
225 225 try:
226 226 return self._manifest[path], self._manifest.flags(path)
227 227 except KeyError:
228 228 raise error.ManifestLookupError(self._node, path,
229 229 _('not found in manifest'))
230 230 if '_manifestdelta' in self.__dict__ or path in self.files():
231 231 if path in self._manifestdelta:
232 232 return (self._manifestdelta[path],
233 233 self._manifestdelta.flags(path))
234 234 node, flag = self._repo.manifest.find(self._changeset[0], path)
235 235 if not node:
236 236 raise error.ManifestLookupError(self._node, path,
237 237 _('not found in manifest'))
238 238
239 239 return node, flag
240 240
241 241 def filenode(self, path):
242 242 return self._fileinfo(path)[0]
243 243
244 244 def flags(self, path):
245 245 try:
246 246 return self._fileinfo(path)[1]
247 247 except error.LookupError:
248 248 return ''
249 249
250 250 def sub(self, path):
251 251 return subrepo.subrepo(self, path)
252 252
253 253 def match(self, pats=[], include=None, exclude=None, default='glob'):
254 254 r = self._repo
255 255 return matchmod.match(r.root, r.getcwd(), pats,
256 256 include, exclude, default,
257 257 auditor=r.auditor, ctx=self)
258 258
259 259 def diff(self, ctx2=None, match=None, **opts):
260 260 """Returns a diff generator for the given contexts and matcher"""
261 261 if ctx2 is None:
262 262 ctx2 = self.p1()
263 263 if ctx2 is not None:
264 264 ctx2 = self._repo[ctx2]
265 265 diffopts = patch.diffopts(self._repo.ui, opts)
266 266 return patch.diff(self._repo, ctx2, self, match=match, opts=diffopts)
267 267
268 268 @propertycache
269 269 def _dirs(self):
270 270 return scmutil.dirs(self._manifest)
271 271
272 272 def dirs(self):
273 273 return self._dirs
274 274
275 275 def dirty(self, missing=False, merge=True, branch=True):
276 276 return False
277 277
278 278 def status(self, other=None, match=None, listignored=False,
279 279 listclean=False, listunknown=False, listsubrepos=False):
280 280 """return status of files between two nodes or node and working
281 281 directory.
282 282
283 283 If other is None, compare this node with working directory.
284 284
285 285 returns (modified, added, removed, deleted, unknown, ignored, clean)
286 286 """
287 287
288 288 ctx1 = self
289 289 ctx2 = self._repo[other]
290 290
291 291 # This next code block is, admittedly, fragile logic that tests for
292 292 # reversing the contexts and wouldn't need to exist if it weren't for
293 293 # the fast (and common) code path of comparing the working directory
294 294 # with its first parent.
295 295 #
296 296 # What we're aiming for here is the ability to call:
297 297 #
298 298 # workingctx.status(parentctx)
299 299 #
300 300 # If we always built the manifest for each context and compared those,
301 301 # then we'd be done. But the special case of the above call means we
302 302 # just copy the manifest of the parent.
303 303 reversed = False
304 304 if (not isinstance(ctx1, changectx)
305 305 and isinstance(ctx2, changectx)):
306 306 reversed = True
307 307 ctx1, ctx2 = ctx2, ctx1
308 308
309 309 match = ctx2._matchstatus(ctx1, match)
310 310 r = scmutil.status([], [], [], [], [], [], [])
311 311 r = ctx2._buildstatus(ctx1, r, match, listignored, listclean,
312 312 listunknown)
313 313
314 314 if reversed:
315 315 # Reverse added and removed. Clear deleted, unknown and ignored as
316 316 # these make no sense to reverse.
317 317 r = scmutil.status(r.modified, r.removed, r.added, [], [], [],
318 318 r.clean)
319 319
320 320 if listsubrepos:
321 321 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
322 322 rev2 = ctx2.subrev(subpath)
323 323 try:
324 324 submatch = matchmod.narrowmatcher(subpath, match)
325 325 s = sub.status(rev2, match=submatch, ignored=listignored,
326 326 clean=listclean, unknown=listunknown,
327 327 listsubrepos=True)
328 328 for rfiles, sfiles in zip(r, s):
329 329 rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
330 330 except error.LookupError:
331 331 self._repo.ui.status(_("skipping missing "
332 332 "subrepository: %s\n") % subpath)
333 333
334 334 for l in r:
335 335 l.sort()
336 336
337 337 return r
338 338
339 339
340 340 def makememctx(repo, parents, text, user, date, branch, files, store,
341 341 editor=None):
342 342 def getfilectx(repo, memctx, path):
343 343 data, mode, copied = store.getfile(path)
344 344 if data is None:
345 345 return None
346 346 islink, isexec = mode
347 347 return memfilectx(repo, path, data, islink=islink, isexec=isexec,
348 348 copied=copied, memctx=memctx)
349 349 extra = {}
350 350 if branch:
351 351 extra['branch'] = encoding.fromlocal(branch)
352 352 ctx = memctx(repo, parents, text, files, getfilectx, user,
353 353 date, extra, editor)
354 354 return ctx
355 355
356 356 class changectx(basectx):
357 357 """A changecontext object makes access to data related to a particular
358 358 changeset convenient. It represents a read-only context already present in
359 359 the repo."""
360 360 def __init__(self, repo, changeid=''):
361 361 """changeid is a revision number, node, or tag"""
362 362
363 363 # since basectx.__new__ already took care of copying the object, we
364 364 # don't need to do anything in __init__, so we just exit here
365 365 if isinstance(changeid, basectx):
366 366 return
367 367
368 368 if changeid == '':
369 369 changeid = '.'
370 370 self._repo = repo
371 371
372 372 try:
373 373 if isinstance(changeid, int):
374 374 self._node = repo.changelog.node(changeid)
375 375 self._rev = changeid
376 376 return
377 377 if isinstance(changeid, long):
378 378 changeid = str(changeid)
379 379 if changeid == '.':
380 380 self._node = repo.dirstate.p1()
381 381 self._rev = repo.changelog.rev(self._node)
382 382 return
383 383 if changeid == 'null':
384 384 self._node = nullid
385 385 self._rev = nullrev
386 386 return
387 387 if changeid == 'tip':
388 388 self._node = repo.changelog.tip()
389 389 self._rev = repo.changelog.rev(self._node)
390 390 return
391 391 if len(changeid) == 20:
392 392 try:
393 393 self._node = changeid
394 394 self._rev = repo.changelog.rev(changeid)
395 395 return
396 396 except error.FilteredRepoLookupError:
397 397 raise
398 398 except LookupError:
399 399 pass
400 400
401 401 try:
402 402 r = int(changeid)
403 403 if str(r) != changeid:
404 404 raise ValueError
405 405 l = len(repo.changelog)
406 406 if r < 0:
407 407 r += l
408 408 if r < 0 or r >= l:
409 409 raise ValueError
410 410 self._rev = r
411 411 self._node = repo.changelog.node(r)
412 412 return
413 413 except error.FilteredIndexError:
414 414 raise
415 415 except (ValueError, OverflowError, IndexError):
416 416 pass
417 417
418 418 if len(changeid) == 40:
419 419 try:
420 420 self._node = bin(changeid)
421 421 self._rev = repo.changelog.rev(self._node)
422 422 return
423 423 except error.FilteredLookupError:
424 424 raise
425 425 except (TypeError, LookupError):
426 426 pass
427 427
428 428 # lookup bookmarks through the name interface
429 429 try:
430 430 self._node = repo.names.singlenode(repo, changeid)
431 431 self._rev = repo.changelog.rev(self._node)
432 432 return
433 433 except KeyError:
434 434 pass
435 435 except error.FilteredRepoLookupError:
436 436 raise
437 437 except error.RepoLookupError:
438 438 pass
439 439
440 440 self._node = repo.unfiltered().changelog._partialmatch(changeid)
441 441 if self._node is not None:
442 442 self._rev = repo.changelog.rev(self._node)
443 443 return
444 444
445 445 # lookup failed
446 446 # check if it might have come from damaged dirstate
447 447 #
448 448 # XXX we could avoid the unfiltered if we had a recognizable
449 449 # exception for filtered changeset access
450 450 if changeid in repo.unfiltered().dirstate.parents():
451 451 msg = _("working directory has unknown parent '%s'!")
452 452 raise error.Abort(msg % short(changeid))
453 453 try:
454 454 if len(changeid) == 20:
455 455 changeid = hex(changeid)
456 456 except TypeError:
457 457 pass
458 458 except (error.FilteredIndexError, error.FilteredLookupError,
459 459 error.FilteredRepoLookupError):
460 460 if repo.filtername == 'visible':
461 461 msg = _("hidden revision '%s'") % changeid
462 462 hint = _('use --hidden to access hidden revisions')
463 463 raise error.FilteredRepoLookupError(msg, hint=hint)
464 464 msg = _("filtered revision '%s' (not in '%s' subset)")
465 465 msg %= (changeid, repo.filtername)
466 466 raise error.FilteredRepoLookupError(msg)
467 467 except IndexError:
468 468 pass
469 469 raise error.RepoLookupError(
470 470 _("unknown revision '%s'") % changeid)
471 471
472 472 def __hash__(self):
473 473 try:
474 474 return hash(self._rev)
475 475 except AttributeError:
476 476 return id(self)
477 477
478 478 def __nonzero__(self):
479 479 return self._rev != nullrev
480 480
481 481 @propertycache
482 482 def _changeset(self):
483 483 return self._repo.changelog.read(self.rev())
484 484
485 485 @propertycache
486 486 def _manifest(self):
487 487 return self._repo.manifest.read(self._changeset[0])
488 488
489 489 @propertycache
490 490 def _manifestdelta(self):
491 491 return self._repo.manifest.readdelta(self._changeset[0])
492 492
493 493 @propertycache
494 494 def _parents(self):
495 495 p = self._repo.changelog.parentrevs(self._rev)
496 496 if p[1] == nullrev:
497 497 p = p[:-1]
498 498 return [changectx(self._repo, x) for x in p]
499 499
500 500 def changeset(self):
501 501 return self._changeset
502 502 def manifestnode(self):
503 503 return self._changeset[0]
504 504
505 505 def user(self):
506 506 return self._changeset[1]
507 507 def date(self):
508 508 return self._changeset[2]
509 509 def files(self):
510 510 return self._changeset[3]
511 511 def description(self):
512 512 return self._changeset[4]
513 513 def branch(self):
514 514 return encoding.tolocal(self._changeset[5].get("branch"))
515 515 def closesbranch(self):
516 516 return 'close' in self._changeset[5]
517 517 def extra(self):
518 518 return self._changeset[5]
519 519 def tags(self):
520 520 return self._repo.nodetags(self._node)
521 521 def bookmarks(self):
522 522 return self._repo.nodebookmarks(self._node)
523 523 def phase(self):
524 524 return self._repo._phasecache.phase(self._repo, self._rev)
525 525 def hidden(self):
526 526 return self._rev in repoview.filterrevs(self._repo, 'visible')
527 527
528 528 def children(self):
529 529 """return contexts for each child changeset"""
530 530 c = self._repo.changelog.children(self._node)
531 531 return [changectx(self._repo, x) for x in c]
532 532
533 533 def ancestors(self):
534 534 for a in self._repo.changelog.ancestors([self._rev]):
535 535 yield changectx(self._repo, a)
536 536
537 537 def descendants(self):
538 538 for d in self._repo.changelog.descendants([self._rev]):
539 539 yield changectx(self._repo, d)
540 540
541 541 def filectx(self, path, fileid=None, filelog=None):
542 542 """get a file context from this changeset"""
543 543 if fileid is None:
544 544 fileid = self.filenode(path)
545 545 return filectx(self._repo, path, fileid=fileid,
546 546 changectx=self, filelog=filelog)
547 547
548 548 def ancestor(self, c2, warn=False):
549 549 """return the "best" ancestor context of self and c2
550 550
551 551 If there are multiple candidates, it will show a message and check
552 552 merge.preferancestor configuration before falling back to the
553 553 revlog ancestor."""
554 554 # deal with workingctxs
555 555 n2 = c2._node
556 556 if n2 is None:
557 557 n2 = c2._parents[0]._node
558 558 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
559 559 if not cahs:
560 560 anc = nullid
561 561 elif len(cahs) == 1:
562 562 anc = cahs[0]
563 563 else:
564 564 for r in self._repo.ui.configlist('merge', 'preferancestor'):
565 565 try:
566 566 ctx = changectx(self._repo, r)
567 567 except error.RepoLookupError:
568 568 continue
569 569 anc = ctx.node()
570 570 if anc in cahs:
571 571 break
572 572 else:
573 573 anc = self._repo.changelog.ancestor(self._node, n2)
574 574 if warn:
575 575 self._repo.ui.status(
576 576 (_("note: using %s as ancestor of %s and %s\n") %
577 577 (short(anc), short(self._node), short(n2))) +
578 578 ''.join(_(" alternatively, use --config "
579 579 "merge.preferancestor=%s\n") %
580 580 short(n) for n in sorted(cahs) if n != anc))
581 581 return changectx(self._repo, anc)
582 582
583 583 def descendant(self, other):
584 584 """True if other is descendant of this changeset"""
585 585 return self._repo.changelog.descendant(self._rev, other._rev)
586 586
587 587 def walk(self, match):
588 588 fset = set(match.files())
589 589 # for dirstate.walk, files=['.'] means "walk the whole tree".
590 590 # follow that here, too
591 591 fset.discard('.')
592 592
593 593 # avoid the entire walk if we're only looking for specific files
594 594 if fset and not match.anypats():
595 595 if util.all([fn in self for fn in fset]):
596 596 for fn in sorted(fset):
597 597 if match(fn):
598 598 yield fn
599 599 raise StopIteration
600 600
601 601 for fn in self:
602 602 if fn in fset:
603 603 # specified pattern is the exact name
604 604 fset.remove(fn)
605 605 if match(fn):
606 606 yield fn
607 607 for fn in sorted(fset):
608 608 if fn in self._dirs:
609 609 # specified pattern is a directory
610 610 continue
611 611 match.bad(fn, _('no such file in rev %s') % self)
612 612
613 613 def matches(self, match):
614 614 return self.walk(match)
615 615
616 616 class basefilectx(object):
617 617 """A filecontext object represents the common logic for its children:
618 618 filectx: read-only access to a filerevision that is already present
619 619 in the repo,
620 620 workingfilectx: a filecontext that represents files from the working
621 621 directory,
622 622 memfilectx: a filecontext that represents files in-memory."""
623 623 def __new__(cls, repo, path, *args, **kwargs):
624 624 return super(basefilectx, cls).__new__(cls)
625 625
626 626 @propertycache
627 627 def _filelog(self):
628 628 return self._repo.file(self._path)
629 629
630 630 @propertycache
631 631 def _changeid(self):
632 632 if '_changeid' in self.__dict__:
633 633 return self._changeid
634 634 elif '_changectx' in self.__dict__:
635 635 return self._changectx.rev()
636 636 else:
637 637 return self._filelog.linkrev(self._filerev)
638 638
639 639 @propertycache
640 640 def _filenode(self):
641 641 if '_fileid' in self.__dict__:
642 642 return self._filelog.lookup(self._fileid)
643 643 else:
644 644 return self._changectx.filenode(self._path)
645 645
646 646 @propertycache
647 647 def _filerev(self):
648 648 return self._filelog.rev(self._filenode)
649 649
650 650 @propertycache
651 651 def _repopath(self):
652 652 return self._path
653 653
654 654 def __nonzero__(self):
655 655 try:
656 656 self._filenode
657 657 return True
658 658 except error.LookupError:
659 659 # file is missing
660 660 return False
661 661
662 662 def __str__(self):
663 663 return "%s@%s" % (self.path(), self._changectx)
664 664
665 665 def __repr__(self):
666 666 return "<%s %s>" % (type(self).__name__, str(self))
667 667
668 668 def __hash__(self):
669 669 try:
670 670 return hash((self._path, self._filenode))
671 671 except AttributeError:
672 672 return id(self)
673 673
674 674 def __eq__(self, other):
675 675 try:
676 676 return (type(self) == type(other) and self._path == other._path
677 677 and self._filenode == other._filenode)
678 678 except AttributeError:
679 679 return False
680 680
681 681 def __ne__(self, other):
682 682 return not (self == other)
683 683
684 684 def filerev(self):
685 685 return self._filerev
686 686 def filenode(self):
687 687 return self._filenode
688 688 def flags(self):
689 689 return self._changectx.flags(self._path)
690 690 def filelog(self):
691 691 return self._filelog
692 692 def rev(self):
693 693 return self._changeid
694 694 def linkrev(self):
695 695 return self._filelog.linkrev(self._filerev)
696 696 def node(self):
697 697 return self._changectx.node()
698 698 def hex(self):
699 699 return self._changectx.hex()
700 700 def user(self):
701 701 return self._changectx.user()
702 702 def date(self):
703 703 return self._changectx.date()
704 704 def files(self):
705 705 return self._changectx.files()
706 706 def description(self):
707 707 return self._changectx.description()
708 708 def branch(self):
709 709 return self._changectx.branch()
710 710 def extra(self):
711 711 return self._changectx.extra()
712 712 def phase(self):
713 713 return self._changectx.phase()
714 714 def phasestr(self):
715 715 return self._changectx.phasestr()
716 716 def manifest(self):
717 717 return self._changectx.manifest()
718 718 def changectx(self):
719 719 return self._changectx
720 720
721 721 def path(self):
722 722 return self._path
723 723
724 724 def isbinary(self):
725 725 try:
726 726 return util.binary(self.data())
727 727 except IOError:
728 728 return False
729 729 def isexec(self):
730 730 return 'x' in self.flags()
731 731 def islink(self):
732 732 return 'l' in self.flags()
733 733
734 734 def cmp(self, fctx):
735 735 """compare with other file context
736 736
737 737 returns True if different than fctx.
738 738 """
739 739 if (fctx._filerev is None
740 740 and (self._repo._encodefilterpats
741 741 # if file data starts with '\1\n', empty metadata block is
742 742 # prepended, which adds 4 bytes to filelog.size().
743 743 or self.size() - 4 == fctx.size())
744 744 or self.size() == fctx.size()):
745 745 return self._filelog.cmp(self._filenode, fctx.data())
746 746
747 747 return True
748 748
749 749 def _adjustlinkrev(self, path, filelog, fnode, srcrev, inclusive=False):
750 750 """return the first ancestor of <srcrev> introducting <fnode>
751 751
752 752 If the linkrev of the file revision does not point to an ancestor of
753 753 srcrev, we'll walk down the ancestors until we find one introducing
754 754 this file revision.
755 755
756 756 :repo: a localrepository object (used to access changelog and manifest)
757 757 :path: the file path
758 758 :fnode: the nodeid of the file revision
759 759 :filelog: the filelog of this path
760 760 :srcrev: the changeset revision we search ancestors from
761 761 :inclusive: if true, the src revision will also be checked
762 762 """
763 763 repo = self._repo
764 764 cl = repo.unfiltered().changelog
765 765 ma = repo.manifest
766 766 # fetch the linkrev
767 767 fr = filelog.rev(fnode)
768 768 lkr = filelog.linkrev(fr)
769 # hack to reuse ancestor computation when searching for renames
770 memberanc = getattr(self, '_ancestrycontext', None)
771 iteranc = None
772 if memberanc is None:
773 memberanc = iteranc = cl.ancestors([srcrev], lkr,
774 inclusive=inclusive)
769 775 # check if this linkrev is an ancestor of srcrev
770 anc = cl.ancestors([srcrev], lkr, inclusive=inclusive)
771 if lkr not in anc:
772 for a in anc:
776 if lkr not in memberanc:
777 if iteranc is None:
778 iteranc = cl.ancestors([srcrev], lkr, inclusive=inclusive)
779 for a in iteranc:
773 780 ac = cl.read(a) # get changeset data (we avoid object creation)
774 781 if path in ac[3]: # checking the 'files' field.
775 782 # The file has been touched, check if the content is
776 783 # similar to the one we search for.
777 784 if fnode == ma.readfast(ac[0]).get(path):
778 785 return a
779 786 # In theory, we should never get out of that loop without a result.
780 787 # But if manifest uses a buggy file revision (not children of the
781 788 # one it replaces) we could. Such a buggy situation will likely
782 789 # result is crash somewhere else at to some point.
783 790 return lkr
784 791
785 792 def introrev(self):
786 793 """return the rev of the changeset which introduced this file revision
787 794
788 795 This method is different from linkrev because it take into account the
789 796 changeset the filectx was created from. It ensures the returned
790 797 revision is one of its ancestors. This prevents bugs from
791 798 'linkrev-shadowing' when a file revision is used by multiple
792 799 changesets.
793 800 """
794 801 lkr = self.linkrev()
795 802 attrs = vars(self)
796 803 noctx = not ('_changeid' in attrs or '_changectx' in attrs)
797 804 if noctx or self.rev() == lkr:
798 805 return self.linkrev()
799 806 return self._adjustlinkrev(self._path, self._filelog, self._filenode,
800 807 self.rev(), inclusive=True)
801 808
802 809 def parents(self):
803 810 _path = self._path
804 811 fl = self._filelog
805 812 parents = self._filelog.parents(self._filenode)
806 813 pl = [(_path, node, fl) for node in parents if node != nullid]
807 814
808 815 r = fl.renamed(self._filenode)
809 816 if r:
810 817 # - In the simple rename case, both parent are nullid, pl is empty.
811 818 # - In case of merge, only one of the parent is null id and should
812 819 # be replaced with the rename information. This parent is -always-
813 820 # the first one.
814 821 #
815 822 # As null id have alway been filtered out in the previous list
816 823 # comprehension, inserting to 0 will always result in "replacing
817 824 # first nullid parent with rename information.
818 825 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
819 826
820 827 ret = []
821 828 for path, fnode, l in pl:
822 829 if '_changeid' in vars(self) or '_changectx' in vars(self):
823 830 # If self is associated with a changeset (probably explicitly
824 831 # fed), ensure the created filectx is associated with a
825 832 # changeset that is an ancestor of self.changectx.
826 833 rev = self._adjustlinkrev(path, l, fnode, self.rev())
827 834 fctx = filectx(self._repo, path, fileid=fnode, filelog=l,
828 835 changeid=rev)
836 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
837
829 838 else:
830 839 fctx = filectx(self._repo, path, fileid=fnode, filelog=l)
831 840 ret.append(fctx)
832 841 return ret
833 842
834 843 def p1(self):
835 844 return self.parents()[0]
836 845
837 846 def p2(self):
838 847 p = self.parents()
839 848 if len(p) == 2:
840 849 return p[1]
841 850 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
842 851
843 852 def annotate(self, follow=False, linenumber=None, diffopts=None):
844 853 '''returns a list of tuples of (ctx, line) for each line
845 854 in the file, where ctx is the filectx of the node where
846 855 that line was last changed.
847 856 This returns tuples of ((ctx, linenumber), line) for each line,
848 857 if "linenumber" parameter is NOT "None".
849 858 In such tuples, linenumber means one at the first appearance
850 859 in the managed file.
851 860 To reduce annotation cost,
852 861 this returns fixed value(False is used) as linenumber,
853 862 if "linenumber" parameter is "False".'''
854 863
855 864 if linenumber is None:
856 865 def decorate(text, rev):
857 866 return ([rev] * len(text.splitlines()), text)
858 867 elif linenumber:
859 868 def decorate(text, rev):
860 869 size = len(text.splitlines())
861 870 return ([(rev, i) for i in xrange(1, size + 1)], text)
862 871 else:
863 872 def decorate(text, rev):
864 873 return ([(rev, False)] * len(text.splitlines()), text)
865 874
866 875 def pair(parent, child):
867 876 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts,
868 877 refine=True)
869 878 for (a1, a2, b1, b2), t in blocks:
870 879 # Changed blocks ('!') or blocks made only of blank lines ('~')
871 880 # belong to the child.
872 881 if t == '=':
873 882 child[0][b1:b2] = parent[0][a1:a2]
874 883 return child
875 884
876 885 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
877 886
878 887 def parents(f):
879 888 pl = f.parents()
880 889
881 890 # Don't return renamed parents if we aren't following.
882 891 if not follow:
883 892 pl = [p for p in pl if p.path() == f.path()]
884 893
885 894 # renamed filectx won't have a filelog yet, so set it
886 895 # from the cache to save time
887 896 for p in pl:
888 897 if not '_filelog' in p.__dict__:
889 898 p._filelog = getlog(p.path())
890 899
891 900 return pl
892 901
893 902 # use linkrev to find the first changeset where self appeared
894 903 base = self
895 904 introrev = self.introrev()
896 905 if self.rev() != introrev:
897 906 base = self.filectx(self.filenode(), changeid=introrev)
898 907
899 908 # This algorithm would prefer to be recursive, but Python is a
900 909 # bit recursion-hostile. Instead we do an iterative
901 910 # depth-first search.
902 911
903 912 visit = [base]
904 913 hist = {}
905 914 pcache = {}
906 915 needed = {base: 1}
907 916 while visit:
908 917 f = visit[-1]
909 918 pcached = f in pcache
910 919 if not pcached:
911 920 pcache[f] = parents(f)
912 921
913 922 ready = True
914 923 pl = pcache[f]
915 924 for p in pl:
916 925 if p not in hist:
917 926 ready = False
918 927 visit.append(p)
919 928 if not pcached:
920 929 needed[p] = needed.get(p, 0) + 1
921 930 if ready:
922 931 visit.pop()
923 932 reusable = f in hist
924 933 if reusable:
925 934 curr = hist[f]
926 935 else:
927 936 curr = decorate(f.data(), f)
928 937 for p in pl:
929 938 if not reusable:
930 939 curr = pair(hist[p], curr)
931 940 if needed[p] == 1:
932 941 del hist[p]
933 942 del needed[p]
934 943 else:
935 944 needed[p] -= 1
936 945
937 946 hist[f] = curr
938 947 pcache[f] = []
939 948
940 949 return zip(hist[base][0], hist[base][1].splitlines(True))
941 950
942 951 def ancestors(self, followfirst=False):
943 952 visit = {}
944 953 c = self
945 954 cut = followfirst and 1 or None
946 955 while True:
947 956 for parent in c.parents()[:cut]:
948 957 visit[(parent.rev(), parent.node())] = parent
949 958 if not visit:
950 959 break
951 960 c = visit.pop(max(visit))
952 961 yield c
953 962
954 963 class filectx(basefilectx):
955 964 """A filecontext object makes access to data related to a particular
956 965 filerevision convenient."""
957 966 def __init__(self, repo, path, changeid=None, fileid=None,
958 967 filelog=None, changectx=None):
959 968 """changeid can be a changeset revision, node, or tag.
960 969 fileid can be a file revision or node."""
961 970 self._repo = repo
962 971 self._path = path
963 972
964 973 assert (changeid is not None
965 974 or fileid is not None
966 975 or changectx is not None), \
967 976 ("bad args: changeid=%r, fileid=%r, changectx=%r"
968 977 % (changeid, fileid, changectx))
969 978
970 979 if filelog is not None:
971 980 self._filelog = filelog
972 981
973 982 if changeid is not None:
974 983 self._changeid = changeid
975 984 if changectx is not None:
976 985 self._changectx = changectx
977 986 if fileid is not None:
978 987 self._fileid = fileid
979 988
980 989 @propertycache
981 990 def _changectx(self):
982 991 try:
983 992 return changectx(self._repo, self._changeid)
984 993 except error.FilteredRepoLookupError:
985 994 # Linkrev may point to any revision in the repository. When the
986 995 # repository is filtered this may lead to `filectx` trying to build
987 996 # `changectx` for filtered revision. In such case we fallback to
988 997 # creating `changectx` on the unfiltered version of the reposition.
989 998 # This fallback should not be an issue because `changectx` from
990 999 # `filectx` are not used in complex operations that care about
991 1000 # filtering.
992 1001 #
993 1002 # This fallback is a cheap and dirty fix that prevent several
994 1003 # crashes. It does not ensure the behavior is correct. However the
995 1004 # behavior was not correct before filtering either and "incorrect
996 1005 # behavior" is seen as better as "crash"
997 1006 #
998 1007 # Linkrevs have several serious troubles with filtering that are
999 1008 # complicated to solve. Proper handling of the issue here should be
1000 1009 # considered when solving linkrev issue are on the table.
1001 1010 return changectx(self._repo.unfiltered(), self._changeid)
1002 1011
1003 1012 def filectx(self, fileid, changeid=None):
1004 1013 '''opens an arbitrary revision of the file without
1005 1014 opening a new filelog'''
1006 1015 return filectx(self._repo, self._path, fileid=fileid,
1007 1016 filelog=self._filelog, changeid=changeid)
1008 1017
1009 1018 def data(self):
1010 1019 try:
1011 1020 return self._filelog.read(self._filenode)
1012 1021 except error.CensoredNodeError:
1013 1022 if self._repo.ui.config("censor", "policy", "abort") == "ignore":
1014 1023 return ""
1015 1024 raise util.Abort(_("censored node: %s") % short(self._filenode),
1016 1025 hint=_("set censor.policy to ignore errors"))
1017 1026
1018 1027 def size(self):
1019 1028 return self._filelog.size(self._filerev)
1020 1029
1021 1030 def renamed(self):
1022 1031 """check if file was actually renamed in this changeset revision
1023 1032
1024 1033 If rename logged in file revision, we report copy for changeset only
1025 1034 if file revisions linkrev points back to the changeset in question
1026 1035 or both changeset parents contain different file revisions.
1027 1036 """
1028 1037
1029 1038 renamed = self._filelog.renamed(self._filenode)
1030 1039 if not renamed:
1031 1040 return renamed
1032 1041
1033 1042 if self.rev() == self.linkrev():
1034 1043 return renamed
1035 1044
1036 1045 name = self.path()
1037 1046 fnode = self._filenode
1038 1047 for p in self._changectx.parents():
1039 1048 try:
1040 1049 if fnode == p.filenode(name):
1041 1050 return None
1042 1051 except error.LookupError:
1043 1052 pass
1044 1053 return renamed
1045 1054
1046 1055 def children(self):
1047 1056 # hard for renames
1048 1057 c = self._filelog.children(self._filenode)
1049 1058 return [filectx(self._repo, self._path, fileid=x,
1050 1059 filelog=self._filelog) for x in c]
1051 1060
1052 1061 class committablectx(basectx):
1053 1062 """A committablectx object provides common functionality for a context that
1054 1063 wants the ability to commit, e.g. workingctx or memctx."""
1055 1064 def __init__(self, repo, text="", user=None, date=None, extra=None,
1056 1065 changes=None):
1057 1066 self._repo = repo
1058 1067 self._rev = None
1059 1068 self._node = None
1060 1069 self._text = text
1061 1070 if date:
1062 1071 self._date = util.parsedate(date)
1063 1072 if user:
1064 1073 self._user = user
1065 1074 if changes:
1066 1075 self._status = changes
1067 1076
1068 1077 self._extra = {}
1069 1078 if extra:
1070 1079 self._extra = extra.copy()
1071 1080 if 'branch' not in self._extra:
1072 1081 try:
1073 1082 branch = encoding.fromlocal(self._repo.dirstate.branch())
1074 1083 except UnicodeDecodeError:
1075 1084 raise util.Abort(_('branch name not in UTF-8!'))
1076 1085 self._extra['branch'] = branch
1077 1086 if self._extra['branch'] == '':
1078 1087 self._extra['branch'] = 'default'
1079 1088
1080 1089 def __str__(self):
1081 1090 return str(self._parents[0]) + "+"
1082 1091
1083 1092 def __nonzero__(self):
1084 1093 return True
1085 1094
1086 1095 def _buildflagfunc(self):
1087 1096 # Create a fallback function for getting file flags when the
1088 1097 # filesystem doesn't support them
1089 1098
1090 1099 copiesget = self._repo.dirstate.copies().get
1091 1100
1092 1101 if len(self._parents) < 2:
1093 1102 # when we have one parent, it's easy: copy from parent
1094 1103 man = self._parents[0].manifest()
1095 1104 def func(f):
1096 1105 f = copiesget(f, f)
1097 1106 return man.flags(f)
1098 1107 else:
1099 1108 # merges are tricky: we try to reconstruct the unstored
1100 1109 # result from the merge (issue1802)
1101 1110 p1, p2 = self._parents
1102 1111 pa = p1.ancestor(p2)
1103 1112 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1104 1113
1105 1114 def func(f):
1106 1115 f = copiesget(f, f) # may be wrong for merges with copies
1107 1116 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1108 1117 if fl1 == fl2:
1109 1118 return fl1
1110 1119 if fl1 == fla:
1111 1120 return fl2
1112 1121 if fl2 == fla:
1113 1122 return fl1
1114 1123 return '' # punt for conflicts
1115 1124
1116 1125 return func
1117 1126
1118 1127 @propertycache
1119 1128 def _flagfunc(self):
1120 1129 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1121 1130
1122 1131 @propertycache
1123 1132 def _manifest(self):
1124 1133 """generate a manifest corresponding to the values in self._status
1125 1134
1126 1135 This reuse the file nodeid from parent, but we append an extra letter
1127 1136 when modified. Modified files get an extra 'm' while added files get
1128 1137 an extra 'a'. This is used by manifests merge to see that files
1129 1138 are different and by update logic to avoid deleting newly added files.
1130 1139 """
1131 1140
1132 1141 man1 = self._parents[0].manifest()
1133 1142 man = man1.copy()
1134 1143 if len(self._parents) > 1:
1135 1144 man2 = self.p2().manifest()
1136 1145 def getman(f):
1137 1146 if f in man1:
1138 1147 return man1
1139 1148 return man2
1140 1149 else:
1141 1150 getman = lambda f: man1
1142 1151
1143 1152 copied = self._repo.dirstate.copies()
1144 1153 ff = self._flagfunc
1145 1154 for i, l in (("a", self._status.added), ("m", self._status.modified)):
1146 1155 for f in l:
1147 1156 orig = copied.get(f, f)
1148 1157 man[f] = getman(orig).get(orig, nullid) + i
1149 1158 try:
1150 1159 man.setflag(f, ff(f))
1151 1160 except OSError:
1152 1161 pass
1153 1162
1154 1163 for f in self._status.deleted + self._status.removed:
1155 1164 if f in man:
1156 1165 del man[f]
1157 1166
1158 1167 return man
1159 1168
1160 1169 @propertycache
1161 1170 def _status(self):
1162 1171 return self._repo.status()
1163 1172
1164 1173 @propertycache
1165 1174 def _user(self):
1166 1175 return self._repo.ui.username()
1167 1176
1168 1177 @propertycache
1169 1178 def _date(self):
1170 1179 return util.makedate()
1171 1180
1172 1181 def subrev(self, subpath):
1173 1182 return None
1174 1183
1175 1184 def user(self):
1176 1185 return self._user or self._repo.ui.username()
1177 1186 def date(self):
1178 1187 return self._date
1179 1188 def description(self):
1180 1189 return self._text
1181 1190 def files(self):
1182 1191 return sorted(self._status.modified + self._status.added +
1183 1192 self._status.removed)
1184 1193
1185 1194 def modified(self):
1186 1195 return self._status.modified
1187 1196 def added(self):
1188 1197 return self._status.added
1189 1198 def removed(self):
1190 1199 return self._status.removed
1191 1200 def deleted(self):
1192 1201 return self._status.deleted
1193 1202 def branch(self):
1194 1203 return encoding.tolocal(self._extra['branch'])
1195 1204 def closesbranch(self):
1196 1205 return 'close' in self._extra
1197 1206 def extra(self):
1198 1207 return self._extra
1199 1208
1200 1209 def tags(self):
1201 1210 t = []
1202 1211 for p in self.parents():
1203 1212 t.extend(p.tags())
1204 1213 return t
1205 1214
1206 1215 def bookmarks(self):
1207 1216 b = []
1208 1217 for p in self.parents():
1209 1218 b.extend(p.bookmarks())
1210 1219 return b
1211 1220
1212 1221 def phase(self):
1213 1222 phase = phases.draft # default phase to draft
1214 1223 for p in self.parents():
1215 1224 phase = max(phase, p.phase())
1216 1225 return phase
1217 1226
1218 1227 def hidden(self):
1219 1228 return False
1220 1229
1221 1230 def children(self):
1222 1231 return []
1223 1232
1224 1233 def flags(self, path):
1225 1234 if '_manifest' in self.__dict__:
1226 1235 try:
1227 1236 return self._manifest.flags(path)
1228 1237 except KeyError:
1229 1238 return ''
1230 1239
1231 1240 try:
1232 1241 return self._flagfunc(path)
1233 1242 except OSError:
1234 1243 return ''
1235 1244
1236 1245 def ancestor(self, c2):
1237 1246 """return the "best" ancestor context of self and c2"""
1238 1247 return self._parents[0].ancestor(c2) # punt on two parents for now
1239 1248
1240 1249 def walk(self, match):
1241 1250 return sorted(self._repo.dirstate.walk(match, sorted(self.substate),
1242 1251 True, False))
1243 1252
1244 1253 def matches(self, match):
1245 1254 return sorted(self._repo.dirstate.matches(match))
1246 1255
1247 1256 def ancestors(self):
1248 1257 for p in self._parents:
1249 1258 yield p
1250 1259 for a in self._repo.changelog.ancestors(
1251 1260 [p.rev() for p in self._parents]):
1252 1261 yield changectx(self._repo, a)
1253 1262
1254 1263 def markcommitted(self, node):
1255 1264 """Perform post-commit cleanup necessary after committing this ctx
1256 1265
1257 1266 Specifically, this updates backing stores this working context
1258 1267 wraps to reflect the fact that the changes reflected by this
1259 1268 workingctx have been committed. For example, it marks
1260 1269 modified and added files as normal in the dirstate.
1261 1270
1262 1271 """
1263 1272
1264 1273 self._repo.dirstate.beginparentchange()
1265 1274 for f in self.modified() + self.added():
1266 1275 self._repo.dirstate.normal(f)
1267 1276 for f in self.removed():
1268 1277 self._repo.dirstate.drop(f)
1269 1278 self._repo.dirstate.setparents(node)
1270 1279 self._repo.dirstate.endparentchange()
1271 1280
1272 1281 def dirs(self):
1273 1282 return self._repo.dirstate.dirs()
1274 1283
1275 1284 class workingctx(committablectx):
1276 1285 """A workingctx object makes access to data related to
1277 1286 the current working directory convenient.
1278 1287 date - any valid date string or (unixtime, offset), or None.
1279 1288 user - username string, or None.
1280 1289 extra - a dictionary of extra values, or None.
1281 1290 changes - a list of file lists as returned by localrepo.status()
1282 1291 or None to use the repository status.
1283 1292 """
1284 1293 def __init__(self, repo, text="", user=None, date=None, extra=None,
1285 1294 changes=None):
1286 1295 super(workingctx, self).__init__(repo, text, user, date, extra, changes)
1287 1296
1288 1297 def __iter__(self):
1289 1298 d = self._repo.dirstate
1290 1299 for f in d:
1291 1300 if d[f] != 'r':
1292 1301 yield f
1293 1302
1294 1303 def __contains__(self, key):
1295 1304 return self._repo.dirstate[key] not in "?r"
1296 1305
1297 1306 @propertycache
1298 1307 def _parents(self):
1299 1308 p = self._repo.dirstate.parents()
1300 1309 if p[1] == nullid:
1301 1310 p = p[:-1]
1302 1311 return [changectx(self._repo, x) for x in p]
1303 1312
1304 1313 def filectx(self, path, filelog=None):
1305 1314 """get a file context from the working directory"""
1306 1315 return workingfilectx(self._repo, path, workingctx=self,
1307 1316 filelog=filelog)
1308 1317
1309 1318 def dirty(self, missing=False, merge=True, branch=True):
1310 1319 "check whether a working directory is modified"
1311 1320 # check subrepos first
1312 1321 for s in sorted(self.substate):
1313 1322 if self.sub(s).dirty():
1314 1323 return True
1315 1324 # check current working dir
1316 1325 return ((merge and self.p2()) or
1317 1326 (branch and self.branch() != self.p1().branch()) or
1318 1327 self.modified() or self.added() or self.removed() or
1319 1328 (missing and self.deleted()))
1320 1329
1321 1330 def add(self, list, prefix=""):
1322 1331 join = lambda f: os.path.join(prefix, f)
1323 1332 wlock = self._repo.wlock()
1324 1333 ui, ds = self._repo.ui, self._repo.dirstate
1325 1334 try:
1326 1335 rejected = []
1327 1336 lstat = self._repo.wvfs.lstat
1328 1337 for f in list:
1329 1338 scmutil.checkportable(ui, join(f))
1330 1339 try:
1331 1340 st = lstat(f)
1332 1341 except OSError:
1333 1342 ui.warn(_("%s does not exist!\n") % join(f))
1334 1343 rejected.append(f)
1335 1344 continue
1336 1345 if st.st_size > 10000000:
1337 1346 ui.warn(_("%s: up to %d MB of RAM may be required "
1338 1347 "to manage this file\n"
1339 1348 "(use 'hg revert %s' to cancel the "
1340 1349 "pending addition)\n")
1341 1350 % (f, 3 * st.st_size // 1000000, join(f)))
1342 1351 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1343 1352 ui.warn(_("%s not added: only files and symlinks "
1344 1353 "supported currently\n") % join(f))
1345 1354 rejected.append(f)
1346 1355 elif ds[f] in 'amn':
1347 1356 ui.warn(_("%s already tracked!\n") % join(f))
1348 1357 elif ds[f] == 'r':
1349 1358 ds.normallookup(f)
1350 1359 else:
1351 1360 ds.add(f)
1352 1361 return rejected
1353 1362 finally:
1354 1363 wlock.release()
1355 1364
1356 1365 def forget(self, files, prefix=""):
1357 1366 join = lambda f: os.path.join(prefix, f)
1358 1367 wlock = self._repo.wlock()
1359 1368 try:
1360 1369 rejected = []
1361 1370 for f in files:
1362 1371 if f not in self._repo.dirstate:
1363 1372 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1364 1373 rejected.append(f)
1365 1374 elif self._repo.dirstate[f] != 'a':
1366 1375 self._repo.dirstate.remove(f)
1367 1376 else:
1368 1377 self._repo.dirstate.drop(f)
1369 1378 return rejected
1370 1379 finally:
1371 1380 wlock.release()
1372 1381
1373 1382 def undelete(self, list):
1374 1383 pctxs = self.parents()
1375 1384 wlock = self._repo.wlock()
1376 1385 try:
1377 1386 for f in list:
1378 1387 if self._repo.dirstate[f] != 'r':
1379 1388 self._repo.ui.warn(_("%s not removed!\n") % f)
1380 1389 else:
1381 1390 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1382 1391 t = fctx.data()
1383 1392 self._repo.wwrite(f, t, fctx.flags())
1384 1393 self._repo.dirstate.normal(f)
1385 1394 finally:
1386 1395 wlock.release()
1387 1396
1388 1397 def copy(self, source, dest):
1389 1398 try:
1390 1399 st = self._repo.wvfs.lstat(dest)
1391 1400 except OSError, err:
1392 1401 if err.errno != errno.ENOENT:
1393 1402 raise
1394 1403 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1395 1404 return
1396 1405 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1397 1406 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1398 1407 "symbolic link\n") % dest)
1399 1408 else:
1400 1409 wlock = self._repo.wlock()
1401 1410 try:
1402 1411 if self._repo.dirstate[dest] in '?':
1403 1412 self._repo.dirstate.add(dest)
1404 1413 elif self._repo.dirstate[dest] in 'r':
1405 1414 self._repo.dirstate.normallookup(dest)
1406 1415 self._repo.dirstate.copy(source, dest)
1407 1416 finally:
1408 1417 wlock.release()
1409 1418
1410 1419 def _filtersuspectsymlink(self, files):
1411 1420 if not files or self._repo.dirstate._checklink:
1412 1421 return files
1413 1422
1414 1423 # Symlink placeholders may get non-symlink-like contents
1415 1424 # via user error or dereferencing by NFS or Samba servers,
1416 1425 # so we filter out any placeholders that don't look like a
1417 1426 # symlink
1418 1427 sane = []
1419 1428 for f in files:
1420 1429 if self.flags(f) == 'l':
1421 1430 d = self[f].data()
1422 1431 if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
1423 1432 self._repo.ui.debug('ignoring suspect symlink placeholder'
1424 1433 ' "%s"\n' % f)
1425 1434 continue
1426 1435 sane.append(f)
1427 1436 return sane
1428 1437
1429 1438 def _checklookup(self, files):
1430 1439 # check for any possibly clean files
1431 1440 if not files:
1432 1441 return [], []
1433 1442
1434 1443 modified = []
1435 1444 fixup = []
1436 1445 pctx = self._parents[0]
1437 1446 # do a full compare of any files that might have changed
1438 1447 for f in sorted(files):
1439 1448 if (f not in pctx or self.flags(f) != pctx.flags(f)
1440 1449 or pctx[f].cmp(self[f])):
1441 1450 modified.append(f)
1442 1451 else:
1443 1452 fixup.append(f)
1444 1453
1445 1454 # update dirstate for files that are actually clean
1446 1455 if fixup:
1447 1456 try:
1448 1457 # updating the dirstate is optional
1449 1458 # so we don't wait on the lock
1450 1459 # wlock can invalidate the dirstate, so cache normal _after_
1451 1460 # taking the lock
1452 1461 wlock = self._repo.wlock(False)
1453 1462 normal = self._repo.dirstate.normal
1454 1463 try:
1455 1464 for f in fixup:
1456 1465 normal(f)
1457 1466 finally:
1458 1467 wlock.release()
1459 1468 except error.LockError:
1460 1469 pass
1461 1470 return modified, fixup
1462 1471
1463 1472 def _manifestmatches(self, match, s):
1464 1473 """Slow path for workingctx
1465 1474
1466 1475 The fast path is when we compare the working directory to its parent
1467 1476 which means this function is comparing with a non-parent; therefore we
1468 1477 need to build a manifest and return what matches.
1469 1478 """
1470 1479 mf = self._repo['.']._manifestmatches(match, s)
1471 1480 for f in s.modified + s.added:
1472 1481 mf[f] = _newnode
1473 1482 mf.setflag(f, self.flags(f))
1474 1483 for f in s.removed:
1475 1484 if f in mf:
1476 1485 del mf[f]
1477 1486 return mf
1478 1487
1479 1488 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1480 1489 unknown=False):
1481 1490 '''Gets the status from the dirstate -- internal use only.'''
1482 1491 listignored, listclean, listunknown = ignored, clean, unknown
1483 1492 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1484 1493 subrepos = []
1485 1494 if '.hgsub' in self:
1486 1495 subrepos = sorted(self.substate)
1487 1496 cmp, s = self._repo.dirstate.status(match, subrepos, listignored,
1488 1497 listclean, listunknown)
1489 1498
1490 1499 # check for any possibly clean files
1491 1500 if cmp:
1492 1501 modified2, fixup = self._checklookup(cmp)
1493 1502 s.modified.extend(modified2)
1494 1503
1495 1504 # update dirstate for files that are actually clean
1496 1505 if fixup and listclean:
1497 1506 s.clean.extend(fixup)
1498 1507
1499 1508 if match.always():
1500 1509 # cache for performance
1501 1510 if s.unknown or s.ignored or s.clean:
1502 1511 # "_status" is cached with list*=False in the normal route
1503 1512 self._status = scmutil.status(s.modified, s.added, s.removed,
1504 1513 s.deleted, [], [], [])
1505 1514 else:
1506 1515 self._status = s
1507 1516
1508 1517 return s
1509 1518
1510 1519 def _buildstatus(self, other, s, match, listignored, listclean,
1511 1520 listunknown):
1512 1521 """build a status with respect to another context
1513 1522
1514 1523 This includes logic for maintaining the fast path of status when
1515 1524 comparing the working directory against its parent, which is to skip
1516 1525 building a new manifest if self (working directory) is not comparing
1517 1526 against its parent (repo['.']).
1518 1527 """
1519 1528 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1520 1529 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1521 1530 # might have accidentally ended up with the entire contents of the file
1522 1531 # they are supposed to be linking to.
1523 1532 s.modified[:] = self._filtersuspectsymlink(s.modified)
1524 1533 if other != self._repo['.']:
1525 1534 s = super(workingctx, self)._buildstatus(other, s, match,
1526 1535 listignored, listclean,
1527 1536 listunknown)
1528 1537 return s
1529 1538
1530 1539 def _matchstatus(self, other, match):
1531 1540 """override the match method with a filter for directory patterns
1532 1541
1533 1542 We use inheritance to customize the match.bad method only in cases of
1534 1543 workingctx since it belongs only to the working directory when
1535 1544 comparing against the parent changeset.
1536 1545
1537 1546 If we aren't comparing against the working directory's parent, then we
1538 1547 just use the default match object sent to us.
1539 1548 """
1540 1549 superself = super(workingctx, self)
1541 1550 match = superself._matchstatus(other, match)
1542 1551 if other != self._repo['.']:
1543 1552 def bad(f, msg):
1544 1553 # 'f' may be a directory pattern from 'match.files()',
1545 1554 # so 'f not in ctx1' is not enough
1546 1555 if f not in other and f not in other.dirs():
1547 1556 self._repo.ui.warn('%s: %s\n' %
1548 1557 (self._repo.dirstate.pathto(f), msg))
1549 1558 match.bad = bad
1550 1559 return match
1551 1560
1552 1561 class committablefilectx(basefilectx):
1553 1562 """A committablefilectx provides common functionality for a file context
1554 1563 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
1555 1564 def __init__(self, repo, path, filelog=None, ctx=None):
1556 1565 self._repo = repo
1557 1566 self._path = path
1558 1567 self._changeid = None
1559 1568 self._filerev = self._filenode = None
1560 1569
1561 1570 if filelog is not None:
1562 1571 self._filelog = filelog
1563 1572 if ctx:
1564 1573 self._changectx = ctx
1565 1574
1566 1575 def __nonzero__(self):
1567 1576 return True
1568 1577
1569 1578 def parents(self):
1570 1579 '''return parent filectxs, following copies if necessary'''
1571 1580 def filenode(ctx, path):
1572 1581 return ctx._manifest.get(path, nullid)
1573 1582
1574 1583 path = self._path
1575 1584 fl = self._filelog
1576 1585 pcl = self._changectx._parents
1577 1586 renamed = self.renamed()
1578 1587
1579 1588 if renamed:
1580 1589 pl = [renamed + (None,)]
1581 1590 else:
1582 1591 pl = [(path, filenode(pcl[0], path), fl)]
1583 1592
1584 1593 for pc in pcl[1:]:
1585 1594 pl.append((path, filenode(pc, path), fl))
1586 1595
1587 1596 return [filectx(self._repo, p, fileid=n, filelog=l)
1588 1597 for p, n, l in pl if n != nullid]
1589 1598
1590 1599 def children(self):
1591 1600 return []
1592 1601
1593 1602 class workingfilectx(committablefilectx):
1594 1603 """A workingfilectx object makes access to data related to a particular
1595 1604 file in the working directory convenient."""
1596 1605 def __init__(self, repo, path, filelog=None, workingctx=None):
1597 1606 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
1598 1607
1599 1608 @propertycache
1600 1609 def _changectx(self):
1601 1610 return workingctx(self._repo)
1602 1611
1603 1612 def data(self):
1604 1613 return self._repo.wread(self._path)
1605 1614 def renamed(self):
1606 1615 rp = self._repo.dirstate.copied(self._path)
1607 1616 if not rp:
1608 1617 return None
1609 1618 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1610 1619
1611 1620 def size(self):
1612 1621 return self._repo.wvfs.lstat(self._path).st_size
1613 1622 def date(self):
1614 1623 t, tz = self._changectx.date()
1615 1624 try:
1616 1625 return (int(self._repo.wvfs.lstat(self._path).st_mtime), tz)
1617 1626 except OSError, err:
1618 1627 if err.errno != errno.ENOENT:
1619 1628 raise
1620 1629 return (t, tz)
1621 1630
1622 1631 def cmp(self, fctx):
1623 1632 """compare with other file context
1624 1633
1625 1634 returns True if different than fctx.
1626 1635 """
1627 1636 # fctx should be a filectx (not a workingfilectx)
1628 1637 # invert comparison to reuse the same code path
1629 1638 return fctx.cmp(self)
1630 1639
1631 1640 def remove(self, ignoremissing=False):
1632 1641 """wraps unlink for a repo's working directory"""
1633 1642 util.unlinkpath(self._repo.wjoin(self._path), ignoremissing)
1634 1643
1635 1644 def write(self, data, flags):
1636 1645 """wraps repo.wwrite"""
1637 1646 self._repo.wwrite(self._path, data, flags)
1638 1647
1639 1648 class workingcommitctx(workingctx):
1640 1649 """A workingcommitctx object makes access to data related to
1641 1650 the revision being committed convenient.
1642 1651
1643 1652 This hides changes in the working directory, if they aren't
1644 1653 committed in this context.
1645 1654 """
1646 1655 def __init__(self, repo, changes,
1647 1656 text="", user=None, date=None, extra=None):
1648 1657 super(workingctx, self).__init__(repo, text, user, date, extra,
1649 1658 changes)
1650 1659
1651 1660 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1652 1661 unknown=False):
1653 1662 """Return matched files only in ``self._status``
1654 1663
1655 1664 Uncommitted files appear "clean" via this context, even if
1656 1665 they aren't actually so in the working directory.
1657 1666 """
1658 1667 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1659 1668 if clean:
1660 1669 clean = [f for f in self._manifest if f not in self._changedset]
1661 1670 else:
1662 1671 clean = []
1663 1672 return scmutil.status([f for f in self._status.modified if match(f)],
1664 1673 [f for f in self._status.added if match(f)],
1665 1674 [f for f in self._status.removed if match(f)],
1666 1675 [], [], [], clean)
1667 1676
1668 1677 @propertycache
1669 1678 def _changedset(self):
1670 1679 """Return the set of files changed in this context
1671 1680 """
1672 1681 changed = set(self._status.modified)
1673 1682 changed.update(self._status.added)
1674 1683 changed.update(self._status.removed)
1675 1684 return changed
1676 1685
1677 1686 class memctx(committablectx):
1678 1687 """Use memctx to perform in-memory commits via localrepo.commitctx().
1679 1688
1680 1689 Revision information is supplied at initialization time while
1681 1690 related files data and is made available through a callback
1682 1691 mechanism. 'repo' is the current localrepo, 'parents' is a
1683 1692 sequence of two parent revisions identifiers (pass None for every
1684 1693 missing parent), 'text' is the commit message and 'files' lists
1685 1694 names of files touched by the revision (normalized and relative to
1686 1695 repository root).
1687 1696
1688 1697 filectxfn(repo, memctx, path) is a callable receiving the
1689 1698 repository, the current memctx object and the normalized path of
1690 1699 requested file, relative to repository root. It is fired by the
1691 1700 commit function for every file in 'files', but calls order is
1692 1701 undefined. If the file is available in the revision being
1693 1702 committed (updated or added), filectxfn returns a memfilectx
1694 1703 object. If the file was removed, filectxfn raises an
1695 1704 IOError. Moved files are represented by marking the source file
1696 1705 removed and the new file added with copy information (see
1697 1706 memfilectx).
1698 1707
1699 1708 user receives the committer name and defaults to current
1700 1709 repository username, date is the commit date in any format
1701 1710 supported by util.parsedate() and defaults to current date, extra
1702 1711 is a dictionary of metadata or is left empty.
1703 1712 """
1704 1713
1705 1714 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
1706 1715 # Extensions that need to retain compatibility across Mercurial 3.1 can use
1707 1716 # this field to determine what to do in filectxfn.
1708 1717 _returnnoneformissingfiles = True
1709 1718
1710 1719 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1711 1720 date=None, extra=None, editor=False):
1712 1721 super(memctx, self).__init__(repo, text, user, date, extra)
1713 1722 self._rev = None
1714 1723 self._node = None
1715 1724 parents = [(p or nullid) for p in parents]
1716 1725 p1, p2 = parents
1717 1726 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1718 1727 files = sorted(set(files))
1719 1728 self._files = files
1720 1729 self.substate = {}
1721 1730
1722 1731 # if store is not callable, wrap it in a function
1723 1732 if not callable(filectxfn):
1724 1733 def getfilectx(repo, memctx, path):
1725 1734 fctx = filectxfn[path]
1726 1735 # this is weird but apparently we only keep track of one parent
1727 1736 # (why not only store that instead of a tuple?)
1728 1737 copied = fctx.renamed()
1729 1738 if copied:
1730 1739 copied = copied[0]
1731 1740 return memfilectx(repo, path, fctx.data(),
1732 1741 islink=fctx.islink(), isexec=fctx.isexec(),
1733 1742 copied=copied, memctx=memctx)
1734 1743 self._filectxfn = getfilectx
1735 1744 else:
1736 1745 # "util.cachefunc" reduces invocation of possibly expensive
1737 1746 # "filectxfn" for performance (e.g. converting from another VCS)
1738 1747 self._filectxfn = util.cachefunc(filectxfn)
1739 1748
1740 1749 self._extra = extra and extra.copy() or {}
1741 1750 if self._extra.get('branch', '') == '':
1742 1751 self._extra['branch'] = 'default'
1743 1752
1744 1753 if editor:
1745 1754 self._text = editor(self._repo, self, [])
1746 1755 self._repo.savecommitmessage(self._text)
1747 1756
1748 1757 def filectx(self, path, filelog=None):
1749 1758 """get a file context from the working directory
1750 1759
1751 1760 Returns None if file doesn't exist and should be removed."""
1752 1761 return self._filectxfn(self._repo, self, path)
1753 1762
1754 1763 def commit(self):
1755 1764 """commit context to the repo"""
1756 1765 return self._repo.commitctx(self)
1757 1766
1758 1767 @propertycache
1759 1768 def _manifest(self):
1760 1769 """generate a manifest based on the return values of filectxfn"""
1761 1770
1762 1771 # keep this simple for now; just worry about p1
1763 1772 pctx = self._parents[0]
1764 1773 man = pctx.manifest().copy()
1765 1774
1766 1775 for f in self._status.modified:
1767 1776 p1node = nullid
1768 1777 p2node = nullid
1769 1778 p = pctx[f].parents() # if file isn't in pctx, check p2?
1770 1779 if len(p) > 0:
1771 1780 p1node = p[0].node()
1772 1781 if len(p) > 1:
1773 1782 p2node = p[1].node()
1774 1783 man[f] = revlog.hash(self[f].data(), p1node, p2node)
1775 1784
1776 1785 for f in self._status.added:
1777 1786 man[f] = revlog.hash(self[f].data(), nullid, nullid)
1778 1787
1779 1788 for f in self._status.removed:
1780 1789 if f in man:
1781 1790 del man[f]
1782 1791
1783 1792 return man
1784 1793
1785 1794 @propertycache
1786 1795 def _status(self):
1787 1796 """Calculate exact status from ``files`` specified at construction
1788 1797 """
1789 1798 man1 = self.p1().manifest()
1790 1799 p2 = self._parents[1]
1791 1800 # "1 < len(self._parents)" can't be used for checking
1792 1801 # existence of the 2nd parent, because "memctx._parents" is
1793 1802 # explicitly initialized by the list, of which length is 2.
1794 1803 if p2.node() != nullid:
1795 1804 man2 = p2.manifest()
1796 1805 managing = lambda f: f in man1 or f in man2
1797 1806 else:
1798 1807 managing = lambda f: f in man1
1799 1808
1800 1809 modified, added, removed = [], [], []
1801 1810 for f in self._files:
1802 1811 if not managing(f):
1803 1812 added.append(f)
1804 1813 elif self[f]:
1805 1814 modified.append(f)
1806 1815 else:
1807 1816 removed.append(f)
1808 1817
1809 1818 return scmutil.status(modified, added, removed, [], [], [], [])
1810 1819
1811 1820 class memfilectx(committablefilectx):
1812 1821 """memfilectx represents an in-memory file to commit.
1813 1822
1814 1823 See memctx and committablefilectx for more details.
1815 1824 """
1816 1825 def __init__(self, repo, path, data, islink=False,
1817 1826 isexec=False, copied=None, memctx=None):
1818 1827 """
1819 1828 path is the normalized file path relative to repository root.
1820 1829 data is the file content as a string.
1821 1830 islink is True if the file is a symbolic link.
1822 1831 isexec is True if the file is executable.
1823 1832 copied is the source file path if current file was copied in the
1824 1833 revision being committed, or None."""
1825 1834 super(memfilectx, self).__init__(repo, path, None, memctx)
1826 1835 self._data = data
1827 1836 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1828 1837 self._copied = None
1829 1838 if copied:
1830 1839 self._copied = (copied, nullid)
1831 1840
1832 1841 def data(self):
1833 1842 return self._data
1834 1843 def size(self):
1835 1844 return len(self.data())
1836 1845 def flags(self):
1837 1846 return self._flags
1838 1847 def renamed(self):
1839 1848 return self._copied
1840 1849
1841 1850 def remove(self, ignoremissing=False):
1842 1851 """wraps unlink for a repo's working directory"""
1843 1852 # need to figure out what to do here
1844 1853 del self._changectx[self._path]
1845 1854
1846 1855 def write(self, data, flags):
1847 1856 """wraps repo.wwrite"""
1848 1857 self._data = data
@@ -1,469 +1,472
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 """
23 23 Find the last revision that needs to be checked to ensure that a full
24 24 transitive closure for file copies can be properly calculated.
25 25 Generally, this means finding the earliest revision number that's an
26 26 ancestor of a or b but not both, except when a or b is a direct descendent
27 27 of the other, in which case we can return the minimum revnum of a and b.
28 28 None if no such revision exists.
29 29 """
30 30
31 31 # basic idea:
32 32 # - mark a and b with different sides
33 33 # - if a parent's children are all on the same side, the parent is
34 34 # on that side, otherwise it is on no side
35 35 # - walk the graph in topological order with the help of a heap;
36 36 # - add unseen parents to side map
37 37 # - clear side of any parent that has children on different sides
38 38 # - track number of interesting revs that might still be on a side
39 39 # - track the lowest interesting rev seen
40 40 # - quit when interesting revs is zero
41 41
42 42 cl = repo.changelog
43 43 working = len(cl) # pseudo rev for the working directory
44 44 if a is None:
45 45 a = working
46 46 if b is None:
47 47 b = working
48 48
49 49 side = {a: -1, b: 1}
50 50 visit = [-a, -b]
51 51 heapq.heapify(visit)
52 52 interesting = len(visit)
53 53 hascommonancestor = False
54 54 limit = working
55 55
56 56 while interesting:
57 57 r = -heapq.heappop(visit)
58 58 if r == working:
59 59 parents = [cl.rev(p) for p in repo.dirstate.parents()]
60 60 else:
61 61 parents = cl.parentrevs(r)
62 62 for p in parents:
63 63 if p < 0:
64 64 continue
65 65 if p not in side:
66 66 # first time we see p; add it to visit
67 67 side[p] = side[r]
68 68 if side[p]:
69 69 interesting += 1
70 70 heapq.heappush(visit, -p)
71 71 elif side[p] and side[p] != side[r]:
72 72 # p was interesting but now we know better
73 73 side[p] = 0
74 74 interesting -= 1
75 75 hascommonancestor = True
76 76 if side[r]:
77 77 limit = r # lowest rev visited
78 78 interesting -= 1
79 79
80 80 if not hascommonancestor:
81 81 return None
82 82
83 83 # Consider the following flow (see test-commit-amend.t under issue4405):
84 84 # 1/ File 'a0' committed
85 85 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
86 86 # 3/ Move back to first commit
87 87 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
88 88 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
89 89 #
90 90 # During the amend in step five, we will be in this state:
91 91 #
92 92 # @ 3 temporary amend commit for a1-amend
93 93 # |
94 94 # o 2 a1-amend
95 95 # |
96 96 # | o 1 a1
97 97 # |/
98 98 # o 0 a0
99 99 #
100 100 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
101 101 # yet the filelog has the copy information in rev 1 and we will not look
102 102 # back far enough unless we also look at the a and b as candidates.
103 103 # This only occurs when a is a descendent of b or visa-versa.
104 104 return min(limit, a, b)
105 105
106 106 def _chain(src, dst, a, b):
107 107 '''chain two sets of copies a->b'''
108 108 t = a.copy()
109 109 for k, v in b.iteritems():
110 110 if v in t:
111 111 # found a chain
112 112 if t[v] != k:
113 113 # file wasn't renamed back to itself
114 114 t[k] = t[v]
115 115 if v not in dst:
116 116 # chain was a rename, not a copy
117 117 del t[v]
118 118 if v in src:
119 119 # file is a copy of an existing file
120 120 t[k] = v
121 121
122 122 # remove criss-crossed copies
123 123 for k, v in t.items():
124 124 if k in src and v in dst:
125 125 del t[k]
126 126
127 127 return t
128 128
129 129 def _tracefile(fctx, am, limit=-1):
130 130 '''return file context that is the ancestor of fctx present in ancestor
131 131 manifest am, stopping after the first ancestor lower than limit'''
132 132
133 133 for f in fctx.ancestors():
134 134 if am.get(f.path(), None) == f.filenode():
135 135 return f
136 136 if f.rev() < limit:
137 137 return None
138 138
139 139 def _dirstatecopies(d):
140 140 ds = d._repo.dirstate
141 141 c = ds.copies().copy()
142 142 for k in c.keys():
143 143 if ds[k] not in 'anm':
144 144 del c[k]
145 145 return c
146 146
147 147 def _forwardcopies(a, b):
148 148 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
149 149
150 150 # check for working copy
151 151 w = None
152 152 if b.rev() is None:
153 153 w = b
154 154 b = w.p1()
155 155 if a == b:
156 156 # short-circuit to avoid issues with merge states
157 157 return _dirstatecopies(w)
158 158
159 159 # files might have to be traced back to the fctx parent of the last
160 160 # one-side-only changeset, but not further back than that
161 161 limit = _findlimit(a._repo, a.rev(), b.rev())
162 162 if limit is None:
163 163 limit = -1
164 164 am = a.manifest()
165 165
166 166 # find where new files came from
167 167 # we currently don't try to find where old files went, too expensive
168 168 # this means we can miss a case like 'hg rm b; hg cp a b'
169 169 cm = {}
170 170 missing = set(b.manifest().iterkeys())
171 171 missing.difference_update(a.manifest().iterkeys())
172 172
173 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
173 174 for f in missing:
174 ofctx = _tracefile(b[f], am, limit)
175 fctx = b[f]
176 fctx._ancestrycontext = ancestrycontext
177 ofctx = _tracefile(fctx, am, limit)
175 178 if ofctx:
176 179 cm[f] = ofctx.path()
177 180
178 181 # combine copies from dirstate if necessary
179 182 if w is not None:
180 183 cm = _chain(a, w, cm, _dirstatecopies(w))
181 184
182 185 return cm
183 186
184 187 def _backwardrenames(a, b):
185 188 # Even though we're not taking copies into account, 1:n rename situations
186 189 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
187 190 # arbitrarily pick one of the renames.
188 191 f = _forwardcopies(b, a)
189 192 r = {}
190 193 for k, v in sorted(f.iteritems()):
191 194 # remove copies
192 195 if v in a:
193 196 continue
194 197 r[v] = k
195 198 return r
196 199
197 200 def pathcopies(x, y):
198 201 '''find {dst@y: src@x} copy mapping for directed compare'''
199 202 if x == y or not x or not y:
200 203 return {}
201 204 a = y.ancestor(x)
202 205 if a == x:
203 206 return _forwardcopies(x, y)
204 207 if a == y:
205 208 return _backwardrenames(x, y)
206 209 return _chain(x, y, _backwardrenames(x, a), _forwardcopies(a, y))
207 210
208 211 def mergecopies(repo, c1, c2, ca):
209 212 """
210 213 Find moves and copies between context c1 and c2 that are relevant
211 214 for merging.
212 215
213 216 Returns four dicts: "copy", "movewithdir", "diverge", and
214 217 "renamedelete".
215 218
216 219 "copy" is a mapping from destination name -> source name,
217 220 where source is in c1 and destination is in c2 or vice-versa.
218 221
219 222 "movewithdir" is a mapping from source name -> destination name,
220 223 where the file at source present in one context but not the other
221 224 needs to be moved to destination by the merge process, because the
222 225 other context moved the directory it is in.
223 226
224 227 "diverge" is a mapping of source name -> list of destination names
225 228 for divergent renames.
226 229
227 230 "renamedelete" is a mapping of source name -> list of destination
228 231 names for files deleted in c1 that were renamed in c2 or vice-versa.
229 232 """
230 233 # avoid silly behavior for update from empty dir
231 234 if not c1 or not c2 or c1 == c2:
232 235 return {}, {}, {}, {}
233 236
234 237 # avoid silly behavior for parent -> working dir
235 238 if c2.node() is None and c1.node() == repo.dirstate.p1():
236 239 return repo.dirstate.copies(), {}, {}, {}
237 240
238 241 limit = _findlimit(repo, c1.rev(), c2.rev())
239 242 if limit is None:
240 243 # no common ancestor, no copies
241 244 return {}, {}, {}, {}
242 245 m1 = c1.manifest()
243 246 m2 = c2.manifest()
244 247 ma = ca.manifest()
245 248
246 249 def makectx(f, n):
247 250 if len(n) != 20: # in a working context?
248 251 if c1.rev() is None:
249 252 return c1.filectx(f)
250 253 return c2.filectx(f)
251 254 return repo.filectx(f, fileid=n)
252 255
253 256 ctx = util.lrucachefunc(makectx)
254 257 copy = {}
255 258 movewithdir = {}
256 259 fullcopy = {}
257 260 diverge = {}
258 261
259 262 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
260 263
261 264 u1 = _nonoverlap(m1, m2, ma)
262 265 u2 = _nonoverlap(m2, m1, ma)
263 266
264 267 if u1:
265 268 repo.ui.debug(" unmatched files in local:\n %s\n"
266 269 % "\n ".join(u1))
267 270 if u2:
268 271 repo.ui.debug(" unmatched files in other:\n %s\n"
269 272 % "\n ".join(u2))
270 273
271 274 for f in u1:
272 275 checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy)
273 276
274 277 for f in u2:
275 278 checkcopies(ctx, f, m2, m1, ca, limit, diverge, copy, fullcopy)
276 279
277 280 renamedelete = {}
278 281 renamedelete2 = set()
279 282 diverge2 = set()
280 283 for of, fl in diverge.items():
281 284 if len(fl) == 1 or of in c1 or of in c2:
282 285 del diverge[of] # not actually divergent, or not a rename
283 286 if of not in c1 and of not in c2:
284 287 # renamed on one side, deleted on the other side, but filter
285 288 # out files that have been renamed and then deleted
286 289 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
287 290 renamedelete2.update(fl) # reverse map for below
288 291 else:
289 292 diverge2.update(fl) # reverse map for below
290 293
291 294 bothnew = sorted([d for d in m1 if d in m2 and d not in ma])
292 295 if bothnew:
293 296 repo.ui.debug(" unmatched files new in both:\n %s\n"
294 297 % "\n ".join(bothnew))
295 298 bothdiverge, _copy, _fullcopy = {}, {}, {}
296 299 for f in bothnew:
297 300 checkcopies(ctx, f, m1, m2, ca, limit, bothdiverge, _copy, _fullcopy)
298 301 checkcopies(ctx, f, m2, m1, ca, limit, bothdiverge, _copy, _fullcopy)
299 302 for of, fl in bothdiverge.items():
300 303 if len(fl) == 2 and fl[0] == fl[1]:
301 304 copy[fl[0]] = of # not actually divergent, just matching renames
302 305
303 306 if fullcopy and repo.ui.debugflag:
304 307 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
305 308 "% = renamed and deleted):\n")
306 309 for f in sorted(fullcopy):
307 310 note = ""
308 311 if f in copy:
309 312 note += "*"
310 313 if f in diverge2:
311 314 note += "!"
312 315 if f in renamedelete2:
313 316 note += "%"
314 317 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
315 318 note))
316 319 del diverge2
317 320
318 321 if not fullcopy:
319 322 return copy, movewithdir, diverge, renamedelete
320 323
321 324 repo.ui.debug(" checking for directory renames\n")
322 325
323 326 # generate a directory move map
324 327 d1, d2 = c1.dirs(), c2.dirs()
325 328 d1.addpath('/')
326 329 d2.addpath('/')
327 330 invalid = set()
328 331 dirmove = {}
329 332
330 333 # examine each file copy for a potential directory move, which is
331 334 # when all the files in a directory are moved to a new directory
332 335 for dst, src in fullcopy.iteritems():
333 336 dsrc, ddst = _dirname(src), _dirname(dst)
334 337 if dsrc in invalid:
335 338 # already seen to be uninteresting
336 339 continue
337 340 elif dsrc in d1 and ddst in d1:
338 341 # directory wasn't entirely moved locally
339 342 invalid.add(dsrc)
340 343 elif dsrc in d2 and ddst in d2:
341 344 # directory wasn't entirely moved remotely
342 345 invalid.add(dsrc)
343 346 elif dsrc in dirmove and dirmove[dsrc] != ddst:
344 347 # files from the same directory moved to two different places
345 348 invalid.add(dsrc)
346 349 else:
347 350 # looks good so far
348 351 dirmove[dsrc + "/"] = ddst + "/"
349 352
350 353 for i in invalid:
351 354 if i in dirmove:
352 355 del dirmove[i]
353 356 del d1, d2, invalid
354 357
355 358 if not dirmove:
356 359 return copy, movewithdir, diverge, renamedelete
357 360
358 361 for d in dirmove:
359 362 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
360 363 (d, dirmove[d]))
361 364
362 365 # check unaccounted nonoverlapping files against directory moves
363 366 for f in u1 + u2:
364 367 if f not in fullcopy:
365 368 for d in dirmove:
366 369 if f.startswith(d):
367 370 # new file added in a directory that was moved, move it
368 371 df = dirmove[d] + f[len(d):]
369 372 if df not in copy:
370 373 movewithdir[f] = df
371 374 repo.ui.debug((" pending file src: '%s' -> "
372 375 "dst: '%s'\n") % (f, df))
373 376 break
374 377
375 378 return copy, movewithdir, diverge, renamedelete
376 379
377 380 def checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy):
378 381 """
379 382 check possible copies of f from m1 to m2
380 383
381 384 ctx = function accepting (filename, node) that returns a filectx.
382 385 f = the filename to check
383 386 m1 = the source manifest
384 387 m2 = the destination manifest
385 388 ca = the changectx of the common ancestor
386 389 limit = the rev number to not search beyond
387 390 diverge = record all diverges in this dict
388 391 copy = record all non-divergent copies in this dict
389 392 fullcopy = record all copies in this dict
390 393 """
391 394
392 395 ma = ca.manifest()
393 396
394 397 def _related(f1, f2, limit):
395 398 # Walk back to common ancestor to see if the two files originate
396 399 # from the same file. Since workingfilectx's rev() is None it messes
397 400 # up the integer comparison logic, hence the pre-step check for
398 401 # None (f1 and f2 can only be workingfilectx's initially).
399 402
400 403 if f1 == f2:
401 404 return f1 # a match
402 405
403 406 g1, g2 = f1.ancestors(), f2.ancestors()
404 407 try:
405 408 f1r, f2r = f1.rev(), f2.rev()
406 409
407 410 if f1r is None:
408 411 f1 = g1.next()
409 412 if f2r is None:
410 413 f2 = g2.next()
411 414
412 415 while True:
413 416 f1r, f2r = f1.rev(), f2.rev()
414 417 if f1r > f2r:
415 418 f1 = g1.next()
416 419 elif f2r > f1r:
417 420 f2 = g2.next()
418 421 elif f1 == f2:
419 422 return f1 # a match
420 423 elif f1r == f2r or f1r < limit or f2r < limit:
421 424 return False # copy no longer relevant
422 425 except StopIteration:
423 426 return False
424 427
425 428 of = None
426 429 seen = set([f])
427 430 for oc in ctx(f, m1[f]).ancestors():
428 431 ocr = oc.rev()
429 432 of = oc.path()
430 433 if of in seen:
431 434 # check limit late - grab last rename before
432 435 if ocr < limit:
433 436 break
434 437 continue
435 438 seen.add(of)
436 439
437 440 fullcopy[f] = of # remember for dir rename detection
438 441 if of not in m2:
439 442 continue # no match, keep looking
440 443 if m2[of] == ma.get(of):
441 444 break # no merge needed, quit early
442 445 c2 = ctx(of, m2[of])
443 446 cr = _related(oc, c2, ca.rev())
444 447 if cr and (of == f or of == c2.path()): # non-divergent
445 448 copy[f] = of
446 449 of = None
447 450 break
448 451
449 452 if of in ma:
450 453 diverge.setdefault(of, []).append(f)
451 454
452 455 def duplicatecopies(repo, rev, fromrev, skiprev=None):
453 456 '''reproduce copies from fromrev to rev in the dirstate
454 457
455 458 If skiprev is specified, it's a revision that should be used to
456 459 filter copy records. Any copies that occur between fromrev and
457 460 skiprev will not be duplicated, even if they appear in the set of
458 461 copies between fromrev and rev.
459 462 '''
460 463 exclude = {}
461 464 if skiprev is not None:
462 465 exclude = pathcopies(repo[fromrev], repo[skiprev])
463 466 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
464 467 # copies.pathcopies returns backward renames, so dst might not
465 468 # actually be in the dirstate
466 469 if dst in exclude:
467 470 continue
468 471 if repo.dirstate[dst] in "nma":
469 472 repo.dirstate.copy(src, dst)
General Comments 0
You need to be logged in to leave comments. Login now