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