Skip to content

Latest commit

 

History

History
377 lines (312 loc) · 16.7 KB

import-custom.asc

File metadata and controls

377 lines (312 loc) · 16.7 KB

Импорт произвольного репозитория

Если вы пользуетесь какой-либо другой системой контроля версий, не перечисленной выше, вам следует поискать инструмент для импорта в Сети — качественные решения доступны для CVS, Clear Case, Visual Source Safe и даже каталогов с архивами. Если всё же существующие решения вам не подошли, вы пользуетесь менее известной системой контроля версий или вам нужно больше контроля над процессом импорта — используйте git fast-import. Эта команда читает простые инструкции из потока ввода и записывает данные в Git. Создать Git-объекты таким путём намного проще, чем через низкоуровневые Git-команды или пытаясь воссоздать их вручную (обратитесь к главе ch10-git-internals.asc за деталями). Таким образом, вы можете написать небольшой скрипт, считывающий нужную информацию из вашего хранилища и выводящий инструкции в стандартный поток вывода. Затем вы можете запустить эту программу и передать её вывод прямиком в 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 по сути представляет собой связанный список ревизий, каждая из которых указывает на слепок состояния. Всё что от вас требуется, это указать `fast-import’у на данные для создания слепков и порядок их применения. Итак, мы пробежимся по всем слепкам, создадим коммит для каждого из них и свяжем каждый новый коммит с предыдущим.

Как и в разделе ch08-customizing-git.asc главы 8, мы проделаем это на Ruby, потому что это тот язык, с которым мы обычно работаем, и его легко читать. Вы можете использовать любой другой язык — всё что требуется, это вывести нужную информацию в стандартный поток вывода.

Если вы работаете на Windows, будьте особо осторожными с переводами строк: 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 = 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>'

Теперь всё готово для вывода нужной `fast-import’у информации. Нужно указать, что создаётся коммит на определённой ветке, затем вывести сгенерированную метку, автора и время изменений и ссылку на предыдущий коммит, если такой имеется. Код выглядит следующим образом:

# 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 прямо в выходной строке. Часовой пояс задаётся как смещение от UTC. Сообщение коммита задаётся следующим образом:

data (size)\n(contents)

Первым идёт слово 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 имеет команды для задания изменений: какие файлы были добавлены, удалены или изменены. Вы можете вычислять разницу между состояниями и передавать её в 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
Note

Если вы используете ОС Windows есть ещё кое-что. Как мы упоминали ранее, Windows использует CRLF для новых строк, в то время как git fast-import ожидает только LF. Чтобы исправить этот недостаток Windows и осчастливить 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
---------------------------------------------------------------------

Как вы можете видеть, после успешного завершения fast-import выводит некоторую статистику о проделанной работе. В этом случае, вы импортировали 13 объектов в 4-х коммитах одной ветки. Теперь можете выполнить 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 гораздо шире описанного: он поддерживает права доступа к файлам, двоичные файлы, множественные ветки и их слияния, метки, индикатор прогресса и ещё кучу вещей. Несколько примеров более сложных сценариев использования fast-import можно найти в каталоге contrib/fast-import исходного кода Git.