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)