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