##// END OF EJS Templates
hgweb: use archivespecs (dict) instead of archives (tuple) for "in" check
av6 -
r30734:b9e49f7b default
parent child Browse files
Show More
@@ -1,1326 +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
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)
977 parity = paritygen(web.stripecount, offset=start - end)
978
978
979 def entries():
979 def entries():
980 l = []
980 l = []
981
981
982 repo = web.repo
982 repo = web.repo
983 revs = fctx.filelog().revs(start, end - 1)
983 revs = fctx.filelog().revs(start, end - 1)
984 for i in revs:
984 for i in revs:
985 iterfctx = fctx.filectx(i)
985 iterfctx = fctx.filectx(i)
986
986
987 l.append(dict(
987 l.append(dict(
988 parity=next(parity),
988 parity=next(parity),
989 filerev=i,
989 filerev=i,
990 file=f,
990 file=f,
991 rename=webutil.renamelink(iterfctx),
991 rename=webutil.renamelink(iterfctx),
992 **webutil.commonentry(repo, iterfctx)))
992 **webutil.commonentry(repo, iterfctx)))
993 for e in reversed(l):
993 for e in reversed(l):
994 yield e
994 yield e
995
995
996 entries = list(entries())
996 entries = list(entries())
997 latestentry = entries[:1]
997 latestentry = entries[:1]
998
998
999 revnav = webutil.filerevnav(web.repo, fctx.path())
999 revnav = webutil.filerevnav(web.repo, fctx.path())
1000 nav = revnav.gen(end - 1, revcount, count)
1000 nav = revnav.gen(end - 1, revcount, count)
1001 return tmpl("filelog",
1001 return tmpl("filelog",
1002 file=f,
1002 file=f,
1003 nav=nav,
1003 nav=nav,
1004 symrev=webutil.symrevorshortnode(req, fctx),
1004 symrev=webutil.symrevorshortnode(req, fctx),
1005 entries=entries,
1005 entries=entries,
1006 latestentry=latestentry,
1006 latestentry=latestentry,
1007 revcount=revcount,
1007 revcount=revcount,
1008 morevars=morevars,
1008 morevars=morevars,
1009 lessvars=lessvars,
1009 lessvars=lessvars,
1010 **webutil.commonentry(web.repo, fctx))
1010 **webutil.commonentry(web.repo, fctx))
1011
1011
1012 @webcommand('archive')
1012 @webcommand('archive')
1013 def archive(web, req, tmpl):
1013 def archive(web, req, tmpl):
1014 """
1014 """
1015 /archive/{revision}.{format}[/{path}]
1015 /archive/{revision}.{format}[/{path}]
1016 -------------------------------------
1016 -------------------------------------
1017
1017
1018 Obtain an archive of repository content.
1018 Obtain an archive of repository content.
1019
1019
1020 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.
1021 ``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.
1022 ``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
1023 server configuration.
1023 server configuration.
1024
1024
1025 The optional ``path`` URL parameter controls content to include in the
1025 The optional ``path`` URL parameter controls content to include in the
1026 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
1027 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
1028 directory will be included in the archive.
1028 directory will be included in the archive.
1029
1029
1030 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.
1031 """
1031 """
1032
1032
1033 type_ = req.form.get('type', [None])[0]
1033 type_ = req.form.get('type', [None])[0]
1034 allowed = web.configlist("web", "allow_archive")
1034 allowed = web.configlist("web", "allow_archive")
1035 key = req.form['node'][0]
1035 key = req.form['node'][0]
1036
1036
1037 if type_ not in web.archives:
1037 if type_ not in web.archivespecs:
1038 msg = 'Unsupported archive type: %s' % type_
1038 msg = 'Unsupported archive type: %s' % type_
1039 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1039 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1040
1040
1041 if not ((type_ in allowed or
1041 if not ((type_ in allowed or
1042 web.configbool("web", "allow" + type_, False))):
1042 web.configbool("web", "allow" + type_, False))):
1043 msg = 'Archive type not allowed: %s' % type_
1043 msg = 'Archive type not allowed: %s' % type_
1044 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1044 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1045
1045
1046 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1046 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1047 cnode = web.repo.lookup(key)
1047 cnode = web.repo.lookup(key)
1048 arch_version = key
1048 arch_version = key
1049 if cnode == key or key == 'tip':
1049 if cnode == key or key == 'tip':
1050 arch_version = short(cnode)
1050 arch_version = short(cnode)
1051 name = "%s-%s" % (reponame, arch_version)
1051 name = "%s-%s" % (reponame, arch_version)
1052
1052
1053 ctx = webutil.changectx(web.repo, req)
1053 ctx = webutil.changectx(web.repo, req)
1054 pats = []
1054 pats = []
1055 matchfn = scmutil.match(ctx, [])
1055 matchfn = scmutil.match(ctx, [])
1056 file = req.form.get('file', None)
1056 file = req.form.get('file', None)
1057 if file:
1057 if file:
1058 pats = ['path:' + file[0]]
1058 pats = ['path:' + file[0]]
1059 matchfn = scmutil.match(ctx, pats, default='path')
1059 matchfn = scmutil.match(ctx, pats, default='path')
1060 if pats:
1060 if pats:
1061 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1061 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1062 if not files:
1062 if not files:
1063 raise ErrorResponse(HTTP_NOT_FOUND,
1063 raise ErrorResponse(HTTP_NOT_FOUND,
1064 'file(s) not found: %s' % file[0])
1064 'file(s) not found: %s' % file[0])
1065
1065
1066 mimetype, artype, extension, encoding = web.archivespecs[type_]
1066 mimetype, artype, extension, encoding = web.archivespecs[type_]
1067 headers = [
1067 headers = [
1068 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1068 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1069 ]
1069 ]
1070 if encoding:
1070 if encoding:
1071 headers.append(('Content-Encoding', encoding))
1071 headers.append(('Content-Encoding', encoding))
1072 req.headers.extend(headers)
1072 req.headers.extend(headers)
1073 req.respond(HTTP_OK, mimetype)
1073 req.respond(HTTP_OK, mimetype)
1074
1074
1075 archival.archive(web.repo, req, cnode, artype, prefix=name,
1075 archival.archive(web.repo, req, cnode, artype, prefix=name,
1076 matchfn=matchfn,
1076 matchfn=matchfn,
1077 subrepos=web.configbool("web", "archivesubrepos"))
1077 subrepos=web.configbool("web", "archivesubrepos"))
1078 return []
1078 return []
1079
1079
1080
1080
1081 @webcommand('static')
1081 @webcommand('static')
1082 def static(web, req, tmpl):
1082 def static(web, req, tmpl):
1083 fname = req.form['file'][0]
1083 fname = req.form['file'][0]
1084 # 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
1085 # readable by the user running the CGI script
1085 # readable by the user running the CGI script
1086 static = web.config("web", "static", None, untrusted=False)
1086 static = web.config("web", "static", None, untrusted=False)
1087 if not static:
1087 if not static:
1088 tp = web.templatepath or templater.templatepaths()
1088 tp = web.templatepath or templater.templatepaths()
1089 if isinstance(tp, str):
1089 if isinstance(tp, str):
1090 tp = [tp]
1090 tp = [tp]
1091 static = [os.path.join(p, 'static') for p in tp]
1091 static = [os.path.join(p, 'static') for p in tp]
1092 staticfile(static, fname, req)
1092 staticfile(static, fname, req)
1093 return []
1093 return []
1094
1094
1095 @webcommand('graph')
1095 @webcommand('graph')
1096 def graph(web, req, tmpl):
1096 def graph(web, req, tmpl):
1097 """
1097 """
1098 /graph[/{revision}]
1098 /graph[/{revision}]
1099 -------------------
1099 -------------------
1100
1100
1101 Show information about the graphical topology of the repository.
1101 Show information about the graphical topology of the repository.
1102
1102
1103 Information rendered by this handler can be used to create visual
1103 Information rendered by this handler can be used to create visual
1104 representations of repository topology.
1104 representations of repository topology.
1105
1105
1106 The ``revision`` URL parameter controls the starting changeset.
1106 The ``revision`` URL parameter controls the starting changeset.
1107
1107
1108 The ``revcount`` query string argument can define the number of changesets
1108 The ``revcount`` query string argument can define the number of changesets
1109 to show information for.
1109 to show information for.
1110
1110
1111 This handler will render the ``graph`` template.
1111 This handler will render the ``graph`` template.
1112 """
1112 """
1113
1113
1114 if 'node' in req.form:
1114 if 'node' in req.form:
1115 ctx = webutil.changectx(web.repo, req)
1115 ctx = webutil.changectx(web.repo, req)
1116 symrev = webutil.symrevorshortnode(req, ctx)
1116 symrev = webutil.symrevorshortnode(req, ctx)
1117 else:
1117 else:
1118 ctx = web.repo['tip']
1118 ctx = web.repo['tip']
1119 symrev = 'tip'
1119 symrev = 'tip'
1120 rev = ctx.rev()
1120 rev = ctx.rev()
1121
1121
1122 bg_height = 39
1122 bg_height = 39
1123 revcount = web.maxshortchanges
1123 revcount = web.maxshortchanges
1124 if 'revcount' in req.form:
1124 if 'revcount' in req.form:
1125 try:
1125 try:
1126 revcount = int(req.form.get('revcount', [revcount])[0])
1126 revcount = int(req.form.get('revcount', [revcount])[0])
1127 revcount = max(revcount, 1)
1127 revcount = max(revcount, 1)
1128 tmpl.defaults['sessionvars']['revcount'] = revcount
1128 tmpl.defaults['sessionvars']['revcount'] = revcount
1129 except ValueError:
1129 except ValueError:
1130 pass
1130 pass
1131
1131
1132 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1132 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1133 lessvars['revcount'] = max(revcount / 2, 1)
1133 lessvars['revcount'] = max(revcount / 2, 1)
1134 morevars = copy.copy(tmpl.defaults['sessionvars'])
1134 morevars = copy.copy(tmpl.defaults['sessionvars'])
1135 morevars['revcount'] = revcount * 2
1135 morevars['revcount'] = revcount * 2
1136
1136
1137 count = len(web.repo)
1137 count = len(web.repo)
1138 pos = rev
1138 pos = rev
1139
1139
1140 uprev = min(max(0, count - 1), rev + revcount)
1140 uprev = min(max(0, count - 1), rev + revcount)
1141 downrev = max(0, rev - revcount)
1141 downrev = max(0, rev - revcount)
1142 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1142 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1143
1143
1144 tree = []
1144 tree = []
1145 if pos != -1:
1145 if pos != -1:
1146 allrevs = web.repo.changelog.revs(pos, 0)
1146 allrevs = web.repo.changelog.revs(pos, 0)
1147 revs = []
1147 revs = []
1148 for i in allrevs:
1148 for i in allrevs:
1149 revs.append(i)
1149 revs.append(i)
1150 if len(revs) >= revcount:
1150 if len(revs) >= revcount:
1151 break
1151 break
1152
1152
1153 # 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
1154 # 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
1155 # since hgweb graphing code is not itself lazy yet.
1155 # since hgweb graphing code is not itself lazy yet.
1156 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1156 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1157 # As we said one line above... not lazy.
1157 # As we said one line above... not lazy.
1158 tree = list(graphmod.colored(dag, web.repo))
1158 tree = list(graphmod.colored(dag, web.repo))
1159
1159
1160 def getcolumns(tree):
1160 def getcolumns(tree):
1161 cols = 0
1161 cols = 0
1162 for (id, type, ctx, vtx, edges) in tree:
1162 for (id, type, ctx, vtx, edges) in tree:
1163 if type != graphmod.CHANGESET:
1163 if type != graphmod.CHANGESET:
1164 continue
1164 continue
1165 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]),
1166 max([edge[1] for edge in edges] or [0]))
1166 max([edge[1] for edge in edges] or [0]))
1167 return cols
1167 return cols
1168
1168
1169 def graphdata(usetuples, encodestr):
1169 def graphdata(usetuples, encodestr):
1170 data = []
1170 data = []
1171
1171
1172 row = 0
1172 row = 0
1173 for (id, type, ctx, vtx, edges) in tree:
1173 for (id, type, ctx, vtx, edges) in tree:
1174 if type != graphmod.CHANGESET:
1174 if type != graphmod.CHANGESET:
1175 continue
1175 continue
1176 node = str(ctx)
1176 node = str(ctx)
1177 age = encodestr(templatefilters.age(ctx.date()))
1177 age = encodestr(templatefilters.age(ctx.date()))
1178 desc = templatefilters.firstline(encodestr(ctx.description()))
1178 desc = templatefilters.firstline(encodestr(ctx.description()))
1179 desc = cgi.escape(templatefilters.nonempty(desc))
1179 desc = cgi.escape(templatefilters.nonempty(desc))
1180 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1180 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1181 branch = cgi.escape(encodestr(ctx.branch()))
1181 branch = cgi.escape(encodestr(ctx.branch()))
1182 try:
1182 try:
1183 branchnode = web.repo.branchtip(branch)
1183 branchnode = web.repo.branchtip(branch)
1184 except error.RepoLookupError:
1184 except error.RepoLookupError:
1185 branchnode = None
1185 branchnode = None
1186 branch = branch, branchnode == ctx.node()
1186 branch = branch, branchnode == ctx.node()
1187
1187
1188 if usetuples:
1188 if usetuples:
1189 data.append((node, vtx, edges, desc, user, age, branch,
1189 data.append((node, vtx, edges, desc, user, age, branch,
1190 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1190 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1191 [cgi.escape(encodestr(x))
1191 [cgi.escape(encodestr(x))
1192 for x in ctx.bookmarks()]))
1192 for x in ctx.bookmarks()]))
1193 else:
1193 else:
1194 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1194 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1195 'color': (edge[2] - 1) % 6 + 1,
1195 'color': (edge[2] - 1) % 6 + 1,
1196 'width': edge[3], 'bcolor': edge[4]}
1196 'width': edge[3], 'bcolor': edge[4]}
1197 for edge in edges]
1197 for edge in edges]
1198
1198
1199 data.append(
1199 data.append(
1200 {'node': node,
1200 {'node': node,
1201 'col': vtx[0],
1201 'col': vtx[0],
1202 'color': (vtx[1] - 1) % 6 + 1,
1202 'color': (vtx[1] - 1) % 6 + 1,
1203 'edges': edgedata,
1203 'edges': edgedata,
1204 'row': row,
1204 'row': row,
1205 'nextrow': row + 1,
1205 'nextrow': row + 1,
1206 'desc': desc,
1206 'desc': desc,
1207 'user': user,
1207 'user': user,
1208 'age': age,
1208 'age': age,
1209 'bookmarks': webutil.nodebookmarksdict(
1209 'bookmarks': webutil.nodebookmarksdict(
1210 web.repo, ctx.node()),
1210 web.repo, ctx.node()),
1211 'branches': webutil.nodebranchdict(web.repo, ctx),
1211 'branches': webutil.nodebranchdict(web.repo, ctx),
1212 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1212 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1213 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1213 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1214
1214
1215 row += 1
1215 row += 1
1216
1216
1217 return data
1217 return data
1218
1218
1219 cols = getcolumns(tree)
1219 cols = getcolumns(tree)
1220 rows = len(tree)
1220 rows = len(tree)
1221 canvasheight = (rows + 1) * bg_height - 27
1221 canvasheight = (rows + 1) * bg_height - 27
1222
1222
1223 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1223 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1224 uprev=uprev,
1224 uprev=uprev,
1225 lessvars=lessvars, morevars=morevars, downrev=downrev,
1225 lessvars=lessvars, morevars=morevars, downrev=downrev,
1226 cols=cols, rows=rows,
1226 cols=cols, rows=rows,
1227 canvaswidth=(cols + 1) * bg_height,
1227 canvaswidth=(cols + 1) * bg_height,
1228 truecanvasheight=rows * bg_height,
1228 truecanvasheight=rows * bg_height,
1229 canvasheight=canvasheight, bg_height=bg_height,
1229 canvasheight=canvasheight, bg_height=bg_height,
1230 # {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
1231 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1231 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1232 nodes=lambda **x: graphdata(False, str),
1232 nodes=lambda **x: graphdata(False, str),
1233 node=ctx.hex(), changenav=changenav)
1233 node=ctx.hex(), changenav=changenav)
1234
1234
1235 def _getdoc(e):
1235 def _getdoc(e):
1236 doc = e[0].__doc__
1236 doc = e[0].__doc__
1237 if doc:
1237 if doc:
1238 doc = _(doc).partition('\n')[0]
1238 doc = _(doc).partition('\n')[0]
1239 else:
1239 else:
1240 doc = _('(no help text available)')
1240 doc = _('(no help text available)')
1241 return doc
1241 return doc
1242
1242
1243 @webcommand('help')
1243 @webcommand('help')
1244 def help(web, req, tmpl):
1244 def help(web, req, tmpl):
1245 """
1245 """
1246 /help[/{topic}]
1246 /help[/{topic}]
1247 ---------------
1247 ---------------
1248
1248
1249 Render help documentation.
1249 Render help documentation.
1250
1250
1251 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``
1252 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
1253 available help topics will be rendered.
1253 available help topics will be rendered.
1254
1254
1255 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.
1256 ``helptopics`` will be rendered for the index of help topics.
1256 ``helptopics`` will be rendered for the index of help topics.
1257 """
1257 """
1258 from .. import commands, help as helpmod # avoid cycle
1258 from .. import commands, help as helpmod # avoid cycle
1259
1259
1260 topicname = req.form.get('node', [None])[0]
1260 topicname = req.form.get('node', [None])[0]
1261 if not topicname:
1261 if not topicname:
1262 def topics(**map):
1262 def topics(**map):
1263 for entries, summary, _doc in helpmod.helptable:
1263 for entries, summary, _doc in helpmod.helptable:
1264 yield {'topic': entries[0], 'summary': summary}
1264 yield {'topic': entries[0], 'summary': summary}
1265
1265
1266 early, other = [], []
1266 early, other = [], []
1267 primary = lambda s: s.partition('|')[0]
1267 primary = lambda s: s.partition('|')[0]
1268 for c, e in commands.table.iteritems():
1268 for c, e in commands.table.iteritems():
1269 doc = _getdoc(e)
1269 doc = _getdoc(e)
1270 if 'DEPRECATED' in doc or c.startswith('debug'):
1270 if 'DEPRECATED' in doc or c.startswith('debug'):
1271 continue
1271 continue
1272 cmd = primary(c)
1272 cmd = primary(c)
1273 if cmd.startswith('^'):
1273 if cmd.startswith('^'):
1274 early.append((cmd[1:], doc))
1274 early.append((cmd[1:], doc))
1275 else:
1275 else:
1276 other.append((cmd, doc))
1276 other.append((cmd, doc))
1277
1277
1278 early.sort()
1278 early.sort()
1279 other.sort()
1279 other.sort()
1280
1280
1281 def earlycommands(**map):
1281 def earlycommands(**map):
1282 for c, doc in early:
1282 for c, doc in early:
1283 yield {'topic': c, 'summary': doc}
1283 yield {'topic': c, 'summary': doc}
1284
1284
1285 def othercommands(**map):
1285 def othercommands(**map):
1286 for c, doc in other:
1286 for c, doc in other:
1287 yield {'topic': c, 'summary': doc}
1287 yield {'topic': c, 'summary': doc}
1288
1288
1289 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1289 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1290 othercommands=othercommands, title='Index')
1290 othercommands=othercommands, title='Index')
1291
1291
1292 # Render an index of sub-topics.
1292 # Render an index of sub-topics.
1293 if topicname in helpmod.subtopics:
1293 if topicname in helpmod.subtopics:
1294 topics = []
1294 topics = []
1295 for entries, summary, _doc in helpmod.subtopics[topicname]:
1295 for entries, summary, _doc in helpmod.subtopics[topicname]:
1296 topics.append({
1296 topics.append({
1297 'topic': '%s.%s' % (topicname, entries[0]),
1297 'topic': '%s.%s' % (topicname, entries[0]),
1298 'basename': entries[0],
1298 'basename': entries[0],
1299 'summary': summary,
1299 'summary': summary,
1300 })
1300 })
1301
1301
1302 return tmpl('helptopics', topics=topics, title=topicname,
1302 return tmpl('helptopics', topics=topics, title=topicname,
1303 subindex=True)
1303 subindex=True)
1304
1304
1305 u = webutil.wsgiui.load()
1305 u = webutil.wsgiui.load()
1306 u.verbose = True
1306 u.verbose = True
1307
1307
1308 # Render a page from a sub-topic.
1308 # Render a page from a sub-topic.
1309 if '.' in topicname:
1309 if '.' in topicname:
1310 # TODO implement support for rendering sections, like
1310 # TODO implement support for rendering sections, like
1311 # `hg help` works.
1311 # `hg help` works.
1312 topic, subtopic = topicname.split('.', 1)
1312 topic, subtopic = topicname.split('.', 1)
1313 if topic not in helpmod.subtopics:
1313 if topic not in helpmod.subtopics:
1314 raise ErrorResponse(HTTP_NOT_FOUND)
1314 raise ErrorResponse(HTTP_NOT_FOUND)
1315 else:
1315 else:
1316 topic = topicname
1316 topic = topicname
1317 subtopic = None
1317 subtopic = None
1318
1318
1319 try:
1319 try:
1320 doc = helpmod.help_(u, topic, subtopic=subtopic)
1320 doc = helpmod.help_(u, topic, subtopic=subtopic)
1321 except error.UnknownCommand:
1321 except error.UnknownCommand:
1322 raise ErrorResponse(HTTP_NOT_FOUND)
1322 raise ErrorResponse(HTTP_NOT_FOUND)
1323 return tmpl('help', topic=topicname, doc=doc)
1323 return tmpl('help', topic=topicname, doc=doc)
1324
1324
1325 # tell hggettext to extract docstrings from these functions:
1325 # tell hggettext to extract docstrings from these functions:
1326 i18nfunctions = commands.values()
1326 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now