Skip to content
File

Blob: src/worker/git/operations/read/commits.ts

typescript175 lines
1import type { CacheContext } from "@/worker/cache";
2import type { CommitInfo, MergeSideOptions } from "./types";
3import { parseCommitText } from "@/worker/git/core/commitParse";
4import { readLooseObjectRaw } from "./objects";
5import { resolveRef } from "./refs";
6import { MAX_SIMULTANEOUS_CONNECTIONS } from "../limits";
7import { createLogger, BinaryHeap } from "@/worker/common";
8 
9export async function readCommit(
10 env: Env,
11 repoId: string,
12 oid: string,
13 cacheCtx?: CacheContext
14): Promise<{ tree: string; parents: string[]; message: string }> {
15 const obj = await readLooseObjectRaw(env, repoId, oid, cacheCtx);
16 if (!obj || obj.type !== "commit") throw new Error("Not a commit");
17 const text = new TextDecoder().decode(obj.payload);
18 const parsed = parseCommitText(text);
19 return { tree: parsed.tree, parents: parsed.parents, message: parsed.message };
20}
21 
22export async function readCommitInfo(
23 env: Env,
24 repoId: string,
25 oid: string,
26 cacheCtx?: CacheContext
27): Promise<CommitInfo> {
28 const obj = await readLooseObjectRaw(env, repoId, oid, cacheCtx);
29 if (!obj || obj.type !== "commit") throw new Error("Not a commit");
30 const text = new TextDecoder().decode(obj.payload);
31 const parsed = parseCommitText(text);
32 const { tree, parents, author, committer, message } = parsed;
33 return { oid, tree, parents, author, committer, message };
34}
35 
36export async function listCommitsFirstParentRange(
37 env: Env,
38 repoId: string,
39 start: string,
40 offset: number,
41 limit: number,
42 cacheCtx?: CacheContext
43): Promise<CommitInfo[]> {
44 let oid = await resolveRef(env, repoId, start);
45 if (!oid && /^[0-9a-f]{40}$/i.test(start)) oid = start.toLowerCase();
46 // Peel annotated tag
47 if (oid) {
48 const obj = await readLooseObjectRaw(env, repoId, oid, cacheCtx);
49 if (obj && obj.type === "tag") {
50 const text = new TextDecoder().decode(obj.payload);
51 const m = text.match(/^object ([0-9a-f]{40})/m);
52 if (m) oid = m[1];
53 }
54 }
55 if (!oid) throw new Error("Ref not found");
56 const seen = new Set<string>();
57 
58 const targetOids: string[] = [];
59 let index = 0;
60 while (oid && !seen.has(oid) && targetOids.length < limit) {
61 seen.add(oid);
62 if (index >= offset) {
63 targetOids.push(oid);
64 }
65 const c = await readCommit(env, repoId, oid, cacheCtx);
66 index++;
67 oid = c.parents[0];
68 }
69 if (targetOids.length === 0) return [];
70 
71 const out: CommitInfo[] = [];
72 const CONCURRENCY = Math.max(1, Math.min(MAX_SIMULTANEOUS_CONNECTIONS, 6));
73 for (let i = 0; i < targetOids.length; i += CONCURRENCY) {
74 const batch = targetOids.slice(i, i + CONCURRENCY);
75 const infos = await Promise.all(batch.map((q) => readCommitInfo(env, repoId, q, cacheCtx)));
76 out.push(...infos);
77 }
78 return out;
79}
80 
81export async function listMergeSideFirstParent(
82 env: Env,
83 repoId: string,
84 mergeOid: string,
85 limit = 20,
86 options: MergeSideOptions = {},
87 cacheCtx?: CacheContext
88): Promise<CommitInfo[]> {
89 const logger = createLogger(env.LOG_LEVEL, { service: "listMergeSideFirstParent", repoId });
90 const scanLimit = Math.min(400, Math.max(limit * 3, options.scanLimit ?? 120));
91 const timeBudgetMs = Math.max(50, Math.min(10000, options.timeBudgetMs ?? 150));
92 const mainlineProbe = Math.min(1000, Math.max(50, options.mainlineProbe ?? 100));
93 const started = Date.now();
94 
95 const merge = await readCommitInfo(env, repoId, mergeOid, cacheCtx);
96 const parents = merge.parents || [];
97 if (parents.length < 2) return [];
98 
99 const mainlineSet = new Set<string>();
100 try {
101 let cur: string | undefined = parents[0];
102 let seen = 0;
103 const visited = new Set<string>();
104 const probeStarted = Date.now();
105 const probeTimeBudget = Math.min(1500, timeBudgetMs / 3);
106 
107 while (
108 cur &&
109 seen < mainlineProbe &&
110 !visited.has(cur) &&
111 Date.now() - probeStarted < probeTimeBudget
112 ) {
113 visited.add(cur);
114 mainlineSet.add(cur);
115 const info = await readCommitInfo(env, repoId, cur, cacheCtx);
116 cur = info.parents?.[0];
117 seen++;
118 }
119 
120 logger.info("Mainline probe completed", {
121 commits: seen,
122 timeMs: Date.now() - probeStarted,
123 mergeOid,
124 });
125 } catch {}
126 
127 const newerFirst = (a: CommitInfo, b: CommitInfo) => {
128 const aw = a.author?.when ?? 0;
129 const bw = b.author?.when ?? 0;
130 if (aw !== bw) return bw - aw;
131 return b.oid.localeCompare(a.oid);
132 };
133 
134 const visited = new Set<string>();
135 const frontier: CommitInfo[] = [];
136 for (let i = 1; i < parents.length; i++) {
137 const p = parents[i];
138 try {
139 const info = await readCommitInfo(env, repoId, p, cacheCtx);
140 frontier.push(info);
141 } catch {}
142 }
143 const heap = new BinaryHeap<CommitInfo>(newerFirst, frontier);
144 
145 const out: CommitInfo[] = [];
146 let scanned = 0;
147 
148 while (
149 out.length < limit &&
150 !heap.isEmpty() &&
151 scanned < scanLimit &&
152 Date.now() - started < timeBudgetMs
153 ) {
154 const current = heap.pop()!;
155 scanned++;
156 if (visited.has(current.oid)) continue;
157 visited.add(current.oid);
158 
159 if (mainlineSet.has(current.oid)) continue;
160 
161 out.push(current);
162 if (out.length >= limit) break;
163 
164 const next = current.parents?.[0];
165 if (next && !visited.has(next)) {
166 try {
167 const ni = await readCommitInfo(env, repoId, next, cacheCtx);
168 heap.push(ni);
169 } catch {}
170 }
171 }
172 
173 return out;
174}