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