Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/filemerge.py @ 38074:242eb5132203
filemerge: support specifying a python function to custom merge-tools
Eliminates the need to specify a python executable, which may not exist on
system. Additionally launching script inprocess aids portability on systems
that can't execute python via the shell.
Example usage "merge-tools.myTool.executable=python:c:\myTool.py:mergefn"
where myTool.py contains a function:
"def mergefn(ui, repo, args, **kwargs):"
where args is list of args passed to merge tool.
(by default, expanded: $local $base $other)
Invoking the specified python function was done by exposing and invoking
(hook._pythonhook -> hook.pythonhook)
author | hindlemail <tom_hindle@sil.org> |
---|---|
date | Wed, 16 May 2018 14:11:41 -0600 |
parents | a8a902d7176e |
children | dea3903175ee |
comparison
equal
deleted
inserted
replaced
38073:37ef6ee87488 | 38074:242eb5132203 |
---|---|
112 return True | 112 return True |
113 | 113 |
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) |
121 | |
122 def _quotetoolpath(cmd): | |
123 if cmd.startswith('python:'): | |
124 return cmd | |
125 return procutil.shellquote(cmd) | |
118 | 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) |
122 if not k: | 130 if not k: |
163 toolpath = _findtool(ui, force) | 171 toolpath = _findtool(ui, force) |
164 if changedelete and not supportscd(toolpath): | 172 if changedelete and not supportscd(toolpath): |
165 return ":prompt", None | 173 return ":prompt", None |
166 else: | 174 else: |
167 if toolpath: | 175 if toolpath: |
168 return (force, procutil.shellquote(toolpath)) | 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) |
172 | 180 |
173 # HGMERGE takes next precedence | 181 # HGMERGE takes next precedence |
181 # then patterns | 189 # then patterns |
182 for pat, tool in ui.configitems("merge-patterns"): | 190 for pat, tool in ui.configitems("merge-patterns"): |
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, procutil.shellquote(toolpath)) | 194 return (tool, _quotetoolpath(toolpath)) |
187 | 195 |
188 # then merge tools | 196 # then merge tools |
189 tools = {} | 197 tools = {} |
190 disabled = set() | 198 disabled = set() |
191 for k, v in ui.configitems("merge-tools"): | 199 for k, v in ui.configitems("merge-tools"): |
206 tools.insert(0, (None, uimerge)) # highest priority | 214 tools.insert(0, (None, uimerge)) # highest priority |
207 tools.append((None, "hgmerge")) # the old default, if found | 215 tools.append((None, "hgmerge")) # the old default, if found |
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, procutil.shellquote(toolpath)) | 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: |
215 if not changedelete and len(tools): | 223 if not changedelete and len(tools): |
216 # any tool is rejected by capability for symlink or binary | 224 # any tool is rejected by capability for symlink or binary |
323 return filectx.changectx()[filectx.path()] | 331 return filectx.changectx()[filectx.path()] |
324 else: | 332 else: |
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 |
332 | 340 |
333 ui = repo.ui | 341 ui = repo.ui |
359 # restore from backup and try again | 367 # restore from backup and try again |
360 _restorebackup(fcd, back) | 368 _restorebackup(fcd, back) |
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())) |
368 return False | 376 return False |
369 if fcd.isabsent() or fco.isabsent(): | 377 if fcd.isabsent() or fco.isabsent(): |
428 labels=None, localorother=None): | 436 labels=None, localorother=None): |
429 """ | 437 """ |
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 |
437 | 445 |
438 @internaltool('merge-local', mergeonly, precheck=_mergecheck) | 446 @internaltool('merge-local', mergeonly, precheck=_mergecheck) |
508 # clunky.) | 516 # clunky.) |
509 raise error.InMemoryMergeConflictsError('in-memory merge does not support ' | 517 raise error.InMemoryMergeConflictsError('in-memory merge does not support ' |
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())) |
517 return False, 1, None | 525 return False, 1, None |
518 unused, unused, unused, back = files | 526 unused, unused, unused, back = files |
549 'output': outpath, 'labellocal': mylabel, | 557 'output': outpath, 'labellocal': mylabel, |
550 'labelother': otherlabel, 'labelbase': baselabel} | 558 'labelother': otherlabel, 'labelbase': baselabel} |
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 |
563 def _formatconflictmarker(ctx, template, label, pad): | 595 def _formatconflictmarker(ctx, template, label, pad): |
564 """Applies the given template to the ctx, prefixed by the label. | 596 """Applies the given template to the ctx, prefixed by the label. |
749 fd = fcd.path() | 781 fd = fcd.path() |
750 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary() | 782 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary() |
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))) |
760 | 807 |
761 if tool in internals: | 808 if tool in internals: |
772 mergetype = fullmerge | 819 mergetype = fullmerge |
773 onfailure = _("merging %s failed!\n") | 820 onfailure = _("merging %s failed!\n") |
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) |
781 return True, r, deleted | 828 return True, r, deleted |
782 | 829 |