##// END OF EJS Templates
py3: use pycompat.strkwargs to convert kwargs keys to str...
Pulkit Goyal -
r36452:1fb9e013 default
parent child Browse files
Show More
@@ -1,1411 +1,1411
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 **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 **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 **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 **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 **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 **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 **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 **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 **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()
@@ -1,654 +1,654
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 patch,
31 patch,
32 pathutil,
32 pathutil,
33 pycompat,
33 pycompat,
34 templatefilters,
34 templatefilters,
35 templatekw,
35 templatekw,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 )
38 )
39
39
40 def up(p):
40 def up(p):
41 if p[0] != "/":
41 if p[0] != "/":
42 p = "/" + p
42 p = "/" + p
43 if p[-1] == "/":
43 if p[-1] == "/":
44 p = p[:-1]
44 p = p[:-1]
45 up = os.path.dirname(p)
45 up = os.path.dirname(p)
46 if up == "/":
46 if up == "/":
47 return "/"
47 return "/"
48 return up + "/"
48 return up + "/"
49
49
50 def _navseq(step, firststep=None):
50 def _navseq(step, firststep=None):
51 if firststep:
51 if firststep:
52 yield firststep
52 yield firststep
53 if firststep >= 20 and firststep <= 40:
53 if firststep >= 20 and firststep <= 40:
54 firststep = 50
54 firststep = 50
55 yield firststep
55 yield firststep
56 assert step > 0
56 assert step > 0
57 assert firststep > 0
57 assert firststep > 0
58 while step <= firststep:
58 while step <= firststep:
59 step *= 10
59 step *= 10
60 while True:
60 while True:
61 yield 1 * step
61 yield 1 * step
62 yield 3 * step
62 yield 3 * step
63 step *= 10
63 step *= 10
64
64
65 class revnav(object):
65 class revnav(object):
66
66
67 def __init__(self, repo):
67 def __init__(self, repo):
68 """Navigation generation object
68 """Navigation generation object
69
69
70 :repo: repo object we generate nav for
70 :repo: repo object we generate nav for
71 """
71 """
72 # used for hex generation
72 # used for hex generation
73 self._revlog = repo.changelog
73 self._revlog = repo.changelog
74
74
75 def __nonzero__(self):
75 def __nonzero__(self):
76 """return True if any revision to navigate over"""
76 """return True if any revision to navigate over"""
77 return self._first() is not None
77 return self._first() is not None
78
78
79 __bool__ = __nonzero__
79 __bool__ = __nonzero__
80
80
81 def _first(self):
81 def _first(self):
82 """return the minimum non-filtered changeset or None"""
82 """return the minimum non-filtered changeset or None"""
83 try:
83 try:
84 return next(iter(self._revlog))
84 return next(iter(self._revlog))
85 except StopIteration:
85 except StopIteration:
86 return None
86 return None
87
87
88 def hex(self, rev):
88 def hex(self, rev):
89 return hex(self._revlog.node(rev))
89 return hex(self._revlog.node(rev))
90
90
91 def gen(self, pos, pagelen, limit):
91 def gen(self, pos, pagelen, limit):
92 """computes label and revision id for navigation link
92 """computes label and revision id for navigation link
93
93
94 :pos: is the revision relative to which we generate navigation.
94 :pos: is the revision relative to which we generate navigation.
95 :pagelen: the size of each navigation page
95 :pagelen: the size of each navigation page
96 :limit: how far shall we link
96 :limit: how far shall we link
97
97
98 The return is:
98 The return is:
99 - a single element tuple
99 - a single element tuple
100 - containing a dictionary with a `before` and `after` key
100 - containing a dictionary with a `before` and `after` key
101 - values are generator functions taking arbitrary number of kwargs
101 - values are generator functions taking arbitrary number of kwargs
102 - yield items are dictionaries with `label` and `node` keys
102 - yield items are dictionaries with `label` and `node` keys
103 """
103 """
104 if not self:
104 if not self:
105 # empty repo
105 # empty repo
106 return ({'before': (), 'after': ()},)
106 return ({'before': (), 'after': ()},)
107
107
108 targets = []
108 targets = []
109 for f in _navseq(1, pagelen):
109 for f in _navseq(1, pagelen):
110 if f > limit:
110 if f > limit:
111 break
111 break
112 targets.append(pos + f)
112 targets.append(pos + f)
113 targets.append(pos - f)
113 targets.append(pos - f)
114 targets.sort()
114 targets.sort()
115
115
116 first = self._first()
116 first = self._first()
117 navbefore = [("(%i)" % first, self.hex(first))]
117 navbefore = [("(%i)" % first, self.hex(first))]
118 navafter = []
118 navafter = []
119 for rev in targets:
119 for rev in targets:
120 if rev not in self._revlog:
120 if rev not in self._revlog:
121 continue
121 continue
122 if pos < rev < limit:
122 if pos < rev < limit:
123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
124 if 0 < rev < pos:
124 if 0 < rev < pos:
125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
126
126
127
127
128 navafter.append(("tip", "tip"))
128 navafter.append(("tip", "tip"))
129
129
130 data = lambda i: {"label": i[0], "node": i[1]}
130 data = lambda i: {"label": i[0], "node": i[1]}
131 return ({'before': lambda **map: (data(i) for i in navbefore),
131 return ({'before': lambda **map: (data(i) for i in navbefore),
132 'after': lambda **map: (data(i) for i in navafter)},)
132 'after': lambda **map: (data(i) for i in navafter)},)
133
133
134 class filerevnav(revnav):
134 class filerevnav(revnav):
135
135
136 def __init__(self, repo, path):
136 def __init__(self, repo, path):
137 """Navigation generation object
137 """Navigation generation object
138
138
139 :repo: repo object we generate nav for
139 :repo: repo object we generate nav for
140 :path: path of the file we generate nav for
140 :path: path of the file we generate nav for
141 """
141 """
142 # used for iteration
142 # used for iteration
143 self._changelog = repo.unfiltered().changelog
143 self._changelog = repo.unfiltered().changelog
144 # used for hex generation
144 # used for hex generation
145 self._revlog = repo.file(path)
145 self._revlog = repo.file(path)
146
146
147 def hex(self, rev):
147 def hex(self, rev):
148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
149
149
150 class _siblings(object):
150 class _siblings(object):
151 def __init__(self, siblings=None, hiderev=None):
151 def __init__(self, siblings=None, hiderev=None):
152 if siblings is None:
152 if siblings is None:
153 siblings = []
153 siblings = []
154 self.siblings = [s for s in siblings if s.node() != nullid]
154 self.siblings = [s for s in siblings if s.node() != nullid]
155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
156 self.siblings = []
156 self.siblings = []
157
157
158 def __iter__(self):
158 def __iter__(self):
159 for s in self.siblings:
159 for s in self.siblings:
160 d = {
160 d = {
161 'node': s.hex(),
161 'node': s.hex(),
162 'rev': s.rev(),
162 'rev': s.rev(),
163 'user': s.user(),
163 'user': s.user(),
164 'date': s.date(),
164 'date': s.date(),
165 'description': s.description(),
165 'description': s.description(),
166 'branch': s.branch(),
166 'branch': s.branch(),
167 }
167 }
168 if util.safehasattr(s, 'path'):
168 if util.safehasattr(s, 'path'):
169 d['file'] = s.path()
169 d['file'] = s.path()
170 yield d
170 yield d
171
171
172 def __len__(self):
172 def __len__(self):
173 return len(self.siblings)
173 return len(self.siblings)
174
174
175 def difffeatureopts(req, ui, section):
175 def difffeatureopts(req, ui, section):
176 diffopts = patch.difffeatureopts(ui, untrusted=True,
176 diffopts = patch.difffeatureopts(ui, untrusted=True,
177 section=section, whitespace=True)
177 section=section, whitespace=True)
178
178
179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
180 v = req.form.get(k, [None])[0]
180 v = req.form.get(k, [None])[0]
181 if v is not None:
181 if v is not None:
182 v = util.parsebool(v)
182 v = util.parsebool(v)
183 setattr(diffopts, k, v if v is not None else True)
183 setattr(diffopts, k, v if v is not None else True)
184
184
185 return diffopts
185 return diffopts
186
186
187 def annotate(req, fctx, ui):
187 def annotate(req, fctx, ui):
188 diffopts = difffeatureopts(req, ui, 'annotate')
188 diffopts = difffeatureopts(req, ui, 'annotate')
189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
190
190
191 def parents(ctx, hide=None):
191 def parents(ctx, hide=None):
192 if isinstance(ctx, context.basefilectx):
192 if isinstance(ctx, context.basefilectx):
193 introrev = ctx.introrev()
193 introrev = ctx.introrev()
194 if ctx.changectx().rev() != introrev:
194 if ctx.changectx().rev() != introrev:
195 return _siblings([ctx.repo()[introrev]], hide)
195 return _siblings([ctx.repo()[introrev]], hide)
196 return _siblings(ctx.parents(), hide)
196 return _siblings(ctx.parents(), hide)
197
197
198 def children(ctx, hide=None):
198 def children(ctx, hide=None):
199 return _siblings(ctx.children(), hide)
199 return _siblings(ctx.children(), hide)
200
200
201 def renamelink(fctx):
201 def renamelink(fctx):
202 r = fctx.renamed()
202 r = fctx.renamed()
203 if r:
203 if r:
204 return [{'file': r[0], 'node': hex(r[1])}]
204 return [{'file': r[0], 'node': hex(r[1])}]
205 return []
205 return []
206
206
207 def nodetagsdict(repo, node):
207 def nodetagsdict(repo, node):
208 return [{"name": i} for i in repo.nodetags(node)]
208 return [{"name": i} for i in repo.nodetags(node)]
209
209
210 def nodebookmarksdict(repo, node):
210 def nodebookmarksdict(repo, node):
211 return [{"name": i} for i in repo.nodebookmarks(node)]
211 return [{"name": i} for i in repo.nodebookmarks(node)]
212
212
213 def nodebranchdict(repo, ctx):
213 def nodebranchdict(repo, ctx):
214 branches = []
214 branches = []
215 branch = ctx.branch()
215 branch = ctx.branch()
216 # If this is an empty repo, ctx.node() == nullid,
216 # If this is an empty repo, ctx.node() == nullid,
217 # ctx.branch() == 'default'.
217 # ctx.branch() == 'default'.
218 try:
218 try:
219 branchnode = repo.branchtip(branch)
219 branchnode = repo.branchtip(branch)
220 except error.RepoLookupError:
220 except error.RepoLookupError:
221 branchnode = None
221 branchnode = None
222 if branchnode == ctx.node():
222 if branchnode == ctx.node():
223 branches.append({"name": branch})
223 branches.append({"name": branch})
224 return branches
224 return branches
225
225
226 def nodeinbranch(repo, ctx):
226 def nodeinbranch(repo, ctx):
227 branches = []
227 branches = []
228 branch = ctx.branch()
228 branch = ctx.branch()
229 try:
229 try:
230 branchnode = repo.branchtip(branch)
230 branchnode = repo.branchtip(branch)
231 except error.RepoLookupError:
231 except error.RepoLookupError:
232 branchnode = None
232 branchnode = None
233 if branch != 'default' and branchnode != ctx.node():
233 if branch != 'default' and branchnode != ctx.node():
234 branches.append({"name": branch})
234 branches.append({"name": branch})
235 return branches
235 return branches
236
236
237 def nodebranchnodefault(ctx):
237 def nodebranchnodefault(ctx):
238 branches = []
238 branches = []
239 branch = ctx.branch()
239 branch = ctx.branch()
240 if branch != 'default':
240 if branch != 'default':
241 branches.append({"name": branch})
241 branches.append({"name": branch})
242 return branches
242 return branches
243
243
244 def showtag(repo, tmpl, t1, node=nullid, **args):
244 def showtag(repo, tmpl, t1, node=nullid, **args):
245 for t in repo.nodetags(node):
245 for t in repo.nodetags(node):
246 yield tmpl(t1, tag=t, **args)
246 yield tmpl(t1, tag=t, **args)
247
247
248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
249 for t in repo.nodebookmarks(node):
249 for t in repo.nodebookmarks(node):
250 yield tmpl(t1, bookmark=t, **args)
250 yield tmpl(t1, bookmark=t, **args)
251
251
252 def branchentries(repo, stripecount, limit=0):
252 def branchentries(repo, stripecount, limit=0):
253 tips = []
253 tips = []
254 heads = repo.heads()
254 heads = repo.heads()
255 parity = paritygen(stripecount)
255 parity = paritygen(stripecount)
256 sortkey = lambda item: (not item[1], item[0].rev())
256 sortkey = lambda item: (not item[1], item[0].rev())
257
257
258 def entries(**map):
258 def entries(**map):
259 count = 0
259 count = 0
260 if not tips:
260 if not tips:
261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
262 tips.append((repo[tip], closed))
262 tips.append((repo[tip], closed))
263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
264 if limit > 0 and count >= limit:
264 if limit > 0 and count >= limit:
265 return
265 return
266 count += 1
266 count += 1
267 if closed:
267 if closed:
268 status = 'closed'
268 status = 'closed'
269 elif ctx.node() not in heads:
269 elif ctx.node() not in heads:
270 status = 'inactive'
270 status = 'inactive'
271 else:
271 else:
272 status = 'open'
272 status = 'open'
273 yield {
273 yield {
274 'parity': next(parity),
274 'parity': next(parity),
275 'branch': ctx.branch(),
275 'branch': ctx.branch(),
276 'status': status,
276 'status': status,
277 'node': ctx.hex(),
277 'node': ctx.hex(),
278 'date': ctx.date()
278 'date': ctx.date()
279 }
279 }
280
280
281 return entries
281 return entries
282
282
283 def cleanpath(repo, path):
283 def cleanpath(repo, path):
284 path = path.lstrip('/')
284 path = path.lstrip('/')
285 return pathutil.canonpath(repo.root, '', path)
285 return pathutil.canonpath(repo.root, '', path)
286
286
287 def changeidctx(repo, changeid):
287 def changeidctx(repo, changeid):
288 try:
288 try:
289 ctx = repo[changeid]
289 ctx = repo[changeid]
290 except error.RepoError:
290 except error.RepoError:
291 man = repo.manifestlog._revlog
291 man = repo.manifestlog._revlog
292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
293
293
294 return ctx
294 return ctx
295
295
296 def changectx(repo, req):
296 def changectx(repo, req):
297 changeid = "tip"
297 changeid = "tip"
298 if 'node' in req.form:
298 if 'node' in req.form:
299 changeid = req.form['node'][0]
299 changeid = req.form['node'][0]
300 ipos = changeid.find(':')
300 ipos = changeid.find(':')
301 if ipos != -1:
301 if ipos != -1:
302 changeid = changeid[(ipos + 1):]
302 changeid = changeid[(ipos + 1):]
303 elif 'manifest' in req.form:
303 elif 'manifest' in req.form:
304 changeid = req.form['manifest'][0]
304 changeid = req.form['manifest'][0]
305
305
306 return changeidctx(repo, changeid)
306 return changeidctx(repo, changeid)
307
307
308 def basechangectx(repo, req):
308 def basechangectx(repo, req):
309 if 'node' in req.form:
309 if 'node' in req.form:
310 changeid = req.form['node'][0]
310 changeid = req.form['node'][0]
311 ipos = changeid.find(':')
311 ipos = changeid.find(':')
312 if ipos != -1:
312 if ipos != -1:
313 changeid = changeid[:ipos]
313 changeid = changeid[:ipos]
314 return changeidctx(repo, changeid)
314 return changeidctx(repo, changeid)
315
315
316 return None
316 return None
317
317
318 def filectx(repo, req):
318 def filectx(repo, req):
319 if 'file' not in req.form:
319 if 'file' not in req.form:
320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 path = cleanpath(repo, req.form['file'][0])
321 path = cleanpath(repo, req.form['file'][0])
322 if 'node' in req.form:
322 if 'node' in req.form:
323 changeid = req.form['node'][0]
323 changeid = req.form['node'][0]
324 elif 'filenode' in req.form:
324 elif 'filenode' in req.form:
325 changeid = req.form['filenode'][0]
325 changeid = req.form['filenode'][0]
326 else:
326 else:
327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
328 try:
328 try:
329 fctx = repo[changeid][path]
329 fctx = repo[changeid][path]
330 except error.RepoError:
330 except error.RepoError:
331 fctx = repo.filectx(path, fileid=changeid)
331 fctx = repo.filectx(path, fileid=changeid)
332
332
333 return fctx
333 return fctx
334
334
335 def linerange(req):
335 def linerange(req):
336 linerange = req.form.get('linerange')
336 linerange = req.form.get('linerange')
337 if linerange is None:
337 if linerange is None:
338 return None
338 return None
339 if len(linerange) > 1:
339 if len(linerange) > 1:
340 raise ErrorResponse(HTTP_BAD_REQUEST,
340 raise ErrorResponse(HTTP_BAD_REQUEST,
341 'redundant linerange parameter')
341 'redundant linerange parameter')
342 try:
342 try:
343 fromline, toline = map(int, linerange[0].split(':', 1))
343 fromline, toline = map(int, linerange[0].split(':', 1))
344 except ValueError:
344 except ValueError:
345 raise ErrorResponse(HTTP_BAD_REQUEST,
345 raise ErrorResponse(HTTP_BAD_REQUEST,
346 'invalid linerange parameter')
346 'invalid linerange parameter')
347 try:
347 try:
348 return util.processlinerange(fromline, toline)
348 return util.processlinerange(fromline, toline)
349 except error.ParseError as exc:
349 except error.ParseError as exc:
350 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
350 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
351
351
352 def formatlinerange(fromline, toline):
352 def formatlinerange(fromline, toline):
353 return '%d:%d' % (fromline + 1, toline)
353 return '%d:%d' % (fromline + 1, toline)
354
354
355 def succsandmarkers(repo, ctx):
355 def succsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
357 item['successors'] = _siblings(repo[successor]
357 item['successors'] = _siblings(repo[successor]
358 for successor in item['successors'])
358 for successor in item['successors'])
359 yield item
359 yield item
360
360
361 def commonentry(repo, ctx):
361 def commonentry(repo, ctx):
362 node = ctx.node()
362 node = ctx.node()
363 return {
363 return {
364 'rev': ctx.rev(),
364 'rev': ctx.rev(),
365 'node': hex(node),
365 'node': hex(node),
366 'author': ctx.user(),
366 'author': ctx.user(),
367 'desc': ctx.description(),
367 'desc': ctx.description(),
368 'date': ctx.date(),
368 'date': ctx.date(),
369 'extra': ctx.extra(),
369 'extra': ctx.extra(),
370 'phase': ctx.phasestr(),
370 'phase': ctx.phasestr(),
371 'obsolete': ctx.obsolete(),
371 'obsolete': ctx.obsolete(),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
374 'branch': nodebranchnodefault(ctx),
374 'branch': nodebranchnodefault(ctx),
375 'inbranch': nodeinbranch(repo, ctx),
375 'inbranch': nodeinbranch(repo, ctx),
376 'branches': nodebranchdict(repo, ctx),
376 'branches': nodebranchdict(repo, ctx),
377 'tags': nodetagsdict(repo, node),
377 'tags': nodetagsdict(repo, node),
378 'bookmarks': nodebookmarksdict(repo, node),
378 'bookmarks': nodebookmarksdict(repo, node),
379 'parent': lambda **x: parents(ctx),
379 'parent': lambda **x: parents(ctx),
380 'child': lambda **x: children(ctx),
380 'child': lambda **x: children(ctx),
381 }
381 }
382
382
383 def changelistentry(web, ctx, tmpl):
383 def changelistentry(web, ctx, tmpl):
384 '''Obtain a dictionary to be used for entries in a changelist.
384 '''Obtain a dictionary to be used for entries in a changelist.
385
385
386 This function is called when producing items for the "entries" list passed
386 This function is called when producing items for the "entries" list passed
387 to the "shortlog" and "changelog" templates.
387 to the "shortlog" and "changelog" templates.
388 '''
388 '''
389 repo = web.repo
389 repo = web.repo
390 rev = ctx.rev()
390 rev = ctx.rev()
391 n = ctx.node()
391 n = ctx.node()
392 showtags = showtag(repo, tmpl, 'changelogtag', n)
392 showtags = showtag(repo, tmpl, 'changelogtag', n)
393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
394
394
395 entry = commonentry(repo, ctx)
395 entry = commonentry(repo, ctx)
396 entry.update(
396 entry.update(
397 allparents=lambda **x: parents(ctx),
397 allparents=lambda **x: parents(ctx),
398 parent=lambda **x: parents(ctx, rev - 1),
398 parent=lambda **x: parents(ctx, rev - 1),
399 child=lambda **x: children(ctx, rev + 1),
399 child=lambda **x: children(ctx, rev + 1),
400 changelogtag=showtags,
400 changelogtag=showtags,
401 files=files,
401 files=files,
402 )
402 )
403 return entry
403 return entry
404
404
405 def symrevorshortnode(req, ctx):
405 def symrevorshortnode(req, ctx):
406 if 'node' in req.form:
406 if 'node' in req.form:
407 return templatefilters.revescape(req.form['node'][0])
407 return templatefilters.revescape(req.form['node'][0])
408 else:
408 else:
409 return short(ctx.node())
409 return short(ctx.node())
410
410
411 def changesetentry(web, req, tmpl, ctx):
411 def changesetentry(web, req, tmpl, ctx):
412 '''Obtain a dictionary to be used to render the "changeset" template.'''
412 '''Obtain a dictionary to be used to render the "changeset" template.'''
413
413
414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
416 ctx.node())
416 ctx.node())
417 showbranch = nodebranchnodefault(ctx)
417 showbranch = nodebranchnodefault(ctx)
418
418
419 files = []
419 files = []
420 parity = paritygen(web.stripecount)
420 parity = paritygen(web.stripecount)
421 for blockno, f in enumerate(ctx.files()):
421 for blockno, f in enumerate(ctx.files()):
422 template = 'filenodelink' if f in ctx else 'filenolink'
422 template = 'filenodelink' if f in ctx else 'filenolink'
423 files.append(tmpl(template,
423 files.append(tmpl(template,
424 node=ctx.hex(), file=f, blockno=blockno + 1,
424 node=ctx.hex(), file=f, blockno=blockno + 1,
425 parity=next(parity)))
425 parity=next(parity)))
426
426
427 basectx = basechangectx(web.repo, req)
427 basectx = basechangectx(web.repo, req)
428 if basectx is None:
428 if basectx is None:
429 basectx = ctx.p1()
429 basectx = ctx.p1()
430
430
431 style = web.config('web', 'style')
431 style = web.config('web', 'style')
432 if 'style' in req.form:
432 if 'style' in req.form:
433 style = req.form['style'][0]
433 style = req.form['style'][0]
434
434
435 diff = diffs(web, tmpl, ctx, basectx, None, style)
435 diff = diffs(web, tmpl, ctx, basectx, None, style)
436
436
437 parity = paritygen(web.stripecount)
437 parity = paritygen(web.stripecount)
438 diffstatsgen = diffstatgen(ctx, basectx)
438 diffstatsgen = diffstatgen(ctx, basectx)
439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
440
440
441 return dict(
441 return dict(
442 diff=diff,
442 diff=diff,
443 symrev=symrevorshortnode(req, ctx),
443 symrev=symrevorshortnode(req, ctx),
444 basenode=basectx.hex(),
444 basenode=basectx.hex(),
445 changesettag=showtags,
445 changesettag=showtags,
446 changesetbookmark=showbookmarks,
446 changesetbookmark=showbookmarks,
447 changesetbranch=showbranch,
447 changesetbranch=showbranch,
448 files=files,
448 files=files,
449 diffsummary=lambda **x: diffsummary(diffstatsgen),
449 diffsummary=lambda **x: diffsummary(diffstatsgen),
450 diffstat=diffstats,
450 diffstat=diffstats,
451 archives=web.archivelist(ctx.hex()),
451 archives=web.archivelist(ctx.hex()),
452 **commonentry(web.repo, ctx))
452 **pycompat.strkwargs(commonentry(web.repo, ctx)))
453
453
454 def listfilediffs(tmpl, files, node, max):
454 def listfilediffs(tmpl, files, node, max):
455 for f in files[:max]:
455 for f in files[:max]:
456 yield tmpl('filedifflink', node=hex(node), file=f)
456 yield tmpl('filedifflink', node=hex(node), file=f)
457 if len(files) > max:
457 if len(files) > max:
458 yield tmpl('fileellipses')
458 yield tmpl('fileellipses')
459
459
460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
461 lineidprefix=''):
461 lineidprefix=''):
462
462
463 def prettyprintlines(lines, blockno):
463 def prettyprintlines(lines, blockno):
464 for lineno, l in enumerate(lines, 1):
464 for lineno, l in enumerate(lines, 1):
465 difflineno = "%d.%d" % (blockno, lineno)
465 difflineno = "%d.%d" % (blockno, lineno)
466 if l.startswith('+'):
466 if l.startswith('+'):
467 ltype = "difflineplus"
467 ltype = "difflineplus"
468 elif l.startswith('-'):
468 elif l.startswith('-'):
469 ltype = "difflineminus"
469 ltype = "difflineminus"
470 elif l.startswith('@'):
470 elif l.startswith('@'):
471 ltype = "difflineat"
471 ltype = "difflineat"
472 else:
472 else:
473 ltype = "diffline"
473 ltype = "diffline"
474 yield tmpl(ltype,
474 yield tmpl(ltype,
475 line=l,
475 line=l,
476 lineno=lineno,
476 lineno=lineno,
477 lineid=lineidprefix + "l%s" % difflineno,
477 lineid=lineidprefix + "l%s" % difflineno,
478 linenumber="% 8s" % difflineno)
478 linenumber="% 8s" % difflineno)
479
479
480 repo = web.repo
480 repo = web.repo
481 if files:
481 if files:
482 m = match.exact(repo.root, repo.getcwd(), files)
482 m = match.exact(repo.root, repo.getcwd(), files)
483 else:
483 else:
484 m = match.always(repo.root, repo.getcwd())
484 m = match.always(repo.root, repo.getcwd())
485
485
486 diffopts = patch.diffopts(repo.ui, untrusted=True)
486 diffopts = patch.diffopts(repo.ui, untrusted=True)
487 node1 = basectx.node()
487 node1 = basectx.node()
488 node2 = ctx.node()
488 node2 = ctx.node()
489 parity = paritygen(web.stripecount)
489 parity = paritygen(web.stripecount)
490
490
491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
493 if style != 'raw':
493 if style != 'raw':
494 header = header[1:]
494 header = header[1:]
495 lines = [h + '\n' for h in header]
495 lines = [h + '\n' for h in header]
496 for hunkrange, hunklines in hunks:
496 for hunkrange, hunklines in hunks:
497 if linerange is not None and hunkrange is not None:
497 if linerange is not None and hunkrange is not None:
498 s1, l1, s2, l2 = hunkrange
498 s1, l1, s2, l2 = hunkrange
499 if not mdiff.hunkinrange((s2, l2), linerange):
499 if not mdiff.hunkinrange((s2, l2), linerange):
500 continue
500 continue
501 lines.extend(hunklines)
501 lines.extend(hunklines)
502 if lines:
502 if lines:
503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
504 lines=prettyprintlines(lines, blockno))
504 lines=prettyprintlines(lines, blockno))
505
505
506 def compare(tmpl, context, leftlines, rightlines):
506 def compare(tmpl, context, leftlines, rightlines):
507 '''Generator function that provides side-by-side comparison data.'''
507 '''Generator function that provides side-by-side comparison data.'''
508
508
509 def compline(type, leftlineno, leftline, rightlineno, rightline):
509 def compline(type, leftlineno, leftline, rightlineno, rightline):
510 lineid = leftlineno and ("l%s" % leftlineno) or ''
510 lineid = leftlineno and ("l%s" % leftlineno) or ''
511 lineid += rightlineno and ("r%s" % rightlineno) or ''
511 lineid += rightlineno and ("r%s" % rightlineno) or ''
512 return tmpl('comparisonline',
512 return tmpl('comparisonline',
513 type=type,
513 type=type,
514 lineid=lineid,
514 lineid=lineid,
515 leftlineno=leftlineno,
515 leftlineno=leftlineno,
516 leftlinenumber="% 6s" % (leftlineno or ''),
516 leftlinenumber="% 6s" % (leftlineno or ''),
517 leftline=leftline or '',
517 leftline=leftline or '',
518 rightlineno=rightlineno,
518 rightlineno=rightlineno,
519 rightlinenumber="% 6s" % (rightlineno or ''),
519 rightlinenumber="% 6s" % (rightlineno or ''),
520 rightline=rightline or '')
520 rightline=rightline or '')
521
521
522 def getblock(opcodes):
522 def getblock(opcodes):
523 for type, llo, lhi, rlo, rhi in opcodes:
523 for type, llo, lhi, rlo, rhi in opcodes:
524 len1 = lhi - llo
524 len1 = lhi - llo
525 len2 = rhi - rlo
525 len2 = rhi - rlo
526 count = min(len1, len2)
526 count = min(len1, len2)
527 for i in xrange(count):
527 for i in xrange(count):
528 yield compline(type=type,
528 yield compline(type=type,
529 leftlineno=llo + i + 1,
529 leftlineno=llo + i + 1,
530 leftline=leftlines[llo + i],
530 leftline=leftlines[llo + i],
531 rightlineno=rlo + i + 1,
531 rightlineno=rlo + i + 1,
532 rightline=rightlines[rlo + i])
532 rightline=rightlines[rlo + i])
533 if len1 > len2:
533 if len1 > len2:
534 for i in xrange(llo + count, lhi):
534 for i in xrange(llo + count, lhi):
535 yield compline(type=type,
535 yield compline(type=type,
536 leftlineno=i + 1,
536 leftlineno=i + 1,
537 leftline=leftlines[i],
537 leftline=leftlines[i],
538 rightlineno=None,
538 rightlineno=None,
539 rightline=None)
539 rightline=None)
540 elif len2 > len1:
540 elif len2 > len1:
541 for i in xrange(rlo + count, rhi):
541 for i in xrange(rlo + count, rhi):
542 yield compline(type=type,
542 yield compline(type=type,
543 leftlineno=None,
543 leftlineno=None,
544 leftline=None,
544 leftline=None,
545 rightlineno=i + 1,
545 rightlineno=i + 1,
546 rightline=rightlines[i])
546 rightline=rightlines[i])
547
547
548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
549 if context < 0:
549 if context < 0:
550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
551 else:
551 else:
552 for oc in s.get_grouped_opcodes(n=context):
552 for oc in s.get_grouped_opcodes(n=context):
553 yield tmpl('comparisonblock', lines=getblock(oc))
553 yield tmpl('comparisonblock', lines=getblock(oc))
554
554
555 def diffstatgen(ctx, basectx):
555 def diffstatgen(ctx, basectx):
556 '''Generator function that provides the diffstat data.'''
556 '''Generator function that provides the diffstat data.'''
557
557
558 stats = patch.diffstatdata(
558 stats = patch.diffstatdata(
559 util.iterlines(ctx.diff(basectx, noprefix=False)))
559 util.iterlines(ctx.diff(basectx, noprefix=False)))
560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
561 while True:
561 while True:
562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
563
563
564 def diffsummary(statgen):
564 def diffsummary(statgen):
565 '''Return a short summary of the diff.'''
565 '''Return a short summary of the diff.'''
566
566
567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
569 len(stats), addtotal, removetotal)
569 len(stats), addtotal, removetotal)
570
570
571 def diffstat(tmpl, ctx, statgen, parity):
571 def diffstat(tmpl, ctx, statgen, parity):
572 '''Return a diffstat template for each file in the diff.'''
572 '''Return a diffstat template for each file in the diff.'''
573
573
574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
575 files = ctx.files()
575 files = ctx.files()
576
576
577 def pct(i):
577 def pct(i):
578 if maxtotal == 0:
578 if maxtotal == 0:
579 return 0
579 return 0
580 return (float(i) / maxtotal) * 100
580 return (float(i) / maxtotal) * 100
581
581
582 fileno = 0
582 fileno = 0
583 for filename, adds, removes, isbinary in stats:
583 for filename, adds, removes, isbinary in stats:
584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
585 total = adds + removes
585 total = adds + removes
586 fileno += 1
586 fileno += 1
587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
588 total=total, addpct=pct(adds), removepct=pct(removes),
588 total=total, addpct=pct(adds), removepct=pct(removes),
589 parity=next(parity))
589 parity=next(parity))
590
590
591 class sessionvars(object):
591 class sessionvars(object):
592 def __init__(self, vars, start='?'):
592 def __init__(self, vars, start='?'):
593 self.start = start
593 self.start = start
594 self.vars = vars
594 self.vars = vars
595 def __getitem__(self, key):
595 def __getitem__(self, key):
596 return self.vars[key]
596 return self.vars[key]
597 def __setitem__(self, key, value):
597 def __setitem__(self, key, value):
598 self.vars[key] = value
598 self.vars[key] = value
599 def __copy__(self):
599 def __copy__(self):
600 return sessionvars(copy.copy(self.vars), self.start)
600 return sessionvars(copy.copy(self.vars), self.start)
601 def __iter__(self):
601 def __iter__(self):
602 separator = self.start
602 separator = self.start
603 for key, value in sorted(self.vars.iteritems()):
603 for key, value in sorted(self.vars.iteritems()):
604 yield {'name': key,
604 yield {'name': key,
605 'value': pycompat.bytestr(value),
605 'value': pycompat.bytestr(value),
606 'separator': separator,
606 'separator': separator,
607 }
607 }
608 separator = '&'
608 separator = '&'
609
609
610 class wsgiui(uimod.ui):
610 class wsgiui(uimod.ui):
611 # default termwidth breaks under mod_wsgi
611 # default termwidth breaks under mod_wsgi
612 def termwidth(self):
612 def termwidth(self):
613 return 80
613 return 80
614
614
615 def getwebsubs(repo):
615 def getwebsubs(repo):
616 websubtable = []
616 websubtable = []
617 websubdefs = repo.ui.configitems('websub')
617 websubdefs = repo.ui.configitems('websub')
618 # we must maintain interhg backwards compatibility
618 # we must maintain interhg backwards compatibility
619 websubdefs += repo.ui.configitems('interhg')
619 websubdefs += repo.ui.configitems('interhg')
620 for key, pattern in websubdefs:
620 for key, pattern in websubdefs:
621 # grab the delimiter from the character after the "s"
621 # grab the delimiter from the character after the "s"
622 unesc = pattern[1:2]
622 unesc = pattern[1:2]
623 delim = re.escape(unesc)
623 delim = re.escape(unesc)
624
624
625 # identify portions of the pattern, taking care to avoid escaped
625 # identify portions of the pattern, taking care to avoid escaped
626 # delimiters. the replace format and flags are optional, but
626 # delimiters. the replace format and flags are optional, but
627 # delimiters are required.
627 # delimiters are required.
628 match = re.match(
628 match = re.match(
629 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
629 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
630 % (delim, delim, delim), pattern)
630 % (delim, delim, delim), pattern)
631 if not match:
631 if not match:
632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
633 % (key, pattern))
633 % (key, pattern))
634 continue
634 continue
635
635
636 # we need to unescape the delimiter for regexp and format
636 # we need to unescape the delimiter for regexp and format
637 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
637 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
638 regexp = delim_re.sub(unesc, match.group(1))
638 regexp = delim_re.sub(unesc, match.group(1))
639 format = delim_re.sub(unesc, match.group(2))
639 format = delim_re.sub(unesc, match.group(2))
640
640
641 # the pattern allows for 6 regexp flags, so set them if necessary
641 # the pattern allows for 6 regexp flags, so set them if necessary
642 flagin = match.group(3)
642 flagin = match.group(3)
643 flags = 0
643 flags = 0
644 if flagin:
644 if flagin:
645 for flag in flagin.upper():
645 for flag in flagin.upper():
646 flags |= re.__dict__[flag]
646 flags |= re.__dict__[flag]
647
647
648 try:
648 try:
649 regexp = re.compile(regexp, flags)
649 regexp = re.compile(regexp, flags)
650 websubtable.append((regexp, format))
650 websubtable.append((regexp, format))
651 except re.error:
651 except re.error:
652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
653 % (key, regexp))
653 % (key, regexp))
654 return websubtable
654 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now