Git 🌙
Chapters ▾ 2nd Edition

9.2 Git en andere systemen - Migreren naar Git

Migreren naar Git

Als je een bestaande codebase in een andere VCS hebt, en je hebt besloten om Git te gaan gebruiken, moet je je project op de een of andere manier migreren. Deze paragraaf behandelt een aantal importeerders voor veelgebruikte systemen, en laat je daarna zien hoe je je eigen importeur kunt ontwikkelen. Je zult kunnen lezen hoe gegevens uit een aantal van de grote professioneel gebruikte SCM systemen te importeren, omdat zij het leeuwendeel van de gebruikers vormen die overgaan, en omdat het eenvoudig is om instrumenten van hoge kwaliteit te pakken te krijgen.

Subversion

Als je de vorige paragraaf leest over het gebruik van`git svn`, kan je eenvoudigweg deze instructies gebruiken om met git svn clone een repository te maken en daarna te stoppen met de Subversion server, naar een nieuwe Git server te pushen en die beginnen te gebruiken. Als je de historie wilt, kan je dat zo snel voor elkaar krijgen als je de gegevens uit de Subversion server kunt krijgen (en dat kan even duren).

Deze import is echter niet perfect, en omdat het zo lang duurt, kan je eigenlijk ook meteen maar goed doen. Het eerste probleem is de auteur-informatie. In Subversion, heeft elke persoon die commit heeft gedaan een gebruikersnaam op het systeem die wordt opgenomen in de commit-informatie. De voorbeelden in de vorige paragraaf tonen schacon in bepaalde plaatsen, zoals de blame uitvoer en de git svn log. Als je dit beter op Git auteur-gegevens wilt mappen, moet je een relatie leggen van de Subversion gebruikers naar de Git auteurs. Maak een bestand genaamd users.txt die deze mapping-informatie heeft in een formaat als deze:

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

Om een lijst van auteur-namen te krijgen die SVN gebruikt, kan je dit aanroepen:

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

Dat maakt de loguitvoer in XML formaat aan, en behoudt vervolgens alleen de regels met auteur-informatie, verwijdert duplicaten en haalt de XML tags weg. (Dit werkt duidelijk alleen op een machine met grep, sort, en perl erop geïnstalleerd). Stuur daarna de uitvoer naar je users.txt bestand zodat je de overeenkomstige Git gebruiker gegevens naast elke regel kunt zetten.

Je kunt dit bestand aan git svn geven om het te helpen de auteur gegevens beter te mappen. Je kunt git svn ook vertellen de meta-data die Subversion normaalgesproken importeert niet mee te nemen, door --no-metadata mee te geven aan de clone of init commando’s. Hierdoor ziet je import commando er ongeveer zo uit:

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

Nu zou je een mooiere Subversion import moeten hebben in je my_project directory. In plaats van commits die er uit zien als dit

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

zien ze er zo uit:

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

    fixed install - go to trunk

Niet alleen ziet het Author veld er veel beter uit, maar de git-svn-id is er ook niet meer.

Je moet ook nog wat opschonen na de import. Onder andere moet je de vreemde referenties opschonen die git svn heeft gemaakt. Allereerst ga je de tags verplaatsen zodat ze echte tags zijn, in plaats van vreemde remote branches, en daarna verplaats je de overige branches zodat ze lokaal zijn.

Om de tags te verplaatsen zodat ze echte Git tags worden, roep je dit aan

$ for t in $(git for-each-ref --format='%(refname:short)' refs/remotes/tags); do git tag ${t/tags\//} $t && git branch -D -r $t; done

Dit neemt de referenties die remote branches waren en begonnen met refs/remotes/tags/ en maakt er echte (lichtgewicht) tags van.

Daarna verplaatsen we de overige referenties onder refs/remotes om er lokale branches van te maken:

$ for b in $(git for-each-ref --format='%(refname:short)' refs/remotes); do git branch $b refs/remotes/$b && git branch -D -r $b; done

Het kan gebeuren dat je een aantal extra branches ziet die vooraf worden gegaan door @xxx (waar xxx een getal is), terwijl je in Subversion aleen maar een branch ziet. Dit is eigenlijk een Subversion kenmerk genaamd “peg-revisions”, wat iets is waar Git gewoonweg geen syntactische tegenhanger voor heeft. Vandaar dat git svn eenvoudigweg het svn versienummer aan de branchnaam toevoegt op dezelfde manier als jij dit zou hebben gedaan in svn om het peg-revisie van die branch te adresseren. Als je niet meer om de peg-revisies geeft, kan je ze simpelweg verwijderen:

$ for p in $(git for-each-ref --format='%(refname:short)' | grep @); do git branch -D $p; done

Nu zijn alle oude branches echte Git branches en alle oude tags echte Git tags.

Er is nog een laatste ding om op te schonen: Helaas maakt git svn een extra branch aan met de naam trunk, wat overeenkomt met de standaard branch in Subversion, maar de trunk-referentie wijst naar dezelfde plek als master. Omdat master idiomatisch meer Git is, is hier de manier om die extra branch te verwijderen:

$ git branch -d trunk

Het laatste om te doen is om je nieuwe Git server als een remote toe te voegen en er naar te pushen. Hier is een voorbeeld van een server als een remote toe te voegen:

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

Omdat al je branches en tags ernaar wilt sturen, kan je nu dit aanroepen:

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

Al je branches en tags zouden nu op je nieuwe Git server moeten staan in een mooie, schone import.

Mercurial

Omdat Mercurial and Git een redelijk overeenkomend model hebben om versies te representeren en omdat Git iets flexibeler is, is het converteren van een repository uit Mercurial naar Git minder omslachtig, gebruik makend van een instrument dat "hg-fast-export" heet, waar je een kopie van nodig gaat hebben:

$ git clone https://github.com/frej/fast-export.git

De eerste stap in de conversie is om een volledige kloon van de Mercurial repository die je wilt converteren te maken:

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

De volgende stap is om een auteur-mapping bestand te maken. Mercurial is wat minder streng dan Git voor wat het in het auteur veld zet voor changesets, dus dit is een goed moment om schoon schip te maken. Het aanmaken hiervan is een enkele commando regel in een bash shell:

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

Dit duurt een paar tellen, afhankelijk van de lengte van de geschiedenis van je project, en nadien ziet het /tmp/authors bestand er ongeveer zo uit:

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

In dit voorbeeld, heeft dezelfde persoon (Bob) changesets aangemaakt onder vier verschillende namen, waarvan er één er wel correct uitziet, en één ervan zou voor een Git commit helemaal niet geldig zijn. Hg-fast-export laat ons dit corrigeren door elke regel in een instructie te veranderen: "<invoer>"="<uitvoer>", waarbij <invoer> in een <uitvoer> wordt gewijzigd. Binnen de <invoer> en <uitvoer> tekenreeksen, worden alle escaped reeksen die door de python string_escape encoding worden begrepen ondersteund. Als het auteur mapping-bestand geen passende <invoer> regel heeft, wordt deze ongewijzigd doorgestuurd naar Git. Als alle gebruikersnamen er goed uitzien, hebben we dit bestand helemaal niet nodig. In ons voorbeeld, willen we dat ons bestand er zo uit ziet:

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

Hetzelfde soort mapping bestand kan worden gebruikt om branches en tags te hernoemen als de Mercurial naam niet wordt toegestaan door Git.

De volgende stap is om onze nieuwe Git repository aan te maken en het volgende export script aan te roepen:

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

De -r vlag vertelt hg-fast-export waar het de Mercurial repository kan vinden die we willen converteren, en de -A vlag vertelt het waar het auteur-mapping bestand te vinden is. Het script verwerkt Mercurial changesets en converteert ze in een script voor de "fast-import" functie (die we iets later zullen bespreken). Dit duurt even (al is het veel sneller dan het via het netwerk zou zijn), en de uitvoer is nogal breedsprakig:

$ /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

Meer valt er eigenlijk niet over te vertellen. Alle Mercurial tags zijn geconverteerd naar Git tags, en Mercurial branches en boekleggers (bookmarks) zijn geconverteerd naar Git branches. Nu ben je klaar om de repository naar zijn nieuwe server-thuis te sturen:

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

Bazaar

Bazaar is een DVCS tool die veel op Git lijkt, en als gevolg is het redelijk probleemloos om een Bazaar repository in een van Git te converteren. Om dit voor elkaar te krijgen, moet je de bzr-fastimport-plugin importeren.

De bzr-fastimport plugin verkrijgen

De procedure om de fastimport plugin te installeren verschilt op een UNIX-achtige besturingssysteem en op Windows. In het eerste geval, is het het eenvoudigste om het bzr-fastimport-pakket te installeren, en dat zal alle benodigde afhankelijkheden installeren.

Bijvoorbeeld, met Debian en afgeleiden, zou je het volgende doen:

$ sudo apt-get install bzr-fastimport

Met RHEL, zou je het volgende doen:

$ sudo yum install bzr-fastimport

Met Fedora, vanaf release 22, is dnf de nieuwe package manager:

$ sudo dnf install bzr-fastimport

Als het pakket niet beschikbaar is, kan je het als een plugin installeren:

$ mkdir --parents ~/.bazaar/plugins     # maakt de benodigde folders voor de plugins
$ cd ~/.bazaar/plugins
$ bzr branch lp:bzr-fastimport fastimport   # iumporteert de fastimport plugin
$ cd fastimport
$ sudo python setup.py install --record=files.txt   # installeert de plugin

Om deze plugin te laten werken, heb je ook de fastimport Python module nodig. Je kunt controleren of deze aanwezig is of niet en het installeren met de volgende commando’s:

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

Als het niet beschikbaar is, kan je hte downloaden van het adres https://pypi.python.org/pypi/fastimport/.

In het tweede geval (op Windows), wordt bzr-fastimport automatisch geinstalleerd met de standalone versie en de standaard installatie (laat alle checkboxen aangevinkt). Dus in dit geval hoef je niets te doen.

Vanaf dat moment, is de manier waarop je een Bazaar repository afhankelijk van of je een enkele branch hebt, of dat je op een repository werkt die meerdere branches heeft.

Project met een enkele branch

Ga met cd in de directory die jouw Bazaar repository bevat en initialiseer de Git repository:

$ cd /path/to/the/bzr/repository
$ git init

Nu kan je eenvoudigweg je Bazaar repository exporteren en converteren in een Git repository met het volgende commando:

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

Afhankelijk van de grootte van het project, wordt jouw Git repository gebouwd in een periode varierend van een paar seconden tot een paar minuten.

Het geval van een project met een hoofd-branch en een werk-branch

Je kunt ook een Bazaar repository importeren dat branches bevat. Laten we aannemen dat je twee branches hebt: een vertegenwoordigt de hoofd-branch (myProject.trunk), de andere is de werk-branch (myProject.work).

$ ls
myProject.trunk myProject.work

Maak de Git repository en ga er met cd erheen:

$ git init git-repo
$ cd git-repo

Pull de master-branch in git:

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

Pull de werk-branch in Git:

$ bzr fast-export --marks=../marks.bzr --git-branch=work ../myProject.work | \
git fast-import --import-marks=../marks.git --export-marks=../marks.git

git branch zal je nu de master-branch alsook de work-branch laten zien. Controleer de logs om je ervan te vergewissen dat ze volledig zijn en verwijder de marks.bzr en de marks.git bestanden.

De staging area synchroniseren

Onafhankelijk van het aantal branches die je had en de import-methode die je gebruikt hebt, is je staging area niet gesynchroniseerd met HEAD, en met het importeren van verschillende branches, is je werk-directory ook niet gesynchroniseerd. Deze situatie is eenvoudig op te lossen met het volgende commando:

$ git reset --hard HEAD

Het negeren van de bestanden die met .bzrignore werden genegeerd

Laten we nu eens kijken naar de te negeren bestanden. Het eerste wat we moeten doen is .bzrignore naar .gitignore hernoemen. Als het .bzrignore bestand een of meerdere regels bevat die beginnen met "!!" of "RE:", zal je deze moetten wijzigen en misschien verscheidene .gitignore-bestanden maken om precies dezelfde bestanden te negerern die Bazaar ook negeerde.

Uiteindelijk zal je een commit moeten maken die deze wijziging voor de migratie bevat:

$ git mv .bzrignore .gitignore
$ # modify .gitignore if needed
$ git commit -am 'Migration from Bazaar to Git'

Jouw repository naar de server sturen

We zijn er! Je kunt nu de repository naar zijn nieuwe thuis-server pushen:

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

Je Git repository is klaar voor gebruik.

Perforce

Het volgende systeem waar we naar gaan kijken is het importeren uit Perforce. Zoals hierboven besproken, zijn er twee manieren om Git en Perforce met elkaar te laten praten: git-p4 en Perforce Git Fusion.

Perforce Git Fusion

Met Git Fusion verloopt dit proces vrijwel pijnloos. Configureer alleen je project settings, user mappings en branches met behulp van een configuratie bestand (zoals behandeld in Git Fusion), en clone de repository. Git Fusion geeft je iets wat eruit ziet als een echte Git repository, die dan klaar is om naar een reguliere Git host te pushen als je dat wilt. Je kunt zelfs Perforce als je Git host gebruiken als je dat wilt.

Git-p4

Git-p4 kan ook als een importeer gereedschap werken. Als voorbeeld zullen we het Jam project importeren van de Perforce Public Depot. Om je werkstation in te richten, moet je de P4PORT omgevingsvariabele exporteren zodat deze wijst naar het Perforce depot:

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

Om dit mee te kunnen doen, moet je een Perforce depot hebben om mee te verbinden. Wij zullen het publieke depot op public.perforce.com gebruiken in onze voorbeelden, maar je kunt elk depot waar jetoegang toe hebt gebruiken.

Roep het git p4 clone commando aan om het Jam project van de Perforce server te importeren, waarbij je het pad van het depot en het project en het pad waar je het project in wilt importeren meegeeft:

$ 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%)

