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