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