1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
"""Module containing util."""
import argparse
import stat
import subprocess
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Optional
REPO_FOLDER: Path = Path(__file__).resolve().parent.parent
class MissingDependencyError(Exception):
"""Exception raised when a depedency is missing from the system running setup-repo."""
def __init__(self, project: Path, dependency: str):
"""Initializes MisssingDependencyError."""
message_lines: list[str] = [
f"Unable to find {dependency=}.",
f"Please ensure that {dependency} is installed before setting up the repo at {project.absolute()}",
]
message: str = "\n".join(message_lines)
super().__init__(message)
def check_dependencies(path: Path, dependencies: list[str]) -> None:
"""Checks for any passed dependencies."""
for dependency in dependencies:
try:
subprocess.check_call([dependency, "--version"], cwd=path)
except subprocess.CalledProcessError as e:
raise MissingDependencyError(path, dependency) from e
def existing_dir(value: str) -> Path:
"""Responsible for validating argparse inputs and returning them as pathlib Path's if they meet criteria."""
path = Path(value).expanduser().resolve()
if not path.exists():
raise argparse.ArgumentTypeError(f"{path} does not exist.")
if not path.is_dir():
raise argparse.ArgumentTypeError(f"{path} is not a directory.")
return path
def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None:
"""Clears the readonly bit and attempts to call the provided function.
This is passed to shutil.rmtree as the onerror kwarg.
"""
Path(path).chmod(stat.S_IWRITE)
func(path)
def get_package_version() -> str:
"""Gets the package version."""
result: subprocess.CompletedProcess = subprocess.run(
["uvx", "--from", "commitizen", "cz", "version", "-p"],
cwd=REPO_FOLDER,
capture_output=True,
)
return result.stdout.decode("utf-8").strip()
def get_bumped_package_version(increment: Optional[str] = None) -> str:
"""Gets the bumped package version."""
args: list[str] = [
"uvx",
"--from",
"commitizen",
"cz",
"bump",
"--get-next",
"--yes",
"--dry-run",
]
if increment is not None:
args.extend(["--increment", increment])
result: subprocess.CompletedProcess = subprocess.run(
args, cwd=REPO_FOLDER, capture_output=True
)
return result.stdout.decode("utf-8").strip()
def create_release_branch(new_version: str) -> None:
"""Creates a release branch."""
commands: list[list[str]] = [
["git", "status", "--porcelain"],
["git", "checkout", "-b", f"release/{new_version}", "main"],
]
for command in commands:
subprocess.run(command, cwd=REPO_FOLDER, capture_output=True, check=True)
def bump_version(increment: Optional[str] = None) -> None:
"""Bumps the package version."""
bump_cmd: list[str] = [
"uvx",
"--from",
"commitizen",
"cz",
"bump",
"--yes",
"--files-only",
"--changelog",
]
if increment is not None:
bump_cmd.extend(["--increment", increment])
subprocess.run(bump_cmd, cwd=REPO_FOLDER, check=True)
def get_latest_tag() -> Optional[str]:
"""Gets the latest git tag."""
sort_tags: list[str] = ["git", "tag", "--sort=-creatordate"]
find_last: list[str] = ["grep", "-v", '"${GITHUB_REF_NAME}"']
echo_none: list[str] = ["echo", "''"]
result: subprocess.CompletedProcess = subprocess.run(
[*sort_tags, "|", *find_last, "|", "tail", "-n1", "||", *echo_none],
cwd=REPO_FOLDER,
capture_output=True,
)
tag: str = result.stdout.decode("utf-8").strip()
if tag == "":
return None
return tag
def get_latest_release_notes() -> str:
"""Gets the release notes.
Assumes the latest_tag hasn't been applied yet.
"""
latest_tag: Optional[str] = get_latest_tag()
latest_version: str = get_package_version()
if latest_tag == latest_version:
raise ValueError(
"The latest tag and version are the same. Please ensure the release notes are taken before tagging."
)
rev_range: str = "" if latest_tag is None else f"{latest_tag}..{latest_version}"
command: list[str] = [
"uvx",
"--from",
"commitizen",
"cz",
"changelog",
rev_range,
"--dry-run",
"--unreleased-version",
latest_version,
]
result: subprocess.CompletedProcess = subprocess.run(
command, cwd=REPO_FOLDER, capture_output=True, check=True
)
return result.stdout.decode("utf-8")
def tag_release() -> None:
"""Tags the release using commitizen bump with tag only."""
subprocess.run(
["uvx", "--from", "commitizen", "cz", "bump", "--tag-only", "--yes"],
cwd=REPO_FOLDER,
check=True,
)
|