diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c74b86..68356d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased +* Added Bazaar support. * Added the `dunamai check` command and the corresponding `check_version` function. * Added the option to check just the latest tag or to keep checking tags diff --git a/README.md b/README.md index c2dc008..323463e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ simply by creating a tag. * Mercurial * Darcs * Subversion + * Bazaar * Version styles: * [PEP 440](https://www.python.org/dev/peps/pep-0440) * [Semantic Versioning](https://semver.org) diff --git a/dunamai/__init__.py b/dunamai/__init__.py index a31fd2a..563330b 100644 --- a/dunamai/__init__.py +++ b/dunamai/__init__.py @@ -478,6 +478,43 @@ def from_subversion( return cls(base, pre=pre, post=post, dev=dev, commit=commit, dirty=dirty) + @classmethod + def from_bazaar(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = False) -> "Version": + r""" + Determine a version based on Bazaar tags. + + :param pattern: Regular expression matched against the version source. + This should contain one capture group named `base` corresponding to + the release segment of the source, and optionally another two groups + named `pre_type` and `pre_number` corresponding to the type (a, b, rc) + and number of prerelease. For example, with a tag like v0.1.0, the + pattern would be `v(?P\d+\.\d+\.\d+)`. + :param latest_tag: If true, only inspect the latest tag for a pattern + match. If false, keep looking at tags until there is a match. + """ + code, msg = _run_cmd("bzr status") + dirty = msg != "" + + code, msg = _run_cmd("bzr log --limit 1 --line") + commit = msg.split(":", 1)[0] if msg else None + + code, msg = _run_cmd("bzr tags") + if not msg or not commit: + return cls("0.0.0", post=0, dev=0, commit=commit, dirty=dirty) + tags_to_revs = {line.split()[0]: int(line.split()[1]) for line in msg.splitlines()} + tags = [x[1] for x in sorted([(v, k) for k, v in tags_to_revs.items()], reverse=True)] + tag, base, pre = _match_version_pattern(pattern, tags, latest_tag) + + distance = int(commit) - tags_to_revs[tag] + + post = None + dev = None + if distance > 0: + post = distance + dev = 0 + + return cls(base, pre=pre, post=post, dev=dev, commit=commit, dirty=dirty) + @classmethod def from_any_vcs(cls, pattern: str = None, latest_tag: bool = False) -> "Version": """ @@ -488,7 +525,7 @@ def from_any_vcs(cls, pattern: str = None, latest_tag: bool = False) -> "Version :param latest_tag: If true, only inspect the latest tag for a pattern match. If false, keep looking at tags until there is a match. """ - vcs = _find_higher_dir(".git", ".hg", "_darcs", ".svn") + vcs = _find_higher_dir(".git", ".hg", "_darcs", ".svn", ".bzr") if pattern is None: pattern = _VERSION_PATTERN @@ -501,6 +538,8 @@ def from_any_vcs(cls, pattern: str = None, latest_tag: bool = False) -> "Version return cls.from_darcs(pattern=pattern, latest_tag=latest_tag) elif vcs == ".svn": return cls.from_subversion(pattern=pattern, latest_tag=latest_tag) + elif vcs == ".bzr": + return cls.from_bazaar(pattern=pattern, latest_tag=latest_tag) else: raise RuntimeError("Unable to detect version control system.") diff --git a/dunamai/__main__.py b/dunamai/__main__.py index b51bd18..6ed46c5 100644 --- a/dunamai/__main__.py +++ b/dunamai/__main__.py @@ -12,6 +12,7 @@ class Vcs(Enum): Mercurial = "mercurial" Darcs = "darcs" Subversion = "subversion" + Bazaar = "bazaar" common_sub_args = [ @@ -107,6 +108,10 @@ class Vcs(Enum): }, ], }, + Vcs.Bazaar.value: { + "description": "Generate version from Bazaar", + "args": common_sub_args, + }, }, }, "check": { @@ -188,6 +193,8 @@ def from_vcs( version = Version.from_darcs(pattern=pattern, latest_tag=latest_tag) elif vcs == Vcs.Subversion: version = Version.from_subversion(pattern=pattern, latest_tag=latest_tag, tag_dir=tag_dir) + elif vcs == Vcs.Bazaar: + version = Version.from_bazaar(pattern=pattern, latest_tag=latest_tag) print(version.serialize(metadata, dirty, format, style)) diff --git a/tests/test_dunamai.py b/tests/test_dunamai.py index 983bae3..7e47a35 100644 --- a/tests/test_dunamai.py +++ b/tests/test_dunamai.py @@ -555,6 +555,44 @@ def test__version__from_subversion(tmp_path): from_vcs(latest_tag=True) +@pytest.mark.skipif(shutil.which("bzr") is None, reason="Requires Bazaar") +def test__version__from_bazaar(tmp_path): + vcs = tmp_path / "dunamai-bzr" + vcs.mkdir() + run = make_run_callback(vcs) + from_vcs = make_from_callback(Version.from_bazaar, mock_commit=None) + + with chdir(vcs): + run("bzr init") + assert from_vcs() == Version("0.0.0", post=0, dev=0, commit=None, dirty=False) + + (vcs / "foo.txt").write_text("hi") + assert from_vcs() == Version("0.0.0", post=0, dev=0, commit=None, dirty=True) + + run("bzr add .") + run('bzr commit -m "Initial commit"') + assert from_vcs() == Version("0.0.0", post=0, dev=0, commit="1", dirty=False) + + run("bzr tag v0.1.0") + assert from_vcs() == Version("0.1.0", commit="1", dirty=False) + assert from_vcs(latest_tag=True) == Version("0.1.0", commit="1", dirty=False) + assert run("dunamai from bazaar") == "0.1.0" + assert run("dunamai from any") == "0.1.0" + + (vcs / "foo.txt").write_text("bye") + assert from_vcs() == Version("0.1.0", commit="1", dirty=True) + + run("bzr add .") + run('bzr commit -m "Second"') + assert from_vcs() == Version("0.1.0", post=1, dev=0, commit="2", dirty=False) + assert from_any_vcs_unmocked() == Version("0.1.0", post=1, dev=0, commit="2", dirty=False) + + run("bzr tag unmatched") + assert from_vcs() == Version("0.1.0", post=1, dev=0, commit="2", dirty=False) + with pytest.raises(ValueError): + from_vcs(latest_tag=True) + + def test__version__from_any_vcs(tmp_path): with chdir(tmp_path): with pytest.raises(RuntimeError): diff --git a/tests/test_main.py b/tests/test_main.py index 4753ab0..037d585 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -22,6 +22,7 @@ def test__parse_args__from(): assert parse_args(["from", "darcs"]).vcs == "darcs" assert parse_args(["from", "subversion"]).vcs == "subversion" assert parse_args(["from", "subversion"]).tag_dir == "tags" + assert parse_args(["from", "bazaar"]).vcs == "bazaar" assert parse_args(["from", "any", "--pattern", r"\d+"]).pattern == r"\d+" assert parse_args(["from", "any", "--metadata"]).metadata is True assert parse_args(["from", "any", "--no-metadata"]).metadata is False