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