git-uncommitted-scanner 0.1.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.
File without changes
git_scanner/main.py ADDED
@@ -0,0 +1,211 @@
1
+ import sys
2
+ import subprocess
3
+ from pathlib import Path
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich import print as rprint
8
+ from textual.app import App, ComposeResult
9
+ from textual.widgets import Header, Footer, DataTable, Label, LoadingIndicator
10
+ from textual.binding import Binding
11
+ from textual.worker import get_current_worker
12
+
13
+ # ---------------------------------------------------------
14
+ # CORE LOGIC
15
+ # ---------------------------------------------------------
16
+ def is_repo_dirty(repo_path: Path) -> bool:
17
+ """Checks if a git repo has uncommitted changes."""
18
+ try:
19
+ result = subprocess.run(
20
+ ['git', 'status', '--porcelain'],
21
+ cwd=repo_path, capture_output=True, text=True, check=True
22
+ )
23
+ return bool(result.stdout.strip())
24
+ except Exception:
25
+ return False
26
+
27
+ def open_external_terminal(path: str) -> None:
28
+ """Cross-platform function to open terminal and execute 'git status'."""
29
+ path_obj = Path(path).resolve()
30
+
31
+ # ➔ FIX: Using 'cwd=path_obj' forces the terminal to spawn inside the repo natively.
32
+ if sys.platform == "win32":
33
+ subprocess.Popen('start cmd /K "git status"', cwd=path_obj, shell=True)
34
+ elif sys.platform == "darwin":
35
+ # macOS: Use AppleScript to strictly open a new Terminal window with commands
36
+ script = f'''
37
+ osascript -e 'tell application "Terminal" to do script "cd \\"{path_obj}\\" && git status"' -e 'tell application "Terminal" to activate'
38
+ '''
39
+ subprocess.Popen(script, shell=True)
40
+ else:
41
+ terminals = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'alacritty', 'xterm']
42
+ for term in terminals:
43
+ if subprocess.run(['which', term], capture_output=True).returncode == 0:
44
+ if term == 'gnome-terminal':
45
+ subprocess.Popen([term, '--', 'bash', '-c', 'git status && exec bash'], cwd=path_obj)
46
+ else:
47
+ subprocess.Popen([term, '-e', 'bash -c "git status && exec bash"'], cwd=path_obj)
48
+ break
49
+
50
+ # ---------------------------------------------------------
51
+ # TUI IMPLEMENTATION
52
+ # ---------------------------------------------------------
53
+ class GitScannerTUI(App):
54
+ """High-Tech TUI for navigating repositories."""
55
+
56
+ # Premium Neon-Cyan Aesthetic
57
+ CSS = """
58
+ Screen { background: #0a0a0a; }
59
+ Header { background: #002222; color: #00ffff; text-style: bold; }
60
+ Footer { background: #002222; color: #00ffff; }
61
+
62
+ DataTable {
63
+ height: 1fr;
64
+ margin: 1 2;
65
+ border: round #00ffff;
66
+ background: #051515;
67
+ color: #e0ffff;
68
+ }
69
+ DataTable > .datatable--header { background: #004444; color: #00ffff; text-style: bold; }
70
+ DataTable > .datatable--cursor { background: #00ffff; color: #000000; text-style: bold; }
71
+
72
+ #status-bar {
73
+ dock: bottom;
74
+ height: 3;
75
+ content-align: center middle;
76
+ background: #001111;
77
+ color: #00ffff;
78
+ border-top: solid #00ffff;
79
+ }
80
+
81
+ LoadingIndicator { color: #00ffff; height: 1fr; }
82
+ """
83
+
84
+ BINDINGS = [
85
+ Binding("q", "quit", "Quit"),
86
+ Binding("o", "open_terminal", "Open Workspace"),
87
+ Binding("r", "refresh_scan", "Refresh Scan")
88
+ ]
89
+
90
+ def __init__(self, target_dir: Path):
91
+ super().__init__()
92
+ self.target_dir = target_dir
93
+
94
+ def compose(self) -> ComposeResult:
95
+ yield Header(show_clock=True)
96
+ yield LoadingIndicator(id="loader")
97
+ yield DataTable(id="repo_table")
98
+ yield Label("INITIALIZING SYSTEM...", id="status-bar")
99
+ yield Footer()
100
+
101
+ def on_mount(self) -> None:
102
+ table = self.query_one(DataTable)
103
+ table.cursor_type = "row"
104
+ table.zebra_stripes = True
105
+ table.add_columns("ID", "Uncommitted Repository Target")
106
+ self.action_refresh_scan()
107
+
108
+ def action_refresh_scan(self) -> None:
109
+ """Triggers the UI loading state and starts the background worker."""
110
+ table = self.query_one(DataTable)
111
+ loader = self.query_one("#loader", LoadingIndicator)
112
+
113
+ table.display = False
114
+ loader.display = True
115
+ self.query_one("#status-bar", Label).update(f"⏳ SCANNING DIRECTORY: {self.target_dir}")
116
+
117
+ self.run_worker(self.scan_directories, thread=True, exclusive=True)
118
+
119
+ def scan_directories(self) -> None:
120
+ worker = get_current_worker()
121
+ dirty_repos = []
122
+
123
+ for git_dir in self.target_dir.rglob('.git'):
124
+ if worker.is_cancelled:
125
+ return
126
+ repo_path = git_dir.parent
127
+ if is_repo_dirty(repo_path):
128
+ dirty_repos.append(repo_path)
129
+
130
+ self.call_from_thread(self.update_table, dirty_repos)
131
+
132
+ def update_table(self, repos: list[Path]) -> None:
133
+ table = self.query_one(DataTable)
134
+ loader = self.query_one("#loader", LoadingIndicator)
135
+ status = self.query_one("#status-bar", Label)
136
+
137
+ table.clear()
138
+ loader.display = False
139
+ table.display = True
140
+
141
+ if not repos:
142
+ status.update("✅ ALL REPOSITORIES SECURED AND COMMITTED")
143
+ return
144
+
145
+ status.update(f"⚠️ DETECTED {len(repos)} REPOSITORIES REQUIRING ATTENTION")
146
+ for idx, repo in enumerate(repos, 1):
147
+ table.add_row(str(idx), str(repo))
148
+
149
+ def action_open_terminal(self) -> None:
150
+ table = self.query_one(DataTable)
151
+ try:
152
+ # Safely grab the exact string from column index 1 of the highlighted row
153
+ row_index = table.cursor_row
154
+ # ➔ FIX: Ensure the extracted cell is cast to a standard string
155
+ repo_path = str(table.get_row_at(row_index)[1])
156
+ open_external_terminal(repo_path)
157
+ except Exception:
158
+ self.notify("ERROR: TARGET A REPOSITORY FIRST", severity="error")
159
+
160
+ # ---------------------------------------------------------
161
+ # CLI & ROUTING
162
+ # ---------------------------------------------------------
163
+ app = typer.Typer(help="Scan directories for uncommitted Git repositories.")
164
+ console = Console()
165
+
166
+ @app.command()
167
+ def scan(
168
+ # ➔ FIX: Default to "." (current directory) if no argument is provided
169
+ directory: str = typer.Argument(".", help="Target directory to scan (defaults to current directory)"),
170
+ interactive: bool = typer.Option(False, "--interactive", "-i", help="Launch the interactive TUI")
171
+ ):
172
+ """Deep scan a directory for uncommitted Git repositories."""
173
+ base_path = Path(directory).expanduser().resolve()
174
+
175
+ if not base_path.exists() or not base_path.is_dir():
176
+ rprint(f"[bold red]❌ Error:[/bold red] Directory '{base_path}' does not exist.")
177
+ raise typer.Exit(code=1)
178
+
179
+ # Route 1: TUI Mode
180
+ if interactive:
181
+ try:
182
+ tui_app = GitScannerTUI(base_path)
183
+ tui_app.run()
184
+ rprint("\n[bold cyan]✅ Workspace Scanner Terminated Successfully.[/bold cyan]\n")
185
+ except Exception as e:
186
+ rprint(f"\n[bold red]❌ CRITICAL TUI ERROR:[/bold red] {e}")
187
+ console.print_exception()
188
+ return
189
+
190
+ # Route 2: CLI Mode
191
+ with console.status(f"[bold cyan]Scanning {base_path}...[/bold cyan]", spinner="dots"):
192
+ dirty_repos = [
193
+ git_dir.parent for git_dir in base_path.rglob('.git')
194
+ if is_repo_dirty(git_dir.parent)
195
+ ]
196
+
197
+ if not dirty_repos:
198
+ rprint("[bold green]✅ All repositories are clean and committed![/bold green]")
199
+ return
200
+
201
+ table = Table(title="⚠️ Uncommitted Repositories", show_header=True, header_style="bold magenta")
202
+ table.add_column("No.", style="dim", width=4)
203
+ table.add_column("Repository Path", style="cyan")
204
+
205
+ for idx, repo in enumerate(dirty_repos, 1):
206
+ table.add_row(str(idx), str(repo))
207
+
208
+ console.print(table)
209
+
210
+ if __name__ == "__main__":
211
+ app()
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-uncommitted-scanner
3
+ Version: 0.1.0
4
+ Summary: A lightweight CLI to scan for uncommitted Git repositories.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer>=0.9.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: textual>=0.50.0
10
+
11
+ # 🔍 Git Uncommitted Scanner
12
+
13
+ ![Python](https://img.shields.io/badge/python-3.8+-blue.svg)
14
+ ![Typer](https://img.shields.io/badge/typer-CLI-black.svg)
15
+ ![Textual](https://img.shields.io/badge/textual-TUI-cyan.svg)
16
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
17
+
18
+ **A lightning-fast, cross-platform utility that recursively deep-scans directories to instantly find Git repositories with uncommitted changes.**
19
+
20
+ ---
21
+
22
+ ## ⚡ Why `git-uncommitted-scanner`?
23
+
24
+ Tired of discovering forgotten, uncommitted work in scattered repositories months later? `git-uncommitted-scanner` hunts down pending changes across your entire filesystem with asynchronous speed, presenting them in either a clean CLI table or a high-tech interactive TUI.
25
+
26
+ ---
27
+
28
+ ## 📦 Installation
29
+
30
+ Install globally via `pip` (or `pipx` for isolated environments):
31
+
32
+ ```bash
33
+ pip install git-uncommitted-scanner
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 🚀 Usage
39
+
40
+ The tool provides a single command `scanrepos` that works in two modes: standard CLI and interactive TUI.
41
+
42
+ ### Standard CLI Mode
43
+ Run a blazing-fast background scan that outputs a clean, readable table of repositories needing your attention.
44
+
45
+ ```bash
46
+ # Scan the current directory
47
+ scanrepos
48
+
49
+ # Scan a specific path
50
+ scanrepos /path/to/your/projects
51
+ ```
52
+
53
+ ### Interactive TUI Mode
54
+ Launch the high-tech Neon-Cyan Terminal User Interface for an interactive experience. Navigate repositories with your keyboard and spawn a native terminal to instantly handle changes.
55
+
56
+ ```bash
57
+ # Launch TUI in the current directory
58
+ scanrepos -i
59
+
60
+ # Launch TUI for a specific path
61
+ scanrepos -i /path/to/your/projects
62
+ ```
63
+
64
+ ---
65
+
66
+ ## ✨ Key Features
67
+
68
+ * **Asynchronous Deep-Scanning**: Rapidly traverses deep folder structures in the background without blocking the UI.
69
+ * **Lightning-Fast CLI Output**: Instantly summarizes findings in a clean, easily readable terminal table.
70
+ * **High-Tech Neon-Cyan TUI**: A beautiful, interactive textual interface accessible via a simple `-i` flag.
71
+ * **Keyboard Navigation**: Full keyboard support in the TUI for seamless, mouse-free workflow.
72
+ * **OS-Aware Terminal Spawning**: Select a repository in the TUI and instantly spawn a native terminal (Windows, macOS, or Linux) running `git status` right in that directory.
73
+ * **Cross-Platform**: Built to work flawlessly across Windows, macOS, and Linux environments.
74
+
75
+ ---
76
+
77
+ *Built with [Python](https://www.python.org/), [Typer](https://typer.tiangolo.com/), and [Textual](https://textual.textualize.io/).*
@@ -0,0 +1,7 @@
1
+ git_scanner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ git_scanner/main.py,sha256=hzDOw2woi4KZ_UtZ2lPwd29sYaIt_NB2Nm6CRchX5q8,8149
3
+ git_uncommitted_scanner-0.1.0.dist-info/METADATA,sha256=oaPgElQ9Ym9y2Ugtl40Ppye9PItAQuP6Zu-4kLvjeHk,2842
4
+ git_uncommitted_scanner-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ git_uncommitted_scanner-0.1.0.dist-info/entry_points.txt,sha256=v4GrKLtYnFGpMcKek3qyTrVCC3GUzV848W7-oGeieqs,51
6
+ git_uncommitted_scanner-0.1.0.dist-info/top_level.txt,sha256=TuzP1RnqSuR-Vvjeg6R1cI5fuI9b1krEI5SE9UzLjz4,12
7
+ git_uncommitted_scanner-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scanrepos = git_scanner.main:app
@@ -0,0 +1 @@
1
+ git_scanner