Naar Git migreren
Als je een bestaande hoeveelheid broncode in een ander VCS hebt, maar je hebt besloten om Git te gaan gebruiken dan moet je je project op een of andere manier migreren. Deze sectie behandelt een aantal importeerders voor veel voorkomende systemen, die bij Git zitten en demonstreert daarna hoe je je eigen importeerder kunt ontwikkelen.
Importeren
Je zult leren hoe je data uit twee van de grotere professioneel gebruikte SCM systemen kunt importeren – Subversion en Perforce – omdat zij op dit moment de grootste hoeveelheid gebruikers hebben waarvan ik op dit moment hoor dat ze willen wisselen, en omdat er tools van hoge kwaliteit voor beide systemen meegeleverd worden met Git.
Subversion
Als je de vorige sectie over het gebruik van git svn
leest, kun je die instructies eenvoudig gebruiken om een git svn clone
te doen op een repository; daarna stop je met het gebruik van de Subversion server, pushed naar de nieuwe Git server, en ga die gebruiken. Als je de historie wil hebben, kun je dat zo snel als dat je van de server kunt pullen voor elkaar krijgen (wat een tijdje kan duren).
Maar, de import is niet perfect; en omdat het zo lang zal duren, kun je het maar beter goed doen. Het eerste probleem is informatie over de auteurs. In Subversion heeft iedere persoon die commit een gebruikersaccount op het systeem, wat wordt opgenomen in de commit informatie. De voorbeelden in de voorgaande sectie toen schacon
op bepaalde plaatsen zoals de blame
output en bij git svn log
. Als je dit beter wil transleren naar Git auteur data, dan heb je een translatie nodig van de Subversion gebruikers naar de Git auteurs. Maak een bestand genaamd users.txt
, die deze translatie in dit formaat heeft:
schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>
Om een lijst te krijgen van de auteurnamen, die SVN gebruikt kun je dit uitvoeren:
$ svn log --xml | grep author | sort -u | perl -pe 's/.>(.?)<./$1 = /'
Daarmee krijg je de log output in XML formaat – je kunt hierin zoeken naar de auteurs, een lijst met unieke vermeldingen creëren en dan de XML eruit halen. (Dit werkt natuurlijk alleen op een machine waarop grep
, sort
en Perl
geïnstalleerd is.) Daarna stuur je die output naar je users.txt bestand zodat je de gelijkwaardige Git gebruiker data naast iedere vermelding kunt zetten.
Je kunt dit bestand meegeven aan git svn
om het te helpen de auteur data accurater te transleren. Je kunt git svn
ook vertellen dat het niet de metadata moet toevoegen die Subversion normaal importeert, door de --no-metadata
optie mee te geven aan het clone
of init
commando. Dit laat je import
commando er zo uit zien:
$ git-svn clone http://my-project.googlecode.com/svn/ \
--authors-file=users.txt --no-metadata -s my_project
Nu zou je een betere Subversion import moeten hebben in je my_project
map. In plaats van commits die er zo uit zien
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 het auteur veld ziet er beter uit, maar het git-svn-id
is ook niet meer aanwezig.
Je moet nog een beetje post-import
opruiming doen. Je moet nog de vreemde referenties die git svn
ingesteld heeft opruimen, bijvoorbeeld. Als eerste verplaats je de tags zodat het echte tags worden, in plaats van vreemde remote branches, en dan verplaats je de rest van de branches zodat ze lokaal worden.
Om de tags goede Git tags te laten worden, voer dit uit
$ cp -Rf .git/refs/remotes/tags/* .git/refs/tags/
$ rm -Rf .git/refs/remotes/tags
Dit neemt de referenties die remote branches waren en met tag/
begonnen, en maakt er echte (lichtgewicht) tags van.
Daarna verplaats je de rest van de referenties onder refs/remotes
zodat het lokale branches worden:
$ cp -Rf .git/refs/remotes/* .git/refs/heads/
$ rm -Rf .git/refs/remotes
Nu zijn alle oude branches echte Git branches, en alle oude tags echte Git tags. Het laatste ding dan je moet doen is je nieuwe Git server als een remote toevoegen en er naar pushen. Omdat je al jouw branches en tags omhoog wil laten gaan, kun je dit uitvoeren:
$ git push origin --all
Al je branches en tags zouden op je Git server moeten staan in een fijne schone import.
Perforce
Het volgende systeem waar je naar gaat kijken om vanuit te importeren is Perforce. Er zit een Perforce importeerder bij de Git distributie, maar alleen in het contrib
gedeelte van de broncode – het is niet standaard beschikbaar zoals git svn
. Om het uit te voeren, moet je de Git broncode pakken, die je van git.kernel.org kunt downloaden:
$ git clone git://git.kernel.org/pub/scm/git/git.git
$ cd git/contrib/fast-import
In deze fast-import
map, zou je een uitvoerbaar Python script genaamd git-p4
moeten vinden. Je moet Python en het p4
tool geïnstalleerd hebben op je machine om deze import te laten werken. Als voorbeeld ga je het Jam project van de Perforce Public Depot importeren. Om je gebruiker in te stellen, moet je de P$PORT omgevingsvariabele laten wijzen naar het Perforce depot:
$ export P4PORT=public.perforce.com:1666
Voer het git-p4-clone
commando uit om het Jam project van de Perforce server te importeren, waarbij je het pad naar het depot en het project en het pad waarnaar je wilt importeren mee geeft:
$ git-p4 clone //public/jam/src@all /opt/p4import
Importing from //public/jam/src@all into /opt/p4import
Reinitialized existing Git repository in /opt/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 4409 (100%)
Als je naar de /opt/p4import
map gaat en git log
uitvoert, kun je je geïmporteerde werk zien:
$ git log -2
commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2
Author: Perforce staff <support@perforce.com>
Date: Thu Aug 19 10:18:45 2004 -0800
Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into
the main part of the document. Built new tar/zip balls.
Only 16 months later.
[git-p4: depot-paths = "//public/jam/src/": change = 4409]
commit ca8870db541a23ed867f38847eda65bf4363371d
Author: Richard Geiger <rmg@perforce.com>
Date: Tue Apr 22 20:51:34 2003 -0800
Update derived jamgram.c
[git-p4: depot-paths = "//public/jam/src/": change = 3108]
Je kunt de git-p4
identificator in iedere commit zien. Het is fijn om die identificator daar te bewaren, voor het geval je later naar het Perforce wijzigingsnummer moet refereren. Maar, als je de identificator wilt verwijderen, is nu het geschikte moment om dat te doen – voordat je begint met werken op de nieuwe repository. Je kunt git filter-branch
gebruiken om de identificatie tekst en masse te verwijderen:
$ git filter-branch --msg-filter '
sed -e "/^\[git-p4:/d"
'
Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123)
Ref 'refs/heads/master' was rewritten
Als je git log
uitvoert, kun je zien dat alle SHA-1 checksums voor de commits gewijzigd zijn, maar de git-p4
teksten staan niet langer in de commit berichten:
$ git log -2
commit 10a16d60cffca14d454a15c6164378f4082bc5b0
Author: Perforce staff <support@perforce.com>
Date: Thu Aug 19 10:18:45 2004 -0800
Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into
the main part of the document. Built new tar/zip balls.
Only 16 months later.
commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2
Author: Richard Geiger <rmg@perforce.com>
Date: Tue Apr 22 20:51:34 2003 -0800
Update derived jamgram.c
Je import is nu klaar om naar je nieuwe Git server gepushed te worden.
Een eigen importeerder
Als het door jou gebruikte systeem niet Subversion of Perforce is, zou je online voor een importeerder moeten zoeken – er zijn importeerders van goede kwaliteit beschikbaar voor CVS, Clear Case, Visual Source Safe, en zelfs een map met archieven. Als geen van deze tools voor jou geschikt is, je hebt een zeldzamer tool, of je hebt om een andere reden een eigen import proces nodig, dan zou je git fast-import
moeten gebruiken. Dit commando leest eenvoudige instructies van stdin om specifieke Git data te schrijven. Het is veel eenvoudiger om op deze manier Git objecten te maken, dan de rauwe Git commando’s uit te voeren, of om te proberen de rauwe objecten te schrijven (zie hoofdstuk 9 voor meer informatie). Op deze manier kun je een import script schrijven dat de noodzakelijke data uit het systeem leest dat je aan het importeren bent en rechttoe rechtaan instructies op stdout afdrukt. Je kunt dit programma dan uitvoeren en de output door git fast-import
sluizen.
Voor een snelle demonstratie zul je een eenvoudige importeerde schrijven. Stel dan je in current werkt, waarbij je je project eens in de zoveel tijd backup’ed door de map te kopiëren naar een backup map die gelabeld is met de tijd back_YYYY_MM_DD
, en je wil dit in Git importeren. Je mappenstructuur ziet er zo uit:
$ ls /opt/import_from
back_2009_01_02
back_2009_01_04
back_2009_01_14
back_2009_02_03
current
Om naar een Git map te importeren, moet je bekijken hoe Git zijn data opslaat. Je kunt je misschien herinneren dat Git in fundament een gelinkte lijst is met commit objecten die naar een snapshot van inhoud wijzen. Het enige dat je hoeft te doen, is fast-import
vertellen wat de inhoud snapshots zijn, welke commit data er naar wijst en de volgorde waarin ze moeten staan. Je strategie bestaat uit het doorlopen van de snapshots en commits te creëren met de inhoud van iedere map, waarbij je iedere commit terug linkt met de vorige.
Zoals je dat gedaan hebt in de “Een Voorbeeld van Git Afgedwongen Beleid” sectie van Hoofdstuk 7, gaan we dit in Ruby schrijven, omdat het is waar ik over het algemeen mee werk en het neigt eenvoudig te lezen te zijn. Je kunt dit voorbeeld vrij eenvoudig schrijven in alles waar je bekend mee bent – het hoeft alleen de juiste informatie naar stdout te schrijven. En dat betekent dat als je op Windows werkt, je erg voorzichtig moet zijn om geen carriage returns te introduceren aan het einde van je regels – git fast-import is erg kieskeurig in de manier waarop hij slechts line feeds (LF) wil hebben en niet de cariage return line feeds (CRLF), die Windows gebruikt.
Om te beginnen ga je naar de doelmap en identificeer je iedere submap, waarvan elk een snapshot is dat je als commit wil importeren. Je zult in iedere submap gaan en de noodzakelijke commando’s printen om ze te exporteren. Je basis 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 voert print_export
uit binnen iedere map, wat het manifest en het merk van de vorige snapshot neemt, en het manifest en merk van deze terug geeft; op die manier kun je ze goed linken. “Mark” is de fast-import
term voor een identificator die je aan een commit geeft; terwijl je commits maakt, geef je ze een merk die je kunt gebruiken om vanuit andere commits naar te linken. Dus, het eerste wat je moet doen in je print_export
functie is een merk genereren van de mapnaam:
mark = convert_dir_to_mark(dir)
Je zult dit doen door een lijst van mappen te creëren en de index waarde als merk te gebruiken, omdat een merk een geheel getal moet zijn. Je functie 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 dat je een geheel getal hebt als voorstelling van je commit, moet je een datum hebben voor de commit metadata. Omdat de datum is uitgedrukt in de naam van de map, zul je het daar uit moeten halen. De volgende regel in de print_export
bestand is
date = convert_dir_to_date(dir)
waarbij convert_dir_to_date
als volgt gedefinieerd is
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 geeft een geheel getal terug als waarde voor de datum van iedere map. Het laatste stukje meta-informatie dat je voor iedere commit nodig hebt is de committer data, wat je in een globale variabele stopt:
$author = 'Scott Chacon <schacon@example.com>'
Nu ben je klaar om te beginnen met de commit data af te drukken voor je importeerder. De initiële informatie stelt dat je een commit object definieert en op welke branch het zit, gevolgd door het merk dat je gegenereerd hebt, de committer informatie en het commit bericht, en 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 stelt de tijdzone (-0700) hardgecodeerd in omdat dat eenvoudig is. Als je vanuit een ander systeem importeert, moet je de tijdzone als een compensatiewaarde specificeren. Het commit bericht moet uitgedrukt worden in een speciaal formaat:
data (size)\n(contents)
Het formaat bestaat uit de woorddata, de grootte van de data die gelezen moet worden, een newline, en als laatste de data. Omdat je hetzelfde formaat nodig hebt om later de bestandsinhoud te specificeren, zul je een hulpfunctie creëren, export_data
:
def export_data(string)
print "data #{string.size}\n#{string}"
end
Het enige dat nog gespecificeerd moet worden is de bestandsinhoud voor ieder snapshot. Dit is makkelijk, omdat je ze allemaal in een map hebt – je kunt het deleteall
commando afdruken, gevolgd door de inhoud van ieder bestand in de map. Git zal dan ieder 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 over hun revisie als veranderingen van de ene naar de andere commit denken, kan fast-import ook commando’s aan waarbij bij iedere commit is gespecificeerd welke bestanden zijn toegevoegd, verwijderd, of aangepast en wat de nieuwe inhouden zijn. Je kunt de verschillen tussen snapshots berekenen en alleen deze data geven, maar om het zo te doen is complexer – je kunt net zo goed Git alle data geven en hem het uit laten zoeken. Als dit beter geschikt is voor jouw data, bekijk dan de fast-import
man pagina voor details over hoe je jouw data op deze manier kunt aanleveren.
Het formaat om de nieuwe bestandsinhoud te tonen of een aangepast bestand met de nieuwe inhoud te specificeren is als volgt:
M 644 inline path/to/file
data (size)
(file contents)
Hierbij is 644 de modus (als je uitvoerbare bestanden hebt, moet je dit detecteren en in plaats daarvan 755 specificeren), en inline verteld dat je de inhoud onmiddellijk na deze regel toont. Je inline_data
functie 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
data functie, die je eerder gedefinieerd hebt, omdat het hetzelfde is als de manier waarop je je commit bericht data gespecificeerd hebt.
Het laatste wat je moet doen is het huidige merk teruggeven, zodat het meegegeven kan worden aan de volgende iteratie:
return mark
Let op: Als je op Windows werkt, moet je er zeker van zijn dat je nog één extra stap toevoegt. Zoals voorheen gemeld is, gebruikt Windows CRLF als new line karakters, terwijl git fast-import alleen LF verwacht. Om om dit probleem heen te werken en git fast-import blij te maken, moet je ruby vertellen om LF in plaats van CRLF te gebruiken:
$stdout.binmode
Dat is alles. Als je dit script uitvoert, zul je inhoud krijgen die er ongeveer zo uit ziet:
$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer Scott Chacon <schacon@geemail.com> 1230883200 -0700
data 29
imported from back_2009_01_02deleteall
M 644 inline file.rb
data 12
version two
commit refs/heads/master
mark :2
committer Scott Chacon <schacon@geemail.com> 1231056000 -0700
data 29
imported from back_2009_01_04from :1
deleteall
M 644 inline file.rb
data 14
version three
M 644 inline new.rb
data 16
new version one
(...)
Om de importeerder uit te voeren, sluis je deze uitvoer door git fast-import
terwijl je in de Git map staat waar je naar toe wilt importeren. Je kunt een nieuwe map aanmaken en er dan git init
in uitvoeren als startpunt, en dan kun je je script uitvoeren:
$ 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: 18 ( 1 duplicates )
blobs : 7 ( 1 duplicates 0 deltas)
trees : 6 ( 0 duplicates 1 deltas)
commits: 5 ( 0 duplicates 0 deltas)
tags : 0 ( 0 duplicates 0 deltas)
Total branches: 1 ( 1 loads )
marks: 1024 ( 5 unique )
atoms: 3
Memory total: 2255 KiB
pools: 2098 KiB
objects: 156 KiB
---------------------------------------------------------------------
pack_report: getpagesize() = 4096
pack_report: core.packedGitWindowSize = 33554432
pack_report: core.packedGitLimit = 268435456
pack_report: pack_used_ctr = 9
pack_report: pack_mmap_calls = 5
pack_report: pack_open_windows = 1 / 1
pack_report: pack_mapped = 1356 / 1356
---------------------------------------------------------------------
Zoals je kunt zien geeft het je een berg statistieken over wat het heeft bereikt, als het succesvol heeft kunnen afronden. In dit geval heb je in totaal 18 objecten geïmporteerd, voor 5 commits in 1 branch. Nu kun je git log
uitvoeren en je nieuwe historie zien:
$ git log -2
commit 10bfe7d22ce15ee25b60a824c8982157ca593d41
Author: Scott Chacon <schacon@example.com>
Date: Sun May 3 12:57:39 2009 -0700
imported from current
commit 7e519590de754d079dd73b44d695a42c9d2df452
Author: Scott Chacon <schacon@example.com>
Date: Tue Feb 3 01:00:00 2009 -0700
imported from back_2009_02_03
Hier heb je ‘t – een fijne, schone Git repository. Het is belangrijk om te zien dat nog niks uitgechecked is – je hebt om te beginnen geen bestanden in je werkmap. Om ze te krijgen moet je je branch resetten tot het punt waar master
nu is:
$ ls
$ git reset --hard master
HEAD is now at 10bfe7d imported from current
$ ls
file.rb lib
Je kunt nog veel meer doen met het fast-import
tool – verschillende bestandsmodi behandelen, binaire gegevens, meerdere branches en merges, tags, voortgangsindicatoren, en meer. Een aantal voorbeelden voor complexe scenario’s zijn voorhanden in de contrib/fast-import
map van de Git broncode; een van de betere is het git-p4
script dat ik zojuist behandeld heb.