Вы находитесь на странице: 1из 64

О. А. СЫЧЕВ, Г. В.

ТЕРЕХОВ

СОВРЕМЕННЫЕ СИСТЕМЫ
РАСПРЕДЕЛЕННОГО КОНТРОЛЯ ВЕРСИЙ
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ
ВОЛГОГРАДСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ

О. А. СЫЧЕВ, Г. В. ТЕРЕХОВ

СОВРЕМЕННЫЕ СИСТЕМЫ
РАСПРЕДЕЛЕННОГО КОНТРОЛЯ ВЕРСИЙ
Учебно-методическое пособие

Волгоград
2018
1
УДК 004.416.6 (075)

Рецензенты:
кафедра «Информационные системы и технологии»
Волгоградского государственного аграрного университета,
зав. кафедрой д-р техн. наук, профессор О. В. Кочеткова
доцент кафедры «Математические методы в экономике,
информационные и сервисные технологии»
Волгоградского института бизнеса
канд. техн. наук М. В. Филиппов

Печатается по решению редакционно-издательского совета


Волгоградского государственного технического университета

Сычев, О. А.
Современные системы распределенного контроля версий : учеб.-
метод. пособие / О. А. Сычев, Г. В. Терехов ; ВолгГТУ. – Волгоград,
2018. – 64 с.
ISBN 978–5–9948–3089–5

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


системах распределенного контроля версий Git и Mercurial, рассмотрены
способы указания ссылок на коммиты и их диапазоны в качестве параметров
различных команд.
Предназначено для изучения дисциплин «Эволюция и сопровождение про-
граммного обеспечения» и «Распределенный контроль версий кода».
Может быть использовано студентами, обучающимися по направлению
09.03.04 «Программная инженерия», а также широким кругом лиц, интересу-
ющихся программированием.

ISBN 978–5–9948–3089–5  Волгоградский государственный


технический университет, 2018
 О. А. Сычев, Г. В. Терехов, 2018

2
ОГЛАВЛЕНИЕ

1. Повседневное использование распределенных систем контроля версий....................... 3

1.1. Особенности работы в распределенных системах контроля версий


на примере Mercurial ..................................................................................................... 4

1.2. Ссылки на коммиты и их диапазоны в Mercurial ....................................................... 6

1.3. Особенности работы в системе Git ............................................................................ 12

1.4. Ссылки на коммиты и их диапазоны в Git................................................................ 21

2. Управление метками и ветками в Mercurial и Git………………...…………………….23

2.1. Стратегии и способы организации ветвления .......................................................... 24

2.2. Команды ветвления и слияния веток в Git ............................................................... 29

2.3. Команды ветвления и слияния веток в Mercurial ..................................................... 32

2.4. «Легкие» ветки в Mercurial ......................................................................................... 36

2.5. Работа с метками в Mercurial и Git ............................................................................ 38

3. Перебазирование изменений………………………………………………………..……41

3.1. Перебазирование изменений в системе Git .............................................................. 45

3.2. Перебазирование изменений в системе Mercurial .................................................... 48

4. Редактирование истории кода ........................................................................................... 50

4.1. Редактирование истории кода в системе Git ............................................................ 51

4.2 Редактирование истории кода в системе Mercurial ................................................... 60

3
1. Повседневное использование распределенных систем
контроля версий

Базовые команды централизованных систем контроля версий нередко


сохраняют свой смысл в распределенных, например для систем Git и
Mercurial это:
1. commit – зафиксировать (закоммиттить) совершенные изменения
(Git, Mercurial).
2. merge – слить вместе изменения в двух различных ветках (Git,
Mercurial) – однако, в отличие от централизованных систем, ветки могут
быть анонимными (созданными в результате параллельной работы
программистов);
3. update – обновить состояние рабочего каталога к состоянию на
момент определенной ревизии (только Mercurial, в Git эту функцию
выполняет команда checkout).
Команда, применяющаяся в начале работы над проектом и
получающая с сервера необходимые данные, изменила свой смысл (теперь
она получает не содержимое рабочего каталога, а создает копию
репозитория) и название – clone вместо checkout. Однако наибольшим
изменением является введение команд для обмена данными между
репозиториями: вытягивание для получения коммитов из удаленного
репозитория (pull, fetch) и выталкивание для отправки коммитов в
удаленный репозиторий (push).

1.1. Особенности работы в распределенных системах контроля версий


на примере Mercurial

Первоначально с точки зрения Mercurial история кода представлялась


неизменной – коммит можно отменить, вводя «противоположный», но не
удалить или изменить. Это дает гарантию корректного слияния кода, если

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. Репозиторий Mercurial с двумя ветками (default и foo),


foo двумя head и
одним tip

Команда pull в Mercurial только перемещает коммиты из удаленного


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

1.2
.2 Ссылки на коммиты и их диапазоны в Mercurial

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).

Рисунок 2.. Пример комплексных ссылок на коммит

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


логическим, при этом в качестве их элементов могут использоваться
коммиты или множества, возвраща
возвращаемые
емые различными предикатами. Так x
and y (эквивалентно x & y)) покажет коммиты, относящиеся к обоим
множествам одновременно; x or y (x | y, x + y) – относящиеся к

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 б).

@ Änderung: 7:52fe4a8ec3cc @ Änderung: 7:52fe4a8ec3cc


|\ Marke: tip |\ Marke: tip
| | Vorgänger: 6:7d3026216270 | | Vorgänger: 6:7d3026216270
| | Vorgänger: 5:848c390645ac | | Vorgänger: 5:848c390645ac
| | Nutzer: Arne Babenhauserheide | | Nutzer: Arne Babenhauserheide
<bab@draketo.de> <bab@draketo.de>
| | Datum: Tue Aug 14 15:09:54 | | Datum: Tue Aug 14 15:09:54
2012 +0200 2012 +0200
| | Zusammenfassung: merge | | Zusammenfassung: merge
| | | |
| o Änderung: 6:7d3026216270 | \
| |\ Vorgänger: 2:b500d0a90d40 | |\
| | | Vorgänger: 4:8dbc55213c9f | | o Änderung: 3:55ba56aa8299
| | | Vorgänger: 0:385d95ab1fea
8
| | | Nutzer: Arne Babenhauserheide | | | Nutzer: Arne Babenhauserheide
<bab@draketo.de> <bab@draketo.de>
| | | Datum: Tue Aug 14 15:09:45 | | | Datum: Tue Aug 14 15:09:40
2012 +0200 2012 +0200
| | | Zusammenfassung: merged 4 | | | Zusammenfassung: 4
| | | | | |
o | | Änderung: 5:848c390645ac | o | Änderung: 2:b500d0a90d40
|\| | Vorgänger: 3:55ba56aa8299 | |/ Vorgänger: 0:385d95ab1fea
| | | Vorgänger: 2:b500d0a90d40 | | Nutzer: Arne Babenhauserheide
| | | Nutzer: Arne Babenhauserheide <bab@draketo.de>
<bab@draketo.de> | | Datum: Tue Aug 14 15:09:39
| | | Datum: Tue Aug 14 15:09:43 2012 +0200
2012 +0200 | | Zusammenfassung: 3
| | | Zusammenfassung: merged 2 | |
| | | o | Änderung: 1:8cc66166edc9
+---o Änderung: 4:8dbc55213c9f |/ Nutzer: Arne Babenhauserheide
| | | Vorgänger: 3:55ba56aa8299 <bab@draketo.de>
| | | Vorgänger: 1:8cc66166edc9 | Datum: Tue Aug 14 15:09:38
| | | Nutzer: Arne Babenhauserheide 2012 +0200
<bab@draketo.de> | Zusammenfassung: 2
| | | Datum: Tue Aug 14 15:09:41 |
2012 +0200 o Änderung: 0:385d95ab1fea
| | | Zusammenfassung: merged 1 Nutzer: Arne Babenhauserheide
| | | <bab@draketo.de>
o | | Änderung: 3:55ba56aa8299 Datum: Tue Aug 14 15:09:38
| | | Vorgänger: 0:385d95ab1fea 2012 +0200
| | | Nutzer: Arne Babenhauserheide Zusammenfassung: 1
<bab@draketo.de>
| | | Datum: Tue Aug 14 15:09:40
2012 +0200
| | | Zusammenfassung: 4
| | |
| o | Änderung: 2:b500d0a90d40
|/ / Vorgänger: 0:385d95ab1fea
| | Nutzer: Arne Babenhauserheide
<bab@draketo.de>
| | Datum: Tue Aug 14 15:09:39
2012 +0200
| | Zusammenfassung: 3
| |
| o Änderung: 1:8cc66166edc9
|/ Nutzer: Arne Babenhauserheide
<bab@draketo.de>
| Datum: Tue Aug 14 15:09:38
2012 +0200
| Zusammenfassung: 2
|
o Änderung: 0:385d95ab1fea
Nutzer: Arne Babenhauserheide
<bab@draketo.de>
Datum: Tue Aug 14 15:09:38 2012
+0200
Zusammenfassung: 1

