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