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