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