Skip to content
File

Blob: src/worker/do/repo/refs.ts

typescript106 lines
1/**
2 * Git refs and HEAD management
3 *
4 * This module handles Git references (branches, tags) and HEAD state,
5 * including resolution and updates with consistency guarantees.
6 */
7 
8import type { RepoStateSchema, Head, TypedStorage } from "./repoState";
9 
10import { asTypedStorage } from "./repoState";
11 
12/**
13 * Retrieves all refs from storage
14 * @param ctx - Durable Object state context
15 * @returns Array of ref objects with name and oid, or empty array if none exist
16 */
17export async function getRefs(ctx: DurableObjectState): Promise<{ name: string; oid: string }[]> {
18 const store = asTypedStorage<RepoStateSchema>(ctx.storage);
19 return (await store.get("refs")) ?? [];
20}
21 
22/**
23 * Updates refs in storage
24 * @param ctx - Durable Object state context
25 * @param refs - New refs array to store
26 */
27export async function setRefs(
28 ctx: DurableObjectState,
29 refs: { name: string; oid: string }[]
30): Promise<void> {
31 const store = asTypedStorage<RepoStateSchema>(ctx.storage);
32 await store.put("refs", refs);
33}
34 
35/**
36 * Resolves the current HEAD state by looking up the target ref
37 * @param ctx - Durable Object state context
38 * @returns The resolved HEAD object with target and either oid or unborn flag
39 */
40export async function resolveHead(ctx: DurableObjectState): Promise<Head> {
41 const store = asTypedStorage<RepoStateSchema>(ctx.storage);
42 const stored = await store.get("head");
43 const refs = await getRefs(ctx);
44 
45 // Determine target (default to main)
46 const target = stored?.target || "refs/heads/main";
47 const match = refs.find((r) => r.name === target);
48 const resolved = match
49 ? ({ target, oid: match.oid } as Head)
50 : ({ target, unborn: true } as Head);
51 
52 // Persist resolved head only if it changed
53 await updateHeadIfChanged(store, stored, resolved);
54 
55 return resolved;
56}
57 
58/**
59 * Sets HEAD to a new value
60 * @param ctx - Durable Object state context
61 * @param head - New HEAD value
62 */
63export async function setHead(ctx: DurableObjectState, head: Head): Promise<void> {
64 const store = asTypedStorage<RepoStateSchema>(ctx.storage);
65 await store.put("head", head);
66}
67 
68/**
69 * Get HEAD and refs in a single operation
70 * @param ctx - Durable Object state context
71 * @returns Object containing HEAD and refs
72 */
73export async function getHeadAndRefs(
74 ctx: DurableObjectState
75): Promise<{ head: Head; refs: { name: string; oid: string }[] }> {
76 const [head, refs] = await Promise.all([resolveHead(ctx), getRefs(ctx)]);
77 return { head, refs };
78}
79 
80/**
81 * Updates HEAD in storage only if the resolved value differs semantically
82 * Handles normalization of legacy HEAD shapes (e.g., both oid and unborn present)
83 * @param store - The typed storage instance
84 * @param stored - The currently stored HEAD value
85 * @param resolved - The newly resolved HEAD value
86 */
87async function updateHeadIfChanged(
88 store: TypedStorage<RepoStateSchema>,
89 stored: Head | undefined,
90 resolved: Head
91): Promise<void> {
92 try {
93 const storedOid = stored?.oid ?? undefined;
94 const resolvedOid = resolved.oid ?? undefined;
95 const sameTarget = !!stored && stored.target === resolved.target;
96 const sameOid = storedOid === resolvedOid;
97 const sameUnborn =
98 storedOid || resolvedOid ? true : (stored?.unborn === true) === (resolved.unborn === true);
99 const same = !!stored && sameTarget && sameOid && sameUnborn;
100 
101 if (!same) {
102 await store.put("head", resolved);
103 }
104 } catch {}
105}