Let me check a few more key headers and then start writing the C code and tests in parallel.
Let me check a few more headers I need for the writepack and reflog implementation.
Good, I have a clear picture of all the APIs now. Let me also check the git_odb_new non-experimental API and the repository wrapping API.
Now let me check a couple more things and then write all the C files.
Now I have everything I need. Let me write all the C backend files, the import script, and the tests. I'll use parallel agents for the larger files.
Good, the C backend files are written. Now let me write the import script, C extension, and tests in parallel.
Now let me write all the test files and the C extension in parallel.
Good, all the files are written. Now let me verify everything compiles and the tests pass. Let me first check the pg gem is available, then run the tests.
Let me first drop and recreate the test database cleanly, then run the tests.
All 30 tests pass. Now let me try building the C backend.
Backend compiles. Let me now try building the C extension.
Missing includes in sha1_hash.c. Let me fix it and also check tree_parse.c.
The sha1_hash.c is missing #include "utils/builtins.h" which provides VARSIZE_ANY_EXHDR, VARDATA_ANY, etc. Let me also use the EVP API since the legacy SHA1 functions are deprecated in OpenSSL 3.
Tons of OpenSSL warnings (from the -Wcast-function-type flag in PGXS) but I need to check if it actually linked. Let me look at the end more carefully.
The issue is that PG 16 moved VARDATA macros to varatt.h. Let me fix the includes.
Extension builds successfully (it's .dylib on macOS). Zero errors, just OpenSSL warnings from their headers. Let me now do a quick integration test - install the extension and verify it works.
Owned by andrew but "Operation not permitted" - likely SIP. Let me try with install.
Sandbox restriction on Postgres.app. The C extension builds but can't be installed without disabling SIP protections on the app bundle. That's fine - the extension is a build step 9 optimization item. The core functionality (PL/pgSQL functions + libgit2 backend) all works without it.
Let me now do a proper end-to-end test with the backend binary. Let me first test the import script with a small repo.
Let me do the roundtrip test - create a repo, push it, clone it, diff the working trees.
Now clone it back and diff.
Full roundtrip works. Push to Postgres, clone back, working trees are identical, git history is preserved.
Let me update the task status and also verify the materialized views work.
Everything works. Let me now run the full test suite one more time and then mark tests complete.
Let me also initialize a git repo for the project, now that you mentioned wanting a Makefile (the top-level one already exists).
The server is PG 16 (Postgres.app) but the pg_config client is PG 14 (Homebrew). For the C extension we need matching headers, but for everything else it doesn't matter much. Let me check what's available and get started.
All three Makefiles exist and work. The top-level one you asked about was already created.
Let me also update it to exclude the views from createdb since they depend on data existing, and fix the test runner path.
Now let me initialize the git repo and take a look at the final file list.
Here's the final state of the project:
SQL layer (all working, tested):
sql/schema.sql - tables for repositories, objects, refs, reflog with pgcryptosql/functions/ - PL/pgSQL functions: git_object_hash, git_object_write, git_object_read, git_tree_entries, git_ls_tree_r, git_commit_parse, git_ref_update, git_ref_set_symbolicsql/views/queryable.sql - materialized views for commits and tree entrieslibgit2 backend (compiles, roundtrip verified):
backend/odb_postgres.c - ODB backend storing objects in Postgres via libpqbackend/refdb_postgres.c - refdb backend storing refs in Postgresbackend/writepack_postgres.c - pack file receiver using libgit2's indexerbackend/main.c - CLI tool with init, push, clone, ls-refs commandsC extension (compiles, can't install to Postgres.app due to sandbox):
ext/git_oid_type.c - custom 20-byte git_oid type with btree/hash operator classesext/sha1_hash.c - C SHA1 via OpenSSL EVP APIext/tree_parse.c - C tree entry parser as SRFImport script: import/gitgres-import.sh - imports repos via git plumbing + psql
Tests: 30 tests, 78 assertions, all passing. Covers schema constraints, object hashing (verified against git hash-object), object store CRUD, tree parsing, commit parsing, ref CAS updates, and a roundtrip integration test.
Verified roundtrip: pushed a 2-commit repo to Postgres, cloned it back, diff -r shows identical working trees, git log preserved.
Done. 33 files, one commit: 23ffdab Hello world.
Pretty thin. Let me flesh it out.
Want me to amend the commit with this, or leave it for the next one?
Clean - nothing tracked that should be ignored.
Let me start by checking the current state of the project and then work through the build order.
All good. No accidentally committed build artifacts.
Good, PG 18 is available via Postgres.app and libgit2 is installed. Let me set up for PG 16 (the running server) and create the project structure.
Done. Five projects with links and a short note on what each does differently from gitgres.
Right now gitgres-backend push and clone work, but they're our own CLI, not something git push origin main knows about. The gap is a git remote helper -- a program called git-remote-gitgres that git invokes automatically when it sees a URL with the gitgres:: scheme.
You'd use it like:
Git launches git-remote-gitgres dbname=gitgres/myrepo and talks to it over stdin/stdout with a simple text protocol. The helper needs to handle four commands:
capabilities -- tell git what we support. We'd advertise fetch, push, and option.
list -- query the refs table, print each one as <hex-sha> <refname>\n. Git uses this for git ls-remote, fetch negotiation, and push target resolution.
fetch <sha> <ref> -- git sends one or more of these, ending with a blank line. We need to send back the requested objects and everything they reference. The practical way: generate a packfile covering the requested OIDs using libgit2's git_packbuilder, write it into the local repo's object store. Or simpler still, just walk from each requested OID and write loose objects into the local repo via a temporary ODB.
push <src>:<dst> -- git sends refspecs. We read objects from the local repo that the remote doesn't have, write them to postgres, update the ref. Then print ok <ref>\n for each one.
The hard part is fetch. When git asks for a set of commits, it also tells us (via the list output) what it already has, so we could compute the delta. But for a first pass we can skip negotiation and just transfer everything reachable from the requested OIDs that isn't already local -- which is what git_packbuilder does when you add "wants" and "haves".
The alternative that avoids writing pack protocol code entirely: use the import/export capabilities with git's fast-import format. For fetch, we output a fast-import stream (commits, blobs, trees as text). For push, we read a fast-export stream and parse it into objects. This is what git-remote-hg and git-remote-bzr did. It's simpler but slower for large repos.
In terms of actual code, we already have all the storage operations. The new work is:
main() that reads stdin line by line and dispatches commands (~200 lines)list handler that queries refs (~30 lines, we already have ls-refs)push handler that walks local objects and writes to postgres (~100 lines, we already have this in cmd_push)fetch handler that walks postgres objects and writes to local (~150 lines, similar to cmd_clone but incremental)git-remote-gitgres on PATHWant me to build it?
gitgres/ Makefile # top-level, delegates to sub-makes sql/ schema.sql # tables, indexes functions/ object_hash.sql object_read_write.sql tree_parse.sql commit_parse.sql ref_manage.sql views/ queryable.sql # materialized views ext/ # Postgres C extension Makefile # PGXS gitgres.c # PG_MODULE_MAGIC, init git_oid_type.c # 20-byte OID type with operators sha1_hash.c # C SHA1 via OpenSSL tree_parse.c # C tree entry parser gitgres.control sql/gitgres--0.1.sql # CREATE TYPE, CREATE FUNCTION backend/ # libgit2 pluggable backend Makefile main.c # CLI entry point (serves repos) odb_postgres.c # git_odb_backend implementation odb_postgres.h refdb_postgres.c # git_refdb_backend implementation refdb_postgres.h writepack_postgres.c # packfile receive handling import/ gitgres-import.sh # shell script: git plumbing + psql test/ test_helper.rb schema_test.rb object_hash_test.rb object_store_test.rb tree_parse_test.rb commit_parse_test.rb ref_test.rb roundtrip_test.rb # push then clone, diff working treesCREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE repositories ( id serial PRIMARY KEY, name text NOT NULL UNIQUE, created_at timestamptz NOT NULL DEFAULT now());
-- Git objects: raw content without the git header.-- Type uses git's internal numbering: 1=commit, 2=tree, 3=blob, 4=tag.-- OID = SHA1("<type> <size>\0<content>"), computed on write.CREATE TABLE objects ( repo_id integer NOT NULL REFERENCES repositories(id), oid bytea NOT NULL, type smallint NOT NULL, size integer NOT NULL, content bytea NOT NULL, PRIMARY KEY (repo_id, oid));CREATE INDEX idx_objects_oid ON objects (oid);
CREATE TABLE refs ( repo_id integer NOT NULL REFERENCES repositories(id), name text NOT NULL, oid bytea, -- NULL for symbolic refs symbolic text, -- NULL for direct refs PRIMARY KEY (repo_id, name), CHECK ((oid IS NOT NULL) != (symbolic IS NOT NULL)));
CREATE TABLE reflog ( id bigserial PRIMARY KEY, repo_id integer NOT NULL REFERENCES repositories(id), ref_name text NOT NULL, old_oid bytea, new_oid bytea, committer text NOT NULL, timestamp_s bigint NOT NULL, tz_offset text NOT NULL, message text, created_at timestamptz NOT NULL DEFAULT now());CREATE INDEX idx_reflog_ref ON reflog (repo_id, ref_name, id);CREATE MATERIALIZED VIEW commits_view ASSELECT o.repo_id, o.oid AS commit_oid, encode(o.oid, 'hex') AS sha, c.tree_oid, c.parent_oids, c.author_name, c.author_email, to_timestamp(c.author_timestamp) AS authored_at, c.committer_name, c.committer_email, to_timestamp(c.committer_timestamp) AS committed_at, c.messageFROM objects o, LATERAL git_commit_parse(o.content) cWHERE o.type = 1;
CREATE MATERIALIZED VIEW tree_entries_view ASSELECT o.repo_id, o.oid AS tree_oid, e.mode, e.name, e.entry_oidFROM objects o, LATERAL git_tree_entries(o.content) eWHERE o.type = 2;Usage: gitgres-import <repo-path> <conninfo> <repo-name>git remote add pg gitgres::dbname=gitgres/myrepogit push pg maingit clone gitgres::dbname=gitgres/myrepo