##// END OF EJS Templates
templates: introduce revescape filter for escaping symbolic revisions...
av6 -
r25778:3a334127 default
parent child Browse files
Show More
@@ -1,510 +1,511 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 import os, copy, urllib
9 import os, copy
10 from mercurial import match, patch, error, ui, util, pathutil, context
10 from mercurial import match, patch, error, ui, util, pathutil, context
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import hex, nullid, short
12 from mercurial.node import hex, nullid, short
13 from mercurial.templatefilters import revescape
13 from common import ErrorResponse, paritygen
14 from common import ErrorResponse, paritygen
14 from common import HTTP_NOT_FOUND
15 from common import HTTP_NOT_FOUND
15 import difflib
16 import difflib
16
17
17 def up(p):
18 def up(p):
18 if p[0] != "/":
19 if p[0] != "/":
19 p = "/" + p
20 p = "/" + p
20 if p[-1] == "/":
21 if p[-1] == "/":
21 p = p[:-1]
22 p = p[:-1]
22 up = os.path.dirname(p)
23 up = os.path.dirname(p)
23 if up == "/":
24 if up == "/":
24 return "/"
25 return "/"
25 return up + "/"
26 return up + "/"
26
27
27 def _navseq(step, firststep=None):
28 def _navseq(step, firststep=None):
28 if firststep:
29 if firststep:
29 yield firststep
30 yield firststep
30 if firststep >= 20 and firststep <= 40:
31 if firststep >= 20 and firststep <= 40:
31 firststep = 50
32 firststep = 50
32 yield firststep
33 yield firststep
33 assert step > 0
34 assert step > 0
34 assert firststep > 0
35 assert firststep > 0
35 while step <= firststep:
36 while step <= firststep:
36 step *= 10
37 step *= 10
37 while True:
38 while True:
38 yield 1 * step
39 yield 1 * step
39 yield 3 * step
40 yield 3 * step
40 step *= 10
41 step *= 10
41
42
42 class revnav(object):
43 class revnav(object):
43
44
44 def __init__(self, repo):
45 def __init__(self, repo):
45 """Navigation generation object
46 """Navigation generation object
46
47
47 :repo: repo object we generate nav for
48 :repo: repo object we generate nav for
48 """
49 """
49 # used for hex generation
50 # used for hex generation
50 self._revlog = repo.changelog
51 self._revlog = repo.changelog
51
52
52 def __nonzero__(self):
53 def __nonzero__(self):
53 """return True if any revision to navigate over"""
54 """return True if any revision to navigate over"""
54 return self._first() is not None
55 return self._first() is not None
55
56
56 def _first(self):
57 def _first(self):
57 """return the minimum non-filtered changeset or None"""
58 """return the minimum non-filtered changeset or None"""
58 try:
59 try:
59 return iter(self._revlog).next()
60 return iter(self._revlog).next()
60 except StopIteration:
61 except StopIteration:
61 return None
62 return None
62
63
63 def hex(self, rev):
64 def hex(self, rev):
64 return hex(self._revlog.node(rev))
65 return hex(self._revlog.node(rev))
65
66
66 def gen(self, pos, pagelen, limit):
67 def gen(self, pos, pagelen, limit):
67 """computes label and revision id for navigation link
68 """computes label and revision id for navigation link
68
69
69 :pos: is the revision relative to which we generate navigation.
70 :pos: is the revision relative to which we generate navigation.
70 :pagelen: the size of each navigation page
71 :pagelen: the size of each navigation page
71 :limit: how far shall we link
72 :limit: how far shall we link
72
73
73 The return is:
74 The return is:
74 - a single element tuple
75 - a single element tuple
75 - containing a dictionary with a `before` and `after` key
76 - containing a dictionary with a `before` and `after` key
76 - values are generator functions taking arbitrary number of kwargs
77 - values are generator functions taking arbitrary number of kwargs
77 - yield items are dictionaries with `label` and `node` keys
78 - yield items are dictionaries with `label` and `node` keys
78 """
79 """
79 if not self:
80 if not self:
80 # empty repo
81 # empty repo
81 return ({'before': (), 'after': ()},)
82 return ({'before': (), 'after': ()},)
82
83
83 targets = []
84 targets = []
84 for f in _navseq(1, pagelen):
85 for f in _navseq(1, pagelen):
85 if f > limit:
86 if f > limit:
86 break
87 break
87 targets.append(pos + f)
88 targets.append(pos + f)
88 targets.append(pos - f)
89 targets.append(pos - f)
89 targets.sort()
90 targets.sort()
90
91
91 first = self._first()
92 first = self._first()
92 navbefore = [("(%i)" % first, self.hex(first))]
93 navbefore = [("(%i)" % first, self.hex(first))]
93 navafter = []
94 navafter = []
94 for rev in targets:
95 for rev in targets:
95 if rev not in self._revlog:
96 if rev not in self._revlog:
96 continue
97 continue
97 if pos < rev < limit:
98 if pos < rev < limit:
98 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
99 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
99 if 0 < rev < pos:
100 if 0 < rev < pos:
100 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
101 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
101
102
102
103
103 navafter.append(("tip", "tip"))
104 navafter.append(("tip", "tip"))
104
105
105 data = lambda i: {"label": i[0], "node": i[1]}
106 data = lambda i: {"label": i[0], "node": i[1]}
106 return ({'before': lambda **map: (data(i) for i in navbefore),
107 return ({'before': lambda **map: (data(i) for i in navbefore),
107 'after': lambda **map: (data(i) for i in navafter)},)
108 'after': lambda **map: (data(i) for i in navafter)},)
108
109
109 class filerevnav(revnav):
110 class filerevnav(revnav):
110
111
111 def __init__(self, repo, path):
112 def __init__(self, repo, path):
112 """Navigation generation object
113 """Navigation generation object
113
114
114 :repo: repo object we generate nav for
115 :repo: repo object we generate nav for
115 :path: path of the file we generate nav for
116 :path: path of the file we generate nav for
116 """
117 """
117 # used for iteration
118 # used for iteration
118 self._changelog = repo.unfiltered().changelog
119 self._changelog = repo.unfiltered().changelog
119 # used for hex generation
120 # used for hex generation
120 self._revlog = repo.file(path)
121 self._revlog = repo.file(path)
121
122
122 def hex(self, rev):
123 def hex(self, rev):
123 return hex(self._changelog.node(self._revlog.linkrev(rev)))
124 return hex(self._changelog.node(self._revlog.linkrev(rev)))
124
125
125
126
126 def _siblings(siblings=[], hiderev=None):
127 def _siblings(siblings=[], hiderev=None):
127 siblings = [s for s in siblings if s.node() != nullid]
128 siblings = [s for s in siblings if s.node() != nullid]
128 if len(siblings) == 1 and siblings[0].rev() == hiderev:
129 if len(siblings) == 1 and siblings[0].rev() == hiderev:
129 return
130 return
130 for s in siblings:
131 for s in siblings:
131 d = {'node': s.hex(), 'rev': s.rev()}
132 d = {'node': s.hex(), 'rev': s.rev()}
132 d['user'] = s.user()
133 d['user'] = s.user()
133 d['date'] = s.date()
134 d['date'] = s.date()
134 d['description'] = s.description()
135 d['description'] = s.description()
135 d['branch'] = s.branch()
136 d['branch'] = s.branch()
136 if util.safehasattr(s, 'path'):
137 if util.safehasattr(s, 'path'):
137 d['file'] = s.path()
138 d['file'] = s.path()
138 yield d
139 yield d
139
140
140 def parents(ctx, hide=None):
141 def parents(ctx, hide=None):
141 if isinstance(ctx, context.basefilectx):
142 if isinstance(ctx, context.basefilectx):
142 introrev = ctx.introrev()
143 introrev = ctx.introrev()
143 if ctx.changectx().rev() != introrev:
144 if ctx.changectx().rev() != introrev:
144 return _siblings([ctx.repo()[introrev]], hide)
145 return _siblings([ctx.repo()[introrev]], hide)
145 return _siblings(ctx.parents(), hide)
146 return _siblings(ctx.parents(), hide)
146
147
147 def children(ctx, hide=None):
148 def children(ctx, hide=None):
148 return _siblings(ctx.children(), hide)
149 return _siblings(ctx.children(), hide)
149
150
150 def renamelink(fctx):
151 def renamelink(fctx):
151 r = fctx.renamed()
152 r = fctx.renamed()
152 if r:
153 if r:
153 return [{'file': r[0], 'node': hex(r[1])}]
154 return [{'file': r[0], 'node': hex(r[1])}]
154 return []
155 return []
155
156
156 def nodetagsdict(repo, node):
157 def nodetagsdict(repo, node):
157 return [{"name": i} for i in repo.nodetags(node)]
158 return [{"name": i} for i in repo.nodetags(node)]
158
159
159 def nodebookmarksdict(repo, node):
160 def nodebookmarksdict(repo, node):
160 return [{"name": i} for i in repo.nodebookmarks(node)]
161 return [{"name": i} for i in repo.nodebookmarks(node)]
161
162
162 def nodebranchdict(repo, ctx):
163 def nodebranchdict(repo, ctx):
163 branches = []
164 branches = []
164 branch = ctx.branch()
165 branch = ctx.branch()
165 # If this is an empty repo, ctx.node() == nullid,
166 # If this is an empty repo, ctx.node() == nullid,
166 # ctx.branch() == 'default'.
167 # ctx.branch() == 'default'.
167 try:
168 try:
168 branchnode = repo.branchtip(branch)
169 branchnode = repo.branchtip(branch)
169 except error.RepoLookupError:
170 except error.RepoLookupError:
170 branchnode = None
171 branchnode = None
171 if branchnode == ctx.node():
172 if branchnode == ctx.node():
172 branches.append({"name": branch})
173 branches.append({"name": branch})
173 return branches
174 return branches
174
175
175 def nodeinbranch(repo, ctx):
176 def nodeinbranch(repo, ctx):
176 branches = []
177 branches = []
177 branch = ctx.branch()
178 branch = ctx.branch()
178 try:
179 try:
179 branchnode = repo.branchtip(branch)
180 branchnode = repo.branchtip(branch)
180 except error.RepoLookupError:
181 except error.RepoLookupError:
181 branchnode = None
182 branchnode = None
182 if branch != 'default' and branchnode != ctx.node():
183 if branch != 'default' and branchnode != ctx.node():
183 branches.append({"name": branch})
184 branches.append({"name": branch})
184 return branches
185 return branches
185
186
186 def nodebranchnodefault(ctx):
187 def nodebranchnodefault(ctx):
187 branches = []
188 branches = []
188 branch = ctx.branch()
189 branch = ctx.branch()
189 if branch != 'default':
190 if branch != 'default':
190 branches.append({"name": branch})
191 branches.append({"name": branch})
191 return branches
192 return branches
192
193
193 def showtag(repo, tmpl, t1, node=nullid, **args):
194 def showtag(repo, tmpl, t1, node=nullid, **args):
194 for t in repo.nodetags(node):
195 for t in repo.nodetags(node):
195 yield tmpl(t1, tag=t, **args)
196 yield tmpl(t1, tag=t, **args)
196
197
197 def showbookmark(repo, tmpl, t1, node=nullid, **args):
198 def showbookmark(repo, tmpl, t1, node=nullid, **args):
198 for t in repo.nodebookmarks(node):
199 for t in repo.nodebookmarks(node):
199 yield tmpl(t1, bookmark=t, **args)
200 yield tmpl(t1, bookmark=t, **args)
200
201
201 def cleanpath(repo, path):
202 def cleanpath(repo, path):
202 path = path.lstrip('/')
203 path = path.lstrip('/')
203 return pathutil.canonpath(repo.root, '', path)
204 return pathutil.canonpath(repo.root, '', path)
204
205
205 def changeidctx (repo, changeid):
206 def changeidctx (repo, changeid):
206 try:
207 try:
207 ctx = repo[changeid]
208 ctx = repo[changeid]
208 except error.RepoError:
209 except error.RepoError:
209 man = repo.manifest
210 man = repo.manifest
210 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
211 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
211
212
212 return ctx
213 return ctx
213
214
214 def changectx (repo, req):
215 def changectx (repo, req):
215 changeid = "tip"
216 changeid = "tip"
216 if 'node' in req.form:
217 if 'node' in req.form:
217 changeid = req.form['node'][0]
218 changeid = req.form['node'][0]
218 ipos=changeid.find(':')
219 ipos=changeid.find(':')
219 if ipos != -1:
220 if ipos != -1:
220 changeid = changeid[(ipos + 1):]
221 changeid = changeid[(ipos + 1):]
221 elif 'manifest' in req.form:
222 elif 'manifest' in req.form:
222 changeid = req.form['manifest'][0]
223 changeid = req.form['manifest'][0]
223
224
224 return changeidctx(repo, changeid)
225 return changeidctx(repo, changeid)
225
226
226 def basechangectx(repo, req):
227 def basechangectx(repo, req):
227 if 'node' in req.form:
228 if 'node' in req.form:
228 changeid = req.form['node'][0]
229 changeid = req.form['node'][0]
229 ipos=changeid.find(':')
230 ipos=changeid.find(':')
230 if ipos != -1:
231 if ipos != -1:
231 changeid = changeid[:ipos]
232 changeid = changeid[:ipos]
232 return changeidctx(repo, changeid)
233 return changeidctx(repo, changeid)
233
234
234 return None
235 return None
235
236
236 def filectx(repo, req):
237 def filectx(repo, req):
237 if 'file' not in req.form:
238 if 'file' not in req.form:
238 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
239 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
239 path = cleanpath(repo, req.form['file'][0])
240 path = cleanpath(repo, req.form['file'][0])
240 if 'node' in req.form:
241 if 'node' in req.form:
241 changeid = req.form['node'][0]
242 changeid = req.form['node'][0]
242 elif 'filenode' in req.form:
243 elif 'filenode' in req.form:
243 changeid = req.form['filenode'][0]
244 changeid = req.form['filenode'][0]
244 else:
245 else:
245 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
246 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
246 try:
247 try:
247 fctx = repo[changeid][path]
248 fctx = repo[changeid][path]
248 except error.RepoError:
249 except error.RepoError:
249 fctx = repo.filectx(path, fileid=changeid)
250 fctx = repo.filectx(path, fileid=changeid)
250
251
251 return fctx
252 return fctx
252
253
253 def changelistentry(web, ctx, tmpl):
254 def changelistentry(web, ctx, tmpl):
254 '''Obtain a dictionary to be used for entries in a changelist.
255 '''Obtain a dictionary to be used for entries in a changelist.
255
256
256 This function is called when producing items for the "entries" list passed
257 This function is called when producing items for the "entries" list passed
257 to the "shortlog" and "changelog" templates.
258 to the "shortlog" and "changelog" templates.
258 '''
259 '''
259 repo = web.repo
260 repo = web.repo
260 rev = ctx.rev()
261 rev = ctx.rev()
261 n = ctx.node()
262 n = ctx.node()
262 showtags = showtag(repo, tmpl, 'changelogtag', n)
263 showtags = showtag(repo, tmpl, 'changelogtag', n)
263 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
264 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
264
265
265 return {
266 return {
266 "author": ctx.user(),
267 "author": ctx.user(),
267 "parent": parents(ctx, rev - 1),
268 "parent": parents(ctx, rev - 1),
268 "child": children(ctx, rev + 1),
269 "child": children(ctx, rev + 1),
269 "changelogtag": showtags,
270 "changelogtag": showtags,
270 "desc": ctx.description(),
271 "desc": ctx.description(),
271 "extra": ctx.extra(),
272 "extra": ctx.extra(),
272 "date": ctx.date(),
273 "date": ctx.date(),
273 "files": files,
274 "files": files,
274 "rev": rev,
275 "rev": rev,
275 "node": hex(n),
276 "node": hex(n),
276 "tags": nodetagsdict(repo, n),
277 "tags": nodetagsdict(repo, n),
277 "bookmarks": nodebookmarksdict(repo, n),
278 "bookmarks": nodebookmarksdict(repo, n),
278 "inbranch": nodeinbranch(repo, ctx),
279 "inbranch": nodeinbranch(repo, ctx),
279 "branches": nodebranchdict(repo, ctx)
280 "branches": nodebranchdict(repo, ctx)
280 }
281 }
281
282
282 def symrevorshortnode(req, ctx):
283 def symrevorshortnode(req, ctx):
283 if 'node' in req.form:
284 if 'node' in req.form:
284 return urllib.quote(req.form['node'][0])
285 return revescape(req.form['node'][0])
285 else:
286 else:
286 return short(ctx.node())
287 return short(ctx.node())
287
288
288 def changesetentry(web, req, tmpl, ctx):
289 def changesetentry(web, req, tmpl, ctx):
289 '''Obtain a dictionary to be used to render the "changeset" template.'''
290 '''Obtain a dictionary to be used to render the "changeset" template.'''
290
291
291 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
292 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
292 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
293 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
293 ctx.node())
294 ctx.node())
294 showbranch = nodebranchnodefault(ctx)
295 showbranch = nodebranchnodefault(ctx)
295
296
296 files = []
297 files = []
297 parity = paritygen(web.stripecount)
298 parity = paritygen(web.stripecount)
298 for blockno, f in enumerate(ctx.files()):
299 for blockno, f in enumerate(ctx.files()):
299 template = f in ctx and 'filenodelink' or 'filenolink'
300 template = f in ctx and 'filenodelink' or 'filenolink'
300 files.append(tmpl(template,
301 files.append(tmpl(template,
301 node=ctx.hex(), file=f, blockno=blockno + 1,
302 node=ctx.hex(), file=f, blockno=blockno + 1,
302 parity=parity.next()))
303 parity=parity.next()))
303
304
304 basectx = basechangectx(web.repo, req)
305 basectx = basechangectx(web.repo, req)
305 if basectx is None:
306 if basectx is None:
306 basectx = ctx.p1()
307 basectx = ctx.p1()
307
308
308 style = web.config('web', 'style', 'paper')
309 style = web.config('web', 'style', 'paper')
309 if 'style' in req.form:
310 if 'style' in req.form:
310 style = req.form['style'][0]
311 style = req.form['style'][0]
311
312
312 parity = paritygen(web.stripecount)
313 parity = paritygen(web.stripecount)
313 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
314 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
314
315
315 parity = paritygen(web.stripecount)
316 parity = paritygen(web.stripecount)
316 diffstatsgen = diffstatgen(ctx, basectx)
317 diffstatsgen = diffstatgen(ctx, basectx)
317 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
318 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
318
319
319 return dict(
320 return dict(
320 diff=diff,
321 diff=diff,
321 rev=ctx.rev(),
322 rev=ctx.rev(),
322 node=ctx.hex(),
323 node=ctx.hex(),
323 symrev=symrevorshortnode(req, ctx),
324 symrev=symrevorshortnode(req, ctx),
324 parent=tuple(parents(ctx)),
325 parent=tuple(parents(ctx)),
325 child=children(ctx),
326 child=children(ctx),
326 basenode=basectx.hex(),
327 basenode=basectx.hex(),
327 changesettag=showtags,
328 changesettag=showtags,
328 changesetbookmark=showbookmarks,
329 changesetbookmark=showbookmarks,
329 changesetbranch=showbranch,
330 changesetbranch=showbranch,
330 author=ctx.user(),
331 author=ctx.user(),
331 desc=ctx.description(),
332 desc=ctx.description(),
332 extra=ctx.extra(),
333 extra=ctx.extra(),
333 date=ctx.date(),
334 date=ctx.date(),
334 phase=ctx.phasestr(),
335 phase=ctx.phasestr(),
335 files=files,
336 files=files,
336 diffsummary=lambda **x: diffsummary(diffstatsgen),
337 diffsummary=lambda **x: diffsummary(diffstatsgen),
337 diffstat=diffstats,
338 diffstat=diffstats,
338 archives=web.archivelist(ctx.hex()),
339 archives=web.archivelist(ctx.hex()),
339 tags=nodetagsdict(web.repo, ctx.node()),
340 tags=nodetagsdict(web.repo, ctx.node()),
340 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
341 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
341 branch=showbranch,
342 branch=showbranch,
342 inbranch=nodeinbranch(web.repo, ctx),
343 inbranch=nodeinbranch(web.repo, ctx),
343 branches=nodebranchdict(web.repo, ctx))
344 branches=nodebranchdict(web.repo, ctx))
344
345
345 def listfilediffs(tmpl, files, node, max):
346 def listfilediffs(tmpl, files, node, max):
346 for f in files[:max]:
347 for f in files[:max]:
347 yield tmpl('filedifflink', node=hex(node), file=f)
348 yield tmpl('filedifflink', node=hex(node), file=f)
348 if len(files) > max:
349 if len(files) > max:
349 yield tmpl('fileellipses')
350 yield tmpl('fileellipses')
350
351
351 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
352 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
352
353
353 def countgen():
354 def countgen():
354 start = 1
355 start = 1
355 while True:
356 while True:
356 yield start
357 yield start
357 start += 1
358 start += 1
358
359
359 blockcount = countgen()
360 blockcount = countgen()
360 def prettyprintlines(diff, blockno):
361 def prettyprintlines(diff, blockno):
361 for lineno, l in enumerate(diff.splitlines(True)):
362 for lineno, l in enumerate(diff.splitlines(True)):
362 difflineno = "%d.%d" % (blockno, lineno + 1)
363 difflineno = "%d.%d" % (blockno, lineno + 1)
363 if l.startswith('+'):
364 if l.startswith('+'):
364 ltype = "difflineplus"
365 ltype = "difflineplus"
365 elif l.startswith('-'):
366 elif l.startswith('-'):
366 ltype = "difflineminus"
367 ltype = "difflineminus"
367 elif l.startswith('@'):
368 elif l.startswith('@'):
368 ltype = "difflineat"
369 ltype = "difflineat"
369 else:
370 else:
370 ltype = "diffline"
371 ltype = "diffline"
371 yield tmpl(ltype,
372 yield tmpl(ltype,
372 line=l,
373 line=l,
373 lineno=lineno + 1,
374 lineno=lineno + 1,
374 lineid="l%s" % difflineno,
375 lineid="l%s" % difflineno,
375 linenumber="% 8s" % difflineno)
376 linenumber="% 8s" % difflineno)
376
377
377 if files:
378 if files:
378 m = match.exact(repo.root, repo.getcwd(), files)
379 m = match.exact(repo.root, repo.getcwd(), files)
379 else:
380 else:
380 m = match.always(repo.root, repo.getcwd())
381 m = match.always(repo.root, repo.getcwd())
381
382
382 diffopts = patch.diffopts(repo.ui, untrusted=True)
383 diffopts = patch.diffopts(repo.ui, untrusted=True)
383 if basectx is None:
384 if basectx is None:
384 parents = ctx.parents()
385 parents = ctx.parents()
385 if parents:
386 if parents:
386 node1 = parents[0].node()
387 node1 = parents[0].node()
387 else:
388 else:
388 node1 = nullid
389 node1 = nullid
389 else:
390 else:
390 node1 = basectx.node()
391 node1 = basectx.node()
391 node2 = ctx.node()
392 node2 = ctx.node()
392
393
393 block = []
394 block = []
394 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
395 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
395 if chunk.startswith('diff') and block:
396 if chunk.startswith('diff') and block:
396 blockno = blockcount.next()
397 blockno = blockcount.next()
397 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
398 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
398 lines=prettyprintlines(''.join(block), blockno))
399 lines=prettyprintlines(''.join(block), blockno))
399 block = []
400 block = []
400 if chunk.startswith('diff') and style != 'raw':
401 if chunk.startswith('diff') and style != 'raw':
401 chunk = ''.join(chunk.splitlines(True)[1:])
402 chunk = ''.join(chunk.splitlines(True)[1:])
402 block.append(chunk)
403 block.append(chunk)
403 blockno = blockcount.next()
404 blockno = blockcount.next()
404 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
405 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
405 lines=prettyprintlines(''.join(block), blockno))
406 lines=prettyprintlines(''.join(block), blockno))
406
407
407 def compare(tmpl, context, leftlines, rightlines):
408 def compare(tmpl, context, leftlines, rightlines):
408 '''Generator function that provides side-by-side comparison data.'''
409 '''Generator function that provides side-by-side comparison data.'''
409
410
410 def compline(type, leftlineno, leftline, rightlineno, rightline):
411 def compline(type, leftlineno, leftline, rightlineno, rightline):
411 lineid = leftlineno and ("l%s" % leftlineno) or ''
412 lineid = leftlineno and ("l%s" % leftlineno) or ''
412 lineid += rightlineno and ("r%s" % rightlineno) or ''
413 lineid += rightlineno and ("r%s" % rightlineno) or ''
413 return tmpl('comparisonline',
414 return tmpl('comparisonline',
414 type=type,
415 type=type,
415 lineid=lineid,
416 lineid=lineid,
416 leftlineno=leftlineno,
417 leftlineno=leftlineno,
417 leftlinenumber="% 6s" % (leftlineno or ''),
418 leftlinenumber="% 6s" % (leftlineno or ''),
418 leftline=leftline or '',
419 leftline=leftline or '',
419 rightlineno=rightlineno,
420 rightlineno=rightlineno,
420 rightlinenumber="% 6s" % (rightlineno or ''),
421 rightlinenumber="% 6s" % (rightlineno or ''),
421 rightline=rightline or '')
422 rightline=rightline or '')
422
423
423 def getblock(opcodes):
424 def getblock(opcodes):
424 for type, llo, lhi, rlo, rhi in opcodes:
425 for type, llo, lhi, rlo, rhi in opcodes:
425 len1 = lhi - llo
426 len1 = lhi - llo
426 len2 = rhi - rlo
427 len2 = rhi - rlo
427 count = min(len1, len2)
428 count = min(len1, len2)
428 for i in xrange(count):
429 for i in xrange(count):
429 yield compline(type=type,
430 yield compline(type=type,
430 leftlineno=llo + i + 1,
431 leftlineno=llo + i + 1,
431 leftline=leftlines[llo + i],
432 leftline=leftlines[llo + i],
432 rightlineno=rlo + i + 1,
433 rightlineno=rlo + i + 1,
433 rightline=rightlines[rlo + i])
434 rightline=rightlines[rlo + i])
434 if len1 > len2:
435 if len1 > len2:
435 for i in xrange(llo + count, lhi):
436 for i in xrange(llo + count, lhi):
436 yield compline(type=type,
437 yield compline(type=type,
437 leftlineno=i + 1,
438 leftlineno=i + 1,
438 leftline=leftlines[i],
439 leftline=leftlines[i],
439 rightlineno=None,
440 rightlineno=None,
440 rightline=None)
441 rightline=None)
441 elif len2 > len1:
442 elif len2 > len1:
442 for i in xrange(rlo + count, rhi):
443 for i in xrange(rlo + count, rhi):
443 yield compline(type=type,
444 yield compline(type=type,
444 leftlineno=None,
445 leftlineno=None,
445 leftline=None,
446 leftline=None,
446 rightlineno=i + 1,
447 rightlineno=i + 1,
447 rightline=rightlines[i])
448 rightline=rightlines[i])
448
449
449 s = difflib.SequenceMatcher(None, leftlines, rightlines)
450 s = difflib.SequenceMatcher(None, leftlines, rightlines)
450 if context < 0:
451 if context < 0:
451 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
452 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
452 else:
453 else:
453 for oc in s.get_grouped_opcodes(n=context):
454 for oc in s.get_grouped_opcodes(n=context):
454 yield tmpl('comparisonblock', lines=getblock(oc))
455 yield tmpl('comparisonblock', lines=getblock(oc))
455
456
456 def diffstatgen(ctx, basectx):
457 def diffstatgen(ctx, basectx):
457 '''Generator function that provides the diffstat data.'''
458 '''Generator function that provides the diffstat data.'''
458
459
459 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
460 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
460 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
461 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
461 while True:
462 while True:
462 yield stats, maxname, maxtotal, addtotal, removetotal, binary
463 yield stats, maxname, maxtotal, addtotal, removetotal, binary
463
464
464 def diffsummary(statgen):
465 def diffsummary(statgen):
465 '''Return a short summary of the diff.'''
466 '''Return a short summary of the diff.'''
466
467
467 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
468 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
468 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
469 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
469 len(stats), addtotal, removetotal)
470 len(stats), addtotal, removetotal)
470
471
471 def diffstat(tmpl, ctx, statgen, parity):
472 def diffstat(tmpl, ctx, statgen, parity):
472 '''Return a diffstat template for each file in the diff.'''
473 '''Return a diffstat template for each file in the diff.'''
473
474
474 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
475 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
475 files = ctx.files()
476 files = ctx.files()
476
477
477 def pct(i):
478 def pct(i):
478 if maxtotal == 0:
479 if maxtotal == 0:
479 return 0
480 return 0
480 return (float(i) / maxtotal) * 100
481 return (float(i) / maxtotal) * 100
481
482
482 fileno = 0
483 fileno = 0
483 for filename, adds, removes, isbinary in stats:
484 for filename, adds, removes, isbinary in stats:
484 template = filename in files and 'diffstatlink' or 'diffstatnolink'
485 template = filename in files and 'diffstatlink' or 'diffstatnolink'
485 total = adds + removes
486 total = adds + removes
486 fileno += 1
487 fileno += 1
487 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
488 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
488 total=total, addpct=pct(adds), removepct=pct(removes),
489 total=total, addpct=pct(adds), removepct=pct(removes),
489 parity=parity.next())
490 parity=parity.next())
490
491
491 class sessionvars(object):
492 class sessionvars(object):
492 def __init__(self, vars, start='?'):
493 def __init__(self, vars, start='?'):
493 self.start = start
494 self.start = start
494 self.vars = vars
495 self.vars = vars
495 def __getitem__(self, key):
496 def __getitem__(self, key):
496 return self.vars[key]
497 return self.vars[key]
497 def __setitem__(self, key, value):
498 def __setitem__(self, key, value):
498 self.vars[key] = value
499 self.vars[key] = value
499 def __copy__(self):
500 def __copy__(self):
500 return sessionvars(copy.copy(self.vars), self.start)
501 return sessionvars(copy.copy(self.vars), self.start)
501 def __iter__(self):
502 def __iter__(self):
502 separator = self.start
503 separator = self.start
503 for key, value in sorted(self.vars.iteritems()):
504 for key, value in sorted(self.vars.iteritems()):
504 yield {'name': key, 'value': str(value), 'separator': separator}
505 yield {'name': key, 'value': str(value), 'separator': separator}
505 separator = '&'
506 separator = '&'
506
507
507 class wsgiui(ui.ui):
508 class wsgiui(ui.ui):
508 # default termwidth breaks under mod_wsgi
509 # default termwidth breaks under mod_wsgi
509 def termwidth(self):
510 def termwidth(self):
510 return 80
511 return 80
@@ -1,433 +1,441 b''
1 # template-filters.py - common template expansion filters
1 # template-filters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import cgi, re, os, time, urllib
8 import cgi, re, os, time, urllib
9 import encoding, node, util
9 import encoding, node, util
10 import hbisect
10 import hbisect
11 import templatekw
11 import templatekw
12
12
13 def addbreaks(text):
13 def addbreaks(text):
14 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
14 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
15 every line except the last.
15 every line except the last.
16 """
16 """
17 return text.replace('\n', '<br/>\n')
17 return text.replace('\n', '<br/>\n')
18
18
19 agescales = [("year", 3600 * 24 * 365, 'Y'),
19 agescales = [("year", 3600 * 24 * 365, 'Y'),
20 ("month", 3600 * 24 * 30, 'M'),
20 ("month", 3600 * 24 * 30, 'M'),
21 ("week", 3600 * 24 * 7, 'W'),
21 ("week", 3600 * 24 * 7, 'W'),
22 ("day", 3600 * 24, 'd'),
22 ("day", 3600 * 24, 'd'),
23 ("hour", 3600, 'h'),
23 ("hour", 3600, 'h'),
24 ("minute", 60, 'm'),
24 ("minute", 60, 'm'),
25 ("second", 1, 's')]
25 ("second", 1, 's')]
26
26
27 def age(date, abbrev=False):
27 def age(date, abbrev=False):
28 """:age: Date. Returns a human-readable date/time difference between the
28 """:age: Date. Returns a human-readable date/time difference between the
29 given date/time and the current date/time.
29 given date/time and the current date/time.
30 """
30 """
31
31
32 def plural(t, c):
32 def plural(t, c):
33 if c == 1:
33 if c == 1:
34 return t
34 return t
35 return t + "s"
35 return t + "s"
36 def fmt(t, c, a):
36 def fmt(t, c, a):
37 if abbrev:
37 if abbrev:
38 return "%d%s" % (c, a)
38 return "%d%s" % (c, a)
39 return "%d %s" % (c, plural(t, c))
39 return "%d %s" % (c, plural(t, c))
40
40
41 now = time.time()
41 now = time.time()
42 then = date[0]
42 then = date[0]
43 future = False
43 future = False
44 if then > now:
44 if then > now:
45 future = True
45 future = True
46 delta = max(1, int(then - now))
46 delta = max(1, int(then - now))
47 if delta > agescales[0][1] * 30:
47 if delta > agescales[0][1] * 30:
48 return 'in the distant future'
48 return 'in the distant future'
49 else:
49 else:
50 delta = max(1, int(now - then))
50 delta = max(1, int(now - then))
51 if delta > agescales[0][1] * 2:
51 if delta > agescales[0][1] * 2:
52 return util.shortdate(date)
52 return util.shortdate(date)
53
53
54 for t, s, a in agescales:
54 for t, s, a in agescales:
55 n = delta // s
55 n = delta // s
56 if n >= 2 or s == 1:
56 if n >= 2 or s == 1:
57 if future:
57 if future:
58 return '%s from now' % fmt(t, n, a)
58 return '%s from now' % fmt(t, n, a)
59 return '%s ago' % fmt(t, n, a)
59 return '%s ago' % fmt(t, n, a)
60
60
61 def basename(path):
61 def basename(path):
62 """:basename: Any text. Treats the text as a path, and returns the last
62 """:basename: Any text. Treats the text as a path, and returns the last
63 component of the path after splitting by the path separator
63 component of the path after splitting by the path separator
64 (ignoring trailing separators). For example, "foo/bar/baz" becomes
64 (ignoring trailing separators). For example, "foo/bar/baz" becomes
65 "baz" and "foo/bar//" becomes "bar".
65 "baz" and "foo/bar//" becomes "bar".
66 """
66 """
67 return os.path.basename(path)
67 return os.path.basename(path)
68
68
69 def count(i):
69 def count(i):
70 """:count: List or text. Returns the length as an integer."""
70 """:count: List or text. Returns the length as an integer."""
71 return len(i)
71 return len(i)
72
72
73 def datefilter(text):
73 def datefilter(text):
74 """:date: Date. Returns a date in a Unix date format, including the
74 """:date: Date. Returns a date in a Unix date format, including the
75 timezone: "Mon Sep 04 15:13:13 2006 0700".
75 timezone: "Mon Sep 04 15:13:13 2006 0700".
76 """
76 """
77 return util.datestr(text)
77 return util.datestr(text)
78
78
79 def domain(author):
79 def domain(author):
80 """:domain: Any text. Finds the first string that looks like an email
80 """:domain: Any text. Finds the first string that looks like an email
81 address, and extracts just the domain component. Example: ``User
81 address, and extracts just the domain component. Example: ``User
82 <user@example.com>`` becomes ``example.com``.
82 <user@example.com>`` becomes ``example.com``.
83 """
83 """
84 f = author.find('@')
84 f = author.find('@')
85 if f == -1:
85 if f == -1:
86 return ''
86 return ''
87 author = author[f + 1:]
87 author = author[f + 1:]
88 f = author.find('>')
88 f = author.find('>')
89 if f >= 0:
89 if f >= 0:
90 author = author[:f]
90 author = author[:f]
91 return author
91 return author
92
92
93 def email(text):
93 def email(text):
94 """:email: Any text. Extracts the first string that looks like an email
94 """:email: Any text. Extracts the first string that looks like an email
95 address. Example: ``User <user@example.com>`` becomes
95 address. Example: ``User <user@example.com>`` becomes
96 ``user@example.com``.
96 ``user@example.com``.
97 """
97 """
98 return util.email(text)
98 return util.email(text)
99
99
100 def escape(text):
100 def escape(text):
101 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
101 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
102 and ">" with XML entities, and filters out NUL characters.
102 and ">" with XML entities, and filters out NUL characters.
103 """
103 """
104 return cgi.escape(text.replace('\0', ''), True)
104 return cgi.escape(text.replace('\0', ''), True)
105
105
106 para_re = None
106 para_re = None
107 space_re = None
107 space_re = None
108
108
109 def fill(text, width, initindent='', hangindent=''):
109 def fill(text, width, initindent='', hangindent=''):
110 '''fill many paragraphs with optional indentation.'''
110 '''fill many paragraphs with optional indentation.'''
111 global para_re, space_re
111 global para_re, space_re
112 if para_re is None:
112 if para_re is None:
113 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
113 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
114 space_re = re.compile(r' +')
114 space_re = re.compile(r' +')
115
115
116 def findparas():
116 def findparas():
117 start = 0
117 start = 0
118 while True:
118 while True:
119 m = para_re.search(text, start)
119 m = para_re.search(text, start)
120 if not m:
120 if not m:
121 uctext = unicode(text[start:], encoding.encoding)
121 uctext = unicode(text[start:], encoding.encoding)
122 w = len(uctext)
122 w = len(uctext)
123 while 0 < w and uctext[w - 1].isspace():
123 while 0 < w and uctext[w - 1].isspace():
124 w -= 1
124 w -= 1
125 yield (uctext[:w].encode(encoding.encoding),
125 yield (uctext[:w].encode(encoding.encoding),
126 uctext[w:].encode(encoding.encoding))
126 uctext[w:].encode(encoding.encoding))
127 break
127 break
128 yield text[start:m.start(0)], m.group(1)
128 yield text[start:m.start(0)], m.group(1)
129 start = m.end(1)
129 start = m.end(1)
130
130
131 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
131 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
132 width, initindent, hangindent) + rest
132 width, initindent, hangindent) + rest
133 for para, rest in findparas()])
133 for para, rest in findparas()])
134
134
135 def fill68(text):
135 def fill68(text):
136 """:fill68: Any text. Wraps the text to fit in 68 columns."""
136 """:fill68: Any text. Wraps the text to fit in 68 columns."""
137 return fill(text, 68)
137 return fill(text, 68)
138
138
139 def fill76(text):
139 def fill76(text):
140 """:fill76: Any text. Wraps the text to fit in 76 columns."""
140 """:fill76: Any text. Wraps the text to fit in 76 columns."""
141 return fill(text, 76)
141 return fill(text, 76)
142
142
143 def firstline(text):
143 def firstline(text):
144 """:firstline: Any text. Returns the first line of text."""
144 """:firstline: Any text. Returns the first line of text."""
145 try:
145 try:
146 return text.splitlines(True)[0].rstrip('\r\n')
146 return text.splitlines(True)[0].rstrip('\r\n')
147 except IndexError:
147 except IndexError:
148 return ''
148 return ''
149
149
150 def hexfilter(text):
150 def hexfilter(text):
151 """:hex: Any text. Convert a binary Mercurial node identifier into
151 """:hex: Any text. Convert a binary Mercurial node identifier into
152 its long hexadecimal representation.
152 its long hexadecimal representation.
153 """
153 """
154 return node.hex(text)
154 return node.hex(text)
155
155
156 def hgdate(text):
156 def hgdate(text):
157 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
157 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
158 25200" (Unix timestamp, timezone offset).
158 25200" (Unix timestamp, timezone offset).
159 """
159 """
160 return "%d %d" % text
160 return "%d %d" % text
161
161
162 def isodate(text):
162 def isodate(text):
163 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
163 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
164 +0200".
164 +0200".
165 """
165 """
166 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
166 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
167
167
168 def isodatesec(text):
168 def isodatesec(text):
169 """:isodatesec: Date. Returns the date in ISO 8601 format, including
169 """:isodatesec: Date. Returns the date in ISO 8601 format, including
170 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
170 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
171 filter.
171 filter.
172 """
172 """
173 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
173 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
174
174
175 def indent(text, prefix):
175 def indent(text, prefix):
176 '''indent each non-empty line of text after first with prefix.'''
176 '''indent each non-empty line of text after first with prefix.'''
177 lines = text.splitlines()
177 lines = text.splitlines()
178 num_lines = len(lines)
178 num_lines = len(lines)
179 endswithnewline = text[-1:] == '\n'
179 endswithnewline = text[-1:] == '\n'
180 def indenter():
180 def indenter():
181 for i in xrange(num_lines):
181 for i in xrange(num_lines):
182 l = lines[i]
182 l = lines[i]
183 if i and l.strip():
183 if i and l.strip():
184 yield prefix
184 yield prefix
185 yield l
185 yield l
186 if i < num_lines - 1 or endswithnewline:
186 if i < num_lines - 1 or endswithnewline:
187 yield '\n'
187 yield '\n'
188 return "".join(indenter())
188 return "".join(indenter())
189
189
190 def json(obj):
190 def json(obj):
191 if obj is None or obj is False or obj is True:
191 if obj is None or obj is False or obj is True:
192 return {None: 'null', False: 'false', True: 'true'}[obj]
192 return {None: 'null', False: 'false', True: 'true'}[obj]
193 elif isinstance(obj, int) or isinstance(obj, float):
193 elif isinstance(obj, int) or isinstance(obj, float):
194 return str(obj)
194 return str(obj)
195 elif isinstance(obj, str):
195 elif isinstance(obj, str):
196 u = unicode(obj, encoding.encoding, 'replace')
196 u = unicode(obj, encoding.encoding, 'replace')
197 return '"%s"' % jsonescape(u)
197 return '"%s"' % jsonescape(u)
198 elif isinstance(obj, unicode):
198 elif isinstance(obj, unicode):
199 return '"%s"' % jsonescape(obj)
199 return '"%s"' % jsonescape(obj)
200 elif util.safehasattr(obj, 'keys'):
200 elif util.safehasattr(obj, 'keys'):
201 out = []
201 out = []
202 for k, v in sorted(obj.iteritems()):
202 for k, v in sorted(obj.iteritems()):
203 s = '%s: %s' % (json(k), json(v))
203 s = '%s: %s' % (json(k), json(v))
204 out.append(s)
204 out.append(s)
205 return '{' + ', '.join(out) + '}'
205 return '{' + ', '.join(out) + '}'
206 elif util.safehasattr(obj, '__iter__'):
206 elif util.safehasattr(obj, '__iter__'):
207 out = []
207 out = []
208 for i in obj:
208 for i in obj:
209 out.append(json(i))
209 out.append(json(i))
210 return '[' + ', '.join(out) + ']'
210 return '[' + ', '.join(out) + ']'
211 elif util.safehasattr(obj, '__call__'):
211 elif util.safehasattr(obj, '__call__'):
212 return json(obj())
212 return json(obj())
213 else:
213 else:
214 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
214 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
215
215
216 def _uescape(c):
216 def _uescape(c):
217 if ord(c) < 0x80:
217 if ord(c) < 0x80:
218 return c
218 return c
219 else:
219 else:
220 return '\\u%04x' % ord(c)
220 return '\\u%04x' % ord(c)
221
221
222 _escapes = [
222 _escapes = [
223 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
223 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
224 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
224 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
225 ('<', '\\u003c'), ('>', '\\u003e'), ('\0', '\\u0000')
225 ('<', '\\u003c'), ('>', '\\u003e'), ('\0', '\\u0000')
226 ]
226 ]
227
227
228 def jsonescape(s):
228 def jsonescape(s):
229 for k, v in _escapes:
229 for k, v in _escapes:
230 s = s.replace(k, v)
230 s = s.replace(k, v)
231 return ''.join(_uescape(c) for c in s)
231 return ''.join(_uescape(c) for c in s)
232
232
233 def localdate(text):
233 def localdate(text):
234 """:localdate: Date. Converts a date to local date."""
234 """:localdate: Date. Converts a date to local date."""
235 return (util.parsedate(text)[0], util.makedate()[1])
235 return (util.parsedate(text)[0], util.makedate()[1])
236
236
237 def lower(text):
237 def lower(text):
238 """:lower: Any text. Converts the text to lowercase."""
238 """:lower: Any text. Converts the text to lowercase."""
239 return encoding.lower(text)
239 return encoding.lower(text)
240
240
241 def nonempty(str):
241 def nonempty(str):
242 """:nonempty: Any text. Returns '(none)' if the string is empty."""
242 """:nonempty: Any text. Returns '(none)' if the string is empty."""
243 return str or "(none)"
243 return str or "(none)"
244
244
245 def obfuscate(text):
245 def obfuscate(text):
246 """:obfuscate: Any text. Returns the input text rendered as a sequence of
246 """:obfuscate: Any text. Returns the input text rendered as a sequence of
247 XML entities.
247 XML entities.
248 """
248 """
249 text = unicode(text, encoding.encoding, 'replace')
249 text = unicode(text, encoding.encoding, 'replace')
250 return ''.join(['&#%d;' % ord(c) for c in text])
250 return ''.join(['&#%d;' % ord(c) for c in text])
251
251
252 def permissions(flags):
252 def permissions(flags):
253 if "l" in flags:
253 if "l" in flags:
254 return "lrwxrwxrwx"
254 return "lrwxrwxrwx"
255 if "x" in flags:
255 if "x" in flags:
256 return "-rwxr-xr-x"
256 return "-rwxr-xr-x"
257 return "-rw-r--r--"
257 return "-rw-r--r--"
258
258
259 def person(author):
259 def person(author):
260 """:person: Any text. Returns the name before an email address,
260 """:person: Any text. Returns the name before an email address,
261 interpreting it as per RFC 5322.
261 interpreting it as per RFC 5322.
262
262
263 >>> person('foo@bar')
263 >>> person('foo@bar')
264 'foo'
264 'foo'
265 >>> person('Foo Bar <foo@bar>')
265 >>> person('Foo Bar <foo@bar>')
266 'Foo Bar'
266 'Foo Bar'
267 >>> person('"Foo Bar" <foo@bar>')
267 >>> person('"Foo Bar" <foo@bar>')
268 'Foo Bar'
268 'Foo Bar'
269 >>> person('"Foo \"buz\" Bar" <foo@bar>')
269 >>> person('"Foo \"buz\" Bar" <foo@bar>')
270 'Foo "buz" Bar'
270 'Foo "buz" Bar'
271 >>> # The following are invalid, but do exist in real-life
271 >>> # The following are invalid, but do exist in real-life
272 ...
272 ...
273 >>> person('Foo "buz" Bar <foo@bar>')
273 >>> person('Foo "buz" Bar <foo@bar>')
274 'Foo "buz" Bar'
274 'Foo "buz" Bar'
275 >>> person('"Foo Bar <foo@bar>')
275 >>> person('"Foo Bar <foo@bar>')
276 'Foo Bar'
276 'Foo Bar'
277 """
277 """
278 if '@' not in author:
278 if '@' not in author:
279 return author
279 return author
280 f = author.find('<')
280 f = author.find('<')
281 if f != -1:
281 if f != -1:
282 return author[:f].strip(' "').replace('\\"', '"')
282 return author[:f].strip(' "').replace('\\"', '"')
283 f = author.find('@')
283 f = author.find('@')
284 return author[:f].replace('.', ' ')
284 return author[:f].replace('.', ' ')
285
285
286 def revescape(text):
287 """:revescape: Any text. Escapes all "special" characters, except @.
288 Forward slashes are escaped twice to prevent web servers from prematurely
289 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
290 """
291 return urllib.quote(text, safe='/@').replace('/', '%252F')
292
286 def rfc3339date(text):
293 def rfc3339date(text):
287 """:rfc3339date: Date. Returns a date using the Internet date format
294 """:rfc3339date: Date. Returns a date using the Internet date format
288 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
295 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
289 """
296 """
290 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
297 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
291
298
292 def rfc822date(text):
299 def rfc822date(text):
293 """:rfc822date: Date. Returns a date using the same format used in email
300 """:rfc822date: Date. Returns a date using the same format used in email
294 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
301 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
295 """
302 """
296 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
303 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
297
304
298 def short(text):
305 def short(text):
299 """:short: Changeset hash. Returns the short form of a changeset hash,
306 """:short: Changeset hash. Returns the short form of a changeset hash,
300 i.e. a 12 hexadecimal digit string.
307 i.e. a 12 hexadecimal digit string.
301 """
308 """
302 return text[:12]
309 return text[:12]
303
310
304 def shortbisect(text):
311 def shortbisect(text):
305 """:shortbisect: Any text. Treats `text` as a bisection status, and
312 """:shortbisect: Any text. Treats `text` as a bisection status, and
306 returns a single-character representing the status (G: good, B: bad,
313 returns a single-character representing the status (G: good, B: bad,
307 S: skipped, U: untested, I: ignored). Returns single space if `text`
314 S: skipped, U: untested, I: ignored). Returns single space if `text`
308 is not a valid bisection status.
315 is not a valid bisection status.
309 """
316 """
310 return hbisect.shortlabel(text) or ' '
317 return hbisect.shortlabel(text) or ' '
311
318
312 def shortdate(text):
319 def shortdate(text):
313 """:shortdate: Date. Returns a date like "2006-09-18"."""
320 """:shortdate: Date. Returns a date like "2006-09-18"."""
314 return util.shortdate(text)
321 return util.shortdate(text)
315
322
316 def splitlines(text):
323 def splitlines(text):
317 """:splitlines: Any text. Split text into a list of lines."""
324 """:splitlines: Any text. Split text into a list of lines."""
318 return templatekw.showlist('line', text.splitlines(), 'lines')
325 return templatekw.showlist('line', text.splitlines(), 'lines')
319
326
320 def stringescape(text):
327 def stringescape(text):
321 return text.encode('string_escape')
328 return text.encode('string_escape')
322
329
323 def stringify(thing):
330 def stringify(thing):
324 """:stringify: Any type. Turns the value into text by converting values into
331 """:stringify: Any type. Turns the value into text by converting values into
325 text and concatenating them.
332 text and concatenating them.
326 """
333 """
327 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
334 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
328 return "".join([stringify(t) for t in thing if t is not None])
335 return "".join([stringify(t) for t in thing if t is not None])
329 if thing is None:
336 if thing is None:
330 return ""
337 return ""
331 return str(thing)
338 return str(thing)
332
339
333 def strip(text):
340 def strip(text):
334 """:strip: Any text. Strips all leading and trailing whitespace."""
341 """:strip: Any text. Strips all leading and trailing whitespace."""
335 return text.strip()
342 return text.strip()
336
343
337 def stripdir(text):
344 def stripdir(text):
338 """:stripdir: Treat the text as path and strip a directory level, if
345 """:stripdir: Treat the text as path and strip a directory level, if
339 possible. For example, "foo" and "foo/bar" becomes "foo".
346 possible. For example, "foo" and "foo/bar" becomes "foo".
340 """
347 """
341 dir = os.path.dirname(text)
348 dir = os.path.dirname(text)
342 if dir == "":
349 if dir == "":
343 return os.path.basename(text)
350 return os.path.basename(text)
344 else:
351 else:
345 return dir
352 return dir
346
353
347 def tabindent(text):
354 def tabindent(text):
348 """:tabindent: Any text. Returns the text, with every non-empty line
355 """:tabindent: Any text. Returns the text, with every non-empty line
349 except the first starting with a tab character.
356 except the first starting with a tab character.
350 """
357 """
351 return indent(text, '\t')
358 return indent(text, '\t')
352
359
353 def upper(text):
360 def upper(text):
354 """:upper: Any text. Converts the text to uppercase."""
361 """:upper: Any text. Converts the text to uppercase."""
355 return encoding.upper(text)
362 return encoding.upper(text)
356
363
357 def urlescape(text):
364 def urlescape(text):
358 """:urlescape: Any text. Escapes all "special" characters. For example,
365 """:urlescape: Any text. Escapes all "special" characters. For example,
359 "foo bar" becomes "foo%20bar".
366 "foo bar" becomes "foo%20bar".
360 """
367 """
361 return urllib.quote(text)
368 return urllib.quote(text)
362
369
363 def userfilter(text):
370 def userfilter(text):
364 """:user: Any text. Returns a short representation of a user name or email
371 """:user: Any text. Returns a short representation of a user name or email
365 address."""
372 address."""
366 return util.shortuser(text)
373 return util.shortuser(text)
367
374
368 def emailuser(text):
375 def emailuser(text):
369 """:emailuser: Any text. Returns the user portion of an email address."""
376 """:emailuser: Any text. Returns the user portion of an email address."""
370 return util.emailuser(text)
377 return util.emailuser(text)
371
378
372 def xmlescape(text):
379 def xmlescape(text):
373 text = (text
380 text = (text
374 .replace('&', '&amp;')
381 .replace('&', '&amp;')
375 .replace('<', '&lt;')
382 .replace('<', '&lt;')
376 .replace('>', '&gt;')
383 .replace('>', '&gt;')
377 .replace('"', '&quot;')
384 .replace('"', '&quot;')
378 .replace("'", '&#39;')) # &apos; invalid in HTML
385 .replace("'", '&#39;')) # &apos; invalid in HTML
379 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
386 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
380
387
381 filters = {
388 filters = {
382 "addbreaks": addbreaks,
389 "addbreaks": addbreaks,
383 "age": age,
390 "age": age,
384 "basename": basename,
391 "basename": basename,
385 "count": count,
392 "count": count,
386 "date": datefilter,
393 "date": datefilter,
387 "domain": domain,
394 "domain": domain,
388 "email": email,
395 "email": email,
389 "escape": escape,
396 "escape": escape,
390 "fill68": fill68,
397 "fill68": fill68,
391 "fill76": fill76,
398 "fill76": fill76,
392 "firstline": firstline,
399 "firstline": firstline,
393 "hex": hexfilter,
400 "hex": hexfilter,
394 "hgdate": hgdate,
401 "hgdate": hgdate,
395 "isodate": isodate,
402 "isodate": isodate,
396 "isodatesec": isodatesec,
403 "isodatesec": isodatesec,
397 "json": json,
404 "json": json,
398 "jsonescape": jsonescape,
405 "jsonescape": jsonescape,
399 "localdate": localdate,
406 "localdate": localdate,
400 "lower": lower,
407 "lower": lower,
401 "nonempty": nonempty,
408 "nonempty": nonempty,
402 "obfuscate": obfuscate,
409 "obfuscate": obfuscate,
403 "permissions": permissions,
410 "permissions": permissions,
404 "person": person,
411 "person": person,
412 "revescape": revescape,
405 "rfc3339date": rfc3339date,
413 "rfc3339date": rfc3339date,
406 "rfc822date": rfc822date,
414 "rfc822date": rfc822date,
407 "short": short,
415 "short": short,
408 "shortbisect": shortbisect,
416 "shortbisect": shortbisect,
409 "shortdate": shortdate,
417 "shortdate": shortdate,
410 "splitlines": splitlines,
418 "splitlines": splitlines,
411 "stringescape": stringescape,
419 "stringescape": stringescape,
412 "stringify": stringify,
420 "stringify": stringify,
413 "strip": strip,
421 "strip": strip,
414 "stripdir": stripdir,
422 "stripdir": stripdir,
415 "tabindent": tabindent,
423 "tabindent": tabindent,
416 "upper": upper,
424 "upper": upper,
417 "urlescape": urlescape,
425 "urlescape": urlescape,
418 "user": userfilter,
426 "user": userfilter,
419 "emailuser": emailuser,
427 "emailuser": emailuser,
420 "xmlescape": xmlescape,
428 "xmlescape": xmlescape,
421 }
429 }
422
430
423 def websub(text, websubtable):
431 def websub(text, websubtable):
424 """:websub: Any text. Only applies to hgweb. Applies the regular
432 """:websub: Any text. Only applies to hgweb. Applies the regular
425 expression replacements defined in the websub section.
433 expression replacements defined in the websub section.
426 """
434 """
427 if websubtable:
435 if websubtable:
428 for regexp, format in websubtable:
436 for regexp, format in websubtable:
429 text = regexp.sub(format, text)
437 text = regexp.sub(format, text)
430 return text
438 return text
431
439
432 # tell hggettext to extract docstrings from these functions:
440 # tell hggettext to extract docstrings from these functions:
433 i18nfunctions = filters.values()
441 i18nfunctions = filters.values()
General Comments 0
You need to be logged in to leave comments. Login now