Skip to content

Commit 7c77a2f

Browse files
codexByron
authored andcommitted
fix(tag): address missing tagger review feedback
Review item #2274 (comment) reports that "reading them raises `AttributeError`" when native decoding has no tagger, and requests preserving initialized fields through a CLI fallback. Four SHA-1/SHA-256 regressions reproduce the native failure. The two cases with a message also expose CLI loss of the first message line, so falling back alone would not restore complete metadata. Adapt `Tag.tagger()` returning `None` to the existing empty `Actor` and zero date/offset defaults through `parse_actor_and_date("")`. Gix already exposes the absence correctly; no new Git semantics or fallback is required. Make the CLI parser recognize the optional tagger header and preserve the entire message. Git 2.54.0's `tag.c::parse_tag_buffer()` accepts an absent tagger and sets its date to zero. The existing GIX-28 strict writer gap remains open. All four regressions now pass on both backends; the native cases forbid `Git.execute()` during metadata access. Reference and CLI object/index tests pass on CLI (105 tests, 24 subtests) and Gix (105 tests, 24 subtests). Ruff, ShellCheck and mypy pass.
1 parent 85874b3 commit 7c77a2f

4 files changed

Lines changed: 40 additions & 18 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ these editing fallbacks.
130130

131131
Commit metadata uses `Repository.find_commit().decode()` plus structured
132132
`Commit.author()`/`committer()` signatures. Tag metadata uses
133-
`Repository.find_tag().decode()` and `Tag.tagger()`. Decoded extra headers retain
133+
`Repository.find_tag().decode()` and `Tag.tagger()`. A missing native tagger maps
134+
to GitPython's empty actor and zero date/offset; tag reads remain native.
135+
Decoded extra headers retain
134136
`gpgsig`. Raw Gix object bytes are used only for object streams. If repository
135137
capabilities require CLI fallback, metadata uses `git cat-file`: general formatted
136138
Git queries cannot faithfully expose all signature headers or arbitrary tags.

‎git/objects/tag.py‎

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def _set_cache_(self, attr: str) -> None:
113113
self.object = object_type(self.repo, bytes.fromhex(data["target"]))
114114
self.tag = data["name"].decode(defenc, "replace")
115115
signature = data["tagger"]
116-
if signature is not None:
116+
if signature is None:
117+
self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date("")
118+
else:
117119
self.tagger = Actor(
118120
signature.name.decode(defenc, "replace"), signature.email.decode(defenc, "replace")
119121
)
@@ -144,21 +146,10 @@ def _set_cache_(self, attr: str) -> None:
144146

145147
self.tag = lines[2][4:] # tag <tag name>
146148

147-
if len(lines) > 3:
148-
tagger_info = lines[3] # tagger <actor> <date>
149-
(
150-
self.tagger,
151-
self.tagged_date,
152-
self.tagger_tz_offset,
153-
) = parse_actor_and_date(tagger_info)
154-
155-
# Line 4 empty - it could mark the beginning of the next header.
156-
# In case there really is no message, it would not exist.
157-
# Otherwise a newline separates header from message.
158-
if len(lines) > 5:
159-
self.message = "\n".join(lines[5:])
160-
else:
161-
self.message = ""
149+
has_tagger = len(lines) > 3 and lines[3].startswith("tagger ")
150+
self.tagger, self.tagged_date, self.tagger_tz_offset = parse_actor_and_date(lines[3] if has_tagger else "")
151+
message_start = 5 if has_tagger else 4
152+
self.message = "\n".join(lines[message_start:])
162153
# END check our attributes
163154
else:
164155
super()._set_cache_(attr)

‎test/test_refs.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ def test_non_utf8_tag_names_resolve(object_format):
7171
assert repo.commit(tag.path) == repo.head.commit
7272

7373

74+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
75+
@pytest.mark.parametrize("message", ["", "release\nmore"])
76+
def test_tag_without_tagger_initializes_all_metadata(object_format, message):
77+
with fixture_repo("refs.sh", "tag-no-tagger", object_format, "message" if message else "empty") as repo:
78+
oid = (Path(repo.working_tree_dir).parent / "tag-id").read_text().strip()
79+
expected = repo.head.commit
80+
tag = TagObject(repo, bytes.fromhex(oid))
81+
native_guard = (
82+
patch.object(Git, "execute", side_effect=AssertionError("unexpected tag metadata CLI call"))
83+
if _backend.name == "gix"
84+
else contextlib.nullcontext()
85+
)
86+
with native_guard:
87+
assert tag.object == expected
88+
assert tag.tag == "no-tagger"
89+
assert tag.tagger == Actor("", None)
90+
assert tag.tagged_date == 0
91+
assert tag.tagger_tz_offset == 0
92+
assert tag.message == message
93+
94+
7495
class TestRefs(TestBase):
7596
def test_from_path(self):
7697
# Should be able to create any reference directly.

‎tests/fixtures/refs.sh‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
22
set -eu
3-
git init --initial-branch=master repo
3+
git init --initial-branch=master --object-format="${2:-sha1}" repo
44
cd repo
55
printf 'initial\n' > file.txt
66
git add file.txt
@@ -22,6 +22,14 @@ case "$1" in
2222
printf 'tagger Mallory <m@evil.example> 1700000001 +0000\n\nrelease\n'
2323
} | git hash-object -w -t tag --literally --stdin >../tag-id
2424
;;
25+
tag-no-tagger)
26+
{
27+
printf 'object %s\ntype commit\ntag no-tagger\n\n' "$(git rev-parse HEAD)"
28+
if test "$3" = message; then
29+
printf 'release\nmore\n'
30+
fi
31+
} | git hash-object -w -t tag --literally --stdin >../tag-id
32+
;;
2533
outside)
2634
git rev-parse HEAD > ../outside-ref
2735
;;

0 commit comments

Comments
 (0)