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

Слияние и конфликт

Ситуация

Страница расписания готова в своей ветке. Пора добавить её на рабочую версию сайта. А Борис тем временем в своей ветке поменял заголовок сайта — ту же строку, которую вы поменяли в main. Какой заголовок должен остаться?

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

Слияние (merge) объединяет историю двух веток. Если ветки меняли разные места, Git соединит изменения сам. Если одно и то же место изменено по-разному, Git не угадывает: он останавливается, помечает спорный фрагмент и ждёт решения человека. Разрешить конфликт — значит написать итоговый текст, который учитывает смысл обеих правок, а не выбрать «чью-то сторону» наугад.

Новые слова

  • Слияние — git merge ветка: влить в текущую ветку коммиты другой ветки.
  • Коммит слияния — коммит с двумя родителями; он связывает две линии истории.
  • Конфликт — место, которое обе ветки изменили по-разному; Git ставит в файле маркеры <<<<<<<, =======, >>>>>>>.

Разбор 1: обычное слияние

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

git switch main
git merge --no-edit schedule-page
Auto-merging README.md
Merge made by the 'ort' strategy.
 README.md     |  2 ++
 index.html    |  1 +
 schedule.html | 11 +++++++++++
 3 files changed, 14 insertions(+)
 create mode 100644 schedule.html

--no-edit — принять стандартное сообщение коммита слияния, не открывая редактор. README меняли обе ветки, но в разных местах (строка после заголовка и конец файла) — Git соединил их сам.

git log --oneline --graph -6
*   d4430b1 Merge branch 'schedule-page'
|\
| * 0781a6d Добавить ссылки на страницу расписания
| * f50cc84 Добавить страницу расписания
* | 9c0c978 Перечислить страницы в README
|/
* c129acc Вернуть порт 8082
* 5ecf0c5 Добавить контакты на страницу «О нас»

Две линии сошлись в коммите слияния. Ветка больше не нужна — её коммиты теперь в main:

git branch -d schedule-page
Deleted branch schedule-page (was 0781a6d).

-d удаляет только имя ветки и только если её коммиты уже слиты; сами коммиты остаются в истории.

Разбор 2: конфликт

Ветку Бориса добавляет скрипт setup.md: в ветке title-update он поменял заголовок на «Учебный сайт: Git для начинающих». Запустите его.

Вы тем временем меняете заголовок в main по-своему:

sed -i 's/^TITLE=.*/TITLE=Учебный сайт курса Git — осень 2026/' site.conf
git add site.conf
git commit -m "Указать сезон в заголовке"

Предскажите, что ответит слияние.

git merge --no-edit title-update
Auto-merging site.conf
CONFLICT (content): Merge conflict in site.conf
Automatic merge failed; fix conflicts and then commit the result.

Слияние остановилось на середине. Что в файле:

cat site.conf
# Настройки учебного сайта
PORT=8082
NAME=site
<<<<<<< HEAD
TITLE=Учебный сайт курса Git — осень 2026
=======
TITLE=Учебный сайт: Git для начинающих
>>>>>>> title-update
  • между <<<<<<< HEAD и ======= — версия текущей ветки (main, ваша);
  • между ======= и >>>>>>> title-update — версия вливаемой ветки (Бориса);
  • строки вне маркеров Git уже соединил.

Путь назад всегда есть

Пока конфликт не разрешён и не закоммичен, слияние можно отменить целиком:

git merge --abort
git status
On branch main
nothing to commit, working tree clean

Всё как до git merge. Повторим слияние и разрешим конфликт:

git merge --no-edit title-update

Разрешаем по смыслу

Обе правки осмысленные: Борис хочет подчеркнуть «для начинающих», вы — указать сезон. Итоговый текст учитывает обе. Откройте site.conf в редакторе (nano site.conf), удалите маркеры и напишите итоговую строку. Или перезапишите файл целиком:

cat > site.conf <<'EOF'
# Настройки учебного сайта
PORT=8082
NAME=site
TITLE=Учебный сайт курса Git для начинающих — осень 2026
EOF

Проверка перед коммитом — не осталось ли маркеров:

git diff --check

Пустой вывод — маркеров нет. Если бы остались, Git назвал бы строку: site.conf:4: leftover conflict marker.

Отмечаем конфликт разрешённым и завершаем слияние:

git add site.conf
git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   site.conf
git commit -m "Слить title-update: объединить заголовки"
git log --oneline --graph -5
*   483a552 Слить title-update: объединить заголовки
|\
| * cb66702 Подчеркнуть в заголовке, что курс для начинающих
* | 7acf180 Указать сезон в заголовке
|/
*   d4430b1 Merge branch 'schedule-page'

Сообщение говорит, как разрешён конфликт, — через месяц это поможет понять, откуда взялся такой заголовок.

git branch -d title-update

Практика: конфликт на странице «О нас»

Исходное состояние создаёт скрипт setup.md: Борис в ветке contacts поменял строку про вопросы на странице «О нас», а в main добавлена почта в ту же строку.

  1. Прочитайте, что и зачем сделал Борис: git log -1 contacts (сообщение из двух абзацев).
  2. Слейте contacts в main и разрешите конфликт так, чтобы на странице осталась актуальная информация из обеих правок.
  3. Завершите слияние коммитом, сообщение которого объясняет решение.

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

  • grep -c '<<<<<<<' about.html — 0, git diff --check перед коммитом — пусто;
  • git log --oneline --graph -4 — коммит слияния с двумя родителями;
  • в about.html нет ссылки на отключённую форму, но есть чат и почта.
Подсказка 1

Во втором абзаце сообщения Бориса сказано, что случилось с формой. Это решает, какую часть строки из main оставлять нельзя.

Подсказка 2

Итоговый текст можно написать двумя строками <p>…</p> вместо одной — конфликт не обязывает выбрать одну из версий.

Подсказка 3

git merge --no-edit contacts → правка about.html в nano → git diff --check → git add about.html → git commit -m "Слить contacts: …".

Решение — в answers/05-merge-conflict.md.

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

  • git add прошёл, а в файле остались маркеры. Git не проверяет содержимое при git add. Перед коммитом всегда git diff --check или grep '<<<<<<<'. Если уже закоммитили — исправьте файл новым коммитом.
  • Открылся редактор с текстом Merge branch …. Вы выполнили git merge или git commit без --no-edit или -m. Сохраните и закройте редактор (в nano — Ctrl+O, Enter, Ctrl+X): коммит создастся с этим сообщением.
  • Запутались посреди конфликта. git merge --abort вернёт всё к состоянию до слияния — ни ваша работа, ни коммиты Бориса не пропадут.

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

  1. Почему Git соединил правки README сам, а правки TITLE — нет?
  2. Вы разрешили конфликт, оставив только свою версию строки и не прочитав сообщение коллеги. Что может пойти не так?

Ответы — в answers/05-merge-conflict.md.

Дальше: 6. Совместная работа.

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

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

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

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

bash <<'SETUP'
rp_before5() {
  # --- Проверки: ничего не меняют ---
  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
printf '\nРасписание занятий — на странице schedule.html.\n' >> README.md
git add index.html README.md
git commit -q -m "Добавить ссылки на страницу расписания"
git switch -q main
  )
  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 --graph --all -5
  echo "Готово: конец главы 4."
}
rp_before5 </dev/null
SETUP

Ветка Бориса для главы 5

Нужна в разборе 2 главы 5, после слияния schedule-page.

bash <<'SETUP'
rp_boris5() {
  # --- Проверки: ничего не меняют ---
  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 "Merge branch 'schedule-page'" || return 1
  no_branch ~/git-course/site title-update || return 1
  # --- Подготовка: после первой записи при ошибке скрипт останавливается ---
  (
    set -e
cd ~/git-course/site
git switch -q -c title-update
sed -i 's/^TITLE=.*/TITLE=Учебный сайт: Git для начинающих/' site.conf
git -c user.name="Борис (учебный)" -c user.email="boris@example.invalid" commit -q -am "Подчеркнуть в заголовке, что курс для начинающих"
git switch -q main
  )
  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 title-update
  echo "Готово: ветка title-update с коммитом Бориса."
}
rp_boris5 </dev/null
SETUP

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

