4.1Remote
Сопоставишь локальную ветку, remote-tracking ref и состояние сервера на момент последнего fetch. Разберёшь refspec, отказ push и защиту от затирания чужих коммитов.
- Что
git cloneпишет в конфиг и почему локальная ветка после клона одна. - Как читается refspec и почему в клоне с
--depth 1не находится соседняя ветка. - Чем
origin/mainотличается отmain, что меняетfetchи что добавляетpull. - Почему сервер отклоняет push и чем «fetch first» отличается от «non-fast-forward».
- Когда
--force-with-leaseспасает чужой коммит, когда нет и что к нему добавляет--force-if-includes.
Клон: имя, адрес и ветки сервера
Примеры главы идут на Go-сервисе shop. Сервер стоит на той же машине: git http-backend
отдаёт голый репозиторий по smart HTTP, как GitLab и GitHub при клоне по HTTPS.
Боря уже отправил туда main с двумя коммитами и ветку metrics. Второго сентября
Аня клонирует проект:
$ git clone http://localhost:8765/shop.git
Cloning into 'shop'...
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 12 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (12/12), done.
Resolving deltas: 100% (2/2), done.
$ cd shop
$ git remote -v
origin http://localhost:8765/shop.git (fetch)
origin http://localhost:8765/shop.git (push)
$ git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
remotes/origin/metrics
Строки с remote: печатает сервер, пока собирает pack-файл (глава 1.4), остальные печатает
клиент, пока его принимает. Такие счётчики есть в каждом обмене, где передаются объекты, дальше в выводе
push и fetch они опущены. Клон по пути к каталогу (git clone ../srv/shop.git) идёт в обход
протокола, после Cloning into там только done.: git копирует объекты напрямую,
связывая файлы жёсткими ссылками.
clone заводит один remote,
origin, и git remote -v печатает его адрес дважды: откуда забирать и куда
отправлять. Адреса разойдутся, если задать отдельный remote.origin.pushurl. Сервер отдал
все ветки, но локальная появилась одна. Остальные лежат под remotes/origin/: это
remote-tracking ветки. В конфиг клон дописал вот что:
$ tail -n 6 .git/config
[remote "origin"]
url = http://localhost:8765/shop.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
url говорит, куда ходить. fetch хранит refspec. Секция [branch "main"] назначает ветке upstream, а про
origin/HEAD было в главе 1.2.
Remote бывает и не один. В клоне форка исходный проект подключают вторым,
git remote add upstream <адрес>, и его ветки едут в refs/remotes/upstream/.
Это имя из документации GitHub, с upstream ветки из этой главы оно не связано. Если ветка есть в обоих
remote, git switch metrics откажет (matched multiple), и remote называют сами:
git switch --track origin/metrics или checkout.defaultRemote=origin в конфиге.
Refspec: какие ссылки куда класть
Строку +refs/heads/*:refs/remotes/origin/* делит двоеточие. Слева ссылки на сервере,
справа имена, под которыми они лягут в клоне, а звёздочка переносит хвост имени:
refs/heads/metrics сервера становится refs/remotes/origin/metrics. Плюс
разрешает перезаписать локальную ссылку, даже если новое значение не потомок старого. Без плюса fetch
отказался бы обновить origin/metrics после чужого rebase и напечатал бы
! [rejected] metrics -> origin/metrics (non-fast-forward). Какие ссылки есть на сервере,
показывает git ls-remote, ничего не скачивая:
$ git ls-remote origin
af76ac493a46ae06b87e04e9efa081d5da3f693b HEAD
af76ac493a46ae06b87e04e9efa081d5da3f693b refs/heads/main
1d477fd3a5b0963b561de14c333507dccd8ad1ac refs/heads/metrics
1d477fd3a5b0963b561de14c333507dccd8ad1ac refs/merge-requests/1/head
Последняя ссылка устроена как на GitLab, который заводит refs/merge-requests/<номер>/head
на каждый merge request (здесь её создали руками). Под refs/heads/* она не подпадает, поэтому
в клон не приехала. Второе правило дописывают ещё одной строкой fetch:
$ git config --add remote.origin.fetch '+refs/merge-requests/*/head:refs/remotes/origin/mr/*'
$ git fetch
From http://localhost:8765/shop
* [new ref] refs/merge-requests/1/head -> origin/mr/1
$ git branch -r
origin/HEAD -> origin/main
origin/main
origin/metrics
origin/mr/1
Теперь код merge request открывается локально через git switch --detach origin/mr/1, даже
если своей ветки у него в этом репозитории нет. Refspec можно написать и прямо в команде:
git fetch origin metrics значит «забери ветку metrics», правая часть пустая.
$ git fetch origin metrics
From http://localhost:8765/shop
* branch metrics -> FETCH_HEAD
Результат лёг в .git/FETCH_HEAD. Если ветка подходит под правило из
remote.origin.fetch, git с версии 1.8.4 попутно сдвигает и её remote-tracking ветку (здесь
origin/metrics и так свежая). Без правила remote-tracking ветки нет.
--depth включает --single-branch, и clone записывает правило только для
одной ветки. Забрать другую по имени получится, но remote-tracking ветки у неё не будет:
$ git clone -q --depth 1 http://localhost:8765/shop.git
$ cd shop
$ git config --get-all remote.origin.fetch
+refs/heads/main:refs/remotes/origin/main
$ git fetch origin metrics
From http://localhost:8765/shop
* branch metrics -> FETCH_HEAD
$ git switch metrics
fatal: invalid reference: metrics
$ git remote set-branches --add origin metrics
$ git fetch
From http://localhost:8765/shop
* [new branch] metrics -> origin/metrics
set-branches --add дописал второе правило, и ветка приехала. На том же спотыкаются шаги
CI, которые сравнивают ветку merge request с origin/main в неглубоком клоне: нет правила,
нет и origin/main. Про глубину истории в CI есть вопрос в главе 1.4.
fetch: забрать и ничего не трогать
Пока Аня возилась с refspec, Боря отправил в main коммит. Клон Ани об этом не знает:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
«Up to date» значит «совпадает с origin/main», а origin/main показывает туда же,
куда при клонировании. status в сеть не ходит. Сервер спрашивает git fetch:
$ git fetch
From http://localhost:8765/shop
af76ac4..d088609 main -> origin/main
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
Строка fetch описывает одну ссылку: флаг, старое и новое значение, откуда и куда. Флаги из этой главы:
| Флаг | Что случилось со ссылкой | Как выглядит |
|---|---|---|
| пробел | перемотана вперёд | af76ac4..d088609 main -> origin/main |
* | новая ссылка | [new branch] metrics -> origin/metrics |
+ | перезаписана не вперёд, между хешами три точки | b6a0d07...a0dc01d retry -> origin/retry (forced update) |
- | удалена при --prune | [deleted] (none) -> origin/metrics |
t | тег переставлен | [tag update] v0.2.0 -> v0.2.0, глава 4.3 |
! | отклонена | [rejected] metrics -> origin/metrics (non-fast-forward) |
fetch докачал недостающие объекты и сдвинул origin/main. Локальная main, индекс
и файлы остались как были: ветки под refs/heads/ fetch трогает, только если правая часть
refspec прямо на них указывает. Поэтому его можно звать когда угодно, хоть посреди незакоммиченной
работы. Что пришло, показывает диапазон из главы 1.2, а список всего принесённого лежит в
FETCH_HEAD (первая колонка с хешами здесь отрезана):
$ git log --oneline main..origin/main
d088609 (origin/main, origin/HEAD) Отдавать версию в /health
$ cut -f 2- .git/FETCH_HEAD
branch 'main' of http://localhost:8765/shop
not-for-merge branch 'metrics' of http://localhost:8765/shop
not-for-merge 'refs/merge-requests/1/head' of http://localhost:8765/shop
Первая строка без пометки not-for-merge: это upstream текущей ветки, её и вливает
git pull. Аня вливает сама, и слияние выходит перемоткой (глава 3.1):
$ git merge origin/main
Updating af76ac4..d088609
Fast-forward
health.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
main на сервере. fetch принёс
новый коммит в клон Ани и сдвинул только origin/main, а main и файлы догонят
сервер после слияния.Upstream: с какой веткой сервера связана твоя
Аня заводит ветку для повторов запросов к базе и пишет store.go. У новой ветки нет пары на
сервере, и push без аргументов не знает, куда отправлять:
$ git switch -c retry
Switched to a new branch 'retry'
$ git add store.go
$ git commit -qm "Повторять запрос к базе"
$ git push
fatal: The current branch retry has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin retry
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
$ git push -u origin retry
To http://localhost:8765/shop.git
* [new branch] retry -> retry
branch 'retry' set up to track 'origin/retry'.
$ git config --get-regexp '^branch\.retry\.'
branch.retry.remote origin
branch.retry.merge refs/heads/retry
Эти две строки и задают upstream. В клоне его
представляет origin/retry. -u (длинно --set-upstream) записал их при
первом push, и дальше по ним работают команды без аргументов: push и pull знают,
куда и откуда, status знает, с чем сравнивать. В командах upstream пишется @{u}:
$ git commit -qam "Ограничить число повторов"
$ git status -sb
## retry...origin/retry [ahead 1]
$ git rev-parse --abbrev-ref @{u}
origin/retry
$ git rev-list --left-right --count @{u}...HEAD
0 1
$ git log --oneline @{u}..
c52f3d6 (HEAD -> retry) Ограничить число повторов
$ git push
To http://localhost:8765/shop.git
6e5288e..c52f3d6 retry -> retry
Ahead и behind git считает обходом с тремя точками из главы 1.2: слева коммиты только upstream, справа
только твоей ветки. Считает по origin/retry, так что «behind» без fetch устаревает, а вот
git log @{u}.. не врёт и без него: свой push сдвигает origin/retry сам.
Upstream появляется и без -u. Если локальной ветки нет, а remote-tracking с таким именем
есть ровно в одном remote, git switch создаёт ветку и сразу связывает её:
$ git switch metrics
branch 'metrics' set up to track 'origin/metrics'.
Switched to a new branch 'metrics'
$ git branch -vv
main d088609 [origin/main] Отдавать версию в /health
* metrics 1d477fd [origin/metrics] Считать запросы
retry c52f3d6 [origin/retry] Ограничить число повторов
Куда уходит git push без аргументов, решает push.default. С git 2.0 там
simple: git отправляет текущую ветку в её upstream, и только если имена совпадают. В другой
remote (git push fork) ветка уйдёт в одноимённую и без upstream. Второе условие всплывает,
когда ветку начинают прямо от remote-tracking:
$ git switch -c hotfix origin/main
branch 'hotfix' set up to track 'origin/main'.
Switched to a new branch 'hotfix'
Upstream ветки hotfix теперь origin/main, и push по upstream ушёл бы прямо в
main сервера. git push отказывается со словами fatal: The upstream branch
of your current branch does not match the name of your current branch. Чинят одним из двух способов:
git push -u origin hotfix перепишет upstream на origin/hotfix, а ветка, созданная с
--no-track, его вовсе не получит. Настройка push.autoSetupRemote=true (git 2.37)
сама добавит -u при первом push ветки без upstream, и после --no-track хватит
простого git push. Ветку hotfix после опыта Аня удалила и вернулась в
main.
Push: сервер ждёт перемотку
Третьего сентября Аня коммитит в main логирование запросов. Боря в это время поднимает
таймаут сервера и отправляет первым. Push Ани:
$ git push
To http://localhost:8765/shop.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'http://localhost:8765/shop.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git fetch
From http://localhost:8765/shop
d088609..23d68bc main -> origin/main
$ git push
To http://localhost:8765/shop.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'http://localhost:8765/shop.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git status -sb
## main...origin/main [ahead 1, behind 1]
Push начинается с того, что сервер перечисляет свои ссылки с хешами. Клиент сравнивает вершину сервера с тем, что собирается отправить, и без флагов принуждения пропускает только перемотку (глава 3.1): новая вершина должна быть потомком старой. Иначе коммит, на который указывала ветка сервера, выпал бы из неё.
Два текста отказа различаются тем, что клиент знает о чужом коммите. В первый раз
23d68bc у Ани не было, и проверить родство git не мог: fetch first. После fetch
коммит есть, и ответ точный:
$ git merge-base --is-ancestor origin/main main; echo $?
1
origin/main не предок main, ветки разошлись: non-fast-forward. Проверять
умеет и сам сервер. Голый репозиторий с receive.denyNonFastForwards=true не пропустит
даже --force: объекты он примет, ссылку не сдвинет и ответит
! [remote rejected] main -> main (non-fast-forward). Защищённые ветки GitLab и GitHub по
умолчанию так же запрещают принудительный push (глава 4.2).
pull: fetch, а потом merge или rebase
git pull запускает git fetch и вливает upstream в текущую ветку. Пока ветки не
разошлись, он просто перематывает. Разошлись, и git 2.54 без настройки не выбирает за тебя:
$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
Отказ появился в git 2.33.1. Раньше pull делал merge-коммит, а с 2.27 ещё и предупреждал об этом. Вот что получается слиянием:
$ git pull --no-rebase
Merge made by the 'ort' strategy.
main.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
$ git log --oneline --graph -4
* 282f10f (HEAD -> main) Merge branch 'main' of http://localhost:8765/shop
|\
| * 23d68bc (origin/main, origin/HEAD) Поднять таймаут до 5 секунд
* | b120e8a Логировать запросы
|/
* d088609 Отдавать версию в /health
Ветка main слилась сама с собой. За узлом «Merge branch 'main' of …» нет никакого решения,
два человека просто коммитили в одну ветку в одно и то же утро, а log --graph и
bisect теперь обходят лишнюю развилку. Аня отменяет слияние (reset разобран в
главе 5.1) и переставляет свой коммит поверх коммита Бори:
$ git reset --hard ORIG_HEAD
HEAD is now at b120e8a Логировать запросы
$ git pull --rebase
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph -3
* 2db9371 (HEAD -> main) Логировать запросы
* 23d68bc (origin/main, origin/HEAD) Поднять таймаут до 5 секунд
* d088609 Отдавать версию в /health
$ git push
To http://localhost:8765/shop.git
23d68bc..2db9371 main -> main
У коммита Ани новый хеш 2db9371, потому что у него новый родитель (глава 3.3). Переписывать
его можно было спокойно: b120e8a на сервер не попал. Чтобы не выбирать каждый раз, способ
задают в конфиге:
| Настройка | Ветки разошлись | Кому подходит |
|---|---|---|
pull.rebase=true | свои коммиты переносятся поверх пришедших | общая ветка, где нужна линейная история |
pull.rebase=false | merge-коммит | тем, кто хочет видеть, где работа шла параллельно |
pull.ff=only | отказ после fetch: ветка и файлы не тронуты | тем, кто после fetch решает сам |
Если в конфиге и pull.rebase=true, и pull.ff=only, git 2.54 на разошедшихся ветках
откажет с Not possible to fast-forward, а вот --rebase в командной строке
pull.ff=only перебивает. Вариант с rebase строже к рабочей копии: с незакоммиченными правками
он не начнётся (cannot pull with rebase: You have unstaged changes), слияние же терпит правки
в файлах, которых не касается. --autostash или rebase.autoStash=true уберут
правки в stash (глава 2.2) на время pull и вернут после.
Коллега переписал ветку, поверх которой лежат твои коммиты. git rebase origin/topic
попробует перенести и его старые коммиты, а git pull --rebase и git rebase без
аргументов ищут точку отсчёта в reflog remote-tracking ветки (git merge-base --fork-point)
и переносят только сделанное поверх её прежней вершины. В опыте, где коллега исправил свой коммит,
git rebase origin/topic встал на конфликте в этом коммите, а git pull --rebase
перенёс один коммит без вопросов. Вручную то же делает
rebase --onto из главы 3.3.
--force, --force-with-lease и --force-if-includes
Отказ non-fast-forward бывает ожидаемым: ты сам переписал свою ветку. У Ани открыт merge request
из retry, а main за это время ушла вперёд. Четвёртого сентября в 10:00 Боря прямо
на ревью дописал в её ветку проверку отмены контекста и отправил. В 10:30 Аня, не делая fetch,
переносит ветку на свежую main:
$ git switch retry
Switched to branch 'retry'
Your branch is up to date with 'origin/retry'.
$ git rebase main
Successfully rebased and updated refs/heads/retry.
Обычный git push отклонён с (fetch first), как в разделе выше: коммита Бори у Ани
нет. --force снимает проверку целиком. Его прогоним на копии сервера и обоих клонов, а после опыта
вернём всё как было:
$ git push --force
To http://localhost:8765/shop.git
+ b6a0d07...a0dc01d retry -> retry (forced update)
У Бори после fetch:
$ git fetch
From http://localhost:8765/shop
+ b6a0d07...a0dc01d retry -> origin/retry (forced update)
$ git log --oneline -3 origin/retry
a0dc01d (origin/retry) Ограничить число повторов
09f8682 Повторять запрос к базе
2db9371 (origin/main, origin/HEAD) Логировать запросы
Коммита b6a0d07 в ветке сервера больше нет, и никто ни о чём не предупредил. Он уцелел только
в клоне Бори. Дальше снова настоящие репозитории, где --force не звали.
--force-with-lease ставит условие: перезаписать ветку сервера, только если она стоит там, где
её последний раз видел твой клон, то есть на origin/retry. У Ани там c52f3d6, на
сервере b6a0d07:
$ git push --force-with-lease
To http://localhost:8765/shop.git
! [rejected] retry -> retry (stale info)
error: failed to push some refs to 'http://localhost:8765/shop.git'
Защита работает, пока origin/retry сдвигается только тогда, когда ты сам посмотрел на
новые коммиты. Фоновый fetch это ломает: IDE, которая забирает изменения по таймеру (в VS Code это
git.autofetch), или git fetch, набранный между делом:
$ git fetch
From http://localhost:8765/shop
c52f3d6..b6a0d07 retry -> origin/retry
Ветка Ани не изменилась, коммита Бори в ней нет, но ожидание lease теперь совпадает с сервером.
git push --force-with-lease выкинул бы b6a0d07 так же, как --force: на
копии --dry-run ответил forced update. Эту дыру закрывает
--force-if-includes из git 2.30:
$ git push --force-with-lease --force-if-includes
To http://localhost:8765/shop.git
! [rejected] retry -> retry (remote ref updated since checkout)
error: failed to push some refs to 'http://localhost:8765/shop.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Второй флаг проверяет, что вершина origin/retry достижима из какой-нибудь записи reflog
локальной retry (глава 5.2). Грубо говоря, этот коммит хоть раз
был в твоей ветке, а b6a0d07 там не был. Сам по себе флаг ничего не делает: он работает
только вместе с --force-with-lease без явного хеша.
main, а на сервере в
retry успел появиться коммит Бори. --force его выбрасывает. Lease спасает, пока
origin/retry не сдвинул фоновый fetch, а --force-if-includes ещё и проверяет, был ли
коммит в ветке Ани.Чтобы push прошёл честно, коммит Бори надо забрать в ветку. Проще всего поставить ветку на серверную версию и повторить rebase:
$ git log --oneline ORIG_HEAD..origin/retry
b6a0d07 (origin/retry) Не повторять после отмены контекста
$ git reset --hard origin/retry
HEAD is now at b6a0d07 Не повторять после отмены контекста
$ git rebase main
Successfully rebased and updated refs/heads/retry.
$ git push --force-with-lease --force-if-includes
To http://localhost:8765/shop.git
+ b6a0d07...7ffc204 retry -> retry (forced update)
ORIG_HEAD после rebase хранит прежнюю вершину ветки (глава 3.3), поэтому диапазон показал
ровно то, что добавили на сервере. После reset вершина b6a0d07 попала в reflog
retry, и проверка прошла. Набирать оба флага не обязательно:
git config push.useForceIfIncludes true добавляет второй к каждому
--force-with-lease.
После отказа git советует git pull. На копии git pull --rebase перенёс на
origin/retry всё, чего там не было, включая два коммита из main. В ветке появились
копии «Поднять таймаут до 5 секунд» и «Логировать запросы» с новыми хешами, а push стал
обычной перемоткой: его пропустил бы и простой git push, --force-if-includes ничего
не решал. А аккуратный git cherry-pick origin/retry
проверку не проходит: у копии другой хеш, и вершина сервера в reflog не попадает. Проверка смотрит на
граф, а не на содержимое.
Удалить ветку на сервере и убрать следы у себя
Пятого сентября Боря влил metrics в main через squash (глава 3.3) и удалил ветку
на сервере. GitLab и GitHub умеют удалять исходную ветку сами после слияния, если это включить. Из
командной строки так:
$ git push origin --delete metrics
To http://localhost:8765/shop.git
- [deleted] metrics
Для git удаление тоже push, с пустой левой частью refspec: git push origin :metrics делает то
же самое. Клон Ани об этом не узнал, и обычный fetch ему не расскажет:
$ git fetch
From http://localhost:8765/shop
2db9371..525eda2 main -> origin/main
$ git branch -a
main
metrics
* retry
remotes/origin/HEAD -> origin/main
remotes/origin/main
remotes/origin/metrics
remotes/origin/mr/1
remotes/origin/retry
$ git fetch --prune
From http://localhost:8765/shop
- [deleted] (none) -> origin/metrics
По умолчанию fetch remote-tracking ветки только заводит и сдвигает. --prune удаляет те, которых
на сервере больше нет, вместе с их reflog. Локальная metrics при этом остаётся, только
upstream у неё пропал:
$ git branch -vv
main 2db9371 [origin/main: behind 1] Логировать запросы
metrics 1d477fd [origin/metrics: gone] Считать запросы
* retry 7ffc204 [origin/retry] Не повторять после отмены контекста
$ git branch -d metrics
error: the branch 'metrics' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D metrics'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
$ git branch -D metrics
Deleted branch metrics (was 1d477fd).
-d удаляет ветку, только если она влита в свой upstream, а без upstream в HEAD. После squash в
main лежит новый коммит с тем же содержимым, а 1d477fd по графу никуда не влит.
Где сливают через squash, -D приходится звать самому, сверившись с main. Ветки с
пропавшим upstream для скрипта перечисляет
git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads, у них в конце
стоит [gone].
Чтобы не вспоминать про --prune, его включают в конфиге: git config fetch.prune
true в этом клоне, с --global во всех. Теги так не чистятся, им нужен отдельный
--prune-tags (глава 4.3).
Вопросы
4Что меняет каждая команда
| Что | git fetch | git pull |
|---|---|---|
| объекты | докачивает | докачивает |
refs/remotes/origin/*, FETCH_HEAD | обновляет | обновляет |
| текущая ветка | не трогает | перематывает, сливает или переносит коммиты |
| индекс и рабочая копия | не трогает | обновляет, может остановиться на конфликте |
Откуда лишние merge-коммиты
Аня и Боря закоммитили в main каждый своё. git pull --no-rebase (и git старше
2.33.1 без настройки) сделал 282f10f Merge branch 'main' of http://localhost:8765/shop, узел
без всякого решения. pull.rebase=true переносит неотправленные коммиты поверх пришедших:
b120e8a стал 2db9371, история осталась линейной.
Как работать без сюрпризов
Разделить шаги: git fetch, git log --oneline main..origin/main, а потом merge
или rebase. Или pull.ff=only: где можно, pull перематывает, на разошедшихся ветках отказывает
уже после fetch: origin/main сдвинут, ветка и файлы нет.
Команда вливает main сервера в текущую ветку. Стоя на feature с
--no-rebase, получишь коммит «Merge branch 'main' of http://localhost:8765/shop into
feature», а локальная main останется на месте. Обновить её без переключения можно refspec с правой частью:
git fetch origin main:main перемотает локальную ветку, если она не выгружена в рабочую
копию и перемотка возможна.
main на сервере
двигает push. origin/main лежит в твоём клоне и помнит, где серверная ветка стояла при
последнем fetch или push. Локальную main двигают commit, merge и rebase. status
сравнивает main с origin/main и в сеть не ходит, поэтому о чужом коммите до fetch
не знает.Как узнать, что на сервере сейчас
До fetch клон Ани и сервер расходились, и это видно, если спросить сервер напрямую:
$ git log --oneline -1 origin/main
af76ac4 (HEAD -> main, origin/main, origin/HEAD) Добавить /health
$ git ls-remote origin main
d088609b9db020b83be01dbf97738cbe9d0bf31c refs/heads/main
ls-remote только читает список ссылок. git remote show origin тоже ходит на
сервер и у отставших веток пишет (local out of date). А git fetch обновляет
origin/main, и после него status говорит правду:
Your branch is behind 'origin/main' by 1 commit.
Почему в origin/main нельзя коммитить
git switch origin/main отказывается: fatal: a branch is expected, got remote branch
'origin/main'. С --detach HEAD отсоединится, и коммит останется без ветки (глава 1.2),
ведь remote-tracking ветку двигает только обмен с сервером.
--force-with-lease.Три вида отказа
| Отказ | Что это значит | Что делать |
|---|---|---|
(fetch first) | вершины сервера в клоне нет, родство не проверить | fetch и посмотреть, что пришло |
(non-fast-forward) | коммит сервера в клоне есть, но он не предок твоей вершины | встроить или, если переписывал сам, lease |
[remote rejected] | сервер сам запрещает перезапись: receive.denyNonFastForwards, защищённая ветка | встроить; --force не поможет |
Сначала посмотреть, чьи коммиты мешают
После git fetch команда git log --oneline main..origin/main покажет коммиты,
которых нет у тебя. У Ани там оказался один чужой:
$ git log --oneline main..origin/main
23d68bc (origin/main, origin/HEAD) Поднять таймаут до 5 секунд
Если в списке только прежние версии твоих коммитов, ветку переписывал ты сам (amend, rebase), и встраивать нечего. Чужую работу надо сохранить.
Как отправить
Чужие коммиты встраивают: git pull --rebase переставил b120e8a поверх
23d68bc, и push прошёл перемоткой 23d68bc..2db9371. Конфликт при rebase разбирают
по главе 3.2, а если за это время кто-то отправил ещё, fetch и rebase повторяют. --force
выбросил бы 23d68bc молча. Свою ветку после amend (глава 2.1) или rebase (глава 3.3)
встраивать не нужно, git pull по подсказке задвоит коммиты. Её отправляют через
--force-with-lease, если на ней никто не строит (следующий вопрос).
--force перезаписывает ветку сервера без условий.
--force-with-lease перезаписывает, только если ветка на сервере стоит там же, где её помнит
remote-tracking ветка, и так спасает от чужого push, которого ты не видел. Фоновый fetch сдвигает
remote-tracking ветку без твоего участия, и защита пропадает. --force-if-includes требует,
чтобы вершина remote-tracking ветки побывала в reflog твоей ветки.Что проверяет каждый вариант
| Команда | Условие перезаписи | Где подводит |
|---|---|---|
--force, +ветка | нет | выбрасывает всё, чего нет у тебя |
--force-with-lease | сервер = origin/<ветка> | после фонового fetch |
--force-with-lease=ветка:хеш | сервер = указанный хеш | хеш надо запомнить до rebase самому |
--force-with-lease --force-if-includes | сервер = origin/<ветка>, и её вершина есть в reflog ветки | после cherry-pick вместо rebase |
--force в той же команде отменяет lease: git push --force-with-lease --force
перезапишет ветку без проверки. Если remote-tracking ветки нет вовсе, а на сервере ветку уже создал
коллега, lease откажет с stale info.
Явное ожидание
Форма с хешем не смотрит на origin/retry, поэтому фоновый fetch ей не мешает. Её удобно
звать из скриптов: запомнить вершину через git rev-parse до переписывания и передать. Вывод
снят раньше, между rebase и последним push Ани:
$ git push --force-with-lease=retry:b6a0d07 --dry-run
To http://localhost:8765/shop.git
+ b6a0d07...7ffc204 retry -> retry (forced update)
С явным хешем --force-if-includes ничего не добавляет, так сказано в git help push.
Как это настроить один раз
git config push.useForceIfIncludes true (git 2.30) добавляет --force-if-includes к
каждому --force-with-lease. Найденные после отказа чужие коммиты забирают в ветку до push.
Все эти проверки смотрят, не появилось ли на сервере неожиданного. Если ты сам потерял коммит при rebase, например выбросил его в интерактивном списке, lease и if-includes честно пропустят push, и коммит исчезнет и с сервера. Вернуть его поможет reflog (глава 5.2), пока запись жива.
Что стоит запомнить
- origin/main - локальная копия известного состояния remote branch.
- fetch обновляет remote-tracking refs, но не сливает их в текущую ветку.
- --force-with-lease проверяет ожидаемое старое значение remote ref.
Частые ошибки
- Up to date относится к последнему известному tracking ref, не к серверу прямо сейчас.
- Force-with-lease может быть ослаблен явно заданным ожидаемым hash.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Прочитай ahead и behind
Что означает это состояние и какой обмен нужен, чтобы увидеть новые коммиты коллеги?
## feature...origin/feature [ahead 2, behind 1]
Ответ и разбор
Локальная feature содержит два коммита сверх tracking ref, но отстаёт от известного origin/feature на один commit. Сначала выполни fetch, чтобы обновить сведения о сервере, затем изучи расхождение и выбери merge или rebase. Push напрямую может быть отклонён как non-fast-forward.
Код-ревью
2. Не затирай чужое обновление
Job делает fetch и rebase, затем должен отправить переписанную ветку. После rebase коллега продвинул remote, а IDE запустила фоновый fetch до push. Почему обычный --force-with-lease может затереть новый commit? Зафиксируй ожидаемый OID до фонового fetch и покажи безопасную команду push.
git fetch origin
expected=$(git rev-parse refs/remotes/origin/feature)
git rebase origin/feature
# colleague pushes while the job is running; IDE fetches the new tip
git fetch origin
git push --force-with-lease origin feature
Ответ и разбор
После первого fetch сохрани ожидаемый OID в expected. Если коллега добавил commit после rebase, а фоновый fetch передвинул origin/feature, обычный lease возьмёт уже обновлённый tracking ref за ожидаемое значение и может разрешить push без commit коллеги. Укажи сохранённый OID явно: git push --force-with-lease=refs/heads/feature:$expected origin feature. Если remote уже изменился, push будет отклонён; затем fetch, проверь diff и перебазируй заново.
Применение
3. Настрой tracking branch
Локальная ветка feature создана вручную и не имеет upstream. Как связать её с удалённой веткой при первом push и чем это поможет дальше?
Ответ и разбор
Используй git push -u origin feature. Git запишет upstream для локальной ветки, после чего обычные status, pull и push смогут определить соответствующую remote branch. Перед первым push проверь, что это нужный remote и имя ветки.
Применение
4. Разбери «up to date» при чужом push
Коллега говорит, что запушил новый commit, но status всё ещё показывает up to date. Что произошло и в каком порядке обновить свою ветку?
Ответ и разбор
Локальный remote-tracking ref ещё не обновлялся и отражает прошлый fetch. Выполни git fetch origin, сравни новые refs, затем интегрируй нужную ветку через merge или rebase. Fetch сам не меняет working tree и текущую ветку.