Git для пользователей Subversion: Часть 2. Принимаем управление

Разбираемся в тонкостях работы с ветвями

Git по сравнению с Subversion предлагает разработчикам на Linux® множество преимуществ при управления версиями, поэтому программисты, занимающиеся совместной разработкой, просто обязаны ознакомиться с лежащими в ее основе базовыми концепциями. В этой статье Тед расскажет о работе с ветвями и слияниями в Git и Subversion, познакомит вас с командной "git bisect" для исследования изменений, и покажет, как разрешать конфликты, возникающие при слиянии изменений.

Теодор Златанов, программист, Gold Software Systems

Теодор Златанов (Teodor Zlatanov) получил диплом магистра по вычислительной технике в Boston University в 1999. Он работает программистом с 1992, используя Perl, Java, C, и C++. Он интересуется работами с открытым исходным кодом по синтаксическому анализу текста, трехуровневыми архитектурами клиент-серверных баз данных, системным администрированием UNIX, CORBA и управлением проектами.



03.02.2011

Это вторая из двух статей серии. Прочитайте первую часть статьи, если вы еще этого не сделали, чтобы познакомиться с настройками Git и Subversion (SVN), которые я буду продолжать использовать и в данной статье, а также чтобы привыкнуть к моему чувству юмора.

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

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

Разработчиков можно понять, так как ветвления и слияния являются довольно рискованными операциями. Их результат не всегда очевиден, а слияния могут уничтожить работу, проделанную другими людьми.

SVN хорошо управляет trunk-версией и многие разработчики работают исключительно с ней, не используя ветвления. SVN-клиенты до версии 1.5 были довольно примитивны в отслеживании слияний, поэтому, если вы привыкли пользоваться старыми SVN-клиентами, вы можете не знать об атрибуте svn:mergeinfo.

Кроме того, существует инструмент svnmerge.py (см. ссылку в разделе Ресурсы), который может отслеживать слияния без поддержки атрибута svn:mergeinfo и поэтому работает со старыми SVN-клиентами.

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


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

Если в части ветвлений и слияний системы управления версиями CVS можно уподобить деревенскому дурачку, то SVN потянет на священника, а Git – на бургомистра. Git изначально проектировалась с поддержкой слияний и ветвлений. Эта функциональность Git не только впечатляет в демонстрационных примерах, но и очень удобна в повседневном использовании.

Например, в Git имеется множество стратегий слияния, в том числе так называемая стратегия осьминога (octopus), которая позволяет выполнять за один раз слияния изменений сразу в нескольких ветвях. Стратегия осьминога! Только представьте себе, каким безумием было бы выполнять подобные слияния в CVS или SVN. Git также поддерживает другой тип слияния, называемый rebasing. Я не буду рассказывать здесь об этой стратегии, скажу только, что она весьма полезна для упрощения истории репозитория, поэтому, возможно, вам стоит с нею ознакомиться.

Прежде чем приступать к приведенному ниже примеру слияния, вам следует ознакомиться с тем, как настроены ветви в части 1 этой серии. У нас есть последняя версия кода - HEAD (в текущей ветви, в данном случае в master) и ветвь empty-gdbinit. Сначала выполним слияние изменений из empty-gdbinit в HEAD, а затем сделаем изменение в HEAD и занесем его в empty-gdbinit:

Листинг 1. Слияние изменений в Git из какой-либо ветви в HEAD
# получаем репозиторий
% git clone git@github.com:tzz/datatest.git
# ...clone output...
# какие имеются ветви?
% git branch -a
#* master
#  origin/HEAD
#  origin/empty-gdbinit
#  origin/master
# выполняем слияние
% git merge origin/empty-gdbinit
#Updating 6750342..5512d0a
#Fast forward
# gdbinit | 1005 ---------------------------------------------------------------
# 1 files changed, 0 insertions(+), 1005 deletions(-)
# теперь публикуем слияние на сервере
% git push
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
#   6750342..5512d0a  master -> master

Все это несложно, если вы понимаете, что в ветви master содержится HEAD-версия, и после слияния с ветвью empty-gdbinit код из ветви master будет опубликован на удаленном сервере для синхронизации с ветвью origin/master. Другими словами, мы выполнили локальное слияние кода из удаленной ветви, а затем опубликовали результат в другой удаленной ветви.

Здесь важно понять, что Git безразлично, какая из ветвей является официальной. Можно выполнить слияние из одной локальной ветви в другую локальную ветвь или в удаленную ветвь. Сервер Git принимает участие только в удаленных операциях. В SVN, напротив, для всех операций требуется SVN-сервер, так как в SVN репозиторий сервера является единственной официальной версией.

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

Теперь давайте создадим еще одну локальную ветвь:

Листинг 2. Создаем новую ветвь release-stable на машине A и переключаемся в нее
# создаем ветвь release-stable
% git checkout -b release-stable
#переключаемся в новую ветвь "release-stable"
% git branch
#  master
#* release-stable
# публикуем новую ветвь на удаленном сервере
% git push --all
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# * [new branch]      release-stable -> release-stable

