Patchwork D8972: [WIP] diff: add a `--tool` flag

login
register
mail settings
Submitter phabricator
Date Aug. 29, 2020, 2:18 p.m.
Message ID <differential-rev-PHID-DREV-m26hrs7igal7i7lnbef5-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/47073/
State New
Headers show

Comments

phabricator - Aug. 29, 2020, 2:18 p.m.
pulkit created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  This is a WIP because it's mostly plumbing at the moment and wanted to share it
  with others.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D8972

AFFECTED FILES
  hgext/extdiff.py
  mercurial/configitems.py
  mercurial/diffutil.py
  mercurial/logcmdutil.py
  mercurial/mdiff.py

CHANGE DETAILS




To: pulkit, #hg-reviewers
Cc: mercurial-patches, mercurial-devel

Patch

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,
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/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/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/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)