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