diff --git a/git/repo/base.py b/git/repo/base.py index 6e582becf..32b99296a 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -35,6 +35,7 @@ ) from git.index import IndexFile from git.objects import Submodule, RootModule, Commit +from git.objects.util import utctz_to_altz from git.refs import HEAD, Head, Reference, SymbolicReference, TagReference from git.remote import Remote, _T_RemoteName, add_progress, to_progress_instance from git.util import ( @@ -1363,11 +1364,13 @@ def blame_incremental( safe_decode(props[b"author-mail"].lstrip(b"<").rstrip(b">")), ), authored_date=int(props[b"author-time"]), + author_tz_offset=utctz_to_altz(props[b"author-tz"].decode("ascii")), committer=Actor( safe_decode(props[b"committer"]), safe_decode(props[b"committer-mail"].lstrip(b"<").rstrip(b">")), ), committed_date=int(props[b"committer-time"]), + committer_tz_offset=utctz_to_altz(props[b"committer-tz"].decode("ascii")), ) commits[hexsha] = c else: @@ -1452,9 +1455,11 @@ class InfoTD(TypedDict, total=False): author: str author_email: str author_date: int + author_tz_offset: int committer: str committer_email: str committer_date: int + committer_tz_offset: int info: InfoTD = {} @@ -1504,13 +1509,15 @@ class InfoTD(TypedDict, total=False): # committer Tom Preston-Werner # committer-mail # committer-time 1192271832 - # committer-tz -0700 - IGNORED BY US + # committer-tz -0700 role = m.group(0) if role == "author": if firstpart.endswith("-mail"): info["author_email"] = parts[-1] elif firstpart.endswith("-time"): info["author_date"] = int(parts[-1]) + elif firstpart.endswith("-tz"): + info["author_tz_offset"] = utctz_to_altz(parts[-1]) elif role == firstpart: info["author"] = parts[-1] elif role == "committer": @@ -1518,6 +1525,8 @@ class InfoTD(TypedDict, total=False): info["committer_email"] = parts[-1] elif firstpart.endswith("-time"): info["committer_date"] = int(parts[-1]) + elif firstpart.endswith("-tz"): + info["committer_tz_offset"] = utctz_to_altz(parts[-1]) elif role == firstpart: info["committer"] = parts[-1] # END distinguish mail,time,name @@ -1540,8 +1549,10 @@ class InfoTD(TypedDict, total=False): hex_to_bin(sha), author=Actor._from_string(f"{info['author']} {info['author_email']}"), authored_date=info["author_date"], + author_tz_offset=info.get("author_tz_offset"), committer=Actor._from_string(f"{info['committer']} {info['committer_email']}"), committed_date=info["committer_date"], + committer_tz_offset=info.get("committer_tz_offset"), ) commits[sha] = c blames[-1][0] = c diff --git a/test/test_repo.py b/test/test_repo.py index 4641b31c9..4f1abcd8d 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -773,6 +773,30 @@ def test_blame_preserves_line_content(self): lines = [line for _commit, blamed in repo.blame("HEAD", "file.txt") for line in blamed] self.assertEqual(lines, ["alpha ", "beta", "\t", "crlf\r", "gamma"]) + def test_blame_preserves_timezones(self): + with fixture_repo("blame-content.sh") as repo: + for incremental in (False, True): + for rev in ("HEAD", None): + with self.subTest(incremental=incremental, rev=rev): + entry = next(iter(repo.blame(rev, "file.txt", incremental=incremental))) + commit = entry[0] + porcelain = repo.git.blame(rev, "--line-porcelain", "--", "file.txt") + offsets = dict( + line.split(" ", 1) + for line in porcelain.splitlines() + if line.startswith(("author-tz ", "committer-tz ")) + ) + with mock.patch.object( + Commit, "_set_cache_", side_effect=AssertionError("unexpected object read") + ): + if rev is None: + self.assertEqual(commit.hexsha, "0" * 40) + else: + self.assertEqual(commit.author_tz_offset, -19800) + self.assertEqual(commit.committer_tz_offset, 14400) + self.assertEqual(commit.authored_datetime.strftime("%z"), offsets["author-tz"]) + self.assertEqual(commit.committed_datetime.strftime("%z"), offsets["committer-tz"]) + @mock.patch.object(Git, "_call_process") def test_blame_complex_revision(self, git): git.return_value = fixture("blame_complex_revision") diff --git a/tests/fixtures/blame-content.sh b/tests/fixtures/blame-content.sh index 7ee38af9f..69e54b5c7 100644 --- a/tests/fixtures/blame-content.sh +++ b/tests/fixtures/blame-content.sh @@ -6,5 +6,9 @@ cd repo # CRLF line survive fixture creation on every platform. blob=$(printf 'alpha \nbeta\n\t\ncrlf\r\ngamma\n' | git hash-object -w --stdin) tree=$(printf '100644 blob %s\tfile.txt\0' "$blob" | git mktree -z) -commit=$(git -c user.name=A -c user.email=a@example.com commit-tree "$tree" -m content) +commit=$(GIT_AUTHOR_DATE='2020-01-01T12:00:00+0530' \ + GIT_COMMITTER_DATE='2020-01-01T13:00:00-0400' \ + git -c user.name=A -c user.email=a@example.com commit-tree "$tree" -m content) git update-ref refs/heads/main "$commit" +git reset --hard -q HEAD +printf 'changed\n' > file.txt