dgit
Host Git repositories and browse their history on Workers and SQLite Durable Objects.
dgit is a self-hosted GitHub/GitLab alternative built on Cloudflare (Durable Objects, R2, Workers). Paid services required. Inspect the source and license in the linked repository.
Source & license
Upstream license: MIT
License TL;DR
You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.
Explain MIT in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Paid services required
dgit has a documented low-volume paid Cloudflare path beginning with Workers Paid ($5 USD/account/month), with separately metered usage. The supplied high CPU limit requires Paid; repository sizes and active Durable Object duration drive costs.
Hosting requirements
- Use Workers Paid for the configured 300,000 ms CPU limit; $5 USD/account/month is a baseline, not a fixed all-inclusive price.
- Workers Paid starts at $5 USD/account/month and includes 10 million requests/month plus 30 million CPU milliseconds/month; higher CPU limits do not make execution unmetered.
- Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
- Paid SQLite Durable Objects include 1 million requests/month, 400,000 GB-s duration/month and 5 GB-month SQL storage; active duration and excess database operations are metered.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: d5eba04. Hosting eligibility reflects the deployment documentation and listed assumptions.
- github ↗
dgit is a git server for Cloudflare Workers and for your own machines with [celld](https://celld.dev). Each repository is a Durable Object: a small server with a name and a private SQLite database that holds the repository's objects and refs, speaks the git smart HTTP protocol to a stock git client, and renders a cgit-style web interface. There is no origin server, no filesystem, and no GitHub in the critical path. A repository nobody touches costs almost nothing, and applications shard by construction: one hot repository cannot slow another. Reads are public; pushes authenticate; pushing to a name that does not exist creates the repository.
- gitlab ↗
dgit is a git server for Cloudflare Workers and for your own machines with [celld](https://celld.dev). Each repository is a Durable Object: a small server with a name and a private SQLite database that holds the repository's objects and refs, speaks the git smart HTTP protocol to a stock git client, and renders a cgit-style web interface. There is no origin server, no filesystem, and no GitHub in the critical path. A repository nobody touches costs almost nothing, and applications shard by construction: one hot repository cannot slow another. Reads are public; pushes authenticate; pushing to a name that does not exist creates the repository.
- workers ↗
script name stays "git-cells" — Durable Object data is bound // to it, and renaming would orphan every hosted repository. "name": "git-cells", "main": "src/index.ts", "compatibility_date": "2026-08-01", "routes": [ { "pattern": "git.littledivy.com", "custom_domain": true } ], "limits": { // pack indexing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell"
- r2 ↗
"class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. // Remove this binding to fall back to the SQLite-only path (as on celld). "r2_buckets": [ { "binding": "PACK_CACHE", "bucket_name": "dgit-pack-cache" } ], "vars": { "SITE_NAME": "git.littledivy.com",
- durable-objects ↗
xing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell" }, { "name": "REGISTRY", "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. /
- paid ↗
script name stays "git-cells" — Durable Object data is bound // to it, and renaming would orphan every hosted repository. "name": "git-cells", "main": "src/index.ts", "compatibility_date": "2026-08-01", "routes": [ { "pattern": "git.littledivy.com", "custom_domain": true } ], "limits": { // pack indexing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell"
- paid ↗
"class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. // Remove this binding to fall back to the SQLite-only path (as on celld). "r2_buckets": [ { "binding": "PACK_CACHE", "bucket_name": "dgit-pack-cache" } ], "vars": { "SITE_NAME": "git.littledivy.com",
- paid ↗
xing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell" }, { "name": "REGISTRY", "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. /
- paid ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- paid ↗
infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp
- paid ↗
jects are available both on Workers Free and Workers Paid plans. - **Workers Free plan**: Only Durable Objects with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) are available. - **Workers Paid plan**: Durable Objects with the SQLite storage backend are available. The [key-value storage backend](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/#storage-backends) is only available to accounts that already have a key-value-backed namespace. If you wish to downgrade from a Workers Paid plan to a Workers Free plan, you must first ensure that you have deleted all Durable Object namespaces with the key-value storage backend. On Workers Free plan: - If you exceed any one of the free tier limits, further operations of that type will fail with an error. - Daily free limits reset at 00:00 UTC. ## Compute billing Durable Objects are billed for compute duration (wall-clock time) while the Durable Object is actively running or is idle in memory but unable to [hibernate](https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/). Durable Objects that are idle and eligible for hibernation are not billed for duration, even before the runtime has hibernated them. Requests to a D
- paid ↗
billed accordingly. | | Free plan | Paid plan | | --- | --- | --- | | Requests | 100,000 / day | 1 million / month, + $0.15/million<br> Includes HTTP requests, RPC sessions<sup>1</sup>, WebSocket messages<sup>2</sup>, and alarm invocations | | Duration<sup>3</sup> | 13,000 GB-s / day | 400,000 GB-s / month, + $12.50/million GB-s<sup>4,5</sup> | <details> <summary> Footnotes </summary> <sup>1</sup> Each <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/">RPC session</a> is billed as one request to your Durable Object. Every <a href="https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/">RPC method call</a> on a <a href="https://developers.cloudflare.com/durable-objects/">Durable Objects stub</a> is its own RPC session and therefore a single billed request. RPC method calls can return objects (stubs) extending <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#lifetimes-memory-and-resource-management"><code>RpcTarget</code></a> and invo
- paid ↗
/). | | Workers Free plan | Workers Paid plan | | --- | --- | --- | | Rows reads <sup>1,2</sup> | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written <sup>1,2,3,4</sup> | 100,000 / day | First 50 million / month included + $1.00 / million rows | | SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month | <details> <summary> Footnotes </summary> <sup>1</sup> Rows read and rows written included limits and rates match <a href="https://developers.cloudflare.com/d1/platform/pricing/">D1 pricing</a>, Cloudflare's serverless SQL database. <sup>2</sup> Key-value methods like <code>get()</code>, <code>put()</code>, <code>delete()</code>, or <code>list(
- paid ↗
es Functions, Workers KV, Hyperdrive, and Durable Objects usage for a minimum charge of $5 USD per month for an account. The plan includes increased initial usage allotments, with clear charges for usage that exceeds the base plan. There are no additional charges for data transfer (egress) or throughput (bandwidth). All included usage is on a monthly basis. Pages Functions billing All [Pages Functions](https://developers.cloudflare.com/pages/functions/) are billed as Workers. All pricing and inclusions in this document apply to Pages Functions. Refer to [Functions Pricing](https://developers.cloudflare.com/pages/functions/pricing/) for more information on Pages Functions pricing. ## Workers Users on the Wo
- MIT ↗
MIT License Copyright (c) 2026 Divy Srivastava Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONI
- architecture ↗
script name stays "git-cells" — Durable Object data is bound // to it, and renaming would orphan every hosted repository. "name": "git-cells", "main": "src/index.ts", "compatibility_date": "2026-08-01", "routes": [ { "pattern": "git.littledivy.com", "custom_domain": true } ], "limits": { // pack indexing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell"
- architecture ↗
"class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. // Remove this binding to fall back to the SQLite-only path (as on celld). "r2_buckets": [ { "binding": "PACK_CACHE", "bucket_name": "dgit-pack-cache" } ], "vars": { "SITE_NAME": "git.littledivy.com",
- architecture ↗
xing of a large push is CPU-bound; 5 minutes is the paid-plan max "cpu_ms": 300000 }, "durable_objects": { "bindings": [ { "name": "REPO", "class_name": "RepoCell" }, { "name": "REGISTRY", "class_name": "Registry" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RepoCell", "Registry"] } ], // Optional: with PACK_CACHE bound, pushed pack bytes are stored in R2 (off the // DO's SQLite) and full clones are served straight from R2 by the Worker. /
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Git repository hosting over Smart HTTP with a web history browser; issues, pull requests, CI runners and full software-forge features are excluded.
See supporting source ↗Git repository hosting over Smart HTTP with a web history browser; issues, pull requests, CI runners and full software-forge features are excluded.
See supporting source ↗How it works
The shape of dgit on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit d5eba04eda58. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 2 files
Cloudflare Workers · compatibility 2026-08-01
git-cells · default
Entrypoint: src/index.ts
Configured route patterns: git.littledivy.com
PACK_CACHE→ R2REPO→ Durable Objects · class RepoCellREGISTRY→ Durable Objects · class Registry
Cloudflare Workers · compatibility 2026-08-01
git-cells · default
Entrypoint: src/index.ts
REPO→ Durable Objects · class RepoCellREGISTRY→ Durable Objects · class Registry
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L101 · tokens calls (conditional paths may differ): split, filter, list.map, t.trim
- L113 · tooManyRequests calls (conditional paths may differ): String, Math.ceil
- L120 · digest calls (conditional paths may differ): crypto.subtle.digest, encode
- L125 · digestsEqual calls (conditional paths may differ): Math.max
- L135 · secretsEqual calls (conditional paths may differ): digestsEqual, digest
- L139 · clientIp calls (conditional paths may differ): req.headers.get
- L147 · parseBasic calls (conditional paths may differ): req.headers.get, header.startsWith, atob, header.slice, decoded.indexOf, decoded.slice
- L165 · pruneAuthFailures calls (conditional paths may differ): authFailures.delete, next, authFailures.keys
- L177 · rateLimited calls (conditional paths may differ): authFailures.get, Date.now
- L183 · recordAuthFailure calls (conditional paths may differ): Date.now, authFailures.get, authFailures.set, pruneAuthFailures
- L195 · tokenAuthorize calls (conditional paths may differ): tokens, ctx.request.headers.get, header.startsWith, unauthorized, clientIp, rateLimited, tooManyRequests, recordAuthFailure, digest, digestsEqual
- L235 · indexPage calls (conditional paths may differ): env.REGISTRY.getByName, registry.list, repoUrl, esc, age, Math.floor, htmlResponse, layout, siteBase
- L267 · pruneInfoMemo calls (conditional paths may differ): next, infoMemo.keys, infoMemo.delete
- L275 · repoInfo calls (conditional paths may differ): infoMemo.get, Date.now, get, env.REGISTRY.getByName, infoMemo.set, pruneInfoMemo
- L307 · serveCloneFromR2 calls (conditional paths may differ): parseUploadRequest, req.wants.every, req.caps.has, stub.currentPackKey, bucket.get, parseInt, Number.isFinite, streamingCloneResponse
- L344 · withCors calls (conditional paths may differ): res.headers.set
- L361 · parseRefUpdates calls (conditional paths may differ): res.headers.get, JSON.parse, decodeURIComponent, Array.isArray
- L383 · repoPath calls (conditional paths may differ): pattern.exec, decodeURIComponent, REPO_NAME.test, segs.push, RESERVED.has, segs.join
- L408 · classify calls (conditional paths may differ): url.searchParams.get, sub.startsWith
- L427 · createDurableGit calls (conditional paths may differ): path.startsWith, corsPreflight, withCors, Response.json, url.searchParams.get, list, env.REGISTRY.getByName, json, repos.map, pageCache, cache.match, indexPage, res.clone, toCache.headers.set, ctx.waitUntil, cache.put, repoPath, errorPage, siteBase, classify
Environment references: env.GIT_TOKEN · env.GIT_TOKENS · env.SITE_NAME · env.SITE_DESC · env.REGISTRY · env.SITE_OWNER · env.PACK_CACHE · env.REPO
- L5 · esc calls (conditional paths may differ): s.replace
- L10 · repoUrl calls (conditional paths may differ): join, map, repo.split
- L15 · repoBase calls (conditional paths may differ): repo.slice, repo.lastIndexOf
- L20 · age calls (conditional paths may differ): Math.max, Math.floor, Date.now, unit.endsWith, fmt
- L33 · fmtDate calls (conditional paths may differ): padStart, String, d.getUTCFullYear, pad, d.getUTCMonth, d.getUTCDate, d.getUTCHours, d.getUTCMinutes, d.getUTCSeconds
- L46 · fmtDate2822 calls (conditional paths may differ): parseInt, tz.slice, padStart, String, d.getUTCDay, d.getUTCDate, d.getUTCMonth, d.getUTCFullYear, pad, d.getUTCHours, d.getUTCMinutes, d.getUTCSeconds
- L79 · layout calls (conditional paths may differ): repoUrl, encodeURIComponent, join, tabs.map, esc, o.branches.map
- L155 · errorPage calls (conditional paths may differ): htmlResponse, layout, esc
- L39 · advertisement calls (conditional paths may differ): concat, pkt, join, store.head, store.resolveHead, refs.push, store.refs, lines.push, refs.forEach
- L85 · advertisedOids calls (conditional paths may differ): store.resolveHead, oids.add, store.refs, r.name.startsWith, store.typeAndSize, peelToCommitOid
- L106 · wantsAreAllTips calls (conditional paths may differ): store.refs, tips.add, wantSet.has
- L116 · sidebandFrames calls (conditional paths may differ): payload.subarray, framed.set, frames.push, pkt
- L137 · streamingCloneResponse calls (conditional paths may differ): body.getReader, ctrl.enqueue, pkt, sidebandFrames, te.encode, reader.read, ctrl.close, ctrl.error, reader.cancel
- L189 · parseUploadRequest calls (conditional paths may differ): parser.read, req.caps.add, line.startsWith, req.wants.push, line.slice, split, trim, req.haves.push, req.clientShallows.push, parseInt
- L224 · lsRefs calls (conditional paths may differ): store.resolveHead, lines.push, pkt, req.caps.has, store.head, store.refs, ref.name.startsWith, store.typeAndSize, peelToCommitOid, concat
- L240 · peelToCommitOid calls (conditional paths may differ): store.get, parseTag
- L260 · latestCommitTime calls (conditional paths may differ): store.resolveHead, tips.add, store.refs, peelToCommitOid, store.get, parseCommit, Date.now
- L285 · computeDepthSet calls (conditional paths may differ): peelToCommitOid, commits.has, commits.add, queue.push, queue.shift, store.get, parseCommit, boundary.add, boundary.delete, store.has
- L332 · interestingCommits calls (conditional paths may differ): peelToCommitOid, haveCommits.has, set.addHex, stack.push, stack.pop, store.get, yieldMaybe, parseCommit, store.has
- L374 · excludedObjects calls (conditional paths may differ): Math.max, setTimeout, store.has, peelToCommitOid, haveCommits.add, interestingCommits, excluded.addHex, commitStack.push, commitStack.pop, store.get, yieldMaybe, parseTag, parseCommit, shallowStops.has, boundary.push, interesting.atHex, excluded.hasHex, trees.push, trees.pop, parseTree
- L477 · collectPackOids calls (conditional paths may differ): walk.addHex, walk.atHex, setTimeout, store.typeAndSize, commitLimit.has, excluded.hasHex, store.get, parseCommit, walk.markHex, childOids
- L519 · uploadPack calls (conditional paths may differ): parseUploadRequest, release, lsRefs, req.wants.some, isOid, pkt, advertisedOids, allowed.has, store.has, preamble.push, computeDepthSet, clientShallowSet.has, commits.has, boundary.has, te.encode, req.haves.filter, common.map, req.caps.has, concat, wantsAreAllTips
- L908 · warmFullClone calls (conditional paths may differ): store.reachableCount, store.reachablePackedAfter, store.reachableHas, w.rawDelta, store.packs.readRaw, store.get, w.object, w.rawFull, store.reachableLooseAfter, objects, concat, pending.push, sidebandFrames, packCache.beginMultipart, capture.push, buffered.push, flushBuffered, te.encode, writer.header, ctrl.enqueue
- L926 · objects calls (conditional paths may differ): store.reachablePackedAfter, store.reachableHas, w.rawDelta, store.packs.readRaw, store.get, w.object, w.rawFull, store.reachableLooseAfter
- L1055 · isAncestor calls (conditional paths may differ): store.get, stack.pop, parseCommit, seen.has, seen.add, stack.push
- L1087 · parsePushCommands calls (conditional paths may differ): parser.read, line.indexOf, split, trim, line.slice, line.match, commands.push
- L1118 · reachableComplete calls (conditional paths may differ): known.addHex, known.atHex, store.typeAndSize, store.get, childOids
- L1165 · childOids calls (conditional paths may differ): parseCommit, parseTag, parseTree, isGitlinkMode, out.push
- L1194 · packComplete calls (conditional paths may differ): store.resetPushEdges, store.addPushEdges, store.firstMissingPushEdge, store.packNonBlobOidsAfter, store.get, childOids, pending.push, flush
- L1225 · validatePush calls (conditional paths may differ): store.getMeta, store.reachableVersion, packComplete, store.refs, known.addHex, peelToCommitOid, store.resolveHead, decisions.push, cmd.ref.startsWith, cmd.ref.includes, BAD_REF.test, cmd.ref.endsWith, some, cmd.ref.split, c.startsWith, store.getRef, reachableComplete, store.has, isAncestor
- L1298 · commitPush calls (conditional paths may differ): results.push, store.getRef, store.delRef, store.setRef, store.head, filter, store.refs, r.name.startsWith, branches.find, store.setHead, store.extendReachable, store.invalidateReachable
- L1350 · renderStatus calls (conditional paths may differ): pkt, lines.push, concat, sidebandFrames
- L4 · concat calls (conditional paths may differ): out.set
- L16 · toHex calls (conditional paths may differ): padStart, toString
- L22 · fromHex calls (conditional paths may differ): parseInt, s.slice
- L30 · isOid calls (conditional paths may differ): test
- L34 · sha1hex calls (conditional paths may differ): toHex, sha1
- L1924 · overDeclaredLength calls (conditional paths may differ): parseInt, req.headers.get, Number.isFinite
- L1929 · tooLargeResponse calls (conditional paths may differ): Math.round
- L1937 · renderHunk calls (conditional paths may differ): esc
- L1947 · rawBlobResponse calls (conditional paths may differ): toLowerCase, name.slice, name.lastIndexOf, Object.hasOwn, isBinary
- L1977 · decodePath calls (conditional paths may differ): filter, map, p.split, decodeURIComponent
Environment references: env.PACK_CACHE · env.MAX_PUSH_MB · env.INGEST_CACHE_MB · env.SHA1DC · env.SITE_NAME · env.SITE_DESC
- L16 · objectHeader calls (conditional paths may differ): te.encode
- L20 · hashObject calls (conditional paths may differ): sha1hex, concat, objectHeader
- L56 · parsePerson calls (conditional paths may differ): line.indexOf, line.slice, split, trim, parseInt, test, Number.isFinite
- L71 · splitHeaders calls (conditional paths may differ): text.indexOf, text.slice, head.split, line.startsWith, push, line.slice, line.indexOf, headers.push, cont.push, join
- L98 · parseCommit calls (conditional paths may differ): splitHeaders, td.decode, message.split, isOid, c.parents.push, parsePerson
- L127 · parseTag calls (conditional paths may differ): splitHeaders, td.decode, isOid, parsePerson
- L145 · parseTree calls (conditional paths may differ): td.decode, data.subarray, toHex, entries.push
- L173 · modeString calls (conditional paths may differ): parseInt, isTreeMode, isGitlinkMode, bits
- L85 · beginRawMultipart calls (conditional paths may differ): bucket.createMultipartUpload, parts.push, mp.uploadPart, mp.complete, mp.abort
- L20 · diffLines calls (conditional paths may differ): aText.split, bText.split, a.pop, b.pop, ops.push, Math.min, Math.max, Math.floor, trace.push, v.slice, ops.reverse
- L93 · toHunks calls (conditional paths may differ): ops.forEach, changes.push, changes.slice, ranges.push, Math.max, Math.min, hunk.ops.push, hunks.push
- L145 · isBinary calls (conditional paths may differ): Math.min
- L27 · blame calls (conditional paths may differ): td.decode, tipText.split, tipLines.pop, tipLines.map, diffLines, eqMap.set, eqMap.get
- L13 · safePath calls (conditional paths may differ): p.startsWith, p.includes, every, p.split
- L18 · safeTarget calls (conditional paths may differ): t.startsWith, every, t.split
- L23 · safeEntry calls (conditional paths may differ): safePath, safeTarget, decode
- L29 · octal calls (conditional paths may differ): padStart, n.toString, te.encode
- L34 · tarHeader calls (conditional paths may differ): te.encode, lastIndexOf, path.slice, h.set, subarray, octal, padStart, sum.toString
- L65 · tarGz calls (conditional paths may differ): safeEntry, decode, parts.push, tarHeader, pako.gzip, concat
- L101 · dosDateTime calls (conditional paths may differ): d.getUTCFullYear, d.getUTCMonth, d.getUTCDate, d.getUTCHours, d.getUTCMinutes, d.getUTCSeconds
- L108 · zip calls (conditional paths may differ): dosDateTime, safeEntry, te.encode, crc32, pako.deflateRaw, lv.setUint32, lv.setUint16, local.set, parts.push, cv.setUint32, cv.setUint16, cd.set, central.push, ev.setUint32, ev.setUint16, concat
- L38 · safeUrl calls (conditional paths may differ): url.trim, test, u.startsWith
- L47 · rewriteUrl calls (conditional paths may differ): safeUrl, test, u.replace, rel.indexOf, rel.slice, encodeURIComponent, join, map, rel.split, filter
- L67 · rewriteSrcset calls (conditional paths may differ): map, val.split, p.trim, seg.search, seg.slice, rewriteUrl, parts.some, join, parts.filter
- L81 · parseAttrs calls (conditional paths may differ): re.exec, toLowerCase, ALLOWED_ATTRS.has, val.slice, URL_ATTRS.has, rewriteSrcset, rewriteUrl, out.push, esc, out.join
- L107 · sanitizeTag calls (conditional paths may differ): raw.match, esc, toLowerCase, ALLOWED_TAGS.has, parseAttrs, VOID_TAGS.has
- L120 · inline calls (conditional paths may differ): stash.push, text.replace, keep, esc, m.slice, sanitizeTag, s.replace, rewriteUrl
- L154 · splitRow calls (conditional paths may differ): s.trim, t.startsWith, t.slice, t.endsWith, map, t.split, replace, c.trim
- L161 · alignOf calls (conditional paths may differ): startsWith, cell.trim, endsWith
- L170 · renderMarkdown calls (conditional paths may differ): split, src.replaceAll, out.push, inline, para.join, listStack.pop, flushPara, closeLists, closeQuote, raw.match, resetBlocks, test, code.push, esc, code.join, replace, raw.includes, DELIM_RE.test, includes, splitRow
- L64 · tokenize calls (conditional paths may differ): tokens.push, test, src.startsWith, flushPlain, src.indexOf, src.slice, Math.min, isWord, lang.keywords.has
- L158 · highlightLines calls (conditional paths may differ): filename.toLowerCase, base.includes, base.slice, base.lastIndexOf, src.split, plainLines.pop, plainLines.map, esc, tokenize, t.text.split, lines.push, lines.pop, lines.slice
Build and deployment pipeline · 1 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: push
publish · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - actions/setup-node@v4
actions/setup-node@v4 - Shell command
npm ci - Shell command
npm run typecheck - tag matches package.json
test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME" - publish, unless the version is already on the registry
v=$(node -p "require('./package.json').version") if npm view "durable-git@$v" version >/dev/null 2>&1; then echo "durable-git@$v already published, skipping" else npm publish fi
deploy: wrangler deploy
Repository README
View original on GitHub ↗Full upstream document by @littledivy · README.md · snapshot d5eba04
dgit
Durable git.
dgit is a git server for Cloudflare Workers and for your own machines with celld. Each repository is a Durable Object: a small server with a name and a private SQLite database that holds the repository's objects and refs, speaks the git smart HTTP protocol to a stock git client, and renders a cgit-style web interface. There is no origin server, no filesystem, and no GitHub in the critical path. A repository nobody touches costs almost nothing, and applications shard by construction: one hot repository cannot slow another. Reads are public; pushes authenticate; pushing to a name that does not exist creates the repository.
How it works
dgit implements git in TypeScript: pkt-line framing, packfile parsing with ofs- and ref-delta resolution, pack generation over a streaming SHA-1, commit/tree/tag codecs, and a Myers diff. The one dependency is pako, for zlib.
A push streams into the repository's cell and is stored as the packfile
the client sent; an index maps each object id to its pack, offset, and
delta base, so the client's compression is preserved rather than
re-derived. When an R2 bucket is bound the pack bytes are written to R2
and only the index stays in the cell's SQLite, so pack storage is no
longer capped by the per-cell database. A clone walks the closure of the
requested refs and copies the stored compressed bytes verbatim into the
outgoing pack; a full clone, once built, is cached in R2 and afterwards
streamed straight from the Worker, so repeat clones never load the cell.
Fetch
negotiation excludes the closure of the client's haves, cut correctly
at shallow boundaries, so an incremental fetch downloads only what is
missing. Shallow clones (--depth, deepening, --unshallow), thin
packs, side-band progress, forced updates, and ref deletion behave as
they do against any git server.
The web interface is the cgit surface: summary, refs, log with search
and per-path history, tree, blob with syntax highlighting, blame,
commit and arbitrary-range diffs, format-patch output that applies
cleanly with git am, tar.gz and zip snapshots of any ref, about pages
rendered from the README, atom feeds, and commit-activity statistics.
Repositories carry a description, an owner, a section on the index
page, and a private flag that hides them and gates every read behind
the push token.
Deploy to Cloudflare
npm install
npx wrangler r2 bucket create dgit-pack-cache # optional: R2 pack offload + clone cache
npx wrangler deploy
npx wrangler secret put GIT_TOKEN # the push password
The PACK_CACHE R2 binding in wrangler.jsonc is optional: with it,
pushed pack bytes live in R2 (off the cell's SQLite) and full clones are
served straight from R2 by the Worker without loading the cell. Remove
the binding and everything falls back to the SQLite-only path.
Then push anything:
git remote add origin https://<your-host>/myrepo.git
git push -u origin main
A Workers request is bounded at 128MB of memory and five minutes of CPU, so a very large history lands as a series of smaller pushes rather than one; day-to-day pushes, clones, and fetches fit comfortably. Building a full-history clone of a repository with millions of objects can exceed the CPU bound the first time — once such a clone is cached in R2 it streams from the Worker without rebuilding, and shallow and incremental fetches of the same repository are fine regardless. The largest repositories belong on celld.
Self-host on celld
celld runs the same Worker against a bucket you own, with none of the
managed platform's request bounds. Set a real GIT_TOKEN var in
wrangler.celld.jsonc first:
celld deploy wrangler.celld.jsonc --bucket s3://my-cells --endpoint https://...
CELLD_V8_HEAP_LIMIT_MB=4096 CELLD_LTX_DURABILITY_TIMEOUT_SECS=180 \
celld --bucket s3://my-cells --endpoint https://... \
--listen 0.0.0.0:8080 --internal-listen 10.0.0.1:8081 --advertise 10.0.0.1:8081
Each repository's SQLite database replicates to the bucket; nodes are disposable, and a killed node's repositories come back bit-identical. The heap and durability-deadline variables give large single-cell ingests the room the defaults do not.
Build on dgit
dgit is also a library, published as
durable-git. The deployed
Worker above is three lines around it:
import { createDurableGit, secretsEqual } from "durable-git";
export { RepoCell, Registry } from "durable-git";
export default createDurableGit({
async authorize({ repo, op, private: priv, credentials, env }) {
if (op === "read" && !priv) return true;
return secretsEqual(credentials?.pass ?? "", await lookupDeployKey(env, repo));
},
onPush(event, env) {
return fetch("https://ci.example.com/hook", { method: "POST", body: JSON.stringify(event) });
},
});
The package ships TypeScript source, which wrangler bundles directly; bind the Durable Object classes as wrangler.jsonc does and re-export them from your entry module.
authorize gates every repository-scoped request and replaces the
stock GIT_TOKEN policy entirely (the private flag is yours to
consult); onPush fires with the ref updates a push applied. Every
repository also serves a JSON content API, gated like its pages, and
the same surface as typed RPC on the cell. ui: false runs it headless
behind your own frontend, cors opens the API cross-origin, and
namespaces: true routes GitHub-style owner/name repositories. See
docs at docs/api.md.
Operate
curl -X PUT -u x:$GIT_TOKEN -d '{"description":"...","section":"tools","private":false}' \
https://<host>/myrepo/config # describe and place a repository
curl -X POST -u x:$GIT_TOKEN https://<host>/myrepo/gc # prune unreachable objects
curl -X DELETE -u x:$GIT_TOKEN https://<host>/myrepo # delete a repository
Garbage collection also runs by itself, from a Durable Object alarm,
after a forced update or a ref deletion. GIT_TOKENS holds additional
comma-separated tokens; MAX_PUSH_MB caps a single push. Setting
SHA1DC=1 screens every pushed object for a SHA-1 collision attack on
ingest; by default objects are hashed with native SHA-1 — the same
object ids, without the check.
Contributions
Pull requests are disabled. Send a git format-patch attachment to me@littledivy.com.
Frequently asked about dgit
What is dgit?+
dgit is a self-hosted GitHub/GitLab alternative built on the Cloudflare developer platform. Host Git repositories and browse their history on Workers and SQLite Durable Objects.
What does dgit replace?+
dgit is listed as an alternative to GitHub, GitLab. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does dgit use?+
dgit is built on Durable Objects, R2, Workers.
How much does dgit cost to run?+
dgit has a documented low-volume paid Cloudflare path beginning with Workers Paid ($5 USD/account/month), with separately metered usage. The supplied high CPU limit requires Paid; repository sizes and active Durable Object duration drive costs. Use Workers Paid for the configured 300,000 ms CPU limit; $5 USD/account/month is a baseline, not a fixed all-inclusive price. Workers Paid starts at $5 USD/account/month and includes 10 million requests/month plus 30 million CPU milliseconds/month; higher CPU limits do not make execution unmetered. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Paid SQLite Durable Objects include 1 million requests/month, 400,000 GB-s duration/month and 5 GB-month SQL storage; active duration and excess database operations are metered. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is dgit open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/littledivy/durable-git/d5eba04eda584b855a1bde1f86a836b751c378c0/LICENSE. Source code and contributor credit are available at https://github.com/littledivy/durable-git.


Discussion · 0
sign in to comment →