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