Git
Chapters ▾ 2nd Edition

9.2 Git et les autres systèmes - Migration vers Git

Migration vers Git

Si vous avez une base de code existant dans un autre Système de Contrôle de Version (SCV) mais que vous avez décidé de commencer à utiliser Git, vous devez migrer votre projet d’une manière ou d’une autre. Cette section passe en revue quelques importateurs pour des systèmes communs, et ensuite démontre comment développer votre propre importateur personnalisé. Vous apprendrez comment importer les données depuis plusieurs des plus gros systèmes de gestion de configuration logicielle (SCM, Software Configuration Management) utilisés professionnellement, parce qu’ils comportent la majorité des utilisateurs qui basculent, et parce que des outils de haute qualité dédiés sont faciles à se procurer.

Subversion

Si vous avez lu la section précédente concernant l’utilisation de git svn, vous pouvez utiliser facilement ces instructions pour git svn clone un dépôt ; ensuite, vous pouvez arrêter d’utiliser le serveur Subversion, pousser vers un nouveau serveur Git, et commencer à l’utiliser. Si vous voulez l’historique, vous pouvez obtenir cela aussi rapidement que vous pouvez tirer les données hors du serveur Subversion (ce qui peut prendre un bout de temps).

Cependant, l’import n’est pas parfait ; et comme ça prendra tant de temps, autant le faire correctement. Le premier problème est l’information d’auteur. Dans Subversion, chaque personne qui crée un commit a un utilisateur sur le système qui est enregistré dans l’information de commit. Les exemples dans la section précédente montrent schacon à quelques endroits, comme la sortie de blame et git svn log. Si vous voulez faire correspondre cela à une meilleure donnée d’auteur Git, vous avez besoin d’une transposition des utilisateurs Subversion vers les auteurs Git. Créez un fichier appelé users.txt qui a cette correspondance dans un format tel que celui-ci :

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

Pour obtenir une liste des noms d’auteur que SVN utilise, vous pouvez lancer ceci :

$ svn log --xml | grep author | sort -u | \
  perl -pe 's/.*>(.*?)<.*/$1 = /'

Cela génère la sortie log dans le format XML, puis garde seulement les lignes avec l’information d’auteur, rejette les doublons, enlève les étiquettes XML. (Bien sûr, cela ne marche que sur une machine ayant grep, sort et perl installés.) Ensuite, redirigez cette sortie dans votre fichier users.txt afin que vous puissiez ajouter l’information d’utilisateur Git équivalente près de chaque entrée.

Vous pouvez fournir ce fichier à git svn pour l’aider à faire correspondre la donnée d’auteur plus précisément. Vous pouvez aussi demander à git svn de ne pas inclure les metadonnées que Subversion importe normalement, en passant --no-metadata à la commande clone ou init. Ceci fait ressembler votre commande import à ceci :

$ git-svn clone http://my-project.googlecode.com/svn/ \
      --authors-file=users.txt --no-metadata -s my_project

Maintenant vous devriez avoir un import Subversion plus joli dans votre dossier my_project. Au lieu de commits qui ressemblent à ceci

commit 37efa680e8473b615de980fa935944215428a35a
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

    git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
    be05-5f7a86268029

ils ressemblent à ceci :

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

Non seulement le champ Auteur a l’air beaucoup mieux, mais le git-svn-id n’est plus là non plus.

Vous devriez aussi faire un peu de ménage post-import. D’abord, vous devriez nettoyer les références bizarres que git svn a installées. Premièrement vous déplacerez les étiquettes afin qu’elles soient de véritables étiquettes plutôt que d’étranges branches distantes, et ensuite vous déplacerez le reste des branches afin qu’elles soient locales.

Pour déplacer les étiquettes pour qu’elles soient des étiquettes Git propres, lancez

$ cp -Rf .git/refs/remotes/origin/tags/* .git/refs/tags/
$ rm -Rf .git/refs/remotes/origin/tags

Ceci prend les références qui étaient des branches distantes qui commençaient par remotes/origin/tags et en fait de vraies étiquettes (légères).

Ensuite, déplacez le reste des références sous refs/remotes pour qu’elles soient des branches locales :

$ cp -Rf .git/refs/remotes/origin/* .git/refs/heads/
$ rm -Rf .git/refs/remotes/origin

Il peut arriver que vous voyiez quelques autres branches qui sont suffixées par @xxx (où xxx est un nombre), alors que dans Subversion vous ne voyez qu’une seule branche. C’est en fait une fonctionnalité Subversion appelée « peg-revisions », qui est quelque chose pour laquelle Git n’a tout simplement pas d’équivalent syntaxique. Donc, git svn ajoute simplement le numéro de version svn au nom de la branche de la même façon que vous l’auriez écrit dans svn pour adresser la « peg-revision » de cette branche. Si vous ne vous souciez plus des « peg-revisions », supprimez-les simplement en utilisant git branch -d.

Maintenant toutes les vieilles branches sont de vraies branches Git et toutes les vieilles étiquettes sont de vraies étiquettes Git.

Il y a une dernière chose à nettoyer. Malheureusement, git svn crée une branche supplémentaire appelée trunk, qui correspond à la branche par défaut de Subversion, mais la ref trunk pointe au même endroit que master. Comme master est plus idiomatiquement Git, voici comment supprimer la branche supplémentaire :

$ git branch -d trunk

La dernière chose à faire est d’ajouter votre nouveau serveur Git en tant que serveur distant et pousser vers lui. Voici un exemple d’ajout de votre serveur en tant que serveur distant :

$ git remote add origin git@my-git-server:myrepository.git

Puisque vous voulez que vos branches et étiquettes montent, vous pouvez maintenant lancer :

$ git push origin --all
$ git push origin --tags

Toutes vos branches et étiquettes devraient être sur votre nouveau serveur Git dans un import joli et propre.

Mercurial

Puisque Mercurial et Git ont des modèles assez similaires pour représenter les versions, et puisque Git est un peu plus flexible, convertir un dépôt depuis Mercurial vers Git est assez simple, en utilisant un outil appelé "hg-fast-export", duquel vous aurez besoin d’une copie :

$ git clone http://repo.or.cz/r/fast-export.git /tmp/fast-export

La première étape dans la conversion est d’obtenir un clone complet du dépôt Mercurial que vous voulez convertir :

$ hg clone <remote repo URL> /tmp/hg-repo

L’étape suivante est de créer un fichier d’association d’auteur. Mercurial est un peu plus indulgent que Git pour ce qu’il mettra dans le champ auteur pour les modifications, donc c’est le bon moment pour faire le ménage. La génération de ceci tient en une ligne de commande dans un shell bash :

$ cd /tmp/hg-repo
$ hg log | grep user: | sort | uniq | sed 's/user: *//' > ../authors

Cela prendra quelques secondes, en fonction de la longueur de l’historique de votre projet, et ensuite le fichier /tmp/authors ressemblera à quelque chose comme ceci :

bob
bob@localhost
bob <bob@company.com>
bob jones <bob <AT> company <DOT> com>
Bob Jones <bob@company.com>
Joe Smith <joe@company.com>

Dans cet exemple, la même personne (Bob) a créé des modifications sous différents noms, dont l’un est correct, et dont un autre est complètement invalide pour un commit Git. Hg-fast-import nous laisse régler cela en ajoutant ={nouveau nom et adresse de courriel} à la fin de chaque ligne que l’on veut changer, et en enlevant les lignes pour les noms d’utilisateur auxquels on ne veut pas toucher. Si tous les noms d’utilisateur ont l’air bien, nous n’aurons pas du tout besoin de ce fichier. Dans cet exemple, nous voulons que notre fichier ressemble à cela :

bob=Bob Jones <bob@company.com>
bob@localhost=Bob Jones <bob@company.com>
bob jones <bob <AT> company <DOT> com>=Bob Jones <bob@company.com>
bob <bob@company.com>=Bob Jones <bob@company.com>

L’étape suivante consiste à créer notre nouveau dépôt Git, et à lancer le script d’export :

$ git init /tmp/converted
$ cd /tmp/converted
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors

L’option -r indique à hg-fast-export où trouver le dépôt Mercurial que l’on veut convertir, et l’option -A lui indique où trouver le fichier de correspondance d’auteur. Le script analyse les modifications Mercurial et les convertit en un script pour la fonctionnalité "fast-import" de Git (que nous détaillerons un peu plus tard). Cela prend un peu de temps (bien que ce soit beaucoup plus rapide que si c’était à travers le réseau), et la sortie est assez verbeuse :

$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
Loaded 4 authors
master: Exporting full revision 1/22208 with 13/0/0 added/changed/removed files
master: Exporting simple delta revision 2/22208 with 1/1/0 added/changed/removed files
master: Exporting simple delta revision 3/22208 with 0/1/0 added/changed/removed files
[…]
master: Exporting simple delta revision 22206/22208 with 0/4/0 added/changed/removed files
master: Exporting simple delta revision 22207/22208 with 0/2/0 added/changed/removed files
master: Exporting thorough delta revision 22208/22208 with 3/213/0 added/changed/removed files
Exporting tag [0.4c] at [hg r9] [git :10]
Exporting tag [0.4d] at [hg r16] [git :17]
[…]
Exporting tag [3.1-rc] at [hg r21926] [git :21927]
Exporting tag [3.1] at [hg r21973] [git :21974]
Issued 22315 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:     120000
Total objects:       115032 (    208171 duplicates                  )
      blobs  :        40504 (    205320 duplicates      26117 deltas of      39602 attempts)
      trees  :        52320 (      2851 duplicates      47467 deltas of      47599 attempts)
      commits:        22208 (         0 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:         109 (         2 loads     )
      marks:        1048576 (     22208 unique    )
      atoms:           1952
Memory total:          7860 KiB
       pools:          2235 KiB
     objects:          5625 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =      90430
pack_report: pack_mmap_calls          =      46771
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =  340852700 /  340852700
---------------------------------------------------------------------

$ git shortlog -sn
   369  Bob Jones
   365  Joe Smith

C’est à peu près tout ce qu’il y a. Toutes les étiquettes Mercurial ont été converties en étiquettes Git, et les branches et marques-page Mercurial ont été convertis en branches Git. Maintenant vous êtes prêt à pousser le dépôt vers son nouveau serveur d’hébergement :

$ git remote add origin git@my-git-server:myrepository.git
$ git push origin --all

Bazaar

Bazaar est un système de contrôle de version distribué tout comme Git, en conséquence de quoi il est assez facile de convertir un dépôt Bazaar en un dépôt Git. Pour cela, vous aurez besoin d’importer le plugin bzr-fastimport.

Obtenir le plugin bzr-fastimport

La procédure d’installation du plugin bzr-fastimport est différente sur les systèmes type UNIX et sur Windows.

Dans le premier cas, le plus simple est d’installer le paquet bzr-fastimport avec toutes les dépendances requises.

Par exemple, sur Debian et dérivés, vous feriez comme cela :

$ sudo apt-get install bzr-fastimport

Avec RHEL, vous feriez ainsi :

$ sudo yum install bzr-fastimport

Avec Fedora, depuis la sortie de la version 22, le nouveau gestionnaire de paquets est dnf :

$ sudo dnf install bzr-fastimport

Si le paquet n’est pas disponible, vous pouvez l’installer en tant que plugin :

$ mkdir --parents ~/.bazaar/plugins/bzr     # crée les dossiers nécessaires aux plugins
$ cd ~/.bazaar/plugins/bzr
$ bzr branch lp:bzr-fastimport fastimport   # importe le plugin bzr-fastimport
$ cd fastimport
$ sudo python setup.py install --record=files.txt   # installe le plugin

Pour que ce plugin fonctionne, vous aurez aussi besoin du module Python fastimport. Vous pouvez vérifier s’il est présent ou non et l’installer avec les commandes suivantes :

$ python -c "import fastimport"
Traceback (most recent call last):
  File "<string>" , line 1, in <module>
ImportError: No module named fastimport
$ pip install fastimport

S’il n’est pas disponible, vous pouvez le télécharger à l’adresse https://pypi.python.org/pypi/fastimport/.

Dans le second cas (sous Windows), bzr-fastimport est automatiquement installé avec la version standalone et l’installation par défaut (laisser toutes les cases à cocher cochées). Alors, vous n’avez rien à faire.

À ce stade, la façon d’importer un dépôt Bazaar diffère selon que vous n’avez qu’une seule branche ou que vous travaillez avec un dépôt qui a plusieurs branches.

Projet avec une seule branche

Maintenant positionnez-vous dans le dossier qui contient votre dépôt Bazaar et initialisez le dépôt Git :

$ cd /chemin/vers/le/depot/bzr
$ git init

Vous pouvez exporter simplement votre dépôt Bazaar et le convertir en un dépôt Git avec la commande suivante :

$ bzr fast-export --plain . | git fast-import

Selon la taille du projet, votre dépôt Git est constitué dans un délai allant de quelques secondes à plusieurs minutes.

Cas d’un projet avec une branche principale et une branche de travail

Vous pouvez aussi importer un dépôt Bazaar qui contient plusieurs branches. Supposons que vous avez deux branches : l’une représente la branche principale (monProjet.trunk), l’autre est la branche de travail (monProjet.travail).

$ ls
monProjet.trunk monProjet.travail

Créez le dépôt Git et placez-vous-y :

$ git init depot-git
$ cd depot-git

Tirez la branche principale dans le dépôt git :

$ bzr fast-export --marks=../marks.bzr --plain ../monProjet.trunk | \
git fast-import --export-marks=../marks.git

Tirez la branche de travail dans le dépôt git :

$ bzr fast-export --marks=../marks.bzr --plain --git-branch=travail ../monProjet.travail | \
git fast-import --import-marks=../marks.git --export-marks=../marks.git

Maintenant, git branch vous montre la branche master tout comme la branche travail. Vérifiez les logs pour vous assurer qu’ils sont complets et supprimez les fichiers marks.bzr et marks.git.

Synchroniser l’index

Quel que soit le nombre de branches que vous aviez et la méthode d’importation, votre index n’est pas synchronisé avec HEAD, et avec l’import de plusieurs branches, votre répertoire de travail n’est pas synchronisé non plus. Cette situation se résout simplement avec la commande suivante :

$ git reset --hard HEAD

Ignorer les fichiers qui étaient ignorés avec .bzrignore

Occupons-nous maintenant des fichiers à ignorer. Comme le format de .bzrignore est le même que celui de .gitignore, le plus simple est de renommer votre fichier .bzrignore. Vous devrez aussi créer un commit qui contient cette modification pour la migration :

$ git mv .bzrignore .gitignore
$ git commit -m 'Migration de Bazaar vers Git'

Envoyer votre dépôt git sur le serveur

Nous y sommes enfin ! Vous pouvez maintenant pousser votre dépôt sur son nouveau serveur d’hébergement :

$ git remote add origin git@mon-serveur-git:mon-depot-git.git
$ git push origin --all
$ git push origin --tags

La migration de Bazaar vers Git est maintenant terminée, vous pouvez travailler sur votre dépôt git.

Perforce

Le système suivant dont vous allez voir l’importation est Perforce. Ainsi que nous l’avons dit plus haut, il y a deux façons de permettre de faire parler Git et Perforce l’un avec l’autre : git-p4 et Perforce Git Fusion.

Perforce Git Fusion

Git Fusion rend ce processus assez indolore. Configurez les paramètres de votre projet, les correspondances utilisateur et les branches en utilisant un fichier de configuration (comme discuté dans Git Fusion), et clonez le dépôt. Git Fusion vous laisse avec ce qui ressemble à un dépôt Git natif, qui est alors prêt à être poussé vers un hôte Git natif si vous le désirez. Vous pouvez même utiliser Perforce comme hôte Git si vous ça vous plaît.

Git-p4

Git-p4 peut aussi agir comme outil d’import. Comme exemple, nous importerons le projet Jam depuis le Dépôt Public Perforce. Pour définir votre client, vous devez exporter la variable d’environnement P4PORT pour pointer vers le dépôt Perforce :

$ export P4PORT=public.perforce.com:1666
Note

Pour suivre tout le long, vous aurez besoin d’un dépôt Perforce auquel vous connecter. Nous utiliserons le dépôt public à public.perforce.com pour nos exemples, mais vous pouvez utiliser n’importe quel dépôt auquel vous avez accès.

Lancez la commande git p4 clone pour importer le projet Jam depuis le serveur Perforce, en fournissant le chemin vers le dépôt et le projet dans lequel vous voulez importer le projet :

$ git-p4 clone //guest/perforce_software/jam@all p4import
Importing from //guest/perforce_software/jam@all into p4import
Initialized empty Git repository in /private/tmp/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 9957 (100%)

Ce projet particulier a seulement une branche, mais si vous avez des branches configurées avec des vues de branche (ou juste un ensemble de dossiers), vous pouvez utiliser l’option --detect-branches avec git p4 clone pour importer aussi toutes les branches du projet. Voyez Branche pour plus de détails sur ceci.

A ce point, vous avez presque terminé. Si vous allez dans le dossier p4import et lancez git log, vous pouvez voir le travail importé :

$ git log -2
commit e5da1c909e5db3036475419f6379f2c73710c4e6
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

    [git-p4: depot-paths = "//public/jam/src/": change = 8068]

commit aa21359a0a135dda85c50a7f7cf249e4f7b8fd98
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

    [git-p4: depot-paths = "//public/jam/src/": change = 7304]

Vous pouvez voir que git-p4 a laissé un identifiant dans chaque message de commit. C’est bien de garder cet identifiant-là, au cas où vous auriez besoin de référencer le numéro de changement Perforce plus tard. Cependant, si vous souhaitez enlever l’identifiant, c’est maintenant le moment de le faire – avant que vous ne commenciez à travailler sur le nouveau dépôt. Vous pouvez utiliser git filter-branch pour enlever en masse les chaînes d’identifiant :

$ git filter-branch --msg-filter 'sed -e "/^\[git-p4:/d"'
Rewrite e5da1c909e5db3036475419f6379f2c73710c4e6 (125/125)
Ref 'refs/heads/master' was rewritten

Si vous lancez git log, vous pouvez voir que toutes les sommes de vérification SHA-1 pour les commits ont changé, mais les chaînes git-p4 ne sont plus dans les messages de commit :

$ git log -2
commit b17341801ed838d97f7800a54a6f9b95750839b7
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

commit 3e68c2e26cd89cb983eb52c024ecdfba1d6b3fff
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

Votre import est prêt à être poussé vers votre nouveau serveur Git.

TFS

Si votre équipe est en train de convertir son code source de TFVC à Git, vous voudrez la conversion de la plus haute fidélité que vous puissiez obtenir. Cela signifie que, tandis que nous couvrions à la fois git-tfs et git-tf pour la section interop, nous couvrirons seulement git-tfs dans cette partie, parce que git-tfs supporte les branches, et c’est excessivement difficile en utilisant git-tf.

Note

Ceci est une conversion à sens unique. Le dépôt Git résultant ne pourra pas se connecter au projet TFVC original.

La première chose à faire est d’associer les noms d’utilisateur. TFC est assez permissif pour ce qui va dans le champ auteur pour les changements, mais Git veut un nom et une adresse de courriel lisibles par un humain. Vous pouvez obtenir cette information depuis la ligne de commande client tf, comme ceci :

PS> tf history $/myproject -recursive > AUTHORS_TMP

Cela récupère toutes les modifications de l’historique du projet et les insère dans le fichier AUTHORS_TMP que nous traiterons pour en extraire la donnée de la colonne Utilisateur (la deuxième). Ouvrez le fichier et trouvez à quels caractères commence et finit la colonne et remplacez, dans la ligne de commande suivante, les paramètres 11-20 de la commande cut par ceux que vous avez trouvés :

PS> cat AUTHORS_TMP | cut -b 11-20 | tail -n+3 | sort | uniq > AUTHORS

La commande cut ignore tout sauf les caractères 11-20 de chaque ligne. La commande tail saute les deux premières lignes, qui sont des champs d’en-tête et des soulignés dans le style ASCII. Le résultat de tout ceci est envoyé à sort et uniq pour éliminer les doublons, et sauvé dans un fichier nommé AUTHORS. L’étape suivante est manuelle ; afin que git-tfs fasse un usage effectif de ce fichier, chaque ligne doit être dans ce format :

DOMAIN\username = User Name <email@address.com>

La partie gauche est le champ “utilisateur” de TFVC, et la partie droite du signe égal est le nom d’utilisateur qui sera utilisé pour les commits Git.

Une fois que vous avez ce fichier, la chose suivante à faire est de faire un clone complet du projet TFVC par lequel vous êtes intéressé :

PS> git tfs clone --with-branches --authors=AUTHORS https://username.visualstudio.com/DefaultCollection $/project/Trunk project_git

Ensuite vous voudrez nettoyer les sections git-tfs-id du bas des messages de commit. La commande suivante le fera :

PS> git filter-branch -f --msg-filter 'sed "s/^git-tfs-id:.*$//g"' '--' --all

Cela utilise la commande sed de l’environnement Git-bash pour remplacer n’importe quelle ligne commençant par "git-tfs-id:" par du vide que Git ignorera ensuite.

Une fois cela fait, vous êtes prêt à ajouter un nouveau serveur distant, y pousser toutes les branches, et vous avez votre équipe prête à commencer à travailler depuis Git.

Un importateur personnalisé

Si votre système n’est pas un de ceux ci-dessus, vous devriez chercher un importateur en ligne – des importateurs de qualité sont disponibles pour plein d’autres systèmes, incluant CVS, Clear Case, Visual Source Safe, même un dossier d’archives. Si aucun de ces outils ne fonctionne pour vous, vous avez un outil plus obscur, ou alors vous avez besoin d’un procédé d’importation personnalisé, vous devriez utiliser git fast-import. Cette commande lit des instructions simples depuis l’entrée standard pour écrire des données Git spécifiques. Il est bien plus facile de créer des objets Git de cette façon que de lancer des commandes Git brutes ou que d’essayer d’écrire les objets bruts (voir Les tripes de Git pour plus d’informations). De cette façon, vous pouvez écrire un script d’importation qui lit l’information nécessaire hors du système duquel vous importez et qui affiche les instructions directement dans la sortie standard. Vous pouvez alors lancer ce programme et envoyer sa sortie à travers un tube dans git fast-import.

Pour démontrer rapidement, vous écrirez un importateur simple. Supposez que vous travaillez dans current, vous sauvegardez votre projet en copiant occasionnellement le dossier dans un dossier de sauvegarde estampillé de la date back_YYYY_MM_DD, et vous voulez importer cela dans Git. Votre structure de dossier ressemble à ceci :

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Pour importer un dossier Git, vous devez passer en revue comment Git stocke ses données. Comme vous vous le rappelez, Git est fondamentalement une liste liée d’objets commit qui pointent sur un instantané de contenu. Tout ce que vous avez à faire est de dire à fast-import ce que sont les instantanés de contenu, quelles données de commit pointent sur eux, et l’ordre dans lequel ils vont. Votre stratégie sera d’explorer les instantanés un à un et créer les commits avec les contenus dans chaque dossier, en liant chaque commit avec le précédent.

Comme nous l’avons fait dans Exemple de politique gérée par Git, nous écrirons ceci en Ruby, parce que c’est ce avec quoi nous travaillons généralement et ça a tendance à être facile à lire. Vous pouvez écrire cet exemple assez facilement avec n’importe quel langage de programmation auquel vous êtes familier – il faut seulement afficher l’information appropriée dans stdout. Et, si vous travaillez sous Windows, cela signifie que vous devrez prendre un soin particulier à ne pas introduire de retour chariot (carriage return, CR) à la fin de vos lignes – git fast-import est très exigeant ; il accepte seulement la fin de ligne (Line Feed, LF) et pas le retour chariot fin de ligne (CRLF) que Windows utilise.

Pour commencer, vous vous placerez dans le dossier cible et identifierez chaque sous-dossier, chacun étant un instantané que vous voulez importer en tant que commit. Vous vous placerez dans chaque sous-dossier et afficherez les commandes nécessaires pour l’exporter. Votre boucle basique principale ressemble à ceci :

last_mark = nil

# boucle sur les dossiers
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # rentre dans chaque dossier cible
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

Vous lancez print_export à l’intérieur de chaque dossier, qui prend le manifeste et la marque de l’instantané précédent et retourne la marque et l’empreinte de celui-ci ; de cette façon, vous pouvez les lier proprement. “Marque” est le terme de fast-import pour un identifiant que vous donnez à un commit ; au fur et à mesure que vous créez des commits, vous donnez à chacun une marque que vous pouvez utiliser pour le lier aux autres commits. Donc, la première chose à faire dans votre méthode print_export est de générer une marque à partir du nom du dossier :

mark = convert_dir_to_mark(dir)

Vous ferez ceci en créant un tableau de dossiers et en utilisant la valeur d’index comme marque, car une marque doit être un nombre entier. Votre méthode ressemble à ceci :

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

Maintenant que vous avez une représentation par un entier de votre commit, vous avez besoin d’une date pour les métadonnées du commit. Puisque la date est exprimée dans le nom du dossier, vous l’analyserez. La ligne suivante dans votre fichier print_export est

date = convert_dir_to_date(dir)

convert_dir_to_date est définie comme

def convert_dir_to_date(dir)
  if dir == 'current'
    return Time.now().to_i
  else
    dir = dir.gsub('back_', '')
    (year, month, day) = dir.split('_')
    return Time.local(year, month, day).to_i
  end
end

Cela retourne une valeur entière pour la date de chaque dossier. Le dernier bout de méta-information dont vous avez besoin pour chaque commit est la donnée de l’auteur, que vous codez en dur dans une variable globale :

$author = 'John Doe <john@example.com>'

Maintenant vous êtes prêt à commencer à publier l’information de commit pour votre importateur. L’information initiale déclare que vous êtes en train de définir un objet commit et sur quelle branche il est, suivi de la marque que vous avez générée, l’information d’auteur et le message de commit, et ensuite le précédent commit, s’il y en a un. Le code ressemble à ceci :

# affiche l'information d'import
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark

Vous codez en dur le fuseau horaire (-0700) parce que c’est facile de faire ainsi. Si vous importez depuis un autre système, vous devez spécifier le fuseau horaire comme décalage. Le message de commit doit être exprimé dans un format spécial :

data (taille)\n(contenu)

Le format est constitué du mot data, de la taille de la donnée à lire, d’une nouvelle ligne et finalement de la donnée. Comme vous avez besoin d’utiliser le même format pour spécifier le contenu du fichier plus tard, vous créez une méthode assistante, export_data :

def export_data(string)
  print "data #{string.size}\n#{string}"
end

Tout ce qui reste à faire est de spécifier le contenu du fichier pour chaque instantané. C’est facile, car vous les avez dans un dossier – vous pouvez imprimer la commande deleteall suivie par le contenu de chaque fichier du dossier. Git enregistrera ensuite chaque instantané de manière appropriée :

puts 'deleteall'
Dir.glob("**/*").each do |file|
  next if !File.file?(file)
  inline_data(file)
end

Note : Comme beaucoup de systèmes conçoivent leurs révisions comme des changements d’un commit à l’autre, fast-import peut aussi prendre des commandes avec chaque commit pour spécifier quels fichiers ont été ajoutés, supprimés ou modifiés et ce qu’est le nouveau contenu. Vous pourriez calculer les différences entre instantanés et fournir seulement cette donnée, mais faire ainsi est plus complexe – vous pouvez aussi bien donner à Git toutes les données et le laisser faire. Si cela convient mieux pour vos données, référez-vous à la page de manuel fast-import pour les détails sur la manière de fournir les données de cette façon.

Le format pour lister le contenu d’un nouveau fichier ou pour spécifier un fichier modifié avec le nouveau contenu est le suivant :

M 644 inline path/to/file
data (taille)
(contenu du fichier)

Ici, 644 est le mode (si vous avez des fichiers exécutables, vous devez le détecter et spécifier 755 à la place), et inline dit que vous listerez le contenu immédiatement après cette ligne. Votre méthode inline_data ressemble à ceci :

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

Vous réutilisez la méthode export_data que vous avez définie plus tôt, parce que c’est de la même façon que vous avez spécifié vos données du message de commit.

La dernière chose que vous avez besoin de faire est de retourner la marque courante pour qu’elle soit passée à la prochaine itération :

return mark
Note

Si vous êtes sous Windows, vous devrez vous assurer d’ajouter une étape supplémentaire. Comme mentionné précédemment, Windows utilise CRLF comme caractères de fin de ligne alors que git fast-import ne s’attend qu’à LF. Pour contourner ce problème et satisfaire git fast-import, vous devez indiquer à Ruby d’utiliser LF au lieu de CRLF :

$stdout.binmode

Et voilà. Voici le script dans son intégralité :

#!/usr/bin/env ruby

$stdout.binmode
$author = "John Doe <john@example.com>"

$marks = []
def convert_dir_to_mark(dir)
    if !$marks.include?(dir)
        $marks << dir
    end
    ($marks.index(dir)+1).to_s
end


def convert_dir_to_date(dir)
    if dir == 'current'
        return Time.now().to_i
    else
        dir = dir.gsub('back_', '')
        (year, month, day) = dir.split('_')
        return Time.local(year, month, day).to_i
    end
end

def export_data(string)
    print "data #{string.size}\n#{string}"
end

def inline_data(file, code='M', mode='644')
    content = File.read(file)
    puts "#{code} #{mode} inline #{file}"
    export_data(content)
end

def print_export(dir, last_mark)
    date = convert_dir_to_date(dir)
    mark = convert_dir_to_mark(dir)

    puts 'commit refs/heads/master'
    puts "mark :#{mark}"
    puts "committer #{$author} #{date} -0700"
    export_data("imported from #{dir}")
    puts "from :#{last_mark}" if last_mark

    puts 'deleteall'
    Dir.glob("**/*").each do |file|
        next if !File.file?(file)
        inline_data(file)
    end
    mark
end


# explore les dossiers
last_mark = nil
Dir.chdir(ARGV[0]) do
    Dir.glob("*").each do |dir|
        next if File.file?(dir)

        # move into the target directory
        Dir.chdir(dir) do
            last_mark = print_export(dir, last_mark)
        end
    end
end

Si vous lancez ce script, vous obtiendrez un contenu qui ressemble à peu près à ceci :

$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <john@example.com> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello

This is my readme.
commit refs/heads/master
mark :2
committer John Doe <john@example.com> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby

puts "Hey there"
M 644 inline README.md
(...)

Pour lancer l’importateur, envoyez à travers un tube cette sortie à git fast-import pendant que vous êtes dans le dossier Git dans lequel vous voulez importer. Vous pouvez créer un nouveau dossier et ensuite exécuter git init à l’intérieur de celui-ci comme point de départ, et ensuite exécuter votre script :

$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:           13 (         6 duplicates                  )
      blobs  :            5 (         4 duplicates          3 deltas of          5 attempts)
      trees  :            4 (         1 duplicates          0 deltas of          4 attempts)
      commits:            4 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              2
Memory total:          2344 KiB
       pools:          2110 KiB
     objects:           234 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =         10
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          2 /          2
pack_report: pack_mapped              =       1457 /       1457
---------------------------------------------------------------------

Comme vous pouvez le voir, lorsque c’est terminé avec succès, il vous donne un lot de statistiques sur ce qu’il a fait. Dans ce cas-ci, vous avez importé un total de 13 objets pour 4 commits dans une branche. Maintenant, vous pouvez lancer git log pour voir votre nouvel historique :

$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <john@example.com>
Date:   Tue Jul 29 19:39:04 2014 -0700

    imported from current

commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <john@example.com>
Date:   Mon Feb 3 01:00:00 2014 -0700

    imported from back_2014_02_03

Vous y voilà — un dépôt Git beau et propre. Il est important de noter que rien n’est extrait – vous n’avez d’abord aucun fichier dans votre répertoire de travail. Pour les obtenir, vous devez réinitialiser votre branche là où master est maintenant :

$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb

Vous pouvez faire beaucoup plus avec l’outil fast-import – manipuler différents modes, les données binaires, les branches multiples et la fusion, les étiquettes, les indicateurs de progression, et plus encore. Nombre d’exemples de scénarios plus complexes sont disponibles dans le dossier contrib/fast-import du code source Git.