Слияние и конфликт
Ситуация
Страница расписания готова в своей ветке. Пора добавить её на рабочую версию сайта. А Борис тем временем
в своей ветке поменял заголовок сайта — ту же строку, которую вы поменяли в 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 добавлена почта в ту же строку.
- Прочитайте, что и зачем сделал Борис:
git log -1 contacts(сообщение из двух абзацев). - Слейте
contactsвmainи разрешите конфликт так, чтобы на странице осталась актуальная информация из обеих правок. - Завершите слияние коммитом, сообщение которого объясняет решение.
Признаки успеха:
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вернёт всё к состоянию до слияния — ни ваша работа, ни коммиты Бориса не пропадут.
Проверка понимания
- Почему Git соединил правки README сам, а правки
TITLE— нет? - Вы разрешили конфликт, оставив только свою версию строки и не прочитав сообщение коллеги. Что может пойти не так?
Ответы — в 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.
Проверка понимания
- Git сравнивает изменения построчно. В README ветки меняли разные места, разделённые неизменёнными строками, —
правки не пересекаются.
TITLEобе ветки поменяли в одной и той же строке по-разному; какая версия верна, решает только человек. - Можно молча отменить чужое решение: например, вернуть ссылку на отключённую форму. Git не заметит ошибку —
конфликт формально разрешён. Перед разрешением читайте, что и зачем меняла другая сторона (
git log -1 ветка).
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.