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.
- git_scanner/__init__.py +0 -0
- git_scanner/main.py +211 -0
- git_uncommitted_scanner-0.1.0.dist-info/METADATA +77 -0
- git_uncommitted_scanner-0.1.0.dist-info/RECORD +7 -0
- git_uncommitted_scanner-0.1.0.dist-info/WHEEL +5 -0
- git_uncommitted_scanner-0.1.0.dist-info/entry_points.txt +2 -0
- git_uncommitted_scanner-0.1.0.dist-info/top_level.txt +1 -0
git_scanner/__init__.py
ADDED
|
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
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+

|
|
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 @@
|
|
|
1
|
+
git_scanner
|