pygitgo 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {pygitgo-1.2.0/src/pygitgo.egg-info → pygitgo-1.3.0}/PKG-INFO +22 -3
  2. {pygitgo-1.2.0 → pygitgo-1.3.0}/README.md +21 -2
  3. {pygitgo-1.2.0 → pygitgo-1.3.0}/pyproject.toml +1 -1
  4. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/auth/ssh_utils.py +5 -1
  5. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/commands/git_operations.py +28 -4
  6. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/commands/jump.py +1 -2
  7. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/commands/state.py +14 -11
  8. pygitgo-1.3.0/src/pygitgo/commands/undo.py +53 -0
  9. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/main.py +38 -6
  10. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/utils/config.py +3 -1
  11. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/utils/executor.py +13 -7
  12. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/utils/setup.py +1 -1
  13. {pygitgo-1.2.0 → pygitgo-1.3.0/src/pygitgo.egg-info}/PKG-INFO +22 -3
  14. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo.egg-info/SOURCES.txt +4 -0
  15. pygitgo-1.3.0/tests/test_config.py +99 -0
  16. {pygitgo-1.2.0 → pygitgo-1.3.0}/tests/test_git_operations.py +31 -1
  17. {pygitgo-1.2.0 → pygitgo-1.3.0}/tests/test_jump.py +13 -18
  18. {pygitgo-1.2.0 → pygitgo-1.3.0}/tests/test_main.py +2 -1
  19. pygitgo-1.3.0/tests/test_setup.py +55 -0
  20. {pygitgo-1.2.0 → pygitgo-1.3.0}/tests/test_state.py +8 -9
  21. pygitgo-1.3.0/tests/test_undo.py +94 -0
  22. {pygitgo-1.2.0 → pygitgo-1.3.0}/LICENSE +0 -0
  23. {pygitgo-1.2.0 → pygitgo-1.3.0}/setup.cfg +0 -0
  24. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/__init__.py +0 -0
  25. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/auth/__init__.py +0 -0
  26. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/auth/account.py +1 -1
  27. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/auth/manager.py +1 -1
  28. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/commands/__init__.py +0 -0
  29. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/exceptions.py +0 -0
  30. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/utils/__init__.py +0 -0
  31. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/utils/colors.py +0 -0
  32. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo/utils/platform_utils.py +0 -0
  33. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo.egg-info/dependency_links.txt +0 -0
  34. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo.egg-info/entry_points.txt +0 -0
  35. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo.egg-info/requires.txt +0 -0
  36. {pygitgo-1.2.0 → pygitgo-1.3.0}/src/pygitgo.egg-info/top_level.txt +0 -0
  37. {pygitgo-1.2.0 → pygitgo-1.3.0}/tests/test_platform.py +0 -0
  38. {pygitgo-1.2.0 → pygitgo-1.3.0}/tests/test_url_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitgo
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -40,7 +40,7 @@ Dynamic: license-file
40
40
 
41
41
  ---
42
42
 
43
- GitGo wraps your most repetitive Git operations init, add, commit, push, branch, stash — into short, memorable commands. It also handles the friction points most tools ignore: automatic SSH setup, HTTPS-to-SSH conversion, and a human-friendly stash interface called **state management**.
43
+ GitGo wraps your most repetitive git commands into short ones you can actually remember. It covers init, add, commit, push, branch, and stash. It also handles the things most tools skip like automatic SSH setup, HTTPS to SSH conversion, and a simple stash interface called state management.
44
44
 
45
45
  ```bash
46
46
  # Instead of this:
@@ -76,6 +76,7 @@ gitgo link https://github.com/username/repo.git "init"
76
76
  GitGo provides a CLI environment designed for faster and simpler Git workflows. Built to be intuitive, fast, and frictionless.
77
77
 
78
78
  - **Simplified Git Operations:** Replaces chained commands with single intuitive commands for linking, pushing, and stashing.
79
+ - **Easy Undo Operations:** Safely undo commits, un-queue files, or wipe local changes without any complex Git jargon.
79
80
  - **Smart Branch Hopping:** Safely traverse branches with `jump`. Auto-stashes messy code and prevents merge conflict disasters with a Try-And-Revert safety engine.
80
81
  - **State Management:** A human-readable interface over `git stash`. States are named and listed by index so you never have to remember cryptic stash references.
81
82
  - **Custom Defaults:** Save your preferred branch name and commit message locally so you never have to type them again.
@@ -147,7 +148,15 @@ Jump to a different branch without worrying about your uncommitted changes. GitG
147
148
  gitgo jump feature/new-login
148
149
  ```
149
150
 
150
- ### 5. Save Your Work-in-Progress
151
+ ### 5. Safely Undo Mistakes
152
+ Easily fix common mistakes without losing work or needing to search for Git commands.
153
+ ```bash
154
+ gitgo undo commit # Undo the last commit (files stay safe)
155
+ gitgo undo add # Un-queue files (undo git add)
156
+ gitgo undo changes # DANGER: permanently wipe all unsaved edits
157
+ ```
158
+
159
+ ### 6. Save Your Work-in-Progress
151
160
  ```bash
152
161
  gitgo state save "halfway through refactor"
153
162
  gitgo state list
@@ -200,6 +209,16 @@ Safely switches branches without losing uncommitted progress. Auto-stashes, jump
200
209
  gitgo jump <branch>
201
210
  ```
202
211
 
212
+ ### `gitgo undo`
213
+
214
+ Safely undo recent actions and mistakes using simple, human-readable commands.
215
+
216
+ ```bash
217
+ gitgo undo commit # Undo your last commit without losing files
218
+ gitgo undo add # Un-queue files (remove them from being ready to commit)
219
+ gitgo undo changes # Permanently wipe all new files and local edits
220
+ ```
221
+
203
222
  ### `gitgo state`
204
223
 
205
224
  A human-readable interface over `git stash`.
@@ -17,7 +17,7 @@
17
17
 
18
18
  ---
19
19
 
20
- GitGo wraps your most repetitive Git operations init, add, commit, push, branch, stash — into short, memorable commands. It also handles the friction points most tools ignore: automatic SSH setup, HTTPS-to-SSH conversion, and a human-friendly stash interface called **state management**.
20
+ GitGo wraps your most repetitive git commands into short ones you can actually remember. It covers init, add, commit, push, branch, and stash. It also handles the things most tools skip like automatic SSH setup, HTTPS to SSH conversion, and a simple stash interface called state management.
21
21
 
