| 1 | --- |
| 2 | title: "Part 1: Push your first repository" |
| 3 | description: Deploy a Git host, push a commit, and clone it back. Add storage and hashing one layer at a time. |
| 4 | --- |
| 5 | |
| 6 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 7 | |
| 8 | Start with a Git remote you can push to. This part deploys a Worker backed by |
| 9 | Durable Objects and R2, then sends a real commit through it and clones it back. |
| 10 | |
| 11 | :::caution |
| 12 | This first deployment is open: anyone who knows its URL can create repositories, |
| 13 | read them, and write to them. Use a disposable repository. [Part 2](/git/tutorial/part-2) |
| 14 | adds access control before you use the host for private work. |
| 15 | ::: |
| 16 | |
| 17 | ## Create the project |
| 18 | |
| 19 | You need Bun or Node.js 22+, Git, and a Cloudflare account with Workers, Durable Objects, and R2. |
| 20 | Complete [Cloudflare setup](/cloudflare/setup) to connect your account. |
| 21 | |
| 22 | <Tabs syncKey="pkgManager"> |
| 23 | <TabItem label="bun" icon="bun"> |
| 24 | <Code code={`mkdir git-tutorial |
| 25 | cd git-tutorial |
| 26 | bun init -y |
| 27 | bun add alchemy effect |
| 28 | mkdir src`} lang="sh" /> |
| 29 | </TabItem> |
| 30 | <TabItem label="npm" icon="npm"> |
| 31 | <Code code={`mkdir git-tutorial |
| 32 | cd git-tutorial |
| 33 | npm init -y |
| 34 | npm install alchemy effect |
| 35 | mkdir src`} lang="sh" /> |
| 36 | </TabItem> |
| 37 | <TabItem label="pnpm" icon="pnpm"> |
| 38 | <Code code={`mkdir git-tutorial |
| 39 | cd git-tutorial |
| 40 | pnpm init |
| 41 | pnpm add alchemy effect |
| 42 | mkdir src`} lang="sh" /> |
| 43 | </TabItem> |
| 44 | <TabItem label="yarn" icon="seti:yarn"> |
| 45 | <Code code={`mkdir git-tutorial |
| 46 | cd git-tutorial |
| 47 | yarn init -y |
| 48 | yarn add alchemy effect |
| 49 | mkdir src`} lang="sh" /> |
| 50 | </TabItem> |
| 51 | </Tabs> |
| 52 | |
| 53 | Run the remaining commands from this project directory. Keep the same directory, |
| 54 | stack name, and deployment stage throughout the tutorial so each deploy updates |
| 55 | this host. |
| 56 | |
| 57 | ## Add the Git routes |
| 58 | |
| 59 | Create `src/git.ts`: |
| 60 | |
| 61 | ```typescript |
| 62 | // src/git.ts |
| 63 | import * as Git from "alchemy/Git"; |
| 64 | import * as Layer from "effect/Layer"; |
| 65 | |
| 66 | export const GitLive = Git.ApiLive.pipe( |
| 67 | Layer.provide(Git.ApiHandlersLive), |
| 68 | ); |
| 69 | ``` |
| 70 | |
| 71 | `Git.ApiLive` registers the Git HTTP endpoints, including the protocol used by |
| 72 | `git push` and `git clone`. `Git.ApiHandlersLive` implements those endpoints. |
| 73 | Its remaining dependencies describe where repositories live and how incoming |
| 74 | objects are checked. Supply each one below before deploying. |
| 75 | |
| 76 | ## Store repository data |
| 77 | |
| 78 | ```diff lang="typescript" |
| 79 | // src/git.ts |
| 80 | export const GitLive = Git.ApiLive.pipe( |
| 81 | Layer.provide(Git.ApiHandlersLive), |
| 82 | + Layer.provide(Git.ReposDurableObject), |
| 83 | ); |
| 84 | ``` |
| 85 | |
| 86 | `Git.ReposDurableObject` gives each repository a Durable Object. It stores the |
| 87 | repository's refs and object metadata and coordinates writes to that repository. |
| 88 | |
| 89 | ## Look up repositories by name |
| 90 | |
| 91 | ```diff lang="typescript" |
| 92 | // src/git.ts |
| 93 | Layer.provide(Git.ReposDurableObject), |
| 94 | + Layer.provide(Git.RegistryDurableObject), |
| 95 | ); |
| 96 | ``` |
| 97 | |
| 98 | The registry maps an `owner/name`, such as `acme/web`, to its repository. This |
| 99 | lets an incoming request find the right Durable Object. |
| 100 | |
| 101 | ## Verify incoming Git objects |
| 102 | |
| 103 | ```diff lang="typescript" |
| 104 | // src/git.ts |
| 105 | Layer.provide(Git.RegistryDurableObject), |
| 106 | + Layer.provide(Git.HasherInline), |
| 107 | ); |
| 108 | ``` |
| 109 | |
| 110 | Git objects are identified by hashes of their contents. `Git.HasherInline` |
| 111 | verifies incoming objects in the Worker during a push. |
| 112 | |
| 113 | ## Store packs and large objects |
| 114 | |
| 115 | ```diff lang="typescript" |
| 116 | // src/git.ts |
| 117 | +import * as Cloudflare from "alchemy/Cloudflare"; |
| 118 | import * as Git from "alchemy/Git"; |
| 119 | import * as Layer from "effect/Layer"; |
| 120 | |
| 121 | +const GitObjects = Cloudflare.R2.Bucket("GitObjects", { forceDestroy: true }); |
| 122 | + |
| 123 | export const GitLive = Git.ApiLive.pipe( |
| 124 | ``` |
| 125 | |
| 126 | This bucket holds packs, clone bundles, and large objects. `forceDestroy: true` |
| 127 | lets you remove the tutorial stack even when the bucket contains data; destroying |
| 128 | this stack also deletes those objects. |
| 129 | |
| 130 | Connect the bucket to Git: |
| 131 | |
| 132 | ```diff lang="typescript" |
| 133 | // src/git.ts |
| 134 | Layer.provide(Git.HasherInline), |
| 135 | + Layer.provide(Git.BlobStoreR2(GitObjects)), |
| 136 | ); |
| 137 | ``` |
| 138 | |
| 139 | The R2 layer supplies the blob-storage operations used by the Worker and the |
| 140 | repository Durable Objects. |
| 141 | |
| 142 | ## Serve Git from a Worker |
| 143 | |
| 144 | Create `src/host.ts`: |
| 145 | |
| 146 | ```typescript |
| 147 | // src/host.ts |
| 148 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 149 | import * as Git from "alchemy/Git"; |
| 150 | import * as Http from "alchemy/Http"; |
| 151 | import * as Effect from "effect/Effect"; |
| 152 | import * as Layer from "effect/Layer"; |
| 153 | import * as HttpRouter from "effect/http/HttpRouter"; |
| 154 | import { GitLive } from "./git.ts"; |
| 155 | |
| 156 | export default class GitHost extends Cloudflare.Worker<GitHost>()( |
| 157 | "GitHost", |
| 158 | { main: import.meta.url, ...Git.GIT_WORKER_OPTIONS }, |
| 159 | Effect.gen(function* () { |
| 160 | const fetch = yield* HttpRouter.toHttpEffect( |
| 161 | GitLive.pipe(Layer.provide(Http.Platform)), |
| 162 | ); |
| 163 | return { fetch }; |
| 164 | }), |
| 165 | ) {} |
| 166 | ``` |
| 167 | |
| 168 | `HttpRouter.toHttpEffect` turns the route layer into the Worker's `fetch` handler. |
| 169 | `Http.Platform` provides its HTTP platform dependencies. `GIT_WORKER_OPTIONS` |
| 170 | sets the Worker compatibility flags and CPU limit used by Git. |
| 171 | |
| 172 | ## Declare the stack |
| 173 | |
| 174 | Create `alchemy.run.ts`: |
| 175 | |
| 176 | ```typescript |
| 177 | // alchemy.run.ts |
| 178 | import * as Alchemy from "alchemy"; |
| 179 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 180 | import * as Effect from "effect/Effect"; |
| 181 | import GitHost from "./src/host.ts"; |
| 182 | |
| 183 | export default Alchemy.Stack( |
| 184 | "GitTutorial", |
| 185 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 186 | Effect.gen(function* () { |
| 187 | const host = yield* GitHost; |
| 188 | return { url: host.url.as<string>() }; |
| 189 | }), |
| 190 | ); |
| 191 | ``` |
| 192 | |
| 193 | Yielding the Worker includes its bucket and Durable Object bindings in the |
| 194 | stack. The output gives you the URL clients will use. |
| 195 | |
| 196 | ## Deploy |
| 197 | |
| 198 | <Tabs syncKey="pkgManager"> |
| 199 | <TabItem label="bun" icon="bun"> |
| 200 | <Code code={`bun alchemy deploy`} lang="sh" /> |
| 201 | </TabItem> |
| 202 | <TabItem label="npm" icon="npm"> |
| 203 | <Code code={`npx alchemy deploy`} lang="sh" /> |
| 204 | </TabItem> |
| 205 | <TabItem label="pnpm" icon="pnpm"> |
| 206 | <Code code={`pnpm alchemy deploy`} lang="sh" /> |
| 207 | </TabItem> |
| 208 | <TabItem label="yarn" icon="seti:yarn"> |
| 209 | <Code code={`yarn alchemy deploy`} lang="sh" /> |
| 210 | </TabItem> |
| 211 | </Tabs> |
| 212 | |
| 213 | Copy the printed URL, including `https://`, without a trailing slash: |
| 214 | |
| 215 | ```sh |
| 216 | export HOST="https://your-worker.workers.dev" |
| 217 | ``` |
| 218 | |
| 219 | Verify that the Git API is reachable: |
| 220 | |
| 221 | ```sh |
| 222 | curl --fail-with-body "$HOST/api/v1/repos" |
| 223 | ``` |
| 224 | |
| 225 | It returns a JSON repository list. On a new host the list is empty. A new |
| 226 | `workers.dev` deployment can briefly return a Cloudflare 404 while it propagates; |
| 227 | repeat this read check until the API responds before creating the repository. |
| 228 | Updates in later parts can also briefly serve the previous version. |
| 229 | |
| 230 | ## Create a repository |
| 231 | |
| 232 | ```sh |
| 233 | curl --fail-with-body -X POST "$HOST/api/v1/repos" \ |
| 234 | -H "Content-Type: application/json" \ |
| 235 | -d '{"owner":"acme","name":"web"}' |
| 236 | ``` |
| 237 | |
| 238 | The repository is named `acme/web`. At this stage `acme` is just a namespace; |
| 239 | there are no user accounts. Creating a repository reserves that name before you |
| 240 | push any commits. |
| 241 | |
| 242 | ## Push a commit |
| 243 | |
| 244 | ```sh |
| 245 | git init -b main work |
| 246 | git -C work config user.name "Tutorial" |
| 247 | git -C work config user.email "tutorial@example.com" |
| 248 | printf '# My repository\n' > work/README.md |
| 249 | git -C work add README.md |
| 250 | git -C work commit -m "First commit" |
| 251 | git -C work remote add origin "$HOST/acme/web.git" |
| 252 | git -C work push -u origin main |
| 253 | ``` |
| 254 | |
| 255 | The push uploads the commit and its objects, then creates `main` on your host. |
| 256 | |
| 257 | ## Clone it back |
| 258 | |
| 259 | ```sh |
| 260 | git clone "$HOST/acme/web.git" verify |
| 261 | git -C verify fsck --strict |
| 262 | git -C work rev-parse HEAD |
| 263 | git -C verify rev-parse HEAD |
| 264 | ``` |
| 265 | |
| 266 | The two commit IDs should match, and `fsck` should report no corrupt or missing |
| 267 | objects. You now have a working Git remote. |
| 268 | |
| 269 | Continue to [Part 2: Control access](/git/tutorial/part-2). If you stop here, |
| 270 | remove the open learning deployment: |
| 271 | |
| 272 | <Tabs syncKey="pkgManager"> |
| 273 | <TabItem label="bun" icon="bun"> |
| 274 | <Code code={`bun alchemy destroy`} lang="sh" /> |
| 275 | </TabItem> |
| 276 | <TabItem label="npm" icon="npm"> |
| 277 | <Code code={`npx alchemy destroy`} lang="sh" /> |
| 278 | </TabItem> |
| 279 | <TabItem label="pnpm" icon="pnpm"> |
| 280 | <Code code={`pnpm alchemy destroy`} lang="sh" /> |
| 281 | </TabItem> |
| 282 | <TabItem label="yarn" icon="seti:yarn"> |
| 283 | <Code code={`yarn alchemy destroy`} lang="sh" /> |
| 284 | </TabItem> |
| 285 | </Tabs> |