Skip to content

Commit b79e1ee

Browse files
committed
feat(sync): clone missing repos before pulling updates
1 parent 828ceae commit b79e1ee

5 files changed

Lines changed: 81 additions & 24 deletions

File tree

packages/smartem-workspace/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "smartem-workspace"
7-
version = "0.4.0"
7+
version = "0.5.0"
88
description = "CLI tool to automate SmartEM multi-repo workspace setup"
99
readme = "README.md"
1010
license = "Apache-2.0"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""SmartEM workspace setup CLI tool."""
22

3-
__version__ = "0.4.0"
3+
__version__ = "0.5.0"

packages/smartem-workspace/smartem_workspace/cli.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,20 @@ def sync(
259259
bool,
260260
typer.Option("--dry-run", "-n", help="Show what would be done without making changes"),
261261
] = False,
262+
git_ssh: Annotated[
263+
bool,
264+
typer.Option("--git-ssh", help="Force SSH URLs for cloning (default: auto-detect)"),
265+
] = False,
266+
git_https: Annotated[
267+
bool,
268+
typer.Option("--git-https", help="Force HTTPS URLs for cloning (default: auto-detect)"),
269+
] = False,
262270
path: Annotated[
263271
Path | None,
264272
typer.Option("--path", "-p", help="Workspace path (auto-detected if not specified)"),
265273
] = None,
266274
) -> None:
267-
"""Pull latest changes from all cloned repositories."""
275+
"""Sync workspace: clone missing repos and pull updates for existing ones."""
268276
out = get_console()
269277
workspace_path = path or find_workspace_root()
270278
if workspace_path is None:
@@ -276,11 +284,17 @@ def sync(
276284
out.print("[red]Failed to load configuration[/red]")
277285
raise typer.Exit(1)
278286

287+
if git_ssh and git_https:
288+
out.print("[red]Cannot specify both --git-ssh and --git-https[/red]")
289+
raise typer.Exit(1)
290+
291+
use_ssh: bool | None = True if git_ssh else (False if git_https else None)
292+
279293
out.print("[bold blue]SmartEM Workspace Sync[/bold blue]")
280294
out.print(f"Workspace: {workspace_path}")
281295

282-
results = sync_all_repos(workspace_path, config, dry_run=dry_run)
283-
print_sync_results(results)
296+
results = sync_all_repos(workspace_path, config, out, dry_run=dry_run, use_ssh=use_ssh)
297+
print_sync_results(results, out)
284298

285299
errors = sum(1 for r in results if r.status == "error")
286300
if errors:

packages/smartem-workspace/smartem_workspace/commands/sync.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,23 @@
77
from rich.console import Console
88
from rich.progress import Progress, SpinnerColumn, TextColumn
99

10-
from smartem_workspace.config.schema import ReposConfig
11-
from smartem_workspace.setup.repos import get_local_dir
10+
from smartem_workspace.config.schema import Organization, ReposConfig, Repository
11+
from smartem_workspace.setup.repos import clone_repo, get_local_dir
1212
from smartem_workspace.utils.git import (
13+
check_github_ssh_auth,
1314
fetch_remote,
1415
get_commits_behind,
1516
get_current_branch,
1617
has_uncommitted_changes,
1718
run_git_command,
1819
)
1920

20-
console = Console()
21-
2221

2322
@dataclass
2423
class SyncResult:
2524
repo_name: str
2625
org_name: str
27-
status: Literal["updated", "up-to-date", "error", "skipped", "dry-run"]
26+
status: Literal["updated", "up-to-date", "error", "skipped", "dry-run", "cloned"]
2827
message: str
2928
commits_behind: int = 0
3029

@@ -69,41 +68,80 @@ def sync_single_repo(repo_path: Path, dry_run: bool = False) -> SyncResult:
6968
def sync_all_repos(
7069
workspace_path: Path,
7170
config: ReposConfig,
71+
console: Console,
7272
dry_run: bool = False,
73+
use_ssh: bool | None = None,
7374
) -> list[SyncResult]:
7475
repos_dir = workspace_path / "repos"
75-
results = []
76+
results: list[SyncResult] = []
7677

7778
if not repos_dir.exists():
78-
console.print("[red]repos directory not found[/red]")
79-
return results
79+
repos_dir.mkdir(parents=True, exist_ok=True)
80+
81+
missing_repos: list[tuple[Organization, Repository, Path]] = []
82+
existing_repos: list[tuple[str, str, Path]] = []
8083

81-
repo_paths = []
8284
for org in config.organizations:
8385
local_dir = get_local_dir(org)
8486
org_dir = repos_dir / local_dir
8587

8688
for repo in org.repos:
8789
repo_path = org_dir / repo.name
8890
if repo_path.exists():
89-
repo_paths.append((org.name, repo.name, repo_path))
90-
91-
if not repo_paths:
92-
console.print("[yellow]No cloned repositories found[/yellow]")
91+
existing_repos.append((org.name, repo.name, repo_path))
92+
else:
93+
missing_repos.append((org, repo, repo_path))
94+
95+
if missing_repos:
96+
github_ssh_ok: bool | None = None
97+
if use_ssh is None:
98+
has_github = any(org.provider == "github" for org, _, _ in missing_repos)
99+
if has_github:
100+
github_ssh_ok = check_github_ssh_auth()
101+
102+
action = "Would clone" if dry_run else "Cloning"
103+
console.print(f"\n[bold]{action} {len(missing_repos)} missing repositories...[/bold]\n")
104+
105+
with Progress(
106+
SpinnerColumn(),
107+
TextColumn("[progress.description]{task.description}"),
108+
console=console,
109+
transient=True,
110+
) as progress:
111+
task = progress.add_task("Starting...", total=len(missing_repos))
112+
113+
for org, repo, repo_path in missing_repos:
114+
progress.update(task, description=f"{org.name}/{repo.name}")
115+
116+
if dry_run:
117+
results.append(SyncResult(repo.name, org.name, "dry-run", "Would clone"))
118+
else:
119+
success = clone_repo(repo, org, repos_dir, use_ssh, github_ssh_ok)
120+
if success:
121+
results.append(SyncResult(repo.name, org.name, "cloned", "Cloned successfully"))
122+
existing_repos.append((org.name, repo.name, repo_path))
123+
else:
124+
results.append(SyncResult(repo.name, org.name, "error", "Clone failed"))
125+
126+
progress.advance(task)
127+
128+
if not existing_repos:
129+
if not missing_repos:
130+
console.print("[yellow]No repositories configured[/yellow]")
93131
return results
94132

95133
action = "Checking" if dry_run else "Syncing"
96-
console.print(f"\n[bold]{action} {len(repo_paths)} repositories...[/bold]\n")
134+
console.print(f"\n[bold]{action} {len(existing_repos)} repositories...[/bold]\n")
97135

98136
with Progress(
99137
SpinnerColumn(),
100138
TextColumn("[progress.description]{task.description}"),
101139
console=console,
102140
transient=True,
103141
) as progress:
104-
task = progress.add_task("Starting...", total=len(repo_paths))
142+
task = progress.add_task("Starting...", total=len(existing_repos))
105143

106-
for org_name, repo_name, repo_path in repo_paths:
144+
for org_name, repo_name, repo_path in existing_repos:
107145
progress.update(task, description=f"{org_name}/{repo_name}")
108146
result = sync_single_repo(repo_path, dry_run=dry_run)
109147
results.append(result)
@@ -112,7 +150,8 @@ def sync_all_repos(
112150
return results
113151

114152

115-
def print_sync_results(results: list[SyncResult]) -> None:
153+
def print_sync_results(results: list[SyncResult], console: Console) -> None:
154+
cloned = sum(1 for r in results if r.status == "cloned")
116155
updated = sum(1 for r in results if r.status == "updated")
117156
up_to_date = sum(1 for r in results if r.status == "up-to-date")
118157
skipped = sum(1 for r in results if r.status == "skipped")
@@ -122,7 +161,9 @@ def print_sync_results(results: list[SyncResult]) -> None:
122161
for result in results:
123162
full_name = f"{result.org_name}/{result.repo_name}"
124163

125-
if result.status == "updated":
164+
if result.status == "cloned":
165+
console.print(f" [green]+[/green] {full_name}: {result.message}")
166+
elif result.status == "updated":
126167
console.print(f" [green]\u2713[/green] {full_name}: {result.message}")
127168
elif result.status == "up-to-date":
128169
console.print(f" [dim]\u2713 {full_name}: {result.message}[/dim]")
@@ -135,6 +176,8 @@ def print_sync_results(results: list[SyncResult]) -> None:
135176

136177
console.print()
137178
parts = []
179+
if cloned:
180+
parts.append(f"[green]{cloned} cloned[/green]")
138181
if updated:
139182
parts.append(f"[green]{updated} updated[/green]")
140183
if would_update:

packages/smartem-workspace/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)