diff --git a/app/commands/check/git.py b/app/commands/check/git.py index ebf8795..0572fd9 100644 --- a/app/commands/check/git.py +++ b/app/commands/check/git.py @@ -1,7 +1,11 @@ import click from app.utils.click import error, info, success -from app.utils.git import get_git_config, is_git_installed +from app.utils.git import ( + MIN_GIT_VERSION, + get_git_config, + get_git_version, +) @click.command() @@ -10,11 +14,22 @@ def git() -> None: Verifies if Git is installed and setup for Git-Mastery. """ info("Checking that you have Git installed and configured") - if is_git_installed(): - info("Git is installed") - else: + + git_version = get_git_version() + if git_version is None: error("Git is not installed") + info("Git is installed") + + if git_version.is_behind(MIN_GIT_VERSION): + error( + f"Git {git_version} is behind the minimum required version. " + f"Please upgrade to Git {MIN_GIT_VERSION} or later. " + f"Refer to https://git-scm.com/downloads" + ) + + info(f"Git {git_version} meets the minimum version requirement.") + config_user_name = get_git_config("user.name") if not config_user_name: error( diff --git a/app/utils/git.py b/app/utils/git.py index 0ee281f..9c72035 100644 --- a/app/utils/git.py +++ b/app/utils/git.py @@ -1,6 +1,10 @@ +import re from typing import Optional from app.utils.command import run +from app.utils.version import Version + +MIN_GIT_VERSION = Version(2, 28, 0) def init() -> None: @@ -23,11 +27,22 @@ def push(remote: str, branch: str) -> None: run(["git", "push", "-u", remote, branch]) -def is_git_installed() -> bool: +def get_git_version() -> Optional[Version]: + """Get the installed git version. + + Returns None if git is not installed or version cannot be parsed. + """ # If git is not installed yet, we should expect a 127 exit code # 127 indicating that the command not found: https://stackoverflow.com/questions/1763156/127-return-code-from result = run(["git", "--version"]) - return result.is_success() + if not result.is_success(): + return None + + match = re.search(r"^git version (\d+\.\d+\.\d+)", result.stdout) + if not match: + return None + + return Version.parse(match.group(1)) def remove_remote(remote: str) -> None: diff --git a/app/utils/version.py b/app/utils/version.py index 629ab28..3ede378 100644 --- a/app/utils/version.py +++ b/app/utils/version.py @@ -9,10 +9,27 @@ class Version: @staticmethod def parse_version_string(version: str) -> "Version": + """Parse a version string with 'v' prefix (e.g., 'v1.2.3').""" only_version = version[1:] [major, minor, patch] = only_version.split(".") return Version(int(major), int(minor), int(patch)) + @staticmethod + def parse(version: str) -> "Version": + """Parse a plain version string (e.g., '1.2.3').""" + parts = version.split(".") + if len(parts) != 3: + raise ValueError( + f"Invalid version string (expected 'MAJOR.MINOR.PATCH'): {version!r}" + ) + try: + major, minor, patch = (int(part) for part in parts) + except ValueError as exc: + raise ValueError( + f"Invalid numeric components in version string: {version!r}" + ) from exc + return Version(major, minor, patch) + def is_behind(self, other: "Version") -> bool: """Returns if the current version is behind the other version based on major and minor versions.""" return (other.major, other.minor) > (self.major, self.minor)