Git
Chapters ▾ 2nd Edition

9.2 Το Git και άλλα συστήματα - Μετανάστευση στο Git

Μετανάστευση στο Git

Αν έχουμε υπάρχουσα βάση κώδικα σε άλλο VCS αλλά έχουμε αποφασίσει να αρχίσουμε να χρησιμοποιούμε το Git, πρέπει να μεταφέρουμε το έργο μας με τον ένα ή τον άλλο τρόπο. Αυτή η ενότητα αφορά ορισμένους εισαγωγείς για τα συνήθη συστήματα και στη συνέχεια δείχνει πώς να αναπτύξουμε το δικό μας, προσαρμοσμένο στις ανάγκες μας εισαγωγέα. Θα μάθουμε πώς μπορούμε να εισάγουμε δεδομένα από πολλά από τα μεγαλύτερα συστήματα SCM που χρησιμοποιούνται επαγγελματικά, επειδή αποτελούν την πλειονότητα των χρηστών που μετακινούνται και επειδή είναι διαθέσιμα εργαλεία υψηλής ποιότητας για αυτά τα συστήματα.

Subversion

Όπως αναφέρεται στην προηγούμενη ενότητα σχετικά με τη χρήση του git svn, μπορούμε εύκολα να χρησιμοποιήσουμε αυτές τις οδηγίες για να κλωνοποιήσουμε ένα αποθετήριο SVN με την git svn clone· στη συνέχεια, σταματούμε να χρησιμοποιούμε τον διακομιστή Subversion, ωθούμε στον νέο διακομιστή Git και αρχίζουμε να χρησιμοποιούμε αυτόν. Εάν θέλουμε το ιστορικό, αυτό είναι κάτι που μπορούμε να το πετύχουμε τόσο γρήγορα όσο γρήγορα μπορούμε να τραβήξουμε τα δεδομένα από τον διακομιστή Subversion (αυτό μπορεί να πάρει διαρκέσει αρκετά).

Ωστόσο, η εισαγωγή δεν είναι τέλεια· και επειδή θα διαρκέσει τόσο πολύ ούτως ή άλλως, ας την κάνουμε σωστά. Το πρώτο πρόβλημα είναι οι πληροφορίες του συγγραφέα. Στο Subversion, κάθε άτομο που υποβάλλει έχει έναν χρήστη στο σύστημα που καταγράφεται καταγραφεί στις πληροφορίες της υποβολής. Τα παραδείγματα στην προηγούμενη ενότητα δείχνουν schacon σε ορισμένα σημεία, όπως στις εξόδους των blame και git svn log. Εάν θέλουμε να αντιστοιχίσουμε αυτό για να βελτιώσουμε τις πληροφορίες των συγγραφέων στο Git, χρειάζεστε μια απεικόνιση από τους χρήστες του Subversion στους συγγραφείς του Git. Δημιουργούμε ένα αρχείο με όνομα users.txt που έχει αυτήν την απεικόνιση στην παρακάτω μορφή:

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

Για να αποκτήσουμε μια λίστα με τα ονόματα συγγραφέων που χρησιμοποιεί το SVN, μπορούμε να εκτελέσουμε το εξής:

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

Αυτό δίνει την έξοδο του αρχείου καταγραφής σε μορφή XML, διατηρεί μόνο τις γραμμές με πληροφορίες συγγραφέα, απορρίπτει διπλότυπα, απομακρύνει τις ετικέτες XML. (Προφανώς αυτό λειτουργεί μόνο σε ένα μηχάνημα που έχει εγκατεστημένα τα προγράμματα grep, sort και perl.) Στη συνέχεια, ανακατευθύνουμε την έξοδο στο αρχείο users.txt, ώστε να μπορούμε να προσθέσουμε τα αντίστοιχα δεδομένα χρήστη Git δίπλα σε κάθε καταχώρηση.

Μπορούμε να δώσουμε αυτό το αρχείο στην git svn για να το βοηθήσουμε να αντιστοιχίσει τα δεδομένα των συγγραφέων με μεγαλύτερη ακρίβεια. Μπορούμε επίσης να πούμε στο git svn να μην συμπεριλάβει τα μεταδεδομένα που εισάγει το Subversion υπό κανονικές συνθήκες, περνώντας την επιλογή --no-metadata στην εντολή clone ή την init. Αυτό κάνει την εντολή import μας να μοιάζει με αυτή:

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

