Created
February 1, 2026 03:17
-
-
Save souporserious/2202ea8ae91a1063e7df2d74451ba029 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class GitBatchCat { | |
| repoPath: string | |
| process: ChildProcess | |
| queue: Array<{ | |
| resolve: ( | |
| value: { sha: string; type: string; content: string } | null | |
| ) => void | |
| reject: (error: Error) => void | |
| }> | |
| // Parser State | |
| #state: 'HEADER' | 'CONTENT' = 'HEADER' | |
| #headerBuffer: Buffer = Buffer.alloc(0) | |
| #contentBuffer: Buffer[] = [] | |
| #contentBytesRead = 0 | |
| #contentBytesExpected = 0 | |
| closed = false | |
| constructor(repoPath: string) { | |
| this.repoPath = repoPath | |
| this.process = spawn('git', ['cat-file', '--batch'], { | |
| cwd: repoPath, | |
| stdio: ['pipe', 'pipe', 'pipe'], | |
| shell: false, | |
| detached: false, | |
| }) | |
| this.queue = [] | |
| this.process.stdin?.on('error', (error) => { | |
| const err = error as NodeJS.ErrnoException | |
| if (err.code === 'EPIPE' && !this.closed) { | |
| this.closed = true | |
| this.#rejectAll(new Error('Git batch process died unexpectedly.')) | |
| } | |
| }) | |
| this.process.stdout?.on('data', (chunk: Buffer) => { | |
| this.#processChunk(chunk) | |
| }) | |
| this.process.on('error', (error) => this.#rejectAll(error)) | |
| const onExit = () => this.close() | |
| const unregister = registerExitHandler(onExit) | |
| this.process.on('close', (code: number) => { | |
| this.closed = true | |
| if (code !== 0 && this.queue.length > 0) { | |
| this.#rejectAll( | |
| new Error(`git cat-file --batch exited with code ${code}`) | |
| ) | |
| } | |
| unregister() | |
| }) | |
| } | |
| #rejectAll(error: Error) { | |
| while (this.queue.length > 0) { | |
| const { reject } = this.queue.shift()! | |
| reject(error) | |
| } | |
| } | |
| #processChunk(chunk: Buffer) { | |
| let offset = 0 | |
| while (offset < chunk.length) { | |
| if (this.queue.length === 0) { | |
| // Unexpected data (or previous request cancelled/timed out) | |
| return | |
| } | |
| if (this.#state === 'HEADER') { | |
| const newlineIdx = chunk.indexOf(10, offset) // 10 is \n | |
| if (newlineIdx !== -1) { | |
| // Found end of header | |
| const headerPart = chunk.subarray(offset, newlineIdx) | |
| // Combine with any pending header fragments | |
| const fullHeader = | |
| this.#headerBuffer.length > 0 | |
| ? Buffer.concat([this.#headerBuffer, headerPart]) | |
| : headerPart | |
| this.#headerBuffer = Buffer.alloc(0) // Reset | |
| offset = newlineIdx + 1 // Advance past newline | |
| const headerStr = fullHeader.toString('utf8').trim() | |
| if (headerStr.endsWith(' missing')) { | |
| // Handle missing object | |
| this.queue.shift()?.resolve(null) | |
| // State remains HEADER for next object | |
| continue | |
| } | |
| const parts = headerStr.split(/\s+/) | |
| const sha = parts[0] | |
| const type = parts[1] | |
| const size = parseInt(parts[2], 10) | |
| if (!sha || !type || isNaN(size)) { | |
| // Malformed output, fatal error for this request | |
| this.queue | |
| .shift() | |
| ?.reject(new Error(`Invalid git header: ${headerStr}`)) | |
| // Try to recover state, though likely unsafe | |
| continue | |
| } | |
| // Prepare for content (+1 for trailing newline) | |
| this.#state = 'CONTENT' | |
| this.#contentBytesExpected = size | |
| this.#contentBytesRead = 0 | |
| this.#contentBuffer = [] | |
| // Metadata is stored in the current closure or could be stored on class if needed | |
| // We'll attach it to the pending request processing momentarily | |
| // For simplicity in this loop, we just transition state | |
| // (We store the header info implicitly by knowing the queue order) | |
| // To be strictly correct with type safety in the loop: | |
| this.queue[0] = { | |
| ...this.queue[0], | |
| ...{ _meta: { sha, type } }, | |
| } as any | |
| } else { | |
| // No newline, accumulate remainder of chunk into headerBuffer | |
| const remainder = chunk.subarray(offset) | |
| this.#headerBuffer = Buffer.concat([this.#headerBuffer, remainder]) | |
| offset = chunk.length | |
| } | |
| } else if (this.#state === 'CONTENT') { | |
| // We need to read contentBytesExpected + 1 (for the trailing newline) | |
| // But the resolved content should NOT include the newline. | |
| const bytesNeeded = | |
| this.#contentBytesExpected + 1 - this.#contentBytesRead | |
| const bytesAvailable = chunk.length - offset | |
| const bytesToTake = Math.min(bytesNeeded, bytesAvailable) | |
| const contentPart = chunk.subarray(offset, offset + bytesToTake) | |
| this.#contentBuffer.push(contentPart) | |
| this.#contentBytesRead += bytesToTake | |
| offset += bytesToTake | |
| if (this.#contentBytesRead === this.#contentBytesExpected + 1) { | |
| // Finished reading content + newline | |
| const fullContentBuffer = Buffer.concat(this.#contentBuffer) | |
| // Remove the trailing newline | |
| const actualContent = fullContentBuffer.subarray( | |
| 0, | |
| fullContentBuffer.length - 1 | |
| ) | |
| const request = this.queue.shift()! | |
| const meta = (request as any)._meta | |
| request.resolve({ | |
| sha: meta.sha, | |
| type: meta.type, | |
| content: actualContent.toString('utf8'), | |
| }) | |
| // Reset state | |
| this.#state = 'HEADER' | |
| this.#contentBuffer = [] | |
| } | |
| } | |
| } | |
| } | |
| getObject( | |
| spec: string | |
| ): Promise<{ sha: string; type: string; content: string } | null> { | |
| return new Promise((resolve, reject) => { | |
| if (this.closed) { | |
| return reject(new Error('Git cat process closed')) | |
| } | |
| this.queue.push({ resolve, reject }) | |
| try { | |
| const ok = this.process.stdin?.write(spec + '\n') | |
| if (!ok) { | |
| this.process.stdin?.once('drain', () => {}) | |
| } | |
| } catch (error) { | |
| this.queue.pop() | |
| reject(error) | |
| } | |
| }) | |
| } | |
| close() { | |
| if (this.closed) return | |
| this.closed = true | |
| this.#rejectAll(new Error('Git batch process closed')) | |
| try { | |
| this.process.stdin?.end() | |
| this.process.kill('SIGTERM') | |
| } catch { | |
| /* ignore */ | |
| } | |
| } | |
| [Symbol.dispose]() { | |
| this.close() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment