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