22
22
  ```bash
23
23
  # Instead of this:
@@ -53,6 +53,7 @@ gitgo link https://github.com/username/repo.git "init"
53
53
  GitGo provides a CLI environment designed for faster and simpler Git workflows. Built to be intuitive, fast, and frictionless.
54
54
 
55
55
  - **Simplified Git Operations:** Replaces chained commands with single intuitive commands for linking, pushing, and stashing.
56
+ - **Easy Undo Operations:** Safely undo commits, un-queue files, or wipe local changes without any complex Git jargon.
56
57
  - **Smart Branch Hopping:** Safely traverse branches with `jump`. Auto-stashes messy code and prevents merge conflict disasters with a Try-And-Revert safety engine.
57
58
  - **State Management:** A human-readable interface over `git stash`. States are named and listed by index so you never have to remember cryptic stash references.
58
59
  - **Custom Defaults:** Save your preferred branch name and commit message locally so you never have to type them again.
@@ -124,7 +125,15 @@ Jump to a different branch without worrying about your uncommitted changes. GitG
124
125
  gitgo jump feature/new-login
125
126
  ```
126
127
 
127
- ### 5. Save Your Work-in-Progress
128
+ ### 5. Safely Undo Mistakes
129
+ Easily fix common mistakes without losing work or needing to search for Git commands.
130
+ ```bash
131
+ gitgo undo commit # Undo the last commit (files stay safe)
132
+ gitgo undo add # Un-queue files (undo git add)
133
+ gitgo undo changes # DANGER: permanently wipe all unsaved edits
134
+ ```
135
+
136
+ ### 6. Save Your Work-in-Progress
128
137
  ```bash
129
138
  gitgo state save "halfway through refactor"
130
139
  gitgo state list
@@ -177,6 +186,16 @@ Safely switches branches without losing uncommitted progress. Auto-stashes, jump
177
186
  gitgo jump <branch>
178
187
  ```
179
188
 
189
+ ### `gitgo undo`
190
+
191
+ Safely undo recent actions and mistakes using simple, human-readable commands.
192
+
193
+ ```bash
194
+ gitgo undo commit # Undo your last commit without losing files
195
+ gitgo undo add # Un-queue files (remove them from being ready to commit)
196
+ gitgo undo changes # Permanently wipe all new files and local edits
197
+ ```
198
+
180
199
  ### `gitgo state`
181
200
 
182
201
  A human-readable interface over `git stash`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitgo"
7
- version = "1.2.0"
7
+ version = "1.3.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"}
@@ -1,5 +1,5 @@
1
- from pygitgo.utils.executor import run_command
2
1
  from pygitgo.utils.colors import info, success, warning, error
2
+ from pygitgo.utils.executor import run_command
3
3
  from pygitgo.utils import platform_utils
4
4
  from pathlib import Path
5
5
  import subprocess
@@ -35,11 +35,15 @@ def ensure_github_known_host():
35
35
  def check_connection():
36
36
  ensure_github_known_host()
37
37
  result = run_command(["ssh", "-T", "git@github.com"], allow_fail=True, return_complete=True)
38
+ if not hasattr(result, "stderr") or not result.stderr:
39
+ return False
38
40
  return "successfully authenticated" in result.stderr
39
41
 
40
42
  def get_github_username():
41
43
 
42
44
  result = run_command(["ssh", "-T", "git@github.com"], allow_fail=True, return_complete=True)
45
+ if not hasattr(result, "stderr") or not result.stderr:
46
+ return None
43
47
  output = result.stderr
44
48
 
45
49
  if "Hi " in output and "!" in output:
@@ -2,11 +2,18 @@ from pygitgo.auth.ssh_utils import convert_https_to_ssh, is_ssh_url, check_conne
2
2
  from pygitgo.utils.colors import info, success, warning, error
3
3
  from pygitgo.utils.executor import run_command
4
4
  from pygitgo.utils.config import get_config
5
+ from argparse import Namespace
5
6
  import subprocess
6
7
  import sys
7
8
  import os
8
9
 
9
10
 
11
+ def get_status_content():
12
+ status = run_command(["git", "status", "--porcelain"], allow_fail=True)
13
+ if isinstance(status, subprocess.CalledProcessError) or not status.strip():
14
+ sys.exit(1)
15
+ return status
16
+
10
17
  def get_current_branch():
11
18
  branch = run_command(["git", "branch", "--show-current"]).strip()
12
19
  if not branch:
@@ -26,8 +33,17 @@ def is_branch_exist(branch):
26
33
 
27
34
 
28
35
  def git_new_branch(branch):
29
- run_command(["git", "checkout", "-b", branch], loading_msg=f"Creating branch '{branch}'...")
30
- success(f"\nBranch '{branch}' created.\n")
36
+ result = run_command(["git", "checkout", "-b", branch], loading_msg=f"Creating branch '{branch}'...")
37
+ if isinstance(result, subprocess.CalledProcessError):
38
+ error(f"Failed to create branch '{branch}'! It may already exist.")
39
+ choice = input("\nWould you like to jump to the existing branch instead? (y/n): ").strip().lower()
40
+ if choice == "y":
41
+ from pygitgo.commands.jump import jump_operation
42
+ jump_operation(Namespace(branch=branch))
43
+ else:
44
+ sys.exit(1)
45
+ else:
46
+ success(f"\nBranch '{branch}' created.\n")
31
47
 
32
48
  return branch
33
49
 
@@ -136,8 +152,16 @@ def git_push(branch):
136
152
  if ssh_url:
137
153
  run_command(["git", "remote", "set-url", "origin", ssh_url], loading_msg="Converting remote from HTTPS to SSH for secure push...")
138
154
  success(f"Remote updated to: {ssh_url}")
139
-
140
- run_command(["git", "push", "-u", "origin", branch], loading_msg=f"Pushing to remote branch '{branch}'...")
155
+
156
+ try:
157
+ run_command(["git", "push", "-u", "origin", branch], loading_msg=f"Pushing to remote branch '{branch}'...")
158
+ except (subprocess.CalledProcessError, OSError) as e:
159
+ error("Failed to push to remote repository!")
160
+ warning("Please check your network connection, remote URL, and authentication.")
161
+ if "rebase in progress" in str(e):
162
+ handle_rebase()
163
+ else:
164
+ sys.exit(1)
141
165
 
142
166
 
143
167
  def handle_rebase():
@@ -1,9 +1,8 @@
1
1
  from pygitgo.commands.git_operations import (
2
2
  is_branch_exist, get_current_branch, git_new_branch, get_main_branch
3
3
  )
4
- from pygitgo.utils.executor import run_command
5
- from pygitgo.commands.state import *
6
4
  from pygitgo.utils.colors import warning, info, success, error
5
+ from pygitgo.utils.executor import run_command
7
6
  import subprocess
8
7
  import sys
9
8
 
@@ -1,5 +1,5 @@
1
- from pygitgo.utils.executor import run_command
2
1
  from pygitgo.utils.colors import info, highlight, error, warning, success
2
+ from pygitgo.utils.executor import run_command
3
3
  import sys
4
4
 
5
5
 
@@ -59,20 +59,19 @@ def display_save_states():
59
59
 
60
60
 
61
61
  def is_number(value):
62
- try:
63
- float(value)
64
- return True
65
- except ValueError:
66
- return False
62
+ val = str(value).strip()
63
+ if val.startswith('-'):
64
+ return val[1:].isdigit()
65
+ return val.isdigit()
67
66
 
68
67
  def validate_state_id(state_id, save_states):
69
68
  if not is_number(state_id):
70
69
  error("\nInvalid input. Please enter a valid state ID.\n")
71
70
  return False
72
- elif (int(float(state_id)) - 1) < 0:
71
+ elif (int(state_id) - 1) < 0:
73
72
  error("\nState ID cannot be '0' or negative. Please enter a valid state ID.\n")
74
73
  return False
75
- elif (int(float(state_id)) - 1) >= len(save_states):
74
+ elif (int(state_id) - 1) >= len(save_states):
76
75
  error("\nState ID out of range. Please enter a valid state ID.\n")
77
76
  return False
78
77
  return True
@@ -124,9 +123,13 @@ def save_state(state_name=None):
124
123
  if not state_name:
125
124
  state_name = "Auto-Save"
126
125
 
127
- run_command(["git", "stash", "push", "-m", state_name])
128
-
129
- success(f"\nState '{state_name}' saved successfully.\n")
126
+ output = run_command(["git", "stash", "push", "-m", state_name], allow_fail=True)
127
+ if isinstance(output, Exception):
128
+ error(f"\nFailed to save state '{state_name}'.\n")
129
+ elif "No local changes to save" in output:
130
+ warning("\nNo local changes to save.\n")
131
+ else:
132
+ success(f"\nState '{state_name}' saved successfully.\n")
130
133
 
131
134
 
132
135
  def delete_state(identifier=None):
@@ -0,0 +1,53 @@
1
+ from pygitgo.utils.colors import success, warning, error, info
2
+ from pygitgo.exceptions import GitCommandError
3
+ from pygitgo.utils.executor import run_command
4
+ import sys
5
+
6
+
7
+ def undo_commit():
8
+ try:
9
+ run_command(["git", "reset", "--soft", "HEAD~"])
10
+ success("\nSuccess! Your last commit is undone. Your files are safe.\n")
11
+ except GitCommandError as e:
12
+ error("\nFailed! You might not have any previous commits to undo yet.")
13
+ error(f"Details: {e}\n")
14
+ sys.exit(1)
15
+
16
+
17
+ def undo_add():
18
+ try:
19
+ run_command(["git", "reset", "HEAD"])
20
+ success("\nSuccess! Files are no longer ready to commit.\n")
21
+ except GitCommandError as e:
22
+ error(f"\n{e}\n")
23
+ sys.exit(1)
24
+
25
+
26
+ def undo_changes():
27
+ warning("\nDANGER: This will permanently delete all your new edits and new files!")
28
+ confirm = input("Are you sure you want to throw away all changes? (y/n): ")
29
+ if confirm.lower() != "y":
30
+ info("\nCanceled. Your files are safe.\n")
31
+ return
32
+ try:
33
+ run_command(["git", "reset", "--hard", "HEAD"], loading_msg="Throwing away edits...")
34
+ run_command(["git", "clean", "-fd"], loading_msg="Removing new files...")
35
+
36
+ success("\nSuccess! All changes are completely gone. You have a clean start.\n")
37
+ except GitCommandError as e:
38
+ error(f"\n{e}\n")
39
+ sys.exit(1)
40
+
41
+
42
+ def undo_operations(args):
43
+ action = args.action
44
+
45
+ if action == "commit":
46
+ undo_commit()
47
+ elif action == "add":
48
+ undo_add()
49
+ elif action == "changes":
50
+ undo_changes()
51
+ else:
52
+ error(f"\nUnknown undo operation: {action}\n")
53
+ sys.exit(1)
@@ -1,18 +1,18 @@
1
- from pygitgo.auth.ssh_utils import ensure_github_known_host
2
1
  from pygitgo.commands.git_operations import (
3
2
  git_new_branch, git_commit, git_init, add_remote_origin,
4
- confirm_remote_link, create_main_branch, check_and_sync_branch,
5
- git_push, handle_rebase, get_current_branch, is_branch_exist
3
+ confirm_remote_link, git_push, get_current_branch, is_branch_exist
6
4
  )
7
- from pygitgo.utils.colors import info, success, warning, error, highlight
5
+ from pygitgo.utils.colors import info, success, warning, error
8
6
  from pygitgo.utils.config import get_config, config_operation
7
+ from pygitgo.exceptions import GitCommandError, GitGoError
9
8
  from pygitgo.utils.setup import ensure_first_run_setup
10
9
  from pygitgo.commands.state import state_operations
10
+ from pygitgo.commands.undo import undo_operations
11
11
  from pygitgo.commands.jump import jump_operation
12
12
  from pygitgo.utils.executor import run_command
13
13
  from pygitgo.auth.manager import login, logout
14
14
  from pygitgo.auth.account import get_user
15
- from pygitgo.exceptions import GitGoError
15
+ from argparse import Namespace
16
16
  import subprocess
17
17
  import argparse
18
18
  import sys
@@ -109,11 +109,24 @@ def push_operation(args):
109
109
  if branch and not message and not is_branch_exist(branch):
110
110
  message = branch
111
111
  branch = get_current_branch()
112
+ info(f"No branch name provided. Using current branch: '{branch}'\n")
113
+
114
+ elif branch and is_branch_exist(branch):
115
+ current_branch = get_current_branch()
116
+ if current_branch != branch:
117
+ warning(f"You are currently on branch '{current_branch}', not '{branch}'.")
118
+ user_choice = input(f"Do you want to switch to branch '{branch}'? (y/n): ").lower()
119
+ if user_choice != 'y':
120
+ error("\nPush aborted to prevent committing to the wrong branch.\n")
121
+ sys.exit(1)
122
+ jump_operation(Namespace(branch=branch))
123
+
112
124
  elif not branch:
113
125
  branch = get_current_branch()
114
126
 
115
127
  if not message:
116
128
  message = get_config("default-message", "New Project Update")
129
+ info(f"No commit message provided. Using default: '{message}'\n")
117
130
 
118
131
  commit_made = git_commit(message)
119
132
 
@@ -129,7 +142,7 @@ def push_operation(args):
129
142
  info("\nWorking tree is clean and everything is up to date.")
130
143
  warning("Make some changes first before using GitGo to commit and push.")
131
144
  return
132
- except:
145
+ except (GitCommandError, Exception):
133
146
  warning("\nNo changes to commit. Cannot verify remote status.")
