Урок курса

Объединить ветки и прочитать граф

Git и GitHub с нуля: бесплатный курс с практикой

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

Условие перед слиянием: чистая рабочая папка

Перед git merge нужно убедиться, что рабочая папка и индекс чистые. Для этого достаточно выполнить git status и прочитать последнюю строку вывода:

nothing to commit, working tree clean

Эта строка означает две вещи одновременно: в индексе нет staged-изменений, и ни один отслеживаемый файл в рабочей папке не отличается от того, что лежит в индексе. Грубо говоря — всё, что было изменено, уже зафиксировано.

Почему это важно именно здесь? Не потому что Git всегда откажет при грязном состоянии. Git действительно может выполнить слияние, если незакоммиченные правки не пересекаются с изменениями из сливаемой ветки — тогда они просто сохранятся в рабочей папке после merge. Но именно это и создаёт путаницу: после слияния вы видите изменённые файлы и уже не можете с первого взгляда понять, это результат слияния или ваши старые правки.

Если же сливаемая ветка трогает те же файлы, что и ваши незакоммиченные изменения, Git защитно остановится и выдаст что-то вроде:

error: Your local changes to the following files would be overwritten by merge:
    README.md
Please commit your changes or stash them before you merge.

Но на этот отказ нельзя рассчитывать как на защитный механизм — он срабатывает не всегда. Надёжный способ один: перед слиянием проверить git status и убедиться в чистоте. Тогда результат merge будет предсказуемым и читаемым.

В обоих подготовленных репозиториях этого урока (prepared/merge_fast_forward и prepared/merge_diverged) рабочая папка уже чистая. Шаг с git status в начале каждого опыта — это привычка, которую стоит вырабатывать, а не формальность.

Fast-forward: сдвиг указателя без нового коммита

Fast-forward срабатывает в одном конкретном случае: когда текущая ветка является прямым предком сливаемой. То есть сливаемая ветка просто продолжает историю текущей — между ними нет никакого «расхождения».

Представьте цепочку коммитов:

A --- B (main) --- C (feature)

Здесь main стоит на коммите B, а feature ушла вперёд до C. Коммит B — прямой предок C. Когда вы находитесь на main и выполняете git merge feature, Git не создаёт новый коммит — он просто сдвигает указатель main вперёд до C:

A --- B --- C   (HEAD -> main, feature)

Для отдельного опыта распакуйте полный git_practice.zip в новую пустую папку. В терминале перейдите в получившуюся папку git_practice, затем откройте подготовленный репозиторий:

cd prepared/merge_fast_forward

В нём уже есть .git, ветки и история; повторный git init не нужен. При проверке этой копии архива с git version 2.55.0.windows.3 получен такой вывод. Короткие ID берите из своего git log: оформление вывода может отличаться, а существенны положение меток и связь коммитов.

$ git status
On branch main
nothing to commit, working tree clean

Граф до слияния:

$ git log --graph --oneline --all
* 4a42e21 (feature) Добавить авторов
* e0b770e (HEAD -> main) Первый справочник

main стоит на e0b770e, а feature — на следующем коммите 4a42e21 «Добавить авторов». История main полностью входит в историю feature.

$ git merge feature
Updating e0b770e..4a42e21
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

Fast-forward сообщает о продвижении указателя. В этом опыте README.md получил две строки из уже существующего коммита feature; нового коммита не появилось.

Граф после:

$ git log --graph --oneline --all
* 4a42e21 (HEAD -> main, feature) Добавить авторов
* e0b770e Первый справочник

Обе метки теперь стоят у 4a42e21. Сравнение с исходным графом показывает, что main переместилась к прежней вершине feature, а набор коммитов остался тем же.

Merge-коммит при разошедшихся ветках и флаг -m

Ситуация меняется, когда у каждой ветки есть свои коммиты, которых нет у другой. Такое происходит, если после создания feature в main тоже появились новые коммиты — ветки «разошлись» от общего предка.

Условная схема разошедшихся веток выглядит так:

      D --- E   (feature)
     /
A---B---C       (main)

main и feature вышли из общего коммита B, но пошли в разные стороны: main получила C, feature — D и E. Fast-forward здесь невозможен: main уже не является прямым предком feature.

Чтобы объединить их, Git создаёт merge-коммит — особый коммит с двумя родителями: последним коммитом main (C) и последним коммитом feature (E). Этот коммит фиксирует момент слияния двух независимых линий истории.

По умолчанию Git открывает текстовый редактор, чтобы вы ввели сообщение merge-коммита. Чтобы не открывать редактор, используют флаг -m:

git merge -m "Merge feature into main" feature

Флаг -m работает точно так же, как в git commit -m: передаёт сообщение прямо в командной строке. При fast-forward он ни на что не влияет — merge-коммита всё равно нет.

