Search code examples
gitgit-diffgit-logdifftoolmergetool

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


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?


Solution

  • 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.