Quel est l'équivalent de use-commit-times pour git?

97

J'ai besoin que les horodatages des fichiers sur mon local et sur mon serveur soient synchronisés. Ceci est accompli avec Subversion en définissant use-commit-times = true dans la configuration afin que la dernière modification de chaque fichier ait lieu lors de sa validation.

Chaque fois que je cloné mon référentiel, je veux que les horodatages des fichiers reflètent la date de leur dernière modification dans le référentiel distant, et non le moment où j'ai cloné le référentiel.

Y a-t-il un moyen de faire cela avec git?

Ben W
la source
Dans le cadre de mon processus de déploiement, je télécharge des actifs (images, fichiers javascript et fichiers css) sur un CDN. Chaque nom de fichier est ajouté avec le dernier horodatage modifié. Il est important de ne pas expirer tous mes actifs à chaque fois que je déploie. (Un autre effet secondaire de use-commit-times est que je peux faire ce processus sur mon local et savoir que mon serveur fera référence aux mêmes fichiers, mais ce n'est pas aussi important.) Si au lieu de faire un clone git, j'ai fait un git fetch suivi d'un git reset --hard de mon référentiel distant, cela fonctionnerait pour un seul serveur, mais pas pour plusieurs serveurs puisque les horodatages sur chacun seraient différents.
Ben W
@BenW: git annexpeut être utile pour garder une trace des images
jfs
Vous pouvez vérifier ce qui a changé en vérifiant les identifiants. Vous essayez de faire en sorte que les horodatages du système de fichiers signifient la même chose que les horodatages vcs. Ils ne veulent pas dire la même chose.
jthill

Réponses:

25

Je ne suis pas sûr que ce serait approprié pour un DVCS (comme dans le VCS «distribué»)

L'énorme discussion avait déjà eu lieu en 2007 (voir ce fil)

Et certaines réponses de Linus n'étaient pas trop enthousiastes à l'idée. Voici un exemple:

Je suis désolé. Si vous ne voyez pas comment il est MAUVAIS de remettre un horodatage à quelque chose qui fera un simple "make" mal compiler votre arbre source, je ne sais pas de quelle définition de "faux" vous parlez.
C'est faux.
C'est stupide.
Et c'est totalement INFAISIBLE à mettre en œuvre.


(Note: petite amélioration: après une extraction, les horodatages des fichiers à jour ne sont plus modifiés (Git 2.2.2+, janvier 2015): "git checkout - comment puis-je maintenir les horodatages lors du changement de branche?" .)


La longue réponse était:

Je pense que vous êtes bien mieux d'utiliser simplement plusieurs référentiels à la place, si c'est quelque chose de commun.

Jouer avec les horodatages ne fonctionnera pas en général. Cela va juste vous garantir que "make" se confond d'une très mauvaise manière, et ne recompile pas assez au lieu de trop recompiler .

Git permet de faire votre chose "check the other branch out" très facilement, de différentes manières.

Vous pouvez créer un script trivial qui effectue l'une des opérations suivantes (allant du plus trivial au plus exotique):

  • créez simplement un nouveau dépôt:
    git clone old new
    cd new
    git checkout origin/<branch>

et vous y êtes. Les anciens horodatages sont corrects dans votre ancien référentiel, et vous pouvez travailler (et compiler) dans le nouveau, sans affecter l'ancien du tout.

Utilisez les indicateurs "-n -l -s" pour "git clone" pour rendre cela instantané. Pour beaucoup de fichiers (par exemple de gros dépôts comme le noyau), ce ne sera pas aussi rapide que de changer de branche, mais avoir une deuxième copie de l'arbre de travail peut être assez puissant.

  • faites la même chose avec juste une balle tar à la place, si vous voulez
    git archive --format=tar --prefix=new-tree/ <branchname> |
            (cd .. ; tar xvf -)

ce qui est vraiment assez rapide, si vous voulez juste un instantané.

  • Habituez-vous à " git show", et regardez simplement les fichiers individuels.
    C'est en fait très utile parfois. Tu fais juste
    git show otherbranch:filename

dans une fenêtre xterm, et regardez le même fichier dans votre branche actuelle dans une autre fenêtre. En particulier, cela devrait être trivial à faire avec les éditeurs scriptables (c'est-à-dire GNU emacs), où il devrait être possible d'avoir fondamentalement un "mode dired" complet pour d'autres branches dans l'éditeur, en utilisant ceci. Pour autant que je sache, le mode git emacs propose déjà quelque chose comme ça (je ne suis pas un utilisateur emacs)

  • et dans l'exemple extrême de ce truc de "répertoire virtuel", il y avait au moins quelqu'un qui travaillait sur un plugin git pour FUSE, c'est-à-dire que vous pouviez littéralement avoir des répertoires virtuels montrant toutes vos branches.

et je suis sûr que toutes les solutions ci-dessus sont de meilleures alternatives que de jouer à des jeux avec des horodatages de fichiers.

Linus

VonC
la source
5
D'accord. Vous ne devriez pas confondre un DVCS avec un système de distribution. gitest un DVCS, pour manipuler le code source qui sera intégré à votre produit final. Si vous voulez un système de distribution, vous savez où le trouver rsync.
Randal Schwartz
14
Hm, je vais devoir faire confiance à son argument selon lequel c'est irréalisable. Que ce soit faux ou stupide, c'est une autre affaire. Je version mes fichiers à l'aide d'un horodatage et je les télécharge sur un CDN, c'est pourquoi il est important que les horodatages reflètent le moment où le fichier a été réellement modifié, pas quand il a été retiré du dépôt pour la dernière fois.
Ben W
3
@Ben W: la "réponse de Linus" n'est pas là pour dire qu'elle est fausse dans votre situation particulière. Il n'est là que pour rappeler qu'un DVCS n'est pas bien adapté à ce type de fonctionnalité (conservation de l'horodatage).
VonC
15
@VonC: Étant donné que d'autres DVCS modernes comme Bazaar et Mercurial gèrent très bien les horodatages, je dirais plutôt que " git n'est pas bien adapté pour ce genre de fonctionnalité". Si "un" DVCS devrait avoir cette fonctionnalité est discutable (et je pense fermement qu'ils le font).
MestreLion
10
Ce n'est pas une réponse à la question, mais une discussion philosophique sur les mérites de le faire dans un système de contrôle de version. Si la personne aurait aimé cela, elle aurait demandé: "Quelle est la raison pour laquelle git n'utilise pas l'heure de validation pour l'heure de modification des fichiers?"
thomasfuchs
85

Si, cependant, vous voulez vraiment utiliser les heures de validation pour les horodatages lors de l'extraction, essayez d'utiliser ce script et placez-le (comme exécutable) dans le fichier $ 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

Notez cependant que ce script entraînera un retard assez important pour l'extraction de grands référentiels (où grand signifie une grande quantité de fichiers, pas de grandes tailles de fichiers).

Giel
la source
55
+1 pour une réponse réelle, plutôt que de simplement dire "Ne faites pas ça"
DanC
4
| head -n 1doit être évité car il engendre un nouveau processus, -n 1car git rev-listet git logpeut être utilisé à la place.
eregon
3
Il vaut mieux NE PAS lire les lignes avec `...`et for; voir Pourquoi vous ne lisez pas les lignes avec "pour" . J'irais pour git ls-files -zet while IFS= read -r -d ''.
musiphil
2
Une version Windows est-elle possible?
Ehryk
2
au lieu de ce que git show --pretty=format:%ai --abbrev-commit "$(get_file_rev "$1")" | head -n 1vous pouvez faire git show --pretty=format:%ai -s "$(get_file_rev "$1")", cela entraîne la génération de beaucoup moins de données par la showcommande et devrait réduire la surcharge.
Scott Chamberlain
79

MISE À JOUR : Ma solution est maintenant intégrée à Debian / Ubuntu / Mint, Fedora, Gentoo et éventuellement d'autres distributions:

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

À mon humble avis, ne pas stocker les horodatages (et d'autres métadonnées comme les autorisations et la propriété) est une grande limitation de git.

La justification de Linus selon laquelle les horodatages sont nuisibles simplement parce qu'ils «confondent make» est boiteuse :

  • make clean suffit à résoudre les problèmes.

  • S'applique uniquement aux projets qui utilisent make, principalement C / C ++. C'est complètement inutile pour les scripts comme Python, Perl ou la documentation en général.

  • Il n'y a de mal que si vous appliquez les horodatages. Il n'y aurait aucun mal à les stocker en pension. Les appliquer pourrait être une simple --with-timestampsoption pour git checkoutet amis ( clone, pulletc.), à la discrétion de l' utilisateur .

Bazaar et Mercurial stockent les métadonnées. Les utilisateurs peuvent les appliquer ou non lors de la vérification. Mais dans git, puisque les horodatages d'origine ne sont même pas disponibles dans le dépôt, il n'y a pas une telle option.

Donc, pour un très petit gain (ne pas avoir à tout recompiler) qui est spécifique à un sous-ensemble de projets, gitcomme un DVCS général a été paralysé , certaines informations sur les fichiers sont perdues , et, comme l'a dit Linus, c'est INFEASIBLE à faire maintenant. Triste .

Cela dit, puis-je proposer 2 approches?

1 - http://repo.or.cz/w/metastore.git , par David Härdeman. Essaie de faire ce qui git aurait dû être fait en premier lieu : stocke les métadonnées (pas seulement les horodatages) dans le dépôt lors de la validation (via un hook de pré-validation), et les réapplique lors de l'extraction (également via des hooks).

2 - Mon humble version d'un script que j'utilisais auparavant pour générer des archives tar. Comme mentionné dans d'autres réponses, l'approche est un peu différente : appliquer pour chaque fichier l' horodatage du commit le plus récent où le fichier a été modifié.

  • git-restore-mtime , avec de nombreuses options, prend en charge toute disposition de référentiel et fonctionne sur Python 3.

Vous trouverez ci-dessous une version vraiment simple du script, comme preuve de concept, sur Python 2.7. Pour une utilisation réelle, je recommande fortement la version complète ci-dessus:

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

Les performances sont assez impressionnantes, même pour les projets monstres wine, gitou même le noyau 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
la source
2
Mais git ne horodatages en magasin, etc. Il ne vient pas défini par défaut les horodateurs. Il suffit de regarder la sortie degit ls-files --debug
Ross Smith II
9
@RossSmithII: git ls-filesfonctionne sur le répertoire de travail et l'index, donc cela ne signifie pas qu'il stocke réellement ces informations sur le dépôt. Si elle stockait, récupérer (et appliquer) mtime serait trivial.
MestreLion
13
"La justification de Linus selon laquelle les horodatages sont nuisibles simplement parce que cela" confond faire "est nul" - d'accord à 100%, un DCVS ne devrait pas connaître ou se soucier du code qu'il contient! Encore une fois, cela montre les pièges d'essayer de réutiliser des outils écrits pour des cas d'utilisation spécifiques dans des cas d'utilisation généraux. Mercurial est et sera toujours un choix supérieur car il a été conçu et non évolué.
Ian Kemp
6
@davec Vous êtes les bienvenus, heureux que cela ait été utile. La version complète sur github.com/MestreLion/git-tools gère déjà Windows, Python 3, les chemins d'accès non-ASCII, etc. Le script ci-dessus est juste une preuve de concept fonctionnelle, évitez-le pour une utilisation en production.
MestreLion
3
Vos arguments sont valables. J'espère que quelqu'un avec un certain poids fera une demande d'amélioration pour que git ait votre option suggérée --with-timestamps.
weberjn
12

J'ai pris la réponse de Giel et au lieu d'utiliser un script hook post-commit, je l'ai intégré dans mon script de déploiement personnalisé.

Mise à jour : J'ai également supprimé une | head -nsuite à la suggestion de @ eregon et ajouté la prise en charge des fichiers contenant des espaces:

# 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
la source
Merci Daniel, c'est utile de savoir
Alex Dean
la commande --abbrev-commitest superflue en git showraison de --pretty=format:%aison utilisation (le hachage de validation ne fait pas partie de la sortie) et | head -n 1pourrait être remplacé par l'utilisation d'un -sindicateur àgit show
Elan Ruusamäe
1
@ DanielS.Sterling: %aiest auteur date, ISO 8601 comme le format, pour le strict usage de ISO8601 %aI: git-scm.com/docs/git-show
Elan Ruusamäe
4

nous avons été obligés d'inventer encore une autre solution, car nous avions spécifiquement besoin de temps de modification et non de temps de validation, et la solution devait également être portable (c'est-à-dire que faire fonctionner python dans les installations git de Windows n'est vraiment pas une tâche simple) et rapide. Cela ressemble à la solution de David Hardeman, que j'ai décidé de ne pas utiliser en raison du manque de documentation (à partir du référentiel, je n'ai pas pu me faire une idée de ce que fait exactement son code).

Cette solution stocke les mtimes dans un fichier .mtimes dans le référentiel git, les met à jour en conséquence sur les commits (jsut sélectivement les mtimes des fichiers staged) et les applique à l'extraction. Cela fonctionne même avec les versions cygwin / mingw de git (mais vous devrez peut-être copier certains fichiers de cygwin standard dans le dossier de git)

La solution se compose de 3 fichiers:

  1. mtimestore - script de base fournissant 3 options -a (save all - pour l'initialisation dans le référentiel déjà existant (fonctionne avec les fichiers vers git)), -s (pour enregistrer les modifications par étapes) et -r pour les restaurer. Cela vient en fait en 2 versions - une version bash (portable, agréable, facile à lire / modifier) ​​et la version c (une version désordonnée mais rapide, car mingw bash est horriblement lent, ce qui rend impossible l'utilisation de la solution bash sur de grands projets).
  2. hook pré-commit
  3. crochet post-paiement

pré-commit:

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

après le paiement

#!/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;
}
  • notez que les hooks peuvent être placés dans le répertoire template pour automatiser leur placement

plus d'informations peuvent être trouvées ici https://github.com/kareltucek/git-mtime-extension certaines informations obsolètes sont à http://www.ktweb.cz/blog/index.php?page=page&id=116

// edit - version C ++ mise à jour:

  • Maintenant, la version c ++ maintient l'ordre alphabétique -> moins de conflits de fusion.
  • Débarrassez-vous des vilains appels system ().
  • Suppression de $ git update-index --refresh $ du hook post-checkout. Cause quelques problèmes avec le retour sous tortue git, et ne semble pas être très important de toute façon.
  • Notre package Windows peut être téléchargé à l' adresse http://ktweb.cz/blog/download/git-mtimestore-1.4.rar

// modifier voir github pour la version à jour

Karel Tucek
la source
1
Notez qu'après une vérification, les horodatages des fichiers à jour ne sont plus modifiés (Git 2.2.2+, janvier 2015): stackoverflow.com/a/28256177/6309
VonC
3

Le script suivant incorpore les suggestions -n 1et HEAD, fonctionne dans la plupart des environnements non Linux (comme Cygwin) et peut être exécuté lors d'une extraction après coup:

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

En supposant que vous avez nommé le script ci-dessus /path/to/templates/hooks/post-checkoutet / ou /path/to/templates/hooks/post-update, vous pouvez l'exécuter sur un référentiel existant via:

git clone git://path/to/repository.git
cd repository
/path/to/templates/hooks/post-checkout
Ross Smith II
la source
Il a besoin d'une dernière ligne de plus: git update-index --refresh // Les outils de l'interface graphique peuvent s'appuyer sur l'index et afficher l'état "sale" à tout le fichier après une telle opération. À savoir que cela se produit dans TortoiseGit pour Windows code.google.com/p/tortoisegit/issues/detail?id=861
Arioch 'The
1
Et merci pour le script. J'aimerais que ce script fasse partie du programme d'installation standard de Git. Non pas que j'en ai besoin personnellement, mais les membres de l'équipe ressentent simplement le ré-horodatage comme une bannière rouge "d'arrêt" dans l'adoption du VCS.
Arioch 'Le
3

Cette solution devrait fonctionner assez rapidement. Il définit des temps pour committer des temps et mtimes pour des temps d'auteur. Il n'utilise aucun module et doit donc être raisonnablement portable.

#!/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
la source
2

Voici une version optimisée des solutions shell ci-dessus, avec des corrections mineures:

#!/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
la source
1

Voici une méthode avec 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);
}

C'est similaire à la réponse ici:

Quel est l'équivalent de use-commit-times pour git?

il construit une liste de fichiers comme cette réponse, mais il se construit à partir de git ls-files au lieu de simplement chercher dans le répertoire de travail. Cela résout le problème de l'exclusion .gitet résout également le problème des fichiers non suivis. De plus, cette réponse échoue si le dernier commit d'un fichier était un commit de fusion, que j'ai résolu avec git log -m. Comme l'autre réponse, s'arrêtera une fois que tous les fichiers seront trouvés, il n'est donc pas nécessaire de lire tous les validations. Par exemple avec:

https://github.com/git/git

à partir de cette publication, il n'avait qu'à lire 292 commits. En outre, il ignore les anciens fichiers de l'historique selon les besoins, et il ne touchera pas un fichier qui a déjà été touché. Enfin cela semble être un peu plus rapide que l'autre solution. Résultats avec git/gitrepo:

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

J'ai vu quelques demandes pour une version Windows, alors la voici. Créez les deux fichiers suivants:

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;
    }
}

Cela utilise git whatchanged , donc il parcourt tous les fichiers en une seule passe au lieu d'appeler git pour chaque fichier.

Brain2000
la source
0

Je travaille sur un projet où je conserve un clone de mon référentiel pour une utilisation avec des rsyncdéploiements basés. J'utilise des branches pour cibler différents environnements et git checkoutprovoque le changement des modifications du fichier.

Ayant appris que git ne fournit pas un moyen d'extraire des fichiers et de préserver les horodatages, je suis tombé sur la commande git log --format=format:%ai --name-only .dans une autre question SO: lister les dates de dernière validation pour un grand nombre de fichiers, rapidement .

J'utilise maintenant le script suivant pour touchmes fichiers et répertoires de projet afin que mon déploiement avec rsyncsoit plus facile à différencier:

<?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
la source