Конец разбора 2 главы 5: коммит «Слить title-update: объединить заголовки». Блок создаёт ветку Бориса contacts и коммит с почтой в main.

bash <<'SETUP'
rp_practice5() {
  # --- Проверки: ничего не меняют ---
  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 "Слить title-update: объединить заголовки" || return 1
  no_branch ~/git-course/site contacts || return 1
  # --- Подготовка: после первой записи при ошибке скрипт останавливается ---
  (
    set -e
cd ~/git-course/site
git switch -q -c contacts
sed -i 's|<p>Вопросы: через форму на странице курса.</p>|<p>Вопросы: в чате курса, ответ в течение дня.</p>|' about.html
git -c user.name="Борис (учебный)" -c user.email="boris@example.invalid" commit -q -am "Перенести вопросы в чат курса" -m "Форму на странице курса отключили, вопросы теперь принимаются в чате."
git switch -q main
sed -i 's|<p>Вопросы: через форму на странице курса.</p>|<p>Вопросы: через форму на странице курса или по почте course@example.invalid.</p>|' about.html
git commit -q -am "Добавить почту для вопросов"
  )
  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 --graph --all -3
  echo "Готово: ветка contacts и коммит с почтой в main."
}
rp_practice5 </dev/null
SETUP

Если папки ~/git-course/site нет, сразу к практике главы 5 приведёт один блок:

bash <<'SETUP'
rp_practice5_fresh() {
  # --- Проверки: ничего не меняют ---
  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; }
  absent ~/git-course/site || return 1
  # --- Подготовка: после первой записи при ошибке скрипт останавливается ---
  (
    set -e
mkdir -p ~/git-course/site
cd ~/git-course/site
git init -q -b main
git config user.name "Анна (учебный)"
git config user.email "anna@example.invalid"
cat > index.html <<'EOF'
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>Учебный сайт</title></head>
<body>
<h1>Учебный сайт</h1>
<p>Здесь будет расписание занятий.</p>
</body>
</html>
EOF
cat > site.conf <<'EOF'
# Настройки учебного сайта
PORT=8081
NAME=site
TITLE=Учебный сайт
EOF
cat > README.md <<'EOF'
# Учебный сайт

Запуск для проверки: python3 -m http.server 8081
EOF
git add index.html site.conf README.md
git commit -q -m "Первая версия учебного сайта"
cat > about.html <<'EOF'
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>О нас</title></head>
<body>
<h1>О нас</h1>
<p>Учебный сайт курса «Git: основы».</p>
</body>
</html>
EOF
git add about.html
git commit -q -m "Добавить страницу «О нас»"
cd ~/git-course/site
sed -i 's/^PORT=8081/PORT=8082/' site.conf
git add site.conf
git commit -q -m "Перенести сайт на порт 8082" -m "Порт 8081 на учебном сервере занят сервисом greeter."
sed -i 's|<p>Здесь будет расписание занятий.</p>|<p>Занятия: вторник и четверг, 19:00.</p>|' index.html
git add index.html
git commit -q -m "Добавить расписание занятий на главную"
sed -i 's/^TITLE=.*/TITLE=Учебный сайт курса Git/' site.conf
git add site.conf
git commit -q -m "Уточнить заголовок сайта"
cd ~/git-course/site
B=(-c user.name="Борис (учебный)" -c user.email="boris@example.invalid")
sed -i 's|<p>Занятия: вторник и четверг, 19:00.</p>|&\n<p><a href="about.html">О нас</a></p>|' index.html
git "${B[@]}" commit -q -am "Добавить ссылку на страницу «О нас»"
cat > README.md <<'EOF'
# Учебный сайт

Запуск для проверки из папки сайта:

    python3 -m http.server 8082

Порт указан в site.conf.
EOF
sed -i 's/^PORT=8082/PORT=80x2/' site.conf
git "${B[@]}" commit -q -am "Обновить описание запуска в README"
sed -i 's|<p>Учебный сайт курса «Git: основы».</p>|&\n<p>Вопросы: через форму на странице курса.</p>|' about.html
git commit -q -am "Добавить контакты на страницу «О нас»"
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»."
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
printf '\nРасписание занятий — на странице schedule.html.\n' >> README.md
git add index.html README.md
git commit -q -m "Добавить ссылки на страницу расписания"
git switch -q main
cd ~/git-course/site
git merge -q --no-edit schedule-page
git branch -q -d schedule-page
cd ~/git-course/site
git switch -q -c title-update
sed -i 's/^TITLE=.*/TITLE=Учебный сайт: Git для начинающих/' site.conf
git -c user.name="Борис (учебный)" -c user.email="boris@example.invalid" commit -q -am "Подчеркнуть в заголовке, что курс для начинающих"
git switch -q main
cd ~/git-course/site
sed -i 's/^TITLE=.*/TITLE=Учебный сайт курса Git — осень 2026/' site.conf
git commit -q -am "Указать сезон в заголовке"
if git merge -q --no-edit title-update >/dev/null 2>&1; then
  echo "Ожидался конфликт в site.conf, а слияние прошло без него." >&2; exit 1
fi
if [ "$(git diff --name-only --diff-filter=U)" != "site.conf" ]; then
  echo "Слияние остановилось не из-за ожидаемого конфликта в site.conf." >&2; exit 1
fi
cat > site.conf <<'EOF'
# Настройки учебного сайта
PORT=8082
NAME=site
TITLE=Учебный сайт курса Git для начинающих — осень 2026
EOF
git add site.conf
git commit -q -m "Слить title-update: объединить заголовки"
git branch -q -d title-update
cd ~/git-course/site
git switch -q -c contacts
sed -i 's|<p>Вопросы: через форму на странице курса.</p>|<p>Вопросы: в чате курса, ответ в течение дня.</p>|' about.html
git -c user.name="Борис (учебный)" -c user.email="boris@example.invalid" commit -q -am "Перенести вопросы в чат курса" -m "Форму на странице курса отключили, вопросы теперь принимаются в чате."
git switch -q main
sed -i 's|<p>Вопросы: через форму на странице курса.</p>|<p>Вопросы: через форму на странице курса или по почте course@example.invalid.</p>|' about.html
git commit -q -am "Добавить почту для вопросов"
  )
  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 --graph --all -3
  echo "Готово: состояние практики главы 5."
}
rp_practice5_fresh </dev/null
SETUP
Разбор и проверка понимания

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

