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