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