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