pygitgo 1.3.0__tar.gz → 1.5.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 (42) hide show
  1. {pygitgo-1.3.0/src/pygitgo.egg-info → pygitgo-1.5.0}/PKG-INFO +13 -1
  2. {pygitgo-1.3.0 → pygitgo-1.5.0}/README.md +11 -0
  3. {pygitgo-1.3.0 → pygitgo-1.5.0}/pyproject.toml +2 -1
  4. pygitgo-1.5.0/src/pygitgo/commands/pull.py +43 -0
  5. pygitgo-1.5.0/src/pygitgo/commands/staging.py +50 -0
  6. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/main.py +62 -16
  7. {pygitgo-1.3.0 → pygitgo-1.5.0/src/pygitgo.egg-info}/PKG-INFO +13 -1
  8. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/SOURCES.txt +4 -0
  9. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/requires.txt +1 -0
  10. pygitgo-1.5.0/tests/test_pull.py +77 -0
  11. pygitgo-1.5.0/tests/test_staging.py +127 -0
  12. {pygitgo-1.3.0 → pygitgo-1.5.0}/LICENSE +0 -0
  13. {pygitgo-1.3.0 → pygitgo-1.5.0}/setup.cfg +0 -0
  14. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/__init__.py +0 -0
  15. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/__init__.py +0 -0
  16. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/account.py +0 -0
  17. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/manager.py +0 -0
  18. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/ssh_utils.py +0 -0
  19. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/__init__.py +0 -0
  20. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/git_operations.py +0 -0
  21. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/jump.py +0 -0
  22. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/state.py +0 -0
  23. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/undo.py +0 -0
  24. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/exceptions.py +0 -0
  25. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/__init__.py +0 -0
  26. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/colors.py +0 -0
  27. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/config.py +0 -0
  28. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/executor.py +0 -0
  29. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/platform_utils.py +0 -0
  30. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/setup.py +0 -0
  31. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/dependency_links.txt +0 -0
  32. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/entry_points.txt +0 -0
  33. {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/top_level.txt +0 -0
  34. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_config.py +0 -0
  35. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_git_operations.py +0 -0
  36. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_jump.py +0 -0
  37. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_main.py +0 -0
  38. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_platform.py +0 -0
  39. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_setup.py +0 -0
  40. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_state.py +0 -0
  41. {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_undo.py +0 -0
  42. {pygitgo-1.3.0 → pygitgo-1.5.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.3.0
3
+ Version: 1.5.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
@@ -16,6 +16,7 @@ Requires-Python: >=3.8
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: yaspin>=3.0
19
+ Requires-Dist: pick>=2.0
19
20
  Provides-Extra: dev
20
21
  Requires-Dist: pytest>=7.0; extra == "dev"
21
22
  Requires-Dist: pytest-mock>=3.10; extra == "dev"
@@ -182,6 +183,7 @@ Stages all changes, commits, and pushes in one command.
182
183
  ```bash
183
184
  gitgo push [branch] [message]
184
185
  gitgo push -n [branch] [message] # create new branch first
186
+ gitgo push -s [branch] [message] # interactively select files to stage
185
187
  ```
186
188
 
187
189
  > [!TIP]
@@ -190,9 +192,19 @@ gitgo push -n [branch] [message] # create new branch first
190
192
  | Flag | Description |
191
193
  |------|-------------|
192
194
  | `-n`, `--new` | Create a new branch before pushing |
195
+ | `-s`, `--select` | Interactively select which files you want to include in this push |
193
196
 
194
197
  If there are no new changes but unpushed commits exist, GitGo detects this and pushes without creating an empty commit.
195
198
 
199
+ ### `gitgo pull`
200
+
201
+ Safely downloads updates from the remote server. It automatically stashes your messy code, pulls the updates cleanly underneath using a rebase, and then instantly puts your code back exactly how it was.
202
+
203
+ ```bash
204
+ gitgo pull # Safely pull updates for your current branch
205
+ gitgo pull <branch> # Safely pull updates from a specific branch
206
+ ```
207
+
196
208
  ### `gitgo link`
197
209
 
198
210
  Initializes a Git repository in the current directory, connects it to a remote, and pushes. Handles already-initialized repos gracefully and pulls unrelated histories.
@@ -159,6 +159,7 @@ Stages all changes, commits, and pushes in one command.
159
159
  ```bash
160
160
  gitgo push [branch] [message]
161
161
  gitgo push -n [branch] [message] # create new branch first
162
+ gitgo push -s [branch] [message] # interactively select files to stage
162
163
  ```
163
164
 
164
165
  > [!TIP]
@@ -167,9 +168,19 @@ gitgo push -n [branch] [message] # create new branch first
167
168
  | Flag | Description |
168
169
  |------|-------------|
169
170
  | `-n`, `--new` | Create a new branch before pushing |
171
+ | `-s`, `--select` | Interactively select which files you want to include in this push |
170
172
 
171
173
  If there are no new changes but unpushed commits exist, GitGo detects this and pushes without creating an empty commit.
172
174
 
175
+ ### `gitgo pull`
176
+
177
+ Safely downloads updates from the remote server. It automatically stashes your messy code, pulls the updates cleanly underneath using a rebase, and then instantly puts your code back exactly how it was.
178
+
179
+ ```bash
180
+ gitgo pull # Safely pull updates for your current branch
181
+ gitgo pull <branch> # Safely pull updates from a specific branch
182
+ ```
183
+
173
184
  ### `gitgo link`
174
185
 
175
186
  Initializes a Git repository in the current directory, connects it to a remote, and pushes. Handles already-initialized repos gracefully and pulls unrelated histories.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitgo"
7
- version = "1.3.0"
7
+ version = "1.5.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"}
@@ -22,6 +22,7 @@ classifiers = [
22
22
  ]
23
23
  dependencies = [
24
24
  "yaspin>=3.0",
25
+ "pick>=2.0",
25
26
  ]
26
27
 
27
28
  [project.optional-dependencies]
@@ -0,0 +1,43 @@
1
+ from pygitgo.utils.colors import success, warning, error, info
2
+ from pygitgo.utils.executor import run_command
3
+ from pygitgo.exceptions import GitCommandError
4
+ from pygitgo.commands.git_operations import get_current_branch
5
+ import subprocess
6
+ import sys
7
+
8
+ def pull_operation(args):
9
+ branch = args.branch
10
+
11
+ if not branch:
12
+ branch = get_current_branch()
13
+ info(f"No branch provided. Pulling latest updates for '{branch}'...\n")
14
+
15
+ try:
16
+ remote_refs = run_command(["git", "ls-remote", "--heads", "origin", branch], allow_fail=True)
17
+ if isinstance(remote_refs, subprocess.CalledProcessError) or not remote_refs.strip():
18
+ error(f"\nFailed! The branch '{branch}' does not exist on the remote server.")
19
+ warning("You might need to push your local branch first.\n")
20
+ sys.exit(1)
21
+
22
+ run_command(
23
+ ["git", "pull", "--rebase", "--autostash", "origin", branch],
24
+ loading_msg=f"Downloading latest updates for '{branch}' (auto-saving your code)..."
25
+ )
26
+
27
+ success(f"\nSuccess! Your project is now up to date with '{branch}'.\n")
28
+
29
+ except GitCommandError as e:
30
+ error_msg = str(e).lower()
31
+ if "conflict" in error_msg or "rebase in progress" in error_msg:
32
+ error("\nMERGE CONFLICT DETECTED!")
33
+ warning("Your local code clashes with the new code downloaded from the server.\n")
34
+ info("1. Open your code editor down below.")
35
+ info("2. Fix the red conflict lines in your files.")
36
+ info("3. Save the files.")
37
+ info("4. Run this command to finish: git rebase --continue\n")
38
+ sys.exit(1)
39
+ else:
40
+ error("\nFailed to pull updates from the server!")
41
+ error("Please check your internet connection and try again.")
42
+ error(f"Details: {e}\n")
43
+ sys.exit(1)
@@ -0,0 +1,50 @@
1
+ from pygitgo.utils.colors import success, warning, error
2
+ from pygitgo.utils.executor import run_command
3
+ from pick import pick
4
+ import subprocess
5
+ import sys
6
+
7
+
8
+ STATUS_LABELS = {
9
+ "M": "modified",
10
+ "A": "added",
11
+ "D": "deleted",
12
+ "R": "renamed",
13
+ "??": "new file",
14
+ }
15
+
16
+
17
+ def get_changed_files():
18
+ status = run_command(["git", "status", "--porcelain"], allow_fail=True)
19
+ if isinstance(status, subprocess.CalledProcessError) or not status.strip():
20
+ return []
21
+
22
+ files = []
23
+ for line in status.strip().splitlines():
24
+ if len(line) < 3:
25
+ continue
26
+ status_code = line[:2].strip()
27
+ filepath = line[3:].strip()
28
+ label = STATUS_LABELS.get(status_code, "changed")
29
+ files.append({"status": status_code, "label": label, "path": filepath})
30
+
31
+ return files
32
+
33
+
34
+ def display_file_picker(files):
35
+ options = [f"({f['label']}) {f['path']}" for f in files]
36
+
37
+ selected = pick(
38
+ options,
39
+ "Select files to stage (SPACE to toggle, ENTER to confirm):",
40
+ multiselect=True,
41
+ min_selection_count=0
42
+ )
43
+
44
+ return [files[idx]["path"] for _, idx in selected]
45
+
46
+
47
+ def selective_stage(selected_files):
48
+ for filepath in selected_files:
49
+ run_command(["git", "add", "--", filepath])
50
+ success(f"\n{len(selected_files)} file(s) staged for commit.")
@@ -2,12 +2,14 @@ from pygitgo.commands.git_operations import (
2
2
  git_new_branch, git_commit, git_init, add_remote_origin,
3
3
  confirm_remote_link, git_push, get_current_branch, is_branch_exist
4
4
  )
5
+ from pygitgo.commands.staging import get_changed_files, display_file_picker, selective_stage
5
6
  from pygitgo.utils.colors import info, success, warning, error
6
7
  from pygitgo.utils.config import get_config, config_operation
7
8
  from pygitgo.exceptions import GitCommandError, GitGoError
8
9
  from pygitgo.utils.setup import ensure_first_run_setup
9
10
  from pygitgo.commands.state import state_operations
10
11
  from pygitgo.commands.undo import undo_operations
12
+ from pygitgo.commands.pull import pull_operation
11
13
  from pygitgo.commands.jump import jump_operation
12
14
  from pygitgo.utils.executor import run_command
13
15
  from pygitgo.auth.manager import login, logout
@@ -99,6 +101,7 @@ def link_operation(args):
99
101
  def push_operation(args):
100
102
  branch = args.branch
101
103
  message = args.message
104
+ select = args.select if hasattr(args, 'select') else False
102
105
 
103
106
  if args.new:
104
107
  if not branch:
@@ -128,24 +131,52 @@ def push_operation(args):
128
131
  message = get_config("default-message", "New Project Update")
129
132
  info(f"No commit message provided. Using default: '{message}'\n")
130
133
 
131
- commit_made = git_commit(message)
132
-
133
- if commit_made:
134
+ if select:
135
+ files = get_changed_files()
136
+ if not files:
137
+ info("\nWorking tree is clean. Nothing to select.")
138
+ warning("Make some changes first before using GitGo to commit and push.")
139
+ return
140
+
141
+ selected = display_file_picker(files)
142
+ if not selected:
143
+ info("\nNo files selected. Push aborted.\n")
144
+ return
145
+
146
+ selective_stage(selected)
147
+ clean_message = message.strip('"\'')
148
+ run_command(["git", "commit", "-m", clean_message], loading_msg="Commiting changes...")
134
149
  git_push(branch)
150
+
151
+ leftover_files = [f for f in files if f["path"] not in selected]
152
+ if leftover_files:
153
+ print()
154
+ warning(f"You have {len(leftover_files)} uncommitted file(s) left in your working directory:")
155
+ for f in leftover_files:
156
+ info(f" - {f['path']}")
157
+ print()
158
+ save_choice = input("Would you like to save these leftovers to a GitGo State for later? (y/n): ").lower()
159
+ if save_choice == 'y':
160
+ state_operations(Namespace(action="save", identifier=f"Leftovers from: {clean_message}"))
135
161
  else:
136
- try:
137
- unpushed = run_command(["git", "log", "--oneline", f"origin/{branch}..HEAD"], allow_fail=True, loading_msg="Checking for unpushed commits...")
138
- if not isinstance(unpushed, subprocess.CalledProcessError) and unpushed.strip():
139
- warning("\nNo changes to commit, but found unpushed commits. Pushing to remote...")
140
- git_push(branch)
141
- else:
142
- info("\nWorking tree is clean and everything is up to date.")
143
- warning("Make some changes first before using GitGo to commit and push.")
162
+ commit_made = git_commit(message)
163
+
164
+ if commit_made:
165
+ git_push(branch)
166
+ else:
167
+ try:
168
+ unpushed = run_command(["git", "log", "--oneline", f"origin/{branch}..HEAD"], allow_fail=True, loading_msg="Checking for unpushed commits...")
169
+ if not isinstance(unpushed, subprocess.CalledProcessError) and unpushed.strip():
170
+ warning("\nNo changes to commit, but found unpushed commits. Pushing to remote...")
171
+ git_push(branch)
172
+ else:
173
+ info("\nWorking tree is clean and everything is up to date.")
174
+ warning("Make some changes first before using GitGo to commit and push.")
175
+ return
176
+ except (GitCommandError, Exception):
177
+ warning("\nNo changes to commit. Cannot verify remote status.")
178
+ warning("Make some changes first or check your git remote configuration.")
144
179
  return
145
- except (GitCommandError, Exception):
146
- warning("\nNo changes to commit. Cannot verify remote status.")
147
- warning("Make some changes first or check your git remote configuration.")
148
- return
149
180
 
150
181
  print("\n" + ("=" * 90))
151
182
  success("MISSION COMPLETE — NO CASUALTIES. ALL TARGETS NEUTRALIZED.\nAWAITING FOR YOUR NEXT ORDERS.\n\n")
@@ -215,11 +246,13 @@ def main():
215
246
  " gitgo push Push current branch with default message\n"
216
247
  " gitgo push main 'fix auth bug' Push to main with a custom message\n"
217
248
  " gitgo push 'fix auth bug' Push current branch with a custom message\n"
218
- " gitgo push -n feature/login 'add login' Create new branch and push"
249
+ " gitgo push -n feature/login 'add login' Create new branch and push\n"
250
+ " gitgo push -s main 'fix bug' Pick which files to include before pushing"
219
251
  ),
220
252
  formatter_class=argparse.RawDescriptionHelpFormatter
221
253
  )
