diff --git a/dulwich/contrib/release_robot.py b/dulwich/contrib/release_robot.py index accc972a..d96ca119 100644 --- a/dulwich/contrib/release_robot.py +++ b/dulwich/contrib/release_robot.py @@ -1,129 +1,170 @@ # release_robot.py # # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as public by the Free Software Foundation; version 2.0 # or (at your option) any later version. You can redistribute it and/or # modify it under the terms of either of these two licenses. # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You should have received a copy of the licenses; if not, see # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Determine last version string from tags. Alternate to `Versioneer `_ using `Dulwich `_ to sort tags by time from newest to oldest. -Import this module into the package ``__init__.py`` and then set ``__version__`` -as follows:: +Copy the following into the package ``__init__.py`` module:: from dulwich.contrib.release_robot import get_current_version + from dulwich.repo import NotGitRepository + import os + import importlib - __version__ = get_current_version() - # other dunder classes like __author__, etc. + BASEDIR = os.path.dirname(__file__) # this directory + VER_FILE = 'version' # name of file to store version + # use release robot to try to get current Git tag + try: + GIT_TAG = get_current_version(os.path.dirname(BASEDIR)) + except NotGitRepository: + GIT_TAG = None + # check version file + try: + version = importlib.import_module('%s.%s' % (__name__, VER_FILE)) + except ImportError: + VERSION = None + else: + VERSION = version.VERSION + # update version file if it differs from Git tag + if GIT_TAG is not None and VERSION != GIT_TAG: + with open(os.path.join(BASEDIR, VER_FILE + '.py'), 'w') as vf: + vf.write('VERSION = "%s"\n' % GIT_TAG) + else: + GIT_TAG = VERSION # if Git tag is none use version file + VERSION = GIT_TAG # version + + __version__ = VERSION + # other dunder constants like __author__, __email__, __url__, etc. This example assumes the tags have a leading "v" like "v0.3", and that the -``.git`` folder is in the project folder that containts the package folder. +``.git`` folder is in a project folder that containts the package folder. + +EG:: + + * project + | + * .git + | + +-* package + | + * __init__.py <-- put __version__ here + + """ -from dulwich.repo import Repo -import time import datetime -import os import re import sys +import time + +from dulwich.repo import Repo # CONSTANTS -DIRNAME = os.path.abspath(os.path.dirname(__file__)) -PROJDIR = os.path.dirname(DIRNAME) -PATTERN = '[ a-zA-Z_\-]*([\d\.]+[\-\w\.]*)' +PROJDIR = '.' +PATTERN = r'[ a-zA-Z_\-]*([\d\.]+[\-\w\.]*)' def get_recent_tags(projdir=PROJDIR): """Get list of tags in order from newest to oldest and their datetimes. :param projdir: path to ``.git`` - :returns: list of (tag, [datetime, commit, author]) sorted from new to old + :returns: list of tags sorted by commit time from newest to oldest + + Each tag in the list contains the tag name, commit time, commit id, author + and any tag meta. If a tag isn't annotated, then its tag meta is ``None``. + Otherwise the tag meta is a tuple containing the tag time, tag id and tag + name. Time is in UTC. """ - project = Repo(projdir) # dulwich repository object - refs = project.get_refs() # dictionary of refs and their SHA-1 values - tags = {} # empty dictionary to hold tags, commits and datetimes - # iterate over refs in repository - for key, value in refs.items(): - obj = project.get_object(value) # dulwich object from SHA-1 - # check if object is tag - if obj.type_name != 'tag': - # skip ref if not a tag - continue - # strip the leading text from "refs/tag/" to get "tag name" - _, tag = key.rsplit('/', 1) - # check if tag object is commit, altho it should always be true - if obj.object[0].type_name == 'commit': - commit = project.get_object(obj.object[1]) # commit object + with Repo(projdir) as project: # dulwich repository object + refs = project.get_refs() # dictionary of refs and their SHA-1 values + tags = {} # empty dictionary to hold tags, commits and datetimes + # iterate over refs in repository + for key, value in refs.items(): + key = key.decode('utf-8') # compatible with Python-3 + obj = project.get_object(value) # dulwich object from SHA-1 + # don't just check if object is "tag" b/c it could be a "commit" + # instead check if "tags" is in the ref-name + if u'tags' not in key: + # skip ref if not a tag + continue + # strip the leading text from refs to get "tag name" + _, tag = key.rsplit(u'/', 1) + # check if tag object is "commit" or "tag" pointing to a "commit" + try: + commit = obj.object # a tuple (commit class, commit id) + except AttributeError: + commit = obj + tag_meta = None + else: + tag_meta = ( + datetime.datetime(*time.gmtime(obj.tag_time)[:6]), + obj.id.decode('utf-8'), + obj.name.decode('utf-8') + ) # compatible with Python-3 + commit = project.get_object(commit[1]) # commit object # get tag commit datetime, but dulwich returns seconds since # beginning of epoch, so use Python time module to convert it to # timetuple then convert to datetime tags[tag] = [ datetime.datetime(*time.gmtime(commit.commit_time)[:6]), - commit.id, - commit.author - ] + commit.id.decode('utf-8'), + commit.author.decode('utf-8'), + tag_meta + ] # compatible with Python-3 # return list of tags sorted by their datetimes from newest to oldest return sorted(tags.items(), key=lambda tag: tag[1][0], reverse=True) -def get_current_version(pattern=PATTERN, projdir=PROJDIR, logger=None): +def get_current_version(projdir=PROJDIR, pattern=PATTERN, logger=None): """Return the most recent tag, using an options regular expression pattern. The default pattern will strip any characters preceding the first semantic version. *EG*: "Release-0.2.1-rc.1" will be come "0.2.1-rc.1". If no match is found, then the most recent tag is return without modification. - :param pattern: regular expression pattern with group that matches version :param projdir: path to ``.git`` + :param pattern: regular expression pattern with group that matches version :param logger: a Python logging instance to capture exception :returns: tag matching first group in regular expression pattern """ tags = get_recent_tags(projdir) try: tag = tags[0][0] except IndexError: return - m = re.match(pattern, tag) + matches = re.match(pattern, tag) try: - current_version = m.group(1) + current_version = matches.group(1) except (IndexError, AttributeError) as err: if logger: logger.exception(err) return tag return current_version -def test_tag_pattern(): - test_cases = { - '0.3': '0.3', 'v0.3': '0.3', 'release0.3': '0.3', 'Release-0.3': '0.3', - 'v0.3rc1': '0.3rc1', 'v0.3-rc1': '0.3-rc1', 'v0.3-rc.1': '0.3-rc.1', - 'version 0.3': '0.3', 'version_0.3_rc_1': '0.3_rc_1', 'v1': '1', - '0.3rc1': '0.3rc1' - } - for tc, version in test_cases.iteritems(): - m = re.match(PATTERN, tc) - assert m.group(1) == version - - if __name__ == '__main__': if len(sys.argv) > 1: - projdir = sys.argv[1] + _PROJDIR = sys.argv[1] else: - projdir = PROJDIR - print(get_current_version(projdir=projdir)) + _PROJDIR = PROJDIR + print(get_current_version(projdir=_PROJDIR)) diff --git a/dulwich/contrib/test_release_robot.py b/dulwich/contrib/test_release_robot.py index 3174004f..28915173 100644 --- a/dulwich/contrib/test_release_robot.py +++ b/dulwich/contrib/test_release_robot.py @@ -1,39 +1,127 @@ # release_robot.py # # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as public by the Free Software Foundation; version 2.0 # or (at your option) any later version. You can redistribute it and/or # modify it under the terms of either of these two licenses. # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You should have received a copy of the licenses; if not, see # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Tests for release_robot.""" +import datetime +import os import re +import shutil +import tempfile +import time import unittest -from dulwich.contrib.release_robot import PATTERN +from dulwich.contrib import release_robot +from dulwich.repo import Repo +from dulwich.tests.utils import make_commit, make_tag + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) # this directory + + +def gmtime_to_datetime(gmt): + return datetime.datetime(*time.gmtime(gmt)[:6]) class TagPatternTests(unittest.TestCase): + """test tag patterns""" def test_tag_pattern(self): + """test tag patterns""" test_cases = { - '0.3': '0.3', 'v0.3': '0.3', 'release0.3': '0.3', 'Release-0.3': '0.3', - 'v0.3rc1': '0.3rc1', 'v0.3-rc1': '0.3-rc1', 'v0.3-rc.1': '0.3-rc.1', - 'version 0.3': '0.3', 'version_0.3_rc_1': '0.3_rc_1', 'v1': '1', - '0.3rc1': '0.3rc1' + '0.3': '0.3', 'v0.3': '0.3', 'release0.3': '0.3', + 'Release-0.3': '0.3', 'v0.3rc1': '0.3rc1', 'v0.3-rc1': '0.3-rc1', + 'v0.3-rc.1': '0.3-rc.1', 'version 0.3': '0.3', + 'version_0.3_rc_1': '0.3_rc_1', 'v1': '1', '0.3rc1': '0.3rc1' } - for tc, version in test_cases.items(): - m = re.match(PATTERN, tc) - self.assertEqual(m.group(1), version) + for testcase, version in test_cases.items(): + matches = re.match(release_robot.PATTERN, testcase) + self.assertEqual(matches.group(1), version) + + +class GetRecentTagsTest(unittest.TestCase): + """test get recent tags""" + + # Git repo for dulwich project + test_repo = os.path.join(BASEDIR, 'dulwich_test_repo.zip') + committer = b"Mark Mikofski " + test_tags = [b'v0.1a', b'v0.1'] + tag_test_data = { + test_tags[0]: [1484788003, b'0' * 40, None], + test_tags[1]: [1484788314, b'1' * 40, (1484788401, b'2' * 40)] + } + + @classmethod + def setUpClass(cls): + cls.projdir = tempfile.mkdtemp() # temporary project directory + cls.repo = Repo.init(cls.projdir) # test repo + obj_store = cls.repo.object_store # test repo object store + # commit 1 ('2017-01-19T01:06:43') + cls.c1 = make_commit( + id=cls.tag_test_data[cls.test_tags[0]][1], + commit_time=cls.tag_test_data[cls.test_tags[0]][0], + message=b'unannotated tag', + author=cls.committer + ) + obj_store.add_object(cls.c1) + # tag 1: unannotated + cls.t1 = cls.test_tags[0] + cls.repo[b'refs/tags/' + cls.t1] = cls.c1.id # add unannotated tag + # commit 2 ('2017-01-19T01:11:54') + cls.c2 = make_commit( + id=cls.tag_test_data[cls.test_tags[1]][1], + commit_time=cls.tag_test_data[cls.test_tags[1]][0], + message=b'annotated tag', + parents=[cls.c1.id], + author=cls.committer + ) + obj_store.add_object(cls.c2) + # tag 2: annotated ('2017-01-19T01:13:21') + cls.t2 = make_tag( + cls.c2, + id=cls.tag_test_data[cls.test_tags[1]][2][1], + name=cls.test_tags[1], + tag_time=cls.tag_test_data[cls.test_tags[1]][2][0] + ) + obj_store.add_object(cls.t2) + cls.repo[b'refs/heads/master'] = cls.c2.id + cls.repo[b'refs/tags/' + cls.t2.name] = cls.t2.id # add annotated tag + + @classmethod + def tearDownClass(cls): + cls.repo.close() + shutil.rmtree(cls.projdir) + + def test_get_recent_tags(self): + """test get recent tags""" + tags = release_robot.get_recent_tags(self.projdir) # get test tags + for tag, metadata in tags: + tag = tag.encode('utf-8') + test_data = self.tag_test_data[tag] # test data tag + # test commit date, id and author name + self.assertEqual(metadata[0], gmtime_to_datetime(test_data[0])) + self.assertEqual(metadata[1].encode('utf-8'), test_data[1]) + self.assertEqual(metadata[2].encode('utf-8'), self.committer) + # skip unannotated tags + tag_obj = test_data[2] + if not tag_obj: + continue + # tag date, id and name + self.assertEqual(metadata[3][0], gmtime_to_datetime(tag_obj[0])) + self.assertEqual(metadata[3][1].encode('utf-8'), tag_obj[1]) + self.assertEqual(metadata[3][2].encode('utf-8'), tag)