Skip to content

Commit d31ac87

Browse files
committed
refactor: replace rich.console with custom logger
- Replace instances of 'rich.console.Console' with 'gcop.utils.logger.Logger' - Add color-coded logging for better visual distinction - Update functions to use the new logger for output messages This refactor improves the consistency and readability of the output messages, and introduces color-coded logging for better visual distinction.
1 parent b038456 commit d31ac87

File tree

5 files changed

+154
-53
lines changed

5 files changed

+154
-53
lines changed

docs/guide/how-to-guide.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ Remember:
3232

3333
1. Install GCOP in each Python environment you want to use it in.
3434
2. The GCOP configuration is shared across all environments, so you only need to set it up once.
35+
36+
### How to see the logs?
37+
38+
GCOP will store the logs in the `logs` folder in the GCOP storage path, which is usually `~/.zeeland/gcop/logs/`.

gcop/__main__.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
import typer
1313
from dotenv import load_dotenv
1414
from pydantic import BaseModel, Field
15-
from rich.console import Console
1615

1716
from gcop import prompt, version
1817
from gcop.config import ModelConfig, get_config
1918
from gcop.utils import check_version_update, migrate_config_if_needed
19+
from gcop.utils.logger import Color, logger
2020

2121
load_dotenv()
2222

@@ -25,7 +25,6 @@
2525
help="gcop is your local git command copilot",
2626
add_completion=False,
2727
)
28-
console = Console()
2928

3029

3130
class CommitMessage(BaseModel):
@@ -99,7 +98,7 @@ def check_version_before_command(f: Callable) -> Callable:
9998

10099
@wraps(f)
101100
def wrapper(*args, **kwargs):
102-
check_version_update(console)
101+
check_version_update()
103102
return f(*args, **kwargs)
104103

105104
return wrapper
@@ -199,10 +198,10 @@ def init_command():
199198
check=True,
200199
encoding="utf-8",
201200
)
202-
console.print("[green]git aliases added successfully[/]")
201+
logger.color_info("git aliases added successfully", color=Color.GREEN)
203202

204203
config_command(from_init=True)
205-
console.print("[green]gcop initialized successfully[/]")
204+
logger.color_info("gcop initialized successfully", color=Color.GREEN)
206205
except subprocess.CalledProcessError as error:
207206
print(f"Error adding git aliases: {error}")
208207

@@ -409,28 +408,28 @@ def info_command():
409408
shell=True,
410409
).strip()
411410

412-
console.print(f"[bold]Project Name:[/] {project_name}")
413-
console.print(f"[bold]Current Branch:[/] {current_branch}")
414-
console.print(f"[bold]Latest Commit:[/] {latest_commit}")
415-
console.print(f"[bold]Uncommitted Changes:[/] {uncommitted_changes}")
416-
console.print(f"[bold]Remote URL:[/] {remote_url}")
417-
console.print(f"[bold]Total Commits:[/] {total_commits}")
418-
console.print(f"[bold]Contributors:[/] {contributors}")
419-
console.print(f"[bold]Repository Created:[/] {creation_time}")
420-
console.print(f"[bold]Last Modified:[/] {last_modified}")
421-
console.print(f"[bold]Repository Size:[/] {repo_size}")
422-
console.print(f"[bold]Most Active Contributor:[/] {most_active}")
423-
console.print(f"[bold]Most Changed File:[/] {most_changed}")
424-
console.print(f"[bold]Line Count by Language:[/]\n{line_count}")
425-
console.print(f"[bold]Latest Tag:[/] {latest_tag}")
426-
console.print(f"[bold]Branch Count:[/] {branch_count}")
427-
console.print(f"[bold]Untracked Files:[/] {untracked_count}")
428-
console.print(f"[bold]Submodules:[/]{submodules}")
429-
console.print(f"[bold]Latest Merge Commit:[/] {latest_merge}")
430-
console.print(f"[bold]File Type Statistics:[/]\n{file_types}")
411+
logger.color_info(f"Project Name: {project_name}")
412+
logger.color_info(f"Current Branch: {current_branch}")
413+
logger.color_info(f"Latest Commit: {latest_commit}")
414+
logger.color_info(f"Uncommitted Changes: {uncommitted_changes}")
415+
logger.color_info(f"Remote URL: {remote_url}")
416+
logger.color_info(f"Total Commits: {total_commits}")
417+
logger.color_info(f"Contributors: {contributors}")
418+
logger.color_info(f"Repository Created: {creation_time}")
419+
logger.color_info(f"Last Modified: {last_modified}")
420+
logger.color_info(f"Repository Size: {repo_size}")
421+
logger.color_info(f"Most Active Contributor: {most_active}")
422+
logger.color_info(f"Most Changed File: {most_changed}")
423+
logger.color_info(f"Line Count by Language:\n{line_count}")
424+
logger.color_info(f"Latest Tag: {latest_tag}")
425+
logger.color_info(f"Branch Count: {branch_count}")
426+
logger.color_info(f"Untracked Files: {untracked_count}")
427+
logger.color_info(f"Submodules: {submodules}")
428+
logger.color_info(f"Latest Merge Commit: {latest_merge}")
429+
logger.color_info(f"File Type Statistics:\n{file_types}")
431430

432431
except subprocess.CalledProcessError as e:
433-
console.print(f"[red]Error getting repository information: {e}[/]")
432+
logger.color_info(f"Error getting repository information: {e}", color=Color.RED)
434433

435434

436435
@app.command(name="commit")
@@ -455,18 +454,20 @@ def commit_command(
455454
diff: str = get_git_diff("--staged")
456455

457456
if not diff:
458-
console.print("[yellow]No staged changes[/]")
457+
logger.color_info("No staged changes", color=Color.YELLOW)
459458
return
460459

461-
console.print(f"[yellow][Code diff] \n{diff}[/]")
462-
console.print("[bold][On Ready] Generating commit message... [/]")
460+
logger.color_info(f"[Code diff] \n{diff}", color=Color.YELLOW)
461+
logger.color_info("[On Ready] Generating commit message...")
463462

464463
commit_messages: CommitMessage = generate_commit_message(
465464
diff, instruction, previous_commit_message
466465
)
467466

468-
console.print(f"[bold][Thought] {commit_messages.thought}[/]")
469-
console.print(f"[green][Generated commit message]\n{commit_messages.content}[/]")
467+
logger.color_info(f"[Thought] {commit_messages.thought}")
468+
logger.color_info(
469+
f"[Generated commit message]\n{commit_messages.content}", color=Color.GREEN
470+
)
470471

471472
actions: Dict[str, Callable] = {
472473
"yes": lambda: subprocess.run(["git", "commit", "-m", commit_messages.content]),
@@ -477,7 +478,9 @@ def commit_command(
477478
instruction=questionary.text("Please enter your feedback:").ask(),
478479
previous_commit_message=commit_messages.content,
479480
),
480-
"exit": lambda: console.print("[yellow]Exiting commit process.[/]"),
481+
"exit": lambda: logger.color_info(
482+
"Exiting commit process.", color=Color.YELLOW
483+
),
481484
}
482485

483486
response = questionary.select(
@@ -492,14 +495,14 @@ def commit_command(
492495
@check_version_before_command
493496
def help_command():
494497
"""Show help message"""
495-
help_message = """
496-
[bold]gcop[/] is your local git command copilot
497-
[bold]Version: [/]{version}
498-
[bold]GitHub: https://github.com/Undertone0809/gcop[/]
498+
help_message = f"""
499+
gcop is your local git command copilot
500+
Version: {version}
501+
GitHub: https://github.com/Undertone0809/gcop
499502
500-
[bold]Usage: gcop [OPTIONS] COMMAND[/]
503+
Usage: gcop [OPTIONS] COMMAND
501504
502-
[bold]Commands:
505+
Commands:
503506
git p Push the changes to the remote repository
504507
git pf Push the changes to the remote repository with force
505508
git undo Undo the last commit but keep the file changes
@@ -514,7 +517,7 @@ def help_command():
514517
git info Display basic information about the current git repository
515518
""" # noqa
516519

517-
console.print(help_message)
520+
logger.color_info(help_message)
518521

519522

520523
if __name__ == "__main__":

gcop/utils/__init__.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from zeeland import get_default_storage_path as _get_default_storage_path
1515

1616
from gcop import version
17+
from gcop.utils.logger import Color, logger
1718

1819

1920
@dataclass
@@ -126,12 +127,9 @@ def _load_metadata(metadata_path: str) -> VersionMetadata:
126127
return metadata
127128

128129

129-
def check_version_update(console: Console) -> None:
130+
def check_version_update() -> None:
130131
"""Check for new version of gcop using cached data.
131132
Only checks PyPI once per day and caches the result.
132-
133-
Args:
134-
console: Rich console instance for output
135133
"""
136134
metadata_path: str = os.path.join(get_default_storage_path(), "metadata.json")
137135
current_time: datetime = datetime.now()
@@ -159,13 +157,15 @@ def check_version_update(console: Console) -> None:
159157

160158
if should_update:
161159
try:
162-
console.print("[yellow]Updating gcop...[/]")
160+
logger.color_info("Updating gcop...", color=Color.YELLOW)
163161
subprocess.run(["pip", "install", "-U", "gcop"], check=True)
164-
console.print("[green]Update successful![/]")
162+
logger.color_info("Update successful!", color=Color.GREEN)
165163
subprocess.run(["gcop", "init"], check=True)
166-
console.print("[green]GCOP reinitialized successfully![/]")
164+
logger.color_info(
165+
"GCOP reinitialized successfully!", color=Color.GREEN
166+
)
167167
except subprocess.CalledProcessError as e:
168-
console.print(f"[red]Failed to update gcop: {e}[/]")
168+
logger.color_info(f"Failed to update gcop: {e}", color=Color.RED)
169169

170170
except Exception:
171171
pass
@@ -181,17 +181,21 @@ def migrate_config_if_needed() -> None:
181181

182182
try:
183183
if not os.path.exists(new_config_path):
184-
print("No new config file found, migrating old config...")
184+
logger.color_info("No new config file found, migrating old config...")
185185
shutil.copy2(old_config_path, new_config_path)
186-
print(f"Config migrated from {old_config_path} to {new_config_path}")
186+
logger.color_info(
187+
f"Config migrated from {old_config_path} to {new_config_path}"
188+
)
187189
else:
188-
print("New config file already exists, skipping migration")
190+
logger.color_info("New config file already exists, skipping migration")
189191

190192
backup_path: str = old_config_path + ".backup"
191193
shutil.copy2(old_config_path, backup_path)
192194
os.remove(old_config_path)
193-
print(f"Old config backup created at {backup_path}")
195+
logger.color_info(f"Old config backup created at {backup_path}")
194196

195197
except Exception as e:
196-
print(f"Error migrating config: {e}")
197-
print("Please manually move your config file to the new location")
198+
logger.color_info(f"Error migrating config: {e}", color=Color.RED)
199+
logger.color_info(
200+
"Please manually move your config file to the new location", color=Color.RED
201+
)

gcop/utils/logger.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import datetime
2+
import logging
3+
import sys
4+
import traceback
5+
from enum import Enum
6+
from logging.handlers import TimedRotatingFileHandler
7+
from pathlib import Path
8+
9+
from rich.console import Console
10+
from zeeland import Singleton, get_default_storage_path
11+
12+
13+
class Color(Enum):
14+
DEFAULT = "default"
15+
GREEN = "green"
16+
YELLOW = "yellow"
17+
RED = "red"
18+
19+
20+
class Logger(logging.Logger, metaclass=Singleton):
21+
"""A custom logger class that extends logging.Logger with color output
22+
capabilities."""
23+
24+
def __init__(self, name: str = "gcop", level: int = logging.DEBUG) -> None:
25+
"""Initialize the logger with file and console handlers.
26+
27+
Args:
28+
name: Logger name, defaults to "gcop"
29+
level: Logging level, defaults to DEBUG
30+
"""
31+
super().__init__(name, level)
32+
self._setup_file_handler()
33+
self.console = Console()
34+
35+
def _setup_file_handler(self) -> None:
36+
"""Set up rotating file handler with formatting."""
37+
log_dir = Path(get_default_storage_path("gcop", "logs"))
38+
log_file = log_dir / f"{datetime.datetime.now().strftime('%Y%m%d')}.log"
39+
40+
handler = TimedRotatingFileHandler(
41+
filename=log_file, when="midnight", interval=1, encoding="utf-8"
42+
)
43+
handler.setLevel(logging.DEBUG)
44+
45+
formatter = logging.Formatter(
46+
"%(asctime)s | %(levelname)s | %(name)s:%(funcName)s:%(lineno)d - %(message)s", # noqa
47+
"%Y-%m-%d %H:%M:%S",
48+
)
49+
handler.setFormatter(formatter)
50+
self.addHandler(handler)
51+
52+
def color_info(
53+
self, message: str, color: Color = Color.DEFAULT, *args, **kwargs
54+
) -> None:
55+
"""Log info message and print to console with optional color.
56+
57+
Args:
58+
message: The message to log
59+
color: Color enum value for console output
60+
*args: Additional args passed to logger
61+
**kwargs: Additional kwargs passed to logger
62+
"""
63+
self.info(message, *args, **kwargs)
64+
formatted_msg = (
65+
f"[{color.value}]{message}[/]" if color != Color.DEFAULT else message
66+
)
67+
self.console.print(formatted_msg, style=color.value)
68+
69+
70+
def handle_exception(exc_type, exc_value, exc_tb) -> None:
71+
"""Handle uncaught exceptions by logging them.
72+
73+
Args:
74+
exc_type: Exception type
75+
exc_value: Exception value
76+
exc_tb: Exception traceback
77+
"""
78+
if issubclass(exc_type, KeyboardInterrupt):
79+
sys.__excepthook__(exc_type, exc_value, exc_tb)
80+
return
81+
82+
logger.error(
83+
"Uncaught exception:\n%s",
84+
"".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
85+
)
86+
sys.__excepthook__(exc_type, exc_value, exc_tb)
87+
88+
89+
logger = Logger()
90+
sys.excepthook = handle_exception

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.7.0"
8+
version = "1.7.1"
99
description = "gcop is your git AI copilot."
1010
readme = "README.md"
1111
authors = ["gcop <zeeland4work@gmail.com>"]

0 commit comments

Comments
 (0)