View full file diff of `x` commits of a single file's history (that's hosted in git)
Asked Answered
S

1

4

Say I have a file in git called filex.code, and I want to see the full code of the last x versions of that file with each changed section highlighted -- all in one place. So an x-paned commit history of filex.code, almost as if I were doing an x-paned diff, but viewing historical versions rather than merging from different branches.

The greater x, the better. Crossplatform would be great, but any of the Big Three works. Being able to edit the latest version would also be great, but read-only visualization is plenty.

Note that this is different from a simple history of commits to a file, so the otherwise wonderful gitk path/to/file (or SourceTree or whatever visual git client you love) isn't what I'm looking for. git log -p also comes close, and its output tantalizingly includes all the information I'd want, just that it's all in a linear, almost "procedural" output format rather than a good, relatively non-hierarchical, visual one like your favorite three-paned GUI'd mergetool's.

(Edit: Another really cool option that ultimately still experiences the shortcomings of only showing each line's latest source & a linear output is git blame, but it's cool.)

So I'm not precisely looking for setting up difftool either, I don't think. Rather than diffing two known versions of a file, I want to visualize x iterations of historical edits to a single file.

Asking too much? Is this a WTFA (Write The "Fantastic" App [yourself]) situation?

Lesser alternative: Is there a three-paned mergetool that I can trick into displaying the last three commits of a single file?

Shon answered 30/7, 2012 at 17:0 Comment(0)
P
3

This script opens last N revisions of the file side-by-side.

#!/usr/bin/env python
import os, sys, tempfile
from shutil import rmtree
from subprocess import call, Popen, PIPE
from optparse import OptionParser
from traceback import print_exc

COMMAND = 'vim -d'

def vcall(cmd, **kwargs):
    if options.verbose:
        print ' '.join(cmd)
    return call(' '.join(cmd) if sys.platform == 'darwin' else cmd, 
                **kwargs)

parser = OptionParser('usage: %s [-n <number of revisions>] filename' % 
                      sys.argv[0])
parser.add_option('-n', '--num', dest='N', type='int', 
                  help='number of revisions', default=3)
parser.add_option('-v', '--verbose', dest='verbose',
                  help='be verbose', default=False, action='store_true')
(options, args) = parser.parse_args()
if len(args) != 1:
    parser.error('incorrect number of arguments')
filename = args[0]

if vcall('git rev-parse'.split()) != 0:
    sys.exit(1)

try:
    cmd = 'git rev-list HEAD --'.split() + [filename]
    if options.verbose:
        print ' '.join(cmd)
    pipe = Popen(' '.join(cmd) if sys.platform == 'darwin' else cmd, 
                 stdout=PIPE).stdout
    revs = []
    for i, line in enumerate(pipe):
        if i == options.N:
            break
        revs.append(line.rstrip())
except:
    print_exc()

N = len(revs)
if N == 0:
    sys.exit('fatal: ambiguous argument %s: path not in the working tree' % 
             filename)
elif N < options.N:
    sys.stderr.write('%s has only %d revision%s' % 
                     (filename, N, 's' if N > 1 else ''))

tempdir = ''
try:
    tempdir = tempfile.mkdtemp()
    head, tail = os.path.split(filename)
    tempfiles = []
    for i in xrange(N):
        tempfiles.append(tail + ('.%d' % i if i else ''))
    for i, f in enumerate(tempfiles):
        with open(os.sep.join((tempdir, f)), 'w') as fout:
            vcall(['git', 'show', '%s:./%s' % (revs[i], filename)], stdout=fout)
    vcall(COMMAND.split() + list(reversed(tempfiles)), shell=True, cwd=tempdir)
except:
    print_exc()
finally:
    try:
        if tempdir and os.path.isdir(tempdir):
            rmtree(tempdir)
    except:
        print_exc()

Notes:

  1. Vimdiff has a limitation of highlighting diffs in only 4 (first) buffers, but as for showing side-by-side - all file revisions are shown (eg N=20 works great). To avoid the warning for N>4 use COMMAND = 'vim -O' to see versions side-by-side without any diffs at all.

  2. The script has grown to be too large for SO style, but it is quite bullet-proof now - yet simple enough for an experienced eye.

Percussive answered 31/7, 2012 at 7:17 Comment(4)
I put in an edit with a fix to work on OS X 10.7.4's stock python. cwd was working, but the call to vimdiff wasn't. Not sure why not. Code here, in case edit's rejected for whatever reason. Thanks. Really good stuff. Going to clean up comments.Shon
As for feeding arguments via one string to call instead of a list: "If a string is specified for args, it will be used as the name or path of the program to execute; this will only work if the program is being given no arguments." Seems as though OS X implementation is somewhat non-standard.Percussive
I've accepted a modified version of your patch, try again plz. What makes it stranger is that other instances of call in this script seem to be working fine under OS X. Do vim and git have different types?Percussive
Works great -- Go figure. Much cleaner fix in your version of the patch. Thanks again.Shon

© 2022 - 2024 — McMap. All rights reserved.