##// END OF EJS Templates
hgweb: cache fctx.parents() in annotate command (issue5414)...
Gregory Szorc -
r30298:4ed8bb8a stable
parent child Browse files
Show More
@@ -1,1314 +1,1326 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
865 # same revision. So it is worth caching.
866 # TODO there are still redundant operations within basefilectx.parents()
867 # and from the fctx.annotate() call itself that could be cached.
868 parentscache = {}
864 def parents(f):
869 def parents(f):
865 for p in f.parents():
870 rev = f.rev()
866 yield {
871 if rev not in parentscache:
867 "node": p.hex(),
872 parentscache[rev] = []
868 "rev": p.rev(),
873 for p in f.parents():
869 }
874 entry = {
875 'node': p.hex(),
876 'rev': p.rev(),
877 }
878 parentscache[rev].append(entry)
879
880 for p in parentscache[rev]:
881 yield p
870
882
871 def annotate(**map):
883 def annotate(**map):
872 if util.binary(fctx.data()):
884 if util.binary(fctx.data()):
873 mt = (mimetypes.guess_type(fctx.path())[0]
885 mt = (mimetypes.guess_type(fctx.path())[0]
874 or 'application/octet-stream')
886 or 'application/octet-stream')
875 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
887 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
876 else:
888 else:
877 lines = webutil.annotate(fctx, web.repo.ui)
889 lines = webutil.annotate(fctx, web.repo.ui)
878
890
879 previousrev = None
891 previousrev = None
880 blockparitygen = paritygen(1)
892 blockparitygen = paritygen(1)
881 for lineno, ((f, targetline), l) in enumerate(lines):
893 for lineno, ((f, targetline), l) in enumerate(lines):
882 rev = f.rev()
894 rev = f.rev()
883 if rev != previousrev:
895 if rev != previousrev:
884 blockhead = True
896 blockhead = True
885 blockparity = next(blockparitygen)
897 blockparity = next(blockparitygen)
886 else:
898 else:
887 blockhead = None
899 blockhead = None
888 previousrev = rev
900 previousrev = rev
889 yield {"parity": next(parity),
901 yield {"parity": next(parity),
890 "node": f.hex(),
902 "node": f.hex(),
891 "rev": rev,
903 "rev": rev,
892 "author": f.user(),
904 "author": f.user(),
893 "parents": parents(f),
905 "parents": parents(f),
894 "desc": f.description(),
906 "desc": f.description(),
895 "extra": f.extra(),
907 "extra": f.extra(),
896 "file": f.path(),
908 "file": f.path(),
897 "blockhead": blockhead,
909 "blockhead": blockhead,
898 "blockparity": blockparity,
910 "blockparity": blockparity,
899 "targetline": targetline,
911 "targetline": targetline,
900 "line": l,
912 "line": l,
901 "lineno": lineno + 1,
913 "lineno": lineno + 1,
902 "lineid": "l%d" % (lineno + 1),
914 "lineid": "l%d" % (lineno + 1),
903 "linenumber": "% 6d" % (lineno + 1),
915 "linenumber": "% 6d" % (lineno + 1),
904 "revdate": f.date()}
916 "revdate": f.date()}
905
917
906 return tmpl("fileannotate",
918 return tmpl("fileannotate",
907 file=f,
919 file=f,
908 annotate=annotate,
920 annotate=annotate,
909 path=webutil.up(f),
921 path=webutil.up(f),
910 symrev=webutil.symrevorshortnode(req, fctx),
922 symrev=webutil.symrevorshortnode(req, fctx),
911 rename=webutil.renamelink(fctx),
923 rename=webutil.renamelink(fctx),
912 permissions=fctx.manifest().flags(f),
924 permissions=fctx.manifest().flags(f),
913 **webutil.commonentry(web.repo, fctx))
925 **webutil.commonentry(web.repo, fctx))
914
926
915 @webcommand('filelog')
927 @webcommand('filelog')
916 def filelog(web, req, tmpl):
928 def filelog(web, req, tmpl):
917 """
929 """
918 /filelog/{revision}/{path}
930 /filelog/{revision}/{path}
919 --------------------------
931 --------------------------
920
932
921 Show information about the history of a file in the repository.
933 Show information about the history of a file in the repository.
922
934
923 The ``revcount`` query string argument can be defined to control the
935 The ``revcount`` query string argument can be defined to control the
924 maximum number of entries to show.
936 maximum number of entries to show.
925
937
926 The ``filelog`` template will be rendered.
938 The ``filelog`` template will be rendered.
927 """
939 """
928
940
929 try:
941 try:
930 fctx = webutil.filectx(web.repo, req)
942 fctx = webutil.filectx(web.repo, req)
931 f = fctx.path()
943 f = fctx.path()
932 fl = fctx.filelog()
944 fl = fctx.filelog()
933 except error.LookupError:
945 except error.LookupError:
934 f = webutil.cleanpath(web.repo, req.form['file'][0])
946 f = webutil.cleanpath(web.repo, req.form['file'][0])
935 fl = web.repo.file(f)
947 fl = web.repo.file(f)
936 numrevs = len(fl)
948 numrevs = len(fl)
937 if not numrevs: # file doesn't exist at all
949 if not numrevs: # file doesn't exist at all
938 raise
950 raise
939 rev = webutil.changectx(web.repo, req).rev()
951 rev = webutil.changectx(web.repo, req).rev()
940 first = fl.linkrev(0)
952 first = fl.linkrev(0)
941 if rev < first: # current rev is from before file existed
953 if rev < first: # current rev is from before file existed
942 raise
954 raise
943 frev = numrevs - 1
955 frev = numrevs - 1
944 while fl.linkrev(frev) > rev:
956 while fl.linkrev(frev) > rev:
945 frev -= 1
957 frev -= 1
946 fctx = web.repo.filectx(f, fl.linkrev(frev))
958 fctx = web.repo.filectx(f, fl.linkrev(frev))
947
959
948 revcount = web.maxshortchanges
960 revcount = web.maxshortchanges
949 if 'revcount' in req.form:
961 if 'revcount' in req.form:
950 try:
962 try:
951 revcount = int(req.form.get('revcount', [revcount])[0])
963 revcount = int(req.form.get('revcount', [revcount])[0])
952 revcount = max(revcount, 1)
964 revcount = max(revcount, 1)
953 tmpl.defaults['sessionvars']['revcount'] = revcount
965 tmpl.defaults['sessionvars']['revcount'] = revcount
954 except ValueError:
966 except ValueError:
955 pass
967 pass
956
968
957 lessvars = copy.copy(tmpl.defaults['sessionvars'])
969 lessvars = copy.copy(tmpl.defaults['sessionvars'])
958 lessvars['revcount'] = max(revcount / 2, 1)
970 lessvars['revcount'] = max(revcount / 2, 1)
959 morevars = copy.copy(tmpl.defaults['sessionvars'])
971 morevars = copy.copy(tmpl.defaults['sessionvars'])
960 morevars['revcount'] = revcount * 2
972 morevars['revcount'] = revcount * 2
961
973
962 count = fctx.filerev() + 1
974 count = fctx.filerev() + 1
963 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
964 end = min(count, start + revcount) # last rev on this page
976 end = min(count, start + revcount) # last rev on this page
965 parity = paritygen(web.stripecount, offset=start - end)
977 parity = paritygen(web.stripecount, offset=start - end)
966
978
967 def entries():
979 def entries():
968 l = []
980 l = []
969
981
970 repo = web.repo
982 repo = web.repo
971 revs = fctx.filelog().revs(start, end - 1)
983 revs = fctx.filelog().revs(start, end - 1)
972 for i in revs:
984 for i in revs:
973 iterfctx = fctx.filectx(i)
985 iterfctx = fctx.filectx(i)
974
986
975 l.append(dict(
987 l.append(dict(
976 parity=next(parity),
988 parity=next(parity),
977 filerev=i,
989 filerev=i,
978 file=f,
990 file=f,
979 rename=webutil.renamelink(iterfctx),
991 rename=webutil.renamelink(iterfctx),
980 **webutil.commonentry(repo, iterfctx)))
992 **webutil.commonentry(repo, iterfctx)))
981 for e in reversed(l):
993 for e in reversed(l):
982 yield e
994 yield e
983
995
984 entries = list(entries())
996 entries = list(entries())
985 latestentry = entries[:1]
997 latestentry = entries[:1]
986
998
987 revnav = webutil.filerevnav(web.repo, fctx.path())
999 revnav = webutil.filerevnav(web.repo, fctx.path())
988 nav = revnav.gen(end - 1, revcount, count)
1000 nav = revnav.gen(end - 1, revcount, count)
989 return tmpl("filelog",
1001 return tmpl("filelog",
990 file=f,
1002 file=f,
991 nav=nav,
1003 nav=nav,
992 symrev=webutil.symrevorshortnode(req, fctx),
1004 symrev=webutil.symrevorshortnode(req, fctx),
993 entries=entries,
1005 entries=entries,
994 latestentry=latestentry,
1006 latestentry=latestentry,
995 revcount=revcount,
1007 revcount=revcount,
996 morevars=morevars,
1008 morevars=morevars,
997 lessvars=lessvars,
1009 lessvars=lessvars,
998 **webutil.commonentry(web.repo, fctx))
1010 **webutil.commonentry(web.repo, fctx))
999
1011
1000 @webcommand('archive')
1012 @webcommand('archive')
1001 def archive(web, req, tmpl):
1013 def archive(web, req, tmpl):
1002 """
1014 """
1003 /archive/{revision}.{format}[/{path}]
1015 /archive/{revision}.{format}[/{path}]
1004 -------------------------------------
1016 -------------------------------------
1005
1017
1006 Obtain an archive of repository content.
1018 Obtain an archive of repository content.
1007
1019
1008 The content and type of the archive is defined by a URL path parameter.
1020 The content and type of the archive is defined by a URL path parameter.
1009 ``format`` is the file extension of the archive type to be generated. e.g.
1021 ``format`` is the file extension of the archive type to be generated. e.g.
1010 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1022 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1011 server configuration.
1023 server configuration.
1012
1024
1013 The optional ``path`` URL parameter controls content to include in the
1025 The optional ``path`` URL parameter controls content to include in the
1014 archive. If omitted, every file in the specified revision is present in the
1026 archive. If omitted, every file in the specified revision is present in the
1015 archive. If included, only the specified file or contents of the specified
1027 archive. If included, only the specified file or contents of the specified
1016 directory will be included in the archive.
1028 directory will be included in the archive.
1017
1029
1018 No template is used for this handler. Raw, binary content is generated.
1030 No template is used for this handler. Raw, binary content is generated.
1019 """
1031 """
1020
1032
1021 type_ = req.form.get('type', [None])[0]
1033 type_ = req.form.get('type', [None])[0]
1022 allowed = web.configlist("web", "allow_archive")
1034 allowed = web.configlist("web", "allow_archive")
1023 key = req.form['node'][0]
1035 key = req.form['node'][0]
1024
1036
1025 if type_ not in web.archives:
1037 if type_ not in web.archives:
1026 msg = 'Unsupported archive type: %s' % type_
1038 msg = 'Unsupported archive type: %s' % type_
1027 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1039 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1028
1040
1029 if not ((type_ in allowed or
1041 if not ((type_ in allowed or
1030 web.configbool("web", "allow" + type_, False))):
1042 web.configbool("web", "allow" + type_, False))):
1031 msg = 'Archive type not allowed: %s' % type_
1043 msg = 'Archive type not allowed: %s' % type_
1032 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1044 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1033
1045
1034 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1046 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1035 cnode = web.repo.lookup(key)
1047 cnode = web.repo.lookup(key)
1036 arch_version = key
1048 arch_version = key
1037 if cnode == key or key == 'tip':
1049 if cnode == key or key == 'tip':
1038 arch_version = short(cnode)
1050 arch_version = short(cnode)
1039 name = "%s-%s" % (reponame, arch_version)
1051 name = "%s-%s" % (reponame, arch_version)
1040
1052
1041 ctx = webutil.changectx(web.repo, req)
1053 ctx = webutil.changectx(web.repo, req)
1042 pats = []
1054 pats = []
1043 matchfn = scmutil.match(ctx, [])
1055 matchfn = scmutil.match(ctx, [])
1044 file = req.form.get('file', None)
1056 file = req.form.get('file', None)
1045 if file:
1057 if file:
1046 pats = ['path:' + file[0]]
1058 pats = ['path:' + file[0]]
1047 matchfn = scmutil.match(ctx, pats, default='path')
1059 matchfn = scmutil.match(ctx, pats, default='path')
1048 if pats:
1060 if pats:
1049 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1061 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1050 if not files:
1062 if not files:
1051 raise ErrorResponse(HTTP_NOT_FOUND,
1063 raise ErrorResponse(HTTP_NOT_FOUND,
1052 'file(s) not found: %s' % file[0])
1064 'file(s) not found: %s' % file[0])
1053
1065
1054 mimetype, artype, extension, encoding = web.archivespecs[type_]
1066 mimetype, artype, extension, encoding = web.archivespecs[type_]
1055 headers = [
1067 headers = [
1056 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1068 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1057 ]
1069 ]
1058 if encoding:
1070 if encoding:
1059 headers.append(('Content-Encoding', encoding))
1071 headers.append(('Content-Encoding', encoding))
1060 req.headers.extend(headers)
1072 req.headers.extend(headers)
1061 req.respond(HTTP_OK, mimetype)
1073 req.respond(HTTP_OK, mimetype)
1062
1074
1063 archival.archive(web.repo, req, cnode, artype, prefix=name,
1075 archival.archive(web.repo, req, cnode, artype, prefix=name,
1064 matchfn=matchfn,
1076 matchfn=matchfn,
1065 subrepos=web.configbool("web", "archivesubrepos"))
1077 subrepos=web.configbool("web", "archivesubrepos"))
1066 return []
1078 return []
1067
1079
1068
1080
1069 @webcommand('static')
1081 @webcommand('static')
1070 def static(web, req, tmpl):
1082 def static(web, req, tmpl):
1071 fname = req.form['file'][0]
1083 fname = req.form['file'][0]
1072 # a repo owner may set web.static in .hg/hgrc to get any file
1084 # a repo owner may set web.static in .hg/hgrc to get any file
1073 # readable by the user running the CGI script
1085 # readable by the user running the CGI script
1074 static = web.config("web", "static", None, untrusted=False)
1086 static = web.config("web", "static", None, untrusted=False)
1075 if not static:
1087 if not static:
1076 tp = web.templatepath or templater.templatepaths()
1088 tp = web.templatepath or templater.templatepaths()
1077 if isinstance(tp, str):
1089 if isinstance(tp, str):
1078 tp = [tp]
1090 tp = [tp]
1079 static = [os.path.join(p, 'static') for p in tp]
1091 static = [os.path.join(p, 'static') for p in tp]
1080 staticfile(static, fname, req)
1092 staticfile(static, fname, req)
1081 return []
1093 return []
1082
1094
1083 @webcommand('graph')
1095 @webcommand('graph')
1084 def graph(web, req, tmpl):
1096 def graph(web, req, tmpl):
1085 """
1097 """
1086 /graph[/{revision}]
1098 /graph[/{revision}]
1087 -------------------
1099 -------------------
1088
1100
1089 Show information about the graphical topology of the repository.
1101 Show information about the graphical topology of the repository.
1090
1102
1091 Information rendered by this handler can be used to create visual
1103 Information rendered by this handler can be used to create visual
1092 representations of repository topology.
1104 representations of repository topology.
1093
1105
1094 The ``revision`` URL parameter controls the starting changeset.
1106 The ``revision`` URL parameter controls the starting changeset.
1095
1107
1096 The ``revcount`` query string argument can define the number of changesets
1108 The ``revcount`` query string argument can define the number of changesets
1097 to show information for.
1109 to show information for.
1098
1110
1099 This handler will render the ``graph`` template.
1111 This handler will render the ``graph`` template.
1100 """
1112 """
1101
1113
1102 if 'node' in req.form:
1114 if 'node' in req.form:
1103 ctx = webutil.changectx(web.repo, req)
1115 ctx = webutil.changectx(web.repo, req)
1104 symrev = webutil.symrevorshortnode(req, ctx)
1116 symrev = webutil.symrevorshortnode(req, ctx)
1105 else:
1117 else:
1106 ctx = web.repo['tip']
1118 ctx = web.repo['tip']
1107 symrev = 'tip'
1119 symrev = 'tip'
1108 rev = ctx.rev()
1120 rev = ctx.rev()
1109
1121
1110 bg_height = 39
1122 bg_height = 39
1111 revcount = web.maxshortchanges
1123 revcount = web.maxshortchanges
1112 if 'revcount' in req.form:
1124 if 'revcount' in req.form:
1113 try:
1125 try:
1114 revcount = int(req.form.get('revcount', [revcount])[0])
1126 revcount = int(req.form.get('revcount', [revcount])[0])
1115 revcount = max(revcount, 1)
1127 revcount = max(revcount, 1)
1116 tmpl.defaults['sessionvars']['revcount'] = revcount
1128 tmpl.defaults['sessionvars']['revcount'] = revcount
1117 except ValueError:
1129 except ValueError:
1118 pass
1130 pass
1119
1131
1120 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1132 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1121 lessvars['revcount'] = max(revcount / 2, 1)
1133 lessvars['revcount'] = max(revcount / 2, 1)
1122 morevars = copy.copy(tmpl.defaults['sessionvars'])
1134 morevars = copy.copy(tmpl.defaults['sessionvars'])
1123 morevars['revcount'] = revcount * 2
1135 morevars['revcount'] = revcount * 2
1124
1136
1125 count = len(web.repo)
1137 count = len(web.repo)
1126 pos = rev
1138 pos = rev
1127
1139
1128 uprev = min(max(0, count - 1), rev + revcount)
1140 uprev = min(max(0, count - 1), rev + revcount)
1129 downrev = max(0, rev - revcount)
1141 downrev = max(0, rev - revcount)
1130 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1142 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1131
1143
1132 tree = []
1144 tree = []
1133 if pos != -1:
1145 if pos != -1:
1134 allrevs = web.repo.changelog.revs(pos, 0)
1146 allrevs = web.repo.changelog.revs(pos, 0)
1135 revs = []
1147 revs = []
1136 for i in allrevs:
1148 for i in allrevs:
1137 revs.append(i)
1149 revs.append(i)
1138 if len(revs) >= revcount:
1150 if len(revs) >= revcount:
1139 break
1151 break
1140
1152
1141 # We have to feed a baseset to dagwalker as it is expecting smartset
1153 # We have to feed a baseset to dagwalker as it is expecting smartset
1142 # object. This does not have a big impact on hgweb performance itself
1154 # object. This does not have a big impact on hgweb performance itself
1143 # since hgweb graphing code is not itself lazy yet.
1155 # since hgweb graphing code is not itself lazy yet.
1144 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1156 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1145 # As we said one line above... not lazy.
1157 # As we said one line above... not lazy.
1146 tree = list(graphmod.colored(dag, web.repo))
1158 tree = list(graphmod.colored(dag, web.repo))
1147
1159
1148 def getcolumns(tree):
1160 def getcolumns(tree):
1149 cols = 0
1161 cols = 0
1150 for (id, type, ctx, vtx, edges) in tree:
1162 for (id, type, ctx, vtx, edges) in tree:
1151 if type != graphmod.CHANGESET:
1163 if type != graphmod.CHANGESET:
1152 continue
1164 continue
1153 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1165 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1154 max([edge[1] for edge in edges] or [0]))
1166 max([edge[1] for edge in edges] or [0]))
1155 return cols
1167 return cols
1156
1168
1157 def graphdata(usetuples, encodestr):
1169 def graphdata(usetuples, encodestr):
1158 data = []
1170 data = []
1159
1171
1160 row = 0
1172 row = 0
1161 for (id, type, ctx, vtx, edges) in tree:
1173 for (id, type, ctx, vtx, edges) in tree:
1162 if type != graphmod.CHANGESET:
1174 if type != graphmod.CHANGESET:
1163 continue
1175 continue
1164 node = str(ctx)
1176 node = str(ctx)
1165 age = encodestr(templatefilters.age(ctx.date()))
1177 age = encodestr(templatefilters.age(ctx.date()))
1166 desc = templatefilters.firstline(encodestr(ctx.description()))
1178 desc = templatefilters.firstline(encodestr(ctx.description()))
1167 desc = cgi.escape(templatefilters.nonempty(desc))
1179 desc = cgi.escape(templatefilters.nonempty(desc))
1168 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1180 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1169 branch = cgi.escape(encodestr(ctx.branch()))
1181 branch = cgi.escape(encodestr(ctx.branch()))
1170 try:
1182 try:
1171 branchnode = web.repo.branchtip(branch)
1183 branchnode = web.repo.branchtip(branch)
1172 except error.RepoLookupError:
1184 except error.RepoLookupError:
1173 branchnode = None
1185 branchnode = None
1174 branch = branch, branchnode == ctx.node()
1186 branch = branch, branchnode == ctx.node()
1175
1187
1176 if usetuples:
1188 if usetuples:
1177 data.append((node, vtx, edges, desc, user, age, branch,
1189 data.append((node, vtx, edges, desc, user, age, branch,
1178 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1190 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1179 [cgi.escape(encodestr(x))
1191 [cgi.escape(encodestr(x))
1180 for x in ctx.bookmarks()]))
1192 for x in ctx.bookmarks()]))
1181 else:
1193 else:
1182 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1194 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1183 'color': (edge[2] - 1) % 6 + 1,
1195 'color': (edge[2] - 1) % 6 + 1,
1184 'width': edge[3], 'bcolor': edge[4]}
1196 'width': edge[3], 'bcolor': edge[4]}
1185 for edge in edges]
1197 for edge in edges]
1186
1198
1187 data.append(
1199 data.append(
1188 {'node': node,
1200 {'node': node,
1189 'col': vtx[0],
1201 'col': vtx[0],
1190 'color': (vtx[1] - 1) % 6 + 1,
1202 'color': (vtx[1] - 1) % 6 + 1,
1191 'edges': edgedata,
1203 'edges': edgedata,
1192 'row': row,
1204 'row': row,
1193 'nextrow': row + 1,
1205 'nextrow': row + 1,
1194 'desc': desc,
1206 'desc': desc,
1195 'user': user,
1207 'user': user,
1196 'age': age,
1208 'age': age,
1197 'bookmarks': webutil.nodebookmarksdict(
1209 'bookmarks': webutil.nodebookmarksdict(
1198 web.repo, ctx.node()),
1210 web.repo, ctx.node()),
1199 'branches': webutil.nodebranchdict(web.repo, ctx),
1211 'branches': webutil.nodebranchdict(web.repo, ctx),
1200 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1212 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1201 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1213 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1202
1214
1203 row += 1
1215 row += 1
1204
1216
1205 return data
1217 return data
1206
1218
1207 cols = getcolumns(tree)
1219 cols = getcolumns(tree)
1208 rows = len(tree)
1220 rows = len(tree)
1209 canvasheight = (rows + 1) * bg_height - 27
1221 canvasheight = (rows + 1) * bg_height - 27
1210
1222
1211 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1223 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1212 uprev=uprev,
1224 uprev=uprev,
1213 lessvars=lessvars, morevars=morevars, downrev=downrev,
1225 lessvars=lessvars, morevars=morevars, downrev=downrev,
1214 cols=cols, rows=rows,
1226 cols=cols, rows=rows,
1215 canvaswidth=(cols + 1) * bg_height,
1227 canvaswidth=(cols + 1) * bg_height,
1216 truecanvasheight=rows * bg_height,
1228 truecanvasheight=rows * bg_height,
1217 canvasheight=canvasheight, bg_height=bg_height,
1229 canvasheight=canvasheight, bg_height=bg_height,
1218 # {jsdata} will be passed to |json, so it must be in utf-8
1230 # {jsdata} will be passed to |json, so it must be in utf-8
1219 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1231 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1220 nodes=lambda **x: graphdata(False, str),
1232 nodes=lambda **x: graphdata(False, str),
1221 node=ctx.hex(), changenav=changenav)
1233 node=ctx.hex(), changenav=changenav)
1222
1234
1223 def _getdoc(e):
1235 def _getdoc(e):
1224 doc = e[0].__doc__
1236 doc = e[0].__doc__
1225 if doc:
1237 if doc:
1226 doc = _(doc).partition('\n')[0]
1238 doc = _(doc).partition('\n')[0]
1227 else:
1239 else:
1228 doc = _('(no help text available)')
1240 doc = _('(no help text available)')
1229 return doc
1241 return doc
1230
1242
1231 @webcommand('help')
1243 @webcommand('help')
1232 def help(web, req, tmpl):
1244 def help(web, req, tmpl):
1233 """
1245 """
1234 /help[/{topic}]
1246 /help[/{topic}]
1235 ---------------
1247 ---------------
1236
1248
1237 Render help documentation.
1249 Render help documentation.
1238
1250
1239 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1251 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1240 is defined, that help topic will be rendered. If not, an index of
1252 is defined, that help topic will be rendered. If not, an index of
1241 available help topics will be rendered.
1253 available help topics will be rendered.
1242
1254
1243 The ``help`` template will be rendered when requesting help for a topic.
1255 The ``help`` template will be rendered when requesting help for a topic.
1244 ``helptopics`` will be rendered for the index of help topics.
1256 ``helptopics`` will be rendered for the index of help topics.
1245 """
1257 """
1246 from .. import commands, help as helpmod # avoid cycle
1258 from .. import commands, help as helpmod # avoid cycle
1247
1259
1248 topicname = req.form.get('node', [None])[0]
1260 topicname = req.form.get('node', [None])[0]
1249 if not topicname:
1261 if not topicname:
1250 def topics(**map):
1262 def topics(**map):
1251 for entries, summary, _doc in helpmod.helptable:
1263 for entries, summary, _doc in helpmod.helptable:
1252 yield {'topic': entries[0], 'summary': summary}
1264 yield {'topic': entries[0], 'summary': summary}
1253
1265
1254 early, other = [], []
1266 early, other = [], []
1255 primary = lambda s: s.partition('|')[0]
1267 primary = lambda s: s.partition('|')[0]
1256 for c, e in commands.table.iteritems():
1268 for c, e in commands.table.iteritems():
1257 doc = _getdoc(e)
1269 doc = _getdoc(e)
1258 if 'DEPRECATED' in doc or c.startswith('debug'):
1270 if 'DEPRECATED' in doc or c.startswith('debug'):
1259 continue
1271 continue
1260 cmd = primary(c)
1272 cmd = primary(c)
1261 if cmd.startswith('^'):
1273 if cmd.startswith('^'):
1262 early.append((cmd[1:], doc))
1274 early.append((cmd[1:], doc))
1263 else:
1275 else:
1264 other.append((cmd, doc))
1276 other.append((cmd, doc))
1265
1277
1266 early.sort()
1278 early.sort()
1267 other.sort()
1279 other.sort()
1268
1280
1269 def earlycommands(**map):
1281 def earlycommands(**map):
1270 for c, doc in early:
1282 for c, doc in early:
1271 yield {'topic': c, 'summary': doc}
1283 yield {'topic': c, 'summary': doc}
1272
1284
1273 def othercommands(**map):
1285 def othercommands(**map):
1274 for c, doc in other:
1286 for c, doc in other:
1275 yield {'topic': c, 'summary': doc}
1287 yield {'topic': c, 'summary': doc}
1276
1288
1277 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1289 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1278 othercommands=othercommands, title='Index')
1290 othercommands=othercommands, title='Index')
1279
1291
1280 # Render an index of sub-topics.
1292 # Render an index of sub-topics.
1281 if topicname in helpmod.subtopics:
1293 if topicname in helpmod.subtopics:
1282 topics = []
1294 topics = []
1283 for entries, summary, _doc in helpmod.subtopics[topicname]:
1295 for entries, summary, _doc in helpmod.subtopics[topicname]:
1284 topics.append({
1296 topics.append({
1285 'topic': '%s.%s' % (topicname, entries[0]),
1297 'topic': '%s.%s' % (topicname, entries[0]),
1286 'basename': entries[0],
1298 'basename': entries[0],
1287 'summary': summary,
1299 'summary': summary,
1288 })
1300 })
1289
1301
1290 return tmpl('helptopics', topics=topics, title=topicname,
1302 return tmpl('helptopics', topics=topics, title=topicname,
1291 subindex=True)
1303 subindex=True)
1292
1304
1293 u = webutil.wsgiui()
1305 u = webutil.wsgiui()
1294 u.verbose = True
1306 u.verbose = True
1295
1307
1296 # Render a page from a sub-topic.
1308 # Render a page from a sub-topic.
1297 if '.' in topicname:
1309 if '.' in topicname:
1298 # TODO implement support for rendering sections, like
1310 # TODO implement support for rendering sections, like
1299 # `hg help` works.
1311 # `hg help` works.
1300 topic, subtopic = topicname.split('.', 1)
1312 topic, subtopic = topicname.split('.', 1)
1301 if topic not in helpmod.subtopics:
1313 if topic not in helpmod.subtopics:
1302 raise ErrorResponse(HTTP_NOT_FOUND)
1314 raise ErrorResponse(HTTP_NOT_FOUND)
1303 else:
1315 else:
1304 topic = topicname
1316 topic = topicname
1305 subtopic = None
1317 subtopic = None
1306
1318
1307 try:
1319 try:
1308 doc = helpmod.help_(u, topic, subtopic=subtopic)
1320 doc = helpmod.help_(u, topic, subtopic=subtopic)
1309 except error.UnknownCommand:
1321 except error.UnknownCommand:
1310 raise ErrorResponse(HTTP_NOT_FOUND)
1322 raise ErrorResponse(HTTP_NOT_FOUND)
1311 return tmpl('help', topic=topicname, doc=doc)
1323 return tmpl('help', topic=topicname, doc=doc)
1312
1324
1313 # tell hggettext to extract docstrings from these functions:
1325 # tell hggettext to extract docstrings from these functions:
1314 i18nfunctions = commands.values()
1326 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now