##// END OF EJS Templates
hgweb: wrap {labels} by hybridlist()...
Yuya Nishihara -
r37528:876d54f8 default
parent child Browse files
Show More
@@ -1,541 +1,542 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import gc
12 12 import os
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_SERVER_ERROR,
20 20 cspvalues,
21 21 get_contact,
22 22 get_mtime,
23 23 ismember,
24 24 paritygen,
25 25 staticfile,
26 26 statusmessage,
27 27 )
28 28
29 29 from .. import (
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 hg,
34 34 profiling,
35 35 pycompat,
36 36 scmutil,
37 37 templater,
38 38 templateutil,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 request as requestmod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49 from ..utils import dateutil
50 50
51 51 def cleannames(items):
52 52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 53
54 54 def findrepos(paths):
55 55 repos = []
56 56 for prefix, root in cleannames(paths):
57 57 roothead, roottail = os.path.split(root)
58 58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 59 # /bar/ be served as as foo/N .
60 60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 62 try:
63 63 recurse = {'*': False, '**': True}[roottail]
64 64 except KeyError:
65 65 repos.append((prefix, root))
66 66 continue
67 67 roothead = os.path.normpath(os.path.abspath(roothead))
68 68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 69 repos.extend(urlrepos(prefix, roothead, paths))
70 70 return repos
71 71
72 72 def urlrepos(prefix, roothead, paths):
73 73 """yield url paths and filesystem paths from a list of repo paths
74 74
75 75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 80 """
81 81 for path in paths:
82 82 path = os.path.normpath(path)
83 83 yield (prefix + '/' +
84 84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 85
86 86 def readallowed(ui, req):
87 87 """Check allow_read and deny_read config options of a repo's ui object
88 88 to determine user permissions. By default, with neither option set (or
89 89 both empty), allow all users to read the repo. There are two ways a
90 90 user can be denied read access: (1) deny_read is not empty, and the
91 91 user is unauthenticated or deny_read contains user (or *), and (2)
92 92 allow_read is not empty and the user is not in allow_read. Return True
93 93 if user is allowed to read the repo, else return False."""
94 94
95 95 user = req.remoteuser
96 96
97 97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 98 if deny_read and (not user or ismember(ui, user, deny_read)):
99 99 return False
100 100
101 101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 102 # by default, allow reading if no allow_read option has been set
103 103 if not allow_read or ismember(ui, user, allow_read):
104 104 return True
105 105
106 106 return False
107 107
108 108 def archivelist(ui, nodeid, url):
109 109 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
110 110 archives = []
111 111
112 112 for typ, spec in hgweb_mod.archivespecs.iteritems():
113 113 if typ in allowed or ui.configbool('web', 'allow' + typ,
114 114 untrusted=True):
115 115 archives.append({
116 116 'type': typ,
117 117 'extension': spec[2],
118 118 'node': nodeid,
119 119 'url': url,
120 120 })
121 121
122 122 return archives
123 123
124 124 def rawindexentries(ui, repos, req, subdir=''):
125 125 descend = ui.configbool('web', 'descend')
126 126 collapse = ui.configbool('web', 'collapse')
127 127 seenrepos = set()
128 128 seendirs = set()
129 129 for name, path in repos:
130 130
131 131 if not name.startswith(subdir):
132 132 continue
133 133 name = name[len(subdir):]
134 134 directory = False
135 135
136 136 if '/' in name:
137 137 if not descend:
138 138 continue
139 139
140 140 nameparts = name.split('/')
141 141 rootname = nameparts[0]
142 142
143 143 if not collapse:
144 144 pass
145 145 elif rootname in seendirs:
146 146 continue
147 147 elif rootname in seenrepos:
148 148 pass
149 149 else:
150 150 directory = True
151 151 name = rootname
152 152
153 153 # redefine the path to refer to the directory
154 154 discarded = '/'.join(nameparts[1:])
155 155
156 156 # remove name parts plus accompanying slash
157 157 path = path[:-len(discarded) - 1]
158 158
159 159 try:
160 160 r = hg.repository(ui, path)
161 161 directory = False
162 162 except (IOError, error.RepoError):
163 163 pass
164 164
165 165 parts = [
166 166 req.apppath.strip('/'),
167 167 subdir.strip('/'),
168 168 name.strip('/'),
169 169 ]
170 170 url = '/' + '/'.join(p for p in parts if p) + '/'
171 171
172 172 # show either a directory entry or a repository
173 173 if directory:
174 174 # get the directory's time information
175 175 try:
176 176 d = (get_mtime(path), dateutil.makedate()[1])
177 177 except OSError:
178 178 continue
179 179
180 180 # add '/' to the name to make it obvious that
181 181 # the entry is a directory, not a regular repository
182 182 row = {'contact': "",
183 183 'contact_sort': "",
184 184 'name': name + '/',
185 185 'name_sort': name,
186 186 'url': url,
187 187 'description': "",
188 188 'description_sort': "",
189 189 'lastchange': d,
190 190 'lastchange_sort': d[1] - d[0],
191 191 'archives': [],
192 192 'isdirectory': True,
193 'labels': [],
193 'labels': templateutil.hybridlist([], name='label'),
194 194 }
195 195
196 196 seendirs.add(name)
197 197 yield row
198 198 continue
199 199
200 200 u = ui.copy()
201 201 try:
202 202 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
203 203 except Exception as e:
204 204 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
205 205 continue
206 206
207 207 def get(section, name, default=uimod._unset):
208 208 return u.config(section, name, default, untrusted=True)
209 209
210 210 if u.configbool("web", "hidden", untrusted=True):
211 211 continue
212 212
213 213 if not readallowed(u, req):
214 214 continue
215 215
216 216 # update time with local timezone
217 217 try:
218 218 r = hg.repository(ui, path)
219 219 except IOError:
220 220 u.warn(_('error accessing repository at %s\n') % path)
221 221 continue
222 222 except error.RepoError:
223 223 u.warn(_('error accessing repository at %s\n') % path)
224 224 continue
225 225 try:
226 226 d = (get_mtime(r.spath), dateutil.makedate()[1])
227 227 except OSError:
228 228 continue
229 229
230 230 contact = get_contact(get)
231 231 description = get("web", "description")
232 232 seenrepos.add(name)
233 233 name = get("web", "name", name)
234 labels = u.configlist('web', 'labels', untrusted=True)
234 235 row = {'contact': contact or "unknown",
235 236 'contact_sort': contact.upper() or "unknown",
236 237 'name': name,
237 238 'name_sort': name,
238 239 'url': url,
239 240 'description': description or "unknown",
240 241 'description_sort': description.upper() or "unknown",
241 242 'lastchange': d,
242 243 'lastchange_sort': d[1] - d[0],
243 244 'archives': archivelist(u, "tip", url),
244 245 'isdirectory': None,
245 'labels': u.configlist('web', 'labels', untrusted=True),
246 'labels': templateutil.hybridlist(labels, name='label'),
246 247 }
247 248
248 249 yield row
249 250
250 251 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
251 252 descending, subdir):
252 253 rows = rawindexentries(ui, repos, req, subdir=subdir)
253 254
254 255 sortdefault = None, False
255 256
256 257 if sortcolumn and sortdefault != (sortcolumn, descending):
257 258 sortkey = '%s_sort' % sortcolumn
258 259 rows = sorted(rows, key=lambda x: x[sortkey],
259 260 reverse=descending)
260 261
261 262 for row, parity in zip(rows, paritygen(stripecount)):
262 263 row['parity'] = parity
263 264 yield row
264 265
265 266 def indexentries(ui, repos, req, stripecount, sortcolumn='',
266 267 descending=False, subdir=''):
267 268 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
268 269 return templateutil.mappinggenerator(_indexentriesgen, args=args)
269 270
270 271 class hgwebdir(object):
271 272 """HTTP server for multiple repositories.
272 273
273 274 Given a configuration, different repositories will be served depending
274 275 on the request path.
275 276
276 277 Instances are typically used as WSGI applications.
277 278 """
278 279 def __init__(self, conf, baseui=None):
279 280 self.conf = conf
280 281 self.baseui = baseui
281 282 self.ui = None
282 283 self.lastrefresh = 0
283 284 self.motd = None
284 285 self.refresh()
285 286
286 287 def refresh(self):
287 288 if self.ui:
288 289 refreshinterval = self.ui.configint('web', 'refreshinterval')
289 290 else:
290 291 item = configitems.coreitems['web']['refreshinterval']
291 292 refreshinterval = item.default
292 293
293 294 # refreshinterval <= 0 means to always refresh.
294 295 if (refreshinterval > 0 and
295 296 self.lastrefresh + refreshinterval > time.time()):
296 297 return
297 298
298 299 if self.baseui:
299 300 u = self.baseui.copy()
300 301 else:
301 302 u = uimod.ui.load()
302 303 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
303 304 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
304 305 # displaying bundling progress bar while serving feels wrong and may
305 306 # break some wsgi implementations.
306 307 u.setconfig('progress', 'disable', 'true', 'hgweb')
307 308
308 309 if not isinstance(self.conf, (dict, list, tuple)):
309 310 map = {'paths': 'hgweb-paths'}
310 311 if not os.path.exists(self.conf):
311 312 raise error.Abort(_('config file %s not found!') % self.conf)
312 313 u.readconfig(self.conf, remap=map, trust=True)
313 314 paths = []
314 315 for name, ignored in u.configitems('hgweb-paths'):
315 316 for path in u.configlist('hgweb-paths', name):
316 317 paths.append((name, path))
317 318 elif isinstance(self.conf, (list, tuple)):
318 319 paths = self.conf
319 320 elif isinstance(self.conf, dict):
320 321 paths = self.conf.items()
321 322
322 323 repos = findrepos(paths)
323 324 for prefix, root in u.configitems('collections'):
324 325 prefix = util.pconvert(prefix)
325 326 for path in scmutil.walkrepos(root, followsym=True):
326 327 repo = os.path.normpath(path)
327 328 name = util.pconvert(repo)
328 329 if name.startswith(prefix):
329 330 name = name[len(prefix):]
330 331 repos.append((name.lstrip('/'), repo))
331 332
332 333 self.repos = repos
333 334 self.ui = u
334 335 encoding.encoding = self.ui.config('web', 'encoding')
335 336 self.style = self.ui.config('web', 'style')
336 337 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
337 338 self.stripecount = self.ui.config('web', 'stripes')
338 339 if self.stripecount:
339 340 self.stripecount = int(self.stripecount)
340 341 prefix = self.ui.config('web', 'prefix')
341 342 if prefix.startswith('/'):
342 343 prefix = prefix[1:]
343 344 if prefix.endswith('/'):
344 345 prefix = prefix[:-1]
345 346 self.prefix = prefix
346 347 self.lastrefresh = time.time()
347 348
348 349 def run(self):
349 350 if not encoding.environ.get('GATEWAY_INTERFACE',
350 351 '').startswith("CGI/1."):
351 352 raise RuntimeError("This function is only intended to be "
352 353 "called while running as a CGI script.")
353 354 wsgicgi.launch(self)
354 355
355 356 def __call__(self, env, respond):
356 357 baseurl = self.ui.config('web', 'baseurl')
357 358 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
358 359 res = requestmod.wsgiresponse(req, respond)
359 360
360 361 return self.run_wsgi(req, res)
361 362
362 363 def run_wsgi(self, req, res):
363 364 profile = self.ui.configbool('profiling', 'enabled')
364 365 with profiling.profile(self.ui, enabled=profile):
365 366 try:
366 367 for r in self._runwsgi(req, res):
367 368 yield r
368 369 finally:
369 370 # There are known cycles in localrepository that prevent
370 371 # those objects (and tons of held references) from being
371 372 # collected through normal refcounting. We mitigate those
372 373 # leaks by performing an explicit GC on every request.
373 374 # TODO remove this once leaks are fixed.
374 375 # TODO only run this on requests that create localrepository
375 376 # instances instead of every request.
376 377 gc.collect()
377 378
378 379 def _runwsgi(self, req, res):
379 380 try:
380 381 self.refresh()
381 382
382 383 csp, nonce = cspvalues(self.ui)
383 384 if csp:
384 385 res.headers['Content-Security-Policy'] = csp
385 386
386 387 virtual = req.dispatchpath.strip('/')
387 388 tmpl = self.templater(req, nonce)
388 389 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
389 390
390 391 # Global defaults. These can be overridden by any handler.
391 392 res.status = '200 Script output follows'
392 393 res.headers['Content-Type'] = ctype
393 394
394 395 # a static file
395 396 if virtual.startswith('static/') or 'static' in req.qsparams:
396 397 if virtual.startswith('static/'):
397 398 fname = virtual[7:]
398 399 else:
399 400 fname = req.qsparams['static']
400 401 static = self.ui.config("web", "static", None,
401 402 untrusted=False)
402 403 if not static:
403 404 tp = self.templatepath or templater.templatepaths()
404 405 if isinstance(tp, str):
405 406 tp = [tp]
406 407 static = [os.path.join(p, 'static') for p in tp]
407 408
408 409 staticfile(static, fname, res)
409 410 return res.sendresponse()
410 411
411 412 # top-level index
412 413
413 414 repos = dict(self.repos)
414 415
415 416 if (not virtual or virtual == 'index') and virtual not in repos:
416 417 return self.makeindex(req, res, tmpl)
417 418
418 419 # nested indexes and hgwebs
419 420
420 421 if virtual.endswith('/index') and virtual not in repos:
421 422 subdir = virtual[:-len('index')]
422 423 if any(r.startswith(subdir) for r in repos):
423 424 return self.makeindex(req, res, tmpl, subdir)
424 425
425 426 def _virtualdirs():
426 427 # Check the full virtual path, each parent, and the root ('')
427 428 if virtual != '':
428 429 yield virtual
429 430
430 431 for p in util.finddirs(virtual):
431 432 yield p
432 433
433 434 yield ''
434 435
435 436 for virtualrepo in _virtualdirs():
436 437 real = repos.get(virtualrepo)
437 438 if real:
438 439 # Re-parse the WSGI environment to take into account our
439 440 # repository path component.
440 441 req = requestmod.parserequestfromenv(
441 442 req.rawenv, reponame=virtualrepo,
442 443 altbaseurl=self.ui.config('web', 'baseurl'))
443 444 try:
444 445 # ensure caller gets private copy of ui
445 446 repo = hg.repository(self.ui.copy(), real)
446 447 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
447 448 except IOError as inst:
448 449 msg = encoding.strtolocal(inst.strerror)
449 450 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
450 451 except error.RepoError as inst:
451 452 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
452 453
453 454 # browse subdirectories
454 455 subdir = virtual + '/'
455 456 if [r for r in repos if r.startswith(subdir)]:
456 457 return self.makeindex(req, res, tmpl, subdir)
457 458
458 459 # prefixes not found
459 460 res.status = '404 Not Found'
460 461 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
461 462 return res.sendresponse()
462 463
463 464 except ErrorResponse as e:
464 465 res.status = statusmessage(e.code, pycompat.bytestr(e))
465 466 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
466 467 return res.sendresponse()
467 468 finally:
468 469 tmpl = None
469 470
470 471 def makeindex(self, req, res, tmpl, subdir=""):
471 472 self.refresh()
472 473 sortable = ["name", "description", "contact", "lastchange"]
473 474 sortcolumn, descending = None, False
474 475 if 'sort' in req.qsparams:
475 476 sortcolumn = req.qsparams['sort']
476 477 descending = sortcolumn.startswith('-')
477 478 if descending:
478 479 sortcolumn = sortcolumn[1:]
479 480 if sortcolumn not in sortable:
480 481 sortcolumn = ""
481 482
482 483 sort = [("sort_%s" % column,
483 484 "%s%s" % ((not descending and column == sortcolumn)
484 485 and "-" or "", column))
485 486 for column in sortable]
486 487
487 488 self.refresh()
488 489
489 490 entries = indexentries(self.ui, self.repos, req,
490 491 self.stripecount, sortcolumn=sortcolumn,
491 492 descending=descending, subdir=subdir)
492 493
493 494 mapping = {
494 495 'entries': entries,
495 496 'subdir': subdir,
496 497 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
497 498 'sortcolumn': sortcolumn,
498 499 'descending': descending,
499 500 }
500 501 mapping.update(sort)
501 502 res.setbodygen(tmpl.generate('index', mapping))
502 503 return res.sendresponse()
503 504
504 505 def templater(self, req, nonce):
505 506
506 507 def motd(**map):
507 508 if self.motd is not None:
508 509 yield self.motd
509 510 else:
510 511 yield config('web', 'motd')
511 512
512 513 def config(section, name, default=uimod._unset, untrusted=True):
513 514 return self.ui.config(section, name, default, untrusted)
514 515
515 516 vars = {}
516 517 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
517 518 self.templatepath)
518 519 if style == styles[0]:
519 520 vars['style'] = style
520 521
521 522 sessionvars = webutil.sessionvars(vars, r'?')
522 523 logourl = config('web', 'logourl')
523 524 logoimg = config('web', 'logoimg')
524 525 staticurl = (config('web', 'staticurl')
525 526 or req.apppath + '/static/')
526 527 if not staticurl.endswith('/'):
527 528 staticurl += '/'
528 529
529 530 defaults = {
530 531 "encoding": encoding.encoding,
531 532 "motd": motd,
532 533 "url": req.apppath + '/',
533 534 "logourl": logourl,
534 535 "logoimg": logoimg,
535 536 "staticurl": staticurl,
536 537 "sessionvars": sessionvars,
537 538 "style": style,
538 539 "nonce": nonce,
539 540 }
540 541 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
541 542 return tmpl
@@ -1,1493 +1,1494 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import mimetypes
12 12 import os
13 13 import re
14 14
15 15 from ..i18n import _
16 16 from ..node import hex, nullid, short
17 17
18 18 from .common import (
19 19 ErrorResponse,
20 20 HTTP_FORBIDDEN,
21 21 HTTP_NOT_FOUND,
22 22 get_contact,
23 23 paritygen,
24 24 staticfile,
25 25 )
26 26
27 27 from .. import (
28 28 archival,
29 29 dagop,
30 30 encoding,
31 31 error,
32 32 graphmod,
33 33 pycompat,
34 34 revset,
35 35 revsetlang,
36 36 scmutil,
37 37 smartset,
38 38 templater,
39 39 templateutil,
40 40 )
41 41
42 42 from ..utils import (
43 43 stringutil,
44 44 )
45 45
46 46 from . import (
47 47 webutil,
48 48 )
49 49
50 50 __all__ = []
51 51 commands = {}
52 52
53 53 class webcommand(object):
54 54 """Decorator used to register a web command handler.
55 55
56 56 The decorator takes as its positional arguments the name/path the
57 57 command should be accessible under.
58 58
59 59 When called, functions receive as arguments a ``requestcontext``,
60 60 ``wsgirequest``, and a templater instance for generatoring output.
61 61 The functions should populate the ``rctx.res`` object with details
62 62 about the HTTP response.
63 63
64 64 The function returns a generator to be consumed by the WSGI application.
65 65 For most commands, this should be the result from
66 66 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
67 67 to render a template.
68 68
69 69 Usage:
70 70
71 71 @webcommand('mycommand')
72 72 def mycommand(web):
73 73 pass
74 74 """
75 75
76 76 def __init__(self, name):
77 77 self.name = name
78 78
79 79 def __call__(self, func):
80 80 __all__.append(self.name)
81 81 commands[self.name] = func
82 82 return func
83 83
84 84 @webcommand('log')
85 85 def log(web):
86 86 """
87 87 /log[/{revision}[/{path}]]
88 88 --------------------------
89 89
90 90 Show repository or file history.
91 91
92 92 For URLs of the form ``/log/{revision}``, a list of changesets starting at
93 93 the specified changeset identifier is shown. If ``{revision}`` is not
94 94 defined, the default is ``tip``. This form is equivalent to the
95 95 ``changelog`` handler.
96 96
97 97 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
98 98 file will be shown. This form is equivalent to the ``filelog`` handler.
99 99 """
100 100
101 101 if web.req.qsparams.get('file'):
102 102 return filelog(web)
103 103 else:
104 104 return changelog(web)
105 105
106 106 @webcommand('rawfile')
107 107 def rawfile(web):
108 108 guessmime = web.configbool('web', 'guessmime')
109 109
110 110 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
111 111 if not path:
112 112 return manifest(web)
113 113
114 114 try:
115 115 fctx = webutil.filectx(web.repo, web.req)
116 116 except error.LookupError as inst:
117 117 try:
118 118 return manifest(web)
119 119 except ErrorResponse:
120 120 raise inst
121 121
122 122 path = fctx.path()
123 123 text = fctx.data()
124 124 mt = 'application/binary'
125 125 if guessmime:
126 126 mt = mimetypes.guess_type(path)[0]
127 127 if mt is None:
128 128 if stringutil.binary(text):
129 129 mt = 'application/binary'
130 130 else:
131 131 mt = 'text/plain'
132 132 if mt.startswith('text/'):
133 133 mt += '; charset="%s"' % encoding.encoding
134 134
135 135 web.res.headers['Content-Type'] = mt
136 136 filename = (path.rpartition('/')[-1]
137 137 .replace('\\', '\\\\').replace('"', '\\"'))
138 138 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
139 139 web.res.setbodybytes(text)
140 140 return web.res.sendresponse()
141 141
142 142 def _filerevision(web, fctx):
143 143 f = fctx.path()
144 144 text = fctx.data()
145 145 parity = paritygen(web.stripecount)
146 146 ishead = fctx.filerev() in fctx.filelog().headrevs()
147 147
148 148 if stringutil.binary(text):
149 149 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
150 150 text = '(binary:%s)' % mt
151 151
152 152 def lines():
153 153 for lineno, t in enumerate(text.splitlines(True)):
154 154 yield {"line": t,
155 155 "lineid": "l%d" % (lineno + 1),
156 156 "linenumber": "% 6d" % (lineno + 1),
157 157 "parity": next(parity)}
158 158
159 159 return web.sendtemplate(
160 160 'filerevision',
161 161 file=f,
162 162 path=webutil.up(f),
163 163 text=lines(),
164 164 symrev=webutil.symrevorshortnode(web.req, fctx),
165 165 rename=webutil.renamelink(fctx),
166 166 permissions=fctx.manifest().flags(f),
167 167 ishead=int(ishead),
168 168 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
169 169
170 170 @webcommand('file')
171 171 def file(web):
172 172 """
173 173 /file/{revision}[/{path}]
174 174 -------------------------
175 175
176 176 Show information about a directory or file in the repository.
177 177
178 178 Info about the ``path`` given as a URL parameter will be rendered.
179 179
180 180 If ``path`` is a directory, information about the entries in that
181 181 directory will be rendered. This form is equivalent to the ``manifest``
182 182 handler.
183 183
184 184 If ``path`` is a file, information about that file will be shown via
185 185 the ``filerevision`` template.
186 186
187 187 If ``path`` is not defined, information about the root directory will
188 188 be rendered.
189 189 """
190 190 if web.req.qsparams.get('style') == 'raw':
191 191 return rawfile(web)
192 192
193 193 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
194 194 if not path:
195 195 return manifest(web)
196 196 try:
197 197 return _filerevision(web, webutil.filectx(web.repo, web.req))
198 198 except error.LookupError as inst:
199 199 try:
200 200 return manifest(web)
201 201 except ErrorResponse:
202 202 raise inst
203 203
204 204 def _search(web):
205 205 MODE_REVISION = 'rev'
206 206 MODE_KEYWORD = 'keyword'
207 207 MODE_REVSET = 'revset'
208 208
209 209 def revsearch(ctx):
210 210 yield ctx
211 211
212 212 def keywordsearch(query):
213 213 lower = encoding.lower
214 214 qw = lower(query).split()
215 215
216 216 def revgen():
217 217 cl = web.repo.changelog
218 218 for i in xrange(len(web.repo) - 1, 0, -100):
219 219 l = []
220 220 for j in cl.revs(max(0, i - 99), i):
221 221 ctx = web.repo[j]
222 222 l.append(ctx)
223 223 l.reverse()
224 224 for e in l:
225 225 yield e
226 226
227 227 for ctx in revgen():
228 228 miss = 0
229 229 for q in qw:
230 230 if not (q in lower(ctx.user()) or
231 231 q in lower(ctx.description()) or
232 232 q in lower(" ".join(ctx.files()))):
233 233 miss = 1
234 234 break
235 235 if miss:
236 236 continue
237 237
238 238 yield ctx
239 239
240 240 def revsetsearch(revs):
241 241 for r in revs:
242 242 yield web.repo[r]
243 243
244 244 searchfuncs = {
245 245 MODE_REVISION: (revsearch, 'exact revision search'),
246 246 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
247 247 MODE_REVSET: (revsetsearch, 'revset expression search'),
248 248 }
249 249
250 250 def getsearchmode(query):
251 251 try:
252 252 ctx = scmutil.revsymbol(web.repo, query)
253 253 except (error.RepoError, error.LookupError):
254 254 # query is not an exact revision pointer, need to
255 255 # decide if it's a revset expression or keywords
256 256 pass
257 257 else:
258 258 return MODE_REVISION, ctx
259 259
260 260 revdef = 'reverse(%s)' % query
261 261 try:
262 262 tree = revsetlang.parse(revdef)
263 263 except error.ParseError:
264 264 # can't parse to a revset tree
265 265 return MODE_KEYWORD, query
266 266
267 267 if revsetlang.depth(tree) <= 2:
268 268 # no revset syntax used
269 269 return MODE_KEYWORD, query
270 270
271 271 if any((token, (value or '')[:3]) == ('string', 're:')
272 272 for token, value, pos in revsetlang.tokenize(revdef)):
273 273 return MODE_KEYWORD, query
274 274
275 275 funcsused = revsetlang.funcsused(tree)
276 276 if not funcsused.issubset(revset.safesymbols):
277 277 return MODE_KEYWORD, query
278 278
279 279 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
280 280 try:
281 281 revs = mfunc(web.repo)
282 282 return MODE_REVSET, revs
283 283 # ParseError: wrongly placed tokens, wrongs arguments, etc
284 284 # RepoLookupError: no such revision, e.g. in 'revision:'
285 285 # Abort: bookmark/tag not exists
286 286 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
287 287 except (error.ParseError, error.RepoLookupError, error.Abort,
288 288 LookupError):
289 289 return MODE_KEYWORD, query
290 290
291 291 def changelist(context):
292 292 count = 0
293 293
294 294 for ctx in searchfunc[0](funcarg):
295 295 count += 1
296 296 n = ctx.node()
297 297 showtags = webutil.showtag(web.repo, web.tmpl, 'changelogtag', n)
298 298 files = webutil.listfilediffs(web.tmpl, ctx.files(), n,
299 299 web.maxfiles)
300 300
301 301 lm = webutil.commonentry(web.repo, ctx)
302 302 lm.update({
303 303 'parity': next(parity),
304 304 'changelogtag': showtags,
305 305 'files': files,
306 306 })
307 307 yield lm
308 308
309 309 if count >= revcount:
310 310 break
311 311
312 312 query = web.req.qsparams['rev']
313 313 revcount = web.maxchanges
314 314 if 'revcount' in web.req.qsparams:
315 315 try:
316 316 revcount = int(web.req.qsparams.get('revcount', revcount))
317 317 revcount = max(revcount, 1)
318 318 web.tmpl.defaults['sessionvars']['revcount'] = revcount
319 319 except ValueError:
320 320 pass
321 321
322 322 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
323 323 lessvars['revcount'] = max(revcount // 2, 1)
324 324 lessvars['rev'] = query
325 325 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
326 326 morevars['revcount'] = revcount * 2
327 327 morevars['rev'] = query
328 328
329 329 mode, funcarg = getsearchmode(query)
330 330
331 331 if 'forcekw' in web.req.qsparams:
332 332 showforcekw = ''
333 333 showunforcekw = searchfuncs[mode][1]
334 334 mode = MODE_KEYWORD
335 335 funcarg = query
336 336 else:
337 337 if mode != MODE_KEYWORD:
338 338 showforcekw = searchfuncs[MODE_KEYWORD][1]
339 339 else:
340 340 showforcekw = ''
341 341 showunforcekw = ''
342 342
343 343 searchfunc = searchfuncs[mode]
344 344
345 345 tip = web.repo['tip']
346 346 parity = paritygen(web.stripecount)
347 347
348 348 return web.sendtemplate(
349 349 'search',
350 350 query=query,
351 351 node=tip.hex(),
352 352 symrev='tip',
353 353 entries=templateutil.mappinggenerator(changelist, name='searchentry'),
354 354 archives=web.archivelist('tip'),
355 355 morevars=morevars,
356 356 lessvars=lessvars,
357 357 modedesc=searchfunc[1],
358 358 showforcekw=showforcekw,
359 359 showunforcekw=showunforcekw)
360 360
361 361 @webcommand('changelog')
362 362 def changelog(web, shortlog=False):
363 363 """
364 364 /changelog[/{revision}]
365 365 -----------------------
366 366
367 367 Show information about multiple changesets.
368 368
369 369 If the optional ``revision`` URL argument is absent, information about
370 370 all changesets starting at ``tip`` will be rendered. If the ``revision``
371 371 argument is present, changesets will be shown starting from the specified
372 372 revision.
373 373
374 374 If ``revision`` is absent, the ``rev`` query string argument may be
375 375 defined. This will perform a search for changesets.
376 376
377 377 The argument for ``rev`` can be a single revision, a revision set,
378 378 or a literal keyword to search for in changeset data (equivalent to
379 379 :hg:`log -k`).
380 380
381 381 The ``revcount`` query string argument defines the maximum numbers of
382 382 changesets to render.
383 383
384 384 For non-searches, the ``changelog`` template will be rendered.
385 385 """
386 386
387 387 query = ''
388 388 if 'node' in web.req.qsparams:
389 389 ctx = webutil.changectx(web.repo, web.req)
390 390 symrev = webutil.symrevorshortnode(web.req, ctx)
391 391 elif 'rev' in web.req.qsparams:
392 392 return _search(web)
393 393 else:
394 394 ctx = web.repo['tip']
395 395 symrev = 'tip'
396 396
397 397 def changelist():
398 398 revs = []
399 399 if pos != -1:
400 400 revs = web.repo.changelog.revs(pos, 0)
401 401 curcount = 0
402 402 for rev in revs:
403 403 curcount += 1
404 404 if curcount > revcount + 1:
405 405 break
406 406
407 407 entry = webutil.changelistentry(web, web.repo[rev])
408 408 entry['parity'] = next(parity)
409 409 yield entry
410 410
411 411 if shortlog:
412 412 revcount = web.maxshortchanges
413 413 else:
414 414 revcount = web.maxchanges
415 415
416 416 if 'revcount' in web.req.qsparams:
417 417 try:
418 418 revcount = int(web.req.qsparams.get('revcount', revcount))
419 419 revcount = max(revcount, 1)
420 420 web.tmpl.defaults['sessionvars']['revcount'] = revcount
421 421 except ValueError:
422 422 pass
423 423
424 424 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
425 425 lessvars['revcount'] = max(revcount // 2, 1)
426 426 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
427 427 morevars['revcount'] = revcount * 2
428 428
429 429 count = len(web.repo)
430 430 pos = ctx.rev()
431 431 parity = paritygen(web.stripecount)
432 432
433 433 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
434 434
435 435 entries = list(changelist())
436 436 latestentry = entries[:1]
437 437 if len(entries) > revcount:
438 438 nextentry = entries[-1:]
439 439 entries = entries[:-1]
440 440 else:
441 441 nextentry = []
442 442
443 443 return web.sendtemplate(
444 444 'shortlog' if shortlog else 'changelog',
445 445 changenav=changenav,
446 446 node=ctx.hex(),
447 447 rev=pos,
448 448 symrev=symrev,
449 449 changesets=count,
450 450 entries=entries,
451 451 latestentry=latestentry,
452 452 nextentry=nextentry,
453 453 archives=web.archivelist('tip'),
454 454 revcount=revcount,
455 455 morevars=morevars,
456 456 lessvars=lessvars,
457 457 query=query)
458 458
459 459 @webcommand('shortlog')
460 460 def shortlog(web):
461 461 """
462 462 /shortlog
463 463 ---------
464 464
465 465 Show basic information about a set of changesets.
466 466
467 467 This accepts the same parameters as the ``changelog`` handler. The only
468 468 difference is the ``shortlog`` template will be rendered instead of the
469 469 ``changelog`` template.
470 470 """
471 471 return changelog(web, shortlog=True)
472 472
473 473 @webcommand('changeset')
474 474 def changeset(web):
475 475 """
476 476 /changeset[/{revision}]
477 477 -----------------------
478 478
479 479 Show information about a single changeset.
480 480
481 481 A URL path argument is the changeset identifier to show. See ``hg help
482 482 revisions`` for possible values. If not defined, the ``tip`` changeset
483 483 will be shown.
484 484
485 485 The ``changeset`` template is rendered. Contents of the ``changesettag``,
486 486 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
487 487 templates related to diffs may all be used to produce the output.
488 488 """
489 489 ctx = webutil.changectx(web.repo, web.req)
490 490
491 491 return web.sendtemplate(
492 492 'changeset',
493 493 **webutil.changesetentry(web, ctx))
494 494
495 495 rev = webcommand('rev')(changeset)
496 496
497 497 def decodepath(path):
498 498 """Hook for mapping a path in the repository to a path in the
499 499 working copy.
500 500
501 501 Extensions (e.g., largefiles) can override this to remap files in
502 502 the virtual file system presented by the manifest command below."""
503 503 return path
504 504
505 505 @webcommand('manifest')
506 506 def manifest(web):
507 507 """
508 508 /manifest[/{revision}[/{path}]]
509 509 -------------------------------
510 510
511 511 Show information about a directory.
512 512
513 513 If the URL path arguments are omitted, information about the root
514 514 directory for the ``tip`` changeset will be shown.
515 515
516 516 Because this handler can only show information for directories, it
517 517 is recommended to use the ``file`` handler instead, as it can handle both
518 518 directories and files.
519 519
520 520 The ``manifest`` template will be rendered for this handler.
521 521 """
522 522 if 'node' in web.req.qsparams:
523 523 ctx = webutil.changectx(web.repo, web.req)
524 524 symrev = webutil.symrevorshortnode(web.req, ctx)
525 525 else:
526 526 ctx = web.repo['tip']
527 527 symrev = 'tip'
528 528 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
529 529 mf = ctx.manifest()
530 530 node = ctx.node()
531 531
532 532 files = {}
533 533 dirs = {}
534 534 parity = paritygen(web.stripecount)
535 535
536 536 if path and path[-1:] != "/":
537 537 path += "/"
538 538 l = len(path)
539 539 abspath = "/" + path
540 540
541 541 for full, n in mf.iteritems():
542 542 # the virtual path (working copy path) used for the full
543 543 # (repository) path
544 544 f = decodepath(full)
545 545
546 546 if f[:l] != path:
547 547 continue
548 548 remain = f[l:]
549 549 elements = remain.split('/')
550 550 if len(elements) == 1:
551 551 files[remain] = full
552 552 else:
553 553 h = dirs # need to retain ref to dirs (root)
554 554 for elem in elements[0:-1]:
555 555 if elem not in h:
556 556 h[elem] = {}
557 557 h = h[elem]
558 558 if len(h) > 1:
559 559 break
560 560 h[None] = None # denotes files present
561 561
562 562 if mf and not files and not dirs:
563 563 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
564 564
565 565 def filelist(**map):
566 566 for f in sorted(files):
567 567 full = files[f]
568 568
569 569 fctx = ctx.filectx(full)
570 570 yield {"file": full,
571 571 "parity": next(parity),
572 572 "basename": f,
573 573 "date": fctx.date(),
574 574 "size": fctx.size(),
575 575 "permissions": mf.flags(full)}
576 576
577 577 def dirlist(**map):
578 578 for d in sorted(dirs):
579 579
580 580 emptydirs = []
581 581 h = dirs[d]
582 582 while isinstance(h, dict) and len(h) == 1:
583 583 k, v = next(iter(h.items()))
584 584 if v:
585 585 emptydirs.append(k)
586 586 h = v
587 587
588 588 path = "%s%s" % (abspath, d)
589 589 yield {"parity": next(parity),
590 590 "path": path,
591 591 "emptydirs": "/".join(emptydirs),
592 592 "basename": d}
593 593
594 594 return web.sendtemplate(
595 595 'manifest',
596 596 symrev=symrev,
597 597 path=abspath,
598 598 up=webutil.up(abspath),
599 599 upparity=next(parity),
600 600 fentries=filelist,
601 601 dentries=dirlist,
602 602 archives=web.archivelist(hex(node)),
603 603 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
604 604
605 605 @webcommand('tags')
606 606 def tags(web):
607 607 """
608 608 /tags
609 609 -----
610 610
611 611 Show information about tags.
612 612
613 613 No arguments are accepted.
614 614
615 615 The ``tags`` template is rendered.
616 616 """
617 617 i = list(reversed(web.repo.tagslist()))
618 618 parity = paritygen(web.stripecount)
619 619
620 620 def entries(notip, latestonly, **map):
621 621 t = i
622 622 if notip:
623 623 t = [(k, n) for k, n in i if k != "tip"]
624 624 if latestonly:
625 625 t = t[:1]
626 626 for k, n in t:
627 627 yield {"parity": next(parity),
628 628 "tag": k,
629 629 "date": web.repo[n].date(),
630 630 "node": hex(n)}
631 631
632 632 return web.sendtemplate(
633 633 'tags',
634 634 node=hex(web.repo.changelog.tip()),
635 635 entries=lambda **x: entries(False, False, **x),
636 636 entriesnotip=lambda **x: entries(True, False, **x),
637 637 latestentry=lambda **x: entries(True, True, **x))
638 638
639 639 @webcommand('bookmarks')
640 640 def bookmarks(web):
641 641 """
642 642 /bookmarks
643 643 ----------
644 644
645 645 Show information about bookmarks.
646 646
647 647 No arguments are accepted.
648 648
649 649 The ``bookmarks`` template is rendered.
650 650 """
651 651 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
652 652 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
653 653 i = sorted(i, key=sortkey, reverse=True)
654 654 parity = paritygen(web.stripecount)
655 655
656 656 def entries(latestonly, **map):
657 657 t = i
658 658 if latestonly:
659 659 t = i[:1]
660 660 for k, n in t:
661 661 yield {"parity": next(parity),
662 662 "bookmark": k,
663 663 "date": web.repo[n].date(),
664 664 "node": hex(n)}
665 665
666 666 if i:
667 667 latestrev = i[0][1]
668 668 else:
669 669 latestrev = -1
670 670
671 671 return web.sendtemplate(
672 672 'bookmarks',
673 673 node=hex(web.repo.changelog.tip()),
674 674 lastchange=[{'date': web.repo[latestrev].date()}],
675 675 entries=lambda **x: entries(latestonly=False, **x),
676 676 latestentry=lambda **x: entries(latestonly=True, **x))
677 677
678 678 @webcommand('branches')
679 679 def branches(web):
680 680 """
681 681 /branches
682 682 ---------
683 683
684 684 Show information about branches.
685 685
686 686 All known branches are contained in the output, even closed branches.
687 687
688 688 No arguments are accepted.
689 689
690 690 The ``branches`` template is rendered.
691 691 """
692 692 entries = webutil.branchentries(web.repo, web.stripecount)
693 693 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
694 694
695 695 return web.sendtemplate(
696 696 'branches',
697 697 node=hex(web.repo.changelog.tip()),
698 698 entries=entries,
699 699 latestentry=latestentry)
700 700
701 701 @webcommand('summary')
702 702 def summary(web):
703 703 """
704 704 /summary
705 705 --------
706 706
707 707 Show a summary of repository state.
708 708
709 709 Information about the latest changesets, bookmarks, tags, and branches
710 710 is captured by this handler.
711 711
712 712 The ``summary`` template is rendered.
713 713 """
714 714 i = reversed(web.repo.tagslist())
715 715
716 716 def tagentries(context):
717 717 parity = paritygen(web.stripecount)
718 718 count = 0
719 719 for k, n in i:
720 720 if k == "tip": # skip tip
721 721 continue
722 722
723 723 count += 1
724 724 if count > 10: # limit to 10 tags
725 725 break
726 726
727 727 yield {
728 728 'parity': next(parity),
729 729 'tag': k,
730 730 'node': hex(n),
731 731 'date': web.repo[n].date(),
732 732 }
733 733
734 734 def bookmarks(**map):
735 735 parity = paritygen(web.stripecount)
736 736 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
737 737 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
738 738 marks = sorted(marks, key=sortkey, reverse=True)
739 739 for k, n in marks[:10]: # limit to 10 bookmarks
740 740 yield {'parity': next(parity),
741 741 'bookmark': k,
742 742 'date': web.repo[n].date(),
743 743 'node': hex(n)}
744 744
745 745 def changelist(context):
746 746 parity = paritygen(web.stripecount, offset=start - end)
747 747 l = [] # build a list in forward order for efficiency
748 748 revs = []
749 749 if start < end:
750 750 revs = web.repo.changelog.revs(start, end - 1)
751 751 for i in revs:
752 752 ctx = web.repo[i]
753 753 lm = webutil.commonentry(web.repo, ctx)
754 754 lm['parity'] = next(parity)
755 755 l.append(lm)
756 756
757 757 for entry in reversed(l):
758 758 yield entry
759 759
760 760 tip = web.repo['tip']
761 761 count = len(web.repo)
762 762 start = max(0, count - web.maxchanges)
763 763 end = min(count, start + web.maxchanges)
764 764
765 765 desc = web.config("web", "description")
766 766 if not desc:
767 767 desc = 'unknown'
768 labels = web.configlist('web', 'labels')
768 769
769 770 return web.sendtemplate(
770 771 'summary',
771 772 desc=desc,
772 773 owner=get_contact(web.config) or 'unknown',
773 774 lastchange=tip.date(),
774 775 tags=templateutil.mappinggenerator(tagentries, name='tagentry'),
775 776 bookmarks=bookmarks,
776 777 branches=webutil.branchentries(web.repo, web.stripecount, 10),
777 778 shortlog=templateutil.mappinggenerator(changelist,
778 779 name='shortlogentry'),
779 780 node=tip.hex(),
780 781 symrev='tip',
781 782 archives=web.archivelist('tip'),
782 labels=web.configlist('web', 'labels'))
783 labels=templateutil.hybridlist(labels, name='label'))
783 784
784 785 @webcommand('filediff')
785 786 def filediff(web):
786 787 """
787 788 /diff/{revision}/{path}
788 789 -----------------------
789 790
790 791 Show how a file changed in a particular commit.
791 792
792 793 The ``filediff`` template is rendered.
793 794
794 795 This handler is registered under both the ``/diff`` and ``/filediff``
795 796 paths. ``/diff`` is used in modern code.
796 797 """
797 798 fctx, ctx = None, None
798 799 try:
799 800 fctx = webutil.filectx(web.repo, web.req)
800 801 except LookupError:
801 802 ctx = webutil.changectx(web.repo, web.req)
802 803 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
803 804 if path not in ctx.files():
804 805 raise
805 806
806 807 if fctx is not None:
807 808 path = fctx.path()
808 809 ctx = fctx.changectx()
809 810 basectx = ctx.p1()
810 811
811 812 style = web.config('web', 'style')
812 813 if 'style' in web.req.qsparams:
813 814 style = web.req.qsparams['style']
814 815
815 816 diffs = webutil.diffs(web, ctx, basectx, [path], style)
816 817 if fctx is not None:
817 818 rename = webutil.renamelink(fctx)
818 819 ctx = fctx
819 820 else:
820 821 rename = []
821 822 ctx = ctx
822 823
823 824 return web.sendtemplate(
824 825 'filediff',
825 826 file=path,
826 827 symrev=webutil.symrevorshortnode(web.req, ctx),
827 828 rename=rename,
828 829 diff=diffs,
829 830 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
830 831
831 832 diff = webcommand('diff')(filediff)
832 833
833 834 @webcommand('comparison')
834 835 def comparison(web):
835 836 """
836 837 /comparison/{revision}/{path}
837 838 -----------------------------
838 839
839 840 Show a comparison between the old and new versions of a file from changes
840 841 made on a particular revision.
841 842
842 843 This is similar to the ``diff`` handler. However, this form features
843 844 a split or side-by-side diff rather than a unified diff.
844 845
845 846 The ``context`` query string argument can be used to control the lines of
846 847 context in the diff.
847 848
848 849 The ``filecomparison`` template is rendered.
849 850 """
850 851 ctx = webutil.changectx(web.repo, web.req)
851 852 if 'file' not in web.req.qsparams:
852 853 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
853 854 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
854 855
855 856 parsecontext = lambda v: v == 'full' and -1 or int(v)
856 857 if 'context' in web.req.qsparams:
857 858 context = parsecontext(web.req.qsparams['context'])
858 859 else:
859 860 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
860 861
861 862 def filelines(f):
862 863 if f.isbinary():
863 864 mt = mimetypes.guess_type(f.path())[0]
864 865 if not mt:
865 866 mt = 'application/octet-stream'
866 867 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
867 868 return f.data().splitlines()
868 869
869 870 fctx = None
870 871 parent = ctx.p1()
871 872 leftrev = parent.rev()
872 873 leftnode = parent.node()
873 874 rightrev = ctx.rev()
874 875 rightnode = ctx.node()
875 876 if path in ctx:
876 877 fctx = ctx[path]
877 878 rightlines = filelines(fctx)
878 879 if path not in parent:
879 880 leftlines = ()
880 881 else:
881 882 pfctx = parent[path]
882 883 leftlines = filelines(pfctx)
883 884 else:
884 885 rightlines = ()
885 886 pfctx = ctx.parents()[0][path]
886 887 leftlines = filelines(pfctx)
887 888
888 889 comparison = webutil.compare(web.tmpl, context, leftlines, rightlines)
889 890 if fctx is not None:
890 891 rename = webutil.renamelink(fctx)
891 892 ctx = fctx
892 893 else:
893 894 rename = []
894 895 ctx = ctx
895 896
896 897 return web.sendtemplate(
897 898 'filecomparison',
898 899 file=path,
899 900 symrev=webutil.symrevorshortnode(web.req, ctx),
900 901 rename=rename,
901 902 leftrev=leftrev,
902 903 leftnode=hex(leftnode),
903 904 rightrev=rightrev,
904 905 rightnode=hex(rightnode),
905 906 comparison=comparison,
906 907 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
907 908
908 909 @webcommand('annotate')
909 910 def annotate(web):
910 911 """
911 912 /annotate/{revision}/{path}
912 913 ---------------------------
913 914
914 915 Show changeset information for each line in a file.
915 916
916 917 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
917 918 ``ignoreblanklines`` query string arguments have the same meaning as
918 919 their ``[annotate]`` config equivalents. It uses the hgrc boolean
919 920 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
920 921 false and ``1`` and ``true`` are true. If not defined, the server
921 922 default settings are used.
922 923
923 924 The ``fileannotate`` template is rendered.
924 925 """
925 926 fctx = webutil.filectx(web.repo, web.req)
926 927 f = fctx.path()
927 928 parity = paritygen(web.stripecount)
928 929 ishead = fctx.filerev() in fctx.filelog().headrevs()
929 930
930 931 # parents() is called once per line and several lines likely belong to
931 932 # same revision. So it is worth caching.
932 933 # TODO there are still redundant operations within basefilectx.parents()
933 934 # and from the fctx.annotate() call itself that could be cached.
934 935 parentscache = {}
935 936 def parents(f):
936 937 rev = f.rev()
937 938 if rev not in parentscache:
938 939 parentscache[rev] = []
939 940 for p in f.parents():
940 941 entry = {
941 942 'node': p.hex(),
942 943 'rev': p.rev(),
943 944 }
944 945 parentscache[rev].append(entry)
945 946
946 947 for p in parentscache[rev]:
947 948 yield p
948 949
949 950 def annotate(**map):
950 951 if fctx.isbinary():
951 952 mt = (mimetypes.guess_type(fctx.path())[0]
952 953 or 'application/octet-stream')
953 954 lines = [dagop.annotateline(fctx=fctx.filectx(fctx.filerev()),
954 955 lineno=1, text='(binary:%s)' % mt)]
955 956 else:
956 957 lines = webutil.annotate(web.req, fctx, web.repo.ui)
957 958
958 959 previousrev = None
959 960 blockparitygen = paritygen(1)
960 961 for lineno, aline in enumerate(lines):
961 962 f = aline.fctx
962 963 rev = f.rev()
963 964 if rev != previousrev:
964 965 blockhead = True
965 966 blockparity = next(blockparitygen)
966 967 else:
967 968 blockhead = None
968 969 previousrev = rev
969 970 yield {"parity": next(parity),
970 971 "node": f.hex(),
971 972 "rev": rev,
972 973 "author": f.user(),
973 974 "parents": parents(f),
974 975 "desc": f.description(),
975 976 "extra": f.extra(),
976 977 "file": f.path(),
977 978 "blockhead": blockhead,
978 979 "blockparity": blockparity,
979 980 "targetline": aline.lineno,
980 981 "line": aline.text,
981 982 "lineno": lineno + 1,
982 983 "lineid": "l%d" % (lineno + 1),
983 984 "linenumber": "% 6d" % (lineno + 1),
984 985 "revdate": f.date()}
985 986
986 987 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
987 988 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
988 989
989 990 return web.sendtemplate(
990 991 'fileannotate',
991 992 file=f,
992 993 annotate=annotate,
993 994 path=webutil.up(f),
994 995 symrev=webutil.symrevorshortnode(web.req, fctx),
995 996 rename=webutil.renamelink(fctx),
996 997 permissions=fctx.manifest().flags(f),
997 998 ishead=int(ishead),
998 999 diffopts=diffopts,
999 1000 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1000 1001
1001 1002 @webcommand('filelog')
1002 1003 def filelog(web):
1003 1004 """
1004 1005 /filelog/{revision}/{path}
1005 1006 --------------------------
1006 1007
1007 1008 Show information about the history of a file in the repository.
1008 1009
1009 1010 The ``revcount`` query string argument can be defined to control the
1010 1011 maximum number of entries to show.
1011 1012
1012 1013 The ``filelog`` template will be rendered.
1013 1014 """
1014 1015
1015 1016 try:
1016 1017 fctx = webutil.filectx(web.repo, web.req)
1017 1018 f = fctx.path()
1018 1019 fl = fctx.filelog()
1019 1020 except error.LookupError:
1020 1021 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1021 1022 fl = web.repo.file(f)
1022 1023 numrevs = len(fl)
1023 1024 if not numrevs: # file doesn't exist at all
1024 1025 raise
1025 1026 rev = webutil.changectx(web.repo, web.req).rev()
1026 1027 first = fl.linkrev(0)
1027 1028 if rev < first: # current rev is from before file existed
1028 1029 raise
1029 1030 frev = numrevs - 1
1030 1031 while fl.linkrev(frev) > rev:
1031 1032 frev -= 1
1032 1033 fctx = web.repo.filectx(f, fl.linkrev(frev))
1033 1034
1034 1035 revcount = web.maxshortchanges
1035 1036 if 'revcount' in web.req.qsparams:
1036 1037 try:
1037 1038 revcount = int(web.req.qsparams.get('revcount', revcount))
1038 1039 revcount = max(revcount, 1)
1039 1040 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1040 1041 except ValueError:
1041 1042 pass
1042 1043
1043 1044 lrange = webutil.linerange(web.req)
1044 1045
1045 1046 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1046 1047 lessvars['revcount'] = max(revcount // 2, 1)
1047 1048 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1048 1049 morevars['revcount'] = revcount * 2
1049 1050
1050 1051 patch = 'patch' in web.req.qsparams
1051 1052 if patch:
1052 1053 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1053 1054 descend = 'descend' in web.req.qsparams
1054 1055 if descend:
1055 1056 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1056 1057
1057 1058 count = fctx.filerev() + 1
1058 1059 start = max(0, count - revcount) # first rev on this page
1059 1060 end = min(count, start + revcount) # last rev on this page
1060 1061 parity = paritygen(web.stripecount, offset=start - end)
1061 1062
1062 1063 repo = web.repo
1063 1064 filelog = fctx.filelog()
1064 1065 revs = [filerev for filerev in filelog.revs(start, end - 1)
1065 1066 if filelog.linkrev(filerev) in repo]
1066 1067 entries = []
1067 1068
1068 1069 diffstyle = web.config('web', 'style')
1069 1070 if 'style' in web.req.qsparams:
1070 1071 diffstyle = web.req.qsparams['style']
1071 1072
1072 1073 def diff(fctx, linerange=None):
1073 1074 ctx = fctx.changectx()
1074 1075 basectx = ctx.p1()
1075 1076 path = fctx.path()
1076 1077 return webutil.diffs(web, ctx, basectx, [path], diffstyle,
1077 1078 linerange=linerange,
1078 1079 lineidprefix='%s-' % ctx.hex()[:12])
1079 1080
1080 1081 linerange = None
1081 1082 if lrange is not None:
1082 1083 linerange = webutil.formatlinerange(*lrange)
1083 1084 # deactivate numeric nav links when linerange is specified as this
1084 1085 # would required a dedicated "revnav" class
1085 1086 nav = []
1086 1087 if descend:
1087 1088 it = dagop.blockdescendants(fctx, *lrange)
1088 1089 else:
1089 1090 it = dagop.blockancestors(fctx, *lrange)
1090 1091 for i, (c, lr) in enumerate(it, 1):
1091 1092 diffs = None
1092 1093 if patch:
1093 1094 diffs = diff(c, linerange=lr)
1094 1095 # follow renames accross filtered (not in range) revisions
1095 1096 path = c.path()
1096 1097 entries.append(dict(
1097 1098 parity=next(parity),
1098 1099 filerev=c.rev(),
1099 1100 file=path,
1100 1101 diff=diffs,
1101 1102 linerange=webutil.formatlinerange(*lr),
1102 1103 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1103 1104 if i == revcount:
1104 1105 break
1105 1106 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1106 1107 morevars['linerange'] = lessvars['linerange']
1107 1108 else:
1108 1109 for i in revs:
1109 1110 iterfctx = fctx.filectx(i)
1110 1111 diffs = None
1111 1112 if patch:
1112 1113 diffs = diff(iterfctx)
1113 1114 entries.append(dict(
1114 1115 parity=next(parity),
1115 1116 filerev=i,
1116 1117 file=f,
1117 1118 diff=diffs,
1118 1119 rename=webutil.renamelink(iterfctx),
1119 1120 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1120 1121 entries.reverse()
1121 1122 revnav = webutil.filerevnav(web.repo, fctx.path())
1122 1123 nav = revnav.gen(end - 1, revcount, count)
1123 1124
1124 1125 latestentry = entries[:1]
1125 1126
1126 1127 return web.sendtemplate(
1127 1128 'filelog',
1128 1129 file=f,
1129 1130 nav=nav,
1130 1131 symrev=webutil.symrevorshortnode(web.req, fctx),
1131 1132 entries=entries,
1132 1133 descend=descend,
1133 1134 patch=patch,
1134 1135 latestentry=latestentry,
1135 1136 linerange=linerange,
1136 1137 revcount=revcount,
1137 1138 morevars=morevars,
1138 1139 lessvars=lessvars,
1139 1140 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1140 1141
1141 1142 @webcommand('archive')
1142 1143 def archive(web):
1143 1144 """
1144 1145 /archive/{revision}.{format}[/{path}]
1145 1146 -------------------------------------
1146 1147
1147 1148 Obtain an archive of repository content.
1148 1149
1149 1150 The content and type of the archive is defined by a URL path parameter.
1150 1151 ``format`` is the file extension of the archive type to be generated. e.g.
1151 1152 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1152 1153 server configuration.
1153 1154
1154 1155 The optional ``path`` URL parameter controls content to include in the
1155 1156 archive. If omitted, every file in the specified revision is present in the
1156 1157 archive. If included, only the specified file or contents of the specified
1157 1158 directory will be included in the archive.
1158 1159
1159 1160 No template is used for this handler. Raw, binary content is generated.
1160 1161 """
1161 1162
1162 1163 type_ = web.req.qsparams.get('type')
1163 1164 allowed = web.configlist("web", "allow_archive")
1164 1165 key = web.req.qsparams['node']
1165 1166
1166 1167 if type_ not in web.archivespecs:
1167 1168 msg = 'Unsupported archive type: %s' % type_
1168 1169 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1169 1170
1170 1171 if not ((type_ in allowed or
1171 1172 web.configbool("web", "allow" + type_))):
1172 1173 msg = 'Archive type not allowed: %s' % type_
1173 1174 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1174 1175
1175 1176 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1176 1177 cnode = web.repo.lookup(key)
1177 1178 arch_version = key
1178 1179 if cnode == key or key == 'tip':
1179 1180 arch_version = short(cnode)
1180 1181 name = "%s-%s" % (reponame, arch_version)
1181 1182
1182 1183 ctx = webutil.changectx(web.repo, web.req)
1183 1184 pats = []
1184 1185 match = scmutil.match(ctx, [])
1185 1186 file = web.req.qsparams.get('file')
1186 1187 if file:
1187 1188 pats = ['path:' + file]
1188 1189 match = scmutil.match(ctx, pats, default='path')
1189 1190 if pats:
1190 1191 files = [f for f in ctx.manifest().keys() if match(f)]
1191 1192 if not files:
1192 1193 raise ErrorResponse(HTTP_NOT_FOUND,
1193 1194 'file(s) not found: %s' % file)
1194 1195
1195 1196 mimetype, artype, extension, encoding = web.archivespecs[type_]
1196 1197
1197 1198 web.res.headers['Content-Type'] = mimetype
1198 1199 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1199 1200 name, extension)
1200 1201
1201 1202 if encoding:
1202 1203 web.res.headers['Content-Encoding'] = encoding
1203 1204
1204 1205 web.res.setbodywillwrite()
1205 1206 if list(web.res.sendresponse()):
1206 1207 raise error.ProgrammingError('sendresponse() should not emit data '
1207 1208 'if writing later')
1208 1209
1209 1210 bodyfh = web.res.getbodyfile()
1210 1211
1211 1212 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1212 1213 matchfn=match,
1213 1214 subrepos=web.configbool("web", "archivesubrepos"))
1214 1215
1215 1216 return []
1216 1217
1217 1218 @webcommand('static')
1218 1219 def static(web):
1219 1220 fname = web.req.qsparams['file']
1220 1221 # a repo owner may set web.static in .hg/hgrc to get any file
1221 1222 # readable by the user running the CGI script
1222 1223 static = web.config("web", "static", None, untrusted=False)
1223 1224 if not static:
1224 1225 tp = web.templatepath or templater.templatepaths()
1225 1226 if isinstance(tp, str):
1226 1227 tp = [tp]
1227 1228 static = [os.path.join(p, 'static') for p in tp]
1228 1229
1229 1230 staticfile(static, fname, web.res)
1230 1231 return web.res.sendresponse()
1231 1232
1232 1233 @webcommand('graph')
1233 1234 def graph(web):
1234 1235 """
1235 1236 /graph[/{revision}]
1236 1237 -------------------
1237 1238
1238 1239 Show information about the graphical topology of the repository.
1239 1240
1240 1241 Information rendered by this handler can be used to create visual
1241 1242 representations of repository topology.
1242 1243
1243 1244 The ``revision`` URL parameter controls the starting changeset. If it's
1244 1245 absent, the default is ``tip``.
1245 1246
1246 1247 The ``revcount`` query string argument can define the number of changesets
1247 1248 to show information for.
1248 1249
1249 1250 The ``graphtop`` query string argument can specify the starting changeset
1250 1251 for producing ``jsdata`` variable that is used for rendering graph in
1251 1252 JavaScript. By default it has the same value as ``revision``.
1252 1253
1253 1254 This handler will render the ``graph`` template.
1254 1255 """
1255 1256
1256 1257 if 'node' in web.req.qsparams:
1257 1258 ctx = webutil.changectx(web.repo, web.req)
1258 1259 symrev = webutil.symrevorshortnode(web.req, ctx)
1259 1260 else:
1260 1261 ctx = web.repo['tip']
1261 1262 symrev = 'tip'
1262 1263 rev = ctx.rev()
1263 1264
1264 1265 bg_height = 39
1265 1266 revcount = web.maxshortchanges
1266 1267 if 'revcount' in web.req.qsparams:
1267 1268 try:
1268 1269 revcount = int(web.req.qsparams.get('revcount', revcount))
1269 1270 revcount = max(revcount, 1)
1270 1271 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1271 1272 except ValueError:
1272 1273 pass
1273 1274
1274 1275 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1275 1276 lessvars['revcount'] = max(revcount // 2, 1)
1276 1277 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1277 1278 morevars['revcount'] = revcount * 2
1278 1279
1279 1280 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1280 1281 graphvars = copy.copy(web.tmpl.defaults['sessionvars'])
1281 1282 graphvars['graphtop'] = graphtop
1282 1283
1283 1284 count = len(web.repo)
1284 1285 pos = rev
1285 1286
1286 1287 uprev = min(max(0, count - 1), rev + revcount)
1287 1288 downrev = max(0, rev - revcount)
1288 1289 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1289 1290
1290 1291 tree = []
1291 1292 nextentry = []
1292 1293 lastrev = 0
1293 1294 if pos != -1:
1294 1295 allrevs = web.repo.changelog.revs(pos, 0)
1295 1296 revs = []
1296 1297 for i in allrevs:
1297 1298 revs.append(i)
1298 1299 if len(revs) >= revcount + 1:
1299 1300 break
1300 1301
1301 1302 if len(revs) > revcount:
1302 1303 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1303 1304 revs = revs[:-1]
1304 1305
1305 1306 lastrev = revs[-1]
1306 1307
1307 1308 # We have to feed a baseset to dagwalker as it is expecting smartset
1308 1309 # object. This does not have a big impact on hgweb performance itself
1309 1310 # since hgweb graphing code is not itself lazy yet.
1310 1311 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1311 1312 # As we said one line above... not lazy.
1312 1313 tree = list(item for item in graphmod.colored(dag, web.repo)
1313 1314 if item[1] == graphmod.CHANGESET)
1314 1315
1315 1316 def nodecurrent(ctx):
1316 1317 wpnodes = web.repo.dirstate.parents()
1317 1318 if wpnodes[1] == nullid:
1318 1319 wpnodes = wpnodes[:1]
1319 1320 if ctx.node() in wpnodes:
1320 1321 return '@'
1321 1322 return ''
1322 1323
1323 1324 def nodesymbol(ctx):
1324 1325 if ctx.obsolete():
1325 1326 return 'x'
1326 1327 elif ctx.isunstable():
1327 1328 return '*'
1328 1329 elif ctx.closesbranch():
1329 1330 return '_'
1330 1331 else:
1331 1332 return 'o'
1332 1333
1333 1334 def fulltree():
1334 1335 pos = web.repo[graphtop].rev()
1335 1336 tree = []
1336 1337 if pos != -1:
1337 1338 revs = web.repo.changelog.revs(pos, lastrev)
1338 1339 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1339 1340 tree = list(item for item in graphmod.colored(dag, web.repo)
1340 1341 if item[1] == graphmod.CHANGESET)
1341 1342 return tree
1342 1343
1343 1344 def jsdata():
1344 1345 return [{'node': pycompat.bytestr(ctx),
1345 1346 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1346 1347 'vertex': vtx,
1347 1348 'edges': edges}
1348 1349 for (id, type, ctx, vtx, edges) in fulltree()]
1349 1350
1350 1351 def nodes():
1351 1352 parity = paritygen(web.stripecount)
1352 1353 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1353 1354 entry = webutil.commonentry(web.repo, ctx)
1354 1355 edgedata = [{'col': edge[0],
1355 1356 'nextcol': edge[1],
1356 1357 'color': (edge[2] - 1) % 6 + 1,
1357 1358 'width': edge[3],
1358 1359 'bcolor': edge[4]}
1359 1360 for edge in edges]
1360 1361
1361 1362 entry.update({'col': vtx[0],
1362 1363 'color': (vtx[1] - 1) % 6 + 1,
1363 1364 'parity': next(parity),
1364 1365 'edges': edgedata,
1365 1366 'row': row,
1366 1367 'nextrow': row + 1})
1367 1368
1368 1369 yield entry
1369 1370
1370 1371 rows = len(tree)
1371 1372
1372 1373 return web.sendtemplate(
1373 1374 'graph',
1374 1375 rev=rev,
1375 1376 symrev=symrev,
1376 1377 revcount=revcount,
1377 1378 uprev=uprev,
1378 1379 lessvars=lessvars,
1379 1380 morevars=morevars,
1380 1381 downrev=downrev,
1381 1382 graphvars=graphvars,
1382 1383 rows=rows,
1383 1384 bg_height=bg_height,
1384 1385 changesets=count,
1385 1386 nextentry=nextentry,
1386 1387 jsdata=lambda **x: jsdata(),
1387 1388 nodes=lambda **x: nodes(),
1388 1389 node=ctx.hex(),
1389 1390 changenav=changenav)
1390 1391
1391 1392 def _getdoc(e):
1392 1393 doc = e[0].__doc__
1393 1394 if doc:
1394 1395 doc = _(doc).partition('\n')[0]
1395 1396 else:
1396 1397 doc = _('(no help text available)')
1397 1398 return doc
1398 1399
1399 1400 @webcommand('help')
1400 1401 def help(web):
1401 1402 """
1402 1403 /help[/{topic}]
1403 1404 ---------------
1404 1405
1405 1406 Render help documentation.
1406 1407
1407 1408 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1408 1409 is defined, that help topic will be rendered. If not, an index of
1409 1410 available help topics will be rendered.
1410 1411
1411 1412 The ``help`` template will be rendered when requesting help for a topic.
1412 1413 ``helptopics`` will be rendered for the index of help topics.
1413 1414 """
1414 1415 from .. import commands, help as helpmod # avoid cycle
1415 1416
1416 1417 topicname = web.req.qsparams.get('node')
1417 1418 if not topicname:
1418 1419 def topics(**map):
1419 1420 for entries, summary, _doc in helpmod.helptable:
1420 1421 yield {'topic': entries[0], 'summary': summary}
1421 1422
1422 1423 early, other = [], []
1423 1424 primary = lambda s: s.partition('|')[0]
1424 1425 for c, e in commands.table.iteritems():
1425 1426 doc = _getdoc(e)
1426 1427 if 'DEPRECATED' in doc or c.startswith('debug'):
1427 1428 continue
1428 1429 cmd = primary(c)
1429 1430 if cmd.startswith('^'):
1430 1431 early.append((cmd[1:], doc))
1431 1432 else:
1432 1433 other.append((cmd, doc))
1433 1434
1434 1435 early.sort()
1435 1436 other.sort()
1436 1437
1437 1438 def earlycommands(**map):
1438 1439 for c, doc in early:
1439 1440 yield {'topic': c, 'summary': doc}
1440 1441
1441 1442 def othercommands(**map):
1442 1443 for c, doc in other:
1443 1444 yield {'topic': c, 'summary': doc}
1444 1445
1445 1446 return web.sendtemplate(
1446 1447 'helptopics',
1447 1448 topics=topics,
1448 1449 earlycommands=earlycommands,
1449 1450 othercommands=othercommands,
1450 1451 title='Index')
1451 1452
1452 1453 # Render an index of sub-topics.
1453 1454 if topicname in helpmod.subtopics:
1454 1455 topics = []
1455 1456 for entries, summary, _doc in helpmod.subtopics[topicname]:
1456 1457 topics.append({
1457 1458 'topic': '%s.%s' % (topicname, entries[0]),
1458 1459 'basename': entries[0],
1459 1460 'summary': summary,
1460 1461 })
1461 1462
1462 1463 return web.sendtemplate(
1463 1464 'helptopics',
1464 1465 topics=topics,
1465 1466 title=topicname,
1466 1467 subindex=True)
1467 1468
1468 1469 u = webutil.wsgiui.load()
1469 1470 u.verbose = True
1470 1471
1471 1472 # Render a page from a sub-topic.
1472 1473 if '.' in topicname:
1473 1474 # TODO implement support for rendering sections, like
1474 1475 # `hg help` works.
1475 1476 topic, subtopic = topicname.split('.', 1)
1476 1477 if topic not in helpmod.subtopics:
1477 1478 raise ErrorResponse(HTTP_NOT_FOUND)
1478 1479 else:
1479 1480 topic = topicname
1480 1481 subtopic = None
1481 1482
1482 1483 try:
1483 1484 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1484 1485 except error.Abort:
1485 1486 raise ErrorResponse(HTTP_NOT_FOUND)
1486 1487
1487 1488 return web.sendtemplate(
1488 1489 'help',
1489 1490 topic=topicname,
1490 1491 doc=doc)
1491 1492
1492 1493 # tell hggettext to extract docstrings from these functions:
1493 1494 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now