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