Git 🌙
Chapters ▾ 2nd Edition

9.2 Git und andere VCS-Systeme - Migration zu Git

Migration zu Git

Wenn du eine bestehende Quelltext-Basis in einem anderen VCS hast, aber dich für die Verwendung von Git entschieden hast, musst du dein Projekt auf die eine oder andere Weise migrieren. Dieser Abschnitt geht auf einige Importfunktionen für gängige Systeme ein und zeigt anschließend, wie du deinen eigenen benutzerdefinierten Importeur entwickeln kannst. Du lernst, wie man Daten aus einigen der größeren, professionell genutzten SCM-Systeme importierst. Sie werden von der Mehrheit der Benutzer, die wechseln wollen genutzt. Für diese Systeme sind oft hochwertige Migrations-Tools verfügbar.

Subversion

Wenn Du den vorherigen Abschnitt über die Verwendung von git svn gelesen hast, kannst du die Anweisungen zu git svn clone dazu benutzen, um ein Repository zu klonen. Anschliessend benötigst du den Subversion-Servers nicht mehr. Pushe alles zu einem neuen Git-Server und nutze dann nur noch diesen. Der Verlauf kann dabei aus dem Subversion-Server gezogen werden, was einige Zeit in Anspruch nehmen kann – abhängig von der Geschwindigkeit, mit der dein SVN-Server die Historie ausliefern kann.

Allerdings ist der Import nicht perfekt. Da er aber so lange dauert, kannst du es auch direkt richtig machen. Das erste Problem sind die Autoreninformationen. In Subversion hat jede Person, die einen Commit durchführt, auch einen Benutzer-Account auf dem System, der in den Commit-Informationen erfasst wird. Die Beispiele im vorherigen Abschnitt zeigen an einigen Stellen schacon, wie z.B. der blame Output und das git svn log. Wenn du diese auf bessere Git-Autorendaten abbilden möchtest, benötigst du eine Zuordnung der Subversion-Benutzer zu den Git-Autoren. Erstelle eine Datei mit Namen users.txt, die diese Zuordnung in folgendem Format vornimmt:

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

Um eine Liste der Autorennamen zu erhalten, die SVN verwendet, kannst du diesen Befehl ausführen:

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

Das erzeugt die Protokollausgabe im XML-Format, behält nur die Zeilen mit Autoreninformationen, verwirft Duplikate und entfernt die XML-Tags. Natürlich funktioniert das nur auf einem Computer, auf dem grep, sort und perl installiert sind. Leite diese Ausgabe dann in deine users.txt Datei um, damit du die entsprechenden Git-Benutzerdaten neben jedem Eintrag hinzufügen kannst.

Anmerkung

Wenn du dies auf einem Windows-Computer versuchen solltest, treten an dieser Stelle Probleme auf. Microsoft hat unter https://docs.microsoft.com/en-us/azure/devops/repos/git/perform-migration-from-svn-to-git einige gute Ratschläge und Beispiele bereitgestellt.

Du kannst diese Datei an git svn übergeben, um die Autorendaten genauer abzubilden. Außerdem kannst du git svn anweisen, die Metadaten, die Subversion normalerweise importiert, nicht zu berücksichtigen. Übergebe dazu --no-metadata an den clone oder init Befehl. Die Metadaten enthalten eine git-svn-id in jeder Commit-Nachricht, die Git während des Imports generiert. Dies kann dein Git-Log aufblähen und es möglicherweise etwas unübersichtlich machen.

Anmerkung

Du musst die Metadaten beibehalten, wenn du im Git-Repository vorgenommene Commits wieder in das ursprüngliche SVN-Repository spiegeln möchtest. Wenn du die Synchronisierung nicht in deinem Commit-Protokoll haben möchtest, kannst du den Parameter --no-metadata weglassen.

Dadurch sieht dein import Befehl so aus:

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

Nun solltest du einen passenderen Subversion-Import in deinem my_project Verzeichnis haben. Anstelle von Commits, die so aussehen:

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

sehen diese jetzt so aus:

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

    fixed install - go to trunk

Nicht nur das Autorenfeld sieht viel besser aus, auch die git-svn-id ist nicht mehr vorhanden.

Du solltest auch eine Bereinigung nach dem Import durchführen. Zum einen solltest du die seltsamen Referenzen bereinigen, die git svn eingerichtet hat. Verschiebe zuerst die Tags so, dass sie echte Tags und nicht merkwürdige Remote-Branches darstellen. Dann verschiebe den Rest der Branches auf lokale Branches.

