Параллельные вычисления в ИММ УрО РАН
 
 

Введение в CVS

Александр Владимирович Коновалов
avkon@imm.uran.ru


Contents

Управление версиями -- зачем это надо

Не требуется большого опыта программирования, чтобы понять -- наиболее чреваты ошибками окрестности недавно поправленных мест. Естественно, что любая сколько-нибудь осмысленная программистская деятельность предполагает: учёт правок, запоминание того, кто правил, комментирование смысла правки. Главное при такой деятельности -- порядок в голове разработчика. Если такой порядок есть, то остальное -- лишь вопрос инструментальных средств. Если нету -- любой инструментарий окажется бесполезен.

Как обычно бывает, всех целей вполне можно добиться просто грамотным использованием текстового редактора. Достаточно окружать правку комментариями специального вида1, не выбрасывать старые варианты, писать дату в формате 020815 и пр. Естественно, что со временем наиболее интересные места исходников читать будет просто невозможно из-за изобилия старых вариантов, но вот тогда можно сделать генеральную уборку и выкинуть старую рухлядь. Всё можно делать руками, и никаких проблем!

Ещё одна, схожая, задача -- слияние правок, сделанных независимо и поступивших из разных источников. Предположим, что вы получили нечто большое и страшное в исходных текстах, и моментально подправили это в соотвествии со своим виденем своих потребностей. Но оригинальный автор не успокоился, отдав вам исходные тексты, а продолжил развитие. Через некоторое время возникнет задача увязать то интересное2, что он понаписал, с вашими правками. Здесь, по-видимому, не обойдёшься просто текстовым редактором, лучше взять как минимум нечто вроде diff(1).

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

Инструментальное средство, которое мы будем разглядывать -- система управления версиями cvs (Concurrent Versions System), ставшая стандартом при разработке свободного ПО3, широко используемая много где ещё, доступная для Win32 и разных Юниксов, и распространяемая под GPL.

Ресурсы

В комплект поставки входит весьма неплохой CVS manual by Per Cederqvist. Его (пристойный) русский перевод можно обнаружить на http://alexm.here.ru/, вместе с ещё другими околоCVSными текстами.

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

По-английски рассматриваемая область деятельности называется Software Configuration Management. Знание ключевых слов позволяет умеющим пользоваться Интернетом найти много разнообразных текстов по SCM.

Базовые представления

Система устроена ``клиент-серверным'' образом: где-то далеко есть хранилище всей истории системы, называемое репозиторий, многочисленные разработчики паралллельно звонят в него, говорят ``дай мне текущую версию'' или ``я вот что написал, сделай ЭТО доступным всем желающим''. Способы звонения могут быть самыми разными, различие между ними важно с точки зрения безопасности, но по предоставляемым возможностям эквивалентно (кратко о доступе -- в разд. 10). Отметим по ходу дела, что CVS вполне себе работает по безобразным TCP-линиям.

В простейшем варианте всё просто и понятно:

cvs checkout pvfs
дозвонится до репозитория, выберет оттуда свежую версию проекта pvfs, и создаст в текущем каталоге соответствующие файлики. Затем начинается самая интересная часть, которая нас в данном случае не интересует. Исправления Ужасных Ошибок нужно сделать доступным всем, для этого выполняем
cvs commit
откуда-то изнутри проекта4. Теперь CVS запустит ваш любимый текстовый редактор5 и попросит объяснить смысл правок.

Иногда, прежде чем объяснять, полезно вспомнить, что же вы накроили. Это можно сделать, выполнив

cvs diff -u
В выдаче будет содержаться нечто наподобие
Index: include/req.h
===============================================================
RCS file: /home/u1305/cvsroot/pvfs/include/req.h,v
retrieving revision 1.1.1.1
diff -u -r1.1.1.1 req.h
--- include/req.h	12 Aug 2002 10:00:19 -0000	1.1.1.1
+++ include/req.h	18 Aug 2002 13:07:39 -0000
@@ -42,8 +42,9 @@
 #define MGR_NOOP     21
 #define MGR_LOOKUP   22
 #define MGR_CTIME    23
+#define MGR_IOD_DIE  24
 
-#define MAX_MGR_REQ  23
+#define MAX_MGR_REQ  24
 
 /* structure for request to manager */
 typedef struct mreq mreq, *mreq_p;
Формат совершнно понятный: - в первой позиции означает ``выбросить'', + -- добавить, а если там пусто -- строчка есть и в старом, и в новом варианте.

Поразглядывать историю файла можно, выполнив cvs log aaa.c. В ответ CVS выдаст нечто вроде

