##// END OF EJS Templates
hgweb: drop unused argument 'tmpl' from webutil.compare()
Yuya Nishihara -
r38013:623dc265 default
parent child Browse files
Show More
@@ -1,1476 +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(ctx.files(), n, web.maxfiles)
299 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles)
300
300
301 lm = webutil.commonentry(web.repo, ctx)
301 lm = webutil.commonentry(web.repo, ctx)
302 lm.update({
302 lm.update({
303 'parity': next(parity),
303 'parity': next(parity),
304 'changelogtag': showtags,
304 'changelogtag': showtags,
305 'files': files,
305 'files': files,
306 })
306 })
307 yield lm
307 yield lm
308
308
309 if count >= revcount:
309 if count >= revcount:
310 break
310 break
311
311
312 query = web.req.qsparams['rev']
312 query = web.req.qsparams['rev']
313 revcount = web.maxchanges
313 revcount = web.maxchanges
314 if 'revcount' in web.req.qsparams:
314 if 'revcount' in web.req.qsparams:
315 try:
315 try:
316 revcount = int(web.req.qsparams.get('revcount', revcount))
316 revcount = int(web.req.qsparams.get('revcount', revcount))
317 revcount = max(revcount, 1)
317 revcount = max(revcount, 1)
318 web.tmpl.defaults['sessionvars']['revcount'] = revcount
318 web.tmpl.defaults['sessionvars']['revcount'] = revcount
319 except ValueError:
319 except ValueError:
320 pass
320 pass
321
321
322 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
322 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
323 lessvars['revcount'] = max(revcount // 2, 1)
323 lessvars['revcount'] = max(revcount // 2, 1)
324 lessvars['rev'] = query
324 lessvars['rev'] = query
325 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
325 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
326 morevars['revcount'] = revcount * 2
326 morevars['revcount'] = revcount * 2
327 morevars['rev'] = query
327 morevars['rev'] = query
328
328
329 mode, funcarg = getsearchmode(query)
329 mode, funcarg = getsearchmode(query)
330
330
331 if 'forcekw' in web.req.qsparams:
331 if 'forcekw' in web.req.qsparams:
332 showforcekw = ''
332 showforcekw = ''
333 showunforcekw = searchfuncs[mode][1]
333 showunforcekw = searchfuncs[mode][1]
334 mode = MODE_KEYWORD
334 mode = MODE_KEYWORD
335 funcarg = query
335 funcarg = query
336 else:
336 else:
337 if mode != MODE_KEYWORD:
337 if mode != MODE_KEYWORD:
338 showforcekw = searchfuncs[MODE_KEYWORD][1]
338 showforcekw = searchfuncs[MODE_KEYWORD][1]
339 else:
339 else:
340 showforcekw = ''
340 showforcekw = ''
341 showunforcekw = ''
341 showunforcekw = ''
342
342
343 searchfunc = searchfuncs[mode]
343 searchfunc = searchfuncs[mode]
344
344
345 tip = web.repo['tip']
345 tip = web.repo['tip']
346 parity = paritygen(web.stripecount)
346 parity = paritygen(web.stripecount)
347
347
348 return web.sendtemplate(
348 return web.sendtemplate(
349 'search',
349 'search',
350 query=query,
350 query=query,
351 node=tip.hex(),
351 node=tip.hex(),
352 symrev='tip',
352 symrev='tip',
353 entries=templateutil.mappinggenerator(changelist, name='searchentry'),
353 entries=templateutil.mappinggenerator(changelist, name='searchentry'),
354 archives=web.archivelist('tip'),
354 archives=web.archivelist('tip'),
355 morevars=morevars,
355 morevars=morevars,
356 lessvars=lessvars,
356 lessvars=lessvars,
357 modedesc=searchfunc[1],
357 modedesc=searchfunc[1],
358 showforcekw=showforcekw,
358 showforcekw=showforcekw,
359 showunforcekw=showunforcekw)
359 showunforcekw=showunforcekw)
360
360
361 @webcommand('changelog')
361 @webcommand('changelog')
362 def changelog(web, shortlog=False):
362 def changelog(web, shortlog=False):
363 """
363 """
364 /changelog[/{revision}]
364 /changelog[/{revision}]
365 -----------------------
365 -----------------------
366
366
367 Show information about multiple changesets.
367 Show information about multiple changesets.
368
368
369 If the optional ``revision`` URL argument is absent, information about
369 If the optional ``revision`` URL argument is absent, information about
370 all changesets starting at ``tip`` will be rendered. If the ``revision``
370 all changesets starting at ``tip`` will be rendered. If the ``revision``
371 argument is present, changesets will be shown starting from the specified
371 argument is present, changesets will be shown starting from the specified
372 revision.
372 revision.
373
373
374 If ``revision`` is absent, the ``rev`` query string argument may be
374 If ``revision`` is absent, the ``rev`` query string argument may be
375 defined. This will perform a search for changesets.
375 defined. This will perform a search for changesets.
376
376
377 The argument for ``rev`` can be a single revision, a revision set,
377 The argument for ``rev`` can be a single revision, a revision set,
378 or a literal keyword to search for in changeset data (equivalent to
378 or a literal keyword to search for in changeset data (equivalent to
379 :hg:`log -k`).
379 :hg:`log -k`).
380
380
381 The ``revcount`` query string argument defines the maximum numbers of
381 The ``revcount`` query string argument defines the maximum numbers of
382 changesets to render.
382 changesets to render.
383
383
384 For non-searches, the ``changelog`` template will be rendered.
384 For non-searches, the ``changelog`` template will be rendered.
385 """
385 """
386
386
387 query = ''
387 query = ''
388 if 'node' in web.req.qsparams:
388 if 'node' in web.req.qsparams:
389 ctx = webutil.changectx(web.repo, web.req)
389 ctx = webutil.changectx(web.repo, web.req)
390 symrev = webutil.symrevorshortnode(web.req, ctx)
390 symrev = webutil.symrevorshortnode(web.req, ctx)
391 elif 'rev' in web.req.qsparams:
391 elif 'rev' in web.req.qsparams:
392 return _search(web)
392 return _search(web)
393 else:
393 else:
394 ctx = web.repo['tip']
394 ctx = web.repo['tip']
395 symrev = 'tip'
395 symrev = 'tip'
396
396
397 def changelist():
397 def changelist():
398 revs = []
398 revs = []
399 if pos != -1:
399 if pos != -1:
400 revs = web.repo.changelog.revs(pos, 0)
400 revs = web.repo.changelog.revs(pos, 0)
401 curcount = 0
401 curcount = 0
402 for rev in revs:
402 for rev in revs:
403 curcount += 1
403 curcount += 1
404 if curcount > revcount + 1:
404 if curcount > revcount + 1:
405 break
405 break
406
406
407 entry = webutil.changelistentry(web, web.repo[rev])
407 entry = webutil.changelistentry(web, web.repo[rev])
408 entry['parity'] = next(parity)
408 entry['parity'] = next(parity)
409 yield entry
409 yield entry
410
410
411 if shortlog:
411 if shortlog:
412 revcount = web.maxshortchanges
412 revcount = web.maxshortchanges
413 else:
413 else:
414 revcount = web.maxchanges
414 revcount = web.maxchanges
415
415
416 if 'revcount' in web.req.qsparams:
416 if 'revcount' in web.req.qsparams:
417 try:
417 try:
418 revcount = int(web.req.qsparams.get('revcount', revcount))
418 revcount = int(web.req.qsparams.get('revcount', revcount))
419 revcount = max(revcount, 1)
419 revcount = max(revcount, 1)
420 web.tmpl.defaults['sessionvars']['revcount'] = revcount
420 web.tmpl.defaults['sessionvars']['revcount'] = revcount
421 except ValueError:
421 except ValueError:
422 pass
422 pass
423
423
424 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
424 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
425 lessvars['revcount'] = max(revcount // 2, 1)
425 lessvars['revcount'] = max(revcount // 2, 1)
426 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
426 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
427 morevars['revcount'] = revcount * 2
427 morevars['revcount'] = revcount * 2
428
428
429 count = len(web.repo)
429 count = len(web.repo)
430 pos = ctx.rev()
430 pos = ctx.rev()
431 parity = paritygen(web.stripecount)
431 parity = paritygen(web.stripecount)
432
432
433 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
433 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
434
434
435 entries = list(changelist())
435 entries = list(changelist())
436 latestentry = entries[:1]
436 latestentry = entries[:1]
437 if len(entries) > revcount:
437 if len(entries) > revcount:
438 nextentry = entries[-1:]
438 nextentry = entries[-1:]
439 entries = entries[:-1]
439 entries = entries[:-1]
440 else:
440 else:
441 nextentry = []
441 nextentry = []
442
442
443 return web.sendtemplate(
443 return web.sendtemplate(
444 'shortlog' if shortlog else 'changelog',
444 'shortlog' if shortlog else 'changelog',
445 changenav=changenav,
445 changenav=changenav,
446 node=ctx.hex(),
446 node=ctx.hex(),
447 rev=pos,
447 rev=pos,
448 symrev=symrev,
448 symrev=symrev,
449 changesets=count,
449 changesets=count,
450 entries=entries,
450 entries=entries,
451 latestentry=latestentry,
451 latestentry=latestentry,
452 nextentry=nextentry,
452 nextentry=nextentry,
453 archives=web.archivelist('tip'),
453 archives=web.archivelist('tip'),
454 revcount=revcount,
454 revcount=revcount,
455 morevars=morevars,
455 morevars=morevars,
456 lessvars=lessvars,
456 lessvars=lessvars,
457 query=query)
457 query=query)
458
458
459 @webcommand('shortlog')
459 @webcommand('shortlog')
460 def shortlog(web):
460 def shortlog(web):
461 """
461 """
462 /shortlog
462 /shortlog
463 ---------
463 ---------
464
464
465 Show basic information about a set of changesets.
465 Show basic information about a set of changesets.
466
466
467 This accepts the same parameters as the ``changelog`` handler. The only
467 This accepts the same parameters as the ``changelog`` handler. The only
468 difference is the ``shortlog`` template will be rendered instead of the
468 difference is the ``shortlog`` template will be rendered instead of the
469 ``changelog`` template.
469 ``changelog`` template.
470 """
470 """
471 return changelog(web, shortlog=True)
471 return changelog(web, shortlog=True)
472
472
473 @webcommand('changeset')
473 @webcommand('changeset')
474 def changeset(web):
474 def changeset(web):
475 """
475 """
476 /changeset[/{revision}]
476 /changeset[/{revision}]
477 -----------------------
477 -----------------------
478
478
479 Show information about a single changeset.
479 Show information about a single changeset.
480
480
481 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
482 revisions`` for possible values. If not defined, the ``tip`` changeset
482 revisions`` for possible values. If not defined, the ``tip`` changeset
483 will be shown.
483 will be shown.
484
484
485 The ``changeset`` template is rendered. Contents of the ``changesettag``,
485 The ``changeset`` template is rendered. Contents of the ``changesettag``,
486 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
486 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
487 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.
488 """
488 """
489 ctx = webutil.changectx(web.repo, web.req)
489 ctx = webutil.changectx(web.repo, web.req)
490
490
491 return web.sendtemplate(
491 return web.sendtemplate(
492 'changeset',
492 'changeset',
493 **webutil.changesetentry(web, ctx))
493 **webutil.changesetentry(web, ctx))
494
494
495 rev = webcommand('rev')(changeset)
495 rev = webcommand('rev')(changeset)
496
496
497 def decodepath(path):
497 def decodepath(path):
498 """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
499 working copy.
499 working copy.
500
500
501 Extensions (e.g., largefiles) can override this to remap files in
501 Extensions (e.g., largefiles) can override this to remap files in
502 the virtual file system presented by the manifest command below."""
502 the virtual file system presented by the manifest command below."""
503 return path
503 return path
504
504
505 @webcommand('manifest')
505 @webcommand('manifest')
506 def manifest(web):
506 def manifest(web):
507 """
507 """
508 /manifest[/{revision}[/{path}]]
508 /manifest[/{revision}[/{path}]]
509 -------------------------------
509 -------------------------------
510
510
511 Show information about a directory.
511 Show information about a directory.
512
512
513 If the URL path arguments are omitted, information about the root
513 If the URL path arguments are omitted, information about the root
514 directory for the ``tip`` changeset will be shown.
514 directory for the ``tip`` changeset will be shown.
515
515
516 Because this handler can only show information for directories, it
516 Because this handler can only show information for directories, it
517 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
518 directories and files.
518 directories and files.
519
519
520 The ``manifest`` template will be rendered for this handler.
520 The ``manifest`` template will be rendered for this handler.
521 """
521 """
522 if 'node' in web.req.qsparams:
522 if 'node' in web.req.qsparams:
523 ctx = webutil.changectx(web.repo, web.req)
523 ctx = webutil.changectx(web.repo, web.req)
524 symrev = webutil.symrevorshortnode(web.req, ctx)
524 symrev = webutil.symrevorshortnode(web.req, ctx)
525 else:
525 else:
526 ctx = web.repo['tip']
526 ctx = web.repo['tip']
527 symrev = 'tip'
527 symrev = 'tip'
528 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
528 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
529 mf = ctx.manifest()
529 mf = ctx.manifest()
530 node = ctx.node()
530 node = ctx.node()
531
531
532 files = {}
532 files = {}
533 dirs = {}
533 dirs = {}
534 parity = paritygen(web.stripecount)
534 parity = paritygen(web.stripecount)
535
535
536 if path and path[-1:] != "/":
536 if path and path[-1:] != "/":
537 path += "/"
537 path += "/"
538 l = len(path)
538 l = len(path)
539 abspath = "/" + path
539 abspath = "/" + path
540
540
541 for full, n in mf.iteritems():
541 for full, n in mf.iteritems():
542 # the virtual path (working copy path) used for the full
542 # the virtual path (working copy path) used for the full
543 # (repository) path
543 # (repository) path
544 f = decodepath(full)
544 f = decodepath(full)
545
545
546 if f[:l] != path:
546 if f[:l] != path:
547 continue
547 continue
548 remain = f[l:]
548 remain = f[l:]
549 elements = remain.split('/')
549 elements = remain.split('/')
550 if len(elements) == 1:
550 if len(elements) == 1:
551 files[remain] = full
551 files[remain] = full
552 else:
552 else:
553 h = dirs # need to retain ref to dirs (root)
553 h = dirs # need to retain ref to dirs (root)
554 for elem in elements[0:-1]:
554 for elem in elements[0:-1]:
555 if elem not in h:
555 if elem not in h:
556 h[elem] = {}
556 h[elem] = {}
557 h = h[elem]
557 h = h[elem]
558 if len(h) > 1:
558 if len(h) > 1:
559 break
559 break
560 h[None] = None # denotes files present
560 h[None] = None # denotes files present
561
561
562 if mf and not files and not dirs:
562 if mf and not files and not dirs:
563 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
563 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
564
564
565 def filelist(**map):
565 def filelist(**map):
566 for f in sorted(files):
566 for f in sorted(files):
567 full = files[f]
567 full = files[f]
568
568
569 fctx = ctx.filectx(full)
569 fctx = ctx.filectx(full)
570 yield {"file": full,
570 yield {"file": full,
571 "parity": next(parity),
571 "parity": next(parity),
572 "basename": f,
572 "basename": f,
573 "date": fctx.date(),
573 "date": fctx.date(),
574 "size": fctx.size(),
574 "size": fctx.size(),
575 "permissions": mf.flags(full)}
575 "permissions": mf.flags(full)}
576
576
577 def dirlist(**map):
577 def dirlist(**map):
578 for d in sorted(dirs):
578 for d in sorted(dirs):
579
579
580 emptydirs = []
580 emptydirs = []
581 h = dirs[d]
581 h = dirs[d]
582 while isinstance(h, dict) and len(h) == 1:
582 while isinstance(h, dict) and len(h) == 1:
583 k, v = next(iter(h.items()))
583 k, v = next(iter(h.items()))
584 if v:
584 if v:
585 emptydirs.append(k)
585 emptydirs.append(k)
586 h = v
586 h = v
587
587
588 path = "%s%s" % (abspath, d)
588 path = "%s%s" % (abspath, d)
589 yield {"parity": next(parity),
589 yield {"parity": next(parity),
590 "path": path,
590 "path": path,
591 "emptydirs": "/".join(emptydirs),
591 "emptydirs": "/".join(emptydirs),
592 "basename": d}
592 "basename": d}
593
593
594 return web.sendtemplate(
594 return web.sendtemplate(
595 'manifest',
595 'manifest',
596 symrev=symrev,
596 symrev=symrev,
597 path=abspath,
597 path=abspath,
598 up=webutil.up(abspath),
598 up=webutil.up(abspath),
599 upparity=next(parity),
599 upparity=next(parity),
600 fentries=filelist,
600 fentries=filelist,
601 dentries=dirlist,
601 dentries=dirlist,
602 archives=web.archivelist(hex(node)),
602 archives=web.archivelist(hex(node)),
603 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
603 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
604
604
605 @webcommand('tags')
605 @webcommand('tags')
606 def tags(web):
606 def tags(web):
607 """
607 """
608 /tags
608 /tags
609 -----
609 -----
610
610
611 Show information about tags.
611 Show information about tags.
612
612
613 No arguments are accepted.
613 No arguments are accepted.
614
614
615 The ``tags`` template is rendered.
615 The ``tags`` template is rendered.
616 """
616 """
617 i = list(reversed(web.repo.tagslist()))
617 i = list(reversed(web.repo.tagslist()))
618 parity = paritygen(web.stripecount)
618 parity = paritygen(web.stripecount)
619
619
620 def entries(notip, latestonly, **map):
620 def entries(notip, latestonly, **map):
621 t = i
621 t = i
622 if notip:
622 if notip:
623 t = [(k, n) for k, n in i if k != "tip"]
623 t = [(k, n) for k, n in i if k != "tip"]
624 if latestonly:
624 if latestonly:
625 t = t[:1]
625 t = t[:1]
626 for k, n in t:
626 for k, n in t:
627 yield {"parity": next(parity),
627 yield {"parity": next(parity),
628 "tag": k,
628 "tag": k,
629 "date": web.repo[n].date(),
629 "date": web.repo[n].date(),
630 "node": hex(n)}
630 "node": hex(n)}
631
631
632 return web.sendtemplate(
632 return web.sendtemplate(
633 'tags',
633 'tags',
634 node=hex(web.repo.changelog.tip()),
634 node=hex(web.repo.changelog.tip()),
635 entries=lambda **x: entries(False, False, **x),
635 entries=lambda **x: entries(False, False, **x),
636 entriesnotip=lambda **x: entries(True, False, **x),
636 entriesnotip=lambda **x: entries(True, False, **x),
637 latestentry=lambda **x: entries(True, True, **x))
637 latestentry=lambda **x: entries(True, True, **x))
638
638
639 @webcommand('bookmarks')
639 @webcommand('bookmarks')
640 def bookmarks(web):
640 def bookmarks(web):
641 """
641 """
642 /bookmarks
642 /bookmarks
643 ----------
643 ----------
644
644
645 Show information about bookmarks.
645 Show information about bookmarks.
646
646
647 No arguments are accepted.
647 No arguments are accepted.
648
648
649 The ``bookmarks`` template is rendered.
649 The ``bookmarks`` template is rendered.
650 """
650 """
651 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]
652 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
652 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
653 i = sorted(i, key=sortkey, reverse=True)
653 i = sorted(i, key=sortkey, reverse=True)
654 parity = paritygen(web.stripecount)
654 parity = paritygen(web.stripecount)
655
655
656 def entries(latestonly, **map):
656 def entries(latestonly, **map):
657 t = i
657 t = i
658 if latestonly:
658 if latestonly:
659 t = i[:1]
659 t = i[:1]
660 for k, n in t:
660 for k, n in t:
661 yield {"parity": next(parity),
661 yield {"parity": next(parity),
662 "bookmark": k,
662 "bookmark": k,
663 "date": web.repo[n].date(),
663 "date": web.repo[n].date(),
664 "node": hex(n)}
664 "node": hex(n)}
665
665
666 if i:
666 if i:
667 latestrev = i[0][1]
667 latestrev = i[0][1]
668 else:
668 else:
669 latestrev = -1
669 latestrev = -1
670
670
671 return web.sendtemplate(
671 return web.sendtemplate(
672 'bookmarks',
672 'bookmarks',
673 node=hex(web.repo.changelog.tip()),
673 node=hex(web.repo.changelog.tip()),
674 lastchange=[{'date': web.repo[latestrev].date()}],
674 lastchange=[{'date': web.repo[latestrev].date()}],
675 entries=lambda **x: entries(latestonly=False, **x),
675 entries=lambda **x: entries(latestonly=False, **x),
676 latestentry=lambda **x: entries(latestonly=True, **x))
676 latestentry=lambda **x: entries(latestonly=True, **x))
677
677
678 @webcommand('branches')
678 @webcommand('branches')
679 def branches(web):
679 def branches(web):
680 """
680 """
681 /branches
681 /branches
682 ---------
682 ---------
683
683
684 Show information about branches.
684 Show information about branches.
685
685
686 All known branches are contained in the output, even closed branches.
686 All known branches are contained in the output, even closed branches.
687
687
688 No arguments are accepted.
688 No arguments are accepted.
689
689
690 The ``branches`` template is rendered.
690 The ``branches`` template is rendered.
691 """
691 """
692 entries = webutil.branchentries(web.repo, web.stripecount)
692 entries = webutil.branchentries(web.repo, web.stripecount)
693 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
693 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
694
694
695 return web.sendtemplate(
695 return web.sendtemplate(
696 'branches',
696 'branches',
697 node=hex(web.repo.changelog.tip()),
697 node=hex(web.repo.changelog.tip()),
698 entries=entries,
698 entries=entries,
699 latestentry=latestentry)
699 latestentry=latestentry)
700
700
701 @webcommand('summary')
701 @webcommand('summary')
702 def summary(web):
702 def summary(web):
703 """
703 """
704 /summary
704 /summary
705 --------
705 --------
706
706
707 Show a summary of repository state.
707 Show a summary of repository state.
708
708
709 Information about the latest changesets, bookmarks, tags, and branches
709 Information about the latest changesets, bookmarks, tags, and branches
710 is captured by this handler.
710 is captured by this handler.
711
711
712 The ``summary`` template is rendered.
712 The ``summary`` template is rendered.
713 """
713 """
714 i = reversed(web.repo.tagslist())
714 i = reversed(web.repo.tagslist())
715
715
716 def tagentries(context):
716 def tagentries(context):
717 parity = paritygen(web.stripecount)
717 parity = paritygen(web.stripecount)
718 count = 0
718 count = 0
719 for k, n in i:
719 for k, n in i:
720 if k == "tip": # skip tip
720 if k == "tip": # skip tip
721 continue
721 continue
722
722
723 count += 1
723 count += 1
724 if count > 10: # limit to 10 tags
724 if count > 10: # limit to 10 tags
725 break
725 break
726
726
727 yield {
727 yield {
728 'parity': next(parity),
728 'parity': next(parity),
729 'tag': k,
729 'tag': k,
730 'node': hex(n),
730 'node': hex(n),
731 'date': web.repo[n].date(),
731 'date': web.repo[n].date(),
732 }
732 }
733
733
734 def bookmarks(**map):
734 def bookmarks(**map):
735 parity = paritygen(web.stripecount)
735 parity = paritygen(web.stripecount)
736 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]
737 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
737 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
738 marks = sorted(marks, key=sortkey, reverse=True)
738 marks = sorted(marks, key=sortkey, reverse=True)
739 for k, n in marks[:10]: # limit to 10 bookmarks
739 for k, n in marks[:10]: # limit to 10 bookmarks
740 yield {'parity': next(parity),
740 yield {'parity': next(parity),
741 'bookmark': k,
741 'bookmark': k,
742 'date': web.repo[n].date(),
742 'date': web.repo[n].date(),
743 'node': hex(n)}
743 'node': hex(n)}
744
744
745 def changelist(context):
745 def changelist(context):
746 parity = paritygen(web.stripecount, offset=start - end)
746 parity = paritygen(web.stripecount, offset=start - end)
747 l = [] # build a list in forward order for efficiency
747 l = [] # build a list in forward order for efficiency
748 revs = []
748 revs = []
749 if start < end:
749 if start < end:
750 revs = web.repo.changelog.revs(start, end - 1)
750 revs = web.repo.changelog.revs(start, end - 1)
751 for i in revs:
751 for i in revs:
752 ctx = web.repo[i]
752 ctx = web.repo[i]
753 lm = webutil.commonentry(web.repo, ctx)
753 lm = webutil.commonentry(web.repo, ctx)
754 lm['parity'] = next(parity)
754 lm['parity'] = next(parity)
755 l.append(lm)
755 l.append(lm)
756
756
757 for entry in reversed(l):
757 for entry in reversed(l):
758 yield entry
758 yield entry
759
759
760 tip = web.repo['tip']
760 tip = web.repo['tip']
761 count = len(web.repo)
761 count = len(web.repo)
762 start = max(0, count - web.maxchanges)
762 start = max(0, count - web.maxchanges)
763 end = min(count, start + web.maxchanges)
763 end = min(count, start + web.maxchanges)
764
764
765 desc = web.config("web", "description")
765 desc = web.config("web", "description")
766 if not desc:
766 if not desc:
767 desc = 'unknown'
767 desc = 'unknown'
768 labels = web.configlist('web', 'labels')
768 labels = web.configlist('web', 'labels')
769
769
770 return web.sendtemplate(
770 return web.sendtemplate(
771 'summary',
771 'summary',
772 desc=desc,
772 desc=desc,
773 owner=get_contact(web.config) or 'unknown',
773 owner=get_contact(web.config) or 'unknown',
774 lastchange=tip.date(),
774 lastchange=tip.date(),
775 tags=templateutil.mappinggenerator(tagentries, name='tagentry'),
775 tags=templateutil.mappinggenerator(tagentries, name='tagentry'),
776 bookmarks=bookmarks,
776 bookmarks=bookmarks,
777 branches=webutil.branchentries(web.repo, web.stripecount, 10),
777 branches=webutil.branchentries(web.repo, web.stripecount, 10),
778 shortlog=templateutil.mappinggenerator(changelist,
778 shortlog=templateutil.mappinggenerator(changelist,
779 name='shortlogentry'),
779 name='shortlogentry'),
780 node=tip.hex(),
780 node=tip.hex(),
781 symrev='tip',
781 symrev='tip',
782 archives=web.archivelist('tip'),
782 archives=web.archivelist('tip'),
783 labels=templateutil.hybridlist(labels, name='label'))
783 labels=templateutil.hybridlist(labels, name='label'))
784
784
785 @webcommand('filediff')
785 @webcommand('filediff')
786 def filediff(web):
786 def filediff(web):
787 """
787 """
788 /diff/{revision}/{path}
788 /diff/{revision}/{path}
789 -----------------------
789 -----------------------
790
790
791 Show how a file changed in a particular commit.
791 Show how a file changed in a particular commit.
792
792
793 The ``filediff`` template is rendered.
793 The ``filediff`` template is rendered.
794
794
795 This handler is registered under both the ``/diff`` and ``/filediff``
795 This handler is registered under both the ``/diff`` and ``/filediff``
796 paths. ``/diff`` is used in modern code.
796 paths. ``/diff`` is used in modern code.
797 """
797 """
798 fctx, ctx = None, None
798 fctx, ctx = None, None
799 try:
799 try:
800 fctx = webutil.filectx(web.repo, web.req)
800 fctx = webutil.filectx(web.repo, web.req)
801 except LookupError:
801 except LookupError:
802 ctx = webutil.changectx(web.repo, web.req)
802 ctx = webutil.changectx(web.repo, web.req)
803 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
803 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
804 if path not in ctx.files():
804 if path not in ctx.files():
805 raise
805 raise
806
806
807 if fctx is not None:
807 if fctx is not None:
808 path = fctx.path()
808 path = fctx.path()
809 ctx = fctx.changectx()
809 ctx = fctx.changectx()
810 basectx = ctx.p1()
810 basectx = ctx.p1()
811
811
812 style = web.config('web', 'style')
812 style = web.config('web', 'style')
813 if 'style' in web.req.qsparams:
813 if 'style' in web.req.qsparams:
814 style = web.req.qsparams['style']
814 style = web.req.qsparams['style']
815
815
816 diffs = webutil.diffs(web, ctx, basectx, [path], style)
816 diffs = webutil.diffs(web, ctx, basectx, [path], style)
817 if fctx is not None:
817 if fctx is not None:
818 rename = webutil.renamelink(fctx)
818 rename = webutil.renamelink(fctx)
819 ctx = fctx
819 ctx = fctx
820 else:
820 else:
821 rename = templateutil.mappinglist([])
821 rename = templateutil.mappinglist([])
822 ctx = ctx
822 ctx = ctx
823
823
824 return web.sendtemplate(
824 return web.sendtemplate(
825 'filediff',
825 'filediff',
826 file=path,
826 file=path,
827 symrev=webutil.symrevorshortnode(web.req, ctx),
827 symrev=webutil.symrevorshortnode(web.req, ctx),
828 rename=rename,
828 rename=rename,
829 diff=diffs,
829 diff=diffs,
830 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
830 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
831
831
832 diff = webcommand('diff')(filediff)
832 diff = webcommand('diff')(filediff)
833
833
834 @webcommand('comparison')
834 @webcommand('comparison')
835 def comparison(web):
835 def comparison(web):
836 """
836 """
837 /comparison/{revision}/{path}
837 /comparison/{revision}/{path}
838 -----------------------------
838 -----------------------------
839
839
840 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
841 made on a particular revision.
841 made on a particular revision.
842
842
843 This is similar to the ``diff`` handler. However, this form features
843 This is similar to the ``diff`` handler. However, this form features
844 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.
845
845
846 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
847 context in the diff.
847 context in the diff.
848
848
849 The ``filecomparison`` template is rendered.
849 The ``filecomparison`` template is rendered.
850 """
850 """
851 ctx = webutil.changectx(web.repo, web.req)
851 ctx = webutil.changectx(web.repo, web.req)
852 if 'file' not in web.req.qsparams:
852 if 'file' not in web.req.qsparams:
853 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
853 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
854 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
854 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
855
855
856 parsecontext = lambda v: v == 'full' and -1 or int(v)
856 parsecontext = lambda v: v == 'full' and -1 or int(v)
857 if 'context' in web.req.qsparams:
857 if 'context' in web.req.qsparams:
858 context = parsecontext(web.req.qsparams['context'])
858 context = parsecontext(web.req.qsparams['context'])
859 else:
859 else:
860 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
860 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
861
861
862 def filelines(f):
862 def filelines(f):
863 if f.isbinary():
863 if f.isbinary():
864 mt = mimetypes.guess_type(f.path())[0]
864 mt = mimetypes.guess_type(f.path())[0]
865 if not mt:
865 if not mt:
866 mt = 'application/octet-stream'
866 mt = 'application/octet-stream'
867 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
867 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
868 return f.data().splitlines()
868 return f.data().splitlines()
869
869
870 fctx = None
870 fctx = None
871 parent = ctx.p1()
871 parent = ctx.p1()
872 leftrev = parent.rev()
872 leftrev = parent.rev()
873 leftnode = parent.node()
873 leftnode = parent.node()
874 rightrev = ctx.rev()
874 rightrev = ctx.rev()
875 rightnode = ctx.node()
875 rightnode = ctx.node()
876 if path in ctx:
876 if path in ctx:
877 fctx = ctx[path]
877 fctx = ctx[path]
878 rightlines = filelines(fctx)
878 rightlines = filelines(fctx)
879 if path not in parent:
879 if path not in parent:
880 leftlines = ()
880 leftlines = ()
881 else:
881 else:
882 pfctx = parent[path]
882 pfctx = parent[path]
883 leftlines = filelines(pfctx)
883 leftlines = filelines(pfctx)
884 else:
884 else:
885 rightlines = ()
885 rightlines = ()
886 pfctx = ctx.parents()[0][path]
886 pfctx = ctx.parents()[0][path]
887 leftlines = filelines(pfctx)
887 leftlines = filelines(pfctx)
888
888
889 comparison = webutil.compare(web.tmpl, context, leftlines, rightlines)
889 comparison = webutil.compare(context, leftlines, rightlines)
890 if fctx is not None:
890 if fctx is not None:
891 rename = webutil.renamelink(fctx)
891 rename = webutil.renamelink(fctx)
892 ctx = fctx
892 ctx = fctx
893 else:
893 else:
894 rename = templateutil.mappinglist([])
894 rename = templateutil.mappinglist([])
895 ctx = ctx
895 ctx = ctx
896
896
897 return web.sendtemplate(
897 return web.sendtemplate(
898 'filecomparison',
898 'filecomparison',
899 file=path,
899 file=path,
900 symrev=webutil.symrevorshortnode(web.req, ctx),
900 symrev=webutil.symrevorshortnode(web.req, ctx),
901 rename=rename,
901 rename=rename,
902 leftrev=leftrev,
902 leftrev=leftrev,
903 leftnode=hex(leftnode),
903 leftnode=hex(leftnode),
904 rightrev=rightrev,
904 rightrev=rightrev,
905 rightnode=hex(rightnode),
905 rightnode=hex(rightnode),
906 comparison=comparison,
906 comparison=comparison,
907 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
907 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
908
908
909 @webcommand('annotate')
909 @webcommand('annotate')
910 def annotate(web):
910 def annotate(web):
911 """
911 """
912 /annotate/{revision}/{path}
912 /annotate/{revision}/{path}
913 ---------------------------
913 ---------------------------
914
914
915 Show changeset information for each line in a file.
915 Show changeset information for each line in a file.
916
916
917 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
917 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
918 ``ignoreblanklines`` query string arguments have the same meaning as
918 ``ignoreblanklines`` query string arguments have the same meaning as
919 their ``[annotate]`` config equivalents. It uses the hgrc boolean
919 their ``[annotate]`` config equivalents. It uses the hgrc boolean
920 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
921 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
922 default settings are used.
922 default settings are used.
923
923
924 The ``fileannotate`` template is rendered.
924 The ``fileannotate`` template is rendered.
925 """
925 """
926 fctx = webutil.filectx(web.repo, web.req)
926 fctx = webutil.filectx(web.repo, web.req)
927 f = fctx.path()
927 f = fctx.path()
928 parity = paritygen(web.stripecount)
928 parity = paritygen(web.stripecount)
929 ishead = fctx.filerev() in fctx.filelog().headrevs()
929 ishead = fctx.filerev() in fctx.filelog().headrevs()
930
930
931 # 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
932 # same revision. So it is worth caching.
932 # same revision. So it is worth caching.
933 # TODO there are still redundant operations within basefilectx.parents()
933 # TODO there are still redundant operations within basefilectx.parents()
934 # and from the fctx.annotate() call itself that could be cached.
934 # and from the fctx.annotate() call itself that could be cached.
935 parentscache = {}
935 parentscache = {}
936 def parents(f):
936 def parents(f):
937 rev = f.rev()
937 rev = f.rev()
938 if rev not in parentscache:
938 if rev not in parentscache:
939 parentscache[rev] = []
939 parentscache[rev] = []
940 for p in f.parents():
940 for p in f.parents():
941 entry = {
941 entry = {
942 'node': p.hex(),
942 'node': p.hex(),
943 'rev': p.rev(),
943 'rev': p.rev(),
944 }
944 }
945 parentscache[rev].append(entry)
945 parentscache[rev].append(entry)
946
946
947 for p in parentscache[rev]:
947 for p in parentscache[rev]:
948 yield p
948 yield p
949
949
950 def annotate(**map):
950 def annotate(**map):
951 if fctx.isbinary():
951 if fctx.isbinary():
952 mt = (mimetypes.guess_type(fctx.path())[0]
952 mt = (mimetypes.guess_type(fctx.path())[0]
953 or 'application/octet-stream')
953 or 'application/octet-stream')
954 lines = [dagop.annotateline(fctx=fctx.filectx(fctx.filerev()),
954 lines = [dagop.annotateline(fctx=fctx.filectx(fctx.filerev()),
955 lineno=1, text='(binary:%s)' % mt)]
955 lineno=1, text='(binary:%s)' % mt)]
956 else:
956 else:
957 lines = webutil.annotate(web.req, fctx, web.repo.ui)
957 lines = webutil.annotate(web.req, fctx, web.repo.ui)
958
958
959 previousrev = None
959 previousrev = None
960 blockparitygen = paritygen(1)
960 blockparitygen = paritygen(1)
961 for lineno, aline in enumerate(lines):
961 for lineno, aline in enumerate(lines):
962 f = aline.fctx
962 f = aline.fctx
963 rev = f.rev()
963 rev = f.rev()
964 if rev != previousrev:
964 if rev != previousrev:
965 blockhead = True
965 blockhead = True
966 blockparity = next(blockparitygen)
966 blockparity = next(blockparitygen)
967 else:
967 else:
968 blockhead = None
968 blockhead = None
969 previousrev = rev
969 previousrev = rev
970 yield {"parity": next(parity),
970 yield {"parity": next(parity),
971 "node": f.hex(),
971 "node": f.hex(),
972 "rev": rev,
972 "rev": rev,
973 "author": f.user(),
973 "author": f.user(),
974 "parents": parents(f),
974 "parents": parents(f),
975 "desc": f.description(),
975 "desc": f.description(),
976 "extra": f.extra(),
976 "extra": f.extra(),
977 "file": f.path(),
977 "file": f.path(),
978 "blockhead": blockhead,
978 "blockhead": blockhead,
979 "blockparity": blockparity,
979 "blockparity": blockparity,
980 "targetline": aline.lineno,
980 "targetline": aline.lineno,
981 "line": aline.text,
981 "line": aline.text,
982 "lineno": lineno + 1,
982 "lineno": lineno + 1,
983 "lineid": "l%d" % (lineno + 1),
983 "lineid": "l%d" % (lineno + 1),
984 "linenumber": "% 6d" % (lineno + 1),
984 "linenumber": "% 6d" % (lineno + 1),
985 "revdate": f.date()}
985 "revdate": f.date()}
986
986
987 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
987 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
988 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
988 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
989
989
990 return web.sendtemplate(
990 return web.sendtemplate(
991 'fileannotate',
991 'fileannotate',
992 file=f,
992 file=f,
993 annotate=annotate,
993 annotate=annotate,
994 path=webutil.up(f),
994 path=webutil.up(f),
995 symrev=webutil.symrevorshortnode(web.req, fctx),
995 symrev=webutil.symrevorshortnode(web.req, fctx),
996 rename=webutil.renamelink(fctx),
996 rename=webutil.renamelink(fctx),
997 permissions=fctx.manifest().flags(f),
997 permissions=fctx.manifest().flags(f),
998 ishead=int(ishead),
998 ishead=int(ishead),
999 diffopts=diffopts,
999 diffopts=diffopts,
1000 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1000 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1001
1001
1002 @webcommand('filelog')
1002 @webcommand('filelog')
1003 def filelog(web):
1003 def filelog(web):
1004 """
1004 """
1005 /filelog/{revision}/{path}
1005 /filelog/{revision}/{path}
1006 --------------------------
1006 --------------------------
1007
1007
1008 Show information about the history of a file in the repository.
1008 Show information about the history of a file in the repository.
1009
1009
1010 The ``revcount`` query string argument can be defined to control the
1010 The ``revcount`` query string argument can be defined to control the
1011 maximum number of entries to show.
1011 maximum number of entries to show.
1012
1012
1013 The ``filelog`` template will be rendered.
1013 The ``filelog`` template will be rendered.
1014 """
1014 """
1015
1015
1016 try:
1016 try:
1017 fctx = webutil.filectx(web.repo, web.req)
1017 fctx = webutil.filectx(web.repo, web.req)
1018 f = fctx.path()
1018 f = fctx.path()
1019 fl = fctx.filelog()
1019 fl = fctx.filelog()
1020 except error.LookupError:
1020 except error.LookupError:
1021 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1021 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1022 fl = web.repo.file(f)
1022 fl = web.repo.file(f)
1023 numrevs = len(fl)
1023 numrevs = len(fl)
1024 if not numrevs: # file doesn't exist at all
1024 if not numrevs: # file doesn't exist at all
1025 raise
1025 raise
1026 rev = webutil.changectx(web.repo, web.req).rev()
1026 rev = webutil.changectx(web.repo, web.req).rev()
1027 first = fl.linkrev(0)
1027 first = fl.linkrev(0)
1028 if rev < first: # current rev is from before file existed
1028 if rev < first: # current rev is from before file existed
1029 raise
1029 raise
1030 frev = numrevs - 1
1030 frev = numrevs - 1
1031 while fl.linkrev(frev) > rev:
1031 while fl.linkrev(frev) > rev:
1032 frev -= 1
1032 frev -= 1
1033 fctx = web.repo.filectx(f, fl.linkrev(frev))
1033 fctx = web.repo.filectx(f, fl.linkrev(frev))
1034
1034
1035 revcount = web.maxshortchanges
1035 revcount = web.maxshortchanges
1036 if 'revcount' in web.req.qsparams:
1036 if 'revcount' in web.req.qsparams:
1037 try:
1037 try:
1038 revcount = int(web.req.qsparams.get('revcount', revcount))
1038 revcount = int(web.req.qsparams.get('revcount', revcount))
1039 revcount = max(revcount, 1)
1039 revcount = max(revcount, 1)
1040 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1040 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1041 except ValueError:
1041 except ValueError:
1042 pass
1042 pass
1043
1043
1044 lrange = webutil.linerange(web.req)
1044 lrange = webutil.linerange(web.req)
1045
1045
1046 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1046 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1047 lessvars['revcount'] = max(revcount // 2, 1)
1047 lessvars['revcount'] = max(revcount // 2, 1)
1048 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1048 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1049 morevars['revcount'] = revcount * 2
1049 morevars['revcount'] = revcount * 2
1050
1050
1051 patch = 'patch' in web.req.qsparams
1051 patch = 'patch' in web.req.qsparams
1052 if patch:
1052 if patch:
1053 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1053 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1054 descend = 'descend' in web.req.qsparams
1054 descend = 'descend' in web.req.qsparams
1055 if descend:
1055 if descend:
1056 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1056 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1057
1057
1058 count = fctx.filerev() + 1
1058 count = fctx.filerev() + 1
1059 start = max(0, count - revcount) # first rev on this page
1059 start = max(0, count - revcount) # first rev on this page
1060 end = min(count, start + revcount) # last rev on this page
1060 end = min(count, start + revcount) # last rev on this page
1061 parity = paritygen(web.stripecount, offset=start - end)
1061 parity = paritygen(web.stripecount, offset=start - end)
1062
1062
1063 repo = web.repo
1063 repo = web.repo
1064 filelog = fctx.filelog()
1064 filelog = fctx.filelog()
1065 revs = [filerev for filerev in filelog.revs(start, end - 1)
1065 revs = [filerev for filerev in filelog.revs(start, end - 1)
1066 if filelog.linkrev(filerev) in repo]
1066 if filelog.linkrev(filerev) in repo]
1067 entries = []
1067 entries = []
1068
1068
1069 diffstyle = web.config('web', 'style')
1069 diffstyle = web.config('web', 'style')
1070 if 'style' in web.req.qsparams:
1070 if 'style' in web.req.qsparams:
1071 diffstyle = web.req.qsparams['style']
1071 diffstyle = web.req.qsparams['style']
1072
1072
1073 def diff(fctx, linerange=None):
1073 def diff(fctx, linerange=None):
1074 ctx = fctx.changectx()
1074 ctx = fctx.changectx()
1075 basectx = ctx.p1()
1075 basectx = ctx.p1()
1076 path = fctx.path()
1076 path = fctx.path()
1077 return webutil.diffs(web, ctx, basectx, [path], diffstyle,
1077 return webutil.diffs(web, ctx, basectx, [path], diffstyle,
1078 linerange=linerange,
1078 linerange=linerange,
1079 lineidprefix='%s-' % ctx.hex()[:12])
1079 lineidprefix='%s-' % ctx.hex()[:12])
1080
1080
1081 linerange = None
1081 linerange = None
1082 if lrange is not None:
1082 if lrange is not None:
1083 linerange = webutil.formatlinerange(*lrange)
1083 linerange = webutil.formatlinerange(*lrange)
1084 # deactivate numeric nav links when linerange is specified as this
1084 # deactivate numeric nav links when linerange is specified as this
1085 # would required a dedicated "revnav" class
1085 # would required a dedicated "revnav" class
1086 nav = templateutil.mappinglist([])
1086 nav = templateutil.mappinglist([])
1087 if descend:
1087 if descend:
1088 it = dagop.blockdescendants(fctx, *lrange)
1088 it = dagop.blockdescendants(fctx, *lrange)
1089 else:
1089 else:
1090 it = dagop.blockancestors(fctx, *lrange)
1090 it = dagop.blockancestors(fctx, *lrange)
1091 for i, (c, lr) in enumerate(it, 1):
1091 for i, (c, lr) in enumerate(it, 1):
1092 diffs = None
1092 diffs = None
1093 if patch:
1093 if patch:
1094 diffs = diff(c, linerange=lr)
1094 diffs = diff(c, linerange=lr)
1095 # follow renames accross filtered (not in range) revisions
1095 # follow renames accross filtered (not in range) revisions
1096 path = c.path()
1096 path = c.path()
1097 entries.append(dict(
1097 entries.append(dict(
1098 parity=next(parity),
1098 parity=next(parity),
1099 filerev=c.rev(),
1099 filerev=c.rev(),
1100 file=path,
1100 file=path,
1101 diff=diffs,
1101 diff=diffs,
1102 linerange=webutil.formatlinerange(*lr),
1102 linerange=webutil.formatlinerange(*lr),
1103 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1103 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1104 if i == revcount:
1104 if i == revcount:
1105 break
1105 break
1106 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1106 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1107 morevars['linerange'] = lessvars['linerange']
1107 morevars['linerange'] = lessvars['linerange']
1108 else:
1108 else:
1109 for i in revs:
1109 for i in revs:
1110 iterfctx = fctx.filectx(i)
1110 iterfctx = fctx.filectx(i)
1111 diffs = None
1111 diffs = None
1112 if patch:
1112 if patch:
1113 diffs = diff(iterfctx)
1113 diffs = diff(iterfctx)
1114 entries.append(dict(
1114 entries.append(dict(
1115 parity=next(parity),
1115 parity=next(parity),
1116 filerev=i,
1116 filerev=i,
1117 file=f,
1117 file=f,
1118 diff=diffs,
1118 diff=diffs,
1119 rename=webutil.renamelink(iterfctx),
1119 rename=webutil.renamelink(iterfctx),
1120 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1120 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1121 entries.reverse()
1121 entries.reverse()
1122 revnav = webutil.filerevnav(web.repo, fctx.path())
1122 revnav = webutil.filerevnav(web.repo, fctx.path())
1123 nav = revnav.gen(end - 1, revcount, count)
1123 nav = revnav.gen(end - 1, revcount, count)
1124
1124
1125 latestentry = entries[:1]
1125 latestentry = entries[:1]
1126
1126
1127 return web.sendtemplate(
1127 return web.sendtemplate(
1128 'filelog',
1128 'filelog',
1129 file=f,
1129 file=f,
1130 nav=nav,
1130 nav=nav,
1131 symrev=webutil.symrevorshortnode(web.req, fctx),
1131 symrev=webutil.symrevorshortnode(web.req, fctx),
1132 entries=entries,
1132 entries=entries,
1133 descend=descend,
1133 descend=descend,
1134 patch=patch,
1134 patch=patch,
1135 latestentry=latestentry,
1135 latestentry=latestentry,
1136 linerange=linerange,
1136 linerange=linerange,
1137 revcount=revcount,
1137 revcount=revcount,
1138 morevars=morevars,
1138 morevars=morevars,
1139 lessvars=lessvars,
1139 lessvars=lessvars,
1140 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1140 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1141
1141
1142 @webcommand('archive')
1142 @webcommand('archive')
1143 def archive(web):
1143 def archive(web):
1144 """
1144 """
1145 /archive/{revision}.{format}[/{path}]
1145 /archive/{revision}.{format}[/{path}]
1146 -------------------------------------
1146 -------------------------------------
1147
1147
1148 Obtain an archive of repository content.
1148 Obtain an archive of repository content.
1149
1149
1150 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.
1151 ``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.
1152 ``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
1153 server configuration.
1153 server configuration.
1154
1154
1155 The optional ``path`` URL parameter controls content to include in the
1155 The optional ``path`` URL parameter controls content to include in the
1156 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
1157 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
1158 directory will be included in the archive.
1158 directory will be included in the archive.
1159
1159
1160 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.
1161 """
1161 """
1162
1162
1163 type_ = web.req.qsparams.get('type')
1163 type_ = web.req.qsparams.get('type')
1164 allowed = web.configlist("web", "allow_archive")
1164 allowed = web.configlist("web", "allow_archive")
1165 key = web.req.qsparams['node']
1165 key = web.req.qsparams['node']
1166
1166
1167 if type_ not in webutil.archivespecs:
1167 if type_ not in webutil.archivespecs:
1168 msg = 'Unsupported archive type: %s' % type_
1168 msg = 'Unsupported archive type: %s' % type_
1169 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1169 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1170
1170
1171 if not ((type_ in allowed or
1171 if not ((type_ in allowed or
1172 web.configbool("web", "allow" + type_))):
1172 web.configbool("web", "allow" + type_))):
1173 msg = 'Archive type not allowed: %s' % type_
1173 msg = 'Archive type not allowed: %s' % type_
1174 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1174 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1175
1175
1176 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1176 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1177 cnode = web.repo.lookup(key)
1177 cnode = web.repo.lookup(key)
1178 arch_version = key
1178 arch_version = key
1179 if cnode == key or key == 'tip':
1179 if cnode == key or key == 'tip':
1180 arch_version = short(cnode)
1180 arch_version = short(cnode)
1181 name = "%s-%s" % (reponame, arch_version)
1181 name = "%s-%s" % (reponame, arch_version)
1182
1182
1183 ctx = webutil.changectx(web.repo, web.req)
1183 ctx = webutil.changectx(web.repo, web.req)
1184 pats = []
1184 pats = []
1185 match = scmutil.match(ctx, [])
1185 match = scmutil.match(ctx, [])
1186 file = web.req.qsparams.get('file')
1186 file = web.req.qsparams.get('file')
1187 if file:
1187 if file:
1188 pats = ['path:' + file]
1188 pats = ['path:' + file]
1189 match = scmutil.match(ctx, pats, default='path')
1189 match = scmutil.match(ctx, pats, default='path')
1190 if pats:
1190 if pats:
1191 files = [f for f in ctx.manifest().keys() if match(f)]
1191 files = [f for f in ctx.manifest().keys() if match(f)]
1192 if not files:
1192 if not files:
1193 raise ErrorResponse(HTTP_NOT_FOUND,
1193 raise ErrorResponse(HTTP_NOT_FOUND,
1194 'file(s) not found: %s' % file)
1194 'file(s) not found: %s' % file)
1195
1195
1196 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1196 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1197
1197
1198 web.res.headers['Content-Type'] = mimetype
1198 web.res.headers['Content-Type'] = mimetype
1199 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1199 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1200 name, extension)
1200 name, extension)
1201
1201
1202 if encoding:
1202 if encoding:
1203 web.res.headers['Content-Encoding'] = encoding
1203 web.res.headers['Content-Encoding'] = encoding
1204
1204
1205 web.res.setbodywillwrite()
1205 web.res.setbodywillwrite()
1206 if list(web.res.sendresponse()):
1206 if list(web.res.sendresponse()):
1207 raise error.ProgrammingError('sendresponse() should not emit data '
1207 raise error.ProgrammingError('sendresponse() should not emit data '
1208 'if writing later')
1208 'if writing later')
1209
1209
1210 bodyfh = web.res.getbodyfile()
1210 bodyfh = web.res.getbodyfile()
1211
1211
1212 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1212 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1213 matchfn=match,
1213 matchfn=match,
1214 subrepos=web.configbool("web", "archivesubrepos"))
1214 subrepos=web.configbool("web", "archivesubrepos"))
1215
1215
1216 return []
1216 return []
1217
1217
1218 @webcommand('static')
1218 @webcommand('static')
1219 def static(web):
1219 def static(web):
1220 fname = web.req.qsparams['file']
1220 fname = web.req.qsparams['file']
1221 # 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
1222 # readable by the user running the CGI script
1222 # readable by the user running the CGI script
1223 static = web.config("web", "static", None, untrusted=False)
1223 static = web.config("web", "static", None, untrusted=False)
1224 if not static:
1224 if not static:
1225 tp = web.templatepath or templater.templatepaths()
1225 tp = web.templatepath or templater.templatepaths()
1226 if isinstance(tp, str):
1226 if isinstance(tp, str):
1227 tp = [tp]
1227 tp = [tp]
1228 static = [os.path.join(p, 'static') for p in tp]
1228 static = [os.path.join(p, 'static') for p in tp]
1229
1229
1230 staticfile(static, fname, web.res)
1230 staticfile(static, fname, web.res)
1231 return web.res.sendresponse()
1231 return web.res.sendresponse()
1232
1232
1233 @webcommand('graph')
1233 @webcommand('graph')
1234 def graph(web):
1234 def graph(web):
1235 """
1235 """
1236 /graph[/{revision}]
1236 /graph[/{revision}]
1237 -------------------
1237 -------------------
1238
1238
1239 Show information about the graphical topology of the repository.
1239 Show information about the graphical topology of the repository.
1240
1240
1241 Information rendered by this handler can be used to create visual
1241 Information rendered by this handler can be used to create visual
1242 representations of repository topology.
1242 representations of repository topology.
1243
1243
1244 The ``revision`` URL parameter controls the starting changeset. If it's
1244 The ``revision`` URL parameter controls the starting changeset. If it's
1245 absent, the default is ``tip``.
1245 absent, the default is ``tip``.
1246
1246
1247 The ``revcount`` query string argument can define the number of changesets
1247 The ``revcount`` query string argument can define the number of changesets
1248 to show information for.
1248 to show information for.
1249
1249
1250 The ``graphtop`` query string argument can specify the starting changeset
1250 The ``graphtop`` query string argument can specify the starting changeset
1251 for producing ``jsdata`` variable that is used for rendering graph in
1251 for producing ``jsdata`` variable that is used for rendering graph in
1252 JavaScript. By default it has the same value as ``revision``.
1252 JavaScript. By default it has the same value as ``revision``.
1253
1253
1254 This handler will render the ``graph`` template.
1254 This handler will render the ``graph`` template.
1255 """
1255 """
1256
1256
1257 if 'node' in web.req.qsparams:
1257 if 'node' in web.req.qsparams:
1258 ctx = webutil.changectx(web.repo, web.req)
1258 ctx = webutil.changectx(web.repo, web.req)
1259 symrev = webutil.symrevorshortnode(web.req, ctx)
1259 symrev = webutil.symrevorshortnode(web.req, ctx)
1260 else:
1260 else:
1261 ctx = web.repo['tip']
1261 ctx = web.repo['tip']
1262 symrev = 'tip'
1262 symrev = 'tip'
1263 rev = ctx.rev()
1263 rev = ctx.rev()
1264
1264
1265 bg_height = 39
1265 bg_height = 39
1266 revcount = web.maxshortchanges
1266 revcount = web.maxshortchanges
1267 if 'revcount' in web.req.qsparams:
1267 if 'revcount' in web.req.qsparams:
1268 try:
1268 try:
1269 revcount = int(web.req.qsparams.get('revcount', revcount))
1269 revcount = int(web.req.qsparams.get('revcount', revcount))
1270 revcount = max(revcount, 1)
1270 revcount = max(revcount, 1)
1271 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1271 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1272 except ValueError:
1272 except ValueError:
1273 pass
1273 pass
1274
1274
1275 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1275 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1276 lessvars['revcount'] = max(revcount // 2, 1)
1276 lessvars['revcount'] = max(revcount // 2, 1)
1277 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1277 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1278 morevars['revcount'] = revcount * 2
1278 morevars['revcount'] = revcount * 2
1279
1279
1280 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1280 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1281 graphvars = copy.copy(web.tmpl.defaults['sessionvars'])
1281 graphvars = copy.copy(web.tmpl.defaults['sessionvars'])
1282 graphvars['graphtop'] = graphtop
1282 graphvars['graphtop'] = graphtop
1283
1283
1284 count = len(web.repo)
1284 count = len(web.repo)
1285 pos = rev
1285 pos = rev
1286
1286
1287 uprev = min(max(0, count - 1), rev + revcount)
1287 uprev = min(max(0, count - 1), rev + revcount)
1288 downrev = max(0, rev - revcount)
1288 downrev = max(0, rev - revcount)
1289 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1289 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1290
1290
1291 tree = []
1291 tree = []
1292 nextentry = []
1292 nextentry = []
1293 lastrev = 0
1293 lastrev = 0
1294 if pos != -1:
1294 if pos != -1:
1295 allrevs = web.repo.changelog.revs(pos, 0)
1295 allrevs = web.repo.changelog.revs(pos, 0)
1296 revs = []
1296 revs = []
1297 for i in allrevs:
1297 for i in allrevs:
1298 revs.append(i)
1298 revs.append(i)
1299 if len(revs) >= revcount + 1:
1299 if len(revs) >= revcount + 1:
1300 break
1300 break
1301
1301
1302 if len(revs) > revcount:
1302 if len(revs) > revcount:
1303 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1303 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1304 revs = revs[:-1]
1304 revs = revs[:-1]
1305
1305
1306 lastrev = revs[-1]
1306 lastrev = revs[-1]
1307
1307
1308 # 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
1309 # 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
1310 # since hgweb graphing code is not itself lazy yet.
1310 # since hgweb graphing code is not itself lazy yet.
1311 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1311 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1312 # As we said one line above... not lazy.
1312 # As we said one line above... not lazy.
1313 tree = list(item for item in graphmod.colored(dag, web.repo)
1313 tree = list(item for item in graphmod.colored(dag, web.repo)
1314 if item[1] == graphmod.CHANGESET)
1314 if item[1] == graphmod.CHANGESET)
1315
1315
1316 def fulltree():
1316 def fulltree():
1317 pos = web.repo[graphtop].rev()
1317 pos = web.repo[graphtop].rev()
1318 tree = []
1318 tree = []
1319 if pos != -1:
1319 if pos != -1:
1320 revs = web.repo.changelog.revs(pos, lastrev)
1320 revs = web.repo.changelog.revs(pos, lastrev)
1321 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1321 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1322 tree = list(item for item in graphmod.colored(dag, web.repo)
1322 tree = list(item for item in graphmod.colored(dag, web.repo)
1323 if item[1] == graphmod.CHANGESET)
1323 if item[1] == graphmod.CHANGESET)
1324 return tree
1324 return tree
1325
1325
1326 def jsdata():
1326 def jsdata():
1327 return [{'node': pycompat.bytestr(ctx),
1327 return [{'node': pycompat.bytestr(ctx),
1328 'graphnode': webutil.getgraphnode(web.repo, ctx),
1328 'graphnode': webutil.getgraphnode(web.repo, ctx),
1329 'vertex': vtx,
1329 'vertex': vtx,
1330 'edges': edges}
1330 'edges': edges}
1331 for (id, type, ctx, vtx, edges) in fulltree()]
1331 for (id, type, ctx, vtx, edges) in fulltree()]
1332
1332
1333 def nodes():
1333 def nodes():
1334 parity = paritygen(web.stripecount)
1334 parity = paritygen(web.stripecount)
1335 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1335 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1336 entry = webutil.commonentry(web.repo, ctx)
1336 entry = webutil.commonentry(web.repo, ctx)
1337 edgedata = [{'col': edge[0],
1337 edgedata = [{'col': edge[0],
1338 'nextcol': edge[1],
1338 'nextcol': edge[1],
1339 'color': (edge[2] - 1) % 6 + 1,
1339 'color': (edge[2] - 1) % 6 + 1,
1340 'width': edge[3],
1340 'width': edge[3],
1341 'bcolor': edge[4]}
1341 'bcolor': edge[4]}
1342 for edge in edges]
1342 for edge in edges]
1343
1343
1344 entry.update({'col': vtx[0],
1344 entry.update({'col': vtx[0],
1345 'color': (vtx[1] - 1) % 6 + 1,
1345 'color': (vtx[1] - 1) % 6 + 1,
1346 'parity': next(parity),
1346 'parity': next(parity),
1347 'edges': edgedata,
1347 'edges': edgedata,
1348 'row': row,
1348 'row': row,
1349 'nextrow': row + 1})
1349 'nextrow': row + 1})
1350
1350
1351 yield entry
1351 yield entry
1352
1352
1353 rows = len(tree)
1353 rows = len(tree)
1354
1354
1355 return web.sendtemplate(
1355 return web.sendtemplate(
1356 'graph',
1356 'graph',
1357 rev=rev,
1357 rev=rev,
1358 symrev=symrev,
1358 symrev=symrev,
1359 revcount=revcount,
1359 revcount=revcount,
1360 uprev=uprev,
1360 uprev=uprev,
1361 lessvars=lessvars,
1361 lessvars=lessvars,
1362 morevars=morevars,
1362 morevars=morevars,
1363 downrev=downrev,
1363 downrev=downrev,
1364 graphvars=graphvars,
1364 graphvars=graphvars,
1365 rows=rows,
1365 rows=rows,
1366 bg_height=bg_height,
1366 bg_height=bg_height,
1367 changesets=count,
1367 changesets=count,
1368 nextentry=nextentry,
1368 nextentry=nextentry,
1369 jsdata=lambda **x: jsdata(),
1369 jsdata=lambda **x: jsdata(),
1370 nodes=lambda **x: nodes(),
1370 nodes=lambda **x: nodes(),
1371 node=ctx.hex(),
1371 node=ctx.hex(),
1372 changenav=changenav)
1372 changenav=changenav)
1373
1373
1374 def _getdoc(e):
1374 def _getdoc(e):
1375 doc = e[0].__doc__
1375 doc = e[0].__doc__
1376 if doc:
1376 if doc:
1377 doc = _(doc).partition('\n')[0]
1377 doc = _(doc).partition('\n')[0]
1378 else:
1378 else:
1379 doc = _('(no help text available)')
1379 doc = _('(no help text available)')
1380 return doc
1380 return doc
1381
1381
1382 @webcommand('help')
1382 @webcommand('help')
1383 def help(web):
1383 def help(web):
1384 """
1384 """
1385 /help[/{topic}]
1385 /help[/{topic}]
1386 ---------------
1386 ---------------
1387
1387
1388 Render help documentation.
1388 Render help documentation.
1389
1389
1390 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``
1391 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
1392 available help topics will be rendered.
1392 available help topics will be rendered.
1393
1393
1394 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.
1395 ``helptopics`` will be rendered for the index of help topics.
1395 ``helptopics`` will be rendered for the index of help topics.
1396 """
1396 """
1397 from .. import commands, help as helpmod # avoid cycle
1397 from .. import commands, help as helpmod # avoid cycle
1398
1398
1399 topicname = web.req.qsparams.get('node')
1399 topicname = web.req.qsparams.get('node')
1400 if not topicname:
1400 if not topicname:
1401 def topics(**map):
1401 def topics(**map):
1402 for entries, summary, _doc in helpmod.helptable:
1402 for entries, summary, _doc in helpmod.helptable:
1403 yield {'topic': entries[0], 'summary': summary}
1403 yield {'topic': entries[0], 'summary': summary}
1404
1404
1405 early, other = [], []
1405 early, other = [], []
1406 primary = lambda s: s.partition('|')[0]
1406 primary = lambda s: s.partition('|')[0]
1407 for c, e in commands.table.iteritems():
1407 for c, e in commands.table.iteritems():
1408 doc = _getdoc(e)
1408 doc = _getdoc(e)
1409 if 'DEPRECATED' in doc or c.startswith('debug'):
1409 if 'DEPRECATED' in doc or c.startswith('debug'):
1410 continue
1410 continue
1411 cmd = primary(c)
1411 cmd = primary(c)
1412 if cmd.startswith('^'):
1412 if cmd.startswith('^'):
1413 early.append((cmd[1:], doc))
1413 early.append((cmd[1:], doc))
1414 else:
1414 else:
1415 other.append((cmd, doc))
1415 other.append((cmd, doc))
1416
1416
1417 early.sort()
1417 early.sort()
1418 other.sort()
1418 other.sort()
1419
1419
1420 def earlycommands(**map):
1420 def earlycommands(**map):
1421 for c, doc in early:
1421 for c, doc in early:
1422 yield {'topic': c, 'summary': doc}
1422 yield {'topic': c, 'summary': doc}
1423
1423
1424 def othercommands(**map):
1424 def othercommands(**map):
1425 for c, doc in other:
1425 for c, doc in other:
1426 yield {'topic': c, 'summary': doc}
1426 yield {'topic': c, 'summary': doc}
1427
1427
1428 return web.sendtemplate(
1428 return web.sendtemplate(
1429 'helptopics',
1429 'helptopics',
1430 topics=topics,
1430 topics=topics,
1431 earlycommands=earlycommands,
1431 earlycommands=earlycommands,
1432 othercommands=othercommands,
1432 othercommands=othercommands,
1433 title='Index')
1433 title='Index')
1434
1434
1435 # Render an index of sub-topics.
1435 # Render an index of sub-topics.
1436 if topicname in helpmod.subtopics:
1436 if topicname in helpmod.subtopics:
1437 topics = []
1437 topics = []
1438 for entries, summary, _doc in helpmod.subtopics[topicname]:
1438 for entries, summary, _doc in helpmod.subtopics[topicname]:
1439 topics.append({
1439 topics.append({
1440 'topic': '%s.%s' % (topicname, entries[0]),
1440 'topic': '%s.%s' % (topicname, entries[0]),
1441 'basename': entries[0],
1441 'basename': entries[0],
1442 'summary': summary,
1442 'summary': summary,
1443 })
1443 })
1444
1444
1445 return web.sendtemplate(
1445 return web.sendtemplate(
1446 'helptopics',
1446 'helptopics',
1447 topics=topics,
1447 topics=topics,
1448 title=topicname,
1448 title=topicname,
1449 subindex=True)
1449 subindex=True)
1450
1450
1451 u = webutil.wsgiui.load()
1451 u = webutil.wsgiui.load()
1452 u.verbose = True
1452 u.verbose = True
1453
1453
1454 # Render a page from a sub-topic.
1454 # Render a page from a sub-topic.
1455 if '.' in topicname:
1455 if '.' in topicname:
1456 # TODO implement support for rendering sections, like
1456 # TODO implement support for rendering sections, like
1457 # `hg help` works.
1457 # `hg help` works.
1458 topic, subtopic = topicname.split('.', 1)
1458 topic, subtopic = topicname.split('.', 1)
1459 if topic not in helpmod.subtopics:
1459 if topic not in helpmod.subtopics:
1460 raise ErrorResponse(HTTP_NOT_FOUND)
1460 raise ErrorResponse(HTTP_NOT_FOUND)
1461 else:
1461 else:
1462 topic = topicname
1462 topic = topicname
1463 subtopic = None
1463 subtopic = None
1464
1464
1465 try:
1465 try:
1466 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1466 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1467 except error.Abort:
1467 except error.Abort:
1468 raise ErrorResponse(HTTP_NOT_FOUND)
1468 raise ErrorResponse(HTTP_NOT_FOUND)
1469
1469
1470 return web.sendtemplate(
1470 return web.sendtemplate(
1471 'help',
1471 'help',
1472 topic=topicname,
1472 topic=topicname,
1473 doc=doc)
1473 doc=doc)
1474
1474
1475 # tell hggettext to extract docstrings from these functions:
1475 # tell hggettext to extract docstrings from these functions:
1476 i18nfunctions = commands.values()
1476 i18nfunctions = commands.values()
@@ -1,764 +1,764 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(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(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 _prettyprintdifflines(context, lines, blockno, lineidprefix):
522 def _prettyprintdifflines(context, lines, blockno, lineidprefix):
523 for lineno, l in enumerate(lines, 1):
523 for lineno, l in enumerate(lines, 1):
524 difflineno = "%d.%d" % (blockno, lineno)
524 difflineno = "%d.%d" % (blockno, lineno)
525 if l.startswith('+'):
525 if l.startswith('+'):
526 ltype = "difflineplus"
526 ltype = "difflineplus"
527 elif l.startswith('-'):
527 elif l.startswith('-'):
528 ltype = "difflineminus"
528 ltype = "difflineminus"
529 elif l.startswith('@'):
529 elif l.startswith('@'):
530 ltype = "difflineat"
530 ltype = "difflineat"
531 else:
531 else:
532 ltype = "diffline"
532 ltype = "diffline"
533 yield context.process(ltype, {
533 yield context.process(ltype, {
534 'line': l,
534 'line': l,
535 'lineno': lineno,
535 'lineno': lineno,
536 'lineid': lineidprefix + "l%s" % difflineno,
536 'lineid': lineidprefix + "l%s" % difflineno,
537 'linenumber': "% 8s" % difflineno,
537 'linenumber': "% 8s" % difflineno,
538 })
538 })
539
539
540 def _diffsgen(context, repo, ctx, basectx, files, style, stripecount,
540 def _diffsgen(context, repo, ctx, basectx, files, style, stripecount,
541 linerange, lineidprefix):
541 linerange, lineidprefix):
542 if files:
542 if files:
543 m = match.exact(repo.root, repo.getcwd(), files)
543 m = match.exact(repo.root, repo.getcwd(), files)
544 else:
544 else:
545 m = match.always(repo.root, repo.getcwd())
545 m = match.always(repo.root, repo.getcwd())
546
546
547 diffopts = patch.diffopts(repo.ui, untrusted=True)
547 diffopts = patch.diffopts(repo.ui, untrusted=True)
548 node1 = basectx.node()
548 node1 = basectx.node()
549 node2 = ctx.node()
549 node2 = ctx.node()
550 parity = paritygen(stripecount)
550 parity = paritygen(stripecount)
551
551
552 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
552 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
553 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
553 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
554 if style != 'raw':
554 if style != 'raw':
555 header = header[1:]
555 header = header[1:]
556 lines = [h + '\n' for h in header]
556 lines = [h + '\n' for h in header]
557 for hunkrange, hunklines in hunks:
557 for hunkrange, hunklines in hunks:
558 if linerange is not None and hunkrange is not None:
558 if linerange is not None and hunkrange is not None:
559 s1, l1, s2, l2 = hunkrange
559 s1, l1, s2, l2 = hunkrange
560 if not mdiff.hunkinrange((s2, l2), linerange):
560 if not mdiff.hunkinrange((s2, l2), linerange):
561 continue
561 continue
562 lines.extend(hunklines)
562 lines.extend(hunklines)
563 if lines:
563 if lines:
564 l = templateutil.mappedgenerator(_prettyprintdifflines,
564 l = templateutil.mappedgenerator(_prettyprintdifflines,
565 args=(lines, blockno,
565 args=(lines, blockno,
566 lineidprefix))
566 lineidprefix))
567 yield {
567 yield {
568 'parity': next(parity),
568 'parity': next(parity),
569 'blockno': blockno,
569 'blockno': blockno,
570 'lines': l,
570 'lines': l,
571 }
571 }
572
572
573 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=''):
573 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=''):
574 args = (web.repo, ctx, basectx, files, style, web.stripecount,
574 args = (web.repo, ctx, basectx, files, style, web.stripecount,
575 linerange, lineidprefix)
575 linerange, lineidprefix)
576 return templateutil.mappinggenerator(_diffsgen, args=args, name='diffblock')
576 return templateutil.mappinggenerator(_diffsgen, args=args, name='diffblock')
577
577
578 def _compline(type, leftlineno, leftline, rightlineno, rightline):
578 def _compline(type, leftlineno, leftline, rightlineno, rightline):
579 lineid = leftlineno and ("l%d" % leftlineno) or ''
579 lineid = leftlineno and ("l%d" % leftlineno) or ''
580 lineid += rightlineno and ("r%d" % rightlineno) or ''
580 lineid += rightlineno and ("r%d" % rightlineno) or ''
581 llno = '%d' % leftlineno if leftlineno else ''
581 llno = '%d' % leftlineno if leftlineno else ''
582 rlno = '%d' % rightlineno if rightlineno else ''
582 rlno = '%d' % rightlineno if rightlineno else ''
583 return {
583 return {
584 'type': type,
584 'type': type,
585 'lineid': lineid,
585 'lineid': lineid,
586 'leftlineno': leftlineno,
586 'leftlineno': leftlineno,
587 'leftlinenumber': "% 6s" % llno,
587 'leftlinenumber': "% 6s" % llno,
588 'leftline': leftline or '',
588 'leftline': leftline or '',
589 'rightlineno': rightlineno,
589 'rightlineno': rightlineno,
590 'rightlinenumber': "% 6s" % rlno,
590 'rightlinenumber': "% 6s" % rlno,
591 'rightline': rightline or '',
591 'rightline': rightline or '',
592 }
592 }
593
593
594 def _getcompblockgen(context, leftlines, rightlines, opcodes):
594 def _getcompblockgen(context, leftlines, rightlines, opcodes):
595 for type, llo, lhi, rlo, rhi in opcodes:
595 for type, llo, lhi, rlo, rhi in opcodes:
596 len1 = lhi - llo
596 len1 = lhi - llo
597 len2 = rhi - rlo
597 len2 = rhi - rlo
598 count = min(len1, len2)
598 count = min(len1, len2)
599 for i in xrange(count):
599 for i in xrange(count):
600 yield _compline(type=type,
600 yield _compline(type=type,
601 leftlineno=llo + i + 1,
601 leftlineno=llo + i + 1,
602 leftline=leftlines[llo + i],
602 leftline=leftlines[llo + i],
603 rightlineno=rlo + i + 1,
603 rightlineno=rlo + i + 1,
604 rightline=rightlines[rlo + i])
604 rightline=rightlines[rlo + i])
605 if len1 > len2:
605 if len1 > len2:
606 for i in xrange(llo + count, lhi):
606 for i in xrange(llo + count, lhi):
607 yield _compline(type=type,
607 yield _compline(type=type,
608 leftlineno=i + 1,
608 leftlineno=i + 1,
609 leftline=leftlines[i],
609 leftline=leftlines[i],
610 rightlineno=None,
610 rightlineno=None,
611 rightline=None)
611 rightline=None)
612 elif len2 > len1:
612 elif len2 > len1:
613 for i in xrange(rlo + count, rhi):
613 for i in xrange(rlo + count, rhi):
614 yield _compline(type=type,
614 yield _compline(type=type,
615 leftlineno=None,
615 leftlineno=None,
616 leftline=None,
616 leftline=None,
617 rightlineno=i + 1,
617 rightlineno=i + 1,
618 rightline=rightlines[i])
618 rightline=rightlines[i])
619
619
620 def _getcompblock(leftlines, rightlines, opcodes):
620 def _getcompblock(leftlines, rightlines, opcodes):
621 args = (leftlines, rightlines, opcodes)
621 args = (leftlines, rightlines, opcodes)
622 return templateutil.mappinggenerator(_getcompblockgen, args=args,
622 return templateutil.mappinggenerator(_getcompblockgen, args=args,
623 name='comparisonline')
623 name='comparisonline')
624
624
625 def _comparegen(context, contextnum, leftlines, rightlines):
625 def _comparegen(context, contextnum, leftlines, rightlines):
626 '''Generator function that provides side-by-side comparison data.'''
626 '''Generator function that provides side-by-side comparison data.'''
627 s = difflib.SequenceMatcher(None, leftlines, rightlines)
627 s = difflib.SequenceMatcher(None, leftlines, rightlines)
628 if contextnum < 0:
628 if contextnum < 0:
629 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
629 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
630 yield {'lines': l}
630 yield {'lines': l}
631 else:
631 else:
632 for oc in s.get_grouped_opcodes(n=contextnum):
632 for oc in s.get_grouped_opcodes(n=contextnum):
633 l = _getcompblock(leftlines, rightlines, oc)
633 l = _getcompblock(leftlines, rightlines, oc)
634 yield {'lines': l}
634 yield {'lines': l}
635
635
636 def compare(tmpl, contextnum, leftlines, rightlines):
636 def compare(contextnum, leftlines, rightlines):
637 args = (contextnum, leftlines, rightlines)
637 args = (contextnum, leftlines, rightlines)
638 return templateutil.mappinggenerator(_comparegen, args=args,
638 return templateutil.mappinggenerator(_comparegen, args=args,
639 name='comparisonblock')
639 name='comparisonblock')
640
640
641 def diffstatgen(ctx, basectx):
641 def diffstatgen(ctx, basectx):
642 '''Generator function that provides the diffstat data.'''
642 '''Generator function that provides the diffstat data.'''
643
643
644 stats = patch.diffstatdata(
644 stats = patch.diffstatdata(
645 util.iterlines(ctx.diff(basectx, noprefix=False)))
645 util.iterlines(ctx.diff(basectx, noprefix=False)))
646 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
646 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
647 while True:
647 while True:
648 yield stats, maxname, maxtotal, addtotal, removetotal, binary
648 yield stats, maxname, maxtotal, addtotal, removetotal, binary
649
649
650 def diffsummary(statgen):
650 def diffsummary(statgen):
651 '''Return a short summary of the diff.'''
651 '''Return a short summary of the diff.'''
652
652
653 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
653 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
654 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
654 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
655 len(stats), addtotal, removetotal)
655 len(stats), addtotal, removetotal)
656
656
657 def diffstat(tmpl, ctx, statgen, parity):
657 def diffstat(tmpl, ctx, statgen, parity):
658 '''Return a diffstat template for each file in the diff.'''
658 '''Return a diffstat template for each file in the diff.'''
659
659
660 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
660 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
661 files = ctx.files()
661 files = ctx.files()
662
662
663 def pct(i):
663 def pct(i):
664 if maxtotal == 0:
664 if maxtotal == 0:
665 return 0
665 return 0
666 return (float(i) / maxtotal) * 100
666 return (float(i) / maxtotal) * 100
667
667
668 fileno = 0
668 fileno = 0
669 for filename, adds, removes, isbinary in stats:
669 for filename, adds, removes, isbinary in stats:
670 template = 'diffstatlink' if filename in files else 'diffstatnolink'
670 template = 'diffstatlink' if filename in files else 'diffstatnolink'
671 total = adds + removes
671 total = adds + removes
672 fileno += 1
672 fileno += 1
673 yield tmpl.generate(template, {
673 yield tmpl.generate(template, {
674 'node': ctx.hex(),
674 'node': ctx.hex(),
675 'file': filename,
675 'file': filename,
676 'fileno': fileno,
676 'fileno': fileno,
677 'total': total,
677 'total': total,
678 'addpct': pct(adds),
678 'addpct': pct(adds),
679 'removepct': pct(removes),
679 'removepct': pct(removes),
680 'parity': next(parity),
680 'parity': next(parity),
681 })
681 })
682
682
683 class sessionvars(templateutil.wrapped):
683 class sessionvars(templateutil.wrapped):
684 def __init__(self, vars, start='?'):
684 def __init__(self, vars, start='?'):
685 self._start = start
685 self._start = start
686 self._vars = vars
686 self._vars = vars
687
687
688 def __getitem__(self, key):
688 def __getitem__(self, key):
689 return self._vars[key]
689 return self._vars[key]
690
690
691 def __setitem__(self, key, value):
691 def __setitem__(self, key, value):
692 self._vars[key] = value
692 self._vars[key] = value
693
693
694 def __copy__(self):
694 def __copy__(self):
695 return sessionvars(copy.copy(self._vars), self._start)
695 return sessionvars(copy.copy(self._vars), self._start)
696
696
697 def itermaps(self, context):
697 def itermaps(self, context):
698 separator = self._start
698 separator = self._start
699 for key, value in sorted(self._vars.iteritems()):
699 for key, value in sorted(self._vars.iteritems()):
700 yield {'name': key,
700 yield {'name': key,
701 'value': pycompat.bytestr(value),
701 'value': pycompat.bytestr(value),
702 'separator': separator,
702 'separator': separator,
703 }
703 }
704 separator = '&'
704 separator = '&'
705
705
706 def join(self, context, mapping, sep):
706 def join(self, context, mapping, sep):
707 # could be '{separator}{name}={value|urlescape}'
707 # could be '{separator}{name}={value|urlescape}'
708 raise error.ParseError(_('not displayable without template'))
708 raise error.ParseError(_('not displayable without template'))
709
709
710 def show(self, context, mapping):
710 def show(self, context, mapping):
711 return self.join(context, '')
711 return self.join(context, '')
712
712
713 def tovalue(self, context, mapping):
713 def tovalue(self, context, mapping):
714 return self._vars
714 return self._vars
715
715
716 class wsgiui(uimod.ui):
716 class wsgiui(uimod.ui):
717 # default termwidth breaks under mod_wsgi
717 # default termwidth breaks under mod_wsgi
718 def termwidth(self):
718 def termwidth(self):
719 return 80
719 return 80
720
720
721 def getwebsubs(repo):
721 def getwebsubs(repo):
722 websubtable = []
722 websubtable = []
723 websubdefs = repo.ui.configitems('websub')
723 websubdefs = repo.ui.configitems('websub')
724 # we must maintain interhg backwards compatibility
724 # we must maintain interhg backwards compatibility
725 websubdefs += repo.ui.configitems('interhg')
725 websubdefs += repo.ui.configitems('interhg')
726 for key, pattern in websubdefs:
726 for key, pattern in websubdefs:
727 # grab the delimiter from the character after the "s"
727 # grab the delimiter from the character after the "s"
728 unesc = pattern[1:2]
728 unesc = pattern[1:2]
729 delim = re.escape(unesc)
729 delim = re.escape(unesc)
730
730
731 # identify portions of the pattern, taking care to avoid escaped
731 # identify portions of the pattern, taking care to avoid escaped
732 # delimiters. the replace format and flags are optional, but
732 # delimiters. the replace format and flags are optional, but
733 # delimiters are required.
733 # delimiters are required.
734 match = re.match(
734 match = re.match(
735 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
735 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
736 % (delim, delim, delim), pattern)
736 % (delim, delim, delim), pattern)
737 if not match:
737 if not match:
738 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
738 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
739 % (key, pattern))
739 % (key, pattern))
740 continue
740 continue
741
741
742 # we need to unescape the delimiter for regexp and format
742 # we need to unescape the delimiter for regexp and format
743 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
743 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
744 regexp = delim_re.sub(unesc, match.group(1))
744 regexp = delim_re.sub(unesc, match.group(1))
745 format = delim_re.sub(unesc, match.group(2))
745 format = delim_re.sub(unesc, match.group(2))
746
746
747 # the pattern allows for 6 regexp flags, so set them if necessary
747 # the pattern allows for 6 regexp flags, so set them if necessary
748 flagin = match.group(3)
748 flagin = match.group(3)
749 flags = 0
749 flags = 0
750 if flagin:
750 if flagin:
751 for flag in flagin.upper():
751 for flag in flagin.upper():
752 flags |= re.__dict__[flag]
752 flags |= re.__dict__[flag]
753
753
754 try:
754 try:
755 regexp = re.compile(regexp, flags)
755 regexp = re.compile(regexp, flags)
756 websubtable.append((regexp, format))
756 websubtable.append((regexp, format))
757 except re.error:
757 except re.error:
758 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
758 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
759 % (key, regexp))
759 % (key, regexp))
760 return websubtable
760 return websubtable
761
761
762 def getgraphnode(repo, ctx):
762 def getgraphnode(repo, ctx):
763 return (templatekw.getgraphnodecurrent(repo, ctx) +
763 return (templatekw.getgraphnodecurrent(repo, ctx) +
764 templatekw.getgraphnodesymbol(ctx))
764 templatekw.getgraphnodesymbol(ctx))
General Comments 0
You need to be logged in to leave comments. Login now