Перейти к содержимому
RootPoligon

Ветки

Ситуация

Вы начали делать страницу расписания. Работы на час, страница пока сырая. В этот момент просят срочно поправить README на рабочей версии сайта. Если вы делаете всё в одной линии истории, сырая страница уйдёт вместе со срочной правкой.

Главная мысль

Ветка — это подвижное имя, которое указывает на последний коммит линии работы. Новый коммит сдвигает текущую ветку вперёд, остальные остаются на месте. Переключение ветки меняет файлы в рабочей папке на снимок из её последнего коммита.

Важное следствие: незакоммиченные изменения не принадлежат ни одной ветке. Они лежат в рабочей папке и при переключении либо переезжают с вами, либо мешают переключиться.

Новые слова

  • Ветка (branch) — имя, указывающее на коммит; main — основная ветка курса.
  • Текущая ветка — та, в которую попадёт следующий коммит; в git branch отмечена *.
  • git switch — переключиться на ветку; git switch -c имя — создать ветку и сразу переключиться.

Разбор: страница расписания в отдельной ветке

Папка ~/git-course/site. Исходное состояние — конец главы 3: ветка main, working tree clean. Если начинаете здесь — setup.md. Хеши — пример.

Создаём ветку и работаем в ней

git branch
* main
git switch -c schedule-page
Switched to a new branch 'schedule-page'

Создаём страницу и коммитим её — уже в новой ветке:

cat > schedule.html <<'EOF'
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>Расписание</title></head>
<body>
<h1>Расписание</h1>
<ul>
<li>Вторник, 19:00 — Git: основы</li>
<li>Четверг, 19:00 — практика</li>
</ul>
</body>
</html>
EOF
git add schedule.html
git commit -m "Добавить страницу расписания"
[schedule-page 85bf960] Добавить страницу расписания
 1 file changed, 11 insertions(+)
 create mode 100644 schedule.html

В квадратных скобках — ветка, в которую лёг коммит.

Срочная правка на main

Предскажите: будет ли schedule.html в папке после переключения на main?

git switch main
ls
Switched to branch 'main'
README.md
about.html
index.html
site.conf

Файла нет: в последнем коммите main его не существует. Он не удалён — он в истории ветки schedule-page.

Срочная правка — перечислить страницы в README. sed вставляет строку после второй строки файла:

sed -i '2a Страницы: index.html, about.html.\n' README.md
git add README.md
git commit -m "Перечислить страницы в README"
git log --oneline --graph --all -4
* 64e50a3 Перечислить страницы в README
| * 85bf960 Добавить страницу расписания
|/
* 04549f4 Вернуть порт 8082
* 3c45247 Добавить контакты на страницу «О нас»

--graph --all рисует все ветки: от общего коммита 04549f4 история разошлась на две линии.

Незакоммиченное переезжает с вами

Вернёмся к задаче и начнём ссылку на расписание на главной — но пока не коммитим:

git switch schedule-page
sed -i 's|<p><a href="about.html">О нас</a></p>|&\n<p><a href="schedule.html">Расписание</a></p>|' index.html
git switch main
Switched to branch 'main'
M	index.html

Переключение прошло, и правка index.html приехала на main: index.html одинаков в обеих ветках, Git нечего перезаписывать, и он оставляет вашу правку в папке. Легко потом закоммитить её не в ту ветку. Вернёмся:

git switch schedule-page
Switched to branch 'schedule-page'
M	index.html

Практика: закончить задачу в ветке

Исходное состояние — конец разбора: вы на ветке schedule-page, index.html изменён и не закоммичен. Если начинаете здесь — setup.md.

  1. Допишите в конец README строку о расписании:

    printf '\nРасписание занятий — на странице schedule.html.\n' >> README.md
    
  2. Предскажите, что ответит git switch main теперь. Выполните и сравните.

  3. Закончите задачу: сохраните ссылку и строку README одним коммитом «Добавить ссылки на страницу расписания» в ветке schedule-page.

  4. Переключитесь на main и убедитесь, что там ссылки ещё нет.

Признаки успеха:

  • git log --oneline --graph --all -5 показывает два коммита в schedule-page и один новый в main;
  • на main: git status — working tree clean, grep -c schedule index.html — 0.
Подсказка 1

