git的use-commit-times相当于什么?

我需要本地和我的服务器上的文件时间戳同步。 这是在Subversion中通过在configuration中设置use-commit-times = true来完成的,以便每个文件的最后修改时间是在提交时。

每次克隆我的存储库时,我都希望文件的时间戳反映在远程存储库中上次更改的时间,而不是在克隆回购时。

有没有办法与Git做到这一点?

我不确定这适合于DVCS(如“分布式”VCS)

这场巨大的讨论已经发生在2007年(见这个主题)

Linus的一些答案对这个想法不太感兴趣。 这是一个样本:

对不起。 如果你没有看到错误的seta datestamp返回到一个简单的“make”错误编译你的源代码树,我不知道你说的是什么“错误”的定义。
这是不对的。
这很傻。
而且这是完全不可行的实施。


(注意:小改进: 结帐后,不再修改最新文件的时间戳 (Git 2.2.2+,2015年1月): “git checkout – 如何在转换分支时保持时间戳?” )


漫长的回答是:

如果这是常见的,我认为你只需使用多个存储库就好多了。

与时间戳混淆是不会工作的一般。 这只是要保证你“make”以一种非常糟糕的方式被混淆,并且不会重新编译而不是重新编译太多

Git确实可以用很多不同的方式很容易地做到“检查其他分支”的事情。

你可以创build一些简单的脚本来执行下面的任何操作(从简单到更奇特):

  • 只需创build一个新的回购:

     git clone old new cd new git checkout origin/<branch> 

    你在那里 你旧的回购中旧的时间戳是好的,你可以在新的回购中工作(和编译),而不会影响到旧的回报。

    使用标志“-n -l -s”来“git clone”基本上做到这一点。 对于很多文件(比如内核这样的大型仓库)来说,它不会像切换分支一样快,但是提供工作树的第二个副本可能非常强大。

  • 如果你愿意的话,只用tar-ball来做同样的事情

     git archive --format=tar --prefix=new-tree/ <branchname> | (cd .. ; tar xvf -) 

    如果你只是想要一个快照,这真的很快。

  • 习惯于“ git show ”,只看单个文件。
    这实际上有时是非常有用的。 你只是做

     git show otherbranch:filename 

    在一个xterm窗口中,在另一个窗口中查看当前分支中的相同文件。 尤其是,对于可编写脚本的编辑器(例如GNU emacs)来说,这应该是微不足道的,因为编辑器中的其他分支应该可以基本上具有完整的“直接模式”。 据我所知,emacs git模式已经提供了这样的东西(我不是一个emacs用户)

  • 在这个“虚拟目录”的极端例子中,至less有人为FUSE开发了一个git插件,也就是说,你可以从字面上看到只有虚拟目录显示你所有的分支。

我敢肯定,以上任何一种都比用文件时间戳玩游戏更好。

莱纳斯

但是,如果您确实希望在签出时使用时间戳的提交时间,请尝试使用此脚本,并将其作为可执行文件放在文件$ GIT_DIR / .git / hooks / post-checkout中:

 #!/bin/sh -e OS=${OS:-`uname`} old_rev="$1" new_rev="$2" get_file_rev() { git rev-list -n 1 "$new_rev" "$1" } if [ "$OS" = 'Linux' ] then update_file_timestamp() { file_time=`git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1` touch -d "$file_time" "$1" } elif [ "$OS" = 'FreeBSD' ] then update_file_timestamp() { file_time=`date -r "$(git show --pretty=format:%at --abbrev-commit "$(get_file_rev "$1")" | head -n 1)" '+%Y%m%d%H%M.%S'` touch -h -t "$file_time" "$1" } else echo "timestamp changing not implemented" >&2 exit 1 fi IFS=`printf '\t\n\t'` git ls-files | while read -r file do update_file_timestamp "$file" done 

但是请注意,这个脚本将会导致很大的延迟来检查大型的存储库(其中大的意味着大量的文件,而不是大的文件大小)。

恕我直言,不存储时间戳(和其他元数据,如权限和所有权)是git一个很大的局限性。

Linus的时间戳的理由是有害的,因为它“混淆”是跛脚的

  • make clean足以解决任何问题。

  • 仅适用于使用make项目,主要是C / C ++。 一般来说,对Python,Perl或文档等脚本完全没有意义。

  • 如果你使用时间戳,这只会有害处。 将它们存储在回购中不会有什么坏处。 应用他们可以是一个简单的--with-timestamps选项的git checkout和朋友( clonepull等),在用户的决定。

Bazaar和Mercurial都存储元数据。 用户可以在退房时使用或不使用。 但是在git中,由于原始时间戳在回购中没有,所以没有这个选项。

所以,对于一小部分项目来说,只是一个非常小的收益(不必重新编译所有的东西), git作为一个通用的DVCS被破坏了 ,一些关于文件的信息丢失了 ,正如Linus所说的,它是不可行的现在做。 悲伤

这就是说,我可以提供2种方法吗?

1 – http://repo.or.cz/w/metastore.git,DavidHärdeman 。 尝试做什么git 应该做的第一个地方存储元数据(不仅时间戳)在提交时(通过预先提交挂钩),并重新应用它们时拉(也通过挂钩)。

2 – 我以前用于生成释放tarball的脚本的简单版本。 正如其他答案中提到的,这种方法有点不同 :为每个文件应用最近提交的文件被修改的时间戳

  • 核心function ,使用–help,debugging消息。 可以在工作树内的任何地方运行
  • 全面的野兽 ,有很多select。 支持任何存储库布局。

下面是一个真正的脚本版本。 对于实际使用,我强烈build议使用上面更强大的版本之一:

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

性能是相当令人印象深刻的,即使是怪物项目winegit ,甚至是Linux内核:

 bash # 0.27 seconds # 5,750 log lines processed # 62 commits evaluated # 1,155 updated files git # 3.71 seconds # 96,702 log lines processed # 24,217 commits evaluated # 2,495 updated files wine # 13.53 seconds # 443,979 log lines processed # 91,703 commits evaluated # 6,005 updated files linux kernel # 59.11 seconds # 1,484,567 log lines processed # 313,164 commits evaluated # 40,902 updated files 

我采取了Giel的答案,而不是使用post-commit钩子脚本,而是将其应用到我的自定义部署脚本中。

更新 :我也删除了一个| head -n | head -n遵循@ eregon的build议,并在其中添加了对空格的文件的支持:

 # Adapted to use HEAD rather than the new commit ref get_file_rev() { git rev-list -n 1 HEAD "$1" } # Same as Giel's answer above update_file_timestamp() { file_time=`git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1` sudo touch -d "$file_time" "$1" } # Loop through and fix timestamps on all files in our CDN directory old_ifs=$IFS IFS=$'\n' # Support files with spaces in them for file in $(git ls-files | grep "$cdn_dir") do update_file_timestamp "${file}" done IFS=$old_ifs 

我们被迫发明了另一种解决scheme,因为我们需要特定的修改时间而不是时间,而且解决scheme也必须是可移植的(例如,在windows的git安装中获取python真的不是一件简单的工作)并且速度很快。 它类似于David Hardeman的解决scheme,我决定不使用,因为缺乏文档(从存储库我不能知道他的代码是什么)。

这个解决scheme在git仓库中将mtimes存储在一个.mtimes文件中,并在提交时相应地更新它们(有select地select分阶段文件的m倍)并在结帐时应用它们。 它甚至适用于cygwin / mingw版本的git(但是你可能需要将一些标准的cygwin文件复制到git的文件夹中)

该解决scheme由3个文件组成:

  1. mtimestore – 核心脚本提供3个选项-a(全部保存 – 用于初始化已经存在的回购(用git-versed文件工作)),-s(保存分阶段更改)和-r恢复它们。 这实际上有两个版本 – 一个bash(一个便携式,很好,易于阅读/修改)和c版本(杂乱的一个,但速度很快,因为mingw bash速度非常慢,这使得不可能在大项目中使用bash解决scheme)。
  2. 预先提交钩子
  3. 结账后挂钩

预提交:

 #!/bin/bash mtimestore -s git add .mtimes 

后结账

 #!/bin/bash mtimestore -r 

mtimestore – bash:

 #!/bin/bash function usage { echo "Usage: mtimestore (-a|-s|-r)" echo "Option Meaning" echo " -a save-all - saves state of all files in a git repository" echo " -s save - saves mtime of all staged files of git repository" echo " -r restore - touches all files saved in .mtimes file" exit 1 } function echodate { echo "$(stat -c %Y "$1")|$1" >> .mtimes } IFS=$'\n' while getopts ":sar" optname do case "$optname" in "s") echo "saving changes of staged files to file .mtimes" if [ -f .mtimes ] then mv .mtimes .mtimes_tmp pattern=".mtimes" for str in $(git diff --name-only --staged) do pattern="$pattern\|$str" done cat .mtimes_tmp | grep -vh "|\($pattern\)\b" >> .mtimes else echo "warning: file .mtimes does not exist - creating new" fi for str in $(git diff --name-only --staged) do echodate "$str" done rm .mtimes_tmp 2> /dev/null ;; "a") echo "saving mtimes of all files to file .mtimes" rm .mtimes 2> /dev/null for str in $(git ls-files) do echodate "$str" done ;; "r") echo "restorim dates from .mtimes" if [ -f .mtimes ] then cat .mtimes | while read line do timestamp=$(date -d "1970-01-01 ${line%|*} sec GMT" +%Y%m%d%H%M.%S) touch -t $timestamp "${line##*|}" done else echo "warning: .mtimes not found" fi ;; ":") usage ;; *) usage ;; esac 

mtimestore – c ++

 #include <time.h> #include <utime.h> #include <sys/stat.h> #include <iostream> #include <cstdlib> #include <fstream> #include <string> #include <cerrno> #include <cstring> #include <sys/types.h> #include <ctime> #include <map> void changedate(int time, const char* filename) { try { struct utimbuf new_times; struct stat foo; stat(filename, &foo); new_times.actime = foo.st_atime; new_times.modtime = time; utime(filename, &new_times); } catch(...) {} } bool parsenum(int& num, char*& ptr) { num = 0; if(!isdigit(*ptr)) return false; while(isdigit(*ptr)) { num = num*10 + (int)(*ptr) - 48; ptr++; } return true; } //splits line into numeral and text part - return numeral into time and set ptr to the position where filename starts bool parseline(const char* line, int& time, char*& ptr) { if(*line == '\n' || *line == '\r') return false; time = 0; ptr = (char*)line; if( parsenum(time, ptr)) { ptr++; return true; } else return false; } //replace \r and \n (otherwise is interpretted as part of filename) void trim(char* string) { char* ptr = string; while(*ptr != '\0') { if(*ptr == '\n' || *ptr == '\r') *ptr = '\0'; ptr++; } } void help() { std::cout << "version: 1.4" << std::endl; std::cout << "usage: mtimestore <switch>" << std::endl; std::cout << "options:" << std::endl; std::cout << " -a saves mtimes of all git-versed files into .mtimes file (meant to be done on intialization of mtime fixes)" << std::endl; std::cout << " -s saves mtimes of modified staged files into .mtimes file(meant to be put into pre-commit hook)" << std::endl; std::cout << " -r restores mtimes from .mtimes file (that is meant to be stored in repository server-side and to be called in post-checkout hook)" << std::endl; std::cout << " -h show this help" << std::endl; } void load_file(const char* file, std::map<std::string,int>& mapa) { std::string line; std::ifstream myfile (file, std::ifstream::in); if(myfile.is_open()) { while ( myfile.good() ) { getline (myfile,line); int time; char* ptr; if( parseline(line.c_str(), time, ptr)) { if(std::string(ptr) != std::string(".mtimes")) mapa[std::string(ptr)] = time; } } myfile.close(); } } void update(std::map<std::string, int>& mapa, bool all) { char path[2048]; FILE *fp; if(all) fp = popen("git ls-files", "r"); else fp = popen("git diff --name-only --staged", "r"); while(fgets(path, 2048, fp) != NULL) { trim(path); struct stat foo; int err = stat(path, &foo); if(std::string(path) != std::string(".mtimes")) mapa[std::string(path)]=foo.st_mtime; } } void write(const char * file, std::map<std::string, int>& mapa) { std::ofstream outputfile; outputfile.open(".mtimes", std::ios::out); for(std::map<std::string, int>::iterator itr = mapa.begin(); itr != mapa.end(); ++itr) { if(*(itr->first.c_str()) != '\0') { outputfile << itr->second << "|" << itr->first << std::endl; } } outputfile.close(); } int main(int argc, char *argv[]) { if(argc >= 2 && argv[1][0] == '-') { switch(argv[1][1]) { case 'r': { std::cout << "restoring modification dates" << std::endl; std::string line; std::ifstream myfile (".mtimes"); if (myfile.is_open()) { while ( myfile.good() ) { getline (myfile,line); int time, time2; char* ptr; parseline(line.c_str(), time, ptr); changedate(time, ptr); } myfile.close(); } } break; case 'a': case 's': { std::cout << "saving modification times" << std::endl; std::map<std::string, int> mapa; load_file(".mtimes", mapa); update(mapa, argv[1][1] == 'a'); write(".mtimes", mapa); } break; default: help(); return 0; } } else { help(); return 0; } return 0; } 
  • 请注意,可以将钩子放入模板目录以自动化其位置

