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