Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Now instead of running git-difftool n+1 times we run it once. The old implementation used the files created by git-difftool directly, but since it deletes the temporary files as soon as it completes, we effectively had to keep it running to keep the files alive. This meant having O(n) pipes open. It also turned out that it didn't work correctly with -M (show moves/renames) as the --name-only flag behaves... inconsistently in this mode. The new implementation has a helper tool that copies (or hardlinks, when possible) the files that appear to be temporary. This means we only run git-difftool once, and we don't rely on the (flaky) --name-only flag so -M actually works as expected. (Well, mostly. Currently we're copying /dev/null, which is kind of weird.)
- Loading branch information
1 parent
162e043
commit 0087d68
Showing
2 changed files
with
104 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
#!/usr/bin/env python | ||
|
||
import errno | ||
import os | ||
import shutil | ||
import stat | ||
import sys | ||
|
||
""" | ||
Helper used by git-multidiff. | ||
This script is used as the "difftool" when invoking git-difftool. It | ||
creates copies of (or hardlinks to) files that appear to be temporary, | ||
and records the names of the files that git-multidiff will need to diff. | ||
""" | ||
|
||
# I can't believe this isn't already in shutil. | ||
def mkdirp(dir): | ||
""" | ||
Python version of 'mkdir -p'. | ||
Creates directory, including parent directories. Doesn't get upset if | ||
directory already exists. | ||
""" | ||
try: | ||
os.makedirs(dir) | ||
except OSError as e: | ||
if e.errno != errno.EEXIST: | ||
raise | ||
|
||
def main(argv): | ||
DEBUG = False | ||
# TODO: find some way to pass fatal errors up to top-level? | ||
tmpdir = os.environ['GIT_MULTIDIFF_TEMP'] | ||
out = open(os.path.join(tmpdir, 'args'), 'a') | ||
for fnam in argv[1:]: | ||
# TODO: treat files in /dev/ like files in workspace? | ||
if os.path.isabs(fnam): | ||
if fnam.startswith('//'): | ||
# A leading double-slash has system defined meaning in POSIX so | ||
# treat such files as different from others just in case. | ||
dest = os.path.join(tmpdir, 'dslash') | ||
else: | ||
dest = os.path.join(tmpdir, 'slash') | ||
dest += fnam | ||
mkdirp(os.path.dirname(dest)) | ||
try: | ||
os.link(fnam, dest) | ||
if DEBUG: print 'ln %r -> %r' % (fnam, dest) | ||
except OSError: | ||
# TODO: this doesn't work with multiple occurrences of the same | ||
# file (in particular, /dev/null) as we make the destination | ||
# read-only. This should get fixed once the perm stuff is moved. | ||
shutil.copy2(fnam, dest) | ||
if DEBUG: print 'cp %r -> %r' % (fnam, dest) | ||
try: | ||
# Make copy read-only | ||
perms = os.stat(dest).st_mode \ | ||
& ~(stat.S_IWGRP | stat.S_IWOTH | stat.S_IWUSR) | ||
os.chmod(dest, perms) | ||
if DEBUG: print 'chmod a-r %r' % dest | ||
except IOError: | ||
pass # we tried, but it isn't critical that we succeed | ||
else: | ||
dest = fnam | ||
out.write(dest) | ||
out.write('\0') | ||
out.close() | ||
|
||
if __name__ == '__main__': | ||
main(sys.argv) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,45 @@ | ||
#!/usr/bin/env python | ||
|
||
import sys | ||
import commands | ||
import os | ||
import shutil | ||
import subprocess | ||
import sys | ||
import tempfile | ||
|
||
""" | ||
Like git-difftool, but executes all diffs in parallel. | ||
TODO: | ||
- run git diff only once instead of n+1 times. The current setup is | ||
flakey when moves/renames happen and -M is passed. | ||
- parameterize multi-diff tool | ||
""" | ||
|
||
def cmd_lines(*args, **kwargs): | ||
""" | ||
Calls subprocess.check_output, but returns result as a list of lines, | ||
with trailing newlines removed. A newline at the end of the file is | ||
also ignored. | ||
""" | ||
output = subprocess.check_output(*args, **kwargs).split('\n') | ||
# deal with trailing newline | ||
if not output[-1]: | ||
del output[-1] | ||
return output | ||
|
||
# git diff --name-only "$@" | while read filename; do | ||
# git difftool "$@" --no-prompt "$filename" & | ||
def main(argv): | ||
diff_fnams = cmd_lines(['git', 'diff', '--name-only'] + argv[1:]) | ||
children = [] | ||
pairs = [] | ||
for fnam in diff_fnams: | ||
child = subprocess.Popen( | ||
['git', 'difftool'] + argv[1:] + ['--no-prompt', '-x', 'echo-and-hang', '--', fnam], | ||
bufsize=1, | ||
stdin=subprocess.PIPE, | ||
stdout=subprocess.PIPE) | ||
pairs.append(child.stdout.readline()[:-1].split('\000')) | ||
children.append(child) | ||
|
||
args = [] | ||
for old, new in pairs: | ||
args.extend((old, new)) | ||
# status = subprocess.call( | ||
# ['mvim', '-f', '+set lines=999 columns=999', '-o'] | ||
# + args | ||
# + ['+silent call TabMultiDiff()']) | ||
status = subprocess.call( | ||
['mvim', '-f', '-c', 'silent call TabMultiDiffMaximized()'] | ||
+ args) | ||
|
||
# Terminate children gracefully... | ||
for child in children: | ||
child.stdin.write('\n') | ||
child.stdin.flush() | ||
child.wait() | ||
try: | ||
tool = subprocess.check_output(['git', 'config', '--get', 'multidiff.tool']).strip() | ||
except subprocess.CalledProcessError as exc: | ||
print >>sys.stderr, \ | ||
'Error: %r returned status %r' % (' '.join(exc.cmd), exc.returncode) | ||
sys.exit(1) | ||
|
||
tmpdir = tempfile.mkdtemp() | ||
try: | ||
os.environ['GIT_MULTIDIFF_TEMP'] = tmpdir | ||
argsfile = os.path.join(tmpdir, 'args') | ||
open(argsfile, 'w').close() | ||
subprocess.check_call(['git', 'difftool', '-y', '-x', | ||
'_git-multidiff-helper'] + argv[1:]) | ||
difftool_args = [x for x in | ||
open(argsfile).read().split('\0') | ||
if x] | ||
assert not (len(difftool_args) % 2), \ | ||
"Expected even number of files, but got %d" % len(difftool_args) | ||
# TODO: move chmod stuff here, check that link count == 1 and in tmpdir. | ||
|
||
if difftool_args: | ||
# Instead of using `subprocess.check_call([tool] + difftool_args)` | ||
# we use os.system. This makes it possible for the tool to contain | ||
# flags (or potentially other stuff evaluated by the shell). | ||
os.system(tool + ''.join(commands.mkarg(x) for x in difftool_args)) | ||
finally: | ||
shutil.rmtree(tmpdir, ignore_errors=True) | ||
|
||
if __name__ == '__main__': | ||
main(sys.argv) |