----------------------------
revision 1.3
date: 1999/08/13 23:26:23;  author: rbross;  state: Exp;  lines: +23 -27
Major cosmetic changes resulting from -Wall output.
----------------------------
revision 1.2
date: 1999/08/13 16:04:52;  author: rbross;  state: Exp;  lines: +6 -37
Reduces log output.
----------------------------
revision 1.1
date: 1999/08/10 17:11:29;  author: rbross;  state: Exp;
branches:  1.1.1;
Initial revision
----------------------------
revision 1.1.1.1
date: 1999/08/10 17:11:29;  author: rbross;  state: Exp;  lines: +0 -0
Original 1.3.3 sources
----------------------------
revision 1.44.2.2
date: 2001/12/07 23:19:14;  author: rbross;  state: Exp;  lines: +51 -10
Patch to fix incorrect stat values in the presence of holes in files.
----------------------------
revision 1.44.2.1
date: 2001/12/07 03:29:58;  author: rbross;  state: Exp;  lines: +2 -1
Patch to set dsize to 0 on IOD_TRUNCATE messages.
========================================================================

Если вы правите локальную копию достаточно долго, другие разработчики, возможно, успеют всё поменять. Чтобы привести локальную копию в соответствие с текущим содержимым репозитория, используется

cvs update
Локальные правки при этом, конечно же, не потеряются, их судьба достаточно интересна и заслуживает отдельного (следующего) раздела.

Довольно важный вопрос: как часто делать commit? Здесь встречаются подходы в диапазоне от ``Не компилируется -- и ладно, зато МЫСЛИ есть'' до ``Все тесты идут -- пора коммитить''. Сам я ориентируюсь на ощущение: захочу ли я увидеть этот вариант однажды в будущем? Необходимо понимать, что репозиторий -- это (идеализированно говоря) такое хранилище, куда добавить можно, а вот убрать оттуда ничего нельзя.

Параллельные разработчики

Проблема понятна: если мы хотим обеспечить одновременную работу нескольких разработчиков над одним проектом, нам нужно каким-то образом обработать случай, когда двое правят одно и то же. Самое простое решение здесь -- пока один правит, остальные тихо ждут своей очереди6. Ударный момент CVS -- как раз отказ от такого естественного решения, настолько ударный, что он и в название (concurrent) попал. А именно, конфликты не предупредаются, а разрешаются уже после возникновения.

