##// END OF EJS Templates
Final stage of the hgweb split up....
Eric Hopper -
r2356:2db831b3 default
parent child Browse files
Show More
@@ -1,3479 +1,3481 b''
1 1 # commands.py - command processing for mercurial
2 2 #
3 3 # Copyright 2005 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 demandload import demandload
9 9 from node import *
10 10 from i18n import gettext as _
11 11 demandload(globals(), "os re sys signal shutil imp urllib pdb")
12 12 demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo")
13 demandload(globals(), "fnmatch hgweb mdiff random signal tempfile time")
13 demandload(globals(), "fnmatch mdiff random signal tempfile time")
14 14 demandload(globals(), "traceback errno socket version struct atexit sets bz2")
15 15 demandload(globals(), "archival changegroup")
16 demandload(globals(), "mercurial.hgweb.server:create_server")
17 demandload(globals(), "mercurial.hgweb:hgweb,hgwebdir")
16 18
17 19 class UnknownCommand(Exception):
18 20 """Exception raised if command is not in the command table."""
19 21 class AmbiguousCommand(Exception):
20 22 """Exception raised if command shortcut matches more than one command."""
21 23
22 24 def bail_if_changed(repo):
23 25 modified, added, removed, deleted, unknown = repo.changes()
24 26 if modified or added or removed or deleted:
25 27 raise util.Abort(_("outstanding uncommitted changes"))
26 28
27 29 def filterfiles(filters, files):
28 30 l = [x for x in files if x in filters]
29 31
30 32 for t in filters:
31 33 if t and t[-1] != "/":
32 34 t += "/"
33 35 l += [x for x in files if x.startswith(t)]
34 36 return l
35 37
36 38 def relpath(repo, args):
37 39 cwd = repo.getcwd()
38 40 if cwd:
39 41 return [util.normpath(os.path.join(cwd, x)) for x in args]
40 42 return args
41 43
42 44 def matchpats(repo, pats=[], opts={}, head=''):
43 45 cwd = repo.getcwd()
44 46 if not pats and cwd:
45 47 opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
46 48 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']]
47 49 cwd = ''
48 50 return util.cmdmatcher(repo.root, cwd, pats or ['.'], opts.get('include'),
49 51 opts.get('exclude'), head)
50 52
51 53 def makewalk(repo, pats, opts, node=None, head='', badmatch=None):
52 54 files, matchfn, anypats = matchpats(repo, pats, opts, head)
53 55 exact = dict(zip(files, files))
54 56 def walk():
55 57 for src, fn in repo.walk(node=node, files=files, match=matchfn,
56 58 badmatch=badmatch):
57 59 yield src, fn, util.pathto(repo.getcwd(), fn), fn in exact
58 60 return files, matchfn, walk()
59 61
60 62 def walk(repo, pats, opts, node=None, head='', badmatch=None):
61 63 files, matchfn, results = makewalk(repo, pats, opts, node, head, badmatch)
62 64 for r in results:
63 65 yield r
64 66
65 67 def walkchangerevs(ui, repo, pats, opts):
66 68 '''Iterate over files and the revs they changed in.
67 69
68 70 Callers most commonly need to iterate backwards over the history
69 71 it is interested in. Doing so has awful (quadratic-looking)
70 72 performance, so we use iterators in a "windowed" way.
71 73
72 74 We walk a window of revisions in the desired order. Within the
73 75 window, we first walk forwards to gather data, then in the desired
74 76 order (usually backwards) to display it.
75 77
76 78 This function returns an (iterator, getchange, matchfn) tuple. The
77 79 getchange function returns the changelog entry for a numeric
78 80 revision. The iterator yields 3-tuples. They will be of one of
79 81 the following forms:
80 82
81 83 "window", incrementing, lastrev: stepping through a window,
82 84 positive if walking forwards through revs, last rev in the
83 85 sequence iterated over - use to reset state for the current window
84 86
85 87 "add", rev, fns: out-of-order traversal of the given file names
86 88 fns, which changed during revision rev - use to gather data for
87 89 possible display
88 90
89 91 "iter", rev, None: in-order traversal of the revs earlier iterated
90 92 over with "add" - use to display data'''
91 93
92 94 def increasing_windows(start, end, windowsize=8, sizelimit=512):
93 95 if start < end:
94 96 while start < end:
95 97 yield start, min(windowsize, end-start)
96 98 start += windowsize
97 99 if windowsize < sizelimit:
98 100 windowsize *= 2
99 101 else:
100 102 while start > end:
101 103 yield start, min(windowsize, start-end-1)
102 104 start -= windowsize
103 105 if windowsize < sizelimit:
104 106 windowsize *= 2
105 107
106 108
107 109 files, matchfn, anypats = matchpats(repo, pats, opts)
108 110
109 111 if repo.changelog.count() == 0:
110 112 return [], False, matchfn
111 113
112 114 revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0']))
113 115 wanted = {}
114 116 slowpath = anypats
115 117 fncache = {}
116 118
117 119 chcache = {}
118 120 def getchange(rev):
119 121 ch = chcache.get(rev)
120 122 if ch is None:
121 123 chcache[rev] = ch = repo.changelog.read(repo.lookup(str(rev)))
122 124 return ch
123 125
124 126 if not slowpath and not files:
125 127 # No files, no patterns. Display all revs.
126 128 wanted = dict(zip(revs, revs))
127 129 if not slowpath:
128 130 # Only files, no patterns. Check the history of each file.
129 131 def filerevgen(filelog):
130 132 for i, window in increasing_windows(filelog.count()-1, -1):
131 133 revs = []
132 134 for j in xrange(i - window, i + 1):
133 135 revs.append(filelog.linkrev(filelog.node(j)))
134 136 revs.reverse()
135 137 for rev in revs:
136 138 yield rev
137 139
138 140 minrev, maxrev = min(revs), max(revs)
139 141 for file_ in files:
140 142 filelog = repo.file(file_)
141 143 # A zero count may be a directory or deleted file, so
142 144 # try to find matching entries on the slow path.
143 145 if filelog.count() == 0:
144 146 slowpath = True
145 147 break
146 148 for rev in filerevgen(filelog):
147 149 if rev <= maxrev:
148 150 if rev < minrev:
149 151 break
150 152 fncache.setdefault(rev, [])
151 153 fncache[rev].append(file_)
152 154 wanted[rev] = 1
153 155 if slowpath:
154 156 # The slow path checks files modified in every changeset.
155 157 def changerevgen():
156 158 for i, window in increasing_windows(repo.changelog.count()-1, -1):
157 159 for j in xrange(i - window, i + 1):
158 160 yield j, getchange(j)[3]
159 161
160 162 for rev, changefiles in changerevgen():
161 163 matches = filter(matchfn, changefiles)
162 164 if matches:
163 165 fncache[rev] = matches
164 166 wanted[rev] = 1
165 167
166 168 def iterate():
167 169 for i, window in increasing_windows(0, len(revs)):
168 170 yield 'window', revs[0] < revs[-1], revs[-1]
169 171 nrevs = [rev for rev in revs[i:i+window]
170 172 if rev in wanted]
171 173 srevs = list(nrevs)
172 174 srevs.sort()
173 175 for rev in srevs:
174 176 fns = fncache.get(rev) or filter(matchfn, getchange(rev)[3])
175 177 yield 'add', rev, fns
176 178 for rev in nrevs:
177 179 yield 'iter', rev, None
178 180 return iterate(), getchange, matchfn
179 181
180 182 revrangesep = ':'
181 183
182 184 def revfix(repo, val, defval):
183 185 '''turn user-level id of changeset into rev number.
184 186 user-level id can be tag, changeset, rev number, or negative rev
185 187 number relative to number of revs (-1 is tip, etc).'''
186 188 if not val:
187 189 return defval
188 190 try:
189 191 num = int(val)
190 192 if str(num) != val:
191 193 raise ValueError
192 194 if num < 0:
193 195 num += repo.changelog.count()
194 196 if num < 0:
195 197 num = 0
196 198 elif num >= repo.changelog.count():
197 199 raise ValueError
198 200 except ValueError:
199 201 try:
200 202 num = repo.changelog.rev(repo.lookup(val))
201 203 except KeyError:
202 204 raise util.Abort(_('invalid revision identifier %s'), val)
203 205 return num
204 206
205 207 def revpair(ui, repo, revs):
206 208 '''return pair of nodes, given list of revisions. second item can
207 209 be None, meaning use working dir.'''
208 210 if not revs:
209 211 return repo.dirstate.parents()[0], None
210 212 end = None
211 213 if len(revs) == 1:
212 214 start = revs[0]
213 215 if revrangesep in start:
214 216 start, end = start.split(revrangesep, 1)
215 217 start = revfix(repo, start, 0)
216 218 end = revfix(repo, end, repo.changelog.count() - 1)
217 219 else:
218 220 start = revfix(repo, start, None)
219 221 elif len(revs) == 2:
220 222 if revrangesep in revs[0] or revrangesep in revs[1]:
221 223 raise util.Abort(_('too many revisions specified'))
222 224 start = revfix(repo, revs[0], None)
223 225 end = revfix(repo, revs[1], None)
224 226 else:
225 227 raise util.Abort(_('too many revisions specified'))
226 228 if end is not None: end = repo.lookup(str(end))
227 229 return repo.lookup(str(start)), end
228 230
229 231 def revrange(ui, repo, revs):
230 232 """Yield revision as strings from a list of revision specifications."""
231 233 seen = {}
232 234 for spec in revs:
233 235 if spec.find(revrangesep) >= 0:
234 236 start, end = spec.split(revrangesep, 1)
235 237 start = revfix(repo, start, 0)
236 238 end = revfix(repo, end, repo.changelog.count() - 1)
237 239 step = start > end and -1 or 1
238 240 for rev in xrange(start, end+step, step):
239 241 if rev in seen:
240 242 continue
241 243 seen[rev] = 1
242 244 yield str(rev)
243 245 else:
244 246 rev = revfix(repo, spec, None)
245 247 if rev in seen:
246 248 continue
247 249 seen[rev] = 1
248 250 yield str(rev)
249 251
250 252 def make_filename(repo, r, pat, node=None,
251 253 total=None, seqno=None, revwidth=None, pathname=None):
252 254 node_expander = {
253 255 'H': lambda: hex(node),
254 256 'R': lambda: str(r.rev(node)),
255 257 'h': lambda: short(node),
256 258 }
257 259 expander = {
258 260 '%': lambda: '%',
259 261 'b': lambda: os.path.basename(repo.root),
260 262 }
261 263
262 264 try:
263 265 if node:
264 266 expander.update(node_expander)
265 267 if node and revwidth is not None:
266 268 expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
267 269 if total is not None:
268 270 expander['N'] = lambda: str(total)
269 271 if seqno is not None:
270 272 expander['n'] = lambda: str(seqno)
271 273 if total is not None and seqno is not None:
272 274 expander['n'] = lambda:str(seqno).zfill(len(str(total)))
273 275 if pathname is not None:
274 276 expander['s'] = lambda: os.path.basename(pathname)
275 277 expander['d'] = lambda: os.path.dirname(pathname) or '.'
276 278 expander['p'] = lambda: pathname
277 279
278 280 newname = []
279 281 patlen = len(pat)
280 282 i = 0
281 283 while i < patlen:
282 284 c = pat[i]
283 285 if c == '%':
284 286 i += 1
285 287 c = pat[i]
286 288 c = expander[c]()
287 289 newname.append(c)
288 290 i += 1
289 291 return ''.join(newname)
290 292 except KeyError, inst:
291 293 raise util.Abort(_("invalid format spec '%%%s' in output file name"),
292 294 inst.args[0])
293 295
294 296 def make_file(repo, r, pat, node=None,
295 297 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
296 298 if not pat or pat == '-':
297 299 return 'w' in mode and sys.stdout or sys.stdin
298 300 if hasattr(pat, 'write') and 'w' in mode:
299 301 return pat
300 302 if hasattr(pat, 'read') and 'r' in mode:
301 303 return pat
302 304 return open(make_filename(repo, r, pat, node, total, seqno, revwidth,
303 305 pathname),
304 306 mode)
305 307
306 308 def write_bundle(cg, filename=None, compress=True):
307 309 """Write a bundle file and return its filename.
308 310
309 311 Existing files will not be overwritten.
310 312 If no filename is specified, a temporary file is created.
311 313 bz2 compression can be turned off.
312 314 The bundle file will be deleted in case of errors.
313 315 """
314 316 class nocompress(object):
315 317 def compress(self, x):
316 318 return x
317 319 def flush(self):
318 320 return ""
319 321
320 322 fh = None
321 323 cleanup = None
322 324 try:
323 325 if filename:
324 326 if os.path.exists(filename):
325 327 raise util.Abort(_("file '%s' already exists"), filename)
326 328 fh = open(filename, "wb")
327 329 else:
328 330 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
329 331 fh = os.fdopen(fd, "wb")
330 332 cleanup = filename
331 333
332 334 if compress:
333 335 fh.write("HG10")
334 336 z = bz2.BZ2Compressor(9)
335 337 else:
336 338 fh.write("HG10UN")
337 339 z = nocompress()
338 340 # parse the changegroup data, otherwise we will block
339 341 # in case of sshrepo because we don't know the end of the stream
340 342
341 343 # an empty chunkiter is the end of the changegroup
342 344 empty = False
343 345 while not empty:
344 346 empty = True
345 347 for chunk in changegroup.chunkiter(cg):
346 348 empty = False
347 349 fh.write(z.compress(changegroup.genchunk(chunk)))
348 350 fh.write(z.compress(changegroup.closechunk()))
349 351 fh.write(z.flush())
350 352 cleanup = None
351 353 return filename
352 354 finally:
353 355 if fh is not None:
354 356 fh.close()
355 357 if cleanup is not None:
356 358 os.unlink(cleanup)
357 359
358 360 def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always,
359 361 changes=None, text=False, opts={}):
360 362 if not node1:
361 363 node1 = repo.dirstate.parents()[0]
362 364 # reading the data for node1 early allows it to play nicely
363 365 # with repo.changes and the revlog cache.
364 366 change = repo.changelog.read(node1)
365 367 mmap = repo.manifest.read(change[0])
366 368 date1 = util.datestr(change[2])
367 369
368 370 if not changes:
369 371 changes = repo.changes(node1, node2, files, match=match)
370 372 modified, added, removed, deleted, unknown = changes
371 373 if files:
372 374 modified, added, removed = map(lambda x: filterfiles(files, x),
373 375 (modified, added, removed))
374 376
375 377 if not modified and not added and not removed:
376 378 return
377 379
378 380 if node2:
379 381 change = repo.changelog.read(node2)
380 382 mmap2 = repo.manifest.read(change[0])
381 383 date2 = util.datestr(change[2])
382 384 def read(f):
383 385 return repo.file(f).read(mmap2[f])
384 386 else:
385 387 date2 = util.datestr()
386 388 def read(f):
387 389 return repo.wread(f)
388 390
389 391 if ui.quiet:
390 392 r = None
391 393 else:
392 394 hexfunc = ui.verbose and hex or short
393 395 r = [hexfunc(node) for node in [node1, node2] if node]
394 396
395 397 diffopts = ui.diffopts()
396 398 showfunc = opts.get('show_function') or diffopts['showfunc']
397 399 ignorews = opts.get('ignore_all_space') or diffopts['ignorews']
398 400 for f in modified:
399 401 to = None
400 402 if f in mmap:
401 403 to = repo.file(f).read(mmap[f])
402 404 tn = read(f)
403 405 fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
404 406 showfunc=showfunc, ignorews=ignorews))
405 407 for f in added:
406 408 to = None
407 409 tn = read(f)
408 410 fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
409 411 showfunc=showfunc, ignorews=ignorews))
410 412 for f in removed:
411 413 to = repo.file(f).read(mmap[f])
412 414 tn = None
413 415 fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
414 416 showfunc=showfunc, ignorews=ignorews))
415 417
416 418 def trimuser(ui, name, rev, revcache):
417 419 """trim the name of the user who committed a change"""
418 420 user = revcache.get(rev)
419 421 if user is None:
420 422 user = revcache[rev] = ui.shortuser(name)
421 423 return user
422 424
423 425 class changeset_printer(object):
424 426 '''show changeset information when templating not requested.'''
425 427
426 428 def __init__(self, ui, repo):
427 429 self.ui = ui
428 430 self.repo = repo
429 431
430 432 def show(self, rev=0, changenode=None, brinfo=None):
431 433 '''show a single changeset or file revision'''
432 434 log = self.repo.changelog
433 435 if changenode is None:
434 436 changenode = log.node(rev)
435 437 elif not rev:
436 438 rev = log.rev(changenode)
437 439
438 440 if self.ui.quiet:
439 441 self.ui.write("%d:%s\n" % (rev, short(changenode)))
440 442 return
441 443
442 444 changes = log.read(changenode)
443 445 date = util.datestr(changes[2])
444 446
445 447 parents = [(log.rev(p), self.ui.verbose and hex(p) or short(p))
446 448 for p in log.parents(changenode)
447 449 if self.ui.debugflag or p != nullid]
448 450 if (not self.ui.debugflag and len(parents) == 1 and
449 451 parents[0][0] == rev-1):
450 452 parents = []
451 453
452 454 if self.ui.verbose:
453 455 self.ui.write(_("changeset: %d:%s\n") % (rev, hex(changenode)))
454 456 else:
455 457 self.ui.write(_("changeset: %d:%s\n") % (rev, short(changenode)))
456 458
457 459 for tag in self.repo.nodetags(changenode):
458 460 self.ui.status(_("tag: %s\n") % tag)
459 461 for parent in parents:
460 462 self.ui.write(_("parent: %d:%s\n") % parent)
461 463
462 464 if brinfo and changenode in brinfo:
463 465 br = brinfo[changenode]
464 466 self.ui.write(_("branch: %s\n") % " ".join(br))
465 467
466 468 self.ui.debug(_("manifest: %d:%s\n") %
467 469 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
468 470 self.ui.status(_("user: %s\n") % changes[1])
469 471 self.ui.status(_("date: %s\n") % date)
470 472
471 473 if self.ui.debugflag:
472 474 files = self.repo.changes(log.parents(changenode)[0], changenode)
473 475 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
474 476 files):
475 477 if value:
476 478 self.ui.note("%-12s %s\n" % (key, " ".join(value)))
477 479 else:
478 480 self.ui.note(_("files: %s\n") % " ".join(changes[3]))
479 481
480 482 description = changes[4].strip()
481 483 if description:
482 484 if self.ui.verbose:
483 485 self.ui.status(_("description:\n"))
484 486 self.ui.status(description)
485 487 self.ui.status("\n\n")
486 488 else:
487 489 self.ui.status(_("summary: %s\n") %
488 490 description.splitlines()[0])
489 491 self.ui.status("\n")
490 492
491 493 def show_changeset(ui, repo, opts):
492 494 '''show one changeset. uses template or regular display. caller
493 495 can pass in 'style' and 'template' options in opts.'''
494 496
495 497 tmpl = opts.get('template')
496 498 if tmpl:
497 499 tmpl = templater.parsestring(tmpl, quoted=False)
498 500 else:
499 501 tmpl = ui.config('ui', 'logtemplate')
500 502 if tmpl: tmpl = templater.parsestring(tmpl)
501 503 mapfile = opts.get('style') or ui.config('ui', 'style')
502 504 if tmpl or mapfile:
503 505 if mapfile:
504 506 if not os.path.isfile(mapfile):
505 507 mapname = templater.templatepath('map-cmdline.' + mapfile)
506 508 if not mapname: mapname = templater.templatepath(mapfile)
507 509 if mapname: mapfile = mapname
508 510 try:
509 511 t = templater.changeset_templater(ui, repo, mapfile)
510 512 except SyntaxError, inst:
511 513 raise util.Abort(inst.args[0])
512 514 if tmpl: t.use_template(tmpl)
513 515 return t
514 516 return changeset_printer(ui, repo)
515 517
516 518 def show_version(ui):
517 519 """output version and copyright information"""
518 520 ui.write(_("Mercurial Distributed SCM (version %s)\n")
519 521 % version.get_version())
520 522 ui.status(_(
521 523 "\nCopyright (C) 2005 Matt Mackall <mpm@selenic.com>\n"
522 524 "This is free software; see the source for copying conditions. "
523 525 "There is NO\nwarranty; "
524 526 "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
525 527 ))
526 528
527 529 def help_(ui, cmd=None, with_version=False):
528 530 """show help for a given command or all commands"""
529 531 option_lists = []
530 532 if cmd and cmd != 'shortlist':
531 533 if with_version:
532 534 show_version(ui)
533 535 ui.write('\n')
534 536 aliases, i = find(cmd)
535 537 # synopsis
536 538 ui.write("%s\n\n" % i[2])
537 539
538 540 # description
539 541 doc = i[0].__doc__
540 542 if not doc:
541 543 doc = _("(No help text available)")
542 544 if ui.quiet:
543 545 doc = doc.splitlines(0)[0]
544 546 ui.write("%s\n" % doc.rstrip())
545 547
546 548 if not ui.quiet:
547 549 # aliases
548 550 if len(aliases) > 1:
549 551 ui.write(_("\naliases: %s\n") % ', '.join(aliases[1:]))
550 552
551 553 # options
552 554 if i[1]:
553 555 option_lists.append(("options", i[1]))
554 556
555 557 else:
556 558 # program name
557 559 if ui.verbose or with_version:
558 560 show_version(ui)
559 561 else:
560 562 ui.status(_("Mercurial Distributed SCM\n"))
561 563 ui.status('\n')
562 564
563 565 # list of commands
564 566 if cmd == "shortlist":
565 567 ui.status(_('basic commands (use "hg help" '
566 568 'for the full list or option "-v" for details):\n\n'))
567 569 elif ui.verbose:
568 570 ui.status(_('list of commands:\n\n'))
569 571 else:
570 572 ui.status(_('list of commands (use "hg help -v" '
571 573 'to show aliases and global options):\n\n'))
572 574
573 575 h = {}
574 576 cmds = {}
575 577 for c, e in table.items():
576 578 f = c.split("|")[0]
577 579 if cmd == "shortlist" and not f.startswith("^"):
578 580 continue
579 581 f = f.lstrip("^")
580 582 if not ui.debugflag and f.startswith("debug"):
581 583 continue
582 584 doc = e[0].__doc__
583 585 if not doc:
584 586 doc = _("(No help text available)")
585 587 h[f] = doc.splitlines(0)[0].rstrip()
586 588 cmds[f] = c.lstrip("^")
587 589
588 590 fns = h.keys()
589 591 fns.sort()
590 592 m = max(map(len, fns))
591 593 for f in fns:
592 594 if ui.verbose:
593 595 commands = cmds[f].replace("|",", ")
594 596 ui.write(" %s:\n %s\n"%(commands, h[f]))
595 597 else:
596 598 ui.write(' %-*s %s\n' % (m, f, h[f]))
597 599
598 600 # global options
599 601 if ui.verbose:
600 602 option_lists.append(("global options", globalopts))
601 603
602 604 # list all option lists
603 605 opt_output = []
604 606 for title, options in option_lists:
605 607 opt_output.append(("\n%s:\n" % title, None))
606 608 for shortopt, longopt, default, desc in options:
607 609 opt_output.append(("%2s%s" % (shortopt and "-%s" % shortopt,
608 610 longopt and " --%s" % longopt),
609 611 "%s%s" % (desc,
610 612 default
611 613 and _(" (default: %s)") % default
612 614 or "")))
613 615
614 616 if opt_output:
615 617 opts_len = max([len(line[0]) for line in opt_output if line[1]])
616 618 for first, second in opt_output:
617 619 if second:
618 620 ui.write(" %-*s %s\n" % (opts_len, first, second))
619 621 else:
620 622 ui.write("%s\n" % first)
621 623
622 624 # Commands start here, listed alphabetically
623 625
624 626 def add(ui, repo, *pats, **opts):
625 627 """add the specified files on the next commit
626 628
627 629 Schedule files to be version controlled and added to the repository.
628 630
629 631 The files will be added to the repository at the next commit.
630 632
631 633 If no names are given, add all files in the repository.
632 634 """
633 635
634 636 names = []
635 637 for src, abs, rel, exact in walk(repo, pats, opts):
636 638 if exact:
637 639 if ui.verbose:
638 640 ui.status(_('adding %s\n') % rel)
639 641 names.append(abs)
640 642 elif repo.dirstate.state(abs) == '?':
641 643 ui.status(_('adding %s\n') % rel)
642 644 names.append(abs)
643 645 repo.add(names)
644 646
645 647 def addremove(ui, repo, *pats, **opts):
646 648 """add all new files, delete all missing files (DEPRECATED)
647 649
648 650 (DEPRECATED)
649 651 Add all new files and remove all missing files from the repository.
650 652
651 653 New files are ignored if they match any of the patterns in .hgignore. As
652 654 with add, these changes take effect at the next commit.
653 655
654 656 This command is now deprecated and will be removed in a future
655 657 release. Please use add and remove --after instead.
656 658 """
657 659 ui.warn(_('(the addremove command is deprecated; use add and remove '
658 660 '--after instead)\n'))
659 661 return addremove_lock(ui, repo, pats, opts)
660 662
661 663 def addremove_lock(ui, repo, pats, opts, wlock=None):
662 664 add, remove = [], []
663 665 for src, abs, rel, exact in walk(repo, pats, opts):
664 666 if src == 'f' and repo.dirstate.state(abs) == '?':
665 667 add.append(abs)
666 668 if ui.verbose or not exact:
667 669 ui.status(_('adding %s\n') % ((pats and rel) or abs))
668 670 if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel):
669 671 remove.append(abs)
670 672 if ui.verbose or not exact:
671 673 ui.status(_('removing %s\n') % ((pats and rel) or abs))
672 674 repo.add(add, wlock=wlock)
673 675 repo.remove(remove, wlock=wlock)
674 676
675 677 def annotate(ui, repo, *pats, **opts):
676 678 """show changeset information per file line
677 679
678 680 List changes in files, showing the revision id responsible for each line
679 681
680 682 This command is useful to discover who did a change or when a change took
681 683 place.
682 684
683 685 Without the -a option, annotate will avoid processing files it
684 686 detects as binary. With -a, annotate will generate an annotation
685 687 anyway, probably with undesirable results.
686 688 """
687 689 def getnode(rev):
688 690 return short(repo.changelog.node(rev))
689 691
690 692 ucache = {}
691 693 def getname(rev):
692 694 cl = repo.changelog.read(repo.changelog.node(rev))
693 695 return trimuser(ui, cl[1], rev, ucache)
694 696
695 697 dcache = {}
696 698 def getdate(rev):
697 699 datestr = dcache.get(rev)
698 700 if datestr is None:
699 701 cl = repo.changelog.read(repo.changelog.node(rev))
700 702 datestr = dcache[rev] = util.datestr(cl[2])
701 703 return datestr
702 704
703 705 if not pats:
704 706 raise util.Abort(_('at least one file name or pattern required'))
705 707
706 708 opmap = [['user', getname], ['number', str], ['changeset', getnode],
707 709 ['date', getdate]]
708 710 if not opts['user'] and not opts['changeset'] and not opts['date']:
709 711 opts['number'] = 1
710 712
711 713 if opts['rev']:
712 714 node = repo.changelog.lookup(opts['rev'])
713 715 else:
714 716 node = repo.dirstate.parents()[0]
715 717 change = repo.changelog.read(node)
716 718 mmap = repo.manifest.read(change[0])
717 719
718 720 for src, abs, rel, exact in walk(repo, pats, opts, node=node):
719 721 f = repo.file(abs)
720 722 if not opts['text'] and util.binary(f.read(mmap[abs])):
721 723 ui.write(_("%s: binary file\n") % ((pats and rel) or abs))
722 724 continue
723 725
724 726 lines = f.annotate(mmap[abs])
725 727 pieces = []
726 728
727 729 for o, f in opmap:
728 730 if opts[o]:
729 731 l = [f(n) for n, dummy in lines]
730 732 if l:
731 733 m = max(map(len, l))
732 734 pieces.append(["%*s" % (m, x) for x in l])
733 735
734 736 if pieces:
735 737 for p, l in zip(zip(*pieces), lines):
736 738 ui.write("%s: %s" % (" ".join(p), l[1]))
737 739
738 740 def archive(ui, repo, dest, **opts):
739 741 '''create unversioned archive of a repository revision
740 742
741 743 By default, the revision used is the parent of the working
742 744 directory; use "-r" to specify a different revision.
743 745
744 746 To specify the type of archive to create, use "-t". Valid
745 747 types are:
746 748
747 749 "files" (default): a directory full of files
748 750 "tar": tar archive, uncompressed
749 751 "tbz2": tar archive, compressed using bzip2
750 752 "tgz": tar archive, compressed using gzip
751 753 "uzip": zip archive, uncompressed
752 754 "zip": zip archive, compressed using deflate
753 755
754 756 The exact name of the destination archive or directory is given
755 757 using a format string; see "hg help export" for details.
756 758
757 759 Each member added to an archive file has a directory prefix
758 760 prepended. Use "-p" to specify a format string for the prefix.
759 761 The default is the basename of the archive, with suffixes removed.
760 762 '''
761 763
762 764 if opts['rev']:
763 765 node = repo.lookup(opts['rev'])
764 766 else:
765 767 node, p2 = repo.dirstate.parents()
766 768 if p2 != nullid:
767 769 raise util.Abort(_('uncommitted merge - please provide a '
768 770 'specific revision'))
769 771
770 772 dest = make_filename(repo, repo.changelog, dest, node)
771 773 prefix = make_filename(repo, repo.changelog, opts['prefix'], node)
772 774 if os.path.realpath(dest) == repo.root:
773 775 raise util.Abort(_('repository root cannot be destination'))
774 776 dummy, matchfn, dummy = matchpats(repo, [], opts)
775 777 archival.archive(repo, dest, node, opts.get('type') or 'files',
776 778 not opts['no_decode'], matchfn, prefix)
777 779
778 780 def backout(ui, repo, rev, **opts):
779 781 '''reverse effect of earlier changeset
780 782
781 783 Commit the backed out changes as a new changeset. The new
782 784 changeset is a child of the backed out changeset.
783 785
784 786 If you back out a changeset other than the tip, a new head is
785 787 created. This head is the parent of the working directory. If
786 788 you back out an old changeset, your working directory will appear
787 789 old after the backout. You should merge the backout changeset
788 790 with another head.
789 791
790 792 The --merge option remembers the parent of the working directory
791 793 before starting the backout, then merges the new head with that
792 794 changeset afterwards. This saves you from doing the merge by
793 795 hand. The result of this merge is not committed, as for a normal
794 796 merge.'''
795 797
796 798 bail_if_changed(repo)
797 799 op1, op2 = repo.dirstate.parents()
798 800 if op2 != nullid:
799 801 raise util.Abort(_('outstanding uncommitted merge'))
800 802 node = repo.lookup(rev)
801 803 parent, p2 = repo.changelog.parents(node)
802 804 if parent == nullid:
803 805 raise util.Abort(_('cannot back out a change with no parents'))
804 806 if p2 != nullid:
805 807 raise util.Abort(_('cannot back out a merge'))
806 808 repo.update(node, force=True, show_stats=False)
807 809 revert_opts = opts.copy()
808 810 revert_opts['rev'] = hex(parent)
809 811 revert(ui, repo, **revert_opts)
810 812 commit_opts = opts.copy()
811 813 commit_opts['addremove'] = False
812 814 if not commit_opts['message'] and not commit_opts['logfile']:
813 815 commit_opts['message'] = _("Backed out changeset %s") % (hex(node))
814 816 commit_opts['force_editor'] = True
815 817 commit(ui, repo, **commit_opts)
816 818 def nice(node):
817 819 return '%d:%s' % (repo.changelog.rev(node), short(node))
818 820 ui.status(_('changeset %s backs out changeset %s\n') %
819 821 (nice(repo.changelog.tip()), nice(node)))
820 822 if opts['merge'] and op1 != node:
821 823 ui.status(_('merging with changeset %s\n') % nice(op1))
822 824 doupdate(ui, repo, hex(op1), **opts)
823 825
824 826 def bundle(ui, repo, fname, dest="default-push", **opts):
825 827 """create a changegroup file
826 828
827 829 Generate a compressed changegroup file collecting all changesets
828 830 not found in the other repository.
829 831
830 832 This file can then be transferred using conventional means and
831 833 applied to another repository with the unbundle command. This is
832 834 useful when native push and pull are not available or when
833 835 exporting an entire repository is undesirable. The standard file
834 836 extension is ".hg".
835 837
836 838 Unlike import/export, this exactly preserves all changeset
837 839 contents including permissions, rename data, and revision history.
838 840 """
839 841 dest = ui.expandpath(dest)
840 842 other = hg.repository(ui, dest)
841 843 o = repo.findoutgoing(other, force=opts['force'])
842 844 cg = repo.changegroup(o, 'bundle')
843 845 write_bundle(cg, fname)
844 846
845 847 def cat(ui, repo, file1, *pats, **opts):
846 848 """output the latest or given revisions of files
847 849
848 850 Print the specified files as they were at the given revision.
849 851 If no revision is given then the tip is used.
850 852
851 853 Output may be to a file, in which case the name of the file is
852 854 given using a format string. The formatting rules are the same as
853 855 for the export command, with the following additions:
854 856
855 857 %s basename of file being printed
856 858 %d dirname of file being printed, or '.' if in repo root
857 859 %p root-relative path name of file being printed
858 860 """
859 861 mf = {}
860 862 rev = opts['rev']
861 863 if rev:
862 864 node = repo.lookup(rev)
863 865 else:
864 866 node = repo.changelog.tip()
865 867 change = repo.changelog.read(node)
866 868 mf = repo.manifest.read(change[0])
867 869 for src, abs, rel, exact in walk(repo, (file1,) + pats, opts, node):
868 870 r = repo.file(abs)
869 871 n = mf[abs]
870 872 fp = make_file(repo, r, opts['output'], node=n, pathname=abs)
871 873 fp.write(r.read(n))
872 874
873 875 def clone(ui, source, dest=None, **opts):
874 876 """make a copy of an existing repository
875 877
876 878 Create a copy of an existing repository in a new directory.
877 879
878 880 If no destination directory name is specified, it defaults to the
879 881 basename of the source.
880 882
881 883 The location of the source is added to the new repository's
882 884 .hg/hgrc file, as the default to be used for future pulls.
883 885
884 886 For efficiency, hardlinks are used for cloning whenever the source
885 887 and destination are on the same filesystem. Some filesystems,
886 888 such as AFS, implement hardlinking incorrectly, but do not report
887 889 errors. In these cases, use the --pull option to avoid
888 890 hardlinking.
889 891
890 892 See pull for valid source format details.
891 893 """
892 894 if dest is None:
893 895 dest = os.path.basename(os.path.normpath(source))
894 896
895 897 if os.path.exists(dest):
896 898 raise util.Abort(_("destination '%s' already exists"), dest)
897 899
898 900 dest = os.path.realpath(dest)
899 901
900 902 class Dircleanup(object):
901 903 def __init__(self, dir_):
902 904 self.rmtree = shutil.rmtree
903 905 self.dir_ = dir_
904 906 os.mkdir(dir_)
905 907 def close(self):
906 908 self.dir_ = None
907 909 def __del__(self):
908 910 if self.dir_:
909 911 self.rmtree(self.dir_, True)
910 912
911 913 if opts['ssh']:
912 914 ui.setconfig("ui", "ssh", opts['ssh'])
913 915 if opts['remotecmd']:
914 916 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
915 917
916 918 source = ui.expandpath(source)
917 919
918 920 d = Dircleanup(dest)
919 921 abspath = source
920 922 other = hg.repository(ui, source)
921 923
922 924 copy = False
923 925 if other.dev() != -1:
924 926 abspath = os.path.abspath(source)
925 927 if not opts['pull'] and not opts['rev']:
926 928 copy = True
927 929
928 930 if copy:
929 931 try:
930 932 # we use a lock here because if we race with commit, we
931 933 # can end up with extra data in the cloned revlogs that's
932 934 # not pointed to by changesets, thus causing verify to
933 935 # fail
934 936 l1 = other.lock()
935 937 except lock.LockException:
936 938 copy = False
937 939
938 940 if copy:
939 941 # we lock here to avoid premature writing to the target
940 942 os.mkdir(os.path.join(dest, ".hg"))
941 943 l2 = lock.lock(os.path.join(dest, ".hg", "lock"))
942 944
943 945 files = "data 00manifest.d 00manifest.i 00changelog.d 00changelog.i"
944 946 for f in files.split():
945 947 src = os.path.join(source, ".hg", f)
946 948 dst = os.path.join(dest, ".hg", f)
947 949 try:
948 950 util.copyfiles(src, dst)
949 951 except OSError, inst:
950 952 if inst.errno != errno.ENOENT:
951 953 raise
952 954
953 955 repo = hg.repository(ui, dest)
954 956
955 957 else:
956 958 revs = None
957 959 if opts['rev']:
958 960 if not other.local():
959 961 error = _("clone -r not supported yet for remote repositories.")
960 962 raise util.Abort(error)
961 963 else:
962 964 revs = [other.lookup(rev) for rev in opts['rev']]
963 965 repo = hg.repository(ui, dest, create=1)
964 966 repo.pull(other, heads = revs)
965 967
966 968 f = repo.opener("hgrc", "w", text=True)
967 969 f.write("[paths]\n")
968 970 f.write("default = %s\n" % abspath)
969 971 f.close()
970 972
971 973 if not opts['noupdate']:
972 974 doupdate(repo.ui, repo)
973 975
974 976 d.close()
975 977
976 978 def commit(ui, repo, *pats, **opts):
977 979 """commit the specified files or all outstanding changes
978 980
979 981 Commit changes to the given files into the repository.
980 982
981 983 If a list of files is omitted, all changes reported by "hg status"
982 984 will be committed.
983 985
984 986 If no commit message is specified, the editor configured in your hgrc
985 987 or in the EDITOR environment variable is started to enter a message.
986 988 """
987 989 message = opts['message']
988 990 logfile = opts['logfile']
989 991
990 992 if message and logfile:
991 993 raise util.Abort(_('options --message and --logfile are mutually '
992 994 'exclusive'))
993 995 if not message and logfile:
994 996 try:
995 997 if logfile == '-':
996 998 message = sys.stdin.read()
997 999 else:
998 1000 message = open(logfile).read()
999 1001 except IOError, inst:
1000 1002 raise util.Abort(_("can't read commit message '%s': %s") %
1001 1003 (logfile, inst.strerror))
1002 1004
1003 1005 if opts['addremove']:
1004 1006 addremove_lock(ui, repo, pats, opts)
1005 1007 fns, match, anypats = matchpats(repo, pats, opts)
1006 1008 if pats:
1007 1009 modified, added, removed, deleted, unknown = (
1008 1010 repo.changes(files=fns, match=match))
1009 1011 files = modified + added + removed
1010 1012 else:
1011 1013 files = []
1012 1014 try:
1013 1015 repo.commit(files, message, opts['user'], opts['date'], match,
1014 1016 force_editor=opts.get('force_editor'))
1015 1017 except ValueError, inst:
1016 1018 raise util.Abort(str(inst))
1017 1019
1018 1020 def docopy(ui, repo, pats, opts, wlock):
1019 1021 # called with the repo lock held
1020 1022 cwd = repo.getcwd()
1021 1023 errors = 0
1022 1024 copied = []
1023 1025 targets = {}
1024 1026
1025 1027 def okaytocopy(abs, rel, exact):
1026 1028 reasons = {'?': _('is not managed'),
1027 1029 'a': _('has been marked for add'),
1028 1030 'r': _('has been marked for remove')}
1029 1031 state = repo.dirstate.state(abs)
1030 1032 reason = reasons.get(state)
1031 1033 if reason:
1032 1034 if state == 'a':
1033 1035 origsrc = repo.dirstate.copied(abs)
1034 1036 if origsrc is not None:
1035 1037 return origsrc
1036 1038 if exact:
1037 1039 ui.warn(_('%s: not copying - file %s\n') % (rel, reason))
1038 1040 else:
1039 1041 return abs
1040 1042
1041 1043 def copy(origsrc, abssrc, relsrc, target, exact):
1042 1044 abstarget = util.canonpath(repo.root, cwd, target)
1043 1045 reltarget = util.pathto(cwd, abstarget)
1044 1046 prevsrc = targets.get(abstarget)
1045 1047 if prevsrc is not None:
1046 1048 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1047 1049 (reltarget, abssrc, prevsrc))
1048 1050 return
1049 1051 if (not opts['after'] and os.path.exists(reltarget) or
1050 1052 opts['after'] and repo.dirstate.state(abstarget) not in '?r'):
1051 1053 if not opts['force']:
1052 1054 ui.warn(_('%s: not overwriting - file exists\n') %
1053 1055 reltarget)
1054 1056 return
1055 1057 if not opts['after']:
1056 1058 os.unlink(reltarget)
1057 1059 if opts['after']:
1058 1060 if not os.path.exists(reltarget):
1059 1061 return
1060 1062 else:
1061 1063 targetdir = os.path.dirname(reltarget) or '.'
1062 1064 if not os.path.isdir(targetdir):
1063 1065 os.makedirs(targetdir)
1064 1066 try:
1065 1067 restore = repo.dirstate.state(abstarget) == 'r'
1066 1068 if restore:
1067 1069 repo.undelete([abstarget], wlock)
1068 1070 try:
1069 1071 shutil.copyfile(relsrc, reltarget)
1070 1072 shutil.copymode(relsrc, reltarget)
1071 1073 restore = False
1072 1074 finally:
1073 1075 if restore:
1074 1076 repo.remove([abstarget], wlock)
1075 1077 except shutil.Error, inst:
1076 1078 raise util.Abort(str(inst))
1077 1079 except IOError, inst:
1078 1080 if inst.errno == errno.ENOENT:
1079 1081 ui.warn(_('%s: deleted in working copy\n') % relsrc)
1080 1082 else:
1081 1083 ui.warn(_('%s: cannot copy - %s\n') %
1082 1084 (relsrc, inst.strerror))
1083 1085 errors += 1
1084 1086 return
1085 1087 if ui.verbose or not exact:
1086 1088 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1087 1089 targets[abstarget] = abssrc
1088 1090 if abstarget != origsrc:
1089 1091 repo.copy(origsrc, abstarget, wlock)
1090 1092 copied.append((abssrc, relsrc, exact))
1091 1093
1092 1094 def targetpathfn(pat, dest, srcs):
1093 1095 if os.path.isdir(pat):
1094 1096 abspfx = util.canonpath(repo.root, cwd, pat)
1095 1097 if destdirexists:
1096 1098 striplen = len(os.path.split(abspfx)[0])
1097 1099 else:
1098 1100 striplen = len(abspfx)
1099 1101 if striplen:
1100 1102 striplen += len(os.sep)
1101 1103 res = lambda p: os.path.join(dest, p[striplen:])
1102 1104 elif destdirexists:
1103 1105 res = lambda p: os.path.join(dest, os.path.basename(p))
1104 1106 else:
1105 1107 res = lambda p: dest
1106 1108 return res
1107 1109
1108 1110 def targetpathafterfn(pat, dest, srcs):
1109 1111 if util.patkind(pat, None)[0]:
1110 1112 # a mercurial pattern
1111 1113 res = lambda p: os.path.join(dest, os.path.basename(p))
1112 1114 else:
1113 1115 abspfx = util.canonpath(repo.root, cwd, pat)
1114 1116 if len(abspfx) < len(srcs[0][0]):
1115 1117 # A directory. Either the target path contains the last
1116 1118 # component of the source path or it does not.
1117 1119 def evalpath(striplen):
1118 1120 score = 0
1119 1121 for s in srcs:
1120 1122 t = os.path.join(dest, s[0][striplen:])
1121 1123 if os.path.exists(t):
1122 1124 score += 1
1123 1125 return score
1124 1126
1125 1127 striplen = len(abspfx)
1126 1128 if striplen:
1127 1129 striplen += len(os.sep)
1128 1130 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1129 1131 score = evalpath(striplen)
1130 1132 striplen1 = len(os.path.split(abspfx)[0])
1131 1133 if striplen1:
1132 1134 striplen1 += len(os.sep)
1133 1135 if evalpath(striplen1) > score:
1134 1136 striplen = striplen1
1135 1137 res = lambda p: os.path.join(dest, p[striplen:])
1136 1138 else:
1137 1139 # a file
1138 1140 if destdirexists:
1139 1141 res = lambda p: os.path.join(dest, os.path.basename(p))
1140 1142 else:
1141 1143 res = lambda p: dest
1142 1144 return res
1143 1145
1144 1146
1145 1147 pats = list(pats)
1146 1148 if not pats:
1147 1149 raise util.Abort(_('no source or destination specified'))
1148 1150 if len(pats) == 1:
1149 1151 raise util.Abort(_('no destination specified'))
1150 1152 dest = pats.pop()
1151 1153 destdirexists = os.path.isdir(dest)
1152 1154 if (len(pats) > 1 or util.patkind(pats[0], None)[0]) and not destdirexists:
1153 1155 raise util.Abort(_('with multiple sources, destination must be an '
1154 1156 'existing directory'))
1155 1157 if opts['after']:
1156 1158 tfn = targetpathafterfn
1157 1159 else:
1158 1160 tfn = targetpathfn
1159 1161 copylist = []
1160 1162 for pat in pats:
1161 1163 srcs = []
1162 1164 for tag, abssrc, relsrc, exact in walk(repo, [pat], opts):
1163 1165 origsrc = okaytocopy(abssrc, relsrc, exact)
1164 1166 if origsrc:
1165 1167 srcs.append((origsrc, abssrc, relsrc, exact))
1166 1168 if not srcs:
1167 1169 continue
1168 1170 copylist.append((tfn(pat, dest, srcs), srcs))
1169 1171 if not copylist:
1170 1172 raise util.Abort(_('no files to copy'))
1171 1173
1172 1174 for targetpath, srcs in copylist:
1173 1175 for origsrc, abssrc, relsrc, exact in srcs:
1174 1176 copy(origsrc, abssrc, relsrc, targetpath(abssrc), exact)
1175 1177
1176 1178 if errors:
1177 1179 ui.warn(_('(consider using --after)\n'))
1178 1180 return errors, copied
1179 1181
1180 1182 def copy(ui, repo, *pats, **opts):
1181 1183 """mark files as copied for the next commit
1182 1184
1183 1185 Mark dest as having copies of source files. If dest is a
1184 1186 directory, copies are put in that directory. If dest is a file,
1185 1187 there can only be one source.
1186 1188
1187 1189 By default, this command copies the contents of files as they
1188 1190 stand in the working directory. If invoked with --after, the
1189 1191 operation is recorded, but no copying is performed.
1190 1192
1191 1193 This command takes effect in the next commit.
1192 1194
1193 1195 NOTE: This command should be treated as experimental. While it
1194 1196 should properly record copied files, this information is not yet
1195 1197 fully used by merge, nor fully reported by log.
1196 1198 """
1197 1199 wlock = repo.wlock(0)
1198 1200 errs, copied = docopy(ui, repo, pats, opts, wlock)
1199 1201 return errs
1200 1202
1201 1203 def debugancestor(ui, index, rev1, rev2):
1202 1204 """find the ancestor revision of two revisions in a given index"""
1203 1205 r = revlog.revlog(util.opener(os.getcwd(), audit=False), index, "", 0)
1204 1206 a = r.ancestor(r.lookup(rev1), r.lookup(rev2))
1205 1207 ui.write("%d:%s\n" % (r.rev(a), hex(a)))
1206 1208
1207 1209 def debugcomplete(ui, cmd='', **opts):
1208 1210 """returns the completion list associated with the given command"""
1209 1211
1210 1212 if opts['options']:
1211 1213 options = []
1212 1214 otables = [globalopts]
1213 1215 if cmd:
1214 1216 aliases, entry = find(cmd)
1215 1217 otables.append(entry[1])
1216 1218 for t in otables:
1217 1219 for o in t:
1218 1220 if o[0]:
1219 1221 options.append('-%s' % o[0])
1220 1222 options.append('--%s' % o[1])
1221 1223 ui.write("%s\n" % "\n".join(options))
1222 1224 return
1223 1225
1224 1226 clist = findpossible(cmd).keys()
1225 1227 clist.sort()
1226 1228 ui.write("%s\n" % "\n".join(clist))
1227 1229
1228 1230 def debugrebuildstate(ui, repo, rev=None):
1229 1231 """rebuild the dirstate as it would look like for the given revision"""
1230 1232 if not rev:
1231 1233 rev = repo.changelog.tip()
1232 1234 else:
1233 1235 rev = repo.lookup(rev)
1234 1236 change = repo.changelog.read(rev)
1235 1237 n = change[0]
1236 1238 files = repo.manifest.readflags(n)
1237 1239 wlock = repo.wlock()
1238 1240 repo.dirstate.rebuild(rev, files.iteritems())
1239 1241
1240 1242 def debugcheckstate(ui, repo):
1241 1243 """validate the correctness of the current dirstate"""
1242 1244 parent1, parent2 = repo.dirstate.parents()
1243 1245 repo.dirstate.read()
1244 1246 dc = repo.dirstate.map
1245 1247 keys = dc.keys()
1246 1248 keys.sort()
1247 1249 m1n = repo.changelog.read(parent1)[0]
1248 1250 m2n = repo.changelog.read(parent2)[0]
1249 1251 m1 = repo.manifest.read(m1n)
1250 1252 m2 = repo.manifest.read(m2n)
1251 1253 errors = 0
1252 1254 for f in dc:
1253 1255 state = repo.dirstate.state(f)
1254 1256 if state in "nr" and f not in m1:
1255 1257 ui.warn(_("%s in state %s, but not in manifest1\n") % (f, state))
1256 1258 errors += 1
1257 1259 if state in "a" and f in m1:
1258 1260 ui.warn(_("%s in state %s, but also in manifest1\n") % (f, state))
1259 1261 errors += 1
1260 1262 if state in "m" and f not in m1 and f not in m2:
1261 1263 ui.warn(_("%s in state %s, but not in either manifest\n") %
1262 1264 (f, state))
1263 1265 errors += 1
1264 1266 for f in m1:
1265 1267 state = repo.dirstate.state(f)
1266 1268 if state not in "nrm":
1267 1269 ui.warn(_("%s in manifest1, but listed as state %s") % (f, state))
1268 1270 errors += 1
1269 1271 if errors:
1270 1272 error = _(".hg/dirstate inconsistent with current parent's manifest")
1271 1273 raise util.Abort(error)
1272 1274
1273 1275 def debugconfig(ui, repo):
1274 1276 """show combined config settings from all hgrc files"""
1275 1277 for section, name, value in ui.walkconfig():
1276 1278 ui.write('%s.%s=%s\n' % (section, name, value))
1277 1279
1278 1280 def debugsetparents(ui, repo, rev1, rev2=None):
1279 1281 """manually set the parents of the current working directory
1280 1282
1281 1283 This is useful for writing repository conversion tools, but should
1282 1284 be used with care.
1283 1285 """
1284 1286
1285 1287 if not rev2:
1286 1288 rev2 = hex(nullid)
1287 1289
1288 1290 repo.dirstate.setparents(repo.lookup(rev1), repo.lookup(rev2))
1289 1291
1290 1292 def debugstate(ui, repo):
1291 1293 """show the contents of the current dirstate"""
1292 1294 repo.dirstate.read()
1293 1295 dc = repo.dirstate.map
1294 1296 keys = dc.keys()
1295 1297 keys.sort()
1296 1298 for file_ in keys:
1297 1299 ui.write("%c %3o %10d %s %s\n"
1298 1300 % (dc[file_][0], dc[file_][1] & 0777, dc[file_][2],
1299 1301 time.strftime("%x %X",
1300 1302 time.localtime(dc[file_][3])), file_))
1301 1303 for f in repo.dirstate.copies:
1302 1304 ui.write(_("copy: %s -> %s\n") % (repo.dirstate.copies[f], f))
1303 1305
1304 1306 def debugdata(ui, file_, rev):
1305 1307 """dump the contents of an data file revision"""
1306 1308 r = revlog.revlog(util.opener(os.getcwd(), audit=False),
1307 1309 file_[:-2] + ".i", file_, 0)
1308 1310 try:
1309 1311 ui.write(r.revision(r.lookup(rev)))
1310 1312 except KeyError:
1311 1313 raise util.Abort(_('invalid revision identifier %s'), rev)
1312 1314
1313 1315 def debugindex(ui, file_):
1314 1316 """dump the contents of an index file"""
1315 1317 r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_, "", 0)
1316 1318 ui.write(" rev offset length base linkrev" +
1317 1319 " nodeid p1 p2\n")
1318 1320 for i in range(r.count()):
1319 1321 node = r.node(i)
1320 1322 pp = r.parents(node)
1321 1323 ui.write("% 6d % 9d % 7d % 6d % 7d %s %s %s\n" % (
1322 1324 i, r.start(i), r.length(i), r.base(i), r.linkrev(node),
1323 1325 short(node), short(pp[0]), short(pp[1])))
1324 1326
1325 1327 def debugindexdot(ui, file_):
1326 1328 """dump an index DAG as a .dot file"""
1327 1329 r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_, "", 0)
1328 1330 ui.write("digraph G {\n")
1329 1331 for i in range(r.count()):
1330 1332 node = r.node(i)
1331 1333 pp = r.parents(node)
1332 1334 ui.write("\t%d -> %d\n" % (r.rev(pp[0]), i))
1333 1335 if pp[1] != nullid:
1334 1336 ui.write("\t%d -> %d\n" % (r.rev(pp[1]), i))
1335 1337 ui.write("}\n")
1336 1338
1337 1339 def debugrename(ui, repo, file, rev=None):
1338 1340 """dump rename information"""
1339 1341 r = repo.file(relpath(repo, [file])[0])
1340 1342 if rev:
1341 1343 try:
1342 1344 # assume all revision numbers are for changesets
1343 1345 n = repo.lookup(rev)
1344 1346 change = repo.changelog.read(n)
1345 1347 m = repo.manifest.read(change[0])
1346 1348 n = m[relpath(repo, [file])[0]]
1347 1349 except (hg.RepoError, KeyError):
1348 1350 n = r.lookup(rev)
1349 1351 else:
1350 1352 n = r.tip()
1351 1353 m = r.renamed(n)
1352 1354 if m:
1353 1355 ui.write(_("renamed from %s:%s\n") % (m[0], hex(m[1])))
1354 1356 else:
1355 1357 ui.write(_("not renamed\n"))
1356 1358
1357 1359 def debugwalk(ui, repo, *pats, **opts):
1358 1360 """show how files match on given patterns"""
1359 1361 items = list(walk(repo, pats, opts))
1360 1362 if not items:
1361 1363 return
1362 1364 fmt = '%%s %%-%ds %%-%ds %%s' % (
1363 1365 max([len(abs) for (src, abs, rel, exact) in items]),
1364 1366 max([len(rel) for (src, abs, rel, exact) in items]))
1365 1367 for src, abs, rel, exact in items:
1366 1368 line = fmt % (src, abs, rel, exact and 'exact' or '')
1367 1369 ui.write("%s\n" % line.rstrip())
1368 1370
1369 1371 def diff(ui, repo, *pats, **opts):
1370 1372 """diff repository (or selected files)
1371 1373
1372 1374 Show differences between revisions for the specified files.
1373 1375
1374 1376 Differences between files are shown using the unified diff format.
1375 1377
1376 1378 When two revision arguments are given, then changes are shown
1377 1379 between those revisions. If only one revision is specified then
1378 1380 that revision is compared to the working directory, and, when no
1379 1381 revisions are specified, the working directory files are compared
1380 1382 to its parent.
1381 1383
1382 1384 Without the -a option, diff will avoid generating diffs of files
1383 1385 it detects as binary. With -a, diff will generate a diff anyway,
1384 1386 probably with undesirable results.
1385 1387 """
1386 1388 node1, node2 = revpair(ui, repo, opts['rev'])
1387 1389
1388 1390 fns, matchfn, anypats = matchpats(repo, pats, opts)
1389 1391
1390 1392 dodiff(sys.stdout, ui, repo, node1, node2, fns, match=matchfn,
1391 1393 text=opts['text'], opts=opts)
1392 1394
1393 1395 def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
1394 1396 node = repo.lookup(changeset)
1395 1397 parents = [p for p in repo.changelog.parents(node) if p != nullid]
1396 1398 if opts['switch_parent']:
1397 1399 parents.reverse()
1398 1400 prev = (parents and parents[0]) or nullid
1399 1401 change = repo.changelog.read(node)
1400 1402
1401 1403 fp = make_file(repo, repo.changelog, opts['output'],
1402 1404 node=node, total=total, seqno=seqno,
1403 1405 revwidth=revwidth)
1404 1406 if fp != sys.stdout:
1405 1407 ui.note("%s\n" % fp.name)
1406 1408
1407 1409 fp.write("# HG changeset patch\n")
1408 1410 fp.write("# User %s\n" % change[1])
1409 1411 fp.write("# Date %d %d\n" % change[2])
1410 1412 fp.write("# Node ID %s\n" % hex(node))
1411 1413 fp.write("# Parent %s\n" % hex(prev))
1412 1414 if len(parents) > 1:
1413 1415 fp.write("# Parent %s\n" % hex(parents[1]))
1414 1416 fp.write(change[4].rstrip())
1415 1417 fp.write("\n\n")
1416 1418
1417 1419 dodiff(fp, ui, repo, prev, node, text=opts['text'])
1418 1420 if fp != sys.stdout:
1419 1421 fp.close()
1420 1422
1421 1423 def export(ui, repo, *changesets, **opts):
1422 1424 """dump the header and diffs for one or more changesets
1423 1425
1424 1426 Print the changeset header and diffs for one or more revisions.
1425 1427
1426 1428 The information shown in the changeset header is: author,
1427 1429 changeset hash, parent and commit comment.
1428 1430
1429 1431 Output may be to a file, in which case the name of the file is
1430 1432 given using a format string. The formatting rules are as follows:
1431 1433
1432 1434 %% literal "%" character
1433 1435 %H changeset hash (40 bytes of hexadecimal)
1434 1436 %N number of patches being generated
1435 1437 %R changeset revision number
1436 1438 %b basename of the exporting repository
1437 1439 %h short-form changeset hash (12 bytes of hexadecimal)
1438 1440 %n zero-padded sequence number, starting at 1
1439 1441 %r zero-padded changeset revision number
1440 1442
1441 1443 Without the -a option, export will avoid generating diffs of files
1442 1444 it detects as binary. With -a, export will generate a diff anyway,
1443 1445 probably with undesirable results.
1444 1446
1445 1447 With the --switch-parent option, the diff will be against the second
1446 1448 parent. It can be useful to review a merge.
1447 1449 """
1448 1450 if not changesets:
1449 1451 raise util.Abort(_("export requires at least one changeset"))
1450 1452 seqno = 0
1451 1453 revs = list(revrange(ui, repo, changesets))
1452 1454 total = len(revs)
1453 1455 revwidth = max(map(len, revs))
1454 1456 msg = len(revs) > 1 and _("Exporting patches:\n") or _("Exporting patch:\n")
1455 1457 ui.note(msg)
1456 1458 for cset in revs:
1457 1459 seqno += 1
1458 1460 doexport(ui, repo, cset, seqno, total, revwidth, opts)
1459 1461
1460 1462 def forget(ui, repo, *pats, **opts):
1461 1463 """don't add the specified files on the next commit (DEPRECATED)
1462 1464
1463 1465 (DEPRECATED)
1464 1466 Undo an 'hg add' scheduled for the next commit.
1465 1467
1466 1468 This command is now deprecated and will be removed in a future
1467 1469 release. Please use revert instead.
1468 1470 """
1469 1471 ui.warn(_("(the forget command is deprecated; use revert instead)\n"))
1470 1472 forget = []
1471 1473 for src, abs, rel, exact in walk(repo, pats, opts):
1472 1474 if repo.dirstate.state(abs) == 'a':
1473 1475 forget.append(abs)
1474 1476 if ui.verbose or not exact:
1475 1477 ui.status(_('forgetting %s\n') % ((pats and rel) or abs))
1476 1478 repo.forget(forget)
1477 1479
1478 1480 def grep(ui, repo, pattern, *pats, **opts):
1479 1481 """search for a pattern in specified files and revisions
1480 1482
1481 1483 Search revisions of files for a regular expression.
1482 1484
1483 1485 This command behaves differently than Unix grep. It only accepts
1484 1486 Python/Perl regexps. It searches repository history, not the
1485 1487 working directory. It always prints the revision number in which
1486 1488 a match appears.
1487 1489
1488 1490 By default, grep only prints output for the first revision of a
1489 1491 file in which it finds a match. To get it to print every revision
1490 1492 that contains a change in match status ("-" for a match that
1491 1493 becomes a non-match, or "+" for a non-match that becomes a match),
1492 1494 use the --all flag.
1493 1495 """
1494 1496 reflags = 0
1495 1497 if opts['ignore_case']:
1496 1498 reflags |= re.I
1497 1499 regexp = re.compile(pattern, reflags)
1498 1500 sep, eol = ':', '\n'
1499 1501 if opts['print0']:
1500 1502 sep = eol = '\0'
1501 1503
1502 1504 fcache = {}
1503 1505 def getfile(fn):
1504 1506 if fn not in fcache:
1505 1507 fcache[fn] = repo.file(fn)
1506 1508 return fcache[fn]
1507 1509
1508 1510 def matchlines(body):
1509 1511 begin = 0
1510 1512 linenum = 0
1511 1513 while True:
1512 1514 match = regexp.search(body, begin)
1513 1515 if not match:
1514 1516 break
1515 1517 mstart, mend = match.span()
1516 1518 linenum += body.count('\n', begin, mstart) + 1
1517 1519 lstart = body.rfind('\n', begin, mstart) + 1 or begin
1518 1520 lend = body.find('\n', mend)
1519 1521 yield linenum, mstart - lstart, mend - lstart, body[lstart:lend]
1520 1522 begin = lend + 1
1521 1523
1522 1524 class linestate(object):
1523 1525 def __init__(self, line, linenum, colstart, colend):
1524 1526 self.line = line
1525 1527 self.linenum = linenum
1526 1528 self.colstart = colstart
1527 1529 self.colend = colend
1528 1530 def __eq__(self, other):
1529 1531 return self.line == other.line
1530 1532 def __hash__(self):
1531 1533 return hash(self.line)
1532 1534
1533 1535 matches = {}
1534 1536 def grepbody(fn, rev, body):
1535 1537 matches[rev].setdefault(fn, {})
1536 1538 m = matches[rev][fn]
1537 1539 for lnum, cstart, cend, line in matchlines(body):
1538 1540 s = linestate(line, lnum, cstart, cend)
1539 1541 m[s] = s
1540 1542
1541 1543 # FIXME: prev isn't used, why ?
1542 1544 prev = {}
1543 1545 ucache = {}
1544 1546 def display(fn, rev, states, prevstates):
1545 1547 diff = list(sets.Set(states).symmetric_difference(sets.Set(prevstates)))
1546 1548 diff.sort(lambda x, y: cmp(x.linenum, y.linenum))
1547 1549 counts = {'-': 0, '+': 0}
1548 1550 filerevmatches = {}
1549 1551 for l in diff:
1550 1552 if incrementing or not opts['all']:
1551 1553 change = ((l in prevstates) and '-') or '+'
1552 1554 r = rev
1553 1555 else:
1554 1556 change = ((l in states) and '-') or '+'
1555 1557 r = prev[fn]
1556 1558 cols = [fn, str(rev)]
1557 1559 if opts['line_number']:
1558 1560 cols.append(str(l.linenum))
1559 1561 if opts['all']:
1560 1562 cols.append(change)
1561 1563 if opts['user']:
1562 1564 cols.append(trimuser(ui, getchange(rev)[1], rev,
1563 1565 ucache))
1564 1566 if opts['files_with_matches']:
1565 1567 c = (fn, rev)
1566 1568 if c in filerevmatches:
1567 1569 continue
1568 1570 filerevmatches[c] = 1
1569 1571 else:
1570 1572 cols.append(l.line)
1571 1573 ui.write(sep.join(cols), eol)
1572 1574 counts[change] += 1
1573 1575 return counts['+'], counts['-']
1574 1576
1575 1577 fstate = {}
1576 1578 skip = {}
1577 1579 changeiter, getchange, matchfn = walkchangerevs(ui, repo, pats, opts)
1578 1580 count = 0
1579 1581 incrementing = False
1580 1582 for st, rev, fns in changeiter:
1581 1583 if st == 'window':
1582 1584 incrementing = rev
1583 1585 matches.clear()
1584 1586 elif st == 'add':
1585 1587 change = repo.changelog.read(repo.lookup(str(rev)))
1586 1588 mf = repo.manifest.read(change[0])
1587 1589 matches[rev] = {}
1588 1590 for fn in fns:
1589 1591 if fn in skip:
1590 1592 continue
1591 1593 fstate.setdefault(fn, {})
1592 1594 try:
1593 1595 grepbody(fn, rev, getfile(fn).read(mf[fn]))
1594 1596 except KeyError:
1595 1597 pass
1596 1598 elif st == 'iter':
1597 1599 states = matches[rev].items()
1598 1600 states.sort()
1599 1601 for fn, m in states:
1600 1602 if fn in skip:
1601 1603 continue
1602 1604 if incrementing or not opts['all'] or fstate[fn]:
1603 1605 pos, neg = display(fn, rev, m, fstate[fn])
1604 1606 count += pos + neg
1605 1607 if pos and not opts['all']:
1606 1608 skip[fn] = True
1607 1609 fstate[fn] = m
1608 1610 prev[fn] = rev
1609 1611
1610 1612 if not incrementing:
1611 1613 fstate = fstate.items()
1612 1614 fstate.sort()
1613 1615 for fn, state in fstate:
1614 1616 if fn in skip:
1615 1617 continue
1616 1618 display(fn, rev, {}, state)
1617 1619 return (count == 0 and 1) or 0
1618 1620
1619 1621 def heads(ui, repo, **opts):
1620 1622 """show current repository heads
1621 1623
1622 1624 Show all repository head changesets.
1623 1625
1624 1626 Repository "heads" are changesets that don't have children
1625 1627 changesets. They are where development generally takes place and
1626 1628 are the usual targets for update and merge operations.
1627 1629 """
1628 1630 if opts['rev']:
1629 1631 heads = repo.heads(repo.lookup(opts['rev']))
1630 1632 else:
1631 1633 heads = repo.heads()
1632 1634 br = None
1633 1635 if opts['branches']:
1634 1636 br = repo.branchlookup(heads)
1635 1637 displayer = show_changeset(ui, repo, opts)
1636 1638 for n in heads:
1637 1639 displayer.show(changenode=n, brinfo=br)
1638 1640
1639 1641 def identify(ui, repo):
1640 1642 """print information about the working copy
1641 1643
1642 1644 Print a short summary of the current state of the repo.
1643 1645
1644 1646 This summary identifies the repository state using one or two parent
1645 1647 hash identifiers, followed by a "+" if there are uncommitted changes
1646 1648 in the working directory, followed by a list of tags for this revision.
1647 1649 """
1648 1650 parents = [p for p in repo.dirstate.parents() if p != nullid]
1649 1651 if not parents:
1650 1652 ui.write(_("unknown\n"))
1651 1653 return
1652 1654
1653 1655 hexfunc = ui.verbose and hex or short
1654 1656 modified, added, removed, deleted, unknown = repo.changes()
1655 1657 output = ["%s%s" %
1656 1658 ('+'.join([hexfunc(parent) for parent in parents]),
1657 1659 (modified or added or removed or deleted) and "+" or "")]
1658 1660
1659 1661 if not ui.quiet:
1660 1662 # multiple tags for a single parent separated by '/'
1661 1663 parenttags = ['/'.join(tags)
1662 1664 for tags in map(repo.nodetags, parents) if tags]
1663 1665 # tags for multiple parents separated by ' + '
1664 1666 if parenttags:
1665 1667 output.append(' + '.join(parenttags))
1666 1668
1667 1669 ui.write("%s\n" % ' '.join(output))
1668 1670
1669 1671 def import_(ui, repo, patch1, *patches, **opts):
1670 1672 """import an ordered set of patches
1671 1673
1672 1674 Import a list of patches and commit them individually.
1673 1675
1674 1676 If there are outstanding changes in the working directory, import
1675 1677 will abort unless given the -f flag.
1676 1678
1677 1679 If a patch looks like a mail message (its first line starts with
1678 1680 "From " or looks like an RFC822 header), it will not be applied
1679 1681 unless the -f option is used. The importer neither parses nor
1680 1682 discards mail headers, so use -f only to override the "mailness"
1681 1683 safety check, not to import a real mail message.
1682 1684 """
1683 1685 patches = (patch1,) + patches
1684 1686
1685 1687 if not opts['force']:
1686 1688 bail_if_changed(repo)
1687 1689
1688 1690 d = opts["base"]
1689 1691 strip = opts["strip"]
1690 1692
1691 1693 mailre = re.compile(r'(?:From |[\w-]+:)')
1692 1694
1693 1695 # attempt to detect the start of a patch
1694 1696 # (this heuristic is borrowed from quilt)
1695 1697 diffre = re.compile(r'(?:Index:[ \t]|diff[ \t]|RCS file: |' +
1696 1698 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
1697 1699 '(---|\*\*\*)[ \t])')
1698 1700
1699 1701 for patch in patches:
1700 1702 ui.status(_("applying %s\n") % patch)
1701 1703 pf = os.path.join(d, patch)
1702 1704
1703 1705 message = []
1704 1706 user = None
1705 1707 date = None
1706 1708 hgpatch = False
1707 1709 for line in file(pf):
1708 1710 line = line.rstrip()
1709 1711 if (not message and not hgpatch and
1710 1712 mailre.match(line) and not opts['force']):
1711 1713 if len(line) > 35:
1712 1714 line = line[:32] + '...'
1713 1715 raise util.Abort(_('first line looks like a '
1714 1716 'mail header: ') + line)
1715 1717 if diffre.match(line):
1716 1718 break
1717 1719 elif hgpatch:
1718 1720 # parse values when importing the result of an hg export
1719 1721 if line.startswith("# User "):
1720 1722 user = line[7:]
1721 1723 ui.debug(_('User: %s\n') % user)
1722 1724 elif line.startswith("# Date "):
1723 1725 date = line[7:]
1724 1726 elif not line.startswith("# ") and line:
1725 1727 message.append(line)
1726 1728 hgpatch = False
1727 1729 elif line == '# HG changeset patch':
1728 1730 hgpatch = True
1729 1731 message = [] # We may have collected garbage
1730 1732 elif message or line:
1731 1733 message.append(line)
1732 1734
1733 1735 # make sure message isn't empty
1734 1736 if not message:
1735 1737 message = _("imported patch %s\n") % patch
1736 1738 else:
1737 1739 message = '\n'.join(message).rstrip()
1738 1740 ui.debug(_('message:\n%s\n') % message)
1739 1741
1740 1742 files = util.patch(strip, pf, ui)
1741 1743
1742 1744 if len(files) > 0:
1743 1745 addremove_lock(ui, repo, files, {})
1744 1746 repo.commit(files, message, user, date)
1745 1747
1746 1748 def incoming(ui, repo, source="default", **opts):
1747 1749 """show new changesets found in source
1748 1750
1749 1751 Show new changesets found in the specified path/URL or the default
1750 1752 pull location. These are the changesets that would be pulled if a pull
1751 1753 was requested.
1752 1754
1753 1755 For remote repository, using --bundle avoids downloading the changesets
1754 1756 twice if the incoming is followed by a pull.
1755 1757
1756 1758 See pull for valid source format details.
1757 1759 """
1758 1760 source = ui.expandpath(source)
1759 1761 if opts['ssh']:
1760 1762 ui.setconfig("ui", "ssh", opts['ssh'])
1761 1763 if opts['remotecmd']:
1762 1764 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
1763 1765
1764 1766 other = hg.repository(ui, source)
1765 1767 incoming = repo.findincoming(other, force=opts["force"])
1766 1768 if not incoming:
1767 1769 ui.status(_("no changes found\n"))
1768 1770 return
1769 1771
1770 1772 cleanup = None
1771 1773 try:
1772 1774 fname = opts["bundle"]
1773 1775 if fname or not other.local():
1774 1776 # create a bundle (uncompressed if other repo is not local)
1775 1777 cg = other.changegroup(incoming, "incoming")
1776 1778 fname = cleanup = write_bundle(cg, fname, compress=other.local())
1777 1779 # keep written bundle?
1778 1780 if opts["bundle"]:
1779 1781 cleanup = None
1780 1782 if not other.local():
1781 1783 # use the created uncompressed bundlerepo
1782 1784 other = bundlerepo.bundlerepository(ui, repo.root, fname)
1783 1785
1784 1786 o = other.changelog.nodesbetween(incoming)[0]
1785 1787 if opts['newest_first']:
1786 1788 o.reverse()
1787 1789 displayer = show_changeset(ui, other, opts)
1788 1790 for n in o:
1789 1791 parents = [p for p in other.changelog.parents(n) if p != nullid]
1790 1792 if opts['no_merges'] and len(parents) == 2:
1791 1793 continue
1792 1794 displayer.show(changenode=n)
1793 1795 if opts['patch']:
1794 1796 prev = (parents and parents[0]) or nullid
1795 1797 dodiff(ui, ui, other, prev, n)
1796 1798 ui.write("\n")
1797 1799 finally:
1798 1800 if hasattr(other, 'close'):
1799 1801 other.close()
1800 1802 if cleanup:
1801 1803 os.unlink(cleanup)
1802 1804
1803 1805 def init(ui, dest="."):
1804 1806 """create a new repository in the given directory
1805 1807
1806 1808 Initialize a new repository in the given directory. If the given
1807 1809 directory does not exist, it is created.
1808 1810
1809 1811 If no directory is given, the current directory is used.
1810 1812 """
1811 1813 if not os.path.exists(dest):
1812 1814 os.mkdir(dest)
1813 1815 hg.repository(ui, dest, create=1)
1814 1816
1815 1817 def locate(ui, repo, *pats, **opts):
1816 1818 """locate files matching specific patterns
1817 1819
1818 1820 Print all files under Mercurial control whose names match the
1819 1821 given patterns.
1820 1822
1821 1823 This command searches the current directory and its
1822 1824 subdirectories. To search an entire repository, move to the root
1823 1825 of the repository.
1824 1826
1825 1827 If no patterns are given to match, this command prints all file
1826 1828 names.
1827 1829
1828 1830 If you want to feed the output of this command into the "xargs"
1829 1831 command, use the "-0" option to both this command and "xargs".
1830 1832 This will avoid the problem of "xargs" treating single filenames
1831 1833 that contain white space as multiple filenames.
1832 1834 """
1833 1835 end = opts['print0'] and '\0' or '\n'
1834 1836 rev = opts['rev']
1835 1837 if rev:
1836 1838 node = repo.lookup(rev)
1837 1839 else:
1838 1840 node = None
1839 1841
1840 1842 for src, abs, rel, exact in walk(repo, pats, opts, node=node,
1841 1843 head='(?:.*/|)'):
1842 1844 if not node and repo.dirstate.state(abs) == '?':
1843 1845 continue
1844 1846 if opts['fullpath']:
1845 1847 ui.write(os.path.join(repo.root, abs), end)
1846 1848 else:
1847 1849 ui.write(((pats and rel) or abs), end)
1848 1850
1849 1851 def log(ui, repo, *pats, **opts):
1850 1852 """show revision history of entire repository or files
1851 1853
1852 1854 Print the revision history of the specified files or the entire project.
1853 1855
1854 1856 By default this command outputs: changeset id and hash, tags,
1855 1857 non-trivial parents, user, date and time, and a summary for each
1856 1858 commit. When the -v/--verbose switch is used, the list of changed
1857 1859 files and full commit message is shown.
1858 1860 """
1859 1861 class dui(object):
1860 1862 # Implement and delegate some ui protocol. Save hunks of
1861 1863 # output for later display in the desired order.
1862 1864 def __init__(self, ui):
1863 1865 self.ui = ui
1864 1866 self.hunk = {}
1865 1867 self.header = {}
1866 1868 def bump(self, rev):
1867 1869 self.rev = rev
1868 1870 self.hunk[rev] = []
1869 1871 self.header[rev] = []
1870 1872 def note(self, *args):
1871 1873 if self.verbose:
1872 1874 self.write(*args)
1873 1875 def status(self, *args):
1874 1876 if not self.quiet:
1875 1877 self.write(*args)
1876 1878 def write(self, *args):
1877 1879 self.hunk[self.rev].append(args)
1878 1880 def write_header(self, *args):
1879 1881 self.header[self.rev].append(args)
1880 1882 def debug(self, *args):
1881 1883 if self.debugflag:
1882 1884 self.write(*args)
1883 1885 def __getattr__(self, key):
1884 1886 return getattr(self.ui, key)
1885 1887
1886 1888 changeiter, getchange, matchfn = walkchangerevs(ui, repo, pats, opts)
1887 1889
1888 1890 if opts['limit']:
1889 1891 try:
1890 1892 limit = int(opts['limit'])
1891 1893 except ValueError:
1892 1894 raise util.Abort(_('limit must be a positive integer'))
1893 1895 if limit <= 0: raise util.Abort(_('limit must be positive'))
1894 1896 else:
1895 1897 limit = sys.maxint
1896 1898 count = 0
1897 1899
1898 1900 displayer = show_changeset(ui, repo, opts)
1899 1901 for st, rev, fns in changeiter:
1900 1902 if st == 'window':
1901 1903 du = dui(ui)
1902 1904 displayer.ui = du
1903 1905 elif st == 'add':
1904 1906 du.bump(rev)
1905 1907 changenode = repo.changelog.node(rev)
1906 1908 parents = [p for p in repo.changelog.parents(changenode)
1907 1909 if p != nullid]
1908 1910 if opts['no_merges'] and len(parents) == 2:
1909 1911 continue
1910 1912 if opts['only_merges'] and len(parents) != 2:
1911 1913 continue
1912 1914
1913 1915 if opts['keyword']:
1914 1916 changes = getchange(rev)
1915 1917 miss = 0
1916 1918 for k in [kw.lower() for kw in opts['keyword']]:
1917 1919 if not (k in changes[1].lower() or
1918 1920 k in changes[4].lower() or
1919 1921 k in " ".join(changes[3][:20]).lower()):
1920 1922 miss = 1
1921 1923 break
1922 1924 if miss:
1923 1925 continue
1924 1926
1925 1927 br = None
1926 1928 if opts['branches']:
1927 1929 br = repo.branchlookup([repo.changelog.node(rev)])
1928 1930
1929 1931 displayer.show(rev, brinfo=br)
1930 1932 if opts['patch']:
1931 1933 prev = (parents and parents[0]) or nullid
1932 1934 dodiff(du, du, repo, prev, changenode, match=matchfn)
1933 1935 du.write("\n\n")
1934 1936 elif st == 'iter':
1935 1937 if count == limit: break
1936 1938 if du.header[rev]:
1937 1939 for args in du.header[rev]:
1938 1940 ui.write_header(*args)
1939 1941 if du.hunk[rev]:
1940 1942 count += 1
1941 1943 for args in du.hunk[rev]:
1942 1944 ui.write(*args)
1943 1945
1944 1946 def manifest(ui, repo, rev=None):
1945 1947 """output the latest or given revision of the project manifest
1946 1948
1947 1949 Print a list of version controlled files for the given revision.
1948 1950
1949 1951 The manifest is the list of files being version controlled. If no revision
1950 1952 is given then the tip is used.
1951 1953 """
1952 1954 if rev:
1953 1955 try:
1954 1956 # assume all revision numbers are for changesets
1955 1957 n = repo.lookup(rev)
1956 1958 change = repo.changelog.read(n)
1957 1959 n = change[0]
1958 1960 except hg.RepoError:
1959 1961 n = repo.manifest.lookup(rev)
1960 1962 else:
1961 1963 n = repo.manifest.tip()
1962 1964 m = repo.manifest.read(n)
1963 1965 mf = repo.manifest.readflags(n)
1964 1966 files = m.keys()
1965 1967 files.sort()
1966 1968
1967 1969 for f in files:
1968 1970 ui.write("%40s %3s %s\n" % (hex(m[f]), mf[f] and "755" or "644", f))
1969 1971
1970 1972 def merge(ui, repo, node=None, **opts):
1971 1973 """Merge working directory with another revision
1972 1974
1973 1975 Merge the contents of the current working directory and the
1974 1976 requested revision. Files that changed between either parent are
1975 1977 marked as changed for the next commit and a commit must be
1976 1978 performed before any further updates are allowed.
1977 1979 """
1978 1980 return doupdate(ui, repo, node=node, merge=True, **opts)
1979 1981
1980 1982 def outgoing(ui, repo, dest="default-push", **opts):
1981 1983 """show changesets not found in destination
1982 1984
1983 1985 Show changesets not found in the specified destination repository or
1984 1986 the default push location. These are the changesets that would be pushed
1985 1987 if a push was requested.
1986 1988
1987 1989 See pull for valid destination format details.
1988 1990 """
1989 1991 dest = ui.expandpath(dest)
1990 1992 if opts['ssh']:
1991 1993 ui.setconfig("ui", "ssh", opts['ssh'])
1992 1994 if opts['remotecmd']:
1993 1995 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
1994 1996
1995 1997 other = hg.repository(ui, dest)
1996 1998 o = repo.findoutgoing(other, force=opts['force'])
1997 1999 if not o:
1998 2000 ui.status(_("no changes found\n"))
1999 2001 return
2000 2002 o = repo.changelog.nodesbetween(o)[0]
2001 2003 if opts['newest_first']:
2002 2004 o.reverse()
2003 2005 displayer = show_changeset(ui, repo, opts)
2004 2006 for n in o:
2005 2007 parents = [p for p in repo.changelog.parents(n) if p != nullid]
2006 2008 if opts['no_merges'] and len(parents) == 2:
2007 2009 continue
2008 2010 displayer.show(changenode=n)
2009 2011 if opts['patch']:
2010 2012 prev = (parents and parents[0]) or nullid
2011 2013 dodiff(ui, ui, repo, prev, n)
2012 2014 ui.write("\n")
2013 2015
2014 2016 def parents(ui, repo, rev=None, branches=None, **opts):
2015 2017 """show the parents of the working dir or revision
2016 2018
2017 2019 Print the working directory's parent revisions.
2018 2020 """
2019 2021 if rev:
2020 2022 p = repo.changelog.parents(repo.lookup(rev))
2021 2023 else:
2022 2024 p = repo.dirstate.parents()
2023 2025
2024 2026 br = None
2025 2027 if branches is not None:
2026 2028 br = repo.branchlookup(p)
2027 2029 displayer = show_changeset(ui, repo, opts)
2028 2030 for n in p:
2029 2031 if n != nullid:
2030 2032 displayer.show(changenode=n, brinfo=br)
2031 2033
2032 2034 def paths(ui, repo, search=None):
2033 2035 """show definition of symbolic path names
2034 2036
2035 2037 Show definition of symbolic path name NAME. If no name is given, show
2036 2038 definition of available names.
2037 2039
2038 2040 Path names are defined in the [paths] section of /etc/mercurial/hgrc
2039 2041 and $HOME/.hgrc. If run inside a repository, .hg/hgrc is used, too.
2040 2042 """
2041 2043 if search:
2042 2044 for name, path in ui.configitems("paths"):
2043 2045 if name == search:
2044 2046 ui.write("%s\n" % path)
2045 2047 return
2046 2048 ui.warn(_("not found!\n"))
2047 2049 return 1
2048 2050 else:
2049 2051 for name, path in ui.configitems("paths"):
2050 2052 ui.write("%s = %s\n" % (name, path))
2051 2053
2052 2054 def postincoming(ui, repo, modheads, optupdate):
2053 2055 if modheads == 0:
2054 2056 return
2055 2057 if optupdate:
2056 2058 if modheads == 1:
2057 2059 return doupdate(ui, repo)
2058 2060 else:
2059 2061 ui.status(_("not updating, since new heads added\n"))
2060 2062 if modheads > 1:
2061 2063 ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n"))
2062 2064 else:
2063 2065 ui.status(_("(run 'hg update' to get a working copy)\n"))
2064 2066
2065 2067 def pull(ui, repo, source="default", **opts):
2066 2068 """pull changes from the specified source
2067 2069
2068 2070 Pull changes from a remote repository to a local one.
2069 2071
2070 2072 This finds all changes from the repository at the specified path
2071 2073 or URL and adds them to the local repository. By default, this
2072 2074 does not update the copy of the project in the working directory.
2073 2075
2074 2076 Valid URLs are of the form:
2075 2077
2076 2078 local/filesystem/path
2077 2079 http://[user@]host[:port][/path]
2078 2080 https://[user@]host[:port][/path]
2079 2081 ssh://[user@]host[:port][/path]
2080 2082
2081 2083 Some notes about using SSH with Mercurial:
2082 2084 - SSH requires an accessible shell account on the destination machine
2083 2085 and a copy of hg in the remote path or specified with as remotecmd.
2084 2086 - /path is relative to the remote user's home directory by default.
2085 2087 Use two slashes at the start of a path to specify an absolute path.
2086 2088 - Mercurial doesn't use its own compression via SSH; the right thing
2087 2089 to do is to configure it in your ~/.ssh/ssh_config, e.g.:
2088 2090 Host *.mylocalnetwork.example.com
2089 2091 Compression off
2090 2092 Host *
2091 2093 Compression on
2092 2094 Alternatively specify "ssh -C" as your ssh command in your hgrc or
2093 2095 with the --ssh command line option.
2094 2096 """
2095 2097 source = ui.expandpath(source)
2096 2098 ui.status(_('pulling from %s\n') % (source))
2097 2099
2098 2100 if opts['ssh']:
2099 2101 ui.setconfig("ui", "ssh", opts['ssh'])
2100 2102 if opts['remotecmd']:
2101 2103 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
2102 2104
2103 2105 other = hg.repository(ui, source)
2104 2106 revs = None
2105 2107 if opts['rev'] and not other.local():
2106 2108 raise util.Abort(_("pull -r doesn't work for remote repositories yet"))
2107 2109 elif opts['rev']:
2108 2110 revs = [other.lookup(rev) for rev in opts['rev']]
2109 2111 modheads = repo.pull(other, heads=revs, force=opts['force'])
2110 2112 return postincoming(ui, repo, modheads, opts['update'])
2111 2113
2112 2114 def push(ui, repo, dest="default-push", **opts):
2113 2115 """push changes to the specified destination
2114 2116
2115 2117 Push changes from the local repository to the given destination.
2116 2118
2117 2119 This is the symmetrical operation for pull. It helps to move
2118 2120 changes from the current repository to a different one. If the
2119 2121 destination is local this is identical to a pull in that directory
2120 2122 from the current one.
2121 2123
2122 2124 By default, push will refuse to run if it detects the result would
2123 2125 increase the number of remote heads. This generally indicates the
2124 2126 the client has forgotten to sync and merge before pushing.
2125 2127
2126 2128 Valid URLs are of the form:
2127 2129
2128 2130 local/filesystem/path
2129 2131 ssh://[user@]host[:port][/path]
2130 2132
2131 2133 Look at the help text for the pull command for important details
2132 2134 about ssh:// URLs.
2133 2135 """
2134 2136 dest = ui.expandpath(dest)
2135 2137 ui.status('pushing to %s\n' % (dest))
2136 2138
2137 2139 if opts['ssh']:
2138 2140 ui.setconfig("ui", "ssh", opts['ssh'])
2139 2141 if opts['remotecmd']:
2140 2142 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
2141 2143
2142 2144 other = hg.repository(ui, dest)
2143 2145 revs = None
2144 2146 if opts['rev']:
2145 2147 revs = [repo.lookup(rev) for rev in opts['rev']]
2146 2148 r = repo.push(other, opts['force'], revs=revs)
2147 2149 return r == 0
2148 2150
2149 2151 def rawcommit(ui, repo, *flist, **rc):
2150 2152 """raw commit interface (DEPRECATED)
2151 2153
2152 2154 (DEPRECATED)
2153 2155 Lowlevel commit, for use in helper scripts.
2154 2156
2155 2157 This command is not intended to be used by normal users, as it is
2156 2158 primarily useful for importing from other SCMs.
2157 2159
2158 2160 This command is now deprecated and will be removed in a future
2159 2161 release, please use debugsetparents and commit instead.
2160 2162 """
2161 2163
2162 2164 ui.warn(_("(the rawcommit command is deprecated)\n"))
2163 2165
2164 2166 message = rc['message']
2165 2167 if not message and rc['logfile']:
2166 2168 try:
2167 2169 message = open(rc['logfile']).read()
2168 2170 except IOError:
2169 2171 pass
2170 2172 if not message and not rc['logfile']:
2171 2173 raise util.Abort(_("missing commit message"))
2172 2174
2173 2175 files = relpath(repo, list(flist))
2174 2176 if rc['files']:
2175 2177 files += open(rc['files']).read().splitlines()
2176 2178
2177 2179 rc['parent'] = map(repo.lookup, rc['parent'])
2178 2180
2179 2181 try:
2180 2182 repo.rawcommit(files, message, rc['user'], rc['date'], *rc['parent'])
2181 2183 except ValueError, inst:
2182 2184 raise util.Abort(str(inst))
2183 2185
2184 2186 def recover(ui, repo):
2185 2187 """roll back an interrupted transaction
2186 2188
2187 2189 Recover from an interrupted commit or pull.
2188 2190
2189 2191 This command tries to fix the repository status after an interrupted
2190 2192 operation. It should only be necessary when Mercurial suggests it.
2191 2193 """
2192 2194 if repo.recover():
2193 2195 return repo.verify()
2194 2196 return 1
2195 2197
2196 2198 def remove(ui, repo, *pats, **opts):
2197 2199 """remove the specified files on the next commit
2198 2200
2199 2201 Schedule the indicated files for removal from the repository.
2200 2202
2201 2203 This command schedules the files to be removed at the next commit.
2202 2204 This only removes files from the current branch, not from the
2203 2205 entire project history. If the files still exist in the working
2204 2206 directory, they will be deleted from it. If invoked with --after,
2205 2207 files that have been manually deleted are marked as removed.
2206 2208
2207 2209 Modified files and added files are not removed by default. To
2208 2210 remove them, use the -f/--force option.
2209 2211 """
2210 2212 names = []
2211 2213 if not opts['after'] and not pats:
2212 2214 raise util.Abort(_('no files specified'))
2213 2215 files, matchfn, anypats = matchpats(repo, pats, opts)
2214 2216 exact = dict.fromkeys(files)
2215 2217 mardu = map(dict.fromkeys, repo.changes(files=files, match=matchfn))
2216 2218 modified, added, removed, deleted, unknown = mardu
2217 2219 remove, forget = [], []
2218 2220 for src, abs, rel, exact in walk(repo, pats, opts):
2219 2221 reason = None
2220 2222 if abs not in deleted and opts['after']:
2221 2223 reason = _('is still present')
2222 2224 elif abs in modified and not opts['force']:
2223 2225 reason = _('is modified (use -f to force removal)')
2224 2226 elif abs in added:
2225 2227 if opts['force']:
2226 2228 forget.append(abs)
2227 2229 continue
2228 2230 reason = _('has been marked for add (use -f to force removal)')
2229 2231 elif abs in unknown:
2230 2232 reason = _('is not managed')
2231 2233 elif abs in removed:
2232 2234 continue
2233 2235 if reason:
2234 2236 if exact:
2235 2237 ui.warn(_('not removing %s: file %s\n') % (rel, reason))
2236 2238 else:
2237 2239 if ui.verbose or not exact:
2238 2240 ui.status(_('removing %s\n') % rel)
2239 2241 remove.append(abs)
2240 2242 repo.forget(forget)
2241 2243 repo.remove(remove, unlink=not opts['after'])
2242 2244
2243 2245 def rename(ui, repo, *pats, **opts):
2244 2246 """rename files; equivalent of copy + remove
2245 2247
2246 2248 Mark dest as copies of sources; mark sources for deletion. If
2247 2249 dest is a directory, copies are put in that directory. If dest is
2248 2250 a file, there can only be one source.
2249 2251
2250 2252 By default, this command copies the contents of files as they
2251 2253 stand in the working directory. If invoked with --after, the
2252 2254 operation is recorded, but no copying is performed.
2253 2255
2254 2256 This command takes effect in the next commit.
2255 2257
2256 2258 NOTE: This command should be treated as experimental. While it
2257 2259 should properly record rename files, this information is not yet
2258 2260 fully used by merge, nor fully reported by log.
2259 2261 """
2260 2262 wlock = repo.wlock(0)
2261 2263 errs, copied = docopy(ui, repo, pats, opts, wlock)
2262 2264 names = []
2263 2265 for abs, rel, exact in copied:
2264 2266 if ui.verbose or not exact:
2265 2267 ui.status(_('removing %s\n') % rel)
2266 2268 names.append(abs)
2267 2269 repo.remove(names, True, wlock)
2268 2270 return errs
2269 2271
2270 2272 def revert(ui, repo, *pats, **opts):
2271 2273 """revert files or dirs to their states as of some revision
2272 2274
2273 2275 With no revision specified, revert the named files or directories
2274 2276 to the contents they had in the parent of the working directory.
2275 2277 This restores the contents of the affected files to an unmodified
2276 2278 state. If the working directory has two parents, you must
2277 2279 explicitly specify the revision to revert to.
2278 2280
2279 2281 Modified files are saved with a .orig suffix before reverting.
2280 2282 To disable these backups, use --no-backup.
2281 2283
2282 2284 Using the -r option, revert the given files or directories to
2283 2285 their contents as of a specific revision. This can be helpful to"roll
2284 2286 back" some or all of a change that should not have been committed.
2285 2287
2286 2288 Revert modifies the working directory. It does not commit any
2287 2289 changes, or change the parent of the working directory. If you
2288 2290 revert to a revision other than the parent of the working
2289 2291 directory, the reverted files will thus appear modified
2290 2292 afterwards.
2291 2293
2292 2294 If a file has been deleted, it is recreated. If the executable
2293 2295 mode of a file was changed, it is reset.
2294 2296
2295 2297 If names are given, all files matching the names are reverted.
2296 2298
2297 2299 If no arguments are given, all files in the repository are reverted.
2298 2300 """
2299 2301 parent, p2 = repo.dirstate.parents()
2300 2302 if opts['rev']:
2301 2303 node = repo.lookup(opts['rev'])
2302 2304 elif p2 != nullid:
2303 2305 raise util.Abort(_('working dir has two parents; '
2304 2306 'you must specify the revision to revert to'))
2305 2307 else:
2306 2308 node = parent
2307 2309 pmf = None
2308 2310 mf = repo.manifest.read(repo.changelog.read(node)[0])
2309 2311
2310 2312 wlock = repo.wlock()
2311 2313
2312 2314 # need all matching names in dirstate and manifest of target rev,
2313 2315 # so have to walk both. do not print errors if files exist in one
2314 2316 # but not other.
2315 2317
2316 2318 names = {}
2317 2319 target_only = {}
2318 2320
2319 2321 # walk dirstate.
2320 2322
2321 2323 for src, abs, rel, exact in walk(repo, pats, opts, badmatch=mf.has_key):
2322 2324 names[abs] = (rel, exact)
2323 2325 if src == 'b':
2324 2326 target_only[abs] = True
2325 2327
2326 2328 # walk target manifest.
2327 2329
2328 2330 for src, abs, rel, exact in walk(repo, pats, opts, node=node,
2329 2331 badmatch=names.has_key):
2330 2332 if abs in names: continue
2331 2333 names[abs] = (rel, exact)
2332 2334 target_only[abs] = True
2333 2335
2334 2336 changes = repo.changes(match=names.has_key, wlock=wlock)
2335 2337 modified, added, removed, deleted, unknown = map(dict.fromkeys, changes)
2336 2338
2337 2339 revert = ([], _('reverting %s\n'))
2338 2340 add = ([], _('adding %s\n'))
2339 2341 remove = ([], _('removing %s\n'))
2340 2342 forget = ([], _('forgetting %s\n'))
2341 2343 undelete = ([], _('undeleting %s\n'))
2342 2344 update = {}
2343 2345
2344 2346 disptable = (
2345 2347 # dispatch table:
2346 2348 # file state
2347 2349 # action if in target manifest
2348 2350 # action if not in target manifest
2349 2351 # make backup if in target manifest
2350 2352 # make backup if not in target manifest
2351 2353 (modified, revert, remove, True, True),
2352 2354 (added, revert, forget, True, False),
2353 2355 (removed, undelete, None, False, False),
2354 2356 (deleted, revert, remove, False, False),
2355 2357 (unknown, add, None, True, False),
2356 2358 (target_only, add, None, False, False),
2357 2359 )
2358 2360
2359 2361 entries = names.items()
2360 2362 entries.sort()
2361 2363
2362 2364 for abs, (rel, exact) in entries:
2363 2365 in_mf = abs in mf
2364 2366 def handle(xlist, dobackup):
2365 2367 xlist[0].append(abs)
2366 2368 if dobackup and not opts['no_backup'] and os.path.exists(rel):
2367 2369 bakname = "%s.orig" % rel
2368 2370 ui.note(_('saving current version of %s as %s\n') %
2369 2371 (rel, bakname))
2370 2372 shutil.copyfile(rel, bakname)
2371 2373 shutil.copymode(rel, bakname)
2372 2374 if ui.verbose or not exact:
2373 2375 ui.status(xlist[1] % rel)
2374 2376 for table, hitlist, misslist, backuphit, backupmiss in disptable:
2375 2377 if abs not in table: continue
2376 2378 # file has changed in dirstate
2377 2379 if in_mf:
2378 2380 handle(hitlist, backuphit)
2379 2381 elif misslist is not None:
2380 2382 handle(misslist, backupmiss)
2381 2383 else:
2382 2384 if exact: ui.warn(_('file not managed: %s\n' % rel))
2383 2385 break
2384 2386 else:
2385 2387 # file has not changed in dirstate
2386 2388 if node == parent:
2387 2389 if exact: ui.warn(_('no changes needed to %s\n' % rel))
2388 2390 continue
2389 2391 if not in_mf:
2390 2392 if pmf is None:
2391 2393 # only need parent manifest in this unlikely case,
2392 2394 # so do not read by default
2393 2395 pmf = repo.manifest.read(repo.changelog.read(parent)[0])
2394 2396 if abs in pmf:
2395 2397 handle(remove, False)
2396 2398 update[abs] = True
2397 2399
2398 2400 repo.dirstate.forget(forget[0])
2399 2401 r = repo.update(node, False, True, update.has_key, False, wlock=wlock,
2400 2402 show_stats=False)
2401 2403 repo.dirstate.update(add[0], 'a')
2402 2404 repo.dirstate.update(undelete[0], 'n')
2403 2405 repo.dirstate.update(remove[0], 'r')
2404 2406 return r
2405 2407
2406 2408 def rollback(ui, repo):
2407 2409 """roll back the last transaction in this repository
2408 2410
2409 2411 Roll back the last transaction in this repository, restoring the
2410 2412 project to its state prior to the transaction.
2411 2413
2412 2414 Transactions are used to encapsulate the effects of all commands
2413 2415 that create new changesets or propagate existing changesets into a
2414 2416 repository. For example, the following commands are transactional,
2415 2417 and their effects can be rolled back:
2416 2418
2417 2419 commit
2418 2420 import
2419 2421 pull
2420 2422 push (with this repository as destination)
2421 2423 unbundle
2422 2424
2423 2425 This command should be used with care. There is only one level of
2424 2426 rollback, and there is no way to undo a rollback.
2425 2427
2426 2428 This command is not intended for use on public repositories. Once
2427 2429 changes are visible for pull by other users, rolling a transaction
2428 2430 back locally is ineffective (someone else may already have pulled
2429 2431 the changes). Furthermore, a race is possible with readers of the
2430 2432 repository; for example an in-progress pull from the repository
2431 2433 may fail if a rollback is performed.
2432 2434 """
2433 2435 repo.undo()
2434 2436
2435 2437 def root(ui, repo):
2436 2438 """print the root (top) of the current working dir
2437 2439
2438 2440 Print the root directory of the current repository.
2439 2441 """
2440 2442 ui.write(repo.root + "\n")
2441 2443
2442 2444 def serve(ui, repo, **opts):
2443 2445 """export the repository via HTTP
2444 2446
2445 2447 Start a local HTTP repository browser and pull server.
2446 2448
2447 2449 By default, the server logs accesses to stdout and errors to
2448 2450 stderr. Use the "-A" and "-E" options to log to files.
2449 2451 """
2450 2452
2451 2453 if opts["stdio"]:
2452 2454 if repo is None:
2453 2455 raise hg.RepoError(_('no repo found'))
2454 2456 fin, fout = sys.stdin, sys.stdout
2455 2457 sys.stdout = sys.stderr
2456 2458
2457 2459 # Prevent insertion/deletion of CRs
2458 2460 util.set_binary(fin)
2459 2461 util.set_binary(fout)
2460 2462
2461 2463 def getarg():
2462 2464 argline = fin.readline()[:-1]
2463 2465 arg, l = argline.split()
2464 2466 val = fin.read(int(l))
2465 2467 return arg, val
2466 2468 def respond(v):
2467 2469 fout.write("%d\n" % len(v))
2468 2470 fout.write(v)
2469 2471 fout.flush()
2470 2472
2471 2473 lock = None
2472 2474
2473 2475 while 1:
2474 2476 cmd = fin.readline()[:-1]
2475 2477 if cmd == '':
2476 2478 return
2477 2479 if cmd == "heads":
2478 2480 h = repo.heads()
2479 2481 respond(" ".join(map(hex, h)) + "\n")
2480 2482 if cmd == "lock":
2481 2483 lock = repo.lock()
2482 2484 respond("")
2483 2485 if cmd == "unlock":
2484 2486 if lock:
2485 2487 lock.release()
2486 2488 lock = None
2487 2489 respond("")
2488 2490 elif cmd == "branches":
2489 2491 arg, nodes = getarg()
2490 2492 nodes = map(bin, nodes.split(" "))
2491 2493 r = []
2492 2494 for b in repo.branches(nodes):
2493 2495 r.append(" ".join(map(hex, b)) + "\n")
2494 2496 respond("".join(r))
2495 2497 elif cmd == "between":
2496 2498 arg, pairs = getarg()
2497 2499 pairs = [map(bin, p.split("-")) for p in pairs.split(" ")]
2498 2500 r = []
2499 2501 for b in repo.between(pairs):
2500 2502 r.append(" ".join(map(hex, b)) + "\n")
2501 2503 respond("".join(r))
2502 2504 elif cmd == "changegroup":
2503 2505 nodes = []
2504 2506 arg, roots = getarg()
2505 2507 nodes = map(bin, roots.split(" "))
2506 2508
2507 2509 cg = repo.changegroup(nodes, 'serve')
2508 2510 while 1:
2509 2511 d = cg.read(4096)
2510 2512 if not d:
2511 2513 break
2512 2514 fout.write(d)
2513 2515
2514 2516 fout.flush()
2515 2517
2516 2518 elif cmd == "addchangegroup":
2517 2519 if not lock:
2518 2520 respond("not locked")
2519 2521 continue
2520 2522 respond("")
2521 2523
2522 2524 r = repo.addchangegroup(fin, 'serve')
2523 2525 respond(str(r))
2524 2526
2525 2527 optlist = ("name templates style address port ipv6"
2526 2528 " accesslog errorlog webdir_conf")
2527 2529 for o in optlist.split():
2528 2530 if opts[o]:
2529 2531 ui.setconfig("web", o, opts[o])
2530 2532
2531 2533 if repo is None and not ui.config("web", "webdir_conf"):
2532 2534 raise hg.RepoError(_('no repo found'))
2533 2535
2534 2536 if opts['daemon'] and not opts['daemon_pipefds']:
2535 2537 rfd, wfd = os.pipe()
2536 2538 args = sys.argv[:]
2537 2539 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
2538 2540 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
2539 2541 args[0], args)
2540 2542 os.close(wfd)
2541 2543 os.read(rfd, 1)
2542 2544 os._exit(0)
2543 2545
2544 2546 try:
2545 httpd = hgweb.create_server(ui, repo, hgweb.hgwebdir, hgweb.hgweb)
2547 httpd = create_server(ui, repo, hgwebdir, hgweb)
2546 2548 except socket.error, inst:
2547 2549 raise util.Abort(_('cannot start server: ') + inst.args[1])
2548 2550
2549 2551 if ui.verbose:
2550 2552 addr, port = httpd.socket.getsockname()
2551 2553 if addr == '0.0.0.0':
2552 2554 addr = socket.gethostname()
2553 2555 else:
2554 2556 try:
2555 2557 addr = socket.gethostbyaddr(addr)[0]
2556 2558 except socket.error:
2557 2559 pass
2558 2560 if port != 80:
2559 2561 ui.status(_('listening at http://%s:%d/\n') % (addr, port))
2560 2562 else:
2561 2563 ui.status(_('listening at http://%s/\n') % addr)
2562 2564
2563 2565 if opts['pid_file']:
2564 2566 fp = open(opts['pid_file'], 'w')
2565 2567 fp.write(str(os.getpid()))
2566 2568 fp.close()
2567 2569
2568 2570 if opts['daemon_pipefds']:
2569 2571 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
2570 2572 os.close(rfd)
2571 2573 os.write(wfd, 'y')
2572 2574 os.close(wfd)
2573 2575 sys.stdout.flush()
2574 2576 sys.stderr.flush()
2575 2577 fd = os.open(util.nulldev, os.O_RDWR)
2576 2578 if fd != 0: os.dup2(fd, 0)
2577 2579 if fd != 1: os.dup2(fd, 1)
2578 2580 if fd != 2: os.dup2(fd, 2)
2579 2581 if fd not in (0, 1, 2): os.close(fd)
2580 2582
2581 2583 httpd.serve_forever()
2582 2584
2583 2585 def status(ui, repo, *pats, **opts):
2584 2586 """show changed files in the working directory
2585 2587
2586 2588 Show changed files in the repository. If names are
2587 2589 given, only files that match are shown.
2588 2590
2589 2591 The codes used to show the status of files are:
2590 2592 M = modified
2591 2593 A = added
2592 2594 R = removed
2593 2595 ! = deleted, but still tracked
2594 2596 ? = not tracked
2595 2597 I = ignored (not shown by default)
2596 2598 """
2597 2599
2598 2600 show_ignored = opts['ignored'] and True or False
2599 2601 files, matchfn, anypats = matchpats(repo, pats, opts)
2600 2602 cwd = (pats and repo.getcwd()) or ''
2601 2603 modified, added, removed, deleted, unknown, ignored = [
2602 2604 [util.pathto(cwd, x) for x in n]
2603 2605 for n in repo.changes(files=files, match=matchfn,
2604 2606 show_ignored=show_ignored)]
2605 2607
2606 2608 changetypes = [('modified', 'M', modified),
2607 2609 ('added', 'A', added),
2608 2610 ('removed', 'R', removed),
2609 2611 ('deleted', '!', deleted),
2610 2612 ('unknown', '?', unknown),
2611 2613 ('ignored', 'I', ignored)]
2612 2614
2613 2615 end = opts['print0'] and '\0' or '\n'
2614 2616
2615 2617 for opt, char, changes in ([ct for ct in changetypes if opts[ct[0]]]
2616 2618 or changetypes):
2617 2619 if opts['no_status']:
2618 2620 format = "%%s%s" % end
2619 2621 else:
2620 2622 format = "%s %%s%s" % (char, end)
2621 2623
2622 2624 for f in changes:
2623 2625 ui.write(format % f)
2624 2626
2625 2627 def tag(ui, repo, name, rev_=None, **opts):
2626 2628 """add a tag for the current tip or a given revision
2627 2629
2628 2630 Name a particular revision using <name>.
2629 2631
2630 2632 Tags are used to name particular revisions of the repository and are
2631 2633 very useful to compare different revision, to go back to significant
2632 2634 earlier versions or to mark branch points as releases, etc.
2633 2635
2634 2636 If no revision is given, the tip is used.
2635 2637
2636 2638 To facilitate version control, distribution, and merging of tags,
2637 2639 they are stored as a file named ".hgtags" which is managed
2638 2640 similarly to other project files and can be hand-edited if
2639 2641 necessary. The file '.hg/localtags' is used for local tags (not
2640 2642 shared among repositories).
2641 2643 """
2642 2644 if name == "tip":
2643 2645 raise util.Abort(_("the name 'tip' is reserved"))
2644 2646 if rev_ is not None:
2645 2647 ui.warn(_("use of 'hg tag NAME [REV]' is deprecated, "
2646 2648 "please use 'hg tag [-r REV] NAME' instead\n"))
2647 2649 if opts['rev']:
2648 2650 raise util.Abort(_("use only one form to specify the revision"))
2649 2651 if opts['rev']:
2650 2652 rev_ = opts['rev']
2651 2653 if rev_:
2652 2654 r = hex(repo.lookup(rev_))
2653 2655 else:
2654 2656 r = hex(repo.changelog.tip())
2655 2657
2656 2658 disallowed = (revrangesep, '\r', '\n')
2657 2659 for c in disallowed:
2658 2660 if name.find(c) >= 0:
2659 2661 raise util.Abort(_("%s cannot be used in a tag name") % repr(c))
2660 2662
2661 2663 repo.hook('pretag', throw=True, node=r, tag=name,
2662 2664 local=int(not not opts['local']))
2663 2665
2664 2666 if opts['local']:
2665 2667 repo.opener("localtags", "a").write("%s %s\n" % (r, name))
2666 2668 repo.hook('tag', node=r, tag=name, local=1)
2667 2669 return
2668 2670
2669 2671 for x in repo.changes():
2670 2672 if ".hgtags" in x:
2671 2673 raise util.Abort(_("working copy of .hgtags is changed "
2672 2674 "(please commit .hgtags manually)"))
2673 2675
2674 2676 repo.wfile(".hgtags", "ab").write("%s %s\n" % (r, name))
2675 2677 if repo.dirstate.state(".hgtags") == '?':
2676 2678 repo.add([".hgtags"])
2677 2679
2678 2680 message = (opts['message'] or
2679 2681 _("Added tag %s for changeset %s") % (name, r))
2680 2682 try:
2681 2683 repo.commit([".hgtags"], message, opts['user'], opts['date'])
2682 2684 repo.hook('tag', node=r, tag=name, local=0)
2683 2685 except ValueError, inst:
2684 2686 raise util.Abort(str(inst))
2685 2687
2686 2688 def tags(ui, repo):
2687 2689 """list repository tags
2688 2690
2689 2691 List the repository tags.
2690 2692
2691 2693 This lists both regular and local tags.
2692 2694 """
2693 2695
2694 2696 l = repo.tagslist()
2695 2697 l.reverse()
2696 2698 for t, n in l:
2697 2699 try:
2698 2700 r = "%5d:%s" % (repo.changelog.rev(n), hex(n))
2699 2701 except KeyError:
2700 2702 r = " ?:?"
2701 2703 if ui.quiet:
2702 2704 ui.write("%s\n" % t)
2703 2705 else:
2704 2706 ui.write("%-30s %s\n" % (t, r))
2705 2707
2706 2708 def tip(ui, repo, **opts):
2707 2709 """show the tip revision
2708 2710
2709 2711 Show the tip revision.
2710 2712 """
2711 2713 n = repo.changelog.tip()
2712 2714 br = None
2713 2715 if opts['branches']:
2714 2716 br = repo.branchlookup([n])
2715 2717 show_changeset(ui, repo, opts).show(changenode=n, brinfo=br)
2716 2718 if opts['patch']:
2717 2719 dodiff(ui, ui, repo, repo.changelog.parents(n)[0], n)
2718 2720
2719 2721 def unbundle(ui, repo, fname, **opts):
2720 2722 """apply a changegroup file
2721 2723
2722 2724 Apply a compressed changegroup file generated by the bundle
2723 2725 command.
2724 2726 """
2725 2727 f = urllib.urlopen(fname)
2726 2728
2727 2729 header = f.read(6)
2728 2730 if not header.startswith("HG"):
2729 2731 raise util.Abort(_("%s: not a Mercurial bundle file") % fname)
2730 2732 elif not header.startswith("HG10"):
2731 2733 raise util.Abort(_("%s: unknown bundle version") % fname)
2732 2734 elif header == "HG10BZ":
2733 2735 def generator(f):
2734 2736 zd = bz2.BZ2Decompressor()
2735 2737 zd.decompress("BZ")
2736 2738 for chunk in f:
2737 2739 yield zd.decompress(chunk)
2738 2740 elif header == "HG10UN":
2739 2741 def generator(f):
2740 2742 for chunk in f:
2741 2743 yield chunk
2742 2744 else:
2743 2745 raise util.Abort(_("%s: unknown bundle compression type")
2744 2746 % fname)
2745 2747 gen = generator(util.filechunkiter(f, 4096))
2746 2748 modheads = repo.addchangegroup(util.chunkbuffer(gen), 'unbundle')
2747 2749 return postincoming(ui, repo, modheads, opts['update'])
2748 2750
2749 2751 def undo(ui, repo):
2750 2752 """undo the last commit or pull (DEPRECATED)
2751 2753
2752 2754 (DEPRECATED)
2753 2755 This command is now deprecated and will be removed in a future
2754 2756 release. Please use the rollback command instead. For usage
2755 2757 instructions, see the rollback command.
2756 2758 """
2757 2759 ui.warn(_('(the undo command is deprecated; use rollback instead)\n'))
2758 2760 repo.undo()
2759 2761
2760 2762 def update(ui, repo, node=None, merge=False, clean=False, force=None,
2761 2763 branch=None, **opts):
2762 2764 """update or merge working directory
2763 2765
2764 2766 Update the working directory to the specified revision.
2765 2767
2766 2768 If there are no outstanding changes in the working directory and
2767 2769 there is a linear relationship between the current version and the
2768 2770 requested version, the result is the requested version.
2769 2771
2770 2772 To merge the working directory with another revision, use the
2771 2773 merge command.
2772 2774
2773 2775 By default, update will refuse to run if doing so would require
2774 2776 merging or discarding local changes.
2775 2777 """
2776 2778 if merge:
2777 2779 ui.warn(_('(the -m/--merge option is deprecated; '
2778 2780 'use the merge command instead)\n'))
2779 2781 return doupdate(ui, repo, node, merge, clean, force, branch, **opts)
2780 2782
2781 2783 def doupdate(ui, repo, node=None, merge=False, clean=False, force=None,
2782 2784 branch=None, **opts):
2783 2785 if branch:
2784 2786 br = repo.branchlookup(branch=branch)
2785 2787 found = []
2786 2788 for x in br:
2787 2789 if branch in br[x]:
2788 2790 found.append(x)
2789 2791 if len(found) > 1:
2790 2792 ui.warn(_("Found multiple heads for %s\n") % branch)
2791 2793 for x in found:
2792 2794 show_changeset(ui, repo, opts).show(changenode=x, brinfo=br)
2793 2795 return 1
2794 2796 if len(found) == 1:
2795 2797 node = found[0]
2796 2798 ui.warn(_("Using head %s for branch %s\n") % (short(node), branch))
2797 2799 else:
2798 2800 ui.warn(_("branch %s not found\n") % (branch))
2799 2801 return 1
2800 2802 else:
2801 2803 node = node and repo.lookup(node) or repo.changelog.tip()
2802 2804 return repo.update(node, allow=merge, force=clean, forcemerge=force)
2803 2805
2804 2806 def verify(ui, repo):
2805 2807 """verify the integrity of the repository
2806 2808
2807 2809 Verify the integrity of the current repository.
2808 2810
2809 2811 This will perform an extensive check of the repository's
2810 2812 integrity, validating the hashes and checksums of each entry in
2811 2813 the changelog, manifest, and tracked files, as well as the
2812 2814 integrity of their crosslinks and indices.
2813 2815 """
2814 2816 return repo.verify()
2815 2817
2816 2818 # Command options and aliases are listed here, alphabetically
2817 2819
2818 2820 table = {
2819 2821 "^add":
2820 2822 (add,
2821 2823 [('I', 'include', [], _('include names matching the given patterns')),
2822 2824 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2823 2825 _('hg add [OPTION]... [FILE]...')),
2824 2826 "debugaddremove|addremove":
2825 2827 (addremove,
2826 2828 [('I', 'include', [], _('include names matching the given patterns')),
2827 2829 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2828 2830 _('hg addremove [OPTION]... [FILE]...')),
2829 2831 "^annotate":
2830 2832 (annotate,
2831 2833 [('r', 'rev', '', _('annotate the specified revision')),
2832 2834 ('a', 'text', None, _('treat all files as text')),
2833 2835 ('u', 'user', None, _('list the author')),
2834 2836 ('d', 'date', None, _('list the date')),
2835 2837 ('n', 'number', None, _('list the revision number (default)')),
2836 2838 ('c', 'changeset', None, _('list the changeset')),
2837 2839 ('I', 'include', [], _('include names matching the given patterns')),
2838 2840 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2839 2841 _('hg annotate [-r REV] [-a] [-u] [-d] [-n] [-c] FILE...')),
2840 2842 "archive":
2841 2843 (archive,
2842 2844 [('', 'no-decode', None, _('do not pass files through decoders')),
2843 2845 ('p', 'prefix', '', _('directory prefix for files in archive')),
2844 2846 ('r', 'rev', '', _('revision to distribute')),
2845 2847 ('t', 'type', '', _('type of distribution to create')),
2846 2848 ('I', 'include', [], _('include names matching the given patterns')),
2847 2849 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2848 2850 _('hg archive [OPTION]... DEST')),
2849 2851 "backout":
2850 2852 (backout,
2851 2853 [('', 'merge', None,
2852 2854 _('merge with old dirstate parent after backout')),
2853 2855 ('m', 'message', '', _('use <text> as commit message')),
2854 2856 ('l', 'logfile', '', _('read commit message from <file>')),
2855 2857 ('d', 'date', '', _('record datecode as commit date')),
2856 2858 ('u', 'user', '', _('record user as committer')),
2857 2859 ('I', 'include', [], _('include names matching the given patterns')),
2858 2860 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2859 2861 _('hg backout [OPTION]... REV')),
2860 2862 "bundle":
2861 2863 (bundle,
2862 2864 [('f', 'force', None,
2863 2865 _('run even when remote repository is unrelated'))],
2864 2866 _('hg bundle FILE DEST')),
2865 2867 "cat":
2866 2868 (cat,
2867 2869 [('o', 'output', '', _('print output to file with formatted name')),
2868 2870 ('r', 'rev', '', _('print the given revision')),
2869 2871 ('I', 'include', [], _('include names matching the given patterns')),
2870 2872 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2871 2873 _('hg cat [OPTION]... FILE...')),
2872 2874 "^clone":
2873 2875 (clone,
2874 2876 [('U', 'noupdate', None, _('do not update the new working directory')),
2875 2877 ('r', 'rev', [],
2876 2878 _('a changeset you would like to have after cloning')),
2877 2879 ('', 'pull', None, _('use pull protocol to copy metadata')),
2878 2880 ('e', 'ssh', '', _('specify ssh command to use')),
2879 2881 ('', 'remotecmd', '',
2880 2882 _('specify hg command to run on the remote side'))],
2881 2883 _('hg clone [OPTION]... SOURCE [DEST]')),
2882 2884 "^commit|ci":
2883 2885 (commit,
2884 2886 [('A', 'addremove', None,
2885 2887 _('mark new/missing files as added/removed before committing')),
2886 2888 ('m', 'message', '', _('use <text> as commit message')),
2887 2889 ('l', 'logfile', '', _('read the commit message from <file>')),
2888 2890 ('d', 'date', '', _('record datecode as commit date')),
2889 2891 ('u', 'user', '', _('record user as commiter')),
2890 2892 ('I', 'include', [], _('include names matching the given patterns')),
2891 2893 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2892 2894 _('hg commit [OPTION]... [FILE]...')),
2893 2895 "copy|cp":
2894 2896 (copy,
2895 2897 [('A', 'after', None, _('record a copy that has already occurred')),
2896 2898 ('f', 'force', None,
2897 2899 _('forcibly copy over an existing managed file')),
2898 2900 ('I', 'include', [], _('include names matching the given patterns')),
2899 2901 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2900 2902 _('hg copy [OPTION]... [SOURCE]... DEST')),
2901 2903 "debugancestor": (debugancestor, [], _('debugancestor INDEX REV1 REV2')),
2902 2904 "debugcomplete":
2903 2905 (debugcomplete,
2904 2906 [('o', 'options', None, _('show the command options'))],
2905 2907 _('debugcomplete [-o] CMD')),
2906 2908 "debugrebuildstate":
2907 2909 (debugrebuildstate,
2908 2910 [('r', 'rev', '', _('revision to rebuild to'))],
2909 2911 _('debugrebuildstate [-r REV] [REV]')),
2910 2912 "debugcheckstate": (debugcheckstate, [], _('debugcheckstate')),
2911 2913 "debugconfig": (debugconfig, [], _('debugconfig')),
2912 2914 "debugsetparents": (debugsetparents, [], _('debugsetparents REV1 [REV2]')),
2913 2915 "debugstate": (debugstate, [], _('debugstate')),
2914 2916 "debugdata": (debugdata, [], _('debugdata FILE REV')),
2915 2917 "debugindex": (debugindex, [], _('debugindex FILE')),
2916 2918 "debugindexdot": (debugindexdot, [], _('debugindexdot FILE')),
2917 2919 "debugrename": (debugrename, [], _('debugrename FILE [REV]')),
2918 2920 "debugwalk":
2919 2921 (debugwalk,
2920 2922 [('I', 'include', [], _('include names matching the given patterns')),
2921 2923 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2922 2924 _('debugwalk [OPTION]... [FILE]...')),
2923 2925 "^diff":
2924 2926 (diff,
2925 2927 [('r', 'rev', [], _('revision')),
2926 2928 ('a', 'text', None, _('treat all files as text')),
2927 2929 ('p', 'show-function', None,
2928 2930 _('show which function each change is in')),
2929 2931 ('w', 'ignore-all-space', None,
2930 2932 _('ignore white space when comparing lines')),
2931 2933 ('I', 'include', [], _('include names matching the given patterns')),
2932 2934 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2933 2935 _('hg diff [-a] [-I] [-X] [-r REV1 [-r REV2]] [FILE]...')),
2934 2936 "^export":
2935 2937 (export,
2936 2938 [('o', 'output', '', _('print output to file with formatted name')),
2937 2939 ('a', 'text', None, _('treat all files as text')),
2938 2940 ('', 'switch-parent', None, _('diff against the second parent'))],
2939 2941 _('hg export [-a] [-o OUTFILESPEC] REV...')),
2940 2942 "debugforget|forget":
2941 2943 (forget,
2942 2944 [('I', 'include', [], _('include names matching the given patterns')),
2943 2945 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2944 2946 _('hg forget [OPTION]... FILE...')),
2945 2947 "grep":
2946 2948 (grep,
2947 2949 [('0', 'print0', None, _('end fields with NUL')),
2948 2950 ('', 'all', None, _('print all revisions that match')),
2949 2951 ('i', 'ignore-case', None, _('ignore case when matching')),
2950 2952 ('l', 'files-with-matches', None,
2951 2953 _('print only filenames and revs that match')),
2952 2954 ('n', 'line-number', None, _('print matching line numbers')),
2953 2955 ('r', 'rev', [], _('search in given revision range')),
2954 2956 ('u', 'user', None, _('print user who committed change')),
2955 2957 ('I', 'include', [], _('include names matching the given patterns')),
2956 2958 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2957 2959 _('hg grep [OPTION]... PATTERN [FILE]...')),
2958 2960 "heads":
2959 2961 (heads,
2960 2962 [('b', 'branches', None, _('show branches')),
2961 2963 ('', 'style', '', _('display using template map file')),
2962 2964 ('r', 'rev', '', _('show only heads which are descendants of rev')),
2963 2965 ('', 'template', '', _('display with template'))],
2964 2966 _('hg heads [-b] [-r <rev>]')),
2965 2967 "help": (help_, [], _('hg help [COMMAND]')),
2966 2968 "identify|id": (identify, [], _('hg identify')),
2967 2969 "import|patch":
2968 2970 (import_,
2969 2971 [('p', 'strip', 1,
2970 2972 _('directory strip option for patch. This has the same\n'
2971 2973 'meaning as the corresponding patch option')),
2972 2974 ('b', 'base', '', _('base path')),
2973 2975 ('f', 'force', None,
2974 2976 _('skip check for outstanding uncommitted changes'))],
2975 2977 _('hg import [-p NUM] [-b BASE] [-f] PATCH...')),
2976 2978 "incoming|in": (incoming,
2977 2979 [('M', 'no-merges', None, _('do not show merges')),
2978 2980 ('f', 'force', None,
2979 2981 _('run even when remote repository is unrelated')),
2980 2982 ('', 'style', '', _('display using template map file')),
2981 2983 ('n', 'newest-first', None, _('show newest record first')),
2982 2984 ('', 'bundle', '', _('file to store the bundles into')),
2983 2985 ('p', 'patch', None, _('show patch')),
2984 2986 ('', 'template', '', _('display with template')),
2985 2987 ('e', 'ssh', '', _('specify ssh command to use')),
2986 2988 ('', 'remotecmd', '',
2987 2989 _('specify hg command to run on the remote side'))],
2988 2990 _('hg incoming [-p] [-n] [-M] [--bundle FILENAME] [SOURCE]')),
2989 2991 "^init": (init, [], _('hg init [DEST]')),
2990 2992 "locate":
2991 2993 (locate,
2992 2994 [('r', 'rev', '', _('search the repository as it stood at rev')),
2993 2995 ('0', 'print0', None,
2994 2996 _('end filenames with NUL, for use with xargs')),
2995 2997 ('f', 'fullpath', None,
2996 2998 _('print complete paths from the filesystem root')),
2997 2999 ('I', 'include', [], _('include names matching the given patterns')),
2998 3000 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2999 3001 _('hg locate [OPTION]... [PATTERN]...')),
3000 3002 "^log|history":
3001 3003 (log,
3002 3004 [('b', 'branches', None, _('show branches')),
3003 3005 ('k', 'keyword', [], _('search for a keyword')),
3004 3006 ('l', 'limit', '', _('limit number of changes displayed')),
3005 3007 ('r', 'rev', [], _('show the specified revision or range')),
3006 3008 ('M', 'no-merges', None, _('do not show merges')),
3007 3009 ('', 'style', '', _('display using template map file')),
3008 3010 ('m', 'only-merges', None, _('show only merges')),
3009 3011 ('p', 'patch', None, _('show patch')),
3010 3012 ('', 'template', '', _('display with template')),
3011 3013 ('I', 'include', [], _('include names matching the given patterns')),
3012 3014 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
3013 3015 _('hg log [OPTION]... [FILE]')),
3014 3016 "manifest": (manifest, [], _('hg manifest [REV]')),
3015 3017 "merge":
3016 3018 (merge,
3017 3019 [('b', 'branch', '', _('merge with head of a specific branch')),
3018 3020 ('f', 'force', None, _('force a merge with outstanding changes'))],
3019 3021 _('hg merge [-b TAG] [-f] [REV]')),
3020 3022 "outgoing|out": (outgoing,
3021 3023 [('M', 'no-merges', None, _('do not show merges')),
3022 3024 ('f', 'force', None,
3023 3025 _('run even when remote repository is unrelated')),
3024 3026 ('p', 'patch', None, _('show patch')),
3025 3027 ('', 'style', '', _('display using template map file')),
3026 3028 ('n', 'newest-first', None, _('show newest record first')),
3027 3029 ('', 'template', '', _('display with template')),
3028 3030 ('e', 'ssh', '', _('specify ssh command to use')),
3029 3031 ('', 'remotecmd', '',
3030 3032 _('specify hg command to run on the remote side'))],
3031 3033 _('hg outgoing [-M] [-p] [-n] [DEST]')),
3032 3034 "^parents":
3033 3035 (parents,
3034 3036 [('b', 'branches', None, _('show branches')),
3035 3037 ('', 'style', '', _('display using template map file')),
3036 3038 ('', 'template', '', _('display with template'))],
3037 3039 _('hg parents [-b] [REV]')),
3038 3040 "paths": (paths, [], _('hg paths [NAME]')),
3039 3041 "^pull":
3040 3042 (pull,
3041 3043 [('u', 'update', None,
3042 3044 _('update the working directory to tip after pull')),
3043 3045 ('e', 'ssh', '', _('specify ssh command to use')),
3044 3046 ('f', 'force', None,
3045 3047 _('run even when remote repository is unrelated')),
3046 3048 ('r', 'rev', [], _('a specific revision you would like to pull')),
3047 3049 ('', 'remotecmd', '',
3048 3050 _('specify hg command to run on the remote side'))],
3049 3051 _('hg pull [-u] [-e FILE] [-r REV]... [--remotecmd FILE] [SOURCE]')),
3050 3052 "^push":
3051 3053 (push,
3052 3054 [('f', 'force', None, _('force push')),
3053 3055 ('e', 'ssh', '', _('specify ssh command to use')),
3054 3056 ('r', 'rev', [], _('a specific revision you would like to push')),
3055 3057 ('', 'remotecmd', '',
3056 3058 _('specify hg command to run on the remote side'))],
3057 3059 _('hg push [-f] [-e FILE] [-r REV]... [--remotecmd FILE] [DEST]')),
3058 3060 "debugrawcommit|rawcommit":
3059 3061 (rawcommit,
3060 3062 [('p', 'parent', [], _('parent')),
3061 3063 ('d', 'date', '', _('date code')),
3062 3064 ('u', 'user', '', _('user')),
3063 3065 ('F', 'files', '', _('file list')),
3064 3066 ('m', 'message', '', _('commit message')),
3065 3067 ('l', 'logfile', '', _('commit message file'))],
3066 3068 _('hg debugrawcommit [OPTION]... [FILE]...')),
3067 3069 "recover": (recover, [], _('hg recover')),
3068 3070 "^remove|rm":
3069 3071 (remove,
3070 3072 [('A', 'after', None, _('record remove that has already occurred')),
3071 3073 ('f', 'force', None, _('remove file even if modified')),
3072 3074 ('I', 'include', [], _('include names matching the given patterns')),
3073 3075 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
3074 3076 _('hg remove [OPTION]... FILE...')),
3075 3077 "rename|mv":
3076 3078 (rename,
3077 3079 [('A', 'after', None, _('record a rename that has already occurred')),
3078 3080 ('f', 'force', None,
3079 3081 _('forcibly copy over an existing managed file')),
3080 3082 ('I', 'include', [], _('include names matching the given patterns')),
3081 3083 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
3082 3084 _('hg rename [OPTION]... SOURCE... DEST')),
3083 3085 "^revert":
3084 3086 (revert,
3085 3087 [('r', 'rev', '', _('revision to revert to')),
3086 3088 ('', 'no-backup', None, _('do not save backup copies of files')),
3087 3089 ('I', 'include', [], _('include names matching given patterns')),
3088 3090 ('X', 'exclude', [], _('exclude names matching given patterns'))],
3089 3091 _('hg revert [-r REV] [NAME]...')),
3090 3092 "rollback": (rollback, [], _('hg rollback')),
3091 3093 "root": (root, [], _('hg root')),
3092 3094 "^serve":
3093 3095 (serve,
3094 3096 [('A', 'accesslog', '', _('name of access log file to write to')),
3095 3097 ('d', 'daemon', None, _('run server in background')),
3096 3098 ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
3097 3099 ('E', 'errorlog', '', _('name of error log file to write to')),
3098 3100 ('p', 'port', 0, _('port to use (default: 8000)')),
3099 3101 ('a', 'address', '', _('address to use')),
3100 3102 ('n', 'name', '',
3101 3103 _('name to show in web pages (default: working dir)')),
3102 3104 ('', 'webdir-conf', '', _('name of the webdir config file'
3103 3105 ' (serve more than one repo)')),
3104 3106 ('', 'pid-file', '', _('name of file to write process ID to')),
3105 3107 ('', 'stdio', None, _('for remote clients')),
3106 3108 ('t', 'templates', '', _('web templates to use')),
3107 3109 ('', 'style', '', _('template style to use')),
3108 3110 ('6', 'ipv6', None, _('use IPv6 in addition to IPv4'))],
3109 3111 _('hg serve [OPTION]...')),
3110 3112 "^status|st":
3111 3113 (status,
3112 3114 [('m', 'modified', None, _('show only modified files')),
3113 3115 ('a', 'added', None, _('show only added files')),
3114 3116 ('r', 'removed', None, _('show only removed files')),
3115 3117 ('d', 'deleted', None, _('show only deleted (but tracked) files')),
3116 3118 ('u', 'unknown', None, _('show only unknown (not tracked) files')),
3117 3119 ('i', 'ignored', None, _('show ignored files')),
3118 3120 ('n', 'no-status', None, _('hide status prefix')),
3119 3121 ('0', 'print0', None,
3120 3122 _('end filenames with NUL, for use with xargs')),
3121 3123 ('I', 'include', [], _('include names matching the given patterns')),
3122 3124 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
3123 3125 _('hg status [OPTION]... [FILE]...')),
3124 3126 "tag":
3125 3127 (tag,
3126 3128 [('l', 'local', None, _('make the tag local')),
3127 3129 ('m', 'message', '', _('message for tag commit log entry')),
3128 3130 ('d', 'date', '', _('record datecode as commit date')),
3129 3131 ('u', 'user', '', _('record user as commiter')),
3130 3132 ('r', 'rev', '', _('revision to tag'))],
3131 3133 _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME')),
3132 3134 "tags": (tags, [], _('hg tags')),
3133 3135 "tip":
3134 3136 (tip,
3135 3137 [('b', 'branches', None, _('show branches')),
3136 3138 ('', 'style', '', _('display using template map file')),
3137 3139 ('p', 'patch', None, _('show patch')),
3138 3140 ('', 'template', '', _('display with template'))],
3139 3141 _('hg tip [-b] [-p]')),
3140 3142 "unbundle":
3141 3143 (unbundle,
3142 3144 [('u', 'update', None,
3143 3145 _('update the working directory to tip after unbundle'))],
3144 3146 _('hg unbundle [-u] FILE')),
3145 3147 "debugundo|undo": (undo, [], _('hg undo')),
3146 3148 "^update|up|checkout|co":
3147 3149 (update,
3148 3150 [('b', 'branch', '', _('checkout the head of a specific branch')),
3149 3151 ('m', 'merge', None, _('allow merging of branches (DEPRECATED)')),
3150 3152 ('C', 'clean', None, _('overwrite locally modified files')),
3151 3153 ('f', 'force', None, _('force a merge with outstanding changes'))],
3152 3154 _('hg update [-b TAG] [-m] [-C] [-f] [REV]')),
3153 3155 "verify": (verify, [], _('hg verify')),
3154 3156 "version": (show_version, [], _('hg version')),
3155 3157 }
3156 3158
3157 3159 globalopts = [
3158 3160 ('R', 'repository', '',
3159 3161 _('repository root directory or symbolic path name')),
3160 3162 ('', 'cwd', '', _('change working directory')),
3161 3163 ('y', 'noninteractive', None,
3162 3164 _('do not prompt, assume \'yes\' for any required answers')),
3163 3165 ('q', 'quiet', None, _('suppress output')),
3164 3166 ('v', 'verbose', None, _('enable additional output')),
3165 3167 ('', 'config', [], _('set/override config option')),
3166 3168 ('', 'debug', None, _('enable debugging output')),
3167 3169 ('', 'debugger', None, _('start debugger')),
3168 3170 ('', 'traceback', None, _('print traceback on exception')),
3169 3171 ('', 'time', None, _('time how long the command takes')),
3170 3172 ('', 'profile', None, _('print command execution profile')),
3171 3173 ('', 'version', None, _('output version information and exit')),
3172 3174 ('h', 'help', None, _('display help and exit')),
3173 3175 ]
3174 3176
3175 3177 norepo = ("clone init version help debugancestor debugcomplete debugdata"
3176 3178 " debugindex debugindexdot")
3177 3179 optionalrepo = ("paths serve debugconfig")
3178 3180
3179 3181 def findpossible(cmd):
3180 3182 """
3181 3183 Return cmd -> (aliases, command table entry)
3182 3184 for each matching command.
3183 3185 Return debug commands (or their aliases) only if no normal command matches.
3184 3186 """
3185 3187 choice = {}
3186 3188 debugchoice = {}
3187 3189 for e in table.keys():
3188 3190 aliases = e.lstrip("^").split("|")
3189 3191 found = None
3190 3192 if cmd in aliases:
3191 3193 found = cmd
3192 3194 else:
3193 3195 for a in aliases:
3194 3196 if a.startswith(cmd):
3195 3197 found = a
3196 3198 break
3197 3199 if found is not None:
3198 3200 if aliases[0].startswith("debug"):
3199 3201 debugchoice[found] = (aliases, table[e])
3200 3202 else:
3201 3203 choice[found] = (aliases, table[e])
3202 3204
3203 3205 if not choice and debugchoice:
3204 3206 choice = debugchoice
3205 3207
3206 3208 return choice
3207 3209
3208 3210 def find(cmd):
3209 3211 """Return (aliases, command table entry) for command string."""
3210 3212 choice = findpossible(cmd)
3211 3213
3212 3214 if choice.has_key(cmd):
3213 3215 return choice[cmd]
3214 3216
3215 3217 if len(choice) > 1:
3216 3218 clist = choice.keys()
3217 3219 clist.sort()
3218 3220 raise AmbiguousCommand(cmd, clist)
3219 3221
3220 3222 if choice:
3221 3223 return choice.values()[0]
3222 3224
3223 3225 raise UnknownCommand(cmd)
3224 3226
3225 3227 def catchterm(*args):
3226 3228 raise util.SignalInterrupt
3227 3229
3228 3230 def run():
3229 3231 sys.exit(dispatch(sys.argv[1:]))
3230 3232
3231 3233 class ParseError(Exception):
3232 3234 """Exception raised on errors in parsing the command line."""
3233 3235
3234 3236 def parse(ui, args):
3235 3237 options = {}
3236 3238 cmdoptions = {}
3237 3239
3238 3240 try:
3239 3241 args = fancyopts.fancyopts(args, globalopts, options)
3240 3242 except fancyopts.getopt.GetoptError, inst:
3241 3243 raise ParseError(None, inst)
3242 3244
3243 3245 if args:
3244 3246 cmd, args = args[0], args[1:]
3245 3247 aliases, i = find(cmd)
3246 3248 cmd = aliases[0]
3247 3249 defaults = ui.config("defaults", cmd)
3248 3250 if defaults:
3249 3251 args = defaults.split() + args
3250 3252 c = list(i[1])
3251 3253 else:
3252 3254 cmd = None
3253 3255 c = []
3254 3256
3255 3257 # combine global options into local
3256 3258 for o in globalopts:
3257 3259 c.append((o[0], o[1], options[o[1]], o[3]))
3258 3260
3259 3261 try:
3260 3262 args = fancyopts.fancyopts(args, c, cmdoptions)
3261 3263 except fancyopts.getopt.GetoptError, inst:
3262 3264 raise ParseError(cmd, inst)
3263 3265
3264 3266 # separate global options back out
3265 3267 for o in globalopts:
3266 3268 n = o[1]
3267 3269 options[n] = cmdoptions[n]
3268 3270 del cmdoptions[n]
3269 3271
3270 3272 return (cmd, cmd and i[0] or None, args, options, cmdoptions)
3271 3273
3272 3274 def dispatch(args):
3273 3275 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
3274 3276 num = getattr(signal, name, None)
3275 3277 if num: signal.signal(num, catchterm)
3276 3278
3277 3279 try:
3278 3280 u = ui.ui(traceback='--traceback' in sys.argv[1:])
3279 3281 except util.Abort, inst:
3280 3282 sys.stderr.write(_("abort: %s\n") % inst)
3281 3283 return -1
3282 3284
3283 3285 external = []
3284 3286 for x in u.extensions():
3285 3287 try:
3286 3288 if x[1]:
3287 3289 mod = imp.load_source(x[0], x[1])
3288 3290 else:
3289 3291 def importh(name):
3290 3292 mod = __import__(name)
3291 3293 components = name.split('.')
3292 3294 for comp in components[1:]:
3293 3295 mod = getattr(mod, comp)
3294 3296 return mod
3295 3297 try:
3296 3298 mod = importh("hgext." + x[0])
3297 3299 except ImportError:
3298 3300 mod = importh(x[0])
3299 3301 external.append(mod)
3300 3302 except Exception, inst:
3301 3303 u.warn(_("*** failed to import extension %s: %s\n") % (x[0], inst))
3302 3304 if u.print_exc():
3303 3305 return 1
3304 3306
3305 3307 for x in external:
3306 3308 uisetup = getattr(x, 'uisetup', None)
3307 3309 if uisetup:
3308 3310 uisetup(u)
3309 3311 cmdtable = getattr(x, 'cmdtable', {})
3310 3312 for t in cmdtable:
3311 3313 if t in table:
3312 3314 u.warn(_("module %s overrides %s\n") % (x.__name__, t))
3313 3315 table.update(cmdtable)
3314 3316
3315 3317 try:
3316 3318 cmd, func, args, options, cmdoptions = parse(u, args)
3317 3319 if options["time"]:
3318 3320 def get_times():
3319 3321 t = os.times()
3320 3322 if t[4] == 0.0: # Windows leaves this as zero, so use time.clock()
3321 3323 t = (t[0], t[1], t[2], t[3], time.clock())
3322 3324 return t
3323 3325 s = get_times()
3324 3326 def print_time():
3325 3327 t = get_times()
3326 3328 u.warn(_("Time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
3327 3329 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
3328 3330 atexit.register(print_time)
3329 3331
3330 3332 u.updateopts(options["verbose"], options["debug"], options["quiet"],
3331 3333 not options["noninteractive"], options["traceback"],
3332 3334 options["config"])
3333 3335
3334 3336 # enter the debugger before command execution
3335 3337 if options['debugger']:
3336 3338 pdb.set_trace()
3337 3339
3338 3340 try:
3339 3341 if options['cwd']:
3340 3342 try:
3341 3343 os.chdir(options['cwd'])
3342 3344 except OSError, inst:
3343 3345 raise util.Abort('%s: %s' %
3344 3346 (options['cwd'], inst.strerror))
3345 3347
3346 3348 path = u.expandpath(options["repository"]) or ""
3347 3349 repo = path and hg.repository(u, path=path) or None
3348 3350
3349 3351 if options['help']:
3350 3352 return help_(u, cmd, options['version'])
3351 3353 elif options['version']:
3352 3354 return show_version(u)
3353 3355 elif not cmd:
3354 3356 return help_(u, 'shortlist')
3355 3357
3356 3358 if cmd not in norepo.split():
3357 3359 try:
3358 3360 if not repo:
3359 3361 repo = hg.repository(u, path=path)
3360 3362 u = repo.ui
3361 3363 for x in external:
3362 3364 if hasattr(x, 'reposetup'):
3363 3365 x.reposetup(u, repo)
3364 3366 except hg.RepoError:
3365 3367 if cmd not in optionalrepo.split():
3366 3368 raise
3367 3369 d = lambda: func(u, repo, *args, **cmdoptions)
3368 3370 else:
3369 3371 d = lambda: func(u, *args, **cmdoptions)
3370 3372
3371 3373 try:
3372 3374 if options['profile']:
3373 3375 import hotshot, hotshot.stats
3374 3376 prof = hotshot.Profile("hg.prof")
3375 3377 try:
3376 3378 try:
3377 3379 return prof.runcall(d)
3378 3380 except:
3379 3381 try:
3380 3382 u.warn(_('exception raised - generating '
3381 3383 'profile anyway\n'))
3382 3384 except:
3383 3385 pass
3384 3386 raise
3385 3387 finally:
3386 3388 prof.close()
3387 3389 stats = hotshot.stats.load("hg.prof")
3388 3390 stats.strip_dirs()
3389 3391 stats.sort_stats('time', 'calls')
3390 3392 stats.print_stats(40)
3391 3393 else:
3392 3394 return d()
3393 3395 finally:
3394 3396 u.flush()
3395 3397 except:
3396 3398 # enter the debugger when we hit an exception
3397 3399 if options['debugger']:
3398 3400 pdb.post_mortem(sys.exc_info()[2])
3399 3401 u.print_exc()
3400 3402 raise
3401 3403 except ParseError, inst:
3402 3404 if inst.args[0]:
3403 3405 u.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
3404 3406 help_(u, inst.args[0])
3405 3407 else:
3406 3408 u.warn(_("hg: %s\n") % inst.args[1])
3407 3409 help_(u, 'shortlist')
3408 3410 except AmbiguousCommand, inst:
3409 3411 u.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
3410 3412 (inst.args[0], " ".join(inst.args[1])))
3411 3413 except UnknownCommand, inst:
3412 3414 u.warn(_("hg: unknown command '%s'\n") % inst.args[0])
3413 3415 help_(u, 'shortlist')
3414 3416 except hg.RepoError, inst:
3415 3417 u.warn(_("abort: %s!\n") % inst)
3416 3418 except lock.LockHeld, inst:
3417 3419 if inst.errno == errno.ETIMEDOUT:
3418 3420 reason = _('timed out waiting for lock held by %s') % inst.locker
3419 3421 else:
3420 3422 reason = _('lock held by %s') % inst.locker
3421 3423 u.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
3422 3424 except lock.LockUnavailable, inst:
3423 3425 u.warn(_("abort: could not lock %s: %s\n") %
3424 3426 (inst.desc or inst.filename, inst.strerror))
3425 3427 except revlog.RevlogError, inst:
3426 3428 u.warn(_("abort: "), inst, "!\n")
3427 3429 except util.SignalInterrupt:
3428 3430 u.warn(_("killed!\n"))
3429 3431 except KeyboardInterrupt:
3430 3432 try:
3431 3433 u.warn(_("interrupted!\n"))
3432 3434 except IOError, inst:
3433 3435 if inst.errno == errno.EPIPE:
3434 3436 if u.debugflag:
3435 3437 u.warn(_("\nbroken pipe\n"))
3436 3438 else:
3437 3439 raise
3438 3440 except IOError, inst:
3439 3441 if hasattr(inst, "code"):
3440 3442 u.warn(_("abort: %s\n") % inst)
3441 3443 elif hasattr(inst, "reason"):
3442 3444 u.warn(_("abort: error: %s\n") % inst.reason[1])
3443 3445 elif hasattr(inst, "args") and inst[0] == errno.EPIPE:
3444 3446 if u.debugflag:
3445 3447 u.warn(_("broken pipe\n"))
3446 3448 elif getattr(inst, "strerror", None):
3447 3449 if getattr(inst, "filename", None):
3448 3450 u.warn(_("abort: %s - %s\n") % (inst.strerror, inst.filename))
3449 3451 else:
3450 3452 u.warn(_("abort: %s\n") % inst.strerror)
3451 3453 else:
3452 3454 raise
3453 3455 except OSError, inst:
3454 3456 if hasattr(inst, "filename"):
3455 3457 u.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
3456 3458 else:
3457 3459 u.warn(_("abort: %s\n") % inst.strerror)
3458 3460 except util.Abort, inst:
3459 3461 u.warn(_('abort: '), inst.args[0] % inst.args[1:], '\n')
3460 3462 except TypeError, inst:
3461 3463 # was this an argument error?
3462 3464 tb = traceback.extract_tb(sys.exc_info()[2])
3463 3465 if len(tb) > 2: # no
3464 3466 raise
3465 3467 u.debug(inst, "\n")
3466 3468 u.warn(_("%s: invalid arguments\n") % cmd)
3467 3469 help_(u, cmd)
3468 3470 except SystemExit, inst:
3469 3471 # Commands shouldn't sys.exit directly, but give a return code.
3470 3472 # Just in case catch this and and pass exit code to caller.
3471 3473 return inst.code
3472 3474 except:
3473 3475 u.warn(_("** unknown exception encountered, details follow\n"))
3474 3476 u.warn(_("** report bug details to mercurial@selenic.com\n"))
3475 3477 u.warn(_("** Mercurial Distributed SCM (version %s)\n")
3476 3478 % version.get_version())
3477 3479 raise
3478 3480
3479 3481 return -1
This diff has been collapsed as it changes many lines, (981 lines changed) Show them Hide them
@@ -1,988 +1,11 b''
1 1 # hgweb.py - web interface to a mercurial repository
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 import os, cgi, sys
10 import mimetypes
11 9 from mercurial.demandload import demandload
12 demandload(globals(), "time re socket zlib errno ConfigParser tempfile")
13 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
14 demandload(globals(), "mercurial.hgweb.request:hgrequest")
15 demandload(globals(), "mercurial.hgweb.server:create_server")
16 from mercurial.node import *
17 from mercurial.i18n import gettext as _
18
19 def up(p):
20 if p[0] != "/":
21 p = "/" + p
22 if p[-1] == "/":
23 p = p[:-1]
24 up = os.path.dirname(p)
25 if up == "/":
26 return "/"
27 return up + "/"
28
29 def get_mtime(repo_path):
30 hg_path = os.path.join(repo_path, ".hg")
31 cl_path = os.path.join(hg_path, "00changelog.i")
32 if os.path.exists(os.path.join(cl_path)):
33 return os.stat(cl_path).st_mtime
34 else:
35 return os.stat(hg_path).st_mtime
36
37 def staticfile(directory, fname):
38 """return a file inside directory with guessed content-type header
39
40 fname always uses '/' as directory separator and isn't allowed to
41 contain unusual path components.
42 Content-type is guessed using the mimetypes module.
43 Return an empty string if fname is illegal or file not found.
44
45 """
46 parts = fname.split('/')
47 path = directory
48 for part in parts:
49 if (part in ('', os.curdir, os.pardir) or
50 os.sep in part or os.altsep is not None and os.altsep in part):
51 return ""
52 path = os.path.join(path, part)
53 try:
54 os.stat(path)
55 ct = mimetypes.guess_type(path)[0] or "text/plain"
56 return "Content-type: %s\n\n%s" % (ct, file(path).read())
57 except (TypeError, OSError):
58 # illegal fname or unreadable file
59 return ""
60
61 class hgweb(object):
62 def __init__(self, repo, name=None):
63 if type(repo) == type(""):
64 self.repo = hg.repository(ui.ui(), repo)
65 else:
66 self.repo = repo
67
68 self.mtime = -1
69 self.reponame = name
70 self.archives = 'zip', 'gz', 'bz2'
71
72 def refresh(self):
73 mtime = get_mtime(self.repo.root)
74 if mtime != self.mtime:
75 self.mtime = mtime
76 self.repo = hg.repository(self.repo.ui, self.repo.root)
77 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
78 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
79 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
80
81 def archivelist(self, nodeid):
82 for i in self.archives:
83 if self.repo.ui.configbool("web", "allow" + i, False):
84 yield {"type" : i, "node" : nodeid, "url": ""}
85
86 def listfiles(self, files, mf):
87 for f in files[:self.maxfiles]:
88 yield self.t("filenodelink", node=hex(mf[f]), file=f)
89 if len(files) > self.maxfiles:
90 yield self.t("fileellipses")
91
92 def listfilediffs(self, files, changeset):
93 for f in files[:self.maxfiles]:
94 yield self.t("filedifflink", node=hex(changeset), file=f)
95 if len(files) > self.maxfiles:
96 yield self.t("fileellipses")
97
98 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
99 if not rev:
100 rev = lambda x: ""
101 siblings = [s for s in siblings if s != nullid]
102 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
103 return
104 for s in siblings:
105 yield dict(node=hex(s), rev=rev(s), **args)
106
107 def renamelink(self, fl, node):
108 r = fl.renamed(node)
109 if r:
110 return [dict(file=r[0], node=hex(r[1]))]
111 return []
112
113 def showtag(self, t1, node=nullid, **args):
114 for t in self.repo.nodetags(node):
115 yield self.t(t1, tag=t, **args)
116
117 def diff(self, node1, node2, files):
118 def filterfiles(filters, files):
119 l = [x for x in files if x in filters]
120
121 for t in filters:
122 if t and t[-1] != os.sep:
123 t += os.sep
124 l += [x for x in files if x.startswith(t)]
125 return l
126
127 parity = [0]
128 def diffblock(diff, f, fn):
129 yield self.t("diffblock",
130 lines=prettyprintlines(diff),
131 parity=parity[0],
132 file=f,
133 filenode=hex(fn or nullid))
134 parity[0] = 1 - parity[0]
135
136 def prettyprintlines(diff):
137 for l in diff.splitlines(1):
138 if l.startswith('+'):
139 yield self.t("difflineplus", line=l)
140 elif l.startswith('-'):
141 yield self.t("difflineminus", line=l)
142 elif l.startswith('@'):
143 yield self.t("difflineat", line=l)
144 else:
145 yield self.t("diffline", line=l)
146
147 r = self.repo
148 cl = r.changelog
149 mf = r.manifest
150 change1 = cl.read(node1)
151 change2 = cl.read(node2)
152 mmap1 = mf.read(change1[0])
153 mmap2 = mf.read(change2[0])
154 date1 = util.datestr(change1[2])
155 date2 = util.datestr(change2[2])
156
157 modified, added, removed, deleted, unknown = r.changes(node1, node2)
158 if files:
159 modified, added, removed = map(lambda x: filterfiles(files, x),
160 (modified, added, removed))
161
162 diffopts = self.repo.ui.diffopts()
163 showfunc = diffopts['showfunc']
164 ignorews = diffopts['ignorews']
165 for f in modified:
166 to = r.file(f).read(mmap1[f])
167 tn = r.file(f).read(mmap2[f])
168 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
169 showfunc=showfunc, ignorews=ignorews), f, tn)
170 for f in added:
171 to = None
172 tn = r.file(f).read(mmap2[f])
173 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
174 showfunc=showfunc, ignorews=ignorews), f, tn)
175 for f in removed:
176 to = r.file(f).read(mmap1[f])
177 tn = None
178 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
179 showfunc=showfunc, ignorews=ignorews), f, tn)
180
181 def changelog(self, pos):
182 def changenav(**map):
183 def seq(factor, maxchanges=None):
184 if maxchanges:
185 yield maxchanges
186 if maxchanges >= 20 and maxchanges <= 40:
187 yield 50
188 else:
189 yield 1 * factor
190 yield 3 * factor
191 for f in seq(factor * 10):
192 yield f
193
194 l = []
195 last = 0
196 for f in seq(1, self.maxchanges):
197 if f < self.maxchanges or f <= last:
198 continue
199 if f > count:
200 break
201 last = f
202 r = "%d" % f
203 if pos + f < count:
204 l.append(("+" + r, pos + f))
205 if pos - f >= 0:
206 l.insert(0, ("-" + r, pos - f))
207
208 yield {"rev": 0, "label": "(0)"}
209
210 for label, rev in l:
211 yield {"label": label, "rev": rev}
212
213 yield {"label": "tip", "rev": "tip"}
214
215 def changelist(**map):
216 parity = (start - end) & 1
217 cl = self.repo.changelog
218 l = [] # build a list in forward order for efficiency
219 for i in range(start, end):
220 n = cl.node(i)
221 changes = cl.read(n)
222 hn = hex(n)
223
224 l.insert(0, {"parity": parity,
225 "author": changes[1],
226 "parent": self.siblings(cl.parents(n), cl.rev,
227 cl.rev(n) - 1),
228 "child": self.siblings(cl.children(n), cl.rev,
229 cl.rev(n) + 1),
230 "changelogtag": self.showtag("changelogtag",n),
231 "manifest": hex(changes[0]),
232 "desc": changes[4],
233 "date": changes[2],
234 "files": self.listfilediffs(changes[3], n),
235 "rev": i,
236 "node": hn})
237 parity = 1 - parity
238
239 for e in l:
240 yield e
241
242 cl = self.repo.changelog
243 mf = cl.read(cl.tip())[0]
244 count = cl.count()
245 start = max(0, pos - self.maxchanges + 1)
246 end = min(count, start + self.maxchanges)
247 pos = end - 1
248
249 yield self.t('changelog',
250 changenav=changenav,
251 manifest=hex(mf),
252 rev=pos, changesets=count, entries=changelist,
253 archives=self.archivelist("tip"))
254
255 def search(self, query):
256
257 def changelist(**map):
258 cl = self.repo.changelog
259 count = 0
260 qw = query.lower().split()
261
262 def revgen():
263 for i in range(cl.count() - 1, 0, -100):
264 l = []
265 for j in range(max(0, i - 100), i):
266 n = cl.node(j)
267 changes = cl.read(n)
268 l.append((n, j, changes))
269 l.reverse()
270 for e in l:
271 yield e
272
273 for n, i, changes in revgen():
274 miss = 0
275 for q in qw:
276 if not (q in changes[1].lower() or
277 q in changes[4].lower() or
278 q in " ".join(changes[3][:20]).lower()):
279 miss = 1
280 break
281 if miss:
282 continue
283
284 count += 1
285 hn = hex(n)
286
287 yield self.t('searchentry',
288 parity=count & 1,
289 author=changes[1],
290 parent=self.siblings(cl.parents(n), cl.rev),
291 child=self.siblings(cl.children(n), cl.rev),
292 changelogtag=self.showtag("changelogtag",n),
293 manifest=hex(changes[0]),
294 desc=changes[4],
295 date=changes[2],
296 files=self.listfilediffs(changes[3], n),
297 rev=i,
298 node=hn)
299
300 if count >= self.maxchanges:
301 break
302
303 cl = self.repo.changelog
304 mf = cl.read(cl.tip())[0]
305
306 yield self.t('search',
307 query=query,
308 manifest=hex(mf),
309 entries=changelist)
310
311 def changeset(self, nodeid):
312 cl = self.repo.changelog
313 n = self.repo.lookup(nodeid)
314 nodeid = hex(n)
315 changes = cl.read(n)
316 p1 = cl.parents(n)[0]
317
318 files = []
319 mf = self.repo.manifest.read(changes[0])
320 for f in changes[3]:
321 files.append(self.t("filenodelink",
322 filenode=hex(mf.get(f, nullid)), file=f))
323
324 def diff(**map):
325 yield self.diff(p1, n, None)
326
327 yield self.t('changeset',
328 diff=diff,
329 rev=cl.rev(n),
330 node=nodeid,
331 parent=self.siblings(cl.parents(n), cl.rev),
332 child=self.siblings(cl.children(n), cl.rev),
333 changesettag=self.showtag("changesettag",n),
334 manifest=hex(changes[0]),
335 author=changes[1],
336 desc=changes[4],
337 date=changes[2],
338 files=files,
339 archives=self.archivelist(nodeid))
340
341 def filelog(self, f, filenode):
342 cl = self.repo.changelog
343 fl = self.repo.file(f)
344 filenode = hex(fl.lookup(filenode))
345 count = fl.count()
346
347 def entries(**map):
348 l = []
349 parity = (count - 1) & 1
350
351 for i in range(count):
352 n = fl.node(i)
353 lr = fl.linkrev(n)
354 cn = cl.node(lr)
355 cs = cl.read(cl.node(lr))
356
357 l.insert(0, {"parity": parity,
358 "filenode": hex(n),
359 "filerev": i,
360 "file": f,
361 "node": hex(cn),
362 "author": cs[1],
363 "date": cs[2],
364 "rename": self.renamelink(fl, n),
365 "parent": self.siblings(fl.parents(n),
366 fl.rev, file=f),
367 "child": self.siblings(fl.children(n),
368 fl.rev, file=f),
369 "desc": cs[4]})
370 parity = 1 - parity
371
372 for e in l:
373 yield e
374
375 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
376
377 def filerevision(self, f, node):
378 fl = self.repo.file(f)
379 n = fl.lookup(node)
380 node = hex(n)
381 text = fl.read(n)
382 changerev = fl.linkrev(n)
383 cl = self.repo.changelog
384 cn = cl.node(changerev)
385 cs = cl.read(cn)
386 mfn = cs[0]
387
388 mt = mimetypes.guess_type(f)[0]
389 rawtext = text
390 if util.binary(text):
391 mt = mt or 'application/octet-stream'
392 text = "(binary:%s)" % mt
393 mt = mt or 'text/plain'
394
395 def lines():
396 for l, t in enumerate(text.splitlines(1)):
397 yield {"line": t,
398 "linenumber": "% 6d" % (l + 1),
399 "parity": l & 1}
400
401 yield self.t("filerevision",
402 file=f,
403 filenode=node,
404 path=up(f),
405 text=lines(),
406 raw=rawtext,
407 mimetype=mt,
408 rev=changerev,
409 node=hex(cn),
410 manifest=hex(mfn),
411 author=cs[1],
412 date=cs[2],
413 parent=self.siblings(fl.parents(n), fl.rev, file=f),
414 child=self.siblings(fl.children(n), fl.rev, file=f),
415 rename=self.renamelink(fl, n),
416 permissions=self.repo.manifest.readflags(mfn)[f])
417
418 def fileannotate(self, f, node):
419 bcache = {}
420 ncache = {}
421 fl = self.repo.file(f)
422 n = fl.lookup(node)
423 node = hex(n)
424 changerev = fl.linkrev(n)
425
426 cl = self.repo.changelog
427 cn = cl.node(changerev)
428 cs = cl.read(cn)
429 mfn = cs[0]
430
431 def annotate(**map):
432 parity = 1
433 last = None
434 for r, l in fl.annotate(n):
435 try:
436 cnode = ncache[r]
437 except KeyError:
438 cnode = ncache[r] = self.repo.changelog.node(r)
439
440 try:
441 name = bcache[r]
442 except KeyError:
443 cl = self.repo.changelog.read(cnode)
444 bcache[r] = name = self.repo.ui.shortuser(cl[1])
445
446 if last != cnode:
447 parity = 1 - parity
448 last = cnode
449
450 yield {"parity": parity,
451 "node": hex(cnode),
452 "rev": r,
453 "author": name,
454 "file": f,
455 "line": l}
456
457 yield self.t("fileannotate",
458 file=f,
459 filenode=node,
460 annotate=annotate,
461 path=up(f),
462 rev=changerev,
463 node=hex(cn),
464 manifest=hex(mfn),
465 author=cs[1],
466 date=cs[2],
467 rename=self.renamelink(fl, n),
468 parent=self.siblings(fl.parents(n), fl.rev, file=f),
469 child=self.siblings(fl.children(n), fl.rev, file=f),
470 permissions=self.repo.manifest.readflags(mfn)[f])
471
472 def manifest(self, mnode, path):
473 man = self.repo.manifest
474 mn = man.lookup(mnode)
475 mnode = hex(mn)
476 mf = man.read(mn)
477 rev = man.rev(mn)
478 changerev = man.linkrev(mn)
479 node = self.repo.changelog.node(changerev)
480 mff = man.readflags(mn)
481
482 files = {}
483
484 p = path[1:]
485 if p and p[-1] != "/":
486 p += "/"
487 l = len(p)
488
489 for f,n in mf.items():
490 if f[:l] != p:
491 continue
492 remain = f[l:]
493 if "/" in remain:
494 short = remain[:remain.find("/") + 1] # bleah
495 files[short] = (f, None)
496 else:
497 short = os.path.basename(remain)
498 files[short] = (f, n)
499
500 def filelist(**map):
501 parity = 0
502 fl = files.keys()
503 fl.sort()
504 for f in fl:
505 full, fnode = files[f]
506 if not fnode:
507 continue
508
509 yield {"file": full,
510 "manifest": mnode,
511 "filenode": hex(fnode),
512 "parity": parity,
513 "basename": f,
514 "permissions": mff[full]}
515 parity = 1 - parity
516
517 def dirlist(**map):
518 parity = 0
519 fl = files.keys()
520 fl.sort()
521 for f in fl:
522 full, fnode = files[f]
523 if fnode:
524 continue
525
526 yield {"parity": parity,
527 "path": os.path.join(path, f),
528 "manifest": mnode,
529 "basename": f[:-1]}
530 parity = 1 - parity
531
532 yield self.t("manifest",
533 manifest=mnode,
534 rev=rev,
535 node=hex(node),
536 path=path,
537 up=up(path),
538 fentries=filelist,
539 dentries=dirlist,
540 archives=self.archivelist(hex(node)))
541
542 def tags(self):
543 cl = self.repo.changelog
544 mf = cl.read(cl.tip())[0]
545
546 i = self.repo.tagslist()
547 i.reverse()
548
549 def entries(notip=False, **map):
550 parity = 0
551 for k,n in i:
552 if notip and k == "tip": continue
553 yield {"parity": parity,
554 "tag": k,
555 "tagmanifest": hex(cl.read(n)[0]),
556 "date": cl.read(n)[2],
557 "node": hex(n)}
558 parity = 1 - parity
559
560 yield self.t("tags",
561 manifest=hex(mf),
562 entries=lambda **x: entries(False, **x),
563 entriesnotip=lambda **x: entries(True, **x))
564
565 def summary(self):
566 cl = self.repo.changelog
567 mf = cl.read(cl.tip())[0]
568
569 i = self.repo.tagslist()
570 i.reverse()
571
572 def tagentries(**map):
573 parity = 0
574 count = 0
575 for k,n in i:
576 if k == "tip": # skip tip
577 continue;
578
579 count += 1
580 if count > 10: # limit to 10 tags
581 break;
582
583 c = cl.read(n)
584 m = c[0]
585 t = c[2]
586
587 yield self.t("tagentry",
588 parity = parity,
589 tag = k,
590 node = hex(n),
591 date = t,
592 tagmanifest = hex(m))
593 parity = 1 - parity
594
595 def changelist(**map):
596 parity = 0
597 cl = self.repo.changelog
598 l = [] # build a list in forward order for efficiency
599 for i in range(start, end):
600 n = cl.node(i)
601 changes = cl.read(n)
602 hn = hex(n)
603 t = changes[2]
604
605 l.insert(0, self.t(
606 'shortlogentry',
607 parity = parity,
608 author = changes[1],
609 manifest = hex(changes[0]),
610 desc = changes[4],
611 date = t,
612 rev = i,
613 node = hn))
614 parity = 1 - parity
615
616 yield l
617
618 cl = self.repo.changelog
619 mf = cl.read(cl.tip())[0]
620 count = cl.count()
621 start = max(0, count - self.maxchanges)
622 end = min(count, start + self.maxchanges)
623 pos = end - 1
624
625 yield self.t("summary",
626 desc = self.repo.ui.config("web", "description", "unknown"),
627 owner = (self.repo.ui.config("ui", "username") or # preferred
628 self.repo.ui.config("web", "contact") or # deprecated
629 self.repo.ui.config("web", "author", "unknown")), # also
630 lastchange = (0, 0), # FIXME
631 manifest = hex(mf),
632 tags = tagentries,
633 shortlog = changelist)
634
635 def filediff(self, file, changeset):
636 cl = self.repo.changelog
637 n = self.repo.lookup(changeset)
638 changeset = hex(n)
639 p1 = cl.parents(n)[0]
640 cs = cl.read(n)
641 mf = self.repo.manifest.read(cs[0])
642
643 def diff(**map):
644 yield self.diff(p1, n, [file])
645
646 yield self.t("filediff",
647 file=file,
648 filenode=hex(mf.get(file, nullid)),
649 node=changeset,
650 rev=self.repo.changelog.rev(n),
651 parent=self.siblings(cl.parents(n), cl.rev),
652 child=self.siblings(cl.children(n), cl.rev),
653 diff=diff)
654
655 archive_specs = {
656 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
657 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
658 'zip': ('application/zip', 'zip', '.zip', None),
659 }
660
661 def archive(self, req, cnode, type):
662 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
663 name = "%s-%s" % (reponame, short(cnode))
664 mimetype, artype, extension, encoding = self.archive_specs[type]
665 headers = [('Content-type', mimetype),
666 ('Content-disposition', 'attachment; filename=%s%s' %
667 (name, extension))]
668 if encoding:
669 headers.append(('Content-encoding', encoding))
670 req.header(headers)
671 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
672
673 # add tags to things
674 # tags -> list of changesets corresponding to tags
675 # find tag, changeset, file
676
677 def run(self, req=hgrequest()):
678 def clean(path):
679 p = util.normpath(path)
680 if p[:2] == "..":
681 raise "suspicious path"
682 return p
683
684 def header(**map):
685 yield self.t("header", **map)
686
687 def footer(**map):
688 yield self.t("footer",
689 motd=self.repo.ui.config("web", "motd", ""),
690 **map)
691
692 def expand_form(form):
693 shortcuts = {
694 'cl': [('cmd', ['changelog']), ('rev', None)],
695 'cs': [('cmd', ['changeset']), ('node', None)],
696 'f': [('cmd', ['file']), ('filenode', None)],
697 'fl': [('cmd', ['filelog']), ('filenode', None)],
698 'fd': [('cmd', ['filediff']), ('node', None)],
699 'fa': [('cmd', ['annotate']), ('filenode', None)],
700 'mf': [('cmd', ['manifest']), ('manifest', None)],
701 'ca': [('cmd', ['archive']), ('node', None)],
702 'tags': [('cmd', ['tags'])],
703 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
704 'static': [('cmd', ['static']), ('file', None)]
705 }
706
707 for k in shortcuts.iterkeys():
708 if form.has_key(k):
709 for name, value in shortcuts[k]:
710 if value is None:
711 value = form[k]
712 form[name] = value
713 del form[k]
714
715 self.refresh()
716
717 expand_form(req.form)
718
719 t = self.repo.ui.config("web", "templates", templater.templatepath())
720 static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
721 m = os.path.join(t, "map")
722 style = self.repo.ui.config("web", "style", "")
723 if req.form.has_key('style'):
724 style = req.form['style'][0]
725 if style:
726 b = os.path.basename("map-" + style)
727 p = os.path.join(t, b)
728 if os.path.isfile(p):
729 m = p
730
731 port = req.env["SERVER_PORT"]
732 port = port != "80" and (":" + port) or ""
733 uri = req.env["REQUEST_URI"]
734 if "?" in uri:
735 uri = uri.split("?")[0]
736 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
737 if not self.reponame:
738 self.reponame = (self.repo.ui.config("web", "name")
739 or uri.strip('/') or self.repo.root)
740
741 self.t = templater.templater(m, templater.common_filters,
742 defaults={"url": url,
743 "repo": self.reponame,
744 "header": header,
745 "footer": footer,
746 })
747
748 if not req.form.has_key('cmd'):
749 req.form['cmd'] = [self.t.cache['default'],]
750
751 cmd = req.form['cmd'][0]
752 if cmd == 'changelog':
753 hi = self.repo.changelog.count() - 1
754 if req.form.has_key('rev'):
755 hi = req.form['rev'][0]
756 try:
757 hi = self.repo.changelog.rev(self.repo.lookup(hi))
758 except hg.RepoError:
759 req.write(self.search(hi)) # XXX redirect to 404 page?
760 return
761
762 req.write(self.changelog(hi))
763
764 elif cmd == 'changeset':
765 req.write(self.changeset(req.form['node'][0]))
766
767 elif cmd == 'manifest':
768 req.write(self.manifest(req.form['manifest'][0],
769 clean(req.form['path'][0])))
770
771 elif cmd == 'tags':
772 req.write(self.tags())
773
774 elif cmd == 'summary':
775 req.write(self.summary())
776
777 elif cmd == 'filediff':
778 req.write(self.filediff(clean(req.form['file'][0]),
779 req.form['node'][0]))
780
781 elif cmd == 'file':
782 req.write(self.filerevision(clean(req.form['file'][0]),
783 req.form['filenode'][0]))
784
785 elif cmd == 'annotate':
786 req.write(self.fileannotate(clean(req.form['file'][0]),
787 req.form['filenode'][0]))
788
789 elif cmd == 'filelog':
790 req.write(self.filelog(clean(req.form['file'][0]),
791 req.form['filenode'][0]))
792
793 elif cmd == 'heads':
794 req.httphdr("application/mercurial-0.1")
795 h = self.repo.heads()
796 req.write(" ".join(map(hex, h)) + "\n")
797
798 elif cmd == 'branches':
799 req.httphdr("application/mercurial-0.1")
800 nodes = []
801 if req.form.has_key('nodes'):
802 nodes = map(bin, req.form['nodes'][0].split(" "))
803 for b in self.repo.branches(nodes):
804 req.write(" ".join(map(hex, b)) + "\n")
805
806 elif cmd == 'between':
807 req.httphdr("application/mercurial-0.1")
808 nodes = []
809 if req.form.has_key('pairs'):
810 pairs = [map(bin, p.split("-"))
811 for p in req.form['pairs'][0].split(" ")]
812 for b in self.repo.between(pairs):
813 req.write(" ".join(map(hex, b)) + "\n")
814
815 elif cmd == 'changegroup':
816 req.httphdr("application/mercurial-0.1")
817 nodes = []
818 if not self.allowpull:
819 return
820
821 if req.form.has_key('roots'):
822 nodes = map(bin, req.form['roots'][0].split(" "))
823
824 z = zlib.compressobj()
825 f = self.repo.changegroup(nodes, 'serve')
826 while 1:
827 chunk = f.read(4096)
828 if not chunk:
829 break
830 req.write(z.compress(chunk))
831
832 req.write(z.flush())
833
834 elif cmd == 'archive':
835 changeset = self.repo.lookup(req.form['node'][0])
836 type = req.form['type'][0]
837 if (type in self.archives and
838 self.repo.ui.configbool("web", "allow" + type, False)):
839 self.archive(req, changeset, type)
840 return
841
842 req.write(self.t("error"))
843
844 elif cmd == 'static':
845 fname = req.form['file'][0]
846 req.write(staticfile(static, fname)
847 or self.t("error", error="%r not found" % fname))
848
849 else:
850 req.write(self.t("error"))
851
852 # This is a stopgap
853 class hgwebdir(object):
854 def __init__(self, config):
855 def cleannames(items):
856 return [(name.strip(os.sep), path) for name, path in items]
857
858 self.motd = ""
859 self.repos_sorted = ('name', False)
860 if isinstance(config, (list, tuple)):
861 self.repos = cleannames(config)
862 self.repos_sorted = ('', False)
863 elif isinstance(config, dict):
864 self.repos = cleannames(config.items())
865 self.repos.sort()
866 else:
867 cp = ConfigParser.SafeConfigParser()
868 cp.read(config)
869 self.repos = []
870 if cp.has_section('web') and cp.has_option('web', 'motd'):
871 self.motd = cp.get('web', 'motd')
872 if cp.has_section('paths'):
873 self.repos.extend(cleannames(cp.items('paths')))
874 if cp.has_section('collections'):
875 for prefix, root in cp.items('collections'):
876 for path in util.walkrepos(root):
877 repo = os.path.normpath(path)
878 name = repo
879 if name.startswith(prefix):
880 name = name[len(prefix):]
881 self.repos.append((name.lstrip(os.sep), repo))
882 self.repos.sort()
883
884 def run(self, req=hgrequest()):
885 def header(**map):
886 yield tmpl("header", **map)
887
888 def footer(**map):
889 yield tmpl("footer", motd=self.motd, **map)
890
891 m = os.path.join(templater.templatepath(), "map")
892 tmpl = templater.templater(m, templater.common_filters,
893 defaults={"header": header,
894 "footer": footer})
895
896 def archivelist(ui, nodeid, url):
897 for i in ['zip', 'gz', 'bz2']:
898 if ui.configbool("web", "allow" + i, False):
899 yield {"type" : i, "node": nodeid, "url": url}
900
901 def entries(sortcolumn="", descending=False, **map):
902 rows = []
903 parity = 0
904 for name, path in self.repos:
905 u = ui.ui()
906 try:
907 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
908 except IOError:
909 pass
910 get = u.config
911
912 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
913 .replace("//", "/"))
914
915 # update time with local timezone
916 try:
917 d = (get_mtime(path), util.makedate()[1])
918 except OSError:
919 continue
920
921 contact = (get("ui", "username") or # preferred
922 get("web", "contact") or # deprecated
923 get("web", "author", "")) # also
924 description = get("web", "description", "")
925 name = get("web", "name", name)
926 row = dict(contact=contact or "unknown",
927 contact_sort=contact.upper() or "unknown",
928 name=name,
929 name_sort=name,
930 url=url,
931 description=description or "unknown",
932 description_sort=description.upper() or "unknown",
933 lastchange=d,
934 lastchange_sort=d[1]-d[0],
935 archives=archivelist(u, "tip", url))
936 if (not sortcolumn
937 or (sortcolumn, descending) == self.repos_sorted):
938 # fast path for unsorted output
939 row['parity'] = parity
940 parity = 1 - parity
941 yield row
942 else:
943 rows.append((row["%s_sort" % sortcolumn], row))
944 if rows:
945 rows.sort()
946 if descending:
947 rows.reverse()
948 for key, row in rows:
949 row['parity'] = parity
950 parity = 1 - parity
951 yield row
952
953 virtual = req.env.get("PATH_INFO", "").strip('/')
954 if virtual:
955 real = dict(self.repos).get(virtual)
956 if real:
957 try:
958 hgweb(real).run(req)
959 except IOError, inst:
960 req.write(tmpl("error", error=inst.strerror))
961 except hg.RepoError, inst:
962 req.write(tmpl("error", error=str(inst)))
963 else:
964 req.write(tmpl("notfound", repo=virtual))
965 else:
966 if req.form.has_key('static'):
967 static = os.path.join(templater.templatepath(), "static")
968 fname = req.form['static'][0]
969 req.write(staticfile(static, fname)
970 or tmpl("error", error="%r not found" % fname))
971 else:
972 sortable = ["name", "description", "contact", "lastchange"]
973 sortcolumn, descending = self.repos_sorted
974 if req.form.has_key('sort'):
975 sortcolumn = req.form['sort'][0]
976 descending = sortcolumn.startswith('-')
977 if descending:
978 sortcolumn = sortcolumn[1:]
979 if sortcolumn not in sortable:
980 sortcolumn = ""
981
982 sort = [("sort_%s" % column,
983 "%s%s" % ((not descending and column == sortcolumn)
984 and "-" or "", column))
985 for column in sortable]
986 req.write(tmpl("index", entries=entries,
987 sortcolumn=sortcolumn, descending=descending,
988 **dict(sort)))
10 demandload(globals(), "mercurial.hgweb.hgweb_mod:hgweb")
11 demandload(globals(), "mercurial.hgweb.hgwebdir_mod:hgwebdir")
This diff has been collapsed as it changes many lines, (950 lines changed) Show them Hide them
@@ -1,988 +1,42 b''
1 1 # hgweb.py - web interface to a mercurial repository
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 import os, cgi, sys
10 import mimetypes
11 from mercurial.demandload import demandload
12 demandload(globals(), "time re socket zlib errno ConfigParser tempfile")
13 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
14 demandload(globals(), "mercurial.hgweb.request:hgrequest")
15 demandload(globals(), "mercurial.hgweb.server:create_server")
16 from mercurial.node import *
17 from mercurial.i18n import gettext as _
18
19 def up(p):
20 if p[0] != "/":
21 p = "/" + p
22 if p[-1] == "/":
23 p = p[:-1]
24 up = os.path.dirname(p)
25 if up == "/":
26 return "/"
27 return up + "/"
9 import os, mimetypes
10 import os.path
28 11
29 12 def get_mtime(repo_path):
30 13 hg_path = os.path.join(repo_path, ".hg")
31 14 cl_path = os.path.join(hg_path, "00changelog.i")
32 15 if os.path.exists(os.path.join(cl_path)):
33 16 return os.stat(cl_path).st_mtime
34 17 else:
35 18 return os.stat(hg_path).st_mtime
36 19
37 20 def staticfile(directory, fname):
38 21 """return a file inside directory with guessed content-type header
39 22
40 23 fname always uses '/' as directory separator and isn't allowed to
41 24 contain unusual path components.
42 25 Content-type is guessed using the mimetypes module.
43 26 Return an empty string if fname is illegal or file not found.
44 27
45 28 """
46 29 parts = fname.split('/')
47 30 path = directory
48 31 for part in parts:
49 32 if (part in ('', os.curdir, os.pardir) or
50 33 os.sep in part or os.altsep is not None and os.altsep in part):
51 34 return ""
52 35 path = os.path.join(path, part)
53 36 try:
54 37 os.stat(path)
55 38 ct = mimetypes.guess_type(path)[0] or "text/plain"
56 39 return "Content-type: %s\n\n%s" % (ct, file(path).read())
57 40 except (TypeError, OSError):
58 41 # illegal fname or unreadable file
59 42 return ""
60
61 class hgweb(object):
62 def __init__(self, repo, name=None):
63 if type(repo) == type(""):
64 self.repo = hg.repository(ui.ui(), repo)
65 else:
66 self.repo = repo
67
68 self.mtime = -1
69 self.reponame = name
70 self.archives = 'zip', 'gz', 'bz2'
71
72 def refresh(self):
73 mtime = get_mtime(self.repo.root)
74 if mtime != self.mtime:
75 self.mtime = mtime
76 self.repo = hg.repository(self.repo.ui, self.repo.root)
77 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
78 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
79 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
80
81 def archivelist(self, nodeid):
82 for i in self.archives:
83 if self.repo.ui.configbool("web", "allow" + i, False):
84 yield {"type" : i, "node" : nodeid, "url": ""}
85
86 def listfiles(self, files, mf):
87 for f in files[:self.maxfiles]:
88 yield self.t("filenodelink", node=hex(mf[f]), file=f)
89 if len(files) > self.maxfiles:
90 yield self.t("fileellipses")
91
92 def listfilediffs(self, files, changeset):
93 for f in files[:self.maxfiles]:
94 yield self.t("filedifflink", node=hex(changeset), file=f)
95 if len(files) > self.maxfiles:
96 yield self.t("fileellipses")
97
98 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
99 if not rev:
100 rev = lambda x: ""
101 siblings = [s for s in siblings if s != nullid]
102 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
103 return
104 for s in siblings:
105 yield dict(node=hex(s), rev=rev(s), **args)
106
107 def renamelink(self, fl, node):
108 r = fl.renamed(node)
109 if r:
110 return [dict(file=r[0], node=hex(r[1]))]
111 return []
112
113 def showtag(self, t1, node=nullid, **args):
114 for t in self.repo.nodetags(node):
115 yield self.t(t1, tag=t, **args)
116
117 def diff(self, node1, node2, files):
118 def filterfiles(filters, files):
119 l = [x for x in files if x in filters]
120
121 for t in filters:
122 if t and t[-1] != os.sep:
123 t += os.sep
124 l += [x for x in files if x.startswith(t)]
125 return l
126
127 parity = [0]
128 def diffblock(diff, f, fn):
129 yield self.t("diffblock",
130 lines=prettyprintlines(diff),
131 parity=parity[0],
132 file=f,
133 filenode=hex(fn or nullid))
134 parity[0] = 1 - parity[0]
135
136 def prettyprintlines(diff):
137 for l in diff.splitlines(1):
138 if l.startswith('+'):
139 yield self.t("difflineplus", line=l)
140 elif l.startswith('-'):
141 yield self.t("difflineminus", line=l)
142 elif l.startswith('@'):
143 yield self.t("difflineat", line=l)
144 else:
145 yield self.t("diffline", line=l)
146
147 r = self.repo
148 cl = r.changelog
149 mf = r.manifest
150 change1 = cl.read(node1)
151 change2 = cl.read(node2)
152 mmap1 = mf.read(change1[0])
153 mmap2 = mf.read(change2[0])
154 date1 = util.datestr(change1[2])
155 date2 = util.datestr(change2[2])
156
157 modified, added, removed, deleted, unknown = r.changes(node1, node2)
158 if files:
159 modified, added, removed = map(lambda x: filterfiles(files, x),
160 (modified, added, removed))
161
162 diffopts = self.repo.ui.diffopts()
163 showfunc = diffopts['showfunc']
164 ignorews = diffopts['ignorews']
165 for f in modified:
166 to = r.file(f).read(mmap1[f])
167 tn = r.file(f).read(mmap2[f])
168 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
169 showfunc=showfunc, ignorews=ignorews), f, tn)
170 for f in added:
171 to = None
172 tn = r.file(f).read(mmap2[f])
173 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
174 showfunc=showfunc, ignorews=ignorews), f, tn)
175 for f in removed:
176 to = r.file(f).read(mmap1[f])
177 tn = None
178 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
179 showfunc=showfunc, ignorews=ignorews), f, tn)
180
181 def changelog(self, pos):
182 def changenav(**map):
183 def seq(factor, maxchanges=None):
184 if maxchanges:
185 yield maxchanges
186 if maxchanges >= 20 and maxchanges <= 40:
187 yield 50
188 else:
189 yield 1 * factor
190 yield 3 * factor
191 for f in seq(factor * 10):
192 yield f
193
194 l = []
195 last = 0
196 for f in seq(1, self.maxchanges):
197 if f < self.maxchanges or f <= last:
198 continue
199 if f > count:
200 break
201 last = f
202 r = "%d" % f
203 if pos + f < count:
204 l.append(("+" + r, pos + f))
205 if pos - f >= 0:
206 l.insert(0, ("-" + r, pos - f))
207
208 yield {"rev": 0, "label": "(0)"}
209
210 for label, rev in l:
211 yield {"label": label, "rev": rev}
212
213 yield {"label": "tip", "rev": "tip"}
214
215 def changelist(**map):
216 parity = (start - end) & 1
217 cl = self.repo.changelog
218 l = [] # build a list in forward order for efficiency
219 for i in range(start, end):
220 n = cl.node(i)
221 changes = cl.read(n)
222 hn = hex(n)
223
224 l.insert(0, {"parity": parity,
225 "author": changes[1],
226 "parent": self.siblings(cl.parents(n), cl.rev,
227 cl.rev(n) - 1),
228 "child": self.siblings(cl.children(n), cl.rev,
229 cl.rev(n) + 1),
230 "changelogtag": self.showtag("changelogtag",n),
231 "manifest": hex(changes[0]),
232 "desc": changes[4],
233 "date": changes[2],
234 "files": self.listfilediffs(changes[3], n),
235 "rev": i,
236 "node": hn})
237 parity = 1 - parity
238
239 for e in l:
240 yield e
241
242 cl = self.repo.changelog
243 mf = cl.read(cl.tip())[0]
244 count = cl.count()
245 start = max(0, pos - self.maxchanges + 1)
246 end = min(count, start + self.maxchanges)
247 pos = end - 1
248
249 yield self.t('changelog',
250 changenav=changenav,
251 manifest=hex(mf),
252 rev=pos, changesets=count, entries=changelist,
253 archives=self.archivelist("tip"))
254
255 def search(self, query):
256
257 def changelist(**map):
258 cl = self.repo.changelog
259 count = 0
260 qw = query.lower().split()
261
262 def revgen():
263 for i in range(cl.count() - 1, 0, -100):
264 l = []
265 for j in range(max(0, i - 100), i):
266 n = cl.node(j)
267 changes = cl.read(n)
268 l.append((n, j, changes))
269 l.reverse()
270 for e in l:
271 yield e
272
273 for n, i, changes in revgen():
274 miss = 0
275 for q in qw:
276 if not (q in changes[1].lower() or
277 q in changes[4].lower() or
278 q in " ".join(changes[3][:20]).lower()):
279 miss = 1
280 break
281 if miss:
282 continue
283
284 count += 1
285 hn = hex(n)
286
287 yield self.t('searchentry',
288 parity=count & 1,
289 author=changes[1],
290 parent=self.siblings(cl.parents(n), cl.rev),
291 child=self.siblings(cl.children(n), cl.rev),
292 changelogtag=self.showtag("changelogtag",n),
293 manifest=hex(changes[0]),
294 desc=changes[4],
295 date=changes[2],
296 files=self.listfilediffs(changes[3], n),
297 rev=i,
298 node=hn)
299
300 if count >= self.maxchanges:
301 break
302
303 cl = self.repo.changelog
304 mf = cl.read(cl.tip())[0]
305
306 yield self.t('search',
307 query=query,
308 manifest=hex(mf),
309 entries=changelist)
310
311 def changeset(self, nodeid):
312 cl = self.repo.changelog
313 n = self.repo.lookup(nodeid)
314 nodeid = hex(n)
315 changes = cl.read(n)
316 p1 = cl.parents(n)[0]
317
318 files = []
319 mf = self.repo.manifest.read(changes[0])
320 for f in changes[3]:
321 files.append(self.t("filenodelink",
322 filenode=hex(mf.get(f, nullid)), file=f))
323
324 def diff(**map):
325 yield self.diff(p1, n, None)
326
327 yield self.t('changeset',
328 diff=diff,
329 rev=cl.rev(n),
330 node=nodeid,
331 parent=self.siblings(cl.parents(n), cl.rev),
332 child=self.siblings(cl.children(n), cl.rev),
333 changesettag=self.showtag("changesettag",n),
334 manifest=hex(changes[0]),
335 author=changes[1],
336 desc=changes[4],
337 date=changes[2],
338 files=files,
339 archives=self.archivelist(nodeid))
340
341 def filelog(self, f, filenode):
342 cl = self.repo.changelog
343 fl = self.repo.file(f)
344 filenode = hex(fl.lookup(filenode))
345 count = fl.count()
346
347 def entries(**map):
348 l = []
349 parity = (count - 1) & 1
350
351 for i in range(count):
352 n = fl.node(i)
353 lr = fl.linkrev(n)
354 cn = cl.node(lr)
355 cs = cl.read(cl.node(lr))
356
357 l.insert(0, {"parity": parity,
358 "filenode": hex(n),
359 "filerev": i,
360 "file": f,
361 "node": hex(cn),
362 "author": cs[1],
363 "date": cs[2],
364 "rename": self.renamelink(fl, n),
365 "parent": self.siblings(fl.parents(n),
366 fl.rev, file=f),
367 "child": self.siblings(fl.children(n),
368 fl.rev, file=f),
369 "desc": cs[4]})
370 parity = 1 - parity
371
372 for e in l:
373 yield e
374
375 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
376
377 def filerevision(self, f, node):
378 fl = self.repo.file(f)
379 n = fl.lookup(node)
380 node = hex(n)
381 text = fl.read(n)
382 changerev = fl.linkrev(n)
383 cl = self.repo.changelog
384 cn = cl.node(changerev)
385 cs = cl.read(cn)
386 mfn = cs[0]
387
388 mt = mimetypes.guess_type(f)[0]
389 rawtext = text
390 if util.binary(text):
391 mt = mt or 'application/octet-stream'
392 text = "(binary:%s)" % mt
393 mt = mt or 'text/plain'
394
395 def lines():
396 for l, t in enumerate(text.splitlines(1)):
397 yield {"line": t,
398 "linenumber": "% 6d" % (l + 1),
399 "parity": l & 1}
400
401 yield self.t("filerevision",
402 file=f,
403 filenode=node,
404 path=up(f),
405 text=lines(),
406 raw=rawtext,
407 mimetype=mt,
408 rev=changerev,
409 node=hex(cn),
410 manifest=hex(mfn),
411 author=cs[1],
412 date=cs[2],
413 parent=self.siblings(fl.parents(n), fl.rev, file=f),
414 child=self.siblings(fl.children(n), fl.rev, file=f),
415 rename=self.renamelink(fl, n),
416 permissions=self.repo.manifest.readflags(mfn)[f])
417
418 def fileannotate(self, f, node):
419 bcache = {}
420 ncache = {}
421 fl = self.repo.file(f)
422 n = fl.lookup(node)
423 node = hex(n)
424 changerev = fl.linkrev(n)
425
426 cl = self.repo.changelog
427 cn = cl.node(changerev)
428 cs = cl.read(cn)
429 mfn = cs[0]
430
431 def annotate(**map):
432 parity = 1
433 last = None
434 for r, l in fl.annotate(n):
435 try:
436 cnode = ncache[r]
437 except KeyError:
438 cnode = ncache[r] = self.repo.changelog.node(r)
439
440 try:
441 name = bcache[r]
442 except KeyError:
443 cl = self.repo.changelog.read(cnode)
444 bcache[r] = name = self.repo.ui.shortuser(cl[1])
445
446 if last != cnode:
447 parity = 1 - parity
448 last = cnode
449
450 yield {"parity": parity,
451 "node": hex(cnode),
452 "rev": r,
453 "author": name,
454 "file": f,
455 "line": l}
456
457 yield self.t("fileannotate",
458 file=f,
459 filenode=node,
460 annotate=annotate,
461 path=up(f),
462 rev=changerev,
463 node=hex(cn),
464 manifest=hex(mfn),
465 author=cs[1],
466 date=cs[2],
467 rename=self.renamelink(fl, n),
468 parent=self.siblings(fl.parents(n), fl.rev, file=f),
469 child=self.siblings(fl.children(n), fl.rev, file=f),
470 permissions=self.repo.manifest.readflags(mfn)[f])
471
472 def manifest(self, mnode, path):
473 man = self.repo.manifest
474 mn = man.lookup(mnode)
475 mnode = hex(mn)
476 mf = man.read(mn)
477 rev = man.rev(mn)
478 changerev = man.linkrev(mn)
479 node = self.repo.changelog.node(changerev)
480 mff = man.readflags(mn)
481
482 files = {}
483
484 p = path[1:]
485 if p and p[-1] != "/":
486 p += "/"
487 l = len(p)
488
489 for f,n in mf.items():
490 if f[:l] != p:
491 continue
492 remain = f[l:]
493 if "/" in remain:
494 short = remain[:remain.find("/") + 1] # bleah
495 files[short] = (f, None)
496 else:
497 short = os.path.basename(remain)
498 files[short] = (f, n)
499
500 def filelist(**map):
501 parity = 0
502 fl = files.keys()
503 fl.sort()
504 for f in fl:
505 full, fnode = files[f]
506 if not fnode:
507 continue
508
509 yield {"file": full,
510 "manifest": mnode,
511 "filenode": hex(fnode),
512 "parity": parity,
513 "basename": f,
514 "permissions": mff[full]}
515 parity = 1 - parity
516
517 def dirlist(**map):
518 parity = 0
519 fl = files.keys()
520 fl.sort()
521 for f in fl:
522 full, fnode = files[f]
523 if fnode:
524 continue
525
526 yield {"parity": parity,
527 "path": os.path.join(path, f),
528 "manifest": mnode,
529 "basename": f[:-1]}
530 parity = 1 - parity
531
532 yield self.t("manifest",
533 manifest=mnode,
534 rev=rev,
535 node=hex(node),
536 path=path,
537 up=up(path),
538 fentries=filelist,
539 dentries=dirlist,
540 archives=self.archivelist(hex(node)))
541
542 def tags(self):
543 cl = self.repo.changelog
544 mf = cl.read(cl.tip())[0]
545
546 i = self.repo.tagslist()
547 i.reverse()
548
549 def entries(notip=False, **map):
550 parity = 0
551 for k,n in i:
552 if notip and k == "tip": continue
553 yield {"parity": parity,
554 "tag": k,
555 "tagmanifest": hex(cl.read(n)[0]),
556 "date": cl.read(n)[2],
557 "node": hex(n)}
558 parity = 1 - parity
559
560 yield self.t("tags",
561 manifest=hex(mf),
562 entries=lambda **x: entries(False, **x),
563 entriesnotip=lambda **x: entries(True, **x))
564
565 def summary(self):
566 cl = self.repo.changelog
567 mf = cl.read(cl.tip())[0]
568
569 i = self.repo.tagslist()
570 i.reverse()
571
572 def tagentries(**map):
573 parity = 0
574 count = 0
575 for k,n in i:
576 if k == "tip": # skip tip
577 continue;
578
579 count += 1
580 if count > 10: # limit to 10 tags
581 break;
582
583 c = cl.read(n)
584 m = c[0]
585 t = c[2]
586
587 yield self.t("tagentry",
588 parity = parity,
589 tag = k,
590 node = hex(n),
591 date = t,
592 tagmanifest = hex(m))
593 parity = 1 - parity
594
595 def changelist(**map):
596 parity = 0
597 cl = self.repo.changelog
598 l = [] # build a list in forward order for efficiency
599 for i in range(start, end):
600 n = cl.node(i)
601 changes = cl.read(n)
602 hn = hex(n)
603 t = changes[2]
604
605 l.insert(0, self.t(
606 'shortlogentry',
607 parity = parity,
608 author = changes[1],
609 manifest = hex(changes[0]),
610 desc = changes[4],
611 date = t,
612 rev = i,
613 node = hn))
614 parity = 1 - parity
615
616 yield l
617
618 cl = self.repo.changelog
619 mf = cl.read(cl.tip())[0]
620 count = cl.count()
621 start = max(0, count - self.maxchanges)
622 end = min(count, start + self.maxchanges)
623 pos = end - 1
624
625 yield self.t("summary",
626 desc = self.repo.ui.config("web", "description", "unknown"),
627 owner = (self.repo.ui.config("ui", "username") or # preferred
628 self.repo.ui.config("web", "contact") or # deprecated
629 self.repo.ui.config("web", "author", "unknown")), # also
630 lastchange = (0, 0), # FIXME
631 manifest = hex(mf),
632 tags = tagentries,
633 shortlog = changelist)
634
635 def filediff(self, file, changeset):
636 cl = self.repo.changelog
637 n = self.repo.lookup(changeset)
638 changeset = hex(n)
639 p1 = cl.parents(n)[0]
640 cs = cl.read(n)
641 mf = self.repo.manifest.read(cs[0])
642
643 def diff(**map):
644 yield self.diff(p1, n, [file])
645
646 yield self.t("filediff",
647 file=file,
648 filenode=hex(mf.get(file, nullid)),
649 node=changeset,
650 rev=self.repo.changelog.rev(n),
651 parent=self.siblings(cl.parents(n), cl.rev),
652 child=self.siblings(cl.children(n), cl.rev),
653 diff=diff)
654
655 archive_specs = {
656 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
657 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
658 'zip': ('application/zip', 'zip', '.zip', None),
659 }
660
661 def archive(self, req, cnode, type):
662 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
663 name = "%s-%s" % (reponame, short(cnode))
664 mimetype, artype, extension, encoding = self.archive_specs[type]
665 headers = [('Content-type', mimetype),
666 ('Content-disposition', 'attachment; filename=%s%s' %
667 (name, extension))]
668 if encoding:
669 headers.append(('Content-encoding', encoding))
670 req.header(headers)
671 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
672
673 # add tags to things
674 # tags -> list of changesets corresponding to tags
675 # find tag, changeset, file
676
677 def run(self, req=hgrequest()):
678 def clean(path):
679 p = util.normpath(path)
680 if p[:2] == "..":
681 raise "suspicious path"
682 return p
683
684 def header(**map):
685 yield self.t("header", **map)
686
687 def footer(**map):
688 yield self.t("footer",
689 motd=self.repo.ui.config("web", "motd", ""),
690 **map)
691
692 def expand_form(form):
693 shortcuts = {
694 'cl': [('cmd', ['changelog']), ('rev', None)],
695 'cs': [('cmd', ['changeset']), ('node', None)],
696 'f': [('cmd', ['file']), ('filenode', None)],
697 'fl': [('cmd', ['filelog']), ('filenode', None)],
698 'fd': [('cmd', ['filediff']), ('node', None)],
699 'fa': [('cmd', ['annotate']), ('filenode', None)],
700 'mf': [('cmd', ['manifest']), ('manifest', None)],
701 'ca': [('cmd', ['archive']), ('node', None)],
702 'tags': [('cmd', ['tags'])],
703 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
704 'static': [('cmd', ['static']), ('file', None)]
705 }
706
707 for k in shortcuts.iterkeys():
708 if form.has_key(k):
709 for name, value in shortcuts[k]:
710 if value is None:
711 value = form[k]
712 form[name] = value
713 del form[k]
714
715 self.refresh()
716
717 expand_form(req.form)
718
719 t = self.repo.ui.config("web", "templates", templater.templatepath())
720 static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
721 m = os.path.join(t, "map")
722 style = self.repo.ui.config("web", "style", "")
723 if req.form.has_key('style'):
724 style = req.form['style'][0]
725 if style:
726 b = os.path.basename("map-" + style)
727 p = os.path.join(t, b)
728 if os.path.isfile(p):
729 m = p
730
731 port = req.env["SERVER_PORT"]
732 port = port != "80" and (":" + port) or ""
733 uri = req.env["REQUEST_URI"]
734 if "?" in uri:
735 uri = uri.split("?")[0]
736 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
737 if not self.reponame:
738 self.reponame = (self.repo.ui.config("web", "name")
739 or uri.strip('/') or self.repo.root)
740
741 self.t = templater.templater(m, templater.common_filters,
742 defaults={"url": url,
743 "repo": self.reponame,
744 "header": header,
745 "footer": footer,
746 })
747
748 if not req.form.has_key('cmd'):
749 req.form['cmd'] = [self.t.cache['default'],]
750
751 cmd = req.form['cmd'][0]
752 if cmd == 'changelog':
753 hi = self.repo.changelog.count() - 1
754 if req.form.has_key('rev'):
755 hi = req.form['rev'][0]
756 try:
757 hi = self.repo.changelog.rev(self.repo.lookup(hi))
758 except hg.RepoError:
759 req.write(self.search(hi)) # XXX redirect to 404 page?
760 return
761
762 req.write(self.changelog(hi))
763
764 elif cmd == 'changeset':
765 req.write(self.changeset(req.form['node'][0]))
766
767 elif cmd == 'manifest':
768 req.write(self.manifest(req.form['manifest'][0],
769 clean(req.form['path'][0])))
770
771 elif cmd == 'tags':
772 req.write(self.tags())
773
774 elif cmd == 'summary':
775 req.write(self.summary())
776
777 elif cmd == 'filediff':
778 req.write(self.filediff(clean(req.form['file'][0]),
779 req.form['node'][0]))
780
781 elif cmd == 'file':
782 req.write(self.filerevision(clean(req.form['file'][0]),
783 req.form['filenode'][0]))
784
785 elif cmd == 'annotate':
786 req.write(self.fileannotate(clean(req.form['file'][0]),
787 req.form['filenode'][0]))
788
789 elif cmd == 'filelog':
790 req.write(self.filelog(clean(req.form['file'][0]),
791 req.form['filenode'][0]))
792
793 elif cmd == 'heads':
794 req.httphdr("application/mercurial-0.1")
795 h = self.repo.heads()
796 req.write(" ".join(map(hex, h)) + "\n")
797
798 elif cmd == 'branches':
799 req.httphdr("application/mercurial-0.1")
800 nodes = []
801 if req.form.has_key('nodes'):
802 nodes = map(bin, req.form['nodes'][0].split(" "))
803 for b in self.repo.branches(nodes):
804 req.write(" ".join(map(hex, b)) + "\n")
805
806 elif cmd == 'between':
807 req.httphdr("application/mercurial-0.1")
808 nodes = []
809 if req.form.has_key('pairs'):
810 pairs = [map(bin, p.split("-"))
811 for p in req.form['pairs'][0].split(" ")]
812 for b in self.repo.between(pairs):
813 req.write(" ".join(map(hex, b)) + "\n")
814
815 elif cmd == 'changegroup':
816 req.httphdr("application/mercurial-0.1")
817 nodes = []
818 if not self.allowpull:
819 return
820
821 if req.form.has_key('roots'):
822 nodes = map(bin, req.form['roots'][0].split(" "))
823
824 z = zlib.compressobj()
825 f = self.repo.changegroup(nodes, 'serve')
826 while 1:
827 chunk = f.read(4096)
828 if not chunk:
829 break
830 req.write(z.compress(chunk))
831
832 req.write(z.flush())
833
834 elif cmd == 'archive':
835 changeset = self.repo.lookup(req.form['node'][0])
836 type = req.form['type'][0]
837 if (type in self.archives and
838 self.repo.ui.configbool("web", "allow" + type, False)):
839 self.archive(req, changeset, type)
840 return
841
842 req.write(self.t("error"))
843
844 elif cmd == 'static':
845 fname = req.form['file'][0]
846 req.write(staticfile(static, fname)
847 or self.t("error", error="%r not found" % fname))
848
849 else:
850 req.write(self.t("error"))
851
852 # This is a stopgap
853 class hgwebdir(object):
854 def __init__(self, config):
855 def cleannames(items):
856 return [(name.strip(os.sep), path) for name, path in items]
857
858 self.motd = ""
859 self.repos_sorted = ('name', False)
860 if isinstance(config, (list, tuple)):
861 self.repos = cleannames(config)
862 self.repos_sorted = ('', False)
863 elif isinstance(config, dict):
864 self.repos = cleannames(config.items())
865 self.repos.sort()
866 else:
867 cp = ConfigParser.SafeConfigParser()
868 cp.read(config)
869 self.repos = []
870 if cp.has_section('web') and cp.has_option('web', 'motd'):
871 self.motd = cp.get('web', 'motd')
872 if cp.has_section('paths'):
873 self.repos.extend(cleannames(cp.items('paths')))
874 if cp.has_section('collections'):
875 for prefix, root in cp.items('collections'):
876 for path in util.walkrepos(root):
877 repo = os.path.normpath(path)
878 name = repo
879 if name.startswith(prefix):
880 name = name[len(prefix):]
881 self.repos.append((name.lstrip(os.sep), repo))
882 self.repos.sort()
883
884 def run(self, req=hgrequest()):
885 def header(**map):
886 yield tmpl("header", **map)
887
888 def footer(**map):
889 yield tmpl("footer", motd=self.motd, **map)
890
891 m = os.path.join(templater.templatepath(), "map")
892 tmpl = templater.templater(m, templater.common_filters,
893 defaults={"header": header,
894 "footer": footer})
895
896 def archivelist(ui, nodeid, url):
897 for i in ['zip', 'gz', 'bz2']:
898 if ui.configbool("web", "allow" + i, False):
899 yield {"type" : i, "node": nodeid, "url": url}
900
901 def entries(sortcolumn="", descending=False, **map):
902 rows = []
903 parity = 0
904 for name, path in self.repos:
905 u = ui.ui()
906 try:
907 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
908 except IOError:
909 pass
910 get = u.config
911
912 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
913 .replace("//", "/"))
914
915 # update time with local timezone
916 try:
917 d = (get_mtime(path), util.makedate()[1])
918 except OSError:
919 continue
920
921 contact = (get("ui", "username") or # preferred
922 get("web", "contact") or # deprecated
923 get("web", "author", "")) # also
924 description = get("web", "description", "")
925 name = get("web", "name", name)
926 row = dict(contact=contact or "unknown",
927 contact_sort=contact.upper() or "unknown",
928 name=name,
929 name_sort=name,
930 url=url,
931 description=description or "unknown",
932 description_sort=description.upper() or "unknown",
933 lastchange=d,
934 lastchange_sort=d[1]-d[0],
935 archives=archivelist(u, "tip", url))
936 if (not sortcolumn
937 or (sortcolumn, descending) == self.repos_sorted):
938 # fast path for unsorted output
939 row['parity'] = parity
940 parity = 1 - parity
941 yield row
942 else:
943 rows.append((row["%s_sort" % sortcolumn], row))
944 if rows:
945 rows.sort()
946 if descending:
947 rows.reverse()
948 for key, row in rows:
949 row['parity'] = parity
950 parity = 1 - parity
951 yield row
952
953 virtual = req.env.get("PATH_INFO", "").strip('/')
954 if virtual:
955 real = dict(self.repos).get(virtual)
956 if real:
957 try:
958 hgweb(real).run(req)
959 except IOError, inst:
960 req.write(tmpl("error", error=inst.strerror))
961 except hg.RepoError, inst:
962 req.write(tmpl("error", error=str(inst)))
963 else:
964 req.write(tmpl("notfound", repo=virtual))
965 else:
966 if req.form.has_key('static'):
967 static = os.path.join(templater.templatepath(), "static")
968 fname = req.form['static'][0]
969 req.write(staticfile(static, fname)
970 or tmpl("error", error="%r not found" % fname))
971 else:
972 sortable = ["name", "description", "contact", "lastchange"]
973 sortcolumn, descending = self.repos_sorted
974 if req.form.has_key('sort'):
975 sortcolumn = req.form['sort'][0]
976 descending = sortcolumn.startswith('-')
977 if descending:
978 sortcolumn = sortcolumn[1:]
979 if sortcolumn not in sortable:
980 sortcolumn = ""
981
982 sort = [("sort_%s" % column,
983 "%s%s" % ((not descending and column == sortcolumn)
984 and "-" or "", column))
985 for column in sortable]
986 req.write(tmpl("index", entries=entries,
987 sortcolumn=sortcolumn, descending=descending,
988 **dict(sort)))
@@ -1,988 +1,819 b''
1 1 # hgweb.py - web interface to a mercurial repository
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 import os, cgi, sys
9 import os
10 import os.path
10 11 import mimetypes
11 12 from mercurial.demandload import demandload
12 demandload(globals(), "time re socket zlib errno ConfigParser tempfile")
13 demandload(globals(), "re zlib ConfigParser")
13 14 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
14 15 demandload(globals(), "mercurial.hgweb.request:hgrequest")
15 demandload(globals(), "mercurial.hgweb.server:create_server")
16 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
16 17 from mercurial.node import *
17 18 from mercurial.i18n import gettext as _
18 19
19 def up(p):
20 def _up(p):
20 21 if p[0] != "/":
21 22 p = "/" + p
22 23 if p[-1] == "/":
23 24 p = p[:-1]
24 25 up = os.path.dirname(p)
25 26 if up == "/":
26 27 return "/"
27 28 return up + "/"
28 29
29 def get_mtime(repo_path):
30 hg_path = os.path.join(repo_path, ".hg")
31 cl_path = os.path.join(hg_path, "00changelog.i")
32 if os.path.exists(os.path.join(cl_path)):
33 return os.stat(cl_path).st_mtime
34 else:
35 return os.stat(hg_path).st_mtime
36
37 def staticfile(directory, fname):
38 """return a file inside directory with guessed content-type header
39
40 fname always uses '/' as directory separator and isn't allowed to
41 contain unusual path components.
42 Content-type is guessed using the mimetypes module.
43 Return an empty string if fname is illegal or file not found.
44
45 """
46 parts = fname.split('/')
47 path = directory
48 for part in parts:
49 if (part in ('', os.curdir, os.pardir) or
50 os.sep in part or os.altsep is not None and os.altsep in part):
51 return ""
52 path = os.path.join(path, part)
53 try:
54 os.stat(path)
55 ct = mimetypes.guess_type(path)[0] or "text/plain"
56 return "Content-type: %s\n\n%s" % (ct, file(path).read())
57 except (TypeError, OSError):
58 # illegal fname or unreadable file
59 return ""
60
61 30 class hgweb(object):
62 31 def __init__(self, repo, name=None):
63 32 if type(repo) == type(""):
64 33 self.repo = hg.repository(ui.ui(), repo)
65 34 else:
66 35 self.repo = repo
67 36
68 37 self.mtime = -1
69 38 self.reponame = name
70 39 self.archives = 'zip', 'gz', 'bz2'
71 40
72 41 def refresh(self):
73 42 mtime = get_mtime(self.repo.root)
74 43 if mtime != self.mtime:
75 44 self.mtime = mtime
76 45 self.repo = hg.repository(self.repo.ui, self.repo.root)
77 46 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
78 47 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
79 48 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
80 49
81 50 def archivelist(self, nodeid):
82 51 for i in self.archives:
83 52 if self.repo.ui.configbool("web", "allow" + i, False):
84 53 yield {"type" : i, "node" : nodeid, "url": ""}
85 54
86 55 def listfiles(self, files, mf):
87 56 for f in files[:self.maxfiles]:
88 57 yield self.t("filenodelink", node=hex(mf[f]), file=f)
89 58 if len(files) > self.maxfiles:
90 59 yield self.t("fileellipses")
91 60
92 61 def listfilediffs(self, files, changeset):
93 62 for f in files[:self.maxfiles]:
94 63 yield self.t("filedifflink", node=hex(changeset), file=f)
95 64 if len(files) > self.maxfiles:
96 65 yield self.t("fileellipses")
97 66
98 67 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
99 68 if not rev:
100 69 rev = lambda x: ""
101 70 siblings = [s for s in siblings if s != nullid]
102 71 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
103 72 return
104 73 for s in siblings:
105 74 yield dict(node=hex(s), rev=rev(s), **args)
106 75
107 76 def renamelink(self, fl, node):
108 77 r = fl.renamed(node)
109 78 if r:
110 79 return [dict(file=r[0], node=hex(r[1]))]
111 80 return []
112 81
113 82 def showtag(self, t1, node=nullid, **args):
114 83 for t in self.repo.nodetags(node):
115 84 yield self.t(t1, tag=t, **args)
116 85
117 86 def diff(self, node1, node2, files):
118 87 def filterfiles(filters, files):
119 88 l = [x for x in files if x in filters]
120 89
121 90 for t in filters:
122 91 if t and t[-1] != os.sep:
123 92 t += os.sep
124 93 l += [x for x in files if x.startswith(t)]
125 94 return l
126 95
127 96 parity = [0]
128 97 def diffblock(diff, f, fn):
129 98 yield self.t("diffblock",
130 99 lines=prettyprintlines(diff),
131 100 parity=parity[0],
132 101 file=f,
133 102 filenode=hex(fn or nullid))
134 103 parity[0] = 1 - parity[0]
135 104
136 105 def prettyprintlines(diff):
137 106 for l in diff.splitlines(1):
138 107 if l.startswith('+'):
139 108 yield self.t("difflineplus", line=l)
140 109 elif l.startswith('-'):
141 110 yield self.t("difflineminus", line=l)
142 111 elif l.startswith('@'):
143 112 yield self.t("difflineat", line=l)
144 113 else:
145 114 yield self.t("diffline", line=l)
146 115
147 116 r = self.repo
148 117 cl = r.changelog
149 118 mf = r.manifest
150 119 change1 = cl.read(node1)
151 120 change2 = cl.read(node2)
152 121 mmap1 = mf.read(change1[0])
153 122 mmap2 = mf.read(change2[0])
154 123 date1 = util.datestr(change1[2])
155 124 date2 = util.datestr(change2[2])
156 125
157 126 modified, added, removed, deleted, unknown = r.changes(node1, node2)
158 127 if files:
159 128 modified, added, removed = map(lambda x: filterfiles(files, x),
160 129 (modified, added, removed))
161 130
162 131 diffopts = self.repo.ui.diffopts()
163 132 showfunc = diffopts['showfunc']
164 133 ignorews = diffopts['ignorews']
165 134 for f in modified:
166 135 to = r.file(f).read(mmap1[f])
167 136 tn = r.file(f).read(mmap2[f])
168 137 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
169 138 showfunc=showfunc, ignorews=ignorews), f, tn)
170 139 for f in added:
171 140 to = None
172 141 tn = r.file(f).read(mmap2[f])
173 142 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
174 143 showfunc=showfunc, ignorews=ignorews), f, tn)
175 144 for f in removed:
176 145 to = r.file(f).read(mmap1[f])
177 146 tn = None
178 147 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
179 148 showfunc=showfunc, ignorews=ignorews), f, tn)
180 149
181 150 def changelog(self, pos):
182 151 def changenav(**map):
183 152 def seq(factor, maxchanges=None):
184 153 if maxchanges:
185 154 yield maxchanges
186 155 if maxchanges >= 20 and maxchanges <= 40:
187 156 yield 50
188 157 else:
189 158 yield 1 * factor
190 159 yield 3 * factor
191 160 for f in seq(factor * 10):
192 161 yield f
193 162
194 163 l = []
195 164 last = 0
196 165 for f in seq(1, self.maxchanges):
197 166 if f < self.maxchanges or f <= last:
198 167 continue
199 168 if f > count:
200 169 break
201 170 last = f
202 171 r = "%d" % f
203 172 if pos + f < count:
204 173 l.append(("+" + r, pos + f))
205 174 if pos - f >= 0:
206 175 l.insert(0, ("-" + r, pos - f))
207 176
208 177 yield {"rev": 0, "label": "(0)"}
209 178
210 179 for label, rev in l:
211 180 yield {"label": label, "rev": rev}
212 181
213 182 yield {"label": "tip", "rev": "tip"}
214 183
215 184 def changelist(**map):
216 185 parity = (start - end) & 1
217 186 cl = self.repo.changelog
218 187 l = [] # build a list in forward order for efficiency
219 188 for i in range(start, end):
220 189 n = cl.node(i)
221 190 changes = cl.read(n)
222 191 hn = hex(n)
223 192
224 193 l.insert(0, {"parity": parity,
225 194 "author": changes[1],
226 195 "parent": self.siblings(cl.parents(n), cl.rev,
227 196 cl.rev(n) - 1),
228 197 "child": self.siblings(cl.children(n), cl.rev,
229 198 cl.rev(n) + 1),
230 199 "changelogtag": self.showtag("changelogtag",n),
231 200 "manifest": hex(changes[0]),
232 201 "desc": changes[4],
233 202 "date": changes[2],
234 203 "files": self.listfilediffs(changes[3], n),
235 204 "rev": i,
236 205 "node": hn})
237 206 parity = 1 - parity
238 207
239 208 for e in l:
240 209 yield e
241 210
242 211 cl = self.repo.changelog
243 212 mf = cl.read(cl.tip())[0]
244 213 count = cl.count()
245 214 start = max(0, pos - self.maxchanges + 1)
246 215 end = min(count, start + self.maxchanges)
247 216 pos = end - 1
248 217
249 218 yield self.t('changelog',
250 219 changenav=changenav,
251 220 manifest=hex(mf),
252 221 rev=pos, changesets=count, entries=changelist,
253 222 archives=self.archivelist("tip"))
254 223
255 224 def search(self, query):
256 225
257 226 def changelist(**map):
258 227 cl = self.repo.changelog
259 228 count = 0
260 229 qw = query.lower().split()
261 230
262 231 def revgen():
263 232 for i in range(cl.count() - 1, 0, -100):
264 233 l = []
265 234 for j in range(max(0, i - 100), i):
266 235 n = cl.node(j)
267 236 changes = cl.read(n)
268 237 l.append((n, j, changes))
269 238 l.reverse()
270 239 for e in l:
271 240 yield e
272 241
273 242 for n, i, changes in revgen():
274 243 miss = 0
275 244 for q in qw:
276 245 if not (q in changes[1].lower() or
277 246 q in changes[4].lower() or
278 247 q in " ".join(changes[3][:20]).lower()):
279 248 miss = 1
280 249 break
281 250 if miss:
282 251 continue
283 252
284 253 count += 1
285 254 hn = hex(n)
286 255
287 256 yield self.t('searchentry',
288 257 parity=count & 1,
289 258 author=changes[1],
290 259 parent=self.siblings(cl.parents(n), cl.rev),
291 260 child=self.siblings(cl.children(n), cl.rev),
292 261 changelogtag=self.showtag("changelogtag",n),
293 262 manifest=hex(changes[0]),
294 263 desc=changes[4],
295 264 date=changes[2],
296 265 files=self.listfilediffs(changes[3], n),
297 266 rev=i,
298 267 node=hn)
299 268
300 269 if count >= self.maxchanges:
301 270 break
302 271
303 272 cl = self.repo.changelog
304 273 mf = cl.read(cl.tip())[0]
305 274
306 275 yield self.t('search',
307 276 query=query,
308 277 manifest=hex(mf),
309 278 entries=changelist)
310 279
311 280 def changeset(self, nodeid):
312 281 cl = self.repo.changelog
313 282 n = self.repo.lookup(nodeid)
314 283 nodeid = hex(n)
315 284 changes = cl.read(n)
316 285 p1 = cl.parents(n)[0]
317 286
318 287 files = []
319 288 mf = self.repo.manifest.read(changes[0])
320 289 for f in changes[3]:
321 290 files.append(self.t("filenodelink",
322 291 filenode=hex(mf.get(f, nullid)), file=f))
323 292
324 293 def diff(**map):
325 294 yield self.diff(p1, n, None)
326 295
327 296 yield self.t('changeset',
328 297 diff=diff,
329 298 rev=cl.rev(n),
330 299 node=nodeid,
331 300 parent=self.siblings(cl.parents(n), cl.rev),
332 301 child=self.siblings(cl.children(n), cl.rev),
333 302 changesettag=self.showtag("changesettag",n),
334 303 manifest=hex(changes[0]),
335 304 author=changes[1],
336 305 desc=changes[4],
337 306 date=changes[2],
338 307 files=files,
339 308 archives=self.archivelist(nodeid))
340 309
341 310 def filelog(self, f, filenode):
342 311 cl = self.repo.changelog
343 312 fl = self.repo.file(f)
344 313 filenode = hex(fl.lookup(filenode))
345 314 count = fl.count()
346 315
347 316 def entries(**map):
348 317 l = []
349 318 parity = (count - 1) & 1
350 319
351 320 for i in range(count):
352 321 n = fl.node(i)
353 322 lr = fl.linkrev(n)
354 323 cn = cl.node(lr)
355 324 cs = cl.read(cl.node(lr))
356 325
357 326 l.insert(0, {"parity": parity,
358 327 "filenode": hex(n),
359 328 "filerev": i,
360 329 "file": f,
361 330 "node": hex(cn),
362 331 "author": cs[1],
363 332 "date": cs[2],
364 333 "rename": self.renamelink(fl, n),
365 334 "parent": self.siblings(fl.parents(n),
366 335 fl.rev, file=f),
367 336 "child": self.siblings(fl.children(n),
368 337 fl.rev, file=f),
369 338 "desc": cs[4]})
370 339 parity = 1 - parity
371 340
372 341 for e in l:
373 342 yield e
374 343
375 344 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
376 345
377 346 def filerevision(self, f, node):
378 347 fl = self.repo.file(f)
379 348 n = fl.lookup(node)
380 349 node = hex(n)
381 350 text = fl.read(n)
382 351 changerev = fl.linkrev(n)
383 352 cl = self.repo.changelog
384 353 cn = cl.node(changerev)
385 354 cs = cl.read(cn)
386 355 mfn = cs[0]
387 356
388 357 mt = mimetypes.guess_type(f)[0]
389 358 rawtext = text
390 359 if util.binary(text):
391 360 mt = mt or 'application/octet-stream'
392 361 text = "(binary:%s)" % mt
393 362 mt = mt or 'text/plain'
394 363
395 364 def lines():
396 365 for l, t in enumerate(text.splitlines(1)):
397 366 yield {"line": t,
398 367 "linenumber": "% 6d" % (l + 1),
399 368 "parity": l & 1}
400 369
401 370 yield self.t("filerevision",
402 371 file=f,
403 372 filenode=node,
404 path=up(f),
373 path=_up(f),
405 374 text=lines(),
406 375 raw=rawtext,
407 376 mimetype=mt,
408 377 rev=changerev,
409 378 node=hex(cn),
410 379 manifest=hex(mfn),
411 380 author=cs[1],
412 381 date=cs[2],
413 382 parent=self.siblings(fl.parents(n), fl.rev, file=f),
414 383 child=self.siblings(fl.children(n), fl.rev, file=f),
415 384 rename=self.renamelink(fl, n),
416 385 permissions=self.repo.manifest.readflags(mfn)[f])
417 386
418 387 def fileannotate(self, f, node):
419 388 bcache = {}
420 389 ncache = {}
421 390 fl = self.repo.file(f)
422 391 n = fl.lookup(node)
423 392 node = hex(n)
424 393 changerev = fl.linkrev(n)
425 394
426 395 cl = self.repo.changelog
427 396 cn = cl.node(changerev)
428 397 cs = cl.read(cn)
429 398 mfn = cs[0]
430 399
431 400 def annotate(**map):
432 401 parity = 1
433 402 last = None
434 403 for r, l in fl.annotate(n):
435 404 try:
436 405 cnode = ncache[r]
437 406 except KeyError:
438 407 cnode = ncache[r] = self.repo.changelog.node(r)
439 408
440 409 try:
441 410 name = bcache[r]
442 411 except KeyError:
443 412 cl = self.repo.changelog.read(cnode)
444 413 bcache[r] = name = self.repo.ui.shortuser(cl[1])
445 414
446 415 if last != cnode:
447 416 parity = 1 - parity
448 417 last = cnode
449 418
450 419 yield {"parity": parity,
451 420 "node": hex(cnode),
452 421 "rev": r,
453 422 "author": name,
454 423 "file": f,
455 424 "line": l}
456 425
457 426 yield self.t("fileannotate",
458 427 file=f,
459 428 filenode=node,
460 429 annotate=annotate,
461 path=up(f),
430 path=_up(f),
462 431 rev=changerev,
463 432 node=hex(cn),
464 433 manifest=hex(mfn),
465 434 author=cs[1],
466 435 date=cs[2],
467 436 rename=self.renamelink(fl, n),
468 437 parent=self.siblings(fl.parents(n), fl.rev, file=f),
469 438 child=self.siblings(fl.children(n), fl.rev, file=f),
470 439 permissions=self.repo.manifest.readflags(mfn)[f])
471 440
472 441 def manifest(self, mnode, path):
473 442 man = self.repo.manifest
474 443 mn = man.lookup(mnode)
475 444 mnode = hex(mn)
476 445 mf = man.read(mn)
477 446 rev = man.rev(mn)
478 447 changerev = man.linkrev(mn)
479 448 node = self.repo.changelog.node(changerev)
480 449 mff = man.readflags(mn)
481 450
482 451 files = {}
483 452
484 453 p = path[1:]
485 454 if p and p[-1] != "/":
486 455 p += "/"
487 456 l = len(p)
488 457
489 458 for f,n in mf.items():
490 459 if f[:l] != p:
491 460 continue
492 461 remain = f[l:]
493 462 if "/" in remain:
494 463 short = remain[:remain.find("/") + 1] # bleah
495 464 files[short] = (f, None)
496 465 else:
497 466 short = os.path.basename(remain)
498 467 files[short] = (f, n)
499 468
500 469 def filelist(**map):
501 470 parity = 0
502 471 fl = files.keys()
503 472 fl.sort()
504 473 for f in fl:
505 474 full, fnode = files[f]
506 475 if not fnode:
507 476 continue
508 477
509 478 yield {"file": full,
510 479 "manifest": mnode,
511 480 "filenode": hex(fnode),
512 481 "parity": parity,
513 482 "basename": f,
514 483 "permissions": mff[full]}
515 484 parity = 1 - parity
516 485
517 486 def dirlist(**map):
518 487 parity = 0
519 488 fl = files.keys()
520 489 fl.sort()
521 490 for f in fl:
522 491 full, fnode = files[f]
523 492 if fnode:
524 493 continue
525 494
526 495 yield {"parity": parity,
527 496 "path": os.path.join(path, f),
528 497 "manifest": mnode,
529 498 "basename": f[:-1]}
530 499 parity = 1 - parity
531 500
532 501 yield self.t("manifest",
533 502 manifest=mnode,
534 503 rev=rev,
535 504 node=hex(node),
536 505 path=path,
537 up=up(path),
506 up=_up(path),
538 507 fentries=filelist,
539 508 dentries=dirlist,
540 509 archives=self.archivelist(hex(node)))
541 510
542 511 def tags(self):
543 512 cl = self.repo.changelog
544 513 mf = cl.read(cl.tip())[0]
545 514
546 515 i = self.repo.tagslist()
547 516 i.reverse()
548 517
549 518 def entries(notip=False, **map):
550 519 parity = 0
551 520 for k,n in i:
552 521 if notip and k == "tip": continue
553 522 yield {"parity": parity,
554 523 "tag": k,
555 524 "tagmanifest": hex(cl.read(n)[0]),
556 525 "date": cl.read(n)[2],
557 526 "node": hex(n)}
558 527 parity = 1 - parity
559 528
560 529 yield self.t("tags",
561 530 manifest=hex(mf),
562 531 entries=lambda **x: entries(False, **x),
563 532 entriesnotip=lambda **x: entries(True, **x))
564 533
565 534 def summary(self):
566 535 cl = self.repo.changelog
567 536 mf = cl.read(cl.tip())[0]
568 537
569 538 i = self.repo.tagslist()
570 539 i.reverse()
571 540
572 541 def tagentries(**map):
573 542 parity = 0
574 543 count = 0
575 544 for k,n in i:
576 545 if k == "tip": # skip tip
577 546 continue;
578 547
579 548 count += 1
580 549 if count > 10: # limit to 10 tags
581 550 break;
582 551
583 552 c = cl.read(n)
584 553 m = c[0]
585 554 t = c[2]
586 555
587 556 yield self.t("tagentry",
588 557 parity = parity,
589 558 tag = k,
590 559 node = hex(n),
591 560 date = t,
592 561 tagmanifest = hex(m))
593 562 parity = 1 - parity
594 563
595 564 def changelist(**map):
596 565 parity = 0
597 566 cl = self.repo.changelog
598 567 l = [] # build a list in forward order for efficiency
599 568 for i in range(start, end):
600 569 n = cl.node(i)
601 570 changes = cl.read(n)
602 571 hn = hex(n)
603 572 t = changes[2]
604 573
605 574 l.insert(0, self.t(
606 575 'shortlogentry',
607 576 parity = parity,
608 577 author = changes[1],
609 578 manifest = hex(changes[0]),
610 579 desc = changes[4],
611 580 date = t,
612 581 rev = i,
613 582 node = hn))
614 583 parity = 1 - parity
615 584
616 585 yield l
617 586
618 587 cl = self.repo.changelog
619 588 mf = cl.read(cl.tip())[0]
620 589 count = cl.count()
621 590 start = max(0, count - self.maxchanges)
622 591 end = min(count, start + self.maxchanges)
623 592 pos = end - 1
624 593
625 594 yield self.t("summary",
626 595 desc = self.repo.ui.config("web", "description", "unknown"),
627 596 owner = (self.repo.ui.config("ui", "username") or # preferred
628 597 self.repo.ui.config("web", "contact") or # deprecated
629 598 self.repo.ui.config("web", "author", "unknown")), # also
630 599 lastchange = (0, 0), # FIXME
631 600 manifest = hex(mf),
632 601 tags = tagentries,
633 602 shortlog = changelist)
634 603
635 604 def filediff(self, file, changeset):
636 605 cl = self.repo.changelog
637 606 n = self.repo.lookup(changeset)
638 607 changeset = hex(n)
639 608 p1 = cl.parents(n)[0]
640 609 cs = cl.read(n)
641 610 mf = self.repo.manifest.read(cs[0])
642 611
643 612 def diff(**map):
644 613 yield self.diff(p1, n, [file])
645 614
646 615 yield self.t("filediff",
647 616 file=file,
648 617 filenode=hex(mf.get(file, nullid)),
649 618 node=changeset,
650 619 rev=self.repo.changelog.rev(n),
651 620 parent=self.siblings(cl.parents(n), cl.rev),
652 621 child=self.siblings(cl.children(n), cl.rev),
653 622 diff=diff)
654 623
655 624 archive_specs = {
656 625 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
657 626 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
658 627 'zip': ('application/zip', 'zip', '.zip', None),
659 628 }
660 629
661 630 def archive(self, req, cnode, type):
662 631 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
663 632 name = "%s-%s" % (reponame, short(cnode))
664 633 mimetype, artype, extension, encoding = self.archive_specs[type]
665 634 headers = [('Content-type', mimetype),
666 635 ('Content-disposition', 'attachment; filename=%s%s' %
667 636 (name, extension))]
668 637 if encoding:
669 638 headers.append(('Content-encoding', encoding))
670 639 req.header(headers)
671 640 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
672 641
673 642 # add tags to things
674 643 # tags -> list of changesets corresponding to tags
675 644 # find tag, changeset, file
676 645
677 646 def run(self, req=hgrequest()):
678 647 def clean(path):
679 648 p = util.normpath(path)
680 649 if p[:2] == "..":
681 650 raise "suspicious path"
682 651 return p
683 652
684 653 def header(**map):
685 654 yield self.t("header", **map)
686 655
687 656 def footer(**map):
688 657 yield self.t("footer",
689 658 motd=self.repo.ui.config("web", "motd", ""),
690 659 **map)
691 660
692 661 def expand_form(form):
693 662 shortcuts = {
694 663 'cl': [('cmd', ['changelog']), ('rev', None)],
695 664 'cs': [('cmd', ['changeset']), ('node', None)],
696 665 'f': [('cmd', ['file']), ('filenode', None)],
697 666 'fl': [('cmd', ['filelog']), ('filenode', None)],
698 667 'fd': [('cmd', ['filediff']), ('node', None)],
699 668 'fa': [('cmd', ['annotate']), ('filenode', None)],
700 669 'mf': [('cmd', ['manifest']), ('manifest', None)],
701 670 'ca': [('cmd', ['archive']), ('node', None)],
702 671 'tags': [('cmd', ['tags'])],
703 672 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
704 673 'static': [('cmd', ['static']), ('file', None)]
705 674 }
706 675
707 676 for k in shortcuts.iterkeys():
708 677 if form.has_key(k):
709 678 for name, value in shortcuts[k]:
710 679 if value is None:
711 680 value = form[k]
712 681 form[name] = value
713 682 del form[k]
714 683
715 684 self.refresh()
716 685
717 686 expand_form(req.form)
718 687
719 688 t = self.repo.ui.config("web", "templates", templater.templatepath())
720 689 static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
721 690 m = os.path.join(t, "map")
722 691 style = self.repo.ui.config("web", "style", "")
723 692 if req.form.has_key('style'):
724 693 style = req.form['style'][0]
725 694 if style:
726 695 b = os.path.basename("map-" + style)
727 696 p = os.path.join(t, b)
728 697 if os.path.isfile(p):
729 698 m = p
730 699
731 700 port = req.env["SERVER_PORT"]
732 701 port = port != "80" and (":" + port) or ""
733 702 uri = req.env["REQUEST_URI"]
734 703 if "?" in uri:
735 704 uri = uri.split("?")[0]
736 705 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
737 706 if not self.reponame:
738 707 self.reponame = (self.repo.ui.config("web", "name")
739 708 or uri.strip('/') or self.repo.root)
740 709
741 710 self.t = templater.templater(m, templater.common_filters,
742 711 defaults={"url": url,
743 712 "repo": self.reponame,
744 713 "header": header,
745 714 "footer": footer,
746 715 })
747 716
748 717 if not req.form.has_key('cmd'):
749 718 req.form['cmd'] = [self.t.cache['default'],]
750 719
751 720 cmd = req.form['cmd'][0]
752 721 if cmd == 'changelog':
753 722 hi = self.repo.changelog.count() - 1
754 723 if req.form.has_key('rev'):
755 724 hi = req.form['rev'][0]
756 725 try:
757 726 hi = self.repo.changelog.rev(self.repo.lookup(hi))
758 727 except hg.RepoError:
759 728 req.write(self.search(hi)) # XXX redirect to 404 page?
760 729 return
761 730
762 731 req.write(self.changelog(hi))
763 732
764 733 elif cmd == 'changeset':
765 734 req.write(self.changeset(req.form['node'][0]))
766 735
767 736 elif cmd == 'manifest':
768 737 req.write(self.manifest(req.form['manifest'][0],
769 738 clean(req.form['path'][0])))
770 739
771 740 elif cmd == 'tags':
772 741 req.write(self.tags())
773 742
774 743 elif cmd == 'summary':
775 744 req.write(self.summary())
776 745
777 746 elif cmd == 'filediff':
778 747 req.write(self.filediff(clean(req.form['file'][0]),
779 748 req.form['node'][0]))
780 749
781 750 elif cmd == 'file':
782 751 req.write(self.filerevision(clean(req.form['file'][0]),
783 752 req.form['filenode'][0]))
784 753
785 754 elif cmd == 'annotate':
786 755 req.write(self.fileannotate(clean(req.form['file'][0]),
787 756 req.form['filenode'][0]))
788 757
789 758 elif cmd == 'filelog':
790 759 req.write(self.filelog(clean(req.form['file'][0]),
791 760 req.form['filenode'][0]))
792 761
793 762 elif cmd == 'heads':
794 763 req.httphdr("application/mercurial-0.1")
795 764 h = self.repo.heads()
796 765 req.write(" ".join(map(hex, h)) + "\n")
797 766
798 767 elif cmd == 'branches':
799 768 req.httphdr("application/mercurial-0.1")
800 769 nodes = []
801 770 if req.form.has_key('nodes'):
802 771 nodes = map(bin, req.form['nodes'][0].split(" "))
803 772 for b in self.repo.branches(nodes):
804 773 req.write(" ".join(map(hex, b)) + "\n")
805 774
806 775 elif cmd == 'between':
807 776 req.httphdr("application/mercurial-0.1")
808 777 nodes = []
809 778 if req.form.has_key('pairs'):
810 779 pairs = [map(bin, p.split("-"))
811 780 for p in req.form['pairs'][0].split(" ")]
812 781 for b in self.repo.between(pairs):
813 782 req.write(" ".join(map(hex, b)) + "\n")
814 783
815 784 elif cmd == 'changegroup':
816 785 req.httphdr("application/mercurial-0.1")
817 786 nodes = []
818 787 if not self.allowpull:
819 788 return
820 789
821 790 if req.form.has_key('roots'):
822 791 nodes = map(bin, req.form['roots'][0].split(" "))
823 792
824 793 z = zlib.compressobj()
825 794 f = self.repo.changegroup(nodes, 'serve')
826 795 while 1:
827 796 chunk = f.read(4096)
828 797 if not chunk:
829 798 break
830 799 req.write(z.compress(chunk))
831 800
832 801 req.write(z.flush())
833 802
834 803 elif cmd == 'archive':
835 804 changeset = self.repo.lookup(req.form['node'][0])
836 805 type = req.form['type'][0]
837 806 if (type in self.archives and
838 807 self.repo.ui.configbool("web", "allow" + type, False)):
839 808 self.archive(req, changeset, type)
840 809 return
841 810
842 811 req.write(self.t("error"))
843 812
844 813 elif cmd == 'static':
845 814 fname = req.form['file'][0]
846 815 req.write(staticfile(static, fname)
847 816 or self.t("error", error="%r not found" % fname))
848 817
849 818 else:
850 819 req.write(self.t("error"))
851
852 # This is a stopgap
853 class hgwebdir(object):
854 def __init__(self, config):
855 def cleannames(items):
856 return [(name.strip(os.sep), path) for name, path in items]
857
858 self.motd = ""
859 self.repos_sorted = ('name', False)
860 if isinstance(config, (list, tuple)):
861 self.repos = cleannames(config)
862 self.repos_sorted = ('', False)
863 elif isinstance(config, dict):
864 self.repos = cleannames(config.items())
865 self.repos.sort()
866 else:
867 cp = ConfigParser.SafeConfigParser()
868 cp.read(config)
869 self.repos = []
870 if cp.has_section('web') and cp.has_option('web', 'motd'):
871 self.motd = cp.get('web', 'motd')
872 if cp.has_section('paths'):
873 self.repos.extend(cleannames(cp.items('paths')))
874 if cp.has_section('collections'):
875 for prefix, root in cp.items('collections'):
876 for path in util.walkrepos(root):
877 repo = os.path.normpath(path)
878 name = repo
879 if name.startswith(prefix):
880 name = name[len(prefix):]
881 self.repos.append((name.lstrip(os.sep), repo))
882 self.repos.sort()
883
884 def run(self, req=hgrequest()):
885 def header(**map):
886 yield tmpl("header", **map)
887
888 def footer(**map):
889 yield tmpl("footer", motd=self.motd, **map)
890
891 m = os.path.join(templater.templatepath(), "map")
892 tmpl = templater.templater(m, templater.common_filters,
893 defaults={"header": header,
894 "footer": footer})
895
896 def archivelist(ui, nodeid, url):
897 for i in ['zip', 'gz', 'bz2']:
898 if ui.configbool("web", "allow" + i, False):
899 yield {"type" : i, "node": nodeid, "url": url}
900
901 def entries(sortcolumn="", descending=False, **map):
902 rows = []
903 parity = 0
904 for name, path in self.repos:
905 u = ui.ui()
906 try:
907 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
908 except IOError:
909 pass
910 get = u.config
911
912 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
913 .replace("//", "/"))
914
915 # update time with local timezone
916 try:
917 d = (get_mtime(path), util.makedate()[1])
918 except OSError:
919 continue
920
921 contact = (get("ui", "username") or # preferred
922 get("web", "contact") or # deprecated
923 get("web", "author", "")) # also
924 description = get("web", "description", "")
925 name = get("web", "name", name)
926 row = dict(contact=contact or "unknown",
927 contact_sort=contact.upper() or "unknown",
928 name=name,
929 name_sort=name,
930 url=url,
931 description=description or "unknown",
932 description_sort=description.upper() or "unknown",
933 lastchange=d,
934 lastchange_sort=d[1]-d[0],
935 archives=archivelist(u, "tip", url))
936 if (not sortcolumn
937 or (sortcolumn, descending) == self.repos_sorted):
938 # fast path for unsorted output
939 row['parity'] = parity
940 parity = 1 - parity
941 yield row
942 else:
943 rows.append((row["%s_sort" % sortcolumn], row))
944 if rows:
945 rows.sort()
946 if descending:
947 rows.reverse()
948 for key, row in rows:
949 row['parity'] = parity
950 parity = 1 - parity
951 yield row
952
953 virtual = req.env.get("PATH_INFO", "").strip('/')
954 if virtual:
955 real = dict(self.repos).get(virtual)
956 if real:
957 try:
958 hgweb(real).run(req)
959 except IOError, inst:
960 req.write(tmpl("error", error=inst.strerror))
961 except hg.RepoError, inst:
962 req.write(tmpl("error", error=str(inst)))
963 else:
964 req.write(tmpl("notfound", repo=virtual))
965 else:
966 if req.form.has_key('static'):
967 static = os.path.join(templater.templatepath(), "static")
968 fname = req.form['static'][0]
969 req.write(staticfile(static, fname)
970 or tmpl("error", error="%r not found" % fname))
971 else:
972 sortable = ["name", "description", "contact", "lastchange"]
973 sortcolumn, descending = self.repos_sorted
974 if req.form.has_key('sort'):
975 sortcolumn = req.form['sort'][0]
976 descending = sortcolumn.startswith('-')
977 if descending:
978 sortcolumn = sortcolumn[1:]
979 if sortcolumn not in sortable:
980 sortcolumn = ""
981
982 sort = [("sort_%s" % column,
983 "%s%s" % ((not descending and column == sortcolumn)
984 and "-" or "", column))
985 for column in sortable]
986 req.write(tmpl("index", entries=entries,
987 sortcolumn=sortcolumn, descending=descending,
988 **dict(sort)))
This diff has been collapsed as it changes many lines, (842 lines changed) Show them Hide them
@@ -1,988 +1,152 b''
1 1 # hgweb.py - web interface to a mercurial repository
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 import os, cgi, sys
10 import mimetypes
9 import os
11 10 from mercurial.demandload import demandload
12 demandload(globals(), "time re socket zlib errno ConfigParser tempfile")
13 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
11 demandload(globals(), "ConfigParser")
12 demandload(globals(), "mercurial:ui,hg,util,templater")
14 13 demandload(globals(), "mercurial.hgweb.request:hgrequest")
15 demandload(globals(), "mercurial.hgweb.server:create_server")
16 from mercurial.node import *
17 14 from mercurial.i18n import gettext as _
18 15
19 def up(p):
20 if p[0] != "/":
21 p = "/" + p
22 if p[-1] == "/":
23 p = p[:-1]
24 up = os.path.dirname(p)
25 if up == "/":
26 return "/"
27 return up + "/"
28
29 def get_mtime(repo_path):
30 hg_path = os.path.join(repo_path, ".hg")
31 cl_path = os.path.join(hg_path, "00changelog.i")
32 if os.path.exists(os.path.join(cl_path)):
33 return os.stat(cl_path).st_mtime
34 else:
35 return os.stat(hg_path).st_mtime
36
37 def staticfile(directory, fname):
38 """return a file inside directory with guessed content-type header
39
40 fname always uses '/' as directory separator and isn't allowed to
41 contain unusual path components.
42 Content-type is guessed using the mimetypes module.
43 Return an empty string if fname is illegal or file not found.
44
45 """
46 parts = fname.split('/')
47 path = directory
48 for part in parts:
49 if (part in ('', os.curdir, os.pardir) or
50 os.sep in part or os.altsep is not None and os.altsep in part):
51 return ""
52 path = os.path.join(path, part)
53 try:
54 os.stat(path)
55 ct = mimetypes.guess_type(path)[0] or "text/plain"
56 return "Content-type: %s\n\n%s" % (ct, file(path).read())
57 except (TypeError, OSError):
58 # illegal fname or unreadable file
59 return ""
60
61 class hgweb(object):
62 def __init__(self, repo, name=None):
63 if type(repo) == type(""):
64 self.repo = hg.repository(ui.ui(), repo)
65 else:
66 self.repo = repo
67
68 self.mtime = -1
69 self.reponame = name
70 self.archives = 'zip', 'gz', 'bz2'
71
72 def refresh(self):
73 mtime = get_mtime(self.repo.root)
74 if mtime != self.mtime:
75 self.mtime = mtime
76 self.repo = hg.repository(self.repo.ui, self.repo.root)
77 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
78 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
79 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
80
81 def archivelist(self, nodeid):
82 for i in self.archives:
83 if self.repo.ui.configbool("web", "allow" + i, False):
84 yield {"type" : i, "node" : nodeid, "url": ""}
85
86 def listfiles(self, files, mf):
87 for f in files[:self.maxfiles]:
88 yield self.t("filenodelink", node=hex(mf[f]), file=f)
89 if len(files) > self.maxfiles:
90 yield self.t("fileellipses")
91
92 def listfilediffs(self, files, changeset):
93 for f in files[:self.maxfiles]:
94 yield self.t("filedifflink", node=hex(changeset), file=f)
95 if len(files) > self.maxfiles:
96 yield self.t("fileellipses")
97
98 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
99 if not rev:
100 rev = lambda x: ""
101 siblings = [s for s in siblings if s != nullid]
102 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
103 return
104 for s in siblings:
105 yield dict(node=hex(s), rev=rev(s), **args)
106
107 def renamelink(self, fl, node):
108 r = fl.renamed(node)
109 if r:
110 return [dict(file=r[0], node=hex(r[1]))]
111 return []
112
113 def showtag(self, t1, node=nullid, **args):
114 for t in self.repo.nodetags(node):
115 yield self.t(t1, tag=t, **args)
116
117 def diff(self, node1, node2, files):
118 def filterfiles(filters, files):
119 l = [x for x in files if x in filters]
120
121 for t in filters:
122 if t and t[-1] != os.sep:
123 t += os.sep
124 l += [x for x in files if x.startswith(t)]
125 return l
126
127 parity = [0]
128 def diffblock(diff, f, fn):
129 yield self.t("diffblock",
130 lines=prettyprintlines(diff),
131 parity=parity[0],
132 file=f,
133 filenode=hex(fn or nullid))
134 parity[0] = 1 - parity[0]
135
136 def prettyprintlines(diff):
137 for l in diff.splitlines(1):
138 if l.startswith('+'):
139 yield self.t("difflineplus", line=l)
140 elif l.startswith('-'):
141 yield self.t("difflineminus", line=l)
142 elif l.startswith('@'):
143 yield self.t("difflineat", line=l)
144 else:
145 yield self.t("diffline", line=l)
146
147 r = self.repo
148 cl = r.changelog
149 mf = r.manifest
150 change1 = cl.read(node1)
151 change2 = cl.read(node2)
152 mmap1 = mf.read(change1[0])
153 mmap2 = mf.read(change2[0])
154 date1 = util.datestr(change1[2])
155 date2 = util.datestr(change2[2])
156
157 modified, added, removed, deleted, unknown = r.changes(node1, node2)
158 if files:
159 modified, added, removed = map(lambda x: filterfiles(files, x),
160 (modified, added, removed))
161
162 diffopts = self.repo.ui.diffopts()
163 showfunc = diffopts['showfunc']
164 ignorews = diffopts['ignorews']
165 for f in modified:
166 to = r.file(f).read(mmap1[f])
167 tn = r.file(f).read(mmap2[f])
168 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
169 showfunc=showfunc, ignorews=ignorews), f, tn)
170 for f in added:
171 to = None
172 tn = r.file(f).read(mmap2[f])
173 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
174 showfunc=showfunc, ignorews=ignorews), f, tn)
175 for f in removed:
176 to = r.file(f).read(mmap1[f])
177 tn = None
178 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
179 showfunc=showfunc, ignorews=ignorews), f, tn)
180
181 def changelog(self, pos):
182 def changenav(**map):
183 def seq(factor, maxchanges=None):
184 if maxchanges:
185 yield maxchanges
186 if maxchanges >= 20 and maxchanges <= 40:
187 yield 50
188 else:
189 yield 1 * factor
190 yield 3 * factor
191 for f in seq(factor * 10):
192 yield f
193
194 l = []
195 last = 0
196 for f in seq(1, self.maxchanges):
197 if f < self.maxchanges or f <= last:
198 continue
199 if f > count:
200 break
201 last = f
202 r = "%d" % f
203 if pos + f < count:
204 l.append(("+" + r, pos + f))
205 if pos - f >= 0:
206 l.insert(0, ("-" + r, pos - f))
207
208 yield {"rev": 0, "label": "(0)"}
209
210 for label, rev in l:
211 yield {"label": label, "rev": rev}
212
213 yield {"label": "tip", "rev": "tip"}
214
215 def changelist(**map):
216 parity = (start - end) & 1
217 cl = self.repo.changelog
218 l = [] # build a list in forward order for efficiency
219 for i in range(start, end):
220 n = cl.node(i)
221 changes = cl.read(n)
222 hn = hex(n)
223
224 l.insert(0, {"parity": parity,
225 "author": changes[1],
226 "parent": self.siblings(cl.parents(n), cl.rev,
227 cl.rev(n) - 1),
228 "child": self.siblings(cl.children(n), cl.rev,
229 cl.rev(n) + 1),
230 "changelogtag": self.showtag("changelogtag",n),
231 "manifest": hex(changes[0]),
232 "desc": changes[4],
233 "date": changes[2],
234 "files": self.listfilediffs(changes[3], n),
235 "rev": i,
236 "node": hn})
237 parity = 1 - parity
238
239 for e in l:
240 yield e
241
242 cl = self.repo.changelog
243 mf = cl.read(cl.tip())[0]
244 count = cl.count()
245 start = max(0, pos - self.maxchanges + 1)
246 end = min(count, start + self.maxchanges)
247 pos = end - 1
248
249 yield self.t('changelog',
250 changenav=changenav,
251 manifest=hex(mf),
252 rev=pos, changesets=count, entries=changelist,
253 archives=self.archivelist("tip"))
254
255 def search(self, query):
256
257 def changelist(**map):
258 cl = self.repo.changelog
259 count = 0
260 qw = query.lower().split()
261
262 def revgen():
263 for i in range(cl.count() - 1, 0, -100):
264 l = []
265 for j in range(max(0, i - 100), i):
266 n = cl.node(j)
267 changes = cl.read(n)
268 l.append((n, j, changes))
269 l.reverse()
270 for e in l:
271 yield e
272
273 for n, i, changes in revgen():
274 miss = 0
275 for q in qw:
276 if not (q in changes[1].lower() or
277 q in changes[4].lower() or
278 q in " ".join(changes[3][:20]).lower()):
279 miss = 1
280 break
281 if miss:
282 continue
283
284 count += 1
285 hn = hex(n)
286
287 yield self.t('searchentry',
288 parity=count & 1,
289 author=changes[1],
290 parent=self.siblings(cl.parents(n), cl.rev),
291 child=self.siblings(cl.children(n), cl.rev),
292 changelogtag=self.showtag("changelogtag",n),
293 manifest=hex(changes[0]),
294 desc=changes[4],
295 date=changes[2],
296 files=self.listfilediffs(changes[3], n),
297 rev=i,
298 node=hn)
299
300 if count >= self.maxchanges:
301 break
302
303 cl = self.repo.changelog
304 mf = cl.read(cl.tip())[0]
305
306 yield self.t('search',
307 query=query,
308 manifest=hex(mf),
309 entries=changelist)
310
311 def changeset(self, nodeid):
312 cl = self.repo.changelog
313 n = self.repo.lookup(nodeid)
314 nodeid = hex(n)
315 changes = cl.read(n)
316 p1 = cl.parents(n)[0]
317
318 files = []
319 mf = self.repo.manifest.read(changes[0])
320 for f in changes[3]:
321 files.append(self.t("filenodelink",
322 filenode=hex(mf.get(f, nullid)), file=f))
323
324 def diff(**map):
325 yield self.diff(p1, n, None)
326
327 yield self.t('changeset',
328 diff=diff,
329 rev=cl.rev(n),
330 node=nodeid,
331 parent=self.siblings(cl.parents(n), cl.rev),
332 child=self.siblings(cl.children(n), cl.rev),
333 changesettag=self.showtag("changesettag",n),
334 manifest=hex(changes[0]),
335 author=changes[1],
336 desc=changes[4],
337 date=changes[2],
338 files=files,
339 archives=self.archivelist(nodeid))
340
341 def filelog(self, f, filenode):
342 cl = self.repo.changelog
343 fl = self.repo.file(f)
344 filenode = hex(fl.lookup(filenode))
345 count = fl.count()
346
347 def entries(**map):
348 l = []
349 parity = (count - 1) & 1
350
351 for i in range(count):
352 n = fl.node(i)
353 lr = fl.linkrev(n)
354 cn = cl.node(lr)
355 cs = cl.read(cl.node(lr))
356
357 l.insert(0, {"parity": parity,
358 "filenode": hex(n),
359 "filerev": i,
360 "file": f,
361 "node": hex(cn),
362 "author": cs[1],
363 "date": cs[2],
364 "rename": self.renamelink(fl, n),
365 "parent": self.siblings(fl.parents(n),
366 fl.rev, file=f),
367 "child": self.siblings(fl.children(n),
368 fl.rev, file=f),
369 "desc": cs[4]})
370 parity = 1 - parity
371
372 for e in l:
373 yield e
374
375 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
376
377 def filerevision(self, f, node):
378 fl = self.repo.file(f)
379 n = fl.lookup(node)
380 node = hex(n)
381 text = fl.read(n)
382 changerev = fl.linkrev(n)
383 cl = self.repo.changelog
384 cn = cl.node(changerev)
385 cs = cl.read(cn)
386 mfn = cs[0]
387
388 mt = mimetypes.guess_type(f)[0]
389 rawtext = text
390 if util.binary(text):
391 mt = mt or 'application/octet-stream'
392 text = "(binary:%s)" % mt
393 mt = mt or 'text/plain'
394
395 def lines():
396 for l, t in enumerate(text.splitlines(1)):
397 yield {"line": t,
398 "linenumber": "% 6d" % (l + 1),
399 "parity": l & 1}
400
401 yield self.t("filerevision",
402 file=f,
403 filenode=node,
404 path=up(f),
405 text=lines(),
406 raw=rawtext,
407 mimetype=mt,
408 rev=changerev,
409 node=hex(cn),
410 manifest=hex(mfn),
411 author=cs[1],
412 date=cs[2],
413 parent=self.siblings(fl.parents(n), fl.rev, file=f),
414 child=self.siblings(fl.children(n), fl.rev, file=f),
415 rename=self.renamelink(fl, n),
416 permissions=self.repo.manifest.readflags(mfn)[f])
417
418 def fileannotate(self, f, node):
419 bcache = {}
420 ncache = {}
421 fl = self.repo.file(f)
422 n = fl.lookup(node)
423 node = hex(n)
424 changerev = fl.linkrev(n)
425
426 cl = self.repo.changelog
427 cn = cl.node(changerev)
428 cs = cl.read(cn)
429 mfn = cs[0]
430
431 def annotate(**map):
432 parity = 1
433 last = None
434 for r, l in fl.annotate(n):
435 try:
436 cnode = ncache[r]
437 except KeyError:
438 cnode = ncache[r] = self.repo.changelog.node(r)
439
440 try:
441 name = bcache[r]
442 except KeyError:
443 cl = self.repo.changelog.read(cnode)
444 bcache[r] = name = self.repo.ui.shortuser(cl[1])
445
446 if last != cnode:
447 parity = 1 - parity
448 last = cnode
449
450 yield {"parity": parity,
451 "node": hex(cnode),
452 "rev": r,
453 "author": name,
454 "file": f,
455 "line": l}
456
457 yield self.t("fileannotate",
458 file=f,
459 filenode=node,
460 annotate=annotate,
461 path=up(f),
462 rev=changerev,
463 node=hex(cn),
464 manifest=hex(mfn),
465 author=cs[1],
466 date=cs[2],
467 rename=self.renamelink(fl, n),
468 parent=self.siblings(fl.parents(n), fl.rev, file=f),
469 child=self.siblings(fl.children(n), fl.rev, file=f),
470 permissions=self.repo.manifest.readflags(mfn)[f])
471
472 def manifest(self, mnode, path):
473 man = self.repo.manifest
474 mn = man.lookup(mnode)
475 mnode = hex(mn)
476 mf = man.read(mn)
477 rev = man.rev(mn)
478 changerev = man.linkrev(mn)
479 node = self.repo.changelog.node(changerev)
480 mff = man.readflags(mn)
481
482 files = {}
483
484 p = path[1:]
485 if p and p[-1] != "/":
486 p += "/"
487 l = len(p)
488
489 for f,n in mf.items():
490 if f[:l] != p:
491 continue
492 remain = f[l:]
493 if "/" in remain:
494 short = remain[:remain.find("/") + 1] # bleah
495 files[short] = (f, None)
496 else:
497 short = os.path.basename(remain)
498 files[short] = (f, n)
499
500 def filelist(**map):
501 parity = 0
502 fl = files.keys()
503 fl.sort()
504 for f in fl:
505 full, fnode = files[f]
506 if not fnode:
507 continue
508
509 yield {"file": full,
510 "manifest": mnode,
511 "filenode": hex(fnode),
512 "parity": parity,
513 "basename": f,
514 "permissions": mff[full]}
515 parity = 1 - parity
516
517 def dirlist(**map):
518 parity = 0
519 fl = files.keys()
520 fl.sort()
521 for f in fl:
522 full, fnode = files[f]
523 if fnode:
524 continue
525
526 yield {"parity": parity,
527 "path": os.path.join(path, f),
528 "manifest": mnode,
529 "basename": f[:-1]}
530 parity = 1 - parity
531
532 yield self.t("manifest",
533 manifest=mnode,
534 rev=rev,
535 node=hex(node),
536 path=path,
537 up=up(path),
538 fentries=filelist,
539 dentries=dirlist,
540 archives=self.archivelist(hex(node)))
541
542 def tags(self):
543 cl = self.repo.changelog
544 mf = cl.read(cl.tip())[0]
545
546 i = self.repo.tagslist()
547 i.reverse()
548
549 def entries(notip=False, **map):
550 parity = 0
551 for k,n in i:
552 if notip and k == "tip": continue
553 yield {"parity": parity,
554 "tag": k,
555 "tagmanifest": hex(cl.read(n)[0]),
556 "date": cl.read(n)[2],
557 "node": hex(n)}
558 parity = 1 - parity
559
560 yield self.t("tags",
561 manifest=hex(mf),
562 entries=lambda **x: entries(False, **x),
563 entriesnotip=lambda **x: entries(True, **x))
564
565 def summary(self):
566 cl = self.repo.changelog
567 mf = cl.read(cl.tip())[0]
568
569 i = self.repo.tagslist()
570 i.reverse()
571
572 def tagentries(**map):
573 parity = 0
574 count = 0
575 for k,n in i:
576 if k == "tip": # skip tip
577 continue;
578
579 count += 1
580 if count > 10: # limit to 10 tags
581 break;
582
583 c = cl.read(n)
584 m = c[0]
585 t = c[2]
586
587 yield self.t("tagentry",
588 parity = parity,
589 tag = k,
590 node = hex(n),
591 date = t,
592 tagmanifest = hex(m))
593 parity = 1 - parity
594
595 def changelist(**map):
596 parity = 0
597 cl = self.repo.changelog
598 l = [] # build a list in forward order for efficiency
599 for i in range(start, end):
600 n = cl.node(i)
601 changes = cl.read(n)
602 hn = hex(n)
603 t = changes[2]
604
605 l.insert(0, self.t(
606 'shortlogentry',
607 parity = parity,
608 author = changes[1],
609 manifest = hex(changes[0]),
610 desc = changes[4],
611 date = t,
612 rev = i,
613 node = hn))
614 parity = 1 - parity
615
616 yield l
617
618 cl = self.repo.changelog
619 mf = cl.read(cl.tip())[0]
620 count = cl.count()
621 start = max(0, count - self.maxchanges)
622 end = min(count, start + self.maxchanges)
623 pos = end - 1
624
625 yield self.t("summary",
626 desc = self.repo.ui.config("web", "description", "unknown"),
627 owner = (self.repo.ui.config("ui", "username") or # preferred
628 self.repo.ui.config("web", "contact") or # deprecated
629 self.repo.ui.config("web", "author", "unknown")), # also
630 lastchange = (0, 0), # FIXME
631 manifest = hex(mf),
632 tags = tagentries,
633 shortlog = changelist)
634
635 def filediff(self, file, changeset):
636 cl = self.repo.changelog
637 n = self.repo.lookup(changeset)
638 changeset = hex(n)
639 p1 = cl.parents(n)[0]
640 cs = cl.read(n)
641 mf = self.repo.manifest.read(cs[0])
642
643 def diff(**map):
644 yield self.diff(p1, n, [file])
645
646 yield self.t("filediff",
647 file=file,
648 filenode=hex(mf.get(file, nullid)),
649 node=changeset,
650 rev=self.repo.changelog.rev(n),
651 parent=self.siblings(cl.parents(n), cl.rev),
652 child=self.siblings(cl.children(n), cl.rev),
653 diff=diff)
654
655 archive_specs = {
656 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
657 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
658 'zip': ('application/zip', 'zip', '.zip', None),
659 }
660
661 def archive(self, req, cnode, type):
662 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
663 name = "%s-%s" % (reponame, short(cnode))
664 mimetype, artype, extension, encoding = self.archive_specs[type]
665 headers = [('Content-type', mimetype),
666 ('Content-disposition', 'attachment; filename=%s%s' %
667 (name, extension))]
668 if encoding:
669 headers.append(('Content-encoding', encoding))
670 req.header(headers)
671 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
672
673 # add tags to things
674 # tags -> list of changesets corresponding to tags
675 # find tag, changeset, file
676
677 def run(self, req=hgrequest()):
678 def clean(path):
679 p = util.normpath(path)
680 if p[:2] == "..":
681 raise "suspicious path"
682 return p
683
684 def header(**map):
685 yield self.t("header", **map)
686
687 def footer(**map):
688 yield self.t("footer",
689 motd=self.repo.ui.config("web", "motd", ""),
690 **map)
691
692 def expand_form(form):
693 shortcuts = {
694 'cl': [('cmd', ['changelog']), ('rev', None)],
695 'cs': [('cmd', ['changeset']), ('node', None)],
696 'f': [('cmd', ['file']), ('filenode', None)],
697 'fl': [('cmd', ['filelog']), ('filenode', None)],
698 'fd': [('cmd', ['filediff']), ('node', None)],
699 'fa': [('cmd', ['annotate']), ('filenode', None)],
700 'mf': [('cmd', ['manifest']), ('manifest', None)],
701 'ca': [('cmd', ['archive']), ('node', None)],
702 'tags': [('cmd', ['tags'])],
703 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
704 'static': [('cmd', ['static']), ('file', None)]
705 }
706
707 for k in shortcuts.iterkeys():
708 if form.has_key(k):
709 for name, value in shortcuts[k]:
710 if value is None:
711 value = form[k]
712 form[name] = value
713 del form[k]
714
715 self.refresh()
716
717 expand_form(req.form)
718
719 t = self.repo.ui.config("web", "templates", templater.templatepath())
720 static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
721 m = os.path.join(t, "map")
722 style = self.repo.ui.config("web", "style", "")
723 if req.form.has_key('style'):
724 style = req.form['style'][0]
725 if style:
726 b = os.path.basename("map-" + style)
727 p = os.path.join(t, b)
728 if os.path.isfile(p):
729 m = p
730
731 port = req.env["SERVER_PORT"]
732 port = port != "80" and (":" + port) or ""
733 uri = req.env["REQUEST_URI"]
734 if "?" in uri:
735 uri = uri.split("?")[0]
736 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
737 if not self.reponame:
738 self.reponame = (self.repo.ui.config("web", "name")
739 or uri.strip('/') or self.repo.root)
740
741 self.t = templater.templater(m, templater.common_filters,
742 defaults={"url": url,
743 "repo": self.reponame,
744 "header": header,
745 "footer": footer,
746 })
747
748 if not req.form.has_key('cmd'):
749 req.form['cmd'] = [self.t.cache['default'],]
750
751 cmd = req.form['cmd'][0]
752 if cmd == 'changelog':
753 hi = self.repo.changelog.count() - 1
754 if req.form.has_key('rev'):
755 hi = req.form['rev'][0]
756 try:
757 hi = self.repo.changelog.rev(self.repo.lookup(hi))
758 except hg.RepoError:
759 req.write(self.search(hi)) # XXX redirect to 404 page?
760 return
761
762 req.write(self.changelog(hi))
763
764 elif cmd == 'changeset':
765 req.write(self.changeset(req.form['node'][0]))
766
767 elif cmd == 'manifest':
768 req.write(self.manifest(req.form['manifest'][0],
769 clean(req.form['path'][0])))
770
771 elif cmd == 'tags':
772 req.write(self.tags())
773
774 elif cmd == 'summary':
775 req.write(self.summary())
776
777 elif cmd == 'filediff':
778 req.write(self.filediff(clean(req.form['file'][0]),
779 req.form['node'][0]))
780
781 elif cmd == 'file':
782 req.write(self.filerevision(clean(req.form['file'][0]),
783 req.form['filenode'][0]))
784
785 elif cmd == 'annotate':
786 req.write(self.fileannotate(clean(req.form['file'][0]),
787 req.form['filenode'][0]))
788
789 elif cmd == 'filelog':
790 req.write(self.filelog(clean(req.form['file'][0]),
791 req.form['filenode'][0]))
792
793 elif cmd == 'heads':
794 req.httphdr("application/mercurial-0.1")
795 h = self.repo.heads()
796 req.write(" ".join(map(hex, h)) + "\n")
797
798 elif cmd == 'branches':
799 req.httphdr("application/mercurial-0.1")
800 nodes = []
801 if req.form.has_key('nodes'):
802 nodes = map(bin, req.form['nodes'][0].split(" "))
803 for b in self.repo.branches(nodes):
804 req.write(" ".join(map(hex, b)) + "\n")
805
806 elif cmd == 'between':
807 req.httphdr("application/mercurial-0.1")
808 nodes = []
809 if req.form.has_key('pairs'):
810 pairs = [map(bin, p.split("-"))
811 for p in req.form['pairs'][0].split(" ")]
812 for b in self.repo.between(pairs):
813 req.write(" ".join(map(hex, b)) + "\n")
814
815 elif cmd == 'changegroup':
816 req.httphdr("application/mercurial-0.1")
817 nodes = []
818 if not self.allowpull:
819 return
820
821 if req.form.has_key('roots'):
822 nodes = map(bin, req.form['roots'][0].split(" "))
823
824 z = zlib.compressobj()
825 f = self.repo.changegroup(nodes, 'serve')
826 while 1:
827 chunk = f.read(4096)
828 if not chunk:
829 break
830 req.write(z.compress(chunk))
831
832 req.write(z.flush())
833
834 elif cmd == 'archive':
835 changeset = self.repo.lookup(req.form['node'][0])
836 type = req.form['type'][0]
837 if (type in self.archives and
838 self.repo.ui.configbool("web", "allow" + type, False)):
839 self.archive(req, changeset, type)
840 return
841
842 req.write(self.t("error"))
843
844 elif cmd == 'static':
845 fname = req.form['file'][0]
846 req.write(staticfile(static, fname)
847 or self.t("error", error="%r not found" % fname))
848
849 else:
850 req.write(self.t("error"))
851
852 16 # This is a stopgap
853 17 class hgwebdir(object):
854 18 def __init__(self, config):
855 19 def cleannames(items):
856 20 return [(name.strip(os.sep), path) for name, path in items]
857 21
858 22 self.motd = ""
859 23 self.repos_sorted = ('name', False)
860 24 if isinstance(config, (list, tuple)):
861 25 self.repos = cleannames(config)
862 26 self.repos_sorted = ('', False)
863 27 elif isinstance(config, dict):
864 28 self.repos = cleannames(config.items())
865 29 self.repos.sort()
866 30 else:
867 31 cp = ConfigParser.SafeConfigParser()
868 32 cp.read(config)
869 33 self.repos = []
870 34 if cp.has_section('web') and cp.has_option('web', 'motd'):
871 35 self.motd = cp.get('web', 'motd')
872 36 if cp.has_section('paths'):
873 37 self.repos.extend(cleannames(cp.items('paths')))
874 38 if cp.has_section('collections'):
875 39 for prefix, root in cp.items('collections'):
876 40 for path in util.walkrepos(root):
877 41 repo = os.path.normpath(path)
878 42 name = repo
879 43 if name.startswith(prefix):
880 44 name = name[len(prefix):]
881 45 self.repos.append((name.lstrip(os.sep), repo))
882 46 self.repos.sort()
883 47
884 48 def run(self, req=hgrequest()):
885 49 def header(**map):
886 50 yield tmpl("header", **map)
887 51
888 52 def footer(**map):
889 53 yield tmpl("footer", motd=self.motd, **map)
890 54
891 55 m = os.path.join(templater.templatepath(), "map")
892 56 tmpl = templater.templater(m, templater.common_filters,
893 57 defaults={"header": header,
894 58 "footer": footer})
895 59
896 60 def archivelist(ui, nodeid, url):
897 61 for i in ['zip', 'gz', 'bz2']:
898 62 if ui.configbool("web", "allow" + i, False):
899 63 yield {"type" : i, "node": nodeid, "url": url}
900 64
901 65 def entries(sortcolumn="", descending=False, **map):
902 66 rows = []
903 67 parity = 0
904 68 for name, path in self.repos:
905 69 u = ui.ui()
906 70 try:
907 71 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
908 72 except IOError:
909 73 pass
910 74 get = u.config
911 75
912 76 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
913 77 .replace("//", "/"))
914 78
915 79 # update time with local timezone
916 80 try:
917 81 d = (get_mtime(path), util.makedate()[1])
918 82 except OSError:
919 83 continue
920 84
921 85 contact = (get("ui", "username") or # preferred
922 86 get("web", "contact") or # deprecated
923 87 get("web", "author", "")) # also
924 88 description = get("web", "description", "")
925 89 name = get("web", "name", name)
926 90 row = dict(contact=contact or "unknown",
927 91 contact_sort=contact.upper() or "unknown",
928 92 name=name,
929 93 name_sort=name,
930 94 url=url,
931 95 description=description or "unknown",
932 96 description_sort=description.upper() or "unknown",
933 97 lastchange=d,
934 98 lastchange_sort=d[1]-d[0],
935 99 archives=archivelist(u, "tip", url))
936 100 if (not sortcolumn
937 101 or (sortcolumn, descending) == self.repos_sorted):
938 102 # fast path for unsorted output
939 103 row['parity'] = parity
940 104 parity = 1 - parity
941 105 yield row
942 106 else:
943 107 rows.append((row["%s_sort" % sortcolumn], row))
944 108 if rows:
945 109 rows.sort()
946 110 if descending:
947 111 rows.reverse()
948 112 for key, row in rows:
949 113 row['parity'] = parity
950 114 parity = 1 - parity
951 115 yield row
952 116
953 117 virtual = req.env.get("PATH_INFO", "").strip('/')
954 118 if virtual:
955 119 real = dict(self.repos).get(virtual)
956 120 if real:
957 121 try:
958 122 hgweb(real).run(req)
959 123 except IOError, inst:
960 124 req.write(tmpl("error", error=inst.strerror))
961 125 except hg.RepoError, inst:
962 126 req.write(tmpl("error", error=str(inst)))
963 127 else:
964 128 req.write(tmpl("notfound", repo=virtual))
965 129 else:
966 130 if req.form.has_key('static'):
967 131 static = os.path.join(templater.templatepath(), "static")
968 132 fname = req.form['static'][0]
969 133 req.write(staticfile(static, fname)
970 134 or tmpl("error", error="%r not found" % fname))
971 135 else:
972 136 sortable = ["name", "description", "contact", "lastchange"]
973 137 sortcolumn, descending = self.repos_sorted
974 138 if req.form.has_key('sort'):
975 139 sortcolumn = req.form['sort'][0]
976 140 descending = sortcolumn.startswith('-')
977 141 if descending:
978 142 sortcolumn = sortcolumn[1:]
979 143 if sortcolumn not in sortable:
980 144 sortcolumn = ""
981 145
982 146 sort = [("sort_%s" % column,
983 147 "%s%s" % ((not descending and column == sortcolumn)
984 148 and "-" or "", column))
985 149 for column in sortable]
986 150 req.write(tmpl("index", entries=entries,
987 151 sortcolumn=sortcolumn, descending=descending,
988 152 **dict(sort)))
General Comments 0
You need to be logged in to leave comments. Login now