更多信息可以在这里findhttps://github.com/kareltucek/git-mtime-extension一些过时的信息在http://www.ktweb.cz/blog/index.php?page=page&id=116

//编辑 – c ++版本更新:

  • 现在,c ++版本保持按字母sorting – >减less合并冲突。
  • 摆脱丑陋的系统()调用。
  • 删除$ git update-index – 从post-checkout挂钩清除$。 导致一些问题,在乌龟GIT下恢复,反正似乎并不重要。
  • 我们的windows包可以在blog/download/git-mtimestore-1.4.rar

//编辑见最新版本的github

以下脚本包含-n 1HEADbuild议,可在大多数非Linux环境(如Cygwin)中使用,并且可以在以下事实之后在结帐中运行:

 #!/bin/bash -e OS=${OS:-`uname`} get_file_rev() { git rev-list -n 1 HEAD "$1" } if [ "$OS" = 'FreeBSD' ] then update_file_timestamp() { file_time=`date -r "$(git show --pretty=format:%at --abbrev-commit "$(get_file_rev "$1")" | head -n 1)" '+%Y%m%d%H%M.%S'` touch -h -t "$file_time" "$1" } else update_file_timestamp() { file_time=`git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1` touch -d "$file_time" "$1" } fi OLD_IFS=$IFS IFS=$'\n' for file in `git ls-files` do update_file_timestamp "$file" done IFS=$OLD_IFS git update-index --refresh 

假设您将上面的脚本/path/to/templates/hooks/post-checkout命名/path/to/templates/hooks/post-checkout和/或/path/to/templates/hooks/post-update ,可以通过以下命令在现有的存储库上运行它:

 git clone git://path/to/repository.git cd repository /path/to/templates/hooks/post-checkout 

这个解决scheme应该运行得很快。 它设置了一次提交次数和提交次数。 它不使用模块,所以应该是合理的便携式。

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

以下是上述shell解决scheme的优化版本,并稍作修改:

 #!/bin/sh if [ "$(uname)" = 'Darwin' ] || [ "$(uname)" = 'FreeBSD' ]; then gittouch() { touch -ch -t "$(date -r "$(git log -1 --format=%ct "$1")" '+%Y%m%d%H%M.%S')" "$1" } else gittouch() { touch -ch -d "$(git log -1 --format=%ci "$1")" "$1" } fi git ls-files | while IFS= read -r file; do gittouch "$file" done 

我正在开发一个项目,其中保留了我的存储库的一个克隆,以便与基于rsync的部署一起使用。 我使用分支来定位不同的环境,而git checkout会导致文件修改的改变。

了解到git没有提供签出文件和保存时间戳的方法,我碰到了命令git log --format=format:%ai --name-only . 在另一个SO问题中:快速列出大量文件的上次提交date 。

我现在使用下面的脚本来touch我的项目文件和目录,以便我的rsync部署更容易区分:

 #!/usr/bin/env php <?php $lines = explode("\n", shell_exec('git log --format=format:%ai --name-only .')); $times = array(); $time = null; $cwd = isset($argv[1]) ? $argv[1] : getcwd(); $dirs = array(); foreach ($lines as $line) { if ($line === '') { $time = null; } else if ($time === null) { $time = strtotime($line); } else { $path = $cwd . DIRECTORY_SEPARATOR . $line; if (file_exists($path)) { $parent = dirname($path); $dirs[$parent] = max(isset($parent) ? $parent : 0, $time); touch($path, $time); } } } foreach ($dirs as $dir => $time) { touch($dir, $time); }