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