##// END OF EJS Templates
webcommands: use explicit integer division for Python 3 compat...
Augie Fackler -
r36571:24897a9d default
parent child Browse files
Show More
@@ -1,1411 +1,1411 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, nullid, short
16 from ..node import hex, nullid, 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 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
154 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
287 **pycompat.strkwargs(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 = next(iter(h.items()))
545 k, v = next(iter(h.items()))
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 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
564 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
711 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
780 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
855 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
946 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1047 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1064 **pycompat.strkwargs(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 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1083 **pycompat.strkwargs(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(br"\W+", "-", os.path.basename(web.reponame))
1119 reponame = re.sub(br"\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. If it's
1179 The ``revision`` URL parameter controls the starting changeset. If it's
1180 absent, the default is ``tip``.
1180 absent, the default is ``tip``.
1181
1181
1182 The ``revcount`` query string argument can define the number of changesets
1182 The ``revcount`` query string argument can define the number of changesets
1183 to show information for.
1183 to show information for.
1184
1184
1185 The ``graphtop`` query string argument can specify the starting changeset
1185 The ``graphtop`` query string argument can specify the starting changeset
1186 for producing ``jsdata`` variable that is used for rendering graph in
1186 for producing ``jsdata`` variable that is used for rendering graph in
1187 JavaScript. By default it has the same value as ``revision``.
1187 JavaScript. By default it has the same value as ``revision``.
1188
1188
1189 This handler will render the ``graph`` template.
1189 This handler will render the ``graph`` template.
1190 """
1190 """
1191
1191
1192 if 'node' in req.form:
1192 if 'node' in req.form:
1193 ctx = webutil.changectx(web.repo, req)
1193 ctx = webutil.changectx(web.repo, req)
1194 symrev = webutil.symrevorshortnode(req, ctx)
1194 symrev = webutil.symrevorshortnode(req, ctx)
1195 else:
1195 else:
1196 ctx = web.repo['tip']
1196 ctx = web.repo['tip']
1197 symrev = 'tip'
1197 symrev = 'tip'
1198 rev = ctx.rev()
1198 rev = ctx.rev()
1199
1199
1200 bg_height = 39
1200 bg_height = 39
1201 revcount = web.maxshortchanges
1201 revcount = web.maxshortchanges
1202 if 'revcount' in req.form:
1202 if 'revcount' in req.form:
1203 try:
1203 try:
1204 revcount = int(req.form.get('revcount', [revcount])[0])
1204 revcount = int(req.form.get('revcount', [revcount])[0])
1205 revcount = max(revcount, 1)
1205 revcount = max(revcount, 1)
1206 tmpl.defaults['sessionvars']['revcount'] = revcount
1206 tmpl.defaults['sessionvars']['revcount'] = revcount
1207 except ValueError:
1207 except ValueError:
1208 pass
1208 pass
1209
1209
1210 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1210 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1211 lessvars['revcount'] = max(revcount / 2, 1)
1211 lessvars['revcount'] = max(revcount // 2, 1)
1212 morevars = copy.copy(tmpl.defaults['sessionvars'])
1212 morevars = copy.copy(tmpl.defaults['sessionvars'])
1213 morevars['revcount'] = revcount * 2
1213 morevars['revcount'] = revcount * 2
1214
1214
1215 graphtop = req.form.get('graphtop', [ctx.hex()])[0]
1215 graphtop = req.form.get('graphtop', [ctx.hex()])[0]
1216 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1216 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1217 graphvars['graphtop'] = graphtop
1217 graphvars['graphtop'] = graphtop
1218
1218
1219 count = len(web.repo)
1219 count = len(web.repo)
1220 pos = rev
1220 pos = rev
1221
1221
1222 uprev = min(max(0, count - 1), rev + revcount)
1222 uprev = min(max(0, count - 1), rev + revcount)
1223 downrev = max(0, rev - revcount)
1223 downrev = max(0, rev - revcount)
1224 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1224 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1225
1225
1226 tree = []
1226 tree = []
1227 nextentry = []
1227 nextentry = []
1228 lastrev = 0
1228 lastrev = 0
1229 if pos != -1:
1229 if pos != -1:
1230 allrevs = web.repo.changelog.revs(pos, 0)
1230 allrevs = web.repo.changelog.revs(pos, 0)
1231 revs = []
1231 revs = []
1232 for i in allrevs:
1232 for i in allrevs:
1233 revs.append(i)
1233 revs.append(i)
1234 if len(revs) >= revcount + 1:
1234 if len(revs) >= revcount + 1:
1235 break
1235 break
1236
1236
1237 if len(revs) > revcount:
1237 if len(revs) > revcount:
1238 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1238 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1239 revs = revs[:-1]
1239 revs = revs[:-1]
1240
1240
1241 lastrev = revs[-1]
1241 lastrev = revs[-1]
1242
1242
1243 # We have to feed a baseset to dagwalker as it is expecting smartset
1243 # We have to feed a baseset to dagwalker as it is expecting smartset
1244 # object. This does not have a big impact on hgweb performance itself
1244 # object. This does not have a big impact on hgweb performance itself
1245 # since hgweb graphing code is not itself lazy yet.
1245 # since hgweb graphing code is not itself lazy yet.
1246 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1246 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1247 # As we said one line above... not lazy.
1247 # As we said one line above... not lazy.
1248 tree = list(item for item in graphmod.colored(dag, web.repo)
1248 tree = list(item for item in graphmod.colored(dag, web.repo)
1249 if item[1] == graphmod.CHANGESET)
1249 if item[1] == graphmod.CHANGESET)
1250
1250
1251 def nodecurrent(ctx):
1251 def nodecurrent(ctx):
1252 wpnodes = web.repo.dirstate.parents()
1252 wpnodes = web.repo.dirstate.parents()
1253 if wpnodes[1] == nullid:
1253 if wpnodes[1] == nullid:
1254 wpnodes = wpnodes[:1]
1254 wpnodes = wpnodes[:1]
1255 if ctx.node() in wpnodes:
1255 if ctx.node() in wpnodes:
1256 return '@'
1256 return '@'
1257 return ''
1257 return ''
1258
1258
1259 def nodesymbol(ctx):
1259 def nodesymbol(ctx):
1260 if ctx.obsolete():
1260 if ctx.obsolete():
1261 return 'x'
1261 return 'x'
1262 elif ctx.isunstable():
1262 elif ctx.isunstable():
1263 return '*'
1263 return '*'
1264 elif ctx.closesbranch():
1264 elif ctx.closesbranch():
1265 return '_'
1265 return '_'
1266 else:
1266 else:
1267 return 'o'
1267 return 'o'
1268
1268
1269 def fulltree():
1269 def fulltree():
1270 pos = web.repo[graphtop].rev()
1270 pos = web.repo[graphtop].rev()
1271 tree = []
1271 tree = []
1272 if pos != -1:
1272 if pos != -1:
1273 revs = web.repo.changelog.revs(pos, lastrev)
1273 revs = web.repo.changelog.revs(pos, lastrev)
1274 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1274 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1275 tree = list(item for item in graphmod.colored(dag, web.repo)
1275 tree = list(item for item in graphmod.colored(dag, web.repo)
1276 if item[1] == graphmod.CHANGESET)
1276 if item[1] == graphmod.CHANGESET)
1277 return tree
1277 return tree
1278
1278
1279 def jsdata():
1279 def jsdata():
1280 return [{'node': pycompat.bytestr(ctx),
1280 return [{'node': pycompat.bytestr(ctx),
1281 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1281 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1282 'vertex': vtx,
1282 'vertex': vtx,
1283 'edges': edges}
1283 'edges': edges}
1284 for (id, type, ctx, vtx, edges) in fulltree()]
1284 for (id, type, ctx, vtx, edges) in fulltree()]
1285
1285
1286 def nodes():
1286 def nodes():
1287 parity = paritygen(web.stripecount)
1287 parity = paritygen(web.stripecount)
1288 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1288 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1289 entry = webutil.commonentry(web.repo, ctx)
1289 entry = webutil.commonentry(web.repo, ctx)
1290 edgedata = [{'col': edge[0],
1290 edgedata = [{'col': edge[0],
1291 'nextcol': edge[1],
1291 'nextcol': edge[1],
1292 'color': (edge[2] - 1) % 6 + 1,
1292 'color': (edge[2] - 1) % 6 + 1,
1293 'width': edge[3],
1293 'width': edge[3],
1294 'bcolor': edge[4]}
1294 'bcolor': edge[4]}
1295 for edge in edges]
1295 for edge in edges]
1296
1296
1297 entry.update({'col': vtx[0],
1297 entry.update({'col': vtx[0],
1298 'color': (vtx[1] - 1) % 6 + 1,
1298 'color': (vtx[1] - 1) % 6 + 1,
1299 'parity': next(parity),
1299 'parity': next(parity),
1300 'edges': edgedata,
1300 'edges': edgedata,
1301 'row': row,
1301 'row': row,
1302 'nextrow': row + 1})
1302 'nextrow': row + 1})
1303
1303
1304 yield entry
1304 yield entry
1305
1305
1306 rows = len(tree)
1306 rows = len(tree)
1307
1307
1308 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1308 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1309 uprev=uprev,
1309 uprev=uprev,
1310 lessvars=lessvars, morevars=morevars, downrev=downrev,
1310 lessvars=lessvars, morevars=morevars, downrev=downrev,
1311 graphvars=graphvars,
1311 graphvars=graphvars,
1312 rows=rows,
1312 rows=rows,
1313 bg_height=bg_height,
1313 bg_height=bg_height,
1314 changesets=count,
1314 changesets=count,
1315 nextentry=nextentry,
1315 nextentry=nextentry,
1316 jsdata=lambda **x: jsdata(),
1316 jsdata=lambda **x: jsdata(),
1317 nodes=lambda **x: nodes(),
1317 nodes=lambda **x: nodes(),
1318 node=ctx.hex(), changenav=changenav)
1318 node=ctx.hex(), changenav=changenav)
1319
1319
1320 def _getdoc(e):
1320 def _getdoc(e):
1321 doc = e[0].__doc__
1321 doc = e[0].__doc__
1322 if doc:
1322 if doc:
1323 doc = _(doc).partition('\n')[0]
1323 doc = _(doc).partition('\n')[0]
1324 else:
1324 else:
1325 doc = _('(no help text available)')
1325 doc = _('(no help text available)')
1326 return doc
1326 return doc
1327
1327
1328 @webcommand('help')
1328 @webcommand('help')
1329 def help(web, req, tmpl):
1329 def help(web, req, tmpl):
1330 """
1330 """
1331 /help[/{topic}]
1331 /help[/{topic}]
1332 ---------------
1332 ---------------
1333
1333
1334 Render help documentation.
1334 Render help documentation.
1335
1335
1336 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1336 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1337 is defined, that help topic will be rendered. If not, an index of
1337 is defined, that help topic will be rendered. If not, an index of
1338 available help topics will be rendered.
1338 available help topics will be rendered.
1339
1339
1340 The ``help`` template will be rendered when requesting help for a topic.
1340 The ``help`` template will be rendered when requesting help for a topic.
1341 ``helptopics`` will be rendered for the index of help topics.
1341 ``helptopics`` will be rendered for the index of help topics.
1342 """
1342 """
1343 from .. import commands, help as helpmod # avoid cycle
1343 from .. import commands, help as helpmod # avoid cycle
1344
1344
1345 topicname = req.form.get('node', [None])[0]
1345 topicname = req.form.get('node', [None])[0]
1346 if not topicname:
1346 if not topicname:
1347 def topics(**map):
1347 def topics(**map):
1348 for entries, summary, _doc in helpmod.helptable:
1348 for entries, summary, _doc in helpmod.helptable:
1349 yield {'topic': entries[0], 'summary': summary}
1349 yield {'topic': entries[0], 'summary': summary}
1350
1350
1351 early, other = [], []
1351 early, other = [], []
1352 primary = lambda s: s.partition('|')[0]
1352 primary = lambda s: s.partition('|')[0]
1353 for c, e in commands.table.iteritems():
1353 for c, e in commands.table.iteritems():
1354 doc = _getdoc(e)
1354 doc = _getdoc(e)
1355 if 'DEPRECATED' in doc or c.startswith('debug'):
1355 if 'DEPRECATED' in doc or c.startswith('debug'):
1356 continue
1356 continue
1357 cmd = primary(c)
1357 cmd = primary(c)
1358 if cmd.startswith('^'):
1358 if cmd.startswith('^'):
1359 early.append((cmd[1:], doc))
1359 early.append((cmd[1:], doc))
1360 else:
1360 else:
1361 other.append((cmd, doc))
1361 other.append((cmd, doc))
1362
1362
1363 early.sort()
1363 early.sort()
1364 other.sort()
1364 other.sort()
1365
1365
1366 def earlycommands(**map):
1366 def earlycommands(**map):
1367 for c, doc in early:
1367 for c, doc in early:
1368 yield {'topic': c, 'summary': doc}
1368 yield {'topic': c, 'summary': doc}
1369
1369
1370 def othercommands(**map):
1370 def othercommands(**map):
1371 for c, doc in other:
1371 for c, doc in other:
1372 yield {'topic': c, 'summary': doc}
1372 yield {'topic': c, 'summary': doc}
1373
1373
1374 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1374 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1375 othercommands=othercommands, title='Index')
1375 othercommands=othercommands, title='Index')
1376
1376
1377 # Render an index of sub-topics.
1377 # Render an index of sub-topics.
1378 if topicname in helpmod.subtopics:
1378 if topicname in helpmod.subtopics:
1379 topics = []
1379 topics = []
1380 for entries, summary, _doc in helpmod.subtopics[topicname]:
1380 for entries, summary, _doc in helpmod.subtopics[topicname]:
1381 topics.append({
1381 topics.append({
1382 'topic': '%s.%s' % (topicname, entries[0]),
1382 'topic': '%s.%s' % (topicname, entries[0]),
1383 'basename': entries[0],
1383 'basename': entries[0],
1384 'summary': summary,
1384 'summary': summary,
1385 })
1385 })
1386
1386
1387 return tmpl('helptopics', topics=topics, title=topicname,
1387 return tmpl('helptopics', topics=topics, title=topicname,
1388 subindex=True)
1388 subindex=True)
1389
1389
1390 u = webutil.wsgiui.load()
1390 u = webutil.wsgiui.load()
1391 u.verbose = True
1391 u.verbose = True
1392
1392
1393 # Render a page from a sub-topic.
1393 # Render a page from a sub-topic.
1394 if '.' in topicname:
1394 if '.' in topicname:
1395 # TODO implement support for rendering sections, like
1395 # TODO implement support for rendering sections, like
1396 # `hg help` works.
1396 # `hg help` works.
1397 topic, subtopic = topicname.split('.', 1)
1397 topic, subtopic = topicname.split('.', 1)
1398 if topic not in helpmod.subtopics:
1398 if topic not in helpmod.subtopics:
1399 raise ErrorResponse(HTTP_NOT_FOUND)
1399 raise ErrorResponse(HTTP_NOT_FOUND)
1400 else:
1400 else:
1401 topic = topicname
1401 topic = topicname
1402 subtopic = None
1402 subtopic = None
1403
1403
1404 try:
1404 try:
1405 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1405 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1406 except error.Abort:
1406 except error.Abort:
1407 raise ErrorResponse(HTTP_NOT_FOUND)
1407 raise ErrorResponse(HTTP_NOT_FOUND)
1408 return tmpl('help', topic=topicname, doc=doc)
1408 return tmpl('help', topic=topicname, doc=doc)
1409
1409
1410 # tell hggettext to extract docstrings from these functions:
1410 # tell hggettext to extract docstrings from these functions:
1411 i18nfunctions = commands.values()
1411 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now