Skip to content
File

Blob: src/worker/git/object-store/candidates.ts

typescript162 lines
1import type { IdxView } from "./types";
2 
3import { bytesToHex, hexToBytes } from "@/worker/common/hex";
4import { findOidIndexFromBytes, getNextOffsetByIndex, getOidHexAt } from "./idxView";
5 
6export type IndexedPackSource = {
7 packKey: string;
8 packBytes: number;
9 idx: IdxView;
10};
11 
12export type PackedObjectCandidate = {
13 source: IndexedPackSource;
14 packSlot: number;
15 objectIndex: number;
16 offset: number;
17 nextOffset: number;
18 oid: string;
19};
20 
21export type OidRun = {
22 /** Inclusive first `.idx` row whose OID matches the lookup OID. */
23 startIndex: number;
24 /** Inclusive final `.idx` row whose OID matches the lookup OID. */
25 endIndex: number;
26};
27 
28export type CandidateCollectionOptions = {
29 excludePackKey?: string;
30};
31 
32type NormalizedOid = {
33 oidHex: string;
34 oidBytes: Uint8Array;
35};
36 
37function normalizeOid(oid: string | Uint8Array): NormalizedOid | undefined {
38 if (typeof oid === "string") {
39 const oidHex = oid.toLowerCase();
40 if (oidHex.length !== 40) return undefined;
41 return { oidHex, oidBytes: hexToBytes(oidHex) };
42 }
43 
44 if (oid.byteLength !== 20) return undefined;
45 return { oidHex: bytesToHex(oid), oidBytes: oid };
46}
47 
48function oidAtIndexMatches(rawNames: Uint8Array, index: number, oidBytes: Uint8Array): boolean {
49 const rawStart = index * 20;
50 for (let offset = 0; offset < 20; offset++) {
51 if (rawNames[rawStart + offset] !== oidBytes[offset]) return false;
52 }
53 return true;
54}
55 
56function candidateFromIndex(
57 source: IndexedPackSource,
58 packSlot: number,
59 objectIndex: number,
60 oidHex: string
61): PackedObjectCandidate | undefined {
62 const nextOffset = getNextOffsetByIndex(source.idx, objectIndex);
63 if (nextOffset === undefined) return undefined;
64 
65 return {
66 source,
67 packSlot,
68 objectIndex,
69 offset: source.idx.offsets[objectIndex],
70 nextOffset,
71 oid: oidHex,
72 };
73}
74 
75/**
76 * Locate the full contiguous duplicate-OID run in an idx view.
77 *
78 * Git idx files are sorted by object ID, so duplicate rows for the same OID
79 * are adjacent. The binary-search hit can land anywhere inside that run; this
80 * helper expands both directions so callers do not accidentally inspect only
81 * one arbitrary duplicate.
82 */
83export function findOidRunInIdx(idx: IdxView, oid: string | Uint8Array): OidRun | undefined {
84 const normalized = normalizeOid(oid);
85 if (!normalized) return undefined;
86 
87 const hitIndex = findOidIndexFromBytes(idx, normalized.oidBytes);
88 if (hitIndex < 0) return undefined;
89 
90 let startIndex = hitIndex;
91 while (startIndex > 0 && oidAtIndexMatches(idx.rawNames, startIndex - 1, normalized.oidBytes)) {
92 startIndex--;
93 }
94 
95 let endIndex = hitIndex;
96 while (
97 endIndex + 1 < idx.count &&
98 oidAtIndexMatches(idx.rawNames, endIndex + 1, normalized.oidBytes)
99 ) {
100 endIndex++;
101 }
102 
103 return { startIndex, endIndex };
104}
105 
106/**
107 * Enumerate every packed-object candidate for an OID in snapshot order.
108 *
109 * Within a pack this returns the entire duplicate run in idx row order. The
110 * caller owns higher-level policy such as whether to accept the first material
111 * object, prefer full objects, or skip a target pack during refs-only backfill.
112 */
113export function collectPackedObjectCandidates(
114 sources: readonly IndexedPackSource[],
115 oid: string | Uint8Array,
116 options: CandidateCollectionOptions = {}
117): PackedObjectCandidate[] {
118 const normalized = normalizeOid(oid);
119 if (!normalized) return [];
120 
121 const candidates: PackedObjectCandidate[] = [];
122 for (let packSlot = 0; packSlot < sources.length; packSlot++) {
123 const source = sources[packSlot]!;
124 if (source.packKey === options.excludePackKey) continue;
125 
126 const run = findOidRunInIdx(source.idx, normalized.oidBytes);
127 if (!run) continue;
128 
129 for (let objectIndex = run.startIndex; objectIndex <= run.endIndex; objectIndex++) {
130 const candidate = candidateFromIndex(source, packSlot, objectIndex, normalized.oidHex);
131 if (candidate) candidates.push(candidate);
132 }
133 }
134 
135 return candidates;
136}
137 
138/**
139 * Find the first snapshot pack that contains an OID, preserving the existing
140 * binary-search hit semantics inside that pack.
141 */
142export function findFirstPackedObjectCandidate(
143 sources: readonly IndexedPackSource[],
144 oid: string | Uint8Array,
145 options: CandidateCollectionOptions = {}
146): PackedObjectCandidate | undefined {
147 const normalized = normalizeOid(oid);
148 if (!normalized) return undefined;
149 
150 for (let packSlot = 0; packSlot < sources.length; packSlot++) {
151 const source = sources[packSlot]!;
152 if (source.packKey === options.excludePackKey) continue;
153 
154 const objectIndex = findOidIndexFromBytes(source.idx, normalized.oidBytes);
155 if (objectIndex < 0) continue;
156 
157 return candidateFromIndex(source, packSlot, objectIndex, getOidHexAt(source.idx, objectIndex));
158 }
159 
160 return undefined;
161}