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.
Files changed (75) hide show
  1. {pygitgo-1.8.2 → pygitgo-1.9.0}/PKG-INFO +6 -2
  2. {pygitgo-1.8.2 → pygitgo-1.9.0}/README.md +5 -1
  3. {pygitgo-1.8.2 → pygitgo-1.9.0}/pyproject.toml +2 -1
  4. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/account.py +7 -3
  5. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/manager.py +24 -17
  6. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/ssh_utils.py +36 -15
  7. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/git_branch.py +16 -3
  8. pygitgo-1.9.0/src/pygitgo/commands/git_core.py +161 -0
  9. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/git_remote.py +11 -3
  10. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/init.py +4 -3
  11. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/jump.py +53 -43
  12. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/link.py +26 -2
  13. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/pull.py +5 -2
  14. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/push.py +9 -5
  15. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/repo.py +17 -1
  16. pygitgo-1.9.0/src/pygitgo/commands/resolve.py +54 -0
  17. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/state.py +5 -2
  18. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/undo.py +125 -117
  19. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/main.py +17 -3
  20. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/executor.py +16 -2
  21. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/PKG-INFO +6 -2
  22. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/SOURCES.txt +2 -0
  23. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_executor.py +2 -2
  24. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_git_core.py +25 -0
  25. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_jump.py +31 -61
  26. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_link.py +21 -6
  27. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_push.py +5 -1
  28. pygitgo-1.9.0/tests/test_resolve.py +105 -0
  29. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_ssh_utils.py +4 -4
  30. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_undo.py +10 -3
  31. pygitgo-1.8.2/src/pygitgo/commands/git_core.py +0 -85
  32. {pygitgo-1.8.2 → pygitgo-1.9.0}/LICENSE +0 -0
  33. {pygitgo-1.8.2 → pygitgo-1.9.0}/setup.cfg +0 -0
  34. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/__init__.py +0 -0
  35. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/auth/__init__.py +0 -0
  36. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/__init__.py +0 -0
  37. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/config.py +0 -0
  38. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/new.py +0 -0
  39. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/staging.py +0 -0
  40. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/stash.py +0 -0
  41. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/commands/user.py +0 -0
  42. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/exceptions.py +0 -0
  43. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/__init__.py +0 -0
  44. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/bootstrap.py +0 -0
  45. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/cli_io.py +0 -0
  46. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/colors.py +0 -0
  47. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/config.py +0 -0
  48. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/platform.py +0 -0
  49. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/update_checker.py +0 -0
  50. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo/utils/validators.py +0 -0
  51. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/dependency_links.txt +0 -0
  52. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/entry_points.txt +0 -0
  53. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/requires.txt +0 -0
  54. {pygitgo-1.8.2 → pygitgo-1.9.0}/src/pygitgo.egg-info/top_level.txt +0 -0
  55. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_account.py +0 -0
  56. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_bootstrap.py +0 -0
  57. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_cli_io.py +0 -0
  58. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_config_command.py +0 -0
  59. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_config_utils.py +0 -0
  60. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_git_branch.py +0 -0
  61. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_git_remote.py +0 -0
  62. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_init.py +0 -0
  63. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_integration_git.py +0 -0
  64. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_main.py +0 -0
  65. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_manager.py +0 -0
  66. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_new.py +0 -0
  67. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_platform.py +0 -0
  68. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_pull.py +0 -0
  69. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_repo.py +0 -0
  70. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_staging.py +0 -0
  71. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_stash.py +0 -0
  72. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_state.py +0 -0
  73. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_update_checker.py +0 -0
  74. {pygitgo-1.8.2 → pygitgo-1.9.0}/tests/test_user.py +0 -0
  75. {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.8.2
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 | `New Project Update` |
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 | `New Project Update` |
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.8.2"
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
- warning("\nGit user identity is not configured!")
45
- info("This is required for your commits to be attributed correctly.")
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
- key_path = ssh_utils.generate_ssh_key(email=email)
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
- error("Login Failed. The SSH key may not have been added to GitHub correctly.")
87
- info("Possible causes:")
88
- info(" 1. The key was not pasted on GitHub")
89
-
90
- if get_platform() == "windows":
91
- info(" 2. SSH agent is not running — run in PowerShell (as Administrator):")
92
- info(" Set-Service ssh-agent -StartupType Automatic")
93
- info(" Start-Service ssh-agent")
94
- info(" Then run 'gitgo user login' again.")
95
- else:
96
- info(" 2. SSH agent is not running (try: eval $(ssh-agent) && ssh-add)")
97
-
98
- info(" 3. Network or firewall is blocking SSH connections")
99
- info("Need help? Full guide: https://github.com/Huerte/GitGo/blob/main/docs/login-guide.md")
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
- return (result.stderr or "") + (result.stdout or "")
52
- except (subprocess.TimeoutExpired, OSError):
53
- return None
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": ok_text or "Verifying GitHub connection..."}
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
- output = _get_cached_ssh_response()
83
- connected = output is not None and "successfully authenticated" in output
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
- if ok_text:
87
- spinner.text = ok_text
109
+ spinner.text = ok_text or "GitHub connection verified."
88
110
  spinner.ok("✔")
89
111
  else:
90
- if fail_text:
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
- output = _get_cached_ssh_response()
99
- if output and "Hi " in output and "!" in output:
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 output.split("Hi ")[1].split("!")[0]
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 success, error, info, confirm
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
- branch = run_command(["git", "rev-parse", "--short", "HEAD"]).strip()
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
- raise GitGoError("Connection failed verify the URL and your SSH key.")
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(" git rebase --continue")
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
- pass
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(['git', 'status', '--porcelain'], loading_msg="Checking for uncommitted changes...")
70
- except GitCommandError:
71
- raise GitGoError("Unable to check for uncommitted changes — make sure you're in a valid git repository.")
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
- print()
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
- warning("Stash failed. Your working tree may have untracked files.")
87
- info("Run: git status to see what's blocking the stash.")
88
- raise GitGoError("Jump aborted: could not save working changes.")
89
- info("Changes saved. Jumping to the new branch...")
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"Branch '{target_branch}' does not exist yet. Create it and switch to it? (y/n): "):
96
- info("Exiting without jumping...")
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="Putting your unsaved changes back...")
103
+ pop_result = git_stash_pop(loading_msg="Restoring your local changes...")
99
104
  if not pop_result:
