使用原始创建/修改时间戳检查旧文件

106

有没有办法知道或获取原始创建/修改时间戳?


3
这是一个更清洁的页面,但问题和最多赞的答案基本上是重复的:https://dev59.com/lHI-5IYBdhLWcg3wN1hD - cregox
1
https://git.wiki.kernel.org/index.php/Git_FAQ#Why_isn.27t_Git_preserving_modification_time_on_files.3F - dopeddude
2
这个回答解决了你的问题吗?Git中类似于use-commit-times的等效选项是什么? - Trilarion
对于GIT 克隆更改文件修改时间的答案可能更加实用和简洁(包括命令行一行代码等)。 - Peter Mortensen
17个回答

64

是的,元数据存储库或 git-cache-meta 可以存储这样的(元)信息! Git 本身无法存储它。 Metastoregit-cache-meta 可以为文件存储任何文件元数据。

这是元数据存储库或 git-cache-meta 的设计初衷,旨在支持备份工具和同步工具,同时也支持此目的。


12
你甚至模仿了他的全大写字母!如果你还加粗字体,我相信你会得到更多的赞。;-) - Michael Scheper
2
所以我有点恼火,主要是因为这两个工具(经过一些深入研究)在 macOS 上表现得非常糟糕。它们完全无法在 Linux 之外移植。git-cache-meta 依赖于 GNU find-printf 扩展,而我几乎可以确定 metastore(作为一个 C 项目)更难以移植。非常不幸。如果我发现情况有所改变,我会在这里回复的。 - Steven Lu
1
现在有一个名为 git-meta 的工具,它是 git-cache-meta 的更新版本并已转化为一个仓库;现在可以将其安装为 git 钩子,这样每次提交时都会自动存储元数据! - dani 'SO learn value newbies'

59
我相信Git数据库中唯一记录的时间戳是作者和提交时间戳。我没有看到Git有修改文件时间戳以匹配最近提交的选项,这是合理的默认行为(因为如果这样做,Makefile将无法正确工作)。
您可以编写一个脚本,将文件的修改日期设置为最近提交的时间。它可能看起来像这样:
# No arguments? Recursively list all git-controlled files in $PWD and start over
if [ $# = 0 ]; then
  git ls-files -z |xargs -0 sh "$0"
  exit $?
fi

for file in "$@"; do
  time="$(git log --pretty=format:%cd -n 1 \
                  --date=format:%Y%m%d%H%M.%S --date-order -- "$file")"
  if [ -z "$time" ]; then
    echo "ERROR: skipping '$file' -- no git log found" >&2
    continue
  fi
  touch -m -t "$time" "$file"
done

这个命令接受特定的文件作为参数,否则会更新当前目录或其子目录中的每个git控制的文件。这样做可以允许文件名中包含空格甚至换行符,因为git ls-files -z输出以null结尾的文件列表,而xargs -0将null结尾的列表解析为参数。

如果你有很多文件,这可能需要一些时间。


10
这段代码存在几个问题:1 - 如果文件名中有空格会失败;2 - 在文件数超过几千个的项目中可能会失败;3 - 在有数千次提交的中等规模项目中(即使只有少量文件),性能极差。 - MestreLion
13
+1也许并不适用于每种情况,但它是一个简单好用的答案。 - qwerty9967
6
OP的问题不是如何将提交时间戳附加到文件上,而是如何保留原始文件的修改时间戳。 - B T
21
围绕 Make 设计 VCS 是目光短浅的。我认为这是 Git 的缺陷。因此,它不成为默认行为真的没有意义。Make 文件应该基于文件内容运行,而不是时间戳。对文件进行哈希处理并检查哈希是否与构建的内容匹配更加健壮。 - B T
4
我同意BT和Dietrich的评论。BT所指的是关于OP的,即你的回答并没有真正允许保留文件的原始时间。相反,它用原始的检出时间替换了它们。这不是同一件事情......因此,我认为他明确表示你的帖子包含事实错误。我也能理解你提到的不存储时间戳的决定来自何处。我也认为BT在那个推理中有点抱怨。在这方面我再次同意BT - 这不是完全不能做到的好理由。其他每个版本控制系统都可以做到。 - cregox
显示剩余20条评论

43

,Git 根本不会存储这种(元)信息,除非您使用第三方工具,如metastore或git-cache-meta。唯一存储的时间戳是创建补丁/更改的时间(作者时间)和创建提交的时间(提交者时间)。

这是出于设计考虑,因为Git是版本控制系统,而不是备份实用程序或同步工具。


有没有适用于Win32的元数据存储构建?还是应该为Windows重新创建脚本/钩子?实际上,我不需要其他属性,只需要mtime。 - Arioch 'The
10
我认为你的答案实际上是“是的!Metastore或git-cache-meta可以为您完成这个任务!”我想这是消极和乐观态度之间的区别。 - B T
3
据我所知,bazaarmercurial 也是“版本控制系统”,它们可以存储元信息。这样做并没有什么不妥之处。 - cregox
1
澄清:Git为每个文件保留两个时间戳:作者日期(我想这就是Jakub所说的“patch时间”)和提交者日期。前者是文件第一次提交的时间,后者是文件最近一次提交的时间。 - Michael Scheper
7
这是出于设计考虑,因为Git是版本控制系统,而不是备份工具或同步工具。这是个无关紧要的论断:忽略元数据(特别是版本相关的日期)与是否是VCS或备份工具没有任何关系。此外,每个版本控制系统都有一个与备份工具功能大重叠的内在功能:它们都努力保留重要的历史状态。最后,即使是Git也不会忽略所有元数据(例如,它跟踪exec位),尽管它是一个版本控制系统。然而,Git的设计仍然是基于对内容的专注。 - Sz.

19
更新: TL;DR: Git本身不保存原始时间,但一些解决方案通过各种方法绕过此问题。其中之一是git-restore-mtime

UbuntuDebiansudo apt install git-restore-mtime FedoraRed Hat Enterprise Linux(RHEL)和CentOSsudo yum install git-tools

更多详情请参见我的另一个回答

免责声明:我是git-tools的作者


这个Python脚本可能有所帮助:对于每个文件,它应用了文件被修改的最近提交的时间戳:

  • 核心功能,带--help和调试消息。可以在工作树中的任何位置运行
  • 全面的版本,具有许多选项。支持任何存储库布局。

下面是一个非常简单版本的脚本。对于实际使用,我强烈建议使用上述更健壮的版本之一:

#!/usr/bin/env python
# Bare-bones version. The current directory 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

所有版本都解析单个git whatchanged命令生成的完整日志,这比每个文件循环快数百倍。对于Git(24,000个提交,2,500个文件),时间不到4秒,对于Linux内核(40,000个文件和300,000个提交)则不到一分钟。


3
你的另一个类似的回答比这个好多了! - cregox
请执行以下命令:$ 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请问需要使用哪个版本的Python?我正在使用3.3.3。 - Rolf
@Cawas:谢谢……我猜。但是两个答案中的代码是相同的,所以我不确定为什么你认为另一个更好。唯一的区别是一些关于git的抱怨。这在那个问题上有点相关,但对这个问题来说并不相关。 - MestreLion
1
@Rolf:我使用的是Python 2.7,看起来代码需要在Python 3中进行一些调整,感谢您指出。原因是:Python 2中的“str”相当于Python 3中的“bytestring”,而Python 3中的“str”则是Python 2中的“unicode”。您能否在https://github.com/MestreLion/git-tools/issues上报告此问题? - MestreLion
不仅仅是“抱怨”。在那里,您还可以更详细地解释代码的功能,从而增加了清晰度。 - cregox
@Cawas:没有解释这段代码,是通过编辑来进行的。这是一个非常棒的改进,我很感激! :) - MestreLion

11

这对我在Ubuntu上起了作用(因为Ubuntu缺少date(1)中的OS X“-j”标志):

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

在Ubuntu上,无需重新格式化时间戳;而是使用touch命令的-d标志,而不是-t。所以:for FILE in $(git ls-files) ; do TIME=$(git log --pretty=format:%cd -n 1 --date=iso $FILE) ; touch -m -d "$TIME" $FILE ; done - Richard Walker

5

本地的Git没有这个功能,但可以通过钩子脚本或第三方工具实现。

我尝试过metastore。它非常快,但我不喜欢需要安装和元数据不以纯文本格式存储的需求。git-cache-meta是一个简单的工具,但对于大型存储库(对于拥有数万个文件的存储库,更新元数据文件需要几分钟)来说速度极慢,并且可能存在跨平台兼容性问题。 setgitperms和其他方法也有其缺点,我不喜欢。

最后,我为这个工作编写了一个钩子脚本:git-store-meta。它有非常轻量的依赖关系(* nix shell,sort和Git所需的perl,以及可选的chownchgrptouch),因此对于可以运行Git的平台不需要安装任何其他内容,具有理想的性能(对于包含成千上万个文件的存储库,更新元数据文件只需< 10秒;虽然创建需要更长时间),以纯文本格式保存数据,并且要“保存”或“加载”的元数据是可定制的。

它对我很好用。如果您对metastore、git-cache-meta和其他方法不满意,请尝试使用此方法。


我尝试了一下,这确实有效,谢谢!唯一的小问题是--install钩子似乎直到我手动运行git-store-meta.pl --store第一次才开始工作。 - Milind R
@MilindR 这是设计上的考虑,因为如果没有运行过先前的存储,git-store-meta 永远不知道如何为自动更新存储。 - Danny Lin
哦,你是指要存储的字段!在此之后添加一个 --init 标志如何?这样钩子就可以正常工作了。只是一个建议... - Milind R

4

我已经在与git和文件时间戳搏斗了一段时间。

测试了一些你的想法,并制作了自己庞大且占用内存较多的脚本,直到我在某个git wiki上找到了一个perl脚本,它几乎做到了我想要的。

https://git.wiki.kernel.org/index.php/ExampleScripts

我想要的是能够基于提交日期保留文件的最后修改时间。

所以经过一些调整,该脚本能够在约2-3分钟内更改200k个文件的创建和修改日期。

#!/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;

假设您的代码库不会有10k+个文件,那么执行此操作只需要几秒钟,因此您可以将其挂钩到checkout、pull或其他git基本挂钩。

3
与将修改时间设置为提交时间的其他解决方案不同,git-store-meta 将元数据(如修改时间)保存到一个名为 .git_store_meta 的文件中,并将其添加到存储库中。它可以将 Git 钩子安装到当前的存储库中,以自动保存和应用元数据。

2

2

我希望你能欣赏这种简洁:

# 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

1
有很多人不相信这不是一个好主意。 - Sz.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接