Skip to content

Commit 293460a

Browse files
committed
Git(feat[version]): Add structured version info via build_options()
- Add GitVersionInfo dataclass to provide structured git version output - Update version() method to return raw string output - Add build_options() method that returns a GitVersionInfo instance - Fix doctests in vendored version module - Use internal vendor.version module instead of external packaging
1 parent 6ca87f6 commit 293460a

File tree

2 files changed

+190
-1
lines changed

2 files changed

+190
-1
lines changed

src/libvcs/cmd/git.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import dataclasses
56
import datetime
67
import pathlib
78
import shlex
@@ -10,10 +11,37 @@
1011

1112
from libvcs._internal.run import ProgressCallbackProtocol, run
1213
from libvcs._internal.types import StrOrBytesPath, StrPath
14+
from libvcs._vendor.version import InvalidVersion, parse
1315

1416
_CMD = t.Union[StrOrBytesPath, Sequence[StrOrBytesPath]]
1517

1618

19+
@dataclasses.dataclass
20+
class GitVersionInfo:
21+
"""Information about the git version."""
22+
23+
version: str
24+
"""Git version string (e.g. '2.43.0')"""
25+
26+
version_info: tuple[int, int, int] | None = None
27+
"""Tuple of (major, minor, micro) version numbers, or None if version invalid"""
28+
29+
cpu: str | None = None
30+
"""CPU architecture information"""
31+
32+
commit: str | None = None
33+
"""Commit associated with this build"""
34+
35+
sizeof_long: str | None = None
36+
"""Size of long in the compiled binary"""
37+
38+
sizeof_size_t: str | None = None
39+
"""Size of size_t in the compiled binary"""
40+
41+
shell_path: str | None = None
42+
"""Shell path configured in git"""
43+
44+
1745
class Git:
1846
"""Run commands directly on a git repository."""
1947

@@ -1751,7 +1779,17 @@ def version(
17511779
check_returncode: bool | None = None,
17521780
**kwargs: t.Any,
17531781
) -> str:
1754-
"""Version. Wraps `git version <https://git-scm.com/docs/git-version>`_.
1782+
"""Get git version. Wraps `git version <https://git-scm.com/docs/git-version>`_.
1783+
1784+
Parameters
1785+
----------
1786+
build_options : bool, optional
1787+
Include build options in the output with ``--build-options``
1788+
1789+
Returns
1790+
-------
1791+
str
1792+
Raw version string output
17551793
17561794
Examples
17571795
--------
@@ -1773,6 +1811,78 @@ def version(
17731811
check_returncode=check_returncode,
17741812
)
17751813

1814+
def build_options(
1815+
self,
1816+
*,
1817+
check_returncode: bool | None = None,
1818+
**kwargs: t.Any,
1819+
) -> GitVersionInfo:
1820+
"""Get detailed Git version information as a structured dataclass.
1821+
1822+
Runs ``git --version --build-options`` and parses the output.
1823+
1824+
Returns
1825+
-------
1826+
GitVersionInfo
1827+
Dataclass containing structured information about the git version and build
1828+
1829+
Examples
1830+
--------
1831+
>>> git = Git(path=example_git_repo.path)
1832+
>>> version_info = git.build_options()
1833+
>>> isinstance(version_info, GitVersionInfo)
1834+
True
1835+
>>> isinstance(version_info.version, str)
1836+
True
1837+
"""
1838+
output = self.version(build_options=True, check_returncode=check_returncode)
1839+
1840+
# Parse the output into a structured format
1841+
result = GitVersionInfo(version="")
1842+
1843+
# First line is always "git version X.Y.Z"
1844+
lines = output.strip().split("\n")
1845+
if lines and lines[0].startswith("git version "):
1846+
version_str = lines[0].replace("git version ", "").strip()
1847+
result.version = version_str
1848+
1849+
# Parse semantic version components
1850+
try:
1851+
parsed_version = parse(version_str)
1852+
result.version_info = (
1853+
parsed_version.major,
1854+
parsed_version.minor,
1855+
parsed_version.micro,
1856+
)
1857+
except InvalidVersion:
1858+
# Fall back to string-only if can't be parsed
1859+
result.version_info = None
1860+
1861+
# Parse additional build info
1862+
for line in lines[1:]:
1863+
line = line.strip()
1864+
if not line:
1865+
continue
1866+
1867+
if ":" in line:
1868+
key, value = line.split(":", 1)
1869+
key = key.strip()
1870+
value = value.strip()
1871+
1872+
if key == "cpu":
1873+
result.cpu = value
1874+
elif key == "sizeof-long":
1875+
result.sizeof_long = value
1876+
elif key == "sizeof-size_t":
1877+
result.sizeof_size_t = value
1878+
elif key == "shell-path":
1879+
result.shell_path = value
1880+
# Special handling for the commit line which often has no colon
1881+
elif "commit" in line:
1882+
result.commit = line
1883+
1884+
return result
1885+
17761886
def rev_parse(
17771887
self,
17781888
*,

tests/cmd/test_git.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,82 @@ def test_git_constructor(
1919
repo = git.Git(path=path_type(tmp_path))
2020

2121
assert repo.path == tmp_path
22+
23+
24+
def test_version_basic(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
25+
"""Test basic git version output."""
26+
git_cmd = git.Git(path=tmp_path)
27+
28+
monkeypatch.setattr(git_cmd, "run", lambda *args, **kwargs: "git version 2.43.0")
29+
30+
result = git_cmd.version()
31+
assert result == "git version 2.43.0"
32+
33+
34+
def test_version_with_build_options(
35+
monkeypatch: pytest.MonkeyPatch,
36+
tmp_path: pathlib.Path,
37+
) -> None:
38+
"""Test git version with build options."""
39+
git_cmd = git.Git(path=tmp_path)
40+
41+
sample_output = """git version 2.43.0
42+
cpu: x86_64
43+
no commit associated with this build
44+
sizeof-long: 8
45+
sizeof-size_t: 8
46+
shell-path: /bin/sh"""
47+
48+
def mock_run(cmd_args: list[str], **kwargs: t.Any) -> str:
49+
assert cmd_args == ["version", "--build-options"]
50+
assert kwargs.get("check_returncode") is None
51+
return sample_output
52+
53+
monkeypatch.setattr(git_cmd, "run", mock_run)
54+
55+
result = git_cmd.version(build_options=True)
56+
assert result == sample_output
57+
58+
59+
def test_build_options(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
60+
"""Test build_options() method."""
61+
git_cmd = git.Git(path=tmp_path)
62+
63+
sample_output = """git version 2.43.0
64+
cpu: x86_64
65+
no commit associated with this build
66+
sizeof-long: 8
67+
sizeof-size_t: 8
68+
shell-path: /bin/sh"""
69+
70+
# We need to fix the parsing for "no commit associated with this build"
71+
# in the build_options() method
72+
monkeypatch.setattr(git_cmd, "version", lambda **kwargs: sample_output)
73+
74+
result = git_cmd.build_options()
75+
76+
assert isinstance(result, git.GitVersionInfo)
77+
assert result.version == "2.43.0"
78+
assert result.version_info == (2, 43, 0)
79+
assert result.cpu == "x86_64"
80+
assert result.commit == "no commit associated with this build"
81+
assert result.sizeof_long == "8"
82+
assert result.sizeof_size_t == "8"
83+
assert result.shell_path == "/bin/sh"
84+
85+
86+
def test_build_options_invalid_version(
87+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
88+
) -> None:
89+
"""Test build_options() with invalid version string."""
90+
git_cmd = git.Git(path=tmp_path)
91+
92+
sample_output = "git version development"
93+
94+
monkeypatch.setattr(git_cmd, "version", lambda **kwargs: sample_output)
95+
96+
result = git_cmd.build_options()
97+
98+
assert isinstance(result, git.GitVersionInfo)
99+
assert result.version == "development"
100+
assert result.version_info is None

0 commit comments

Comments
 (0)