← назад к разделу

Локально Git самодостаточен, но команда синхронизируется через удалённый репозиторий — общую копию проекта на сервере (GitHub, GitLab, Bitbucket). Три команды — push, pull, fetch — весь трафик между вами и сервером. Здесь же живут два главных страха: «push отклонён» и «я затёр чужую работу». Разберём так, чтобы страхов не осталось.

сервер (origin) origin/main — ваша копия ваша ветка main ваш коммит коллега копия устарела git push → ! [rejected]на сервере есть коммит,которого у вас нет git fetchобновилась только копия,файлы не тронуты git pullчужой коммит приезжаетв вашу ветку слияние git push — приняттеперь ваша ветка растётиз серверной вершины

Коллега запушил коммит: на сервере он есть, а в вашей копии origin/main его нет — Git сам в сеть не ходит, и копия устаревает. Поэтому push и отклонён: вы просите сервер заменить его историю вашей, а в вашей этого коммита нет. fetch обновляет только копию и файлы не трогает; pull доводит чужой коммит и до вашей ветки — после этого push проходит.

origin и отслеживаемые ветки

При git clone адрес сервера сохраняется под именем origin — это просто псевдоним URL (серверов может быть и несколько, но обычно один). Локальная ветка отслеживает (tracks) одноимённую серверную: main знает про origin/main, и git status сообщает расхождение: «ваша ветка отстаёт на 2 коммита».

Важная деталь: origin/main — это локальная копия серверного состояния, снятая при последней синхронизации. Между синхронизациями она устаревает — Git не ходит в сеть сам.

Из этого следует ловушка git status: расхождение он считает по этой самой копии. Если коллега запушил час назад, а вы с тех пор не делали fetch, status честно пишет «ваша ветка актуальна», потому что сравнивает с устаревшим origin/main. Поэтому перед выводами о состоянии ветки сначала git fetch, и только потом git status.

git push -u origin fix-cart   # первый push новой ветки: -u связывает её с серверной
git push                      # дальше просто push

Откуда origin берётся, если репозиторий не клонировали, а создали через git init: его добавляют руками, взяв адрес со страницы пустого репозитория на сервере.

git remote -v                                       # какие серверы известны и по каким адресам
git remote add origin git@github.com:team/shop.git  # назвать сервер origin
git push -u origin main                             # отправить первую ветку и связать её

Адрес бывает двух видов, https://… и git@…, и это выбор способа доступа: по HTTPS сервер спросит персональный токен вместо пароля, по SSH пустит по ключу; как их завести, разобрано в первой статье раздела. У сервера может быть и второе имя. Когда вы правите чужой проект, у вас его копия (fork) под своим аккаунтом: origin смотрит на копию, а исходный репозиторий добавляют как upstream и время от времени подтягивают из него: git fetch upstream && git rebase upstream/main. Pull request при этом открывают из ветки вашей копии в исходный репозиторий.