README в двух ветках уже разный: на main есть строка «Страницы: …». Что сделает Git с вашей правкой README, если переключение заменит файл версией из main?

Подсказка 2

Git откажется переключаться и подскажет два выхода. Один из них — тот, что нужен для законченной задачи.

Подсказка 3

git add index.html README.md → git commit -m "…" → git switch main.

Решение — в answers/04-branches.md.

Если результат другой

  • Коммит попал в main, а не в ветку. Посмотрите git branch перед коммитом: * стоит у текущей ветки. Перенести коммит между ветками — тема глав 5 и 7; пока сделайте вывод и проверяйте ветку перед git commit.
  • git switch пишет invalid reference. Опечатка в имени ветки. Список — git branch.
  • Переключение отказало, а вы не хотите коммитить сырую работу. Временное сохранение без коммита — git stash, глава 7.

Проверка понимания

  1. Вы на ветке schedule-page создали файл и не закоммитили его. Переключились на main. Где файл?
  2. Почему Git отказался переключаться из-за README, но пропустил изменённый index.html?

Ответы — в answers/04-branches.md.

Дальше: 5. Слияние и конфликт.

Подготовка исходного состояния (setup.md)

Блоки работают только в отдельной учебной папке ~/git-course. При неожиданном состоянии они отказывают до записи; ошибка после начала подготовки может оставить часть действий выполненной. Сначала прочитайте сообщение, не удаляйте существующие файлы. Для продолжения после предыдущей главы выбирайте только нужный блок.

Перед главой 4

Конец главы 3: коммит «Вернуть порт 8082». Выполняется после блоков «Перед главой 3».

