linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть IV — Совместная работа

$ глава 12 · 55 минут

Удалённые репозитории: fetch, pull, push, tracking

«Remote» в Git - это просто URL под именем. Никакой особой логики. Когда ты делаешь git fetch origin, Git идёт по URL, привязанному к имени origin, и тянет коммиты. Всё, что про синхронизацию с «облаком» - построено на этом простом примитиве.

Но за этим примитивом стоит несколько тонкостей, которые регулярно кусают команды: чем fetch отличается от pull, что делает --force-with-lease и почему --force - почти всегда плохо, как устроены tracking-ветки и куда они смотрят при работе через fork.

Эта глава - про эти тонкости. Цель - после неё уметь работать с удалёнными так, чтобы коллеги не приходили с фразой «ты только что затёр мой коммит».

12.1 Remote - это имя для URL

После git clone в .git/config появляется секция:

[remote "origin"]
    url = git@github.com:foo/bar.git
    fetch = +refs/heads/*:refs/remotes/origin/*

Это весь «секрет» origin. Имя origin - конвенция clone'а, его можно переименовать. URL - куда ходить за коммитами и куда пушить. Refspec в fetch = определяет, какие удалённые ветки превращаются в локальные origin/<branch>.

Базовые команды управления:

bash
git remote -v                    # список с URL'ами
git remote add upstream <url>    # добавить
git remote rename origin foo     # переименовать
git remote set-url origin <new>  # сменить URL
git remote remove upstream       # удалить

Подробности - в remote-cmd.

Один репо может иметь сколько угодно remote. Типовая работа - один (origin). Работа через fork - два (origin для своего форка, upstream для оригинала, см. upstream-vs-origin). Редко - три и больше: например, при миграции с одного хостинга на другой временно держат оба URL.

12.2 fetch: главное оружие

git fetch - половина того, что делает git pull. Только половина: «скачать», без «слить». В этом и его сила.

bash
git fetch origin

Что делает (см. fetch):

  1. Подключается к remote.
  2. Узнаёт список refs и SHA.
  3. Скачивает все коммиты, которых нет локально, в .git/objects/.
  4. Обновляет refs/remotes/origin/<branch> - это remote-tracking ветки.

Локальные main, feat/x, HEAD -не меняются. Меняются только origin/main, origin/feat/x. Это безопасная команда: её можно вызывать сколько угодно раз, рабочий каталог не затрагивается.

Что с этим делать дальше:

bash
git log main..origin/main --oneline   # коммиты, пришедшие в origin/main
git diff main origin/main             # diff между ними
git switch main && git merge --ff-only origin/main   # догнать main

Между fetch и merge есть пауза, в которую можно подумать. На сложных репозиториях это полезно: ты видишь, что прилетело, прежде чем что-то локально менять.

Полезные флаги:

bash
git fetch --all              # все remote сразу
git fetch --prune            # удалить локальные refs мёртвых веток
git fetch --tags             # включая теги

--prune особенно полезен: в долго живущем клоне ветки в origin/ копятся, даже после того как их удалили на remote. Без --prune через год их там десятки. Поставь раз и навсегда:

bash
git config --global fetch.prune true

Теперь каждый git fetch чистит за собой.

12.3 fetch vs pull: главная развилка

git pull origin main - это два шага в одной команде:

git fetch origin
git merge origin/main         # или git rebase, см. ниже

Удобно и быстро. Минус - между «скачать» и «слить» нет паузы. Если на remote прилетел спорный коммит, ты его уже смерджил, прежде чем понял, что прилетело.

Привычка «всегда git pull» работает 95% времени и кусает в оставшихся 5%, когда нужно было хотя бы посмотреть, что пришло. Поэтому многие опытные разработчики используют git fetch && <смотрю> && <решаю что делать> как дефолт, а git pull - только когда заведомо знают, что прилетит ff-слияние.

Это прагматичный выбор, не догматический. Если у тебя маленький репо, один разработчик и нечасто пушится -git pull без проблем. Если репо большое, активная команда, регулярные конфликты -fetch отдельно становится удобнее.

Стратегии слияния у pull:

bash
git pull                  # fetch + merge (по умолчанию)
git pull --rebase         # fetch + rebase (твои коммиты переписываются поверх)
git pull --ff-only        # fetch + только fast-forward, иначе отказ

Какую брать - следующая секция.

12.4 pull --ff-only: разумный дефолт

Из трёх вариантов pull (merge / rebase / ff-only) самый безопасный ---ff-only. И самый недооценённый.

Что он делает: попытается fast-forward (см. fast-forward). Если можно - двигает локальную ветку вперёд. Если нельзя (есть свои локальные коммиты после общего предка) - отказывает:

fatal: Not possible to fast-forward, aborting.

Это значит: «у тебя расхождение, реши явно - merge или rebase». Команда не делает выбор за тебя.

Альтернативные дефолты:

  • git pull без флага в Git 2.27+ при расхождении ветки (divergent branches) требует явной стратегии (pull.ff=... или pull.rebase=...), иначе пишет warning/ошибку. Если же возможен fast-forward, pull срабатывает без выбора - дефолт срабатывает только на разошедшихся историях.
  • pull.rebase = true - твои локальные коммиты ребейзятся поверх удалённых. Линейная история, но переписываются SHA; если ты уже push'нул эти коммиты, нужен force-with-lease.
  • pull.rebase = false (merge) - создаёт merge-коммит при каждом расхождении. Шум в истории, особенно на main.

Лучший выбор -pull.ff = only:

bash
git config --global pull.ff only

Теперь git pull либо fast-forward, либо отказ. Никаких тихих merge-коммитов в твоём main. При расхождении ты сам решаешь: git rebase origin/main для своей ветки, git merge origin/main для общей. Выбор осознанный.

Если тебя бесит «pull не работает без явной стратегии» - это хорошее место, чтобы пересмотреть привычки, а не подавлять warning'и.

12.5 push: что физически происходит

git push origin feat делает четыре вещи:

  1. Собирает packfile из коммитов, которых нет на remote.
  2. Передаёт их по сети.
  3. Просит remote передвинуть ref refs/heads/feat на новый SHA.
  4. Remote проверяет: можно ли fast-forward? Если да - двигает. Если нет - отказывает.

Самая частая ошибка - отказ:

! [rejected]    feat -> feat (non-fast-forward)

Это значит: на remote есть коммиты, которых у тебя нет. Решение - git fetch && git rebase origin/feat (затащить и переписать свои поверх), или git pull origin feat (затащить с merge). После этого push пройдёт.

tracking и push без аргументов

Чтобы git push без аргументов знал, куда идти, у ветки должен быть upstream - связь с конкретной удалённой веткой. Устанавливается при первом push (см. tracking-branch):

bash
git push -u origin feat
# эквивалент: git push --set-upstream origin feat

После этого git push пушит в origin/feat. И git status показывает, насколько ты «ahead/behind» этой ветки.

Без -u Git ругается и просит указать явно:

fatal: The current branch feat has no upstream branch.

Удаление удалённой ветки

bash
git push origin --delete feat
# или старый синтаксис (увидишь в скриптах):
git push origin :feat

Семантика старого варианта -«запушить пустоту в feat», что означает удалить. Современный синтаксис яснее.

Что push НЕ делает

  • Не пушит все локальные ветки. Только указанную (или текущую, если есть tracking). Чтобы все - нужно git push --all, и это редко то, что хочется.
  • Не пушит теги. Отдельно: git push --tags или git push origin v1.4.0.
  • Не делает fetch до push. Если на remote есть коммиты, push просто откажет.

12.6 --force-with-lease вместо --force

После локального rebase, amend, interactive rebase - SHA коммитов изменились. Обычный push отказывается перезаписать историю на remote:

! [rejected]    feat -> feat (non-fast-forward)

Это правильное поведение. Перезаписать нужно осознанно. Для этого есть два варианта:

bash
git push --force                # тупой: затирает всё
git push --force-with-lease     # умный: проверяет, что remote не ушёл

Подробности - в force-push. Короткая версия:

  • --force - говорит remote'у «забудь, что у тебя там, поставь мою историю». Если кто-то другой запушил между твоим последним fetch и push'ем - его коммит исчезает.
  • --force-with-lease - говорит «перезапиши, но только если SHA на remote = тому, что я последний раз видел». Если кто-то запушил - force отменяется с ошибкой (stale info).

Правило: никогда --force, всегда --force-with-lease.

Полезный алиас:

bash
git config --global alias.pushf 'push --force-with-lease'

Теперь git pushf - это уже безопасный force.

Один нюанс: --force-with-lease сравнивает с тем, что ты последний раз видел при fetch. Если ты давно не fetch'ил, твой «lease» устаревший. Но и противоположная крайность опасна: git fetch && git push --force-with-lease подряд обнуляет защиту - после fetch lease «свежий», и если кто-то успел запушить, ты затрёшь его коммиты, даже не посмотрев на них. Правильный порядок:

bash
git fetch
git log feat..origin/feat   # что прилетело?
# если есть чужие коммиты - rebase/merge их в свою ветку
git push --force-with-lease

То есть fetch → inspect → integrate → force-with-lease, а не fetch → force-with-lease «одной строкой».

Когда force нужен (короткий список)

  • После rebase своей feature-ветки на свежий main - стандартный сценарий перед PR.
  • После amend последнего коммита уже запушенной ветки.
  • После interactive rebase для очистки истории.

Всё это -только на ветках, которые видишь ты один. На main, master, develop force запрещён branch-protection правилами форджа. Если protection нет - поставь.

12.7 Tracking-ветки: что куда указывает

Два разных термина легко путаются:

  • Tracking branch (или «upstream») - локальная ветка с привязкой к удалённой. Хранится в .git/config:
    [branch "main"]
        remote = origin
        merge = refs/heads/main
  • Remote-tracking branch - это origin/main, origin/feat/x и т.п. Не «локальная ветка с tracking», а отдельный класс refs: snapshot remote'а на момент последнего fetch.

Связь:

локальная main  ─ tracks ─→  origin/main (remote-tracking branch)
                                 ↑
                                 git fetch обновляет это

Что даёт tracking:

  • git pull без аргументов знает, откуда тянуть.
  • git push без аргументов знает, куда пушить.
  • git status пишет «ahead 3, behind 2».
  • git branch -vv показывает все локальные ветки с upstream.

Как установить:

bash
# При первом push
git push -u origin feat
# Постфактум
git branch --set-upstream-to=origin/feat
# При switch -c из remote
git switch -c feat origin/feat

Все три эквивалентны: в каждом случае в .git/config появится branch.feat.remote = origin и branch.feat.merge = refs/heads/feat.

remote-tracking ветки нельзя редактировать

origin/main - это локальное «отражение» remote-ветки. git checkout origin/main переведёт тебя в detached HEAD (см. detached-head) - это нормально, но непривычно. Чтобы продолжить работу, нужно создать локальную ветку:

bash
git switch -c hotfix origin/main

Теперь у тебя локальная hotfix, ветвящаяся от состояния origin/main на момент последнего fetch.

12.7.1 Подводный камень: refspec и что пушится по умолчанию

В .git/config под каждым remote стоит:

fetch = +refs/heads/*:refs/remotes/origin/*

Это «refspec»: «маппинг» того, какие refs тащить с remote и под каким именем класть локально. По умолчанию - все ветки remote под именами origin/*.

Можно сузить:

fetch = +refs/heads/main:refs/remotes/origin/main

Теперь fetch тянет только main. На репах с тысячами веток экономит трафик и место.

Для push есть симметричный refspec - но обычно полагаются на настройку push.default:

bash
git config --global push.default simple

simple - пушит текущую локальную ветку в одноимённую удалённую, и только если есть tracking. Это разумный дефолт с Git 2.0.

Альтернативы:

  • current - пушит в одноимённую удалённую, даже без tracking (создаёт её при необходимости).
  • upstream - пушит в upstream, даже если она называется иначе.
  • nothing - отказывается пушить без явного указания.
  • matching - старый дефолт до Git 2.0, пушит все локальные ветки в одноимённые удалённые. Не используй, это источник «случайно запушил с десяток ненужных веток».

12.8 Работа через fork с upstream

Самая частая ситуация в open-source: у тебя нет прав writer'а на основной репо, нужно сделать fork.

Цикл (подробнее в upstream-vs-origin):

bash
# 1. На GitHub: Fork кнопка
# 2. Локально клонируем свой fork
git clone git@github.com:me/awesome.git
cd awesome
# 3. Добавляем оригинал как upstream
git remote add upstream git@github.com:original/awesome.git
git fetch upstream
# 4. Создаём фича-ветку от свежей upstream/main
git switch -c feat/my-fix upstream/main

Главная деталь шага 4: ветвиться от upstream/main, не от origin/main. Иначе твой fork может отставать на дни, и в PR попадёт устаревшая база.

Продолжаем:

bash
# 5. ... коммиты ...
# 6. Пушим в свой fork (origin), не в upstream
git push -u origin feat/my-fix
# 7. На GitHub открываем PR: me:feat/my-fix → original:main

Когда нужно догнать upstream после долгой работы:

bash
git fetch upstream
git switch feat/my-fix
git rebase upstream/main
# Если есть конфликт - разрешаем, --continue.
# После rebase нужен force-with-lease, так как SHA изменились
git push --force-with-lease

Это и есть «sync fork» из терминала. На GitHub есть кнопка «Sync fork» в UI; из CLI её эквивалент - gh repo sync <owner>/<repo> -b main (работает и для своего fork, и для форков, к которым у тебя есть доступ). Ручной цикл fetch + rebase + force-with-lease остаётся незаменимым, когда нужно контролировать стратегию слияния или разрешать конфликты, - gh repo sync отказывается синкать при расхождении.

Уроки в sandbox

lab-12.1. Fork-flow с двумя remote вживую

Цель - пройти полный цикл работы через fork: clone, добавить upstream, ветвиться от upstream, push в origin, синхронизация через fetch+rebase. Без GitHub UI: всё из терминала на двух локальных репозиториях, имитирующих «оригинал» и «fork».

  1. Создай два локальных репозитория, имитирующих оригинал и форк: mkdir -p /tmp/fork-lab && cd /tmp/fork-lab && git init --bare original.git && git init --bare fork.git. Bare-репозитории - это серверная сторона, без working tree.

  2. Засей оригинал одним коммитом: mkdir seed && cd seed && git init && echo 'v1' > version.txt && git add . && git commit -m 'init' && git remote add origin /tmp/fork-lab/original.git && git branch -M main && git push origin main && cd .. && rm -rf seed.

  3. Засей форк копией: cd /tmp/fork-lab && git clone original.git seed2 && cd seed2 && git remote set-url origin /tmp/fork-lab/fork.git && git push origin main && cd .. && rm -rf seed2. Теперь fork отдельный bare с тем же содержимым.

  4. Клонируем форк, как обычный разработчик: cd /tmp/fork-lab && git clone fork.git my-work && cd my-work && git remote -v. Видишь один origin → fork.git.

  5. Добавляем оригинал как upstream: git remote add upstream /tmp/fork-lab/original.git && git fetch upstream && git remote -v. Теперь у тебя два remote: origin (твой fork) и upstream (оригинал).

  6. Имитируем активность в оригинале (другой разработчик). В отдельной сессии: cd /tmp && git clone /tmp/fork-lab/original.git orig-clone && cd orig-clone && echo 'v2' > version.txt && git commit -am 'bump to v2' && git push origin main. Оригинал ушёл вперёд.

  7. Возвращайся в my-work. Сделаем свою фичу, ветвимся от свежего upstream: cd /tmp/fork-lab/my-work && git fetch upstream && git switch -c feat/add-changelog upstream/main. Обрати внимание: НЕ git switch -c feat/x main - иначе ветка пошла бы от устаревшего origin.

  8. Сделай коммит на фиче: echo '# Changelog' > CHANGELOG.md && git add . && git commit -m 'add changelog'. Запушь в свой fork: git push -u origin feat/add-changelog. Обрати внимание - пушим в origin (свой fork), НЕ в upstream.

  9. Посмотри git log --oneline --graph --all. Увидишь: на upstream/main - два коммита (init, bump to v2), на твоей feat/add-changelog - три (init, bump to v2, add changelog). Это корректно: твоя ветка ушла от свежего upstream.

  10. Имитируем ещё один коммит в upstream (третий разработчик): cd /tmp/orig-clone && echo 'docs added' > README.md && git add . && git commit -m 'add readme' && git push origin main. Оригинал ушёл ещё дальше.

  11. Догоняем upstream через rebase: cd /tmp/fork-lab/my-work && git fetch upstream && git rebase upstream/main. Это перепишет твой коммит 'add changelog' поверх свежей upstream/main.

  12. После rebase нужен force-push в свой fork (origin), потому что SHA изменился: git push --force-with-lease. Обрати внимание ---force-with-lease, не просто --force.

  13. Посмотри git log --oneline --graph --all ещё раз. Теперь history линейная: init → bump v2 → add readme → add changelog. Это и есть «sync fork» из терминала.

  14. Подсчитай: ты использовал в этой лабе clone, remote add, fetch, switch -c <remote-branch>, push - u, rebase, push --force-with-lease. Это полный набор команд для работы через fork.

  15. Уборка: cd /tmp && rm -rf fork-lab orig-clone.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Remote - это просто имя для URL под этим именем. Никакой магии, всё описано в .git/config. См. [[remote-cmd]].
  • fetch скачивает обновления и обновляет refs/remotes/origin/*, но не трогает локальные ветки. Безопасная команда, можно вызывать сколько угодно.
  • pull = fetch + merge (или rebase). Удобно, но между шагами нет паузы. Прагматичный дефолт -`git config --global pull.ff only`: либо fast-forward, либо явный выбор стратегии.
  • push требует fast-forward на стороне remote. После rebase/amend нужен force, и тогда - всегда --force-with-lease, никогда --force.
  • Tracking-ветка (`branch.X.remote` в config) связывает локальную с конкретной удалённой. Устанавливается через `push -u` или `branch --set-upstream-to`.
  • При работе через fork два remote: origin (свой fork) и upstream (оригинал). Ветки создаются от upstream/main, пушатся в origin/*, синхронизация - через fetch upstream + rebase + force-with-lease.
  • Branch protection на main - обязательная страховка. Без неё человеческая ошибка force-push в main стирает чужие коммиты.

Контрольные вопросы

  1. Я пушу: 'rejected: non-fast-forward'. Что произошло и какие у меня варианты?

    Показать ответ

    На remote есть коммиты, которых у тебя нет - кто-то запушил между твоим последним pull/fetch и push'ем. Push отказался, потому что иначе пришлось бы либо перезаписать чужие коммиты (опасно), либо смешать истории способом, который Git не делает молча. Варианты:

    1. fetch + rebase (для твоей feature-ветки): git fetch origin && git rebase origin/feat. Затащит чужие коммиты, перепишет твои поверх. После - push (нужен --force-with-lease, потому что SHA изменился).
    2. fetch + merge (для общих веток типа main): git pull или git fetch && git merge origin/main. Получишь merge-коммит. После - обычный push, без force.

    Никогда не решай это через git push --force без with-lease: это может молча затереть чужие коммиты, если кто-то ещё успел что-то запушить.

  2. В чём принципиальная разница между `git fetch origin` и `git pull origin main`?

    Показать ответ

    fetch скачивает данные с remote и обновляет remote-tracking-ветки (origin/main, origin/feat/x), но локальные ветки (main, feat/x) не трогает. Рабочий каталог не меняется. Это безопасная команда, её можно вызывать сколько угодно раз.

    pull делает то же самое, плюс сразу же мерджит (или ребейзит) origin/main в твою локальную main. То есть изменяет локальную ветку и, возможно, файлы в рабочем каталоге.

    Прагматичная привычка для опытных команд: fetch отдельно, смотрим git log main..origin/main, потом сознательно merge или rebase. Для одиночной работы или маленьких репов pull тоже нормально, особенно с pull.ff only.

  3. У меня всегда `git pull` ругается warning'ом: 'You have divergent branches and need to specify how to reconcile them'. Как правильно реагировать?

    Показать ответ

    Git 2.27+ не делает выбор за тебя между merge и rebase. Это правильно. Самый безопасный ответ - настроить дефолт ff only:

    git config --global pull.ff only

    Теперь git pull либо fast-forward, либо отказ. На отказе сам решаешь: git rebase origin/main для своей feature-ветки, git merge origin/main для общей. Альтернативы (pull.rebase true, pull.ff false) тоже работают, но fewer surprises.

    Подавлять warning через git config pull.rebase false без понимания - это вернуться в режим «Git делает решение за меня», где и появлялись неожиданные merge-коммиты в main.

  4. Я сделал rebase локальной feature-ветки, теперь push'ю с --force-with-lease, и Git говорит '(stale info)'. Что это и что делать?

    Показать ответ

    --force-with-lease сравнивает SHA удалённой ветки с тем, что ты последний раз видел через fetch. (stale info) означает, что remote ушёл вперёд: кто-то запушил в твою ветку после твоего последнего fetch'а. Force отменён, чтобы случайно не затереть эти коммиты.

    Что делать: git fetch origin, посмотри что прилетело - git log origin/feat..feat и git log feat..origin/feat. Если это нормальные коммиты, которые тебе нужны - сделай git rebase origin/feat (или git pull --rebase), потом повтори --force-with-lease. Теперь lease свежий, force пройдёт.

    Если ты единственный, кто работает с этой веткой, и точно знаешь, что прилетевшее не нужно ---force без lease затрёт их. Но это редкая ситуация: обычно stale info - это сигнал «кто-то ещё что-то делал, посмотри».

  5. Я работаю через fork: origin = мой fork, upstream = оригинал. Когда я делаю новую ветку, надо ветвиться от origin/main или от upstream/main?

    Показать ответ

    От upstream/main. Это важно. Если ты ветвишься от origin/main, твоя ветка стартует с того состояния, в котором твой fork был при последней синхронизации. Если ты давно не синкал - это может быть состояние на несколько дней или недель назад, и в PR попадут все «промежуточные» коммиты разницы между origin/main и upstream/main.

    Правильно:

    git fetch upstream
    git switch -c feat/x upstream/main

    Это гарантирует, что твоя ветка стартует от самой свежей версии оригинала. После коммитов пушишь её в origin (свой fork) и открываешь PR из неё в upstream.

    Полезно держать в main свой fork актуальной для случаев, когда нужно ветвиться без upstream/:

    git fetch upstream
    git switch main
    git rebase upstream/main
    git push origin main

    Но даже после этого ветвиться от upstream/main безопаснее: не зависит от того, успел ли ты обновить свой main.

← Предыдущая11-branching-strategiesСледующая →13-submodules-worktrees
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки