Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a trick to git log --follow a directory which has been renamed?

Tags:

git

The git command has a useful command to follow a file after a rename, as in git log --follow path/to/some/file. Unfortunately, it works only for an individual file. I'd like to be able to do the equivalent of git log --follow some/directory.

One way of doing this would probably be to massage the output of git ls-tree and do that in a loop, but the problem then becomes that commits affecting multiple files won't be "coalesced" into one commit.

Is there a better way to do it? Note: using git 2.7.4

like image 859
fge Avatar asked Aug 10 '16 10:08

fge


People also ask

How do I see changed files in git log?

Find what file changed in a commit To find out which files changed in a given commit, use the git log --raw command. It's the fastest and simplest way to get insight into which files a commit affects.

How do I rename a directory in git without losing history?

Renaming a file or directory in git preserves its history. There are two main ways to git rename file or directory. You can use the mv or git rm commands.

Does git keep track of moved files?

Git keeps track of changes to files in the working directory of a repository by their name. When you move or rename a file, Git doesn't see that a file was moved; it sees that there's a file with a new filename, and the file with the old filename was deleted (even if the contents remain the same).

Is git log chronological?

By default, with no arguments, git log lists the commits made in that repository in reverse chronological order; that is, the most recent commits show up first.


2 Answers

No.

Git does not store directories, only files. When you use any path name to commands like git log or git diff that look at entire commits, Git essentially says "start with the whole commit, then shrink it down to file(s) matching that path". Directories here simply wind up selecting every file in the directory.

The --follow option can only follow one file. So if you could somehow manage to get it to apply to a directory, Git would first turn the directory into a set of files, then pick one of those files and follow just that one.

(The actual --follow code is terribly hacky. It leverages off the rename detection code, but only works when commits are being compared in the newer-to-older order: if you add --reverse, --follow never works at all. Probably the whole thing needs to be thrown out and re-coded. Perhaps by re-coding it, you could make it handle multiple file names, and even directories full of files.)

like image 57
torek Avatar answered Nov 15 '22 13:11

torek


There does not seem to be a builtin way to do this.

One can do this using a script and following a simple algorithm:

  • For each file generate a list of commits associated with it
  • Combine all the lists and do a stable sort on them (this is tricky because determining ordering of commits isn't straightforward)
  • Iteratively execute git log for each of the commits and pipe it all through less

Here's a hacky way to do it using python 3 and the sh module, won't work on Windows for various reasons.

#!/usr/bin/env python3

import os
import shlex
import sys
import tempfile

import sh

def _get_commits(fname):
    # Ask for the commit and the timestamp
    # .. the timestamp doesn't guarantee ordering, but good enough
    for c in sh.git('log', '--follow', '--pretty=%ct %h', fname,
                   _tty_out=False, _iter=True):
        c = c.strip().split()
        yield int(c[0]), c[1]

def git_log_follow_multi(filenames):
    if len(filenames) == 0:
        print("Specify at least one file to log")
    elif len(filenames) <= 1:
        os.system('git log --follow -p %s' % filenames[0])
    else:
        # Use git log to generate lists of commits for each file, sort
        commits = []
        for fname in filenames:
            commits += _get_commits(fname)

        # Sort the lists (python's sort is stable)
        commits.sort(reverse=True)

        # Uniquify (http://www.peterbe.com/plog/uniqifiers-benchmark)
        seen = set()
        seen_add = seen.add
        commits = [c for c in commits if not (c in seen or seen_add(c))]

        # Finally, display them
        tname = None

        try:
            file_list = ' '.join(shlex.quote(fname) for fname in filenames)

            with tempfile.NamedTemporaryFile(mode='w', delete=False) as fp:
                tname = fp.name
                for _, commit in commits:
                    fp.write('git log -p -1 --color %s %s\n' % (commit, file_list))

            # Use os.system to make our lives easier
            os.system('bash %s | less -FRX' % tname)
        finally:
            if tname:
                os.unlink(tname)


if __name__ == '__main__':
    git_log_follow_multi(sys.argv[1:])

Now, this script doesn't exactly meet your needs since it takes a list of files, but you can execute it using a glob and it'll do what you're looking for.

./script.py src/*
like image 38
Dustin Spicuzza Avatar answered Nov 15 '22 12:11

Dustin Spicuzza