Skip to content

Commit dac965d

Browse files
committed
Rename scm.py to scm_old.py and add new utility functions.
Refactored SCM-related imports to use the renamed `scm_old.py` for better module organization. Introduced `is_subpath` utility to simplify path checks and added support for moveable tags in version control systems. These changes improve code structure and extend functionality for tagging.
1 parent 7e50ac7 commit dac965d

File tree

9 files changed

+64
-36
lines changed

9 files changed

+64
-36
lines changed

bumpversion/config/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def get_configuration(config_file: Union[str, Path, None] = None, **overrides: A
6565
The configuration
6666
"""
6767
from bumpversion.config.utils import get_all_file_configs, get_all_part_configs
68-
from bumpversion.scm import SCMInfo, SourceCodeManager, get_scm_info # noqa: F401
68+
from bumpversion.scm_old import SCMInfo, SourceCodeManager, get_scm_info # noqa: F401
6969

7070
logger.info("Reading configuration")
7171
logger.indent()

bumpversion/config/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from bumpversion.versioning.models import VersionComponentSpec # NOQA: TC001
1515

1616
if TYPE_CHECKING:
17-
from bumpversion.scm import SCMInfo
17+
from bumpversion.scm_old import SCMInfo
1818
from bumpversion.versioning.models import VersionSpec
1919
from bumpversion.versioning.version_config import VersionConfig
2020

bumpversion/context.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
if TYPE_CHECKING: # pragma: no-coverage
1010
from bumpversion.config import Config
11-
from bumpversion.scm import SCMInfo
11+
from bumpversion.scm_old import SCMInfo
1212
from bumpversion.versioning.models import Version
1313

1414
MONTH_ABBREVIATIONS = [abbr for abbr in calendar.month_abbr if abbr]
@@ -56,7 +56,7 @@ def prefixed_environ() -> dict:
5656

5757
def base_context(scm_info: Optional["SCMInfo"] = None) -> ChainMap:
5858
"""The default context for rendering messages and tags."""
59-
from bumpversion.scm import SCMInfo # Including this here to avoid circular imports
59+
from bumpversion.scm_old import SCMInfo # Including this here to avoid circular imports
6060

6161
scm = asdict(scm_info) if scm_info else asdict(SCMInfo())
6262

bumpversion/scm.py bumpversion/scm_old.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, MutableMapping, Optional, Type, Union
1010

1111
from bumpversion.ui import get_indented_logger
12-
from bumpversion.utils import extract_regex_flags, format_and_raise_error, run_command
12+
from bumpversion.utils import extract_regex_flags, format_and_raise_error, is_subpath, run_command
1313

1414
if TYPE_CHECKING: # pragma: no-coverage
1515
from bumpversion.config import Config
@@ -50,14 +50,12 @@ def __repr__(self):
5050
f"dirty={self.dirty})"
5151
)
5252

53-
def path_in_repo(self, path: Union[Path, str]) -> bool:
53+
def path_in_repo(self, path: Path | str) -> bool:
5454
"""Return whether a path is inside this repository."""
5555
if self.repository_root is None:
5656
return True
57-
elif not Path(path).is_absolute():
58-
return True
5957

60-
return str(path).startswith(str(self.repository_root))
58+
return is_subpath(self.repository_root, path)
6159

6260

6361
class SourceCodeManager:
@@ -128,6 +126,11 @@ def tag(cls, name: str, sign: bool = False, message: Optional[str] = None) -> No
128126
"""Create a tag of the new_version in VCS."""
129127
raise NotImplementedError
130128

129+
@classmethod
130+
def moveable_tag(cls, name: str) -> None:
131+
"""Create a new lightweight tag that should overwrite any previous tags with the same name."""
132+
raise NotImplementedError
133+
131134
@classmethod
132135
def get_all_tags(cls) -> List[str]:
133136
"""Return all tags in VCS."""
@@ -229,6 +232,12 @@ def tag_in_scm(cls, config: "Config", context: MutableMapping, dry_run: bool = F
229232
if do_tag:
230233
cls.tag(tag_name, sign_tags, tag_message)
231234

235+
for moveable_tag in config.moveable_tags:
236+
tag_name = moveable_tag.format(**context)
237+
logger.info("%s moveable tag '%s' in %s", "Tagging" if do_tag else "Would tag", tag_name, cls.__name__)
238+
if do_tag:
239+
cls.moveable_tag(moveable_tag)
240+
232241
def __str__(self):
233242
return self.__repr__()
234243

@@ -382,6 +391,17 @@ def tag(cls, name: str, sign: bool = False, message: Optional[str] = None) -> No
382391
command += ["--message", message]
383392
run_command(command)
384393

394+
@classmethod
395+
def moveable_tag(cls, name: str) -> None:
396+
"""
397+
Create a new lightweight tag that should overwrite any previous tags with the same name.
398+
399+
Args:
400+
name: The name of the moveable tag.
401+
"""
402+
run_command(["git", "tag", "-f", name])
403+
run_command(["git", "push", "origin", name, "--force"])
404+
385405

386406
class Mercurial(SourceCodeManager):
387407
"""Mercurial implementation."""

bumpversion/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import string
44
import subprocess
5+
from pathlib import Path
56
from subprocess import CompletedProcess
67
from typing import Any, List, Optional, Tuple, Union
78

@@ -126,3 +127,8 @@ def run_command(command: list, env: Optional[dict] = None) -> CompletedProcess:
126127
result = subprocess.run(command, text=True, check=True, capture_output=True, env=env) # NOQA: S603
127128
result.check_returncode()
128129
return result
130+
131+
132+
def is_subpath(parent: Path | str, path: Path | str) -> bool:
133+
"""Return whether a path is inside the parent."""
134+
return str(path).startswith(str(parent)) if Path(path).is_absolute() else True

tests/test_bump.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for the bump module."""
22

3-
from pathlib import Path
43
import shutil
4+
from pathlib import Path
55
from textwrap import dedent
66
from unittest.mock import MagicMock, patch
77

@@ -10,7 +10,7 @@
1010
from bumpversion import bump
1111
from bumpversion.exceptions import ConfigurationError, VersionNotFoundError
1212
from bumpversion.files import ConfiguredFile
13-
from bumpversion.scm import Git, SCMInfo
13+
from bumpversion.scm_old import Git, SCMInfo
1414
from bumpversion.utils import run_command
1515
from tests.conftest import get_config_data, inside_dir
1616

tests/test_config/test_init.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from dataclasses import dataclass
44
from typing import Optional
55

6-
from bumpversion.scm import SCMInfo
76
from bumpversion.config import check_current_version
7+
from bumpversion.scm_old import SCMInfo
88

99

1010
@dataclass

tests/test_scm.py tests/test_scm_old.py

+25-23
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from pathlib import Path
77

88
import pytest
9-
from pytest import param, LogCaptureFixture
9+
from pytest import LogCaptureFixture, param
1010

11-
from bumpversion import scm
12-
from bumpversion.exceptions import DirtyWorkingDirectoryError, BumpVersionError
11+
from bumpversion import scm_old
12+
from bumpversion.exceptions import BumpVersionError, DirtyWorkingDirectoryError
1313
from bumpversion.ui import setup_logging
1414
from bumpversion.utils import run_command
1515
from tests.conftest import get_config_data, inside_dir
@@ -43,7 +43,7 @@ class TestGetVersionFromTag:
4343
def returns_version_from_pattern(self, tag: str, tag_name: str, parse_pattern: str, expected: str) -> None:
4444
"""It properly returns the version from the tag."""
4545
# Act
46-
version = scm.SourceCodeManager.get_version_from_tag(tag, tag_name, parse_pattern)
46+
version = scm_old.SourceCodeManager.get_version_from_tag(tag, tag_name, parse_pattern)
4747

4848
# Assert
4949
assert version == expected
@@ -55,7 +55,7 @@ def test_format_and_raise_error_returns_scm_error(self, git_repo: Path) -> None:
5555
run_command(["git", "add", "newfile.txt"])
5656
except subprocess.CalledProcessError as e:
5757
with pytest.raises(BumpVersionError) as bump_error:
58-
scm.Git.format_and_raise_error(e)
58+
scm_old.Git.format_and_raise_error(e)
5959
assert bump_error.value.message == (
6060
"Failed to run `git add newfile.txt`: return code 128, output: "
6161
"fatal: pathspec 'newfile.txt' did not match any files\n"
@@ -71,29 +71,29 @@ class TestIsUsable:
7171
def test_recognizes_a_git_repo(self, git_repo: Path) -> None:
7272
"""Should return true if git is available, and it is a git repo."""
7373
with inside_dir(git_repo):
74-
assert scm.Git.is_usable()
74+
assert scm_old.Git.is_usable()
7575

7676
def test_recognizes_not_a_git_repo(self, tmp_path: Path) -> None:
7777
"""Should return false if it is not a git repo."""
7878
with inside_dir(tmp_path):
79-
assert not scm.Git.is_usable()
79+
assert not scm_old.Git.is_usable()
8080

8181
class TestAssertNonDirty:
8282
"""Tests for the Git.assert_nondirty() function."""
8383

8484
def test_does_nothing_when_not_dirty(self, git_repo: Path) -> None:
8585
"""If the git repo is clean, assert_nondirty should do nothing."""
8686
with inside_dir(git_repo):
87-
scm.Git.assert_nondirty()
87+
scm_old.Git.assert_nondirty()
8888

8989
def test_raises_error_when_dirty(self, git_repo: Path) -> None:
9090
"""If the git repo has modified files, assert_nondirty should return false."""
9191
readme = git_repo.joinpath("readme.md")
9292
readme.touch()
9393
with pytest.raises(DirtyWorkingDirectoryError):
9494
with inside_dir(git_repo):
95-
subprocess.run(["git", "add", "readme.md"])
96-
scm.Git.assert_nondirty()
95+
subprocess.run(["git", "add", "readme.md"], check=False)
96+
scm_old.Git.assert_nondirty()
9797

9898
class TestLatestTagInfo:
9999
"""Test for the Git.latest_tag_info() function."""
@@ -107,8 +107,8 @@ def test_an_empty_repo_only_fills_tool_info(self, git_repo: Path) -> None:
107107
tag_prefix = "app/"
108108
parse_pattern = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"
109109
tag_name = f"{tag_prefix}{{new_version}}"
110-
expected = scm.SCMInfo(
111-
tool=scm.Git,
110+
expected = scm_old.SCMInfo(
111+
tool=scm_old.Git,
112112
commit_sha=None,
113113
distance_to_latest_tag=0,
114114
current_version=None,
@@ -118,7 +118,7 @@ def test_an_empty_repo_only_fills_tool_info(self, git_repo: Path) -> None:
118118
dirty=None,
119119
)
120120
with inside_dir(git_repo):
121-
latest_tag_info = scm.Git.latest_tag_info(tag_name, parse_pattern=parse_pattern)
121+
latest_tag_info = scm_old.Git.latest_tag_info(tag_name, parse_pattern=parse_pattern)
122122
assert latest_tag_info == expected
123123

124124
def test_returns_commit_and_tag_info(self, git_repo: Path) -> None:
@@ -133,7 +133,7 @@ def test_returns_commit_and_tag_info(self, git_repo: Path) -> None:
133133
subprocess.run(["git", "add", "readme.md"])
134134
subprocess.run(["git", "commit", "-m", "first"])
135135
subprocess.run(["git", "tag", f"{tag_prefix}0.1.0"])
136-
tag_info = scm.Git.latest_tag_info(tag_name, parse_pattern=parse_pattern)
136+
tag_info = scm_old.Git.latest_tag_info(tag_name, parse_pattern=parse_pattern)
137137
assert tag_info.commit_sha is not None
138138
assert tag_info.current_version == "0.1.0"
139139
assert tag_info.current_tag == f"{tag_prefix}0.1.0"
@@ -165,7 +165,7 @@ def test_git_detects_existing_tag(git_repo: Path, caplog: LogCaptureFixture) ->
165165
subprocess.run(["git", "tag", "v0.2.0"])
166166

167167
# Act
168-
scm.Git.tag_in_scm(config=conf, context=context)
168+
scm_old.Git.tag_in_scm(config=conf, context=context)
169169

170170
# Assert
171171
assert "Will not tag" in caplog.text
@@ -174,14 +174,14 @@ def test_git_detects_existing_tag(git_repo: Path, caplog: LogCaptureFixture) ->
174174
def test_hg_is_not_usable(tmp_path: Path) -> None:
175175
"""Should return false if it is not a mercurial repo."""
176176
with inside_dir(tmp_path):
177-
assert not scm.Mercurial.is_usable()
177+
assert not scm_old.Mercurial.is_usable()
178178

179179

180180
@pytest.mark.skipif(not shutil.which("hg"), reason="Mercurial is not available.")
181181
def test_hg_is_usable(hg_repo: Path) -> None:
182182
"""Should return false if it is not a mercurial repo."""
183183
with inside_dir(hg_repo):
184-
assert scm.Mercurial.is_usable()
184+
assert scm_old.Mercurial.is_usable()
185185

186186

187187
@pytest.mark.parametrize(
@@ -190,20 +190,22 @@ def test_hg_is_usable(hg_repo: Path) -> None:
190190
param(
191191
"git_repo",
192192
"git",
193-
scm.Git,
193+
scm_old.Git,
194194
id="git",
195195
marks=pytest.mark.skipif(not shutil.which("git"), reason="Git is not available."),
196196
),
197197
param(
198198
"hg_repo",
199199
"hg",
200-
scm.Mercurial,
200+
scm_old.Mercurial,
201201
id="hg",
202202
marks=pytest.mark.skipif(not shutil.which("hg"), reason="Mercurial is not available."),
203203
),
204204
],
205205
)
206-
def test_commit_and_tag_from_below_scm_root(repo: str, scm_command: str, scm_class: scm.SourceCodeManager, request):
206+
def test_commit_and_tag_from_below_scm_root(
207+
repo: str, scm_command: str, scm_class: scm_old.SourceCodeManager, request
208+
):
207209
# Arrange
208210
repo_path: Path = request.getfixturevalue(repo)
209211
version_path = repo_path / "VERSION"
@@ -243,14 +245,14 @@ def test_commit_and_tag_from_below_scm_root(repo: str, scm_command: str, scm_cla
243245
param(
244246
"git_repo",
245247
"git",
246-
scm.Git,
248+
scm_old.Git,
247249
id="git",
248250
marks=pytest.mark.skipif(not shutil.which("git"), reason="Git is not available."),
249251
),
250252
param(
251253
"hg_repo",
252254
"hg",
253-
scm.Mercurial,
255+
scm_old.Mercurial,
254256
id="hg",
255257
marks=pytest.mark.skipif(not shutil.which("hg"), reason="Mercurial is not available."),
256258
),
@@ -267,7 +269,7 @@ def test_commit_and_tag_from_below_scm_root(repo: str, scm_command: str, scm_cla
267269
def test_commit_tag_dry_run_interactions(
268270
repo: str,
269271
scm_command: str,
270-
scm_class: scm.SourceCodeManager,
272+
scm_class: scm_old.SourceCodeManager,
271273
commit: bool,
272274
tag: bool,
273275
dry_run: bool,

tests/test_versioning/test_version_config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class TestSerialize:
5151

5252
def test_distance_to_latest_tag_in_pattern(self):
5353
"""Using ``distance_to_latest_tag`` in the serialization string outputs correctly."""
54-
from bumpversion.scm import Git, SCMInfo
54+
from bumpversion.scm_old import Git, SCMInfo
5555

5656
overrides = {
5757
"current_version": "19.6.0",

0 commit comments

Comments
 (0)