← 输入框|github.com|刷新
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/gix-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,7 @@ identify the historical fixture where needed.
| 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. |
| 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. |
| 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. |
| 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. |

The PR #2306 regression exposed an adapter ownership-check bypass in both
backends: `Repo`'s automatic `GIT_DIR` made Git 2.54.0 treat hook execution as
Expand Down
9 changes: 9 additions & 0 deletions git/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ def object_metadata(command: Any, oid: str, kind: str) -> Any:
"signature": data.signature,
"tagger": obj.tagger(),
}
signatures = (
((data.author, result["author"]), (data.committer, result["committer"]))
if kind == "commit"
else ((data.tagger, result["tagger"]),)
)
for raw, signature in signatures:
# Gix reads a time zone of 0 when other fields follow the date (GIX-34).
if signature is not None and not raw.endswith(b"> " + _date(signature)):
raise _Unsupported("signature trailing fields (GIX-34)")
except _Unsupported as exc:
return _fallback(kind + ".decode", str(exc))
except gix.Error as exc:
Expand Down
18 changes: 14 additions & 4 deletions git/objects/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
# Check the line ending once, before parsing fields, to avoid repeated backtracking.
# Keep the field name from consuming spaces belonging to the actor.
_re_actor_epoch = re.compile(r"^(?=[^\n]*$).[^ \n]* (.*) (\d+) ([+-]\d+)")
# Like Git's split_ident_line, take the date only from right after the last '>'.
_re_actor_mail = re.compile(r"^(?=[^\n]*$).[^ \n]* (.*>)(.*)$")
_re_epoch = re.compile(r"[ \t\r]*(\d+)[ \t\r]*([+-]\d+)")
_re_only_actor = re.compile(r"^.[^ \n]* (.*)$")


Expand All @@ -278,12 +281,19 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
[Actor, int_seconds_since_epoch, int_timezone_offset]
"""
actor, epoch, offset = "", "0", "0"
m = _re_actor_epoch.search(line)
m = _re_actor_mail.search(line)
if m:
actor, epoch, offset = m.groups()
actor, rest = m.groups()
m = _re_epoch.match(rest)
if m:
epoch, offset = m.groups()
else:
m = _re_only_actor.search(line)
actor = m.group(1) if m else line or ""
m = _re_actor_epoch.search(line)
if m:
actor, epoch, offset = m.groups()
else:
m = _re_only_actor.search(line)
actor = m.group(1) if m else line or ""
return (Actor._from_string(actor), int(epoch), utctz_to_altz(offset))


Expand Down
27 changes: 26 additions & 1 deletion test/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from gitdb import IStream

from git import Actor, Commit, Repo, _backend
from git import Actor, Commit, Repo, TagObject, _backend
from git.compat import safe_decode, safe_encode
from git.exc import UnsafeOptionError
from git.objects.util import tzoffset, utc
Expand Down Expand Up @@ -646,6 +646,31 @@ def test_deserialization_headers_end_at_empty_line(self):
key = ("commit.decode", "CLI: " + reason)
self.assertEqual(_backend.statistics().get(key, 0), before.get(key, 0) + 1)

def test_deserialization_reads_date_after_email(self):
"""Only the date right after the email counts, as in Git's split_ident_line."""
with fixture_repo("commit.sh", "trailing-date") as repo:
root = Path(repo.working_dir).parent
oid = (root / "object-id").read_text().strip()
tag_oid = (root / "tag-id").read_text().strip()
expected = repo.git.show("--no-patch", "--format=%ad%n%cd", "--date=raw", oid, "--")
self.assertEqual(expected, "1112591163 +0200\n1112591163 +0200")
self.assertTrue(repo.git.cat_file("-p", tag_oid).split("\n")[3].endswith(" +0200 1700000000 -0500"))
self.assertEqual(repo.git.for_each_ref("--format=%(taggerdate:raw)", "refs/tags/v1"), "1112591163 +0200")
raw = repo.git.cat_file("commit", oid, stdout_as_string=False, strip_newline_in_stdout=False)
before = _backend.statistics()
for cmt in (Commit(repo, bytes.fromhex(oid))._deserialize(BytesIO(raw)), Commit(repo, bytes.fromhex(oid))):
self.assertEqual((cmt.authored_date, cmt.author_tz_offset), (1112591163, -7200))
self.assertEqual((cmt.committed_date, cmt.committer_tz_offset), (1112591163, -7200))
self.assertEqual((cmt.author.email, cmt.committer.email), ("a@example.com", "c@example.com"))
tag = TagObject(repo, bytes.fromhex(tag_oid))
self.assertEqual(
(tag.tagged_date, tag.tagger_tz_offset, tag.tagger.email), (1112591163, -7200, "t@example.com")
)
if _backend.name == "gix":
for kind in ("commit", "tag"):
key = (kind + ".decode", "CLI: signature trailing fields (GIX-34)")
self.assertEqual(_backend.statistics().get(key, 0), before.get(key, 0) + 1)

def test_identity_cannot_alter_headers(self):
"""A name or email must not add header lines or present another identity."""
with fixture_repo("commit.sh", "tree", write=True) as rw_repo:
Expand Down
6 changes: 5 additions & 1 deletion test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,11 @@ def test_actor_from_string(self):
("committer Name <email> 42 +0530\n", Actor("Name", "email"), 42, -19800),
("tagger Name <email> 42 +0000\r\n", Actor("Name", "email"), 42, 0),
("author Name <email> 42 -0700 trailing", Actor("Name", "email"), 42, 25200),
("author Name <email> 1 +0 42 -0700", Actor("Name", "email"), 42, 25200),
("author Name <email> 1 +0 42 -0700", Actor("Name", "email"), 1, 0),
("author Name <email>42-0700", Actor("Name", "email"), 42, 25200),
("author Name <email> junk 42 -0700", Actor("Name", "email"), 0, 0),
("author Name <email> <x> 42 -0700", Actor("Name", "email"), 42, 25200),
("author Name <email> 42 -0700 <x>", Actor("Name", "email"), 0, 0),
("author Name <email> invalid -0700", Actor("Name", "email"), 0, 0),
("author Name <email> 42 invalid", Actor("Name", "email"), 0, 0),
("author 42 -0700", Actor("42 -0700", None), 0, 0),
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ case "$1" in
raw)
git hash-object --literally -w -t commit "$2" > ../object-id
;;
trailing-date)
tree=$(git mktree </dev/null)
{
printf 'tree %s\n' "$tree"
printf 'author A <a@example.com> 1112591163 +0200 1700000000 -0500\n'
printf 'committer C <c@example.com> 1112591163 +0200 1700000000 -0500\n'
printf '\nmessage\n'
} | git hash-object --literally -w -t commit --stdin > ../object-id
{
printf 'object %s\ntype commit\ntag v1\n' "$(cat ../object-id)"
printf 'tagger T <t@example.com> 1112591163 +0200 1700000000 -0500\n'
printf '\nmessage\n'
} | git hash-object --literally -w -t tag --stdin > ../tag-id
git update-ref refs/tags/v1 "$(cat ../tag-id)"
;;
whitespace-headers)
tree=$(git mktree </dev/null)
{
Expand Down
Loading