diff --git a/CHANGELOG.md b/CHANGELOG.md index df42c69..396025d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## Unreleased + +* Generally, when Dunamai can detect the VCS in use, but there's no version set yet, + then Dunamai uses 0.0.0 as a fallback, unless strict mode is enabled. + This is useful for new projects that do not yet have a release. + + However, if there were some tags and none matched the version pattern, + then Dunamai would yield an error. + That wouldn't be helpful for a new project with some non-version tag, + and it could be incorrect for a monorepo with different tags for different packages. + + Now, Dunamai will use 0.0.0 in this case as well, unless strict mode is enabled. + ## v1.20.0 (2024-04-12) * Updated `Version.bump()` to add a `smart` argument, diff --git a/dunamai/__init__.py b/dunamai/__init__.py index f30ccc3..baacd69 100644 --- a/dunamai/__init__.py +++ b/dunamai/__init__.py @@ -226,8 +226,8 @@ def _run_cmd( def _match_version_pattern( - pattern: Union[str, Pattern], sources: Sequence[str], latest_source: bool -) -> _MatchedVersionPattern: + pattern: Union[str, Pattern], sources: Sequence[str], latest_source: bool, strict: bool +) -> Optional[_MatchedVersionPattern]: """ :returns: Tuple of: * matched tag @@ -281,8 +281,10 @@ def _match_version_pattern( sources, ) ) - else: + elif strict: raise ValueError(_pattern_error("The pattern did not match any tags", pattern, sources)) + else: + return None stage = pattern_match.groupdict().get("stage") revision = pattern_match.groupdict().get("revision") @@ -845,9 +847,13 @@ def parse(cls, version: str, pattern: Union[str, Pattern] = Pattern.Default) -> if not version.startswith("v") and pattern in [VERSION_SOURCE_PATTERN, Pattern.Default]: normalized = "v{}".format(version) + failed = False try: - matched_pattern = _match_version_pattern(pattern, [normalized], True) + matched_pattern = _match_version_pattern(pattern, [normalized], True, strict=True) except ValueError: + failed = True + + if failed or matched_pattern is None: replaced = re.sub(r"(\.post(\d+)\.dev\d+)", r".dev\2", version, 1) if replaced != version: alt = Version.parse(replaced, pattern) @@ -1060,9 +1066,19 @@ def from_git( vcs=vcs, ) - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, [tag], latest_tag - ) + matched_pattern = _match_version_pattern(pattern, [tag], latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + branch=branch, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern + version = cls( base, stage=stage, @@ -1158,9 +1174,7 @@ def from_git( vcs=vcs, ) tags = [line.replace("refs/tags/", "") for line in msg.splitlines()] - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag - ) + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) else: code, msg = _run_cmd( 'git for-each-ref "refs/tags/**" --merged {}'.format(tag_branch) @@ -1199,9 +1213,28 @@ def from_git( detailed_tags.append(_GitRefInfo(*parts).with_tag_topo_lookup(tag_topo_lookup)) tags = [t.ref for t in sorted(detailed_tags, key=lambda x: x.sort_key, reverse=True)] - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + + if matched_pattern is None: + distance = 0 + + code, msg = _run_cmd("git rev-list --max-parents=0 HEAD", path) + if msg: + initial_commit = msg.splitlines()[0].strip() + code, msg = _run_cmd("git rev-list --count {}..HEAD".format(initial_commit), path) + distance = int(msg) + + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + branch=branch, + timestamp=timestamp, + concerns=concerns, + vcs=vcs, ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern code, msg = _run_cmd("git rev-list --count refs/tags/{}..HEAD".format(tag), path) distance = int(msg) @@ -1281,9 +1314,17 @@ def from_mercurial( continue all_tags.append(parts[1]) - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, all_tags, latest_tag - ) + matched_pattern = _match_version_pattern(pattern, all_tags, latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + branch=branch, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern + version = cls( base, stage=stage, @@ -1336,9 +1377,19 @@ def from_mercurial( vcs=vcs, ) tags = [tag for tags in [line.split(":") for line in msg.splitlines()] for tag in tags] - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag - ) + + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + branch=branch, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern code, msg = _run_cmd('hg log -r "{0}::{1} - {0}" --template "."'.format(tag, commit), path) # The tag itself is in the list, so offset by 1. @@ -1407,9 +1458,18 @@ def from_darcs( strict, distance=distance, commit=commit, dirty=dirty, timestamp=timestamp, vcs=vcs ) tags = msg.splitlines() - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag - ) + + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern code, msg = _run_cmd("darcs log --from-tag {} --count".format(tag), path) # The tag itself is in the list, so offset by 1. @@ -1501,9 +1561,18 @@ def from_subversion( source = int(match.group(1)) tags_to_sources_revs[tag] = (source, rev) tags = sorted(tags_to_sources_revs, key=lambda x: tags_to_sources_revs[x], reverse=True) - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag - ) + + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern source, rev = tags_to_sources_revs[tag] # The tag itself is in the list, so offset by 1. @@ -1589,9 +1658,19 @@ def from_bazaar( if line.split()[1] != "?" } tags = [x[1] for x in sorted([(v, k) for k, v in tags_to_revs.items()], reverse=True)] - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag - ) + + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + branch=branch, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern distance = int(commit) - tags_to_revs[tag] @@ -1712,9 +1791,22 @@ def from_fossil( (line.rsplit(",", 1)[0][5:-1], int(line.rsplit(",", 1)[1]) - 1) for line in msg.splitlines() ] - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, [t for t, d in tags_to_distance], latest_tag + + matched_pattern = _match_version_pattern( + pattern, [t for t, d in tags_to_distance], latest_tag, strict ) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + branch=branch, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern + distance = dict(tags_to_distance)[tag] version = cls( @@ -1841,9 +1933,19 @@ def from_pijul( t["message"] for t in sorted(tag_meta_by_msg.values(), key=lambda x: x["timestamp"], reverse=True) ] - tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern( - pattern, tags, latest_tag - ) + + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + if matched_pattern is None: + return cls._fallback( + strict, + distance=distance, + commit=commit, + dirty=dirty, + branch=branch, + timestamp=timestamp, + vcs=vcs, + ) + tag, base, stage, unmatched, tagged_metadata, epoch = matched_pattern tag_id = tag_meta_by_msg[tag]["state"] _run_cmd("pijul tag checkout {}".format(tag_id), path, codes=[0, 1]) diff --git a/tests/integration/test_dunamai.py b/tests/integration/test_dunamai.py index 275af7a..9403c1c 100644 --- a/tests/integration/test_dunamai.py +++ b/tests/integration/test_dunamai.py @@ -132,8 +132,9 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None: # Additional one-off check not in other VCS integration tests: # when the only tag in the repository does not match the pattern. run("git tag other -m Annotated") + assert from_vcs() == Version("0.0.0", dirty=False, branch=b) with pytest.raises(ValueError): - from_vcs() + from_vcs(strict=True) avoid_identical_ref_timestamps() run("git tag v0.1.0 -m Annotated")