Skip to content

0012 — studio remote-git projects (R-GIT-⅖)

Status: IN PROGRESS (reviewed 2026-06-22; §10 resolved. The WorkFS-independent half is built & verified live — commit-on-save migrated onto vcs/repo, the local-clone remote backend, push-on-commit, token auth, and the global-identity gate fix. Remaining: the in-memory backend (R-GIT-5), gated on the GTB vcs/repo WorkFS() adapter (feature request filed); plus SSH auth + branch selection, both deferred.) Date: 2026-06-22 Depends on: 0011 §2.2 (the local switcher + commit-on-save it extends); 0002 §1.5 (R-GIT-2/3/4/5); GTB pkg/vcs/repo (the git layer — see §2); the project registry + the mutable active-project model; pkg/notify (push-failure alerts).

1. Goal & scope

Let the studio author against a project that lives as a git remote. The headline capability — the one this feature exists for — is in-memory operation: clone, edit, commit, and push entirely in RAM, with no local checkout, for users who don't want to clone locally or lack filesystem permissions. A local clone is offered as an equally-valid alternative. Either way the reels are authored against the remote and pushed back. Credentials come from config/env (forge-aware, via GTB), never committed; a push failure surfaces in the UI and, in CI, alerts.

In scope: remote project entries with a storage-backend choice (in-memory | local clone), clone, push-on-commit, the per-project filesystem, auth, and push status/alerts. Out of scope: changing the posting flow; auto-rebase on a diverged remote (surfaced, not resolved).

2. The git layer is GTB pkg/vcs/repo (not a bespoke wrapper)

GTB already provides exactly this, so keryx consumes it rather than hand-rolling:

  • Two storage backendsrepo.LocalRepo (on disk) and repo.InMemoryRepo (go-git memfs, no disk). NewRepo(p, …) then Open(type, …) / Clone(uri, target, …) returns a go-git *Repository + *Worktree.
  • Clone / Checkout / Commit / Push / tree inspection, behind the narrow RepoLike role interfaces (Committer, Brancher, Authenticator, …).
  • Forge-aware authNewRepo reads credentials from the tool's forge config subtree (<forge>.auth + the <FORGE>_TOKEN env fallback for tokens, <forge>.ssh for SSH). Token auth needs no keychain — a GITLAB_TOKEN/GITHUB_TOKEN env var works — so this is testable on this dev box, unlike the OAuth keychain work.
  • Clone options: shallow, single-branch, no-tags, submodules.

Consequence: keryx's own internal/gitrepo (the commit-on-save wrapper) should migrate onto vcs/repo so there is one git abstraction. The keryx-specific identity gate is preserved at the keryx layer: vcs/repo.Commit takes a *git.CommitOptions, so keryx resolves the effective identity and passes Author, refusing (the gate) when user.name/user.email aren't set — GTB does not enforce that itself. (Decision §10.1.)

3. The per-project filesystem — the one real new abstraction

