diff --git a/hgext/extdiff.py b/hgext/extdiff.py --- a/hgext/extdiff.py +++ b/hgext/extdiff.py @@ -97,10 +97,13 @@ from mercurial import ( archival, cmdutil, + commands, encoding, error, + extensions, filemerge, formatter, + patch, pycompat, registrar, scmutil, @@ -759,3 +762,178 @@ # tell hggettext to extract docstrings from these functions: i18nfunctions = [savedcmd] + +_temproots = {} + + +def _gettemproot(repo, node, tmproot): + global _temproots + + if node not in _temproots: + dirname = os.path.basename(repo.root) + if dirname == b"": + dirname = b"root" + if node is not None: + dirname = b'%s.%s' % (dirname, short(node)) + base = os.path.join(tmproot, dirname) + else: + base = repo.root + _temproots[node] = base + return base + + return _temproots[node] + + +def extdiffhunks( + orig, + repo, + ctx1, + ctx2, + match=None, + changes=None, + opts=None, + losedatafn=None, + pathfn=None, + copy=None, + copysourcematch=None, +): + """ Wraps patch.diffhunks to show diff using external diff tools. + + Does following things in order: + * Checks if we are diffing externally or not, if not call orig() + * Creates temporary directories where temporary files will be written + for external tools + * Calls orig(), we are wrapping `patch.diffcontent()` to write content + of both diff sides to files instead of producing diffs + * Gets the difftool to call from config and build the command + which needs to be run + * Once all diff sides are written to temp files (if required), runs + difftool for each file + * Deletes the temporary directory created + """ + if opts is None or not opts.external: + # mdiffopts does not have the external part set, means + # we are not diffing externally + return orig( + repo, + ctx1, + ctx2, + match, + changes, + opts, + losedatafn, + pathfn, + copy, + copysourcematch, + ) + + # create the base paths for each changesets + tmproot = pycompat.mkdtemp(prefix=b'extdiff.') + try: + node1 = ctx1.node() + node2 = ctx2.node() + root1 = _gettemproot(repo, node1, tmproot) + root2 = _gettemproot(repo, node2, tmproot) + if node1 is not None: + os.makedirs(root1) + if node2 is not None: + os.makedirs(root2) + + changes = [] + for c in orig( + repo, + ctx1, + ctx2, + match, + changes, + opts, + losedatafn, + pathfn, + copy, + copysourcematch, + ): + changes.append(c[0]) + + # TODO: we should get this from config option + program = b'vimdiff' + option = [] + cmdline = b' '.join(map(procutil.shellquote, [program] + option)) + + _runperfilediff( + cmdline, + repo.root, + repo.ui, + False, + False, + False, + changes, + tmproot, + root1, + None, + root2, + node1 if node1 else '', + None, + node2 if node2 else '', + ) + + return [] + finally: + repo.ui.note(_(b'cleaning up temp directory\n')) + shutil.rmtree(tmproot) + + +def extdiffcontent(orig, data1, data2, header, binary, opts): + """ Wraps patch.diffcontent to write file contents to temporary files + instead of calling mdiff to produce diffs. + + This is done only when we are using external tools to diff + """ + if not opts.external: + # not diffing externally, go back to original way + return orig(data1, data2, header, binary, opts) + + ctx1, fctx1, path1, flag1, content1, date1 = data1 + ctx2, fctx2, path2, flag2, content2, date2 = data2 + + # Write content to temporary files instead of calling mdiff + # If node is None, means we need to diff with working directory, hence + # no need to write the file + # If content is empty, we can skip writing the file and _runperfilediff() + # will use /dev/null as the file is missing + for node, content, path in ( + (ctx1.node(), content1, path1), + (ctx2.node(), content2, path2), + ): + if node is not None and content: + dirpath = _gettemproot(None, node, None) + fpath = os.path.join(dirpath, path) + dirfpath = os.path.dirname(fpath) + if not os.path.exists(dirfpath): + os.makedirs(dirfpath) + + with open(fpath, 'wb') as fp: + fp.write(content) + + return path1, path2, None, None + + +def _diff(orig, ui, repo, *pats, **opts): + overrides = {} + if opts.get('tool'): + # stat cannot be show using an external tool + cmdutil.check_at_most_one_arg(opts, 'tool', 'stat') + # if we will be diffing using external tool, turn off the pager + overrides[(b'ui', b'paginate')] = False + + with ui.configoverride(overrides, b'extdiff'): + orig(ui, repo, *pats, **opts) + + +def extsetup(ui): + diffentry = extensions.wrapcommand(commands.table, b'diff', _diff) + diffentry[1].append( + (b'', b'tool', False, _(b'show diff using external tool'),) + ) + + extensions.wrapfunction(patch, b'diffhunks', extdiffhunks) + extensions.wrapfunction(patch, b'diffcontent', extdiffcontent) diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -135,6 +135,10 @@ coreconfigitem( section, configprefix + b'nodates', default=False, ) + # TODO: this should be value one instead of boolean + coreconfigitem( + section, configprefix + b'tool', default=False, + ) coreconfigitem( section, configprefix + b'showfunc', default=False, ) diff --git a/mercurial/diffutil.py b/mercurial/diffutil.py --- a/mercurial/diffutil.py +++ b/mercurial/diffutil.py @@ -77,6 +77,7 @@ b'context': get(b'unified', getter=ui.config), } buildopts[b'xdiff'] = ui.configbool(b'experimental', b'xdiff') + buildopts[b'external'] = get(b'tool') if git: buildopts[b'git'] = get(b'git') diff --git a/mercurial/logcmdutil.py b/mercurial/logcmdutil.py --- a/mercurial/logcmdutil.py +++ b/mercurial/logcmdutil.py @@ -91,6 +91,10 @@ relroot = b'' copysourcematch = None + if stat: + # explicitly set external tooling to false if we are processing stat + diffopts.external = False + def compose(f, g): return lambda x: f(g(x)) diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py --- a/mercurial/mdiff.py +++ b/mercurial/mdiff.py @@ -40,6 +40,7 @@ # TODO: this looks like it could be an attrs, which might help pytype class diffopts(object): '''context is the number of context lines + external represents whether diff will be done using external tools text treats all files as text showfunc enables diff -p output git enables the git extended patch format @@ -56,6 +57,7 @@ defaults = { b'context': 3, + b'external': False, b'text': False, b'showfunc': False, b'git': False,