##// END OF EJS Templates
hgweb: show correct error message for i18n environment...
Takumi IINO -
r18855:50c922c1 2.5.3 stable
parent child Browse files
Show More
@@ -1,1364 +1,1364 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import nullid, nullrev, short, hex, bin
9 9 from i18n import _
10 10 import ancestor, mdiff, error, util, scmutil, subrepo, patch, encoding, phases
11 11 import copies
12 12 import match as matchmod
13 13 import os, errno, stat
14 14 import obsolete as obsmod
15 15 import repoview
16 16
17 17 propertycache = util.propertycache
18 18
19 19 class changectx(object):
20 20 """A changecontext object makes access to data related to a particular
21 21 changeset convenient."""
22 22 def __init__(self, repo, changeid=''):
23 23 """changeid is a revision number, node, or tag"""
24 24 if changeid == '':
25 25 changeid = '.'
26 26 self._repo = repo
27 27
28 28 if isinstance(changeid, int):
29 29 try:
30 30 self._node = repo.changelog.node(changeid)
31 31 except IndexError:
32 32 raise error.RepoLookupError(
33 33 _("unknown revision '%s'") % changeid)
34 34 self._rev = changeid
35 35 return
36 36 if isinstance(changeid, long):
37 37 changeid = str(changeid)
38 38 if changeid == '.':
39 39 self._node = repo.dirstate.p1()
40 40 self._rev = repo.changelog.rev(self._node)
41 41 return
42 42 if changeid == 'null':
43 43 self._node = nullid
44 44 self._rev = nullrev
45 45 return
46 46 if changeid == 'tip':
47 47 self._node = repo.changelog.tip()
48 48 self._rev = repo.changelog.rev(self._node)
49 49 return
50 50 if len(changeid) == 20:
51 51 try:
52 52 self._node = changeid
53 53 self._rev = repo.changelog.rev(changeid)
54 54 return
55 55 except LookupError:
56 56 pass
57 57
58 58 try:
59 59 r = int(changeid)
60 60 if str(r) != changeid:
61 61 raise ValueError
62 62 l = len(repo.changelog)
63 63 if r < 0:
64 64 r += l
65 65 if r < 0 or r >= l:
66 66 raise ValueError
67 67 self._rev = r
68 68 self._node = repo.changelog.node(r)
69 69 return
70 70 except (ValueError, OverflowError, IndexError):
71 71 pass
72 72
73 73 if len(changeid) == 40:
74 74 try:
75 75 self._node = bin(changeid)
76 76 self._rev = repo.changelog.rev(self._node)
77 77 return
78 78 except (TypeError, LookupError):
79 79 pass
80 80
81 81 if changeid in repo._bookmarks:
82 82 self._node = repo._bookmarks[changeid]
83 83 self._rev = repo.changelog.rev(self._node)
84 84 return
85 85 if changeid in repo._tagscache.tags:
86 86 self._node = repo._tagscache.tags[changeid]
87 87 self._rev = repo.changelog.rev(self._node)
88 88 return
89 89 try:
90 90 self._node = repo.branchtip(changeid)
91 91 self._rev = repo.changelog.rev(self._node)
92 92 return
93 93 except error.RepoLookupError:
94 94 pass
95 95
96 96 self._node = repo.changelog._partialmatch(changeid)
97 97 if self._node is not None:
98 98 self._rev = repo.changelog.rev(self._node)
99 99 return
100 100
101 101 # lookup failed
102 102 # check if it might have come from damaged dirstate
103 103 #
104 104 # XXX we could avoid the unfiltered if we had a recognizable exception
105 105 # for filtered changeset access
106 106 if changeid in repo.unfiltered().dirstate.parents():
107 107 raise error.Abort(_("working directory has unknown parent '%s'!")
108 108 % short(changeid))
109 109 try:
110 110 if len(changeid) == 20:
111 111 changeid = hex(changeid)
112 112 except TypeError:
113 113 pass
114 114 raise error.RepoLookupError(
115 115 _("unknown revision '%s'") % changeid)
116 116
117 117 def __str__(self):
118 118 return short(self.node())
119 119
120 120 def __int__(self):
121 121 return self.rev()
122 122
123 123 def __repr__(self):
124 124 return "<changectx %s>" % str(self)
125 125
126 126 def __hash__(self):
127 127 try:
128 128 return hash(self._rev)
129 129 except AttributeError:
130 130 return id(self)
131 131
132 132 def __eq__(self, other):
133 133 try:
134 134 return self._rev == other._rev
135 135 except AttributeError:
136 136 return False
137 137
138 138 def __ne__(self, other):
139 139 return not (self == other)
140 140
141 141 def __nonzero__(self):
142 142 return self._rev != nullrev
143 143
144 144 @propertycache
145 145 def _changeset(self):
146 146 return self._repo.changelog.read(self.rev())
147 147
148 148 @propertycache
149 149 def _manifest(self):
150 150 return self._repo.manifest.read(self._changeset[0])
151 151
152 152 @propertycache
153 153 def _manifestdelta(self):
154 154 return self._repo.manifest.readdelta(self._changeset[0])
155 155
156 156 @propertycache
157 157 def _parents(self):
158 158 p = self._repo.changelog.parentrevs(self._rev)
159 159 if p[1] == nullrev:
160 160 p = p[:-1]
161 161 return [changectx(self._repo, x) for x in p]
162 162
163 163 @propertycache
164 164 def substate(self):
165 165 return subrepo.state(self, self._repo.ui)
166 166
167 167 def __contains__(self, key):
168 168 return key in self._manifest
169 169
170 170 def __getitem__(self, key):
171 171 return self.filectx(key)
172 172
173 173 def __iter__(self):
174 174 for f in sorted(self._manifest):
175 175 yield f
176 176
177 177 def changeset(self):
178 178 return self._changeset
179 179 def manifest(self):
180 180 return self._manifest
181 181 def manifestnode(self):
182 182 return self._changeset[0]
183 183
184 184 def rev(self):
185 185 return self._rev
186 186 def node(self):
187 187 return self._node
188 188 def hex(self):
189 189 return hex(self._node)
190 190 def user(self):
191 191 return self._changeset[1]
192 192 def date(self):
193 193 return self._changeset[2]
194 194 def files(self):
195 195 return self._changeset[3]
196 196 def description(self):
197 197 return self._changeset[4]
198 198 def branch(self):
199 199 return encoding.tolocal(self._changeset[5].get("branch"))
200 200 def closesbranch(self):
201 201 return 'close' in self._changeset[5]
202 202 def extra(self):
203 203 return self._changeset[5]
204 204 def tags(self):
205 205 return self._repo.nodetags(self._node)
206 206 def bookmarks(self):
207 207 return self._repo.nodebookmarks(self._node)
208 208 def phase(self):
209 209 return self._repo._phasecache.phase(self._repo, self._rev)
210 210 def phasestr(self):
211 211 return phases.phasenames[self.phase()]
212 212 def mutable(self):
213 213 return self.phase() > phases.public
214 214 def hidden(self):
215 215 return self._rev in repoview.filterrevs(self._repo, 'visible')
216 216
217 217 def parents(self):
218 218 """return contexts for each parent changeset"""
219 219 return self._parents
220 220
221 221 def p1(self):
222 222 return self._parents[0]
223 223
224 224 def p2(self):
225 225 if len(self._parents) == 2:
226 226 return self._parents[1]
227 227 return changectx(self._repo, -1)
228 228
229 229 def children(self):
230 230 """return contexts for each child changeset"""
231 231 c = self._repo.changelog.children(self._node)
232 232 return [changectx(self._repo, x) for x in c]
233 233
234 234 def ancestors(self):
235 235 for a in self._repo.changelog.ancestors([self._rev]):
236 236 yield changectx(self._repo, a)
237 237
238 238 def descendants(self):
239 239 for d in self._repo.changelog.descendants([self._rev]):
240 240 yield changectx(self._repo, d)
241 241
242 242 def obsolete(self):
243 243 """True if the changeset is obsolete"""
244 244 return self.rev() in obsmod.getrevs(self._repo, 'obsolete')
245 245
246 246 def extinct(self):
247 247 """True if the changeset is extinct"""
248 248 return self.rev() in obsmod.getrevs(self._repo, 'extinct')
249 249
250 250 def unstable(self):
251 251 """True if the changeset is not obsolete but it's ancestor are"""
252 252 return self.rev() in obsmod.getrevs(self._repo, 'unstable')
253 253
254 254 def bumped(self):
255 255 """True if the changeset try to be a successor of a public changeset
256 256
257 257 Only non-public and non-obsolete changesets may be bumped.
258 258 """
259 259 return self.rev() in obsmod.getrevs(self._repo, 'bumped')
260 260
261 261 def divergent(self):
262 262 """Is a successors of a changeset with multiple possible successors set
263 263
264 264 Only non-public and non-obsolete changesets may be divergent.
265 265 """
266 266 return self.rev() in obsmod.getrevs(self._repo, 'divergent')
267 267
268 268 def troubled(self):
269 269 """True if the changeset is either unstable, bumped or divergent"""
270 270 return self.unstable() or self.bumped() or self.divergent()
271 271
272 272 def troubles(self):
273 273 """return the list of troubles affecting this changesets.
274 274
275 275 Troubles are returned as strings. possible values are:
276 276 - unstable,
277 277 - bumped,
278 278 - divergent.
279 279 """
280 280 troubles = []
281 281 if self.unstable():
282 282 troubles.append('unstable')
283 283 if self.bumped():
284 284 troubles.append('bumped')
285 285 if self.divergent():
286 286 troubles.append('divergent')
287 287 return troubles
288 288
289 289 def _fileinfo(self, path):
290 290 if '_manifest' in self.__dict__:
291 291 try:
292 292 return self._manifest[path], self._manifest.flags(path)
293 293 except KeyError:
294 raise error.LookupError(self._node, path,
295 _('not found in manifest'))
294 raise error.ManifestLookupError(self._node, path,
295 _('not found in manifest'))
296 296 if '_manifestdelta' in self.__dict__ or path in self.files():
297 297 if path in self._manifestdelta:
298 298 return (self._manifestdelta[path],
299 299 self._manifestdelta.flags(path))
300 300 node, flag = self._repo.manifest.find(self._changeset[0], path)
301 301 if not node:
302 raise error.LookupError(self._node, path,
303 _('not found in manifest'))
302 raise error.ManifestLookupError(self._node, path,
303 _('not found in manifest'))
304 304
305 305 return node, flag
306 306
307 307 def filenode(self, path):
308 308 return self._fileinfo(path)[0]
309 309
310 310 def flags(self, path):
311 311 try:
312 312 return self._fileinfo(path)[1]
313 313 except error.LookupError:
314 314 return ''
315 315
316 316 def filectx(self, path, fileid=None, filelog=None):
317 317 """get a file context from this changeset"""
318 318 if fileid is None:
319 319 fileid = self.filenode(path)
320 320 return filectx(self._repo, path, fileid=fileid,
321 321 changectx=self, filelog=filelog)
322 322
323 323 def ancestor(self, c2):
324 324 """
325 325 return the ancestor context of self and c2
326 326 """
327 327 # deal with workingctxs
328 328 n2 = c2._node
329 329 if n2 is None:
330 330 n2 = c2._parents[0]._node
331 331 n = self._repo.changelog.ancestor(self._node, n2)
332 332 return changectx(self._repo, n)
333 333
334 334 def descendant(self, other):
335 335 """True if other is descendant of this changeset"""
336 336 return self._repo.changelog.descendant(self._rev, other._rev)
337 337
338 338 def walk(self, match):
339 339 fset = set(match.files())
340 340 # for dirstate.walk, files=['.'] means "walk the whole tree".
341 341 # follow that here, too
342 342 fset.discard('.')
343 343 for fn in self:
344 344 if fn in fset:
345 345 # specified pattern is the exact name
346 346 fset.remove(fn)
347 347 if match(fn):
348 348 yield fn
349 349 for fn in sorted(fset):
350 350 if fn in self._dirs:
351 351 # specified pattern is a directory
352 352 continue
353 353 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
354 354 yield fn
355 355
356 356 def sub(self, path):
357 357 return subrepo.subrepo(self, path)
358 358
359 359 def match(self, pats=[], include=None, exclude=None, default='glob'):
360 360 r = self._repo
361 361 return matchmod.match(r.root, r.getcwd(), pats,
362 362 include, exclude, default,
363 363 auditor=r.auditor, ctx=self)
364 364
365 365 def diff(self, ctx2=None, match=None, **opts):
366 366 """Returns a diff generator for the given contexts and matcher"""
367 367 if ctx2 is None:
368 368 ctx2 = self.p1()
369 369 if ctx2 is not None and not isinstance(ctx2, changectx):
370 370 ctx2 = self._repo[ctx2]
371 371 diffopts = patch.diffopts(self._repo.ui, opts)
372 372 return patch.diff(self._repo, ctx2.node(), self.node(),
373 373 match=match, opts=diffopts)
374 374
375 375 @propertycache
376 376 def _dirs(self):
377 377 dirs = set()
378 378 for f in self._manifest:
379 379 pos = f.rfind('/')
380 380 while pos != -1:
381 381 f = f[:pos]
382 382 if f in dirs:
383 383 break # dirs already contains this and above
384 384 dirs.add(f)
385 385 pos = f.rfind('/')
386 386 return dirs
387 387
388 388 def dirs(self):
389 389 return self._dirs
390 390
391 391 def dirty(self):
392 392 return False
393 393
394 394 class filectx(object):
395 395 """A filecontext object makes access to data related to a particular
396 396 filerevision convenient."""
397 397 def __init__(self, repo, path, changeid=None, fileid=None,
398 398 filelog=None, changectx=None):
399 399 """changeid can be a changeset revision, node, or tag.
400 400 fileid can be a file revision or node."""
401 401 self._repo = repo
402 402 self._path = path
403 403
404 404 assert (changeid is not None
405 405 or fileid is not None
406 406 or changectx is not None), \
407 407 ("bad args: changeid=%r, fileid=%r, changectx=%r"
408 408 % (changeid, fileid, changectx))
409 409
410 410 if filelog:
411 411 self._filelog = filelog
412 412
413 413 if changeid is not None:
414 414 self._changeid = changeid
415 415 if changectx is not None:
416 416 self._changectx = changectx
417 417 if fileid is not None:
418 418 self._fileid = fileid
419 419
420 420 @propertycache
421 421 def _changectx(self):
422 422 try:
423 423 return changectx(self._repo, self._changeid)
424 424 except error.RepoLookupError:
425 425 # Linkrev may point to any revision in the repository. When the
426 426 # repository is filtered this may lead to `filectx` trying to build
427 427 # `changectx` for filtered revision. In such case we fallback to
428 428 # creating `changectx` on the unfiltered version of the reposition.
429 429 # This fallback should not be an issue because`changectx` from
430 430 # `filectx` are not used in complexe operation that care about
431 431 # filtering.
432 432 #
433 433 # This fallback is a cheap and dirty fix that prevent several
434 434 # crash. It does not ensure the behavior is correct. However the
435 435 # behavior was not correct before filtering either and "incorrect
436 436 # behavior" is seen as better as "crash"
437 437 #
438 438 # Linkrevs have several serious troubles with filtering that are
439 439 # complicated to solve. Proper handling of the issue here should be
440 440 # considered when solving linkrev issue are on the table.
441 441 return changectx(self._repo.unfiltered(), self._changeid)
442 442
443 443 @propertycache
444 444 def _filelog(self):
445 445 return self._repo.file(self._path)
446 446
447 447 @propertycache
448 448 def _changeid(self):
449 449 if '_changectx' in self.__dict__:
450 450 return self._changectx.rev()
451 451 else:
452 452 return self._filelog.linkrev(self._filerev)
453 453
454 454 @propertycache
455 455 def _filenode(self):
456 456 if '_fileid' in self.__dict__:
457 457 return self._filelog.lookup(self._fileid)
458 458 else:
459 459 return self._changectx.filenode(self._path)
460 460
461 461 @propertycache
462 462 def _filerev(self):
463 463 return self._filelog.rev(self._filenode)
464 464
465 465 @propertycache
466 466 def _repopath(self):
467 467 return self._path
468 468
469 469 def __nonzero__(self):
470 470 try:
471 471 self._filenode
472 472 return True
473 473 except error.LookupError:
474 474 # file is missing
475 475 return False
476 476
477 477 def __str__(self):
478 478 return "%s@%s" % (self.path(), short(self.node()))
479 479
480 480 def __repr__(self):
481 481 return "<filectx %s>" % str(self)
482 482
483 483 def __hash__(self):
484 484 try:
485 485 return hash((self._path, self._filenode))
486 486 except AttributeError:
487 487 return id(self)
488 488
489 489 def __eq__(self, other):
490 490 try:
491 491 return (self._path == other._path
492 492 and self._filenode == other._filenode)
493 493 except AttributeError:
494 494 return False
495 495
496 496 def __ne__(self, other):
497 497 return not (self == other)
498 498
499 499 def filectx(self, fileid):
500 500 '''opens an arbitrary revision of the file without
501 501 opening a new filelog'''
502 502 return filectx(self._repo, self._path, fileid=fileid,
503 503 filelog=self._filelog)
504 504
505 505 def filerev(self):
506 506 return self._filerev
507 507 def filenode(self):
508 508 return self._filenode
509 509 def flags(self):
510 510 return self._changectx.flags(self._path)
511 511 def filelog(self):
512 512 return self._filelog
513 513
514 514 def rev(self):
515 515 if '_changectx' in self.__dict__:
516 516 return self._changectx.rev()
517 517 if '_changeid' in self.__dict__:
518 518 return self._changectx.rev()
519 519 return self._filelog.linkrev(self._filerev)
520 520
521 521 def linkrev(self):
522 522 return self._filelog.linkrev(self._filerev)
523 523 def node(self):
524 524 return self._changectx.node()
525 525 def hex(self):
526 526 return hex(self.node())
527 527 def user(self):
528 528 return self._changectx.user()
529 529 def date(self):
530 530 return self._changectx.date()
531 531 def files(self):
532 532 return self._changectx.files()
533 533 def description(self):
534 534 return self._changectx.description()
535 535 def branch(self):
536 536 return self._changectx.branch()
537 537 def extra(self):
538 538 return self._changectx.extra()
539 539 def phase(self):
540 540 return self._changectx.phase()
541 541 def phasestr(self):
542 542 return self._changectx.phasestr()
543 543 def manifest(self):
544 544 return self._changectx.manifest()
545 545 def changectx(self):
546 546 return self._changectx
547 547
548 548 def data(self):
549 549 return self._filelog.read(self._filenode)
550 550 def path(self):
551 551 return self._path
552 552 def size(self):
553 553 return self._filelog.size(self._filerev)
554 554
555 555 def isbinary(self):
556 556 try:
557 557 return util.binary(self.data())
558 558 except IOError:
559 559 return False
560 560
561 561 def cmp(self, fctx):
562 562 """compare with other file context
563 563
564 564 returns True if different than fctx.
565 565 """
566 566 if (fctx._filerev is None
567 567 and (self._repo._encodefilterpats
568 568 # if file data starts with '\1\n', empty metadata block is
569 569 # prepended, which adds 4 bytes to filelog.size().
570 570 or self.size() - 4 == fctx.size())
571 571 or self.size() == fctx.size()):
572 572 return self._filelog.cmp(self._filenode, fctx.data())
573 573
574 574 return True
575 575
576 576 def renamed(self):
577 577 """check if file was actually renamed in this changeset revision
578 578
579 579 If rename logged in file revision, we report copy for changeset only
580 580 if file revisions linkrev points back to the changeset in question
581 581 or both changeset parents contain different file revisions.
582 582 """
583 583
584 584 renamed = self._filelog.renamed(self._filenode)
585 585 if not renamed:
586 586 return renamed
587 587
588 588 if self.rev() == self.linkrev():
589 589 return renamed
590 590
591 591 name = self.path()
592 592 fnode = self._filenode
593 593 for p in self._changectx.parents():
594 594 try:
595 595 if fnode == p.filenode(name):
596 596 return None
597 597 except error.LookupError:
598 598 pass
599 599 return renamed
600 600
601 601 def parents(self):
602 602 p = self._path
603 603 fl = self._filelog
604 604 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
605 605
606 606 r = self._filelog.renamed(self._filenode)
607 607 if r:
608 608 pl[0] = (r[0], r[1], None)
609 609
610 610 return [filectx(self._repo, p, fileid=n, filelog=l)
611 611 for p, n, l in pl if n != nullid]
612 612
613 613 def p1(self):
614 614 return self.parents()[0]
615 615
616 616 def p2(self):
617 617 p = self.parents()
618 618 if len(p) == 2:
619 619 return p[1]
620 620 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
621 621
622 622 def children(self):
623 623 # hard for renames
624 624 c = self._filelog.children(self._filenode)
625 625 return [filectx(self._repo, self._path, fileid=x,
626 626 filelog=self._filelog) for x in c]
627 627
628 628 def annotate(self, follow=False, linenumber=None, diffopts=None):
629 629 '''returns a list of tuples of (ctx, line) for each line
630 630 in the file, where ctx is the filectx of the node where
631 631 that line was last changed.
632 632 This returns tuples of ((ctx, linenumber), line) for each line,
633 633 if "linenumber" parameter is NOT "None".
634 634 In such tuples, linenumber means one at the first appearance
635 635 in the managed file.
636 636 To reduce annotation cost,
637 637 this returns fixed value(False is used) as linenumber,
638 638 if "linenumber" parameter is "False".'''
639 639
640 640 def decorate_compat(text, rev):
641 641 return ([rev] * len(text.splitlines()), text)
642 642
643 643 def without_linenumber(text, rev):
644 644 return ([(rev, False)] * len(text.splitlines()), text)
645 645
646 646 def with_linenumber(text, rev):
647 647 size = len(text.splitlines())
648 648 return ([(rev, i) for i in xrange(1, size + 1)], text)
649 649
650 650 decorate = (((linenumber is None) and decorate_compat) or
651 651 (linenumber and with_linenumber) or
652 652 without_linenumber)
653 653
654 654 def pair(parent, child):
655 655 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts,
656 656 refine=True)
657 657 for (a1, a2, b1, b2), t in blocks:
658 658 # Changed blocks ('!') or blocks made only of blank lines ('~')
659 659 # belong to the child.
660 660 if t == '=':
661 661 child[0][b1:b2] = parent[0][a1:a2]
662 662 return child
663 663
664 664 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
665 665 def getctx(path, fileid):
666 666 log = path == self._path and self._filelog or getlog(path)
667 667 return filectx(self._repo, path, fileid=fileid, filelog=log)
668 668 getctx = util.lrucachefunc(getctx)
669 669
670 670 def parents(f):
671 671 # we want to reuse filectx objects as much as possible
672 672 p = f._path
673 673 if f._filerev is None: # working dir
674 674 pl = [(n.path(), n.filerev()) for n in f.parents()]
675 675 else:
676 676 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
677 677
678 678 if follow:
679 679 r = f.renamed()
680 680 if r:
681 681 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
682 682
683 683 return [getctx(p, n) for p, n in pl if n != nullrev]
684 684
685 685 # use linkrev to find the first changeset where self appeared
686 686 if self.rev() != self.linkrev():
687 687 base = self.filectx(self.filerev())
688 688 else:
689 689 base = self
690 690
691 691 # This algorithm would prefer to be recursive, but Python is a
692 692 # bit recursion-hostile. Instead we do an iterative
693 693 # depth-first search.
694 694
695 695 visit = [base]
696 696 hist = {}
697 697 pcache = {}
698 698 needed = {base: 1}
699 699 while visit:
700 700 f = visit[-1]
701 701 if f not in pcache:
702 702 pcache[f] = parents(f)
703 703
704 704 ready = True
705 705 pl = pcache[f]
706 706 for p in pl:
707 707 if p not in hist:
708 708 ready = False
709 709 visit.append(p)
710 710 needed[p] = needed.get(p, 0) + 1
711 711 if ready:
712 712 visit.pop()
713 713 curr = decorate(f.data(), f)
714 714 for p in pl:
715 715 curr = pair(hist[p], curr)
716 716 if needed[p] == 1:
717 717 del hist[p]
718 718 else:
719 719 needed[p] -= 1
720 720
721 721 hist[f] = curr
722 722 pcache[f] = []
723 723
724 724 return zip(hist[base][0], hist[base][1].splitlines(True))
725 725
726 726 def ancestor(self, fc2, actx):
727 727 """
728 728 find the common ancestor file context, if any, of self, and fc2
729 729
730 730 actx must be the changectx of the common ancestor
731 731 of self's and fc2's respective changesets.
732 732 """
733 733
734 734 # the easy case: no (relevant) renames
735 735 if fc2.path() == self.path() and self.path() in actx:
736 736 return actx[self.path()]
737 737
738 738 # the next easiest cases: unambiguous predecessor (name trumps
739 739 # history)
740 740 if self.path() in actx and fc2.path() not in actx:
741 741 return actx[self.path()]
742 742 if fc2.path() in actx and self.path() not in actx:
743 743 return actx[fc2.path()]
744 744
745 745 # prime the ancestor cache for the working directory
746 746 acache = {}
747 747 for c in (self, fc2):
748 748 if c._filerev is None:
749 749 pl = [(n.path(), n.filenode()) for n in c.parents()]
750 750 acache[(c._path, None)] = pl
751 751
752 752 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
753 753 def parents(vertex):
754 754 if vertex in acache:
755 755 return acache[vertex]
756 756 f, n = vertex
757 757 if f not in flcache:
758 758 flcache[f] = self._repo.file(f)
759 759 fl = flcache[f]
760 760 pl = [(f, p) for p in fl.parents(n) if p != nullid]
761 761 re = fl.renamed(n)
762 762 if re:
763 763 pl.append(re)
764 764 acache[vertex] = pl
765 765 return pl
766 766
767 767 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
768 768 v = ancestor.ancestor(a, b, parents)
769 769 if v:
770 770 f, n = v
771 771 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
772 772
773 773 return None
774 774
775 775 def ancestors(self, followfirst=False):
776 776 visit = {}
777 777 c = self
778 778 cut = followfirst and 1 or None
779 779 while True:
780 780 for parent in c.parents()[:cut]:
781 781 visit[(parent.rev(), parent.node())] = parent
782 782 if not visit:
783 783 break
784 784 c = visit.pop(max(visit))
785 785 yield c
786 786
787 787 def copies(self, c2):
788 788 if not util.safehasattr(self, "_copycache"):
789 789 self._copycache = {}
790 790 sc2 = str(c2)
791 791 if sc2 not in self._copycache:
792 792 self._copycache[sc2] = copies.pathcopies(c2)
793 793 return self._copycache[sc2]
794 794
795 795 class workingctx(changectx):
796 796 """A workingctx object makes access to data related to
797 797 the current working directory convenient.
798 798 date - any valid date string or (unixtime, offset), or None.
799 799 user - username string, or None.
800 800 extra - a dictionary of extra values, or None.
801 801 changes - a list of file lists as returned by localrepo.status()
802 802 or None to use the repository status.
803 803 """
804 804 def __init__(self, repo, text="", user=None, date=None, extra=None,
805 805 changes=None):
806 806 self._repo = repo
807 807 self._rev = None
808 808 self._node = None
809 809 self._text = text
810 810 if date:
811 811 self._date = util.parsedate(date)
812 812 if user:
813 813 self._user = user
814 814 if changes:
815 815 self._status = list(changes[:4])
816 816 self._unknown = changes[4]
817 817 self._ignored = changes[5]
818 818 self._clean = changes[6]
819 819 else:
820 820 self._unknown = None
821 821 self._ignored = None
822 822 self._clean = None
823 823
824 824 self._extra = {}
825 825 if extra:
826 826 self._extra = extra.copy()
827 827 if 'branch' not in self._extra:
828 828 try:
829 829 branch = encoding.fromlocal(self._repo.dirstate.branch())
830 830 except UnicodeDecodeError:
831 831 raise util.Abort(_('branch name not in UTF-8!'))
832 832 self._extra['branch'] = branch
833 833 if self._extra['branch'] == '':
834 834 self._extra['branch'] = 'default'
835 835
836 836 def __str__(self):
837 837 return str(self._parents[0]) + "+"
838 838
839 839 def __repr__(self):
840 840 return "<workingctx %s>" % str(self)
841 841
842 842 def __nonzero__(self):
843 843 return True
844 844
845 845 def __contains__(self, key):
846 846 return self._repo.dirstate[key] not in "?r"
847 847
848 848 def _buildflagfunc(self):
849 849 # Create a fallback function for getting file flags when the
850 850 # filesystem doesn't support them
851 851
852 852 copiesget = self._repo.dirstate.copies().get
853 853
854 854 if len(self._parents) < 2:
855 855 # when we have one parent, it's easy: copy from parent
856 856 man = self._parents[0].manifest()
857 857 def func(f):
858 858 f = copiesget(f, f)
859 859 return man.flags(f)
860 860 else:
861 861 # merges are tricky: we try to reconstruct the unstored
862 862 # result from the merge (issue1802)
863 863 p1, p2 = self._parents
864 864 pa = p1.ancestor(p2)
865 865 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
866 866
867 867 def func(f):
868 868 f = copiesget(f, f) # may be wrong for merges with copies
869 869 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
870 870 if fl1 == fl2:
871 871 return fl1
872 872 if fl1 == fla:
873 873 return fl2
874 874 if fl2 == fla:
875 875 return fl1
876 876 return '' # punt for conflicts
877 877
878 878 return func
879 879
880 880 @propertycache
881 881 def _flagfunc(self):
882 882 return self._repo.dirstate.flagfunc(self._buildflagfunc)
883 883
884 884 @propertycache
885 885 def _manifest(self):
886 886 """generate a manifest corresponding to the working directory"""
887 887
888 888 man = self._parents[0].manifest().copy()
889 889 if len(self._parents) > 1:
890 890 man2 = self.p2().manifest()
891 891 def getman(f):
892 892 if f in man:
893 893 return man
894 894 return man2
895 895 else:
896 896 getman = lambda f: man
897 897
898 898 copied = self._repo.dirstate.copies()
899 899 ff = self._flagfunc
900 900 modified, added, removed, deleted = self._status
901 901 for i, l in (("a", added), ("m", modified)):
902 902 for f in l:
903 903 orig = copied.get(f, f)
904 904 man[f] = getman(orig).get(orig, nullid) + i
905 905 try:
906 906 man.set(f, ff(f))
907 907 except OSError:
908 908 pass
909 909
910 910 for f in deleted + removed:
911 911 if f in man:
912 912 del man[f]
913 913
914 914 return man
915 915
916 916 def __iter__(self):
917 917 d = self._repo.dirstate
918 918 for f in d:
919 919 if d[f] != 'r':
920 920 yield f
921 921
922 922 @propertycache
923 923 def _status(self):
924 924 return self._repo.status()[:4]
925 925
926 926 @propertycache
927 927 def _user(self):
928 928 return self._repo.ui.username()
929 929
930 930 @propertycache
931 931 def _date(self):
932 932 return util.makedate()
933 933
934 934 @propertycache
935 935 def _parents(self):
936 936 p = self._repo.dirstate.parents()
937 937 if p[1] == nullid:
938 938 p = p[:-1]
939 939 return [changectx(self._repo, x) for x in p]
940 940
941 941 def status(self, ignored=False, clean=False, unknown=False):
942 942 """Explicit status query
943 943 Unless this method is used to query the working copy status, the
944 944 _status property will implicitly read the status using its default
945 945 arguments."""
946 946 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
947 947 self._unknown = self._ignored = self._clean = None
948 948 if unknown:
949 949 self._unknown = stat[4]
950 950 if ignored:
951 951 self._ignored = stat[5]
952 952 if clean:
953 953 self._clean = stat[6]
954 954 self._status = stat[:4]
955 955 return stat
956 956
957 957 def manifest(self):
958 958 return self._manifest
959 959 def user(self):
960 960 return self._user or self._repo.ui.username()
961 961 def date(self):
962 962 return self._date
963 963 def description(self):
964 964 return self._text
965 965 def files(self):
966 966 return sorted(self._status[0] + self._status[1] + self._status[2])
967 967
968 968 def modified(self):
969 969 return self._status[0]
970 970 def added(self):
971 971 return self._status[1]
972 972 def removed(self):
973 973 return self._status[2]
974 974 def deleted(self):
975 975 return self._status[3]
976 976 def unknown(self):
977 977 assert self._unknown is not None # must call status first
978 978 return self._unknown
979 979 def ignored(self):
980 980 assert self._ignored is not None # must call status first
981 981 return self._ignored
982 982 def clean(self):
983 983 assert self._clean is not None # must call status first
984 984 return self._clean
985 985 def branch(self):
986 986 return encoding.tolocal(self._extra['branch'])
987 987 def closesbranch(self):
988 988 return 'close' in self._extra
989 989 def extra(self):
990 990 return self._extra
991 991
992 992 def tags(self):
993 993 t = []
994 994 for p in self.parents():
995 995 t.extend(p.tags())
996 996 return t
997 997
998 998 def bookmarks(self):
999 999 b = []
1000 1000 for p in self.parents():
1001 1001 b.extend(p.bookmarks())
1002 1002 return b
1003 1003
1004 1004 def phase(self):
1005 1005 phase = phases.draft # default phase to draft
1006 1006 for p in self.parents():
1007 1007 phase = max(phase, p.phase())
1008 1008 return phase
1009 1009
1010 1010 def hidden(self):
1011 1011 return False
1012 1012
1013 1013 def children(self):
1014 1014 return []
1015 1015
1016 1016 def flags(self, path):
1017 1017 if '_manifest' in self.__dict__:
1018 1018 try:
1019 1019 return self._manifest.flags(path)
1020 1020 except KeyError:
1021 1021 return ''
1022 1022
1023 1023 try:
1024 1024 return self._flagfunc(path)
1025 1025 except OSError:
1026 1026 return ''
1027 1027
1028 1028 def filectx(self, path, filelog=None):
1029 1029 """get a file context from the working directory"""
1030 1030 return workingfilectx(self._repo, path, workingctx=self,
1031 1031 filelog=filelog)
1032 1032
1033 1033 def ancestor(self, c2):
1034 1034 """return the ancestor context of self and c2"""
1035 1035 return self._parents[0].ancestor(c2) # punt on two parents for now
1036 1036
1037 1037 def walk(self, match):
1038 1038 return sorted(self._repo.dirstate.walk(match, sorted(self.substate),
1039 1039 True, False))
1040 1040
1041 1041 def dirty(self, missing=False, merge=True, branch=True):
1042 1042 "check whether a working directory is modified"
1043 1043 # check subrepos first
1044 1044 for s in sorted(self.substate):
1045 1045 if self.sub(s).dirty():
1046 1046 return True
1047 1047 # check current working dir
1048 1048 return ((merge and self.p2()) or
1049 1049 (branch and self.branch() != self.p1().branch()) or
1050 1050 self.modified() or self.added() or self.removed() or
1051 1051 (missing and self.deleted()))
1052 1052
1053 1053 def add(self, list, prefix=""):
1054 1054 join = lambda f: os.path.join(prefix, f)
1055 1055 wlock = self._repo.wlock()
1056 1056 ui, ds = self._repo.ui, self._repo.dirstate
1057 1057 try:
1058 1058 rejected = []
1059 1059 for f in list:
1060 1060 scmutil.checkportable(ui, join(f))
1061 1061 p = self._repo.wjoin(f)
1062 1062 try:
1063 1063 st = os.lstat(p)
1064 1064 except OSError:
1065 1065 ui.warn(_("%s does not exist!\n") % join(f))
1066 1066 rejected.append(f)
1067 1067 continue
1068 1068 if st.st_size > 10000000:
1069 1069 ui.warn(_("%s: up to %d MB of RAM may be required "
1070 1070 "to manage this file\n"
1071 1071 "(use 'hg revert %s' to cancel the "
1072 1072 "pending addition)\n")
1073 1073 % (f, 3 * st.st_size // 1000000, join(f)))
1074 1074 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1075 1075 ui.warn(_("%s not added: only files and symlinks "
1076 1076 "supported currently\n") % join(f))
1077 1077 rejected.append(p)
1078 1078 elif ds[f] in 'amn':
1079 1079 ui.warn(_("%s already tracked!\n") % join(f))
1080 1080 elif ds[f] == 'r':
1081 1081 ds.normallookup(f)
1082 1082 else:
1083 1083 ds.add(f)
1084 1084 return rejected
1085 1085 finally:
1086 1086 wlock.release()
1087 1087
1088 1088 def forget(self, files, prefix=""):
1089 1089 join = lambda f: os.path.join(prefix, f)
1090 1090 wlock = self._repo.wlock()
1091 1091 try:
1092 1092 rejected = []
1093 1093 for f in files:
1094 1094 if f not in self._repo.dirstate:
1095 1095 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1096 1096 rejected.append(f)
1097 1097 elif self._repo.dirstate[f] != 'a':
1098 1098 self._repo.dirstate.remove(f)
1099 1099 else:
1100 1100 self._repo.dirstate.drop(f)
1101 1101 return rejected
1102 1102 finally:
1103 1103 wlock.release()
1104 1104
1105 1105 def ancestors(self):
1106 1106 for a in self._repo.changelog.ancestors(
1107 1107 [p.rev() for p in self._parents]):
1108 1108 yield changectx(self._repo, a)
1109 1109
1110 1110 def undelete(self, list):
1111 1111 pctxs = self.parents()
1112 1112 wlock = self._repo.wlock()
1113 1113 try:
1114 1114 for f in list:
1115 1115 if self._repo.dirstate[f] != 'r':
1116 1116 self._repo.ui.warn(_("%s not removed!\n") % f)
1117 1117 else:
1118 1118 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1119 1119 t = fctx.data()
1120 1120 self._repo.wwrite(f, t, fctx.flags())
1121 1121 self._repo.dirstate.normal(f)
1122 1122 finally:
1123 1123 wlock.release()
1124 1124
1125 1125 def copy(self, source, dest):
1126 1126 p = self._repo.wjoin(dest)
1127 1127 if not os.path.lexists(p):
1128 1128 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1129 1129 elif not (os.path.isfile(p) or os.path.islink(p)):
1130 1130 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1131 1131 "symbolic link\n") % dest)
1132 1132 else:
1133 1133 wlock = self._repo.wlock()
1134 1134 try:
1135 1135 if self._repo.dirstate[dest] in '?r':
1136 1136 self._repo.dirstate.add(dest)
1137 1137 self._repo.dirstate.copy(source, dest)
1138 1138 finally:
1139 1139 wlock.release()
1140 1140
1141 1141 def dirs(self):
1142 1142 return set(self._repo.dirstate.dirs())
1143 1143
1144 1144 class workingfilectx(filectx):
1145 1145 """A workingfilectx object makes access to data related to a particular
1146 1146 file in the working directory convenient."""
1147 1147 def __init__(self, repo, path, filelog=None, workingctx=None):
1148 1148 """changeid can be a changeset revision, node, or tag.
1149 1149 fileid can be a file revision or node."""
1150 1150 self._repo = repo
1151 1151 self._path = path
1152 1152 self._changeid = None
1153 1153 self._filerev = self._filenode = None
1154 1154
1155 1155 if filelog:
1156 1156 self._filelog = filelog
1157 1157 if workingctx:
1158 1158 self._changectx = workingctx
1159 1159
1160 1160 @propertycache
1161 1161 def _changectx(self):
1162 1162 return workingctx(self._repo)
1163 1163
1164 1164 def __nonzero__(self):
1165 1165 return True
1166 1166
1167 1167 def __str__(self):
1168 1168 return "%s@%s" % (self.path(), self._changectx)
1169 1169
1170 1170 def __repr__(self):
1171 1171 return "<workingfilectx %s>" % str(self)
1172 1172
1173 1173 def data(self):
1174 1174 return self._repo.wread(self._path)
1175 1175 def renamed(self):
1176 1176 rp = self._repo.dirstate.copied(self._path)
1177 1177 if not rp:
1178 1178 return None
1179 1179 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1180 1180
1181 1181 def parents(self):
1182 1182 '''return parent filectxs, following copies if necessary'''
1183 1183 def filenode(ctx, path):
1184 1184 return ctx._manifest.get(path, nullid)
1185 1185
1186 1186 path = self._path
1187 1187 fl = self._filelog
1188 1188 pcl = self._changectx._parents
1189 1189 renamed = self.renamed()
1190 1190
1191 1191 if renamed:
1192 1192 pl = [renamed + (None,)]
1193 1193 else:
1194 1194 pl = [(path, filenode(pcl[0], path), fl)]
1195 1195
1196 1196 for pc in pcl[1:]:
1197 1197 pl.append((path, filenode(pc, path), fl))
1198 1198
1199 1199 return [filectx(self._repo, p, fileid=n, filelog=l)
1200 1200 for p, n, l in pl if n != nullid]
1201 1201
1202 1202 def children(self):
1203 1203 return []
1204 1204
1205 1205 def size(self):
1206 1206 return os.lstat(self._repo.wjoin(self._path)).st_size
1207 1207 def date(self):
1208 1208 t, tz = self._changectx.date()
1209 1209 try:
1210 1210 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
1211 1211 except OSError, err:
1212 1212 if err.errno != errno.ENOENT:
1213 1213 raise
1214 1214 return (t, tz)
1215 1215
1216 1216 def cmp(self, fctx):
1217 1217 """compare with other file context
1218 1218
1219 1219 returns True if different than fctx.
1220 1220 """
1221 1221 # fctx should be a filectx (not a workingfilectx)
1222 1222 # invert comparison to reuse the same code path
1223 1223 return fctx.cmp(self)
1224 1224
1225 1225 class memctx(object):
1226 1226 """Use memctx to perform in-memory commits via localrepo.commitctx().
1227 1227
1228 1228 Revision information is supplied at initialization time while
1229 1229 related files data and is made available through a callback
1230 1230 mechanism. 'repo' is the current localrepo, 'parents' is a
1231 1231 sequence of two parent revisions identifiers (pass None for every
1232 1232 missing parent), 'text' is the commit message and 'files' lists
1233 1233 names of files touched by the revision (normalized and relative to
1234 1234 repository root).
1235 1235
1236 1236 filectxfn(repo, memctx, path) is a callable receiving the
1237 1237 repository, the current memctx object and the normalized path of
1238 1238 requested file, relative to repository root. It is fired by the
1239 1239 commit function for every file in 'files', but calls order is
1240 1240 undefined. If the file is available in the revision being
1241 1241 committed (updated or added), filectxfn returns a memfilectx
1242 1242 object. If the file was removed, filectxfn raises an
1243 1243 IOError. Moved files are represented by marking the source file
1244 1244 removed and the new file added with copy information (see
1245 1245 memfilectx).
1246 1246
1247 1247 user receives the committer name and defaults to current
1248 1248 repository username, date is the commit date in any format
1249 1249 supported by util.parsedate() and defaults to current date, extra
1250 1250 is a dictionary of metadata or is left empty.
1251 1251 """
1252 1252 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1253 1253 date=None, extra=None):
1254 1254 self._repo = repo
1255 1255 self._rev = None
1256 1256 self._node = None
1257 1257 self._text = text
1258 1258 self._date = date and util.parsedate(date) or util.makedate()
1259 1259 self._user = user
1260 1260 parents = [(p or nullid) for p in parents]
1261 1261 p1, p2 = parents
1262 1262 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1263 1263 files = sorted(set(files))
1264 1264 self._status = [files, [], [], [], []]
1265 1265 self._filectxfn = filectxfn
1266 1266
1267 1267 self._extra = extra and extra.copy() or {}
1268 1268 if self._extra.get('branch', '') == '':
1269 1269 self._extra['branch'] = 'default'
1270 1270
1271 1271 def __str__(self):
1272 1272 return str(self._parents[0]) + "+"
1273 1273
1274 1274 def __int__(self):
1275 1275 return self._rev
1276 1276
1277 1277 def __nonzero__(self):
1278 1278 return True
1279 1279
1280 1280 def __getitem__(self, key):
1281 1281 return self.filectx(key)
1282 1282
1283 1283 def p1(self):
1284 1284 return self._parents[0]
1285 1285 def p2(self):
1286 1286 return self._parents[1]
1287 1287
1288 1288 def user(self):
1289 1289 return self._user or self._repo.ui.username()
1290 1290 def date(self):
1291 1291 return self._date
1292 1292 def description(self):
1293 1293 return self._text
1294 1294 def files(self):
1295 1295 return self.modified()
1296 1296 def modified(self):
1297 1297 return self._status[0]
1298 1298 def added(self):
1299 1299 return self._status[1]
1300 1300 def removed(self):
1301 1301 return self._status[2]
1302 1302 def deleted(self):
1303 1303 return self._status[3]
1304 1304 def unknown(self):
1305 1305 return self._status[4]
1306 1306 def ignored(self):
1307 1307 return self._status[5]
1308 1308 def clean(self):
1309 1309 return self._status[6]
1310 1310 def branch(self):
1311 1311 return encoding.tolocal(self._extra['branch'])
1312 1312 def extra(self):
1313 1313 return self._extra
1314 1314 def flags(self, f):
1315 1315 return self[f].flags()
1316 1316
1317 1317 def parents(self):
1318 1318 """return contexts for each parent changeset"""
1319 1319 return self._parents
1320 1320
1321 1321 def filectx(self, path, filelog=None):
1322 1322 """get a file context from the working directory"""
1323 1323 return self._filectxfn(self._repo, self, path)
1324 1324
1325 1325 def commit(self):
1326 1326 """commit context to the repo"""
1327 1327 return self._repo.commitctx(self)
1328 1328
1329 1329 class memfilectx(object):
1330 1330 """memfilectx represents an in-memory file to commit.
1331 1331
1332 1332 See memctx for more details.
1333 1333 """
1334 1334 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1335 1335 """
1336 1336 path is the normalized file path relative to repository root.
1337 1337 data is the file content as a string.
1338 1338 islink is True if the file is a symbolic link.
1339 1339 isexec is True if the file is executable.
1340 1340 copied is the source file path if current file was copied in the
1341 1341 revision being committed, or None."""
1342 1342 self._path = path
1343 1343 self._data = data
1344 1344 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1345 1345 self._copied = None
1346 1346 if copied:
1347 1347 self._copied = (copied, nullid)
1348 1348
1349 1349 def __nonzero__(self):
1350 1350 return True
1351 1351 def __str__(self):
1352 1352 return "%s@%s" % (self.path(), self._changectx)
1353 1353 def path(self):
1354 1354 return self._path
1355 1355 def data(self):
1356 1356 return self._data
1357 1357 def flags(self):
1358 1358 return self._flags
1359 1359 def isexec(self):
1360 1360 return 'x' in self._flags
1361 1361 def islink(self):
1362 1362 return 'l' in self._flags
1363 1363 def renamed(self):
1364 1364 return self._copied
@@ -1,90 +1,93 b''
1 1 # error.py - Mercurial exceptions
2 2 #
3 3 # Copyright 2005-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 or any later version.
7 7
8 8 """Mercurial exceptions.
9 9
10 10 This allows us to catch exceptions at higher levels without forcing
11 11 imports.
12 12 """
13 13
14 14 # Do not import anything here, please
15 15
16 16 class RevlogError(Exception):
17 17 pass
18 18
19 19 class LookupError(RevlogError, KeyError):
20 20 def __init__(self, name, index, message):
21 21 self.name = name
22 22 if isinstance(name, str) and len(name) == 20:
23 23 from node import short
24 24 name = short(name)
25 25 RevlogError.__init__(self, '%s@%s: %s' % (index, name, message))
26 26
27 27 def __str__(self):
28 28 return RevlogError.__str__(self)
29 29
30 class ManifestLookupError(LookupError):
31 pass
32
30 33 class CommandError(Exception):
31 34 """Exception raised on errors in parsing the command line."""
32 35
33 36 class Abort(Exception):
34 37 """Raised if a command needs to print an error and exit."""
35 38 def __init__(self, *args, **kw):
36 39 Exception.__init__(self, *args)
37 40 self.hint = kw.get('hint')
38 41
39 42 class ConfigError(Abort):
40 43 'Exception raised when parsing config files'
41 44
42 45 class OutOfBandError(Exception):
43 46 'Exception raised when a remote repo reports failure'
44 47
45 48 class ParseError(Exception):
46 49 'Exception raised when parsing config files (msg[, pos])'
47 50
48 51 class RepoError(Exception):
49 52 def __init__(self, *args, **kw):
50 53 Exception.__init__(self, *args)
51 54 self.hint = kw.get('hint')
52 55
53 56 class RepoLookupError(RepoError):
54 57 pass
55 58
56 59 class CapabilityError(RepoError):
57 60 pass
58 61
59 62 class RequirementError(RepoError):
60 63 """Exception raised if .hg/requires has an unknown entry."""
61 64 pass
62 65
63 66 class LockError(IOError):
64 67 def __init__(self, errno, strerror, filename, desc):
65 68 IOError.__init__(self, errno, strerror, filename)
66 69 self.desc = desc
67 70
68 71 class LockHeld(LockError):
69 72 def __init__(self, errno, filename, desc, locker):
70 73 LockError.__init__(self, errno, 'Lock held', filename, desc)
71 74 self.locker = locker
72 75
73 76 class LockUnavailable(LockError):
74 77 pass
75 78
76 79 class ResponseError(Exception):
77 80 """Raised to print an error with part of output and exit."""
78 81
79 82 class UnknownCommand(Exception):
80 83 """Exception raised if command is not in the command table."""
81 84
82 85 class AmbiguousCommand(Exception):
83 86 """Exception raised if command shortcut matches more than one command."""
84 87
85 88 # derived from KeyboardInterrupt to simplify some breakout code
86 89 class SignalInterrupt(KeyboardInterrupt):
87 90 """Exception raised on SIGTERM and SIGHUP."""
88 91
89 92 class SignatureError(Exception):
90 93 pass
@@ -1,345 +1,346 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os
10 10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 11 from common import get_stat, ErrorResponse, permhooks, caching
12 12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
13 13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 14 from request import wsgirequest
15 15 import webcommands, protocol, webutil
16 16
17 17 perms = {
18 18 'changegroup': 'pull',
19 19 'changegroupsubset': 'pull',
20 20 'getbundle': 'pull',
21 21 'stream_out': 'pull',
22 22 'listkeys': 'pull',
23 23 'unbundle': 'push',
24 24 'pushkey': 'push',
25 25 }
26 26
27 27 def makebreadcrumb(url, prefix=''):
28 28 '''Return a 'URL breadcrumb' list
29 29
30 30 A 'URL breadcrumb' is a list of URL-name pairs,
31 31 corresponding to each of the path items on a URL.
32 32 This can be used to create path navigation entries.
33 33 '''
34 34 if url.endswith('/'):
35 35 url = url[:-1]
36 36 if prefix:
37 37 url = '/' + prefix + url
38 38 relpath = url
39 39 if relpath.startswith('/'):
40 40 relpath = relpath[1:]
41 41
42 42 breadcrumb = []
43 43 urlel = url
44 44 pathitems = [''] + relpath.split('/')
45 45 for pathel in reversed(pathitems):
46 46 if not pathel or not urlel:
47 47 break
48 48 breadcrumb.append({'url': urlel, 'name': pathel})
49 49 urlel = os.path.dirname(urlel)
50 50 return reversed(breadcrumb)
51 51
52 52
53 53 class hgweb(object):
54 54 def __init__(self, repo, name=None, baseui=None):
55 55 if isinstance(repo, str):
56 56 if baseui:
57 57 u = baseui.copy()
58 58 else:
59 59 u = ui.ui()
60 60 self.repo = hg.repository(u, repo)
61 61 else:
62 62 self.repo = repo
63 63
64 64 self.repo = self._getview(self.repo)
65 65 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
66 66 self.repo.ui.setconfig('ui', 'nontty', 'true')
67 67 hook.redirect(True)
68 68 self.mtime = -1
69 69 self.size = -1
70 70 self.reponame = name
71 71 self.archives = 'zip', 'gz', 'bz2'
72 72 self.stripecount = 1
73 73 # a repo owner may set web.templates in .hg/hgrc to get any file
74 74 # readable by the user running the CGI script
75 75 self.templatepath = self.config('web', 'templates')
76 76
77 77 # The CGI scripts are often run by a user different from the repo owner.
78 78 # Trust the settings from the .hg/hgrc files by default.
79 79 def config(self, section, name, default=None, untrusted=True):
80 80 return self.repo.ui.config(section, name, default,
81 81 untrusted=untrusted)
82 82
83 83 def configbool(self, section, name, default=False, untrusted=True):
84 84 return self.repo.ui.configbool(section, name, default,
85 85 untrusted=untrusted)
86 86
87 87 def configlist(self, section, name, default=None, untrusted=True):
88 88 return self.repo.ui.configlist(section, name, default,
89 89 untrusted=untrusted)
90 90
91 91 def _getview(self, repo):
92 92 viewconfig = self.config('web', 'view', 'served')
93 93 if viewconfig == 'all':
94 94 return repo.unfiltered()
95 95 elif viewconfig in repoview.filtertable:
96 96 return repo.filtered(viewconfig)
97 97 else:
98 98 return repo.filtered('served')
99 99
100 100 def refresh(self, request=None):
101 101 if request:
102 102 self.repo.ui.environ = request.env
103 103 st = get_stat(self.repo.spath)
104 104 # compare changelog size in addition to mtime to catch
105 105 # rollbacks made less than a second ago
106 106 if st.st_mtime != self.mtime or st.st_size != self.size:
107 107 self.mtime = st.st_mtime
108 108 self.size = st.st_size
109 109 self.repo = hg.repository(self.repo.ui, self.repo.root)
110 110 self.repo = self._getview(self.repo)
111 111 self.maxchanges = int(self.config("web", "maxchanges", 10))
112 112 self.stripecount = int(self.config("web", "stripes", 1))
113 113 self.maxshortchanges = int(self.config("web", "maxshortchanges",
114 114 60))
115 115 self.maxfiles = int(self.config("web", "maxfiles", 10))
116 116 self.allowpull = self.configbool("web", "allowpull", True)
117 117 encoding.encoding = self.config("web", "encoding",
118 118 encoding.encoding)
119 119
120 120 def run(self):
121 121 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
122 122 raise RuntimeError("This function is only intended to be "
123 123 "called while running as a CGI script.")
124 124 import mercurial.hgweb.wsgicgi as wsgicgi
125 125 wsgicgi.launch(self)
126 126
127 127 def __call__(self, env, respond):
128 128 req = wsgirequest(env, respond)
129 129 return self.run_wsgi(req)
130 130
131 131 def run_wsgi(self, req):
132 132
133 133 self.refresh(req)
134 134
135 135 # work with CGI variables to create coherent structure
136 136 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
137 137
138 138 req.url = req.env['SCRIPT_NAME']
139 139 if not req.url.endswith('/'):
140 140 req.url += '/'
141 141 if 'REPO_NAME' in req.env:
142 142 req.url += req.env['REPO_NAME'] + '/'
143 143
144 144 if 'PATH_INFO' in req.env:
145 145 parts = req.env['PATH_INFO'].strip('/').split('/')
146 146 repo_parts = req.env.get('REPO_NAME', '').split('/')
147 147 if parts[:len(repo_parts)] == repo_parts:
148 148 parts = parts[len(repo_parts):]
149 149 query = '/'.join(parts)
150 150 else:
151 151 query = req.env['QUERY_STRING'].split('&', 1)[0]
152 152 query = query.split(';', 1)[0]
153 153
154 154 # process this if it's a protocol request
155 155 # protocol bits don't need to create any URLs
156 156 # and the clients always use the old URL structure
157 157
158 158 cmd = req.form.get('cmd', [''])[0]
159 159 if protocol.iscmd(cmd):
160 160 try:
161 161 if query:
162 162 raise ErrorResponse(HTTP_NOT_FOUND)
163 163 if cmd in perms:
164 164 self.check_perm(req, perms[cmd])
165 165 return protocol.call(self.repo, req, cmd)
166 166 except ErrorResponse, inst:
167 167 # A client that sends unbundle without 100-continue will
168 168 # break if we respond early.
169 169 if (cmd == 'unbundle' and
170 170 (req.env.get('HTTP_EXPECT',
171 171 '').lower() != '100-continue') or
172 172 req.env.get('X-HgHttp2', '')):
173 173 req.drain()
174 174 req.respond(inst, protocol.HGTYPE,
175 175 body='0\n%s\n' % inst.message)
176 176 return ''
177 177
178 178 # translate user-visible url structure to internal structure
179 179
180 180 args = query.split('/', 2)
181 181 if 'cmd' not in req.form and args and args[0]:
182 182
183 183 cmd = args.pop(0)
184 184 style = cmd.rfind('-')
185 185 if style != -1:
186 186 req.form['style'] = [cmd[:style]]
187 187 cmd = cmd[style + 1:]
188 188
189 189 # avoid accepting e.g. style parameter as command
190 190 if util.safehasattr(webcommands, cmd):
191 191 req.form['cmd'] = [cmd]
192 192 else:
193 193 cmd = ''
194 194
195 195 if cmd == 'static':
196 196 req.form['file'] = ['/'.join(args)]
197 197 else:
198 198 if args and args[0]:
199 199 node = args.pop(0)
200 200 req.form['node'] = [node]
201 201 if args:
202 202 req.form['file'] = args
203 203
204 204 ua = req.env.get('HTTP_USER_AGENT', '')
205 205 if cmd == 'rev' and 'mercurial' in ua:
206 206 req.form['style'] = ['raw']
207 207
208 208 if cmd == 'archive':
209 209 fn = req.form['node'][0]
210 210 for type_, spec in self.archive_specs.iteritems():
211 211 ext = spec[2]
212 212 if fn.endswith(ext):
213 213 req.form['node'] = [fn[:-len(ext)]]
214 214 req.form['type'] = [type_]
215 215
216 216 # process the web interface request
217 217
218 218 try:
219 219 tmpl = self.templater(req)
220 220 ctype = tmpl('mimetype', encoding=encoding.encoding)
221 221 ctype = templater.stringify(ctype)
222 222
223 223 # check read permissions non-static content
224 224 if cmd != 'static':
225 225 self.check_perm(req, None)
226 226
227 227 if cmd == '':
228 228 req.form['cmd'] = [tmpl.cache['default']]
229 229 cmd = req.form['cmd'][0]
230 230
231 231 if self.configbool('web', 'cache', True):
232 232 caching(self, req) # sets ETag header or raises NOT_MODIFIED
233 233 if cmd not in webcommands.__all__:
234 234 msg = 'no such method: %s' % cmd
235 235 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
236 236 elif cmd == 'file' and 'raw' in req.form.get('style', []):
237 237 self.ctype = ctype
238 238 content = webcommands.rawfile(self, req, tmpl)
239 239 else:
240 240 content = getattr(webcommands, cmd)(self, req, tmpl)
241 241 req.respond(HTTP_OK, ctype)
242 242
243 243 return content
244 244
245 245 except (error.LookupError, error.RepoLookupError), err:
246 246 req.respond(HTTP_NOT_FOUND, ctype)
247 247 msg = str(err)
248 if util.safehasattr(err, 'name') and 'manifest' not in msg:
248 if (util.safehasattr(err, 'name') and
249 not isinstance(err, error.ManifestLookupError)):
249 250 msg = 'revision not found: %s' % err.name
250 251 return tmpl('error', error=msg)
251 252 except (error.RepoError, error.RevlogError), inst:
252 253 req.respond(HTTP_SERVER_ERROR, ctype)
253 254 return tmpl('error', error=str(inst))
254 255 except ErrorResponse, inst:
255 256 req.respond(inst, ctype)
256 257 if inst.code == HTTP_NOT_MODIFIED:
257 258 # Not allowed to return a body on a 304
258 259 return ['']
259 260 return tmpl('error', error=inst.message)
260 261
261 262 def templater(self, req):
262 263
263 264 # determine scheme, port and server name
264 265 # this is needed to create absolute urls
265 266
266 267 proto = req.env.get('wsgi.url_scheme')
267 268 if proto == 'https':
268 269 proto = 'https'
269 270 default_port = "443"
270 271 else:
271 272 proto = 'http'
272 273 default_port = "80"
273 274
274 275 port = req.env["SERVER_PORT"]
275 276 port = port != default_port and (":" + port) or ""
276 277 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
277 278 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
278 279 logoimg = self.config("web", "logoimg", "hglogo.png")
279 280 staticurl = self.config("web", "staticurl") or req.url + 'static/'
280 281 if not staticurl.endswith('/'):
281 282 staticurl += '/'
282 283
283 284 # some functions for the templater
284 285
285 286 def header(**map):
286 287 yield tmpl('header', encoding=encoding.encoding, **map)
287 288
288 289 def footer(**map):
289 290 yield tmpl("footer", **map)
290 291
291 292 def motd(**map):
292 293 yield self.config("web", "motd", "")
293 294
294 295 # figure out which style to use
295 296
296 297 vars = {}
297 298 styles = (
298 299 req.form.get('style', [None])[0],
299 300 self.config('web', 'style'),
300 301 'paper',
301 302 )
302 303 style, mapfile = templater.stylemap(styles, self.templatepath)
303 304 if style == styles[0]:
304 305 vars['style'] = style
305 306
306 307 start = req.url[-1] == '?' and '&' or '?'
307 308 sessionvars = webutil.sessionvars(vars, start)
308 309
309 310 if not self.reponame:
310 311 self.reponame = (self.config("web", "name")
311 312 or req.env.get('REPO_NAME')
312 313 or req.url.strip('/') or self.repo.root)
313 314
314 315 # create the templater
315 316
316 317 tmpl = templater.templater(mapfile,
317 318 defaults={"url": req.url,
318 319 "logourl": logourl,
319 320 "logoimg": logoimg,
320 321 "staticurl": staticurl,
321 322 "urlbase": urlbase,
322 323 "repo": self.reponame,
323 324 "header": header,
324 325 "footer": footer,
325 326 "motd": motd,
326 327 "sessionvars": sessionvars,
327 328 "pathdef": makebreadcrumb(req.url),
328 329 })
329 330 return tmpl
330 331
331 332 def archivelist(self, nodeid):
332 333 allowed = self.configlist("web", "allow_archive")
333 334 for i, spec in self.archive_specs.iteritems():
334 335 if i in allowed or self.configbool("web", "allow" + i):
335 336 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
336 337
337 338 archive_specs = {
338 339 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
339 340 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
340 341 'zip': ('application/zip', 'zip', '.zip', None),
341 342 }
342 343
343 344 def check_perm(self, req, op):
344 345 for hook in permhooks:
345 346 hook(self, req, op)
General Comments 0
You need to be logged in to leave comments. Login now