pygitgo 1.8.2__tar.gz → 1.9.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.
- {pygitgo-1.8.2 → pygitgo-1.9.0}/PKG-INFO +6 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/README.md +5 -1
- {pygitgo-1.8.2 → pygitgo-1.9.0}/pyproject.toml +2 -1
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/account.py +7 -3
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/manager.py +24 -17
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/ssh_utils.py +36 -15
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/git_branch.py +16 -3
- pygitgo-1.9.0/src/pygitgo/commands/git_core.py +161 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/git_remote.py +11 -3
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/init.py +4 -3
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/jump.py +53 -43
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/link.py +26 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/pull.py +5 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/push.py +9 -5
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/repo.py +17 -1
- pygitgo-1.9.0/src/pygitgo/commands/resolve.py +54 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/state.py +5 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/undo.py +125 -117
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/main.py +17 -3
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/executor.py +16 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/PKG-INFO +6 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/SOURCES.txt +2 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_executor.py +2 -2
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_git_core.py +25 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_jump.py +31 -61
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_link.py +21 -6
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_push.py +5 -1
- pygitgo-1.9.0/tests/test_resolve.py +105 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_ssh_utils.py +4 -4
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_undo.py +10 -3
- pygitgo-1.8.2/src/pygitgo/commands/git_core.py +0 -85
- {pygitgo-1.8.2 → pygitgo-1.9.0}/LICENSE +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/setup.cfg +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/__init__.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/__init__.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/__init__.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/config.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/new.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/staging.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/stash.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/user.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/exceptions.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/__init__.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/bootstrap.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/cli_io.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/colors.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/config.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/platform.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/update_checker.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/validators.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/dependency_links.txt +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/entry_points.txt +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/requires.txt +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/top_level.txt +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_account.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_bootstrap.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_cli_io.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_config_command.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_config_utils.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_git_branch.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_git_remote.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_init.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_integration_git.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_main.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_manager.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_new.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_platform.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_pull.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_repo.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_staging.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_stash.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_state.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_update_checker.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_user.py +0 -0
- {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygitgo
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: GitGo CLI - Your Fast Git Companion. Simplifies git push, link, stash, and user management.
|
|
5
5
|
Author: Huerte
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -76,6 +76,8 @@ gitgo new my-app python
|
|
|
76
76
|
- [License](#license)
|
|
77
77
|
- [Changelog](CHANGELOG.md)
|
|
78
78
|
- [Contributing Guide](CONTRIBUTING.md)
|
|
79
|
+
- [First Contribution Walkthrough](docs/first-contribution.md)
|
|
80
|
+
- [Troubleshooting](docs/troubleshooting.md)
|
|
79
81
|
|
|
80
82
|
---
|
|
81
83
|
|
|
@@ -388,7 +390,7 @@ gitgo config get <key>
|
|
|
388
390
|
| Key | Description | Default |
|
|
389
391
|
|-----|-------------|---------|
|
|
390
392
|
| `default-branch` | The branch used for push/link | `main` |
|
|
391
|
-
| `default-message` | The commit message used for push | `
|
|
393
|
+
| `default-message` | The commit message used for push | `chore: new changes applied` |
|
|
392
394
|
|
|
393
395
|
### Global Flags
|
|
394
396
|
|
|
@@ -415,6 +417,8 @@ gitgo -r # verify GitGo is ready
|
|
|
415
417
|
|
|
416
418
|
Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide, including project structure, test instructions, commit conventions, and a [Good First Issues](CONTRIBUTING.md#good-first-issues) table if you're not sure where to start.
|
|
417
419
|
|
|
420
|
+
If this is your first time contributing to an open-source project, follow our step-by-step [First Contribution Walkthrough](docs/first-contribution.md) guide.
|
|
421
|
+
|
|
418
422
|
---
|
|
419
423
|
|
|
420
424
|
## Credits
|
|
@@ -51,6 +51,8 @@ gitgo new my-app python
|
|
|
51
51
|
- [License](#license)
|
|
52
52
|
- [Changelog](CHANGELOG.md)
|
|
53
53
|
- [Contributing Guide](CONTRIBUTING.md)
|
|
54
|
+
- [First Contribution Walkthrough](docs/first-contribution.md)
|
|
55
|
+
- [Troubleshooting](docs/troubleshooting.md)
|
|
54
56
|
|
|
55
57
|
---
|
|
56
58
|
|
|
@@ -363,7 +365,7 @@ gitgo config get <key>
|
|
|
363
365
|
| Key | Description | Default |
|
|
364
366
|
|-----|-------------|---------|
|
|
365
367
|
| `default-branch` | The branch used for push/link | `main` |
|
|
366
|
-
| `default-message` | The commit message used for push | `
|
|
368
|
+
| `default-message` | The commit message used for push | `chore: new changes applied` |
|
|
367
369
|
|
|
368
370
|
### Global Flags
|
|
369
371
|
|
|
@@ -390,6 +392,8 @@ gitgo -r # verify GitGo is ready
|
|
|
390
392
|
|
|
391
393
|
Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide, including project structure, test instructions, commit conventions, and a [Good First Issues](CONTRIBUTING.md#good-first-issues) table if you're not sure where to start.
|
|
392
394
|
|
|
395
|
+
If this is your first time contributing to an open-source project, follow our step-by-step [First Contribution Walkthrough](docs/first-contribution.md) guide.
|
|
396
|
+
|
|
393
397
|
---
|
|
394
398
|
|
|
395
399
|
## Credits
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pygitgo"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.9.0"
|
|
8
8
|
description = "GitGo CLI - Your Fast Git Companion. Simplifies git push, link, stash, and user management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "GPL-3.0-or-later"}
|
|
@@ -45,3 +45,4 @@ where = ["src"]
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
46
|
testpaths = ["tests"]
|
|
47
47
|
addopts = "--cov=pygitgo --cov-report=term-missing"
|
|
48
|
+
pythonpath = ["src"]
|
|
@@ -41,9 +41,13 @@ def ensure_user_configure(default_email=None, default_username=None):
|
|
|
41
41
|
set_user(default_username, default_email)
|
|
42
42
|
return True
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
print()
|
|
45
|
+
print(" Git Identity Setup")
|
|
46
|
+
print(" " + "-" * 36)
|
|
47
|
+
warning("Git user identity is not configured.")
|
|
48
|
+
info("Needed so your commits are attributed to the right account.")
|
|
49
|
+
print()
|
|
50
|
+
|
|
47
51
|
if default_username:
|
|
48
52
|
new_username = default_username
|
|
49
53
|
info(f"Using GitHub username: {new_username}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pygitgo.utils.cli_io import info, success, warning, error
|
|
2
2
|
from pygitgo.utils.platform import get_platform
|
|
3
3
|
from pygitgo.utils.executor import run_command
|
|
4
|
-
from pygitgo.exceptions import GitCommandError
|
|
4
|
+
from pygitgo.exceptions import GitCommandError, GitGoError
|
|
5
5
|
from pygitgo.utils.platform import open_url
|
|
6
6
|
from . import ssh_utils
|
|
7
7
|
import os
|
|
@@ -44,7 +44,12 @@ def login():
|
|
|
44
44
|
else:
|
|
45
45
|
error("Please enter a valid email address.")
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
try:
|
|
48
|
+
key_path = ssh_utils.generate_ssh_key(email=email)
|
|
49
|
+
except GitGoError as e:
|
|
50
|
+
error(str(e))
|
|
51
|
+
return False
|
|
52
|
+
|
|
48
53
|
pub_key_path = str(key_path) + ".pub"
|
|
49
54
|
|
|
50
55
|
with open(pub_key_path, "r") as f:
|
|
@@ -82,21 +87,23 @@ def login():
|
|
|
82
87
|
github_username = ssh_utils.get_github_username()
|
|
83
88
|
ensure_user_configure(default_email=email, default_username=github_username)
|
|
84
89
|
return True
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
info("
|
|
94
|
-
info("
|
|
95
|
-
|
|
96
|
-
info("
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
|
|
91
|
+
raw_output, timed_out, os_error = ssh_utils._get_cached_ssh_response()
|
|
92
|
+
cause = ssh_utils.classify_connection_error(raw_output, timed_out, os_error)
|
|
93
|
+
|
|
94
|
+
error("Login failed. GitHub did not accept the SSH key.")
|
|
95
|
+
info(f"Reason: {cause}")
|
|
96
|
+
|
|
97
|
+
if get_platform() == "windows" and not timed_out:
|
|
98
|
+
info("")
|
|
99
|
+
info("If the key was added correctly but still fails, the SSH agent may not be running.")
|
|
100
|
+
info("Fix (run PowerShell as Administrator):")
|
|
101
|
+
info(" Set-Service ssh-agent -StartupType Automatic")
|
|
102
|
+
info(" Start-Service ssh-agent")
|
|
103
|
+
info("Then run 'gitgo user login' again.")
|
|
104
|
+
|
|
105
|
+
info("")
|
|
106
|
+
info("Full guide: https://github.com/Huerte/GitGo/blob/main/docs/login-guide.md")
|
|
100
107
|
return False
|
|
101
108
|
|
|
102
109
|
|
|
@@ -48,9 +48,12 @@ def _get_github_ssh_response():
|
|
|
48
48
|
capture_output=True, text=True,
|
|
49
49
|
timeout=SSH_TIMEOUT_SECONDS, stdin=subprocess.DEVNULL,
|
|
50
50
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
combined = (result.stderr or "") + (result.stdout or "")
|
|
52
|
+
return combined, False, None
|
|
53
|
+
except subprocess.TimeoutExpired:
|
|
54
|
+
return "", True, None
|
|
55
|
+
except OSError as e:
|
|
56
|
+
return "", False, str(e)
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
def _get_cached_ssh_response():
|
|
@@ -58,7 +61,7 @@ def _get_cached_ssh_response():
|
|
|
58
61
|
if not _cache_populated:
|
|
59
62
|
_cached_ssh_response = _get_github_ssh_response()
|
|
60
63
|
_cache_populated = True
|
|
61
|
-
return _cached_ssh_response
|
|
64
|
+
return _cached_ssh_response # (raw_output, timed_out, os_error)
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
def clear_ssh_cache():
|
|
@@ -66,39 +69,57 @@ def clear_ssh_cache():
|
|
|
66
69
|
_cached_ssh_response = None
|
|
67
70
|
_cache_populated = False
|
|
68
71
|
|
|
72
|
+
from typing import Optional
|
|
73
|
+
def classify_connection_error(raw_output: str, timed_out: bool, os_error: Optional[str]) -> str:
|
|
74
|
+
"""Return a specific, human-readable cause string based on the SSH response."""
|
|
75
|
+
if timed_out:
|
|
76
|
+
return "Connection timed out. GitHub SSH port (22) may be blocked by your network or firewall."
|
|
77
|
+
if os_error:
|
|
78
|
+
return f"Could not launch the SSH client: {os_error}"
|
|
79
|
+
if not raw_output:
|
|
80
|
+
return "No response from GitHub. Check your internet connection."
|
|
81
|
+
if "Permission denied" in raw_output:
|
|
82
|
+
return "Permission denied. The SSH key has not been added to your GitHub account, or it was added incorrectly."
|
|
83
|
+
if "Connection refused" in raw_output:
|
|
84
|
+
return "Connection refused on port 22. Try a network without strict firewall rules."
|
|
85
|
+
if "Host key verification failed" in raw_output:
|
|
86
|
+
return "Host key verification failed. Run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts"
|
|
87
|
+
if "Could not resolve hostname" in raw_output:
|
|
88
|
+
return "DNS lookup failed. You may be offline or behind a restrictive proxy."
|
|
89
|
+
# Return the raw SSH message so users see the actual problem.
|
|
90
|
+
return raw_output.strip() or "Unknown SSH error."
|
|
91
|
+
|
|
69
92
|
|
|
70
93
|
def check_connection(ok_text=None, fail_text=None):
|
|
71
94
|
from yaspin import yaspin
|
|
72
95
|
import sys
|
|
73
|
-
|
|
96
|
+
|
|
74
97
|
ensure_github_known_host()
|
|
75
98
|
|
|
76
|
-
kwargs = {"text":
|
|
99
|
+
kwargs = {"text": "Verifying GitHub connection..."}
|
|
77
100
|
if sys.stdout.isatty():
|
|
78
101
|
kwargs["color"] = "cyan"
|
|
79
102
|
spinner = yaspin(**kwargs)
|
|
80
103
|
spinner.start()
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
connected =
|
|
105
|
+
raw_output, timed_out, os_error = _get_cached_ssh_response()
|
|
106
|
+
connected = not timed_out and not os_error and "successfully authenticated" in raw_output
|
|
84
107
|
|
|
85
108
|
if connected:
|
|
86
|
-
|
|
87
|
-
spinner.text = ok_text
|
|
109
|
+
spinner.text = ok_text or "GitHub connection verified."
|
|
88
110
|
spinner.ok("✔")
|
|
89
111
|
else:
|
|
90
|
-
|
|
91
|
-
spinner.text = fail_text
|
|
112
|
+
spinner.text = fail_text or "Could not connect to GitHub."
|
|
92
113
|
spinner.fail("✖")
|
|
93
114
|
|
|
94
115
|
return connected
|
|
95
116
|
|
|
96
117
|
|
|
97
118
|
def get_github_username():
|
|
98
|
-
|
|
99
|
-
if
|
|
119
|
+
raw_output, _timed_out, _os_error = _get_cached_ssh_response()
|
|
120
|
+
if raw_output and "Hi " in raw_output and "!" in raw_output:
|
|
100
121
|
try:
|
|
101
|
-
return
|
|
122
|
+
return raw_output.split("Hi ")[1].split("!")[0]
|
|
102
123
|
except (IndexError, ValueError):
|
|
103
124
|
pass
|
|
104
125
|
return None
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
from pygitgo.utils.cli_io import
|
|
1
|
+
from pygitgo.utils.cli_io import warning, error, info, confirm
|
|
2
2
|
from pygitgo.exceptions import GitCommandError, GitGoError
|
|
3
3
|
from pygitgo.utils.executor import run_command
|
|
4
4
|
from pygitgo.utils.config import get_config
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def get_current_branch():
|
|
8
|
+
def get_current_branch(safe=False):
|
|
9
9
|
branch = run_command(["git", "branch", "--show-current"]).strip()
|
|
10
10
|
if not branch:
|
|
11
|
-
|
|
11
|
+
# We are in a detached HEAD state.
|
|
12
|
+
commit_hash = run_command(["git", "rev-parse", "--short", "HEAD"]).strip()
|
|
13
|
+
|
|
14
|
+
if safe:
|
|
15
|
+
warning("You are in a 'detached HEAD' state (not on any branch).")
|
|
16
|
+
warning("If you switch away now, your current commits could be lost.")
|
|
17
|
+
if confirm("Would you like to create a new branch here to save your work? (y/n): "):
|
|
18
|
+
new_branch = input("Enter a name for the new branch: ").strip()
|
|
19
|
+
if new_branch:
|
|
20
|
+
run_command(["git", "checkout", "-b", new_branch])
|
|
21
|
+
return new_branch
|
|
22
|
+
raise GitGoError("Operation aborted to prevent data loss in detached HEAD state.")
|
|
23
|
+
|
|
24
|
+
return commit_hash
|
|
12
25
|
return branch
|
|
13
26
|
|
|
14
27
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from pygitgo.auth.ssh_utils import convert_https_to_ssh, get_ssh_key_path, is_ssh_url, check_connection
|
|
2
|
+
from pygitgo.exceptions import GitGoError, GitCommandError
|
|
3
|
+
from pygitgo.auth.account import sanitize_signing_config
|
|
4
|
+
from pygitgo.commands.git_remote import handle_rebase
|
|
5
|
+
from pygitgo.utils.config import get_default_branch
|
|
6
|
+
from pygitgo.utils.executor import run_command
|
|
7
|
+
from pygitgo.utils.cli_io import info, warning
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_signing_flags():
|
|
13
|
+
key_path = get_ssh_key_path()
|
|
14
|
+
if not key_path.exists():
|
|
15
|
+
return []
|
|
16
|
+
return [
|
|
17
|
+
"-c", "gpg.format=ssh",
|
|
18
|
+
"-c", f"user.signingkey={key_path}",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def git_commit(commit_message, loading_msg="Committing changes...", skip_staging=False, ok_text=None):
|
|
23
|
+
if not ok_text:
|
|
24
|
+
ok_text = "Changes committed."
|
|
25
|
+
try:
|
|
26
|
+
status_result = run_command(["git", "status", "--porcelain"])
|
|
27
|
+
if not status_result.strip():
|
|
28
|
+
return False
|
|
29
|
+
except GitCommandError as e:
|
|
30
|
+
stderr = getattr(e, "stderr", str(e))
|
|
31
|
+
if "not a git repository" in stderr.lower():
|
|
32
|
+
raise GitGoError("Not inside a git repository. Run 'gitgo init' or 'gitgo link' first.")
|
|
33
|
+
raise GitGoError(f"Could not check repository status: {stderr}")
|
|
34
|
+
|
|
35
|
+
sanitize_signing_config()
|
|
36
|
+
|
|
37
|
+
if not skip_staging:
|
|
38
|
+
run_command(["git", "add", "."], loading_msg="Staging files...", ok_text="Files staged.")
|
|
39
|
+
|
|
40
|
+
clean_message = commit_message.strip('"\'')
|
|
41
|
+
|
|
42
|
+
signing_flags = _get_signing_flags()
|
|
43
|
+
commit_command = ["git"] + signing_flags + ["commit", "-S", "-m", clean_message]
|
|
44
|
+
run_command(commit_command, loading_msg=loading_msg, ok_text=ok_text)
|
|
45
|
+
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def git_init(ok_text=None):
|
|
50
|
+
if os.path.isdir(".git"):
|
|
51
|
+
warning("Already a git repository! Skipping init...")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
default_main_branch = get_default_branch()
|
|
55
|
+
|
|
56
|
+
if not ok_text:
|
|
57
|
+
ok_text = "Git repository initialized."
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
run_command(["git", "init", "-b", default_main_branch], loading_msg="Initializing git repository...", ok_text=ok_text)
|
|
61
|
+
except GitCommandError:
|
|
62
|
+
run_command(["git", "init"], loading_msg="Initializing git repository...")
|
|
63
|
+
run_command(["git", "checkout", "-b", default_main_branch], ok_text=ok_text)
|
|
64
|
+
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def git_push(branch, ok_text=None):
|
|
69
|
+
if not ok_text:
|
|
70
|
+
ok_text = f"Pushed to remote branch '{branch}'."
|
|
71
|
+
try:
|
|
72
|
+
remote_url = run_command(["git", "remote", "get-url", "origin"]).strip()
|
|
73
|
+
except GitCommandError:
|
|
74
|
+
remote_url = None
|
|
75
|
+
|
|
76
|
+
if remote_url and not is_ssh_url(remote_url) and check_connection():
|
|
77
|
+
ssh_url = convert_https_to_ssh(remote_url)
|
|
78
|
+
if ssh_url:
|
|
79
|
+
run_command(["git", "remote", "set-url", "origin", ssh_url], loading_msg="Converting remote from HTTPS to SSH for secure push...", ok_text=f"Remote updated to: {ssh_url}")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
run_command(["git", "push", "-u", "origin", branch], loading_msg=f"Pushing to remote branch '{branch}'...", ok_text=ok_text, err_text="Push failed: verify your remote URL and SSH key, then try again.")
|
|
83
|
+
except (GitCommandError, OSError) as e:
|
|
84
|
+
stderr = getattr(e, "stderr", str(e))
|
|
85
|
+
if "rebase in progress" in stderr.lower():
|
|
86
|
+
handle_rebase()
|
|
87
|
+
elif "non-fast-forward" in stderr.lower() or "rejected" in stderr.lower():
|
|
88
|
+
info("The remote has commits you don't have locally.")
|
|
89
|
+
info("Run 'gitgo pull' first, then push again.")
|
|
90
|
+
raise GitGoError("Push rejected: pull the latest changes first.")
|
|
91
|
+
elif "repository not found" in stderr.lower() or "does not exist" in stderr.lower():
|
|
92
|
+
info("Run: git remote -v to verify the remote URL.")
|
|
93
|
+
raise GitGoError("Push failed: remote repository not found.")
|
|
94
|
+
elif "permission denied" in stderr.lower():
|
|
95
|
+
info("Check that your SSH key is added to GitHub.")
|
|
96
|
+
info("Run: gitgo user login to re-authenticate.")
|
|
97
|
+
raise GitGoError("Push failed: permission denied.")
|
|
98
|
+
else:
|
|
99
|
+
info("Run: git remote -v to inspect your current remote.")
|
|
100
|
+
raise GitGoError(f"Push failed: {stderr}" if stderr else "Push failed — check the output above.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def abort_pull_conflict():
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
from pygitgo.utils.cli_io import warning, info, confirm
|
|
106
|
+
from pygitgo.exceptions import GitCommandError, GitGoError
|
|
107
|
+
from pygitgo.utils.executor import run_command
|
|
108
|
+
from pygitgo.commands.git_branch import get_current_branch
|
|
109
|
+
|
|
110
|
+
rebase_in_progress = Path(".git/rebase-merge").exists() or Path(".git/rebase-apply").exists()
|
|
111
|
+
|
|
112
|
+
if rebase_in_progress:
|
|
113
|
+
warning("A sync or jump hit a merge conflict and is paused.")
|
|
114
|
+
if not confirm("Cancel the sync and discard any conflict fixes? (y/n): ", destructive=True):
|
|
115
|
+
info("Canceled. The conflict is still active.")
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
run_command(["git", "rebase", "--abort"], loading_msg="Aborting sync...", ok_text="Sync aborted. Branch is back to how it was before the conflict.")
|
|
120
|
+
return True
|
|
121
|
+
except GitCommandError as e:
|
|
122
|
+
raise GitGoError(f"Abort failed: {getattr(e, 'stderr', str(e))}")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
run_command(["git", "rev-parse", "ORIG_HEAD"])
|
|
126
|
+
except GitCommandError:
|
|
127
|
+
raise GitGoError(
|
|
128
|
+
"No pull to undo. ORIG_HEAD not found — this means no pull has been run yet."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
branch = get_current_branch(safe=True)
|
|
133
|
+
except GitCommandError as e:
|
|
134
|
+
raise GitGoError(f"Could not determine the current branch: {e}")
|
|
135
|
+
|
|
136
|
+
warning("This will reset your branch to the state before the last pull.")
|
|
137
|
+
warning("Any commits that arrived in the pull will be removed locally.")
|
|
138
|
+
if not confirm("Undo the last pull? Type 'y' to confirm: ", destructive=True):
|
|
139
|
+
info("Canceled. Branch is unchanged.")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
run_command(
|
|
144
|
+
["git", "reset", "--hard", "ORIG_HEAD"],
|
|
145
|
+
loading_msg="Reverting to pre-pull state...",
|
|
146
|
+
ok_text="Branch reset to its state before the last pull."
|
|
147
|
+
)
|
|
148
|
+
except GitCommandError as e:
|
|
149
|
+
raise GitGoError(f"Undo failed: {getattr(e, 'stderr', str(e))}")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
behind = run_command(["git", "rev-list", "--count", f"HEAD..origin/{branch}"]).strip()
|
|
153
|
+
if behind and int(behind) > 0:
|
|
154
|
+
warning(f"Your local branch is now {behind} commit(s) behind remote '{branch}'.")
|
|
155
|
+
info("The remote still has the pulled commits. Your local copy has been reset.")
|
|
156
|
+
info(f"To push your undo to the remote: git push --force origin {branch}")
|
|
157
|
+
info("Only do this if you are the only person working on this branch.")
|
|
158
|
+
except (GitCommandError, ValueError):
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
return True
|
|
@@ -20,9 +20,17 @@ def confirm_remote_link(ok_text=None):
|
|
|
20
20
|
try:
|
|
21
21
|
run_command(["git", "ls-remote", "origin"], loading_msg="Testing connection to remote...", ok_text=ok_text)
|
|
22
22
|
return True
|
|
23
|
-
except GitCommandError:
|
|
23
|
+
except GitCommandError as e:
|
|
24
|
+
stderr = getattr(e, "stderr", str(e))
|
|
24
25
|
info("Run: git remote -v to inspect your current remote.")
|
|
25
|
-
|
|
26
|
+
if "could not resolve" in stderr.lower():
|
|
27
|
+
raise GitGoError("Connection failed: DNS lookup failed for the remote host. Check your internet.")
|
|
28
|
+
elif "permission denied" in stderr.lower():
|
|
29
|
+
raise GitGoError("Connection failed: SSH key not accepted. Run 'gitgo user login' to re-authenticate.")
|
|
30
|
+
elif stderr:
|
|
31
|
+
raise GitGoError(f"Connection failed: {stderr}")
|
|
32
|
+
else:
|
|
33
|
+
raise GitGoError("Connection failed — verify the URL and your SSH key.")
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
def check_and_sync_branch(branch):
|
|
@@ -56,6 +64,6 @@ def handle_rebase():
|
|
|
56
64
|
warning("Conflict detected during rebase.")
|
|
57
65
|
info("Resolve conflicts manually, then run:")
|
|
58
66
|
info(" git add <files>")
|
|
59
|
-
info("
|
|
67
|
+
info(" gitgo resolve")
|
|
60
68
|
info("When finished, run 'gitgo push <branch> <message>' again.")
|
|
61
69
|
raise GitGoError("Push aborted — rebase conflict in progress.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pygitgo.commands.git_core import git_init
|
|
2
2
|
from pygitgo.exceptions import GitGoError
|
|
3
|
-
from pygitgo.utils.cli_io import info
|
|
3
|
+
from pygitgo.utils.cli_io import info, warning
|
|
4
4
|
import urllib.request
|
|
5
5
|
import urllib.error
|
|
6
6
|
import zipfile
|
|
@@ -320,8 +320,9 @@ def init_operation(args, standalone=False):
|
|
|
320
320
|
if os.path.exists(target_dir) and not os.listdir(target_dir):
|
|
321
321
|
try:
|
|
322
322
|
os.rmdir(target_dir)
|
|
323
|
-
except Exception:
|
|
324
|
-
|
|
323
|
+
except Exception as cleanup_err:
|
|
324
|
+
warning(f"Could not remove empty folder '{target_dir}': {cleanup_err}")
|
|
325
|
+
warning(f"Delete it manually before running this command again.")
|
|
325
326
|
raise e
|
|
326
327
|
finally:
|
|
327
328
|
os.chdir(orig_cwd)
|
|
@@ -59,92 +59,101 @@ def _jump_interrupt_cleanup(original_branch, stashed_code, created_branch):
|
|
|
59
59
|
|
|
60
60
|
def jump_operation(args):
|
|
61
61
|
target_branch = args.branch
|
|
62
|
-
original_branch = get_current_branch()
|
|
62
|
+
original_branch = get_current_branch(safe=True)
|
|
63
63
|
|
|
64
64
|
if original_branch == target_branch:
|
|
65
65
|
warning(f"Already on branch '{target_branch}'.")
|
|
66
66
|
return
|
|
67
67
|
|
|
68
68
|
try:
|
|
69
|
-
has_changes = run_command([
|
|
70
|
-
except GitCommandError:
|
|
71
|
-
|
|
69
|
+
has_changes = run_command(["git", "status", "--porcelain"], loading_msg="Checking for local changes...")
|
|
70
|
+
except GitCommandError as e:
|
|
71
|
+
stderr = getattr(e, "stderr", str(e))
|
|
72
|
+
if "not a git repository" in stderr.lower():
|
|
73
|
+
raise GitGoError("Not inside a git repository. Run 'gitgo init' or 'gitgo link' first.")
|
|
74
|
+
raise GitGoError(f"Could not check repository status: {stderr}")
|
|
72
75
|
|
|
73
76
|
stashed_code = False
|
|
74
77
|
created_branch = None
|
|
75
78
|
|
|
76
79
|
try:
|
|
77
80
|
if has_changes.strip():
|
|
78
|
-
|
|
79
|
-
info("You have unsaved changes in this folder.")
|
|
80
|
-
if not confirm(f"Move your unsaved edits to branch '{target_branch}'? (y/n): "):
|
|
81
|
-
print()
|
|
82
|
-
warning("You cannot switch branches with unsaved edits. Jump canceled.")
|
|
83
|
-
return
|
|
84
|
-
stash_result = git_stash_push(label="GitGo Jump Auto-Stash", loading_msg="Saving your changes before jumping...")
|
|
81
|
+
stash_result = git_stash_push(label="GitGo Jump Auto-Stash", loading_msg="Auto-saving local changes before switching...")
|
|
85
82
|
if not stash_result:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
lock_file = Path(".git/index.lock")
|
|
85
|
+
if lock_file.exists():
|
|
86
|
+
warning("A stale lock file is blocking git.")
|
|
87
|
+
info(f"Delete this file and try again: {lock_file.absolute()}")
|
|
88
|
+
elif Path(".git/rebase-merge").exists() or Path(".git/rebase-apply").exists():
|
|
89
|
+
warning("A rebase is in progress. Finish or abort it first.")
|
|
90
|
+
info("Abort with: gitgo resolve --abort")
|
|
91
|
+
else:
|
|
92
|
+
warning("Could not auto-save changes before switching.")
|
|
93
|
+
info("Run: git status to see what is blocking the stash.")
|
|
94
|
+
raise GitGoError("Jump aborted: could not save local changes.")
|
|
90
95
|
stashed_code = True
|
|
91
96
|
|
|
92
97
|
if not is_branch_exist(target_branch):
|
|
93
98
|
print()
|
|
94
99
|
warning(f"Branch '{target_branch}' does not exist.")
|
|
95
|
-
if not confirm(f"
|
|
96
|
-
info("
|
|
100
|
+
if not confirm(f"Create '{target_branch}' and switch to it? (y/n): "):
|
|
101
|
+
info("Jump canceled.")
|
|
97
102
|
if stashed_code:
|
|
98
|
-
pop_result = git_stash_pop(loading_msg="
|
|
103
|
+
pop_result = git_stash_pop(loading_msg="Restoring your local changes...")
|
|
99
104
|
if not pop_result:
|
|
100
|
-
warning("Could not restore
|
|
105
|
+
warning("Could not restore changes automatically. Run 'gitgo state list' to recover them.")
|
|
101
106
|
return
|
|
102
107
|
|
|
103
108
|
ok_text = f"Branch '{target_branch}' created." if stashed_code else f"On '{target_branch}'."
|
|
104
109
|
git_new_branch(target_branch, ok_text=ok_text)
|
|
105
110
|
created_branch = target_branch
|
|
106
111
|
else:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
run_command(
|
|
113
|
+
["git", "checkout", target_branch],
|
|
114
|
+
loading_msg=f"Switching to '{target_branch}'...",
|
|
115
|
+
ok_text=f"Switched to '{target_branch}'."
|
|
116
|
+
)
|
|
117
|
+
main_branch = get_main_branch()
|
|
118
|
+
try:
|
|
119
|
+
run_command(
|
|
120
|
+
["git", "pull", "--rebase", "--autostash", "origin", main_branch],
|
|
121
|
+
loading_msg=f"Syncing '{target_branch}' with latest from '{main_branch}'...",
|
|
122
|
+
ok_text=f"'{target_branch}' is up to date with '{main_branch}'."
|
|
123
|
+
)
|
|
124
|
+
except GitCommandError as e:
|
|
125
|
+
stderr = getattr(e, "stderr", str(e))
|
|
126
|
+
if "conflict" in stderr.lower() or "rebase in progress" in stderr.lower():
|
|
127
|
+
warning(f"Sync from '{main_branch}' hit a conflict.")
|
|
128
|
+
info("Fix the conflict files, then run: gitgo resolve")
|
|
129
|
+
info("Or cancel the jump with: gitgo resolve --abort")
|
|
130
|
+
else:
|
|
131
|
+
warning(f"Could not sync from '{main_branch}': no remote or no internet.")
|
|
132
|
+
info(f"On '{target_branch}', but not yet synced with '{main_branch}'.")
|
|
123
133
|
|
|
124
134
|
if stashed_code:
|
|
125
|
-
apply_result = git_stash_apply(loading_msg="
|
|
135
|
+
apply_result = git_stash_apply(loading_msg="Restoring your local changes...")
|
|
126
136
|
if not apply_result:
|
|
127
137
|
print()
|
|
128
|
-
error("
|
|
138
|
+
error("CONFLICT: Your local changes clash with the target branch.")
|
|
129
139
|
print()
|
|
130
140
|
info("Option [Y]: Stay here and fix the conflict lines in your files.")
|
|
131
141
|
info("Option [N]: Undo the switch and go back to where you started.")
|
|
132
142
|
print()
|
|
133
|
-
if not confirm("Fix the conflicts yourself? (y = stay
|
|
143
|
+
if not confirm("Fix the conflicts yourself? (y = stay / n = go back): "):
|
|
134
144
|
undo_jump_operation(original_branch, stashed_code, created_branch)
|
|
135
145
|
return
|
|
136
146
|
else:
|
|
137
147
|
print()
|
|
138
|
-
success(f"On '{target_branch}'.
|
|
139
|
-
warning("
|
|
140
|
-
info("Your stash backup is still saved. Run 'gitgo state list' to see it.")
|
|
148
|
+
success(f"On '{target_branch}'. Fix the conflict markers in your files.")
|
|
149
|
+
warning("Your stash backup is still saved. Run 'gitgo state list' to see it.")
|
|
141
150
|
return
|
|
142
151
|
else:
|
|
143
|
-
drop_result = git_stash_drop(loading_msg="Cleaning up
|
|
152
|
+
drop_result = git_stash_drop(loading_msg="Cleaning up temporary stash...", ok_text=f"On '{target_branch}'. Your changes came with you.")
|
|
144
153
|
if not drop_result:
|
|
145
154
|
warning("Could not clean up the temporary stash. Run 'gitgo state list' to remove it manually.")
|
|
146
155
|
|
|
147
|
-
if not getattr(args,
|
|
156
|
+
if not getattr(args, "nested", False):
|
|
148
157
|
banner("WORKSPACE RE-POSITIONED. TARGET DEPLOYMENT SECURED.", "ON TARGET BRANCH WITH RE-APPLIED STATE.")
|
|
149
158
|
return
|
|
150
159
|
|
|
@@ -153,3 +162,4 @@ def jump_operation(args):
|
|
|
153
162
|
warning("Jump interrupted (Ctrl+C).")
|
|
154
163
|
_jump_interrupt_cleanup(original_branch, stashed_code, created_branch)
|
|
155
164
|
sys.exit(130)
|
|
165
|
+
|