Gitapp
Gitapp

Getting Started

End-to-end: install the stack, build a DocStore against a GitHub repo, and run a transaction. The same shape works for Node CLIs, browser apps, and React Native apps — only the adapter changes.

1. Install

pnpm add @dev-bench/docstore @dev-bench/git-fs @dev-bench/gitapp

For the UI layer (optional):

pnpm add @dev-bench/gitapp-ui
# Peer deps if you don't already have them:
pnpm add react react-native react-native-svg

2. Build a DocStore

createGitAppDocStore takes three things: a GitFs (storage), a RepoManagement (REST provider), and a RepoRef (which repo / branch).

import { createBrowserGitFs } from "@dev-bench/git-fs/adapters/browser";
import { githubManagement } from "@dev-bench/git-fs/mgmt/github";
import { createGitAppDocStore } from "@dev-bench/gitapp";

const docStore = createGitAppDocStore({
  gitFs: createBrowserGitFs({ dbName: "myapp" }),
  mgmt: githubManagement({
    proxyBase: "https://cors.example.com/github",
  }),
  repoRef: {
    provider: "github",
    owner: "octocat",
    name: "notes",
    branch: "main",
    proxyBase: "https://cors.example.com/github",
  },
  // Token lookup is a function so the docstore picks up fresh tokens
  // when the user re-auths without needing reconstruction.
  getToken: () => ({ kind: "present", token: process.env.GITHUB_TOKEN! }),
  author: {
    name: "Octocat",
    email: "octocat@github.com",
    timestampISO: () => new Date().toISOString(),
  },
});

Why a CORS proxy? Browsers can't talk to github.com directly for git smart-http or REST. In Node, swap createBrowserGitFs for createNodeGitFs and drop proxyBase.

3. Read

import { match } from "ts-pattern";

const r = await docStore.read("todos/1.md");
match(r)
  .with({ kind: "found" }, ({ content, versionToken }) => {
    console.log(content);
    return versionToken; // pass back as ifMatch for safe updates
  })
  .with({ kind: "missing" }, () => console.log("no such doc"))
  .exhaustive();

Reads never throw on user-recoverable conditions; they return a discriminated union. Same for list, runTransaction, syncIn.

4. Write a transaction

const result = await docStore.runTransaction({
  mutations: [
    {
      kind: "put",
      path: "todos/1.md",
      content: "# buy milk\n\nstatus: open\n",
      ifMatch: { kind: "absent" }, // create-only; fails if path exists
    },
  ],
  message: "todo: add buy milk",
  onConflict: { kind: "rebase-mutations", maxAttempts: 3 },
});

match(result)
  .with({ kind: "ok" }, ({ commitOid }) => console.log("ok", commitOid))
  .with({ kind: "conflict" }, ({ paths }) => console.warn("conflict", paths))
  .with({ kind: "rejected" }, ({ reason }) => console.warn(reason))
  .with({ kind: "network" }, ({ message }) => console.warn(message))
  .exhaustive();

Available mutation kinds (v1):

  • put — create or update a doc.
  • delete — remove a doc.
  • json-patch — RFC 6902 patch on a JSON doc.
  • upsert-frontmatter — merge YAML frontmatter on a markdown doc.

5. Watch

const unsub = docStore.watch("todos/", (event) => {
  // Fires on remote-driven changes only — local writes don't echo.
  console.log("remote changed:", event);
});

await docStore.syncIn(); // force a pull-then-emit cycle
unsub();

6. UI (optional)

@dev-bench/gitapp-ui provides view-model-driven components — every async control surfaces idle / loading / ready / error explicitly.

import { ThemeProvider, defaultLightTheme, ConnectionPanel } from "@dev-bench/gitapp-ui";

export function App() {
  return (
    <ThemeProvider theme={defaultLightTheme}>
      <ConnectionPanel model={connectionModel} />
    </ThemeProvider>
  );
}

The component takes a connectionModel (a thin view-model the host app implements). See API for the full set.

Tests

For unit tests, swap the adapter:

import { createInMemoryGitFs, createInMemoryRemote } from "@dev-bench/git-fs/adapters/in-memory";

const remote = createInMemoryRemote("test-remote");
const gitFs = createInMemoryGitFs({ remote });
// ... build docStore as above; no network involved.

On this page