Qual é o equivalente a use-commit-times para git?

97

Preciso que os carimbos de data / hora dos arquivos no meu local e no meu servidor estejam sincronizados. Isso é realizado com o Subversion definindo use-commit-times = true na configuração de forma que a última modificação de cada arquivo seja quando ele foi submetido.

Cada vez que clonar meu repositório, quero que os carimbos de data / hora dos arquivos reflitam quando foram alterados pela última vez no repositório remoto, não quando clonei o repo.

Existe alguma maneira de fazer isso com o git?

Ben W
fonte
Como parte do meu processo de implantação, faço upload de ativos (imagens, arquivos javascript e arquivos css) para um CDN. Cada nome de arquivo é anexado com o último carimbo de data / hora modificado. É importante que eu não expire todos os meus ativos cada vez que implantar. (Outro efeito colateral de use-commit-times é que posso fazer esse processo no meu local e saber que meu servidor se referirá aos mesmos arquivos, mas isso não é tão importante.) Se, em vez de fazer um clone git, eu fizesse um git fetch seguido por um git reset --hard do meu repo remoto, que funcionaria para um único servidor, mas não para vários servidores, uma vez que os carimbos de data / hora em cada um seriam diff.
Ben W
@BenW: git annexpode ser útil para rastrear imagens
jfs
Você pode verificar o que mudou verificando o id. Você está tentando fazer os timestamps do sistema de arquivos significarem a mesma coisa que os timestamps do vcs. Eles não significam a mesma coisa.
Jthill

Respostas:

25

Não tenho certeza se isso seria apropriado para um DVCS (como em VCS "Distribuído")

A grande discussão já havia ocorrido em 2007 (veja este tópico)

E algumas das respostas de Linus não gostaram muito da ideia. Aqui está um exemplo:

Eu sinto Muito. Se você não vê como é ERRADO definir um datestamp de volta para algo que fará um simples "make" compilar incorretamente sua árvore de origem, não sei de que definição de "errado" você está falando.
Está errado.
É estupido.
E é totalmente INFEASÍVEL de implementar.


(Observação: pequena melhoria: após um check-out, os carimbos de data / hora dos arquivos atualizados não são mais modificados (Git 2.2.2+, janeiro de 2015): "git checkout - como posso manter carimbos de data / hora ao trocar de branch?" .)


A longa resposta foi:

Acho que é muito melhor você usar vários repositórios, se isso for algo comum.

Mexer com timestamps não vai funcionar em geral. Isso só vai garantir que o "make" fique confuso de uma maneira muito ruim, e não recompila o suficiente ao invés de recompilar demais .

O Git torna possível fazer o seu "check the other branch out" muito facilmente, de muitas maneiras diferentes.

Você pode criar algum script trivial que faça qualquer um dos seguintes (variando do trivial ao mais exótico):

  • basta criar um novo repo:
    git clone old new
    cd new
    git checkout origin/<branch>

e aí está você. Os carimbos de data / hora antigos funcionam em seu repositório antigo e você pode trabalhar (e compilar) no novo, sem afetar o antigo de forma alguma.

Use os sinalizadores "-n -l -s" para "git clone" para tornar isso basicamente instantâneo. Para muitos arquivos (por exemplo, grandes repositórios como o kernel), não será tão rápido quanto apenas trocar de branches, mas ter uma segunda cópia da árvore de trabalho pode ser bastante poderoso.

  • faça a mesma coisa com apenas um tarball, se você quiser
    git archive --format=tar --prefix=new-tree/ <branchname> |
            (cd .. ; tar xvf -)

o que é realmente muito rápido, se você quiser apenas um instantâneo.

  • acostume-se a " git show" e apenas olhe os arquivos individuais.
    Isso é realmente muito útil às vezes. Você acabou de fazer
    git show otherbranch:filename

em uma janela xterm, e olhe para o mesmo arquivo em seu branch atual em outra janela. Em particular, isso deve ser trivial para fazer com editores programáveis ​​(por exemplo, GNU emacs), onde deve ser possível ter basicamente um "modo dired" inteiro para outros ramos dentro do editor, usando isso. Pelo que eu sei, o modo git do emacs já oferece algo assim (não sou um usuário do emacs)

  • e no exemplo extremo dessa coisa de "diretório virtual", havia pelo menos alguém trabalhando em um plugin git para FUSE, ou seja, você poderia literalmente apenas ter diretórios virtuais mostrando todos os seus branches.

