linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть III — Ежедневный Git

$ глава 6 · 50 минут

Ветки

С главы 6 начинается ежедневная работа. До этого мы разобрали, что Git хранит и как, теперь - что с этим делать.

Ветки - главная причина, по которой Git победил остальные VCS. В Subversion ветка - это копия всего проекта в отдельной директории репозитория, операция занимает заметное время. В Git ветка - это файл с одним SHA. Создаётся за миллисекунду, удаляется так же.

Дешевизна веток поменяла подход к разработке. Вместо «давай заведу ветку для большого рефакторинга» - «давай заведу ветку для одной опечатки». Эта глава - про то, как этим пользоваться.

6.1 Ветка - это файл с одним SHA

Возьмём свежий репозиторий, сделаем коммит, заглянем в .git/:

bash
ls .git/refs/heads/
# main
cat .git/refs/heads/main
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Один файл. Внутри - 40 символов SHA коммита. Это всё.

Создать новую ветку - создать ещё один такой файл. Удалить - удалить файл. Передвинуть на другой коммит - переписать 40 символов. Все три операции для Git мгновенные.

bash
git branch feature           # создаст .git/refs/heads/feature
cat .git/refs/heads/feature
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
#
# Тот же SHA, что и у main - feature создан "от main".

После создания у нас два указателя на один коммит. Если сделать коммит на feature - её SHA сменится, main останется. Если сделать коммит на main - наоборот.

См. branch.

6.2 HEAD - указатель на указатель

У Git есть один особый файл - .git/HEAD. В нём написано, на какую ветку мы сейчас смотрим.

bash
cat .git/HEAD
# ref: refs/heads/main

Это значит: «текущая ветка - main». Все команды, которые работают с «текущим коммитом» (git status, git commit, git log), читают HEAD, идут по ссылке на refs/heads/main, оттуда достают SHA, и с этим SHA работают.

Когда мы переключаемся на другую ветку:

bash
git switch feature
cat .git/HEAD
# ref: refs/heads/feature

Поменялась одна строка в HEAD. И всё. Файлы в рабочем дереве Git привёл к состоянию feature (если оно отличалось от main).

Когда коммитим:

  1. Создаётся commit-объект.
  2. Берётся текущая ветка из HEAD (refs/heads/feature).
  3. Эта ветка двигается на новый SHA.
  4. HEAD продолжает указывать на ту же ветку - то есть автоматом «следует» за ней.

Получается «указатель на указатель»: HEAD → ветка → коммит.

6.3 Detached HEAD

Иногда HEAD указывает не на ветку, а прямо на коммит:

bash
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Это detached HEAD - «оторванный». Происходит когда переключаешься на коммит напрямую, на тег, или на удалённую ветку без локального аналога:

bash
git switch --detach v1.0     # явно
git checkout HEAD~3           # неявно
git checkout origin/main      # неявно

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

Git при detached предупреждает большим оранжевым текстом и подсказывает:

You are in 'detached HEAD' state. You can look around, make
experimental changes ... If you want to create a new branch
to retain commits you create, you may do so by running:
    git switch -c <new-branch-name>

Если ты эти коммиты сделал и нужно сохранить - создай ветку из текущего HEAD:

bash
git switch -c rescue

Эта ветка теперь держит твои коммиты, они не потеряются. См. detached-head.

6.3.1 Подводный камень: detached HEAD после git pull --rebase

Менее очевидный сценарий, как попасть в detached HEAD - прерванный git rebase. Если в середине rebase возникает конфликт, и ты вводишь git rebase --abort - всё нормально. Но если просто закроешь терминал или сделаешь git checkout на другую ветку без --abort, можно остаться с HEAD, указывающим на промежуточный «синтезированный» коммит.

Симптомы - git status пишет «HEAD detached at ...» вместо «On branch ...». Лечится тем же - git switch -c rescue если нужны коммиты, или git switch main если нет.

Reflog (git reflog) покажет полную последовательность состояний HEAD и поможет понять, где именно произошёл detach.

6.4 switch и restore - новые команды

Исторически git checkout делал три вещи:

  • переключал ветку
  • откатывал файлы в рабочем дереве
  • создавал ветку из коммита

Это было неудобно - одна команда для трёх операций приводила к опечаткам с потерей данных. В Git 2.23 (2019) ввели две специализированных:

  • git switch - только про ветки
  • git restore - только про файлы
bash
# Старый стиль (всё ещё работает)
git checkout feature                  # переключиться на feature
git checkout -b feature               # создать и переключиться
git checkout -- file.txt              # откатить файл
git checkout HEAD~3 -- file.txt       # достать файл из старого коммита
# Новый стиль
git switch feature                    # переключиться
git switch -c feature                 # создать и переключиться
git restore file.txt                  # откатить файл
git restore --source=HEAD~3 file.txt  # достать файл из старого коммита

Семантика та же, но имена ясные. Если только учишь Git - используй switch и restore. Если читаешь чужие туториалы и видишь checkout - это то же самое, просто старая запись.

Одно отличие: switch отказывается переключаться, если есть несохранённые изменения, которые будут перезаписаны. checkout в той же ситуации мог проглотить и забыть; switch строже.

6.5 Базовый цикл с веткой

Типичная история работы с новой фичей:

bash
# 1. Начинаем с обновлённой main
git switch main
git pull --ff-only
# 2. Создаём ветку для фичи
git switch -c feat/login-form
# 3. Работаем: правим, добавляем, коммитим
vim src/auth/login.ts
git add src/auth/login.ts
git commit -m "add login form skeleton"
# ... ещё несколько итераций ...
# 4. Запушили (с tracking)
git push -u origin feat/login-form
# 5. Создали PR, обсудили, поправили
# ... ещё коммиты, ещё push'ы ...
# 6. После merge PR - переключаемся обратно
git switch main
git pull --ff-only
# 7. Удаляем локальную ветку, она больше не нужна
git branch -d feat/login-form

-d (lowercase) удаляет только смерженные ветки. Если ветка не была смержена в текущую - Git откажет. Это страховка от потери работы. Принудительно - -D (uppercase), без вопросов.

Удалённая ветка после merge обычно удаляется автоматически (на GitHub есть галочка в настройках репо «automatically delete head branches»). Если не - git push origin --delete feat/login-form.

6.6 Fast-forward merge

Самая простая форма слияния. Возникает, когда у целевой ветки нет новых коммитов после ветвления.

до:    main:  A → B
       feat:  A → B → C → D
git switch main
git merge feat
после: main:  A → B → C → D   ← main "перемотался" на D
       feat:  A → B → C → D

Никакой merge-коммит не создаётся. Git просто переписывает файл .git/refs/heads/main со старого SHA на новый. История остаётся линейной.

Когда это работает: на main не было коммитов после того, как от неё отвели feat. Часто бывает с короткоживущими feature-ветками: завёл, поработал час, помержил.

Когда не работает: на main кто-то успел запушить свой коммит. Тогда true merge с трёхсторонним слиянием - см. главу 8.

Если хочешь, чтобы факт ветвления оставался в истории даже при возможности fast-forward - git merge --no-ff feat. Это создаст merge-коммит даже когда не обязательно. Полезно для команд, которые хотят видеть «вот тут была фича».

См. fast-forward и merge.

6.7 Tracking-ветки

Локальная ветка может отслеживать удалённую. После настройки Git знает, куда push и откуда pull, без явных аргументов.

bash
# При первом push с -u - установить tracking
git push -u origin feature

▸теперь feature → origin/feature

# При создании от удалённой
git switch -c local-name origin/remote-name

▸tracking устанавливается автоматически

# Поменять tracking уже существующей ветки
git branch -u origin/main main

После tracking:

bash
git status
# On branch feature
# Your branch is ahead of 'origin/feature' by 2 commits.

Эта строка про ahead/behind - это и есть результат tracking'а. Без него status молчит про remote.

Все tracking-ветки лежат в .git/refs/remotes/origin/:

bash
ls .git/refs/remotes/origin/
# HEAD  main  feature

Они обновляются при git fetch (и при pull, который внутри делает fetch). Это «слепок» того, что на сервере. Сравнивая main с origin/main, Git и считает ahead/behind.

