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