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 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
- @click.group()
5
- @click.version_option()
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='Repository path')
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='Limit number of commits to show')
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
- """Quickly revert to a specific commit"""
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='Name for the snapshot')
184
+ @click.option('--name', help='Custom name for the snapshot (optional)')
62
185
  def snapshot(name):
63
- """Create a compressed snapshot of the current repository state"""
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 repository from a snapshot"""
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 to save space"""
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
- print(f"Initialized empty SimpleVCS repository at {self.repo_path}")
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} does not exist")
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"{file_path} is not a file")
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 {file_path} is not within the repository")
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
- print(f"Added {file_path.name} to staging area")
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 to commit")
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
- print(f"Committed changes with ID: {commit['id']}")
110
- print(f"Message: {commit['message']}")
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
- print(f"\nDifferences between commit {commit1['id']} and {commit2['id']}:")
139
- print("-" * 50)
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
- if new_files:
147
- print("New files:")
148
- for file in new_files:
149
- print(f" + {file}")
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
- if deleted_files:
154
- print("Deleted files:")
155
- for file in deleted_files:
156
- print(f" - {file}")
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
- 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
-
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
- print("No commits found")
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
- print("\nCommit History:")
189
- print("=" * 50)
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
- 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
-
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
- print(f"\nRepository: {self.repo_path}")
211
- print(f"Current commit: {current_commit['id'] if current_commit else 'None'}")
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
- print("\nStaged files:")
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
- print(f" {file}")
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("\nNo files staged")
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. Run 'svcs init' first.")
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
- print(f"Reverted to commit {commit_id}: {commit['message']}")
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
- print(f"Created snapshot: {snapshot_path}")
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} does not exist")
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
- print(f"Restored from snapshot: {snapshot_path}")
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 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()
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
- print(f"Compression completed. Saved approximately {saved_space} bytes.")
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.1.0
4
- Summary: A simple version control system with unique features for easy version management
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,,