$ sudo teach IT
Модуль 6 · Работа в команде · Урок 6.5

Практикум: команда из двух

Вы включите защиту для main, откроете два pull request от двух участников и решите конфликт между ними.

Практикум~30 минутНовичоккомандный циклконфликт в PR

Задание

Конфликт в pull request — это ситуация, когда ветка PR и main изменили одни и те же строки, и GitHub не может слить их сам. Решают его так же, как конфликт слияния из модуля про ветки: объединяют изменения вручную и делают новый коммит. В практикуме вы пройдёте командный цикл вдвоём — вы и «коллега», роль которого играет второй клон portfolio-colleague, — закроете main от прямых правок и доведёте оба PR до слияния.

Каждый приём по отдельности знаком: ветка и pull request из первого урока модуля, ревью, issues, форки. Теперь соберём их вместе и добавим то, что в реальной команде случается постоянно: две правки одной строки.

Схема
portfolio (вы)                    portfolio-colleague (коллега)
  feature/footer-note               feature/colleague-footer
        |                                   |
        +--- PR #5 ---> main <--- PR #6 ----+

обе ветки стартуют от одного и того же main
и обе меняют одну и ту же строку подвала

На схеме: две ветки отходят от одного main и вливаются в него отдельными PR. Первый PR уйдёт без проблем, а второй упрётся в конфликт.

Что нужно сделать, по шагам:

  1. Включить для main правило: изменения только через pull request.
  2. Создать две ветки: одну в portfolio, вторую в portfolio-colleague. Обе меняют подпись в подвале, которую вы дописали, когда публиковали сайт.
  3. Слить первый PR, увидеть конфликт во втором и решить его.
  4. Слить второй PR и обновить оба клона.

Шаг 1. Правило для main

Чтобы никто (и вы тоже) не пушил в main напрямую, включают правило. На GitHub оно называется ruleset (набор правил для веток). По документации GitHub, у бесплатного аккаунта публичные репозитории получают полный набор возможностей, поэтому для вашего публичного portfolio оно доступно. Если у вас приватный репозиторий на бесплатном тарифе, расширенных настроек может не быть.

  1. Откройте репозиторий, вкладку Settings.
  2. В левой панели в разделе Code, planning, and automation выберите Rulesets (если пункт раскрывается, внутри нажмите Rulesets ещё раз).
  3. Нажмите New ruleset, затем New branch ruleset.
  4. В поле Ruleset name введите название, например «Защита main».
  5. Статус Enforcement status по умолчанию Disabled (правило не действует). Нажмите на него и выберите Active.
  6. В разделе Target branches нажмите Add a target и выберите вариант, который включает ветку по умолчанию (в интерфейсе он называется Include default branch).
  7. В разделе Branch protections отметьте Require a pull request before merging. Число обязательных одобрений оставьте равным 0: вы здесь единственный участник, а свой PR одобрить нельзя. Пункты Restrict deletions и Block force pushes отмечены по умолчанию — оставьте их: они запрещают удалить main и перезаписать её принудительной отправкой.
  8. Список Bypass list оставьте пустым и нажмите Create.

Теперь прямой git push в main будет отклонён, а изменения попадают туда только слиянием PR. Для личного проекта это тренировка, для командного — обязательная страховка от случайных правок в рабочей ветке.

Rulesets и классические правила

Старый способ защиты веток называется branch protection rules. GitHub поддерживает оба, но развивает rulesets: они гибче, и их можно временно отключать, не удаляя. Если в чужом проекте вы увидите «branch protection», это тот же замысел.

Шаг 2. Две ветки с одной строкой

Обе ветки должны стартовать от одного и того же состояния main. Поэтому сначала обновите оба клона, и только потом создавайте ветки. Начните с вашего portfolio:

Терминал
$ cd ~/projects/portfolio
$ git switch main
$ git pull
$ git switch -c feature/footer-note
Switched to a new branch 'feature/footer-note'

В index.html сократите подпись в подвале. Было:

HTML · index.html (было)
  <footer><p>&copy; 2026 Алекс Иванов. Сайт опубликован на GitHub Pages.</p></footer>

Стало:

HTML · index.html
  <footer><p>&copy; 2026 Алекс Иванов. Сайт на GitHub Pages.</p></footer>
Терминал
$ git add index.html
$ git commit -m "Сократить подпись в подвале"
$ git push -u origin feature/footer-note
...
 * [new branch]      feature/footer-note -> feature/footer-note
branch 'feature/footer-note' set up to track 'origin/feature/footer-note'.

Теперь переходите в клон «коллеги». Пока вы ничего не сливали, поэтому git pull там ничего нового не принесёт: main тот же.

Терминал
$ cd ~/projects/portfolio-colleague
$ git switch main
$ git pull
$ git switch -c feature/colleague-footer
Switched to a new branch 'feature/colleague-footer'

