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