а) б)
Рисунок 3. Пример обычного hg log – G (а); пример истории комиттов
без отображения промежуточных слияний (б)

Для получения графа, показанного на рисунке 3 б, можно


воспользоваться системой задания множества коммитов, используя
команду, показанную на рисунке 4.
$ hg log -Gr "(all() - merge()) or head()"

Рисунок 4. Пример hg log без отображения промежуточных слияний


9
В данной команде содержится три предиката. Предикат all()
возвращает все изменения (коммиты) (тоже, что "0:tip"), предикат
merge() возвращает набор коммитов, содержащий только слияния и
предикат head() возвращающий головную ревизию. Нам необходимо из
множества всех ревизий вычесть слияния, но добавить головные
независимо от того, являются ли они слияниями, что и достигается
операциями разности и объединения множеств в указанном выражении.
В качестве второго примера рассмотрим использование hg diff в
совокупности с системой задания диапазона коммитов. Периодически
возникает потребность найти коммит, в котором были внесены изменения.
Известен примерный период, в котором был сделан коммит: например с
марта по апрель 2017 года. Также известен программист, который его
сделал и слово, упомянутое в списке изменений. Чтобы найти точный
набор изменений в заданном промежутке дат с заданным словом, можно
воспользоваться командой, показанной на рисунке 5.
$ hg log -r "date('>05/01/17') and date('<06/01/17') and
user(vasiliy) and desc('refactoring')"
Рисунок 5. Пример поиска коммитов с заданными параметрами

Результатом работы команды будут все изменения, в которых дата


была между 1 мая 2017 года и 1 июня 2017 года, сделанная пользователем,
чье имя содержит строку «vasiliy» и где описание коммита включает в себя
слово «refactoring».
Часто в проектах присутствует несколько веток, живущих достаточно
долго. Эти ветки периодически сливаются друг с другом. При разработке
новой функциональности важно иметь в feature-ветке актуальные коммиты
из основной ветки проекта, чтобы гарантировать, что исправление ошибок
также распространяется и на feature-ветку. Иногда при слияниях
10
появляются конфликты, и
и, если изменения особенно запутанные,
запутанные эти
конфликты может быть довольно сложно разрешить. Манипуляция
наборами изменений (revsets
evsets) может помочь в решении и этой проблемы.
проблемы
Есть несколько наборов изменений, содержащих полезн
полезную
ую информацию:
последний раз, когда основная ветка разработки была слита с текущей
веткой; самый последний общий пред
предок
ок двух наборов изменений и т.д.
т.д
Например, допустим, в середине слияния желательно увидеть изменения,
которые были сделаны в конкретном файле между последним слиянием
(общий предок для двух изменяемых веток) и набором изменений, которые
которы
мы пытаемся в настоящее время слить (второй родитель)
родитель).. Можно узнать
все различия между общим предком двух веток и вторым родителем из
текущего
го рабочего катало
каталога (см.рис.6).

$ 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

Git в применении обычных команд сильнее отличается от классических


централизованных систем контроля версий, чем Mercurial.. Это связано, в
частности, с тем, что Линус Торвальдс при разработке Git пользовался
принципом
ринципом «от противного» относительно популярной централизованной
системы CVS,, делая при каждом удобном случае наоборот. Хотя это
обеспечило Git много сильных и эффективных сторон, это также привело к
заменам удобного и привычного интерфейса пользователя там,
т где этого
совсем не требовалось или к спорным решениям. Поэтому работа
некоторых команд git ((add, commit, reset, checkout)
checkout выглядит
несколько эзотеричной и требует понимания особенностей его устройства.
В дополнение к двум элементам обычной системы контроля
кон версий:
файлам и репозиторию – Git вводит еще один – индекс. Индекс содержит
изменения, которые будут включены в следующий коммит. В типовом
рабочем цикле вы не просто делаете коммит, а сначала заносите желаемые
изменения в индекс (командами git add и git remove),
remove можете
перепроверить себя, посмотрев состояние индекса (команда git status)
и, наконец, переместить изменения из индекса в коммит ((git
git commit).
Обратите внимание, что это изменяет привычную семантику команды add
– если в большинстве систем кконтроля
онтроля версий она выполняется только при
12
добавлении новых файлов под контроль версий, то в Git она заносит
добавленные или измененные файлы в индекс, и должна выполняться
всякий раз после изменения файла перед коммитом. Наличие индекса
позволяет вам подготовить изменения, включенные в коммит, отдельно от
содержимого рабочего каталога (так что легко закомиттить его часть),
перепроверить себя перед коммитом, а также содержит интерфейс выбора
ханков, включаемых в коммит (git add --patch).
Допустим, у нас есть репозиторий, в котором не было никаких
изменений. Выполним команду git status, чтобы убедиться, что
изменений не было (см. рис. 8).
$ git status
# On branch master
nothing to commit, working directory clean
Рисунок 8. Статус репозитория без изменений

Добавим в проект файл 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. Статус репозитория при добавлении нового файла в рабочий каталог

Тот факт, что файл main.cpp является не отслеживаемым, по сути


означает, что Git видит файл, которого не было в предыдущем снимке
состояния (коммите). Git не станет добавлять файл в коммит, пока этого
13
явно не указать. Для того чтобы добавить файл под версионный контроль,
необходимо выполнить git add (см. рис. 10).
$ git add main.cpp
Рисунок 10. Добавление файла в индекс

Теперь при помощи команды 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. Статус репозитория при добавленном в индекс файле

Если внести изменения в файл functions.cpp, который уже есть в


индекce, а затем выполнить git status, то команда покажет, что файл
functions.cpp был изменен (см. рис. 12).
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: main.cpp
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be
committed)
#
# modified: functions.cpp
Рисунок 12. Статус репозитория при добавленном файле в индекс и измененном
индексируемом файле

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. Добавление изменения в индекс

Внесем изменение в functions.cpp, и проверим его статус (см. рис. 14).


$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: main.cpp
# modified: functions.cpp
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be
committed)
#
# modified: functions.cpp
Рисунок 14. Статус репозитория при добавленном в индекс новом и измененном
файлах и измененном индексируемом файле

15
Теперь файл functions.cpp отображается в блоке проиндексированных и
не проиндексированных файлов. Это связано с тем, что Git индексирует
файл в точности в том состоянии, в котором он находился, когда была
выполнена команда git add. Если выполнить коммит, то в него попадут те
изменения, которые попали в индекс при последнем выполнении команды
git add, а не те, которые актуальны на данный момент.

Чтобы удалить файл из Git’a, нужно сначала удалить его из индекса, а


затем сделать коммит. Это можно сделать при помощи команды git rm,

которая также удаляет файл из рабочего каталога. Если просто удалить


файл из рабочего каталога, он будет показан в секции “Changes not staged
for commit” вывода команды git status (см. рис. 15).
$ rm functions.cpp
$ git status
# On branch master
#
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be
committed)
#
# deleted: functions.cpp
Рисунок 15. Статус репозитория при удаленном из рабочего каталога файле

После выполнения команды git rm, удаление файла попадет в индекс


(см. рис. 16).
$ git rm functions.cpp
rm ' functions.cpp '
$ git status
# On branch master
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
16
# deleted: functions.cpp
Рисунок 16. Удаление файла из индекса с удалением его из рабочего каталога

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


рабочем каталоге, то необходимо выполнить git rm с опцией –-cached
(см. рис. 17).
$ git rm --cached functions.cpp
Рисунок 17. Удаление файла из индекса с оставлением его в рабочем каталоге

