Системы контроля версий. Git, Gitosis

Начну я с краткого введения в системы контроля версий вообще.

Что такое управление версиями и зачем оно?

Итак, предположим, вы разрабатываете программу. Для простоты сначала предположим, что вы разрабатываете ее в одиночку и для собственного удовольствия. Как это выглядит?

Создаем директорию MyApplication, в ней - собственно файлы программы (скажем, *.c). Далее начинается итерационный процесс: написали некоторое количество кода → скомпилировали → запустили → обнаружили ошибки или недоработки → отредактировали исходники → … Всё просто, в целом. Но через некоторое время начинаются нетривиальные вещи.

Неочевидности:

  • Последнее изменение сделало программу хуже, чем она была до того. Хочется вернуть всё "как было". Хорошо, если не успели закрыть редактор - делаем $Undo. А если закрыли?

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

  • Если программа предназначена не только для вас лично, то вам придется делать "релизы", т.е. выпускать заведомо работающие версии программы. Для подготовки релиза вам придется фиксить некоторое количество багов. Это довольно скучный процесс, хочется разбавлять его добавлением новых возможностей в программу (это, как правило, интереснее). Но нельзя: добавление новых фич обязательно добавит багов, и вы никогда не доедете до релиза.

Первое, что приходит в голову для решения всех этих проблем - резервные копии. Скажем, каждую работающую версию программы упаковывать в MyApplication_v0.001.tar.gz, в случае чего - откатываться. Для подготовки к релизу - сделать копию директории MyApplication, в которой только фиксить баги, а в основной - добавлять функционал. При этом подходе через некоторое время разработки у вас появится куча *.tar.gz и не меньше копий рабочей директории. Очень быстро вы начнете путаться, где что.

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

  • Вы добавили в программу (в свою копию) возможность A, а ваш товарищ (в свою) - возможность B. Хотелось бы, чтобы у обоих была версия с обоими возможностями.

  • Вы добавили в программу некоторую функцию, ваш товарищ добавил ее же, но реализовал по-другому. Какую версию выпускать и поддерживать дальше?

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

Даже в случае двух разработчиков, при "ручном" согласовании изменений после определенного объема программы эти неочевидности превращаются в окончательную неразбериху. При разработке сколько-нибудь большой программы в конторе "ручное" согласование изменений видится почти невозможным.

Все эти проблемы призваны решить специальные программы - системы контроля версий (version control systems, vcs).

Общие свойства VCS

Если вы работаете с vcs, то у вас есть место, где хранится вся история изменений в программе - это место называется репозиторий (repository). Для внесения изменений в программу вам нужно получить копию текущей разрабатываемой версии - она называется вашей рабочей копией (working copy). Процесс получения рабочей копии традиционно называется словом "чекаут" (checkout). В случае коллективной разработки, вам придется поддерживать актуальность вашей рабочей копии (проверять, что там остальные наработали) - это обычно называется "апдейт" (update). Чтобы ваши изменения попали в репозиторий, вам нужно сделать коммит (commit) (еще это называется словом checkin). Часто словом commit называют собственно сам набор изменений, зафиксированный этой операцией (более точный термин - changeset). Каждое зафиксированное изменение называют ревизией. Вы можете в любой момент откатить свою рабочую копию к любой предыдущей ревизии - это называется revert.

Для того, чтобы отслеживать такие вещи, как релизы, или варианты рефакторинга программы, или тестировать версии с новыми функциями - в репозитории создаются ветви разработки (branches). Для согласования хода разработки в разных ветках (она может вестись, и обычно ведется, параллельно) время от времени делают слияние веток (merge) - когда к одной ветке применяются те же изменения, что были проделаны в другой. Если какое-то место какого-то файла было по-разному изменено в разных ветках, при слиянии возникает конфликт (merge conflict) - его приходится решать вручную, выбирая один из вариантов или как-то логично их объединяя.

Так как ревизий в репозитории обычно довольно много, возникает необходимость некоторые из них как-то помечать (например, выпущенные релизы, или просто состояния программы, в которых она заведомо работала). Такие пометки называют тегами (tags). Таким образом, теги - это поименованные ревизии в репозитории.

Автоматизация всех перечисленных операций - дело VCS.

Централизованные vs распределенные VCS

Всё множество VCS можно разделить на два класса - централизованные и распределенные.

