Как стать автором
Обновить

История одного Репозитория

Время на прочтение14 мин
Количество просмотров8.7K
Эта история началась много-много ревизий назад – тогда SVN Репозиторий был девственно чист, и ни один баг еще не осквернил его своим присутствием. Первые коммиты, первые откаты, просмотры лога – все это было так захватывающе, так ново. И разве мог Репозиторий тогда предполагать, что эти первые, такие приятные шаги впоследствии приведут его на хирургический стол?

Репозиторий рос, креп, матерел. Со временем привык к коммитам, появились первые тэги, и даже мечты о ветках перестали казаться несбыточными. Репозиторий познакомился с другими SVN репозиториями, а с некоторыми даже стал обмениваться файлами. Порой он подолгу выкачивал изменения у своих новых друзей, по ходу процесса наслаждаясь анализом диффов.

Первые тучи над горизонтом стали появляться, когда Репозиторий начал все чаще слышать в разговорах разработчиков незнакомые слова “Git” и “DVCS”. Он пробовал расспрашивать об этом своих друзей-репозиториев, но те лишь стыдливо отводили глаза…

Со временем эти тревоги улеглись, однако жизнь уже не была такой как прежде. Репозиторий погрузнел, стал больше задумываться о смысле жизни. Старые связи уже не доставляли удовольствия, а превратились, скорее, в обременительные зависимости. Все чаще стали сниться ночные кошмары про неразрешимые конфликты при мержах.

Жизнь текла размеренно, когда вдруг прекратились коммиты. Тревожные предчувствия вмиг возобновились. Репозиторий, конечно, успокаивал себя мыслями о том, что разработчики, должно быть, в отпуске, и скоро все вернется в привычное русло, но ощущение чего-то плохого не покидало.

Однажды вечером в дверь постучали. “Наконец-то, svn commit!” – радостно подумал Репозиторий и не по годам ловко подскочил к дверям. “Кто там?” – спросил он, но в ответ услышал лишь напряженное сопение за дверью. Гадко засосало под ложечкой. Через мгновение за дверью пробасили: “Открывайте, svnadmin dump”. Дрожащими руками Репозиторий отворил дверь… и пал без чувств.

Коммиты мчались перед глазами один за одним, они то появлялись, то исчезали, и так повторялось вновь и вновь. “svnadmin load, svnadmin dump, svnadmin load, svnadmin dump” – Репозиторий не мог понять, что происходит, он то приходил в сознание, то снова проваливался в забытье. И лишь маленький, мерцающий огонек вдалеке вселял надежду…

Оставим нашего героя ненадолго и попробуем разобраться, что же произошло.

Глазами разработчиков


Проницательный читатель может предположить, что ситуация, в которой оказался Репозиторий, как-то связана с распределенными системами контроля версий. Именно так и произошло – разработчики прониклись всеми преимуществами Mercurial и решили мигрировать весь свой код туда. Переход с Subversion на Mercurial, однако, можно осуществить достаточно безболезненно, так зачем же потребовалось мучить Репозиторий?

Дело в том, что ранее разработчики допустили две ошибки, которые теперь проявили себя во всей красе:
  1. Изначально, вопреки всем best practices, стали коммитить в корень репозитория, не удосужившись создать традиционные папки trunk, branches и tags. Значительно позже эти папки были созданы и файлы перенесены, но, как известно, из репозитория коммитов не выкинешь. Теперь, если оставить все без изменений, то при миграции на Mercurial коммиты вне trunk пропадут.
  2. Как мы помним, наш герой Репозиторий погряз в “обременительных зависимостях”, и речь здесь, конечно, идет об svn:externals. Ранее я рассказывал о противоречивости данной технологии. При миграции на Mercurial на данный момент не существует простого способа перенести svn:externals.
Для корректного переноса всей истории из Subversion в Mercurial разработчиками было принято решение модифицировать SVN репозиторий так, чтобы избавиться от вышеуказанных проблем.

Репозиторий под скальпелем


