Skip to content

Commit 1dec316

Browse files
committed
Add repo remote verifier and improve port conflict warnings
1 parent 70e4c20 commit 1dec316

3 files changed

Lines changed: 173 additions & 4 deletions

File tree

local_nexus_controller/services/registry_import.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
KeyRef,
1313
Service,
1414
)
15-
from local_nexus_controller.services.ports import is_port_in_use, next_available_port, reserved_ports
15+
from local_nexus_controller.services.ports import is_port_in_use, next_available_port
1616

1717

1818
def _now_utc() -> datetime:
@@ -84,9 +84,12 @@ def import_bundle(session: Session, bundle: ImportBundle, host_for_port_checks:
8484

8585
# Check conflicts
8686
if port is not None:
87-
reserved = reserved_ports(session)
88-
if port in reserved:
89-
warnings.append(f"Port {port} is already reserved in the registry.")
87+
# Only warn if the port is reserved by a *different* service.
88+
conflicting = session.exec(
89+
select(Service).where(Service.port == int(port), Service.name != bundle.service.name)
90+
).first()
91+
if conflicting:
92+
warnings.append(f"Port {port} is already reserved in the registry (by '{conflicting.name}').")
9093
if is_port_in_use(host_for_port_checks, port):
9194
warnings.append(f"Port {port} appears to be in use on {host_for_port_checks}.")
9295

repo_remotes.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"repos": [
3+
{
4+
"path": "C:/Users/nedpe/LocalNexusController",
5+
"name": "LocalNexusController",
6+
"expected_branch": "main",
7+
"expected_origin": "https://github.com/nedpearson/LocalProgramControlCenter.git"
8+
},
9+
{
10+
"path": "C:/Users/nedpe/job-tracker",
11+
"name": "job-tracker",
12+
"expected_branch": "main",
13+
"expected_origin": "https://github.com/nedpearson/JobTracker.git"
14+
},
15+
{
16+
"path": "C:/Users/nedpe/Documents/PearsonNexusAI/prototype",
17+
"name": "prototype",
18+
"expected_branch": "main",
19+
"expected_origin": "https://github.com/nedpearson/New-Pearson_Nexus_AI.git"
20+
},
21+
{
22+
"path": "C:/Users/nedpe/backend",
23+
"name": "local-nexus-sample-backend",
24+
"expected_branch": "main",
25+
"expected_origin": "https://github.com/nedpearson/LocalNexus-Sample-Backend.git"
26+
},
27+
{
28+
"path": "C:/Users/nedpe/frontend",
29+
"name": "local-nexus-sample-frontend",
30+
"expected_branch": "main",
31+
"expected_origin": "https://github.com/nedpearson/LocalNexus-Sample-Frontend.git"
32+
}
33+
]
34+
}