fetch и pull: посмотреть или забратьспросят на собеседовании

  • git fetch — скачать новое с сервера, не трогая ваши файлы: обновляются только копии origin/*. После этого можно спокойно посмотреть, что принесли коллеги: git log main..origin/main.
  • git pull = fetch + слияние отслеживаемой ветки (для main это origin/main) в вашу. Файлы обновляются сразу.

Pull — повседневность, fetch — аккуратность: когда хочется сначала увидеть чужие изменения, а потом решать.

Отдельная привычка — git pull --rebase (или pull.rebase=true в конфиге): ваши локальные коммиты переносятся поверх свежих серверных вместо создания мелких merge-коммитов «Merge branch main», и история заметно чище. Почему это работает — в статье про rebase.

«Push отклонён»: что происходит и что делатьспросят на собеседовании

! [rejected]  main -> main (fetch first)

Это не поломка, а защита: на сервере есть коммиты, которых нет у вас (коллега успел запушить). Git отказывается затирать их вашей версией истории. Лечение штатное:

git pull --rebase   # забрать чужое и переставить свои коммиты поверх
git push            # теперь уходит

Флаг тут не украшение. Начиная с версии 2.34 голый git pull в этой ситуации ничего не сливает, а останавливается: fatal: Need to specify how to reconcile divergent branches. Git отказывается угадывать, чего вы хотите — слияния или переноса, — и ждёт прямого ответа. Отвечать можно каждый раз флагом (--rebase либо --no-rebase) или один раз на все проекты:

git config --global pull.rebase true   # pull всегда переносит ваши коммиты поверх

Конфликт при этом возможен и разбирается как обычно.

git push --force в общую ветку перезаписывает серверную историю вашей: коммиты коллег, которых у вас нет, исчезают с сервера, и остаются только в их локальных копиях, если остаются. Поэтому в общую ветку так не делают, а в main/develop force-push обычно запрещён настройками сервера, и это правильно. Настройка называется защитой ветки (branch protection): владелец репозитория включает её для main, и там же живут остальные правила, на которые опирается процесс с pull request: запрет прямого push, обязательные проверки конвейера, обязательное одобрение одного или двух ревьюеров, требование линейной истории или подписанных коммитов. Легитимен он в одном случае: ваша личная ветка, историю которой вы сознательно переписали перед ревью, и лучше в форме --force-with-lease: она сверяет серверную ветку с вашей копией origin/* и отказывается перетирать чужое, которого вы ещё не видели. У этой сверки есть слабое место, разобранное в статье про rebase.

на сервере a1b2 c3d4 коллега 9f07 у вас локально a1b2 c3d4 ваш 4be1 после --force a1b2 c3d4 ваш 4be1

В нижнем ряду коммита коллеги 9f07 уже нет: force-push заменил серверную ленту вашей, и вернуть чужую работу можно только с его машины.

Ежедневный цикл

Типичный день с удалённым репозиторием:

git switch main && git pull        # утро: свежий main
git switch -c feature-export      # ветка под задачу
# ...работа, коммиты...
git push -u origin feature-export # отправить ветку
# дальше — pull request, ревью и слияние на сервере
git switch main && git pull        # после вливания: обновиться
git branch -d feature-export       # прибрать ветку

Одна неожиданность ждёт на последней строке. Если ветку слили через squash — а это самый ходовой способ — Git не считает её влитой: в main уехал один новый коммит, а ваших в нём формально нет. git branch -d ответит error: the branch 'feature-export' is not fully merged. Это не поломка, а честный вопрос «точно удалять?». Содержимое уже в main, так что отвечать надо заглавной буквой: git branch -D feature-export.

Ветка при этом удалена только у вас: на сервере origin/feature-export жива, пока её не удалили там, и копия origin/feature-export в вашем клоне тоже. Серверную ветку обычно удаляет кнопка при слиянии pull request, руками это git push origin --delete feature-export. А устаревшие копии origin/* веток, которых на сервере уже нет, Git сам не чистит: через месяц git branch -a показывает десятки мёртвых имён. Убирает их git fetch -p (prune), а чтобы не помнить об этом, включают git config --global fetch.prune true.

Единственная привычка, которая делает всё это гладким: начинать от свежего main и пушить ветку рано, не копя неделю локальных коммитов. Ранний push — это ещё и бэкап: сгоревший ноутбук не уносит работу.

Коротко

  • origin — псевдоним сервера; origin/main — локальный снимок серверного состояния, устаревает между синхронизациями.
  • fetch скачивает не трогая файлы, pull = fetch + слияние. pull --rebase держит историю чистой.
  • «Push rejected» = на сервере есть новое: pull --rebase, затем push. Это защита, а не ошибка; голый pull на разошедшихся ветках Git делать откажется.
  • Force-push — только в свою ветку и лучше --force-with-lease; в общие ветки — никогда.
  • Ритм: от свежего main, ветка на задачу, ранний push.
  • git status сравнивает с устаревшей копией origin/main и может писать «актуально», когда на сервере уже есть новое: сначала fetch.
  • Сервер добавляют git remote add origin <адрес> (SSH или HTTPS с токеном); чужой проект правят через fork с origin и upstream; правила main (запрет push, проверки, одобрения) это защита ветки.
  • Серверную ветку удаляет git push origin --delete, мёртвые копии origin/* чистит git fetch -p или fetch.prune true.

Что почитать дальше