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