...
...

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

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

В прошлый раз мы остановились на том, что создали репозиторий, поместили в него (с помощью команды 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 номера ревизий являются "сквозными" для всех проектов в рамках одного репозитория. Таким образом, возможна ситуация, когда для только что добавленного проекта номер ревизии будет равен 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" покажет пути к репозиторию, номер ревизии, сведения о лице, выполнившем в нем последние правки (см. рис. 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 на понятие ветвей и тегов.

black-zorro@tut.by, black-zorro.com

© Компьютерная газета

полезные ссылки
Аренда ноутбуков
Ремонт ноутбуков в центре Минска