Skip to content

Commit 00ae9a2

Browse files
authored
Merge pull request #2331 from Keerthana-64/identity-date-after-email
fix(objects): email ke baad identity dates ko git ki tarah correctly read karo
2 parents 7d5dcd2 + 40ffb3e commit 00ae9a2

7 files changed

Lines changed: 115 additions & 6 deletions

File tree

‎AGENTS.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,20 @@ Commit messages must stand on their own. Put the information needed to understan
9898
and review the change in the commit body, even when it also appears in a pull
9999
request description. PR and issue links may provide additional context, but must
100100
not substitute for that explanation.
101+
102+
## Agent trailers
103+
104+
Every commit created by an agent must include an `Agent-Model` trailer identifying
105+
the model and version reported by its session or runtime. Include an
106+
`Agent-Reasoning-Effort` trailer when the configured reasoning effort is available.
107+
108+
For example:
109+
110+
```text
111+
Agent-Model: gpt-6.1-sol
112+
Agent-Reasoning-Effort: high
113+
```
114+
115+
Do not guess these values or infer them from global defaults. If the model version
116+
is unavailable, use `Agent-Model: unknown`; omit the reasoning-effort trailer when
117+
its value is unavailable.

‎doc/gix-backend.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,7 @@ identify the historical fixture where needed.
966966
| GIX-31 | Bug (open) | GixPython 0.2.0 `Commit.decode()` rejects commit headers containing a CR-only or tab-only line with `object parsing failed, at gix-object/src/commit/decode.rs:20`. Git 2.54.0 accepts both objects with `fsck --strict`, continues through their header block and renders the later Latin-1 declaration and message correctly. A space-only continuation line decodes natively but retains the late encoding declaration in `extra_headers` (GIX-30). `test_deserialization_headers_end_at_empty_line` reproduces all three using the `commit.sh whitespace-headers` fixtures and checks Git's projection. | Accept Git-compatible header lines and end headers only at an empty line. The existing `object_metadata()` native decoder diagnostics fallback retains Git for CR/tab inputs; the existing GIX-30 fallback covers the space input. Neither fallback closes these bugs. |
967967
| GIX-32 | Missing capability (open); no demonstrated Git compatibility mismatch | Windows CI on GixPython 0.3.0, Git 2.56.0.windows.2 and CPython 3.12.10 enumerates the non-UTF-8 packed names in `legacy-refs.sh`, but exact byte-name lookup selects `Reference.read: CLI: native read diagnostics`; the no-CLI guard fails for SHA-1 and SHA-256. POSIX lookup succeeds. Git's Windows CLI likewise cannot address those original bytes through UTF-16 arguments, so there is no demonstrated equivalent Windows CLI operand. Git enumeration does preserve both names. | Provide native Windows packed-reference lookup for byte names without first converting the name to a loose-reference filesystem path. Keep `reference_info()`'s existing diagnostic fallback; `test_non_utf8_tag_names_preserve_bytes` checks it on Windows. Resolution coverage and native claims remain POSIX-only. The upstream lookup source attempts loose-path conversion before consulting packed refs; CI demonstrates the fallback, but does not capture the underlying Gix error text. |
968968
| GIX-33 | Bug (open); missing exact-name mode | With Git 2.54.0 and GixPython 0.3.0, `exact-refs.sh sha1 files symbolic` makes `refs/tags/REBASE_HEAD` a symbolic ref to another tag while the root `REBASE_HEAD` is absent. `Repository.try_find_reference("REBASE_HEAD")` returns that tag; native `_symbolic_ref` formerly returned its target, whereas `git symbolic-ref --quiet --no-recurse -- REBASE_HEAD` exits 1. GitPython's root-reference object read consequently returned a commit through Gix and raised `ValueError` through CLI. SHA-256 reproduces the same mismatch. | Expose an exact-name lookup mode, including root references, whose absence and error behavior matches Git. `_symbolic_ref()` compares the native reference's name with the requested bytes and selects CLI for a lookalike. `test_missing_root_reference_does_not_follow_symbolic_lookalike_tag` verifies parity and forced CLI fallback. This adapter guard does not close the upstream compatibility bug. |
969+
| GIX-34 | Bug (open) | With Git 2.54.0 and GixPython 0.3.0, an author, committer or tagger line with fields after its time zone, such as `A <a@example.com> 1112591163 +0200 1700000000 -0500`, decodes through `Commit.author()`/`committer()` and `Tag.tagger()` with the correct seconds but an offset of 0. Git reads only the first date and time zone after the last `>` (`split_ident_line`) and reports `1112591163 +0200` through `git show --format=%ad --date=raw` and `%(taggerdate:raw)`. `git fsck` reports `badTimezone` for such objects, but the default `transfer.fsckObjects=false` accepts them. The `trailing-date` read-only `commit.sh` fixture in `test_deserialization_reads_date_after_email` reproduces both object kinds. | Report the time zone Git reads for identity lines with trailing fields, or reject them consistently. `object_metadata()` selects CLI when the decoded signature does not reproduce the end of its raw header line. This adapter guard does not close the upstream compatibility bug. |
969970

