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