##// END OF EJS Templates
py3: use '%d' for integers instead of '%s'...
Pulkit Goyal -
r34507:1d804c22 default
parent child Browse files
Show More
@@ -1,779 +1,779 b''
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 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 filecmp
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import nullid, short
17 17
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 formatter,
22 22 match,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 simplemerge,
27 27 tagmerge,
28 28 templatekw,
29 29 templater,
30 30 util,
31 31 )
32 32
33 33 def _toolstr(ui, tool, part, default=""):
34 34 return ui.config("merge-tools", tool + "." + part, default)
35 35
36 36 def _toolbool(ui, tool, part, default=False):
37 37 return ui.configbool("merge-tools", tool + "." + part, default)
38 38
39 39 def _toollist(ui, tool, part, default=None):
40 40 if default is None:
41 41 default = []
42 42 return ui.configlist("merge-tools", tool + "." + part, default)
43 43
44 44 internals = {}
45 45 # Merge tools to document.
46 46 internalsdoc = {}
47 47
48 48 internaltool = registrar.internalmerge()
49 49
50 50 # internal tool merge types
51 51 nomerge = internaltool.nomerge
52 52 mergeonly = internaltool.mergeonly # just the full merge, no premerge
53 53 fullmerge = internaltool.fullmerge # both premerge and merge
54 54
55 55 _localchangedotherdeletedmsg = _(
56 56 "local%(l)s changed %(fd)s which other%(o)s deleted\n"
57 57 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
58 58 "$$ &Changed $$ &Delete $$ &Unresolved")
59 59
60 60 _otherchangedlocaldeletedmsg = _(
61 61 "other%(o)s changed %(fd)s which local%(l)s deleted\n"
62 62 "use (c)hanged version, leave (d)eleted, or "
63 63 "leave (u)nresolved?"
64 64 "$$ &Changed $$ &Deleted $$ &Unresolved")
65 65
66 66 class absentfilectx(object):
67 67 """Represents a file that's ostensibly in a context but is actually not
68 68 present in it.
69 69
70 70 This is here because it's very specific to the filemerge code for now --
71 71 other code is likely going to break with the values this returns."""
72 72 def __init__(self, ctx, f):
73 73 self._ctx = ctx
74 74 self._f = f
75 75
76 76 def path(self):
77 77 return self._f
78 78
79 79 def size(self):
80 80 return None
81 81
82 82 def data(self):
83 83 return None
84 84
85 85 def filenode(self):
86 86 return nullid
87 87
88 88 _customcmp = True
89 89 def cmp(self, fctx):
90 90 """compare with other file context
91 91
92 92 returns True if different from fctx.
93 93 """
94 94 return not (fctx.isabsent() and
95 95 fctx.ctx() == self.ctx() and
96 96 fctx.path() == self.path())
97 97
98 98 def flags(self):
99 99 return ''
100 100
101 101 def changectx(self):
102 102 return self._ctx
103 103
104 104 def isbinary(self):
105 105 return False
106 106
107 107 def isabsent(self):
108 108 return True
109 109
110 110 def _findtool(ui, tool):
111 111 if tool in internals:
112 112 return tool
113 113 return findexternaltool(ui, tool)
114 114
115 115 def findexternaltool(ui, tool):
116 116 for kn in ("regkey", "regkeyalt"):
117 117 k = _toolstr(ui, tool, kn)
118 118 if not k:
119 119 continue
120 120 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
121 121 if p:
122 122 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
123 123 if p:
124 124 return p
125 125 exe = _toolstr(ui, tool, "executable", tool)
126 126 return util.findexe(util.expandpath(exe))
127 127
128 128 def _picktool(repo, ui, path, binary, symlink, changedelete):
129 129 def supportscd(tool):
130 130 return tool in internals and internals[tool].mergetype == nomerge
131 131
132 132 def check(tool, pat, symlink, binary, changedelete):
133 133 tmsg = tool
134 134 if pat:
135 135 tmsg = _("%s (for pattern %s)") % (tool, pat)
136 136 if not _findtool(ui, tool):
137 137 if pat: # explicitly requested tool deserves a warning
138 138 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
139 139 else: # configured but non-existing tools are more silent
140 140 ui.note(_("couldn't find merge tool %s\n") % tmsg)
141 141 elif symlink and not _toolbool(ui, tool, "symlink"):
142 142 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
143 143 elif binary and not _toolbool(ui, tool, "binary"):
144 144 ui.warn(_("tool %s can't handle binary\n") % tmsg)
145 145 elif changedelete and not supportscd(tool):
146 146 # the nomerge tools are the only tools that support change/delete
147 147 # conflicts
148 148 pass
149 149 elif not util.gui() and _toolbool(ui, tool, "gui"):
150 150 ui.warn(_("tool %s requires a GUI\n") % tmsg)
151 151 else:
152 152 return True
153 153 return False
154 154
155 155 # internal config: ui.forcemerge
156 156 # forcemerge comes from command line arguments, highest priority
157 157 force = ui.config('ui', 'forcemerge')
158 158 if force:
159 159 toolpath = _findtool(ui, force)
160 160 if changedelete and not supportscd(toolpath):
161 161 return ":prompt", None
162 162 else:
163 163 if toolpath:
164 164 return (force, util.shellquote(toolpath))
165 165 else:
166 166 # mimic HGMERGE if given tool not found
167 167 return (force, force)
168 168
169 169 # HGMERGE takes next precedence
170 170 hgmerge = encoding.environ.get("HGMERGE")
171 171 if hgmerge:
172 172 if changedelete and not supportscd(hgmerge):
173 173 return ":prompt", None
174 174 else:
175 175 return (hgmerge, hgmerge)
176 176
177 177 # then patterns
178 178 for pat, tool in ui.configitems("merge-patterns"):
179 179 mf = match.match(repo.root, '', [pat])
180 180 if mf(path) and check(tool, pat, symlink, False, changedelete):
181 181 toolpath = _findtool(ui, tool)
182 182 return (tool, util.shellquote(toolpath))
183 183
184 184 # then merge tools
185 185 tools = {}
186 186 disabled = set()
187 187 for k, v in ui.configitems("merge-tools"):
188 188 t = k.split('.')[0]
189 189 if t not in tools:
190 190 tools[t] = int(_toolstr(ui, t, "priority", "0"))
191 191 if _toolbool(ui, t, "disabled", False):
192 192 disabled.add(t)
193 193 names = tools.keys()
194 194 tools = sorted([(-p, tool) for tool, p in tools.items()
195 195 if tool not in disabled])
196 196 uimerge = ui.config("ui", "merge")
197 197 if uimerge:
198 198 # external tools defined in uimerge won't be able to handle
199 199 # change/delete conflicts
200 200 if uimerge not in names and not changedelete:
201 201 return (uimerge, uimerge)
202 202 tools.insert(0, (None, uimerge)) # highest priority
203 203 tools.append((None, "hgmerge")) # the old default, if found
204 204 for p, t in tools:
205 205 if check(t, None, symlink, binary, changedelete):
206 206 toolpath = _findtool(ui, t)
207 207 return (t, util.shellquote(toolpath))
208 208
209 209 # internal merge or prompt as last resort
210 210 if symlink or binary or changedelete:
211 211 if not changedelete and len(tools):
212 212 # any tool is rejected by capability for symlink or binary
213 213 ui.warn(_("no tool found to merge %s\n") % path)
214 214 return ":prompt", None
215 215 return ":merge", None
216 216
217 217 def _eoltype(data):
218 218 "Guess the EOL type of a file"
219 219 if '\0' in data: # binary
220 220 return None
221 221 if '\r\n' in data: # Windows
222 222 return '\r\n'
223 223 if '\r' in data: # Old Mac
224 224 return '\r'
225 225 if '\n' in data: # UNIX
226 226 return '\n'
227 227 return None # unknown
228 228
229 229 def _matcheol(file, origfile):
230 230 "Convert EOL markers in a file to match origfile"
231 231 tostyle = _eoltype(util.readfile(origfile))
232 232 if tostyle:
233 233 data = util.readfile(file)
234 234 style = _eoltype(data)
235 235 if style:
236 236 newdata = data.replace(style, tostyle)
237 237 if newdata != data:
238 238 util.writefile(file, newdata)
239 239
240 240 @internaltool('prompt', nomerge)
241 241 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
242 242 """Asks the user which of the local `p1()` or the other `p2()` version to
243 243 keep as the merged version."""
244 244 ui = repo.ui
245 245 fd = fcd.path()
246 246
247 247 prompts = partextras(labels)
248 248 prompts['fd'] = fd
249 249 try:
250 250 if fco.isabsent():
251 251 index = ui.promptchoice(
252 252 _localchangedotherdeletedmsg % prompts, 2)
253 253 choice = ['local', 'other', 'unresolved'][index]
254 254 elif fcd.isabsent():
255 255 index = ui.promptchoice(
256 256 _otherchangedlocaldeletedmsg % prompts, 2)
257 257 choice = ['other', 'local', 'unresolved'][index]
258 258 else:
259 259 index = ui.promptchoice(
260 260 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
261 261 " for %(fd)s?"
262 262 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
263 263 choice = ['local', 'other', 'unresolved'][index]
264 264
265 265 if choice == 'other':
266 266 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
267 267 labels)
268 268 elif choice == 'local':
269 269 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
270 270 labels)
271 271 elif choice == 'unresolved':
272 272 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
273 273 labels)
274 274 except error.ResponseExpected:
275 275 ui.write("\n")
276 276 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
277 277 labels)
278 278
279 279 @internaltool('local', nomerge)
280 280 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
281 281 """Uses the local `p1()` version of files as the merged version."""
282 282 return 0, fcd.isabsent()
283 283
284 284 @internaltool('other', nomerge)
285 285 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
286 286 """Uses the other `p2()` version of files as the merged version."""
287 287 if fco.isabsent():
288 288 # local changed, remote deleted -- 'deleted' picked
289 289 _underlyingfctxifabsent(fcd).remove()
290 290 deleted = True
291 291 else:
292 292 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
293 293 deleted = False
294 294 return 0, deleted
295 295
296 296 @internaltool('fail', nomerge)
297 297 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
298 298 """
299 299 Rather than attempting to merge files that were modified on both
300 300 branches, it marks them as unresolved. The resolve command must be
301 301 used to resolve these conflicts."""
302 302 # for change/delete conflicts write out the changed version, then fail
303 303 if fcd.isabsent():
304 304 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
305 305 return 1, False
306 306
307 307 def _underlyingfctxifabsent(filectx):
308 308 """Sometimes when resolving, our fcd is actually an absentfilectx, but
309 309 we want to write to it (to do the resolve). This helper returns the
310 310 underyling workingfilectx in that case.
311 311 """
312 312 if filectx.isabsent():
313 313 return filectx.changectx()[filectx.path()]
314 314 else:
315 315 return filectx
316 316
317 317 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
318 318 tool, toolpath, binary, symlink = toolconf
319 319 if symlink or fcd.isabsent() or fco.isabsent():
320 320 return 1
321 321 unused, unused, unused, back = files
322 322
323 323 ui = repo.ui
324 324
325 325 validkeep = ['keep', 'keep-merge3']
326 326
327 327 # do we attempt to simplemerge first?
328 328 try:
329 329 premerge = _toolbool(ui, tool, "premerge", not binary)
330 330 except error.ConfigError:
331 331 premerge = _toolstr(ui, tool, "premerge").lower()
332 332 if premerge not in validkeep:
333 333 _valid = ', '.join(["'" + v + "'" for v in validkeep])
334 334 raise error.ConfigError(_("%s.premerge not valid "
335 335 "('%s' is neither boolean nor %s)") %
336 336 (tool, premerge, _valid))
337 337
338 338 if premerge:
339 339 if premerge == 'keep-merge3':
340 340 if not labels:
341 341 labels = _defaultconflictlabels
342 342 if len(labels) < 3:
343 343 labels.append('base')
344 344 r = simplemerge.simplemerge(ui, fcd, fca, fco, quiet=True, label=labels)
345 345 if not r:
346 346 ui.debug(" premerge successful\n")
347 347 return 0
348 348 if premerge not in validkeep:
349 349 # restore from backup and try again
350 350 _restorebackup(fcd, back)
351 351 return 1 # continue merging
352 352
353 353 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
354 354 tool, toolpath, binary, symlink = toolconf
355 355 if symlink:
356 356 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
357 357 'for %s\n') % (tool, fcd.path()))
358 358 return False
359 359 if fcd.isabsent() or fco.isabsent():
360 360 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
361 361 'conflict for %s\n') % (tool, fcd.path()))
362 362 return False
363 363 return True
364 364
365 365 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
366 366 """
367 367 Uses the internal non-interactive simple merge algorithm for merging
368 368 files. It will fail if there are any conflicts and leave markers in
369 369 the partially merged file. Markers will have two sections, one for each side
370 370 of merge, unless mode equals 'union' which suppresses the markers."""
371 371 ui = repo.ui
372 372
373 373 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
374 374 return True, r, False
375 375
376 376 @internaltool('union', fullmerge,
377 377 _("warning: conflicts while merging %s! "
378 378 "(edit, then use 'hg resolve --mark')\n"),
379 379 precheck=_mergecheck)
380 380 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
381 381 """
382 382 Uses the internal non-interactive simple merge algorithm for merging
383 383 files. It will use both left and right sides for conflict regions.
384 384 No markers are inserted."""
385 385 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
386 386 files, labels, 'union')
387 387
388 388 @internaltool('merge', fullmerge,
389 389 _("warning: conflicts while merging %s! "
390 390 "(edit, then use 'hg resolve --mark')\n"),
391 391 precheck=_mergecheck)
392 392 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
393 393 """
394 394 Uses the internal non-interactive simple merge algorithm for merging
395 395 files. It will fail if there are any conflicts and leave markers in
396 396 the partially merged file. Markers will have two sections, one for each side
397 397 of merge."""
398 398 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
399 399 files, labels, 'merge')
400 400
401 401 @internaltool('merge3', fullmerge,
402 402 _("warning: conflicts while merging %s! "
403 403 "(edit, then use 'hg resolve --mark')\n"),
404 404 precheck=_mergecheck)
405 405 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
406 406 """
407 407 Uses the internal non-interactive simple merge algorithm for merging
408 408 files. It will fail if there are any conflicts and leave markers in
409 409 the partially merged file. Marker will have three sections, one from each
410 410 side of the merge and one for the base content."""
411 411 if not labels:
412 412 labels = _defaultconflictlabels
413 413 if len(labels) < 3:
414 414 labels.append('base')
415 415 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
416 416
417 417 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
418 418 labels=None, localorother=None):
419 419 """
420 420 Generic driver for _imergelocal and _imergeother
421 421 """
422 422 assert localorother is not None
423 423 tool, toolpath, binary, symlink = toolconf
424 424 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
425 425 localorother=localorother)
426 426 return True, r
427 427
428 428 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
429 429 def _imergelocal(*args, **kwargs):
430 430 """
431 431 Like :merge, but resolve all conflicts non-interactively in favor
432 432 of the local `p1()` changes."""
433 433 success, status = _imergeauto(localorother='local', *args, **kwargs)
434 434 return success, status, False
435 435
436 436 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
437 437 def _imergeother(*args, **kwargs):
438 438 """
439 439 Like :merge, but resolve all conflicts non-interactively in favor
440 440 of the other `p2()` changes."""
441 441 success, status = _imergeauto(localorother='other', *args, **kwargs)
442 442 return success, status, False
443 443
444 444 @internaltool('tagmerge', mergeonly,
445 445 _("automatic tag merging of %s failed! "
446 446 "(use 'hg resolve --tool :merge' or another merge "
447 447 "tool of your choice)\n"))
448 448 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
449 449 """
450 450 Uses the internal tag merge algorithm (experimental).
451 451 """
452 452 success, status = tagmerge.merge(repo, fcd, fco, fca)
453 453 return success, status, False
454 454
455 455 @internaltool('dump', fullmerge)
456 456 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
457 457 """
458 458 Creates three versions of the files to merge, containing the
459 459 contents of local, other and base. These files can then be used to
460 460 perform a merge manually. If the file to be merged is named
461 461 ``a.txt``, these files will accordingly be named ``a.txt.local``,
462 462 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
463 463 same directory as ``a.txt``.
464 464
465 465 This implies permerge. Therefore, files aren't dumped, if premerge
466 466 runs successfully. Use :forcedump to forcibly write files out.
467 467 """
468 468 a = _workingpath(repo, fcd)
469 469 fd = fcd.path()
470 470
471 471 util.writefile(a + ".local", fcd.decodeddata())
472 472 repo.wwrite(fd + ".other", fco.data(), fco.flags())
473 473 repo.wwrite(fd + ".base", fca.data(), fca.flags())
474 474 return False, 1, False
475 475
476 476 @internaltool('forcedump', mergeonly)
477 477 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
478 478 labels=None):
479 479 """
480 480 Creates three versions of the files as same as :dump, but omits premerge.
481 481 """
482 482 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
483 483 labels=labels)
484 484
485 485 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
486 486 tool, toolpath, binary, symlink = toolconf
487 487 if fcd.isabsent() or fco.isabsent():
488 488 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
489 489 'for %s\n') % (tool, fcd.path()))
490 490 return False, 1, None
491 491 unused, unused, unused, back = files
492 492 a = _workingpath(repo, fcd)
493 493 b, c = _maketempfiles(repo, fco, fca)
494 494 try:
495 495 out = ""
496 496 env = {'HG_FILE': fcd.path(),
497 497 'HG_MY_NODE': short(mynode),
498 498 'HG_OTHER_NODE': str(fco.changectx()),
499 499 'HG_BASE_NODE': str(fca.changectx()),
500 500 'HG_MY_ISLINK': 'l' in fcd.flags(),
501 501 'HG_OTHER_ISLINK': 'l' in fco.flags(),
502 502 'HG_BASE_ISLINK': 'l' in fca.flags(),
503 503 }
504 504 ui = repo.ui
505 505
506 506 args = _toolstr(ui, tool, "args", '$local $base $other')
507 507 if "$output" in args:
508 508 out, a = a, back # read input from backup, write to original
509 509 replace = {'local': a, 'base': b, 'other': c, 'output': out}
510 510 args = util.interpolate(r'\$', replace, args,
511 511 lambda s: util.shellquote(util.localpath(s)))
512 512 cmd = toolpath + ' ' + args
513 513 if _toolbool(ui, tool, "gui"):
514 514 repo.ui.status(_('running merge tool %s for file %s\n') %
515 515 (tool, fcd.path()))
516 516 repo.ui.debug('launching merge tool: %s\n' % cmd)
517 517 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
518 repo.ui.debug('merge tool returned: %s\n' % r)
518 repo.ui.debug('merge tool returned: %d\n' % r)
519 519 return True, r, False
520 520 finally:
521 521 util.unlink(b)
522 522 util.unlink(c)
523 523
524 524 def _formatconflictmarker(repo, ctx, template, label, pad):
525 525 """Applies the given template to the ctx, prefixed by the label.
526 526
527 527 Pad is the minimum width of the label prefix, so that multiple markers
528 528 can have aligned templated parts.
529 529 """
530 530 if ctx.node() is None:
531 531 ctx = ctx.p1()
532 532
533 533 props = templatekw.keywords.copy()
534 534 props['templ'] = template
535 535 props['ctx'] = ctx
536 536 props['repo'] = repo
537 537 templateresult = template.render(props)
538 538
539 539 label = ('%s:' % label).ljust(pad + 1)
540 540 mark = '%s %s' % (label, templateresult)
541 541
542 542 if mark:
543 543 mark = mark.splitlines()[0] # split for safety
544 544
545 545 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
546 546 return util.ellipsis(mark, 80 - 8)
547 547
548 548 _defaultconflictlabels = ['local', 'other']
549 549
550 550 def _formatlabels(repo, fcd, fco, fca, labels):
551 551 """Formats the given labels using the conflict marker template.
552 552
553 553 Returns a list of formatted labels.
554 554 """
555 555 cd = fcd.changectx()
556 556 co = fco.changectx()
557 557 ca = fca.changectx()
558 558
559 559 ui = repo.ui
560 560 template = ui.config('ui', 'mergemarkertemplate')
561 561 template = templater.unquotestring(template)
562 562 tmpl = formatter.maketemplater(ui, template)
563 563
564 564 pad = max(len(l) for l in labels)
565 565
566 566 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
567 567 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
568 568 if len(labels) > 2:
569 569 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
570 570 return newlabels
571 571
572 572 def partextras(labels):
573 573 """Return a dictionary of extra labels for use in prompts to the user
574 574
575 575 Intended use is in strings of the form "(l)ocal%(l)s".
576 576 """
577 577 if labels is None:
578 578 return {
579 579 "l": "",
580 580 "o": "",
581 581 }
582 582
583 583 return {
584 584 "l": " [%s]" % labels[0],
585 585 "o": " [%s]" % labels[1],
586 586 }
587 587
588 588 def _restorebackup(fcd, back):
589 589 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
590 590 # util.copy here instead.
591 591 fcd.write(util.readfile(back), fcd.flags())
592 592
593 593 def _makebackup(repo, ui, fcd, premerge):
594 594 """Makes a backup of the local `fcd` file prior to merging.
595 595
596 596 In addition to preserving the user's pre-existing modifications to `fcd`
597 597 (if any), the backup is used to undo certain premerges, confirm whether a
598 598 merge changed anything, and determine what line endings the new file should
599 599 have.
600 600 """
601 601 if fcd.isabsent():
602 602 return None
603 603
604 604 a = _workingpath(repo, fcd)
605 605 back = scmutil.origpath(ui, repo, a)
606 606 if premerge:
607 607 util.copyfile(a, back)
608 608 return back
609 609
610 610 def _maketempfiles(repo, fco, fca):
611 611 """Writes out `fco` and `fca` as temporary files, so an external merge
612 612 tool may use them.
613 613 """
614 614 def temp(prefix, ctx):
615 615 fullbase, ext = os.path.splitext(ctx.path())
616 616 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
617 617 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
618 618 data = repo.wwritedata(ctx.path(), ctx.data())
619 619 f = os.fdopen(fd, pycompat.sysstr("wb"))
620 620 f.write(data)
621 621 f.close()
622 622 return name
623 623
624 624 b = temp("base", fca)
625 625 c = temp("other", fco)
626 626
627 627 return b, c
628 628
629 629 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
630 630 """perform a 3-way merge in the working directory
631 631
632 632 premerge = whether this is a premerge
633 633 mynode = parent node before merge
634 634 orig = original local filename before merge
635 635 fco = other file context
636 636 fca = ancestor file context
637 637 fcd = local file context for current/destination file
638 638
639 639 Returns whether the merge is complete, the return value of the merge, and
640 640 a boolean indicating whether the file was deleted from disk."""
641 641
642 642 if not fco.cmp(fcd): # files identical?
643 643 return True, None, False
644 644
645 645 ui = repo.ui
646 646 fd = fcd.path()
647 647 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
648 648 symlink = 'l' in fcd.flags() + fco.flags()
649 649 changedelete = fcd.isabsent() or fco.isabsent()
650 650 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
651 651 if tool in internals and tool.startswith('internal:'):
652 652 # normalize to new-style names (':merge' etc)
653 653 tool = tool[len('internal'):]
654 654 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
655 655 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
656 656 pycompat.bytestr(changedelete)))
657 657
658 658 if tool in internals:
659 659 func = internals[tool]
660 660 mergetype = func.mergetype
661 661 onfailure = func.onfailure
662 662 precheck = func.precheck
663 663 else:
664 664 func = _xmerge
665 665 mergetype = fullmerge
666 666 onfailure = _("merging %s failed!\n")
667 667 precheck = None
668 668
669 669 # If using deferred writes, must flush any deferred contents if running
670 670 # an external merge tool since it has arbitrary access to the working
671 671 # copy.
672 672 wctx.flushall()
673 673
674 674 toolconf = tool, toolpath, binary, symlink
675 675
676 676 if mergetype == nomerge:
677 677 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
678 678 return True, r, deleted
679 679
680 680 if premerge:
681 681 if orig != fco.path():
682 682 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
683 683 else:
684 684 ui.status(_("merging %s\n") % fd)
685 685
686 686 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
687 687
688 688 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
689 689 toolconf):
690 690 if onfailure:
691 691 ui.warn(onfailure % fd)
692 692 return True, 1, False
693 693
694 694 back = _makebackup(repo, ui, fcd, premerge)
695 695 files = (None, None, None, back)
696 696 r = 1
697 697 try:
698 698 markerstyle = ui.config('ui', 'mergemarkers')
699 699 if not labels:
700 700 labels = _defaultconflictlabels
701 701 if markerstyle != 'basic':
702 702 labels = _formatlabels(repo, fcd, fco, fca, labels)
703 703
704 704 if premerge and mergetype == fullmerge:
705 705 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
706 706 # complete if premerge successful (r is 0)
707 707 return not r, r, False
708 708
709 709 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
710 710 toolconf, files, labels=labels)
711 711
712 712 if needcheck:
713 713 r = _check(repo, r, ui, tool, fcd, files)
714 714
715 715 if r:
716 716 if onfailure:
717 717 ui.warn(onfailure % fd)
718 718
719 719 return True, r, deleted
720 720 finally:
721 721 if not r and back is not None:
722 722 util.unlink(back)
723 723
724 724 def _check(repo, r, ui, tool, fcd, files):
725 725 fd = fcd.path()
726 726 unused, unused, unused, back = files
727 727
728 728 if not r and (_toolbool(ui, tool, "checkconflicts") or
729 729 'conflicts' in _toollist(ui, tool, "check")):
730 730 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
731 731 re.MULTILINE):
732 732 r = 1
733 733
734 734 checked = False
735 735 if 'prompt' in _toollist(ui, tool, "check"):
736 736 checked = True
737 737 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
738 738 "$$ &Yes $$ &No") % fd, 1):
739 739 r = 1
740 740
741 741 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
742 742 'changed' in
743 743 _toollist(ui, tool, "check")):
744 744 if back is not None and filecmp.cmp(_workingpath(repo, fcd), back):
745 745 if ui.promptchoice(_(" output file %s appears unchanged\n"
746 746 "was merge successful (yn)?"
747 747 "$$ &Yes $$ &No") % fd, 1):
748 748 r = 1
749 749
750 750 if back is not None and _toolbool(ui, tool, "fixeol"):
751 751 _matcheol(_workingpath(repo, fcd), back)
752 752
753 753 return r
754 754
755 755 def _workingpath(repo, ctx):
756 756 return repo.wjoin(ctx.path())
757 757
758 758 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
759 759 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
760 760 labels=labels)
761 761
762 762 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
763 763 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
764 764 labels=labels)
765 765
766 766 def loadinternalmerge(ui, extname, registrarobj):
767 767 """Load internal merge tool from specified registrarobj
768 768 """
769 769 for name, func in registrarobj._table.iteritems():
770 770 fullname = ':' + name
771 771 internals[fullname] = func
772 772 internals['internal:' + name] = func
773 773 internalsdoc[fullname] = func
774 774
775 775 # load built-in merge tools explicitly to setup internalsdoc
776 776 loadinternalmerge(None, None, internaltool)
777 777
778 778 # tell hggettext to extract docstrings from these functions:
779 779 i18nfunctions = internals.values()
@@ -1,491 +1,491 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 error,
17 17 policy,
18 18 pycompat,
19 19 util,
20 20 )
21 21
22 22 bdiff = policy.importmod(r'bdiff')
23 23 mpatch = policy.importmod(r'mpatch')
24 24
25 25 blocks = bdiff.blocks
26 26 fixws = bdiff.fixws
27 27 patches = mpatch.patches
28 28 patchedsize = mpatch.patchedsize
29 29 textdiff = bdiff.bdiff
30 30
31 31 def splitnewlines(text):
32 32 '''like str.splitlines, but only split on newlines.'''
33 33 lines = [l + '\n' for l in text.split('\n')]
34 34 if lines:
35 35 if lines[-1] == '\n':
36 36 lines.pop()
37 37 else:
38 38 lines[-1] = lines[-1][:-1]
39 39 return lines
40 40
41 41 class diffopts(object):
42 42 '''context is the number of context lines
43 43 text treats all files as text
44 44 showfunc enables diff -p output
45 45 git enables the git extended patch format
46 46 nodates removes dates from diff headers
47 47 nobinary ignores binary files
48 48 noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode)
49 49 ignorews ignores all whitespace changes in the diff
50 50 ignorewsamount ignores changes in the amount of whitespace
51 51 ignoreblanklines ignores changes whose lines are all blank
52 52 upgrade generates git diffs to avoid data loss
53 53 '''
54 54
55 55 defaults = {
56 56 'context': 3,
57 57 'text': False,
58 58 'showfunc': False,
59 59 'git': False,
60 60 'nodates': False,
61 61 'nobinary': False,
62 62 'noprefix': False,
63 63 'index': 0,
64 64 'ignorews': False,
65 65 'ignorewsamount': False,
66 66 'ignorewseol': False,
67 67 'ignoreblanklines': False,
68 68 'upgrade': False,
69 69 'showsimilarity': False,
70 70 }
71 71
72 72 def __init__(self, **opts):
73 73 opts = pycompat.byteskwargs(opts)
74 74 for k in self.defaults.keys():
75 75 v = opts.get(k)
76 76 if v is None:
77 77 v = self.defaults[k]
78 78 setattr(self, k, v)
79 79
80 80 try:
81 81 self.context = int(self.context)
82 82 except ValueError:
83 83 raise error.Abort(_('diff context lines count must be '
84 84 'an integer, not %r') % self.context)
85 85
86 86 def copy(self, **kwargs):
87 87 opts = dict((k, getattr(self, k)) for k in self.defaults)
88 88 opts = pycompat.strkwargs(opts)
89 89 opts.update(kwargs)
90 90 return diffopts(**opts)
91 91
92 92 defaultopts = diffopts()
93 93
94 94 def wsclean(opts, text, blank=True):
95 95 if opts.ignorews:
96 96 text = bdiff.fixws(text, 1)
97 97 elif opts.ignorewsamount:
98 98 text = bdiff.fixws(text, 0)
99 99 if blank and opts.ignoreblanklines:
100 100 text = re.sub('\n+', '\n', text).strip('\n')
101 101 if opts.ignorewseol:
102 102 text = re.sub(r'[ \t\r\f]+\n', r'\n', text)
103 103 return text
104 104
105 105 def splitblock(base1, lines1, base2, lines2, opts):
106 106 # The input lines matches except for interwoven blank lines. We
107 107 # transform it into a sequence of matching blocks and blank blocks.
108 108 lines1 = [(wsclean(opts, l) and 1 or 0) for l in lines1]
109 109 lines2 = [(wsclean(opts, l) and 1 or 0) for l in lines2]
110 110 s1, e1 = 0, len(lines1)
111 111 s2, e2 = 0, len(lines2)
112 112 while s1 < e1 or s2 < e2:
113 113 i1, i2, btype = s1, s2, '='
114 114 if (i1 >= e1 or lines1[i1] == 0
115 115 or i2 >= e2 or lines2[i2] == 0):
116 116 # Consume the block of blank lines
117 117 btype = '~'
118 118 while i1 < e1 and lines1[i1] == 0:
119 119 i1 += 1
120 120 while i2 < e2 and lines2[i2] == 0:
121 121 i2 += 1
122 122 else:
123 123 # Consume the matching lines
124 124 while i1 < e1 and lines1[i1] == 1 and lines2[i2] == 1:
125 125 i1 += 1
126 126 i2 += 1
127 127 yield [base1 + s1, base1 + i1, base2 + s2, base2 + i2], btype
128 128 s1 = i1
129 129 s2 = i2
130 130
131 131 def hunkinrange(hunk, linerange):
132 132 """Return True if `hunk` defined as (start, length) is in `linerange`
133 133 defined as (lowerbound, upperbound).
134 134
135 135 >>> hunkinrange((5, 10), (2, 7))
136 136 True
137 137 >>> hunkinrange((5, 10), (6, 12))
138 138 True
139 139 >>> hunkinrange((5, 10), (13, 17))
140 140 True
141 141 >>> hunkinrange((5, 10), (3, 17))
142 142 True
143 143 >>> hunkinrange((5, 10), (1, 3))
144 144 False
145 145 >>> hunkinrange((5, 10), (18, 20))
146 146 False
147 147 >>> hunkinrange((5, 10), (1, 5))
148 148 False
149 149 >>> hunkinrange((5, 10), (15, 27))
150 150 False
151 151 """
152 152 start, length = hunk
153 153 lowerbound, upperbound = linerange
154 154 return lowerbound < start + length and start < upperbound
155 155
156 156 def blocksinrange(blocks, rangeb):
157 157 """filter `blocks` like (a1, a2, b1, b2) from items outside line range
158 158 `rangeb` from ``(b1, b2)`` point of view.
159 159
160 160 Return `filteredblocks, rangea` where:
161 161
162 162 * `filteredblocks` is list of ``block = (a1, a2, b1, b2), stype`` items of
163 163 `blocks` that are inside `rangeb` from ``(b1, b2)`` point of view; a
164 164 block ``(b1, b2)`` being inside `rangeb` if
165 165 ``rangeb[0] < b2 and b1 < rangeb[1]``;
166 166 * `rangea` is the line range w.r.t. to ``(a1, a2)`` parts of `blocks`.
167 167 """
168 168 lbb, ubb = rangeb
169 169 lba, uba = None, None
170 170 filteredblocks = []
171 171 for block in blocks:
172 172 (a1, a2, b1, b2), stype = block
173 173 if lbb >= b1 and ubb <= b2 and stype == '=':
174 174 # rangeb is within a single "=" hunk, restrict back linerange1
175 175 # by offsetting rangeb
176 176 lba = lbb - b1 + a1
177 177 uba = ubb - b1 + a1
178 178 else:
179 179 if b1 <= lbb < b2:
180 180 if stype == '=':
181 181 lba = a2 - (b2 - lbb)
182 182 else:
183 183 lba = a1
184 184 if b1 < ubb <= b2:
185 185 if stype == '=':
186 186 uba = a1 + (ubb - b1)
187 187 else:
188 188 uba = a2
189 189 if hunkinrange((b1, (b2 - b1)), rangeb):
190 190 filteredblocks.append(block)
191 191 if lba is None or uba is None or uba < lba:
192 192 raise error.Abort(_('line range exceeds file size'))
193 193 return filteredblocks, (lba, uba)
194 194
195 195 def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
196 196 """Return (block, type) tuples, where block is an mdiff.blocks
197 197 line entry. type is '=' for blocks matching exactly one another
198 198 (bdiff blocks), '!' for non-matching blocks and '~' for blocks
199 199 matching only after having filtered blank lines.
200 200 line1 and line2 are text1 and text2 split with splitnewlines() if
201 201 they are already available.
202 202 """
203 203 if opts is None:
204 204 opts = defaultopts
205 205 if opts.ignorews or opts.ignorewsamount or opts.ignorewseol:
206 206 text1 = wsclean(opts, text1, False)
207 207 text2 = wsclean(opts, text2, False)
208 208 diff = bdiff.blocks(text1, text2)
209 209 for i, s1 in enumerate(diff):
210 210 # The first match is special.
211 211 # we've either found a match starting at line 0 or a match later
212 212 # in the file. If it starts later, old and new below will both be
213 213 # empty and we'll continue to the next match.
214 214 if i > 0:
215 215 s = diff[i - 1]
216 216 else:
217 217 s = [0, 0, 0, 0]
218 218 s = [s[1], s1[0], s[3], s1[2]]
219 219
220 220 # bdiff sometimes gives huge matches past eof, this check eats them,
221 221 # and deals with the special first match case described above
222 222 if s[0] != s[1] or s[2] != s[3]:
223 223 type = '!'
224 224 if opts.ignoreblanklines:
225 225 if lines1 is None:
226 226 lines1 = splitnewlines(text1)
227 227 if lines2 is None:
228 228 lines2 = splitnewlines(text2)
229 229 old = wsclean(opts, "".join(lines1[s[0]:s[1]]))
230 230 new = wsclean(opts, "".join(lines2[s[2]:s[3]]))
231 231 if old == new:
232 232 type = '~'
233 233 yield s, type
234 234 yield s1, '='
235 235
236 236 def unidiff(a, ad, b, bd, fn1, fn2, opts=defaultopts):
237 237 """Return a unified diff as a (headers, hunks) tuple.
238 238
239 239 If the diff is not null, `headers` is a list with unified diff header
240 240 lines "--- <original>" and "+++ <new>" and `hunks` is a generator yielding
241 241 (hunkrange, hunklines) coming from _unidiff().
242 242 Otherwise, `headers` and `hunks` are empty.
243 243 """
244 244 def datetag(date, fn=None):
245 245 if not opts.git and not opts.nodates:
246 246 return '\t%s' % date
247 247 if fn and ' ' in fn:
248 248 return '\t'
249 249 return ''
250 250
251 251 sentinel = [], ()
252 252 if not a and not b:
253 253 return sentinel
254 254
255 255 if opts.noprefix:
256 256 aprefix = bprefix = ''
257 257 else:
258 258 aprefix = 'a/'
259 259 bprefix = 'b/'
260 260
261 261 epoch = util.datestr((0, 0))
262 262
263 263 fn1 = util.pconvert(fn1)
264 264 fn2 = util.pconvert(fn2)
265 265
266 266 def checknonewline(lines):
267 267 for text in lines:
268 268 if text[-1:] != '\n':
269 269 text += "\n\ No newline at end of file\n"
270 270 yield text
271 271
272 272 if not opts.text and (util.binary(a) or util.binary(b)):
273 273 if a and b and len(a) == len(b) and a == b:
274 274 return sentinel
275 275 headerlines = []
276 276 hunks = (None, ['Binary file %s has changed\n' % fn1]),
277 277 elif not a:
278 278 b = splitnewlines(b)
279 279 if a is None:
280 280 l1 = '--- /dev/null%s' % datetag(epoch)
281 281 else:
282 282 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
283 283 l2 = "+++ %s%s" % (bprefix + fn2, datetag(bd, fn2))
284 284 headerlines = [l1, l2]
285 285 size = len(b)
286 286 hunkrange = (0, 0, 1, size)
287 287 hunklines = ["@@ -0,0 +1,%d @@\n" % size] + ["+" + e for e in b]
288 288 hunks = (hunkrange, checknonewline(hunklines)),
289 289 elif not b:
290 290 a = splitnewlines(a)
291 291 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
292 292 if b is None:
293 293 l2 = '+++ /dev/null%s' % datetag(epoch)
294 294 else:
295 295 l2 = "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2))
296 296 headerlines = [l1, l2]
297 297 size = len(a)
298 298 hunkrange = (1, size, 0, 0)
299 299 hunklines = ["@@ -1,%d +0,0 @@\n" % size] + ["-" + e for e in a]
300 300 hunks = (hunkrange, checknonewline(hunklines)),
301 301 else:
302 302 diffhunks = _unidiff(a, b, opts=opts)
303 303 try:
304 304 hunkrange, hunklines = next(diffhunks)
305 305 except StopIteration:
306 306 return sentinel
307 307
308 308 headerlines = [
309 309 "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1)),
310 310 "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2)),
311 311 ]
312 312 def rewindhunks():
313 313 yield hunkrange, checknonewline(hunklines)
314 314 for hr, hl in diffhunks:
315 315 yield hr, checknonewline(hl)
316 316
317 317 hunks = rewindhunks()
318 318
319 319 return headerlines, hunks
320 320
321 321 def _unidiff(t1, t2, opts=defaultopts):
322 322 """Yield hunks of a headerless unified diff from t1 and t2 texts.
323 323
324 324 Each hunk consists of a (hunkrange, hunklines) tuple where `hunkrange` is a
325 325 tuple (s1, l1, s2, l2) representing the range information of the hunk to
326 326 form the '@@ -s1,l1 +s2,l2 @@' header and `hunklines` is a list of lines
327 327 of the hunk combining said header followed by line additions and
328 328 deletions.
329 329 """
330 330 l1 = splitnewlines(t1)
331 331 l2 = splitnewlines(t2)
332 332 def contextend(l, len):
333 333 ret = l + opts.context
334 334 if ret > len:
335 335 ret = len
336 336 return ret
337 337
338 338 def contextstart(l):
339 339 ret = l - opts.context
340 340 if ret < 0:
341 341 return 0
342 342 return ret
343 343
344 344 lastfunc = [0, '']
345 345 def yieldhunk(hunk):
346 346 (astart, a2, bstart, b2, delta) = hunk
347 347 aend = contextend(a2, len(l1))
348 348 alen = aend - astart
349 349 blen = b2 - bstart + aend - a2
350 350
351 351 func = ""
352 352 if opts.showfunc:
353 353 lastpos, func = lastfunc
354 354 # walk backwards from the start of the context up to the start of
355 355 # the previous hunk context until we find a line starting with an
356 356 # alphanumeric char.
357 357 for i in xrange(astart - 1, lastpos - 1, -1):
358 358 if l1[i][0].isalnum():
359 359 func = ' ' + l1[i].rstrip()[:40]
360 360 lastfunc[1] = func
361 361 break
362 362 # by recording this hunk's starting point as the next place to
363 363 # start looking for function lines, we avoid reading any line in
364 364 # the file more than once.
365 365 lastfunc[0] = astart
366 366
367 367 # zero-length hunk ranges report their start line as one less
368 368 if alen:
369 369 astart += 1
370 370 if blen:
371 371 bstart += 1
372 372
373 373 hunkrange = astart, alen, bstart, blen
374 374 hunklines = (
375 375 ["@@ -%d,%d +%d,%d @@%s\n" % (hunkrange + (func,))]
376 376 + delta
377 377 + [' ' + l1[x] for x in xrange(a2, aend)]
378 378 )
379 379 yield hunkrange, hunklines
380 380
381 381 # bdiff.blocks gives us the matching sequences in the files. The loop
382 382 # below finds the spaces between those matching sequences and translates
383 383 # them into diff output.
384 384 #
385 385 hunk = None
386 386 ignoredlines = 0
387 387 for s, stype in allblocks(t1, t2, opts, l1, l2):
388 388 a1, a2, b1, b2 = s
389 389 if stype != '!':
390 390 if stype == '~':
391 391 # The diff context lines are based on t1 content. When
392 392 # blank lines are ignored, the new lines offsets must
393 393 # be adjusted as if equivalent blocks ('~') had the
394 394 # same sizes on both sides.
395 395 ignoredlines += (b2 - b1) - (a2 - a1)
396 396 continue
397 397 delta = []
398 398 old = l1[a1:a2]
399 399 new = l2[b1:b2]
400 400
401 401 b1 -= ignoredlines
402 402 b2 -= ignoredlines
403 403 astart = contextstart(a1)
404 404 bstart = contextstart(b1)
405 405 prev = None
406 406 if hunk:
407 407 # join with the previous hunk if it falls inside the context
408 408 if astart < hunk[1] + opts.context + 1:
409 409 prev = hunk
410 410 astart = hunk[1]
411 411 bstart = hunk[3]
412 412 else:
413 413 for x in yieldhunk(hunk):
414 414 yield x
415 415 if prev:
416 416 # we've joined the previous hunk, record the new ending points.
417 417 hunk[1] = a2
418 418 hunk[3] = b2
419 419 delta = hunk[4]
420 420 else:
421 421 # create a new hunk
422 422 hunk = [astart, a2, bstart, b2, delta]
423 423
424 424 delta[len(delta):] = [' ' + x for x in l1[astart:a1]]
425 425 delta[len(delta):] = ['-' + x for x in old]
426 426 delta[len(delta):] = ['+' + x for x in new]
427 427
428 428 if hunk:
429 429 for x in yieldhunk(hunk):
430 430 yield x
431 431
432 432 def b85diff(to, tn):
433 433 '''print base85-encoded binary diff'''
434 434 def fmtline(line):
435 435 l = len(line)
436 436 if l <= 26:
437 437 l = chr(ord('A') + l - 1)
438 438 else:
439 439 l = chr(l - 26 + ord('a') - 1)
440 440 return '%c%s\n' % (l, util.b85encode(line, True))
441 441
442 442 def chunk(text, csize=52):
443 443 l = len(text)
444 444 i = 0
445 445 while i < l:
446 446 yield text[i:i + csize]
447 447 i += csize
448 448
449 449 if to is None:
450 450 to = ''
451 451 if tn is None:
452 452 tn = ''
453 453
454 454 if to == tn:
455 455 return ''
456 456
457 457 # TODO: deltas
458 458 ret = []
459 459 ret.append('GIT binary patch\n')
460 ret.append('literal %s\n' % len(tn))
460 ret.append('literal %d\n' % len(tn))
461 461 for l in chunk(zlib.compress(tn)):
462 462 ret.append(fmtline(l))
463 463 ret.append('\n')
464 464
465 465 return ''.join(ret)
466 466
467 467 def patchtext(bin):
468 468 pos = 0
469 469 t = []
470 470 while pos < len(bin):
471 471 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
472 472 pos += 12
473 473 t.append(bin[pos:pos + l])
474 474 pos += l
475 475 return "".join(t)
476 476
477 477 def patch(a, bin):
478 478 if len(a) == 0:
479 479 # skip over trivial delta header
480 480 return util.buffer(bin, 12)
481 481 return mpatch.patches(a, [bin])
482 482
483 483 # similar to difflib.SequenceMatcher.get_matching_blocks
484 484 def get_matching_blocks(a, b):
485 485 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
486 486
487 487 def trivialdiffheader(length):
488 488 return struct.pack(">lll", 0, 0, length) if length else ''
489 489
490 490 def replacediffheader(oldlen, newlen):
491 491 return struct.pack(">lll", 0, oldlen, newlen)
General Comments 0
You need to be logged in to leave comments. Login now