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