Удалённые репозитории и протокол: refspec, fetch, push, транспорт
Одиннадцать статей трека мы работали внутри одного репозитория. Между тем половина ежедневных страданий с Git происходит ровно на границе: push отклонён с непонятной формулировкой, pull притащил чужой merge-коммит, ветка на сервере есть, а локально её «нет», после --force у коллеги пропала работа, а в CI клон висит четыре минуты.
Причина одна и та же: remote воспринимают как «сервер», как некую сущность с состоянием, куда «отправляют код». На самом деле никакого специального протокола общения репозитория с «центром» не существует, и центра тоже не существует. Есть две строчки в .git/config, есть грамматика отображения имён ссылок (refspec) и есть очень простой протокол «покажи, что у тебя есть — вот чего мне не хватает — на, забирай пачку объектов».
Эта статья выводит всю сетевую часть Git из этих трёх вещей. Она опирается на объектную модель и достижимость из «Внутри Git», на три пространства имён ссылок из обзора трека и на приёмы восстановления после чужого force-push из «Восстановления». Механику фильтров --filter=blob:none и shallow-клонов не повторяем — это «Большие репозитории».
Remote — это две строчки в конфиге
Начнём с фактов из файловой системы:
$ git remote add upstream https://github.com/acme/shop.git
$ git config --get-regexp '^remote\.|^branch\.'
remote.origin.url git@github.com:my-fork/shop.git
remote.origin.fetch +refs/heads/*:refs/remotes/origin/*
remote.upstream.url https://github.com/acme/shop.git
remote.upstream.fetch +refs/heads/*:refs/remotes/upstream/*
branch.main.remote origin
branch.main.merge refs/heads/main
Это весь «удалённый репозиторий»: имя, URL и правило отображения ссылок. Никакого состояния, никакой сессии, никакого «подключения». git remote add дописывает две строки, git remote remove их удаляет (плюс подчищает refs/remotes/<имя>/).
Отсюда сразу два следствия, снимающих типовые вопросы. Первое: «origin» — не ключевое слово, а просто имя по умолчанию, которое ставит git clone;ремоутов может быть сколько угодно, и они равноправны. Второе: branch.main.remote + branch.main.merge — это и есть «upstream-ветка», связь, из-за которой git status умеет писать «ahead 2, behind 1».
Refspec: грамматика, из которой выводится всё
Refspec выглядит так: [+]<src>:<dst>. Слева — имя ссылки на источнике, справа — куда её записать на приёмнике. Кто источник, а кто приёмник, зависит от направления: при fetch источник — сервер, при push — вы.
| Элемент | Значение |
|---|---|
+ |
разрешить обновление, не являющееся fast-forward (перезаписать) |
* |
подстановка: должна быть ровно один раз с каждой стороны |
пустой src (:refs/heads/tmp) |
при push — удалить ссылку на той стороне |
^refs/heads/wip/* |
негативный refspec: исключить из выборки (Git 2.29+) |
Стандартный fetch-refspec +refs/heads/*:refs/remotes/origin/* читается буквально: «все ветки сервера положить в моё локальное зеркало под refs/remotes/origin/, перезаписывая даже при расхождении». Префикс + тут не опасен: refs/remotes/* — не ваши ветки, это кэш чужого состояния, и он обязан отражать сервер как есть.
Практические применения, которые обычно ищут по StackOverflow:
# скачивать только одну ветку — экономия на репозитории с тысячей веток
git config remote.origin.fetch '+refs/heads/main:refs/remotes/origin/main'
# добавить второе правило: скачивать ещё и pull request'ы GitHub, только для чтения
git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin && git switch --detach origin/pr/812
# не тянуть служебные ветки автоматики
git config --add remote.origin.fetch '^refs/heads/dependabot/*'
# запушить локальную ветку под другим именем
git push origin feature/pricing:refs/heads/review/pricing
# удалить ветку на сервере: пустой src
git push origin :refs/heads/old-experiment
git push origin --delete old-experiment # то же самое человеческим языком
# отправить текущий HEAD, не создавая локальную ветку
git push origin HEAD:refs/heads/hotfix-2026-07-16
Почему git push origin main иногда «делает не то». Потому что это сокращение: если refspec не указан полностью, Git достраивает dst по правилам push.default.
Значение push.default |
Поведение |
|---|---|
simple (по умолчанию с Git 2.0) |
пушить текущую ветку в upstream-ветку того же имени; иначе отказ |
upstream |
пушить в настроенный upstream, даже если имя отличается |
current |
пушить в ветку того же имени на указанном remote; удобно для треугольных потоков |
matching |
пушить все одноимённые ветки — поведение Git 1.x, источник классических аварий |
nothing |
требовать явный refspec всегда |
Для форк-ориентированной работы (тяну из upstream, пушу в origin) есть отдельная настройка: remote.pushDefault = origin плюс push.default = current. После этого @{upstream} указывает на апстрим, а @{push} — на вашу ветку в форке, и git log @{push}..HEAD показывает «что ещё не отправлено».
Что fetch делает и чего он не делает
git fetch не трогает ни одну вашу ветку и ни один файл в рабочем каталоге. Он только скачивает объекты и обновляет refs/remotes/<remote>/* плюс пишет FETCH_HEAD. Это операция без побочных эффектов: её можно запускать когда угодно и сколько угодно, в том числе по таймеру.
$ git fetch origin
remote: Enumerating objects: 42, done.
remote: Counting objects: 100% (42/42), done.
remote: Compressing objects: 100% (18/18), done.
Unpacking objects: 100% (31/31), 12.42 KiB | 708.00 KiB/s, done.
From github.com:acme/shop
2d368f9..bc8d834 main -> origin/main
* [new branch] feature/tax -> origin/feature/tax
+ 9f3a2c1...4d1e8b7 release/2.4 -> origin/release/2.4 (forced update)
Три вида записей в выводе читаются по модели: .. — fast-forward, ветка сервера ушла вперёд; [new branch] — появилось новое имя; ... со знаком + — на сервере переписали историю, и ваше зеркало перезаписано принудительно (спасибо префиксу + в refspec). Последняя строка — единственное штатное уведомление о чужом force-push, и её стоит замечать.
Опасна не первая, а вторая половина git pull: это fetch плюс merge (или rebase при pull.rebase=true). Именно она создаёт неожиданные merge-коммиты и втягивает вас в конфликты в неподходящий момент. Разумный дефолт для команды:
git config --global pull.ff only # pull, который не создаёт коммитов молча
git config --global fetch.prune true # удалять зеркала веток, удалённых на сервере
git config --global fetch.pruneTags false
fetch.prune решает вечную проблему «в git branch -r сорок веток, которых на сервере давно нет»: без него удалённые на сервере ветки остаются в вашем refs/remotes/ навсегда. Разово то же самое делает git remote prune origin, а git remote show origin показывает список устаревших зеркал и то, какая ветка настроена как HEAD:
$ git remote show origin
* remote origin
Fetch URL: git@github.com:acme/shop.git
HEAD branch: main
Remote branches:
main tracked
feature/tax tracked
refs/remotes/origin/old stale (use 'git remote prune' to remove)
Local branch configured for 'git pull':
main merges with remote main
Теги приезжают отдельно. Refspec +refs/heads/*:... их не покрывает; Git отдельно «доследует» (auto-follow) аннотированные теги, указывающие внутрь скачанной истории. Управляется это --tags, --no-tags и remote.<имя>.tagOpt. В обратную сторону симметрии нет: git push теги не отправляет — нужен git push origin v2.4.0, git push --tags или, что аккуратнее всего, git push --follow-tags (отправит только аннотированные теги, достижимые из отправляемых коммитов). Классическая авария релиза «тег есть у меня локально, а в CI его нет» — ровно про это.
Что происходит на проводе
Транспортов несколько, но протокол один. Единица обмена — pkt-line: четыре шестнадцатеричных символа длины, затем данные. Длина считается вместе с префиксом, поэтому минимальный содержательный пакет — 0005 плюс один байт.
Пакеты 0000, 0001 и 0002 данных не несут и работают как знаки препинания: «конец секции», «граница возможностей и аргументов», «конец ответа».
Исторически было два поколения протокола. v0 начинал разговор с того, что сервер выкладывал весь список ссылок — на репозитории с сотней тысяч тегов и веток это мегабайты трафика ещё до того, как клиент сказал, что ему нужно. v2 (по умолчанию начиная с Git 2.26) сделал диалог командным: клиент запрашивает ls-refs с фильтром по префиксу и получает только нужное. Отсюда правило: если у вас «медленный git fetch» на большом репозитории, первым делом проверьте версию протокола.
$ git config --global protocol.version 2
$ GIT_TRACE_PACKET=1 git fetch origin 2>&1 | head -20
packet: upload-pack> version 2
packet: upload-pack> agent=git/2.43.0
packet: upload-pack> ls-refs=unborn
packet: upload-pack> fetch=shallow wait-for-done
packet: upload-pack> object-format=sha1
packet: upload-pack> 0000
packet: fetch> command=ls-refs
packet: fetch> agent=git/2.43.0
packet: fetch> 0001
packet: fetch> peel
packet: fetch> symrefs
packet: fetch> ref-prefix refs/heads/
packet: fetch> 0000
packet: upload-pack< bc8d834... refs/heads/main
Это не отладочная экзотика, а самый быстрый способ понять, что сломалось: видно, какие возможности объявила каждая сторона, где оборвался диалог и дошло ли дело до передачи pack-файла вообще.
Дальше начинается согласование (negotiation): клиент говорит, какие вершины ему нужны (want <oid>), сервер спрашивает, что у клиента уже есть, клиент перечисляет свои вершины (have <oid>), обе стороны сходятся на общей базе, и сервер шлёт pack только с недостающими объектами. Пакет при этом тонкий (thin pack): объекты в нём могут быть дельтами к объектам, которых в пакете нет, — предполагается, что у клиента они уже есть; на приёме git index-pack --fix-thin достраивает пакет до самодостаточного.
Для push роли зеркальны: на той стороне работает git-receive-pack, клиент шлёт тройки «старое значение — новое значение — имя ссылки» и pack с объектами, сервер проверяет права, запускает хуки pre-receive и update (см. «Хуки и автоматизацию») и отвечает построчно ok refs/heads/main или ng refs/heads/main non-fast-forward.
Транспорты
| Транспорт | Пример URL | Аутентификация | Когда выбирать |
|---|---|---|---|
| SSH | git@github.com:acme/shop.git |
ключи, ssh-agent | основной для людей: нет ввода пароля, работает за большинством прокси |
| Smart HTTPS | https://github.com/acme/shop.git |
токен/пароль через credential helper | CI, корпоративные сети, где открыт только 443 |
| Dumb HTTP | обычный веб-сервер над .git |
базовая HTTP | легаси; сервер ничего не считает, клиент тянет файлы как есть |
git:// |
git://host/repo.git |
нет никакой | не использовать: нет шифрования и подлинности, GitHub отключил его в 2022 |
| Локальный путь | /srv/git/shop.git |
права файловой системы | клон на одной машине; по умолчанию использует жёсткие ссылки на объекты |
file:// |
file:///srv/git/shop.git |
права файловой системы | тот же путь, но через полноценный протокол — полезно для воспроизведения багов |
Разница между /srv/git/repo и file:///srv/git/repo неочевидна и иногда важна: первый вариант — оптимизация, при которой Git просто делает жёсткие ссылки на файлы объектов (мгновенно, но клон «связан» с оригиналом на уровне inode; отключается --no-hardlinks), второй честно поднимает upload-pack и гоняет pkt-line.
Smart HTTPS — это два запроса: GET /info/refs?service=git-upload-pack для знакомства и POST /git-upload-pack для собственно обмена. Отсюда, кстати, растёт частая корпоративная проблема: прокси, который буферизует или обрезает POST-тело, ломает именно вторую фазу — клон «зависает» после «Enumerating objects».
Аутентификация: ключи, токены и где их не хранить
# SSH: проверить, что ключ вообще принимается — до всяких git-команд
$ ssh -T git@github.com
Hi ada! You've successfully authenticated, but GitHub does not provide shell access.
# отдельный ключ для конкретного репозитория, без правки ~/.ssh/config
$ git config core.sshCommand 'ssh -i ~/.ssh/id_deploy_shop -o IdentitiesOnly=yes'
# посмотреть, каким ключом Git реально ходит
$ GIT_SSH_COMMAND='ssh -v' git fetch origin 2>&1 | grep 'Offering\|Authenticated'
Для HTTPS пароли давно заменены токенами, и хранит их credential helper — отдельная программа, к которой Git обращается по простому текстовому протоколу.
| Helper | Где лежит секрет | Оценка |
|---|---|---|
store |
~/.git-credentials, открытым текстом |
только для одноразовых контейнеров |
cache |
в памяти демона, --timeout (по умолчанию 15 минут) |
приемлемо на личной машине |
osxkeychain, libsecret, wincred |
системное хранилище ОС | правильный выбор для рабочей станции |
| Git Credential Manager | системное хранилище + OAuth-поток | корпоративные среды с SSO |
git config --global credential.helper libsecret
git config --global credential.https://git.corp.example.com.username ada
В CI хранить нечего — токен приходит из секретов пайплайна, и задача только в том, чтобы он не утёк в логи:
# правильно: заголовок, значение подставляется из секрета окружения
git -c http.extraHeader="Authorization: Basic $(printf 'x-access-token:%s' "$TOKEN" | base64)" \
clone --filter=blob:none https://git.corp.example.com/acme/shop.git
# неправильно: токен окажется в URL, а URL — в логах, в .git/config и в выводе ошибок
git clone https://x-access-token:$TOKEN@git.corp.example.com/acme/shop.git
Отдельный полезный механизм — переписывание URL. Он позволяет держать в коде и подмодулях один канонический адрес, а ходить по нему разными способами:
# у себя работаем по SSH, хотя везде записан HTTPS
git config --global url."git@github.com:".insteadOf "https://github.com/"
# в CI наоборот: только HTTPS
git config --global url."https://github.com/".insteadOf "git@github.com:"
# читаем с зеркала, пишем в оригинал
git config --global url."https://mirror.corp/gh/".insteadOf "https://github.com/"
git config --global url."git@github.com:".pushInsteadOf "https://github.com/"
Это же — рабочий ответ на боль с подмодулями, у которых в .gitmodules зашит неудобный протокол (см. «Большие репозитории»). Про хранение секретов вообще — «Управление секретами».
Push отклонён: разбор по причинам
Сервер соблюдает один инвариант: ссылка двигается только вперёд по графу, то есть старое значение обязано быть предком нового. Всё остальное — либо отказ, либо явное принуждение.
в его текущую ветку"] T -- "src refspec does not match any" --> D["Такой локальной ветки нет:
опечатка или не создан коммит"] T -- "protected branch / required checks" --> E["Правило платформы, не Git"] A --> A1{"Ветку публиковали
другие люди?"} A1 -- "да" --> A2["git fetch + rebase/merge,
затем обычный push"] A1 -- "нет, ветка личная" --> A3["git push --force-with-lease
+ push.useForceIfIncludes"] B --> B1["git fetch, посмотреть чужие коммиты,
решить осознанно и повторить"] C --> C1["На сервере держать bare-репозиторий
или receive.denyCurrentBranch=updateInstead"] E --> E1["Читать настройки защиты ветки,
Git тут ни при чём"]
Реальные тексты, которые стоит уметь опознавать:
$ git push origin main
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:acme/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.
$ git push --force-with-lease origin main
! [rejected] main -> main (stale info)
Второй отказ — самый ценный в этом списке. --force-with-lease передаёт серверу ожидаемое старое значение ссылки: «перезапиши main, но только если он сейчас равен тому, что записано в моём refs/remotes/origin/main». Сервер сравнивает и отказывает, если кто-то успел запушить. Это ровно та проверка, которой нет у голого --force.
На такой картинке git push --force уничтожит «чужой B» без единого вопроса; --force-with-lease откажет, потому что реальное значение ссылки на сервере не совпадёт с ожидаемым.
Ловушка, из-за которой lease часто бесполезен. Гарантия строится на вашем зеркале refs/remotes/origin/main, а его обновляет любой git fetch — в том числе автоматический fetch, который ваша IDE делает раз в минуту в фоне. Тогда зеркало уже знает про «чужой B», проверка проходит, и коммит коллеги всё равно теряется. Лечится второй проверкой, появившейся в Git 2.30:
git config --global push.useForceIfIncludes true
--force-if-includes дополнительно требует, чтобы коммит, на который указывает зеркало, был достижим из вашего локального reflog ветки — то есть чтобы вы действительно видели чужую работу и построили свою поверх неё, а не просто скачали её фоном. Практический вывод: --force-with-lease + push.useForceIfIncludes — рабочий дефолт; голый --force не нужен почти никогда. Что делать, если force-push всё-таки затёр чужое, — в «Восстановлении».
Серверная сторона без хостинга
Полноценный Git-сервер — это каталог с bare-репозиторием и доступ по SSH. Никакого демона, базы данных и веб-интерфейса для работы Git не требуется.
# на сервере
$ sudo -u git git init --bare /srv/git/shop.git
$ sudo -u git git -C /srv/git/shop.git symbolic-ref HEAD refs/heads/main
$ sudo -u git git -C /srv/git/shop.git config receive.denyNonFastForwards true
$ sudo -u git git -C /srv/git/shop.git config receive.denyDeletes true
$ sudo -u git git -C /srv/git/shop.git config transfer.fsckObjects true
# на клиенте
$ git remote add origin git@server:/srv/git/shop.git
$ git push -u origin main
Ключевые настройки приёмной стороны:
| Настройка | Что делает |
|---|---|
receive.denyNonFastForwards |
запрещает любой force-push на уровне сервера |
receive.denyDeletes |
запрещает удаление веток и тегов |
receive.denyCurrentBranch |
по умолчанию refuse: нельзя пушить в ветку, которая на сервере выкачана в рабочий каталог; updateInstead разрешает и обновляет файлы |
transfer.fsckObjects |
проверять целостность и корректность приходящих объектов, а не верить на слово |
receive.maxInputSize |
ограничение на размер приходящего pack — защита от случайного гигабайта |
uploadpack.allowFilter |
разрешить partial clone (--filter=blob:none) клиентам |
receive.denyCurrentBranch объясняет знаменитую ошибку «refusing to update checked out branch». Причина логичная: Git обновил бы ссылку, но не файлы в рабочем каталоге сервера, и репозиторий оказался бы в состоянии «всё закоммичено и одновременно всё изменено». Правильный ответ — держать на сервере bare-репозиторий; вариант updateInstead существует для деплой-сценариев вроде «пушим прямо на прод-машину».
Два вида «полной копии» легко перепутать:
git clone --bare origin.git shop.git # без рабочего каталога, refspec НЕ настроен
git clone --mirror origin.git shop.git # bare + refspec +refs/*:refs/* + remote.origin.mirror
--mirror копирует всё пространство имён — ветки, теги, remote-tracking-ссылки, notes — и git remote update в нём приводит копию к состоянию оригинала один в один, включая удаления. Это правильный инструмент для миграции между хостингами и для резервной копии; --bare — для «пустого сервера, куда будут пушить».
Когда сети нет: bundle
git bundle упаковывает произвольный диапазон истории в один файл, который сам по себе является валидным remote. Его можно клонировать, из него можно тянуть — без сети вообще.
# полная копия всей истории одним файлом
$ git bundle create /media/usb/shop-full.bundle --all
# инкремент: только то, что появилось после известной обеим сторонам точки
$ git bundle create /media/usb/shop-delta.bundle v2.3.0..main
# на другой стороне: проверить, что бандл применим к тому репозиторию
$ git bundle verify /media/usb/shop-delta.bundle
The bundle contains this ref:
bc8d834... refs/heads/main
The bundle requires this ref:
2d368f9...
The bundle uses this hash algorithm: sha1
# использовать как обычный remote
$ git clone /media/usb/shop-full.bundle shop
$ git -C shop pull /media/usb/shop-delta.bundle main
$ git ls-remote /media/usb/shop-full.bundle
Три сценария, где это выигрывает у архива с .git: перенос через физически изолированный контур, где нет сети; «первичная заливка» огромного репозитория в CI (бандл лежит в объектном хранилище, воркер качает его одним потоком, потом добирает свежие коммиты обычным fetch); проверяемый холодный бэкап — git bundle verify честно скажет, что файл цел и применим, чего tar про .git не скажет никогда. Тот же механизм в промышленном виде — bundle URI (transfer.bundleURI), когда сервер отдаёт клиенту ссылку на заранее собранный бандл, чтобы разгрузить upload-pack.
Диагностика
Порядок действий при любой сетевой проблеме — от дешёвого к дорогому:
git ls-remote origin # 1. вижу ли я сервер и его ссылки вообще
ssh -T git@github.com # 2. проходит ли аутентификация (для SSH)
GIT_TRACE=1 git fetch origin # 3. какие процессы Git запускает и с чем
GIT_TRACE_PACKET=1 git fetch origin # 4. диалог протокола построчно
GIT_TRACE_CURL=1 git fetch origin # 5. HTTP-обмен (заголовки авторизации маскируются)
GIT_SSH_COMMAND='ssh -vvv' git fetch # 6. подробности SSH: какой ключ, какой хост
GIT_TRACE2_PERF=1 git fetch origin # 7. где именно уходит время
git ls-remote стоит запомнить отдельно: он не создаёт ничего локально и отвечает на вопрос «проблема в доступе или в моём репозитории» за одну секунду.
Пара распространённых заблуждений:
- «Увеличьте
http.postBufferдо 500 МБ». Совет кочует по форумам и почти всегда бесполезен. Буфер влияет лишь на то, использовать ли chunked transfer encoding; поднимать его осмысленно только против древних или несовместимых прокси, и он съедает соответствующий объём памяти. Настоящие причины «RPC failed» — лимиты прокси на размер тела, обрыв по таймауту и падение сервера на большом pack. - «
--depth=1ускорит клон в CI». Иногда да, но серверу shallow-клон обходится дороже обычного, а сборка теряетmerge-base,describeиbisect. Для CI почти всегда лучше--filter=blob:none— разбор компромиссов в «Больших репозиториях».
Типичные ошибки
- Считать
git push --forceнормальной командой. Он не задаёт вопросов и не проверяет ничего. Дефолт —--force-with-leaseвместе сpush.useForceIfIncludes. - Полагаться на
--force-with-leaseпри включённом фоновом fetch в IDE. Гарантия тихо перестаёт работать; см. разбор выше. - Не включать
fetch.prune. Через полгода локальный список удалённых веток не имеет отношения к реальности, аgit branch -rбесполезен. - Забывать, что теги не пушатся сами. Релиз собран, тег есть локально, в CI его нет — и пайплайн собирает не то. Лечится
--follow-tags. - Класть токен в URL remote. Он осядет в
.git/config, в логах CI и в тексте любой ошибки. Только credential helper илиhttp.extraHeader. - Пушить в не-bare репозиторий на сервере. Ошибка
denyCurrentBranch— не каприз, а защита от рассинхронизации ссылки и рабочего каталога. - Путать
--bareи--mirrorпри миграции.--bareне перенесёт все ссылки, и часть тегов и веток тихо потеряется. - Лечить сетевые симптомы
http.postBufferи--depth=1. СначалаGIT_TRACE_PACKET, потом выводы.
Мини-итог
- Remote — это имя, URL и refspec в
.git/config. Ни состояния, ни сессии, ни «главного» репозитория в Git не существует. - Refspec
[+]<src>:<dst>объясняет всё:+refs/heads/*:refs/remotes/origin/*— обычный fetch, пустойsrc— удаление ветки на сервере,^— исключение (Git 2.29+). fetchбезопасен всегда: он не трогает ваши ветки и файлы, толькоrefs/remotes/*иFETCH_HEAD. Опасна вторая половинаpull.- Строка вида
+ 9f3a2c1...4d1e8b7 release/2.4 (forced update)в выводе fetch — единственное штатное уведомление о чужом force-push. - Теги приезжают auto-follow’ом, но не уезжают сами: нужен
git push --follow-tags. - Протокол — это pkt-line: четыре hex-символа длины плюс данные,
0000/0001/0002как знаки препинания. v2 (по умолчанию с Git 2.26) не выкладывает все ссылки сразу и потому радикально быстрее на больших репозиториях. - Обмен состоит из знакомства (
ls-refs), согласованияwant/haveи передачи thin pack, который достраивается на приёме черезindex-pack --fix-thin. - Сервер двигает ссылку только вперёд по графу.
fetch first— у вас нет чужих коммитов;stale info— сработала защита lease. --force-with-leaseсверяет ожидаемое старое значение ссылки; фоновый fetch обесценивает эту проверку, поэтому нужен ещёpush.useForceIfIncludes(Git 2.30+).- Настоящий Git-сервер — это
git init --bareплюс SSH;receive.denyNonFastForwards,receive.denyDeletesиtransfer.fsckObjectsдают больше гарантий, чем любые договорённости. git bundle— самодостаточный однофайловый remote: перенос без сети, заливка кэша в CI и единственный проверяемый формат холодного бэкапа.- Диагностика идёт от
git ls-remoteкGIT_TRACE_PACKET, а не от советов с форумов проhttp.postBuffer.
Источники
- Pro Git, главы «Transfer Protocols» и «Git on the Server» — канонический разбор обмена и способов поднять сервер.
- Документация: git-fetch(1), git-push(1), git-remote(1), git-ls-remote(1), git-bundle(1), gitcredentials(7).
- Спецификации протокола: protocol-v2, pack-protocol, protocol-common — там же точное определение pkt-line.
- «Git Protocol v2 now generally available» — что именно ускорилось и почему.
- «Improving Git protocol security on GitHub» — отключение неаутентифицированного
git://и устаревших SSH-алгоритмов. - «Highlights from Git 2.30» — появление
--force-if-includesи разбор дыры в--force-with-lease. - Git Credential Manager — кроссплатформенный helper с поддержкой OAuth и корпоративного SSO.
- На портале: как устроен TLS под HTTPS-транспортом — «TLS»; хранение токенов — «Управление секретами»; клонирование в пайплайнах — «Основы CI».
Что дальше
Трек закончен. Мы прошли путь от «Git — это key-value хранилище с DAG поверх» до расследований по истории и разбора протокола на уровне пакетов, и на каждом шаге ответ находился в модели данных, а не в списке флагов. Если сейчас перечитать карту трека, большинство пунктов должно читаться как очевидные следствия, а не как набор команд для заучивания. Куда двигаться дальше — зависит от того, что стало узким местом:
- Красный trunk, долгие сборки, ручные деплои — DevOps: CI/CD, контейнеры, облака, особенно «CD и стратегии релиза».
- Ревью находит дефекты, которые должны были найти тесты — Тестирование и «Тесты в CI».
- Споры на ревью идут о структуре кода, а не о поведении — Принципы разработки и «Код-ревью и стандарты кода».
- Подпись коммитов и происхождение артефактов оказались важнее, чем казалось — Безопасность, в частности «Безопасность цепочки поставок».
- Проблема не в инструментах, а в потоке работы и метриках — Управление проектами и «DORA и инженерные метрики».
Общая карта портала с порядком изучения треков — Роадмап.