Skip to content

Commit 052ff5e

Browse files
committed
refactor: add version check decorator and update handling
- Add `check_version_before_command` decorator to `config`, `init`, `info`, `commit`, and `help` commands - Implement `check_version_update` utility function to check for new version and handle updates - Cache version check results to avoid frequent API calls - Provide option to update package if new version is available This refactor ensures that users are notified of new versions and can easily update the package, improving the user experience.
1 parent 479a503 commit 052ff5e

File tree

3 files changed

+135
-19
lines changed

3 files changed

+135
-19
lines changed

gcop/__main__.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import subprocess
33
from enum import Enum
4+
from functools import wraps
45
from pathlib import Path
56
from typing import Callable, Dict, List, Literal, Optional
67

@@ -15,6 +16,7 @@
1516

1617
from gcop import prompt, version
1718
from gcop.config import ModelConfig, gcop_config
19+
from gcop.utils import check_version_update
1820

1921
load_dotenv()
2022

@@ -90,7 +92,19 @@ def generate_commit_message(
9092
)
9193

9294

95+
def check_version_before_command(f: Callable) -> Callable:
96+
"""Decorator to check version before executing any command."""
97+
98+
@wraps(f)
99+
def wrapper(*args, **kwargs):
100+
check_version_update(console)
101+
return f(*args, **kwargs)
102+
103+
return wrapper
104+
105+
93106
@app.command(name="config")
107+
@check_version_before_command
94108
def config_command(from_init: bool = False):
95109
"""Open the config file in the default editor."""
96110
initial_content = (
@@ -112,6 +126,7 @@ def config_command(from_init: bool = False):
112126

113127

114128
@app.command(name="init")
129+
@check_version_before_command
115130
def init_command():
116131
"""Add command into git config"""
117132
try:
@@ -174,6 +189,7 @@ def init_command():
174189

175190

176191
@app.command(name="info")
192+
@check_version_before_command
177193
def info_command():
178194
"""Display detailed information about the current git repository."""
179195
try:
@@ -399,8 +415,14 @@ def info_command():
399415

400416

401417
@app.command(name="commit")
418+
@check_version_before_command
402419
def commit_command(
403-
instruction: Optional[str] = None, previous_commit_message: Optional[str] = None
420+
instruction: Optional[str] = typer.Option(
421+
None, help="Additional instruction for commit message generation"
422+
),
423+
previous_commit_message: Optional[str] = typer.Option(
424+
None, help="Previous commit message to refine"
425+
),
404426
):
405427
"""Generate a git commit message based on the staged changes and commit the
406428
changes.
@@ -410,11 +432,6 @@ def commit_command(
410432
select "retry". If you want to retry the commit message generation with new
411433
feedback, please select "retry by feedback". If you want to exit the commit
412434
process, please select "exit".
413-
414-
Args:
415-
instruction(Optional[str]): additional instruction. Defaults to None.
416-
previous_commit_message(Optional[str]): previous commit message. Defaults to
417-
None.
418435
"""
419436
diff: str = get_git_diff("--staged")
420437

@@ -451,19 +468,9 @@ def commit_command(
451468

452469
actions[response]()
453470

454-
# # request pypi to get the latest version
455-
# # TODO optimize logic, everyday check the latest version one time
456-
# response = requests.get("https://pypi.org/pypi/gcop/json")
457-
# latest_version = response.json()["info"]["version"]
458-
# if version != latest_version:
459-
# console.print(f"[bold]A new version of gcop is available: {latest_version}[/]") # noqa
460-
# console.print(f"[bold]Your current version: {version}[/]")
461-
# console.print(
462-
# "[bold]Please consider upgrading by running: pip install -U gcop[/]"
463-
# )
464-
465471

466472
@app.command(name="help")
473+
@check_version_before_command
467474
def help_command():
468475
"""Show help message"""
469476
help_message = """

gcop/utils/__init__.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
1+
import json
12
import os
3+
import subprocess
24
import tempfile
5+
from dataclasses import dataclass
6+
from datetime import datetime, timedelta
7+
from typing import Optional, Tuple
38

9+
import questionary
10+
import requests
411
import yaml
12+
from rich.console import Console
513

14+
from gcop import version
615

7-
def convert_backslashes(path: str):
16+
17+
@dataclass
18+
class VersionMetadata:
19+
"""Version metadata for caching version information."""
20+
21+
last_check: Optional[datetime] = None
22+
latest_version: Optional[str] = None
23+
24+
@classmethod
25+
def from_dict(cls, data: dict) -> "VersionMetadata":
26+
"""Create VersionMetadata from dictionary."""
27+
last_check = None
28+
if data.get("last_check"):
29+
try:
30+
last_check = datetime.fromisoformat(data["last_check"])
31+
except (ValueError, TypeError):
32+
last_check = None
33+
34+
return cls(last_check=last_check, latest_version=data.get("latest_version"))
35+
36+
def to_dict(self) -> dict:
37+
"""Convert to dictionary for JSON serialization."""
38+
return {
39+
"last_check": self.last_check.isoformat() if self.last_check else None,
40+
"latest_version": self.latest_version,
41+
}
42+
43+
44+
def convert_backslashes(path: str) -> str:
845
"""Convert all \\ to / of file path."""
946
return path.replace("\\", "/")
1047

@@ -54,3 +91,75 @@ def read_yaml(file_path: str) -> dict:
5491
with open(file_path, "r") as file:
5592
config = yaml.safe_load(file)
5693
return config
94+
95+
96+
def _load_metadata(metadata_path: str) -> VersionMetadata:
97+
"""Load version metadata from file or create new if not exists.
98+
99+
Args:
100+
metadata_path(str): Path to metadata file
101+
102+
Returns:
103+
VersionMetadata: Loaded or new metadata object
104+
"""
105+
if os.path.exists(metadata_path):
106+
try:
107+
with open(metadata_path, "r") as f:
108+
return VersionMetadata.from_dict(json.load(f))
109+
except (json.JSONDecodeError, IOError):
110+
# If file is corrupted, create new metadata
111+
pass
112+
113+
os.makedirs(os.path.dirname(metadata_path), exist_ok=True)
114+
115+
metadata = VersionMetadata()
116+
117+
with open(metadata_path, "w") as f:
118+
json.dump(metadata.to_dict(), f)
119+
120+
return metadata
121+
122+
123+
def check_version_update(console: Console) -> None:
124+
"""Check for new version of gcop using cached data.
125+
Only checks PyPI once per day and caches the result.
126+
127+
Args:
128+
console: Rich console instance for output
129+
"""
130+
metadata_path = os.path.join(get_default_storage_path(), "metadata.json")
131+
current_time = datetime.now()
132+
133+
metadata = _load_metadata(metadata_path)
134+
135+
if metadata.last_check and current_time - metadata.last_check <= timedelta(days=1):
136+
return
137+
138+
try:
139+
response = requests.get("https://pypi.org/pypi/gcop/json", timeout=1)
140+
latest_version = response.json()["info"]["version"]
141+
142+
metadata = VersionMetadata(
143+
last_check=current_time, latest_version=latest_version
144+
)
145+
with open(metadata_path, "w") as f:
146+
json.dump(metadata.to_dict(), f)
147+
148+
if latest_version != version:
149+
should_update = questionary.confirm(
150+
f"A new version of gcop is available: {latest_version} "
151+
f"(current: {version}). Would you like to update now?"
152+
).ask()
153+
154+
if should_update:
155+
try:
156+
console.print("[yellow]Updating gcop...[/]")
157+
subprocess.run(["pip", "install", "-U", "gcop"], check=True)
158+
console.print("[green]Update successful![/]")
159+
subprocess.run(["gcop", "init"], check=True)
160+
console.print("[green]GCOP reinitialized successfully![/]")
161+
except subprocess.CalledProcessError as e:
162+
console.print(f"[red]Failed to update gcop: {e}[/]")
163+
164+
except Exception:
165+
pass

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ requires = ["poetry-core"]
55

66
[tool.poetry]
77
name = "gcop"
8-
version = "1.5.1"
8+
version = "1.6.0"
99
description = "gcop is your git AI copilot."
1010
readme = "README.md"
1111
authors = ["gcop <zeeland4work@gmail.com>"]

0 commit comments

Comments
 (0)