aboutsummaryrefslogtreecommitdiffstats
authorDivy Srivastava <me@littledivy.com>2026-08-19 16:26:56 +0530
committerDivy Srivastava <me@littledivy.com>2026-08-19 16:26:56 +0530
commit17b03631eb6676f3b2c86b6662d9af4aa9237bef (patch)
tree1638b45809cd4436b2fe94bc9cd182ee29341bc5
parent4fffc3a9fd6d04293af3494faf44c85d523a4dc4 (diff)
downloaddgit-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 &mdash; create one by pushing: <code>git push https://&lt;this-host&gt;/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;