release_robot.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # release_robot.py
  2. #
  3. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Determine last version string from tags.
  21. Alternate to `Versioneer <https://pypi.python.org/pypi/versioneer/>`_ using
  22. `Dulwich <https://pypi.python.org/pypi/dulwich>`_ to sort tags by time from
  23. newest to oldest.
  24. Copy the following into the package ``__init__.py`` module::
  25. from dulwich.contrib.release_robot import get_current_version
  26. __version__ = get_current_version()
  27. This example assumes the tags have a leading "v" like "v0.3", and that the
  28. ``.git`` folder is in a project folder that contains the package folder.
  29. EG::
  30. * project
  31. |
  32. * .git
  33. |
  34. +-* package
  35. |
  36. * __init__.py <-- put __version__ here
  37. """
  38. import datetime
  39. import logging
  40. import re
  41. import sys
  42. import time
  43. from typing import Any, Optional, cast
  44. from ..repo import Repo
  45. # CONSTANTS
  46. PROJDIR = "."
  47. PATTERN = r"[ a-zA-Z_\-]*([\d\.]+[\-\w\.]*)"
  48. def get_recent_tags(projdir: str = PROJDIR) -> list[tuple[str, list[Any]]]:
  49. """Get list of tags in order from newest to oldest and their datetimes.
  50. Args:
  51. projdir: path to ``.git``
  52. Returns:
  53. list of tags sorted by commit time from newest to oldest
  54. Each tag in the list contains the tag name, commit time, commit id, author
  55. and any tag meta. If a tag isn't annotated, then its tag meta is ``None``.
  56. Otherwise the tag meta is a tuple containing the tag time, tag id and tag
  57. name. Time is in UTC.
  58. """
  59. with Repo(projdir) as project: # dulwich repository object
  60. refs = project.get_refs() # dictionary of refs and their SHA-1 values
  61. tags = {} # empty dictionary to hold tags, commits and datetimes
  62. # iterate over refs in repository
  63. for key_bytes, value in refs.items():
  64. key = key_bytes.decode("utf-8") # compatible with Python-3
  65. obj = project.get_object(value) # dulwich object from SHA-1
  66. # don't just check if object is "tag" b/c it could be a "commit"
  67. # instead check if "tags" is in the ref-name
  68. if "tags" not in key:
  69. # skip ref if not a tag
  70. continue
  71. # strip the leading text from refs to get "tag name"
  72. _, tag = key.rsplit("/", 1)
  73. # check if tag object is "commit" or "tag" pointing to a "commit"
  74. from ..objects import Commit, Tag
  75. if isinstance(obj, Tag):
  76. commit_info = obj.object # a tuple (commit class, commit id)
  77. tag_meta = (
  78. datetime.datetime(*time.gmtime(obj.tag_time)[:6]),
  79. obj.id.decode("utf-8"),
  80. obj.name.decode("utf-8"),
  81. ) # compatible with Python-3
  82. commit = project.get_object(commit_info[1]) # commit object
  83. else:
  84. commit = obj
  85. tag_meta = None
  86. # get tag commit datetime, but dulwich returns seconds since
  87. # beginning of epoch, so use Python time module to convert it to
  88. # timetuple then convert to datetime
  89. commit_obj = cast(Commit, commit)
  90. tags[tag] = [
  91. datetime.datetime(*time.gmtime(commit_obj.commit_time)[:6]),
  92. commit_obj.id.decode("utf-8"),
  93. commit_obj.author.decode("utf-8"),
  94. tag_meta,
  95. ] # compatible with Python-3
  96. # return list of tags sorted by their datetimes from newest to oldest
  97. return sorted(tags.items(), key=lambda tag: tag[1][0], reverse=True)
  98. def get_current_version(
  99. projdir: str = PROJDIR,
  100. pattern: str = PATTERN,
  101. logger: Optional[logging.Logger] = None,
  102. ) -> Optional[str]:
  103. """Return the most recent tag, using an options regular expression pattern.
  104. The default pattern will strip any characters preceding the first semantic
  105. version. *EG*: "Release-0.2.1-rc.1" will be come "0.2.1-rc.1". If no match
  106. is found, then the most recent tag is return without modification.
  107. Args:
  108. projdir: path to ``.git``
  109. pattern: regular expression pattern with group that matches version
  110. logger: a Python logging instance to capture exception
  111. Returns:
  112. tag matching first group in regular expression pattern
  113. """
  114. tags = get_recent_tags(projdir)
  115. try:
  116. tag = tags[0][0]
  117. except IndexError:
  118. return None
  119. matches = re.match(pattern, tag)
  120. if matches:
  121. try:
  122. current_version = matches.group(1)
  123. return current_version
  124. except IndexError as err:
  125. if logger:
  126. logger.debug("Pattern %r didn't match tag %r: %s", pattern, tag, err)
  127. return tag
  128. else:
  129. if logger:
  130. logger.debug("Pattern %r didn't match tag %r", pattern, tag)
  131. return tag
  132. if __name__ == "__main__":
  133. if len(sys.argv) > 1:
  134. _PROJDIR = sys.argv[1]
  135. else:
  136. _PROJDIR = PROJDIR
  137. print(get_current_version(projdir=_PROJDIR))