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