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