«Коллега» меняет ту же самую строку подвала, только по-своему: оставляет подпись полной и дописывает слова про Git:

HTML · index.html
  <footer><p>&copy; 2026 Алекс Иванов. Сайт опубликован на GitHub Pages, сделано с помощью Git.</p></footer>
Терминал
$ git add index.html
$ git commit -m "Добавить в подвал слова про Git"
$ git push -u origin feature/colleague-footer
...
 * [new branch]      feature/colleague-footer -> feature/colleague-footer
branch 'feature/colleague-footer' set up to track 'origin/feature/colleague-footer'

На GitHub откройте для каждой ветки pull request кнопкой Compare & pull request. Первый (feature/footer-note) получит номер #5, второй (feature/colleague-footer) — #6; номера у вас могут отличаться. В описании второго для порядка укажите, что это правка «коллеги».

Шаг 3. Первый PR и конфликт во втором

Слейте PR #5: Merge pull request, Confirm merge, Delete branch. Теперь откройте PR #6. Блок слияния внизу вкладки Conversation изменился: GitHub пишет This branch has conflicts that must be resolved («в ветке есть конфликты, которые нужно решить»), а кнопка Merge pull request недоступна. Рядом появилась кнопка Resolve conflicts.

Причина понятна: main уже получил короткую подпись «Сайт на GitHub Pages.», а ветка «коллеги» меняет ту же строку по-своему. Какую оставить, решает человек.

Решить можно двумя путями.

  • На GitHub. Кнопка Resolve conflicts открывает редактор: вы видите файл с маркерами <<<<<<<, ======= и >>>>>>>, удаляете маркеры и ненужные строки, нажимаете Mark as resolved, затем Commit merge. Редактор справляется только с простыми конфликтами; если кнопка недоступна, остаётся командная строка.
  • У себя на компьютере. Надёжнее и в реальной работе нужно почти всегда. Так мы и сделаем.

Идея та же, что при слиянии веток: влить свежий main в ветку PR, решить конфликт и отправить результат. Решает его тот, кто владеет веткой, то есть «коллега»:

Терминал
$ cd ~/projects/portfolio-colleague
$ git switch main
$ git pull
From https://github.com/alex-ivanov/portfolio
   917622a..139f2b6  main       -> origin/main
Updating 917622a..139f2b6
Fast-forward
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch feature/colleague-footer
Switched to branch 'feature/colleague-footer'
$ git merge main
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Разбор по строкам

  • git pull на main — клон «коллеги» узнал о слиянии PR #5 и перемотал main на коммит 139f2b6.
  • git switch feature/colleague-footer — возвращаемся в ветку PR #6. Решать конфликт нужно в ней, а не в main.
  • git merge main — влить свежий main в ветку (rebase пока не нужен: он в следующем модуле).
  • CONFLICT (content) — одну и ту же строку в index.html изменили обе стороны; Git остановился и ждёт решения.

Откройте index.html в VS Code. Подвал выглядит так:

HTML · index.html
<<<<<<< HEAD
  <footer><p>&copy; 2026 Алекс Иванов. Сайт опубликован на GitHub Pages, сделано с помощью Git.</p></footer>
=======
  <footer><p>&copy; 2026 Алекс Иванов. Сайт на GitHub Pages.</p></footer>
>>>>>>> main

Между <<<<<<< HEAD и ======= — версия вашей ветки («коллеги»), между ======= и >>>>>>> main — версия из main. Объедините смысл — короткую подпись из main и слова про Git от «коллеги» — и оставьте одну строку без маркеров:

HTML · index.html
  <footer><p>&copy; 2026 Алекс Иванов. Сайт на GitHub Pages, сделано с помощью Git.</p></footer>

Дальше всё как в конфликте из модуля про ветки: подготовить файл, закоммитить слияние и отправить ветку.

Терминал
$ git add index.html
$ git commit
[feature/colleague-footer 66e19ee] Merge branch 'main' into feature/colleague-footer
$ git push
To https://github.com/alex-ivanov/portfolio.git
   c1bd89b..66e19ee  feature/colleague-footer -> feature/colleague-footer

Разбор по строкам

  • git add index.html — сообщить Git, что конфликт в файле решён.
  • git commit без -m — откроется редактор с готовым сообщением Merge branch 'main' into feature/colleague-footer; сохраните и закройте окно, менять сообщение не нужно.
  • 66e19ee — коммит слияния, который объединил две ветки истории.
  • git push — отправляет ветку; -u не нужен, связь с origin уже есть. push проходит как обычно: конфликт остался в слиянии, а на GitHub уходит уже решённый результат.

Вернитесь на страницу PR #6: предупреждение исчезло, конфликт решён, а в списке коммитов появился коммит слияния.

Шаг 4. Слияние и порядок в клонах

Слейте PR #6 теми же кнопками: Merge pull request, Confirm merge, Delete branch. Затем приведите в порядок оба клона. В каждом выполните:

Терминал
$ git switch main
$ git pull
$ git branch -d feature/footer-note
$ git fetch --prune

В portfolio-colleague имя удаляемой ветки — feature/colleague-footer. Проверим результат:

Терминал
$ git log --oneline -7
75011d6 Merge pull request #6 from alex-ivanov/feature/colleague-footer
66e19ee Merge branch 'main' into feature/colleague-footer
139f2b6 Merge pull request #5 from alex-ivanov/feature/footer-note
c1bd89b Добавить в подвал слова про Git
08f0777 Сократить подпись в подвале
917622a Merge pull request #4 from alex-ivanov/feature/github-link
28c6c68 Добавить ссылку на GitHub в контакты

Обратите внимание на историю. Две маленькие правки превратились в семь строк, три из которых — коммиты слияния. Через пару недель командной работы история будет вся из таких строк, и найти среди них настоящие изменения непросто. Как привести историю в порядок — задача следующего модуля.

Шпаргалка: день в команде

Шпаргалка
Утро:           git switch main
                git pull
Новая задача:   git switch -c feature/что-делаем
Работа:         правки, git add, git commit -m "..."
Main ушёл:      git switch main, git pull,
                git switch feature/..., git merge main
Отправка:       git push -u origin feature/что-делаем
Pull request:   Compare & pull request, описание, Closes #N
Ревью:          новые коммиты в ту же ветку (после Commit suggestion
                сначала git pull)
После merge:    git switch main, git pull,
                git branch -d feature/..., git fetch --prune

Проверьте себя

  • В настройках репозитория есть активный ruleset для ветки по умолчанию с правилом Require a pull request before merging.
  • Оба PR (#5 и #6) в состоянии Merged, обе ветки удалены на GitHub и локально (git branch показывает только main).
  • Во втором клоне в index.html одна строка подвала без маркеров <<<<<<<: с подписью «Сайт на GitHub Pages, сделано с помощью Git».
  • В git log --oneline видны два коммита слияния Merge pull request и один коммит Merge branch 'main' into feature/colleague-footer.
  • Вы можете объяснить, почему конфликт решают в ветке PR, а не в main.

Частые ошибки

Закоммитили прямо в main, и push отклонён. Когда правило включено, GitHub отвечает отказом. Текст может немного отличаться, но смысл такой:

Вывод
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - Changes must be made through a pull request.
To https://github.com/alex-ivanov/portfolio.git
 ! [remote rejected] main -> main (push declined due to repository rule violations)

Коммит не пропал, он у вас в локальном main. Перенесите его в ветку, а main верните как на GitHub:

Терминал
$ git switch -c feature/readme-update
$ git switch main
$ git reset --hard origin/main
HEAD is now at 75011d6 Merge pull request #6 from alex-ivanov/feature/colleague-footer

reset --hard стирает несохранённое

Команда git reset --hard возвращает файлы к состоянию origin/main и безвозвратно удаляет все правки, не попавшие в коммит. Здесь это безопасно, потому что коммит сохранён в новой ветке feature/readme-update. Перед сбросом выполните git log feature/readme-update и убедитесь, что коммит там. Незакоммиченные правки сначала сохраните.

В файле остались маркеры конфликта. Git не проверяет, что вы убрали <<<<<<<: после git add файл считается решённым. На сайте потом видны эти строки. Перед git add найдите в файле <<<<<<<, ======= и >>>>>>> (в VS Code — поиск по файлу).

Конфликт решали в main. Свежий main вливают в ветку PR, а не наоборот: так правки остаются на проверке в PR, а в main попадает уже готовый результат.

Ветка стартовала от старого main. Вы забыли git pull перед git switch -c, и конфликты появляются там, где их не должно быть. Начинайте день с git switch main и git pull.

Принудительная отправка. Увидев отказ, не добавляйте --force — так можно стереть чужую работу. Правильное лечение — git pull, решить конфликт, git push.

Что важно запомнить

  • Правило (ruleset) для main запрещает прямой push: изменения идут только через pull request.
  • Две ветки, меняющие одни строки, дают конфликт в PR после слияния первой из них.
  • Конфликт решают в ветке PR: git merge main, правка маркеров, git add, git commit, git push.
  • На GitHub есть кнопка Resolve conflicts, но она справляется только с простыми случаями.
  • После слияния в каждом клоне: git switch main, git pull, git branch -d, git fetch --prune.

Вы прошли весь цикл командной работы. Но посмотрите на git log: он засорён коммитами слияния, а в реальных проектах к ним добавляются ещё и «wip», «ещё раз», «опечатка». Как перенести ветку на свежий main без лишних слияний и как склеить мусорные коммиты в аккуратные — в следующем модуле.

На следующий урок