Damit die Tags zu richtigen Git-Tags werden, führe folgenden Befehl aus:

$ 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

Dabei werden die Referenzen, die Remote-Branches waren und mit refs/remotes/tags/ begonnen haben zu richtigen (leichten) Tags gemacht.

Als nächstes verschiebe den Rest der Referenzen unter refs/remotes in lokale Branches:

$ 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

Es kann vorkommen, dass du einige zusätzliche Branches siehst, die durch @xxx ergänzt sind (wobei xxx eine Zahl ist), während du in Subversion nur einen Branch siehst. Es handelt sich hierbei um eine Subversion-Funktion mit der Bezeichnung „peg-revisions“, für die Git kein syntaktisches Gegenstück hat. Daher fügt git svn einfach die SVN-Versionsnummer zum Branch-Namen hinzu, genau so, wie du es in SVN geschrieben hättest, um die peg-Revision dieses Branchs anzusprechen. Wenn du dich nicht mehr um die peg-Revisionen sorgen willst, entferne diese einfach:

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

Jetzt sind alle alten Branches echte Git-Branches und alle alten Tags sind echte Git-Tags.

Da wäre noch eine letzte Sache zu klären. Leider erstellt git svn einen zusätzlichen Branch mit dem Namen trunk, der auf den Standard-Branch von Subversion gemappt wird, aber die trunk Referenz zeigt auf die gleiche Position wie master. Da master in Git eher idiomatisch ist, hier die Anleitung zum Entfernen des extra Branchs:

$ git branch -d trunk

Das Letzte, was du tun musst, ist deinem neuen Git-Server als Remote hinzuzufügen und zu ihm zu pushen. Hier ist ein Beispiel für das hinzufügen deines Servers als Remote:

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

Um alle deine Branches und Tags zu aktualisieren, kannst du jetzt diese Anweisungen ausführen:

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

Alle deine Branches und Tags sollten sich nun auf deinem neuen Git-Server in einem schönen, sauberen Import befinden.

Mercurial

Merkurial und Git haben ziemlich ähnliche Modelle für die Darstellung von Versionen. Außerdem ist Git etwas flexibler, so dass die Konvertierung eines Repositorys von Merkurial nach Git ziemlich einfach ist. Dazu wird ein Tool mit der Bezeichnung „hg-fast-export“ verwendet, dass du aus github beziehen kannst:

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

Der erste Schritt bei der Umstellung besteht darin, einen vollständigen Klon des zu konvertierenden Mercurial-Repository zu erhalten:

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

Der nächste Schritt ist die Erstellung einer Autor-Mapping-Datei. Mercurial ist etwas toleranter als Git für das, was es in das Autorenfeld für Changesets stellt. Das ist daher ein guter Zeitpunkt, um das ganze Projekt zu bereinigen. Das lässt sich mit einem einzeiligen Befehl in einer bash Shell erreichen:

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

Das dauert nur ein paar Sekunden, abhängig davon, wie umfangreich der Verlauf deines Projekts ist. Danach wird die Datei /tmp/authors in etwa so aussehen:

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 diesem Beispiel hat die gleiche Person (Bob) Changesets unter vier verschiedenen Namen erstellt, von denen einer tatsächlich korrekt aussieht und einer für einen Git-Commit völlig ungültig wäre. Mit hg-fast-export können wir das beheben. Jede Zeile wird in eine Regel umgewandelt: "<input>"="<output>", wobei ein <input> auf einen <output> abgebildet wird. Innerhalb der Zeichenketten <input> und <output> werden alle Escape-Sequenzen unterstützt, die von Python string_escape Encoding verstanden werden. Wenn die Autor-Mapping-Datei keinen passenden <input> enthält, wird dieser Autor unverändert an Git übergeben. Wenn alle Benutzernamen korrekt aussehen, werden wir diese Datei überhaupt nicht brauchen. In diesem Beispiel soll unsere Datei so aussehen:

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

Die gleiche Art von Mapping-Datei kann zum Umbenennen von Branches und Tags verwendet werden, wenn der Mercurial-Name in Git nicht zulässig ist.

Der nächste Schritt ist die Erstellung unseres neuen Git-Repository und das Ausführen des Exportskripts:

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

