polygit 0.4.0__tar.gz → 0.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.
- {polygit-0.4.0 → polygit-0.5.0}/PKG-INFO +15 -1
- {polygit-0.4.0 → polygit-0.5.0}/README.md +14 -0
- {polygit-0.4.0 → polygit-0.5.0}/pyproject.toml +1 -1
- {polygit-0.4.0 → polygit-0.5.0}/src/polygit/cli.py +4 -2
- {polygit-0.4.0 → polygit-0.5.0}/src/polygit/repo.py +11 -1
- {polygit-0.4.0 → polygit-0.5.0}/tests/test_polygit.py +55 -1
- {polygit-0.4.0 → polygit-0.5.0}/uv.lock +1 -1
- {polygit-0.4.0 → polygit-0.5.0}/.github/workflows/ci.yml +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/.gitignore +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/AGENTS.md +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/CLAUDE.md +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/LICENSE.txt +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/src/polygit/__init__.py +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/src/polygit/config.py +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/src/polygit/repos.py +0 -0
- {polygit-0.4.0 → polygit-0.5.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: polygit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Manage multiple git repos from a single .pgit config file.
|
|
5
5
|
Project-URL: Repository, https://github.com/gschaetz/polygit
|
|
6
6
|
Author-email: Gary Schaetz <gary@schaetzkc.com>
|
|
@@ -96,6 +96,20 @@ pgit push --dry-run # list dirty repos that would be committed and pushed
|
|
|
96
96
|
|
|
97
97
|
`pull --dry-run` compares against the last fetched state of each upstream, so run `pgit sync` first for current numbers.
|
|
98
98
|
|
|
99
|
+
## status --json
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pgit status --json # machine-readable list of dirty repos
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Prints a single JSON array on stdout and nothing else:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
[{"platform": "github", "path": "gschaetz/mousehat", "changed_files": 2}]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`changed_files` is the number of entries in `git status --short`. Clean repos are omitted, so the array is empty when there is nothing to commit.
|
|
112
|
+
|
|
99
113
|
## repos — drift detection
|
|
100
114
|
|
|
101
115
|
`pgit repos` compares what's actually on disk against your `.pgit` config and reports:
|
|
@@ -63,6 +63,20 @@ pgit push --dry-run # list dirty repos that would be committed and pushed
|
|
|
63
63
|
|
|
64
64
|
`pull --dry-run` compares against the last fetched state of each upstream, so run `pgit sync` first for current numbers.
|
|
65
65
|
|
|
66
|
+
## status --json
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pgit status --json # machine-readable list of dirty repos
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Prints a single JSON array on stdout and nothing else:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
[{"platform": "github", "path": "gschaetz/mousehat", "changed_files": 2}]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`changed_files` is the number of entries in `git status --short`. Clean repos are omitted, so the array is empty when there is nothing to commit.
|
|
79
|
+
|
|
66
80
|
## repos — drift detection
|
|
67
81
|
|
|
68
82
|
`pgit repos` compares what's actually on disk against your `.pgit` config and reports:
|
|
@@ -13,7 +13,9 @@ def main():
|
|
|
13
13
|
sub = parser.add_subparsers(dest="command", metavar="command")
|
|
14
14
|
|
|
15
15
|
sub.add_parser("sync", help="Clone missing repos and fetch all")
|
|
16
|
-
sub.add_parser("status", help="Show repos with uncommitted changes")
|
|
16
|
+
status_p = sub.add_parser("status", help="Show repos with uncommitted changes")
|
|
17
|
+
status_p.add_argument("--json", action="store_true",
|
|
18
|
+
help="Print dirty repos as a JSON list")
|
|
17
19
|
pull_p = sub.add_parser("pull", help="Pull all repos, auto-stashing local changes")
|
|
18
20
|
pull_p.add_argument("--dry-run", action="store_true",
|
|
19
21
|
help="Show ahead/behind vs. last-fetched upstream; changes nothing")
|
|
@@ -49,7 +51,7 @@ def main():
|
|
|
49
51
|
|
|
50
52
|
commands = {
|
|
51
53
|
"sync": lambda: cmd_sync(config, base),
|
|
52
|
-
"status": lambda: cmd_status(config, base),
|
|
54
|
+
"status": lambda: cmd_status(config, base, json_output=args.json),
|
|
53
55
|
"pull": lambda: cmd_pull(config, base, dry_run=args.dry_run),
|
|
54
56
|
"push": lambda: cmd_push(config, base, mode=args.push_mode, dry_run=args.dry_run),
|
|
55
57
|
"repos": lambda: cmd_repos(config, base, config_path),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import shutil
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
@@ -194,7 +195,7 @@ def cmd_sync(config, base):
|
|
|
194
195
|
f.result()
|
|
195
196
|
|
|
196
197
|
|
|
197
|
-
def cmd_status(config, base):
|
|
198
|
+
def cmd_status(config, base, json_output=False):
|
|
198
199
|
results = []
|
|
199
200
|
|
|
200
201
|
def _check(platform, local_path, d):
|
|
@@ -210,6 +211,15 @@ def cmd_status(config, base):
|
|
|
210
211
|
for f in as_completed(futures):
|
|
211
212
|
results.append(f.result())
|
|
212
213
|
|
|
214
|
+
if json_output:
|
|
215
|
+
entries = [
|
|
216
|
+
{"platform": platform, "path": local_path, "changed_files": len(output.splitlines())}
|
|
217
|
+
for platform, local_path, output in sorted(results)
|
|
218
|
+
if output
|
|
219
|
+
]
|
|
220
|
+
print(json.dumps(entries))
|
|
221
|
+
return
|
|
222
|
+
|
|
213
223
|
found_dirty = False
|
|
214
224
|
for platform, local_path, output in sorted(results):
|
|
215
225
|
if output:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import subprocess
|
|
2
3
|
import tempfile
|
|
3
4
|
import pytest
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from polygit.config import find_config, load_config, write_config
|
|
6
7
|
from polygit.repos import parse_remote_url, del_nested, _add_to_config
|
|
7
|
-
from polygit.repo import walk, _pull_one, cmd_push
|
|
8
|
+
from polygit.repo import walk, _pull_one, cmd_push, cmd_status
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
# ── parse_remote_url ──────────────────────────────────────────────────────────
|
|
@@ -195,3 +196,56 @@ def test_push_dry_run_lists_dirty_without_committing(capsys, tmp_path):
|
|
|
195
196
|
log = subprocess.run(["git", "-C", str(d), "log", "--oneline"],
|
|
196
197
|
capture_output=True, text=True).stdout
|
|
197
198
|
assert len(log.splitlines()) == 1
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── status --json ────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def test_status_json_lists_dirty_repos(capsys, tmp_path):
|
|
204
|
+
d = _make_clone(tmp_path)
|
|
205
|
+
(d / "init").write_text("edited")
|
|
206
|
+
(d / "a.txt").write_text("a")
|
|
207
|
+
(d / "b.txt").write_text("b")
|
|
208
|
+
|
|
209
|
+
cmd_status({"github": {"r": None}}, tmp_path, json_output=True)
|
|
210
|
+
|
|
211
|
+
payload = json.loads(capsys.readouterr().out)
|
|
212
|
+
assert payload == [{"platform": "github", "path": "r", "changed_files": 3}]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_status_json_empty_when_clean(capsys, tmp_path):
|
|
216
|
+
_make_clone(tmp_path)
|
|
217
|
+
cmd_status({"github": {"r": None}}, tmp_path, json_output=True)
|
|
218
|
+
assert json.loads(capsys.readouterr().out) == []
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_status_json_omits_clean_repos(capsys, tmp_path):
|
|
222
|
+
_make_clone(tmp_path)
|
|
223
|
+
dirty = tmp_path / "github" / "other"
|
|
224
|
+
subprocess.run(["git", "clone", "-q", str(tmp_path / "origin.git"), str(dirty)],
|
|
225
|
+
check=True, capture_output=True)
|
|
226
|
+
(dirty / "x.txt").write_text("x")
|
|
227
|
+
|
|
228
|
+
cmd_status({"github": {"r": None, "other": None}}, tmp_path, json_output=True)
|
|
229
|
+
|
|
230
|
+
payload = json.loads(capsys.readouterr().out)
|
|
231
|
+
assert payload == [{"platform": "github", "path": "other", "changed_files": 1}]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_status_default_output_unchanged(capsys, tmp_path):
|
|
235
|
+
d = _make_clone(tmp_path)
|
|
236
|
+
(d / "dirty.txt").write_text("x")
|
|
237
|
+
|
|
238
|
+
cmd_status({"github": {"r": None}}, tmp_path)
|
|
239
|
+
out = capsys.readouterr().out
|
|
240
|
+
|
|
241
|
+
assert "github/r" in out
|
|
242
|
+
assert "dirty.txt" in out
|
|
243
|
+
assert out.strip()[0] != "["
|
|
244
|
+
with pytest.raises(ValueError):
|
|
245
|
+
json.loads(out)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_status_default_clean_message(capsys, tmp_path):
|
|
249
|
+
_make_clone(tmp_path)
|
|
250
|
+
cmd_status({"github": {"r": None}}, tmp_path)
|
|
251
|
+
assert "All repos are clean." in capsys.readouterr().out
|
|
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
|