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

时间:2009-12-26 21:53:53

标签: git

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

每次我克隆我的存储库时,我都希望文件的时间戳能够反映最后一次更改它们在远程存储库中的时间,而不是当我克隆存储库时。

有没有办法用git做到这一点?

11 个答案:

答案 0 :(得分:84)

但是,如果确实想要在签出时使用时间戳的提交时间,那么请尝试使用此脚本并将其(作为可执行文件)放在$ GIT_DIR / .git / hooks / post-文件中结帐:

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

但请注意,此脚本会导致检出大型存储库的延迟时间相当长(大型存储库意味着大量文件,而不是大文件)。

答案 1 :(得分:70)

更新:我的解决方案现已打包成Debian / Ubuntu / Mint,Fedora,Gentoo以及其他可能的发行版:

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


恕我直言,不存储时间戳(以及权限和所有权等其他元数据)是git限制。

Linus关于时间戳有害的原因只是因为它“混淆make”是 lame

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

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

  • 如果应用时间戳,则只会造成伤害。在回购中存储它们没有任何害处。对于--with-timestamps和朋友(git checkoutclone等),用户的可以自行决定是否应用它们。

Bazaar和Mercurial都存储元数据。用户可以在结账时申请或不申请。但是在git中,由于回购中的原始时间戳甚至不是可用,因此没有这样的选项。

因此,对于一个特定于项目子集的非常小的收益(不必重新编译所有内容),pull作为一般DVCS 瘫痪,来自关于文件丢失,正如Linus所说,现在这样做是不可能的。的 悲伤

那就是说,我可以提供两种方法吗?

1 - http://repo.or.cz/w/metastore.git,DavidHärdeman。尝试做git 首先应该做的事情在提交时在回购中存储元数据(不仅仅是时间戳)(通过预提交挂钩) ,并在拉动时重新应用它们(也可以通过钩子)。

2 - 我之前用于生成发布tarball的脚本的简陋版本。正如其他答案所述,该方法略有不同:为每个文件申请最新提交 时间戳,其中文件为修改。

以下是该脚本的真正裸机版本。对于实际使用,我强烈建议使用上面一个更强大的版本:

git

性能非常令人印象深刻,即使对于怪物项目#!/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甚至是linux内核:

git

答案 2 :(得分:22)

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

已经在2007 (see this thread)

进行了大量讨论

Linus的一些回答并不太热衷于这个想法。这是一个样本:

  

对不起如果你没有看到如何错误地将日期戳回到可以简化“make”错误编译你的源代码树的东西,我不知道你所说的“错误”的定义是什么。
  这是错的。
  这是愚蠢的。
  实施它是完全不可能的。


(注意:小改进:结账后,不再修改最新文件的时间戳(Git 2.2.2 +,2015年1月):"git checkout - how can I maintain timestamps when switching branches?"。)


答案很长:

  

我觉得你只需要使用多个存储库就好了,如果这是常见的事情。

     

与时间戳混淆一般不会起作用。它只是向你保证“make”会以一种非常糟糕的方式混淆,并且不会重新编译足够而不是重新编译太多

     

Git确实能够以很多不同的方式轻松地“检查其他分支”。

     

你可以创建一些琐碎的脚本来执行以下任何操作(从简单到更具异国情调):

     
      
  • 只需创建一个新的仓库:

    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)来说应该是微不足道的,在编辑器中,使用它可以基本上为编辑器中的其他分支创建一个完整的“dired模式”。据我所知,emacs git模式已经提供了这样的东西(我不是emacs用户)

  •   
  • 并且在那个“虚拟目录”的极端例子中,至少有人在为FUSE开发一个git插件,也就是说,你真的只有虚拟目录显示 all 你的分支。

  •   
     

我确信上述任何一种都比使用文件时间戳玩游戏更好。

     

莱纳斯

答案 3 :(得分:13)

我接受了Giel的回答,而不是使用post-commit钩子脚本,将其用于我的自定义部署脚本。

更新:我还根据@ eregon的建议删除了一个| head -n,并添加了对包含空格的文件的支持:

# 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

答案 4 :(得分:5)

我们被迫发明另一个解决方案,因为我们需要特定的修改时间而不是提交时间,并且解决方案也必须是可移植的(即在Windows的git安装中使python工作真的不是一项简单的任务)而且速度快。它类似于David Hardeman的解决方案,我决定不使用它,因为缺少文档(从存储库我无法知道他的代码到底是什么)。

此解决方案将mtimes存储在git存储库中的文件中。在提交时相应地更新它们(有选择地jsut是阶段文件的mtimes)并在结帐时应用它们。它甚至适用于git的cygwin / mingw版本(但你可能需要将一些文件从标准cygwin复制到git的文件夹中)

该解决方案由3个文件组成:

  1. mtimestore - 提供3个选项的核心脚本-a(全部保存 - 用于现有仓库中的初始化(与git-versed文件一起工作)), - s(保存分阶段更改)和-r恢复它们。这实际上有两个版本 - 一个bash one(便携式,漂亮,易于阅读/修改)和c版本(凌乱但快速,因为mingw bash非常慢,这使得无法在大型项目上使用bash解决方案)。
  2. pre-commit hook
  3. post-checkout hook
  4. 预提交:

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

    //编辑 - 更新了c ++版本:

    • 现在c ++版本保持字母顺序 - &gt;减少合并冲突。
    • 摆脱丑陋的系统()调用。
    • 从post-checkout hook中删除了$ git update-index --refresh $。在乌龟git下恢复会产生一些问题,但无论如何似乎都不重要。
    • 我们的Windows软件包可以在http://ktweb.cz/blog/download/git-mtimestore-1.4.rar
    • 下载

    //编辑请参阅github获取最新版本

答案 5 :(得分:4)

以下脚本包含-n 1HEAD建议,适用于大多数非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

答案 6 :(得分:3)

以上是上述shell解决方案的优化版本,其中包含一些小修复:

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

答案 7 :(得分:3)

此解决方案应该很快运行。它设置了提交者时间和工作时间的m次。它不使用任何模块,因此应该是合理的便携式。

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

答案 8 :(得分:1)

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

在得知git没有提供签出文件和保留时间戳的方法后,我在另一个SO问题中遇到了git log --format=format:%ai --name-only .命令:List last commit dates for a large number of files, quickly

我现在使用以下脚本来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);
}

答案 9 :(得分:1)

我看到了一些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。

答案 10 :(得分:1)

这是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);
}

这与这里的答案类似:

What's the equivalent of use-commit-times for git?

它建立了一个像这样的答案的文件列表,但是它是从git ls-files建立的 而不是仅仅在工作目录中查找。解决了 排除.git,还解决了未跟踪文件的问题。还有 如果文件的最后一次提交是合并提交,答案将失败,这是我解决的 与git log -m。像其他答案一样,一旦找到所有文件,它将停止, 因此,它不必读取所有提交。例如:

https://github.com/git/git

到发布为止,它只需要读取292次提交。也忽略旧文件 根据需要从历史记录中删除,并且不会触摸已经存在的文件 感动。最后,它似乎比其他解决方案要快一点。结果 与git/git回购:

PS C:\git> Measure-Command { git-touch.php }
TotalSeconds      : 3.4215134