pygitgo 1.4.0__tar.gz → 1.6.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 (44) hide show
  1. {pygitgo-1.4.0/src/pygitgo.egg-info → pygitgo-1.6.0}/PKG-INFO +19 -1
  2. {pygitgo-1.4.0 → pygitgo-1.6.0}/README.md +17 -0
  3. {pygitgo-1.4.0 → pygitgo-1.6.0}/pyproject.toml +2 -1
  4. pygitgo-1.6.0/src/pygitgo/commands/staging.py +50 -0
  5. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/main.py +51 -17
  6. pygitgo-1.6.0/src/pygitgo/utils/update_checker.py +97 -0
  7. {pygitgo-1.4.0 → pygitgo-1.6.0/src/pygitgo.egg-info}/PKG-INFO +19 -1
  8. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo.egg-info/SOURCES.txt +4 -0
  9. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo.egg-info/requires.txt +1 -0
  10. pygitgo-1.6.0/tests/test_staging.py +127 -0
  11. pygitgo-1.6.0/tests/test_update_checker.py +143 -0
  12. {pygitgo-1.4.0 → pygitgo-1.6.0}/LICENSE +0 -0
  13. {pygitgo-1.4.0 → pygitgo-1.6.0}/setup.cfg +0 -0
  14. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/__init__.py +0 -0
  15. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/auth/__init__.py +0 -0
  16. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/auth/account.py +0 -0
  17. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/auth/manager.py +0 -0
  18. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/auth/ssh_utils.py +0 -0
  19. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/commands/__init__.py +0 -0
  20. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/commands/git_operations.py +0 -0
  21. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/commands/jump.py +0 -0
  22. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/commands/pull.py +0 -0
  23. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/commands/state.py +0 -0
  24. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/commands/undo.py +0 -0
  25. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/exceptions.py +0 -0
  26. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/utils/__init__.py +0 -0
  27. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/utils/colors.py +0 -0
  28. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/utils/config.py +0 -0
  29. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/utils/executor.py +0 -0
  30. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/utils/platform_utils.py +0 -0
  31. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo/utils/setup.py +0 -0
  32. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo.egg-info/dependency_links.txt +0 -0
  33. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo.egg-info/entry_points.txt +0 -0
  34. {pygitgo-1.4.0 → pygitgo-1.6.0}/src/pygitgo.egg-info/top_level.txt +0 -0
  35. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_config.py +0 -0
  36. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_git_operations.py +0 -0
  37. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_jump.py +0 -0
  38. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_main.py +0 -0
  39. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_platform.py +0 -0
  40. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_pull.py +0 -0
  41. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_setup.py +0 -0
  42. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_state.py +0 -0
  43. {pygitgo-1.4.0 → pygitgo-1.6.0}/tests/test_undo.py +0 -0
  44. {pygitgo-1.4.0 → pygitgo-1.6.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.4.0
3
+ Version: 1.6.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"
@@ -30,6 +31,7 @@ Dynamic: license-file
30
31
  [![PyPI Downloads](https://img.shields.io/pypi/dm/pygitgo?color=blue)](https://pypi.org/project/pygitgo)
31
32
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
32
33
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
34
+ [![Ko-fi](https://img.shields.io/badge/Support-Ko--fi-FF5E5B?logo=ko-fi&logoColor=white)](https://ko-fi.com/huerte)
33
35
  [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Termux-lightgrey)](https://github.com/Huerte/GitGo)
34
36
 
35
37
  **Stop typing the same five Git commands. Run one instead.**
@@ -80,6 +82,7 @@ GitGo provides a CLI environment designed for faster and simpler Git workflows.
80
82
  - **Smart Branch Hopping:** Safely traverse branches with `jump`. Auto-stashes messy code and prevents merge conflict disasters with a Try-And-Revert safety engine.
81
83
  - **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.
82
84
  - **Custom Defaults:** Save your preferred branch name and commit message locally so you never have to type them again.
85
+ - **Auto-Update Checker:** Silently checks for newer versions in the background and notifies you, without slowing down your commands.
83
86
  - **SSH Auto-Setup:** Generates an SSH key, adds it to `ssh-agent`, and opens your GitHub settings automatically.
84
87
  - **HTTPS to SSH Conversion:** Silently converts the remote to SSH before pushing if your SSH is configured.
85
88
  - **Termux Compatibility:** Works natively on Android natively handling common issues like dubious ownership errors.
@@ -182,6 +185,7 @@ Stages all changes, commits, and pushes in one command.
182
185
  ```bash
183
186
  gitgo push [branch] [message]
184
187
  gitgo push -n [branch] [message] # create new branch first
188
+ gitgo push -s [branch] [message] # interactively select files to stage
185
189
  ```
186
190
 
187
191
  > [!TIP]
@@ -190,6 +194,7 @@ gitgo push -n [branch] [message] # create new branch first
190
194
  | Flag | Description |
191
195
  |------|-------------|
192
196
  | `-n`, `--new` | Create a new branch before pushing |
197
+ | `-s`, `--select` | Interactively select which files you want to include in this push |
193
198
 
194
199
  If there are no new changes but unpushed commits exist, GitGo detects this and pushes without creating an empty commit.
195
200
 
@@ -278,6 +283,7 @@ gitgo -r # verify GitGo is ready
278
283
 
279
284
  - **SSH Auto-Setup:** `gitgo user login` generates an `ed25519` SSH key, adds it to `ssh-agent`, prints the public key, and opens `github.com/settings/ssh/new`.
280
285
  - **HTTPS to SSH Conversion:** If your remote is set to HTTPS, GitGo converts the remote to SSH before pushing if SSH is configured. No `git remote set-url` is required.
286
+ - **Auto-Update Checker:** Spawns a non-blocking background thread on startup to query PyPI for newer versions. Results are cached locally for 7 days to prevent unnecessary network requests.
281
287
  - **Termux Compatibility:** Detects Termux via environment variables, adjusts binary locations (`$PREFIX/bin`), uses `termux-open` for browser actions, and natively handles the `detected dubious ownership` Git error.
282
288
  - **State Management:** `gitgo state` wraps `git stash` with named saves, indexed listing, and confirmation prompts.
283
289
 
@@ -308,6 +314,18 @@ Contributions are welcome and appreciated!
308
314
 
309
315
  ---
310
316
 
317
+ ## Support
318
+
319
+ If GitGo saves you time, consider buying me a coffee. It helps keep the project going.
320
+
321
+ <div align="center">
322
+ <a href="https://ko-fi.com/huerte">
323
+ <img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi" />
324
+ </a>
325
+ </div>
326
+
327
+ ---
328
+
311
329
  ## License
312
330
 
313
331
  Distributed under the **GPLv3** License. See [`LICENSE`](LICENSE) for details.
@@ -7,6 +7,7 @@
7
7
  [![PyPI Downloads](https://img.shields.io/pypi/dm/pygitgo?color=blue)](https://pypi.org/project/pygitgo)
8
8
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
9
9
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
10
+ [![Ko-fi](https://img.shields.io/badge/Support-Ko--fi-FF5E5B?logo=ko-fi&logoColor=white)](https://ko-fi.com/huerte)
10
11
  [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Termux-lightgrey)](https://github.com/Huerte/GitGo)
11
12
 
12
13
  **Stop typing the same five Git commands. Run one instead.**
@@ -57,6 +58,7 @@ GitGo provides a CLI environment designed for faster and simpler Git workflows.
57
58
  - **Smart Branch Hopping:** Safely traverse branches with `jump`. Auto-stashes messy code and prevents merge conflict disasters with a Try-And-Revert safety engine.
58
59
  - **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.
59
60
  - **Custom Defaults:** Save your preferred branch name and commit message locally so you never have to type them again.
61
+ - **Auto-Update Checker:** Silently checks for newer versions in the background and notifies you, without slowing down your commands.
60
62
  - **SSH Auto-Setup:** Generates an SSH key, adds it to `ssh-agent`, and opens your GitHub settings automatically.
61
63
  - **HTTPS to SSH Conversion:** Silently converts the remote to SSH before pushing if your SSH is configured.
62
64
  - **Termux Compatibility:** Works natively on Android natively handling common issues like dubious ownership errors.
@@ -159,6 +161,7 @@ Stages all changes, commits, and pushes in one command.
159
161
  ```bash
160
162
  gitgo push [branch] [message]
161
163
  gitgo push -n [branch] [message] # create new branch first
164
+ gitgo push -s [branch] [message] # interactively select files to stage
162
165
  ```
163
166
 
164
167
  > [!TIP]
@@ -167,6 +170,7 @@ gitgo push -n [branch] [message] # create new branch first
167
170
  | Flag | Description |
168
171
  |------|-------------|
169
172
  | `-n`, `--new` | Create a new branch before pushing |
173
+ | `-s`, `--select` | Interactively select which files you want to include in this push |
170
174
 
171
175
  If there are no new changes but unpushed commits exist, GitGo detects this and pushes without creating an empty commit.
172
176
 
@@ -255,6 +259,7 @@ gitgo -r # verify GitGo is ready
255
259
 
256
260
  - **SSH Auto-Setup:** `gitgo user login` generates an `ed25519` SSH key, adds it to `ssh-agent`, prints the public key, and opens `github.com/settings/ssh/new`.
257
261
  - **HTTPS to SSH Conversion:** If your remote is set to HTTPS, GitGo converts the remote to SSH before pushing if SSH is configured. No `git remote set-url` is required.
262
+ - **Auto-Update Checker:** Spawns a non-blocking background thread on startup to query PyPI for newer versions. Results are cached locally for 7 days to prevent unnecessary network requests.
258
263
  - **Termux Compatibility:** Detects Termux via environment variables, adjusts binary locations (`$PREFIX/bin`), uses `termux-open` for browser actions, and natively handles the `detected dubious ownership` Git error.
259
264
  - **State Management:** `gitgo state` wraps `git stash` with named saves, indexed listing, and confirmation prompts.
260
265
 
@@ -285,6 +290,18 @@ Contributions are welcome and appreciated!
285
290
 
286
291
  ---
287
292
 
293
+ ## Support
294
+
295
+ If GitGo saves you time, consider buying me a coffee. It helps keep the project going.
296
+
297
+ <div align="center">
298
+ <a href="https://ko-fi.com/huerte">
299
+ <img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi" />
300
+ </a>
301
+ </div>
302
+
303
+ ---
304
+
288
305
  ## License
289
306
 
290
307
  Distributed under the **GPLv3** License. See [`LICENSE`](LICENSE) for details.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitgo"
7
- version = "1.4.0"
7
+ version = "1.6.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,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,9 +2,11 @@ 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
9
+ from pygitgo.utils.update_checker import check_for_updates_background
8
10
  from pygitgo.utils.setup import ensure_first_run_setup
9
11
  from pygitgo.commands.state import state_operations
10
12
  from pygitgo.commands.undo import undo_operations
@@ -100,6 +102,7 @@ def link_operation(args):
100
102
  def push_operation(args):
101
103
  branch = args.branch
102
104
  message = args.message
105
+ select = args.select if hasattr(args, 'select') else False
103
106
 
104
107
  if args.new:
105
108
  if not branch:
@@ -129,24 +132,52 @@ def push_operation(args):
129
132
  message = get_config("default-message", "New Project Update")
130
133
  info(f"No commit message provided. Using default: '{message}'\n")
131
134
 
132
- commit_made = git_commit(message)
133
-
134
- if commit_made:
135
+ if select:
136
+ files = get_changed_files()
137
+ if not files:
138
+ info("\nWorking tree is clean. Nothing to select.")
139
+ warning("Make some changes first before using GitGo to commit and push.")
140
+ return
141
+
142
+ selected = display_file_picker(files)
143
+ if not selected:
144
+ info("\nNo files selected. Push aborted.\n")
145
+ return
146
+
147
+ selective_stage(selected)
148
+ clean_message = message.strip('"\'')
149
+ run_command(["git", "commit", "-m", clean_message], loading_msg="Commiting changes...")
135
150
  git_push(branch)
151
+
152
+ leftover_files = [f for f in files if f["path"] not in selected]
153
+ if leftover_files:
154
+ print()
155
+ warning(f"You have {len(leftover_files)} uncommitted file(s) left in your working directory:")
156
+ for f in leftover_files:
157
+ info(f" - {f['path']}")
158
+ print()
159
+ save_choice = input("Would you like to save these leftovers to a GitGo State for later? (y/n): ").lower()
160
+ if save_choice == 'y':
161
+ state_operations(Namespace(action="save", identifier=f"Leftovers from: {clean_message}"))
136
162
  else:
137
- try:
138
- unpushed = run_command(["git", "log", "--oneline", f"origin/{branch}..HEAD"], allow_fail=True, loading_msg="Checking for unpushed commits...")
139
- if not isinstance(unpushed, subprocess.CalledProcessError) and unpushed.strip():
140
- warning("\nNo changes to commit, but found unpushed commits. Pushing to remote...")
141
- git_push(branch)
142
- else:
143
- info("\nWorking tree is clean and everything is up to date.")
144
- warning("Make some changes first before using GitGo to commit and push.")
163
+ commit_made = git_commit(message)
164
+
165
+ if commit_made:
166
+ git_push(branch)
167
+ else:
168
+ try:
169
+ unpushed = run_command(["git", "log", "--oneline", f"origin/{branch}..HEAD"], allow_fail=True, loading_msg="Checking for unpushed commits...")
170
+ if not isinstance(unpushed, subprocess.CalledProcessError) and unpushed.strip():
171
+ warning("\nNo changes to commit, but found unpushed commits. Pushing to remote...")
172
+ git_push(branch)
173
+ else:
174
+ info("\nWorking tree is clean and everything is up to date.")
175
+ warning("Make some changes first before using GitGo to commit and push.")
176
+ return
177
+ except (GitCommandError, Exception):
178
+ warning("\nNo changes to commit. Cannot verify remote status.")
179
+ warning("Make some changes first or check your git remote configuration.")
145
180
  return
146
- except (GitCommandError, Exception):
147
- warning("\nNo changes to commit. Cannot verify remote status.")
148
- warning("Make some changes first or check your git remote configuration.")
149
- return
150
181
 
151
182
  print("\n" + ("=" * 90))
152
183
  success("MISSION COMPLETE — NO CASUALTIES. ALL TARGETS NEUTRALIZED.\nAWAITING FOR YOUR NEXT ORDERS.\n\n")
@@ -195,7 +226,7 @@ def main():
195
226
  epilog="Use 'gitgo <command> -h' for help on a specific command."
196
227
  )
197
228
 
198
- parser.add_argument("-v", "-V", "--version", action="version", version=f"GitGo {get_version()}")
229
+ parser.add_argument("-v", "-V", "--version", action="version", version=f"GitGo {get_version()}\nSupport GitGo: https://ko-fi.com/huerte")
199
230
  parser.add_argument("-r", "--ready", action="store_true", help="Check tool readiness")
200
231
 
201
232
  subparsers = parser.add_subparsers(title="Commands", dest="command")
@@ -216,11 +247,13 @@ def main():
216
247
  " gitgo push Push current branch with default message\n"
217
248
  " gitgo push main 'fix auth bug' Push to main with a custom message\n"
218
249
  " gitgo push 'fix auth bug' Push current branch with a custom message\n"
219
- " gitgo push -n feature/login 'add login' Create new branch and push"
250
+ " gitgo push -n feature/login 'add login' Create new branch and push\n"
251
+ " gitgo push -s main 'fix bug' Pick which files to include before pushing"
220
252
  ),
221
253
  formatter_class=argparse.RawDescriptionHelpFormatter
222
254
  )
223
255
  push_parser.add_argument("-n", "--new", action="store_true", help="Create a new branch before pushing")
256
+ push_parser.add_argument("-s", "--select", action="store_true", help="Interactively select which files to stage")
224
257
  push_parser.add_argument("branch", nargs="?", default=None, help="Branch to push to (default: current branch)")
225
258
  push_parser.add_argument("message", nargs="?", default=None, help="Commit message")
226
259
 
@@ -305,6 +338,7 @@ def main():
305
338
  sys.exit(0)
306
339
 
307
340
  ensure_first_run_setup()
341
+ check_for_updates_background(get_version())
308
342
 
309
343
  try:
310
344
  if args.command == "jump":
@@ -0,0 +1,97 @@
1
+ import json
2
+ import threading
3
+ import urllib.request
4
+ import urllib.error
5
+ from datetime import datetime, timedelta
6
+ from pathlib import Path
7
+
8
+ from pygitgo.utils.colors import info, warning
9
+
10
+
11
+ CACHE_DIR = Path.home() / ".gitgo"
12
+ CACHE_FILE = CACHE_DIR / "update_check.json"
13
+ PYPI_URL = "https://pypi.org/pypi/pygitgo/json"
14
+ CHECK_INTERVAL = timedelta(days=7)
15
+ REQUEST_TIMEOUT = 2
16
+
17
+
18
+ def read_cache():
19
+ if not CACHE_FILE.exists():
20
+ return {}
21
+ try:
22
+ with open(CACHE_FILE, "r") as f:
23
+ return json.load(f)
24
+ except (json.JSONDecodeError, IOError):
25
+ return {}
26
+
27
+
28
+ def write_cache(data):
29
+ try:
30
+ CACHE_DIR.mkdir(exist_ok=True)
31
+ with open(CACHE_FILE, "w") as f:
32
+ json.dump(data, f)
33
+ except IOError:
34
+ pass
35
+
36
+
37
+ def should_check():
38
+ cache = read_cache()
39
+ last_check = cache.get("last_check")
40
+
41
+ if not last_check:
42
+ return True
43
+
44
+ try:
45
+ last_check_time = datetime.fromisoformat(last_check)
46
+ return (datetime.now() - last_check_time) > CHECK_INTERVAL
47
+ except ValueError:
48
+ return True
49
+
50
+
51
+ def get_latest_version():
52
+ try:
53
+ with urllib.request.urlopen(PYPI_URL, timeout=REQUEST_TIMEOUT) as response:
54
+ data = json.loads(response.read().decode("utf-8"))
55
+ return data["info"]["version"]
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def parse_version(version_string):
61
+ parts = [int(x) for x in version_string.split(".")]
62
+ while len(parts) < 3:
63
+ parts.append(0)
64
+ return parts
65
+
66
+
67
+ def check_for_updates(current_version):
68
+ if not should_check():
69
+ return
70
+
71
+ cache = read_cache()
72
+ cache["last_check"] = datetime.now().isoformat()
73
+ write_cache(cache)
74
+
75
+ latest_version = get_latest_version()
76
+ if not latest_version or latest_version == current_version:
77
+ return
78
+
79
+ try:
80
+ current_parts = parse_version(current_version)
81
+ latest_parts = parse_version(latest_version)
82
+
83
+ if latest_parts > current_parts:
84
+ print()
85
+ warning(f"GitGo update available: {current_version} -> {latest_version}")
86
+ info("Run: pip install --upgrade pygitgo")
87
+ print()
88
+
89
+ cache["notified_version"] = latest_version
90
+ write_cache(cache)
91
+ except (ValueError, AttributeError):
92
+ pass
93
+
94
+
95
+ def check_for_updates_background(current_version):
96
+ thread = threading.Thread(target=check_for_updates, args=(current_version,), daemon=True)
97
+ thread.start()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitgo
3
- Version: 1.4.0
3
+ Version: 1.6.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"
@@ -30,6 +31,7 @@ Dynamic: license-file
30
31
  [![PyPI Downloads](https://img.shields.io/pypi/dm/pygitgo?color=blue)](https://pypi.org/project/pygitgo)
31
32
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
32
33
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
34
+ [![Ko-fi](https://img.shields.io/badge/Support-Ko--fi-FF5E5B?logo=ko-fi&logoColor=white)](https://ko-fi.com/huerte)
33
35
  [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Termux-lightgrey)](https://github.com/Huerte/GitGo)
34
36
 
35
37
  **Stop typing the same five Git commands. Run one instead.**
@@ -80,6 +82,7 @@ GitGo provides a CLI environment designed for faster and simpler Git workflows.
80
82
  - **Smart Branch Hopping:** Safely traverse branches with `jump`. Auto-stashes messy code and prevents merge conflict disasters with a Try-And-Revert safety engine.
81
83
  - **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.
82
84
  - **Custom Defaults:** Save your preferred branch name and commit message locally so you never have to type them again.
85
+ - **Auto-Update Checker:** Silently checks for newer versions in the background and notifies you, without slowing down your commands.
83
86
  - **SSH Auto-Setup:** Generates an SSH key, adds it to `ssh-agent`, and opens your GitHub settings automatically.
84
87
  - **HTTPS to SSH Conversion:** Silently converts the remote to SSH before pushing if your SSH is configured.
85
88
  - **Termux Compatibility:** Works natively on Android natively handling common issues like dubious ownership errors.
@@ -182,6 +185,7 @@ Stages all changes, commits, and pushes in one command.
182
185
  ```bash
183
186
  gitgo push [branch] [message]
184
187
  gitgo push -n [branch] [message] # create new branch first
188
+ gitgo push -s [branch] [message] # interactively select files to stage
185
189
  ```
186
190
 
187
191
  > [!TIP]
@@ -190,6 +194,7 @@ gitgo push -n [branch] [message] # create new branch first
190
194
  | Flag | Description |
191
195
  |------|-------------|
192
196
  | `-n`, `--new` | Create a new branch before pushing |
197
+ | `-s`, `--select` | Interactively select which files you want to include in this push |
193
198
 
194
199
  If there are no new changes but unpushed commits exist, GitGo detects this and pushes without creating an empty commit.
195
200
 
@@ -278,6 +283,7 @@ gitgo -r # verify GitGo is ready
278
283
 
279
284
  - **SSH Auto-Setup:** `gitgo user login` generates an `ed25519` SSH key, adds it to `ssh-agent`, prints the public key, and opens `github.com/settings/ssh/new`.
280
285
  - **HTTPS to SSH Conversion:** If your remote is set to HTTPS, GitGo converts the remote to SSH before pushing if SSH is configured. No `git remote set-url` is required.
286
+ - **Auto-Update Checker:** Spawns a non-blocking background thread on startup to query PyPI for newer versions. Results are cached locally for 7 days to prevent unnecessary network requests.
281
287
  - **Termux Compatibility:** Detects Termux via environment variables, adjusts binary locations (`$PREFIX/bin`), uses `termux-open` for browser actions, and natively handles the `detected dubious ownership` Git error.
282
288
  - **State Management:** `gitgo state` wraps `git stash` with named saves, indexed listing, and confirmation prompts.
283
289
 
@@ -308,6 +314,18 @@ Contributions are welcome and appreciated!
308
314
 
309
315
  ---
310
316
 
317
+ ## Support
318
+
319
+ If GitGo saves you time, consider buying me a coffee. It helps keep the project going.
320
+
321
+ <div align="center">
322
+ <a href="https://ko-fi.com/huerte">
323
+ <img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi" />
324
+ </a>
325
+ </div>
326
+
327
+ ---
328
+
311
329
  ## License
312
330
 
313
331
  Distributed under the **GPLv3** License. See [`LICENSE`](LICENSE) for details.
@@ -18,6 +18,7 @@ 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/pull.py
21
+ src/pygitgo/commands/staging.py
21
22
  src/pygitgo/commands/state.py
22
23
  src/pygitgo/commands/undo.py
23
24
  src/pygitgo/utils/__init__.py
@@ -26,6 +27,7 @@ src/pygitgo/utils/config.py
26
27
  src/pygitgo/utils/executor.py
27
28
  src/pygitgo/utils/platform_utils.py
28
29
  src/pygitgo/utils/setup.py
30
+ src/pygitgo/utils/update_checker.py
29
31
  tests/test_config.py
30
32
  tests/test_git_operations.py
31
33
  tests/test_jump.py
@@ -33,6 +35,8 @@ tests/test_main.py
33
35
  tests/test_platform.py
34
36
  tests/test_pull.py
35
37
  tests/test_setup.py
38
+ tests/test_staging.py
36
39
  tests/test_state.py
37
40
  tests/test_undo.py
41
+ tests/test_update_checker.py
38
42
  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,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()
@@ -0,0 +1,143 @@
1
+ from pygitgo.utils.update_checker import (
2
+ read_cache, write_cache, should_check, get_latest_version,
3
+ parse_version, check_for_updates, check_for_updates_background,
4
+ CACHE_FILE
5
+ )
6
+ from datetime import datetime, timedelta
7
+ from unittest.mock import patch, MagicMock
8
+ import json
9
+
10
+
11
+ def test_read_cache_empty(tmp_path, mocker):
12
+ mocker.patch("pygitgo.utils.update_checker.CACHE_FILE", tmp_path / "missing.json")
13
+ assert read_cache() == {}
14
+
15
+
16
+ def test_read_cache_valid(tmp_path, mocker):
17
+ cache_file = tmp_path / "update_check.json"
18
+ cache_file.write_text(json.dumps({"last_check": "2026-01-01T00:00:00"}))
19
+ mocker.patch("pygitgo.utils.update_checker.CACHE_FILE", cache_file)
20
+ result = read_cache()
21
+ assert result["last_check"] == "2026-01-01T00:00:00"
22
+
23
+
24
+ def test_read_cache_corrupted(tmp_path, mocker):
25
+ cache_file = tmp_path / "update_check.json"
26
+ cache_file.write_text("not json")
27
+ mocker.patch("pygitgo.utils.update_checker.CACHE_FILE", cache_file)
28
+ assert read_cache() == {}
29
+
30
+
31
+ def test_write_cache(tmp_path, mocker):
32
+ cache_file = tmp_path / "update_check.json"
33
+ mocker.patch("pygitgo.utils.update_checker.CACHE_DIR", tmp_path)
34
+ mocker.patch("pygitgo.utils.update_checker.CACHE_FILE", cache_file)
35
+ write_cache({"last_check": "2026-01-01T00:00:00"})
36
+ result = json.loads(cache_file.read_text())
37
+ assert result["last_check"] == "2026-01-01T00:00:00"
38
+
39
+
40
+ def test_should_check_no_cache(mocker):
41
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={})
42
+ assert should_check() is True
43
+
44
+
45
+ def test_should_check_expired(mocker):
46
+ old_date = (datetime.now() - timedelta(days=8)).isoformat()
47
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={"last_check": old_date})
48
+ assert should_check() is True
49
+
50
+
51
+ def test_should_check_still_valid(mocker):
52
+ recent_date = (datetime.now() - timedelta(days=1)).isoformat()
53
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={"last_check": recent_date})
54
+ assert should_check() is False
55
+
56
+
57
+ def test_should_check_invalid_date(mocker):
58
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={"last_check": "not-a-date"})
59
+ assert should_check() is True
60
+
61
+
62
+ def test_parse_version_standard():
63
+ assert parse_version("1.5.0") == [1, 5, 0]
64
+
65
+
66
+ def test_parse_version_short():
67
+ assert parse_version("2.0") == [2, 0, 0]
68
+
69
+
70
+ def test_parse_version_comparison():
71
+ assert parse_version("1.6.0") > parse_version("1.5.0")
72
+ assert parse_version("2.0.0") > parse_version("1.9.9")
73
+ assert parse_version("1.5.0") == parse_version("1.5.0")
74
+
75
+
76
+ def test_get_latest_version_success(mocker):
77
+ fake_response = MagicMock()
78
+ fake_response.read.return_value = json.dumps({"info": {"version": "1.6.0"}}).encode("utf-8")
79
+ fake_response.__enter__ = lambda s: s
80
+ fake_response.__exit__ = MagicMock(return_value=False)
81
+ mocker.patch("pygitgo.utils.update_checker.urllib.request.urlopen", return_value=fake_response)
82
+ assert get_latest_version() == "1.6.0"
83
+
84
+
85
+ def test_get_latest_version_network_failure(mocker):
86
+ mocker.patch("pygitgo.utils.update_checker.urllib.request.urlopen", side_effect=Exception("timeout"))
87
+ assert get_latest_version() is None
88
+
89
+
90
+ def test_check_for_updates_skips_when_cached(mocker):
91
+ mocker.patch("pygitgo.utils.update_checker.should_check", return_value=False)
92
+ fake_get = mocker.patch("pygitgo.utils.update_checker.get_latest_version")
93
+ check_for_updates("1.5.0")
94
+ fake_get.assert_not_called()
95
+
96
+
97
+ def test_check_for_updates_no_update_available(mocker):
98
+ mocker.patch("pygitgo.utils.update_checker.should_check", return_value=True)
99
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={})
100
+ mocker.patch("pygitgo.utils.update_checker.write_cache")
101
+ mocker.patch("pygitgo.utils.update_checker.get_latest_version", return_value="1.5.0")
102
+ fake_warning = mocker.patch("pygitgo.utils.update_checker.warning")
103
+ check_for_updates("1.5.0")
104
+ fake_warning.assert_not_called()
105
+
106
+
107
+ def test_check_for_updates_newer_version_available(mocker):
108
+ mocker.patch("pygitgo.utils.update_checker.should_check", return_value=True)
109
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={})
110
+ mocker.patch("pygitgo.utils.update_checker.write_cache")
111
+ mocker.patch("pygitgo.utils.update_checker.get_latest_version", return_value="2.0.0")
112
+ fake_warning = mocker.patch("pygitgo.utils.update_checker.warning")
113
+ fake_info = mocker.patch("pygitgo.utils.update_checker.info")
114
+ check_for_updates("1.5.0")
115
+ fake_warning.assert_called_with("GitGo update available: 1.5.0 -> 2.0.0")
116
+ fake_info.assert_called_with("Run: pip install --upgrade pygitgo")
117
+
118
+
119
+ def test_check_for_updates_older_version_on_pypi(mocker):
120
+ mocker.patch("pygitgo.utils.update_checker.should_check", return_value=True)
121
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={})
122
+ mocker.patch("pygitgo.utils.update_checker.write_cache")
123
+ mocker.patch("pygitgo.utils.update_checker.get_latest_version", return_value="1.4.0")
124
+ fake_warning = mocker.patch("pygitgo.utils.update_checker.warning")
125
+ check_for_updates("1.5.0")
126
+ fake_warning.assert_not_called()
127
+
128
+
129
+ def test_check_for_updates_pypi_returns_none(mocker):
130
+ mocker.patch("pygitgo.utils.update_checker.should_check", return_value=True)
131
+ mocker.patch("pygitgo.utils.update_checker.read_cache", return_value={})
132
+ mocker.patch("pygitgo.utils.update_checker.write_cache")
133
+ mocker.patch("pygitgo.utils.update_checker.get_latest_version", return_value=None)
134
+ fake_warning = mocker.patch("pygitgo.utils.update_checker.warning")
135
+ check_for_updates("1.5.0")
136
+ fake_warning.assert_not_called()
137
+
138
+
139
+ def test_check_for_updates_background_starts_thread(mocker):
140
+ fake_thread = MagicMock()
141
+ mocker.patch("pygitgo.utils.update_checker.threading.Thread", return_value=fake_thread)
142
+ check_for_updates_background("1.5.0")
143
+ fake_thread.start.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