100
- warning("Could not restore your unsaved changes automatically. Run 'gitgo state list' to recover them.")
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
- if stashed_code:
108
- run_command(['git', 'checkout', target_branch], loading_msg=f"Moving you to branch '{target_branch}'...", ok_text=f"Moved to branch '{target_branch}'.")
109
- main_branch = get_main_branch()
110
- try:
111
- run_command(['git', 'pull', 'origin', main_branch], loading_msg=f"Downloading the latest updates from '{main_branch}'...", ok_text=f"Downloaded updates from '{main_branch}'.")
112
- except GitCommandError:
113
- warning(f"Failed to pull updates from '{main_branch}'. No internet, or the remote branch doesn't exist yet.")
114
- info(f"On '{target_branch}', but without the latest updates from '{main_branch}'.")
115
- else:
116
- run_command(['git', 'checkout', target_branch], loading_msg=f"Moving you to branch '{target_branch}'...", ok_text=f"Moved to branch '{target_branch}'.")
117
- main_branch = get_main_branch()
118
- try:
119
- run_command(['git', 'pull', 'origin', main_branch], loading_msg=f"Downloading the latest updates from '{main_branch}'...", ok_text=f"On '{target_branch}'. Up to date with '{main_branch}'.")
120
- except GitCommandError:
121
- warning(f"Failed to pull updates from '{main_branch}'. No internet, or the remote branch doesn't exist yet.")
122
- info(f"On '{target_branch}', but without the latest updates from '{main_branch}'.")
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="Unpacking your unsaved changes...")
135
+ apply_result = git_stash_apply(loading_msg="Restoring your local changes...")
126
136
  if not apply_result:
127
137
  print()
128
- error("MERGE CONFLICT your changes clash with the target branch.")
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 and fix / n = go back): "):
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}'. Conflict markers are in your files.")
139
- warning("Open your editor and fix the conflict lines.")
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 the temporary stash...", ok_text=f"On '{target_branch}'. Your changes came with you.")
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, 'nested', False):
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
+