simple-vcs 1.1.0__py3-none-any.whl → 1.2.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 +83 -18
- simple_vcs/core.py +295 -108
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.2.0.dist-info}/METADATA +23 -3
- simple_vcs-1.2.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.2.0.dist-info}/WHEEL +0 -0
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.2.0.dist-info}/entry_points.txt +0 -0
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {simple_vcs-1.1.0.dist-info → simple_vcs-1.2.0.dist-info}/top_level.txt +0 -0
simple_vcs/cli.py
CHANGED
|
@@ -1,79 +1,144 @@
|
|
|
1
1
|
import click
|
|
2
2
|
from .core import SimpleVCS
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
3
6
|
|
|
4
7
|
@click.group()
|
|
5
|
-
@click.version_option()
|
|
8
|
+
@click.version_option(version="1.2.0", prog_name="SimpleVCS")
|
|
6
9
|
def main():
|
|
7
|
-
"""
|
|
10
|
+
"""
|
|
11
|
+
SimpleVCS - A beautiful and simple version control system
|
|
12
|
+
|
|
13
|
+
A lightweight VCS with an intuitive interface for managing your project versions.
|
|
14
|
+
"""
|
|
8
15
|
pass
|
|
9
16
|
|
|
10
17
|
@main.command()
|
|
11
|
-
@click.option('--path', default='.', help='
|
|
18
|
+
@click.option('--path', default='.', help='Path where repository will be created')
|
|
12
19
|
def init(path):
|
|
13
|
-
"""Initialize a new repository
|
|
20
|
+
"""Initialize a new SimpleVCS repository
|
|
21
|
+
|
|
22
|
+
Creates a new .svcs directory with all necessary files for version control.
|
|
23
|
+
|
|
24
|
+
Example: svcs init --path ./my-project
|
|
25
|
+
"""
|
|
14
26
|
vcs = SimpleVCS(path)
|
|
15
27
|
vcs.init_repo()
|
|
16
28
|
|
|
17
29
|
@main.command()
|
|
18
30
|
@click.argument('files', nargs=-1, required=True)
|
|
19
31
|
def add(files):
|
|
20
|
-
"""Add files to staging area
|
|
32
|
+
"""Add files to the staging area
|
|
33
|
+
|
|
34
|
+
Stage files to be included in the next commit. You can add multiple files at once.
|
|
35
|
+
|
|
36
|
+
Example: svcs add file1.txt file2.py
|
|
37
|
+
"""
|
|
21
38
|
vcs = SimpleVCS()
|
|
22
39
|
for file in files:
|
|
23
40
|
vcs.add_file(file)
|
|
24
41
|
|
|
25
42
|
@main.command()
|
|
26
|
-
@click.option('-m', '--message', help='Commit message')
|
|
43
|
+
@click.option('-m', '--message', help='Commit message describing the changes')
|
|
27
44
|
def commit(message):
|
|
28
|
-
"""Commit staged changes
|
|
45
|
+
"""Commit staged changes to the repository
|
|
46
|
+
|
|
47
|
+
Creates a new commit with all staged files. If no message is provided,
|
|
48
|
+
an automatic timestamp-based message will be generated.
|
|
49
|
+
|
|
50
|
+
Example: svcs commit -m "Add new feature"
|
|
51
|
+
"""
|
|
29
52
|
vcs = SimpleVCS()
|
|
30
53
|
vcs.commit(message)
|
|
31
54
|
|
|
32
55
|
@main.command()
|
|
33
|
-
@click.option('--c1', type=int, help='First commit ID')
|
|
34
|
-
@click.option('--c2', type=int, help='Second commit ID')
|
|
56
|
+
@click.option('--c1', type=int, help='First commit ID (defaults to second-last commit)')
|
|
57
|
+
@click.option('--c2', type=int, help='Second commit ID (defaults to last commit)')
|
|
35
58
|
def diff(c1, c2):
|
|
36
|
-
"""Show differences between commits
|
|
59
|
+
"""Show differences between commits
|
|
60
|
+
|
|
61
|
+
Compare files between two commits to see what changed. Without arguments,
|
|
62
|
+
compares the last two commits.
|
|
63
|
+
|
|
64
|
+
Example: svcs diff --c1 1 --c2 3
|
|
65
|
+
"""
|
|
37
66
|
vcs = SimpleVCS()
|
|
38
67
|
vcs.show_diff(c1, c2)
|
|
39
68
|
|
|
40
69
|
@main.command()
|
|
41
|
-
@click.option('--limit', type=int, help='
|
|
70
|
+
@click.option('--limit', type=int, help='Maximum number of commits to display')
|
|
42
71
|
def log(limit):
|
|
43
|
-
"""Show commit history
|
|
72
|
+
"""Show commit history
|
|
73
|
+
|
|
74
|
+
Display a beautiful table of all commits with their messages, dates, and files.
|
|
75
|
+
Use --limit to show only recent commits.
|
|
76
|
+
|
|
77
|
+
Example: svcs log --limit 10
|
|
78
|
+
"""
|
|
44
79
|
vcs = SimpleVCS()
|
|
45
80
|
vcs.show_log(limit)
|
|
46
81
|
|
|
47
82
|
@main.command()
|
|
48
83
|
def status():
|
|
49
|
-
"""Show repository status
|
|
84
|
+
"""Show current repository status
|
|
85
|
+
|
|
86
|
+
Display information about the repository including current commit,
|
|
87
|
+
total commits, and staged files ready for commit.
|
|
88
|
+
|
|
89
|
+
Example: svcs status
|
|
90
|
+
"""
|
|
50
91
|
vcs = SimpleVCS()
|
|
51
92
|
vcs.status()
|
|
52
93
|
|
|
53
94
|
@main.command()
|
|
54
95
|
@click.argument('commit_id', type=int)
|
|
55
96
|
def revert(commit_id):
|
|
56
|
-
"""
|
|
97
|
+
"""Revert to a specific commit
|
|
98
|
+
|
|
99
|
+
Quickly restore your repository to a previous commit state.
|
|
100
|
+
All files will be restored to their state at that commit.
|
|
101
|
+
|
|
102
|
+
Example: svcs revert 3
|
|
103
|
+
"""
|
|
57
104
|
vcs = SimpleVCS()
|
|
58
105
|
vcs.quick_revert(commit_id)
|
|
59
106
|
|
|
60
107
|
@main.command()
|
|
61
|
-
@click.option('--name', help='
|
|
108
|
+
@click.option('--name', help='Custom name for the snapshot (optional)')
|
|
62
109
|
def snapshot(name):
|
|
63
|
-
"""Create a compressed snapshot
|
|
110
|
+
"""Create a compressed snapshot
|
|
111
|
+
|
|
112
|
+
Creates a ZIP archive of your entire repository (excluding .svcs directory).
|
|
113
|
+
Perfect for backups or sharing your project.
|
|
114
|
+
|
|
115
|
+
Example: svcs snapshot --name my-backup
|
|
116
|
+
"""
|
|
64
117
|
vcs = SimpleVCS()
|
|
65
118
|
vcs.create_snapshot(name)
|
|
66
119
|
|
|
67
120
|
@main.command()
|
|
68
121
|
@click.argument('snapshot_path', type=click.Path(exists=True))
|
|
69
122
|
def restore(snapshot_path):
|
|
70
|
-
"""Restore
|
|
123
|
+
"""Restore from a snapshot
|
|
124
|
+
|
|
125
|
+
Restore your repository from a previously created snapshot ZIP file.
|
|
126
|
+
Current files will be replaced with snapshot contents.
|
|
127
|
+
|
|
128
|
+
Example: svcs restore snapshot_12345.zip
|
|
129
|
+
"""
|
|
71
130
|
vcs = SimpleVCS()
|
|
72
131
|
vcs.restore_from_snapshot(snapshot_path)
|
|
73
132
|
|
|
74
133
|
@main.command()
|
|
75
134
|
def compress():
|
|
76
|
-
"""Compress stored objects
|
|
135
|
+
"""Compress stored objects
|
|
136
|
+
|
|
137
|
+
Optimize repository storage by compressing object files.
|
|
138
|
+
Helps save disk space without losing any data.
|
|
139
|
+
|
|
140
|
+
Example: svcs compress
|
|
141
|
+
"""
|
|
77
142
|
vcs = SimpleVCS()
|
|
78
143
|
vcs.compress_objects()
|
|
79
144
|
|
simple_vcs/core.py
CHANGED
|
@@ -3,14 +3,22 @@ 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
|
|
10
18
|
|
|
11
19
|
class SimpleVCS:
|
|
12
20
|
"""Simple Version Control System core functionality"""
|
|
13
|
-
|
|
21
|
+
|
|
14
22
|
def __init__(self, repo_path: str = "."):
|
|
15
23
|
self.repo_path = Path(repo_path).resolve()
|
|
16
24
|
self.svcs_dir = self.repo_path / ".svcs"
|
|
@@ -18,52 +26,66 @@ class SimpleVCS:
|
|
|
18
26
|
self.commits_file = self.svcs_dir / "commits.json"
|
|
19
27
|
self.staging_file = self.svcs_dir / "staging.json"
|
|
20
28
|
self.head_file = self.svcs_dir / "HEAD"
|
|
29
|
+
# Force UTF-8 output and disable emoji on Windows
|
|
30
|
+
self.is_windows = sys.platform == "win32"
|
|
31
|
+
self.console = Console(force_terminal=True, legacy_windows=False)
|
|
21
32
|
|
|
22
33
|
def init_repo(self) -> bool:
|
|
23
34
|
"""Initialize a new repository"""
|
|
24
35
|
if self.svcs_dir.exists():
|
|
25
|
-
print(f"Repository already exists at {self.repo_path}")
|
|
36
|
+
self.console.print(f"[yellow]WARNING: Repository already exists at[/yellow] [cyan]{self.repo_path}[/cyan]")
|
|
26
37
|
return False
|
|
27
|
-
|
|
38
|
+
|
|
39
|
+
self.console.print("[cyan]Initializing repository...[/cyan]")
|
|
40
|
+
|
|
28
41
|
# Create directory structure
|
|
29
42
|
self.svcs_dir.mkdir()
|
|
30
43
|
self.objects_dir.mkdir()
|
|
31
|
-
|
|
44
|
+
|
|
32
45
|
# Initialize files
|
|
33
46
|
self._write_json(self.commits_file, [])
|
|
34
47
|
self._write_json(self.staging_file, {})
|
|
35
48
|
self.head_file.write_text("0") # Start with commit 0
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
|
|
50
|
+
panel = Panel(
|
|
51
|
+
f"[green]SUCCESS[/green] Repository initialized at [cyan]{self.repo_path}[/cyan]\n\n"
|
|
52
|
+
"[dim]Structure created:[/dim]\n"
|
|
53
|
+
" - .svcs/objects/ - File storage\n"
|
|
54
|
+
" - .svcs/commits.json - Commit history\n"
|
|
55
|
+
" - .svcs/staging.json - Staged files\n"
|
|
56
|
+
" - .svcs/HEAD - Current commit",
|
|
57
|
+
title="[bold green]SimpleVCS Repository[/bold green]",
|
|
58
|
+
border_style="green",
|
|
59
|
+
box=box.ROUNDED
|
|
60
|
+
)
|
|
61
|
+
self.console.print(panel)
|
|
38
62
|
return True
|
|
39
63
|
|
|
40
64
|
def add_file(self, file_path: str) -> bool:
|
|
41
65
|
"""Add a file to staging area"""
|
|
42
66
|
if not self._check_repo():
|
|
43
67
|
return False
|
|
44
|
-
|
|
68
|
+
|
|
45
69
|
file_path = Path(file_path).resolve() # Convert to absolute path
|
|
46
70
|
if not file_path.exists():
|
|
47
|
-
print(f"File {file_path}
|
|
71
|
+
self.console.print(f"[red]ERROR: File not found:[/red] [yellow]{file_path}[/yellow]")
|
|
48
72
|
return False
|
|
49
|
-
|
|
73
|
+
|
|
50
74
|
if not file_path.is_file():
|
|
51
|
-
print(f"
|
|
75
|
+
self.console.print(f"[red]ERROR: Not a file:[/red] [yellow]{file_path}[/yellow]")
|
|
52
76
|
return False
|
|
53
|
-
|
|
77
|
+
|
|
54
78
|
# Check if file is within repository
|
|
55
79
|
try:
|
|
56
80
|
relative_path = file_path.relative_to(self.repo_path)
|
|
57
81
|
except ValueError:
|
|
58
|
-
print(f"File
|
|
82
|
+
self.console.print(f"[red]ERROR: File not in repository:[/red] [yellow]{file_path}[/yellow]")
|
|
59
83
|
return False
|
|
60
|
-
|
|
61
|
-
# Calculate file hash
|
|
84
|
+
|
|
85
|
+
# Calculate file hash and store
|
|
62
86
|
file_hash = self._calculate_file_hash(file_path)
|
|
63
|
-
|
|
64
|
-
# Store file content in objects
|
|
65
87
|
self._store_object(file_hash, file_path.read_bytes())
|
|
66
|
-
|
|
88
|
+
|
|
67
89
|
# Add to staging
|
|
68
90
|
staging = self._read_json(self.staging_file)
|
|
69
91
|
staging[str(relative_path)] = {
|
|
@@ -72,20 +94,25 @@ class SimpleVCS:
|
|
|
72
94
|
"modified": file_path.stat().st_mtime
|
|
73
95
|
}
|
|
74
96
|
self._write_json(self.staging_file, staging)
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
|
|
98
|
+
# Format file size
|
|
99
|
+
size_kb = file_path.stat().st_size / 1024
|
|
100
|
+
size_str = f"{size_kb:.1f}KB" if size_kb < 1024 else f"{size_kb/1024:.1f}MB"
|
|
101
|
+
|
|
102
|
+
self.console.print(f"[green]Added:[/green] [cyan]{relative_path}[/cyan] [dim]({size_str})[/dim]")
|
|
77
103
|
return True
|
|
78
104
|
|
|
79
105
|
def commit(self, message: Optional[str] = None) -> bool:
|
|
80
106
|
"""Commit staged changes"""
|
|
81
107
|
if not self._check_repo():
|
|
82
108
|
return False
|
|
83
|
-
|
|
109
|
+
|
|
84
110
|
staging = self._read_json(self.staging_file)
|
|
85
111
|
if not staging:
|
|
86
|
-
print("No changes
|
|
112
|
+
self.console.print("[yellow]WARNING: No changes staged for commit[/yellow]")
|
|
113
|
+
self.console.print("[dim]Tip: Use 'svcs add <file>' to stage files[/dim]")
|
|
87
114
|
return False
|
|
88
|
-
|
|
115
|
+
|
|
89
116
|
# Create commit object
|
|
90
117
|
commit = {
|
|
91
118
|
"id": len(self._read_json(self.commits_file)) + 1,
|
|
@@ -94,136 +121,230 @@ class SimpleVCS:
|
|
|
94
121
|
"files": staging.copy(),
|
|
95
122
|
"parent": self._get_current_commit_id()
|
|
96
123
|
}
|
|
97
|
-
|
|
124
|
+
|
|
98
125
|
# Save commit
|
|
99
126
|
commits = self._read_json(self.commits_file)
|
|
100
127
|
commits.append(commit)
|
|
101
128
|
self._write_json(self.commits_file, commits)
|
|
102
|
-
|
|
129
|
+
|
|
103
130
|
# Update HEAD
|
|
104
131
|
self.head_file.write_text(str(commit["id"]))
|
|
105
|
-
|
|
132
|
+
|
|
106
133
|
# Clear staging
|
|
107
134
|
self._write_json(self.staging_file, {})
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
|
|
136
|
+
# Create summary panel
|
|
137
|
+
files_list = "\n".join([f" - [cyan]{f}[/cyan]" for f in list(staging.keys())[:5]])
|
|
138
|
+
if len(staging) > 5:
|
|
139
|
+
files_list += f"\n [dim]... and {len(staging) - 5} more file(s)[/dim]"
|
|
140
|
+
|
|
141
|
+
panel = Panel(
|
|
142
|
+
f"[bold green]Commit #{commit['id']}[/bold green]\n\n"
|
|
143
|
+
f"[bold]Message:[/bold] {commit['message']}\n"
|
|
144
|
+
f"[bold]Files:[/bold] {len(staging)} file(s)\n\n"
|
|
145
|
+
f"{files_list}",
|
|
146
|
+
title="[bold green]Commit Successful[/bold green]",
|
|
147
|
+
border_style="green",
|
|
148
|
+
box=box.ROUNDED
|
|
149
|
+
)
|
|
150
|
+
self.console.print(panel)
|
|
111
151
|
return True
|
|
112
152
|
|
|
113
153
|
def show_diff(self, commit_id1: Optional[int] = None, commit_id2: Optional[int] = None) -> bool:
|
|
114
154
|
"""Show differences between commits"""
|
|
115
155
|
if not self._check_repo():
|
|
116
156
|
return False
|
|
117
|
-
|
|
157
|
+
|
|
118
158
|
commits = self._read_json(self.commits_file)
|
|
119
159
|
if not commits:
|
|
120
|
-
print("No commits found")
|
|
160
|
+
self.console.print("[yellow]WARNING: No commits found[/yellow]")
|
|
121
161
|
return False
|
|
122
|
-
|
|
162
|
+
|
|
123
163
|
# Default to comparing last two commits
|
|
124
164
|
if commit_id1 is None and commit_id2 is None:
|
|
125
165
|
if len(commits) < 2:
|
|
126
|
-
print("Need at least 2 commits to show diff")
|
|
166
|
+
self.console.print("[yellow]WARNING: Need at least 2 commits to show diff[/yellow]")
|
|
127
167
|
return False
|
|
128
168
|
commit1 = commits[-2]
|
|
129
169
|
commit2 = commits[-1]
|
|
130
170
|
else:
|
|
131
171
|
commit1 = self._get_commit_by_id(commit_id1 or (len(commits) - 1))
|
|
132
172
|
commit2 = self._get_commit_by_id(commit_id2 or len(commits))
|
|
133
|
-
|
|
173
|
+
|
|
134
174
|
if not commit1 or not commit2:
|
|
135
|
-
print("Invalid commit IDs")
|
|
175
|
+
self.console.print("[red]ERROR: Invalid commit IDs[/red]")
|
|
136
176
|
return False
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
|
|
178
|
+
# Create comparison table
|
|
179
|
+
table = Table(
|
|
180
|
+
title=f"[bold]Differences: Commit #{commit1['id']} to Commit #{commit2['id']}[/bold]",
|
|
181
|
+
box=box.ROUNDED,
|
|
182
|
+
show_header=True,
|
|
183
|
+
header_style="bold cyan"
|
|
184
|
+
)
|
|
185
|
+
table.add_column("Status", style="bold", width=10)
|
|
186
|
+
table.add_column("File", style="cyan")
|
|
187
|
+
table.add_column("Details", style="dim")
|
|
188
|
+
|
|
141
189
|
files1 = set(commit1["files"].keys())
|
|
142
190
|
files2 = set(commit2["files"].keys())
|
|
143
|
-
|
|
191
|
+
|
|
192
|
+
has_changes = False
|
|
193
|
+
|
|
144
194
|
# New files
|
|
145
|
-
new_files = files2 - files1
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
195
|
+
new_files = sorted(files2 - files1)
|
|
196
|
+
for file in new_files:
|
|
197
|
+
size = commit2["files"][file]["size"]
|
|
198
|
+
size_str = f"{size/1024:.1f}KB" if size < 1024*1024 else f"{size/(1024*1024):.1f}MB"
|
|
199
|
+
table.add_row("[green]+ Added[/green]", file, f"Size: {size_str}")
|
|
200
|
+
has_changes = True
|
|
201
|
+
|
|
151
202
|
# Deleted files
|
|
152
|
-
deleted_files = files1 - files2
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
deleted_files = sorted(files1 - files2)
|
|
204
|
+
for file in deleted_files:
|
|
205
|
+
table.add_row("[red]- Deleted[/red]", file, "")
|
|
206
|
+
has_changes = True
|
|
207
|
+
|
|
158
208
|
# Modified files
|
|
159
209
|
common_files = files1 & files2
|
|
160
210
|
modified_files = []
|
|
161
|
-
for file in common_files:
|
|
211
|
+
for file in sorted(common_files):
|
|
162
212
|
if commit1["files"][file]["hash"] != commit2["files"][file]["hash"]:
|
|
213
|
+
size1 = commit1["files"][file]["size"]
|
|
214
|
+
size2 = commit2["files"][file]["size"]
|
|
215
|
+
diff = size2 - size1
|
|
216
|
+
diff_str = f"+{diff/1024:.1f}KB" if diff > 0 else f"{diff/1024:.1f}KB"
|
|
217
|
+
table.add_row("[yellow]M Modified[/yellow]", file, f"Size change: {diff_str}")
|
|
163
218
|
modified_files.append(file)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
has_changes = True
|
|
220
|
+
|
|
221
|
+
if not has_changes:
|
|
222
|
+
self.console.print("[dim]No differences found between commits[/dim]")
|
|
223
|
+
else:
|
|
224
|
+
self.console.print(table)
|
|
225
|
+
self.console.print(f"\n[dim]Summary: [green]{len(new_files)} added[/green], "
|
|
226
|
+
f"[yellow]{len(modified_files)} modified[/yellow], "
|
|
227
|
+
f"[red]{len(deleted_files)} deleted[/red][/dim]")
|
|
228
|
+
|
|
173
229
|
return True
|
|
174
230
|
|
|
175
231
|
def show_log(self, limit: Optional[int] = None) -> bool:
|
|
176
232
|
"""Show commit history"""
|
|
177
233
|
if not self._check_repo():
|
|
178
234
|
return False
|
|
179
|
-
|
|
235
|
+
|
|
180
236
|
commits = self._read_json(self.commits_file)
|
|
181
237
|
if not commits:
|
|
182
|
-
print("No commits found")
|
|
238
|
+
self.console.print("[yellow]WARNING: No commits found[/yellow]")
|
|
239
|
+
self.console.print("[dim]Tip: Use 'svcs commit -m \"message\"' to create your first commit[/dim]")
|
|
183
240
|
return False
|
|
184
|
-
|
|
241
|
+
|
|
185
242
|
commits_to_show = commits[-limit:] if limit else commits
|
|
186
243
|
commits_to_show.reverse() # Show newest first
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
244
|
+
|
|
245
|
+
# Create commits table
|
|
246
|
+
table = Table(
|
|
247
|
+
title="[bold cyan]Commit History[/bold cyan]",
|
|
248
|
+
box=box.ROUNDED,
|
|
249
|
+
show_header=True,
|
|
250
|
+
header_style="bold magenta"
|
|
251
|
+
)
|
|
252
|
+
table.add_column("ID", justify="center", style="cyan", width=6)
|
|
253
|
+
table.add_column("Date", style="green", width=19)
|
|
254
|
+
table.add_column("Message", style="white")
|
|
255
|
+
table.add_column("Files", justify="center", style="yellow", width=7)
|
|
256
|
+
table.add_column("Parent", justify="center", style="dim", width=7)
|
|
257
|
+
|
|
258
|
+
current_commit_id = self._get_current_commit_id()
|
|
259
|
+
|
|
191
260
|
for commit in commits_to_show:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
261
|
+
commit_id = str(commit['id'])
|
|
262
|
+
if commit['id'] == current_commit_id:
|
|
263
|
+
commit_id = f"* {commit_id}" # Mark current commit
|
|
264
|
+
|
|
265
|
+
date_str = datetime.fromtimestamp(commit['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
|
|
266
|
+
message = commit['message']
|
|
267
|
+
if len(message) > 50:
|
|
268
|
+
message = message[:47] + "..."
|
|
269
|
+
files_count = str(len(commit['files']))
|
|
270
|
+
parent = str(commit.get('parent', '-'))
|
|
271
|
+
|
|
272
|
+
# Style current commit differently
|
|
273
|
+
if commit['id'] == current_commit_id:
|
|
274
|
+
table.add_row(
|
|
275
|
+
f"[bold cyan]{commit_id}[/bold cyan]",
|
|
276
|
+
date_str,
|
|
277
|
+
f"[bold]{message}[/bold]",
|
|
278
|
+
files_count,
|
|
279
|
+
parent
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
table.add_row(commit_id, date_str, message, files_count, parent)
|
|
283
|
+
|
|
284
|
+
self.console.print(table)
|
|
285
|
+
|
|
286
|
+
if limit and len(commits) > limit:
|
|
287
|
+
self.console.print(f"\n[dim]Showing last {limit} of {len(commits)} commits[/dim]")
|
|
288
|
+
|
|
200
289
|
return True
|
|
201
290
|
|
|
202
291
|
def status(self) -> bool:
|
|
203
292
|
"""Show repository status"""
|
|
204
293
|
if not self._check_repo():
|
|
205
294
|
return False
|
|
206
|
-
|
|
295
|
+
|
|
207
296
|
staging = self._read_json(self.staging_file)
|
|
208
297
|
current_commit = self._get_current_commit()
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
298
|
+
total_commits = len(self._read_json(self.commits_file))
|
|
299
|
+
|
|
300
|
+
# Create status panel
|
|
301
|
+
status_text = f"[bold]Repository:[/bold] [cyan]{self.repo_path.name}[/cyan]\n"
|
|
302
|
+
status_text += f"[bold]Location:[/bold] [dim]{self.repo_path}[/dim]\n"
|
|
303
|
+
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"
|
|
304
|
+
if current_commit:
|
|
305
|
+
status_text += f"[dim]({current_commit['message']})[/dim]\n"
|
|
306
|
+
status_text += f"[bold]Total Commits:[/bold] {total_commits}"
|
|
307
|
+
|
|
308
|
+
panel = Panel(
|
|
309
|
+
status_text,
|
|
310
|
+
title="[bold cyan]Repository Status[/bold cyan]",
|
|
311
|
+
border_style="cyan",
|
|
312
|
+
box=box.ROUNDED
|
|
313
|
+
)
|
|
314
|
+
self.console.print(panel)
|
|
315
|
+
|
|
316
|
+
# Show staged files
|
|
213
317
|
if staging:
|
|
214
|
-
|
|
318
|
+
table = Table(
|
|
319
|
+
title="[bold yellow]Staged Files[/bold yellow]",
|
|
320
|
+
box=box.ROUNDED,
|
|
321
|
+
show_header=True,
|
|
322
|
+
header_style="bold yellow"
|
|
323
|
+
)
|
|
324
|
+
table.add_column("File", style="cyan")
|
|
325
|
+
table.add_column("Size", justify="right", style="green")
|
|
326
|
+
table.add_column("Hash", style="dim", width=16)
|
|
327
|
+
|
|
215
328
|
for file, info in staging.items():
|
|
216
|
-
|
|
329
|
+
size = info['size']
|
|
330
|
+
size_str = f"{size/1024:.1f}KB" if size < 1024*1024 else f"{size/(1024*1024):.1f}MB"
|
|
331
|
+
hash_short = info['hash'][:14] + "..."
|
|
332
|
+
table.add_row(file, size_str, hash_short)
|
|
333
|
+
|
|
334
|
+
self.console.print("\n", table)
|
|
335
|
+
self.console.print(f"[dim]Ready to commit {len(staging)} file(s)[/dim]")
|
|
217
336
|
else:
|
|
218
|
-
print("\
|
|
219
|
-
|
|
337
|
+
self.console.print("\n[yellow]No files staged[/yellow]")
|
|
338
|
+
self.console.print("[dim]Use 'svcs add <file>' to stage files for commit[/dim]")
|
|
339
|
+
|
|
220
340
|
return True
|
|
221
341
|
|
|
222
342
|
# Helper methods
|
|
223
343
|
def _check_repo(self) -> bool:
|
|
224
344
|
"""Check if repository is initialized"""
|
|
225
345
|
if not self.svcs_dir.exists():
|
|
226
|
-
print("Not a SimpleVCS repository
|
|
346
|
+
self.console.print("[red]ERROR: Not a SimpleVCS repository[/red]")
|
|
347
|
+
self.console.print("[dim]Tip: Run 'svcs init' to initialize a repository[/dim]")
|
|
227
348
|
return False
|
|
228
349
|
return True
|
|
229
350
|
|
|
@@ -283,9 +404,11 @@ class SimpleVCS:
|
|
|
283
404
|
|
|
284
405
|
commit = self._get_commit_by_id(commit_id)
|
|
285
406
|
if not commit:
|
|
286
|
-
print(f"Commit {commit_id} not found")
|
|
407
|
+
self.console.print(f"[red]ERROR: Commit #{commit_id} not found[/red]")
|
|
287
408
|
return False
|
|
288
409
|
|
|
410
|
+
self.console.print(f"[cyan]Reverting to commit #{commit_id}...[/cyan]")
|
|
411
|
+
|
|
289
412
|
# Restore files from the specified commit
|
|
290
413
|
for file_path, file_info in commit["files"].items():
|
|
291
414
|
target_path = self.repo_path / file_path
|
|
@@ -301,7 +424,16 @@ class SimpleVCS:
|
|
|
301
424
|
# Update HEAD to point to the reverted commit
|
|
302
425
|
self.head_file.write_text(str(commit_id))
|
|
303
426
|
|
|
304
|
-
|
|
427
|
+
panel = Panel(
|
|
428
|
+
f"[bold green]Successfully reverted to commit #{commit_id}[/bold green]\n\n"
|
|
429
|
+
f"[bold]Message:[/bold] {commit['message']}\n"
|
|
430
|
+
f"[bold]Files restored:[/bold] {len(commit['files'])}\n"
|
|
431
|
+
f"[bold]Date:[/bold] {datetime.fromtimestamp(commit['timestamp']).strftime('%Y-%m-%d %H:%M:%S')}",
|
|
432
|
+
title="[bold green]Revert Complete[/bold green]",
|
|
433
|
+
border_style="green",
|
|
434
|
+
box=box.ROUNDED
|
|
435
|
+
)
|
|
436
|
+
self.console.print(panel)
|
|
305
437
|
return True
|
|
306
438
|
|
|
307
439
|
def create_snapshot(self, name: str = None) -> bool:
|
|
@@ -312,6 +444,14 @@ class SimpleVCS:
|
|
|
312
444
|
snapshot_name = name or f"snapshot_{int(time.time())}"
|
|
313
445
|
snapshot_path = self.repo_path / f"{snapshot_name}.zip"
|
|
314
446
|
|
|
447
|
+
self.console.print("[cyan]Creating snapshot...[/cyan]")
|
|
448
|
+
|
|
449
|
+
# Count files first
|
|
450
|
+
file_count = 0
|
|
451
|
+
for root, dirs, files in os.walk(self.repo_path):
|
|
452
|
+
dirs[:] = [d for d in dirs if d != '.svcs']
|
|
453
|
+
file_count += len(files)
|
|
454
|
+
|
|
315
455
|
# Create a zip archive of all tracked files
|
|
316
456
|
with zipfile.ZipFile(snapshot_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
317
457
|
for root, dirs, files in os.walk(self.repo_path):
|
|
@@ -324,18 +464,35 @@ class SimpleVCS:
|
|
|
324
464
|
arc_path = file_path.relative_to(self.repo_path)
|
|
325
465
|
zipf.write(file_path, arc_path)
|
|
326
466
|
|
|
327
|
-
|
|
467
|
+
snapshot_size = snapshot_path.stat().st_size
|
|
468
|
+
size_str = f"{snapshot_size/1024:.1f}KB" if snapshot_size < 1024*1024 else f"{snapshot_size/(1024*1024):.1f}MB"
|
|
469
|
+
|
|
470
|
+
panel = Panel(
|
|
471
|
+
f"[bold green]Snapshot created successfully[/bold green]\n\n"
|
|
472
|
+
f"[bold]Name:[/bold] [cyan]{snapshot_name}.zip[/cyan]\n"
|
|
473
|
+
f"[bold]Location:[/bold] [dim]{snapshot_path}[/dim]\n"
|
|
474
|
+
f"[bold]Size:[/bold] {size_str}\n"
|
|
475
|
+
f"[bold]Files:[/bold] {file_count}",
|
|
476
|
+
title="[bold green]Snapshot Created[/bold green]",
|
|
477
|
+
border_style="green",
|
|
478
|
+
box=box.ROUNDED
|
|
479
|
+
)
|
|
480
|
+
self.console.print(panel)
|
|
328
481
|
return True
|
|
329
482
|
|
|
330
483
|
def restore_from_snapshot(self, snapshot_path: str) -> bool:
|
|
331
484
|
"""Restore repository from a snapshot"""
|
|
332
485
|
snapshot_path = Path(snapshot_path)
|
|
333
486
|
if not snapshot_path.exists():
|
|
334
|
-
print(f"Snapshot {snapshot_path}
|
|
487
|
+
self.console.print(f"[red]ERROR: Snapshot not found:[/red] [yellow]{snapshot_path}[/yellow]")
|
|
335
488
|
return False
|
|
336
489
|
|
|
490
|
+
self.console.print("[cyan]Restoring from snapshot...[/cyan]")
|
|
491
|
+
|
|
337
492
|
# Extract the zip archive
|
|
338
493
|
with zipfile.ZipFile(snapshot_path, 'r') as zipf:
|
|
494
|
+
file_list = zipf.namelist()
|
|
495
|
+
|
|
339
496
|
# Clear current files (but preserve .svcs directory)
|
|
340
497
|
for item in self.repo_path.iterdir():
|
|
341
498
|
if item.name != '.svcs':
|
|
@@ -347,7 +504,15 @@ class SimpleVCS:
|
|
|
347
504
|
# Extract all files
|
|
348
505
|
zipf.extractall(self.repo_path)
|
|
349
506
|
|
|
350
|
-
|
|
507
|
+
panel = Panel(
|
|
508
|
+
f"[bold green]Repository restored successfully[/bold green]\n\n"
|
|
509
|
+
f"[bold]Snapshot:[/bold] [cyan]{snapshot_path.name}[/cyan]\n"
|
|
510
|
+
f"[bold]Files restored:[/bold] {len(file_list)}",
|
|
511
|
+
title="[bold green]Restore Complete[/bold green]",
|
|
512
|
+
border_style="green",
|
|
513
|
+
box=box.ROUNDED
|
|
514
|
+
)
|
|
515
|
+
self.console.print(panel)
|
|
351
516
|
return True
|
|
352
517
|
|
|
353
518
|
def compress_objects(self) -> bool:
|
|
@@ -356,27 +521,49 @@ class SimpleVCS:
|
|
|
356
521
|
return False
|
|
357
522
|
|
|
358
523
|
original_size = sum(f.stat().st_size for f in self.objects_dir.glob('*') if f.is_file())
|
|
524
|
+
obj_files = [f for f in self.objects_dir.glob('*') if f.is_file() and f.stat().st_size > 1024]
|
|
525
|
+
|
|
526
|
+
if not obj_files:
|
|
527
|
+
self.console.print("[yellow]WARNING: No objects to compress (files are too small)[/yellow]")
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
self.console.print("[cyan]Compressing objects...[/cyan]")
|
|
359
531
|
|
|
360
532
|
# 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()
|
|
533
|
+
for obj_file in obj_files:
|
|
534
|
+
# Create a compressed version with .gz extension
|
|
535
|
+
compressed_path = obj_file.with_suffix(obj_file.suffix + '.gz')
|
|
536
|
+
with open(obj_file, 'rb') as f_in:
|
|
537
|
+
import gzip
|
|
538
|
+
with gzip.open(compressed_path, 'wb') as f_out:
|
|
539
|
+
f_out.writelines(f_in)
|
|
540
|
+
|
|
541
|
+
# Replace original with compressed version
|
|
542
|
+
obj_file.unlink()
|
|
543
|
+
# Decompress back to original name for compatibility
|
|
544
|
+
with gzip.open(compressed_path, 'rb') as f_in:
|
|
545
|
+
with open(obj_file, 'wb') as f_out:
|
|
546
|
+
f_out.write(f_in.read())
|
|
547
|
+
compressed_path.unlink()
|
|
377
548
|
|
|
378
549
|
new_size = sum(f.stat().st_size for f in self.objects_dir.glob('*') if f.is_file())
|
|
379
550
|
saved_space = original_size - new_size
|
|
551
|
+
saved_percent = (saved_space / original_size * 100) if original_size > 0 else 0
|
|
552
|
+
|
|
553
|
+
# Format sizes
|
|
554
|
+
orig_str = f"{original_size/1024:.1f}KB" if original_size < 1024*1024 else f"{original_size/(1024*1024):.1f}MB"
|
|
555
|
+
new_str = f"{new_size/1024:.1f}KB" if new_size < 1024*1024 else f"{new_size/(1024*1024):.1f}MB"
|
|
556
|
+
saved_str = f"{saved_space/1024:.1f}KB" if saved_space < 1024*1024 else f"{saved_space/(1024*1024):.1f}MB"
|
|
380
557
|
|
|
381
|
-
|
|
558
|
+
panel = Panel(
|
|
559
|
+
f"[bold green]Compression completed successfully[/bold green]\n\n"
|
|
560
|
+
f"[bold]Original size:[/bold] {orig_str}\n"
|
|
561
|
+
f"[bold]New size:[/bold] {new_str}\n"
|
|
562
|
+
f"[bold]Space saved:[/bold] [green]{saved_str}[/green] [dim]({saved_percent:.1f}%)[/dim]\n"
|
|
563
|
+
f"[bold]Objects compressed:[/bold] {len(obj_files)}",
|
|
564
|
+
title="[bold green]Compression Complete[/bold green]",
|
|
565
|
+
border_style="green",
|
|
566
|
+
box=box.ROUNDED
|
|
567
|
+
)
|
|
568
|
+
self.console.print(panel)
|
|
382
569
|
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.2.0
|
|
4
|
+
Summary: A beautiful and simple version control system with a modern 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,15 @@ 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.2.0
|
|
48
|
+
|
|
49
|
+
**Stunning Visual Interface** - SimpleVCS now features a gorgeous terminal interface powered by Rich:
|
|
50
|
+
- 🎨 **Colored Output** - Color-coded messages for success, warnings, and errors
|
|
51
|
+
- 📊 **Beautiful Tables** - Commit history and status displayed in elegant tables
|
|
52
|
+
- 📦 **Styled Panels** - Information presented in clean, bordered boxes
|
|
53
|
+
- 🌟 **Professional Look** - Modern, easy-to-read formatting throughout
|
|
45
54
|
|
|
46
55
|
## Features
|
|
47
56
|
|
|
@@ -240,6 +249,7 @@ When initialized, SimpleVCS creates a `.svcs` directory containing:
|
|
|
240
249
|
|
|
241
250
|
- Python 3.7 or higher
|
|
242
251
|
- click>=7.0 (for CLI functionality)
|
|
252
|
+
- rich>=10.0.0 (for beautiful terminal interface)
|
|
243
253
|
|
|
244
254
|
## Development
|
|
245
255
|
|
|
@@ -317,6 +327,16 @@ Project Link: [https://github.com/muhammadsufiyanbaig/simple_vcs](https://github
|
|
|
317
327
|
|
|
318
328
|
## Changelog
|
|
319
329
|
|
|
330
|
+
### Version 1.2.0
|
|
331
|
+
- **Beautiful Terminal Interface**: Complete visual overhaul with Rich library
|
|
332
|
+
- **Colored Output**: Green for success, yellow for warnings, red for errors
|
|
333
|
+
- **Elegant Tables**: Commit history, status, and diffs in formatted tables
|
|
334
|
+
- **Styled Panels**: Information displayed in bordered panels with rounded corners
|
|
335
|
+
- **Enhanced Commands**: All commands now have beautiful, professional output
|
|
336
|
+
- **Better UX**: Clear visual hierarchy and consistent formatting
|
|
337
|
+
- **Windows Compatible**: No problematic Unicode characters
|
|
338
|
+
- **Enhanced Help**: Detailed descriptions and examples for all commands
|
|
339
|
+
|
|
320
340
|
### Version 1.1.0
|
|
321
341
|
- Added quick revert functionality to go back to any commit instantly
|
|
322
342
|
- Added snapshot creation and restoration features
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
simple_vcs/__init__.py,sha256=RcYfgE1z2am5TUREGRVniDWErxGOW8dHH3hmt13bebE,166
|
|
2
|
+
simple_vcs/cli.py,sha256=GUwtLDFMM9musEPJCIn8DM94Sb4vnCXJ3QuWWWV1QaU,3916
|
|
3
|
+
simple_vcs/core.py,sha256=SggyUSmLeOCENRihS3JTjoDhKjxSVgTsMckcRIFMNF0,22684
|
|
4
|
+
simple_vcs/utils.py,sha256=CTd4gDdHqP-dawjtEeKU3csnT-Fe7_SN9a5ENQlo7wk,1202
|
|
5
|
+
simple_vcs-1.2.0.dist-info/licenses/LICENSE,sha256=6o_m1QgCywYf-QZnE6cuLTwu5kVVQn3vJ7JJUd0V_iY,1085
|
|
6
|
+
simple_vcs-1.2.0.dist-info/METADATA,sha256=ClxCpAV9I69xYpKnuHPa63fWIPT8pUDUVHUk8I5WApE,9438
|
|
7
|
+
simple_vcs-1.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
simple_vcs-1.2.0.dist-info/entry_points.txt,sha256=19JeWUvRFzwKF5p_iLQiSwCV3XTgxB7mkTLmFGrc_aY,45
|
|
9
|
+
simple_vcs-1.2.0.dist-info/top_level.txt,sha256=YcaiuqQjjXFL-H62tfC-hTcg-7sWFmLh65zghskauL4,11
|
|
10
|
+
simple_vcs-1.2.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
|