84

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

4

13 Answers 13

42

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.

Sign up to request clarification or add additional context in comments.

4 Comments

the perl version seems not work..
Re "may try": Does it work or not? Where and how was it tested?
Doesn't work with touch from busybox: touch: invalid date '1708888815'.
This sets mtime to "last commit time of the path" not "creation time of the blob / contents" in case you have moved/renamed the file. Moving files does not change mtime in filesystem-level.
25

Git Tools:

sudo apt install git-restore-mtime cd [repo] git restore-mtime 

5 Comments

Haha! That is actually an Answer from Stackoverflow, which made it into all linux distros meanwhile stackoverflow.com/a/13284229
Arch: sudo pacman -S git-warp-time github.com/alerque/git-warp-time
@xeruf: git-warp-time is an unrelated project. git-restore-mtime is also packaged into Arch: aur.archlinux.org/packages/git-tools-git, so sudo pacman -S git-tools-git
unrelated, but afaik with the same functionality
Sadly this sets mtime to "last commit time of the path" not "creation time of the blob / contents" in case you have moved/renamed the file. Moving files should not change mtime.
21

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.

12 Comments

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.
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.
fwiw, limiting your version control system based on unrobust tools like make is not a good way to design a system
You are wrong: the information is not there in the git repo. That is one big disadvantage of git these days.
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"..
|
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 

11 Comments

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.
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
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
@ClementCherlin Ooops you have two lower-case %ms for month in there -- the second one should be %M for minute: --date=format:%Y%m%d%H%M.%S
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!
|
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.

4 Comments

that'd be nice for future projects :)
you can use an adler or crc or even rc4, something fast the sha1 is too much effort even for modern cpus
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_ =>☔+☕
Note for anyone who only needs to cache compilation results: The hash-based approach described here is exactly what tools like ccache (C/C++/...) and sccache (C/C++/Rust/...) do for simple compilation steps.
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 <[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; 

Comments

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.

1 Comment

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

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 

Comments

2

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

Comments

0

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.

1 Comment

Note: timestamps after a checkout are no longer always modified (with Git 2.2.2+, January 2015): stackoverflow.com/a/28256177/6309
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 ...

Comments

0

This simple extended command works just fine for me:

git ls-files | while read file; do echo $file; touch -d $(git log --date=local -1 --format="@%ct" "$file") "$file"; done 

No additional modules needed.

Comments

0

We don't really need to restore the timestamp, there is a better trick to support build tool:

replace

git checkout new_branch 

with

git rebase origin/main new_branch 

With that, you always works on the main branch with only simple changes.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.