##// END OF EJS Templates
webcommands: document "filediff" web command
Gregory Szorc -
r24092:55dfea65 default
parent child Browse files
Show More
@@ -1,1276 +1,1287 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 """
801 /diff/{revision}/{path}
802 -----------------------
803
804 Show how a file changed in a particular commit.
805
806 The ``filediff`` template is rendered.
807
808 This hander is registered under both the ``/diff`` and ``/filediff``
809 paths. ``/diff`` is used in modern code.
810 """
800 fctx, ctx = None, None
811 fctx, ctx = None, None
801 try:
812 try:
802 fctx = webutil.filectx(web.repo, req)
813 fctx = webutil.filectx(web.repo, req)
803 except LookupError:
814 except LookupError:
804 ctx = webutil.changectx(web.repo, req)
815 ctx = webutil.changectx(web.repo, req)
805 path = webutil.cleanpath(web.repo, req.form['file'][0])
816 path = webutil.cleanpath(web.repo, req.form['file'][0])
806 if path not in ctx.files():
817 if path not in ctx.files():
807 raise
818 raise
808
819
809 if fctx is not None:
820 if fctx is not None:
810 n = fctx.node()
821 n = fctx.node()
811 path = fctx.path()
822 path = fctx.path()
812 ctx = fctx.changectx()
823 ctx = fctx.changectx()
813 else:
824 else:
814 n = ctx.node()
825 n = ctx.node()
815 # path already defined in except clause
826 # path already defined in except clause
816
827
817 parity = paritygen(web.stripecount)
828 parity = paritygen(web.stripecount)
818 style = web.config('web', 'style', 'paper')
829 style = web.config('web', 'style', 'paper')
819 if 'style' in req.form:
830 if 'style' in req.form:
820 style = req.form['style'][0]
831 style = req.form['style'][0]
821
832
822 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
833 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
823 rename = fctx and webutil.renamelink(fctx) or []
834 rename = fctx and webutil.renamelink(fctx) or []
824 ctx = fctx and fctx or ctx
835 ctx = fctx and fctx or ctx
825 return tmpl("filediff",
836 return tmpl("filediff",
826 file=path,
837 file=path,
827 node=hex(n),
838 node=hex(n),
828 rev=ctx.rev(),
839 rev=ctx.rev(),
829 date=ctx.date(),
840 date=ctx.date(),
830 desc=ctx.description(),
841 desc=ctx.description(),
831 extra=ctx.extra(),
842 extra=ctx.extra(),
832 author=ctx.user(),
843 author=ctx.user(),
833 rename=rename,
844 rename=rename,
834 branch=webutil.nodebranchnodefault(ctx),
845 branch=webutil.nodebranchnodefault(ctx),
835 parent=webutil.parents(ctx),
846 parent=webutil.parents(ctx),
836 child=webutil.children(ctx),
847 child=webutil.children(ctx),
837 diff=diffs)
848 diff=diffs)
838
849
839 diff = webcommand('diff')(filediff)
850 diff = webcommand('diff')(filediff)
840
851
841 @webcommand('comparison')
852 @webcommand('comparison')
842 def comparison(web, req, tmpl):
853 def comparison(web, req, tmpl):
843 ctx = webutil.changectx(web.repo, req)
854 ctx = webutil.changectx(web.repo, req)
844 if 'file' not in req.form:
855 if 'file' not in req.form:
845 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
856 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
846 path = webutil.cleanpath(web.repo, req.form['file'][0])
857 path = webutil.cleanpath(web.repo, req.form['file'][0])
847 rename = path in ctx and webutil.renamelink(ctx[path]) or []
858 rename = path in ctx and webutil.renamelink(ctx[path]) or []
848
859
849 parsecontext = lambda v: v == 'full' and -1 or int(v)
860 parsecontext = lambda v: v == 'full' and -1 or int(v)
850 if 'context' in req.form:
861 if 'context' in req.form:
851 context = parsecontext(req.form['context'][0])
862 context = parsecontext(req.form['context'][0])
852 else:
863 else:
853 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
864 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
854
865
855 def filelines(f):
866 def filelines(f):
856 if util.binary(f.data()):
867 if util.binary(f.data()):
857 mt = mimetypes.guess_type(f.path())[0]
868 mt = mimetypes.guess_type(f.path())[0]
858 if not mt:
869 if not mt:
859 mt = 'application/octet-stream'
870 mt = 'application/octet-stream'
860 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
871 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
861 return f.data().splitlines()
872 return f.data().splitlines()
862
873
863 parent = ctx.p1()
874 parent = ctx.p1()
864 leftrev = parent.rev()
875 leftrev = parent.rev()
865 leftnode = parent.node()
876 leftnode = parent.node()
866 rightrev = ctx.rev()
877 rightrev = ctx.rev()
867 rightnode = ctx.node()
878 rightnode = ctx.node()
868 if path in ctx:
879 if path in ctx:
869 fctx = ctx[path]
880 fctx = ctx[path]
870 rightlines = filelines(fctx)
881 rightlines = filelines(fctx)
871 if path not in parent:
882 if path not in parent:
872 leftlines = ()
883 leftlines = ()
873 else:
884 else:
874 pfctx = parent[path]
885 pfctx = parent[path]
875 leftlines = filelines(pfctx)
886 leftlines = filelines(pfctx)
876 else:
887 else:
877 rightlines = ()
888 rightlines = ()
878 fctx = ctx.parents()[0][path]
889 fctx = ctx.parents()[0][path]
879 leftlines = filelines(fctx)
890 leftlines = filelines(fctx)
880
891
881 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
892 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
882 return tmpl('filecomparison',
893 return tmpl('filecomparison',
883 file=path,
894 file=path,
884 node=hex(ctx.node()),
895 node=hex(ctx.node()),
885 rev=ctx.rev(),
896 rev=ctx.rev(),
886 date=ctx.date(),
897 date=ctx.date(),
887 desc=ctx.description(),
898 desc=ctx.description(),
888 extra=ctx.extra(),
899 extra=ctx.extra(),
889 author=ctx.user(),
900 author=ctx.user(),
890 rename=rename,
901 rename=rename,
891 branch=webutil.nodebranchnodefault(ctx),
902 branch=webutil.nodebranchnodefault(ctx),
892 parent=webutil.parents(fctx),
903 parent=webutil.parents(fctx),
893 child=webutil.children(fctx),
904 child=webutil.children(fctx),
894 leftrev=leftrev,
905 leftrev=leftrev,
895 leftnode=hex(leftnode),
906 leftnode=hex(leftnode),
896 rightrev=rightrev,
907 rightrev=rightrev,
897 rightnode=hex(rightnode),
908 rightnode=hex(rightnode),
898 comparison=comparison)
909 comparison=comparison)
899
910
900 @webcommand('annotate')
911 @webcommand('annotate')
901 def annotate(web, req, tmpl):
912 def annotate(web, req, tmpl):
902 fctx = webutil.filectx(web.repo, req)
913 fctx = webutil.filectx(web.repo, req)
903 f = fctx.path()
914 f = fctx.path()
904 parity = paritygen(web.stripecount)
915 parity = paritygen(web.stripecount)
905 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
916 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
906 section='annotate', whitespace=True)
917 section='annotate', whitespace=True)
907
918
908 def annotate(**map):
919 def annotate(**map):
909 last = None
920 last = None
910 if util.binary(fctx.data()):
921 if util.binary(fctx.data()):
911 mt = (mimetypes.guess_type(fctx.path())[0]
922 mt = (mimetypes.guess_type(fctx.path())[0]
912 or 'application/octet-stream')
923 or 'application/octet-stream')
913 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
924 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
914 '(binary:%s)' % mt)])
925 '(binary:%s)' % mt)])
915 else:
926 else:
916 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
927 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
917 diffopts=diffopts))
928 diffopts=diffopts))
918 for lineno, ((f, targetline), l) in lines:
929 for lineno, ((f, targetline), l) in lines:
919 fnode = f.filenode()
930 fnode = f.filenode()
920
931
921 if last != fnode:
932 if last != fnode:
922 last = fnode
933 last = fnode
923
934
924 yield {"parity": parity.next(),
935 yield {"parity": parity.next(),
925 "node": f.hex(),
936 "node": f.hex(),
926 "rev": f.rev(),
937 "rev": f.rev(),
927 "author": f.user(),
938 "author": f.user(),
928 "desc": f.description(),
939 "desc": f.description(),
929 "extra": f.extra(),
940 "extra": f.extra(),
930 "file": f.path(),
941 "file": f.path(),
931 "targetline": targetline,
942 "targetline": targetline,
932 "line": l,
943 "line": l,
933 "lineid": "l%d" % (lineno + 1),
944 "lineid": "l%d" % (lineno + 1),
934 "linenumber": "% 6d" % (lineno + 1),
945 "linenumber": "% 6d" % (lineno + 1),
935 "revdate": f.date()}
946 "revdate": f.date()}
936
947
937 return tmpl("fileannotate",
948 return tmpl("fileannotate",
938 file=f,
949 file=f,
939 annotate=annotate,
950 annotate=annotate,
940 path=webutil.up(f),
951 path=webutil.up(f),
941 rev=fctx.rev(),
952 rev=fctx.rev(),
942 node=fctx.hex(),
953 node=fctx.hex(),
943 author=fctx.user(),
954 author=fctx.user(),
944 date=fctx.date(),
955 date=fctx.date(),
945 desc=fctx.description(),
956 desc=fctx.description(),
946 extra=fctx.extra(),
957 extra=fctx.extra(),
947 rename=webutil.renamelink(fctx),
958 rename=webutil.renamelink(fctx),
948 branch=webutil.nodebranchnodefault(fctx),
959 branch=webutil.nodebranchnodefault(fctx),
949 parent=webutil.parents(fctx),
960 parent=webutil.parents(fctx),
950 child=webutil.children(fctx),
961 child=webutil.children(fctx),
951 permissions=fctx.manifest().flags(f))
962 permissions=fctx.manifest().flags(f))
952
963
953 @webcommand('filelog')
964 @webcommand('filelog')
954 def filelog(web, req, tmpl):
965 def filelog(web, req, tmpl):
955
966
956 try:
967 try:
957 fctx = webutil.filectx(web.repo, req)
968 fctx = webutil.filectx(web.repo, req)
958 f = fctx.path()
969 f = fctx.path()
959 fl = fctx.filelog()
970 fl = fctx.filelog()
960 except error.LookupError:
971 except error.LookupError:
961 f = webutil.cleanpath(web.repo, req.form['file'][0])
972 f = webutil.cleanpath(web.repo, req.form['file'][0])
962 fl = web.repo.file(f)
973 fl = web.repo.file(f)
963 numrevs = len(fl)
974 numrevs = len(fl)
964 if not numrevs: # file doesn't exist at all
975 if not numrevs: # file doesn't exist at all
965 raise
976 raise
966 rev = webutil.changectx(web.repo, req).rev()
977 rev = webutil.changectx(web.repo, req).rev()
967 first = fl.linkrev(0)
978 first = fl.linkrev(0)
968 if rev < first: # current rev is from before file existed
979 if rev < first: # current rev is from before file existed
969 raise
980 raise
970 frev = numrevs - 1
981 frev = numrevs - 1
971 while fl.linkrev(frev) > rev:
982 while fl.linkrev(frev) > rev:
972 frev -= 1
983 frev -= 1
973 fctx = web.repo.filectx(f, fl.linkrev(frev))
984 fctx = web.repo.filectx(f, fl.linkrev(frev))
974
985
975 revcount = web.maxshortchanges
986 revcount = web.maxshortchanges
976 if 'revcount' in req.form:
987 if 'revcount' in req.form:
977 try:
988 try:
978 revcount = int(req.form.get('revcount', [revcount])[0])
989 revcount = int(req.form.get('revcount', [revcount])[0])
979 revcount = max(revcount, 1)
990 revcount = max(revcount, 1)
980 tmpl.defaults['sessionvars']['revcount'] = revcount
991 tmpl.defaults['sessionvars']['revcount'] = revcount
981 except ValueError:
992 except ValueError:
982 pass
993 pass
983
994
984 lessvars = copy.copy(tmpl.defaults['sessionvars'])
995 lessvars = copy.copy(tmpl.defaults['sessionvars'])
985 lessvars['revcount'] = max(revcount / 2, 1)
996 lessvars['revcount'] = max(revcount / 2, 1)
986 morevars = copy.copy(tmpl.defaults['sessionvars'])
997 morevars = copy.copy(tmpl.defaults['sessionvars'])
987 morevars['revcount'] = revcount * 2
998 morevars['revcount'] = revcount * 2
988
999
989 count = fctx.filerev() + 1
1000 count = fctx.filerev() + 1
990 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
1001 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
991 end = min(count, start + revcount) # last rev on this page
1002 end = min(count, start + revcount) # last rev on this page
992 parity = paritygen(web.stripecount, offset=start - end)
1003 parity = paritygen(web.stripecount, offset=start - end)
993
1004
994 def entries():
1005 def entries():
995 l = []
1006 l = []
996
1007
997 repo = web.repo
1008 repo = web.repo
998 revs = fctx.filelog().revs(start, end - 1)
1009 revs = fctx.filelog().revs(start, end - 1)
999 for i in revs:
1010 for i in revs:
1000 iterfctx = fctx.filectx(i)
1011 iterfctx = fctx.filectx(i)
1001
1012
1002 l.append({"parity": parity.next(),
1013 l.append({"parity": parity.next(),
1003 "filerev": i,
1014 "filerev": i,
1004 "file": f,
1015 "file": f,
1005 "node": iterfctx.hex(),
1016 "node": iterfctx.hex(),
1006 "author": iterfctx.user(),
1017 "author": iterfctx.user(),
1007 "date": iterfctx.date(),
1018 "date": iterfctx.date(),
1008 "rename": webutil.renamelink(iterfctx),
1019 "rename": webutil.renamelink(iterfctx),
1009 "parent": webutil.parents(iterfctx),
1020 "parent": webutil.parents(iterfctx),
1010 "child": webutil.children(iterfctx),
1021 "child": webutil.children(iterfctx),
1011 "desc": iterfctx.description(),
1022 "desc": iterfctx.description(),
1012 "extra": iterfctx.extra(),
1023 "extra": iterfctx.extra(),
1013 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1024 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1014 "bookmarks": webutil.nodebookmarksdict(
1025 "bookmarks": webutil.nodebookmarksdict(
1015 repo, iterfctx.node()),
1026 repo, iterfctx.node()),
1016 "branch": webutil.nodebranchnodefault(iterfctx),
1027 "branch": webutil.nodebranchnodefault(iterfctx),
1017 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1028 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1018 "branches": webutil.nodebranchdict(repo, iterfctx)})
1029 "branches": webutil.nodebranchdict(repo, iterfctx)})
1019 for e in reversed(l):
1030 for e in reversed(l):
1020 yield e
1031 yield e
1021
1032
1022 entries = list(entries())
1033 entries = list(entries())
1023 latestentry = entries[:1]
1034 latestentry = entries[:1]
1024
1035
1025 revnav = webutil.filerevnav(web.repo, fctx.path())
1036 revnav = webutil.filerevnav(web.repo, fctx.path())
1026 nav = revnav.gen(end - 1, revcount, count)
1037 nav = revnav.gen(end - 1, revcount, count)
1027 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1038 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1028 entries=entries,
1039 entries=entries,
1029 latestentry=latestentry,
1040 latestentry=latestentry,
1030 revcount=revcount, morevars=morevars, lessvars=lessvars)
1041 revcount=revcount, morevars=morevars, lessvars=lessvars)
1031
1042
1032 @webcommand('archive')
1043 @webcommand('archive')
1033 def archive(web, req, tmpl):
1044 def archive(web, req, tmpl):
1034 type_ = req.form.get('type', [None])[0]
1045 type_ = req.form.get('type', [None])[0]
1035 allowed = web.configlist("web", "allow_archive")
1046 allowed = web.configlist("web", "allow_archive")
1036 key = req.form['node'][0]
1047 key = req.form['node'][0]
1037
1048
1038 if type_ not in web.archives:
1049 if type_ not in web.archives:
1039 msg = 'Unsupported archive type: %s' % type_
1050 msg = 'Unsupported archive type: %s' % type_
1040 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1051 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1041
1052
1042 if not ((type_ in allowed or
1053 if not ((type_ in allowed or
1043 web.configbool("web", "allow" + type_, False))):
1054 web.configbool("web", "allow" + type_, False))):
1044 msg = 'Archive type not allowed: %s' % type_
1055 msg = 'Archive type not allowed: %s' % type_
1045 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1056 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1046
1057
1047 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1058 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1048 cnode = web.repo.lookup(key)
1059 cnode = web.repo.lookup(key)
1049 arch_version = key
1060 arch_version = key
1050 if cnode == key or key == 'tip':
1061 if cnode == key or key == 'tip':
1051 arch_version = short(cnode)
1062 arch_version = short(cnode)
1052 name = "%s-%s" % (reponame, arch_version)
1063 name = "%s-%s" % (reponame, arch_version)
1053
1064
1054 ctx = webutil.changectx(web.repo, req)
1065 ctx = webutil.changectx(web.repo, req)
1055 pats = []
1066 pats = []
1056 matchfn = scmutil.match(ctx, [])
1067 matchfn = scmutil.match(ctx, [])
1057 file = req.form.get('file', None)
1068 file = req.form.get('file', None)
1058 if file:
1069 if file:
1059 pats = ['path:' + file[0]]
1070 pats = ['path:' + file[0]]
1060 matchfn = scmutil.match(ctx, pats, default='path')
1071 matchfn = scmutil.match(ctx, pats, default='path')
1061 if pats:
1072 if pats:
1062 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1073 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1063 if not files:
1074 if not files:
1064 raise ErrorResponse(HTTP_NOT_FOUND,
1075 raise ErrorResponse(HTTP_NOT_FOUND,
1065 'file(s) not found: %s' % file[0])
1076 'file(s) not found: %s' % file[0])
1066
1077
1067 mimetype, artype, extension, encoding = web.archive_specs[type_]
1078 mimetype, artype, extension, encoding = web.archive_specs[type_]
1068 headers = [
1079 headers = [
1069 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1080 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1070 ]
1081 ]
1071 if encoding:
1082 if encoding:
1072 headers.append(('Content-Encoding', encoding))
1083 headers.append(('Content-Encoding', encoding))
1073 req.headers.extend(headers)
1084 req.headers.extend(headers)
1074 req.respond(HTTP_OK, mimetype)
1085 req.respond(HTTP_OK, mimetype)
1075
1086
1076 archival.archive(web.repo, req, cnode, artype, prefix=name,
1087 archival.archive(web.repo, req, cnode, artype, prefix=name,
1077 matchfn=matchfn,
1088 matchfn=matchfn,
1078 subrepos=web.configbool("web", "archivesubrepos"))
1089 subrepos=web.configbool("web", "archivesubrepos"))
1079 return []
1090 return []
1080
1091
1081
1092
1082 @webcommand('static')
1093 @webcommand('static')
1083 def static(web, req, tmpl):
1094 def static(web, req, tmpl):
1084 fname = req.form['file'][0]
1095 fname = req.form['file'][0]
1085 # a repo owner may set web.static in .hg/hgrc to get any file
1096 # a repo owner may set web.static in .hg/hgrc to get any file
1086 # readable by the user running the CGI script
1097 # readable by the user running the CGI script
1087 static = web.config("web", "static", None, untrusted=False)
1098 static = web.config("web", "static", None, untrusted=False)
1088 if not static:
1099 if not static:
1089 tp = web.templatepath or templater.templatepaths()
1100 tp = web.templatepath or templater.templatepaths()
1090 if isinstance(tp, str):
1101 if isinstance(tp, str):
1091 tp = [tp]
1102 tp = [tp]
1092 static = [os.path.join(p, 'static') for p in tp]
1103 static = [os.path.join(p, 'static') for p in tp]
1093 staticfile(static, fname, req)
1104 staticfile(static, fname, req)
1094 return []
1105 return []
1095
1106
1096 @webcommand('graph')
1107 @webcommand('graph')
1097 def graph(web, req, tmpl):
1108 def graph(web, req, tmpl):
1098
1109
1099 ctx = webutil.changectx(web.repo, req)
1110 ctx = webutil.changectx(web.repo, req)
1100 rev = ctx.rev()
1111 rev = ctx.rev()
1101
1112
1102 bg_height = 39
1113 bg_height = 39
1103 revcount = web.maxshortchanges
1114 revcount = web.maxshortchanges
1104 if 'revcount' in req.form:
1115 if 'revcount' in req.form:
1105 try:
1116 try:
1106 revcount = int(req.form.get('revcount', [revcount])[0])
1117 revcount = int(req.form.get('revcount', [revcount])[0])
1107 revcount = max(revcount, 1)
1118 revcount = max(revcount, 1)
1108 tmpl.defaults['sessionvars']['revcount'] = revcount
1119 tmpl.defaults['sessionvars']['revcount'] = revcount
1109 except ValueError:
1120 except ValueError:
1110 pass
1121 pass
1111
1122
1112 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1123 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1113 lessvars['revcount'] = max(revcount / 2, 1)
1124 lessvars['revcount'] = max(revcount / 2, 1)
1114 morevars = copy.copy(tmpl.defaults['sessionvars'])
1125 morevars = copy.copy(tmpl.defaults['sessionvars'])
1115 morevars['revcount'] = revcount * 2
1126 morevars['revcount'] = revcount * 2
1116
1127
1117 count = len(web.repo)
1128 count = len(web.repo)
1118 pos = rev
1129 pos = rev
1119
1130
1120 uprev = min(max(0, count - 1), rev + revcount)
1131 uprev = min(max(0, count - 1), rev + revcount)
1121 downrev = max(0, rev - revcount)
1132 downrev = max(0, rev - revcount)
1122 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1133 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1123
1134
1124 tree = []
1135 tree = []
1125 if pos != -1:
1136 if pos != -1:
1126 allrevs = web.repo.changelog.revs(pos, 0)
1137 allrevs = web.repo.changelog.revs(pos, 0)
1127 revs = []
1138 revs = []
1128 for i in allrevs:
1139 for i in allrevs:
1129 revs.append(i)
1140 revs.append(i)
1130 if len(revs) >= revcount:
1141 if len(revs) >= revcount:
1131 break
1142 break
1132
1143
1133 # We have to feed a baseset to dagwalker as it is expecting smartset
1144 # We have to feed a baseset to dagwalker as it is expecting smartset
1134 # object. This does not have a big impact on hgweb performance itself
1145 # object. This does not have a big impact on hgweb performance itself
1135 # since hgweb graphing code is not itself lazy yet.
1146 # since hgweb graphing code is not itself lazy yet.
1136 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1147 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1137 # As we said one line above... not lazy.
1148 # As we said one line above... not lazy.
1138 tree = list(graphmod.colored(dag, web.repo))
1149 tree = list(graphmod.colored(dag, web.repo))
1139
1150
1140 def getcolumns(tree):
1151 def getcolumns(tree):
1141 cols = 0
1152 cols = 0
1142 for (id, type, ctx, vtx, edges) in tree:
1153 for (id, type, ctx, vtx, edges) in tree:
1143 if type != graphmod.CHANGESET:
1154 if type != graphmod.CHANGESET:
1144 continue
1155 continue
1145 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1156 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1146 max([edge[1] for edge in edges] or [0]))
1157 max([edge[1] for edge in edges] or [0]))
1147 return cols
1158 return cols
1148
1159
1149 def graphdata(usetuples, **map):
1160 def graphdata(usetuples, **map):
1150 data = []
1161 data = []
1151
1162
1152 row = 0
1163 row = 0
1153 for (id, type, ctx, vtx, edges) in tree:
1164 for (id, type, ctx, vtx, edges) in tree:
1154 if type != graphmod.CHANGESET:
1165 if type != graphmod.CHANGESET:
1155 continue
1166 continue
1156 node = str(ctx)
1167 node = str(ctx)
1157 age = templatefilters.age(ctx.date())
1168 age = templatefilters.age(ctx.date())
1158 desc = templatefilters.firstline(ctx.description())
1169 desc = templatefilters.firstline(ctx.description())
1159 desc = cgi.escape(templatefilters.nonempty(desc))
1170 desc = cgi.escape(templatefilters.nonempty(desc))
1160 user = cgi.escape(templatefilters.person(ctx.user()))
1171 user = cgi.escape(templatefilters.person(ctx.user()))
1161 branch = cgi.escape(ctx.branch())
1172 branch = cgi.escape(ctx.branch())
1162 try:
1173 try:
1163 branchnode = web.repo.branchtip(branch)
1174 branchnode = web.repo.branchtip(branch)
1164 except error.RepoLookupError:
1175 except error.RepoLookupError:
1165 branchnode = None
1176 branchnode = None
1166 branch = branch, branchnode == ctx.node()
1177 branch = branch, branchnode == ctx.node()
1167
1178
1168 if usetuples:
1179 if usetuples:
1169 data.append((node, vtx, edges, desc, user, age, branch,
1180 data.append((node, vtx, edges, desc, user, age, branch,
1170 [cgi.escape(x) for x in ctx.tags()],
1181 [cgi.escape(x) for x in ctx.tags()],
1171 [cgi.escape(x) for x in ctx.bookmarks()]))
1182 [cgi.escape(x) for x in ctx.bookmarks()]))
1172 else:
1183 else:
1173 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1184 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1174 'color': (edge[2] - 1) % 6 + 1,
1185 'color': (edge[2] - 1) % 6 + 1,
1175 'width': edge[3], 'bcolor': edge[4]}
1186 'width': edge[3], 'bcolor': edge[4]}
1176 for edge in edges]
1187 for edge in edges]
1177
1188
1178 data.append(
1189 data.append(
1179 {'node': node,
1190 {'node': node,
1180 'col': vtx[0],
1191 'col': vtx[0],
1181 'color': (vtx[1] - 1) % 6 + 1,
1192 'color': (vtx[1] - 1) % 6 + 1,
1182 'edges': edgedata,
1193 'edges': edgedata,
1183 'row': row,
1194 'row': row,
1184 'nextrow': row + 1,
1195 'nextrow': row + 1,
1185 'desc': desc,
1196 'desc': desc,
1186 'user': user,
1197 'user': user,
1187 'age': age,
1198 'age': age,
1188 'bookmarks': webutil.nodebookmarksdict(
1199 'bookmarks': webutil.nodebookmarksdict(
1189 web.repo, ctx.node()),
1200 web.repo, ctx.node()),
1190 'branches': webutil.nodebranchdict(web.repo, ctx),
1201 'branches': webutil.nodebranchdict(web.repo, ctx),
1191 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1202 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1192 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1203 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1193
1204
1194 row += 1
1205 row += 1
1195
1206
1196 return data
1207 return data
1197
1208
1198 cols = getcolumns(tree)
1209 cols = getcolumns(tree)
1199 rows = len(tree)
1210 rows = len(tree)
1200 canvasheight = (rows + 1) * bg_height - 27
1211 canvasheight = (rows + 1) * bg_height - 27
1201
1212
1202 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
1213 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
1203 lessvars=lessvars, morevars=morevars, downrev=downrev,
1214 lessvars=lessvars, morevars=morevars, downrev=downrev,
1204 cols=cols, rows=rows,
1215 cols=cols, rows=rows,
1205 canvaswidth=(cols + 1) * bg_height,
1216 canvaswidth=(cols + 1) * bg_height,
1206 truecanvasheight=rows * bg_height,
1217 truecanvasheight=rows * bg_height,
1207 canvasheight=canvasheight, bg_height=bg_height,
1218 canvasheight=canvasheight, bg_height=bg_height,
1208 jsdata=lambda **x: graphdata(True, **x),
1219 jsdata=lambda **x: graphdata(True, **x),
1209 nodes=lambda **x: graphdata(False, **x),
1220 nodes=lambda **x: graphdata(False, **x),
1210 node=ctx.hex(), changenav=changenav)
1221 node=ctx.hex(), changenav=changenav)
1211
1222
1212 def _getdoc(e):
1223 def _getdoc(e):
1213 doc = e[0].__doc__
1224 doc = e[0].__doc__
1214 if doc:
1225 if doc:
1215 doc = _(doc).split('\n')[0]
1226 doc = _(doc).split('\n')[0]
1216 else:
1227 else:
1217 doc = _('(no help text available)')
1228 doc = _('(no help text available)')
1218 return doc
1229 return doc
1219
1230
1220 @webcommand('help')
1231 @webcommand('help')
1221 def help(web, req, tmpl):
1232 def help(web, req, tmpl):
1222 """
1233 """
1223 /help[/{topic}]
1234 /help[/{topic}]
1224 ---------------
1235 ---------------
1225
1236
1226 Render help documentation.
1237 Render help documentation.
1227
1238
1228 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1239 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1229 is defined, that help topic will be rendered. If not, an index of
1240 is defined, that help topic will be rendered. If not, an index of
1230 available help topics will be rendered.
1241 available help topics will be rendered.
1231
1242
1232 The ``help`` template will be rendered when requesting help for a topic.
1243 The ``help`` template will be rendered when requesting help for a topic.
1233 ``helptopics`` will be rendered for the index of help topics.
1244 ``helptopics`` will be rendered for the index of help topics.
1234 """
1245 """
1235 from mercurial import commands # avoid cycle
1246 from mercurial import commands # avoid cycle
1236 from mercurial import help as helpmod # avoid cycle
1247 from mercurial import help as helpmod # avoid cycle
1237
1248
1238 topicname = req.form.get('node', [None])[0]
1249 topicname = req.form.get('node', [None])[0]
1239 if not topicname:
1250 if not topicname:
1240 def topics(**map):
1251 def topics(**map):
1241 for entries, summary, _doc in helpmod.helptable:
1252 for entries, summary, _doc in helpmod.helptable:
1242 yield {'topic': entries[0], 'summary': summary}
1253 yield {'topic': entries[0], 'summary': summary}
1243
1254
1244 early, other = [], []
1255 early, other = [], []
1245 primary = lambda s: s.split('|')[0]
1256 primary = lambda s: s.split('|')[0]
1246 for c, e in commands.table.iteritems():
1257 for c, e in commands.table.iteritems():
1247 doc = _getdoc(e)
1258 doc = _getdoc(e)
1248 if 'DEPRECATED' in doc or c.startswith('debug'):
1259 if 'DEPRECATED' in doc or c.startswith('debug'):
1249 continue
1260 continue
1250 cmd = primary(c)
1261 cmd = primary(c)
1251 if cmd.startswith('^'):
1262 if cmd.startswith('^'):
1252 early.append((cmd[1:], doc))
1263 early.append((cmd[1:], doc))
1253 else:
1264 else:
1254 other.append((cmd, doc))
1265 other.append((cmd, doc))
1255
1266
1256 early.sort()
1267 early.sort()
1257 other.sort()
1268 other.sort()
1258
1269
1259 def earlycommands(**map):
1270 def earlycommands(**map):
1260 for c, doc in early:
1271 for c, doc in early:
1261 yield {'topic': c, 'summary': doc}
1272 yield {'topic': c, 'summary': doc}
1262
1273
1263 def othercommands(**map):
1274 def othercommands(**map):
1264 for c, doc in other:
1275 for c, doc in other:
1265 yield {'topic': c, 'summary': doc}
1276 yield {'topic': c, 'summary': doc}
1266
1277
1267 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1278 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1268 othercommands=othercommands, title='Index')
1279 othercommands=othercommands, title='Index')
1269
1280
1270 u = webutil.wsgiui()
1281 u = webutil.wsgiui()
1271 u.verbose = True
1282 u.verbose = True
1272 try:
1283 try:
1273 doc = helpmod.help_(u, topicname)
1284 doc = helpmod.help_(u, topicname)
1274 except error.UnknownCommand:
1285 except error.UnknownCommand:
1275 raise ErrorResponse(HTTP_NOT_FOUND)
1286 raise ErrorResponse(HTTP_NOT_FOUND)
1276 return tmpl('help', topic=topicname, doc=doc)
1287 return tmpl('help', topic=topicname, doc=doc)
General Comments 0
You need to be logged in to leave comments. Login now