Skip to content

Instantly share code, notes, and snippets.

@rubeniskov
Last active May 14, 2022 17:03
Show Gist options
  • Save rubeniskov/583eb8633bc65053b9577cc9213cb610 to your computer and use it in GitHub Desktop.
Save rubeniskov/583eb8633bc65053b9577cc9213cb610 to your computer and use it in GitHub Desktop.
Guess the version using the git history message commits as a reference
#!/usr/bin/env python
from __future__ import print_function
import os, getopt, sys, re, sys, time
# Globals vars
__VERBOSE__ = False
__VERSION__ = "0.0.2"
# Default vars
DEFAULT_GIT_LOG_COMMAND = "git log --pretty=format:'%s' --reverse"
DEFAULT_VERSION_FORMAT = "{version_core}{prerelease}{build}"
DEFAULT_VERSION_FROM = "0.0.0"
# Semantic Versioning 2.0.0
# https://semver.org/
PREFIX_PRERELEASE = "-"
PREFIX_BUILD = "+"
VERSION_CORE_SEPARATOR = "."
VERSION_CORE_FRAGMENTS = [ "major", "minor", "patch" ]
REGEX_SEMVER = re.compile(
r"""
^v?
""" +
"\.".join(
map(lambda x: "(?P<{0}>0|[1-9]\d*)".format(x),
VERSION_CORE_FRAGMENTS
)
)
+
"""
(?:-(?P<prerelease>
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
))?
(?:\+(?P<build>
[0-9a-zA-Z-]+
(?:\.[0-9a-zA-Z-]+)*
))?
$
""",
re.VERBOSE,
)
# Semantic Commit Messages
# https://www.conventionalcommits.org/en/v1.0.0-beta.2/
# https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716
# related order with VERSION_CORE_FRAGMENTS
MESSAGE_BUMP_RULES = [
"BREAKING[_\s-]CHANGES",
"^feat(?:ure)?(?:\([0-9a-zA-Z-]+\))?:",
"^(?:bug|hot)?fix(?:\([0-9a-zA-Z-]+\))?:"
]
REGEX_MESSAGE_BUMP = re.compile(
r"|".join(
map(lambda x: "({0})".format(x),
MESSAGE_BUMP_RULES
)
),
re.IGNORECASE
)
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def verbose(*args, **kwargs):
if __VERBOSE__:
eprint(time.time(), *args, **kwargs)
def merge_dicts(x, y):
z = x.copy()
z.update(y)
return z
def fallfront(value, front = None):
return (front or value) if value else ""
def parse_version(version):
"""
Parse version to major, minor, patch parts.
:param version: version string
:return: array with major, minor, patch numbers,
:rtype: dict
"""
match = REGEX_SEMVER.match(version)
if match is None:
raise ValueError("%s is not valid SemVer string \n-- SEMVER PATTERN -- \n%s" % (version, REGEX_SEMVER.pattern))
groups = match.groupdict()
groups["version_core"] = map(lambda x: int(groups[x]), list(VERSION_CORE_FRAGMENTS))
return groups
def format_version_core(version_core):
return VERSION_CORE_SEPARATOR.join(str(x) for x in version_core)
def format_version(semver, only_core = False):
return format_version_core(semver["version_core"]) if only_core else (
DEFAULT_VERSION_FORMAT
).format(**merge_dicts(semver, {
"version_core": format_version_core(semver["version_core"]),
"prerelease": fallfront(semver["prerelease"], PREFIX_PRERELEASE + fallfront(semver["prerelease"])),
"build": fallfront(semver["build"], PREFIX_BUILD + fallfront(semver["build"])),
}))
def bump_version_by_index(version_core, index, step = 1):
"""
Increase the version core fragment by index using the step count
:param version_core: version list
:return: the ref of list version_core,
:rtype: list
"""
vclen = len(version_core)
for i in range(vclen - 1 - index):
# Reset decendents counters
version_core[vclen - i - 1] = 0
# Increase the version core fragment
version_core[index] += step
return version_core
def guess_version_from_commit_message(commit_message, current_version = None):
# Use a pre defined current_version or initialize one using zeros
current_version = current_version or [0] * len(VERSION_CORE_FRAGMENTS)
matches = REGEX_MESSAGE_BUMP.search(commit_message)
if matches:
# Detected a commit message which match with any rule
groups = matches.groups()
# Identify what kind of rule is matched associated by
# version core fragment index
version_core_fragment_index = next(i for i,v in enumerate(groups) if v is not None)
# Bump version core fragment by index
bump_version_by_index(current_version["version_core"], version_core_fragment_index)
return current_version
def guess_all_versions(stream):
version = parse_version(DEFAULT_VERSION_FROM)
for commit_message in stream:
version = guess_version_from_commit_message(commit_message, version)
print('\033[93m'
+ format_version(version, True)
+ '\033[0m' + "\t"
+ commit_message.rstrip()
)
def guess_current_version(stream):
version = parse_version(DEFAULT_VERSION_FROM)
for commit_message in stream:
version = guess_version_from_commit_message(commit_message, version)
print(format_version(version))
def show_version():
eprint("v{0} License MIT 2020".format(__VERSION__))
def usage():
eprint("""Usage: {program} [options] <filename>
Guess the version using the git history message commits as a reference taking the following rules
* Commit message prefixed with "fix: " means a "patch" version change
* Commit message prefixed with "feat: " means a "minor" version change
* Commit containing the keyword "BREAKING_CHANGES" means a "major" version change
Available options:
-a, --all Show the list of commits with the corresponding version changes
-f, --format=<value> Formatter used to generate the version string ("{version_format}")
-s, --from=<value> The initial version to start count ({version_from})
-v, --verbose Enable verbose traces
-h, --help Show usage
-v, --version Show version
Examples
{program}
{program} <file_name>
{program} <({git_log_command})
{git_log_command} | {program}
""".format(**{
"program": sys.argv[0],
"version_format": DEFAULT_VERSION_FORMAT,
"version_from": DEFAULT_VERSION_FROM,
"git_log_command": DEFAULT_GIT_LOG_COMMAND,
"version": show_version()
}))
def main():
global __VERBOSE__
global DEFAULT_VERSION_FORMAT
global DEFAULT_VERSION_FROM
global DEFAULT_GIT_LOG_COMMAND
stream = None
all = False
try:
opts, args = getopt.getopt(sys.argv[1:], "hvVaf:s:", ["help", "version", "verbose", "all", "from=", "format="])
except getopt.GetoptError as err:
eprint(err)
usage(cmd)
sys.exit(2)
for opt, value in opts:
if opt in ("-V", "--verbose"):
__VERBOSE__ = True
verbose("Enabled verbose mode")
elif opt in ("--all"):
all = True
elif opt in ("-f", "--format"):
DEFAULT_VERSION_FORMAT = value
elif opt in ("-s", "--from"):
DEFAULT_VERSION_FROM = value
elif opt in ("-v", "--version"):
show_version()
sys.exit()
elif opt in ("-h", "--help"):
usage()
sys.exit()
else:
assert False, usage()
if len(args) > 0:
try:
filename = os.path.join(os.getcwd(), args[0])
stream = open(filename, 'r')
verbose("Reading log from file \"{0}\"".format(filename))
except OSError:
pass
if stream is None:
if sys.stdin.isatty():
verbose("Executing git command \"{0}\"".format(DEFAULT_GIT_LOG_COMMAND))
stream = os.popen(DEFAULT_GIT_LOG_COMMAND)
else:
verbose("Pointing reading from stdin data")
stream = sys.stdin
if all:
guess_all_versions(stream)
else:
guess_current_version(stream)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment