commit - f8437ed2129092cf41e89e26510c51576b271aa4
commit + f0daf70250f11e3a337bbb4f768b9fc6683d3a23
blob - 5bf2112bfdfb23fb71b880f3a18c79f389ebe232
blob + 7136bdbcf995cac7d0671d400dac3162cf53a18a
--- dulwich/porcelain.py
+++ dulwich/porcelain.py
class RemoteExists(Error):
"""Raised when the remote already exists."""
+
+
+class TimezoneFormatError(Error):
+ """Raised when the timezone cannot be determined from a given string."""
+
+
+def parse_timezone_format(tz_str):
+ """Parse given string and attempt to return a timezone offset.
+ Different formats are considered in the following order:
+ - Git internal format: <unix timestamp> <timezone offset>
+ - RFC 2822: e.g. Mon, 20 Nov 1995 19:12:08 -0500
+ - ISO 8601: e.g. 1995-11-20T19:12:08-0500
+ Args:
+ tz_str: datetime string
+ Returns: Timezone offset as integer
+ Raises:
+ TimezoneFormatError: if timezone information cannot be extracted
+ """
+ import re
+
+ # Git internal format
+ internal_format_pattern = re.compile("^[0-9]+ [+-][0-9]{,4}$")
+ if re.match(internal_format_pattern, tz_str):
+ try:
+ tz_internal = parse_timezone(tz_str.split(" ")[1].encode(DEFAULT_ENCODING))
+ return tz_internal[0]
+ except ValueError:
+ pass
+
+ # RFC 2822
+ import email.utils
+ rfc_2822 = email.utils.parsedate_tz(tz_str)
+ if rfc_2822:
+ return rfc_2822[9]
+
+ # ISO 8601
+
+ # Supported offsets:
+ # sHHMM, sHH:MM, sHH
+ iso_8601_pattern = re.compile("[0-9] ?([+-])([0-9]{2})(?::(?=[0-9]{2}))?([0-9]{2})?$")
+ match = re.search(iso_8601_pattern, tz_str)
+ total_secs = 0
+ if match:
+ sign, hours, minutes = match.groups()
+ total_secs += int(hours) * 3600
+ if minutes:
+ total_secs += int(minutes) * 60
+ total_secs = -total_secs if sign == "-" else total_secs
+ return total_secs
+
+ # YYYY.MM.DD, MM/DD/YYYY, DD.MM.YYYY contain no timezone information
+
+ raise TimezoneFormatError(tz_str)
+
+
+def get_user_timezones():
+ """Retrieve local timezone as described in
+ https://raw.githubusercontent.com/git/git/v2.3.0/Documentation/date-formats.txt
+ Returns: A tuple containing author timezone, committer timezone
+ """
+ local_timezone = time.localtime().tm_gmtoff
+
+ if os.environ.get("GIT_AUTHOR_DATE"):
+ author_timezone = parse_timezone_format(os.environ["GIT_AUTHOR_DATE"])
+ else:
+ author_timezone = local_timezone
+ if os.environ.get("GIT_COMMITTER_DATE"):
+ commit_timezone = parse_timezone_format(os.environ["GIT_COMMITTER_DATE"])
+ else:
+ commit_timezone = local_timezone
+ return author_timezone, commit_timezone
+
def open_repo(path_or_repo):
"""Open an argument that can be a repository or a path for a repository."""
if isinstance(path_or_repo, BaseRepo):
repo=".",
message=None,
author=None,
+ author_timezone=None,
committer=None,
+ commit_timezone=None,
encoding=None,
no_verify=False,
signoff=False,
repo: Path to repository
message: Optional commit message
author: Optional author name and email
+ author_timezone: Author timestamp timezone
committer: Optional committer name and email
+ commit_timezone: Commit timestamp timezone
no_verify: Skip pre-commit and commit-msg hooks
signoff: GPG Sign the commit (bool, defaults to False,
pass True to use default GPG key,
author = author.encode(encoding or DEFAULT_ENCODING)
if getattr(committer, "encode", None):
committer = committer.encode(encoding or DEFAULT_ENCODING)
+ local_timezone = get_user_timezones()
+ if author_timezone is None:
+ author_timezone = local_timezone[0]
+ if commit_timezone is None:
+ commit_timezone = local_timezone[1]
with open_repo_closing(repo) as r:
return r.do_commit(
message=message,
author=author,
+ author_timezone=author_timezone,
committer=committer,
+ commit_timezone=commit_timezone,
encoding=encoding,
no_verify=no_verify,
sign=signoff if isinstance(signoff, (str, bool)) else None,
tag_time = int(time.time())
tag_obj.tag_time = tag_time
if tag_timezone is None:
- # TODO(jelmer) Use current user timezone rather than UTC
- tag_timezone = 0
+ tag_timezone = get_user_timezones()[1]
elif isinstance(tag_timezone, str):
tag_timezone = parse_timezone(tag_timezone)
tag_obj.tag_timezone = tag_timezone
blob - 19420dd6c32c51f52bc16824822cf29326369fc0
blob + c19cf0ed1f69cc6e61b11467e2c5bfd9ca157737
--- dulwich/tests/test_porcelain.py
+++ dulwich/tests/test_porcelain.py
author="Joe <joe@example.com>",
committer="Bob <bob@example.com>",
no_verify=True,
+ )
+ self.assertIsInstance(sha, bytes)
+ self.assertEqual(len(sha), 40)
+
+ def test_timezone(self):
+ c1, c2, c3 = build_commit_graph(
+ self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+ )
+ self.repo.refs[b"refs/heads/foo"] = c3.id
+ sha = porcelain.commit(
+ self.repo.path,
+ message="Some message",
+ author="Joe <joe@example.com>",
+ author_timezone=18000,
+ committer="Bob <bob@example.com>",
+ commit_timezone=18000,
+ )
+ self.assertIsInstance(sha, bytes)
+ self.assertEqual(len(sha), 40)
+
+ commit = self.repo.get_object(sha)
+ self.assertEqual(commit._author_timezone, 18000)
+ self.assertEqual(commit._commit_timezone, 18000)
+
+ os.environ["GIT_AUTHOR_DATE"] = os.environ["GIT_COMMITTER_DATE"] = "1995-11-20T19:12:08-0501"
+
+ sha = porcelain.commit(
+ self.repo.path,
+ message="Some message",
+ author="Joe <joe@example.com>",
+ committer="Bob <bob@example.com>",
+ )
+ self.assertIsInstance(sha, bytes)
+ self.assertEqual(len(sha), 40)
+
+ commit = self.repo.get_object(sha)
+ self.assertEqual(commit._author_timezone, -18060)
+ self.assertEqual(commit._commit_timezone, -18060)
+
+ del os.environ["GIT_AUTHOR_DATE"]
+ del os.environ["GIT_COMMITTER_DATE"]
+ local_timezone = time.localtime().tm_gmtoff
+
+ sha = porcelain.commit(
+ self.repo.path,
+ message="Some message",
+ author="Joe <joe@example.com>",
+ committer="Bob <bob@example.com>",
)
self.assertIsInstance(sha, bytes)
self.assertEqual(len(sha), 40)
+ commit = self.repo.get_object(sha)
+ self.assertEqual(commit._author_timezone, local_timezone)
+ self.assertEqual(commit._commit_timezone, local_timezone)
+
@skipIf(platform.python_implementation() == "PyPy" or sys.platform == "win32", "gpgme not easily available or supported on Windows and PyPy")
class CommitSignTests(PorcelainGpgTestCase):
commit.verify()
+class TimezoneTests(PorcelainTestCase):
+
+ def put_envs(self, value):
+ os.environ["GIT_AUTHOR_DATE"] = os.environ["GIT_COMMITTER_DATE"] = value
+
+ def fallback(self, value):
+ self.put_envs(value)
+ self.assertRaises(porcelain.TimezoneFormatError, porcelain.get_user_timezones)
+
+ def test_internal_format(self):
+ self.put_envs("0 +0500")
+ self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
+
+ def test_rfc_2822(self):
+ self.put_envs("Mon, 20 Nov 1995 19:12:08 -0500")
+ self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
+
+ self.put_envs("Mon, 20 Nov 1995 19:12:08")
+ self.assertTupleEqual((0, 0), porcelain.get_user_timezones())
+
+ def test_iso8601(self):
+ self.put_envs("1995-11-20T19:12:08-0501")
+ self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
+
+ self.put_envs("1995-11-20T19:12:08+0501")
+ self.assertTupleEqual((18060, 18060), porcelain.get_user_timezones())
+
+ self.put_envs("1995-11-20T19:12:08-05:01")
+ self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
+
+ self.put_envs("1995-11-20 19:12:08-05")
+ self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
+
+ # https://github.com/git/git/blob/96b2d4fa927c5055adc5b1d08f10a5d7352e2989/t/t6300-for-each-ref.sh#L128
+ self.put_envs("2006-07-03 17:18:44 +0200")
+ self.assertTupleEqual((7200, 7200), porcelain.get_user_timezones())
+
+ def test_missing_or_malformed(self):
+ # TODO: add more here
+ self.fallback("0 + 0500")
+ self.fallback("a +0500")
+
+ self.fallback("1995-11-20T19:12:08")
+ self.fallback("1995-11-20T19:12:08-05:")
+
+ self.fallback("1995.11.20")
+ self.fallback("11/20/1995")
+ self.fallback("20.11.1995")
+
+ def test_different_envs(self):
+ os.environ["GIT_AUTHOR_DATE"] = "0 +0500"
+ os.environ["GIT_COMMITTER_DATE"] = "0 +0501"
+ self.assertTupleEqual((18000, 18060), porcelain.get_user_timezones())
+
+ def test_no_envs(self):
+ local_timezone = time.localtime().tm_gmtoff
+
+ self.put_envs("0 +0500")
+ self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
+
+ del os.environ["GIT_COMMITTER_DATE"]
+ self.assertTupleEqual((18000, local_timezone), porcelain.get_user_timezones())
+
+ self.put_envs("0 +0500")
+ del os.environ["GIT_AUTHOR_DATE"]
+ self.assertTupleEqual((local_timezone, 18000), porcelain.get_user_timezones())
+
+ self.put_envs("0 +0500")
+ del os.environ["GIT_AUTHOR_DATE"]
+ del os.environ["GIT_COMMITTER_DATE"]
+ self.assertTupleEqual((local_timezone, local_timezone), porcelain.get_user_timezones())
+
+
class CleanTests(PorcelainTestCase):
def put_files(self, tracked, ignored, untracked, empty_dirs):
"""Put the described files in the wd"""