| … | … | @@ -0,0 +1,119 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | /** | |
| 3 | * Mini CLI example for hyper-sync | |
| 4 | * | |
| 5 | * Usage: | |
| 6 | * node examples/cli.js status [--repos <dir>] | |
| 7 | * node examples/cli.js serve [name] [--repos <dir>] | |
| 8 | * node examples/cli.js add-remote <name> <serverKey> | |
| 9 | * node examples/cli.js pull <remote> [repo] | |
| 10 | */ | |
| 11 | ||
| 12 | const HyperSync = require('../index') | |
| 13 | ||
| 14 | const args = process.argv.slice(2) | |
| 15 | const cmd = args[0] | |
| 16 | ||
| 17 | function getFlag (name) { | |
| 18 | const i = args.indexOf('--' + name) | |
| 19 | return i !== -1 && args[i + 1] ? args[i + 1] : null | |
| 20 | } | |
| 21 | ||
| 22 | function fmt (b) { | |
| 23 | if (b < 1024) return b + ' B' | |
| 24 | if (b < 1048576) return (b / 1024).toFixed(1) + ' KB' | |
| 25 | return (b / 1048576).toFixed(1) + ' MB' | |
| 26 | } | |
| 27 | ||
| 28 | async function main () { | |
| 29 | const repos = getFlag('repos') | |
| 30 | const sync = new HyperSync(repos ? { repos } : {}) | |
| 31 | await sync.ready() | |
| 32 | ||
| 33 | if (cmd === 'status') { | |
| 34 | const s = sync.status() | |
| 35 | console.log('Server key: %s', s.serverKey) | |
| 36 | console.log('Repos: %d, Serving: %d, Remotes: %d\n', s.repoCount, s.serving, s.remotes) | |
| 37 | ||
| 38 | if (s.repos.length === 0) { | |
| 39 | console.log(' (no repos found)') | |
| 40 | } else { | |
| 41 | for (const r of s.repos) { | |
| 42 | console.log(' %s %d branches %s %s', | |
| 43 | r.name.padEnd(30), r.branches, fmt(r.packSize).padStart(8), | |
| 44 | r.head ? r.head : '(empty)') | |
| 45 | } | |
| 46 | } | |
| 47 | ||
| 48 | await sync.close() | |
| 49 | } else if (cmd === 'serve') { | |
| 50 | const name = args[1] && !args[1].startsWith('--') ? args[1] : null | |
| 51 | ||
| 52 | sync.on('serve', (n, topic) => console.log(' %s → %s', n, topic.slice(0, 16) + '...')) | |
| 53 | sync.on('connection', (peer, repo) => console.log('Peer %s connected (repo: %s)', peer, repo)) | |
| 54 | ||
| 55 | await sync.serve(name) | |
| 56 | console.log('Serving %d repo(s). Server key: %s', sync.serving, sync.serverKey) | |
| 57 | console.log('Ctrl+C to stop') | |
| 58 | } else if (cmd === 'add-remote') { | |
| 59 | const name = args[1] | |
| 60 | const key = args[2] | |
| 61 | if (!name || !key) { | |
| 62 | console.error('Usage: cli.js add-remote <name> <serverKey>') | |
| 63 | await sync.close() | |
| 64 | process.exit(1) | |
| 65 | } | |
| 66 | ||
| 67 | sync.addRemote(name, key) | |
| 68 | console.log('Added remote "%s" → %s', name, key.slice(0, 16) + '...') | |
| 69 | ||
| 70 | // Note: remotes are in-memory only — a real app would persist them | |
| 71 | console.log('(remote stored in memory — will not persist across restarts)') | |
| 72 | await sync.close() | |
| 73 | } else if (cmd === 'pull') { | |
| 74 | const remoteName = args[1] | |
| 75 | const repoName = args[2] && !args[2].startsWith('--') ? args[2] : null | |
| 76 | ||
| 77 | if (!remoteName) { | |
| 78 | console.error('Usage: cli.js pull <remote> [repo]') | |
| 79 | await sync.close() | |
| 80 | process.exit(1) | |
| 81 | } | |
| 82 | ||
| 83 | // For this example, accept serverKey directly as the remote name | |
| 84 | // and auto-register it | |
| 85 | if (remoteName.length === 64) { | |
| 86 | sync.addRemote('remote', remoteName) | |
| 87 | console.log('Auto-registered remote from key: %s...', remoteName.slice(0, 16)) | |
| 88 | } | |
| 89 | ||
| 90 | const remote = remoteName.length === 64 ? 'remote' : remoteName | |
| 91 | ||
| 92 | sync.on('pull', (r, name, state) => console.log(' %s/%s: %s', r, name, state)) | |
| 93 | ||
| 94 | try { | |
| 95 | const result = await sync.pull(remote, repoName) | |
| 96 | console.log('Synced: %d, Failed: %d, Elapsed: %dms', | |
| 97 | result.synced.length, result.failed.length, result.elapsed) | |
| 98 | if (result.failed.length) { | |
| 99 | for (const f of result.failed) console.log(' FAIL: %s — %s', f.name, f.error) | |
| 100 | } | |
| 101 | } catch (err) { | |
| 102 | console.error('Pull failed: %s', err.message) | |
| 103 | } | |
| 104 | ||
| 105 | await sync.close() | |
| 106 | } else { | |
| 107 | console.log('hyper-sync example CLI') | |
| 108 | console.log(' status List repos') | |
| 109 | console.log(' serve [name] Announce repos on DHT') | |
| 110 | console.log(' add-remote <name> <key> Register a remote server') | |
| 111 | console.log(' pull <remote|key> [repo] Fetch updates from remote') | |
| 112 | console.log('') | |
| 113 | console.log('Options:') | |
| 114 | console.log(' --repos <dir> Git repos directory (default: ~/git-repos)') | |
| 115 | await sync.close() | |
| 116 | } | |
| 117 | } | |
| 118 | ||
| 119 | main().catch((err) => { console.error(err.message); process.exit(1) }) |
| … | … | @@ -0,0 +1,151 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | /** | |
| 3 | * Mini web UI example for hyper-sync | |
| 4 | * | |
| 5 | * Usage: | |
| 6 | * node examples/web.js [--port 9091] [--repos <dir>] | |
| 7 | * | |
| 8 | * Starts a sync server and HTTP dashboard showing repo status. | |
| 9 | */ | |
| 10 | ||
| 11 | const http = require('http') | |
| 12 | const HyperSync = require('../index') | |
| 13 | ||
| 14 | const args = process.argv.slice(2) | |
| 15 | ||
| 16 | function getFlag (name) { | |
| 17 | const i = args.indexOf('--' + name) | |
| 18 | return i !== -1 && args[i + 1] ? args[i + 1] : null | |
| 19 | } | |
| 20 | ||
| 21 | function fmt (b) { | |
| 22 | if (b < 1024) return b + ' B' | |
| 23 | if (b < 1048576) return (b / 1024).toFixed(1) + ' KB' | |
| 24 | return (b / 1048576).toFixed(1) + ' MB' | |
| 25 | } | |
| 26 | ||
| 27 | const port = parseInt(getFlag('port')) || 9091 | |
| 28 | const reposDir = getFlag('repos') | |
| 29 | ||
| 30 | const sync = new HyperSync(reposDir ? { repos: reposDir } : {}) | |
| 31 | const events = [] | |
| 32 | ||
| 33 | function pushEvent (msg) { | |
| 34 | events.push({ t: Date.now(), msg }) | |
| 35 | if (events.length > 200) events.shift() | |
| 36 | } | |
| 37 | ||
| 38 | sync.on('serve', (name) => pushEvent('serving ' + name)) | |
| 39 | sync.on('connection', (peer, repo) => pushEvent('peer ' + peer + ' → ' + (repo || '?'))) | |
| 40 | sync.on('pull', (remote, name, state) => pushEvent('pull ' + remote + '/' + name + ': ' + state)) | |
| 41 | ||
| 42 | const httpServer = http.createServer((req, res) => { | |
| 43 | const url = req.url.split('?')[0] | |
| 44 | ||
| 45 | if (url === '/api/status') { | |
| 46 | res.writeHead(200, { 'Content-Type': 'application/json' }) | |
| 47 | res.end(JSON.stringify(sync.status())) | |
| 48 | return | |
| 49 | } | |
| 50 | ||
| 51 | if (url === '/api/rescan' && req.method === 'POST') { | |
| 52 | sync.scan().then(() => { | |
| 53 | res.writeHead(200, { 'Content-Type': 'application/json' }) | |
| 54 | res.end(JSON.stringify(sync.status())) | |
| 55 | }).catch((err) => { | |
| 56 | res.writeHead(500, { 'Content-Type': 'application/json' }) | |
| 57 | res.end(JSON.stringify({ error: err.message })) | |
| 58 | }) | |
| 59 | return | |
| 60 | } | |
| 61 | ||
| 62 | if (url === '/api/events') { | |
| 63 | res.writeHead(200, { 'Content-Type': 'application/json' }) | |
| 64 | res.end(JSON.stringify(events.slice(-50))) | |
| 65 | return | |
| 66 | } | |
| 67 | ||
| 68 | res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) | |
| 69 | res.end(renderPage()) | |
| 70 | }) | |
| 71 | ||
| 72 | function renderPage () { | |
| 73 | return `<!DOCTYPE html> | |
| 74 | <html><head><title>hyper-sync</title> | |
| 75 | <style> | |
| 76 | body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 720px; margin: 0 auto; } | |
| 77 | h1 { color: #58a6ff; } | |
| 78 | .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; } | |
| 79 | .card h2 { font-size: 0.75rem; text-transform: uppercase; color: #8b949e; margin: 0 0 0.5rem; } | |
| 80 | .key { font-size: 0.75rem; word-break: break-all; color: #79c0ff; } | |
| 81 | .row { display: flex; justify-content: space-between; padding: 0.3rem 0; border-bottom: 1px solid #21262d; } | |
| 82 | .row:last-child { border-bottom: none; } | |
| 83 | .label { color: #8b949e; } | |
| 84 | table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } | |
| 85 | th { text-align: left; color: #8b949e; padding: 0.3rem 0.5rem; border-bottom: 1px solid #30363d; } | |
| 86 | td { padding: 0.3rem 0.5rem; border-bottom: 1px solid #21262d; } | |
| 87 | .serving { color: #3fb950; } | |
| 88 | .btn { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 0.3rem 0.6rem; border-radius: 4px; cursor: pointer; font-size: 0.8rem; } | |
| 89 | .btn:hover { background: #30363d; } | |
| 90 | #log { max-height: 200px; overflow-y: auto; font-size: 0.8rem; color: #8b949e; } | |
| 91 | #log div { padding: 2px 0; } | |
| 92 | </style></head><body> | |
| 93 | <h1>hyper-sync</h1> | |
| 94 | ||
| 95 | <div class="card"> | |
| 96 | <h2>Server <button class="btn" onclick="rescan()">Rescan</button></h2> | |
| 97 | <div class="key" id="key">...</div> | |
| 98 | <div class="row" style="margin-top:0.5rem"><span class="label">Repos</span><span id="count">-</span></div> | |
| 99 | <div class="row"><span class="label">Serving</span><span id="serving">-</span></div> | |
| 100 | </div> | |
| 101 | ||
| 102 | <div class="card"> | |
| 103 | <h2>Repositories</h2> | |
| 104 | <table><thead><tr><th>Name</th><th>Branches</th><th>Size</th><th>HEAD</th><th>Status</th></tr></thead> | |
| 105 | <tbody id="repos"></tbody></table> | |
| 106 | </div> | |
| 107 | ||
| 108 | <div class="card"><h2>Events</h2><div id="log"></div></div> | |
| 109 | ||
| 110 | <script> | |
| 111 | const $ = id => document.getElementById(id) | |
| 112 | function fmt(b) { | |
| 113 | if (b < 1024) return b + ' B' | |
| 114 | if (b < 1048576) return (b / 1024).toFixed(1) + ' KB' | |
| 115 | return (b / 1048576).toFixed(1) + ' MB' | |
| 116 | } | |
| 117 | ||
| 118 | function refresh() { | |
| 119 | fetch('/api/status').then(r => r.json()).then(d => { | |
| 120 | $('key').textContent = d.serverKey || '(none)' | |
| 121 | $('count').textContent = d.repoCount | |
| 122 | $('serving').textContent = d.serving | |
| 123 | $('repos').innerHTML = d.repos.map(r => | |
| 124 | '<tr><td>' + r.name + '</td><td>' + r.branches + '</td><td>' + fmt(r.packSize) + | |
| 125 | '</td><td>' + (r.head || '-') + '</td><td>' + | |
| 126 | (r.serving ? '<span class="serving">serving</span>' : '-') + '</td></tr>' | |
| 127 | ).join('') | |
| 128 | }) | |
| 129 | fetch('/api/events').then(r => r.json()).then(evts => { | |
| 130 | $('log').innerHTML = evts.reverse().map(e => | |
| 131 | '<div>' + new Date(e.t).toLocaleTimeString() + ' ' + e.msg + '</div>' | |
| 132 | ).join('') | |
| 133 | }) | |
| 134 | } | |
| 135 | ||
| 136 | function rescan() { fetch('/api/rescan', { method: 'POST' }).then(refresh) } | |
| 137 | ||
| 138 | refresh() | |
| 139 | setInterval(refresh, 5000) | |
| 140 | </script></body></html>` | |
| 141 | } | |
| 142 | ||
| 143 | sync.ready().then(async () => { | |
| 144 | await sync.serve() | |
| 145 | httpServer.listen(port, () => { | |
| 146 | console.log('Server key: %s', sync.serverKey) | |
| 147 | console.log('Repos: %d', sync.repos.size) | |
| 148 | console.log('Serving: %d topics', sync.serving) | |
| 149 | console.log('Web UI: http://localhost:%d', port) | |
| 150 | }) | |
| 151 | }).catch((err) => { console.error(err.message); process.exit(1) }) |
| … | … | @@ -8,6 +8,7 @@ const { spawn } = require('child_process'); |
| 8 | 8 | |
| 9 | 9 | const { scanRepos, repoInfo, readRefs } = require('./lib/repos'); |
| 10 | 10 | const { deriveTopic } = require('./lib/topics'); |
| 11 | const { browserMethods } = require('./lib/browser'); | |
| 11 | 12 | |
| 12 | 13 | module.exports = class HyperSync extends ReadyResource { |
| 13 | 14 | constructor (opts = {}) { |
| … | … | @@ -349,3 +350,6 @@ module.exports = class HyperSync extends ReadyResource { |
| 349 | 350 | }); |
| 350 | 351 | } |
| 351 | 352 | }; |
| 353 | ||
| 354 | // Mix in browser methods (listFiles, readFile, getCommits, getDiff, etc.) | |
| 355 | Object.assign(module.exports.prototype, browserMethods); |
| … | … | @@ -0,0 +1,227 @@ |
| 1 | const { spawn } = require('child_process') | |
| 2 | const path = require('path') | |
| 3 | ||
| 4 | /** | |
| 5 | * Git repo browser — reads files, commits, diffs from bare git repos | |
| 6 | * using native git commands. Ported from pear-git v1's GitBrowser. | |
| 7 | * | |
| 8 | * Not a standalone class — methods are mixed into HyperSync. | |
| 9 | */ | |
| 10 | ||
| 11 | function execGit (repoPath, args, timeout) { | |
| 12 | return new Promise((resolve, reject) => { | |
| 13 | const proc = spawn('git', ['-C', repoPath, ...args], { | |
| 14 | stdio: ['ignore', 'pipe', 'pipe'] | |
| 15 | }) | |
| 16 | ||
| 17 | const chunks = [] | |
| 18 | const errorChunks = [] | |
| 19 | ||
| 20 | const timer = setTimeout(() => { | |
| 21 | proc.kill('SIGTERM') | |
| 22 | reject(new Error('git command timed out: git ' + args.join(' '))) | |
| 23 | }, timeout || 30000) | |
| 24 | ||
| 25 | proc.stdout.on('data', (chunk) => chunks.push(chunk)) | |
| 26 | proc.stderr.on('data', (chunk) => errorChunks.push(chunk)) | |
| 27 | ||
| 28 | proc.on('close', (code) => { | |
| 29 | clearTimeout(timer) | |
| 30 | if (code !== 0) { | |
| 31 | const errMsg = Buffer.concat(errorChunks).toString().trim() | |
| 32 | reject(new Error(errMsg || 'git exited with code ' + code)) | |
| 33 | } else { | |
| 34 | resolve(Buffer.concat(chunks).toString()) | |
| 35 | } | |
| 36 | }) | |
| 37 | ||
| 38 | proc.on('error', (err) => { | |
| 39 | clearTimeout(timer) | |
| 40 | reject(err) | |
| 41 | }) | |
| 42 | }) | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * Browser methods to mix into HyperSync.prototype | |
| 47 | */ | |
| 48 | const browserMethods = { | |
| 49 | _repoPath (name) { | |
| 50 | const resolved = path.resolve(this._reposDir, name + '.git') | |
| 51 | if (!resolved.startsWith(path.resolve(this._reposDir))) { | |
| 52 | throw new Error('Invalid repository name') | |
| 53 | } | |
| 54 | return resolved | |
| 55 | }, | |
| 56 | ||
| 57 | async listFiles (name, ref, dirPath) { | |
| 58 | ref = ref || 'HEAD' | |
| 59 | dirPath = dirPath || '' | |
| 60 | const repoPath = this._repoPath(name) | |
| 61 | const treePath = dirPath ? ref + ':' + dirPath : ref | |
| 62 | ||
| 63 | const output = await execGit(repoPath, ['ls-tree', '-l', '--', treePath]) | |
| 64 | return output.trim().split('\n').filter(Boolean).map(line => { | |
| 65 | const parts = line.split(/\s+/) | |
| 66 | const type = parts[1] | |
| 67 | return { | |
| 68 | name: parts.slice(4).join(' '), | |
| 69 | type: type === 'tree' ? 'directory' : type === 'commit' ? 'submodule' : 'file', | |
| 70 | hash: parts[2], | |
| 71 | size: parts[3] === '-' ? null : parseInt(parts[3]), | |
| 72 | mode: parts[0], | |
| 73 | path: dirPath ? dirPath + '/' + parts.slice(4).join(' ') : parts.slice(4).join(' ') | |
| 74 | } | |
| 75 | }) | |
| 76 | }, | |
| 77 | ||
| 78 | async readFile (name, filePath, ref) { | |
| 79 | ref = ref || 'HEAD' | |
| 80 | const repoPath = this._repoPath(name) | |
| 81 | return execGit(repoPath, ['show', ref + ':' + filePath]) | |
| 82 | }, | |
| 83 | ||
| 84 | async getCommits (name, ref, limit) { | |
| 85 | ref = ref || 'HEAD' | |
| 86 | limit = limit || 20 | |
| 87 | const repoPath = this._repoPath(name) | |
| 88 | const format = '%H%n%an%n%ae%n%at%n%s%n%b%n--END--' | |
| 89 | ||
| 90 | const output = await execGit(repoPath, [ | |
| 91 | 'log', '--max-count=' + limit, '--format=' + format, ref, '--' | |
| 92 | ]) | |
| 93 | ||
| 94 | return output.split('--END--\n').filter(e => e.trim()).map(entry => { | |
| 95 | const lines = entry.trim().split('\n') | |
| 96 | if (lines.length < 5) return null | |
| 97 | return { | |
| 98 | hash: lines[0], | |
| 99 | author: lines[1], | |
| 100 | email: lines[2], | |
| 101 | timestamp: parseInt(lines[3]), | |
| 102 | subject: lines[4], | |
| 103 | body: lines.slice(5).join('\n').trim(), | |
| 104 | shortHash: lines[0].substring(0, 7), | |
| 105 | date: new Date(parseInt(lines[3]) * 1000).toISOString() | |
| 106 | } | |
| 107 | }).filter(Boolean) | |
| 108 | }, | |
| 109 | ||
| 110 | async getCommit (name, hash) { | |
| 111 | const commits = await this.getCommits(name, hash, 1) | |
| 112 | return commits[0] || null | |
| 113 | }, | |
| 114 | ||
| 115 | async getBranches (name) { | |
| 116 | const repoPath = this._repoPath(name) | |
| 117 | try { | |
| 118 | const output = await execGit(repoPath, ['branch', '-a']) | |
| 119 | return output.trim().split('\n').filter(Boolean).map(line => ({ | |
| 120 | name: line.replace(/^\*?\s+/, '').trim(), | |
| 121 | current: line.startsWith('*') | |
| 122 | })) | |
| 123 | } catch { return [] } | |
| 124 | }, | |
| 125 | ||
| 126 | async getTags (name) { | |
| 127 | const repoPath = this._repoPath(name) | |
| 128 | try { | |
| 129 | const output = await execGit(repoPath, ['tag', '-l']) | |
| 130 | return output.trim().split('\n').filter(Boolean) | |
| 131 | } catch { return [] } | |
| 132 | }, | |
| 133 | ||
| 134 | async getStats (name) { | |
| 135 | const repoPath = this._repoPath(name) | |
| 136 | try { | |
| 137 | const [commitCount, branches, tags] = await Promise.all([ | |
| 138 | execGit(repoPath, ['rev-list', '--count', 'HEAD']).catch(() => '0'), | |
| 139 | this.getBranches(name), | |
| 140 | this.getTags(name) | |
| 141 | ]) | |
| 142 | return { commits: parseInt(commitCount.trim()), branches: branches.length, tags: tags.length } | |
| 143 | } catch { return { commits: 0, branches: 0, tags: 0 } } | |
| 144 | }, | |
| 145 | ||
| 146 | async getReadme (name, ref) { | |
| 147 | ref = ref || 'HEAD' | |
| 148 | const files = await this.listFiles(name, ref, '') | |
| 149 | const readme = files.find(f => | |
| 150 | f.name.toLowerCase() === 'readme.md' || | |
| 151 | f.name.toLowerCase() === 'readme' || | |
| 152 | f.name.toLowerCase() === 'readme.txt' | |
| 153 | ) | |
| 154 | if (!readme) return null | |
| 155 | const content = await this.readFile(name, readme.name, ref) | |
| 156 | return { name: readme.name, content } | |
| 157 | }, | |
| 158 | ||
| 159 | async getDiff (name, fromCommit, toCommit) { | |
| 160 | toCommit = toCommit || 'HEAD' | |
| 161 | const repoPath = this._repoPath(name) | |
| 162 | return execGit(repoPath, ['diff', fromCommit, toCommit]) | |
| 163 | }, | |
| 164 | ||
| 165 | async getCommitDiff (name, commitHash) { | |
| 166 | const repoPath = this._repoPath(name) | |
| 167 | return execGit(repoPath, ['diff-tree', '-p', '--root', commitHash]) | |
| 168 | }, | |
| 169 | ||
| 170 | async searchFiles (name, query, ref) { | |
| 171 | ref = ref || 'HEAD' | |
| 172 | const repoPath = this._repoPath(name) | |
| 173 | const output = await execGit(repoPath, ['ls-tree', '-r', '--name-only', '--', ref]) | |
| 174 | const allFiles = output.trim().split('\n').filter(Boolean) | |
| 175 | return allFiles.filter(f => f.toLowerCase().includes(query.toLowerCase())).slice(0, 50) | |
| 176 | }, | |
| 177 | ||
| 178 | async isBinary (name, filePath, ref) { | |
| 179 | ref = ref || 'HEAD' | |
| 180 | try { | |
| 181 | const content = await this.readFile(name, filePath, ref) | |
| 182 | const sample = content.substring(0, 8192) | |
| 183 | const nullCount = (sample.match(/\0/g) || []).length | |
| 184 | return nullCount / sample.length > 0.01 | |
| 185 | } catch { return false } | |
| 186 | }, | |
| 187 | ||
| 188 | /** | |
| 189 | * Handle git smart HTTP info/refs request | |
| 190 | */ | |
| 191 | async handleInfoRefs (repoPath, service) { | |
| 192 | if (service !== 'git-upload-pack' && service !== 'git-receive-pack') { | |
| 193 | throw new Error('Invalid service: ' + service) | |
| 194 | } | |
| 195 | // git-upload-pack and git-receive-pack are standalone binaries, not git subcommands | |
| 196 | const output = await new Promise((resolve, reject) => { | |
| 197 | const proc = spawn(service, ['--stateless-rpc', '--advertise-refs', repoPath], { | |
| 198 | stdio: ['ignore', 'pipe', 'pipe'] | |
| 199 | }) | |
| 200 | const chunks = [] | |
| 201 | proc.stdout.on('data', (c) => chunks.push(c)) | |
| 202 | proc.on('close', (code) => { | |
| 203 | if (code !== 0) reject(new Error(service + ' exited with code ' + code)) | |
| 204 | else resolve(Buffer.concat(chunks).toString()) | |
| 205 | }) | |
| 206 | proc.on('error', reject) | |
| 207 | }) | |
| 208 | // Prepend service announcement per smart HTTP protocol | |
| 209 | const announcement = '# service=' + service + '\n' | |
| 210 | const lenHex = (announcement.length + 4).toString(16).padStart(4, '0') | |
| 211 | return lenHex + announcement + '0000' + output | |
| 212 | }, | |
| 213 | ||
| 214 | /** | |
| 215 | * Spawn git service process for smart HTTP pack exchange | |
| 216 | */ | |
| 217 | spawnGitService (repoPath, service) { | |
| 218 | if (service !== 'git-upload-pack' && service !== 'git-receive-pack') { | |
| 219 | throw new Error('Invalid service: ' + service) | |
| 220 | } | |
| 221 | return spawn(service, ['--stateless-rpc', repoPath], { | |
| 222 | stdio: ['pipe', 'pipe', 'pipe'] | |
| 223 | }) | |
| 224 | } | |
| 225 | } | |
| 226 | ||
| 227 | module.exports = { browserMethods, execGit } |