Restore a file's modification time in Git
GitGit Problem Overview
I understand the default Git behaviour of updating the modification time every time it changes a file, but there are times when I want to restore a file's original modification time.
Is there a way I can tell Git to do this?
(As an example, when working on a large project, I made some changes to configure.ac
, found out that autotools doesn't work on my system, and wanted to restore configure.ac
's to its original contents and modification time so that make
doesn't try to update configure
with my broken autotools.)
Git Solutions
Solution 1 - Git
Restore the modificaton time of a list of files to the author date of the their last commit with
gitmtim(){ local f;for f;do touch -d @0git log --pretty=%at -n1 -- "$f"
"$f"; done;}; gitmtim configure.ac
It will not change directories recursively, though.
If you want to change a whole working tree, e.g. after a fresh clone or checkout, you may try
git log --pretty=%at --name-status --reverse | perl -ane '($x,$f)=@F;next if !$x;$t=$x,next if !defined($f)||$s{$f};$s{$f}=utime($t,$t,$f),next if $x=~/[AM]/;'
NB: I grepped for utime in builtin/clone.c and got no matches.
Solution 2 - Git
Git does not do this. Like your linked FAQ says, it would break using timestamp-based dependency analysis tools like make.
Think about what would happen if old timestamps were applied to files checked out from ‘old’ commits:
- make from a clean directory works fine
- checkout an older branch/tag/commit (the files would have timestamps older than the build products now!)
- make now does nothing, because all the build products are newer than their dependencies
But, if you really want it, all the information is there. You could write your own tool to do it.
In your case, just use something like touch -r configure configure.ac
to reset the modification time of only configure.ac
, (or bring configure forward in time with touch configure
).
Actually, this is an easy “exercise for the reader” if you want to practice reading C code. The function that changes timestamps is utime
or utimes
. Search the code for uses of those functions (hint: git grep utime
in a git.git clone). If there are some uses, analyze the code paths to find out when it updates timestamps.
Solution 3 - Git
The following shell script should work on any POSIX-compatible system to set the modification and access timestamp of all tracked files (and directories). The only downside I could determine yet is that it is quite slow but that's fine for my use case (setting the right dates when producing release archives).
rev=HEAD for f in $(git ls-tree -r -t --full-name --name-only "$rev") ; do touch -d $(git log --pretty=format:%cI -1 "$rev" -- "$f") "$f"; done
Solution 4 - Git
sudo apt install git-restore-mtime
cd [repo]
git restore-mtime
Solution 5 - Git
I believe the 'proper' fix is to actually compare the SHA-1 hash value of each input file to see if it's changed from the last build.
This is a lot of work. However, I have started a project to try and create a proof of concept (still very early stage). As well as identifying the correct build steps, it's also designed to create an audit list of input files for later forensics.
See Git building -- it's based on something similar I did a few years ago with Subversion.
Solution 6 - Git
This tool should do the trick. It updates mtimes to the author time and the atimes to the committer time. It would work as a checkout hook.
Run with DEBUG=1
to get it to tell you exactly what it's doing.
Notice also that it uses no modules, just basic Perl, so should run anywhere.
#!/usr/bin/perl
# git-utimes: update file times to last commit on them
# Tom Christiansen <[email protected]>
use v5.10; # for pipe open on a list
use strict;
use warnings;
use constant DEBUG => !!$ENV{DEBUG};
my @gitlog = (
qw[git log --name-only],
qq[--format=format:"%s" %ct %at],
@ARGV,
);
open(GITLOG, "-|", @gitlog) || die "$0: Cannot open pipe from `@gitlog`: $!\n";
our $Oops = 0;
our %Seen;
$/ = "";
while (<GITLOG>) {
next if /^"Merge branch/;
s/^"(.*)" // || die;
my $msg = $1;
s/^(\d+) (\d+)\n//gm || die;
my @times = ($1, $2); # last one, others are merges
for my $file (split /\R/) { # I'll kill you if you put vertical whitespace in our paths
next if $Seen{$file}++;
next if !-f $file; # no longer here
printf "atime=%s mtime=%s %s -- %s\n",
(map { scalar localtime $_ } @times),
$file, $msg,
if DEBUG;
unless (utime @times, $file) {
print STDERR "$0: Couldn't reset utimes on $file: $!\n";
$Oops++;
}
}
}
exit $Oops;
Solution 7 - Git
We had the same issue at work and have successfully been using the git-store-meta Perl script by Danny Lin.
It definitely solved the problem indicated in your question.
Solution 8 - Git
This takes most of what stefanct proposed, but while implementing a similar script I just added a parallel feature.
In my case (1000 files) I went from 60 seconds to 15 seconds to do the operation doing it in parallel.
#!/bin/bash
change_date() {
local dd=`git log -1 HEAD --pretty="%ci" -- $1`
if [ -z "$dd" ];
then echo "$1 is not versionned";
else touch -d "$dd" $1;
fi
}
#list_of_files = find .
list_of_files=`git ls-tree -r -t --full-name --name-only HEAD`
for f in $list_of_files;do
if test "$(jobs | wc -l)" -ge 16; then
wait
fi
{
change_date $f;
} &
done
wait
You can adjust the number of parallel jobs allowed by changing this line:
test "$(jobs | wc -l)" -ge 16
Solution 9 - Git
I wrote a little tool that will allow you to restore the modification time of the files in a directory after doing a merge or checkout with Git.
https://bitbucket.org/chabernac/metadatarestore/wiki/Home
Use the tool as a hook in Git when doing a commit, checkout or merge. See 8.3 Customizing Git - Git Hooks for information about Git hooks. You can find examples of Git hooks in the .git/hooks
directory of your project.