Компьютеры · Инструкция
git update-ref: безопасное обновление refs без ручного редактирования файлов
git update-ref нужен не большинству ежедневных пользователей Git, а скриптам и обслуживающим операциям, где ref должен.
Короткий ответ
git update-ref позволяет менять refs напрямую и проверять ожидаемый old OID. Это полезно для скриптов, где обычный git branch -f недостаточно атомарен.
Почему нельзя просто записать SHA в .git/refs/heads
Refs в Git могут храниться не только отдельными файлами под `.git/refs`, но и в других backend‑представлениях, включая packed refs. Кроме того, корректное обновление должно учитывать блокировки, reflog и конкурирующие процессы. Поэтому ручное `echo <sha> > .git/refs/heads/main` — хрупкий путь: он обходит штатные проверки и способен конфликтовать с параллельным Git‑процессом. `git update-ref` существует именно как низкоуровневая, но поддерживаемая команда для изменения refs. В простейшей форме вы передаёте имя ref и новый OID. В более безопасной форме добавляете old OID: Git проверяет текущее значение и отказывается обновлять ref, если оно уже изменилось. Это похоже на compare-and-swap и особенно важно в hooks, release tooling и maintenance scripts.
Совет: Используйте полные имена refs, например `refs/heads/release`, когда скрипт должен быть однозначным. Это уменьшает риск DWIM-разрешения не того имени.
Как передвинуть ветку только из ожидаемого состояния
- Получите текущее значение ref: `old=$(git rev-parse refs/heads/release)`. Не берите SHA из давно сохранённого файла, если между чтением и обновлением могли работать другие процессы.
- Получите новый объект и убедитесь, что он существует: `new=$(git rev-parse <commit>)` и при необходимости `git cat-file -e "$new^{commit}"`. Так скрипт не пытается указывать branch на случайную строку.
- Выполните `git update-ref -m "release: advance after validation" refs/heads/release "$new" "$old"`. Третий OID — ожидаемое прежнее значение. Если branch уже передвинул кто-то другой, команда завершится ошибкой вместо молчаливого перезаписывания.
- После успеха прочитайте ref заново и проверьте reflog: `git rev-parse refs/heads/release` и `git reflog show release -1`. Сообщение `-m` поможет понять происхождение автоматического движения ветки.
- Обрабатывайте ненулевой exit code как конфликт состояния, а не как повод повторить команду без old OID. Сначала перечитайте ref и заново решите, допустимо ли обновление при новом состоянии.
Важно: Главная ценность old OID — не проверка синтаксиса, а защита от гонки. Не убирайте его из скрипта только ради «чтобы проходило всегда».
Формы update-ref и уровень контроля
- git update-ref <ref> <new>. Обновляет ref на new. Не проверяет ожидаемое старое значение
- git update-ref <ref> <new> <old>. Обновляет только при совпадении old. Требует обработать конфликт
- git update-ref -d <ref> <old>. Удаляет ref с проверкой старого значения. Опасная операция, нужна строгая проверка имени
- git update-ref --stdin. Подходит для пакетных/транзакционных операций. Нужно точно соблюдать протокол входных команд
Как old OID предотвращает потерю чужого обновления
Представьте release‑бота и разработчика, которые почти одновременно двигают одну служебную ветку. Бот читает старый SHA A и после долгой проверки решает поставить C. Пока проверка идёт, разработчик обновляет ветку с A на B. Если бот затем выполнит update-ref только с C, он затрёт движение на B, хотя исходное условие уже изменилось. При передаче A как old OID команда заметит, что ref теперь указывает на B, и завершится ошибкой. Скрипт сможет перечитать состояние, сравнить историю и принять новое решение. Такой паттерн важнее скорости самой команды: он делает автоматизацию предсказуемой при конкурентных изменениях. Повторять update-ref после ошибки следует только после новой валидации, иначе compare-and-swap теряет смысл.
Предупреждение: Не превращайте конфликт old OID в бесконечный retry. Конфликт означает, что предпосылки операции изменились и их надо проверить заново.
Когда update-ref лучше branch -f, reset или symbolic-ref
`git branch -f name <commit>` удобнее человеку, когда надо просто передвинуть локальную ветку. `git reset` меняет текущую ветку и может затрагивать index/worktree в зависимости от режима, поэтому для служебного ref в скрипте это иной уровень действия. `git symbolic-ref` работает с символическими ссылками вроде HEAD, а не заменяет update-ref для обычной прямой branch ref. `git update-ref` выигрывает там, где нужен точный refname, old OID, reflog message и машинная обработка ошибки. Это низкоуровневый инструмент, поэтому его стоит оборачивать в проверки: разрешён ли ref, является ли new нужным типом объекта, допустимо ли перемещение относительно текущей истории. Команда не знает бизнес‑правил вашего release process и не запретит логически неправильный, но технически валидный ref.
Минимальная защита для скрипта с update-ref
- Refname строится из доверенного источника и проверен через `git check-ref-format`.
- New OID разрешён через `git rev-parse` и существует.
- Old OID прочитан непосредственно перед критической фазой.
- Трёхаргументная форма используется при конкурентном доступе.
- Exit code проверяется, ошибка не игнорируется `|| true`.
- Reflog message через `-m` объясняет автоматическое изменение.
Что делать, если ref нужно обновить пакетно
Если операция затрагивает несколько refs, последовательность отдельных команд может оставить промежуточное состояние: первый ref обновился, второй — нет. Для таких задач документация `git update-ref` предусматривает режим `--stdin`, где можно описывать операции и использовать пакетные механизмы команды. Прежде чем переносить туда production‑скрипт, протестируйте сценарии отказа на копии репозитория и используйте формат, поддерживаемый установленной версией Git. Не подменяйте транзакционность shell‑цепочкой с ручным откатом, если от целостности нескольких refs зависит публикация релиза. И отдельно помните о remote: `update-ref` меняет refs локального репозитория; серверные refs изменятся только через соответствующий push или серверную операцию. В режиме `--stdin` полезно передавать ожидаемые старые OID и для каждой операции, а не только перечислять новые значения: тогда пакетная операция сохраняет ту же защиту от неожиданного изменения refs. Современная документация также описывает команды `start`, `prepare`, `commit` и `abort` для явного управления транзакцией при работе через stdin. Это особенно полезно, когда скрипт должен либо согласованно изменить несколько refs, либо не оставить частично применённый набор. Если установленная версия Git не поддерживает нужный вариант протокола, скрипт должен обнаружить это заранее, а не молча переходить к небезопасной последовательности отдельных записей.
Что учитывать
Refs в Git могут храниться не только отдельными файлами под `.git/refs`, но и в других backend‑представлениях, включая packed refs. Кроме того, корректное обновление должно учитывать блокировки, reflog и конкурирующие процессы. Поэтому ручное `echo <sha> > .git/refs/heads/main` — хрупкий путь: он обходит штатные проверки и способен конфликтовать с параллельным Git‑процессом. `git update-ref` существует именно как низкоуровневая, но поддерживаемая команда для изменения refs. В простейшей форме…
Источники и проверка
Фактическая часть сверена по первичным источникам (в т.ч. git-update-ref Documentation). Пример и формулировки — редакция N1RO на 2026-09-21.