| 1 | --- |
| 2 | title: Cloning & pushing |
| 3 | description: Standard git clients over smart HTTP. What the wire supports, where credentials go, and what to do when something hangs. |
| 4 | --- |
| 5 | |
| 6 | The host speaks git's smart HTTP protocol. Standard clients work |
| 7 | unmodified, with no extensions and no wrapper CLI: |
| 8 | |
| 9 | ```sh |
| 10 | git clone "https://$HOST/acme/web.git" |
| 11 | ``` |
| 12 | |
| 13 | ## Credentials |
| 14 | |
| 15 | `git` sends HTTP Basic. The credential goes in the password field of |
| 16 | the remote URL and the username is ignored: |
| 17 | |
| 18 | ```sh |
| 19 | git remote add origin "https://x:$TOKEN@$HOST/acme/web.git" |
| 20 | ``` |
| 21 | |
| 22 | What `$TOKEN` is depends on the middleware the host runs. |
| 23 | |
| 24 | ### The starter secret |
| 25 | |
| 26 | The host from [Getting Started](/git/getting-started) accepts one |
| 27 | secret, an `Alchemy.Random` the stack outputs. `alchemy deploy` prints |
| 28 | it: |
| 29 | |
| 30 | ```sh |
| 31 | bun alchemy deploy |
| 32 | # { url: "https://…workers.dev", secret: "9f2c…" } |
| 33 | export TOKEN=9f2c… |
| 34 | ``` |
| 35 | |
| 36 | ### A Better Auth API key |
| 37 | |
| 38 | The host from [Part 4](/git/tutorial/part-4) verifies API keys. A |
| 39 | signed-in user mints one; it is shown once: |
| 40 | |
| 41 | ```sh |
| 42 | curl -s -b jar -X POST "$HOST/api/auth/api-key/create" \ |
| 43 | -H "Origin: $HOST" -H "Content-Type: application/json" \ |
| 44 | -d '{"name":"laptop"}' | jq -r .key |
| 45 | ``` |
| 46 | |
| 47 | The example app has a settings page that does the same. |
| 48 | |
| 49 | ### Your own |
| 50 | |
| 51 | The token is whatever your middleware accepts. The one |
| 52 | [Part 4](/git/tutorial/part-4) put in front of the host decodes the |
| 53 | Basic password and asks Better Auth whose key it is: |
| 54 | |
| 55 | ```typescript |
| 56 | const { password } = yield* HttpApiBuilder.securityDecode( |
| 57 | HttpApiSecurity.basic, |
| 58 | ); |
| 59 | const key = Redacted.value(password); |
| 60 | if (key !== "") { |
| 61 | const verified = yield* auth.api.verifyApiKey({ body: { key } }); |
| 62 | return verified.valid && verified.key |
| 63 | ? { id: verified.key.referenceId } |
| 64 | : undefined; |
| 65 | } |
| 66 | ``` |
| 67 | |
| 68 | A token issued by anything else is verified the same way: read it |
| 69 | from the request, run the route or refuse. [Auth](/git/blocks/auth) |
| 70 | has the pattern. |
| 71 | |
| 72 | ### Keeping it out of `.git/config` |
| 73 | |
| 74 | Leave the credential out of the URL and let a credential helper |
| 75 | supply it. A middleware that answers an anonymous request with `401` |
| 76 | and `WWW-Authenticate` makes `git` prompt: |
| 77 | |
| 78 | ```sh |
| 79 | git config credential.helper osxkeychain # or store, or your manager |
| 80 | git clone "https://$HOST/acme/web.git" |
| 81 | Password for 'https://x@…': <paste $TOKEN> |
| 82 | ``` |
| 83 | |
| 84 | A middleware that honors `public` serves reads of a public repository |
| 85 | with no credential at all; the tutorial's does. |
| 86 | |
| 87 | ## What is supported |
| 88 | |
| 89 | | Capability | Notes | |
| 90 | | --- | --- | |
| 91 | | Clone | served from a precomputed bundle when one is current | |
| 92 | | Incremental fetch | `multi_ack_detailed` with `no-done` | |
| 93 | | Shallow clone | `--depth`, including deepening later | |
| 94 | | Push | thin packs with offset and reference deltas | |
| 95 | | Atomic push | all refs move or none do | |
| 96 | | Force push | compare-and-swap on the old value | |
| 97 | | Tags | lightweight and annotated, with peeled advertisement | |
| 98 | | Status | `report-status`, `report-status-v2`, side-band | |
| 99 | |
| 100 | Every push runs a full connectivity check. The objects it references |
| 101 | must already exist or arrive in the same pack, so a repository is never |
| 102 | left with dangling references. |
| 103 | |
| 104 | Protocol v0 only. Clients that prefer v2 negotiate down on their own. |
| 105 | |
| 106 | ## Large pushes |
| 107 | |
| 108 | Push size is not capped. The body streams through the Worker and into |
| 109 | the blob store as it arrives, and the repository's Durable Object |
| 110 | receives rows rather than the pack. Peak memory is a fixed window, |
| 111 | whatever the push size. [Hasher](/git/blocks/hasher) has the path and |
| 112 | [Scaling](/git/recipes/scaling) the numbers. |
| 113 | |
| 114 | ## Troubleshooting |
| 115 | |
| 116 | **A credential prompt in a script.** Set `GIT_TERMINAL_PROMPT=0` so a |
| 117 | missing credential fails at once instead of hanging. |
| 118 | |
| 119 | **A push rejected with a ref message.** That is the protocol working: |
| 120 | a non-fast-forward, a stale compare-and-swap, or a read-only |
| 121 | repository. The message names the ref. |
| 122 | |
| 123 | **A hung clone behind a proxy.** The wire endpoints must be forwarded |
| 124 | verbatim, with the request body streamed and the `Host` header kept. |
| 125 | A proxy that buffers bodies breaks streaming pushes. |