What's the equivalent of use-commit-times for git?

If, however you really want to use commit times for timestamps when checking out then try using this script and place it (as executable) in the file $GIT_DIR/.git/hooks/post-checkout:

#!/bin/sh -e

OS=${OS:-`uname`}
old_rev="$1"
new_rev="$2"

get_file_rev() {
    git rev-list -n 1 "$new_rev" "$1"
}

if   [ "$OS" = 'Linux' ]
then
    update_file_timestamp() {
        file_time=`git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1`
        touch -d "$file_time" "$1"
    }
elif [ "$OS" = 'FreeBSD' ]
then
    update_file_timestamp() {
        file_time=`date -r "$(git show --pretty=format:%at --abbrev-commit "$(get_file_rev "$1")" | head -n 1)" '+%Y%m%d%H%M.%S'`
        touch -h -t "$file_time" "$1"
    }
else
    echo "timestamp changing not implemented" >&2
    exit 1
fi

IFS=`printf '\t\n\t'`

git ls-files | while read -r file
do
    update_file_timestamp "$file"
done

Note however, that this script will cause quite a large delay for checking out large repositories (where large means large amount of files, not large file sizes).


UPDATE: My solution is now packaged into Debian/Ubuntu/Mint, Fedora, Gentoo and possibly other distros:

https://github.com/MestreLion/git-tools#install

sudo apt install git-restore-mtime  # Debian/Ubuntu/Mint
yum install git-tools               # Fedora/ RHEL / CentOS
emerge dev-vcs/git-tools            # Gentoo

IMHO, not storing timestamps (and other metadata like permissions and ownership) is a big limitation of git.

Linus' rationale of timestamps being harmful just because it "confuses make" is lame:

  • make clean is enough to fix any problems.

  • Applies only to projects that use make, mostly C/C++. It is completely moot for scripts like Python, Perl, or documentation in general.

  • There is only harm if you apply the timestamps. There would be no harm in storing them in repo. Applying them could be a simple --with-timestamps option for git checkout and friends (clone, pull etc), at the user's discretion.

Both Bazaar and Mercurial stores metadata. Users can apply them or not when checking out. But in git, since original timestamps are not even available in the repo, there is no such option.

So, for a very small gain (not having to re-compile everything) that is specific to a subset of projects, git as a general DVCS was crippled, some information from about files is lost, and, as Linus said, it's INFEASIBLE to do it now. Sad.

That said, may I offer 2 approaches?

1 - http://repo.or.cz/w/metastore.git , by David Härdeman. Tries to do what git should have done in the first place: stores metadata (not only timestamps) in the repo when commiting (via pre-commit hook), and re-applies them when pulling (also via hooks).

2 - My humble version of a script I used before for generating release tarballs. As mentioned in other answers, the approach is a little different: to apply for each file the timestamp of the most recent commit where the file was modified.

  • git-restore-mtime, with lots of options, supports any repository layout, and runs on Python 3.

Below is a really bare-bones version of the script, as a proof-of-concept, on Python 2.7. For actual usage I strongly recommend the full version above:

#!/usr/bin/env python
# Bare-bones version. Current dir must be top-level of work tree.
# Usage: git-restore-mtime-bare [pathspecs...]
# By default update all files
# Example: to only update only the README and files in ./doc:
# git-restore-mtime-bare README doc

import subprocess, shlex
import sys, os.path

filelist = set()
for path in (sys.argv[1:] or [os.path.curdir]):
    if os.path.isfile(path) or os.path.islink(path):
        filelist.add(os.path.relpath(path))
    elif os.path.isdir(path):
        for root, subdirs, files in os.walk(path):
            if '.git' in subdirs:
                subdirs.remove('.git')
            for file in files:
                filelist.add(os.path.relpath(os.path.join(root, file)))

mtime = 0
gitobj = subprocess.Popen(shlex.split('git whatchanged --pretty=%at'),
                          stdout=subprocess.PIPE)
for line in gitobj.stdout:
    line = line.strip()
    if not line: continue

    if line.startswith(':'):
        file = line.split('\t')[-1]
        if file in filelist:
            filelist.remove(file)
            #print mtime, file
            os.utime(file, (mtime, mtime))
    else:
        mtime = long(line)

    # All files done?
    if not filelist:
        break

Performance is pretty impressive, even for monster projects wine, git or even the linux kernel:

bash
# 0.27 seconds
# 5,750 log lines processed
# 62 commits evaluated
# 1,155 updated files

git
# 3.71 seconds
# 96,702 log lines processed
# 24,217 commits evaluated
# 2,495 updated files

wine
# 13.53 seconds
# 443,979 log lines processed
# 91,703 commits evaluated
# 6,005 updated files

linux kernel
# 59.11 seconds
# 1,484,567 log lines processed
# 313,164 commits evaluated
# 40,902 updated files

I am not sure this would be appropriate for a DVCS (as in "Distributed" VCS)

The huge discussion had already took place in 2007 (see this thread)

And some of Linus's answer were not too keen on the idea. Here is one sample:

I'm sorry. If you don't see how it's WRONG to set a datestamp back to something that will make a simple "make" miscompile your source tree, I don't know what defintiion of "wrong" you are talking about.
It's WRONG.
It's STUPID.
And it's totally INFEASIBLE to implement.


(Note: small improvement: after a checkout, timestamps of up-to-date files are no longer modified (Git 2.2.2+, January 2015): "git checkout - how can I maintain timestamps when switching branches?".)


The long answer was:

I think you're much better off just using multiple repositories instead, if this is something common.

Messing with timestamps is not going to work in general. It's just going to guarantee you that "make" gets confused in a really bad way, and does not recompile enough instead of recompiling too much.

Git does make it possible to do your "check the other branch out" thing very easily, in many different ways.

You could create some trivial script that does any of the following (ranging from the trivial to the more exotic):

  • just create a new repo:
    git clone old new
    cd new
    git checkout origin/<branch>

and there you are. The old timestamps are fine in your old repo, and you can work (and compile) in the new one, without affectign the old one at all.

Use the flags "-n -l -s" to "git clone" to basically make this instantaneous. For lots of files (eg big repos like the kernel), it's not going to be as fast as just switching branches, but havign a second copy of the working tree can be quite powerful.

  • do the same thing with just a tar-ball instead, if you want to
    git archive --format=tar --prefix=new-tree/ <branchname> |
            (cd .. ; tar xvf -)

which is really quite fast, if you just want a snapshot.

  • get used to "git show", and just look at individual files.
    This is actually really useful at times. You just do
    git show otherbranch:filename

in one xterm window, and look at the same file in your current branch in another window. In particular, this should be trivial to do with scriptable editors (ie GNU emacs), where it should be possible to basically have a whole "dired mode" for other branches within the editor, using this. For all I know, the emacs git mode already offers something like this (I'm not an emacs user)

  • and in the extreme example of that "virtual directory" thing, there was at least somebody working on a git plugin for FUSE, ie you could literally just have virtual directories showing all your branches.

and I'm sure any of the above are better alternatives than playing games with file timestamps.

Linus

Tags:

Git