##// END OF EJS Templates
hgweb: convert an assert to a ProgrammingError...
Gregory Szorc -
r36995:a82fc392 default
parent child Browse files
Show More
@@ -1,1484 +1,1486 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, nullid, short
16 from ..node import hex, nullid, 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 util,
39 util,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 webutil,
43 webutil,
44 )
44 )
45
45
46 __all__ = []
46 __all__ = []
47 commands = {}
47 commands = {}
48
48
49 class webcommand(object):
49 class webcommand(object):
50 """Decorator used to register a web command handler.
50 """Decorator used to register a web command handler.
51
51
52 The decorator takes as its positional arguments the name/path the
52 The decorator takes as its positional arguments the name/path the
53 command should be accessible under.
53 command should be accessible under.
54
54
55 When called, functions receive as arguments a ``requestcontext``,
55 When called, functions receive as arguments a ``requestcontext``,
56 ``wsgirequest``, and a templater instance for generatoring output.
56 ``wsgirequest``, and a templater instance for generatoring output.
57 The functions should populate the ``rctx.res`` object with details
57 The functions should populate the ``rctx.res`` object with details
58 about the HTTP response.
58 about the HTTP response.
59
59
60 The function returns a generator to be consumed by the WSGI application.
60 The function returns a generator to be consumed by the WSGI application.
61 For most commands, this should be the result from
61 For most commands, this should be the result from
62 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
62 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
63 to render a template.
63 to render a template.
64
64
65 Usage:
65 Usage:
66
66
67 @webcommand('mycommand')
67 @webcommand('mycommand')
68 def mycommand(web):
68 def mycommand(web):
69 pass
69 pass
70 """
70 """
71
71
72 def __init__(self, name):
72 def __init__(self, name):
73 self.name = name
73 self.name = name
74
74
75 def __call__(self, func):
75 def __call__(self, func):
76 __all__.append(self.name)
76 __all__.append(self.name)
77 commands[self.name] = func
77 commands[self.name] = func
78 return func
78 return func
79
79
80 @webcommand('log')
80 @webcommand('log')
81 def log(web):
81 def log(web):
82 """
82 """
83 /log[/{revision}[/{path}]]
83 /log[/{revision}[/{path}]]
84 --------------------------
84 --------------------------
85
85
86 Show repository or file history.
86 Show repository or file history.
87
87
88 For URLs of the form ``/log/{revision}``, a list of changesets starting at
88 For URLs of the form ``/log/{revision}``, a list of changesets starting at
89 the specified changeset identifier is shown. If ``{revision}`` is not
89 the specified changeset identifier is shown. If ``{revision}`` is not
90 defined, the default is ``tip``. This form is equivalent to the
90 defined, the default is ``tip``. This form is equivalent to the
91 ``changelog`` handler.
91 ``changelog`` handler.
92
92
93 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
93 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
94 file will be shown. This form is equivalent to the ``filelog`` handler.
94 file will be shown. This form is equivalent to the ``filelog`` handler.
95 """
95 """
96
96
97 if web.req.qsparams.get('file'):
97 if web.req.qsparams.get('file'):
98 return filelog(web)
98 return filelog(web)
99 else:
99 else:
100 return changelog(web)
100 return changelog(web)
101
101
102 @webcommand('rawfile')
102 @webcommand('rawfile')
103 def rawfile(web):
103 def rawfile(web):
104 guessmime = web.configbool('web', 'guessmime')
104 guessmime = web.configbool('web', 'guessmime')
105
105
106 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
106 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
107 if not path:
107 if not path:
108 return manifest(web)
108 return manifest(web)
109
109
110 try:
110 try:
111 fctx = webutil.filectx(web.repo, web.req)
111 fctx = webutil.filectx(web.repo, web.req)
112 except error.LookupError as inst:
112 except error.LookupError as inst:
113 try:
113 try:
114 return manifest(web)
114 return manifest(web)
115 except ErrorResponse:
115 except ErrorResponse:
116 raise inst
116 raise inst
117
117
118 path = fctx.path()
118 path = fctx.path()
119 text = fctx.data()
119 text = fctx.data()
120 mt = 'application/binary'
120 mt = 'application/binary'
121 if guessmime:
121 if guessmime:
122 mt = mimetypes.guess_type(path)[0]
122 mt = mimetypes.guess_type(path)[0]
123 if mt is None:
123 if mt is None:
124 if util.binary(text):
124 if util.binary(text):
125 mt = 'application/binary'
125 mt = 'application/binary'
126 else:
126 else:
127 mt = 'text/plain'
127 mt = 'text/plain'
128 if mt.startswith('text/'):
128 if mt.startswith('text/'):
129 mt += '; charset="%s"' % encoding.encoding
129 mt += '; charset="%s"' % encoding.encoding
130
130
131 web.res.headers['Content-Type'] = mt
131 web.res.headers['Content-Type'] = mt
132 filename = (path.rpartition('/')[-1]
132 filename = (path.rpartition('/')[-1]
133 .replace('\\', '\\\\').replace('"', '\\"'))
133 .replace('\\', '\\\\').replace('"', '\\"'))
134 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
134 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
135 web.res.setbodybytes(text)
135 web.res.setbodybytes(text)
136 return web.res.sendresponse()
136 return web.res.sendresponse()
137
137
138 def _filerevision(web, fctx):
138 def _filerevision(web, fctx):
139 f = fctx.path()
139 f = fctx.path()
140 text = fctx.data()
140 text = fctx.data()
141 parity = paritygen(web.stripecount)
141 parity = paritygen(web.stripecount)
142 ishead = fctx.filerev() in fctx.filelog().headrevs()
142 ishead = fctx.filerev() in fctx.filelog().headrevs()
143
143
144 if util.binary(text):
144 if util.binary(text):
145 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
145 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
146 text = '(binary:%s)' % mt
146 text = '(binary:%s)' % mt
147
147
148 def lines():
148 def lines():
149 for lineno, t in enumerate(text.splitlines(True)):
149 for lineno, t in enumerate(text.splitlines(True)):
150 yield {"line": t,
150 yield {"line": t,
151 "lineid": "l%d" % (lineno + 1),
151 "lineid": "l%d" % (lineno + 1),
152 "linenumber": "% 6d" % (lineno + 1),
152 "linenumber": "% 6d" % (lineno + 1),
153 "parity": next(parity)}
153 "parity": next(parity)}
154
154
155 return web.sendtemplate(
155 return web.sendtemplate(
156 'filerevision',
156 'filerevision',
157 file=f,
157 file=f,
158 path=webutil.up(f),
158 path=webutil.up(f),
159 text=lines(),
159 text=lines(),
160 symrev=webutil.symrevorshortnode(web.req, fctx),
160 symrev=webutil.symrevorshortnode(web.req, fctx),
161 rename=webutil.renamelink(fctx),
161 rename=webutil.renamelink(fctx),
162 permissions=fctx.manifest().flags(f),
162 permissions=fctx.manifest().flags(f),
163 ishead=int(ishead),
163 ishead=int(ishead),
164 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
164 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
165
165
166 @webcommand('file')
166 @webcommand('file')
167 def file(web):
167 def file(web):
168 """
168 """
169 /file/{revision}[/{path}]
169 /file/{revision}[/{path}]
170 -------------------------
170 -------------------------
171
171
172 Show information about a directory or file in the repository.
172 Show information about a directory or file in the repository.
173
173
174 Info about the ``path`` given as a URL parameter will be rendered.
174 Info about the ``path`` given as a URL parameter will be rendered.
175
175
176 If ``path`` is a directory, information about the entries in that
176 If ``path`` is a directory, information about the entries in that
177 directory will be rendered. This form is equivalent to the ``manifest``
177 directory will be rendered. This form is equivalent to the ``manifest``
178 handler.
178 handler.
179
179
180 If ``path`` is a file, information about that file will be shown via
180 If ``path`` is a file, information about that file will be shown via
181 the ``filerevision`` template.
181 the ``filerevision`` template.
182
182
183 If ``path`` is not defined, information about the root directory will
183 If ``path`` is not defined, information about the root directory will
184 be rendered.
184 be rendered.
185 """
185 """
186 if web.req.qsparams.get('style') == 'raw':
186 if web.req.qsparams.get('style') == 'raw':
187 return rawfile(web)
187 return rawfile(web)
188
188
189 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
189 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
190 if not path:
190 if not path:
191 return manifest(web)
191 return manifest(web)
192 try:
192 try:
193 return _filerevision(web, webutil.filectx(web.repo, web.req))
193 return _filerevision(web, webutil.filectx(web.repo, web.req))
194 except error.LookupError as inst:
194 except error.LookupError as inst:
195 try:
195 try:
196 return manifest(web)
196 return manifest(web)
197 except ErrorResponse:
197 except ErrorResponse:
198 raise inst
198 raise inst
199
199
200 def _search(web):
200 def _search(web):
201 MODE_REVISION = 'rev'
201 MODE_REVISION = 'rev'
202 MODE_KEYWORD = 'keyword'
202 MODE_KEYWORD = 'keyword'
203 MODE_REVSET = 'revset'
203 MODE_REVSET = 'revset'
204
204
205 def revsearch(ctx):
205 def revsearch(ctx):
206 yield ctx
206 yield ctx
207
207
208 def keywordsearch(query):
208 def keywordsearch(query):
209 lower = encoding.lower
209 lower = encoding.lower
210 qw = lower(query).split()
210 qw = lower(query).split()
211
211
212 def revgen():
212 def revgen():
213 cl = web.repo.changelog
213 cl = web.repo.changelog
214 for i in xrange(len(web.repo) - 1, 0, -100):
214 for i in xrange(len(web.repo) - 1, 0, -100):
215 l = []
215 l = []
216 for j in cl.revs(max(0, i - 99), i):
216 for j in cl.revs(max(0, i - 99), i):
217 ctx = web.repo[j]
217 ctx = web.repo[j]
218 l.append(ctx)
218 l.append(ctx)
219 l.reverse()
219 l.reverse()
220 for e in l:
220 for e in l:
221 yield e
221 yield e
222
222
223 for ctx in revgen():
223 for ctx in revgen():
224 miss = 0
224 miss = 0
225 for q in qw:
225 for q in qw:
226 if not (q in lower(ctx.user()) or
226 if not (q in lower(ctx.user()) or
227 q in lower(ctx.description()) or
227 q in lower(ctx.description()) or
228 q in lower(" ".join(ctx.files()))):
228 q in lower(" ".join(ctx.files()))):
229 miss = 1
229 miss = 1
230 break
230 break
231 if miss:
231 if miss:
232 continue
232 continue
233
233
234 yield ctx
234 yield ctx
235
235
236 def revsetsearch(revs):
236 def revsetsearch(revs):
237 for r in revs:
237 for r in revs:
238 yield web.repo[r]
238 yield web.repo[r]
239
239
240 searchfuncs = {
240 searchfuncs = {
241 MODE_REVISION: (revsearch, 'exact revision search'),
241 MODE_REVISION: (revsearch, 'exact revision search'),
242 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
242 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
243 MODE_REVSET: (revsetsearch, 'revset expression search'),
243 MODE_REVSET: (revsetsearch, 'revset expression search'),
244 }
244 }
245
245
246 def getsearchmode(query):
246 def getsearchmode(query):
247 try:
247 try:
248 ctx = web.repo[query]
248 ctx = web.repo[query]
249 except (error.RepoError, error.LookupError):
249 except (error.RepoError, error.LookupError):
250 # query is not an exact revision pointer, need to
250 # query is not an exact revision pointer, need to
251 # decide if it's a revset expression or keywords
251 # decide if it's a revset expression or keywords
252 pass
252 pass
253 else:
253 else:
254 return MODE_REVISION, ctx
254 return MODE_REVISION, ctx
255
255
256 revdef = 'reverse(%s)' % query
256 revdef = 'reverse(%s)' % query
257 try:
257 try:
258 tree = revsetlang.parse(revdef)
258 tree = revsetlang.parse(revdef)
259 except error.ParseError:
259 except error.ParseError:
260 # can't parse to a revset tree
260 # can't parse to a revset tree
261 return MODE_KEYWORD, query
261 return MODE_KEYWORD, query
262
262
263 if revsetlang.depth(tree) <= 2:
263 if revsetlang.depth(tree) <= 2:
264 # no revset syntax used
264 # no revset syntax used
265 return MODE_KEYWORD, query
265 return MODE_KEYWORD, query
266
266
267 if any((token, (value or '')[:3]) == ('string', 're:')
267 if any((token, (value or '')[:3]) == ('string', 're:')
268 for token, value, pos in revsetlang.tokenize(revdef)):
268 for token, value, pos in revsetlang.tokenize(revdef)):
269 return MODE_KEYWORD, query
269 return MODE_KEYWORD, query
270
270
271 funcsused = revsetlang.funcsused(tree)
271 funcsused = revsetlang.funcsused(tree)
272 if not funcsused.issubset(revset.safesymbols):
272 if not funcsused.issubset(revset.safesymbols):
273 return MODE_KEYWORD, query
273 return MODE_KEYWORD, query
274
274
275 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
275 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
276 try:
276 try:
277 revs = mfunc(web.repo)
277 revs = mfunc(web.repo)
278 return MODE_REVSET, revs
278 return MODE_REVSET, revs
279 # ParseError: wrongly placed tokens, wrongs arguments, etc
279 # ParseError: wrongly placed tokens, wrongs arguments, etc
280 # RepoLookupError: no such revision, e.g. in 'revision:'
280 # RepoLookupError: no such revision, e.g. in 'revision:'
281 # Abort: bookmark/tag not exists
281 # Abort: bookmark/tag not exists
282 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
282 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
283 except (error.ParseError, error.RepoLookupError, error.Abort,
283 except (error.ParseError, error.RepoLookupError, error.Abort,
284 LookupError):
284 LookupError):
285 return MODE_KEYWORD, query
285 return MODE_KEYWORD, query
286
286
287 def changelist(**map):
287 def changelist(**map):
288 count = 0
288 count = 0
289
289
290 for ctx in searchfunc[0](funcarg):
290 for ctx in searchfunc[0](funcarg):
291 count += 1
291 count += 1
292 n = ctx.node()
292 n = ctx.node()
293 showtags = webutil.showtag(web.repo, web.tmpl, 'changelogtag', n)
293 showtags = webutil.showtag(web.repo, web.tmpl, 'changelogtag', n)
294 files = webutil.listfilediffs(web.tmpl, ctx.files(), n,
294 files = webutil.listfilediffs(web.tmpl, ctx.files(), n,
295 web.maxfiles)
295 web.maxfiles)
296
296
297 yield web.tmpl(
297 yield web.tmpl(
298 'searchentry',
298 'searchentry',
299 parity=next(parity),
299 parity=next(parity),
300 changelogtag=showtags,
300 changelogtag=showtags,
301 files=files,
301 files=files,
302 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
302 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
303
303
304 if count >= revcount:
304 if count >= revcount:
305 break
305 break
306
306
307 query = web.req.qsparams['rev']
307 query = web.req.qsparams['rev']
308 revcount = web.maxchanges
308 revcount = web.maxchanges
309 if 'revcount' in web.req.qsparams:
309 if 'revcount' in web.req.qsparams:
310 try:
310 try:
311 revcount = int(web.req.qsparams.get('revcount', revcount))
311 revcount = int(web.req.qsparams.get('revcount', revcount))
312 revcount = max(revcount, 1)
312 revcount = max(revcount, 1)
313 web.tmpl.defaults['sessionvars']['revcount'] = revcount
313 web.tmpl.defaults['sessionvars']['revcount'] = revcount
314 except ValueError:
314 except ValueError:
315 pass
315 pass
316
316
317 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
317 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
318 lessvars['revcount'] = max(revcount // 2, 1)
318 lessvars['revcount'] = max(revcount // 2, 1)
319 lessvars['rev'] = query
319 lessvars['rev'] = query
320 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
320 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
321 morevars['revcount'] = revcount * 2
321 morevars['revcount'] = revcount * 2
322 morevars['rev'] = query
322 morevars['rev'] = query
323
323
324 mode, funcarg = getsearchmode(query)
324 mode, funcarg = getsearchmode(query)
325
325
326 if 'forcekw' in web.req.qsparams:
326 if 'forcekw' in web.req.qsparams:
327 showforcekw = ''
327 showforcekw = ''
328 showunforcekw = searchfuncs[mode][1]
328 showunforcekw = searchfuncs[mode][1]
329 mode = MODE_KEYWORD
329 mode = MODE_KEYWORD
330 funcarg = query
330 funcarg = query
331 else:
331 else:
332 if mode != MODE_KEYWORD:
332 if mode != MODE_KEYWORD:
333 showforcekw = searchfuncs[MODE_KEYWORD][1]
333 showforcekw = searchfuncs[MODE_KEYWORD][1]
334 else:
334 else:
335 showforcekw = ''
335 showforcekw = ''
336 showunforcekw = ''
336 showunforcekw = ''
337
337
338 searchfunc = searchfuncs[mode]
338 searchfunc = searchfuncs[mode]
339
339
340 tip = web.repo['tip']
340 tip = web.repo['tip']
341 parity = paritygen(web.stripecount)
341 parity = paritygen(web.stripecount)
342
342
343 return web.sendtemplate(
343 return web.sendtemplate(
344 'search',
344 'search',
345 query=query,
345 query=query,
346 node=tip.hex(),
346 node=tip.hex(),
347 symrev='tip',
347 symrev='tip',
348 entries=changelist,
348 entries=changelist,
349 archives=web.archivelist('tip'),
349 archives=web.archivelist('tip'),
350 morevars=morevars,
350 morevars=morevars,
351 lessvars=lessvars,
351 lessvars=lessvars,
352 modedesc=searchfunc[1],
352 modedesc=searchfunc[1],
353 showforcekw=showforcekw,
353 showforcekw=showforcekw,
354 showunforcekw=showunforcekw)
354 showunforcekw=showunforcekw)
355
355
356 @webcommand('changelog')
356 @webcommand('changelog')
357 def changelog(web, shortlog=False):
357 def changelog(web, shortlog=False):
358 """
358 """
359 /changelog[/{revision}]
359 /changelog[/{revision}]
360 -----------------------
360 -----------------------
361
361
362 Show information about multiple changesets.
362 Show information about multiple changesets.
363
363
364 If the optional ``revision`` URL argument is absent, information about
364 If the optional ``revision`` URL argument is absent, information about
365 all changesets starting at ``tip`` will be rendered. If the ``revision``
365 all changesets starting at ``tip`` will be rendered. If the ``revision``
366 argument is present, changesets will be shown starting from the specified
366 argument is present, changesets will be shown starting from the specified
367 revision.
367 revision.
368
368
369 If ``revision`` is absent, the ``rev`` query string argument may be
369 If ``revision`` is absent, the ``rev`` query string argument may be
370 defined. This will perform a search for changesets.
370 defined. This will perform a search for changesets.
371
371
372 The argument for ``rev`` can be a single revision, a revision set,
372 The argument for ``rev`` can be a single revision, a revision set,
373 or a literal keyword to search for in changeset data (equivalent to
373 or a literal keyword to search for in changeset data (equivalent to
374 :hg:`log -k`).
374 :hg:`log -k`).
375
375
376 The ``revcount`` query string argument defines the maximum numbers of
376 The ``revcount`` query string argument defines the maximum numbers of
377 changesets to render.
377 changesets to render.
378
378
379 For non-searches, the ``changelog`` template will be rendered.
379 For non-searches, the ``changelog`` template will be rendered.
380 """
380 """
381
381
382 query = ''
382 query = ''
383 if 'node' in web.req.qsparams:
383 if 'node' in web.req.qsparams:
384 ctx = webutil.changectx(web.repo, web.req)
384 ctx = webutil.changectx(web.repo, web.req)
385 symrev = webutil.symrevorshortnode(web.req, ctx)
385 symrev = webutil.symrevorshortnode(web.req, ctx)
386 elif 'rev' in web.req.qsparams:
386 elif 'rev' in web.req.qsparams:
387 return _search(web)
387 return _search(web)
388 else:
388 else:
389 ctx = web.repo['tip']
389 ctx = web.repo['tip']
390 symrev = 'tip'
390 symrev = 'tip'
391
391
392 def changelist():
392 def changelist():
393 revs = []
393 revs = []
394 if pos != -1:
394 if pos != -1:
395 revs = web.repo.changelog.revs(pos, 0)
395 revs = web.repo.changelog.revs(pos, 0)
396 curcount = 0
396 curcount = 0
397 for rev in revs:
397 for rev in revs:
398 curcount += 1
398 curcount += 1
399 if curcount > revcount + 1:
399 if curcount > revcount + 1:
400 break
400 break
401
401
402 entry = webutil.changelistentry(web, web.repo[rev])
402 entry = webutil.changelistentry(web, web.repo[rev])
403 entry['parity'] = next(parity)
403 entry['parity'] = next(parity)
404 yield entry
404 yield entry
405
405
406 if shortlog:
406 if shortlog:
407 revcount = web.maxshortchanges
407 revcount = web.maxshortchanges
408 else:
408 else:
409 revcount = web.maxchanges
409 revcount = web.maxchanges
410
410
411 if 'revcount' in web.req.qsparams:
411 if 'revcount' in web.req.qsparams:
412 try:
412 try:
413 revcount = int(web.req.qsparams.get('revcount', revcount))
413 revcount = int(web.req.qsparams.get('revcount', revcount))
414 revcount = max(revcount, 1)
414 revcount = max(revcount, 1)
415 web.tmpl.defaults['sessionvars']['revcount'] = revcount
415 web.tmpl.defaults['sessionvars']['revcount'] = revcount
416 except ValueError:
416 except ValueError:
417 pass
417 pass
418
418
419 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
419 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
420 lessvars['revcount'] = max(revcount // 2, 1)
420 lessvars['revcount'] = max(revcount // 2, 1)
421 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
421 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
422 morevars['revcount'] = revcount * 2
422 morevars['revcount'] = revcount * 2
423
423
424 count = len(web.repo)
424 count = len(web.repo)
425 pos = ctx.rev()
425 pos = ctx.rev()
426 parity = paritygen(web.stripecount)
426 parity = paritygen(web.stripecount)
427
427
428 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
428 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
429
429
430 entries = list(changelist())
430 entries = list(changelist())
431 latestentry = entries[:1]
431 latestentry = entries[:1]
432 if len(entries) > revcount:
432 if len(entries) > revcount:
433 nextentry = entries[-1:]
433 nextentry = entries[-1:]
434 entries = entries[:-1]
434 entries = entries[:-1]
435 else:
435 else:
436 nextentry = []
436 nextentry = []
437
437
438 return web.sendtemplate(
438 return web.sendtemplate(
439 'shortlog' if shortlog else 'changelog',
439 'shortlog' if shortlog else 'changelog',
440 changenav=changenav,
440 changenav=changenav,
441 node=ctx.hex(),
441 node=ctx.hex(),
442 rev=pos,
442 rev=pos,
443 symrev=symrev,
443 symrev=symrev,
444 changesets=count,
444 changesets=count,
445 entries=entries,
445 entries=entries,
446 latestentry=latestentry,
446 latestentry=latestentry,
447 nextentry=nextentry,
447 nextentry=nextentry,
448 archives=web.archivelist('tip'),
448 archives=web.archivelist('tip'),
449 revcount=revcount,
449 revcount=revcount,
450 morevars=morevars,
450 morevars=morevars,
451 lessvars=lessvars,
451 lessvars=lessvars,
452 query=query)
452 query=query)
453
453
454 @webcommand('shortlog')
454 @webcommand('shortlog')
455 def shortlog(web):
455 def shortlog(web):
456 """
456 """
457 /shortlog
457 /shortlog
458 ---------
458 ---------
459
459
460 Show basic information about a set of changesets.
460 Show basic information about a set of changesets.
461
461
462 This accepts the same parameters as the ``changelog`` handler. The only
462 This accepts the same parameters as the ``changelog`` handler. The only
463 difference is the ``shortlog`` template will be rendered instead of the
463 difference is the ``shortlog`` template will be rendered instead of the
464 ``changelog`` template.
464 ``changelog`` template.
465 """
465 """
466 return changelog(web, shortlog=True)
466 return changelog(web, shortlog=True)
467
467
468 @webcommand('changeset')
468 @webcommand('changeset')
469 def changeset(web):
469 def changeset(web):
470 """
470 """
471 /changeset[/{revision}]
471 /changeset[/{revision}]
472 -----------------------
472 -----------------------
473
473
474 Show information about a single changeset.
474 Show information about a single changeset.
475
475
476 A URL path argument is the changeset identifier to show. See ``hg help
476 A URL path argument is the changeset identifier to show. See ``hg help
477 revisions`` for possible values. If not defined, the ``tip`` changeset
477 revisions`` for possible values. If not defined, the ``tip`` changeset
478 will be shown.
478 will be shown.
479
479
480 The ``changeset`` template is rendered. Contents of the ``changesettag``,
480 The ``changeset`` template is rendered. Contents of the ``changesettag``,
481 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
481 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
482 templates related to diffs may all be used to produce the output.
482 templates related to diffs may all be used to produce the output.
483 """
483 """
484 ctx = webutil.changectx(web.repo, web.req)
484 ctx = webutil.changectx(web.repo, web.req)
485
485
486 return web.sendtemplate(
486 return web.sendtemplate(
487 'changeset',
487 'changeset',
488 **webutil.changesetentry(web, ctx))
488 **webutil.changesetentry(web, ctx))
489
489
490 rev = webcommand('rev')(changeset)
490 rev = webcommand('rev')(changeset)
491
491
492 def decodepath(path):
492 def decodepath(path):
493 """Hook for mapping a path in the repository to a path in the
493 """Hook for mapping a path in the repository to a path in the
494 working copy.
494 working copy.
495
495
496 Extensions (e.g., largefiles) can override this to remap files in
496 Extensions (e.g., largefiles) can override this to remap files in
497 the virtual file system presented by the manifest command below."""
497 the virtual file system presented by the manifest command below."""
498 return path
498 return path
499
499
500 @webcommand('manifest')
500 @webcommand('manifest')
501 def manifest(web):
501 def manifest(web):
502 """
502 """
503 /manifest[/{revision}[/{path}]]
503 /manifest[/{revision}[/{path}]]
504 -------------------------------
504 -------------------------------
505
505
506 Show information about a directory.
506 Show information about a directory.
507
507
508 If the URL path arguments are omitted, information about the root
508 If the URL path arguments are omitted, information about the root
509 directory for the ``tip`` changeset will be shown.
509 directory for the ``tip`` changeset will be shown.
510
510
511 Because this handler can only show information for directories, it
511 Because this handler can only show information for directories, it
512 is recommended to use the ``file`` handler instead, as it can handle both
512 is recommended to use the ``file`` handler instead, as it can handle both
513 directories and files.
513 directories and files.
514
514
515 The ``manifest`` template will be rendered for this handler.
515 The ``manifest`` template will be rendered for this handler.
516 """
516 """
517 if 'node' in web.req.qsparams:
517 if 'node' in web.req.qsparams:
518 ctx = webutil.changectx(web.repo, web.req)
518 ctx = webutil.changectx(web.repo, web.req)
519 symrev = webutil.symrevorshortnode(web.req, ctx)
519 symrev = webutil.symrevorshortnode(web.req, ctx)
520 else:
520 else:
521 ctx = web.repo['tip']
521 ctx = web.repo['tip']
522 symrev = 'tip'
522 symrev = 'tip'
523 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
523 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
524 mf = ctx.manifest()
524 mf = ctx.manifest()
525 node = ctx.node()
525 node = ctx.node()
526
526
527 files = {}
527 files = {}
528 dirs = {}
528 dirs = {}
529 parity = paritygen(web.stripecount)
529 parity = paritygen(web.stripecount)
530
530
531 if path and path[-1:] != "/":
531 if path and path[-1:] != "/":
532 path += "/"
532 path += "/"
533 l = len(path)
533 l = len(path)
534 abspath = "/" + path
534 abspath = "/" + path
535
535
536 for full, n in mf.iteritems():
536 for full, n in mf.iteritems():
537 # the virtual path (working copy path) used for the full
537 # the virtual path (working copy path) used for the full
538 # (repository) path
538 # (repository) path
539 f = decodepath(full)
539 f = decodepath(full)
540
540
541 if f[:l] != path:
541 if f[:l] != path:
542 continue
542 continue
543 remain = f[l:]
543 remain = f[l:]
544 elements = remain.split('/')
544 elements = remain.split('/')
545 if len(elements) == 1:
545 if len(elements) == 1:
546 files[remain] = full
546 files[remain] = full
547 else:
547 else:
548 h = dirs # need to retain ref to dirs (root)
548 h = dirs # need to retain ref to dirs (root)
549 for elem in elements[0:-1]:
549 for elem in elements[0:-1]:
550 if elem not in h:
550 if elem not in h:
551 h[elem] = {}
551 h[elem] = {}
552 h = h[elem]
552 h = h[elem]
553 if len(h) > 1:
553 if len(h) > 1:
554 break
554 break
555 h[None] = None # denotes files present
555 h[None] = None # denotes files present
556
556
557 if mf and not files and not dirs:
557 if mf and not files and not dirs:
558 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
558 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
559
559
560 def filelist(**map):
560 def filelist(**map):
561 for f in sorted(files):
561 for f in sorted(files):
562 full = files[f]
562 full = files[f]
563
563
564 fctx = ctx.filectx(full)
564 fctx = ctx.filectx(full)
565 yield {"file": full,
565 yield {"file": full,
566 "parity": next(parity),
566 "parity": next(parity),
567 "basename": f,
567 "basename": f,
568 "date": fctx.date(),
568 "date": fctx.date(),
569 "size": fctx.size(),
569 "size": fctx.size(),
570 "permissions": mf.flags(full)}
570 "permissions": mf.flags(full)}
571
571
572 def dirlist(**map):
572 def dirlist(**map):
573 for d in sorted(dirs):
573 for d in sorted(dirs):
574
574
575 emptydirs = []
575 emptydirs = []
576 h = dirs[d]
576 h = dirs[d]
577 while isinstance(h, dict) and len(h) == 1:
577 while isinstance(h, dict) and len(h) == 1:
578 k, v = next(iter(h.items()))
578 k, v = next(iter(h.items()))
579 if v:
579 if v:
580 emptydirs.append(k)
580 emptydirs.append(k)
581 h = v
581 h = v
582
582
583 path = "%s%s" % (abspath, d)
583 path = "%s%s" % (abspath, d)
584 yield {"parity": next(parity),
584 yield {"parity": next(parity),
585 "path": path,
585 "path": path,
586 "emptydirs": "/".join(emptydirs),
586 "emptydirs": "/".join(emptydirs),
587 "basename": d}
587 "basename": d}
588
588
589 return web.sendtemplate(
589 return web.sendtemplate(
590 'manifest',
590 'manifest',
591 symrev=symrev,
591 symrev=symrev,
592 path=abspath,
592 path=abspath,
593 up=webutil.up(abspath),
593 up=webutil.up(abspath),
594 upparity=next(parity),
594 upparity=next(parity),
595 fentries=filelist,
595 fentries=filelist,
596 dentries=dirlist,
596 dentries=dirlist,
597 archives=web.archivelist(hex(node)),
597 archives=web.archivelist(hex(node)),
598 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
598 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
599
599
600 @webcommand('tags')
600 @webcommand('tags')
601 def tags(web):
601 def tags(web):
602 """
602 """
603 /tags
603 /tags
604 -----
604 -----
605
605
606 Show information about tags.
606 Show information about tags.
607
607
608 No arguments are accepted.
608 No arguments are accepted.
609
609
610 The ``tags`` template is rendered.
610 The ``tags`` template is rendered.
611 """
611 """
612 i = list(reversed(web.repo.tagslist()))
612 i = list(reversed(web.repo.tagslist()))
613 parity = paritygen(web.stripecount)
613 parity = paritygen(web.stripecount)
614
614
615 def entries(notip, latestonly, **map):
615 def entries(notip, latestonly, **map):
616 t = i
616 t = i
617 if notip:
617 if notip:
618 t = [(k, n) for k, n in i if k != "tip"]
618 t = [(k, n) for k, n in i if k != "tip"]
619 if latestonly:
619 if latestonly:
620 t = t[:1]
620 t = t[:1]
621 for k, n in t:
621 for k, n in t:
622 yield {"parity": next(parity),
622 yield {"parity": next(parity),
623 "tag": k,
623 "tag": k,
624 "date": web.repo[n].date(),
624 "date": web.repo[n].date(),
625 "node": hex(n)}
625 "node": hex(n)}
626
626
627 return web.sendtemplate(
627 return web.sendtemplate(
628 'tags',
628 'tags',
629 node=hex(web.repo.changelog.tip()),
629 node=hex(web.repo.changelog.tip()),
630 entries=lambda **x: entries(False, False, **x),
630 entries=lambda **x: entries(False, False, **x),
631 entriesnotip=lambda **x: entries(True, False, **x),
631 entriesnotip=lambda **x: entries(True, False, **x),
632 latestentry=lambda **x: entries(True, True, **x))
632 latestentry=lambda **x: entries(True, True, **x))
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(latestonly, **map):
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=lambda **x: entries(latestonly=False, **x),
671 latestentry=lambda **x: entries(latestonly=True, **x))
671 latestentry=lambda **x: entries(latestonly=True, **x))
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(**map):
711 def tagentries(**map):
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 web.tmpl(
722 yield web.tmpl(
723 'tagentry',
723 'tagentry',
724 parity=next(parity),
724 parity=next(parity),
725 tag=k,
725 tag=k,
726 node=hex(n),
726 node=hex(n),
727 date=web.repo[n].date())
727 date=web.repo[n].date())
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(**map):
740 def changelist(**map):
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
748
749 l.append(web.tmpl(
749 l.append(web.tmpl(
750 'shortlogentry',
750 'shortlogentry',
751 parity=next(parity),
751 parity=next(parity),
752 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
752 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
753
753
754 for entry in reversed(l):
754 for entry in reversed(l):
755 yield entry
755 yield entry
756
756
757 tip = web.repo['tip']
757 tip = web.repo['tip']
758 count = len(web.repo)
758 count = len(web.repo)
759 start = max(0, count - web.maxchanges)
759 start = max(0, count - web.maxchanges)
760 end = min(count, start + web.maxchanges)
760 end = min(count, start + web.maxchanges)
761
761
762 desc = web.config("web", "description")
762 desc = web.config("web", "description")
763 if not desc:
763 if not desc:
764 desc = 'unknown'
764 desc = 'unknown'
765
765
766 return web.sendtemplate(
766 return web.sendtemplate(
767 'summary',
767 'summary',
768 desc=desc,
768 desc=desc,
769 owner=get_contact(web.config) or 'unknown',
769 owner=get_contact(web.config) or 'unknown',
770 lastchange=tip.date(),
770 lastchange=tip.date(),
771 tags=tagentries,
771 tags=tagentries,
772 bookmarks=bookmarks,
772 bookmarks=bookmarks,
773 branches=webutil.branchentries(web.repo, web.stripecount, 10),
773 branches=webutil.branchentries(web.repo, web.stripecount, 10),
774 shortlog=changelist,
774 shortlog=changelist,
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=web.configlist('web', 'labels'))
778 labels=web.configlist('web', 'labels'))
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 = []
816 rename = []
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(web.tmpl, context, leftlines, rightlines)
884 comparison = webutil.compare(web.tmpl, 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 = []
889 rename = []
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 = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
949 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
950 else:
950 else:
951 lines = webutil.annotate(web.req, fctx, web.repo.ui)
951 lines = webutil.annotate(web.req, fctx, web.repo.ui)
952
952
953 previousrev = None
953 previousrev = None
954 blockparitygen = paritygen(1)
954 blockparitygen = paritygen(1)
955 for lineno, (aline, l) in enumerate(lines):
955 for lineno, (aline, l) in enumerate(lines):
956 f = aline.fctx
956 f = aline.fctx
957 rev = f.rev()
957 rev = f.rev()
958 if rev != previousrev:
958 if rev != previousrev:
959 blockhead = True
959 blockhead = True
960 blockparity = next(blockparitygen)
960 blockparity = next(blockparitygen)
961 else:
961 else:
962 blockhead = None
962 blockhead = None
963 previousrev = rev
963 previousrev = rev
964 yield {"parity": next(parity),
964 yield {"parity": next(parity),
965 "node": f.hex(),
965 "node": f.hex(),
966 "rev": rev,
966 "rev": rev,
967 "author": f.user(),
967 "author": f.user(),
968 "parents": parents(f),
968 "parents": parents(f),
969 "desc": f.description(),
969 "desc": f.description(),
970 "extra": f.extra(),
970 "extra": f.extra(),
971 "file": f.path(),
971 "file": f.path(),
972 "blockhead": blockhead,
972 "blockhead": blockhead,
973 "blockparity": blockparity,
973 "blockparity": blockparity,
974 "targetline": aline.lineno,
974 "targetline": aline.lineno,
975 "line": l,
975 "line": l,
976 "lineno": lineno + 1,
976 "lineno": lineno + 1,
977 "lineid": "l%d" % (lineno + 1),
977 "lineid": "l%d" % (lineno + 1),
978 "linenumber": "% 6d" % (lineno + 1),
978 "linenumber": "% 6d" % (lineno + 1),
979 "revdate": f.date()}
979 "revdate": f.date()}
980
980
981 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
981 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
982 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
982 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
983
983
984 return web.sendtemplate(
984 return web.sendtemplate(
985 'fileannotate',
985 'fileannotate',
986 file=f,
986 file=f,
987 annotate=annotate,
987 annotate=annotate,
988 path=webutil.up(f),
988 path=webutil.up(f),
989 symrev=webutil.symrevorshortnode(web.req, fctx),
989 symrev=webutil.symrevorshortnode(web.req, fctx),
990 rename=webutil.renamelink(fctx),
990 rename=webutil.renamelink(fctx),
991 permissions=fctx.manifest().flags(f),
991 permissions=fctx.manifest().flags(f),
992 ishead=int(ishead),
992 ishead=int(ishead),
993 diffopts=diffopts,
993 diffopts=diffopts,
994 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
994 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
995
995
996 @webcommand('filelog')
996 @webcommand('filelog')
997 def filelog(web):
997 def filelog(web):
998 """
998 """
999 /filelog/{revision}/{path}
999 /filelog/{revision}/{path}
1000 --------------------------
1000 --------------------------
1001
1001
1002 Show information about the history of a file in the repository.
1002 Show information about the history of a file in the repository.
1003
1003
1004 The ``revcount`` query string argument can be defined to control the
1004 The ``revcount`` query string argument can be defined to control the
1005 maximum number of entries to show.
1005 maximum number of entries to show.
1006
1006
1007 The ``filelog`` template will be rendered.
1007 The ``filelog`` template will be rendered.
1008 """
1008 """
1009
1009
1010 try:
1010 try:
1011 fctx = webutil.filectx(web.repo, web.req)
1011 fctx = webutil.filectx(web.repo, web.req)
1012 f = fctx.path()
1012 f = fctx.path()
1013 fl = fctx.filelog()
1013 fl = fctx.filelog()
1014 except error.LookupError:
1014 except error.LookupError:
1015 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1015 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1016 fl = web.repo.file(f)
1016 fl = web.repo.file(f)
1017 numrevs = len(fl)
1017 numrevs = len(fl)
1018 if not numrevs: # file doesn't exist at all
1018 if not numrevs: # file doesn't exist at all
1019 raise
1019 raise
1020 rev = webutil.changectx(web.repo, web.req).rev()
1020 rev = webutil.changectx(web.repo, web.req).rev()
1021 first = fl.linkrev(0)
1021 first = fl.linkrev(0)
1022 if rev < first: # current rev is from before file existed
1022 if rev < first: # current rev is from before file existed
1023 raise
1023 raise
1024 frev = numrevs - 1
1024 frev = numrevs - 1
1025 while fl.linkrev(frev) > rev:
1025 while fl.linkrev(frev) > rev:
1026 frev -= 1
1026 frev -= 1
1027 fctx = web.repo.filectx(f, fl.linkrev(frev))
1027 fctx = web.repo.filectx(f, fl.linkrev(frev))
1028
1028
1029 revcount = web.maxshortchanges
1029 revcount = web.maxshortchanges
1030 if 'revcount' in web.req.qsparams:
1030 if 'revcount' in web.req.qsparams:
1031 try:
1031 try:
1032 revcount = int(web.req.qsparams.get('revcount', revcount))
1032 revcount = int(web.req.qsparams.get('revcount', revcount))
1033 revcount = max(revcount, 1)
1033 revcount = max(revcount, 1)
1034 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1034 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1035 except ValueError:
1035 except ValueError:
1036 pass
1036 pass
1037
1037
1038 lrange = webutil.linerange(web.req)
1038 lrange = webutil.linerange(web.req)
1039
1039
1040 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1040 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1041 lessvars['revcount'] = max(revcount // 2, 1)
1041 lessvars['revcount'] = max(revcount // 2, 1)
1042 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1042 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1043 morevars['revcount'] = revcount * 2
1043 morevars['revcount'] = revcount * 2
1044
1044
1045 patch = 'patch' in web.req.qsparams
1045 patch = 'patch' in web.req.qsparams
1046 if patch:
1046 if patch:
1047 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1047 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1048 descend = 'descend' in web.req.qsparams
1048 descend = 'descend' in web.req.qsparams
1049 if descend:
1049 if descend:
1050 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1050 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1051
1051
1052 count = fctx.filerev() + 1
1052 count = fctx.filerev() + 1
1053 start = max(0, count - revcount) # first rev on this page
1053 start = max(0, count - revcount) # first rev on this page
1054 end = min(count, start + revcount) # last rev on this page
1054 end = min(count, start + revcount) # last rev on this page
1055 parity = paritygen(web.stripecount, offset=start - end)
1055 parity = paritygen(web.stripecount, offset=start - end)
1056
1056
1057 repo = web.repo
1057 repo = web.repo
1058 revs = fctx.filelog().revs(start, end - 1)
1058 revs = fctx.filelog().revs(start, end - 1)
1059 entries = []
1059 entries = []
1060
1060
1061 diffstyle = web.config('web', 'style')
1061 diffstyle = web.config('web', 'style')
1062 if 'style' in web.req.qsparams:
1062 if 'style' in web.req.qsparams:
1063 diffstyle = web.req.qsparams['style']
1063 diffstyle = web.req.qsparams['style']
1064
1064
1065 def diff(fctx, linerange=None):
1065 def diff(fctx, linerange=None):
1066 ctx = fctx.changectx()
1066 ctx = fctx.changectx()
1067 basectx = ctx.p1()
1067 basectx = ctx.p1()
1068 path = fctx.path()
1068 path = fctx.path()
1069 return webutil.diffs(web, ctx, basectx, [path], diffstyle,
1069 return webutil.diffs(web, ctx, basectx, [path], diffstyle,
1070 linerange=linerange,
1070 linerange=linerange,
1071 lineidprefix='%s-' % ctx.hex()[:12])
1071 lineidprefix='%s-' % ctx.hex()[:12])
1072
1072
1073 linerange = None
1073 linerange = None
1074 if lrange is not None:
1074 if lrange is not None:
1075 linerange = webutil.formatlinerange(*lrange)
1075 linerange = webutil.formatlinerange(*lrange)
1076 # deactivate numeric nav links when linerange is specified as this
1076 # deactivate numeric nav links when linerange is specified as this
1077 # would required a dedicated "revnav" class
1077 # would required a dedicated "revnav" class
1078 nav = None
1078 nav = None
1079 if descend:
1079 if descend:
1080 it = dagop.blockdescendants(fctx, *lrange)
1080 it = dagop.blockdescendants(fctx, *lrange)
1081 else:
1081 else:
1082 it = dagop.blockancestors(fctx, *lrange)
1082 it = dagop.blockancestors(fctx, *lrange)
1083 for i, (c, lr) in enumerate(it, 1):
1083 for i, (c, lr) in enumerate(it, 1):
1084 diffs = None
1084 diffs = None
1085 if patch:
1085 if patch:
1086 diffs = diff(c, linerange=lr)
1086 diffs = diff(c, linerange=lr)
1087 # follow renames accross filtered (not in range) revisions
1087 # follow renames accross filtered (not in range) revisions
1088 path = c.path()
1088 path = c.path()
1089 entries.append(dict(
1089 entries.append(dict(
1090 parity=next(parity),
1090 parity=next(parity),
1091 filerev=c.rev(),
1091 filerev=c.rev(),
1092 file=path,
1092 file=path,
1093 diff=diffs,
1093 diff=diffs,
1094 linerange=webutil.formatlinerange(*lr),
1094 linerange=webutil.formatlinerange(*lr),
1095 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1095 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1096 if i == revcount:
1096 if i == revcount:
1097 break
1097 break
1098 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1098 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1099 morevars['linerange'] = lessvars['linerange']
1099 morevars['linerange'] = lessvars['linerange']
1100 else:
1100 else:
1101 for i in revs:
1101 for i in revs:
1102 iterfctx = fctx.filectx(i)
1102 iterfctx = fctx.filectx(i)
1103 diffs = None
1103 diffs = None
1104 if patch:
1104 if patch:
1105 diffs = diff(iterfctx)
1105 diffs = diff(iterfctx)
1106 entries.append(dict(
1106 entries.append(dict(
1107 parity=next(parity),
1107 parity=next(parity),
1108 filerev=i,
1108 filerev=i,
1109 file=f,
1109 file=f,
1110 diff=diffs,
1110 diff=diffs,
1111 rename=webutil.renamelink(iterfctx),
1111 rename=webutil.renamelink(iterfctx),
1112 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1112 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1113 entries.reverse()
1113 entries.reverse()
1114 revnav = webutil.filerevnav(web.repo, fctx.path())
1114 revnav = webutil.filerevnav(web.repo, fctx.path())
1115 nav = revnav.gen(end - 1, revcount, count)
1115 nav = revnav.gen(end - 1, revcount, count)
1116
1116
1117 latestentry = entries[:1]
1117 latestentry = entries[:1]
1118
1118
1119 return web.sendtemplate(
1119 return web.sendtemplate(
1120 'filelog',
1120 'filelog',
1121 file=f,
1121 file=f,
1122 nav=nav,
1122 nav=nav,
1123 symrev=webutil.symrevorshortnode(web.req, fctx),
1123 symrev=webutil.symrevorshortnode(web.req, fctx),
1124 entries=entries,
1124 entries=entries,
1125 descend=descend,
1125 descend=descend,
1126 patch=patch,
1126 patch=patch,
1127 latestentry=latestentry,
1127 latestentry=latestentry,
1128 linerange=linerange,
1128 linerange=linerange,
1129 revcount=revcount,
1129 revcount=revcount,
1130 morevars=morevars,
1130 morevars=morevars,
1131 lessvars=lessvars,
1131 lessvars=lessvars,
1132 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1132 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1133
1133
1134 @webcommand('archive')
1134 @webcommand('archive')
1135 def archive(web):
1135 def archive(web):
1136 """
1136 """
1137 /archive/{revision}.{format}[/{path}]
1137 /archive/{revision}.{format}[/{path}]
1138 -------------------------------------
1138 -------------------------------------
1139
1139
1140 Obtain an archive of repository content.
1140 Obtain an archive of repository content.
1141
1141
1142 The content and type of the archive is defined by a URL path parameter.
1142 The content and type of the archive is defined by a URL path parameter.
1143 ``format`` is the file extension of the archive type to be generated. e.g.
1143 ``format`` is the file extension of the archive type to be generated. e.g.
1144 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1144 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1145 server configuration.
1145 server configuration.
1146
1146
1147 The optional ``path`` URL parameter controls content to include in the
1147 The optional ``path`` URL parameter controls content to include in the
1148 archive. If omitted, every file in the specified revision is present in the
1148 archive. If omitted, every file in the specified revision is present in the
1149 archive. If included, only the specified file or contents of the specified
1149 archive. If included, only the specified file or contents of the specified
1150 directory will be included in the archive.
1150 directory will be included in the archive.
1151
1151
1152 No template is used for this handler. Raw, binary content is generated.
1152 No template is used for this handler. Raw, binary content is generated.
1153 """
1153 """
1154
1154
1155 type_ = web.req.qsparams.get('type')
1155 type_ = web.req.qsparams.get('type')
1156 allowed = web.configlist("web", "allow_archive")
1156 allowed = web.configlist("web", "allow_archive")
1157 key = web.req.qsparams['node']
1157 key = web.req.qsparams['node']
1158
1158
1159 if type_ not in web.archivespecs:
1159 if type_ not in web.archivespecs:
1160 msg = 'Unsupported archive type: %s' % type_
1160 msg = 'Unsupported archive type: %s' % type_
1161 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1161 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1162
1162
1163 if not ((type_ in allowed or
1163 if not ((type_ in allowed or
1164 web.configbool("web", "allow" + type_))):
1164 web.configbool("web", "allow" + type_))):
1165 msg = 'Archive type not allowed: %s' % type_
1165 msg = 'Archive type not allowed: %s' % type_
1166 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1166 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1167
1167
1168 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1168 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1169 cnode = web.repo.lookup(key)
1169 cnode = web.repo.lookup(key)
1170 arch_version = key
1170 arch_version = key
1171 if cnode == key or key == 'tip':
1171 if cnode == key or key == 'tip':
1172 arch_version = short(cnode)
1172 arch_version = short(cnode)
1173 name = "%s-%s" % (reponame, arch_version)
1173 name = "%s-%s" % (reponame, arch_version)
1174
1174
1175 ctx = webutil.changectx(web.repo, web.req)
1175 ctx = webutil.changectx(web.repo, web.req)
1176 pats = []
1176 pats = []
1177 match = scmutil.match(ctx, [])
1177 match = scmutil.match(ctx, [])
1178 file = web.req.qsparams.get('file')
1178 file = web.req.qsparams.get('file')
1179 if file:
1179 if file:
1180 pats = ['path:' + file]
1180 pats = ['path:' + file]
1181 match = scmutil.match(ctx, pats, default='path')
1181 match = scmutil.match(ctx, pats, default='path')
1182 if pats:
1182 if pats:
1183 files = [f for f in ctx.manifest().keys() if match(f)]
1183 files = [f for f in ctx.manifest().keys() if match(f)]
1184 if not files:
1184 if not files:
1185 raise ErrorResponse(HTTP_NOT_FOUND,
1185 raise ErrorResponse(HTTP_NOT_FOUND,
1186 'file(s) not found: %s' % file)
1186 'file(s) not found: %s' % file)
1187
1187
1188 mimetype, artype, extension, encoding = web.archivespecs[type_]
1188 mimetype, artype, extension, encoding = web.archivespecs[type_]
1189
1189
1190 web.res.headers['Content-Type'] = mimetype
1190 web.res.headers['Content-Type'] = mimetype
1191 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1191 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1192 name, extension)
1192 name, extension)
1193
1193
1194 if encoding:
1194 if encoding:
1195 web.res.headers['Content-Encoding'] = encoding
1195 web.res.headers['Content-Encoding'] = encoding
1196
1196
1197 web.res.setbodywillwrite()
1197 web.res.setbodywillwrite()
1198 assert list(web.res.sendresponse()) == []
1198 if list(web.res.sendresponse()):
1199 raise error.ProgrammingError('sendresponse() should not emit data '
1200 'if writing later')
1199
1201
1200 bodyfh = web.res.getbodyfile()
1202 bodyfh = web.res.getbodyfile()
1201
1203
1202 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1204 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1203 matchfn=match,
1205 matchfn=match,
1204 subrepos=web.configbool("web", "archivesubrepos"))
1206 subrepos=web.configbool("web", "archivesubrepos"))
1205
1207
1206 return []
1208 return []
1207
1209
1208 @webcommand('static')
1210 @webcommand('static')
1209 def static(web):
1211 def static(web):
1210 fname = web.req.qsparams['file']
1212 fname = web.req.qsparams['file']
1211 # a repo owner may set web.static in .hg/hgrc to get any file
1213 # a repo owner may set web.static in .hg/hgrc to get any file
1212 # readable by the user running the CGI script
1214 # readable by the user running the CGI script
1213 static = web.config("web", "static", None, untrusted=False)
1215 static = web.config("web", "static", None, untrusted=False)
1214 if not static:
1216 if not static:
1215 tp = web.templatepath or templater.templatepaths()
1217 tp = web.templatepath or templater.templatepaths()
1216 if isinstance(tp, str):
1218 if isinstance(tp, str):
1217 tp = [tp]
1219 tp = [tp]
1218 static = [os.path.join(p, 'static') for p in tp]
1220 static = [os.path.join(p, 'static') for p in tp]
1219
1221
1220 staticfile(static, fname, web.res)
1222 staticfile(static, fname, web.res)
1221 return web.res.sendresponse()
1223 return web.res.sendresponse()
1222
1224
1223 @webcommand('graph')
1225 @webcommand('graph')
1224 def graph(web):
1226 def graph(web):
1225 """
1227 """
1226 /graph[/{revision}]
1228 /graph[/{revision}]
1227 -------------------
1229 -------------------
1228
1230
1229 Show information about the graphical topology of the repository.
1231 Show information about the graphical topology of the repository.
1230
1232
1231 Information rendered by this handler can be used to create visual
1233 Information rendered by this handler can be used to create visual
1232 representations of repository topology.
1234 representations of repository topology.
1233
1235
1234 The ``revision`` URL parameter controls the starting changeset. If it's
1236 The ``revision`` URL parameter controls the starting changeset. If it's
1235 absent, the default is ``tip``.
1237 absent, the default is ``tip``.
1236
1238
1237 The ``revcount`` query string argument can define the number of changesets
1239 The ``revcount`` query string argument can define the number of changesets
1238 to show information for.
1240 to show information for.
1239
1241
1240 The ``graphtop`` query string argument can specify the starting changeset
1242 The ``graphtop`` query string argument can specify the starting changeset
1241 for producing ``jsdata`` variable that is used for rendering graph in
1243 for producing ``jsdata`` variable that is used for rendering graph in
1242 JavaScript. By default it has the same value as ``revision``.
1244 JavaScript. By default it has the same value as ``revision``.
1243
1245
1244 This handler will render the ``graph`` template.
1246 This handler will render the ``graph`` template.
1245 """
1247 """
1246
1248
1247 if 'node' in web.req.qsparams:
1249 if 'node' in web.req.qsparams:
1248 ctx = webutil.changectx(web.repo, web.req)
1250 ctx = webutil.changectx(web.repo, web.req)
1249 symrev = webutil.symrevorshortnode(web.req, ctx)
1251 symrev = webutil.symrevorshortnode(web.req, ctx)
1250 else:
1252 else:
1251 ctx = web.repo['tip']
1253 ctx = web.repo['tip']
1252 symrev = 'tip'
1254 symrev = 'tip'
1253 rev = ctx.rev()
1255 rev = ctx.rev()
1254
1256
1255 bg_height = 39
1257 bg_height = 39
1256 revcount = web.maxshortchanges
1258 revcount = web.maxshortchanges
1257 if 'revcount' in web.req.qsparams:
1259 if 'revcount' in web.req.qsparams:
1258 try:
1260 try:
1259 revcount = int(web.req.qsparams.get('revcount', revcount))
1261 revcount = int(web.req.qsparams.get('revcount', revcount))
1260 revcount = max(revcount, 1)
1262 revcount = max(revcount, 1)
1261 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1263 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1262 except ValueError:
1264 except ValueError:
1263 pass
1265 pass
1264
1266
1265 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1267 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1266 lessvars['revcount'] = max(revcount // 2, 1)
1268 lessvars['revcount'] = max(revcount // 2, 1)
1267 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1269 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1268 morevars['revcount'] = revcount * 2
1270 morevars['revcount'] = revcount * 2
1269
1271
1270 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1272 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1271 graphvars = copy.copy(web.tmpl.defaults['sessionvars'])
1273 graphvars = copy.copy(web.tmpl.defaults['sessionvars'])
1272 graphvars['graphtop'] = graphtop
1274 graphvars['graphtop'] = graphtop
1273
1275
1274 count = len(web.repo)
1276 count = len(web.repo)
1275 pos = rev
1277 pos = rev
1276
1278
1277 uprev = min(max(0, count - 1), rev + revcount)
1279 uprev = min(max(0, count - 1), rev + revcount)
1278 downrev = max(0, rev - revcount)
1280 downrev = max(0, rev - revcount)
1279 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1281 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1280
1282
1281 tree = []
1283 tree = []
1282 nextentry = []
1284 nextentry = []
1283 lastrev = 0
1285 lastrev = 0
1284 if pos != -1:
1286 if pos != -1:
1285 allrevs = web.repo.changelog.revs(pos, 0)
1287 allrevs = web.repo.changelog.revs(pos, 0)
1286 revs = []
1288 revs = []
1287 for i in allrevs:
1289 for i in allrevs:
1288 revs.append(i)
1290 revs.append(i)
1289 if len(revs) >= revcount + 1:
1291 if len(revs) >= revcount + 1:
1290 break
1292 break
1291
1293
1292 if len(revs) > revcount:
1294 if len(revs) > revcount:
1293 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1295 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1294 revs = revs[:-1]
1296 revs = revs[:-1]
1295
1297
1296 lastrev = revs[-1]
1298 lastrev = revs[-1]
1297
1299
1298 # We have to feed a baseset to dagwalker as it is expecting smartset
1300 # We have to feed a baseset to dagwalker as it is expecting smartset
1299 # object. This does not have a big impact on hgweb performance itself
1301 # object. This does not have a big impact on hgweb performance itself
1300 # since hgweb graphing code is not itself lazy yet.
1302 # since hgweb graphing code is not itself lazy yet.
1301 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1303 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1302 # As we said one line above... not lazy.
1304 # As we said one line above... not lazy.
1303 tree = list(item for item in graphmod.colored(dag, web.repo)
1305 tree = list(item for item in graphmod.colored(dag, web.repo)
1304 if item[1] == graphmod.CHANGESET)
1306 if item[1] == graphmod.CHANGESET)
1305
1307
1306 def nodecurrent(ctx):
1308 def nodecurrent(ctx):
1307 wpnodes = web.repo.dirstate.parents()
1309 wpnodes = web.repo.dirstate.parents()
1308 if wpnodes[1] == nullid:
1310 if wpnodes[1] == nullid:
1309 wpnodes = wpnodes[:1]
1311 wpnodes = wpnodes[:1]
1310 if ctx.node() in wpnodes:
1312 if ctx.node() in wpnodes:
1311 return '@'
1313 return '@'
1312 return ''
1314 return ''
1313
1315
1314 def nodesymbol(ctx):
1316 def nodesymbol(ctx):
1315 if ctx.obsolete():
1317 if ctx.obsolete():
1316 return 'x'
1318 return 'x'
1317 elif ctx.isunstable():
1319 elif ctx.isunstable():
1318 return '*'
1320 return '*'
1319 elif ctx.closesbranch():
1321 elif ctx.closesbranch():
1320 return '_'
1322 return '_'
1321 else:
1323 else:
1322 return 'o'
1324 return 'o'
1323
1325
1324 def fulltree():
1326 def fulltree():
1325 pos = web.repo[graphtop].rev()
1327 pos = web.repo[graphtop].rev()
1326 tree = []
1328 tree = []
1327 if pos != -1:
1329 if pos != -1:
1328 revs = web.repo.changelog.revs(pos, lastrev)
1330 revs = web.repo.changelog.revs(pos, lastrev)
1329 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1331 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1330 tree = list(item for item in graphmod.colored(dag, web.repo)
1332 tree = list(item for item in graphmod.colored(dag, web.repo)
1331 if item[1] == graphmod.CHANGESET)
1333 if item[1] == graphmod.CHANGESET)
1332 return tree
1334 return tree
1333
1335
1334 def jsdata():
1336 def jsdata():
1335 return [{'node': pycompat.bytestr(ctx),
1337 return [{'node': pycompat.bytestr(ctx),
1336 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1338 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1337 'vertex': vtx,
1339 'vertex': vtx,
1338 'edges': edges}
1340 'edges': edges}
1339 for (id, type, ctx, vtx, edges) in fulltree()]
1341 for (id, type, ctx, vtx, edges) in fulltree()]
1340
1342
1341 def nodes():
1343 def nodes():
1342 parity = paritygen(web.stripecount)
1344 parity = paritygen(web.stripecount)
1343 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1345 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1344 entry = webutil.commonentry(web.repo, ctx)
1346 entry = webutil.commonentry(web.repo, ctx)
1345 edgedata = [{'col': edge[0],
1347 edgedata = [{'col': edge[0],
1346 'nextcol': edge[1],
1348 'nextcol': edge[1],
1347 'color': (edge[2] - 1) % 6 + 1,
1349 'color': (edge[2] - 1) % 6 + 1,
1348 'width': edge[3],
1350 'width': edge[3],
1349 'bcolor': edge[4]}
1351 'bcolor': edge[4]}
1350 for edge in edges]
1352 for edge in edges]
1351
1353
1352 entry.update({'col': vtx[0],
1354 entry.update({'col': vtx[0],
1353 'color': (vtx[1] - 1) % 6 + 1,
1355 'color': (vtx[1] - 1) % 6 + 1,
1354 'parity': next(parity),
1356 'parity': next(parity),
1355 'edges': edgedata,
1357 'edges': edgedata,
1356 'row': row,
1358 'row': row,
1357 'nextrow': row + 1})
1359 'nextrow': row + 1})
1358
1360
1359 yield entry
1361 yield entry
1360
1362
1361 rows = len(tree)
1363 rows = len(tree)
1362
1364
1363 return web.sendtemplate(
1365 return web.sendtemplate(
1364 'graph',
1366 'graph',
1365 rev=rev,
1367 rev=rev,
1366 symrev=symrev,
1368 symrev=symrev,
1367 revcount=revcount,
1369 revcount=revcount,
1368 uprev=uprev,
1370 uprev=uprev,
1369 lessvars=lessvars,
1371 lessvars=lessvars,
1370 morevars=morevars,
1372 morevars=morevars,
1371 downrev=downrev,
1373 downrev=downrev,
1372 graphvars=graphvars,
1374 graphvars=graphvars,
1373 rows=rows,
1375 rows=rows,
1374 bg_height=bg_height,
1376 bg_height=bg_height,
1375 changesets=count,
1377 changesets=count,
1376 nextentry=nextentry,
1378 nextentry=nextentry,
1377 jsdata=lambda **x: jsdata(),
1379 jsdata=lambda **x: jsdata(),
1378 nodes=lambda **x: nodes(),
1380 nodes=lambda **x: nodes(),
1379 node=ctx.hex(),
1381 node=ctx.hex(),
1380 changenav=changenav)
1382 changenav=changenav)
1381
1383
1382 def _getdoc(e):
1384 def _getdoc(e):
1383 doc = e[0].__doc__
1385 doc = e[0].__doc__
1384 if doc:
1386 if doc:
1385 doc = _(doc).partition('\n')[0]
1387 doc = _(doc).partition('\n')[0]
1386 else:
1388 else:
1387 doc = _('(no help text available)')
1389 doc = _('(no help text available)')
1388 return doc
1390 return doc
1389
1391
1390 @webcommand('help')
1392 @webcommand('help')
1391 def help(web):
1393 def help(web):
1392 """
1394 """
1393 /help[/{topic}]
1395 /help[/{topic}]
1394 ---------------
1396 ---------------
1395
1397
1396 Render help documentation.
1398 Render help documentation.
1397
1399
1398 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1400 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
1401 is defined, that help topic will be rendered. If not, an index of
1400 available help topics will be rendered.
1402 available help topics will be rendered.
1401
1403
1402 The ``help`` template will be rendered when requesting help for a topic.
1404 The ``help`` template will be rendered when requesting help for a topic.
1403 ``helptopics`` will be rendered for the index of help topics.
1405 ``helptopics`` will be rendered for the index of help topics.
1404 """
1406 """
1405 from .. import commands, help as helpmod # avoid cycle
1407 from .. import commands, help as helpmod # avoid cycle
1406
1408
1407 topicname = web.req.qsparams.get('node')
1409 topicname = web.req.qsparams.get('node')
1408 if not topicname:
1410 if not topicname:
1409 def topics(**map):
1411 def topics(**map):
1410 for entries, summary, _doc in helpmod.helptable:
1412 for entries, summary, _doc in helpmod.helptable:
1411 yield {'topic': entries[0], 'summary': summary}
1413 yield {'topic': entries[0], 'summary': summary}
1412
1414
1413 early, other = [], []
1415 early, other = [], []
1414 primary = lambda s: s.partition('|')[0]
1416 primary = lambda s: s.partition('|')[0]
1415 for c, e in commands.table.iteritems():
1417 for c, e in commands.table.iteritems():
1416 doc = _getdoc(e)
1418 doc = _getdoc(e)
1417 if 'DEPRECATED' in doc or c.startswith('debug'):
1419 if 'DEPRECATED' in doc or c.startswith('debug'):
1418 continue
1420 continue
1419 cmd = primary(c)
1421 cmd = primary(c)
1420 if cmd.startswith('^'):
1422 if cmd.startswith('^'):
1421 early.append((cmd[1:], doc))
1423 early.append((cmd[1:], doc))
1422 else:
1424 else:
1423 other.append((cmd, doc))
1425 other.append((cmd, doc))
1424
1426
1425 early.sort()
1427 early.sort()
1426 other.sort()
1428 other.sort()
1427
1429
1428 def earlycommands(**map):
1430 def earlycommands(**map):
1429 for c, doc in early:
1431 for c, doc in early:
1430 yield {'topic': c, 'summary': doc}
1432 yield {'topic': c, 'summary': doc}
1431
1433
1432 def othercommands(**map):
1434 def othercommands(**map):
1433 for c, doc in other:
1435 for c, doc in other:
1434 yield {'topic': c, 'summary': doc}
1436 yield {'topic': c, 'summary': doc}
1435
1437
1436 return web.sendtemplate(
1438 return web.sendtemplate(
1437 'helptopics',
1439 'helptopics',
1438 topics=topics,
1440 topics=topics,
1439 earlycommands=earlycommands,
1441 earlycommands=earlycommands,
1440 othercommands=othercommands,
1442 othercommands=othercommands,
1441 title='Index')
1443 title='Index')
1442
1444
1443 # Render an index of sub-topics.
1445 # Render an index of sub-topics.
1444 if topicname in helpmod.subtopics:
1446 if topicname in helpmod.subtopics:
1445 topics = []
1447 topics = []
1446 for entries, summary, _doc in helpmod.subtopics[topicname]:
1448 for entries, summary, _doc in helpmod.subtopics[topicname]:
1447 topics.append({
1449 topics.append({
1448 'topic': '%s.%s' % (topicname, entries[0]),
1450 'topic': '%s.%s' % (topicname, entries[0]),
1449 'basename': entries[0],
1451 'basename': entries[0],
1450 'summary': summary,
1452 'summary': summary,
1451 })
1453 })
1452
1454
1453 return web.sendtemplate(
1455 return web.sendtemplate(
1454 'helptopics',
1456 'helptopics',
1455 topics=topics,
1457 topics=topics,
1456 title=topicname,
1458 title=topicname,
1457 subindex=True)
1459 subindex=True)
1458
1460
1459 u = webutil.wsgiui.load()
1461 u = webutil.wsgiui.load()
1460 u.verbose = True
1462 u.verbose = True
1461
1463
1462 # Render a page from a sub-topic.
1464 # Render a page from a sub-topic.
1463 if '.' in topicname:
1465 if '.' in topicname:
1464 # TODO implement support for rendering sections, like
1466 # TODO implement support for rendering sections, like
1465 # `hg help` works.
1467 # `hg help` works.
1466 topic, subtopic = topicname.split('.', 1)
1468 topic, subtopic = topicname.split('.', 1)
1467 if topic not in helpmod.subtopics:
1469 if topic not in helpmod.subtopics:
1468 raise ErrorResponse(HTTP_NOT_FOUND)
1470 raise ErrorResponse(HTTP_NOT_FOUND)
1469 else:
1471 else:
1470 topic = topicname
1472 topic = topicname
1471 subtopic = None
1473 subtopic = None
1472
1474
1473 try:
1475 try:
1474 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1476 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1475 except error.Abort:
1477 except error.Abort:
1476 raise ErrorResponse(HTTP_NOT_FOUND)
1478 raise ErrorResponse(HTTP_NOT_FOUND)
1477
1479
1478 return web.sendtemplate(
1480 return web.sendtemplate(
1479 'help',
1481 'help',
1480 topic=topicname,
1482 topic=topicname,
1481 doc=doc)
1483 doc=doc)
1482
1484
1483 # tell hggettext to extract docstrings from these functions:
1485 # tell hggettext to extract docstrings from these functions:
1484 i18nfunctions = commands.values()
1486 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now