polygit 0.5.0__tar.gz → 0.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.
- {polygit-0.5.0 → polygit-0.6.0}/PKG-INFO +12 -1
- {polygit-0.5.0 → polygit-0.6.0}/README.md +11 -0
- {polygit-0.5.0 → polygit-0.6.0}/pyproject.toml +1 -1
- {polygit-0.5.0 → polygit-0.6.0}/src/polygit/cli.py +13 -4
- {polygit-0.5.0 → polygit-0.6.0}/src/polygit/repo.py +14 -8
- {polygit-0.5.0 → polygit-0.6.0}/tests/test_polygit.py +87 -1
- {polygit-0.5.0 → polygit-0.6.0}/uv.lock +1 -1
- {polygit-0.5.0 → polygit-0.6.0}/.github/workflows/ci.yml +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/.gitignore +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/AGENTS.md +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/CLAUDE.md +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/LICENSE.txt +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/src/polygit/__init__.py +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/src/polygit/config.py +0 -0
- {polygit-0.5.0 → polygit-0.6.0}/src/polygit/repos.py +0 -0
- {polygit-0.5.0 → polygit-0.6.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.6.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>
|
|
@@ -49,6 +49,17 @@ pgit push # interactively commit and push dirty repos
|
|
|
49
49
|
pgit repos # discover drift between .pgit and the filesystem
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
`status`, `pull` and `push` accept `--only <platform>` to operate on a single platform
|
|
53
|
+
(e.g. `github`) instead of all of them:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pgit status --only gitlab
|
|
57
|
+
pgit pull --only github --dry-run
|
|
58
|
+
pgit push --only gitlab --all
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Passing an unknown platform prints an error to stderr and exits 1.
|
|
62
|
+
|
|
52
63
|
## Configuration
|
|
53
64
|
|
|
54
65
|
Create a `.pgit` file in the root directory that contains your repos:
|
|
@@ -16,6 +16,17 @@ pgit push # interactively commit and push dirty repos
|
|
|
16
16
|
pgit repos # discover drift between .pgit and the filesystem
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`status`, `pull` and `push` accept `--only <platform>` to operate on a single platform
|
|
20
|
+
(e.g. `github`) instead of all of them:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pgit status --only gitlab
|
|
24
|
+
pgit pull --only github --dry-run
|
|
25
|
+
pgit push --only gitlab --all
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Passing an unknown platform prints an error to stderr and exits 1.
|
|
29
|
+
|
|
19
30
|
## Configuration
|
|
20
31
|
|
|
21
32
|
Create a `.pgit` file in the root directory that contains your repos:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import argparse
|
|
3
|
-
from .config import find_config, load_config
|
|
3
|
+
from .config import find_config, load_config, HOSTS
|
|
4
4
|
from .repo import cmd_sync, cmd_status, cmd_pull, cmd_push
|
|
5
5
|
from .repos import cmd_repos
|
|
6
6
|
|
|
@@ -16,9 +16,11 @@ def main():
|
|
|
16
16
|
status_p = sub.add_parser("status", help="Show repos with uncommitted changes")
|
|
17
17
|
status_p.add_argument("--json", action="store_true",
|
|
18
18
|
help="Print dirty repos as a JSON list")
|
|
19
|
+
status_p.add_argument("--only", dest="only_platform", help="Limit to a single platform (e.g. github)")
|
|
19
20
|
pull_p = sub.add_parser("pull", help="Pull all repos, auto-stashing local changes")
|
|
20
21
|
pull_p.add_argument("--dry-run", action="store_true",
|
|
21
22
|
help="Show ahead/behind vs. last-fetched upstream; changes nothing")
|
|
23
|
+
pull_p.add_argument("--only", dest="only_platform", help="Limit to a single platform (e.g. github)")
|
|
22
24
|
|
|
23
25
|
push_p = sub.add_parser("push", help="Commit and push dirty repos")
|
|
24
26
|
mode_g = push_p.add_mutually_exclusive_group()
|
|
@@ -31,6 +33,7 @@ def main():
|
|
|
31
33
|
push_p.set_defaults(push_mode="interactive")
|
|
32
34
|
push_p.add_argument("--dry-run", action="store_true",
|
|
33
35
|
help="Show the commit/push plan without committing or pushing")
|
|
36
|
+
push_p.add_argument("--only", dest="only_platform", help="Limit to a single platform (e.g. github)")
|
|
34
37
|
|
|
35
38
|
sub.add_parser("repos", help="Discover drift between .pgit and the filesystem")
|
|
36
39
|
|
|
@@ -49,11 +52,17 @@ def main():
|
|
|
49
52
|
base = config_path.parent
|
|
50
53
|
config = load_config(config_path)
|
|
51
54
|
|
|
55
|
+
only_platform = getattr(args, "only_platform", None)
|
|
56
|
+
if only_platform is not None and only_platform not in HOSTS:
|
|
57
|
+
print(f"Error: unknown platform '{only_platform}'. "
|
|
58
|
+
f"Available: {', '.join(sorted(HOSTS))}", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
52
61
|
commands = {
|
|
53
62
|
"sync": lambda: cmd_sync(config, base),
|
|
54
|
-
"status": lambda: cmd_status(config, base, json_output=args.json),
|
|
55
|
-
"pull": lambda: cmd_pull(config, base, dry_run=args.dry_run),
|
|
56
|
-
"push": lambda: cmd_push(config, base, mode=args.push_mode, dry_run=args.dry_run),
|
|
63
|
+
"status": lambda: cmd_status(config, base, json_output=args.json, only=only_platform),
|
|
64
|
+
"pull": lambda: cmd_pull(config, base, dry_run=args.dry_run, only=only_platform),
|
|
65
|
+
"push": lambda: cmd_push(config, base, mode=args.push_mode, dry_run=args.dry_run, only=only_platform),
|
|
57
66
|
"repos": lambda: cmd_repos(config, base, config_path),
|
|
58
67
|
}
|
|
59
68
|
commands[args.command]()
|
|
@@ -89,9 +89,15 @@ def each_repo(config, base):
|
|
|
89
89
|
yield platform, local_path, remote_path, base / platform / local_path
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def each_existing_repo(config, base):
|
|
93
|
-
"""Yield only repos that are already cloned locally.
|
|
92
|
+
def each_existing_repo(config, base, only=None):
|
|
93
|
+
"""Yield only repos that are already cloned locally.
|
|
94
|
+
|
|
95
|
+
If *only* is set, only repos under that platform are yielded. The helper
|
|
96
|
+
is used by :func:`cmd_status`, :func:`cmd_pull` and :func:`cmd_push`.
|
|
97
|
+
"""
|
|
94
98
|
for platform, local_path, remote_path, d in each_repo(config, base):
|
|
99
|
+
if only is not None and platform != only:
|
|
100
|
+
continue
|
|
95
101
|
if (d / ".git").exists():
|
|
96
102
|
yield platform, local_path, remote_path, d
|
|
97
103
|
|
|
@@ -195,7 +201,7 @@ def cmd_sync(config, base):
|
|
|
195
201
|
f.result()
|
|
196
202
|
|
|
197
203
|
|
|
198
|
-
def cmd_status(config, base, json_output=False):
|
|
204
|
+
def cmd_status(config, base, json_output=False, only=None):
|
|
199
205
|
results = []
|
|
200
206
|
|
|
201
207
|
def _check(platform, local_path, d):
|
|
@@ -205,7 +211,7 @@ def cmd_status(config, base, json_output=False):
|
|
|
205
211
|
)
|
|
206
212
|
return platform, local_path, result.stdout.strip()
|
|
207
213
|
|
|
208
|
-
repos = list(each_existing_repo(config, base))
|
|
214
|
+
repos = list(each_existing_repo(config, base, only=only))
|
|
209
215
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
|
|
210
216
|
futures = {pool.submit(_check, p, lp, d): (p, lp) for p, lp, _, d in repos}
|
|
211
217
|
for f in as_completed(futures):
|
|
@@ -231,18 +237,18 @@ def cmd_status(config, base, json_output=False):
|
|
|
231
237
|
print(f"{OK} All repos are clean.")
|
|
232
238
|
|
|
233
239
|
|
|
234
|
-
def cmd_pull(config, base, dry_run=False):
|
|
235
|
-
repos = list(each_existing_repo(config, base))
|
|
240
|
+
def cmd_pull(config, base, dry_run=False, only=None):
|
|
241
|
+
repos = list(each_existing_repo(config, base, only=only))
|
|
236
242
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
|
|
237
243
|
futures = [pool.submit(_pull_one, p, lp, d, dry_run) for p, lp, _, d in repos]
|
|
238
244
|
for f in as_completed(futures):
|
|
239
245
|
f.result()
|
|
240
246
|
|
|
241
247
|
|
|
242
|
-
def cmd_push(config, base, mode="interactive", dry_run=False):
|
|
248
|
+
def cmd_push(config, base, mode="interactive", dry_run=False, only=None):
|
|
243
249
|
dirty = [
|
|
244
250
|
(platform, local_path, d)
|
|
245
|
-
for platform, local_path, _, d in each_existing_repo(config, base)
|
|
251
|
+
for platform, local_path, _, d in each_existing_repo(config, base, only=only)
|
|
246
252
|
if is_dirty(d)
|
|
247
253
|
]
|
|
248
254
|
|
|
@@ -5,7 +5,7 @@ import pytest
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from polygit.config import find_config, load_config, write_config
|
|
7
7
|
from polygit.repos import parse_remote_url, del_nested, _add_to_config
|
|
8
|
-
from polygit.repo import walk, _pull_one, cmd_push, cmd_status
|
|
8
|
+
from polygit.repo import walk, _pull_one, cmd_push, cmd_status, cmd_pull
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# ── parse_remote_url ──────────────────────────────────────────────────────────
|
|
@@ -249,3 +249,89 @@ def test_status_default_clean_message(capsys, tmp_path):
|
|
|
249
249
|
_make_clone(tmp_path)
|
|
250
250
|
cmd_status({"github": {"r": None}}, tmp_path)
|
|
251
251
|
assert "All repos are clean." in capsys.readouterr().out
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ── --only platform filter ───────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
def _gitlab_clone(tmp_path, name="gl", commit=True):
|
|
257
|
+
d = tmp_path / "gitlab" / name
|
|
258
|
+
d.parent.mkdir()
|
|
259
|
+
subprocess.run(["git", "clone", "-q", str(tmp_path / "origin.git"), str(d)],
|
|
260
|
+
check=True, capture_output=True)
|
|
261
|
+
if commit:
|
|
262
|
+
_commit(d, "seed")
|
|
263
|
+
_git(d, "push", "-q", "origin", "HEAD:main")
|
|
264
|
+
return d
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_status_only_limits_to_platform(capsys, tmp_path):
|
|
268
|
+
github = _make_clone(tmp_path)
|
|
269
|
+
(github / "x.txt").write_text("x")
|
|
270
|
+
_gitlab_clone(tmp_path)
|
|
271
|
+
(tmp_path / "gitlab" / "gl" / "y.txt").write_text("y")
|
|
272
|
+
|
|
273
|
+
cmd_status({"github": {"r": None}, "gitlab": {"gl": None}}, tmp_path, only="github")
|
|
274
|
+
|
|
275
|
+
out = capsys.readouterr().out
|
|
276
|
+
assert "github/r" in out
|
|
277
|
+
assert "gitlab/gl" not in out
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_status_only_json(capsys, tmp_path):
|
|
281
|
+
github = _make_clone(tmp_path)
|
|
282
|
+
(github / "x.txt").write_text("x")
|
|
283
|
+
_gitlab_clone(tmp_path)
|
|
284
|
+
(tmp_path / "gitlab" / "gl" / "y.txt").write_text("y")
|
|
285
|
+
|
|
286
|
+
cmd_status({"github": {"r": None}, "gitlab": {"gl": None}}, tmp_path,
|
|
287
|
+
json_output=True, only="github")
|
|
288
|
+
|
|
289
|
+
assert json.loads(capsys.readouterr().out) == [
|
|
290
|
+
{"platform": "github", "path": "r", "changed_files": 1}
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_pull_only_limits_to_platform(capsys, tmp_path):
|
|
295
|
+
_make_clone(tmp_path, ahead=2)
|
|
296
|
+
_gitlab_clone(tmp_path)
|
|
297
|
+
_commit(tmp_path / "gitlab" / "gl", "ahead")
|
|
298
|
+
_git(tmp_path / "gitlab" / "gl", "push", "-q", "origin", "HEAD:main")
|
|
299
|
+
|
|
300
|
+
cmd_pull({"github": {"r": None}, "gitlab": {"gl": None}}, tmp_path,
|
|
301
|
+
dry_run=True, only="github")
|
|
302
|
+
|
|
303
|
+
out = capsys.readouterr().out
|
|
304
|
+
assert "github/r" in out
|
|
305
|
+
assert "gitlab/gl" not in out
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_push_only_limits_to_platform(capsys, tmp_path):
|
|
309
|
+
github = _make_clone(tmp_path)
|
|
310
|
+
(github / "g.txt").write_text("x")
|
|
311
|
+
_gitlab_clone(tmp_path)
|
|
312
|
+
(tmp_path / "gitlab" / "gl" / "l.txt").write_text("x")
|
|
313
|
+
|
|
314
|
+
cmd_push({"github": {"r": None}, "gitlab": {"gl": None}}, tmp_path,
|
|
315
|
+
dry_run=True, only="github")
|
|
316
|
+
|
|
317
|
+
out = capsys.readouterr().out
|
|
318
|
+
assert "github/r" in out
|
|
319
|
+
assert "gitlab/gl" not in out
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_status_unknown_platform_errors(capsys, tmp_path, monkeypatch):
|
|
323
|
+
import sys
|
|
324
|
+
from polygit.cli import main
|
|
325
|
+
|
|
326
|
+
_make_clone(tmp_path)
|
|
327
|
+
(tmp_path / ".pgit").write_text("github:\n r:\n")
|
|
328
|
+
monkeypatch.chdir(tmp_path)
|
|
329
|
+
monkeypatch.setattr(sys, "argv", ["pgit", "status", "--only", "bitbucket"])
|
|
330
|
+
|
|
331
|
+
with pytest.raises(SystemExit) as exc:
|
|
332
|
+
main()
|
|
333
|
+
|
|
334
|
+
assert exc.value.code == 1
|
|
335
|
+
err = capsys.readouterr().err
|
|
336
|
+
assert "bitbucket" in err
|
|
337
|
+
assert "github" in err
|
|
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
|