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.
- {pygitgo-1.3.0/src/pygitgo.egg-info → pygitgo-1.5.0}/PKG-INFO +13 -1
- {pygitgo-1.3.0 → pygitgo-1.5.0}/README.md +11 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/pyproject.toml +2 -1
- pygitgo-1.5.0/src/pygitgo/commands/pull.py +43 -0
- pygitgo-1.5.0/src/pygitgo/commands/staging.py +50 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/main.py +62 -16
- {pygitgo-1.3.0 → pygitgo-1.5.0/src/pygitgo.egg-info}/PKG-INFO +13 -1
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/SOURCES.txt +4 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/requires.txt +1 -0
- pygitgo-1.5.0/tests/test_pull.py +77 -0
- pygitgo-1.5.0/tests/test_staging.py +127 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/LICENSE +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/setup.cfg +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/__init__.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/__init__.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/account.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/manager.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/auth/ssh_utils.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/__init__.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/git_operations.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/jump.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/state.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/commands/undo.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/exceptions.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/__init__.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/colors.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/config.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/executor.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/platform_utils.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo/utils/setup.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/dependency_links.txt +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/entry_points.txt +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/src/pygitgo.egg-info/top_level.txt +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_config.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_git_operations.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_jump.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_main.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_platform.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_setup.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_state.py +0 -0
- {pygitgo-1.3.0 → pygitgo-1.5.0}/tests/test_undo.py +0 -0
- {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
|
+
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.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
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
|
|
@@ -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
|
|
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
|
|
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
|
|
File without changes
|