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