Liking cljdoc? Tell your friends :D

Fulcro RAD Git

A database adapter plugin for Fulcro RAD that uses Git as the persistence layer. Entities are EDN files, transactions are git commits, and history is built in.

Features

  • Entities stored as EDN files in typed subdirectories

  • Each save/delete is an isolated git commit (git commit --only)

  • Automatic resolver generation from RAD attributes — Pathom 2 by default, Pathom 3 optional

  • Save and delete middleware for RAD forms

  • Entity history via git log

  • Content search via git grep

  • Bare repository support with worktree operations

  • Conversation-style storage (each conversation = branch/worktree)

  • Worktree forking from arbitrary commits

  • Hybrid engine — JGit in-process for most read/write operations (fast, no subprocess), with the git CLI as a fallback and for worktree operations JGit 7.x does not support

Requirements

  • Git 2.17+ installed and on PATH (for worktree support)

  • Clojure 1.12+

  • A Pathom parser — Pathom 2 (default) or Pathom 3. Pathom is not bundled by this library; add com.wsscode/pathom (2) or com.wsscode/pathom3 to your own project.

Installation

Add to your deps.edn:

{:deps {us.whitford/fulcro-rad-git {:git/url "https://github.com/michaelwhitford/fulcro-rad-git"
                                     :git/sha "LATEST_SHA"}}}

Or use a local checkout:

{:deps {us.whitford/fulcro-rad-git {:local/root "/path/to/fulcro-rad-git"}}}

Quick Start

1. Define RAD Attributes

(ns com.example.model.memory
  (:require
    [com.fulcrologic.rad.attributes :as attr :refer [defattr]]
    [us.whitford.fulcro.rad.database-adapters.git-options :as gto]))

(defattr id :memory/id :uuid
  {::attr/schema     :knowledge
   ::attr/identity?  true})

(defattr title :memory/title :string
  {::attr/schema      :knowledge
   ::attr/identities  #{:memory/id}
   ::attr/required?   true})

(defattr content :memory/content :string
  {::attr/schema      :knowledge
   ::attr/identities  #{:memory/id}})

(def attributes [id title content])

2. Start Git Repositories

(ns com.example.components.database
  (:require
    [us.whitford.fulcro.rad.database-adapters.git :as git]
    [us.whitford.fulcro.rad.database-adapters.git-options :as gto]
    [com.example.model.memory :as memory]))

(defonce repositories (atom {}))

(defn start! []
  (reset! repositories
    (git/start-repositories
      {::gto/repositories
       {:knowledge {:dir       "data/knowledge"
                    :auto-init? true}}}
      {:attributes memory/attributes})))

(defn stop! []
  (git/stop-repositories @repositories)
  (reset! repositories {}))

3. Configure Pathom Parser

The adapter is pathom-version-neutral. generate-resolvers produces Pathom 2 resolvers by default (the Fulcro ecosystem default). Pathom 3 is opt-in and loaded lazily — it is only touched when pathom3 is on the classpath.

Pathom 2 (default)

generate-resolvers returns plain Pathom 2 resolver maps. Register them with your Pathom 2 parser (for example, Fulcro RAD’s own parser) and add wrap-env to your env-middleware chain.

(ns com.example.components.parser
  (:require
    [us.whitford.fulcro.rad.database-adapters.git :as git]
    [com.example.model.memory :as memory]
    [com.example.components.database :as db]))

(def all-attributes
  (vec (concat memory/attributes)))

;; Plain Pathom 2 resolver maps
(def auto-resolvers
  (git/generate-resolvers all-attributes :knowledge))

;; Env wrapper that injects repos (works with Pathom 2 and 3)
(def wrap-env
  (git/wrap-env (fn [_env] @db/repositories)))

Pathom 3 (optional)

Use generate-resolvers-pathom3 to emit Pathom 3 Resolver objects, then register them with pci/register. Requires com.wsscode/pathom3 on the classpath.

(ns com.example.components.parser
  (:require
    [us.whitford.fulcro.rad.database-adapters.git :as git]
    [com.wsscode.pathom3.connect.indexes :as pci]
    [com.example.model.memory :as memory]
    [com.example.components.database :as db]))

(def all-attributes
  (vec (concat memory/attributes)))

(def auto-resolvers
  (git/generate-resolvers-pathom3 all-attributes :knowledge))

(def indexes
  (pci/register auto-resolvers))

;; Env wrapper that injects repos
(def wrap-env
  (git/wrap-env (fn [_env] @db/repositories)))

4. Configure Form Middleware

(ns com.example.components.middleware
  (:require
    [us.whitford.fulcro.rad.database-adapters.git :as git]))

;; Terminal middleware (no handler to wrap)
(def save-middleware  (git/wrap-git-save))
(def delete-middleware (git/wrap-git-delete))

;; Or wrapping another handler
(def save-middleware
  (git/wrap-git-save base-handler))

Bare Repositories & Worktrees

For conversation-style storage where each conversation is an independent branch:

Start a Bare Repository

(git/start-repositories
  {::gto/repositories
   {:conversations {:dir         "data/conversations.git"
                    :bare?       true
                    :worktrees?  true
                    :worktree-dir "worktrees"}}})

Worktree Operations

;; Create a new conversation (worktree + branch)
(git/add-worktree! repo-dir "/path/to/worktrees/conv-123" "conv-123")
;; => {:success true, :path "/abs/path/to/worktrees/conv-123", :branch "conv-123"}

;; Fork a conversation from a specific point
(git/fork-worktree! repo-dir "/path/to/worktrees/conv-456" "conv-456" "abc1234")
;; => {:success true, :path "...", :branch "conv-456"}

;; Write and commit within a worktree
(spit "/path/to/worktrees/conv-123/message.edn" (pr-str {:role :user :text "Hello"}))
(git/worktree-commit! "/path/to/worktrees/conv-123" "/path/to/worktrees/conv-123/message.edn" "user message")
;; => {:success true, :commit "[conv-123 abc1234] user message"}

;; List all worktrees
(git/list-worktrees repo-dir)
;; => [{:path "..." :head "abc1234" :branch "refs/heads/conv-123"} ...]

;; Remove a worktree (branch history preserved)
(git/remove-worktree! repo-dir "/path/to/worktrees/conv-123")
;; => {:success true}

;; Clean up stale references
(git/prune-worktrees! repo-dir)

;; Garbage collect to reclaim space
(git/gc! repo-dir)
(git/gc! repo-dir {:aggressive? true})

API Reference

Repository Lifecycle

start-repositories

Initialize all git repos from config map

stop-repositories

Shut down all repos (no-op for git)

start-repository!

Start a single repository (normal or bare)

stop-repository!

Stop a single repository

Pathom Integration

wrap-env

Pathom environment middleware to inject repository references (works with Pathom 2 and 3)

generate-resolvers

Auto-generate Pathom 2 resolvers from RAD attributes for a schema (default)

generate-resolvers-pathom3

Auto-generate Pathom 3 resolvers (requires pathom3 on the classpath; loaded lazily)

Form Middleware

wrap-git-save

Save middleware: delta → EDN write → git commit

wrap-git-delete

Delete middleware: file remove → git commit

Direct Entity Operations

entity-path

Build file path for an entity: dir/entity-dir/id.edn

read-entity

Read an EDN entity file, returns map or nil

write-entity!

Write entity map to EDN file

list-entities

List all entities of a type (reads directory)

Git Query Operations

entity-history

Get git log for an entity file

search-content

Search entity files via git grep

git!

Execute arbitrary git commands in a repo directory

Bare Repository

init-bare!

Initialize a bare git repository

bare-repo?

Check if directory is a bare repo

bare-repo-status

Return status with HEAD and worktree count

Worktree Operations

add-worktree!

Add a new worktree with a new branch

fork-worktree!

Add a worktree branching from a specific commit

remove-worktree!

Remove a worktree (keeps branch history)

prune-worktrees!

Clean up stale worktree references

list-worktrees

List all worktrees with path/head/branch info

worktree-commit!

Stage and commit a file within a worktree

Maintenance

gc!

Run git garbage collection (optionally aggressive)

Configuration Options

The git-options namespace provides configuration keys:

(require '[us.whitford.fulcro.rad.database-adapters.git-options :as gto])

;; Environment keys (in Pathom env)
::gto/repositories     ; Map of schema -> repo map

;; Attribute-level options
::gto/entity-dir       ; Override entity directory name
::gto/file-key         ; Use this attribute's value as filename
::gto/generate-resolvers? ; Disable auto resolver generation
::gto/commit-message-fn   ; Custom commit message function
::gto/read-only?          ; Attribute is derived from git metadata

;; Bare repo / worktree options
::gto/bare?            ; Initialize as bare repo
::gto/worktrees?       ; Enable worktree support
::gto/worktree-dir     ; Subdirectory for worktrees (default: "worktrees")

Architecture

┌─────────────────────────────────────────────┐
│               Fulcro RAD Forms              │
│         (save / delete / resolve)           │
├─────────────────────────────────────────────┤
│             wrap-git-save                   │
│             wrap-git-delete                 │
│         delta → EDN → git commit            │
├─────────────────────────────────────────────┤
│           generate-resolvers                │
│     ID resolver (read EDN by ID)            │
│     All-IDs resolver (list directory)       │
├─────────────────────────────────────────────┤
│              utilities.clj                  │
│   git!  read-entity  write-entity!          │
│   entity-history  search-content            │
│   init-bare!  add-worktree!  gc!            │
│        dual-path dispatch: JGit ▸ CLI       │
├──────────────────────┬──────────────────────┤
│  JGit (in-process)   │   git CLI (shell)    │
│  primary read+write  │   fallback + ops     │
│  init/commit/log     │   JGit 7.x lacks     │
│  grep/worktree add   │   (e.g. worktree     │
│  fast, no subprocess │    list/prune)       │
└──────────────────────┴──────────────────────┘

Storage Model

  • Normal repos: Working directory with entity subdirectories

    • data/knowledge/memory/uuid-1.edn

    • data/knowledge/memory/uuid-2.edn

  • Bare repos + worktrees: Each worktree is an independent working directory on its own branch

    • data/conversations.git/ (bare repo, no working dir)

    • data/conversations.git/worktrees/conv-123/ (worktree on branch conv-123)

    • data/conversations.git/worktrees/conv-456/ (worktree on branch conv-456)

Development

Running Tests

clojure -M:run-tests                                                   # all tests
clojure -M:run-tests --focus us.whitford.fulcro.rad.database-adapters.git-test   # one ns

The :run-tests alias pins both Pathom 2 and Pathom 3 so the default (Pathom 2) and optional (Pathom 3) resolver code paths are both exercised.

Building the JAR

Builds are defined in build.clj (tools.build):

clojure -T:build jar        # write pom + thin jar into target/
clojure -T:build clean      # remove target/

Installing Locally

clojure -T:build install    # install to your local ~/.m2 repository

Deploying to Clojars

Deploy runs in a separate, isolated classpath (deps-deploy pulls legacy Maven libraries that conflict with tools.build’s Maven resolver, so they cannot share a process). Build the jar first, then deploy. A Clojars deploy token is required — not your account password:

clojure -T:build jar
CLOJARS_USERNAME=<user> CLOJARS_PASSWORD=<deploy-token> clojure -X:deploy

License

Copyright (c) 2026 Michael Whitford

Distributed under the MIT License.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close