##// END OF EJS Templates
copies: re-include root directory in directory rename detection (issue3511)
Matt Mackall -
r17055:8b7cd9a9 default
parent child Browse files
Show More
@@ -1,1279 +1,1279
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
15 15 propertycache = util.propertycache
16 16
17 17 class changectx(object):
18 18 """A changecontext object makes access to data related to a particular
19 19 changeset convenient."""
20 20 def __init__(self, repo, changeid=''):
21 21 """changeid is a revision number, node, or tag"""
22 22 if changeid == '':
23 23 changeid = '.'
24 24 self._repo = repo
25 25
26 26 if isinstance(changeid, int):
27 27 self._rev = changeid
28 28 self._node = repo.changelog.node(changeid)
29 29 return
30 30 if isinstance(changeid, long):
31 31 changeid = str(changeid)
32 32 if changeid == '.':
33 33 self._node = repo.dirstate.p1()
34 34 self._rev = repo.changelog.rev(self._node)
35 35 return
36 36 if changeid == 'null':
37 37 self._node = nullid
38 38 self._rev = nullrev
39 39 return
40 40 if changeid == 'tip':
41 41 self._rev = len(repo.changelog) - 1
42 42 self._node = repo.changelog.node(self._rev)
43 43 return
44 44 if len(changeid) == 20:
45 45 try:
46 46 self._node = changeid
47 47 self._rev = repo.changelog.rev(changeid)
48 48 return
49 49 except LookupError:
50 50 pass
51 51
52 52 try:
53 53 r = int(changeid)
54 54 if str(r) != changeid:
55 55 raise ValueError
56 56 l = len(repo.changelog)
57 57 if r < 0:
58 58 r += l
59 59 if r < 0 or r >= l:
60 60 raise ValueError
61 61 self._rev = r
62 62 self._node = repo.changelog.node(r)
63 63 return
64 64 except (ValueError, OverflowError):
65 65 pass
66 66
67 67 if len(changeid) == 40:
68 68 try:
69 69 self._node = bin(changeid)
70 70 self._rev = repo.changelog.rev(self._node)
71 71 return
72 72 except (TypeError, LookupError):
73 73 pass
74 74
75 75 if changeid in repo._bookmarks:
76 76 self._node = repo._bookmarks[changeid]
77 77 self._rev = repo.changelog.rev(self._node)
78 78 return
79 79 if changeid in repo._tagscache.tags:
80 80 self._node = repo._tagscache.tags[changeid]
81 81 self._rev = repo.changelog.rev(self._node)
82 82 return
83 83 try:
84 84 self._node = repo.branchtip(changeid)
85 85 self._rev = repo.changelog.rev(self._node)
86 86 return
87 87 except error.RepoLookupError:
88 88 pass
89 89
90 90 self._node = repo.changelog._partialmatch(changeid)
91 91 if self._node is not None:
92 92 self._rev = repo.changelog.rev(self._node)
93 93 return
94 94
95 95 # lookup failed
96 96 # check if it might have come from damaged dirstate
97 97 if changeid in repo.dirstate.parents():
98 98 raise error.Abort(_("working directory has unknown parent '%s'!")
99 99 % short(changeid))
100 100 try:
101 101 if len(changeid) == 20:
102 102 changeid = hex(changeid)
103 103 except TypeError:
104 104 pass
105 105 raise error.RepoLookupError(
106 106 _("unknown revision '%s'") % changeid)
107 107
108 108 def __str__(self):
109 109 return short(self.node())
110 110
111 111 def __int__(self):
112 112 return self.rev()
113 113
114 114 def __repr__(self):
115 115 return "<changectx %s>" % str(self)
116 116
117 117 def __hash__(self):
118 118 try:
119 119 return hash(self._rev)
120 120 except AttributeError:
121 121 return id(self)
122 122
123 123 def __eq__(self, other):
124 124 try:
125 125 return self._rev == other._rev
126 126 except AttributeError:
127 127 return False
128 128
129 129 def __ne__(self, other):
130 130 return not (self == other)
131 131
132 132 def __nonzero__(self):
133 133 return self._rev != nullrev
134 134
135 135 @propertycache
136 136 def _changeset(self):
137 137 return self._repo.changelog.read(self.rev())
138 138
139 139 @propertycache
140 140 def _manifest(self):
141 141 return self._repo.manifest.read(self._changeset[0])
142 142
143 143 @propertycache
144 144 def _manifestdelta(self):
145 145 return self._repo.manifest.readdelta(self._changeset[0])
146 146
147 147 @propertycache
148 148 def _parents(self):
149 149 p = self._repo.changelog.parentrevs(self._rev)
150 150 if p[1] == nullrev:
151 151 p = p[:-1]
152 152 return [changectx(self._repo, x) for x in p]
153 153
154 154 @propertycache
155 155 def substate(self):
156 156 return subrepo.state(self, self._repo.ui)
157 157
158 158 def __contains__(self, key):
159 159 return key in self._manifest
160 160
161 161 def __getitem__(self, key):
162 162 return self.filectx(key)
163 163
164 164 def __iter__(self):
165 165 for f in sorted(self._manifest):
166 166 yield f
167 167
168 168 def changeset(self):
169 169 return self._changeset
170 170 def manifest(self):
171 171 return self._manifest
172 172 def manifestnode(self):
173 173 return self._changeset[0]
174 174
175 175 def rev(self):
176 176 return self._rev
177 177 def node(self):
178 178 return self._node
179 179 def hex(self):
180 180 return hex(self._node)
181 181 def user(self):
182 182 return self._changeset[1]
183 183 def date(self):
184 184 return self._changeset[2]
185 185 def files(self):
186 186 return self._changeset[3]
187 187 def description(self):
188 188 return self._changeset[4]
189 189 def branch(self):
190 190 return encoding.tolocal(self._changeset[5].get("branch"))
191 191 def closesbranch(self):
192 192 return 'close' in self._changeset[5]
193 193 def extra(self):
194 194 return self._changeset[5]
195 195 def tags(self):
196 196 return self._repo.nodetags(self._node)
197 197 def bookmarks(self):
198 198 return self._repo.nodebookmarks(self._node)
199 199 def phase(self):
200 200 return self._repo._phasecache.phase(self._repo, self._rev)
201 201 def phasestr(self):
202 202 return phases.phasenames[self.phase()]
203 203 def mutable(self):
204 204 return self.phase() > phases.public
205 205 def hidden(self):
206 206 return self._rev in self._repo.changelog.hiddenrevs
207 207
208 208 def parents(self):
209 209 """return contexts for each parent changeset"""
210 210 return self._parents
211 211
212 212 def p1(self):
213 213 return self._parents[0]
214 214
215 215 def p2(self):
216 216 if len(self._parents) == 2:
217 217 return self._parents[1]
218 218 return changectx(self._repo, -1)
219 219
220 220 def children(self):
221 221 """return contexts for each child changeset"""
222 222 c = self._repo.changelog.children(self._node)
223 223 return [changectx(self._repo, x) for x in c]
224 224
225 225 def ancestors(self):
226 226 for a in self._repo.changelog.ancestors([self._rev]):
227 227 yield changectx(self._repo, a)
228 228
229 229 def descendants(self):
230 230 for d in self._repo.changelog.descendants([self._rev]):
231 231 yield changectx(self._repo, d)
232 232
233 233 def _fileinfo(self, path):
234 234 if '_manifest' in self.__dict__:
235 235 try:
236 236 return self._manifest[path], self._manifest.flags(path)
237 237 except KeyError:
238 238 raise error.LookupError(self._node, path,
239 239 _('not found in manifest'))
240 240 if '_manifestdelta' in self.__dict__ or path in self.files():
241 241 if path in self._manifestdelta:
242 242 return (self._manifestdelta[path],
243 243 self._manifestdelta.flags(path))
244 244 node, flag = self._repo.manifest.find(self._changeset[0], path)
245 245 if not node:
246 246 raise error.LookupError(self._node, path,
247 247 _('not found in manifest'))
248 248
249 249 return node, flag
250 250
251 251 def filenode(self, path):
252 252 return self._fileinfo(path)[0]
253 253
254 254 def flags(self, path):
255 255 try:
256 256 return self._fileinfo(path)[1]
257 257 except error.LookupError:
258 258 return ''
259 259
260 260 def filectx(self, path, fileid=None, filelog=None):
261 261 """get a file context from this changeset"""
262 262 if fileid is None:
263 263 fileid = self.filenode(path)
264 264 return filectx(self._repo, path, fileid=fileid,
265 265 changectx=self, filelog=filelog)
266 266
267 267 def ancestor(self, c2):
268 268 """
269 269 return the ancestor context of self and c2
270 270 """
271 271 # deal with workingctxs
272 272 n2 = c2._node
273 273 if n2 is None:
274 274 n2 = c2._parents[0]._node
275 275 n = self._repo.changelog.ancestor(self._node, n2)
276 276 return changectx(self._repo, n)
277 277
278 278 def walk(self, match):
279 279 fset = set(match.files())
280 280 # for dirstate.walk, files=['.'] means "walk the whole tree".
281 281 # follow that here, too
282 282 fset.discard('.')
283 283 for fn in self:
284 284 if fn in fset:
285 285 # specified pattern is the exact name
286 286 fset.remove(fn)
287 287 if match(fn):
288 288 yield fn
289 289 for fn in sorted(fset):
290 290 if fn in self._dirs:
291 291 # specified pattern is a directory
292 292 continue
293 293 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
294 294 yield fn
295 295
296 296 def sub(self, path):
297 297 return subrepo.subrepo(self, path)
298 298
299 299 def match(self, pats=[], include=None, exclude=None, default='glob'):
300 300 r = self._repo
301 301 return matchmod.match(r.root, r.getcwd(), pats,
302 302 include, exclude, default,
303 303 auditor=r.auditor, ctx=self)
304 304
305 305 def diff(self, ctx2=None, match=None, **opts):
306 306 """Returns a diff generator for the given contexts and matcher"""
307 307 if ctx2 is None:
308 308 ctx2 = self.p1()
309 309 if ctx2 is not None and not isinstance(ctx2, changectx):
310 310 ctx2 = self._repo[ctx2]
311 311 diffopts = patch.diffopts(self._repo.ui, opts)
312 312 return patch.diff(self._repo, ctx2.node(), self.node(),
313 313 match=match, opts=diffopts)
314 314
315 315 @propertycache
316 316 def _dirs(self):
317 317 dirs = set()
318 318 for f in self._manifest:
319 319 pos = f.rfind('/')
320 320 while pos != -1:
321 321 f = f[:pos]
322 322 if f in dirs:
323 323 break # dirs already contains this and above
324 324 dirs.add(f)
325 325 pos = f.rfind('/')
326 326 return dirs
327 327
328 328 def dirs(self):
329 329 return self._dirs
330 330
331 331 class filectx(object):
332 332 """A filecontext object makes access to data related to a particular
333 333 filerevision convenient."""
334 334 def __init__(self, repo, path, changeid=None, fileid=None,
335 335 filelog=None, changectx=None):
336 336 """changeid can be a changeset revision, node, or tag.
337 337 fileid can be a file revision or node."""
338 338 self._repo = repo
339 339 self._path = path
340 340
341 341 assert (changeid is not None
342 342 or fileid is not None
343 343 or changectx is not None), \
344 344 ("bad args: changeid=%r, fileid=%r, changectx=%r"
345 345 % (changeid, fileid, changectx))
346 346
347 347 if filelog:
348 348 self._filelog = filelog
349 349
350 350 if changeid is not None:
351 351 self._changeid = changeid
352 352 if changectx is not None:
353 353 self._changectx = changectx
354 354 if fileid is not None:
355 355 self._fileid = fileid
356 356
357 357 @propertycache
358 358 def _changectx(self):
359 359 return changectx(self._repo, self._changeid)
360 360
361 361 @propertycache
362 362 def _filelog(self):
363 363 return self._repo.file(self._path)
364 364
365 365 @propertycache
366 366 def _changeid(self):
367 367 if '_changectx' in self.__dict__:
368 368 return self._changectx.rev()
369 369 else:
370 370 return self._filelog.linkrev(self._filerev)
371 371
372 372 @propertycache
373 373 def _filenode(self):
374 374 if '_fileid' in self.__dict__:
375 375 return self._filelog.lookup(self._fileid)
376 376 else:
377 377 return self._changectx.filenode(self._path)
378 378
379 379 @propertycache
380 380 def _filerev(self):
381 381 return self._filelog.rev(self._filenode)
382 382
383 383 @propertycache
384 384 def _repopath(self):
385 385 return self._path
386 386
387 387 def __nonzero__(self):
388 388 try:
389 389 self._filenode
390 390 return True
391 391 except error.LookupError:
392 392 # file is missing
393 393 return False
394 394
395 395 def __str__(self):
396 396 return "%s@%s" % (self.path(), short(self.node()))
397 397
398 398 def __repr__(self):
399 399 return "<filectx %s>" % str(self)
400 400
401 401 def __hash__(self):
402 402 try:
403 403 return hash((self._path, self._filenode))
404 404 except AttributeError:
405 405 return id(self)
406 406
407 407 def __eq__(self, other):
408 408 try:
409 409 return (self._path == other._path
410 410 and self._filenode == other._filenode)
411 411 except AttributeError:
412 412 return False
413 413
414 414 def __ne__(self, other):
415 415 return not (self == other)
416 416
417 417 def filectx(self, fileid):
418 418 '''opens an arbitrary revision of the file without
419 419 opening a new filelog'''
420 420 return filectx(self._repo, self._path, fileid=fileid,
421 421 filelog=self._filelog)
422 422
423 423 def filerev(self):
424 424 return self._filerev
425 425 def filenode(self):
426 426 return self._filenode
427 427 def flags(self):
428 428 return self._changectx.flags(self._path)
429 429 def filelog(self):
430 430 return self._filelog
431 431
432 432 def rev(self):
433 433 if '_changectx' in self.__dict__:
434 434 return self._changectx.rev()
435 435 if '_changeid' in self.__dict__:
436 436 return self._changectx.rev()
437 437 return self._filelog.linkrev(self._filerev)
438 438
439 439 def linkrev(self):
440 440 return self._filelog.linkrev(self._filerev)
441 441 def node(self):
442 442 return self._changectx.node()
443 443 def hex(self):
444 444 return hex(self.node())
445 445 def user(self):
446 446 return self._changectx.user()
447 447 def date(self):
448 448 return self._changectx.date()
449 449 def files(self):
450 450 return self._changectx.files()
451 451 def description(self):
452 452 return self._changectx.description()
453 453 def branch(self):
454 454 return self._changectx.branch()
455 455 def extra(self):
456 456 return self._changectx.extra()
457 457 def manifest(self):
458 458 return self._changectx.manifest()
459 459 def changectx(self):
460 460 return self._changectx
461 461
462 462 def data(self):
463 463 return self._filelog.read(self._filenode)
464 464 def path(self):
465 465 return self._path
466 466 def size(self):
467 467 return self._filelog.size(self._filerev)
468 468
469 469 def isbinary(self):
470 470 try:
471 471 return util.binary(self.data())
472 472 except IOError:
473 473 return False
474 474
475 475 def cmp(self, fctx):
476 476 """compare with other file context
477 477
478 478 returns True if different than fctx.
479 479 """
480 480 if (fctx._filerev is None
481 481 and (self._repo._encodefilterpats
482 482 # if file data starts with '\1\n', empty metadata block is
483 483 # prepended, which adds 4 bytes to filelog.size().
484 484 or self.size() - 4 == fctx.size())
485 485 or self.size() == fctx.size()):
486 486 return self._filelog.cmp(self._filenode, fctx.data())
487 487
488 488 return True
489 489
490 490 def renamed(self):
491 491 """check if file was actually renamed in this changeset revision
492 492
493 493 If rename logged in file revision, we report copy for changeset only
494 494 if file revisions linkrev points back to the changeset in question
495 495 or both changeset parents contain different file revisions.
496 496 """
497 497
498 498 renamed = self._filelog.renamed(self._filenode)
499 499 if not renamed:
500 500 return renamed
501 501
502 502 if self.rev() == self.linkrev():
503 503 return renamed
504 504
505 505 name = self.path()
506 506 fnode = self._filenode
507 507 for p in self._changectx.parents():
508 508 try:
509 509 if fnode == p.filenode(name):
510 510 return None
511 511 except error.LookupError:
512 512 pass
513 513 return renamed
514 514
515 515 def parents(self):
516 516 p = self._path
517 517 fl = self._filelog
518 518 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
519 519
520 520 r = self._filelog.renamed(self._filenode)
521 521 if r:
522 522 pl[0] = (r[0], r[1], None)
523 523
524 524 return [filectx(self._repo, p, fileid=n, filelog=l)
525 525 for p, n, l in pl if n != nullid]
526 526
527 527 def p1(self):
528 528 return self.parents()[0]
529 529
530 530 def p2(self):
531 531 p = self.parents()
532 532 if len(p) == 2:
533 533 return p[1]
534 534 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
535 535
536 536 def children(self):
537 537 # hard for renames
538 538 c = self._filelog.children(self._filenode)
539 539 return [filectx(self._repo, self._path, fileid=x,
540 540 filelog=self._filelog) for x in c]
541 541
542 542 def annotate(self, follow=False, linenumber=None, diffopts=None):
543 543 '''returns a list of tuples of (ctx, line) for each line
544 544 in the file, where ctx is the filectx of the node where
545 545 that line was last changed.
546 546 This returns tuples of ((ctx, linenumber), line) for each line,
547 547 if "linenumber" parameter is NOT "None".
548 548 In such tuples, linenumber means one at the first appearance
549 549 in the managed file.
550 550 To reduce annotation cost,
551 551 this returns fixed value(False is used) as linenumber,
552 552 if "linenumber" parameter is "False".'''
553 553
554 554 def decorate_compat(text, rev):
555 555 return ([rev] * len(text.splitlines()), text)
556 556
557 557 def without_linenumber(text, rev):
558 558 return ([(rev, False)] * len(text.splitlines()), text)
559 559
560 560 def with_linenumber(text, rev):
561 561 size = len(text.splitlines())
562 562 return ([(rev, i) for i in xrange(1, size + 1)], text)
563 563
564 564 decorate = (((linenumber is None) and decorate_compat) or
565 565 (linenumber and with_linenumber) or
566 566 without_linenumber)
567 567
568 568 def pair(parent, child):
569 569 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts,
570 570 refine=True)
571 571 for (a1, a2, b1, b2), t in blocks:
572 572 # Changed blocks ('!') or blocks made only of blank lines ('~')
573 573 # belong to the child.
574 574 if t == '=':
575 575 child[0][b1:b2] = parent[0][a1:a2]
576 576 return child
577 577
578 578 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
579 579 def getctx(path, fileid):
580 580 log = path == self._path and self._filelog or getlog(path)
581 581 return filectx(self._repo, path, fileid=fileid, filelog=log)
582 582 getctx = util.lrucachefunc(getctx)
583 583
584 584 def parents(f):
585 585 # we want to reuse filectx objects as much as possible
586 586 p = f._path
587 587 if f._filerev is None: # working dir
588 588 pl = [(n.path(), n.filerev()) for n in f.parents()]
589 589 else:
590 590 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
591 591
592 592 if follow:
593 593 r = f.renamed()
594 594 if r:
595 595 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
596 596
597 597 return [getctx(p, n) for p, n in pl if n != nullrev]
598 598
599 599 # use linkrev to find the first changeset where self appeared
600 600 if self.rev() != self.linkrev():
601 601 base = self.filectx(self.filerev())
602 602 else:
603 603 base = self
604 604
605 605 # This algorithm would prefer to be recursive, but Python is a
606 606 # bit recursion-hostile. Instead we do an iterative
607 607 # depth-first search.
608 608
609 609 visit = [base]
610 610 hist = {}
611 611 pcache = {}
612 612 needed = {base: 1}
613 613 while visit:
614 614 f = visit[-1]
615 615 if f not in pcache:
616 616 pcache[f] = parents(f)
617 617
618 618 ready = True
619 619 pl = pcache[f]
620 620 for p in pl:
621 621 if p not in hist:
622 622 ready = False
623 623 visit.append(p)
624 624 needed[p] = needed.get(p, 0) + 1
625 625 if ready:
626 626 visit.pop()
627 627 curr = decorate(f.data(), f)
628 628 for p in pl:
629 629 curr = pair(hist[p], curr)
630 630 if needed[p] == 1:
631 631 del hist[p]
632 632 else:
633 633 needed[p] -= 1
634 634
635 635 hist[f] = curr
636 636 pcache[f] = []
637 637
638 638 return zip(hist[base][0], hist[base][1].splitlines(True))
639 639
640 640 def ancestor(self, fc2, actx):
641 641 """
642 642 find the common ancestor file context, if any, of self, and fc2
643 643
644 644 actx must be the changectx of the common ancestor
645 645 of self's and fc2's respective changesets.
646 646 """
647 647
648 648 # the easy case: no (relevant) renames
649 649 if fc2.path() == self.path() and self.path() in actx:
650 650 return actx[self.path()]
651 651
652 652 # the next easiest cases: unambiguous predecessor (name trumps
653 653 # history)
654 654 if self.path() in actx and fc2.path() not in actx:
655 655 return actx[self.path()]
656 656 if fc2.path() in actx and self.path() not in actx:
657 657 return actx[fc2.path()]
658 658
659 659 # prime the ancestor cache for the working directory
660 660 acache = {}
661 661 for c in (self, fc2):
662 662 if c._filerev is None:
663 663 pl = [(n.path(), n.filenode()) for n in c.parents()]
664 664 acache[(c._path, None)] = pl
665 665
666 666 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
667 667 def parents(vertex):
668 668 if vertex in acache:
669 669 return acache[vertex]
670 670 f, n = vertex
671 671 if f not in flcache:
672 672 flcache[f] = self._repo.file(f)
673 673 fl = flcache[f]
674 674 pl = [(f, p) for p in fl.parents(n) if p != nullid]
675 675 re = fl.renamed(n)
676 676 if re:
677 677 pl.append(re)
678 678 acache[vertex] = pl
679 679 return pl
680 680
681 681 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
682 682 v = ancestor.ancestor(a, b, parents)
683 683 if v:
684 684 f, n = v
685 685 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
686 686
687 687 return None
688 688
689 689 def ancestors(self, followfirst=False):
690 690 visit = {}
691 691 c = self
692 692 cut = followfirst and 1 or None
693 693 while True:
694 694 for parent in c.parents()[:cut]:
695 695 visit[(parent.rev(), parent.node())] = parent
696 696 if not visit:
697 697 break
698 698 c = visit.pop(max(visit))
699 699 yield c
700 700
701 701 def copies(self, c2):
702 702 if not util.safehasattr(self, "_copycache"):
703 703 self._copycache = {}
704 704 sc2 = str(c2)
705 705 if sc2 not in self._copycache:
706 706 self._copycache[sc2] = copies.pathcopies(c2)
707 707 return self._copycache[sc2]
708 708
709 709 class workingctx(changectx):
710 710 """A workingctx object makes access to data related to
711 711 the current working directory convenient.
712 712 date - any valid date string or (unixtime, offset), or None.
713 713 user - username string, or None.
714 714 extra - a dictionary of extra values, or None.
715 715 changes - a list of file lists as returned by localrepo.status()
716 716 or None to use the repository status.
717 717 """
718 718 def __init__(self, repo, text="", user=None, date=None, extra=None,
719 719 changes=None):
720 720 self._repo = repo
721 721 self._rev = None
722 722 self._node = None
723 723 self._text = text
724 724 if date:
725 725 self._date = util.parsedate(date)
726 726 if user:
727 727 self._user = user
728 728 if changes:
729 729 self._status = list(changes[:4])
730 730 self._unknown = changes[4]
731 731 self._ignored = changes[5]
732 732 self._clean = changes[6]
733 733 else:
734 734 self._unknown = None
735 735 self._ignored = None
736 736 self._clean = None
737 737
738 738 self._extra = {}
739 739 if extra:
740 740 self._extra = extra.copy()
741 741 if 'branch' not in self._extra:
742 742 try:
743 743 branch = encoding.fromlocal(self._repo.dirstate.branch())
744 744 except UnicodeDecodeError:
745 745 raise util.Abort(_('branch name not in UTF-8!'))
746 746 self._extra['branch'] = branch
747 747 if self._extra['branch'] == '':
748 748 self._extra['branch'] = 'default'
749 749
750 750 def __str__(self):
751 751 return str(self._parents[0]) + "+"
752 752
753 753 def __repr__(self):
754 754 return "<workingctx %s>" % str(self)
755 755
756 756 def __nonzero__(self):
757 757 return True
758 758
759 759 def __contains__(self, key):
760 760 return self._repo.dirstate[key] not in "?r"
761 761
762 762 def _buildflagfunc(self):
763 763 # Create a fallback function for getting file flags when the
764 764 # filesystem doesn't support them
765 765
766 766 copiesget = self._repo.dirstate.copies().get
767 767
768 768 if len(self._parents) < 2:
769 769 # when we have one parent, it's easy: copy from parent
770 770 man = self._parents[0].manifest()
771 771 def func(f):
772 772 f = copiesget(f, f)
773 773 return man.flags(f)
774 774 else:
775 775 # merges are tricky: we try to reconstruct the unstored
776 776 # result from the merge (issue1802)
777 777 p1, p2 = self._parents
778 778 pa = p1.ancestor(p2)
779 779 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
780 780
781 781 def func(f):
782 782 f = copiesget(f, f) # may be wrong for merges with copies
783 783 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
784 784 if fl1 == fl2:
785 785 return fl1
786 786 if fl1 == fla:
787 787 return fl2
788 788 if fl2 == fla:
789 789 return fl1
790 790 return '' # punt for conflicts
791 791
792 792 return func
793 793
794 794 @propertycache
795 795 def _flagfunc(self):
796 796 return self._repo.dirstate.flagfunc(self._buildflagfunc)
797 797
798 798 @propertycache
799 799 def _manifest(self):
800 800 """generate a manifest corresponding to the working directory"""
801 801
802 802 man = self._parents[0].manifest().copy()
803 803 if len(self._parents) > 1:
804 804 man2 = self.p2().manifest()
805 805 def getman(f):
806 806 if f in man:
807 807 return man
808 808 return man2
809 809 else:
810 810 getman = lambda f: man
811 811
812 812 copied = self._repo.dirstate.copies()
813 813 ff = self._flagfunc
814 814 modified, added, removed, deleted = self._status
815 815 for i, l in (("a", added), ("m", modified)):
816 816 for f in l:
817 817 orig = copied.get(f, f)
818 818 man[f] = getman(orig).get(orig, nullid) + i
819 819 try:
820 820 man.set(f, ff(f))
821 821 except OSError:
822 822 pass
823 823
824 824 for f in deleted + removed:
825 825 if f in man:
826 826 del man[f]
827 827
828 828 return man
829 829
830 830 def __iter__(self):
831 831 d = self._repo.dirstate
832 832 for f in d:
833 833 if d[f] != 'r':
834 834 yield f
835 835
836 836 @propertycache
837 837 def _status(self):
838 838 return self._repo.status()[:4]
839 839
840 840 @propertycache
841 841 def _user(self):
842 842 return self._repo.ui.username()
843 843
844 844 @propertycache
845 845 def _date(self):
846 846 return util.makedate()
847 847
848 848 @propertycache
849 849 def _parents(self):
850 850 p = self._repo.dirstate.parents()
851 851 if p[1] == nullid:
852 852 p = p[:-1]
853 853 self._parents = [changectx(self._repo, x) for x in p]
854 854 return self._parents
855 855
856 856 def status(self, ignored=False, clean=False, unknown=False):
857 857 """Explicit status query
858 858 Unless this method is used to query the working copy status, the
859 859 _status property will implicitly read the status using its default
860 860 arguments."""
861 861 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
862 862 self._unknown = self._ignored = self._clean = None
863 863 if unknown:
864 864 self._unknown = stat[4]
865 865 if ignored:
866 866 self._ignored = stat[5]
867 867 if clean:
868 868 self._clean = stat[6]
869 869 self._status = stat[:4]
870 870 return stat
871 871
872 872 def manifest(self):
873 873 return self._manifest
874 874 def user(self):
875 875 return self._user or self._repo.ui.username()
876 876 def date(self):
877 877 return self._date
878 878 def description(self):
879 879 return self._text
880 880 def files(self):
881 881 return sorted(self._status[0] + self._status[1] + self._status[2])
882 882
883 883 def modified(self):
884 884 return self._status[0]
885 885 def added(self):
886 886 return self._status[1]
887 887 def removed(self):
888 888 return self._status[2]
889 889 def deleted(self):
890 890 return self._status[3]
891 891 def unknown(self):
892 892 assert self._unknown is not None # must call status first
893 893 return self._unknown
894 894 def ignored(self):
895 895 assert self._ignored is not None # must call status first
896 896 return self._ignored
897 897 def clean(self):
898 898 assert self._clean is not None # must call status first
899 899 return self._clean
900 900 def branch(self):
901 901 return encoding.tolocal(self._extra['branch'])
902 902 def closesbranch(self):
903 903 return 'close' in self._extra
904 904 def extra(self):
905 905 return self._extra
906 906
907 907 def tags(self):
908 908 t = []
909 909 for p in self.parents():
910 910 t.extend(p.tags())
911 911 return t
912 912
913 913 def bookmarks(self):
914 914 b = []
915 915 for p in self.parents():
916 916 b.extend(p.bookmarks())
917 917 return b
918 918
919 919 def phase(self):
920 920 phase = phases.draft # default phase to draft
921 921 for p in self.parents():
922 922 phase = max(phase, p.phase())
923 923 return phase
924 924
925 925 def hidden(self):
926 926 return False
927 927
928 928 def children(self):
929 929 return []
930 930
931 931 def flags(self, path):
932 932 if '_manifest' in self.__dict__:
933 933 try:
934 934 return self._manifest.flags(path)
935 935 except KeyError:
936 936 return ''
937 937
938 938 try:
939 939 return self._flagfunc(path)
940 940 except OSError:
941 941 return ''
942 942
943 943 def filectx(self, path, filelog=None):
944 944 """get a file context from the working directory"""
945 945 return workingfilectx(self._repo, path, workingctx=self,
946 946 filelog=filelog)
947 947
948 948 def ancestor(self, c2):
949 949 """return the ancestor context of self and c2"""
950 950 return self._parents[0].ancestor(c2) # punt on two parents for now
951 951
952 952 def walk(self, match):
953 953 return sorted(self._repo.dirstate.walk(match, self.substate.keys(),
954 954 True, False))
955 955
956 956 def dirty(self, missing=False, merge=True, branch=True):
957 957 "check whether a working directory is modified"
958 958 # check subrepos first
959 959 for s in self.substate:
960 960 if self.sub(s).dirty():
961 961 return True
962 962 # check current working dir
963 963 return ((merge and self.p2()) or
964 964 (branch and self.branch() != self.p1().branch()) or
965 965 self.modified() or self.added() or self.removed() or
966 966 (missing and self.deleted()))
967 967
968 968 def add(self, list, prefix=""):
969 969 join = lambda f: os.path.join(prefix, f)
970 970 wlock = self._repo.wlock()
971 971 ui, ds = self._repo.ui, self._repo.dirstate
972 972 try:
973 973 rejected = []
974 974 for f in list:
975 975 scmutil.checkportable(ui, join(f))
976 976 p = self._repo.wjoin(f)
977 977 try:
978 978 st = os.lstat(p)
979 979 except OSError:
980 980 ui.warn(_("%s does not exist!\n") % join(f))
981 981 rejected.append(f)
982 982 continue
983 983 if st.st_size > 10000000:
984 984 ui.warn(_("%s: up to %d MB of RAM may be required "
985 985 "to manage this file\n"
986 986 "(use 'hg revert %s' to cancel the "
987 987 "pending addition)\n")
988 988 % (f, 3 * st.st_size // 1000000, join(f)))
989 989 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
990 990 ui.warn(_("%s not added: only files and symlinks "
991 991 "supported currently\n") % join(f))
992 992 rejected.append(p)
993 993 elif ds[f] in 'amn':
994 994 ui.warn(_("%s already tracked!\n") % join(f))
995 995 elif ds[f] == 'r':
996 996 ds.normallookup(f)
997 997 else:
998 998 ds.add(f)
999 999 return rejected
1000 1000 finally:
1001 1001 wlock.release()
1002 1002
1003 1003 def forget(self, files, prefix=""):
1004 1004 join = lambda f: os.path.join(prefix, f)
1005 1005 wlock = self._repo.wlock()
1006 1006 try:
1007 1007 rejected = []
1008 1008 for f in files:
1009 1009 if f not in self._repo.dirstate:
1010 1010 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1011 1011 rejected.append(f)
1012 1012 elif self._repo.dirstate[f] != 'a':
1013 1013 self._repo.dirstate.remove(f)
1014 1014 else:
1015 1015 self._repo.dirstate.drop(f)
1016 1016 return rejected
1017 1017 finally:
1018 1018 wlock.release()
1019 1019
1020 1020 def ancestors(self):
1021 1021 for a in self._repo.changelog.ancestors(
1022 1022 [p.rev() for p in self._parents]):
1023 1023 yield changectx(self._repo, a)
1024 1024
1025 1025 def undelete(self, list):
1026 1026 pctxs = self.parents()
1027 1027 wlock = self._repo.wlock()
1028 1028 try:
1029 1029 for f in list:
1030 1030 if self._repo.dirstate[f] != 'r':
1031 1031 self._repo.ui.warn(_("%s not removed!\n") % f)
1032 1032 else:
1033 1033 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1034 1034 t = fctx.data()
1035 1035 self._repo.wwrite(f, t, fctx.flags())
1036 1036 self._repo.dirstate.normal(f)
1037 1037 finally:
1038 1038 wlock.release()
1039 1039
1040 1040 def copy(self, source, dest):
1041 1041 p = self._repo.wjoin(dest)
1042 1042 if not os.path.lexists(p):
1043 1043 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1044 1044 elif not (os.path.isfile(p) or os.path.islink(p)):
1045 1045 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1046 1046 "symbolic link\n") % dest)
1047 1047 else:
1048 1048 wlock = self._repo.wlock()
1049 1049 try:
1050 1050 if self._repo.dirstate[dest] in '?r':
1051 1051 self._repo.dirstate.add(dest)
1052 1052 self._repo.dirstate.copy(source, dest)
1053 1053 finally:
1054 1054 wlock.release()
1055 1055
1056 1056 def dirs(self):
1057 return self._repo.dirstate.dirs()
1057 return set(self._repo.dirstate.dirs())
1058 1058
1059 1059 class workingfilectx(filectx):
1060 1060 """A workingfilectx object makes access to data related to a particular
1061 1061 file in the working directory convenient."""
1062 1062 def __init__(self, repo, path, filelog=None, workingctx=None):
1063 1063 """changeid can be a changeset revision, node, or tag.
1064 1064 fileid can be a file revision or node."""
1065 1065 self._repo = repo
1066 1066 self._path = path
1067 1067 self._changeid = None
1068 1068 self._filerev = self._filenode = None
1069 1069
1070 1070 if filelog:
1071 1071 self._filelog = filelog
1072 1072 if workingctx:
1073 1073 self._changectx = workingctx
1074 1074
1075 1075 @propertycache
1076 1076 def _changectx(self):
1077 1077 return workingctx(self._repo)
1078 1078
1079 1079 def __nonzero__(self):
1080 1080 return True
1081 1081
1082 1082 def __str__(self):
1083 1083 return "%s@%s" % (self.path(), self._changectx)
1084 1084
1085 1085 def __repr__(self):
1086 1086 return "<workingfilectx %s>" % str(self)
1087 1087
1088 1088 def data(self):
1089 1089 return self._repo.wread(self._path)
1090 1090 def renamed(self):
1091 1091 rp = self._repo.dirstate.copied(self._path)
1092 1092 if not rp:
1093 1093 return None
1094 1094 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1095 1095
1096 1096 def parents(self):
1097 1097 '''return parent filectxs, following copies if necessary'''
1098 1098 def filenode(ctx, path):
1099 1099 return ctx._manifest.get(path, nullid)
1100 1100
1101 1101 path = self._path
1102 1102 fl = self._filelog
1103 1103 pcl = self._changectx._parents
1104 1104 renamed = self.renamed()
1105 1105
1106 1106 if renamed:
1107 1107 pl = [renamed + (None,)]
1108 1108 else:
1109 1109 pl = [(path, filenode(pcl[0], path), fl)]
1110 1110
1111 1111 for pc in pcl[1:]:
1112 1112 pl.append((path, filenode(pc, path), fl))
1113 1113
1114 1114 return [filectx(self._repo, p, fileid=n, filelog=l)
1115 1115 for p, n, l in pl if n != nullid]
1116 1116
1117 1117 def children(self):
1118 1118 return []
1119 1119
1120 1120 def size(self):
1121 1121 return os.lstat(self._repo.wjoin(self._path)).st_size
1122 1122 def date(self):
1123 1123 t, tz = self._changectx.date()
1124 1124 try:
1125 1125 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
1126 1126 except OSError, err:
1127 1127 if err.errno != errno.ENOENT:
1128 1128 raise
1129 1129 return (t, tz)
1130 1130
1131 1131 def cmp(self, fctx):
1132 1132 """compare with other file context
1133 1133
1134 1134 returns True if different than fctx.
1135 1135 """
1136 1136 # fctx should be a filectx (not a wfctx)
1137 1137 # invert comparison to reuse the same code path
1138 1138 return fctx.cmp(self)
1139 1139
1140 1140 class memctx(object):
1141 1141 """Use memctx to perform in-memory commits via localrepo.commitctx().
1142 1142
1143 1143 Revision information is supplied at initialization time while
1144 1144 related files data and is made available through a callback
1145 1145 mechanism. 'repo' is the current localrepo, 'parents' is a
1146 1146 sequence of two parent revisions identifiers (pass None for every
1147 1147 missing parent), 'text' is the commit message and 'files' lists
1148 1148 names of files touched by the revision (normalized and relative to
1149 1149 repository root).
1150 1150
1151 1151 filectxfn(repo, memctx, path) is a callable receiving the
1152 1152 repository, the current memctx object and the normalized path of
1153 1153 requested file, relative to repository root. It is fired by the
1154 1154 commit function for every file in 'files', but calls order is
1155 1155 undefined. If the file is available in the revision being
1156 1156 committed (updated or added), filectxfn returns a memfilectx
1157 1157 object. If the file was removed, filectxfn raises an
1158 1158 IOError. Moved files are represented by marking the source file
1159 1159 removed and the new file added with copy information (see
1160 1160 memfilectx).
1161 1161
1162 1162 user receives the committer name and defaults to current
1163 1163 repository username, date is the commit date in any format
1164 1164 supported by util.parsedate() and defaults to current date, extra
1165 1165 is a dictionary of metadata or is left empty.
1166 1166 """
1167 1167 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1168 1168 date=None, extra=None):
1169 1169 self._repo = repo
1170 1170 self._rev = None
1171 1171 self._node = None
1172 1172 self._text = text
1173 1173 self._date = date and util.parsedate(date) or util.makedate()
1174 1174 self._user = user
1175 1175 parents = [(p or nullid) for p in parents]
1176 1176 p1, p2 = parents
1177 1177 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1178 1178 files = sorted(set(files))
1179 1179 self._status = [files, [], [], [], []]
1180 1180 self._filectxfn = filectxfn
1181 1181
1182 1182 self._extra = extra and extra.copy() or {}
1183 1183 if self._extra.get('branch', '') == '':
1184 1184 self._extra['branch'] = 'default'
1185 1185
1186 1186 def __str__(self):
1187 1187 return str(self._parents[0]) + "+"
1188 1188
1189 1189 def __int__(self):
1190 1190 return self._rev
1191 1191
1192 1192 def __nonzero__(self):
1193 1193 return True
1194 1194
1195 1195 def __getitem__(self, key):
1196 1196 return self.filectx(key)
1197 1197
1198 1198 def p1(self):
1199 1199 return self._parents[0]
1200 1200 def p2(self):
1201 1201 return self._parents[1]
1202 1202
1203 1203 def user(self):
1204 1204 return self._user or self._repo.ui.username()
1205 1205 def date(self):
1206 1206 return self._date
1207 1207 def description(self):
1208 1208 return self._text
1209 1209 def files(self):
1210 1210 return self.modified()
1211 1211 def modified(self):
1212 1212 return self._status[0]
1213 1213 def added(self):
1214 1214 return self._status[1]
1215 1215 def removed(self):
1216 1216 return self._status[2]
1217 1217 def deleted(self):
1218 1218 return self._status[3]
1219 1219 def unknown(self):
1220 1220 return self._status[4]
1221 1221 def ignored(self):
1222 1222 return self._status[5]
1223 1223 def clean(self):
1224 1224 return self._status[6]
1225 1225 def branch(self):
1226 1226 return encoding.tolocal(self._extra['branch'])
1227 1227 def extra(self):
1228 1228 return self._extra
1229 1229 def flags(self, f):
1230 1230 return self[f].flags()
1231 1231
1232 1232 def parents(self):
1233 1233 """return contexts for each parent changeset"""
1234 1234 return self._parents
1235 1235
1236 1236 def filectx(self, path, filelog=None):
1237 1237 """get a file context from the working directory"""
1238 1238 return self._filectxfn(self._repo, self, path)
1239 1239
1240 1240 def commit(self):
1241 1241 """commit context to the repo"""
1242 1242 return self._repo.commitctx(self)
1243 1243
1244 1244 class memfilectx(object):
1245 1245 """memfilectx represents an in-memory file to commit.
1246 1246
1247 1247 See memctx for more details.
1248 1248 """
1249 1249 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1250 1250 """
1251 1251 path is the normalized file path relative to repository root.
1252 1252 data is the file content as a string.
1253 1253 islink is True if the file is a symbolic link.
1254 1254 isexec is True if the file is executable.
1255 1255 copied is the source file path if current file was copied in the
1256 1256 revision being committed, or None."""
1257 1257 self._path = path
1258 1258 self._data = data
1259 1259 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1260 1260 self._copied = None
1261 1261 if copied:
1262 1262 self._copied = (copied, nullid)
1263 1263
1264 1264 def __nonzero__(self):
1265 1265 return True
1266 1266 def __str__(self):
1267 1267 return "%s@%s" % (self.path(), self._changectx)
1268 1268 def path(self):
1269 1269 return self._path
1270 1270 def data(self):
1271 1271 return self._data
1272 1272 def flags(self):
1273 1273 return self._flags
1274 1274 def isexec(self):
1275 1275 return 'x' in self._flags
1276 1276 def islink(self):
1277 1277 return 'l' in self._flags
1278 1278 def renamed(self):
1279 1279 return self._copied
@@ -1,370 +1,372
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 for f in b:
137 137 if f not in a:
138 138 ofctx = _tracefile(b[f], a)
139 139 if ofctx:
140 140 cm[f] = ofctx.path()
141 141
142 142 # combine copies from dirstate if necessary
143 143 if w is not None:
144 144 cm = _chain(a, w, cm, _dirstatecopies(w))
145 145
146 146 return cm
147 147
148 148 def _backwardcopies(a, b):
149 149 # because the forward mapping is 1:n, we can lose renames here
150 150 # in particular, we find renames better than copies
151 151 f = _forwardcopies(b, a)
152 152 r = {}
153 153 for k, v in f.iteritems():
154 154 r[v] = k
155 155 return r
156 156
157 157 def pathcopies(x, y):
158 158 '''find {dst@y: src@x} copy mapping for directed compare'''
159 159 if x == y or not x or not y:
160 160 return {}
161 161 a = y.ancestor(x)
162 162 if a == x:
163 163 return _forwardcopies(x, y)
164 164 if a == y:
165 165 return _backwardcopies(x, y)
166 166 return _chain(x, y, _backwardcopies(x, a), _forwardcopies(a, y))
167 167
168 168 def mergecopies(repo, c1, c2, ca):
169 169 """
170 170 Find moves and copies between context c1 and c2 that are relevant
171 171 for merging.
172 172
173 173 Returns two dicts, "copy" and "diverge".
174 174
175 175 "copy" is a mapping from destination name -> source name,
176 176 where source is in c1 and destination is in c2 or vice-versa.
177 177
178 178 "diverge" is a mapping of source name -> list of destination names
179 179 for divergent renames.
180 180
181 181 "renamedelete" is a mapping of source name -> list of destination
182 182 names for files deleted in c1 that were renamed in c2 or vice-versa.
183 183 """
184 184 # avoid silly behavior for update from empty dir
185 185 if not c1 or not c2 or c1 == c2:
186 186 return {}, {}, {}
187 187
188 188 # avoid silly behavior for parent -> working dir
189 189 if c2.node() is None and c1.node() == repo.dirstate.p1():
190 190 return repo.dirstate.copies(), {}, {}
191 191
192 192 limit = _findlimit(repo, c1.rev(), c2.rev())
193 193 if limit is None:
194 194 # no common ancestor, no copies
195 195 return {}, {}, {}
196 196 m1 = c1.manifest()
197 197 m2 = c2.manifest()
198 198 ma = ca.manifest()
199 199
200 200 def makectx(f, n):
201 201 if len(n) != 20: # in a working context?
202 202 if c1.rev() is None:
203 203 return c1.filectx(f)
204 204 return c2.filectx(f)
205 205 return repo.filectx(f, fileid=n)
206 206
207 207 ctx = util.lrucachefunc(makectx)
208 208 copy = {}
209 209 fullcopy = {}
210 210 diverge = {}
211 211
212 212 def related(f1, f2, limit):
213 213 # Walk back to common ancestor to see if the two files originate
214 214 # from the same file. Since workingfilectx's rev() is None it messes
215 215 # up the integer comparison logic, hence the pre-step check for
216 216 # None (f1 and f2 can only be workingfilectx's initially).
217 217
218 218 if f1 == f2:
219 219 return f1 # a match
220 220
221 221 g1, g2 = f1.ancestors(), f2.ancestors()
222 222 try:
223 223 f1r, f2r = f1.rev(), f2.rev()
224 224
225 225 if f1r is None:
226 226 f1 = g1.next()
227 227 if f2r is None:
228 228 f2 = g2.next()
229 229
230 230 while True:
231 231 f1r, f2r = f1.rev(), f2.rev()
232 232 if f1r > f2r:
233 233 f1 = g1.next()
234 234 elif f2r > f1r:
235 235 f2 = g2.next()
236 236 elif f1 == f2:
237 237 return f1 # a match
238 238 elif f1r == f2r or f1r < limit or f2r < limit:
239 239 return False # copy no longer relevant
240 240 except StopIteration:
241 241 return False
242 242
243 243 def checkcopies(f, m1, m2):
244 244 '''check possible copies of f from m1 to m2'''
245 245 of = None
246 246 seen = set([f])
247 247 for oc in ctx(f, m1[f]).ancestors():
248 248 ocr = oc.rev()
249 249 of = oc.path()
250 250 if of in seen:
251 251 # check limit late - grab last rename before
252 252 if ocr < limit:
253 253 break
254 254 continue
255 255 seen.add(of)
256 256
257 257 fullcopy[f] = of # remember for dir rename detection
258 258 if of not in m2:
259 259 continue # no match, keep looking
260 260 if m2[of] == ma.get(of):
261 261 break # no merge needed, quit early
262 262 c2 = ctx(of, m2[of])
263 263 cr = related(oc, c2, ca.rev())
264 264 if cr and (of == f or of == c2.path()): # non-divergent
265 265 copy[f] = of
266 266 of = None
267 267 break
268 268
269 269 if of in ma:
270 270 diverge.setdefault(of, []).append(f)
271 271
272 272 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
273 273
274 274 u1 = _nonoverlap(m1, m2, ma)
275 275 u2 = _nonoverlap(m2, m1, ma)
276 276
277 277 if u1:
278 278 repo.ui.debug(" unmatched files in local:\n %s\n"
279 279 % "\n ".join(u1))
280 280 if u2:
281 281 repo.ui.debug(" unmatched files in other:\n %s\n"
282 282 % "\n ".join(u2))
283 283
284 284 for f in u1:
285 285 checkcopies(f, m1, m2)
286 286 for f in u2:
287 287 checkcopies(f, m2, m1)
288 288
289 289 renamedelete = {}
290 290 renamedelete2 = set()
291 291 diverge2 = set()
292 292 for of, fl in diverge.items():
293 293 if len(fl) == 1 or of in c1 or of in c2:
294 294 del diverge[of] # not actually divergent, or not a rename
295 295 if of not in c1 and of not in c2:
296 296 # renamed on one side, deleted on the other side, but filter
297 297 # out files that have been renamed and then deleted
298 298 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
299 299 renamedelete2.update(fl) # reverse map for below
300 300 else:
301 301 diverge2.update(fl) # reverse map for below
302 302
303 303 if fullcopy:
304 304 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
305 305 "% = renamed and deleted):\n")
306 306 for f in fullcopy:
307 307 note = ""
308 308 if f in copy:
309 309 note += "*"
310 310 if f in diverge2:
311 311 note += "!"
312 312 if f in renamedelete2:
313 313 note += "%"
314 314 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
315 315 del diverge2
316 316
317 317 if not fullcopy:
318 318 return copy, diverge, renamedelete
319 319
320 320 repo.ui.debug(" checking for directory renames\n")
321 321
322 322 # generate a directory move map
323 323 d1, d2 = c1.dirs(), c2.dirs()
324 invalid = set([""])
324 d1.add('')
325 d2.add('')
326 invalid = set()
325 327 dirmove = {}
326 328
327 329 # examine each file copy for a potential directory move, which is
328 330 # when all the files in a directory are moved to a new directory
329 331 for dst, src in fullcopy.iteritems():
330 332 dsrc, ddst = _dirname(src), _dirname(dst)
331 333 if dsrc in invalid:
332 334 # already seen to be uninteresting
333 335 continue
334 336 elif dsrc in d1 and ddst in d1:
335 337 # directory wasn't entirely moved locally
336 338 invalid.add(dsrc)
337 339 elif dsrc in d2 and ddst in d2:
338 340 # directory wasn't entirely moved remotely
339 341 invalid.add(dsrc)
340 342 elif dsrc in dirmove and dirmove[dsrc] != ddst:
341 343 # files from the same directory moved to two different places
342 344 invalid.add(dsrc)
343 345 else:
344 346 # looks good so far
345 347 dirmove[dsrc + "/"] = ddst + "/"
346 348
347 349 for i in invalid:
348 350 if i in dirmove:
349 351 del dirmove[i]
350 352 del d1, d2, invalid
351 353
352 354 if not dirmove:
353 355 return copy, diverge, renamedelete
354 356
355 357 for d in dirmove:
356 358 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
357 359
358 360 # check unaccounted nonoverlapping files against directory moves
359 361 for f in u1 + u2:
360 362 if f not in fullcopy:
361 363 for d in dirmove:
362 364 if f.startswith(d):
363 365 # new file added in a directory that was moved, move it
364 366 df = dirmove[d] + f[len(d):]
365 367 if df not in copy:
366 368 copy[f] = df
367 369 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
368 370 break
369 371
370 372 return copy, diverge, renamedelete
General Comments 0
You need to be logged in to leave comments. Login now