Git
Chapters ▾ 2nd Edition

9.2 Git and Other Systems - Міграція на Git

Міграція на Git

Якщо ваш код вже зберігається в іншій системі контролю версій, але ви вирішили почати використовувати Git, вам необхідно так чи інакше здійснити міграцію проекту. У цій секції описано деякі варіанти імпорту для поширених систем, а потім показано, як розробити власні варіанти імпорту. Ви дізнаєтесь як імпортувати дані з деякий найбільших професійних систем контролю версій, оскільки вони використовуються більшістю розробників та для них легко знайти якісні інструменти міграції.

Subversion

Якщо ви прочитали попередню секцію про використання git 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 --quiet | 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 --prefix "" -s my_project
$ cd 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

Не лише поле з автором виглядає набагато краще, а й більше немає git-svn-id.

Також варто трохи прибрати після імпортування. По-перше, треба вичистити дивні посилання, які налаштував git svn. Спершу, ми перемістимо теґи, щоб вони стали справжніми теґами, а не дивними віддаленими гілками, а потім перемістимо решту гілок, щоб вони стали локальними.

Щоб перемістити теґи до належних теґів Git, виконайте:

$ 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

Це бере посилання, які були віддаленими гілками, що починались з refs/remotes/tags/ та перетворює їх на справжні (легкі) теґи.

Далі, перемістіть решту посилань під refs/remotes до локальних гілок:

$ 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

Може так статися, що ви побачите якісь зайві гілки з наростками @xxx (де xxx — число), хоча в Subversion ви бачите лише одну гілку. Насправді це функціонал Subversion під назвою “peg-revisions” для якого в Git просто немає синтаксичного відповідника. Отже, git svn просто додає номер версії svn до імʼя гілки саме так, як би ви написали його в svn, щоб звернутися до peg-revision цієї гілки. Якщо вам начхати на ці peg-revisions, просто вилучите їх:

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

Тепер усі старі гілки стали справжніми гілками Git, та всі старі теґи стали справжніми теґами Git.

Залишилось прибрати єдину річ. На жаль, git svn створює додаткову гілку trunk, яка відповідає типовій гілці Subversion, проте посилання trunk вказує туди ж, куди й master. Оскільки master більш традиційний для Git, ось як вилучити зайву гілку:

$ git branch -d trunk

Залишилось лише додати новий сервер Git як віддалене сховище та надіслати до нього. Ось приклад додавання сервера як віддаленого:

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

Оскільки ви бажаєте надіслати всі гілки та теґи, то можете виконати наступне:

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

Усі ваші гілки та теґи мають бути на новому сервері 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 <remote repo URL> /tmp/hg-repo

Наступним кроком є створення файлу відображення авторів. Mercurial трохи більш поблажливий, ніж Git, щодо того, що він дозволить записати в поле автора набору змін (changeset), отже, настав час прибратися в домі. Ось однорядкова команда оболонки 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>

У цьому прикладі, одна людина (Боб) створювала набори змін під чотирма різними іменами, одне з яких дійсно виглядає правильним — те, яке буде цілком відповідним для коміту Git. Hg-fast-export дозволяє нам виправити це, якщо додати ={нове імʼя та поштова адреса} наприкінці кожного рядка, який ми бажаємо змінити, та якщо видалити будь-які імена користувачів, які ми бажаємо облишити. Якщо всі імена користувачів виглядають правильно, то нам взагалі не потрібен цей файл. У цьому прикладі, ми бажаємо, щоб наш файл виглядав так:

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>

Наступним кроком є створення нашого нового сховища Git, та виконання скрипту експорту:

$ 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 повідомляє йому, де знайти файл відображення авторів. Скрипт зчитує набори змін Mercurial та перетворює їх на скрипт для функції Git "fast-import" (ми обговоримо її докладно трохи пізніше). Це може зайняти деякий час (хоча набагато швидше, ніж було б через мережу), та вивід буде доволі детальним:

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

Bazaar

Bazaar is a DVCS tool much like Git, and as a result it’s pretty straightforward to convert a Bazaar repository into a Git one. To accomplish this, you’ll need to import the bzr-fastimport plugin.

Getting the bzr-fastimport plugin

The procedure for installing the fastimport plugin is different on UNIX-like operating systems and on Windows. In the first case, the simplest is to install the bzr-fastimport package that will install all the required dependencies.

For example, with Debian and derived, you would do the following:

$ sudo apt-get install bzr-fastimport

With RHEL, you would do the following:

$ sudo yum install bzr-fastimport

With Fedora, since release 22, the new package manager is dnf:

$ sudo dnf install bzr-fastimport

If the package is not available, you may install it as a plugin:

$ mkdir --parents ~/.bazaar/plugins/bzr     # creates the necessary folders for the plugins
$ cd ~/.bazaar/plugins/bzr
$ bzr branch lp:bzr-fastimport fastimport   # imports the fastimport plugin
$ cd fastimport
$ sudo python setup.py install --record=files.txt   # installs the plugin

For this plugin to work, you’ll also need the fastimport Python module. You can check whether it is present or not and install it with the following commands:

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

If it is not available, you can download it at address https://pypi.python.org/pypi/fastimport/.

In the second case (on Windows), bzr-fastimport is automatically installed with the standalone version and the default installation (let all the checkboxes checked). So in this case you have nothing to do.

At this point, the way to import a Bazaar repository differs according to that you have a single branch or you are working with a repository that has several branches.

Project with a single branch

Now cd in the directory that contains your Bazaar repository and initialize the Git repository:

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

Now, you can simply export your Bazaar repository and convert it into a Git repository using the following command:

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

Depending on the size of the project, your Git repository is built in a lapse from a few seconds to a few minutes.

Case of a project with a main branch and a working branch

You can also import a Bazaar repository that contains branches. Let us suppose that you have two branches: one represents the main branch (myProject.trunk), the other one is the working branch (myProject.work).

$ ls
myProject.trunk myProject.work

Create the Git repository and cd into it:

$ git init git-repo
$ cd git-repo

Pull the master branch into git:

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

Pull the working branch into Git:

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

Now git branch shows you the master branch as well as the work branch. Check the logs to make sure they’re complete and get rid of the marks.bzr and marks.git files.

Synchronizing the staging area

Whatever the number of branches you had and the import method you used, your staging area is not synchronized with HEAD, and with the import of several branches, your working directory is not synchronized either. This situation is easily solved by the following command:

$ git reset --hard HEAD

Ignoring the files that were ignored with .bzrignore

Now let’s have a look at the files to ignore. The first thing to do is to rename .bzrignore into .gitignore. If the .bzrignore file contains one or several lines starting with "!!" or "RE:", you’ll have to modify it and perhaps create several .gitignore files in order to ignore exactly the same files that Bazaar was ignoring.

Finally, you will have to create a commit that contains this modification for the migration:

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

Sending your repository to the server

Here we are! Now you can push the repository onto its new home server:

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

Your Git repository is ready to use.

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 більше немає в повідомленнях комітів:

$ 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-tf неприйнятно складно.

Note

Це однобічна конвертація. Отримане сховище Git не буде в змозі взаємодіяти з оригінальним проектом TFVC.

Спершу треба встановити відображення імен користувачів. TFVC доволі ліберальний щодо того, що може бути в полі автора для набору змін, проте Git бажає читабельне ім’я та поштову адресу. Ви можете отримати цю інформацію з консольного клієнта tf ось так:

PS> tf history $/myproject -recursive > AUTHORS_TMP

Ця команда отримує всі набори змін в історії проекту та кладе їх у файл AUTHORS_TMP, з якого ми отримаємо дані зі стовпчика User (другого). Відкрийте файл та з’ясуйте з якого символу починається та на якому закінчується стовпчик, та замініть у наступній команді параметри 11-20 команди cut своїми значеннями:

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

Команда cut залишає лише символи між 11 та 20 кожного рядка. Команда tail ігнорує перші два рядки, що є заголовками та лінією ASCII-арт. Результат пропускається через sort і 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 зсередини). Таким чином, ви пишете скрипт імпортування, який читає необхідну інформацію зі системи, з якої ви імпортуєте та друкує зрозумілі інструкції до 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, адже це те, з чим ми зазвичай працюємо, та його легко читати. Ви можете написати цей приклад доволі легко будь-якою мовою, з якою знайомі – скрипт просто має виводити відповідну інформацію до stdout. І, якщо ви використовуєте Windows, це означає, що вам необхідно окремо попіклуватися про те, щоб не виводити символів повернення каретки наприкінці рядків — git fast-import дуже вибагливо бажає лише зміни рядків (LF), а не повернення каретки та зміни рядків (CRLF), які використовує Windows.

Спочатку, треба перейти до цільової директорії та визначити всі піддиректорії, кожна з яких є відбитком, який ви бажаєте імпортувати як коміт. Ви перейдете до кожної піддиректорії та надрукуєте команди, необхідні для її експорту. Базовий головний цикл виглядає так:

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

Ви виконуєте метод 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>'

Тепер ви готові почати друкувати дані комітів для імпортера. Початкова інформація зазначає, що ви визначаєте обʼєкт коміту та в якій ви гілці, після чого йде позначка, яку ви згенерували, інформація про автора коміту та повідомлення коміту, а потім попередній коміт, якщо такий є. Код виглядає так:

# 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

Ви прописуєте в коді часовий пояс (-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 всі дані та дозволити йому самому все зробити. Якщо це доречніше для ваших даних, подивіться довідку (man page) fast-import для детального опису того, як можна надати дані в такому форматі.

Формат для надання вмісту нового файлу чи зазначення зміненого файлу з новим вмістом наступний:

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

Тут, 644 — це права доступу (якщо у вас виконанний файл, то треба це визначити та задати натомість 755), а 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

Це все. Ось весь скрипт цілком:

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

Якщо виконати цей скрипт, то отримаємо дані, що виглядають приблизно так:

$ 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, а вже після цього виконати ваш скрипт:

$ 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. Важливо зазначити, що нічого не отримано (checked out) – у вас спочатку немає жодного файлу в робочій директорій. Щоб отримати їх, ви маєте пересунути свою гілку до теперішнього 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.