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>.
Базовые команды управления:
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. Только
половина: «скачать», без «слить». В этом и его сила.
git fetch origin
Что делает (см. fetch):
- Подключается к remote.
- Узнаёт список refs и SHA.
- Скачивает все коммиты, которых нет локально, в
.git/objects/. - Обновляет
refs/remotes/origin/<branch>- это remote-tracking ветки.
Локальные main, feat/x, HEAD -не меняются. Меняются
только origin/main, origin/feat/x. Это безопасная команда:
её можно вызывать сколько угодно раз, рабочий каталог не
затрагивается.
Что с этим делать дальше:
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 есть пауза, в которую можно подумать. На сложных репозиториях это полезно: ты видишь, что прилетело, прежде чем что-то локально менять.
Полезные флаги:
git fetch --all # все remote сразу
git fetch --prune # удалить локальные refs мёртвых веток
git fetch --tags # включая теги
--prune особенно полезен: в долго живущем клоне ветки в
origin/ копятся, даже после того как их удалили на remote.
Без --prune через год их там десятки. Поставь раз и навсегда:
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:
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:
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 делает четыре вещи:
- Собирает packfile из коммитов, которых нет на remote.
- Передаёт их по сети.
- Просит remote передвинуть ref
refs/heads/featна новый SHA. - 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):
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.
Удаление удалённой ветки
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)
Это правильное поведение. Перезаписать нужно осознанно. Для этого есть два варианта:
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.
Полезный алиас:
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 «свежий», и если кто-то успел
запушить, ты затрёшь его коммиты, даже не посмотрев на них.
Правильный порядок:
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.
Как установить:
# При первом 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) - это нормально, но непривычно. Чтобы
продолжить работу, нужно создать локальную ветку:
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:
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):
# 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 попадёт устаревшая база.
Продолжаем:
# 5. ... коммиты ...
# 6. Пушим в свой fork (origin), не в upstream
git push -u origin feat/my-fix
# 7. На GitHub открываем PR: me:feat/my-fix → original:main
Когда нужно догнать upstream после долгой работы:
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».
Создай два локальных репозитория, имитирующих оригинал и форк:
mkdir -p /tmp/fork-lab && cd /tmp/fork-lab && git init --bare original.git && git init --bare fork.git. Bare-репозитории - это серверная сторона, без working tree.Засей оригинал одним коммитом:
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.Засей форк копией:
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 с тем же содержимым.Клонируем форк, как обычный разработчик:
cd /tmp/fork-lab && git clone fork.git my-work && cd my-work && git remote -v. Видишь один origin → fork.git.Добавляем оригинал как upstream:
git remote add upstream /tmp/fork-lab/original.git && git fetch upstream && git remote -v. Теперь у тебя два remote: origin (твой fork) и upstream (оригинал).Имитируем активность в оригинале (другой разработчик). В отдельной сессии:
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. Оригинал ушёл вперёд.Возвращайся в 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.Сделай коммит на фиче:
echo '# Changelog' > CHANGELOG.md && git add . && git commit -m 'add changelog'. Запушь в свой fork:git push -u origin feat/add-changelog. Обрати внимание - пушим в origin (свой fork), НЕ в upstream.Посмотри
git log --oneline --graph --all. Увидишь: на upstream/main - два коммита (init, bump to v2), на твоей feat/add-changelog - три (init, bump to v2, add changelog). Это корректно: твоя ветка ушла от свежего upstream.Имитируем ещё один коммит в upstream (третий разработчик):
cd /tmp/orig-clone && echo 'docs added' > README.md && git add . && git commit -m 'add readme' && git push origin main. Оригинал ушёл ещё дальше.Догоняем upstream через rebase:
cd /tmp/fork-lab/my-work && git fetch upstream && git rebase upstream/main. Это перепишет твой коммит 'add changelog' поверх свежей upstream/main.После rebase нужен force-push в свой fork (origin), потому что SHA изменился:
git push --force-with-lease. Обрати внимание ---force-with-lease, не просто--force.Посмотри
git log --oneline --graph --allещё раз. Теперь history линейная: init → bump v2 → add readme → add changelog. Это и есть «sync fork» из терминала.Подсчитай: ты использовал в этой лабе clone, remote add, fetch, switch -c <remote-branch>, push - u, rebase, push --force-with-lease. Это полный набор команд для работы через fork.
Уборка:
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 стирает чужие коммиты.
Контрольные вопросы
Я пушу: 'rejected: non-fast-forward'. Что произошло и какие у меня варианты?
Показать ответ
На remote есть коммиты, которых у тебя нет - кто-то запушил между твоим последним pull/fetch и push'ем. Push отказался, потому что иначе пришлось бы либо перезаписать чужие коммиты (опасно), либо смешать истории способом, который Git не делает молча. Варианты:
- fetch + rebase (для твоей feature-ветки):
git fetch origin && git rebase origin/feat. Затащит чужие коммиты, перепишет твои поверх. После - push (нужен--force-with-lease, потому что SHA изменился). - fetch + merge (для общих веток типа main):
git pullилиgit fetch && git merge origin/main. Получишь merge-коммит. После - обычный push, без force.
Никогда не решай это через
git push --forceбезwith-lease: это может молча затереть чужие коммиты, если кто-то ещё успел что-то запушить.- fetch + rebase (для твоей feature-ветки):
В чём принципиальная разница между `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.У меня всегда `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.Я сделал 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- это сигнал «кто-то ещё что-то делал, посмотри».Я работаю через 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.