Для модификации репозитория нам в первую очередь потребуется утилита svnadmin, которая позволяет делать полный дамп SVN репозитория, дамп для конкретных ревизий или диапазона ревизий, а также накатывать дампы на уже существующий репозиторий. Небольшое отступление для Windows пользователей – данная утилита не является частью TortoiseSVN, но можно установить дополнительный Subversion клиент, например Slik SVN, в котором svnadmin присутствует.

Общая идея модификации репозитория такова: мы должны воссоздать репозиторий с нуля, скопировав “правильные” коммиты без изменений и руками заменив “неправильные”. Под правильностью в нашем случае понимается ситуация, когда коммит не содержит упоминаний об svn:externals и все файлы находятся в trunk. После воссоздания нужно поправить для измененных коммитов дату и время на исходные.

Для того, чтобы понять, как работает метод, рассмотрим модификацию реального репозитория. Я подготовил небольшой тестовый репозиторий, в который присутствуют все указанные выше проблемы. Скачать его дамп (demo_repo.dmp) вместе с остальными дампами, упоминающимися в статье, можно тут.

Репозиторий содержит 8 ревизий, вот описание каждой из них:
  1. В корень репозитория добавлен текстовый файл.
  2. В корень репозитория добавлен небольшой C# проект HelloWorld.
  3. Добавлены папки trunk, tags, branches. Все файлы из корня перенесены в trunk.
  4. Добавлена папка ‘3rd party’, и для нее установлено свойство svn:externals для скачивания файлов из внешнего репозитория в подпапку.
  5. Свойство svn:externals удалено для папки ‘3rd party’ и установлено на папке trunk.
  6. Удален текстовый файл из trunk.
  7. Для папки trunk установлено свойство svn:ignore. Модифицированы некоторые файлы в HelloWorld.
  8. Добавлен текстовый файл в trunk. Удалена подпапка в HelloWorld.
Очевидно “правильные” здесь лишь ревизии 6 и 8. Все остальные так или иначе потребуется модифицировать.

Проблема с ревизиями 1, 2, 3 в том, что файлы находятся вне trunk. Чтобы это исправить мы вставим перед ними один коммит, в котором создадим папки branches, tags, trunk; коммиты 1 и 2 модифицируем, чтобы файлы добавлялись в trunk; коммит 3 вовсе удалим.

Ревизии 4 и 5 связаны с svn:externals, мы должны заменить внешние зависимости непосредственно файлами, загружаемыми из внешнего репозитория. Ревизия 7, внешне безобидная, также косвенно связана с svn:externals, подробнее далее.

Итак, начнем. Я буду модифицировать репозиторий под Windows, однако ничто не мешает проделать в точности те же шаги, например, под Linux.

Избавляемся от svn:externals


Не стоит сразу пытаться и переместить все коммиты в trunk, и избавиться от svn:externals. Лучше выполнять эти задачи последовательно. Мы начнем со второй задачи.

Сначала мы должны определить все проблемные ревизии, которые потребуется заменить вручную. Открываем полный дамп нашего репозитория в текстовом редакторе (я использую Notepad++) и последовательно ищем вхождения фразы “svn:externals”. Их может быть значительно больше, чем нужно, нас интересуют только такие вхождения:

K 13
svn:externals

Для каждого такого места нам нужно найти строку “Revision-number:” выше. Она и содержит нужный нам номер проблемной ревизии.



Когда мы дойдем до конца дампа для нашего тестового репозитория, то должны получить следующие номера проблемных ревизий: 4, 5 и 7.
Теперь нам нужно подготовить частичные дампы для ревизий, не требующих изменений. Для этого:
  1. Загружаем наш репозиторий полностью в некоторую локальную папку, назовем ее full_repo. Это делается с помощью следующих вызовов в командной строке:

    C:\Subversion> svnadmin create full_repo
    C:\Subversion> svnadmin load full_repo < demo_repo.dmp

  2. Далее подготавливаем дампы для тех ревизий репозитория, которые не потребуется менять (1-3, 6 и 8):

    C:\Subversion> svnadmin dump full_repo -r 0:3 --incremental >demo0_3.dmp
    C:\Subversion> svnadmin dump full_repo -r 6 --incremental >demo6.dmp
    C:\Subversion> svnadmin dump full_repo -r 8 --incremental >demo8.dmp

  3. Проверяем, что полученные дампы не содержат вхождений строки “svn:externals”. Важно аккуратно проверить это сейчас, случайно допущенная ошибка на этом этапе может сильно осложнить жизнь в дальнейшем.
Далее начинаем создавать модифицированный репозиторий. Сначала загружаем в него (назовем его, например, result_repo) первые 3 ревизии:

C:\Subversion> svnadmin create result_repo
C:\Subversion> svnadmin load result_repo < demo0_3.dmp

Теперь необходимо исправить 4-й коммит. Для этого нам потребуется сделать checkout нашего нового репозитория. Для это можно воспользоваться TortoiseSVN либо продолжить работать из командной строки, например так:

C:\Subversion> svn co file:///C:/Subversion/result_repo result_checkout

Проверяем, что все идет по нашему плану – переходим в папку result_checkout и проверяем, что загружено 3 ревизии:

C:\Subversion> cd result_checkout
C:\Subversion\result_checkout> svn log -l 1
------------------------------------------------------------------------
r3 | shibaev | 2010-12-09 23:53:09 +0600 (Чт, 09 дек 2010) | 1 line

Moved to trunk.
------------------------------------------------------------------------

Теперь мы должны исполнить “правильный” 4-й коммит. Для этого нам потребуется состояние оригинального репозитория на 4-й ревизии. Поэтому делаем checkout и для него, но сразу на ревизию №4:

C:\Subversion\result_checkout > cd ..
C:\Subversion> svn co file:///C:/Subversion/full_repo full_checkout -r 4

Последняя команда может немного затянуться, поскольку будут выгружаться файлы с внешнего репозитория.

Сверим часы. Наша рабочая папка выглядит так:



А лог для full_checkout в TortoiseSVN – так:



В логе смотрим, что же изменилось в 4-й ревизии. А изменилось следующее – в trunk была добавлена папка “3rd party”, да не простая, а с выставленным свойством svn:externals:

iTextSharp https://itextsharp.svn.sourceforge.net/svnroot/itextsharp/tags/iTextSharp_5_0_5/iTextSharp/text/xml/simpleparser/

Поэтому, чтобы поправить 4-й коммит, мы должны в исправляемый репозиторий добавить папку /trunk/3rd party/iTextSharp и закоммитить:
  1. Переходим в result_checkout/trunk
  2. Создаем папку 3rd party
  3. Копируем в нее папку iTextSharp из full_checkout
  4. Удаляем из нее подпапку .svn
  5. Добавляем (svn add) в репозиторий result_checkout папку 3rd party со всем содержимым
  6. Делаем коммит, сохраняя оригинальную подпись, например так:
    C:\Subversion\result_checkout> svn commit -m"Added external reference"
Отлично, есть первый модифицированный коммит! Однако, прежде чем идти дальше, давайте подумаем, чем чревата ошибка в ходе такого процессе (если мы, например, закоммитили не то или накатили неверный дамп). Представим, что в репозитории не 8 коммитов, а 2000, и первый раз svn:externals встречаются в ревизии №1200. В этом случае уже загрузка самого первого дампа (ревизии с 0 до 1199) и его чекаут будут занимать неприлично много времени.

Последствия ошибки могут быть очень неприятными – запросто можем испортить воссоздаваемый репозиторий, и придется начинать все сначала. Поэтому необходимо сразу озаботиться проблемой бэкапа для промежуточных результатов. Сохранять необходимо как минимум репозиторий (result_repo), а также checkout (result_checkout), если его восстановление с нуля занимает существенное время.

Организуем бэкап так – будем сжимать необходимые папки в zip-архив и копировать в некоторую папку. Для этого воспользуемся архиватором 7-zip. Установим его и добавим путь к 7z.exe в переменную окружения PATH. Теперь можно делать бэкап промежуточных состояний репозитория так:

C:\Subversion> 7z a backups\result_repoX.zip result_repo,
где X – номер ревизии (например, сейчас в нашем случае X == 4)

После того, как мы сохранили промежуточный результат, самое время двигаться дальше – нужно сделать вручную коммит №5. Для этого:
  1. обновляем full_checkout до ревизии №5:

    C:\Subversion\full_checkout> svn up -r 5

  2. смотрим, какие изменения содержит пятый коммит
В данном случае изменения минимальны – свойство svn:externals удалено для папки “3rd party” и создано для папки trunk. Адрес внешнего репозитория не изменился, как и папка, в которую требуется выкачивать файлы. Поэтому для нашего нового репозитория никаких изменений не потребуется, нужно лишь как-то сделать коммит. Для этого можно создать пустой файл Fictive в trunk, добавить его в SVN и сделать коммит. Подпись к коммиту лучше не менять (почему – станет ясно позже)

После того, как 5-й коммит добавлен, накатываем demo6.dmp:

C:\Subversion> svnadmin load result_repo < demo6.dmp

Делаем бэкап и переходим к последнему серьезному пункту нашей программы – модификации коммита №7. Обновляем full_checkout репозиторий до 7-й ревизии и смотрим список изменений:



Причина, по которой этот коммит требуется модифицировать, ясна не сразу, ведь никаких изменений, касающихся svn:externals не делалось. Для папки trunk было изменено свойство svn:ignore, может в этом причина? Так и есть. Дело в том, что в SVN дампе для ревизии 7 хранится полное описание свойства папки trunk, вместе с svn:externals:



Итак, за дело. Есть 2 способа сделать 7-й коммит: хитрый и прямолинейный.

Первый заключается в том, чтобы взять из оригинального репозитория дамп для 7-й ревизии, удалить из свойства папки trunk упоминание об svn:externals (открываем в текстовом редакторе, удаляем, вычитаем количество удаленных байт из свойств “Prop-content-length” и “Content-length”. Или же пользуемся командой “svnadmin setrevprop”) и накатить такой, уже “правильный”, дамп в новый репозиторий. В данном случае этот подход является наилучшим решением.

Однако интересней рассмотреть второй способ, поскольку в большинстве случаев для реальных репозиториев придется пользоваться именно им. Этот способ заключается в том, чтобы примерить на себе шкуру TortoiseSVN и проделать все изменения 7 коммита самостоятельно.

То, насколько аккуратно мы будем повторять действия SVN клиента, полностью зависит от нашего терпения и внимательности. Не стоит обрабатывать каждое изменение индивидуально, главное добиться, чтобы наборы файлов в исходном и новом репозиториях совпадали. Единственное –перемещения файлов/папки в SVN желательно повторить в точности, чтобы не потерять историю объекта до перемещения. Кстати, на этом этапе также можно удалить файл Fictive, созданный ранее в trunk.

После того, как все изменения произведены, стоит проверить, что затронутые проекты по-прежнему компилируются и тесты проходят. Впрочем, если есть полная уверенность в своих действиях в качестве Subversion клиента, то можно и не проверять.

Далее коммитим наши изменения и выходим на финишную прямую. Накатываем последний дамп demo8.dmp. Обновляем обе рабочие копии result_checkout и full_checkout до последней ревизии и сравниваем их. Если все было проделано правильно, то они должны быть полностью идентичны по набору файлов.

Получаем дамп для измененного репозитория:

C:\Subversion> svnadmin dump result_repo >without_externals.dmp

Данный дамп полностью избавлен от внешних зависимостей, однако от нас еще потребуются некоторые изменения. Дело в том, что для коммитов, сделанных вручную, в дампе прописано свежее время коммита, а также неверное имя пользователя (обычно совпадает с именем пользователя операционной системы в Windows). Надо исправлять.

Открываем оригинальный и новый дампы в текстовом редакторе и последовательно проходим по модифицированным ревизиям (4, 5, 7 в нашем примере). Выглядеть это будет примерно так:



Теперь нужно аккуратно заменить соответствующие даты в новом дампе на оригинальные. Для этого можно снова воспользоваться утилитой svnadmin и ее опцией “setrevprop”, однако, на мой взгляд, быстрее это сделать руками в текстовом редакторе.

Замена даты является безобидной операцией, а вот замена автора коммита потребует дополнительных усилий. Как можно заметить, значения свойств “Prop-content-length” и “Content-length”, а также число над именем автора коммита для исходного и обновленного дампов не совпадают. Это происходит из-за того, что длина имени автора отличается от исходной. Поэтому сначала меняем автора коммита, а затем обновляем соответствующие значения. Notepad++ предоставляет удобный способ проверить, что все было сделано правильно:



После того, как свойства всех модифицированных коммитов будут исправлены, можно считать этап избавления от svn:externals завершенным. Однако, рекомендую после этого проверить, что полученный дамп полностью корректно загружается в локальный репозиторий:

C:\Subversion> svnadmin create test_repo
C:\Subversion> svnadmin load test_repo < without_externals.dmp

В ходе данной проверки могут автоматически выявиться ошибки, допущенные в ходе модификации репозитория. В этом случае загрузка дампа прервется в месте ошибки, и мы сможем выяснить причины. Обычно это происходит, если мы испортили содержимое некоторого файла или не обновили какой-то файл. Сообщение об ошибке будет сигнализировать о несовпадении контрольных сумм.

Переносим все коммиты в trunk


С навыками, полученными на предыдущем этапе, перенести все коммиты в trunk не составит труда. Итак, мы имеем репозиторий without_externals.dmp, в котором первые два коммита сделаны в корень репозитория, а в третьем коммите создаются папки trunk, tags, branches, и все файлы переносятся туда.

План действий:
  1. Создаем чистый репозиторий, в него коммитим пустые папки branches, tags, trunk
  2. Разбиваем дамп without_externals.dmp на два – ревизии с 0-й по 2-ю и ревизии с 4-й по 8-ю. Коммит №3 выкидывается!
  3. В первом дампе (с 0 по 2 ревизии) добавляем ко всем путям в файле префикс “trunk/”
  4. Последовательно накатываем оба дампа на наш новый репозиторий
  5. Проверяем, что не было допущено ошибок, и делаем полный дамп получившегося репозитория
  6. Модифицируем время и автора для первого коммита
Поехали. Создаем чистый репозиторий (папка final_repo), чекаутим его (final_checkout). Создаем 3 папки, добавляем в репозиторий и коммитим:

C:\Subversion> svnadmin create final_repo
C:\Subversion> svn co file:///C:/Subversion/final_repo final_checkout
C:\Subversion> mkdir branches tags trunk
C:\Subversion> svn add branches tags trunk
C:\Subversion> svn commit -m"Prepared repository structure"

Разбиваем дамп для репозитория без внешних зависимостей на два, чтобы исключить ненужный коммит.

C:\Subversion> svnadmin create without_externals_repo
C:\Subversion> svnadmin load without_externals_repo < without_externals.dmp
C:\Subversion> svnadmin dump without_externals_repo -r 0:2 --incremental >final0_2.dmp
C:\Subversion> svnadmin dump without_externals_repo -r 4:8 --incremental >final4_8.dmp

Открываем дамп final0_2.dmp и заменяем все строки “-path: ” на “ -path: trunk/”. Важно – заменяем в точности так (а не только “Node-path: ”), поскольку пути могут использоваться в свойствах “Node-path” и “Node-copyfrom-path”.



Накатываем final0_2.dmp и final4_8.dmp на новый репозиторий:

C:\Subversion> svnadmin load final_repo < final0_2.dmp
C:\Subversion> svnadmin load final_repo < final4_8.dmp

Проверям SVN лог для final_checkout, все коммиты должны оперировать файлами только в trunk. Получаем обновленный дамп:

C:\Subversion> svnadmin dump final_repo > final.dmp

Модифицируем время и автора для 1-й ревизии, а также время 0-й ревизии. За основу берем время 0-й ревизии в without_externals.dmp, используем его для 0-й ревизии в новом дампе, затем прибавляем к нему несколько секунд и устанавливаем для 1-й ревизии.
Проверяем, что полученный дамп в порядке, загружая его в репозиторий:

C:\Subversion> svnadmin create test_final_repo
C:\Subversion> svnadmin load test_final_repo < final.dmp

Если в ходе загрузки возникла ошибка о несовпадении контрольных сумм, вероятней всего строка “-path:” встречалась где-то внутри файлов, и мы ее случайно заменили (в реальных репозиториях мы не можем глазами просматривать все места, требующие замены, поскольку их может быть очень много). При этом бывает, что строка встречается точно в таком виде, как и в свойствах ревизии (например, “Node-path: …”), поэтому ужесточение шаблона замены не поможет.

В нашем примере ошибки не происходит, а это значит, что мы получили полностью пригодный для миграции в Mercurial репозиторий.

Подведем итоги


Мы рассмотрели ряд приемов, применяемых для модификации SVN репозиториев. Основные моменты:
  1. Утилита svnadmin – основной инструмент. Позволяет загрузить дамп в репозиторий, получить дамп для нужных ревизий.
  2. Можем чередовать в модифицируемом репозитории “ручные” коммиты и загрузку существующих дампов. Типичный прием – выкидываем из репозитория ненужные коммиты и заменяем (или просто пропускаем) их в новом репозитории.
  3. Делаем бэкап для промежуточных результатов как можно чаще.
  4. Для всех типовых операций (загрузка дампа, создание дампа, чекаут, бэкап) удобно использовать bat или bash скрипты.
  5. Когда модифицируем SVN дамп в текстовом редакторе – не забываем обновлять числовые значения – размеры соответствующего контента в байтах.
  6. Если наш SVN дамп большой (>700 Мб), то появляются проблемы с его редактированием, поскольку большинство текстовых редакторов под Windows не могут его нормально открыть. Notepad++ не исключение. Я пробовал множество разных вариантов, помог лишь EmEditor, нормально работающий даже на машине с небольшим количеством оперативной памяти.
  7. Если после модификации не удается загрузить репозиторий из полученного дампа – либо что-то напутали со смещениями, либо забыли обновить какой-то файл, либо испортили содержимое файла.
  8. Когда приходится самостоятельно повторять изменения в некотором коммите, бывает полезно рекурсивно удалить внутри какой-нибудь папки все подпапки .svn. Для этого можно воспользоваться таким скриптом (для Windows), который рекурсивно удаляет все подпапки в текущей директории:

    FOR /F "tokens=*" %%G IN ('DIR /B /AD /S *.svn*') DO RMDIR /S /Q "%%G"

  9. Не стоит каким-либо образом модифицировать подписи к коммитам, поскольку это влияет на количество байт в описании ревизии в дампе. Из-за этого будет чуть сложнее обновлять имя автора коммита, поскольку придется учитывать еще и разницу в длине подписи к коммиту.

Заключение


Репозиторий приходил в себя. Все смешалось в его некогда светлой голове – коммиты, логи, svnadmin. Пелена наркоза еще не рассеялась окончательно, но Репозиторий уже почувствовал – что-то внутри него переменилось.

Открыв глаза, Репозиторий первым делом осмотрелся по сторонам – комната была просторная, но интерьер совершенно незнакомый. На месте дубового шифоньера с коммитами высилась небольшая этажерка, а вместо коробок с тэгами на стене был прикреплен небольшой стикер с малопонятными строками вроде “6fc1d7a7ae346b0a09be5647e94c1561764b8619”. Однако, на удивление, общее убранство комнаты Репозиторию понравилось, всё было оформлено с чувством и по фэн-шую.

Репозиторий не успел еще толком освоиться, как в дверь позвонили. “Здравствуйте! Доставка коммитов” – протараторил невысокий человек в кепке с надписью “Mercurial”. Репозиторий не успел открыть рта, как незнакомец быстро вручил ему два буклета с надписью “hg commit” на обложке и был таков.

Репозиторий вспомнил, что ему уже доводилось испытывать подобное. Давным-давно, в самом начале, незнакомый человек затащил в его квартиру самый первый “svn commit”. Неужели сейчас история повторяется? Репозиторию захватило дух от восторга – мало кому посчастливится вернуться в детство вместе со всем накопленным опытом и знаниями, а с ним, похоже, произошло именно это! И зря он перестал верить в Деда Мороза – не исключено, что это чудо – именно его рук дело…

Репозиторий не знал, что ждет его впереди. Одно он понимал четко – у него началась новая жизнь – интересная, непредсказуемая, с приключениями. Жизнь на планете Mercurial.
Теги:
Хабы:
Всего голосов 97: ↑92 и ↓5+87
Комментарии52

Публикации

Истории

Ближайшие события