Dit specifieke project heeft maar een branch, maar als je branches hebt die geconfigureerd zijn met branch views (of alleen een set directories), kan je de --detect-branches vlag gebruiken bij git p4 clone om alle branches van het project ook te importeren. Zie Branchen voor wat meer diepgang op dit onderwerp.

Op dit punt ben je bijna klaar. Als je naar de p4import directory gaat en git log aanroept, kan je je geïmporteerde werk zien:

$ 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]

Je kunt zien dat git-p4 een identificerend element heeft achtergelaten in elk commit-bericht. Je kunt dit element prima daar laten, in het geval dat je in de toekomst naar het Perforce change nummer moet refereren. Echter, als je dit element wilt weghalen, is dit het moment om het te doen - voordat je begint te werken met de nieuwe repository. Je kunt git filter-branch gebruiken om de element-tekenreeksen en masse te verwijderen:

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

Als je git log aanroept, kan je zien dat alle SHA-1 checksums voor de commits zijn gewijzigd, maar de git-p4 tekenreeksen staan niet meer in de commit-berichten:

$ 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).

Je import is klaar om te worden gepusht naar je nieuwe Git server.

TFS

Als je team haar versiebeheer uit TFVC naar Git gaat converteren, wil je de hoogst-betrouwbare conversie gebruiken die je maar kunt krijgen. Dit houdt in dat, hoewel we zowel git-tfs als git-tf in de samenwerkings-paragraaf hebben behandeld, zullen we hier alleen git-tfs behandelen, omdat git-tfs branches ondersteunt, en deze beperking het vrijwel onmogelijk maakt om git-tf hiervoor te gebruiken.

Noot

Dit is een eenrichtings conversie. De Git repository die hier wordt aangemaakt kan geen verbinding meer leggen met het oorspronkelijk TFVC project.

Het eerste om te doen is gebruikersnamen mappen. TFVC is nogal ruimdenkend met wat er in het auteur veld gaat voor changesets, maar Git wil een voor de mens leesbare naam en email adres hebben. Je kunt deze informatie als volgt van het tf commando-regel client krijgen:

PS> tf history $/myproject -recursive > AUTHORS_TMP

Dit pakt alle changesets in de geschiedenis van het project en zet dit in het AUTHORS_TMP bestand die we verder gaan verwerken om de gegevens uit het User kolom (de tweede) te halen. Open het bestand en bekijk op welke karakter de kolom begint en eindigt en vervang, in de volgende commando-regel, de parameters 11-20 van het cut commando met de waarden die jij gevonden hebt:

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

Het cut commando behoudt alleen de karakters tussen 11 en 20 van elke regel. Het tail commando slaat de eerste twee regels over, die kolom-koppen en ASCII-art onderstrepingen zijn. Het resultaat van dit alles wordt aan sort en uniq doorgegeven om duplicaten te verwijderen en bewaard in een bestand genaamd AUTHORS. De volgende stap is handmatig; om git-tfs van dit bestand gebruik te laten maken, moet elke regel in dit formaat staan:

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

Het gedeelte links is het “User” veld van TFVC, en het gedeelte rechts van het gelijk-teken is de gebruikersnaam die voor Git commits gaat worden gebruikt.

Als je dit bestand eenmaal hebt, is de volgende stap om te nemen een volledige kloon van het TFVC project waar je in bent geïnteresseerd te maken:

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

Vervolgens wil je de git-tfs-id gedeeltes aan het eind van de commit-berichten opschonen. Het volgende commando gaat dit doen:

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

Dit gebruikt het sed commando van de Git-bash omgeving om elke regel die begint met “git-tfs-id:” met leegte te vervangen, dit Git vervolgens dan zal negeren.

