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