63

When I clone a Git repository using the "git clone ..." command, all cloned files in my local repository have the same modification time with date and time as when the git clone command was issued.

Is there a way to clone a remote Git repository with the actual modification time for each file?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user3302761
  • 631
  • 1
  • 5
  • 3
  • 3
    You can get the time of the last modification from `git log -n1 -- file`; that is what `git` is for. – Amadan Feb 12 '14 at 17:49
  • 9
    I do not quite understand the statement "this is what git is for". Why mod. time is not saved just like in CVS? – user3302761 Feb 12 '14 at 18:00
  • @Amadan: you only get the last commit time, not the last time the file was modified. – eltiare May 22 '16 at 16:45
  • 4
    @turnt It's not an issue... programs can change the modification times of files they create so it's a choice of the program – golimar Mar 27 '17 at 11:23
  • 2
    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:10
  • 1
    Does this answer your question? [Checking out old files WITH original create/modified timestamps](https://stackoverflow.com/questions/2179722/checking-out-old-files-with-original-create-modified-timestamps) – Ivan Baldo Dec 01 '21 at 22:44

9 Answers9

41

Git does not record timestamp for the files, since it is a Distributed VCS (meaning the time on your computer can be different from mine: there is no "central" notion of time and date)

The official argument for not recording that metadata is explained in this answer.

But you can find scripts which will attempt to restore a meaningful date, like this one (or a simpler version of the same idea).

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 5
    But it could save the local time on the remote end. How do I solve the problem with builds when files are complied based on their modification time? – user3302761 Feb 12 '14 at 18:08
  • @user3302761 "it could save the local time on the remote end": that is what the metastore.git approach does, as mentioned in http://stackoverflow.com/a/13284229/6309: stores metadata (not only timestamps) in the repo when commiting (via pre-commit hook), and re-applies them when pulling (also via hooks). – VonC Feb 12 '14 at 18:10
  • 6
    OK, thanks for the reply and suggested solutions. I saw the discussion,but the arguments for not saving mod times because git is version control system do not look strong to me. I used CVS for years and it has this feature and it does not hurt it. Indeed simple ls -ltr command shows you the order of modified files checked out from CVS repository. – user3302761 Feb 12 '14 at 18:23
  • Actually all solutions I saw before do not guaranty that the actual file is is in the remote repository. Git log shows commits but some commits could be still local. – user3302761 Feb 12 '14 at 20:13
  • 3
    The fact that builds rely on the modification time of files is actually a reason to _not_ store this as part of the metadata. If the mtime _is_ updated to the time of commit you'd have to start a clean build after checking out an older commit, since files would be considered older than the corresponding derived files and won't cause them to be rebuilt. (Assuming a build system that relies on the mtime, obviously.) – Magnus Bäck Feb 12 '14 at 20:20
  • 1
    I am not talking particularly about builds. Look at my comment "... Indeed simple ls -ltr command shows you the order of modified files checked out from CVS repository". – user3302761 Feb 14 '14 at 20:57
  • And why would you need to re-build not modified files? – user3302761 Feb 14 '14 at 21:03
  • 8
    It could save UTC time, can't it? – user626528 Feb 17 '15 at 12:07
  • @user626528 UTC or not, there is no guarantee that the time you are saving is the "right" one. – VonC Feb 17 '15 at 12:08
  • 4
    @VonC, you never have any guarantee that your time is right. That's not the reason to abandon using time, though. – user626528 Feb 17 '15 at 12:13
  • 1
    @user626528 I agree with you, and I have seen in the past some "workaround/hacks" to record it anyway: you can see various scripts in http://stackoverflow.com/q/1964470/6309. – VonC Feb 17 '15 at 12:29
  • 1
    [This](https://stackoverflow.com/a/13284229/6309) solution (as pointed in the answer) works really well. Tested on ubuntu 20.04 just now. :) – agent18 May 03 '21 at 12:48
33

You can retrieve the last modification date of all files in a Git repository (last commit time). See How to retrieve the last modification date of all files in a Git repository.

Then use the touch command change the modification date:

git ls-tree -r --name-only HEAD | while read filename; do
  unixtime=$(git log -1 --format="%at" -- "${filename}")
  touchtime=$(date -d @$unixtime +'%Y%m%d%H%M.%S')
  touch -t ${touchtime} "${filename}"
done

Also see my gist here.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tong
  • 870
  • 10
  • 19
  • 1
    Brilliant! Works like a charm. For us, this was critical so we could speed up our `Makefile` based builds. – Erik Osterman Jul 04 '19 at 20:56
  • 1
    This is the answer. Except one change, in case of filenames with spaces, you should add quotes around the $filename. – P. T. Oct 01 '19 at 21:14
  • @P.T. I see quotes have been added now – barlop Apr 29 '20 at 22:51
  • 1
    How is this the answer.. You say "You can retrieve the last modification date of all files in a git repository. (lat commit time)" <--- That's the date and time of the commit. That's not the date and time for the individual file. That's not a "last modification date" for the file. That(as far as I can tell), is the date/time for the commit that involved the adding of that file. So if a bunch of files were all added in that commit, your script would give them all the same date/time even if they were days or months or years or hours apart. – barlop Apr 30 '20 at 17:10
  • 1
    So if somebody did a first commit, on a directory of little ruby scripts that have been written at a variety of times, and they commit it , make some chanegs, and commit again, and push it to a repo, and then they git clone it from on another computer, then it's all same date, and then they run your script, they'll only get a bunch of files stamped with one date/time, and a bunch of files stamped with another date/time, and that's it. Just for the commit dates, that's not the file's last modification date/time at all – barlop Apr 30 '20 at 17:14
  • 2
    @barlop git history is not always the history of actual edits, it can be modified by amending and rebase -i. It's a _logical_ history the author chose to present, and in it a commit is a logically _atomic_, simultaneous change to a set of files. If you want to record "file1 changed before file2", then there must exist a point between those - these must be separate commits. – Beni Cherniavsky-Paskin Jun 17 '20 at 16:41
  • Great command! This demonstrate another lack of intuitiveness & simplicity of GIT. This should be the default behavior when you clone a depository. – Le Droid Nov 02 '20 at 14:06
12

Another option for resetting the mtime is git-restore-mtime.

sudo apt install git-restore-mtime # Debian/Ubuntu example
git clone <myurl>
cd <mydir>
git restore-mtime
mikebridge
  • 4,209
  • 2
  • 40
  • 50
8

This Linux one-liner will fix the problem with all the files (not folders - just files) - and it will also fix the problem with files with spaces in them too:

git ls-files -z | xargs -0 -n1 -I{} -- git log -1 --format="%ai {}" {} | perl -ne 'chomp;next if(/'"'"'/);($d,$f)=(/(^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(?: \+\d\d\d\d|)) (.*)/);print "d=$d f=$f\n"; `touch -d "$d" '"'"'$f'"'"'`;'
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 1
    Very, very nice. Cool!!!! As far as I see it, some files may not yet converted with this solution unfortunately. E.g.: 1. Files containing the character `'` (39 0027 ' APOSTROPHE), 2. Files in the root directory of the repository, 3. Files containing a `(` (could be also a `)` ). Maybe you could find the time to have a look for these specific cases, too? – user7468395 Jan 05 '20 at 11:42
  • 1
    That's retrieving from git repo, so would only give commit dates. Not actual file last modification date/time. – barlop Apr 30 '20 at 17:16
3

A shorter variant of user11882487's answer that I find easier to understand:

git ls-files | xargs -I{} git log -1 --date=format:%Y%m%d%H%M.%S --format='touch -t %ad "{}"' "{}" | $SHELL
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Neal Fultz
  • 9,282
  • 1
  • 39
  • 60
3

Adding to the list of one-liners ...

for f in $(git ls-files) ; do touch -d $(git log -1 --format='%aI' "$f") "$f" ; done
rickhg12hs
  • 10,638
  • 6
  • 24
  • 42
1

This applies to solutions in multiple previous answers:

Use the %at format, and then touch -d \@$epochdelta, to avoid date-time conversion issues.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
druud62
  • 11
  • 1
1

Running log -1 once per file irks me so I wrote this to do them all in one pass:

( # don't alter any modified-file stamps:
  git diff --name-status --no-find-copies --no-renames | awk '$1="D"' FS=$'\t' OFS=$'\t'
  git log --pretty=%cI --first-parent --name-status -m --no-find-copies --no-renames
) | awk ' NF==1 { date=$1 }
          NF<2 || seen[$2]++ { next }
          $1!="D" { print "touch -d",date,$2 }' FS=$'\t'

which does the linux history in like ten seconds (piping all the touch commands through a shell takes a minute).

This is a good way to ruin e.g. bisecting, and I'm in the camp of ~don't even start down the road of trying to overload filesystem timestamps, the people who insist on doing this are apparently going to have to learn the hard way~, but I can see that maybe there's workflows where this really won't hurt you.

Whatever. But, for sure, do not do this blindly.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • Can you make your answer more self-contained? E.g., does it follow/operate on the output of `git ls-files` (and instead of xargs)? What answer(s) does "Running log -1 once per file" refer to (four answers has "`log -1`")? (Use a link to the answer as user names may change at any time.) – Peter Mortensen Oct 23 '21 at 11:08
  • @PeterMortensen It prints touch commands as-is, it doesn't need anything added. Pipe them through a shell, which I think the mention of "piping all the touch commands through a shell" suggested explicitly. Any answer that runs `log -1` necessarily runs it once per file, my objection is to the method. – jthill Oct 23 '21 at 13:13
1

To do this in Python is simpler than some of these other options, as os.utime accepts the Unix timestamp output by the git log command. This example uses GitPython but it'd also work with subprocess.run to call git log.

import git
from os import utime
from pathlib import Path

repo_path = "my_repo"
repo = git.Repo(repo_path)

for n in repo.tree().list_traverse():
    filepath = Path(repo.working_dir) / n.path
    unixtime = repo.git.log(
        "-1", "--format='%at'", "--", n.path
    ).strip("'")
    if not unixtime.isnumeric():
        raise ValueError(
            f"git log gave non-numeric timestamp {unixtime} for {n.path}"
        )
    utime(filepath, times=(int(unixtime), int(unixtime)))

This matches the results of the git restore-mtime command in this answer and the script in the highest rated answer.

If you're doing this immediately after cloning, then you can reuse the to_path parameter passed to git.Repo.clone_from instead of accessing the working_dir attribute on the Repo object.

Louis Maddox
  • 5,226
  • 5
  • 36
  • 66