1---
2title: Engine operations
3description: Build your own HTTP API over Git operations, with scoped preparation, application authorization, and explicit commit.
4---
5
6`Git.Engine` supplies repository operations. Your application defines its API,
7chooses middleware, and decides when an operation may commit.
8
9## Own the HTTP group
10
11```typescript
12import * as Git from "alchemy/Git";
13import * as GitHttp from "alchemy/Git/Http";
14import * as Effect from "effect/Effect";
15import * as HttpApi from "effect/http-api/HttpApi";
16import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
17import { Authentication } from "./authentication.ts";
18import { receivePack } from "./receive-pack.ts";
19
20class AppApi extends HttpApi.make("app")
21 .add(GitHttp.Protocol)
22 .middleware(Authentication) {}
23
24const ProtocolLive = HttpApiBuilder.group(AppApi, "protocol", (h) =>
25 Effect.gen(function* () {
26 const defaults = yield* Git.Handlers;
27 const git = yield* Git.Engine;
28 return h
29 .handleRaw("infoRefs", defaults.protocol.infoRefs)
30 .handleRaw("uploadPack", defaults.protocol.uploadPack)
31 .handleRaw("receivePack", input => receivePack(git, input));
32 }),
33);
34```
35
36`Authentication` is a native `HttpApiMiddleware.Service`. It can provide your
37`CurrentUser` service to all three handlers. `handleRaw` preserves that middleware
38while leaving the binary body for Git's streaming decoder. The default read
39handlers rely on your middleware to authorize repository access.
40
41Capture long-lived services such as `Git.Engine` or a database while building
42the group, or explicitly provide them to an extracted handler. Read the request's
43user inside the handler. The [complete example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-git-service/src/api/protocol.ts)
44shows this with `Session` and native API middleware.
45
46## Prepare a push, then commit
47
48Inside your receive-pack handler:
49
50```typescript
51const push = yield* GitHttp.ReceivePack.decode(request);
52if (push._tag === "Probe") return GitHttp.ReceivePack.probeResponse();
53
54const repo = yield* git.repositories.get(params);
55yield* checkRefChanges(repo, push.updates);
56
57const prepared = yield* git.preparePush(repo, push.input);
58yield* validateContents(prepared);
59
60const result = yield* git.commitPush(prepared);
61return GitHttp.ReceivePack.response(push, result);
62```
63
64Run the whole operation inside `Effect.scoped`. These steps have different effects:
65
66| Step | What happens |
67| --- | --- |
68| `ReceivePack.decode(request)` | Decode bounded command pkt-lines. Keep the pack streaming and retain negotiated capabilities. |
69| Application policy | Check the caller, repository, proposed refs, or database permissions before ingesting the pack. |
70| `engine.preparePush(repo, input)` | Verify and stage objects; check connectivity. No refs move. |
71| Application validation | Inspect incoming objects and existing objects through the prepared view. |
72| `engine.commitPush(prepared)` | Recheck connectivity and apply ref CAS, respecting negotiated atomic behavior. |
73| `ReceivePack.response(push, result)` | Encode Git report-status and sideband framing. |
74
75The input and prepared push are scoped and single-use. Exiting without commit
76aborts staging. A second commit, use after scope closure, or a commit through a
77different engine instance fails. Cleanup checks durable commit state before
78deleting spilled bytes, because an interrupted RPC may already have committed.
79
80## Read incoming content
81
82```typescript
83const object = yield* prepared.readObject(update.newOid);
84if (object?.type === 1) {
85 const commit = yield* Git.parseCommit(object.content);
86 // Inspect the commit's tree with prepared.readObject(commit.tree).
87}
88```
89
90The view includes live objects and objects staged by this push, never another
91push's uncommitted objects. Reads default to a 1 MiB uncompressed size limit;
92pass a second argument to choose a different per-object limit. Nothing becomes
93visible to ordinary repository reads until commit.
94
95## Authorize with application data
96
97Policies are ordinary functions. For example, an application with `CurrentUser`
98and `Database` services can define:
99
100```typescript
101const checkRefChanges = (repo: Git.RepoMetaData, updates: ReadonlyArray<Git.RefUpdate>) =>
102 Effect.gen(function* () {
103 const user = yield* CurrentUser;
104 const database = yield* Database;
105 for (const update of updates) {
106 const allowed = yield* database.mayUpdateBranch(user.id, repo.repoId, update.ref);
107 if (!allowed) {
108 return yield* new Git.PushDenied({ ref: update.ref, reason: "branch is protected" });
109 }
110 }
111 });
112```
113
114`Database.mayUpdateBranch` is an application method, not a Git export. Its typed
115errors and service requirements remain part of the effect. Provide the database
116implementation when constructing the group; native API middleware supplies the user.
117You can split each policy into its own module and call several policies in order.
118
119Checking an external database and committing Git refs are separate transactions.
120Ref CAS prevents stale Git writes; a policy requiring atomic coordination with a
121database needs an application transaction or version protocol as well.
122
123## REST updates and merges
124
125The same policy can guard the other mutation paths:
126
127```typescript
128const prepared = yield* git.prepareRefUpdate(repo, {
129 ref: "refs/heads/main",
130 newOid,
131 expectedOid, // null means the ref must not exist
132});
133yield* checkRefChanges(repo, prepared.updates);
134const ref = yield* prepared.commit;
135```
136
137`prepareRefRemoval(repo, { ref, expectedOid })` prepares a deletion.
138`prepareMerge(repo, { number, message, expectedHeadOid })` prepares a pull merge.
139Each returns `updates` and a single-use `commit` effect; use `Effect.scoped`.
140Ref writes pin the inspected ref. Merges pin both inspected branch tips.
141
142A merge's `updates` describe the base-to-head proposal. The final merge commit
143is constructed during commit; this preview does not expose a staged merge tree.
144For rules about content, validate the inspected tips and their changes before
145committing. Push preparation, by contrast, exposes the uploaded objects directly.
146
147Call the shared application policy from **every exposed mutation path**, including
148GitHub-compatible merges. Default handlers perform operations without application
149authorization. The complete example routes both REST and GitHub merges through
150one application `mergePull` function.
151
152## Errors and responses
153
154Authenticate before reading the pack. HTTP `401` responses should include Git's
155Basic challenge. After commands are decoded, translate `PushDenied` with
156`ReceivePack.reject(push, reason)` so Git displays a per-ref rejection. On a JSON
157endpoint, `Git.PushDenied` has a `403` schema.
158
159`ReceivePack.failure(reason)` encodes malformed protocol input or unpack failures.
160Database failures remain failures; they should not be silently converted into
161permission denials. [Part 6](/git/tutorial/part-6) shows the complete response mapping.
162
163## Supply your own transport
164
165For a background operation or a protocol implementation of your own, construct
166input from decoded updates and a stream of raw pack bytes:
167
168```typescript
169const input = yield* Git.Push.fromStream(updates, packStream, { atomic: true });
170const prepared = yield* git.preparePush(repo, input);
171yield* validateContents(prepared);
172const result = yield* git.commitPush(prepared);
173```
174
175No `HttpServerRequest` is required. Run this in `Effect.scoped`. Use `Stream.empty`
176when there is no pack, such as a delete-only push. `fromStream` validates ref/OID
177shapes and ties the stream producer to the input's scope.
178
179## Supply the implementation
180
181`Git.EngineLive` needs `RepoStore`, `RegistryStore`, `BlobStore`, and `Hasher`.
182It does not need an HTTP request, identity service, or server. It exposes
183`repositories`, `refs`, `objects`, and `pulls` operations, plus preparation and
184commit methods. These can also be called from background work.
185
186`Git.ApiHandlersLive` supplies the engine and the optional default HTTP handlers.
187Use it when mixing custom handlers with defaults. Use `EngineLive` alone when
188your application implements all the HTTP handling it needs.