mirror of
https://github.com/trailofbits/dropkit
synced 2026-06-21 14:11:54 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""Version checking functionality for dropkit."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from dropkit import __version__
|
||||
from dropkit.config import Config
|
||||
|
||||
|
||||
def get_last_check_file() -> Path:
|
||||
"""Get the path to the last version check file."""
|
||||
return Config.get_config_dir() / ".last_version_check"
|
||||
|
||||
|
||||
def should_check_version() -> bool:
|
||||
"""
|
||||
Check if we should check for a new version.
|
||||
|
||||
Only checks once per day to avoid slowing down commands.
|
||||
"""
|
||||
check_file = get_last_check_file()
|
||||
|
||||
if not check_file.exists():
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(check_file) as f:
|
||||
data = json.load(f)
|
||||
last_check = data.get("timestamp", 0)
|
||||
# Check if more than 24 hours have passed
|
||||
return (time.time() - last_check) > 86400 # 24 hours in seconds
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return True
|
||||
|
||||
|
||||
def update_last_check_time() -> None:
|
||||
"""Update the last version check timestamp."""
|
||||
check_file = get_last_check_file()
|
||||
check_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
data = {"timestamp": time.time(), "current_version": __version__}
|
||||
|
||||
try:
|
||||
with open(check_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
except OSError:
|
||||
# Silently fail if we can't write the file
|
||||
pass
|
||||
|
||||
|
||||
def get_latest_git_commit() -> str | None:
|
||||
"""
|
||||
Get the latest git commit hash from the main branch.
|
||||
|
||||
Returns:
|
||||
Latest commit hash (short, 7 chars) or None if unable to fetch
|
||||
"""
|
||||
try:
|
||||
# Get the latest commit from main branch
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"ls-remote",
|
||||
"https://github.com/trailofbits/dropkit.git",
|
||||
"HEAD",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
# Parse the output to get the commit hash
|
||||
# Format: "commit_hash\tHEAD"
|
||||
lines = result.stdout.strip().split("\n")
|
||||
if not lines or not lines[0]:
|
||||
return None
|
||||
|
||||
# Extract commit hash (first column)
|
||||
commit_hash = lines[0].split("\t")[0]
|
||||
# Return short hash (first 7 chars)
|
||||
return commit_hash[:7] if commit_hash else None
|
||||
|
||||
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def extract_commit_from_version(version: str) -> str | None:
|
||||
"""
|
||||
Extract commit hash from version string.
|
||||
|
||||
Version format: "0.1.0+git.a1b2c3d" or similar
|
||||
|
||||
Args:
|
||||
version: Version string potentially containing a commit hash
|
||||
|
||||
Returns:
|
||||
Commit hash (short) or None if not found
|
||||
"""
|
||||
# Version format from hatchling-vcs: "0.1.0+git.a1b2c3d" or "0.1.0.dev123+a1b2c3d"
|
||||
if "+git." in version:
|
||||
# Format: "0.1.0+git.a1b2c3d"
|
||||
parts = version.split("+git.")
|
||||
if len(parts) == 2:
|
||||
return parts[1][:7] # Return first 7 chars
|
||||
elif "+" in version:
|
||||
# Format: "0.1.0+a1b2c3d" or similar
|
||||
parts = version.split("+")
|
||||
if len(parts) == 2:
|
||||
commit = parts[1]
|
||||
# Extract hash if it contains other info
|
||||
if "." in commit:
|
||||
commit = commit.split(".")[-1]
|
||||
return commit[:7]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def commits_differ(current_version: str, latest_commit: str) -> bool:
|
||||
"""
|
||||
Check if current version's commit differs from latest commit.
|
||||
|
||||
Args:
|
||||
current_version: Current version string (e.g., "0.1.0+git.a1b2c3d")
|
||||
latest_commit: Latest commit hash from remote
|
||||
|
||||
Returns:
|
||||
True if commits differ (update available), False otherwise
|
||||
"""
|
||||
current_commit = extract_commit_from_version(current_version)
|
||||
|
||||
if not current_commit:
|
||||
# Can't determine current commit, don't show update
|
||||
return False
|
||||
|
||||
# Compare commit hashes (case-insensitive)
|
||||
return current_commit.lower() != latest_commit.lower()
|
||||
|
||||
|
||||
def check_for_updates() -> None:
|
||||
"""
|
||||
Check for updates and display a message if a new version is available.
|
||||
|
||||
This function:
|
||||
- Skips check in development mode (version == "dev")
|
||||
- Only runs once per day for installed versions
|
||||
- Fetches the latest git commit from main branch
|
||||
- Compares with current version's commit
|
||||
- Shows a non-blocking message if commits differ
|
||||
"""
|
||||
# Skip check in development mode
|
||||
if __version__ == "dev" or "dev" in __version__.lower():
|
||||
return
|
||||
|
||||
# Only check once per day
|
||||
if not should_check_version():
|
||||
return
|
||||
|
||||
# Update the check time regardless of success
|
||||
update_last_check_time()
|
||||
|
||||
# Try to get latest commit
|
||||
latest_commit = get_latest_git_commit()
|
||||
|
||||
if not latest_commit:
|
||||
# Silently fail if we can't check
|
||||
return
|
||||
|
||||
# Compare commits
|
||||
if commits_differ(__version__, latest_commit):
|
||||
# Import here to avoid circular dependency
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
current_commit = extract_commit_from_version(__version__)
|
||||
console.print(
|
||||
f"\n[yellow]New version available:[/yellow] [cyan]{latest_commit}[/cyan] "
|
||||
f"[dim](current: {current_commit or __version__})[/dim]"
|
||||
)
|
||||
console.print(
|
||||
"[yellow]Run[/yellow] [cyan]uv tool upgrade dropkit[/cyan] [yellow]to update[/yellow]\n"
|
||||
)
|
||||
Reference in New Issue
Block a user