6.8 Конвенции именования

Git не диктует, как называть ветки. Но команды и компании обычно договариваются о схеме. Самые частые:

  • main - основная ветка (раньше master, многие проекты переименовали).
    • develop - интеграционная (если используется GitFlow).
    • feat/<short-name> или feature/<short-name> - фичи.
    • fix/<short-name> или bugfix/<short-name> - багфиксы.
    • hotfix/<short-name> - срочные багфиксы в проде.
    • release/<version> - подготовка релиза (в GitFlow).
    • chore/<short-name> - обслуживающие задачи.
    • docs/<short-name> - документация.

Префикс с слэшем - не магия, это просто текст. Git разрешает в именах веток слэши, и многие GUI/инструменты группируют по ним («все feat/...» в одной папке в дереве).

Иногда добавляют автора или тикет:

  • dmitry/feat/login-form
  • feat/JIRA-1234-login-form

Главное - договориться в команде один раз и придерживаться. Технически Git разрешит любое имя из ASCII без спецсимволов.

Подробнее про стратегии - глава 11 (GitFlow / GitHub Flow / trunk-based).

6.9 Что делать, когда наошибался

Несколько сценариев, которые случаются у всех.

«Закоммитил не в ту ветку».

Если коммит ещё не запушен:

bash
# Запомним SHA коммита
LAST=$(git rev-parse HEAD)
# Откатим текущую ветку на один коммит назад
git reset --hard HEAD~1
# Переключимся на правильную ветку
git switch correct-branch
# Применим коммит сюда
git cherry-pick $LAST

cherry-pick - это «возьми один коммит из другого места и применить сюда». Подробно - в главе 8.

«Удалил ветку, в которой были несмерженные коммиты».

bash
git reflog
# ... покажет последние позиции HEAD,
# в одной из них будет нужный SHA ...
git branch rescue <sha-из-reflog>

Reflog хранит позиции HEAD 30+ дней. Пока не было git gc --prune=now - коммиты живы. Подробно - в главе 9.

«Переключился, и пропали изменения».

Если изменения были застейджены или закоммичены - они в reflog. Если были только в рабочем дереве (не add, не commit) - потеряны. Это и есть причина коммитить чаще: не жди готовности, ставь промежуточные снимки.

«Случайно сделал коммит в detached HEAD».

bash
git switch -c rescue
# Ветка из текущего HEAD, коммит спасён

После этого можно git switch main и git merge rescue или cherry-pick.

Уроки в sandbox

lab-6.1. Полный цикл с веткой: создать, разработать, помержить, удалить

Цель - пройти типичный workflow с веткой и увидеть всё в git log --graph. После лабы становится понятно, чем fast-forward отличается от non-ff, и как branch -d страхует от потери работы.

  1. Создай новый репозиторий, сделай первый коммит: mkdir branches-lab && cd branches-lab && git init && echo "first" > a.txt && git add a.txt && git commit -m "first commit".

  2. Создай ветку feature и переключись на неё: git switch -c feature. Проверь содержимое .git/HEAD и .git/refs/heads/feature - оба должны совпадать с SHA первого коммита.

  3. Сделай два коммита на feature: добавь файлы b.txt и c.txt, каждый коммитом по отдельности. После - git log --oneline --graph --all. Должно быть видно три коммита на feature, main стоит на первом.

  4. Вернись на main: git switch main. Проверь, что b.txt и c.txt исчезли из рабочего дерева (они есть только на feature).

  5. Сделай fast-forward merge: git merge feature. Снова посмотри git log --oneline --graph --all. Заметь: merge-коммита нет, main просто перемотался.

  6. Удали ветку: git branch -d feature. Заметь - Git разрешил, потому что feature смержена.

  7. Воспроизведи non-ff. Сделай два коммита на main, потом создай новую feat-ветку, два коммита там тоже, переключись на main, git merge feat. Теперь Git создаст merge-коммит - это three-way merge, тема главы 8.

  8. Попробуй удалить эту feat без merge'а: верни состояние через git reset --hard <sha-до-merge>, потом git branch -d feat - увидишь отказ. Принудительно: git branch -D feat.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Ветка в Git - файл в `.git/refs/heads/` с одним SHA коммита внутри. Создание, переключение, удаление - мгновенные операции.
  • HEAD - указатель на текущую ветку (или прямо на коммит в detached-режиме). Коммиты двигают ветку, на которую указывает HEAD.
  • Detached HEAD - HEAD указывает не на ветку. Безопасно для просмотра, но коммиты в этом состоянии теряются, если не создать ветку.
  • С Git 2.23 есть `git switch` (для веток) и `git restore` (для файлов). Старый `git checkout` всё ещё работает, но новые команды менее опасны.
  • Fast-forward merge - простое перемещение указателя без создания merge-коммита. Возможен, если у целевой ветки нет новых коммитов после ветвления.
  • Tracking-ветки связывают локальную ветку с удалённой. После `-u` команды `push`/`pull` знают, куда идти, и `status` показывает ahead/behind.
  • `git branch -d` отказывает удалять несмерженную ветку (страховка). `-D` - принудительно.
  • Большинство «ошибок» с ветками поправимы через reflog. Главное - не делать `git gc --prune=now` сразу после катастрофы.

Контрольные вопросы

  1. Чем `git switch feature` отличается от `git checkout feature`?

    Показать ответ

    Семантически - ничем, обе команды переключают ветку. Но switch появился в 2019 как специализированная команда только для веток. Он строже: отказывается переключаться, если есть unstaged изменения, которые будут перезаписаны. checkout в той же ситуации мог иногда «проглотить» правки без предупреждения. Если есть выбор - используй switch (и restore для файлов), это безопаснее. checkout сохраняется для совместимости со старыми скриптами и туториалами.

  2. Я в detached HEAD сделал три коммита и переключился на main. Где мои коммиты, как их вернуть?

    Показать ответ

    Коммиты живы - они в .git/objects/, но ни одна ветка на них не указывает. Открой git reflog - увидишь последние позиции HEAD, включая SHA каждого из тех трёх коммитов. Возьми SHA последнего (вершины) и сделай git branch rescue <sha> или git switch -c rescue <sha>. Теперь ветка rescue держит твои коммиты, и они не потеряются при следующем gc. Дальше можно git switch main && git merge rescue или cherry-pick, смотря что нужно.

  3. Почему `git branch -d feature` иногда отказывает с «not fully merged», а иногда нет?

    Показать ответ

    Отказывает, если в feature есть коммиты, которых нет в текущей ветке (или в её апстриме). Это значит, что после удаления эти коммиты станут недостижимыми, и Git их в итоге соберёт gc-ом. Команда страхует от потери работы. Если ветка реально не нужна - git branch -D feature (uppercase) удалит без вопросов. Перед -D можно посмотреть, что внутри: git log main..feature --oneline.

  4. Что значит, когда `git status` пишет `Your branch is ahead of 'origin/main' by 2 commits`?

    Показать ответ

    У тебя локально на ветке main есть 2 коммита, которых нет в origin/main (удалённой версии). Чтобы их увидели другие, нужно git push. Если параллельно появилась строка behind by N commits, значит наоборот: на сервере есть коммиты, которых нет у тебя, и git pull их затащит. Если и ahead, и behind - ветка разошлась с удалённой, нужно merge или rebase (см. главу 8). Сама строка появляется только если у локальной ветки настроен tracking; без него status про remote молчит.

  5. В чём разница между удалением локальной и удалённой ветки?

    Показать ответ

    git branch -d feature удаляет ветку только в твоём локальном .git/refs/heads/. На удалённом сервере (origin) ветка остаётся. Чтобы удалить и удалённую - отдельная команда: git push origin --delete feature (или старая запись git push origin :feature). Многие хостинги (GitHub, GitLab) автоматически удаляют ветку после merge PR - это настройка в репозитории. После удалённого удаления у тебя в git branch -a всё ещё может висеть origin/feature - это локальный кэш. Очистить - git fetch --prune или git remote prune origin.

← Предыдущая05-three-areasСледующая →07-commits-pro
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки