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