222
254
  push_parser.add_argument("-n", "--new", action="store_true", help="Create a new branch before pushing")
255
+ push_parser.add_argument("-s", "--select", action="store_true", help="Interactively select which files to stage")
223
256
  push_parser.add_argument("branch", nargs="?", default=None, help="Branch to push to (default: current branch)")
224
257
  push_parser.add_argument("message", nargs="?", default=None, help="Commit message")
225
258
 
@@ -282,6 +315,17 @@ def main():
282
315
  help="What to undo: 'commit', 'add', or 'changes'"
283
316
  )
284
317
 
318
+ pull_parser = subparsers.add_parser("pull",
319
+ help="Safely pull the latest code without losing your changes",
320
+ epilog=(
321
+ "Examples:\n"
322
+ " gitgo pull Safely pull updates for your current branch\n"
323
+ " gitgo pull main Safely pull updates from the 'main' branch\n"
324
+ ),
325
+ formatter_class=argparse.RawDescriptionHelpFormatter
326
+ )
327
+ pull_parser.add_argument("branch", nargs="?", default=None, help="The branch to pull from (default is your current branch)")
328
+
285
329
  args = parser.parse_args()
286
330
 
287
331
  if args.ready:
@@ -309,6 +353,8 @@ def main():
309
353
  config_operation(args)
310
354
  elif args.command == "undo":
311
355
  undo_operations(args)
356
+ elif args.command == "pull":
357
+ pull_operation(args)
312
358
  except GitGoError as e:
313
359
  error(f"\n{e}\n")
314
360
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitgo
3
- Version: 1.3.0
3
+ Version: 1.5.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
@@ -16,6 +16,7 @@ Requires-Python: >=3.8
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: yaspin>=3.0
19
+ Requires-Dist: pick>=2.0
19
20
  Provides-Extra: dev
20
21
  Requires-Dist: pytest>=7.0; extra == "dev"
21
22
  Requires-Dist: pytest-mock>=3.10; extra == "dev"
@@ -182,6 +183,7 @@ Stages all changes, commits, and pushes in one command.
182
183
  ```bash
183
184
  gitgo push [branch] [message]
184
185
  gitgo push -n [branch] [message] # create new branch first
186
+ gitgo push -s [branch] [message] # interactively select files to stage
185
187
  ```
186
188
 
187
189
  > [!TIP]
@@ -190,9 +192,19 @@ gitgo push -n [branch] [message] # create new branch first
190
192
  | Flag | Description |
191
193
  |------|-------------|
192
194
  | `-n`, `--new` | Create a new branch before pushing |
195
+ | `-s`, `--select` | Interactively select which files you want to include in this push |
193
196
 
194
197
  If there are no new changes but unpushed commits exist, GitGo detects this and pushes without creating an empty commit.
195
198
 
199
+ ### `gitgo pull`
200
+
201
+ Safely downloads updates from the remote server. It automatically stashes your messy code, pulls the updates cleanly underneath using a rebase, and then instantly puts your code back exactly how it was.
202
+
203
+ ```bash
204
+ gitgo pull # Safely pull updates for your current branch
205
+ gitgo pull <branch> # Safely pull updates from a specific branch
206
+ ```
207
+
196
208
  ### `gitgo link`
197
209
 
198
210
  Initializes a Git repository in the current directory, connects it to a remote, and pushes. Handles already-initialized repos gracefully and pulls unrelated histories.
@@ -17,6 +17,8 @@ src/pygitgo/auth/ssh_utils.py
17
17
  src/pygitgo/commands/__init__.py
18
18
  src/pygitgo/commands/git_operations.py
19
19
  src/pygitgo/commands/jump.py
20
+ src/pygitgo/commands/pull.py
21
+ src/pygitgo/commands/staging.py
20
22
  src/pygitgo/commands/state.py
21
23
  src/pygitgo/commands/undo.py
22
24
  src/pygitgo/utils/__init__.py
@@ -30,7 +32,9 @@ tests/test_git_operations.py
30
32
  tests/test_jump.py
31
33
  tests/test_main.py
32
34
  tests/test_platform.py
35
+ tests/test_pull.py
33
36
  tests/test_setup.py
37
+ tests/test_staging.py
34
38
  tests/test_state.py
35
39
  tests/test_undo.py
36
40
  tests/test_url_validator.py
@@ -1,4 +1,5 @@
1
1
  yaspin>=3.0
2
+ pick>=2.0
2
3
 
3
4
  [dev]
4
5
  pytest>=7.0
@@ -0,0 +1,77 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ from pygitgo.commands.pull import pull_operation
4
+ from pygitgo.exceptions import GitCommandError
5
+ from argparse import Namespace
6
+ import subprocess
7
+
8
+ @patch("pygitgo.commands.pull.run_command")
9
+ @patch("pygitgo.commands.pull.success")
10
+ @patch("pygitgo.commands.pull.get_current_branch", return_value="main")
11
+ def test_pull_operation_success_no_branch(mock_get_branch, mock_success, mock_run_command):
12
+ mock_run_command.side_effect = [
13
+ "1234567890abcdef",
14
+ None
15
+ ]
16
+
17
+ args = Namespace(branch=None)
18
+ pull_operation(args)
19
+
20
+ assert mock_run_command.call_count == 2
21
+ mock_run_command.assert_any_call(["git", "ls-remote", "--heads", "origin", "main"], allow_fail=True)
22
+ mock_run_command.assert_any_call(
23
+ ["git", "pull", "--rebase", "--autostash", "origin", "main"],
24
+ loading_msg="Downloading latest updates for 'main' (auto-saving your code)..."
25
+ )
26
+ mock_success.assert_called_once()
27
+
28
+ @patch("pygitgo.commands.pull.run_command")
29
+ @patch("pygitgo.commands.pull.success")
30
+ def test_pull_operation_success_with_branch(mock_success, mock_run_command):
31
+ mock_run_command.side_effect = [
32
+ "1234567890abcdef",
33
+ None
34
+ ]
35
+
36
+ args = Namespace(branch="feature/test")
37
+ pull_operation(args)
38
+
39
+ assert mock_run_command.call_count == 2
40
+ mock_run_command.assert_any_call(["git", "ls-remote", "--heads", "origin", "feature/test"], allow_fail=True)
41
+ mock_run_command.assert_any_call(
42
+ ["git", "pull", "--rebase", "--autostash", "origin", "feature/test"],
43
+ loading_msg="Downloading latest updates for 'feature/test' (auto-saving your code)..."
44
+ )
45
+ mock_success.assert_called_once()
46
+
47
+ @patch("pygitgo.commands.pull.sys.exit")
48
+ @patch("pygitgo.commands.pull.error")
49
+ @patch("pygitgo.commands.pull.run_command")
50
+ def test_pull_operation_branch_not_found(mock_run_command, mock_error, mock_exit):
51
+ mock_run_command.return_value = subprocess.CalledProcessError(128, ["git", "ls-remote"])
52
+
53
+ args = Namespace(branch="does-not-exist")
54
+ pull_operation(args)
55
+
56
+ mock_exit.assert_called_once_with(1)
57
+ mock_error.assert_called()
58
+
59
+ @patch("pygitgo.commands.pull.sys.exit")
60
+ @patch("pygitgo.commands.pull.error")
61
+ @patch("pygitgo.commands.pull.run_command")
62
+ def test_pull_operation_merge_conflict(mock_run_command, mock_error, mock_exit):
63
+ conflict_err = GitCommandError(["git", "pull"], stderr="merge conflict in file.py")
64
+
65
+ def side_effect_fn(*args, **kwargs):
66
+ cmd = args[0]
67
+ if cmd[1] == "ls-remote":
68
+ return "1234567890abcdef"
69
+ raise conflict_err
70
+
71
+ mock_run_command.side_effect = side_effect_fn
72
+
73
+ args = Namespace(branch="main")
74
+ pull_operation(args)
75
+
76
+ mock_exit.assert_called_once_with(1)
77
+ mock_error.assert_any_call("\nMERGE CONFLICT DETECTED!")
@@ -0,0 +1,127 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ from pygitgo.commands.staging import get_changed_files, display_file_picker, selective_stage
4
+ from argparse import Namespace
5
+ import subprocess
6
+
7
+ @patch("pygitgo.commands.staging.run_command")
8
+ def test_get_changed_files_with_changes(mock_run_command):
9
+ mock_run_command.return_value = "M src/main.py\n?? new_file.txt\nM README.md"
10
+
11
+ files = get_changed_files()
12
+
13
+ assert len(files) == 3
14
+ assert files[0]["path"] == "src/main.py"
15
+ assert files[0]["label"] == "modified"
16
+ assert files[1]["path"] == "new_file.txt"
17
+ assert files[1]["label"] == "new file"
18
+ assert files[2]["path"] == "README.md"
19
+ assert files[2]["label"] == "modified"
20
+
21
+
22
+ @patch("pygitgo.commands.staging.run_command")
23
+ def test_get_changed_files_no_changes(mock_run_command):
24
+ mock_run_command.return_value = subprocess.CalledProcessError(1, ["git", "status"])
25
+
26
+ files = get_changed_files()
27
+
28
+ assert files == []
29
+
30
+
31
+ @patch("pygitgo.commands.staging.run_command")
32
+ def test_get_changed_files_empty_status(mock_run_command):
33
+ mock_run_command.return_value = ""
34
+
35
+ files = get_changed_files()
36
+
37
+ assert files == []
38
+
39
+
40
+ @patch("pygitgo.commands.staging.run_command")
41
+ def test_get_changed_files_deleted_file(mock_run_command):
42
+ mock_run_command.return_value = "D old_file.py"
43
+
44
+ files = get_changed_files()
45
+
46
+ assert len(files) == 1
47
+ assert files[0]["path"] == "old_file.py"
48
+ assert files[0]["label"] == "deleted"
49
+
50
+
51
+ @patch("pygitgo.commands.staging.pick")
52
+ def test_display_file_picker_select_specific(mock_pick):
53
+ mock_pick.return_value = [("(modified) src/main.py", 0), ("(modified) README.md", 2)]
54
+
55
+ files = [
56
+ {"status": "M", "label": "modified", "path": "src/main.py"},
57
+ {"status": "??", "label": "new file", "path": "new_file.txt"},
58
+ {"status": "M", "label": "modified", "path": "README.md"},
59
+ ]
60
+
61
+ selected = display_file_picker(files)
62
+
63
+ assert selected == ["src/main.py", "README.md"]
64
+ mock_pick.assert_called_once()
65
+
66
+
67
+ @patch("pygitgo.commands.staging.pick")
68
+ def test_display_file_picker_select_all(mock_pick):
69
+ mock_pick.return_value = [("(modified) src/main.py", 0), ("(new file) new_file.txt", 1)]
70
+
71
+ files = [
72
+ {"status": "M", "label": "modified", "path": "src/main.py"},
73
+ {"status": "??", "label": "new file", "path": "new_file.txt"},
74
+ ]
75
+
76
+ selected = display_file_picker(files)
77
+
78
+ assert selected == ["src/main.py", "new_file.txt"]
79
+
80
+
81
+ @patch("pygitgo.commands.staging.pick")
82
+ def test_display_file_picker_cancel(mock_pick):
83
+ mock_pick.return_value = []
84
+
85
+ files = [
86
+ {"status": "M", "label": "modified", "path": "src/main.py"},
87
+ ]
88
+
89
+ selected = display_file_picker(files)
90
+
91
+ assert selected == []
92
+
93
+
94
+ @patch("pygitgo.commands.staging.pick")
95
+ def test_display_file_picker_select_single(mock_pick):
96
+ mock_pick.return_value = [("(new file) new_file.txt", 1)]
97
+
98
+ files = [
99
+ {"status": "M", "label": "modified", "path": "src/main.py"},
100
+ {"status": "??", "label": "new file", "path": "new_file.txt"},
101
+ ]
102
+
103
+ selected = display_file_picker(files)
104
+
105
+ assert selected == ["new_file.txt"]
106
+
107
+
108
+ @patch("pygitgo.commands.staging.run_command")
109
+ @patch("pygitgo.commands.staging.success")
110
+ def test_selective_stage(mock_success, mock_run_command):
111
+ selected_files = ["src/main.py", "README.md"]
112
+
113
+ selective_stage(selected_files)
114
+
115
+ assert mock_run_command.call_count == 2
116
+ mock_run_command.assert_any_call(["git", "add", "--", "src/main.py"])
117
+ mock_run_command.assert_any_call(["git", "add", "--", "README.md"])
118
+ mock_success.assert_called_once()
119
+
120
+
121
+ @patch("pygitgo.commands.staging.run_command")
122
+ @patch("pygitgo.commands.staging.success")
123
+ def test_selective_stage_single_file(mock_success, mock_run_command):
124
+ selective_stage(["README.md"])
125
+
126
+ mock_run_command.assert_called_once_with(["git", "add", "--", "README.md"])
127
+ mock_success.assert_called_once()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes