simple-vcs 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_vcs/cli.py +160 -19
- simple_vcs/core.py +383 -108
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.3.0.dist-info}/METADATA +34 -3
- simple_vcs-1.3.0.dist-info/RECORD +10 -0
- simple_vcs-1.1.0.dist-info/RECORD +0 -10
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.3.0.dist-info}/WHEEL +0 -0
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.3.0.dist-info}/entry_points.txt +0 -0
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.3.0.dist-info}/top_level.txt +0 -0
simple_vcs/cli.py
CHANGED
|
@@ -1,79 +1,220 @@
|
|
|
1
1
|
import click
|
|
2
2
|
from .core import SimpleVCS
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from rich import box
|
|
7
|
+
from rich.columns import Columns
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
class RichGroup(click.Group):
|
|
12
|
+
"""Custom Click Group that adds Rich formatting to help output"""
|
|
13
|
+
|
|
14
|
+
def format_help(self, ctx, formatter):
|
|
15
|
+
"""Override help formatting with Rich output"""
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Beautiful header
|
|
19
|
+
console.print()
|
|
20
|
+
title = Text("SimpleVCS", style="bold cyan", justify="center")
|
|
21
|
+
subtitle = Text("A Beautiful Version Control System", style="dim", justify="center")
|
|
22
|
+
console.print(title)
|
|
23
|
+
console.print(subtitle)
|
|
24
|
+
console.print()
|
|
25
|
+
|
|
26
|
+
# Description panel
|
|
27
|
+
description = Panel(
|
|
28
|
+
"[white]A lightweight, elegant version control system with a modern terminal interface.\n"
|
|
29
|
+
"Track your files, manage versions, and keep your project history organized.[/white]",
|
|
30
|
+
title="[bold cyan]About[/bold cyan]",
|
|
31
|
+
border_style="cyan",
|
|
32
|
+
box=box.ROUNDED,
|
|
33
|
+
padding=(0, 2)
|
|
34
|
+
)
|
|
35
|
+
console.print(description)
|
|
36
|
+
console.print()
|
|
37
|
+
|
|
38
|
+
# Commands section
|
|
39
|
+
console.print("[bold white]Available Commands:[/bold white]")
|
|
40
|
+
console.print()
|
|
41
|
+
|
|
42
|
+
commands_info = [
|
|
43
|
+
("init", "Initialize a new repository", "cyan"),
|
|
44
|
+
("add", "Add files to staging area", "green"),
|
|
45
|
+
("commit", "Create a new commit", "green"),
|
|
46
|
+
("status", "Show repository status", "yellow"),
|
|
47
|
+
("log", "View commit history", "blue"),
|
|
48
|
+
("diff", "Compare commits", "magenta"),
|
|
49
|
+
("revert", "Revert to previous commit", "red"),
|
|
50
|
+
("snapshot", "Create backup snapshot", "cyan"),
|
|
51
|
+
("restore", "Restore from snapshot", "cyan"),
|
|
52
|
+
("compress", "Optimize storage", "yellow"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for cmd_name, cmd_desc, color in commands_info:
|
|
56
|
+
console.print(f" [{color}]{cmd_name:12}[/{color}] [dim]{cmd_desc}[/dim]")
|
|
57
|
+
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
# Usage examples
|
|
61
|
+
examples = Panel(
|
|
62
|
+
"[bold]Quick Start:[/bold]\n"
|
|
63
|
+
"[cyan]$[/cyan] svcs init [dim]# Create repository[/dim]\n"
|
|
64
|
+
"[cyan]$[/cyan] svcs add file.txt [dim]# Stage files[/dim]\n"
|
|
65
|
+
"[cyan]$[/cyan] svcs commit -m \"message\" [dim]# Save changes[/dim]\n"
|
|
66
|
+
"[cyan]$[/cyan] svcs log [dim]# View history[/dim]\n\n"
|
|
67
|
+
"[bold]Get Help:[/bold]\n"
|
|
68
|
+
"[cyan]$[/cyan] svcs [yellow]<command>[/yellow] --help [dim]# Help for specific command[/dim]",
|
|
69
|
+
title="[bold green]Examples[/bold green]",
|
|
70
|
+
border_style="green",
|
|
71
|
+
box=box.ROUNDED,
|
|
72
|
+
padding=(0, 2)
|
|
73
|
+
)
|
|
74
|
+
console.print(examples)
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
# Footer
|
|
78
|
+
console.print(
|
|
79
|
+
"[dim]Version 1.2.0 | "
|
|
80
|
+
"More info: [/dim][cyan]https://github.com/muhammadsufiyanbaig/simple_vcs[/cyan]"
|
|
81
|
+
)
|
|
82
|
+
console.print()
|
|
83
|
+
|
|
84
|
+
# Prevent Click from outputting its own help
|
|
85
|
+
ctx.resilient_parsing = True
|
|
86
|
+
|
|
87
|
+
@click.group(cls=RichGroup)
|
|
88
|
+
@click.version_option(version="1.3.0", prog_name="SimpleVCS")
|
|
6
89
|
def main():
|
|
7
|
-
"""SimpleVCS - A simple version control system"""
|
|
90
|
+
"""SimpleVCS - A beautiful and simple version control system"""
|
|
8
91
|
pass
|
|
9
92
|
|
|
10
93
|
@main.command()
|
|
11
|
-
@click.option('--path', default='.', help='
|
|
94
|
+
@click.option('--path', default='.', help='Path where repository will be created')
|
|
12
95
|
def init(path):
|
|
13
|
-
"""Initialize a new repository
|
|
96
|
+
"""Initialize a new SimpleVCS repository
|
|
97
|
+
|
|
98
|
+
Creates a new .svcs directory with all necessary files for version control.
|
|
99
|
+
|
|
100
|
+
Example: svcs init --path ./my-project
|
|
101
|
+
"""
|
|
14
102
|
vcs = SimpleVCS(path)
|
|
15
103
|
vcs.init_repo()
|
|
16
104
|
|
|
17
105
|
@main.command()
|
|
18
106
|
@click.argument('files', nargs=-1, required=True)
|
|
19
107
|
def add(files):
|
|
20
|
-
"""Add files to staging area
|
|
108
|
+
"""Add files to the staging area
|
|
109
|
+
|
|
110
|
+
Stage files to be included in the next commit. You can add multiple files at once.
|
|
111
|
+
|
|
112
|
+
Example: svcs add file1.txt file2.py
|
|
113
|
+
"""
|
|
21
114
|
vcs = SimpleVCS()
|
|
22
115
|
for file in files:
|
|
23
116
|
vcs.add_file(file)
|
|
24
117
|
|
|
25
118
|
@main.command()
|
|
26
|
-
@click.option('-m', '--message', help='Commit message')
|
|
119
|
+
@click.option('-m', '--message', help='Commit message describing the changes')
|
|
27
120
|
def commit(message):
|
|
28
|
-
"""Commit staged changes
|
|
121
|
+
"""Commit staged changes to the repository
|
|
122
|
+
|
|
123
|
+
Creates a new commit with all staged files. If no message is provided,
|
|
124
|
+
an automatic timestamp-based message will be generated.
|
|
125
|
+
|
|
126
|
+
Example: svcs commit -m "Add new feature"
|
|
127
|
+
"""
|
|
29
128
|
vcs = SimpleVCS()
|
|
30
129
|
vcs.commit(message)
|
|
31
130
|
|
|
32
131
|
@main.command()
|
|
33
|
-
@click.option('--c1', type=int, help='First commit ID')
|
|
34
|
-
@click.option('--c2', type=int, help='Second commit ID')
|
|
132
|
+
@click.option('--c1', type=int, help='First commit ID (defaults to second-last commit)')
|
|
133
|
+
@click.option('--c2', type=int, help='Second commit ID (defaults to last commit)')
|
|
35
134
|
def diff(c1, c2):
|
|
36
|
-
"""Show differences between commits
|
|
135
|
+
"""Show differences between commits
|
|
136
|
+
|
|
137
|
+
Compare files between two commits to see what changed. Without arguments,
|
|
138
|
+
compares the last two commits.
|
|
139
|
+
|
|
140
|
+
Example: svcs diff --c1 1 --c2 3
|
|
141
|
+
"""
|
|
37
142
|
vcs = SimpleVCS()
|
|
38
143
|
vcs.show_diff(c1, c2)
|
|
39
144
|
|
|
40
145
|
@main.command()
|
|
41
|
-
@click.option('--limit', type=int, help='
|
|
146
|
+
@click.option('--limit', type=int, help='Maximum number of commits to display')
|
|
42
147
|
def log(limit):
|
|
43
|
-
"""Show commit history
|
|
148
|
+
"""Show commit history
|
|
149
|
+
|
|
150
|
+
Display a beautiful table of all commits with their messages, dates, and files.
|
|
151
|
+
Use --limit to show only recent commits.
|
|
152
|
+
|
|
153
|
+
Example: svcs log --limit 10
|
|
154
|
+
"""
|
|
44
155
|
vcs = SimpleVCS()
|
|
45
156
|
vcs.show_log(limit)
|
|
46
157
|
|
|
47
158
|
@main.command()
|
|
48
159
|
def status():
|
|
49
|
-
"""Show repository status
|
|
160
|
+
"""Show current repository status
|
|
161
|
+
|
|
162
|
+
Display information about the repository including current commit,
|
|
163
|
+
total commits, and staged files ready for commit.
|
|
164
|
+
|
|
165
|
+
Example: svcs status
|
|
166
|
+
"""
|
|
50
167
|
vcs = SimpleVCS()
|
|
51
168
|
vcs.status()
|
|
52
169
|
|
|
53
170
|
@main.command()
|
|
54
171
|
@click.argument('commit_id', type=int)
|
|
55
172
|
def revert(commit_id):
|
|
56
|
-
"""
|
|
173
|
+
"""Revert to a specific commit
|
|
174
|
+
|
|
175
|
+
Quickly restore your repository to a previous commit state.
|
|
176
|
+
All files will be restored to their state at that commit.
|
|
177
|
+
|
|
178
|
+
Example: svcs revert 3
|
|
179
|
+
"""
|
|
57
180
|
vcs = SimpleVCS()
|
|
58
181
|
vcs.quick_revert(commit_id)
|
|
59
182
|
|
|
60
183
|
@main.command()
|
|
61
|
-
@click.option('--name', help='
|
|
184
|
+
@click.option('--name', help='Custom name for the snapshot (optional)')
|
|
62
185
|
def snapshot(name):
|
|
63
|
-
"""Create a compressed snapshot
|
|
186
|
+
"""Create a compressed snapshot
|
|
187
|
+
|
|
188
|
+
Creates a ZIP archive of your entire repository (excluding .svcs directory).
|
|
189
|
+
Perfect for backups or sharing your project.
|
|
190
|
+
|
|
191
|
+
Example: svcs snapshot --name my-backup
|
|
192
|
+
"""
|
|
64
193
|
vcs = SimpleVCS()
|
|
65
194
|
vcs.create_snapshot(name)
|
|
66
195
|
|
|
67
196
|
@main.command()
|
|
68
197
|
@click.argument('snapshot_path', type=click.Path(exists=True))
|
|
69
198
|
def restore(snapshot_path):
|
|
70
|
-
"""Restore
|
|
199
|
+
"""Restore from a snapshot
|
|
200
|
+
|
|
201
|
+
Restore your repository from a previously created snapshot ZIP file.
|
|
202
|
+
Current files will be replaced with snapshot contents.
|
|
203
|
+
|
|
204
|
+
Example: svcs restore snapshot_12345.zip
|
|
205
|
+
"""
|
|
71
206
|
vcs = SimpleVCS()
|
|
72
207
|
vcs.restore_from_snapshot(snapshot_path)
|
|
73
208
|
|
|
74
209
|
@main.command()
|
|
75
210
|
def compress():
|
|
76
|
-
"""Compress stored objects
|
|
211
|
+
"""Compress stored objects
|
|
212
|
+
|
|
213
|
+
Optimize repository storage by compressing object files.
|
|
214
|
+
Helps save disk space without losing any data.
|
|
215
|
+
|
|
216
|
+
Example: svcs compress
|
|
217
|
+
"""
|
|
77
218
|
vcs = SimpleVCS()
|
|
78
219
|
vcs.compress_objects()
|
|
79
220
|
|
simple_vcs/core.py
CHANGED
|
@@ -3,14 +3,25 @@ import json
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import shutil
|
|
5
5
|
import time
|
|
6
|
+
import sys
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Dict, List, Optional, Tuple
|
|
9
10
|
import zipfile
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
from rich import box
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
from rich.columns import Columns
|
|
19
|
+
from rich.tree import Tree
|
|
20
|
+
from rich.align import Align
|
|
10
21
|
|
|
11
22
|
class SimpleVCS:
|
|
12
23
|
"""Simple Version Control System core functionality"""
|
|
13
|
-
|
|
24
|
+
|
|
14
25
|
def __init__(self, repo_path: str = "."):
|
|
15
26
|
self.repo_path = Path(repo_path).resolve()
|
|
16
27
|
self.svcs_dir = self.repo_path / ".svcs"
|
|
@@ -18,52 +29,105 @@ class SimpleVCS:
|
|
|
18
29
|
self.commits_file = self.svcs_dir / "commits.json"
|
|
19
30
|
self.staging_file = self.svcs_dir / "staging.json"
|
|
20
31
|
self.head_file = self.svcs_dir / "HEAD"
|
|
32
|
+
# Force UTF-8 output and disable emoji on Windows
|
|
33
|
+
self.is_windows = sys.platform == "win32"
|
|
34
|
+
self.console = Console(force_terminal=True, legacy_windows=False)
|
|
21
35
|
|
|
22
36
|
def init_repo(self) -> bool:
|
|
23
37
|
"""Initialize a new repository"""
|
|
24
38
|
if self.svcs_dir.exists():
|
|
25
|
-
print(f"Repository already exists at {self.repo_path}")
|
|
39
|
+
self.console.print(f"[yellow]WARNING: Repository already exists at[/yellow] [cyan]{self.repo_path}[/cyan]")
|
|
26
40
|
return False
|
|
27
|
-
|
|
41
|
+
|
|
42
|
+
# Print beautiful header
|
|
43
|
+
self.console.print()
|
|
44
|
+
header = Text("SimpleVCS", style="bold cyan", justify="center")
|
|
45
|
+
self.console.print(header)
|
|
46
|
+
self.console.print(Align.center("[dim]A Beautiful Version Control System[/dim]"))
|
|
47
|
+
self.console.print()
|
|
48
|
+
|
|
49
|
+
self.console.print("[cyan]Initializing repository...[/cyan]")
|
|
50
|
+
|
|
28
51
|
# Create directory structure
|
|
29
52
|
self.svcs_dir.mkdir()
|
|
30
53
|
self.objects_dir.mkdir()
|
|
31
|
-
|
|
54
|
+
|
|
32
55
|
# Initialize files
|
|
33
56
|
self._write_json(self.commits_file, [])
|
|
34
57
|
self._write_json(self.staging_file, {})
|
|
35
58
|
self.head_file.write_text("0") # Start with commit 0
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
|
|
60
|
+
# Create tree structure visualization
|
|
61
|
+
tree = Tree(
|
|
62
|
+
"[bold cyan].svcs/[/bold cyan] [dim](Repository Root)[/dim]",
|
|
63
|
+
guide_style="cyan"
|
|
64
|
+
)
|
|
65
|
+
tree.add("[green]objects/[/green] [dim]- Stores file content by hash[/dim]")
|
|
66
|
+
tree.add("[green]commits.json[/green] [dim]- Tracks all commits and history[/dim]")
|
|
67
|
+
tree.add("[green]staging.json[/green] [dim]- Lists files ready to commit[/dim]")
|
|
68
|
+
tree.add("[green]HEAD[/green] [dim]- Points to current commit[/dim]")
|
|
69
|
+
|
|
70
|
+
# Create success panel with tree
|
|
71
|
+
panel_content = (
|
|
72
|
+
f"[bold green]SUCCESS![/bold green] Repository initialized\n\n"
|
|
73
|
+
f"[bold]Location:[/bold]\n"
|
|
74
|
+
f"[cyan]{self.repo_path}[/cyan]\n\n"
|
|
75
|
+
f"[bold]Directory Structure:[/bold]"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
panel = Panel(
|
|
79
|
+
panel_content,
|
|
80
|
+
title="[bold white on green] Repository Created [/bold white on green]",
|
|
81
|
+
border_style="green",
|
|
82
|
+
box=box.DOUBLE,
|
|
83
|
+
padding=(1, 2)
|
|
84
|
+
)
|
|
85
|
+
self.console.print(panel)
|
|
86
|
+
self.console.print(tree)
|
|
87
|
+
|
|
88
|
+
# Quick start guide
|
|
89
|
+
guide_panel = Panel(
|
|
90
|
+
"[bold]Quick Start:[/bold]\n\n"
|
|
91
|
+
"[cyan]1.[/cyan] Add files: [yellow]svcs add <file>[/yellow]\n"
|
|
92
|
+
"[cyan]2.[/cyan] Commit changes: [yellow]svcs commit -m \"message\"[/yellow]\n"
|
|
93
|
+
"[cyan]3.[/cyan] View history: [yellow]svcs log[/yellow]\n"
|
|
94
|
+
"[cyan]4.[/cyan] Check status: [yellow]svcs status[/yellow]",
|
|
95
|
+
title="[bold cyan]Next Steps[/bold cyan]",
|
|
96
|
+
border_style="cyan",
|
|
97
|
+
box=box.ROUNDED,
|
|
98
|
+
padding=(0, 2)
|
|
99
|
+
)
|
|
100
|
+
self.console.print()
|
|
101
|
+
self.console.print(guide_panel)
|
|
102
|
+
self.console.print()
|
|
103
|
+
|
|
38
104
|
return True
|
|
39
105
|
|
|
40
106
|
def add_file(self, file_path: str) -> bool:
|
|
41
107
|
"""Add a file to staging area"""
|
|
42
108
|
if not self._check_repo():
|
|
43
109
|
return False
|
|
44
|
-
|
|
110
|
+
|
|
45
111
|
file_path = Path(file_path).resolve() # Convert to absolute path
|
|
46
112
|
if not file_path.exists():
|
|
47
|
-
print(f"File {file_path}
|
|
113
|
+
self.console.print(f"[red]ERROR: File not found:[/red] [yellow]{file_path}[/yellow]")
|
|
48
114
|
return False
|
|
49
|
-
|
|
115
|
+
|
|
50
116
|
if not file_path.is_file():
|
|
51
|
-
print(f"
|
|
117
|
+
self.console.print(f"[red]ERROR: Not a file:[/red] [yellow]{file_path}[/yellow]")
|
|
52
118
|
return False
|
|
53
|
-
|
|
119
|
+
|
|
54
120
|
# Check if file is within repository
|
|
55
121
|
try:
|
|
56
122
|
relative_path = file_path.relative_to(self.repo_path)
|
|
57
123
|
except ValueError:
|
|
58
|
-
print(f"File
|
|
124
|
+
self.console.print(f"[red]ERROR: File not in repository:[/red] [yellow]{file_path}[/yellow]")
|
|
59
125
|
return False
|
|
60
|
-
|
|
61
|
-
# Calculate file hash
|
|
126
|
+
|
|
127
|
+
# Calculate file hash and store
|
|
62
128
|
file_hash = self._calculate_file_hash(file_path)
|
|
63
|
-
|
|
64
|
-
# Store file content in objects
|
|
65
129
|
self._store_object(file_hash, file_path.read_bytes())
|
|
66
|
-
|
|
130
|
+
|
|
67
131
|
# Add to staging
|
|
68
132
|
staging = self._read_json(self.staging_file)
|
|
69
133
|
staging[str(relative_path)] = {
|
|
@@ -72,20 +136,25 @@ class SimpleVCS:
|
|
|
72
136
|
"modified": file_path.stat().st_mtime
|
|
73
137
|
}
|
|
74
138
|
self._write_json(self.staging_file, staging)
|
|
75
|
-
|
|
76
|
-
|
|
139
|
+
|
|
140
|
+
# Format file size
|
|
141
|
+
size_kb = file_path.stat().st_size / 1024
|
|
142
|
+
size_str = f"{size_kb:.1f}KB" if size_kb < 1024 else f"{size_kb/1024:.1f}MB"
|
|
143
|
+
|
|
144
|
+
self.console.print(f"[green]Added:[/green] [cyan]{relative_path}[/cyan] [dim]({size_str})[/dim]")
|
|
77
145
|
return True
|
|
78
146
|
|
|
79
147
|
def commit(self, message: Optional[str] = None) -> bool:
|
|
80
148
|
"""Commit staged changes"""
|
|
81
149
|
if not self._check_repo():
|
|
82
150
|
return False
|
|
83
|
-
|
|
151
|
+
|
|
84
152
|
staging = self._read_json(self.staging_file)
|
|
85
153
|
if not staging:
|
|
86
|
-
print("No changes
|
|
154
|
+
self.console.print("[yellow]WARNING: No changes staged for commit[/yellow]")
|
|
155
|
+
self.console.print("[dim]Tip: Use 'svcs add <file>' to stage files[/dim]")
|
|
87
156
|
return False
|
|
88
|
-
|
|
157
|
+
|
|
89
158
|
# Create commit object
|
|
90
159
|
commit = {
|
|
91
160
|
"id": len(self._read_json(self.commits_file)) + 1,
|
|
@@ -94,136 +163,276 @@ class SimpleVCS:
|
|
|
94
163
|
"files": staging.copy(),
|
|
95
164
|
"parent": self._get_current_commit_id()
|
|
96
165
|
}
|
|
97
|
-
|
|
166
|
+
|
|
98
167
|
# Save commit
|
|
99
168
|
commits = self._read_json(self.commits_file)
|
|
100
169
|
commits.append(commit)
|
|
101
170
|
self._write_json(self.commits_file, commits)
|
|
102
|
-
|
|
171
|
+
|
|
103
172
|
# Update HEAD
|
|
104
173
|
self.head_file.write_text(str(commit["id"]))
|
|
105
|
-
|
|
174
|
+
|
|
106
175
|
# Clear staging
|
|
107
176
|
self._write_json(self.staging_file, {})
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
177
|
+
|
|
178
|
+
# Create summary panel
|
|
179
|
+
files_list = "\n".join([f" - [cyan]{f}[/cyan]" for f in list(staging.keys())[:5]])
|
|
180
|
+
if len(staging) > 5:
|
|
181
|
+
files_list += f"\n [dim]... and {len(staging) - 5} more file(s)[/dim]"
|
|
182
|
+
|
|
183
|
+
panel = Panel(
|
|
184
|
+
f"[bold green]Commit #{commit['id']}[/bold green]\n\n"
|
|
185
|
+
f"[bold]Message:[/bold] {commit['message']}\n"
|
|
186
|
+
f"[bold]Files:[/bold] {len(staging)} file(s)\n\n"
|
|
187
|
+
f"{files_list}",
|
|
188
|
+
title="[bold green]Commit Successful[/bold green]",
|
|
189
|
+
border_style="green",
|
|
190
|
+
box=box.ROUNDED
|
|
191
|
+
)
|
|
192
|
+
self.console.print(panel)
|
|
111
193
|
return True
|
|
112
194
|
|
|
113
195
|
def show_diff(self, commit_id1: Optional[int] = None, commit_id2: Optional[int] = None) -> bool:
|
|
114
196
|
"""Show differences between commits"""
|
|
115
197
|
if not self._check_repo():
|
|
116
198
|
return False
|
|
117
|
-
|
|
199
|
+
|
|
118
200
|
commits = self._read_json(self.commits_file)
|
|
119
201
|
if not commits:
|
|
120
|
-
print("No commits found")
|
|
202
|
+
self.console.print("[yellow]WARNING: No commits found[/yellow]")
|
|
121
203
|
return False
|
|
122
|
-
|
|
204
|
+
|
|
123
205
|
# Default to comparing last two commits
|
|
124
206
|
if commit_id1 is None and commit_id2 is None:
|
|
125
207
|
if len(commits) < 2:
|
|
126
|
-
print("Need at least 2 commits to show diff")
|
|
208
|
+
self.console.print("[yellow]WARNING: Need at least 2 commits to show diff[/yellow]")
|
|
127
209
|
return False
|
|
128
210
|
commit1 = commits[-2]
|
|
129
211
|
commit2 = commits[-1]
|
|
130
212
|
else:
|
|
131
213
|
commit1 = self._get_commit_by_id(commit_id1 or (len(commits) - 1))
|
|
132
214
|
commit2 = self._get_commit_by_id(commit_id2 or len(commits))
|
|
133
|
-
|
|
215
|
+
|
|
134
216
|
if not commit1 or not commit2:
|
|
135
|
-
print("Invalid commit IDs")
|
|
217
|
+
self.console.print("[red]ERROR: Invalid commit IDs[/red]")
|
|
136
218
|
return False
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
219
|
+
|
|
220
|
+
# Create comparison table
|
|
221
|
+
table = Table(
|
|
222
|
+
title=f"[bold]Differences: Commit #{commit1['id']} to Commit #{commit2['id']}[/bold]",
|
|
223
|
+
box=box.ROUNDED,
|
|
224
|
+
show_header=True,
|
|
225
|
+
header_style="bold cyan"
|
|
226
|
+
)
|
|
227
|
+
table.add_column("Status", style="bold", width=10)
|
|
228
|
+
table.add_column("File", style="cyan")
|
|
229
|
+
table.add_column("Details", style="dim")
|
|
230
|
+
|
|
141
231
|
files1 = set(commit1["files"].keys())
|
|
142
232
|
files2 = set(commit2["files"].keys())
|
|
143
|
-
|
|
233
|
+
|
|
234
|
+
has_changes = False
|
|
235
|
+
|
|
144
236
|
# New files
|
|
145
|
-
new_files = files2 - files1
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
237
|
+
new_files = sorted(files2 - files1)
|
|
238
|
+
for file in new_files:
|
|
239
|
+
size = commit2["files"][file]["size"]
|
|
240
|
+
size_str = f"{size/1024:.1f}KB" if size < 1024*1024 else f"{size/(1024*1024):.1f}MB"
|
|
241
|
+
table.add_row("[green]+ Added[/green]", file, f"Size: {size_str}")
|
|
242
|
+
has_changes = True
|
|
243
|
+
|
|
151
244
|
# Deleted files
|
|
152
|
-
deleted_files = files1 - files2
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
245
|
+
deleted_files = sorted(files1 - files2)
|
|
246
|
+
for file in deleted_files:
|
|
247
|
+
table.add_row("[red]- Deleted[/red]", file, "")
|
|
248
|
+
has_changes = True
|
|
249
|
+
|
|
158
250
|
# Modified files
|
|
159
251
|
common_files = files1 & files2
|
|
160
252
|
modified_files = []
|
|
161
|
-
for file in common_files:
|
|
253
|
+
for file in sorted(common_files):
|
|
162
254
|
if commit1["files"][file]["hash"] != commit2["files"][file]["hash"]:
|
|
255
|
+
size1 = commit1["files"][file]["size"]
|
|
256
|
+
size2 = commit2["files"][file]["size"]
|
|
257
|
+
diff = size2 - size1
|
|
258
|
+
diff_str = f"+{diff/1024:.1f}KB" if diff > 0 else f"{diff/1024:.1f}KB"
|
|
259
|
+
table.add_row("[yellow]M Modified[/yellow]", file, f"Size change: {diff_str}")
|
|
163
260
|
modified_files.append(file)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
has_changes = True
|
|
262
|
+
|
|
263
|
+
if not has_changes:
|
|
264
|
+
self.console.print("[dim]No differences found between commits[/dim]")
|
|
265
|
+
else:
|
|
266
|
+
self.console.print(table)
|
|
267
|
+
self.console.print(f"\n[dim]Summary: [green]{len(new_files)} added[/green], "
|
|
268
|
+
f"[yellow]{len(modified_files)} modified[/yellow], "
|
|
269
|
+
f"[red]{len(deleted_files)} deleted[/red][/dim]")
|
|
270
|
+
|
|
173
271
|
return True
|
|
174
272
|
|
|
175
273
|
def show_log(self, limit: Optional[int] = None) -> bool:
|
|
176
274
|
"""Show commit history"""
|
|
177
275
|
if not self._check_repo():
|
|
178
276
|
return False
|
|
179
|
-
|
|
277
|
+
|
|
180
278
|
commits = self._read_json(self.commits_file)
|
|
181
279
|
if not commits:
|
|
182
|
-
|
|
280
|
+
# Create empty state panel
|
|
281
|
+
empty_panel = Panel(
|
|
282
|
+
"[yellow]No commits yet[/yellow]\n\n"
|
|
283
|
+
"[dim]Your repository is ready, but you haven't made any commits.[/dim]\n\n"
|
|
284
|
+
"[bold]Get started:[/bold]\n"
|
|
285
|
+
"[cyan]1.[/cyan] Add files: [yellow]svcs add <file>[/yellow]\n"
|
|
286
|
+
"[cyan]2.[/cyan] Commit: [yellow]svcs commit -m \"First commit\"[/yellow]",
|
|
287
|
+
title="[bold yellow]Empty Repository[/bold yellow]",
|
|
288
|
+
border_style="yellow",
|
|
289
|
+
box=box.ROUNDED,
|
|
290
|
+
padding=(1, 2)
|
|
291
|
+
)
|
|
292
|
+
self.console.print()
|
|
293
|
+
self.console.print(empty_panel)
|
|
294
|
+
self.console.print()
|
|
183
295
|
return False
|
|
184
|
-
|
|
296
|
+
|
|
185
297
|
commits_to_show = commits[-limit:] if limit else commits
|
|
186
298
|
commits_to_show.reverse() # Show newest first
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
print(
|
|
190
|
-
|
|
299
|
+
|
|
300
|
+
# Print beautiful header
|
|
301
|
+
self.console.print()
|
|
302
|
+
header_text = Text()
|
|
303
|
+
header_text.append("Commit History", style="bold cyan")
|
|
304
|
+
self.console.print(Align.center(header_text))
|
|
305
|
+
self.console.print(Align.center(f"[dim]Repository: {self.repo_path.name}[/dim]"))
|
|
306
|
+
self.console.print()
|
|
307
|
+
|
|
308
|
+
# Create commits table with enhanced styling
|
|
309
|
+
table = Table(
|
|
310
|
+
box=box.DOUBLE_EDGE,
|
|
311
|
+
show_header=True,
|
|
312
|
+
header_style="bold white on blue",
|
|
313
|
+
border_style="cyan",
|
|
314
|
+
padding=(0, 1)
|
|
315
|
+
)
|
|
316
|
+
table.add_column("ID", justify="center", style="bold cyan", width=8)
|
|
317
|
+
table.add_column("Date & Time", style="green", width=19)
|
|
318
|
+
table.add_column("Commit Message", style="white", no_wrap=False)
|
|
319
|
+
table.add_column("Files", justify="center", style="bold yellow", width=7)
|
|
320
|
+
table.add_column("Parent", justify="center", style="dim", width=8)
|
|
321
|
+
|
|
322
|
+
current_commit_id = self._get_current_commit_id()
|
|
323
|
+
|
|
191
324
|
for commit in commits_to_show:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
325
|
+
commit_id = str(commit['id'])
|
|
326
|
+
is_current = commit['id'] == current_commit_id
|
|
327
|
+
|
|
328
|
+
if is_current:
|
|
329
|
+
commit_id = f"-> {commit_id}" # Mark current commit with arrow
|
|
330
|
+
|
|
331
|
+
date_str = datetime.fromtimestamp(commit['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
|
|
332
|
+
message = commit['message']
|
|
333
|
+
if len(message) > 60:
|
|
334
|
+
message = message[:57] + "..."
|
|
335
|
+
files_count = str(len(commit['files']))
|
|
336
|
+
parent = str(commit.get('parent', 'None'))
|
|
337
|
+
|
|
338
|
+
# Style current commit differently with background
|
|
339
|
+
if is_current:
|
|
340
|
+
table.add_row(
|
|
341
|
+
f"[bold white on blue]{commit_id}[/bold white on blue]",
|
|
342
|
+
f"[bold]{date_str}[/bold]",
|
|
343
|
+
f"[bold white]{message}[/bold white]",
|
|
344
|
+
f"[bold]{files_count}[/bold]",
|
|
345
|
+
parent,
|
|
346
|
+
style="on blue"
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
table.add_row(commit_id, date_str, message, files_count, parent)
|
|
350
|
+
|
|
351
|
+
self.console.print(table)
|
|
352
|
+
|
|
353
|
+
# Statistics footer
|
|
354
|
+
stats_text = (
|
|
355
|
+
f"[dim]Total commits: [/dim][cyan]{len(commits)}[/cyan]"
|
|
356
|
+
)
|
|
357
|
+
if limit and len(commits) > limit:
|
|
358
|
+
stats_text += f"[dim] | Showing: [/dim][yellow]Last {limit}[/yellow]"
|
|
359
|
+
if current_commit_id:
|
|
360
|
+
stats_text += f"[dim] | Current: [/dim][green]#{current_commit_id}[/green]"
|
|
361
|
+
|
|
362
|
+
self.console.print()
|
|
363
|
+
self.console.print(Align.center(stats_text))
|
|
364
|
+
self.console.print()
|
|
365
|
+
|
|
366
|
+
# Legend
|
|
367
|
+
legend = Panel(
|
|
368
|
+
"[bold white on blue]-> ID[/bold white on blue] = Current commit | "
|
|
369
|
+
"[dim]Parent[/dim] = Previous commit ID",
|
|
370
|
+
border_style="dim",
|
|
371
|
+
box=box.ROUNDED,
|
|
372
|
+
padding=(0, 2)
|
|
373
|
+
)
|
|
374
|
+
self.console.print(legend)
|
|
375
|
+
self.console.print()
|
|
376
|
+
|
|
200
377
|
return True
|
|
201
378
|
|
|
202
379
|
def status(self) -> bool:
|
|
203
380
|
"""Show repository status"""
|
|
204
381
|
if not self._check_repo():
|
|
205
382
|
return False
|
|
206
|
-
|
|
383
|
+
|
|
207
384
|
staging = self._read_json(self.staging_file)
|
|
208
385
|
current_commit = self._get_current_commit()
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
386
|
+
total_commits = len(self._read_json(self.commits_file))
|
|
387
|
+
|
|
388
|
+
# Create status panel
|
|
389
|
+
status_text = f"[bold]Repository:[/bold] [cyan]{self.repo_path.name}[/cyan]\n"
|
|
390
|
+
status_text += f"[bold]Location:[/bold] [dim]{self.repo_path}[/dim]\n"
|
|
391
|
+
status_text += f"[bold]Current Commit:[/bold] [green]#{current_commit['id']}[/green] " if current_commit else "[bold]Current Commit:[/bold] [yellow]None (no commits yet)[/yellow]\n"
|
|
392
|
+
if current_commit:
|
|
393
|
+
status_text += f"[dim]({current_commit['message']})[/dim]\n"
|
|
394
|
+
status_text += f"[bold]Total Commits:[/bold] {total_commits}"
|
|
395
|
+
|
|
396
|
+
panel = Panel(
|
|
397
|
+
status_text,
|
|
398
|
+
title="[bold cyan]Repository Status[/bold cyan]",
|
|
399
|
+
border_style="cyan",
|
|
400
|
+
box=box.ROUNDED
|
|
401
|
+
)
|
|
402
|
+
self.console.print(panel)
|
|
403
|
+
|
|
404
|
+
# Show staged files
|
|
213
405
|
if staging:
|
|
214
|
-
|
|
406
|
+
table = Table(
|
|
407
|
+
title="[bold yellow]Staged Files[/bold yellow]",
|
|
408
|
+
box=box.ROUNDED,
|
|
409
|
+
show_header=True,
|
|
410
|
+
header_style="bold yellow"
|
|
411
|
+
)
|
|
412
|
+
table.add_column("File", style="cyan")
|
|
413
|
+
table.add_column("Size", justify="right", style="green")
|
|
414
|
+
table.add_column("Hash", style="dim", width=16)
|
|
415
|
+
|
|
215
416
|
for file, info in staging.items():
|
|
216
|
-
|
|
417
|
+
size = info['size']
|
|
418
|
+
size_str = f"{size/1024:.1f}KB" if size < 1024*1024 else f"{size/(1024*1024):.1f}MB"
|
|
419
|
+
hash_short = info['hash'][:14] + "..."
|
|
420
|
+
table.add_row(file, size_str, hash_short)
|
|
421
|
+
|
|
422
|
+
self.console.print("\n", table)
|
|
423
|
+
self.console.print(f"[dim]Ready to commit {len(staging)} file(s)[/dim]")
|
|
217
424
|
else:
|
|
218
|
-
print("\
|
|
219
|
-
|
|
425
|
+
self.console.print("\n[yellow]No files staged[/yellow]")
|
|
426
|
+
self.console.print("[dim]Use 'svcs add <file>' to stage files for commit[/dim]")
|
|
427
|
+
|
|
220
428
|
return True
|
|
221
429
|
|
|
222
430
|
# Helper methods
|
|
223
431
|
def _check_repo(self) -> bool:
|
|
224
432
|
"""Check if repository is initialized"""
|
|
225
433
|
if not self.svcs_dir.exists():
|
|
226
|
-
print("Not a SimpleVCS repository
|
|
434
|
+
self.console.print("[red]ERROR: Not a SimpleVCS repository[/red]")
|
|
435
|
+
self.console.print("[dim]Tip: Run 'svcs init' to initialize a repository[/dim]")
|
|
227
436
|
return False
|
|
228
437
|
return True
|
|
229
438
|
|
|
@@ -283,9 +492,11 @@ class SimpleVCS:
|
|
|
283
492
|
|
|
284
493
|
commit = self._get_commit_by_id(commit_id)
|
|
285
494
|
if not commit:
|
|
286
|
-
print(f"Commit {commit_id} not found")
|
|
495
|
+
self.console.print(f"[red]ERROR: Commit #{commit_id} not found[/red]")
|
|
287
496
|
return False
|
|
288
497
|
|
|
498
|
+
self.console.print(f"[cyan]Reverting to commit #{commit_id}...[/cyan]")
|
|
499
|
+
|
|
289
500
|
# Restore files from the specified commit
|
|
290
501
|
for file_path, file_info in commit["files"].items():
|
|
291
502
|
target_path = self.repo_path / file_path
|
|
@@ -301,7 +512,16 @@ class SimpleVCS:
|
|
|
301
512
|
# Update HEAD to point to the reverted commit
|
|
302
513
|
self.head_file.write_text(str(commit_id))
|
|
303
514
|
|
|
304
|
-
|
|
515
|
+
panel = Panel(
|
|
516
|
+
f"[bold green]Successfully reverted to commit #{commit_id}[/bold green]\n\n"
|
|
517
|
+
f"[bold]Message:[/bold] {commit['message']}\n"
|
|
518
|
+
f"[bold]Files restored:[/bold] {len(commit['files'])}\n"
|
|
519
|
+
f"[bold]Date:[/bold] {datetime.fromtimestamp(commit['timestamp']).strftime('%Y-%m-%d %H:%M:%S')}",
|
|
520
|
+
title="[bold green]Revert Complete[/bold green]",
|
|
521
|
+
border_style="green",
|
|
522
|
+
box=box.ROUNDED
|
|
523
|
+
)
|
|
524
|
+
self.console.print(panel)
|
|
305
525
|
return True
|
|
306
526
|
|
|
307
527
|
def create_snapshot(self, name: str = None) -> bool:
|
|
@@ -312,6 +532,14 @@ class SimpleVCS:
|
|
|
312
532
|
snapshot_name = name or f"snapshot_{int(time.time())}"
|
|
313
533
|
snapshot_path = self.repo_path / f"{snapshot_name}.zip"
|
|
314
534
|
|
|
535
|
+
self.console.print("[cyan]Creating snapshot...[/cyan]")
|
|
536
|
+
|
|
537
|
+
# Count files first
|
|
538
|
+
file_count = 0
|
|
539
|
+
for root, dirs, files in os.walk(self.repo_path):
|
|
540
|
+
dirs[:] = [d for d in dirs if d != '.svcs']
|
|
541
|
+
file_count += len(files)
|
|
542
|
+
|
|
315
543
|
# Create a zip archive of all tracked files
|
|
316
544
|
with zipfile.ZipFile(snapshot_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
317
545
|
for root, dirs, files in os.walk(self.repo_path):
|
|
@@ -324,18 +552,35 @@ class SimpleVCS:
|
|
|
324
552
|
arc_path = file_path.relative_to(self.repo_path)
|
|
325
553
|
zipf.write(file_path, arc_path)
|
|
326
554
|
|
|
327
|
-
|
|
555
|
+
snapshot_size = snapshot_path.stat().st_size
|
|
556
|
+
size_str = f"{snapshot_size/1024:.1f}KB" if snapshot_size < 1024*1024 else f"{snapshot_size/(1024*1024):.1f}MB"
|
|
557
|
+
|
|
558
|
+
panel = Panel(
|
|
559
|
+
f"[bold green]Snapshot created successfully[/bold green]\n\n"
|
|
560
|
+
f"[bold]Name:[/bold] [cyan]{snapshot_name}.zip[/cyan]\n"
|
|
561
|
+
f"[bold]Location:[/bold] [dim]{snapshot_path}[/dim]\n"
|
|
562
|
+
f"[bold]Size:[/bold] {size_str}\n"
|
|
563
|
+
f"[bold]Files:[/bold] {file_count}",
|
|
564
|
+
title="[bold green]Snapshot Created[/bold green]",
|
|
565
|
+
border_style="green",
|
|
566
|
+
box=box.ROUNDED
|
|
567
|
+
)
|
|
568
|
+
self.console.print(panel)
|
|
328
569
|
return True
|
|
329
570
|
|
|
330
571
|
def restore_from_snapshot(self, snapshot_path: str) -> bool:
|
|
331
572
|
"""Restore repository from a snapshot"""
|
|
332
573
|
snapshot_path = Path(snapshot_path)
|
|
333
574
|
if not snapshot_path.exists():
|
|
334
|
-
print(f"Snapshot {snapshot_path}
|
|
575
|
+
self.console.print(f"[red]ERROR: Snapshot not found:[/red] [yellow]{snapshot_path}[/yellow]")
|
|
335
576
|
return False
|
|
336
577
|
|
|
578
|
+
self.console.print("[cyan]Restoring from snapshot...[/cyan]")
|
|
579
|
+
|
|
337
580
|
# Extract the zip archive
|
|
338
581
|
with zipfile.ZipFile(snapshot_path, 'r') as zipf:
|
|
582
|
+
file_list = zipf.namelist()
|
|
583
|
+
|
|
339
584
|
# Clear current files (but preserve .svcs directory)
|
|
340
585
|
for item in self.repo_path.iterdir():
|
|
341
586
|
if item.name != '.svcs':
|
|
@@ -347,7 +592,15 @@ class SimpleVCS:
|
|
|
347
592
|
# Extract all files
|
|
348
593
|
zipf.extractall(self.repo_path)
|
|
349
594
|
|
|
350
|
-
|
|
595
|
+
panel = Panel(
|
|
596
|
+
f"[bold green]Repository restored successfully[/bold green]\n\n"
|
|
597
|
+
f"[bold]Snapshot:[/bold] [cyan]{snapshot_path.name}[/cyan]\n"
|
|
598
|
+
f"[bold]Files restored:[/bold] {len(file_list)}",
|
|
599
|
+
title="[bold green]Restore Complete[/bold green]",
|
|
600
|
+
border_style="green",
|
|
601
|
+
box=box.ROUNDED
|
|
602
|
+
)
|
|
603
|
+
self.console.print(panel)
|
|
351
604
|
return True
|
|
352
605
|
|
|
353
606
|
def compress_objects(self) -> bool:
|
|
@@ -356,27 +609,49 @@ class SimpleVCS:
|
|
|
356
609
|
return False
|
|
357
610
|
|
|
358
611
|
original_size = sum(f.stat().st_size for f in self.objects_dir.glob('*') if f.is_file())
|
|
612
|
+
obj_files = [f for f in self.objects_dir.glob('*') if f.is_file() and f.stat().st_size > 1024]
|
|
613
|
+
|
|
614
|
+
if not obj_files:
|
|
615
|
+
self.console.print("[yellow]WARNING: No objects to compress (files are too small)[/yellow]")
|
|
616
|
+
return True
|
|
617
|
+
|
|
618
|
+
self.console.print("[cyan]Compressing objects...[/cyan]")
|
|
359
619
|
|
|
360
620
|
# For each object file, compress it if it's large enough to benefit
|
|
361
|
-
for obj_file in
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
with
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
compressed_path.unlink()
|
|
621
|
+
for obj_file in obj_files:
|
|
622
|
+
# Create a compressed version with .gz extension
|
|
623
|
+
compressed_path = obj_file.with_suffix(obj_file.suffix + '.gz')
|
|
624
|
+
with open(obj_file, 'rb') as f_in:
|
|
625
|
+
import gzip
|
|
626
|
+
with gzip.open(compressed_path, 'wb') as f_out:
|
|
627
|
+
f_out.writelines(f_in)
|
|
628
|
+
|
|
629
|
+
# Replace original with compressed version
|
|
630
|
+
obj_file.unlink()
|
|
631
|
+
# Decompress back to original name for compatibility
|
|
632
|
+
with gzip.open(compressed_path, 'rb') as f_in:
|
|
633
|
+
with open(obj_file, 'wb') as f_out:
|
|
634
|
+
f_out.write(f_in.read())
|
|
635
|
+
compressed_path.unlink()
|
|
377
636
|
|
|
378
637
|
new_size = sum(f.stat().st_size for f in self.objects_dir.glob('*') if f.is_file())
|
|
379
638
|
saved_space = original_size - new_size
|
|
639
|
+
saved_percent = (saved_space / original_size * 100) if original_size > 0 else 0
|
|
640
|
+
|
|
641
|
+
# Format sizes
|
|
642
|
+
orig_str = f"{original_size/1024:.1f}KB" if original_size < 1024*1024 else f"{original_size/(1024*1024):.1f}MB"
|
|
643
|
+
new_str = f"{new_size/1024:.1f}KB" if new_size < 1024*1024 else f"{new_size/(1024*1024):.1f}MB"
|
|
644
|
+
saved_str = f"{saved_space/1024:.1f}KB" if saved_space < 1024*1024 else f"{saved_space/(1024*1024):.1f}MB"
|
|
380
645
|
|
|
381
|
-
|
|
646
|
+
panel = Panel(
|
|
647
|
+
f"[bold green]Compression completed successfully[/bold green]\n\n"
|
|
648
|
+
f"[bold]Original size:[/bold] {orig_str}\n"
|
|
649
|
+
f"[bold]New size:[/bold] {new_str}\n"
|
|
650
|
+
f"[bold]Space saved:[/bold] [green]{saved_str}[/green] [dim]({saved_percent:.1f}%)[/dim]\n"
|
|
651
|
+
f"[bold]Objects compressed:[/bold] {len(obj_files)}",
|
|
652
|
+
title="[bold green]Compression Complete[/bold green]",
|
|
653
|
+
border_style="green",
|
|
654
|
+
box=box.ROUNDED
|
|
655
|
+
)
|
|
656
|
+
self.console.print(panel)
|
|
382
657
|
return True
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple-vcs
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: A simple version control system with
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: A beautiful and simple version control system with a stunning terminal interface
|
|
5
5
|
Home-page: https://github.com/muhammadsufiyanbaig/simple_vcs
|
|
6
6
|
Author: Muhammad Sufiyan Baig
|
|
7
7
|
Author-email: Muhammad Sufiyan Baig <send.sufiyan@gmail.com>
|
|
@@ -34,6 +34,7 @@ Requires-Python: >=3.7
|
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: click>=7.0
|
|
37
|
+
Requires-Dist: rich>=10.0.0
|
|
37
38
|
Dynamic: author
|
|
38
39
|
Dynamic: home-page
|
|
39
40
|
Dynamic: license-file
|
|
@@ -41,7 +42,16 @@ Dynamic: requires-python
|
|
|
41
42
|
|
|
42
43
|
# SimpleVCS
|
|
43
44
|
|
|
44
|
-
A simple version control system written in Python that provides basic VCS functionality similar to Git
|
|
45
|
+
A simple version control system written in Python that provides basic VCS functionality similar to Git - now with a **beautiful modern terminal interface**!
|
|
46
|
+
|
|
47
|
+
## ✨ New in Version 1.3.0
|
|
48
|
+
|
|
49
|
+
**Enhanced Visual Experience** - SimpleVCS now features an even more stunning terminal interface:
|
|
50
|
+
- 🎨 **Enhanced Init** - Beautiful header, tree structure, and quick start guide
|
|
51
|
+
- 📜 **Gorgeous Log** - Double-bordered tables with highlighted current commit and legend
|
|
52
|
+
- 💡 **Rich Help** - Custom help interface with examples and organized commands
|
|
53
|
+
- 🌈 **Professional Design** - Centered headers, better spacing, and visual hierarchy
|
|
54
|
+
- ⭐ **Better UX** - Empty state messages, statistics, and helpful tips throughout
|
|
45
55
|
|
|
46
56
|
## Features
|
|
47
57
|
|
|
@@ -240,6 +250,7 @@ When initialized, SimpleVCS creates a `.svcs` directory containing:
|
|
|
240
250
|
|
|
241
251
|
- Python 3.7 or higher
|
|
242
252
|
- click>=7.0 (for CLI functionality)
|
|
253
|
+
- rich>=10.0.0 (for beautiful terminal interface)
|
|
243
254
|
|
|
244
255
|
## Development
|
|
245
256
|
|
|
@@ -317,6 +328,26 @@ Project Link: [https://github.com/muhammadsufiyanbaig/simple_vcs](https://github
|
|
|
317
328
|
|
|
318
329
|
## Changelog
|
|
319
330
|
|
|
331
|
+
### Version 1.3.0
|
|
332
|
+
- **Enhanced Init Command**: Beautiful header, tree visualization, and quick start guide
|
|
333
|
+
- **Gorgeous Log Display**: Double-bordered tables with current commit highlighting
|
|
334
|
+
- **Custom Help Interface**: Rich-formatted help with organized commands and examples
|
|
335
|
+
- **Better Visual Hierarchy**: Centered headers, improved spacing, and alignment
|
|
336
|
+
- **Enhanced Empty States**: Helpful messages when no commits exist
|
|
337
|
+
- **Statistics Display**: Show total commits, current commit, and filters in log
|
|
338
|
+
- **Legend Support**: Clear explanations of symbols and formatting
|
|
339
|
+
- **Professional Polish**: Refined colors, borders, and overall presentation
|
|
340
|
+
|
|
341
|
+
### Version 1.2.0
|
|
342
|
+
- **Beautiful Terminal Interface**: Complete visual overhaul with Rich library
|
|
343
|
+
- **Colored Output**: Green for success, yellow for warnings, red for errors
|
|
344
|
+
- **Elegant Tables**: Commit history, status, and diffs in formatted tables
|
|
345
|
+
- **Styled Panels**: Information displayed in bordered panels with rounded corners
|
|
346
|
+
- **Enhanced Commands**: All commands now have beautiful, professional output
|
|
347
|
+
- **Better UX**: Clear visual hierarchy and consistent formatting
|
|
348
|
+
- **Windows Compatible**: No problematic Unicode characters
|
|
349
|
+
- **Enhanced Help**: Detailed descriptions and examples for all commands
|
|
350
|
+
|
|
320
351
|
### Version 1.1.0
|
|
321
352
|
- Added quick revert functionality to go back to any commit instantly
|
|
322
353
|
- Added snapshot creation and restoration features
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
simple_vcs/__init__.py,sha256=RcYfgE1z2am5TUREGRVniDWErxGOW8dHH3hmt13bebE,166
|
|
2
|
+
simple_vcs/cli.py,sha256=LSKU6nRwhl261s4MZnOzhpyyZV6pEWoH1v0AOJQXqNI,6950
|
|
3
|
+
simple_vcs/core.py,sha256=ZpdBo-fNQzyBWGA0si2F8VR6_ZKpnbhQICN352mbwjE,26105
|
|
4
|
+
simple_vcs/utils.py,sha256=CTd4gDdHqP-dawjtEeKU3csnT-Fe7_SN9a5ENQlo7wk,1202
|
|
5
|
+
simple_vcs-1.3.0.dist-info/licenses/LICENSE,sha256=6o_m1QgCywYf-QZnE6cuLTwu5kVVQn3vJ7JJUd0V_iY,1085
|
|
6
|
+
simple_vcs-1.3.0.dist-info/METADATA,sha256=dMl9Yf-X96YbuZSVnXLptJJ1Kd2-O0CxL6bVk29C5pI,10213
|
|
7
|
+
simple_vcs-1.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
simple_vcs-1.3.0.dist-info/entry_points.txt,sha256=19JeWUvRFzwKF5p_iLQiSwCV3XTgxB7mkTLmFGrc_aY,45
|
|
9
|
+
simple_vcs-1.3.0.dist-info/top_level.txt,sha256=YcaiuqQjjXFL-H62tfC-hTcg-7sWFmLh65zghskauL4,11
|
|
10
|
+
simple_vcs-1.3.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
simple_vcs/__init__.py,sha256=RcYfgE1z2am5TUREGRVniDWErxGOW8dHH3hmt13bebE,166
|
|
2
|
-
simple_vcs/cli.py,sha256=ealBu5Q5XpeguLRnXY7OTqKuJXKFQKI5dn0dHynLGA4,2004
|
|
3
|
-
simple_vcs/core.py,sha256=I83hj6IxJE4q5nyXcq6kAxM8Y8MzpB_maJGEBoXOuVk,13779
|
|
4
|
-
simple_vcs/utils.py,sha256=CTd4gDdHqP-dawjtEeKU3csnT-Fe7_SN9a5ENQlo7wk,1202
|
|
5
|
-
simple_vcs-1.1.0.dist-info/licenses/LICENSE,sha256=6o_m1QgCywYf-QZnE6cuLTwu5kVVQn3vJ7JJUd0V_iY,1085
|
|
6
|
-
simple_vcs-1.1.0.dist-info/METADATA,sha256=QRbAb_AyBiC7LRfP3S1N1rej9_EB5dm-UJwnO6LgzAM,8231
|
|
7
|
-
simple_vcs-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
-
simple_vcs-1.1.0.dist-info/entry_points.txt,sha256=19JeWUvRFzwKF5p_iLQiSwCV3XTgxB7mkTLmFGrc_aY,45
|
|
9
|
-
simple_vcs-1.1.0.dist-info/top_level.txt,sha256=YcaiuqQjjXFL-H62tfC-hTcg-7sWFmLh65zghskauL4,11
|
|
10
|
-
simple_vcs-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|