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,426 @@
|
|
|
1
|
+
"""Git command operations using GitPython."""
|
|
2
|
+
|
|
3
|
+
import git
|
|
4
|
+
from typing import Optional, List, Any
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import questionary
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_repo() -> git.Repo:
|
|
16
|
+
"""Get current Git repository."""
|
|
17
|
+
try:
|
|
18
|
+
return git.Repo(".", search_parent_directories=True)
|
|
19
|
+
except git.InvalidGitRepositoryError:
|
|
20
|
+
console.print("[red]✗ Not a git repository. Run 'git-auto init' first.[/red]")
|
|
21
|
+
raise
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def git_init(connect: Optional[str] = None) -> None:
|
|
25
|
+
"""Initialize Git repository and optionally connect to remote."""
|
|
26
|
+
try:
|
|
27
|
+
repo_exists = Path(".git").exists()
|
|
28
|
+
|
|
29
|
+
if repo_exists:
|
|
30
|
+
console.print("[yellow]Repository already initialized[/yellow]")
|
|
31
|
+
repo = git.Repo(".")
|
|
32
|
+
else:
|
|
33
|
+
repo = git.Repo.init(".")
|
|
34
|
+
console.print("[green]✓ Initialized empty Git repository[/green]")
|
|
35
|
+
|
|
36
|
+
# Add remote even if repo already exists (FIXED!)
|
|
37
|
+
if connect:
|
|
38
|
+
try:
|
|
39
|
+
# Check if remote already exists
|
|
40
|
+
remotes = {remote.name: remote for remote in repo.remotes}
|
|
41
|
+
|
|
42
|
+
if "origin" in remotes:
|
|
43
|
+
# Remote exists, check if URL is different
|
|
44
|
+
existing_url = remotes["origin"].url
|
|
45
|
+
if existing_url == connect:
|
|
46
|
+
console.print(f"[yellow]Remote 'origin' already set to: {connect}[/yellow]")
|
|
47
|
+
else:
|
|
48
|
+
# Update the URL
|
|
49
|
+
remotes["origin"].set_url(connect)
|
|
50
|
+
console.print(f"[green]✓ Updated remote 'origin' to: {connect}[/green]")
|
|
51
|
+
else:
|
|
52
|
+
# Add new remote
|
|
53
|
+
repo.create_remote("origin", connect)
|
|
54
|
+
console.print(f"[green]✓ Added remote 'origin': {connect}[/green]")
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
console.print(f"[red]✗ Failed to configure remote: {e}[/red]")
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
console.print(f"[red]✗ Failed to initialize repository: {e}[/red]")
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def git_add(files: Optional[List[str]] = None, all: bool = False) -> None:
|
|
66
|
+
"""Stage files for commit."""
|
|
67
|
+
try:
|
|
68
|
+
repo = get_repo()
|
|
69
|
+
|
|
70
|
+
if all or not files:
|
|
71
|
+
# Use getattr to avoid type errors
|
|
72
|
+
git_cmd = getattr(repo, 'git')
|
|
73
|
+
git_cmd.add(A=True)
|
|
74
|
+
console.print("[green]✓ Staged all changes[/green]")
|
|
75
|
+
else:
|
|
76
|
+
repo.index.add(files)
|
|
77
|
+
console.print(f"[green]✓ Staged: {', '.join(files)}[/green]")
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
console.print(f"[red]✗ Failed to stage files: {e}[/red]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def git_commit(message: str, conventional: bool = False, amend: bool = False) -> None:
|
|
84
|
+
"""Commit staged changes."""
|
|
85
|
+
try:
|
|
86
|
+
repo = get_repo()
|
|
87
|
+
|
|
88
|
+
if conventional:
|
|
89
|
+
commit_type = questionary.select(
|
|
90
|
+
"Select commit type:",
|
|
91
|
+
choices=[
|
|
92
|
+
"feat: A new feature",
|
|
93
|
+
"fix: A bug fix",
|
|
94
|
+
"docs: Documentation changes",
|
|
95
|
+
"style: Code style changes",
|
|
96
|
+
"refactor: Code refactoring",
|
|
97
|
+
"test: Adding tests",
|
|
98
|
+
"chore: Maintenance tasks",
|
|
99
|
+
]
|
|
100
|
+
).ask()
|
|
101
|
+
|
|
102
|
+
if commit_type:
|
|
103
|
+
prefix = commit_type.split(":")[0]
|
|
104
|
+
message = f"{prefix}: {message}"
|
|
105
|
+
|
|
106
|
+
if amend:
|
|
107
|
+
git_cmd = getattr(repo, 'git')
|
|
108
|
+
git_cmd.commit("--amend", "-m", message)
|
|
109
|
+
console.print("[green]✓ Amended previous commit[/green]")
|
|
110
|
+
else:
|
|
111
|
+
repo.index.commit(message)
|
|
112
|
+
console.print(f"[green]✓ Committed: {message}[/green]")
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
console.print(f"[red]✗ Failed to commit: {e}[/red]")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def git_push(message: Optional[str] = None, branch: str = "main", force: bool = False) -> None:
|
|
119
|
+
"""Push commits to remote."""
|
|
120
|
+
try:
|
|
121
|
+
repo = get_repo()
|
|
122
|
+
git_cmd = getattr(repo, 'git')
|
|
123
|
+
|
|
124
|
+
# If message provided, do add + commit + push
|
|
125
|
+
if message:
|
|
126
|
+
git_cmd.add(A=True)
|
|
127
|
+
repo.index.commit(message)
|
|
128
|
+
console.print(f"[green]✓ Committed: {message}[/green]")
|
|
129
|
+
|
|
130
|
+
# Check if remote exists
|
|
131
|
+
if not repo.remotes:
|
|
132
|
+
console.print("[red]✗ No remote configured. Use 'git-auto init --connect <url>' first.[/red]")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Push to remote
|
|
136
|
+
if force:
|
|
137
|
+
git_cmd.push("origin", branch, force=True)
|
|
138
|
+
console.print(f"[yellow]⚠ Force pushed to {branch}[/yellow]")
|
|
139
|
+
else:
|
|
140
|
+
git_cmd.push("origin", branch)
|
|
141
|
+
console.print(f"[green]✓ Pushed to {branch}[/green]")
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
console.print(f"[red]✗ Failed to push: {e}[/red]")
|
|
145
|
+
console.print("[yellow]Hint: Make sure remote is configured with 'git-auto init --connect <url>'[/yellow]")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def git_pull(branch: Optional[str] = None, rebase: bool = False) -> None:
|
|
149
|
+
"""Pull changes from remote."""
|
|
150
|
+
try:
|
|
151
|
+
repo = get_repo()
|
|
152
|
+
git_cmd = getattr(repo, 'git')
|
|
153
|
+
|
|
154
|
+
if branch:
|
|
155
|
+
if rebase:
|
|
156
|
+
git_cmd.pull("origin", branch, rebase=True)
|
|
157
|
+
else:
|
|
158
|
+
git_cmd.pull("origin", branch)
|
|
159
|
+
else:
|
|
160
|
+
if rebase:
|
|
161
|
+
git_cmd.pull(rebase=True)
|
|
162
|
+
else:
|
|
163
|
+
git_cmd.pull()
|
|
164
|
+
|
|
165
|
+
console.print("[green]✓ Pulled changes from remote[/green]")
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
console.print(f"[red]✗ Failed to pull: {e}[/red]")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def git_status(short: bool = False) -> None:
|
|
172
|
+
"""Show working tree status."""
|
|
173
|
+
try:
|
|
174
|
+
repo = get_repo()
|
|
175
|
+
git_cmd = getattr(repo, 'git')
|
|
176
|
+
|
|
177
|
+
if short:
|
|
178
|
+
status = git_cmd.status("-s")
|
|
179
|
+
console.print(status)
|
|
180
|
+
else:
|
|
181
|
+
# Custom formatted status
|
|
182
|
+
changed = [item.a_path for item in repo.index.diff(None)]
|
|
183
|
+
staged = [item.a_path for item in repo.index.diff("HEAD")]
|
|
184
|
+
untracked = repo.untracked_files
|
|
185
|
+
|
|
186
|
+
table = Table(title="Repository Status", show_header=True)
|
|
187
|
+
table.add_column("Status", style="cyan")
|
|
188
|
+
table.add_column("Files", style="white")
|
|
189
|
+
|
|
190
|
+
if staged:
|
|
191
|
+
table.add_row("Staged", "\n".join(f"✓ {f}" for f in staged))
|
|
192
|
+
if changed:
|
|
193
|
+
table.add_row("Modified", "\n".join(f"M {f}" for f in changed))
|
|
194
|
+
if untracked:
|
|
195
|
+
table.add_row("Untracked", "\n".join(f"? {f}" for f in untracked))
|
|
196
|
+
|
|
197
|
+
if not (staged or changed or untracked):
|
|
198
|
+
console.print("[green]✓ Working tree clean[/green]")
|
|
199
|
+
else:
|
|
200
|
+
console.print(table)
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
console.print(f"[red]✗ Failed to get status: {e}[/red]")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def git_log(limit: int = 10, oneline: bool = False, graph: bool = False) -> None:
|
|
207
|
+
"""Show commit history."""
|
|
208
|
+
try:
|
|
209
|
+
repo = get_repo()
|
|
210
|
+
git_cmd = getattr(repo, 'git')
|
|
211
|
+
|
|
212
|
+
if oneline:
|
|
213
|
+
log = git_cmd.log(f"--oneline", f"-{limit}")
|
|
214
|
+
console.print(log)
|
|
215
|
+
elif graph:
|
|
216
|
+
log = git_cmd.log(f"--graph", f"--oneline", f"-{limit}")
|
|
217
|
+
console.print(log)
|
|
218
|
+
else:
|
|
219
|
+
table = Table(title=f"Last {limit} Commits", show_header=True)
|
|
220
|
+
table.add_column("Hash", style="yellow", width=8)
|
|
221
|
+
table.add_column("Author", style="cyan", width=20)
|
|
222
|
+
table.add_column("Date", style="green", width=20)
|
|
223
|
+
table.add_column("Message", style="white")
|
|
224
|
+
|
|
225
|
+
for commit in list(repo.iter_commits())[:limit]:
|
|
226
|
+
date = datetime.fromtimestamp(commit.committed_date).strftime("%Y-%m-%d %H:%M")
|
|
227
|
+
# Convert all fields to strings explicitly
|
|
228
|
+
hash_str = str(commit.hexsha[:8])
|
|
229
|
+
author_str = str(commit.author.name)
|
|
230
|
+
date_str = str(date)
|
|
231
|
+
message_str = str(commit.message.strip())
|
|
232
|
+
|
|
233
|
+
table.add_row(hash_str, author_str, date_str, message_str)
|
|
234
|
+
|
|
235
|
+
console.print(table)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
console.print(f"[red]✗ Failed to show log: {e}[/red]")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def git_branch(name: Optional[str] = None, list: bool = False, remote: bool = False) -> None:
|
|
242
|
+
"""Create or list branches."""
|
|
243
|
+
try:
|
|
244
|
+
repo = get_repo()
|
|
245
|
+
git_cmd = getattr(repo, 'git')
|
|
246
|
+
|
|
247
|
+
if list or not name:
|
|
248
|
+
branches_output = git_cmd.branch("-r" if remote else "-a")
|
|
249
|
+
branches = branches_output.split("\n")
|
|
250
|
+
|
|
251
|
+
table = Table(title="Branches", show_header=False)
|
|
252
|
+
table.add_column("Branch", style="cyan")
|
|
253
|
+
|
|
254
|
+
for branch in branches:
|
|
255
|
+
branch = branch.strip()
|
|
256
|
+
if branch.startswith("*"):
|
|
257
|
+
table.add_row(f"[bold green]{branch}[/bold green]")
|
|
258
|
+
else:
|
|
259
|
+
table.add_row(branch)
|
|
260
|
+
|
|
261
|
+
console.print(table)
|
|
262
|
+
else:
|
|
263
|
+
repo.create_head(name)
|
|
264
|
+
console.print(f"[green]✓ Created branch: {name}[/green]")
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
console.print(f"[red]✗ Failed to manage branches: {e}[/red]")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def git_switch(name: str, create: bool = False) -> None:
|
|
271
|
+
"""Switch to a different branch."""
|
|
272
|
+
try:
|
|
273
|
+
repo = get_repo()
|
|
274
|
+
git_cmd = getattr(repo, 'git')
|
|
275
|
+
|
|
276
|
+
if create:
|
|
277
|
+
git_cmd.checkout("-b", name)
|
|
278
|
+
console.print(f"[green]✓ Created and switched to: {name}[/green]")
|
|
279
|
+
else:
|
|
280
|
+
git_cmd.checkout(name)
|
|
281
|
+
console.print(f"[green]✓ Switched to: {name}[/green]")
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
console.print(f"[red]✗ Failed to switch branch: {e}[/red]")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def git_delete_branch(name: str, force: bool = False) -> None:
|
|
288
|
+
"""Delete a branch."""
|
|
289
|
+
try:
|
|
290
|
+
repo = get_repo()
|
|
291
|
+
git_cmd = getattr(repo, 'git')
|
|
292
|
+
|
|
293
|
+
if force:
|
|
294
|
+
git_cmd.branch("-D", name)
|
|
295
|
+
else:
|
|
296
|
+
git_cmd.branch("-d", name)
|
|
297
|
+
|
|
298
|
+
console.print(f"[green]✓ Deleted branch: {name}[/green]")
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
console.print(f"[red]✗ Failed to delete branch: {e}[/red]")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def git_stash(message: Optional[str] = None, list: bool = False) -> None:
|
|
305
|
+
"""Stash uncommitted changes."""
|
|
306
|
+
try:
|
|
307
|
+
repo = get_repo()
|
|
308
|
+
git_cmd = getattr(repo, 'git')
|
|
309
|
+
|
|
310
|
+
if list:
|
|
311
|
+
stash_list = git_cmd.stash("list")
|
|
312
|
+
stashes = stash_list.split("\n") if stash_list else []
|
|
313
|
+
if stashes:
|
|
314
|
+
table = Table(title="Stashes", show_header=True)
|
|
315
|
+
table.add_column("Index", style="yellow", width=8)
|
|
316
|
+
table.add_column("Message", style="white")
|
|
317
|
+
|
|
318
|
+
for i, stash in enumerate(stashes):
|
|
319
|
+
table.add_row(str(i), stash)
|
|
320
|
+
|
|
321
|
+
console.print(table)
|
|
322
|
+
else:
|
|
323
|
+
console.print("[yellow]No stashes found[/yellow]")
|
|
324
|
+
else:
|
|
325
|
+
if message:
|
|
326
|
+
git_cmd.stash("push", "-m", message)
|
|
327
|
+
else:
|
|
328
|
+
git_cmd.stash()
|
|
329
|
+
console.print("[green]✓ Changes stashed[/green]")
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
console.print(f"[red]✗ Failed to stash: {e}[/red]")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def git_stash_apply(index: int = 0, pop: bool = False) -> None:
|
|
336
|
+
"""Apply stashed changes."""
|
|
337
|
+
try:
|
|
338
|
+
repo = get_repo()
|
|
339
|
+
git_cmd = getattr(repo, 'git')
|
|
340
|
+
|
|
341
|
+
if pop:
|
|
342
|
+
git_cmd.stash("pop", f"stash@{{{index}}}")
|
|
343
|
+
console.print(f"[green]✓ Applied and removed stash {index}[/green]")
|
|
344
|
+
else:
|
|
345
|
+
git_cmd.stash("apply", f"stash@{{{index}}}")
|
|
346
|
+
console.print(f"[green]✓ Applied stash {index}[/green]")
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
console.print(f"[red]✗ Failed to apply stash: {e}[/red]")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def git_merge(branch: str, no_ff: bool = False, squash: bool = False) -> None:
|
|
353
|
+
"""Merge branches."""
|
|
354
|
+
try:
|
|
355
|
+
repo = get_repo()
|
|
356
|
+
git_cmd = getattr(repo, 'git')
|
|
357
|
+
|
|
358
|
+
if squash:
|
|
359
|
+
git_cmd.merge(branch, squash=True)
|
|
360
|
+
console.print(f"[green]✓ Squash merged: {branch}[/green]")
|
|
361
|
+
elif no_ff:
|
|
362
|
+
git_cmd.merge(branch, no_ff=True)
|
|
363
|
+
console.print(f"[green]✓ Merged (no fast-forward): {branch}[/green]")
|
|
364
|
+
else:
|
|
365
|
+
git_cmd.merge(branch)
|
|
366
|
+
console.print(f"[green]✓ Merged: {branch}[/green]")
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
console.print(f"[red]✗ Failed to merge: {e}[/red]")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def git_clone(url: str, directory: Optional[str] = None, depth: Optional[int] = None) -> None:
|
|
373
|
+
"""Clone a repository."""
|
|
374
|
+
try:
|
|
375
|
+
kwargs: dict[str, Any] = {}
|
|
376
|
+
if depth:
|
|
377
|
+
kwargs["depth"] = depth
|
|
378
|
+
|
|
379
|
+
target = directory if directory else url.split("/")[-1].replace(".git", "")
|
|
380
|
+
|
|
381
|
+
git.Repo.clone_from(url, target, **kwargs)
|
|
382
|
+
console.print(f"[green]✓ Cloned to: {target}[/green]")
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
console.print(f"[red]✗ Failed to clone: {e}[/red]")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def git_stats(detailed: bool = False) -> None:
|
|
389
|
+
"""Show repository statistics."""
|
|
390
|
+
try:
|
|
391
|
+
repo = get_repo()
|
|
392
|
+
|
|
393
|
+
# Basic stats
|
|
394
|
+
commits = list(repo.iter_commits())
|
|
395
|
+
branches = list(repo.branches)
|
|
396
|
+
contributors: dict[str, int] = {}
|
|
397
|
+
|
|
398
|
+
for commit in commits:
|
|
399
|
+
author = str(commit.author.name)
|
|
400
|
+
contributors[author] = contributors.get(author, 0) + 1
|
|
401
|
+
|
|
402
|
+
table = Table(title="Repository Statistics", show_header=True)
|
|
403
|
+
table.add_column("Metric", style="cyan", width=25)
|
|
404
|
+
table.add_column("Value", style="yellow")
|
|
405
|
+
|
|
406
|
+
table.add_row("Total Commits", str(len(commits)))
|
|
407
|
+
table.add_row("Total Branches", str(len(branches)))
|
|
408
|
+
table.add_row("Contributors", str(len(contributors)))
|
|
409
|
+
table.add_row("Current Branch", str(repo.active_branch))
|
|
410
|
+
|
|
411
|
+
console.print(table)
|
|
412
|
+
|
|
413
|
+
if detailed:
|
|
414
|
+
# Top contributors
|
|
415
|
+
contrib_table = Table(title="Top Contributors", show_header=True)
|
|
416
|
+
contrib_table.add_column("Author", style="cyan")
|
|
417
|
+
contrib_table.add_column("Commits", style="yellow")
|
|
418
|
+
|
|
419
|
+
sorted_contributors = sorted(contributors.items(), key=lambda x: x[1], reverse=True)
|
|
420
|
+
for author, count in sorted_contributors[:10]:
|
|
421
|
+
contrib_table.add_row(author, str(count))
|
|
422
|
+
|
|
423
|
+
console.print("\n", contrib_table)
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
console.print(f"[red]✗ Failed to get stats: {e}[/red]")
|