| author | Divy Srivastava <me@littledivy.com> | 2026-08-19 16:26:56 +0530 |
|---|---|---|
| committer | Divy Srivastava <me@littledivy.com> | 2026-08-19 16:26:56 +0530 |
| commit | 17b03631eb6676f3b2c86b6662d9af4aa9237bef (patch) | |
| tree | 1638b45809cd4436b2fe94bc9cd182ee29341bc5 | |
| parent | 4fffc3a9fd6d04293af3494faf44c85d523a4dc4 (diff) | |
| download | dgit-17b03631eb.tar.gz zip | |
v0.0.3
Diffstat
| scripts/e2e.sh | +13 | -3 |
| src/git/protocol.ts | +27 | -0 |
| src/index.ts | +22 | -17 |
| src/registry.ts | +5 | -5 |
| src/repo.ts | +35 | -24 |
| src/ui/markdown.ts | +0 | -0 |
| src/ui/style.ts | +21 | -4 |
7 files changed, 123 insertions(+), 53 deletions(-)
diff --git a/scripts/e2e.sh b/scripts/e2e.sh @@ -132,10 +132,20 @@ blob=$(cd src && git rev-parse "HEAD:lib/math.js") curl -s "$BASE/demo/blob/?id=$blob" | grep -q "function add"; check $? 0 "blob by id" -echo "== 11. config: description/section/owner + index sections ==" +echo "== 11. config: description/section/owner + registry idle from commit date ==" curl -s -X PUT -u "x:$TOKEN" -d '{"description":"a lovely demo","section":"experiments","owner":"divy"}' "$BASE/demo/config" > /dev/null -curl -s "$BASE/" | grep -q "a lovely demo"; check $? 0 "index shows description" -curl -s "$BASE/" | grep -q "reposection.*experiments\|experiments"; check $? 0 "index shows section" +# a repo whose only commit is dated 2010: its index "Updated" age must track that +# commit's committer date, not the push time (idle = latest commit time) +curl -s -o /dev/null -X DELETE -u "x:$TOKEN" "$BASE/old-date-repo" +git init -q -b main olddate && (cd olddate && git config user.email o@o && git config user.name o \ + && echo old > o.txt && git add -A \ + && GIT_AUTHOR_DATE="2010-06-15T12:00:00" GIT_COMMITTER_DATE="2010-06-15T12:00:00" git commit -qm "ancient commit" \ + && $GITC push -q "$AUTH_BASE/old-date-repo.git" main) +idx=$(curl -s "$BASE/") +echo "$idx" | grep -q "a lovely demo"; check $? 0 "index shows description" +row=$(echo "$idx" | perl -pe 's{</tr>}{</tr>\n}g' | grep "old-date-repo") +echo " old-date-repo index row: $row" +echo "$row" | grep -q "year"; check $? 0 "old-date-repo age reflects 2010 commit, not push time" echo "== 12. private repos ==" git init -q -b main priv && (cd priv && git config user.email t@t && git config user.name t && echo secret > s.txt && git add -A && git commit -qm secret && $GITC push -q "$AUTH_BASE/private-repo.git" main) diff --git a/src/git/protocol.ts b/src/git/protocol.ts @@ -228,6 +228,33 @@ } /** + * Newest committer time (ms) across every current ref tip and HEAD: each tip is + * peeled through annotated tags to its commit and its committer time taken; + * non-commit tips are ignored, parse errors skipped. Falls back to Date.now() + * when the repo holds no commits. Committer time is unix seconds, so ×1000. + */ +export async function latestCommitTime(store: GitStore): Promise<number> { + const tips = new Set<string>(); + const head = store.resolveHead(); + if (head) tips.add(head); + for (const r of store.refs()) tips.add(r.target); + let max = 0; + for (const tip of tips) { + try { + const oid = await peelToCommitOid(store, tip); + if (!oid) continue; + const obj = await store.get(oid); + if (obj?.type !== "commit") continue; + const t = parseCommit(obj.data).committer.time; + if (t > max) max = t; + } catch { + // broken/unparseable tip: skip it, never fail the push/config + } + } + return max > 0 ? max * 1000 : Date.now(); +} + +/** * Depth-limited commit set from the wants (BFS, min depth wins; the tip is * depth 1, like git). Boundary commits are included but their parents cut. */ diff --git a/src/index.ts b/src/index.ts @@ -136,14 +136,7 @@ const registry = env.REGISTRY.getByName("registry"); const repos = await registry.list(); let rows = ""; - let lastSection: string | null = null; for (const r of repos) { - if (r.section !== lastSection) { - if (r.section) { - rows += `<tr class='nohover'><td class='reposection' colspan='4'>${esc(r.section)}</td></tr>`; - } - lastSection = r.section; - } rows += `<tr><td><a href='/${encodeURIComponent(r.name)}/'>${esc(r.name)}</a></td>` + `<td>${esc(r.desc || "[no description]")}</td>` + @@ -152,7 +145,7 @@ } const body = ` <table class='list nowrap'> -<tr class='nohover'><th class='left'>Name</th><th class='left'>Description</th><th class='left'>Owner</th><th class='left'>Idle</th></tr> +<tr class='nohover'><th class='left'>Name</th><th class='left'>Description</th><th class='left'>Owner</th><th class='left'>Updated</th></tr> ${rows || "<tr class='nohover'><td colspan='4'>no repositories yet — create one by pushing: <code>git push https://<this-host>/myrepo.git main</code></td></tr>"} </table>`; return htmlResponse(layout({ ...siteBase(env), body })); @@ -376,9 +369,16 @@ fwd.headers.set("x-proto", url.protocol.replace(":", "")); const res = await stub.fetch(fwd); - // bookkeeping after successful mutations + // bookkeeping after successful mutations. idle is the newest committer date + // (x-commit-time), computed by the DO which alone can read the object graph if (res.ok && sub === "/git-receive-pack" && res.headers.get("x-changed") === "1") { - await registry.upsert(repo, Date.now()); + const idle = parseInt(res.headers.get("x-commit-time") ?? "", 10) || Date.now(); + try { + // a registry hiccup must not fail an already-applied push; next push heals + await registry.upsert(repo, idle); + } catch { + // registry deferred + } } if (res.ok && (sub === "/config" || sub === "/description") && req.method === "PUT") { try { @@ -388,13 +388,18 @@ section: string; private: boolean; }; - if (!info) await registry.upsert(repo, Date.now()); - await registry.setConfig(repo, { - desc: cfg.description, - owner: cfg.owner, - section: cfg.section, - priv: cfg.private, - }); + const idle = parseInt(res.headers.get("x-commit-time") ?? "", 10) || Date.now(); + if (!info) await registry.upsert(repo, idle); + await registry.setConfig( + repo, + { + desc: cfg.description, + owner: cfg.owner, + section: cfg.section, + priv: cfg.private, + }, + idle + ); } catch { // non-JSON response; skip registry sync } diff --git a/src/registry.ts b/src/registry.ts @@ -7,7 +7,7 @@ owner: string; section: string; priv: number; // 1 = requires auth for all access, hidden from index - /** unix millis of last push */ + /** unix millis of the newest commit; the index sort key */ idle: number; /** bumped on every push/config change; used as the page-cache version */ ver: number; @@ -34,7 +34,6 @@ idle INTEGER NOT NULL DEFAULT 0, ver INTEGER NOT NULL DEFAULT 1 ); - CREATE INDEX IF NOT EXISTS idx_repos_section_name ON repos (section, name); `); // upgrade path for databases created by earlier versions for (const col of ["section TEXT NOT NULL DEFAULT ''", "priv INTEGER NOT NULL DEFAULT 0", "ver INTEGER NOT NULL DEFAULT 1"]) { @@ -57,15 +56,16 @@ ); } - setConfig(name: string, cfg: RepoConfig): void { + setConfig(name: string, cfg: RepoConfig, idle?: number): void { const cur = this.get(name); if (!cur) return; this.ctx.storage.sql.exec( - "UPDATE repos SET desc = ?, owner = ?, section = ?, priv = ?, ver = ver + 1 WHERE name = ?", + "UPDATE repos SET desc = ?, owner = ?, section = ?, priv = ?, idle = ?, ver = ver + 1 WHERE name = ?", cfg.desc ?? cur.desc, cfg.owner ?? cur.owner, cfg.section ?? cur.section, cfg.priv === undefined ? cur.priv : cfg.priv ? 1 : 0, + idle ?? cur.idle, name ); } @@ -85,7 +85,7 @@ list(limit = 1000): RepoInfo[] { return this.ctx.storage.sql .exec<RepoInfo>( - "SELECT name, desc, owner, section, priv, idle, ver FROM repos WHERE priv = 0 ORDER BY section, name LIMIT ?", + "SELECT name, desc, owner, section, priv, idle, ver FROM repos WHERE priv = 0 ORDER BY idle DESC LIMIT ?", limit ) .toArray(); diff --git a/src/repo.ts b/src/repo.ts @@ -14,6 +14,7 @@ renderStatus, sidebandFrames, wantsAreAllTips, + latestCommitTime, Service, PackCache, } from "./git/protocol"; @@ -246,7 +247,7 @@ return run.then(() => result); } - private handleConfig(body: string): Response { + private async handleConfig(body: string): Promise<Response> { let cfg: Record<string, unknown>; try { cfg = JSON.parse(body); @@ -257,12 +258,18 @@ if (typeof cfg.owner === "string") this.store.setMeta("owner", cfg.owner.slice(0, 100)); if (typeof cfg.section === "string") this.store.setMeta("section", cfg.section.slice(0, 100)); if (typeof cfg.private === "boolean") this.store.setMeta("private", cfg.private ? "1" : "0"); - return Response.json({ - description: this.store.getMeta("description") ?? "", - owner: this.store.getMeta("owner") ?? "", - section: this.store.getMeta("section") ?? "", - private: this.store.getMeta("private") === "1", - }); + // backfill the registry sort key: re-PUTing config recomputes idle from the + // real latest-commit date, so a mirrored repo's index age stops tracking + // its push time. The Worker reads this and threads it into the registry. + return Response.json( + { + description: this.store.getMeta("description") ?? "", + owner: this.store.getMeta("owner") ?? "", + section: this.store.getMeta("section") ?? "", + private: this.store.getMeta("private") === "1", + }, + { headers: { "x-commit-time": String(await latestCommitTime(this.store)) } } + ); } /** @@ -442,8 +449,9 @@ () => {}, () => {} ); + let commitTime: number | null; try { - await run; + commitTime = await run; } catch (err) { console.log(`[receive ${repo}] FAILED: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); const msg = err instanceof Error ? err.message : String(err); @@ -450,12 +458,17 @@ if (msg.includes("exceeds maximum size")) return tooLargeResponse(maxBytes); return new Response(`error: ${msg}\n`, { status: 500 }); } - return new Response(concat(chunks) as unknown as BodyInit, { - headers: { - "content-type": "application/x-git-receive-pack-result", - "cache-control": "no-cache", - }, - }); + const headers: Record<string, string> = { + "content-type": "application/x-git-receive-pack-result", + "cache-control": "no-cache", + }; + // a real ref change: hand the Worker the newest committer date so it can set + // the registry sort key (x-changed gates the upsert, x-commit-time is the ms value) + if (commitTime !== null) { + headers["x-changed"] = "1"; + headers["x-commit-time"] = String(commitTime); + } + return new Response(concat(chunks) as unknown as BodyInit, { headers }); } private async processReceive( @@ -463,7 +476,7 @@ repo: string, maxBytes: number, emit: (chunk: Uint8Array) => void - ): Promise<void> { + ): Promise<number | null> { console.log(`[receive ${repo}] processing push request`); let reader: ReadableStreamDefaultReader<Uint8Array> | null = null; let buf: Uint8Array; @@ -548,17 +561,14 @@ const { results, changed, needsGc } = this.ctx.storage.transactionSync(() => commitPush(this.store, plan) ); + let commitTime: number | null = null; if (changed) { this.store.setMeta("created", "1"); this.store.setMeta("last-push", String(Date.now())); - try { - // after a huge ingest, celld's output gate can refuse outbound calls - // until the burst is proven durable — registration must not take the - // whole (already applied) push down with it; the next push heals it - await this.env.REGISTRY.getByName("registry").upsert(repo, Date.now()); - } catch (err) { - console.log(`[receive ${repo}] registry upsert deferred: ${err instanceof Error ? err.message : String(err)}`); - } + // the registry sort key is the newest committer date, not push time; the + // Worker reads this off the response (x-changed / x-commit-time) and does + // the registry upsert, so a registry hiccup never fails the applied push + commitTime = await latestCommitTime(this.store); } if (needsGc) { this.store.setMeta("gc-pending", "1"); @@ -569,6 +579,7 @@ } } if (wantStatus) emit(renderStatus(results, unpackError, sideband)); + return commitTime; } @@ -1005,7 +1016,7 @@ const lower = readme.name.toLowerCase(); const body = lower.endsWith(".md") || lower.endsWith(".markdown") - ? `<div class='md'>${renderMarkdown(text)}</div>` + ? `<div class='md'>${renderMarkdown(text, { repo, ref: h ?? base.ref })}</div>` : `<pre>${esc(text)}</pre>`; return htmlResponse(layout({ ...base, body })); } diff --git a/src/ui/markdown.ts b/src/ui/markdown.ts Binary files differ diff --git a/src/ui/style.ts b/src/ui/style.ts @@ -299,10 +299,6 @@ div#cgit .right { text-align: right; } -div#cgit table.list td.reposection { - font-style: italic; - color: #888; -} div#cgit a.branch-deco { color: #000; margin: 0px 0.5em; @@ -398,6 +394,27 @@ padding-left: 1em; color: #666; } +div#cgit div.md img { + max-width: 100%; +} +div#cgit div.md table { + border-collapse: collapse; + margin: 0.5em 0; +} +div#cgit div.md table th, div#cgit div.md table td { + border: solid 1px #ccc; + padding: 0.3em 0.6em; +} +div#cgit div.md table th { + background: #f4f4f4; +} +div#cgit div.md li.task { + list-style: none; + margin-left: -1.2em; +} +div#cgit div.md li.task input { + margin-right: 0.4em; +} div#cgit table.stats th { text-align: left; padding: 0.1em 0.5em; |