Рассмотрим, во что это выливается в терминах командочек. Пусть 2 разработчика одновременно поправили одно и то же. Об этом они узнают, когда попытаются выполнить commit. Точнее, поскольку репозиторий-то один, один из разработчиков успеет первым, а узнает о конфликте лишь оказавшийся вторым. Когда он в свою очередь попытается зафиксировать изменения, ему объяснят, что копия у него -- старая, и надо бы её обновить (cvs commit: Up-to-date check failed for `include/req.h'). Попытку разрешения конфликтов предпримет update, полем для экспериментов будет рабочая локальная копия 2-го разработчкика (репозиторий про конфликт не узнает). cvs работает пофайлово, поэтому если правлены разные файлы, проблем (с точки зрения CVS) нет вовсе. Если правлен один и тот же файл, но в разных местах, CVS выполнит слияние (merge) изменений. Если же правилось одно и то же место, CVS сообщит о конфликте

Merging differences between 1.1.1.1 and 1.2 into req.h
rcsmerge: warning: conflicts during merge
cvs update: conflicts found in include/req.h
C include/req.h
, поместив в исходник нечто наподобие
#define MGR_NOOP     21
#define MGR_LOOKUP   22
#define MGR_CTIME    23
<<<<<<< req.h
#define MGR_SUBTRACT 24
=======
#define MGR_IOD_DIE  24
>>>>>>> 1.2

#define MAX_MGR_REQ  24

/* structure for request to manager */
typedef struct mreq mreq, *mreq_p;
Конечно же, отсутствие конфликтов с точки зрения CVS и даже транслятора не означает, что семантика не порушилась. Проверка этого, возможно нетривиальная, сваливается на 2-го разработчика. После того, как катаклизм (в каком-то приближении) ликвидирован, он вновь попытается сказать cvs commit (2-ой разработчик может не успеть и 2-ой раз, перед нами распределённые читатели-писатели...).

Прелести версионирования перед блокировщиками можно осознать, например, привлекая следующие соображения:

  • Разработчик захватил файл для изменеий и:
    • отправился пить кофе, либо
    • канал до него упал.
    Работа остальных будет стоять, а зачем, спрашивается?
  • Если многие рвутся править одно и тоже, кооперация в команде, наверное, не на высоте.
Никто, впрочем, не пытается утверждать, что блокировщики не имеют права на существование, о реализации блокирующего поведения в рамках CVS см. раздел 7.

Ревизии и теги

Повозившись с commit-ами, легко увидеть, что при коммитах изменяется (возрастает) некий параметр под названием ревизия (revision). Таким образом, если файл меняется часто, то его номер ``убежит вперед'' по сравнению со стабильными кусками. Это сугубо внутреннее дело CVS и никого не волнует, пока не возникает Внешний Мир. Предположим, что какой-то вариант системы мы передали счастливому заказчику. Через некоторое время заказчик сообщит нам, что релиз содержит Ужасные Ошибки. Проблема здесь в том, что мы знаем ревизии переданных файлов, и сами файлы у нас, естественно, есть, но вот обращаться к ним совершенно неудобно. Для решения проблемы в CVS введена возможность давать набору файлов символические имена -- теги. Это делается так: выполнив cvs tag REL-1-0 изнутри локальной копии, мы присвоили тег REL-1-0 ревизиям файлов, которые там находились. В сущности, для этой операции локальная копия не очень и нужна: команда cvs rtag -D "1 hour ago" STABLE-1-1 c-project присвоит тег STABLE-1-1 ревизиям, бывшим наиболее свежими час назад. Ключу -D можно указывать и нормальные даты, например, 16 Sep. Если таймзона не указана явно, она считается локальной7.

Выбрать файлы по тегу можно, выполнив cvs checkout -r REL-1-0. Теперь будет получена не последняя версия, а соотвествующая тегу REL-1-0. Ключ -r можно использовать ещё много где, например cvs diff -u -r REL-1-0 -r REL-1-1 сравнит релизы 1.0 и 1.18.

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

Ветви и слияния

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

Ветвь можно создать на основе существующего тега, вот так cvs rtag -b -r STABLE-1-10 STABLE-1-10-PATCHES pvfs Другой способ -- на основе содержимого рабочего каталога, вот так: cvs tag -b. После этого репозиторий узнает про ветвь. Но текущая рабочая копия, из которой получена ветвь, отнюдь не окажется автоматический на ветви. Это -- весьма тонкий место, и его необходимо осознать: с точки зрения пользовательских файлов в начальный момент ветвь ничем не отличается от основного дерева, и если при checkout-е не указать явно (вот так: cvs checkout -r IAR-1 pvf), что мы хотим оказаться на ветви, мы там и не окажемся.

Обычные теги, и теги, обозначающие ветви, ведут себя по-разному при редактировании файлов: обычный тег -- это постоянная веха где-то позади, а тег, образующий ветви, движется вместе с меняющимися файлами, что, собственно, и позволяет идентифицировть ветвь (для обращения к ветви IAR-1 неизменно нужно указывать этот тег, как бы она не ``уросла'' вперёд).

Слияние выполняется так: в существующей рабочей копии выполняем cvs update -j IAR-1. Теперь рабочая копия будет содержать правки из IAR-1, но принадлежать по-прежнему к той ветви, что и до слияниия. Слияние опять-таки производится не в репозитории, и разработчик должен решить, когда получившееся созреет до commit-а.


Реализация политики исключительного владения

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

Идея с исключительным владением такая: наблюдаемые (cvs watch on file.c) файлы извлекаются из репозитория read-only, а если нужно подправить, то потребуется вызвать cvs edit file.c. После commit-а захват ``для записи'' будет снят.

Спецпеременные

Часто в прошлом, разглядывая чужие исходники, я удивлялся: и не лень людям писать шапку навроде

 * $Log: cvs.tex,v $
 * Revision 1.1.1.1  2002/08/23 04:22:06  u1305
 * Import
 *
 * Revision 1.6  2002/01/28 23:31:48  rbross
 * Applied patch from Christopher Bilderback <cbilder@umr.edu> to fix
 * potential zero buffer size problem in u2p.
 *
 * Revision 1.5  2001/01/30 23:32:25  pcarns
 * Changed code headers to point to COPYING file in toplevel directory for
 * GPL.
 *
 * Revision 1.4  2000/10/30 14:51:59  rbross
 * Took out all the glibc files, since we're not using them any more.
 * Removed instances of syscall() all over the place.
 *
 * Revision 1.3  2000/10/03 21:33:13  rbross
 * These files implement 64-bit access from the client using the 64-bit
 * calls pvfs_lseek64, pvfs_read, and pvfs_open, pvfs_write, and
 * pvfs_close.  I don't know if anything else works.
Оказывается, всё-таки лень, это пишут не люди, а автоматический механизм.

Идея состоит в том, что когда CVS видит нечто наподобие $KEYWORD$, она заменяет его на $KEYWORD:VALUE$. Задача программиста -- обеспечить осмысленное использование вставленного текста.

На мой взгляд, именно с осмысленностью у $Log: cvs.tex,v $ не всё в порядке. Зачем мне в начале файла длинно рассказывают о изменениях исходников, которых в этом файле нет (они же изменились!). Если захочется посмотреть на историю файла (это, безусловно, полезно), в наших руках cvs diff, cvs log и пр., ну а что двусточным сообщениям в исходнике делать?

Назначение переменной $Header$ -- быть заменённой на полное имя файла, текущую ревизию, имя последнего разработчика, менявшего файл и т.п. Практически это используется так:

static char iod_c_hdr[] = 
  "$Header: /home/u1305/cvsroot/c-project/cvs.tex,v 1.1.1.1 2002/08/23 04:22:06 u1305 Exp $";
После этого по бинарнику легко установить, из каких он текстов собран (см. ident(1)).

У того, что CVS шибко умный, есть и обратная сторона -- если мы поместим туда JPG-картинку, а в ней по случайности окажется что-нибудь вроде $Header$, то внешний вид картинки пострадает. Чтобы замена не происходила, при добавлении файла нужно указывать ключ -ko. Ещё одна проблема связана с концами строк. Чтобы при извлечении в Windows не появлялся возврат каретки, нужно добавлять с -kb9.

Работа с файлами как целым

Для того, чтобы поместить целый проект под управление CVS, используется cvs import. Для того, чтобы добавить файл, применяется cvs add file.c. При этом файл не помещается в репозиторий, реальное помещение произойдёт лишь после commit-а. Удаление файлов устроено тоже 2-ступенчато: сначала cvs remove, а потом cvs commit.

Переименование файлов непосредственно не поддержано: можно либо удалить старый и добавить новый (при этом у нового история начнётся с начала), либо переименовать файл в репозитории (перестанут собираться старые версии), либо создать копию файла в репозитории и cvs remove старый из текущей ревизии.

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


Способы доступа к репозиторию

Способ доступа берётся из переменной окружения CVSROOT, либо из параметра -d командной строки вызова cvs10. Выглядит это примерно так: :extssh:u1305@localhost:/usr/local/cvsroot. Здесь extssh -- способ доступа, а дальше идут пользователь, машина и путь на этой машине к репозиторию.

В наиболее простом случае репозиторий доступен просто как локальный каталог на машине, где живёт разработчик. Способ доступа тогда -- :local:, что можно не писать, и получится просто export CVSROOT=/usr/local/cvsroot. При удалённом доступе есть, грубо говоря, 2 сорта проверки пользователя: силами CVS (:pserver:), либо системой (:extssh: как раз из таких). :pserver: -- стандартный способ read-only доступа к разного рода свободным проектам в интернете. Для более сложных задач его лучше не использовать -- с точки зрения защищённости он организован в разгильдяйских традициях начальных лет Интернета, когда он был маленьким и все всех любили: и сами исходники, и пароли (УЖАС!) передаются открытым текстом. Защищённость :extssh:, напротив, на высоте, единственно, я не понимаю, как под Win32 обустроить сервер, реализующий такой доступ (впрочем, я и не пытался). Если вы культивируете CVS в надёжно за-firewall-енной локальной сети, и уверены во вменяемости всех её обитателей, то, видимо, особых причин не использовать :pserver: нет.

Обвязки вокруг CVS

Выше мы размышляли о CVS в терминах командной строки. Это не значит, что никак иначе с CVS работать нельзя. Существуют как stand-along окошечные обвязки (например, http://www.wincvs.org/), так и средства интеграции CVS с популярными средами разработки. Ни на чём не настаивая, хотелось бы поделиться собственной практикой. Из интегрированных сред почему-то получается говорить cvs commit и, в некоторых случаях11, разглядывать diff-ы. Всё остальное хочется делать в командной строке.

Иные средства управления версиями

Проблема здесь в том, что CVS я довольно долго пользуюсь, а о всем остальном лишь рассказки читал.

Практика

Коллективная правка разных мест одной и той же Явовской программы.



Footnotes

... вида1
Чтобы они легко искались, например, grep-ом.
... интересное2
Именно интересное, а не всё.
... ПО3
Одно значительное исключение известно всем, поэтому его можно проигнорировать.
... проекта4
В Некоторых Других Системах Управления Версиями это действие называется check-in. В ранних версиях CVS название было таким же, это его потом переименовали. Проблема понятна: check-in и check-out легко перепутать, с понятными последствиями (к сожалению, автору эта опасность известна не по наслышке).
... ре\-дак\-тор5
Если Вы не знаете, какой редактор Ваш любимый, то тут-то и узнаете. Чтобы не попасть в неудобное положение, заранее научитесь выходить из него.
... очереди6
Если у читателя в этом месте возникли мысли о СУБД-блокировочниках и настоящих, а не из книжки, читателях-писателях, то это Правильно.
... локальной7
Понимающие люди писали. Надеюсь, ясно, почему хранить время надо в UTC?
... 1.18
Во всех diff-ах я упорно пишу -u, но ни на чём не настаиваю. Можно писать и без -u, и даже, ужас, с -e!
...-kb9
cvs add -kb iod.jpg
... cvs10
Всюду в тексте для краткости предполагается, что правильный CVSROOT установлен.
... случаях11
Borland JavaBuilder.