##// END OF EJS Templates
hgweb: wrap {instabilities} by hybridlist()...
Yuya Nishihara -
r37935:34f259a1 default
parent child Browse files
Show More
@@ -1,742 +1,743 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 _whyunstablegen(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):
405 def whyunstable(context, mapping):
406 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
406 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
407
407
408 whyunstable._requires = {'repo', 'ctx'}
408 whyunstable._requires = {'repo', 'ctx'}
409
409
410 def commonentry(repo, ctx):
410 def commonentry(repo, ctx):
411 node = ctx.node()
411 node = ctx.node()
412 return {
412 return {
413 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
413 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
414 # 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
415 # fctx.parents() != fctx.changectx.parents() for example.
415 # fctx.parents() != fctx.changectx.parents() for example.
416 'ctx': ctx,
416 'ctx': ctx,
417 'rev': ctx.rev(),
417 'rev': ctx.rev(),
418 'node': hex(node),
418 'node': hex(node),
419 'author': ctx.user(),
419 'author': ctx.user(),
420 'desc': ctx.description(),
420 'desc': ctx.description(),
421 'date': ctx.date(),
421 'date': ctx.date(),
422 'extra': ctx.extra(),
422 'extra': ctx.extra(),
423 'phase': ctx.phasestr(),
423 'phase': ctx.phasestr(),
424 'obsolete': ctx.obsolete(),
424 'obsolete': ctx.obsolete(),
425 'succsandmarkers': succsandmarkers,
425 'succsandmarkers': succsandmarkers,
426 'instabilities': [{"instability": i} for i in ctx.instabilities()],
426 'instabilities': templateutil.hybridlist(ctx.instabilities(),
427 name='instability'),
427 'whyunstable': whyunstable,
428 'whyunstable': whyunstable,
428 'branch': nodebranchnodefault(ctx),
429 'branch': nodebranchnodefault(ctx),
429 'inbranch': nodeinbranch(repo, ctx),
430 'inbranch': nodeinbranch(repo, ctx),
430 'branches': nodebranchdict(repo, ctx),
431 'branches': nodebranchdict(repo, ctx),
431 'tags': nodetagsdict(repo, node),
432 'tags': nodetagsdict(repo, node),
432 'bookmarks': nodebookmarksdict(repo, node),
433 'bookmarks': nodebookmarksdict(repo, node),
433 'parent': lambda **x: parents(ctx),
434 'parent': lambda **x: parents(ctx),
434 'child': lambda **x: children(ctx),
435 'child': lambda **x: children(ctx),
435 }
436 }
436
437
437 def changelistentry(web, ctx):
438 def changelistentry(web, ctx):
438 '''Obtain a dictionary to be used for entries in a changelist.
439 '''Obtain a dictionary to be used for entries in a changelist.
439
440
440 This function is called when producing items for the "entries" list passed
441 This function is called when producing items for the "entries" list passed
441 to the "shortlog" and "changelog" templates.
442 to the "shortlog" and "changelog" templates.
442 '''
443 '''
443 repo = web.repo
444 repo = web.repo
444 rev = ctx.rev()
445 rev = ctx.rev()
445 n = ctx.node()
446 n = ctx.node()
446 showtags = showtag(repo, 'changelogtag', n)
447 showtags = showtag(repo, 'changelogtag', n)
447 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
448 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
448
449
449 entry = commonentry(repo, ctx)
450 entry = commonentry(repo, ctx)
450 entry.update(
451 entry.update(
451 allparents=lambda **x: parents(ctx),
452 allparents=lambda **x: parents(ctx),
452 parent=lambda **x: parents(ctx, rev - 1),
453 parent=lambda **x: parents(ctx, rev - 1),
453 child=lambda **x: children(ctx, rev + 1),
454 child=lambda **x: children(ctx, rev + 1),
454 changelogtag=showtags,
455 changelogtag=showtags,
455 files=files,
456 files=files,
456 )
457 )
457 return entry
458 return entry
458
459
459 def symrevorshortnode(req, ctx):
460 def symrevorshortnode(req, ctx):
460 if 'node' in req.qsparams:
461 if 'node' in req.qsparams:
461 return templatefilters.revescape(req.qsparams['node'])
462 return templatefilters.revescape(req.qsparams['node'])
462 else:
463 else:
463 return short(ctx.node())
464 return short(ctx.node())
464
465
465 def changesetentry(web, ctx):
466 def changesetentry(web, ctx):
466 '''Obtain a dictionary to be used to render the "changeset" template.'''
467 '''Obtain a dictionary to be used to render the "changeset" template.'''
467
468
468 showtags = showtag(web.repo, 'changesettag', ctx.node())
469 showtags = showtag(web.repo, 'changesettag', ctx.node())
469 showbookmarks = showbookmark(web.repo, 'changesetbookmark', ctx.node())
470 showbookmarks = showbookmark(web.repo, 'changesetbookmark', ctx.node())
470 showbranch = nodebranchnodefault(ctx)
471 showbranch = nodebranchnodefault(ctx)
471
472
472 files = []
473 files = []
473 parity = paritygen(web.stripecount)
474 parity = paritygen(web.stripecount)
474 for blockno, f in enumerate(ctx.files()):
475 for blockno, f in enumerate(ctx.files()):
475 template = 'filenodelink' if f in ctx else 'filenolink'
476 template = 'filenodelink' if f in ctx else 'filenolink'
476 files.append(web.tmpl.generate(template, {
477 files.append(web.tmpl.generate(template, {
477 'node': ctx.hex(),
478 'node': ctx.hex(),
478 'file': f,
479 'file': f,
479 'blockno': blockno + 1,
480 'blockno': blockno + 1,
480 'parity': next(parity),
481 'parity': next(parity),
481 }))
482 }))
482
483
483 basectx = basechangectx(web.repo, web.req)
484 basectx = basechangectx(web.repo, web.req)
484 if basectx is None:
485 if basectx is None:
485 basectx = ctx.p1()
486 basectx = ctx.p1()
486
487
487 style = web.config('web', 'style')
488 style = web.config('web', 'style')
488 if 'style' in web.req.qsparams:
489 if 'style' in web.req.qsparams:
489 style = web.req.qsparams['style']
490 style = web.req.qsparams['style']
490
491
491 diff = diffs(web, ctx, basectx, None, style)
492 diff = diffs(web, ctx, basectx, None, style)
492
493
493 parity = paritygen(web.stripecount)
494 parity = paritygen(web.stripecount)
494 diffstatsgen = diffstatgen(ctx, basectx)
495 diffstatsgen = diffstatgen(ctx, basectx)
495 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
496 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
496
497
497 return dict(
498 return dict(
498 diff=diff,
499 diff=diff,
499 symrev=symrevorshortnode(web.req, ctx),
500 symrev=symrevorshortnode(web.req, ctx),
500 basenode=basectx.hex(),
501 basenode=basectx.hex(),
501 changesettag=showtags,
502 changesettag=showtags,
502 changesetbookmark=showbookmarks,
503 changesetbookmark=showbookmarks,
503 changesetbranch=showbranch,
504 changesetbranch=showbranch,
504 files=files,
505 files=files,
505 diffsummary=lambda **x: diffsummary(diffstatsgen),
506 diffsummary=lambda **x: diffsummary(diffstatsgen),
506 diffstat=diffstats,
507 diffstat=diffstats,
507 archives=web.archivelist(ctx.hex()),
508 archives=web.archivelist(ctx.hex()),
508 **pycompat.strkwargs(commonentry(web.repo, ctx)))
509 **pycompat.strkwargs(commonentry(web.repo, ctx)))
509
510
510 def listfilediffs(tmpl, files, node, max):
511 def listfilediffs(tmpl, files, node, max):
511 for f in files[:max]:
512 for f in files[:max]:
512 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
513 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
513 if len(files) > max:
514 if len(files) > max:
514 yield tmpl.generate('fileellipses', {})
515 yield tmpl.generate('fileellipses', {})
515
516
516 def diffs(web, ctx, basectx, files, style, linerange=None,
517 def diffs(web, ctx, basectx, files, style, linerange=None,
517 lineidprefix=''):
518 lineidprefix=''):
518
519
519 def prettyprintlines(lines, blockno):
520 def prettyprintlines(lines, blockno):
520 for lineno, l in enumerate(lines, 1):
521 for lineno, l in enumerate(lines, 1):
521 difflineno = "%d.%d" % (blockno, lineno)
522 difflineno = "%d.%d" % (blockno, lineno)
522 if l.startswith('+'):
523 if l.startswith('+'):
523 ltype = "difflineplus"
524 ltype = "difflineplus"
524 elif l.startswith('-'):
525 elif l.startswith('-'):
525 ltype = "difflineminus"
526 ltype = "difflineminus"
526 elif l.startswith('@'):
527 elif l.startswith('@'):
527 ltype = "difflineat"
528 ltype = "difflineat"
528 else:
529 else:
529 ltype = "diffline"
530 ltype = "diffline"
530 yield web.tmpl.generate(ltype, {
531 yield web.tmpl.generate(ltype, {
531 'line': l,
532 'line': l,
532 'lineno': lineno,
533 'lineno': lineno,
533 'lineid': lineidprefix + "l%s" % difflineno,
534 'lineid': lineidprefix + "l%s" % difflineno,
534 'linenumber': "% 8s" % difflineno,
535 'linenumber': "% 8s" % difflineno,
535 })
536 })
536
537
537 repo = web.repo
538 repo = web.repo
538 if files:
539 if files:
539 m = match.exact(repo.root, repo.getcwd(), files)
540 m = match.exact(repo.root, repo.getcwd(), files)
540 else:
541 else:
541 m = match.always(repo.root, repo.getcwd())
542 m = match.always(repo.root, repo.getcwd())
542
543
543 diffopts = patch.diffopts(repo.ui, untrusted=True)
544 diffopts = patch.diffopts(repo.ui, untrusted=True)
544 node1 = basectx.node()
545 node1 = basectx.node()
545 node2 = ctx.node()
546 node2 = ctx.node()
546 parity = paritygen(web.stripecount)
547 parity = paritygen(web.stripecount)
547
548
548 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
549 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
549 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
550 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
550 if style != 'raw':
551 if style != 'raw':
551 header = header[1:]
552 header = header[1:]
552 lines = [h + '\n' for h in header]
553 lines = [h + '\n' for h in header]
553 for hunkrange, hunklines in hunks:
554 for hunkrange, hunklines in hunks:
554 if linerange is not None and hunkrange is not None:
555 if linerange is not None and hunkrange is not None:
555 s1, l1, s2, l2 = hunkrange
556 s1, l1, s2, l2 = hunkrange
556 if not mdiff.hunkinrange((s2, l2), linerange):
557 if not mdiff.hunkinrange((s2, l2), linerange):
557 continue
558 continue
558 lines.extend(hunklines)
559 lines.extend(hunklines)
559 if lines:
560 if lines:
560 yield web.tmpl.generate('diffblock', {
561 yield web.tmpl.generate('diffblock', {
561 'parity': next(parity),
562 'parity': next(parity),
562 'blockno': blockno,
563 'blockno': blockno,
563 'lines': prettyprintlines(lines, blockno),
564 'lines': prettyprintlines(lines, blockno),
564 })
565 })
565
566
566 def compare(tmpl, context, leftlines, rightlines):
567 def compare(tmpl, context, leftlines, rightlines):
567 '''Generator function that provides side-by-side comparison data.'''
568 '''Generator function that provides side-by-side comparison data.'''
568
569
569 def compline(type, leftlineno, leftline, rightlineno, rightline):
570 def compline(type, leftlineno, leftline, rightlineno, rightline):
570 lineid = leftlineno and ("l%d" % leftlineno) or ''
571 lineid = leftlineno and ("l%d" % leftlineno) or ''
571 lineid += rightlineno and ("r%d" % rightlineno) or ''
572 lineid += rightlineno and ("r%d" % rightlineno) or ''
572 llno = '%d' % leftlineno if leftlineno else ''
573 llno = '%d' % leftlineno if leftlineno else ''
573 rlno = '%d' % rightlineno if rightlineno else ''
574 rlno = '%d' % rightlineno if rightlineno else ''
574 return tmpl.generate('comparisonline', {
575 return tmpl.generate('comparisonline', {
575 'type': type,
576 'type': type,
576 'lineid': lineid,
577 'lineid': lineid,
577 'leftlineno': leftlineno,
578 'leftlineno': leftlineno,
578 'leftlinenumber': "% 6s" % llno,
579 'leftlinenumber': "% 6s" % llno,
579 'leftline': leftline or '',
580 'leftline': leftline or '',
580 'rightlineno': rightlineno,
581 'rightlineno': rightlineno,
581 'rightlinenumber': "% 6s" % rlno,
582 'rightlinenumber': "% 6s" % rlno,
582 'rightline': rightline or '',
583 'rightline': rightline or '',
583 })
584 })
584
585
585 def getblock(opcodes):
586 def getblock(opcodes):
586 for type, llo, lhi, rlo, rhi in opcodes:
587 for type, llo, lhi, rlo, rhi in opcodes:
587 len1 = lhi - llo
588 len1 = lhi - llo
588 len2 = rhi - rlo
589 len2 = rhi - rlo
589 count = min(len1, len2)
590 count = min(len1, len2)
590 for i in xrange(count):
591 for i in xrange(count):
591 yield compline(type=type,
592 yield compline(type=type,
592 leftlineno=llo + i + 1,
593 leftlineno=llo + i + 1,
593 leftline=leftlines[llo + i],
594 leftline=leftlines[llo + i],
594 rightlineno=rlo + i + 1,
595 rightlineno=rlo + i + 1,
595 rightline=rightlines[rlo + i])
596 rightline=rightlines[rlo + i])
596 if len1 > len2:
597 if len1 > len2:
597 for i in xrange(llo + count, lhi):
598 for i in xrange(llo + count, lhi):
598 yield compline(type=type,
599 yield compline(type=type,
599 leftlineno=i + 1,
600 leftlineno=i + 1,
600 leftline=leftlines[i],
601 leftline=leftlines[i],
601 rightlineno=None,
602 rightlineno=None,
602 rightline=None)
603 rightline=None)
603 elif len2 > len1:
604 elif len2 > len1:
604 for i in xrange(rlo + count, rhi):
605 for i in xrange(rlo + count, rhi):
605 yield compline(type=type,
606 yield compline(type=type,
606 leftlineno=None,
607 leftlineno=None,
607 leftline=None,
608 leftline=None,
608 rightlineno=i + 1,
609 rightlineno=i + 1,
609 rightline=rightlines[i])
610 rightline=rightlines[i])
610
611
611 s = difflib.SequenceMatcher(None, leftlines, rightlines)
612 s = difflib.SequenceMatcher(None, leftlines, rightlines)
612 if context < 0:
613 if context < 0:
613 yield tmpl.generate('comparisonblock',
614 yield tmpl.generate('comparisonblock',
614 {'lines': getblock(s.get_opcodes())})
615 {'lines': getblock(s.get_opcodes())})
615 else:
616 else:
616 for oc in s.get_grouped_opcodes(n=context):
617 for oc in s.get_grouped_opcodes(n=context):
617 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
618 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
618
619
619 def diffstatgen(ctx, basectx):
620 def diffstatgen(ctx, basectx):
620 '''Generator function that provides the diffstat data.'''
621 '''Generator function that provides the diffstat data.'''
621
622
622 stats = patch.diffstatdata(
623 stats = patch.diffstatdata(
623 util.iterlines(ctx.diff(basectx, noprefix=False)))
624 util.iterlines(ctx.diff(basectx, noprefix=False)))
624 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
625 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
625 while True:
626 while True:
626 yield stats, maxname, maxtotal, addtotal, removetotal, binary
627 yield stats, maxname, maxtotal, addtotal, removetotal, binary
627
628
628 def diffsummary(statgen):
629 def diffsummary(statgen):
629 '''Return a short summary of the diff.'''
630 '''Return a short summary of the diff.'''
630
631
631 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
632 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
632 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
633 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
633 len(stats), addtotal, removetotal)
634 len(stats), addtotal, removetotal)
634
635
635 def diffstat(tmpl, ctx, statgen, parity):
636 def diffstat(tmpl, ctx, statgen, parity):
636 '''Return a diffstat template for each file in the diff.'''
637 '''Return a diffstat template for each file in the diff.'''
637
638
638 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
639 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
639 files = ctx.files()
640 files = ctx.files()
640
641
641 def pct(i):
642 def pct(i):
642 if maxtotal == 0:
643 if maxtotal == 0:
643 return 0
644 return 0
644 return (float(i) / maxtotal) * 100
645 return (float(i) / maxtotal) * 100
645
646
646 fileno = 0
647 fileno = 0
647 for filename, adds, removes, isbinary in stats:
648 for filename, adds, removes, isbinary in stats:
648 template = 'diffstatlink' if filename in files else 'diffstatnolink'
649 template = 'diffstatlink' if filename in files else 'diffstatnolink'
649 total = adds + removes
650 total = adds + removes
650 fileno += 1
651 fileno += 1
651 yield tmpl.generate(template, {
652 yield tmpl.generate(template, {
652 'node': ctx.hex(),
653 'node': ctx.hex(),
653 'file': filename,
654 'file': filename,
654 'fileno': fileno,
655 'fileno': fileno,
655 'total': total,
656 'total': total,
656 'addpct': pct(adds),
657 'addpct': pct(adds),
657 'removepct': pct(removes),
658 'removepct': pct(removes),
658 'parity': next(parity),
659 'parity': next(parity),
659 })
660 })
660
661
661 class sessionvars(templateutil.wrapped):
662 class sessionvars(templateutil.wrapped):
662 def __init__(self, vars, start='?'):
663 def __init__(self, vars, start='?'):
663 self._start = start
664 self._start = start
664 self._vars = vars
665 self._vars = vars
665
666
666 def __getitem__(self, key):
667 def __getitem__(self, key):
667 return self._vars[key]
668 return self._vars[key]
668
669
669 def __setitem__(self, key, value):
670 def __setitem__(self, key, value):
670 self._vars[key] = value
671 self._vars[key] = value
671
672
672 def __copy__(self):
673 def __copy__(self):
673 return sessionvars(copy.copy(self._vars), self._start)
674 return sessionvars(copy.copy(self._vars), self._start)
674
675
675 def itermaps(self, context):
676 def itermaps(self, context):
676 separator = self._start
677 separator = self._start
677 for key, value in sorted(self._vars.iteritems()):
678 for key, value in sorted(self._vars.iteritems()):
678 yield {'name': key,
679 yield {'name': key,
679 'value': pycompat.bytestr(value),
680 'value': pycompat.bytestr(value),
680 'separator': separator,
681 'separator': separator,
681 }
682 }
682 separator = '&'
683 separator = '&'
683
684
684 def join(self, context, mapping, sep):
685 def join(self, context, mapping, sep):
685 # could be '{separator}{name}={value|urlescape}'
686 # could be '{separator}{name}={value|urlescape}'
686 raise error.ParseError(_('not displayable without template'))
687 raise error.ParseError(_('not displayable without template'))
687
688
688 def show(self, context, mapping):
689 def show(self, context, mapping):
689 return self.join(context, '')
690 return self.join(context, '')
690
691
691 def tovalue(self, context, mapping):
692 def tovalue(self, context, mapping):
692 return self._vars
693 return self._vars
693
694
694 class wsgiui(uimod.ui):
695 class wsgiui(uimod.ui):
695 # default termwidth breaks under mod_wsgi
696 # default termwidth breaks under mod_wsgi
696 def termwidth(self):
697 def termwidth(self):
697 return 80
698 return 80
698
699
699 def getwebsubs(repo):
700 def getwebsubs(repo):
700 websubtable = []
701 websubtable = []
701 websubdefs = repo.ui.configitems('websub')
702 websubdefs = repo.ui.configitems('websub')
702 # we must maintain interhg backwards compatibility
703 # we must maintain interhg backwards compatibility
703 websubdefs += repo.ui.configitems('interhg')
704 websubdefs += repo.ui.configitems('interhg')
704 for key, pattern in websubdefs:
705 for key, pattern in websubdefs:
705 # grab the delimiter from the character after the "s"
706 # grab the delimiter from the character after the "s"
706 unesc = pattern[1:2]
707 unesc = pattern[1:2]
707 delim = re.escape(unesc)
708 delim = re.escape(unesc)
708
709
709 # identify portions of the pattern, taking care to avoid escaped
710 # identify portions of the pattern, taking care to avoid escaped
710 # delimiters. the replace format and flags are optional, but
711 # delimiters. the replace format and flags are optional, but
711 # delimiters are required.
712 # delimiters are required.
712 match = re.match(
713 match = re.match(
713 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
714 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
714 % (delim, delim, delim), pattern)
715 % (delim, delim, delim), pattern)
715 if not match:
716 if not match:
716 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
717 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
717 % (key, pattern))
718 % (key, pattern))
718 continue
719 continue
719
720
720 # we need to unescape the delimiter for regexp and format
721 # we need to unescape the delimiter for regexp and format
721 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
722 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
722 regexp = delim_re.sub(unesc, match.group(1))
723 regexp = delim_re.sub(unesc, match.group(1))
723 format = delim_re.sub(unesc, match.group(2))
724 format = delim_re.sub(unesc, match.group(2))
724
725
725 # the pattern allows for 6 regexp flags, so set them if necessary
726 # the pattern allows for 6 regexp flags, so set them if necessary
726 flagin = match.group(3)
727 flagin = match.group(3)
727 flags = 0
728 flags = 0
728 if flagin:
729 if flagin:
729 for flag in flagin.upper():
730 for flag in flagin.upper():
730 flags |= re.__dict__[flag]
731 flags |= re.__dict__[flag]
731
732
732 try:
733 try:
733 regexp = re.compile(regexp, flags)
734 regexp = re.compile(regexp, flags)
734 websubtable.append((regexp, format))
735 websubtable.append((regexp, format))
735 except re.error:
736 except re.error:
736 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
737 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
737 % (key, regexp))
738 % (key, regexp))
738 return websubtable
739 return websubtable
739
740
740 def getgraphnode(repo, ctx):
741 def getgraphnode(repo, ctx):
741 return (templatekw.getgraphnodecurrent(repo, ctx) +
742 return (templatekw.getgraphnodecurrent(repo, ctx) +
742 templatekw.getgraphnodesymbol(ctx))
743 templatekw.getgraphnodesymbol(ctx))
General Comments 0
You need to be logged in to leave comments. Login now