e tenho certeza que qualquer uma das alternativas acima são alternativas melhores do que jogar jogos com carimbos de data / hora de arquivo.

Linus

VonC
fonte
5
Acordado. Você não deve confundir um DVCS com um sistema de distribuição. gité um DVCS, para manipular o código-fonte que será integrado ao seu produto final. Se você quer um sistema de distribuição, você sabe onde encontrar rsync.
Randal Schwartz
14
Hm, vou ter que confiar em seu argumento de que é inviável. Se é errado ou estúpido, é outra questão. Eu faço a versão de meus arquivos usando um carimbo de data / hora e os carrego em um CDN, por isso é importante que os carimbos de data / hora reflitam quando o arquivo foi realmente modificado, não quando foi retirado do repo pela última vez.
Ben W
3
@Ben W: a "resposta de Linus" não está aqui para dizer que está errada em sua situação particular. Ele está lá apenas como um lembrete de que um DVCS não é adequado para esse tipo de recurso (preservação de carimbo de data / hora).
VonC
15
@VonC: Já que outros DVCS modernos como o Bazaar e o Mercurial tratam perfeitamente os timestamps, prefiro dizer que "o git não é adequado para esse tipo de recurso". Se "um" DVCS deveria ter esse recurso é discutível (e eu acho que eles têm).
MestreLion
10
Esta não é uma resposta à pergunta, mas uma discussão filosófica sobre os méritos de fazer isso em um sistema de controle de versão. Se a pessoa tivesse gostado disso, ela teria perguntado, "Qual é a razão pela qual o git não usa o tempo de commit para o tempo de modificação dos arquivos?"
thomasfuchs
85

Se, no entanto, você realmente deseja usar tempos de confirmação para carimbos de data / hora ao fazer check-out, tente usar este script e coloque-o (como executável) no arquivo $ 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

Observe, entretanto, que este script causará um grande atraso para fazer check-out de grandes repositórios (onde grande significa grande quantidade de arquivos, não tamanhos de arquivo grandes).

Giel
fonte
55
+1 para uma resposta real, em vez de apenas dizer "Não faça isso"
DanC
4
| head -n 1deve ser evitado, pois gera um novo processo, -n 1pois git rev-liste git logpode ser usado em seu lugar.
Eregon,
3
É melhor NÃO ler as linhas com `...`e for; veja Por que você não lê linhas com "para" . Eu iria para git ls-files -ze while IFS= read -r -d ''.
musiphil
2
É possível uma versão do Windows?
Ehryk
2
em vez do que git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1você pode fazer git show --pretty=format:%ai -s "$(get_file_rev "$1")", isso faz com que muito menos dados sejam gerados pelo showcomando e deve reduzir a sobrecarga.
Scott Chamberlain
79

ATUALIZAÇÃO : minha solução agora está incluída em Debian / Ubuntu / Mint, Fedora, Gentoo e possivelmente outras distros:

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

IMHO, não armazenar carimbos de data / hora (e outros metadados como permissões e propriedade) é uma grande limitação do git.

A lógica de Linus de que os carimbos de data / hora são prejudiciais apenas porque "confundem make" é falha :

  • make clean é o suficiente para resolver quaisquer problemas.

  • Aplica-se apenas a projetos que usam make, principalmente C / C ++. É completamente discutível para scripts como Python, Perl ou documentação em geral.

  • Só há dano se você aplicar os carimbos de data / hora. Não haveria mal nenhum em armazená- los no repositório. Aplicá-los pode ser uma --with-timestampsopção simples para git checkoute amigos ( clone, pulletc), a critério do usuário .

Tanto o Bazaar quanto o Mercurial armazenam metadados. Os usuários podem aplicá-los ou não no check-out. Mas no git, como os timestamps originais nem mesmo estão disponíveis no repo, essa opção não existe.

Então, para um ganho muito pequeno (não ter que recompilar tudo) que é específico para um subconjunto de projetos, gitcomo um DVCS geral foi danificado , algumas informações sobre os arquivos são perdidas e, como Linus disse, é INFEASÍVEL fazer Isso agora. Triste .

Dito isso, posso oferecer 2 abordagens?

