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