##// END OF EJS Templates
context: generate file ancestors in reverse revision order (issue2642)...
Matt Mackall -
r13468:d1007023 stable
parent child Browse files
Show More
@@ -1,1106 +1,1110
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import nullid, nullrev, short, hex
9 9 from i18n import _
10 10 import ancestor, bdiff, error, util, subrepo, patch, encoding
11 11 import os, errno, stat
12 12
13 13 propertycache = util.propertycache
14 14
15 15 class changectx(object):
16 16 """A changecontext object makes access to data related to a particular
17 17 changeset convenient."""
18 18 def __init__(self, repo, changeid=''):
19 19 """changeid is a revision number, node, or tag"""
20 20 if changeid == '':
21 21 changeid = '.'
22 22 self._repo = repo
23 23 if isinstance(changeid, (long, int)):
24 24 self._rev = changeid
25 25 self._node = self._repo.changelog.node(changeid)
26 26 else:
27 27 self._node = self._repo.lookup(changeid)
28 28 self._rev = self._repo.changelog.rev(self._node)
29 29
30 30 def __str__(self):
31 31 return short(self.node())
32 32
33 33 def __int__(self):
34 34 return self.rev()
35 35
36 36 def __repr__(self):
37 37 return "<changectx %s>" % str(self)
38 38
39 39 def __hash__(self):
40 40 try:
41 41 return hash(self._rev)
42 42 except AttributeError:
43 43 return id(self)
44 44
45 45 def __eq__(self, other):
46 46 try:
47 47 return self._rev == other._rev
48 48 except AttributeError:
49 49 return False
50 50
51 51 def __ne__(self, other):
52 52 return not (self == other)
53 53
54 54 def __nonzero__(self):
55 55 return self._rev != nullrev
56 56
57 57 @propertycache
58 58 def _changeset(self):
59 59 return self._repo.changelog.read(self.node())
60 60
61 61 @propertycache
62 62 def _manifest(self):
63 63 return self._repo.manifest.read(self._changeset[0])
64 64
65 65 @propertycache
66 66 def _manifestdelta(self):
67 67 return self._repo.manifest.readdelta(self._changeset[0])
68 68
69 69 @propertycache
70 70 def _parents(self):
71 71 p = self._repo.changelog.parentrevs(self._rev)
72 72 if p[1] == nullrev:
73 73 p = p[:-1]
74 74 return [changectx(self._repo, x) for x in p]
75 75
76 76 @propertycache
77 77 def substate(self):
78 78 return subrepo.state(self, self._repo.ui)
79 79
80 80 def __contains__(self, key):
81 81 return key in self._manifest
82 82
83 83 def __getitem__(self, key):
84 84 return self.filectx(key)
85 85
86 86 def __iter__(self):
87 87 for f in sorted(self._manifest):
88 88 yield f
89 89
90 90 def changeset(self):
91 91 return self._changeset
92 92 def manifest(self):
93 93 return self._manifest
94 94 def manifestnode(self):
95 95 return self._changeset[0]
96 96
97 97 def rev(self):
98 98 return self._rev
99 99 def node(self):
100 100 return self._node
101 101 def hex(self):
102 102 return hex(self._node)
103 103 def user(self):
104 104 return self._changeset[1]
105 105 def date(self):
106 106 return self._changeset[2]
107 107 def files(self):
108 108 return self._changeset[3]
109 109 def description(self):
110 110 return self._changeset[4]
111 111 def branch(self):
112 112 return encoding.tolocal(self._changeset[5].get("branch"))
113 113 def extra(self):
114 114 return self._changeset[5]
115 115 def tags(self):
116 116 return self._repo.nodetags(self._node)
117 117 def bookmarks(self):
118 118 return self._repo.nodebookmarks(self._node)
119 119
120 120 def parents(self):
121 121 """return contexts for each parent changeset"""
122 122 return self._parents
123 123
124 124 def p1(self):
125 125 return self._parents[0]
126 126
127 127 def p2(self):
128 128 if len(self._parents) == 2:
129 129 return self._parents[1]
130 130 return changectx(self._repo, -1)
131 131
132 132 def children(self):
133 133 """return contexts for each child changeset"""
134 134 c = self._repo.changelog.children(self._node)
135 135 return [changectx(self._repo, x) for x in c]
136 136
137 137 def ancestors(self):
138 138 for a in self._repo.changelog.ancestors(self._rev):
139 139 yield changectx(self._repo, a)
140 140
141 141 def descendants(self):
142 142 for d in self._repo.changelog.descendants(self._rev):
143 143 yield changectx(self._repo, d)
144 144
145 145 def _fileinfo(self, path):
146 146 if '_manifest' in self.__dict__:
147 147 try:
148 148 return self._manifest[path], self._manifest.flags(path)
149 149 except KeyError:
150 150 raise error.LookupError(self._node, path,
151 151 _('not found in manifest'))
152 152 if '_manifestdelta' in self.__dict__ or path in self.files():
153 153 if path in self._manifestdelta:
154 154 return self._manifestdelta[path], self._manifestdelta.flags(path)
155 155 node, flag = self._repo.manifest.find(self._changeset[0], path)
156 156 if not node:
157 157 raise error.LookupError(self._node, path,
158 158 _('not found in manifest'))
159 159
160 160 return node, flag
161 161
162 162 def filenode(self, path):
163 163 return self._fileinfo(path)[0]
164 164
165 165 def flags(self, path):
166 166 try:
167 167 return self._fileinfo(path)[1]
168 168 except error.LookupError:
169 169 return ''
170 170
171 171 def filectx(self, path, fileid=None, filelog=None):
172 172 """get a file context from this changeset"""
173 173 if fileid is None:
174 174 fileid = self.filenode(path)
175 175 return filectx(self._repo, path, fileid=fileid,
176 176 changectx=self, filelog=filelog)
177 177
178 178 def ancestor(self, c2):
179 179 """
180 180 return the ancestor context of self and c2
181 181 """
182 182 # deal with workingctxs
183 183 n2 = c2._node
184 184 if n2 is None:
185 185 n2 = c2._parents[0]._node
186 186 n = self._repo.changelog.ancestor(self._node, n2)
187 187 return changectx(self._repo, n)
188 188
189 189 def walk(self, match):
190 190 fset = set(match.files())
191 191 # for dirstate.walk, files=['.'] means "walk the whole tree".
192 192 # follow that here, too
193 193 fset.discard('.')
194 194 for fn in self:
195 195 for ffn in fset:
196 196 # match if the file is the exact name or a directory
197 197 if ffn == fn or fn.startswith("%s/" % ffn):
198 198 fset.remove(ffn)
199 199 break
200 200 if match(fn):
201 201 yield fn
202 202 for fn in sorted(fset):
203 203 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
204 204 yield fn
205 205
206 206 def sub(self, path):
207 207 return subrepo.subrepo(self, path)
208 208
209 209 def diff(self, ctx2=None, match=None, **opts):
210 210 """Returns a diff generator for the given contexts and matcher"""
211 211 if ctx2 is None:
212 212 ctx2 = self.p1()
213 213 if ctx2 is not None and not isinstance(ctx2, changectx):
214 214 ctx2 = self._repo[ctx2]
215 215 diffopts = patch.diffopts(self._repo.ui, opts)
216 216 return patch.diff(self._repo, ctx2.node(), self.node(),
217 217 match=match, opts=diffopts)
218 218
219 219 class filectx(object):
220 220 """A filecontext object makes access to data related to a particular
221 221 filerevision convenient."""
222 222 def __init__(self, repo, path, changeid=None, fileid=None,
223 223 filelog=None, changectx=None):
224 224 """changeid can be a changeset revision, node, or tag.
225 225 fileid can be a file revision or node."""
226 226 self._repo = repo
227 227 self._path = path
228 228
229 229 assert (changeid is not None
230 230 or fileid is not None
231 231 or changectx is not None), \
232 232 ("bad args: changeid=%r, fileid=%r, changectx=%r"
233 233 % (changeid, fileid, changectx))
234 234
235 235 if filelog:
236 236 self._filelog = filelog
237 237
238 238 if changeid is not None:
239 239 self._changeid = changeid
240 240 if changectx is not None:
241 241 self._changectx = changectx
242 242 if fileid is not None:
243 243 self._fileid = fileid
244 244
245 245 @propertycache
246 246 def _changectx(self):
247 247 return changectx(self._repo, self._changeid)
248 248
249 249 @propertycache
250 250 def _filelog(self):
251 251 return self._repo.file(self._path)
252 252
253 253 @propertycache
254 254 def _changeid(self):
255 255 if '_changectx' in self.__dict__:
256 256 return self._changectx.rev()
257 257 else:
258 258 return self._filelog.linkrev(self._filerev)
259 259
260 260 @propertycache
261 261 def _filenode(self):
262 262 if '_fileid' in self.__dict__:
263 263 return self._filelog.lookup(self._fileid)
264 264 else:
265 265 return self._changectx.filenode(self._path)
266 266
267 267 @propertycache
268 268 def _filerev(self):
269 269 return self._filelog.rev(self._filenode)
270 270
271 271 @propertycache
272 272 def _repopath(self):
273 273 return self._path
274 274
275 275 def __nonzero__(self):
276 276 try:
277 277 self._filenode
278 278 return True
279 279 except error.LookupError:
280 280 # file is missing
281 281 return False
282 282
283 283 def __str__(self):
284 284 return "%s@%s" % (self.path(), short(self.node()))
285 285
286 286 def __repr__(self):
287 287 return "<filectx %s>" % str(self)
288 288
289 289 def __hash__(self):
290 290 try:
291 291 return hash((self._path, self._filenode))
292 292 except AttributeError:
293 293 return id(self)
294 294
295 295 def __eq__(self, other):
296 296 try:
297 297 return (self._path == other._path
298 298 and self._filenode == other._filenode)
299 299 except AttributeError:
300 300 return False
301 301
302 302 def __ne__(self, other):
303 303 return not (self == other)
304 304
305 305 def filectx(self, fileid):
306 306 '''opens an arbitrary revision of the file without
307 307 opening a new filelog'''
308 308 return filectx(self._repo, self._path, fileid=fileid,
309 309 filelog=self._filelog)
310 310
311 311 def filerev(self):
312 312 return self._filerev
313 313 def filenode(self):
314 314 return self._filenode
315 315 def flags(self):
316 316 return self._changectx.flags(self._path)
317 317 def filelog(self):
318 318 return self._filelog
319 319
320 320 def rev(self):
321 321 if '_changectx' in self.__dict__:
322 322 return self._changectx.rev()
323 323 if '_changeid' in self.__dict__:
324 324 return self._changectx.rev()
325 325 return self._filelog.linkrev(self._filerev)
326 326
327 327 def linkrev(self):
328 328 return self._filelog.linkrev(self._filerev)
329 329 def node(self):
330 330 return self._changectx.node()
331 331 def hex(self):
332 332 return hex(self.node())
333 333 def user(self):
334 334 return self._changectx.user()
335 335 def date(self):
336 336 return self._changectx.date()
337 337 def files(self):
338 338 return self._changectx.files()
339 339 def description(self):
340 340 return self._changectx.description()
341 341 def branch(self):
342 342 return self._changectx.branch()
343 343 def extra(self):
344 344 return self._changectx.extra()
345 345 def manifest(self):
346 346 return self._changectx.manifest()
347 347 def changectx(self):
348 348 return self._changectx
349 349
350 350 def data(self):
351 351 return self._filelog.read(self._filenode)
352 352 def path(self):
353 353 return self._path
354 354 def size(self):
355 355 return self._filelog.size(self._filerev)
356 356
357 357 def cmp(self, fctx):
358 358 """compare with other file context
359 359
360 360 returns True if different than fctx.
361 361 """
362 362 if (fctx._filerev is None and self._repo._encodefilterpats
363 363 or self.size() == fctx.size()):
364 364 return self._filelog.cmp(self._filenode, fctx.data())
365 365
366 366 return True
367 367
368 368 def renamed(self):
369 369 """check if file was actually renamed in this changeset revision
370 370
371 371 If rename logged in file revision, we report copy for changeset only
372 372 if file revisions linkrev points back to the changeset in question
373 373 or both changeset parents contain different file revisions.
374 374 """
375 375
376 376 renamed = self._filelog.renamed(self._filenode)
377 377 if not renamed:
378 378 return renamed
379 379
380 380 if self.rev() == self.linkrev():
381 381 return renamed
382 382
383 383 name = self.path()
384 384 fnode = self._filenode
385 385 for p in self._changectx.parents():
386 386 try:
387 387 if fnode == p.filenode(name):
388 388 return None
389 389 except error.LookupError:
390 390 pass
391 391 return renamed
392 392
393 393 def parents(self):
394 394 p = self._path
395 395 fl = self._filelog
396 396 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
397 397
398 398 r = self._filelog.renamed(self._filenode)
399 399 if r:
400 400 pl[0] = (r[0], r[1], None)
401 401
402 402 return [filectx(self._repo, p, fileid=n, filelog=l)
403 403 for p, n, l in pl if n != nullid]
404 404
405 405 def children(self):
406 406 # hard for renames
407 407 c = self._filelog.children(self._filenode)
408 408 return [filectx(self._repo, self._path, fileid=x,
409 409 filelog=self._filelog) for x in c]
410 410
411 411 def annotate(self, follow=False, linenumber=None):
412 412 '''returns a list of tuples of (ctx, line) for each line
413 413 in the file, where ctx is the filectx of the node where
414 414 that line was last changed.
415 415 This returns tuples of ((ctx, linenumber), line) for each line,
416 416 if "linenumber" parameter is NOT "None".
417 417 In such tuples, linenumber means one at the first appearance
418 418 in the managed file.
419 419 To reduce annotation cost,
420 420 this returns fixed value(False is used) as linenumber,
421 421 if "linenumber" parameter is "False".'''
422 422
423 423 def decorate_compat(text, rev):
424 424 return ([rev] * len(text.splitlines()), text)
425 425
426 426 def without_linenumber(text, rev):
427 427 return ([(rev, False)] * len(text.splitlines()), text)
428 428
429 429 def with_linenumber(text, rev):
430 430 size = len(text.splitlines())
431 431 return ([(rev, i) for i in xrange(1, size + 1)], text)
432 432
433 433 decorate = (((linenumber is None) and decorate_compat) or
434 434 (linenumber and with_linenumber) or
435 435 without_linenumber)
436 436
437 437 def pair(parent, child):
438 438 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
439 439 child[0][b1:b2] = parent[0][a1:a2]
440 440 return child
441 441
442 442 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
443 443 def getctx(path, fileid):
444 444 log = path == self._path and self._filelog or getlog(path)
445 445 return filectx(self._repo, path, fileid=fileid, filelog=log)
446 446 getctx = util.lrucachefunc(getctx)
447 447
448 448 def parents(f):
449 449 # we want to reuse filectx objects as much as possible
450 450 p = f._path
451 451 if f._filerev is None: # working dir
452 452 pl = [(n.path(), n.filerev()) for n in f.parents()]
453 453 else:
454 454 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
455 455
456 456 if follow:
457 457 r = f.renamed()
458 458 if r:
459 459 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
460 460
461 461 return [getctx(p, n) for p, n in pl if n != nullrev]
462 462
463 463 # use linkrev to find the first changeset where self appeared
464 464 if self.rev() != self.linkrev():
465 465 base = self.filectx(self.filerev())
466 466 else:
467 467 base = self
468 468
469 469 # find all ancestors
470 470 needed = {base: 1}
471 471 visit = [base]
472 472 files = [base._path]
473 473 while visit:
474 474 f = visit.pop(0)
475 475 for p in parents(f):
476 476 if p not in needed:
477 477 needed[p] = 1
478 478 visit.append(p)
479 479 if p._path not in files:
480 480 files.append(p._path)
481 481 else:
482 482 # count how many times we'll use this
483 483 needed[p] += 1
484 484
485 485 # sort by revision (per file) which is a topological order
486 486 visit = []
487 487 for f in files:
488 488 visit.extend(n for n in needed if n._path == f)
489 489
490 490 hist = {}
491 491 for f in sorted(visit, key=lambda x: x.rev()):
492 492 curr = decorate(f.data(), f)
493 493 for p in parents(f):
494 494 curr = pair(hist[p], curr)
495 495 # trim the history of unneeded revs
496 496 needed[p] -= 1
497 497 if not needed[p]:
498 498 del hist[p]
499 499 hist[f] = curr
500 500
501 501 return zip(hist[f][0], hist[f][1].splitlines(True))
502 502
503 503 def ancestor(self, fc2, actx=None):
504 504 """
505 505 find the common ancestor file context, if any, of self, and fc2
506 506
507 507 If actx is given, it must be the changectx of the common ancestor
508 508 of self's and fc2's respective changesets.
509 509 """
510 510
511 511 if actx is None:
512 512 actx = self.changectx().ancestor(fc2.changectx())
513 513
514 514 # the trivial case: changesets are unrelated, files must be too
515 515 if not actx:
516 516 return None
517 517
518 518 # the easy case: no (relevant) renames
519 519 if fc2.path() == self.path() and self.path() in actx:
520 520 return actx[self.path()]
521 521 acache = {}
522 522
523 523 # prime the ancestor cache for the working directory
524 524 for c in (self, fc2):
525 525 if c._filerev is None:
526 526 pl = [(n.path(), n.filenode()) for n in c.parents()]
527 527 acache[(c._path, None)] = pl
528 528
529 529 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
530 530 def parents(vertex):
531 531 if vertex in acache:
532 532 return acache[vertex]
533 533 f, n = vertex
534 534 if f not in flcache:
535 535 flcache[f] = self._repo.file(f)
536 536 fl = flcache[f]
537 537 pl = [(f, p) for p in fl.parents(n) if p != nullid]
538 538 re = fl.renamed(n)
539 539 if re:
540 540 pl.append(re)
541 541 acache[vertex] = pl
542 542 return pl
543 543
544 544 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
545 545 v = ancestor.ancestor(a, b, parents)
546 546 if v:
547 547 f, n = v
548 548 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
549 549
550 550 return None
551 551
552 552 def ancestors(self):
553 seen = set(str(self))
553 seen = set()
554 554 visit = [self]
555 555 while visit:
556 for parent in visit.pop(0).parents():
556 parents = visit.pop(0).parents()
557 if len(parents) > 1 and parents[1].rev() > parents[0].rev():
558 # make sure we return ancestors in reverse revision order
559 parents = reversed(parents)
560 for parent in parents:
557 561 s = str(parent)
558 562 if s not in seen:
559 563 visit.append(parent)
560 564 seen.add(s)
561 565 yield parent
562 566
563 567 class workingctx(changectx):
564 568 """A workingctx object makes access to data related to
565 569 the current working directory convenient.
566 570 date - any valid date string or (unixtime, offset), or None.
567 571 user - username string, or None.
568 572 extra - a dictionary of extra values, or None.
569 573 changes - a list of file lists as returned by localrepo.status()
570 574 or None to use the repository status.
571 575 """
572 576 def __init__(self, repo, text="", user=None, date=None, extra=None,
573 577 changes=None):
574 578 self._repo = repo
575 579 self._rev = None
576 580 self._node = None
577 581 self._text = text
578 582 if date:
579 583 self._date = util.parsedate(date)
580 584 if user:
581 585 self._user = user
582 586 if changes:
583 587 self._status = list(changes[:4])
584 588 self._unknown = changes[4]
585 589 self._ignored = changes[5]
586 590 self._clean = changes[6]
587 591 else:
588 592 self._unknown = None
589 593 self._ignored = None
590 594 self._clean = None
591 595
592 596 self._extra = {}
593 597 if extra:
594 598 self._extra = extra.copy()
595 599 if 'branch' not in self._extra:
596 600 try:
597 601 branch = encoding.fromlocal(self._repo.dirstate.branch())
598 602 except UnicodeDecodeError:
599 603 raise util.Abort(_('branch name not in UTF-8!'))
600 604 self._extra['branch'] = branch
601 605 if self._extra['branch'] == '':
602 606 self._extra['branch'] = 'default'
603 607
604 608 def __str__(self):
605 609 return str(self._parents[0]) + "+"
606 610
607 611 def __repr__(self):
608 612 return "<workingctx %s>" % str(self)
609 613
610 614 def __nonzero__(self):
611 615 return True
612 616
613 617 def __contains__(self, key):
614 618 return self._repo.dirstate[key] not in "?r"
615 619
616 620 @propertycache
617 621 def _manifest(self):
618 622 """generate a manifest corresponding to the working directory"""
619 623
620 624 if self._unknown is None:
621 625 self.status(unknown=True)
622 626
623 627 man = self._parents[0].manifest().copy()
624 628 copied = self._repo.dirstate.copies()
625 629 if len(self._parents) > 1:
626 630 man2 = self.p2().manifest()
627 631 def getman(f):
628 632 if f in man:
629 633 return man
630 634 return man2
631 635 else:
632 636 getman = lambda f: man
633 637 def cf(f):
634 638 f = copied.get(f, f)
635 639 return getman(f).flags(f)
636 640 ff = self._repo.dirstate.flagfunc(cf)
637 641 modified, added, removed, deleted = self._status
638 642 unknown = self._unknown
639 643 for i, l in (("a", added), ("m", modified), ("u", unknown)):
640 644 for f in l:
641 645 orig = copied.get(f, f)
642 646 man[f] = getman(orig).get(orig, nullid) + i
643 647 try:
644 648 man.set(f, ff(f))
645 649 except OSError:
646 650 pass
647 651
648 652 for f in deleted + removed:
649 653 if f in man:
650 654 del man[f]
651 655
652 656 return man
653 657
654 658 @propertycache
655 659 def _status(self):
656 660 return self._repo.status()[:4]
657 661
658 662 @propertycache
659 663 def _user(self):
660 664 return self._repo.ui.username()
661 665
662 666 @propertycache
663 667 def _date(self):
664 668 return util.makedate()
665 669
666 670 @propertycache
667 671 def _parents(self):
668 672 p = self._repo.dirstate.parents()
669 673 if p[1] == nullid:
670 674 p = p[:-1]
671 675 self._parents = [changectx(self._repo, x) for x in p]
672 676 return self._parents
673 677
674 678 def status(self, ignored=False, clean=False, unknown=False):
675 679 """Explicit status query
676 680 Unless this method is used to query the working copy status, the
677 681 _status property will implicitly read the status using its default
678 682 arguments."""
679 683 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
680 684 self._unknown = self._ignored = self._clean = None
681 685 if unknown:
682 686 self._unknown = stat[4]
683 687 if ignored:
684 688 self._ignored = stat[5]
685 689 if clean:
686 690 self._clean = stat[6]
687 691 self._status = stat[:4]
688 692 return stat
689 693
690 694 def manifest(self):
691 695 return self._manifest
692 696 def user(self):
693 697 return self._user or self._repo.ui.username()
694 698 def date(self):
695 699 return self._date
696 700 def description(self):
697 701 return self._text
698 702 def files(self):
699 703 return sorted(self._status[0] + self._status[1] + self._status[2])
700 704
701 705 def modified(self):
702 706 return self._status[0]
703 707 def added(self):
704 708 return self._status[1]
705 709 def removed(self):
706 710 return self._status[2]
707 711 def deleted(self):
708 712 return self._status[3]
709 713 def unknown(self):
710 714 assert self._unknown is not None # must call status first
711 715 return self._unknown
712 716 def ignored(self):
713 717 assert self._ignored is not None # must call status first
714 718 return self._ignored
715 719 def clean(self):
716 720 assert self._clean is not None # must call status first
717 721 return self._clean
718 722 def branch(self):
719 723 return encoding.tolocal(self._extra['branch'])
720 724 def extra(self):
721 725 return self._extra
722 726
723 727 def tags(self):
724 728 t = []
725 729 for p in self.parents():
726 730 t.extend(p.tags())
727 731 return t
728 732
729 733 def children(self):
730 734 return []
731 735
732 736 def flags(self, path):
733 737 if '_manifest' in self.__dict__:
734 738 try:
735 739 return self._manifest.flags(path)
736 740 except KeyError:
737 741 return ''
738 742
739 743 orig = self._repo.dirstate.copies().get(path, path)
740 744
741 745 def findflag(ctx):
742 746 mnode = ctx.changeset()[0]
743 747 node, flag = self._repo.manifest.find(mnode, orig)
744 748 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
745 749 try:
746 750 return ff(path)
747 751 except OSError:
748 752 pass
749 753
750 754 flag = findflag(self._parents[0])
751 755 if flag is None and len(self.parents()) > 1:
752 756 flag = findflag(self._parents[1])
753 757 if flag is None or self._repo.dirstate[path] == 'r':
754 758 return ''
755 759 return flag
756 760
757 761 def filectx(self, path, filelog=None):
758 762 """get a file context from the working directory"""
759 763 return workingfilectx(self._repo, path, workingctx=self,
760 764 filelog=filelog)
761 765
762 766 def ancestor(self, c2):
763 767 """return the ancestor context of self and c2"""
764 768 return self._parents[0].ancestor(c2) # punt on two parents for now
765 769
766 770 def walk(self, match):
767 771 return sorted(self._repo.dirstate.walk(match, self.substate.keys(),
768 772 True, False))
769 773
770 774 def dirty(self, missing=False):
771 775 "check whether a working directory is modified"
772 776 # check subrepos first
773 777 for s in self.substate:
774 778 if self.sub(s).dirty():
775 779 return True
776 780 # check current working dir
777 781 return (self.p2() or self.branch() != self.p1().branch() or
778 782 self.modified() or self.added() or self.removed() or
779 783 (missing and self.deleted()))
780 784
781 785 def add(self, list, prefix=""):
782 786 join = lambda f: os.path.join(prefix, f)
783 787 wlock = self._repo.wlock()
784 788 ui, ds = self._repo.ui, self._repo.dirstate
785 789 try:
786 790 rejected = []
787 791 for f in list:
788 792 p = self._repo.wjoin(f)
789 793 try:
790 794 st = os.lstat(p)
791 795 except:
792 796 ui.warn(_("%s does not exist!\n") % join(f))
793 797 rejected.append(f)
794 798 continue
795 799 if st.st_size > 10000000:
796 800 ui.warn(_("%s: up to %d MB of RAM may be required "
797 801 "to manage this file\n"
798 802 "(use 'hg revert %s' to cancel the "
799 803 "pending addition)\n")
800 804 % (f, 3 * st.st_size // 1000000, join(f)))
801 805 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
802 806 ui.warn(_("%s not added: only files and symlinks "
803 807 "supported currently\n") % join(f))
804 808 rejected.append(p)
805 809 elif ds[f] in 'amn':
806 810 ui.warn(_("%s already tracked!\n") % join(f))
807 811 elif ds[f] == 'r':
808 812 ds.normallookup(f)
809 813 else:
810 814 ds.add(f)
811 815 return rejected
812 816 finally:
813 817 wlock.release()
814 818
815 819 def forget(self, list):
816 820 wlock = self._repo.wlock()
817 821 try:
818 822 for f in list:
819 823 if self._repo.dirstate[f] != 'a':
820 824 self._repo.ui.warn(_("%s not added!\n") % f)
821 825 else:
822 826 self._repo.dirstate.forget(f)
823 827 finally:
824 828 wlock.release()
825 829
826 830 def ancestors(self):
827 831 for a in self._repo.changelog.ancestors(
828 832 *[p.rev() for p in self._parents]):
829 833 yield changectx(self._repo, a)
830 834
831 835 def remove(self, list, unlink=False):
832 836 if unlink:
833 837 for f in list:
834 838 try:
835 839 util.unlinkpath(self._repo.wjoin(f))
836 840 except OSError, inst:
837 841 if inst.errno != errno.ENOENT:
838 842 raise
839 843 wlock = self._repo.wlock()
840 844 try:
841 845 for f in list:
842 846 if unlink and os.path.lexists(self._repo.wjoin(f)):
843 847 self._repo.ui.warn(_("%s still exists!\n") % f)
844 848 elif self._repo.dirstate[f] == 'a':
845 849 self._repo.dirstate.forget(f)
846 850 elif f not in self._repo.dirstate:
847 851 self._repo.ui.warn(_("%s not tracked!\n") % f)
848 852 else:
849 853 self._repo.dirstate.remove(f)
850 854 finally:
851 855 wlock.release()
852 856
853 857 def undelete(self, list):
854 858 pctxs = self.parents()
855 859 wlock = self._repo.wlock()
856 860 try:
857 861 for f in list:
858 862 if self._repo.dirstate[f] != 'r':
859 863 self._repo.ui.warn(_("%s not removed!\n") % f)
860 864 else:
861 865 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
862 866 t = fctx.data()
863 867 self._repo.wwrite(f, t, fctx.flags())
864 868 self._repo.dirstate.normal(f)
865 869 finally:
866 870 wlock.release()
867 871
868 872 def copy(self, source, dest):
869 873 p = self._repo.wjoin(dest)
870 874 if not os.path.lexists(p):
871 875 self._repo.ui.warn(_("%s does not exist!\n") % dest)
872 876 elif not (os.path.isfile(p) or os.path.islink(p)):
873 877 self._repo.ui.warn(_("copy failed: %s is not a file or a "
874 878 "symbolic link\n") % dest)
875 879 else:
876 880 wlock = self._repo.wlock()
877 881 try:
878 882 if self._repo.dirstate[dest] in '?r':
879 883 self._repo.dirstate.add(dest)
880 884 self._repo.dirstate.copy(source, dest)
881 885 finally:
882 886 wlock.release()
883 887
884 888 class workingfilectx(filectx):
885 889 """A workingfilectx object makes access to data related to a particular
886 890 file in the working directory convenient."""
887 891 def __init__(self, repo, path, filelog=None, workingctx=None):
888 892 """changeid can be a changeset revision, node, or tag.
889 893 fileid can be a file revision or node."""
890 894 self._repo = repo
891 895 self._path = path
892 896 self._changeid = None
893 897 self._filerev = self._filenode = None
894 898
895 899 if filelog:
896 900 self._filelog = filelog
897 901 if workingctx:
898 902 self._changectx = workingctx
899 903
900 904 @propertycache
901 905 def _changectx(self):
902 906 return workingctx(self._repo)
903 907
904 908 def __nonzero__(self):
905 909 return True
906 910
907 911 def __str__(self):
908 912 return "%s@%s" % (self.path(), self._changectx)
909 913
910 914 def __repr__(self):
911 915 return "<workingfilectx %s>" % str(self)
912 916
913 917 def data(self):
914 918 return self._repo.wread(self._path)
915 919 def renamed(self):
916 920 rp = self._repo.dirstate.copied(self._path)
917 921 if not rp:
918 922 return None
919 923 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
920 924
921 925 def parents(self):
922 926 '''return parent filectxs, following copies if necessary'''
923 927 def filenode(ctx, path):
924 928 return ctx._manifest.get(path, nullid)
925 929
926 930 path = self._path
927 931 fl = self._filelog
928 932 pcl = self._changectx._parents
929 933 renamed = self.renamed()
930 934
931 935 if renamed:
932 936 pl = [renamed + (None,)]
933 937 else:
934 938 pl = [(path, filenode(pcl[0], path), fl)]
935 939
936 940 for pc in pcl[1:]:
937 941 pl.append((path, filenode(pc, path), fl))
938 942
939 943 return [filectx(self._repo, p, fileid=n, filelog=l)
940 944 for p, n, l in pl if n != nullid]
941 945
942 946 def children(self):
943 947 return []
944 948
945 949 def size(self):
946 950 return os.lstat(self._repo.wjoin(self._path)).st_size
947 951 def date(self):
948 952 t, tz = self._changectx.date()
949 953 try:
950 954 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
951 955 except OSError, err:
952 956 if err.errno != errno.ENOENT:
953 957 raise
954 958 return (t, tz)
955 959
956 960 def cmp(self, fctx):
957 961 """compare with other file context
958 962
959 963 returns True if different than fctx.
960 964 """
961 965 # fctx should be a filectx (not a wfctx)
962 966 # invert comparison to reuse the same code path
963 967 return fctx.cmp(self)
964 968
965 969 class memctx(object):
966 970 """Use memctx to perform in-memory commits via localrepo.commitctx().
967 971
968 972 Revision information is supplied at initialization time while
969 973 related files data and is made available through a callback
970 974 mechanism. 'repo' is the current localrepo, 'parents' is a
971 975 sequence of two parent revisions identifiers (pass None for every
972 976 missing parent), 'text' is the commit message and 'files' lists
973 977 names of files touched by the revision (normalized and relative to
974 978 repository root).
975 979
976 980 filectxfn(repo, memctx, path) is a callable receiving the
977 981 repository, the current memctx object and the normalized path of
978 982 requested file, relative to repository root. It is fired by the
979 983 commit function for every file in 'files', but calls order is
980 984 undefined. If the file is available in the revision being
981 985 committed (updated or added), filectxfn returns a memfilectx
982 986 object. If the file was removed, filectxfn raises an
983 987 IOError. Moved files are represented by marking the source file
984 988 removed and the new file added with copy information (see
985 989 memfilectx).
986 990
987 991 user receives the committer name and defaults to current
988 992 repository username, date is the commit date in any format
989 993 supported by util.parsedate() and defaults to current date, extra
990 994 is a dictionary of metadata or is left empty.
991 995 """
992 996 def __init__(self, repo, parents, text, files, filectxfn, user=None,
993 997 date=None, extra=None):
994 998 self._repo = repo
995 999 self._rev = None
996 1000 self._node = None
997 1001 self._text = text
998 1002 self._date = date and util.parsedate(date) or util.makedate()
999 1003 self._user = user
1000 1004 parents = [(p or nullid) for p in parents]
1001 1005 p1, p2 = parents
1002 1006 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1003 1007 files = sorted(set(files))
1004 1008 self._status = [files, [], [], [], []]
1005 1009 self._filectxfn = filectxfn
1006 1010
1007 1011 self._extra = extra and extra.copy() or {}
1008 1012 if 'branch' not in self._extra:
1009 1013 self._extra['branch'] = 'default'
1010 1014 elif self._extra.get('branch') == '':
1011 1015 self._extra['branch'] = 'default'
1012 1016
1013 1017 def __str__(self):
1014 1018 return str(self._parents[0]) + "+"
1015 1019
1016 1020 def __int__(self):
1017 1021 return self._rev
1018 1022
1019 1023 def __nonzero__(self):
1020 1024 return True
1021 1025
1022 1026 def __getitem__(self, key):
1023 1027 return self.filectx(key)
1024 1028
1025 1029 def p1(self):
1026 1030 return self._parents[0]
1027 1031 def p2(self):
1028 1032 return self._parents[1]
1029 1033
1030 1034 def user(self):
1031 1035 return self._user or self._repo.ui.username()
1032 1036 def date(self):
1033 1037 return self._date
1034 1038 def description(self):
1035 1039 return self._text
1036 1040 def files(self):
1037 1041 return self.modified()
1038 1042 def modified(self):
1039 1043 return self._status[0]
1040 1044 def added(self):
1041 1045 return self._status[1]
1042 1046 def removed(self):
1043 1047 return self._status[2]
1044 1048 def deleted(self):
1045 1049 return self._status[3]
1046 1050 def unknown(self):
1047 1051 return self._status[4]
1048 1052 def ignored(self):
1049 1053 return self._status[5]
1050 1054 def clean(self):
1051 1055 return self._status[6]
1052 1056 def branch(self):
1053 1057 return encoding.tolocal(self._extra['branch'])
1054 1058 def extra(self):
1055 1059 return self._extra
1056 1060 def flags(self, f):
1057 1061 return self[f].flags()
1058 1062
1059 1063 def parents(self):
1060 1064 """return contexts for each parent changeset"""
1061 1065 return self._parents
1062 1066
1063 1067 def filectx(self, path, filelog=None):
1064 1068 """get a file context from the working directory"""
1065 1069 return self._filectxfn(self._repo, self, path)
1066 1070
1067 1071 def commit(self):
1068 1072 """commit context to the repo"""
1069 1073 return self._repo.commitctx(self)
1070 1074
1071 1075 class memfilectx(object):
1072 1076 """memfilectx represents an in-memory file to commit.
1073 1077
1074 1078 See memctx for more details.
1075 1079 """
1076 1080 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1077 1081 """
1078 1082 path is the normalized file path relative to repository root.
1079 1083 data is the file content as a string.
1080 1084 islink is True if the file is a symbolic link.
1081 1085 isexec is True if the file is executable.
1082 1086 copied is the source file path if current file was copied in the
1083 1087 revision being committed, or None."""
1084 1088 self._path = path
1085 1089 self._data = data
1086 1090 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1087 1091 self._copied = None
1088 1092 if copied:
1089 1093 self._copied = (copied, nullid)
1090 1094
1091 1095 def __nonzero__(self):
1092 1096 return True
1093 1097 def __str__(self):
1094 1098 return "%s@%s" % (self.path(), self._changectx)
1095 1099 def path(self):
1096 1100 return self._path
1097 1101 def data(self):
1098 1102 return self._data
1099 1103 def flags(self):
1100 1104 return self._flags
1101 1105 def isexec(self):
1102 1106 return 'x' in self._flags
1103 1107 def islink(self):
1104 1108 return 'l' in self._flags
1105 1109 def renamed(self):
1106 1110 return self._copied
@@ -1,101 +1,127
1 1 $ hg init
2 2
3 3 $ echo "[merge]" >> .hg/hgrc
4 4 $ echo "followcopies = 1" >> .hg/hgrc
5 5
6 6 $ echo foo > a
7 7 $ echo foo > a2
8 8 $ hg add a a2
9 9 $ hg ci -m "start"
10 10
11 11 $ hg mv a b
12 12 $ hg mv a2 b2
13 13 $ hg ci -m "rename"
14 14
15 15 $ hg co 0
16 16 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
17 17
18 18 $ echo blahblah > a
19 19 $ echo blahblah > a2
20 20 $ hg mv a2 c2
21 21 $ hg ci -m "modify"
22 22 created new head
23 23
24 24 $ hg merge -y --debug
25 25 searching for copies back to rev 1
26 26 unmatched files in local:
27 27 c2
28 28 unmatched files in other:
29 29 b
30 30 b2
31 31 all copies found (* = to merge, ! = divergent):
32 32 c2 -> a2 !
33 33 b -> a *
34 34 b2 -> a2 !
35 35 checking for directory renames
36 36 a2: divergent renames -> dr
37 37 resolving manifests
38 38 overwrite None partial False
39 39 ancestor af1939970a1c local 044f8520aeeb+ remote 85c198ef2f6c
40 40 a: remote moved to b -> m
41 41 b2: remote created -> g
42 42 preserving a for resolve of b
43 43 removing a
44 44 updating: a 1/3 files (33.33%)
45 45 picked tool 'internal:merge' for b (binary False symlink False)
46 46 merging a and b to b
47 47 my b@044f8520aeeb+ other b@85c198ef2f6c ancestor a@af1939970a1c
48 48 premerge successful
49 49 updating: a2 2/3 files (66.67%)
50 50 note: possible conflict - a2 was renamed multiple times to:
51 51 c2
52 52 b2
53 53 updating: b2 3/3 files (100.00%)
54 54 getting b2
55 55 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
56 56 (branch merge, don't forget to commit)
57 57
58 58 $ hg status -AC
59 59 M b
60 60 a
61 61 M b2
62 62 R a
63 63 C c2
64 64
65 65 $ cat b
66 66 blahblah
67 67
68 68 $ hg ci -m "merge"
69 69
70 70 $ hg debugindex .hg/store/data/b.i
71 71 rev offset length base linkrev nodeid p1 p2
72 72 0 0 67 0 1 57eacc201a7f 000000000000 000000000000
73 73 1 67 72 1 3 4727ba907962 000000000000 57eacc201a7f
74 74
75 75 $ hg debugrename b
76 76 b renamed from a:dd03b83622e78778b403775d0d074b9ac7387a66
77 77
78 78 This used to trigger a "divergent renames" warning, despite no renames
79 79
80 80 $ hg cp b b3
81 81 $ hg cp b b4
82 82 $ hg ci -A -m 'copy b twice'
83 83 $ hg up eb92d88a9712
84 84 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
85 85 $ hg up
86 86 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 87 $ hg rm b3 b4
88 88 $ hg ci -m 'clean up a bit of our mess'
89 89
90 90 We'd rather not warn on divergent renames done in the same changeset (issue2113)
91 91
92 92 $ hg cp b b3
93 93 $ hg mv b b4
94 94 $ hg ci -A -m 'divergent renames in same changeset'
95 95 $ hg up c761c6948de0
96 96 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
97 97 $ hg up
98 98 note: possible conflict - b was renamed multiple times to:
99 99 b3
100 100 b4
101 101 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
102
103 Check for issue2642
104
105 $ hg init t
106 $ cd t
107
108 $ echo c0 > f1
109 $ hg ci -Aqm0
110
111 $ hg up null -q
112 $ echo c1 > f1 # backport
113 $ hg ci -Aqm1
114 $ hg mv f1 f2
115 $ hg ci -qm2
116
117 $ hg up 0 -q
118 $ hg merge 1 -q --tool internal:local
119 $ hg ci -qm3
120
121 $ hg merge 2
122 merging f1 and f2 to f2
123 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
124 (branch merge, don't forget to commit)
125
126 $ cat f2
127 c0
General Comments 0
You need to be logged in to leave comments. Login now