85

Is there any way to know or get the original create/modified timestamps?

Trilarion
  • 9,318
  • 9
  • 55
  • 91
Scud
  • 36,193
  • 6
  • 23
  • 18
  • 1
    this is a cleaner page, but both question and most voted answer are basically duplicated: http://stackoverflow.com/questions/1964470/whats-the-equivalent-of-use-commit-times-for-git – cregox Jul 25 '13 at 20:51
  • 1
    https://git.wiki.kernel.org/index.php/Git_FAQ#Why_isn.27t_Git_preserving_modification_time_on_files.3F – dopeddude Dec 29 '17 at 06:29
  • Does this answer your question? [What's the equivalent of use-commit-times for git?](https://stackoverflow.com/questions/1964470/whats-the-equivalent-of-use-commit-times-for-git) – Trilarion Dec 15 '20 at 10:51

15 Answers15

58

YES, metastore or git-cache-meta can store such (meta-)information! Git by itself, without third party tools, can't. Metastore or git-cache-meta can store any file metadata for a file.

That is by design, as metastore or git-cache-meta are intended for that very purpose, as well as supporting backup utilities and synchronization tools.

(Sorry just a little fun spin on Jakub's answer)

zelusp
  • 2,773
  • 2
  • 26
  • 52
B T
  • 46,771
  • 31
  • 164
  • 191
  • 9
    You even mimicked his all-caps! If you apply the bold, too, I'm sure you'll get even more upvotes. ;-) – Michael Scheper Apr 02 '14 at 23:07
  • 1
    So I'm a bit miffed mainly because both of these tools (after some delving into them) drop the ball in spectacular fashion on macOS. They are entirely nonportable out of Linux. git-cache-meta relies on GNU `find`'s `-printf` extension, and I'm almost certain that metastore (being a C project) is even more work to make portable. Quite unfortunate. I will post back here if I find out that this situation changes. – Steven Lu May 06 '18 at 05:24
47

I believe that the only timestamps recorded in the Git database are the author and commit timestamps. I don't see an option for Git to modify the file's timestamp to match the most recent commit, and it makes sense that this wouldn't be the default behavior (because if it were, Makefiles wouldn't work correctly).

You could write a script to set the modification date of your files to the the time of the most recent commit. It might look something like this:

IFS="
"
for FILE in $(git ls-files)
do
    TIME=$(git log --pretty=format:%cd -n 1 --date=iso -- "$FILE")

    # Choose 1 version.
    # This is for BSD date (macOS, FreeBSD, etc)
    #TIME=$(date -j -f '%Y-%m-%d %H:%M:%S %z' "$TIME" +%Y%m%d%H%M.%S)

    # And this is the equivalent for GNU coreutils date (Linux)
    TIME=$(date --date="$TIME" +%Y%m%d%H%M.%S)

    touch -m -t "$TIME" "$FILE"
done
kristopolous
  • 1,558
  • 2
  • 13
  • 23