Преимущества такого подхода не очень велики, т.к. GUI клиенты


большинства систем контроля версий имеют схожие функции (и более
удобны за счет графического интерфейса). К недостаткам же следует
отнести большее количество команд в типовой операции коммита и
дополнительные шансы на неполный коммит за счет отсутствия
измененных файлов. В обычной системе контроля версий забываются, как
правило, только новые файлы, т.к. все измененные предлагаются к
коммиту по умолчанию. На практике, в условиях ограниченного времени,
программисты часто делают коммиты в спешке, не уделяя им достаточно
внимания (будучи сосредоточенными на проблемах программного кода),
от чего дополнительная команда не очень помогает, а временами и мешает.
Кроме того, такая осторожность в создании коммитов несколько нелогична
именно для Git, в котором коммиты легко редактируются и даже
полностью удаляются. Впрочем, любители традиционного интерфейса,
могут использовать команду commit с опцией –a для автоматического
включения в коммит всех измененных версионируемых файлов без
предварительных добавлений их в индекс (см. рис. 18).
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
17
#
# new file: main.cpp
#
# 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: functions.php
#
$ git commit -a -m 'добавлена новая функция'
[master 5c25980] 7777
2 files changed, 45 insertions(+), 1 deletion(-)
create mode 100644 main.cpp
$ git status
# On branch master
nothing to commit (working directory clean)
Рисунок 18. Коммит без предварительных изменений в индекс

Другой особенностью Git является отказ от стандартной команды


update, обеспечивавшей переключение между ревизиями, в пользу команды
checkout, которая используется как для переключения между ветками, так
и ревизиями. Для того, чтобы правильно понять работу этой команды
необходимо разобраться в системе ссылок (refs) в Git.
С точки зрения Git, ссылки – это указатели на коммиты, которые
используются (в частности) для реализации веток (указатель переставляется
при коммите на вершину ветки) и меток (указатель остается на месте).
Любой коммит надежно существует только до тех пор, пока он «доступен»
по какой-либо ссылке, т.е. является предком коммита, на которую указывает
эта ссылка – доступность идет назад по линии времени. Недоступные
коммиты могут быть уничтожены сборщиком мусора, хотя это обычно
18
происходит далеко не сразу (в пределах нескольких месяцев). Сослаться на
недоступный коммит можно только по его хешу, который еще нужно как-
как
то записать или узнать.
По умолчанию, во вновь созданном репозитории создается
создает указатель
ветки master (аналог ветки default в Mercurial)) и специальный системный
указатель HEAD (не путать с понятием head в Mercurial,
Mercurial их смысл
различный), который определяет точку, в которую будет сделан очередной
коммит – т.е. при выполнении команды git commit коммит, на который
указывает HEAD,, становится предком нового. В нормальном состоянии
HEAD указывает не напрямую на коммиты, а на ветку, выбранную
текущей (так что переставляется вместе с ней). Команда checkout
перемещает HEAD (и обновляет содер
содержимое
жимое рабочего каталога) в
указанное место – обычно на новую ветку, но при указании конкретного
коммита перемещает HEAD на него – т.е. может быть использована вместо
традиционной в других системах команды update.. Такое состояние
называется «отсоединенным HEAD» т.к. HEAD не указывает ни на одну из
веток (см. рис. 19-21).

Рисунок 19.. Пример состояния репозитория с присоединенным HEAD

$ git checkout d6f27a


Note: checking out 'd6f27a'.

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.

If you want to create a new branch to retain commits


you create, you may
do so (now or later) by using -b with the checkout
command again. Example:

git checkout -b new_branch_name

HEAD is now at d6f27a... First Commit


Рисунок 20
20. Пример использования git checkout

Рисунок 21.. Пример состояния репозитория c отсоединенным HEAD

Если сделать в этом состоянии коммит, то он произведет анонимную


ветку, которая может быть уничтожена сборщиком мусора, если позже не
создать на ее окончание метку или ветку. Такое действие обычно является
ошибкой, поэтому перед фиксацией коммита проверьте, что вы не видите
сообщение о «Detached HEAD
HEAD».

20
В отличие от Mercurial, для простого вытягивания коммитов из
удаленного репозитория в Git используется команда fetch. Команда
pull вытягивает ветку и сливает ее с текущей (точнее с местом, на
который указывает HEAD), так что при ее применении следует быть
осторожным чтобы не произвести нежелательных слияний.

1.4. Ссылки на коммиты и их диапазоны в Git

При составлении ссылок на предков текущих коммитов операции ^ и ~


работают в Git так же, как и в Mercurial (см. раздел 1.2).
Для указания диапазона коммитов используется синтаксис двух или
трех точек, которые имеют разный смысл. При применении двух точек
будут показаны все коммиты, достижимые из правой ссылки и не
достижимые из левой. Т.е. при наличии двух веток master и feature
master..feature будет обозначать все коммиты в feature которых нет в master
(а feature..master – наоборот). Это также удобно при просмотре линейной
истории с ограничением по глубине: master~3..master покажет три
последних коммита ветки master, а master~5..master~2 покажет коммиты,
отстоящие от вершины master на 2, 3 и 4 коммита соответственно. В общем
случае вы можете составить выражение из коммитов, перечисляя те из них,
предков которых надо добавить просто так, а коммиты (ветки), предков
которых надо исключить – предваряя ^. Так master..feature эквивалентно
^master feature. В случае, если в нашем репозитории есть 3 ветки (master,
bugfixes, feature), 2 из которых были слиты (master и bugfixes), и мы хотим
посмотреть, каких же коммитов не хватает в нашей feature ветке из ветки
master, за исключением коммита слияния, нам нужно выполнить команду
git log feature..master~1 (см. рис. 22).

21
Рисунок 22.. Зеленым выделены коммиты, возвращаемые выражением
feature..master~1

Запись из трех точек покажет коммиты, которые достижимы по одной


из ссылок, но не по обеим сразу – т.е. master…feature
feature покажет и
master..feature, и feature
feature..master. В общем
ем случае вы можете вместо
диапазона перечислить несколько ссылок на ветки, перед некоторыми из
которых может быть указан знак ^, обозначающий отрицание (т.е. берутся
коммиты, недостижимые из них). Допустим, мы хотим получить для
предыдущего примера все ко
коммиты, которые есть в ветке feature и master,
за исключением мержа bugfixes в master и коммитов из bugfixes,
bugfixes для этого
воспользуемся командой git log (master~1…feature)…^
)…^bugfixes (см.

рис. 23).

22
Рисунок 23.. Зеленым выделены коммиты, возвращаемые выражением
выражение
(master~1…feature)…^bugfixes

2.. Управление метками и ветками в Mercurial и Git

В распределенных системах контроля версий команды ветвления и


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

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

2.1 Стратегии и способы организации ветвления

Ответ на вопросы «когда создавать и сливать ветки?» – стратегия


ветвления – имеет большое значение для работы над кодом проекта.
Правильный выбор стратегии ветвления с учетом характеристик
сотрудников и работы над проектом способен сильно облегчить работу
лидера проекта и улучшить качество кода, ошибочный же выбор вполне
способен как добавить работы, так и отрицательно сказаться на
результатах разработки и создать немало конфликтных ситуаций.
Перечисленные ниже стратегии не исключают друг друга и могут
использоваться в любых сочетаниях.
1) Ветки (клон) для каждого разработчика (developer-branch). В этом
случае каждый разработчик производит коммиты только в свою ветку
(чаще всего – отдельный серверный клон), куда они имеют право
выталкивать; релиз-версия собирается руководителем или специально
назначенным сотрудником в специальном клоне. Централизованный
подход (когда все разработчики выталкивают свой код в одну ветку
единого серверного репозитория) в настоящее время встречается все реже
и реже (в основном в малых коллективах на небольших проектах), так как
он приводит к значительному хаосу в репозитории и большому количеству
мелких разветвлений и слияний. Кроме того, выталкивание каждым
разработчиком своих изменений напрямую в ветку и репозиторий, в которых
подготавливается релиз-версия затрудняет выявление и исправление
ошибок. Поэтому обычно для каждого разработчика выделяется отдельный
серверный клон (потому, что на клоны можно настраивать права на чтение
24
и запись, в то время как при выделении ветки для разработчика следование
сл
правилам остается на его совести), а руководитель или специально
выделенный сотрудник вытягивают изменения из клонов разработчиков в
основной (релиз-клон)
клон) и сливают их вместе. На рисунке 24 показан пример
репозитория, в котором коммиты разработчик
разработчикаа по имени John
периодически сливаются из одноименной ветки в ветку master,
master в то время
как коммиты разработчика Bob еще не готовы попасть в master, но его
ветка своевременно актуализируется, затягивая коммиты из master. Код
третьего разработчика, Alice
Alice, был слит в master,, но после слияния эта ветка
новых коммитов пока не содержит.

