gitpush-tool 0.2.10__tar.gz → 0.3.0__tar.gz
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.
- {gitpush_tool-0.2.10/gitpush_tool.egg-info → gitpush_tool-0.3.0}/PKG-INFO +1 -1
- gitpush_tool-0.3.0/gitpush_tool/__init__.py +1 -0
- gitpush_tool-0.3.0/gitpush_tool/cli.py +970 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0/gitpush_tool.egg-info}/PKG-INFO +1 -1
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/setup.py +1 -1
- gitpush_tool-0.2.10/gitpush_tool/__init__.py +0 -1
- gitpush_tool-0.2.10/gitpush_tool/cli.py +0 -279
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/LICENSE +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/MANIFEST.in +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/README.md +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/gitpush_tool/__doc__.py +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/gitpush_tool.egg-info/SOURCES.txt +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/gitpush_tool.egg-info/dependency_links.txt +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/gitpush_tool.egg-info/entry_points.txt +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/gitpush_tool.egg-info/top_level.txt +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/pyproject.toml +0 -0
- {gitpush_tool-0.2.10 → gitpush_tool-0.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
import shutil
|
|
6
|
+
import platform
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
import urllib.request
|
|
15
|
+
import subprocess
|
|
16
|
+
from typing import Optional, Tuple
|
|
17
|
+
|
|
18
|
+
def check_gh_installed() -> bool:
|
|
19
|
+
"""Check if GitHub CLI is installed with proper verification"""
|
|
20
|
+
if shutil.which("gh"):
|
|
21
|
+
try:
|
|
22
|
+
# Verify gh is actually working
|
|
23
|
+
subprocess.run(["gh", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
24
|
+
return True
|
|
25
|
+
except:
|
|
26
|
+
# Found but not working - might be PATH issue
|
|
27
|
+
return False
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def install_gh_cli() -> bool:
|
|
31
|
+
"""Main installation function with comprehensive error handling"""
|
|
32
|
+
system = platform.system()
|
|
33
|
+
machine = platform.machine().lower()
|
|
34
|
+
|
|
35
|
+
print("\n🔧 Installing GitHub CLI...")
|
|
36
|
+
print(f"📋 System: {system}, Architecture: {machine}")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if system == "Windows":
|
|
40
|
+
return install_gh_cli_windows()
|
|
41
|
+
elif system == "Darwin":
|
|
42
|
+
return install_gh_cli_mac()
|
|
43
|
+
elif system == "Linux":
|
|
44
|
+
return install_gh_cli_linux()
|
|
45
|
+
else:
|
|
46
|
+
print(f"❌ Unsupported OS: {system}")
|
|
47
|
+
return False
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print(f"❌ Installation failed: {str(e)}")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def install_gh_cli_windows() -> bool:
|
|
53
|
+
"""Windows installation with multiple fallback methods and PATH management"""
|
|
54
|
+
methods = [
|
|
55
|
+
try_winget_install,
|
|
56
|
+
try_scoop_install,
|
|
57
|
+
try_choco_install,
|
|
58
|
+
try_direct_msi_install,
|
|
59
|
+
try_direct_zip_install
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for method in methods:
|
|
63
|
+
if method():
|
|
64
|
+
if verify_gh_installation():
|
|
65
|
+
return True
|
|
66
|
+
print("⚠️ Trying next installation method...")
|
|
67
|
+
|
|
68
|
+
print("❌ All installation methods failed")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def try_winget_install() -> bool:
|
|
72
|
+
"""Attempt installation via winget"""
|
|
73
|
+
if not shutil.which("winget"):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
print("\n🔄 Attempting winget installation...")
|
|
77
|
+
try:
|
|
78
|
+
subprocess.run(
|
|
79
|
+
["winget", "install", "--id", "GitHub.cli", "--silent", "--accept-package-agreements", "--accept-source-agreements"],
|
|
80
|
+
check=True,
|
|
81
|
+
stdout=subprocess.PIPE,
|
|
82
|
+
stderr=subprocess.PIPE
|
|
83
|
+
)
|
|
84
|
+
return True
|
|
85
|
+
except subprocess.CalledProcessError as e:
|
|
86
|
+
print(f"⚠️ winget failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def try_scoop_install() -> bool:
|
|
90
|
+
"""Attempt installation via scoop"""
|
|
91
|
+
if not shutil.which("scoop"):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
print("\n🔄 Attempting scoop installation...")
|
|
95
|
+
try:
|
|
96
|
+
subprocess.run(
|
|
97
|
+
["scoop", "install", "gh"],
|
|
98
|
+
check=True,
|
|
99
|
+
stdout=subprocess.PIPE,
|
|
100
|
+
stderr=subprocess.PIPE
|
|
101
|
+
)
|
|
102
|
+
return True
|
|
103
|
+
except subprocess.CalledProcessError as e:
|
|
104
|
+
print(f"⚠️ scoop failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def try_choco_install() -> bool:
|
|
108
|
+
"""Attempt installation via chocolatey"""
|
|
109
|
+
if not shutil.which("choco"):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
print("\n🔄 Attempting chocolatey installation...")
|
|
113
|
+
try:
|
|
114
|
+
subprocess.run(
|
|
115
|
+
["choco", "install", "gh", "-y"],
|
|
116
|
+
check=True,
|
|
117
|
+
stdout=subprocess.PIPE,
|
|
118
|
+
stderr=subprocess.PIPE
|
|
119
|
+
)
|
|
120
|
+
return True
|
|
121
|
+
except subprocess.CalledProcessError as e:
|
|
122
|
+
print(f"⚠️ chocolatey failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def try_direct_msi_install() -> bool:
|
|
126
|
+
"""Direct MSI installation with proper PATH handling"""
|
|
127
|
+
print("\n🔄 Attempting direct MSI installation...")
|
|
128
|
+
try:
|
|
129
|
+
# Get latest release info
|
|
130
|
+
release_info = get_github_release_info()
|
|
131
|
+
if not release_info:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Find appropriate MSI
|
|
135
|
+
msi_asset = next(
|
|
136
|
+
(a for a in release_info.get('assets', [])
|
|
137
|
+
if a['name'].endswith('_windows_amd64.msi') or
|
|
138
|
+
a['name'].endswith('_windows_386.msi')),
|
|
139
|
+
None
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if not msi_asset:
|
|
143
|
+
print("❌ Could not find Windows MSI installer")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Download MSI
|
|
147
|
+
temp_dir = tempfile.mkdtemp()
|
|
148
|
+
msi_path = os.path.join(temp_dir, msi_asset['name'])
|
|
149
|
+
print(f"⬇️ Downloading {msi_asset['name']}...")
|
|
150
|
+
download_file(msi_asset['browser_download_url'], msi_path)
|
|
151
|
+
|
|
152
|
+
# Install with appropriate privileges
|
|
153
|
+
print("🛠 Installing...")
|
|
154
|
+
try:
|
|
155
|
+
# Try with admin privileges first
|
|
156
|
+
subprocess.run(
|
|
157
|
+
["msiexec", "/i", msi_path, "/quiet", "/norestart"],
|
|
158
|
+
check=True,
|
|
159
|
+
stdout=subprocess.PIPE,
|
|
160
|
+
stderr=subprocess.PIPE
|
|
161
|
+
)
|
|
162
|
+
except subprocess.CalledProcessError:
|
|
163
|
+
# Fallback to user installation
|
|
164
|
+
print("⚠️ Admin install failed, trying user installation...")
|
|
165
|
+
subprocess.run(
|
|
166
|
+
["msiexec", "/i", msi_path, "/quiet", "/norestart", "ALLUSERS=2"],
|
|
167
|
+
check=True,
|
|
168
|
+
stdout=subprocess.PIPE,
|
|
169
|
+
stderr=subprocess.PIPE
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Clean up
|
|
173
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
174
|
+
|
|
175
|
+
# Add to PATH if needed
|
|
176
|
+
program_files = os.environ.get("ProgramFiles", "C:\\Program Files")
|
|
177
|
+
gh_path = os.path.join(program_files, "GitHub CLI", "gh.exe")
|
|
178
|
+
if os.path.exists(gh_path):
|
|
179
|
+
add_to_path(os.path.dirname(gh_path))
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
local_appdata = os.environ.get("LOCALAPPDATA", "")
|
|
183
|
+
gh_path = os.path.join(local_appdata, "GitHub CLI", "gh.exe")
|
|
184
|
+
if os.path.exists(gh_path):
|
|
185
|
+
add_to_path(os.path.dirname(gh_path))
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
print("❌ Installation completed but couldn't find gh.exe")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"❌ MSI installation failed: {str(e)}")
|
|
193
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def try_direct_zip_install() -> bool:
|
|
197
|
+
"""Fallback ZIP installation for Windows"""
|
|
198
|
+
print("\n🔄 Attempting direct ZIP installation...")
|
|
199
|
+
try:
|
|
200
|
+
# Get latest release info
|
|
201
|
+
release_info = get_github_release_info()
|
|
202
|
+
if not release_info:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
# Find appropriate ZIP
|
|
206
|
+
zip_asset = next(
|
|
207
|
+
(a for a in release_info.get('assets', [])
|
|
208
|
+
if a['name'].endswith('windows_amd64.zip') or
|
|
209
|
+
a['name'].endswith('windows_386.zip')),
|
|
210
|
+
None
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if not zip_asset:
|
|
214
|
+
print("❌ Could not find Windows ZIP package")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# Download and extract
|
|
218
|
+
temp_dir = tempfile.mkdtemp()
|
|
219
|
+
zip_path = os.path.join(temp_dir, zip_asset['name'])
|
|
220
|
+
print(f"⬇️ Downloading {zip_asset['name']}...")
|
|
221
|
+
download_file(zip_asset['browser_download_url'], zip_path)
|
|
222
|
+
|
|
223
|
+
print("📦 Extracting...")
|
|
224
|
+
shutil.unpack_archive(zip_path, temp_dir)
|
|
225
|
+
|
|
226
|
+
# Find the binary
|
|
227
|
+
for root, _, files in os.walk(temp_dir):
|
|
228
|
+
if "gh.exe" in files:
|
|
229
|
+
bin_dir = root
|
|
230
|
+
break
|
|
231
|
+
else:
|
|
232
|
+
print("❌ Could not find gh.exe in extracted files")
|
|
233
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
# Install to local apps directory
|
|
237
|
+
install_dir = os.path.join(os.environ.get("LOCALAPPDATA", ""), "GitHubCLI")
|
|
238
|
+
os.makedirs(install_dir, exist_ok=True)
|
|
239
|
+
|
|
240
|
+
# Copy files
|
|
241
|
+
for item in os.listdir(bin_dir):
|
|
242
|
+
src = os.path.join(bin_dir, item)
|
|
243
|
+
dst = os.path.join(install_dir, item)
|
|
244
|
+
if os.path.isdir(src):
|
|
245
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
246
|
+
else:
|
|
247
|
+
shutil.copy2(src, dst)
|
|
248
|
+
|
|
249
|
+
# Add to PATH
|
|
250
|
+
add_to_path(install_dir)
|
|
251
|
+
|
|
252
|
+
# Clean up
|
|
253
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
254
|
+
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f"❌ ZIP installation failed: {str(e)}")
|
|
259
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def install_gh_cli_mac() -> bool:
|
|
263
|
+
"""macOS installation with multiple methods"""
|
|
264
|
+
methods = [
|
|
265
|
+
try_brew_install,
|
|
266
|
+
try_direct_pkg_install,
|
|
267
|
+
try_direct_tar_install
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
for method in methods:
|
|
271
|
+
if method():
|
|
272
|
+
if verify_gh_installation():
|
|
273
|
+
return True
|
|
274
|
+
print("⚠️ Trying next installation method...")
|
|
275
|
+
|
|
276
|
+
print("❌ All installation methods failed")
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
def try_brew_install() -> bool:
|
|
280
|
+
"""Attempt installation via Homebrew"""
|
|
281
|
+
if not shutil.which("brew"):
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
print("\n🔄 Attempting Homebrew installation...")
|
|
285
|
+
try:
|
|
286
|
+
subprocess.run(
|
|
287
|
+
["brew", "install", "gh"],
|
|
288
|
+
check=True,
|
|
289
|
+
stdout=subprocess.PIPE,
|
|
290
|
+
stderr=subprocess.PIPE
|
|
291
|
+
)
|
|
292
|
+
return True
|
|
293
|
+
except subprocess.CalledProcessError as e:
|
|
294
|
+
print(f"⚠️ Homebrew failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
def try_direct_pkg_install() -> bool:
|
|
298
|
+
"""Direct PKG installation for macOS"""
|
|
299
|
+
print("\n🔄 Attempting direct PKG installation...")
|
|
300
|
+
try:
|
|
301
|
+
release_info = get_github_release_info()
|
|
302
|
+
if not release_info:
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
pkg_asset = next(
|
|
306
|
+
(a for a in release_info.get('assets', [])
|
|
307
|
+
if a['name'].endswith('.pkg') and 'macOS' in a['name']),
|
|
308
|
+
None
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if not pkg_asset:
|
|
312
|
+
print("❌ Could not find macOS PKG installer")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
temp_dir = tempfile.mkdtemp()
|
|
316
|
+
pkg_path = os.path.join(temp_dir, pkg_asset['name'])
|
|
317
|
+
print(f"⬇️ Downloading {pkg_asset['name']}...")
|
|
318
|
+
download_file(pkg_asset['browser_download_url'], pkg_path)
|
|
319
|
+
|
|
320
|
+
print("🛠 Installing...")
|
|
321
|
+
subprocess.run(
|
|
322
|
+
["sudo", "installer", "-pkg", pkg_path, "-target", "/"],
|
|
323
|
+
check=True,
|
|
324
|
+
stdout=subprocess.PIPE,
|
|
325
|
+
stderr=subprocess.PIPE
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f"❌ PKG installation failed: {str(e)}")
|
|
333
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
def try_direct_tar_install() -> bool:
|
|
337
|
+
"""Fallback tar.gz installation for macOS"""
|
|
338
|
+
print("\n🔄 Attempting direct tar.gz installation...")
|
|
339
|
+
try:
|
|
340
|
+
release_info = get_github_release_info()
|
|
341
|
+
if not release_info:
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
tar_asset = next(
|
|
345
|
+
(a for a in release_info.get('assets', [])
|
|
346
|
+
if a['name'].endswith('macOS_amd64.tar.gz')),
|
|
347
|
+
None
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if not tar_asset:
|
|
351
|
+
print("❌ Could not find macOS tar.gz package")
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
temp_dir = tempfile.mkdtemp()
|
|
355
|
+
tar_path = os.path.join(temp_dir, tar_asset['name'])
|
|
356
|
+
print(f"⬇️ Downloading {tar_asset['name']}...")
|
|
357
|
+
download_file(tar_asset['browser_download_url'], tar_path)
|
|
358
|
+
|
|
359
|
+
print("📦 Extracting...")
|
|
360
|
+
subprocess.run(
|
|
361
|
+
["tar", "-xzf", tar_path, "-C", temp_dir],
|
|
362
|
+
check=True,
|
|
363
|
+
stdout=subprocess.PIPE,
|
|
364
|
+
stderr=subprocess.PIPE
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Find the binary
|
|
368
|
+
for root, _, files in os.walk(temp_dir):
|
|
369
|
+
if "gh" in files:
|
|
370
|
+
bin_path = os.path.join(root, "gh")
|
|
371
|
+
break
|
|
372
|
+
else:
|
|
373
|
+
print("❌ Could not find gh binary in extracted files")
|
|
374
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
# Install to /usr/local/bin
|
|
378
|
+
print("🛠 Installing to /usr/local/bin...")
|
|
379
|
+
subprocess.run(
|
|
380
|
+
["sudo", "install", "-m", "755", bin_path, "/usr/local/bin/gh"],
|
|
381
|
+
check=True,
|
|
382
|
+
stdout=subprocess.PIPE,
|
|
383
|
+
stderr=subprocess.PIPE
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
print(f"❌ tar.gz installation failed: {str(e)}")
|
|
391
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
def install_gh_cli_linux() -> bool:
|
|
395
|
+
"""Linux installation with distro detection and multiple methods"""
|
|
396
|
+
methods = []
|
|
397
|
+
|
|
398
|
+
# Detect distribution
|
|
399
|
+
if os.path.exists("/etc/debian_version"):
|
|
400
|
+
methods.extend([
|
|
401
|
+
try_apt_install,
|
|
402
|
+
try_deb_install
|
|
403
|
+
])
|
|
404
|
+
elif os.path.exists("/etc/redhat-release"):
|
|
405
|
+
methods.extend([
|
|
406
|
+
try_yum_install,
|
|
407
|
+
try_dnf_install
|
|
408
|
+
])
|
|
409
|
+
elif os.path.exists("/etc/arch-release"):
|
|
410
|
+
methods.extend([
|
|
411
|
+
try_pacman_install
|
|
412
|
+
])
|
|
413
|
+
else:
|
|
414
|
+
print("⚠️ Unknown Linux distribution, trying generic methods")
|
|
415
|
+
|
|
416
|
+
# Add fallback methods
|
|
417
|
+
methods.extend([
|
|
418
|
+
try_tar_install_linux,
|
|
419
|
+
try_script_install
|
|
420
|
+
])
|
|
421
|
+
|
|
422
|
+
for method in methods:
|
|
423
|
+
if method():
|
|
424
|
+
if verify_gh_installation():
|
|
425
|
+
return True
|
|
426
|
+
print("⚠️ Trying next installation method...")
|
|
427
|
+
|
|
428
|
+
print("❌ All installation methods failed")
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
def try_apt_install() -> bool:
|
|
432
|
+
"""APT installation for Debian/Ubuntu"""
|
|
433
|
+
if not shutil.which("apt"):
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
print("\n🔄 Attempting apt installation...")
|
|
437
|
+
try:
|
|
438
|
+
subprocess.run(
|
|
439
|
+
["sudo", "apt", "update"],
|
|
440
|
+
check=True,
|
|
441
|
+
stdout=subprocess.PIPE,
|
|
442
|
+
stderr=subprocess.PIPE
|
|
443
|
+
)
|
|
444
|
+
subprocess.run(
|
|
445
|
+
["sudo", "apt", "install", "-y", "gh"],
|
|
446
|
+
check=True,
|
|
447
|
+
stdout=subprocess.PIPE,
|
|
448
|
+
stderr=subprocess.PIPE
|
|
449
|
+
)
|
|
450
|
+
return True
|
|
451
|
+
except subprocess.CalledProcessError as e:
|
|
452
|
+
print(f"⚠️ apt failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def try_deb_install() -> bool:
|
|
456
|
+
"""Direct DEB package installation"""
|
|
457
|
+
print("\n🔄 Attempting deb package installation...")
|
|
458
|
+
try:
|
|
459
|
+
release_info = get_github_release_info()
|
|
460
|
+
if not release_info:
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
deb_asset = next(
|
|
464
|
+
(a for a in release_info.get('assets', [])
|
|
465
|
+
if a['name'].endswith('linux_amd64.deb') or
|
|
466
|
+
a['name'].endswith('linux_arm64.deb')),
|
|
467
|
+
None
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if not deb_asset:
|
|
471
|
+
print("❌ Could not find DEB package")
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
temp_dir = tempfile.mkdtemp()
|
|
475
|
+
deb_path = os.path.join(temp_dir, deb_asset['name'])
|
|
476
|
+
print(f"⬇️ Downloading {deb_asset['name']}...")
|
|
477
|
+
download_file(deb_asset['browser_download_url'], deb_path)
|
|
478
|
+
|
|
479
|
+
print("🛠 Installing...")
|
|
480
|
+
subprocess.run(
|
|
481
|
+
["sudo", "dpkg", "-i", deb_path],
|
|
482
|
+
check=True,
|
|
483
|
+
stdout=subprocess.PIPE,
|
|
484
|
+
stderr=subprocess.PIPE
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Fix potential dependencies
|
|
488
|
+
subprocess.run(
|
|
489
|
+
["sudo", "apt", "--fix-broken", "install", "-y"],
|
|
490
|
+
stdout=subprocess.PIPE,
|
|
491
|
+
stderr=subprocess.PIPE
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
495
|
+
return True
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
print(f"❌ DEB installation failed: {str(e)}")
|
|
499
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
def try_yum_install() -> bool:
|
|
503
|
+
"""YUM installation for RHEL/CentOS"""
|
|
504
|
+
if not shutil.which("yum"):
|
|
505
|
+
return False
|
|
506
|
+
|
|
507
|
+
print("\n🔄 Attempting yum installation...")
|
|
508
|
+
try:
|
|
509
|
+
subprocess.run(
|
|
510
|
+
["sudo", "yum", "install", "-y", "gh"],
|
|
511
|
+
check=True,
|
|
512
|
+
stdout=subprocess.PIPE,
|
|
513
|
+
stderr=subprocess.PIPE
|
|
514
|
+
)
|
|
515
|
+
return True
|
|
516
|
+
except subprocess.CalledProcessError as e:
|
|
517
|
+
print(f"⚠️ yum failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
def try_dnf_install() -> bool:
|
|
521
|
+
"""DNF installation for Fedora"""
|
|
522
|
+
if not shutil.which("dnf"):
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
print("\n🔄 Attempting dnf installation...")
|
|
526
|
+
try:
|
|
527
|
+
subprocess.run(
|
|
528
|
+
["sudo", "dnf", "install", "-y", "gh"],
|
|
529
|
+
check=True,
|
|
530
|
+
stdout=subprocess.PIPE,
|
|
531
|
+
stderr=subprocess.PIPE
|
|
532
|
+
)
|
|
533
|
+
return True
|
|
534
|
+
except subprocess.CalledProcessError as e:
|
|
535
|
+
print(f"⚠️ dnf failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
def try_pacman_install() -> bool:
|
|
539
|
+
"""Pacman installation for Arch"""
|
|
540
|
+
if not shutil.which("pacman"):
|
|
541
|
+
return False
|
|
542
|
+
|
|
543
|
+
print("\n🔄 Attempting pacman installation...")
|
|
544
|
+
try:
|
|
545
|
+
subprocess.run(
|
|
546
|
+
["sudo", "pacman", "-Sy", "--noconfirm", "github-cli"],
|
|
547
|
+
check=True,
|
|
548
|
+
stdout=subprocess.PIPE,
|
|
549
|
+
stderr=subprocess.PIPE
|
|
550
|
+
)
|
|
551
|
+
return True
|
|
552
|
+
except subprocess.CalledProcessError as e:
|
|
553
|
+
print(f"⚠️ pacman failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
def try_tar_install_linux() -> bool:
|
|
557
|
+
"""Generic tar.gz installation for Linux"""
|
|
558
|
+
print("\n🔄 Attempting tar.gz installation...")
|
|
559
|
+
try:
|
|
560
|
+
release_info = get_github_release_info()
|
|
561
|
+
if not release_info:
|
|
562
|
+
return False
|
|
563
|
+
|
|
564
|
+
tar_asset = next(
|
|
565
|
+
(a for a in release_info.get('assets', [])
|
|
566
|
+
if a['name'].endswith('linux_amd64.tar.gz') or
|
|
567
|
+
a['name'].endswith('linux_arm64.tar.gz')),
|
|
568
|
+
None
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if not tar_asset:
|
|
572
|
+
print("❌ Could not find tar.gz package")
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
temp_dir = tempfile.mkdtemp()
|
|
576
|
+
tar_path = os.path.join(temp_dir, tar_asset['name'])
|
|
577
|
+
print(f"⬇️ Downloading {tar_asset['name']}...")
|
|
578
|
+
download_file(tar_asset['browser_download_url'], tar_path)
|
|
579
|
+
|
|
580
|
+
print("📦 Extracting...")
|
|
581
|
+
subprocess.run(
|
|
582
|
+
["tar", "-xzf", tar_path, "-C", temp_dir],
|
|
583
|
+
check=True,
|
|
584
|
+
stdout=subprocess.PIPE,
|
|
585
|
+
stderr=subprocess.PIPE
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Find the binary
|
|
589
|
+
for root, _, files in os.walk(temp_dir):
|
|
590
|
+
if "gh" in files:
|
|
591
|
+
bin_path = os.path.join(root, "gh")
|
|
592
|
+
break
|
|
593
|
+
else:
|
|
594
|
+
print("❌ Could not find gh binary in extracted files")
|
|
595
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
596
|
+
return False
|
|
597
|
+
|
|
598
|
+
# Install to /usr/local/bin
|
|
599
|
+
print("🛠 Installing to /usr/local/bin...")
|
|
600
|
+
subprocess.run(
|
|
601
|
+
["sudo", "install", "-m", "755", bin_path, "/usr/local/bin/gh"],
|
|
602
|
+
check=True,
|
|
603
|
+
stdout=subprocess.PIPE,
|
|
604
|
+
stderr=subprocess.PIPE
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
608
|
+
return True
|
|
609
|
+
|
|
610
|
+
except Exception as e:
|
|
611
|
+
print(f"❌ tar.gz installation failed: {str(e)}")
|
|
612
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
613
|
+
return False
|
|
614
|
+
|
|
615
|
+
def try_script_install() -> bool:
|
|
616
|
+
"""Fallback script installation"""
|
|
617
|
+
print("\n🔄 Attempting script installation...")
|
|
618
|
+
try:
|
|
619
|
+
subprocess.run(
|
|
620
|
+
["curl", "-fsSL", "https://cli.github.com/packages/githubcli-archive-keyring.gpg", "|", "sudo", "dd", "of=/usr/share/keyrings/githubcli-archive-keyring.gpg"],
|
|
621
|
+
check=True,
|
|
622
|
+
stdout=subprocess.PIPE,
|
|
623
|
+
stderr=subprocess.PIPE
|
|
624
|
+
)
|
|
625
|
+
subprocess.run(
|
|
626
|
+
["sudo", "chmod", "go+r", "/usr/share/keyrings/githubcli-archive-keyring.gpg"],
|
|
627
|
+
check=True,
|
|
628
|
+
stdout=subprocess.PIPE,
|
|
629
|
+
stderr=subprocess.PIPE
|
|
630
|
+
)
|
|
631
|
+
subprocess.run(
|
|
632
|
+
['echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null'],
|
|
633
|
+
shell=True,
|
|
634
|
+
check=True,
|
|
635
|
+
stdout=subprocess.PIPE,
|
|
636
|
+
stderr=subprocess.PIPE
|
|
637
|
+
)
|
|
638
|
+
subprocess.run(
|
|
639
|
+
["sudo", "apt", "update"],
|
|
640
|
+
check=True,
|
|
641
|
+
stdout=subprocess.PIPE,
|
|
642
|
+
stderr=subprocess.PIPE
|
|
643
|
+
)
|
|
644
|
+
subprocess.run(
|
|
645
|
+
["sudo", "apt", "install", "-y", "gh"],
|
|
646
|
+
check=True,
|
|
647
|
+
stdout=subprocess.PIPE,
|
|
648
|
+
stderr=subprocess.PIPE
|
|
649
|
+
)
|
|
650
|
+
return True
|
|
651
|
+
except subprocess.CalledProcessError as e:
|
|
652
|
+
print(f"⚠️ Script installation failed: {e.stderr.decode().strip() if e.stderr else 'Unknown error'}")
|
|
653
|
+
return False
|
|
654
|
+
|
|
655
|
+
def get_github_release_info() -> Optional[dict]:
|
|
656
|
+
"""Get latest release info from GitHub API"""
|
|
657
|
+
try:
|
|
658
|
+
with urllib.request.urlopen("https://api.github.com/repos/cli/cli/releases/latest") as response:
|
|
659
|
+
return json.loads(response.read().decode())
|
|
660
|
+
except Exception as e:
|
|
661
|
+
print(f"❌ Failed to get release info: {str(e)}")
|
|
662
|
+
return None
|
|
663
|
+
|
|
664
|
+
def download_file(url: str, path: str) -> bool:
|
|
665
|
+
"""Download a file with progress reporting"""
|
|
666
|
+
try:
|
|
667
|
+
def reporthook(count, block_size, total_size):
|
|
668
|
+
percent = int(count * block_size * 100 / total_size)
|
|
669
|
+
sys.stdout.write(f"\rDownloading... {percent}%")
|
|
670
|
+
sys.stdout.flush()
|
|
671
|
+
|
|
672
|
+
urllib.request.urlretrieve(url, path, reporthook=reporthook)
|
|
673
|
+
print() # New line after progress
|
|
674
|
+
return True
|
|
675
|
+
except Exception as e:
|
|
676
|
+
print(f"\n❌ Download failed: {str(e)}")
|
|
677
|
+
return False
|
|
678
|
+
|
|
679
|
+
def add_to_path(directory: str) -> bool:
|
|
680
|
+
"""Add directory to PATH if not already present"""
|
|
681
|
+
try:
|
|
682
|
+
current_path = os.environ.get("PATH", "")
|
|
683
|
+
if directory not in current_path.split(os.pathsep):
|
|
684
|
+
if platform.system() == "Windows":
|
|
685
|
+
# Permanent PATH modification on Windows
|
|
686
|
+
import winreg
|
|
687
|
+
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root:
|
|
688
|
+
with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key:
|
|
689
|
+
path_value, _ = winreg.QueryValueEx(key, "PATH")
|
|
690
|
+
new_path = f"{path_value};{directory}" if path_value else directory
|
|
691
|
+
winreg.SetValueEx(key, "PATH", 0, winreg.REG_EXPAND_SZ, new_path)
|
|
692
|
+
# Notify other processes of PATH change
|
|
693
|
+
import ctypes
|
|
694
|
+
ctypes.windll.user32.SendMessageTimeoutW(
|
|
695
|
+
0xFFFF, 0x001A, 0, "Environment", 0x02, 5000, None
|
|
696
|
+
)
|
|
697
|
+
else:
|
|
698
|
+
# For Unix-like systems, modify current session PATH
|
|
699
|
+
os.environ["PATH"] = f"{directory}{os.pathsep}{os.environ.get('PATH', '')}"
|
|
700
|
+
# Add to shell profile files
|
|
701
|
+
profile_files = [
|
|
702
|
+
os.path.expanduser("~/.bashrc"),
|
|
703
|
+
os.path.expanduser("~/.zshrc"),
|
|
704
|
+
os.path.expanduser("~/.profile")
|
|
705
|
+
]
|
|
706
|
+
export_line = f'\nexport PATH="{directory}:$PATH"\n'
|
|
707
|
+
for profile in profile_files:
|
|
708
|
+
if os.path.exists(profile):
|
|
709
|
+
with open(profile, "a") as f:
|
|
710
|
+
f.write(export_line)
|
|
711
|
+
print(f"✅ Added {directory} to PATH")
|
|
712
|
+
return True
|
|
713
|
+
except Exception as e:
|
|
714
|
+
print(f"⚠️ Failed to update PATH: {str(e)}")
|
|
715
|
+
return False
|
|
716
|
+
|
|
717
|
+
def verify_gh_installation() -> bool:
|
|
718
|
+
"""Verify gh is properly installed and in PATH"""
|
|
719
|
+
if not shutil.which("gh"):
|
|
720
|
+
print("❌ GitHub CLI not found in PATH after installation")
|
|
721
|
+
return False
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
result = subprocess.run(
|
|
725
|
+
["gh", "--version"],
|
|
726
|
+
check=True,
|
|
727
|
+
stdout=subprocess.PIPE,
|
|
728
|
+
stderr=subprocess.PIPE,
|
|
729
|
+
text=True
|
|
730
|
+
)
|
|
731
|
+
print(f"✅ GitHub CLI installed: {result.stdout.splitlines()[0]}")
|
|
732
|
+
return True
|
|
733
|
+
except subprocess.CalledProcessError:
|
|
734
|
+
print("❌ GitHub CLI found but not working")
|
|
735
|
+
return False
|
|
736
|
+
|
|
737
|
+
def check_and_install_gh() -> bool:
|
|
738
|
+
"""Main function to check and install GitHub CLI"""
|
|
739
|
+
if check_gh_installed():
|
|
740
|
+
return True
|
|
741
|
+
|
|
742
|
+
if not install_gh_cli():
|
|
743
|
+
print("\n❌ Failed to install GitHub CLI. Please try manual installation:")
|
|
744
|
+
print("Visit https://github.com/cli/cli#installation for instructions")
|
|
745
|
+
return False
|
|
746
|
+
|
|
747
|
+
if not check_gh_installed():
|
|
748
|
+
print("\n⚠️ Installation completed but GitHub CLI not detected in PATH")
|
|
749
|
+
print("Please restart your terminal or add the installation directory to your PATH")
|
|
750
|
+
return False
|
|
751
|
+
|
|
752
|
+
return True
|
|
753
|
+
|
|
754
|
+
def gh_authenticated():
|
|
755
|
+
"""Check if user is authenticated with GitHub CLI"""
|
|
756
|
+
try:
|
|
757
|
+
result = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True, check=True)
|
|
758
|
+
return "Logged in to github.com" in result.stderr
|
|
759
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
760
|
+
return False
|
|
761
|
+
|
|
762
|
+
def authenticate_with_gh():
|
|
763
|
+
"""Authenticate user with GitHub CLI"""
|
|
764
|
+
print("\n🔑 GitHub authentication required.")
|
|
765
|
+
print("The tool will use the GitHub CLI (gh) to open a browser for secure login.")
|
|
766
|
+
|
|
767
|
+
try:
|
|
768
|
+
subprocess.run(["gh", "auth", "login", "--web", "-h", "github.com"], check=True)
|
|
769
|
+
return True
|
|
770
|
+
except subprocess.CalledProcessError:
|
|
771
|
+
print("❌ Authentication failed. Please try running 'gh auth login' manually.", file=sys.stderr)
|
|
772
|
+
return False
|
|
773
|
+
|
|
774
|
+
def initialize_git_repository():
|
|
775
|
+
"""Initialize git repository if not already initialized"""
|
|
776
|
+
if os.path.exists(".git"):
|
|
777
|
+
return False
|
|
778
|
+
|
|
779
|
+
print("🛠 Initializing git repository")
|
|
780
|
+
try:
|
|
781
|
+
subprocess.run(["git", "init"], check=True, capture_output=True)
|
|
782
|
+
subprocess.run(["git", "branch", "-M", "main"], check=True, capture_output=True)
|
|
783
|
+
|
|
784
|
+
if not os.path.exists(".gitignore"):
|
|
785
|
+
with open(".gitignore", "w") as f:
|
|
786
|
+
f.write("""# Python
|
|
787
|
+
__pycache__/
|
|
788
|
+
*.py[cod]
|
|
789
|
+
*.so
|
|
790
|
+
.Python
|
|
791
|
+
env/
|
|
792
|
+
venv/
|
|
793
|
+
.env
|
|
794
|
+
|
|
795
|
+
# IDE
|
|
796
|
+
.vscode/
|
|
797
|
+
.idea/
|
|
798
|
+
*.swp
|
|
799
|
+
*.swo
|
|
800
|
+
|
|
801
|
+
# System
|
|
802
|
+
.DS_Store
|
|
803
|
+
Thumbs.db
|
|
804
|
+
|
|
805
|
+
# Project specific
|
|
806
|
+
*.log
|
|
807
|
+
*.tmp
|
|
808
|
+
*.bak
|
|
809
|
+
""")
|
|
810
|
+
print("📁 Created .gitignore file")
|
|
811
|
+
return True
|
|
812
|
+
except subprocess.CalledProcessError as e:
|
|
813
|
+
print(f"❌ Failed to initialize Git repository: {e.stderr.decode().strip()}", file=sys.stderr)
|
|
814
|
+
return False
|
|
815
|
+
|
|
816
|
+
def create_initial_commit(commit_message="Initial commit"):
|
|
817
|
+
"""Create initial commit if no commits exist"""
|
|
818
|
+
try:
|
|
819
|
+
result = subprocess.run(["git", "rev-list", "--count", "HEAD"],
|
|
820
|
+
capture_output=True, text=True)
|
|
821
|
+
commit_count = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
822
|
+
|
|
823
|
+
if commit_count == 0:
|
|
824
|
+
print("📦 Creating initial commit")
|
|
825
|
+
subprocess.run(["git", "add", "."], check=True)
|
|
826
|
+
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
|
827
|
+
return True
|
|
828
|
+
return False
|
|
829
|
+
except subprocess.CalledProcessError as e:
|
|
830
|
+
if "nothing to commit" in e.stderr.decode():
|
|
831
|
+
print(f"❌ Failed to create initial commit: No files found to commit.", file=sys.stderr)
|
|
832
|
+
print("➡️ Add some files to your project directory before creating a repository.", file=sys.stderr)
|
|
833
|
+
else:
|
|
834
|
+
print(f"❌ Failed to create initial commit: {e.stderr.decode().strip()}", file=sys.stderr)
|
|
835
|
+
return False
|
|
836
|
+
|
|
837
|
+
def create_with_gh_cli(repo_name, private=False, description="", commit_message="Initial commit"):
|
|
838
|
+
"""Create and push to new repository using GitHub CLI"""
|
|
839
|
+
try:
|
|
840
|
+
if not os.path.exists(".git"):
|
|
841
|
+
if not initialize_git_repository():
|
|
842
|
+
return False
|
|
843
|
+
|
|
844
|
+
if not create_initial_commit(commit_message):
|
|
845
|
+
if subprocess.run(["git", "status"], capture_output=True).returncode != 0:
|
|
846
|
+
return False
|
|
847
|
+
print("ℹ️ Using existing commits")
|
|
848
|
+
|
|
849
|
+
private_flag = "--private" if private else "--public"
|
|
850
|
+
cmd = ["gh", "repo", "create", repo_name, private_flag,
|
|
851
|
+
"--source=.", "--remote=origin", "--push"]
|
|
852
|
+
|
|
853
|
+
if description:
|
|
854
|
+
cmd.extend(["--description", description])
|
|
855
|
+
|
|
856
|
+
print("🚀 Creating repository and pushing code...")
|
|
857
|
+
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
858
|
+
|
|
859
|
+
repo_url = process.stderr.strip()
|
|
860
|
+
print(f"✅ Successfully created repository: {repo_url}")
|
|
861
|
+
return True
|
|
862
|
+
|
|
863
|
+
except subprocess.CalledProcessError as e:
|
|
864
|
+
error_message = e.stderr.strip()
|
|
865
|
+
if "already exists" in error_message:
|
|
866
|
+
print(f"❌ Failed to create repository: {error_message}", file=sys.stderr)
|
|
867
|
+
print("➡️ Please choose a different repository name.", file=sys.stderr)
|
|
868
|
+
else:
|
|
869
|
+
print(f"❌ Failed to create repository: {error_message}", file=sys.stderr)
|
|
870
|
+
return False
|
|
871
|
+
except Exception as e:
|
|
872
|
+
print(f"❌ An unexpected error occurred: {str(e)}", file=sys.stderr)
|
|
873
|
+
return False
|
|
874
|
+
|
|
875
|
+
def standard_git_push(commit_message, branch, remote, force=False, tags=False):
|
|
876
|
+
"""Handle standard git push operations"""
|
|
877
|
+
try:
|
|
878
|
+
subprocess.run(["git", "add", "."], check=True)
|
|
879
|
+
|
|
880
|
+
if commit_message:
|
|
881
|
+
print(f"📦 Committing with message: '{commit_message}'")
|
|
882
|
+
subprocess.run(["git", "commit", "-m", commit_message, "--allow-empty-message"], check=True)
|
|
883
|
+
else:
|
|
884
|
+
print("ℹ️ No commit message provided. Pushing only staged changes.")
|
|
885
|
+
|
|
886
|
+
push_cmd = ["git", "push"]
|
|
887
|
+
if force:
|
|
888
|
+
push_cmd.append("--force-with-lease")
|
|
889
|
+
print("⚠️ Using safe force push (--force-with-lease).")
|
|
890
|
+
if tags:
|
|
891
|
+
push_cmd.append("--tags")
|
|
892
|
+
if remote and branch:
|
|
893
|
+
push_cmd.extend([remote, branch])
|
|
894
|
+
|
|
895
|
+
print(f"🚀 Executing: {' '.join(push_cmd)}")
|
|
896
|
+
subprocess.run(push_cmd, check=True)
|
|
897
|
+
print("✅ Successfully pushed changes.")
|
|
898
|
+
return True
|
|
899
|
+
except subprocess.CalledProcessError as e:
|
|
900
|
+
error_output = e.stderr.decode().strip() if e.stderr else str(e)
|
|
901
|
+
if "nothing to commit" in error_output:
|
|
902
|
+
print("ℹ️ No changes to commit. Nothing to do.")
|
|
903
|
+
return True
|
|
904
|
+
print(f"❌ Push failed: {error_output}", file=sys.stderr)
|
|
905
|
+
return False
|
|
906
|
+
|
|
907
|
+
def run():
|
|
908
|
+
parser = argparse.ArgumentParser(
|
|
909
|
+
description="🚀 Supercharged Git push tool with GitHub repo creation",
|
|
910
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
911
|
+
epilog="""Examples:
|
|
912
|
+
Standard push: gitpush_tool "My new feature"
|
|
913
|
+
Create new repo: gitpush_tool "Initial commit" --new-repo my-awesome-project
|
|
914
|
+
Private repository: gitpush_tool "Initial commit" --new-repo my-secret-project --private
|
|
915
|
+
Force push (safe): gitpush_tool "Rebased feature" --force
|
|
916
|
+
Initialize only: gitpush_tool --init
|
|
917
|
+
"""
|
|
918
|
+
)
|
|
919
|
+
parser.add_argument("commit", nargs="?", help="Commit message (optional if just pushing staged changes).")
|
|
920
|
+
parser.add_argument("branch", nargs="?", default=None, help="Branch name (defaults to current branch).")
|
|
921
|
+
parser.add_argument("remote", nargs="?", default="origin", help="Remote name (default: origin).")
|
|
922
|
+
parser.add_argument("--force", action="store_true", help="Force push with --force-with-lease.")
|
|
923
|
+
parser.add_argument("--tags", action="store_true", help="Push all tags.")
|
|
924
|
+
parser.add_argument("--init", action="store_true", help="Initialize a new Git repository and exit.")
|
|
925
|
+
parser.add_argument("--new-repo", metavar="REPO_NAME", help="Create a new GitHub repository with the given name.")
|
|
926
|
+
parser.add_argument("--private", action="store_true", help="Make the new repository private.")
|
|
927
|
+
parser.add_argument("--description", help="Description for the new repository.")
|
|
928
|
+
|
|
929
|
+
args = parser.parse_args()
|
|
930
|
+
|
|
931
|
+
target_branch = args.branch
|
|
932
|
+
if not target_branch:
|
|
933
|
+
try:
|
|
934
|
+
branch_result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=True)
|
|
935
|
+
target_branch = branch_result.stdout.strip()
|
|
936
|
+
except subprocess.CalledProcessError:
|
|
937
|
+
target_branch = "main"
|
|
938
|
+
|
|
939
|
+
if args.new_repo:
|
|
940
|
+
if not check_gh_installed():
|
|
941
|
+
sys.exit(1)
|
|
942
|
+
|
|
943
|
+
if not gh_authenticated():
|
|
944
|
+
if not authenticate_with_gh():
|
|
945
|
+
sys.exit(1)
|
|
946
|
+
|
|
947
|
+
if not create_with_gh_cli(
|
|
948
|
+
args.new_repo,
|
|
949
|
+
private=args.private,
|
|
950
|
+
description=args.description or "",
|
|
951
|
+
commit_message=args.commit or "Initial commit"
|
|
952
|
+
):
|
|
953
|
+
sys.exit(1)
|
|
954
|
+
|
|
955
|
+
elif args.init:
|
|
956
|
+
if initialize_git_repository():
|
|
957
|
+
print("✅ Git repository initialized successfully.")
|
|
958
|
+
|
|
959
|
+
else:
|
|
960
|
+
if not standard_git_push(
|
|
961
|
+
args.commit,
|
|
962
|
+
target_branch,
|
|
963
|
+
args.remote,
|
|
964
|
+
args.force,
|
|
965
|
+
args.tags
|
|
966
|
+
):
|
|
967
|
+
sys.exit(1)
|
|
968
|
+
|
|
969
|
+
if __name__ == "__main__":
|
|
970
|
+
run()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.10"
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import argparse
|
|
3
|
-
import sys
|
|
4
|
-
import subprocess
|
|
5
|
-
import shutil
|
|
6
|
-
import platform
|
|
7
|
-
|
|
8
|
-
def check_gh_installed():
|
|
9
|
-
"""
|
|
10
|
-
Checks if GitHub CLI is installed. If not, it provides a comprehensive list
|
|
11
|
-
of platform-specific installation commands and fallbacks.
|
|
12
|
-
"""
|
|
13
|
-
if shutil.which("gh"):
|
|
14
|
-
return True
|
|
15
|
-
|
|
16
|
-
print("❌ Error: GitHub CLI (gh) is not installed or not in your system's PATH.", file=sys.stderr)
|
|
17
|
-
print(" The '--new-repo' feature requires the GitHub CLI for all repository operations.", file=sys.stderr)
|
|
18
|
-
|
|
19
|
-
system = platform.system()
|
|
20
|
-
install_options = []
|
|
21
|
-
|
|
22
|
-
# --- Platform-specific detection with fallbacks ---
|
|
23
|
-
if system == "Windows":
|
|
24
|
-
print("\n➡️ For Windows, we recommend one of the following package managers:", file=sys.stderr)
|
|
25
|
-
if shutil.which("winget"):
|
|
26
|
-
install_options.append(("Winget (Recommended)", "winget install --id GitHub.cli --source winget"))
|
|
27
|
-
if shutil.which("scoop"):
|
|
28
|
-
install_options.append(("Scoop", "scoop install gh"))
|
|
29
|
-
if shutil.which("choco"):
|
|
30
|
-
install_options.append(("Chocolatey", "choco install gh"))
|
|
31
|
-
|
|
32
|
-
elif system == "Darwin":
|
|
33
|
-
print("\n➡️ For macOS, we recommend using Homebrew:", file=sys.stderr)
|
|
34
|
-
if shutil.which("brew"):
|
|
35
|
-
install_options.append(("Homebrew (Recommended)", "brew install gh"))
|
|
36
|
-
|
|
37
|
-
elif system == "Linux":
|
|
38
|
-
print("\n➡️ For Linux, we recommend one of the following package managers:", file=sys.stderr)
|
|
39
|
-
if shutil.which("apt") or shutil.which("apt-get"):
|
|
40
|
-
install_options.append(("Debian/Ubuntu/Mint", "sudo apt update && sudo apt install gh -y"))
|
|
41
|
-
if shutil.which("dnf"):
|
|
42
|
-
install_options.append(("Fedora/RHEL/CentOS", "sudo dnf install gh"))
|
|
43
|
-
if shutil.which("yum"):
|
|
44
|
-
install_options.append(("Older Fedora/RHEL/CentOS", "sudo yum install gh"))
|
|
45
|
-
if shutil.which("pacman"):
|
|
46
|
-
install_options.append(("Arch Linux", "sudo pacman -S github-cli"))
|
|
47
|
-
if shutil.which("zypper"):
|
|
48
|
-
install_options.append(("openSUSE", "sudo zypper install gh"))
|
|
49
|
-
|
|
50
|
-
# --- Displaying the options and the ultimate fallback ---
|
|
51
|
-
if install_options:
|
|
52
|
-
print(" Please copy and run one of the commands below in your terminal:\n", file=sys.stderr)
|
|
53
|
-
for name, command in install_options:
|
|
54
|
-
print(f" # For {name}:\n {command}\n", file=sys.stderr)
|
|
55
|
-
|
|
56
|
-
print(" If none of the above are available on your system, please follow the", file=sys.stderr)
|
|
57
|
-
print(" official installation guide for more options (including manual download):", file=sys.stderr)
|
|
58
|
-
print(" https://github.com/cli/cli#installation\n", file=sys.stderr)
|
|
59
|
-
print("‼️ After installation, please open a NEW terminal and run your command again.", file=sys.stderr)
|
|
60
|
-
|
|
61
|
-
return False
|
|
62
|
-
|
|
63
|
-
def gh_authenticated():
|
|
64
|
-
"""Check if user is authenticated with GitHub CLI"""
|
|
65
|
-
try:
|
|
66
|
-
result = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True, check=True)
|
|
67
|
-
return "Logged in to github.com" in result.stderr
|
|
68
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
def authenticate_with_gh():
|
|
72
|
-
"""Authenticate user with GitHub CLI"""
|
|
73
|
-
print("\n🔑 GitHub authentication required.")
|
|
74
|
-
print("The tool will use the GitHub CLI (gh) to open a browser for secure login.")
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
subprocess.run(["gh", "auth", "login", "--web", "-h", "github.com"], check=True)
|
|
78
|
-
return True
|
|
79
|
-
except subprocess.CalledProcessError:
|
|
80
|
-
print("❌ Authentication failed. Please try running 'gh auth login' manually.", file=sys.stderr)
|
|
81
|
-
return False
|
|
82
|
-
|
|
83
|
-
def initialize_git_repository():
|
|
84
|
-
"""Initialize git repository if not already initialized"""
|
|
85
|
-
if os.path.exists(".git"):
|
|
86
|
-
return False
|
|
87
|
-
|
|
88
|
-
print("🛠 Initializing git repository")
|
|
89
|
-
try:
|
|
90
|
-
subprocess.run(["git", "init"], check=True, capture_output=True)
|
|
91
|
-
subprocess.run(["git", "branch", "-M", "main"], check=True, capture_output=True)
|
|
92
|
-
|
|
93
|
-
if not os.path.exists(".gitignore"):
|
|
94
|
-
with open(".gitignore", "w") as f:
|
|
95
|
-
f.write("""# Python
|
|
96
|
-
__pycache__/
|
|
97
|
-
*.py[cod]
|
|
98
|
-
*.so
|
|
99
|
-
.Python
|
|
100
|
-
env/
|
|
101
|
-
venv/
|
|
102
|
-
.env
|
|
103
|
-
|
|
104
|
-
# IDE
|
|
105
|
-
.vscode/
|
|
106
|
-
.idea/
|
|
107
|
-
*.swp
|
|
108
|
-
*.swo
|
|
109
|
-
|
|
110
|
-
# System
|
|
111
|
-
.DS_Store
|
|
112
|
-
Thumbs.db
|
|
113
|
-
|
|
114
|
-
# Project specific
|
|
115
|
-
*.log
|
|
116
|
-
*.tmp
|
|
117
|
-
*.bak
|
|
118
|
-
""")
|
|
119
|
-
print("📁 Created .gitignore file")
|
|
120
|
-
return True
|
|
121
|
-
except subprocess.CalledProcessError as e:
|
|
122
|
-
print(f"❌ Failed to initialize Git repository: {e.stderr.decode().strip()}", file=sys.stderr)
|
|
123
|
-
return False
|
|
124
|
-
|
|
125
|
-
def create_initial_commit(commit_message="Initial commit"):
|
|
126
|
-
"""Create initial commit if no commits exist"""
|
|
127
|
-
try:
|
|
128
|
-
result = subprocess.run(["git", "rev-list", "--count", "HEAD"],
|
|
129
|
-
capture_output=True, text=True)
|
|
130
|
-
commit_count = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
131
|
-
|
|
132
|
-
if commit_count == 0:
|
|
133
|
-
print("📦 Creating initial commit")
|
|
134
|
-
subprocess.run(["git", "add", "."], check=True)
|
|
135
|
-
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
|
136
|
-
return True
|
|
137
|
-
return False
|
|
138
|
-
except subprocess.CalledProcessError as e:
|
|
139
|
-
if "nothing to commit" in e.stderr.decode():
|
|
140
|
-
print(f"❌ Failed to create initial commit: No files found to commit.", file=sys.stderr)
|
|
141
|
-
print("➡️ Add some files to your project directory before creating a repository.", file=sys.stderr)
|
|
142
|
-
else:
|
|
143
|
-
print(f"❌ Failed to create initial commit: {e.stderr.decode().strip()}", file=sys.stderr)
|
|
144
|
-
return False
|
|
145
|
-
|
|
146
|
-
def create_with_gh_cli(repo_name, private=False, description="", commit_message="Initial commit"):
|
|
147
|
-
"""Create and push to new repository using GitHub CLI"""
|
|
148
|
-
try:
|
|
149
|
-
if not os.path.exists(".git"):
|
|
150
|
-
if not initialize_git_repository():
|
|
151
|
-
return False
|
|
152
|
-
|
|
153
|
-
if not create_initial_commit(commit_message):
|
|
154
|
-
if subprocess.run(["git", "status"], capture_output=True).returncode != 0:
|
|
155
|
-
return False
|
|
156
|
-
print("ℹ️ Using existing commits")
|
|
157
|
-
|
|
158
|
-
private_flag = "--private" if private else "--public"
|
|
159
|
-
cmd = ["gh", "repo", "create", repo_name, private_flag,
|
|
160
|
-
"--source=.", "--remote=origin", "--push"]
|
|
161
|
-
|
|
162
|
-
if description:
|
|
163
|
-
cmd.extend(["--description", description])
|
|
164
|
-
|
|
165
|
-
print("🚀 Creating repository and pushing code...")
|
|
166
|
-
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
167
|
-
|
|
168
|
-
repo_url = process.stderr.strip()
|
|
169
|
-
print(f"✅ Successfully created repository: {repo_url}")
|
|
170
|
-
return True
|
|
171
|
-
|
|
172
|
-
except subprocess.CalledProcessError as e:
|
|
173
|
-
error_message = e.stderr.strip()
|
|
174
|
-
if "already exists" in error_message:
|
|
175
|
-
print(f"❌ Failed to create repository: {error_message}", file=sys.stderr)
|
|
176
|
-
print("➡️ Please choose a different repository name.", file=sys.stderr)
|
|
177
|
-
else:
|
|
178
|
-
print(f"❌ Failed to create repository: {error_message}", file=sys.stderr)
|
|
179
|
-
return False
|
|
180
|
-
except Exception as e:
|
|
181
|
-
print(f"❌ An unexpected error occurred: {str(e)}", file=sys.stderr)
|
|
182
|
-
return False
|
|
183
|
-
|
|
184
|
-
def standard_git_push(commit_message, branch, remote, force=False, tags=False):
|
|
185
|
-
"""Handle standard git push operations"""
|
|
186
|
-
try:
|
|
187
|
-
subprocess.run(["git", "add", "."], check=True)
|
|
188
|
-
|
|
189
|
-
if commit_message:
|
|
190
|
-
print(f"📦 Committing with message: '{commit_message}'")
|
|
191
|
-
subprocess.run(["git", "commit", "-m", commit_message, "--allow-empty-message"], check=True)
|
|
192
|
-
else:
|
|
193
|
-
print("ℹ️ No commit message provided. Pushing only staged changes.")
|
|
194
|
-
|
|
195
|
-
push_cmd = ["git", "push"]
|
|
196
|
-
if force:
|
|
197
|
-
push_cmd.append("--force-with-lease")
|
|
198
|
-
print("⚠️ Using safe force push (--force-with-lease).")
|
|
199
|
-
if tags:
|
|
200
|
-
push_cmd.append("--tags")
|
|
201
|
-
if remote and branch:
|
|
202
|
-
push_cmd.extend([remote, branch])
|
|
203
|
-
|
|
204
|
-
print(f"🚀 Executing: {' '.join(push_cmd)}")
|
|
205
|
-
subprocess.run(push_cmd, check=True)
|
|
206
|
-
print("✅ Successfully pushed changes.")
|
|
207
|
-
return True
|
|
208
|
-
except subprocess.CalledProcessError as e:
|
|
209
|
-
error_output = e.stderr.decode().strip() if e.stderr else str(e)
|
|
210
|
-
if "nothing to commit" in error_output:
|
|
211
|
-
print("ℹ️ No changes to commit. Nothing to do.")
|
|
212
|
-
return True
|
|
213
|
-
print(f"❌ Push failed: {error_output}", file=sys.stderr)
|
|
214
|
-
return False
|
|
215
|
-
|
|
216
|
-
def run():
|
|
217
|
-
parser = argparse.ArgumentParser(
|
|
218
|
-
description="🚀 Supercharged Git push tool with GitHub repo creation",
|
|
219
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
220
|
-
epilog="""Examples:
|
|
221
|
-
Standard push: gitpush_tool "My new feature"
|
|
222
|
-
Create new repo: gitpush_tool "Initial commit" --new-repo my-awesome-project
|
|
223
|
-
Private repository: gitpush_tool "Initial commit" --new-repo my-secret-project --private
|
|
224
|
-
Force push (safe): gitpush_tool "Rebased feature" --force
|
|
225
|
-
Initialize only: gitpush_tool --init
|
|
226
|
-
"""
|
|
227
|
-
)
|
|
228
|
-
parser.add_argument("commit", nargs="?", help="Commit message (optional if just pushing staged changes).")
|
|
229
|
-
parser.add_argument("branch", nargs="?", default=None, help="Branch name (defaults to current branch).")
|
|
230
|
-
parser.add_argument("remote", nargs="?", default="origin", help="Remote name (default: origin).")
|
|
231
|
-
parser.add_argument("--force", action="store_true", help="Force push with --force-with-lease.")
|
|
232
|
-
parser.add_argument("--tags", action="store_true", help="Push all tags.")
|
|
233
|
-
parser.add_argument("--init", action="store_true", help="Initialize a new Git repository and exit.")
|
|
234
|
-
parser.add_argument("--new-repo", metavar="REPO_NAME", help="Create a new GitHub repository with the given name.")
|
|
235
|
-
parser.add_argument("--private", action="store_true", help="Make the new repository private.")
|
|
236
|
-
parser.add_argument("--description", help="Description for the new repository.")
|
|
237
|
-
|
|
238
|
-
args = parser.parse_args()
|
|
239
|
-
|
|
240
|
-
target_branch = args.branch
|
|
241
|
-
if not target_branch:
|
|
242
|
-
try:
|
|
243
|
-
branch_result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=True)
|
|
244
|
-
target_branch = branch_result.stdout.strip()
|
|
245
|
-
except subprocess.CalledProcessError:
|
|
246
|
-
target_branch = "main"
|
|
247
|
-
|
|
248
|
-
if args.new_repo:
|
|
249
|
-
if not check_gh_installed():
|
|
250
|
-
sys.exit(1)
|
|
251
|
-
|
|
252
|
-
if not gh_authenticated():
|
|
253
|
-
if not authenticate_with_gh():
|
|
254
|
-
sys.exit(1)
|
|
255
|
-
|
|
256
|
-
if not create_with_gh_cli(
|
|
257
|
-
args.new_repo,
|
|
258
|
-
private=args.private,
|
|
259
|
-
description=args.description or "",
|
|
260
|
-
commit_message=args.commit or "Initial commit"
|
|
261
|
-
):
|
|
262
|
-
sys.exit(1)
|
|
263
|
-
|
|
264
|
-
elif args.init:
|
|
265
|
-
if initialize_git_repository():
|
|
266
|
-
print("✅ Git repository initialized successfully.")
|
|
267
|
-
|
|
268
|
-
else:
|
|
269
|
-
if not standard_git_push(
|
|
270
|
-
args.commit,
|
|
271
|
-
target_branch,
|
|
272
|
-
args.remote,
|
|
273
|
-
args.force,
|
|
274
|
-
args.tags
|
|
275
|
-
):
|
|
276
|
-
sys.exit(1)
|
|
277
|
-
|
|
278
|
-
if __name__ == "__main__":
|
|
279
|
-
run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|