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