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/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]")