Миграция на 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 <[email protected]>
selse = Someo Nelse <[email protected]>
Для того чтобы получить список авторов, который использует 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>'
Теперь мы готовы приступить к выводу данных коммита в своём сценарии импорта. Дадим начальную информацию говорящую, что мы задаём объект коммита, ветку, на которой он находится, затем отметку, которую мы ранее сгенерировали, информацию о коммитере и сообщение коммита, а затем предыдущий коммит, если он есть. Код выглядит следующим образом:
# 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 <[email protected]> 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 <[email protected]> 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 <[email protected]>
Date: Sun May 3 12:57:39 2009 -0700
imported from current
commit 7e519590de754d079dd73b44d695a42c9d2df452
Author: Scott Chacon <[email protected]>
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
, о котором я уже рассказывал.