##// END OF EJS Templates
mdiff: add a hunkinrange helper function...
Denis Laxalde -
r31808:ca3b4a2b default
parent child Browse files
Show More
@@ -1,628 +1,628 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 mdiff,
30 31 patch,
31 32 pathutil,
32 33 templatefilters,
33 34 ui as uimod,
34 35 util,
35 36 )
36 37
37 38 def up(p):
38 39 if p[0] != "/":
39 40 p = "/" + p
40 41 if p[-1] == "/":
41 42 p = p[:-1]
42 43 up = os.path.dirname(p)
43 44 if up == "/":
44 45 return "/"
45 46 return up + "/"
46 47
47 48 def _navseq(step, firststep=None):
48 49 if firststep:
49 50 yield firststep
50 51 if firststep >= 20 and firststep <= 40:
51 52 firststep = 50
52 53 yield firststep
53 54 assert step > 0
54 55 assert firststep > 0
55 56 while step <= firststep:
56 57 step *= 10
57 58 while True:
58 59 yield 1 * step
59 60 yield 3 * step
60 61 step *= 10
61 62
62 63 class revnav(object):
63 64
64 65 def __init__(self, repo):
65 66 """Navigation generation object
66 67
67 68 :repo: repo object we generate nav for
68 69 """
69 70 # used for hex generation
70 71 self._revlog = repo.changelog
71 72
72 73 def __nonzero__(self):
73 74 """return True if any revision to navigate over"""
74 75 return self._first() is not None
75 76
76 77 __bool__ = __nonzero__
77 78
78 79 def _first(self):
79 80 """return the minimum non-filtered changeset or None"""
80 81 try:
81 82 return next(iter(self._revlog))
82 83 except StopIteration:
83 84 return None
84 85
85 86 def hex(self, rev):
86 87 return hex(self._revlog.node(rev))
87 88
88 89 def gen(self, pos, pagelen, limit):
89 90 """computes label and revision id for navigation link
90 91
91 92 :pos: is the revision relative to which we generate navigation.
92 93 :pagelen: the size of each navigation page
93 94 :limit: how far shall we link
94 95
95 96 The return is:
96 97 - a single element tuple
97 98 - containing a dictionary with a `before` and `after` key
98 99 - values are generator functions taking arbitrary number of kwargs
99 100 - yield items are dictionaries with `label` and `node` keys
100 101 """
101 102 if not self:
102 103 # empty repo
103 104 return ({'before': (), 'after': ()},)
104 105
105 106 targets = []
106 107 for f in _navseq(1, pagelen):
107 108 if f > limit:
108 109 break
109 110 targets.append(pos + f)
110 111 targets.append(pos - f)
111 112 targets.sort()
112 113
113 114 first = self._first()
114 115 navbefore = [("(%i)" % first, self.hex(first))]
115 116 navafter = []
116 117 for rev in targets:
117 118 if rev not in self._revlog:
118 119 continue
119 120 if pos < rev < limit:
120 121 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
121 122 if 0 < rev < pos:
122 123 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
123 124
124 125
125 126 navafter.append(("tip", "tip"))
126 127
127 128 data = lambda i: {"label": i[0], "node": i[1]}
128 129 return ({'before': lambda **map: (data(i) for i in navbefore),
129 130 'after': lambda **map: (data(i) for i in navafter)},)
130 131
131 132 class filerevnav(revnav):
132 133
133 134 def __init__(self, repo, path):
134 135 """Navigation generation object
135 136
136 137 :repo: repo object we generate nav for
137 138 :path: path of the file we generate nav for
138 139 """
139 140 # used for iteration
140 141 self._changelog = repo.unfiltered().changelog
141 142 # used for hex generation
142 143 self._revlog = repo.file(path)
143 144
144 145 def hex(self, rev):
145 146 return hex(self._changelog.node(self._revlog.linkrev(rev)))
146 147
147 148 class _siblings(object):
148 149 def __init__(self, siblings=None, hiderev=None):
149 150 if siblings is None:
150 151 siblings = []
151 152 self.siblings = [s for s in siblings if s.node() != nullid]
152 153 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
153 154 self.siblings = []
154 155
155 156 def __iter__(self):
156 157 for s in self.siblings:
157 158 d = {
158 159 'node': s.hex(),
159 160 'rev': s.rev(),
160 161 'user': s.user(),
161 162 'date': s.date(),
162 163 'description': s.description(),
163 164 'branch': s.branch(),
164 165 }
165 166 if util.safehasattr(s, 'path'):
166 167 d['file'] = s.path()
167 168 yield d
168 169
169 170 def __len__(self):
170 171 return len(self.siblings)
171 172
172 173 def annotate(fctx, ui):
173 174 diffopts = patch.difffeatureopts(ui, untrusted=True,
174 175 section='annotate', whitespace=True)
175 176 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
176 177
177 178 def parents(ctx, hide=None):
178 179 if isinstance(ctx, context.basefilectx):
179 180 introrev = ctx.introrev()
180 181 if ctx.changectx().rev() != introrev:
181 182 return _siblings([ctx.repo()[introrev]], hide)
182 183 return _siblings(ctx.parents(), hide)
183 184
184 185 def children(ctx, hide=None):
185 186 return _siblings(ctx.children(), hide)
186 187
187 188 def renamelink(fctx):
188 189 r = fctx.renamed()
189 190 if r:
190 191 return [{'file': r[0], 'node': hex(r[1])}]
191 192 return []
192 193
193 194 def nodetagsdict(repo, node):
194 195 return [{"name": i} for i in repo.nodetags(node)]
195 196
196 197 def nodebookmarksdict(repo, node):
197 198 return [{"name": i} for i in repo.nodebookmarks(node)]
198 199
199 200 def nodebranchdict(repo, ctx):
200 201 branches = []
201 202 branch = ctx.branch()
202 203 # If this is an empty repo, ctx.node() == nullid,
203 204 # ctx.branch() == 'default'.
204 205 try:
205 206 branchnode = repo.branchtip(branch)
206 207 except error.RepoLookupError:
207 208 branchnode = None
208 209 if branchnode == ctx.node():
209 210 branches.append({"name": branch})
210 211 return branches
211 212
212 213 def nodeinbranch(repo, ctx):
213 214 branches = []
214 215 branch = ctx.branch()
215 216 try:
216 217 branchnode = repo.branchtip(branch)
217 218 except error.RepoLookupError:
218 219 branchnode = None
219 220 if branch != 'default' and branchnode != ctx.node():
220 221 branches.append({"name": branch})
221 222 return branches
222 223
223 224 def nodebranchnodefault(ctx):
224 225 branches = []
225 226 branch = ctx.branch()
226 227 if branch != 'default':
227 228 branches.append({"name": branch})
228 229 return branches
229 230
230 231 def showtag(repo, tmpl, t1, node=nullid, **args):
231 232 for t in repo.nodetags(node):
232 233 yield tmpl(t1, tag=t, **args)
233 234
234 235 def showbookmark(repo, tmpl, t1, node=nullid, **args):
235 236 for t in repo.nodebookmarks(node):
236 237 yield tmpl(t1, bookmark=t, **args)
237 238
238 239 def branchentries(repo, stripecount, limit=0):
239 240 tips = []
240 241 heads = repo.heads()
241 242 parity = paritygen(stripecount)
242 243 sortkey = lambda item: (not item[1], item[0].rev())
243 244
244 245 def entries(**map):
245 246 count = 0
246 247 if not tips:
247 248 for tag, hs, tip, closed in repo.branchmap().iterbranches():
248 249 tips.append((repo[tip], closed))
249 250 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
250 251 if limit > 0 and count >= limit:
251 252 return
252 253 count += 1
253 254 if closed:
254 255 status = 'closed'
255 256 elif ctx.node() not in heads:
256 257 status = 'inactive'
257 258 else:
258 259 status = 'open'
259 260 yield {
260 261 'parity': next(parity),
261 262 'branch': ctx.branch(),
262 263 'status': status,
263 264 'node': ctx.hex(),
264 265 'date': ctx.date()
265 266 }
266 267
267 268 return entries
268 269
269 270 def cleanpath(repo, path):
270 271 path = path.lstrip('/')
271 272 return pathutil.canonpath(repo.root, '', path)
272 273
273 274 def changeidctx(repo, changeid):
274 275 try:
275 276 ctx = repo[changeid]
276 277 except error.RepoError:
277 278 man = repo.manifestlog._revlog
278 279 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
279 280
280 281 return ctx
281 282
282 283 def changectx(repo, req):
283 284 changeid = "tip"
284 285 if 'node' in req.form:
285 286 changeid = req.form['node'][0]
286 287 ipos = changeid.find(':')
287 288 if ipos != -1:
288 289 changeid = changeid[(ipos + 1):]
289 290 elif 'manifest' in req.form:
290 291 changeid = req.form['manifest'][0]
291 292
292 293 return changeidctx(repo, changeid)
293 294
294 295 def basechangectx(repo, req):
295 296 if 'node' in req.form:
296 297 changeid = req.form['node'][0]
297 298 ipos = changeid.find(':')
298 299 if ipos != -1:
299 300 changeid = changeid[:ipos]
300 301 return changeidctx(repo, changeid)
301 302
302 303 return None
303 304
304 305 def filectx(repo, req):
305 306 if 'file' not in req.form:
306 307 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
307 308 path = cleanpath(repo, req.form['file'][0])
308 309 if 'node' in req.form:
309 310 changeid = req.form['node'][0]
310 311 elif 'filenode' in req.form:
311 312 changeid = req.form['filenode'][0]
312 313 else:
313 314 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
314 315 try:
315 316 fctx = repo[changeid][path]
316 317 except error.RepoError:
317 318 fctx = repo.filectx(path, fileid=changeid)
318 319
319 320 return fctx
320 321
321 322 def linerange(req):
322 323 linerange = req.form.get('linerange')
323 324 if linerange is None:
324 325 return None
325 326 if len(linerange) > 1:
326 327 raise ErrorResponse(HTTP_BAD_REQUEST,
327 328 'redundant linerange parameter')
328 329 try:
329 330 fromline, toline = map(int, linerange[0].split(':', 1))
330 331 except ValueError:
331 332 raise ErrorResponse(HTTP_BAD_REQUEST,
332 333 'invalid linerange parameter')
333 334 try:
334 335 return util.processlinerange(fromline, toline)
335 336 except error.ParseError as exc:
336 337 raise ErrorResponse(HTTP_BAD_REQUEST, str(exc))
337 338
338 339 def formatlinerange(fromline, toline):
339 340 return '%d:%d' % (fromline + 1, toline)
340 341
341 342 def commonentry(repo, ctx):
342 343 node = ctx.node()
343 344 return {
344 345 'rev': ctx.rev(),
345 346 'node': hex(node),
346 347 'author': ctx.user(),
347 348 'desc': ctx.description(),
348 349 'date': ctx.date(),
349 350 'extra': ctx.extra(),
350 351 'phase': ctx.phasestr(),
351 352 'branch': nodebranchnodefault(ctx),
352 353 'inbranch': nodeinbranch(repo, ctx),
353 354 'branches': nodebranchdict(repo, ctx),
354 355 'tags': nodetagsdict(repo, node),
355 356 'bookmarks': nodebookmarksdict(repo, node),
356 357 'parent': lambda **x: parents(ctx),
357 358 'child': lambda **x: children(ctx),
358 359 }
359 360
360 361 def changelistentry(web, ctx, tmpl):
361 362 '''Obtain a dictionary to be used for entries in a changelist.
362 363
363 364 This function is called when producing items for the "entries" list passed
364 365 to the "shortlog" and "changelog" templates.
365 366 '''
366 367 repo = web.repo
367 368 rev = ctx.rev()
368 369 n = ctx.node()
369 370 showtags = showtag(repo, tmpl, 'changelogtag', n)
370 371 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
371 372
372 373 entry = commonentry(repo, ctx)
373 374 entry.update(
374 375 allparents=lambda **x: parents(ctx),
375 376 parent=lambda **x: parents(ctx, rev - 1),
376 377 child=lambda **x: children(ctx, rev + 1),
377 378 changelogtag=showtags,
378 379 files=files,
379 380 )
380 381 return entry
381 382
382 383 def symrevorshortnode(req, ctx):
383 384 if 'node' in req.form:
384 385 return templatefilters.revescape(req.form['node'][0])
385 386 else:
386 387 return short(ctx.node())
387 388
388 389 def changesetentry(web, req, tmpl, ctx):
389 390 '''Obtain a dictionary to be used to render the "changeset" template.'''
390 391
391 392 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
392 393 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
393 394 ctx.node())
394 395 showbranch = nodebranchnodefault(ctx)
395 396
396 397 files = []
397 398 parity = paritygen(web.stripecount)
398 399 for blockno, f in enumerate(ctx.files()):
399 400 template = f in ctx and 'filenodelink' or 'filenolink'
400 401 files.append(tmpl(template,
401 402 node=ctx.hex(), file=f, blockno=blockno + 1,
402 403 parity=next(parity)))
403 404
404 405 basectx = basechangectx(web.repo, req)
405 406 if basectx is None:
406 407 basectx = ctx.p1()
407 408
408 409 style = web.config('web', 'style', 'paper')
409 410 if 'style' in req.form:
410 411 style = req.form['style'][0]
411 412
412 413 diff = diffs(web, tmpl, ctx, basectx, None, style)
413 414
414 415 parity = paritygen(web.stripecount)
415 416 diffstatsgen = diffstatgen(ctx, basectx)
416 417 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
417 418
418 419 return dict(
419 420 diff=diff,
420 421 symrev=symrevorshortnode(req, ctx),
421 422 basenode=basectx.hex(),
422 423 changesettag=showtags,
423 424 changesetbookmark=showbookmarks,
424 425 changesetbranch=showbranch,
425 426 files=files,
426 427 diffsummary=lambda **x: diffsummary(diffstatsgen),
427 428 diffstat=diffstats,
428 429 archives=web.archivelist(ctx.hex()),
429 430 **commonentry(web.repo, ctx))
430 431
431 432 def listfilediffs(tmpl, files, node, max):
432 433 for f in files[:max]:
433 434 yield tmpl('filedifflink', node=hex(node), file=f)
434 435 if len(files) > max:
435 436 yield tmpl('fileellipses')
436 437
437 438 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
438 439 lineidprefix=''):
439 440
440 441 def prettyprintlines(lines, blockno):
441 442 for lineno, l in enumerate(lines, 1):
442 443 difflineno = "%d.%d" % (blockno, lineno)
443 444 if l.startswith('+'):
444 445 ltype = "difflineplus"
445 446 elif l.startswith('-'):
446 447 ltype = "difflineminus"
447 448 elif l.startswith('@'):
448 449 ltype = "difflineat"
449 450 else:
450 451 ltype = "diffline"
451 452 yield tmpl(ltype,
452 453 line=l,
453 454 lineno=lineno,
454 455 lineid=lineidprefix + "l%s" % difflineno,
455 456 linenumber="% 8s" % difflineno)
456 457
457 458 repo = web.repo
458 459 if files:
459 460 m = match.exact(repo.root, repo.getcwd(), files)
460 461 else:
461 462 m = match.always(repo.root, repo.getcwd())
462 463
463 464 diffopts = patch.diffopts(repo.ui, untrusted=True)
464 465 node1 = basectx.node()
465 466 node2 = ctx.node()
466 467 parity = paritygen(web.stripecount)
467 468
468 469 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
469 470 for blockno, (header, hunks) in enumerate(diffhunks, 1):
470 471 if style != 'raw':
471 472 header = header[1:]
472 473 lines = [h + '\n' for h in header]
473 474 for hunkrange, hunklines in hunks:
474 475 if linerange is not None and hunkrange is not None:
475 476 s1, l1, s2, l2 = hunkrange
476 lb, ub = linerange
477 if not (lb < s2 + l2 and ub > s2):
477 if not mdiff.hunkinrange((s2, l2), linerange):
478 478 continue
479 479 lines.extend(hunklines)
480 480 if lines:
481 481 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
482 482 lines=prettyprintlines(lines, blockno))
483 483
484 484 def compare(tmpl, context, leftlines, rightlines):
485 485 '''Generator function that provides side-by-side comparison data.'''
486 486
487 487 def compline(type, leftlineno, leftline, rightlineno, rightline):
488 488 lineid = leftlineno and ("l%s" % leftlineno) or ''
489 489 lineid += rightlineno and ("r%s" % rightlineno) or ''
490 490 return tmpl('comparisonline',
491 491 type=type,
492 492 lineid=lineid,
493 493 leftlineno=leftlineno,
494 494 leftlinenumber="% 6s" % (leftlineno or ''),
495 495 leftline=leftline or '',
496 496 rightlineno=rightlineno,
497 497 rightlinenumber="% 6s" % (rightlineno or ''),
498 498 rightline=rightline or '')
499 499
500 500 def getblock(opcodes):
501 501 for type, llo, lhi, rlo, rhi in opcodes:
502 502 len1 = lhi - llo
503 503 len2 = rhi - rlo
504 504 count = min(len1, len2)
505 505 for i in xrange(count):
506 506 yield compline(type=type,
507 507 leftlineno=llo + i + 1,
508 508 leftline=leftlines[llo + i],
509 509 rightlineno=rlo + i + 1,
510 510 rightline=rightlines[rlo + i])
511 511 if len1 > len2:
512 512 for i in xrange(llo + count, lhi):
513 513 yield compline(type=type,
514 514 leftlineno=i + 1,
515 515 leftline=leftlines[i],
516 516 rightlineno=None,
517 517 rightline=None)
518 518 elif len2 > len1:
519 519 for i in xrange(rlo + count, rhi):
520 520 yield compline(type=type,
521 521 leftlineno=None,
522 522 leftline=None,
523 523 rightlineno=i + 1,
524 524 rightline=rightlines[i])
525 525
526 526 s = difflib.SequenceMatcher(None, leftlines, rightlines)
527 527 if context < 0:
528 528 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
529 529 else:
530 530 for oc in s.get_grouped_opcodes(n=context):
531 531 yield tmpl('comparisonblock', lines=getblock(oc))
532 532
533 533 def diffstatgen(ctx, basectx):
534 534 '''Generator function that provides the diffstat data.'''
535 535
536 536 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
537 537 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
538 538 while True:
539 539 yield stats, maxname, maxtotal, addtotal, removetotal, binary
540 540
541 541 def diffsummary(statgen):
542 542 '''Return a short summary of the diff.'''
543 543
544 544 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
545 545 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
546 546 len(stats), addtotal, removetotal)
547 547
548 548 def diffstat(tmpl, ctx, statgen, parity):
549 549 '''Return a diffstat template for each file in the diff.'''
550 550
551 551 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
552 552 files = ctx.files()
553 553
554 554 def pct(i):
555 555 if maxtotal == 0:
556 556 return 0
557 557 return (float(i) / maxtotal) * 100
558 558
559 559 fileno = 0
560 560 for filename, adds, removes, isbinary in stats:
561 561 template = filename in files and 'diffstatlink' or 'diffstatnolink'
562 562 total = adds + removes
563 563 fileno += 1
564 564 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
565 565 total=total, addpct=pct(adds), removepct=pct(removes),
566 566 parity=next(parity))
567 567
568 568 class sessionvars(object):
569 569 def __init__(self, vars, start='?'):
570 570 self.start = start
571 571 self.vars = vars
572 572 def __getitem__(self, key):
573 573 return self.vars[key]
574 574 def __setitem__(self, key, value):
575 575 self.vars[key] = value
576 576 def __copy__(self):
577 577 return sessionvars(copy.copy(self.vars), self.start)
578 578 def __iter__(self):
579 579 separator = self.start
580 580 for key, value in sorted(self.vars.iteritems()):
581 581 yield {'name': key, 'value': str(value), 'separator': separator}
582 582 separator = '&'
583 583
584 584 class wsgiui(uimod.ui):
585 585 # default termwidth breaks under mod_wsgi
586 586 def termwidth(self):
587 587 return 80
588 588
589 589 def getwebsubs(repo):
590 590 websubtable = []
591 591 websubdefs = repo.ui.configitems('websub')
592 592 # we must maintain interhg backwards compatibility
593 593 websubdefs += repo.ui.configitems('interhg')
594 594 for key, pattern in websubdefs:
595 595 # grab the delimiter from the character after the "s"
596 596 unesc = pattern[1]
597 597 delim = re.escape(unesc)
598 598
599 599 # identify portions of the pattern, taking care to avoid escaped
600 600 # delimiters. the replace format and flags are optional, but
601 601 # delimiters are required.
602 602 match = re.match(
603 603 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
604 604 % (delim, delim, delim), pattern)
605 605 if not match:
606 606 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
607 607 % (key, pattern))
608 608 continue
609 609
610 610 # we need to unescape the delimiter for regexp and format
611 611 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
612 612 regexp = delim_re.sub(unesc, match.group(1))
613 613 format = delim_re.sub(unesc, match.group(2))
614 614
615 615 # the pattern allows for 6 regexp flags, so set them if necessary
616 616 flagin = match.group(3)
617 617 flags = 0
618 618 if flagin:
619 619 for flag in flagin.upper():
620 620 flags |= re.__dict__[flag]
621 621
622 622 try:
623 623 regexp = re.compile(regexp, flags)
624 624 websubtable.append((regexp, format))
625 625 except re.error:
626 626 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
627 627 % (key, regexp))
628 628 return websubtable
@@ -1,459 +1,484 b''
1 1 # mdiff.py - diff and patch routines for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import re
11 11 import struct
12 12 import zlib
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 base85,
17 17 bdiff,
18 18 error,
19 19 mpatch,
20 20 pycompat,
21 21 util,
22 22 )
23 23
24 24 def splitnewlines(text):
25 25 '''like str.splitlines, but only split on newlines.'''
26 26 lines = [l + '\n' for l in text.split('\n')]
27 27 if lines:
28 28 if lines[-1] == '\n':
29 29 lines.pop()
30 30 else:
31 31 lines[-1] = lines[-1][:-1]
32 32 return lines
33 33
34 34 class diffopts(object):
35 35 '''context is the number of context lines
36 36 text treats all files as text
37 37 showfunc enables diff -p output
38 38 git enables the git extended patch format
39 39 nodates removes dates from diff headers
40 40 nobinary ignores binary files
41 41 noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode)
42 42 ignorews ignores all whitespace changes in the diff
43 43 ignorewsamount ignores changes in the amount of whitespace
44 44 ignoreblanklines ignores changes whose lines are all blank
45 45 upgrade generates git diffs to avoid data loss
46 46 '''
47 47
48 48 defaults = {
49 49 'context': 3,
50 50 'text': False,
51 51 'showfunc': False,
52 52 'git': False,
53 53 'nodates': False,
54 54 'nobinary': False,
55 55 'noprefix': False,
56 56 'index': 0,
57 57 'ignorews': False,
58 58 'ignorewsamount': False,
59 59 'ignoreblanklines': False,
60 60 'upgrade': False,
61 61 'showsimilarity': False,
62 62 }
63 63
64 64 def __init__(self, **opts):
65 65 opts = pycompat.byteskwargs(opts)
66 66 for k in self.defaults.keys():
67 67 v = opts.get(k)
68 68 if v is None:
69 69 v = self.defaults[k]
70 70 setattr(self, k, v)
71 71
72 72 try:
73 73 self.context = int(self.context)
74 74 except ValueError:
75 75 raise error.Abort(_('diff context lines count must be '
76 76 'an integer, not %r') % self.context)
77 77
78 78 def copy(self, **kwargs):
79 79 opts = dict((k, getattr(self, k)) for k in self.defaults)
80 80 opts.update(kwargs)
81 81 return diffopts(**opts)
82 82
83 83 defaultopts = diffopts()
84 84
85 85 def wsclean(opts, text, blank=True):
86 86 if opts.ignorews:
87 87 text = bdiff.fixws(text, 1)
88 88 elif opts.ignorewsamount:
89 89 text = bdiff.fixws(text, 0)
90 90 if blank and opts.ignoreblanklines:
91 91 text = re.sub('\n+', '\n', text).strip('\n')
92 92 return text
93 93
94 94 def splitblock(base1, lines1, base2, lines2, opts):
95 95 # The input lines matches except for interwoven blank lines. We
96 96 # transform it into a sequence of matching blocks and blank blocks.
97 97 lines1 = [(wsclean(opts, l) and 1 or 0) for l in lines1]
98 98 lines2 = [(wsclean(opts, l) and 1 or 0) for l in lines2]
99 99 s1, e1 = 0, len(lines1)
100 100 s2, e2 = 0, len(lines2)
101 101 while s1 < e1 or s2 < e2:
102 102 i1, i2, btype = s1, s2, '='
103 103 if (i1 >= e1 or lines1[i1] == 0
104 104 or i2 >= e2 or lines2[i2] == 0):
105 105 # Consume the block of blank lines
106 106 btype = '~'
107 107 while i1 < e1 and lines1[i1] == 0:
108 108 i1 += 1
109 109 while i2 < e2 and lines2[i2] == 0:
110 110 i2 += 1
111 111 else:
112 112 # Consume the matching lines
113 113 while i1 < e1 and lines1[i1] == 1 and lines2[i2] == 1:
114 114 i1 += 1
115 115 i2 += 1
116 116 yield [base1 + s1, base1 + i1, base2 + s2, base2 + i2], btype
117 117 s1 = i1
118 118 s2 = i2
119 119
120 def hunkinrange(hunk, linerange):
121 """Return True if `hunk` defined as (start, length) is in `linerange`
122 defined as (lowerbound, upperbound).
123
124 >>> hunkinrange((5, 10), (2, 7))
125 True
126 >>> hunkinrange((5, 10), (6, 12))
127 True
128 >>> hunkinrange((5, 10), (13, 17))
129 True
130 >>> hunkinrange((5, 10), (3, 17))
131 True
132 >>> hunkinrange((5, 10), (1, 3))
133 False
134 >>> hunkinrange((5, 10), (18, 20))
135 False
136 >>> hunkinrange((5, 10), (1, 5))
137 False
138 >>> hunkinrange((5, 10), (15, 27))
139 False
140 """
141 start, length = hunk
142 lowerbound, upperbound = linerange
143 return lowerbound < start + length and start < upperbound
144
120 145 def blocksinrange(blocks, rangeb):
121 146 """filter `blocks` like (a1, a2, b1, b2) from items outside line range
122 147 `rangeb` from ``(b1, b2)`` point of view.
123 148
124 149 Return `filteredblocks, rangea` where:
125 150
126 151 * `filteredblocks` is list of ``block = (a1, a2, b1, b2), stype`` items of
127 152 `blocks` that are inside `rangeb` from ``(b1, b2)`` point of view; a
128 153 block ``(b1, b2)`` being inside `rangeb` if
129 154 ``rangeb[0] < b2 and b1 < rangeb[1]``;
130 155 * `rangea` is the line range w.r.t. to ``(a1, a2)`` parts of `blocks`.
131 156 """
132 157 lbb, ubb = rangeb
133 158 lba, uba = None, None
134 159 filteredblocks = []
135 160 for block in blocks:
136 161 (a1, a2, b1, b2), stype = block
137 162 if lbb >= b1 and ubb <= b2 and stype == '=':
138 163 # rangeb is within a single "=" hunk, restrict back linerange1
139 164 # by offsetting rangeb
140 165 lba = lbb - b1 + a1
141 166 uba = ubb - b1 + a1
142 167 else:
143 168 if b1 <= lbb < b2:
144 169 if stype == '=':
145 170 lba = a2 - (b2 - lbb)
146 171 else:
147 172 lba = a1
148 173 if b1 < ubb <= b2:
149 174 if stype == '=':
150 175 uba = a1 + (ubb - b1)
151 176 else:
152 177 uba = a2
153 if lbb < b2 and b1 < ubb:
178 if hunkinrange((b1, (b2 - b1)), rangeb):
154 179 filteredblocks.append(block)
155 180 if lba is None or uba is None or uba < lba:
156 181 raise error.Abort(_('line range exceeds file size'))
157 182 return filteredblocks, (lba, uba)
158 183
159 184 def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
160 185 """Return (block, type) tuples, where block is an mdiff.blocks
161 186 line entry. type is '=' for blocks matching exactly one another
162 187 (bdiff blocks), '!' for non-matching blocks and '~' for blocks
163 188 matching only after having filtered blank lines.
164 189 line1 and line2 are text1 and text2 split with splitnewlines() if
165 190 they are already available.
166 191 """
167 192 if opts is None:
168 193 opts = defaultopts
169 194 if opts.ignorews or opts.ignorewsamount:
170 195 text1 = wsclean(opts, text1, False)
171 196 text2 = wsclean(opts, text2, False)
172 197 diff = bdiff.blocks(text1, text2)
173 198 for i, s1 in enumerate(diff):
174 199 # The first match is special.
175 200 # we've either found a match starting at line 0 or a match later
176 201 # in the file. If it starts later, old and new below will both be
177 202 # empty and we'll continue to the next match.
178 203 if i > 0:
179 204 s = diff[i - 1]
180 205 else:
181 206 s = [0, 0, 0, 0]
182 207 s = [s[1], s1[0], s[3], s1[2]]
183 208
184 209 # bdiff sometimes gives huge matches past eof, this check eats them,
185 210 # and deals with the special first match case described above
186 211 if s[0] != s[1] or s[2] != s[3]:
187 212 type = '!'
188 213 if opts.ignoreblanklines:
189 214 if lines1 is None:
190 215 lines1 = splitnewlines(text1)
191 216 if lines2 is None:
192 217 lines2 = splitnewlines(text2)
193 218 old = wsclean(opts, "".join(lines1[s[0]:s[1]]))
194 219 new = wsclean(opts, "".join(lines2[s[2]:s[3]]))
195 220 if old == new:
196 221 type = '~'
197 222 yield s, type
198 223 yield s1, '='
199 224
200 225 def unidiff(a, ad, b, bd, fn1, fn2, opts=defaultopts):
201 226 """Return a unified diff as a (headers, hunks) tuple.
202 227
203 228 If the diff is not null, `headers` is a list with unified diff header
204 229 lines "--- <original>" and "+++ <new>" and `hunks` is a generator yielding
205 230 (hunkrange, hunklines) coming from _unidiff().
206 231 Otherwise, `headers` and `hunks` are empty.
207 232 """
208 233 def datetag(date, fn=None):
209 234 if not opts.git and not opts.nodates:
210 235 return '\t%s' % date
211 236 if fn and ' ' in fn:
212 237 return '\t'
213 238 return ''
214 239
215 240 sentinel = [], ()
216 241 if not a and not b:
217 242 return sentinel
218 243
219 244 if opts.noprefix:
220 245 aprefix = bprefix = ''
221 246 else:
222 247 aprefix = 'a/'
223 248 bprefix = 'b/'
224 249
225 250 epoch = util.datestr((0, 0))
226 251
227 252 fn1 = util.pconvert(fn1)
228 253 fn2 = util.pconvert(fn2)
229 254
230 255 def checknonewline(lines):
231 256 for text in lines:
232 257 if text[-1:] != '\n':
233 258 text += "\n\ No newline at end of file\n"
234 259 yield text
235 260
236 261 if not opts.text and (util.binary(a) or util.binary(b)):
237 262 if a and b and len(a) == len(b) and a == b:
238 263 return sentinel
239 264 headerlines = []
240 265 hunks = (None, ['Binary file %s has changed\n' % fn1]),
241 266 elif not a:
242 267 b = splitnewlines(b)
243 268 if a is None:
244 269 l1 = '--- /dev/null%s' % datetag(epoch)
245 270 else:
246 271 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
247 272 l2 = "+++ %s%s" % (bprefix + fn2, datetag(bd, fn2))
248 273 headerlines = [l1, l2]
249 274 size = len(b)
250 275 hunkrange = (0, 0, 1, size)
251 276 hunklines = ["@@ -0,0 +1,%d @@\n" % size] + ["+" + e for e in b]
252 277 hunks = (hunkrange, checknonewline(hunklines)),
253 278 elif not b:
254 279 a = splitnewlines(a)
255 280 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
256 281 if b is None:
257 282 l2 = '+++ /dev/null%s' % datetag(epoch)
258 283 else:
259 284 l2 = "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2))
260 285 headerlines = [l1, l2]
261 286 size = len(a)
262 287 hunkrange = (1, size, 0, 0)
263 288 hunklines = ["@@ -1,%d +0,0 @@\n" % size] + ["-" + e for e in a]
264 289 hunks = (hunkrange, checknonewline(hunklines)),
265 290 else:
266 291 diffhunks = _unidiff(a, b, opts=opts)
267 292 try:
268 293 hunkrange, hunklines = next(diffhunks)
269 294 except StopIteration:
270 295 return sentinel
271 296
272 297 headerlines = [
273 298 "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1)),
274 299 "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2)),
275 300 ]
276 301 def rewindhunks():
277 302 yield hunkrange, checknonewline(hunklines)
278 303 for hr, hl in diffhunks:
279 304 yield hr, checknonewline(hl)
280 305
281 306 hunks = rewindhunks()
282 307
283 308 return headerlines, hunks
284 309
285 310 def _unidiff(t1, t2, opts=defaultopts):
286 311 """Yield hunks of a headerless unified diff from t1 and t2 texts.
287 312
288 313 Each hunk consists of a (hunkrange, hunklines) tuple where `hunkrange` is a
289 314 tuple (s1, l1, s2, l2) representing the range information of the hunk to
290 315 form the '@@ -s1,l1 +s2,l2 @@' header and `hunklines` is a list of lines
291 316 of the hunk combining said header followed by line additions and
292 317 deletions.
293 318 """
294 319 l1 = splitnewlines(t1)
295 320 l2 = splitnewlines(t2)
296 321 def contextend(l, len):
297 322 ret = l + opts.context
298 323 if ret > len:
299 324 ret = len
300 325 return ret
301 326
302 327 def contextstart(l):
303 328 ret = l - opts.context
304 329 if ret < 0:
305 330 return 0
306 331 return ret
307 332
308 333 lastfunc = [0, '']
309 334 def yieldhunk(hunk):
310 335 (astart, a2, bstart, b2, delta) = hunk
311 336 aend = contextend(a2, len(l1))
312 337 alen = aend - astart
313 338 blen = b2 - bstart + aend - a2
314 339
315 340 func = ""
316 341 if opts.showfunc:
317 342 lastpos, func = lastfunc
318 343 # walk backwards from the start of the context up to the start of
319 344 # the previous hunk context until we find a line starting with an
320 345 # alphanumeric char.
321 346 for i in xrange(astart - 1, lastpos - 1, -1):
322 347 if l1[i][0].isalnum():
323 348 func = ' ' + l1[i].rstrip()[:40]
324 349 lastfunc[1] = func
325 350 break
326 351 # by recording this hunk's starting point as the next place to
327 352 # start looking for function lines, we avoid reading any line in
328 353 # the file more than once.
329 354 lastfunc[0] = astart
330 355
331 356 # zero-length hunk ranges report their start line as one less
332 357 if alen:
333 358 astart += 1
334 359 if blen:
335 360 bstart += 1
336 361
337 362 hunkrange = astart, alen, bstart, blen
338 363 hunklines = (
339 364 ["@@ -%d,%d +%d,%d @@%s\n" % (hunkrange + (func,))]
340 365 + delta
341 366 + [' ' + l1[x] for x in xrange(a2, aend)]
342 367 )
343 368 yield hunkrange, hunklines
344 369
345 370 # bdiff.blocks gives us the matching sequences in the files. The loop
346 371 # below finds the spaces between those matching sequences and translates
347 372 # them into diff output.
348 373 #
349 374 hunk = None
350 375 ignoredlines = 0
351 376 for s, stype in allblocks(t1, t2, opts, l1, l2):
352 377 a1, a2, b1, b2 = s
353 378 if stype != '!':
354 379 if stype == '~':
355 380 # The diff context lines are based on t1 content. When
356 381 # blank lines are ignored, the new lines offsets must
357 382 # be adjusted as if equivalent blocks ('~') had the
358 383 # same sizes on both sides.
359 384 ignoredlines += (b2 - b1) - (a2 - a1)
360 385 continue
361 386 delta = []
362 387 old = l1[a1:a2]
363 388 new = l2[b1:b2]
364 389
365 390 b1 -= ignoredlines
366 391 b2 -= ignoredlines
367 392 astart = contextstart(a1)
368 393 bstart = contextstart(b1)
369 394 prev = None
370 395 if hunk:
371 396 # join with the previous hunk if it falls inside the context
372 397 if astart < hunk[1] + opts.context + 1:
373 398 prev = hunk
374 399 astart = hunk[1]
375 400 bstart = hunk[3]
376 401 else:
377 402 for x in yieldhunk(hunk):
378 403 yield x
379 404 if prev:
380 405 # we've joined the previous hunk, record the new ending points.
381 406 hunk[1] = a2
382 407 hunk[3] = b2
383 408 delta = hunk[4]
384 409 else:
385 410 # create a new hunk
386 411 hunk = [astart, a2, bstart, b2, delta]
387 412
388 413 delta[len(delta):] = [' ' + x for x in l1[astart:a1]]
389 414 delta[len(delta):] = ['-' + x for x in old]
390 415 delta[len(delta):] = ['+' + x for x in new]
391 416
392 417 if hunk:
393 418 for x in yieldhunk(hunk):
394 419 yield x
395 420
396 421 def b85diff(to, tn):
397 422 '''print base85-encoded binary diff'''
398 423 def fmtline(line):
399 424 l = len(line)
400 425 if l <= 26:
401 426 l = chr(ord('A') + l - 1)
402 427 else:
403 428 l = chr(l - 26 + ord('a') - 1)
404 429 return '%c%s\n' % (l, base85.b85encode(line, True))
405 430
406 431 def chunk(text, csize=52):
407 432 l = len(text)
408 433 i = 0
409 434 while i < l:
410 435 yield text[i:i + csize]
411 436 i += csize
412 437
413 438 if to is None:
414 439 to = ''
415 440 if tn is None:
416 441 tn = ''
417 442
418 443 if to == tn:
419 444 return ''
420 445
421 446 # TODO: deltas
422 447 ret = []
423 448 ret.append('GIT binary patch\n')
424 449 ret.append('literal %s\n' % len(tn))
425 450 for l in chunk(zlib.compress(tn)):
426 451 ret.append(fmtline(l))
427 452 ret.append('\n')
428 453
429 454 return ''.join(ret)
430 455
431 456 def patchtext(bin):
432 457 pos = 0
433 458 t = []
434 459 while pos < len(bin):
435 460 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
436 461 pos += 12
437 462 t.append(bin[pos:pos + l])
438 463 pos += l
439 464 return "".join(t)
440 465
441 466 def patch(a, bin):
442 467 if len(a) == 0:
443 468 # skip over trivial delta header
444 469 return util.buffer(bin, 12)
445 470 return mpatch.patches(a, [bin])
446 471
447 472 # similar to difflib.SequenceMatcher.get_matching_blocks
448 473 def get_matching_blocks(a, b):
449 474 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
450 475
451 476 def trivialdiffheader(length):
452 477 return struct.pack(">lll", 0, 0, length) if length else ''
453 478
454 479 def replacediffheader(oldlen, newlen):
455 480 return struct.pack(">lll", 0, oldlen, newlen)
456 481
457 482 patches = mpatch.patches
458 483 patchedsize = mpatch.patchedsize
459 484 textdiff = bdiff.bdiff
@@ -1,55 +1,56 b''
1 1 # this is hack to make sure no escape characters are inserted into the output
2 2
3 3 from __future__ import absolute_import
4 4
5 5 import doctest
6 6 import os
7 7 import sys
8 8
9 9 ispy3 = (sys.version_info[0] >= 3)
10 10
11 11 if 'TERM' in os.environ:
12 12 del os.environ['TERM']
13 13
14 14 # TODO: migrate doctests to py3 and enable them on both versions
15 15 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=False):
16 16 if not (not ispy3 and py2 or ispy3 and py3):
17 17 return
18 18 __import__(name)
19 19 mod = sys.modules[name]
20 20 if testtarget is not None:
21 21 mod = getattr(mod, testtarget)
22 22 doctest.testmod(mod, optionflags=optionflags)
23 23
24 24 testmod('mercurial.changegroup')
25 25 testmod('mercurial.changelog')
26 26 testmod('mercurial.color')
27 27 testmod('mercurial.config')
28 28 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
29 29 testmod('mercurial.dispatch')
30 30 testmod('mercurial.encoding')
31 31 testmod('mercurial.formatter')
32 32 testmod('mercurial.hg')
33 33 testmod('mercurial.hgweb.hgwebdir_mod')
34 34 testmod('mercurial.match')
35 testmod('mercurial.mdiff')
35 36 testmod('mercurial.minirst')
36 37 testmod('mercurial.patch')
37 38 testmod('mercurial.pathutil')
38 39 testmod('mercurial.parser')
39 40 testmod('mercurial.pycompat', py3=True)
40 41 testmod('mercurial.revsetlang')
41 42 testmod('mercurial.smartset')
42 43 testmod('mercurial.store')
43 44 testmod('mercurial.subrepo')
44 45 testmod('mercurial.templatefilters')
45 46 testmod('mercurial.templater')
46 47 testmod('mercurial.ui')
47 48 testmod('mercurial.url')
48 49 testmod('mercurial.util')
49 50 testmod('mercurial.util', testtarget='platform')
50 51 testmod('hgext.convert.convcmd')
51 52 testmod('hgext.convert.cvsps')
52 53 testmod('hgext.convert.filemap')
53 54 testmod('hgext.convert.p4')
54 55 testmod('hgext.convert.subversion')
55 56 testmod('hgext.mq')
General Comments 0
You need to be logged in to leave comments. Login now