Dietrich Epp
  • 182,361
  • 34
  • 307
  • 387
  • 10
    There are several issues with this snippet: 1 - It fails if there are spaces in filenames; 2 - May fail for projects that are more than a few thousand files; 3 - performance is absolutely miserable any medium-sized project with a few thousand commits (even with few files) – MestreLion Nov 08 '12 at 07:30
  • 11
    +1 maybe it doesn't work for every possible case, but it is a good simple answer. – qwerty9967 Mar 19 '13 at 17:17
  • 5
    Isn't the OP's question how to preserve the original file modified timestamps, not strap on the commit timestamp to the files? – B T Jul 11 '13 at 00:38
  • 1
    Note: this answer describes why makefiles wouldn't work when file timestamps are preserved: http://stackoverflow.com/questions/2458042/restore-files-modification-time-in-git – B T Jul 11 '13 at 00:41
  • 16
    Designing a VCS around Make is shortsighted. I think this is a fault of Git. So really it *doesn't* make sense that it isn't default behavior. Make files should run on the files contents, not timestamps. Hashing the file and seeing if the hash matches what you built is much more robust. – B T Jul 11 '13 at 00:58
  • 2
    @BT: Lots of people use Make, especially if you count indirect users, and lots of other programs use timestamps. Do you think my post contains factual errors, or are you just using these comments as a platform for your personal gripes with the build systems that people use? – Dietrich Epp Jul 11 '13 at 02:58
  • 4
    I agree with BT and parts of your comment Dietrich. What BT meant about the OP is that your answer doesn't really allow to keep the file's original time. Instead, it replaces them with the original checkout time. Not the same thing... *So*, I think he clearly said your post contains factual errors. And I can see where the decision to not storing timestamps came from, as you point. I also think BT is ranting a bit back to that reasoning there. To which I agree with BT again - not good reasons to not being able to do it at all. Every other VCS can do it. – cregox Jul 19 '13 at 21:36
  • 1
    @Cawas: The question is phrased, "Is X possible?" and my answer is "X is unsupported, but you can do Y instead if that meets your needs." You're right that it's not the same thing, as I said in my answer. – Dietrich Epp Jul 19 '13 at 21:57
  • @Cawas: "Every other VCS can do it." Well, SVN, CVS, and Mercurial don't do it as far as I know, since they don't store the file modification dates in the repository. I could be wrong... do you have documentation to support this? (Of course you can hack in ways to add any meta-information you like into any revision-control system, but that doesn't change the fact that the data wasn't already recorded.) – Dietrich Epp Jul 19 '13 at 22:01
  • Yeah, I didn't say your post does contain factual errors, I just said he said it! (if you really going pedantic way ;-) ). But I do think it is misleading the way you wrote it. Anyway, for [SVN, set the config](http://stackoverflow.com/questions/2171939/how-can-i-keep-the-original-file-timestamp-on-subversion), for Mercurial I don't know if there's any native form (just read many places there is), but there's at least a [simple way to do it with extensions](http://mercurial.selenic.com/wiki/TimestampExtension). I couldn't care less for CVS. :P – cregox Jul 19 '13 at 22:06
  • 1
    @Cawas: That doesn't work in SVN: read more closely, it's doing the same thing I explain here. And if you're going to use a plugin in Mercurial which stores metadata in an additional file, you might as well write a pre-commit and post-checkout hook for Git that does the same thing. – Dietrich Epp Jul 19 '13 at 22:19
  • I wish it were that simple in git. Still trying to create that hook... As for the SVN, I did I read it closely but not the comments. Even still, there seems to be a command there that can be used. – cregox Jul 19 '13 at 22:40
  • 1
    @BT hashing the file content is too slow – Johan Boulé Mar 12 '16 at 19:38
  • 2
    What is `date -j` supposed to do? I don't have this option in my `date` (Arch Linux). – Martin Tournoij May 05 '17 at 12:14
  • 2
    I had to change the second TIME row to `TIME=$(date --date="$TIME" +%Y%m%d%H%M.%S)` to get it to work in bash on Ubuntu. – Anders Marzi Tornblad Sep 11 '17 at 14:12
  • @BT I'm a bit late to the party but "Make files should run on the files contents, not timestamps" is just nonsense, unless I miss something. How is a build system to correlate the contents of a binary (hashed or not) with the contents of a source file? Do you want to store meta information in the form of files with hashes? Do you want to open and read each file in order to compute a hash so that you know whether it has changed? How long should that take, in your opinion? It sounds patently absurd to me. Are there build systems which do that? – Peter - Reinstate Monica Mar 15 '18 at 08:03
  • @Peter: There are build systems that do that, such as waf. – Dietrich Epp Mar 15 '18 at 13:05
  • 1
    @DietrichEpp Thanks for the pointer, interesting. Not sure how they achieve acceptable run times (computing a hash sum should not be much faster than simply compiling a C file right away -- I/O is the bottleneck); hierarchical hashes over souce trees which make many checks unnecessary? The docs seem not eloquent on the basics. – Peter - Reinstate Monica Mar 15 '18 at 13:45
  • @PeterA.Schneider: Compiling a C file is much slower than hashing it. I just tested by compiling Git with optimizations disabled, on a powerful Xeon workstation, and less than 14% of the compilation time was spent in I/O. I believe that waf only checks the file hash if the metadata has changed but the file size is correct, so it doesn't rehash everything every time you call it. But waf is still noticeably slower than make for some projects. – Dietrich Epp Mar 15 '18 at 15:25
  • Also note that file hashes are the right way to go for distributed build caching, which can *drastically* speed up large builds. I think this is the way Bazel's remote cache works. – Dietrich Epp Mar 15 '18 at 15:32
  • `date` by me does not know anything about the `-j` and `-f` flags. – peterh Jul 07 '20 at 15:06
  • This version works in CentOS 7 with spanish accents and spaces:` IFS=" " for FILE in $(git ls-files -z | tr '\0' '\n') do TIME=$(git log --pretty=format:%cd -n 1 --date=iso -- "$FILE") touch -c -m -d "$TIME" "$FILE" done` – Ivan Baldo Aug 29 '20 at 00:21
39

NO, Git simply does not store such (meta-)information, unless you use third party tools like metastore or git-cache-meta. The only timestamp that get stored is the time patch/change was created (author time), and the time commit was created (committer time).

That is by design, as Git is version control system, not a backup utility or synchronization tool.

zelusp
  • 2,773
  • 2
  • 26
  • 52
Jakub Narębski
  • 268,805
  • 58
  • 209
  • 228
  • is there metastore build for win32 ? or should one re-create scripts/hooks for Windows? Franklt, i don't need other attrs, only mtime – Arioch 'The Oct 23 '12 at 13:18
  • 8
    I think your answer is actually "YES! Metastore or git-cache-meta can do this for you!" I guess its the difference between defeatist and optimist attiuteds. – B T Jul 11 '13 at 00:30
  • 3
    Plus, as I heard, *bazaar* and *mercurial* are also "version control systems" which do store meta information. [There's nothing so wrong](http://stackoverflow.com/a/13284229/274502) with doing so. – cregox Jul 19 '13 at 21:30
  • Clarification: Git keeps two timestamps for each file: the author date (which I think is what Jakub means by 'time patch') and the committer date. The former is the time the file was first committed, and the latter is the time the file was most recently committed. – Michael Scheper Apr 02 '14 at 23:42
  • 4
    *"That is by design, as Git is version control system, not a backup utility or synchronization tool."* That's a *non sequitur*: disregarding metadata (*especially* dates, which are intimately related to versions) has nothing to do with being a VCS, or a backup tool. Also, every VCS has a big inherent overlap of functionality with backup tools: they both strive to preserve important past states. Finally, even Git does not ignore all metadata (e.g. it tracks the exec. bit), despite being a VCS. It still *is* by design, though, just for a different reason: Git's exclusive focus on content. – Sz. Sep 19 '19 at 13:12
13

UPDATE: TL;DR: git itself does not save original times, but some solutions circumvent this by various methods. git-restore-mtime is one of them:

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

Ubuntu/Debian: sudo apt install git-restore-mtime
Fedora/RHEL/CentOS: sudo yum install git-tools

See my other answer for more details

Full disclaimer: I'm the author of git-tools


This python script may help: for each file applies the timestamp of the most recent commit where the file was modified:

Below is a really bare-bones version of the script. For actual usage I strongly suggest one of the more robust versions 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

All versions parse the full log generated by a single git whatchanged command, which is hundreds of times faster than lopping for each file. Under 4 seconds for git (24,000 commits, 2,500 files) and less than 1 minute for linux kernel (40,000 files, 300,000 commits)

MestreLion
  • 10,095
  • 2
  • 52
  • 49
  • 2
    Your other [similar answer](http://stackoverflow.com/a/13284229/274502) is much better than this! – cregox Jul 25 '13 at 20:49
  • `$ python ./git-restore-mtime Traceback (most recent call last): File "./git-restore-mtime", line 122, in 'git rev-parse --show-toplevel --git-dir')).split('\n')[:2] TypeError: Type str doesn't support the buffer API` Would you mind maybe telling us what version of Python is needed? I'm using 3.3.3 – Rolf Dec 30 '13 at 12:41
  • @Cawas: Thanks... I guess. But the code in both answers are identical, so I'm not sure why you think the other one is better. The only difference is some ranting about git. Which was somewhat pertinent to that question, but not to this one. – MestreLion Jan 08 '14 at 15:02
  • 1
    @Rolf: I used Python 2.7, and it seems the code needs some tweaking in Python 3, thanks for pointing out. The reason is: `str` in Python 2 is the equivalent of `bytestring` in Python 3 , while `str` in Python 3 is `unicode` in Python 2. Can you please report this issue at https://github.com/MestreLion/git-tools/issues ? – MestreLion Jan 08 '14 at 15:08
  • It's not just the "rant". There you also explain what the code does in much more detail and, thus, clarity. – cregox Jan 09 '14 at 17:24
  • @Cawas: *I* didn't explain the code, *you* did with your edit. It was an amazing improvement, and I appreciate! :) – MestreLion Jan 15 '14 at 00:19
7

This did he trick for me on ubuntu (which lacks OSX's the "-j" flag on date(1))

for FILE in $(git ls-files)
do
    TIME=$(git log --pretty=format:%cd -n 1 --date=iso $FILE)
    TIME2=`echo $TIME | sed 's/-//g;s/ //;s/://;s/:/\./;s/ .*//'`
    touch -m -t $TIME2 $FILE
done 
eludom
  • 407
  • 5
  • 5
4

I have been skirmishing with git and file timestamps for some time already.

Tested some of your ideas and made my own awfully huge and predecessor/ram heavy scripts, untill i found (on some git wiki) a script in perl that does almost what i wanted. https://git.wiki.kernel.org/index.php/ExampleScripts

And what i wanted is to be able to preserve last modification of files based on commit dates.

So after some readjustment the script is able to change creation and modification date of 200k files in around 2-3min.

#!/usr/bin/perl
my %attributions;
my $remaining = 0;

open IN, "git ls-tree -r --full-name HEAD |" or die;
while (<IN>) {
    if (/^\S+\s+blob \S+\s+(\S+)$/) {
        $attributions{$1} = -1;
    }
}
close IN;

$remaining = (keys %attributions) + 1;
print "Number of files: $remaining\n";
open IN, "git log -r --root --raw --no-abbrev --date=raw --pretty=format:%h~%cd~ |" or die;
while (<IN>) {
    if (/^([^:~]+)~([^~]+)~$/) {
        ($commit, $date) = ($1, $2);
    } elsif (/^:\S+\s+1\S+\s+\S+\s+\S+\s+\S\s+(.*)$/) {
        if ($attributions{$1} == -1) {
            $attributions{$1} = "$date";
            $remaining--;

            utime $date, $date, $1;
            if ($remaining % 1000 == 0) {               
                print "$remaining\n";
            }
            if ($remaining <= 0) {
                break;
            }
        }
    }
}
close IN;

Assuming that your repositories wont have 10k+ files this should take seconds to execute, so you can hook it to the checkout, pull or other git basic hooks.

Lukasz Kruszyna
  • 746
  • 7
  • 6
2

Here is my solution that takes into consideration paths that contain spaces:

#! /bin/bash

IFS=$'\n'
list_of_files=($(git ls-files | sort))
unset IFS

for file in "${list_of_files[@]}"; do
  file_name=$(echo $file)

  ## When you collect the timestamps:
  TIME=$(date -r "$file_name" -Ins)

  ## When you want to recover back the timestamps:
  touch -m -d $TIME "$file_name"
done

Note that this does not take the time which git log reports, it's the time reported by the system. If you want the time since the files were commited use git log solution instead of date -r

Lilian A. Moraru
  • 1,010
  • 12
  • 19
2

Native git doesn't have the functionality, but it can be achieved by hook scripts or third party tools.

I've tried metastore. It's very fast, but I don't like the need to install and that metadata are not stored in plain text format. git-cache-meta is a simple tool I've tried, but it's extremely slow for large repos (for a repo with tens of thousands of files, it takes minutes to update the metadata file) and could have cross-platform compatibility issues. setgitperms and other approaches also have their shortcomings that I don't like.

At last I made a hook script for this job: git-store-meta. It has very light dependency (*nix shell, sort, and perl, which is required by git, and optionally chown, chgrp and touch) so that nothing additional have to be installed for a platform that can run git, desirable performance (for a repo with tens of thousands of files, it takes < 10 seconds to update the metadata file; although longer to create), saves data in plain text format, and which metadata to be "saved" or "loaded" is customizable.

It has worked fine for me. Try this if you are not satisfied with metastore, git-cache-meta, and other approaches.

Danny Lin
  • 1,700
  • 1
  • 14
  • 31
  • I tried this out, and this does actually work, thanks! Only slight issue is that the `--install` hooks do not seem to start working until I manually run `git-store-meta.pl --store` the first time. – Milind R Feb 22 '21 at 14:51
  • @MilindR This is by design, as git-store-meta can never know how to store for an auto update if no previous store has been run. – Danny Lin Feb 23 '21 at 15:29
  • Oh you mean the fields to be stored! How about having an `--init` flag, after which the hooks can work normally? Just a suggestion... – Milind R Feb 25 '21 at 10:51
2

I hope you appreciate the simplicity:

# getcheckin - Retrieve the last committed checkin date and time for
#              each of the files in the git project.  After a "pull"
#              of the project, you can update the timestamp on the
#              pulled files to match that date/time.  There are many
#              that believe that this is not a good idea, but
#              I found it useful to get the right source file dates
#
#              NOTE: This script produces commands suitable for
#                    piping into BASH or other shell
# License: Creative Commons Attribution 3.0 United States
# (CC by 3.0 US)

##########
# walk back to the project parent or the relative pathnames don't make
# sense
##########
while [ ! -d ./.git ]
do
    cd ..
done
echo "cd $(pwd)"
##########
# Note that the date format is ISO so that touch will work
##########
git ls-tree -r --full-tree HEAD |\
    sed -e "s/.*\t//" | while read filename; do
    echo "touch --date=\"$(git log -1 --date=iso --format="%ad" -- "$filename")\" -m $filename" 
done
sailnfool
  • 21
  • 3
  • (FYI, there's an unintended double negation in the header comment, which you may want to fix in your original, too: "There are many that *don't* believe that this is *not* a good idea.") – Sz. Sep 19 '19 at 12:58
1

For Windows environment I wrote a small (quick and dirty) EXE in Delphi 10.1 Berlin that collects all file dates in the source tree into the file .gitfilattr and can apply them on the checked our source tree again.

Of course I share the code in GitHub:

https://github.com/michaschumann/gitfiledates/blob/master/gitFileDates.dpr

I use it in my build system based on GitLab runners.

MichaSchumann
  • 1,111
  • 11
  • 29
1

There's some ambiguity in my (and others') interpretation of the OP about whether this means the commit time or something else, but assuming it means commit time, then this simple one-liner will work in linux (based on answer snippet from Dietrich Epp):

git ls-files | xargs -I{} bash -c 'touch "{}" --date=@$(git log -n1 --pretty=format:%ct -- "{}")'

But there's more sophisticated answers (including git hooks) linked from a comment to the original question by cregox.

mza
  • 81
  • 2
  • 4
1

In CentOS 7 you have /usr/share/doc/rsync-*/support/git-set-file-times and in Debian (and derivatives) the same script in /usr/share/doc/rsync/scripts/git-set-file-times.gz, the original is from Eric Wong and is here https://yhbt.net/git-set-file-times.

It works faster than other examples mentioned here and you may find it more handy to have it already on your Linux distribution.

Ivan Baldo
  • 310
  • 2
  • 6
  • 1
    For my Debian testing installation, there is a similar script located at `/usr/share/rsync/scripts/git-set-file-times`. It is Python3 and not Perl, and it originates from the `rsync` package. – hochl Apr 21 '21 at 12:00
0

With GNU tools.

s=$(git ls-files  | wc -l); 
git ls-files -z  |
 xargs -0 -I{} -n1 bash -c \
"git log --date=format:%Y%m%d%H%M.%S '--pretty=format:touch -m -t %cd \"{}\"%n' -n1 -- {}"|
 pv -l -s$s |
 parallel -n1 -j8

 967  0:00:05 [ 171 /s] [=====================================>  ] 16% 

.

$ git --version ; xargs --version | sed 1q ; ls --version | sed 1q;
  parallel --version  | sed 1q;  pv --version | sed 1q; sh --version | sed 1q 
git version 2.13.0
xargs (GNU findutils) 4.6.0
ls (GNU coreutils) 8.25
GNU parallel 20150522
pv 1.6.0 - Copyright 2015 Andrew Wood <andrew.wood@ivarch.com>
GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
0

Here's mine.

A little quicker than some others, as I'm not calling 'get log' for each file found; instead, calling 'git log' once and transforming that output into touch commands.

There'll be cases where there are too many listed files in 1 commit to fit into a single shell command buffer; run "getconf ARG_MAX" to see the max length of a command in bytes - on my debian install, it's 2MB, which is plenty.

# set file last modification time to last commit of file
git log --reverse --date=iso --name-only | \
  grep -vE "^(commit |Merge:|Author:|    |^$)" | \
  grep -B 1 "^[^D][^a][^t][^e][^:][^ ]" | \
  grep -v "^\-\-" | \
  sed "s|^\(.*\)$|\"\1\"|;s|^\"Date: *\(.*\)\"$|~touch -c -m -d'\1'|" | \
  tr '~\n' '\n ' | \
  sh -

description by line:

  • earliest-first list of commits and filenames
  • filter out unneeded commit/merge/author lines
  • filter out lines starting with double-dash
  • sed (stream-edit) command a) prepend/append double-quote to lines, and b) replace "Date: ." with ~touch -c -m -d. ( the touch command options are -c = don't create if it doesn't exist, -m = change file modification time, and -d = use the provided date/time )
  • translate tilda(~) and newline(\n) chars to newline and space, respectively
  • pipe the resulting stream of text lines into a shell.

In terms of speed, it 5 seconds 1700 commits for 6500 files in 700 directories.

jmullee
  • 332
  • 2
  • 6
0

https://github.com/DotCi/jenkinsci-dotci-example/commit/5a45034d13b85ab4746650995db55b5281451cec#diff-a83424d0d40754ac7e2029b13daa2db43651eb65aabf8c9a5a45005b56f259bdR19

for file in `find . -type f -not -path "./.git/*"`; do 
  touch -d "`git rev-list -n 1 HEAD \$file | xargs git show -s --format=%ai`" $file; 
done