Show More
@@ -114,8 +114,16 b' class absentfilectx(object):' | |||||
114 | def _findtool(ui, tool): |
|
114 | def _findtool(ui, tool): | |
115 | if tool in internals: |
|
115 | if tool in internals: | |
116 | return tool |
|
116 | return tool | |
|
117 | cmd = _toolstr(ui, tool, "executable", tool) | |||
|
118 | if cmd.startswith('python:'): | |||
|
119 | return cmd | |||
117 | return findexternaltool(ui, tool) |
|
120 | return findexternaltool(ui, tool) | |
118 |
|
121 | |||
|
122 | def _quotetoolpath(cmd): | |||
|
123 | if cmd.startswith('python:'): | |||
|
124 | return cmd | |||
|
125 | return procutil.shellquote(cmd) | |||
|
126 | ||||
119 | def findexternaltool(ui, tool): |
|
127 | def findexternaltool(ui, tool): | |
120 | for kn in ("regkey", "regkeyalt"): |
|
128 | for kn in ("regkey", "regkeyalt"): | |
121 | k = _toolstr(ui, tool, kn) |
|
129 | k = _toolstr(ui, tool, kn) | |
@@ -165,7 +173,7 b' def _picktool(repo, ui, path, binary, sy' | |||||
165 | return ":prompt", None |
|
173 | return ":prompt", None | |
166 | else: |
|
174 | else: | |
167 | if toolpath: |
|
175 | if toolpath: | |
168 |
return (force, |
|
176 | return (force, _quotetoolpath(toolpath)) | |
169 | else: |
|
177 | else: | |
170 | # mimic HGMERGE if given tool not found |
|
178 | # mimic HGMERGE if given tool not found | |
171 | return (force, force) |
|
179 | return (force, force) | |
@@ -183,7 +191,7 b' def _picktool(repo, ui, path, binary, sy' | |||||
183 | mf = match.match(repo.root, '', [pat]) |
|
191 | mf = match.match(repo.root, '', [pat]) | |
184 | if mf(path) and check(tool, pat, symlink, False, changedelete): |
|
192 | if mf(path) and check(tool, pat, symlink, False, changedelete): | |
185 | toolpath = _findtool(ui, tool) |
|
193 | toolpath = _findtool(ui, tool) | |
186 |
return (tool, |
|
194 | return (tool, _quotetoolpath(toolpath)) | |
187 |
|
195 | |||
188 | # then merge tools |
|
196 | # then merge tools | |
189 | tools = {} |
|
197 | tools = {} | |
@@ -208,7 +216,7 b' def _picktool(repo, ui, path, binary, sy' | |||||
208 | for p, t in tools: |
|
216 | for p, t in tools: | |
209 | if check(t, None, symlink, binary, changedelete): |
|
217 | if check(t, None, symlink, binary, changedelete): | |
210 | toolpath = _findtool(ui, t) |
|
218 | toolpath = _findtool(ui, t) | |
211 |
return (t, |
|
219 | return (t, _quotetoolpath(toolpath)) | |
212 |
|
220 | |||
213 | # internal merge or prompt as last resort |
|
221 | # internal merge or prompt as last resort | |
214 | if symlink or binary or changedelete: |
|
222 | if symlink or binary or changedelete: | |
@@ -325,7 +333,7 b' def _underlyingfctxifabsent(filectx):' | |||||
325 | return filectx |
|
333 | return filectx | |
326 |
|
334 | |||
327 | def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None): |
|
335 | def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None): | |
328 | tool, toolpath, binary, symlink = toolconf |
|
336 | tool, toolpath, binary, symlink, scriptfn = toolconf | |
329 | if symlink or fcd.isabsent() or fco.isabsent(): |
|
337 | if symlink or fcd.isabsent() or fco.isabsent(): | |
330 | return 1 |
|
338 | return 1 | |
331 | unused, unused, unused, back = files |
|
339 | unused, unused, unused, back = files | |
@@ -361,7 +369,7 b' def _premerge(repo, fcd, fco, fca, toolc' | |||||
361 | return 1 # continue merging |
|
369 | return 1 # continue merging | |
362 |
|
370 | |||
363 | def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf): |
|
371 | def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf): | |
364 | tool, toolpath, binary, symlink = toolconf |
|
372 | tool, toolpath, binary, symlink, scriptfn = toolconf | |
365 | if symlink: |
|
373 | if symlink: | |
366 | repo.ui.warn(_('warning: internal %s cannot merge symlinks ' |
|
374 | repo.ui.warn(_('warning: internal %s cannot merge symlinks ' | |
367 | 'for %s\n') % (tool, fcd.path())) |
|
375 | 'for %s\n') % (tool, fcd.path())) | |
@@ -430,7 +438,7 b' def _imergeauto(repo, mynode, orig, fcd,' | |||||
430 | Generic driver for _imergelocal and _imergeother |
|
438 | Generic driver for _imergelocal and _imergeother | |
431 | """ |
|
439 | """ | |
432 | assert localorother is not None |
|
440 | assert localorother is not None | |
433 | tool, toolpath, binary, symlink = toolconf |
|
441 | tool, toolpath, binary, symlink, scriptfn = toolconf | |
434 | r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels, |
|
442 | r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels, | |
435 | localorother=localorother) |
|
443 | localorother=localorother) | |
436 | return True, r |
|
444 | return True, r | |
@@ -510,7 +518,7 b' def _xmergeimm(repo, mynode, orig, fcd, ' | |||||
510 | 'external merge tools') |
|
518 | 'external merge tools') | |
511 |
|
519 | |||
512 | def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None): |
|
520 | def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None): | |
513 | tool, toolpath, binary, symlink = toolconf |
|
521 | tool, toolpath, binary, symlink, scriptfn = toolconf | |
514 | if fcd.isabsent() or fco.isabsent(): |
|
522 | if fcd.isabsent() or fco.isabsent(): | |
515 | repo.ui.warn(_('warning: %s cannot merge change/delete conflict ' |
|
523 | repo.ui.warn(_('warning: %s cannot merge change/delete conflict ' | |
516 | 'for %s\n') % (tool, fcd.path())) |
|
524 | 'for %s\n') % (tool, fcd.path())) | |
@@ -551,12 +559,36 b' def _xmerge(repo, mynode, orig, fcd, fco' | |||||
551 | args = util.interpolate( |
|
559 | args = util.interpolate( | |
552 | br'\$', replace, args, |
|
560 | br'\$', replace, args, | |
553 | lambda s: procutil.shellquote(util.localpath(s))) |
|
561 | lambda s: procutil.shellquote(util.localpath(s))) | |
554 | cmd = toolpath + ' ' + args |
|
|||
555 | if _toolbool(ui, tool, "gui"): |
|
562 | if _toolbool(ui, tool, "gui"): | |
556 | repo.ui.status(_('running merge tool %s for file %s\n') % |
|
563 | repo.ui.status(_('running merge tool %s for file %s\n') % | |
557 | (tool, fcd.path())) |
|
564 | (tool, fcd.path())) | |
558 | repo.ui.debug('launching merge tool: %s\n' % cmd) |
|
565 | if scriptfn is None: | |
559 | r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool') |
|
566 | cmd = toolpath + ' ' + args | |
|
567 | repo.ui.debug('launching merge tool: %s\n' % cmd) | |||
|
568 | r = ui.system(cmd, cwd=repo.root, environ=env, | |||
|
569 | blockedtag='mergetool') | |||
|
570 | else: | |||
|
571 | repo.ui.debug('launching python merge script: %s:%s\n' % | |||
|
572 | (toolpath, scriptfn)) | |||
|
573 | r = 0 | |||
|
574 | try: | |||
|
575 | # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil | |||
|
576 | from . import extensions | |||
|
577 | mod = extensions.loadpath(toolpath, 'hgmerge.%s' % scriptfn) | |||
|
578 | except Exception: | |||
|
579 | raise error.Abort(_("loading python merge script failed: %s") % | |||
|
580 | toolpath) | |||
|
581 | mergefn = getattr(mod, scriptfn, None) | |||
|
582 | if mergefn is None: | |||
|
583 | raise error.Abort(_("%s does not have function: %s") % | |||
|
584 | (toolpath, scriptfn)) | |||
|
585 | argslist = procutil.shellsplit(args) | |||
|
586 | # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil | |||
|
587 | from . import hook | |||
|
588 | ret, raised = hook.pythonhook(ui, repo, "merge", toolpath, | |||
|
589 | mergefn, {'args': argslist}, True) | |||
|
590 | if raised: | |||
|
591 | r = 1 | |||
560 | repo.ui.debug('merge tool returned: %d\n' % r) |
|
592 | repo.ui.debug('merge tool returned: %d\n' % r) | |
561 | return True, r, False |
|
593 | return True, r, False | |
562 |
|
594 | |||
@@ -751,9 +783,24 b' def _filemerge(premerge, repo, wctx, myn' | |||||
751 | symlink = 'l' in fcd.flags() + fco.flags() |
|
783 | symlink = 'l' in fcd.flags() + fco.flags() | |
752 | changedelete = fcd.isabsent() or fco.isabsent() |
|
784 | changedelete = fcd.isabsent() or fco.isabsent() | |
753 | tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete) |
|
785 | tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete) | |
|
786 | scriptfn = None | |||
754 | if tool in internals and tool.startswith('internal:'): |
|
787 | if tool in internals and tool.startswith('internal:'): | |
755 | # normalize to new-style names (':merge' etc) |
|
788 | # normalize to new-style names (':merge' etc) | |
756 | tool = tool[len('internal'):] |
|
789 | tool = tool[len('internal'):] | |
|
790 | if toolpath and toolpath.startswith('python:'): | |||
|
791 | invalidsyntax = False | |||
|
792 | if toolpath.count(':') >= 2: | |||
|
793 | script, scriptfn = toolpath[7:].rsplit(':', 1) | |||
|
794 | if not scriptfn: | |||
|
795 | invalidsyntax = True | |||
|
796 | # missing :callable can lead to spliting on windows drive letter | |||
|
797 | if '\\' in scriptfn or '/' in scriptfn: | |||
|
798 | invalidsyntax = True | |||
|
799 | else: | |||
|
800 | invalidsyntax = True | |||
|
801 | if invalidsyntax: | |||
|
802 | raise error.Abort(_("invalid 'python:' syntax: %s") % toolpath) | |||
|
803 | toolpath = script | |||
757 | ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n" |
|
804 | ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n" | |
758 | % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink), |
|
805 | % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink), | |
759 | pycompat.bytestr(changedelete))) |
|
806 | pycompat.bytestr(changedelete))) | |
@@ -774,7 +821,7 b' def _filemerge(premerge, repo, wctx, myn' | |||||
774 | precheck = None |
|
821 | precheck = None | |
775 | isexternal = True |
|
822 | isexternal = True | |
776 |
|
823 | |||
777 | toolconf = tool, toolpath, binary, symlink |
|
824 | toolconf = tool, toolpath, binary, symlink, scriptfn | |
778 |
|
825 | |||
779 | if mergetype == nomerge: |
|
826 | if mergetype == nomerge: | |
780 | r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels) |
|
827 | r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels) |
@@ -24,7 +24,7 b' from .utils import (' | |||||
24 | stringutil, |
|
24 | stringutil, | |
25 | ) |
|
25 | ) | |
26 |
|
26 | |||
27 |
def |
|
27 | def pythonhook(ui, repo, htype, hname, funcname, args, throw): | |
28 | '''call python hook. hook is callable object, looked up as |
|
28 | '''call python hook. hook is callable object, looked up as | |
29 | name in python module. if callable returns "true", hook |
|
29 | name in python module. if callable returns "true", hook | |
30 | fails, else passes. if hook raises exception, treated as |
|
30 | fails, else passes. if hook raises exception, treated as | |
@@ -242,7 +242,7 b' def runhooks(ui, repo, htype, hooks, thr' | |||||
242 | r = 1 |
|
242 | r = 1 | |
243 | raised = False |
|
243 | raised = False | |
244 | elif callable(cmd): |
|
244 | elif callable(cmd): | |
245 |
r, raised = |
|
245 | r, raised = pythonhook(ui, repo, htype, hname, cmd, args, | |
246 | throw) |
|
246 | throw) | |
247 | elif cmd.startswith('python:'): |
|
247 | elif cmd.startswith('python:'): | |
248 | if cmd.count(':') >= 2: |
|
248 | if cmd.count(':') >= 2: | |
@@ -258,7 +258,7 b' def runhooks(ui, repo, htype, hooks, thr' | |||||
258 | hookfn = getattr(mod, cmd) |
|
258 | hookfn = getattr(mod, cmd) | |
259 | else: |
|
259 | else: | |
260 | hookfn = cmd[7:].strip() |
|
260 | hookfn = cmd[7:].strip() | |
261 |
r, raised = |
|
261 | r, raised = pythonhook(ui, repo, htype, hname, hookfn, args, | |
262 | throw) |
|
262 | throw) | |
263 | else: |
|
263 | else: | |
264 | r = _exthook(ui, repo, htype, hname, cmd, args, throw) |
|
264 | r = _exthook(ui, repo, htype, hname, cmd, args, throw) |
@@ -329,6 +329,183 b' and true.executable set to cat with path' | |||||
329 | # hg resolve --list |
|
329 | # hg resolve --list | |
330 | R f |
|
330 | R f | |
331 |
|
331 | |||
|
332 | executable set to python script that succeeds: | |||
|
333 | ||||
|
334 | $ cat > "$TESTTMP/myworkingmerge.py" <<EOF | |||
|
335 | > def myworkingmergefn(ui, repo, args, **kwargs): | |||
|
336 | > return False | |||
|
337 | > EOF | |||
|
338 | $ beforemerge | |||
|
339 | [merge-tools] | |||
|
340 | false.whatever= | |||
|
341 | true.priority=1 | |||
|
342 | true.executable=cat | |||
|
343 | # hg update -C 1 | |||
|
344 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:myworkingmergefn" | |||
|
345 | merging f | |||
|
346 | 0 files updated, 1 files merged, 0 files removed, 0 files unresolved | |||
|
347 | (branch merge, don't forget to commit) | |||
|
348 | $ aftermerge | |||
|
349 | # cat f | |||
|
350 | revision 1 | |||
|
351 | space | |||
|
352 | # hg stat | |||
|
353 | M f | |||
|
354 | # hg resolve --list | |||
|
355 | R f | |||
|
356 | ||||
|
357 | executable set to python script that fails: | |||
|
358 | ||||
|
359 | $ cat > "$TESTTMP/mybrokenmerge.py" <<EOF | |||
|
360 | > def mybrokenmergefn(ui, repo, args, **kwargs): | |||
|
361 | > ui.write(b"some fail message\n") | |||
|
362 | > return True | |||
|
363 | > EOF | |||
|
364 | $ beforemerge | |||
|
365 | [merge-tools] | |||
|
366 | false.whatever= | |||
|
367 | true.priority=1 | |||
|
368 | true.executable=cat | |||
|
369 | # hg update -C 1 | |||
|
370 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/mybrokenmerge.py:mybrokenmergefn" | |||
|
371 | merging f | |||
|
372 | some fail message | |||
|
373 | abort: $TESTTMP/mybrokenmerge.py hook failed | |||
|
374 | [255] | |||
|
375 | $ aftermerge | |||
|
376 | # cat f | |||
|
377 | revision 1 | |||
|
378 | space | |||
|
379 | # hg stat | |||
|
380 | ? f.orig | |||
|
381 | # hg resolve --list | |||
|
382 | U f | |||
|
383 | ||||
|
384 | executable set to python script that is missing function: | |||
|
385 | ||||
|
386 | $ beforemerge | |||
|
387 | [merge-tools] | |||
|
388 | false.whatever= | |||
|
389 | true.priority=1 | |||
|
390 | true.executable=cat | |||
|
391 | # hg update -C 1 | |||
|
392 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:missingFunction" | |||
|
393 | merging f | |||
|
394 | abort: $TESTTMP/myworkingmerge.py does not have function: missingFunction | |||
|
395 | [255] | |||
|
396 | $ aftermerge | |||
|
397 | # cat f | |||
|
398 | revision 1 | |||
|
399 | space | |||
|
400 | # hg stat | |||
|
401 | ? f.orig | |||
|
402 | # hg resolve --list | |||
|
403 | U f | |||
|
404 | ||||
|
405 | executable set to missing python script: | |||
|
406 | ||||
|
407 | $ beforemerge | |||
|
408 | [merge-tools] | |||
|
409 | false.whatever= | |||
|
410 | true.priority=1 | |||
|
411 | true.executable=cat | |||
|
412 | # hg update -C 1 | |||
|
413 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/missingpythonscript.py:mergefn" | |||
|
414 | merging f | |||
|
415 | abort: loading python merge script failed: $TESTTMP/missingpythonscript.py | |||
|
416 | [255] | |||
|
417 | $ aftermerge | |||
|
418 | # cat f | |||
|
419 | revision 1 | |||
|
420 | space | |||
|
421 | # hg stat | |||
|
422 | ? f.orig | |||
|
423 | # hg resolve --list | |||
|
424 | U f | |||
|
425 | ||||
|
426 | executable set to python script but callable function is missing: | |||
|
427 | ||||
|
428 | $ beforemerge | |||
|
429 | [merge-tools] | |||
|
430 | false.whatever= | |||
|
431 | true.priority=1 | |||
|
432 | true.executable=cat | |||
|
433 | # hg update -C 1 | |||
|
434 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py" | |||
|
435 | abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py | |||
|
436 | [255] | |||
|
437 | $ aftermerge | |||
|
438 | # cat f | |||
|
439 | revision 1 | |||
|
440 | space | |||
|
441 | # hg stat | |||
|
442 | # hg resolve --list | |||
|
443 | U f | |||
|
444 | ||||
|
445 | executable set to python script but callable function is empty string: | |||
|
446 | ||||
|
447 | $ beforemerge | |||
|
448 | [merge-tools] | |||
|
449 | false.whatever= | |||
|
450 | true.priority=1 | |||
|
451 | true.executable=cat | |||
|
452 | # hg update -C 1 | |||
|
453 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:" | |||
|
454 | abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py: | |||
|
455 | [255] | |||
|
456 | $ aftermerge | |||
|
457 | # cat f | |||
|
458 | revision 1 | |||
|
459 | space | |||
|
460 | # hg stat | |||
|
461 | # hg resolve --list | |||
|
462 | U f | |||
|
463 | ||||
|
464 | executable set to python script but callable function is missing and path contains colon: | |||
|
465 | ||||
|
466 | $ beforemerge | |||
|
467 | [merge-tools] | |||
|
468 | false.whatever= | |||
|
469 | true.priority=1 | |||
|
470 | true.executable=cat | |||
|
471 | # hg update -C 1 | |||
|
472 | $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/some:dir/myworkingmerge.py" | |||
|
473 | abort: invalid 'python:' syntax: python:$TESTTMP/some:dir/myworkingmerge.py | |||
|
474 | [255] | |||
|
475 | $ aftermerge | |||
|
476 | # cat f | |||
|
477 | revision 1 | |||
|
478 | space | |||
|
479 | # hg stat | |||
|
480 | # hg resolve --list | |||
|
481 | U f | |||
|
482 | ||||
|
483 | executable set to python script filename that contains spaces: | |||
|
484 | ||||
|
485 | $ mkdir -p "$TESTTMP/my path" | |||
|
486 | $ cat > "$TESTTMP/my path/my working merge with spaces in filename.py" <<EOF | |||
|
487 | > def myworkingmergefn(ui, repo, args, **kwargs): | |||
|
488 | > return False | |||
|
489 | > EOF | |||
|
490 | $ beforemerge | |||
|
491 | [merge-tools] | |||
|
492 | false.whatever= | |||
|
493 | true.priority=1 | |||
|
494 | true.executable=cat | |||
|
495 | # hg update -C 1 | |||
|
496 | $ hg merge -r 2 --config "merge-tools.true.executable=python:$TESTTMP/my path/my working merge with spaces in filename.py:myworkingmergefn" | |||
|
497 | merging f | |||
|
498 | 0 files updated, 1 files merged, 0 files removed, 0 files unresolved | |||
|
499 | (branch merge, don't forget to commit) | |||
|
500 | $ aftermerge | |||
|
501 | # cat f | |||
|
502 | revision 1 | |||
|
503 | space | |||
|
504 | # hg stat | |||
|
505 | M f | |||
|
506 | # hg resolve --list | |||
|
507 | R f | |||
|
508 | ||||
332 | #if unix-permissions |
|
509 | #if unix-permissions | |
333 |
|
510 | |||
334 | environment variables in true.executable are handled: |
|
511 | environment variables in true.executable are handled: |
General Comments 0
You need to be logged in to leave comments.
Login now