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