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 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
- """SimpleVCS - A simple version control system"""
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='Repository path')
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='Limit number of commits to show')
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
- """Quickly revert to a specific commit"""
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='Name for the snapshot')
108
+ @click.option('--name', help='Custom name for the snapshot (optional)')
62
109
  def snapshot(name):
63
- """Create a compressed snapshot of the current repository state"""
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 repository from a snapshot"""
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 to save space"""
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
- print(f"Initialized empty SimpleVCS repository at {self.repo_path}")
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} does not exist")
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"{file_path} is not a file")
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 {file_path} is not within the repository")
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
- print(f"Added {file_path.name} to staging area")
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 to commit")
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
- print(f"Committed changes with ID: {commit['id']}")
110
- print(f"Message: {commit['message']}")
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
- print(f"\nDifferences between commit {commit1['id']} and {commit2['id']}:")
139
- print("-" * 50)
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
- if new_files:
147
- print("New files:")
148
- for file in new_files:
149
- print(f" + {file}")
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
- if deleted_files:
154
- print("Deleted files:")
155
- for file in deleted_files:
156
- print(f" - {file}")
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
- if modified_files:
166
- print("Modified files:")
167
- for file in modified_files:
168
- print(f" M {file}")
169
-
170
- if not new_files and not deleted_files and not modified_files:
171
- print("No differences found")
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
- print("\nCommit History:")
189
- print("=" * 50)
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
- print(f"Commit ID: {commit['id']}")
193
- print(f"Date: {datetime.fromtimestamp(commit['timestamp']).strftime('%Y-%m-%d %H:%M:%S')}")
194
- print(f"Message: {commit['message']}")
195
- print(f"Files: {len(commit['files'])} file(s)")
196
- if commit.get('parent'):
197
- print(f"Parent: {commit['parent']}")
198
- print("-" * 30)
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
- print(f"\nRepository: {self.repo_path}")
211
- print(f"Current commit: {current_commit['id'] if current_commit else 'None'}")
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
- print("\nStaged files:")
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
- print(f" {file}")
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("\nNo files staged")
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. Run 'svcs init' first.")
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
- print(f"Reverted to commit {commit_id}: {commit['message']}")
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
- print(f"Created snapshot: {snapshot_path}")
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} does not exist")
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
- print(f"Restored from snapshot: {snapshot_path}")
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 self.objects_dir.glob('*'):
362
- if obj_file.is_file() and obj_file.stat().st_size > 1024: # Only compress files > 1KB
363
- # Create a compressed version with .gz extension
364
- compressed_path = obj_file.with_suffix(obj_file.suffix + '.gz')
365
- with open(obj_file, 'rb') as f_in:
366
- import gzip
367
- with gzip.open(compressed_path, 'wb') as f_out:
368
- f_out.writelines(f_in)
369
-
370
- # Replace original with compressed version
371
- obj_file.unlink()
372
- # Decompress back to original name for compatibility
373
- with gzip.open(compressed_path, 'rb') as f_in:
374
- with open(obj_file, 'wb') as f_out:
375
- f_out.write(f_in.read())
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
- print(f"Compression completed. Saved approximately {saved_space} bytes.")
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.1.0
4
- Summary: A simple version control system with unique features for easy version management
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,,