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