Практика

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

git log -1 contacts
commit 0e4a37c…
Author: Борис (учебный) <boris@example.invalid>
…
    Перенести вопросы в чат курса

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

Главный факт: формы больше нет. Значит, из строки в main («через форму … или по почте») форму оставлять нельзя, а почту — можно: её никто не отменял.

git merge --no-edit contacts
Auto-merging about.html
CONFLICT (content): Merge conflict in about.html
Automatic merge failed; fix conflicts and then commit the result.

В nano about.html заменить блок от <<<<<<< HEAD до >>>>>>> contacts двумя строками:

<p>Вопросы: в чате курса, ответ в течение дня.</p>
<p>Почта для вопросов: course@example.invalid.</p>
git diff --check
git add about.html
git commit -m "Слить contacts: вопросы в чате, почта отдельной строкой"
git log --oneline --graph -4
*   6a2dea6 Слить contacts: вопросы в чате, почта отдельной строкой
|\
| * 0e4a37c Перенести вопросы в чат курса
* | f050e3e Добавить почту для вопросов
|/
*   dfdb0c3 Слить title-update: объединить заголовки

git branch -d contacts — ветка слита, имя можно удалить.

Допустимые варианты: одна строка «Вопросы: в чате курса или по почте course@example.invalid» — тоже верно. Неверно: оставить «через форму» (её отключили), молча выбросить почту, оставить маркеры.

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

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

  1. Git сравнивает изменения построчно. В README ветки меняли разные места, разделённые неизменёнными строками, — правки не пересекаются. TITLE обе ветки поменяли в одной и той же строке по-разному; какая версия верна, решает только человек.
  2. Можно молча отменить чужое решение: например, вернуть ссылку на отключённую форму. Git не заметит ошибку — конфликт формально разрешён. Перед разрешением читайте, что и зачем меняла другая сторона (git log -1 ветка).

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