Git
Chapters ▾ 1st Edition

8.2 Git и другие системы контроля версий - Миграция на Git

Миграция на Git

Если вы решили начать использовать Git, а у вас уже есть база исходного кода в другой СКВ, вам придётся как-то мигрировать свой проект. Этот раздел описывает некоторые из включённых в состав Git'а инструментов для импортирования проектов из самых распространённых систем, в конце описывается создание вашего собственного инструмента для импортирования.

Импортирование

Вы научитесь импортировать данные из двух самых популярных систем контроля версий — Subversion и Perforce — поскольку они охватывают большинство пользователей, которые переходят на 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 | grep -P "^<author" | sort -u | \
      perl -pe 's/<author>(.*?)<\/author>/$1 = /'

Эта команда выдаст журнал в формате XML — мы можем найти в нём информацию об авторах, создать из неё список с уникальными записями и избавиться от XML-разметки. (Разумеется, эта команда сработает только на машине с установленными grep, sort и perl). Затем перенаправьте вывод этой команды в файл users.txt, чтобы потом можно было добавить к каждой записи данные о соответствующих пользователях Git'а.

Вы можете передать этот файл как параметр команде git svn для более точного преобразования данных об авторах. Кроме того, можно дать указание git svn не включать метаданные, обычно импортируемые Subversion, передав параметр --no-metadata команде clone или init. Таким образом, команда для импортирования будет выглядеть так:

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

