##// END OF EJS Templates
copies: speed up copy detection...
Matt Mackall -
r10262:eb243551 stable
parent child Browse files
Show More
@@ -1,832 +1,843
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, incorporated herein by reference.
7 7
8 8 from node import nullid, nullrev, short, hex
9 9 from i18n import _
10 10 import ancestor, bdiff, error, util, subrepo
11 11 import os, errno
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)
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): return self._changeset
91 91 def manifest(self): return self._manifest
92 92 def manifestnode(self): return self._changeset[0]
93 93
94 94 def rev(self): return self._rev
95 95 def node(self): return self._node
96 96 def hex(self): return hex(self._node)
97 97 def user(self): return self._changeset[1]
98 98 def date(self): return self._changeset[2]
99 99 def files(self): return self._changeset[3]
100 100 def description(self): return self._changeset[4]
101 101 def branch(self): return self._changeset[5].get("branch")
102 102 def extra(self): return self._changeset[5]
103 103 def tags(self): return self._repo.nodetags(self._node)
104 104
105 105 def parents(self):
106 106 """return contexts for each parent changeset"""
107 107 return self._parents
108 108
109 109 def p1(self):
110 110 return self._parents[0]
111 111
112 112 def p2(self):
113 113 if len(self._parents) == 2:
114 114 return self._parents[1]
115 115 return changectx(self._repo, -1)
116 116
117 117 def children(self):
118 118 """return contexts for each child changeset"""
119 119 c = self._repo.changelog.children(self._node)
120 120 return [changectx(self._repo, x) for x in c]
121 121
122 122 def ancestors(self):
123 123 for a in self._repo.changelog.ancestors(self._rev):
124 124 yield changectx(self._repo, a)
125 125
126 126 def descendants(self):
127 127 for d in self._repo.changelog.descendants(self._rev):
128 128 yield changectx(self._repo, d)
129 129
130 130 def _fileinfo(self, path):
131 131 if '_manifest' in self.__dict__:
132 132 try:
133 133 return self._manifest[path], self._manifest.flags(path)
134 134 except KeyError:
135 135 raise error.LookupError(self._node, path,
136 136 _('not found in manifest'))
137 137 if '_manifestdelta' in self.__dict__ or path in self.files():
138 138 if path in self._manifestdelta:
139 139 return self._manifestdelta[path], self._manifestdelta.flags(path)
140 140 node, flag = self._repo.manifest.find(self._changeset[0], path)
141 141 if not node:
142 142 raise error.LookupError(self._node, path,
143 143 _('not found in manifest'))
144 144
145 145 return node, flag
146 146
147 147 def filenode(self, path):
148 148 return self._fileinfo(path)[0]
149 149
150 150 def flags(self, path):
151 151 try:
152 152 return self._fileinfo(path)[1]
153 153 except error.LookupError:
154 154 return ''
155 155
156 156 def filectx(self, path, fileid=None, filelog=None):
157 157 """get a file context from this changeset"""
158 158 if fileid is None:
159 159 fileid = self.filenode(path)
160 160 return filectx(self._repo, path, fileid=fileid,
161 161 changectx=self, filelog=filelog)
162 162
163 163 def ancestor(self, c2):
164 164 """
165 165 return the ancestor context of self and c2
166 166 """
167 167 # deal with workingctxs
168 168 n2 = c2._node
169 169 if n2 == None:
170 170 n2 = c2._parents[0]._node
171 171 n = self._repo.changelog.ancestor(self._node, n2)
172 172 return changectx(self._repo, n)
173 173
174 174 def walk(self, match):
175 175 fset = set(match.files())
176 176 # for dirstate.walk, files=['.'] means "walk the whole tree".
177 177 # follow that here, too
178 178 fset.discard('.')
179 179 for fn in self:
180 180 for ffn in fset:
181 181 # match if the file is the exact name or a directory
182 182 if ffn == fn or fn.startswith("%s/" % ffn):
183 183 fset.remove(ffn)
184 184 break
185 185 if match(fn):
186 186 yield fn
187 187 for fn in sorted(fset):
188 188 if match.bad(fn, 'No such file in rev ' + str(self)) and match(fn):
189 189 yield fn
190 190
191 191 def sub(self, path):
192 192 return subrepo.subrepo(self, path)
193 193
194 194 class filectx(object):
195 195 """A filecontext object makes access to data related to a particular
196 196 filerevision convenient."""
197 197 def __init__(self, repo, path, changeid=None, fileid=None,
198 198 filelog=None, changectx=None):
199 199 """changeid can be a changeset revision, node, or tag.
200 200 fileid can be a file revision or node."""
201 201 self._repo = repo
202 202 self._path = path
203 203
204 204 assert (changeid is not None
205 205 or fileid is not None
206 206 or changectx is not None), \
207 207 ("bad args: changeid=%r, fileid=%r, changectx=%r"
208 208 % (changeid, fileid, changectx))
209 209
210 210 if filelog:
211 211 self._filelog = filelog
212 212
213 213 if changeid is not None:
214 214 self._changeid = changeid
215 215 if changectx is not None:
216 216 self._changectx = changectx
217 217 if fileid is not None:
218 218 self._fileid = fileid
219 219
220 220 @propertycache
221 221 def _changectx(self):
222 222 return changectx(self._repo, self._changeid)
223 223
224 224 @propertycache
225 225 def _filelog(self):
226 226 return self._repo.file(self._path)
227 227
228 228 @propertycache
229 229 def _changeid(self):
230 230 if '_changectx' in self.__dict__:
231 231 return self._changectx.rev()
232 232 else:
233 233 return self._filelog.linkrev(self._filerev)
234 234
235 235 @propertycache
236 236 def _filenode(self):
237 237 if '_fileid' in self.__dict__:
238 238 return self._filelog.lookup(self._fileid)
239 239 else:
240 240 return self._changectx.filenode(self._path)
241 241
242 242 @propertycache
243 243 def _filerev(self):
244 244 return self._filelog.rev(self._filenode)
245 245
246 246 @propertycache
247 247 def _repopath(self):
248 248 return self._path
249 249
250 250 def __nonzero__(self):
251 251 try:
252 252 self._filenode
253 253 return True
254 254 except error.LookupError:
255 255 # file is missing
256 256 return False
257 257
258 258 def __str__(self):
259 259 return "%s@%s" % (self.path(), short(self.node()))
260 260
261 261 def __repr__(self):
262 262 return "<filectx %s>" % str(self)
263 263
264 264 def __hash__(self):
265 265 try:
266 266 return hash((self._path, self._fileid))
267 267 except AttributeError:
268 268 return id(self)
269 269
270 270 def __eq__(self, other):
271 271 try:
272 272 return (self._path == other._path
273 273 and self._fileid == other._fileid)
274 274 except AttributeError:
275 275 return False
276 276
277 277 def __ne__(self, other):
278 278 return not (self == other)
279 279
280 280 def filectx(self, fileid):
281 281 '''opens an arbitrary revision of the file without
282 282 opening a new filelog'''
283 283 return filectx(self._repo, self._path, fileid=fileid,
284 284 filelog=self._filelog)
285 285
286 286 def filerev(self): return self._filerev
287 287 def filenode(self): return self._filenode
288 288 def flags(self): return self._changectx.flags(self._path)
289 289 def filelog(self): return self._filelog
290 290
291 291 def rev(self):
292 292 if '_changectx' in self.__dict__:
293 293 return self._changectx.rev()
294 294 if '_changeid' in self.__dict__:
295 295 return self._changectx.rev()
296 296 return self._filelog.linkrev(self._filerev)
297 297
298 298 def linkrev(self): return self._filelog.linkrev(self._filerev)
299 299 def node(self): return self._changectx.node()
300 300 def hex(self): return hex(self.node())
301 301 def user(self): return self._changectx.user()
302 302 def date(self): return self._changectx.date()
303 303 def files(self): return self._changectx.files()
304 304 def description(self): return self._changectx.description()
305 305 def branch(self): return self._changectx.branch()
306 306 def extra(self): return self._changectx.extra()
307 307 def manifest(self): return self._changectx.manifest()
308 308 def changectx(self): return self._changectx
309 309
310 310 def data(self): return self._filelog.read(self._filenode)
311 311 def path(self): return self._path
312 312 def size(self): return self._filelog.size(self._filerev)
313 313
314 314 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
315 315
316 316 def renamed(self):
317 317 """check if file was actually renamed in this changeset revision
318 318
319 319 If rename logged in file revision, we report copy for changeset only
320 320 if file revisions linkrev points back to the changeset in question
321 321 or both changeset parents contain different file revisions.
322 322 """
323 323
324 324 renamed = self._filelog.renamed(self._filenode)
325 325 if not renamed:
326 326 return renamed
327 327
328 328 if self.rev() == self.linkrev():
329 329 return renamed
330 330
331 331 name = self.path()
332 332 fnode = self._filenode
333 333 for p in self._changectx.parents():
334 334 try:
335 335 if fnode == p.filenode(name):
336 336 return None
337 337 except error.LookupError:
338 338 pass
339 339 return renamed
340 340
341 341 def parents(self):
342 342 p = self._path
343 343 fl = self._filelog
344 344 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
345 345
346 346 r = self._filelog.renamed(self._filenode)
347 347 if r:
348 348 pl[0] = (r[0], r[1], None)
349 349
350 350 return [filectx(self._repo, p, fileid=n, filelog=l)
351 351 for p,n,l in pl if n != nullid]
352 352
353 353 def children(self):
354 354 # hard for renames
355 355 c = self._filelog.children(self._filenode)
356 356 return [filectx(self._repo, self._path, fileid=x,
357 357 filelog=self._filelog) for x in c]
358 358
359 359 def annotate(self, follow=False, linenumber=None):
360 360 '''returns a list of tuples of (ctx, line) for each line
361 361 in the file, where ctx is the filectx of the node where
362 362 that line was last changed.
363 363 This returns tuples of ((ctx, linenumber), line) for each line,
364 364 if "linenumber" parameter is NOT "None".
365 365 In such tuples, linenumber means one at the first appearance
366 366 in the managed file.
367 367 To reduce annotation cost,
368 368 this returns fixed value(False is used) as linenumber,
369 369 if "linenumber" parameter is "False".'''
370 370
371 371 def decorate_compat(text, rev):
372 372 return ([rev] * len(text.splitlines()), text)
373 373
374 374 def without_linenumber(text, rev):
375 375 return ([(rev, False)] * len(text.splitlines()), text)
376 376
377 377 def with_linenumber(text, rev):
378 378 size = len(text.splitlines())
379 379 return ([(rev, i) for i in xrange(1, size + 1)], text)
380 380
381 381 decorate = (((linenumber is None) and decorate_compat) or
382 382 (linenumber and with_linenumber) or
383 383 without_linenumber)
384 384
385 385 def pair(parent, child):
386 386 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
387 387 child[0][b1:b2] = parent[0][a1:a2]
388 388 return child
389 389
390 390 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
391 391 def getctx(path, fileid):
392 392 log = path == self._path and self._filelog or getlog(path)
393 393 return filectx(self._repo, path, fileid=fileid, filelog=log)
394 394 getctx = util.lrucachefunc(getctx)
395 395
396 396 def parents(f):
397 397 # we want to reuse filectx objects as much as possible
398 398 p = f._path
399 399 if f._filerev is None: # working dir
400 400 pl = [(n.path(), n.filerev()) for n in f.parents()]
401 401 else:
402 402 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
403 403
404 404 if follow:
405 405 r = f.renamed()
406 406 if r:
407 407 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
408 408
409 409 return [getctx(p, n) for p, n in pl if n != nullrev]
410 410
411 411 # use linkrev to find the first changeset where self appeared
412 412 if self.rev() != self.linkrev():
413 413 base = self.filectx(self.filerev())
414 414 else:
415 415 base = self
416 416
417 417 # find all ancestors
418 418 needed = {base: 1}
419 419 visit = [base]
420 420 files = [base._path]
421 421 while visit:
422 422 f = visit.pop(0)
423 423 for p in parents(f):
424 424 if p not in needed:
425 425 needed[p] = 1
426 426 visit.append(p)
427 427 if p._path not in files:
428 428 files.append(p._path)
429 429 else:
430 430 # count how many times we'll use this
431 431 needed[p] += 1
432 432
433 433 # sort by revision (per file) which is a topological order
434 434 visit = []
435 435 for f in files:
436 436 fn = [(n.rev(), n) for n in needed if n._path == f]
437 437 visit.extend(fn)
438 438
439 439 hist = {}
440 440 for r, f in sorted(visit):
441 441 curr = decorate(f.data(), f)
442 442 for p in parents(f):
443 443 if p != nullid:
444 444 curr = pair(hist[p], curr)
445 445 # trim the history of unneeded revs
446 446 needed[p] -= 1
447 447 if not needed[p]:
448 448 del hist[p]
449 449 hist[f] = curr
450 450
451 451 return zip(hist[f][0], hist[f][1].splitlines(True))
452 452
453 453 def ancestor(self, fc2):
454 454 """
455 455 find the common ancestor file context, if any, of self, and fc2
456 456 """
457 457
458 458 actx = self.changectx().ancestor(fc2.changectx())
459 459
460 460 # the trivial case: changesets are unrelated, files must be too
461 461 if not actx:
462 462 return None
463 463
464 464 # the easy case: no (relevant) renames
465 465 if fc2.path() == self.path() and self.path() in actx:
466 466 return actx[self.path()]
467 467 acache = {}
468 468
469 469 # prime the ancestor cache for the working directory
470 470 for c in (self, fc2):
471 471 if c._filerev is None:
472 472 pl = [(n.path(), n.filenode()) for n in c.parents()]
473 473 acache[(c._path, None)] = pl
474 474
475 475 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
476 476 def parents(vertex):
477 477 if vertex in acache:
478 478 return acache[vertex]
479 479 f, n = vertex
480 480 if f not in flcache:
481 481 flcache[f] = self._repo.file(f)
482 482 fl = flcache[f]
483 483 pl = [(f, p) for p in fl.parents(n) if p != nullid]
484 484 re = fl.renamed(n)
485 485 if re:
486 486 pl.append(re)
487 487 acache[vertex] = pl
488 488 return pl
489 489
490 490 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
491 491 v = ancestor.ancestor(a, b, parents)
492 492 if v:
493 493 f, n = v
494 494 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
495 495
496 496 return None
497 497
498 def ancestors(self):
499 seen = set(str(self))
500 visit = [self]
501 while visit:
502 for parent in visit.pop(0).parents():
503 s = str(parent)
504 if s not in seen:
505 visit.append(parent)
506 seen.add(s)
507 yield parent
508
498 509 class workingctx(changectx):
499 510 """A workingctx object makes access to data related to
500 511 the current working directory convenient.
501 512 parents - a pair of parent nodeids, or None to use the dirstate.
502 513 date - any valid date string or (unixtime, offset), or None.
503 514 user - username string, or None.
504 515 extra - a dictionary of extra values, or None.
505 516 changes - a list of file lists as returned by localrepo.status()
506 517 or None to use the repository status.
507 518 """
508 519 def __init__(self, repo, parents=None, text="", user=None, date=None,
509 520 extra=None, changes=None):
510 521 self._repo = repo
511 522 self._rev = None
512 523 self._node = None
513 524 self._text = text
514 525 if date:
515 526 self._date = util.parsedate(date)
516 527 if user:
517 528 self._user = user
518 529 if parents:
519 530 self._parents = [changectx(self._repo, p) for p in parents]
520 531 if changes:
521 532 self._status = list(changes)
522 533
523 534 self._extra = {}
524 535 if extra:
525 536 self._extra = extra.copy()
526 537 if 'branch' not in self._extra:
527 538 branch = self._repo.dirstate.branch()
528 539 try:
529 540 branch = branch.decode('UTF-8').encode('UTF-8')
530 541 except UnicodeDecodeError:
531 542 raise util.Abort(_('branch name not in UTF-8!'))
532 543 self._extra['branch'] = branch
533 544 if self._extra['branch'] == '':
534 545 self._extra['branch'] = 'default'
535 546
536 547 def __str__(self):
537 548 return str(self._parents[0]) + "+"
538 549
539 550 def __nonzero__(self):
540 551 return True
541 552
542 553 def __contains__(self, key):
543 554 return self._repo.dirstate[key] not in "?r"
544 555
545 556 @propertycache
546 557 def _manifest(self):
547 558 """generate a manifest corresponding to the working directory"""
548 559
549 560 man = self._parents[0].manifest().copy()
550 561 copied = self._repo.dirstate.copies()
551 562 cf = lambda x: man.flags(copied.get(x, x))
552 563 ff = self._repo.dirstate.flagfunc(cf)
553 564 modified, added, removed, deleted, unknown = self._status[:5]
554 565 for i, l in (("a", added), ("m", modified), ("u", unknown)):
555 566 for f in l:
556 567 man[f] = man.get(copied.get(f, f), nullid) + i
557 568 try:
558 569 man.set(f, ff(f))
559 570 except OSError:
560 571 pass
561 572
562 573 for f in deleted + removed:
563 574 if f in man:
564 575 del man[f]
565 576
566 577 return man
567 578
568 579 @propertycache
569 580 def _status(self):
570 581 return self._repo.status(unknown=True)
571 582
572 583 @propertycache
573 584 def _user(self):
574 585 return self._repo.ui.username()
575 586
576 587 @propertycache
577 588 def _date(self):
578 589 return util.makedate()
579 590
580 591 @propertycache
581 592 def _parents(self):
582 593 p = self._repo.dirstate.parents()
583 594 if p[1] == nullid:
584 595 p = p[:-1]
585 596 self._parents = [changectx(self._repo, x) for x in p]
586 597 return self._parents
587 598
588 599 def manifest(self): return self._manifest
589 600
590 601 def user(self): return self._user or self._repo.ui.username()
591 602 def date(self): return self._date
592 603 def description(self): return self._text
593 604 def files(self):
594 605 return sorted(self._status[0] + self._status[1] + self._status[2])
595 606
596 607 def modified(self): return self._status[0]
597 608 def added(self): return self._status[1]
598 609 def removed(self): return self._status[2]
599 610 def deleted(self): return self._status[3]
600 611 def unknown(self): return self._status[4]
601 612 def clean(self): return self._status[5]
602 613 def branch(self): return self._extra['branch']
603 614 def extra(self): return self._extra
604 615
605 616 def tags(self):
606 617 t = []
607 618 [t.extend(p.tags()) for p in self.parents()]
608 619 return t
609 620
610 621 def children(self):
611 622 return []
612 623
613 624 def flags(self, path):
614 625 if '_manifest' in self.__dict__:
615 626 try:
616 627 return self._manifest.flags(path)
617 628 except KeyError:
618 629 return ''
619 630
620 631 pnode = self._parents[0].changeset()[0]
621 632 orig = self._repo.dirstate.copies().get(path, path)
622 633 node, flag = self._repo.manifest.find(pnode, orig)
623 634 try:
624 635 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
625 636 return ff(path)
626 637 except OSError:
627 638 pass
628 639
629 640 if not node or path in self.deleted() or path in self.removed():
630 641 return ''
631 642 return flag
632 643
633 644 def filectx(self, path, filelog=None):
634 645 """get a file context from the working directory"""
635 646 return workingfilectx(self._repo, path, workingctx=self,
636 647 filelog=filelog)
637 648
638 649 def ancestor(self, c2):
639 650 """return the ancestor context of self and c2"""
640 651 return self._parents[0].ancestor(c2) # punt on two parents for now
641 652
642 653 def walk(self, match):
643 654 return sorted(self._repo.dirstate.walk(match, True, False))
644 655
645 656 def dirty(self, missing=False):
646 657 "check whether a working directory is modified"
647 658
648 659 return (self.p2() or self.branch() != self.p1().branch() or
649 660 self.modified() or self.added() or self.removed() or
650 661 (missing and self.deleted()))
651 662
652 663 class workingfilectx(filectx):
653 664 """A workingfilectx object makes access to data related to a particular
654 665 file in the working directory convenient."""
655 666 def __init__(self, repo, path, filelog=None, workingctx=None):
656 667 """changeid can be a changeset revision, node, or tag.
657 668 fileid can be a file revision or node."""
658 669 self._repo = repo
659 670 self._path = path
660 671 self._changeid = None
661 672 self._filerev = self._filenode = None
662 673
663 674 if filelog:
664 675 self._filelog = filelog
665 676 if workingctx:
666 677 self._changectx = workingctx
667 678
668 679 @propertycache
669 680 def _changectx(self):
670 681 return workingctx(self._repo)
671 682
672 683 def __nonzero__(self):
673 684 return True
674 685
675 686 def __str__(self):
676 687 return "%s@%s" % (self.path(), self._changectx)
677 688
678 689 def data(self): return self._repo.wread(self._path)
679 690 def renamed(self):
680 691 rp = self._repo.dirstate.copied(self._path)
681 692 if not rp:
682 693 return None
683 694 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
684 695
685 696 def parents(self):
686 697 '''return parent filectxs, following copies if necessary'''
687 698 def filenode(ctx, path):
688 699 return ctx._manifest.get(path, nullid)
689 700
690 701 path = self._path
691 702 fl = self._filelog
692 703 pcl = self._changectx._parents
693 704 renamed = self.renamed()
694 705
695 706 if renamed:
696 707 pl = [renamed + (None,)]
697 708 else:
698 709 pl = [(path, filenode(pcl[0], path), fl)]
699 710
700 711 for pc in pcl[1:]:
701 712 pl.append((path, filenode(pc, path), fl))
702 713
703 714 return [filectx(self._repo, p, fileid=n, filelog=l)
704 715 for p,n,l in pl if n != nullid]
705 716
706 717 def children(self):
707 718 return []
708 719
709 720 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
710 721 def date(self):
711 722 t, tz = self._changectx.date()
712 723 try:
713 724 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
714 725 except OSError, err:
715 726 if err.errno != errno.ENOENT: raise
716 727 return (t, tz)
717 728
718 729 def cmp(self, text): return self._repo.wread(self._path) == text
719 730
720 731 class memctx(object):
721 732 """Use memctx to perform in-memory commits via localrepo.commitctx().
722 733
723 734 Revision information is supplied at initialization time while
724 735 related files data and is made available through a callback
725 736 mechanism. 'repo' is the current localrepo, 'parents' is a
726 737 sequence of two parent revisions identifiers (pass None for every
727 738 missing parent), 'text' is the commit message and 'files' lists
728 739 names of files touched by the revision (normalized and relative to
729 740 repository root).
730 741
731 742 filectxfn(repo, memctx, path) is a callable receiving the
732 743 repository, the current memctx object and the normalized path of
733 744 requested file, relative to repository root. It is fired by the
734 745 commit function for every file in 'files', but calls order is
735 746 undefined. If the file is available in the revision being
736 747 committed (updated or added), filectxfn returns a memfilectx
737 748 object. If the file was removed, filectxfn raises an
738 749 IOError. Moved files are represented by marking the source file
739 750 removed and the new file added with copy information (see
740 751 memfilectx).
741 752
742 753 user receives the committer name and defaults to current
743 754 repository username, date is the commit date in any format
744 755 supported by util.parsedate() and defaults to current date, extra
745 756 is a dictionary of metadata or is left empty.
746 757 """
747 758 def __init__(self, repo, parents, text, files, filectxfn, user=None,
748 759 date=None, extra=None):
749 760 self._repo = repo
750 761 self._rev = None
751 762 self._node = None
752 763 self._text = text
753 764 self._date = date and util.parsedate(date) or util.makedate()
754 765 self._user = user
755 766 parents = [(p or nullid) for p in parents]
756 767 p1, p2 = parents
757 768 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
758 769 files = sorted(set(files))
759 770 self._status = [files, [], [], [], []]
760 771 self._filectxfn = filectxfn
761 772
762 773 self._extra = extra and extra.copy() or {}
763 774 if 'branch' not in self._extra:
764 775 self._extra['branch'] = 'default'
765 776 elif self._extra.get('branch') == '':
766 777 self._extra['branch'] = 'default'
767 778
768 779 def __str__(self):
769 780 return str(self._parents[0]) + "+"
770 781
771 782 def __int__(self):
772 783 return self._rev
773 784
774 785 def __nonzero__(self):
775 786 return True
776 787
777 788 def __getitem__(self, key):
778 789 return self.filectx(key)
779 790
780 791 def p1(self): return self._parents[0]
781 792 def p2(self): return self._parents[1]
782 793
783 794 def user(self): return self._user or self._repo.ui.username()
784 795 def date(self): return self._date
785 796 def description(self): return self._text
786 797 def files(self): return self.modified()
787 798 def modified(self): return self._status[0]
788 799 def added(self): return self._status[1]
789 800 def removed(self): return self._status[2]
790 801 def deleted(self): return self._status[3]
791 802 def unknown(self): return self._status[4]
792 803 def clean(self): return self._status[5]
793 804 def branch(self): return self._extra['branch']
794 805 def extra(self): return self._extra
795 806 def flags(self, f): return self[f].flags()
796 807
797 808 def parents(self):
798 809 """return contexts for each parent changeset"""
799 810 return self._parents
800 811
801 812 def filectx(self, path, filelog=None):
802 813 """get a file context from the working directory"""
803 814 return self._filectxfn(self._repo, self, path)
804 815
805 816 class memfilectx(object):
806 817 """memfilectx represents an in-memory file to commit.
807 818
808 819 See memctx for more details.
809 820 """
810 821 def __init__(self, path, data, islink, isexec, copied):
811 822 """
812 823 path is the normalized file path relative to repository root.
813 824 data is the file content as a string.
814 825 islink is True if the file is a symbolic link.
815 826 isexec is True if the file is executable.
816 827 copied is the source file path if current file was copied in the
817 828 revision being committed, or None."""
818 829 self._path = path
819 830 self._data = data
820 831 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
821 832 self._copied = None
822 833 if copied:
823 834 self._copied = (copied, nullid)
824 835
825 836 def __nonzero__(self): return True
826 837 def __str__(self): return "%s@%s" % (self.path(), self._changectx)
827 838 def path(self): return self._path
828 839 def data(self): return self._data
829 840 def flags(self): return self._flags
830 841 def isexec(self): return 'x' in self._flags
831 842 def islink(self): return 'l' in self._flags
832 843 def renamed(self): return self._copied
@@ -1,245 +1,251
1 1 # copies.py - copy detection for Mercurial
2 2 #
3 3 # Copyright 2008 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, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 import util
10 10 import heapq
11 11
12 12 def _nonoverlap(d1, d2, d3):
13 13 "Return list of elements in d1 not in d2 or d3"
14 14 return sorted([d for d in d1 if d not in d3 and d not in d2])
15 15
16 16 def _dirname(f):
17 17 s = f.rfind("/")
18 18 if s == -1:
19 19 return ""
20 20 return f[:s]
21 21
22 22 def _dirs(files):
23 23 d = set()
24 24 for f in files:
25 25 f = _dirname(f)
26 26 while f not in d:
27 27 d.add(f)
28 28 f = _dirname(f)
29 29 return d
30 30
31 def _findoldnames(fctx, limit):
32 "find files that path was copied from, back to linkrev limit"
33 old = {}
34 seen = set()
35 orig = fctx.path()
36 visit = [(fctx, 0)]
37 while visit:
38 fc, depth = visit.pop()
39 s = str(fc)
40 if s in seen:
41 continue
42 seen.add(s)
43 if fc.path() != orig and fc.path() not in old:
44 old[fc.path()] = (depth, fc.path()) # remember depth
45 if fc.rev() is not None and fc.rev() < limit:
46 continue
47 visit += [(p, depth - 1) for p in fc.parents()]
48
49 # return old names sorted by depth
50 return [o[1] for o in sorted(old.values())]
51
52 31 def _findlimit(repo, a, b):
53 32 """Find the earliest revision that's an ancestor of a or b but not both,
54 33 None if no such revision exists.
55 34 """
56 35 # basic idea:
57 36 # - mark a and b with different sides
58 37 # - if a parent's children are all on the same side, the parent is
59 38 # on that side, otherwise it is on no side
60 39 # - walk the graph in topological order with the help of a heap;
61 40 # - add unseen parents to side map
62 41 # - clear side of any parent that has children on different sides
63 42 # - track number of interesting revs that might still be on a side
64 43 # - track the lowest interesting rev seen
65 44 # - quit when interesting revs is zero
66 45
67 46 cl = repo.changelog
68 47 working = len(cl) # pseudo rev for the working directory
69 48 if a is None:
70 49 a = working
71 50 if b is None:
72 51 b = working
73 52
74 53 side = {a: -1, b: 1}
75 54 visit = [-a, -b]
76 55 heapq.heapify(visit)
77 56 interesting = len(visit)
78 57 hascommonancestor = False
79 58 limit = working
80 59
81 60 while interesting:
82 61 r = -heapq.heappop(visit)
83 62 if r == working:
84 63 parents = [cl.rev(p) for p in repo.dirstate.parents()]
85 64 else:
86 65 parents = cl.parentrevs(r)
87 66 for p in parents:
88 67 if p < 0:
89 68 continue
90 69 if p not in side:
91 70 # first time we see p; add it to visit
92 71 side[p] = side[r]
93 72 if side[p]:
94 73 interesting += 1
95 74 heapq.heappush(visit, -p)
96 75 elif side[p] and side[p] != side[r]:
97 76 # p was interesting but now we know better
98 77 side[p] = 0
99 78 interesting -= 1
100 79 hascommonancestor = True
101 80 if side[r]:
102 81 limit = r # lowest rev visited
103 82 interesting -= 1
104 83
105 84 if not hascommonancestor:
106 85 return None
107 86 return limit
108 87
109 88 def copies(repo, c1, c2, ca, checkdirs=False):
110 89 """
111 90 Find moves and copies between context c1 and c2
112 91 """
113 92 # avoid silly behavior for update from empty dir
114 93 if not c1 or not c2 or c1 == c2:
115 94 return {}, {}
116 95
117 96 # avoid silly behavior for parent -> working dir
118 97 if c2.node() is None and c1.node() == repo.dirstate.parents()[0]:
119 98 return repo.dirstate.copies(), {}
120 99
121 100 limit = _findlimit(repo, c1.rev(), c2.rev())
122 101 if limit is None:
123 102 # no common ancestor, no copies
124 103 return {}, {}
125 104 m1 = c1.manifest()
126 105 m2 = c2.manifest()
127 106 ma = ca.manifest()
128 107
129 108 def makectx(f, n):
130 109 if len(n) != 20: # in a working context?
131 110 if c1.rev() is None:
132 111 return c1.filectx(f)
133 112 return c2.filectx(f)
134 113 return repo.filectx(f, fileid=n)
135 114
136 115 ctx = util.lrucachefunc(makectx)
137 116 copy = {}
138 117 fullcopy = {}
139 118 diverge = {}
140 119
120 def related(f1, f2, limit):
121 g1, g2 = f1.ancestors(), f2.ancestors()
122 try:
123 while 1:
124 f1r, f2r = f1.rev(), f2.rev()
125 if f1r > f2r:
126 f1 = g1.next()
127 elif f2r > f1r:
128 f2 = g2.next()
129 elif f1 == f2:
130 return f1 # a match
131 elif f1r == f2r or f1r < limit or f2r < limit:
132 return False # copy no longer relevant
133 except StopIteration:
134 return False
135
141 136 def checkcopies(f, m1, m2):
142 137 '''check possible copies of f from m1 to m2'''
143 c1 = ctx(f, m1[f])
144 for of in _findoldnames(c1, limit):
138 of = None
139 seen = set([f])
140 for oc in ctx(f, m1[f]).ancestors():
141 ocr = oc.rev()
142 of = oc.path()
143 if of in seen:
144 # check limit late - grab last rename before
145 if ocr < limit:
146 break
147 continue
148 seen.add(of)
149
145 150 fullcopy[f] = of # remember for dir rename detection
146 if of in m2: # original file not in other manifest?
147 # if the original file is unchanged on the other branch,
148 # no merge needed
149 if m2[of] != ma.get(of):
150 c2 = ctx(of, m2[of])
151 ca = c1.ancestor(c2)
152 # related and named changed on only one side?
153 if ca and (ca.path() == f or ca.path() == c2.path()):
154 if c1 != ca or c2 != ca: # merge needed?
155 copy[f] = of
156 elif of in ma:
157 diverge.setdefault(of, []).append(f)
151 if of not in m2:
152 continue # no match, keep looking
153 if m2[of] == ma.get(of):
154 break # no merge needed, quit early
155 c2 = ctx(of, m2[of])
156 cr = related(oc, c2, ca.rev())
157 if of == f or of == c2.path(): # non-divergent
158 copy[f] = of
159 of = None
160 break
161
162 if of in ma:
163 diverge.setdefault(of, []).append(f)
158 164
159 165 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
160 166
161 167 u1 = _nonoverlap(m1, m2, ma)
162 168 u2 = _nonoverlap(m2, m1, ma)
163 169
164 170 if u1:
165 171 repo.ui.debug(" unmatched files in local:\n %s\n"
166 172 % "\n ".join(u1))
167 173 if u2:
168 174 repo.ui.debug(" unmatched files in other:\n %s\n"
169 175 % "\n ".join(u2))
170 176
171 177 for f in u1:
172 178 checkcopies(f, m1, m2)
173 179 for f in u2:
174 180 checkcopies(f, m2, m1)
175 181
176 182 diverge2 = set()
177 183 for of, fl in diverge.items():
178 184 if len(fl) == 1:
179 185 del diverge[of] # not actually divergent
180 186 else:
181 187 diverge2.update(fl) # reverse map for below
182 188
183 189 if fullcopy:
184 190 repo.ui.debug(" all copies found (* = to merge, ! = divergent):\n")
185 191 for f in fullcopy:
186 192 note = ""
187 193 if f in copy: note += "*"
188 194 if f in diverge2: note += "!"
189 195 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
190 196 del diverge2
191 197
192 198 if not fullcopy or not checkdirs:
193 199 return copy, diverge
194 200
195 201 repo.ui.debug(" checking for directory renames\n")
196 202
197 203 # generate a directory move map
198 204 d1, d2 = _dirs(m1), _dirs(m2)
199 205 invalid = set()
200 206 dirmove = {}
201 207
202 208 # examine each file copy for a potential directory move, which is
203 209 # when all the files in a directory are moved to a new directory
204 210 for dst, src in fullcopy.iteritems():
205 211 dsrc, ddst = _dirname(src), _dirname(dst)
206 212 if dsrc in invalid:
207 213 # already seen to be uninteresting
208 214 continue
209 215 elif dsrc in d1 and ddst in d1:
210 216 # directory wasn't entirely moved locally
211 217 invalid.add(dsrc)
212 218 elif dsrc in d2 and ddst in d2:
213 219 # directory wasn't entirely moved remotely
214 220 invalid.add(dsrc)
215 221 elif dsrc in dirmove and dirmove[dsrc] != ddst:
216 222 # files from the same directory moved to two different places
217 223 invalid.add(dsrc)
218 224 else:
219 225 # looks good so far
220 226 dirmove[dsrc + "/"] = ddst + "/"
221 227
222 228 for i in invalid:
223 229 if i in dirmove:
224 230 del dirmove[i]
225 231 del d1, d2, invalid
226 232
227 233 if not dirmove:
228 234 return copy, diverge
229 235
230 236 for d in dirmove:
231 237 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
232 238
233 239 # check unaccounted nonoverlapping files against directory moves
234 240 for f in u1 + u2:
235 241 if f not in fullcopy:
236 242 for d in dirmove:
237 243 if f.startswith(d):
238 244 # new file added in a directory that was moved, move it
239 245 df = dirmove[d] + f[len(d):]
240 246 if df not in copy:
241 247 copy[f] = df
242 248 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
243 249 break
244 250
245 251 return copy, diverge
General Comments 0
You need to be logged in to leave comments. Login now