The studio's reel/workspace ops are afero-based. Today every project shares one afero.OsFs. A remote project's reels live in its worktree, which is:

  • local clone → an on-disk dir → afero.OsFs (today's path, unchanged), or
  • in-memory → the worktree's billy memfsnot the OS FS.

So the active project gains its own afero.Fs (already foreshadowed by the switcher's active-project model), and the handlers read/write through a.proj().fs instead of a shared a.fs. The workspace/reel ops are already afero-generic, so they work over either FS unchanged once it's threaded through.

The in-memory bridge is a thin afero.Fs over the worktree's billy.Filesystem. This belongs in GTB, not keryxvcs/repo already hands every consumer a billy worktree, so the bridge is a general capability. A feature request is filed for GTB to add it as a first-class citizen — repo.AferoFS(billy.Filesystem) afero.Fs plus a WorkFS() (afero.Fs, error) accessor on RepoLike (go-tool-base/FEATURE-REQUEST-vcs-repo-afero-worktree.md, with a reference implementation). keryx then just sets the active project's fs = repo.WorkFS() for in-memory projects — no local adapter to vendor or test. Sequencing dependency: the in-memory backend lands once GTB ships WorkFS(); if keryx needs it sooner, a local copy of the proposed adapter is the stopgap, swapped for GTB's once available. (The AddToFS materialise alternative was rejected — duplicate state + write-back sync; the bridge keeps one source of truth.)

4. Adding & switching a remote project

POST /api/v1/projects accepts, besides {path} (local dir):

{ "remote": "[email protected]:phpboyscout/blog.git", "branch": "main",
  "storage": "inmemory" }      // or "local"
  • inmemory — clone into a GTB InMemoryRepo held for the session; the active project's fs is the billy bridge; no disk touched.
  • local — clone into a cache dir (<configdir>/cache/<hash>/); the active project's fs is OsFs and reelRoot points into the clone — i.e. it becomes a normal local project (reusing the switcher + commit-on-save wholesale).

The registry entry records remote + branch + storage (+ the cache path for local). forget drops the entry (and offers to delete a local cache clone); it never touches the remote. Switching to an in-memory project re-clones (cheap for a reel-sized repo); switching to a local one reopens the cache.

5. Commit + push (R-GIT-3 remote half)

Commit-on-save already commits (it migrates onto vcs/repo, §2). For a remote project, after a successful commit, push via vcs/repo.Push with the forge-resolved auth. git.auto_push is config — default on for remote projects, n/a for local-dir projects. The save response gains a push alongside commit:

{ "commit": {"committed": true, "hash": "ab12cd34"},
  "push":   {"pushed": true} }            // or {"pushed": false, "reason": "…"}

The editor surfaces both ("✓ committed + pushed" / "committed — not pushed: "). Two gates, mirrored: the identity gate (commit) and an auth gate (push) — a missing forge credential refuses the push with an actionable reason, not a raw transport error. A non-fast-forward (remote moved) surfaces with a "pull/rebase needed" hint; auto-rebase is out of scope.

6. Push-failure alerts (R-GIT-4)

Reuse the pkg/notify seam (none/webhook/email) built for auth-refresh: an unattended/CI push failure fires the configured notifier; the interactive studio's UI surfacing (§5) is the alert. Config: git.alerts.backend (its own selector so git + auth alerts can differ).

7. Security

  • Credentials only from config/env/keychain via GTB (<forge>.auth, <FORGE>_TOKEN, <forge>.ssh); never in .keryx.yaml, never in studio.yaml (remotes/paths only), never logged.
  • In-memory leaves nothing on disk — the point for permission-constrained or shared environments. A local cache clone lives under the user config dir.
  • Localhost bind still applies.

8. Architecture summary

Concern Where Notes
git ops (clone/commit/push, both backends) GTB vcs/repo replaces internal/gitrepo
identity gate (commit author) keryx — resolve + pass CommitOptions.Author preserved from commit-on-save
auth (token/ssh, forge-aware) GTB vcs/repo (config + <FORGE>_TOKEN) no keychain needed for tokens
per-project afero.Fs (OsFs / billy-bridge) api.proj().fs + a billy↔afero adapter the one new abstraction
remote entry (url/branch/storage/cache) registry.go (projectEntry) Remote already reserved
add/switch remote project handlers_projects.go storage choice in the body/UI
push-on-commit + status + UI studio Pusher + Editor.svelte git.auto_push
push-failure alerts reuse pkg/notify git.alerts.backend

9. Testing

  • git mechanics (clone/commit/push, both backends) against a local bare repo as a fake remote — no network, no auth — and an in-memory clone of it.
  • Token auth + the auth gate: a <FORGE>_TOKEN env var against the local remote (or a fake) — runnable on this dev box (no keychain dependency). SSH auth is env-gated.
  • billy↔afero bridge: tested directly (write/read/stat/readdir parity).
  • Studio wiring: a fake Pusher asserts push-after-commit + the save-response shape; a godog scenario clones a local bare "remote" in-memory, edits, saves, and asserts the commit landed in the bare repo after push.

10. Questions for review

  1. Migrate commit-on-save onto GTB vcs/repo (one git layer; keep the keryx identity gate by passing Author), retiring internal/gitrepo? (Recommend yes — one abstraction, and vcs/repo is what gives us in-memory + push + auth.)
  2. Resolved — the adapter, and it lands in GTB as a first-class vcs/repo citizen (WorkFS()), not vendored in keryx. Feature request filed (go-tool-base/FEATURE-REQUEST-vcs-repo-afero-worktree.md). keryx consumes repo.WorkFS(); the in-memory backend sequences on that GTB change (stopgap: a local copy of the proposed adapter). See §3.
  3. Offer both backends, user-chosen per project; default to in-memory for a remote add (the headline use-case), local clone opt-in? Or default local on desktop? (Lean in-memory-default given the stated permission-less intent — confirm.)
  4. Auth v1 = token (forge <FORGE>_TOKEN/config), SSH second — both are GTB- provided; which to wire/test first? (Recommend token first — keychain-free, testable now.)
  5. auto_push default-on for remote projects (the point of a remote project is that saves propagate)? (Recommend yes.)
  6. Sequencing. Since GTB provides in-memory, this no longer needs to wait on the mobile UI — in-memory is independently valuable (permission-less desktops/CI). Build it now as fast-follow 3, with the mobile UI separate? (Recommend yes.) ```