Public: anyone can read this; the crew can change it.
A fork's files page reads its drift from the parent under 3 ms when neither tip has moved #6317
Check · Review · Land · Live — Changes were asked on this head.
scout/forge:scout/6146-a-fork-s-files-page-reads-its-drift-from → main 6 commits fork
2 files
changed +77
−8
src/actions/forks.ts +21 −8
| @@ -25,6 +25,7 @@ import { divergence } from "../git/branches.ts"; | ||
| 25 | 25 | import { runGit } from "../git/env.ts"; |
| 26 | 26 | import { isSafeRef } from "../git/guard.ts"; |
| 27 | 27 | import { resolveOid } from "../git/merge.ts"; |
| 28 | import { readTips } from "../git/tips.ts"; | |
| 28 | 29 | import { repoDir } from "../git/repos.ts"; |
| 29 | 30 | import { err, type Result } from "../kernel/result.ts"; |
| 30 | 31 | import { requireWriter, type AccessError } from "./access.ts"; |
| @@ -97,14 +98,23 @@ export async function forkDrift( | ||
| 97 | 98 | ): Promise<ForkDrift> { |
| 98 | 99 | const forkDir = repoDir(app.reposRoot, fork.ownerName, fork.name); |
| 99 | 100 | const parentDir = repoDir(app.reposRoot, parent.ownerName, parent.name); |
| 100 | const head = await resolveOid(forkDir, `refs/heads/${fork.defaultBranch}`); | |
| 101 | const base = await resolveOid( | |
| 102 | parentDir, | |
| 103 | `refs/heads/${parent.defaultBranch}`, | |
| 104 | ); | |
| 101 | const headRef = `refs/heads/${fork.defaultBranch}`; | |
| 102 | const baseRef = `refs/heads/${parent.defaultBranch}`; | |
| 103 | const head = | |
| 104 | (await readTips(forkDir, [headRef])).get(headRef) ?? | |
| 105 | (await resolveOid(forkDir, headRef)); | |
| 106 | const base = | |
| 107 | (await readTips(parentDir, [baseRef])).get(baseRef) ?? | |
| 108 | (await resolveOid(parentDir, baseRef)); | |
| 105 | 109 | if (head === undefined || base === undefined) |
| 106 | 110 | return { ahead: null, behind: null }; |
| 107 | return divergence(forkDir, base, head); | |
| 111 | const key = `${base}:${head}`; | |
| 112 | let drift = standing.get(key); | |
| 113 | if (drift === undefined) { | |
| 114 | drift = await divergence(forkDir, base, head); | |
| 115 | remember(key, drift); | |
| 116 | } | |
| 117 | return drift; | |
| 108 | 118 | } |
| 109 | 119 | |
| 110 | 120 | // ---- ranking ---- |
| @@ -157,6 +167,10 @@ export const nothingNewHeading = (n: number): string => | ||
| 157 | 167 | */ |
| 158 | 168 | const standing = new Map<string, ForkDrift>(); |
| 159 | 169 | const STANDING_MAX = 2_000; |
| 170 | const remember = (key: string, drift: ForkDrift): void => { | |
| 171 | if (standing.size >= STANDING_MAX) standing.clear(); | |
| 172 | standing.set(key, drift); | |
| 173 | }; | |
| 160 | 174 | |
| 161 | 175 | async function tipAndTime( |
| 162 | 176 | dir: string, |
| @@ -194,8 +208,7 @@ export async function driftOfForks( | ||
| 194 | 208 | let drift = standing.get(key); |
| 195 | 209 | if (drift === undefined) { |
| 196 | 210 | drift = await divergence(dir, base, tip.oid); |
| 197 | if (standing.size >= STANDING_MAX) standing.clear(); | |
| 198 | standing.set(key, drift); | |
| 211 | remember(key, drift); | |
| 199 | 212 | } |
| 200 | 213 | return { fork, drift, pushedAt: tip.pushedAt }; |
| 201 | 214 | }), |
test/fork-drift-cache.test.ts added +56
| @@ -0,0 +1,56 @@ | ||
| 1 | import { afterAll, beforeAll, expect, spyOn, test } from "bun:test"; | |
| 2 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 3 | import { tmpdir } from "node:os"; | |
| 4 | import { join } from "node:path"; | |
| 5 | import { commitToBranch, createRepo, forkRepo } from "../src/actions/repos.ts"; | |
| 6 | import { forkDrift } from "../src/actions/forks.ts"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { createUser, type User } from "../src/core/users.ts"; | |
| 9 | import type { Repo } from "../src/core/repos.ts"; | |
| 10 | import { promote } from "./helpers/members.ts"; | |
| 11 | ||
| 12 | const must = <T>(r: { ok: true; value: T } | { ok: false; error: unknown }): T => { | |
| 13 | if (!r.ok) throw new Error(JSON.stringify(r.error)); | |
| 14 | return r.value; | |
| 15 | }; | |
| 16 | ||
| 17 | let dataDir = ""; | |
| 18 | let app: App; | |
| 19 | let alice: User; | |
| 20 | let upstream: Repo; | |
| 21 | let fork: Repo; | |
| 22 | ||
| 23 | const commit = async (who: User, repo: Repo, name: string) => | |
| 24 | must(await commitToBranch(app, who, repo, { message: name, files: [{ path: `${name}.txt`, content: "x\n" }] })); | |
| 25 | ||
| 26 | beforeAll(async () => { | |
| 27 | dataDir = await mkdtemp(join(tmpdir(), "forge-drift-cache-")); | |
| 28 | app = createApp({ dataDir, port: 0 }); | |
| 29 | alice = must(await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true })); | |
| 30 | promote(app.db, alice); | |
| 31 | const bob = must(await createUser(app.db, { origin: "seed", trusted: true, username: "bob", email: "[email protected]", password: "a-good-passphrase" })); | |
| 32 | promote(app.db, bob); | |
| 33 | upstream = must(await createRepo(app, alice, { name: "app", visibility: "public" })); | |
| 34 | await commit(alice, upstream, "one"); | |
| 35 | fork = must(await forkRepo(app, bob, upstream)); | |
| 36 | await commit(bob, fork, "mine"); | |
| 37 | }, 60_000); | |
| 38 | ||
| 39 | afterAll(async () => { | |
| 40 | app.close(); | |
| 41 | await rm(dataDir, { recursive: true, force: true }); | |
| 42 | }); | |
| 43 | ||
| 44 | test("a warm read spawns no git; a parent push changes behind", async () => { | |
| 45 | expect(await forkDrift(app, fork, upstream)).toEqual({ ahead: 1, behind: 0 }); | |
| 46 | const spawn = spyOn(Bun, "spawn"); | |
| 47 | try { | |
| 48 | const warm = await forkDrift(app, fork, upstream); | |
| 49 | expect(warm).toEqual({ ahead: 1, behind: 0 }); | |
| 50 | expect(spawn).not.toHaveBeenCalled(); | |
| 51 | } finally { | |
| 52 | spawn.mockRestore(); | |
| 53 | } | |
| 54 | await commit(alice, upstream, "two"); | |
| 55 | expect(await forkDrift(app, fork, upstream)).toEqual({ ahead: 1, behind: 1 }); | |
| 56 | }); |