diff --git a/.pylintrc b/.pylintrc index d45a50a..9cff3b3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -294,7 +294,7 @@ indent-string=' ' max-line-length=120 # Maximum number of lines in a module. -max-module-lines=1000 +max-module-lines=2000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. diff --git a/Changelog.md b/Changelog.md index afcf1a4..0cfa847 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ _WIP_ * Make detection of comment style based on suffix case insensitive * Fix handling of files with unicode BOM * Allow selection of latest year vs range of years +* Fix handling of author years when amending a commit v2.6.2 ------ diff --git a/license_tools/__init__.py b/license_tools/__init__.py index e0a7d68..ccc10c3 100644 --- a/license_tools/__init__.py +++ b/license_tools/__init__.py @@ -62,6 +62,46 @@ def current_year(): DateUtils._current_year = int(year) return DateUtils._current_year + @staticmethod + def parse_git_date(git_date: str) -> datetime.datetime: + """ + Returns a datetime parsed from a git date string + as documented at https://git-scm.com/docs/git-commit/2.24.0#_date_formats + """ + # Git internal format + try: + if git_date.startswith('@'): + unix_timestamp, tz_offset = git_date[1:].split(' ', maxsplit=1) + unix_timestamp = int(unix_timestamp) + timezone = datetime.timezone(datetime.datetime.strptime(tz_offset, '%z').utcoffset()) + return datetime.datetime.fromtimestamp(unix_timestamp, tz=timezone) + except: # pylint: disable=bare-except + pass + + # RFC 2822 + try: + from email.utils import parsedate_to_datetime # pylint: disable=import-outside-toplevel + author_date = parsedate_to_datetime(git_date) + if author_date: + return author_date + except: # pylint: disable=bare-except + pass + + # ISO 8601 + try: + return datetime.datetime.fromisoformat(git_date) + except: # pylint: disable=bare-except + pass + + # plain formats + for format in ['%Y.%m.%d', '%m/%d/%Y', '%d.%m.%Y']: + try: + return datetime.datetime.strptime(git_date, format) + except: # pylint: disable=bare-except + pass + + raise RuntimeError(f"Not a supported git date format: {git_date}") + class Style(enum.Enum): """Enumerates the different known comment styles""" @@ -258,7 +298,11 @@ def __init__(self, name: str, year_from: int = None, self.git_repo = git_repo self.name_from_git = git_repo is not None if year_to is None: - year_to = DateUtils.current_year() + git_author_date = os.environ.get('GIT_AUTHOR_DATE', None) + if self.name_from_git and git_author_date: + year_to = DateUtils.parse_git_date(git_author_date).year + else: + year_to = DateUtils.current_year() self.year_to = year_to if year_from: self.year_from = year_from diff --git a/test.py b/test.py index c1ea336..62bba9e 100644 --- a/test.py +++ b/test.py @@ -18,6 +18,7 @@ # limitations under the License. from asyncio import subprocess +import datetime import functools import os import pathlib @@ -95,6 +96,23 @@ def test_recursive_includes(self): raise +class TestDateUtils(unittest.TestCase): + + def test_git_date_format(self): + EXPRESSIONS = [ + ('Thu, 07 Apr 2005 22:13:13 +0200', 2005, 4, 7), # RFC 2822 + ('2005-04-07T22:13:13', 2005, 4, 7), # ISO 8601 + ('1998.02.01', 1998, 2, 1), + ('01/02/1987', 1987, 1, 2), + ('@1453791344 +0100', 2016, 1, 26), # git internal format + ] + for expression, year, month, day in EXPRESSIONS: + date = license_tools.DateUtils.parse_git_date(expression) + self.assertEqual(date.year, year) + self.assertEqual(date.month, month) + self.assertEqual(date.day, day) + + def parser_test(file: pathlib.Path): def file_wrapper(func): @functools.wraps(func)