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