Browse Source

fix release robot issue for commit only tag

- if tag is not annotated, then it will be a commit object, instead of
creating a tag object
- don't check if ref object is tag, instead check if "tags" is in the
ref name
- change docstring to show how release_robot could be used as alternate
to versioneer, ie: copy and paste gist

https://gist.github.com/mikofski/e923750b415e4e4961b65a8eb42999e8#file-xyz__init__-py
- don't use contrib folder as default projdir, instead default to '.'
- return tag meta data if there is any from get_recent_tags() as tuple
(tag time, tag id, tag name) otherwise it's (None, None, None)
- remove test_tag_pattern from release_robot since it's in
test_release_robot()
- add tests

Signed-off-by: Mark Mikofski <mark.mikofski@sunpowercorp.com>
Mark Mikofski 8 years ago
parent
commit
e8c79d3096
2 changed files with 186 additions and 57 deletions
  1. 90 49
      dulwich/contrib/release_robot.py
  2. 96 8
      dulwich/contrib/test_release_robot.py

+ 90 - 49
dulwich/contrib/release_robot.py

@@ -23,74 +23,127 @@ Alternate to `Versioneer <https://pypi.python.org/pypi/versioneer/>`_ using
 `Dulwich <https://pypi.python.org/pypi/dulwich>`_ to sort tags by time from
 `Dulwich <https://pypi.python.org/pypi/dulwich>`_ to sort tags by time from
 newest to oldest.
 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.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
 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 datetime
-import os
 import re
 import re
 import sys
 import sys
+import time
+
+from dulwich.repo import Repo
 
 
 # CONSTANTS
 # 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):
 def get_recent_tags(projdir=PROJDIR):
     """Get list of tags in order from newest to oldest and their datetimes.
     """Get list of tags in order from newest to oldest and their datetimes.
 
 
     :param projdir: path to ``.git``
     :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/<tag name>" 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
             # get tag commit datetime, but dulwich returns seconds since
             # beginning of epoch, so use Python time module to convert it to
             # beginning of epoch, so use Python time module to convert it to
             # timetuple then convert to datetime
             # timetuple then convert to datetime
             tags[tag] = [
             tags[tag] = [
                 datetime.datetime(*time.gmtime(commit.commit_time)[:6]),
                 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 list of tags sorted by their datetimes from newest to oldest
     return sorted(tags.items(), key=lambda tag: tag[1][0], reverse=True)
     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.
     """Return the most recent tag, using an options regular expression pattern.
 
 
     The default pattern will strip any characters preceding the first semantic
     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
     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.
     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 projdir: path to ``.git``
+    :param pattern: regular expression pattern with group that matches version
     :param logger: a Python logging instance to capture exception
     :param logger: a Python logging instance to capture exception
     :returns: tag matching first group in regular expression pattern
     :returns: tag matching first group in regular expression pattern
     """
     """
@@ -99,9 +152,9 @@ def get_current_version(pattern=PATTERN, projdir=PROJDIR, logger=None):
         tag = tags[0][0]
         tag = tags[0][0]
     except IndexError:
     except IndexError:
         return
         return
-    m = re.match(pattern, tag)
+    matches = re.match(pattern, tag)
     try:
     try:
-        current_version = m.group(1)
+        current_version = matches.group(1)
     except (IndexError, AttributeError) as err:
     except (IndexError, AttributeError) as err:
         if logger:
         if logger:
             logger.exception(err)
             logger.exception(err)
@@ -109,21 +162,9 @@ def get_current_version(pattern=PATTERN, projdir=PROJDIR, logger=None):
     return current_version
     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 __name__ == '__main__':
     if len(sys.argv) > 1:
     if len(sys.argv) > 1:
-        projdir = sys.argv[1]
+        _PROJDIR = sys.argv[1]
     else:
     else:
-        projdir = PROJDIR
-    print(get_current_version(projdir=projdir))
+        _PROJDIR = PROJDIR
+    print(get_current_version(projdir=_PROJDIR))

+ 96 - 8
dulwich/contrib/test_release_robot.py

@@ -19,21 +19,109 @@
 
 
 """Tests for release_robot."""
 """Tests for release_robot."""
 
 
+import datetime
+import os
 import re
 import re
+import shutil
+import tempfile
+import time
 import unittest
 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):
 class TagPatternTests(unittest.TestCase):
+    """test tag patterns"""
 
 
     def test_tag_pattern(self):
     def test_tag_pattern(self):
+        """test tag patterns"""
         test_cases = {
         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 <mark.mikofski@sunpowercorp.com>"
+    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)