tools/verify_remotes.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import json
5+
import subprocess
6+
import sys
7+
from dataclasses import dataclass
8+
from pathlib import Path
9+
from typing import Any
10+
11+
12+
@dataclass(frozen=True)
13+
class RepoSpec:
14+
path: str
15+
name: str
16+
expected_origin: str
17+
expected_branch: str = "main"
18+
19+
20+
def _run_git(repo_path: Path, args: list[str]) -> tuple[int, str, str]:
21+
p = subprocess.run(
22+
["git", "-C", str(repo_path), *args],
23+
capture_output=True,
24+
text=True,
25+
)
26+
return p.returncode, (p.stdout or "").strip(), (p.stderr or "").strip()
27+
28+
29+
def _load_specs(config_path: Path) -> list[RepoSpec]:
30+
raw = json.loads(config_path.read_text(encoding="utf-8"))
31+
repos = raw.get("repos")
32+
if not isinstance(repos, list):
33+
raise SystemExit("Config must contain a top-level 'repos' list.")
34+
35+
out: list[RepoSpec] = []
36+
for i, item in enumerate(repos, start=1):
37+
if not isinstance(item, dict):
38+
raise SystemExit(f"repos[{i}] must be an object.")
39+
path = str(item.get("path") or "").strip()
40+
name = str(item.get("name") or "").strip()
41+
expected_origin = str(item.get("expected_origin") or "").strip()
42+
expected_branch = str(item.get("expected_branch") or "main").strip()
43+
if not path or not expected_origin:
44+
raise SystemExit(f"repos[{i}] must include 'path' and 'expected_origin'.")
45+
out.append(
46+
RepoSpec(
47+
path=path,
48+
name=name or Path(path).name,
49+
expected_origin=expected_origin,
50+
expected_branch=expected_branch or "main",
51+
)
52+
)
53+
return out
54+
55+
56+
def _is_git_repo(repo_path: Path) -> bool:
57+
rc, out, _ = _run_git(repo_path, ["rev-parse", "--is-inside-work-tree"])
58+
return rc == 0 and out.lower() == "true"
59+
60+
61+
def main() -> int:
62+
ap = argparse.ArgumentParser(description="Verify each repo points to the correct origin URL.")
63+
ap.add_argument(
64+
"--config",
65+
default=str(Path(__file__).resolve().parents[1] / "repo_remotes.json"),
66+
help="Path to repo_remotes.json (defaults to controller repo root).",
67+
)
68+
ap.add_argument("--fix", action="store_true", help="Apply fixes (set origin URL and rename branch).")
69+
args = ap.parse_args()
70+
71+
config_path = Path(args.config).expanduser().resolve()
72+
if not config_path.exists():
73+
print(f"Config not found: {config_path}", file=sys.stderr)
74+
return 2
75+
76+
specs = _load_specs(config_path)
77+
failures: list[str] = []
78+
79+
for spec in specs:
80+
repo_path = Path(spec.path).expanduser().resolve()
81+
label = f"{spec.name} ({repo_path})"
82+
83+
if not repo_path.exists():
84+
failures.append(f"{label}: path does not exist")
85+
continue
86+
87+
if not _is_git_repo(repo_path):
88+
failures.append(f"{label}: not a git repo")
89+
continue
90+
91+
rc, origin, err = _run_git(repo_path, ["remote", "get-url", "origin"])
92+
if rc != 0:
93+
failures.append(f"{label}: missing origin remote ({err or 'unknown error'})")
94+
continue
95+
96+
if origin != spec.expected_origin:
97+
msg = f"{label}: origin mismatch\n actual: {origin}\n expected: {spec.expected_origin}"
98+
if args.fix:
99+
rc2, _, err2 = _run_git(repo_path, ["remote", "set-url", "origin", spec.expected_origin])
100+
if rc2 != 0:
101+
failures.append(msg + f"\n fix_failed: {err2 or 'unknown error'}")
102+
else:
103+
print(msg + "\n fixed: set-url origin")
104+
else:
105+
failures.append(msg)
106+
107+
# Branch check (best effort)
108+
rc, branch, _ = _run_git(repo_path, ["branch", "--show-current"])
109+
if rc == 0 and branch and branch != spec.expected_branch:
110+
msg = f"{label}: branch mismatch\n actual: {branch}\n expected: {spec.expected_branch}"
111+
if args.fix:
112+
rc2, _, err2 = _run_git(repo_path, ["branch", "-M", spec.expected_branch])
113+
if rc2 != 0:
114+
failures.append(msg + f"\n fix_failed: {err2 or 'unknown error'}")
115+
else:
116+
print(msg + "\n fixed: branch -M")
117+
else:
118+
failures.append(msg)
119+
120+
if failures:
121+
print("\nVERIFY FAILED:")
122+
for f in failures:
123+
print(f"- {f}")
124+
return 1
125+
126+
print("OK: all repos match expected origin/branch.")
127+
return 0
128+
129+
130+
if __name__ == "__main__":
131+
raise SystemExit(main())
132+

0 commit comments

Comments
 (0)