Академический Документы
Профессиональный Документы
Культура Документы
ТЕРЕХОВ
СОВРЕМЕННЫЕ СИСТЕМЫ
РАСПРЕДЕЛЕННОГО КОНТРОЛЯ ВЕРСИЙ
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ
ВОЛГОГРАДСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ
О. А. СЫЧЕВ, Г. В. ТЕРЕХОВ
СОВРЕМЕННЫЕ СИСТЕМЫ
РАСПРЕДЕЛЕННОГО КОНТРОЛЯ ВЕРСИЙ
Учебно-методическое пособие
Волгоград
2018
1
УДК 004.416.6 (075)
Рецензенты:
кафедра «Информационные системы и технологии»
Волгоградского государственного аграрного университета,
зав. кафедрой д-р техн. наук, профессор О. В. Кочеткова
доцент кафедры «Математические методы в экономике,
информационные и сервисные технологии»
Волгоградского института бизнеса
канд. техн. наук М. В. Филиппов
Сычев, О. А.
Современные системы распределенного контроля версий : учеб.-
метод. пособие / О. А. Сычев, Г. В. Терехов ; ВолгГТУ. – Волгоград,
2018. – 64 с.
ISBN 978–5–9948–3089–5
2
ОГЛАВЛЕНИЕ
3. Перебазирование изменений………………………………………………………..……41
3
1. Повседневное использование распределенных систем
контроля версий
4
работа клонировалась из общего репозитория, но затрудняет некоторые
типовые приемы работы, связанные с созданием идеализированной
истории коммитов. Поэтому позже возможность редактировать коммиты
была введена, но с ограничениями, для чего было введено понятие фазы
коммита. Коммит может быть в одной из трех фаз: публичной (был
опубликован и доступен для вытягивания другим, не может быть изменен),
черновик (может быть изменен) и секретный (не показывается и не
участвует в обычных командах, используется внутренне для реализации
некоторых команд (см., например, очереди патчей). По умолчанию коммит
создается черновиком и может быть отредактирован до выталкивания на
внешний сервер, но при выталкивании становится публичным (как в том
репозитории, куда выталкивают, так и в том, откуда выталкивают), т.к.
выталкивание обычно происходит на внешний, доступный другим
программистам сервер. Однако, если есть потребность иметь сервер для
предварительного просмотра другими программистами работы с
возможностью последующего редактирования коммитов, можно в
настройках принудительно выставить его непубликующим (non-
publishing), в этом случае вытолкнутые коммиты сохранят свой статус
черновика. Обычно непубликующими делаются репозитории,
предназначенные для предварительного просмотра работы начальником
или старшим программистом и недоступные для других членов команды.
Для корректного понимания работы Mercurial и сообщений от него
необходимо знать следующие понятия. Head (голова, вершина) – это
любой коммит, который не имеет потомков; в данный момент репозиторий
(и любая именованная ветка в нем) может иметь несколько head-коммитов,
самой типичной ситуацией с несколькими head-коммитами является
вытягивание изменений от других программистов, сделанных параллельно
со своими. Для уменьшения количества head-коммитов используется
команда merge. В свою очередь tip (наконечник) – это коммит, который в
5
данном репозитории был сделан последним. Tip всегда один и всегда
является head,, но не каждый head есть tip (обратите внимание, что tip хотя
и похож на HEAD в Git
Git,, но не является его аналогом: если привести
рабочий каталог в состояние промежуточного (не
(не-head)) коммита, то tip
останется наверху; в то время как HEAD в Git указывает именно на
текущий коммит). Default – имя ветки, создаваемой в репозитории
репозитор по
умолчанию (см. рис. 1).
1.2
.2 Ссылки на коммиты и их диапазоны в Mercurial
6
и ссылки могут приме
применяться
няться дополнительные операции. Операция ^
указывает на предок коммита с указанным номером, т.е. tip^ это предок
текущего головного коммита. Указывать номер предка имеет смысл для
коммитов-слияний,
слияний, т.к. у них имеется несколько предков – tip^2 будет
обозначать
ать второго предка головного коммита, из другой ветки. Операция
~ позволяет продвигаться дальше по временной линии предков. tip~2 будет
обозначать предка предка головного коммита (т.е. через один коммит от
него) и т.д. (см. рис. 2).
). Обратите внимание, что для операции ~ нет формы
без второго операнда, в отличие от ^, т.е. число указывать обязательно.
Допустим, в репозитории, показанном на рисунке 2,, нам нужно получить
коммит, который был слит в ветку default из ветки foo предпоследним (то
есть коммит G). Это
о можно сделать несколькими способами: получить
третьего предка последнего коммита ((tip~3),
~3), т.к. последний коммит сделан
в ветку foo,, получить третьего предка последнего коммита, явно указав
ветку foo (foo~3)
~3) или получить предка второго предка предпоследнего
предпоследне
коммита в ветке default ((default~1^2~1).
7
какому либо из них; x – y покажет логическую разность (находящиеся в
x, но не в y), операция x%y подобна отрицанию, но включает всех предков
коммитов из множеств x и y. Восклицательный знак или not задает
отрицание, префиксная операция ::x возвращает всех предков коммита x.
Существует также значительное число предикатов, позволяющих получить
различные множества коммитов. Так branch(feature) вернет все
коммиты, относящиеся к данной ветке, only(x,y) возвращает предков x
которые не являются предками y и т.д. (полный перечень представлен в
https://www.selenic.com/mercurial/hg.1.html#revsets) Можно задавать свои
предикаты, используя присваивания в специальной секции [revsetalias]
конфигурационного файла Mercurial. В целом, система задания множеств
коммитов в Mercurial является очень гибкой (реализован небольшой язык в
функциональной парадигме программирования), однако мало знакома
большинству пользователей.
В качестве первого примера рассмотрим вариацию команды hg log.
Обычный hg log –G выглядит достаточно громоздко и даже для
небольшой истории изменений может быть сложен для понимания (см.
рис. 3 а). Промежуточные слияния – это слияния, после которых обе ветки
продолжают развиваться отдельно (выделены курсивом на рисунке 3 а).
Как правило, они выполняются для актуализации состояния веток и не
нужны для понимания истории кода. Для упрощения чтения истории
можно сделать свою команду, которая будет отображать более
компактный граф коммитов, без промежуточных слияний (см. рис. 3 б).
а) б)
Рисунок 3. Пример обычного hg log – G (а); пример истории комиттов
без отображения промежуточных слияний (б)
$ hg diff -r
r "ancestor (p1(), p2())" -r
r "p2()" main.cpp
Рисунок 6.. Просмотр списка изменений заданного файла между последним
слиянием и набором изменений, которые подлежат слиянию
Результатом
ом работы команды на рисунке 6 будет разница между
межд
общим предком p1() (локального родителя) и p2() (другим родителем /
набором изменений,
ий, который мы объединяем) и p2().. Это покажет все
изменения, произошедшие в другой ветке разработки, в файле
файл main.cpp.
Чтобы увидеть все изменения, произошедшие в feature-ветке в
конкретном файле,
е, нужно просто заменить
ть в команде, показанной на
рисунке 2, вторую ревизию на p1(),, как показано на рисунке 7.
11
$ hg diff -r
r "ancestor (p1 (), p2 ())" -r
r "p1 ()" main.cpp
Рисунок 7.. Просмотр всех изменений заданного файла в feature-ветке
featur
1.3.
.3. Особенности работы в системе Git
Добавим в проект файл main.cpp. Так как такого файла раньше в проекте
не было, git status покажет не отслеживаемый файл (см. рис. 9).
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be
committed)
#
# main.cpp
# nothing added to commit but untracked files present
(use "git add" to track)
Рисунок 9. Статус репозитория при добавлении нового файла в рабочий каталог
Теперь при помощи команды git status можно убедиться, что main.cpp
теперь отслеживаемый и индексируемы (см. рис. 11).
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: main.cpp
Рисунок 11. Статус репозитория при добавленном в индекс файле
14
Файл functions.cpp находится в секции “Changes not staged for commit”,
что означает, что этот файл был изменен, но пока не проиндексирован.
Чтобы проиндексировать его, необходимо выполнить команду git add
(см. рис. 13).
$ git add functions.cpp
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: main.cpp
# modified: functions.cpp
Рисунок 13. Добавление изменения в индекс
15
Теперь файл functions.cpp отображается в блоке проиндексированных и
не проиндексированных файлов. Это связано с тем, что Git индексирует
файл в точности в том состоянии, в котором он находился, когда была
выполнена команда git add. Если выполнить коммит, то в него попадут те
изменения, которые попали в индекс при последнем выполнении команды
git add, а не те, которые актуальны на данный момент.
19
You are in 'detached HEAD' state. You can look around,
make experimental
changes and commit them, and you can discard any
commits you make in this
state without impacting any branches by performing
another checkout.
20
В отличие от Mercurial, для простого вытягивания коммитов из
удаленного репозитория в Git используется команда fetch. Команда
pull вытягивает ветку и сливает ее с текущей (точнее с местом, на
который указывает HEAD), так что при ее применении следует быть
осторожным чтобы не произвести нежелательных слияний.
21
Рисунок 22.. Зеленым выделены коммиты, возвращаемые выражением
feature..master~1
рис. 23).
22
Рисунок 23.. Зеленым выделены коммиты, возвращаемые выражением
выражение
(master~1…feature)…^bugfixes
23
разработки чем отменять сделанные. Поэтому слияние в выпускающую
ветку обычно выполняется руководителем (лидером) проекта или
опытным сотрудником, ответственным за его качество.
26
Рисунок 26
26. Пример реализации стратегии feature-branch
branch
27
Рисунок 27
27. Пример реализации стратегии issue-branch
branch
28
применение для задач ветвления требует помнить хештэг
(шестнадцатиричное уникальное число) последнего коммита в каждой
ветке, что крайне неудобно. Поэтому, как правило, анонимное ветвление
используется только для слияния своих изменений с изменениями других
разработчиков, сделанными параллельно.
29
Для слияния веток используется команда merge. Для этого вы должны
находится в одной ветке и передать имя другой в качестве параметра
команды merge. Например, если мы хотим слить ветку foo в главную
master то нужно выполнить команды, представленные на рисунке 30.
$ git checkout master
Переключено на ветку «master»
$ git merge foo
Рисунок 30. Пример слияния веток в Git
30
Рисунок 31. Пример веток репозитория
Как видно из рисунка, ветка foo указывает туда же, куда и master
(например, после слияния). Если эта ветка нам не нужна, ее можно
удалить, выполнив команду git branch foo –d (см. рис. 32).
3
$ git branch -d
d foo
Deleted branch foo (3a0874c).
Рисунок 32.. Удаление ветки из репозитория при помощи команды git branch foo –d
Рисунок 33.. Попытка удалить не слитую с master ветку из репозитория при помощи
команды git branch foo –d
31
Deleted branch bar (3a0874c).
Рисунок 34. Удаление ветки из репозитория при помощи команды git branch foo –D
32
Если команде branch передать в качестве параметра имя ветки, то она
будет создана. Для переключения между ветками используется команда
update, при этом ей вместо ревизии передается имя целевой ветки (см.
рис. 36).
$ hg branch foo
marked working directory as branch foo
$ hg branch
foo
$ hg checkout lualatex_testing
13 файлов обновлено, 0 слито, 12 удалено, 0 c конфликтами
$ hg branch
lualatex_testing
$ hg update default
17 файлов обновлено, 0 слито, 9 удалено, 0 c конфликтами
$ hg merge foo
$ 1 файлов обновлено, 0 слито, 0 удалено, 0 c конфликтами
$ hg commit -m ‘Merge’
33
корректно компилироваться и работать1. Вы можете захотеть запустить
юнит-тесты и произвести другие виды тестирования (или, по крайней мере,
проверить что результат компилируется без ошибок) при слиянии
(особенно в стратегиях с долгоживущими ветками типа feature-branch) и
коммитить результат после исправления проблем, если они возникли.
Mercurial считает именованные ветки постоянными и не удаляемыми
полностью из истории проекта, данные о ветке прописываются в наборе
данных коммита и к ней всегда можно вернутся используя команду
update. Поэтому именованные ветки рекомендуется создавать для
долгосрочных проектов (стратегии major release-branch, developer-branch,
иногда feature-branch). Поэтому удалить ветку полностью из репозитория
нельзя, но можно ее закрыть, используя команду commit с параметром --
close-branch (см. рис. 38).
$ hg commit --close-branch -m 'this branch no longer required'
Рисунок 38. Пример закрытия ветки в Mercurial
1
Простейший пример такой ситуации – это когда в одной ветке изменяется
интерфейс функции (вместе со всеми ее вызовами для сохранения работоспособности),
а другая ветка вводит новые вызовы этой функции со старым интерфейсом. Обе ветки
будут работать и сливаться без конфликтов, однако результат их слияния выдаст
ошибку компиляции (или времени выполнения в интерпретируемом языке).
34
Рисунок 39.. Пример состояния репозитория с тремя не слитыми ветками
Нам нужны только ветки default и dev, ветка test_dev является для нас
лишней. Клонируем репозиторий без нее, как показано на рисунке 40.
35
Обратите внимание, что при выполнении команд push и pull Mercurial
по умолчанию выполняет действия со всеми ветками. Если вы хотите
вытянуть или вытолкнуть изменения только в одной ветке, вы должны
использовать опцию –b (см. рис. 42).
$ hg pull -b default
pulling from https://gvterechov@bitbucket.org/gvterechov/preg
searching for changes
adding changesets
adding manifests
adding file changes
added 136 changesets with 266 changes to 100 files
$ hg push -b default
pushing to https://gvterechov@bitbucket.org/gvterechov/preg
searching for changes
no changes found
Рисунок 42. Пример вытягивания и выталкивания в одной ветке в Mercurial
38
Рисунок 46.. Пример репозитория с ответвлениями на большие релизы и
расставленными метками на малые релизы
3. Перебазирование изменений
Рисунок 49
49.. Пример трех репозиториев до слияния
42
Рисунок 50.. Пример репозитория с бессистемным слиянием
43
должен заниматься именно самый опытный разработчик (начальник), но
бывает достаточно большое количество простых, но трудоемких в
разрешении конфликтов (особенно при слиянии выправлений стиля
кодирования и т.д.), которые могут быть разрешены простыми
разработчиками. Перебазирование позволяет начальнику, не отдавая права
на слияние, переложить разрешение конфликтов на подчиненных. В этом
случае подчиненный выполняет перебазирование своей ветки перед
окончательным слиянием, а начальнику останется лишь проверить
(используя diff между ветками и автоматические тесты), что слияние ничего
не испортило и запустить команду слияния без конфликтов. В этом случае
подчиненному может прийтись применять push --force, так как он уже
выталкивал свои изменения раньше для обзора начальнику. Безопасность
таких изменений истории гарантируется корпоративной культурой: никто
кроме начальника не должен вытягивать изменения у его подчиненного, а
сам начальник не должен делать поверх них свои коммиты, если он хочет,
чтобы изменения были перебазированы подчиненным.
Еще один типичный пример использования перебазирования – когда
вы работаете длительное время над веткой с новыми возможностями
(feature) и вам нужно периодически добавлять туда новые доработки из
master (или default). Использование merge в таких условиях может создать
большое количество дополнительных коммитов-слияний в вашей ветке, в
то время как перебазирование ее на вершину master будет обновлять код в
ней без дополнительных коммитов. Однако такое перебазирование следует
делать осторожно, тестируя результат после каждого перебазирования
(особенно с конфликтами): отсутствие дополнительных коммитов-слияний
не позволит вам обнаружить ошибку, внесенную неудачным слиянием, с
помощью bisect.
Самый частый и простой случай использования команды rebase в Git
и Mercurial это указание ее опцией команды pull: pull --rebase. В
44
этом случае команда вытягивает изменения в текущей (или указанной)
ветке и перебазирует все локальные (не вытолкнутые на сервер)
сер изменения
в ней поверх вытянутых. Обычно такая команда выполняется перед
выталкиванием и служит для поддержки линейной истории кода.
46
Рисунок 55.. Пример Git репозитория с тремя ветками до перебазирования
Рисунок 58
58. Пример Git репозитория до перебазирования
47
$ git rebase --onto
onto master~2 master~1 master
Рисунок 59. Перебазирование в Git,, приводящее к удалению коммита С
48
Допустим, у нас имеется репозиторий, состояние которого показано на
рисунке 62.. Необходимо, как и в примере, описанном выше, выполнить
перебазирование ветки feature2 в ветку default.. Для начала нужно
переключиться на последний
следний коммит I из множества коммитов ветки
feature2,, затем перебазировать всю ветку после коммита D ветки
default,, как показано на рисунке 63 а). Тоже самое можно сделать с
помощью одной команды, достаточно указать после ключа --base
последний коммит ветки
ки feature2 (см. рис. 63 б). Также можно указать
источник – коммит, с которого начинать перебазирование при помощи
опции --source (см. рис. 663 в). Результирующие состояние репозитория
ре
показано на рисунке 64.
$ hg rebase -s
s F -d D
Рисунок 66.. Перебазирование в Mercurial с указанием источника
50
или новичков. Да и самому бывает стыдно выталкивать сделанный в
спешке неудачный коммит и его исправление дополнительным коммитом.
Существует две стратегии отношения к истории проекта. Простейшая
стратегия «как есть» подразумевает сохранение истории проекта именно
как истории – со всеми ее проблемами. Такой подход по-своему логичен,
но неудобен для анализа истории кода с помощью bisect, blame и
подобных инструментов, а также, например, при переносе изменений с
помощью cherry-pick. Вторая стратегия подразумевает подчистку
неудач и хранение идеализированной истории. Она требует, чтобы после
завершения реализации какого-либо подпроекта (исправление ошибки,
реализация новых функций) коммиты были исправлены так, чтобы
представить идеализированную ситуацию разработки. Это делается либо
разработчиком перед выталкиванием на сервер, либо (при наличии
иерархии) после просмотра кода начальником (без слияния его до
исправления недостатков).
Многие формы такого редактирования можно симулировать с
помощью различных команд системы контроля версий, правильно опре-
делив значения параметров (см. удаление коммитов с помощью перебази-
рования, например), однако такой подход неудобен и связан с большим
риском сделать ошибку. Поэтому системы контроля версий предусматри-
вают специальные команды для редактирования истории изменений.
51
ветке после ответвления от указанной), либо указать диапазон коммитов.
Так если вы находитесь в ветке feature, то команда git rebase –i
master позволит вам отредактировать все коммиты после ответвления
feature от master. Команда git rebase –i HEAD~5 даст вам
отредактировать последние 5 коммитов.
При выполнении этой команды Git вызывает текстовый редактор со
специальным временным файлом, представляющим собой план
редактирования: списком коммитов, перед каждым из которых находится
команда pick. Коммиты находятся в том порядке, в котором они
производились (т.е. с точностью до наоборот по отношению к результатам
команды log). В файле также выводится закомментированная справка.
Ваша задача на этом этапе – отредактировать файл так, чтобы он содержал
план желаемых исправлений.
Вы можете удалять строки (что приведет к удалению соответствующих
коммитов со всем содержимым) и переставлять их (меняя местами
коммиты). Перестановка коммитов может оказаться очень полезной, если
вы заметили ошибку в каком-то коммите не сразу (или ее заметил ваш
начальник, а коммит был не последним), так что исправляющий ошибку
коммит находится не сразу за ошибочным, что усложняет их объединение.
Также на коммиты можно влиять, изменяя команды перед ними (см. рис.
68-69). Возможные команды приведены в таблице 1.
Таблица 1. Команды интерактивного перебазирования Git
Команда Описание
pick Стандартная команда, применяет коммит в указанном месте
reword Сохранить коммит, но дать отредактировать коммит-сообщение
edit Остановится перед применением этого коммита, давая возможность
отредактировать его содержание (т. е. код программы) вручную.
Может быть использована для разбиения большого коммита на
несколько
52
squash Объединить этот коммит с предыдущим. Команда открывает
текстовый редактор с коммит
коммит-сообщениями
сообщениями всех объединяемых
коммитов чтобы дать вам сделать новое коммит
коммит-сообщение
сообщение
fixup Объединить
ъединить этот коммит с предыдущим, отбросив его коммит-
коммит
сообщение. Обычно применяется в случае, если объединяемый
коммит исправляет ошибки в предыдущем
exec Позволяет запустить команду операционной
54
и сам коммит, после чего продолжить перебазирование командой git
rebase --continue (которая воспроизведет исправленный коммит).
Следующим в нашем примере будет как раз инструкция edit (см. рис. 73).
Stopped at 73c3dfa... commit c
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
Рисунок 73. Пример работы инструкции edit
55
1 file changed, 1 insertion(+), 1 deletion(
deletion(-)
[detached
hed HEAD 9a4c59a] commit f
1 file changed, 1 insertion(+), 1 deletion(
deletion(-)
Successfully rebased and updated refs/heads/master.
Рисунок 75.. Пример окончания перебазирования
56
Затем изменяем инструкции таким образом, чтобы отредактировать
предпоследний коммит (см. рис. 78).
pick 0c9351d commit b
edit 73c3dfa commit c
pick b37ca24 commit d
$ git add -p
diff --git a/index.php b/index.php
index 9cd06d5..d32e5ad 100644
--- a/index.php
+++ b/index.php
57
@@ -1,6 +1,5 @@
…
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
@@ -17,7 +16,6 @@
…
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n
@@ -39,6 +37,6 @@
…
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n
$ git status
# Not currently on any branch.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: index.php
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working
directory)
#
# modified: index.php
#
$ git status
# Not currently on any branch.
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working
directory)
#
# modified: index.php
58
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git add -p
diff --git a/index.php b/index.php
index 9cd06d5..d32e5ad 100644
--- a/index.php
+++ b/index.php
@@ -17,7 +16,6 @@
…
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? y
@@ -39,6 +37,6 @@
…
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n
$ git status
# Not currently on any branch.
nothing to commit (working directory clean)
59
fe7eea2 commit a
Рисунок 81. Разбиения предпоследнего коммита
61
оставшиеся изменения создадут коммит, чье сообщение по умолчанию
будет совпадать с сообщением редактируемого коммита.
Обратите внимание, что в Mercurial для удаления коммита надо явно
использовать команду drop, а не просто удалить строчку. Это страхует от
случайных малозаметных удалений, что важно – восстановить удаленный
коммит будет невозможно.
При выполнении редактирования истории коммитов следует быть
очень внимательным, так как после ее завершения предыдущее состояние
стирается, и вернуться к нему будет невозможно. Если редактирование
пошло не так, вы можете прервать его с помощью git rebase --
abort или hg histedit --abort и начать заново.
62
Учебное издание
Олег Александрович Сычев
Григорий Владимирович Терехов
СОВРЕМЕННЫЕ СИСТЕМЫ
РАСПРЕДЕЛЕННОГО КОНТРОЛЯ ВЕРСИЙ
Учебно-методическое пособие
Редактор Л. Н. Рыжих