В случае с централизованной VCS репозиторий хранится на одном сервере, и все разработчики работают с ним. Очевидное преимущество: простое управление выпуском релизов и вообще ходом развития программы, раз весь код в одном месте. Очевидный недостаток: если с сервером что-то случится, работа всех разработчиков пропадет (даже в случае регулярных бэкапов - пропадет работа всех разработчиков, скажем, за последнюю неделю). Известные примеры централизованных vcs - CVS, Subversion, Perforce.

В случае с распределенной VCS, каждый разработчик имеет полную копию репозитория и работает с ним, время от времени синхронизируясь с репозиториями остальных. Таким образом, для организации целенаправленной разработки потребуются какие-то не технические, а организационно-административные средства. Зато получаем множество преимуществ:

  • Раз у каждого есть полная копия репозитория, работа 'всех' разработчиков пропасть не может вообще.

  • Часто выполняемые операции - прежде всего, commit - происходят почти мгновенно, т.к. не требуют соединения по сети.

  • Каждый разработчик может создавать в своем репозитории ветки для каких-то экспериментов, всем остальным даже не нужно знать об этом.

  • Т.к. в распределенных VCS предполагается регулярная синхронизация репозиториев, в них гораздо более эффективно реализована операция слияния веток (здесь это одна из базовых операций, в отличие от централизованных VCS, где это делается нечасто).

  • Каждый разработчик может взять ("утянуть") у другого один или несколько коммитов, применив их к своему коду.

Git

Git - это одна из распределенных VCS (distributed version control system, dvcs). Другими примерами dvcs являются Mercurial, Bazaar, Monotone.

