##// END OF EJS Templates
status: handle more node indicators in buildstatus...
Durham Goode -
r31258:c414e339 default
parent child Browse files
Show More
@@ -1,2116 +1,2117 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 __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 12 import re
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 addednodeid,
18 18 bin,
19 19 hex,
20 20 modifiednodeid,
21 21 newnodeid,
22 22 nullid,
23 23 nullrev,
24 24 short,
25 25 wdirid,
26 wdirnodes,
26 27 )
27 28 from . import (
28 29 encoding,
29 30 error,
30 31 fileset,
31 32 match as matchmod,
32 33 mdiff,
33 34 obsolete as obsmod,
34 35 patch,
35 36 phases,
36 37 repoview,
37 38 revlog,
38 39 scmutil,
39 40 subrepo,
40 41 util,
41 42 )
42 43
43 44 propertycache = util.propertycache
44 45
45 46 nonascii = re.compile(r'[^\x21-\x7f]').search
46 47
47 48 class basectx(object):
48 49 """A basectx object represents the common logic for its children:
49 50 changectx: read-only context that is already present in the repo,
50 51 workingctx: a context that represents the working directory and can
51 52 be committed,
52 53 memctx: a context that represents changes in-memory and can also
53 54 be committed."""
54 55 def __new__(cls, repo, changeid='', *args, **kwargs):
55 56 if isinstance(changeid, basectx):
56 57 return changeid
57 58
58 59 o = super(basectx, cls).__new__(cls)
59 60
60 61 o._repo = repo
61 62 o._rev = nullrev
62 63 o._node = nullid
63 64
64 65 return o
65 66
66 67 def __str__(self):
67 68 return short(self.node())
68 69
69 70 def __int__(self):
70 71 return self.rev()
71 72
72 73 def __repr__(self):
73 74 return "<%s %s>" % (type(self).__name__, str(self))
74 75
75 76 def __eq__(self, other):
76 77 try:
77 78 return type(self) == type(other) and self._rev == other._rev
78 79 except AttributeError:
79 80 return False
80 81
81 82 def __ne__(self, other):
82 83 return not (self == other)
83 84
84 85 def __contains__(self, key):
85 86 return key in self._manifest
86 87
87 88 def __getitem__(self, key):
88 89 return self.filectx(key)
89 90
90 91 def __iter__(self):
91 92 return iter(self._manifest)
92 93
93 94 def _manifestmatches(self, match, s):
94 95 """generate a new manifest filtered by the match argument
95 96
96 97 This method is for internal use only and mainly exists to provide an
97 98 object oriented way for other contexts to customize the manifest
98 99 generation.
99 100 """
100 101 return self.manifest().matches(match)
101 102
102 103 def _matchstatus(self, other, match):
103 104 """return match.always if match is none
104 105
105 106 This internal method provides a way for child objects to override the
106 107 match operator.
107 108 """
108 109 return match or matchmod.always(self._repo.root, self._repo.getcwd())
109 110
110 111 def _buildstatus(self, other, s, match, listignored, listclean,
111 112 listunknown):
112 113 """build a status with respect to another context"""
113 114 # Load earliest manifest first for caching reasons. More specifically,
114 115 # if you have revisions 1000 and 1001, 1001 is probably stored as a
115 116 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
116 117 # 1000 and cache it so that when you read 1001, we just need to apply a
117 118 # delta to what's in the cache. So that's one full reconstruction + one
118 119 # delta application.
119 120 if self.rev() is not None and self.rev() < other.rev():
120 121 self.manifest()
121 122 mf1 = other._manifestmatches(match, s)
122 123 mf2 = self._manifestmatches(match, s)
123 124
124 125 modified, added = [], []
125 126 removed = []
126 127 clean = []
127 128 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
128 129 deletedset = set(deleted)
129 130 d = mf1.diff(mf2, clean=listclean)
130 131 for fn, value in d.iteritems():
131 132 if fn in deletedset:
132 133 continue
133 134 if value is None:
134 135 clean.append(fn)
135 136 continue
136 137 (node1, flag1), (node2, flag2) = value
137 138 if node1 is None:
138 139 added.append(fn)
139 140 elif node2 is None:
140 141 removed.append(fn)
141 142 elif flag1 != flag2:
142 143 modified.append(fn)
143 elif node2 != newnodeid:
144 elif node2 not in wdirnodes:
144 145 # When comparing files between two commits, we save time by
145 146 # not comparing the file contents when the nodeids differ.
146 147 # Note that this means we incorrectly report a reverted change
147 148 # to a file as a modification.
148 149 modified.append(fn)
149 150 elif self[fn].cmp(other[fn]):
150 151 modified.append(fn)
151 152 else:
152 153 clean.append(fn)
153 154
154 155 if removed:
155 156 # need to filter files if they are already reported as removed
156 157 unknown = [fn for fn in unknown if fn not in mf1]
157 158 ignored = [fn for fn in ignored if fn not in mf1]
158 159 # if they're deleted, don't report them as removed
159 160 removed = [fn for fn in removed if fn not in deletedset]
160 161
161 162 return scmutil.status(modified, added, removed, deleted, unknown,
162 163 ignored, clean)
163 164
164 165 @propertycache
165 166 def substate(self):
166 167 return subrepo.state(self, self._repo.ui)
167 168
168 169 def subrev(self, subpath):
169 170 return self.substate[subpath][1]
170 171
171 172 def rev(self):
172 173 return self._rev
173 174 def node(self):
174 175 return self._node
175 176 def hex(self):
176 177 return hex(self.node())
177 178 def manifest(self):
178 179 return self._manifest
179 180 def manifestctx(self):
180 181 return self._manifestctx
181 182 def repo(self):
182 183 return self._repo
183 184 def phasestr(self):
184 185 return phases.phasenames[self.phase()]
185 186 def mutable(self):
186 187 return self.phase() > phases.public
187 188
188 189 def getfileset(self, expr):
189 190 return fileset.getfileset(self, expr)
190 191
191 192 def obsolete(self):
192 193 """True if the changeset is obsolete"""
193 194 return self.rev() in obsmod.getrevs(self._repo, 'obsolete')
194 195
195 196 def extinct(self):
196 197 """True if the changeset is extinct"""
197 198 return self.rev() in obsmod.getrevs(self._repo, 'extinct')
198 199
199 200 def unstable(self):
200 201 """True if the changeset is not obsolete but it's ancestor are"""
201 202 return self.rev() in obsmod.getrevs(self._repo, 'unstable')
202 203
203 204 def bumped(self):
204 205 """True if the changeset try to be a successor of a public changeset
205 206
206 207 Only non-public and non-obsolete changesets may be bumped.
207 208 """
208 209 return self.rev() in obsmod.getrevs(self._repo, 'bumped')
209 210
210 211 def divergent(self):
211 212 """Is a successors of a changeset with multiple possible successors set
212 213
213 214 Only non-public and non-obsolete changesets may be divergent.
214 215 """
215 216 return self.rev() in obsmod.getrevs(self._repo, 'divergent')
216 217
217 218 def troubled(self):
218 219 """True if the changeset is either unstable, bumped or divergent"""
219 220 return self.unstable() or self.bumped() or self.divergent()
220 221
221 222 def troubles(self):
222 223 """return the list of troubles affecting this changesets.
223 224
224 225 Troubles are returned as strings. possible values are:
225 226 - unstable,
226 227 - bumped,
227 228 - divergent.
228 229 """
229 230 troubles = []
230 231 if self.unstable():
231 232 troubles.append('unstable')
232 233 if self.bumped():
233 234 troubles.append('bumped')
234 235 if self.divergent():
235 236 troubles.append('divergent')
236 237 return troubles
237 238
238 239 def parents(self):
239 240 """return contexts for each parent changeset"""
240 241 return self._parents
241 242
242 243 def p1(self):
243 244 return self._parents[0]
244 245
245 246 def p2(self):
246 247 parents = self._parents
247 248 if len(parents) == 2:
248 249 return parents[1]
249 250 return changectx(self._repo, nullrev)
250 251
251 252 def _fileinfo(self, path):
252 253 if '_manifest' in self.__dict__:
253 254 try:
254 255 return self._manifest[path], self._manifest.flags(path)
255 256 except KeyError:
256 257 raise error.ManifestLookupError(self._node, path,
257 258 _('not found in manifest'))
258 259 if '_manifestdelta' in self.__dict__ or path in self.files():
259 260 if path in self._manifestdelta:
260 261 return (self._manifestdelta[path],
261 262 self._manifestdelta.flags(path))
262 263 mfl = self._repo.manifestlog
263 264 try:
264 265 node, flag = mfl[self._changeset.manifest].find(path)
265 266 except KeyError:
266 267 raise error.ManifestLookupError(self._node, path,
267 268 _('not found in manifest'))
268 269
269 270 return node, flag
270 271
271 272 def filenode(self, path):
272 273 return self._fileinfo(path)[0]
273 274
274 275 def flags(self, path):
275 276 try:
276 277 return self._fileinfo(path)[1]
277 278 except error.LookupError:
278 279 return ''
279 280
280 281 def sub(self, path, allowcreate=True):
281 282 '''return a subrepo for the stored revision of path, never wdir()'''
282 283 return subrepo.subrepo(self, path, allowcreate=allowcreate)
283 284
284 285 def nullsub(self, path, pctx):
285 286 return subrepo.nullsubrepo(self, path, pctx)
286 287
287 288 def workingsub(self, path):
288 289 '''return a subrepo for the stored revision, or wdir if this is a wdir
289 290 context.
290 291 '''
291 292 return subrepo.subrepo(self, path, allowwdir=True)
292 293
293 294 def match(self, pats=[], include=None, exclude=None, default='glob',
294 295 listsubrepos=False, badfn=None):
295 296 r = self._repo
296 297 return matchmod.match(r.root, r.getcwd(), pats,
297 298 include, exclude, default,
298 299 auditor=r.nofsauditor, ctx=self,
299 300 listsubrepos=listsubrepos, badfn=badfn)
300 301
301 302 def diff(self, ctx2=None, match=None, **opts):
302 303 """Returns a diff generator for the given contexts and matcher"""
303 304 if ctx2 is None:
304 305 ctx2 = self.p1()
305 306 if ctx2 is not None:
306 307 ctx2 = self._repo[ctx2]
307 308 diffopts = patch.diffopts(self._repo.ui, opts)
308 309 return patch.diff(self._repo, ctx2, self, match=match, opts=diffopts)
309 310
310 311 def dirs(self):
311 312 return self._manifest.dirs()
312 313
313 314 def hasdir(self, dir):
314 315 return self._manifest.hasdir(dir)
315 316
316 317 def dirty(self, missing=False, merge=True, branch=True):
317 318 return False
318 319
319 320 def status(self, other=None, match=None, listignored=False,
320 321 listclean=False, listunknown=False, listsubrepos=False):
321 322 """return status of files between two nodes or node and working
322 323 directory.
323 324
324 325 If other is None, compare this node with working directory.
325 326
326 327 returns (modified, added, removed, deleted, unknown, ignored, clean)
327 328 """
328 329
329 330 ctx1 = self
330 331 ctx2 = self._repo[other]
331 332
332 333 # This next code block is, admittedly, fragile logic that tests for
333 334 # reversing the contexts and wouldn't need to exist if it weren't for
334 335 # the fast (and common) code path of comparing the working directory
335 336 # with its first parent.
336 337 #
337 338 # What we're aiming for here is the ability to call:
338 339 #
339 340 # workingctx.status(parentctx)
340 341 #
341 342 # If we always built the manifest for each context and compared those,
342 343 # then we'd be done. But the special case of the above call means we
343 344 # just copy the manifest of the parent.
344 345 reversed = False
345 346 if (not isinstance(ctx1, changectx)
346 347 and isinstance(ctx2, changectx)):
347 348 reversed = True
348 349 ctx1, ctx2 = ctx2, ctx1
349 350
350 351 match = ctx2._matchstatus(ctx1, match)
351 352 r = scmutil.status([], [], [], [], [], [], [])
352 353 r = ctx2._buildstatus(ctx1, r, match, listignored, listclean,
353 354 listunknown)
354 355
355 356 if reversed:
356 357 # Reverse added and removed. Clear deleted, unknown and ignored as
357 358 # these make no sense to reverse.
358 359 r = scmutil.status(r.modified, r.removed, r.added, [], [], [],
359 360 r.clean)
360 361
361 362 if listsubrepos:
362 363 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
363 364 try:
364 365 rev2 = ctx2.subrev(subpath)
365 366 except KeyError:
366 367 # A subrepo that existed in node1 was deleted between
367 368 # node1 and node2 (inclusive). Thus, ctx2's substate
368 369 # won't contain that subpath. The best we can do ignore it.
369 370 rev2 = None
370 371 submatch = matchmod.subdirmatcher(subpath, match)
371 372 s = sub.status(rev2, match=submatch, ignored=listignored,
372 373 clean=listclean, unknown=listunknown,
373 374 listsubrepos=True)
374 375 for rfiles, sfiles in zip(r, s):
375 376 rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
376 377
377 378 for l in r:
378 379 l.sort()
379 380
380 381 return r
381 382
382 383
383 384 def makememctx(repo, parents, text, user, date, branch, files, store,
384 385 editor=None, extra=None):
385 386 def getfilectx(repo, memctx, path):
386 387 data, mode, copied = store.getfile(path)
387 388 if data is None:
388 389 return None
389 390 islink, isexec = mode
390 391 return memfilectx(repo, path, data, islink=islink, isexec=isexec,
391 392 copied=copied, memctx=memctx)
392 393 if extra is None:
393 394 extra = {}
394 395 if branch:
395 396 extra['branch'] = encoding.fromlocal(branch)
396 397 ctx = memctx(repo, parents, text, files, getfilectx, user,
397 398 date, extra, editor)
398 399 return ctx
399 400
400 401 class changectx(basectx):
401 402 """A changecontext object makes access to data related to a particular
402 403 changeset convenient. It represents a read-only context already present in
403 404 the repo."""
404 405 def __init__(self, repo, changeid=''):
405 406 """changeid is a revision number, node, or tag"""
406 407
407 408 # since basectx.__new__ already took care of copying the object, we
408 409 # don't need to do anything in __init__, so we just exit here
409 410 if isinstance(changeid, basectx):
410 411 return
411 412
412 413 if changeid == '':
413 414 changeid = '.'
414 415 self._repo = repo
415 416
416 417 try:
417 418 if isinstance(changeid, int):
418 419 self._node = repo.changelog.node(changeid)
419 420 self._rev = changeid
420 421 return
421 422 if isinstance(changeid, long):
422 423 changeid = str(changeid)
423 424 if changeid == 'null':
424 425 self._node = nullid
425 426 self._rev = nullrev
426 427 return
427 428 if changeid == 'tip':
428 429 self._node = repo.changelog.tip()
429 430 self._rev = repo.changelog.rev(self._node)
430 431 return
431 432 if changeid == '.' or changeid == repo.dirstate.p1():
432 433 # this is a hack to delay/avoid loading obsmarkers
433 434 # when we know that '.' won't be hidden
434 435 self._node = repo.dirstate.p1()
435 436 self._rev = repo.unfiltered().changelog.rev(self._node)
436 437 return
437 438 if len(changeid) == 20:
438 439 try:
439 440 self._node = changeid
440 441 self._rev = repo.changelog.rev(changeid)
441 442 return
442 443 except error.FilteredRepoLookupError:
443 444 raise
444 445 except LookupError:
445 446 pass
446 447
447 448 try:
448 449 r = int(changeid)
449 450 if str(r) != changeid:
450 451 raise ValueError
451 452 l = len(repo.changelog)
452 453 if r < 0:
453 454 r += l
454 455 if r < 0 or r >= l:
455 456 raise ValueError
456 457 self._rev = r
457 458 self._node = repo.changelog.node(r)
458 459 return
459 460 except error.FilteredIndexError:
460 461 raise
461 462 except (ValueError, OverflowError, IndexError):
462 463 pass
463 464
464 465 if len(changeid) == 40:
465 466 try:
466 467 self._node = bin(changeid)
467 468 self._rev = repo.changelog.rev(self._node)
468 469 return
469 470 except error.FilteredLookupError:
470 471 raise
471 472 except (TypeError, LookupError):
472 473 pass
473 474
474 475 # lookup bookmarks through the name interface
475 476 try:
476 477 self._node = repo.names.singlenode(repo, changeid)
477 478 self._rev = repo.changelog.rev(self._node)
478 479 return
479 480 except KeyError:
480 481 pass
481 482 except error.FilteredRepoLookupError:
482 483 raise
483 484 except error.RepoLookupError:
484 485 pass
485 486
486 487 self._node = repo.unfiltered().changelog._partialmatch(changeid)
487 488 if self._node is not None:
488 489 self._rev = repo.changelog.rev(self._node)
489 490 return
490 491
491 492 # lookup failed
492 493 # check if it might have come from damaged dirstate
493 494 #
494 495 # XXX we could avoid the unfiltered if we had a recognizable
495 496 # exception for filtered changeset access
496 497 if changeid in repo.unfiltered().dirstate.parents():
497 498 msg = _("working directory has unknown parent '%s'!")
498 499 raise error.Abort(msg % short(changeid))
499 500 try:
500 501 if len(changeid) == 20 and nonascii(changeid):
501 502 changeid = hex(changeid)
502 503 except TypeError:
503 504 pass
504 505 except (error.FilteredIndexError, error.FilteredLookupError,
505 506 error.FilteredRepoLookupError):
506 507 if repo.filtername.startswith('visible'):
507 508 msg = _("hidden revision '%s'") % changeid
508 509 hint = _('use --hidden to access hidden revisions')
509 510 raise error.FilteredRepoLookupError(msg, hint=hint)
510 511 msg = _("filtered revision '%s' (not in '%s' subset)")
511 512 msg %= (changeid, repo.filtername)
512 513 raise error.FilteredRepoLookupError(msg)
513 514 except IndexError:
514 515 pass
515 516 raise error.RepoLookupError(
516 517 _("unknown revision '%s'") % changeid)
517 518
518 519 def __hash__(self):
519 520 try:
520 521 return hash(self._rev)
521 522 except AttributeError:
522 523 return id(self)
523 524
524 525 def __nonzero__(self):
525 526 return self._rev != nullrev
526 527
527 528 @propertycache
528 529 def _changeset(self):
529 530 return self._repo.changelog.changelogrevision(self.rev())
530 531
531 532 @propertycache
532 533 def _manifest(self):
533 534 return self._manifestctx.read()
534 535
535 536 @propertycache
536 537 def _manifestctx(self):
537 538 return self._repo.manifestlog[self._changeset.manifest]
538 539
539 540 @propertycache
540 541 def _manifestdelta(self):
541 542 return self._manifestctx.readdelta()
542 543
543 544 @propertycache
544 545 def _parents(self):
545 546 repo = self._repo
546 547 p1, p2 = repo.changelog.parentrevs(self._rev)
547 548 if p2 == nullrev:
548 549 return [changectx(repo, p1)]
549 550 return [changectx(repo, p1), changectx(repo, p2)]
550 551
551 552 def changeset(self):
552 553 c = self._changeset
553 554 return (
554 555 c.manifest,
555 556 c.user,
556 557 c.date,
557 558 c.files,
558 559 c.description,
559 560 c.extra,
560 561 )
561 562 def manifestnode(self):
562 563 return self._changeset.manifest
563 564
564 565 def user(self):
565 566 return self._changeset.user
566 567 def date(self):
567 568 return self._changeset.date
568 569 def files(self):
569 570 return self._changeset.files
570 571 def description(self):
571 572 return self._changeset.description
572 573 def branch(self):
573 574 return encoding.tolocal(self._changeset.extra.get("branch"))
574 575 def closesbranch(self):
575 576 return 'close' in self._changeset.extra
576 577 def extra(self):
577 578 return self._changeset.extra
578 579 def tags(self):
579 580 return self._repo.nodetags(self._node)
580 581 def bookmarks(self):
581 582 return self._repo.nodebookmarks(self._node)
582 583 def phase(self):
583 584 return self._repo._phasecache.phase(self._repo, self._rev)
584 585 def hidden(self):
585 586 return self._rev in repoview.filterrevs(self._repo, 'visible')
586 587
587 588 def children(self):
588 589 """return contexts for each child changeset"""
589 590 c = self._repo.changelog.children(self._node)
590 591 return [changectx(self._repo, x) for x in c]
591 592
592 593 def ancestors(self):
593 594 for a in self._repo.changelog.ancestors([self._rev]):
594 595 yield changectx(self._repo, a)
595 596
596 597 def descendants(self):
597 598 for d in self._repo.changelog.descendants([self._rev]):
598 599 yield changectx(self._repo, d)
599 600
600 601 def filectx(self, path, fileid=None, filelog=None):
601 602 """get a file context from this changeset"""
602 603 if fileid is None:
603 604 fileid = self.filenode(path)
604 605 return filectx(self._repo, path, fileid=fileid,
605 606 changectx=self, filelog=filelog)
606 607
607 608 def ancestor(self, c2, warn=False):
608 609 """return the "best" ancestor context of self and c2
609 610
610 611 If there are multiple candidates, it will show a message and check
611 612 merge.preferancestor configuration before falling back to the
612 613 revlog ancestor."""
613 614 # deal with workingctxs
614 615 n2 = c2._node
615 616 if n2 is None:
616 617 n2 = c2._parents[0]._node
617 618 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
618 619 if not cahs:
619 620 anc = nullid
620 621 elif len(cahs) == 1:
621 622 anc = cahs[0]
622 623 else:
623 624 # experimental config: merge.preferancestor
624 625 for r in self._repo.ui.configlist('merge', 'preferancestor', ['*']):
625 626 try:
626 627 ctx = changectx(self._repo, r)
627 628 except error.RepoLookupError:
628 629 continue
629 630 anc = ctx.node()
630 631 if anc in cahs:
631 632 break
632 633 else:
633 634 anc = self._repo.changelog.ancestor(self._node, n2)
634 635 if warn:
635 636 self._repo.ui.status(
636 637 (_("note: using %s as ancestor of %s and %s\n") %
637 638 (short(anc), short(self._node), short(n2))) +
638 639 ''.join(_(" alternatively, use --config "
639 640 "merge.preferancestor=%s\n") %
640 641 short(n) for n in sorted(cahs) if n != anc))
641 642 return changectx(self._repo, anc)
642 643
643 644 def descendant(self, other):
644 645 """True if other is descendant of this changeset"""
645 646 return self._repo.changelog.descendant(self._rev, other._rev)
646 647
647 648 def walk(self, match):
648 649 '''Generates matching file names.'''
649 650
650 651 # Wrap match.bad method to have message with nodeid
651 652 def bad(fn, msg):
652 653 # The manifest doesn't know about subrepos, so don't complain about
653 654 # paths into valid subrepos.
654 655 if any(fn == s or fn.startswith(s + '/')
655 656 for s in self.substate):
656 657 return
657 658 match.bad(fn, _('no such file in rev %s') % self)
658 659
659 660 m = matchmod.badmatch(match, bad)
660 661 return self._manifest.walk(m)
661 662
662 663 def matches(self, match):
663 664 return self.walk(match)
664 665
665 666 class basefilectx(object):
666 667 """A filecontext object represents the common logic for its children:
667 668 filectx: read-only access to a filerevision that is already present
668 669 in the repo,
669 670 workingfilectx: a filecontext that represents files from the working
670 671 directory,
671 672 memfilectx: a filecontext that represents files in-memory."""
672 673 def __new__(cls, repo, path, *args, **kwargs):
673 674 return super(basefilectx, cls).__new__(cls)
674 675
675 676 @propertycache
676 677 def _filelog(self):
677 678 return self._repo.file(self._path)
678 679
679 680 @propertycache
680 681 def _changeid(self):
681 682 if '_changeid' in self.__dict__:
682 683 return self._changeid
683 684 elif '_changectx' in self.__dict__:
684 685 return self._changectx.rev()
685 686 elif '_descendantrev' in self.__dict__:
686 687 # this file context was created from a revision with a known
687 688 # descendant, we can (lazily) correct for linkrev aliases
688 689 return self._adjustlinkrev(self._descendantrev)
689 690 else:
690 691 return self._filelog.linkrev(self._filerev)
691 692
692 693 @propertycache
693 694 def _filenode(self):
694 695 if '_fileid' in self.__dict__:
695 696 return self._filelog.lookup(self._fileid)
696 697 else:
697 698 return self._changectx.filenode(self._path)
698 699
699 700 @propertycache
700 701 def _filerev(self):
701 702 return self._filelog.rev(self._filenode)
702 703
703 704 @propertycache
704 705 def _repopath(self):
705 706 return self._path
706 707
707 708 def __nonzero__(self):
708 709 try:
709 710 self._filenode
710 711 return True
711 712 except error.LookupError:
712 713 # file is missing
713 714 return False
714 715
715 716 def __str__(self):
716 717 try:
717 718 return "%s@%s" % (self.path(), self._changectx)
718 719 except error.LookupError:
719 720 return "%s@???" % self.path()
720 721
721 722 def __repr__(self):
722 723 return "<%s %s>" % (type(self).__name__, str(self))
723 724
724 725 def __hash__(self):
725 726 try:
726 727 return hash((self._path, self._filenode))
727 728 except AttributeError:
728 729 return id(self)
729 730
730 731 def __eq__(self, other):
731 732 try:
732 733 return (type(self) == type(other) and self._path == other._path
733 734 and self._filenode == other._filenode)
734 735 except AttributeError:
735 736 return False
736 737
737 738 def __ne__(self, other):
738 739 return not (self == other)
739 740
740 741 def filerev(self):
741 742 return self._filerev
742 743 def filenode(self):
743 744 return self._filenode
744 745 def flags(self):
745 746 return self._changectx.flags(self._path)
746 747 def filelog(self):
747 748 return self._filelog
748 749 def rev(self):
749 750 return self._changeid
750 751 def linkrev(self):
751 752 return self._filelog.linkrev(self._filerev)
752 753 def node(self):
753 754 return self._changectx.node()
754 755 def hex(self):
755 756 return self._changectx.hex()
756 757 def user(self):
757 758 return self._changectx.user()
758 759 def date(self):
759 760 return self._changectx.date()
760 761 def files(self):
761 762 return self._changectx.files()
762 763 def description(self):
763 764 return self._changectx.description()
764 765 def branch(self):
765 766 return self._changectx.branch()
766 767 def extra(self):
767 768 return self._changectx.extra()
768 769 def phase(self):
769 770 return self._changectx.phase()
770 771 def phasestr(self):
771 772 return self._changectx.phasestr()
772 773 def manifest(self):
773 774 return self._changectx.manifest()
774 775 def changectx(self):
775 776 return self._changectx
776 777 def repo(self):
777 778 return self._repo
778 779
779 780 def path(self):
780 781 return self._path
781 782
782 783 def isbinary(self):
783 784 try:
784 785 return util.binary(self.data())
785 786 except IOError:
786 787 return False
787 788 def isexec(self):
788 789 return 'x' in self.flags()
789 790 def islink(self):
790 791 return 'l' in self.flags()
791 792
792 793 def isabsent(self):
793 794 """whether this filectx represents a file not in self._changectx
794 795
795 796 This is mainly for merge code to detect change/delete conflicts. This is
796 797 expected to be True for all subclasses of basectx."""
797 798 return False
798 799
799 800 _customcmp = False
800 801 def cmp(self, fctx):
801 802 """compare with other file context
802 803
803 804 returns True if different than fctx.
804 805 """
805 806 if fctx._customcmp:
806 807 return fctx.cmp(self)
807 808
808 809 if (fctx._filenode is None
809 810 and (self._repo._encodefilterpats
810 811 # if file data starts with '\1\n', empty metadata block is
811 812 # prepended, which adds 4 bytes to filelog.size().
812 813 or self.size() - 4 == fctx.size())
813 814 or self.size() == fctx.size()):
814 815 return self._filelog.cmp(self._filenode, fctx.data())
815 816
816 817 return True
817 818
818 819 def _adjustlinkrev(self, srcrev, inclusive=False):
819 820 """return the first ancestor of <srcrev> introducing <fnode>
820 821
821 822 If the linkrev of the file revision does not point to an ancestor of
822 823 srcrev, we'll walk down the ancestors until we find one introducing
823 824 this file revision.
824 825
825 826 :srcrev: the changeset revision we search ancestors from
826 827 :inclusive: if true, the src revision will also be checked
827 828 """
828 829 repo = self._repo
829 830 cl = repo.unfiltered().changelog
830 831 mfl = repo.manifestlog
831 832 # fetch the linkrev
832 833 lkr = self.linkrev()
833 834 # hack to reuse ancestor computation when searching for renames
834 835 memberanc = getattr(self, '_ancestrycontext', None)
835 836 iteranc = None
836 837 if srcrev is None:
837 838 # wctx case, used by workingfilectx during mergecopy
838 839 revs = [p.rev() for p in self._repo[None].parents()]
839 840 inclusive = True # we skipped the real (revless) source
840 841 else:
841 842 revs = [srcrev]
842 843 if memberanc is None:
843 844 memberanc = iteranc = cl.ancestors(revs, lkr,
844 845 inclusive=inclusive)
845 846 # check if this linkrev is an ancestor of srcrev
846 847 if lkr not in memberanc:
847 848 if iteranc is None:
848 849 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
849 850 fnode = self._filenode
850 851 path = self._path
851 852 for a in iteranc:
852 853 ac = cl.read(a) # get changeset data (we avoid object creation)
853 854 if path in ac[3]: # checking the 'files' field.
854 855 # The file has been touched, check if the content is
855 856 # similar to the one we search for.
856 857 if fnode == mfl[ac[0]].readfast().get(path):
857 858 return a
858 859 # In theory, we should never get out of that loop without a result.
859 860 # But if manifest uses a buggy file revision (not children of the
860 861 # one it replaces) we could. Such a buggy situation will likely
861 862 # result is crash somewhere else at to some point.
862 863 return lkr
863 864
864 865 def introrev(self):
865 866 """return the rev of the changeset which introduced this file revision
866 867
867 868 This method is different from linkrev because it take into account the
868 869 changeset the filectx was created from. It ensures the returned
869 870 revision is one of its ancestors. This prevents bugs from
870 871 'linkrev-shadowing' when a file revision is used by multiple
871 872 changesets.
872 873 """
873 874 lkr = self.linkrev()
874 875 attrs = vars(self)
875 876 noctx = not ('_changeid' in attrs or '_changectx' in attrs)
876 877 if noctx or self.rev() == lkr:
877 878 return self.linkrev()
878 879 return self._adjustlinkrev(self.rev(), inclusive=True)
879 880
880 881 def _parentfilectx(self, path, fileid, filelog):
881 882 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
882 883 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
883 884 if '_changeid' in vars(self) or '_changectx' in vars(self):
884 885 # If self is associated with a changeset (probably explicitly
885 886 # fed), ensure the created filectx is associated with a
886 887 # changeset that is an ancestor of self.changectx.
887 888 # This lets us later use _adjustlinkrev to get a correct link.
888 889 fctx._descendantrev = self.rev()
889 890 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
890 891 elif '_descendantrev' in vars(self):
891 892 # Otherwise propagate _descendantrev if we have one associated.
892 893 fctx._descendantrev = self._descendantrev
893 894 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
894 895 return fctx
895 896
896 897 def parents(self):
897 898 _path = self._path
898 899 fl = self._filelog
899 900 parents = self._filelog.parents(self._filenode)
900 901 pl = [(_path, node, fl) for node in parents if node != nullid]
901 902
902 903 r = fl.renamed(self._filenode)
903 904 if r:
904 905 # - In the simple rename case, both parent are nullid, pl is empty.
905 906 # - In case of merge, only one of the parent is null id and should
906 907 # be replaced with the rename information. This parent is -always-
907 908 # the first one.
908 909 #
909 910 # As null id have always been filtered out in the previous list
910 911 # comprehension, inserting to 0 will always result in "replacing
911 912 # first nullid parent with rename information.
912 913 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
913 914
914 915 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
915 916
916 917 def p1(self):
917 918 return self.parents()[0]
918 919
919 920 def p2(self):
920 921 p = self.parents()
921 922 if len(p) == 2:
922 923 return p[1]
923 924 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
924 925
925 926 def annotate(self, follow=False, linenumber=False, diffopts=None):
926 927 '''returns a list of tuples of ((ctx, number), line) for each line
927 928 in the file, where ctx is the filectx of the node where
928 929 that line was last changed; if linenumber parameter is true, number is
929 930 the line number at the first appearance in the managed file, otherwise,
930 931 number has a fixed value of False.
931 932 '''
932 933
933 934 def lines(text):
934 935 if text.endswith("\n"):
935 936 return text.count("\n")
936 937 return text.count("\n") + int(bool(text))
937 938
938 939 if linenumber:
939 940 def decorate(text, rev):
940 941 return ([(rev, i) for i in xrange(1, lines(text) + 1)], text)
941 942 else:
942 943 def decorate(text, rev):
943 944 return ([(rev, False)] * lines(text), text)
944 945
945 946 def pair(parent, child):
946 947 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts)
947 948 for (a1, a2, b1, b2), t in blocks:
948 949 # Changed blocks ('!') or blocks made only of blank lines ('~')
949 950 # belong to the child.
950 951 if t == '=':
951 952 child[0][b1:b2] = parent[0][a1:a2]
952 953 return child
953 954
954 955 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
955 956
956 957 def parents(f):
957 958 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
958 959 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
959 960 # from the topmost introrev (= srcrev) down to p.linkrev() if it
960 961 # isn't an ancestor of the srcrev.
961 962 f._changeid
962 963 pl = f.parents()
963 964
964 965 # Don't return renamed parents if we aren't following.
965 966 if not follow:
966 967 pl = [p for p in pl if p.path() == f.path()]
967 968
968 969 # renamed filectx won't have a filelog yet, so set it
969 970 # from the cache to save time
970 971 for p in pl:
971 972 if not '_filelog' in p.__dict__:
972 973 p._filelog = getlog(p.path())
973 974
974 975 return pl
975 976
976 977 # use linkrev to find the first changeset where self appeared
977 978 base = self
978 979 introrev = self.introrev()
979 980 if self.rev() != introrev:
980 981 base = self.filectx(self.filenode(), changeid=introrev)
981 982 if getattr(base, '_ancestrycontext', None) is None:
982 983 cl = self._repo.changelog
983 984 if introrev is None:
984 985 # wctx is not inclusive, but works because _ancestrycontext
985 986 # is used to test filelog revisions
986 987 ac = cl.ancestors([p.rev() for p in base.parents()],
987 988 inclusive=True)
988 989 else:
989 990 ac = cl.ancestors([introrev], inclusive=True)
990 991 base._ancestrycontext = ac
991 992
992 993 # This algorithm would prefer to be recursive, but Python is a
993 994 # bit recursion-hostile. Instead we do an iterative
994 995 # depth-first search.
995 996
996 997 # 1st DFS pre-calculates pcache and needed
997 998 visit = [base]
998 999 pcache = {}
999 1000 needed = {base: 1}
1000 1001 while visit:
1001 1002 f = visit.pop()
1002 1003 if f in pcache:
1003 1004 continue
1004 1005 pl = parents(f)
1005 1006 pcache[f] = pl
1006 1007 for p in pl:
1007 1008 needed[p] = needed.get(p, 0) + 1
1008 1009 if p not in pcache:
1009 1010 visit.append(p)
1010 1011
1011 1012 # 2nd DFS does the actual annotate
1012 1013 visit[:] = [base]
1013 1014 hist = {}
1014 1015 while visit:
1015 1016 f = visit[-1]
1016 1017 if f in hist:
1017 1018 visit.pop()
1018 1019 continue
1019 1020
1020 1021 ready = True
1021 1022 pl = pcache[f]
1022 1023 for p in pl:
1023 1024 if p not in hist:
1024 1025 ready = False
1025 1026 visit.append(p)
1026 1027 if ready:
1027 1028 visit.pop()
1028 1029 curr = decorate(f.data(), f)
1029 1030 for p in pl:
1030 1031 curr = pair(hist[p], curr)
1031 1032 if needed[p] == 1:
1032 1033 del hist[p]
1033 1034 del needed[p]
1034 1035 else:
1035 1036 needed[p] -= 1
1036 1037
1037 1038 hist[f] = curr
1038 1039 del pcache[f]
1039 1040
1040 1041 return zip(hist[base][0], hist[base][1].splitlines(True))
1041 1042
1042 1043 def ancestors(self, followfirst=False):
1043 1044 visit = {}
1044 1045 c = self
1045 1046 if followfirst:
1046 1047 cut = 1
1047 1048 else:
1048 1049 cut = None
1049 1050
1050 1051 while True:
1051 1052 for parent in c.parents()[:cut]:
1052 1053 visit[(parent.linkrev(), parent.filenode())] = parent
1053 1054 if not visit:
1054 1055 break
1055 1056 c = visit.pop(max(visit))
1056 1057 yield c
1057 1058
1058 1059 class filectx(basefilectx):
1059 1060 """A filecontext object makes access to data related to a particular
1060 1061 filerevision convenient."""
1061 1062 def __init__(self, repo, path, changeid=None, fileid=None,
1062 1063 filelog=None, changectx=None):
1063 1064 """changeid can be a changeset revision, node, or tag.
1064 1065 fileid can be a file revision or node."""
1065 1066 self._repo = repo
1066 1067 self._path = path
1067 1068
1068 1069 assert (changeid is not None
1069 1070 or fileid is not None
1070 1071 or changectx is not None), \
1071 1072 ("bad args: changeid=%r, fileid=%r, changectx=%r"
1072 1073 % (changeid, fileid, changectx))
1073 1074
1074 1075 if filelog is not None:
1075 1076 self._filelog = filelog
1076 1077
1077 1078 if changeid is not None:
1078 1079 self._changeid = changeid
1079 1080 if changectx is not None:
1080 1081 self._changectx = changectx
1081 1082 if fileid is not None:
1082 1083 self._fileid = fileid
1083 1084
1084 1085 @propertycache
1085 1086 def _changectx(self):
1086 1087 try:
1087 1088 return changectx(self._repo, self._changeid)
1088 1089 except error.FilteredRepoLookupError:
1089 1090 # Linkrev may point to any revision in the repository. When the
1090 1091 # repository is filtered this may lead to `filectx` trying to build
1091 1092 # `changectx` for filtered revision. In such case we fallback to
1092 1093 # creating `changectx` on the unfiltered version of the reposition.
1093 1094 # This fallback should not be an issue because `changectx` from
1094 1095 # `filectx` are not used in complex operations that care about
1095 1096 # filtering.
1096 1097 #
1097 1098 # This fallback is a cheap and dirty fix that prevent several
1098 1099 # crashes. It does not ensure the behavior is correct. However the
1099 1100 # behavior was not correct before filtering either and "incorrect
1100 1101 # behavior" is seen as better as "crash"
1101 1102 #
1102 1103 # Linkrevs have several serious troubles with filtering that are
1103 1104 # complicated to solve. Proper handling of the issue here should be
1104 1105 # considered when solving linkrev issue are on the table.
1105 1106 return changectx(self._repo.unfiltered(), self._changeid)
1106 1107
1107 1108 def filectx(self, fileid, changeid=None):
1108 1109 '''opens an arbitrary revision of the file without
1109 1110 opening a new filelog'''
1110 1111 return filectx(self._repo, self._path, fileid=fileid,
1111 1112 filelog=self._filelog, changeid=changeid)
1112 1113
1113 1114 def rawdata(self):
1114 1115 return self._filelog.revision(self._filenode, raw=True)
1115 1116
1116 1117 def data(self):
1117 1118 try:
1118 1119 return self._filelog.read(self._filenode)
1119 1120 except error.CensoredNodeError:
1120 1121 if self._repo.ui.config("censor", "policy", "abort") == "ignore":
1121 1122 return ""
1122 1123 raise error.Abort(_("censored node: %s") % short(self._filenode),
1123 1124 hint=_("set censor.policy to ignore errors"))
1124 1125
1125 1126 def size(self):
1126 1127 return self._filelog.size(self._filerev)
1127 1128
1128 1129 def renamed(self):
1129 1130 """check if file was actually renamed in this changeset revision
1130 1131
1131 1132 If rename logged in file revision, we report copy for changeset only
1132 1133 if file revisions linkrev points back to the changeset in question
1133 1134 or both changeset parents contain different file revisions.
1134 1135 """
1135 1136
1136 1137 renamed = self._filelog.renamed(self._filenode)
1137 1138 if not renamed:
1138 1139 return renamed
1139 1140
1140 1141 if self.rev() == self.linkrev():
1141 1142 return renamed
1142 1143
1143 1144 name = self.path()
1144 1145 fnode = self._filenode
1145 1146 for p in self._changectx.parents():
1146 1147 try:
1147 1148 if fnode == p.filenode(name):
1148 1149 return None
1149 1150 except error.LookupError:
1150 1151 pass
1151 1152 return renamed
1152 1153
1153 1154 def children(self):
1154 1155 # hard for renames
1155 1156 c = self._filelog.children(self._filenode)
1156 1157 return [filectx(self._repo, self._path, fileid=x,
1157 1158 filelog=self._filelog) for x in c]
1158 1159
1159 1160 def _changesrange(fctx1, fctx2, linerange2, diffopts):
1160 1161 """Return `(diffinrange, linerange1)` where `diffinrange` is True
1161 1162 if diff from fctx2 to fctx1 has changes in linerange2 and
1162 1163 `linerange1` is the new line range for fctx1.
1163 1164 """
1164 1165 blocks = mdiff.allblocks(fctx1.data(), fctx2.data(), diffopts)
1165 1166 filteredblocks, linerange1 = mdiff.blocksinrange(blocks, linerange2)
1166 1167 diffinrange = any(stype == '!' for _, stype in filteredblocks)
1167 1168 return diffinrange, linerange1
1168 1169
1169 1170 def blockancestors(fctx, fromline, toline, followfirst=False):
1170 1171 """Yield ancestors of `fctx` with respect to the block of lines within
1171 1172 `fromline`-`toline` range.
1172 1173 """
1173 1174 diffopts = patch.diffopts(fctx._repo.ui)
1174 1175 visit = {(fctx.linkrev(), fctx.filenode()): (fctx, (fromline, toline))}
1175 1176 while visit:
1176 1177 c, linerange2 = visit.pop(max(visit))
1177 1178 pl = c.parents()
1178 1179 if followfirst:
1179 1180 pl = pl[:1]
1180 1181 if not pl:
1181 1182 # The block originates from the initial revision.
1182 1183 yield c, linerange2
1183 1184 continue
1184 1185 inrange = False
1185 1186 for p in pl:
1186 1187 inrangep, linerange1 = _changesrange(p, c, linerange2, diffopts)
1187 1188 inrange = inrange or inrangep
1188 1189 if linerange1[0] == linerange1[1]:
1189 1190 # Parent's linerange is empty, meaning that the block got
1190 1191 # introduced in this revision; no need to go futher in this
1191 1192 # branch.
1192 1193 continue
1193 1194 visit[p.linkrev(), p.filenode()] = p, linerange1
1194 1195 if inrange:
1195 1196 yield c, linerange2
1196 1197
1197 1198 class committablectx(basectx):
1198 1199 """A committablectx object provides common functionality for a context that
1199 1200 wants the ability to commit, e.g. workingctx or memctx."""
1200 1201 def __init__(self, repo, text="", user=None, date=None, extra=None,
1201 1202 changes=None):
1202 1203 self._repo = repo
1203 1204 self._rev = None
1204 1205 self._node = None
1205 1206 self._text = text
1206 1207 if date:
1207 1208 self._date = util.parsedate(date)
1208 1209 if user:
1209 1210 self._user = user
1210 1211 if changes:
1211 1212 self._status = changes
1212 1213
1213 1214 self._extra = {}
1214 1215 if extra:
1215 1216 self._extra = extra.copy()
1216 1217 if 'branch' not in self._extra:
1217 1218 try:
1218 1219 branch = encoding.fromlocal(self._repo.dirstate.branch())
1219 1220 except UnicodeDecodeError:
1220 1221 raise error.Abort(_('branch name not in UTF-8!'))
1221 1222 self._extra['branch'] = branch
1222 1223 if self._extra['branch'] == '':
1223 1224 self._extra['branch'] = 'default'
1224 1225
1225 1226 def __str__(self):
1226 1227 return str(self._parents[0]) + "+"
1227 1228
1228 1229 def __nonzero__(self):
1229 1230 return True
1230 1231
1231 1232 def _buildflagfunc(self):
1232 1233 # Create a fallback function for getting file flags when the
1233 1234 # filesystem doesn't support them
1234 1235
1235 1236 copiesget = self._repo.dirstate.copies().get
1236 1237 parents = self.parents()
1237 1238 if len(parents) < 2:
1238 1239 # when we have one parent, it's easy: copy from parent
1239 1240 man = parents[0].manifest()
1240 1241 def func(f):
1241 1242 f = copiesget(f, f)
1242 1243 return man.flags(f)
1243 1244 else:
1244 1245 # merges are tricky: we try to reconstruct the unstored
1245 1246 # result from the merge (issue1802)
1246 1247 p1, p2 = parents
1247 1248 pa = p1.ancestor(p2)
1248 1249 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1249 1250
1250 1251 def func(f):
1251 1252 f = copiesget(f, f) # may be wrong for merges with copies
1252 1253 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1253 1254 if fl1 == fl2:
1254 1255 return fl1
1255 1256 if fl1 == fla:
1256 1257 return fl2
1257 1258 if fl2 == fla:
1258 1259 return fl1
1259 1260 return '' # punt for conflicts
1260 1261
1261 1262 return func
1262 1263
1263 1264 @propertycache
1264 1265 def _flagfunc(self):
1265 1266 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1266 1267
1267 1268 @propertycache
1268 1269 def _manifest(self):
1269 1270 """generate a manifest corresponding to the values in self._status
1270 1271
1271 1272 This reuse the file nodeid from parent, but we append an extra letter
1272 1273 when modified. Modified files get an extra 'm' while added files get
1273 1274 an extra 'a'. This is used by manifests merge to see that files
1274 1275 are different and by update logic to avoid deleting newly added files.
1275 1276 """
1276 1277 parents = self.parents()
1277 1278
1278 1279 man = parents[0].manifest().copy()
1279 1280
1280 1281 ff = self._flagfunc
1281 1282 for i, l in ((addednodeid, self._status.added),
1282 1283 (modifiednodeid, self._status.modified)):
1283 1284 for f in l:
1284 1285 man[f] = i
1285 1286 try:
1286 1287 man.setflag(f, ff(f))
1287 1288 except OSError:
1288 1289 pass
1289 1290
1290 1291 for f in self._status.deleted + self._status.removed:
1291 1292 if f in man:
1292 1293 del man[f]
1293 1294
1294 1295 return man
1295 1296
1296 1297 @propertycache
1297 1298 def _status(self):
1298 1299 return self._repo.status()
1299 1300
1300 1301 @propertycache
1301 1302 def _user(self):
1302 1303 return self._repo.ui.username()
1303 1304
1304 1305 @propertycache
1305 1306 def _date(self):
1306 1307 return util.makedate()
1307 1308
1308 1309 def subrev(self, subpath):
1309 1310 return None
1310 1311
1311 1312 def manifestnode(self):
1312 1313 return None
1313 1314 def user(self):
1314 1315 return self._user or self._repo.ui.username()
1315 1316 def date(self):
1316 1317 return self._date
1317 1318 def description(self):
1318 1319 return self._text
1319 1320 def files(self):
1320 1321 return sorted(self._status.modified + self._status.added +
1321 1322 self._status.removed)
1322 1323
1323 1324 def modified(self):
1324 1325 return self._status.modified
1325 1326 def added(self):
1326 1327 return self._status.added
1327 1328 def removed(self):
1328 1329 return self._status.removed
1329 1330 def deleted(self):
1330 1331 return self._status.deleted
1331 1332 def branch(self):
1332 1333 return encoding.tolocal(self._extra['branch'])
1333 1334 def closesbranch(self):
1334 1335 return 'close' in self._extra
1335 1336 def extra(self):
1336 1337 return self._extra
1337 1338
1338 1339 def tags(self):
1339 1340 return []
1340 1341
1341 1342 def bookmarks(self):
1342 1343 b = []
1343 1344 for p in self.parents():
1344 1345 b.extend(p.bookmarks())
1345 1346 return b
1346 1347
1347 1348 def phase(self):
1348 1349 phase = phases.draft # default phase to draft
1349 1350 for p in self.parents():
1350 1351 phase = max(phase, p.phase())
1351 1352 return phase
1352 1353
1353 1354 def hidden(self):
1354 1355 return False
1355 1356
1356 1357 def children(self):
1357 1358 return []
1358 1359
1359 1360 def flags(self, path):
1360 1361 if '_manifest' in self.__dict__:
1361 1362 try:
1362 1363 return self._manifest.flags(path)
1363 1364 except KeyError:
1364 1365 return ''
1365 1366
1366 1367 try:
1367 1368 return self._flagfunc(path)
1368 1369 except OSError:
1369 1370 return ''
1370 1371
1371 1372 def ancestor(self, c2):
1372 1373 """return the "best" ancestor context of self and c2"""
1373 1374 return self._parents[0].ancestor(c2) # punt on two parents for now
1374 1375
1375 1376 def walk(self, match):
1376 1377 '''Generates matching file names.'''
1377 1378 return sorted(self._repo.dirstate.walk(match, sorted(self.substate),
1378 1379 True, False))
1379 1380
1380 1381 def matches(self, match):
1381 1382 return sorted(self._repo.dirstate.matches(match))
1382 1383
1383 1384 def ancestors(self):
1384 1385 for p in self._parents:
1385 1386 yield p
1386 1387 for a in self._repo.changelog.ancestors(
1387 1388 [p.rev() for p in self._parents]):
1388 1389 yield changectx(self._repo, a)
1389 1390
1390 1391 def markcommitted(self, node):
1391 1392 """Perform post-commit cleanup necessary after committing this ctx
1392 1393
1393 1394 Specifically, this updates backing stores this working context
1394 1395 wraps to reflect the fact that the changes reflected by this
1395 1396 workingctx have been committed. For example, it marks
1396 1397 modified and added files as normal in the dirstate.
1397 1398
1398 1399 """
1399 1400
1400 1401 self._repo.dirstate.beginparentchange()
1401 1402 for f in self.modified() + self.added():
1402 1403 self._repo.dirstate.normal(f)
1403 1404 for f in self.removed():
1404 1405 self._repo.dirstate.drop(f)
1405 1406 self._repo.dirstate.setparents(node)
1406 1407 self._repo.dirstate.endparentchange()
1407 1408
1408 1409 # write changes out explicitly, because nesting wlock at
1409 1410 # runtime may prevent 'wlock.release()' in 'repo.commit()'
1410 1411 # from immediately doing so for subsequent changing files
1411 1412 self._repo.dirstate.write(self._repo.currenttransaction())
1412 1413
1413 1414 class workingctx(committablectx):
1414 1415 """A workingctx object makes access to data related to
1415 1416 the current working directory convenient.
1416 1417 date - any valid date string or (unixtime, offset), or None.
1417 1418 user - username string, or None.
1418 1419 extra - a dictionary of extra values, or None.
1419 1420 changes - a list of file lists as returned by localrepo.status()
1420 1421 or None to use the repository status.
1421 1422 """
1422 1423 def __init__(self, repo, text="", user=None, date=None, extra=None,
1423 1424 changes=None):
1424 1425 super(workingctx, self).__init__(repo, text, user, date, extra, changes)
1425 1426
1426 1427 def __iter__(self):
1427 1428 d = self._repo.dirstate
1428 1429 for f in d:
1429 1430 if d[f] != 'r':
1430 1431 yield f
1431 1432
1432 1433 def __contains__(self, key):
1433 1434 return self._repo.dirstate[key] not in "?r"
1434 1435
1435 1436 def hex(self):
1436 1437 return hex(wdirid)
1437 1438
1438 1439 @propertycache
1439 1440 def _parents(self):
1440 1441 p = self._repo.dirstate.parents()
1441 1442 if p[1] == nullid:
1442 1443 p = p[:-1]
1443 1444 return [changectx(self._repo, x) for x in p]
1444 1445
1445 1446 def filectx(self, path, filelog=None):
1446 1447 """get a file context from the working directory"""
1447 1448 return workingfilectx(self._repo, path, workingctx=self,
1448 1449 filelog=filelog)
1449 1450
1450 1451 def dirty(self, missing=False, merge=True, branch=True):
1451 1452 "check whether a working directory is modified"
1452 1453 # check subrepos first
1453 1454 for s in sorted(self.substate):
1454 1455 if self.sub(s).dirty():
1455 1456 return True
1456 1457 # check current working dir
1457 1458 return ((merge and self.p2()) or
1458 1459 (branch and self.branch() != self.p1().branch()) or
1459 1460 self.modified() or self.added() or self.removed() or
1460 1461 (missing and self.deleted()))
1461 1462
1462 1463 def add(self, list, prefix=""):
1463 1464 join = lambda f: os.path.join(prefix, f)
1464 1465 with self._repo.wlock():
1465 1466 ui, ds = self._repo.ui, self._repo.dirstate
1466 1467 rejected = []
1467 1468 lstat = self._repo.wvfs.lstat
1468 1469 for f in list:
1469 1470 scmutil.checkportable(ui, join(f))
1470 1471 try:
1471 1472 st = lstat(f)
1472 1473 except OSError:
1473 1474 ui.warn(_("%s does not exist!\n") % join(f))
1474 1475 rejected.append(f)
1475 1476 continue
1476 1477 if st.st_size > 10000000:
1477 1478 ui.warn(_("%s: up to %d MB of RAM may be required "
1478 1479 "to manage this file\n"
1479 1480 "(use 'hg revert %s' to cancel the "
1480 1481 "pending addition)\n")
1481 1482 % (f, 3 * st.st_size // 1000000, join(f)))
1482 1483 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1483 1484 ui.warn(_("%s not added: only files and symlinks "
1484 1485 "supported currently\n") % join(f))
1485 1486 rejected.append(f)
1486 1487 elif ds[f] in 'amn':
1487 1488 ui.warn(_("%s already tracked!\n") % join(f))
1488 1489 elif ds[f] == 'r':
1489 1490 ds.normallookup(f)
1490 1491 else:
1491 1492 ds.add(f)
1492 1493 return rejected
1493 1494
1494 1495 def forget(self, files, prefix=""):
1495 1496 join = lambda f: os.path.join(prefix, f)
1496 1497 with self._repo.wlock():
1497 1498 rejected = []
1498 1499 for f in files:
1499 1500 if f not in self._repo.dirstate:
1500 1501 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1501 1502 rejected.append(f)
1502 1503 elif self._repo.dirstate[f] != 'a':
1503 1504 self._repo.dirstate.remove(f)
1504 1505 else:
1505 1506 self._repo.dirstate.drop(f)
1506 1507 return rejected
1507 1508
1508 1509 def undelete(self, list):
1509 1510 pctxs = self.parents()
1510 1511 with self._repo.wlock():
1511 1512 for f in list:
1512 1513 if self._repo.dirstate[f] != 'r':
1513 1514 self._repo.ui.warn(_("%s not removed!\n") % f)
1514 1515 else:
1515 1516 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1516 1517 t = fctx.data()
1517 1518 self._repo.wwrite(f, t, fctx.flags())
1518 1519 self._repo.dirstate.normal(f)
1519 1520
1520 1521 def copy(self, source, dest):
1521 1522 try:
1522 1523 st = self._repo.wvfs.lstat(dest)
1523 1524 except OSError as err:
1524 1525 if err.errno != errno.ENOENT:
1525 1526 raise
1526 1527 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1527 1528 return
1528 1529 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1529 1530 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1530 1531 "symbolic link\n") % dest)
1531 1532 else:
1532 1533 with self._repo.wlock():
1533 1534 if self._repo.dirstate[dest] in '?':
1534 1535 self._repo.dirstate.add(dest)
1535 1536 elif self._repo.dirstate[dest] in 'r':
1536 1537 self._repo.dirstate.normallookup(dest)
1537 1538 self._repo.dirstate.copy(source, dest)
1538 1539
1539 1540 def match(self, pats=[], include=None, exclude=None, default='glob',
1540 1541 listsubrepos=False, badfn=None):
1541 1542 r = self._repo
1542 1543
1543 1544 # Only a case insensitive filesystem needs magic to translate user input
1544 1545 # to actual case in the filesystem.
1545 1546 if not util.fscasesensitive(r.root):
1546 1547 return matchmod.icasefsmatcher(r.root, r.getcwd(), pats, include,
1547 1548 exclude, default, r.auditor, self,
1548 1549 listsubrepos=listsubrepos,
1549 1550 badfn=badfn)
1550 1551 return matchmod.match(r.root, r.getcwd(), pats,
1551 1552 include, exclude, default,
1552 1553 auditor=r.auditor, ctx=self,
1553 1554 listsubrepos=listsubrepos, badfn=badfn)
1554 1555
1555 1556 def _filtersuspectsymlink(self, files):
1556 1557 if not files or self._repo.dirstate._checklink:
1557 1558 return files
1558 1559
1559 1560 # Symlink placeholders may get non-symlink-like contents
1560 1561 # via user error or dereferencing by NFS or Samba servers,
1561 1562 # so we filter out any placeholders that don't look like a
1562 1563 # symlink
1563 1564 sane = []
1564 1565 for f in files:
1565 1566 if self.flags(f) == 'l':
1566 1567 d = self[f].data()
1567 1568 if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
1568 1569 self._repo.ui.debug('ignoring suspect symlink placeholder'
1569 1570 ' "%s"\n' % f)
1570 1571 continue
1571 1572 sane.append(f)
1572 1573 return sane
1573 1574
1574 1575 def _checklookup(self, files):
1575 1576 # check for any possibly clean files
1576 1577 if not files:
1577 1578 return [], []
1578 1579
1579 1580 modified = []
1580 1581 fixup = []
1581 1582 pctx = self._parents[0]
1582 1583 # do a full compare of any files that might have changed
1583 1584 for f in sorted(files):
1584 1585 if (f not in pctx or self.flags(f) != pctx.flags(f)
1585 1586 or pctx[f].cmp(self[f])):
1586 1587 modified.append(f)
1587 1588 else:
1588 1589 fixup.append(f)
1589 1590
1590 1591 # update dirstate for files that are actually clean
1591 1592 if fixup:
1592 1593 try:
1593 1594 # updating the dirstate is optional
1594 1595 # so we don't wait on the lock
1595 1596 # wlock can invalidate the dirstate, so cache normal _after_
1596 1597 # taking the lock
1597 1598 with self._repo.wlock(False):
1598 1599 normal = self._repo.dirstate.normal
1599 1600 for f in fixup:
1600 1601 normal(f)
1601 1602 # write changes out explicitly, because nesting
1602 1603 # wlock at runtime may prevent 'wlock.release()'
1603 1604 # after this block from doing so for subsequent
1604 1605 # changing files
1605 1606 self._repo.dirstate.write(self._repo.currenttransaction())
1606 1607 except error.LockError:
1607 1608 pass
1608 1609 return modified, fixup
1609 1610
1610 1611 def _manifestmatches(self, match, s):
1611 1612 """Slow path for workingctx
1612 1613
1613 1614 The fast path is when we compare the working directory to its parent
1614 1615 which means this function is comparing with a non-parent; therefore we
1615 1616 need to build a manifest and return what matches.
1616 1617 """
1617 1618 mf = self._repo['.']._manifestmatches(match, s)
1618 1619 for f in s.modified + s.added:
1619 1620 mf[f] = newnodeid
1620 1621 mf.setflag(f, self.flags(f))
1621 1622 for f in s.removed:
1622 1623 if f in mf:
1623 1624 del mf[f]
1624 1625 return mf
1625 1626
1626 1627 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1627 1628 unknown=False):
1628 1629 '''Gets the status from the dirstate -- internal use only.'''
1629 1630 listignored, listclean, listunknown = ignored, clean, unknown
1630 1631 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1631 1632 subrepos = []
1632 1633 if '.hgsub' in self:
1633 1634 subrepos = sorted(self.substate)
1634 1635 cmp, s = self._repo.dirstate.status(match, subrepos, listignored,
1635 1636 listclean, listunknown)
1636 1637
1637 1638 # check for any possibly clean files
1638 1639 if cmp:
1639 1640 modified2, fixup = self._checklookup(cmp)
1640 1641 s.modified.extend(modified2)
1641 1642
1642 1643 # update dirstate for files that are actually clean
1643 1644 if fixup and listclean:
1644 1645 s.clean.extend(fixup)
1645 1646
1646 1647 if match.always():
1647 1648 # cache for performance
1648 1649 if s.unknown or s.ignored or s.clean:
1649 1650 # "_status" is cached with list*=False in the normal route
1650 1651 self._status = scmutil.status(s.modified, s.added, s.removed,
1651 1652 s.deleted, [], [], [])
1652 1653 else:
1653 1654 self._status = s
1654 1655
1655 1656 return s
1656 1657
1657 1658 def _buildstatus(self, other, s, match, listignored, listclean,
1658 1659 listunknown):
1659 1660 """build a status with respect to another context
1660 1661
1661 1662 This includes logic for maintaining the fast path of status when
1662 1663 comparing the working directory against its parent, which is to skip
1663 1664 building a new manifest if self (working directory) is not comparing
1664 1665 against its parent (repo['.']).
1665 1666 """
1666 1667 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1667 1668 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1668 1669 # might have accidentally ended up with the entire contents of the file
1669 1670 # they are supposed to be linking to.
1670 1671 s.modified[:] = self._filtersuspectsymlink(s.modified)
1671 1672 if other != self._repo['.']:
1672 1673 s = super(workingctx, self)._buildstatus(other, s, match,
1673 1674 listignored, listclean,
1674 1675 listunknown)
1675 1676 return s
1676 1677
1677 1678 def _matchstatus(self, other, match):
1678 1679 """override the match method with a filter for directory patterns
1679 1680
1680 1681 We use inheritance to customize the match.bad method only in cases of
1681 1682 workingctx since it belongs only to the working directory when
1682 1683 comparing against the parent changeset.
1683 1684
1684 1685 If we aren't comparing against the working directory's parent, then we
1685 1686 just use the default match object sent to us.
1686 1687 """
1687 1688 superself = super(workingctx, self)
1688 1689 match = superself._matchstatus(other, match)
1689 1690 if other != self._repo['.']:
1690 1691 def bad(f, msg):
1691 1692 # 'f' may be a directory pattern from 'match.files()',
1692 1693 # so 'f not in ctx1' is not enough
1693 1694 if f not in other and not other.hasdir(f):
1694 1695 self._repo.ui.warn('%s: %s\n' %
1695 1696 (self._repo.dirstate.pathto(f), msg))
1696 1697 match.bad = bad
1697 1698 return match
1698 1699
1699 1700 class committablefilectx(basefilectx):
1700 1701 """A committablefilectx provides common functionality for a file context
1701 1702 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
1702 1703 def __init__(self, repo, path, filelog=None, ctx=None):
1703 1704 self._repo = repo
1704 1705 self._path = path
1705 1706 self._changeid = None
1706 1707 self._filerev = self._filenode = None
1707 1708
1708 1709 if filelog is not None:
1709 1710 self._filelog = filelog
1710 1711 if ctx:
1711 1712 self._changectx = ctx
1712 1713
1713 1714 def __nonzero__(self):
1714 1715 return True
1715 1716
1716 1717 def linkrev(self):
1717 1718 # linked to self._changectx no matter if file is modified or not
1718 1719 return self.rev()
1719 1720
1720 1721 def parents(self):
1721 1722 '''return parent filectxs, following copies if necessary'''
1722 1723 def filenode(ctx, path):
1723 1724 return ctx._manifest.get(path, nullid)
1724 1725
1725 1726 path = self._path
1726 1727 fl = self._filelog
1727 1728 pcl = self._changectx._parents
1728 1729 renamed = self.renamed()
1729 1730
1730 1731 if renamed:
1731 1732 pl = [renamed + (None,)]
1732 1733 else:
1733 1734 pl = [(path, filenode(pcl[0], path), fl)]
1734 1735
1735 1736 for pc in pcl[1:]:
1736 1737 pl.append((path, filenode(pc, path), fl))
1737 1738
1738 1739 return [self._parentfilectx(p, fileid=n, filelog=l)
1739 1740 for p, n, l in pl if n != nullid]
1740 1741
1741 1742 def children(self):
1742 1743 return []
1743 1744
1744 1745 class workingfilectx(committablefilectx):
1745 1746 """A workingfilectx object makes access to data related to a particular
1746 1747 file in the working directory convenient."""
1747 1748 def __init__(self, repo, path, filelog=None, workingctx=None):
1748 1749 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
1749 1750
1750 1751 @propertycache
1751 1752 def _changectx(self):
1752 1753 return workingctx(self._repo)
1753 1754
1754 1755 def data(self):
1755 1756 return self._repo.wread(self._path)
1756 1757 def renamed(self):
1757 1758 rp = self._repo.dirstate.copied(self._path)
1758 1759 if not rp:
1759 1760 return None
1760 1761 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1761 1762
1762 1763 def size(self):
1763 1764 return self._repo.wvfs.lstat(self._path).st_size
1764 1765 def date(self):
1765 1766 t, tz = self._changectx.date()
1766 1767 try:
1767 1768 return (self._repo.wvfs.lstat(self._path).st_mtime, tz)
1768 1769 except OSError as err:
1769 1770 if err.errno != errno.ENOENT:
1770 1771 raise
1771 1772 return (t, tz)
1772 1773
1773 1774 def cmp(self, fctx):
1774 1775 """compare with other file context
1775 1776
1776 1777 returns True if different than fctx.
1777 1778 """
1778 1779 # fctx should be a filectx (not a workingfilectx)
1779 1780 # invert comparison to reuse the same code path
1780 1781 return fctx.cmp(self)
1781 1782
1782 1783 def remove(self, ignoremissing=False):
1783 1784 """wraps unlink for a repo's working directory"""
1784 1785 util.unlinkpath(self._repo.wjoin(self._path), ignoremissing)
1785 1786
1786 1787 def write(self, data, flags):
1787 1788 """wraps repo.wwrite"""
1788 1789 self._repo.wwrite(self._path, data, flags)
1789 1790
1790 1791 class workingcommitctx(workingctx):
1791 1792 """A workingcommitctx object makes access to data related to
1792 1793 the revision being committed convenient.
1793 1794
1794 1795 This hides changes in the working directory, if they aren't
1795 1796 committed in this context.
1796 1797 """
1797 1798 def __init__(self, repo, changes,
1798 1799 text="", user=None, date=None, extra=None):
1799 1800 super(workingctx, self).__init__(repo, text, user, date, extra,
1800 1801 changes)
1801 1802
1802 1803 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1803 1804 unknown=False):
1804 1805 """Return matched files only in ``self._status``
1805 1806
1806 1807 Uncommitted files appear "clean" via this context, even if
1807 1808 they aren't actually so in the working directory.
1808 1809 """
1809 1810 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1810 1811 if clean:
1811 1812 clean = [f for f in self._manifest if f not in self._changedset]
1812 1813 else:
1813 1814 clean = []
1814 1815 return scmutil.status([f for f in self._status.modified if match(f)],
1815 1816 [f for f in self._status.added if match(f)],
1816 1817 [f for f in self._status.removed if match(f)],
1817 1818 [], [], [], clean)
1818 1819
1819 1820 @propertycache
1820 1821 def _changedset(self):
1821 1822 """Return the set of files changed in this context
1822 1823 """
1823 1824 changed = set(self._status.modified)
1824 1825 changed.update(self._status.added)
1825 1826 changed.update(self._status.removed)
1826 1827 return changed
1827 1828
1828 1829 def makecachingfilectxfn(func):
1829 1830 """Create a filectxfn that caches based on the path.
1830 1831
1831 1832 We can't use util.cachefunc because it uses all arguments as the cache
1832 1833 key and this creates a cycle since the arguments include the repo and
1833 1834 memctx.
1834 1835 """
1835 1836 cache = {}
1836 1837
1837 1838 def getfilectx(repo, memctx, path):
1838 1839 if path not in cache:
1839 1840 cache[path] = func(repo, memctx, path)
1840 1841 return cache[path]
1841 1842
1842 1843 return getfilectx
1843 1844
1844 1845 class memctx(committablectx):
1845 1846 """Use memctx to perform in-memory commits via localrepo.commitctx().
1846 1847
1847 1848 Revision information is supplied at initialization time while
1848 1849 related files data and is made available through a callback
1849 1850 mechanism. 'repo' is the current localrepo, 'parents' is a
1850 1851 sequence of two parent revisions identifiers (pass None for every
1851 1852 missing parent), 'text' is the commit message and 'files' lists
1852 1853 names of files touched by the revision (normalized and relative to
1853 1854 repository root).
1854 1855
1855 1856 filectxfn(repo, memctx, path) is a callable receiving the
1856 1857 repository, the current memctx object and the normalized path of
1857 1858 requested file, relative to repository root. It is fired by the
1858 1859 commit function for every file in 'files', but calls order is
1859 1860 undefined. If the file is available in the revision being
1860 1861 committed (updated or added), filectxfn returns a memfilectx
1861 1862 object. If the file was removed, filectxfn raises an
1862 1863 IOError. Moved files are represented by marking the source file
1863 1864 removed and the new file added with copy information (see
1864 1865 memfilectx).
1865 1866
1866 1867 user receives the committer name and defaults to current
1867 1868 repository username, date is the commit date in any format
1868 1869 supported by util.parsedate() and defaults to current date, extra
1869 1870 is a dictionary of metadata or is left empty.
1870 1871 """
1871 1872
1872 1873 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
1873 1874 # Extensions that need to retain compatibility across Mercurial 3.1 can use
1874 1875 # this field to determine what to do in filectxfn.
1875 1876 _returnnoneformissingfiles = True
1876 1877
1877 1878 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1878 1879 date=None, extra=None, editor=False):
1879 1880 super(memctx, self).__init__(repo, text, user, date, extra)
1880 1881 self._rev = None
1881 1882 self._node = None
1882 1883 parents = [(p or nullid) for p in parents]
1883 1884 p1, p2 = parents
1884 1885 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1885 1886 files = sorted(set(files))
1886 1887 self._files = files
1887 1888 self.substate = {}
1888 1889
1889 1890 # if store is not callable, wrap it in a function
1890 1891 if not callable(filectxfn):
1891 1892 def getfilectx(repo, memctx, path):
1892 1893 fctx = filectxfn[path]
1893 1894 # this is weird but apparently we only keep track of one parent
1894 1895 # (why not only store that instead of a tuple?)
1895 1896 copied = fctx.renamed()
1896 1897 if copied:
1897 1898 copied = copied[0]
1898 1899 return memfilectx(repo, path, fctx.data(),
1899 1900 islink=fctx.islink(), isexec=fctx.isexec(),
1900 1901 copied=copied, memctx=memctx)
1901 1902 self._filectxfn = getfilectx
1902 1903 else:
1903 1904 # memoizing increases performance for e.g. vcs convert scenarios.
1904 1905 self._filectxfn = makecachingfilectxfn(filectxfn)
1905 1906
1906 1907 if extra:
1907 1908 self._extra = extra.copy()
1908 1909 else:
1909 1910 self._extra = {}
1910 1911
1911 1912 if self._extra.get('branch', '') == '':
1912 1913 self._extra['branch'] = 'default'
1913 1914
1914 1915 if editor:
1915 1916 self._text = editor(self._repo, self, [])
1916 1917 self._repo.savecommitmessage(self._text)
1917 1918
1918 1919 def filectx(self, path, filelog=None):
1919 1920 """get a file context from the working directory
1920 1921
1921 1922 Returns None if file doesn't exist and should be removed."""
1922 1923 return self._filectxfn(self._repo, self, path)
1923 1924
1924 1925 def commit(self):
1925 1926 """commit context to the repo"""
1926 1927 return self._repo.commitctx(self)
1927 1928
1928 1929 @propertycache
1929 1930 def _manifest(self):
1930 1931 """generate a manifest based on the return values of filectxfn"""
1931 1932
1932 1933 # keep this simple for now; just worry about p1
1933 1934 pctx = self._parents[0]
1934 1935 man = pctx.manifest().copy()
1935 1936
1936 1937 for f in self._status.modified:
1937 1938 p1node = nullid
1938 1939 p2node = nullid
1939 1940 p = pctx[f].parents() # if file isn't in pctx, check p2?
1940 1941 if len(p) > 0:
1941 1942 p1node = p[0].filenode()
1942 1943 if len(p) > 1:
1943 1944 p2node = p[1].filenode()
1944 1945 man[f] = revlog.hash(self[f].data(), p1node, p2node)
1945 1946
1946 1947 for f in self._status.added:
1947 1948 man[f] = revlog.hash(self[f].data(), nullid, nullid)
1948 1949
1949 1950 for f in self._status.removed:
1950 1951 if f in man:
1951 1952 del man[f]
1952 1953
1953 1954 return man
1954 1955
1955 1956 @propertycache
1956 1957 def _status(self):
1957 1958 """Calculate exact status from ``files`` specified at construction
1958 1959 """
1959 1960 man1 = self.p1().manifest()
1960 1961 p2 = self._parents[1]
1961 1962 # "1 < len(self._parents)" can't be used for checking
1962 1963 # existence of the 2nd parent, because "memctx._parents" is
1963 1964 # explicitly initialized by the list, of which length is 2.
1964 1965 if p2.node() != nullid:
1965 1966 man2 = p2.manifest()
1966 1967 managing = lambda f: f in man1 or f in man2
1967 1968 else:
1968 1969 managing = lambda f: f in man1
1969 1970
1970 1971 modified, added, removed = [], [], []
1971 1972 for f in self._files:
1972 1973 if not managing(f):
1973 1974 added.append(f)
1974 1975 elif self[f]:
1975 1976 modified.append(f)
1976 1977 else:
1977 1978 removed.append(f)
1978 1979
1979 1980 return scmutil.status(modified, added, removed, [], [], [], [])
1980 1981
1981 1982 class memfilectx(committablefilectx):
1982 1983 """memfilectx represents an in-memory file to commit.
1983 1984
1984 1985 See memctx and committablefilectx for more details.
1985 1986 """
1986 1987 def __init__(self, repo, path, data, islink=False,
1987 1988 isexec=False, copied=None, memctx=None):
1988 1989 """
1989 1990 path is the normalized file path relative to repository root.
1990 1991 data is the file content as a string.
1991 1992 islink is True if the file is a symbolic link.
1992 1993 isexec is True if the file is executable.
1993 1994 copied is the source file path if current file was copied in the
1994 1995 revision being committed, or None."""
1995 1996 super(memfilectx, self).__init__(repo, path, None, memctx)
1996 1997 self._data = data
1997 1998 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1998 1999 self._copied = None
1999 2000 if copied:
2000 2001 self._copied = (copied, nullid)
2001 2002
2002 2003 def data(self):
2003 2004 return self._data
2004 2005 def size(self):
2005 2006 return len(self.data())
2006 2007 def flags(self):
2007 2008 return self._flags
2008 2009 def renamed(self):
2009 2010 return self._copied
2010 2011
2011 2012 def remove(self, ignoremissing=False):
2012 2013 """wraps unlink for a repo's working directory"""
2013 2014 # need to figure out what to do here
2014 2015 del self._changectx[self._path]
2015 2016
2016 2017 def write(self, data, flags):
2017 2018 """wraps repo.wwrite"""
2018 2019 self._data = data
2019 2020
2020 2021 class metadataonlyctx(committablectx):
2021 2022 """Like memctx but it's reusing the manifest of different commit.
2022 2023 Intended to be used by lightweight operations that are creating
2023 2024 metadata-only changes.
2024 2025
2025 2026 Revision information is supplied at initialization time. 'repo' is the
2026 2027 current localrepo, 'ctx' is original revision which manifest we're reuisng
2027 2028 'parents' is a sequence of two parent revisions identifiers (pass None for
2028 2029 every missing parent), 'text' is the commit.
2029 2030
2030 2031 user receives the committer name and defaults to current repository
2031 2032 username, date is the commit date in any format supported by
2032 2033 util.parsedate() and defaults to current date, extra is a dictionary of
2033 2034 metadata or is left empty.
2034 2035 """
2035 2036 def __new__(cls, repo, originalctx, *args, **kwargs):
2036 2037 return super(metadataonlyctx, cls).__new__(cls, repo)
2037 2038
2038 2039 def __init__(self, repo, originalctx, parents, text, user=None, date=None,
2039 2040 extra=None, editor=False):
2040 2041 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
2041 2042 self._rev = None
2042 2043 self._node = None
2043 2044 self._originalctx = originalctx
2044 2045 self._manifestnode = originalctx.manifestnode()
2045 2046 parents = [(p or nullid) for p in parents]
2046 2047 p1, p2 = self._parents = [changectx(self._repo, p) for p in parents]
2047 2048
2048 2049 # sanity check to ensure that the reused manifest parents are
2049 2050 # manifests of our commit parents
2050 2051 mp1, mp2 = self.manifestctx().parents
2051 2052 if p1 != nullid and p1.manifestctx().node() != mp1:
2052 2053 raise RuntimeError('can\'t reuse the manifest: '
2053 2054 'its p1 doesn\'t match the new ctx p1')
2054 2055 if p2 != nullid and p2.manifestctx().node() != mp2:
2055 2056 raise RuntimeError('can\'t reuse the manifest: '
2056 2057 'its p2 doesn\'t match the new ctx p2')
2057 2058
2058 2059 self._files = originalctx.files()
2059 2060 self.substate = {}
2060 2061
2061 2062 if extra:
2062 2063 self._extra = extra.copy()
2063 2064 else:
2064 2065 self._extra = {}
2065 2066
2066 2067 if self._extra.get('branch', '') == '':
2067 2068 self._extra['branch'] = 'default'
2068 2069
2069 2070 if editor:
2070 2071 self._text = editor(self._repo, self, [])
2071 2072 self._repo.savecommitmessage(self._text)
2072 2073
2073 2074 def manifestnode(self):
2074 2075 return self._manifestnode
2075 2076
2076 2077 @propertycache
2077 2078 def _manifestctx(self):
2078 2079 return self._repo.manifestlog[self._manifestnode]
2079 2080
2080 2081 def filectx(self, path, filelog=None):
2081 2082 return self._originalctx.filectx(path, filelog=filelog)
2082 2083
2083 2084 def commit(self):
2084 2085 """commit context to the repo"""
2085 2086 return self._repo.commitctx(self)
2086 2087
2087 2088 @property
2088 2089 def _manifest(self):
2089 2090 return self._originalctx.manifest()
2090 2091
2091 2092 @propertycache
2092 2093 def _status(self):
2093 2094 """Calculate exact status from ``files`` specified in the ``origctx``
2094 2095 and parents manifests.
2095 2096 """
2096 2097 man1 = self.p1().manifest()
2097 2098 p2 = self._parents[1]
2098 2099 # "1 < len(self._parents)" can't be used for checking
2099 2100 # existence of the 2nd parent, because "metadataonlyctx._parents" is
2100 2101 # explicitly initialized by the list, of which length is 2.
2101 2102 if p2.node() != nullid:
2102 2103 man2 = p2.manifest()
2103 2104 managing = lambda f: f in man1 or f in man2
2104 2105 else:
2105 2106 managing = lambda f: f in man1
2106 2107
2107 2108 modified, added, removed = [], [], []
2108 2109 for f in self._files:
2109 2110 if not managing(f):
2110 2111 added.append(f)
2111 2112 elif self[f]:
2112 2113 modified.append(f)
2113 2114 else:
2114 2115 removed.append(f)
2115 2116
2116 2117 return scmutil.status(modified, added, removed, [], [], [], [])
General Comments 0
You need to be logged in to leave comments. Login now