В prepared/merge_diverged из того же архива — другой, независимый репозиторий. Вернитесь в корень распакованной папки git_practice и откройте его:

cd prepared/merge_diverged

В проверенной копии у каждой ветки по одному собственному коммиту после общего предка:

$ git status
On branch main
nothing to commit, working tree clean
$ git log --graph --oneline --all
* 67925ef (feature) Добавить навигацию
| * 4a42e21 (HEAD -> main) Добавить авторов
|/  
* e0b770e Первый справочник

Общий предок — e0b770e «Первый справочник». Коммит main «Добавить авторов» изменил README.md; коммит feature «Добавить навигацию» добавил navigation.md. Эти изменения объединяются без конфликта.

$ git merge -m "Merge feature into main" feature
Merge made by the 'ort' strategy.
 navigation.md | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 navigation.md

Здесь создан merge-коммит, а navigation.md появился в рабочей папке main. Изменения README из прежнего коммита main сохранились.

$ git log --graph --oneline --all
*   5f862bf (HEAD -> main) Merge feature into main
|\  
| * 67925ef (feature) Добавить навигацию
* | 4a42e21 Добавить авторов
|/  
* e0b770e Первый справочник

В этом запуске новый коммит получил короткий ID 5f862bf; у вас ID нового объекта может отличаться. Метка main стоит у него, а feature осталась у 67925ef. Два родителя нового коммита — прежняя вершина main (4a42e21) и вершина feature (67925ef). Читайте их связь по линиям графа, а не по заранее заданному хешу.

Чтение графа git log --graph --oneline --all

git log --graph --oneline --all рисует ASCII-граф слева от каждого коммита. Три флага работают вместе:

  • --graph — добавляет символы ветвления и слияния;
  • --oneline — сжимает каждый коммит до одной строки: короткий хеш + сообщение;
  • --all — включает в вывод все локальные ветки, а не только текущую.

Выход из просмотра — клавиша q.

Символы графа: — * — сам коммит;

  • | — вертикальная линия, продолжение ветки;
  • \ — уход ветки вправо (ответвление или начало линии слияния);
  • / — возврат ветки влево (линия сходится к точке слияния).

Паттерн 1: прямая линия (fast-forward)

* 4a42e21 (HEAD -> main, feature) Добавить авторов
* e0b770e Первый справочник

Все * в одном столбце. Рядом с одним коммитом стоят обе метки веток — HEAD -> main и feature. Нового коммита нет: указатель просто сдвинулся.

Паттерн 2: сходящиеся линии (merge-коммит)

*   5f862bf (HEAD -> main) Merge feature into main
|\  
| * 67925ef (feature) Добавить навигацию
* | 4a42e21 Добавить авторов
|/  
* e0b770e Первый справочник

Мerge-коммит виден как * со звёздочкой, после которой идёт |\ — это расхождение двух линий к двум родителям. Левая линия * | — история main, правая | * — история feature. Строка |/ показывает, что линии сходятся в один предок e0b770e.

По этим двум паттернам можно сразу определить, что произошло: если после git merge граф остался прямой линией — был fast-forward; если появилась точка с расходящимися линиями под ней — был создан merge-коммит с двумя родителями.

Задача 1

В чистом репозитории main находится на А, а feature — на потомке Б. У main нет собственных новых коммитов после А. Обычное слияние допускает fast-forward; принудительное создание merge-коммита не включено. Выполнили git merge feature на main. Что получится?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Практика: из свежей отдельной копии ZIP откройте git_practice/prepared/merge_fast_forward. Подтвердите чистое состояние, прочитайте граф, выполните слияние feature в текущую main и сравните граф и файлы. Запишите указатели веток до и после fast-forward и подтвердите рабочее содержимое main. Письменно объясните коллеге, почему отсутствие нового merge-коммита не означает отсутствие результата слияния.

Платформа проверяет только ответ ниже; действия в своём репозитории и письменные ответы вы проверяете самостоятельно.

В контрольной записи после fast-forward обе ветки показывают Б. Нужно ли переключаться на feature, чтобы увидеть в рабочей папке включённые изменения Б?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Практика: начните со свежей отдельной копии git_practice/prepared/merge_diverged. Состояние чистое, ветки разошлись и изменяли разные файлы. Изучите граф, объедините feature в main с явным сообщением merge-коммита, затем проверьте файлы, status и новый граф. Запишите два разных родительских направления нового merge-коммита и подтвердите сохранность правок обеих веток.

Платформа проверяет только ответ ниже; действия в своём репозитории и письменные ответы вы проверяете самостоятельно.

В контрольном случае такой merge создал М с двумя родителями: Б от прежней main и В от feature. Какое объяснение этой структуры верно?

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку