version_helpers.pyā¢5.99 kB
"""Version management helpers for service installations."""
import subprocess
import logging
from typing import List, Optional, Tuple
from pathlib import Path
import re
logger = logging.getLogger("voicemode")
def get_git_tags(repo_url: str) -> List[str]:
"""Fetch git tags from a repository URL."""
try:
# Use git ls-remote to get tags without cloning
result = subprocess.run(
["git", "ls-remote", "--tags", repo_url],
capture_output=True,
text=True,
check=True
)
tags = []
for line in result.stdout.strip().split('\n'):
if line:
# Extract tag name from refs/tags/tagname
parts = line.split('\t')
if len(parts) == 2 and 'refs/tags/' in parts[1]:
tag = parts[1].replace('refs/tags/', '')
# Skip annotated tag references (ending with ^{})
if not tag.endswith('^{}'):
tags.append(tag)
return sorted(tags, key=parse_version, reverse=True)
except subprocess.CalledProcessError as e:
logger.error(f"Failed to fetch tags from {repo_url}: {e}")
return []
def parse_version(version: str) -> Tuple:
"""Parse version string for sorting.
Handles versions like v1.2.3, v1.2.3-pre, v1.2.3-rc1, etc.
Returns tuple for proper sorting.
"""
# Remove 'v' prefix if present
if version.startswith('v'):
version = version[1:]
# Split on dash for pre-release info
parts = version.split('-', 1)
main_version = parts[0]
pre_release = parts[1] if len(parts) > 1 else None
# Parse main version
try:
version_parts = [int(x) for x in main_version.split('.')]
except ValueError:
# Handle versions with non-numeric parts
version_parts = []
for part in main_version.split('.'):
try:
version_parts.append(int(part))
except ValueError:
# Convert non-numeric parts to 0 for consistent comparison
version_parts.append(0)
# Pad version parts to ensure consistent comparison
while len(version_parts) < 3:
version_parts.append(0)
# Handle pre-release sorting
# Stable releases come after pre-releases
if pre_release is None:
pre_release_order = (1, '') # Stable
else:
# Extract pre-release type and number
match = re.match(r'(alpha|beta|rc|pre)(\d*)', pre_release)
if match:
pre_type, pre_num = match.groups()
pre_num = int(pre_num) if pre_num else 0
# Order: alpha < beta < rc/pre < stable
type_order = {'alpha': 0, 'beta': 1, 'rc': 2, 'pre': 2}
pre_release_order = (0, type_order.get(pre_type, 3), pre_num)
else:
pre_release_order = (0, 4, pre_release)
return tuple(version_parts + [pre_release_order])
def get_latest_stable_tag(tags: List[str]) -> Optional[str]:
"""Get the latest stable release tag (no pre-release suffixes)."""
for tag in tags:
# Skip pre-release versions
if not any(suffix in tag for suffix in ['-pre', '-rc', '-alpha', '-beta', 'post']):
return tag
return tags[0] if tags else None
def get_current_version(install_dir: Path) -> Optional[str]:
"""Get the currently installed version from a git repository."""
if not install_dir.exists() or not (install_dir / '.git').exists():
return None
try:
# Try git describe first (shows tag + commits since tag)
result = subprocess.run(
["git", "describe", "--tags", "--always"],
cwd=install_dir,
capture_output=True,
text=True,
check=True
)
version = result.stdout.strip()
# If we got a commit hash only, try to get the exact tag
if re.match(r'^[0-9a-f]+$', version):
result = subprocess.run(
["git", "describe", "--tags", "--exact-match"],
cwd=install_dir,
capture_output=True,
text=True
)
if result.returncode == 0:
version = result.stdout.strip()
return version
except subprocess.CalledProcessError:
# Fallback to commit hash
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd=install_dir,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
def checkout_version(install_dir: Path, version: str) -> bool:
"""Checkout a specific version in a git repository."""
try:
# Fetch latest tags
subprocess.run(
["git", "fetch", "--tags"],
cwd=install_dir,
check=True,
capture_output=True
)
# Checkout the version
subprocess.run(
["git", "checkout", version],
cwd=install_dir,
check=True,
capture_output=True
)
return True
except subprocess.CalledProcessError as e:
logger.error(f"Failed to checkout version {version}: {e}")
return False
def is_version_installed(install_dir: Path, version: str) -> bool:
"""Check if a specific version is currently installed."""
current = get_current_version(install_dir)
if not current:
return False
# Handle exact matches
if current == version:
return True
# Handle cases where current is "v1.2.3-4-gabcdef" and version is "v1.2.3"
if '-' in current:
base_version = current.split('-')[0]
if base_version == version:
return True
return False