ref:8de061c4ca174f71cee22b04293470bc64ce4f6c

feat: ReceivePack supports atomic push

Advertises `atomic` alongside `report-status` and `delete-refs`. When the client indicates atomic in its capabilities list, ReceivePack validates every ref-update command before touching storage; if any command fails validation (bad ref name, stale old_sha, non-ff, update_hook rejection), the whole batch is refused with `ng` per ref and no ref moves. Two-phase flow: 1. Validate all commands. Per-command validation checks: * Ref.validate_ref_name/1 * old_sha state (must be zero for create, current for update, present for delete) * update_hook's return value 2. Commit all. Snapshot every target ref's current value, then apply commands sequentially. If any apply fails mid-way, restore every touched ref from its snapshot (and delete refs that didn't exist pre-flight). Note on transactionality: ref storage backends (filesystem, S3) are not multi-key atomic. A rollback can itself fail, leaving refs in a partially-applied state. This implementation is as atomic as the storage layer allows — the doc comment on do_atomic_ref_updates/1 calls this out explicitly. Test rewritten to actually exercise the atomic path: a pre-registered update_hook rejects any update to refs/heads/rejected. The client pushes `main:refs/heads/accepted` and `main:refs/heads/rejected` in a single --atomic push; the test asserts BOTH refs are absent on the server afterwards (not just that the push exit code is non-zero). Non-atomic behaviour is unchanged: per-ref failures still produce per-ref `ng` entries without affecting other refs.
SHA: 8de061c4ca174f71cee22b04293470bc64ce4f6c
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-04-19 00:51
Parents: dfd72ff
4 files changed +184 -56
Type
lib/ex_git_objectstore/pack/filter.ex +4 −6
@@ -195,12 +195,10 @@
defp pattern_match?(pattern, path) do
pattern = String.trim_leading(pattern, "/")
cond do
String.ends_with?(pattern, "/") ->
String.starts_with?(path, pattern) or String.starts_with?(path <> "/", pattern)
true ->
path == pattern or String.starts_with?(path, pattern <> "/")
if String.ends_with?(pattern, "/") do
String.starts_with?(path, pattern) or String.starts_with?(path <> "/", pattern)
else
path == pattern or String.starts_with?(path, pattern <> "/")
end
end
end