OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / p2p / sync / git-sync-pipe.js

139 lines · javascript ref:
/**
 * 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 {}
    }
  }
}