Системы управления версиями для программистов и не только. Часть 2

April 19, 2008

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

В прошлый раз мы остановились на том, что создали репозиторий, поместили в него (с помощью команды import) каталог с проектом и выучили, что даже такие простые действия как удаление и добавление файлов не может быть выполнены в отрыве от репозитория (нельзя просто удалить файл – нужно попросить репозиторий сделать это). Хотя я рассказываю о командах работающих с репозиторием в командной строке, но, естественно, что есть достаточно большое количество визуальных клиентов, о которых я также должен буду рассказать – но позже. Пока же рассмотрим типовой сценарий работы. Утром вы приходите на работу и должны извлечь из репозитория новую версию файлов (возможно, что никто не вносил правки в проект с момента вашего вчерашнего ухода, но лучше не рисковать). Для извлечения проекта используется команда “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 номера ревизий являются “сквозными” для всех проектов в рамках одного репозитория. Таким образом, возможна ситуация, когда для только что добавленного проекта номер ревизии будет равен ста или более.

Итак, после обновления проекта я начинаю работать с ним, добавляю, удаляю (это мы уже знаем, как делать) и правлю файлы. В текстовый файл 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” покажет пути к репозиторию, номер ревизии, сведения о лице, выполнившем последние правки в нем (см. рис. 1).



Хорошей практикой перед отправкой изменений в репозиторий является бегло просмотреть выполненные вами правки. В служебном каталоге “.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 на понятие ветвей и тегов.