Теперь в вашем каталоге 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/tags/* .git/refs/tags/
$ rm -Rf .git/refs/remotes/tags

Эти действия переместят ссылки, которые были удалёнными ветками начинающимися с tag/, и сделают их настоящими (легковесными) метками.

Затем, переместите остальные ссылки в 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-сервер в чистом и опрятном виде.

Perforce

Следующей системой, для которой мы рассмотрим процедуру импортирования, будет Perforce. Утилита импортирования для Perforce также распространяется в составе Git'а. Если вы используете Git версии старее 1.7.11, то сценарий доступен только в секции contrib исходного кода — он не доступен по умолчанию, как git svn. В этом случае вам потребуется получить исходный код Git'а, располагающийся на git.kernel.org:

$ git clone git://git.kernel.org/pub/scm/git/git.git
$ cd git/contrib/fast-import

В каталоге fast-import вы найдёте исполняемый сценарий на языке Python с названием git-p4. У вас на компьютере должен быть установлен Python и утилита p4 для того, чтобы этот сценарий смог осуществить импортирование. Допустим, например, что вы импортируете проект Jam из Perforce Public Depot. Для настройки вашей клиентской машины необходимо установить переменную окружения P4PORT, указывающую на депо Perforce:

$ export P4PORT=public.perforce.com:1666

Запустите команду git-p4 clone для импортирования проекта Jam с Perforce-сервера, передав в качестве параметров депо и путь к проекту, а также путь к каталогу, в который вы хотите импортировать проект:

$ git-p4 clone //public/jam/src@all /opt/p4import
Importing from //public/jam/src@all into /opt/p4import
Reinitialized existing Git repository in /opt/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 4409 (100%)

Если вы теперь перейдёте в каталог /opt/p4import и выполните команду git log, вы увидите импортированную информацию:

$ git log -2
commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2
Author: Perforce staff <support@perforce.com>
Date:   Thu Aug 19 10:18:45 2004 -0800

    Drop 'rc3' moniker of jam-2.5.  Folded rc2 and rc3 RELNOTES into
    the main part of the document.  Built new tar/zip balls.

    Only 16 months later.

    [git-p4: depot-paths = "//public/jam/src/": change = 4409]

commit ca8870db541a23ed867f38847eda65bf4363371d
Author: Richard Geiger <rmg@perforce.com>
Date:   Tue Apr 22 20:51:34 2003 -0800

    Update derived jamgram.c

    [git-p4: depot-paths = "//public/jam/src/": change = 3108]

Как видите, в каждом коммите есть идентификатор git-p4. Оставить этот идентификатор будет хорошим решением, если позже вам понадобится узнать номер изменения в Perforce. Однако, если вы всё же хотите удалить этот идентификатор — теперь самое время это сделать, до того, как вы начнёте работать в новом репозитории. Можно воспользоваться командой git filter-branch для одновременного удаления всех строк с идентификатором:

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

Если вы теперь выполните git log, то увидите, что все контрольные суммы SHA-1 изменились, и что строки содержащие git-p4 больше не появляются в сообщениях коммитов:

$ git log -2
commit 10a16d60cffca14d454a15c6164378f4082bc5b0
Author: Perforce staff <support@perforce.com>
Date:   Thu Aug 19 10:18:45 2004 -0800

    Drop 'rc3' moniker of jam-2.5.  Folded rc2 and rc3 RELNOTES into
    the main part of the document.  Built new tar/zip balls.

    Only 16 months later.

commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2
Author: Richard Geiger <rmg@perforce.com>
Date:   Tue Apr 22 20:51:34 2003 -0800

    Update derived jamgram.c

Ваш импортируемый репозиторий готов к отправке на новый Git-сервер.

Собственная утилита для импорта

Если вы используете систему, отличную от Subversion или Perforce, вы можете поискать утилиту для импорта под свою систему в интернете — для CVS, Clear Case, Visual Source Safe и даже для простого каталога с архивами уже существуют качественные инструменты для импортирования. Если ни один из этих инструментов не подходит для ваших целей, либо если вам нужен больший контроль над процессом импортирования, вам стоит использовать утилиту git fast-import. Эта команда принимает простые инструкции со стандартного ввода для управления процессом записи нужных данных в базу Git'а. Намного проще создать необходимые объекты в Git'е так, чем запуская базовые команды Git'а либо пытаясь записать сырые объекты (см. главу 9). При использовании git fast-import вы можете создать сценарий для импортирования, который считывает всю необходимую информацию из импортируемой системы и выводит простые понятные инструкции на стандартный вывод. Затем вы просто запускаете этот сценарий и, используя конвейер (pipe), передаёте результаты его работы на вход git fast-import.

Чтобы быстро продемонстрировать суть этого подхода, напишем простую утилиту для импорта. Положим, что вы работаете в каталоге current и время от времени делаете резервную копию этого каталога, добавляя к имени дату — back_YYYY_MM_DD, и вы хотите импортировать это всё в Git. Допустим, ваше дерево каталогов выглядит таким образом:

$ ls /opt/import_from
back_2009_01_02
back_2009_01_04
back_2009_01_14
back_2009_02_03
current

Для того чтобы импортировать всё это в Git, надо вспомнить, как Git хранит данные. Как вы помните, Git в своей основе представляет собой связный список объектов-коммитов, указывающих на снимки состояния их содержимого. Всё, что вам требуется, это сообщить команде fast-import что является данными снимков состояния, какие данные коммитов указывают на них и порядок их следования. Стратегией наших действий будет обход всех снимков состояния по очереди и создание соответствующих коммитов с содержимым каждого каталога, с привязкой каждого коммита к предыдущему.

Так же как и в главе 7 в разделе «Пример создания политики в Git», мы напишем сценарий на Ruby, поскольку это то, с чем я обычно работаю, и, кроме того, он легко читается. Но вы можете создать его на любом другом языке, которым владеете — он просто должен выводить необходимую информацию на стандартный вывод. Если вы работаете под 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 внутри каждого каталога, она берёт запись и отметку предыдущего снимка состояния и возвращает запись и отметку текущего; так вы сможете их правильно между собой соединить. «Отметка» — это термин утилиты 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 = 'Scott Chacon <schacon@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'у все данные для того, чтобы он сам вычислил разницу. Если с вашими данными проще предоставлять разницу между снимками состояния, обратитесь к странице руководства 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

ПРИМЕЧАНИЕ: Если вы работаете под Windows, то должны убедиться, что добавили ещё один дополнительный шаг. Мы уже упоминали, что Windows использует CRLF для перехода на новую строку, тогда как git fast-import ожидает только LF. Для того чтобы избежать этой проблемы и сделать процесс импорта безошибочным, вам нужно сказать Ruby использовать LF вместо CRLF:

$stdout.binmode

Это всё. Если вы теперь запустите этот сценарий, то получите примерно следующее содержимое:

$ ruby import.rb /opt/import_from 
commit refs/heads/master
mark :1
committer Scott Chacon <schacon@geemail.com> 1230883200 -0700
data 29
imported from back_2009_01_02deleteall
M 644 inline file.rb
data 12
version two
commit refs/heads/master
mark :2
committer Scott Chacon <schacon@geemail.com> 1231056000 -0700
data 29
imported from back_2009_01_04from :1
deleteall
M 644 inline file.rb
data 14
version three
M 644 inline new.rb
data 16
new version one
(...)

Для того чтобы запустить утилиту импорта, перенаправьте этот вывод на вход 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:           18 (         1 duplicates                  )
      blobs  :            7 (         1 duplicates          0 deltas)
      trees  :            6 (         0 duplicates          1 deltas)
      commits:            5 (         0 duplicates          0 deltas)
      tags   :            0 (         0 duplicates          0 deltas)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              3
Memory total:          2255 KiB
       pools:          2098 KiB
     objects:           156 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize =   33554432
pack_report: core.packedGitLimit      =  268435456
pack_report: pack_used_ctr            =          9
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =       1356 /       1356
---------------------------------------------------------------------

Как видите, после успешного завершения Git выдаёт большое количество информации о проделанной работе. В нашем случае мы импортировали в общей сложности 18 объектов для 5 коммитов в одной ветке. Теперь выполните git log, чтобы увидеть свою новую историю изменений:

$ git log -2
commit 10bfe7d22ce15ee25b60a824c8982157ca593d41
Author: Scott Chacon <schacon@example.com>
Date:   Sun May 3 12:57:39 2009 -0700

    imported from current

commit 7e519590de754d079dd73b44d695a42c9d2df452
Author: Scott Chacon <schacon@example.com>
Date:   Tue Feb 3 01:00:00 2009 -0700

    imported from back_2009_02_03

Ну вот, вы получили чистый и красивый Git-репозиторий. Важно отметить, что пока что у вас нет никаких файлов в рабочем каталоге — вы должны сбросить свою ветку на ветку master:

$ ls
$ git reset --hard master
HEAD is now at 10bfe7d imported from current
$ ls
file.rb  lib

С помощью утилиты fast-import можно делать намного больше — манипулировать разными правами доступа, двоичными данными, несколькими ветками, совершать слияния, назначать метки, отображать индикаторы прогресса и многое другое. Некоторое количество примеров более сложных сценариев содержится в каталоге contrib/fast-import в исходном коде Git'а; один из самых лучших из них — сценарий git-p4, о котором я уже рассказывал.