feat(pre-push-safety-net): resolve immutable push comparisons

This commit is contained in:
Marius Ciepluch
2026-09-12 20:43:51 +02:00
parent 14a2a60e52
commit 05da202d67
8 changed files with 421 additions and 11 deletions
@@ -142,3 +142,18 @@ The content review checked boundary ownership, failure handling, vendor exclusio
and independent finding/coverage/push status. Draft defaults remain clearly labeled. Runtime
integration, performance and capability admission remain unverified work; no code tests were
needed for these documentation-only amendments.
### Group 2 implementation: immutable Git reads
The local Git runtime is 2.43.0. Snapshot reads must suppress replacement objects
and implicit promisor fetching, including on Git versions that predate newer
lazy-fetch controls. The protocol allow-list is the stronger boundary: an empty
`GIT_ALLOW_PROTOCOL` forbids transports even when repository configuration permits
an individual protocol. Git documents this precedence in its
[environment-variable reference](https://git-scm.com/docs/git#Documentation/git.txt-GITALLOWPROTOCOL).
Regression fixtures must prove supplied-object identity and lack of helper execution;
setting environment variables alone is not evidence of either property.
The snapshot adapter provides change evidence, not a second region selector. Source
materialization, changed spans, and correspondence must remain language agnostic;
unknown parser or correspondence boundaries cannot become a successful negative.
+9 -3
View File
@@ -1,7 +1,7 @@
{
"feature_name": "pre-push-safety-net",
"created_at": "2026-09-12T15:35:00Z",
"updated_at": "2026-09-12T17:21:05.783007Z",
"updated_at": "2026-09-12T18:32:19.033933Z",
"language": "en",
"phase": "implementation",
"approvals": {
@@ -19,7 +19,7 @@
}
},
"ready_for_implementation": true,
"implementation_approval_note": "2026-09-12: maintainer explicitly said 'approve.' in response to approval of the generated task plan and starting implementation with group 1. All tasks are approved; this implementation run starts with tasks 1.1\u20131.4. No task is complete until its fresh-evidence and reviewer gates pass.",
"implementation_approval_note": "Groups 1 and 2 explicitly authorized by the maintainer. Group 1 verified complete; this run implements group 2 (2.1\u20132.3). Remaining task plan retains approval, with execution sequenced by the spec.",
"depends_on": [
"contributor-scan"
],
@@ -40,6 +40,11 @@
"at": "2026-09-12T16:20:40.814596Z",
"source": "approve.",
"action": "Accepted the generated task plan and authorized starting group 1 (1.1\u20131.4). Implementation started; tasks require independent review and fresh verification before completion."
},
{
"at": "2026-09-12T18:32:19.033933Z",
"source": "ok, next kiro phase, approved; correct. we can continue with the spec",
"action": "Authorized group 2 (2.1\u20132.3), resumed after local Claude Mem repair. Independent review and fresh verification remain required."
}
],
"task_review": {
@@ -60,6 +65,7 @@
],
"remaining_tasks": 23,
"verification_record": "implementation-group-1.md",
"next_group": 2
"next_group": 2,
"active_group": 2
}
}
+2 -1
View File
@@ -56,12 +56,13 @@ the hook and capability admission are not yet implemented. See `implementation-g
- _Verified 2026-09-12:_ independent review APPROVED; 965 tests passed, 9 skipped; touched-source Ruff/mypy passed. Parent rebuilt the local image and verified non-root, network-disabled CLI startup, all contract imports, an 83-byte configuration round trip and invalid-budget rejection. Comparison-owned completion preserves different bases for a shared head. Contracts are structural; runtime snapshot/cache/runner/admission implementations remain in later tasks.
- [ ] 2. Resolve immutable pushed content
- [ ] 2.1 Resolve every supplied ref update and comparison
- [x] 2.1 Resolve every supplied ref update and comparison
- Parse Git's complete update input using object-format-aware validation; resolve existing tips, new-branch configured bases, force-pushes, merges, peeled tags, deleted refs and unsupported objects.
- Deduplicate equal targets without merging different base comparisons; retain every ref association. Do not silently substitute HEAD or fetch missing objects.
- Done when multi-ref and missing-base fixtures produce exact comparison identities and explicit dispositions, including deletion-only no-analysis results.
- _Boundary: Snapshot Adapter_
- _Requirements: 1.1, 1.3, 1.4, 7.1_
- _Verified 2026-09-12:_ independent review APPROVED after Unicode-ref repair; 997 tests passed, 9 skipped; focused 76 passed and touched Ruff/mypy passed. Real SHA-1/SHA-256 fixtures cover exact comparisons, tags, deletion, missing/ambiguous bases, replacement/graft suppression and blocked promisor fetch. Parent read 2,547 pinned source bytes and verified real-repository comparison/ref identity with dirty state and index preserved. ASCII protocol framing retains valid Unicode ref names.
- [ ] 2.2 Materialize isolated object snapshots
- Read immutable tracked blobs into bounded scratch storage using machine-readable paths; do not checkout into the live tree or run filters, project scripts or network requests.
- Preserve path identity and report symlinks, gitlinks, unavailable LFS blobs and unsupported content; never follow source outside the snapshot.
@@ -1,10 +1,10 @@
{
"counts": {
"load_bearing": 97,
"load_bearing": 98,
"standalone_capability": 9
},
"modules": 106,
"note": "Refreshed 2026-09-12 after pre-push-safety-net task 1.4: immutable push and generic engine contracts are spec-owned; runtime integration remains pending.",
"modules": 107,
"note": "Refreshed 2026-09-12 after pre-push-safety-net task 2.1: local immutable ref resolution is spec-owned; runner integration remains pending.",
"orphaned": [],
"rows": [
{
@@ -633,16 +633,26 @@
},
{
"classification": "load_bearing",
"importers": [],
"importers": [
"push.snapshot"
],
"module": "push",
"reason": "pre-push-safety-net Req 5.5/7.3: experimental push contract package; no hook installed"
},
{
"classification": "load_bearing",
"importers": [],
"importers": [
"push.snapshot"
],
"module": "push.contracts",
"reason": "pre-push-safety-net Req 5.5/7.3: immutable comparisons and independent result statuses; runner pending"
},
{
"classification": "load_bearing",
"importers": [],
"module": "push.snapshot",
"reason": "pre-push-safety-net Req 1.1/1.3/1.4/7.1: local immutable ref resolution; runner integration pending"
},
{
"classification": "standalone_capability",
"importers": [],
@@ -1125,6 +1135,5 @@
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path",
"slot_contract": "the slot/contract protocol stage_processors implements",
"stage_processors": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate"
},
"updated_at": "2026-09-12"
}
}
+1
View File
@@ -28,6 +28,7 @@ ROOTS = ("gate", "map_gate", "pair_gate", "cli", "__main__")
# Modules that stand alone: their own CLI subcommand or external protocol, deliberately off the scan path.
SPEC_OWNED = {
"push.snapshot": "pre-push-safety-net Req 1.1/1.3/1.4/7.1: local immutable ref resolution; runner integration pending",
"contracts": "pre-push-safety-net Req 7.3: strict shared manifest serialization, independent of push",
"model.contracts": "pre-push-safety-net Req 4.1/7.3: generic deadline, change and exact scope contracts; integration pending",
"cpg.artifact": "pre-push-safety-net Req 4.3/7.3: backend graph identity and live lease contract; validation integration pending",
+20
View File
@@ -84,3 +84,23 @@ class PushResult(Contract):
raise ValueError("aggregate complete coverage requires completed comparison analyses")
if self.coverage_status == "not_applicable" and (self.analyses or self.actionable_defect_ids):
raise ValueError("not applicable cannot carry analyzed scope or findings")
@dataclass(frozen=True)
class UpdateResolution(Contract):
"""One record per supplied line, including repeated ref associations."""
update: PushUpdate
disposition: Literal["ready", "deleted", "missing_target", "unsupported_target", "missing_base", "unsupported_base",
"no_merge_base", "ambiguous_base", "resolution_error"]
head_oid: str | None
base_oid: str | None
base_reason: str
@dataclass(frozen=True)
class PushResolution(Contract):
object_format: str
updates: tuple[UpdateResolution, ...]
comparisons: tuple[PushComparison, ...]
targets: tuple[str, ...]
+151
View File
@@ -0,0 +1,151 @@
"""Resolve supplied push tips using local Git objects, never the live checkout.
No fetch, replacement objects, checkout, filters or project commands are used.
Materialization and changed spans are separate subsequent snapshot-adapter tasks.
"""
from __future__ import annotations
import os
import re
import subprocess
from dataclasses import replace
from pathlib import Path
from openultrasast.push.contracts import PushComparison, PushResolution, PushUpdate, UpdateResolution
class SnapshotInputError(ValueError):
"""Malformed protocol input or an unavailable repository, never an empty success."""
class SnapshotAdapter:
def __init__(self, repository: Path, *, comparison_base: str | None = None) -> None:
self.repository = repository.resolve()
self.comparison_base = comparison_base
# Hook-inherited Git variables may redirect objects or worktrees elsewhere.
self._env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
self._env.update(GIT_NO_REPLACE_OBJECTS="1", GIT_NO_LAZY_FETCH="1", GIT_TERMINAL_PROMPT="0",
GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, GIT_ALLOW_PROTOCOL="", GIT_OPTIONAL_LOCKS="0",
GIT_GRAFT_FILE=os.devnull)
self.object_format = self._required("rev-parse", "--show-object-format=storage")
# Ask the installed Git for its object ID width rather than assuming SHA-1.
self._oid_width = len(self._required("hash-object", "--stdin", input=""))
self._zero = "0" * self._oid_width
def _git(self, *args: str, input: str | None = None) -> subprocess.CompletedProcess[str]:
try:
return subprocess.run(
["git", "-c", "protocol.allow=never", "-c", "core.hooksPath=" + os.devnull,
"-C", str(self.repository), *args],
input=input, capture_output=True, text=True, encoding="utf-8", errors="surrogateescape",
env=self._env, timeout=10, check=False,
)
except (OSError, subprocess.TimeoutExpired) as error:
raise SnapshotInputError(f"local Git resolution failed: {type(error).__name__}") from error
def _required(self, *args: str, input: str | None = None) -> str:
result = self._git(*args, input=input)
if result.returncode:
raise SnapshotInputError(f"local Git {args[0]} failed (exit {result.returncode})")
return result.stdout.strip()
def parse_updates(self, data: str) -> tuple[PushUpdate, ...]:
"""Validate every record before attempting resolution; malformed input fails closed."""
updates = []
# Git frames records with ASCII LF and fields with ASCII SP. Unicode
# whitespace is legal inside refs and must never be stripped or split.
records = data.split("\n") if data else []
if records and records[-1] == "":
records.pop()
for number, record in enumerate(records, 1):
parts = record.split(" ")
if len(parts) != 4 or any(not part for part in parts) or any(ord(char) < 32 for char in record):
raise SnapshotInputError(f"invalid pre-push update at line {number}")
local_ref, local_oid, remote_ref, remote_oid = parts
if any(re.fullmatch(r"[0-9a-f]{" + str(self._oid_width) + r"}", oid) is None
for oid in (local_oid, remote_oid)):
raise SnapshotInputError(f"invalid {self.object_format} object ID at line {number}")
if self._git("check-ref-format", remote_ref).returncode or not remote_ref.startswith("refs/"):
raise SnapshotInputError(f"invalid destination ref at line {number}")
if (local_ref == "(delete)") != (local_oid == self._zero) or (local_oid == remote_oid == self._zero):
raise SnapshotInputError(f"invalid deletion update at line {number}")
updates.append(PushUpdate(*parts))
return tuple(updates)
def _commit(self, oid: str) -> tuple[str | None, str]:
seen: set[str] = set()
while oid not in seen:
seen.add(oid)
kind = self._git("cat-file", "-t", oid)
if kind.returncode:
return None, "missing"
object_type = kind.stdout.strip()
if object_type not in ("commit", "tag"):
return None, "unsupported"
content = self._git("cat-file", object_type, oid)
if content.returncode:
return None, "missing"
if object_type == "commit":
return oid, "commit"
first = content.stdout.splitlines()[0] if content.stdout else ""
if re.fullmatch(r"object [0-9a-f]{" + str(self._oid_width) + r"}", first) is None:
return None, "unsupported"
oid = first.removeprefix("object ")
return None, "unsupported"
def _resolve(self, update: PushUpdate) -> UpdateResolution:
if update.local_oid == self._zero:
return UpdateResolution(update, "deleted", None, None, "ref_deleted_no_analysis")
head, kind = self._commit(update.local_oid)
if head is None:
return UpdateResolution(update, "missing_target" if kind == "missing" else "unsupported_target",
None, None, "target_not_a_local_commit")
if update.remote_oid != self._zero:
base, kind = self._commit(update.remote_oid)
disposition = "ready" if base else ("missing_base" if kind == "missing" else "unsupported_base")
return UpdateResolution(update, disposition, head, base, "supplied_remote_tip") # type: ignore[arg-type]
reason = "configured_unique_merge_base"
if not self.comparison_base:
return UpdateResolution(update, "missing_base", head, None, "new_ref_no_configured_base")
configured = self._git("rev-parse", "--verify", "--end-of-options", self.comparison_base)
if configured.returncode:
return UpdateResolution(update, "missing_base", head, None, "configured_base_unavailable")
anchor, kind = self._commit(configured.stdout.strip())
if anchor is None:
return UpdateResolution(update, "missing_base" if kind == "missing" else "unsupported_base",
head, None, "configured_base_not_a_local_commit")
reason += ":" + anchor
merged = self._git("merge-base", "--all", anchor, head)
if merged.returncode not in (0, 1):
return UpdateResolution(update, "resolution_error", head, None, reason)
bases = merged.stdout.splitlines()
if len(bases) != 1:
return UpdateResolution(update, "ambiguous_base" if bases else "no_merge_base", head, None, reason)
base, kind = self._commit(bases[0])
return UpdateResolution(update, "ready" if base else "missing_base", head, base, reason)
def resolve_updates(self, data: str) -> PushResolution:
updates = self.parse_updates(data)
records: list[UpdateResolution] = []
comparisons: list[PushComparison] = []
targets: list[str] = []
for update in updates:
try:
record = self._resolve(update)
except SnapshotInputError:
record = UpdateResolution(update, "resolution_error", None, None, "local_git_resolution_failed")
records.append(record)
if record.head_oid is None:
continue
if record.head_oid not in targets:
targets.append(record.head_oid)
# Choice provenance is part of comparison identity, not just the target graph.
key = (record.head_oid, record.base_oid, record.base_reason)
for index, comparison in enumerate(comparisons):
if (comparison.head_oid, comparison.base_oid, comparison.base_reason) == key:
if update.remote_ref not in comparison.refs:
comparisons[index] = replace(comparison, refs=(*comparison.refs, update.remote_ref))
break
else:
comparisons.append(PushComparison(*key, refs=(update.remote_ref,)))
return PushResolution(self.object_format, tuple(records), tuple(comparisons), tuple(targets))
+207
View File
@@ -0,0 +1,207 @@
"""Exercise the snapshot resolver with real local Git objects, not mocked answers."""
import subprocess
from pathlib import Path
import pytest
from openultrasast.push.snapshot import SnapshotAdapter
def git(repo: Path, *args: str, input: str | None = None) -> str:
result = subprocess.run(["git", "-C", str(repo), *args], input=input, text=True, capture_output=True, check=True)
return result.stdout.strip()
@pytest.fixture(params=["sha1", "sha256"])
def repo(tmp_path: Path, request: pytest.FixtureRequest) -> Path:
git(tmp_path, "init", "--object-format=" + request.param)
git(tmp_path, "config", "user.name", "Fixture")
git(tmp_path, "config", "user.email", "[email protected]")
return tmp_path
def commit(repo: Path, label: str, *parents: str) -> str:
blob = git(repo, "hash-object", "-w", "--stdin", input=label)
assert git(repo, "cat-file", "-s", blob) == str(len(label))
tree = git(repo, "mktree", input=f"100644 blob {blob}\tfile.js\n")
args = ["commit-tree", tree]
for parent in parents:
args.extend(["-p", parent])
return git(repo, *args, input=label)
def line(head: str, base: str, name: str = "main") -> str:
return f"refs/heads/{name} {head} refs/heads/{name} {base}\n"
def test_preserves_every_ref_and_distinct_comparisons(repo: Path) -> None:
base = commit(repo, "base")
other = commit(repo, "other", base)
head = commit(repo, "head", base)
result = SnapshotAdapter(repo).resolve_updates(line(head, base) + line(head, other, "force") + line(head, base, "alias"))
assert result.targets == (head,)
assert [(c.head_oid, c.base_oid, c.refs) for c in result.comparisons] == [
(head, base, ("refs/heads/main", "refs/heads/alias")), (head, other, ("refs/heads/force",))]
assert [r.disposition for r in result.updates] == ["ready"] * 3
assert len(result.updates) == 3
def test_configured_new_branch_uses_unique_merge_base(repo: Path) -> None:
base = commit(repo, "base")
left, right = commit(repo, "left", base), commit(repo, "right", base)
git(repo, "update-ref", "refs/heads/baseline", right)
merge = commit(repo, "merge", left, right)
result = SnapshotAdapter(repo, comparison_base="refs/heads/baseline").resolve_updates(line(merge, "0" * len(base)))
assert result.comparisons[0].base_oid == right
assert result.comparisons[0].head_oid == merge
assert result.updates[0].base_reason == "configured_unique_merge_base:" + right
assert result.updates[0].disposition == "ready"
def test_no_implicit_head_or_fetch_for_missing_base(repo: Path) -> None:
head = commit(repo, "head")
git(repo, "update-ref", "HEAD", head)
for configured in (None, "refs/remotes/origin/absent"):
result = SnapshotAdapter(repo, comparison_base=configured).resolve_updates(line(head, "0" * len(head)))
assert result.updates[0].disposition == "missing_base"
assert result.comparisons[0].base_oid is None
result = SnapshotAdapter(repo).resolve_updates(line(head, "1" * len(head)))
assert result.updates[0].disposition == "missing_base"
assert result.comparisons[0].base_reason == "supplied_remote_tip"
def test_unrelated_and_ambiguous_new_branch_bases(repo: Path) -> None:
root = commit(repo, "root")
unrelated = commit(repo, "unrelated")
result = SnapshotAdapter(repo, comparison_base=unrelated).resolve_updates(line(root, "0" * len(root)))
assert result.updates[0].disposition == "no_merge_base"
left, right = commit(repo, "left", root), commit(repo, "right", root)
one, two = commit(repo, "one", left, right), commit(repo, "two", right, left)
result = SnapshotAdapter(repo, comparison_base=one).resolve_updates(line(two, "0" * len(root)))
assert result.updates[0].disposition == "ambiguous_base"
assert result.comparisons[0].base_oid is None
def test_tags_peel_to_commit_and_retain_input_identity(repo: Path) -> None:
base, head = commit(repo, "base"), commit(repo, "head")
git(repo, "tag", "-a", "old", base, "-m", "old")
git(repo, "tag", "-a", "new", head, "-m", "new")
old_tag, new_tag = git(repo, "rev-parse", "old"), git(repo, "rev-parse", "new")
data = f"refs/tags/new {new_tag} refs/tags/new {old_tag}\n" + line(head, base)
result = SnapshotAdapter(repo).resolve_updates(data)
assert result.targets == (head,)
assert result.comparisons[0].base_oid == base
assert result.comparisons[0].refs == ("refs/tags/new", "refs/heads/main")
assert result.updates[0].update.local_oid == new_tag
assert result.updates[0].update.remote_oid == old_tag
assert type(result).from_payload(result.to_payload()) == result
def test_deletion_only_has_no_analysis(repo: Path) -> None:
old = commit(repo, "old")
result = SnapshotAdapter(repo).resolve_updates(f"(delete) {'0' * len(old)} refs/heads/old {old}\n")
assert result.targets == result.comparisons == ()
assert result.updates[0].disposition == "deleted"
assert SnapshotAdapter(repo).resolve_updates("").updates == ()
def test_missing_and_unsupported_objects_are_explicit(repo: Path) -> None:
head = commit(repo, "head")
blob = git(repo, "hash-object", "-w", "--stdin", input="unsupported source")
git(repo, "tag", "-a", "blob-tag", blob, "-m", "unsupported")
tag = git(repo, "rev-parse", "blob-tag")
result = SnapshotAdapter(repo).resolve_updates(line("1" * len(head), head) + line(blob, head, "blob") + line(tag, head, "tag"))
assert [r.disposition for r in result.updates] == ["missing_target", "unsupported_target", "unsupported_target"]
assert result.targets == result.comparisons == ()
result = SnapshotAdapter(repo).resolve_updates(line(head, blob))
assert result.updates[0].disposition == "unsupported_base"
def test_bad_protocol_never_silently_skips_lines(repo: Path) -> None:
from openultrasast.push.snapshot import SnapshotInputError
head = commit(repo, "head")
for invalid in ("\n", "only three fields\n", line(head[:-1], head), line(head, head, "bad..ref"),
line("0" * len(head), head), f"(delete) {head} refs/heads/main {head}\n"):
with pytest.raises(SnapshotInputError):
SnapshotAdapter(repo).resolve_updates(line(head, head) + invalid)
def test_replacements_and_inherited_git_environment_do_not_change_identity(repo: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = commit(repo, "root")
original = commit(repo, "original", root)
replacement = commit(repo, "replacement")
git(repo, "replace", original, replacement)
monkeypatch.setenv("GIT_DIR", str(repo / "nonexistent"))
monkeypatch.setenv("GIT_OBJECT_DIRECTORY", str(repo / "nonexistent"))
result = SnapshotAdapter(repo, comparison_base=root).resolve_updates(line(original, "0" * len(root)))
assert result.updates[0].disposition == "ready"
assert result.comparisons[0].base_oid == root
assert result.comparisons[0].head_oid == original
def test_missing_promisor_object_cannot_fetch_even_when_repo_allows_file(repo: Path, tmp_path: Path) -> None:
source = repo / "source"
source.mkdir()
fmt = git(repo, "rev-parse", "--show-object-format")
git(source, "init", "--object-format=" + fmt)
git(source, "config", "user.name", "Fixture")
git(source, "config", "user.email", "[email protected]")
missing = commit(source, "must not fetch")
git(source, "update-ref", "refs/heads/main", missing)
git(repo, "config", "remote.origin.url", str(source))
git(repo, "config", "remote.origin.promisor", "true")
git(repo, "config", "remote.origin.partialclonefilter", "blob:none")
git(repo, "config", "protocol.file.allow", "always")
before = sorted(p.relative_to(repo / ".git/objects") for p in (repo / ".git/objects").rglob("*") if p.is_file())
result = SnapshotAdapter(repo).resolve_updates(line(missing, "0" * len(missing)))
after = sorted(p.relative_to(repo / ".git/objects") for p in (repo / ".git/objects").rglob("*") if p.is_file())
assert result.updates[0].disposition == "missing_target"
assert before == after
def test_missing_tag_referent_is_missing_not_unsupported(repo: Path) -> None:
head = commit(repo, "head")
git(repo, "tag", "-a", "dangling", head, "-m", "dangling")
tag = git(repo, "rev-parse", "dangling")
(repo / ".git" / "objects" / head[:2] / head[2:]).unlink()
result = SnapshotAdapter(repo).resolve_updates(f"refs/tags/dangling {tag} refs/tags/dangling {'0' * len(head)}\n")
assert result.updates[0].disposition == "missing_target"
def test_legacy_grafts_cannot_rewrite_comparison_ancestry(repo: Path) -> None:
root = commit(repo, "root")
head = commit(repo, "head", root)
(repo / ".git/info/grafts").write_text(head + "\n")
result = SnapshotAdapter(repo, comparison_base=root).resolve_updates(line(head, "0" * len(head)))
assert result.comparisons[0].base_oid == root
def test_dirty_non_head_and_duplicate_update_records_are_preserved(repo: Path) -> None:
base = commit(repo, "base")
pushed = commit(repo, "pushed", base)
checked = commit(repo, "checked", base)
git(repo, "update-ref", "HEAD", checked)
(repo / "file.js").write_text("dirty file")
git(repo, "add", "file.js")
before_index = (repo / ".git/index").read_bytes()
result = SnapshotAdapter(repo).resolve_updates(line(pushed, base) * 2)
assert result.targets == (pushed,)
assert len(result.updates) == 2
assert len(result.comparisons) == 1
assert (repo / ".git/index").read_bytes() == before_index
assert (repo / "file.js").read_text() == "dirty file"
assert git(repo, "rev-parse", "HEAD") == checked
@pytest.mark.parametrize("name", ["unicode\u00a0inside", "unicode-trailing\u00a0", "unicode\u2028separator"])
def test_git_valid_unicode_ref_names_are_preserved(repo: Path, name: str) -> None:
base = commit(repo, "base")
head = commit(repo, "head", base)
ref = "refs/heads/" + name
git(repo, "check-ref-format", ref)
git(repo, "update-ref", ref, head)
result = SnapshotAdapter(repo).resolve_updates(line(head, base, name))
assert result.updates[0].update.local_ref == ref
assert result.updates[0].update.remote_ref == ref
assert result.comparisons[0].refs == (ref,)
assert result.updates[0].disposition == "ready"