77

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
rampion
  • 87,131
  • 49
  • 199
  • 315
  • possible duplicate of [What's the equivalent of use-commit-times for git?](http://stackoverflow.com/questions/1964470/whats-the-equivalent-of-use-commit-times-for-git) – MestreLion Aug 14 '14 at 20:58
  • And also: http://stackoverflow.com/questions/2179722/checking-out-old-file-with-original-create-modified-timestamps – MestreLion Aug 14 '14 at 20:59
  • 1
    None of the scripts here worked for me; what did was this answer on the linked-to similar question: http://stackoverflow.com/a/30143117/841830 – Darren Cook Jan 22 '17 at 14:28
  • 1
    Candidates for the canonical question: *[What's the equivalent of Subversion's "use-commit-times" for Git?](https://stackoverflow.com/questions/1964470/)* (2009) and *[Checking out old files WITH original create/modified timestamps](https://stackoverflow.com/questions/2179722)* (2010). Mercurial has [the Timestamp extension](https://stackoverflow.com/a/7809151) (though that does not help much). – Peter Mortensen Sep 17 '21 at 10:11

11 Answers11

38

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 @0`git 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.

snipsnipsnip
  • 2,268
  • 2
  • 33
  • 34
TallOne
  • 391
  • 3
  • 2
20

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chris Johnsen
  • 214,407
  • 26
  • 209
  • 186
  • 9
    I agree, it's a good default. I was just hoping there was a flag or command for non-default behaviour. I suppose for now I'll put off playing with git's plumbing in favor of just using `touch` to get the job done. – rampion Mar 17 '10 at 14:07
  • 5
    It would be nice if `git archive` could restore the original file modification time of every file in the archive, but unfortunately, it sets all the modification times to either the current time or the timestamp of the selected commit. – Derek Mahar Mar 25 '10 at 19:09
  • 1
    I think you mean `touch -r configure configure.ac` rather than -t, as -t wants a timestamp and -r references another file for the timestamp to use. – David Gardner Jun 21 '11 at 09:41
  • 11
    fwiw, limiting your version control system based on unrobust tools like make is not a good way to design a system – B T Jul 17 '13 at 19:59
  • `utime` only changes `mtim` and you'd need to change `ctim`. – ArtemGr Dec 06 '14 at 20:00
  • 4
    You are wrong: the information is not there in the `git` repo. That is one big disadvantage of `git` these days. – Robert Siemer Feb 17 '15 at 17:40
  • Visual Studio recognizes if the file modification date has changed and rebuilds correctly if the date becomes older than it was (i.e. when using Perforce). So I agree with @BT - this feature would be useful in many cases. Might be complex to implement, of course. – DarkWanderer Sep 01 '15 at 12:47
  • 1
    @Chris: Your reasoning is argumenting from a wrong use case: When rolling back a file, you should always execute a full rebuild. Rolling back is an action that explicitly falls out of the standard incremental build scenario. You'd have the same use case when manually copying a file back into the repository without git. git should behave transparently here and maintain the files' original creation/modification date. – AxD Jun 23 '17 at 09:38
  • +1 for the comment mentioning `-t`. This led me to discover `-t` or `-d` for setting arbitrary dates. Note: It appears to set the date modified, rather than date created (unless `-a` is specified). – mpag Jul 12 '18 at 07:41
  • 5
    All the comments about how it would break builds etc. If it was a flag, and if I did something like git pull pictures --restore-times . - when I'm pulling down 340 MB of picture files (don't care about the dates, other than I'm doing a robocopy to another directory when I Update... And msbuild? No, not in this case, these are other files..) . Adding the flag makes sense, if you want to use it, you have to understand it, if you don't and it is off by default, you are good. Horrible reason to leave it out: "someone might use it without understanding the impact".. – Traderhut Games Dec 18 '18 at 22:38
  • A good dependency track system does store a file hash instead of a modification timestamp. The timestamp might be a use for a poor or old dependency track system, which currently is outdated. So that argument is outdated with it. – Andry Feb 09 '22 at 09:33
  • For those of us not using a timestamp based build system, it would be a nice config to revert timestamps as well. – Anders Lindén Jun 09 '22 at 11:45
18

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
Léa Gris
  • 17,497
  • 4
  • 32
  • 41
stefanct
  • 2,503
  • 1
  • 28
  • 32
  • 1
    My version of git is old enough that it doesn't understand `%cI`, but I found that `touch -d "$(git log --pretty=format:%ci -1 "$rev" -- "$f")"` (with the quotes!) works as well. – Wolfgang Nov 11 '16 at 03:27
  • The flag is contained since git v2.2.0. After looking at the POSIX specification of `touch` (cf. http://pubs.opengroup.org/onlinepubs/9699919799/utilities/touch.html) I agree that it will most likely work as good as `%Ci`, which unfortunately does not mean either will work on very strict implementations of POSIX. The space used by `%ci` instead of `T` is allowed by POSIX. However, the suffix of `+/-hh:mm` (with or without leading space) is not mandatory to be understood by a POSIX-compatible `touch`. bummer :) – stefanct Nov 11 '16 at 10:41
  • `-d` is not supported on BSD touch found in OSX (which is POSIX) – arve0 Apr 02 '17 at 19:10
  • Apparently not in its current version of 2008, because there `-d` is defined: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/touch.html – stefanct Apr 03 '17 at 22:20
  • 2
    To get this to work on macOS, use `touch -t` instead of `touch -d` and `--pretty=format:%cd --date=format:%Y%m%d%H%m.%S` instead of `--pretty=format:%cI` – Clement Cherlin Jan 04 '19 at 15:40
  • 3
    Combining this answer with @ClementCherlin's tip, you get: ```rev=HEAD; for f in $(git ls-tree -r -t --full-name --name-only "$rev") ; do touch -t $(git log --pretty=format:%cd --date=format:%Y%m%d%H%m.%S -1 "$rev" -- "$f") "$f"; done``` – mwag Aug 05 '19 at 01:42
  • I would recommend setting `IFS=$(echo -en "\n\b")` around the `for` loop to avoid errors on filenames with spaces... – Chris Maes Oct 07 '19 at 09:35
  • 3
    @ClementCherlin Ooops you have two lower-case `%m`s for month in there -- the second one should be `%M` for minute: `--date=format:%Y%m%d%H%M.%S` – DouglasDD Dec 16 '19 at 17:02
  • 1
    any chance you would like to update your answer with @mwag comment? I have tested mwag comment on both OSX and Debian and it seems to be working on both! – Tommy Apr 21 '21 at 16:27
  • 1
    Revised for @ClementCherlin's further m/M catch: `rev=HEAD; for f in $(git ls-tree -r -t --full-name --name-only "$rev") ; do touch -t $(git log --pretty=format:%cd --date=format:%Y%m%d%H%M.%S -1 "$rev" -- "$f") "$f"; done` – mwag Apr 21 '21 at 19:20
  • just change the `touch -t ... "$f"; ` line to `touch -t .. "$f" &` to fork the touch command( DANGER: LOAD AHEAD ON MANY FILES ), maybe make it look like `"$f" & sleep 0.05; ` to only fork 20 touch operations per second – Bencho Naut Mar 17 '23 at 12:40
16

Git Tools:

sudo apt install git-restore-mtime
cd [repo]
git restore-mtime
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Andrew Murphy
  • 1,336
  • 1
  • 11
  • 10
  • Haha! That is actually an Answer from Stackoverflow, which made it into all linux distros meanwhile https://stackoverflow.com/a/13284229 – Ichthyo Nov 10 '22 at 10:00
6

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alec the Geek
  • 1,970
  • 14
  • 14
  • that'd be nice for future projects :) – rampion Mar 18 '10 at 15:21
  • you can use an adler or crc or even rc4, something fast the sha1 is too much effort even for modern cpus – Michael McCallum Sep 29 '15 at 08:48
  • I've actually found a better solution, I store the last modification time of two of important files in a text-file as part of the registry, and restore it later to the file using `touch -t`, it is simple, yet effective, this is so I could use the last-modification time calculation in server side PHP or Apache etag and last mod. headers. this is how you get the format to use in `touch` from `stat`: first get the unix time using `$(stat --format=%s _your_filename_)` then feed it to `date --date="@_the_unix_time_from_before" "+%Y%m%d%H%M"` -> `touch -t "_result_" --time=modify _filename_` =>`☔+☕` –  Jan 06 '16 at 21:19
  • Note for anyone who only needs to cache compilation results: The hash-based approach described here is exactly what tools like [ccache](https://ccache.dev/) (C/C++/...) and [sccache](https://github.com/mozilla/sccache) (C/C++/Rust/...) do for simple compilation steps. – smheidrich Oct 22 '22 at 10:03
3

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 <tchrist@perl.com>

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;
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
tchrist
  • 78,834
  • 30
  • 123
  • 180
3

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
gsl
  • 1,063
  • 1
  • 16
  • 27
  • 1
    Actually this is the only correct answer. Any other solutions in other answers restore only commit time rather than modification time, because git doesn't even record modification time. – Zim Nov 21 '21 at 13:02
2

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
PilouPili
  • 2,601
  • 2
  • 17
  • 31
1

The following sets the timestamps of the files to the last commit time for me:

git log --pretty=%at --name-status | 
perl -ane '($x,$f)=@F; $x or next; $t=$x, next if !$f; next if $s{$f} || $x!~/[AM]/; $s{$f}++; utime($t,$t,$f)'
Bert Jahn
  • 11
  • 2
0

since the post of @stefanct has too many open edits pending :

here is a version that forks 20 touch commands per second ( hint: will fail on openwrt and other systems without proper sleep 0.x ( not integer) capable implementation )

The following shell script should work on any POSIX-compatible system to set >the modification and access timestamp of all tracked files (and directories).

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" & sleep 0.05 ;
done
wait

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

not anymore ...

Bencho Naut
  • 348
  • 1
  • 6
-1

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Guy Chauliac
  • 640
  • 7
  • 8
  • 1
    Note: timestamps after a checkout are no longer *always* modified (with Git 2.2.2+, January 2015): http://stackoverflow.com/a/28256177/6309 – VonC Jan 31 '15 at 20:32