Als dit eenmaal gedaan is, ben je klaar om een nieuwe remote toe te voegen, al je branches te pushen, en je team is klaar om met Git te gaan werken.

Importeren op maat

Als jouw systeem niet een van de bovenstaande is, moet je op het internet gaan zoeken naar een importeerder - goede importeerders zijn beschikbaar voor vele andere systemen, inclusief CVS, Clear Case, Visual Source Safe en zelfs een directory met archieven. Als geen van die tools voor jou geschikt zijn, je hebt een heel obscure tool, of je hebt om een andere reden een meer aangepaste importeer proces nodig, dan moet je git fast-import gebruiken. Dit commando leest eenvoudige instructies van stdin om specifieke Git gegevens te schrijven. Het is veel eenvoudiger om op deze manier Git objecten aan te maken dan de rauwe Git commando’s te gebruiken of om zelf de rauwe objecten te schrijven (zie Git Binnenwerk voor meer informatie). Op deze manier kan je een import script schrijven die de benodigde informatie uit het te importeren systeem leest en op stdout eenvoudige instructies afdrukt. Je kunt dit programma dan aanroepen en de uitvoer doorgeven aan git fast-import met een pipe-instructie.

Om een snelle demonstratie te geven, ga je een eenvoudige importeerder schrijven. Stel dat je in current aan het werk bent, je maakt op gezette tijden een backup van je project door de directory naar een andere te kopieren met een datum-tijd indicatie genaamd back_YYYY_MM_DD, en je wilt dit importeren in Git. Je directory-structuur ziet er zo uit:

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Om een Git directory te importeren, moet je weten hoe Git haar gegevens opslaat. Zoals je je zult herinneren, is Git in de basis een geschakelde lijst van commit objecten die verwijzen naar een snapshot van de inhoud. Alles wat je hoeft te doen is fast-import te vertellen wat de snapshots van de inhoud zijn, welke commit-gegevens hiernaar verwijzen en de volgorde waarin ze staan. Je strategie zal zijn om een voor een door de snapshots te gaan en commits te maken met de inhoud van elke directory, en elke commit terug te laten verwijzen naar de vorige.

Zoals we in Een voorbeeld van Git-afgedwongen beleid gedaan hebben, schrijven we dit in Ruby, omdat dit is waar we gewoonlijk mee werken en het redelijk eenvoudig te lezen is. Je kunt dit voorbeeld redelijk eenvoudig in iets schrijven waar je bekend mee bent - het moet gewoon de juiste informatie naar stdout uitvoeren. En als je op Windows draait, betekent dit dat je ervoor moet zorgen dat je geen carriage returns aan het eind van je regels gebruikt - git fast-import is erg secuur in het alleen accepteren van line feeds (LF) niet de carriage return line feeds (CFLF) die door Windows wordt gebruikt.

Om te beginnen, spring je in de doel directory en identificeert elke subdirectory, die elk een snapshot is van wat je wilt importeren als een commit. Je springt in elke subdirectory en drukt de commando’s af die nodig zijn om het te exporteren. Je hoofdlus ziet er zo uit:

last_mark = nil

# loop through the directories
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

Je roept print_export in elke directory aan, die de inhoud en kenmerk (mark) van de vorige snapshot neemt en je de inhoud en kenmerk van deze teruggeeft; op die manier kan je ze goed koppelen.

“Mark” is de term die fast-import gebruikt voor een identificatie die je aan een commit geeft; tijdens het aanmaken van commit geef je elk een kenmerk die je kunt gebruiken om als koppeling vanaf andere commits. Dus, het eerste wat je moet doen in je print_export methode is een kenmerk genereren van de naam van de directory:

mark = convert_dir_to_mark(dir)

Je doet dit door een reeks (array) directories te maken en de indexwaarde als het kenmerk te gebruiken, omdat een kenmerk een integer dient te zijn. Je methode ziet er zo uit:

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

Nu je een integer representatie hebt van je commit, heb je een datum nodig voor de commit metadata. Omdat de datum in de naam van de directory zit, ga je het eruit halen. De volgende regel in je print_export bestand is:

date = convert_dir_to_date(dir)

where convert_dir_to_date is defined as:

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

Dat retourneert een integerwaarde voor de datum van elke directory. Het laatste stukje meta-informatie dat je nodig hebt voor elke commit zijn de gegevens van de committer, die je hardgecodeerd hebt in een globale variabele:

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

Nu ben je klaar om de commit data af te drukken voor je importeerder. De initiële informatie geeft aan dat je een commit object definieert en op welke branch deze staat, gevolgd door het kenmerk die je hebt gegenereert, de informatie van de committer en het commit bericht, en dan de vorige commit als die er is. De code ziet er zo uit:

# print the import information
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

Je geeft de tijdzone (-0700) hardgecodeerd, omdat dit nu eenmaal makkelijk is. Als je vanuit een ander systeem importeert, moet je de tijdzone als een relatieve verschuiving (offset) weergeven. Het commit bericht moet worden uitgedrukt in een speciaal formaat:

data (size)\n(contents)

Het formaat bestaat uit het woord data, de grootte van de te lezen gegevens, een nieuwe regel en tot slot de gegevens. Omdat je hetzelfde formaat later nodig hebt om de bestandsinhoud te specificeren, maak je een hulpmethode, export_data:

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

Wat er nu nog overblijft is het specifieren van de bestandsinhoud voor elk snapshot. Dit is makkelijk, omdat je elk van deze in een directory hebt - je kunt het deleteall commando afdrukken gevolgd door de inhoud van elk bestand in de directory. Git zal dan elke snapshot op de juiste manier opslaan:

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

Let op: Omdat veel systemen hun revisies zien als wijzigingen van de ene commit naar de ander, kan fast-import ook commando’s accepteren waar bij elke commit kan worden aangegeven welke bestanden er zijn toegevoegd, verwijderd of gewijzigd en welke nieuwe elementen erbij zijn gekomen. Je kunt de verschillen tussen de snapshots berekenen en alleen deze gegevens meegeven, maar het is veel ingewikkelder om dit te doen - je kunt net zo makkelijk Git alle gegevens meegeven en het hem laten uitzoeken. Als dit beter past bij jouw gegevens, neem dan de fast-import man page door voor de details hoe je deze gegevens moet doorgeven in dat geval.

Het formaat om de nieuwe bestandsinhoud weer te geven of om een gewijzigd bestand te specificeren met de nieuwe inhoud is als volgt:

M 644 inline path/to/file
data (size)
(file contents)

Hier is 644 de mode (als je aanroepbare bestanden hebt, moet je dit detecteren en in plaats daarvan 755 opgeven), en in de code (inline) geef je de inhoud direct achter deze regel weer. Je inline_data methode ziet er zo uit:

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

Je hergebruikt de export_data methode die je eerder hebt gedefinieerd, omdat dit formaat hetzelfde is als waarmee je de commit bericht gegevens specificeert.

Het laatste wat je nog moet doen is om het huidige kenmerk te retourneren, zodat het kan worden doorgegeven in de volgende iteratie:

return mark
Noot

Als je op Windows draait moet je je ervan verzekeren dat je een extra stap toevoegt. Zoals eerder opgemerkt, gebruikt Windows CRLF voor nieuwe regel tekens waar git fast-import alleen LF verwacht. Om dit probleem te verhelpen en git fast-import blij te maken, moet je ruby vertellen om LF te gebruiken in plaats van CRLF:

$stdout.binmode

Dat is 't. Hier is het script in zijn totaliteit:

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

# Loop through the directories
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

Als je dit script aanroept, krijg je inhoud dat er ongeveer zo uit ziet:

$ 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
(...)

Om de importeerder te laten draaien, geeft je de uitvoer met een pipe door aan git fast-import terwjil je in de Git directory staat waar je naartoe wilt importeren. Je kunt een nieuwe directory aanmaken en daarna git init daarin aanroepen als een begin, en daarna je script aanroepen:

$ 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
---------------------------------------------------------------------

Zoals je kunt zien, als het succesvol eindigt, geeft het je een bergje statistieken wat het allemaal heeft bereikt. In dit geval, heb je in totaal 13 objecten geïmporteerd voor 4 commits in 1 branch. Je kunt nu git log aanroepen om je nieuwe historie te bekijken:

$ 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

Kijk eens aan - een mooie, schone Git repository. Het is belangrijk om op te merken dat er niets is uitgecheckt - je hebt initieel nog geen enkel bestand in je werk directory. Om deze te krijgen, moet je je branch resetten tot waar master nu is:

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

Je kunt nog veel meer doen met het fast-import gereedschap - verschillende modes behandelen, binaire gegevens, meerdere branches en mergen, tags, voortgangs-indicators en meer. Een aantal voorbeelden van meer ingewikkelde scenarios zijn beschikbaar in de contrib/fast-import directory van de Git broncode.

scroll-to-top