Теперь на другой машине мы удалим из ветви master файл gdbinit. Конечно же, это не обязательно должна быть отдельная машина, это можно сделать просто в другой директории, но я здесь буду использовать репозиторий "The Other Ted" на машине B с Ubuntu, который мы создали в части 1.

Листинг 3. Удаляем файл gdbinit из ветви master на машине B
# получаем репозиторий и удаляем файл
% git clone git@github.com:tzz/datatest.git
# ...clone output...
% git rm gdbinit
# rm 'gdbinit'
# в какой мы сейчас ветви?
% git branch
#* master
# все в порядке, фиксируем изменение
% git commit -m "removed gdbinit"
#Created commit 259e0fd: removed gdbinit
# 1 files changed, 0 insertions(+), 1 deletions(-)
# delete mode 100644 gdbinit
# а теперь публикуем изменение в удаленной ветви
% git push
#updating 'refs/heads/master'
#  from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#  to   259e0fda9a8e9f3b0a4b3019781b99a914891150
#Generating pack...
#Done counting 3 objects.
#Result has 2 objects.
#Deltifying 2 objects...
# 100% (2/2) done
#Writing 2 objects...
# 100% (2/2) done
#Total 2 (delta 1), reused 0 (delta 0)

Ничего безумного здесь не происходит (за исключением использования термина "deltifying", который звучит как что-то, что можно делать в тренажерном зале, или что-то, что делает река вблизи своего устья). Что же происходит на машине A в ветви release-stable?

Листинг 4. Слияние удаления файла gdbinit из ветви master в ветвь release-stable на машине A
# помните, что мы находимся в ветви release-stable
% git branch
#  master
#* release-stable
# в чем ее отличия от ветви master?
% git diff origin/master
#diff --git a/gdbinit b/gdbinit
#new file mode 100644
#index 0000000..8b13789
#--- /dev/null
#+++ b/gdbinit
#@@ -0,0 +1 @@
#+
# вытягиваем изменения (т.е. удаление файла gdbinit)
% git pull origin master
#From git@github.com:tzz/datatest
# * branch            master     -> FETCH_HEAD
#Updating 5512d0a..259e0fd
#Fast forward
# gdbinit |    1 -
# 1 files changed, 0 insertions(+), 1 deletions(-)
# delete mode 100644 gdbinit
# публикуем изменения на удаленном сервере (обновляем удаленную ветвь release-stable)
% git push
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
#   5512d0a..259e0fd  release-stable -> release-stable

Интерфейс для ментатов, о котором я уже упоминал в части 1, дает о себе знать и при просмотре различий между ветвями. Вы должны знать, что /dev/null - это специальный пустой файл, поэтому в удаленной ветви master нет ничего, тогда как в локальной ветви release-stable содержится файл gdbinit. Не всем пользователям это будет очевидно.

После этого команда pull выполняет слияние локальной ветви с ветвью origin/master, а затем команда push заносит сделанные изменения в ветвь origin/release-stable. Как обычно, разработчики Git не упустили возможности использовать свое любимое слово "delta".


Разделяем изменения

Я не буду здесь углубляться в детали команды git bisect, так как она довольно сложна, но я все же хочу упомянуть этот потрясающий инструмент. Фактически эта команда осуществляет бинарный поиск по журналу внесения изменений. Слово "бинарный" здесь обозначает, что программа каждый раз разбивает надвое интервал поиска и определяет, выше или ниже середины находится искомый сегмент.

Это работает довольно просто. Вы сообщаете Git, что версия A является хорошей, а версия Z - плохой. После этого Git спрашивает вас (или автоматизированный сценарий), является ли плохой версия, находящаяся посередине между A и Z, скажем, Q. Если Q - плохая версия, значит, плохое изменение находится между A и Q; иначе оно находится между Q и Z. Данный процесс повторяется до тех пор, пока не будет найдено нужное нам плохое изменение.

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


Разрешение конфликтов

Конфликты слияний неизбежны в любой VCS, а в особенности в распределенных системах, таких как Git. Что происходит, если два человека вносят в один и тот же файл одной и той же ветви конфликтующие друг с другом изменения? В обоих следующих примерах используется ветвь master репозитория datatest.

Сначала сделаем изменения в файле encode.pl на машине B:

Листинг 5. Вносим на машине B изменение "Does not work"
# сейчас момент времени T1
# изменяем содержимое файла
% echo "# this script doesn't work" > encode.pl
% git commit -a -m 'does not work'
#Created commit e61713b: does not work
# 1 files changed, 1 insertions(+), 1 deletions(-)
# сейчас настал момент времени T2, каков наш статус?
% git status
# On branch master
#nothing to commit (working directory clean)

Теперь мы изменим файл encode.pl на машине A, не зная о том, что он был изменен на машине B, и командой push опубликуем изменения в репозитории:

Листинг 6. Вносим изменение "Does work" на машине A
# сейчас момент времени T2
# изменяем содержимое файла
% echo "this script does work" > encode.pl
% git commit -a -m 'does not work'
#Created commit e61713b: does not work
# 1 files changed, 1 insertions(+), 1 deletions(-)
# сейчас момент времени T3, каков наш статус?
% git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
#nothing to commit (working directory clean)
% git push
#Counting objects: 5, done.
#Delta compression using 2 threads.
#Compressing objects: 100% (2/2), done.
#Writing objects: 100% (3/3), 298 bytes, done.
#Total 3 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
#   259e0fd..f949703  master -> master

Далее мы выполняем на машине B команду git pull и обнаруживаем, что все не так гладко:

Листинг 7. Конфликт на машине B
% git pull
#remote: Counting objects: 5, done.
#Compressing objects: 100% (2/2), done.)   
#remote: Total 3 (delta 0), reused 0 (delta 0)
#Unpacking 3 objects...
# 100% (3/3) done
#* refs/remotes/origin/master: fast forward to branch 'master' 
#   of git@github.com:tzz/datatest
#  old..new: 259e0fd..f949703
#Auto-merged encode.pl
#CONFLICT (content): Merge conflict in encode.pl
#Automatic merge failed; fix conflicts and then commit the result.
# следующая команда не обязательна
% echo uh-oh
#uh-oh
# также можно использовать команду "git diff" для просмотра конфликтов
% cat encode.pl
#<<<<<<< HEAD:encode.pl
## this script doesn't work
#=======
#this script works
#>>>>>>> f9497037ce14f87ff984c1391b6811507a4dd86c:encode.pl

Эта ситуация также очень типична и для SVN. Чьи-то изменения не согласуются с вашей версией файла. Просто отредактируем файл и зафиксируем изменения:

Листинг 8. Исправляем файл и фиксируем изменения на машине B
# исправляем файл encode.pl так, чтобы он содержал текст "# this script doesn't work"...
% echo "# this script doesn't work" > encode.pl
# конфликт разрешен, теперь фиксируем изменения
% git commit -a -m ''
#Created commit 05ecdf1: Merge branch 'master' of git@github.com:tzz/datatest
% git push
#updating 'refs/heads/master'
#  from f9497037ce14f87ff984c1391b6811507a4dd86c
#  to   05ecdf164f17cd416f356385ce8f5c491b40bf01
#updating 'refs/remotes/origin/HEAD'
#  from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#  to   f9497037ce14f87ff984c1391b6811507a4dd86c
#updating 'refs/remotes/origin/master'
#  from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#  to   f9497037ce14f87ff984c1391b6811507a4dd86c
#Generating pack...
#Done counting 8 objects.
#Result has 4 objects.
#Deltifying 4 objects...
# 100% (4/4) done
#Writing 4 objects...
# 100% (4/4) done
#Total 4 (delta 0), reused 0 (delta 0)

Это было просто, не правда ли? Давайте посмотрим, что произойдет на машине A при следующем обновлении кода.

Листинг 9. Исправляем файл и фиксируем изменения на машине B
% git pull
#remote: Counting objects: 8, done.
#remote: Compressing objects: 100% (3/3), done.
#remote: Total 4 (delta 0), reused 0 (delta 0)
#Unpacking objects: 100% (4/4), done.
#From git@github.com:tzz/datatest
#   f949703..05ecdf1  master     -> origin/master
#Updating f949703..05ecdf1
#Fast forward
# encode.pl |    2 +-
# 1 files changed, 1 insertions(+), 1 deletions(-)
% cat encode.pl
## this script doesn't work

Строка Fast forward означает, что локальная ветвь автоматически синхронизировалась с удаленной ветвью, так как в ней не было ничего нового для удаленной ветви. Другими словами, fast forward подразумевает, что не потребовалось выполнять никаких слияний; все локальные файлы были не новее, чем последняя версия удаленной ветви.

И, наконец, я должен упомянуть команды git revert и git reset, которые очень полезны для отката фиксаций и других изменений в дереве Git. В этой статье уже нет места для того, чтобы рассказать о них, но вы должны знать об этих командах и уметь ими пользоваться.


Заключение

В этой статье я рассказал о концепции слияний, проиллюстрировал примерами работу с локальной и удаленной ветвями на разных машинах и показал, как разрешать конфликты. Также я обратил внимание на сложные, порой даже загадочные сообщения Git. По сравнению с SVN сообщения Git намного более подробны, но гораздо менее понятны. Этот факт, а также сложный синтаксис команд делают Git немного страшноватым для большинства новичков. Однако после объяснения некоторых базовых концепций, работать с Git становится намного проще и даже приятнее!

Ресурсы

Научиться

Получить продукты и технологии

  • svnmerge.py - это инструмент для автоматического отслеживания слияний. Он позволяет с легкостью делать слияния изменений в ветвь и из нее, а также автоматически записывает изменения, слияние которых уже было сделано.
  • На Web-сайте проекта Git можно загрузить сам Git, документацию и различные инструменты. (EN)

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=620814
ArticleTitle=Git для пользователей Subversion: Часть 2. Принимаем управление
publish-date=02032011