@usecontextlayer/pggit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.d.mts +274 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1362 -0
- package/dist/index.mjs.map +1 -0
- package/dist/schema.d.mts +100 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +1 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the pggit authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# pggit
|
|
4
|
+
|
|
5
|
+
A battle-tested git server that runs on Postgres.
|
|
6
|
+
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# Point it at a Postgres database and start it. No bare repo on disk, no git binary.
|
|
13
|
+
PGGIT_DATABASE_URL=postgres://localhost/pggit pnpm run dev
|
|
14
|
+
# [pggit] listening on http://localhost:8080
|
|
15
|
+
|
|
16
|
+
# Push with stock git. The repo is created on first push, HEAD -> refs/heads/main.
|
|
17
|
+
git push http://localhost:8080/myrepo main
|
|
18
|
+
|
|
19
|
+
# Clone it back. Every object came out of Postgres. (git >= 2.26)
|
|
20
|
+
git clone http://localhost:8080/myrepo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```sql
|
|
24
|
+
-- A branch's working tree is plain SQL — no clone, no checkout, no git.
|
|
25
|
+
select f.path, f.mode, o.content
|
|
26
|
+
from repo_file f
|
|
27
|
+
join repos r on r.id = f.repo_id
|
|
28
|
+
join git_object o on o.repo_id = f.repo_id and o.oid = f.blob_oid
|
|
29
|
+
where r.name = 'myrepo' and f.ref_name = 'refs/heads/main'
|
|
30
|
+
order by f.path;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **It's just Postgres** — every git object and ref is a row in your database, sitting next to the rest of your data. No bare repos on disk, no git host to run.
|
|
34
|
+
- **Compatible with any modern git client, nothing special needed** — `clone`, `fetch`, and `push` work over standard smart-HTTP with the git you already have (>= 2.26, the default since 2020).
|
|
35
|
+
- **Queryable** — an optional per-branch index maps each branch tip's files to their blobs, so a `SELECT` returns a repo's working tree with no clone and no checkout.
|
|
36
|
+
- **Thoroughly tested** — every operation round-trips against the canonical `git` binary (clone or push, then `fsck --full`), plus property-based differential tests over thousands of random commit graphs.
|
|
37
|
+
- **Pure TypeScript** — a git smart-HTTP server built from scratch; no native addons and no `git` binary on the request path.
|
|
38
|
+
- **Embed into your app** — it's a Hono sub-app: `host.route("/git", createGitApp(deps))` runs the git server in-process, inside your own service.
|
|
39
|
+
|
|
40
|
+
## Why?
|
|
41
|
+
|
|
42
|
+
A git server normally needs a filesystem: bare repos on disk, backed up and replicated out of band, reached through a `git` process. pggit drops that. Objects and refs live in Postgres, so the database you already run, back up, and replicate holds your git data too — right next to everything else. Point any `git remote` at it; it doesn't care what you store: generated files, per-user workspaces, agent output, application content.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm install @usecontextlayer/pggit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or build from source to hack on it (Node >= 20, pnpm, and a Postgres you can reach):
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git clone https://github.com/usecontextlayer/pggit
|
|
54
|
+
cd pggit
|
|
55
|
+
pnpm install
|
|
56
|
+
pnpm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Create the schema. Migrations use `DATABASE_URL` (note: the server uses `PGGIT_DATABASE_URL`):
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
DATABASE_URL=postgres://localhost/pggit pnpm run db.manage latest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### As a standalone server
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
PGGIT_DATABASE_URL=postgres://localhost/pggit pnpm run dev
|
|
71
|
+
# [pggit] listening on http://localhost:8080
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then treat it as an ordinary remote:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
git push http://localhost:8080/myrepo main # first push creates the repo
|
|
78
|
+
git clone http://localhost:8080/myrepo # git >= 2.26 (negotiates v2)
|
|
79
|
+
git clone --filter=blob:none http://localhost:8080/myrepo # blobless / partial clone
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The standalone server wires the queryable view on by default, so the `repo_file` SQL above works against any repo you push.
|
|
83
|
+
|
|
84
|
+
### As a Hono sub-app
|
|
85
|
+
|
|
86
|
+
`createGitApp(deps)` returns a Hono app you mount into your own server, keeping one Postgres connection for the whole host:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createGitApp } from "@usecontextlayer/pggit"
|
|
90
|
+
|
|
91
|
+
// `deps` supplies the Postgres-backed object and ref stores, plus an optional
|
|
92
|
+
// snapshot store that maintains the queryable repo_file index. Omit `snapshots`
|
|
93
|
+
// and pggit is a plain git remote.
|
|
94
|
+
host.route("/git", createGitApp({ objects, refs, snapshots }))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The app exposes the smart-HTTP endpoints under the mount point: `GET /:repo/info/refs`, `POST /:repo/git-upload-pack` (fetch), `POST /:repo/git-receive-pack` (push), and `GET /health`.
|
|
98
|
+
|
|
99
|
+
## How It Works
|
|
100
|
+
|
|
101
|
+
git's core data is immutable, content-addressed objects, so pggit stores them that way rather than as packfiles:
|
|
102
|
+
|
|
103
|
+
- **`git_object`** holds one row per object — the raw inflated body (no loose `<type> <size>\0` header, no zlib), LZ4-compressed Postgres-side, keyed `(repo_id, oid)` and hash-partitioned by repo. Append-only on the push path; unreachable rows are later reclaimed by the background GC drain (below).
|
|
104
|
+
- **`git_ref`** is git's only mutable surface. Each row is either a direct ref or a symref; ref updates are compare-and-swap against the advertised old OID.
|
|
105
|
+
- **`git_edge`** materializes the commit/tree/tag DAG so reachability — fetch negotiation and the push connectivity check — is a recursive SQL walk instead of re-parsing objects.
|
|
106
|
+
- **`repo_file`** is the optional projection: on each push it rebuilds a branch tip's `path -> (mode, blob_oid)` index. It stores no duplicate bytes; content is read by joining `git_object`.
|
|
107
|
+
|
|
108
|
+
Pushes ingest via binary `COPY` (no per-row bind-parameter ceiling, so a single push can carry huge blobs and tens of thousands of files). Thin-pack delta bases are resolved against objects already in the store. Fetches serve undeltified packs built straight from the closure. OIDs are SHA-1 throughout; correctness is pinned by a suite that round-trips every operation against the real `git` binary and diffs generated commit graphs with `fast-check`.
|
|
109
|
+
|
|
110
|
+
Because objects are append-only, cleanup is deletion, not rewriting. A background **GC drain** keeps storage bounded: for any repo pushed since its last sweep, it reclaims objects unreachable from every ref and older than a grace window, using the same reachability engine the serve path uses — so a reachable object is never deleted — on a connection pool separate from the request path. The standalone server runs it on by default; a mounted host opts in with the exported `createGcScheduler`.
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
| Variable | Used by | Default | Notes |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| `PGGIT_DATABASE_URL` | the server (`pnpm run dev`, `startServer`) | — | Required to serve; throws on boot if unset. |
|
|
117
|
+
| `PGGIT_PORT` | the server | `8080` | Listen port. |
|
|
118
|
+
| `PGGIT_GC_ENABLED` | the server | `true` | Runs the background GC drain that reclaims unreachable objects. |
|
|
119
|
+
| `PGGIT_GC_GRACE_SECONDS` | the server | `60` | Reclaim only objects unreachable for longer than this. |
|
|
120
|
+
| `PGGIT_GC_INTERVAL_MS` | the server | `30000` | How often the drain polls for repos to sweep. |
|
|
121
|
+
| `PGGIT_GC_CONCURRENCY` | the server | `4` | Max repos swept per drain pass. |
|
|
122
|
+
| `DATABASE_URL` | the migration CLI (`pnpm run db.manage`) | — | Required for `latest`/`up`/`down`/`reset`/`drop`. |
|
|
123
|
+
|
|
124
|
+
## Scope
|
|
125
|
+
|
|
126
|
+
pggit is deliberately narrow:
|
|
127
|
+
|
|
128
|
+
- **No authentication.** It serves every request; put it behind your own auth/network boundary.
|
|
129
|
+
- **SHA-1 only.** A SHA-256 client is rejected at the wire boundary.
|
|
130
|
+
- **Fetch is protocol v2 only**, push is v0. A v0/v1 fetch client fails loudly rather than silently cloning nothing. Shallow clones are rejected; `blob:none` is the only partial-clone filter that's honored — other filters are accepted but ignored, so you get a full clone.
|
|
131
|
+
|
|
132
|
+
MIT License
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { Sql } from "postgres";
|
|
3
|
+
|
|
4
|
+
//#region src/object/object.d.ts
|
|
5
|
+
/** The four addressable git object types (deltas resolve into one of these). */
|
|
6
|
+
type GitObjectType = "blob" | "commit" | "tree" | "tag";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/repo-view/build-file-list.d.ts
|
|
9
|
+
type FileEntry = {
|
|
10
|
+
path: string;
|
|
11
|
+
mode: string;
|
|
12
|
+
blobOid: string;
|
|
13
|
+
};
|
|
14
|
+
type FileList = {
|
|
15
|
+
files: FileEntry[];
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/repo-view/repo-file-projection.d.ts
|
|
19
|
+
type RepoFileProjection = ReturnType<typeof createRepoFileProjection>;
|
|
20
|
+
/**
|
|
21
|
+
* Write-only maintainer of `repo_file`: the slim per-branch-tip `path → (mode,
|
|
22
|
+
* blob_oid)` index that IS pggit's public read surface. Reads never go through this
|
|
23
|
+
* module — a consumer queries `repo_file ⋈ git_object` (on `oid = blob_oid`) with
|
|
24
|
+
* direct SQL, the one read mechanism (docs/2026-06-26-read-surface-sharpening-design.md).
|
|
25
|
+
* So this only ever rebuilds or drops the projection on push; there is no read method
|
|
26
|
+
* here by design. It is a derived projection of the canonical objects — no duplicate
|
|
27
|
+
* blob bytes, no orphan reaper (the redesign's collapse, §4.5) — droppable and
|
|
28
|
+
* rebuildable at will. The wire repo name resolves to its bigint surrogate (memoized)
|
|
29
|
+
* here, like the other stores.
|
|
30
|
+
*/
|
|
31
|
+
declare function createRepoFileProjection(pg: Sql): {
|
|
32
|
+
/** Drop a repo's entire projection (all branches) — the clean slate for a full
|
|
33
|
+
* rebuild. No blob bytes to reap; the index is the only state. */
|
|
34
|
+
clearRepo(repoId: string): Promise<void>; /** Drop `refName`'s snapshot (branch deleted). */
|
|
35
|
+
dropRefSnapshot(repoId: string, refName: string): Promise<void>;
|
|
36
|
+
/** Replace `refName`'s snapshot with `fileList` (one atomic transaction). The
|
|
37
|
+
* blobs already live in git_object — we store only the path→blob_oid index. */
|
|
38
|
+
rebuildRefSnapshot(repoId: string, refName: string, fileList: FileList): Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/pack/write-pack.d.ts
|
|
42
|
+
type PackInputObject = {
|
|
43
|
+
type: GitObjectType;
|
|
44
|
+
content: Buffer;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/store/object-store.d.ts
|
|
48
|
+
type StoredObject = {
|
|
49
|
+
type: GitObjectType;
|
|
50
|
+
content: Buffer;
|
|
51
|
+
};
|
|
52
|
+
type ObjectStore = ReturnType<typeof createObjectStore>;
|
|
53
|
+
/**
|
|
54
|
+
* Postgres-backed git object store. Each immutable object is one row in the
|
|
55
|
+
* per-repo, HASH-partitioned `git_object` (raw 20-byte `bytea` OID, pack type
|
|
56
|
+
* code, raw inflated body lz4-TOASTed Postgres-side) — packs are a transport
|
|
57
|
+
* encoding produced on serve and consumed on ingest, never stored. So a fetch is
|
|
58
|
+
* a primary-key point-read, not a whole-pack re-inflate.
|
|
59
|
+
*
|
|
60
|
+
* The store is the wire→DB boundary: callers speak hex OIDs and the wire repo
|
|
61
|
+
* name; OIDs are coerced hex↔raw here, and the repo name is resolved to its
|
|
62
|
+
* bigint surrogate (memoized) here.
|
|
63
|
+
*/
|
|
64
|
+
declare function createObjectStore(pg: Sql): {
|
|
65
|
+
/**
|
|
66
|
+
* Build the served pack for a fetch: the want-closure minus the have-closure,
|
|
67
|
+
* re-adding the explicit wants (promisor lazy-fetch roots — a partial clone may
|
|
68
|
+
* want a blob reachable from a tree it already has, so it must not be
|
|
69
|
+
* subtracted). The object count is known from the closure before any content is
|
|
70
|
+
* read; content then streams in keyset batches into the pack encoder, so only
|
|
71
|
+
* one batch of inflated content is ever held (never the whole repo).
|
|
72
|
+
*/
|
|
73
|
+
buildPack(repoId: string, wants: string[], haves: string[], omitBlobs: boolean, includeTag?: boolean): Promise<Buffer>;
|
|
74
|
+
/** The subset of `haves` this repo actually has — the negotiation common set,
|
|
75
|
+
* in one indexed lookup rather than a per-have probe. */
|
|
76
|
+
commonHaves(repoId: string, haves: string[]): Promise<string[]>;
|
|
77
|
+
getObject(repoId: string, oid: string): Promise<StoredObject | null>;
|
|
78
|
+
hasObject(repoId: string, oid: string): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Ingest a received pack: parse it — resolving in-pack deltas, and thin-pack
|
|
81
|
+
* REF_DELTA bases against objects already in this repo — then insert every
|
|
82
|
+
* resolved object as a row.
|
|
83
|
+
*/
|
|
84
|
+
ingestPack(repoId: string, packBytes: Buffer): Promise<{
|
|
85
|
+
oids: string[];
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Connectivity check (spec §5.2): is every object reachable from `oid` present?
|
|
89
|
+
* A push whose new tip fails this references an object the pack neither carried
|
|
90
|
+
* nor delta-resolved, and must be rejected. Delegates to the one reachability
|
|
91
|
+
* engine (`reachableClosure`) shared with clone/fetch, so connectivity and
|
|
92
|
+
* serving can never disagree on what is reachable. Full-closure (matching the
|
|
93
|
+
* old walk's scope); the bounded "new objects only" form is a deferred
|
|
94
|
+
* optimization (OQ-14).
|
|
95
|
+
*/
|
|
96
|
+
isConnected(repoId: string, oid: string): Promise<boolean>;
|
|
97
|
+
/** Seed objects directly (the differential harness + perf bench path): insert
|
|
98
|
+
* each as a row, idempotently. Equivalent to `ingestPack` minus the pack codec. */
|
|
99
|
+
putPack(repoId: string, objects: PackInputObject[]): Promise<{
|
|
100
|
+
oids: string[];
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* git's `ok_to_give_up`: ready once every want reaches a common have by commit/
|
|
104
|
+
* tag ancestry (the haves form a cut below all wants, so the delta is well-
|
|
105
|
+
* defined). One ancestry CTE (edge kinds 2,5) per want replaces `reachesCommon`'s
|
|
106
|
+
* per-object BFS. Generation-number pruning is a deferred §6.4 lever.
|
|
107
|
+
*/
|
|
108
|
+
readyToGiveUp(repoId: string, wants: string[], common: string[]): Promise<boolean>;
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/store/refs-store.d.ts
|
|
112
|
+
type RefRow = {
|
|
113
|
+
name: string;
|
|
114
|
+
oid: string;
|
|
115
|
+
peeled?: string;
|
|
116
|
+
};
|
|
117
|
+
/** A ref change: create (oldOid=zero), update (old→new), or delete (newOid=zero). */
|
|
118
|
+
type RefUpdate = {
|
|
119
|
+
oldOid: string;
|
|
120
|
+
newOid: string;
|
|
121
|
+
ref: string;
|
|
122
|
+
};
|
|
123
|
+
type RefStore = ReturnType<typeof createRefStore>;
|
|
124
|
+
/**
|
|
125
|
+
* Postgres-backed git refs: direct refs (name → oid) and symbolic refs
|
|
126
|
+
* (HEAD → refs/heads/...). Push applies ref changes through `applyRefUpdates`;
|
|
127
|
+
* `setRef`/`setSymref` are the seeding helpers.
|
|
128
|
+
*
|
|
129
|
+
* Like the object store, this is the wire→DB boundary: the repo name resolves to
|
|
130
|
+
* its bigint surrogate (memoized) here, ref names cast to their branded column
|
|
131
|
+
* type, and oids coerce hex↔raw `bytea`.
|
|
132
|
+
*/
|
|
133
|
+
declare function createRefStore(pg: Sql): {
|
|
134
|
+
/**
|
|
135
|
+
* Apply a batch of ref CAS updates. Non-atomic (the default push mode): each
|
|
136
|
+
* ref is independent and the returned flags are per-command. Atomic
|
|
137
|
+
* (`--atomic`): all-or-nothing in one transaction — if any CAS fails, every
|
|
138
|
+
* command is rolled back and the result is all-false (spec §3.6).
|
|
139
|
+
*/
|
|
140
|
+
applyRefUpdates(repoId: string, commands: RefUpdate[], atomic: boolean): Promise<boolean[]>;
|
|
141
|
+
getSymref(repoId: string, name: string): Promise<string | null>;
|
|
142
|
+
/** Direct refs (name → oid + peeled tag target), sorted by name. Excludes
|
|
143
|
+
* symbolic refs. */
|
|
144
|
+
listRefs(repoId: string): Promise<RefRow[]>;
|
|
145
|
+
setRef(repoId: string, name: string, oid: string): Promise<void>;
|
|
146
|
+
setSymref(repoId: string, name: string, target: string): Promise<void>;
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/gc-scheduler.d.ts
|
|
150
|
+
/**
|
|
151
|
+
* Self-scheduling GC — the background drain that decides WHEN the per-repo
|
|
152
|
+
* reachability GC (`store/gc.ts`) runs, off the push/fetch hot path. See
|
|
153
|
+
* `docs/2026-06-24-gc-scheduler-design.md`; the observable contract is §6 of that
|
|
154
|
+
* doc (SCH-1 … SCH-11 / PBT-S1).
|
|
155
|
+
*
|
|
156
|
+
* Mechanism (data-structures-first): every storage-mutating push stamps
|
|
157
|
+
* `repos.last_pushed_at` in its own transaction (the store), so the scheduler is a
|
|
158
|
+
* pure poll loop over Postgres with NO coupling to the request path. One pass
|
|
159
|
+
* (`drainOnce`) selects the eligible repos — `last_pushed_at > last_gc_at`
|
|
160
|
+
* (or `last_gc_at is null`) — and runs `gc()` on each (per-repo serialized,
|
|
161
|
+
* bounded concurrency), then advances `last_gc_at` to the pass's start time so a
|
|
162
|
+
* push landing mid-pass re-qualifies the repo next loop (no lost garbage). `start`
|
|
163
|
+
* is just `drainOnce` on a `setInterval`; all correctness lives in `drainOnce`,
|
|
164
|
+
* which the tests drive directly (the timer is never in a test's critical path).
|
|
165
|
+
*/
|
|
166
|
+
/** One repo's outcome in a drain pass: the repo and what its GC reclaimed.
|
|
167
|
+
* Emitted for EVERY repo the pass judged eligible (including zero-reclaim), so the
|
|
168
|
+
* eligible set itself is observable (SCH-3). */
|
|
169
|
+
type DrainEntry = {
|
|
170
|
+
repo: string;
|
|
171
|
+
deletedObjects: number;
|
|
172
|
+
deletedEdges: number;
|
|
173
|
+
};
|
|
174
|
+
/** What one `drainOnce()` reclaimed, one entry per eligible repo. */
|
|
175
|
+
type DrainSummary = DrainEntry[];
|
|
176
|
+
/** Scheduler tunables (resolved from `env` / `startServer` opts). `graceSeconds`
|
|
177
|
+
* is passed straight to `gc()`; `intervalMs` is the drain cadence (the debounce
|
|
178
|
+
* window); `concurrency` caps repos GC'd at once per pass so one large-orphan repo
|
|
179
|
+
* cannot head-of-line-block the rest. */
|
|
180
|
+
type GcSchedulerOptions = {
|
|
181
|
+
graceSeconds: number;
|
|
182
|
+
intervalMs: number;
|
|
183
|
+
concurrency: number;
|
|
184
|
+
};
|
|
185
|
+
type GcScheduler = ReturnType<typeof createGcScheduler>;
|
|
186
|
+
/**
|
|
187
|
+
* Build the GC scheduler over a porsager client (the same wire→DB boundary the
|
|
188
|
+
* stores take). `drainOnce()` runs one poll+sweep pass; `start()`/`stop()` drive
|
|
189
|
+
* it on `intervalMs`. Reachable objects are never touched — it only invokes the
|
|
190
|
+
* per-repo GC primitive, which is reachability-safe.
|
|
191
|
+
*/
|
|
192
|
+
declare function createGcScheduler(pg: Sql, opts: GcSchedulerOptions): {
|
|
193
|
+
drainOnce: () => Promise<DrainSummary>;
|
|
194
|
+
start: () => void;
|
|
195
|
+
stop: () => Promise<void>;
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/store/gc.d.ts
|
|
199
|
+
/**
|
|
200
|
+
* Per-repo reachability GC — the one piece of the Postgres-native redesign (§7)
|
|
201
|
+
* that the rest of the spine deferred. See `docs/2026-06-24-force-commit-gc-design.md`
|
|
202
|
+
* for the design; the observable contract is §4 of that doc, and the authoritative
|
|
203
|
+
* algorithm is §7 of `internal/archived/2026-06-22-pggit-postgres-native-storage-
|
|
204
|
+
* redesign.md`.
|
|
205
|
+
*
|
|
206
|
+
* The mechanism (data-structures-first): materialize the LIVE set — the reachable
|
|
207
|
+
* closure from every ref tip — into an UNLOGGED table, then sweep `git_object` in
|
|
208
|
+
* batched short transactions with a server-side anti-join (`NOT EXISTS`) against
|
|
209
|
+
* that table plus a `created_at` grace cutoff. Reachability itself is NOT re-derived
|
|
210
|
+
* here: it is exactly `reachableClosure(omitBlobs=false)`, the one engine clone /
|
|
211
|
+
* fetch / connectivity already share, so GC can never disagree with them about what
|
|
212
|
+
* is reachable.
|
|
213
|
+
*/
|
|
214
|
+
/** Tunables for one GC pass. `graceSeconds` is REQUIRED — no silent default: an
|
|
215
|
+
* object is reclaimed iff it is unreachable from every ref AND its `created_at`
|
|
216
|
+
* is older than `graceSeconds` (0 ⇒ reclaim all unreachable; a huge value ⇒
|
|
217
|
+
* retain). `batchLimit` caps the per-batch DELETE size (sweep tuning only — it
|
|
218
|
+
* never changes the final observable state). `maintain` (default true) runs the
|
|
219
|
+
* post-sweep VACUUM/REINDEX; the self-scheduling drain passes `false` so a
|
|
220
|
+
* frequent per-repo pass never triggers a full-table VACUUM on the hot cadence
|
|
221
|
+
* (autovacuum reclaims the GC churn instead). Maintenance is observable-neutral —
|
|
222
|
+
* it changes dead-tuple bloat, never the row/clone state. */
|
|
223
|
+
type GcOptions = {
|
|
224
|
+
graceSeconds: number;
|
|
225
|
+
batchLimit?: number;
|
|
226
|
+
maintain?: boolean;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Internal-only test seam (NOT part of the public `GcOptions` contract): hooks the
|
|
230
|
+
* GC pass at the one point §5 in-flight safety depends on. `afterLiveSet` is awaited
|
|
231
|
+
* AFTER the live set is materialized and BEFORE the object sweep begins, so a test
|
|
232
|
+
* can deterministically interpose a concurrent push there and assert the just-pushed
|
|
233
|
+
* tip is never partially reclaimed. Test-only; do not document or use in production.
|
|
234
|
+
*/
|
|
235
|
+
type GcHooks = {
|
|
236
|
+
afterLiveSet?: () => Promise<void>;
|
|
237
|
+
};
|
|
238
|
+
type InternalGcOptions = GcOptions & {
|
|
239
|
+
_hooks?: GcHooks;
|
|
240
|
+
};
|
|
241
|
+
/** What one GC pass reclaimed: the deleted `git_object` / `git_edge` row counts. */
|
|
242
|
+
type GcResult = {
|
|
243
|
+
deletedObjects: number;
|
|
244
|
+
deletedEdges: number;
|
|
245
|
+
};
|
|
246
|
+
type Gc = ReturnType<typeof createGc>;
|
|
247
|
+
/**
|
|
248
|
+
* Build the GC over a porsager client (the same wire→DB boundary the object and ref
|
|
249
|
+
* stores take). `gc(repo, opts)` reclaims a single repo's unreachable-and-old-enough
|
|
250
|
+
* objects offline; reachable objects are always retained.
|
|
251
|
+
*/
|
|
252
|
+
declare function createGc(pg: Sql): {
|
|
253
|
+
gc(repo: string, opts: InternalGcOptions): Promise<GcResult>;
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/index.d.ts
|
|
257
|
+
type GitAppDeps = {
|
|
258
|
+
objects: ObjectStore;
|
|
259
|
+
refs: RefStore;
|
|
260
|
+
/** Optional queryable-view layer. When provided, push maintains a `repo_file`
|
|
261
|
+
* path→blob index per branch; when omitted, this is a plain git remote. */
|
|
262
|
+
snapshots?: RepoFileProjection;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Build the git-remote Hono app (smart-HTTP, protocol v2 fetch). Mountable into
|
|
266
|
+
* a host app via `host.route("/git", createGitApp(deps))`; the host owns the
|
|
267
|
+
* Postgres lifecycle behind `deps`.
|
|
268
|
+
*/
|
|
269
|
+
declare function createGitApp(deps: GitAppDeps, opts?: {
|
|
270
|
+
instrument?: boolean;
|
|
271
|
+
}): Hono;
|
|
272
|
+
//#endregion
|
|
273
|
+
export { type DrainEntry, type DrainSummary, type Gc, type GcOptions, type GcResult, type GcScheduler, type GcSchedulerOptions, GitAppDeps, createGc, createGcScheduler, createGitApp };
|
|
274
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/object/object.ts","../src/repo-view/build-file-list.ts","../src/repo-view/repo-file-projection.ts","../src/pack/write-pack.ts","../src/store/object-store.ts","../src/store/refs-store.ts","../src/gc-scheduler.ts","../src/store/gc.ts","../src/index.ts"],"mappings":";;;;;KAIY,aAAA;;;KCSA,SAAA;EAAc,IAAA;EAAc,IAAA;EAAc,OAAA;AAAA;AAAA,KAC1C,QAAA;EAAa,KAAA,EAAO,SAAS;AAAA;;;KCR7B,kBAAA,GAAqB,UAAU,QAAQ,wBAAA;;AFFnD;;;;AAAyB;;;;ACSzB;;iBCMgB,wBAAA,CAAyB,EAAA,EAAI,GAAA;EDNxB;;6BCac,OAAA,QDbmB;kCCoBhB,OAAA,WAAoB,OAAA;EDpBG;AAC7D;qCCgCiB,OAAA,UACC,QAAA,EACL,QAAA,GACR,OAAA;AAAA;;;KC3CO,eAAA;EACX,IAAA,EAAM,aAAA;EACN,OAAA,EAAS,MAAM;AAAA;;;KCkCJ,YAAA;EACX,IAAA,EAAM,aAAA;EACN,OAAA,EAAS,MAAM;AAAA;AAAA,KAGJ,WAAA,GAAc,UAAU,QAAQ,iBAAA;AJ3CnB;;;;ACSzB;;;;;;;ADTyB,iBIgFT,iBAAA,CAAkB,EAAA,EAAI,GAAA;EHvEuB;AAC7D;;;;AAAyC;;;4BGoFxB,KAAA,YACC,KAAA,YACA,SAAA,WACG,UAAA,aAEhB,OAAA,CAAQ,MAAA;EFjGD;;8BEuKsB,KAAA,aAAoB,OAAA;4BAmBtB,GAAA,WAAgB,OAAA,CAAQ,YAAA;4BA2BxB,GAAA,WAAgB,OAAA;EFxMhC;;;;;6BEyNiB,SAAA,EAAa,MAAA,GAAS,OAAA;IAAU,IAAA;EAAA;EF3LrD;;;;;;;;;8BE8MsB,GAAA,WAAgB,OAAA;EF9NQ;;0BEwOzC,OAAA,EACL,eAAA,KACP,OAAA;IAAU,IAAA;EAAA;EF5NG;;;AAEN;;;gCEuOK,KAAA,YACC,MAAA,aAEb,OAAA;AAAA;;;KClRO,MAAA;EAAW,IAAA;EAAc,GAAA;EAAa,MAAA;AAAA;;KAGtC,SAAA;EAAc,MAAA;EAAgB,MAAA;EAAgB,GAAA;AAAA;AAAA,KAE9C,QAAA,GAAW,UAAU,QAAQ,cAAA;;;;;;;;AJDoB;AAC7D;iBIsLgB,cAAA,CAAe,EAAA,EAAI,GAAA;;;AJtLM;;;;kCIkMxB,QAAA,EACJ,SAAA,IAAW,MAAA,YAEnB,OAAA;4BAiD2B,IAAA,WAAiB,OAAA;;;4BAcf,OAAA,CAAQ,MAAA;yBAiBb,IAAA,UAAc,GAAA,WAAgB,OAAA;4BAe3B,IAAA,UAAc,MAAA,WAAmB,OAAA;AAAA;;;;;;AL9SjE;;;;AAAyB;;;;ACSzB;;;;;;;;KKUY,UAAA;EAAe,IAAA;EAAc,cAAA;EAAwB,YAAA;AAAA;ALTxB;AAAA,KKY7B,YAAA,GAAe,UAAU;;;AJpBrC;;KI0BY,kBAAA;EACX,YAAA;EACA,UAAA;EACA,WAAA;AAAA;AAAA,KAGW,WAAA,GAAc,UAAU,QAAQ,iBAAA;;;;;;;iBAa5B,iBAAA,CAAkB,EAAA,EAAI,GAAA,EAAK,IAAA,EAAM,kBAAA;mBAkDpB,OAAA,CAAQ,YAAA;;cA6Bb,OAAA;AAAA;;;;;;AN9HxB;;;;AAAyB;;;;ACSzB;;;;;;;;AAA6D;AAC7D;;;;KMmBY,SAAA;EAAc,YAAA;EAAsB,UAAA;EAAqB,QAAA;AAAA;;;;AL3BM;AAa3E;;;KKuBK,OAAA;EAAY,YAAA,SAAqB,OAAO;AAAA;AAAA,KACxC,iBAAA,GAAoB,SAAA;EAAc,MAAA,GAAS,OAAO;AAAA;;KAG3C,QAAA;EAAa,cAAA;EAAwB,YAAY;AAAA;AAAA,KAEjD,EAAA,GAAK,UAAU,QAAQ,QAAA;;;;;;iBAgBnB,QAAA,CAAS,EAAA,EAAI,GAAA;mBAKN,IAAA,EAAQ,iBAAA,GAAoB,OAAA,CAAQ,QAAA;AAAA;;;KCnD/C,UAAA;EACX,OAAA,EAAS,WAAA;EACT,IAAA,EAAM,QAAA;ERhBkB;AAAA;EQmBxB,SAAA,GAAY,kBAAA;AAAA;;APVb;;;;iBOmHgB,YAAA,CACf,IAAA,EAAM,UAAA,EACN,IAAA;EAAQ,UAAA;AAAA,IACN,IAAI"}
|