From 25bcaeaa0c66cdb9219517dc479444f4d7938c83 Mon Sep 17 00:00:00 2001 From: Scott Prager Date: Sat, 6 Jun 2015 16:17:35 -0400 Subject: gittorrentd: Use git-upload-pack for pack files. Add `upload_pack` to the git module to (partially) implement the pack protocol, then use this in gittorrentd instead of `git pack-objects`. This lets us generate packs based off HEAD instead of packing the whole tree each time. --- git-remote-gittorrent | 12 ++---- git.js | 104 +++++++++++++++++++++++++++++++++++++++++++++++++- gittorrentd | 39 ++++++++++--------- 3 files changed, 128 insertions(+), 27 deletions(-) diff --git a/git-remote-gittorrent b/git-remote-gittorrent index ad9825b..bb76dc8 100755 --- a/git-remote-gittorrent +++ b/git-remote-gittorrent @@ -125,13 +125,6 @@ dht.on('peer', function (addr, hash, from) { goal.swarm.addPeer(addr) }) -function update_ref (sha) { - fetching[sha].branches.forEach(function (branch) { - branch = remotename + '/' + branch - spawn('git', ['update-ref', branch, sha]) - }) -} - function get_infohash (sha, branch) { branch = branch.replace(/^refs\/(heads\/)?/, '') branch = branch.replace(/\/head$/, '') @@ -170,8 +163,10 @@ function get_infohash (sha, branch) { tracker: false }) client.download(infoHash, function (torrent) { - console.warn('Downloading git pack with infohash: ' + chalk.green(infoHash) + '\n') + console.warn('Downloading ' + chalk.green(torrent.files[0].path) + + ' with infohash: ' + chalk.green(infoHash) + '\n') torrent.on('done', function (done) { + console.warn('done downloading: ' + chalk.green(torrent.files[0].path)) fetching[sha].got = true var stream = torrent.files[0].createReadStream() @@ -179,7 +174,6 @@ function get_infohash (sha, branch) { stream.pipe(unpack.stdin) unpack.stderr.pipe(process.stderr) unpack.on('exit', function (code) { - update_ref(sha) todo-- if (todo <= 0) { // These writes are actually necessary for git to finish diff --git a/git.js b/git.js index cd9efe3..b564e74 100644 --- a/git.js +++ b/git.js @@ -24,4 +24,106 @@ function ls (url, with_ref) { return ls } -module.exports = {ls: ls} +function pad4 (num) { + num = num.toString(16) + while (num.length < 4) { + num = '0' + num + } + return num +} + +// Invokes `$ git-upload-pack --strict `, communicates haves and wants and +// emits 'ready' when stdout becomes a pack file stream. +function upload_pack (dir, want, have) { + // reference: + // https://github.com/git/git/blob/b594c975c7e865be23477989d7f36157ad437dc7/Documentation/technical/pack-protocol.txt#L346-L393 + var upload = spawn('git-upload-pack', ['--strict', dir]) + writeln('want ' + want) + writeln() + if (have) { + writeln('have ' + have) + writeln() + } + writeln('done') + + // We want to read git's output one line at a time, and not read any more + // than we have to. That way, when we finish discussing wants and haves, we + // can pipe the rest of the output to a stream. + // + // We use `mode` to keep track of state and formulate responses. It returns + // `false` when we should stop reading. + var mode = list + upload.stdout.on('readable', function () { + while (true) { + var line = getline() + if (line === null) { + return // to wait for more output + } + if (!mode(line)) { + upload.stdout.removeAllListeners('readable') + upload.emit('ready') + return + } + } + }) + + var getline_len = null + // Extracts exactly one line from the stream. Uses `getline_len` in case the + // whole line could not be read. + function getline () { + // Format: '####line' where '####' represents the length of 'line' in hex. + if (!getline_len) { + getline_len = upload.stdout.read(4) + if (getline_len === null) { + return null + } + getline_len = parseInt(getline_len, 16) + } + + if (getline_len === 0) { + return '' + } + + // Subtract by the four we just read, and the terminating newline. + var line = upload.stdout.read(getline_len - 4 - 1) + if (!line) { + return null + } + getline_len = null + upload.stdout.read(1) // And discard the newline. + return line.toString() + } + + // First, the server lists the refs it has, but we already know from + // `git ls-remote`, so wait for it to signal the end. + function list (line) { + if (line === '') { + mode = have ? ack_objects_continue : wait_for_nak + } + return true + } + + // If we only gave wants, git should respond with 'NAK', then the pack file. + function wait_for_nak (line) { + return line !== 'NAK' + } + + // With haves, we wait for 'ACK', but only if not ending in 'continue'. + function ack_objects_continue (line) { + return !(line.search(/^ACK/) !== -1 && line.search(/continue$/) === -1) + } + + // Writes one line to stdin so git-upload-pack can understand. + function writeln (line) { + if (line) { + var len = pad4(line.length + 4 + 1) // Add one for the newline. + upload.stdin.write(len + line + '\n') + } else { + upload.stdin.write('0000') + } + } + + return upload +} + +module.exports = {ls: ls, upload_pack: upload_pack} diff --git a/gittorrentd b/gittorrentd index aa043d1..d498590 100755 --- a/gittorrentd +++ b/gittorrentd @@ -72,6 +72,8 @@ function bpad (n, buf) { } } +var head = '' + dht.on('ready', function () { // Spider all */.git dirs and announce all refs. var repos = glob.sync('*/{,.git/}git-daemon-export-ok', {strict: false}) @@ -89,6 +91,9 @@ dht.on('ready', function () { if (ref !== 'HEAD' && !ref.match(/^refs\/heads\//)) { return } + if (ref === 'refs/heads/master') { + head = sha + } userProfile.repositories[reponame][ref] = sha if (!announcedRefs[sha]) { console.log('Announcing ' + sha + ' for ' + ref + ' on repo ' + repo) @@ -148,19 +153,23 @@ dht.on('ready', function () { wire.handshake(new Buffer(infoHash), new Buffer(myPeerId)) }) wire.ut_gittorrent.on('generatePack', function (sha) { - console.error('calling git pack-objects') - var filename = sha + '.pack' - var stream = fs.createWriteStream(filename) + console.error('calling git pack-objects for ' + sha) if (!announcedRefs[sha]) { console.error('Asked for an unknown sha: ' + sha) return } var directory = announcedRefs[sha] - var pack = spawn('git', ['pack-objects', '--revs', '--thin', '--stdout', '--delta-base-offset'], {cwd: directory}) - pack.on('close', function (code) { - if (code !== 0) { - console.error('git pack-objects process exited with code ' + code) - } else { + var have = null + if (sha !== head) { + have = head + } + var pack = git.upload_pack(directory, sha, have) + pack.stderr.pipe(process.stderr) + pack.on('ready', function () { + var filename = sha + '.pack' + var stream = fs.createWriteStream(filename) + pack.stdout.pipe(stream) + stream.on('close', function () { console.error('Finished writing ' + filename) var webtorrent = new WebTorrent({ dht: {bootstrap: config.dht.bootstrap}, @@ -170,17 +179,13 @@ dht.on('ready', function () { console.error(torrent.infoHash) wire.ut_gittorrent.sendTorrent(torrent.infoHash) }) - } - }) - pack.stdout.pipe(stream) - pack.stderr.on('data', function (data) { - console.error(data.toString()) + }) }) - pack.on('exit', function () { - console.log('exited') + pack.on('exit', function (code) { + if (code !== 0) { + console.error('git-upload-pack process exited with code ' + code) + } }) - pack.stdin.write(sha + '\n') - pack.stdin.write('--not\n\n') }) }).listen(config.dht.announce) }) -- cgit v1.2.3