Рисунок 24.. Пример реализации стратегии developer-branch


branch

Ветки для каждой большой версии (major release-branch


branch). Большими
версиями (major release
release)) обычно называют версии, приносящие
прино
значительные изменения в функциях (и в некоторых случаях API)
программы, в отличие от малых ((minor release),
), сконцентрированных
только на исправлениях найденных ошибок и мелких совершенствованиях.
При типовой ситуации в разработке ПО команде необходимо
необходи исправлять
ошибки в одной-двух
двух выпущенных ранее версиях (такие версии
называются стабильными) и параллельно вести разработку новой большой
версии (нестабильной). В этом случае логичным является создание ветки
при каждом выпуске большого релиза, с тем, ччтобы
тобы всегда можно было
переключиться на эти ветки для исправления ошибок, обнаруженных
пользователем. Малые релизы накапливают исправления ошибок в рамках
одной стабильной версии и ветвления не требуют (см. рис. 25).
2
25
Рисунок 25.. Пример реализации стратег
стратегии major release-branch
release

2) Ветки для новой функции (feature-branch).


). В этом случае в ветке
изолируется работа над новой значительной функцией (или крупном
изменении ядра системы). Обычно применяется не ко всем новым
функциям, а в специфических случаях. Так ззначительное
начительное изменение ядра
может приводить к временной неработоспособности большей части
программы, что не дает работать разработчикам, занимающимся другими
участками кода. С другой стороны, ветка может быть использована для
изоляции рискованного проекта, в удачной реализации и отладке которого
нет уверенности (написание нового, либо внесение изменений в уже
отлаженный сложный код): отменить сделанные изменения и вернуться к
стабильной работе соответствующей части программы в случае неудачи
проекта гораздо легче, если эти изменения изолированы в отдельной ветке,
а не разбросаны многочисленными коммитами среди других изменений.
Такая ветка может быть закрыта без слияния с основными ветками. На
рисунке 26 показан пример репозитория, в котором задача, реализованная
реализова
в ветке feature11 была слита с master,, после чего начата реализация новой
функциональности в ветке feature2. Ветка subfeature2
subfeature содержит
специфичные для feature
feature2
2 экспериментальные изменения, которые с
высокой вероятностью могут быть отменены. В feature3 код в процессе
разработки, ветка своевременно актуализируется.

26
Рисунок 26
26. Пример реализации стратегии feature-branch
branch

3) Ветки для задачи на трекере (issue-branch).


). Это стратегия, при
которой создается множество маленьких веточек, посвященных каждой
отдельной
ьной задаче, которые сливаются независимо от других. Хотя
создание и слияние веток для каждого отдельного иссью может казаться
утомительным, оно имеет свои преимущества. При профессиональной
работе программист за день обычно выполняет более одной задачи
(иссью).
ссью). При этом часть задач может быть выполнена хорошо, а часть – с
недостаточным качеством. Если все свои наработки разработчик комиттит
в единственную ветку, то его руководитель имеет только один выбор –
либо слить все, сделанное за какой
какой-то промежуток времени, либо – ничего.
В реальности часто оказывается, что часть работы сделана хорошо, а часть
требует доработки перед слиянием. При этом не исключена ситуация,
когда хорошо сделанную работу необходимо слить как можно скорее
(потому что она блокирует раб
работу
оту других разработчиков или исправляет
критически важную ошибку). Выделение отдельных веток для каждой
задачи позволяет руководителю выборочно сливать только хорошо
выполненные задачи, оставляя остальные не слитыми до полного
п
исправления. На рисунке 27 показан
казан пример репозитория, в котором
задачи с трекера под номерами issue-51 и issue-56
56 решены в одноименных
ветках и слиты с веткой master. Задача issue-53
53 требует дальнейшей
доработки и еще не готова к слиянию в master,, однако своевременно
актуализируется, получая коммиты из ветки master.

27
Рисунок 27
27. Пример реализации стратегии issue-branch
branch

Всего существует 4 способа организации ветвления в распределенных


системах.
1) Клонирование репозитория – любая копия репозитория может
использоваться как ветка, в кото
которой
рой идет самостоятельная разработка.
Основным недостатком данного метода является хранение копии
совместной истории веток в каждом клоне, а также необходимость ручного
выполнения команд для вытягивания (выталкивания в) каждого отдельного
клона. Чаще всего этот подход применяется на уровне developer-branch,
developer
поскольку отдельный репозиторий
репозиторий-клон
клон позволяет отдельно настроить
права доступа для каждого разработчика, заставляя их соблюдать правила
– кто и куда может выталкивать. Кроме того, существование отдельных
репозиториев уменьшает количество разветвлений и слияний при
параллельной работе нескольких программистов в рамках одной ветки.
2) «Тяжелые» ветки Mercurial – являются частью истории репозитория,
их имя навсегда связывается историей кода, по крайней мере, на ее отрезке
от разделения до слияния вместе.
3) Закладки в Mercurial
Mercurial, «легкие» ветки в Git – версионируются как
отдельные объекты и бесследно исчезают после удаления. В целом обычно
производительнее легких веток.
4) Анонимные ветки – образуются естественным образом
обра при
вытягивании параллельно сделанных изменений или коммите после
обновления к набору изменений, не являющемуся головным. Хотя их
возможности почти не отличаются от именованных веток, практическое

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

2.2 Команды ветвления и слияния веток в Git

В репозитории Git всегда существует главная ветка (под названием


master и default) соответственно. Для просмотра списка веток и уточнения
текущей ветки в Git используется команда branch без параметров,
которая выводит список веток и помечает активную ветку звездочкой (см.
рис. 28).
$ git branch
* dev
Master
Рисунок 28. Пример использования команды branch

Если команде branch передать в качестве параметра имя ветки, то она


будет создана. Для переключения между ветками используется команда
checkout, ей также передается имя целевой ветки (см. рис. 29).
$ git branch foo
$ git checkout foo
Переключено на ветку «foo»
$ git branch
dev
* foo
Master
Рисунок 29. Пример переключения между ветками в Git

29
Для слияния веток используется команда merge. Для этого вы должны
находится в одной ветке и передать имя другой в качестве параметра
команды merge. Например, если мы хотим слить ветку foo в главную
master то нужно выполнить команды, представленные на рисунке 30.
$ git checkout master
Переключено на ветку «master»
$ git merge foo
Рисунок 30. Пример слияния веток в Git

Git автоматически создает коммит если слияние не вызывает


конфликтов. В случае наличия конфликтов вы должны их разрешить,
используя команду mergetool и произвести коммит вручную.
Обратите внимание, что слияние ветки не приводит к ее удалению – и
в дальнейшем пути слитых веток снова могут разойтись. Удалить ставшую
ненужной ветку можно с помощью команды git branch с указанием
специальной опции и имени ветки. Для этого используются опция –d
(удаляет ветку только если она была слита с другой веткой) или –D
(удаляет ветку в любом случае, при этом код в ней будет потерян если
ветка не была слита). Стратегии feature-branch и особенно issue-branch
приводят к частой необходимости окончательного (с удалением) слияния
веток, чтобы не загромождать репозиторий большим количеством уже
ненужных имен веток. В этом случае использование опции –d
представляет собой полезную предосторожность во избежание случайного
удаления не слитой ветки с полезным кодом при рутинной чистке
репозитория от старых веток.
Рассмотрим в качестве примера репозиторий, ветки в котором
показаны на рисунке 31.

30
Рисунок 31. Пример веток репозитория

Как видно из рисунка, ветка foo указывает туда же, куда и master
(например, после слияния). Если эта ветка нам не нужна, ее можно
удалить, выполнив команду git branch foo –d (см. рис. 32).
3
$ git branch -d
d foo
Deleted branch foo (3a0874c).