Git выделяется на их фоне главным образом из-за того, что он применяется для разработки ядра Linux (http://www.kernel.org). Собственно, именно для этого Git и разрабатывался. Ядро Linux - весьма немаленький проект, как по объему исходного кода, так и по числу участников, поэтому при разработке Git большое внимание было уделено производительности и легкости слияния веток.

Создание репозитория

Одна из особенностей распределённых VCS и Git в частности состоит в лёгкости создания репозитория. В случае svn, например, вам надо поднимать специальный сервер, настраивать его… В случае с Git, вы даёте одну команду:

$ git init

и всё, в текущей директории готов репозиторий Git. Вся информация репозитория хранится в директории ./.git.

Bare-репозитории.Иногда бывает нужно создать репозиторий без рабочей копии (т.е. состоящий только из директории .git). Такие репозитории называются bare-репозиториями. Директория такого репозитория содержит всё то же, что содержит директория .git обычного репозитория. Для создания bare-репозиториев предназначена опция --bare.

Коммит

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

$ git add имя-файла

(конечно, можно добавить все файлы по маске, или сразу всю директорию). У команды git add есть интересные параметры. git add --interactive покажет список изменившихся файлов и позволит вам в интерактивном режиме выбрать, какие из них включать в коммит. git add --patch работает аналогично, но не с файлами, а с отдельными изменения (hunks в терминологии diff).

Чтобы сделать собственно коммит, используется команда

$ git commit

или

$ git commit -a

чтобы предварительно добавить в индекс все изменившиеся файлы. Команда git commit -i (--interactive) работает аналогично последовательности git add --interactive; git commit.

Если сразу после коммита вы обнаружили, что что-то забыли включить в него, можно поправить:

$ git commit --amend # Добавить последние изменения в последний коммит, "заодно".

Особенность Git состоит в том, что ревизии (коммиты) обозначаются не номерами, как, например, в svn или cvs, а 40-значными идентификаторами типа a21b96…. Идентификаторы можно сокращать до первых нескольких знаков, лишь бы они однозначно определяли коммит. Кроме того, если нужный вам коммит помечен тегом, то можно использовать имя тега вместо идентификатора коммита. На последний коммит в текущей ветке можно также ссылаться по имени HEAD, на предпоследний - HEAD^, пред-предпоследний - HEAD^\^ итп. Существуют и другие способы обозначить нужный коммит, подробности см. в документации.

Состояние репозитория

Увидеть, какие файлы были изменены, добавлены итп после последнего коммита можно командой

$ git status

Сами изменения, сделанные с момента последнего коммита, можно посмотреть командой

$ git diff

У этой команды есть варианты: git diff <идентификатор> - посмотреть изменения с указанного коммита по текущее состояние, git diff <идентификатор1> <идентификатор2> - различия между двумя ревизиями.

Посмотреть журнал последних коммитов - git log.

Работа с клонированным репозиторием

Чтобы создать новый репозитоий, взяв за основу существующий, используется команда

$ git clone git://remote.server/repo.git

Кроме собственного протокола, git поддерживает http://, ssh:// и мн. др.

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

$ git pull

Если вам разрешено вносить изменения в исходный репозиторий (это называется push access, предполагает договоренность с владельцем репозитория, и, в зависимости от протокола доступа (ssh или еще что-то), какие-то пароли итп), это делается командой

$ git push

При этом ваши изменения вливаются в репозиторий, который вы клонировали.

Карман

Git ппредоставляет такую удобную вещь, как "карман" (stash). Это временное хранилище для изменений. Рассмотрим следующий простой пример. Вы начали вносить изменения в программу, но вдруг обнаружили, что забыли переключиться на свою ветку и правите чужую. Делать commit в такой ситуации бессмысленно. Тогда достаточно сделать следующее

$ git stash # спрятать изменения в карман

$ git checkout my-branch # переключиться на нужную ветку

$ git stash apply # применить изменения из кармана

$ git stash clear # очистить карман

Теги, ветки

Пометить только что созданный коммит можно командой

$ git tag имя-тега

Любой другой коммит помечается так:

$ git tag имя-тега идентификатор-коммита

Посмотреть список веток в репозитории -

$ git branch

(изначально создается одна ветка под названием master).

Создать новую ветку -

$ git branch имя-ветки

или

$ git branch имя-ветки идентификатор-коммита-начинающего-ветку

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

$ git checkout имя-ветки

С тем же успехом этой командой можно переключиться на любой коммит - git checkout some-commit. Командой git checkout -b new-branch commit-id можно переключиться на указанный коммит и сразу создать новую ветку, начинающуюся с него.

Влить изменения из другой ветки в текущую -

$ git merge имя-ветки-с-которой-сливаем

Можно взять из другой ветки один конкретный коммит (например, багофикс) -

$ git cherry-pick идентификатор-коммита

Merge и Rebase

Постановка задачиКак я уже говорил, в случае распределённых VCS объединение (merge) веток - очень частая операция. Но в некоторых ситуациях этой операции оказывается недостаточно. На самом деле, merge делает следующее. Пусть у нас есть такая схема:

merge1.png

(здесь овалами обозначены коммиты; цветом я выделил коммиты в ветке yours, остальные - в ветке master). Так вот, команда git merge yours (при условии, что вы находитесь в ветке master), сделает из этой диаграммы следующее:

merge2.png

Здесь "New commit" - это коммит, содержащий все изменения коммитов YC1 и YC2 с предыдущей схемы. Таким образом, изменения из ветки yours будут перенесены в конец ветки master. Заметим, что это изменяет саму ветку master. В случае, когда ветка master находится в некотором "главном" репозитории, а ветка yours - в вашем, это означает, что объединение может выполнять только владелец главного репозитория. И ему же придётся разрешать все конфликты. Однако во многих случаях владелец главного репозитория не желает (не может) вникать во все эти конфликты. Для таких случаев предназначена команда git rebase master (находясь в ветке yours). Она сделает из первой диаграммы следующее:

rebase.png

Т.е. получится, что теперь ваша ветка будет базироваться на последнем коммите ветки master (и, таким образом, будет включать все изменения ветки master). Конечно, при такой операции тоже возможны конфликты, но разрешать их будете вы, а не владелец ветки master. Ему достаточно будет сделать git merge yours - такое объединение теперь пройдёт заведомо без конфликтов.

Кроме этого, команда git rebase -i (--interactive) позволяет делать ещё многое. По этой команде откроется текстовый редактор со списком коммитов в вашей ветке. Изменяя этот список, вы можете, например, исключить некоторые коммиты из операции, или попросить подредактировать некоторые. Отсюда же можно выполнять операцию squash - когда изменения нескольких подряд идущих коммитов сливаются в один.

Работа с удалёнными репозиториями

Как я уже многократно говорил, модель Git предполагает, что в одном проекте используется множество репозиториев. Соответственно, нужны некие инструменты для работы с другими (отличными от текущего) репозиториями. Благодаря тому, что git прозрачно поддерживает разные протоколы доступа, принципиальной разницы между репозиторием, находящимся на данном компьютере и репозиторием, находящимся где-то в интернете нет.

Для работы с удалёнными репозиториями используется команда git remote с подкомандами. Команда

git remote show

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

git remote add reponame ssh://host/path/to/repo.git

Для обмена кодом с удалёнными репозиториями служат всё те же команды - git pull, git push, git cherry-pick. Например, после предыдущей команды команда

git push reponame

пошлёт все изменения в текущем репозитории в репозиторий на хосте host.

Тут же следует заметить, что команду git push редко имеет смысл выполнять в направлении не-bare-репозиториев. Ведь если в репозитории есть рабочая копия, то, вероятно, в ней есть незафиксированные изменения. Изменение состояния репозитория без предварительной фиксации изменений вряд ли приведёт к чему-то хорошему. Обычно вместо того, чтобы делать git push vanya-repo, лучше позвонить этому Ване и сказать "сделай, пожалуйста, pull моих изменений". А команду git push обычно выполняют в направлении репозитория на сервере, который предусмотрительно создаётся в виде bare-репозитория. Или любого другого репозитория, если заранее известно, что в нём точно не может быть никаких своих изменений.

Gitosis

Одним из следствий распределённой модели VCS является отсутствие проблемы разграничения прав доступа. У каждого разработчика есть свой репозиторий, и он имеет на него все возможные права. "Главный" репозиторий принадлежит главному разработчику, и главный разработчик может брать у других изменения и вносить их в свой (главный) репозиторий.

Однако, во многих случаях распределённые VCS используются в централизованном режиме. В такой схеме главный репозиторий не принадлежит никому, push в него могут делать все кому положено (а не только один человек). На самом деле, эта схема оправдана только в двух случаях:

  • Все разработчики находятся в разных локальных сетях, за NAT-ами итп, и из-за этого не могут делать pull друг у друга; в этом случае для обмена кодом нужен отдельный сервер, видимый из всех сетей (например, в интернете);

  • Ситуация, когда нужно организовать публичный хостинг - например, аналог github.org или gitorious.org.

В таких ситуациях нужно раздавать разные права на центральный репозиторий разным разработчикам. Сам Git не имеет никакой модели прав доступа по указанной выше причине. Однако это можно организовать с помощью отдельного ПО. Самый распространённый вариант - gitosis. Gitosis предоставляет доступ к репозиториям, расположенным на сервере (сложенным в одной директории), по протоколу ssh.

Gitosis использует для хранения своей конфигурации git-репозиторий. Для авторизации используются SSH-ключи разработчиков.

Для настройки gitosis нужно:

  • Создать системного пользователя git, имеющего доступ ко всем репозиториям на сервере (но больше никуда);

  • Разрешить ему вход по ssh; я не привожу конкретных команд, как это делать - это описано в документации gitosis; Суть в том, что средствами ssh-сервера этому пользователю разрешается выполнять только очень небольшой набор команд (git push итп);

  • В репозиторий с настройками gitosis, в папку keydir, положить открытые ssh-ключи всех разработчиков, назвав файлы по шаблону user@host;

  • Написать конфиг для gitosis, в котором описать, какие разработчики имеют доступ к каким проектам.

Пример конфига:

[gitosis]

[group gitosis-admin]

writable = gitosis-admin

members = kossnocorp@kossnocorp-desktop

[group octolab]

writable = zfk

members = kossnocorp@kossnocorp-desktop edward@amilo

После этого доступ к репозиториям возможен по URL ssh://git@server.org:repo.git. Заметим, что доступ всегда выполняется под пользователем git (это тот пользователь, от имени которого работает на сервере gitosis). Разработчику совсем не обязательно иметь свою учётную запись на сервере.

Презентация к докладу: http://lug-mgn.ru/files/git/vcss-presentation.ps

Version 1.0

Last updated 2009-11-25 01:04:21 YEKST

07.06.2010 - 17:26
Спасибо, отличный подробный мануал!
05.08.2010 - 01:05
..и мне пригодилось. Спасибо за проделанную работу
15.09.2010 - 23:13
> Ещё одна особенность Git - он не отслеживает автоматически все подряд файлы, находящиеся в директории проекта

нельзя сказать, чтобы хоть одна vcs как-то «отслеживала» файлы. по крайней мере, afaik.
«отследить» можно соответствующей командой. и в git-е тоже такая команда есть — git status.
следовательно, переход к index-у как к «замене отслеживанию» выглядит, imho, не совсем удачным.
16.05.2011 - 20:42
Спасибо за полезную информацию, очень пригодилась!!! Smiling
RSS-материал RSS-материал