Какой эквивалент использования-commit-times для git?

Мне нужны временные метки файлов на моем локальном и на моем сервере для синхронизации. Это выполняется с помощью Subversion, установив use-commit-times = true в конфигурацию, чтобы последнее изменение каждого файла было зафиксировано.

Каждый раз, когда я клонирую свой репозиторий, я хочу, чтобы временные метки файлов отражались при их последнем изменении в удаленном репозитории, а не при клонировании репо.

Есть ли способ сделать это с помощью git?

Ответ 1

Я не уверен, что это будет подходящим для DVCS (как в "Распределенной" VCS)

Огромное обсуждение уже состоялось в 2007 (см. эту тему)

И некоторые из ответов Линуса не слишком увлекались этой идеей. Вот один пример:

Прости. Если вы не видите, как WRONG возвращает seta datestamp к чему-то, что сделает простой "make" miscompile исходное дерево, я не знаю, в чем заключается дефиниция "неправильного", о котором вы говорите.
Это НЕПРАВИЛЬНО.
Это STUPID.
И это совершенно НЕОБХОДИМО реализовать.


(Примечание: небольшое улучшение: после проверки, временные метки обновленных файлов больше не изменяются (Git 2.2.2+, январь 2015 г.): "git checkout - как я могу сохранять временные метки при переключении ветвей?" .)


Долгий ответ:

Я думаю, вам намного лучше просто использовать несколько репозиториев, если это что-то общее.

Мессинг с отметками времени не будет работать вообще. Это просто гарантирует вам, что "make" запутается очень плохо, и не перекомпилирует достаточно вместо перекомпиляции слишком много.

Git делает возможным очень легко, "по-другому", сделать "проверять другую ветку" очень много.

Вы можете создать тривиальный script, который выполняет любое из следующих действий (от тривиального до более экзотического):

  • просто создайте новое репо:

      git clone старый новый
    cd new
    git checkout origin/<branch>
    Код>

    и вот ты где. Старые метки времени хороши в вашем старом репо, и вы можете работать (и компилировать) в новом, не затрагивая старый.

    Используйте флаги "-n -l -s" для "git clone", чтобы сделать это мгновенно. Для большого количества файлов (например, больших репозиториев, таких как ядро), это не будет так быстро, как просто переключение ветвей, но havign вторая копия рабочего дерева может быть довольно мощной.

  • сделать то же самое с помощью только tar-шара, если вы хотите

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

    который действительно довольно быстр, если вы просто хотите моментальный снимок.

  • привыкнуть к git show "и просто посмотреть на отдельные файлы.
    Это действительно действительно полезно время от времени. Вы просто делаете

      git show otherbranch: filename
    Код>

    в одном окне xterm и посмотрите один и тот же файл в текущей ветке в другом окне. В частности, это должно быть тривиально делать с редакторами сценариев (например, GNU emacs), где в принципе можно иметь целый "режим ожидания" для других ветвей в редакторе, используя это. Насколько мне известно, режим emacs git уже предлагает что-то вроде этого (я не пользователь emacs)

  • и в крайнем примере этого "виртуального каталога", по крайней мере, кто-то работал над плагином git для FUSE, то есть вы могли буквально просто иметь виртуальные каталоги, показывающие all ваш ветки.

и я уверен, что любой из вышеперечисленных вариантов является лучшим вариантом, чем игра с файловыми метками времени.

Линус

Ответ 2

Если вы действительно хотите использовать время фиксации для временных меток при проверке, попробуйте использовать этот script и поместите его (как исполняемый файл) в файл $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

Обратите внимание, однако, что этот script приведет к довольно большой задержке для проверки больших репозиториев (где большой означает большое количество файлов, а не большие размеры файлов).

Ответ 3

ОБНОВЛЕНИЕ: Мое решение теперь упаковано в Debian/Ubuntu/Mint, Fedora, Gentoo и, возможно, другие дистрибутивы:

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


ИМХО, не хранить временные метки (и другие метаданные, такие как разрешения и владение) - это большое ограничение git.

Объяснение Линусом временных отметок, являющихся вредными только потому, что это "сбивает с толку, make ", является хромым:

  • make clean достаточно, чтобы исправить любые проблемы.

  • Относится только к проектам, использующим make, в основном C/C++. Это полностью спорный для сценариев, как Python, Perl или документации в целом.

  • Существует только вред, если вы применяете метки времени. Не было бы никакого вреда в хранении их в репо. Их применение может быть простой опцией --with-timestamps для git checkout и друзей (clone, pull т.д.) По усмотрению пользователя.

И базар, и Mercurial хранят метаданные. Пользователи могут применять их или нет при оформлении заказа. Но в git, поскольку оригинальные метки времени даже не доступны в репо, такой опции нет.

Таким образом, для очень небольшого выигрыша (без необходимости перекомпиляции всего), специфичного для подмножества проектов, git как обычный DVCS был поврежден, некоторая информация о файлах потеряна, и, как сказал Линус, это НЕОБХОДИМО для сделай это сейчас. Грустный

Тем не менее, я могу предложить 2 подхода?

1 - http://repo.or.cz/w/metastore.git, автор David Härdeman. Пытается сделать то, что git должен был сделать в первую очередь: сохраняет метаданные (не только временные метки) в репо при фиксации (через ловушку перед фиксацией), и повторно применяет их при извлечении (также через ловушки).

2 - Моя скромная версия скрипта, которую я использовал раньше для создания архивных релизов. Как упоминалось в других ответах, подход немного отличается: для каждого файла применяется метка времени последнего коммита, в котором файл был изменен.

Ниже приведена действительно простая версия сценария. Для реального использования я настоятельно рекомендую одну из более надежных версий выше:

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

Ответ 4

Я взял ответ Giel и вместо использования post-commit hook script обработал его в моем пользовательском развертывании script.

Обновление. Я также удалил один | 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 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

Ответ 5

мы были вынуждены изобрести еще одно решение, потому что нам требовались особые времена модификации, а не время фиксации, и решение также должно было быть переносимым (т.е. получение python, работающего в windows git, на самом деле не простая задача) и быстро. Он напоминает решение David Hardeman, которое я решил не использовать из-за отсутствия документации (из хранилища я не смог понять, что именно делает его код).

Это решение хранит mtimes в файле .mtimes в репозитории git, обновляет их соответственно на commits (jsut выборочно mtimes поэтапных файлов) и применяет их при проверке. Он работает даже с версиями cygwin/mingw git (но вам может потребоваться скопировать некоторые файлы из стандартного cygwin в папку git)

Решение состоит из 3 файлов:

  • mtimestore - core script предоставляет 3 варианта -a (сохранить все - для инициализации в уже существующем репо (работает с git -перевернутыми файлами)), -s (для сохранения поэтапных изменений) и -r для восстановления их. Это фактически происходит в двух версиях - bash одном (портативном, приятном, легко читаемом/модифицированном) и версии c (беспорядочный, но быстрый, поскольку mingw bash ужасно медленный, что делает невозможным использование bash решение по крупным проектам).
  • pre-commit hook
  • крюк после проверки.

перед фиксацией:

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

#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 - обновлена ​​версия С++:

  • Теперь версия С++ поддерживает алфавитный порядок → меньше конфликтов слияния.
  • Выбрали уродливые вызовы system().
  • Удалено $ git update-index --refresh $из кэша после проверки. Вызывает некоторые проблемы с возвратом под черепаху git, и, похоже, это не так важно.
  • Наш пакет Windows можно скачать по адресу http://ktweb.cz/blog/download/git-mtimestore-1.4.rar

//редактируем см. github для актуальной версии

Ответ 6

Следующий script включает предложения -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

Предполагая, что вы назвали выше script /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

Ответ 7

Вот оптимизированная версия вышеупомянутых решений оболочки с небольшими исправлениями:

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

Ответ 8

Это решение должно выполняться довольно быстро. Он устанавливает время от времени умножения и mtimes до времени автора. Он не использует модули, поэтому должен быть достаточно переносимым.

#!/usr/bin/perl

# git-utimes: update file times to last commit on them
# Tom Christiansen <[email protected]>

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;

Ответ 9

Я работаю над проектом, в котором хранится клоун моего репозитория для использования с развертываниями на основе rsync. Я использую ветки для таргетинга в разных средах, а git checkout приводит к изменению изменений файла.

Узнав, что git не предоставляет способ проверки файлов и сохраняет метки времени, я встретил команду git log --format=format:%ai --name-only . в другом вопросе SO: Список последних дат фиксации для большого количество файлов, быстро.

Теперь я использую следующие файлы script to 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);
}

Ответ 10

Я видел несколько запросов на версию для 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 для каждого файла.