системы управления версиями для программистов, и не только
Я полагаю, что многие из тех, кто занят в сфере информационных технологий, слышали про системы управления версиями. Вот только список тех, кто активно использует эту технологию в своей практике, гораздо короче. Часто говорят, что системы управления версиями (далее СУВ) нужны только программистам, и только тем из них, кто работает в команде. То есть когда кодом владеет не один “избранный”, а любой программист в команде может взять и внести в него изменения. Говорят, что разобраться в СУВ практически невозможно: все эти ветки, ревизии, теги, репозитории слишком сложны и непонятны. Я же, наоборот, утверждаю, что разобраться с СУВ очень легко, а после того, как вы поработали с СУВ несколько дней и перешли на новое рабочее место, где нет какой-либо из СУВ, у вас начнется настоящая ломка и боязнь, “как бы не запортить этот код”. Это похоже на то, как если бы вы были монтажником-высотником, и у вас резко отняли страховочный пояс. Конечно, ваши “крутые” коллеги, предпочитающие жить без лишней страховки, могут смеяться над вами, но только до тех пор, пока кто-то из них не сорвется и не полетит вниз головой. Заканчивая столь занимательную аналогию, хочу сказать, что для того, чтобы получить настоящее удовольствие от работы с СУВ, необходимо, чтобы ваша среда разработки (среда написания кода, графический или какой-то еще редактор) поддерживали СУВ напрямую. Но даже если это не так, то на рынке достаточно много как платных, так и бесплатных программ для работы с СУВ, похожих на привычный вам total commander или непосредственно интегрирующихся в проводник windows. Из собственного опыта я могу вспомнить случай, когда научил девушек-секретарш хранить офисные файлы в svn- репозитории, и они были страшно довольны открывшимися возможностями.
Итак, что такое СУВ? СУВ — это программа, которая упрощает работу с постоянно меняющимися файлами (как текстовыми, так и двоичными). Существует некоторое “безопасное” место, где хранятся ваши документы. Каждый раз, когда вы создаете новую версию некоторого файла, он сохраняется не только на вашем компьютере, но и в этом репозитории, и ничего не теряется. «И всего-то?» — скажете вы. Ведь я и сам могу создавать копию своих файлов в конце рабочего дня. Да, можете, но единое место хранения, за которое отвечает специально выделенный сотрудник (системный администратор), гораздо лучше в плане надежности и резервного копирования. Централизованное хранилище кода очень удобно в ситуации, когда вы хотите что-то показать вашему другу, находящемуся в другой комнате: “эй, вот здесь в файле X.java в ревизии номер 999 странная ошибка — ты знаешь, что это может быть?” Ваш друг за пару кликов мышью извлекает из репозитория ваш пример и подсказывает, в чем дело. Это гораздо лучше, чем просить друга расшарить сетевую папку и затем копировать в нее файлы. В практике системы СУВ редко используются сами по себе — часто они интегрируются с другими системами, поддерживающими жизненный цикл разрабатываемой программы: средства моделирования, ведения журналов ошибок, написания документации и тестирования. Однако сегодня я говорю только о СУВ. Централизованное хранилище позволит вам легче “играться” с кодом. Например, вы создали новую версию программы (состоящую из трех файлов: A, B, C). Затем вы решили поэкспериментировать и внести изменения в один из них. Вы создаете резервную копию этого файла и делаете правки, которые затрагивают также файлы B и C (ну забылись вы, с кем не бывает). Затем, “поигравшись”, вы решаете отменить изменения и замещаете файл A старой версией… Стоп, я же забыл сделать копию файлов B и C (надо было делать копию всего проекта — надеюсь, он не слишком большой). Еще раз стоп… Часть изменений, которые я внес в экспериментальную версию, могут пригодиться чуть позже: их нельзя потерять (сохраним их под именем “A12_new_4.java” и постараемся не забыть, что означает это головоломное имя). И чем больше файлов в проекте, и чем более экстремальны методы разработки (я про идеи экстремального программирования с их короткими итерациями), тем больше проблем. А как только код будете писать не только вы один, но и еще несколько человек (и, самое замечательное, если они территориально распределены), то проблемы из снежного кома превращаются в лавину, которая сметает все.
При коллективной разработке одна из наиболее часто встречающихся проблем — коллизия изменений. К примеру: программист Вася берет из хранилища файл A.java. Затем в то время, пока Вася сосредоточенно думает над кодом, программист Петя также берет из хранилища тот же файл и вносит в него правки, после чего сохраняет файл назад в репозиторий. И как только это было сделано, Вася завершает редактирование своей версии файла A.java и тоже сохраняет ее, затирая при этом все наработки, выполненные предыдущими участниками. Таким образом, СУВ не только играют роль архива, хранящего историю изменений, но и служат для обнаружения конфликтов и не позволяют кому-либо потерять результаты работы. В общем случае есть две методики обеспечения совместной работы двух людей над одним и тем же файлом без конфликтов. Первый подход называется “Lock-Modify-Unlock” («захвати-отредактируй-освободи»). Здесь, когда программисту Васе нужно выполнить правки в файле, то он его монопольно “захватывает”. Грубо говоря, у каждого файла есть флажок “свободен?” При попытке захвата файла значение этого флажка проверяется, затем, если флажок уже установлен (значит, кто-то успел раньше меня захватить файл), операция захвата завершается неудачей. И наш программист должен ждать, пока кто-то закончит редактирование этого файла, сохранит его, “освободит”, затем повторить попытку захвата и самому продолжить редактирование. Методика очень проста и… плоха. В проектах со “свободным владением кода” возникают две основные проблемы: “забывчивость” и “лишние блокировки”. “Забывчивость” рассмотрим на примере, когда Вася захватил файл и… уехал в отпуск. Значит, мы должны либо ждать, пока Вася вернется из отпуска и разблокирует файл, либо обратиться к администратору репозитория, чтобы он там “что-то подправил руками”. Проблема “лишних блокировок” (мне она кажется признаком других проблем в планировании архитектуры проекта, но раз ее часто выделяют в специальной литературе, то расскажу). Предположим, у нас есть большой файл, где хранится текст программы, который состоит из (условно) двух функций: A и B. Программисту Васе нужно внести изменения в функцию A в то время, как программисту Пете — в функцию B. Хотя правки выполнятся над одним и тем же документом, но над разными его функциями (разными частями файла), а, следовательно, теоретически можно совместить работу Васи и Пети. Система “Lock-Modify-Unlock” это не позволяет. Помните, что СУВ не дают гарантии работоспособного кода. Фактически если код редактируется несколькими программистами, то есть вероятность того, что изменения, сделанные одним из них, будут несовместимыми с изменениями другого программиста (в одном файле или в нескольких — не важно). Повторю еще раз: СУВ надо использовать совместно с другими средствами поддержки жизненного цикла разрабатываемой программы. Например, если заранее написаны тесты, проверяющие работоспособность программы, и эти тесты запускаются регулярно (в идеале - автоматизированно), то менеджер проекта быстро обнаружит, что “программа поломалась”, затем глянет в репозиторий и узнает, кто последний вносил правки, и, соответственно, будет знать, кого “назначить виновным”.
Вторая стратегия организации коллективной работы называется “Copy-Modify-Merge” («скопируй себе, измени код, объедини свои изменения с основным проектом»). Рассмотрим сценарий с общим файлом для редактирования и двумя программистами. Вася взял из репозитория файл (просто взял — никаких запретов для других пользователей репозитория не появилось). Затем, пока Вася думает над кодом, Петя также взял из репозитория файл, быстренько внес в него правки и сохранил обратно в репозиторий. Вася тем временем закончил редактировать файл и хочет сохранить его в репозиторий. А он ему и говорит: мол, так и так, пока ты что-то там делал, файл был изменен другим программистом, и я не позволю тебе сохранить файл до тех пор, пока ты не просмотришь выполненные Петей правки и не отредактируешь файл заново. Такой сценарий вовсе не означает, что программисты только тем и занимаются, что разрешают конфликты и изучают чужой код (кстати, очень неплохое занятие). В практике 99% правок выполняются без конфликтов, и только в оставшемся одном проценте вам требуется встать, подойти к столу, где сидит Петя, и обсудить с ним, что с этим конфликтующим кодом делать дальше. А если вы внесли правки в место, отличное от того, где правил Петя, конфликт разрешается еще проще.
Список доступных для использования СУВ достаточно велик, в нем есть платные и бесплатные продукты. Цели, которые ставятся перед СУВ, совпадают лишь в перечне базовых требований (их я описал выше: “безопасность”, “все сохранено”, “нет конфликтам”). Есть и дополнительные требования, связанные часто со спецификой тех проектов, для управления которыми предназначены конкретные СУВ.
В своей практике я работал только с SVN и Perforce — вот о них и буду рассказывать. Сегодня я начну рассказ о SVN. На самом деле правильнее называть ее не SVN, а Subversion (дело в том, что svn — название главной утилиты, с помощью которой и выполняется работа с Subversion- репозиторием). Итак: Subversion — это централизованная система (ага, есть и нецентрализованные), в которой хранятся файлы проекта и “всякая всячина” о версиях файлов. Кто, когда и что редактировал, также хранится на сервере. Фактически вы можете придти на любую машину, ввести в командной строке одну-единственную строку, и к вам на компьютер будут скопированы нужные для работы файлы проекта. Давайте разберем основную терминологию: есть “SVN-репозиторий и его адрес”, “рабочая копия”, “дерево ревизий”. Чтобы создать репозиторий и разрешить к нему доступ по сети (на самом деле svn-репозиторий может быть и локальным — только для вас и ни для кого более), нам нужны специальные программы (найти их можно на сайте проекта).
Итак, вы скачали и установили СУВ в какую-то папку. Затем вы заходите в папку bin и видите там все нужные для работы как svn-сервера, так и svn- клиента программы (в папке doc находится просто замечательная книжка “svn-book.chm” — настоятельно советую использовать ее как справочник, когда что-то не будет получаться). Для удобства работы я добавил в переменную среды окружения PATH путь к каталогу bin. Начнем мы с создания локального репозитория. Для этого нужно запустить следующую команду (все команды администратора SVN формируются по сходному принципу: “svnadmin команда путь_к_репозиторию”):
svnadmin create SVN2
svnadmin create "h:/docs_xp/SVN/"
Две приведенные команды полностью эквивалентны, однако в первом случае предполагается, что каталог репозитория будет создан как подпапка текущего каталога с именем SVN2. Во втором же случае я указал полный путь к каталогу, где будет храниться репозиторий. Хотя наш репозиторий пуст, давайте сразу разберем несколько полезных функций. Прежде всего, проверка репозитория на целостность (в примере предполагается, что я зашел в каталог репозитория, и поэтому могу использовать в качестве пути к репозиторию точку — текущий каталог).
svnadmin verify .
В случае сбоев вы можете восстановить репозиторий с помощью команды “recover” (а вообще не забывайте о резервном копировании, ведь в одном репозитории будут храниться данные всех программистов и, возможно, не одного проекта, так что смерть жесткого диска сервера будет страшна). Для переноса репозитория из одного места в другое можно выполнить прямое копирование. Хотя это не самый лучший способ, например, из-за возможных “висящих” блокировок файлов или различных версий svn на источнике и приемнике (маловероятно, так как последние версии svn не грешат несовместимостью форматов). Кроме того, в ходе переноса имеет смысл избавиться от части устаревшей информации (например, сохранить только последние 10 правок файлов). В этом случае вы можете использовать функцию предварительного экспорта содержимого репозитория в специальный файл “dump” с последующим восстановлением из этого файла репозитория на новой машине.
svnadmin dump svn > dumpfile
В результате этой команды вы создадите полную копию репозитория внутри файла dumpfile (файл похож на текстовый — всего лишь похож — его содержимое можно просмотреть с помощью того же блокнота, вот только править и сохранять его не стоит). Затем я импортирую данный файл в другой репозиторий.
svnadmin load svn2 < ../dumpfile
В ходе выполнения этой команды на экран выводится журнал выполняемых действий: какие файлы/каталоги были импортированы. Остальные команды svnadmin имеет смысл рассматривать уже после того, как у нас хранилище будет наполнено содержимым. Для этого я создал псевдопроект со структурой, показанной на рис. 1. В корень каталога project я поместил текстовый файл (about.txt) с кратким примечанием к проекту. Теперь необходимо поместить этот каталог в созданный на предыдущем шаге репозиторий. Это и все последующие действия выполняются с помощью утилиты svn.
svn import project file:///h:/docs_xp/SVN/test/project -m"initial import"
Эту команду я выполнил, находясь в каталоге, родительском для project. В ходе импорта внутри хранилища SVN были созданы подкаталоги test и project, куда и были скопированы все файлы проекта. Параметр “-m” служит для указания некоторого текста примечания к создаваемому проекту (комментарий должен быть обязательно). После импорта проекта в репозиторий мы должны его извлечь (этот шаг обязателен). Ради эксперимента удалите каталог project (не бойтесь, все сохранилось) и выполните следующую команду:
svn checkout file:///h:/docs_xp/SVN/test/project
В результате каталог project со всем его содержимым будет воссоздан, а кроме того в каждый из каталогов и подкаталогов проекта еще попадет “странный” каталог с именем “.svn”. Не трогайте его: это служебный каталог, внутри которого хранятся сведения о том, из какого репозитория был загружен проект, сведения о текущей ревизии и так называемая “Pristine Copy”. Создадим рядом с файлом about.txt еще один файл test.txt с каким- либо текстом, затем внесем внутрь файла about.txt пару строчек текста (имитация редактирования) и удалим каталог css/main. Будем считать, что мы сегодня изрядно поработали и теперь хотим сохранить наработки в репозиторий и пойти домой. Команда commit отправляет изменения в репозиторий с некоторым примечанием:
svn commit project -m"first changes"
Что получилось? Да ошибка получилась, вот такая:
svn: Commit failed (details follow):
svn: Directory 'H:\docs_xp\My temps\svzone\project\css\main' is missing
Дело в том, что проект, обслуживаемый svn — это не просто папка с файлами, с которыми вы что хотите, то и делаете — это часть процесса разработки, и просто так удалить файл, каталог, добавить файл и каталог нельзя. Необходимо сообщить svn-репозиторию о своем намерении удалить или добавить файл — иначе никак. Отменяем удаление каталога css/main, удалив весь каталог project и заново извлекая (checkout) его из репозитория. Теперь делаем все правильно: прежде всего, сообщим svn о намерении удалить каталог.
svn delete project/css/main
D project\css\main
Проверим, удалился ли каталог css/main. Нет, каталог остался на месте. Где-то ошибка? Ошибки нет, так как SVN использует понятие “планируемого пожелания”. Команды добавления и удаления файлов/каталогов не приводят к непосредственному изменению вашего проекта. Все эти пожелания планируются к исполнению спустя некоторое время, и для того, чтобы их выполнить, нужно отправить команду commit (подтверждение изменений).
svn commit project -m"deleted dir"
Deleting project\css\main
Committed revision 14.
Нам сообщили, что операция удаления была выполнена, и текущее состояние проекта было сохранено как ревизия номер 14. Стоп. Так что, каждое изменение (даже удаление одного файла или каталога) приводит к созданию новой копии проекта, ведь так репозиторий скоро станет просто огромного размера? Нет, не станет. В SVN используется хитрая методика хранения информации, когда очередная ревизия является не просто копией файла, а определяется его разницей с предыдущим состоянием. Если вы добавили в файл одну строку, то размер репозитория станет больше не на величину файла, а на размер только одной этой строки. Теперь попробуем добавить новый файл в репозиторий. Попытка просто создать файл test.txt и выполнить команду commit завершится… удачей. Так что, я снова где-то обманываю? Нет, все гораздо хитрее. Хотя команда commit завершилась удачно, но кто сказал, что файл test.txt попал в репозиторий? Файла на самом деле нет. Попробуйте для проверки удалить проект и заново извлечь его из репозитория. Файл появился? То-то же, нам нужно после физического создания нового файла сообщить svn, что этот файл тоже является частью проекта и должен быть помещен в репозиторий. Такой подход, несмотря на некоторое неудобство, оправдан, так как часто в состав проекта входят файлы, которые хранить в репозитории не имеет смысла или из-за их гигантского размера, или из-за того, что этот файл формируется динамически, или что- то еще. Пробуем добавить файл и сохранить изменения — теперь все должно получиться:
svn add project/test.txt
A project\test.txt
svn commit project -m"new file"
Adding project\test.txt
Transmitting file data .
Committed revision 15.
Теперь рассмотрим типовой сценарий работы. Утром вы приходите на работу и должны извлечь из репозитория новую версию файлов (возможно, никто не вносил правки в проект с момента вашего вчерашнего ухода, но лучше не рисковать). Для извлечения проекта используется команда “svn checkout” (я про нее рассказал в прошлый раз), но используется эта команда только в первый раз, когда файлы копируются из репозитория, и создаются служебные каталоги “.svn”. Во все последующие разы мы должны использовать другую команду — “update”.
svn update
At revision 15.
В моем примере текущая рабочая копия проекта полностью совпала с содержимым репозитория (мне сказали, какой номер ревизии активен — 15). В случае, если бы в каталоге проекта куда-то “потерялись” файлы или каталоги, svn восстановил бы их из репозитория (в следующем примере я предварительно удалил файлы about, test и каталог banners):
Restored 'about.txt'
Restored 'test.txt'
A images\banners
Помимо буквы A, говорящей “объект был добавлен” (Append), есть и другие буквы-подсказки: D — deleted, U — updated, C — conflicted, G — merged. Если смысл букв A и D ясен, то буква U говорит, что файл был изменен за прошедшее время, и из репозитория к вам на компьютер была загружена последняя его версия. Значение же остальных букв я расскажу попозже. Пока же задумаемся… Стоп … Я ведь сказал, что update синхронизирует состояние проекта с тем, что находится в репозитории. Значит, если я вчера вечером выполнил какие-то правки и забыл их поместить в репозиторий, то утренняя команда update их уничтожит? Нет: чтобы потерять информацию в svn, нужно изрядно постараться. Для примера я внес изменения в один из текстовых файлов и выполнил команду update — и файл не был затерт, из репозитория ничего не было скопировано. Если кто-то создал файл с именем X.java, поместил его в репозиторий, я также создал собственную версию файла с таким же именем (забыл поместить ее в репозиторий и утром хочу извлечь свежую версию проекта), то, опять-таки, операция update не приведет к потере файла:
svn update
svn: Failed to add file 'me.txt': object of the same name already exists
Чтобы дальше разбираться в материале, нам нужно ввести понятие ревизии. Интуитивно понятно, что такое ревизия некоторого документа. Это номер его версии, и чем больше номер, тем этот документ “свежее и актуальнее”. В svn понятие ревизии распространяется не на каждый из документов по отдельности, а на целый проект. Мне это кажется очень удобным, т.к. одновременное существование в одном проекте ревизии 4 для файла A, ревизии 10 для файла B и т.д. несколько сбивает с толку и требует больше “глазного” контроля (хотя подход с раздельной нумерацией документов имеет право на существование и применяется в некоторых СУВ). Всякий раз, когда выполняется команда commit (сохранить изменения в репозитории), всем файлам назначается новый номер ревизии. Сведения о том, какой номер ревизии активен, хранятся внутри служебного каталога “.svn”, там же хранится и так называемая “pristine copy”. Когда я извлекаю update’ом проект, то могу попросить извлечь не самую последнюю версию проекта, а проект под ревизией, например, 10.
svn update -r 14
D test.txt
Updated to revision 14.
Интересный момент в том, что в svn номера ревизий являются сквозными для всех проектов в рамках одного репозитория. Таким образом, возможна ситуация, когда для только что добавленного проекта номер ревизии будет равен 100 или более. Итак, после обновления проекта я начинаю работать с ним, добавляю, удаляю (это мы уже знаем, как делать) и правлю файлы. В текстовый файл about.txt поместите следующую строку “my project”. Затем сохраните изменения в репозиторий (commit) и создайте еще одну локальную копию проекта (выполните команду checkout в другом каталоге). В “копии 1” добавим в конец файла about.txt еще одну строку текста “2-я строка”. Сделаем commit, перейдем в “копию 2”, сделаем update.
svn update
U about.txt
Проверим — да, оба файла теперь содержат только что добавленную строчку текста. Теперь имитируем конфликт: в каждой копии добавим в конец файла еще несколько строк — так, чтобы они выглядели следующим образом:
“Копия 1”:
my project
2-я строка
добавлено в копии 1
“Копия 2”:
my project
2-я строка
добавлено в копии 2
Теперь в каждой из копий сделаем commit. Сначала для “копии 1” — операция выполнится успешно. А вот для “копии 2” возникает ошибка: “Копия 1”:
svn commit -m"test"
Sending about.txt
Transmitting file data .
Committed revision 18.
“Копия 2”:
svn commit -m"test"
Sending about.txt
svn: Commit failed (details follow):
svn: Out of date: '/test/project/about.txt' in transaction '18-1'
Действительно, файл about.txt невозможно сохранить: другой разработчик уже внес правки в него и сохранил в репозиторий. Итак, мы переходим в стадию разрешения конфликта. Прежде всего, я должен загрузить из репозитория свежую версию файла about.txt (и не потерять свои правки). Используя команду update,
svn update
C about.txt
Updated to revision 18.
я получу в каталоге проекта три новых файла about.txt.mine, about.txt.r17, about.txt.r18, и даже файл about.txt был изменен (если его открыть, вы увидите, что старый текст стал чередоваться с какими-то “плюсиками” и “минусиками”). Что хранится в каждом из файлов? Файл about.txt.mine — в нем хранится тот текст файла about.txt, который сделали вы в этой правке, но не можете сохранить в репозиторий. Файлы about.txt.r17, about.txt.r18 хранят текст документа в ревизиях номер 17 и 18 соответственно. Ревизия 17 — это номер ревизии, бывшей актуальной на момент извлечения мною проекта из репозитарий. Ревизия 18 — это номер ревизии, под которой файл был как бы сохранен другим программистом. Файл about.txt содержит уже не оригинальный текст документа, а сведения о правках в формате diff:
my project
<<<<<<< .mine
2-я строка
добавлено в копии 2=======
2-я строка
добавлено в копии 1>>>>>>> .r18
Знаки: “<”, “>”, “=” — это так называемые маркеры конфликта. То, что находится между первыми двумя полосами маркеров — ваши правки, между второй и третьей полосой — правки другого программиста. Затем вы просматриваете файл и определяете, как он должен выглядеть после совместной правки (если вы приняли неверное решение, не страшно — всегда можно вернуться назад к той ревизии, которую отправили до вас). Файл отредактирован, маркеры удалены, и нужно сообщить svn, что конфликт был разрешен. После выполнения команды resolved “лишние файлы” (about.txt.mine, about.txt.r17, about.txt.r18) исчезнут.
svn resolved about.txt
Resolved conflicted state of 'about.txt'
svn commit -m"test"
Sending about.txt
Transmitting file data .
Committed revision 19.
Внимательный читатель заметил, что в приведенном выше примере файла с конфликтными маркерами что-то неладно. Фраза “2-я строка” дублируется два раза и как моя правка, и как правка того, другого, программиста. А если учесть, что эта строка была внесена в репозиторий (был успешно выполнен и commit, и update), еще до возникновения конфликта, то вопросов еще больше. Где нас обманывают? Нигде — нужно, чтобы вы понимали, что поиск различий в файлах возможен только если они являются текстовыми, более того: выполняемый анализ не может быть настолько интеллектуальным, чтобы отследить ситуации правки отдельного слова или символа в строке. Минимальной анализируемой единицей является строка, а строка заканчивается на “символ перевода каретки” или не заканчивается. В примере я забыл поставить после строки “2-я строка” символ ввода. Затем, когда я хотел дописать фразы “добавлено в копии 1” и “добавлено в копии 2”, я поставил этот забытый ввод (на предыдущей строке) и тем самым ее изменил. Svn это обнаружил и сообщил мне. Вернемся к буквам, выводимым командой “update” для каждого из обрабатываемых файлов. Мы узнали, что “C” означает конфликт, и мы должны его разрешить. Теперь осталось разобраться с буквой “G”. Для этого я вернул файл about.txt в изначальное состояние, когда он состоял всего из двух строк (теперь уже не забыв поставить ввод после последней строки). Затем сохранил (commit) изменения и загрузил обновленную версию файла в два проекта. Снова будем пытаться вызывать конфликт. Для этого в первой копии файла перед первой строкой добавим пробел (вот такая минимальная правка), во второй же копии пробел будет добавлен перед второй строкой. Пробуем сделать commit по очереди в этих двух проектах, и, как ожидалось, второй из них завершился неудачей: нам предложили выполнить команду update для обновления текущего состояния проекта. Делаем ее (помните, в прошлый раз именно здесь возникла ошибка), и в этот раз… ошибки нет. Почему нет ошибки, ведь конфликт был? Был, но svn смогла самостоятельно его разрешить. Т.к. правки затрагивали различные места файла (две разные строки), то в общем случае их можно было слить автоматизированно. Так и получилось: новая версия файла about.txt содержит пробелы перед каждой из строк. Вот вам значение буквы G — конфликт был, но мы его разрешили без привлечения внимания программиста (естественно, полученный результат слияния может быть неработоспособен, но это уже задача, решаемая не СУВ, а автоматизированными тестами).
svn update
G about.txt
Важным моментом при работе с SVN является атомарность изменений. Например, вы хотите поместить в репозиторий 10 измененных файлов. Первые 9 из них сохраняются без ошибок, а вот последний вызывает конфликт. Так вот, если хотя бы один файл не может быть сохранен в репозиторий, то и все остальные файлы в него помещены не будут. Так что лениться и откладывать время commit’а на попозже не стоит: скорее всего, через несколько дней вам придется перед отправкой изменений потратить массу времени на разрешение конфликтов. С другой стороны, и отправлять изменения в репозиторий каждые 5 минут (одновременно с нажатием кнопки Save) глупо, так как количество ревизий становится огромным, и, хоть “в svn ничего не теряется”, но найти нужную редакцию файла может стать затруднительным. К тому же, часто для начинающих программистов доступ к репозиторию строится по двухуровневой схеме, когда они не имеют доступа к сохранению информации в репозиторий, а все их правки перед сохранением должен просматривать куратор. Теперь рассмотрим несколько приемов получения статистической информации о репозитории и его содержимом. Простейшая команда “svn info” покажет пути к репозиторию, номер ревизии, сведения о лице, выполнившем в нем последние правки. Хорошей практикой перед отправкой изменений в репозиторий является бегло просмотреть выполненные вами правки. В служебном каталоге .svn хранится pristine copy, т.е. оригинальная версия всех файлов, взятых из репозитория. Они могут быть сравнены с редактированными вами файлами (используйте для этого команду “svn diff”). Результат выполнения команды я не привожу, т.к. выглядит он несколько… неудобочитаемо, зато есть достаточное количество программ, способных красиво отобразить различия между правками. Более того, команда diff очень полезна в ходе расследования истории правок — например, вы хотите посмотреть, как изменился файл about.txt начиная с версии 15.
svn diff -r 15 about.txt
Можно увидеть и чем различаются не ваша рабочая версия файла и произвольная ревизия, а две ревизии между собой:
svn diff -r 15:17 about.txt
В случае, если перед commit’ом вы решили, что ошиблись, и нужно отменить изменения во всех файлах проекта (или в отдельных избранных файлах), вам пригодится команда revert (второй вариант команды отменяет изменения в текущем каталоге и всех вложенных в него, т.е. рекурсивно).
svn revert about.txt
Reverted 'about.txt'
svn revert . --recursive
Пригодится и функция просмотра состояния ваших файлов и того, как они соотносятся с “pristine copy”. Для примера я в “копии 1” внес правки в файл about.txt, сохранил его в репозиторий. Затем в папке “копии 2” я создал новый текстовой файл x.txt, также удалил каталог images. Затем выполняю команду “status”:
svn status
? x.txt
! images
Команда status находит те элементы проекта, которые не совпадают с pristine copy (в ходе этого обращение к серверу svn не требуется) и сообщает нам, что статус файла x.txt не определен (он не добавлен в репозиторий, но это подсказывает значок “?”), а каталог images вообще пропал (значок “!”). Подобных значков довольно много (часть из них совпадает с флажками update), но встречаются они довольно редко. Еще одна функция для определения истории правок — это log. Ее назначение — не вывести изменения в файлах, а распечатать, кто когда выполнил правки, также выводится текст примечания к commit’у (значение параметра -m). Если команда log не содержит параметров, то будут выведены все сведения о правках, в противном случае я могу задать диапазон номеров ревизий:
svn log -r 15:20
svn log — все правки
svn log –r 20:15 — меняем порядок сортировки на обратный
Теперь поговорим про блокировки. В прошлый раз я сказал, что есть две основные методики организации коллективной работы — Lock-Modify-Unlock и Copy-Modify-Merge — и даже раскритиковал первую из них. На самом деле никогда не стоит отвергать какую-либо из идей за ее недостатки, нужно уметь комбинировать сильные стороны каждого из подходов. Идея с обнаружением конфликтов и последующим их разрешением посредством редактирования очень неплоха, но только для текстовых файлов. Если же файлы бинарные (например, картинка), то все становится хуже. Например, Вася и Петя решили нарисовать дизайн коробки для будущей программы. И вот в последний момент, когда все уже почти готово, заказчик звонит Васе и говорит, что нужно в углу коробки нарисовать его, заказчика, портрет. Вася, скрипя душой, принимается за дело. В это же время Пете звонит секретарша заказчика с просьбой добавить в углу коробки надпись “Суперпрога”. Петя извлекает из репозитория файл картинки и начинает “малевать”. Внимание: они рисуют одновременно, не позвонив друг другу (svn — это, конечно, средство для организации коллективной работы, но другие средства никто еще не отменял). Как ожидалось, на стадии сохранения возникла проблема: Вася, закончив первым, сохраняет свой файл, а Петя — нет: возник конфликт. И как его разрешить, непонятно. Т.е., конечно, можно открыть два файла (точнее, три файла, учитывая файл до внесения любых правок) в трех окнах фотошопа и как-то там скомбинировать картинки между собой. Проблемы можно было бы избежать, если бы предварительно два наших художника переговорили друг с другом (ага, легко сказать, а если бы их было 10 человек — представьте, какой объем “говорильни”). Еще лучше, если бы был какой-то “арбитр”, который сказал бы Пете, что сейчас файл картинки занят — над ним работает Вася, и Пете лучше подождать. Одним словом, в SVN входит механизм блокировки файлов. Для блокировки файла about.txt делаем так (предварительно удостоверьтесь с помощью update, что у вас самая последняя версия файла):
svn lock about.txt
'about.txt' locked by user 'Programmer'.
Теперь, когда кто-либо попытается заблокировать этот файл, то получит сообщение об ошибке:
svn lock about.txt
svn: warning: Path '/test/project/about.txt' is already locked by user 'Programmer' in filesystem 'h:/docs_xp/svn/db'
Также, если на момент действия блокировки кто-то еще попытается сохранить изменения, то его ждет сообщение об ошибке:
svn commit -m"test"
Sending about.txt
Transmitting file data .svn: Commit failed (details follow):
svn: Cannot verify lock on path '/test/project/about.txt'; no matching lock-token available
Правки может выполнять только владелец блокировки. Наконец, Вася, завершив работу, решает освободить файл и говорит:
svn unlock about.txt
Но что делать, если Вася блокировал файл и уехал в отпуск? Можно форсировать процедуру блокировки, если указать параметр “--force”.
svn lock about.txt –force
Как только Вася вернется из отпуска и попробует получить статус (svn status) репозитория, то ему сообщат, что его блокировка была “повреждена” (Bad). Проблем это не составляет: нужно выполнить команду update, чтобы сбросить поврежденную блокировку.
Сегодня мы почти разобрались с терминологией SVN, так что в следующий раз, когда я продолжу рассказ о SVN, будет самое время перейти от работы с svn посредством командной строки к более удобным (для некоторых) GUI-клиентам. Еще нужно разобраться со взглядом SVN на понятие ветвей и тегов.
black-zorro@tut.by
Итак, что такое СУВ? СУВ — это программа, которая упрощает работу с постоянно меняющимися файлами (как текстовыми, так и двоичными). Существует некоторое “безопасное” место, где хранятся ваши документы. Каждый раз, когда вы создаете новую версию некоторого файла, он сохраняется не только на вашем компьютере, но и в этом репозитории, и ничего не теряется. «И всего-то?» — скажете вы. Ведь я и сам могу создавать копию своих файлов в конце рабочего дня. Да, можете, но единое место хранения, за которое отвечает специально выделенный сотрудник (системный администратор), гораздо лучше в плане надежности и резервного копирования. Централизованное хранилище кода очень удобно в ситуации, когда вы хотите что-то показать вашему другу, находящемуся в другой комнате: “эй, вот здесь в файле X.java в ревизии номер 999 странная ошибка — ты знаешь, что это может быть?” Ваш друг за пару кликов мышью извлекает из репозитория ваш пример и подсказывает, в чем дело. Это гораздо лучше, чем просить друга расшарить сетевую папку и затем копировать в нее файлы. В практике системы СУВ редко используются сами по себе — часто они интегрируются с другими системами, поддерживающими жизненный цикл разрабатываемой программы: средства моделирования, ведения журналов ошибок, написания документации и тестирования. Однако сегодня я говорю только о СУВ. Централизованное хранилище позволит вам легче “играться” с кодом. Например, вы создали новую версию программы (состоящую из трех файлов: A, B, C). Затем вы решили поэкспериментировать и внести изменения в один из них. Вы создаете резервную копию этого файла и делаете правки, которые затрагивают также файлы B и C (ну забылись вы, с кем не бывает). Затем, “поигравшись”, вы решаете отменить изменения и замещаете файл A старой версией… Стоп, я же забыл сделать копию файлов B и C (надо было делать копию всего проекта — надеюсь, он не слишком большой). Еще раз стоп… Часть изменений, которые я внес в экспериментальную версию, могут пригодиться чуть позже: их нельзя потерять (сохраним их под именем “A12_new_4.java” и постараемся не забыть, что означает это головоломное имя). И чем больше файлов в проекте, и чем более экстремальны методы разработки (я про идеи экстремального программирования с их короткими итерациями), тем больше проблем. А как только код будете писать не только вы один, но и еще несколько человек (и, самое замечательное, если они территориально распределены), то проблемы из снежного кома превращаются в лавину, которая сметает все.
При коллективной разработке одна из наиболее часто встречающихся проблем — коллизия изменений. К примеру: программист Вася берет из хранилища файл A.java. Затем в то время, пока Вася сосредоточенно думает над кодом, программист Петя также берет из хранилища тот же файл и вносит в него правки, после чего сохраняет файл назад в репозиторий. И как только это было сделано, Вася завершает редактирование своей версии файла A.java и тоже сохраняет ее, затирая при этом все наработки, выполненные предыдущими участниками. Таким образом, СУВ не только играют роль архива, хранящего историю изменений, но и служат для обнаружения конфликтов и не позволяют кому-либо потерять результаты работы. В общем случае есть две методики обеспечения совместной работы двух людей над одним и тем же файлом без конфликтов. Первый подход называется “Lock-Modify-Unlock” («захвати-отредактируй-освободи»). Здесь, когда программисту Васе нужно выполнить правки в файле, то он его монопольно “захватывает”. Грубо говоря, у каждого файла есть флажок “свободен?” При попытке захвата файла значение этого флажка проверяется, затем, если флажок уже установлен (значит, кто-то успел раньше меня захватить файл), операция захвата завершается неудачей. И наш программист должен ждать, пока кто-то закончит редактирование этого файла, сохранит его, “освободит”, затем повторить попытку захвата и самому продолжить редактирование. Методика очень проста и… плоха. В проектах со “свободным владением кода” возникают две основные проблемы: “забывчивость” и “лишние блокировки”. “Забывчивость” рассмотрим на примере, когда Вася захватил файл и… уехал в отпуск. Значит, мы должны либо ждать, пока Вася вернется из отпуска и разблокирует файл, либо обратиться к администратору репозитория, чтобы он там “что-то подправил руками”. Проблема “лишних блокировок” (мне она кажется признаком других проблем в планировании архитектуры проекта, но раз ее часто выделяют в специальной литературе, то расскажу). Предположим, у нас есть большой файл, где хранится текст программы, который состоит из (условно) двух функций: A и B. Программисту Васе нужно внести изменения в функцию A в то время, как программисту Пете — в функцию B. Хотя правки выполнятся над одним и тем же документом, но над разными его функциями (разными частями файла), а, следовательно, теоретически можно совместить работу Васи и Пети. Система “Lock-Modify-Unlock” это не позволяет. Помните, что СУВ не дают гарантии работоспособного кода. Фактически если код редактируется несколькими программистами, то есть вероятность того, что изменения, сделанные одним из них, будут несовместимыми с изменениями другого программиста (в одном файле или в нескольких — не важно). Повторю еще раз: СУВ надо использовать совместно с другими средствами поддержки жизненного цикла разрабатываемой программы. Например, если заранее написаны тесты, проверяющие работоспособность программы, и эти тесты запускаются регулярно (в идеале - автоматизированно), то менеджер проекта быстро обнаружит, что “программа поломалась”, затем глянет в репозиторий и узнает, кто последний вносил правки, и, соответственно, будет знать, кого “назначить виновным”.
Вторая стратегия организации коллективной работы называется “Copy-Modify-Merge” («скопируй себе, измени код, объедини свои изменения с основным проектом»). Рассмотрим сценарий с общим файлом для редактирования и двумя программистами. Вася взял из репозитория файл (просто взял — никаких запретов для других пользователей репозитория не появилось). Затем, пока Вася думает над кодом, Петя также взял из репозитория файл, быстренько внес в него правки и сохранил обратно в репозиторий. Вася тем временем закончил редактировать файл и хочет сохранить его в репозиторий. А он ему и говорит: мол, так и так, пока ты что-то там делал, файл был изменен другим программистом, и я не позволю тебе сохранить файл до тех пор, пока ты не просмотришь выполненные Петей правки и не отредактируешь файл заново. Такой сценарий вовсе не означает, что программисты только тем и занимаются, что разрешают конфликты и изучают чужой код (кстати, очень неплохое занятие). В практике 99% правок выполняются без конфликтов, и только в оставшемся одном проценте вам требуется встать, подойти к столу, где сидит Петя, и обсудить с ним, что с этим конфликтующим кодом делать дальше. А если вы внесли правки в место, отличное от того, где правил Петя, конфликт разрешается еще проще.
Список доступных для использования СУВ достаточно велик, в нем есть платные и бесплатные продукты. Цели, которые ставятся перед СУВ, совпадают лишь в перечне базовых требований (их я описал выше: “безопасность”, “все сохранено”, “нет конфликтам”). Есть и дополнительные требования, связанные часто со спецификой тех проектов, для управления которыми предназначены конкретные СУВ.
В своей практике я работал только с SVN и Perforce — вот о них и буду рассказывать. Сегодня я начну рассказ о SVN. На самом деле правильнее называть ее не SVN, а Subversion (дело в том, что svn — название главной утилиты, с помощью которой и выполняется работа с Subversion- репозиторием). Итак: Subversion — это централизованная система (ага, есть и нецентрализованные), в которой хранятся файлы проекта и “всякая всячина” о версиях файлов. Кто, когда и что редактировал, также хранится на сервере. Фактически вы можете придти на любую машину, ввести в командной строке одну-единственную строку, и к вам на компьютер будут скопированы нужные для работы файлы проекта. Давайте разберем основную терминологию: есть “SVN-репозиторий и его адрес”, “рабочая копия”, “дерево ревизий”. Чтобы создать репозиторий и разрешить к нему доступ по сети (на самом деле svn-репозиторий может быть и локальным — только для вас и ни для кого более), нам нужны специальные программы (найти их можно на сайте проекта).
Итак, вы скачали и установили СУВ в какую-то папку. Затем вы заходите в папку bin и видите там все нужные для работы как svn-сервера, так и svn- клиента программы (в папке doc находится просто замечательная книжка “svn-book.chm” — настоятельно советую использовать ее как справочник, когда что-то не будет получаться). Для удобства работы я добавил в переменную среды окружения PATH путь к каталогу bin. Начнем мы с создания локального репозитория. Для этого нужно запустить следующую команду (все команды администратора SVN формируются по сходному принципу: “svnadmin команда путь_к_репозиторию”):
svnadmin create SVN2
svnadmin create "h:/docs_xp/SVN/"
Две приведенные команды полностью эквивалентны, однако в первом случае предполагается, что каталог репозитория будет создан как подпапка текущего каталога с именем SVN2. Во втором же случае я указал полный путь к каталогу, где будет храниться репозиторий. Хотя наш репозиторий пуст, давайте сразу разберем несколько полезных функций. Прежде всего, проверка репозитория на целостность (в примере предполагается, что я зашел в каталог репозитория, и поэтому могу использовать в качестве пути к репозиторию точку — текущий каталог).
svnadmin verify .
В случае сбоев вы можете восстановить репозиторий с помощью команды “recover” (а вообще не забывайте о резервном копировании, ведь в одном репозитории будут храниться данные всех программистов и, возможно, не одного проекта, так что смерть жесткого диска сервера будет страшна). Для переноса репозитория из одного места в другое можно выполнить прямое копирование. Хотя это не самый лучший способ, например, из-за возможных “висящих” блокировок файлов или различных версий svn на источнике и приемнике (маловероятно, так как последние версии svn не грешат несовместимостью форматов). Кроме того, в ходе переноса имеет смысл избавиться от части устаревшей информации (например, сохранить только последние 10 правок файлов). В этом случае вы можете использовать функцию предварительного экспорта содержимого репозитория в специальный файл “dump” с последующим восстановлением из этого файла репозитория на новой машине.
svnadmin dump svn > dumpfile
В результате этой команды вы создадите полную копию репозитория внутри файла dumpfile (файл похож на текстовый — всего лишь похож — его содержимое можно просмотреть с помощью того же блокнота, вот только править и сохранять его не стоит). Затем я импортирую данный файл в другой репозиторий.
svnadmin load svn2 < ../dumpfile
В ходе выполнения этой команды на экран выводится журнал выполняемых действий: какие файлы/каталоги были импортированы. Остальные команды svnadmin имеет смысл рассматривать уже после того, как у нас хранилище будет наполнено содержимым. Для этого я создал псевдопроект со структурой, показанной на рис. 1. В корень каталога project я поместил текстовый файл (about.txt) с кратким примечанием к проекту. Теперь необходимо поместить этот каталог в созданный на предыдущем шаге репозиторий. Это и все последующие действия выполняются с помощью утилиты svn.
svn import project file:///h:/docs_xp/SVN/test/project -m"initial import"
Эту команду я выполнил, находясь в каталоге, родительском для project. В ходе импорта внутри хранилища SVN были созданы подкаталоги test и project, куда и были скопированы все файлы проекта. Параметр “-m” служит для указания некоторого текста примечания к создаваемому проекту (комментарий должен быть обязательно). После импорта проекта в репозиторий мы должны его извлечь (этот шаг обязателен). Ради эксперимента удалите каталог project (не бойтесь, все сохранилось) и выполните следующую команду:
svn checkout file:///h:/docs_xp/SVN/test/project
В результате каталог project со всем его содержимым будет воссоздан, а кроме того в каждый из каталогов и подкаталогов проекта еще попадет “странный” каталог с именем “.svn”. Не трогайте его: это служебный каталог, внутри которого хранятся сведения о том, из какого репозитория был загружен проект, сведения о текущей ревизии и так называемая “Pristine Copy”. Создадим рядом с файлом about.txt еще один файл test.txt с каким- либо текстом, затем внесем внутрь файла about.txt пару строчек текста (имитация редактирования) и удалим каталог css/main. Будем считать, что мы сегодня изрядно поработали и теперь хотим сохранить наработки в репозиторий и пойти домой. Команда commit отправляет изменения в репозиторий с некоторым примечанием:
svn commit project -m"first changes"
Что получилось? Да ошибка получилась, вот такая:
svn: Commit failed (details follow):
svn: Directory 'H:\docs_xp\My temps\svzone\project\css\main' is missing
Дело в том, что проект, обслуживаемый svn — это не просто папка с файлами, с которыми вы что хотите, то и делаете — это часть процесса разработки, и просто так удалить файл, каталог, добавить файл и каталог нельзя. Необходимо сообщить svn-репозиторию о своем намерении удалить или добавить файл — иначе никак. Отменяем удаление каталога css/main, удалив весь каталог project и заново извлекая (checkout) его из репозитория. Теперь делаем все правильно: прежде всего, сообщим svn о намерении удалить каталог.
svn delete project/css/main
D project\css\main
Проверим, удалился ли каталог css/main. Нет, каталог остался на месте. Где-то ошибка? Ошибки нет, так как SVN использует понятие “планируемого пожелания”. Команды добавления и удаления файлов/каталогов не приводят к непосредственному изменению вашего проекта. Все эти пожелания планируются к исполнению спустя некоторое время, и для того, чтобы их выполнить, нужно отправить команду commit (подтверждение изменений).
svn commit project -m"deleted dir"
Deleting project\css\main
Committed revision 14.
Нам сообщили, что операция удаления была выполнена, и текущее состояние проекта было сохранено как ревизия номер 14. Стоп. Так что, каждое изменение (даже удаление одного файла или каталога) приводит к созданию новой копии проекта, ведь так репозиторий скоро станет просто огромного размера? Нет, не станет. В SVN используется хитрая методика хранения информации, когда очередная ревизия является не просто копией файла, а определяется его разницей с предыдущим состоянием. Если вы добавили в файл одну строку, то размер репозитория станет больше не на величину файла, а на размер только одной этой строки. Теперь попробуем добавить новый файл в репозиторий. Попытка просто создать файл test.txt и выполнить команду commit завершится… удачей. Так что, я снова где-то обманываю? Нет, все гораздо хитрее. Хотя команда commit завершилась удачно, но кто сказал, что файл test.txt попал в репозиторий? Файла на самом деле нет. Попробуйте для проверки удалить проект и заново извлечь его из репозитория. Файл появился? То-то же, нам нужно после физического создания нового файла сообщить svn, что этот файл тоже является частью проекта и должен быть помещен в репозиторий. Такой подход, несмотря на некоторое неудобство, оправдан, так как часто в состав проекта входят файлы, которые хранить в репозитории не имеет смысла или из-за их гигантского размера, или из-за того, что этот файл формируется динамически, или что- то еще. Пробуем добавить файл и сохранить изменения — теперь все должно получиться:
svn add project/test.txt
A project\test.txt
svn commit project -m"new file"
Adding project\test.txt
Transmitting file data .
Committed revision 15.
Теперь рассмотрим типовой сценарий работы. Утром вы приходите на работу и должны извлечь из репозитория новую версию файлов (возможно, никто не вносил правки в проект с момента вашего вчерашнего ухода, но лучше не рисковать). Для извлечения проекта используется команда “svn checkout” (я про нее рассказал в прошлый раз), но используется эта команда только в первый раз, когда файлы копируются из репозитория, и создаются служебные каталоги “.svn”. Во все последующие разы мы должны использовать другую команду — “update”.
svn update
At revision 15.
В моем примере текущая рабочая копия проекта полностью совпала с содержимым репозитория (мне сказали, какой номер ревизии активен — 15). В случае, если бы в каталоге проекта куда-то “потерялись” файлы или каталоги, svn восстановил бы их из репозитория (в следующем примере я предварительно удалил файлы about, test и каталог banners):
Restored 'about.txt'
Restored 'test.txt'
A images\banners
Помимо буквы A, говорящей “объект был добавлен” (Append), есть и другие буквы-подсказки: D — deleted, U — updated, C — conflicted, G — merged. Если смысл букв A и D ясен, то буква U говорит, что файл был изменен за прошедшее время, и из репозитория к вам на компьютер была загружена последняя его версия. Значение же остальных букв я расскажу попозже. Пока же задумаемся… Стоп … Я ведь сказал, что update синхронизирует состояние проекта с тем, что находится в репозитории. Значит, если я вчера вечером выполнил какие-то правки и забыл их поместить в репозиторий, то утренняя команда update их уничтожит? Нет: чтобы потерять информацию в svn, нужно изрядно постараться. Для примера я внес изменения в один из текстовых файлов и выполнил команду update — и файл не был затерт, из репозитория ничего не было скопировано. Если кто-то создал файл с именем X.java, поместил его в репозиторий, я также создал собственную версию файла с таким же именем (забыл поместить ее в репозиторий и утром хочу извлечь свежую версию проекта), то, опять-таки, операция update не приведет к потере файла:
svn update
svn: Failed to add file 'me.txt': object of the same name already exists
Чтобы дальше разбираться в материале, нам нужно ввести понятие ревизии. Интуитивно понятно, что такое ревизия некоторого документа. Это номер его версии, и чем больше номер, тем этот документ “свежее и актуальнее”. В svn понятие ревизии распространяется не на каждый из документов по отдельности, а на целый проект. Мне это кажется очень удобным, т.к. одновременное существование в одном проекте ревизии 4 для файла A, ревизии 10 для файла B и т.д. несколько сбивает с толку и требует больше “глазного” контроля (хотя подход с раздельной нумерацией документов имеет право на существование и применяется в некоторых СУВ). Всякий раз, когда выполняется команда commit (сохранить изменения в репозитории), всем файлам назначается новый номер ревизии. Сведения о том, какой номер ревизии активен, хранятся внутри служебного каталога “.svn”, там же хранится и так называемая “pristine copy”. Когда я извлекаю update’ом проект, то могу попросить извлечь не самую последнюю версию проекта, а проект под ревизией, например, 10.
svn update -r 14
D test.txt
Updated to revision 14.
Интересный момент в том, что в svn номера ревизий являются сквозными для всех проектов в рамках одного репозитория. Таким образом, возможна ситуация, когда для только что добавленного проекта номер ревизии будет равен 100 или более. Итак, после обновления проекта я начинаю работать с ним, добавляю, удаляю (это мы уже знаем, как делать) и правлю файлы. В текстовый файл about.txt поместите следующую строку “my project”. Затем сохраните изменения в репозиторий (commit) и создайте еще одну локальную копию проекта (выполните команду checkout в другом каталоге). В “копии 1” добавим в конец файла about.txt еще одну строку текста “2-я строка”. Сделаем commit, перейдем в “копию 2”, сделаем update.
svn update
U about.txt
Проверим — да, оба файла теперь содержат только что добавленную строчку текста. Теперь имитируем конфликт: в каждой копии добавим в конец файла еще несколько строк — так, чтобы они выглядели следующим образом:
“Копия 1”:
my project
2-я строка
добавлено в копии 1
“Копия 2”:
my project
2-я строка
добавлено в копии 2
Теперь в каждой из копий сделаем commit. Сначала для “копии 1” — операция выполнится успешно. А вот для “копии 2” возникает ошибка: “Копия 1”:
svn commit -m"test"
Sending about.txt
Transmitting file data .
Committed revision 18.
“Копия 2”:
svn commit -m"test"
Sending about.txt
svn: Commit failed (details follow):
svn: Out of date: '/test/project/about.txt' in transaction '18-1'
Действительно, файл about.txt невозможно сохранить: другой разработчик уже внес правки в него и сохранил в репозиторий. Итак, мы переходим в стадию разрешения конфликта. Прежде всего, я должен загрузить из репозитория свежую версию файла about.txt (и не потерять свои правки). Используя команду update,
svn update
C about.txt
Updated to revision 18.
я получу в каталоге проекта три новых файла about.txt.mine, about.txt.r17, about.txt.r18, и даже файл about.txt был изменен (если его открыть, вы увидите, что старый текст стал чередоваться с какими-то “плюсиками” и “минусиками”). Что хранится в каждом из файлов? Файл about.txt.mine — в нем хранится тот текст файла about.txt, который сделали вы в этой правке, но не можете сохранить в репозиторий. Файлы about.txt.r17, about.txt.r18 хранят текст документа в ревизиях номер 17 и 18 соответственно. Ревизия 17 — это номер ревизии, бывшей актуальной на момент извлечения мною проекта из репозитарий. Ревизия 18 — это номер ревизии, под которой файл был как бы сохранен другим программистом. Файл about.txt содержит уже не оригинальный текст документа, а сведения о правках в формате diff:
my project
<<<<<<< .mine
2-я строка
добавлено в копии 2=======
2-я строка
добавлено в копии 1>>>>>>> .r18
Знаки: “<”, “>”, “=” — это так называемые маркеры конфликта. То, что находится между первыми двумя полосами маркеров — ваши правки, между второй и третьей полосой — правки другого программиста. Затем вы просматриваете файл и определяете, как он должен выглядеть после совместной правки (если вы приняли неверное решение, не страшно — всегда можно вернуться назад к той ревизии, которую отправили до вас). Файл отредактирован, маркеры удалены, и нужно сообщить svn, что конфликт был разрешен. После выполнения команды resolved “лишние файлы” (about.txt.mine, about.txt.r17, about.txt.r18) исчезнут.
svn resolved about.txt
Resolved conflicted state of 'about.txt'
svn commit -m"test"
Sending about.txt
Transmitting file data .
Committed revision 19.
Внимательный читатель заметил, что в приведенном выше примере файла с конфликтными маркерами что-то неладно. Фраза “2-я строка” дублируется два раза и как моя правка, и как правка того, другого, программиста. А если учесть, что эта строка была внесена в репозиторий (был успешно выполнен и commit, и update), еще до возникновения конфликта, то вопросов еще больше. Где нас обманывают? Нигде — нужно, чтобы вы понимали, что поиск различий в файлах возможен только если они являются текстовыми, более того: выполняемый анализ не может быть настолько интеллектуальным, чтобы отследить ситуации правки отдельного слова или символа в строке. Минимальной анализируемой единицей является строка, а строка заканчивается на “символ перевода каретки” или не заканчивается. В примере я забыл поставить после строки “2-я строка” символ ввода. Затем, когда я хотел дописать фразы “добавлено в копии 1” и “добавлено в копии 2”, я поставил этот забытый ввод (на предыдущей строке) и тем самым ее изменил. Svn это обнаружил и сообщил мне. Вернемся к буквам, выводимым командой “update” для каждого из обрабатываемых файлов. Мы узнали, что “C” означает конфликт, и мы должны его разрешить. Теперь осталось разобраться с буквой “G”. Для этого я вернул файл about.txt в изначальное состояние, когда он состоял всего из двух строк (теперь уже не забыв поставить ввод после последней строки). Затем сохранил (commit) изменения и загрузил обновленную версию файла в два проекта. Снова будем пытаться вызывать конфликт. Для этого в первой копии файла перед первой строкой добавим пробел (вот такая минимальная правка), во второй же копии пробел будет добавлен перед второй строкой. Пробуем сделать commit по очереди в этих двух проектах, и, как ожидалось, второй из них завершился неудачей: нам предложили выполнить команду update для обновления текущего состояния проекта. Делаем ее (помните, в прошлый раз именно здесь возникла ошибка), и в этот раз… ошибки нет. Почему нет ошибки, ведь конфликт был? Был, но svn смогла самостоятельно его разрешить. Т.к. правки затрагивали различные места файла (две разные строки), то в общем случае их можно было слить автоматизированно. Так и получилось: новая версия файла about.txt содержит пробелы перед каждой из строк. Вот вам значение буквы G — конфликт был, но мы его разрешили без привлечения внимания программиста (естественно, полученный результат слияния может быть неработоспособен, но это уже задача, решаемая не СУВ, а автоматизированными тестами).
svn update
G about.txt
Важным моментом при работе с SVN является атомарность изменений. Например, вы хотите поместить в репозиторий 10 измененных файлов. Первые 9 из них сохраняются без ошибок, а вот последний вызывает конфликт. Так вот, если хотя бы один файл не может быть сохранен в репозиторий, то и все остальные файлы в него помещены не будут. Так что лениться и откладывать время commit’а на попозже не стоит: скорее всего, через несколько дней вам придется перед отправкой изменений потратить массу времени на разрешение конфликтов. С другой стороны, и отправлять изменения в репозиторий каждые 5 минут (одновременно с нажатием кнопки Save) глупо, так как количество ревизий становится огромным, и, хоть “в svn ничего не теряется”, но найти нужную редакцию файла может стать затруднительным. К тому же, часто для начинающих программистов доступ к репозиторию строится по двухуровневой схеме, когда они не имеют доступа к сохранению информации в репозиторий, а все их правки перед сохранением должен просматривать куратор. Теперь рассмотрим несколько приемов получения статистической информации о репозитории и его содержимом. Простейшая команда “svn info” покажет пути к репозиторию, номер ревизии, сведения о лице, выполнившем в нем последние правки. Хорошей практикой перед отправкой изменений в репозиторий является бегло просмотреть выполненные вами правки. В служебном каталоге .svn хранится pristine copy, т.е. оригинальная версия всех файлов, взятых из репозитория. Они могут быть сравнены с редактированными вами файлами (используйте для этого команду “svn diff”). Результат выполнения команды я не привожу, т.к. выглядит он несколько… неудобочитаемо, зато есть достаточное количество программ, способных красиво отобразить различия между правками. Более того, команда diff очень полезна в ходе расследования истории правок — например, вы хотите посмотреть, как изменился файл about.txt начиная с версии 15.
svn diff -r 15 about.txt
Можно увидеть и чем различаются не ваша рабочая версия файла и произвольная ревизия, а две ревизии между собой:
svn diff -r 15:17 about.txt
В случае, если перед commit’ом вы решили, что ошиблись, и нужно отменить изменения во всех файлах проекта (или в отдельных избранных файлах), вам пригодится команда revert (второй вариант команды отменяет изменения в текущем каталоге и всех вложенных в него, т.е. рекурсивно).
svn revert about.txt
Reverted 'about.txt'
svn revert . --recursive
Пригодится и функция просмотра состояния ваших файлов и того, как они соотносятся с “pristine copy”. Для примера я в “копии 1” внес правки в файл about.txt, сохранил его в репозиторий. Затем в папке “копии 2” я создал новый текстовой файл x.txt, также удалил каталог images. Затем выполняю команду “status”:
svn status
? x.txt
! images
Команда status находит те элементы проекта, которые не совпадают с pristine copy (в ходе этого обращение к серверу svn не требуется) и сообщает нам, что статус файла x.txt не определен (он не добавлен в репозиторий, но это подсказывает значок “?”), а каталог images вообще пропал (значок “!”). Подобных значков довольно много (часть из них совпадает с флажками update), но встречаются они довольно редко. Еще одна функция для определения истории правок — это log. Ее назначение — не вывести изменения в файлах, а распечатать, кто когда выполнил правки, также выводится текст примечания к commit’у (значение параметра -m). Если команда log не содержит параметров, то будут выведены все сведения о правках, в противном случае я могу задать диапазон номеров ревизий:
svn log -r 15:20
svn log — все правки
svn log –r 20:15 — меняем порядок сортировки на обратный
Теперь поговорим про блокировки. В прошлый раз я сказал, что есть две основные методики организации коллективной работы — Lock-Modify-Unlock и Copy-Modify-Merge — и даже раскритиковал первую из них. На самом деле никогда не стоит отвергать какую-либо из идей за ее недостатки, нужно уметь комбинировать сильные стороны каждого из подходов. Идея с обнаружением конфликтов и последующим их разрешением посредством редактирования очень неплоха, но только для текстовых файлов. Если же файлы бинарные (например, картинка), то все становится хуже. Например, Вася и Петя решили нарисовать дизайн коробки для будущей программы. И вот в последний момент, когда все уже почти готово, заказчик звонит Васе и говорит, что нужно в углу коробки нарисовать его, заказчика, портрет. Вася, скрипя душой, принимается за дело. В это же время Пете звонит секретарша заказчика с просьбой добавить в углу коробки надпись “Суперпрога”. Петя извлекает из репозитория файл картинки и начинает “малевать”. Внимание: они рисуют одновременно, не позвонив друг другу (svn — это, конечно, средство для организации коллективной работы, но другие средства никто еще не отменял). Как ожидалось, на стадии сохранения возникла проблема: Вася, закончив первым, сохраняет свой файл, а Петя — нет: возник конфликт. И как его разрешить, непонятно. Т.е., конечно, можно открыть два файла (точнее, три файла, учитывая файл до внесения любых правок) в трех окнах фотошопа и как-то там скомбинировать картинки между собой. Проблемы можно было бы избежать, если бы предварительно два наших художника переговорили друг с другом (ага, легко сказать, а если бы их было 10 человек — представьте, какой объем “говорильни”). Еще лучше, если бы был какой-то “арбитр”, который сказал бы Пете, что сейчас файл картинки занят — над ним работает Вася, и Пете лучше подождать. Одним словом, в SVN входит механизм блокировки файлов. Для блокировки файла about.txt делаем так (предварительно удостоверьтесь с помощью update, что у вас самая последняя версия файла):
svn lock about.txt
'about.txt' locked by user 'Programmer'.
Теперь, когда кто-либо попытается заблокировать этот файл, то получит сообщение об ошибке:
svn lock about.txt
svn: warning: Path '/test/project/about.txt' is already locked by user 'Programmer' in filesystem 'h:/docs_xp/svn/db'
Также, если на момент действия блокировки кто-то еще попытается сохранить изменения, то его ждет сообщение об ошибке:
svn commit -m"test"
Sending about.txt
Transmitting file data .svn: Commit failed (details follow):
svn: Cannot verify lock on path '/test/project/about.txt'; no matching lock-token available
Правки может выполнять только владелец блокировки. Наконец, Вася, завершив работу, решает освободить файл и говорит:
svn unlock about.txt
Но что делать, если Вася блокировал файл и уехал в отпуск? Можно форсировать процедуру блокировки, если указать параметр “--force”.
svn lock about.txt –force
Как только Вася вернется из отпуска и попробует получить статус (svn status) репозитория, то ему сообщат, что его блокировка была “повреждена” (Bad). Проблем это не составляет: нужно выполнить команду update, чтобы сбросить поврежденную блокировку.
Сегодня мы почти разобрались с терминологией SVN, так что в следующий раз, когда я продолжу рассказ о SVN, будет самое время перейти от работы с svn посредством командной строки к более удобным (для некоторых) GUI-клиентам. Еще нужно разобраться со взглядом SVN на понятие ветвей и тегов.
black-zorro@tut.by
Сетевые решения. Статья была опубликована в номере 05 за 2008 год в рубрике программирование