git-auto-pro 1.0.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_auto_pro/__init__.py +5 -0
- git_auto_pro/backup.py +87 -0
- git_auto_pro/cli.py +474 -0
- git_auto_pro/config.py +100 -0
- git_auto_pro/git_commands.py +426 -0
- git_auto_pro/github.py +318 -0
- git_auto_pro/gitignore_manager.py +400 -0
- git_auto_pro/scaffolding/__init__.py +1 -0
- git_auto_pro/scaffolding/github_templates.py +635 -0
- git_auto_pro/scaffolding/gitignore.py +149 -0
- git_auto_pro/scaffolding/hooks.py +331 -0
- git_auto_pro/scaffolding/license.py +109 -0
- git_auto_pro/scaffolding/project.py +83 -0
- git_auto_pro/scaffolding/readme.py +109 -0
- git_auto_pro/scaffolding/templates.py +336 -0
- git_auto_pro/scaffolding/workflows.py +236 -0
- git_auto_pro-1.0.0.dist-info/METADATA +477 -0
- git_auto_pro-1.0.0.dist-info/RECORD +22 -0
- git_auto_pro-1.0.0.dist-info/WHEEL +5 -0
- git_auto_pro-1.0.0.dist-info/entry_points.txt +2 -0
- git_auto_pro-1.0.0.dist-info/licenses/LICENSE +0 -0
- git_auto_pro-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
|
|
2
|
+
"""Interactive .gitignore file manager."""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Set
|
|
6
|
+
import questionary
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.tree import Tree
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_all_files(directory: Path = Path("."), max_depth: int = 3) -> List[Path]:
|
|
15
|
+
"""Get all files in directory recursively."""
|
|
16
|
+
all_files = []
|
|
17
|
+
|
|
18
|
+
def scan_dir(path: Path, depth: int = 0):
|
|
19
|
+
if depth > max_depth:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
for item in path.iterdir():
|
|
24
|
+
# Skip .git directory
|
|
25
|
+
if item.name == ".git":
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
if item.is_file():
|
|
29
|
+
all_files.append(item.relative_to(directory))
|
|
30
|
+
elif item.is_dir():
|
|
31
|
+
scan_dir(item, depth + 1)
|
|
32
|
+
except PermissionError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
scan_dir(directory)
|
|
36
|
+
return sorted(all_files)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_gitignore() -> Set[str]:
|
|
40
|
+
"""Load existing .gitignore patterns."""
|
|
41
|
+
gitignore_file = Path(".gitignore")
|
|
42
|
+
if not gitignore_file.exists():
|
|
43
|
+
return set()
|
|
44
|
+
|
|
45
|
+
patterns = set()
|
|
46
|
+
with open(gitignore_file, "r") as f:
|
|
47
|
+
for line in f:
|
|
48
|
+
line = line.strip()
|
|
49
|
+
if line and not line.startswith("#"):
|
|
50
|
+
patterns.add(line)
|
|
51
|
+
|
|
52
|
+
return patterns
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def save_gitignore(patterns: Set[str]) -> None:
|
|
56
|
+
"""Save .gitignore patterns."""
|
|
57
|
+
gitignore_file = Path(".gitignore")
|
|
58
|
+
|
|
59
|
+
# Read existing content to preserve comments
|
|
60
|
+
existing_content = ""
|
|
61
|
+
if gitignore_file.exists():
|
|
62
|
+
with open(gitignore_file, "r") as f:
|
|
63
|
+
existing_content = f.read()
|
|
64
|
+
|
|
65
|
+
# Write new patterns
|
|
66
|
+
with open(gitignore_file, "w") as f:
|
|
67
|
+
# Keep header comments if they exist
|
|
68
|
+
if existing_content and existing_content.startswith("#"):
|
|
69
|
+
lines = existing_content.split("\n")
|
|
70
|
+
for line in lines:
|
|
71
|
+
if line.startswith("#"):
|
|
72
|
+
f.write(line + "\n")
|
|
73
|
+
else:
|
|
74
|
+
break
|
|
75
|
+
f.write("\n")
|
|
76
|
+
|
|
77
|
+
# Write patterns
|
|
78
|
+
for pattern in sorted(patterns):
|
|
79
|
+
f.write(pattern + "\n")
|
|
80
|
+
|
|
81
|
+
console.print(f"[green]✓ Saved {len(patterns)} patterns to .gitignore[/green]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def display_file_tree(files: List[Path], ignored: Set[str]) -> None:
|
|
85
|
+
"""Display files in a tree structure."""
|
|
86
|
+
tree = Tree("📁 Project Files")
|
|
87
|
+
|
|
88
|
+
# Group by directory
|
|
89
|
+
dirs = {}
|
|
90
|
+
for file in files[:50]: # Limit to first 50 for display
|
|
91
|
+
parts = file.parts
|
|
92
|
+
if len(parts) == 1:
|
|
93
|
+
|
|
94
|
+
status = "🚫" if should_ignore(file, ignored) else "✅"
|
|
95
|
+
tree.add(f"{status} {file}")
|
|
96
|
+
else:
|
|
97
|
+
# File in directory
|
|
98
|
+
dir_name = parts[0]
|
|
99
|
+
if dir_name not in dirs:
|
|
100
|
+
dirs[dir_name] = tree.add(f"📂 {dir_name}/")
|
|
101
|
+
|
|
102
|
+
status = "🚫" if should_ignore(file, ignored) else "✅"
|
|
103
|
+
dirs[dir_name].add(f"{status} {'/'.join(parts[1:])}")
|
|
104
|
+
|
|
105
|
+
console.print(tree)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def should_ignore(file: Path, patterns: Set[str]) -> bool:
|
|
109
|
+
"""Check if file matches any ignore pattern."""
|
|
110
|
+
file_str = str(file)
|
|
111
|
+
|
|
112
|
+
for pattern in patterns:
|
|
113
|
+
# Simple pattern matching
|
|
114
|
+
if pattern.endswith("/"):
|
|
115
|
+
# Directory pattern
|
|
116
|
+
if file_str.startswith(pattern.rstrip("/")):
|
|
117
|
+
return True
|
|
118
|
+
elif pattern.startswith("*"):
|
|
119
|
+
# Extension pattern
|
|
120
|
+
if file_str.endswith(pattern[1:]):
|
|
121
|
+
return True
|
|
122
|
+
elif pattern in file_str:
|
|
123
|
+
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def interactive_gitignore_manager() -> None:
|
|
130
|
+
"""Interactive .gitignore file manager."""
|
|
131
|
+
console.print("[bold cyan]📝 Interactive .gitignore Manager[/bold cyan]\n")
|
|
132
|
+
|
|
133
|
+
# Load existing patterns
|
|
134
|
+
ignored_patterns = load_gitignore()
|
|
135
|
+
console.print(f"Loaded {len(ignored_patterns)} existing patterns\n")
|
|
136
|
+
|
|
137
|
+
while True:
|
|
138
|
+
choice = questionary.select(
|
|
139
|
+
"What would you like to do?",
|
|
140
|
+
choices=[
|
|
141
|
+
"📋 View all files (with ignore status)",
|
|
142
|
+
"➕ Add files/patterns to ignore",
|
|
143
|
+
"➖ Remove patterns from ignore",
|
|
144
|
+
"🎯 Browse and select files to ignore",
|
|
145
|
+
"📊 Show current .gitignore",
|
|
146
|
+
"🧹 Clean: Remove ignored files from git",
|
|
147
|
+
"💾 Save and exit",
|
|
148
|
+
"❌ Exit without saving",
|
|
149
|
+
]
|
|
150
|
+
).ask()
|
|
151
|
+
|
|
152
|
+
if not choice:
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if "View all files" in choice:
|
|
156
|
+
view_files_with_status(ignored_patterns)
|
|
157
|
+
|
|
158
|
+
elif "Add files/patterns" in choice:
|
|
159
|
+
add_patterns(ignored_patterns)
|
|
160
|
+
|
|
161
|
+
elif "Remove patterns" in choice:
|
|
162
|
+
remove_patterns(ignored_patterns)
|
|
163
|
+
|
|
164
|
+
elif "Browse and select" in choice:
|
|
165
|
+
browse_and_select(ignored_patterns)
|
|
166
|
+
|
|
167
|
+
elif "Show current" in choice:
|
|
168
|
+
show_current_gitignore(ignored_patterns)
|
|
169
|
+
|
|
170
|
+
elif "Clean: Remove" in choice:
|
|
171
|
+
clean_ignored_files()
|
|
172
|
+
|
|
173
|
+
elif "Save and exit" in choice:
|
|
174
|
+
save_gitignore(ignored_patterns)
|
|
175
|
+
console.print("[bold green]✓ Changes saved![/bold green]")
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
elif "Exit without saving" in choice:
|
|
179
|
+
confirm = questionary.confirm("Exit without saving changes?").ask()
|
|
180
|
+
if confirm:
|
|
181
|
+
console.print("[yellow]Changes discarded[/yellow]")
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def view_files_with_status(ignored: Set[str]) -> None:
|
|
186
|
+
"""View all files with their ignore status."""
|
|
187
|
+
console.print("\n[cyan]📁 Project Files:[/cyan]\n")
|
|
188
|
+
|
|
189
|
+
files = get_all_files()
|
|
190
|
+
|
|
191
|
+
if not files:
|
|
192
|
+
console.print("[yellow]No files found[/yellow]")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
table = Table(show_header=True)
|
|
196
|
+
table.add_column("Status", style="cyan", width=8)
|
|
197
|
+
table.add_column("File", style="white")
|
|
198
|
+
table.add_column("Reason", style="dim")
|
|
199
|
+
|
|
200
|
+
for file in files[:100]: # Limit to 100 files
|
|
201
|
+
if should_ignore(file, ignored):
|
|
202
|
+
status = "🚫 Ignored"
|
|
203
|
+
reason = get_ignore_reason(file, ignored)
|
|
204
|
+
else:
|
|
205
|
+
status = "✅ Tracked"
|
|
206
|
+
reason = ""
|
|
207
|
+
|
|
208
|
+
table.add_row(status, str(file), reason)
|
|
209
|
+
|
|
210
|
+
console.print(table)
|
|
211
|
+
|
|
212
|
+
if len(files) > 100:
|
|
213
|
+
console.print(f"\n[dim]... and {len(files) - 100} more files[/dim]")
|
|
214
|
+
|
|
215
|
+
input("\nPress Enter to continue...")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_ignore_reason(file: Path, patterns: Set[str]) -> str:
|
|
219
|
+
"""Get the pattern that causes file to be ignored."""
|
|
220
|
+
file_str = str(file)
|
|
221
|
+
|
|
222
|
+
for pattern in patterns:
|
|
223
|
+
if pattern.endswith("/") and file_str.startswith(pattern.rstrip("/")):
|
|
224
|
+
return f"matches: {pattern}"
|
|
225
|
+
elif pattern.startswith("*") and file_str.endswith(pattern[1:]):
|
|
226
|
+
return f"matches: {pattern}"
|
|
227
|
+
elif pattern in file_str:
|
|
228
|
+
return f"matches: {pattern}"
|
|
229
|
+
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def add_patterns(ignored: Set[str]) -> None:
|
|
234
|
+
"""Add new patterns to ignore."""
|
|
235
|
+
console.print("\n[cyan]➕ Add Patterns to .gitignore[/cyan]\n")
|
|
236
|
+
|
|
237
|
+
pattern_type = questionary.select(
|
|
238
|
+
"What would you like to add?",
|
|
239
|
+
choices=[
|
|
240
|
+
"📁 Folder (e.g., node_modules/)",
|
|
241
|
+
"📄 File extension (e.g., *.pyc)",
|
|
242
|
+
"📝 Specific file (e.g., config.local.py)",
|
|
243
|
+
"🎯 Custom pattern",
|
|
244
|
+
"📦 Common presets (Python, Node, etc.)",
|
|
245
|
+
]
|
|
246
|
+
).ask()
|
|
247
|
+
|
|
248
|
+
if "Folder" in pattern_type:
|
|
249
|
+
folder = questionary.text("Folder name (will add trailing /):").ask()
|
|
250
|
+
if folder:
|
|
251
|
+
pattern = folder if folder.endswith("/") else folder + "/"
|
|
252
|
+
ignored.add(pattern)
|
|
253
|
+
console.print(f"[green]✓ Added: {pattern}[/green]")
|
|
254
|
+
|
|
255
|
+
elif "File extension" in pattern_type:
|
|
256
|
+
ext = questionary.text("Extension (e.g., pyc, log):").ask()
|
|
257
|
+
if ext:
|
|
258
|
+
pattern = f"*.{ext}" if not ext.startswith("*.") else ext
|
|
259
|
+
ignored.add(pattern)
|
|
260
|
+
console.print(f"[green]✓ Added: {pattern}[/green]")
|
|
261
|
+
|
|
262
|
+
elif "Specific file" in pattern_type:
|
|
263
|
+
filename = questionary.text("File name or path:").ask()
|
|
264
|
+
if filename:
|
|
265
|
+
ignored.add(filename)
|
|
266
|
+
console.print(f"[green]✓ Added: {filename}[/green]")
|
|
267
|
+
|
|
268
|
+
elif "Custom pattern" in pattern_type:
|
|
269
|
+
pattern = questionary.text("Enter pattern:").ask()
|
|
270
|
+
if pattern:
|
|
271
|
+
ignored.add(pattern)
|
|
272
|
+
console.print(f"[green]✓ Added: {pattern}[/green]")
|
|
273
|
+
|
|
274
|
+
elif "Common presets" in pattern_type:
|
|
275
|
+
add_preset(ignored)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def add_preset(ignored: Set[str]) -> None:
|
|
279
|
+
"""Add common preset patterns."""
|
|
280
|
+
presets = {
|
|
281
|
+
"Python": ["__pycache__/", "*.py[cod]", "*.so", ".Python", "*.egg-info/",
|
|
282
|
+
"dist/", "build/", ".pytest_cache/", ".coverage", "venv/", "env/"],
|
|
283
|
+
"Node.js": ["node_modules/", "npm-debug.log", "*.tsbuildinfo", "dist/", ".env"],
|
|
284
|
+
"Build artifacts": ["*.o", "*.a", "*.lib", "*.dll", "*.exe", "*.out"],
|
|
285
|
+
"IDEs": [".vscode/", ".idea/", "*.swp", "*.swo", ".DS_Store"],
|
|
286
|
+
"Logs": ["*.log", "logs/"],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
preset = questionary.select(
|
|
290
|
+
"Select preset:",
|
|
291
|
+
choices=list(presets.keys())
|
|
292
|
+
).ask()
|
|
293
|
+
|
|
294
|
+
if preset:
|
|
295
|
+
patterns = presets[preset]
|
|
296
|
+
for pattern in patterns:
|
|
297
|
+
ignored.add(pattern)
|
|
298
|
+
console.print(f"[green]✓ Added {len(patterns)} patterns from {preset} preset[/green]")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def remove_patterns(ignored: Set[str]) -> None:
|
|
302
|
+
"""Remove patterns from ignore list."""
|
|
303
|
+
if not ignored:
|
|
304
|
+
console.print("[yellow]No patterns to remove[/yellow]")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
patterns_to_remove = questionary.checkbox(
|
|
308
|
+
"Select patterns to remove:",
|
|
309
|
+
choices=sorted(ignored)
|
|
310
|
+
).ask()
|
|
311
|
+
|
|
312
|
+
if patterns_to_remove:
|
|
313
|
+
for pattern in patterns_to_remove:
|
|
314
|
+
ignored.discard(pattern)
|
|
315
|
+
console.print(f"[green]✓ Removed {len(patterns_to_remove)} patterns[/green]")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def browse_and_select(ignored: Set[str]) -> None:
|
|
319
|
+
"""Browse files and select which to ignore."""
|
|
320
|
+
console.print("\n[cyan]🎯 Browse and Select Files[/cyan]\n")
|
|
321
|
+
|
|
322
|
+
files = get_all_files()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
tracked_files = [f for f in files if not should_ignore(f, ignored)]
|
|
326
|
+
|
|
327
|
+
if not tracked_files:
|
|
328
|
+
console.print("[yellow]All files are already ignored[/yellow]")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
# Show in groups
|
|
332
|
+
file_choices = [str(f) for f in tracked_files[:200]] # Limit to 200
|
|
333
|
+
|
|
334
|
+
selected = questionary.checkbox(
|
|
335
|
+
"Select files/folders to ignore:",
|
|
336
|
+
choices=file_choices
|
|
337
|
+
).ask()
|
|
338
|
+
|
|
339
|
+
if selected:
|
|
340
|
+
for item in selected:
|
|
341
|
+
ignored.add(item)
|
|
342
|
+
console.print(f"[green]✓ Added {len(selected)} items to ignore[/green]")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def show_current_gitignore(ignored: Set[str]) -> None:
|
|
346
|
+
"""Show current .gitignore patterns."""
|
|
347
|
+
console.print("\n[cyan]📊 Current .gitignore Patterns:[/cyan]\n")
|
|
348
|
+
|
|
349
|
+
if not ignored:
|
|
350
|
+
console.print("[yellow]No patterns defined[/yellow]")
|
|
351
|
+
else:
|
|
352
|
+
table = Table(show_header=True)
|
|
353
|
+
table.add_column("#", style="dim", width=5)
|
|
354
|
+
table.add_column("Pattern", style="cyan")
|
|
355
|
+
table.add_column("Type", style="yellow")
|
|
356
|
+
|
|
357
|
+
for i, pattern in enumerate(sorted(ignored), 1):
|
|
358
|
+
if pattern.endswith("/"):
|
|
359
|
+
ptype = "Folder"
|
|
360
|
+
elif pattern.startswith("*"):
|
|
361
|
+
ptype = "Extension"
|
|
362
|
+
else:
|
|
363
|
+
ptype = "File/Pattern"
|
|
364
|
+
|
|
365
|
+
table.add_row(str(i), pattern, ptype)
|
|
366
|
+
|
|
367
|
+
console.print(table)
|
|
368
|
+
|
|
369
|
+
input("\nPress Enter to continue...")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def clean_ignored_files() -> None:
|
|
373
|
+
"""Remove ignored files from git tracking."""
|
|
374
|
+
console.print("\n[yellow]⚠️ This will remove ignored files from git tracking[/yellow]")
|
|
375
|
+
console.print("[dim]Files will remain on disk but won't be tracked[/dim]\n")
|
|
376
|
+
|
|
377
|
+
confirm = questionary.confirm("Continue?").ask()
|
|
378
|
+
|
|
379
|
+
if confirm:
|
|
380
|
+
import subprocess
|
|
381
|
+
try:
|
|
382
|
+
|
|
383
|
+
result = subprocess.run(
|
|
384
|
+
["git", "rm", "-r", "--cached", "."],
|
|
385
|
+
capture_output=True,
|
|
386
|
+
text=True
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Re-add everything (respecting .gitignore)
|
|
390
|
+
subprocess.run(["git", "add", "."])
|
|
391
|
+
|
|
392
|
+
console.print("[green]✓ Cleaned ignored files from git[/green]")
|
|
393
|
+
console.print("[dim]Run 'git-auto commit' to save changes[/dim]")
|
|
394
|
+
except Exception as e:
|
|
395
|
+
console.print(f"[red]✗ Error: {e}[/red]")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Scaffolding modules for project generation."""
|