import subprocess from 'bare-subprocess'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
/**
* Git Smart HTTP Protocol Handler
*
* Implements the git smart HTTP protocol for clone/push operations.
* Uses native git commands to serve repositories.
*/
export class GitProtocolHandler {
constructor(options = {}) {
this.gitReposPath = options.gitReposPath || path.join(os.homedir(), 'git-repos')
}
/**
* Get the path to a bare repository
*/
getRepoPath(username, repoName) {
// For now, ignore username and just look in git-repos
const repoPath = path.join(this.gitReposPath, `${repoName}.git`)
return repoPath
}
/**
* Check if a repository exists
*/
async repoExists(username, repoName) {
const repoPath = this.getRepoPath(username, repoName)
try {
const stat = await fs.promises.stat(repoPath)
return stat.isDirectory()
} catch {
return false
}
}
/**
* Handle /info/refs endpoint
*
* GET /repo.git/info/refs?service=git-upload-pack
* GET /repo.git/info/refs?service=git-receive-pack
*/
async handleInfoRefs(req, res, username, repoName) {
const url = new URL(req.url, `http://${req.headers.host}`)
const service = url.searchParams.get('service')
if (!service || !['git-upload-pack', 'git-receive-pack'].includes(service)) {
res.statusCode = 400
res.end('Bad Request: Missing or invalid service parameter')
return
}
const repoPath = this.getRepoPath(username, repoName)
// Check if repo exists
if (!await this.repoExists(username, repoName)) {
res.statusCode = 404
res.end('Repository not found')
return
}
try {
// Run git command to get refs (git-upload-pack or git-receive-pack)
const proc = subprocess.spawn(service, ['--stateless-rpc', '--advertise-refs', repoPath], {
stdio: ['ignore', 'pipe', 'pipe']
})
// Collect stdout and stderr
const chunks = []
const errorChunks = []
proc.stdout.on('data', (chunk) => {
chunks.push(chunk)
})
proc.stderr.on('data', (chunk) => {
errorChunks.push(chunk)
})
// Wait for process to complete
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
proc.kill('SIGTERM')
reject(new Error(`Git info-refs timed out after 60s`))
}, 60000)
proc.on('exit', (code) => {
clearTimeout(timeout)
if (code === 0) {
resolve()
} else {
const errorMsg = Buffer.concat(errorChunks).toString()
reject(new Error(`Git command failed with code ${code}: ${errorMsg}`))
}
})
proc.on('error', (err) => {
clearTimeout(timeout)
reject(err)
})
})
const output = Buffer.concat(chunks)
// Format response with git smart HTTP framing
const serviceHeader = this.pktLine(`# service=${service}\n`)
const flushPkt = '0000'
res.statusCode = 200
res.setHeader('Content-Type', `application/x-${service}-advertisement`)
res.setHeader('Cache-Control', 'no-cache')
// Write service header
res.write(serviceHeader)
res.write(flushPkt)
// Write refs
res.write(output)
res.end()
} catch (err) {
console.error('Git info-refs error:', err)
res.statusCode = 500
res.end('Internal Server Error')
}
}
/**
* Handle /git-upload-pack endpoint (clone/fetch)
*
* POST /repo.git/git-upload-pack
*/
async handleUploadPack(req, res, username, repoName) {
const repoPath = this.getRepoPath(username, repoName)
// Check if repo exists
if (!await this.repoExists(username, repoName)) {
res.statusCode = 404
res.end('Repository not found')
return
}
try {
// Spawn git-upload-pack
const proc = subprocess.spawn('git-upload-pack', ['--stateless-rpc', repoPath], {
stdio: ['pipe', 'pipe', 'pipe']
})
// 5 min timeout for large clone/fetch operations
const timeout = setTimeout(() => {
console.error('[Git] upload-pack timed out after 5 min')
proc.kill('SIGTERM')
}, 300000)
// Set response headers immediately
res.statusCode = 200
res.setHeader('Content-Type', 'application/x-git-upload-pack-result')
res.setHeader('Cache-Control', 'no-cache')
// Pipe request body to git stdin
req.on('data', (chunk) => {
proc.stdin.write(chunk)
})
req.on('end', () => {
proc.stdin.end()
})
// Pipe git stdout to response
proc.stdout.on('data', (chunk) => {
res.write(chunk)
})
// Handle process completion
proc.on('exit', (code) => {
clearTimeout(timeout)
if (code !== 0) {
console.error(`Git upload-pack exited with code ${code}`)
}
res.end()
})
proc.on('error', (err) => {
clearTimeout(timeout)
console.error('Git upload-pack error:', err)
if (!res.headersSent) {
res.statusCode = 500
res.end('Internal Server Error')
} else {
res.end()
}
})
// Handle stderr
proc.stderr.on('data', (chunk) => {
console.error('Git upload-pack stderr:', chunk.toString())
})
} catch (err) {
console.error('Git upload-pack error:', err)
res.statusCode = 500
res.end('Internal Server Error')
}
}
/**
* Handle /git-receive-pack endpoint (push)
*
* POST /repo.git/git-receive-pack
*/
async handleReceivePack(req, res, username, repoName) {
const repoPath = this.getRepoPath(username, repoName)
// Check if repo exists
if (!await this.repoExists(username, repoName)) {
res.statusCode = 404
res.end('Repository not found')
return
}
try {
// Spawn git-receive-pack
const proc = subprocess.spawn('git-receive-pack', ['--stateless-rpc', repoPath], {
stdio: ['pipe', 'pipe', 'pipe']
})
// 5 min timeout for large push operations
const timeout = setTimeout(() => {
console.error('[Git] receive-pack timed out after 5 min')
proc.kill('SIGTERM')
}, 300000)
// Set response headers immediately
res.statusCode = 200
res.setHeader('Content-Type', 'application/x-git-receive-pack-result')
res.setHeader('Cache-Control', 'no-cache')
// Pipe request body to git stdin
req.on('data', (chunk) => {
proc.stdin.write(chunk)
})
req.on('end', () => {
proc.stdin.end()
})
// Pipe git stdout to response
proc.stdout.on('data', (chunk) => {
res.write(chunk)
})
// Handle process completion
proc.on('exit', (code) => {
clearTimeout(timeout)
if (code !== 0) {
console.error(`Git receive-pack exited with code ${code}`)
}
res.end()
})
proc.on('error', (err) => {
clearTimeout(timeout)
console.error('Git receive-pack error:', err)
if (!res.headersSent) {
res.statusCode = 500
res.end('Internal Server Error')
} else {
res.end()
}
})
// Handle stderr (progress messages)
proc.stderr.on('data', (chunk) => {
// Git sends progress to stderr, could forward to client if needed
console.log('Git receive-pack:', chunk.toString())
})
} catch (err) {
console.error('Git receive-pack error:', err)
res.statusCode = 500
res.end('Internal Server Error')
}
}
/**
* Create a pkt-line formatted string
*
* Git uses 4-byte hex length prefix + data format
*/
pktLine(data) {
const len = data.length + 4
const hexLen = len.toString(16).padStart(4, '0')
return hexLen + data
}
}