gitpush-tool 0.3.0__tar.gz → 0.3.2__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.3.0/gitpush_tool.egg-info → gitpush_tool-0.3.2}/PKG-INFO +6 -6
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/README.md +14 -14
- {gitpush_tool-0.3.0/gitpush_tool → gitpush_tool-0.3.2/gitpush}/__doc__.py +3 -3
- gitpush_tool-0.3.2/gitpush/__init__.py +1 -0
- gitpush_tool-0.3.2/gitpush/cli.py +545 -0
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2/gitpush_tool.egg-info}/PKG-INFO +6 -6
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/gitpush_tool.egg-info/SOURCES.txt +3 -3
- gitpush_tool-0.3.2/gitpush_tool.egg-info/entry_points.txt +2 -0
- gitpush_tool-0.3.2/gitpush_tool.egg-info/top_level.txt +1 -0
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/setup.py +2 -2
- gitpush_tool-0.3.0/gitpush_tool/__init__.py +0 -1
- gitpush_tool-0.3.0/gitpush_tool/cli.py +0 -970
- gitpush_tool-0.3.0/gitpush_tool.egg-info/entry_points.txt +0 -2
- gitpush_tool-0.3.0/gitpush_tool.egg-info/top_level.txt +0 -1
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/LICENSE +0 -0
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/MANIFEST.in +0 -0
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/gitpush_tool.egg-info/dependency_links.txt +0 -0
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/pyproject.toml +0 -0
- {gitpush_tool-0.3.0 → gitpush_tool-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpush-tool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Supercharged Git push tool with automatic GitHub repo creation and pushing
|
|
5
5
|
Home-page: https://github.com/inevitablegs/gitpush
|
|
6
6
|
Author: Ganesh Sonawane
|
|
@@ -40,22 +40,22 @@ gh auth login # Authenticate with GitHub
|
|
|
40
40
|
### 1. New Repository Creation
|
|
41
41
|
```bash
|
|
42
42
|
# Create public repo
|
|
43
|
-
|
|
43
|
+
gitpush "Initial commit" --new-repo my-project
|
|
44
44
|
|
|
45
45
|
# Create private repo
|
|
46
|
-
|
|
46
|
+
gitpush "Initial commit" --new-repo private-project --private
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
### 2. Standard Git Operations
|
|
50
50
|
```bash
|
|
51
51
|
# Regular push
|
|
52
|
-
|
|
52
|
+
gitpush "Fixed login bug"
|
|
53
53
|
|
|
54
54
|
# Force push
|
|
55
|
-
|
|
55
|
+
gitpush "Rebased history" --force
|
|
56
56
|
|
|
57
57
|
# Push tags
|
|
58
|
-
|
|
58
|
+
gitpush --tags
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
## ⚙️ Configuration
|
|
@@ -27,36 +27,36 @@ pip install gitpush-tool
|
|
|
27
27
|
|
|
28
28
|
| Command | Description |
|
|
29
29
|
|--------|-------------|
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
30
|
+
| `gitpush "Commit message"` | Standard push with commit |
|
|
31
|
+
| `gitpush` | Push without commit (only staged changes) |
|
|
32
|
+
| `gitpush --force` | Safe force push |
|
|
33
|
+
| `gitpush --tags` | Push all tags |
|
|
34
34
|
|
|
35
35
|
### New Repository Workflow
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
# Create new public repo
|
|
39
|
-
|
|
39
|
+
gitpush "Initial commit" --new-repo project-name
|
|
40
40
|
|
|
41
41
|
# Create private repo with description
|
|
42
|
-
|
|
42
|
+
gitpush "Initial commit" --new-repo private-project --private --description "My awesome project"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
### Branch Management
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
# Push to specific branch
|
|
49
|
-
|
|
49
|
+
gitpush "Commit message" feature-branch
|
|
50
50
|
|
|
51
51
|
# Push to specific remote and branch
|
|
52
|
-
|
|
52
|
+
gitpush "Commit message" main upstream
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
### Initialization
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
# Initialize new repo only
|
|
59
|
-
|
|
59
|
+
gitpush --init
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## Workflow Examples 🔥
|
|
@@ -67,17 +67,17 @@ gitpush_tool --init
|
|
|
67
67
|
mkdir my-app
|
|
68
68
|
cd my-app
|
|
69
69
|
touch README.md main.py
|
|
70
|
-
|
|
70
|
+
gitpush "Initial commit" --new-repo my-app
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
### Scenario 2: Existing Project Updates
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
# After making changes
|
|
77
|
-
|
|
77
|
+
gitpush "Fixed authentication bug"
|
|
78
78
|
|
|
79
79
|
# Force push after rebase
|
|
80
|
-
|
|
80
|
+
gitpush "Rebased commits" --force
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
### Scenario 3: Create Empty Repository
|
|
@@ -85,7 +85,7 @@ gitpush_tool "Rebased commits" --force
|
|
|
85
85
|
```bash
|
|
86
86
|
mkdir empty-project
|
|
87
87
|
cd empty-project
|
|
88
|
-
|
|
88
|
+
gitpush --init
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
## Configuration ⚙️
|
|
@@ -161,6 +161,6 @@ Contributions welcome! Please follow these steps:
|
|
|
161
161
|
|
|
162
162
|
## License 📄
|
|
163
163
|
|
|
164
|
-
MIT - See LICENSE for details.
|
|
164
|
+
MIT - See LICENSE for details.
|
|
165
165
|
|
|
166
166
|
<center>✨ <strong>Happy Coding!</strong> ✨</center>
|
|
@@ -11,9 +11,9 @@ Key Features:
|
|
|
11
11
|
• Fresh project setup in one command
|
|
12
12
|
|
|
13
13
|
Basic Usage:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
gitpush "Commit message" # Standard push
|
|
15
|
+
gitpush --new-repo project-name # Create new repo
|
|
16
|
+
gitpush --force # Safe force push
|
|
17
17
|
|
|
18
18
|
Install GitHub CLI first:
|
|
19
19
|
macOS: brew install gh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2"
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
import shutil
|
|
6
|
+
import platform
|
|
7
|
+
import json
|
|
8
|
+
import tempfile
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
# --- Installation Orchestrator and Helpers (Your Code, Integrated) ---
|
|
13
|
+
|
|
14
|
+
def check_gh_installed() -> bool:
|
|
15
|
+
"""Check if GitHub CLI is installed with proper verification"""
|
|
16
|
+
if shutil.which("gh"):
|
|
17
|
+
try:
|
|
18
|
+
# Verify gh is actually working
|
|
19
|
+
subprocess.run(["gh", "--version"], check=True, capture_output=True)
|
|
20
|
+
return True
|
|
21
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
22
|
+
# Found but not working - might be a PATH issue or broken install
|
|
23
|
+
return False
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def install_gh_cli() -> bool:
|
|
27
|
+
"""Main installation function with comprehensive error handling"""
|
|
28
|
+
system = platform.system()
|
|
29
|
+
machine = platform.machine().lower()
|
|
30
|
+
|
|
31
|
+
print("\n🔧 Installing GitHub CLI...")
|
|
32
|
+
print(f"📋 System: {system}, Architecture: {machine}")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if system == "Windows":
|
|
36
|
+
return install_gh_cli_windows()
|
|
37
|
+
elif system == "Darwin":
|
|
38
|
+
return install_gh_cli_mac()
|
|
39
|
+
elif system == "Linux":
|
|
40
|
+
return install_gh_cli_linux()
|
|
41
|
+
else:
|
|
42
|
+
print(f"❌ Unsupported OS: {system}")
|
|
43
|
+
return False
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"❌ Installation failed: {str(e)}")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def install_gh_cli_windows() -> bool:
|
|
49
|
+
"""Windows installation with multiple fallback methods and PATH management"""
|
|
50
|
+
methods = [
|
|
51
|
+
try_winget_install,
|
|
52
|
+
try_scoop_install,
|
|
53
|
+
try_choco_install,
|
|
54
|
+
try_direct_msi_install,
|
|
55
|
+
try_direct_zip_install
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for method in methods:
|
|
59
|
+
if method():
|
|
60
|
+
if verify_gh_installation():
|
|
61
|
+
return True
|
|
62
|
+
print(" ⚠️ Trying next installation method...")
|
|
63
|
+
|
|
64
|
+
print("❌ All Windows installation methods failed.")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def try_winget_install() -> bool:
|
|
68
|
+
"""Attempt installation via winget"""
|
|
69
|
+
if not shutil.which("winget"):
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
print("\n 🔄 Attempting winget installation...")
|
|
73
|
+
try:
|
|
74
|
+
subprocess.run(
|
|
75
|
+
["winget", "install", "--id", "GitHub.cli", "--silent", "--accept-package-agreements", "--accept-source-agreements"],
|
|
76
|
+
check=True,
|
|
77
|
+
capture_output=True
|
|
78
|
+
)
|
|
79
|
+
return True
|
|
80
|
+
except subprocess.CalledProcessError as e:
|
|
81
|
+
print(f" ⚠️ winget failed: {e.stderr.decode(errors='ignore').strip() if e.stderr else 'Unknown error'}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def try_scoop_install() -> bool:
|
|
85
|
+
"""Attempt installation via scoop"""
|
|
86
|
+
if not shutil.which("scoop"):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
print("\n 🔄 Attempting scoop installation...")
|
|
90
|
+
try:
|
|
91
|
+
subprocess.run(["scoop", "install", "gh"], check=True, capture_output=True)
|
|
92
|
+
return True
|
|
93
|
+
except subprocess.CalledProcessError as e:
|
|
94
|
+
print(f" ⚠️ scoop failed: {e.stderr.decode(errors='ignore').strip() if e.stderr else 'Unknown error'}")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def try_choco_install() -> bool:
|
|
98
|
+
"""Attempt installation via chocolatey"""
|
|
99
|
+
if not shutil.which("choco"):
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
print("\n 🔄 Attempting chocolatey installation...")
|
|
103
|
+
try:
|
|
104
|
+
subprocess.run(["choco", "install", "gh", "-y"], check=True, capture_output=True)
|
|
105
|
+
return True
|
|
106
|
+
except subprocess.CalledProcessError as e:
|
|
107
|
+
print(f" ⚠️ chocolatey failed: {e.stderr.decode(errors='ignore').strip() if e.stderr else 'Unknown error'}")
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def try_direct_msi_install() -> bool:
|
|
111
|
+
"""Direct MSI installation with proper PATH handling"""
|
|
112
|
+
print("\n 🔄 Attempting direct MSI installation...")
|
|
113
|
+
temp_dir = ""
|
|
114
|
+
try:
|
|
115
|
+
release_info = get_github_release_info()
|
|
116
|
+
if not release_info: return False
|
|
117
|
+
|
|
118
|
+
msi_asset = next((a for a in release_info.get('assets', []) if a['name'].endswith('_windows_amd64.msi')), None)
|
|
119
|
+
if not msi_asset:
|
|
120
|
+
print(" ❌ Could not find Windows MSI installer.")
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
temp_dir = tempfile.mkdtemp()
|
|
124
|
+
msi_path = os.path.join(temp_dir, msi_asset['name'])
|
|
125
|
+
print(f" ⬇️ Downloading {msi_asset['name']}...")
|
|
126
|
+
if not download_file(msi_asset['browser_download_url'], msi_path): return False
|
|
127
|
+
|
|
128
|
+
print(" 🛠 Installing (this may require administrator privileges)...")
|
|
129
|
+
subprocess.run(["msiexec", "/i", msi_path, "/quiet", "/norestart"], check=True)
|
|
130
|
+
|
|
131
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
132
|
+
|
|
133
|
+
program_files = os.environ.get("ProgramFiles", "C:\\Program Files")
|
|
134
|
+
gh_path = os.path.join(program_files, "GitHub CLI", "gh.exe")
|
|
135
|
+
if os.path.exists(gh_path): add_to_path(os.path.dirname(gh_path))
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f" ❌ MSI installation failed: {str(e)}")
|
|
140
|
+
if temp_dir: shutil.rmtree(temp_dir, ignore_errors=True)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def try_direct_zip_install() -> bool:
|
|
144
|
+
"""Fallback ZIP installation for Windows"""
|
|
145
|
+
print("\n 🔄 Attempting direct ZIP installation...")
|
|
146
|
+
temp_dir = ""
|
|
147
|
+
try:
|
|
148
|
+
release_info = get_github_release_info()
|
|
149
|
+
if not release_info: return False
|
|
150
|
+
|
|
151
|
+
zip_asset = next((a for a in release_info.get('assets', []) if a['name'].endswith('windows_amd64.zip')), None)
|
|
152
|
+
if not zip_asset:
|
|
153
|
+
print(" ❌ Could not find Windows ZIP package.")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
temp_dir = tempfile.mkdtemp()
|
|
157
|
+
zip_path = os.path.join(temp_dir, zip_asset['name'])
|
|
158
|
+
print(f" ⬇️ Downloading {zip_asset['name']}...")
|
|
159
|
+
if not download_file(zip_asset['browser_download_url'], zip_path): return False
|
|
160
|
+
|
|
161
|
+
print(" 📦 Extracting...")
|
|
162
|
+
shutil.unpack_archive(zip_path, temp_dir)
|
|
163
|
+
|
|
164
|
+
bin_dir = next((root for root, _, files in os.walk(temp_dir) if "gh.exe" in files), None)
|
|
165
|
+
if not bin_dir:
|
|
166
|
+
print(" ❌ Could not find gh.exe in extracted files.")
|
|
167
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
install_dir = os.path.join(os.environ.get("LOCALAPPDATA", ""), "GitHubCLI")
|
|
171
|
+
os.makedirs(install_dir, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
shutil.copytree(bin_dir, install_dir, dirs_exist_ok=True)
|
|
174
|
+
add_to_path(install_dir)
|
|
175
|
+
|
|
176
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
177
|
+
return True
|
|
178
|
+
except Exception as e:
|
|
179
|
+
print(f" ❌ ZIP installation failed: {str(e)}")
|
|
180
|
+
if temp_dir: shutil.rmtree(temp_dir, ignore_errors=True)
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def install_gh_cli_mac() -> bool:
|
|
184
|
+
"""macOS installation with multiple methods"""
|
|
185
|
+
if shutil.which("brew"):
|
|
186
|
+
print("\n 🔄 Attempting Homebrew installation...")
|
|
187
|
+
try:
|
|
188
|
+
subprocess.run(["brew", "install", "gh"], check=True, capture_output=True)
|
|
189
|
+
if verify_gh_installation(): return True
|
|
190
|
+
except subprocess.CalledProcessError as e:
|
|
191
|
+
print(f" ⚠️ Homebrew failed: {e.stderr.decode(errors='ignore').strip() if e.stderr else 'Unknown error'}")
|
|
192
|
+
|
|
193
|
+
print("❌ All macOS installation methods failed.")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def install_gh_cli_linux() -> bool:
|
|
197
|
+
"""Linux installation with distro detection and multiple methods"""
|
|
198
|
+
package_managers = [
|
|
199
|
+
("apt-get", "sudo apt-get update && sudo apt-get install -y gh"),
|
|
200
|
+
("apt", "sudo apt update && sudo apt install -y gh"),
|
|
201
|
+
("dnf", "sudo dnf install -y gh"),
|
|
202
|
+
("yum", "sudo yum install -y gh"),
|
|
203
|
+
("pacman", "sudo pacman -S --noconfirm github-cli"),
|
|
204
|
+
("zypper", "sudo zypper install -y gh"),
|
|
205
|
+
]
|
|
206
|
+
for pm, command in package_managers:
|
|
207
|
+
if shutil.which(pm):
|
|
208
|
+
print(f"\n 🔄 Attempting installation via {pm}...")
|
|
209
|
+
try:
|
|
210
|
+
subprocess.run(command, shell=True, check=True, capture_output=True)
|
|
211
|
+
if verify_gh_installation(): return True
|
|
212
|
+
except subprocess.CalledProcessError as e:
|
|
213
|
+
print(f" ⚠️ {pm} failed: {e.stderr.decode(errors='ignore').strip() if e.stderr else 'Unknown error'}")
|
|
214
|
+
|
|
215
|
+
print("❌ All Linux package manager installations failed.")
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
def get_github_release_info() -> Optional[dict]:
|
|
219
|
+
"""Get latest release info from GitHub API"""
|
|
220
|
+
try:
|
|
221
|
+
with urllib.request.urlopen("https://api.github.com/repos/cli/cli/releases/latest") as response:
|
|
222
|
+
return json.loads(response.read().decode())
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f" ❌ Failed to get release info from GitHub API: {str(e)}")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def download_file(url: str, path: str) -> bool:
|
|
228
|
+
"""Download a file with progress reporting"""
|
|
229
|
+
try:
|
|
230
|
+
def reporthook(count, block_size, total_size):
|
|
231
|
+
if total_size > 0:
|
|
232
|
+
percent = int(count * block_size * 100 / total_size)
|
|
233
|
+
sys.stdout.write(f"\r Downloading... {percent}%")
|
|
234
|
+
sys.stdout.flush()
|
|
235
|
+
|
|
236
|
+
urllib.request.urlretrieve(url, path, reporthook=reporthook)
|
|
237
|
+
sys.stdout.write("\r Downloading... 100%\n")
|
|
238
|
+
sys.stdout.flush()
|
|
239
|
+
return True
|
|
240
|
+
except Exception as e:
|
|
241
|
+
print(f"\n ❌ Download failed: {str(e)}")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def add_to_path(directory: str):
|
|
245
|
+
"""Add directory to PATH for the current session and try to make it permanent."""
|
|
246
|
+
print(f" ✅ Adding {directory} to PATH...")
|
|
247
|
+
os.environ["PATH"] = f"{directory}{os.pathsep}{os.environ['PATH']}"
|
|
248
|
+
|
|
249
|
+
if platform.system() == "Windows":
|
|
250
|
+
try:
|
|
251
|
+
# This makes the PATH change permanent for the current user
|
|
252
|
+
subprocess.run(
|
|
253
|
+
f'setx PATH "%PATH%;{directory}"',
|
|
254
|
+
shell=True, check=True, capture_output=True
|
|
255
|
+
)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f" ⚠️ Could not make PATH change permanent: {e}")
|
|
258
|
+
print(" You may need to add it manually.")
|
|
259
|
+
else: # macOS and Linux
|
|
260
|
+
# Suggest adding to shell profile
|
|
261
|
+
profile_file = ""
|
|
262
|
+
shell = os.environ.get("SHELL", "")
|
|
263
|
+
if "bash" in shell: profile_file = "~/.bashrc"
|
|
264
|
+
elif "zsh" in shell: profile_file = "~/.zshrc"
|
|
265
|
+
else: profile_file = "~/.profile"
|
|
266
|
+
print(f" To make this change permanent, add the following to your {profile_file}:")
|
|
267
|
+
print(f' export PATH="{directory}:$PATH"')
|
|
268
|
+
|
|
269
|
+
def verify_gh_installation() -> bool:
|
|
270
|
+
"""Verify gh is properly installed and in PATH"""
|
|
271
|
+
if not shutil.which("gh"):
|
|
272
|
+
return False
|
|
273
|
+
try:
|
|
274
|
+
result = subprocess.run(["gh", "--version"], check=True, capture_output=True, text=True)
|
|
275
|
+
print(f"✅ GitHub CLI successfully installed: {result.stdout.splitlines()[0]}")
|
|
276
|
+
return True
|
|
277
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
def check_and_install_gh() -> bool:
|
|
281
|
+
"""Main function to check and install GitHub CLI, WITH USER PROMPT."""
|
|
282
|
+
if check_gh_installed():
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
# --- ADDED USER PROMPT ---
|
|
286
|
+
print("\n❓ GitHub CLI (gh) is required for this feature but is not installed.", file=sys.stderr)
|
|
287
|
+
try:
|
|
288
|
+
answer = input(" Would you like this tool to attempt an automatic installation? (y/n): ").lower().strip()
|
|
289
|
+
if answer != 'y':
|
|
290
|
+
print("\n❌ Installation cancelled by user. Please install gh manually from https://cli.github.com/")
|
|
291
|
+
return False
|
|
292
|
+
except (EOFError, KeyboardInterrupt):
|
|
293
|
+
print("\n❌ Installation cancelled by user.")
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
if not install_gh_cli():
|
|
297
|
+
print("\n❌ Failed to install GitHub CLI automatically. Please try manual installation:")
|
|
298
|
+
print(" Visit https://github.com/cli/cli#installation for instructions.")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
# After installation, a PATH refresh might be needed
|
|
302
|
+
if not check_gh_installed():
|
|
303
|
+
print("\n‼️ IMPORTANT: Installation completed, but GitHub CLI is not yet available in this terminal session.")
|
|
304
|
+
print(" Please open a NEW terminal and run your command again.")
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# --- Core Tool Functions ---
|
|
311
|
+
|
|
312
|
+
def gh_authenticated():
|
|
313
|
+
"""Check if user is authenticated with GitHub CLI"""
|
|
314
|
+
try:
|
|
315
|
+
result = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True, check=True)
|
|
316
|
+
return "Logged in to github.com" in result.stderr
|
|
317
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def is_local_ahead() -> bool:
|
|
322
|
+
try:
|
|
323
|
+
result = subprocess.run(
|
|
324
|
+
["git", "rev-list", "--left-right", "--count", "origin/main...HEAD"],
|
|
325
|
+
capture_output=True, text=True, check=True
|
|
326
|
+
)
|
|
327
|
+
behind_ahead = result.stdout.strip().split()
|
|
328
|
+
if len(behind_ahead) == 2:
|
|
329
|
+
behind, ahead = map(int, behind_ahead)
|
|
330
|
+
return ahead > 0
|
|
331
|
+
return False
|
|
332
|
+
except subprocess.CalledProcessError:
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def authenticate_with_gh():
|
|
337
|
+
"""Authenticate user with GitHub CLI"""
|
|
338
|
+
print("\n🔑 GitHub authentication required.")
|
|
339
|
+
print("The tool will use the GitHub CLI (gh) to open a browser for secure login.")
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
subprocess.run(["gh", "auth", "login", "--web", "-h", "github.com"], check=True)
|
|
343
|
+
return True
|
|
344
|
+
except subprocess.CalledProcessError:
|
|
345
|
+
print("❌ Authentication failed. Please try running 'gh auth login' manually.", file=sys.stderr)
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
def initialize_git_repository():
|
|
349
|
+
"""Initialize git repository if not already initialized"""
|
|
350
|
+
if os.path.exists(".git"):
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
print("🛠 Initializing git repository")
|
|
354
|
+
try:
|
|
355
|
+
subprocess.run(["git", "init"], check=True, capture_output=True)
|
|
356
|
+
subprocess.run(["git", "branch", "-M", "main"], check=True, capture_output=True)
|
|
357
|
+
|
|
358
|
+
if not os.path.exists(".gitignore"):
|
|
359
|
+
with open(".gitignore", "w") as f:
|
|
360
|
+
f.write("""# Python
|
|
361
|
+
__pycache__/
|
|
362
|
+
*.py[cod]
|
|
363
|
+
*.so
|
|
364
|
+
.Python
|
|
365
|
+
env/
|
|
366
|
+
venv/
|
|
367
|
+
.env
|
|
368
|
+
|
|
369
|
+
# IDE
|
|
370
|
+
.vscode/
|
|
371
|
+
.idea/
|
|
372
|
+
*.swp
|
|
373
|
+
*.swo
|
|
374
|
+
|
|
375
|
+
# System
|
|
376
|
+
.DS_Store
|
|
377
|
+
Thumbs.db
|
|
378
|
+
|
|
379
|
+
# Project specific
|
|
380
|
+
*.log
|
|
381
|
+
*.tmp
|
|
382
|
+
*.bak
|
|
383
|
+
""")
|
|
384
|
+
print("📁 Created .gitignore file")
|
|
385
|
+
return True
|
|
386
|
+
except subprocess.CalledProcessError as e:
|
|
387
|
+
print(f"❌ Failed to initialize Git repository: {e.stderr.decode(errors='ignore').strip()}", file=sys.stderr)
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
def create_initial_commit(commit_message="Initial commit"):
|
|
391
|
+
"""Create initial commit if no commits exist"""
|
|
392
|
+
try:
|
|
393
|
+
result = subprocess.run(["git", "rev-list", "--count", "HEAD"], capture_output=True, text=True)
|
|
394
|
+
commit_count = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
|
|
395
|
+
|
|
396
|
+
if commit_count == 0:
|
|
397
|
+
print("📦 Creating initial commit")
|
|
398
|
+
subprocess.run(["git", "add", "."], check=True)
|
|
399
|
+
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
|
400
|
+
return True
|
|
401
|
+
return False
|
|
402
|
+
except subprocess.CalledProcessError as e:
|
|
403
|
+
error_output = e.stderr.decode(errors='ignore').strip()
|
|
404
|
+
if "nothing to commit" in error_output:
|
|
405
|
+
print(f"❌ Failed to create initial commit: No files found to commit.", file=sys.stderr)
|
|
406
|
+
print("➡️ Add some files to your project directory before creating a repository.", file=sys.stderr)
|
|
407
|
+
else:
|
|
408
|
+
print(f"❌ Failed to create initial commit: {error_output}", file=sys.stderr)
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
def create_with_gh_cli(repo_name, private=False, description="", commit_message="Initial commit"):
|
|
412
|
+
"""Create and push to new repository using GitHub CLI"""
|
|
413
|
+
try:
|
|
414
|
+
if not os.path.exists(".git"):
|
|
415
|
+
if not initialize_git_repository():
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
if not create_initial_commit(commit_message):
|
|
419
|
+
if subprocess.run(["git", "status"], capture_output=True).returncode != 0:
|
|
420
|
+
return False
|
|
421
|
+
print("ℹ️ Using existing commits")
|
|
422
|
+
|
|
423
|
+
private_flag = "--private" if private else "--public"
|
|
424
|
+
cmd = ["gh", "repo", "create", repo_name, private_flag, "--source=.", "--remote=origin", "--push"]
|
|
425
|
+
if description: cmd.extend(["--description", description])
|
|
426
|
+
|
|
427
|
+
print("🚀 Creating repository and pushing code...")
|
|
428
|
+
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
429
|
+
|
|
430
|
+
repo_url = process.stderr.strip()
|
|
431
|
+
print(f"✅ Successfully created repository: {repo_url}")
|
|
432
|
+
return True
|
|
433
|
+
except subprocess.CalledProcessError as e:
|
|
434
|
+
error_message = e.stderr.strip()
|
|
435
|
+
if "already exists" in error_message:
|
|
436
|
+
print(f"❌ Failed to create repository: {error_message}", file=sys.stderr)
|
|
437
|
+
print("➡️ Please choose a different repository name.", file=sys.stderr)
|
|
438
|
+
else:
|
|
439
|
+
print(f"❌ Failed to create repository: {error_message}", file=sys.stderr)
|
|
440
|
+
return False
|
|
441
|
+
except Exception as e:
|
|
442
|
+
print(f"❌ An unexpected error occurred: {str(e)}", file=sys.stderr)
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
def standard_git_push(commit_message, branch, remote, force=False, tags=False):
|
|
446
|
+
"""Handle standard git push operations"""
|
|
447
|
+
try:
|
|
448
|
+
subprocess.run(["git", "add", "."], check=True)
|
|
449
|
+
|
|
450
|
+
if commit_message:
|
|
451
|
+
print(f"📦 Committing with message: '{commit_message}'")
|
|
452
|
+
subprocess.run(["git", "commit", "-m", commit_message, "--allow-empty-message"], check=True)
|
|
453
|
+
else:
|
|
454
|
+
print("ℹ️ No commit message provided. Pushing only staged changes.")
|
|
455
|
+
|
|
456
|
+
if is_local_ahead():
|
|
457
|
+
push_cmd = ["git", "push"]
|
|
458
|
+
else:
|
|
459
|
+
push_cmd = ["git", "push", "origin", "main"]
|
|
460
|
+
|
|
461
|
+
if force:
|
|
462
|
+
push_cmd.append("--force-with-lease")
|
|
463
|
+
print("⚠️ Using safe force push (--force-with-lease).")
|
|
464
|
+
if tags: push_cmd.append("--tags")
|
|
465
|
+
if remote and branch: push_cmd.extend([remote, branch])
|
|
466
|
+
|
|
467
|
+
print(f"🚀 Executing: {' '.join(push_cmd)}")
|
|
468
|
+
subprocess.run(push_cmd, check=True)
|
|
469
|
+
print("✅ Successfully pushed changes.")
|
|
470
|
+
return True
|
|
471
|
+
except subprocess.CalledProcessError as e:
|
|
472
|
+
error_output = e.stderr.decode(errors='ignore').strip() if e.stderr else str(e)
|
|
473
|
+
if "nothing to commit" in error_output:
|
|
474
|
+
print("ℹ️ No changes to commit. Nothing to do.")
|
|
475
|
+
return True
|
|
476
|
+
print(f"❌ Push failed: {error_output}", file=sys.stderr)
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
# --- Main Entry Point ---
|
|
480
|
+
|
|
481
|
+
def run():
|
|
482
|
+
parser = argparse.ArgumentParser(
|
|
483
|
+
description="🚀 Supercharged Git push tool with GitHub repo creation",
|
|
484
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
485
|
+
epilog="""Examples:
|
|
486
|
+
Standard push: gitpush "My new feature"
|
|
487
|
+
Create new repo: gitpush "Initial commit" --new-repo my-awesome-project
|
|
488
|
+
Private repository: gitpush "Initial commit" --new-repo my-secret-project --private
|
|
489
|
+
Force push (safe): gitpush "Rebased feature" --force
|
|
490
|
+
Initialize only: gitpush --init
|
|
491
|
+
"""
|
|
492
|
+
)
|
|
493
|
+
parser.add_argument("commit", nargs="?", help="Commit message (optional if just pushing staged changes).")
|
|
494
|
+
parser.add_argument("branch", nargs="?", default=None, help="Branch name (defaults to current branch).")
|
|
495
|
+
parser.add_argument("remote", nargs="?", default="origin", help="Remote name (default: origin).")
|
|
496
|
+
parser.add_argument("--force", action="store_true", help="Force push with --force-with-lease.")
|
|
497
|
+
parser.add_argument("--tags", action="store_true", help="Push all tags.")
|
|
498
|
+
parser.add_argument("--init", action="store_true", help="Initialize a new Git repository and exit.")
|
|
499
|
+
parser.add_argument("--new-repo", metavar="REPO_NAME", help="Create a new GitHub repository with the given name.")
|
|
500
|
+
parser.add_argument("--private", action="store_true", help="Make the new repository private.")
|
|
501
|
+
parser.add_argument("--description", help="Description for the new repository.")
|
|
502
|
+
|
|
503
|
+
args = parser.parse_args()
|
|
504
|
+
|
|
505
|
+
target_branch = args.branch
|
|
506
|
+
if not target_branch:
|
|
507
|
+
try:
|
|
508
|
+
branch_result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=True)
|
|
509
|
+
target_branch = branch_result.stdout.strip()
|
|
510
|
+
except subprocess.CalledProcessError:
|
|
511
|
+
target_branch = "main"
|
|
512
|
+
|
|
513
|
+
if args.new_repo:
|
|
514
|
+
# *** THIS IS THE MAIN FIX: Calling your orchestrator function ***
|
|
515
|
+
if not check_and_install_gh():
|
|
516
|
+
sys.exit(1)
|
|
517
|
+
|
|
518
|
+
if not gh_authenticated():
|
|
519
|
+
if not authenticate_with_gh():
|
|
520
|
+
sys.exit(1)
|
|
521
|
+
|
|
522
|
+
if not create_with_gh_cli(
|
|
523
|
+
args.new_repo,
|
|
524
|
+
private=args.private,
|
|
525
|
+
description=args.description or "",
|
|
526
|
+
commit_message=args.commit or "Initial commit"
|
|
527
|
+
):
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
|
|
530
|
+
elif args.init:
|
|
531
|
+
if initialize_git_repository():
|
|
532
|
+
print("✅ Git repository initialized successfully.")
|
|
533
|
+
|
|
534
|
+
else:
|
|
535
|
+
if not standard_git_push(
|
|
536
|
+
args.commit,
|
|
537
|
+
target_branch,
|
|
538
|
+
args.remote,
|
|
539
|
+
args.force,
|
|
540
|
+
args.tags
|
|
541
|
+
):
|
|
542
|
+
sys.exit(1)
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
run()
|