1 - http://repo.or.cz/w/metastore.git , de David Härdeman. Tenta fazer o que git deveria ter sido feito em primeiro lugar : armazena metadados (não apenas timestamps) no repositório ao fazer commit (via gancho de pré-confirmação) e os reaplica ao puxar (também por meio de ganchos).

2 - Minha versão humilde de um script que usei antes para gerar tarballs de lançamento. Conforme mencionado em outras respostas, a abordagem é um pouco diferente : aplicar para cada arquivo o carimbo de data / hora do commit mais recente em que o arquivo foi modificado.

  • git-restore-mtime , com muitas opções, suporta qualquer layout de repositório e roda em Python 3.

Abaixo está uma versão realmente básica do script, como uma prova de conceito, no Python 2.7. Para uso real, recomendo fortemente a versão completa acima:

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

O desempenho é bastante impressionante, mesmo para projetos de monstro wine, gitou mesmo o kernel 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
MestreLion
fonte
2
Mas git faz armazenar timestamps, etc. Ele simplesmente não definir a data e hora por padrão. Basta olhar para a saída degit ls-files --debug
Ross Smith II
9
@RossSmithII: git ls-filesopera no diretório de trabalho e no índice, então não significa que realmente armazena essa informação no repo. Se armazenasse, recuperar (e aplicar) mtime seria trivial.
MestreLion
13
"A lógica de Linus de que os carimbos de data / hora são prejudiciais apenas porque" confunde o make "é coxo" - concordou 100%, um DCVS não deve saber ou se preocupar com o código que contém! Novamente, isso mostra as armadilhas de tentar reaproveitar ferramentas escritas para casos de uso específicos em casos de uso gerais. O Mercurial é e sempre será uma escolha superior porque foi projetado, não evoluiu.
Ian Kemp
6
@davec De nada, que bom que foi útil. A versão completa em github.com/MestreLion/git-tools já lida com Windows, Python 3, nomes de caminhos não ASCII, etc. O script acima é apenas uma prova de conceito funcional, evite-o para uso em produção.
MestreLion
3
Seus argumentos são válidos. Eu espero que alguém com alguma influência faça uma solicitação de melhoria para o git ter sua opção sugerida --with-timestamps.
weberjn
12

Peguei a resposta de Giel e, em vez de usar um script de gancho post-commit, trabalhei em meu script de implantação personalizada.

Atualização : também removi uma | head -nsugestão de @eregon, e adicionei suporte para arquivos com espaços:

# 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
Alex Dean
fonte
Obrigado Daniel, isso é útil saber
Alex Dean
o --abbrev-commité supérfluo no git showcomando devido a --pretty=format:%aiser usado (commit hash não faz parte da saída) e | head -n 1pode ser substituído com o uso de -ssinalizador paragit show
Elan Ruusamäe
1
@ DanielS.Sterling: %aié a data do autor, formato semelhante ao ISO 8601 , para uso estrito iso8601 %aI: git-scm.com/docs/git-show
Elan Ruusamäe
4

fomos forçados a inventar outra solução, porque precisávamos especificamente de tempos de modificação e não de tempos de commit, e a solução também tinha que ser portátil (ou seja, fazer o python funcionar nas instalações git do Windows realmente não é uma tarefa simples) e rápido. Assemelha-se à solução de David Hardeman, que decidi não usar por falta de documentação (do repositório não consegui ter ideia do que exatamente o código dele faz).

Esta solução armazena mtimes em um arquivo .mtimes no repositório git, atualiza-os de acordo com os commits (apenas seletivamente os mtimes dos arquivos testados) e os aplica no checkout. Funciona mesmo com versões cygwin / mingw do git (mas você pode precisar copiar alguns arquivos do cygwin padrão para a pasta do git)

A solução consiste em 3 arquivos:

  1. mtimestore - script principal que fornece 3 opções -a (salvar tudo - para inicialização em repositório já existente (funciona com arquivos git-versed)), -s (para salvar alterações em etapas) e -r para restaurá-los. Na verdade, vem em 2 versões - uma bash (portátil, boa, fácil de ler / modificar) e uma versão c (bagunçada, mas rápida, porque o mingw bash é terrivelmente lento, o que torna impossível usar a solução bash em grandes projetos).
  2. gancho de pré-confirmação
  3. gancho pós-checkout

pré-compromisso:

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

pós-checkout

#!/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;
}
  • note que os ganchos podem ser colocados no diretório de modelos para automatizar sua colocação

mais informações podem ser encontradas aqui https://github.com/kareltucek/git-mtime-extension algumas informações desatualizadas estão em http://www.ktweb.cz/blog/index.php?page=page&id=116

// editar - versão c ++ atualizada:

  • Agora a versão c ++ mantém a ordem alfabética -> menos conflitos de mesclagem.
  • Livre-se das chamadas feias de sistema ().
  • Excluído $ git update-index --refresh $ do gancho pós-checkout. Causa alguns problemas com a reversão sob o git tartaruga e não parece ser muito importante de qualquer maneira.
  • Nosso pacote do Windows pode ser baixado em http://ktweb.cz/blog/download/git-mtimestore-1.4.rar

// edite veja o github para a versão atualizada

Karel Tucek
fonte
1
Observe que, após uma verificação, os carimbos de data / hora dos arquivos atualizados não são mais modificados (Git 2.2.2+, janeiro de 2015): stackoverflow.com/a/28256177/6309
VonC
3

O script a seguir incorpora as sugestões -n 1e HEAD, funciona na maioria dos ambientes não Linux (como Cygwin) e pode ser executado em uma verificação após o fato:

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

Supondo que você nomeou o script acima /path/to/templates/hooks/post-checkoute / ou /path/to/templates/hooks/post-update, você pode executá-lo em um repositório existente via:

git clone git://path/to/repository.git
cd repository
/path/to/templates/hooks/post-checkout
Ross Smith II
fonte
Ele precisa de mais uma última linha: git update-index --refresh // As ferramentas da GUI podem se basear no índice e mostrar o status "sujo" para todos os arquivos após essa operação. Ou seja, isso acontece no TortoiseGit para Windows code.google.com/p/tortoisegit/issues/detail?id=861
'The
1
E obrigado pelo script. Gostaria que esse script fizesse parte do instalador padrão do Git. Não que eu precise dele pessoalmente, mas os membros da equipe sentem que o timestamp se atualiza como um banner vermelho de "pare" na adoção do VCS.
Arioch 'The
3

Esta solução deve ser executada muito rapidamente. Ele define as horas para as horas de committer e as horas para as horas do autor. Ele não usa módulos, portanto deve ser razoavelmente portátil.

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

Aqui está uma versão otimizada das soluções shell acima, com pequenas correções:

#!/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
vszakats
fonte
1

Aqui está um método com 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);
}

É semelhante à resposta aqui:

Qual é o equivalente a use-commit-times para git?

ele constrói uma lista de arquivos como aquela resposta, mas constrói em git ls-files vez de apenas olhar no diretório de trabalho. Isso resolve o problema de exclusão .gite também resolve o problema de arquivos não rastreados. Além disso, essa resposta falha se o último commit de um arquivo foi um commit de mesclagem, o que eu resolvi git log -m. Como a outra resposta, irá parar assim que todos os arquivos forem encontrados, então não precisa ler todos os commits. Por exemplo com:

https://github.com/git/git

nesta postagem, ele só tinha que ler 292 commits. Também ignora arquivos antigos do histórico conforme necessário e não afetará um arquivo que já foi alterado. Finalmente parece ser um pouco mais rápido do que a outra solução. Resultados com git/gitrepo:

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

Eu vi alguns pedidos para uma versão do Windows, então aqui está. Crie os dois arquivos a seguir:

C: \ Arquivos de programas \ 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: \ Arquivos de programas \ 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;
    }
}

Isso utiliza git whatchanged , de modo que executa todos os arquivos em uma passagem, em vez de chamar git para cada arquivo.

Brain2000
fonte
0

Estou trabalhando em um projeto em que mantenho um clone do meu repositório para uso com rsyncimplantações baseadas. Eu uso branches para atingir diferentes ambientes e fazer git checkoutcom que as modificações do arquivo mudem.

Tendo aprendido que o git não fornece uma maneira de fazer check-out de arquivos e preservar carimbos de data / hora, me deparei com o comando git log --format=format:%ai --name-only .em outra pergunta do SO: Liste as datas dos últimos commits para um grande número de arquivos, rapidamente .

Agora estou usando o seguinte script para touchos arquivos e diretórios do meu projeto, para que rsyncseja mais fácil diferenciar minha implantação :

<?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);
}
Andrew Mackrodt
fonte