1---
2title: "Part 6: Protect a branch"
3description: Authorize parsed Git mutations in ordinary HTTP handlers, using the request's user before committing changes.
4---
5
6import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
7
8The middleware decides who may push. Now add a rule about what an authorized
9push may do: it may update `main`, but it may not delete it.
10
11Your handler decodes the Git commands, calls an application policy, then prepares
12and commits the push. It runs in the same request as the middleware from Part 4,
13so `Session` is available. Git's binary body does not require a separate middleware
14system.
15
16## Define the branch rule
17
18Create `src/branch-policy.ts`:
19
20```typescript
21// src/branch-policy.ts
22import * as Git from "alchemy/Git";
23import * as Effect from "effect/Effect";
24import { Session } from "./session.ts";
25
26export const checkRefChanges = (repo: Git.RepoMetaData, updates: ReadonlyArray<Git.RefUpdate>) =>
27 Effect.gen(function* () {
28 const { user } = yield* Session;
29 for (const update of updates) {
30 if (user === null || user.id !== repo.owner) {
31 return yield* new Git.PushDenied({ ref: update.ref, reason: "only the owner may change refs" });
32 }
33 if (update.ref === "refs/heads/main" && /^0+$/.test(update.newOid)) {
34 return yield* new Git.PushDenied({ ref: update.ref, reason: "main cannot be deleted" });
35 }
36 }
37 });
38```
39
40A deletion uses an all-zero `newOid`. This ordinary function can live in its own
41module. More policies are more Effect-returning functions; call them in sequence.
42A function may require a database service and query it with `user.id`, just like
43any other application effect.
44
45## Implement the push endpoint
46
47Create `src/protocol.ts`:
48
49```typescript
50// src/protocol.ts
51import * as Git from "alchemy/Git";
52import * as GitHttp from "alchemy/Git/Http";
53import * as Effect from "effect/Effect";
54import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
55import { checkRefChanges } from "./branch-policy.ts";
56
57export const ProtocolLive = HttpApiBuilder.group(Git.Api, "protocol", (h) => Effect.gen(function* () {
58 const git = yield* Git.Engine;
59 const defaults = yield* Git.Handlers;
60 return h
61 .handleRaw("infoRefs", defaults.protocol.infoRefs)
62 .handleRaw("uploadPack", defaults.protocol.uploadPack)
63 .handleRaw("receivePack", ({ params, request }) => Effect.scoped(Effect.gen(function* () {
64 const repo = yield* git.repositories.get(params).pipe(Effect.catchTag("StoreError", Effect.die));
65 const push = yield* GitHttp.ReceivePack.decode(request);
66 if (push._tag === "Probe") return GitHttp.ReceivePack.probeResponse();
67 return yield* Effect.gen(function* () {
68 yield* checkRefChanges(repo, push.updates);
69 const prepared = yield* git.preparePush(repo, push.input);
70 // Additional application validation can read prepared.readObject(oid) here.
71 return GitHttp.ReceivePack.response(push, yield* git.commitPush(prepared));
72 }).pipe(Effect.catchTag("PushDenied", error => Effect.succeed(GitHttp.ReceivePack.reject(push, error.reason))));
73 })).pipe(
74 Effect.catchTag("StoreError", error => Effect.succeed(GitHttp.ReceivePack.failure(error.reason))),
75 Effect.catchTag(["WireProtocolError", "PackIngestError"], error => Effect.succeed(GitHttp.ReceivePack.failure(error.reason))),
76 ));
77}));
78
79```
80
81`decode` reads a bounded command header and leaves the pack streaming.
82`preparePush` stages and validates objects without moving refs. `commitPush`
83changes refs using Git's expected-old-OID checks and atomic capability. Leaving
84the scope without committing cleans up staging. A policy failure becomes Git's
85report-status, so `git push` prints your rejection reason.
86
87For content validation, call `prepared.readObject(oid)` between prepare and commit.
88It reads this push's incoming objects and existing live objects, with a 1 MiB
89per-object limit by default. [Engine operations](/git/blocks/engine) covers this boundary.
90
91## Apply the same rule to REST writes
92
93The host also exposes a REST endpoint for deleting refs. Protect it with the
94same function so it cannot bypass the push policy. Create `src/ref-writes.ts`:
95
96```typescript
97// src/ref-writes.ts
98import * as Git from "alchemy/Git";
99import * as Effect from "effect/Effect";
100import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
101import { checkRefChanges } from "./branch-policy.ts";
102
103export const RefsLive = HttpApiBuilder.group(Git.Api, "refs", (h) => Effect.gen(function* () {
104 const git = yield* Git.Engine;
105 const defaults = yield* Git.Handlers;
106 return h.handleAll({ ...defaults.refs,
107 update: ({ params, query, payload }) => Effect.scoped(Effect.gen(function* () {
108 if (/^0+$/.test(payload.newOid)) return yield* new Git.PushDenied({ ref: query.name, reason: "use the DELETE endpoint to remove a ref" });
109 const repo = yield* git.repositories.get(params);
110 const prepared = yield* git.prepareRefUpdate(repo, { ref: query.name, ...payload });
111 yield* checkRefChanges(repo, prepared.updates);
112 const result = yield* prepared.commit;
113 if (result === undefined) return yield* new Git.PushDenied({ ref: query.name, reason: "use the DELETE endpoint to remove a ref" });
114 return new Git.Ref({ name: result.name, oid: result.oid as Git.Oid });
115 })).pipe(Effect.catchTag("StoreError", Effect.die)),
116 remove: ({ params, query, payload }) => Effect.scoped(Effect.gen(function* () {
117 const repo = yield* git.repositories.get(params);
118 const prepared = yield* git.prepareRefRemoval(repo, { ref: query.name, ...payload });
119 yield* checkRefChanges(repo, prepared.updates);
120 yield* prepared.commit;
121 })).pipe(Effect.catchTag("StoreError", Effect.die)),
122 });
123}));
124```
125
126The prepared ref operation pins the state you inspected. If another request moves
127the ref before commit, the commit fails with `RefConflict`.
128
129This tutorial's new rule prevents deletion. Merges cannot delete a branch. For
130policies that restrict other ref changes, also authorize REST and GitHub merges;
131`prepareMerge` captures both branch tips, and the [application example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-git-service/src/api/ref-writes.ts)
132shares its policy across those paths.
133
134## Register your implementations
135
136Add these imports to `src/git.ts`:
137
138```typescript
139// src/git.ts (additional imports)
140import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
141import { ProtocolLive } from "./protocol.ts";
142import { RefsLive } from "./ref-writes.ts";
143```
144
145Replace `PublicRoutes` with:
146
147```typescript
148// src/git.ts
149const GitRoutes = HttpApiBuilder.layer(Git.Api).pipe(
150 Layer.provide(Layer.mergeAll(Git.GroupsLive, ProtocolLive, RefsLive)),
151);
152
153const PublicRoutes = Layer.mergeAll(AppApiLive, GitRoutes).pipe(
154 Layer.provide(Authentication.layer),
155);
156```
157
158These groups use the existing `Git.Api` contract, so the remaining default groups
159can be reused. Your existing `HttpLive` storage layers stay the same. If you add
160API middleware or change a prefix on a different `AppApi`, build every affected
161group against that actual API instead; see [HTTP routes](/git/blocks/server).
162
163## Deploy the rule
164
165<Tabs syncKey="pkgManager">
166 <TabItem label="bun" icon="bun">
167 <Code code={`bun alchemy deploy`} lang="sh" />
168 </TabItem>
169 <TabItem label="npm" icon="npm">
170 <Code code={`npx alchemy deploy`} lang="sh" />
171 </TabItem>
172 <TabItem label="pnpm" icon="pnpm">
173 <Code code={`pnpm alchemy deploy`} lang="sh" />
174 </TabItem>
175 <TabItem label="yarn" icon="seti:yarn">
176 <Code code={`yarn alchemy deploy`} lang="sh" />
177 </TabItem>
178</Tabs>
179
180Use Dana's `$KEY` when Git prompts during the checks below. Keep the remote
181pointed at `$HOST/$OWNER/web.git` from Part 4.
182
183## Verify an ordinary update
184
185```sh
186printf 'Main is protected from deletion.\n' >> work/README.md
187git -C work commit -am "Document the branch rule"
188git -c credential.helper= -C work push origin main
189```
190
191The push should succeed: updating `main` does not match the deletion rule.
192
193## Verify that another branch can be deleted
194
195```sh
196git -c credential.helper= -C work push origin HEAD:feature
197git -c credential.helper= -C work push origin --delete feature
198```
199
200Both operations should succeed.
201
202## Verify that main cannot be deleted
203
204```sh
205git -c credential.helper= -C work push origin --delete main
206```
207
208Expect the push to be rejected with `main cannot be deleted`. Verify that the
209remote still has the branch:
210
211```sh
212git -c credential.helper= -C work ls-remote origin refs/heads/main
213```
214
215It should print the current commit ID and `refs/heads/main`. Authentication
216accepted the caller; the application handler rejected the proposed change.
217
218## Remove the tutorial deployment
219
220When you finish experimenting:
221
222<Tabs syncKey="pkgManager">
223 <TabItem label="bun" icon="bun">
224 <Code code={`bun alchemy destroy`} lang="sh" />
225 </TabItem>
226 <TabItem label="npm" icon="npm">
227 <Code code={`npx alchemy destroy`} lang="sh" />
228 </TabItem>
229 <TabItem label="pnpm" icon="pnpm">
230 <Code code={`pnpm alchemy destroy`} lang="sh" />
231 </TabItem>
232 <TabItem label="yarn" icon="seti:yarn">
233 <Code code={`yarn alchemy destroy`} lang="sh" />
234 </TabItem>
235</Tabs>
236
237This removes the Worker, repositories' storage, R2 bucket contents, and account
238database from this tutorial stack. Keep your local `work` checkout if you want
239to retain its commits.
240
241## Build further
242
243You have deployed Git, controlled access, allowed public reads, introduced user
244credentials, added your own API, and enforced a ref rule. Each behavior can now
245change independently.
246
247- [HTTP routes](/git/blocks/server) covers custom Git handlers and API schemas.
248- [Authentication and authorization](/git/blocks/auth) describes the two policy boundaries.
249- [Recipes](/git/recipes) covers other storage and hashing implementations.
250- The [complete application example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-git-service)
251 adds a browser UI and serves it on the Git host's origin.