Рисунок 32.. Удаление ветки из репозитория при помощи команды git branch foo –d

Удалить ветку bar командой с опцией -d не получится (см. рис. 33).


3
$ git branch -d foo
error: The branch 'foo' is not fully merged.
If you are sure you want to delet
delete
e it, run 'git branch -D foo'.

Рисунок 33.. Попытка удалить не слитую с master ветку из репозитория при помощи
команды git branch foo –d

Если мы хотим удалить ветку bar (что, в конце концов, приведет к


удалению коммита Е), необходимо выполнить команду git branch bar
–D (см. рис. 34).
$ git branch -D
D bar

31
Deleted branch bar (3a0874c).
Рисунок 34. Удаление ветки из репозитория при помощи команды git branch foo –D

Git поддерживает отдельное понятие «удаленных» (remote) веток,


которые отслеживают состояние некоторой ветки на удаленном сервере.
Называться такая ветка будет по принципу «имя сервера/имя ветки» -
например origin/master (origin – по умолчанию имя репозитория, из
которого вы сделали клон своего). Команда fetch при получении
изменений от удаленного репозитория обновляет именно такие ветки,
оставляя ваши локальные копии этих веток нетронутыми. Команда pull
позволяет получить изменения и автоматически слить их с вашими
локальными ветками, отслеживающими (tracking branches) изменения в
ветках на сервере. Обратите внимание, что в Git команды типа push и
pull обычно используются с одной веткой. Если ваш репозиторий ранее
не получал (используя команду checkout) какую-либо ветку с сервера, то
команды fetch и pull не создадут ее автоматически у вас.

2.3 Команды ветвления и слияния веток в Mercurial

В репозитории Mercurial всегда существует главная ветка под


названием default. Для просмотра списка веток и определения текущей
ветки в Mercurial можно использовать команду branches без параметров
(см. рис. 35).
$ hg branches
default 153:835591ff7167
lualatex_testing 124:2ccaa4dbfac2 (неактивна)
$ hg branch
default
Рисунок 35. Просмотр веток в Mercurial

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

Рисунок 36. Пример создания ветки в Mercurial

Для слияния веток используется команда merge. Для этого вы должны


находится в одной ветке и передать имя другой в качестве параметра
команды merge. Например, если мы хотим слить ветку foo в главную
master то нужно выполнить команды, представленные на рисунке 37.

$ hg update default
17 файлов обновлено, 0 слито, 9 удалено, 0 c конфликтами
$ hg merge foo
$ 1 файлов обновлено, 0 слито, 0 удалено, 0 c конфликтами
$ hg commit -m ‘Merge’

Рисунок 37. Пример слияния веток в Mercurial

Обратите внимание, что Mercurial не создает автоматически коммит


слияния даже при отсутствии конфликтов, поэтому вы должны запустить
команду коммита сами. Это может быть полезно, поскольку формальное
отсутствие конфликтов слияния еще не означает, что слитый код будет

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

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


воспользоваться командой clone с опцией --rev так, чтобы не
затягивать в новый репозиторий коммиты соответствующей ветки. Также
для команды clone можно использовать опцию --branch, явно указав
ветки, которые нужно клонировать. Рассмотрим репозиторий, состояние
которого отображено на рисунке 39.

1
Простейший пример такой ситуации – это когда в одной ветке изменяется
интерфейс функции (вместе со всеми ее вызовами для сохранения работоспособности),
а другая ветка вводит новые вызовы этой функции со старым интерфейсом. Обе ветки
будут работать и сливаться без конфликтов, однако результат их слияния выдаст
ошибку компиляции (или времени выполнения в интерпретируемом языке).
34
Рисунок 39.. Пример состояния репозитория с тремя не слитыми ветками

Нам нужны только ветки default и dev, ветка test_dev является для нас
лишней. Клонируем репозиторий без нее, как показано на рисунке 40.

$ hg clone backup repo --branch default dev


Рисунок 40.. Клонирование репозитория в Mercurial без ветки dev_test

После такого клонирования наш репозиторий будет выглядеть так, как


показано на рисунке 41.

Рисунок 41.. Пример репозитория после клонирования части веток

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

2.4 "Легкие" ветки в Mercurial

В Mercurial существует еще один способ ветвления репозитория -


“закладки”, bookmarks. Работа закладок в Mercurial похожа ветки в Git: они
не хранятся постоянно, а существуют лишь как именованные указатели на
вершины веток, поэтому могут быть полностью удалены. Поэтому
закладки рекомендуется использовать при необходимости создавать
значительное количество временных веток (стратегии issue-branch и
иногда feature-branch).
Закладка - это ссылка на коммит, которая автоматически обновляется
при добавлении новых коммитов поверх него. Команды bookmark и
bookmarks работают аналогично командам branch и branches. Если
вы выполните команду hg bookmark foo, созданная закладка foo будет
ссылаться на текущий набор изменений (см. рис. 43).
36
$ hg bookmark foo
Рисунок 43. Пример создания закладки в Mercurial

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


вперед с каждым коммитом. Закладки всегда указывают на последнюю
ревизию в направлении (“ветке”) работы. Вы можете переключаться между
закладками так же, как и между ветками, используя команду update,
использовать имя закладки в качестве аргумента команды merge и т.д (см.
рис. 44).
$ hg bookmark bar
$ hg update foo
0 files updated, 0 files merged, 0 files removed, 0 files
unresolved
$ hg update bar
0 files updated, 0 files merged, 0 files removed, 0 files
unresolved
Рисунок 44. Пример переключения между закладками в Mercurial

Закладка может быть удалена с помощью команды hg bookmark –


delete с указанием имени закладки (см. рис. 45). Однако, в отличие от
Git, удаление не слитой закладки удаляет только саму закладку (имя),
оставляя коммиты этой ветки на месте (ветка становится анонимной). Вы
можете закрыть ветку предварительно, используя commit --close-
branch.
$ hg bookmark --delete bar
Рисунок 45. Пример удаления закладки в Mercurial

В современных версиях Mercurial приняты следующие правила работы


с закладками на удаленных серверах. При клонировании или вытягивании
из репозитория передаются все закладки. При выталкивании же
выталкиваются все закладки, которые есть как в локальном, так и в
удаленном репозиториях; если вы хотите вытолкнуть на сервер новую для
37
него закладку это надо указать команде push специальной опцией –B
<имя закладки>. Таким образом вы можете держать часть закладок
чисто для локальной работы, не делясь ими с окружающими. Этот прием
хорошо согласуется с возможностью полностью удалить закладку позднее.

2.5 Работа с метками в Mercurial и Git

Метки (tags) – это возможность давать определенным (важным)


ревизиям проекта человеко-читаемые имена. В отличие от веток, указатели
которых обновляются после нового коммита, метки продолжают указывать
на ту же ревизию. Например, при использовании стратегии major release-
branch на каждый большой релиз выделяется отдельная ветка, в рамках
которой делается потом несколько малых релизов. Для обеспечения
возможности легко получить код малого релиза командой update на этот
коммит ставится метка (см. рис. 46).

38
Рисунок 46.. Пример репозитория с ответвлениями на большие релизы и
расставленными метками на малые релизы

В Mercurial метки хранятся в специальном версионируемом файле с


именем .hgtags.. Это сделано для того, чтобы сами метки (и процесс их
изменения) были версионируемыми, их можно было сливать при слиянии
веток, добавлять теги к уже существующим коммитам и изменять
положение
ложение тегов. Однако это создает некоторые странные ситуации, к
которым необходимо быть готовым. Так метка какого-либо
либо коммита не
может быть частью того коммита, на который она указывает – она должна
быть добавлена после, причем ее добавле
добавление необходимо закомиттить.
акомиттить. При
слиянии файла с метками возможно возникновение конфликтов слияния,
причем Mercurial не занимается их специальным разрешением – это должен
делать пользователь – обычно конфликт возникает при добавлении меток в
39
обеих ветвях и при его разрешении необходимо взять обе добавленные
строки. Наконец, если вы обновитесь до ревизии с какой-либо меткой, вы
не обнаружите этой метки (ибо она была добавлена более поздней
ревизией). Команды типа clone, push и pull работают со всеми
метками автоматически (т.к. они являются частью файловой истории), а
команда merge берет на себя слияние меток из разных репозиториев.
Для создания метки используется команда hg tag <имя метки>,
которая создает метку, указывающую на предыдущий коммит (от которого
непосредственно происходит содержимое рабочей копии). Для ее удаления
используется эта же команда с ключом --remove. Команда update
позволяет переключиться к отмеченной ревизии, а команда tags
показывает список всех меток, доступных на данный момент (см. рис. 47).
$ hg tag dev_tag
$ hg tags
tip 6070:5210c7425f67
длина названия теста увеличена до 255 5625:5020f2fc1aea
добавлен код для создания message_provider'a 5547:760b293defe3
добавлен промежуточный этап для импорта 5535:ef2929d4e490
Удаление заданий - интерфейс 5485:e050146dbaac
dev_tag 4612:d29aee1e939d
$ hg update tip
2 files updated, 0 files merged, 0 files removed, 0 files
unresolved
$ hg tag --remove dev_tag
Рисунок 47. Пример работы с метками в Mercurial

