##// END OF EJS Templates
webcommands: document "graph" web command
Gregory Szorc -
r24097:8e04a73b default
parent child Browse files
Show More
@@ -1,1340 +1,1356
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 """
989 """
990 /filelog/{revision}/{path}
990 /filelog/{revision}/{path}
991 --------------------------
991 --------------------------
992
992
993 Show information about the history of a file in the repository.
993 Show information about the history of a file in the repository.
994
994
995 The ``revcount`` query string argument can be defined to control the
995 The ``revcount`` query string argument can be defined to control the
996 maximum number of entries to show.
996 maximum number of entries to show.
997
997
998 The ``filelog`` template will be rendered.
998 The ``filelog`` template will be rendered.
999 """
999 """
1000
1000
1001 try:
1001 try:
1002 fctx = webutil.filectx(web.repo, req)
1002 fctx = webutil.filectx(web.repo, req)
1003 f = fctx.path()
1003 f = fctx.path()
1004 fl = fctx.filelog()
1004 fl = fctx.filelog()
1005 except error.LookupError:
1005 except error.LookupError:
1006 f = webutil.cleanpath(web.repo, req.form['file'][0])
1006 f = webutil.cleanpath(web.repo, req.form['file'][0])
1007 fl = web.repo.file(f)
1007 fl = web.repo.file(f)
1008 numrevs = len(fl)
1008 numrevs = len(fl)
1009 if not numrevs: # file doesn't exist at all
1009 if not numrevs: # file doesn't exist at all
1010 raise
1010 raise
1011 rev = webutil.changectx(web.repo, req).rev()
1011 rev = webutil.changectx(web.repo, req).rev()
1012 first = fl.linkrev(0)
1012 first = fl.linkrev(0)
1013 if rev < first: # current rev is from before file existed
1013 if rev < first: # current rev is from before file existed
1014 raise
1014 raise
1015 frev = numrevs - 1
1015 frev = numrevs - 1
1016 while fl.linkrev(frev) > rev:
1016 while fl.linkrev(frev) > rev:
1017 frev -= 1
1017 frev -= 1
1018 fctx = web.repo.filectx(f, fl.linkrev(frev))
1018 fctx = web.repo.filectx(f, fl.linkrev(frev))
1019
1019
1020 revcount = web.maxshortchanges
1020 revcount = web.maxshortchanges
1021 if 'revcount' in req.form:
1021 if 'revcount' in req.form:
1022 try:
1022 try:
1023 revcount = int(req.form.get('revcount', [revcount])[0])
1023 revcount = int(req.form.get('revcount', [revcount])[0])
1024 revcount = max(revcount, 1)
1024 revcount = max(revcount, 1)
1025 tmpl.defaults['sessionvars']['revcount'] = revcount
1025 tmpl.defaults['sessionvars']['revcount'] = revcount
1026 except ValueError:
1026 except ValueError:
1027 pass
1027 pass
1028
1028
1029 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1029 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1030 lessvars['revcount'] = max(revcount / 2, 1)
1030 lessvars['revcount'] = max(revcount / 2, 1)
1031 morevars = copy.copy(tmpl.defaults['sessionvars'])
1031 morevars = copy.copy(tmpl.defaults['sessionvars'])
1032 morevars['revcount'] = revcount * 2
1032 morevars['revcount'] = revcount * 2
1033
1033
1034 count = fctx.filerev() + 1
1034 count = fctx.filerev() + 1
1035 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
1036 end = min(count, start + revcount) # last rev on this page
1036 end = min(count, start + revcount) # last rev on this page
1037 parity = paritygen(web.stripecount, offset=start - end)
1037 parity = paritygen(web.stripecount, offset=start - end)
1038
1038
1039 def entries():
1039 def entries():
1040 l = []
1040 l = []
1041
1041
1042 repo = web.repo
1042 repo = web.repo
1043 revs = fctx.filelog().revs(start, end - 1)
1043 revs = fctx.filelog().revs(start, end - 1)
1044 for i in revs:
1044 for i in revs:
1045 iterfctx = fctx.filectx(i)
1045 iterfctx = fctx.filectx(i)
1046
1046
1047 l.append({"parity": parity.next(),
1047 l.append({"parity": parity.next(),
1048 "filerev": i,
1048 "filerev": i,
1049 "file": f,
1049 "file": f,
1050 "node": iterfctx.hex(),
1050 "node": iterfctx.hex(),
1051 "author": iterfctx.user(),
1051 "author": iterfctx.user(),
1052 "date": iterfctx.date(),
1052 "date": iterfctx.date(),
1053 "rename": webutil.renamelink(iterfctx),
1053 "rename": webutil.renamelink(iterfctx),
1054 "parent": webutil.parents(iterfctx),
1054 "parent": webutil.parents(iterfctx),
1055 "child": webutil.children(iterfctx),
1055 "child": webutil.children(iterfctx),
1056 "desc": iterfctx.description(),
1056 "desc": iterfctx.description(),
1057 "extra": iterfctx.extra(),
1057 "extra": iterfctx.extra(),
1058 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1058 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1059 "bookmarks": webutil.nodebookmarksdict(
1059 "bookmarks": webutil.nodebookmarksdict(
1060 repo, iterfctx.node()),
1060 repo, iterfctx.node()),
1061 "branch": webutil.nodebranchnodefault(iterfctx),
1061 "branch": webutil.nodebranchnodefault(iterfctx),
1062 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1062 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1063 "branches": webutil.nodebranchdict(repo, iterfctx)})
1063 "branches": webutil.nodebranchdict(repo, iterfctx)})
1064 for e in reversed(l):
1064 for e in reversed(l):
1065 yield e
1065 yield e
1066
1066
1067 entries = list(entries())
1067 entries = list(entries())
1068 latestentry = entries[:1]
1068 latestentry = entries[:1]
1069
1069
1070 revnav = webutil.filerevnav(web.repo, fctx.path())
1070 revnav = webutil.filerevnav(web.repo, fctx.path())
1071 nav = revnav.gen(end - 1, revcount, count)
1071 nav = revnav.gen(end - 1, revcount, count)
1072 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1072 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1073 entries=entries,
1073 entries=entries,
1074 latestentry=latestentry,
1074 latestentry=latestentry,
1075 revcount=revcount, morevars=morevars, lessvars=lessvars)
1075 revcount=revcount, morevars=morevars, lessvars=lessvars)
1076
1076
1077 @webcommand('archive')
1077 @webcommand('archive')
1078 def archive(web, req, tmpl):
1078 def archive(web, req, tmpl):
1079 """
1079 """
1080 /archive/{revision}.{format}[/{path}]
1080 /archive/{revision}.{format}[/{path}]
1081 -------------------------------------
1081 -------------------------------------
1082
1082
1083 Obtain an archive of repository content.
1083 Obtain an archive of repository content.
1084
1084
1085 The content and type of the archive is defined by a URL path parameter.
1085 The content and type of the archive is defined by a URL path parameter.
1086 ``format`` is the file extension of the archive type to be generated. e.g.
1086 ``format`` is the file extension of the archive type to be generated. e.g.
1087 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1087 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1088 server configuration.
1088 server configuration.
1089
1089
1090 The optional ``path`` URL parameter controls content to include in the
1090 The optional ``path`` URL parameter controls content to include in the
1091 archive. If omitted, every file in the specified revision is present in the
1091 archive. If omitted, every file in the specified revision is present in the
1092 archive. If included, only the specified file or contents of the specified
1092 archive. If included, only the specified file or contents of the specified
1093 directory will be included in the archive.
1093 directory will be included in the archive.
1094
1094
1095 No template is used for this handler. Raw, binary content is generated.
1095 No template is used for this handler. Raw, binary content is generated.
1096 """
1096 """
1097
1097
1098 type_ = req.form.get('type', [None])[0]
1098 type_ = req.form.get('type', [None])[0]
1099 allowed = web.configlist("web", "allow_archive")
1099 allowed = web.configlist("web", "allow_archive")
1100 key = req.form['node'][0]
1100 key = req.form['node'][0]
1101
1101
1102 if type_ not in web.archives:
1102 if type_ not in web.archives:
1103 msg = 'Unsupported archive type: %s' % type_
1103 msg = 'Unsupported archive type: %s' % type_
1104 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1104 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1105
1105
1106 if not ((type_ in allowed or
1106 if not ((type_ in allowed or
1107 web.configbool("web", "allow" + type_, False))):
1107 web.configbool("web", "allow" + type_, False))):
1108 msg = 'Archive type not allowed: %s' % type_
1108 msg = 'Archive type not allowed: %s' % type_
1109 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1109 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1110
1110
1111 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1111 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1112 cnode = web.repo.lookup(key)
1112 cnode = web.repo.lookup(key)
1113 arch_version = key
1113 arch_version = key
1114 if cnode == key or key == 'tip':
1114 if cnode == key or key == 'tip':
1115 arch_version = short(cnode)
1115 arch_version = short(cnode)
1116 name = "%s-%s" % (reponame, arch_version)
1116 name = "%s-%s" % (reponame, arch_version)
1117
1117
1118 ctx = webutil.changectx(web.repo, req)
1118 ctx = webutil.changectx(web.repo, req)
1119 pats = []
1119 pats = []
1120 matchfn = scmutil.match(ctx, [])
1120 matchfn = scmutil.match(ctx, [])
1121 file = req.form.get('file', None)
1121 file = req.form.get('file', None)
1122 if file:
1122 if file:
1123 pats = ['path:' + file[0]]
1123 pats = ['path:' + file[0]]
1124 matchfn = scmutil.match(ctx, pats, default='path')
1124 matchfn = scmutil.match(ctx, pats, default='path')
1125 if pats:
1125 if pats:
1126 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1126 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1127 if not files:
1127 if not files:
1128 raise ErrorResponse(HTTP_NOT_FOUND,
1128 raise ErrorResponse(HTTP_NOT_FOUND,
1129 'file(s) not found: %s' % file[0])
1129 'file(s) not found: %s' % file[0])
1130
1130
1131 mimetype, artype, extension, encoding = web.archive_specs[type_]
1131 mimetype, artype, extension, encoding = web.archive_specs[type_]
1132 headers = [
1132 headers = [
1133 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1133 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1134 ]
1134 ]
1135 if encoding:
1135 if encoding:
1136 headers.append(('Content-Encoding', encoding))
1136 headers.append(('Content-Encoding', encoding))
1137 req.headers.extend(headers)
1137 req.headers.extend(headers)
1138 req.respond(HTTP_OK, mimetype)
1138 req.respond(HTTP_OK, mimetype)
1139
1139
1140 archival.archive(web.repo, req, cnode, artype, prefix=name,
1140 archival.archive(web.repo, req, cnode, artype, prefix=name,
1141 matchfn=matchfn,
1141 matchfn=matchfn,
1142 subrepos=web.configbool("web", "archivesubrepos"))
1142 subrepos=web.configbool("web", "archivesubrepos"))
1143 return []
1143 return []
1144
1144
1145
1145
1146 @webcommand('static')
1146 @webcommand('static')
1147 def static(web, req, tmpl):
1147 def static(web, req, tmpl):
1148 fname = req.form['file'][0]
1148 fname = req.form['file'][0]
1149 # a repo owner may set web.static in .hg/hgrc to get any file
1149 # a repo owner may set web.static in .hg/hgrc to get any file
1150 # readable by the user running the CGI script
1150 # readable by the user running the CGI script
1151 static = web.config("web", "static", None, untrusted=False)
1151 static = web.config("web", "static", None, untrusted=False)
1152 if not static:
1152 if not static:
1153 tp = web.templatepath or templater.templatepaths()
1153 tp = web.templatepath or templater.templatepaths()
1154 if isinstance(tp, str):
1154 if isinstance(tp, str):
1155 tp = [tp]
1155 tp = [tp]
1156 static = [os.path.join(p, 'static') for p in tp]
1156 static = [os.path.join(p, 'static') for p in tp]
1157 staticfile(static, fname, req)
1157 staticfile(static, fname, req)
1158 return []
1158 return []
1159
1159
1160 @webcommand('graph')
1160 @webcommand('graph')
1161 def graph(web, req, tmpl):
1161 def graph(web, req, tmpl):
1162 """
1163 /graph[/{revision}]
1164 -------------------
1165
1166 Show information about the graphical topology of the repository.
1167
1168 Information rendered by this handler can be used to create visual
1169 representations of repository topology.
1170
1171 The ``revision`` URL parameter controls the starting changeset.
1172
1173 The ``revcount`` query string argument can define the number of changesets
1174 to show information for.
1175
1176 This handler will render the ``graph`` template.
1177 """
1162
1178
1163 ctx = webutil.changectx(web.repo, req)
1179 ctx = webutil.changectx(web.repo, req)
1164 rev = ctx.rev()
1180 rev = ctx.rev()
1165
1181
1166 bg_height = 39
1182 bg_height = 39
1167 revcount = web.maxshortchanges
1183 revcount = web.maxshortchanges
1168 if 'revcount' in req.form:
1184 if 'revcount' in req.form:
1169 try:
1185 try:
1170 revcount = int(req.form.get('revcount', [revcount])[0])
1186 revcount = int(req.form.get('revcount', [revcount])[0])
1171 revcount = max(revcount, 1)
1187 revcount = max(revcount, 1)
1172 tmpl.defaults['sessionvars']['revcount'] = revcount
1188 tmpl.defaults['sessionvars']['revcount'] = revcount
1173 except ValueError:
1189 except ValueError:
1174 pass
1190 pass
1175
1191
1176 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1192 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1177 lessvars['revcount'] = max(revcount / 2, 1)
1193 lessvars['revcount'] = max(revcount / 2, 1)
1178 morevars = copy.copy(tmpl.defaults['sessionvars'])
1194 morevars = copy.copy(tmpl.defaults['sessionvars'])
1179 morevars['revcount'] = revcount * 2
1195 morevars['revcount'] = revcount * 2
1180
1196
1181 count = len(web.repo)
1197 count = len(web.repo)
1182 pos = rev
1198 pos = rev
1183
1199
1184 uprev = min(max(0, count - 1), rev + revcount)
1200 uprev = min(max(0, count - 1), rev + revcount)
1185 downrev = max(0, rev - revcount)
1201 downrev = max(0, rev - revcount)
1186 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1202 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1187
1203
1188 tree = []
1204 tree = []
1189 if pos != -1:
1205 if pos != -1:
1190 allrevs = web.repo.changelog.revs(pos, 0)
1206 allrevs = web.repo.changelog.revs(pos, 0)
1191 revs = []
1207 revs = []
1192 for i in allrevs:
1208 for i in allrevs:
1193 revs.append(i)
1209 revs.append(i)
1194 if len(revs) >= revcount:
1210 if len(revs) >= revcount:
1195 break
1211 break
1196
1212
1197 # We have to feed a baseset to dagwalker as it is expecting smartset
1213 # We have to feed a baseset to dagwalker as it is expecting smartset
1198 # object. This does not have a big impact on hgweb performance itself
1214 # object. This does not have a big impact on hgweb performance itself
1199 # since hgweb graphing code is not itself lazy yet.
1215 # since hgweb graphing code is not itself lazy yet.
1200 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1216 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1201 # As we said one line above... not lazy.
1217 # As we said one line above... not lazy.
1202 tree = list(graphmod.colored(dag, web.repo))
1218 tree = list(graphmod.colored(dag, web.repo))
1203
1219
1204 def getcolumns(tree):
1220 def getcolumns(tree):
1205 cols = 0
1221 cols = 0
1206 for (id, type, ctx, vtx, edges) in tree:
1222 for (id, type, ctx, vtx, edges) in tree:
1207 if type != graphmod.CHANGESET:
1223 if type != graphmod.CHANGESET:
1208 continue
1224 continue
1209 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1225 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1210 max([edge[1] for edge in edges] or [0]))
1226 max([edge[1] for edge in edges] or [0]))
1211 return cols
1227 return cols
1212
1228
1213 def graphdata(usetuples, **map):
1229 def graphdata(usetuples, **map):
1214 data = []
1230 data = []
1215
1231
1216 row = 0
1232 row = 0
1217 for (id, type, ctx, vtx, edges) in tree:
1233 for (id, type, ctx, vtx, edges) in tree:
1218 if type != graphmod.CHANGESET:
1234 if type != graphmod.CHANGESET:
1219 continue
1235 continue
1220 node = str(ctx)
1236 node = str(ctx)
1221 age = templatefilters.age(ctx.date())
1237 age = templatefilters.age(ctx.date())
1222 desc = templatefilters.firstline(ctx.description())
1238 desc = templatefilters.firstline(ctx.description())
1223 desc = cgi.escape(templatefilters.nonempty(desc))
1239 desc = cgi.escape(templatefilters.nonempty(desc))
1224 user = cgi.escape(templatefilters.person(ctx.user()))
1240 user = cgi.escape(templatefilters.person(ctx.user()))
1225 branch = cgi.escape(ctx.branch())
1241 branch = cgi.escape(ctx.branch())
1226 try:
1242 try:
1227 branchnode = web.repo.branchtip(branch)
1243 branchnode = web.repo.branchtip(branch)
1228 except error.RepoLookupError:
1244 except error.RepoLookupError:
1229 branchnode = None
1245 branchnode = None
1230 branch = branch, branchnode == ctx.node()
1246 branch = branch, branchnode == ctx.node()
1231
1247
1232 if usetuples:
1248 if usetuples:
1233 data.append((node, vtx, edges, desc, user, age, branch,
1249 data.append((node, vtx, edges, desc, user, age, branch,
1234 [cgi.escape(x) for x in ctx.tags()],
1250 [cgi.escape(x) for x in ctx.tags()],
1235 [cgi.escape(x) for x in ctx.bookmarks()]))
1251 [cgi.escape(x) for x in ctx.bookmarks()]))
1236 else:
1252 else:
1237 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1253 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1238 'color': (edge[2] - 1) % 6 + 1,
1254 'color': (edge[2] - 1) % 6 + 1,
1239 'width': edge[3], 'bcolor': edge[4]}
1255 'width': edge[3], 'bcolor': edge[4]}
1240 for edge in edges]
1256 for edge in edges]
1241
1257
1242 data.append(
1258 data.append(
1243 {'node': node,
1259 {'node': node,
1244 'col': vtx[0],
1260 'col': vtx[0],
1245 'color': (vtx[1] - 1) % 6 + 1,
1261 'color': (vtx[1] - 1) % 6 + 1,
1246 'edges': edgedata,
1262 'edges': edgedata,
1247 'row': row,
1263 'row': row,
1248 'nextrow': row + 1,
1264 'nextrow': row + 1,
1249 'desc': desc,
1265 'desc': desc,
1250 'user': user,
1266 'user': user,
1251 'age': age,
1267 'age': age,
1252 'bookmarks': webutil.nodebookmarksdict(
1268 'bookmarks': webutil.nodebookmarksdict(
1253 web.repo, ctx.node()),
1269 web.repo, ctx.node()),
1254 'branches': webutil.nodebranchdict(web.repo, ctx),
1270 'branches': webutil.nodebranchdict(web.repo, ctx),
1255 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1271 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1256 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1272 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1257
1273
1258 row += 1
1274 row += 1
1259
1275
1260 return data
1276 return data
1261
1277
1262 cols = getcolumns(tree)
1278 cols = getcolumns(tree)
1263 rows = len(tree)
1279 rows = len(tree)
1264 canvasheight = (rows + 1) * bg_height - 27
1280 canvasheight = (rows + 1) * bg_height - 27
1265
1281
1266 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
1282 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
1267 lessvars=lessvars, morevars=morevars, downrev=downrev,
1283 lessvars=lessvars, morevars=morevars, downrev=downrev,
1268 cols=cols, rows=rows,
1284 cols=cols, rows=rows,
1269 canvaswidth=(cols + 1) * bg_height,
1285 canvaswidth=(cols + 1) * bg_height,
1270 truecanvasheight=rows * bg_height,
1286 truecanvasheight=rows * bg_height,
1271 canvasheight=canvasheight, bg_height=bg_height,
1287 canvasheight=canvasheight, bg_height=bg_height,
1272 jsdata=lambda **x: graphdata(True, **x),
1288 jsdata=lambda **x: graphdata(True, **x),
1273 nodes=lambda **x: graphdata(False, **x),
1289 nodes=lambda **x: graphdata(False, **x),
1274 node=ctx.hex(), changenav=changenav)
1290 node=ctx.hex(), changenav=changenav)
1275
1291
1276 def _getdoc(e):
1292 def _getdoc(e):
1277 doc = e[0].__doc__
1293 doc = e[0].__doc__
1278 if doc:
1294 if doc:
1279 doc = _(doc).split('\n')[0]
1295 doc = _(doc).split('\n')[0]
1280 else:
1296 else:
1281 doc = _('(no help text available)')
1297 doc = _('(no help text available)')
1282 return doc
1298 return doc
1283
1299
1284 @webcommand('help')
1300 @webcommand('help')
1285 def help(web, req, tmpl):
1301 def help(web, req, tmpl):
1286 """
1302 """
1287 /help[/{topic}]
1303 /help[/{topic}]
1288 ---------------
1304 ---------------
1289
1305
1290 Render help documentation.
1306 Render help documentation.
1291
1307
1292 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1308 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1293 is defined, that help topic will be rendered. If not, an index of
1309 is defined, that help topic will be rendered. If not, an index of
1294 available help topics will be rendered.
1310 available help topics will be rendered.
1295
1311
1296 The ``help`` template will be rendered when requesting help for a topic.
1312 The ``help`` template will be rendered when requesting help for a topic.
1297 ``helptopics`` will be rendered for the index of help topics.
1313 ``helptopics`` will be rendered for the index of help topics.
1298 """
1314 """
1299 from mercurial import commands # avoid cycle
1315 from mercurial import commands # avoid cycle
1300 from mercurial import help as helpmod # avoid cycle
1316 from mercurial import help as helpmod # avoid cycle
1301
1317
1302 topicname = req.form.get('node', [None])[0]
1318 topicname = req.form.get('node', [None])[0]
1303 if not topicname:
1319 if not topicname:
1304 def topics(**map):
1320 def topics(**map):
1305 for entries, summary, _doc in helpmod.helptable:
1321 for entries, summary, _doc in helpmod.helptable:
1306 yield {'topic': entries[0], 'summary': summary}
1322 yield {'topic': entries[0], 'summary': summary}
1307
1323
1308 early, other = [], []
1324 early, other = [], []
1309 primary = lambda s: s.split('|')[0]
1325 primary = lambda s: s.split('|')[0]
1310 for c, e in commands.table.iteritems():
1326 for c, e in commands.table.iteritems():
1311 doc = _getdoc(e)
1327 doc = _getdoc(e)
1312 if 'DEPRECATED' in doc or c.startswith('debug'):
1328 if 'DEPRECATED' in doc or c.startswith('debug'):
1313 continue
1329 continue
1314 cmd = primary(c)
1330 cmd = primary(c)
1315 if cmd.startswith('^'):
1331 if cmd.startswith('^'):
1316 early.append((cmd[1:], doc))
1332 early.append((cmd[1:], doc))
1317 else:
1333 else:
1318 other.append((cmd, doc))
1334 other.append((cmd, doc))
1319
1335
1320 early.sort()
1336 early.sort()
1321 other.sort()
1337 other.sort()
1322
1338
1323 def earlycommands(**map):
1339 def earlycommands(**map):
1324 for c, doc in early:
1340 for c, doc in early:
1325 yield {'topic': c, 'summary': doc}
1341 yield {'topic': c, 'summary': doc}
1326
1342
1327 def othercommands(**map):
1343 def othercommands(**map):
1328 for c, doc in other:
1344 for c, doc in other:
1329 yield {'topic': c, 'summary': doc}
1345 yield {'topic': c, 'summary': doc}
1330
1346
1331 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1347 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1332 othercommands=othercommands, title='Index')
1348 othercommands=othercommands, title='Index')
1333
1349
1334 u = webutil.wsgiui()
1350 u = webutil.wsgiui()
1335 u.verbose = True
1351 u.verbose = True
1336 try:
1352 try:
1337 doc = helpmod.help_(u, topicname)
1353 doc = helpmod.help_(u, topicname)
1338 except error.UnknownCommand:
1354 except error.UnknownCommand:
1339 raise ErrorResponse(HTTP_NOT_FOUND)
1355 raise ErrorResponse(HTTP_NOT_FOUND)
1340 return tmpl('help', topic=topicname, doc=doc)
1356 return tmpl('help', topic=topicname, doc=doc)
General Comments 0
You need to be logged in to leave comments. Login now