我需要本地和服务器上文件的时间戳同步。这是通过Subversion实现的,方法是在配置中设置use-commit-times = true,以便每个文件的最后修改时间是在提交时。
每次克隆存储库时,我都希望文件的时间戳反映远程存储库中上次更改的时间,而不是克隆存储库的时间。
有什么办法可以用git吗?
我需要本地和服务器上文件的时间戳同步。这是通过Subversion实现的,方法是在配置中设置use-commit-times = true,以便每个文件的最后修改时间是在提交时。
每次克隆存储库时,我都希望文件的时间戳反映远程存储库中上次更改的时间,而不是克隆存储库的时间。
有什么办法可以用git吗?
Answers:
我不确定这是否适用于DVCS(如“分布式” VCS中所述)
巨大的讨论已经在2007年进行(请参阅此主题)
Linus的一些答案并不十分喜欢这个想法。这是一个示例:
对不起。如果你看不出这是错误设定日期戳回东西,这将使一个简单的“化妆” miscompile源代码树,我不知道“错了”你说的是什么defintiion。
这是不对的。
这很傻。
而且这是完全不可行的。
(注意:小改进:签出后,不再修改最新文件的时间戳(Git 2.2.2 +,2015年1月):“ git checkout-切换分支时如何维护时间戳?”。)
长答案是:
我认为,如果这很常见,那么只使用多个存储库会更好。
一般情况下,打乱时间戳是行不通的。这只是要确保您“ make”以一种非常糟糕的方式感到困惑,并且不会进行足够的重新编译而不是重新编译太多。
Git确实可以通过许多不同方式非常轻松地完成“检查另一个分支”任务。
您可以创建一些琐碎的脚本来执行以下任一操作(从琐碎的代码到更奇特的代码):
- 只需创建一个新的仓库:
git clone old new cd new git checkout origin/<branch>
你在那里。旧的时间戳在您的旧存储库中很好,并且您可以在新的仓库中工作(和编译),而完全不会影响旧的时间戳。
使用标志“ -n -l -s”到“ git clone”基本上可以使此瞬间。对于很多文件(例如像内核这样的大仓库),它的速度不会像切换分支那样快,但是导航工作树的第二个副本可能会非常强大。
- 如果您想只用tar球做同样的事情
git archive --format=tar --prefix=new-tree/ <branchname> | (cd .. ; tar xvf -)
如果您只想要快照,那确实非常快。
- 习惯了“
git show”,只需查看单个文件即可。
实际上,有时这确实很有用。你只是做
git show otherbranch:filename
在一个xterm窗口中,在另一个窗口中查看当前分支中的相同文件。特别是,这对于使用脚本编写的编辑器(即GNU emacs)来说应该是微不足道的,在这种情况下,使用该脚本编辑器中的其他分支应该可以基本上具有完整的“ dirted模式”。就我所知,emacs git模式已经提供了这样的功能(我不是emacs用户)
- 在那个“虚拟目录”的极端例子中,至少有人在为FUSE开发git插件,即,您可以从字面上看就是显示所有分支的虚拟目录。
而且我敢肯定,上述任何一种方法都比带文件时间戳的游戏更好。
莱纳斯
git是DVCS,用于处理最终产品中内置的源代码。如果您要使用分配系统,则知道在哪里可以找到rsync。
但是,如果您真的想在结帐时使用提交时间作为时间戳,请尝试使用此脚本并将其(作为可执行文件)放置在文件$ 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
但是请注意,此脚本将导致检出大型存储库的较大延迟(其中大型表示大量文件,而不是大型文件)。
| head -n 1应避免使用,因为它会生成一个新的进程,-n 1为git rev-list和git log可用于代替。
git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1您可以做的是git show --pretty=format:%ai -s "$(get_file_rev "$1")",它使show命令生成的数据少得多,并应减少开销。
更新:我的解决方案现已打包到Debian / Ubuntu / Mint,Fedora,Gentoo和其他发行版中:
https://github.com/MestreLion/git-tools#install
sudo apt install git-restore-mtime # Debian/Ubuntu/Mint
yum install git-tools # Fedora/ RHEL / CentOS
emerge dev-vcs/git-tools # Gentoo
恕我直言,不存储时间戳(以及其他诸如权限和所有权之类的元数据)是的一大限制git。
莱纳斯的时间戳之所以有害是因为“混淆make”,这是la脚的:
make clean 足以解决任何问题。
仅适用于使用make(主要是C / C ++)的项目。对于诸如Python,Perl或一般文档之类的脚本,这完全没有意义。
如果应用时间戳,则只会造成伤害。将它们存储在回购中不会有任何危害。--with-timestamps对于用户git checkout和朋友(clone,pull等等),根据用户的判断,应用它们可能是一个简单的选择。
集市和Mercurial都存储元数据。结帐时,用户可以应用或不应用它们。但是在git中,由于原始时间戳记在存储库中甚至不可用,因此没有这样的选项。
因此,对于特定于项目子集的很小的收益(不必重新编译所有内容),git由于一般的DVCS受到损害,因此有关文件的某些信息会丢失,并且正如Linus所说,这样做是不可行的现在。伤心。
也就是说,我可以提供2种方法吗?
1- http: //repo.or.cz/w/metastore.git,作者: DavidHärdeman。尝试首先要做的是:在提交时(通过预提交钩子)将元数据(不仅时间戳)存储git 在存储库中,在拉动时(也通过钩子)重新应用元数据。
2-我以前用于生成发行版tarball的脚本的简陋版本。正如在其他的答案中提到,这种方法是有一点不同:申请每个文件的时间戳中的最近一次提交该文件已被修改。
作为概念证明,以下是该脚本的一个非常简陋的版本,用于Python 2.7。对于实际使用,我强烈建议您使用上述完整版本:
#!/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
即使对于怪物项目wine,git甚至是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
git 会存储时间戳等。默认情况下,它只是不设置时间戳。只要看看git ls-files --debug
git ls-files在工作目录和索引上运行,因此这并不意味着它实际上将该信息存储在存储库中。如果确实存储了,那么检索(和应用)mtime将会是微不足道的。
我接受了Giel的回答,而不是使用提交后的钩子脚本,而是将其用于我的自定义部署脚本中。
更新:我也删除了| head -n以下@eregon的建议,并增加了对其中包含空格的文件的支持:
# 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
--abbrev-commit是多余的(commit hash不属于输出的一部分),并可以使用以下标志替换它:git show--pretty=format:%ai| head -n 1-sgit show
我们被迫发明另一种解决方案,因为我们需要专门的修改时间而不是提交时间,并且该解决方案还必须具有可移植性(即,让python在Windows的git安装中正常工作并不是一件容易的事)并且速度很快。它类似于David Hardeman的解决方案,由于缺少文档,我决定不使用它(从存储库中我无法知道他的代码到底做什么)。
此解决方案将mtimes存储在git存储库中的文件.mtimes中,在提交时进行相应更新(选择性地选择暂存文件的mtimes),并将其应用于结帐。它甚至适用于cygwin / mingw版本的git(但您可能需要将一些文件从标准cygwin复制到git的文件夹中)
该解决方案包含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;
}
更多信息可以在这里找到 https://github.com/kareltucek/git-mtime-extension 一些过时的信息在 http://www.ktweb.cz/blog/index.php?page=page&id=116
// edit-c ++版本已更新:
//编辑请参阅github以获取最新版本
以下脚本结合了-n 1和HEAD建议,可在大多数非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-update,则可以通过以下方式在现有存储库上运行它:
git clone git://path/to/repository.git
cd repository
/path/to/templates/hooks/post-checkout
该解决方案应该很快运行。它设置提交者时间为atimes,作者时间为mtimes。它不使用任何模块,因此应合理移植。
#!/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;
这是上述外壳解决方案的优化版本,并进行了较小的修复:
#!/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
这是使用PHP的方法:
<?php
$r = popen('git ls-files', 'r');
$n_file = 0;
while (true) {
$s_gets = fgets($r);
if (feof($r)) {
break;
}
$s_trim = rtrim($s_gets);
$m_file[$s_trim] = false;
$n_file++;
}
$r = popen('git log -m -z --name-only --relative --format=%ct .', 'r');
while ($n_file > 0) {
$s_get = fgets($r);
$s_trim = rtrim($s_get);
$a_name = explode("\x0", $s_trim);
$s_unix = array_pop($a_name);
foreach ($a_name as $s_name) {
if (! array_key_exists($s_name, $m_file)) {
continue;
}
if ($m_file[$s_name]) {
continue;
}
touch($s_name, $n_unix);
$m_file[$s_name] = true;
$n_file--;
}
$n_unix = (int)($s_unix);
}
它类似于此处的答案:
它建立了一个像这样的答案的文件列表,但是它是从构建git ls-files
而不是仅仅在工作目录中查找的。这解决了排除问题,.git也解决了未跟踪文件的问题。另外,如果文件的最后一次提交是合并提交,则该答案将失败,我使用解决了这个问题git log -m。像其他答案一样,一旦找到所有文件,它将停止,因此它不必读取所有提交。例如:
截至本文发布时,它仅需读取292次提交。此外,它会根据需要忽略历史记录中的旧文件,并且不会触摸已被触摸的文件。最后,它似乎比其他解决方案要快一些。git/git回购结果:
PS C:\git> Measure-Command { git-touch.php }
TotalSeconds : 3.4215134
我看到了对Windows版本的一些要求,所以就在这里。创建以下两个文件:
C:\ Program Files \ Git \ mingw64 \ share \ git-core \ templates \ hooks \ post-checkout
#!C:/Program\ Files/Git/usr/bin/sh.exe
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "./$0.ps1"
C:\ Program Files \ Git \ mingw64 \ share \ git-core \ templates \ hooks \ post-checkout.ps1
[string[]]$changes = &git whatchanged --pretty=%at
$mtime = [DateTime]::Now;
[string]$change = $null;
foreach($change in $changes)
{
if($change.Length -eq 0) { continue; }
if($change[0] -eq ":")
{
$parts = $change.Split("`t");
$file = $parts[$parts.Length - 1];
if([System.IO.File]::Exists($file))
{
[System.IO.File]::SetLastWriteTimeUtc($file, $mtime);
}
}
else
{
#get timestamp
$mtime = [DateTimeOffset]::FromUnixTimeSeconds([Int64]::Parse($change)).DateTime;
}
}
这利用了git whatchanged,因此它一次通过所有文件,而不是为每个文件调用git。
我正在做一个项目,其中保留我的存储库的副本以用于rsync基于基础的部署。我使用分支来定位不同的环境,git checkout导致文件修改发生更改。
了解到git无法提供检出文件和保留时间戳的方法后,我git log --format=format:%ai --name-only .在另一个SO问题中遇到了该命令:快速列出大量文件的最后提交日期。
我现在在touch项目文件和目录中使用以下脚本,以便rsync更轻松地进行部署:
<?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);
}