Если вы хотите завести метку для себя работая с Mercurial, то можете


использовать ключ –l, создаваемые с его помощью метки будут
принадлежать только вашему репозиторию.
В Git метки представляют из себя указатели на коммиты и делятся на
две категории: легковесные (по сути ветка, указатель на которую навсегда
40
заморожен на определенном коммите) и аннотированные.
Аннотированные ветки являются объектом подобно наборам изменений –
они содержат помимо имени запись об авторе, могут содержать сообщение
и могут быть подписанными. Создаются метки командой tag, при этом
для создания аннотированной метки применяется опция –a, для указания
сообщения –m, а если вы хотите подписать метку с помощью GPG – вы
используете опцию –s вместо –a. Для создания легковесной метки не
должно указываться опций –s, -a и –m. Просмотреть метки можно с
помощью команды tag без параметров (см. рис. 48).
$ git tag
v0.1
v1.3
Рисунок 48. Пример просмотра меток в Git

Git по умолчанию не выталкивает метки. Вы можете вытолкнуть


конкретную метку, указав ее имя в опциях команды push, или
использовать опцию --tags для отправки на удаленный сервер всех
меток. При вытягивании или клонировании удаленного сервера, однако,
получаются все находящиеся там метки.

3. Перебазирование изменений

Назовем базой коммита предыдущий, то есть родительский коммит.


Базой для ветки будет, соответственно, коммит, после которого эта ветка
отделилась от главной. Перебазирование – это изменение базы, то есть
перенос ветки (начиная с какого-то коммита и до конца) на другое место,
чаще всего вперед по линии коммитов. Обратите внимание, что
перебазирование изменяет историю коммитов и, если ваш репозиторий до
перебазирования был кем-то вытянут (и он делал на нем свою работу), то
41
этот разработчик получит проблемы с обновлением после
перебазирования.
зирования. Поэтому перебазирование лучше употреблять только
если вы уверены, что вашу работу никто не затягивал.
Наиболее частое употребление перебазирования заключается в
перебазировании локальных изменений до выталкивания их на сервер.
Предположим, вы вытянули
тянули изменения коллег, провели свою работу и
хотите ее вытолкнуть, но ее необходимо слить с коммитами людей,
работавших параллельно с вами. При использовании команды merge мы
получим дополнительный коммит и разветвляющуюся историю – там, где,
возможно, никакого
икакого особого смысла в ветках нет – они образовались
просто случайно из-за
за вашей одновременной работы. При наличии в
команде более двух разработчиков ситуация становится еще хуже,
особенно при отсутствии строгой дисциплины в отношении того, кто и в
каком порядке сливает изменения. Три разработчика, сделав по одному
рабочему
очему (с новым кодом (см. рис. 449))
)) коммиту, бессистемно их сливая
способны
бны создать показанную на рис. 550 сетку из 9 коммитов (цветом
выделены коммиты-слияния).
слияния). Такую историю очень трудно читать
чи и
отлаживать, используя bisect
bisect.

Рисунок 49
49.. Пример трех репозиториев до слияния

42
Рисунок 50.. Пример репозитория с бессистемным слиянием

Однако в тех же условиях историю можно сохранить линейной, с


тремя коммитами. Для этого при затягивании изменений
изменен можно
использовать команду pull --rebase,, которая затянет изменения с
указанного репозитория и перебазирует ваши коммиты на их вершину, как
будто вы работали последовательно. Это абсолютно безопасно для
репозитория если перед началом своей работы вы сде
сделали
лали pull с того же
сервера, так как перебазируются локальные коммиты, которые никуда не
выталкивались. Если три программиста в примере выше использовали бы
pull --rebase,, история ко
кода
да выглядела бы как на рисунке 51.
51

Рисунок 51.. Пример репозитория с испо


использованием
льзованием перебазирования

Второй частый случай применения rebase заключается в


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

43
должен заниматься именно самый опытный разработчик (начальник), но
бывает достаточно большое количество простых, но трудоемких в
разрешении конфликтов (особенно при слиянии выправлений стиля
кодирования и т.д.), которые могут быть разрешены простыми
разработчиками. Перебазирование позволяет начальнику, не отдавая права
на слияние, переложить разрешение конфликтов на подчиненных. В этом
случае подчиненный выполняет перебазирование своей ветки перед
окончательным слиянием, а начальнику останется лишь проверить
(используя diff между ветками и автоматические тесты), что слияние ничего
не испортило и запустить команду слияния без конфликтов. В этом случае
подчиненному может прийтись применять push --force, так как он уже
выталкивал свои изменения раньше для обзора начальнику. Безопасность
таких изменений истории гарантируется корпоративной культурой: никто
кроме начальника не должен вытягивать изменения у его подчиненного, а
сам начальник не должен делать поверх них свои коммиты, если он хочет,
чтобы изменения были перебазированы подчиненным.
Еще один типичный пример использования перебазирования – когда
вы работаете длительное время над веткой с новыми возможностями
(feature) и вам нужно периодически добавлять туда новые доработки из
master (или default). Использование merge в таких условиях может создать
большое количество дополнительных коммитов-слияний в вашей ветке, в
то время как перебазирование ее на вершину master будет обновлять код в
ней без дополнительных коммитов. Однако такое перебазирование следует
делать осторожно, тестируя результат после каждого перебазирования
(особенно с конфликтами): отсутствие дополнительных коммитов-слияний
не позволит вам обнаружить ошибку, внесенную неудачным слиянием, с
помощью bisect.
Самый частый и простой случай использования команды rebase в Git
и Mercurial это указание ее опцией команды pull: pull --rebase. В
44
этом случае команда вытягивает изменения в текущей (или указанной)
ветке и перебазирует все локальные (не вытолкнутые на сервер)
сер изменения
в ней поверх вытянутых. Обычно такая команда выполняется перед
выталкиванием и служит для поддержки линейной истории кода.

3.1 Перебазирование изменений в системе Git

Команда git rebase понимает задание на перебазирование через


три основных
х параметра в указанном порядке: приемник (куда
перебазировать, задается через опцию --onto,, по умолчанию
принимается как вершина апстрима), апстрим (ветка, являющаяся базовой
для перебазирования – набор перебазируемых коммитов начинает
отсчитываться с ответвления
етвления перебазируемой ветки от апстрима) и
собственно перебазируемую ветку (по умолчанию – текущая ветка).
Классическое перебазирование в виде переноса точки ответвления
перебазируемой ветки на вершину ветки апстрима, получается если
переключиться на перебазируемую
ребазируемую ветку командой git checkout и
указать в параметрах git rebase только
олько ветку апстрима (см. рис. 52-54).
5

Рисунок 52.. Пример состояния репозитория до перебазирования

$ git checkout foo


$ git rebase master
First, rewinding head to replay your wo
work
rk on top of it...
Applying: added staged command
Рисунок 533. Пример классического перебазирования в Git
45
Рисунок 54.. Пример состояния репозитория после перебазирования

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


