/**
* Git Sync Pipe — Pipes git processes to/from Protomux data channels
*
* Bridges git protocol processes (git-upload-pack, git-fetch-pack) with
* Protomux data channels from GitSyncServer/Client. Git stdout flows
* through channel.message.send(), channel data flows to git stdin.
*
* Usage:
* const dataEntry = syncServer.openDataChannel('my-repo')
* const pipe = createGitPipe('git-upload-pack', repoPath, dataEntry)
* // dataEntry.message.onmessage is now wired to git stdin
* // git stdout is forwarded via dataEntry.message.send()
* const { exitCode, stderr } = await pipe.exitPromise
*/
import { spawn } from 'bare-subprocess'
/**
* Create a bidirectional pipe between a git process and a Protomux data channel.
*
* @param {string} service — git command to spawn (e.g. 'git-upload-pack')
* @param {string} repoPath — path to the bare git repository
* @param {{ message: { send: Function, onmessage: Function } }} dataEntry — data channel entry from openDataChannel()
* @param {object} [opts]
* @param {number} [opts.timeout=120000] — kill process after this many ms
* @returns {{ writeToGit: Function, exitPromise: Promise, kill: Function }}
*/
export function createGitPipe (service, repoPath, dataEntry, opts = {}) {
const timeout = opts.timeout !== undefined ? opts.timeout : 120000
const child = spawn(service, [repoPath], {
stdio: ['pipe', 'pipe', 'pipe']
})
let timedOut = false
let killed = false
const stderrChunks = []
// -- git stdout → channel.message.send() --
// NOTE: Protomux message.send() does not return a backpressure signal.
// The underlying mux stream handles buffering. If the remote can't keep
// up, data accumulates in the mux write buffer. For very large transfers,
// the direct punch:// path (git-p2p-handler) should be preferred over
// this Protomux path.
child.stdout.on('data', (chunk) => {
dataEntry.message.send(chunk)
})
// -- channel data → git stdin (wire up onmessage) --
// Protomux delivers messages synchronously — we can't pause delivery,
// but we can let Node/Bare buffer in git's stdin pipe.
dataEntry.message.onmessage = (data) => {
if (child.stdin.writable !== false) {
child.stdin.write(data)
}
}
// -- collect stderr --
child.stderr.on('data', (chunk) => {
stderrChunks.push(chunk)
})
// -- error handlers to prevent uncaught exceptions --
child.stdin.on('error', (err) => {
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
// Silently ignore pipe errors (expected when process exits)
}
})
child.stdout.on('error', () => {})
// -- timeout --
let timer = null
if (timeout > 0) {
timer = setTimeout(() => {
timedOut = true
try { child.kill('SIGKILL') } catch {}
}, timeout)
}
// -- exitPromise --
const exitPromise = new Promise((resolve) => {
child.on('exit', (code) => {
if (timer) clearTimeout(timer)
resolve({
exitCode: code,
stderr: Buffer.concat(stderrChunks).toString('utf-8'),
timedOut,
killed
})
})
child.on('error', (err) => {
if (timer) clearTimeout(timer)
resolve({
exitCode: -1,
stderr: err.message,
timedOut,
killed
})
})
})
// -- public API --
return {
/**
* Write data to the git process stdin.
* Used when data arrives from the Protomux channel.
*/
writeToGit (data) {
if (child.stdin.writable !== false) {
child.stdin.write(data)
}
},
/**
* Close the git process stdin.
* Signals end-of-input to the git process (e.g. after protocol exchange).
*/
closeStdin () {
try { child.stdin.end() } catch {}
},
/**
* Promise that resolves when the git process exits.
* Result: { exitCode, stderr, timedOut, killed }
*/
exitPromise,
/**
* Kill the git process.
*/
kill () {
killed = true
try { child.kill('SIGKILL') } catch {}
}
}
}