Skip to content

Instantly share code, notes, and snippets.

@souporserious
Created February 1, 2026 03:17
Show Gist options
  • Select an option

  • Save souporserious/2202ea8ae91a1063e7df2d74451ba029 to your computer and use it in GitHub Desktop.

Select an option

Save souporserious/2202ea8ae91a1063e7df2d74451ba029 to your computer and use it in GitHub Desktop.
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