выталкивания послее перебазирования понадобится использовать опцию --
force.. При этом надо следить, чтобы выталкивать таким образом только
одну ветку – если вы забудете указать имя выталкиваемой ветки и
вытолкните их все, вы можете стереть незатянутые к себе коммиты в
других ветках.
Использование опции --onto с явным указанием приемника
позволяет переводить при перебазировании более сложные перемещения,
например, изменить точку ответвления на другую ветку.
В качестве примера рассмотрим репозиторий с тремя ветками: master,
feature1 и feature22 (см. рис. 55
55). В ветке feature1
1 находятся сложные
функции в процессе реализации, которые еще рано добавлять в master, а в
ветке feature2
2 реализовано небольшое улучшение, которое уже готово к
интеграции в master.. Для этого необходимо переб
перебазировать
азировать feature2 на
вершину master,, как показано на рисунке 56
56.. Результирующий
репозиторий показан на рисунке 57.

46
Рисунок 55.. Пример Git репозитория с тремя ветками до перебазирования

$ git rebase --onto


onto master feature1 feature2
Рисунок 56.. Перебазиро
Перебазирование ветки feature2 на вершину master с использованием
опции --onto

Рисунок 57.. Пример Git репозитория после перебазирования

Также с помощью git rebase --onto можно удалить коммиты


комми в
середине ветки (см. рис. 58
58-60),
), если все три параметра ссылаются на
коммиты в одной ветке и приемник расположен раньше апстрима. Однако
такое удаление требует большой аккуратности при вводе команды и не
рекомендуется в общем случае, подобных результаты можно достичь легче
используя git rebase --interactive.

Рисунок 58
58. Пример Git репозитория до перебазирования

47
$ git rebase --onto
onto master~2 master~1 master
Рисунок 59. Перебазирование в Git,, приводящее к удалению коммита С

Рисунок 60.. Результат перебазирования для удаления коммитов

3.2 Перебазирование изменений в сист


системе
еме Mercurial

В Mercurial команда rebase является расширением (поставляемым в


стандартной инсталляции) и должна быть специально включена в файле
настроек перед началом ра
работы,
боты, как показано на рисунке 61.
61
[extensions]
rebase =
Рисунок 61.. Пример строки из файла настроек для включения перебазирования

Команда hg rebase использует другую систему опций для


определения, что и куда переносить. Коммит
Коммит-приемник
приемник (куда переносить)
указывается с помощью опции –d (--dest),
), по умолчанию это верхний
коммит в текущей им
именованной
енованной ветке. Существует две возможности
указать ветку для перебазирования: база или коммит
коммит-источник.
источник. Указание
базы (опции -b, --base
base)) менее точно, но более удобно: в этом случае
перебазирование использует всю ветку от базы до первого коммита,
ответвившего
его ее от общего потомка с приемником. Это наиболее
типичный случай перебазирования и использование базы позволяет
избавить пользователя от ручного поиска коммита, начавшего
перебазируемую ветку. По умолчанию базой принимается коммит,
являющийся предком те
текущего
кущего содержания рабочего каталога.

48
Допустим, у нас имеется репозиторий, состояние которого показано на
рисунке 62.. Необходимо, как и в примере, описанном выше, выполнить
перебазирование ветки feature2 в ветку default.. Для начала нужно
переключиться на последний
следний коммит I из множества коммитов ветки
feature2,, затем перебазировать всю ветку после коммита D ветки
default,, как показано на рисунке 63 а). Тоже самое можно сделать с
помощью одной команды, достаточно указать после ключа --base
последний коммит ветки
ки feature2 (см. рис. 63 б). Также можно указать
источник – коммит, с которого начинать перебазирование при помощи
опции --source (см. рис. 663 в). Результирующие состояние репозитория
ре
показано на рисунке 64.

Рисунок 62. Пример Mercurial репозитория с тремя


мя ветками до перебазирования

$ hg update I $ hg rebase -- $ hg rebase --


$ hg rebase --dest D dest D --base I dest D --source H
а) б) в)
Рисунок 63. Перебазирование в Mercurial

Рисунок 64.. Пример Mercurial репозитория после перебазирования


49
жно перебазировать не всю ветку, а ее часть, то можно
Если вам нужно
использовать только опцию –s (--source):
): в этом случае указывается
точный коммит, с которого и начнется перебазирование. Допустим, у нас
есть репозиторий, состояние которого показано на рисунке 65.
65
Необходимо
ходимо перебазировать коммиты F и G после коммита D, а коммиты
E оставить ответвленным от B,, как есть сейчас (см. рис. 66).
6
Результирующий репозиторий показан на рисунке 67
67.

Рисунок 65. Пример Mercurial репозитория с двумя ветками до перебазирования

$ hg rebase -s
s F -d D
Рисунок 66.. Перебазирование в Mercurial с указанием источника

Рисунок 67.. Пример Mercurial репозитория после перебазирования

4. Редактирование истории кода

В повседневной работе часто делаются неудачные коммиты: от случаев


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

50
или новичков. Да и самому бывает стыдно выталкивать сделанный в
спешке неудачный коммит и его исправление дополнительным коммитом.
Существует две стратегии отношения к истории проекта. Простейшая
стратегия «как есть» подразумевает сохранение истории проекта именно
как истории – со всеми ее проблемами. Такой подход по-своему логичен,
но неудобен для анализа истории кода с помощью bisect, blame и
подобных инструментов, а также, например, при переносе изменений с
помощью cherry-pick. Вторая стратегия подразумевает подчистку
неудач и хранение идеализированной истории. Она требует, чтобы после
завершения реализации какого-либо подпроекта (исправление ошибки,
реализация новых функций) коммиты были исправлены так, чтобы
представить идеализированную ситуацию разработки. Это делается либо
разработчиком перед выталкиванием на сервер, либо (при наличии
иерархии) после просмотра кода начальником (без слияния его до
исправления недостатков).
Многие формы такого редактирования можно симулировать с
помощью различных команд системы контроля версий, правильно опре-
делив значения параметров (см. удаление коммитов с помощью перебази-
рования, например), однако такой подход неудобен и связан с большим
риском сделать ошибку. Поэтому системы контроля версий предусматри-
вают специальные команды для редактирования истории изменений.

4.1. Редактирование истории кода в системе Git

В Git редактирование истории называется интерактивным пере-


базированием и вызывается командой rebase -i или rebase --
interactive. При этом, чтобы определить диапазон коммитов для
редактирования, надо указать либо ветку, относительно которой делается
перебазирование (тогда редактироваться будут все коммиты в текущей

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 Позволяет запустить команду операционной

Допустим, у нас есть репозиторий, чье состояние изображено


изображе на
рисунке 68.

$ git log --pretty=format:"%h


pretty=format:"%h %s"
b25038f commit g
f465624 commit f
77c6ee5 commit e
b37ca24 commit d
73c3dfa commit c
0c9351d commit b
fe7eea2 commit a
Рисунок 68. Пример репозитория для редактирования истории

Запустим редактирование коммитов,


тов, как показано на рисунке 69.
69
$ git rebase –i
i HEAD~6

Рисунок 69. Запуск редактирования последних шести коммитов

После запуска rebase –i откроется текстовый редактор с


командами, которые необходимо п
применить
рименить к коммитам (см. рис. 70).
7
Допустим, нам нужно оставить как есть коммит A, заменить коммит
сообщение коммита B, отредактировать коммит C, объединить коммит D с
F,, и объединить коммит E с G,, отбросив при этом коммит сообщение
коммита G. Отредактируем его, как показано на рисунке 71..
pick 0c9351d commit b
53
pick 73c3dfa commit c
pick b37ca24 commit d
pick 77c6ee5 commit e
pick f465624 commit f
pick b25038f commit g
Рисунок 70. Пример файла после использования git rebase –i

Изменим верхнюю часть файла на список команд, показанный на


рисунке 71.
reword 0c9351d commit b
edit 73c3dfa commit c
pick b37ca24 commit d
squash f465624 commit f
pick 77c6ee5 commit e
fixup b25038f commit g
Рисунок 71. Пример отредактированного файла с инструкциями для git rebase
–i

После того, как вы введете новый план, сохраните файл и выйдете из


редактора, система контроля версий отменяет все указанные коммиты
(сохраняя их содержимое во временной зоне) и начинает применять их
заново в соответствии с вашим планом. Команды reword и squash
откроют текстовый редактор для редактирования коммит-сообщения.
В нашем примере сначала сработает инструкция reword, вводим
новое коммит сообщение и сохраняем изменения (см. рис. 72).
[detached HEAD db90798] edited message commit b
1 file changed, 1 insertion(+), 1 deletion(-)
Рисунок 72. Пример работы инструкции reword

Если вы ввели команду edit то система остановится после этого


коммита и выдаст инструкции по изменению коммита. Вы можете
использовать git commit --amend чтобы отредактировать сообщение

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

После того, как отредактируем текущие изменения, вводим git


commit –amend, редактируем коммит сообщение, вводим git rebase
–continue чтобы продолжить редактирование истории. Следующей
командой будет squash, откроется текстовый редактор, предлагающий
отредактировать коммит-сообщение, как показано на рисунке 74.
# This is a combination of 2 commits.
# The first commit's message is:
commit d
# This is the 2nd commit message:
commit f
# Please enter the commit message for your changes. Lines
starting
# with '#' will be ignored, and an empty message aborts the
commit.
# Not currently on any branch.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: index.php
Рисунок 74. Пример работы инструкции squash

После редактирования коммита увидим следующее (см. рис. 75).


$ git rebase --continue
[detached HEAD 7fda761] commit d

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.. Пример окончания перебазирования

В результате редактирования истории кода будет получен репозито-


репозито
рий, показанный на рисунке
исунке 76.

$ git log --pretty=format:"%h


pretty=format:"%h %s"
9a4c59a commit e
7fda761 commit df
2f94a70 commit c
db90798 edited message commit b
fe7eea2 commit a
Рисунок 76. Результат редактирования истории

Если вы использовали edit с целью разбить один коммит на


несколько,
лько, возможностей git commit --amend уже недостаточно. Вы
должны начать с отмены этого коммита без очистки содержимого файлов
(т.е. в mixed режиме) выполнив git reset HEAD^.. Далее вы можете
добавлять изменения в индекс и производить коммиты в любом
количестве.
стве. Когда вы сделаете все необходимые коммиты, используйте
continue для продолжения перебазирования.
git rebase --continue
Для демонстрации разбиения одного коммита на несколько, запустим
перебазирование для предыдущего примера (см. рис 68), как показано на
рисунке 77.
$ git rebase -i
i HEAD~3
Рисунок 77. Пример разбиения одного коммита на три с использованием
инструкции edit

56
Затем изменяем инструкции таким образом, чтобы отредактировать
предпоследний коммит (см. рис. 78).
pick 0c9351d commit b
edit 73c3dfa commit c
pick b37ca24 commit d

Рисунок 78. Пример инструкций для редактирования коммита


в режиме интерактивного перебазирования

В итоге получим вывод, показанный на рисунке 80.


Stopped at 0c9351d... commit b
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue

Рисунок 80. Пример вывода результата после редактирования коммита

Сначала отменяем текущий коммит с сохранением изменений при


помощи команды git reset HEAD^. Наш коммит содержит три ханка,
разобьём коммит на три (под каждый ханк свой коммит) при помощи
команды git add -p (см. рис. 81). Повторим команды git add –p,
выбрав опцию y для добавления в индекс текущего ханка и git commit
–m трижды, для каждого нового коммита. Как только все изменения будут
зафиксированны, продолжим перебазирование командой git rebase –
continue.
$ git reset HEAD^
Unstaged changes after reset:
M index.php

$ 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 commit -m 'commit c1'


[detached HEAD bf6c8c0] commit c1
1 file changed, 1 insertion(+), 1 deletion(-)

$ 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 commit -m 'commit c2'


[detached HEAD bf6c8c0] commit c2
1 file changed, 1 insertion(+), 1 deletion(-)

$ git commit –a -m 'commit c3'


[detached HEAD bf6c8c0] commit c3
1 file changed, 1 insertion(+), 1 deletion(-)

$ git status
# Not currently on any branch.
nothing to commit (working directory clean)

$ git rebase --continue


Successfully rebased and updated refs/heads/master.

$ git log --pretty=format:"%h %s"


b37ca24 commit d
2d1b1dd commit c3
05154a8 commit c2
0d81691 commit c1
0c9351d commit b

59
fe7eea2 commit a
Рисунок 81. Разбиения предпоследнего коммита

Стоит отметить, что у команды git add –p есть несколько


вариантов дальнейших действий. Если вы введете ?, Git отобразит, что
именно вы можете сделать (см. рис. 82).

Добавить в индекс эту часть [y,n,a,d,/,j,J,g,e,?]? ?


y - добавить в индекс эту часть
n - не добавлять в индекс эту часть
a - добавить в индекс эту и все оставшиеся в этом файле части
d - не добавлять в индекс эту и все оставшиеся в этом файле части
g - перейти к некоторой части файла (g - показывает список частей
и затем выполняет переход, g<N> - перейти к части N)
/ - найти часть, соответствующую регулярному выражению
j - отложить принятие решения по этой части, перейти к следующей
части, решение по которой не принято
J - отложить принятие решения по этой части, перейти к следующей
части
k - отложить принятие решения по этой части, перейти к предыдущей
части, решение по которой не принято
K - отложить принятие решения по этой части, перейти к предыдущей
части
s - разбить текущую часть на части меньшего размера
e - вручную отредактировать текущую часть
? - отобразить помощь
Рисунок 82. Варианты действий команды git add –p

4.2 Редактирование истории кода в системе Mercurial

В системе контроля версий Mercurial редактирование истории


коммитов осуществляется с помощью расширения histedit. Как и любое
расширение, оно должно быть явно включено в файле настроек для
использования (см. рис. 83).
60
[extensions]
histedit =
Рисунок 83. Пример включения расширения histedit

Вы можете передать команде histedit хеш коммита, начиная с


которого вы хотите редактировать историю (команда откажется работать,
если встретит опубликованный коммит). Чаще всего эту команду удобнее
запускать в виде hg histedit --outgoing, что позволяет вам
отредактировать все коммиты, которые еще не были отправлены на сервер
(т.е. те, которые вытолкнет очередной push). После чего вы также
получаете для редактирования файл с планом редактирования истории в
виде перечня коммитов с командами. Вы можете перемещать строчки
коммитов для изменения их порядка. Набор команд немного отличается от
интерактивного перебазирования Git (см. таблицу 2)
Таблица 2. Команды редактирования истории Mercurial
Команда Описание
pick Использовать коммит.
mess Отредактировать сообщение коммита.
edit Отредактировать содержание коммита. Может использоваться для
разбиения коммита на несколько.
fold Слить коммит с предыдущим, аналогично squash в Git.
drop Удалить коммит.

Редактирование коммита в Mercurial осуществляется немного по-


другому: вам не нужно его отменять. Вы получаете файлы с содержимым
коммита и можете их редактировать, а также делать дополнительные
коммиты. Для частичного коммита ханков файла можно воспользоваться
визуальным интерфейсом или расширением record. После завершения
редактирования, при выполнении hg histedit --continue

61
оставшиеся изменения создадут коммит, чье сообщение по умолчанию
будет совпадать с сообщением редактируемого коммита.
Обратите внимание, что в Mercurial для удаления коммита надо явно
использовать команду drop, а не просто удалить строчку. Это страхует от
случайных малозаметных удалений, что важно – восстановить удаленный
коммит будет невозможно.
При выполнении редактирования истории коммитов следует быть
очень внимательным, так как после ее завершения предыдущее состояние
стирается, и вернуться к нему будет невозможно. Если редактирование
пошло не так, вы можете прервать его с помощью git rebase --
abort или hg histedit --abort и начать заново.

62
Учебное издание
Олег Александрович Сычев
Григорий Владимирович Терехов

СОВРЕМЕННЫЕ СИСТЕМЫ
РАСПРЕДЕЛЕННОГО КОНТРОЛЯ ВЕРСИЙ

Учебно-методическое пособие

Редактор Л. Н. Рыжих

Темплан 2018 г. (учебники и учебные пособия). Поз. № 113.


Подписано в печать 15.10.2018. Формат 60x84 1/16. Бумага газетная.
Гарнитура Times. Печать офсетная. Усл. печ. л. 3,72. Уч.-изд. л. 2,78.
Тираж 50 экз. Заказ

Волгоградский государственный технический университет.


400005, г. Волгоград, просп. В. И. Ленина, 28, корп. 1.

Отпечатано в типографии ИУНЛ ВолгГТУ.


400005, г. Волгоград, просп. В. И. Ленина, 28, корп. 7.
63

Вам также может понравиться