Τώρα θα πρέπει να έχουμε μια καλύτερη εισαγωγή από το Subversion στον κατάλογο my_project. Αντί οι υποβολές που μοιάζουν με αυτό:

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

μοιάζουν με αυτό:

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

    fixed install - go to trunk

Όχι μόνο το πεδίο “Author” φαίνεται πολύ καλύτερα, αλλά επιπλέον το git-svn-id δεν βρίσκεται πια εκεί.

Θα πρέπει επίσης να κάνουμε ένα συμμαζεματάκι μετά την εισαγωγή. Καταρχάς, θα πρέπει να καθαρίσουμε τις περίεργες αναφορές που έβαλε η git svn. Αρχικά θα μετακινήσουμε τις ετικέτες έτσι ώστε να είναι πραγματικές ετικέτες και όχι περίεργοι απομακρυσμένοι κλάδοι και στη συνέχεια θα μετακινήσουμε τους υπόλοιπους κλάδους έτσι ώστε να είναι τοπικοί.

Για να μετακινήσουμε τις ετικέτες ώστε να είναι κατάλληλες ετικέτες Git, εκτελούμε:

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

Το παραπάνω παίρνει τις αναφορές που ήταν απομακρυσμένοι κλάδοι που ξεκίνησαν με remotes/origin/tags/ και τα κάνει πραγματικές (ελαφριές) ετικέτες.

Στη συνέχεια μετακινούμε τις υπόλοιπες αναφορές του φακέλου refs/remotes ώστε να είναι τοπικοί κλάδοι:

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

Τώρα όλοι οι παλιοί κλάδοι είναι πραγματικοί κλάδοι Git και όλες οι παλιές ετικέτες είναι πραγματικές ετικέτες Git. Το τελευταίο πράγμα που πρέπει να κάνουμε είναι να προσθέσουμε τον νέο μας διακομιστή Git ως απομακρυσμένο και να τον ωθήσουμε. Ακολουθεί ένα παράδειγμα προσθήκης του διακομιστή μας ως απομακρυσμένου αποθετηρίου:

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

Επειδή θέλουμε να ανέβουν όλοι οι κλάδοι και οι ετικέτες μας, μπορούμε τώρα να εκτελέσουμε το εξής:

$ git push origin --all

Όλοι οι κλάδοι μας και οι ετικέτες μας θα πρέπει να βρίσκονται στο νέο μας διακομιστή Git σε μια ωραία και καθαρή εισαγωγή.

Mercurial

Δεδομένου ότι τα Mercurial και Git έχουν αρκετά παρόμοια μοντέλα για να αναπαριστούν τις εκδόσεις και καθώς το Git είναι λίγο πιο ευέλικτο, η μετατροπή ενός αποθετηρίου από το Mercurial στο Git είναι αρκετά απλή, χάρη σε ένα εργαλείο που ονομάζεται hg-fast-export, το οποιο θα χρειαστούμε:

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

Το πρώτο βήμα της μετατροπής είναι να αποκτήσουμε έναν πλήρη κλώνο του αποθετηρίου Mercurial που θέλουμε να μετατρέψουμε:

$ hg clone <URL_απομακρυσμένου_αποθετηρίου> /tmp/hg-repo

Το επόμενο βήμα είναι να δημιουργήσουμε ένα αρχείο αντιστοίχισης των συγγραφέων. Το Mercurial είναι λίγο πιο συγχωρητικό από το Git όσον αφορά σε αυτά που θα θέσει στο πεδίο author για το σύνολο των αλλαγών, γι' αυτό είναι μια καλή ευκαιρία για ένα μικρό συμμάζεμα. Το συμμάζεμα είναι μία εντολή της μιας γραμμής στο κέλυφος bash:

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

Αυτό θα διαρκέσει μερικά δευτερόλεπτα, ανάλογα με το πόσο εκτενές είναι το ιστορικό του έργου μας και μετά το αρχείο /tmp/authors θα μοιάζει με αυτό:

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

Σε αυτό το παράδειγμα, το ίδιο άτομο (Bob) δημιούργησε σύνολα αλλαγών κάτω από τέσσερα διαφορετικά ονόματα, ένα από τα οποία φαίνεται πραγματικά σωστό και ένα από τα οποία θα ήταν εντελώς άκυρο για μια υποβολή Git. Η hg-fast-export μάς επιτρέπει να το διορθώσουμε προσθέτοντας ={νέο όνομα και νέα διεύθυνση e-mail} στο τέλος κάθε γραμμής που θέλουμε να αλλάξουμε και αφαιρώντας τις γραμμές για όλα τα ονόματα χρήστη που θέλουμε να αφήσουμε ανέγγιχτα. Αν όλα τα ονόματα χρηστών φαίνονται ωραία, δεν θα χρειαστεί καθόλου αυτό το αρχείο. Σε αυτό το παράδειγμα, θέλουμε το αρχείο μας να μοιάζει με αυτό:

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>

Το επόμενο βήμα είναι να δημιουργήσουμε το νέο μας αποθετήριο Git και να εκτελέσουμε το script εξαγωγής:

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

Η σημαία -r λέει στο hg-fast-export πού θα βρει το αποθετήριο Mercurial που θέλουμε να μετατρέψουμε και η σημαία -A του λέει πού θα βρει το αρχείο αντιστοίχισης των συγγραφέων. Το script αναλύει τα σύνολα αλλαγών του Mercurial changesets και τα μετατρέπει σε ένα script για τη λειτουργία “γρήγορης εισαγωγής” του Git (η οποία θα συζητηθεί λεπτομερώς λίγο αργότερα). Αυτό διαρκεί λιγάκι (αν και είναι πολύ πιο γρήγορα από ό,τι θα ήταν πάνω από το δίκτυο) και η έξοδος είναι αρκετά λεπτομερής:

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

Σε γενικές γραμμές αυτό ήταν όλο. Όλες οι ετικέτες του Mercurial έχουν μετατραπεί σε ετικέτες Git και τα οι κλάδοι και σελιδοδείκτες του Mercurial έχουν μετατραπεί σε κλάδους του Git. Τώρα είμαστε έτοιμοι να ωθήσουμε το αποθετήριο στον διακομιστή που θα είναι το νέο του σπίτι:

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

Perforce

Το επόμενο σύστημα από το οποίο θα εξετάσουμε την εισαγωγή είναι το Perforce. Όπως συζητήσαμε παραπάνω, υπάρχουν δύο τρόποι να αφήσουμε τα Git και Perforce να μιλήσουν μεταξύ τους: οι git-p4 και Perforce Git Fusion.

Perforce Git Fusion

Το Git Fusion καθιστά αυτήν τη διαδικασία αρκετά ανώδυνη. Απλά διαμορφώνουμε τις ρυθμίσεις του έργου, τις αντιστοιχίσεις χρηστών και τους κλάδους μας χρησιμοποιώντας ένα αρχείο διαμόρφωσης (όπως αναλύθηκε στην ενότητα Git Fusion) και κλωνοποιούμε το αποθετήριο. Το Git Fusion μας αφήνει με κάτι που μοιάζει με εγγενές αποθετήριο Git, το οποίο είναι έτοιμο να ωθήσει σε έναν εγγενή κεντρικό υπολογιστή Git, εφόσον το επιθυμούμε. Θα μπορούσαμε ακόμη και να χρησιμοποιήσουμε το Perforce ως τον κεντρικό υπολογιστή του Git, αν θέλουμε.

git-p4

Η git-p4 μπορεί επίσης να λειτουργήσει ως εργαλείο εισαγωγής. Για παράδειγμα, θα εισάγουμε το έργο Jam από το Perforce Public Depot. Για να ρυθμίσουμε τον πελάτη μας, πρέπει να εξάγουμε τη μεταβλητή περιβάλλοντος P4PORT για να δείξουμε στην αποθήκη Perforce:

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

Για τη συνέχεια της επίδειξης, θα χρειαστεί να συνδεθούμε σε μία αποθήκη Perforce. Θα χρησιμοποιήσουμε τη δημόσια αποθήκη στο public.perforce.com για τα παραδείγματα μας, αλλά μπορούμε να χρησιμοποιήσουμε οποιαδήποτε αποθήκη στην οποία έχουμε πρόσβαση.