970971
The PR #2306 regression exposed an adapter ownership-check bypass in both
971972
backends: `Repo`'s automatic `GIT_DIR` made Git 2.54.0 treat hook execution as

‎git/_backend.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ def object_metadata(command: Any, oid: str, kind: str) -> Any:
239239
"signature": data.signature,
240240
"tagger": obj.tagger(),
241241
}
242+
signatures = (
243+
((data.author, result["author"]), (data.committer, result["committer"]))
244+
if kind == "commit"
245+
else ((data.tagger, result["tagger"]),)
246+
)
247+
for raw, signature in signatures:
248+
# Gix reads a time zone of 0 with trailing fields (GIX-34); accept either UTC sign.
249+
if signature is not None and not raw.replace(b" -0000", b" +0000").endswith(b"> " + _date(signature)):
250+
raise _Unsupported("signature trailing fields (GIX-34)")
242251
except _Unsupported as exc:
243252
return _fallback(kind + ".decode", str(exc))
244253
except gix.Error as exc:

‎git/objects/util.py‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
266266
# Check the line ending once, before parsing fields, to avoid repeated backtracking.
267267
# Keep the field name from consuming spaces belonging to the actor.
268268
_re_actor_epoch = re.compile(r"^(?=[^\n]*$).[^ \n]* (.*) (\d+) ([+-]\d+)")
269+
# Like Git's split_ident_line, take the date only from right after the last '>'.
270+
_re_actor_mail = re.compile(r"^(?=[^\n]*$).[^ \n]* (.*>)(.*)$")
271+
_re_epoch = re.compile(r"[ \t\r]*(\d+)[ \t\r]*([+-]\d+)")
269272
_re_only_actor = re.compile(r"^.[^ \n]* (.*)$")
270273

271274

@@ -278,12 +281,19 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
278281
[Actor, int_seconds_since_epoch, int_timezone_offset]
279282
"""
280283
actor, epoch, offset = "", "0", "0"
281-
m = _re_actor_epoch.search(line)
284+
m = _re_actor_mail.search(line)
282285
if m:
283-
actor, epoch, offset = m.groups()
286+
actor, rest = m.groups()
287+
m = _re_epoch.match(rest)
288+
if m:
289+
epoch, offset = m.groups()
284290
else:
285-
m = _re_only_actor.search(line)
286-
actor = m.group(1) if m else line or ""
291+
m = _re_actor_epoch.search(line)
292+
if m:
293+
actor, epoch, offset = m.groups()
294+
else:
295+
m = _re_only_actor.search(line)
296+
actor = m.group(1) if m else line or ""
287297
return (Actor._from_string(actor), int(epoch), utctz_to_altz(offset))
288298

289299

‎test/test_commit.py‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from gitdb import IStream
1616

17-
from git import Actor, Commit, Repo, _backend
17+
from git import Actor, Commit, Repo, TagObject, _backend
1818
from git.compat import safe_decode, safe_encode
1919
from git.exc import UnsafeOptionError
2020
from git.objects.util import tzoffset, utc
@@ -646,6 +646,57 @@ def test_deserialization_headers_end_at_empty_line(self):
646646
key = ("commit.decode", "CLI: " + reason)
647647
self.assertEqual(_backend.statistics().get(key, 0), before.get(key, 0) + 1)
648648

649+
def test_deserialization_reads_date_after_email(self):
650+
"""Only the date right after the email counts, as in Git's split_ident_line."""
651+
with fixture_repo("commit.sh", "trailing-date") as repo:
652+
root = Path(repo.working_dir).parent
653+
oid = (root / "object-id").read_text().strip()
654+
tag_oid = (root / "tag-id").read_text().strip()
655+
expected = repo.git.show("--no-patch", "--format=%ad%n%cd", "--date=raw", oid, "--")
656+
self.assertEqual(expected, "1112591163 +0200\n1112591163 +0200")
657+
self.assertTrue(repo.git.cat_file("-p", tag_oid).split("\n")[3].endswith(" +0200 1700000000 -0500"))
658+
self.assertEqual(repo.git.for_each_ref("--format=%(taggerdate:raw)", "refs/tags/v1"), "1112591163 +0200")
659+
raw = repo.git.cat_file("commit", oid, stdout_as_string=False, strip_newline_in_stdout=False)
660+
before = _backend.statistics()
661+
for cmt in (Commit(repo, bytes.fromhex(oid))._deserialize(BytesIO(raw)), Commit(repo, bytes.fromhex(oid))):
662+
self.assertEqual((cmt.authored_date, cmt.author_tz_offset), (1112591163, -7200))
663+
self.assertEqual((cmt.committed_date, cmt.committer_tz_offset), (1112591163, -7200))
664+
self.assertEqual((cmt.author.email, cmt.committer.email), ("a@example.com", "c@example.com"))
665+
tag = TagObject(repo, bytes.fromhex(tag_oid))
666+
self.assertEqual(
667+
(tag.tagged_date, tag.tagger_tz_offset, tag.tagger.email), (1112591163, -7200, "t@example.com")
668+
)
669+
if _backend.name == "gix":
670+
for kind in ("commit", "tag"):
671+
key = (kind + ".decode", "CLI: signature trailing fields (GIX-34)")
672+
self.assertEqual(_backend.statistics().get(key, 0), before.get(key, 0) + 1)
673+
674+
def test_deserialization_negative_zero_offset_stays_native(self):
675+
with fixture_repo("commit.sh", "zero-offset") as repo:
676+
root = Path(repo.working_dir).parent
677+
oid = (root / "object-id").read_text().strip()
678+
tag_oid = (root / "tag-id").read_text().strip()
679+
repo.git.fsck("--strict", oid, tag_oid)
680+
self.assertTrue(repo.git.cat_file("-p", tag_oid).split("\n")[3].endswith(" -0000"))
681+
self.assertEqual(
682+
repo.git.show("--no-patch", "--format=%ad%n%cd", "--date=raw", oid, "--"),
683+
"1112591163 +0000\n1112591163 +0000",
684+
)
685+
self.assertEqual(repo.git.for_each_ref("--format=%(taggerdate:raw)", "refs/tags/v1"), "1112591163 +0000")
686+
before = _backend.statistics()
687+
cmt = Commit(repo, bytes.fromhex(oid))
688+
self.assertEqual((cmt.authored_date, cmt.author_tz_offset), (1112591163, 0))
689+
self.assertEqual((cmt.committed_date, cmt.committer_tz_offset), (1112591163, 0))
690+
tag = TagObject(repo, bytes.fromhex(tag_oid))
691+
self.assertEqual((tag.tagged_date, tag.tagger_tz_offset), (1112591163, 0))
692+
if _backend.name == "gix":
693+
after = _backend.statistics()
694+
for kind in ("commit", "tag"):
695+
key = (kind + ".decode", "native")
696+
self.assertEqual(after.get(key, 0), before.get(key, 0) + 1)
697+
key = ("Git.execute", "CLI process")
698+
self.assertEqual(after.get(key, 0), before.get(key, 0))
699+
649700
def test_identity_cannot_alter_headers(self):
650701
"""A name or email must not add header lines or present another identity."""
651702
with fixture_repo("commit.sh", "tree", write=True) as rw_repo:

‎test/test_util.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,11 @@ def test_actor_from_string(self):
646646
("committer Name <email> 42 +0530\n", Actor("Name", "email"), 42, -19800),
647647
("tagger Name <email> 42 +0000\r\n", Actor("Name", "email"), 42, 0),
648648
("author Name <email> 42 -0700 trailing", Actor("Name", "email"), 42, 25200),
649-
("author Name <email> 1 +0 42 -0700", Actor("Name", "email"), 42, 25200),
649+
("author Name <email> 1 +0 42 -0700", Actor("Name", "email"), 1, 0),
650+
("author Name <email>42-0700", Actor("Name", "email"), 42, 25200),
651+
("author Name <email> junk 42 -0700", Actor("Name", "email"), 0, 0),
652+
("author Name <email> <x> 42 -0700", Actor("Name", "email"), 42, 25200),
653+
("author Name <email> 42 -0700 <x>", Actor("Name", "email"), 0, 0),
650654
("author Name <email> invalid -0700", Actor("Name", "email"), 0, 0),
651655
("author Name <email> 42 invalid", Actor("Name", "email"), 0, 0),
652656
("author 42 -0700", Actor("42 -0700", None), 0, 0),

‎tests/fixtures/commit.sh‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ case "$1" in
4747
raw)
4848
git hash-object --literally -w -t commit "$2" > ../object-id
4949
;;
50+
trailing-date|zero-offset)
51+
date='1112591163 +0200 1700000000 -0500'
52+
if [ "$1" = zero-offset ]; then date='1112591163 -0000'; fi
53+
tree=$(git mktree </dev/null)
54+
{
55+
printf 'tree %s\n' "$tree"
56+
printf 'author A <a@example.com> %s\n' "$date"
57+
printf 'committer C <c@example.com> %s\n' "$date"
58+
printf '\nmessage\n'
59+
} | git hash-object --literally -w -t commit --stdin > ../object-id
60+
{
61+
printf 'object %s\ntype commit\ntag v1\n' "$(cat ../object-id)"
62+
printf 'tagger T <t@example.com> %s\n' "$date"
63+
printf '\nmessage\n'
64+
} | git hash-object --literally -w -t tag --stdin > ../tag-id
65+
git update-ref refs/tags/v1 "$(cat ../tag-id)"
66+
;;
5067
whitespace-headers)
5168
tree=$(git mktree </dev/null)
5269
{

0 commit comments

Comments
 (0)