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