Первая задача недоделана и ломает сборку, а вторую уже надо начинать, и в одной папке это невозможно: либо доделывать первую, либо жить со сломанной. Ветка снимает этот выбор: работа над задачей идёт в изоляции, можно сколько угодно коммитить и экспериментировать, не трогая рабочую версию, а потом одним действием влить результат обратно. Ветки в Git дешёвые и мгновенные, поэтому весь командный процесс, задачи, ревью, релизы, построен на них.
Одна и та же ветка fix в двух исходах. Сверху main стоял на месте — Git просто переносит закладку main на последний коммит ветки, история остаётся прямой линией. Снизу main ушёл вперёд — появляется коммит с двумя родителями, и в истории видно, где линии разошлись и где сошлись.
Ветка — это указатель
Ментальная модель: история — цепочка коммитов, а ветка — передвижная закладка, указывающая на один из них. Новый коммит в ветке просто сдвигает закладку вперёд. Создать ветку = поставить ещё одну закладку — операция мгновенная, ничего не копируется.
git branch # список веток, * — текущая
git switch -c fix-cart-total # создать ветку и переключиться на неё
git switch main # вернуться на main
Специальная закладка HEAD отмечает, где вы сейчас. Переключение ветки меняет файлы в рабочей директории на снимок той ветки — поэтому Git откажется переключаться, если незакоммиченные правки при этом затрёт (спрячьте их в git stash — карман для незавершённого). А если в целевой ветке файл ровно такой же, Git молча возьмёт вашу правку с собой, и она окажется на другой ветке. Отсюда регулярная паника «мои изменения не в той ветке»: Git их не потерял, он их перенёс. Привычка простая — git status до переключения, а не после.
Сам карман устроен просто: git stash убирает незакоммиченные правки в сторону и оставляет чистую рабочую директорию, git stash pop возвращает их обратно, git stash list показывает, что лежит. Новые, ещё не добавленные файлы обычный stash не трогает, для них git stash -u. Подробности и ловушки кармана разобраны в статье про отмену.
git stash # спрятать правки
git switch main # теперь переключение проходит
git switch - # вернуться на прежнюю ветку
git stash pop # достать правки обратно
Рабочий ритм: под каждую задачу — своя ветка от свежего main (fix-cart-total, feature-export-csv), в ней коммиты, после вливания ветка удаляется. Ветки — расходный материал, а не долгожители.
Удаляют ветку двумя буквами, и разница между ними важна. git branch -d fix-cart-total удаляет, только если все коммиты ветки уже влиты куда-то ещё; иначе Git откажет с not fully merged, и это защита от потери работы. git branch -D удаляет без проверки. Отказ -d бывает и ложным: если ветку слили через squash, в main уехал один новый коммит, а ваших там формально нет, и Git не видит их влитыми; содержимое при этом на месте, и отвечать надо -D. Правило: -d по умолчанию, -D только когда точно знаете, куда делись коммиты.
Слияние: fast-forward и merge-коммитспросят на собеседовании
Влить ветку в main:
git switch main
git merge fix-cart-total
Возможны два сценария:
- Fast-forward. Пока вы работали, main не двигался. Тогда сливать нечего — Git просто передвигает закладку main вперёд на ваши коммиты. История остаётся прямой линией.
- Merge-коммит. Main успел уйти вперёд (коллеги вливали своё). Git создаёт специальный коммит с двумя родителями, объединяющий обе линии. История показывает ромб: разошлись — сошлись. Это и нарисовано на анимации выше.
Оба исхода нормальны. Флаг --no-ff заставляет Git сделать merge-коммит даже там, где хватило бы fast-forward, — так в истории остаётся видимая граница задачи. Граница нужна не для красоты: после fast-forward коммиты задачи неотличимы от соседних, и откатить задачу целиком значит откатывать их по одному; merge-коммит же откатывается одной командой git revert -m 1 <sha>, и вся задача исчезает из main разом. Это и есть единственный практический довод за --no-ff. Какой вариант предпочитать и почему некоторые команды запрещают merge-коммиты — вопрос модели ветвления и rebase; пока достаточно понимать, что происходит.
Конфликты: не авария, а вопросспросят на собеседовании
Конфликт возникает, когда Git не может свести две версии сам. Самый частый случай — обе ветки изменили одни и те же строки по-разному. Git не угадывает, чья правка правильная, — он останавливается и спрашивает вас:
<<<<<<< HEAD
цена = сумма * 0.9; // ваша версия (текущая ветка)
=======
цена = сумма * 0.85; // версия вливаемой ветки
>>>>>>> fix-discount
Порядок действий без паники:
git statusпокажет конфликтующие файлы.- Открыть файл, найти маркеры
<<<<<<</>>>>>>>, решить по смыслу: оставить одну версию, другую или написать объединённую. Маркеры удалить. git add файл— пометить конфликт решённым.git commit— завершить слияние. Передумали —git merge --abortвернёт всё как было.
Смотреть конфликт можно не только глазами в файле. git diff во время слияния показывает особый формат с двумя колонками знаков: что пришло из вашей ветки, что из вливаемой и что вы уже поправили. Когда ответ известен заранее, файл целиком берут с одной стороны: git checkout --ours путь оставляет вашу версию, --theirs вливаемую (при rebase стороны меняются местами, потому что там «вашей» считается ветка, на которую переносят). А на практике конфликты чаще всего разбирают в трёхпанельном виде IDE или git mergetool: слева ваша версия, справа чужая, посередине результат, и у каждого куска кнопки «взять слева», «взять справа», «оба».
Слияние останавливается на файле с маркерами, и дальше выбираете вы: оставить свою версию, взять чужую или свести обе в одну.
Дословное пересечение строк — не единственная форма конфликта. Бывает и так, что одна ветка файл удалила, а другая его правила: Git пишет CONFLICT (modify/delete), а git status помечает файл как deleted by us или deleted by them. Маркеров внутри файла тут нет — решать надо не текст, а судьбу файла: вернуть его (git add) или согласиться с удалением (git rm). Так же всплывают переименования и правки в соседних строках, которые дословно не пересеклись, но попали в один и тот же кусок изменений.
Два взрослых правила. Конфликт решается по смыслу задачи, а не механически: «оставить свою» — не стратегия, вы можете затереть чужой багфикс; сомневаетесь — спросите автора второй правки (blame покажет кого). И профилактика лучше лечения: короткие ветки и регулярное подтягивание main в свою ветку делают конфликты маленькими и редкими. Подтянуть можно двумя способами, и это два разных решения:
git switch main && git pull # сначала обновить сам main
git switch feature-export
git merge main # влить свежий main в свою ветку — появится merge-коммит
# либо
git rebase main # переставить свои коммиты поверх свежего main
merge ничего не переписывает и безопасен всегда; rebase даёт прямую историю, но создаёт ваши коммиты заново — где так можно, а где нельзя, разбирает статья про rebase.
Ветки как навигация по проекту
Ветки — это ещё и навигация. Проверить чужую работу до ревью — переключиться на её ветку. Срочный фикс во время большой задачи — stash, ветка от main, фикс, обратно. Посмотреть, как выглядел код в проде — git switch --detach v2.3.1 по тегу релиза.
Про последнее отдельно. Без --detach Git откажется: он ждёт имя ветки, а тег не ветка. С ним вы окажетесь «вне веток», в состоянии detached HEAD: смотреть можно, а коммитить не стоит, потому что такой коммит не принадлежит ни одной ветке и после переключения его придётся искать через reflog. git switch main возвращает обратно. Умение свободно ходить по веткам превращает Git из «сдать код» в машину времени проекта.
Как называть ветки
fix, fix2, vasya-test, new: через месяц в git branch -a сорок таких имён, и никто не помнит, что в них и можно ли их удалять. Имя ветки читают чаще, чем её код: в списке веток, в pull request, в сообщении CI, поэтому оно должно отвечать на два вопроса: что это и откуда взялось.
Ходовая схема: тип, косая черта, номер задачи и два-три слова о сути, всё строчными и через дефис:
feature/PROJ-142-export-orders-csv новая возможность
fix/PROJ-171-null-customer-on-refund исправление в рабочей ветке
hotfix/2.3.1-payment-timeout срочная правка в проде
chore/upgrade-postgres-driver обслуживание без изменения поведения
release/2.4 релизная ветка, если модель их предполагает
Косая черта здесь не украшение: Git хранит ветки как файлы в .git/refs/heads/, и feature/ становится папкой, поэтому git branch --list 'feature/*' показывает только рабочие ветки. Из этого же следует ограничение: если есть ветка feature, ветку feature/login создать нельзя, файл и папка с одним именем не уживаются. Номер задачи в имени связывает ветку с трекером, и многие трекеры по нему сами показывают ветку и pull request в карточке задачи. Суть в два-три слова нужна тому, кто смотрит список веток без трекера под рукой.
Чего избегать: пробелов и кириллицы, которые придётся экранировать в каждой команде; имён, совпадающих с тегами (v2.3.1 как ветка и как тег заставит Git спрашивать, что вы имели в виду); имён длиннее одной строки в списке. Соглашение о типах фиксируют письменно, в CONTRIBUTING.md, и проверяют на сервере: GitLab и GitHub умеют запрещать push ветки, имя которой не подходит под шаблон.
Глубже: теги и версиирасширенное
Ветка двигается с каждым коммитом, а версия должна стоять на месте: «v2.3.1» всегда обязана указывать на один и тот же снимок. Для этого и есть тег, указатель, который Git никогда не двигает.
Тегов два вида. Лёгкий (git tag v2.3.1) это просто имя для коммита. Аннотированный (-a) это полноценный объект: у него есть автор, дата и сообщение, его можно подписать, и git describe считает версию от него. Релизы помечают аннотированными.
git tag -a v2.3.1 -m "Release 2.3.1"
git push origin v2.3.1 # теги не уезжают с обычным push
git push --tags # или все сразу
git checkout v2.3.1 # посмотреть код релиза: detached HEAD, коммитить отсюда нельзя
git tag -d v2.3.1 && git push origin :refs/tags/v2.3.1 # удалить, если поставили не туда
Удалять и переставлять опубликованные теги можно только до того, как ими воспользовались: CI, который собирает образ по тегу, и коллега, который его уже скачал, получат разные снимки под одним именем. Поэтому ошибку в релизе чинят новым тегом v2.3.2, а не переносом старого.
Коротко
- Ветка — передвижной указатель на коммит; создание мгновенно. Задача = ветка, влили = удалили.
- HEAD — где вы; переключение меняет файлы; незакоммиченное — в stash.
- Merge: fast-forward (main не двигался — прямая линия) или merge-коммит с двумя родителями (ромб).
- Конфликт — вопрос, а не авария: маркеры в файле → решить по смыслу → add → commit;
--abortотменяет. - Профилактика конфликтов: короткие ветки, регулярно подтягивать main.
- Версия это аннотированный тег:
git tag -a v2.3.1 -m …и отдельныйgit push origin v2.3.1; опубликованный тег не переставляют. git branch -dудаляет только влитую ветку и ложно отказывает после squash,-Dбез проверки;stashиstash popпрячут и возвращают правки при переключении.- Конфликт смотрят через
git diff, берут файл целиком через--ours/--theirsили разбирают в трёх панелях IDE;--no-ffнужен, чтобы откатить задачу однимrevert -m 1.
Что почитать дальше
- Удалённые репозитории — push, pull и работа с сервером.
- Rebase и cherry-pick — альтернатива merge и перенос отдельных коммитов.
- Модели ветвления — как команды организуют ветки: git flow, GitHub flow, trunk-based.
- Отмена и спасение — что делать, если слияние пошло не туда: reset, revert, reflog.