Feature 10: import Git repositories from GitHub / any git source (with UI) #21
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#21
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Import Git repositories from GitHub / any git source — implementation plan
0. What exists today (verified in code, not guessed)
wal.Registry.Create(internal/wal/registry.go:211) doesPutCreateofmanifest.pb(revision:1, head_seq:0, min_seq:0) → 412 mapsto
ErrExists; thengit.InitLocalRepo.cmd/walhub/serve.go:556(
repoRegistry.Create) mapsWalErrAlreadyExists→api.ErrExists.Auto-create-on-push lives in
internal/server/smart.go:145+internal/server/bind_wal.go.access.jsondefaults are synthesized by theidentity bootstrap (
internal/identity/bootstrap.go:SynthesizeDefaultPutCreate, 412 = skip) — import must reuse this, not invent defaults.cmd/walhub/ops.go:320runImportimplements theclassic path (source refs via
sourceRefs+ ref-glob filter; reuse sourcepacks as tier-0 entries by trailer checksum; full repack as tier-2 base).
import --directis a stub (ops.go:330→notImplemented), but fullyspec'd in
docs/go/11_config_cli.md:400: verify closure → side files →history pack → striped uploads (HEAD-skip existing, marker-file
resumability,
--forceafter moved target) → ref snapshot + checkpoint →manifest CAS (
min_seq = seq+1,first_state_at = as_of = now) → bundlelist (supersede same-strategy + dependents); re-run = no-op;
--parallelismworkers over a bounded channel (11 §6.4).
docs/features/03 §7builds forks(
pull-fork) on theimport --direct"already-on-bucket" mode (HEAD-skipshared packs,
Createfork manifest, 409 on name taken,meta/forks.jsonGC rule). Import-from-URL is the same shape with a network fetch first.
docs/go/04_git.md §11upstreamhelpers — repair (
fetch_objects_as_pack, 500-oid batches,git -c fetch.negotiationAlgorithm=noop -c protocol.version=2 fetch --no-tags --no-write-fetch-head --quiet --depth=1 <upstream> <oid>…) andfollow (
fetch_refs, persistent scratch<cache.dir>/follow/<o>/<n>.gitwith alternates,
update-ref --stdinstagingrefs/follow/*,git -c fetch.unpackLimit=1 -c transfer.unpackLimit=1 -c fetch.writeCommitGraph=false -c gc.auto=0 -c protocol.version=2 fetch <upstream> +<ref>:refs/follow/<ref>…,for-each-refreadback). Code:internal/git/bundle.go:108,internal/maintain/follow.go:24,298,internal/maintain/repair.go:70. Credential pattern is fixed: inlineconfig-pair helper
-c credential.helper= -c credential.helper=!f(){ echo username=x-access-token; echo password=$WALGIT_UPSTREAM_TOKEN; };f, token from env var named byupstream.token_env,GIT_TERMINAL_PROMPT=0always.internal/wal/tasks.goTaskTable.Run(repo, kind)— key"repo/kind", second start JOINS (bounded by joiner ctx),Broadcast[T]replay ring 200 / sub cap 16 / lag-tolerant drop,Notice/Progress(label,done,total,unit)/log_tail(60),TaskRecordfrozen shape (
docs/go/07_api.md §12.3), recent ring 30, byID janitor 1h,Drain()phase-1 cancel. Ops seam:cmd/walhub/serve.go:581opsTasks(
Ops/List/Get/Begin/Attach;Beginsubscribes thenTasks().Run(WithoutCancel…)). Wire:GET …/ops,POST …/ops/{op}→SSE attach stream,
GET …/tasks,GET …/tasks/{id}(JSON orAccept: text/event-streamattach, §12.4 replay-then-live, terminalresult/errorexactly once). Frozen kinds list at 07 §12.3(
materialize, remote-index, history-pack, compact, bundle, checkpoint, fsck, repair, follow, rev-index, sync, rematerialize, prewarm).Extension kinds:
maintain.RegisterKind(14 Seam 5). Note: featurepackages also carry their own tables (
internal/pulls/tasks.go) — importmust use the core wal table, not a second table.
exec.CommandContext,GIT_DIR=,feeder goroutine + stdout drain + 8 KiB stderr ring +
Wait; deadlockrule),
Pool.Runcapgit.max_git_procs(4×GOMAXPROCS), per-reposerver.max_concurrent_per_reposemaphore in the HTTP layer, timeoutsingest 600 s / connectivity 300 s / maintenance 1800 s / follow 900 s
per batch. Bulk-vs-control-plane separation is load-bearing (13 §4.6,
§7 incident 2).
upstream.token_envis host-only(rejected in repo settings —
internal/config/settings.go:49;internal/api/settings.go:147strips it, returns bool presence;internal/api/gaps3_test.go:225,handlers_test.go:847pin the leaktests). Secrets never live in TOML values (
token_envnames an env var;setup file mode 0600). Webhooks (features/06): secret write-only,
secret_set: bool, shown once at creation. CI tokens (features/05):wct_<id>.<secret>, only sha256 hash stored, revoked retained,scopesfield, handler-side capability check, frozen
Principaluntouched.web/src/index.jsx(
/Owners,/:ownerRepos,/:owner/:nameRepo shell + children,/setup,/keys,/api), chrome inweb/src/App.jsx, SDK submodulesin
web/sdk/src/*.jsesbuild-bundled intoweb/dist/repos.js(dogfoodrule: every call through the SDK; lane rewrite
api/api-browseras inweb/sdk/src/pulls.js:77; envelope/SSE parsing shared insdk/src/sse.js).Dark mode default, Tailwind v4.
1. Objective + scope
Objective: a user pastes a git URL (GitHub shorthand or any git URL),
picks target owner/name, optionally supplies a token for private sources,
and gets a first-class walhub repo — refs, objects, default
access.json(creator admin), default
policy.json, import provenance — with narratedprogress (no silent spinner) and safe re-runs.
IN scope
git clone-able URL (https://,git@…:scp-like SSH,ssh://,git://,file://for tests/fixtures); GitHubowner/reposhorthand + full
https://github.com/owner/repo[.git]accepted andnormalized to one canonical URL.
--refallowlist / default-branch-onlyoption; annotated tags preserved with peel.
mismatch with an existing target = 409, never convert).
OUT of scope (v1, explicitly)
endpoints untouched. UI states this.
refs/pull/*, Gerritrefs/changes/*: skipped bydefault (opt-in:
refs/pull/N/headonly — never/merge; see §9).No Octokit, no new dependency (budget law).
--depth,--filter): refused with a plain-textmessage telling the user to do a full import (mirrors the D17 forcing
precedent, 04 §8.2). Rationale: walhub serves full clones; a filtered
base breaks connectivity guarantees.
publish path already emits.
2. Recommended architecture
New package
internal/import(RouteProvider + task kind + CLI glue).Depends ONLY on seam interfaces + frozen types
(
store.Backend,wal.Registry,git.Layer, server auth chain,identity.Service,config.Config) — never oninternal/serverorfeature-package internals (law 8). Registration:
server.RouteProviderchain /
ChainAPI.Handle, both lanes — per the Wave A amendment note in14 §14.11; do NOT invent a second router). Top-level twins
/api/v1/...+/api-browser/v1/...(same handler, 14 §14.12 two-lanerule) + discovery
endpoints[]entry (07 §12 discovery lists only realroutes — D-API-2).
repo-importviamaintain.RegisterKind(panics on duplicate — pick the name once). Single-flight key
"<target o/r>,repo-import"(13 §3"task:"row). Runs on the corewal.TaskTable, not a private table.walhub import(keep--fromclassic path andthe
--directspec untouched):walhub import --url URL owner/name [--ref …] [--default-branch-only] [--token-env VAR] [--format sha1|sha256].CLI goes through the same publish/CAS path as the server (14 §14.9).
Bucket objects. No WAL kind (closed enum — 14 §14.11.1). One addition to
the frozen overwritable-key list in the same revision (14 §14.11.2):
repos/<o>/<r>/meta/import.jsonfork.json, 03 §7){version, source_url (canonical, token scrubbed), source_kind: "github"|"generic"|"file", requested_refs?, imported_at RFC3339, head_shas {ref: sha}, importer, format}Everything else reuses existing objects:
manifest.pb(commit point),checkpoint.pb ∥ refs.pb,wal/*packs + side files,access.json(identity bootstrap),
policy.json(defaults). Cancel-before-commit leavesonly task-scoped scratch + possibly orphan pack objects (harmless; same
orphan philosophy as 14 §14.10.2 / 05 §6.4 burn).
Git-level approach (never hand-roll object writes).
Task-scoped scratch under
<cache.dir>/import/<o>/<r>.<nanos>/(unique perattempt;
defer os.RemoveAll; never the serving copy — 04 §3.1 pattern):git clone --mirror <url> <scratch>(exact argv pinned in code + doc;--mirrorgives all refs + tags; no--depth/--filteraccepted).git for-each-ref --format=%(objectname) %(refname) [...]enumerate;apply refmap: drop
refs/pull/*,refs/changes/*,refs/review/*unless opted in (heads only); drop nothing else.
pick one in implementation (both reuse frozen code):
(a) hand the mirror dir to the classic
runImportmachinery (publishpacks tier-0 + full bitmap'd repack as tier-2 base), or
(b)
index-packscratch ingest + stock connectivity pipeline(
rev-list --objects --stdin --not --all | cat-file --batch-check,04 §7.1) + WAL publish. (a) is preferred: it is the tested path and
forks already bless its shape.
manifest.pbPutCreateon the target(
min_seq = seq+1,first_state_at = as_of = now, per the--directspec) — the CAS decides ownership (same arbitration as
create:13 §3and forks 03 §7: Create conflict = name taken → 409 with winner URL).
Then
access.jsonbootstrap (creator = importer admin), defaultpolicy.jsonif absent,import.jsonprovenanceCreate.GIT_DIR/GIT_TERMINAL_PROMPT=0, credentialhelper as
-cpairs (04 §11 exact text), token ONLY via child env(never argv),
PATH-only inheritance. All spawns inPool.Runwithctx timeouts (clone phase: new
import.clone_timeout, default 1800 s).Concurrency (of this section)
Hazard: import holds no repo locks across subprocesses/store (13 §2 rule 4
— the target may not even exist yet; there is no handle to lock). The task
goes through
Registry.Create+ the ordinary publish path. Avoidance:single-flight key dedups duplicate POSTs; manifest
Createarbitratescross-instance races; scratch dirs unique per attempt.
3. Credential handling (private repos)
Decision: never-stored (v1). The token arrives in the POST body, lives
in task memory only, is injected as child-env (
WALGIT_IMPORT_TOKEN_<id>or reuse of the
WALGIT_UPSTREAM_TOKENpattern — one name, documented)for the clone spawn, and is never written to the bucket, the TaskRecord
params, logs, or
import.json.GETresponses carrysecret_set: boolonly (webhook precedent, 06 §webhooks table).
Justification (existing hygiene): tokens-in-bucket would create a new
secret-management surface (rotation, revocation, at-rest exposure via
any read path); the codebase's consistent answer is indirection or
ephemerality —
token_envnames env (never the value), repo settingsreject host secrets, webhook secrets are write-only, CI secrets store
only hashes. Import tokens are user PATs with cross-repo power: persisting
them in per-repo sidecars maximizes blast radius. Host-level static needs
are already covered by
upstream.token_env(follow/repair/CLI--token-env).grained token with contents:read on the source repo (document exact
scope; least privilege). Generic HTTPS → password/token with read.
sshtransport is OUT for v1 (no key agent onthe host; document
https + tokenas the path).file://allowed onlywhen
import.allow_file_urls=true(default false outside tests — SSRF/local-read guard).
https://user:pass@…) are refused(400, "strip credentials; use the token field") so tokens never land in
import.json, logs, or the task params.import.allow_private_networks=falsedefault (loopback/RFC1918 deniedwith a plain-text 400) +
import.url_allowlist(empty = allow public).Document; no new dep (stdlib
netparse).4. API endpoints + task SSE attach + progress packets
Auth per P6 (roles
read < triage < write < maintain < admin; resolutionaccess.json→ org ownership → principal flags → anonymous). Import needscreate rights on the target namespace: host
write/adminflag, ororg owner, or org member with
write+ on the target owner scope (identity01 owns the exact check); anonymous → 401 (real 401 with
WWW-Authenticate: Bearer realm="walgit"so git/clients erase creds —law 9). Reads of task status:
readon the target namespace (task mayexist before the repo does — gate on namespace, not repo).
POST /api/v1/repos/imports(+/api-browser/v1twin){source_url, owner, name, token?, refs?[], default_branch_only?, include_pull_heads?, format?}→202 {task: TaskRecord, target: "o/r"}+ SSE attach streamGET /api/v1/repos/imports/{id}(+ twin)Accept: text/event-stream→ attach (task packet → replay → live → terminalresult/error)GET /api/v1/repos/imports(+ twin)?owner=→{imports:[{id, target, source_url (scrubbed), state, updated_at}], more}(fromrecentring + running; paged, no LIST — task records are instance-memory, same caveat as 07 §12.3)Wire rules (07 conventions + 14 §14.12): plain-text errors,
[]nevernull, RFC3339 UTC, full SHAs,
no-storeon task starts, ETag/SWR notapplicable (task records mutate by design — same reasoning as checks 05).
Status mapping:
400bad URL/options,401bad credential (upstream authfailure surfaces as task
error, NOT HTTP 401 — the HTTP 401 is only forwalhub auth),
404unknown task id on this instance,409name taken/ source-mismatch / same-target import running on another host (07 §12.2
cross-host rule: record with
hostname+ terminalerror 409),422empty source / no refs after filter / format mismatch,
503+Retry-After: 15on drain interrupt.Progress packets (law 7 — no silent spinners). Clone phase parses
git clone --progressstderr (Receiving objects: %,Resolving deltas)into
Progress(label="clone", done, total?, unit="objects"|"bytes")+Noticelines at phase edges (clone start,clone done: N refs, M objects,ingest,verify,publish,refs snapshot,done). Fallback:indeterminate
Noticeheartbeat ≥ every 15 s when git emits nothing (aquiet 60 s clone is the spinner bug reborn). Bars dedup by label in the
200-packet replay buffer (07 §12.4); terminal
result {"task", "value": {repo, head_shas}}/error {"status", "message"}exactly once.5. UI: pages/flows, SDK, theming, gating
Mount: top-level
/importroute (newImport.jsx), linked fromOwners.jsx(global "Import repository" button) andRepos.jsx(org page,prefills
owner). Rationale: the target repo does not exist yet, so itcannot live under
/:owner/:name/*; this mirrors the top-level forksroute (
POST /api/v1/repos/{o}/{r}/forks, 03 §8).Flow (one page, four states; Solid signals/stores + context only — no new
deps, D-WEB-6):
form: URL field (acceptsowner/repo, full GitHub URL, any git URL;client normalizes + suggests
owner/name, validated againstParseRepoIdrules client-side: charset/length/no-leading-dot), ownerpicker (only namespaces the principal may create in), name field,
"private source" toggle → password-style token field (never echoed,
autocomplete off, excluded from any persisted draft), options
(default-branch-only checkbox; "include GitHub PR heads" checkbox with
help text; LFS-as-pointers note; SSH-unsupported note). Submit → SDK
imports.start.running: progress bars (clone,ingest,publish) from the SSEtask stream + scrolling log tail (last 60) + keepalive-safe attach
(replay-then-live via
sdk/src/sse.js; terminal packet ends the view).done: success card → link to/:owner/:name, head SHAs, "importedfrom at
error: plain-text reason (name taken → link to winner; auth failed →"check token scope"; empty source; timeout/size cap) + Retry button
(re-POST joins-or-restarts per §6 idempotency).
SDK: new submodule
web/sdk/src/import.js(start(payload),get(id),attach(id, onEvent)), group on the default client, registeredin
index.jsre-exports; esbuild-bundled intorepos.js(D-WEB-2);envelope/SSE parsing reuses
sdk/src/sse.js(one parser — 05 precedent).Theming/gating: existing Tailwind classes, dark-by-default
(
dark:variants on every new surface — CI greps this); permission gatingboth disables the form (with "request access" hint) and honors server
409/401/403 (never client-only enforcement).
6. Concurrency: single-flight, pools, idempotency, cancel, caps
("o/r", "repo-import")in the core table; secondPOST joins and reuses the outcome (bounded by joiner ctx, 13 §3). Cross-
host: 409 + owning
hostname(07 §12.2 rule).git.Pool.Run(
max_git_procs); HTTP layer takesserver.max_concurrent_per_repofor the target name (counts against flood even pre-create); bulk pack
uploads go through the bulk pool/worker channel, never request
goroutines (13 §4.6; incident-2 rule).
(target, canonical source URL)re-POST aftersuccess →
200 {repo, import}no-op (compareimport.json.source_url+head SHAs; zero pack traffic). Same target + different source →
409(delete first / pick another name). Crash mid-task → safe re-POST:
manifest
Createeither won (no-op path) or not (fresh attempt; scratchunique per attempt, orphan packs inert).
leader —
WithoutCancelas inopsTasks.Begin; explicit cancel viadrain or a
DELETE …/imports/{id}kill-switch) →CommandContextSIGKILLs git, scratch
RemoveAll, task records terminalerror(drain: 503 "interrupted…", 07 §12.4).
import.clone_timeout(default 1800 s),import.max_bytes(default =server.max_push_bytes, enforced while streaming like ingest §3.2),import.max_refs(default 100 000 — prefix-filter cost guard),import.max_concurrent(default 2 server-side clones; bounded channel,sender-owns-close per 13 §5). Exceeding → terminal
errornaming thekey and the fix (same style as
max_wantsguard, 04 §8.2).Concurrency (of this plan)
Hazard: two imports to one target interleaving scratch/publish; clone
holding a pool slot forever; pre-create task with no handle to lock.
Avoidance: single-flight + manifest-CAS arbitration + unique scratch +
pool/timeout ctx + no repo locks held (see §2 Concurrency).
7. Performance: budget + why it can't explode + EVIDENCE sketch
Import is a task, never the push/fetch hot path — it adds zero round
trips to push ≤ 5 / warm-refs-1 / checkpoint-4 (sim budgets, 15_testing;
law 6). Per-import store budget (control plane; bulk pack bytes excluded
by definition):
--parallelism-boundedPutCreatecheckpoint.pb ∥ refs.pbaccess.json+policy.json+import.jsonCreatesCannot explode: exact-key probes only; ref enumeration is local
(
for-each-refon scratch, capped bymax_refs); git concurrency pool-gated; bulk/control transports split (13 §4.6); no lock held across I/O.
EVIDENCE.md entry sketch (E5 — import): harness
internal/import/importbench_test.go(orinternal/devtools/importbenchwith the
evidencetag precedent) driving the real task path overmemory + filesystem (contract-suite pattern) and the S3 rig when
touched: fixture remote = local
file://mirror repo at two populations(S: ~200 commits, M: ~5k commits + tags); measure store ops by class
(counting store), wall time, peak scratch bytes, git subprocess count;
assert ops ≈ 6+Npacks at both sizes (flat), time linear in pack bytes only.
Also record one live
github.comrun (env-gated, manual — same gating asWALHUB_TEST_S3_ENDPOINT) as a comment, not a CI gate.8. Acceptance criteria
POST /api/v1/repos/imports+ browser twin;GETone/list; impl ininternal/importonly (+ registrations); no change tointernal/{store,wal,git}except (if any) a spec-amended touch listedin the commit (09 §4 pattern).
repo-importregistered (Seam 5);(target, kind)single-flight join verified; SSE attach (replay → live → terminal
result/error); progress ≥1 packet per phase + 15 s heartbeat floor(law 7 — fail the test on a silent 60 s window).
GIT_TERMINAL_PROMPT=0,-ccredential pairs, token via child env,Pool.Run, ctx timeouts). No go-git, no new module (budget law).--directmachinery);manifest
PutCreateis the commit point;min_seq = seq+1honored.meta/import.jsonadded to the frozen overwritable list indocs/go/14_extensibility.md+ this plan's doc section in the samechange (law 12);
DEVIATIONS.mduntouched unless a real deviation.secret_setbool only); embedded-URL creds refused; SSRF defaults on.WWW-Authenticate; 409-winner-URL; plain-text errors;[]-not-null;RFC3339; full SHAs;
no-storeon starts; discoveryendpoints[].walhub import --urlworks againstfile://fixture;--helpdocuments all flags; exit codes 0/1/2 per 11 §6.3.
/import(form → running → done/error), SDKimport.js, dark +light, server-enforced gating;
make test-webgreen.internal/import(make cover);-raceclean;stress
-count=100on the single-flight/join test; no skipped tests.one optional live
github.comrun documented (env-gated)./,/importfull flow,repo page,
/setup; console clean; screenshots attached to the change.make simif WALtouched, else budget table in tests); EVIDENCE E5 entry committed.
make fmt && make vetclean; commit message names the doc section +appended decisions.
9. Open questions / risks
default branch before import" is wanted, a client-side (browser→api.
github.com) prefill keeps server egress pure — no token leaves the
browser. Server calling GitHub API is rejected for v1 (new auth,
rate-limit, PII surface).
refs/pull/*: skip by default (importingrefs/pull/N/headrewritesGitHub's ephemeral namespace into permanent local refs;
/mergerefsare computed, often dangling). Opt-in imports heads only, renamed?
No — import verbatim or not at all (renaming breaks
ghtoolingexpectations). Document.
HEADsymref; targetHEADfollows source (else
refs/heads/mainfallback per 04 §1.2). Unborn-HEAD sources → 422.
git init --bare --object-format=sha256, 04 §1.2). Mixed-format sources don't existfrom stock git; a lying remote fails connectivity → terminal error.
max_bytes/clone timeout;document "import from a nearby mirror / seed via
import --direct"for the pathological case (the
--directpath stays the bulk loader).§3 + §6 (deny-private default, allowlist,
allow_file_urls=false,timeouts). Call out in review; consider a confirming
dangerous: trueflag when the allowlist is empty and the URL is non-GitHub.
in-flight request credential). Never log at any level (audit: grep
token|password|secreton the new package in CI).if a later fork feature shares their packs, the
meta/forks.jsonGCrule (03 §7) applies unchanged — import writes no fork metadata, so
nothing to reconcile.
owner/repocould collide with ageneric-host path — resolve strictly as github.com; anything else must
be a full URL.
\.gitsuffix stripped once (ParseRepoId precedent).10. Provenance of this plan
Explored files (read-only):
AGENTS.md,docs/features/README.md,docs/features/09_rollout.md,docs/features/03_pull_requests.md§7–8,docs/features/05_checks_statuses.md(CI-token hygiene),docs/features/06_notifications.md(webhook secrets),docs/go/04_git.md,docs/go/07_api.md§12,docs/go/11_config_cli.md§import,
docs/go/13_concurrency.md,docs/go/14_extensibility.md,DEVIATIONS.md,docs/EVIDENCE.md(method standards),internal/wal/tasks.go,internal/wal/registry.go:211,cmd/walhub/serve.go:556+opsTasks,cmd/walhub/ops.go:320,internal/git/bundle.go:108,internal/maintain/follow.go,internal/identity/bootstrap.go,internal/config/settings.go,internal/api/settings.go,web/src/index.jsx,web/src/App.jsx,web/sdk/src/pulls.js. No worktree file was written; pre-existinggit statusdirt (AGENTS.md, serve.go, EVIDENCE.md, 03, 06, 12, 14,bind_ssh.go, health.go, router.go, core.js, App.jsx, index.jsx,
Owners.jsx, Repo.jsx, Repos.jsx + untracked
.opencode/, pulls.go,managed.go,
internal/pulls/) predates this plan.Review (1/3) — §0 factual claims checked against CODE (confirmations + inaccuracies). Full text: /tmp/opencode/import-plan-review.md §2.
CONFIRMED ACCURATE (with pins): Registry.Create + PutCreate rev:1/head_seq:0/min_seq:0 (internal/wal/registry.go:211,224-244); WalErrAlreadyExists→api.ErrExists (cmd/walhub/serve.go:556-560); auto-create (internal/server/smart.go:144-145, bind_wal.go:300-303); bootstrap SynthesizeDefault+PutCreate+412=skip (internal/identity/bootstrap.go:47-56, def access.go:95); classic import + --direct stub (cmd/walhub/ops.go:320,330); credential helper text (internal/git/bundle.go:107-108, internal/maintain/follow.go:24,297-298, repair.go:70-75); task ring 200 / sub-cap 16 / recent 30 / janitor 1h / Drain (internal/wal/tasks.go:28-34,240-241,286-308); opsTasks Begin+WithoutCancel (cmd/walhub/serve.go:581,611-624); secret-hygiene pins (internal/config/settings.go:49, internal/api/settings.go:147-150, gaps3_test.go:225, handlers_test.go:847,873-895); UI routes (web/src/index.jsx); lane twin (web/sdk/src/pulls.js:77-79); frozen kinds list matches docs/go/07_api.md:612-613; internal/pulls/tasks.go carries its own taskTable (so "use the core wal table" is the right call).
INACCURACIES (severity + ref):
internal/importdoes not compile.importis a Go keyword;package importis a syntax error — verified with the toolchain (gofmt: expected 'IDENT', found 'import'). Rename package AND directory (e.g.internal/repoimport); AGENTS.md naming law needs the amendment anyway. §2 + §8 must be rewritten.maintain.RegisterKinddoesn't exist in code — it's a doc sketch (docs/go/14_extensibility.md:301). The code seam is wal.TaskTable.Run (internal/wal/tasks.go:180) + maintain.TaskRunner (internal/maintain/units.go:82-86) + the opFn switch (cmd/walhub/serve.go:687). Rewrite the Seam-5 registration recipe in code terms.TaskTable.Run(repo, kind)shorthand vs actualRun(ctx, repo, kind, params, fn)(tasks.go:180); key isrepo+"/"+kind(tasks.go:181) but §6 writes"<target>,repo-import"(that's the 13 §3 Group-key sketch, not the TaskTable key). Pick one spelling.sdk/src/sse.js→web/sdk/src/sse.js.Review (2/3) — design stress-test findings. Full text: /tmp/opencode/import-plan-review.md §1+§3.
BLOCKING (plan must change):
SHOULD-FIX: S1 (§6) max_bytes unenforceable during clone (git writes scratch directly, no feeder) → post-clone du gate + publish-time gate. S2 (§3) TaskRecord.Params is persisted (tasks.go:206) → forbid token/raw URL there; scrub progress packets + terminal errors (git stderr echoes URLs); extend the grep-clean gate. S3 (§2) per-task env name needs dynamic -c helper argv (it hardcodes $WALGIT_UPSTREAM_TOKEN). S4 (§2) refmap undecided: refs/notes/, refs/replace/ (replace rewrites object identity — default-drop), refs/meta/*, keep-around; empty source 422 vs create-empty. S5 (§3) SSRF residuals: DNS TOCTOU + redirect following (allowlist must cover final URL; token helper host-pinned); keep the dangerous:true idea. S6 (§4) order: auth → authorize namespace → THEN join-or-start; joins must not leak cross-principal task existence. S7 (§0 vs §2) SynthesizeDefault(owner) binds user:, not the importer — import must write importer-admin explicitly via the identity service. S8 (§2) name short timeouts for for-each-ref/verify/repack, not just clone_timeout. S9 (§6) max_concurrent_per_repo "for the target name" has no handle pre-create — name-keyed gate or drop; keep import.max_concurrent=2. S10 (§7) "≈6+Npacks" needs a pinned single-pack fixture or a range, else unmeasurable. S11 (§1) LFS: add a server Notice + docs line, not just UI text. S12 (§8/09 §4) enumerate the touch list (server reg = item 3 allowed; additive [import] config; same-change doc amendments).
Review (3/3) — scope judgment + verdict. Full text: /tmp/opencode/import-plan-review.md §4.
SCOPE: pure-git v1 (no GitHub API) is the RIGHT call — keeps the dependency budget (AGENTS.md law 1), avoids a new auth/PII/rate-limit surface; browser-side prefill (§9.1) is a clean compromise. Keep. LFS-as-pointers + no-submodule-recursion + PR-ref default-skip + no-shallow (mirrors the D17 precedent, 04 §8.2) are all correct v1 exclusions.
CUT for v1: (a) GET /repos/imports list — pagination over instance-memory, cross-namespace records (see B4); POST + GET-one suffice. (b) DELETE imports/{id} (see B5). KEEP the include_pull_heads flag (cheap, default-off) and format handling. MISSING for shippable: B2/B3/B4 decisions, the S4 refmap table, S2 hygiene checklist, S5 redirect/DNS note, S10 fixture pinning.
VERDICT: PROCEED WITH FIXES. Architecture direction is sound — existing publish path, manifest-CAS commit point, never-stored tokens, no new WAL kind, zero hot-path round trips. But B1–B6 (package name; RegisterKind; params-aware join; manifest-without-import.json state machine; pre-create task home; DELETE cancel) MUST change in the plan before implementation; S1–S12 addressed or explicitly deferred with reasons.
Plan revision R1 (incorporates review comments 318–320 — all B-items blocking, all S-items)
Blocking fixes (normative for implementation)
internal/repoimport(dir + package identical;importis a Go keyword). All plan references tointernal/importnow meaninternal/repoimport; coverage gate applies to it. Naming-law doc amendment included in the change.TaskTable.Run. A join never leaks another principal's task: the 409/join decision requires the same read-on-namespace auth, and joined outcome contains no secrets (S2).import.jsonexists and matches canonical source (→ idempotent no-op path). Never silently adopt a foreign manifest (covers PUT-created and auto-create-on-push targets).wal.TaskTable(exists without a handle) under key<target>/repo-import. Progress/attach is served from the table-level replay ring keyed by task id (GET /api/v1/repos/imports/{id}withAccept: text/event-stream, 07 §12.4 attach semantics) — NOT the per-handle broadcast. After manifest Create, later attaches may use the handle; the id-keyed path always works.maintain.RegisterKind(doc sketch only). Recipe: kind string"repo-import"run through the corewal.TaskTablevia anopsTasks-style wiring incmd/walhub(Begin subscribes table-level replay, thenTasks().Run(WithoutCancel…)), plus theopFn-switch-adjacent dispatch entry. Panic-on-duplicate-kind behavior preserved by using the single kind constant.Should-fix adoptions (all S-items)
import.max_bytesenforced as post-clone scratchdugate + publish-time pack gate (git writes scratch directly; no streaming enforcement possible). Terminal error names the key and fix.TaskRecord.Paramscarries scrubbed canonical URL + options only; progress/log_tail packets and terminal errors scrubbed (git stderr echoes URLs); grep-clean acceptance covers bucket, logs, record params, packets, errors.-cargv built dynamically per spawn (per-task env name); static copy-paste forbidden.refs/pull/*,refs/changes/*,refs/review/*(opt-in heads-only flag kept),refs/notes/*(default drop, flag to keep),refs/replace/*(always drop — rewrites object identity),refs/meta/*,refs/keep-around/*(drop). Empty source (no refs after filter, incl. empty GitHub repos) → 422 "source has no importable refs".dangerous: trueconfirm flag for empty-allowlist non-GitHub URLs.SynthesizeDefaultstays the backstop only.for-each-ref, verify pipeline, repack tail) —import.for_each_ref_timeoutetc. or oneimport.git_timeout(default 300 s);clone_timeout1800 s stays.max_concurrent_per_repo-for-target claim (no handle pre-create, cf. B4); keepimport.max_concurrent= 2 (bounded channel, sender-owns-close).Notice+ docs line + UI note (clones work, checkouts yield pointer text).[import]config (14 §14.12), overwritable-list (meta/import.json) + discovery amendments same-change (law 12). Nointernal/waltouch (B5 cut).Scope cuts for v1 (per review verdict)
GET /api/v1/repos/importslist (instance-memory pagination across namespaces); CUT:DELETE …/imports/{id}(B5). Ship:POSTstart +GETone (+ SSE attach). KEPT:include_pull_headsflag (default off),formathandling, browser-lane twins,endpoints[]discovery.All section references above are to the original plan (issue #21 body). Unchanged plan text stands except where R1 overrides it; on conflict R1 wins.
Feature 10 implementation starting on branch feat/repo-import (worktree /tmp/walhub-repoimport, from origin/main). Plan + R1 normative; building internal/repoimport, [import] config, CLI --url, /import UI + SDK, docs/features/10, EVIDENCE entry.
Feature 10 implementation ready for review: PR #22 (#22) on branch feat/repo-import — docs/features/10 plus the full implementation (internal/repoimport, [import] config, CLI --url, /import UI + SDK, E11). Do NOT merge (review requested).
PR #22 review (Feature 10 import, feat/repo-import @
83d0cc1) — findings + fixes pushed (now3bff123)Review axes all checked against code. Pre-fix verification (worktree /tmp/walhub-repoimport): gofmt clean,
go vetclean,go test -race ./internal/repoimport/...PASS (24s), coverage 95.6% (gate 95%),node --test web/test/unit/sdk-import.test.js6/6 PASS, live e2e from file:// fixture: 202 start → SSE replay→live→resultexactly once → 200 no-op re-POST →git cloneserves commit+tag; 409/400 matrix (foreign source, embedded creds, non-allowlisted host, unknown key) all per spec.internal/api+internal/configsuites green.Spec conformance (confirmed, no change needed)
internal/git,store,wal,identity,server/auth), core never importsrepoimport(grep-clean). B1 package name, B6 kind registration (repoimport.RegisterKind+api.RegisterExposed, law 12 same-change) correct.clone --mirror --progress --, for-each-ref format, rev-parse, conditionalindex-pack <pack>,show HEAD:.gitattributes); token only via per-task child env (CredentialEnv), host-pinned helper, clear-then-set order. GIT_TERMINAL_PROMPT=0 on every spawn; CloneTimeout/GitTimeout/MaintTTL bound every spawn (FullRepack rides MaintTTL — S8 holds)..idxlatent-gap claim is REAL:installPackFile(internal/wal/publish.go:1037) writes only.packwhile reconcile (reconcile.go:104-109,186-232) expectswal/<sha>.idx; fix confined topublishPack, zerointernal/wal,git,storediff — classic path untouched. Verified.loadPolicymissing = allow-all nil doc (internal/api/policy.go:25). Verified.TaskTable.Drain→ table-ctx cancel (drive's detached ctx is correct — cancellation comes fromt.ctx, tasks.go:227); P6 order authenticate→authorize→join-or-start, anonymous 401+Bearer; wire (plain-text, [], RFC3339, no-store, 404/405/503+Retry-After, discovery twins); Import.jsx hasdark:variants + anonymous-disable + server-status honoring (server-side gating intact).Findings fixed + pushed (
3bff123, re-tested below)internal/repoimport/doc.go:42— corrupt/unreadableimport.jsonreturned 500, but R1 B3 / feature doc say "never adopts (409)". Now 409 naming delete-and-retry; store failures stay 500. TestTestProbeCorruptDoc500→TestProbeCorruptDoc409(cover2_test.go).internal/repoimport/service.go:196—Beginhelds.muacross the B3 probe's store I/O (Head+GET), violating 13 §2 rule 4. Probe now runs unlocked; the join window is re-checked under the lock before installingrunning(that re-check already existed).internal/repoimport/service.go:402—Service.Janitor()had zero callers: finishedstreamsrings retained forever (unbounded growth). Rings now prune lazily (capped, lock-safe,s.mu→stream.mudirection preserved) on Begin/Lookup;Janitor()kept as the explicit-sweep entry.internal/repoimport/http.go:172—getlacked the nil-Svcguardposthas (nil deref panic). Now 503 like POST. Regression testTestNilSvc503added.internal/repoimport/http.go:427— SSE keepalivefor range s.ka.Cparks forever afterStop(ticker channels never close): one leaked goroutine per attach, against 13 §1. Nowselectons.ctx.Done()(+idempotent Stop). Same shape noted ininternal/notify/stream.go:251— not touched here (out of scope), flagging for its owner.docs/features/10_git_import.mdclaimed a "table fallback for pruned windows" that cannot fire (service ids ≠ table ids); behavior (404 past 1h retention) already matches the §3 status table — sentence corrected.docs/EVIDENCE.mdE11 header now reads "PUTs total (control)" for the14 (12)cells.Post-fix results (scratch worktree /tmp/pr22 @
3bff123)go test -race -count=1 ./internal/repoimport/...PASS; coverage 95.5%;internal/api,internal/configPASS; B2 join tests-count=20PASS; rebuilt binary live e2e re-passed (202 → ok:true 3 refs → 200 no-op; unknown id 404)./import+/setupin the CDP Chrome before merge),make sim(no publish-path change — correctly skipped), S3 contract (no backend touched).MERGE RECOMMENDATION: ready to merge (after the author's browser pass on
/import)Feature 10 complete: plan reviewed (6 blocking items → R1), implemented in PR #22, code-reviewed (6 findings fixed: import.json 409, unlocked probe I/O, janitor prune, nil-Svc guard, SSE goroutine leak, doc nits), merged. internal/repoimport 95.5%, -race clean, browser pass done by implementer. Closing.