fix(version): resolve version dynamically from git in dev checkouts
When running from a git repo, use setuptools-scm at runtime to derive the version from tags. Falls back to the static _version.py only when not in a git repo (e.g. installed from wheel). This ensures `markitect version` stays correct without requiring `pip install -e .` after every tag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,109 +1,146 @@
|
||||
"""
|
||||
Version information for MarkiTect.
|
||||
|
||||
This module provides version information using setuptools-scm.
|
||||
Version is automatically derived from git tags.
|
||||
Uses setuptools-scm for version derivation. When running from a dev
|
||||
checkout (git repo present), the version is resolved dynamically from
|
||||
git tags so it stays correct even without ``pip install -e .``.
|
||||
"""
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
# Fallback when _version.py is not available (e.g., during development without setuptools-scm)
|
||||
__version__ = "unknown"
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
_PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def _git_version() -> str | None:
|
||||
"""Derive version from git via setuptools-scm (runtime, no install needed)."""
|
||||
try:
|
||||
from setuptools_scm import get_version
|
||||
return get_version(root=str(_PROJECT_ROOT))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _static_version() -> str:
|
||||
"""Read the version baked into _version.py at install time."""
|
||||
try:
|
||||
from ._version import version
|
||||
return version
|
||||
except ImportError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _is_git_repo() -> bool:
|
||||
try:
|
||||
subprocess.check_output(
|
||||
["git", "rev-parse", "--git-dir"],
|
||||
cwd=_PROJECT_ROOT,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_version() -> str:
|
||||
"""Pick the best available version string.
|
||||
|
||||
In a git checkout, setuptools-scm gives the authoritative answer
|
||||
(reflects tags even before reinstall). Otherwise fall back to the
|
||||
static _version.py produced at install time.
|
||||
"""
|
||||
if _is_git_repo():
|
||||
v = _git_version()
|
||||
if v:
|
||||
return v
|
||||
return _static_version()
|
||||
|
||||
|
||||
__version__ = _resolve_version()
|
||||
|
||||
|
||||
def get_version():
|
||||
"""Get the current version string."""
|
||||
return __version__
|
||||
|
||||
def get_version_info():
|
||||
"""Get comprehensive version information by delegating to release-management capability."""
|
||||
try:
|
||||
# Delegate to release-management capability
|
||||
from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent
|
||||
|
||||
def _git_info() -> dict:
|
||||
"""Gather git metadata (commit, branch, tag)."""
|
||||
info: dict = {"is_git_repo": False}
|
||||
if not _is_git_repo():
|
||||
return info
|
||||
info["is_git_repo"] = True
|
||||
|
||||
def _run(*args: str) -> str | None:
|
||||
try:
|
||||
from release_management.utils.version import get_version_info as rm_get_version_info
|
||||
return rm_get_version_info(project_root)
|
||||
except ImportError:
|
||||
# Fallback if release-management capability is not available
|
||||
pass
|
||||
except Exception:
|
||||
return subprocess.check_output(
|
||||
["git", *args],
|
||||
cwd=_PROJECT_ROOT,
|
||||
stderr=subprocess.DEVNULL,
|
||||
).decode().strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
info["git_commit"] = _run("rev-parse", "--short", "HEAD") or "unknown"
|
||||
info["git_branch"] = _run("rev-parse", "--abbrev-ref", "HEAD") or "unknown"
|
||||
info["git_tag"] = _run("describe", "--tags", "--exact-match", "HEAD")
|
||||
return info
|
||||
|
||||
|
||||
def get_version_info():
|
||||
"""Get comprehensive version information."""
|
||||
try:
|
||||
from release_management.utils.version import get_version_info as rm_get_version_info
|
||||
return rm_get_version_info(_PROJECT_ROOT)
|
||||
except (ImportError, Exception):
|
||||
pass
|
||||
|
||||
# Simple fallback implementation
|
||||
try:
|
||||
from ._version import version_tuple, commit_id
|
||||
except ImportError:
|
||||
version_tuple = ("unknown",)
|
||||
commit_id = "unknown"
|
||||
git = _git_info()
|
||||
is_dev = ".dev" in __version__
|
||||
|
||||
return {
|
||||
'full_version': __version__,
|
||||
'short_version': __version__.split('.dev')[0] if '.dev' in __version__ else __version__,
|
||||
'version_tuple': version_tuple,
|
||||
'commit_id': commit_id,
|
||||
'is_dev': '.dev' in __version__,
|
||||
'git_commit': commit_id,
|
||||
'git_branch': 'unknown',
|
||||
'is_git_repo': False
|
||||
"full_version": __version__,
|
||||
"short_version": __version__.split(".dev")[0] if is_dev else __version__,
|
||||
"is_dev": is_dev,
|
||||
"is_git_repo": git.get("is_git_repo", False),
|
||||
"git_commit": git.get("git_commit", "unknown"),
|
||||
"git_branch": git.get("git_branch", "unknown"),
|
||||
"git_tag": git.get("git_tag"),
|
||||
}
|
||||
|
||||
|
||||
def _normalize_release_info(raw):
|
||||
"""Ensure release info dict has the keys the CLI release command expects."""
|
||||
if 'full_version' in raw:
|
||||
return raw # already in expected format
|
||||
if "full_version" in raw:
|
||||
return raw
|
||||
|
||||
import subprocess
|
||||
|
||||
version = raw.get('version', 'unknown')
|
||||
is_dev = raw.get('is_development', '.dev' in version)
|
||||
commit = raw.get('git_commit', 'unknown')
|
||||
|
||||
# Detect git repo and current tag
|
||||
is_git_repo = False
|
||||
git_tag = None
|
||||
try:
|
||||
subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
|
||||
is_git_repo = True
|
||||
tag_out = subprocess.check_output(
|
||||
['git', 'describe', '--tags', '--exact-match', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL,
|
||||
).decode().strip()
|
||||
if tag_out:
|
||||
git_tag = tag_out
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
version = raw.get("version", "unknown")
|
||||
is_dev = raw.get("is_development", ".dev" in version)
|
||||
commit = raw.get("git_commit", "unknown")
|
||||
git = _git_info()
|
||||
|
||||
return {
|
||||
'full_version': version,
|
||||
'release_type': 'development' if is_dev else 'release',
|
||||
'build_from': 'git' if is_git_repo else 'source',
|
||||
'commit': commit,
|
||||
'clean_build': not is_dev,
|
||||
'is_git_repo': is_git_repo,
|
||||
'git_tag': git_tag,
|
||||
"full_version": version,
|
||||
"release_type": "development" if is_dev else "release",
|
||||
"build_from": "git" if git.get("is_git_repo") else "source",
|
||||
"commit": commit,
|
||||
"clean_build": not is_dev,
|
||||
"is_git_repo": git.get("is_git_repo", False),
|
||||
"git_tag": git.get("git_tag"),
|
||||
}
|
||||
|
||||
|
||||
def get_release_info():
|
||||
"""Get release information by delegating to release-management capability."""
|
||||
"""Get release information."""
|
||||
try:
|
||||
from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent
|
||||
|
||||
try:
|
||||
from release_management.utils.version import get_release_info as rm_get_release_info
|
||||
return _normalize_release_info(rm_get_release_info(project_root))
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
from release_management.utils.version import get_release_info as rm_get_release_info
|
||||
return _normalize_release_info(rm_get_release_info(_PROJECT_ROOT))
|
||||
except (ImportError, Exception):
|
||||
pass
|
||||
|
||||
# Fallback — build from version_info directly
|
||||
version_info = get_version_info()
|
||||
info = get_version_info()
|
||||
return _normalize_release_info({
|
||||
'version': version_info['full_version'],
|
||||
'is_development': version_info['is_dev'],
|
||||
'git_commit': version_info.get('git_commit', 'unknown'),
|
||||
})
|
||||
"version": info["full_version"],
|
||||
"is_development": info["is_dev"],
|
||||
"git_commit": info.get("git_commit", "unknown"),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user