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