134
147
  warning("Make some changes first or check your git remote configuration.")
135
148
  return
@@ -173,6 +186,7 @@ def get_version():
173
186
  except Exception:
174
187
  return "dev"
175
188
 
189
+
176
190
  def main():
177
191
  parser = argparse.ArgumentParser(
178
192
  prog='gitgo',
@@ -252,6 +266,22 @@ def main():
252
266
  config_parser.add_argument("key", choices=["default-branch", "default-message"], help="The setting to change")
253
267
  config_parser.add_argument("value", nargs="?", help="The new value (required for 'set')")
254
268
 
269
+ undo_parser = subparsers.add_parser("undo",
270
+ help="Safely undo mistakes",
271
+ epilog=(
272
+ "Examples:\n"
273
+ " gitgo undo commit Undo your last commit (your files are safe)\n"
274
+ " gitgo undo add Undo 'git add' (files are no longer ready to commit)\n"
275
+ " gitgo undo changes DANGER: Throw away all new changes and start fresh"
276
+ ),
277
+ formatter_class=argparse.RawDescriptionHelpFormatter
278
+ )
279
+ undo_parser.add_argument(
280
+ "action",
281
+ choices=["commit", "add", "changes"],
282
+ help="What to undo: 'commit', 'add', or 'changes'"
283
+ )
284
+
255
285
  args = parser.parse_args()
256
286
 
257
287
  if args.ready:
@@ -277,6 +307,8 @@ def main():
277
307
  user_management(args)
278
308
  elif args.command == "config":
279
309
  config_operation(args)
310
+ elif args.command == "undo":
311
+ undo_operations(args)
280
312
  except GitGoError as e:
281
313
  error(f"\n{e}\n")
282
314
  sys.exit(1)
@@ -51,4 +51,6 @@ def config_operation(args):
51
51
  if current_value:
52
52
  info(f"\n{key} is currently set to: '{current_value}'\n")
53
53
  else:
54
- warning(f"\n{key} is not currently set.\n")
54
+ warning(f"\n{key} is not currently set.\n")
55
+
56
+
@@ -1,8 +1,8 @@
1
- import subprocess
2
- import sys
3
- import os
4
1
  from pygitgo.utils.colors import error, info, success, warning
5
2
  from pygitgo.exceptions import GitCommandError
3
+ from yaspin import yaspin
4
+ import subprocess
5
+ import os
6
6
 
7
7
 
8
8
  def run_command(command, allow_fail=False, return_complete=False, loading_msg=None):
@@ -15,7 +15,6 @@ def run_command(command, allow_fail=False, return_complete=False, loading_msg=No
15
15
  :param loading_msg: if provided, show a yaspin spinner with this message
16
16
  :raises GitCommandError: when the command fails and allow_fail is False
17
17
  """
18
- from yaspin import yaspin
19
18
 
20
19
  spinner = yaspin(text=loading_msg, color="cyan") if loading_msg else None
21
20
 
@@ -34,11 +33,18 @@ def run_command(command, allow_fail=False, return_complete=False, loading_msg=No
34
33
  spinner.ok("✔")
35
34
 
36
35
  return result if return_complete else result.stdout.strip()
37
- except subprocess.CalledProcessError as e:
36
+ except (subprocess.CalledProcessError, OSError) as e:
38
37
  if spinner:
39
38
  spinner.fail("✖")
40
39
 
41
- stderr = e.stderr.strip() if e.stderr else ""
40
+ stderr = ""
41
+ returncode = 1
42
+
43
+ if isinstance(e, subprocess.CalledProcessError):
44
+ stderr = e.stderr.strip() if e.stderr else ""
45
+ returncode = e.returncode
46
+ else:
47
+ stderr = f"Command not found or execution failed: {str(e)}"
42
48
 
43
49
  if "detected dubious ownership" in stderr:
44
50
  error(f"\nSECURITY ALERT: {stderr}")
@@ -68,4 +74,4 @@ def run_command(command, allow_fail=False, return_complete=False, loading_msg=No
68
74
  if allow_fail:
69
75
  return e
70
76
 
71
- raise GitCommandError(command, stderr=stderr, returncode=e.returncode)
77
+ raise GitCommandError(command, stderr=stderr, returncode=returncode)
@@ -19,7 +19,7 @@ def ensure_first_run_setup():
19
19
 
20
20
  is_initialized = get_config("initialized", fallback_value="false")
21
21
 
22
- if not is_initialized:
22
+ if is_initialized == "false":
23
23
  info("\nInitializing GitGo network settings for the first time... please wait.")
24
24
  ensure_github_known_host()
25
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitgo
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -40,7 +40,7 @@ Dynamic: license-file
40
40
 
41
41
  ---
42
42
 
43
- GitGo wraps your most repetitive Git operations init, add, commit, push, branch, stash — into short, memorable commands. It also handles the friction points most tools ignore: automatic SSH setup, HTTPS-to-SSH conversion, and a human-friendly stash interface called **state management**.
43
+ GitGo wraps your most repetitive git commands into short ones you can actually remember. It covers init, add, commit, push, branch, and stash. It also handles the things most tools skip like automatic SSH setup, HTTPS to SSH conversion, and a simple stash interface called state management.
44
44
 
45
45
  ```bash
46
46
  # Instead of this:
@@ -76,6 +76,7 @@ gitgo link https://github.com/username/repo.git "init"
76
76
  GitGo provides a CLI environment designed for faster and simpler Git workflows. Built to be intuitive, fast, and frictionless.
77
77
 
78
78
  - **Simplified Git Operations:** Replaces chained commands with single intuitive commands for linking, pushing, and stashing.
79
+ - **Easy Undo Operations:** Safely undo commits, un-queue files, or wipe local changes without any complex Git jargon.
79
80
  - **Smart Branch Hopping:** Safely traverse branches with `jump`. Auto-stashes messy code and prevents merge conflict disasters with a Try-And-Revert safety engine.
80
81
  - **State Management:** A human-readable interface over `git stash`. States are named and listed by index so you never have to remember cryptic stash references.
81
82
  - **Custom Defaults:** Save your preferred branch name and commit message locally so you never have to type them again.
@@ -147,7 +148,15 @@ Jump to a different branch without worrying about your uncommitted changes. GitG
147
148
  gitgo jump feature/new-login
148
149
  ```
149
150
 
150
- ### 5. Save Your Work-in-Progress
151
+ ### 5. Safely Undo Mistakes
152
+ Easily fix common mistakes without losing work or needing to search for Git commands.
153
+ ```bash
154
+ gitgo undo commit # Undo the last commit (files stay safe)
155
+ gitgo undo add # Un-queue files (undo git add)
156
+ gitgo undo changes # DANGER: permanently wipe all unsaved edits
157
+ ```
158
+
159
+ ### 6. Save Your Work-in-Progress
151
160
  ```bash
152
161
  gitgo state save "halfway through refactor"
153
162
  gitgo state list
@@ -200,6 +209,16 @@ Safely switches branches without losing uncommitted progress. Auto-stashes, jump
200
209
  gitgo jump <branch>
201
210
  ```
202
211
 
212
+ ### `gitgo undo`
213
+
214
+ Safely undo recent actions and mistakes using simple, human-readable commands.
215
+
216
+ ```bash
217
+ gitgo undo commit # Undo your last commit without losing files
218
+ gitgo undo add # Un-queue files (remove them from being ready to commit)
219
+ gitgo undo changes # Permanently wipe all new files and local edits
220
+ ```
221
+
203
222
  ### `gitgo state`
204
223
 
205
224
  A human-readable interface over `git stash`.
@@ -18,15 +18,19 @@ src/pygitgo/commands/__init__.py
18
18
  src/pygitgo/commands/git_operations.py
19
19
  src/pygitgo/commands/jump.py
20
20
  src/pygitgo/commands/state.py
21
+ src/pygitgo/commands/undo.py
21
22
  src/pygitgo/utils/__init__.py
22
23
  src/pygitgo/utils/colors.py
23
24
  src/pygitgo/utils/config.py
24
25
  src/pygitgo/utils/executor.py
25
26
  src/pygitgo/utils/platform_utils.py
26
27
  src/pygitgo/utils/setup.py
28
+ tests/test_config.py
27
29
  tests/test_git_operations.py
28
30
  tests/test_jump.py
29
31
  tests/test_main.py
30
32
  tests/test_platform.py
33
+ tests/test_setup.py
31
34
  tests/test_state.py
35
+ tests/test_undo.py
32
36
  tests/test_url_validator.py
@@ -0,0 +1,99 @@
1
+ from pygitgo.utils.config import get_config, set_config, config_operation
2
+ from conftest import capture_system_exit_code
3
+ from argparse import Namespace
4
+ import subprocess
5
+
6
+
7
+ def test_get_config_fallback(mocker):
8
+ fake_run = mocker.patch('pygitgo.utils.config.run_command', return_value="")
9
+
10
+ fallback_value = "false"
11
+
12
+ result = get_config("default-key", fallback_value)
13
+ assert result == fallback_value
14
+
15
+ def test_get_config_ok(mocker):
16
+ fake_run = mocker.patch('pygitgo.utils.config.run_command', return_value="true")
17
+
18
+ fallback_value = "false"
19
+
20
+ result = get_config("default-key", fallback_value)
21
+ assert result != fallback_value
22
+
23
+ def test_set_config_fallback(mocker):
24
+ fake_run = mocker.patch('pygitgo.utils.config.run_command', return_value=subprocess.CalledProcessError(1, 'git'))
25
+ fake_error = mocker.patch('pygitgo.utils.config.error')
26
+
27
+ key = 'default-key'
28
+
29
+ result = set_config(key, 'true')
30
+ assert result == False
31
+
32
+ fake_error.assert_called_with(f"\nFailed to save configuration for '{key}'.")
33
+
34
+ def test_set_config_ok(mocker):
35
+ fake_run = mocker.patch('pygitgo.utils.config.run_command', return_value='ok')
36
+ fake_error = mocker.patch('pygitgo.utils.config.error')
37
+ fake_success = mocker.patch('pygitgo.utils.config.success')
38
+
39
+ key = 'default-key'
40
+ value = 'true'
41
+
42
+ result = set_config(key, value)
43
+ assert result == True
44
+
45
+ fake_error.assert_not_called()
46
+ fake_success.assert_called_with(f"\nConfiguration saved: {key} = '{value}'")
47
+
48
+ def test_config_operation_not_valid_keys(mocker):
49
+ fake_error = mocker.patch('pygitgo.utils.config.error')
50
+ fake_warning = mocker.patch('pygitgo.utils.config.warning')
51
+
52
+
53
+ VALID_KEYS = ["default-branch", "default-message"]
54
+
55
+ key = "not-valid"
56
+ value = "true"
57
+ args = Namespace(key=key, action="set", value=value)
58
+
59
+ assert capture_system_exit_code(lambda: config_operation(args)) is None
60
+
61
+ fake_error.assert_called_with(f"\nInvalid configuration key: '{key}'")
62
+ fake_warning.assert_called_with(f"Valid keys are: {', '.join(VALID_KEYS)}\n")
63
+
64
+ def test_get_config_strip(mocker):
65
+ fake_run = mocker.patch('pygitgo.utils.config.run_command', return_value=" true \n")
66
+ result = get_config("default-key", "false")
67
+ assert result == "true"
68
+
69
+ def test_get_config_error_handling(mocker):
70
+ fake_run = mocker.patch('pygitgo.utils.config.run_command', return_value=subprocess.CalledProcessError(1, 'git'))
71
+ result = get_config("default-key", "fallback")
72
+ assert result == "fallback"
73
+
74
+ def test_config_operation_set_no_value(mocker):
75
+ fake_error = mocker.patch('pygitgo.utils.config.error')
76
+ args = Namespace(key="default-branch", action="set") # value is missing
77
+ config_operation(args)
78
+ fake_error.assert_called_with("\nYou must provide a value to set!\n")
79
+
80
+ def test_config_operation_set_ok(mocker):
81
+ fake_set = mocker.patch('pygitgo.utils.config.set_config')
82
+ args = Namespace(key="default-branch", action="set", value="main")
83
+ config_operation(args)
84
+ fake_set.assert_called_with("default-branch", "main")
85
+
86
+ def test_config_operation_get_ok(mocker):
87
+ mocker.patch('pygitgo.utils.config.get_config', return_value="main")
88
+ fake_info = mocker.patch('pygitgo.utils.config.info')
89
+ args = Namespace(key="default-branch", action="get")
90
+ config_operation(args)
91
+ fake_info.assert_called_with("\ndefault-branch is currently set to: 'main'\n")
92
+
93
+ def test_config_operation_get_none(mocker):
94
+ mocker.patch('pygitgo.utils.config.get_config', return_value=None)
95
+ fake_warning = mocker.patch('pygitgo.utils.config.warning')
96
+ args = Namespace(key="default-branch", action="get")
97
+ config_operation(args)
98
+ fake_warning.assert_called_with("\ndefault-branch is not currently set.\n")
99
+
@@ -7,7 +7,7 @@ from pygitgo.commands.git_operations import (
7
7
  from unittest.mock import call
8
8
  import subprocess
9
9
  import pytest
10
- import os
10
+
11
11
 
12
12
  def test_git_branch_logic(mocker):
13
13
  fake_run = mocker.patch("pygitgo.commands.git_operations.run_command")
@@ -21,6 +21,36 @@ def test_git_branch_logic(mocker):
21
21
  loading_msg="Creating branch 'hello-world'..."
22
22
  )
23
23
 
24
+ def test_git_branch_exists_jump_yes(mocker):
25
+ fake_run = mocker.patch("pygitgo.commands.git_operations.run_command", return_value=subprocess.CalledProcessError(1, 'git'))
26
+ mocker.patch("builtins.input", return_value="y")
27
+ fake_jump = mocker.patch("pygitgo.commands.jump.jump_operation")
28
+ fake_error = mocker.patch("pygitgo.commands.git_operations.error")
29
+
30
+ branch_name = "existing-branch"
31
+ result = git_new_branch(branch_name)
32
+
33
+ assert result == "existing-branch"
34
+ fake_error.assert_called_once_with(f"Failed to create branch '{branch_name}'! It may already exist.")
35
+
36
+ args = fake_jump.call_args[0][0]
37
+ assert args.branch == branch_name
38
+
39
+ def test_git_branch_exists_jump_no(mocker):
40
+ fake_run = mocker.patch("pygitgo.commands.git_operations.run_command", return_value=subprocess.CalledProcessError(1, 'git'))
41
+ mocker.patch("builtins.input", return_value="n")
42
+ fake_jump = mocker.patch("pygitgo.commands.jump.jump_operation")
43
+ fake_error = mocker.patch("pygitgo.commands.git_operations.error")
44
+
45
+ branch_name = "existing-branch"
46
+
47
+ with pytest.raises(SystemExit) as exc_info:
48
+ git_new_branch(branch_name)
49
+
50
+ assert exc_info.value.code == 1
51
+ fake_error.assert_called_once_with(f"Failed to create branch '{branch_name}'! It may already exist.")
52
+ fake_jump.assert_not_called()
53
+
24
54
  def test_git_commit(mocker):
25
55
  fake_run = mocker.patch("pygitgo.commands.git_operations.run_command")\
26
56
 
@@ -1,18 +1,13 @@
1
1
  from pygitgo.commands.jump import undo_jump_operation, jump_operation
2
+ from conftest import capture_system_exit_code
2
3
  from argparse import Namespace
3
4
  import subprocess
4
5
  import pytest
5
6
 
7
+
6
8
  def make_args(branch):
7
- """Helper to create an argparse-like Namespace for jump_operation."""
8
9
  return Namespace(branch=branch)
9
10
 
10
- def check_pytest_system_exit(function):
11
- with pytest.raises(SystemExit) as exc_info:
12
- function()
13
-
14
- return exc_info.value.code
15
-
16
11
  def test_undo_jump_operation_no_stash(mocker):
17
12
  fake_run = mocker.patch('pygitgo.commands.jump.run_command', return_value="")
18
13
 
@@ -47,7 +42,7 @@ def test_jump_operation_same_branch(mocker):
47
42
  )
48
43
  fake_warning = mocker.patch('pygitgo.commands.jump.warning')
49
44
 
50
- assert check_pytest_system_exit(lambda: jump_operation(make_args(main_branch))) == 0
45
+ assert capture_system_exit_code(lambda: jump_operation(make_args(main_branch))) == 0
51
46
 
52
47
  fake_warning.assert_called_with(f"\nYou are already on branch '{main_branch}'.\n")
53
48
 
@@ -60,7 +55,7 @@ def test_jump_operation_not_valid_repo(mocker):
60
55
  return_value=subprocess.CalledProcessError(1, 'git')
61
56
  )
62
57
 
63
- assert check_pytest_system_exit(lambda: jump_operation(make_args('main'))) == 1
58
+ assert capture_system_exit_code(lambda: jump_operation(make_args('main'))) == 1
64
59
 
65
60
  fake_warning.assert_called_with("\nUnable to check for uncommitted changes. Please ensure you're in a valid git repository.")
66
61
 
@@ -77,7 +72,7 @@ def test_jump_operation_has_changes_exit(mocker):
77
72
  side_effect=['some-changes']
78
73
  )
79
74
 
80
- assert check_pytest_system_exit(lambda: jump_operation(make_args('main'))) == 0
75
+ assert capture_system_exit_code(lambda: jump_operation(make_args('main'))) == 0
81
76
 
82
77
  fake_warning.assert_any_call("\nYou cannot switch branches with unsaved changes. Jump canceled.\n")
83
78
  fake_run.assert_any_call(['git', 'status', '--porcelain'], allow_fail=True, loading_msg="Checking for uncommitted changes...")
@@ -94,7 +89,7 @@ def test_jump_operation_save_changes_error(mocker):
94
89
  )
95
90
  )
96
91
 
97
- assert check_pytest_system_exit(lambda: jump_operation(make_args('main'))) == 1
92
+ assert capture_system_exit_code(lambda: jump_operation(make_args('main'))) == 1
98
93
 
99
94
  fake_warning.assert_called_with("\nFailed to save your changes. Please resolve any issues and try again.")
100
95
  fake_run.assert_any_call(['git', 'status', '--porcelain'], allow_fail=True, loading_msg="Checking for uncommitted changes...")
@@ -115,7 +110,7 @@ def test_jump_operation_no_changes(mocker):
115
110
 
116
111
  target_branch = 'feature'
117
112
 
118
- assert check_pytest_system_exit(lambda: jump_operation(make_args(target_branch))) == 0
113
+ assert capture_system_exit_code(lambda: jump_operation(make_args(target_branch))) == 0
119
114
 
120
115
  fake_success.assert_called_with(f"\nSuccess! You are now on '{target_branch}'.\n")
121
116
  fake_run.assert_any_call(['git', 'status', '--porcelain'], allow_fail=True, loading_msg="Checking for uncommitted changes...")
@@ -143,7 +138,7 @@ def test_jump_operation_branch_not_exist_cancel_operation(mocker):
143
138
 
144
139
  target_branch = 'feature'
145
140
 
146
- assert check_pytest_system_exit(lambda: jump_operation(make_args(target_branch))) == 0
141
+ assert capture_system_exit_code(lambda: jump_operation(make_args(target_branch))) == 0
147
142
 
148
143
  fake_info.assert_any_call('\nYour changes have been saved. Jumping to the new branch...')
149
144
  fake_info.assert_any_call("Exiting without jumping...\n")
@@ -175,7 +170,7 @@ def test_jump_operation_branch_not_exist_create_branch(mocker):
175
170
 
176
171
  target_branch = 'feature'
177
172
 
178
- assert check_pytest_system_exit(lambda: jump_operation(make_args(target_branch))) == 0
173
+ assert capture_system_exit_code(lambda: jump_operation(make_args(target_branch))) == 0
179
174
 
180
175
  fake_info.assert_called_with('\nYour changes have been saved. Jumping to the new branch...')
181
176
  fake_success.assert_any_call(f"\nSuccess! You are now on '{target_branch}'.")
@@ -204,7 +199,7 @@ def test_jump_operation_sync_fail_cancel(mocker):
204
199
  )
205
200
  )
206
201
 
207
- assert check_pytest_system_exit(lambda: jump_operation(make_args('feature'))) == 1
202
+ assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 1
208
203
 
209
204
  fake_warning.assert_any_call("\nFailed to pull updates from 'main'. Make sure you have internet or the remote branch exists.")
210
205
 
@@ -225,7 +220,7 @@ def test_jump_operation_sync_fail_stay(mocker):
225
220
  )
226
221
  )
227
222
 
228
- assert check_pytest_system_exit(lambda: jump_operation(make_args('feature'))) == 0
223
+ assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0
229
224
 
230
225
  fake_success.assert_any_call("\nOkay! You are on the new branch, but without the latest updates from 'main'.")
231
226
 
@@ -247,7 +242,7 @@ def test_jump_operation_merge_conflict_cancel(mocker):
247
242
  )
248
243
  )
249
244
 
250
- assert check_pytest_system_exit(lambda: jump_operation(make_args('feature'))) == 0
245
+ assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0
251
246
 
252
247
  fake_error.assert_any_call("\nSTOP! There is a 'Merge Conflict'.")
253
248
 
@@ -270,7 +265,7 @@ def test_jump_operation_merge_conflict_stay(mocker):
270
265
  )
271
266
  )
272
267
 
273
- assert check_pytest_system_exit(lambda: jump_operation(make_args('feature'))) == 0
268
+ assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0
274
269
 
275
270
  fake_success.assert_any_call("\nOkay! You are on the new branch with your code.")
276
271
  fake_warning.assert_any_call("Please open your code editor RIGHT NOW to fix the conflicts!")
@@ -1,5 +1,6 @@
1
- import pytest
2
1
  from pygitgo.main import validate_repo_url
2
+ import pytest
3
+
3
4
 
4
5
  @pytest.mark.parametrize("url", [
5
6
  "https://github.com/Huerte/GitGo.git",
@@ -0,0 +1,55 @@
1
+ from pygitgo.utils.setup import check_git_installed, ensure_first_run_setup
2
+ from conftest import capture_system_exit_code
3
+
4
+
5
+ def test_check_git_installed_no_exist(mocker):
6
+ mocker.patch("shutil.which", return_value=None)
7
+ fake_error = mocker.patch("pygitgo.utils.setup.error")
8
+ fake_info = mocker.patch("pygitgo.utils.setup.info")
9
+
10
+ assert capture_system_exit_code(lambda: check_git_installed()) == 1
11
+
12
+ fake_error.assert_called_with("\nGit is not installed or not found on your PATH!")
13
+ fake_info.assert_any_call("Install Git from: https://git-scm.com/downloads")
14
+ fake_info.assert_any_call("After installing, restart your terminal and try again.\n")
15
+
16
+ def test_check_git_installed_exist(mocker):
17
+ mocker.patch("shutil.which", return_value="ok")
18
+ fake_error = mocker.patch("pygitgo.utils.setup.error")
19
+ fake_info = mocker.patch("pygitgo.utils.setup.info")
20
+
21
+ assert capture_system_exit_code(lambda: check_git_installed()) == None
22
+
23
+ fake_error.assert_not_called()
24
+ fake_info.assert_not_called()
25
+
26
+ def test_ensure_first_run_setup_already_initialized(mocker):
27
+ mocker.patch('pygitgo.utils.setup.check_git_installed', return_value=None)
28
+ fake_get_config = mocker.patch('pygitgo.utils.setup.get_config', return_value="ok")
29
+ fake_github_function = mocker.patch('pygitgo.utils.setup.ensure_github_known_host', return_value=None)
30
+ fake_set_config = mocker.patch('pygitgo.utils.setup.set_config', return_value=True)
31
+ fake_info = mocker.patch("pygitgo.utils.setup.info")
32
+
33
+ ensure_first_run_setup()
34
+
35
+ fake_info.assert_not_called()
36
+ fake_github_function.assert_not_called()
37
+ fake_set_config.assert_not_called()
38
+
39
+ fake_get_config.assert_called_with("initialized", fallback_value="false")
40
+
41
+ def test_ensure_first_run_setup_not_initialized(mocker):
42
+ mocker.patch('pygitgo.utils.setup.check_git_installed', return_value=None)
43
+ fake_get_config = mocker.patch('pygitgo.utils.setup.get_config', return_value="false")
44
+ fake_github_function = mocker.patch('pygitgo.utils.setup.ensure_github_known_host', return_value=None)
45
+ fake_set_config = mocker.patch('pygitgo.utils.setup.set_config', return_value=True)
46
+ fake_info = mocker.patch("pygitgo.utils.setup.info")
47
+
48
+ ensure_first_run_setup()
49
+
50
+ fake_info.assert_called_with("\nInitializing GitGo network settings for the first time... please wait.")
51
+ fake_github_function.assert_called_once()
52
+ fake_set_config.assert_called_with("initialized", "true")
53
+
54
+ fake_get_config.assert_called_with("initialized", fallback_value="false")
55
+
@@ -3,12 +3,11 @@ from pygitgo.commands.state import (
3
3
  all_save_state
4
4
  )
5
5
  from unittest.mock import call
6
- import subprocess
7
6
  import pytest
8
- import os
7
+
9
8
 
10
9
  @pytest.mark.parametrize('state_id',[
11
- '1', '3', '11', '00002', '7.0'
10
+ '1', '3', '11', '00002'
12
11
  ])
13
12
  def test_validate_state_id(state_id, mocker):
14
13
  fake_error = mocker.patch('pygitgo.commands.state.error')
@@ -20,7 +19,7 @@ def test_validate_state_id(state_id, mocker):
20
19
  fake_error.assert_not_called()
21
20
 
22
21
  @pytest.mark.parametrize('state_id',[
23
- '-1', '-3', '-11', '-00002', '-7.0'
22
+ '-1', '-3', '-11', '-00002'
24
23
  ])
25
24
  def test_validate_state_id_negative(state_id, mocker):
26
25
 
@@ -33,7 +32,7 @@ def test_validate_state_id_negative(state_id, mocker):
33
32
  fake_error.assert_called_with("\nState ID cannot be '0' or negative. Please enter a valid state ID.\n")
34
33
 
35
34
  @pytest.mark.parametrize('state_id',[
36
- '4', '10', '15', '0000020', '00007.00000'
35
+ '4', '10', '15', '0000020'
37
36
  ])
38
37
  def test_validate_state_id_out_scope(state_id, mocker):
39
38
 
@@ -156,21 +155,21 @@ def test_load_state_no_args(mocker):
156
155
 
157
156
 
158
157
  def test_save_state_no_args(mocker):
159
- fake_run = mocker.patch("pygitgo.commands.state.run_command")
158
+ fake_run = mocker.patch("pygitgo.commands.state.run_command", return_value="Saved working directory")
160
159
  fake_success = mocker.patch("pygitgo.commands.state.success")
161
160
 
162
161
  save_state()
163
162
 
164
- fake_run.assert_called_once_with(["git", "stash", "push", "-m", "Auto-Save"])
163
+ fake_run.assert_called_once_with(["git", "stash", "push", "-m", "Auto-Save"], allow_fail=True)
165
164
  fake_success.assert_called_once_with("\nState 'Auto-Save' saved successfully.\n")
166
165
 
167
166
  def test_save_state_with_name(mocker):
168
- fake_run = mocker.patch("pygitgo.commands.state.run_command")
167
+ fake_run = mocker.patch("pygitgo.commands.state.run_command", return_value="Saved working directory")
169
168
  fake_success = mocker.patch("pygitgo.commands.state.success")
170
169
 
171
170
  save_state("My-State")
172
171
 
173
- fake_run.assert_called_once_with(["git", "stash", "push", "-m", "My-State"])
172
+ fake_run.assert_called_once_with(["git", "stash", "push", "-m", "My-State"], allow_fail=True)
174
173
  fake_success.assert_called_once_with("\nState 'My-State' saved successfully.\n")
175
174
 
176
175
 
@@ -0,0 +1,94 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ from pygitgo.commands.undo import undo_commit, undo_add, undo_changes, undo_operations
4
+ from pygitgo.exceptions import GitCommandError
5
+ from argparse import Namespace
6
+
7
+ @patch("pygitgo.commands.undo.run_command")
8
+ @patch("pygitgo.commands.undo.success")
9
+ def test_undo_commit_success(mock_success, mock_run_command):
10
+ undo_commit()
11
+ mock_run_command.assert_called_once_with(["git", "reset", "--soft", "HEAD~"])
12
+ mock_success.assert_called_once()
13
+
14
+
15
+ @patch("pygitgo.commands.undo.sys.exit")
16
+ @patch("pygitgo.commands.undo.error")
17
+ @patch("pygitgo.commands.undo.run_command")
18
+ def test_undo_commit_failure(mock_run_command, mock_error, mock_exit):
19
+ mock_run_command.side_effect = GitCommandError(["git", "reset", "--soft", "HEAD~"])
20
+ undo_commit()
21
+ mock_run_command.assert_called_once()
22
+ assert mock_error.call_count == 2
23
+ mock_exit.assert_called_once_with(1)
24
+
25
+
26
+ @patch("pygitgo.commands.undo.run_command")
27
+ @patch("pygitgo.commands.undo.success")
28
+ def test_undo_add_success(mock_success, mock_run_command):
29
+ undo_add()
30
+ mock_run_command.assert_called_once_with(["git", "reset", "HEAD"])
31
+ mock_success.assert_called_once()
32
+
33
+
34
+ @patch("pygitgo.commands.undo.sys.exit")
35
+ @patch("pygitgo.commands.undo.error")
36
+ @patch("pygitgo.commands.undo.run_command")
37
+ def test_undo_add_failure(mock_run_command, mock_error, mock_exit):
38
+ mock_run_command.side_effect = GitCommandError(["git", "reset", "HEAD"])
39
+ undo_add()
40
+ mock_run_command.assert_called_once()
41
+ mock_error.assert_called_once()
42
+ mock_exit.assert_called_once_with(1)
43
+
44
+
45
+ @patch("pygitgo.commands.undo.run_command")
46
+ @patch("pygitgo.commands.undo.success")
47
+ @patch("pygitgo.commands.undo.input", return_value="y")
48
+ def test_undo_changes_success(mock_input, mock_success, mock_run_command):
49
+ undo_changes()
50
+ mock_input.assert_called_once()
51
+ assert mock_run_command.call_count == 2
52
+ mock_run_command.assert_any_call(["git", "reset", "--hard", "HEAD"], loading_msg="Throwing away edits...")
53
+ mock_run_command.assert_any_call(["git", "clean", "-fd"], loading_msg="Removing new files...")
54
+ mock_success.assert_called_once()
55
+
56
+
57
+ @patch("pygitgo.commands.undo.run_command")
58
+ @patch("pygitgo.commands.undo.info")
59
+ @patch("pygitgo.commands.undo.input", return_value="n")
60
+ def test_undo_changes_abort(mock_input, mock_info, mock_run_command):
61
+ undo_changes()
62
+ mock_input.assert_called_once()
63
+ mock_info.assert_called_once()
64
+ mock_run_command.assert_not_called()
65
+
66
+
67
+ @patch("pygitgo.commands.undo.undo_commit")
68
+ def test_undo_operations_commit(mock_undo_commit):
69
+ args = Namespace(action="commit")
70
+ undo_operations(args)
71
+ mock_undo_commit.assert_called_once()
72
+
73
+
74
+ @patch("pygitgo.commands.undo.undo_add")
75
+ def test_undo_operations_add(mock_undo_add):
76
+ args = Namespace(action="add")
77
+ undo_operations(args)
78
+ mock_undo_add.assert_called_once()
79
+
80
+
81
+ @patch("pygitgo.commands.undo.undo_changes")
82
+ def test_undo_operations_changes(mock_undo_changes):
83
+ args = Namespace(action="changes")
84
+ undo_operations(args)
85
+ mock_undo_changes.assert_called_once()
86
+
87
+
88
+ @patch("pygitgo.commands.undo.sys.exit")
89
+ @patch("pygitgo.commands.undo.error")
90
+ def test_undo_operations_invalid(mock_error, mock_exit):
91
+ args = Namespace(action="invalid")
92
+ undo_operations(args)
93
+ mock_error.assert_called_once()
94
+ mock_exit.assert_called_once_with(1)
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
- from pygitgo.utils.executor import run_command
2
1
  from pygitgo.utils.colors import info, success, warning, error, BLUE, RESET
2
+ from pygitgo.utils.executor import run_command
3
3
  import subprocess
4
4
 
5
5
 
@@ -1,5 +1,5 @@
1
- from pygitgo.utils.executor import run_command
2
1
  from pygitgo.utils.colors import info, success, warning, error
2
+ from pygitgo.utils.executor import run_command
3
3
  from . import ssh_utils
4
4
  import os
5
5
 
File without changes