Εκτελούμε την εντολή git p4 clone για να εισάγουμε το έργο Jam από τον διακομιστή Perforce, παρέχοντας την αποθήκη, τη διαδρομή έργου και τη διαδρομή στην οποία θέλουμε να εισάγουμε το έργο:

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

Αυτό το συγκεκριμένο έργο έχει μόνο έναν κλάδο, αλλά αν έχουμε κλάδους που έχουν διαμορφωθεί με προβολές κλάδων (ή απλώς ένα σύνολο καταλόγων), μπορούμε να χρησιμοποιήσουμε τη σημαία --detect-branches στην git p4 clone για να εισάγουμε όλους τους κλάδους του έργου. Βλ. ενότητα Διακλάδωση για λίγο περισσότερες λεπτομέρειες σχετικά με αυτό.

Σε αυτό το σημείο είμαστε σχεδόν έτοιμοι. Εάν μεταβούμε στον κατάλογο p4import και εκτελέσουμε την git log, μπορούμε να δούμε την εργασία που εισάγαμε:

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

Μπορούμε να δούμε ότι το git-p4 έχει αφήσει ένα αναγνωριστικό σε κάθε μήνυμα υποβολής. Μπορούμε να διατηρήσουμε αυτό το αναγνωριστικό εκεί, σε περίπτωση που χρειάζεται να αναφερθούμε αργότερα στον αριθμό αλλαγής Perforce. Ωστόσο, αν θέλουμε να καταργήσουμε το αναγνωριστικό, τώρα είναι η κατάλληλη στιγμή να το κάνουμε —προτού αρχίσουμε να εργαζόμαστε στο νέο αποθετήριο. Μπορούμε να χρησιμοποιήσουμε την git filter-branch για να αφαιρέσουμε τις σειρές αναγνωριστικών μαζικά:

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

Αν εκτελέσουμε την git log, μπορούμε να δούμε ότι όλα τα αθροίσματα ελέγχου SHA-1 για τις υποβολές έχουν αλλάξει αλλά οι συμβολοσειρές git-p4 δεν βρίσκονται πλέον στα μηνύματα 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).

Η εισαγωγή μας είναι έτοιμη να ωθηθεί στον νέο μας διακομιστή Git.

TFS

Αν η ομάδα μας μετατρέπει τον έλεγχο πηγαίου κώδικα από το TFVC στο Git, θα θελήσουμε να έχουμε την πιο πιστή μετατροπή που μπορούμε να αποκτήσουμε. Αυτό σημαίνει ότι, αν και είδαμε τόσο την git-tfs όσο και την git-tf στην ενότητα της διαλειτουργικότητας, θα δούμε μόνο την git-tfs σε αυτό το μέρος, επειδή η git-tfs υποστηρίζει κλάδους κάτι που είναι απαγορευτικά δύσκολο με την git-tf.

Note

Πρόκειται για μια μονόδρομη μετατροπή. Το αποθετήριο Git που προκύπτει δεν θα μπορεί να συνδεθεί με το αρχικό έργο TFVC.

Το πρώτο πράγμα που πρέπει να κάνουμε είναι να αντιστοιχίσουμε τα ονόματα χρηστών. Το TFVC είναι αρκετά φιλελεύθερο με αυτό που πηγαίνει στο πεδίο author για σύνολα αλλαγών, αλλά το Git θέλει ένα ανθρωπανάγνωστο όνομα και διεύθυνση e-mail. Μπορούμε να λάβουμε αυτές τις πληροφορίες από τον πελάτη γραμμής εντολών tf, όπως π.χ.:

PS> tf history $/myproject -recursive > AUTHORS_TMP

Αυτό τραβάει όλα τα σύνολα αλλαγών στο ιστορικό του έργου και τα βάζει στο αρχείο AUTHORS_TMP που θα επεξεργαστούμε για την εξαγωγή των δεδομένων της στήλης User (2η στήλη). Ανοίγουμε το αρχείο και βρίσκουμε ποιοι χαρακτήρες ξεκινούν και τελειώνουν τη στήλη αυτή και αντικαθιστούμε στην ακόλουθη γραμμή εντολών τις παραμέτρους 11-20 της εντολής cut με αυτούς που βρέθηκαν:

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

Η εντολή cut διατηρεί μόνο τους χαρακτήρες μεταξύ των θέσεων 11 και 20 από κάθε γραμμή. Η εντολή tail παραλείπει τις δύο πρώτες γραμμές, οι οποίες είναι κεφαλίδες πεδίων και υπογραμμίσεις ASCII-art. Το αποτέλεσμα όλων αυτών παροχετεύεται στη uniq για να εξαλείψει τις διπλές καταχωρήσεις και αποθηκεύεται σε ένα αρχείο που ονομάζεται AUTHORS. Το επόμενο βήμα είναι χειροκίνητο· για να μπορέσει η git-tfs να χρησιμοποιήσει αποτελεσματικά αυτό το αρχείο, κάθε γραμμή πρέπει να είναι σε αυτήν τη μορφή:

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

Το τμήμα στα αριστερά είναι το πεδίο “User” από το TFVC και το τμήμα στη δεξιά πλευρά του = είναι το όνομα χρήστη που θα χρησιμοποιηθεί για τις υποβολές Git.

Μόλις έχουμε αυτό το αρχείο, το επόμενο πράγμα που πρέπει να κάνουμε είναι να φτιάξουμε έναν πλήρη κλώνο του έργου TFVC που μας ενδιαφέρει:

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

Στη συνέχεια, θέλουμε να καθαρίσουμε τις ενότητες git-tfs-id από το κάτω μέρος των μηνυμάτων υποβολής. Αυτό μπορεί να γίνει με την ακόλουθη εντολή:

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

Αυτό χρησιμοποιεί την εντολή sed από το περιβάλλον Git-bash για να αντικαταστήσει οποιαδήποτε γραμμή που ξεκινά με git-tfs-id: με κενό, το οποίο το Git θα αγνοήσει.

Μόλις γίνει αυτό, είμαστε έτοιμοι να προσθέσουμε ένα νέο απομακρυσμένο αποθετήριο, να ωθήσουμε όλους τους κλάδους μας και να αρχίσουμε να εργαζόμαστε στο Git.

Ένας εξατομικευμένος εισαγωγέας

Εάν το σύστημά μας δεν είναι ένα από τα παραπάνω, θα πρέπει να αναζητήσουμε έναν εισαγωγέα σε απευθείας σύνδεση —διατίθενται ποιοτικοί εισαγωγείς για πολλά άλλα συστήματα, όπως τα CVS, Clear Case, Visual Source Safe, ακόμη και έναν κατάλογο αρχειοθηκών. Εάν κανένα από αυτά τα εργαλεία δεν μας κάνει, έχουμε ένα πιο ανιγματώδες εργαλείο ή τέλος πάντων χρειαζόμαστε μια πιο εξατομικευμένη διαδικασία εισαγωγής, θα πρέπει να χρησιμοποιήσουμε την git fast-import. Αυτή η εντολή διαβάζει απλές οδηγίες από τη stdin και γράφει συγκεκριμένα δεδομένα Git. Είναι πολύ πιο εύκολο να δημιουργήσουμε αντικείμενα Git με αυτόν τον τρόπο από το να εκτελέσουμε τις εντολές του Git ή να προσπαθήσουμε να γράψουμε τα ανεπεξέργαστα αντικείμενα (για περισσότερες πληροφορίες, βλ. το Εσωτερική λειτουργία του Git). Με αυτόν τον τρόπο, μπορούμε να γράψουμε ένα script εισαγωγής που διαβάζει τις απαραίτητες πληροφορίες από το σύστημα από το οποίο εισάγουμε και εκτυπώνει απλές οδηγίες στη stdout. Στη συνέχεια μπορούμε να εκτελέσουμε αυτό το πρόγραμμα και να παροχετεύσουμε την έξοδό του μέσω της git fast-import.

Για μία γρήγορη επίδειξη, θα γράψουμε έναν απλό εισαγωγέα. Ας υποθέσουμε ότι εργαζόμαστε στον current, δημιουργούμε αντίγραφα ασφαλείας του έργου μας αντιγράφοντας περιστασιακά τον κατάλογο σε έναν χρονοσημασμένο back_YYYY_MM_DD κατάλογο αντιγράφων ασφαλείας και θέλουμε να τον εισαγάγουμε στο Git. Η δομή του καταλόγου μας μοιάζει με αυτό:

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Για να εισαγάγουμε έναν κατάλογο Git, θα πρέπει να θυμηθούμε τον τρόπο με τον οποίο το Git αποθηκεύει δεδομένα. Όπως έχει αναφερθεί, το Git είναι βασικά μια συνδεδεμένη λίστα αντικειμένων υποβολής που δείχνουν σε ένα στιγμιότυπο του περιεχομένου. Το μόνο που έχουμε να κάνουμε είναι να πούμε στην fast-import ποια είναι τα στιγμιότυπα του περιεχομένου, ποια δεδομένα υποβολών δείχνουν σε αυτά και τη χρονική σειρά τους. Η στρατηγική μας θα είναι να περάσουμε από όλα τα στιγμιότυπα ένα προς ένα και να δημιουργήσουμε υποβολές με τα περιεχόμενα κάθε καταλόγου, συνδέοντας κάθε υποβολή με την προηγούμενη.

Όπως κάναμε στην ενότητα Ένα παράδειγμα επιβολής πολιτικής από το Git, θα το γράψουμε σε Ruby, επειδή είναι αυτό που γενικά δουλεύουμε και είναι εύκολο να το διαβαστεί. Μπορούμε να γράψουμε αρκετά εύκολα το συγκεκριμένο script σε οποιαδήποτε γλώσσα με την οποία είμαστε εξοικειωμένοι —χρειάζεται μόνο να τυπώσουμε τις κατάλληλες πληροφορίες στη stdout. Και αν έχουμε Windows, αυτό σημαίνει ότι θα πρέπει να προσέξουμε να μην εισάγουμε χαρακτήρες επαναφοράς (CR) στο τέλος των γραμμών μας —η git fast-import είναι πολύ ιδιότροπη σε αυτό το θέμα και θέλει μόνο χαρακτήρες τροφοδότησης γραμμής (LF) και όχι χαρακτήρες επαναφοράς και τροφοδότησης γραμμής(CRLF) που χρησιμοποιούν τα Windows.

Αρχικά, θα μεταβούμε στον κατάλογο-στόχο και θα αναγνωρίσουμε κάθε υποκατάλογο, καθένας από τους οποίους είναι ένα στιγμιότυπο που θέλουμε να εισάγουμε ως υποβολή. Θα μεταβούμε σε κάθε υποκατάλογο και θα εκτυπώσουμε τις εντολές που είναι απαραίτητες για την εξαγωγή του. Ο βασικός μας κύριος βρόχος μοιάζει με αυτόν:

last_mark = nil

# Βρόχος επανάληψης σε όλους τους υποκαταλόγους
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # Μετακινήσου στον κατάλογο-στόχο
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

Εκτελούμε την print_export μέσα σε κάθε κατάλογο, ο οποίος λαμβάνει το δηλωτικό και το σημάδι του προηγούμενου στιγμιότυπου και επιστρέφει το δηλωτικό και το σημάδι αυτού. Με αυτόν τον τρόπο, θα μπορέσουμε να τα συνδέσουμε σωστά. “Σημάδι” (mark) είναι ο όρος της fast-import για ένα αναγνωριστικό που δίνουμε σε μια υποβολή· όταν δημιουργούμε υποβολές, δίνουμε σε καθεμία από αυτές ένα σημάδι το οποίο μπορούμε να χρησιμοποιήσουμε για να συνδεθούμε σε αυτήν από άλλες υποβολές. Έτσι, το πρώτο πράγμα που πρέπει να κάνουμε στη μέθοδο print_export μας είναι να δημιουργήσουμε ένα σημάδι από το όνομα του καταλόγου:

mark = convert_dir_to_mark(dir)

Θα το κάνουμε αυτό δημιουργώντας έναν πίνακα από καταλόγους και χρησιμοποιώντας την τιμή του δείκτη ως σημάδι, διότι το σημάδι πρέπει να είναι ένας ακέραιος αριθμός. Η μέθοδος μας μοιάζει με αυτό:

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

Τώρα που έχουμε μια ακέραια αναπαράσταση της υποβολής μας, χρειαζόμαστε μια ημερομηνία για τα μεταδεδομένα της υποβολής. Επειδή η ημερομηνία εκφράζεται στο όνομα του καταλόγου, θα την εξάγουμε κάνοντας συντακτική ανάλυση. Η επόμενη γραμμή στο αρχείο print_export είναι:

date = convert_dir_to_date(dir)

όπου η convert_dir_to_date ορίζεται ως:

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

Αυτό επιστρέφει μια ακέραια τιμή για την ημερομηνία κάθε καταλόγου. Το τελευταίο κομμάτι των μετα-πληροφοριών που χρειάζεστε για κάθε υποβολή είναι τα δεδομένα αυτού που έκανε την υποβολή, τα οποία έχουμε κωδικοποιήσει σε μια καθολική μεταβλητή:

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

Τώρα είμαστε έτοιμοι να ξεκινήσουμε να εκτυπώνουμε τα στοιχεία της υποβολής για τον εισαγωγέα μας. Οι αρχικές πληροφορίες δηλώνουν ότι ορίζουμε ένα αντικείμενο υποβολής και σε ποιον κλάδο είναι, ακολουθούμενες από το σημάδι που δημιουργήσαμε, τις πληροφορίες αυτού που υπέβαλε, και το μήνυμα υποβολής και μετά την προηγούμενη υποβολή, εφόσον υπάρχει. Ο κώδικας μοιάζει με αυτό:

# Εκτύπωσε τις πληροφορίες εισαγωγής
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

Μπορούμε να θέσουμε τη ζώνη ώρας (-0700) αντί να τη διαβάσουμε, διότι αυτό είναι το πιο εύκολο. Αν εισάγουμε από άλλο σύστημα, πρέπει να καθορίσουμε τη ζώνη ώρας ως διαφορά. Το μήνυμα υποβολής πρέπει να εκφράζεται σε ειδική μορφή:

data (size)\n(contents)

Η μορφή αποτελείται από τη λέξη data, το μέγεθος των προς ανάγνωση δεδομένων, μια νέα γραμμή και τελικά τα δεδομένα. Επειδή πρέπει να χρησιμοποιήσουμε την ίδια μορφή για να καθορίσουμε αργότερα τα περιεχόμενα του αρχείου, δημιουργούμε μια βοηθητική μέθοδο, export_data:

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

Το μόνο που έχει απομείνει είναι να καθορίσουμε τα περιεχόμενα του αρχείου για κάθε στιγμιότυπο. Αυτό είναι εύκολο, επειδή το κάθε στιγμιότυπο είναι σε έναν κατάλογο —μπορούμε να εκτυπώσουμε την εντολή deleteall ακολουθούμενη από τα περιεχόμενα κάθε αρχείου στον κατάλογο. Το Git θα καταγράψει στη συνέχεια κάθε στιγμιότυπο κατάλληλα:

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

Σημείωση: Επειδή πολλά συστήματα ντιμετωπίζουν τις αναθεωρήσεις τους ως μεταβολές από μία υποβολή σε μία άλλη, η fast-import μπορεί επίσης να λάβει εντολές με κάθε υποβολή για να καθορίσει ποια αρχεία έχουν προστεθεί, αφαιρεθεί ή τροποποιηθεί και ποια είναι τα νέα περιεχόμενα. Θα μπορούσαμε να υπολογίσουμε τις διαφορές μεταξύ των στιγμιότυπων και να δώσουμε μόνο αυτά τα δεδομένα, αλλά αυτό είναι πιο περίπλοκο —μπορούμε απλά να δώσουμε στο Git όλα τα δεδομένα και να αφήσουμε αυτό να καταλάβει τι γίνεται. Αν κάτι τέτοιο ταιριάζει καλύτερα στα δεδομένα μας, καλό είναι να ελέγξουμε τη σελίδα του εγχειριδίου για την fast-import για λεπτομέρειες σχετικά με τον τρόπο παροχής των δεδομένων μας με αυτόν τον τρόπο.

