##// END OF EJS Templates
context: preserve changeset in filectx if we have one...
Maxim Dounin -
r5810:124577de default
parent child Browse files
Show More
@@ -1,591 +1,595 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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 10 import ancestor, bdiff, repo, revlog, util, os, errno
11 11
12 12 class changectx(object):
13 13 """A changecontext object makes access to data related to a particular
14 14 changeset convenient."""
15 15 def __init__(self, repo, changeid=None):
16 16 """changeid is a revision number, node, or tag"""
17 17 self._repo = repo
18 18
19 19 if not changeid and changeid != 0:
20 20 p1, p2 = self._repo.dirstate.parents()
21 21 self._rev = self._repo.changelog.rev(p1)
22 22 if self._rev == -1:
23 23 changeid = 'tip'
24 24 else:
25 25 self._node = p1
26 26 return
27 27
28 28 self._node = self._repo.lookup(changeid)
29 29 self._rev = self._repo.changelog.rev(self._node)
30 30
31 31 def __str__(self):
32 32 return short(self.node())
33 33
34 34 def __repr__(self):
35 35 return "<changectx %s>" % str(self)
36 36
37 37 def __eq__(self, other):
38 38 try:
39 39 return self._rev == other._rev
40 40 except AttributeError:
41 41 return False
42 42
43 43 def __ne__(self, other):
44 44 return not (self == other)
45 45
46 46 def __nonzero__(self):
47 47 return self._rev != nullrev
48 48
49 49 def __getattr__(self, name):
50 50 if name == '_changeset':
51 51 self._changeset = self._repo.changelog.read(self.node())
52 52 return self._changeset
53 53 elif name == '_manifest':
54 54 self._manifest = self._repo.manifest.read(self._changeset[0])
55 55 return self._manifest
56 56 elif name == '_manifestdelta':
57 57 md = self._repo.manifest.readdelta(self._changeset[0])
58 58 self._manifestdelta = md
59 59 return self._manifestdelta
60 60 else:
61 61 raise AttributeError, name
62 62
63 63 def __contains__(self, key):
64 64 return key in self._manifest
65 65
66 66 def __getitem__(self, key):
67 67 return self.filectx(key)
68 68
69 69 def __iter__(self):
70 70 a = self._manifest.keys()
71 71 a.sort()
72 72 for f in a:
73 73 yield f
74 74
75 75 def changeset(self): return self._changeset
76 76 def manifest(self): return self._manifest
77 77
78 78 def rev(self): return self._rev
79 79 def node(self): return self._node
80 80 def user(self): return self._changeset[1]
81 81 def date(self): return self._changeset[2]
82 82 def files(self): return self._changeset[3]
83 83 def description(self): return self._changeset[4]
84 84 def branch(self): return self._changeset[5].get("branch")
85 85 def extra(self): return self._changeset[5]
86 86 def tags(self): return self._repo.nodetags(self._node)
87 87
88 88 def parents(self):
89 89 """return contexts for each parent changeset"""
90 90 p = self._repo.changelog.parents(self._node)
91 91 return [changectx(self._repo, x) for x in p]
92 92
93 93 def children(self):
94 94 """return contexts for each child changeset"""
95 95 c = self._repo.changelog.children(self._node)
96 96 return [changectx(self._repo, x) for x in c]
97 97
98 98 def _fileinfo(self, path):
99 99 if '_manifest' in self.__dict__:
100 100 try:
101 101 return self._manifest[path], self._manifest.flags(path)
102 102 except KeyError:
103 103 raise revlog.LookupError(path, _("'%s' not found in manifest") % path)
104 104 if '_manifestdelta' in self.__dict__ or path in self.files():
105 105 if path in self._manifestdelta:
106 106 return self._manifestdelta[path], self._manifestdelta.flags(path)
107 107 node, flag = self._repo.manifest.find(self._changeset[0], path)
108 108 if not node:
109 109 raise revlog.LookupError(path, _("'%s' not found in manifest") % path)
110 110
111 111 return node, flag
112 112
113 113 def filenode(self, path):
114 114 return self._fileinfo(path)[0]
115 115
116 116 def fileflags(self, path):
117 117 try:
118 118 return self._fileinfo(path)[1]
119 119 except revlog.LookupError:
120 120 return ''
121 121
122 122 def filectx(self, path, fileid=None, filelog=None):
123 123 """get a file context from this changeset"""
124 124 if fileid is None:
125 125 fileid = self.filenode(path)
126 126 return filectx(self._repo, path, fileid=fileid,
127 127 changectx=self, filelog=filelog)
128 128
129 129 def filectxs(self):
130 130 """generate a file context for each file in this changeset's
131 131 manifest"""
132 132 mf = self.manifest()
133 133 m = mf.keys()
134 134 m.sort()
135 135 for f in m:
136 136 yield self.filectx(f, fileid=mf[f])
137 137
138 138 def ancestor(self, c2):
139 139 """
140 140 return the ancestor context of self and c2
141 141 """
142 142 n = self._repo.changelog.ancestor(self._node, c2._node)
143 143 return changectx(self._repo, n)
144 144
145 145 class filectx(object):
146 146 """A filecontext object makes access to data related to a particular
147 147 filerevision convenient."""
148 148 def __init__(self, repo, path, changeid=None, fileid=None,
149 149 filelog=None, changectx=None):
150 150 """changeid can be a changeset revision, node, or tag.
151 151 fileid can be a file revision or node."""
152 152 self._repo = repo
153 153 self._path = path
154 154
155 155 assert (changeid is not None
156 156 or fileid is not None
157 157 or changectx is not None)
158 158
159 159 if filelog:
160 160 self._filelog = filelog
161 161
162 if fileid is None:
163 if changectx is None:
164 self._changeid = changeid
165 else:
166 self._changectx = changectx
167 else:
162 if changeid is not None:
163 self._changeid = changeid
164 if changectx is not None:
165 self._changectx = changectx
166 if fileid is not None:
168 167 self._fileid = fileid
169 168
170 169 def __getattr__(self, name):
171 170 if name == '_changectx':
172 171 self._changectx = changectx(self._repo, self._changeid)
173 172 return self._changectx
174 173 elif name == '_filelog':
175 174 self._filelog = self._repo.file(self._path)
176 175 return self._filelog
177 176 elif name == '_changeid':
178 self._changeid = self._filelog.linkrev(self._filenode)
177 if '_changectx' in self.__dict__:
178 self._changeid = self._changectx.rev()
179 else:
180 self._changeid = self._filelog.linkrev(self._filenode)
179 181 return self._changeid
180 182 elif name == '_filenode':
181 183 if '_fileid' in self.__dict__:
182 184 self._filenode = self._filelog.lookup(self._fileid)
183 185 else:
184 186 self._filenode = self._changectx.filenode(self._path)
185 187 return self._filenode
186 188 elif name == '_filerev':
187 189 self._filerev = self._filelog.rev(self._filenode)
188 190 return self._filerev
189 191 else:
190 192 raise AttributeError, name
191 193
192 194 def __nonzero__(self):
193 195 try:
194 196 n = self._filenode
195 197 return True
196 198 except revlog.LookupError:
197 199 # file is missing
198 200 return False
199 201
200 202 def __str__(self):
201 203 return "%s@%s" % (self.path(), short(self.node()))
202 204
203 205 def __repr__(self):
204 206 return "<filectx %s>" % str(self)
205 207
206 208 def __eq__(self, other):
207 209 try:
208 210 return (self._path == other._path
209 211 and self._fileid == other._fileid)
210 212 except AttributeError:
211 213 return False
212 214
213 215 def __ne__(self, other):
214 216 return not (self == other)
215 217
216 218 def filectx(self, fileid):
217 219 '''opens an arbitrary revision of the file without
218 220 opening a new filelog'''
219 221 return filectx(self._repo, self._path, fileid=fileid,
220 222 filelog=self._filelog)
221 223
222 224 def filerev(self): return self._filerev
223 225 def filenode(self): return self._filenode
224 226 def fileflags(self): return self._changectx.fileflags(self._path)
225 227 def isexec(self): return 'x' in self.fileflags()
226 228 def islink(self): return 'l' in self.fileflags()
227 229 def filelog(self): return self._filelog
228 230
229 231 def rev(self):
230 232 if '_changectx' in self.__dict__:
231 233 return self._changectx.rev()
234 if '_changeid' in self.__dict__:
235 return self._changectx.rev()
232 236 return self._filelog.linkrev(self._filenode)
233 237
234 238 def node(self): return self._changectx.node()
235 239 def user(self): return self._changectx.user()
236 240 def date(self): return self._changectx.date()
237 241 def files(self): return self._changectx.files()
238 242 def description(self): return self._changectx.description()
239 243 def branch(self): return self._changectx.branch()
240 244 def manifest(self): return self._changectx.manifest()
241 245 def changectx(self): return self._changectx
242 246
243 247 def data(self): return self._filelog.read(self._filenode)
244 248 def renamed(self): return self._filelog.renamed(self._filenode)
245 249 def path(self): return self._path
246 250 def size(self): return self._filelog.size(self._filerev)
247 251
248 252 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
249 253
250 254 def parents(self):
251 255 p = self._path
252 256 fl = self._filelog
253 257 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
254 258
255 259 r = self.renamed()
256 260 if r:
257 261 pl[0] = (r[0], r[1], None)
258 262
259 263 return [filectx(self._repo, p, fileid=n, filelog=l)
260 264 for p,n,l in pl if n != nullid]
261 265
262 266 def children(self):
263 267 # hard for renames
264 268 c = self._filelog.children(self._filenode)
265 269 return [filectx(self._repo, self._path, fileid=x,
266 270 filelog=self._filelog) for x in c]
267 271
268 272 def annotate(self, follow=False, linenumber=None):
269 273 '''returns a list of tuples of (ctx, line) for each line
270 274 in the file, where ctx is the filectx of the node where
271 275 that line was last changed.
272 276 This returns tuples of ((ctx, linenumber), line) for each line,
273 277 if "linenumber" parameter is NOT "None".
274 278 In such tuples, linenumber means one at the first appearance
275 279 in the managed file.
276 280 To reduce annotation cost,
277 281 this returns fixed value(False is used) as linenumber,
278 282 if "linenumber" parameter is "False".'''
279 283
280 284 def decorate_compat(text, rev):
281 285 return ([rev] * len(text.splitlines()), text)
282 286
283 287 def without_linenumber(text, rev):
284 288 return ([(rev, False)] * len(text.splitlines()), text)
285 289
286 290 def with_linenumber(text, rev):
287 291 size = len(text.splitlines())
288 292 return ([(rev, i) for i in xrange(1, size + 1)], text)
289 293
290 294 decorate = (((linenumber is None) and decorate_compat) or
291 295 (linenumber and with_linenumber) or
292 296 without_linenumber)
293 297
294 298 def pair(parent, child):
295 299 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
296 300 child[0][b1:b2] = parent[0][a1:a2]
297 301 return child
298 302
299 303 getlog = util.cachefunc(lambda x: self._repo.file(x))
300 304 def getctx(path, fileid):
301 305 log = path == self._path and self._filelog or getlog(path)
302 306 return filectx(self._repo, path, fileid=fileid, filelog=log)
303 307 getctx = util.cachefunc(getctx)
304 308
305 309 def parents(f):
306 310 # we want to reuse filectx objects as much as possible
307 311 p = f._path
308 312 if f._filerev is None: # working dir
309 313 pl = [(n.path(), n.filerev()) for n in f.parents()]
310 314 else:
311 315 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
312 316
313 317 if follow:
314 318 r = f.renamed()
315 319 if r:
316 320 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
317 321
318 322 return [getctx(p, n) for p, n in pl if n != nullrev]
319 323
320 324 # use linkrev to find the first changeset where self appeared
321 325 if self.rev() != self._filelog.linkrev(self._filenode):
322 326 base = self.filectx(self.filerev())
323 327 else:
324 328 base = self
325 329
326 330 # find all ancestors
327 331 needed = {base: 1}
328 332 visit = [base]
329 333 files = [base._path]
330 334 while visit:
331 335 f = visit.pop(0)
332 336 for p in parents(f):
333 337 if p not in needed:
334 338 needed[p] = 1
335 339 visit.append(p)
336 340 if p._path not in files:
337 341 files.append(p._path)
338 342 else:
339 343 # count how many times we'll use this
340 344 needed[p] += 1
341 345
342 346 # sort by revision (per file) which is a topological order
343 347 visit = []
344 348 for f in files:
345 349 fn = [(n.rev(), n) for n in needed.keys() if n._path == f]
346 350 visit.extend(fn)
347 351 visit.sort()
348 352 hist = {}
349 353
350 354 for r, f in visit:
351 355 curr = decorate(f.data(), f)
352 356 for p in parents(f):
353 357 if p != nullid:
354 358 curr = pair(hist[p], curr)
355 359 # trim the history of unneeded revs
356 360 needed[p] -= 1
357 361 if not needed[p]:
358 362 del hist[p]
359 363 hist[f] = curr
360 364
361 365 return zip(hist[f][0], hist[f][1].splitlines(1))
362 366
363 367 def ancestor(self, fc2):
364 368 """
365 369 find the common ancestor file context, if any, of self, and fc2
366 370 """
367 371
368 372 acache = {}
369 373
370 374 # prime the ancestor cache for the working directory
371 375 for c in (self, fc2):
372 376 if c._filerev == None:
373 377 pl = [(n.path(), n.filenode()) for n in c.parents()]
374 378 acache[(c._path, None)] = pl
375 379
376 380 flcache = {self._path:self._filelog, fc2._path:fc2._filelog}
377 381 def parents(vertex):
378 382 if vertex in acache:
379 383 return acache[vertex]
380 384 f, n = vertex
381 385 if f not in flcache:
382 386 flcache[f] = self._repo.file(f)
383 387 fl = flcache[f]
384 388 pl = [(f, p) for p in fl.parents(n) if p != nullid]
385 389 re = fl.renamed(n)
386 390 if re:
387 391 pl.append(re)
388 392 acache[vertex] = pl
389 393 return pl
390 394
391 395 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
392 396 v = ancestor.ancestor(a, b, parents)
393 397 if v:
394 398 f, n = v
395 399 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
396 400
397 401 return None
398 402
399 403 class workingctx(changectx):
400 404 """A workingctx object makes access to data related to
401 405 the current working directory convenient."""
402 406 def __init__(self, repo):
403 407 self._repo = repo
404 408 self._rev = None
405 409 self._node = None
406 410
407 411 def __str__(self):
408 412 return str(self._parents[0]) + "+"
409 413
410 414 def __nonzero__(self):
411 415 return True
412 416
413 417 def __getattr__(self, name):
414 418 if name == '_parents':
415 419 self._parents = self._repo.parents()
416 420 return self._parents
417 421 if name == '_status':
418 422 self._status = self._repo.status()
419 423 return self._status
420 424 if name == '_manifest':
421 425 self._buildmanifest()
422 426 return self._manifest
423 427 else:
424 428 raise AttributeError, name
425 429
426 430 def _buildmanifest(self):
427 431 """generate a manifest corresponding to the working directory"""
428 432
429 433 man = self._parents[0].manifest().copy()
430 434 copied = self._repo.dirstate.copies()
431 435 is_exec = util.execfunc(self._repo.root,
432 436 lambda p: man.execf(copied.get(p,p)))
433 437 is_link = util.linkfunc(self._repo.root,
434 438 lambda p: man.linkf(copied.get(p,p)))
435 439 modified, added, removed, deleted, unknown = self._status[:5]
436 440 for i, l in (("a", added), ("m", modified), ("u", unknown)):
437 441 for f in l:
438 442 man[f] = man.get(copied.get(f, f), nullid) + i
439 443 try:
440 444 man.set(f, is_exec(f), is_link(f))
441 445 except OSError:
442 446 pass
443 447
444 448 for f in deleted + removed:
445 449 if f in man:
446 450 del man[f]
447 451
448 452 self._manifest = man
449 453
450 454 def manifest(self): return self._manifest
451 455
452 456 def user(self): return self._repo.ui.username()
453 457 def date(self): return util.makedate()
454 458 def description(self): return ""
455 459 def files(self):
456 460 f = self.modified() + self.added() + self.removed()
457 461 f.sort()
458 462 return f
459 463
460 464 def modified(self): return self._status[0]
461 465 def added(self): return self._status[1]
462 466 def removed(self): return self._status[2]
463 467 def deleted(self): return self._status[3]
464 468 def unknown(self): return self._status[4]
465 469 def clean(self): return self._status[5]
466 470 def branch(self): return self._repo.dirstate.branch()
467 471
468 472 def tags(self):
469 473 t = []
470 474 [t.extend(p.tags()) for p in self.parents()]
471 475 return t
472 476
473 477 def parents(self):
474 478 """return contexts for each parent changeset"""
475 479 return self._parents
476 480
477 481 def children(self):
478 482 return []
479 483
480 484 def fileflags(self, path):
481 485 if '_manifest' in self.__dict__:
482 486 try:
483 487 return self._manifest.flags(path)
484 488 except KeyError:
485 489 return ''
486 490
487 491 pnode = self._parents[0].changeset()[0]
488 492 orig = self._repo.dirstate.copies().get(path, path)
489 493 node, flag = self._repo.manifest.find(pnode, orig)
490 494 is_link = util.linkfunc(self._repo.root, lambda p: 'l' in flag)
491 495 is_exec = util.execfunc(self._repo.root, lambda p: 'x' in flag)
492 496 try:
493 497 return (is_link(path) and 'l' or '') + (is_exec(path) and 'e' or '')
494 498 except OSError:
495 499 pass
496 500
497 501 if not node or path in self.deleted() or path in self.removed():
498 502 return ''
499 503 return flag
500 504
501 505 def filectx(self, path, filelog=None):
502 506 """get a file context from the working directory"""
503 507 return workingfilectx(self._repo, path, workingctx=self,
504 508 filelog=filelog)
505 509
506 510 def ancestor(self, c2):
507 511 """return the ancestor context of self and c2"""
508 512 return self._parents[0].ancestor(c2) # punt on two parents for now
509 513
510 514 class workingfilectx(filectx):
511 515 """A workingfilectx object makes access to data related to a particular
512 516 file in the working directory convenient."""
513 517 def __init__(self, repo, path, filelog=None, workingctx=None):
514 518 """changeid can be a changeset revision, node, or tag.
515 519 fileid can be a file revision or node."""
516 520 self._repo = repo
517 521 self._path = path
518 522 self._changeid = None
519 523 self._filerev = self._filenode = None
520 524
521 525 if filelog:
522 526 self._filelog = filelog
523 527 if workingctx:
524 528 self._changectx = workingctx
525 529
526 530 def __getattr__(self, name):
527 531 if name == '_changectx':
528 532 self._changectx = workingctx(self._repo)
529 533 return self._changectx
530 534 elif name == '_repopath':
531 535 self._repopath = (self._repo.dirstate.copied(self._path)
532 536 or self._path)
533 537 return self._repopath
534 538 elif name == '_filelog':
535 539 self._filelog = self._repo.file(self._repopath)
536 540 return self._filelog
537 541 else:
538 542 raise AttributeError, name
539 543
540 544 def __nonzero__(self):
541 545 return True
542 546
543 547 def __str__(self):
544 548 return "%s@%s" % (self.path(), self._changectx)
545 549
546 550 def filectx(self, fileid):
547 551 '''opens an arbitrary revision of the file without
548 552 opening a new filelog'''
549 553 return filectx(self._repo, self._repopath, fileid=fileid,
550 554 filelog=self._filelog)
551 555
552 556 def rev(self):
553 557 if '_changectx' in self.__dict__:
554 558 return self._changectx.rev()
555 559 return self._filelog.linkrev(self._filenode)
556 560
557 561 def data(self): return self._repo.wread(self._path)
558 562 def renamed(self):
559 563 rp = self._repopath
560 564 if rp == self._path:
561 565 return None
562 566 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
563 567
564 568 def parents(self):
565 569 '''return parent filectxs, following copies if necessary'''
566 570 p = self._path
567 571 rp = self._repopath
568 572 pcl = self._changectx._parents
569 573 fl = self._filelog
570 574 pl = [(rp, pcl[0]._manifest.get(rp, nullid), fl)]
571 575 if len(pcl) > 1:
572 576 if rp != p:
573 577 fl = None
574 578 pl.append((p, pcl[1]._manifest.get(p, nullid), fl))
575 579
576 580 return [filectx(self._repo, p, fileid=n, filelog=l)
577 581 for p,n,l in pl if n != nullid]
578 582
579 583 def children(self):
580 584 return []
581 585
582 586 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
583 587 def date(self):
584 588 t, tz = self._changectx.date()
585 589 try:
586 590 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
587 591 except OSError, err:
588 592 if err.errno != errno.ENOENT: raise
589 593 return (t, tz)
590 594
591 595 def cmp(self, text): return self._repo.wread(self._path) == text
@@ -1,51 +1,51 b''
1 1 adding 1
2 2 adding 2
3 3 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
4 4 resolving manifests
5 5 overwrite None partial False
6 6 ancestor 81f4b099af3d local c64f439569a9+ remote 2f8037f47a5c
7 7 searching for copies back to rev 1
8 8 unmatched files in other:
9 9 1a
10 10 all copies found (* = to merge, ! = divergent):
11 11 1a -> 1
12 12 checking for directory renames
13 13 1: other deleted -> r
14 14 1a: remote created -> g
15 15 removing 1
16 16 getting 1a
17 17 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
18 18 (branch merge, don't forget to commit)
19 19 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
20 20 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
21 21 resolving manifests
22 22 overwrite None partial False
23 23 ancestor c64f439569a9 local ac7575e3c052+ remote 746e9549ea96
24 24 searching for copies back to rev 1
25 25 unmatched files in local:
26 26 1a
27 27 all copies found (* = to merge, ! = divergent):
28 28 1a -> 1 *
29 29 checking for directory renames
30 30 1a: local moved to 1 -> m
31 31 merging 1a and 1
32 32 my 1a@ac7575e3c052+ other 1@746e9549ea96 ancestor 1@81f4b099af3d
33 33 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
34 34 (branch merge, don't forget to commit)
35 35 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
36 36 resolving manifests
37 37 overwrite None partial False
38 38 ancestor c64f439569a9 local 746e9549ea96+ remote ac7575e3c052
39 39 searching for copies back to rev 1
40 40 unmatched files in other:
41 41 1a
42 42 all copies found (* = to merge, ! = divergent):
43 43 1a -> 1 *
44 44 checking for directory renames
45 45 1: remote moved to 1a -> m
46 46 copying 1 to 1a
47 47 merging 1 and 1a
48 my 1@746e9549ea96+ other 1a@2f8037f47a5c ancestor 1@81f4b099af3d
48 my 1@746e9549ea96+ other 1a@ac7575e3c052 ancestor 1@81f4b099af3d
49 49 removing 1
50 50 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
51 51 (branch merge, don't forget to commit)
General Comments 0
You need to be logged in to leave comments. Login now