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