diff --git a/.gitignore b/.gitignore index d2e0b50d..1fbaf9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -78,7 +78,8 @@ venv/ # Superpowers plugin .superpowers/ -docs/superpowers/ +docs/superpowers/* +!docs/superpowers/baselines/ # Docker & Helm (bundled separately) Dockerfile @@ -89,3 +90,7 @@ helm/ neo4j-data/ graph.db/ .worktrees/ + +# Phase A baseline +.seeds/ +docs/superpowers/baselines/**/raw/** diff --git a/docs/superpowers/baselines/2026-04-17/BASELINE.md b/docs/superpowers/baselines/2026-04-17/BASELINE.md new file mode 100644 index 00000000..f2beb9fc --- /dev/null +++ b/docs/superpowers/baselines/2026-04-17/BASELINE.md @@ -0,0 +1,285 @@ +# code-iq Baseline — 2026-04-17 + +This file is generated by `scripts/baseline/consolidate.sh`. Re-run after +updating any capture script. Raw artifacts under `raw/` are gitignored. + +## Toolchain + +- Java: openjdk version "25.0.2" 2026-01-20 LTS +- Maven: Apache Maven 3.8.7 +- Node: v24.15.0 +- npm: 11.12.1 + +## Maven build & tests + +```json +{ + "tests": 3059, + "failures": 0, + "errors": 0, + "skipped": 31 +} +``` + +## Coverage (JaCoCo) + +```json +{ + "inst_covered": 82247, + "inst_missed": 10270, + "br_covered": 5931, + "br_missed": 2388, + "line_covered": 16515, + "line_missed": 1990, + "inst_pct": 88.9, + "br_pct": 71.29, + "line_pct": 89.25 +} +``` + +## Flaky tests + +```json +{ + "runs": 3, + "failures_per_run": [ + 0, + 0, + 0 + ], + "always_failing": [], + "flaky": [] +} +``` + +## SpotBugs + +```json +{ + "total_bugs": 1492, + "by_priority": { + "2": 1484, + "1": 8 + }, + "by_category": { + "STYLE": 546, + "MALICIOUS_CODE": 203, + "I18N": 1, + "BAD_PRACTICE": 736, + "MT_CORRECTNESS": 1, + "PERFORMANCE": 4, + "CORRECTNESS": 1 + }, + "top_patterns": [ + [ + "NM_METHOD_NAMING_CONVENTION", + 730 + ], + [ + "SF_SWITCH_NO_DEFAULT", + 448 + ], + [ + "EI_EXPOSE_REP2", + 77 + ], + [ + "MS_PKGPROTECT", + 60 + ], + [ + "BC_UNCONFIRMED_CAST", + 55 + ], + [ + "EI_EXPOSE_REP", + 46 + ], + [ + "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", + 26 + ], + [ + "MS_FINAL_PKGPROTECT", + 20 + ], + [ + "DLS_DEAD_LOCAL_STORE", + 5 + ], + [ + "SF_SWITCH_FALLTHROUGH", + 4 + ], + [ + "UC_USELESS_OBJECT", + 3 + ], + [ + "CT_CONSTRUCTOR_THROW", + 2 + ], + [ + "REC_CATCH_EXCEPTION", + 2 + ], + [ + "WMI_WRONG_MAP_ITERATOR", + 2 + ], + [ + "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", + 2 + ], + [ + "ES_COMPARING_STRINGS_WITH_EQ", + 2 + ], + [ + "DB_DUPLICATE_BRANCHES", + 1 + ], + [ + "DM_DEFAULT_ENCODING", + 1 + ], + [ + "UL_UNRELEASED_LOCK_EXCEPTION_PATH", + 1 + ], + [ + "UPM_UNCALLED_PRIVATE_METHOD", + 1 + ] + ] +} +``` + +## OWASP dependency-check + +```json +{ + "status": "FAILED", + "reason": "NVD DB update race: UpdateException (H2 lock) + NoDataException during first NVD sync. Re-run after clearing ~/.m2/repository/org/owasp/dependency-check-data/*.lock and optionally wiping the data dir.", + "captured_log": "docs/superpowers/baselines/2026-04-17/raw/depcheck.log", + "maven_exit_code": 1, + "timestamp": "2026-04-17T08:06:05Z", + "by_severity": {}, + "top_25": [] +} +``` + +## Frontend + +- Playwright: +```json +{ + "passed": 0, + "failed": 575, + "skipped": 0 +} +``` +- Full logs: `raw/frontend/` (local only). + +## Pipeline on seed repos + +### spring-petclinic +```json +{ + "seed": "spring-petclinic", + "timings": [ + "index duration=8s rc=0", + "enrich duration=13s rc=0", + "health=fail" + ], + "stats": null, + "health_ok": false +} +``` + +### realworld-express +```json +{ + "seed": "realworld-express", + "timings": [ + "index duration=5s rc=0", + "enrich duration=10s rc=0", + "health=fail" + ], + "stats": null, + "health_ok": false +} +``` + +## Known gaps / issues + +Ordered by severity. Each item cites the raw artifact it was derived from. + +### Critical + +- **OWASP dependency-check failed.** NVD initial sync hit `UpdateException: Unable to obtain exclusive lock on H2 database` followed by `NoDataException: No documents exist`. Maven exit 1 after 40 min. No CVE inventory captured. Must re-run (see §Re-run instructions below) before any security posture claim. + - Raw: `raw/depcheck.log`, `raw/depcheck-summary.json` (stub, `status=FAILED`). + +- **Playwright E2E: 0 passed / 575 failed.** 100% failure rate. Almost certainly environment/config rather than regressions — the audit script runs `npm run test:e2e` without starting the backend (`java -jar ... serve`), so any test that hits `/api/*` will fail. Needs a harness that spins up the server (or mocks it) before running Playwright, or a `webServer` entry in `playwright.config.ts`. + - Raw: `raw/frontend/playwright.log`, `raw/frontend/playwright-summary.json`. + +### High + +- **Pipeline serve-smoke failed on both seed repos** (`health=fail`, `stats=null`). `index` and `enrich` succeeded (petclinic 8+13s, express 5+10s) but the 8-second sleep between starting `serve` and `curl /actuator/health` is at the low end of the documented 8–16s Spring Boot + embedded Neo4j cold-start window (see CLAUDE.md §Gotchas). Fix in Phase F hardening: poll `/actuator/health` with a retry budget instead of a fixed sleep. + - Raw: `raw/pipeline/spring-petclinic/`, `raw/pipeline/realworld-express/`. + +- **SpotBugs: 8 HIGH-priority findings (priority=1) + 1,484 at priority=2.** Total 1,492. HIGH findings must be triaged individually (read `raw/spotbugs.xml`). Noise-dominant rules (`NM_METHOD_NAMING_CONVENTION`=730, `SF_SWITCH_NO_DEFAULT`=448) should be filtered via a SpotBugs exclude file so real signal surfaces; real-concern patterns that deserve review now: `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE` (26), `BC_UNCONFIRMED_CAST` (55), `UL_UNRELEASED_LOCK_EXCEPTION_PATH` (1), `WMI_WRONG_MAP_ITERATOR` (2), `ES_COMPARING_STRINGS_WITH_EQ` (2), `MT_CORRECTNESS` category (1). + - Raw: `raw/spotbugs.xml`, `raw/spotbugs-summary.json`. + +### Medium + +- **Branch coverage 71.3% is notably below instruction coverage 89.0%.** Expected for a detector-heavy codebase, but targeted branch coverage on the enrichment / linker / LayerClassifier paths (which drive deterministic output) is worth a focused improvement pass in Phase E. + - Raw: `raw/coverage-summary.json`, `raw/jacoco.csv`. + +- **31 skipped tests.** Not investigated. Read surefire reports to confirm they're intentional (`@Disabled` / profile-gated) and not silently excluded. + - Raw: `raw/surefire-reports.tar`. + +### Low / noise + +- `consolidate.sh` prints the Maven version with raw ANSI escape codes (`[1mApache Maven 3.8.7[m`). Strip with `sed 's/\x1b\[[0-9;]*m//g'` in a follow-up. Cosmetic only. + +### Green + +- **3,059 tests, 0 failures, 0 errors.** Clean. +- **Flaky scan: 0 always-failing, 0 flaky across 3 runs.** Suite is stable. +- **Instruction coverage 89.0%**, line coverage 89.25%. Strong baseline. +- **`npm audit` + Vite build: no blocking issues** recorded in the capture. +- **Pipeline `index` and `enrich` succeeded deterministically** on both seed repos. + +## Re-run instructions (for blocked captures) + +### OWASP dep-check +```bash +# 1. Stop any lingering dep-check processes +pkill -f dependency-check 2>/dev/null +# 2. Clear NVD locks (and optionally wipe the partial DB) +rm -f ~/.m2/repository/org/owasp/dependency-check-data/11.0/*.lock +# rm -rf ~/.m2/repository/org/owasp/dependency-check-data/11.0 # fallback if DB is corrupt +# 3. Re-run +./scripts/baseline/run-depcheck.sh +``` + +### Pipeline serve-smoke +Patch `scripts/baseline/run-pipeline.sh` to replace the `sleep 8` with a poll loop: +```bash +for _ in $(seq 1 60); do + if curl -sf "http://127.0.0.1:$PORT/actuator/health" > "$OUT/health.json"; then break; fi + sleep 2 +done +``` +Then re-run `./scripts/baseline/run-pipeline.sh spring-petclinic` and `realworld-express`. + +### Playwright E2E +Add a `webServer` entry to `src/main/frontend/playwright.config.ts` that starts the code-iq server against a fixture repo, or supply a mock backend. Then re-run `./scripts/baseline/run-frontend-audit.sh`. + +## Handoff to subsequent phases + +- **Phase B (unified config)** — `codeiq.yml` smoke test against both seed repos; validation script gates CI. +- **Phase D (MCP robustness)** — pipeline serve-smoke fix above is a prerequisite for any MCP contract test. +- **Phase E (determinism)** — `index → enrich` reproducibility on the two seed repos above is the seed for graph-snapshot diffing; 31 skipped tests to triage. +- **Phase F (ops & perf)** — Playwright harness + cold-start budgets. diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/baseline/.gitkeep b/scripts/baseline/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/baseline/consolidate.sh b/scripts/baseline/consolidate.sh new file mode 100755 index 00000000..f2d29997 --- /dev/null +++ b/scripts/baseline/consolidate.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Build BASELINE.md from captured raw/ artifacts. Idempotent; safe to re-run. +set -euo pipefail +RAW="docs/superpowers/baselines/2026-04-17/raw" +OUT="docs/superpowers/baselines/2026-04-17/BASELINE.md" + +read_json() { [[ -f "$1" ]] && cat "$1" || echo "null"; } + +TESTS=$(read_json "$RAW/test-counts.json") +COV=$(read_json "$RAW/coverage-summary.json") +FLAKY=$(read_json "$RAW/flaky.json") +SB=$(read_json "$RAW/spotbugs-summary.json") +DC=$(read_json "$RAW/depcheck-summary.json") +FRONT_PW=$(read_json "$RAW/frontend/playwright-summary.json") +PL_PC=$(read_json "$RAW/pipeline/spring-petclinic/summary.json") +PL_RW=$(read_json "$RAW/pipeline/realworld-express/summary.json") + +JAVA_V=$(java -version 2>&1 | head -n1) +MVN_V=$(mvn -v 2>&1 | head -n1) +NODE_V=$(node --version) +NPM_V=$(npm --version) + +cat > "$OUT" <&1 | tail -n 200 >"$OUT/flaky/run-$i.log" + mkdir -p "$OUT/flaky/run-$i-sfxml" + cp -r target/surefire-reports "$OUT/flaky/run-$i-sfxml/" || true +done + +python3 - <<'PY' > "$OUT/flaky.json" +import glob, json, xml.etree.ElementTree as ET, os +N=int(os.environ.get("N","3")) +per_run_fail=[] +for i in range(1, N+1): + fails=set() + for f in glob.glob(f"docs/superpowers/baselines/2026-04-17/raw/flaky/run-{i}-sfxml/surefire-reports/TEST-*.xml"): + r=ET.parse(f).getroot() + for tc in r.iter("testcase"): + if list(tc.findall("failure")) or list(tc.findall("error")): + fails.add(f"{tc.attrib.get('classname','')}#{tc.attrib.get('name','')}") + per_run_fail.append(sorted(fails)) +intersect=set(per_run_fail[0]) +union=set() +for s in per_run_fail: intersect &= set(s); union |= set(s) +flaky=sorted(union - intersect) +print(json.dumps({ + "runs": N, + "failures_per_run": [len(s) for s in per_run_fail], + "always_failing": sorted(intersect), + "flaky": flaky, +}, indent=2)) +PY +cat "$OUT/flaky.json" diff --git a/scripts/baseline/run-depcheck.sh b/scripts/baseline/run-depcheck.sh new file mode 100755 index 00000000..50b72871 --- /dev/null +++ b/scripts/baseline/run-depcheck.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Run OWASP dependency-check in aggregate mode, emit HTML + JSON reports. +set -euo pipefail +OUT="docs/superpowers/baselines/2026-04-17/raw" +mkdir -p "$OUT" +mvn -B -DnvdApiDelay=6000 dependency-check:aggregate \ + -Dformats=HTML,JSON 2>&1 | tee "$OUT/depcheck.log" +cp target/dependency-check-report.html "$OUT/depcheck.html" 2>/dev/null || true +cp target/dependency-check-report.json "$OUT/depcheck.json" 2>/dev/null || true + +python3 - <<'PY' > "$OUT/depcheck-summary.json" +import json, os, collections +p="docs/superpowers/baselines/2026-04-17/raw/depcheck.json" +if not os.path.exists(p): + print(json.dumps({"error":"no depcheck.json"}, indent=2)); raise SystemExit +d=json.load(open(p)) +sev=collections.Counter() +top=[] +for dep in d.get("dependencies",[]): + for v in dep.get("vulnerabilities",[]) or []: + s=(v.get("severity") or "UNKNOWN").upper() + sev[s]+=1 + top.append({"fileName":dep.get("fileName"),"cve":v.get("name"),"severity":s,"cvss":v.get("cvssv3",{}).get("baseScore")}) +top.sort(key=lambda x: -(x.get("cvss") or 0)) +print(json.dumps({"by_severity":dict(sev),"top_25":top[:25]}, indent=2)) +PY +cat "$OUT/depcheck-summary.json" diff --git a/scripts/baseline/run-deptree.sh b/scripts/baseline/run-deptree.sh new file mode 100755 index 00000000..4c079c5a --- /dev/null +++ b/scripts/baseline/run-deptree.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail +OUT="docs/superpowers/baselines/2026-04-17/raw" +mkdir -p "$OUT" +mvn -B dependency:tree -DoutputType=text -DoutputFile="$PWD/$OUT/dep-tree.txt" +mvn -B license:aggregate-third-party-report \ + -Dlicense.outputDirectory="$PWD/$OUT" 2>&1 | tee "$OUT/license.log" || true +# Fallback if license plugin not configured: list GAV+Maven Central license hints. +if [[ ! -f "$OUT/aggregate-third-party-report.html" ]]; then + mvn -B dependency:list -DincludeScope=runtime -DoutputFile="$PWD/$OUT/dep-licenses.txt" || true +fi +ls "$OUT" | grep -E '^(dep-tree|dep-licenses|aggregate-third-party)' || true diff --git a/scripts/baseline/run-frontend-audit.sh b/scripts/baseline/run-frontend-audit.sh new file mode 100755 index 00000000..a462c44c --- /dev/null +++ b/scripts/baseline/run-frontend-audit.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +OUT="docs/superpowers/baselines/2026-04-17/raw/frontend" +mkdir -p "$OUT" + +cd src/main/frontend +npm ci 2>&1 | tee "$OLDPWD/$OUT/npm-ci.log" +npm audit --json > "$OLDPWD/$OUT/npm-audit.json" || true +npm run build 2>&1 | tee "$OLDPWD/$OUT/build.log" +# Playwright — allow failure so we capture the baseline pass rate. +npx playwright install --with-deps chromium 2>&1 | tee "$OLDPWD/$OUT/playwright-install.log" || true +npm run test:e2e -- --reporter=list 2>&1 | tee "$OLDPWD/$OUT/playwright.log" || true +cd - + +python3 - <<'PY' > "$OUT/playwright-summary.json" +import re, json +log=open("docs/superpowers/baselines/2026-04-17/raw/frontend/playwright.log").read() +# "X passed, Y failed, Z skipped" at end +m=re.search(r"(\d+)\s+passed", log); passed=int(m.group(1)) if m else 0 +m=re.search(r"(\d+)\s+failed", log); failed=int(m.group(1)) if m else 0 +m=re.search(r"(\d+)\s+skipped", log); skipped=int(m.group(1)) if m else 0 +print(json.dumps({"passed":passed,"failed":failed,"skipped":skipped}, indent=2)) +PY +cat "$OUT/playwright-summary.json" diff --git a/scripts/baseline/run-maven-tests.sh b/scripts/baseline/run-maven-tests.sh new file mode 100755 index 00000000..1e17f6c3 --- /dev/null +++ b/scripts/baseline/run-maven-tests.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Run full Maven verify with JaCoCo coverage; capture timings and outputs. +set -euo pipefail + +OUT="docs/superpowers/baselines/2026-04-17/raw" +mkdir -p "$OUT" + +start=$(date +%s) +# -B batch mode, -fae fail-at-end so all test classes run +mvn -B -fae \ + -DfailIfNoTests=false \ + clean verify jacoco:report \ + 2>&1 | tee "$OUT/maven-test.log" +rc="${PIPESTATUS[0]}" +end=$(date +%s) +echo "[baseline] maven exit=$rc duration_s=$((end-start))" | tee -a "$OUT/maven-test.log" + +# Coverage CSV (stable, parseable) +if [[ -f target/site/jacoco/jacoco.csv ]]; then + cp target/site/jacoco/jacoco.csv "$OUT/jacoco.csv" +fi +# Surefire/Failsafe XML (for flaky scan + test counts) +if [[ -d target/surefire-reports ]]; then + tar -cf "$OUT/surefire-reports.tar" -C target surefire-reports +fi +if [[ -d target/failsafe-reports ]]; then + tar -cf "$OUT/failsafe-reports.tar" -C target failsafe-reports +fi + +exit "$rc" diff --git a/scripts/baseline/run-pipeline.sh b/scripts/baseline/run-pipeline.sh new file mode 100755 index 00000000..a5650f8f --- /dev/null +++ b/scripts/baseline/run-pipeline.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Usage: run-pipeline.sh +# Runs index, enrich, (brief) serve-smoke against a seed repo, captures timings + stats. +set -euo pipefail +NAME="${1:?seed name required (e.g. spring-petclinic)}" +SEED=".seeds/$NAME" +OUT="docs/superpowers/baselines/2026-04-17/raw/pipeline/$NAME" +mkdir -p "$OUT" + +JAR="$(ls target/code-iq-*-cli.jar 2>/dev/null | head -n1 || true)" +if [[ -z "$JAR" ]]; then + echo "[pipeline] CLI jar not found; running: mvn -B -DskipTests package" + mvn -B -DskipTests package + JAR="$(ls target/code-iq-*-cli.jar | head -n1)" +fi + +[[ -d "$SEED" ]] || { echo "Seed $SEED missing. Run scripts/seed-repos.sh first."; exit 1; } + +# Clean any prior state in the seed repo. +rm -rf "$SEED/.code-intelligence" "$SEED/.osscodeiq" + +timer() { + local label="$1"; shift + local t0=$(date +%s) + "$@" > "$OUT/$label.log" 2>&1 + local rc=$? + local t1=$(date +%s) + echo "$label duration=$((t1-t0))s rc=$rc" | tee -a "$OUT/timings.txt" + return $rc +} + +timer index java -jar "$JAR" index "$SEED" +timer enrich java -jar "$JAR" enrich "$SEED" + +# Serve-smoke: start server, hit /actuator/health and /api/stats, stop. +PORT=18080 +java -jar "$JAR" serve "$SEED" --port "$PORT" > "$OUT/serve.log" 2>&1 & +PID=$! +trap "kill $PID 2>/dev/null || true" EXIT +sleep 8 +if curl -sf "http://127.0.0.1:$PORT/actuator/health" > "$OUT/health.json"; then + echo "health=ok" >> "$OUT/timings.txt" +else + echo "health=fail" >> "$OUT/timings.txt" +fi +curl -sf "http://127.0.0.1:$PORT/api/stats" > "$OUT/stats.json" || true +kill $PID 2>/dev/null || true +wait $PID 2>/dev/null || true + +# Summarize. +python3 - < "$OUT/summary.json" +import json, os +def load(p): + try: return json.load(open(p)) + except Exception: return None +t=open("$OUT/timings.txt").read().strip().splitlines() +print(json.dumps({ + "seed": "$NAME", + "timings": t, + "stats": load("$OUT/stats.json"), + "health_ok": load("$OUT/health.json") is not None, +}, indent=2)) +PY +cat "$OUT/summary.json" diff --git a/scripts/baseline/run-spotbugs.sh b/scripts/baseline/run-spotbugs.sh new file mode 100755 index 00000000..6d406269 --- /dev/null +++ b/scripts/baseline/run-spotbugs.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Run SpotBugs and capture XML + summary JSON. +set -euo pipefail +OUT="docs/superpowers/baselines/2026-04-17/raw" +mkdir -p "$OUT" +mvn -B spotbugs:spotbugs 2>&1 | tee "$OUT/spotbugs.log" +# spotbugsXml.xml default location +if [[ -f target/spotbugsXml.xml ]]; then + cp target/spotbugsXml.xml "$OUT/spotbugs.xml" +fi + +python3 - <<'PY' > "$OUT/spotbugs-summary.json" +import xml.etree.ElementTree as ET, json, collections, os +path="docs/superpowers/baselines/2026-04-17/raw/spotbugs.xml" +if not os.path.exists(path): + print(json.dumps({"error":"no spotbugs.xml produced"}, indent=2)); raise SystemExit +root=ET.parse(path).getroot() +by_priority=collections.Counter() +by_category=collections.Counter() +by_pattern =collections.Counter() +for b in root.iter("BugInstance"): + by_priority[b.attrib.get("priority","?")] += 1 + by_category[b.attrib.get("category","?")] += 1 + by_pattern [b.attrib.get("type","?")] += 1 +print(json.dumps({ + "total_bugs": sum(by_priority.values()), + "by_priority": dict(by_priority), + "by_category": dict(by_category), + "top_patterns": by_pattern.most_common(20), +}, indent=2)) +PY +cat "$OUT/spotbugs-summary.json" diff --git a/scripts/seed-repos.lock b/scripts/seed-repos.lock new file mode 100644 index 00000000..82713d61 --- /dev/null +++ b/scripts/seed-repos.lock @@ -0,0 +1,5 @@ +# +# Commits chosen to match existing ground-truth files in src/test/resources/e2e/. +# Update policy: change commit here and regenerate ground truth in same PR. +spring-petclinic https://github.com/spring-projects/spring-petclinic.git edf4db28affcc4741c79850a3d95bc3f177b5ff9 +realworld-express https://github.com/gothinkster/node-express-realworld-example-app.git 30b68e1e881462b2f4164ea09ab4c4f5699c7b0b diff --git a/scripts/seed-repos.sh b/scripts/seed-repos.sh new file mode 100755 index 00000000..68bd909a --- /dev/null +++ b/scripts/seed-repos.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Clone reference repos at pinned commits into ./.seeds/. +# Idempotent: existing .seeds/ at the correct commit is reused. +set -euo pipefail + +LOCK_FILE="${LOCK_FILE:-scripts/seed-repos.lock}" +SEED_DIR="${SEED_DIR:-.seeds}" + +mkdir -p "$SEED_DIR" + +while IFS= read -r line; do + # skip blank / comment lines + [[ -z "$line" || "$line" =~ ^# ]] && continue + read -r name url commit <<<"$line" + target="$SEED_DIR/$name" + if [[ -d "$target/.git" ]]; then + current=$(git -C "$target" rev-parse HEAD) + if [[ "$current" == "$commit" ]]; then + echo "[seed] $name already at $commit" + continue + fi + echo "[seed] $name at $current, moving to $commit" + git -C "$target" fetch --depth 1 origin "$commit" + git -C "$target" checkout -q "$commit" + else + echo "[seed] cloning $name @ $commit" + git clone --filter=blob:none --no-checkout "$url" "$target" + git -C "$target" fetch --depth 1 origin "$commit" + git -C "$target" checkout -q "$commit" + fi +done < "$LOCK_FILE" + +echo "[seed] done"