##// END OF EJS Templates
hgweb: drop an unused variable assignment...
Matt Harbison -
r44432:3e4294aa default
parent child Browse files
Show More
@@ -1,1599 +1,1598 b''
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 pats = []
1272 match = scmutil.match(ctx, [])
1271 match = scmutil.match(ctx, [])
1273 file = web.req.qsparams.get(b'file')
1272 file = web.req.qsparams.get(b'file')
1274 if file:
1273 if file:
1275 pats = [b'path:' + file]
1274 pats = [b'path:' + file]
1276 match = scmutil.match(ctx, pats, default=b'path')
1275 match = scmutil.match(ctx, pats, default=b'path')
1277 if pats:
1276 if pats:
1278 files = [f for f in ctx.manifest().keys() if match(f)]
1277 files = [f for f in ctx.manifest().keys() if match(f)]
1279 if not files:
1278 if not files:
1280 raise ErrorResponse(
1279 raise ErrorResponse(
1281 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1280 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1282 )
1281 )
1283
1282
1284 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1283 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1285
1284
1286 web.res.headers[b'Content-Type'] = mimetype
1285 web.res.headers[b'Content-Type'] = mimetype
1287 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1286 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1288 name,
1287 name,
1289 extension,
1288 extension,
1290 )
1289 )
1291
1290
1292 if encoding:
1291 if encoding:
1293 web.res.headers[b'Content-Encoding'] = encoding
1292 web.res.headers[b'Content-Encoding'] = encoding
1294
1293
1295 web.res.setbodywillwrite()
1294 web.res.setbodywillwrite()
1296 if list(web.res.sendresponse()):
1295 if list(web.res.sendresponse()):
1297 raise error.ProgrammingError(
1296 raise error.ProgrammingError(
1298 b'sendresponse() should not emit data if writing later'
1297 b'sendresponse() should not emit data if writing later'
1299 )
1298 )
1300
1299
1301 bodyfh = web.res.getbodyfile()
1300 bodyfh = web.res.getbodyfile()
1302
1301
1303 archival.archive(
1302 archival.archive(
1304 web.repo,
1303 web.repo,
1305 bodyfh,
1304 bodyfh,
1306 cnode,
1305 cnode,
1307 artype,
1306 artype,
1308 prefix=name,
1307 prefix=name,
1309 match=match,
1308 match=match,
1310 subrepos=web.configbool(b"web", b"archivesubrepos"),
1309 subrepos=web.configbool(b"web", b"archivesubrepos"),
1311 )
1310 )
1312
1311
1313 return []
1312 return []
1314
1313
1315
1314
1316 @webcommand(b'static')
1315 @webcommand(b'static')
1317 def static(web):
1316 def static(web):
1318 fname = web.req.qsparams[b'file']
1317 fname = web.req.qsparams[b'file']
1319 # 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
1320 # readable by the user running the CGI script
1319 # readable by the user running the CGI script
1321 static = web.config(b"web", b"static", untrusted=False)
1320 static = web.config(b"web", b"static", untrusted=False)
1322 if not static:
1321 if not static:
1323 tp = web.templatepath or templater.templatepaths()
1322 tp = web.templatepath or templater.templatepaths()
1324 if isinstance(tp, bytes):
1323 if isinstance(tp, bytes):
1325 tp = [tp]
1324 tp = [tp]
1326 static = [os.path.join(p, b'static') for p in tp]
1325 static = [os.path.join(p, b'static') for p in tp]
1327
1326
1328 staticfile(static, fname, web.res)
1327 staticfile(static, fname, web.res)
1329 return web.res.sendresponse()
1328 return web.res.sendresponse()
1330
1329
1331
1330
1332 @webcommand(b'graph')
1331 @webcommand(b'graph')
1333 def graph(web):
1332 def graph(web):
1334 """
1333 """
1335 /graph[/{revision}]
1334 /graph[/{revision}]
1336 -------------------
1335 -------------------
1337
1336
1338 Show information about the graphical topology of the repository.
1337 Show information about the graphical topology of the repository.
1339
1338
1340 Information rendered by this handler can be used to create visual
1339 Information rendered by this handler can be used to create visual
1341 representations of repository topology.
1340 representations of repository topology.
1342
1341
1343 The ``revision`` URL parameter controls the starting changeset. If it's
1342 The ``revision`` URL parameter controls the starting changeset. If it's
1344 absent, the default is ``tip``.
1343 absent, the default is ``tip``.
1345
1344
1346 The ``revcount`` query string argument can define the number of changesets
1345 The ``revcount`` query string argument can define the number of changesets
1347 to show information for.
1346 to show information for.
1348
1347
1349 The ``graphtop`` query string argument can specify the starting changeset
1348 The ``graphtop`` query string argument can specify the starting changeset
1350 for producing ``jsdata`` variable that is used for rendering graph in
1349 for producing ``jsdata`` variable that is used for rendering graph in
1351 JavaScript. By default it has the same value as ``revision``.
1350 JavaScript. By default it has the same value as ``revision``.
1352
1351
1353 This handler will render the ``graph`` template.
1352 This handler will render the ``graph`` template.
1354 """
1353 """
1355
1354
1356 if b'node' in web.req.qsparams:
1355 if b'node' in web.req.qsparams:
1357 ctx = webutil.changectx(web.repo, web.req)
1356 ctx = webutil.changectx(web.repo, web.req)
1358 symrev = webutil.symrevorshortnode(web.req, ctx)
1357 symrev = webutil.symrevorshortnode(web.req, ctx)
1359 else:
1358 else:
1360 ctx = web.repo[b'tip']
1359 ctx = web.repo[b'tip']
1361 symrev = b'tip'
1360 symrev = b'tip'
1362 rev = ctx.rev()
1361 rev = ctx.rev()
1363
1362
1364 bg_height = 39
1363 bg_height = 39
1365 revcount = web.maxshortchanges
1364 revcount = web.maxshortchanges
1366 if b'revcount' in web.req.qsparams:
1365 if b'revcount' in web.req.qsparams:
1367 try:
1366 try:
1368 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1367 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1369 revcount = max(revcount, 1)
1368 revcount = max(revcount, 1)
1370 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1369 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1371 except ValueError:
1370 except ValueError:
1372 pass
1371 pass
1373
1372
1374 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1373 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1375 lessvars[b'revcount'] = max(revcount // 2, 1)
1374 lessvars[b'revcount'] = max(revcount // 2, 1)
1376 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1375 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1377 morevars[b'revcount'] = revcount * 2
1376 morevars[b'revcount'] = revcount * 2
1378
1377
1379 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1378 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1380 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1379 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1381 graphvars[b'graphtop'] = graphtop
1380 graphvars[b'graphtop'] = graphtop
1382
1381
1383 count = len(web.repo)
1382 count = len(web.repo)
1384 pos = rev
1383 pos = rev
1385
1384
1386 uprev = min(max(0, count - 1), rev + revcount)
1385 uprev = min(max(0, count - 1), rev + revcount)
1387 downrev = max(0, rev - revcount)
1386 downrev = max(0, rev - revcount)
1388 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1387 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1389
1388
1390 tree = []
1389 tree = []
1391 nextentry = []
1390 nextentry = []
1392 lastrev = 0
1391 lastrev = 0
1393 if pos != -1:
1392 if pos != -1:
1394 allrevs = web.repo.changelog.revs(pos, 0)
1393 allrevs = web.repo.changelog.revs(pos, 0)
1395 revs = []
1394 revs = []
1396 for i in allrevs:
1395 for i in allrevs:
1397 revs.append(i)
1396 revs.append(i)
1398 if len(revs) >= revcount + 1:
1397 if len(revs) >= revcount + 1:
1399 break
1398 break
1400
1399
1401 if len(revs) > revcount:
1400 if len(revs) > revcount:
1402 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1401 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1403 revs = revs[:-1]
1402 revs = revs[:-1]
1404
1403
1405 lastrev = revs[-1]
1404 lastrev = revs[-1]
1406
1405
1407 # We have to feed a baseset to dagwalker as it is expecting smartset
1406 # We have to feed a baseset to dagwalker as it is expecting smartset
1408 # object. This does not have a big impact on hgweb performance itself
1407 # object. This does not have a big impact on hgweb performance itself
1409 # since hgweb graphing code is not itself lazy yet.
1408 # since hgweb graphing code is not itself lazy yet.
1410 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1409 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1411 # As we said one line above... not lazy.
1410 # As we said one line above... not lazy.
1412 tree = list(
1411 tree = list(
1413 item
1412 item
1414 for item in graphmod.colored(dag, web.repo)
1413 for item in graphmod.colored(dag, web.repo)
1415 if item[1] == graphmod.CHANGESET
1414 if item[1] == graphmod.CHANGESET
1416 )
1415 )
1417
1416
1418 def fulltree():
1417 def fulltree():
1419 pos = web.repo[graphtop].rev()
1418 pos = web.repo[graphtop].rev()
1420 tree = []
1419 tree = []
1421 if pos != -1:
1420 if pos != -1:
1422 revs = web.repo.changelog.revs(pos, lastrev)
1421 revs = web.repo.changelog.revs(pos, lastrev)
1423 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1422 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1424 tree = list(
1423 tree = list(
1425 item
1424 item
1426 for item in graphmod.colored(dag, web.repo)
1425 for item in graphmod.colored(dag, web.repo)
1427 if item[1] == graphmod.CHANGESET
1426 if item[1] == graphmod.CHANGESET
1428 )
1427 )
1429 return tree
1428 return tree
1430
1429
1431 def jsdata(context):
1430 def jsdata(context):
1432 for (id, type, ctx, vtx, edges) in fulltree():
1431 for (id, type, ctx, vtx, edges) in fulltree():
1433 yield {
1432 yield {
1434 b'node': pycompat.bytestr(ctx),
1433 b'node': pycompat.bytestr(ctx),
1435 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1434 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1436 b'vertex': vtx,
1435 b'vertex': vtx,
1437 b'edges': edges,
1436 b'edges': edges,
1438 }
1437 }
1439
1438
1440 def nodes(context):
1439 def nodes(context):
1441 parity = paritygen(web.stripecount)
1440 parity = paritygen(web.stripecount)
1442 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1441 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1443 entry = webutil.commonentry(web.repo, ctx)
1442 entry = webutil.commonentry(web.repo, ctx)
1444 edgedata = [
1443 edgedata = [
1445 {
1444 {
1446 b'col': edge[0],
1445 b'col': edge[0],
1447 b'nextcol': edge[1],
1446 b'nextcol': edge[1],
1448 b'color': (edge[2] - 1) % 6 + 1,
1447 b'color': (edge[2] - 1) % 6 + 1,
1449 b'width': edge[3],
1448 b'width': edge[3],
1450 b'bcolor': edge[4],
1449 b'bcolor': edge[4],
1451 }
1450 }
1452 for edge in edges
1451 for edge in edges
1453 ]
1452 ]
1454
1453
1455 entry.update(
1454 entry.update(
1456 {
1455 {
1457 b'col': vtx[0],
1456 b'col': vtx[0],
1458 b'color': (vtx[1] - 1) % 6 + 1,
1457 b'color': (vtx[1] - 1) % 6 + 1,
1459 b'parity': next(parity),
1458 b'parity': next(parity),
1460 b'edges': templateutil.mappinglist(edgedata),
1459 b'edges': templateutil.mappinglist(edgedata),
1461 b'row': row,
1460 b'row': row,
1462 b'nextrow': row + 1,
1461 b'nextrow': row + 1,
1463 }
1462 }
1464 )
1463 )
1465
1464
1466 yield entry
1465 yield entry
1467
1466
1468 rows = len(tree)
1467 rows = len(tree)
1469
1468
1470 return web.sendtemplate(
1469 return web.sendtemplate(
1471 b'graph',
1470 b'graph',
1472 rev=rev,
1471 rev=rev,
1473 symrev=symrev,
1472 symrev=symrev,
1474 revcount=revcount,
1473 revcount=revcount,
1475 uprev=uprev,
1474 uprev=uprev,
1476 lessvars=lessvars,
1475 lessvars=lessvars,
1477 morevars=morevars,
1476 morevars=morevars,
1478 downrev=downrev,
1477 downrev=downrev,
1479 graphvars=graphvars,
1478 graphvars=graphvars,
1480 rows=rows,
1479 rows=rows,
1481 bg_height=bg_height,
1480 bg_height=bg_height,
1482 changesets=count,
1481 changesets=count,
1483 nextentry=templateutil.mappinglist(nextentry),
1482 nextentry=templateutil.mappinglist(nextentry),
1484 jsdata=templateutil.mappinggenerator(jsdata),
1483 jsdata=templateutil.mappinggenerator(jsdata),
1485 nodes=templateutil.mappinggenerator(nodes),
1484 nodes=templateutil.mappinggenerator(nodes),
1486 node=ctx.hex(),
1485 node=ctx.hex(),
1487 archives=web.archivelist(b'tip'),
1486 archives=web.archivelist(b'tip'),
1488 changenav=changenav,
1487 changenav=changenav,
1489 )
1488 )
1490
1489
1491
1490
1492 def _getdoc(e):
1491 def _getdoc(e):
1493 doc = e[0].__doc__
1492 doc = e[0].__doc__
1494 if doc:
1493 if doc:
1495 doc = _(doc).partition(b'\n')[0]
1494 doc = _(doc).partition(b'\n')[0]
1496 else:
1495 else:
1497 doc = _(b'(no help text available)')
1496 doc = _(b'(no help text available)')
1498 return doc
1497 return doc
1499
1498
1500
1499
1501 @webcommand(b'help')
1500 @webcommand(b'help')
1502 def help(web):
1501 def help(web):
1503 """
1502 """
1504 /help[/{topic}]
1503 /help[/{topic}]
1505 ---------------
1504 ---------------
1506
1505
1507 Render help documentation.
1506 Render help documentation.
1508
1507
1509 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1508 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1510 is defined, that help topic will be rendered. If not, an index of
1509 is defined, that help topic will be rendered. If not, an index of
1511 available help topics will be rendered.
1510 available help topics will be rendered.
1512
1511
1513 The ``help`` template will be rendered when requesting help for a topic.
1512 The ``help`` template will be rendered when requesting help for a topic.
1514 ``helptopics`` will be rendered for the index of help topics.
1513 ``helptopics`` will be rendered for the index of help topics.
1515 """
1514 """
1516 from .. import commands, help as helpmod # avoid cycle
1515 from .. import commands, help as helpmod # avoid cycle
1517
1516
1518 topicname = web.req.qsparams.get(b'node')
1517 topicname = web.req.qsparams.get(b'node')
1519 if not topicname:
1518 if not topicname:
1520
1519
1521 def topics(context):
1520 def topics(context):
1522 for h in helpmod.helptable:
1521 for h in helpmod.helptable:
1523 entries, summary, _doc = h[0:3]
1522 entries, summary, _doc = h[0:3]
1524 yield {b'topic': entries[0], b'summary': summary}
1523 yield {b'topic': entries[0], b'summary': summary}
1525
1524
1526 early, other = [], []
1525 early, other = [], []
1527 primary = lambda s: s.partition(b'|')[0]
1526 primary = lambda s: s.partition(b'|')[0]
1528 for c, e in pycompat.iteritems(commands.table):
1527 for c, e in pycompat.iteritems(commands.table):
1529 doc = _getdoc(e)
1528 doc = _getdoc(e)
1530 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1529 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1531 continue
1530 continue
1532 cmd = primary(c)
1531 cmd = primary(c)
1533 if getattr(e[0], 'helpbasic', False):
1532 if getattr(e[0], 'helpbasic', False):
1534 early.append((cmd, doc))
1533 early.append((cmd, doc))
1535 else:
1534 else:
1536 other.append((cmd, doc))
1535 other.append((cmd, doc))
1537
1536
1538 early.sort()
1537 early.sort()
1539 other.sort()
1538 other.sort()
1540
1539
1541 def earlycommands(context):
1540 def earlycommands(context):
1542 for c, doc in early:
1541 for c, doc in early:
1543 yield {b'topic': c, b'summary': doc}
1542 yield {b'topic': c, b'summary': doc}
1544
1543
1545 def othercommands(context):
1544 def othercommands(context):
1546 for c, doc in other:
1545 for c, doc in other:
1547 yield {b'topic': c, b'summary': doc}
1546 yield {b'topic': c, b'summary': doc}
1548
1547
1549 return web.sendtemplate(
1548 return web.sendtemplate(
1550 b'helptopics',
1549 b'helptopics',
1551 topics=templateutil.mappinggenerator(topics),
1550 topics=templateutil.mappinggenerator(topics),
1552 earlycommands=templateutil.mappinggenerator(earlycommands),
1551 earlycommands=templateutil.mappinggenerator(earlycommands),
1553 othercommands=templateutil.mappinggenerator(othercommands),
1552 othercommands=templateutil.mappinggenerator(othercommands),
1554 title=b'Index',
1553 title=b'Index',
1555 )
1554 )
1556
1555
1557 # Render an index of sub-topics.
1556 # Render an index of sub-topics.
1558 if topicname in helpmod.subtopics:
1557 if topicname in helpmod.subtopics:
1559 topics = []
1558 topics = []
1560 for entries, summary, _doc in helpmod.subtopics[topicname]:
1559 for entries, summary, _doc in helpmod.subtopics[topicname]:
1561 topics.append(
1560 topics.append(
1562 {
1561 {
1563 b'topic': b'%s.%s' % (topicname, entries[0]),
1562 b'topic': b'%s.%s' % (topicname, entries[0]),
1564 b'basename': entries[0],
1563 b'basename': entries[0],
1565 b'summary': summary,
1564 b'summary': summary,
1566 }
1565 }
1567 )
1566 )
1568
1567
1569 return web.sendtemplate(
1568 return web.sendtemplate(
1570 b'helptopics',
1569 b'helptopics',
1571 topics=templateutil.mappinglist(topics),
1570 topics=templateutil.mappinglist(topics),
1572 title=topicname,
1571 title=topicname,
1573 subindex=True,
1572 subindex=True,
1574 )
1573 )
1575
1574
1576 u = webutil.wsgiui.load()
1575 u = webutil.wsgiui.load()
1577 u.verbose = True
1576 u.verbose = True
1578
1577
1579 # Render a page from a sub-topic.
1578 # Render a page from a sub-topic.
1580 if b'.' in topicname:
1579 if b'.' in topicname:
1581 # TODO implement support for rendering sections, like
1580 # TODO implement support for rendering sections, like
1582 # `hg help` works.
1581 # `hg help` works.
1583 topic, subtopic = topicname.split(b'.', 1)
1582 topic, subtopic = topicname.split(b'.', 1)
1584 if topic not in helpmod.subtopics:
1583 if topic not in helpmod.subtopics:
1585 raise ErrorResponse(HTTP_NOT_FOUND)
1584 raise ErrorResponse(HTTP_NOT_FOUND)
1586 else:
1585 else:
1587 topic = topicname
1586 topic = topicname
1588 subtopic = None
1587 subtopic = None
1589
1588
1590 try:
1589 try:
1591 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1590 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1592 except error.Abort:
1591 except error.Abort:
1593 raise ErrorResponse(HTTP_NOT_FOUND)
1592 raise ErrorResponse(HTTP_NOT_FOUND)
1594
1593
1595 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1594 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1596
1595
1597
1596
1598 # tell hggettext to extract docstrings from these functions:
1597 # tell hggettext to extract docstrings from these functions:
1599 i18nfunctions = commands.values()
1598 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now