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
git_auto_pro/github.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""GitHub API integration module."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import keyring
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional, List, Dict
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Prompt, Confirm
|
|
10
|
+
import questionary
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
SERVICE_NAME = "git-auto-pro"
|
|
15
|
+
TOKEN_KEY = "github-token"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
TOKEN_FILE = Path.home() / ".git-auto-token.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _use_file_storage() -> bool:
|
|
22
|
+
"""Check if we should use file-based storage."""
|
|
23
|
+
try:
|
|
24
|
+
|
|
25
|
+
keyring.get_keyring()
|
|
26
|
+
|
|
27
|
+
test_service = "git-auto-test"
|
|
28
|
+
try:
|
|
29
|
+
keyring.set_password(test_service, "test", "test")
|
|
30
|
+
keyring.delete_password(test_service, "test")
|
|
31
|
+
return False
|
|
32
|
+
except Exception:
|
|
33
|
+
return True
|
|
34
|
+
except Exception:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_stored_token() -> Optional[str]:
|
|
39
|
+
"""Retrieve stored GitHub token from keyring or file."""
|
|
40
|
+
|
|
41
|
+
if not _use_file_storage():
|
|
42
|
+
try:
|
|
43
|
+
token = keyring.get_password(SERVICE_NAME, TOKEN_KEY)
|
|
44
|
+
if token:
|
|
45
|
+
return token
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[yellow]Warning: Keyring access failed: {e}[/yellow]")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if TOKEN_FILE.exists():
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(TOKEN_FILE.read_text())
|
|
53
|
+
return data.get("token")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
console.print(f"[yellow]Warning: Could not read token file: {e}[/yellow]")
|
|
56
|
+
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def store_token(token: str) -> None:
|
|
61
|
+
"""Store GitHub token in keyring or file."""
|
|
62
|
+
use_file = _use_file_storage()
|
|
63
|
+
|
|
64
|
+
if use_file:
|
|
65
|
+
|
|
66
|
+
console.print("[yellow]⚠️ Keyring not available, using file-based storage[/yellow]")
|
|
67
|
+
console.print(f"[dim]Token will be stored in: {TOKEN_FILE}[/dim]")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
data = {"token": token}
|
|
72
|
+
TOKEN_FILE.write_text(json.dumps(data, indent=2))
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
TOKEN_FILE.chmod(0o600)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
console.print("[green]✓ Token stored securely in file[/green]")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
console.print(f"[red]✗ Failed to store token: {e}[/red]")
|
|
81
|
+
raise
|
|
82
|
+
else:
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
keyring.set_password(SERVICE_NAME, TOKEN_KEY, token)
|
|
86
|
+
console.print("[green]✓ Token stored securely in keyring[/green]")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
console.print(f"[red]✗ Failed to store token in keyring: {e}[/red]")
|
|
89
|
+
|
|
90
|
+
console.print("[yellow]Falling back to file storage...[/yellow]")
|
|
91
|
+
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
data = {"token": token}
|
|
93
|
+
TOKEN_FILE.write_text(json.dumps(data, indent=2))
|
|
94
|
+
try:
|
|
95
|
+
TOKEN_FILE.chmod(0o600)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
console.print("[green]✓ Token stored in file[/green]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_token(token: str) -> bool:
|
|
102
|
+
"""Validate GitHub token using API."""
|
|
103
|
+
headers = {
|
|
104
|
+
"Authorization": f"token {token}",
|
|
105
|
+
"Accept": "application/vnd.github.v3+json"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = requests.get("https://api.github.com/user", headers=headers)
|
|
110
|
+
if response.status_code == 200:
|
|
111
|
+
user_data = response.json()
|
|
112
|
+
console.print(f"[green]✓ Authenticated as: {user_data['login']}[/green]")
|
|
113
|
+
return True
|
|
114
|
+
else:
|
|
115
|
+
console.print(f"[red]✗ Invalid token: {response.status_code}[/red]")
|
|
116
|
+
return False
|
|
117
|
+
except Exception as e:
|
|
118
|
+
console.print(f"[red]✗ Validation failed: {e}[/red]")
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def login_github(token: Optional[str] = None) -> None:
|
|
123
|
+
"""Login to GitHub using Personal Access Token."""
|
|
124
|
+
console.print("[bold cyan]🔐 GitHub Authentication[/bold cyan]\n")
|
|
125
|
+
|
|
126
|
+
if not token:
|
|
127
|
+
console.print("To create a token, visit: https://github.com/settings/tokens")
|
|
128
|
+
console.print("Required scopes: repo, workflow, admin:org\n")
|
|
129
|
+
token = Prompt.ask("Enter your GitHub Personal Access Token", password=True)
|
|
130
|
+
|
|
131
|
+
if not token:
|
|
132
|
+
console.print("[red]✗ No token provided[/red]")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if validate_token(token):
|
|
136
|
+
store_token(token)
|
|
137
|
+
console.print("[bold green]✓ Login successful![/bold green]")
|
|
138
|
+
else:
|
|
139
|
+
console.print("[red]✗ Login failed[/red]")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_authenticated_session() -> requests.Session:
|
|
143
|
+
"""Get authenticated requests session."""
|
|
144
|
+
token = get_stored_token()
|
|
145
|
+
if not token:
|
|
146
|
+
console.print("[red]✗ Not authenticated. Run 'git-auto login' first.[/red]")
|
|
147
|
+
raise ValueError("Not authenticated")
|
|
148
|
+
|
|
149
|
+
session = requests.Session()
|
|
150
|
+
session.headers.update({
|
|
151
|
+
"Authorization": f"token {token}",
|
|
152
|
+
"Accept": "application/vnd.github.v3+json"
|
|
153
|
+
})
|
|
154
|
+
return session
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_current_user() -> Dict:
|
|
158
|
+
"""Get current authenticated user information."""
|
|
159
|
+
session = get_authenticated_session()
|
|
160
|
+
response = session.get("https://api.github.com/user")
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
return response.json()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_github_repo(
|
|
166
|
+
name: str,
|
|
167
|
+
private: bool = False,
|
|
168
|
+
description: Optional[str] = None,
|
|
169
|
+
homepage: Optional[str] = None,
|
|
170
|
+
topics: Optional[List[str]] = None,
|
|
171
|
+
auto_init: bool = False,
|
|
172
|
+
) -> Dict:
|
|
173
|
+
"""Create a new GitHub repository."""
|
|
174
|
+
console.print(f"[bold cyan]📦 Creating repository: {name}[/bold cyan]\n")
|
|
175
|
+
|
|
176
|
+
session = get_authenticated_session()
|
|
177
|
+
user = get_current_user()
|
|
178
|
+
|
|
179
|
+
data = {
|
|
180
|
+
"name": name,
|
|
181
|
+
"private": private,
|
|
182
|
+
"auto_init": auto_init,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if description:
|
|
186
|
+
data["description"] = description
|
|
187
|
+
if homepage:
|
|
188
|
+
data["homepage"] = homepage
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
response = session.post("https://api.github.com/user/repos", json=data)
|
|
192
|
+
response.raise_for_status()
|
|
193
|
+
repo_data = response.json()
|
|
194
|
+
|
|
195
|
+
console.print(f"[green]✓ Repository created: {repo_data['html_url']}[/green]")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if topics:
|
|
199
|
+
topics_response = session.put(
|
|
200
|
+
f"https://api.github.com/repos/{user['login']}/{name}/topics",
|
|
201
|
+
json={"names": topics},
|
|
202
|
+
headers={"Accept": "application/vnd.github.mercy-preview+json"}
|
|
203
|
+
)
|
|
204
|
+
if topics_response.status_code == 200:
|
|
205
|
+
console.print(f"[green]✓ Topics added: {', '.join(topics)}[/green]")
|
|
206
|
+
|
|
207
|
+
return repo_data
|
|
208
|
+
|
|
209
|
+
except requests.exceptions.HTTPError as e:
|
|
210
|
+
if e.response.status_code == 422:
|
|
211
|
+
console.print(f"[red]✗ Repository '{name}' already exists[/red]")
|
|
212
|
+
else:
|
|
213
|
+
console.print(f"[red]✗ Failed to create repository: {e}[/red]")
|
|
214
|
+
raise
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def add_collaborator(
|
|
218
|
+
username: str,
|
|
219
|
+
repo: Optional[str] = None,
|
|
220
|
+
permission: str = "push"
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Add a collaborator to a repository."""
|
|
223
|
+
session = get_authenticated_session()
|
|
224
|
+
user = get_current_user()
|
|
225
|
+
|
|
226
|
+
if not repo:
|
|
227
|
+
|
|
228
|
+
import git
|
|
229
|
+
try:
|
|
230
|
+
repo_obj = git.Repo(".")
|
|
231
|
+
|
|
232
|
+
remotes = getattr(repo_obj, 'remotes')
|
|
233
|
+
origin = getattr(remotes, 'origin')
|
|
234
|
+
remote_url = origin.url
|
|
235
|
+
repo = remote_url.split("/")[-1].replace(".git", "")
|
|
236
|
+
except:
|
|
237
|
+
console.print("[red]✗ Could not detect repository. Use --repo option.[/red]")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
console.print(f"[cyan]Adding {username} as collaborator to {repo}...[/cyan]")
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
response = session.put(
|
|
244
|
+
f"https://api.github.com/repos/{user['login']}/{repo}/collaborators/{username}",
|
|
245
|
+
json={"permission": permission}
|
|
246
|
+
)
|
|
247
|
+
response.raise_for_status()
|
|
248
|
+
console.print(f"[green]✓ Collaborator added: {username} ({permission})[/green]")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
console.print(f"[red]✗ Failed to add collaborator: {e}[/red]")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def protect_branch(
|
|
254
|
+
branch: str = "main",
|
|
255
|
+
repo: Optional[str] = None
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Setup branch protection rules."""
|
|
258
|
+
session = get_authenticated_session()
|
|
259
|
+
user = get_current_user()
|
|
260
|
+
|
|
261
|
+
if not repo:
|
|
262
|
+
|
|
263
|
+
import git
|
|
264
|
+
try:
|
|
265
|
+
repo_obj = git.Repo(".")
|
|
266
|
+
|
|
267
|
+
remotes = getattr(repo_obj, 'remotes')
|
|
268
|
+
origin = getattr(remotes, 'origin')
|
|
269
|
+
remote_url = origin.url
|
|
270
|
+
repo = remote_url.split("/")[-1].replace(".git", "")
|
|
271
|
+
except:
|
|
272
|
+
console.print("[red]✗ Could not detect repository. Use --repo option.[/red]")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
console.print(f"[cyan]Setting up protection for branch '{branch}'...[/cyan]")
|
|
276
|
+
|
|
277
|
+
protection_data = {
|
|
278
|
+
"required_status_checks": None,
|
|
279
|
+
"enforce_admins": False,
|
|
280
|
+
"required_pull_request_reviews": {
|
|
281
|
+
"dismissal_restrictions": {},
|
|
282
|
+
"dismiss_stale_reviews": True,
|
|
283
|
+
"require_code_owner_reviews": False,
|
|
284
|
+
"required_approving_review_count": 1
|
|
285
|
+
},
|
|
286
|
+
"restrictions": None
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
response = session.put(
|
|
291
|
+
f"https://api.github.com/repos/{user['login']}/{repo}/branches/{branch}/protection",
|
|
292
|
+
json=protection_data,
|
|
293
|
+
headers={"Accept": "application/vnd.github.luke-cage-preview+json"}
|
|
294
|
+
)
|
|
295
|
+
response.raise_for_status()
|
|
296
|
+
console.print(f"[green]✓ Branch protection enabled for '{branch}'[/green]")
|
|
297
|
+
except Exception as e:
|
|
298
|
+
console.print(f"[red]✗ Failed to protect branch: {e}[/red]")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def clear_stored_token() -> None:
|
|
302
|
+
"""Clear stored token (for logout/reset)."""
|
|
303
|
+
|
|
304
|
+
if not _use_file_storage():
|
|
305
|
+
try:
|
|
306
|
+
keyring.delete_password(SERVICE_NAME, TOKEN_KEY)
|
|
307
|
+
console.print("[green]✓ Token cleared from keyring[/green]")
|
|
308
|
+
return
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if TOKEN_FILE.exists():
|
|
314
|
+
try:
|
|
315
|
+
TOKEN_FILE.unlink()
|
|
316
|
+
console.print("[green]✓ Token cleared from file[/green]")
|
|
317
|
+
except Exception as e:
|
|
318
|
+
console.print(f"[red]✗ Failed to clear token: {e}[/red]")
|