Когда над проектом работает больше одного человека, возникает вопрос: что будет, если двое одновременно изменят один файл? Кто кого затрёт?
Ответ устроен так, что молча перезаписать чужую работу в Git почти невозможно. Вокруг этого факта выстроена вся механика совместной разработки — от коммита до проектов с тысячами открытых запросов на слияние.
- Что такое коммит на самом деле
- Что происходит, когда двое трогают один файл
- Первый, кто отправляет, проходит
- Второй получает отказ
- Второй забирает чужие изменения
- Второй отправляет результат — теперь проходит
- Pull Request — запрос на слияние
- Как настроить автоматические проверки через GitHub Actions
- Чем форк отличается от ветки
- Что происходит, когда у проекта тысячи запросов на слияние
- Что повышает шансы PR на слияние
- Как сам проект справляется с потоком
- Очередь слияния — защита main при масштабе
- Как ориентироваться на практике
- Заключение
Что такое коммит на самом деле
Главное заблуждение, мешающее понять остальное: коммит — это не «правка файла».
Коммит — это снимок всего проекта на определённый момент плюс указатель на родительский коммит. Из указателей складывается цепочка истории. У каждого коммита есть хеш (SHA), автор, время и сообщение.
Второй момент: у каждого разработчика собственная полная копия репозитория со всей историей (клон). Коммиты делаются локально, на своей машине, и только командой push отправляются на общий сервер. До момента отправки участники друг другу не мешают.
Снимок, а не дифф — ключевая модель Git. Разница между версиями вычисляется на лету, но хранится именно состояние дерева целиком.
Что происходит, когда двое трогают один файл
Основной сценарий: два разработчика стартуют от одного коммита и правят один файл.
┌──► A (Разработчик A) ──┐
│ │
C1 ──► C2 ──────────┤ ├──► M (слияние)
общая история │ │
└──► B (Разработчик B) ──┘ Оба сделали клон от коммита C2. Разработчик A сделал коммит A, разработчик B — коммит B.
Первый, кто отправляет, проходит
Допустим, A отправляет первым. На сервере было C2, стало C2 → A. Чисто, без проблем.
Второй получает отказ
Когда B пытается отправить свой коммит, сервер видит, что история разошлась: на сервере уже есть A, которого у B нет. Git отклоняет отправку с ошибкой вида non-fast-forward / rejected.
Это и есть защита — Git не даёт перезаписать коммит A.
Второй забирает чужие изменения
B выполняет git pull — это fetch (скачать A) плюс merge (слить с локальной работой). Здесь два исхода, в зависимости от того, какие строки трогали оба:
- Правки в разных местах файла — Git сливает автоматически, построчно. Создаётся коммит слияния
Mс обеими правками. Конфликта нет. - Правки в одних и тех же строках — конфликт. Git не знает, чью версию оставить, и помечает спорные участки прямо в файле:
<<<<<<< HEAD
вариант разработчика B
=======
вариант разработчика A
>>>>>>> a1b2c3d Разработчик вручную решает, что оставить — свой кусок, чужой или собрать новый, — убирает маркеры, делает git add и git commit. Получается коммит слияния M.
Второй отправляет результат — теперь проходит
После слияния история на стороне B содержит A, B и M. Отправка принимается, на сервере сохранены оба изменения.
Единственный штатный способ реально перезаписать чужой коммит — принудительная отправка:
git push --force или безопаснее:
git push --force-with-lease --force грубо перезаписывает историю на сервере и может уничтожить чужие коммиты, поэтому на общих ветках его блокируют настройками защиты. --force-with-lease мягче: откажется отправлять, если на сервере появилось что-то новое, чего ты не видел.
Pull Request — запрос на слияние
В нормальном процессе никто не отправляет коммиты прямо в основную ветку (main). Вместо этого используют Pull Request (PR) — запрос на слияние одной ветки в другую.
PR — это не команда Git, а функция GitHub, надстройка поверх Git. Механика: создаётся ветка, в неё коммитятся изменения, ветка отправляется на GitHub, открывается PR с предложением влить её в main. GitHub формирует страницу с полным списком коммитов, построчной разницей по файлам, статусом конфликтов с актуальным main и результатами автоматических проверок.
PR добавляет поверх обычного слияния три вещи:
- Проверка перед вливанием. Ревьюер смотрит изменения, оставляет комментарии к конкретным строкам, запрашивает правки. Пока не одобрено — не вливается.
- Автоматические проверки. К PR привязывается запуск тестов, линтера, сборки контейнера. Если что-то падает — слияние блокируется.
- Обсуждение и история. Вся переписка по изменению хранится рядом с кодом. Спустя время видно не только что поменяли, но и почему.
Когда PR одобрен и проверки зелёные, нажимается «Merge», и GitHub сам выполняет слияние (тот самый коммит M). Доступны три стратегии:
- Merge commit — создаётся отдельный коммит слияния, история сохраняет факт ветвления.
- Squash and merge — все коммиты ветки схлопываются в один. Удобно, когда в ветке было полтора десятка мелких коммитов «фикс», а в
mainнужен один аккуратный. - Rebase and merge — коммиты переносятся в
mainпо одному, без коммита слияния. История остаётся линейной.
Как настроить автоматические проверки через GitHub Actions
Привязать проверки к PR можно через GitHub Actions — встроенную систему автоматизации. Логика: в репозиторий кладётся файл с описанием того, что запускать и когда, а GitHub при каждом PR поднимает чистую виртуальную машину, прогоняет шаги и показывает результат прямо в PR — зелёная галочка или красный крест.
Файлы лежат по пути .github/workflows/. Каждый файл — это один рабочий процесс. Внутри: триггер (on:) — когда запускать; задачи (jobs:) — что делать, каждая на своей машине и параллельно; шаги (steps:) — конкретные команды.
Полная конфигурация под проект на Python с Docker — три параллельные задачи: линтер, тесты, сборка образа. Файл .github/workflows/ci.yml:
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Установка ruff
run: pip install ruff
- name: Проверка линтером
run: ruff check .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Установка зависимостей
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
- name: Запуск тестов
run: pytest -v
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Сборка образа
run: docker build -t myapp:ci . Разбор по строкам:
on:— процесс запускается при открытии или обновлении PR вmain, а также при прямой отправке вmain, чтобы основная ветка тоже всегда проверялась.runs-on: ubuntu-latest— GitHub поднимает чистую Ubuntu под каждую задачу. Сборка контейнера работает сразу, Docker там уже установлен.actions/checkout@v4— скачивает код на машину. Без него машина пустая.actions/setup-python@v5сcache: pip— ставит Python и кеширует скачанные пакеты между запусками, ускоряя повторные прогоны.
Три задачи идут параллельно и независимо. Если падает любая — PR помечается красным.
Сам значок ничего не запрещает, он лишь показывает результат. Чтобы GitHub не давал влить сломанный код, включается защита ветки: в репозитории Settings → Branches → Add branch ruleset, имя ветки main, опция Require status checks to pass before merging и выбор нужных проверок (появятся в списке после первого запуска процесса). После этого кнопка «Merge» блокируется, пока все проверки не станут зелёными.
Если тестов в проекте пока нет, pytest завершится с кодом 5 («нет тестов») и задача упадёт. На это время либо убери задачу test, либо замени команду на pytest -v || [ $? -eq 5 ].
Сборка образа здесь только проверяет, что Dockerfile собирается; образ никуда не публикуется — это отдельная задача с docker push в реестр.
Чем форк отличается от ветки
Форк (fork) — это личная копия чужого репозитория на GitHub, целиком, под своим аккаунтом. Если ветка живёт внутри одного репозитория, то форк — отдельный репозиторий, отвязанный от оригинала, но помнящий, откуда он родом.
Разница принципиальная. Ветка существует внутри одного репозитория, и чтобы отправить в неё коммит, нужны права на запись. Так работают внутри своей команды, где у всех есть доступ. Форк нужен, когда прав на запись в оригинал нет — например, при участии в чужом открытом проекте.
Сценарий «fork and pull» — основная модель вклада в открытый код:
- Нажимаешь «Fork» — появляется полная копия репозитория под своим аккаунтом.
- Клонируешь свой форк, заводишь ветку, вносишь правку.
- Отправляешь в свой форк (туда права есть).
- Открываешь Pull Request из своего форка в оригинальный репозиторий.
Дальше — та же механика PR: автор оригинала смотрит изменения, обсуждает и, если одобряет, вливает их к себе.
Форк помнит свой источник — его называют upstream. Оригинал продолжает развиваться, и копия отстаёт. Чтобы подтянуть свежие изменения:
git remote add upstream https://github.com/owner/repo.git
git fetch upstream
git merge upstream/main Здесь origin — твой форк (куда отправляешь), upstream — оригинал (откуда тянешь обновления). В интерфейсе форка есть кнопка «Sync fork», которая делает то же самое.
Правило выбора: есть права на запись (свой проект, репозиторий команды) — работаешь веткой и PR внутри репозитория, форк только запутает. Прав нет (чужой открытый код) — форк и PR из форка.
Важный нюанс: форк на GitHub — это сетевая копия именно в рамках GitHub. Если репозиторий просто склонирован на свой сервер через git clone, это локальная копия, а не форк — у неё нет связи «предложить изменения обратно» через интерфейс GitHub. Форк создаётся именно кнопкой на сайте.
Что происходит, когда у проекта тысячи запросов на слияние
В крупных проектах число открытых PR доходит до тысяч. Здесь важно понять: 5000 открытых PR — это почти никогда не честная очередь, которую кто-то разгребает по порядку. Это смесь из нескольких разных вещей:
- залежавшиеся — открыли год назад, автор пропал, никто не закрыл;
- дубликаты — несколько человек независимо чинят один баг;
- ботовые — автообновления зависимостей плодят PR пачками;
- отклонённые де-факто — висят открытыми, хотя сопровождающий уже отказал;
- и лишь небольшая доля живых, которые реально движутся к слиянию.
То есть число — это не глубина очереди. Реально активных в каждый момент десятки. Работать нужно не пробиваясь сквозь толпу, а попадая в правильный поток.
Что повышает шансы PR на слияние
- Сначала читается
CONTRIBUTING.md. У проектов с большой очередью жёсткий процесс. Нарушил формат — закроют не глядя. - PR привязывается к задаче. Идеально — к задаче с меткой
help wantedилиgood first issue. PR «по своей инициативе» без обсуждения часто игнорируют: его никто не просил. - Маленький, сфокусированный PR. Одно изменение на один PR. Огромные PR на десятки файлов не ревьюят никогда — слишком дорого проверять, они и составляют костяк протухших тысяч.
- Зелёные проверки и подписанное соглашение участника (CLA, часто требуют в корпоративных проектах). Иначе PR не дойдёт до человека.
- Быстрый отклик на ревью. PR с запрошенными правками, на которые автор молчит неделю, уходит в протухшие. Скорость ответа — главный фактор, доедет ли он до слияния.
Как сам проект справляется с потоком
Без автоматизации тысячи PR неуправляемы:
- Метки и автотриаж. Боты при открытии PR навешивают метки (область кода, размер, нужен ли CLA) и раскидывают по сопровождающим.
CODEOWNERS. Файл в репозитории: PR, трогающий каталогparsers/, автоматически назначается ответственному. Ревьюера не выбирают вручную.- Бот протухания (stale bot). Автоматически помечает и через заданное число дней закрывает PR без активности. Так очередь не растёт до бесконечности — мёртвое отсекается само.
- Обязательные проверки. Те же GitHub Actions, только на уровне правил репозитория: без зелёных проверок кнопка слияния заблокирована для всех.
Очередь слияния — защита main при масштабе
Это ключевая для масштаба вещь, и она напрямую связана с гонкой «кто первый отправил».
При полусотне одобренных PR проблема обостряется. Каждый PR тестировался против старого main. Как только влили первый — остальные формально устарели: их проверки были зелёными на коде, которого в main уже нет. Технически любой из них теперь может сломать сборку после слияния.
Решение — очередь слияния (merge queue), встроенная в GitHub. Одобренные PR не вливаются сразу, а становятся в очередь. Система берёт их по одному, временно ставит каждый поверх актуального main плюс всех предыдущих в очереди, заново прогоняет проверки на этой комбинации и вливает только если зелено.
Merge queue гарантирует, что main никогда не ломается, даже когда десятки PR готовы одновременно. Человек жмёт «в очередь», дальше всё автоматически.
Как ориентироваться на практике
Никто не листает тысячи страниц — используют поиск GitHub с фильтрами в строке. Рабочие запросы:
is:pr is:open author:USERNAME — только мои PR
is:pr is:open label:"good first issue" — куда можно зайти новичку
is:pr is:open review:required sort:updated-desc — что реально движется сейчас
is:pr is:open draft:false -is:stale — живое, не черновики Просматривается не весь список, а узкий срез: свои PR, конкретная область кода, недавно обновлённые. Само число открытых PR на работу почти не влияет — релевантных всегда десяток.
Заключение
Вся механика совместной работы в GitHub растёт из одного принципа: молча перезаписать чужой коммит нельзя. Отсюда следует отказ при отправке устаревшей ветки, необходимость подтягивать чужие изменения, разрешение конфликтов, Pull Request как точка проверки, GitHub Actions как страховка от сломанного кода, форки для участия без прав на запись и очередь слияния для масштаба — единая выстроенная логика от двух разработчиков с одним файлом до проектов с тысячами участников.









