fastws-cli 0.0.16__tar.gz → 0.0.17__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.
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/PKG-INFO +5 -4
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/README.md +3 -2
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws/__init__.py +2 -1
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws/core.py +29 -21
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws_cli.egg-info/PKG-INFO +5 -4
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws_cli.egg-info/requires.txt +1 -1
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/pyproject.toml +1 -1
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/tests/test_sync.py +27 -24
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/LICENSE +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws/releases.py +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws_cli.egg-info/SOURCES.txt +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/setup.cfg +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/tests/test_build.py +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/tests/test_releases.py +0 -0
- {fastws_cli-0.0.16 → fastws_cli-0.0.17}/tests/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.17
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License: Apache-2.0
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: fastcore>=2.2.
|
|
13
|
+
Requires-Dist: fastcore>=2.2.29
|
|
14
14
|
Requires-Dist: fastgit>=0.1.2
|
|
15
15
|
Requires-Dist: ghapi>=2.0.6
|
|
16
16
|
Requires-Dist: packaging
|
|
@@ -261,15 +261,16 @@ ws-add ~/private # stays where it is, recorded in repos-local.txt with its loca
|
|
|
261
261
|
|
|
262
262
|
### `ws-remove`
|
|
263
263
|
|
|
264
|
-
Delete
|
|
264
|
+
Delete one or more personal repo clones and remove them from `repos-local.txt` and the workspace `pyproject.toml`. Run `uv sync` once after processing all repos.
|
|
265
265
|
|
|
266
266
|
The command refuses shared baseline members. Edit `repos.txt` deliberately to change the team's baseline. It also refuses external or custom checkout locations, which must be managed explicitly.
|
|
267
267
|
|
|
268
|
-
Removal requires a clean Git checkout with an `origin` remote and no uncommitted changes or unpushed commits. The command
|
|
268
|
+
Removal requires a clean Git checkout with an `origin` remote and no uncommitted changes or unpushed commits. The command validates all targets before changing anything. It asks for confirmation before deleting each checkout. Declining keeps that checkout but still removes its workspace metadata. Repeated targets are processed once:
|
|
269
269
|
|
|
270
270
|
```bash
|
|
271
271
|
ws-remove owner/personal-project
|
|
272
272
|
ws-remove personal-project # bare folder name also works if the directory exists
|
|
273
|
+
ws-remove owner/project-one project-two project-three
|
|
273
274
|
```
|
|
274
275
|
|
|
275
276
|
### `ws-releases`
|
|
@@ -236,15 +236,16 @@ ws-add ~/private # stays where it is, recorded in repos-local.txt with its loca
|
|
|
236
236
|
|
|
237
237
|
### `ws-remove`
|
|
238
238
|
|
|
239
|
-
Delete
|
|
239
|
+
Delete one or more personal repo clones and remove them from `repos-local.txt` and the workspace `pyproject.toml`. Run `uv sync` once after processing all repos.
|
|
240
240
|
|
|
241
241
|
The command refuses shared baseline members. Edit `repos.txt` deliberately to change the team's baseline. It also refuses external or custom checkout locations, which must be managed explicitly.
|
|
242
242
|
|
|
243
|
-
Removal requires a clean Git checkout with an `origin` remote and no uncommitted changes or unpushed commits. The command
|
|
243
|
+
Removal requires a clean Git checkout with an `origin` remote and no uncommitted changes or unpushed commits. The command validates all targets before changing anything. It asks for confirmation before deleting each checkout. Declining keeps that checkout but still removes its workspace metadata. Repeated targets are processed once:
|
|
244
244
|
|
|
245
245
|
```bash
|
|
246
246
|
ws-remove owner/personal-project
|
|
247
247
|
ws-remove personal-project # bare folder name also works if the directory exists
|
|
248
|
+
ws-remove owner/project-one project-two project-three
|
|
248
249
|
```
|
|
249
250
|
|
|
250
251
|
### `ws-releases`
|
|
@@ -984,33 +984,41 @@ def _repo_safety_issues(d: Path) -> list[str]:
|
|
|
984
984
|
|
|
985
985
|
@call_parse
|
|
986
986
|
def ws_remove(
|
|
987
|
-
repo: str, # Repo to remove, e.g.
|
|
987
|
+
repo: str, # Repo to remove, e.g. owner/repo or a local folder name
|
|
988
|
+
*repos: str, # Additional repos to remove
|
|
988
989
|
workspace: str = "", # Workspace root; defaults to active venv parent when available
|
|
989
990
|
repos_file: str = "repos.txt", # Repo list to update
|
|
990
991
|
pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
|
|
991
992
|
template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
|
|
992
993
|
):
|
|
993
|
-
"Remove
|
|
994
|
+
"Remove personal repos and their local metadata; shared baseline members cannot be removed."
|
|
994
995
|
root = _ws_root(workspace, repos_file, pyproject_file, template_file)
|
|
995
996
|
repos_path = _resolve_path(root, repos_file)
|
|
996
997
|
pyproject_path = _resolve_path(root, pyproject_file)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
998
|
+
local_path = _local_repos_path(repos_path)
|
|
999
|
+
baseline = {_repo_key(r) for r in _load_repos(repos_path)}
|
|
1000
|
+
local = {_repo_key(r) for r in _load_repos(local_path)}
|
|
1001
|
+
entries = _load_repo_entries(repos_path, root)
|
|
1002
|
+
targets = {}
|
|
1003
|
+
for repo in (repo, *repos):
|
|
1004
|
+
repo = _resolve_removal_target(root, repo, repos_path)
|
|
1005
|
+
key = _repo_key(repo)
|
|
1006
|
+
if key in baseline: raise SystemExit(f'{repo} belongs to the shared baseline: edit {repos_path} deliberately to remove it')
|
|
1007
|
+
d = _resolve_repo_dir(root, repo)
|
|
1008
|
+
for r,location,_ in entries:
|
|
1009
|
+
if _repo_key(r) == key and location.resolve() != d:
|
|
1010
|
+
raise SystemExit(f'Refusing to remove {repo}: custom checkout location {location}; manage it explicitly')
|
|
1011
|
+
if d in targets: continue
|
|
1012
|
+
if not d.exists() and key not in local: raise SystemExit(f"Nothing to remove for {repo}")
|
|
1013
|
+
if d.exists() and (issues := _repo_safety_issues(d)):
|
|
1014
|
+
raise SystemExit("Refusing to remove:\n" + "\n".join(f" - {i}" for i in issues))
|
|
1015
|
+
name = _read_pyproject_name(d/"pyproject.toml") if (d/"pyproject.toml").exists() else None
|
|
1016
|
+
targets[d] = repo, name or d.name
|
|
1017
|
+
for d,(repo,name) in targets.items():
|
|
1018
|
+
_remove_from_repos_file(local_path, repo)
|
|
1019
|
+
_remove_from_pyproject(pyproject_path, [name])
|
|
1020
|
+
if d.exists():
|
|
1021
|
+
try: ans = input(f"Remove directory {d}? [y/N] ")
|
|
1022
|
+
except EOFError: ans = ""
|
|
1023
|
+
if ans.strip().lower() in ("y", "yes"): shutil.rmtree(d)
|
|
1016
1024
|
subprocess.run(["uv", "sync"], check=True, cwd=root)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.17
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License: Apache-2.0
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: fastcore>=2.2.
|
|
13
|
+
Requires-Dist: fastcore>=2.2.29
|
|
14
14
|
Requires-Dist: fastgit>=0.1.2
|
|
15
15
|
Requires-Dist: ghapi>=2.0.6
|
|
16
16
|
Requires-Dist: packaging
|
|
@@ -261,15 +261,16 @@ ws-add ~/private # stays where it is, recorded in repos-local.txt with its loca
|
|
|
261
261
|
|
|
262
262
|
### `ws-remove`
|
|
263
263
|
|
|
264
|
-
Delete
|
|
264
|
+
Delete one or more personal repo clones and remove them from `repos-local.txt` and the workspace `pyproject.toml`. Run `uv sync` once after processing all repos.
|
|
265
265
|
|
|
266
266
|
The command refuses shared baseline members. Edit `repos.txt` deliberately to change the team's baseline. It also refuses external or custom checkout locations, which must be managed explicitly.
|
|
267
267
|
|
|
268
|
-
Removal requires a clean Git checkout with an `origin` remote and no uncommitted changes or unpushed commits. The command
|
|
268
|
+
Removal requires a clean Git checkout with an `origin` remote and no uncommitted changes or unpushed commits. The command validates all targets before changing anything. It asks for confirmation before deleting each checkout. Declining keeps that checkout but still removes its workspace metadata. Repeated targets are processed once:
|
|
269
269
|
|
|
270
270
|
```bash
|
|
271
271
|
ws-remove owner/personal-project
|
|
272
272
|
ws-remove personal-project # bare folder name also works if the directory exists
|
|
273
|
+
ws-remove owner/project-one project-two project-three
|
|
273
274
|
```
|
|
274
275
|
|
|
275
276
|
### `ws-releases`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import json, os, pytest
|
|
1
|
+
import json, os, runpy, shutil, sys, pytest
|
|
2
2
|
from types import SimpleNamespace
|
|
3
3
|
from fastgit import Git
|
|
4
4
|
import fastws.core as core
|
|
@@ -6,7 +6,7 @@ import fastws.core as core
|
|
|
6
6
|
os.environ.update(GIT_AUTHOR_NAME='fastws', GIT_AUTHOR_EMAIL='fastws@example.com',
|
|
7
7
|
GIT_COMMITTER_NAME='fastws', GIT_COMMITTER_EMAIL='fastws@example.com')
|
|
8
8
|
|
|
9
|
-
WS_META = '[project]\nname = "uvws"\ndependencies = [\n "repo1pkg",\n "keeppkg",\n]\n\n[tool.uv.sources]\nrepo1pkg = { workspace = true }\nkeeppkg = { workspace = true }\n'
|
|
9
|
+
WS_META = '[project]\nname = "uvws"\ndependencies = [\n "repo1pkg",\n "repo2pkg",\n "keeppkg",\n]\n\n[tool.uv.sources]\nrepo1pkg = { workspace = true }\nrepo2pkg = { workspace = true }\nkeeppkg = { workspace = true }\n'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def mk_repo(d, origin=None):
|
|
@@ -301,47 +301,50 @@ async def test_git_repo_resolution(tmp_path):
|
|
|
301
301
|
def test_ws_remove_workflow(tmp_path, monkeypatch, fake_uv):
|
|
302
302
|
(tmp_path/'repos.txt').write_text('AnswerDotAI/keep\n')
|
|
303
303
|
local = tmp_path/'repos-local.txt'
|
|
304
|
-
local.write_text('AnswerDotAI/repo1\norg/external ../external\n')
|
|
304
|
+
local.write_text('AnswerDotAI/repo1\nAnswerDotAI/repo2\norg/external ../external\n')
|
|
305
305
|
(tmp_path/'pyproject.toml').write_text(WS_META)
|
|
306
|
-
repo = tmp_path/'repo1'
|
|
307
|
-
repo
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
repo, repo2 = tmp_path/'repo1', tmp_path/'repo2'
|
|
307
|
+
for d in repo, repo2:
|
|
308
|
+
d.mkdir()
|
|
309
|
+
(d/'pyproject.toml').write_text(f'[project]\nname = "{d.name}pkg"\n')
|
|
310
|
+
g = mk_repo(d, origin=tmp_path/'origins'/d.name)
|
|
311
|
+
answer = ['n', 'y', 'y']
|
|
312
|
+
monkeypatch.setattr('builtins.input', lambda *a: answer.pop(0))
|
|
312
313
|
|
|
313
|
-
with pytest.raises(SystemExit, match='baseline'): core.ws_remove('AnswerDotAI/keep', workspace=str(tmp_path))
|
|
314
|
-
with pytest.raises(SystemExit, match='location'): core.ws_remove('org/external', workspace=str(tmp_path))
|
|
314
|
+
with pytest.raises(SystemExit, match='baseline'): core.ws_remove('repo1', 'AnswerDotAI/keep', workspace=str(tmp_path))
|
|
315
|
+
with pytest.raises(SystemExit, match='location'): core.ws_remove('repo1', 'org/external', workspace=str(tmp_path))
|
|
315
316
|
assert 'org/external ../external' in local.read_text()
|
|
316
317
|
assert (tmp_path/'repos.txt').read_text() == 'AnswerDotAI/keep\n'
|
|
317
318
|
assert not fake_uv
|
|
318
319
|
|
|
319
320
|
# a dirty tree refuses before any mutation
|
|
320
|
-
(
|
|
321
|
-
with pytest.raises(SystemExit): core.ws_remove('AnswerDotAI/repo1', workspace=str(tmp_path))
|
|
322
|
-
assert repo.exists() and 'repo1' in local.read_text()
|
|
321
|
+
(repo2/'pyproject.toml').write_text('[project]\nname = "repo2pkg"\nversion = "1"\n')
|
|
322
|
+
with pytest.raises(SystemExit): core.ws_remove('AnswerDotAI/repo1', 'repo2', workspace=str(tmp_path))
|
|
323
|
+
assert repo.exists() and repo2.exists() and 'repo1' in local.read_text() and 'repo2' in local.read_text()
|
|
324
|
+
assert (tmp_path/'pyproject.toml').read_text() == WS_META
|
|
323
325
|
|
|
324
326
|
# so does an unpushed commit on a clean tree
|
|
325
327
|
g.commit('-a', m='ahead')
|
|
326
|
-
with pytest.raises(SystemExit): core.ws_remove('AnswerDotAI/repo1', workspace=str(tmp_path))
|
|
327
|
-
assert repo.exists()
|
|
328
|
+
with pytest.raises(SystemExit): core.ws_remove('AnswerDotAI/repo1', 'repo2', workspace=str(tmp_path))
|
|
329
|
+
assert repo.exists() and repo2.exists()
|
|
328
330
|
|
|
329
|
-
#
|
|
331
|
+
# Each checkout has its own prompt; duplicate targets are removed once, with one sync for the batch.
|
|
330
332
|
g.push()
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
assert
|
|
334
|
-
assert
|
|
335
|
-
assert '
|
|
336
|
-
assert
|
|
333
|
+
monkeypatch.setattr(sys, 'argv', ['ws-remove', 'AnswerDotAI/repo1', 'repo2', 'repo1', '--workspace', str(tmp_path)])
|
|
334
|
+
with pytest.raises(SystemExit) as exc: runpy.run_path(shutil.which('ws-remove'), run_name='__main__')
|
|
335
|
+
assert exc.value.code in (None, 0)
|
|
336
|
+
assert repo.exists() and not repo2.exists()
|
|
337
|
+
assert 'repo1' not in local.read_text() and 'repo2' not in local.read_text()
|
|
338
|
+
assert 'repo1pkg' not in (content := (tmp_path/'pyproject.toml').read_text()) and 'repo2pkg' not in content
|
|
339
|
+
assert 'keeppkg' in content and fake_uv == [['uv', 'sync']]
|
|
337
340
|
|
|
338
341
|
# answering 'y' (by folder name this time) also deletes the checkout
|
|
339
342
|
local.write_text('AnswerDotAI/repo1\n')
|
|
340
343
|
(tmp_path/'pyproject.toml').write_text(WS_META)
|
|
341
|
-
answer[0] = 'y'
|
|
342
344
|
core.ws_remove('repo1', workspace=str(tmp_path))
|
|
343
345
|
assert not repo.exists()
|
|
344
346
|
assert (tmp_path/'repos.txt').read_text() == 'AnswerDotAI/keep\n'
|
|
347
|
+
assert not answer
|
|
345
348
|
|
|
346
349
|
|
|
347
350
|
def test_upgrade_stamp(tmp_path):
|
|
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
|