##// END OF EJS Templates
hgweb: use patch.diffhunks in webutil.diffs to simplify the algorithm...
Denis Laxalde -
r31276:cd29673c default
parent child Browse files
Show More
@@ -1,606 +1,596 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_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=[], hiderev=None):
146 146 self.siblings = [s for s in siblings if s.node() != nullid]
147 147 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
148 148 self.siblings = []
149 149
150 150 def __iter__(self):
151 151 for s in self.siblings:
152 152 d = {
153 153 'node': s.hex(),
154 154 'rev': s.rev(),
155 155 'user': s.user(),
156 156 'date': s.date(),
157 157 'description': s.description(),
158 158 'branch': s.branch(),
159 159 }
160 160 if util.safehasattr(s, 'path'):
161 161 d['file'] = s.path()
162 162 yield d
163 163
164 164 def __len__(self):
165 165 return len(self.siblings)
166 166
167 167 def annotate(fctx, ui):
168 168 diffopts = patch.difffeatureopts(ui, untrusted=True,
169 169 section='annotate', whitespace=True)
170 170 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
171 171
172 172 def parents(ctx, hide=None):
173 173 if isinstance(ctx, context.basefilectx):
174 174 introrev = ctx.introrev()
175 175 if ctx.changectx().rev() != introrev:
176 176 return _siblings([ctx.repo()[introrev]], hide)
177 177 return _siblings(ctx.parents(), hide)
178 178
179 179 def children(ctx, hide=None):
180 180 return _siblings(ctx.children(), hide)
181 181
182 182 def renamelink(fctx):
183 183 r = fctx.renamed()
184 184 if r:
185 185 return [{'file': r[0], 'node': hex(r[1])}]
186 186 return []
187 187
188 188 def nodetagsdict(repo, node):
189 189 return [{"name": i} for i in repo.nodetags(node)]
190 190
191 191 def nodebookmarksdict(repo, node):
192 192 return [{"name": i} for i in repo.nodebookmarks(node)]
193 193
194 194 def nodebranchdict(repo, ctx):
195 195 branches = []
196 196 branch = ctx.branch()
197 197 # If this is an empty repo, ctx.node() == nullid,
198 198 # ctx.branch() == 'default'.
199 199 try:
200 200 branchnode = repo.branchtip(branch)
201 201 except error.RepoLookupError:
202 202 branchnode = None
203 203 if branchnode == ctx.node():
204 204 branches.append({"name": branch})
205 205 return branches
206 206
207 207 def nodeinbranch(repo, ctx):
208 208 branches = []
209 209 branch = ctx.branch()
210 210 try:
211 211 branchnode = repo.branchtip(branch)
212 212 except error.RepoLookupError:
213 213 branchnode = None
214 214 if branch != 'default' and branchnode != ctx.node():
215 215 branches.append({"name": branch})
216 216 return branches
217 217
218 218 def nodebranchnodefault(ctx):
219 219 branches = []
220 220 branch = ctx.branch()
221 221 if branch != 'default':
222 222 branches.append({"name": branch})
223 223 return branches
224 224
225 225 def showtag(repo, tmpl, t1, node=nullid, **args):
226 226 for t in repo.nodetags(node):
227 227 yield tmpl(t1, tag=t, **args)
228 228
229 229 def showbookmark(repo, tmpl, t1, node=nullid, **args):
230 230 for t in repo.nodebookmarks(node):
231 231 yield tmpl(t1, bookmark=t, **args)
232 232
233 233 def branchentries(repo, stripecount, limit=0):
234 234 tips = []
235 235 heads = repo.heads()
236 236 parity = paritygen(stripecount)
237 237 sortkey = lambda item: (not item[1], item[0].rev())
238 238
239 239 def entries(**map):
240 240 count = 0
241 241 if not tips:
242 242 for tag, hs, tip, closed in repo.branchmap().iterbranches():
243 243 tips.append((repo[tip], closed))
244 244 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
245 245 if limit > 0 and count >= limit:
246 246 return
247 247 count += 1
248 248 if closed:
249 249 status = 'closed'
250 250 elif ctx.node() not in heads:
251 251 status = 'inactive'
252 252 else:
253 253 status = 'open'
254 254 yield {
255 255 'parity': next(parity),
256 256 'branch': ctx.branch(),
257 257 'status': status,
258 258 'node': ctx.hex(),
259 259 'date': ctx.date()
260 260 }
261 261
262 262 return entries
263 263
264 264 def cleanpath(repo, path):
265 265 path = path.lstrip('/')
266 266 return pathutil.canonpath(repo.root, '', path)
267 267
268 268 def changeidctx(repo, changeid):
269 269 try:
270 270 ctx = repo[changeid]
271 271 except error.RepoError:
272 272 man = repo.manifestlog._revlog
273 273 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
274 274
275 275 return ctx
276 276
277 277 def changectx(repo, req):
278 278 changeid = "tip"
279 279 if 'node' in req.form:
280 280 changeid = req.form['node'][0]
281 281 ipos = changeid.find(':')
282 282 if ipos != -1:
283 283 changeid = changeid[(ipos + 1):]
284 284 elif 'manifest' in req.form:
285 285 changeid = req.form['manifest'][0]
286 286
287 287 return changeidctx(repo, changeid)
288 288
289 289 def basechangectx(repo, req):
290 290 if 'node' in req.form:
291 291 changeid = req.form['node'][0]
292 292 ipos = changeid.find(':')
293 293 if ipos != -1:
294 294 changeid = changeid[:ipos]
295 295 return changeidctx(repo, changeid)
296 296
297 297 return None
298 298
299 299 def filectx(repo, req):
300 300 if 'file' not in req.form:
301 301 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
302 302 path = cleanpath(repo, req.form['file'][0])
303 303 if 'node' in req.form:
304 304 changeid = req.form['node'][0]
305 305 elif 'filenode' in req.form:
306 306 changeid = req.form['filenode'][0]
307 307 else:
308 308 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
309 309 try:
310 310 fctx = repo[changeid][path]
311 311 except error.RepoError:
312 312 fctx = repo.filectx(path, fileid=changeid)
313 313
314 314 return fctx
315 315
316 316 def commonentry(repo, ctx):
317 317 node = ctx.node()
318 318 return {
319 319 'rev': ctx.rev(),
320 320 'node': hex(node),
321 321 'author': ctx.user(),
322 322 'desc': ctx.description(),
323 323 'date': ctx.date(),
324 324 'extra': ctx.extra(),
325 325 'phase': ctx.phasestr(),
326 326 'branch': nodebranchnodefault(ctx),
327 327 'inbranch': nodeinbranch(repo, ctx),
328 328 'branches': nodebranchdict(repo, ctx),
329 329 'tags': nodetagsdict(repo, node),
330 330 'bookmarks': nodebookmarksdict(repo, node),
331 331 'parent': lambda **x: parents(ctx),
332 332 'child': lambda **x: children(ctx),
333 333 }
334 334
335 335 def changelistentry(web, ctx, tmpl):
336 336 '''Obtain a dictionary to be used for entries in a changelist.
337 337
338 338 This function is called when producing items for the "entries" list passed
339 339 to the "shortlog" and "changelog" templates.
340 340 '''
341 341 repo = web.repo
342 342 rev = ctx.rev()
343 343 n = ctx.node()
344 344 showtags = showtag(repo, tmpl, 'changelogtag', n)
345 345 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
346 346
347 347 entry = commonentry(repo, ctx)
348 348 entry.update(
349 349 allparents=lambda **x: parents(ctx),
350 350 parent=lambda **x: parents(ctx, rev - 1),
351 351 child=lambda **x: children(ctx, rev + 1),
352 352 changelogtag=showtags,
353 353 files=files,
354 354 )
355 355 return entry
356 356
357 357 def symrevorshortnode(req, ctx):
358 358 if 'node' in req.form:
359 359 return templatefilters.revescape(req.form['node'][0])
360 360 else:
361 361 return short(ctx.node())
362 362
363 363 def changesetentry(web, req, tmpl, ctx):
364 364 '''Obtain a dictionary to be used to render the "changeset" template.'''
365 365
366 366 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
367 367 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
368 368 ctx.node())
369 369 showbranch = nodebranchnodefault(ctx)
370 370
371 371 files = []
372 372 parity = paritygen(web.stripecount)
373 373 for blockno, f in enumerate(ctx.files()):
374 374 template = f in ctx and 'filenodelink' or 'filenolink'
375 375 files.append(tmpl(template,
376 376 node=ctx.hex(), file=f, blockno=blockno + 1,
377 377 parity=next(parity)))
378 378
379 379 basectx = basechangectx(web.repo, req)
380 380 if basectx is None:
381 381 basectx = ctx.p1()
382 382
383 383 style = web.config('web', 'style', 'paper')
384 384 if 'style' in req.form:
385 385 style = req.form['style'][0]
386 386
387 387 parity = paritygen(web.stripecount)
388 388 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
389 389
390 390 parity = paritygen(web.stripecount)
391 391 diffstatsgen = diffstatgen(ctx, basectx)
392 392 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
393 393
394 394 return dict(
395 395 diff=diff,
396 396 symrev=symrevorshortnode(req, ctx),
397 397 basenode=basectx.hex(),
398 398 changesettag=showtags,
399 399 changesetbookmark=showbookmarks,
400 400 changesetbranch=showbranch,
401 401 files=files,
402 402 diffsummary=lambda **x: diffsummary(diffstatsgen),
403 403 diffstat=diffstats,
404 404 archives=web.archivelist(ctx.hex()),
405 405 **commonentry(web.repo, ctx))
406 406
407 407 def listfilediffs(tmpl, files, node, max):
408 408 for f in files[:max]:
409 409 yield tmpl('filedifflink', node=hex(node), file=f)
410 410 if len(files) > max:
411 411 yield tmpl('fileellipses')
412 412
413 413 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
414 414
415 def countgen():
416 start = 1
417 while True:
418 yield start
419 start += 1
420
421 blockcount = countgen()
422 def prettyprintlines(diff, blockno):
423 for lineno, l in enumerate(diff.splitlines(True), 1):
415 def prettyprintlines(lines, blockno):
416 for lineno, l in enumerate(lines, 1):
424 417 difflineno = "%d.%d" % (blockno, lineno)
425 418 if l.startswith('+'):
426 419 ltype = "difflineplus"
427 420 elif l.startswith('-'):
428 421 ltype = "difflineminus"
429 422 elif l.startswith('@'):
430 423 ltype = "difflineat"
431 424 else:
432 425 ltype = "diffline"
433 426 yield tmpl(ltype,
434 427 line=l,
435 428 lineno=lineno,
436 429 lineid="l%s" % difflineno,
437 430 linenumber="% 8s" % difflineno)
438 431
439 432 if files:
440 433 m = match.exact(repo.root, repo.getcwd(), files)
441 434 else:
442 435 m = match.always(repo.root, repo.getcwd())
443 436
444 437 diffopts = patch.diffopts(repo.ui, untrusted=True)
445 438 node1 = basectx.node()
446 439 node2 = ctx.node()
447 440
448 block = []
449 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
450 if chunk.startswith('diff') and block:
451 blockno = next(blockcount)
441 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
442 for blockno, (header, hunks) in enumerate(diffhunks, 1):
443 if style != 'raw':
444 header = header[1:]
445 lines = [h + '\n' for h in header]
446 for hunkrange, hunklines in hunks:
447 lines.extend(hunklines)
448 if lines:
452 449 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
453 lines=prettyprintlines(''.join(block), blockno))
454 block = []
455 if chunk.startswith('diff') and style != 'raw':
456 chunk = ''.join(chunk.splitlines(True)[1:])
457 block.append(chunk)
458 blockno = next(blockcount)
459 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
460 lines=prettyprintlines(''.join(block), blockno))
450 lines=prettyprintlines(lines, blockno))
461 451
462 452 def compare(tmpl, context, leftlines, rightlines):
463 453 '''Generator function that provides side-by-side comparison data.'''
464 454
465 455 def compline(type, leftlineno, leftline, rightlineno, rightline):
466 456 lineid = leftlineno and ("l%s" % leftlineno) or ''
467 457 lineid += rightlineno and ("r%s" % rightlineno) or ''
468 458 return tmpl('comparisonline',
469 459 type=type,
470 460 lineid=lineid,
471 461 leftlineno=leftlineno,
472 462 leftlinenumber="% 6s" % (leftlineno or ''),
473 463 leftline=leftline or '',
474 464 rightlineno=rightlineno,
475 465 rightlinenumber="% 6s" % (rightlineno or ''),
476 466 rightline=rightline or '')
477 467
478 468 def getblock(opcodes):
479 469 for type, llo, lhi, rlo, rhi in opcodes:
480 470 len1 = lhi - llo
481 471 len2 = rhi - rlo
482 472 count = min(len1, len2)
483 473 for i in xrange(count):
484 474 yield compline(type=type,
485 475 leftlineno=llo + i + 1,
486 476 leftline=leftlines[llo + i],
487 477 rightlineno=rlo + i + 1,
488 478 rightline=rightlines[rlo + i])
489 479 if len1 > len2:
490 480 for i in xrange(llo + count, lhi):
491 481 yield compline(type=type,
492 482 leftlineno=i + 1,
493 483 leftline=leftlines[i],
494 484 rightlineno=None,
495 485 rightline=None)
496 486 elif len2 > len1:
497 487 for i in xrange(rlo + count, rhi):
498 488 yield compline(type=type,
499 489 leftlineno=None,
500 490 leftline=None,
501 491 rightlineno=i + 1,
502 492 rightline=rightlines[i])
503 493
504 494 s = difflib.SequenceMatcher(None, leftlines, rightlines)
505 495 if context < 0:
506 496 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
507 497 else:
508 498 for oc in s.get_grouped_opcodes(n=context):
509 499 yield tmpl('comparisonblock', lines=getblock(oc))
510 500
511 501 def diffstatgen(ctx, basectx):
512 502 '''Generator function that provides the diffstat data.'''
513 503
514 504 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
515 505 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
516 506 while True:
517 507 yield stats, maxname, maxtotal, addtotal, removetotal, binary
518 508
519 509 def diffsummary(statgen):
520 510 '''Return a short summary of the diff.'''
521 511
522 512 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
523 513 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
524 514 len(stats), addtotal, removetotal)
525 515
526 516 def diffstat(tmpl, ctx, statgen, parity):
527 517 '''Return a diffstat template for each file in the diff.'''
528 518
529 519 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
530 520 files = ctx.files()
531 521
532 522 def pct(i):
533 523 if maxtotal == 0:
534 524 return 0
535 525 return (float(i) / maxtotal) * 100
536 526
537 527 fileno = 0
538 528 for filename, adds, removes, isbinary in stats:
539 529 template = filename in files and 'diffstatlink' or 'diffstatnolink'
540 530 total = adds + removes
541 531 fileno += 1
542 532 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
543 533 total=total, addpct=pct(adds), removepct=pct(removes),
544 534 parity=next(parity))
545 535
546 536 class sessionvars(object):
547 537 def __init__(self, vars, start='?'):
548 538 self.start = start
549 539 self.vars = vars
550 540 def __getitem__(self, key):
551 541 return self.vars[key]
552 542 def __setitem__(self, key, value):
553 543 self.vars[key] = value
554 544 def __copy__(self):
555 545 return sessionvars(copy.copy(self.vars), self.start)
556 546 def __iter__(self):
557 547 separator = self.start
558 548 for key, value in sorted(self.vars.iteritems()):
559 549 yield {'name': key, 'value': str(value), 'separator': separator}
560 550 separator = '&'
561 551
562 552 class wsgiui(uimod.ui):
563 553 # default termwidth breaks under mod_wsgi
564 554 def termwidth(self):
565 555 return 80
566 556
567 557 def getwebsubs(repo):
568 558 websubtable = []
569 559 websubdefs = repo.ui.configitems('websub')
570 560 # we must maintain interhg backwards compatibility
571 561 websubdefs += repo.ui.configitems('interhg')
572 562 for key, pattern in websubdefs:
573 563 # grab the delimiter from the character after the "s"
574 564 unesc = pattern[1]
575 565 delim = re.escape(unesc)
576 566
577 567 # identify portions of the pattern, taking care to avoid escaped
578 568 # delimiters. the replace format and flags are optional, but
579 569 # delimiters are required.
580 570 match = re.match(
581 571 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
582 572 % (delim, delim, delim), pattern)
583 573 if not match:
584 574 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
585 575 % (key, pattern))
586 576 continue
587 577
588 578 # we need to unescape the delimiter for regexp and format
589 579 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
590 580 regexp = delim_re.sub(unesc, match.group(1))
591 581 format = delim_re.sub(unesc, match.group(2))
592 582
593 583 # the pattern allows for 6 regexp flags, so set them if necessary
594 584 flagin = match.group(3)
595 585 flags = 0
596 586 if flagin:
597 587 for flag in flagin.upper():
598 588 flags |= re.__dict__[flag]
599 589
600 590 try:
601 591 regexp = re.compile(regexp, flags)
602 592 websubtable.append((regexp, format))
603 593 except re.error:
604 594 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
605 595 % (key, regexp))
606 596 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now