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