fastws-cli 0.0.5__tar.gz → 0.0.6__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.5 → fastws_cli-0.0.6}/CHANGELOG.md +7 -0
- {fastws_cli-0.0.5/fastws_cli.egg-info → fastws_cli-0.0.6}/PKG-INFO +2 -2
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/README.md +1 -1
- fastws_cli-0.0.6/fastws/__init__.py +4 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/fastws/core.py +38 -21
- {fastws_cli-0.0.5 → fastws_cli-0.0.6/fastws_cli.egg-info}/PKG-INFO +2 -2
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/tests/test_sync.py +16 -7
- fastws_cli-0.0.5/fastws/__init__.py +0 -6
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/LICENSE +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/MANIFEST.in +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/fastws_cli.egg-info/SOURCES.txt +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/fastws_cli.egg-info/requires.txt +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/pyproject.toml +0 -0
- {fastws_cli-0.0.5 → fastws_cli-0.0.6}/setup.cfg +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.6
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License: Apache-2.0
|
|
@@ -80,7 +80,7 @@ ws-branches --expected develop
|
|
|
80
80
|
|
|
81
81
|
### `ws-sync`
|
|
82
82
|
|
|
83
|
-
Sync the workspace metadata, install updates, and refresh Pyright editable paths.
|
|
83
|
+
Sync the workspace metadata, pull local repos, install updates, and refresh Pyright editable paths.
|
|
84
84
|
By default it uses the active venv parent as the workspace root, so you do not need to `cd` first:
|
|
85
85
|
It respects `tool.uv.workspace.members` and `exclude` when scanning local projects.
|
|
86
86
|
|
|
@@ -59,7 +59,7 @@ ws-branches --expected develop
|
|
|
59
59
|
|
|
60
60
|
### `ws-sync`
|
|
61
61
|
|
|
62
|
-
Sync the workspace metadata, install updates, and refresh Pyright editable paths.
|
|
62
|
+
Sync the workspace metadata, pull local repos, install updates, and refresh Pyright editable paths.
|
|
63
63
|
By default it uses the active venv parent as the workspace root, so you do not need to `cd` first:
|
|
64
64
|
It respects `tool.uv.workspace.members` and `exclude` when scanning local projects.
|
|
65
65
|
|
|
@@ -206,22 +206,26 @@ def _write_pyright_pth_files(root: Path) -> list[Path]:
|
|
|
206
206
|
created.append(pth)
|
|
207
207
|
return created
|
|
208
208
|
|
|
209
|
-
def _clone_one(repo: str) -> str:
|
|
210
|
-
d = _repo_dir(repo)
|
|
211
|
-
if
|
|
209
|
+
def _clone_one(repo: str, root: str = ".") -> str:
|
|
210
|
+
d = Path(root)/_repo_dir(repo)
|
|
211
|
+
if d.exists(): return f"✓ {d.name}: already exists"
|
|
212
212
|
try:
|
|
213
|
-
subprocess.run(["git", "clone", f"git@github.com:{repo}.git"], check=True, capture_output=True)
|
|
214
|
-
return f"✓ {d}: cloned"
|
|
215
|
-
except subprocess.CalledProcessError as e: return f"✗ {d}: {e.stderr.decode().strip()}"
|
|
213
|
+
subprocess.run(["git", "clone", f"git@github.com:{repo}.git", str(d)], check=True, capture_output=True)
|
|
214
|
+
return f"✓ {d.name}: cloned"
|
|
215
|
+
except subprocess.CalledProcessError as e: return f"✗ {d.name}: {e.stderr.decode().strip()}"
|
|
216
216
|
|
|
217
|
-
def _pull_one(repo: str) -> str:
|
|
218
|
-
d = _repo_dir(repo)
|
|
219
|
-
if not
|
|
217
|
+
def _pull_one(repo: str, root: str = ".") -> str:
|
|
218
|
+
d = Path(root)/_repo_dir(repo)
|
|
219
|
+
if not d.exists(): return f"✗ {d.name}: directory not found"
|
|
220
220
|
try:
|
|
221
|
-
res = subprocess.run(["git", "-C", d, "pull", "-q", "--stat"], check=True, capture_output=True, text=True)
|
|
222
|
-
return f"✓ {d}" + (f"\n{res.stdout.strip()}" if res.stdout.strip() else "")
|
|
221
|
+
res = subprocess.run(["git", "-C", str(d), "pull", "-q", "--stat"], check=True, capture_output=True, text=True)
|
|
222
|
+
return f"✓ {d.name}" + (f"\n{res.stdout.strip()}" if res.stdout.strip() else "")
|
|
223
223
|
except subprocess.CalledProcessError as e: return f"✗ {d}: {e.stderr.strip()}"
|
|
224
224
|
|
|
225
|
+
def _pull(repos: list[str], workers: int = 16, root: str = "."):
|
|
226
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
227
|
+
for result in as_completed([ex.submit(_pull_one, r, root) for r in repos]): print(result.result())
|
|
228
|
+
|
|
225
229
|
def ws_clone(
|
|
226
230
|
repos_file: str = "repos.txt", # File containing repo list (one per line: owner/repo)
|
|
227
231
|
workers: int = 16, # Number of parallel workers
|
|
@@ -235,22 +239,24 @@ def ws_clone(
|
|
|
235
239
|
def ws_clone_cli(
|
|
236
240
|
repos_file: str = "repos.txt", # File containing repo list (one per line: owner/repo)
|
|
237
241
|
workers: int = 16, # Number of parallel workers
|
|
238
|
-
):
|
|
242
|
+
):
|
|
243
|
+
"Clone all repos from a repos file."
|
|
244
|
+
ws_clone(repos_file, workers)
|
|
239
245
|
|
|
240
246
|
def ws_pull(
|
|
241
247
|
repos_file: str = "repos.txt", # File containing repo list
|
|
242
248
|
workers: int = 16, # Number of parallel workers
|
|
243
249
|
):
|
|
244
250
|
"Pull updates for all repos."
|
|
245
|
-
|
|
246
|
-
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
247
|
-
for result in as_completed([ex.submit(_pull_one, r) for r in repos]): print(result.result())
|
|
251
|
+
_pull(_load_repos(repos_file), workers)
|
|
248
252
|
|
|
249
253
|
@call_parse
|
|
250
254
|
def ws_pull_cli(
|
|
251
255
|
repos_file: str = "repos.txt", # File containing repo list
|
|
252
256
|
workers: int = 16, # Number of parallel workers
|
|
253
|
-
):
|
|
257
|
+
):
|
|
258
|
+
"Pull updates for all repos."
|
|
259
|
+
ws_pull(repos_file, workers)
|
|
254
260
|
|
|
255
261
|
def ws_status(
|
|
256
262
|
repos_file: str = "repos.txt", # File containing repo list
|
|
@@ -281,7 +287,9 @@ def ws_status(
|
|
|
281
287
|
def ws_status_cli(
|
|
282
288
|
repos_file: str = "repos.txt", # File containing repo list
|
|
283
289
|
branches: bool = False, # Show unpushed commit details
|
|
284
|
-
):
|
|
290
|
+
):
|
|
291
|
+
"Show uncommitted changes and optionally unpushed commit details across repos."
|
|
292
|
+
ws_status(repos_file, branches)
|
|
285
293
|
|
|
286
294
|
def ws_branches(
|
|
287
295
|
repos_file: str = "repos.txt", # File containing repo list
|
|
@@ -305,7 +313,9 @@ def ws_branches(
|
|
|
305
313
|
def ws_branches_cli(
|
|
306
314
|
repos_file: str = "repos.txt", # File containing repo list
|
|
307
315
|
expected: str = "main", # Expected branch name
|
|
308
|
-
):
|
|
316
|
+
):
|
|
317
|
+
"Check if all repos are on the expected branch."
|
|
318
|
+
ws_branches(repos_file, expected)
|
|
309
319
|
|
|
310
320
|
def ws_sync(
|
|
311
321
|
workspace: str = "", # Workspace root; defaults to active venv parent when available
|
|
@@ -318,8 +328,10 @@ def ws_sync(
|
|
|
318
328
|
repos_path = _resolve_path(root, repos_file)
|
|
319
329
|
pyproject_path = _resolve_path(root, pyproject_file)
|
|
320
330
|
template_path = _resolve_path(root, template_file)
|
|
331
|
+
repos = _discover_ws_repos(root)
|
|
321
332
|
|
|
322
|
-
if missing_repos := _update_repos_file(repos_path,
|
|
333
|
+
if missing_repos := _update_repos_file(repos_path, repos): print(f"Added repos: {', '.join(missing_repos)}")
|
|
334
|
+
_pull(repos, root=str(root))
|
|
323
335
|
|
|
324
336
|
if missing_projects := _sync_ws_pyproject(pyproject_path, template_path, _ws_projects(root)): print(f"Added workspace projects: {', '.join(missing_projects)}")
|
|
325
337
|
|
|
@@ -332,7 +344,9 @@ def ws_sync_cli(
|
|
|
332
344
|
repos_file: str = "repos.txt", # Repo list to update from local git remotes
|
|
333
345
|
pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
|
|
334
346
|
template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
|
|
335
|
-
):
|
|
347
|
+
):
|
|
348
|
+
"Sync workspace metadata, run uv sync -U, and refresh Pyright editable paths."
|
|
349
|
+
ws_sync(workspace, repos_file, pyproject_file, template_file)
|
|
336
350
|
|
|
337
351
|
def ws_add(
|
|
338
352
|
repo: str, # Repo to add, e.g. AnswerDotAI/fastws
|
|
@@ -348,6 +362,7 @@ def ws_add(
|
|
|
348
362
|
added = _update_repos_file(repos_path, [repo])
|
|
349
363
|
if added: print(f"Added repo: {repo}")
|
|
350
364
|
else: print(f"Repo already present: {repo}")
|
|
365
|
+
print(_clone_one(repo, str(root)))
|
|
351
366
|
ws_sync(str(root), repos_file, pyproject_file, template_file)
|
|
352
367
|
|
|
353
368
|
@call_parse
|
|
@@ -357,4 +372,6 @@ def ws_add_cli(
|
|
|
357
372
|
repos_file: str = "repos.txt", # Repo list to update
|
|
358
373
|
pyproject_file: str = "pyproject.toml", # Workspace pyproject to update
|
|
359
374
|
template_file: str = "pyproject.tmpl", # Template copied when pyproject.toml is missing
|
|
360
|
-
):
|
|
375
|
+
):
|
|
376
|
+
"Add a repo to repos.txt and then run ws-sync."
|
|
377
|
+
ws_add(repo, workspace, repos_file, pyproject_file, template_file)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License: Apache-2.0
|
|
@@ -80,7 +80,7 @@ ws-branches --expected develop
|
|
|
80
80
|
|
|
81
81
|
### `ws-sync`
|
|
82
82
|
|
|
83
|
-
Sync the workspace metadata, install updates, and refresh Pyright editable paths.
|
|
83
|
+
Sync the workspace metadata, pull local repos, install updates, and refresh Pyright editable paths.
|
|
84
84
|
By default it uses the active venv parent as the workspace root, so you do not need to `cd` first:
|
|
85
85
|
It respects `tool.uv.workspace.members` and `exclude` when scanning local projects.
|
|
86
86
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
1
|
import fastws.core as core
|
|
4
2
|
|
|
5
3
|
|
|
@@ -98,6 +96,9 @@ def test_ws_sync_updates_workspace_and_runs_uv(tmp_path, monkeypatch):
|
|
|
98
96
|
if cmd[:5] == ["git", "-C", str(repo), "remote", "get-url"]:
|
|
99
97
|
class Res: stdout = "git@github.com:AnswerDotAI/repo1.git\n"
|
|
100
98
|
return Res()
|
|
99
|
+
if cmd[:4] == ["git", "-C", str(repo), "pull"]:
|
|
100
|
+
class Res: stdout = ""
|
|
101
|
+
return Res()
|
|
101
102
|
if cmd == ["uv", "sync", "-U"]:
|
|
102
103
|
class Res: stdout = ""
|
|
103
104
|
return Res()
|
|
@@ -112,6 +113,7 @@ def test_ws_sync_updates_workspace_and_runs_uv(tmp_path, monkeypatch):
|
|
|
112
113
|
assert 'newpkg = { workspace = true }' in pyproject
|
|
113
114
|
assert '"newpkg"' in pyproject
|
|
114
115
|
assert (site/"_pyright_editable_newpkg.pth").read_text() == "/tmp/ws/src/newpkg\n"
|
|
116
|
+
assert next(i for i,(cmd,_) in enumerate(calls) if cmd[:4] == ["git", "-C", str(repo), "pull"]) < next(i for i,(cmd,_) in enumerate(calls) if cmd == ["uv", "sync", "-U"])
|
|
115
117
|
assert any(cmd == ["uv", "sync", "-U"] and kwargs["cwd"] == tmp_path for cmd,kwargs in calls)
|
|
116
118
|
|
|
117
119
|
|
|
@@ -138,6 +140,9 @@ def test_ws_sync_uses_active_venv_parent_by_default(tmp_path, monkeypatch):
|
|
|
138
140
|
if cmd[:5] == ["git", "-C", str(repo), "remote", "get-url"]:
|
|
139
141
|
class Res: stdout = "git@github.com:AnswerDotAI/repo1.git\n"
|
|
140
142
|
return Res()
|
|
143
|
+
if cmd[:4] == ["git", "-C", str(repo), "pull"]:
|
|
144
|
+
class Res: stdout = ""
|
|
145
|
+
return Res()
|
|
141
146
|
if cmd == ["uv", "sync", "-U"]:
|
|
142
147
|
class Res: stdout = ""
|
|
143
148
|
return Res()
|
|
@@ -153,19 +158,23 @@ def test_ws_sync_uses_active_venv_parent_by_default(tmp_path, monkeypatch):
|
|
|
153
158
|
assert any(cmd == ["uv", "sync", "-U"] and kwargs["cwd"] == workspace for cmd,kwargs in calls)
|
|
154
159
|
|
|
155
160
|
|
|
156
|
-
def
|
|
161
|
+
def test_ws_add_clones_and_syncs(tmp_path, monkeypatch):
|
|
157
162
|
(tmp_path/"repos.txt").write_text("AnswerDotAI/existing\n")
|
|
158
|
-
|
|
163
|
+
sync_calls, clone_calls = [], []
|
|
159
164
|
|
|
160
|
-
def fake_sync(
|
|
161
|
-
|
|
165
|
+
def fake_sync(*a): sync_calls.append(a)
|
|
166
|
+
def fake_clone(repo, root="."):
|
|
167
|
+
clone_calls.append((repo, root))
|
|
168
|
+
return "✓ cloned"
|
|
162
169
|
|
|
163
170
|
monkeypatch.setattr(core, "ws_sync", fake_sync)
|
|
171
|
+
monkeypatch.setattr(core, "_clone_one", fake_clone)
|
|
164
172
|
|
|
165
173
|
core.ws_add("answerdotai/fastws", workspace=str(tmp_path))
|
|
166
174
|
|
|
167
175
|
assert (tmp_path/"repos.txt").read_text() == "AnswerDotAI/existing\nanswerdotai/fastws\n"
|
|
168
|
-
assert
|
|
176
|
+
assert clone_calls == [("answerdotai/fastws", str(tmp_path))]
|
|
177
|
+
assert sync_calls == [(str(tmp_path), "repos.txt", "pyproject.toml", "pyproject.tmpl")]
|
|
169
178
|
|
|
170
179
|
|
|
171
180
|
def test_ws_status_summarizes_unpushed_commits_by_default(tmp_path, monkeypatch, capsys):
|
|
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
|