bitbucket-manager 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.
- bitbucket_manager/__init__.py +1 -0
- bitbucket_manager/__main__.py +4 -0
- bitbucket_manager/api.py +496 -0
- bitbucket_manager/config.py +38 -0
- bitbucket_manager/tui/__init__.py +45 -0
- bitbucket_manager/tui/archive_screen.py +144 -0
- bitbucket_manager/tui/dashboard_screen.py +174 -0
- bitbucket_manager/tui/deps_screen.py +163 -0
- bitbucket_manager/tui/explorer.py +186 -0
- bitbucket_manager/tui/groups_screen.py +189 -0
- bitbucket_manager/tui/home.py +243 -0
- bitbucket_manager/tui/members_screen.py +53 -0
- bitbucket_manager/tui/migration_screen.py +187 -0
- bitbucket_manager/tui/permissions_screen.py +160 -0
- bitbucket_manager/tui/pr_screen.py +133 -0
- bitbucket_manager/tui/splash.py +79 -0
- bitbucket_manager/tui/widgets.py +64 -0
- bitbucket_manager/version.py +1 -0
- bitbucket_manager-0.1.0.dist-info/METADATA +235 -0
- bitbucket_manager-0.1.0.dist-info/RECORD +23 -0
- bitbucket_manager-0.1.0.dist-info/WHEEL +5 -0
- bitbucket_manager-0.1.0.dist-info/entry_points.txt +2 -0
- bitbucket_manager-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
bitbucket_manager/api.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import requests
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from bitbucket_manager.config import get_auth, load_env_file
|
|
7
|
+
|
|
8
|
+
load_env_file()
|
|
9
|
+
|
|
10
|
+
BASE_URL = "https://api.bitbucket.org/2.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _http_auth():
|
|
14
|
+
token, username, _ = get_auth()
|
|
15
|
+
if not token or not username:
|
|
16
|
+
return None
|
|
17
|
+
return (username, token)
|
|
18
|
+
|
|
19
|
+
def get_repos(workspace=None):
|
|
20
|
+
auth = _http_auth()
|
|
21
|
+
if not auth:
|
|
22
|
+
print("Error: credenciales no configuradas")
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
_, _, ws = get_auth()
|
|
26
|
+
workspace = workspace or ws
|
|
27
|
+
if not workspace:
|
|
28
|
+
print("Error: BB_WORKSPACE no está configurado")
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
dev_dir = os.environ.get("DEV_DIR", os.path.join(os.path.expanduser("~"), "bitbucket-repos"))
|
|
32
|
+
try:
|
|
33
|
+
all_repos = []
|
|
34
|
+
repos_url = f"{BASE_URL}/repositories/{workspace}?pagelen=100&role=member"
|
|
35
|
+
|
|
36
|
+
while repos_url:
|
|
37
|
+
response = requests.get(repos_url, auth=auth, timeout=10)
|
|
38
|
+
if response.status_code != 200:
|
|
39
|
+
print(f"Error: {response.status_code}")
|
|
40
|
+
break
|
|
41
|
+
data = response.json()
|
|
42
|
+
for repo in data.get('values', []):
|
|
43
|
+
updated = repo.get('updated_on', '')
|
|
44
|
+
try:
|
|
45
|
+
updated_dt = datetime.fromisoformat(updated.replace('Z', '+00:00'))
|
|
46
|
+
except Exception:
|
|
47
|
+
updated_dt = datetime.min.replace(tzinfo=timezone.utc)
|
|
48
|
+
all_repos.append({
|
|
49
|
+
'name': repo['name'],
|
|
50
|
+
'url': repo['links']['html']['href'],
|
|
51
|
+
'workspace': workspace,
|
|
52
|
+
'ws_slug': workspace,
|
|
53
|
+
'updated': updated_dt,
|
|
54
|
+
'updated_str': updated[:10] if updated else 'N/A',
|
|
55
|
+
'cloned': _is_cloned(repo['name'], dev_dir),
|
|
56
|
+
})
|
|
57
|
+
repos_url = data.get('next')
|
|
58
|
+
|
|
59
|
+
all_repos.sort(key=lambda x: x['updated'], reverse=True)
|
|
60
|
+
return all_repos
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Error de conexión: {e}")
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
def _is_cloned(repo_name, dev_dir):
|
|
66
|
+
target = os.path.join(dev_dir, repo_name)
|
|
67
|
+
return os.path.exists(target) and os.path.isdir(os.path.join(target, '.git'))
|
|
68
|
+
|
|
69
|
+
def clone_repo(repo_name, workspace=None):
|
|
70
|
+
_, _, ws = get_auth()
|
|
71
|
+
workspace = workspace or ws
|
|
72
|
+
dev_dir = os.environ.get("DEV_DIR", os.path.join(os.path.expanduser("~"), "bitbucket-repos"))
|
|
73
|
+
repo_url = f"git@bitbucket.org:{workspace}/{repo_name}.git"
|
|
74
|
+
target_dir = os.path.join(dev_dir, repo_name)
|
|
75
|
+
|
|
76
|
+
if os.path.exists(target_dir):
|
|
77
|
+
return False, f"El directorio {target_dir} ya existe"
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
os.makedirs(dev_dir, exist_ok=True)
|
|
81
|
+
result = subprocess.run(['git', 'clone', repo_url, target_dir],
|
|
82
|
+
capture_output=True, text=True)
|
|
83
|
+
if result.returncode == 0:
|
|
84
|
+
subprocess.run(['git', 'config', 'user.name',
|
|
85
|
+
os.environ.get('GIT_USER_NAME', 'Your Name')], cwd=target_dir)
|
|
86
|
+
subprocess.run(['git', 'config', 'user.email',
|
|
87
|
+
os.environ.get('GIT_USER_EMAIL', 'your-email@example.com')], cwd=target_dir)
|
|
88
|
+
return True, f"Repositorio clonado en: {target_dir}"
|
|
89
|
+
return False, result.stderr
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return False, str(e)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def pull_repo(repo_name, workspace=None):
|
|
95
|
+
_, _, ws = get_auth()
|
|
96
|
+
workspace = workspace or ws
|
|
97
|
+
dev_dir = os.environ.get("DEV_DIR", os.path.join(os.path.expanduser("~"), "bitbucket-repos"))
|
|
98
|
+
target_dir = os.path.join(dev_dir, repo_name)
|
|
99
|
+
|
|
100
|
+
if not os.path.isdir(os.path.join(target_dir, '.git')):
|
|
101
|
+
return False, f"{repo_name} no está clonado en {target_dir}"
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
result = subprocess.run(['git', '-C', target_dir, 'pull'],
|
|
105
|
+
capture_output=True, text=True)
|
|
106
|
+
if result.returncode == 0:
|
|
107
|
+
return True, result.stdout.strip() or "Pull exitoso"
|
|
108
|
+
return False, result.stderr
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return False, str(e)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_repo_branches(repo_name):
|
|
114
|
+
dev_dir = os.environ.get("DEV_DIR", os.path.join(os.path.expanduser("~"), "bitbucket-repos"))
|
|
115
|
+
target_dir = os.path.join(dev_dir, repo_name)
|
|
116
|
+
|
|
117
|
+
if not os.path.isdir(os.path.join(target_dir, '.git')):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
result = subprocess.run(['git', '-C', target_dir, 'branch', '-a'],
|
|
122
|
+
capture_output=True, text=True)
|
|
123
|
+
if result.returncode != 0:
|
|
124
|
+
return None
|
|
125
|
+
branches = []
|
|
126
|
+
for line in result.stdout.strip().split("\n"):
|
|
127
|
+
line = line.strip()
|
|
128
|
+
if not line or line.startswith("HEAD") or "HEAD ->" in line:
|
|
129
|
+
continue
|
|
130
|
+
if line.startswith("* "):
|
|
131
|
+
branches.append(line[2:].strip())
|
|
132
|
+
elif line.startswith("remotes/origin/"):
|
|
133
|
+
b = line.replace("remotes/origin/", "").strip()
|
|
134
|
+
if b and b not in branches and b != "HEAD":
|
|
135
|
+
branches.append(b)
|
|
136
|
+
else:
|
|
137
|
+
b = line.strip()
|
|
138
|
+
if b and b not in branches:
|
|
139
|
+
branches.append(b)
|
|
140
|
+
return branches
|
|
141
|
+
except Exception:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def checkout_repo(repo_name, branch):
|
|
146
|
+
dev_dir = os.environ.get("DEV_DIR", os.path.join(os.path.expanduser("~"), "bitbucket-repos"))
|
|
147
|
+
target_dir = os.path.join(dev_dir, repo_name)
|
|
148
|
+
|
|
149
|
+
if not os.path.isdir(os.path.join(target_dir, '.git')):
|
|
150
|
+
return False, f"{repo_name} no está clonado"
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(['git', '-C', target_dir, 'checkout', branch],
|
|
154
|
+
capture_output=True, text=True)
|
|
155
|
+
if result.returncode == 0:
|
|
156
|
+
return True, f"Checkout a '{branch}' exitoso"
|
|
157
|
+
return False, result.stderr
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return False, str(e)
|
|
160
|
+
|
|
161
|
+
def get_permissions_users(workspace, repo):
|
|
162
|
+
auth = _http_auth()
|
|
163
|
+
if not auth:
|
|
164
|
+
return None
|
|
165
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/permissions-config/users"
|
|
166
|
+
try:
|
|
167
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
168
|
+
if r.status_code == 200:
|
|
169
|
+
return r.json().get('values', [])
|
|
170
|
+
if r.status_code in (401, 403):
|
|
171
|
+
return [] # No tenemos permiso de admin en este repo; no es un error fatal
|
|
172
|
+
print(f" Error al obtener permisos de usuarios: {r.status_code}")
|
|
173
|
+
return None
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f" Error de conexión: {e}")
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def get_permissions_groups(workspace, repo):
|
|
179
|
+
auth = _http_auth()
|
|
180
|
+
if not auth:
|
|
181
|
+
return None
|
|
182
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/permissions-config/groups"
|
|
183
|
+
try:
|
|
184
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
185
|
+
if r.status_code == 200:
|
|
186
|
+
return r.json().get('values', [])
|
|
187
|
+
if r.status_code in (401, 403):
|
|
188
|
+
return [] # No tenemos permiso de admin en este repo
|
|
189
|
+
print(f" Error al obtener permisos de grupos: {r.status_code}")
|
|
190
|
+
return None
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f" Error de conexión: {e}")
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
def set_user_permission(workspace, repo, user, permission):
|
|
196
|
+
auth = _http_auth()
|
|
197
|
+
if not auth:
|
|
198
|
+
return False, "Credenciales no configuradas"
|
|
199
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/permissions-config/users/{user}"
|
|
200
|
+
try:
|
|
201
|
+
r = requests.put(url, auth=auth, json={"permission": permission.lower()}, timeout=10)
|
|
202
|
+
if r.status_code in (200, 201, 204):
|
|
203
|
+
return True, None
|
|
204
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return False, str(e)
|
|
207
|
+
|
|
208
|
+
def delete_user_permission(workspace, repo, user):
|
|
209
|
+
auth = _http_auth()
|
|
210
|
+
if not auth:
|
|
211
|
+
return False, "Credenciales no configuradas"
|
|
212
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/permissions-config/users/{user}"
|
|
213
|
+
try:
|
|
214
|
+
r = requests.delete(url, auth=auth, timeout=10)
|
|
215
|
+
if r.status_code in (200, 204):
|
|
216
|
+
return True, None
|
|
217
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return False, str(e)
|
|
220
|
+
|
|
221
|
+
def set_group_permission(workspace, repo, group, permission):
|
|
222
|
+
auth = _http_auth()
|
|
223
|
+
if not auth:
|
|
224
|
+
return False, "Credenciales no configuradas"
|
|
225
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/permissions-config/groups/{group}"
|
|
226
|
+
try:
|
|
227
|
+
r = requests.put(url, auth=auth, json={"permission": permission.lower()}, timeout=10)
|
|
228
|
+
if r.status_code in (200, 201, 204):
|
|
229
|
+
return True, None
|
|
230
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
231
|
+
except Exception as e:
|
|
232
|
+
return False, str(e)
|
|
233
|
+
|
|
234
|
+
def delete_group_permission(workspace, repo, group):
|
|
235
|
+
auth = _http_auth()
|
|
236
|
+
if not auth:
|
|
237
|
+
return False, "Credenciales no configuradas"
|
|
238
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/permissions-config/groups/{group}"
|
|
239
|
+
try:
|
|
240
|
+
r = requests.delete(url, auth=auth, timeout=10)
|
|
241
|
+
if r.status_code in (200, 204):
|
|
242
|
+
return True, None
|
|
243
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return False, str(e)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ─── PRs ────────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
def get_pullrequests(workspace, repo, state='OPEN', limit=50):
|
|
251
|
+
auth = _http_auth()
|
|
252
|
+
if not auth:
|
|
253
|
+
return None
|
|
254
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/pullrequests?state={state}&pagelen={limit}"
|
|
255
|
+
try:
|
|
256
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
257
|
+
if r.status_code == 200:
|
|
258
|
+
return r.json().get('values', [])
|
|
259
|
+
if r.status_code == 403:
|
|
260
|
+
print(f" PRs 403 en {repo}: verificá que el token tenga scope 'pullrequest'")
|
|
261
|
+
return None
|
|
262
|
+
print(f" Error al obtener PRs: {r.status_code}")
|
|
263
|
+
return None
|
|
264
|
+
except Exception as e:
|
|
265
|
+
print(f" Error de conexión: {e}")
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def approve_pullrequest(workspace, repo, pr_id):
|
|
270
|
+
auth = _http_auth()
|
|
271
|
+
if not auth:
|
|
272
|
+
return False, "Credenciales no configuradas"
|
|
273
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/pullrequests/{pr_id}/approve"
|
|
274
|
+
try:
|
|
275
|
+
r = requests.post(url, auth=auth, timeout=10)
|
|
276
|
+
if r.status_code in (200, 201, 204):
|
|
277
|
+
return True, None
|
|
278
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
279
|
+
except Exception as e:
|
|
280
|
+
return False, str(e)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def unapprove_pullrequest(workspace, repo, pr_id):
|
|
284
|
+
auth = _http_auth()
|
|
285
|
+
if not auth:
|
|
286
|
+
return False, "Credenciales no configuradas"
|
|
287
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/pullrequests/{pr_id}/approve"
|
|
288
|
+
try:
|
|
289
|
+
r = requests.delete(url, auth=auth, timeout=10)
|
|
290
|
+
if r.status_code in (200, 204):
|
|
291
|
+
return True, None
|
|
292
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
293
|
+
except Exception as e:
|
|
294
|
+
return False, str(e)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_branches(workspace, repo, limit=100):
|
|
298
|
+
auth = _http_auth()
|
|
299
|
+
if not auth:
|
|
300
|
+
return None
|
|
301
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}/refs/branches?pagelen={limit}"
|
|
302
|
+
try:
|
|
303
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
304
|
+
if r.status_code == 200:
|
|
305
|
+
return r.json().get('values', [])
|
|
306
|
+
print(f" Error al obtener branches: {r.status_code}")
|
|
307
|
+
return None
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print(f" Error de conexión: {e}")
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def create_repository(workspace, repo_name, is_private=True):
|
|
314
|
+
auth = _http_auth()
|
|
315
|
+
if not auth:
|
|
316
|
+
return False, "Credenciales no configuradas"
|
|
317
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo_name}"
|
|
318
|
+
try:
|
|
319
|
+
r = requests.post(url, auth=auth,
|
|
320
|
+
json={"scm": "git", "is_private": is_private}, timeout=15)
|
|
321
|
+
if r.status_code in (200, 201):
|
|
322
|
+
return True, r.json()
|
|
323
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
324
|
+
except Exception as e:
|
|
325
|
+
return False, str(e)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def update_repository(workspace, repo, data):
|
|
329
|
+
auth = _http_auth()
|
|
330
|
+
if not auth:
|
|
331
|
+
return False, "Credenciales no configuradas"
|
|
332
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}"
|
|
333
|
+
try:
|
|
334
|
+
r = requests.put(url, auth=auth, json=data, timeout=10)
|
|
335
|
+
if r.status_code == 200:
|
|
336
|
+
return True, None
|
|
337
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
338
|
+
except Exception as e:
|
|
339
|
+
return False, str(e)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_workspace_projects(workspace):
|
|
343
|
+
auth = _http_auth()
|
|
344
|
+
if not auth:
|
|
345
|
+
return None
|
|
346
|
+
url = f"{BASE_URL}/workspaces/{workspace}/projects"
|
|
347
|
+
try:
|
|
348
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
349
|
+
if r.status_code == 200:
|
|
350
|
+
return r.json().get('values', [])
|
|
351
|
+
print(f" Error al obtener proyectos: {r.status_code}")
|
|
352
|
+
return None
|
|
353
|
+
except Exception as e:
|
|
354
|
+
print(f" Error de conexión: {e}")
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def upsert_workspace_project(workspace, project_key, name, description=""):
|
|
359
|
+
auth = _http_auth()
|
|
360
|
+
if not auth:
|
|
361
|
+
return None
|
|
362
|
+
url = f"{BASE_URL}/workspaces/{workspace}/projects/{project_key}"
|
|
363
|
+
try:
|
|
364
|
+
r = requests.put(url, auth=auth,
|
|
365
|
+
json={"name": name, "key": project_key, "description": description},
|
|
366
|
+
timeout=10)
|
|
367
|
+
if r.status_code in (200, 201):
|
|
368
|
+
return r.json()
|
|
369
|
+
print(f" Error al crear/actualizar proyecto: {r.status_code}")
|
|
370
|
+
return None
|
|
371
|
+
except Exception as e:
|
|
372
|
+
print(f" Error de conexión: {e}")
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ─── Groups ──────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
def get_workspace_groups(workspace):
|
|
379
|
+
auth = _http_auth()
|
|
380
|
+
if not auth:
|
|
381
|
+
return None
|
|
382
|
+
url = f"{BASE_URL}/workspaces/{workspace}/groups"
|
|
383
|
+
try:
|
|
384
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
385
|
+
if r.status_code == 200:
|
|
386
|
+
return r.json().get('values', [])
|
|
387
|
+
print(f" Error al obtener grupos: {r.status_code}")
|
|
388
|
+
return None
|
|
389
|
+
except Exception as e:
|
|
390
|
+
print(f" Error de conexión: {e}")
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def create_workspace_group(workspace, name):
|
|
395
|
+
auth = _http_auth()
|
|
396
|
+
if not auth:
|
|
397
|
+
return False, "Credenciales no configuradas"
|
|
398
|
+
url = f"{BASE_URL}/workspaces/{workspace}/groups"
|
|
399
|
+
try:
|
|
400
|
+
r = requests.post(url, auth=auth, json={"name": name}, timeout=10)
|
|
401
|
+
if r.status_code in (200, 201):
|
|
402
|
+
return True, r.json()
|
|
403
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
404
|
+
except Exception as e:
|
|
405
|
+
return False, str(e)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def delete_workspace_group(workspace, group_slug):
|
|
409
|
+
auth = _http_auth()
|
|
410
|
+
if not auth:
|
|
411
|
+
return False, "Credenciales no configuradas"
|
|
412
|
+
url = f"{BASE_URL}/workspaces/{workspace}/groups/{group_slug}"
|
|
413
|
+
try:
|
|
414
|
+
r = requests.delete(url, auth=auth, timeout=10)
|
|
415
|
+
if r.status_code in (200, 204):
|
|
416
|
+
return True, None
|
|
417
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
418
|
+
except Exception as e:
|
|
419
|
+
return False, str(e)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def get_group_members(workspace, group_slug):
|
|
423
|
+
auth = _http_auth()
|
|
424
|
+
if not auth:
|
|
425
|
+
return None
|
|
426
|
+
url = f"{BASE_URL}/workspaces/{workspace}/groups/{group_slug}/members"
|
|
427
|
+
try:
|
|
428
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
429
|
+
if r.status_code == 200:
|
|
430
|
+
return r.json().get('values', [])
|
|
431
|
+
print(f" Error al obtener miembros del grupo: {r.status_code}")
|
|
432
|
+
return None
|
|
433
|
+
except Exception as e:
|
|
434
|
+
print(f" Error de conexión: {e}")
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def add_group_member(workspace, group_slug, member):
|
|
439
|
+
auth = _http_auth()
|
|
440
|
+
if not auth:
|
|
441
|
+
return False, "Credenciales no configuradas"
|
|
442
|
+
url = f"{BASE_URL}/workspaces/{workspace}/groups/{group_slug}/members/{member}"
|
|
443
|
+
try:
|
|
444
|
+
r = requests.put(url, auth=auth, timeout=10)
|
|
445
|
+
if r.status_code in (200, 201, 204):
|
|
446
|
+
return True, None
|
|
447
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
448
|
+
except Exception as e:
|
|
449
|
+
return False, str(e)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def remove_group_member(workspace, group_slug, member):
|
|
453
|
+
auth = _http_auth()
|
|
454
|
+
if not auth:
|
|
455
|
+
return False, "Credenciales no configuradas"
|
|
456
|
+
url = f"{BASE_URL}/workspaces/{workspace}/groups/{group_slug}/members/{member}"
|
|
457
|
+
try:
|
|
458
|
+
r = requests.delete(url, auth=auth, timeout=10)
|
|
459
|
+
if r.status_code in (200, 204):
|
|
460
|
+
return True, None
|
|
461
|
+
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
|
462
|
+
except Exception as e:
|
|
463
|
+
return False, str(e)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ─── Members ─────────────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
def get_workspace_members(workspace):
|
|
469
|
+
auth = _http_auth()
|
|
470
|
+
if not auth:
|
|
471
|
+
return None
|
|
472
|
+
url = f"{BASE_URL}/workspaces/{workspace}/members"
|
|
473
|
+
try:
|
|
474
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
475
|
+
if r.status_code == 200:
|
|
476
|
+
return r.json().get('values', [])
|
|
477
|
+
print(f" Error al obtener miembros del workspace: {r.status_code}")
|
|
478
|
+
return None
|
|
479
|
+
except Exception as e:
|
|
480
|
+
print(f" Error de conexión: {e}")
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def get_repository(workspace, repo):
|
|
485
|
+
auth = _http_auth()
|
|
486
|
+
if not auth:
|
|
487
|
+
return None
|
|
488
|
+
url = f"{BASE_URL}/repositories/{workspace}/{repo}"
|
|
489
|
+
try:
|
|
490
|
+
r = requests.get(url, auth=auth, timeout=10)
|
|
491
|
+
if r.status_code == 200:
|
|
492
|
+
return r.json()
|
|
493
|
+
return None
|
|
494
|
+
except Exception as e:
|
|
495
|
+
print(f" Error de conexión: {e}")
|
|
496
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
def load_env_file():
|
|
4
|
+
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '.env')
|
|
5
|
+
alt_path = os.path.join(os.path.expanduser("~"), ".bbm", ".env")
|
|
6
|
+
new_path = os.path.join(os.path.expanduser("~"), ".config", "bitbucket-manager", "env")
|
|
7
|
+
for path in [env_path, alt_path, new_path]:
|
|
8
|
+
if os.path.exists(path):
|
|
9
|
+
with open(path, 'r') as f:
|
|
10
|
+
for line in f:
|
|
11
|
+
line = line.strip()
|
|
12
|
+
if line and not line.startswith('#') and '=' in line:
|
|
13
|
+
key, value = line.split('=', 1)
|
|
14
|
+
os.environ.setdefault(key.strip(), value.strip())
|
|
15
|
+
|
|
16
|
+
def get_auth():
|
|
17
|
+
token = os.environ.get("BB_TOKEN")
|
|
18
|
+
username = os.environ.get("BB_USERNAME")
|
|
19
|
+
workspace = os.environ.get("BB_WORKSPACE")
|
|
20
|
+
return token, username, workspace
|
|
21
|
+
|
|
22
|
+
def get_config():
|
|
23
|
+
token = os.environ.get("BB_TOKEN")
|
|
24
|
+
username = os.environ.get("BB_USERNAME")
|
|
25
|
+
workspace = os.environ.get("BB_WORKSPACE")
|
|
26
|
+
dev_dir = os.environ.get("DEV_DIR", os.path.expanduser("~/Documents/bitbucket-repos"))
|
|
27
|
+
return token, username, workspace, dev_dir
|
|
28
|
+
|
|
29
|
+
def validate_env():
|
|
30
|
+
token, username, workspace, dev_dir = get_config()
|
|
31
|
+
errors = []
|
|
32
|
+
if not token:
|
|
33
|
+
errors.append("BB_TOKEN no está configurado. Exportalo o creá un .env")
|
|
34
|
+
if not username:
|
|
35
|
+
errors.append("BB_USERNAME no está configurado (usá tu email de Atlassian)")
|
|
36
|
+
if not workspace:
|
|
37
|
+
errors.append("BB_WORKSPACE no está configurado")
|
|
38
|
+
return errors
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual.app import App
|
|
4
|
+
|
|
5
|
+
from .splash import BootScreen
|
|
6
|
+
from .home import HomeScreen
|
|
7
|
+
from .dashboard_screen import DashboardScreen
|
|
8
|
+
from .explorer import ExplorerScreen
|
|
9
|
+
from .permissions_screen import PermissionsScreen
|
|
10
|
+
from .pr_screen import PRScreen
|
|
11
|
+
from .migration_screen import MigrationScreen
|
|
12
|
+
from .archive_screen import ArchiveScreen
|
|
13
|
+
from .deps_screen import DepsScreen
|
|
14
|
+
from .groups_screen import GroupsScreen
|
|
15
|
+
from .members_screen import MembersScreen
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BitbucketManagerApp(App):
|
|
19
|
+
CSS_PATH = "styles.tcss"
|
|
20
|
+
SCREENS = {
|
|
21
|
+
"boot": BootScreen,
|
|
22
|
+
"home": HomeScreen,
|
|
23
|
+
"dashboard": DashboardScreen,
|
|
24
|
+
"explorer": ExplorerScreen,
|
|
25
|
+
"permissions": PermissionsScreen,
|
|
26
|
+
"pr": PRScreen,
|
|
27
|
+
"migration": MigrationScreen,
|
|
28
|
+
"archive": ArchiveScreen,
|
|
29
|
+
"deps": DepsScreen,
|
|
30
|
+
"groups": GroupsScreen,
|
|
31
|
+
"members": MembersScreen,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
TITLE = "Bitbucket Manager"
|
|
35
|
+
BINDINGS = [
|
|
36
|
+
("ctrl+q", "quit", "Salir"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def on_mount(self) -> None:
|
|
40
|
+
self.push_screen("boot")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_tui() -> None:
|
|
44
|
+
app = BitbucketManagerApp()
|
|
45
|
+
app.run()
|