From 9791469cf86eba811c113ccc042f0d908fa2b374 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:14:06 +0800 Subject: [PATCH 1/4] feat: add minimum git version requirement check --- app/commands/check/git.py | 23 +++++++++++++++++++---- app/utils/git.py | 19 +++++++++++++++++-- app/utils/version.py | 7 +++++++ 3 files changed, 43 insertions(+), 6 deletions(-) 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..ef022b1 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, 25, 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"(\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..c6a97b5 100644 --- a/app/utils/version.py +++ b/app/utils/version.py @@ -9,10 +9,17 @@ 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').""" + [major, minor, patch] = version.split(".") + return Version(int(major), int(minor), int(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) From 7d55b4b4c1d127d49f2d61ff2646146784cab36e Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:16:56 +0800 Subject: [PATCH 2/4] chore: bump minimum git required version to 2.37.0 --- app/utils/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/git.py b/app/utils/git.py index ef022b1..02823a4 100644 --- a/app/utils/git.py +++ b/app/utils/git.py @@ -4,7 +4,7 @@ from app.utils.command import run from app.utils.version import Version -MIN_GIT_VERSION = Version(2, 25, 0) +MIN_GIT_VERSION = Version(2, 37, 0) def init() -> None: From 41d843cc7d1be6677c227987a8fb40c554492811 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:21:08 +0800 Subject: [PATCH 3/4] chore: change to minimum version 2.28.0 --- app/utils/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/git.py b/app/utils/git.py index 02823a4..f0df195 100644 --- a/app/utils/git.py +++ b/app/utils/git.py @@ -4,7 +4,7 @@ from app.utils.command import run from app.utils.version import Version -MIN_GIT_VERSION = Version(2, 37, 0) +MIN_GIT_VERSION = Version(2, 28, 0) def init() -> None: From 34cecb95f8dfa71cc194c151bbe85bbf3762940d Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:33:37 +0800 Subject: [PATCH 4/4] chore: address comments from copilot --- app/utils/git.py | 2 +- app/utils/version.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/utils/git.py b/app/utils/git.py index f0df195..9c72035 100644 --- a/app/utils/git.py +++ b/app/utils/git.py @@ -38,7 +38,7 @@ def get_git_version() -> Optional[Version]: if not result.is_success(): return None - match = re.search(r"(\d+\.\d+\.\d+)", result.stdout) + match = re.search(r"^git version (\d+\.\d+\.\d+)", result.stdout) if not match: return None diff --git a/app/utils/version.py b/app/utils/version.py index c6a97b5..3ede378 100644 --- a/app/utils/version.py +++ b/app/utils/version.py @@ -17,8 +17,18 @@ def parse_version_string(version: str) -> "Version": @staticmethod def parse(version: str) -> "Version": """Parse a plain version string (e.g., '1.2.3').""" - [major, minor, patch] = version.split(".") - return Version(int(major), int(minor), int(patch)) + 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."""