Η μορφή εμφάνισης των νέων περιεχομένων του αρχείου ή ο προσδιορισμός ενός τροποποιημένου αρχείου με τα νέα περιεχόμενα είναι ο εξής:

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

Εδώ, 644 είναι ο τρόπος λειτουργίας (αν έχουμε εκτελέσιμα αρχεία, θα πρέπει να τα εντοπίσουμε και να θέσουμε τρόπο λειτουργίας 755 αντί 644), και η inline λέει ότι θα παραθέσουμε το περιεχόμενο αμέσως μετά από αυτήν τη γραμμή. Η μέθοδος inline_data μοιάζει με κάτι τέτοιο:

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

Επαναχρησιμοποιούμε τη μέθοδο export_data που ορίσαμε νωρίτερα, επειδή είναι ο ίδιος τρόπος με αυτόν που καθορίσαμε τα δεδομένα του μηνύματος αποστολής.

Το τελευταίο πράγμα που πρέπει να κάνουμε είναι να επιστρέψουμε το τρέχον σημάδι ώστε να μπορεί να μεταβιβαστεί στην επόμενη επανάληψη:

return mark
Note

Εάν τρέχουμε σε Windows, θα πρέπει οπωσδήποτε να προσθέτουμε ένα επιπλέον βήμα. Όπως αναφέρθηκε προηγουμένως, τα Windows χρησιμοποιούν CRLF για χαρακτήρες νέας γραμμής ενώ η git fast-import αναμένει μόνο LF. Για να αντιμετωπίσουμε αυτό το πρόβλημα και να κάνουμε την git fast-import ευτυχισμένη, πρέπει πρέπει να πούμε στη Ruby να χρησιμοποιήσει τον LF αντί του CRLF:

$stdout.binmode

Αυτό ήταν. Ακολουθεί ολόκληρο το script:

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


# Βρόχος επανάληψης σε όλους τους υποκαταλόγους
last_mark = nil
Dir.chdir(ARGV[0]) do
    Dir.glob("*").each do |dir|
        next if File.file?(dir)

        # Μετακινήσου στον κατάλογο-στόχο
        Dir.chdir(dir) do
            last_mark = print_export(dir, last_mark)
        end
    end
end

Αν εκτελέσουμε αυτό το script, θα πάρουμε περιεχόμενο που μοιάζει με αυτό:

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

Για να τρέξουμε τον εισαγωγέα, παροχετεύουμε αυτήν την έξοδο μέσω της git fast-import ενώ βρισκόμαστε στον κατάλογο Git που θέλουμε να εισάγουμε. Μπορούμε να δημιουργήσουμε ένα νέο κατάλογο και στη συνέχεια να εκτελέσουμε την git init μέσα σε αυτόν για να δημιουργήσουμε ένα σημείο εκκίνησης και στη συνέχεια να εκτελέσουμε το 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
---------------------------------------------------------------------

Όπως μπορούμε να δούμε, όταν ολοκληρώενται με επιτυχία, μας δίνει μια κάμποσα στατιστικά για το τι έχει επιτύχει. Σε αυτήν την περίπτωση, εισάγαμε 13 αντικείμενα συνολικά για 4 υποβολές σε 1 υποκατάστημα. Τώρα, μπορούμε να εκτελέσουμε την git log για να δούμε το νέο μας ιστορικό:

$ 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

Αυτό ήταν! Ένα ωραίο, καθαρό αποθετήριο Git. Είναι σημαντικό να σημειώσουμε ότι τίποτα δεν έχει ανακτηθεί —αρχικά δεν έχουμε αρχεία στον κατάλογο εργασίας μας. Για να τα πάρουμε, πρέπει να επαναφέρουμε τον κλάδο μας στο σημείο όπου είναι τώρα ο master:

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

Μπορούμε να κάνουμε πολλά περισσότερα με το εργαλείο fast-import —να χειριστούμε διαφορετικά είδη, δυαδικά δεδομένα, πολλαπλούς κλάδους και συγχωνεύσεις, ετικέτες, δείκτες προόδου και πολλά άλλα. Ορισμένα παραδείγματα πιο σύνθετων σεναρίων είναι διαθέσιμα στον κατάλογο contrib/fast-import του πηγαίου κώδικα του Git.