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