Das -r Flag informiert hg-fast-export darüber, wo das Mercurial-Repository zu finden ist, das wir konvertieren möchten. Das -A Flag sagt ihm, wo es die Autor-Mapping-Datei findet (Branch- und Tag-Mapping-Dateien werden jeweils durch die -B und -T Flags definiert). Das Skript analysiert Mercurial Change-Sets und konvertiert sie in ein Skript für Gits „fast-import“ Funktion (auf die wir später noch näher eingehen werden). Das dauert ein wenig (obwohl es viel schneller ist, als wenn es über das Netzwerk laufen würde). Der Output ist ziemlich umfangreich:

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

Das ist so ziemlich alles, was es dazu zu sagen gibt. Alle Mercurial-Tags wurden in Git-Tags umgewandelt, und Mercurial-Branches und -Lesezeichen wurden in Git-Branches umgewandelt. Jetzt kannst du das Repository in das neue serverseitige System pushen:

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

Perforce

Bei dem nächsten System, aus dem du importieren könntest, handelt es sich um Perforce. Wie bereits erwähnt, gibt es zwei Möglichkeiten, wie Git und Perforce miteinander kommunizieren können: git-p4 und Perforce Git Fusion.

Perforce Git Fusion

Git Fusion macht diesen Prozess relativ unkompliziert. Konfiguriere einfach deine Projekteinstellungen, Benutzerzuordnungen und Branches mit Hilfe einer Konfigurationsdatei (wie in Git Fusion beschrieben) und klone das Repository. Git Fusion bietet dir ein natives Git-Repository, mit dem du nach Belieben auf einen nativen Git-Host wechseln kannst. Du kannst Perforce sogar als deinen Git-Host verwenden, wenn du möchtest.

Git-p4

Git-p4 kann auch als Import-Tool fungieren. Als Beispiel werden wir das Jam-Projekt aus dem Perforce Public Depot importieren. Um deinen Client einzurichten, musst du die Umgebungsvariable P4PORT exportieren und auf das Perforce-Depot verweisen:

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

Zur weiteren Bearbeitung benötigst du ein Perforce-Depot, mit dem du dich verbinden kannst. Wir werden das öffentliche Depot unter public.perforce.com für unsere Beispiele verwenden. Du kannst aber jedes Depot nutzen, zu dem du Zugang hast.

Führe den Befehl git p4 clone aus, um das Jam-Projekt vom Perforce-Server zu importieren, wobei du das Depot, den Projektpfad und den Pfad angibst, in den du das Projekt importieren möchtest:

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

Dieses spezielle Projekt hat nur einen Branch, aber wenn du Branches hast, die mit Branch Views (oder nur einer Gruppe von Verzeichnissen) eingerichtet sind, kannst du ergänzend zum Befehl git p4 clone das Flag --detect-branches verwenden, um alle Branches des Projekts zu importieren. Siehe Branching für ein paar weitere Details.

Jetzt bist du fast fertig. Wenn du in das Verzeichnis p4import wechselst und git log ausführst, kannst du dein importiertes Projekt sehen:

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

Du kannst sehen, dass git-p4 in jeder Commit-Nachricht eine Kennung hinterlassen hat. Es ist gut, diese Kennung dort zu behalten, falls du später auf die Perforce-Änderungsnummer verweisen musst. Wenn du den Identifier jedoch entfernen möchtest, ist jetzt der richtige Zeitpunkt – bevor du mit der Arbeit am neuen Repository beginnst. Du kannst git filter-branch verwenden, um die Identifikations-Strings in großer Anzahl zu entfernen:

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

Wenn du git log ausführst, kannst du sehen, dass sich alle SHA-1-Prüfsummen für die Commits geändert haben, aber die git-p4 Zeichenketten sind nicht mehr in den Commit-Nachrichten enthalten:

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

Dein Import ist bereit, um ihn auf deinen neuen Git-Server zu pushen.

Benutzerdefinierter Import

Wenn dein System nicht zu den vorgenannten gehört, solltest du online nach einer Import-Schnittstelle suchen – hochwertige Importer sind für viele andere Systeme verfügbar, darunter CVS, Clear Case, Visual Source Safe, sogar für ein Verzeichnis von Archiven. Wenn keines dieser Tools für dich geeignet ist, du ein obskures Tool nutzt oder anderweitig einen benutzerdefinierten Importprozess benötigst, solltest du git fast-import verwenden. Dieser Befehl liest die einfachen Anweisungen von „stdin“ aus, um bestimmte Git-Daten zu schreiben. Es ist viel einfacher, Git-Objekte auf diese Weise zu erstellen, als die Git-Befehle manuell auszuführen oder zu versuchen, Raw-Objekte zu erstellen (siehe Kapitel 10, Git Interna für weitere Informationen). Auf diese Weise kannst du ein Import-Skript schreiben, das die notwendigen Informationen aus dem System liest, aus dem du importierst, und Anweisungen direkt auf „stdout“ ausgibt. Du kannst dann dieses Programm ausführen und seine Ausgaben über git fast-import pipen.

Um das kurz zu demonstrieren, schreibe eine einfache Import-Anweisung. Angenommen, du arbeitest im current Branch, du sicherst dein Projekt, indem du das Verzeichnis gelegentlich in ein mit Zeitstempel versehenes back_YYYY_MM_DD Backup-Verzeichnis kopieren und dieses in Git importieren möchtest. Deine Verzeichnisstruktur sieht wie folgt aus:

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Damit du ein Git-Verzeichnis importieren kannst, musst du dir ansehen, wie Git seine Daten speichert. Wie du dich vielleicht erinnerst, ist Git im Grunde genommen eine verknüpfte Liste von Commit-Objekten, die auf einen Schnappschuss des Inhalts verweisen. Alles, was du tun musst, ist fast-import mitzuteilen, worum es sich bei den Content-Snapshots handelt, welche Commit-Datenpunkte zu ihnen gehören und in welcher Reihenfolge sie in den jeweiligen Ordner gehören. Deine Strategie besteht darin, die Snapshots einzeln durchzugehen und Commits mit dem Inhalt jedes Verzeichnisses zu erstellen. Dabei wird jeder Commit mit dem vorherigen verknüpft.

Wie wir es in Kapitel 8, Beispiel für Git-forcierte Regeln getan haben, werden wir das in Ruby schreiben, denn damit arbeiten wir normalerweise und es ist eher leicht zu lesen. Du kannst dieses Beispiel sehr leicht in jedem Editor schreiben, den du kennst – er muss nur die entsprechenden Informationen nach stdout ausgeben können. Unter Windows musst du besonders darauf achten, dass du am Ende deiner Zeilen keine Zeilenumbrüche einfügst – git fast-import ist da sehr empfindlich, wenn es darum geht, nur Zeilenvorschübe (LF) und nicht die von Windows verwendeten Zeilenvorschübe (CRLF) zu verwenden.

Zunächst wechselst du in das Zielverzeichnis und identifizierst jene Unterverzeichnisse, von denen jedes ein Snapshot ist, den du als Commit importieren möchtest. Du wechselst in jedes Unterverzeichnis und gibst die für den Export notwendigen Befehle aus. Deine Hauptschleife sieht so aus:

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

Führe print_export in jedem Verzeichnis aus, das das Manifest und die Markierung des vorherigen Snapshots enthält und das Manifest und die Markierung dieses Verzeichnisses zurückgibt. So kannst du sie richtig verlinken. „Mark“ ist der fast-import Begriff für eine Kennung, die du einem Commit mitgibst. Wenn du Commits erstellst, gibst du jedem eine Markierung, mit dem du ihn von anderen Commits aus verlinken kannst. Daher ist das Erste, was deine print_export Methode tut, die Generierung einer Markierung aus dem Verzeichnisnamen:

mark = convert_dir_to_mark(dir)

Du wirst dazu ein Array von Verzeichnissen erstellen und den Indexwert als Markierung verwenden. Eine Markierung muss nämlich eine Ganzzahl (Integer) sein. Deine Methode sieht so aus:

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

Nachdem du nun eine ganzzahlige Darstellung deines Commits hast, benötigst du ein Datum für die Commit-Metadaten. Das Datum wird im Namen des Verzeichnisses ausgewiesen, daher wirst du es auswerten. Die nächste Zeile in deiner print_export Datei lautet:

date = convert_dir_to_date(dir)

wobei convert_dir_to_date definiert ist als:

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

Das gibt einen ganzzahligen Wert für das Datum jedes Verzeichnisses zurück. Die letzte Meta-Information, die du für jeden Commit benötigst, sind die Committer-Daten, die du in einer globalen Variable hartkodierst:

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

Damit bist du startklar für die Ausgabe der Commit-Daten für deinen Importer. Die ersten Informationen beschreiben, dass du ein Commit-Objekt definierst und in welchem Branch es sich befindet, gefolgt von der Markierung, die du generiert hast, den Committer-Informationen und der Commit-Beschreibung und dann, falls vorhanden, der vorherige Commit. Der Code sieht jetzt so aus:

# 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

Du kannst die Zeitzone (-0700) hartkodieren, da das einfach ist. Wenn du sie aus einem anderen System importierst, musst du die Zeitzone als Offset angeben. Die Commit-Beschreibung muss in einem speziellen Format ausgegeben werden:

data (size)\n(contents)

Das Format besteht aus den Wortdaten, der Größe der zu lesenden Daten, einer neuen Zeile und schließlich den Daten. Da du später das gleiche Format verwenden musst, um den Datei-Inhalt festzulegen, erstellst du mit export_data eine Hilfs-Methode:

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

Nun musst du nur noch den Dateiinhalt für jeden Schnappschuss angeben. Das ist einfach, denn du hast jeden in einem eigenen Verzeichnis. Du kannst den Befehl deleteall ausgeben, gefolgt vom Inhalt jeder Datei im Verzeichnis. Git zeichnet dann jeden Schnappschuss entsprechend auf:

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

Hinweis: Da viele Systeme ihre Revisionen als Änderungen von einem Commit zum anderen betrachten, kann fast-import auch Befehle mit jedem Commit übernehmen, um anzugeben, welche Dateien hinzugefügt, entfernt oder geändert wurden und was die neuen Inhalte sind. Du kannst die Unterschiede zwischen den Snapshots berechnen und nur diese Daten bereitstellen, aber das ist komplizierter – in diesem Fall solltest du Git alle Daten übergeben und sie auswerten lassen. Sollte diese Option für deine Daten besser geeignet sein, informiere dich in der fast-import Man-Page, wie du deine Daten auf diese Weise bereitstellen kannst.

Das Format für die Auflistung des neuen Datei-Inhalts oder die Angabe einer modifizierten Datei mit dem neuen Inhalt lautet wie folgt:

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

Im Beispiel ist es der Modus 644 (wenn du ausführbare Dateien hast musst du stattdessen 755 ermitteln und spezifizieren), und inline besagt, dass der Inhalt unmittelbar nach dieser Zeile aufgelistet wird. Das Verfahren inline_data sieht so aus:

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

Bei der Wiederverwendung der Methode export_data, die du zuvor definiert hast, handelt es sich um das gleiche Verfahren wie bei der Angabe deiner Commit-Message-Daten.

Als Letztes musst du die aktuelle Markierung an das System zurückgeben, damit sie an die nächste Iteration weitergegeben werden kann:

return mark
Anmerkung

Wenn du unter Windows arbeitest, musst du unbedingt einen zusätzlichen Arbeitsschritt hinzufügen. Wie bereits erwähnt, verwendet Windows CRLF für Zeilenumbrüche, während git fast-import nur LF erwartet. Um dieses Problem zu umgehen und git fast-import gerecht zu werden, musst du Ruby anweisen, LF anstelle von CRLF zu verwenden:

$stdout.binmode

Das war' s. Das Skript ist jetzt komplett:

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

Wenn du dieses Skript ausführst, solltest du eine Ausgabe wie die folgende erhalten:

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

Um den Importer aufzurufen, übergibst du diese Output-Pipe an git fast-import, während du dich in dem Git-Verzeichnis befindest, in das du importieren willst. Du kannst ein neues Verzeichnis erstellen und dort git init für einen Anfangspunkt ausführen und danach dein Skript ausführen:

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

Wie du siehst, gibt es nach erfolgreichem Abschluss eine Reihe von Statistiken über den erreichten Status. In diesem Fall hast du 13 Objekte mit insgesamt 4 Commits in einen Branch importiert. Jetzt kannst du git log ausführen, um deine neue Historie zu sehen:

$ 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

So ist es richtig – ein ordentliches, sauberes Git-Repository. Es ist wichtig zu beachten, dass nichts ausgecheckt ist – du hast zunächst keine Dateien in deinem Arbeitsverzeichnis. Um sie zu erhalten, musst du deinen Branch auf den aktuellen master zurücksetzen:

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

Mit dem fast-import Tool kannst du noch viel mehr tun – bearbeiten von unterschiedlichen Modi, binären Daten, multiplen Branches und Merges, Tags, Verlaufsindikatoren und mehr. Eine Reihe von Beispielen für komplexere Szenarien findest du im contrib/fast-import Verzeichnis des Git-Quellcodes.

scroll-to-top