bash <<'SETUP'
rp_before4() {
  # --- Проверки: ничего не меняют ---
  fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
  course_repo() {
    [ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
    git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
      || { fail "$1 — не учебный репозиторий курса"; return 1; }
  }
  quiet() {  # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
    local x f
    [ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
    for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
      git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
    done
    for x in rebase-merge rebase-apply; do
      [ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
    done
    [ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
    for f in $(git -C "$1" ls-files --others --exclude-standard); do
      case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
    done
  }
  last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
  no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
  absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
  command -v git >/dev/null || { fail "не найден git"; return 1; }
  course_repo ~/git-course/site || return 1
  quiet ~/git-course/site main || return 1
  last_is ~/git-course/site "Добавить контакты на страницу «О нас»" || return 1
  # --- Подготовка: после первой записи при ошибке скрипт останавливается ---
  (
    set -e
cd ~/git-course/site
sed -i 's/^PORT=80x2/PORT=8082/' site.conf
git add site.conf
git commit -q -m "Вернуть порт 8082" -m "Опечатка 80x2 попала в коммит «Обновить описание запуска в README»."
  )
  if [ $? -ne 0 ]; then
    echo "Ошибка после начала подготовки: часть шагов могла выполниться."
    echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
    echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
    return 1
  fi
  git -C ~/git-course/site log --oneline -1
  echo "Готово: конец главы 3."
}
rp_before4 </dev/null
SETUP

Практика главы 4

Конец разбора главы 4: ветка schedule-page с одним коммитом, в main новый коммит README, вы на schedule-page, index.html изменён и не закоммичен. Выполняется после блока «Перед главой 4». Ветки schedule-page быть не должно.

bash <<'SETUP'
rp_practice4() {
  # --- Проверки: ничего не меняют ---
  fail() { echo "Отказ: $1"; echo "Ничего не изменено."; return 1; }
  course_repo() {
    [ -d "$1/.git" ] || { fail "$1 — нет такого репозитория Git"; return 1; }
    git -C "$1" log --max-parents=0 --format=%s 2>/dev/null | grep -qx 'Первая версия учебного сайта' \
      || { fail "$1 — не учебный репозиторий курса"; return 1; }
  }
  quiet() {  # папка, ожидаемая ветка, разрешённые неотслеживаемые файлы через пробел
    local x f
    [ "$(git -C "$1" symbolic-ref --short -q HEAD)" = "$2" ] || { fail "в $1 текущая ветка не $2"; return 1; }
    for x in MERGE_HEAD REVERT_HEAD CHERRY_PICK_HEAD REBASE_HEAD; do
      git -C "$1" rev-parse -q --verify "$x" >/dev/null && { fail "в $1 не завершена операция Git ($x) — завершите или отмените её"; return 1; }
    done
    for x in rebase-merge rebase-apply; do
      [ -e "$(git -C "$1" rev-parse --git-path "$x")" ] && { fail "в $1 не завершён rebase"; return 1; }
    done
    [ -z "$(git -C "$1" status --porcelain --untracked-files=no)" ] || { fail "в $1 есть незакоммиченные изменения"; return 1; }
    for f in $(git -C "$1" ls-files --others --exclude-standard); do
      case " $3 " in *" $f "*) ;; *) fail "в $1 лишний неотслеживаемый файл $f"; return 1 ;; esac
    done
  }
  last_is() { [ "$(git -C "$1" log -1 --format=%s)" = "$2" ] || { fail "в $1 последний коммит не «$2»"; return 1; }; }
  no_branch() { ! git -C "$1" show-ref --verify -q "refs/heads/$2" || { fail "в $1 уже есть ветка $2"; return 1; }; }
  absent() { [ ! -e "$1" ] || { fail "$1 уже существует — переименуйте её сами, если хотите начать заново"; return 1; }; }
  command -v git >/dev/null || { fail "не найден git"; return 1; }
  course_repo ~/git-course/site || return 1
  quiet ~/git-course/site main || return 1
  last_is ~/git-course/site "Вернуть порт 8082" || return 1
  no_branch ~/git-course/site schedule-page || return 1
  # --- Подготовка: после первой записи при ошибке скрипт останавливается ---
  (
    set -e
cd ~/git-course/site
git switch -q -c schedule-page
cat > schedule.html <<'EOF'
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>Расписание</title></head>
<body>
<h1>Расписание</h1>
<ul>
<li>Вторник, 19:00 — Git: основы</li>
<li>Четверг, 19:00 — практика</li>
</ul>
</body>
</html>
EOF
git add schedule.html
git commit -q -m "Добавить страницу расписания"
git switch -q main
sed -i '2a Страницы: index.html, about.html.\n' README.md
git add README.md
git commit -q -m "Перечислить страницы в README"
git switch -q schedule-page
cd ~/git-course/site
sed -i 's|<p><a href="about.html">О нас</a></p>|&\n<p><a href="schedule.html">Расписание</a></p>|' index.html
git status --short
  )
  if [ $? -ne 0 ]; then
    echo "Ошибка после начала подготовки: часть шагов могла выполниться."
    echo "Посмотрите git status и git log --oneline --all -5. Начать заново: переименуйте ~/git-course"
    echo "(mv ~/git-course ~/git-course-old) и выполните блоки этого файла с начала."
    return 1
  fi
  echo "Готово: вы на ветке schedule-page, index.html изменён."
}
rp_practice4 </dev/null
SETUP
Разбор и проверка понимания

Лист ученика: 04-branches.md. Хеши — из проверочного запуска.

Практика

Папка ~/git-course/site, ветка schedule-page.

printf '\nРасписание занятий — на странице schedule.html.\n' >> README.md
git switch main
error: Your local changes to the following files would be overwritten by checkout:
	README.md
Please commit your changes or stash them before you switch branches.
Aborting

README в main и schedule-page различается. Переключение заменило бы файл версией из main, и ваша правка потерялась бы — поэтому Git отказался и ничего не изменил. Задача закончена — коммитим:

git add index.html README.md
git commit -m "Добавить ссылки на страницу расписания"
git switch main
git status
grep -c schedule index.html
Switched to branch 'main'
On branch main
nothing to commit, working tree clean
0
git log --oneline --graph --all -5
* 64e50a3 Перечислить страницы в README
| * fc46230 Добавить ссылки на страницу расписания
| * 85bf960 Добавить страницу расписания
|/
* 04549f4 Вернуть порт 8082
* 3c45247 Добавить контакты на страницу «О нас»

Воспроизведено: Git 2.43.0, Ubuntu 24.04.

Проверка понимания

  1. В рабочей папке. Неотслеживаемый файл Git не хранит ни в одной ветке; на main он виден как ?? имя-файла. Если закоммитить его там, он попадёт в main.
  2. Git отказывается, только когда переключение затёрло бы вашу правку. index.html в обеих ветках одинаковый — заменять нечего, правка просто остаётся в папке. README разный — заменить файл значило бы потерять правку.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.