From 7ebc1ffc8dbf7d3e85f5eb3e4e3f82acd13760c6 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 14:00:18 -0500 Subject: [PATCH 001/256] Don't show delta progress is there are none --- js-git.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-git.js b/js-git.js index ef02889..82a8ab6 100644 --- a/js-git.js +++ b/js-git.js @@ -815,7 +815,7 @@ module.exports = function (platform) { } function cleanup() { - if (opts.onProgress) { + if (opts.onProgress && deltas) { opts.onProgress(deltaProgress() + "\n"); } var hashes = Object.keys(toDelete); From 5f87423a50623f5f7d1a81916e4414d607c270d3 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 14:11:54 -0500 Subject: [PATCH 002/256] Add more options to clone test --- examples/clone.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/clone.js b/examples/clone.js index 886ddd4..0d3511c 100644 --- a/examples/clone.js +++ b/examples/clone.js @@ -10,7 +10,7 @@ var url = process.argv[2] || "git://github.com/creationix/conquest.git"; var remote = gitRemote(url); // Create a local repo -var path = basename(remote.pathname); +var path = process.argv[3] || basename(remote.pathname); var repo = jsGit(fsDb(fs(path))); console.log("Cloning %s to %s", url, path); @@ -20,6 +20,9 @@ var opts = { process.stdout.write(progress); } }; +if (process.env.DEPTH) { + opts.depth = parseInt(process.env.DEPTH, 10); +} repo.fetch(remote, opts, function (err) { if (err) throw err; From db3b01666224d3d5b303275ff4e1c7c3099e2d9d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 16:00:12 -0500 Subject: [PATCH 003/256] Optimize and cleanup delta application logic --- js-git.js | 84 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/js-git.js b/js-git.js index 82a8ab6..6965fea 100644 --- a/js-git.js +++ b/js-git.js @@ -694,7 +694,7 @@ module.exports = function (platform) { function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); // TODO: save the stream to the local repo. - var version, num, count = 0, deltas = 0; + var version, num, count = 0, deltas = 0, done; // hashes keyed by offset var hashes = {}; @@ -703,14 +703,23 @@ module.exports = function (platform) { var pending = {}; var queue = []; - packStream.read(function (err, stats) { - if (err) return callback(err); + return packStream.read(onStats); + + function onDone(err) { + if (done) return; + done = true; + return callback(err); + } + + function onStats(err, stats) { + if (err) return onDone(err); version = stats.version; num = stats.num; packStream.read(onRead); - }); + } + function onRead(err, item) { - if (err) return callback(err); + if (err) return onDone(err); if (opts.onProgress) { var percent = Math.round(count / num * 100); opts.onProgress("Receiving objects: " + percent + "% (" + count + "/" + num + ") " + (item ? "\r" : "\n")); @@ -722,7 +731,7 @@ module.exports = function (platform) { return checkExisting(); } if (item.size !== item.body.length) { - return callback(new Error("Body size mismatch")); + return onDone(new Error("Body size mismatch")); } var buffer = bops.join([ bops.from(item.type + " " + item.size + "\0"), @@ -746,7 +755,7 @@ module.exports = function (platform) { } db.save(hash, buffer, function (err) { - if (err) return callback(err); + if (err) return onDone(err); if (trace) trace("save", null, hash); packStream.read(onRead); }); @@ -763,7 +772,7 @@ module.exports = function (platform) { return db.has(hash, onHas); } function onHas(err, has) { - if (err) return callback(err); + if (err) return onDone(err); if (has) seen[hash] = true; return pop(); } @@ -788,30 +797,43 @@ module.exports = function (platform) { function check() { var item = queue.pop(); + var target, delta; if (!item) return applyDeltas(); if (opts.onProgress) { opts.onProgress(deltaProgress() + "\r"); } - db.load(item.ref, function (err, target) { - if (err) return callback(err); - db.load(item.hash, function (err, delta) { - if (err) return callback(err); - target = deframe(target); - delta = deframe(delta); - var buffer = frame(target[0], applyDelta(delta[1], target[1])); - var hash = sha1(buffer); - db.save(hash, buffer, function (err) { - if (err) return callback(err); - var deps = pending[item.hash]; - if (deps) { - pending[hash] = deps; - delete pending[item.hash]; - } - seen[hash] = true; - return check(); - }); - }); - }); + db.load(item.ref, onTarget); + db.load(item.hash, onDelta); + return; + + function onTarget(err, raw) { + if (err) return onDone(err); + target = deframe(raw); + if (delta) return onPair(item, target, delta); + } + + function onDelta(err, raw) { + if (err) return onDone(err); + delta = deframe(raw); + if (target) return onPair(item, target, delta); + } + } + + function onPair(item, target, delta) { + var buffer = frame(target[0], applyDelta(delta[1], target[1])); + var hash = sha1(buffer); + db.save(hash, buffer, onSave); + + function onSave(err) { + if (err) return onDone(err); + var deps = pending[item.hash]; + if (deps) { + pending[hash] = deps; + delete pending[item.hash]; + } + seen[hash] = true; + return check(); + } } function cleanup() { @@ -821,14 +843,12 @@ module.exports = function (platform) { var hashes = Object.keys(toDelete); next(); function next(err) { - if (err) return callback(err); + if (err) return onDone(err); var hash = hashes.pop(); - if (!hash) return callback(); + if (!hash) return onDone(); remove(hash, next); } } } - } - }; \ No newline at end of file From 1fa5c3824d92a5f9e51f71a90a00c7245e077d8b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 16:03:17 -0500 Subject: [PATCH 004/256] Reduce indentation in main file a bit --- js-git.js | 1454 +++++++++++++++++++++++++++-------------------------- 1 file changed, 730 insertions(+), 724 deletions(-) diff --git a/js-git.js b/js-git.js index 6965fea..7f95676 100644 --- a/js-git.js +++ b/js-git.js @@ -1,854 +1,860 @@ -module.exports = function (platform) { - var applyDelta = require('git-pack-codec/apply-delta.js')(platform); - var pushToPull = require('push-to-pull'); - var parse = pushToPull(require('git-pack-codec/decode.js')(platform)); +var platform; +var applyDelta, pushToPull, parse; + +module.exports = function (imports) { + if (platform) return newRepo; + + platform = imports; + applyDelta = require('git-pack-codec/apply-delta.js')(platform); + pushToPull = require('push-to-pull'); + parse = pushToPull(require('git-pack-codec/decode.js')(platform)); platform.agent = platform.agent || "js-git/" + require('./package.json').version; return newRepo; +}; + +// platform options are: db, proto, and trace +function newRepo(db, workDir) { + var trace = platform.trace; + var sha1 = platform.sha1; + var bops = platform.bops; + + var encoders = { + commit: encodeCommit, + tag: encodeTag, + tree: encodeTree, + blob: encodeBlob + }; + + var decoders = { + commit: decodeCommit, + tag: decodeTag, + tree: decodeTree, + blob: decodeBlob + }; + + var repo = {}; + + if (db) { + // Git Objects + repo.load = load; // (hashish) -> object + repo.save = save; // (object) -> hash + repo.loadAs = loadAs; // (type, hashish) -> value + repo.saveAs = saveAs; // (type, value) -> hash + repo.remove = remove; // (hashish) + repo.unpack = unpack; // (opts, packStream) + + // Refs + repo.resolveHashish = resolveHashish; // (hashish) -> hash + repo.updateHead = updateHead; // (hash) + repo.getHead = getHead; // () -> ref + repo.setHead = setHead; // (ref) + repo.createRef = createRef; // (ref, hash) + repo.deleteRef = deleteRef; // (ref) + repo.listRefs = listRefs; // (prefix) -> refs + + if (workDir) { + // TODO: figure out API for working repos + } + } - // platform options are: db, proto, and trace - function newRepo(db, workDir) { - var trace = platform.trace; - var sha1 = platform.sha1; - var bops = platform.bops; - - var encoders = { - commit: encodeCommit, - tag: encodeTag, - tree: encodeTree, - blob: encodeBlob - }; + // Network Protocols - var decoders = { - commit: decodeCommit, - tag: decodeTag, - tree: decodeTree, - blob: decodeBlob - }; + repo.lsRemote = lsRemote; + if (db) { + repo.fetch = fetch; + repo.push = push; + } - var repo = {}; - - if (db) { - // Git Objects - repo.load = load; // (hashish) -> object - repo.save = save; // (object) -> hash - repo.loadAs = loadAs; // (type, hashish) -> value - repo.saveAs = saveAs; // (type, value) -> hash - repo.remove = remove; // (hashish) - repo.unpack = unpack; // (opts, packStream) - - // Refs - repo.resolveHashish = resolveHashish; // (hashish) -> hash - repo.updateHead = updateHead; // (hash) - repo.getHead = getHead; // () -> ref - repo.setHead = setHead; // (ref) - repo.createRef = createRef; // (ref, hash) - repo.deleteRef = deleteRef; // (ref) - repo.listRefs = listRefs; // (prefix) -> refs - - if (workDir) { - // TODO: figure out API for working repos - } - } + return repo; - // Network Protocols + function load(hashish, callback) { + if (!callback) return load.bind(this, hashish); + var hash; + return resolveHashish(hashish, onHash); - repo.lsRemote = lsRemote; - if (db) { - repo.fetch = fetch; - repo.push = push; + function onHash(err, result) { + if (err) return callback(err); + hash = result; + return db.load(hash, onBuffer); } - return repo; - - function load(hashish, callback) { - if (!callback) return load.bind(this, hashish); - var hash; - return resolveHashish(hashish, onHash); - - function onHash(err, result) { + function onBuffer(err, buffer) { + if (err) return callback(err); + var type, object; + try { + if (sha1(buffer) !== hash) { + throw new Error("Hash checksum failed for " + hash); + } + var pair = deframe(buffer); + type = pair[0]; + buffer = pair[1]; + object = { + type: type, + body: decoders[type](buffer) + }; + } catch (err) { if (err) return callback(err); - hash = result; - return db.load(hash, onBuffer); } + if (trace) trace("load", null, hash); + return callback(null, object, hash); + } + } - function onBuffer(err, buffer) { - if (err) return callback(err); - var type, object; - try { - if (sha1(buffer) !== hash) { - throw new Error("Hash checksum failed for " + hash); - } - var pair = deframe(buffer); - type = pair[0]; - buffer = pair[1]; - object = { - type: type, - body: decoders[type](buffer) - }; - } catch (err) { - if (err) return callback(err); - } - if (trace) trace("load", null, hash); - return callback(null, object, hash); - } + function save(object, callback) { + if (!callback) return save.bind(this, object); + var buffer, hash; + try { + buffer = encoders[object.type](object.body); + buffer = frame(object.type, buffer); + hash = sha1(buffer); + } + catch (err) { + return callback(err); } + return db.save(hash, buffer, onSave); - function save(object, callback) { - if (!callback) return save.bind(this, object); - var buffer, hash; - try { - buffer = encoders[object.type](object.body); - buffer = frame(object.type, buffer); - hash = sha1(buffer); - } - catch (err) { - return callback(err); - } - return db.save(hash, buffer, onSave); + function onSave(err) { + if (err) return callback(err); + if (trace) trace("save", null, hash); + return callback(null, hash); + } + } - function onSave(err) { - if (err) return callback(err); - if (trace) trace("save", null, hash); - return callback(null, hash); + function loadAs(type, hashish, callback) { + if (!callback) return loadAs.bind(this, type, hashish); + return load(hashish, onObject); + + function onObject(err, object, hash) { + if (err) return callback(err); + if (object.type !== type) { + return new Error("Expected " + type + ", but found " + object.type); } + return callback(null, object.body, hash); } + } - function loadAs(type, hashish, callback) { - if (!callback) return loadAs.bind(this, type, hashish); - return load(hashish, onObject); + function saveAs(type, body, callback) { + if (!callback) return saveAs.bind(this, type, body); + return save({ type: type, body: body }, callback); + } - function onObject(err, object, hash) { - if (err) return callback(err); - if (object.type !== type) { - return new Error("Expected " + type + ", but found " + object.type); - } - return callback(null, object.body, hash); - } + function remove(hashish, callback) { + if (!callback) return remove.bind(this, hashish); + var hash; + return resolveHashish(hashish, onHash); + + function onHash(err, result) { + if (err) return callback(err); + hash = result; + return db.remove(hash, onRemove); } - function saveAs(type, body, callback) { - if (!callback) return saveAs.bind(this, type, body); - return save({ type: type, body: body }, callback); + function onRemove(err) { + if (err) return callback(err); + if (trace) trace("remove", null, hash); + return callback(null, hash); } + } - function remove(hashish, callback) { - if (!callback) return remove.bind(this, hashish); - var hash; - return resolveHashish(hashish, onHash); + function resolveHashish(hashish, callback) { + if (!callback) return resolveHashish.bind(this, hashish); + hashish = hashish.trim(); + if ((/^[0-9a-f]{40}$/i).test(hashish)) { + return callback(null, hashish.toLowerCase()); + } + if (hashish === "HEAD") return getHead(onBranch); + if ((/^refs\//).test(hashish)) { + return db.read(hashish, checkBranch); + } + return checkBranch(); - function onHash(err, result) { - if (err) return callback(err); - hash = result; - return db.remove(hash, onRemove); - } + function onBranch(err, ref) { + if (err) return callback(err); + return resolveHashish(ref, callback); + } - function onRemove(err) { - if (err) return callback(err); - if (trace) trace("remove", null, hash); - return callback(null, hash); + function checkBranch(err, hash) { + if (err) return callback(err); + if (hash) { + return resolveHashish(hash, callback); } + return db.read("refs/heads/" + hashish, checkTag); } - function resolveHashish(hashish, callback) { - if (!callback) return resolveHashish.bind(this, hashish); - hashish = hashish.trim(); - if ((/^[0-9a-f]{40}$/i).test(hashish)) { - return callback(null, hashish.toLowerCase()); + function checkTag(err, hash) { + if (err) return callback(err); + if (hash) { + return resolveHashish(hash, callback); } - if (hashish === "HEAD") return getHead(onBranch); - if ((/^refs\//).test(hashish)) { - return db.read(hashish, checkBranch); - } - return checkBranch(); + return db.read("refs/tags/" + hashish, final); + } - function onBranch(err, ref) { - if (err) return callback(err); - return resolveHashish(ref, callback); + function final(err, hash) { + if (err) return callback(err); + if (hash) { + return resolveHashish(hash, callback); } + return callback(new Error("Cannot find hashish: " + hashish)); + } + } - function checkBranch(err, hash) { - if (err) return callback(err); - if (hash) { - return resolveHashish(hash, callback); - } - return db.read("refs/heads/" + hashish, checkTag); - } + function updateHead(hash, callback) { + if (!callback) return updateHead.bind(this, hash); + var ref; + return getHead(onBranch); - function checkTag(err, hash) { - if (err) return callback(err); - if (hash) { - return resolveHashish(hash, callback); - } - return db.read("refs/tags/" + hashish, final); - } + function onBranch(err, result) { + if (err) return callback(err); + ref = result; + return db.write(ref, hash + "\n", callback); + } + } - function final(err, hash) { - if (err) return callback(err); - if (hash) { - return resolveHashish(hash, callback); - } - return callback(new Error("Cannot find hashish: " + hashish)); - } + function getHead(callback) { + if (!callback) return getHead.bind(this); + return db.read("HEAD", onRead); + + function onRead(err, ref) { + if (err) return callback(err); + if (!ref) return callback(new Error("Missing HEAD")); + var match = ref.match(/^ref: *(.*)/); + if (!match) return callback(new Error("Invalid HEAD")); + return callback(null, match[1]); } + } - function updateHead(hash, callback) { - if (!callback) return updateHead.bind(this, hash); - var ref; - return getHead(onBranch); + function setHead(branchName, callback) { + if (!callback) return setHead.bind(this, branchName); + var ref = "refs/heads/" + branchName; + return db.write("HEAD", "ref: " + ref + "\n", callback); + } - function onBranch(err, result) { - if (err) return callback(err); - ref = result; - return db.write(ref, hash + "\n", callback); + function createRef(ref, hash, callback) { + if (!callback) return createRef.bind(this, ref, hash); + return db.write(ref, hash + "\n", callback); + } + + function deleteRef(ref, callback) { + if (!callback) return deleteRef.bind(this, ref); + return db.unlink(ref, callback); + } + + function listRefs(prefix, callback) { + if (!callback) return listRefs.bind(this, prefix); + var branches = {}, list = [], target = prefix; + return db.readdir(target, onNames); + + function onNames(err, names) { + if (err) { + if (err.code === "ENOENT") return shift(); + return callback(err); } + for (var i = 0, l = names.length; i < l; ++i) { + list.push(target + "/" + names[i]); + } + return shift(); } - function getHead(callback) { - if (!callback) return getHead.bind(this); - return db.read("HEAD", onRead); + function shift(err) { + if (err) return callback(err); + target = list.shift(); + if (!target) return callback(null, branches); + return db.read(target, onRead); + } - function onRead(err, ref) { - if (err) return callback(err); - if (!ref) return callback(new Error("Missing HEAD")); - var match = ref.match(/^ref: *(.*)/); - if (!match) return callback(new Error("Invalid HEAD")); - return callback(null, match[1]); + function onRead(err, hash) { + if (err) { + if (err.code === "EISDIR") return db.readdir(target, onNames); + return callback(err); } + if (hash) { + branches[target] = hash.trim(); + return shift(); + } + return db.readdir(target, onNames); } + } - function setHead(branchName, callback) { - if (!callback) return setHead.bind(this, branchName); - var ref = "refs/heads/" + branchName; - return db.write("HEAD", "ref: " + ref + "\n", callback); + function indexOf(buffer, byte, i) { + i |= 0; + var length = buffer.length; + for (;;i++) { + if (i >= length) return -1; + if (buffer[i] === byte) return i; } + } - function createRef(ref, hash, callback) { - if (!callback) return createRef.bind(this, ref, hash); - return db.write(ref, hash + "\n", callback); + function parseAscii(buffer, start, end) { + var val = ""; + while (start < end) { + val += String.fromCharCode(buffer[start++]); } + return val; + } - function deleteRef(ref, callback) { - if (!callback) return deleteRef.bind(this, ref); - return db.unlink(ref, callback); + function parseDec(buffer, start, end) { + var val = 0; + while (start < end) { + val = val * 10 + buffer[start++] - 0x30; } + return val; + } - function listRefs(prefix, callback) { - if (!callback) return listRefs.bind(this, prefix); - var branches = {}, list = [], target = prefix; - return db.readdir(target, onNames); + function parseOct(buffer, start, end) { + var val = 0; + while (start < end) { + val = (val << 3) + buffer[start++] - 0x30; + } + return val; + } - function onNames(err, names) { - if (err) { - if (err.code === "ENOENT") return shift(); - return callback(err); - } - for (var i = 0, l = names.length; i < l; ++i) { - list.push(target + "/" + names[i]); - } - return shift(); - } + function deframe(buffer) { + var space = indexOf(buffer, 0x20); + if (space < 0) throw new Error("Invalid git object buffer"); + var nil = indexOf(buffer, 0x00, space); + if (nil < 0) throw new Error("Invalid git object buffer"); + var body = bops.subarray(buffer, nil + 1); + var size = parseDec(buffer, space + 1, nil); + if (size !== body.length) throw new Error("Invalid body length."); + return [ + parseAscii(buffer, 0, space), + body + ]; + } - function shift(err) { - if (err) return callback(err); - target = list.shift(); - if (!target) return callback(null, branches); - return db.read(target, onRead); - } + function frame(type, body) { + return bops.join([ + bops.from(type + " " + body.length + "\0"), + body + ]); + } - function onRead(err, hash) { - if (err) { - if (err.code === "EISDIR") return db.readdir(target, onNames); - return callback(err); - } - if (hash) { - branches[target] = hash.trim(); - return shift(); - } - return db.readdir(target, onNames); - } + // A sequence of bytes not containing the ASCII character byte + // values NUL (0x00), LF (0x0a), '<' (0c3c), or '>' (0x3e). + // The sequence may not begin or end with any bytes with the + // following ASCII character byte values: SPACE (0x20), + // '.' (0x2e), ',' (0x2c), ':' (0x3a), ';' (0x3b), '<' (0x3c), + // '>' (0x3e), '"' (0x22), "'" (0x27). + function safe(string) { + return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); + } + + function formatDate(date) { + var timezone = (date.timeZoneoffset || date.getTimezoneOffset()) / 60; + var seconds = Math.floor(date.getTime() / 1000); + return seconds + " " + (timezone > 0 ? "-0" : "0") + timezone + "00"; + } + + function encodePerson(person) { + if (!person.name || !person.email) { + throw new TypeError("Name and email are required for person fields"); } + return safe(person.name) + + " <" + safe(person.email) + "> " + + formatDate(person.date || new Date()); + } - function indexOf(buffer, byte, i) { - i |= 0; - var length = buffer.length; - for (;;i++) { - if (i >= length) return -1; - if (buffer[i] === byte) return i; - } + function encodeCommit(commit) { + if (!commit.tree || !commit.author || !commit.message) { + throw new TypeError("Tree, author, and message are require for commits"); } + var parents = commit.parents || (commit.parent ? [ commit.parent ] : []); + if (!Array.isArray(parents)) { + throw new TypeError("Parents must be an array"); + } + var str = "tree " + commit.tree; + for (var i = 0, l = parents.length; i < l; ++i) { + str += "\nparent " + parents[i]; + } + str += "\nauthor " + encodePerson(commit.author) + + "\ncommitter " + encodePerson(commit.committer || commit.author) + + "\n\n" + commit.message; + return bops.from(str); + } - function parseAscii(buffer, start, end) { - var val = ""; - while (start < end) { - val += String.fromCharCode(buffer[start++]); - } - return val; + function encodeTag(tag) { + if (!tag.object || !tag.type || !tag.tag || !tag.tagger || !tag.message) { + throw new TypeError("Object, type, tag, tagger, and message required"); } + var str = "object " + tag.object + + "\ntype " + tag.type + + "\ntag " + tag.tag + + "\ntagger " + encodePerson(tag.tagger) + + "\n\n" + tag.message; + return bops.from(str + "\n" + tag.message); + } - function parseDec(buffer, start, end) { - var val = 0; - while (start < end) { - val = val * 10 + buffer[start++] - 0x30; - } - return val; + function pathCmp(a, b) { + a += "/"; b += "/"; + return a < b ? -1 : a > b ? 1 : 0; + } + + function encodeTree(tree) { + var chunks = []; + Object.keys(tree).sort(pathCmp).forEach(onName); + return bops.join(chunks); + + function onName(name) { + var entry = tree[name]; + chunks.push( + bops.from(entry.mode.toString(8) + " " + name + "\0"), + bops.from(entry.hash, "hex") + ); } + } + + function encodeBlob(blob) { + if (bops.is(blob)) return blob; + return bops.from(blob); + } - function parseOct(buffer, start, end) { - var val = 0; - while (start < end) { - val = (val << 3) + buffer[start++] - 0x30; + function decodePerson(string) { + var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); + if (!match) throw new Error("Improperly formatted person string"); + var sec = parseInt(match[3], 10); + var date = new Date(sec * 1000); + date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; + return { + name: match[1], + email: match[2], + date: date + }; + } + + + function decodeCommit(body) { + var i = 0; + var start; + var key; + var parents = []; + var commit = { + tree: "", + parents: parents, + author: "", + committer: "", + message: "" + }; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = parseAscii(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = bops.to(bops.subarray(body, start, i++)); + if (key === "parent") { + parents.push(value); + } + else { + if (key === "author" || key === "committer") { + value = decodePerson(value); + } + commit[key] = value; } - return val; - } - - function deframe(buffer) { - var space = indexOf(buffer, 0x20); - if (space < 0) throw new Error("Invalid git object buffer"); - var nil = indexOf(buffer, 0x00, space); - if (nil < 0) throw new Error("Invalid git object buffer"); - var body = bops.subarray(buffer, nil + 1); - var size = parseDec(buffer, space + 1, nil); - if (size !== body.length) throw new Error("Invalid body length."); - return [ - parseAscii(buffer, 0, space), - body - ]; - } - - function frame(type, body) { - return bops.join([ - bops.from(type + " " + body.length + "\0"), - body - ]); } + i++; + commit.message = bops.to(bops.subarray(body, i)); + return commit; + } + + function decodeTag(body) { + var i = 0; + var start; + var key; + var tag = {}; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = parseAscii(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = bops.to(bops.subarray(body, start, i++)); + if (key === "tagger") value = decodePerson(value); + tag[key] = value; + } + i++; + tag.message = bops.to(bops.subarray(body, i)); + return tag; + } + + function decodeTree(body) { + var i = 0; + var length = body.length; + var start; + var mode; + var name; + var hash; + var tree = []; + while (i < length) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + mode = parseOct(body, start, i++); + start = i; + i = indexOf(body, 0x00, start); + name = bops.to(bops.subarray(body, start, i++)); + hash = bops.to(bops.subarray(body, i, i += 20), "hex"); + tree.push({ + mode: mode, + name: name, + hash: hash + }); + } + return tree; + } + + function decodeBlob(body) { + return body; + } + + function lsRemote(remote, callback) { + if (!callback) return lsRemote.bind(this, remote); + var refs; + return remote.discover(onDiscover); - // A sequence of bytes not containing the ASCII character byte - // values NUL (0x00), LF (0x0a), '<' (0c3c), or '>' (0x3e). - // The sequence may not begin or end with any bytes with the - // following ASCII character byte values: SPACE (0x20), - // '.' (0x2e), ',' (0x2c), ':' (0x3a), ';' (0x3b), '<' (0x3c), - // '>' (0x3e), '"' (0x22), "'" (0x27). - function safe(string) { - return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); + function onDiscover(err, result) { + if (err) return callback(err); + refs = result; + return remote.close(onClose); } - function formatDate(date) { - var timezone = (date.timeZoneoffset || date.getTimezoneOffset()) / 60; - var seconds = Math.floor(date.getTime() / 1000); - return seconds + " " + (timezone > 0 ? "-0" : "0") + timezone + "00"; + function onClose(err) { + if (err) return callback(err); + return callback(null, refs); } + } - function encodePerson(person) { - if (!person.name || !person.email) { - throw new TypeError("Name and email are required for person fields"); - } - return safe(person.name) + - " <" + safe(person.email) + "> " + - formatDate(person.date || new Date()); + function fetch(remote, opts, callback) { + if (!callback) return fetch.bind(this, remote, opts); + var refs, branch, queue, ref, hash; + return remote.discover(onDiscover); + + function onDiscover(err, serverRefs, serverCaps) { + if (err) return callback(err); + refs = serverRefs; + opts.caps = processCaps(opts, serverCaps); + return processWants(refs, opts.want, onWants); } - function encodeCommit(commit) { - if (!commit.tree || !commit.author || !commit.message) { - throw new TypeError("Tree, author, and message are require for commits"); - } - var parents = commit.parents || (commit.parent ? [ commit.parent ] : []); - if (!Array.isArray(parents)) { - throw new TypeError("Parents must be an array"); - } - var str = "tree " + commit.tree; - for (var i = 0, l = parents.length; i < l; ++i) { - str += "\nparent " + parents[i]; - } - str += "\nauthor " + encodePerson(commit.author) + - "\ncommitter " + encodePerson(commit.committer || commit.author) + - "\n\n" + commit.message; - return bops.from(str); + function onWants(err, wants) { + if (err) return callback(err); + opts.wants = wants; + return remote.fetch(repo, opts, onPackStream); } - function encodeTag(tag) { - if (!tag.object || !tag.type || !tag.tag || !tag.tagger || !tag.message) { - throw new TypeError("Object, type, tag, tagger, and message required"); - } - var str = "object " + tag.object + - "\ntype " + tag.type + - "\ntag " + tag.tag + - "\ntagger " + encodePerson(tag.tagger) + - "\n\n" + tag.message; - return bops.from(str + "\n" + tag.message); - } - - function pathCmp(a, b) { - a += "/"; b += "/"; - return a < b ? -1 : a > b ? 1 : 0; - } - - function encodeTree(tree) { - var chunks = []; - Object.keys(tree).sort(pathCmp).forEach(onName); - return bops.join(chunks); - - function onName(name) { - var entry = tree[name]; - chunks.push( - bops.from(entry.mode.toString(8) + " " + name + "\0"), - bops.from(entry.hash, "hex") - ); - } + function onPackStream(err, raw) { + if (err) return callback(err); + if (!raw) return remote.close(callback); + var packStream = parse(raw); + return unpack(packStream, opts, onUnpack); } - function encodeBlob(blob) { - if (bops.is(blob)) return blob; - return bops.from(blob); - } - - function decodePerson(string) { - var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); - if (!match) throw new Error("Improperly formatted person string"); - var sec = parseInt(match[3], 10); - var date = new Date(sec * 1000); - date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; - return { - name: match[1], - email: match[2], - date: date - }; - } - - - function decodeCommit(body) { - var i = 0; - var start; - var key; - var parents = []; - var commit = { - tree: "", - parents: parents, - author: "", - committer: "", - message: "" - }; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = bops.to(bops.subarray(body, start, i++)); - if (key === "parent") { - parents.push(value); - } - else { - if (key === "author" || key === "committer") { - value = decodePerson(value); - } - commit[key] = value; - } - } - i++; - commit.message = bops.to(bops.subarray(body, i)); - return commit; - } - - function decodeTag(body) { - var i = 0; - var start; - var key; - var tag = {}; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = bops.to(bops.subarray(body, start, i++)); - if (key === "tagger") value = decodePerson(value); - tag[key] = value; - } - i++; - tag.message = bops.to(bops.subarray(body, i)); - return tag; + function onUnpack(err) { + if (err) return callback(err); + return remote.close(onClose); } - function decodeTree(body) { - var i = 0; - var length = body.length; - var start; - var mode; - var name; - var hash; - var tree = []; - while (i < length) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - mode = parseOct(body, start, i++); - start = i; - i = indexOf(body, 0x00, start); - name = bops.to(bops.subarray(body, start, i++)); - hash = bops.to(bops.subarray(body, i, i += 20), "hex"); - tree.push({ - mode: mode, - name: name, - hash: hash - }); - } - return tree; + function onClose(err) { + if (err) return callback(err); + queue = Object.keys(refs); + return next(); } - function decodeBlob(body) { - return body; + function next(err) { + if (err) return callback(err); + ref = queue.shift(); + if (!ref) return setHead(branch, callback); + if (ref === "HEAD" || /{}$/.test(ref)) return next(); + hash = refs[ref]; + if (!branch && (hash === refs.HEAD)) branch = ref.substr(11); + db.has(hash, onHas); } - function lsRemote(remote, callback) { - if (!callback) return lsRemote.bind(this, remote); - var refs; - return remote.discover(onDiscover); + function onHas(err, has) { + if (err) return callback(err); + if (!has) return next(); + return db.write(ref, hash + "\n", next); + } + } - function onDiscover(err, result) { - if (err) return callback(err); - refs = result; - return remote.close(onClose); + function processCaps(opts, serverCaps) { + var caps = []; + if (serverCaps["ofs-delta"]) caps.push("ofs-delta"); + if (serverCaps["thin-pack"]) caps.push("thin-pack"); + if (opts.includeTag && serverCaps["include-tag"]) caps.push("include-tag"); + if ((opts.onProgress || opts.onError) && + (serverCaps["side-band-64k"] || serverCaps["side-band"])) { + caps.push(serverCaps["side-band-64k"] ? "side-band-64k" : "side-band"); + if (!opts.onProgress && serverCaps["no-progress"]) { + caps.push("no-progress"); } + } + if (serverCaps.agent) caps.push("agent=" + platform.agent); + return caps; + } - function onClose(err) { - if (err) return callback(err); - return callback(null, refs); - } + // Possible values for `filter` + // "HEAD" - fetch whatever the remote head is + // "refs/heads/master - ref + // ["refs/heads/master"] - list of refs + // "master" - branch + // ["master"] - list of branches + // "0.0.1" - tag + // ["0.0.1"] - list of tags + // function (ref, callback) { callback(null, true); } - interactive + // true - Fetch all remote refs. + function processWants(refs, filter, callback) { + if (filter === null || filter === undefined) { + return defaultWants(refs, callback); + } + filter = Array.isArray(filter) ? arrayFilter(filter) : + typeof filter === "function" ? filter = filter : + wantFilter(filter); + + var list = Object.keys(refs); + var wants = {}; + var ref, hash; + return shift(); + function shift() { + ref = list.shift(); + if (!ref) return callback(null, Object.keys(wants)); + hash = refs[ref]; + resolveHashish(ref, onResolve); + } + function onResolve(err, oldHash) { + // Skip refs we already have + if (hash === oldHash) return shift(); + filter(ref, onFilter); + } + function onFilter(err, want) { + if (err) return callback(err); + // Skip refs the user doesn't want + if (want) wants[hash] = true; + return shift(); } + } - function fetch(remote, opts, callback) { - if (!callback) return fetch.bind(this, remote, opts); - var refs, branch, queue, ref, hash; - return remote.discover(onDiscover); + function defaultWants(refs, callback) { + return listRefs("refs/heads", onRefs); - function onDiscover(err, serverRefs, serverCaps) { - if (err) return callback(err); - refs = serverRefs; - opts.caps = processCaps(opts, serverCaps); - return processWants(refs, opts.want, onWants); - } + function onRefs(err, branches) { + if (err) return callback(err); + var wants = Object.keys(branches); + wants.unshift("HEAD"); + return processWants(refs, wants, callback); + } + } - function onWants(err, wants) { - if (err) return callback(err); - opts.wants = wants; - return remote.fetch(repo, opts, onPackStream); - } + function wantMatch(ref, want) { + if (want === "HEAD" || want === null || want === undefined) { + return ref === "HEAD"; + } + if (Object.prototype.toString.call(want) === '[object RegExp]') { + return want.test(ref); + } + if (typeof want === "boolean") return want; + if (typeof want !== "string") { + throw new TypeError("Invalid want type: " + typeof want); + } + return (/^refs\//.test(ref) && ref === want) || + (ref === "refs/heads/" + want) || + (ref === "refs/tags/" + want); + } - function onPackStream(err, raw) { - if (err) return callback(err); - if (!raw) return remote.close(callback); - var packStream = parse(raw); - return unpack(packStream, opts, onUnpack); + function wantFilter(want) { + return function (ref, callback) { + var result; + try { + result = wantMatch(ref, want); } - - function onUnpack(err) { - if (err) return callback(err); - return remote.close(onClose); + catch (err) { + return callback(err); } + return callback(null, result); + }; + } - function onClose(err) { - if (err) return callback(err); - queue = Object.keys(refs); - return next(); + function arrayFilter(want) { + var length = want.length; + return function (ref, callback) { + var result; + try { + for (var i = 0; i < length; ++i) { + if (result = wantMatch(ref, want[i])) break; + } } - - function next(err) { - if (err) return callback(err); - ref = queue.shift(); - if (!ref) return setHead(branch, callback); - if (ref === "HEAD" || /{}$/.test(ref)) return next(); - hash = refs[ref]; - if (!branch && (hash === refs.HEAD)) branch = ref.substr(11); - db.has(hash, onHas); + catch (err) { + return callback(err); } + return callback(null, result); + }; + } - function onHas(err, has) { - if (err) return callback(err); - if (!has) return next(); - return db.write(ref, hash + "\n", next); - } + function push() { + throw new Error("TODO: Implement repo.fetch"); + } + + function unpack(packStream, opts, callback) { + if (!callback) return unpack.bind(this, packStream, opts); + // TODO: save the stream to the local repo. + var version, num, count = 0, deltas = 0, done; + + // hashes keyed by offset + var hashes = {}; + var seen = {}; + var toDelete = {}; + var pending = {}; + var queue = []; + + return packStream.read(onStats); + + function onDone(err) { + if (done) return; + done = true; + return callback(err); } - function processCaps(opts, serverCaps) { - var caps = []; - if (serverCaps["ofs-delta"]) caps.push("ofs-delta"); - if (serverCaps["thin-pack"]) caps.push("thin-pack"); - if (opts.includeTag && serverCaps["include-tag"]) caps.push("include-tag"); - if ((opts.onProgress || opts.onError) && - (serverCaps["side-band-64k"] || serverCaps["side-band"])) { - caps.push(serverCaps["side-band-64k"] ? "side-band-64k" : "side-band"); - if (!opts.onProgress && serverCaps["no-progress"]) { - caps.push("no-progress"); - } + function onStats(err, stats) { + if (err) return onDone(err); + version = stats.version; + num = stats.num; + packStream.read(onRead); + } + + function onRead(err, item) { + if (err) return onDone(err); + if (opts.onProgress) { + var percent = Math.round(count / num * 100); + opts.onProgress("Receiving objects: " + percent + "% (" + count + "/" + num + ") " + (item ? "\r" : "\n")); + count++; } - if (serverCaps.agent) caps.push("agent=" + platform.agent); - return caps; - } - - // Possible values for `filter` - // "HEAD" - fetch whatever the remote head is - // "refs/heads/master - ref - // ["refs/heads/master"] - list of refs - // "master" - branch - // ["master"] - list of branches - // "0.0.1" - tag - // ["0.0.1"] - list of tags - // function (ref, callback) { callback(null, true); } - interactive - // true - Fetch all remote refs. - function processWants(refs, filter, callback) { - if (filter === null || filter === undefined) { - return defaultWants(refs, callback); + if (item === undefined) { + hashes = null; + count = 0; + return checkExisting(); } - filter = Array.isArray(filter) ? arrayFilter(filter) : - typeof filter === "function" ? filter = filter : - wantFilter(filter); - - var list = Object.keys(refs); - var wants = {}; - var ref, hash; - return shift(); - function shift() { - ref = list.shift(); - if (!ref) return callback(null, Object.keys(wants)); - hash = refs[ref]; - resolveHashish(ref, onResolve); + if (item.size !== item.body.length) { + return onDone(new Error("Body size mismatch")); } - function onResolve(err, oldHash) { - // Skip refs we already have - if (hash === oldHash) return shift(); - filter(ref, onFilter); + var buffer = bops.join([ + bops.from(item.type + " " + item.size + "\0"), + item.body + ]); + var hash = sha1(buffer); + hashes[item.offset] = hash; + var ref = item.ref; + if (ref !== undefined) { + deltas++; + if (item.type === "ofs-delta") { + ref = hashes[item.offset - ref]; + } + var list = pending[ref]; + if (list) list.push(hash); + else pending[ref] = [hash]; + toDelete[hash] = true; } - function onFilter(err, want) { - if (err) return callback(err); - // Skip refs the user doesn't want - if (want) wants[hash] = true; - return shift(); + else { + seen[hash] = true; } - } - - function defaultWants(refs, callback) { - return listRefs("refs/heads", onRefs); - function onRefs(err, branches) { - if (err) return callback(err); - var wants = Object.keys(branches); - wants.unshift("HEAD"); - return processWants(refs, wants, callback); - } + db.save(hash, buffer, function (err) { + if (err) return onDone(err); + if (trace) trace("save", null, hash); + packStream.read(onRead); + }); } - function wantMatch(ref, want) { - if (want === "HEAD" || want === null || want === undefined) { - return ref === "HEAD"; - } - if (Object.prototype.toString.call(want) === '[object RegExp]') { - return want.test(ref); + function checkExisting() { + var list = Object.keys(pending); + var hash; + return pop(); + function pop() { + hash = list.pop(); + if (!hash) return applyDeltas(); + if (toDelete[hash]) return pop(); + return db.has(hash, onHas); } - if (typeof want === "boolean") return want; - if (typeof want !== "string") { - throw new TypeError("Invalid want type: " + typeof want); + function onHas(err, has) { + if (err) return onDone(err); + if (has) seen[hash] = true; + return pop(); } - return (/^refs\//.test(ref) && ref === want) || - (ref === "refs/heads/" + want) || - (ref === "refs/tags/" + want); } - function wantFilter(want) { - return function (ref, callback) { - var result; - try { - result = wantMatch(ref, want); - } - catch (err) { - return callback(err); - } - return callback(null, result); - }; - } - - function arrayFilter(want) { - var length = want.length; - return function (ref, callback) { - var result; - try { - for (var i = 0; i < length; ++i) { - if (result = wantMatch(ref, want[i])) break; - } + function applyDeltas() { + Object.keys(pending).forEach(function (ref) { + if (seen[ref]) { + pending[ref].forEach(function (hash) { + queue.push({hash:hash,ref:ref}); + }); + delete pending[ref]; } - catch (err) { - return callback(err); - } - return callback(null, result); - }; + }); + return queue.length ? check() : cleanup(); } - function push() { - throw new Error("TODO: Implement repo.fetch"); + function deltaProgress() { + var percent = Math.round(count / deltas * 100); + return "Applying deltas: " + percent + "% (" + count++ + "/" + deltas + ") "; } - function unpack(packStream, opts, callback) { - if (!callback) return unpack.bind(this, packStream, opts); - // TODO: save the stream to the local repo. - var version, num, count = 0, deltas = 0, done; - - // hashes keyed by offset - var hashes = {}; - var seen = {}; - var toDelete = {}; - var pending = {}; - var queue = []; - - return packStream.read(onStats); - - function onDone(err) { - if (done) return; - done = true; - return callback(err); + function check() { + var item = queue.pop(); + var target, delta; + if (!item) return applyDeltas(); + if (opts.onProgress) { + opts.onProgress(deltaProgress() + "\r"); } + db.load(item.ref, onTarget); + db.load(item.hash, onDelta); + return; - function onStats(err, stats) { + function onTarget(err, raw) { if (err) return onDone(err); - version = stats.version; - num = stats.num; - packStream.read(onRead); + target = deframe(raw); + if (delta) return onPair(item, target, delta); } - function onRead(err, item) { + function onDelta(err, raw) { if (err) return onDone(err); - if (opts.onProgress) { - var percent = Math.round(count / num * 100); - opts.onProgress("Receiving objects: " + percent + "% (" + count + "/" + num + ") " + (item ? "\r" : "\n")); - count++; - } - if (item === undefined) { - hashes = null; - count = 0; - return checkExisting(); - } - if (item.size !== item.body.length) { - return onDone(new Error("Body size mismatch")); - } - var buffer = bops.join([ - bops.from(item.type + " " + item.size + "\0"), - item.body - ]); - var hash = sha1(buffer); - hashes[item.offset] = hash; - var ref = item.ref; - if (ref !== undefined) { - deltas++; - if (item.type === "ofs-delta") { - ref = hashes[item.offset - ref]; - } - var list = pending[ref]; - if (list) list.push(hash); - else pending[ref] = [hash]; - toDelete[hash] = true; - } - else { - seen[hash] = true; - } - - db.save(hash, buffer, function (err) { - if (err) return onDone(err); - if (trace) trace("save", null, hash); - packStream.read(onRead); - }); - } - - function checkExisting() { - var list = Object.keys(pending); - var hash; - return pop(); - function pop() { - hash = list.pop(); - if (!hash) return applyDeltas(); - if (toDelete[hash]) return pop(); - return db.has(hash, onHas); - } - function onHas(err, has) { - if (err) return onDone(err); - if (has) seen[hash] = true; - return pop(); - } - } - - function applyDeltas() { - Object.keys(pending).forEach(function (ref) { - if (seen[ref]) { - pending[ref].forEach(function (hash) { - queue.push({hash:hash,ref:ref}); - }); - delete pending[ref]; - } - }); - return queue.length ? check() : cleanup(); - } - - function deltaProgress() { - var percent = Math.round(count / deltas * 100); - return "Applying deltas: " + percent + "% (" + count++ + "/" + deltas + ") "; + delta = deframe(raw); + if (target) return onPair(item, target, delta); } + } - function check() { - var item = queue.pop(); - var target, delta; - if (!item) return applyDeltas(); - if (opts.onProgress) { - opts.onProgress(deltaProgress() + "\r"); - } - db.load(item.ref, onTarget); - db.load(item.hash, onDelta); - return; - - function onTarget(err, raw) { - if (err) return onDone(err); - target = deframe(raw); - if (delta) return onPair(item, target, delta); - } + function onPair(item, target, delta) { + var buffer = frame(target[0], applyDelta(delta[1], target[1])); + var hash = sha1(buffer); + db.save(hash, buffer, onSave); - function onDelta(err, raw) { - if (err) return onDone(err); - delta = deframe(raw); - if (target) return onPair(item, target, delta); + function onSave(err) { + if (err) return onDone(err); + var deps = pending[item.hash]; + if (deps) { + pending[hash] = deps; + delete pending[item.hash]; } + seen[hash] = true; + return check(); } + } - function onPair(item, target, delta) { - var buffer = frame(target[0], applyDelta(delta[1], target[1])); - var hash = sha1(buffer); - db.save(hash, buffer, onSave); - - function onSave(err) { - if (err) return onDone(err); - var deps = pending[item.hash]; - if (deps) { - pending[hash] = deps; - delete pending[item.hash]; - } - seen[hash] = true; - return check(); - } + function cleanup() { + if (opts.onProgress && deltas) { + opts.onProgress(deltaProgress() + "\n"); } - - function cleanup() { - if (opts.onProgress && deltas) { - opts.onProgress(deltaProgress() + "\n"); - } - var hashes = Object.keys(toDelete); - next(); - function next(err) { - if (err) return onDone(err); - var hash = hashes.pop(); - if (!hash) return onDone(); - remove(hash, next); - } + var hashes = Object.keys(toDelete); + next(); + function next(err) { + if (err) return onDone(err); + var hash = hashes.pop(); + if (!hash) return onDone(); + remove(hash, next); } } } -}; \ No newline at end of file +} From 2ccef3dc88d1d1105a4b53268208076ed16fb9f1 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 16:16:36 -0500 Subject: [PATCH 005/256] Clean up structure some more --- js-git.js | 84 ++++++++++++++++++++++--------------------------------- 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/js-git.js b/js-git.js index 7f95676..6e16f44 100644 --- a/js-git.js +++ b/js-git.js @@ -1,5 +1,5 @@ var platform; -var applyDelta, pushToPull, parse; +var applyDelta, pushToPull, parse, sha1, bops; module.exports = function (imports) { if (platform) return newRepo; @@ -9,15 +9,13 @@ module.exports = function (imports) { pushToPull = require('push-to-pull'); parse = pushToPull(require('git-pack-codec/decode.js')(platform)); platform.agent = platform.agent || "js-git/" + require('./package.json').version; + sha1 = platform.sha1; + bops = platform.bops; return newRepo; }; -// platform options are: db, proto, and trace function newRepo(db, workDir) { - var trace = platform.trace; - var sha1 = platform.sha1; - var bops = platform.bops; var encoders = { commit: encodeCommit, @@ -59,7 +57,6 @@ function newRepo(db, workDir) { } // Network Protocols - repo.lsRemote = lsRemote; if (db) { repo.fetch = fetch; @@ -96,7 +93,6 @@ function newRepo(db, workDir) { } catch (err) { if (err) return callback(err); } - if (trace) trace("load", null, hash); return callback(null, object, hash); } } @@ -116,7 +112,6 @@ function newRepo(db, workDir) { function onSave(err) { if (err) return callback(err); - if (trace) trace("save", null, hash); return callback(null, hash); } } @@ -147,13 +142,7 @@ function newRepo(db, workDir) { function onHash(err, result) { if (err) return callback(err); hash = result; - return db.remove(hash, onRemove); - } - - function onRemove(err) { - if (err) return callback(err); - if (trace) trace("remove", null, hash); - return callback(null, hash); + return db.remove(hash, callback); } } @@ -597,16 +586,6 @@ function newRepo(db, workDir) { return caps; } - // Possible values for `filter` - // "HEAD" - fetch whatever the remote head is - // "refs/heads/master - ref - // ["refs/heads/master"] - list of refs - // "master" - branch - // ["master"] - list of branches - // "0.0.1" - tag - // ["0.0.1"] - list of tags - // function (ref, callback) { callback(null, true); } - interactive - // true - Fetch all remote refs. function processWants(refs, filter, callback) { if (filter === null || filter === undefined) { return defaultWants(refs, callback); @@ -666,7 +645,8 @@ function newRepo(db, workDir) { } function wantFilter(want) { - return function (ref, callback) { + return filter; + function filter(ref, callback) { var result; try { result = wantMatch(ref, want); @@ -675,12 +655,13 @@ function newRepo(db, workDir) { return callback(err); } return callback(null, result); - }; + } } function arrayFilter(want) { var length = want.length; - return function (ref, callback) { + return filter; + function filter(ref, callback) { var result; try { for (var i = 0; i < length; ++i) { @@ -691,7 +672,7 @@ function newRepo(db, workDir) { return callback(err); } return callback(null, result); - }; + } } function push() { @@ -709,6 +690,7 @@ function newRepo(db, workDir) { var toDelete = {}; var pending = {}; var queue = []; + var list, current; return packStream.read(onStats); @@ -761,28 +743,30 @@ function newRepo(db, workDir) { seen[hash] = true; } - db.save(hash, buffer, function (err) { - if (err) return onDone(err); - if (trace) trace("save", null, hash); - packStream.read(onRead); - }); + return db.save(hash, buffer, onSave); + } + + function onSave(err) { + if (err) return callback(err); + packStream.read(onRead); } function checkExisting() { - var list = Object.keys(pending); - var hash; - return pop(); - function pop() { - hash = list.pop(); - if (!hash) return applyDeltas(); - if (toDelete[hash]) return pop(); - return db.has(hash, onHas); - } - function onHas(err, has) { - if (err) return onDone(err); - if (has) seen[hash] = true; - return pop(); - } + list = Object.keys(pending); + return popPending(); + } + + function popPending() { + current = list.pop(); + if (!current) return applyDeltas(); + if (toDelete[current]) return popPending(); + return db.has(current, onHas); + } + + function onHas(err, has) { + if (err) return onDone(err); + if (has) seen[current] = true; + return popPending(); } function applyDeltas() { @@ -829,9 +813,9 @@ function newRepo(db, workDir) { function onPair(item, target, delta) { var buffer = frame(target[0], applyDelta(delta[1], target[1])); var hash = sha1(buffer); - db.save(hash, buffer, onSave); + db.save(hash, buffer, onSaveConbined); - function onSave(err) { + function onSaveConbined(err) { if (err) return onDone(err); var deps = pending[item.hash]; if (deps) { From b5fd73120a22159b86fd31e755658f04cbe9b26c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 16:26:51 -0500 Subject: [PATCH 006/256] Centralize db tracing --- js-git.js | 43 ++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/js-git.js b/js-git.js index 6e16f44..1065061 100644 --- a/js-git.js +++ b/js-git.js @@ -1,5 +1,5 @@ var platform; -var applyDelta, pushToPull, parse, sha1, bops; +var applyDelta, pushToPull, parse, sha1, bops, trace; module.exports = function (imports) { if (platform) return newRepo; @@ -11,6 +11,7 @@ module.exports = function (imports) { platform.agent = platform.agent || "js-git/" + require('./package.json').version; sha1 = platform.sha1; bops = platform.bops; + trace = platform.trace; return newRepo; }; @@ -34,6 +35,20 @@ function newRepo(db, workDir) { var repo = {}; if (db) { + + if (trace) { + db = { + load: wrap1("load", db.load), + save: wrap2("save", db.save), + has: wrap1("has", db.has), + remove: wrap1("remove", db.remove), + read: wrap1("read", db.read), + write: wrap2("write", db.write), + unlink: wrap1("unlink", db.unlink), + readdir: wrap1("readdir", db.readdir) + }; + } + // Git Objects repo.load = load; // (hashish) -> object repo.save = save; // (object) -> hash @@ -65,6 +80,32 @@ function newRepo(db, workDir) { return repo; + function wrap1(type, fn) { + return one; + function one(arg, callback) { + if (!callback) return one.bind(this, arg); + return fn.call(this, arg, check); + function check(err) { + if (err) return callback(err); + trace(type, null, arg); + return callback.apply(this, arguments); + } + } + } + + function wrap2(type, fn) { + return two; + function two(arg1, arg2, callback) { + if (!callback) return two.bind(this, arg1. arg2); + return fn.call(this, arg1, arg2, check); + function check(err) { + if (err) return callback(err); + trace(type, null, arg1); + return callback.apply(this, arguments); + } + } + } + function load(hashish, callback) { if (!callback) return load.bind(this, hashish); var hash; diff --git a/package.json b/package.json index c207e09..f2a76d4 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "git://github.com/creationix/js-git.git" }, "devDependencies": { - "git-fs-db": "~0.1.0", + "git-fs-db": "~0.1.1", "git-node-platform": "~0.1.2", "git-net": "~0.0.1", "gen-run": "~0.1.1" From baa71b2b756ebe5232e651fd3578854e6a346cf9 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 16:40:36 -0500 Subject: [PATCH 007/256] Stub out walk helpers --- js-git.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/js-git.js b/js-git.js index 1065061..a77087f 100644 --- a/js-git.js +++ b/js-git.js @@ -40,8 +40,8 @@ function newRepo(db, workDir) { db = { load: wrap1("load", db.load), save: wrap2("save", db.save), - has: wrap1("has", db.has), remove: wrap1("remove", db.remove), + has: wrap1("has", db.has), read: wrap1("read", db.read), write: wrap2("write", db.write), unlink: wrap1("unlink", db.unlink), @@ -57,6 +57,11 @@ function newRepo(db, workDir) { repo.remove = remove; // (hashish) repo.unpack = unpack; // (opts, packStream) + // Convenience Readers + repo.log = log; // (hashish) -> stream + repo.tree = tree; // (hashish) -> stream + repo.walk = walk; // (hashish, scan, compare) -> stream + // Refs repo.resolveHashish = resolveHashish; // (hashish) -> hash repo.updateHead = updateHead; // (hash) @@ -106,6 +111,35 @@ function newRepo(db, workDir) { } } + function log(hashish, callback) { + return walk(hashish, logScan, logCompare, callback); + } + + function logScan(object, callback) { + throw new Error("TODO: Implement logScan"); + } + + function logCompare(obj1, obj2, callback) { + throw new Error("TODO: Implement logCompare"); + } + + function tree(hashish, callback) { + return walk(hashish, treeScan, treeCompare, callback); + } + + function treeScan(object, callback) { + throw new Error("TODO: Implement treeScan"); + } + + function treeCompare(obj1, obj2, callback) { + throw new Error("TODO: Implement treeCompare"); + } + + function walk(hashish, scan, compare, callback) { + if (!callback) return walk.bind(this, hashish, scan, compare); + throw new Error("TODO: Implement walk"); + } + function load(hashish, callback) { if (!callback) return load.bind(this, hashish); var hash; From 7d531389b62f76e6971a9bf246407a7f60c80580 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 17:08:47 -0500 Subject: [PATCH 008/256] Fix create examples --- examples/create-harmony.js | 2 +- examples/create.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/create-harmony.js b/examples/create-harmony.js index ab00f23..a63dd48 100644 --- a/examples/create-harmony.js +++ b/examples/create-harmony.js @@ -11,7 +11,7 @@ let repo = jsGit(fsDb(fs("test.git"))); let mock = require('./mock.js'); run(function *() { - yield repo.setBranch("master"); + yield repo.setHead("master"); console.log("Git database Initialized"); let head; diff --git a/examples/create.js b/examples/create.js index 449d1ba..2803f4b 100644 --- a/examples/create.js +++ b/examples/create.js @@ -8,7 +8,7 @@ var repo = jsGit(fsDb(fs("test.git"))); var mock = require('./mock.js'); -repo.setBranch("master", function (err) { +repo.setHead("master", function (err) { if (err) throw err; console.log("Git database Initialized"); From 3df0854d1c792bf2ee4a239991538ea0cbe45a10 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 22:37:40 -0500 Subject: [PATCH 009/256] Implement tree and history walkers --- examples/walk.js | 46 +++++++++++++++++ js-git.js | 127 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 examples/walk.js diff --git a/examples/walk.js b/examples/walk.js new file mode 100644 index 0000000..30979e6 --- /dev/null +++ b/examples/walk.js @@ -0,0 +1,46 @@ +var platform = require('git-node-platform'); +var jsGit = require('../.')(platform); +var fsDb = require('git-fs-db')(platform); +var fs = platform.fs; + +// Create a filesystem backed bare repo +var repo = jsGit(fsDb(fs("test.git"))); +repo.log("HEAD", function (err, log) { + if (err) throw err; + return log.read(onRead); + + function onRead(err, commit) { + if (err) throw err; + if (!commit) return; + logCommit(commit); + repo.tree(commit.body.tree, function (err, tree) { + if (err) throw err; + tree.read(onEntry); + function onEntry(err, entry) { + if (err) throw err; + if (!entry) { + console.log(); + return log.read(onRead); + } + logEntry(entry); + return tree.read(onEntry); + } + }); + } +}); + +function logCommit(commit) { + var author = commit.body.author; + var message = commit.body.message; + console.log("\x1B[33mcommit %s\x1B[0m", commit.hash); + console.log("Author: %s <%s>", author.name, author.email); + console.log("Date: %s", author.date); + console.log("\n \x1B[32;1m" + message.trim().split("\n").join("\x1B[0m\n \x1B[32m") + "\x1B[0m\n"); +} + +function logEntry(entry) { + // if (entry.type === "blob") { + var path = entry.path.replace(/\//g, "\x1B[1;34m/\x1B[0;34m") + "\x1B[0m"; + console.log(" %s %s", entry.hash, path); + // } +} \ No newline at end of file diff --git a/js-git.js b/js-git.js index a77087f..56f0dfa 100644 --- a/js-git.js +++ b/js-git.js @@ -112,32 +112,91 @@ function newRepo(db, workDir) { } function log(hashish, callback) { - return walk(hashish, logScan, logCompare, callback); + if (!callback) return log.bind(this, hashish); + return resolveHashish(hashish, onResolve); + function onResolve(err, hash) { + if (err) return callback(err); + var item = {hash: hash}; + return callback(null, walk(item, logScan, logCompare, true)); + } } - function logScan(object, callback) { - throw new Error("TODO: Implement logScan"); + function tree(hashish, callback) { + if (!callback) return tree.bind(this, hashish); + return resolveHashish(hashish, onResolve); + function onResolve(err, hash) { + if (err) return callback(err); + var item = {hash: hash, path: "/"}; + return callback(null, walk(item, treeScan, treeCompare)); + } } - function logCompare(obj1, obj2, callback) { - throw new Error("TODO: Implement logCompare"); - } + function walk(seed, scan, sortKey, reverse) { + var queue = []; + var seen = {}; + var working = 0, error, cb, done; + + enqueue(seed); + return {read: read, abort: abort}; + + function enqueue(item) { + if (item.hash in seen) return; + seen[item.hash] = true; + working++; + load(item.hash, function (err, object) { + if (err) { + error = err; + return check(); + } + item.type = object.type; + item.body = object.body; + var sortValue = sortKey(item); + var index = queue.length; + if (reverse) { + while (index > 0 && queue[index - 1][1] > sortValue) index--; + } + else { + while (index > 0 && queue[index - 1][1] < sortValue) index--; + } + queue.splice(index, 0, [item, sortValue]); + return check(); + }); + } - function tree(hashish, callback) { - return walk(hashish, treeScan, treeCompare, callback); - } + function check() { + if (!--working && cb) { + var callback = cb; + cb = null; + read(callback); + } + } - function treeScan(object, callback) { - throw new Error("TODO: Implement treeScan"); - } + function read(callback) { + if (cb) return callback(new Error("Only one read at a time")); + if (error) { + var err = error; + error = null; + return callback(err); + } + if (done) return callback(); + if (working) { + cb = callback; + return; + } + var next = queue.pop(); + if (!next) return abort(callback); + next = next[0]; + scan(next).forEach(enqueue); + return callback(null, next); + } - function treeCompare(obj1, obj2, callback) { - throw new Error("TODO: Implement treeCompare"); - } + function abort(callback) { + done = true; + queue = null; + seen = null; + return callback(); + } - function walk(hashish, scan, compare, callback) { - if (!callback) return walk.bind(this, hashish, scan, compare); - throw new Error("TODO: Implement walk"); } function load(hashish, callback) { @@ -917,3 +976,35 @@ function newRepo(db, workDir) { } } } + +function assertType(object, type) { + if (object.type !== type) { + throw new Error(type + " expected, but found " + object.type); + } +} + +function logScan(object) { + assertType(object, "commit"); + return object.body.parents.map(function (hash) { + return { hash: hash }; + }); +} + +function logCompare(object) { + return object.body.author.date; +} + +function treeScan(object) { + if (object.type === "blob") return []; + assertType(object, "tree"); + return object.body.map(function (entry) { + var path = object.path + entry.name; + if (entry.mode === 040000) path += "/"; + return {hash:entry.hash,path:path}; + }); +} + +function treeCompare(object) { + return object.path.toLowerCase(); +} + From aa48477120d904f840db8932d34be64a0b356b1a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 22:40:47 -0500 Subject: [PATCH 010/256] Cleanup walk example a bit --- examples/walk.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/walk.js b/examples/walk.js index 30979e6..7d686e8 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -11,7 +11,7 @@ repo.log("HEAD", function (err, log) { function onRead(err, commit) { if (err) throw err; - if (!commit) return; + if (!commit) return logEnd() logCommit(commit); repo.tree(commit.body.tree, function (err, tree) { if (err) throw err; @@ -19,7 +19,6 @@ repo.log("HEAD", function (err, log) { function onEntry(err, entry) { if (err) throw err; if (!entry) { - console.log(); return log.read(onRead); } logEntry(entry); @@ -32,15 +31,17 @@ repo.log("HEAD", function (err, log) { function logCommit(commit) { var author = commit.body.author; var message = commit.body.message; - console.log("\x1B[33mcommit %s\x1B[0m", commit.hash); + console.log("\n\x1B[33mcommit %s\x1B[0m", commit.hash); console.log("Author: %s <%s>", author.name, author.email); console.log("Date: %s", author.date); console.log("\n \x1B[32;1m" + message.trim().split("\n").join("\x1B[0m\n \x1B[32m") + "\x1B[0m\n"); } function logEntry(entry) { - // if (entry.type === "blob") { - var path = entry.path.replace(/\//g, "\x1B[1;34m/\x1B[0;34m") + "\x1B[0m"; - console.log(" %s %s", entry.hash, path); - // } + var path = entry.path.replace(/\//g, "\x1B[1;34m/\x1B[0;34m") + "\x1B[0m"; + console.log(" %s %s", entry.hash, path); +} + +function logEnd() { + console.log("\n\x1B[30;1mBeginning of History.\x1B[0m\n"); } \ No newline at end of file From dafd1c97d898689e73cd5894d791dc4a13ae4fc5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 22:49:16 -0500 Subject: [PATCH 011/256] Improve names from log/tree to logWalk/treeWalk --- examples/walk.js | 6 +++--- js-git.js | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/walk.js b/examples/walk.js index 7d686e8..d100048 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -5,15 +5,15 @@ var fs = platform.fs; // Create a filesystem backed bare repo var repo = jsGit(fsDb(fs("test.git"))); -repo.log("HEAD", function (err, log) { +repo.logWalk("HEAD", function (err, log) { if (err) throw err; return log.read(onRead); function onRead(err, commit) { if (err) throw err; - if (!commit) return logEnd() + if (!commit) return logEnd(); logCommit(commit); - repo.tree(commit.body.tree, function (err, tree) { + repo.treeWalk(commit.body.tree, function (err, tree) { if (err) throw err; tree.read(onEntry); function onEntry(err, entry) { diff --git a/js-git.js b/js-git.js index 56f0dfa..ca8730b 100644 --- a/js-git.js +++ b/js-git.js @@ -58,9 +58,9 @@ function newRepo(db, workDir) { repo.unpack = unpack; // (opts, packStream) // Convenience Readers - repo.log = log; // (hashish) -> stream - repo.tree = tree; // (hashish) -> stream - repo.walk = walk; // (hashish, scan, compare) -> stream + repo.logWalk = logWalk; // (hashish) -> stream + repo.treeWalk = treeWalk; // (hashish) -> stream + repo.walk = walk; // (hashish, scan, compare) -> stream // Refs repo.resolveHashish = resolveHashish; // (hashish) -> hash @@ -111,8 +111,8 @@ function newRepo(db, workDir) { } } - function log(hashish, callback) { - if (!callback) return log.bind(this, hashish); + function logWalk(hashish, callback) { + if (!callback) return logWalk.bind(this, hashish); return resolveHashish(hashish, onResolve); function onResolve(err, hash) { if (err) return callback(err); @@ -121,8 +121,8 @@ function newRepo(db, workDir) { } } - function tree(hashish, callback) { - if (!callback) return tree.bind(this, hashish); + function treeWalk(hashish, callback) { + if (!callback) return treeWalk.bind(this, hashish); return resolveHashish(hashish, onResolve); function onResolve(err, hash) { if (err) return callback(err); From a2f8e53bce772ef84783017f4a8756b001be5592 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 23:01:13 -0500 Subject: [PATCH 012/256] Allow passing in folder on command-line --- examples/walk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/walk.js b/examples/walk.js index d100048..0afe09a 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -4,7 +4,7 @@ var fsDb = require('git-fs-db')(platform); var fs = platform.fs; // Create a filesystem backed bare repo -var repo = jsGit(fsDb(fs("test.git"))); +var repo = jsGit(fsDb(fs(process.argv[2] || "test.git"))); repo.logWalk("HEAD", function (err, log) { if (err) throw err; return log.read(onRead); From 47c3ba68cafa73a4f7dc9f4342053029e22338a5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 23:22:38 -0500 Subject: [PATCH 013/256] Fix logWalker to not crash on shallow repos --- examples/walk.js | 9 ++++++--- js-git.js | 26 ++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/examples/walk.js b/examples/walk.js index 0afe09a..76f7f58 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -7,11 +7,13 @@ var fs = platform.fs; var repo = jsGit(fsDb(fs(process.argv[2] || "test.git"))); repo.logWalk("HEAD", function (err, log) { if (err) throw err; + var shallow; return log.read(onRead); function onRead(err, commit) { if (err) throw err; - if (!commit) return logEnd(); + if (!commit) return logEnd(shallow); + if (commit.last) shallow = true; logCommit(commit); repo.treeWalk(commit.body.tree, function (err, tree) { if (err) throw err; @@ -42,6 +44,7 @@ function logEntry(entry) { console.log(" %s %s", entry.hash, path); } -function logEnd() { - console.log("\n\x1B[30;1mBeginning of History.\x1B[0m\n"); +function logEnd(shallow) { + var message = shallow ? "End of shallow record." : "Beginning of history"; + console.log("\n\x1B[30;1m%s\x1B[0m\n", message); } \ No newline at end of file diff --git a/js-git.js b/js-git.js index ca8730b..0c188e6 100644 --- a/js-git.js +++ b/js-git.js @@ -67,6 +67,7 @@ function newRepo(db, workDir) { repo.updateHead = updateHead; // (hash) repo.getHead = getHead; // () -> ref repo.setHead = setHead; // (ref) + repo.readRef = readRef; // (ref) -> hash repo.createRef = createRef; // (ref, hash) repo.deleteRef = deleteRef; // (ref) repo.listRefs = listRefs; // (prefix) -> refs @@ -116,8 +117,14 @@ function newRepo(db, workDir) { return resolveHashish(hashish, onResolve); function onResolve(err, hash) { if (err) return callback(err); + var options = { + reverse: true, + }; var item = {hash: hash}; - return callback(null, walk(item, logScan, logCompare, true)); + readRef("shallow", function (err, shallow) { + if (shallow) options.last = shallow; + return callback(null, walk(item, logScan, logCompare, options)); + }); } } @@ -127,14 +134,16 @@ function newRepo(db, workDir) { function onResolve(err, hash) { if (err) return callback(err); var item = {hash: hash, path: "/"}; - return callback(null, walk(item, treeScan, treeCompare)); + return callback(null, walk(item, treeScan, treeCompare, {})); } } - function walk(seed, scan, sortKey, reverse) { + function walk(seed, scan, sortKey, options) { var queue = []; var seen = {}; var working = 0, error, cb, done; + var reverse = options.reverse; + var last = options.last; enqueue(seed); return {read: read, abort: abort}; @@ -186,7 +195,8 @@ function newRepo(db, workDir) { var next = queue.pop(); if (!next) return abort(callback); next = next[0]; - scan(next).forEach(enqueue); + if (next.hash === last) next.last = true; + else scan(next).forEach(enqueue); return callback(null, next); } @@ -353,6 +363,14 @@ function newRepo(db, workDir) { return db.write("HEAD", "ref: " + ref + "\n", callback); } + function readRef(ref, callback) { + if (!callback) return readRef.bind(this, ref); + return db.read(ref, function (err, result) { + if (err) return callback(err); + return callback(null, result.trim()); + }); + } + function createRef(ref, hash, callback) { if (!callback) return createRef.bind(this, ref, hash); return db.write(ref, hash + "\n", callback); From db477a765427f13e16b4185dd66750126f2a0dcd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 12 Sep 2013 23:34:21 -0500 Subject: [PATCH 014/256] Update deps and bump version to 0.4.1 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f2a76d4..7d81353 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.4.0", + "version": "0.4.1", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { @@ -9,8 +9,8 @@ }, "devDependencies": { "git-fs-db": "~0.1.1", - "git-node-platform": "~0.1.2", - "git-net": "~0.0.1", + "git-net": "~0.0.2", + "git-node-platform": "~0.1.3", "gen-run": "~0.1.1" }, "keywords": [ From 9d44902c03beecbb05bfb83fe9451d6c0a69d13c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 13 Sep 2013 11:44:34 -0500 Subject: [PATCH 015/256] Don't recurse into git submodules when walking --- js-git.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js-git.js b/js-git.js index 0c188e6..6ceadc5 100644 --- a/js-git.js +++ b/js-git.js @@ -1015,7 +1015,9 @@ function logCompare(object) { function treeScan(object) { if (object.type === "blob") return []; assertType(object, "tree"); - return object.body.map(function (entry) { + return object.body.filter(function (entry) { + return entry.mode !== 0160000; + }).map(function (entry) { var path = object.path + entry.name; if (entry.mode === 040000) path += "/"; return {hash:entry.hash,path:path}; From f338f29781d216279d9816bc1974a6ecf8e8cf55 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 13 Sep 2013 11:46:57 -0500 Subject: [PATCH 016/256] Fix hashish resolving --- js-git.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js-git.js b/js-git.js index 6ceadc5..dfe8683 100644 --- a/js-git.js +++ b/js-git.js @@ -308,7 +308,7 @@ function newRepo(db, workDir) { } function checkBranch(err, hash) { - if (err) return callback(err); + if (err && err.code !== "ENOENT") return callback(err); if (hash) { return resolveHashish(hash, callback); } @@ -316,7 +316,7 @@ function newRepo(db, workDir) { } function checkTag(err, hash) { - if (err) return callback(err); + if (err && err.code !== "ENOENT") return callback(err); if (hash) { return resolveHashish(hash, callback); } From e4b2a4cf13abea3742b087e35d2e9fc93e3dd8c8 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 13 Sep 2013 18:15:38 -0500 Subject: [PATCH 017/256] Rewrite unpack code in effort to fix bug --- examples/clone.js | 2 +- js-git.js | 216 +++++++++++++++++++++++++++------------------- 2 files changed, 130 insertions(+), 88 deletions(-) diff --git a/examples/clone.js b/examples/clone.js index 0d3511c..3adddf2 100644 --- a/examples/clone.js +++ b/examples/clone.js @@ -17,7 +17,7 @@ console.log("Cloning %s to %s", url, path); var opts = { onProgress: function (progress) { - process.stdout.write(progress); + process.stderr.write(progress); } }; if (process.env.DEPTH) { diff --git a/js-git.js b/js-git.js index dfe8683..4bd247d 100644 --- a/js-git.js +++ b/js-git.js @@ -833,16 +833,22 @@ function newRepo(db, workDir) { function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); - // TODO: save the stream to the local repo. + var version, num, count = 0, deltas = 0, done; - // hashes keyed by offset + // hashes keyed by offset for ofs-delta resolving var hashes = {}; - var seen = {}; - var toDelete = {}; + // Cache for hasHash + var has = {}; + // work queue as a tree + var work = {}; + // deltas waiting on a target to depend on. var pending = {}; - var queue = []; - var list, current; + // references to deltas for sub-dependencies + var index = {}; + + // hashes of target and delta objects in progress + var target, delta; return packStream.read(onStats); @@ -852,6 +858,15 @@ function newRepo(db, workDir) { return callback(err); } + function hasHash(hash, callback) { + if (hash in has) return callback(null, has[hash]); + db.has(hash, function (err, value) { + if (err) return onDone(err); + has[hash] = value; + return callback(null, value); + }); + } + function onStats(err, stats) { if (err) return onDone(err); version = stats.version; @@ -867,9 +882,10 @@ function newRepo(db, workDir) { count++; } if (item === undefined) { - hashes = null; + has = null; + index = null; count = 0; - return checkExisting(); + return getDelta(); } if (item.size !== item.body.length) { return onDone(new Error("Body size mismatch")); @@ -880,21 +896,50 @@ function newRepo(db, workDir) { ]); var hash = sha1(buffer); hashes[item.offset] = hash; - var ref = item.ref; - if (ref !== undefined) { + if ("ref" in item) { deltas++; + var ref = item.ref; if (item.type === "ofs-delta") { ref = hashes[item.offset - ref]; } - var list = pending[ref]; - if (list) list.push(hash); - else pending[ref] = [hash]; - toDelete[hash] = true; + + // If someone was looking for this hash, adopt the orphans + var deps; + if (hash in pending) { + throw "Wrench" + deps = pending[hash]; + delete pending[hash]; + } + // Otherwise start fresh. + else { + deps = {}; + } + + // Store a direct line to this family unit. + index[hash] = deps; + + // Shortcut for deltas already gone before. + if (ref in index) { + index[ref][hash] = deps; + return db.save(hash, buffer, onSave); + } + + // Check if the target is in the database already + return hasHash(ref, function (err, value) { + if (err) return onDone(err); + // Create a new group + var obj = index[ref] = {}; + (value ? work : pending)[ref] = obj; + // And attach to it. + obj[hash] = deps; + return db.save(hash, buffer, onSave); + }); } - else { - seen[hash] = true; + has[hash] = true; + if (hash in pending) { + work[hash] = pending[hash]; + delete pending[hash]; } - return db.save(hash, buffer, onSave); } @@ -903,34 +948,8 @@ function newRepo(db, workDir) { packStream.read(onRead); } - function checkExisting() { - list = Object.keys(pending); - return popPending(); - } - - function popPending() { - current = list.pop(); - if (!current) return applyDeltas(); - if (toDelete[current]) return popPending(); - return db.has(current, onHas); - } - - function onHas(err, has) { - if (err) return onDone(err); - if (has) seen[current] = true; - return popPending(); - } - - function applyDeltas() { - Object.keys(pending).forEach(function (ref) { - if (seen[ref]) { - pending[ref].forEach(function (hash) { - queue.push({hash:hash,ref:ref}); - }); - delete pending[ref]; - } - }); - return queue.length ? check() : cleanup(); + function key(obj) { + for (var k in obj) return k; } function deltaProgress() { @@ -938,59 +957,82 @@ function newRepo(db, workDir) { return "Applying deltas: " + percent + "% (" + count++ + "/" + deltas + ") "; } - function check() { - var item = queue.pop(); - var target, delta; - if (!item) return applyDeltas(); + function getDelta() { + console.log({ + work: Object.keys(work).length, + pending: Object.keys(pending).length + }); + var targetHash, deltaHash, group; + while (true) { + targetHash = key(work); + if (!targetHash) { + if (opts.onProgress) { + opts.onProgress(deltaProgress() + "\n"); + } + return onDone(); + } + group = work[targetHash]; + deltaHash = key(group); + if (!deltaHash) { + delete work[targetHash]; + continue; + } + break; + } if (opts.onProgress) { opts.onProgress(deltaProgress() + "\r"); } - db.load(item.ref, onTarget); - db.load(item.hash, onDelta); - return; + target = { hash: targetHash }; + delta = { + hash: deltaHash, + deps: group[deltaHash] + }; + delete group[deltaHash]; + db.load(target.hash, onLoadTarget); + db.load(delta.hash, onLoadDelta); + } - function onTarget(err, raw) { - if (err) return onDone(err); - target = deframe(raw); - if (delta) return onPair(item, target, delta); - } + function onLoadTarget(err, buffer) { + if (err) return onDone(err); + var pair = deframe(buffer); + target.type = pair[0]; + target.buffer = pair[1]; + if (delta.type) return onBoth(); + } - function onDelta(err, raw) { - if (err) return onDone(err); - delta = deframe(raw); - if (target) return onPair(item, target, delta); - } + function onLoadDelta(err, buffer) { + if (err) return onDone(err); + var pair = deframe(buffer); + delta.type = pair[0]; + delta.buffer = pair[1]; + if (target.type) return onBoth(); } - function onPair(item, target, delta) { - var buffer = frame(target[0], applyDelta(delta[1], target[1])); + function onBoth() { + var buffer = applyDelta(delta.buffer, target.buffer); + buffer = frame(target.type, buffer); var hash = sha1(buffer); - db.save(hash, buffer, onSaveConbined); - - function onSaveConbined(err) { - if (err) return onDone(err); - var deps = pending[item.hash]; - if (deps) { - pending[hash] = deps; - delete pending[item.hash]; - } - seen[hash] = true; - return check(); + console.log({ + target: target.hash, + delta: delta.hash, + combined: hash, + deps: Object.keys(delta.deps).length + }); + // console.log("DEPS", delta.deps) + if (key(delta.deps)) { + work[hash] = delta.deps; + if (hash in pending) throw "TODO: merge"; + } + else if (hash in pending) { + work[hash] = pending[hash]; + delete pending[hash]; } + return db.save(hash, buffer, onCombine); } - function cleanup() { - if (opts.onProgress && deltas) { - opts.onProgress(deltaProgress() + "\n"); - } - var hashes = Object.keys(toDelete); - next(); - function next(err) { - if (err) return onDone(err); - var hash = hashes.pop(); - if (!hash) return onDone(); - remove(hash, next); - } + function onCombine(err) { + if (err) return onDone(err); + return db.remove(delta.hash, getDelta); } } } From 3727be7cf963f93e9eca784c6e33bde3822b7489 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 13 Sep 2013 22:52:00 -0500 Subject: [PATCH 018/256] Change unpack strategy again. This one is fast and accurate for most repos. --- .gitignore | 1 + js-git.js | 217 ++++++++++++++--------------------------------------- 2 files changed, 59 insertions(+), 159 deletions(-) diff --git a/.gitignore b/.gitignore index 349834e..b9e6895 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.git node_modules +.zedstate diff --git a/js-git.js b/js-git.js index 4bd247d..923125d 100644 --- a/js-git.js +++ b/js-git.js @@ -834,21 +834,12 @@ function newRepo(db, workDir) { function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); - var version, num, count = 0, deltas = 0, done; + var version, num, numDeltas = 0, count = 0, countDeltas = 0; + var done, startDeltaProgress = false; // hashes keyed by offset for ofs-delta resolving var hashes = {}; - // Cache for hasHash var has = {}; - // work queue as a tree - var work = {}; - // deltas waiting on a target to depend on. - var pending = {}; - // references to deltas for sub-dependencies - var index = {}; - - // hashes of target and delta objects in progress - var target, delta; return packStream.read(onStats); @@ -858,15 +849,6 @@ function newRepo(db, workDir) { return callback(err); } - function hasHash(hash, callback) { - if (hash in has) return callback(null, has[hash]); - db.has(hash, function (err, value) { - if (err) return onDone(err); - has[hash] = value; - return callback(null, value); - }); - } - function onStats(err, stats) { if (err) return onDone(err); version = stats.version; @@ -874,166 +856,83 @@ function newRepo(db, workDir) { packStream.read(onRead); } + function objectProgress(more) { + if (!more) startDeltaProgress = true; + var percent = Math.round(count / num * 100); + return opts.onProgress("Receiving objects: " + percent + "% (" + (count++) + "/" + num + ") " + (more ? "\r" : "\n")); + } + + function deltaProgress(more) { + if (!startDeltaProgress) return; + var percent = Math.round(countDeltas / numDeltas * 100); + return opts.onProgress("Applying deltas: " + percent + "% (" + (countDeltas++) + "/" + numDeltas + ") " + (more ? "\r" : "\n")); + } + function onRead(err, item) { if (err) return onDone(err); - if (opts.onProgress) { - var percent = Math.round(count / num * 100); - opts.onProgress("Receiving objects: " + percent + "% (" + count + "/" + num + ") " + (item ? "\r" : "\n")); - count++; - } - if (item === undefined) { - has = null; - index = null; - count = 0; - return getDelta(); - } + if (opts.onProgress) objectProgress(item); + if (item === undefined) return resolveDeltas(); if (item.size !== item.body.length) { return onDone(new Error("Body size mismatch")); } - var buffer = bops.join([ - bops.from(item.type + " " + item.size + "\0"), - item.body - ]); - var hash = sha1(buffer); - hashes[item.offset] = hash; - if ("ref" in item) { - deltas++; - var ref = item.ref; - if (item.type === "ofs-delta") { - ref = hashes[item.offset - ref]; - } - - // If someone was looking for this hash, adopt the orphans - var deps; - if (hash in pending) { - throw "Wrench" - deps = pending[hash]; - delete pending[hash]; - } - // Otherwise start fresh. - else { - deps = {}; - } - - // Store a direct line to this family unit. - index[hash] = deps; - - // Shortcut for deltas already gone before. - if (ref in index) { - index[ref][hash] = deps; - return db.save(hash, buffer, onSave); - } - - // Check if the target is in the database already - return hasHash(ref, function (err, value) { - if (err) return onDone(err); - // Create a new group - var obj = index[ref] = {}; - (value ? work : pending)[ref] = obj; - // And attach to it. - obj[hash] = deps; - return db.save(hash, buffer, onSave); - }); + if (item.type === "ofs-delta") { + numDeltas++; + item.ref = hashes[item.offset - item.ref]; + return resolveDelta(item); } - has[hash] = true; - if (hash in pending) { - work[hash] = pending[hash]; - delete pending[hash]; + if (item.type === "ref-delta") { + numDeltas++; + return checkDelta(item); } - return db.save(hash, buffer, onSave); + return saveValue(item); } - function onSave(err) { - if (err) return callback(err); - packStream.read(onRead); + function resolveDelta(item) { + if (opts.onProgress) deltaProgress(); + return db.load(item.ref, function (err, buffer) { + if (err) return onDone(err); + var target = deframe(buffer); + item.type = target[0]; + item.body = applyDelta(item.body, target[1]); + return saveValue(item); + }); } - function key(obj) { - for (var k in obj) return k; + function checkDelta(item) { + var hasTarget = has[item.ref]; + if (hasTarget === true) return resolveDelta(item); + if (hasTarget === false) return enqueueDelta(item); + return db.has(item.ref, function (err, value) { + if (err) return onDone(err); + has[item.ref] = value; + if (value) return resolveDelta(item); + return enqueueDelta(item); + }); } - function deltaProgress() { - var percent = Math.round(count / deltas * 100); - return "Applying deltas: " + percent + "% (" + count++ + "/" + deltas + ") "; + function saveValue(item) { + var buffer = frame(item.type, item.body); + var hash = hashes[item.offset] = sha1(buffer); + has[hash] = true; + return db.save(hash, buffer, onSave); } - function getDelta() { - console.log({ - work: Object.keys(work).length, - pending: Object.keys(pending).length - }); - var targetHash, deltaHash, group; - while (true) { - targetHash = key(work); - if (!targetHash) { - if (opts.onProgress) { - opts.onProgress(deltaProgress() + "\n"); - } - return onDone(); - } - group = work[targetHash]; - deltaHash = key(group); - if (!deltaHash) { - delete work[targetHash]; - continue; - } - break; - } - if (opts.onProgress) { - opts.onProgress(deltaProgress() + "\r"); - } - target = { hash: targetHash }; - delta = { - hash: deltaHash, - deps: group[deltaHash] - }; - delete group[deltaHash]; - db.load(target.hash, onLoadTarget); - db.load(delta.hash, onLoadDelta); + function onSave(err) { + if (err) return callback(err); + packStream.read(onRead); } - function onLoadTarget(err, buffer) { - if (err) return onDone(err); - var pair = deframe(buffer); - target.type = pair[0]; - target.buffer = pair[1]; - if (delta.type) return onBoth(); + function enqueueDelta(item) { + // I have yet to come across a repo that actually needs this path. + // It's hard to implement without something to test against. + throw "TODO: enqueueDelta"; } - function onLoadDelta(err, buffer) { - if (err) return onDone(err); - var pair = deframe(buffer); - delta.type = pair[0]; - delta.buffer = pair[1]; - if (target.type) return onBoth(); - } - - function onBoth() { - var buffer = applyDelta(delta.buffer, target.buffer); - buffer = frame(target.type, buffer); - var hash = sha1(buffer); - console.log({ - target: target.hash, - delta: delta.hash, - combined: hash, - deps: Object.keys(delta.deps).length - }); - // console.log("DEPS", delta.deps) - if (key(delta.deps)) { - work[hash] = delta.deps; - if (hash in pending) throw "TODO: merge"; - } - else if (hash in pending) { - work[hash] = pending[hash]; - delete pending[hash]; - } - return db.save(hash, buffer, onCombine); + function resolveDeltas() { + // TODO: resolve any pending deltas once enqueueDelta is implemented. + return onDone(); } - function onCombine(err) { - if (err) return onDone(err); - return db.remove(delta.hash, getDelta); - } } } From c1ef9cdf801fcbc47133b2aa73a1da1aed4231dc Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 13 Sep 2013 22:52:21 -0500 Subject: [PATCH 019/256] Bump version to 0.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d81353..ae8c07e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.4.1", + "version": "0.4.2", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From 3b63f68eaf3a5e63e2c99f30bc2e2842d00bfbb2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 14 Sep 2013 01:02:12 -0500 Subject: [PATCH 020/256] Return all metadata in file walker --- js-git.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js-git.js b/js-git.js index 923125d..d989727 100644 --- a/js-git.js +++ b/js-git.js @@ -961,7 +961,8 @@ function treeScan(object) { }).map(function (entry) { var path = object.path + entry.name; if (entry.mode === 040000) path += "/"; - return {hash:entry.hash,path:path}; + entry.path = path; + return entry; }); } From da41db061f334c1492ffb5b79f4e2f7e0270c333 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 14 Sep 2013 01:10:59 -0500 Subject: [PATCH 021/256] Allow commit hashish for walkTree --- js-git.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/js-git.js b/js-git.js index d989727..48ef390 100644 --- a/js-git.js +++ b/js-git.js @@ -133,8 +133,15 @@ function newRepo(db, workDir) { return resolveHashish(hashish, onResolve); function onResolve(err, hash) { if (err) return callback(err); - var item = {hash: hash, path: "/"}; - return callback(null, walk(item, treeScan, treeCompare, {})); + load(hash, function (err, item) { + if (err) return callback(err); + if (item.type === "commit") { + return onResolve(null, item.body.tree); + } + item.hash = hash; + item.path = "/"; + return callback(null, walk(item, treeScan, treeCompare, {})); + }); } } From e084c8844d76b44ce1e070a9200ad7e4b8fa68e0 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 14 Sep 2013 13:18:36 -0500 Subject: [PATCH 022/256] Make db required and remove lsRemote --- js-git.js | 96 +++++++++++++++++++++---------------------------------- 1 file changed, 37 insertions(+), 59 deletions(-) diff --git a/js-git.js b/js-git.js index 48ef390..e44ea24 100644 --- a/js-git.js +++ b/js-git.js @@ -17,6 +17,7 @@ module.exports = function (imports) { }; function newRepo(db, workDir) { + if (!db) throw new TypeError("A db interface instance is required"); var encoders = { commit: encodeCommit, @@ -34,55 +35,49 @@ function newRepo(db, workDir) { var repo = {}; - if (db) { - - if (trace) { - db = { - load: wrap1("load", db.load), - save: wrap2("save", db.save), - remove: wrap1("remove", db.remove), - has: wrap1("has", db.has), - read: wrap1("read", db.read), - write: wrap2("write", db.write), - unlink: wrap1("unlink", db.unlink), - readdir: wrap1("readdir", db.readdir) - }; - } + if (trace) { + db = { + load: wrap1("load", db.load), + save: wrap2("save", db.save), + remove: wrap1("remove", db.remove), + has: wrap1("has", db.has), + read: wrap1("read", db.read), + write: wrap2("write", db.write), + unlink: wrap1("unlink", db.unlink), + readdir: wrap1("readdir", db.readdir) + }; + } - // Git Objects - repo.load = load; // (hashish) -> object - repo.save = save; // (object) -> hash - repo.loadAs = loadAs; // (type, hashish) -> value - repo.saveAs = saveAs; // (type, value) -> hash - repo.remove = remove; // (hashish) - repo.unpack = unpack; // (opts, packStream) + // Git Objects + repo.load = load; // (hashish) -> object + repo.save = save; // (object) -> hash + repo.loadAs = loadAs; // (type, hashish) -> value + repo.saveAs = saveAs; // (type, value) -> hash + repo.remove = remove; // (hashish) + repo.unpack = unpack; // (opts, packStream) - // Convenience Readers - repo.logWalk = logWalk; // (hashish) -> stream - repo.treeWalk = treeWalk; // (hashish) -> stream - repo.walk = walk; // (hashish, scan, compare) -> stream + // Convenience Readers + repo.logWalk = logWalk; // (hashish) => stream + repo.treeWalk = treeWalk; // (hashish) => stream + repo.walk = walk; // (seed, scan, compare) -> stream - // Refs - repo.resolveHashish = resolveHashish; // (hashish) -> hash - repo.updateHead = updateHead; // (hash) - repo.getHead = getHead; // () -> ref - repo.setHead = setHead; // (ref) - repo.readRef = readRef; // (ref) -> hash - repo.createRef = createRef; // (ref, hash) - repo.deleteRef = deleteRef; // (ref) - repo.listRefs = listRefs; // (prefix) -> refs + // Refs + repo.resolveHashish = resolveHashish; // (hashish) -> hash + repo.updateHead = updateHead; // (hash) + repo.getHead = getHead; // () -> ref + repo.setHead = setHead; // (ref) + repo.readRef = readRef; // (ref) -> hash + repo.createRef = createRef; // (ref, hash) + repo.deleteRef = deleteRef; // (ref) + repo.listRefs = listRefs; // (prefix) -> refs - if (workDir) { - // TODO: figure out API for working repos - } + if (workDir) { + // TODO: figure out API for working repos } // Network Protocols - repo.lsRemote = lsRemote; - if (db) { - repo.fetch = fetch; - repo.push = push; - } + repo.fetch = fetch; + repo.push = push; return repo; @@ -659,23 +654,6 @@ function newRepo(db, workDir) { return body; } - function lsRemote(remote, callback) { - if (!callback) return lsRemote.bind(this, remote); - var refs; - return remote.discover(onDiscover); - - function onDiscover(err, result) { - if (err) return callback(err); - refs = result; - return remote.close(onClose); - } - - function onClose(err) { - if (err) return callback(err); - return callback(null, refs); - } - } - function fetch(remote, opts, callback) { if (!callback) return fetch.bind(this, remote, opts); var refs, branch, queue, ref, hash; From bca95dfb4f446d42c336251bfeaa8f8df58888df Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 16 Sep 2013 08:04:44 -0500 Subject: [PATCH 023/256] Update walk API --- js-git.js | 216 ++++++++++++++++++++++++++---------------------------- 1 file changed, 105 insertions(+), 111 deletions(-) diff --git a/js-git.js b/js-git.js index e44ea24..da94966 100644 --- a/js-git.js +++ b/js-git.js @@ -58,7 +58,7 @@ function newRepo(db, workDir) { // Convenience Readers repo.logWalk = logWalk; // (hashish) => stream - repo.treeWalk = treeWalk; // (hashish) => stream + repo.treeWalk = treeWalk; // (hashish) => stream repo.walk = walk; // (seed, scan, compare) -> stream // Refs @@ -107,108 +107,131 @@ function newRepo(db, workDir) { } } +// function logMap(object) { +// assertType(object, "commit"); +// return object.body; +// } + +// function logScan(commit) { +// return commit.parents.map(function (hash) { +// return { hash: hash }; +// }); +// } + +// function logCompare(commit) { +// return commit.author.date; +// } + +// function treeScan(object) { +// if (object.type === "blob") return []; +// assertType(object, "tree"); +// return object.body.filter(function (entry) { +// return entry.mode !== 0160000; +// }).map(function (entry) { +// var path = object.path + entry.name; +// if (entry.mode === 040000) path += "/"; +// entry.path = path; +// return entry; +// }); +// } + +// function treeCompare(object) { +// return object.path.toLowerCase(); +// } + + + function logWalk(hashish, callback) { if (!callback) return logWalk.bind(this, hashish); - return resolveHashish(hashish, onResolve); - function onResolve(err, hash) { + var last, seen = {}; + return readRef("shallow", onShallow); + + function onShallow(err, shallow) { + last = shallow; + return loadAs("commit", hashish, onLoad); + } + + function onLoad(err, commit, hash) { if (err) return callback(err); - var options = { - reverse: true, - }; - var item = {hash: hash}; - readRef("shallow", function (err, shallow) { - if (shallow) options.last = shallow; - return callback(null, walk(item, logScan, logCompare, options)); + commit.hash = hash; + seen[hash] = true; + return callback(null, walk(commit, scan, loadKey, compare)); + } + + function scan(commit) { + if (last === commit) return []; + return commit.parents.filter(function (hash) { + return !seen[hash]; }); } - } - function treeWalk(hashish, callback) { - if (!callback) return treeWalk.bind(this, hashish); - return resolveHashish(hashish, onResolve); - function onResolve(err, hash) { - if (err) return callback(err); - load(hash, function (err, item) { + function loadKey(hash, callback) { + return loadAs("commit", hash, function (err, commit) { if (err) return callback(err); - if (item.type === "commit") { - return onResolve(null, item.body.tree); - } - item.hash = hash; - item.path = "/"; - return callback(null, walk(item, treeScan, treeCompare, {})); + commit.hash = hash; + if (hash === last) commit.last = true; + return callback(null, commit); }); } - } - function walk(seed, scan, sortKey, options) { - var queue = []; - var seen = {}; - var working = 0, error, cb, done; - var reverse = options.reverse; - var last = options.last; + function compare(commit, other) { + return commit.author.date < other.author.date; + } + } - enqueue(seed); + function treeWalk(hashish, callback) { + if (!callback) return treeWalk.bind(this, hashish); + // return load(hashish, onLoad); + // function onLoad(err, item, hash) { + // if (err) return callback(err); + // if (item.type === "commit") return load(item.body.tree, onLoad); + // item.hash = hash; + // item.path = "/"; + // return callback(null, walk(item, treeScan, treeCompare, {})); + // } + } + + function walk(seed, scan, loadKey, compare) { + var queue = [seed]; + var working = 0, error, cb; return {read: read, abort: abort}; - function enqueue(item) { - if (item.hash in seen) return; - seen[item.hash] = true; - working++; - load(item.hash, function (err, object) { - if (err) { - error = err; - return check(); - } - item.type = object.type; - item.body = object.body; - var sortValue = sortKey(item); - var index = queue.length; - if (reverse) { - while (index > 0 && queue[index - 1][1] > sortValue) index--; - } - else { - while (index > 0 && queue[index - 1][1] < sortValue) index--; - } - queue.splice(index, 0, [item, sortValue]); - return check(); - }); + function read(callback) { + if (cb) return callback(new Error("Only one read at a time")); + if (working) { cb = callback; return; } + var item = queue.shift(); + if (!item) return callback(); + try { scan(item).forEach(onKey); } + catch (err) { return callback(err); } + return callback(null, item); } - function check() { - if (!--working && cb) { - var callback = cb; - cb = null; - read(callback); - } - } + function abort(callback) { return callback(); } - function read(callback) { - if (cb) return callback(new Error("Only one read at a time")); - if (error) { - var err = error; - error = null; + function onError(err) { + if (cb) { + var callback = cb; cb = null; return callback(err); } - if (done) return callback(); - if (working) { - cb = callback; - return; - } - var next = queue.pop(); - if (!next) return abort(callback); - next = next[0]; - if (next.hash === last) next.last = true; - else scan(next).forEach(enqueue); - return callback(null, next); + error = err; } - function abort(callback) { - done = true; - queue = null; - seen = null; - return callback(); + function onKey(key) { + working++; + loadKey(key, onItem); + } + + function onItem(err, item) { + working--; + if (err) return onError(err); + var index = queue.length; + while (index && compare(item, queue[index - 1])) index--; + queue.splice(index, 0, item); + if (!working && cb) { + var callback = cb; cb = null; + return read(callback); + } } - } function load(hashish, callback) { @@ -926,32 +949,3 @@ function assertType(object, type) { throw new Error(type + " expected, but found " + object.type); } } - -function logScan(object) { - assertType(object, "commit"); - return object.body.parents.map(function (hash) { - return { hash: hash }; - }); -} - -function logCompare(object) { - return object.body.author.date; -} - -function treeScan(object) { - if (object.type === "blob") return []; - assertType(object, "tree"); - return object.body.filter(function (entry) { - return entry.mode !== 0160000; - }).map(function (entry) { - var path = object.path + entry.name; - if (entry.mode === 040000) path += "/"; - entry.path = path; - return entry; - }); -} - -function treeCompare(object) { - return object.path.toLowerCase(); -} - From 8c5ad9fb00b78daa54c7b2c50fdb3f56bd1114b2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 25 Sep 2013 10:24:45 -0500 Subject: [PATCH 024/256] Finish implementing new walker API --- js-git.js | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/js-git.js b/js-git.js index da94966..9910f41 100644 --- a/js-git.js +++ b/js-git.js @@ -181,14 +181,40 @@ function newRepo(db, workDir) { function treeWalk(hashish, callback) { if (!callback) return treeWalk.bind(this, hashish); - // return load(hashish, onLoad); - // function onLoad(err, item, hash) { - // if (err) return callback(err); - // if (item.type === "commit") return load(item.body.tree, onLoad); - // item.hash = hash; - // item.path = "/"; - // return callback(null, walk(item, treeScan, treeCompare, {})); - // } + return load(hashish, onLoad); + function onLoad(err, item, hash) { + if (err) return callback(err); + if (item.type === "commit") return load(item.body.tree, onLoad); + item.hash = hash; + item.path = "/"; + return callback(null, walk(item, treeScan, treeLoadKey, treeCompare)); + } + } + + function treeScan(object) { + if (object.type === "blob") return []; + assertType(object, "tree"); + return object.body.filter(function (entry) { + return entry.mode !== 0160000; + }).map(function (entry) { + var path = object.path + entry.name; + if (entry.mode === 040000) path += "/"; + entry.path = path; + return entry; + }); + } + + function treeLoadKey(entry, callback) { + return load(entry.hash, function (err, object) { + if (err) return callback(err); + entry.type = object.type; + entry.body = object.body; + return callback(null, entry); + }); + } + + function treeCompare(first, second) { + return first.path < second.path; } function walk(seed, scan, loadKey, compare) { From 82cb322667fef07c7947010b372cc8dc243a673f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 25 Sep 2013 14:11:20 -0500 Subject: [PATCH 025/256] Update walk example to use new API --- examples/walk.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/walk.js b/examples/walk.js index 76f7f58..ced7e10 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -15,7 +15,7 @@ repo.logWalk("HEAD", function (err, log) { if (!commit) return logEnd(shallow); if (commit.last) shallow = true; logCommit(commit); - repo.treeWalk(commit.body.tree, function (err, tree) { + repo.treeWalk(commit.tree, function (err, tree) { if (err) throw err; tree.read(onEntry); function onEntry(err, entry) { @@ -31,8 +31,8 @@ repo.logWalk("HEAD", function (err, log) { }); function logCommit(commit) { - var author = commit.body.author; - var message = commit.body.message; + var author = commit.author; + var message = commit.message; console.log("\n\x1B[33mcommit %s\x1B[0m", commit.hash); console.log("Author: %s <%s>", author.name, author.email); console.log("Date: %s", author.date); From 659438e9fd166caeed6172b64b20342165521508 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 25 Sep 2013 14:17:34 -0500 Subject: [PATCH 026/256] Bump version to 0.5.0 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ae8c07e..0920bd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.4.2", + "version": "0.5.0", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { @@ -9,8 +9,8 @@ }, "devDependencies": { "git-fs-db": "~0.1.1", - "git-net": "~0.0.2", - "git-node-platform": "~0.1.3", + "git-net": "~0.0.3", + "git-node-platform": "~0.1.4", "gen-run": "~0.1.1" }, "keywords": [ From 7857e79c9f011a6f302e4ff23d3103f376fd4fe9 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 3 Oct 2013 13:39:07 -0500 Subject: [PATCH 027/256] Update to new db interface and bump version to 0.5.1 --- js-git.js | 98 ++++++++++++++++++++-------------------------------- package.json | 2 +- 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/js-git.js b/js-git.js index 9910f41..663fe19 100644 --- a/js-git.js +++ b/js-git.js @@ -37,14 +37,12 @@ function newRepo(db, workDir) { if (trace) { db = { - load: wrap1("load", db.load), - save: wrap2("save", db.save), - remove: wrap1("remove", db.remove), + get: wrap1("get", db.get), + set: wrap2("set", db.set), has: wrap1("has", db.has), - read: wrap1("read", db.read), - write: wrap2("write", db.write), - unlink: wrap1("unlink", db.unlink), - readdir: wrap1("readdir", db.readdir) + del: wrap1("del", db.del), + keys: wrap1("keys", db.keys), + init: wrap0("init", db.init), }; } @@ -81,6 +79,19 @@ function newRepo(db, workDir) { return repo; + function wrap0(type, fn) { + return zero; + function zero(callback) { + if (!callback) return zero.bind(this); + return fn.call(this, check); + function check(err) { + if (err) return callback(err); + trace(type, null); + return callback.apply(this, arguments); + } + } + } + function wrap1(type, fn) { return one; function one(arg, callback) { @@ -107,40 +118,6 @@ function newRepo(db, workDir) { } } -// function logMap(object) { -// assertType(object, "commit"); -// return object.body; -// } - -// function logScan(commit) { -// return commit.parents.map(function (hash) { -// return { hash: hash }; -// }); -// } - -// function logCompare(commit) { -// return commit.author.date; -// } - -// function treeScan(object) { -// if (object.type === "blob") return []; -// assertType(object, "tree"); -// return object.body.filter(function (entry) { -// return entry.mode !== 0160000; -// }).map(function (entry) { -// var path = object.path + entry.name; -// if (entry.mode === 040000) path += "/"; -// entry.path = path; -// return entry; -// }); -// } - -// function treeCompare(object) { -// return object.path.toLowerCase(); -// } - - - function logWalk(hashish, callback) { if (!callback) return logWalk.bind(this, hashish); var last, seen = {}; @@ -268,7 +245,7 @@ function newRepo(db, workDir) { function onHash(err, result) { if (err) return callback(err); hash = result; - return db.load(hash, onBuffer); + return db.get(hash, onBuffer); } function onBuffer(err, buffer) { @@ -303,7 +280,7 @@ function newRepo(db, workDir) { catch (err) { return callback(err); } - return db.save(hash, buffer, onSave); + return db.set(hash, buffer, onSave); function onSave(err) { if (err) return callback(err); @@ -337,7 +314,7 @@ function newRepo(db, workDir) { function onHash(err, result) { if (err) return callback(err); hash = result; - return db.remove(hash, callback); + return db.del(hash, callback); } } @@ -349,7 +326,7 @@ function newRepo(db, workDir) { } if (hashish === "HEAD") return getHead(onBranch); if ((/^refs\//).test(hashish)) { - return db.read(hashish, checkBranch); + return db.get(hashish, checkBranch); } return checkBranch(); @@ -363,7 +340,7 @@ function newRepo(db, workDir) { if (hash) { return resolveHashish(hash, callback); } - return db.read("refs/heads/" + hashish, checkTag); + return db.get("refs/heads/" + hashish, checkTag); } function checkTag(err, hash) { @@ -371,7 +348,7 @@ function newRepo(db, workDir) { if (hash) { return resolveHashish(hash, callback); } - return db.read("refs/tags/" + hashish, final); + return db.get("refs/tags/" + hashish, final); } function final(err, hash) { @@ -391,13 +368,13 @@ function newRepo(db, workDir) { function onBranch(err, result) { if (err) return callback(err); ref = result; - return db.write(ref, hash + "\n", callback); + return db.set(ref, hash + "\n", callback); } } function getHead(callback) { if (!callback) return getHead.bind(this); - return db.read("HEAD", onRead); + return db.get("HEAD", onRead); function onRead(err, ref) { if (err) return callback(err); @@ -411,31 +388,32 @@ function newRepo(db, workDir) { function setHead(branchName, callback) { if (!callback) return setHead.bind(this, branchName); var ref = "refs/heads/" + branchName; - return db.write("HEAD", "ref: " + ref + "\n", callback); + return db.set("HEAD", "ref: " + ref + "\n", callback); } function readRef(ref, callback) { if (!callback) return readRef.bind(this, ref); - return db.read(ref, function (err, result) { + return db.get(ref, function (err, result) { if (err) return callback(err); + if (!result) return callback(); return callback(null, result.trim()); }); } function createRef(ref, hash, callback) { if (!callback) return createRef.bind(this, ref, hash); - return db.write(ref, hash + "\n", callback); + return db.set(ref, hash + "\n", callback); } function deleteRef(ref, callback) { if (!callback) return deleteRef.bind(this, ref); - return db.unlink(ref, callback); + return db.del(ref, callback); } function listRefs(prefix, callback) { if (!callback) return listRefs.bind(this, prefix); var branches = {}, list = [], target = prefix; - return db.readdir(target, onNames); + return db.keys(target, onNames); function onNames(err, names) { if (err) { @@ -452,19 +430,19 @@ function newRepo(db, workDir) { if (err) return callback(err); target = list.shift(); if (!target) return callback(null, branches); - return db.read(target, onRead); + return db.get(target, onRead); } function onRead(err, hash) { if (err) { - if (err.code === "EISDIR") return db.readdir(target, onNames); + if (err.code === "EISDIR") return db.keys(target, onNames); return callback(err); } if (hash) { branches[target] = hash.trim(); return shift(); } - return db.readdir(target, onNames); + return db.keys(target, onNames); } } @@ -752,7 +730,7 @@ function newRepo(db, workDir) { function onHas(err, has) { if (err) return callback(err); if (!has) return next(); - return db.write(ref, hash + "\n", next); + return db.set(ref, hash + "\n", next); } } @@ -923,7 +901,7 @@ function newRepo(db, workDir) { function resolveDelta(item) { if (opts.onProgress) deltaProgress(); - return db.load(item.ref, function (err, buffer) { + return db.get(item.ref, function (err, buffer) { if (err) return onDone(err); var target = deframe(buffer); item.type = target[0]; @@ -948,7 +926,7 @@ function newRepo(db, workDir) { var buffer = frame(item.type, item.body); var hash = hashes[item.offset] = sha1(buffer); has[hash] = true; - return db.save(hash, buffer, onSave); + return db.set(hash, buffer, onSave); } function onSave(err) { diff --git a/package.json b/package.json index 0920bd7..1f427d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.5.0", + "version": "0.5.1", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From e09db6fd0327ed1115cc5f79446dd7d154dba665 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 3 Oct 2013 16:35:16 -0500 Subject: [PATCH 028/256] Handle missing head more gracefully --- js-git.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js-git.js b/js-git.js index 663fe19..1508c8b 100644 --- a/js-git.js +++ b/js-git.js @@ -332,6 +332,7 @@ function newRepo(db, workDir) { function onBranch(err, ref) { if (err) return callback(err); + if (!ref) return callback(); return resolveHashish(ref, callback); } @@ -378,7 +379,7 @@ function newRepo(db, workDir) { function onRead(err, ref) { if (err) return callback(err); - if (!ref) return callback(new Error("Missing HEAD")); + if (!ref) return callback(); var match = ref.match(/^ref: *(.*)/); if (!match) return callback(new Error("Invalid HEAD")); return callback(null, match[1]); From d32d42fd38f8dc01333d512a0a73d2ddd9c23ae0 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 3 Oct 2013 19:21:24 -0500 Subject: [PATCH 029/256] Bump version to 0.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f427d5..27cece3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.5.1", + "version": "0.5.2", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From f5486d2d162a06eaaff9dba8f2b4d9d9f2400092 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 4 Oct 2013 14:14:24 -0500 Subject: [PATCH 030/256] Update README.md --- README.md | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/README.md b/README.md index b4f3c23..a9f1528 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,197 @@ js-git ====== Git Implemented in JavaScript. + +This project is very modular and configurable by gluing different components together. + +This repo, `js-git`, is the core implementation of git and consumes various instances of interfaces. This means that your network and persistance stack is completely pluggable. + +If you're looking for a more pre-packaged system, consider packages like `creationix/git-node` that implement all the abstract interfaces using node.js native APIs. The `creationix/jsgit` package is an example of a CLI tool that consumes this. + +The main end-user API as exported by this module for working with local repositories is: + +## Initialize the library + +First you create an instance of the library by injecting the platform dependencies. + +```js +var platform = require('git-node-platform'); +var jsGit = require('js-git')(platform); +``` + +## Wrap a Database + +Then you implement the database interface (or more likely use a library to create it for you). + +```js +var fsDb = require('git-fs-db')(platform); +var db = fsDb("/path/to/repo.git"); +``` + +The database interface is documented later on. + +## Continuables + +In all public async functions you can either pass in a node-style callback last or omit the callback and it will return you a continuable. + +This means you can consume the js-git library using normal ES3 code or if you prefer use [gen-run][] and consume the continuables. + +If the callback is omitted, a continuable is returned. You must pass a callback into this continuable to actually start the action. + +```js +// Callback mode +jsgit.someAction(arg1, arg2, function (err, result) { + ... +}); + +// Continuable mode +var cont = jsgit.someAction(arg1, arg2); +cont(function (err, result) { + ... +}); + +// Continuable mode with gen-run +var result = yield jsgit.someAction(arg1, arg2); +``` + +### db.get(key, [callback]) -> value + +Load a ref or object from the database. + +The database should assume that keys that are 40-character long hex strings are sha1 hashes. The value for these will always be binary (`Buffer` in node, `Uint8Array` in browser) +All other keys are paths like `refs/heads/master` or `HEAD` and the value is a string. + + +### db.set(key, value, [callback]) + +Save a value to the database. Same rules apply about hash keys being binary values and other keys being string values. + +### db.has(key, [callback]) -> hasKey? + +Check if a key is in the database + +### db.del(key, [callback]) + +Remove an object or ref from the database. + +### db.keys(prefix, [callback]) -> keys + +Given a path prefix, give all the keys. This is like a readdir if you treat the keys as paths. + +For example, given the keys `refs/heads/master`, `refs/headers/experimental`, `refs/tags/0.1.3` and the prefix `refs/heads/`, the output would be `master` and `experimental`. + +A null prefix returns all non hash keys. + +### db.init([callback]) + +Initialize a database. This is where you db implementation can setup stuff. + +### db.clear([callback]) + +This is for when the user wants to delete or otherwise reclaim your database's resources. + + +### Wrapping the DataBase + +Now that you have a database instance, you can use the jsGit library created above. + +```js +var repo = jsGit(db); +``` + +### repo.load(hash(ish), [callback]) -> git object + +Load a git object from the database. You can pass in either a hash or a symbolic name like `HEAD` or `refs/tags/v3.1.4`. + +The object will be of the form: + +```js +{ + type: "commit", // Or "tag", "tree", or "blob" + body: { ... } // Or an array for tree and a binary value for blob. +} +``` + +### repo.save(object, [callback]) -> hash + +Save an object to the database. This will give you back the hash of the cotent by which you can retrieve the value back. + +### repo.loadAs(type, hash, [callback]) -> body + +This convenience wrapper will call `repo.save` for you and then check if the type is what you expected. If it is, it will return the body directly. If it's not, it will error. + +```js +var commit = yield repo.loadAs("commit", "HEAD"); +var tree = yield repo.loadAs("tree", commit.tree); +``` + +I'm using yield syntax because it's simpler, you can use callbacks instead if you prefer. + +### repo.saveAs(type, body, [callback]) -> hash + +Another convenience wrapper, this time to save objects as a specefic type. The body must be in the right format. + +```js +var blobHash = yield repo.saveAs("blob", binaryData); +var treeHash = yield repo.saveAs("tree", [ + { mode: 0100644, name: "file.dat, hash: blobHash } +]); +var commitHash = yield repo.saveAs("commit", { + tree: treeHash, + author: { name: "Tim Caswell", email: "tim@creationix.com", date: new Date }, + message: "Save the blob" +}); +``` + +### repo.remove(hash, [callback]) + +Remove an object. + +### repo.unpack(packFileStream, opts, [callback]) + +Import a packfile stream (simple-stream format) into the current database. This is used mostly for clone and fetch operations where the stream comes from a remote repo. + +`opts` is a hash of optional configs. + + - `opts.onProgress(progress)` - listen to the git progress channel by passing in a event listener. + - `opts.onError(error)` - same thing, but for the error channel. + - `opts.deline` - If this is truthy, the progress and error messages will be rechunked to be whole lines. They usually come jumbled in the internal sidechannel. + +### repo.logWalk(hash(ish), [callback]) -> log stream + +This convenience wrapper creates a readable stream of the history sorted by author date. + +If you want full history, pass in `HEAD` for the hash. + +### repo.treeWalk(hash(ish), [callback]) -> file stream + +This helper will return a stream of files suitable for traversing a file tree as a linear stream. The hash can be a ref to a commit, a commit hash or a tree hash directly. + +### repo.walk(seed, scan, loadKey, compare) -> stream + +This is the generic helper that `logWalk` and `treeWalk` use. See `js-git.js` source for usage. + +### repo.resolveHashish(hashish, [callback]) -> hash + +Resolve a ref, branch, or tag to a real hash. + +### repo.updateHead(hash, [callback]) + +Update whatever branch `HEAD` is pointing to so that it points to `hash`. + +You'll usually want to do this after creating a new commint in the HEAD branch. + +### repo.getHead([callback]) -> ref name + +Read the current active branch. + +### repo.setHead(ref, [callback]) + +Set the current active branch. + +### repo.fetch(remote, opts, [callback]) + +Convenience wrapper that fetches from a remote instance and calls `repo.unpack` with the resulting packfile stream for you. + + +[gen-run]: https://github.com/creationix/gen-run From 15dcb3e7ab014ccd2525d3d8aaaa660cb381f13a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 4 Oct 2013 14:26:47 -0500 Subject: [PATCH 031/256] Update README.md --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index a9f1528..f5de19d 100644 --- a/README.md +++ b/README.md @@ -194,5 +194,25 @@ Set the current active branch. Convenience wrapper that fetches from a remote instance and calls `repo.unpack` with the resulting packfile stream for you. +## Related Packages + +Being that js-git is so modular, here is a list of the most relevent modules that work with js-git: + + - - A generic remote protocol implementation that wraps the platform interfaces and consumes urls. + - Example Applications + - - A multi-platform GUI program that clones and browses git repos. + - - An example of using js-git in node. This is a CLI tool. + - - A packaged version of js-git made for node.js + - Platform Helpers + - - A git-http platform interface adapter that wraps git-tcp platform instances. + - - Just the platform interface for using js-git on node.js. + - - A pure-js implementation of the sha1 part of the platform interface. + - - An implementation of js-git platform for browsers. + - - An implementation of the git-tcp interface that consumes a websocket to tcp proxy server. + - - A pure-js implementation of the zlib parts of the platform interface. + - Storage Backends + - - A database interface adapter that wraps a fs interface. + - - A git-db implementation based on `localStorage`. + - - A git-db implementation that stores data in ram for quick testing. [gen-run]: https://github.com/creationix/gen-run From 44f9deff485f83b9042dd02239a3665415a9d6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Fri, 4 Oct 2013 15:32:34 -0400 Subject: [PATCH 032/256] README: Fixed sample for repo.saveAs() --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5de19d..b031e78 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Another convenience wrapper, this time to save objects as a specefic type. The ```js var blobHash = yield repo.saveAs("blob", binaryData); var treeHash = yield repo.saveAs("tree", [ - { mode: 0100644, name: "file.dat, hash: blobHash } + { mode: 0100644, name: "file.dat", hash: blobHash } ]); var commitHash = yield repo.saveAs("commit", { tree: treeHash, From b65c7a38f18b695e5470d6da9bf8f89bb6c6da95 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 4 Oct 2013 15:06:40 -0500 Subject: [PATCH 033/256] Update BACKERS-2.md --- BACKERS-2.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/BACKERS-2.md b/BACKERS-2.md index b6bfc40..18d523e 100644 --- a/BACKERS-2.md +++ b/BACKERS-2.md @@ -30,7 +30,58 @@ TODO: List 11 people who contributed here. > Your name will be listed in BACKERS-2.md in the main source tree of js-git. -TODO: List 54 people who contributed here. + - servergrove $25.00 Jun 7, 2013 3:06:28 PM + - bluntworks $25.00 Jun 7, 2013 3:51:00 PM + - pdillon $25.00 Jun 7, 2013 4:08:37 PM + - pizzapanther $25.00 Jun 7, 2013 6:08:26 PM + - nschneble $25.00 Jun 7, 2013 11:07:39 PM + - Ohad Assulin $25.00 Jun 8, 2013 6:03:54 AM + - oxy $25.00 Jun 8, 2013 12:06:22 PM + - lettertwo $25.00 Jun 12, 2013 8:32:42 AM + - tmcw $25.00 Jun 13, 2013 12:49:57 PM + - joeandaverde $25.00 Jun 14, 2013 6:39:39 PM + - airportyh $25.00 Jun 18, 2013 2:10:33 PM + - nathanathan $25.00 Jun 21, 2013 4:14:24 PM + - signalwerk $25.00 Jun 22, 2013 1:38:23 PM + - ripta $25.00 Jun 22, 2013 7:01:42 PM + - vaughan $25.00 Jun 22, 2013 10:43:07 PM + - neilk $25.00 Jun 22, 2013 10:45:17 PM + - mikehenrty $25.00 Jun 22, 2013 10:53:50 PM + - vardump $50.00 Jun 23, 2013 5:19:59 AM + - Peter Burns $25.00 Jun 23, 2013 8:56:10 AM + - blittle $25.00 Jun 23, 2013 11:34:08 AM + - Stefan Stoichev $25.00 Jun 23, 2013 12:06:44 PM + - amaxwell01 $25.00 Jun 23, 2013 3:14:05 PM + - dannyfritz $25.00 Jun 26, 2013 9:29:14 AM + - George V. Reilly $25.00 Jun 26, 2013 11:13:30 AM + - euforic $25.00 Jun 26, 2013 2:07:43 PM + - gflarity $25.00 Jun 27, 2013 8:47:16 AM + - generalhenry $25.00 Jun 28, 2013 10:50:17 AM + - piredman $25.00 Jun 30, 2013 11:22:08 PM + - Rebecca $100.00 Jun 9, 2013 1:05:18 PM + - st-luke $25.00 Jun 9, 2013 5:51:50 PM + - asafy $25.00 Jun 11, 2013 7:13:15 AM + - alessioalex $25.00 Jun 11, 2013 12:09:03 PM + - sergi $25.00 Jun 11, 2013 12:50:20 PM + - diversario $25.00 Jun 18, 2013 6:57:00 PM + - seriema $25.00 Jun 19, 2013 11:14:51 AM + - desaintmartin $25.00 Jun 19, 2013 11:28:23 AM + - DinisCruz $40.00 Jun 23, 2013 3:25:56 PM + - gotcha $25.00 Jun 24, 2013 3:43:58 AM + - nikolay $25.00 Jun 24, 2013 5:47:01 PM + - saintedlama $25.00 Jun 24, 2013 10:25:02 PM + - begebot $30.00 Jun 25, 2013 8:13:47 AM + - jbarratt $25.00 Jun 25, 2013 11:57:20 AM + - mikaelkaron $25.00 Jun 25, 2013 9:03:31 PM + - colinscroggins $25.00 Jun 26, 2013 12:02:10 AM + - Eric Elliott $25.00 Jun 26, 2013 2:03:06 AM + - owenb $25.00 Jun 26, 2013 7:26:43 AM + - balupton $25.00 Jul 1, 2013 1:14:29 AM + - fjakobs $25.00 Jul 2, 2013 7:27:52 AM + - romainhuet $25.00 Jul 2, 2013 10:58:13 AM + - angelyordanov $25.00 Jul 7, 2013 4:29:02 PM + - cscott $50.00 Jul 8, 2013 10:43:26 AM + - ilsken $25.00 Jul 8, 2013 11:58:57 AM ## Anonymous Supporters From 1cba31efb0c21b1b63ecbf72efc95b17b26abe46 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 4 Oct 2013 15:09:54 -0500 Subject: [PATCH 034/256] update BACKERS-2.md --- BACKERS-2.md | 104 +++++++++++++++++++++++++-------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/BACKERS-2.md b/BACKERS-2.md index 18d523e..54b974d 100644 --- a/BACKERS-2.md +++ b/BACKERS-2.md @@ -30,58 +30,58 @@ TODO: List 11 people who contributed here. > Your name will be listed in BACKERS-2.md in the main source tree of js-git. - - servergrove $25.00 Jun 7, 2013 3:06:28 PM - - bluntworks $25.00 Jun 7, 2013 3:51:00 PM - - pdillon $25.00 Jun 7, 2013 4:08:37 PM - - pizzapanther $25.00 Jun 7, 2013 6:08:26 PM - - nschneble $25.00 Jun 7, 2013 11:07:39 PM - - Ohad Assulin $25.00 Jun 8, 2013 6:03:54 AM - - oxy $25.00 Jun 8, 2013 12:06:22 PM - - lettertwo $25.00 Jun 12, 2013 8:32:42 AM - - tmcw $25.00 Jun 13, 2013 12:49:57 PM - - joeandaverde $25.00 Jun 14, 2013 6:39:39 PM - - airportyh $25.00 Jun 18, 2013 2:10:33 PM - - nathanathan $25.00 Jun 21, 2013 4:14:24 PM - - signalwerk $25.00 Jun 22, 2013 1:38:23 PM - - ripta $25.00 Jun 22, 2013 7:01:42 PM - - vaughan $25.00 Jun 22, 2013 10:43:07 PM - - neilk $25.00 Jun 22, 2013 10:45:17 PM - - mikehenrty $25.00 Jun 22, 2013 10:53:50 PM - - vardump $50.00 Jun 23, 2013 5:19:59 AM - - Peter Burns $25.00 Jun 23, 2013 8:56:10 AM - - blittle $25.00 Jun 23, 2013 11:34:08 AM - - Stefan Stoichev $25.00 Jun 23, 2013 12:06:44 PM - - amaxwell01 $25.00 Jun 23, 2013 3:14:05 PM - - dannyfritz $25.00 Jun 26, 2013 9:29:14 AM - - George V. Reilly $25.00 Jun 26, 2013 11:13:30 AM - - euforic $25.00 Jun 26, 2013 2:07:43 PM - - gflarity $25.00 Jun 27, 2013 8:47:16 AM - - generalhenry $25.00 Jun 28, 2013 10:50:17 AM - - piredman $25.00 Jun 30, 2013 11:22:08 PM - - Rebecca $100.00 Jun 9, 2013 1:05:18 PM - - st-luke $25.00 Jun 9, 2013 5:51:50 PM - - asafy $25.00 Jun 11, 2013 7:13:15 AM - - alessioalex $25.00 Jun 11, 2013 12:09:03 PM - - sergi $25.00 Jun 11, 2013 12:50:20 PM - - diversario $25.00 Jun 18, 2013 6:57:00 PM - - seriema $25.00 Jun 19, 2013 11:14:51 AM - - desaintmartin $25.00 Jun 19, 2013 11:28:23 AM - - DinisCruz $40.00 Jun 23, 2013 3:25:56 PM - - gotcha $25.00 Jun 24, 2013 3:43:58 AM - - nikolay $25.00 Jun 24, 2013 5:47:01 PM - - saintedlama $25.00 Jun 24, 2013 10:25:02 PM - - begebot $30.00 Jun 25, 2013 8:13:47 AM - - jbarratt $25.00 Jun 25, 2013 11:57:20 AM - - mikaelkaron $25.00 Jun 25, 2013 9:03:31 PM - - colinscroggins $25.00 Jun 26, 2013 12:02:10 AM - - Eric Elliott $25.00 Jun 26, 2013 2:03:06 AM - - owenb $25.00 Jun 26, 2013 7:26:43 AM - - balupton $25.00 Jul 1, 2013 1:14:29 AM - - fjakobs $25.00 Jul 2, 2013 7:27:52 AM - - romainhuet $25.00 Jul 2, 2013 10:58:13 AM - - angelyordanov $25.00 Jul 7, 2013 4:29:02 PM - - cscott $50.00 Jul 8, 2013 10:43:26 AM - - ilsken $25.00 Jul 8, 2013 11:58:57 AM + - servergrove + - bluntworks + - pdillon + - pizzapanther + - nschneble + - Ohad Assulin + - oxy + - lettertwo + - tmcw + - joeandaverde + - airportyh + - nathanathan + - signalwerk + - ripta + - vaughan + - neilk + - mikehenrty + - vardump + - Peter Burns + - blittle + - Stefan Stoichev + - amaxwell01 + - dannyfritz + - George V. Reilly + - euforic + - gflarity + - generalhenry + - piredman + - Rebecca + - st-luke + - asafy + - alessioalex + - sergi + - diversario + - seriema + - desaintmartin + - DinisCruz + - gotcha + - nikolay + - saintedlama + - begebot + - jbarratt + - mikaelkaron + - colinscroggins + - Eric Elliott + - owenb + - balupton + - fjakobs + - romainhuet + - angelyordanov + - cscott + - ilsken ## Anonymous Supporters From a20ab9878d054044b46f657538e9ab674f9bf9dc Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 4 Oct 2013 15:10:02 -0500 Subject: [PATCH 035/256] Update devDeps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 27cece3..6e34ed3 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "url": "git://github.com/creationix/js-git.git" }, "devDependencies": { - "git-fs-db": "~0.1.1", - "git-net": "~0.0.3", + "git-fs-db": "~0.2.0", + "git-net": "~0.0.4", "git-node-platform": "~0.1.4", "gen-run": "~0.1.1" }, From 2fec9f5eb69b0d17cdb400110767979067d7b95d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 4 Oct 2013 15:12:07 -0500 Subject: [PATCH 036/256] Update BACKERS-2.md --- BACKERS-2.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/BACKERS-2.md b/BACKERS-2.md index 54b974d..a8fdbae 100644 --- a/BACKERS-2.md +++ b/BACKERS-2.md @@ -24,7 +24,17 @@ After the successful KickStarter, I decided to do a second fundraiser so that I > >Also you will be listed in BACKERS-2.md for all history to see. -TODO: List 11 people who contributed here. + - jden + - othiym23 + - chrisjpowers + - JohnSz + - sindresorhus + - aeby + - maks + - julien51 + - mofoghlu + - JPBarringer + - jeffslofish ## Basic Supporter From 38dc5d2477dd66c78bf6b1854291cdc7aed78cb9 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 18 Oct 2013 11:39:40 -0500 Subject: [PATCH 037/256] Add the ability to load blobs as text directly --- js-git.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js-git.js b/js-git.js index 1508c8b..ecd4e9f 100644 --- a/js-git.js +++ b/js-git.js @@ -294,6 +294,10 @@ function newRepo(db, workDir) { function onObject(err, object, hash) { if (err) return callback(err); + if (type === "text") { + type = "blob"; + object.body = bops.to(object.body); + } if (object.type !== type) { return new Error("Expected " + type + ", but found " + object.type); } From d8d21a191fd9f52e516f8c969e78430729057c5b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 18 Oct 2013 11:40:45 -0500 Subject: [PATCH 038/256] Alias 'blob' to 'text' when saving for consistency --- js-git.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js-git.js b/js-git.js index ecd4e9f..7242010 100644 --- a/js-git.js +++ b/js-git.js @@ -307,6 +307,7 @@ function newRepo(db, workDir) { function saveAs(type, body, callback) { if (!callback) return saveAs.bind(this, type, body); + if (type === "text") type = "blob"; return save({ type: type, body: body }, callback); } From 437c2b6d58fb0b502bdc5d7dd1aeaf164dfcfa87 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 18 Oct 2013 16:23:07 -0500 Subject: [PATCH 039/256] Bump version to 0.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e34ed3..d845715 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.5.2", + "version": "0.5.3", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From 4852e2fe58be221c1bbf361b7159ed3257955f77 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 21 Oct 2013 12:36:36 -0500 Subject: [PATCH 040/256] Make tree encoding accept Array format too --- js-git.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/js-git.js b/js-git.js index 7242010..2d76281 100644 --- a/js-git.js +++ b/js-git.js @@ -561,20 +561,28 @@ function newRepo(db, workDir) { return bops.from(str + "\n" + tag.message); } - function pathCmp(a, b) { + function pathCmp(oa, ob) { + var a = oa.name; + var b = ob.name; a += "/"; b += "/"; return a < b ? -1 : a > b ? 1 : 0; } function encodeTree(tree) { var chunks = []; - Object.keys(tree).sort(pathCmp).forEach(onName); + if (!Array.isArray(tree)) { + tree = Object.keys(tree).map(function (name) { + var entry = tree[name]; + entry.name = name; + return entry; + }); + } + tree.sort(pathCmp).forEach(onEntry); return bops.join(chunks); - function onName(name) { - var entry = tree[name]; + function onEntry(entry) { chunks.push( - bops.from(entry.mode.toString(8) + " " + name + "\0"), + bops.from(entry.mode.toString(8) + " " + entry.name + "\0"), bops.from(entry.hash, "hex") ); } From e189598631ac995adb4a4e6be5cafa0e8520e5b6 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 21 Oct 2013 15:38:34 -0500 Subject: [PATCH 041/256] Make js-git exit more gracefully when target refs don't exist --- js-git.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js-git.js b/js-git.js index 2d76281..6db65ed 100644 --- a/js-git.js +++ b/js-git.js @@ -129,7 +129,7 @@ function newRepo(db, workDir) { } function onLoad(err, commit, hash) { - if (err) return callback(err); + if (commit === undefined) return callback(err); commit.hash = hash; seen[hash] = true; return callback(null, walk(commit, scan, loadKey, compare)); @@ -243,7 +243,7 @@ function newRepo(db, workDir) { return resolveHashish(hashish, onHash); function onHash(err, result) { - if (err) return callback(err); + if (result === undefined) return callback(err); hash = result; return db.get(hash, onBuffer); } @@ -293,7 +293,7 @@ function newRepo(db, workDir) { return load(hashish, onObject); function onObject(err, object, hash) { - if (err) return callback(err); + if (object === undefined) return callback(err); if (type === "text") { type = "blob"; object.body = bops.to(object.body); From bb18d064bf79320c5719a648fd0f749b82d9ee9f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 22 Oct 2013 20:14:05 -0500 Subject: [PATCH 042/256] Bump version to 0.5.4 and make more robust --- js-git.js | 10 +++++++++- package.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/js-git.js b/js-git.js index 6db65ed..379dd10 100644 --- a/js-git.js +++ b/js-git.js @@ -362,7 +362,9 @@ function newRepo(db, workDir) { if (hash) { return resolveHashish(hash, callback); } - return callback(new Error("Cannot find hashish: " + hashish)); + err = new Error("ENOENT: Cannot find " + hashish); + err.code = "ENOENT"; + return callback(err); } } @@ -373,6 +375,12 @@ function newRepo(db, workDir) { function onBranch(err, result) { if (err) return callback(err); + if (result === undefined) { + return setHead("master", function (err) { + if (err) return callback(err); + onBranch(err, "refs/heads/master"); + }); + } ref = result; return db.set(ref, hash + "\n", callback); } diff --git a/package.json b/package.json index d845715..f67336c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.5.3", + "version": "0.5.4", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From 24294a7f8d650bd8b2fd300df80961fd7e198ebd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 6 Nov 2013 11:10:13 -0600 Subject: [PATCH 043/256] Send back refs when fetch is done --- js-git.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/js-git.js b/js-git.js index 379dd10..02ee112 100644 --- a/js-git.js +++ b/js-git.js @@ -723,7 +723,7 @@ function newRepo(db, workDir) { function onPackStream(err, raw) { if (err) return callback(err); - if (!raw) return remote.close(callback); + if (!raw) return remote.close(onDone); var packStream = parse(raw); return unpack(packStream, opts, onUnpack); } @@ -742,7 +742,7 @@ function newRepo(db, workDir) { function next(err) { if (err) return callback(err); ref = queue.shift(); - if (!ref) return setHead(branch, callback); + if (!ref) return setHead(branch, onDone); if (ref === "HEAD" || /{}$/.test(ref)) return next(); hash = refs[ref]; if (!branch && (hash === refs.HEAD)) branch = ref.substr(11); @@ -754,6 +754,11 @@ function newRepo(db, workDir) { if (!has) return next(); return db.set(ref, hash + "\n", next); } + + function onDone(err) { + if (err) return callback(err); + return callback(null, refs); + } } function processCaps(opts, serverCaps) { @@ -862,7 +867,7 @@ function newRepo(db, workDir) { } function push() { - throw new Error("TODO: Implement repo.fetch"); + throw new Error("TODO: Implement repo.push"); } function unpack(packStream, opts, callback) { From 079204f5dfb6a562341ae026853811c8440b539a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Nov 2013 21:47:04 -0600 Subject: [PATCH 044/256] Start to reorganize code to not need platform injection everywhere --- .gitignore | 1 + examples/clone.js | 2 +- examples/create-harmony.js | 2 +- examples/create.js | 2 +- examples/ls-remote.js | 2 +- examples/read-harmony.js | 2 +- examples/read.js | 2 +- examples/walk.js | 2 +- js-git.js | 302 +------------ lib/agent.js | 2 + lib/apply-delta.js | 94 ++++ lib/decode-pack.js | 188 ++++++++ lib/decoders.js | 105 +++++ lib/deframe.js | 18 + lib/encoders.js | 76 ++++ lib/frame.js | 8 + lib/indexof.js | 8 + lib/inflate.js | 853 +++++++++++++++++++++++++++++++++++++ lib/parseascii.js | 7 + lib/parsedec.js | 7 + lib/parseoct.js | 7 + lib/parsetohex.js | 10 + lib/pathcmp.js | 6 + lib/sha1.js | 144 +++++++ lib/trace.js | 1 + package.json | 3 +- test/test-sha1.js | 51 +++ 27 files changed, 1616 insertions(+), 289 deletions(-) create mode 100644 lib/agent.js create mode 100644 lib/apply-delta.js create mode 100644 lib/decode-pack.js create mode 100644 lib/decoders.js create mode 100644 lib/deframe.js create mode 100644 lib/encoders.js create mode 100644 lib/frame.js create mode 100644 lib/indexof.js create mode 100644 lib/inflate.js create mode 100644 lib/parseascii.js create mode 100644 lib/parsedec.js create mode 100644 lib/parseoct.js create mode 100644 lib/parsetohex.js create mode 100644 lib/pathcmp.js create mode 100644 lib/sha1.js create mode 100644 lib/trace.js create mode 100644 test/test-sha1.js diff --git a/.gitignore b/.gitignore index b9e6895..6161444 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.git node_modules .zedstate +tags diff --git a/examples/clone.js b/examples/clone.js index 3adddf2..16ec3a4 100644 --- a/examples/clone.js +++ b/examples/clone.js @@ -1,5 +1,5 @@ var platform = require('git-node-platform'); -var jsGit = require('../.')(platform); +var jsGit = require('../.'); var gitRemote = require('git-net')(platform); var fsDb = require('git-fs-db')(platform); var fs = platform.fs; diff --git a/examples/create-harmony.js b/examples/create-harmony.js index a63dd48..29d821d 100644 --- a/examples/create-harmony.js +++ b/examples/create-harmony.js @@ -1,6 +1,6 @@ "use strict"; let platform = require('git-node-platform'); -let jsGit = require('../.')(platform); +let jsGit = require('../.'); let fsDb = require('git-fs-db')(platform); let fs = platform.fs; let run = require('gen-run'); diff --git a/examples/create.js b/examples/create.js index 2803f4b..01b94df 100644 --- a/examples/create.js +++ b/examples/create.js @@ -1,5 +1,5 @@ var platform = require('git-node-platform'); -var jsGit = require('../.')(platform); +var jsGit = require('../.'); var fsDb = require('git-fs-db')(platform); var fs = platform.fs; diff --git a/examples/ls-remote.js b/examples/ls-remote.js index 9128185..f249dc5 100644 --- a/examples/ls-remote.js +++ b/examples/ls-remote.js @@ -1,5 +1,5 @@ var platform = require('git-node-platform'); -var jsGit = require('../.')(platform); +var jsGit = require('../.'); var gitRemote = require('git-net')(platform); var repo = jsGit(); diff --git a/examples/read-harmony.js b/examples/read-harmony.js index 2e536ad..16677bf 100644 --- a/examples/read-harmony.js +++ b/examples/read-harmony.js @@ -1,6 +1,6 @@ "use strict"; let platform = require('git-node-platform'); -let jsGit = require('../.')(platform); +let jsGit = require('../.'); let fsDb = require('git-fs-db')(platform); let fs = platform.fs; let run = require('gen-run'); diff --git a/examples/read.js b/examples/read.js index fcf82b2..2eb5082 100644 --- a/examples/read.js +++ b/examples/read.js @@ -1,5 +1,5 @@ var platform = require('git-node-platform'); -var jsGit = require('../.')(platform); +var jsGit = require('../.'); var fsDb = require('git-fs-db')(platform); var fs = platform.fs; diff --git a/examples/walk.js b/examples/walk.js index ced7e10..63f4103 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -1,5 +1,5 @@ var platform = require('git-node-platform'); -var jsGit = require('../.')(platform); +var jsGit = require('../.'); var fsDb = require('git-fs-db')(platform); var fs = platform.fs; diff --git a/js-git.js b/js-git.js index 02ee112..81c4004 100644 --- a/js-git.js +++ b/js-git.js @@ -1,38 +1,20 @@ -var platform; -var applyDelta, pushToPull, parse, sha1, bops, trace; - -module.exports = function (imports) { - if (platform) return newRepo; - - platform = imports; - applyDelta = require('git-pack-codec/apply-delta.js')(platform); - pushToPull = require('push-to-pull'); - parse = pushToPull(require('git-pack-codec/decode.js')(platform)); - platform.agent = platform.agent || "js-git/" + require('./package.json').version; - sha1 = platform.sha1; - bops = platform.bops; - trace = platform.trace; - - return newRepo; -}; +var parseAscii = require('./lib/parseascii.js'); +var encoders = require('./lib/encoders.js'); +var decoders = require('./lib/decoders.js'); +var frame = require('./lib/frame.js'); +var deframe = require('./lib/deframe.js'); +var sha1 = require('./lib/sha1.js'); +var agent = require('./lib/agent.js'); +var trace = require('./lib/trace.js'); +var applyDelta = require('./lib/apply-delta.js'); +var pushToPull = require('push-to-pull'); +var parse = pushToPull(require('./lib/decode-pack.js')); + +module.exports = newRepo; function newRepo(db, workDir) { if (!db) throw new TypeError("A db interface instance is required"); - var encoders = { - commit: encodeCommit, - tag: encodeTag, - tree: encodeTree, - blob: encodeBlob - }; - - var decoders = { - commit: decodeCommit, - tag: decodeTag, - tree: decodeTree, - blob: decodeBlob - }; - var repo = {}; if (trace) { @@ -47,20 +29,20 @@ function newRepo(db, workDir) { } // Git Objects - repo.load = load; // (hashish) -> object + repo.load = load; // (hash-ish) -> object repo.save = save; // (object) -> hash - repo.loadAs = loadAs; // (type, hashish) -> value + repo.loadAs = loadAs; // (type, hash-ish) -> value repo.saveAs = saveAs; // (type, value) -> hash - repo.remove = remove; // (hashish) + repo.remove = remove; // (hash-ish) repo.unpack = unpack; // (opts, packStream) // Convenience Readers - repo.logWalk = logWalk; // (hashish) => stream - repo.treeWalk = treeWalk; // (hashish) => stream + repo.logWalk = logWalk; // (hash-ish) => stream + repo.treeWalk = treeWalk; // (hash-ish) => stream repo.walk = walk; // (seed, scan, compare) -> stream // Refs - repo.resolveHashish = resolveHashish; // (hashish) -> hash + repo.resolveHashish = resolveHashish; // (hash-ish) -> hash repo.updateHead = updateHead; // (hash) repo.getHead = getHead; // () -> ref repo.setHead = setHead; // (ref) @@ -296,7 +278,7 @@ function newRepo(db, workDir) { if (object === undefined) return callback(err); if (type === "text") { type = "blob"; - object.body = bops.to(object.body); + object.body = parseAscii(object.body, 0, object.body.length); } if (object.type !== type) { return new Error("Expected " + type + ", but found " + object.type); @@ -460,248 +442,6 @@ function newRepo(db, workDir) { } } - function indexOf(buffer, byte, i) { - i |= 0; - var length = buffer.length; - for (;;i++) { - if (i >= length) return -1; - if (buffer[i] === byte) return i; - } - } - - function parseAscii(buffer, start, end) { - var val = ""; - while (start < end) { - val += String.fromCharCode(buffer[start++]); - } - return val; - } - - function parseDec(buffer, start, end) { - var val = 0; - while (start < end) { - val = val * 10 + buffer[start++] - 0x30; - } - return val; - } - - function parseOct(buffer, start, end) { - var val = 0; - while (start < end) { - val = (val << 3) + buffer[start++] - 0x30; - } - return val; - } - - function deframe(buffer) { - var space = indexOf(buffer, 0x20); - if (space < 0) throw new Error("Invalid git object buffer"); - var nil = indexOf(buffer, 0x00, space); - if (nil < 0) throw new Error("Invalid git object buffer"); - var body = bops.subarray(buffer, nil + 1); - var size = parseDec(buffer, space + 1, nil); - if (size !== body.length) throw new Error("Invalid body length."); - return [ - parseAscii(buffer, 0, space), - body - ]; - } - - function frame(type, body) { - return bops.join([ - bops.from(type + " " + body.length + "\0"), - body - ]); - } - - // A sequence of bytes not containing the ASCII character byte - // values NUL (0x00), LF (0x0a), '<' (0c3c), or '>' (0x3e). - // The sequence may not begin or end with any bytes with the - // following ASCII character byte values: SPACE (0x20), - // '.' (0x2e), ',' (0x2c), ':' (0x3a), ';' (0x3b), '<' (0x3c), - // '>' (0x3e), '"' (0x22), "'" (0x27). - function safe(string) { - return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); - } - - function formatDate(date) { - var timezone = (date.timeZoneoffset || date.getTimezoneOffset()) / 60; - var seconds = Math.floor(date.getTime() / 1000); - return seconds + " " + (timezone > 0 ? "-0" : "0") + timezone + "00"; - } - - function encodePerson(person) { - if (!person.name || !person.email) { - throw new TypeError("Name and email are required for person fields"); - } - return safe(person.name) + - " <" + safe(person.email) + "> " + - formatDate(person.date || new Date()); - } - - function encodeCommit(commit) { - if (!commit.tree || !commit.author || !commit.message) { - throw new TypeError("Tree, author, and message are require for commits"); - } - var parents = commit.parents || (commit.parent ? [ commit.parent ] : []); - if (!Array.isArray(parents)) { - throw new TypeError("Parents must be an array"); - } - var str = "tree " + commit.tree; - for (var i = 0, l = parents.length; i < l; ++i) { - str += "\nparent " + parents[i]; - } - str += "\nauthor " + encodePerson(commit.author) + - "\ncommitter " + encodePerson(commit.committer || commit.author) + - "\n\n" + commit.message; - return bops.from(str); - } - - function encodeTag(tag) { - if (!tag.object || !tag.type || !tag.tag || !tag.tagger || !tag.message) { - throw new TypeError("Object, type, tag, tagger, and message required"); - } - var str = "object " + tag.object + - "\ntype " + tag.type + - "\ntag " + tag.tag + - "\ntagger " + encodePerson(tag.tagger) + - "\n\n" + tag.message; - return bops.from(str + "\n" + tag.message); - } - - function pathCmp(oa, ob) { - var a = oa.name; - var b = ob.name; - a += "/"; b += "/"; - return a < b ? -1 : a > b ? 1 : 0; - } - - function encodeTree(tree) { - var chunks = []; - if (!Array.isArray(tree)) { - tree = Object.keys(tree).map(function (name) { - var entry = tree[name]; - entry.name = name; - return entry; - }); - } - tree.sort(pathCmp).forEach(onEntry); - return bops.join(chunks); - - function onEntry(entry) { - chunks.push( - bops.from(entry.mode.toString(8) + " " + entry.name + "\0"), - bops.from(entry.hash, "hex") - ); - } - } - - function encodeBlob(blob) { - if (bops.is(blob)) return blob; - return bops.from(blob); - } - - function decodePerson(string) { - var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); - if (!match) throw new Error("Improperly formatted person string"); - var sec = parseInt(match[3], 10); - var date = new Date(sec * 1000); - date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; - return { - name: match[1], - email: match[2], - date: date - }; - } - - - function decodeCommit(body) { - var i = 0; - var start; - var key; - var parents = []; - var commit = { - tree: "", - parents: parents, - author: "", - committer: "", - message: "" - }; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = bops.to(bops.subarray(body, start, i++)); - if (key === "parent") { - parents.push(value); - } - else { - if (key === "author" || key === "committer") { - value = decodePerson(value); - } - commit[key] = value; - } - } - i++; - commit.message = bops.to(bops.subarray(body, i)); - return commit; - } - - function decodeTag(body) { - var i = 0; - var start; - var key; - var tag = {}; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = bops.to(bops.subarray(body, start, i++)); - if (key === "tagger") value = decodePerson(value); - tag[key] = value; - } - i++; - tag.message = bops.to(bops.subarray(body, i)); - return tag; - } - - function decodeTree(body) { - var i = 0; - var length = body.length; - var start; - var mode; - var name; - var hash; - var tree = []; - while (i < length) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - mode = parseOct(body, start, i++); - start = i; - i = indexOf(body, 0x00, start); - name = bops.to(bops.subarray(body, start, i++)); - hash = bops.to(bops.subarray(body, i, i += 20), "hex"); - tree.push({ - mode: mode, - name: name, - hash: hash - }); - } - return tree; - } - - function decodeBlob(body) { - return body; - } function fetch(remote, opts, callback) { if (!callback) return fetch.bind(this, remote, opts); @@ -773,7 +513,7 @@ function newRepo(db, workDir) { caps.push("no-progress"); } } - if (serverCaps.agent) caps.push("agent=" + platform.agent); + if (serverCaps.agent) caps.push("agent=" + agent); return caps; } diff --git a/lib/agent.js b/lib/agent.js new file mode 100644 index 0000000..565482d --- /dev/null +++ b/lib/agent.js @@ -0,0 +1,2 @@ +var meta = require('../package.json'); +module.exports = meta.name + "/" + meta.version; diff --git a/lib/apply-delta.js b/lib/apply-delta.js new file mode 100644 index 0000000..20a7f00 --- /dev/null +++ b/lib/apply-delta.js @@ -0,0 +1,94 @@ +// This is Chris Dickinson's code + +var binary = require('bops') + , Decoder = require('varint/decode.js') + , vi = new Decoder + +// we use writeUint[8|32][LE|BE] instead of indexing +// into buffers so that we get buffer-browserify compat. +var OFFSET_BUFFER = binary.create(4) + , LENGTH_BUFFER = binary.create(4) + +module.exports = apply_delta; +function apply_delta(delta, target) { + var base_size_info = {size: null, buffer: null} + , resized_size_info = {size: null, buffer: null} + , output_buffer + , out_idx + , command + , len + , idx + + delta_header(delta, base_size_info) + delta_header(base_size_info.buffer, resized_size_info) + + delta = resized_size_info.buffer + + idx = + out_idx = 0 + output_buffer = binary.create(resized_size_info.size) + + len = delta.length + + while(idx < len) { + command = delta[idx++] + command & 0x80 ? copy() : insert() + } + + return output_buffer + + function copy() { + binary.writeUInt32LE(OFFSET_BUFFER, 0, 0) + binary.writeUInt32LE(LENGTH_BUFFER, 0, 0) + + var check = 1 + , length + , offset + + for(var x = 0; x < 4; ++x) { + if(command & check) { + OFFSET_BUFFER[3 - x] = delta[idx++] + } + check <<= 1 + } + + for(var x = 0; x < 3; ++x) { + if(command & check) { + LENGTH_BUFFER[3 - x] = delta[idx++] + } + check <<= 1 + } + LENGTH_BUFFER[0] = 0 + + length = binary.readUInt32BE(LENGTH_BUFFER, 0) || 0x10000 + offset = binary.readUInt32BE(OFFSET_BUFFER, 0) + + binary.copy(target, output_buffer, out_idx, offset, offset + length) + out_idx += length + } + + function insert() { + binary.copy(delta, output_buffer, out_idx, idx, command + idx) + idx += command + out_idx += command + } +} + +function delta_header(buf, output) { + var done = false + , idx = 0 + , size = 0 + + vi.ondata = function(s) { + size = s + done = true + } + + do { + vi.write(buf[idx++]) + } while(!done) + + output.size = size + output.buffer = binary.subarray(buf, idx) + +} \ No newline at end of file diff --git a/lib/decode-pack.js b/lib/decode-pack.js new file mode 100644 index 0000000..b97af55 --- /dev/null +++ b/lib/decode-pack.js @@ -0,0 +1,188 @@ + +var types = { + "1": "commit", + "2": "tree", + "3": "blob", + "4": "tag", + "6": "ofs-delta", + "7": "ref-delta" +}; + +var inflate = require('./inflate.js'); +var sha1 = require('./sha1.js'); +var bops = require('bops'); + +module.exports = function (emit) { + + var state = $pack; + var sha1sum = sha1(); + var inf = inflate(); + + var offset = 0; + var position = 0; + var version = 0x4b434150; // PACK reversed + var num = 0; + var type = 0; + var length = 0; + var ref = null; + var checksum = ""; + var start = 0; + var parts = []; + + + return function (chunk) { + if (chunk === undefined) { + if (num || checksum.length < 40) throw new Error("Unexpected end of input stream"); + return emit(); + } + + for (var i = 0, l = chunk.length; i < l; i++) { + // console.log([state, i, chunk[i].toString(16)]); + if (!state) throw new Error("Unexpected extra bytes: " + bops.subarray(chunk, i)); + state = state(chunk[i], i, chunk); + position++; + } + if (!state) return; + if (state !== $checksum) sha1sum.update(chunk); + var buff = inf.flush(); + if (buff.length) { + parts.push(buff); + } + }; + + // The first four bytes in a packfile are the bytes 'PACK' + function $pack(byte) { + if ((version & 0xff) === byte) { + version >>>= 8; + return version ? $pack : $version; + } + throw new Error("Invalid packfile header"); + } + + // The version is stored as an unsigned 32 integer in network byte order. + // It must be version 2 or 3. + function $version(byte) { + version = (version << 8) | byte; + if (++offset < 4) return $version; + if (version >= 2 && version <= 3) { + offset = 0; + return $num; + } + throw new Error("Invalid version number " + num); + } + + // The number of objects in this packfile is also stored as an unsigned 32 bit int. + function $num(byte) { + num = (num << 8) | byte; + if (++offset < 4) return $num; + offset = 0; + emit({version: version, num: num}); + return $header; + } + + // n-byte type and length (3-bit type, (n-1)*7+4-bit length) + // CTTTSSSS + // C is continue bit, TTT is type, S+ is length + function $header(byte) { + if (start === 0) start = position; + type = byte >> 4 & 0x07; + length = byte & 0x0f; + if (byte & 0x80) { + offset = 4; + return $header2; + } + return afterHeader(); + } + + // Second state in the same header parsing. + // CSSSSSSS* + function $header2(byte) { + length |= (byte & 0x7f) << offset; + if (byte & 0x80) { + offset += 7; + return $header2; + } + return afterHeader(); + } + + // Common helper for finishing tiny and normal headers. + function afterHeader() { + offset = 0; + if (type === 6) { + ref = 0; + return $ofsDelta; + } + if (type === 7) { + ref = ""; + return $refDelta; + } + return $body; + } + + // Big-endian modified base 128 number encoded ref offset + function $ofsDelta(byte) { + ref = byte & 0x7f; + if (byte & 0x80) return $ofsDelta2; + return $body; + } + + function $ofsDelta2(byte) { + ref = ((ref + 1) << 7) | (byte & 0x7f); + if (byte & 0x80) return $ofsDelta2; + return $body; + } + + // 20 byte raw sha1 hash for ref + function $refDelta(byte) { + ref += toHex(byte); + if (++offset < 20) return $refDelta; + return $body; + } + + // Common helper for generating 2-character hex numbers + function toHex(num) { + return num < 0x10 ? "0" + num.toString(16) : num.toString(16); + } + + // Common helper for emitting all three object shapes + function emitObject() { + var item = { + type: types[type], + size: length, + body: bops.join(parts), + offset: start + }; + if (ref) item.ref = ref; + parts.length = 0; + start = 0; + offset = 0; + type = 0; + length = 0; + ref = null; + emit(item); + } + + // Feed the deflated code to the inflate engine + function $body(byte, i, chunk) { + if (inf.write(byte)) return $body; + var buf = inf.flush(); + inf.recycle(); + if (buf.length) { + parts.push(buf); + } + emitObject(); + // If this was all the objects, start calculating the sha1sum + if (--num) return $header; + sha1sum.update(bops.subarray(chunk, 0, i + 1)); + return $checksum; + } + + // 20 byte checksum + function $checksum(byte) { + checksum += toHex(byte); + if (++offset < 20) return $checksum; + var actual = sha1sum.digest(); + if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum); + } + +}; diff --git a/lib/decoders.js b/lib/decoders.js new file mode 100644 index 0000000..9713e60 --- /dev/null +++ b/lib/decoders.js @@ -0,0 +1,105 @@ +var indexOf = require('./indexof.js'); +var parseOct = require('./parseoct.js'); +var parseAscii = require('./parseascii.js'); +var parseToHex = require('./parsetohex.js'); + +exports.commit = function decodeCommit(body) { + var i = 0; + var start; + var key; + var parents = []; + var commit = { + tree: "", + parents: parents, + author: "", + committer: "", + message: "" + }; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = parseAscii(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = parseAscii(body, start, i++); + if (key === "parent") { + parents.push(value); + } + else { + if (key === "author" || key === "committer") { + value = decodePerson(value); + } + commit[key] = value; + } + } + i++; + commit.message = parseAscii(body, i, body.length); + return commit; +}; + +exports.tag = function decodeTag(body) { + var i = 0; + var start; + var key; + var tag = {}; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = parseAscii(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = parseAscii(body, start, i++); + if (key === "tagger") value = decodePerson(value); + tag[key] = value; + } + i++; + tag.message = parseAscii(body, i, body.length); + return tag; +}; + +exports.tree = function decodeTree(body) { + var i = 0; + var length = body.length; + var start; + var mode; + var name; + var hash; + var tree = []; + while (i < length) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + mode = parseOct(body, start, i++); + start = i; + i = indexOf(body, 0x00, start); + name = parseAscii(body, start, i++); + hash = parseToHex(body, i, i += 20); + tree.push({ + mode: mode, + name: name, + hash: hash + }); + } + return tree; +}; + +exports.blob = function decodeBlob(body) { + return body; +}; + +function decodePerson(string) { + var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); + if (!match) throw new Error("Improperly formatted person string"); + var sec = parseInt(match[3], 10); + var date = new Date(sec * 1000); + date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; + return { + name: match[1], + email: match[2], + date: date + }; +} diff --git a/lib/deframe.js b/lib/deframe.js new file mode 100644 index 0000000..30d02e6 --- /dev/null +++ b/lib/deframe.js @@ -0,0 +1,18 @@ +var bops = require('bops'); +var indexOf = require('./indexof.js'); +var parseDec = require('./parsedec.js'); +var parseAscii = require('./parseascii.js'); + +module.exports = function deframe(buffer) { + var space = indexOf(buffer, 0x20); + if (space < 0) throw new Error("Invalid git object buffer"); + var nil = indexOf(buffer, 0x00, space); + if (nil < 0) throw new Error("Invalid git object buffer"); + var body = bops.subarray(buffer, nil + 1); + var size = parseDec(buffer, space + 1, nil); + if (size !== body.length) throw new Error("Invalid body length."); + return [ + parseAscii(buffer, 0, space), + body + ]; +}; diff --git a/lib/encoders.js b/lib/encoders.js new file mode 100644 index 0000000..1462efc --- /dev/null +++ b/lib/encoders.js @@ -0,0 +1,76 @@ +var bops = require('bops'); +var pathCmp = require('./pathcmp.js'); + +exports.commit = function encodeCommit(commit) { + if (!commit.tree || !commit.author || !commit.message) { + throw new TypeError("Tree, author, and message are require for commits"); + } + var parents = commit.parents || (commit.parent ? [ commit.parent ] : []); + if (!Array.isArray(parents)) { + throw new TypeError("Parents must be an array"); + } + var str = "tree " + commit.tree; + for (var i = 0, l = parents.length; i < l; ++i) { + str += "\nparent " + parents[i]; + } + str += "\nauthor " + encodePerson(commit.author) + + "\ncommitter " + encodePerson(commit.committer || commit.author) + + "\n\n" + commit.message; + return bops.from(str); +}; + +exports.tag = function encodeTag(tag) { + if (!tag.object || !tag.type || !tag.tag || !tag.tagger || !tag.message) { + throw new TypeError("Object, type, tag, tagger, and message required"); + } + var str = "object " + tag.object + + "\ntype " + tag.type + + "\ntag " + tag.tag + + "\ntagger " + encodePerson(tag.tagger) + + "\n\n" + tag.message; + return bops.from(str + "\n" + tag.message); +}; + +exports.tree = function encodeTree(tree) { + var chunks = []; + if (!Array.isArray(tree)) { + tree = Object.keys(tree).map(function (name) { + var entry = tree[name]; + entry.name = name; + return entry; + }); + } + tree.sort(pathCmp).forEach(onEntry); + return bops.join(chunks); + + function onEntry(entry) { + chunks.push( + bops.from(entry.mode.toString(8) + " " + entry.name + "\0"), + bops.from(entry.hash, "hex") + ); + } +}; + +exports.blob = function encodeBlob(blob) { + if (bops.is(blob)) return blob; + return bops.from(blob); +}; + +function encodePerson(person) { + if (!person.name || !person.email) { + throw new TypeError("Name and email are required for person fields"); + } + return safe(person.name) + + " <" + safe(person.email) + "> " + + formatDate(person.date || new Date()); +} + +function safe(string) { + return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); +} + +function formatDate(date) { + var timezone = (date.timeZoneoffset || date.getTimezoneOffset()) / 60; + var seconds = Math.floor(date.getTime() / 1000); + return seconds + " " + (timezone > 0 ? "-0" : "0") + timezone + "00"; +} diff --git a/lib/frame.js b/lib/frame.js new file mode 100644 index 0000000..3717b83 --- /dev/null +++ b/lib/frame.js @@ -0,0 +1,8 @@ +var bops = require('bops'); + +module.exports = function frame(type, body) { + return bops.join([ + bops.from(type + " " + body.length + "\0"), + body + ]); +}; diff --git a/lib/indexof.js b/lib/indexof.js new file mode 100644 index 0000000..18c61a5 --- /dev/null +++ b/lib/indexof.js @@ -0,0 +1,8 @@ +module.exports = function indexOf(buffer, byte, i) { + i |= 0; + var length = buffer.length; + for (;;i++) { + if (i >= length) return -1; + if (buffer[i] === byte) return i; + } +}; diff --git a/lib/inflate.js b/lib/inflate.js new file mode 100644 index 0000000..48dc528 --- /dev/null +++ b/lib/inflate.js @@ -0,0 +1,853 @@ +var bops = require('bops'); + +// Wrapper for proposed new API to inflate: +// +// var inf = inflate(); +// inf.write(byte) -> more - Write a byte to inflate's state-machine. +// Returns true if more data is expected. +// inf.recycle() - Reset the internal state machine. +// inf.flush() -> data - Flush the output as a binary buffer. +// +// This is quite slow, but could be made fast if baked into inflate itself. +module.exports = function () { + var push = inflate(onEmit, onUnused); + var more = true; + var chunks = []; + var b = bops.create(1); + + return { write: write, recycle: recycle, flush: flush }; + + function write(byte) { + b[0] = byte; + push(null, b); + return more; + } + + function recycle() { + push.recycle(); + more = true; + } + + function flush() { + var buffer = bops.join(chunks); + chunks.length = 0; + return buffer; + } + + function onEmit(err, item) { + if (err) throw err; + if (item === undefined) { + // console.log("onEnd"); + more = false; + return; + } + chunks.push(item); + } + + function onUnused(chunks) { + // console.log("onUnused", chunks); + more = false; + } +}; + +var MAXBITS = 15 + , MAXLCODES = 286 + , MAXDCODES = 30 + , MAXCODES = (MAXLCODES+MAXDCODES) + , FIXLCODES = 288 + +var lens = [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 +] + +var lext = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 +] + +var dists = [ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 +] + +var dext = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13 +] + +var order = [ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 +] + +var WINDOW = 32768 + , WINDOW_MINUS_ONE = WINDOW - 1 + +function inflate(emit, on_unused) { + var output = new Uint8Array(WINDOW) + , need_input = false + , buffer_offset = 0 + , bytes_read = 0 + , output_idx = 0 + , ended = false + , state = null + , states = [] + , buffer = [] + , got = 0 + + // buffer up to 128k "output one" bytes + var OUTPUT_ONE_LENGTH = 131070 + , output_one_offs = OUTPUT_ONE_LENGTH + , output_one_buf + + var bitbuf = 0 + , bitcnt = 0 + , is_final = false + , fixed_codes + + var adler_s1 = 1 + , adler_s2 = 0 + + onread.recycle = function recycle() { + var out + buffer.length = 0 + buffer_offset = 0 + output_idx = 0 + bitbuf = 0 + bitcnt = 0 + states.length = 0 + is_final = false + need_input = false + bytes_read = 0 + output_idx = 0 + ended = false + got = 0 + adler_s1 = 1 + adler_s2 = 0 + output_one_offs = 0 + become(noop, {}, noop) + start_stream_header() + // return stream + } + + var bytes_need = 0 + , bytes_value = [] + + var bits_need = 0 + , bits_value = [] + + var codes_distcode = null + , codes_lencode = null + , codes_len = 0 + , codes_dist = 0 + , codes_symbol = 0 + + var dynamic_distcode = {symbol: [], count: []} + , dynamic_lencode = {symbol: [], count: []} + , dynamic_lengths = [] + , dynamic_nlen = 0 + , dynamic_ndist = 0 + , dynamic_ncode = 0 + , dynamic_index = 0 + , dynamic_symbol = 0 + , dynamic_len = 0 + + var decode_huffman = null + , decode_len = 0 + , decode_code = 0 + , decode_first = 0 + , decode_count = 0 + , decode_index = 0 + + var last = null + + become(noop, {}, noop) + start_stream_header() + + return onread + + function onread(err, buf) { + if(buf === undefined) { + return emit(err) + } + + return write(buf) + } + + function noop() { + + } + + function call_header() { + } + + function call_bytes(need) { + bytes_value.length = 0 + bytes_need = need + } + + function call_bits(need) { + bits_value = 0 + bits_need = need + } + + function call_codes(distcode, lencode) { + codes_len = + codes_dist = + codes_symbol = 0 + codes_distcode = distcode + codes_lencode = lencode + } + + function call_dynamic() { + dynamic_distcode.symbol.length = + dynamic_distcode.count.length = + dynamic_lencode.symbol.length = + dynamic_lencode.count.length = + dynamic_lengths.length = 0 + dynamic_nlen = 0 + dynamic_ndist = 0 + dynamic_ncode = 0 + dynamic_index = 0 + dynamic_symbol = 0 + dynamic_len = 0 + } + + function call_decode(h) { + decode_huffman = h + decode_len = 1 + decode_first = + decode_index = + decode_code = 0 + } + + function write(buf) { + buffer.push(buf) + got += buf.length + if(!ended) { + execute() + } + } + + function execute() { + do { + states[0].current() + } while(!need_input && !ended) + + var needed = need_input + need_input = false + } + + function start_stream_header() { + become(bytes, call_bytes(2), got_stream_header) + } + + function got_stream_header() { + var cmf = last[0] + , flg = last[1] + + + if((cmf << 8 | flg) % 31 !== 0) { + emit(new Error( + 'failed header check' + )) + return + } + + + + + if(flg & 32) { + return become(bytes, call_bytes(4), on_got_fdict) + } + return become(bits, call_bits(1), on_got_is_final) + } + + + + + function on_got_fdict() { + return become(bits, call_bits(1), on_got_is_final) + } + + + + + + + + + function on_got_is_final() { + is_final = last + become(bits, call_bits(2), on_got_type) + } + + + + + + + + + + + + + function on_got_type() { + if(last === 0) { + become(bytes, call_bytes(4), on_got_len_nlen) + return + } + + if(last === 1) { + // `fixed` and `dynamic` blocks both eventually delegate + // to the "codes" state -- which reads bits of input, throws + // them into a huffman tree, and produces "symbols" of output. + fixed_codes = fixed_codes || build_fixed() + become(start_codes, call_codes( + fixed_codes.distcode + , fixed_codes.lencode + ), done_with_codes) + return + } + + become(start_dynamic, call_dynamic(), done_with_codes) + return + } + + + + + function on_got_len_nlen() { + var want = last[0] | (last[1] << 8) + , nlen = last[2] | (last[3] << 8) + + if((~nlen & 0xFFFF) !== want) { + emit(new Error( + 'failed len / nlen check' + )) + } + + if(!want) { + become(bits, call_bits(1), on_got_is_final) + return + } + become(bytes, call_bytes(want), on_got_stored) + } + + + + + function on_got_stored() { + output_many(last) + if(is_final) { + become(bytes, call_bytes(4), on_got_adler) + return + } + become(bits, call_bits(1), on_got_is_final) + } + + + + + + + function start_dynamic() { + become(bits, call_bits(5), on_got_nlen) + } + + function on_got_nlen() { + dynamic_nlen = last + 257 + become(bits, call_bits(5), on_got_ndist) + } + + function on_got_ndist() { + dynamic_ndist = last + 1 + become(bits, call_bits(4), on_got_ncode) + } + + function on_got_ncode() { + dynamic_ncode = last + 4 + if(dynamic_nlen > MAXLCODES || dynamic_ndist > MAXDCODES) { + emit(new Error('bad counts')) + return + } + + become(bits, call_bits(3), on_got_lengths_part) + } + + function on_got_lengths_part() { + dynamic_lengths[order[dynamic_index]] = last + + ++dynamic_index + if(dynamic_index === dynamic_ncode) { + for(; dynamic_index < 19; ++dynamic_index) { + dynamic_lengths[order[dynamic_index]] = 0 + } + + // temporarily construct the `lencode` using the + // lengths we've read. we'll actually be using the + // symbols produced by throwing bits into the huffman + // tree to constuct the `lencode` and `distcode` huffman + // trees. + construct(dynamic_lencode, dynamic_lengths, 19) + dynamic_index = 0 + + become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) + return + } + become(bits, call_bits(3), on_got_lengths_part) + } + + function on_got_dynamic_symbol_iter() { + dynamic_symbol = last + + if(dynamic_symbol < 16) { + dynamic_lengths[dynamic_index++] = dynamic_symbol + do_check() + return + } + + dynamic_len = 0 + if(dynamic_symbol === 16) { + become(bits, call_bits(2), on_got_dynamic_symbol_16) + return + } + + if(dynamic_symbol === 17) { + become(bits, call_bits(3), on_got_dynamic_symbol_17) + return + } + + become(bits, call_bits(7), on_got_dynamic_symbol) + } + + function on_got_dynamic_symbol_16() { + dynamic_len = dynamic_lengths[dynamic_index - 1] + on_got_dynamic_symbol_17() + } + + function on_got_dynamic_symbol_17() { + dynamic_symbol = 3 + last + do_dynamic_end_loop() + } + + function on_got_dynamic_symbol() { + dynamic_symbol = 11 + last + do_dynamic_end_loop() + } + + function do_dynamic_end_loop() { + if(dynamic_index + dynamic_symbol > dynamic_nlen + dynamic_ndist) { + emit(new Error('too many lengths')) + return + } + + while(dynamic_symbol--) { + dynamic_lengths[dynamic_index++] = dynamic_len + } + + do_check() + } + + function do_check() { + if(dynamic_index >= dynamic_nlen + dynamic_ndist) { + end_read_dynamic() + return + } + become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) + } + + function end_read_dynamic() { + // okay, we can finally start reading data out of the stream. + construct(dynamic_lencode, dynamic_lengths, dynamic_nlen) + construct(dynamic_distcode, dynamic_lengths.slice(dynamic_nlen), dynamic_ndist) + become(start_codes, call_codes( + dynamic_distcode + , dynamic_lencode + ), done_with_codes) + } + + function start_codes() { + become(decode, call_decode(codes_lencode), on_got_codes_symbol) + } + + function on_got_codes_symbol() { + var symbol = codes_symbol = last + if(symbol < 0) { + emit(new Error('invalid symbol')) + return + } + + if(symbol < 256) { + output_one(symbol) + become(decode, call_decode(codes_lencode), on_got_codes_symbol) + return + } + + if(symbol > 256) { + symbol = codes_symbol -= 257 + if(symbol >= 29) { + emit(new Error('invalid fixed code')) + return + } + + become(bits, call_bits(lext[symbol]), on_got_codes_len) + return + } + + if(symbol === 256) { + unbecome() + return + } + } + + + + + + + function on_got_codes_len() { + codes_len = lens[codes_symbol] + last + become(decode, call_decode(codes_distcode), on_got_codes_dist_symbol) + } + + + function on_got_codes_dist_symbol() { + codes_symbol = last + if(codes_symbol < 0) { + emit(new Error('invalid distance symbol')) + return + } + + become(bits, call_bits(dext[codes_symbol]), on_got_codes_dist_dist) + } + + function on_got_codes_dist_dist() { + var dist = dists[codes_symbol] + last + + // Once we have a "distance" and a "length", we start to output bytes. + // We reach "dist" back from our current output position to get the byte + // we should repeat and output it (thus moving the output window cursor forward). + // Two notes: + // + // 1. Theoretically we could overlap our output and input. + // 2. `X % (2^N) == X & (2^N - 1)` with the distinction that + // the result of the bitwise AND won't be negative for the + // range of values we're feeding it. Spare a modulo, spoil the child. + while(codes_len--) { + output_one(output[(output_idx - dist) & WINDOW_MINUS_ONE]) + } + + become(decode, call_decode(codes_lencode), on_got_codes_symbol) + } + + function done_with_codes() { + if(is_final) { + become(bytes, call_bytes(4), on_got_adler) + return + } + become(bits, call_bits(1), on_got_is_final) + } + + + + + function on_got_adler() { + var check_s1 = last[3] | (last[2] << 8) + , check_s2 = last[1] | (last[0] << 8) + + if(check_s2 !== adler_s2 || check_s1 !== adler_s1) { + emit(new Error( + 'bad adler checksum: '+[check_s2, adler_s2, check_s1, adler_s1] + )) + return + } + + ended = true + + output_one_recycle() + + if(on_unused) { + on_unused( + [bops.subarray(buffer[0], buffer_offset)].concat(buffer.slice(1)) + , bytes_read + ) + } + + output_idx = 0 + ended = true + emit() + } + + function decode() { + _decode() + } + + function _decode() { + if(decode_len > MAXBITS) { + emit(new Error('ran out of codes')) + return + } + + become(bits, call_bits(1), got_decode_bit) + } + + function got_decode_bit() { + decode_code = (decode_code | last) >>> 0 + decode_count = decode_huffman.count[decode_len] + if(decode_code < decode_first + decode_count) { + unbecome(decode_huffman.symbol[decode_index + (decode_code - decode_first)]) + return + } + decode_index += decode_count + decode_first += decode_count + decode_first <<= 1 + decode_code = (decode_code << 1) >>> 0 + ++decode_len + _decode() + } + + + function become(fn, s, then) { + if(typeof then !== 'function') { + throw new Error + } + states.unshift({ + current: fn + , next: then + }) + } + + function unbecome(result) { + if(states.length > 1) { + states[1].current = states[0].next + } + states.shift() + if(!states.length) { + ended = true + + output_one_recycle() + if(on_unused) { + on_unused( + [bops.subarray(buffer[0], buffer_offset)].concat(buffer.slice(1)) + , bytes_read + ) + } + output_idx = 0 + ended = true + emit() + // return + } + else { + last = result + } + } + + function bits() { + var byt + , idx + + idx = 0 + bits_value = bitbuf + while(bitcnt < bits_need) { + // we do this to preserve `bits_value` when + // "need_input" is tripped. + // + // fun fact: if we moved that into the `if` statement + // below, it would trigger a deoptimization of this (very + // hot) function. JITs! + bitbuf = bits_value + byt = take() + if(need_input) { + break + } + ++idx + bits_value = (bits_value | (byt << bitcnt)) >>> 0 + bitcnt += 8 + } + + if(!need_input) { + bitbuf = bits_value >>> bits_need + bitcnt -= bits_need + unbecome((bits_value & ((1 << bits_need) - 1)) >>> 0) + } + } + + + + function bytes() { + var byte_accum = bytes_value + , value + + while(bytes_need--) { + value = take() + + + if(need_input) { + bitbuf = bitcnt = 0 + bytes_need += 1 + break + } + byte_accum[byte_accum.length] = value + } + if(!need_input) { + bitcnt = bitbuf = 0 + unbecome(byte_accum) + } + } + + + + function take() { + if(!buffer.length) { + need_input = true + return + } + + if(buffer_offset === buffer[0].length) { + buffer.shift() + buffer_offset = 0 + return take() + } + + ++bytes_read + + return bitbuf = takebyte() + } + + function takebyte() { + return buffer[0][buffer_offset++] + } + + + + function output_one(val) { + adler_s1 = (adler_s1 + val) % 65521 + adler_s2 = (adler_s2 + adler_s1) % 65521 + output[output_idx++] = val + output_idx &= WINDOW_MINUS_ONE + output_one_pool(val) + } + + function output_one_pool(val) { + if(output_one_offs === OUTPUT_ONE_LENGTH) { + output_one_recycle() + } + + output_one_buf[output_one_offs++] = val + } + + function output_one_recycle() { + if(output_one_offs > 0) { + if(output_one_buf) { + emit(null, bops.subarray(output_one_buf, 0, output_one_offs)) + } else { + } + output_one_buf = bops.create(OUTPUT_ONE_LENGTH) + output_one_offs = 0 + } + } + + function output_many(vals) { + var len + , byt + , olen + + output_one_recycle() + for(var i = 0, len = vals.length; i < len; ++i) { + byt = vals[i] + adler_s1 = (adler_s1 + byt) % 65521 + adler_s2 = (adler_s2 + adler_s1) % 65521 + output[output_idx++] = byt + output_idx &= WINDOW_MINUS_ONE + } + + emit(null, bops.from(vals)) + } +} + +function build_fixed() { + var lencnt = [] + , lensym = [] + , distcnt = [] + , distsym = [] + + var lencode = { + count: lencnt + , symbol: lensym + } + + var distcode = { + count: distcnt + , symbol: distsym + } + + var lengths = [] + , symbol + + for(symbol = 0; symbol < 144; ++symbol) { + lengths[symbol] = 8 + } + for(; symbol < 256; ++symbol) { + lengths[symbol] = 9 + } + for(; symbol < 280; ++symbol) { + lengths[symbol] = 7 + } + for(; symbol < FIXLCODES; ++symbol) { + lengths[symbol] = 8 + } + construct(lencode, lengths, FIXLCODES) + + for(symbol = 0; symbol < MAXDCODES; ++symbol) { + lengths[symbol] = 5 + } + construct(distcode, lengths, MAXDCODES) + return {lencode: lencode, distcode: distcode} +} + +function construct(huffman, lengths, num) { + var symbol + , left + , offs + , len + + offs = [] + + for(len = 0; len <= MAXBITS; ++len) { + huffman.count[len] = 0 + } + + for(symbol = 0; symbol < num; ++symbol) { + huffman.count[lengths[symbol]] += 1 + } + + if(huffman.count[0] === num) { + return + } + + left = 1 + for(len = 1; len <= MAXBITS; ++len) { + left <<= 1 + left -= huffman.count[len] + if(left < 0) { + return left + } + } + + offs[1] = 0 + for(len = 1; len < MAXBITS; ++len) { + offs[len + 1] = offs[len] + huffman.count[len] + } + + for(symbol = 0; symbol < num; ++symbol) { + if(lengths[symbol] !== 0) { + huffman.symbol[offs[lengths[symbol]]++] = symbol + } + } + + return left +} diff --git a/lib/parseascii.js b/lib/parseascii.js new file mode 100644 index 0000000..78f5eb5 --- /dev/null +++ b/lib/parseascii.js @@ -0,0 +1,7 @@ +module.exports = function parseAscii(buffer, start, end) { + var val = ""; + while (start < end) { + val += String.fromCharCode(buffer[start++]); + } + return val; +}; diff --git a/lib/parsedec.js b/lib/parsedec.js new file mode 100644 index 0000000..e87151d --- /dev/null +++ b/lib/parsedec.js @@ -0,0 +1,7 @@ +module.exports = function parseDec(buffer, start, end) { + var val = 0; + while (start < end) { + val = val * 10 + buffer[start++] - 0x30; + } + return val; +}; diff --git a/lib/parseoct.js b/lib/parseoct.js new file mode 100644 index 0000000..d67d8d9 --- /dev/null +++ b/lib/parseoct.js @@ -0,0 +1,7 @@ +module.exports = function parseOct(buffer, start, end) { + var val = 0; + while (start < end) { + val = (val << 3) + buffer[start++] - 0x30; + } + return val; +}; diff --git a/lib/parsetohex.js b/lib/parsetohex.js new file mode 100644 index 0000000..a2a02af --- /dev/null +++ b/lib/parsetohex.js @@ -0,0 +1,10 @@ +var chars = "0123456789abcdef"; + +module.exports = function parseToHex(buffer, start, end) { + var val = ""; + while (start < end) { + var byte = buffer[start++]; + val += chars[byte >> 4] + chars[byte & 0xf]; + } + return val; +}; diff --git a/lib/pathcmp.js b/lib/pathcmp.js new file mode 100644 index 0000000..bc3189d --- /dev/null +++ b/lib/pathcmp.js @@ -0,0 +1,6 @@ +module.exports = function pathCmp(oa, ob) { + var a = oa.name; + var b = ob.name; + a += "/"; b += "/"; + return a < b ? -1 : a > b ? 1 : 0; +}; diff --git a/lib/sha1.js b/lib/sha1.js new file mode 100644 index 0000000..5de9f76 --- /dev/null +++ b/lib/sha1.js @@ -0,0 +1,144 @@ +module.exports = function sha1(buffer) { + if (buffer === undefined) return create(); + var shasum = create(); + shasum.update(buffer); + return shasum.digest(); +}; + +// A streaming interface for when nothing is passed in. +function create() { + var h0 = 0x67452301; + var h1 = 0xEFCDAB89; + var h2 = 0x98BADCFE; + var h3 = 0x10325476; + var h4 = 0xC3D2E1F0; + // The first 64 bytes (16 words) is the data chunk + var block = new Array(80), offset = 0, shift = 24; + var totalLength = 0; + + return { update: update, digest: digest }; + + // The user gave us more data. Store it! + function update(chunk) { + if (typeof chunk === "string") return updateString(chunk); + var length = chunk.length; + totalLength += length * 8; + for (var i = 0; i < length; i++) { + write(chunk[i]); + } + } + + function updateString(string) { + var length = string.length; + totalLength += length * 8; + for (var i = 0; i < length; i++) { + write(string.charCodeAt(i)); + } + } + + function write(byte) { + block[offset] |= (byte & 0xff) << shift; + if (shift) { + shift -= 8; + } + else { + offset++; + shift = 24; + } + if (offset === 16) processBlock(); + } + + // No more data will come, pad the block, process and return the result. + function digest() { + // Pad + write(0x80); + if (offset > 14 || (offset === 14 && shift < 24)) { + processBlock(); + } + offset = 14; + shift = 24; + + // 64-bit length big-endian + write(0x00); // numbers this big aren't accurate in javascript anyway + write(0x00); // ..So just hard-code to zero. + write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); + write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); + for (var s = 24; s >= 0; s -= 8) { + write(totalLength >> s); + } + + // At this point one last processBlock() should trigger and we can pull out the result. + return toHex(h0) + + toHex(h1) + + toHex(h2) + + toHex(h3) + + toHex(h4); + } + + // We have a full block to process. Let's do it! + function processBlock() { + // Extend the sixteen 32-bit words into eighty 32-bit words: + for (var i = 16; i < 80; i++) { + var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; + block[i] = (w << 1) | (w >>> 31); + } + + // log(block); + + // Initialize hash value for this chunk: + var a = h0; + var b = h1; + var c = h2; + var d = h3; + var e = h4; + var f, k; + + // Main loop: + for (i = 0; i < 80; i++) { + if (i < 20) { + f = d ^ (b & (c ^ d)); + k = 0x5A827999; + } + else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (i < 60) { + f = (b & c) | (d & (b | c)); + k = 0x8F1BBCDC; + } + else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); + e = d; + d = c; + c = (b << 30 | b >>> 2); + b = a; + a = temp; + } + + // Add this chunk's hash to result so far: + h0 = (h0 + a) | 0; + h1 = (h1 + b) | 0; + h2 = (h2 + c) | 0; + h3 = (h3 + d) | 0; + h4 = (h4 + e) | 0; + + // The block is now reusable. + offset = 0; + for (i = 0; i < 16; i++) { + block[i] = 0; + } + } + + function toHex(word) { + var hex = ""; + for (var i = 28; i >= 0; i -= 4) { + hex += ((word >> i) & 0xf).toString(16); + } + return hex; + } + +} diff --git a/lib/trace.js b/lib/trace.js new file mode 100644 index 0000000..a5d3020 --- /dev/null +++ b/lib/trace.js @@ -0,0 +1 @@ +module.exports = false; diff --git a/package.json b/package.json index f67336c..279167f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "push-to-pull": "~0.1.0", - "git-pack-codec": "~0.0.1" + "varint": "0.0.3", + "bops": "~0.1.0" } } diff --git a/test/test-sha1.js b/test/test-sha1.js new file mode 100644 index 0000000..7c30561 --- /dev/null +++ b/test/test-sha1.js @@ -0,0 +1,51 @@ +var sha1 = require('../lib/sha1.js'); + +var tests = [ + "", "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "a", "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8", + "abc", "a9993e364706816aba3e25717850c26c9cd0d89d", + "message digest", "c12252ceda8be8994d5fa0290a47231c1d16aae3", + "abcdefghijklmnopqrstuvwxyz", "32d10c7b8cf96570ca04ce37f2a19d84240d3a89", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabc", + "a6319f25020d5ff8722d40ae750dbab67d94fe4f", + "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZab", + "edb3a03256d1c6d148034ec4795181931c933f46", + "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZa", + "677734f7bf40b2b244cae100bf365598fbf4741d", +]; + +for (var i = 0; i < tests.length; i += 2) { + var input = tests[i]; + console.log("\n" + JSON.stringify(input)); + var expectedHex = tests[i + 1]; + console.log(expectedHex); + var hash = sha1(input); + console.log(hash); + if (hash !== expectedHex) { + throw new Error(hash + " != " + expectedHex + " for '" + input + "'"); + } + var sha1sum = sha1(); + for (var j = 0, l = input.length; j < l; j += 17) { + sha1sum.update(input.substr(j, 17)); + } + hash = sha1sum.digest(); + console.log(hash); + if (hash !== expectedHex) { + throw new Error(hash + " != " + expectedHex + " for '" + input + "'"); + } +} + +console.log("\n1,000,000 repetitions of the character 'a'"); +var expectedHex = "34aa973cd4c4daa4f61eeb2bdbad27316534016f"; +console.log(expectedHex); +var sha1sum = sha1(); +for (var i = 0; i < 100000; i++) { + sha1sum.update("aaaaaaaaaa"); +} +var hash = sha1sum.digest(); +console.log(hash); +if (hash !== expectedHex) { + throw new Error(hash + " != " + expectedHex + " for '" + input + "'"); +} From 97d5d0b5d444f39d043b5d2ffbd363aeb5d7da69 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Nov 2013 21:50:50 -0600 Subject: [PATCH 045/256] Move db tracing to it's own module --- js-git.js | 56 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/js-git.js b/js-git.js index 81c4004..8a8a695 100644 --- a/js-git.js +++ b/js-git.js @@ -12,21 +12,12 @@ var parse = pushToPull(require('./lib/decode-pack.js')); module.exports = newRepo; -function newRepo(db, workDir) { +function newRepo(db) { if (!db) throw new TypeError("A db interface instance is required"); var repo = {}; - if (trace) { - db = { - get: wrap1("get", db.get), - set: wrap2("set", db.set), - has: wrap1("has", db.has), - del: wrap1("del", db.del), - keys: wrap1("keys", db.keys), - init: wrap0("init", db.init), - }; - } + if (trace) db = require('./lib/tracedb.js')(db); // Git Objects repo.load = load; // (hash-ish) -> object @@ -51,55 +42,12 @@ function newRepo(db, workDir) { repo.deleteRef = deleteRef; // (ref) repo.listRefs = listRefs; // (prefix) -> refs - if (workDir) { - // TODO: figure out API for working repos - } - // Network Protocols repo.fetch = fetch; repo.push = push; return repo; - function wrap0(type, fn) { - return zero; - function zero(callback) { - if (!callback) return zero.bind(this); - return fn.call(this, check); - function check(err) { - if (err) return callback(err); - trace(type, null); - return callback.apply(this, arguments); - } - } - } - - function wrap1(type, fn) { - return one; - function one(arg, callback) { - if (!callback) return one.bind(this, arg); - return fn.call(this, arg, check); - function check(err) { - if (err) return callback(err); - trace(type, null, arg); - return callback.apply(this, arguments); - } - } - } - - function wrap2(type, fn) { - return two; - function two(arg1, arg2, callback) { - if (!callback) return two.bind(this, arg1. arg2); - return fn.call(this, arg1, arg2, check); - function check(err) { - if (err) return callback(err); - trace(type, null, arg1); - return callback.apply(this, arguments); - } - } - } - function logWalk(hashish, callback) { if (!callback) return logWalk.bind(this, hashish); var last, seen = {}; From 39c6b08c60374747ed52bf42927180fa7c3b0bbc Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Nov 2013 22:01:06 -0600 Subject: [PATCH 046/256] Move walk helpers into own modules --- js-git.js | 127 ++------------------------------------------- lib/assert-type.js | 5 ++ lib/log-walk.js | 43 +++++++++++++++ lib/tracedb.js | 52 +++++++++++++++++++ lib/tree-walk.js | 45 ++++++++++++++++ lib/walk.js | 42 +++++++++++++++ 6 files changed, 190 insertions(+), 124 deletions(-) create mode 100644 lib/assert-type.js create mode 100644 lib/log-walk.js create mode 100644 lib/tracedb.js create mode 100644 lib/tree-walk.js create mode 100644 lib/walk.js diff --git a/js-git.js b/js-git.js index 8a8a695..0980bde 100644 --- a/js-git.js +++ b/js-git.js @@ -9,6 +9,9 @@ var trace = require('./lib/trace.js'); var applyDelta = require('./lib/apply-delta.js'); var pushToPull = require('push-to-pull'); var parse = pushToPull(require('./lib/decode-pack.js')); +var walk = require('./lib/walk.js'); +var treeWalk = require('./lib/tree-walk.js'); +var logWalk = require('./lib/log-walk.js'); module.exports = newRepo; @@ -30,7 +33,6 @@ function newRepo(db) { // Convenience Readers repo.logWalk = logWalk; // (hash-ish) => stream repo.treeWalk = treeWalk; // (hash-ish) => stream - repo.walk = walk; // (seed, scan, compare) -> stream // Refs repo.resolveHashish = resolveHashish; // (hash-ish) -> hash @@ -48,124 +50,6 @@ function newRepo(db) { return repo; - function logWalk(hashish, callback) { - if (!callback) return logWalk.bind(this, hashish); - var last, seen = {}; - return readRef("shallow", onShallow); - - function onShallow(err, shallow) { - last = shallow; - return loadAs("commit", hashish, onLoad); - } - - function onLoad(err, commit, hash) { - if (commit === undefined) return callback(err); - commit.hash = hash; - seen[hash] = true; - return callback(null, walk(commit, scan, loadKey, compare)); - } - - function scan(commit) { - if (last === commit) return []; - return commit.parents.filter(function (hash) { - return !seen[hash]; - }); - } - - function loadKey(hash, callback) { - return loadAs("commit", hash, function (err, commit) { - if (err) return callback(err); - commit.hash = hash; - if (hash === last) commit.last = true; - return callback(null, commit); - }); - } - - function compare(commit, other) { - return commit.author.date < other.author.date; - } - } - - function treeWalk(hashish, callback) { - if (!callback) return treeWalk.bind(this, hashish); - return load(hashish, onLoad); - function onLoad(err, item, hash) { - if (err) return callback(err); - if (item.type === "commit") return load(item.body.tree, onLoad); - item.hash = hash; - item.path = "/"; - return callback(null, walk(item, treeScan, treeLoadKey, treeCompare)); - } - } - - function treeScan(object) { - if (object.type === "blob") return []; - assertType(object, "tree"); - return object.body.filter(function (entry) { - return entry.mode !== 0160000; - }).map(function (entry) { - var path = object.path + entry.name; - if (entry.mode === 040000) path += "/"; - entry.path = path; - return entry; - }); - } - - function treeLoadKey(entry, callback) { - return load(entry.hash, function (err, object) { - if (err) return callback(err); - entry.type = object.type; - entry.body = object.body; - return callback(null, entry); - }); - } - - function treeCompare(first, second) { - return first.path < second.path; - } - - function walk(seed, scan, loadKey, compare) { - var queue = [seed]; - var working = 0, error, cb; - return {read: read, abort: abort}; - - function read(callback) { - if (cb) return callback(new Error("Only one read at a time")); - if (working) { cb = callback; return; } - var item = queue.shift(); - if (!item) return callback(); - try { scan(item).forEach(onKey); } - catch (err) { return callback(err); } - return callback(null, item); - } - - function abort(callback) { return callback(); } - - function onError(err) { - if (cb) { - var callback = cb; cb = null; - return callback(err); - } - error = err; - } - - function onKey(key) { - working++; - loadKey(key, onItem); - } - - function onItem(err, item) { - working--; - if (err) return onError(err); - var index = queue.length; - while (index && compare(item, queue[index - 1])) index--; - queue.splice(index, 0, item); - if (!working && cb) { - var callback = cb; cb = null; - return read(callback); - } - } - } function load(hashish, callback) { if (!callback) return load.bind(this, hashish); @@ -663,8 +547,3 @@ function newRepo(db) { } } -function assertType(object, type) { - if (object.type !== type) { - throw new Error(type + " expected, but found " + object.type); - } -} diff --git a/lib/assert-type.js b/lib/assert-type.js new file mode 100644 index 0000000..c1808db --- /dev/null +++ b/lib/assert-type.js @@ -0,0 +1,5 @@ +module.exports = function assertType(object, type) { + if (object.type !== type) { + throw new Error(type + " expected, but found " + object.type); + } +}; diff --git a/lib/log-walk.js b/lib/log-walk.js new file mode 100644 index 0000000..6420129 --- /dev/null +++ b/lib/log-walk.js @@ -0,0 +1,43 @@ +var walk = require('./walk.js'); + +module.exports = logWalk; + +function logWalk(hashish, callback) { + if (!callback) return logWalk.bind(this, hashish); + var last, seen = {}; + var repo = this; + return repo.readRef("shallow", onShallow); + + function onShallow(err, shallow) { + last = shallow; + return repo.loadAs("commit", hashish, onLoad); + } + + function onLoad(err, commit, hash) { + if (commit === undefined) return callback(err); + commit.hash = hash; + seen[hash] = true; + return callback(null, walk(commit, scan, loadKey, compare)); + } + + function scan(commit) { + if (last === commit) return []; + return commit.parents.filter(function (hash) { + return !seen[hash]; + }); + } + + function loadKey(hash, callback) { + return repo.loadAs("commit", hash, function (err, commit) { + if (err) return callback(err); + commit.hash = hash; + if (hash === last) commit.last = true; + return callback(null, commit); + }); + } + +} + +function compare(commit, other) { + return commit.author.date < other.author.date; +} diff --git a/lib/tracedb.js b/lib/tracedb.js new file mode 100644 index 0000000..65da059 --- /dev/null +++ b/lib/tracedb.js @@ -0,0 +1,52 @@ +var trace = require('./trace.js'); + +module.exports = function (db) { + return { + get: wrap1("get", db.get), + set: wrap2("set", db.set), + has: wrap1("has", db.has), + del: wrap1("del", db.del), + keys: wrap1("keys", db.keys), + init: wrap0("init", db.init), + }; +}; + +function wrap0(type, fn) { + return zero; + function zero(callback) { + if (!callback) return zero.bind(this); + return fn.call(this, check); + function check(err) { + if (err) return callback(err); + trace(type, null); + return callback.apply(this, arguments); + } + } +} + +function wrap1(type, fn) { + return one; + function one(arg, callback) { + if (!callback) return one.bind(this, arg); + return fn.call(this, arg, check); + function check(err) { + if (err) return callback(err); + trace(type, null, arg); + return callback.apply(this, arguments); + } + } +} + +function wrap2(type, fn) { + return two; + function two(arg1, arg2, callback) { + if (!callback) return two.bind(this, arg1. arg2); + return fn.call(this, arg1, arg2, check); + function check(err) { + if (err) return callback(err); + trace(type, null, arg1); + return callback.apply(this, arguments); + } + } +} + diff --git a/lib/tree-walk.js b/lib/tree-walk.js new file mode 100644 index 0000000..22dba0b --- /dev/null +++ b/lib/tree-walk.js @@ -0,0 +1,45 @@ +var walk = require('./walk.js'); +var assertType = require('./assert-type.js'); + +module.exports = treeWalk; + +function treeWalk(hashish, callback) { + if (!callback) return treeWalk.bind(this, hashish); + var repo = this; + return repo.load(hashish, onLoad); + function onLoad(err, item, hash) { + if (err) return callback(err); + if (item.type === "commit") return repo.load(item.body.tree, onLoad); + item.hash = hash; + item.path = "/"; + return callback(null, walk(item, treeScan, treeLoadKey, treeCompare)); + } + + function treeLoadKey(entry, callback) { + return repo.load(entry.hash, function (err, object) { + if (err) return callback(err); + entry.type = object.type; + entry.body = object.body; + return callback(null, entry); + }); + } + +} + +function treeScan(object) { + if (object.type === "blob") return []; + assertType(object, "tree"); + return object.body.filter(function (entry) { + return entry.mode !== 0160000; + }).map(function (entry) { + var path = object.path + entry.name; + if (entry.mode === 040000) path += "/"; + entry.path = path; + return entry; + }); +} + +function treeCompare(first, second) { + return first.path < second.path; +} + diff --git a/lib/walk.js b/lib/walk.js new file mode 100644 index 0000000..677b9dc --- /dev/null +++ b/lib/walk.js @@ -0,0 +1,42 @@ +module.exports = function walk(seed, scan, loadKey, compare) { + var queue = [seed]; + var working = 0, error, cb; + return {read: read, abort: abort}; + + function read(callback) { + if (cb) return callback(new Error("Only one read at a time")); + if (working) { cb = callback; return; } + var item = queue.shift(); + if (!item) return callback(); + try { scan(item).forEach(onKey); } + catch (err) { return callback(err); } + return callback(null, item); + } + + function abort(callback) { return callback(); } + + function onError(err) { + if (cb) { + var callback = cb; cb = null; + return callback(err); + } + error = err; + } + + function onKey(key) { + working++; + loadKey(key, onItem); + } + + function onItem(err, item) { + working--; + if (err) return onError(err); + var index = queue.length; + while (index && compare(item, queue[index - 1])) index--; + queue.splice(index, 0, item); + if (!working && cb) { + var callback = cb; cb = null; + return read(callback); + } + } +}; \ No newline at end of file From fbf4502912c684ea78397a5af48814a28f9792f7 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Nov 2013 22:07:55 -0600 Subject: [PATCH 047/256] Move fetch into it's own module --- js-git.js | 171 +-------------------------------------------------- lib/fetch.js | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 168 deletions(-) create mode 100644 lib/fetch.js diff --git a/js-git.js b/js-git.js index 0980bde..bcc3000 100644 --- a/js-git.js +++ b/js-git.js @@ -4,14 +4,11 @@ var decoders = require('./lib/decoders.js'); var frame = require('./lib/frame.js'); var deframe = require('./lib/deframe.js'); var sha1 = require('./lib/sha1.js'); -var agent = require('./lib/agent.js'); var trace = require('./lib/trace.js'); var applyDelta = require('./lib/apply-delta.js'); -var pushToPull = require('push-to-pull'); -var parse = pushToPull(require('./lib/decode-pack.js')); -var walk = require('./lib/walk.js'); var treeWalk = require('./lib/tree-walk.js'); var logWalk = require('./lib/log-walk.js'); +var fetch = require('./lib/fetch.js'); module.exports = newRepo; @@ -20,6 +17,8 @@ function newRepo(db) { var repo = {}; + repo.db = db; + if (trace) db = require('./lib/tracedb.js')(db); // Git Objects @@ -50,7 +49,6 @@ function newRepo(db) { return repo; - function load(hashish, callback) { if (!callback) return load.bind(this, hashish); var hash; @@ -275,169 +273,6 @@ function newRepo(db) { } - function fetch(remote, opts, callback) { - if (!callback) return fetch.bind(this, remote, opts); - var refs, branch, queue, ref, hash; - return remote.discover(onDiscover); - - function onDiscover(err, serverRefs, serverCaps) { - if (err) return callback(err); - refs = serverRefs; - opts.caps = processCaps(opts, serverCaps); - return processWants(refs, opts.want, onWants); - } - - function onWants(err, wants) { - if (err) return callback(err); - opts.wants = wants; - return remote.fetch(repo, opts, onPackStream); - } - - function onPackStream(err, raw) { - if (err) return callback(err); - if (!raw) return remote.close(onDone); - var packStream = parse(raw); - return unpack(packStream, opts, onUnpack); - } - - function onUnpack(err) { - if (err) return callback(err); - return remote.close(onClose); - } - - function onClose(err) { - if (err) return callback(err); - queue = Object.keys(refs); - return next(); - } - - function next(err) { - if (err) return callback(err); - ref = queue.shift(); - if (!ref) return setHead(branch, onDone); - if (ref === "HEAD" || /{}$/.test(ref)) return next(); - hash = refs[ref]; - if (!branch && (hash === refs.HEAD)) branch = ref.substr(11); - db.has(hash, onHas); - } - - function onHas(err, has) { - if (err) return callback(err); - if (!has) return next(); - return db.set(ref, hash + "\n", next); - } - - function onDone(err) { - if (err) return callback(err); - return callback(null, refs); - } - } - - function processCaps(opts, serverCaps) { - var caps = []; - if (serverCaps["ofs-delta"]) caps.push("ofs-delta"); - if (serverCaps["thin-pack"]) caps.push("thin-pack"); - if (opts.includeTag && serverCaps["include-tag"]) caps.push("include-tag"); - if ((opts.onProgress || opts.onError) && - (serverCaps["side-band-64k"] || serverCaps["side-band"])) { - caps.push(serverCaps["side-band-64k"] ? "side-band-64k" : "side-band"); - if (!opts.onProgress && serverCaps["no-progress"]) { - caps.push("no-progress"); - } - } - if (serverCaps.agent) caps.push("agent=" + agent); - return caps; - } - - function processWants(refs, filter, callback) { - if (filter === null || filter === undefined) { - return defaultWants(refs, callback); - } - filter = Array.isArray(filter) ? arrayFilter(filter) : - typeof filter === "function" ? filter = filter : - wantFilter(filter); - - var list = Object.keys(refs); - var wants = {}; - var ref, hash; - return shift(); - function shift() { - ref = list.shift(); - if (!ref) return callback(null, Object.keys(wants)); - hash = refs[ref]; - resolveHashish(ref, onResolve); - } - function onResolve(err, oldHash) { - // Skip refs we already have - if (hash === oldHash) return shift(); - filter(ref, onFilter); - } - function onFilter(err, want) { - if (err) return callback(err); - // Skip refs the user doesn't want - if (want) wants[hash] = true; - return shift(); - } - } - - function defaultWants(refs, callback) { - return listRefs("refs/heads", onRefs); - - function onRefs(err, branches) { - if (err) return callback(err); - var wants = Object.keys(branches); - wants.unshift("HEAD"); - return processWants(refs, wants, callback); - } - } - - function wantMatch(ref, want) { - if (want === "HEAD" || want === null || want === undefined) { - return ref === "HEAD"; - } - if (Object.prototype.toString.call(want) === '[object RegExp]') { - return want.test(ref); - } - if (typeof want === "boolean") return want; - if (typeof want !== "string") { - throw new TypeError("Invalid want type: " + typeof want); - } - return (/^refs\//.test(ref) && ref === want) || - (ref === "refs/heads/" + want) || - (ref === "refs/tags/" + want); - } - - function wantFilter(want) { - return filter; - function filter(ref, callback) { - var result; - try { - result = wantMatch(ref, want); - } - catch (err) { - return callback(err); - } - return callback(null, result); - } - } - - function arrayFilter(want) { - var length = want.length; - return filter; - function filter(ref, callback) { - var result; - try { - for (var i = 0; i < length; ++i) { - if (result = wantMatch(ref, want[i])) break; - } - } - catch (err) { - return callback(err); - } - return callback(null, result); - } - } - function push() { throw new Error("TODO: Implement repo.push"); } diff --git a/lib/fetch.js b/lib/fetch.js new file mode 100644 index 0000000..7beef45 --- /dev/null +++ b/lib/fetch.js @@ -0,0 +1,171 @@ +var agent = require('./agent.js'); +var pushToPull = require('push-to-pull'); +var parse = pushToPull(require('./decode-pack.js')); + +module.exports = fetch; + +function fetch(remote, opts, callback) { + if (!callback) return fetch.bind(this, remote, opts); + var repo = this; + var db = repo.db; + var refs, branch, queue, ref, hash; + return remote.discover(onDiscover); + + function onDiscover(err, serverRefs, serverCaps) { + if (err) return callback(err); + refs = serverRefs; + opts.caps = processCaps(opts, serverCaps); + return processWants(refs, opts.want, onWants); + } + + function onWants(err, wants) { + if (err) return callback(err); + opts.wants = wants; + return remote.fetch(repo, opts, onPackStream); + } + + function onPackStream(err, raw) { + if (err) return callback(err); + if (!raw) return remote.close(onDone); + var packStream = parse(raw); + return repo.unpack(packStream, opts, onUnpack); + } + + function onUnpack(err) { + if (err) return callback(err); + return remote.close(onClose); + } + + function onClose(err) { + if (err) return callback(err); + queue = Object.keys(refs); + return next(); + } + + function next(err) { + if (err) return callback(err); + ref = queue.shift(); + if (!ref) return repo.setHead(branch, onDone); + if (ref === "HEAD" || /{}$/.test(ref)) return next(); + hash = refs[ref]; + if (!branch && (hash === refs.HEAD)) branch = ref.substr(11); + db.has(hash, onHas); + } + + function onHas(err, has) { + if (err) return callback(err); + if (!has) return next(); + return db.set(ref, hash + "\n", next); + } + + function onDone(err) { + if (err) return callback(err); + return callback(null, refs); + } + + function processCaps(opts, serverCaps) { + var caps = []; + if (serverCaps["ofs-delta"]) caps.push("ofs-delta"); + if (serverCaps["thin-pack"]) caps.push("thin-pack"); + if (opts.includeTag && serverCaps["include-tag"]) caps.push("include-tag"); + if ((opts.onProgress || opts.onError) && + (serverCaps["side-band-64k"] || serverCaps["side-band"])) { + caps.push(serverCaps["side-band-64k"] ? "side-band-64k" : "side-band"); + if (!opts.onProgress && serverCaps["no-progress"]) { + caps.push("no-progress"); + } + } + if (serverCaps.agent) caps.push("agent=" + agent); + return caps; + } + + function processWants(refs, filter, callback) { + if (filter === null || filter === undefined) { + return defaultWants(refs, callback); + } + filter = Array.isArray(filter) ? arrayFilter(filter) : + typeof filter === "function" ? filter = filter : + wantFilter(filter); + + var list = Object.keys(refs); + var wants = {}; + var ref, hash; + return shift(); + function shift() { + ref = list.shift(); + if (!ref) return callback(null, Object.keys(wants)); + hash = refs[ref]; + repo.resolveHashish(ref, onResolve); + } + function onResolve(err, oldHash) { + // Skip refs we already have + if (hash === oldHash) return shift(); + filter(ref, onFilter); + } + function onFilter(err, want) { + if (err) return callback(err); + // Skip refs the user doesn't want + if (want) wants[hash] = true; + return shift(); + } + } + + function defaultWants(refs, callback) { + return repo.listRefs("refs/heads", onRefs); + + function onRefs(err, branches) { + if (err) return callback(err); + var wants = Object.keys(branches); + wants.unshift("HEAD"); + return processWants(refs, wants, callback); + } + } + +} + +function wantMatch(ref, want) { + if (want === "HEAD" || want === null || want === undefined) { + return ref === "HEAD"; + } + if (Object.prototype.toString.call(want) === '[object RegExp]') { + return want.test(ref); + } + if (typeof want === "boolean") return want; + if (typeof want !== "string") { + throw new TypeError("Invalid want type: " + typeof want); + } + return (/^refs\//.test(ref) && ref === want) || + (ref === "refs/heads/" + want) || + (ref === "refs/tags/" + want); +} + +function wantFilter(want) { + return filter; + function filter(ref, callback) { + var result; + try { + result = wantMatch(ref, want); + } + catch (err) { + return callback(err); + } + return callback(null, result); + } +} + +function arrayFilter(want) { + var length = want.length; + return filter; + function filter(ref, callback) { + var result; + try { + for (var i = 0; i < length; ++i) { + if (result = wantMatch(ref, want[i])) break; + } + } + catch (err) { + return callback(err); + } + return callback(null, result); + } +} From ba6f5a7a963cd24dd685b0306a7c7e2bccaea898 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Nov 2013 22:10:51 -0600 Subject: [PATCH 048/256] Move unpack into it's own module --- js-git.js | 106 +---------------------------------------------- lib/unpack.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 105 deletions(-) create mode 100644 lib/unpack.js diff --git a/js-git.js b/js-git.js index bcc3000..1dd6a26 100644 --- a/js-git.js +++ b/js-git.js @@ -5,10 +5,10 @@ var frame = require('./lib/frame.js'); var deframe = require('./lib/deframe.js'); var sha1 = require('./lib/sha1.js'); var trace = require('./lib/trace.js'); -var applyDelta = require('./lib/apply-delta.js'); var treeWalk = require('./lib/tree-walk.js'); var logWalk = require('./lib/log-walk.js'); var fetch = require('./lib/fetch.js'); +var unpack = require('./lib/unpack.js'); module.exports = newRepo; @@ -272,113 +272,9 @@ function newRepo(db) { } } - function push() { throw new Error("TODO: Implement repo.push"); } - function unpack(packStream, opts, callback) { - if (!callback) return unpack.bind(this, packStream, opts); - - var version, num, numDeltas = 0, count = 0, countDeltas = 0; - var done, startDeltaProgress = false; - - // hashes keyed by offset for ofs-delta resolving - var hashes = {}; - var has = {}; - - return packStream.read(onStats); - - function onDone(err) { - if (done) return; - done = true; - return callback(err); - } - - function onStats(err, stats) { - if (err) return onDone(err); - version = stats.version; - num = stats.num; - packStream.read(onRead); - } - - function objectProgress(more) { - if (!more) startDeltaProgress = true; - var percent = Math.round(count / num * 100); - return opts.onProgress("Receiving objects: " + percent + "% (" + (count++) + "/" + num + ") " + (more ? "\r" : "\n")); - } - - function deltaProgress(more) { - if (!startDeltaProgress) return; - var percent = Math.round(countDeltas / numDeltas * 100); - return opts.onProgress("Applying deltas: " + percent + "% (" + (countDeltas++) + "/" + numDeltas + ") " + (more ? "\r" : "\n")); - } - - function onRead(err, item) { - if (err) return onDone(err); - if (opts.onProgress) objectProgress(item); - if (item === undefined) return resolveDeltas(); - if (item.size !== item.body.length) { - return onDone(new Error("Body size mismatch")); - } - if (item.type === "ofs-delta") { - numDeltas++; - item.ref = hashes[item.offset - item.ref]; - return resolveDelta(item); - } - if (item.type === "ref-delta") { - numDeltas++; - return checkDelta(item); - } - return saveValue(item); - } - - function resolveDelta(item) { - if (opts.onProgress) deltaProgress(); - return db.get(item.ref, function (err, buffer) { - if (err) return onDone(err); - var target = deframe(buffer); - item.type = target[0]; - item.body = applyDelta(item.body, target[1]); - return saveValue(item); - }); - } - - function checkDelta(item) { - var hasTarget = has[item.ref]; - if (hasTarget === true) return resolveDelta(item); - if (hasTarget === false) return enqueueDelta(item); - return db.has(item.ref, function (err, value) { - if (err) return onDone(err); - has[item.ref] = value; - if (value) return resolveDelta(item); - return enqueueDelta(item); - }); - } - - function saveValue(item) { - var buffer = frame(item.type, item.body); - var hash = hashes[item.offset] = sha1(buffer); - has[hash] = true; - return db.set(hash, buffer, onSave); - } - - function onSave(err) { - if (err) return callback(err); - packStream.read(onRead); - } - - function enqueueDelta(item) { - // I have yet to come across a repo that actually needs this path. - // It's hard to implement without something to test against. - throw "TODO: enqueueDelta"; - } - - function resolveDeltas() { - // TODO: resolve any pending deltas once enqueueDelta is implemented. - return onDone(); - } - - } } diff --git a/lib/unpack.js b/lib/unpack.js new file mode 100644 index 0000000..0565fba --- /dev/null +++ b/lib/unpack.js @@ -0,0 +1,111 @@ +var deframe = require('./deframe.js'); +var frame = require('./frame.js'); +var sha1 = require('./sha1.js'); +var applyDelta = require('./apply-delta.js'); +module.exports = unpack; + +function unpack(packStream, opts, callback) { + if (!callback) return unpack.bind(this, packStream, opts); + var repo = this; + var db = repo.db; + + var version, num, numDeltas = 0, count = 0, countDeltas = 0; + var done, startDeltaProgress = false; + + // hashes keyed by offset for ofs-delta resolving + var hashes = {}; + var has = {}; + + return packStream.read(onStats); + + function onDone(err) { + if (done) return; + done = true; + return callback(err); + } + + function onStats(err, stats) { + if (err) return onDone(err); + version = stats.version; + num = stats.num; + packStream.read(onRead); + } + + function objectProgress(more) { + if (!more) startDeltaProgress = true; + var percent = Math.round(count / num * 100); + return opts.onProgress("Receiving objects: " + percent + "% (" + (count++) + "/" + num + ") " + (more ? "\r" : "\n")); + } + + function deltaProgress(more) { + if (!startDeltaProgress) return; + var percent = Math.round(countDeltas / numDeltas * 100); + return opts.onProgress("Applying deltas: " + percent + "% (" + (countDeltas++) + "/" + numDeltas + ") " + (more ? "\r" : "\n")); + } + + function onRead(err, item) { + if (err) return onDone(err); + if (opts.onProgress) objectProgress(item); + if (item === undefined) return resolveDeltas(); + if (item.size !== item.body.length) { + return onDone(new Error("Body size mismatch")); + } + if (item.type === "ofs-delta") { + numDeltas++; + item.ref = hashes[item.offset - item.ref]; + return resolveDelta(item); + } + if (item.type === "ref-delta") { + numDeltas++; + return checkDelta(item); + } + return saveValue(item); + } + + function resolveDelta(item) { + if (opts.onProgress) deltaProgress(); + return db.get(item.ref, function (err, buffer) { + if (err) return onDone(err); + var target = deframe(buffer); + item.type = target[0]; + item.body = applyDelta(item.body, target[1]); + return saveValue(item); + }); + } + + function checkDelta(item) { + var hasTarget = has[item.ref]; + if (hasTarget === true) return resolveDelta(item); + if (hasTarget === false) return enqueueDelta(item); + return db.has(item.ref, function (err, value) { + if (err) return onDone(err); + has[item.ref] = value; + if (value) return resolveDelta(item); + return enqueueDelta(item); + }); + } + + function saveValue(item) { + var buffer = frame(item.type, item.body); + var hash = hashes[item.offset] = sha1(buffer); + has[hash] = true; + return db.set(hash, buffer, onSave); + } + + function onSave(err) { + if (err) return callback(err); + packStream.read(onRead); + } + + function enqueueDelta(item) { + // I have yet to come across a repo that actually needs this path. + // It's hard to implement without something to test against. + throw "TODO: enqueueDelta"; + } + + function resolveDeltas() { + // TODO: resolve any pending deltas once enqueueDelta is implemented. + return onDone(); + } + +} From 4234eb45eefcb149d0037f66d728f3e10829a44b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Nov 2013 22:12:04 -0600 Subject: [PATCH 049/256] Move push stub into it's own module --- js-git.js | 6 +----- lib/push.js | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 lib/push.js diff --git a/js-git.js b/js-git.js index 1dd6a26..153c648 100644 --- a/js-git.js +++ b/js-git.js @@ -8,6 +8,7 @@ var trace = require('./lib/trace.js'); var treeWalk = require('./lib/tree-walk.js'); var logWalk = require('./lib/log-walk.js'); var fetch = require('./lib/fetch.js'); +var push = require('./lib/push.js'); var unpack = require('./lib/unpack.js'); module.exports = newRepo; @@ -272,9 +273,4 @@ function newRepo(db) { } } - function push() { - throw new Error("TODO: Implement repo.push"); - } - } - diff --git a/lib/push.js b/lib/push.js new file mode 100644 index 0000000..c0ad1f8 --- /dev/null +++ b/lib/push.js @@ -0,0 +1,3 @@ +module.exports = function push() { + throw new Error("TODO: Implement repo.push"); +}; From 379508704a7c4bd9b8e8178003b1b7e30ba1c5b5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 10:12:36 -0600 Subject: [PATCH 050/256] Break out object store methods to a mixin module --- js-git.js | 132 ++++++++-------------------------------------- lib/fetch.js | 2 +- mixins/objects.js | 114 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 112 deletions(-) create mode 100644 mixins/objects.js diff --git a/js-git.js b/js-git.js index 153c648..d1b44b6 100644 --- a/js-git.js +++ b/js-git.js @@ -1,10 +1,5 @@ -var parseAscii = require('./lib/parseascii.js'); -var encoders = require('./lib/encoders.js'); -var decoders = require('./lib/decoders.js'); -var frame = require('./lib/frame.js'); -var deframe = require('./lib/deframe.js'); -var sha1 = require('./lib/sha1.js'); var trace = require('./lib/trace.js'); + var treeWalk = require('./lib/tree-walk.js'); var logWalk = require('./lib/log-walk.js'); var fetch = require('./lib/fetch.js'); @@ -18,16 +13,16 @@ function newRepo(db) { var repo = {}; + // Auto trace the db if tracing is turned on. + if (trace) db = require('./lib/tracedb.js')(db); + + // Add the db interface (used by objects, refs, and unpack mixins) repo.db = db; - if (trace) db = require('./lib/tracedb.js')(db); + // Add in object store interfaces + require('./mixins/objects.js')(repo); // Git Objects - repo.load = load; // (hash-ish) -> object - repo.save = save; // (object) -> hash - repo.loadAs = loadAs; // (type, hash-ish) -> value - repo.saveAs = saveAs; // (type, value) -> hash - repo.remove = remove; // (hash-ish) repo.unpack = unpack; // (opts, packStream) // Convenience Readers @@ -35,14 +30,14 @@ function newRepo(db) { repo.treeWalk = treeWalk; // (hash-ish) => stream // Refs - repo.resolveHashish = resolveHashish; // (hash-ish) -> hash - repo.updateHead = updateHead; // (hash) - repo.getHead = getHead; // () -> ref - repo.setHead = setHead; // (ref) - repo.readRef = readRef; // (ref) -> hash - repo.createRef = createRef; // (ref, hash) - repo.deleteRef = deleteRef; // (ref) - repo.listRefs = listRefs; // (prefix) -> refs + repo.resolve = resolve; // (hash-ish) -> hash + repo.updateHead = updateHead; // (hash) + repo.getHead = getHead; // () -> ref + repo.setHead = setHead; // (ref) + repo.readRef = readRef; // (ref) -> hash + repo.createRef = createRef; // (ref, hash) + repo.deleteRef = deleteRef; // (ref) + repo.listRefs = listRefs; // (prefix) -> refs // Network Protocols repo.fetch = fetch; @@ -50,94 +45,9 @@ function newRepo(db) { return repo; - function load(hashish, callback) { - if (!callback) return load.bind(this, hashish); - var hash; - return resolveHashish(hashish, onHash); - - function onHash(err, result) { - if (result === undefined) return callback(err); - hash = result; - return db.get(hash, onBuffer); - } - - function onBuffer(err, buffer) { - if (err) return callback(err); - var type, object; - try { - if (sha1(buffer) !== hash) { - throw new Error("Hash checksum failed for " + hash); - } - var pair = deframe(buffer); - type = pair[0]; - buffer = pair[1]; - object = { - type: type, - body: decoders[type](buffer) - }; - } catch (err) { - if (err) return callback(err); - } - return callback(null, object, hash); - } - } - - function save(object, callback) { - if (!callback) return save.bind(this, object); - var buffer, hash; - try { - buffer = encoders[object.type](object.body); - buffer = frame(object.type, buffer); - hash = sha1(buffer); - } - catch (err) { - return callback(err); - } - return db.set(hash, buffer, onSave); - - function onSave(err) { - if (err) return callback(err); - return callback(null, hash); - } - } - - function loadAs(type, hashish, callback) { - if (!callback) return loadAs.bind(this, type, hashish); - return load(hashish, onObject); - - function onObject(err, object, hash) { - if (object === undefined) return callback(err); - if (type === "text") { - type = "blob"; - object.body = parseAscii(object.body, 0, object.body.length); - } - if (object.type !== type) { - return new Error("Expected " + type + ", but found " + object.type); - } - return callback(null, object.body, hash); - } - } - - function saveAs(type, body, callback) { - if (!callback) return saveAs.bind(this, type, body); - if (type === "text") type = "blob"; - return save({ type: type, body: body }, callback); - } - - function remove(hashish, callback) { - if (!callback) return remove.bind(this, hashish); - var hash; - return resolveHashish(hashish, onHash); - - function onHash(err, result) { - if (err) return callback(err); - hash = result; - return db.del(hash, callback); - } - } - function resolveHashish(hashish, callback) { - if (!callback) return resolveHashish.bind(this, hashish); + function resolve(hashish, callback) { + if (!callback) return resolve.bind(this, hashish); hashish = hashish.trim(); if ((/^[0-9a-f]{40}$/i).test(hashish)) { return callback(null, hashish.toLowerCase()); @@ -151,13 +61,13 @@ function newRepo(db) { function onBranch(err, ref) { if (err) return callback(err); if (!ref) return callback(); - return resolveHashish(ref, callback); + return resolve(ref, callback); } function checkBranch(err, hash) { if (err && err.code !== "ENOENT") return callback(err); if (hash) { - return resolveHashish(hash, callback); + return resolve(hash, callback); } return db.get("refs/heads/" + hashish, checkTag); } @@ -165,7 +75,7 @@ function newRepo(db) { function checkTag(err, hash) { if (err && err.code !== "ENOENT") return callback(err); if (hash) { - return resolveHashish(hash, callback); + return resolve(hash, callback); } return db.get("refs/tags/" + hashish, final); } @@ -173,7 +83,7 @@ function newRepo(db) { function final(err, hash) { if (err) return callback(err); if (hash) { - return resolveHashish(hash, callback); + return resolve(hash, callback); } err = new Error("ENOENT: Cannot find " + hashish); err.code = "ENOENT"; diff --git a/lib/fetch.js b/lib/fetch.js index 7beef45..8ca5b4f 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -95,7 +95,7 @@ function fetch(remote, opts, callback) { ref = list.shift(); if (!ref) return callback(null, Object.keys(wants)); hash = refs[ref]; - repo.resolveHashish(ref, onResolve); + repo.resolve(ref, onResolve); } function onResolve(err, oldHash) { // Skip refs we already have diff --git a/mixins/objects.js b/mixins/objects.js new file mode 100644 index 0000000..cfb184b --- /dev/null +++ b/mixins/objects.js @@ -0,0 +1,114 @@ +var sha1 = require('../lib/sha1.js'); +var frame = require('../lib/frame.js'); +var deframe = require('../lib/deframe.js'); +var encoders = require('../lib/encoders.js'); +var decoders = require('../lib/decoders.js'); +var parseAscii = require('../lib/parseascii.js'); + +// Add "objects" capabilities to a repo using db as storage. +module.exports = function (repo) { + + // Add Object store capability to the system + repo.load = load; // (hash-ish) -> object + repo.save = save; // (object) -> hash + repo.loadAs = loadAs; // (type, hash-ish) -> value + repo.saveAs = saveAs; // (type, value) -> hash + repo.remove = remove; // (hash) + + // This is a fallback resolve in case there is no refs system installed. + if (!repo.resolve) repo.resolve = function (hash, callback) { + if (isHash(hash)) return callback(null, hash); + return callback(new Error("This repo only supports direct hashes")); + }; + +}; + +function load(hashish, callback) { + if (!callback) return load.bind(this, hashish); + var hash; + var repo = this; + var db = repo.db; + return repo.resolve(hashish, onHash); + + function onHash(err, result) { + if (result === undefined) return callback(err); + hash = result; + return db.get(hash, onBuffer); + } + + function onBuffer(err, buffer) { + if (err) return callback(err); + var type, object; + try { + if (sha1(buffer) !== hash) { + throw new Error("Hash checksum failed for " + hash); + } + var pair = deframe(buffer); + type = pair[0]; + buffer = pair[1]; + object = { + type: type, + body: decoders[type](buffer) + }; + } catch (err) { + if (err) return callback(err); + } + return callback(null, object, hash); + } +} + +function save(object, callback) { + if (!callback) return save.bind(this, object); + var buffer, hash; + var repo = this; + var db = repo.db; + try { + buffer = encoders[object.type](object.body); + buffer = frame(object.type, buffer); + hash = sha1(buffer); + } + catch (err) { + return callback(err); + } + return db.set(hash, buffer, onSave); + + function onSave(err) { + if (err) return callback(err); + return callback(null, hash); + } +} + +function remove(hash, callback) { + if (!callback) return remove.bind(this, hash); + if (!isHash(hash)) return callback(new Error("Invalid hash: " + hash)); + var repo = this; + var db = repo.db; + return db.del(hash, callback); +} + +function loadAs(type, hashish, callback) { + if (!callback) return loadAs.bind(this, type, hashish); + return this.load(hashish, onObject); + + function onObject(err, object, hash) { + if (object === undefined) return callback(err); + if (type === "text") { + type = "blob"; + object.body = parseAscii(object.body, 0, object.body.length); + } + if (object.type !== type) { + return new Error("Expected " + type + ", but found " + object.type); + } + return callback(null, object.body, hash); + } +} + +function saveAs(type, body, callback) { + if (!callback) return saveAs.bind(this, type, body); + if (type === "text") type = "blob"; + return this.save({ type: type, body: body }, callback); +} + +function isHash(hash) { + return (/^[0-9a-f]{40}$/).test(hash); +} From e4bcb2b188befbd14f470ce719bf26ef4aff87e2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 10:22:34 -0600 Subject: [PATCH 051/256] Move refs methods into mixin module --- js-git.js | 153 ++-------------------------------------------- lib/ishash.js | 3 + mixins/objects.js | 5 +- mixins/refs.js | 152 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 152 deletions(-) create mode 100644 lib/ishash.js create mode 100644 mixins/refs.js diff --git a/js-git.js b/js-git.js index d1b44b6..36a3973 100644 --- a/js-git.js +++ b/js-git.js @@ -1,4 +1,3 @@ -var trace = require('./lib/trace.js'); var treeWalk = require('./lib/tree-walk.js'); var logWalk = require('./lib/log-walk.js'); @@ -14,14 +13,17 @@ function newRepo(db) { var repo = {}; // Auto trace the db if tracing is turned on. - if (trace) db = require('./lib/tracedb.js')(db); + if (require('./lib/trace.js')) db = require('./lib/tracedb.js')(db); // Add the db interface (used by objects, refs, and unpack mixins) repo.db = db; - // Add in object store interfaces + // Mix in object store interface require('./mixins/objects.js')(repo); + // Mix in the references interface + require('./mixins/refs.js')(repo); + // Git Objects repo.unpack = unpack; // (opts, packStream) @@ -29,15 +31,6 @@ function newRepo(db) { repo.logWalk = logWalk; // (hash-ish) => stream repo.treeWalk = treeWalk; // (hash-ish) => stream - // Refs - repo.resolve = resolve; // (hash-ish) -> hash - repo.updateHead = updateHead; // (hash) - repo.getHead = getHead; // () -> ref - repo.setHead = setHead; // (ref) - repo.readRef = readRef; // (ref) -> hash - repo.createRef = createRef; // (ref, hash) - repo.deleteRef = deleteRef; // (ref) - repo.listRefs = listRefs; // (prefix) -> refs // Network Protocols repo.fetch = fetch; @@ -46,141 +39,5 @@ function newRepo(db) { return repo; - function resolve(hashish, callback) { - if (!callback) return resolve.bind(this, hashish); - hashish = hashish.trim(); - if ((/^[0-9a-f]{40}$/i).test(hashish)) { - return callback(null, hashish.toLowerCase()); - } - if (hashish === "HEAD") return getHead(onBranch); - if ((/^refs\//).test(hashish)) { - return db.get(hashish, checkBranch); - } - return checkBranch(); - - function onBranch(err, ref) { - if (err) return callback(err); - if (!ref) return callback(); - return resolve(ref, callback); - } - - function checkBranch(err, hash) { - if (err && err.code !== "ENOENT") return callback(err); - if (hash) { - return resolve(hash, callback); - } - return db.get("refs/heads/" + hashish, checkTag); - } - - function checkTag(err, hash) { - if (err && err.code !== "ENOENT") return callback(err); - if (hash) { - return resolve(hash, callback); - } - return db.get("refs/tags/" + hashish, final); - } - - function final(err, hash) { - if (err) return callback(err); - if (hash) { - return resolve(hash, callback); - } - err = new Error("ENOENT: Cannot find " + hashish); - err.code = "ENOENT"; - return callback(err); - } - } - - function updateHead(hash, callback) { - if (!callback) return updateHead.bind(this, hash); - var ref; - return getHead(onBranch); - - function onBranch(err, result) { - if (err) return callback(err); - if (result === undefined) { - return setHead("master", function (err) { - if (err) return callback(err); - onBranch(err, "refs/heads/master"); - }); - } - ref = result; - return db.set(ref, hash + "\n", callback); - } - } - - function getHead(callback) { - if (!callback) return getHead.bind(this); - return db.get("HEAD", onRead); - - function onRead(err, ref) { - if (err) return callback(err); - if (!ref) return callback(); - var match = ref.match(/^ref: *(.*)/); - if (!match) return callback(new Error("Invalid HEAD")); - return callback(null, match[1]); - } - } - - function setHead(branchName, callback) { - if (!callback) return setHead.bind(this, branchName); - var ref = "refs/heads/" + branchName; - return db.set("HEAD", "ref: " + ref + "\n", callback); - } - - function readRef(ref, callback) { - if (!callback) return readRef.bind(this, ref); - return db.get(ref, function (err, result) { - if (err) return callback(err); - if (!result) return callback(); - return callback(null, result.trim()); - }); - } - - function createRef(ref, hash, callback) { - if (!callback) return createRef.bind(this, ref, hash); - return db.set(ref, hash + "\n", callback); - } - - function deleteRef(ref, callback) { - if (!callback) return deleteRef.bind(this, ref); - return db.del(ref, callback); - } - - function listRefs(prefix, callback) { - if (!callback) return listRefs.bind(this, prefix); - var branches = {}, list = [], target = prefix; - return db.keys(target, onNames); - - function onNames(err, names) { - if (err) { - if (err.code === "ENOENT") return shift(); - return callback(err); - } - for (var i = 0, l = names.length; i < l; ++i) { - list.push(target + "/" + names[i]); - } - return shift(); - } - - function shift(err) { - if (err) return callback(err); - target = list.shift(); - if (!target) return callback(null, branches); - return db.get(target, onRead); - } - - function onRead(err, hash) { - if (err) { - if (err.code === "EISDIR") return db.keys(target, onNames); - return callback(err); - } - if (hash) { - branches[target] = hash.trim(); - return shift(); - } - return db.keys(target, onNames); - } - } } diff --git a/lib/ishash.js b/lib/ishash.js new file mode 100644 index 0000000..6e46845 --- /dev/null +++ b/lib/ishash.js @@ -0,0 +1,3 @@ +module.exports = function isHash(hash) { + return (/^[0-9a-f]{40}$/).test(hash); +}; diff --git a/mixins/objects.js b/mixins/objects.js index cfb184b..f816cc4 100644 --- a/mixins/objects.js +++ b/mixins/objects.js @@ -4,6 +4,7 @@ var deframe = require('../lib/deframe.js'); var encoders = require('../lib/encoders.js'); var decoders = require('../lib/decoders.js'); var parseAscii = require('../lib/parseascii.js'); +var isHash = require('../lib/ishash.js'); // Add "objects" capabilities to a repo using db as storage. module.exports = function (repo) { @@ -108,7 +109,3 @@ function saveAs(type, body, callback) { if (type === "text") type = "blob"; return this.save({ type: type, body: body }, callback); } - -function isHash(hash) { - return (/^[0-9a-f]{40}$/).test(hash); -} diff --git a/mixins/refs.js b/mixins/refs.js new file mode 100644 index 0000000..35a83c9 --- /dev/null +++ b/mixins/refs.js @@ -0,0 +1,152 @@ +var isHash = require('../lib/ishash.js'); + +module.exports = function (repo) { + // Refs + repo.resolve = resolve; // (hash-ish) -> hash + repo.updateHead = updateHead; // (hash) + repo.getHead = getHead; // () -> ref + repo.setHead = setHead; // (ref) + repo.readRef = readRef; // (ref) -> hash + repo.writeRef = writeRef; // (ref, hash) + repo.deleteRef = deleteRef; // (ref) + repo.listRefs = listRefs; // (prefix) -> refs +}; + +function resolve(hashish, callback) { + if (!callback) return resolve.bind(this, hashish); + hashish = hashish.trim(); + var repo = this, db = repo.db; + if (isHash(hashish)) return callback(null, hashish); + if (hashish === "HEAD") return repo.getHead(onBranch); + if ((/^refs\//).test(hashish)) { + return db.get(hashish, checkBranch); + } + return checkBranch(); + + function onBranch(err, ref) { + if (err) return callback(err); + if (!ref) return callback(); + return repo.resolve(ref, callback); + } + + function checkBranch(err, hash) { + if (err && err.code !== "ENOENT") return callback(err); + if (hash) { + return repo.resolve(hash, callback); + } + return db.get("refs/heads/" + hashish, checkTag); + } + + function checkTag(err, hash) { + if (err && err.code !== "ENOENT") return callback(err); + if (hash) { + return repo.resolve(hash, callback); + } + return db.get("refs/tags/" + hashish, final); + } + + function final(err, hash) { + if (err) return callback(err); + if (hash) { + return repo.resolve(hash, callback); + } + err = new Error("ENOENT: Cannot find " + hashish); + err.code = "ENOENT"; + return callback(err); + } +} + +function updateHead(hash, callback) { + if (!callback) return updateHead.bind(this, hash); + var ref; + var repo = this, db = repo.db; + return getHead(onBranch); + + function onBranch(err, result) { + if (err) return callback(err); + if (result === undefined) { + return setHead("master", function (err) { + if (err) return callback(err); + onBranch(err, "refs/heads/master"); + }); + } + ref = result; + return db.set(ref, hash + "\n", callback); + } +} + +function getHead(callback) { + if (!callback) return getHead.bind(this); + var repo = this, db = repo.db; + return db.get("HEAD", onRead); + + function onRead(err, ref) { + if (err) return callback(err); + if (!ref) return callback(); + var match = ref.match(/^ref: *(.*)/); + if (!match) return callback(new Error("Invalid HEAD")); + return callback(null, match[1]); + } +} + +function setHead(branchName, callback) { + if (!callback) return setHead.bind(this, branchName); + var ref = "refs/heads/" + branchName; + return this.db.set("HEAD", "ref: " + ref + "\n", callback); +} + +function readRef(ref, callback) { + if (!callback) return readRef.bind(this, ref); + return this.db.get(ref, function (err, result) { + if (err) return callback(err); + if (!result) return callback(); + return callback(null, result.trim()); + }); +} + +function writeRef(ref, hash, callback) { + if (!callback) return writeRef.bind(this, ref, hash); + return this.db.set(ref, hash + "\n", callback); +} + +function deleteRef(ref, callback) { + if (!callback) return deleteRef.bind(this, ref); + return this.db.del(ref, callback); +} + +function listRefs(prefix, callback) { + if (!callback) return listRefs.bind(this, prefix); + var branches = {}, list = [], target = prefix; + var repo = this, db = repo.db; + return db.keys(target, onNames); + + function onNames(err, names) { + if (err) { + if (err.code === "ENOENT") return shift(); + return callback(err); + } + for (var i = 0, l = names.length; i < l; ++i) { + list.push(target + "/" + names[i]); + } + return shift(); + } + + function shift(err) { + if (err) return callback(err); + target = list.shift(); + if (!target) return callback(null, branches); + return db.get(target, onRead); + } + + function onRead(err, hash) { + if (err) { + if (err.code === "EISDIR") return db.keys(target, onNames); + return callback(err); + } + if (hash) { + branches[target] = hash.trim(); + return shift(); + } + return db.keys(target, onNames); + } +} From e32bfa58d9c905fb5b73d3d32c8bd3b8c894dc46 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 10:26:42 -0600 Subject: [PATCH 052/256] Move walkers into mixin --- js-git.js | 11 ++---- lib/log-walk.js | 43 ----------------------- lib/tree-walk.js | 45 ------------------------ mixins/walkers.js | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 96 deletions(-) delete mode 100644 lib/log-walk.js delete mode 100644 lib/tree-walk.js create mode 100644 mixins/walkers.js diff --git a/js-git.js b/js-git.js index 36a3973..70a2ff1 100644 --- a/js-git.js +++ b/js-git.js @@ -1,6 +1,3 @@ - -var treeWalk = require('./lib/tree-walk.js'); -var logWalk = require('./lib/log-walk.js'); var fetch = require('./lib/fetch.js'); var push = require('./lib/push.js'); var unpack = require('./lib/unpack.js'); @@ -24,14 +21,12 @@ function newRepo(db) { // Mix in the references interface require('./mixins/refs.js')(repo); + // Mix in the walk helpers + require('./mixins/walkers.js')(repo); + // Git Objects repo.unpack = unpack; // (opts, packStream) - // Convenience Readers - repo.logWalk = logWalk; // (hash-ish) => stream - repo.treeWalk = treeWalk; // (hash-ish) => stream - - // Network Protocols repo.fetch = fetch; repo.push = push; diff --git a/lib/log-walk.js b/lib/log-walk.js deleted file mode 100644 index 6420129..0000000 --- a/lib/log-walk.js +++ /dev/null @@ -1,43 +0,0 @@ -var walk = require('./walk.js'); - -module.exports = logWalk; - -function logWalk(hashish, callback) { - if (!callback) return logWalk.bind(this, hashish); - var last, seen = {}; - var repo = this; - return repo.readRef("shallow", onShallow); - - function onShallow(err, shallow) { - last = shallow; - return repo.loadAs("commit", hashish, onLoad); - } - - function onLoad(err, commit, hash) { - if (commit === undefined) return callback(err); - commit.hash = hash; - seen[hash] = true; - return callback(null, walk(commit, scan, loadKey, compare)); - } - - function scan(commit) { - if (last === commit) return []; - return commit.parents.filter(function (hash) { - return !seen[hash]; - }); - } - - function loadKey(hash, callback) { - return repo.loadAs("commit", hash, function (err, commit) { - if (err) return callback(err); - commit.hash = hash; - if (hash === last) commit.last = true; - return callback(null, commit); - }); - } - -} - -function compare(commit, other) { - return commit.author.date < other.author.date; -} diff --git a/lib/tree-walk.js b/lib/tree-walk.js deleted file mode 100644 index 22dba0b..0000000 --- a/lib/tree-walk.js +++ /dev/null @@ -1,45 +0,0 @@ -var walk = require('./walk.js'); -var assertType = require('./assert-type.js'); - -module.exports = treeWalk; - -function treeWalk(hashish, callback) { - if (!callback) return treeWalk.bind(this, hashish); - var repo = this; - return repo.load(hashish, onLoad); - function onLoad(err, item, hash) { - if (err) return callback(err); - if (item.type === "commit") return repo.load(item.body.tree, onLoad); - item.hash = hash; - item.path = "/"; - return callback(null, walk(item, treeScan, treeLoadKey, treeCompare)); - } - - function treeLoadKey(entry, callback) { - return repo.load(entry.hash, function (err, object) { - if (err) return callback(err); - entry.type = object.type; - entry.body = object.body; - return callback(null, entry); - }); - } - -} - -function treeScan(object) { - if (object.type === "blob") return []; - assertType(object, "tree"); - return object.body.filter(function (entry) { - return entry.mode !== 0160000; - }).map(function (entry) { - var path = object.path + entry.name; - if (entry.mode === 040000) path += "/"; - entry.path = path; - return entry; - }); -} - -function treeCompare(first, second) { - return first.path < second.path; -} - diff --git a/mixins/walkers.js b/mixins/walkers.js new file mode 100644 index 0000000..5373043 --- /dev/null +++ b/mixins/walkers.js @@ -0,0 +1,88 @@ +var walk = require('../lib/walk.js'); +var assertType = require('../lib/assert-type.js'); + +module.exports = function (repo) { + repo.logWalk = logWalk; // (hash-ish) => stream + repo.treeWalk = treeWalk; // (hash-ish) => stream +}; + +function logWalk(hashish, callback) { + if (!callback) return logWalk.bind(this, hashish); + var last, seen = {}; + var repo = this; + return repo.readRef("shallow", onShallow); + + function onShallow(err, shallow) { + last = shallow; + return repo.loadAs("commit", hashish, onLoad); + } + + function onLoad(err, commit, hash) { + if (commit === undefined) return callback(err); + commit.hash = hash; + seen[hash] = true; + return callback(null, walk(commit, scan, loadKey, compare)); + } + + function scan(commit) { + if (last === commit) return []; + return commit.parents.filter(function (hash) { + return !seen[hash]; + }); + } + + function loadKey(hash, callback) { + return repo.loadAs("commit", hash, function (err, commit) { + if (err) return callback(err); + commit.hash = hash; + if (hash === last) commit.last = true; + return callback(null, commit); + }); + } + +} + +function compare(commit, other) { + return commit.author.date < other.author.date; +} + +function treeWalk(hashish, callback) { + if (!callback) return treeWalk.bind(this, hashish); + var repo = this; + return repo.load(hashish, onLoad); + function onLoad(err, item, hash) { + if (err) return callback(err); + if (item.type === "commit") return repo.load(item.body.tree, onLoad); + item.hash = hash; + item.path = "/"; + return callback(null, walk(item, treeScan, treeLoadKey, treeCompare)); + } + + function treeLoadKey(entry, callback) { + return repo.load(entry.hash, function (err, object) { + if (err) return callback(err); + entry.type = object.type; + entry.body = object.body; + return callback(null, entry); + }); + } + +} + +function treeScan(object) { + if (object.type === "blob") return []; + assertType(object, "tree"); + return object.body.filter(function (entry) { + return entry.mode !== 0160000; + }).map(function (entry) { + var path = object.path + entry.name; + if (entry.mode === 040000) path += "/"; + entry.path = path; + return entry; + }); +} + +function treeCompare(first, second) { + return first.path < second.path; +} + From dc7bba72b89f9f54fe27404a698b6c51545e755c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 10:31:34 -0600 Subject: [PATCH 053/256] Convert unpack to packops mixin --- js-git.js | 7 ++++--- lib/unpack.js => mixins/packops.js | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) rename lib/unpack.js => mixins/packops.js (85%) diff --git a/js-git.js b/js-git.js index 70a2ff1..ba03b7d 100644 --- a/js-git.js +++ b/js-git.js @@ -1,6 +1,5 @@ var fetch = require('./lib/fetch.js'); var push = require('./lib/push.js'); -var unpack = require('./lib/unpack.js'); module.exports = newRepo; @@ -21,11 +20,13 @@ function newRepo(db) { // Mix in the references interface require('./mixins/refs.js')(repo); - // Mix in the walk helpers + // Mix in the walker helpers require('./mixins/walkers.js')(repo); + // Mix in packfile import and export ability + require('./mixins/packops.js')(repo); + // Git Objects - repo.unpack = unpack; // (opts, packStream) // Network Protocols repo.fetch = fetch; diff --git a/lib/unpack.js b/mixins/packops.js similarity index 85% rename from lib/unpack.js rename to mixins/packops.js index 0565fba..68654f5 100644 --- a/lib/unpack.js +++ b/mixins/packops.js @@ -1,13 +1,16 @@ -var deframe = require('./deframe.js'); -var frame = require('./frame.js'); -var sha1 = require('./sha1.js'); -var applyDelta = require('./apply-delta.js'); -module.exports = unpack; +var deframe = require('../lib/deframe.js'); +var frame = require('../lib/frame.js'); +var sha1 = require('../lib/sha1.js'); +var applyDelta = require('../lib/apply-delta.js'); + +module.exports = function (repo) { + repo.unpack = unpack; // (packStream, opts) -> hashes + repo.pack = pack; // (hashes, opts) -> packStream +}; function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); - var repo = this; - var db = repo.db; + var repo = this, db = repo.db; var version, num, numDeltas = 0, count = 0, countDeltas = 0; var done, startDeltaProgress = false; @@ -109,3 +112,8 @@ function unpack(packStream, opts, callback) { } } + +function pack(hashes, opts, callback) { + if (!callback) return pack.bind(this, hashes, opts); + callback(new Error("TODO: Implement pack")); +} \ No newline at end of file From 0467cf213820f4d916fc1f30acdc3da7a03a5c46 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 10:37:01 -0600 Subject: [PATCH 054/256] Move network ops into client mixin --- js-git.js | 17 ++++++----------- lib/push.js | 3 --- lib/fetch.js => mixins/client.js | 17 +++++++++++++---- mixins/server.js | 2 ++ 4 files changed, 21 insertions(+), 18 deletions(-) delete mode 100644 lib/push.js rename lib/fetch.js => mixins/client.js (93%) create mode 100644 mixins/server.js diff --git a/js-git.js b/js-git.js index ba03b7d..dc48018 100644 --- a/js-git.js +++ b/js-git.js @@ -1,17 +1,15 @@ -var fetch = require('./lib/fetch.js'); -var push = require('./lib/push.js'); - module.exports = newRepo; function newRepo(db) { if (!db) throw new TypeError("A db interface instance is required"); + // Create a new repo object. var repo = {}; // Auto trace the db if tracing is turned on. if (require('./lib/trace.js')) db = require('./lib/tracedb.js')(db); - // Add the db interface (used by objects, refs, and unpack mixins) + // Add the db interface (used by objects, refs, and packops mixins) repo.db = db; // Mix in object store interface @@ -26,14 +24,11 @@ function newRepo(db) { // Mix in packfile import and export ability require('./mixins/packops.js')(repo); - // Git Objects + // Mix in git network client ability + require('./mixins/client.js')(repo); - // Network Protocols - repo.fetch = fetch; - repo.push = push; + // Mix in git network client ability + require('./mixins/server.js')(repo); return repo; - - - } diff --git a/lib/push.js b/lib/push.js deleted file mode 100644 index c0ad1f8..0000000 --- a/lib/push.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function push() { - throw new Error("TODO: Implement repo.push"); -}; diff --git a/lib/fetch.js b/mixins/client.js similarity index 93% rename from lib/fetch.js rename to mixins/client.js index 8ca5b4f..5859b2b 100644 --- a/lib/fetch.js +++ b/mixins/client.js @@ -1,8 +1,11 @@ -var agent = require('./agent.js'); var pushToPull = require('push-to-pull'); -var parse = pushToPull(require('./decode-pack.js')); +var parse = pushToPull(require('../lib/decode-pack.js')); +var agent = require('../lib/agent.js'); -module.exports = fetch; +module.exports = function (repo) { + repo.fetch = fetch; + repo.push = push; +}; function fetch(remote, opts, callback) { if (!callback) return fetch.bind(this, remote, opts); @@ -160,7 +163,8 @@ function arrayFilter(want) { var result; try { for (var i = 0; i < length; ++i) { - if (result = wantMatch(ref, want[i])) break; + result = wantMatch(ref, want[i]); + if (result) break; } } catch (err) { @@ -169,3 +173,8 @@ function arrayFilter(want) { return callback(null, result); } } + +function push() { + throw new Error("TODO: Implement repo.push"); +} + diff --git a/mixins/server.js b/mixins/server.js new file mode 100644 index 0000000..b4d24c6 --- /dev/null +++ b/mixins/server.js @@ -0,0 +1,2 @@ +module.exports = function (repo) { +}; \ No newline at end of file From 58d87b978e9a2463c9c27fb0ca4280ff56077ccd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 10:52:00 -0600 Subject: [PATCH 055/256] Actually output unpacked hashes --- mixins/client.js | 1 - mixins/packops.js | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/mixins/client.js b/mixins/client.js index 5859b2b..bf14ada 100644 --- a/mixins/client.js +++ b/mixins/client.js @@ -177,4 +177,3 @@ function arrayFilter(want) { function push() { throw new Error("TODO: Implement repo.push"); } - diff --git a/mixins/packops.js b/mixins/packops.js index 68654f5..10d752f 100644 --- a/mixins/packops.js +++ b/mixins/packops.js @@ -10,7 +10,7 @@ module.exports = function (repo) { function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); - var repo = this, db = repo.db; + var db = this.db; var version, num, numDeltas = 0, count = 0, countDeltas = 0; var done, startDeltaProgress = false; @@ -24,7 +24,8 @@ function unpack(packStream, opts, callback) { function onDone(err) { if (done) return; done = true; - return callback(err); + if (err) return callback(err); + return callback(null, values(hashes)); } function onStats(err, stats) { @@ -116,4 +117,14 @@ function unpack(packStream, opts, callback) { function pack(hashes, opts, callback) { if (!callback) return pack.bind(this, hashes, opts); callback(new Error("TODO: Implement pack")); +} + +function values(object) { + var keys = Object.keys(object); + var length = keys.length; + var out = new Array(length); + for (var i = 0; i < length; i++) { + out[i] = object[keys[i]]; + } + return out; } \ No newline at end of file From df273f9107df5b64b2ea86f87f3bd0e5737800bd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Nov 2013 22:14:25 -0600 Subject: [PATCH 056/256] Bump version to 0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 279167f..d417c0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.5.4", + "version": "0.6.0", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From bfb2575225701d7d32d30b3243bee942a49a1370 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 11 Nov 2013 20:18:41 +1100 Subject: [PATCH 057/256] Adding git-indexeddb link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b031e78..284ba52 100644 --- a/README.md +++ b/README.md @@ -214,5 +214,6 @@ Being that js-git is so modular, here is a list of the most relevent modules tha - - A database interface adapter that wraps a fs interface. - - A git-db implementation based on `localStorage`. - - A git-db implementation that stores data in ram for quick testing. + - - A git-db implementation cased on `indexedDB`. [gen-run]: https://github.com/creationix/gen-run From 843c9839c7693d0d805e902111583a08a71be95c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 15 Nov 2013 15:51:08 -0600 Subject: [PATCH 058/256] Fix bug in getHead, bump to 0.6.1 --- mixins/refs.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mixins/refs.js b/mixins/refs.js index 35a83c9..26dfd47 100644 --- a/mixins/refs.js +++ b/mixins/refs.js @@ -60,12 +60,12 @@ function updateHead(hash, callback) { if (!callback) return updateHead.bind(this, hash); var ref; var repo = this, db = repo.db; - return getHead(onBranch); + return this.getHead(onBranch); function onBranch(err, result) { if (err) return callback(err); if (result === undefined) { - return setHead("master", function (err) { + return repo.setHead("master", function (err) { if (err) return callback(err); onBranch(err, "refs/heads/master"); }); diff --git a/package.json b/package.json index d417c0b..f2e1bd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.6.0", + "version": "0.6.1", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From 509ba5f5a99bb478393fb9c9058fc270a53b107b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 21 Nov 2013 13:30:19 -0600 Subject: [PATCH 059/256] Make sha1 code run on old browsers --- lib/sha1.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/sha1.js b/lib/sha1.js index 5de9f76..bcb00e4 100644 --- a/lib/sha1.js +++ b/lib/sha1.js @@ -1,3 +1,5 @@ +var Array32 = typeof Uint8Array === "function" ? Uint8Array : Array; + module.exports = function sha1(buffer) { if (buffer === undefined) return create(); var shasum = create(); @@ -13,7 +15,7 @@ function create() { var h3 = 0x10325476; var h4 = 0xC3D2E1F0; // The first 64 bytes (16 words) is the data chunk - var block = new Array(80), offset = 0, shift = 24; + var block = new Array32(80), offset = 0, shift = 24; var totalLength = 0; return { update: update, digest: digest }; From da4e60620392220557a5922e4608ee70c4f72645 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 21 Nov 2013 13:30:41 -0600 Subject: [PATCH 060/256] Stub out network functions --- mixins/client.js | 15 ++++++++------- mixins/clone.js | 3 +++ mixins/server.js | 14 +++++++++++++- 3 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 mixins/clone.js diff --git a/mixins/client.js b/mixins/client.js index bf14ada..d104d7e 100644 --- a/mixins/client.js +++ b/mixins/client.js @@ -3,12 +3,12 @@ var parse = pushToPull(require('../lib/decode-pack.js')); var agent = require('../lib/agent.js'); module.exports = function (repo) { - repo.fetch = fetch; - repo.push = push; + repo.fetchPack = fetchPack; + repo.uploadPack = uploadPack; }; -function fetch(remote, opts, callback) { - if (!callback) return fetch.bind(this, remote, opts); +function fetchPack(remote, opts, callback) { + if (!callback) return fetchPack.bind(this, remote, opts); var repo = this; var db = repo.db; var refs, branch, queue, ref, hash; @@ -24,7 +24,7 @@ function fetch(remote, opts, callback) { function onWants(err, wants) { if (err) return callback(err); opts.wants = wants; - return remote.fetch(repo, opts, onPackStream); + return remote.fetchPack(repo, opts, onPackStream); } function onPackStream(err, raw) { @@ -174,6 +174,7 @@ function arrayFilter(want) { } } -function push() { - throw new Error("TODO: Implement repo.push"); +function uploadPack(remote, opts, callback) { + if (!callback) return uploadPack.bind(this, remote, opts); + throw "TODO: Implement repo.uploadPack"; } diff --git a/mixins/clone.js b/mixins/clone.js new file mode 100644 index 0000000..53b6524 --- /dev/null +++ b/mixins/clone.js @@ -0,0 +1,3 @@ +module.exports = function (repo) { + // TODO: Implement clone +}; \ No newline at end of file diff --git a/mixins/server.js b/mixins/server.js index b4d24c6..ce719c8 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -1,2 +1,14 @@ module.exports = function (repo) { -}; \ No newline at end of file + repo.uploadPack = uploadPack; + repo.receivePack = receivePack; +}; + +function uploadPack(remote, opts, callback) { + if (!callback) return uploadPack.bind(this, remote, opts); + throw "TODO: Implement repo.uploadPack"; +} + +function receivePack(remote, opts, callback) { + if (!callback) return receivePack.bind(this, remote, opts); + throw "TODO: Implement repo.uploadPack"; +} \ No newline at end of file From 2933d37f6fedc05e78d7f0a6f0bd24dbbe3fb635 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 21 Nov 2013 16:53:33 -0600 Subject: [PATCH 061/256] Add example git server --- examples/clone.js | 3 +- examples/create.js | 85 ++++++++-------- examples/pkt-line.js | 131 +++++++++++++++++++++++++ examples/serve.js | 224 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 404 insertions(+), 39 deletions(-) create mode 100644 examples/pkt-line.js create mode 100644 examples/serve.js diff --git a/examples/clone.js b/examples/clone.js index 16ec3a4..1402d3e 100644 --- a/examples/clone.js +++ b/examples/clone.js @@ -24,7 +24,8 @@ if (process.env.DEPTH) { opts.depth = parseInt(process.env.DEPTH, 10); } -repo.fetch(remote, opts, function (err) { +repo.fetchPack(remote, opts, function (err) { if (err) throw err; console.log("Done"); }); + diff --git a/examples/create.js b/examples/create.js index 01b94df..7a52716 100644 --- a/examples/create.js +++ b/examples/create.js @@ -3,53 +3,62 @@ var jsGit = require('../.'); var fsDb = require('git-fs-db')(platform); var fs = platform.fs; -// Create a filesystem backed bare repo -var repo = jsGit(fsDb(fs("test.git"))); +if (!module.parent) { + // Create a filesystem backed bare repo + var repo = jsGit(fsDb(fs("test.git"))); + create(repo, function (err) { + if (err) throw err; + }); +} +else { + module.exports = create; +} -var mock = require('./mock.js'); -repo.setHead("master", function (err) { - if (err) throw err; - console.log("Git database Initialized"); +function create(repo, callback) { - var parent; - serialEach(mock.commits, function (message, files, next) { - // Start building a tree object. - var tree = {}; - parallelEach(files, function (name, contents, next) { - repo.saveAs("blob", contents, function (err, hash) { - if (err) return next(err); - tree[name] = { - mode: 0100644, - hash: hash - }; - next(); - }); - }, function (err) { - if (err) return next(err); - repo.saveAs("tree", tree, function (err, hash) { + var mock = require('./mock.js'); + + repo.setHead("master", function (err) { + if (err) return callback(err); + console.log("Git database Initialized"); + + var parent; + serialEach(mock.commits, function (message, files, next) { + // Start building a tree object. + var tree = {}; + parallelEach(files, function (name, contents, next) { + repo.saveAs("blob", contents, function (err, hash) { + if (err) return next(err); + tree[name] = { + mode: 0100644, + hash: hash + }; + next(); + }); + }, function (err) { if (err) return next(err); - var commit = { - tree: hash, - parent: parent, - author: mock.author, - committer: mock.committer, - message: message - }; - if (!parent) delete commit.parent; - repo.saveAs("commit", commit, function (err, hash) { + repo.saveAs("tree", tree, function (err, hash) { if (err) return next(err); - parent = hash; - repo.updateHead(hash, next); + var commit = { + tree: hash, + parent: parent, + author: mock.author, + committer: mock.committer, + message: message + }; + if (!parent) delete commit.parent; + repo.saveAs("commit", commit, function (err, hash) { + if (err) return next(err); + parent = hash; + repo.updateHead(hash, next); + }); }); }); - }); - }, function (err) { - if (err) throw err; - console.log("Done"); + }, callback); }); -}); +} // Mini control-flow library function serialEach(object, fn, callback) { diff --git a/examples/pkt-line.js b/examples/pkt-line.js new file mode 100644 index 0000000..f41a2f7 --- /dev/null +++ b/examples/pkt-line.js @@ -0,0 +1,131 @@ +var bops = { + to: require('bops/to.js'), + from: require('bops/from.js'), + create: require('bops/create.js'), + subarray: require('bops/subarray.js'), + join: require('bops/join.js'), +}; + +var PACK = bops.from("PACK"); + +module.exports = { + deframer: deframer, + framer: framer +}; + +function deframer(emit) { + var state = 0; + var offset = 4; + var length = 0; + var data; + + return function (item) { + + // Forward the EOS marker + if (item === undefined) return emit(); + + // Once we're in pack mode, everything goes straight through + if (state === 3) return emit(item); + + // Otherwise parse the data using a state machine. + for (var i = 0, l = item.length; i < l; i++) { + var byte = item[i]; + if (state === 0) { + var val = fromHexChar(byte); + if (val === -1) { + if (byte === PACK[0]) { + offset = 1; + state = 2; + continue; + } + state = -1; + throw new SyntaxError("Not a hex char: " + String.fromCharCode(byte)); + } + length |= val << ((--offset) * 4); + if (offset === 0) { + if (length === 4) { + offset = 4; + emit(""); + } + else if (length === 0) { + offset = 4; + emit(null); + } + else if (length > 4) { + length -= 4; + data = bops.create(length); + state = 1; + } + else { + state = -1; + throw new SyntaxError("Invalid length: " + length); + } + } + } + else if (state === 1) { + data[offset++] = byte; + if (offset === length) { + offset = 4; + state = 0; + length = 0; + if (data[0] === 1) { + emit(bops.subarray(data, 1)); + } + else if (data[0] === 2) { + emit({progress: bops.to(bops.subarray(data, 1))}); + } + else if (data[0] === 3) { + emit({error: bops.to(bops.subarray(data, 1))}); + } + else { + emit(bops.to(data)); + } + } + } + else if (state === 2) { + if (offset < 4 && byte === PACK[offset++]) { + continue; + } + state = 3; + emit(bops.join([PACK, bops.subarray(item, i)])); + break; + } + else { + throw new Error("pkt-line decoder in invalid state"); + } + } + }; + +} + +function framer(emit) { + return function (item) { + if (item === undefined) return emit(); + if (item === null) { + emit(bops.from("0000")); + return; + } + if (typeof item === "string") { + item = bops.from(item); + } + emit(bops.join([frameHead(item.length + 4), item])); + }; +} + +function frameHead(length) { + var buffer = bops.create(4); + buffer[0] = toHexChar(length >>> 12); + buffer[1] = toHexChar((length >>> 8) & 0xf); + buffer[2] = toHexChar((length >>> 4) & 0xf); + buffer[3] = toHexChar(length & 0xf); + return buffer; +} + +function fromHexChar(val) { + return (val >= 0x30 && val < 0x40) ? val - 0x30 : + ((val > 0x60 && val <= 0x66) ? val - 0x57 : -1); +} + +function toHexChar(val) { + return val < 0x0a ? val + 0x30 : val + 0x57; +} diff --git a/examples/serve.js b/examples/serve.js new file mode 100644 index 0000000..8a746a9 --- /dev/null +++ b/examples/serve.js @@ -0,0 +1,224 @@ +var jsGit = require('../.'); +var net = require('net'); +var inspect = require('util').inspect; + +var db = memDb(); +var repo = jsGit(db); +db.init(function (err) { + if (err) throw err; + require('./create.js')(repo, function (err) { + if (err) throw err; + console.log("Repo Initialized with sample data"); + }); +}); + +var server = net.createServer(function (socket) { + var remote = wrap(socket); + socket.on("error", onDone); + remote.read(function (err, line) { + if (err) return onDone(err); + var match = line.match(/^(git-upload-pack|git-receive-pack) (.+?)\0(?:host=(.+?)\0)$/); + if (!match) return onDone(new Error("Invalid connection message: " + line)); + var command = match[1]; + var path = match[2]; + if (path !== "/test.git") return onDone(new Error("Unknown repo: " + path)); + if (command === "git-upload-pack") { + return repo.uploadPack(remote, {}, onDone); + } + if (command === "git-receive-pack") { + return repo.receivePack(remote, {onProgress:onProgress}, onDone); + } + }); + + function onProgress(progress) { + console.log("P", progress); + } + function onDone(err) { + if (err) console.error(err.stack); + socket.destroy(); + } +}); +server.listen(9418, "127.0.0.1", function () { + console.log("GIT server listening at", server.address()); +}); + +////////////////////// TCP transport for git:// uris /////////////////////////// + +var pktLine = require('./pkt-line.js'); +function wrap(socket) { + var queue = []; + var rerr = null; + var rcb = null, wcb = null; + var onChunk = pktLine.deframer(onFrame); + var writeFrame = pktLine.framer(writeChunk); + socket.on("data", function (chunk) { + try { + onChunk(chunk); + } + catch (err) { + rerr = err; + check(); + } + }); + socket.on("end", onChunk); + socket.on("drain", onDrain); + return { read: read, write: write }; + + function onFrame(frame) { + console.log("<-", inspect(frame, {colors:true})); + queue.push(frame); + check(); + } + + function read(callback) { + if (!callback) return read; + if (rcb) return callback(new Error("Only one read at a time")); + rcb = callback; + check(); + } + + function check() { + if (rcb && (rerr || queue.length)) { + var callback = rcb; + rcb = null; + if (rerr) { + var err = rerr; + rerr = null; + callback(err); + } + else { + callback(null, queue.shift()); + } + } + if (queue.length) socket.pause(); + else if (rcb) socket.resume(); + } + + function write(frame, callback) { + if (callback === undefined) return write.bind(this, frame); + if (callback) { + if (wcb) return callback(new Error("Only one write at a time")); + wcb = callback; + } + try { + console.log("->", inspect(frame, {colors:true})); + writeFrame(frame); + } + catch (err) { + if (wcb) { + wcb = null; + callback(err); + } + else { + throw err; + } + } + } + + function writeChunk(chunk) { + if (chunk === undefined) { + socket.end(); + onDrain(); + } + else if (socket.write(chunk)) { + onDrain(); + } + } + + function onDrain() { + if (wcb) { + var callback = wcb; + wcb = null; + callback(); + } + } + +} + +/////////////////// inMemory database for easy testing ///////////////////////// + +function makeAsync(fn, callback) { + if (!callback) return makeAsync.bind(this, fn); + process.nextTick(function () { + var result; + try { result = fn(); } + catch (err) { return callback(err); } + if (result === undefined) return callback(); + return callback(null, result); + }); +} + +function memDb() { + + // Store everything in ram! + var objects; + var others; + var isHash = /^[a-z0-9]{40}$/; + + return { + get: get, + set: set, + has: has, + del: del, + keys: keys, + init: init, + clear: init, + }; + + function get(key, callback) { + return makeAsync(function () { + if (isHash.test(key)) { + return objects[key]; + } + return others[key]; + }, callback); + } + + function set(key, value, callback) { + return makeAsync(function () { + if (isHash.test(key)) { + objects[key] = value; + } + else { + others[key] = value.toString(); + } + }, callback); + } + + function has(key, callback) { + return makeAsync(function () { + if (isHash.test(key)) { + return key in objects; + } + return key in others; + }, callback); + } + + function del(key, callback) { + return makeAsync(function () { + if (isHash.test(key)) { + delete objects[key]; + } + else { + delete others[key]; + } + }, callback); + } + + function keys(prefix, callback) { + return makeAsync(function () { + var length = prefix.length; + return Object.keys(others).filter(function (key) { + return key.substr(0, length) === prefix; + }); + }, callback); + } + + function init(callback) { + return makeAsync(function () { + objects = {}; + others = {}; + }, callback); + } + +} From 8afa18bc42836883e50343ff2b1b008f3e27999b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 21 Nov 2013 16:54:44 -0600 Subject: [PATCH 062/256] Start to implement repo.receivePack --- mixins/client.js | 10 +++++----- mixins/refs.js | 51 +++++++++++++++++++++--------------------------- mixins/server.js | 45 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/mixins/client.js b/mixins/client.js index d104d7e..1f33a03 100644 --- a/mixins/client.js +++ b/mixins/client.js @@ -4,7 +4,7 @@ var agent = require('../lib/agent.js'); module.exports = function (repo) { repo.fetchPack = fetchPack; - repo.uploadPack = uploadPack; + repo.sendPack = sendPack; }; function fetchPack(remote, opts, callback) { @@ -24,7 +24,7 @@ function fetchPack(remote, opts, callback) { function onWants(err, wants) { if (err) return callback(err); opts.wants = wants; - return remote.fetchPack(repo, opts, onPackStream); + return remote.fetch(repo, opts, onPackStream); } function onPackStream(err, raw) { @@ -174,7 +174,7 @@ function arrayFilter(want) { } } -function uploadPack(remote, opts, callback) { - if (!callback) return uploadPack.bind(this, remote, opts); - throw "TODO: Implement repo.uploadPack"; +function sendPack(remote, opts, callback) { + if (!callback) return sendPack.bind(this, remote, opts); + throw "TODO: Implement repo.sendPack"; } diff --git a/mixins/refs.js b/mixins/refs.js index 26dfd47..c077986 100644 --- a/mixins/refs.js +++ b/mixins/refs.js @@ -116,37 +116,30 @@ function deleteRef(ref, callback) { function listRefs(prefix, callback) { if (!callback) return listRefs.bind(this, prefix); - var branches = {}, list = [], target = prefix; - var repo = this, db = repo.db; - return db.keys(target, onNames); - - function onNames(err, names) { - if (err) { - if (err.code === "ENOENT") return shift(); - return callback(err); - } - for (var i = 0, l = names.length; i < l; ++i) { - list.push(target + "/" + names[i]); - } - return shift(); + if (!prefix) prefix = "refs\/"; + else if (!/^refs\//.test(prefix)) { + return callback(new TypeError("Invalid prefix: " + prefix)); } + var db = this.db; + var refs = {}; + return db.keys(prefix, onKeys); - function shift(err) { + function onKeys(err, keys) { if (err) return callback(err); - target = list.shift(); - if (!target) return callback(null, branches); - return db.get(target, onRead); - } - - function onRead(err, hash) { - if (err) { - if (err.code === "EISDIR") return db.keys(target, onNames); - return callback(err); - } - if (hash) { - branches[target] = hash.trim(); - return shift(); - } - return db.keys(target, onNames); + var left = keys.length, done = false; + if (!left) return callback(null, refs); + keys.forEach(function (key) { + db.get(key, function (err, value) { + if (done) return; + if (err) { + done = true; + return callback(err); + } + refs[key] = value.trim(); + if (--left) return; + done = true; + callback(null, refs); + }); + }); } } diff --git a/mixins/server.js b/mixins/server.js index ce719c8..dfca298 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -10,5 +10,48 @@ function uploadPack(remote, opts, callback) { function receivePack(remote, opts, callback) { if (!callback) return receivePack.bind(this, remote, opts); - throw "TODO: Implement repo.uploadPack"; + var clientCaps = null, changes = []; + var repo = this; + this.listRefs(null, function (err, refs) { + if (err) return callback(err); + Object.keys(refs).forEach(function (ref, i) { + var hash = refs[ref]; + var line = hash + " " + ref; + if (!i) line += " report-status delete-refs"; + remote.write(line, null); + }); + remote.write(null, null); + remote.read(onLine); + }); + + function onLine(err, line) { + if (err) return callback(err); + if (line === null) { + return repo.unpack(remote, opts, onUnpack); + } + var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) (.+?)(?: (.+))?$/); + changes.push({ + oldHash: match[1], + newHash: match[2], + ref: match[3] + }); + if (match[4]) { + match[4].split(" ").map(function (cap) { + var pair = cap.split("="); + clientCaps[pair[0]] = pair[1] || true; + }); + } + remote.read(onLine); + } + + function onUnpack(err, out) { + if (err) return callback(err); + console.log({ + caps: clientCaps, + changes: changes, + out: out + }); + } + + } \ No newline at end of file From 553a79d8e02514edea88fda77497216b3d89722d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 21 Nov 2013 17:02:01 -0600 Subject: [PATCH 063/256] Clean up serve example some --- examples/serve.js | 63 ++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/examples/serve.js b/examples/serve.js index 8a746a9..e1feea9 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -12,38 +12,49 @@ db.init(function (err) { }); }); -var server = net.createServer(function (socket) { - var remote = wrap(socket); - socket.on("error", onDone); - remote.read(function (err, line) { - if (err) return onDone(err); - var match = line.match(/^(git-upload-pack|git-receive-pack) (.+?)\0(?:host=(.+?)\0)$/); - if (!match) return onDone(new Error("Invalid connection message: " + line)); - var command = match[1]; - var path = match[2]; - if (path !== "/test.git") return onDone(new Error("Unknown repo: " + path)); - if (command === "git-upload-pack") { - return repo.uploadPack(remote, {}, onDone); - } - if (command === "git-receive-pack") { - return repo.receivePack(remote, {onProgress:onProgress}, onDone); - } - }); - - function onProgress(progress) { - console.log("P", progress); - } - function onDone(err) { - if (err) console.error(err.stack); - socket.destroy(); - } -}); +var server = net.createServer(connectionHandler(function (req, callback) { + if (req.path !== "/test.git") return callback(new Error("Unknown repo: " + req.path)); + callback(null, repo); +}, {onProgress:console.log})); server.listen(9418, "127.0.0.1", function () { console.log("GIT server listening at", server.address()); }); ////////////////////// TCP transport for git:// uris /////////////////////////// +function connectionHandler(onReq, opts) { + opts = opts || {}; + return function (socket) { + var remote = wrap(socket), command; + socket.on("error", onDone); + remote.read(function (err, line) { + if (err) return onDone(err); + var match = line.match(/^(git-upload-pack|git-receive-pack) (.+?)\0(?:host=(.+?)\0)$/); + if (!match) return onDone(new Error("Invalid connection message: " + line)); + command = match[1]; + onReq({ + path: match[2], + host: match[3] + }, onRepo); + }); + + function onRepo(err, repo) { + if (err) return onDone(err); + if (command === "git-upload-pack") { + return repo.uploadPack(remote, opts, onDone); + } + if (command === "git-receive-pack") { + return repo.receivePack(remote, opts, onDone); + } + } + + function onDone(err) { + if (err) console.error(err.stack); + socket.destroy(); + } + }; +} + var pktLine = require('./pkt-line.js'); function wrap(socket) { var queue = []; From 167e15d6fdf3d85ded1b655dcc5cc6e265cd2111 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 22 Nov 2013 10:40:36 -0600 Subject: [PATCH 064/256] Fix typo in sha1 code --- lib/sha1.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sha1.js b/lib/sha1.js index bcb00e4..1438d5c 100644 --- a/lib/sha1.js +++ b/lib/sha1.js @@ -1,4 +1,4 @@ -var Array32 = typeof Uint8Array === "function" ? Uint8Array : Array; +var Array32 = typeof Uint32Array === "function" ? Uint32Array : Array; module.exports = function sha1(buffer) { if (buffer === undefined) return create(); From c1fc85696748fe5da18fc428356cffe6db2b3459 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 22 Nov 2013 10:53:21 -0600 Subject: [PATCH 065/256] Fix unpack to work with receivePack --- examples/serve.js | 2 +- mixins/packops.js | 221 +++++++++++++++++++++++++++++++++++++++++++--- mixins/server.js | 8 +- 3 files changed, 216 insertions(+), 15 deletions(-) diff --git a/examples/serve.js b/examples/serve.js index e1feea9..1f6dfb1 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -15,7 +15,7 @@ db.init(function (err) { var server = net.createServer(connectionHandler(function (req, callback) { if (req.path !== "/test.git") return callback(new Error("Unknown repo: " + req.path)); callback(null, repo); -}, {onProgress:console.log})); +})); server.listen(9418, "127.0.0.1", function () { console.log("GIT server listening at", server.address()); }); diff --git a/mixins/packops.js b/mixins/packops.js index 10d752f..0d28f44 100644 --- a/mixins/packops.js +++ b/mixins/packops.js @@ -1,15 +1,24 @@ +var bops = require('bops'); var deframe = require('../lib/deframe.js'); var frame = require('../lib/frame.js'); var sha1 = require('../lib/sha1.js'); +var inflate = require('../lib/inflate.js'); var applyDelta = require('../lib/apply-delta.js'); +var pushToPull = require('push-to-pull'); module.exports = function (repo) { + // packStream is a simple-stream containing raw packfile binary data + // opts can contain "onProgress" or "onError" hook functions. + // callback will be called with a list of all unpacked hashes on success. repo.unpack = unpack; // (packStream, opts) -> hashes + repo.pack = pack; // (hashes, opts) -> packStream }; function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); + packStream = pushToPull(decodePack)(packStream); + var db = this.db; var version, num, numDeltas = 0, count = 0, countDeltas = 0; @@ -17,7 +26,10 @@ function unpack(packStream, opts, callback) { // hashes keyed by offset for ofs-delta resolving var hashes = {}; + // key is hash, boolean is cached "has" value of true or false var has = {}; + // key is hash we're waiting for, value is array of items that are waiting. + var pending = {}; return packStream.read(onStats); @@ -50,7 +62,7 @@ function unpack(packStream, opts, callback) { function onRead(err, item) { if (err) return onDone(err); if (opts.onProgress) objectProgress(item); - if (item === undefined) return resolveDeltas(); + if (item === undefined) return onDone(); if (item.size !== item.body.length) { return onDone(new Error("Body size mismatch")); } @@ -93,6 +105,15 @@ function unpack(packStream, opts, callback) { var buffer = frame(item.type, item.body); var hash = hashes[item.offset] = sha1(buffer); has[hash] = true; + if (hash in pending) { + // I have yet to come across a pack stream that actually needs this. + // So I will only implement it when I have concrete data to test against. + console.error({ + list: pending[hash], + item: item + }); + throw "TODO: pending value was found, resolve it"; + } return db.set(hash, buffer, onSave); } @@ -102,14 +123,10 @@ function unpack(packStream, opts, callback) { } function enqueueDelta(item) { - // I have yet to come across a repo that actually needs this path. - // It's hard to implement without something to test against. - throw "TODO: enqueueDelta"; - } - - function resolveDeltas() { - // TODO: resolve any pending deltas once enqueueDelta is implemented. - return onDone(); + var list = pending[item.ref]; + if (!list) pending[item.ref] = [item]; + else list.push(item); + packStream.read(onRead); } } @@ -127,4 +144,188 @@ function values(object) { out[i] = object[keys[i]]; } return out; -} \ No newline at end of file +} + +var types = { + "1": "commit", + "2": "tree", + "3": "blob", + "4": "tag", + "6": "ofs-delta", + "7": "ref-delta" +}; + +function decodePack(emit) { + + var state = $pack; + var sha1sum = sha1(); + var inf = inflate(); + + var offset = 0; + var position = 0; + var version = 0x4b434150; // PACK reversed + var num = 0; + var type = 0; + var length = 0; + var ref = null; + var checksum = ""; + var start = 0; + var parts = []; + + + return function (chunk) { + if (chunk === undefined) { + if (num || checksum.length < 40) throw new Error("Unexpected end of input stream"); + return emit(); + } + + for (var i = 0, l = chunk.length; i < l; i++) { + // console.log([state, i, chunk[i].toString(16)]); + if (!state) throw new Error("Unexpected extra bytes: " + bops.subarray(chunk, i)); + state = state(chunk[i], i, chunk); + position++; + } + if (!state) return; + if (state !== $checksum) sha1sum.update(chunk); + var buff = inf.flush(); + if (buff.length) { + parts.push(buff); + } + }; + + // The first four bytes in a packfile are the bytes 'PACK' + function $pack(byte) { + if ((version & 0xff) === byte) { + version >>>= 8; + return version ? $pack : $version; + } + throw new Error("Invalid packfile header"); + } + + // The version is stored as an unsigned 32 integer in network byte order. + // It must be version 2 or 3. + function $version(byte) { + version = (version << 8) | byte; + if (++offset < 4) return $version; + if (version >= 2 && version <= 3) { + offset = 0; + return $num; + } + throw new Error("Invalid version number " + num); + } + + // The number of objects in this packfile is also stored as an unsigned 32 bit int. + function $num(byte) { + num = (num << 8) | byte; + if (++offset < 4) return $num; + offset = 0; + emit({version: version, num: num}); + return $header; + } + + // n-byte type and length (3-bit type, (n-1)*7+4-bit length) + // CTTTSSSS + // C is continue bit, TTT is type, S+ is length + function $header(byte) { + if (start === 0) start = position; + type = byte >> 4 & 0x07; + length = byte & 0x0f; + if (byte & 0x80) { + offset = 4; + return $header2; + } + return afterHeader(); + } + + // Second state in the same header parsing. + // CSSSSSSS* + function $header2(byte) { + length |= (byte & 0x7f) << offset; + if (byte & 0x80) { + offset += 7; + return $header2; + } + return afterHeader(); + } + + // Common helper for finishing tiny and normal headers. + function afterHeader() { + offset = 0; + if (type === 6) { + ref = 0; + return $ofsDelta; + } + if (type === 7) { + ref = ""; + return $refDelta; + } + return $body; + } + + // Big-endian modified base 128 number encoded ref offset + function $ofsDelta(byte) { + ref = byte & 0x7f; + if (byte & 0x80) return $ofsDelta2; + return $body; + } + + function $ofsDelta2(byte) { + ref = ((ref + 1) << 7) | (byte & 0x7f); + if (byte & 0x80) return $ofsDelta2; + return $body; + } + + // 20 byte raw sha1 hash for ref + function $refDelta(byte) { + ref += toHex(byte); + if (++offset < 20) return $refDelta; + return $body; + } + + // Common helper for generating 2-character hex numbers + function toHex(num) { + return num < 0x10 ? "0" + num.toString(16) : num.toString(16); + } + + // Common helper for emitting all three object shapes + function emitObject() { + var item = { + type: types[type], + size: length, + body: bops.join(parts), + offset: start + }; + if (ref) item.ref = ref; + parts.length = 0; + start = 0; + offset = 0; + type = 0; + length = 0; + ref = null; + emit(item); + } + + // Feed the deflated code to the inflate engine + function $body(byte, i, chunk) { + if (inf.write(byte)) return $body; + var buf = inf.flush(); + inf.recycle(); + if (buf.length) { + parts.push(buf); + } + emitObject(); + // If this was all the objects, start calculating the sha1sum + if (--num) return $header; + sha1sum.update(bops.subarray(chunk, 0, i + 1)); + return $checksum; + } + + // 20 byte checksum + function $checksum(byte) { + checksum += toHex(byte); + if (++offset < 20) return $checksum; + var actual = sha1sum.digest(); + if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum); + } + +} diff --git a/mixins/server.js b/mixins/server.js index dfca298..7db7fa6 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -10,7 +10,7 @@ function uploadPack(remote, opts, callback) { function receivePack(remote, opts, callback) { if (!callback) return receivePack.bind(this, remote, opts); - var clientCaps = null, changes = []; + var clientCaps = {}, changes = []; var repo = this; this.listRefs(null, function (err, refs) { if (err) return callback(err); @@ -29,7 +29,7 @@ function receivePack(remote, opts, callback) { if (line === null) { return repo.unpack(remote, opts, onUnpack); } - var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) (.+?)(?: (.+))?$/); + var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) ([^ ]+)(?: (.+))?$/); changes.push({ oldHash: match[1], newHash: match[2], @@ -44,12 +44,12 @@ function receivePack(remote, opts, callback) { remote.read(onLine); } - function onUnpack(err, out) { + function onUnpack(err, hashes) { if (err) return callback(err); console.log({ caps: clientCaps, changes: changes, - out: out + numHashes: hashes.length }); } From 84b8d9e88907519701f179c4bfa5ed7b4b9fe6ee Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 22 Nov 2013 11:36:40 -0600 Subject: [PATCH 066/256] Implement all of receivePack except for report-status capability --- examples/serve.js | 17 +++++++++++------ mixins/refs.js | 14 +++++++++++--- mixins/server.js | 29 +++++++++++++++++++---------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/examples/serve.js b/examples/serve.js index 1f6dfb1..3f36cdf 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -25,17 +25,16 @@ server.listen(9418, "127.0.0.1", function () { function connectionHandler(onReq, opts) { opts = opts || {}; return function (socket) { - var remote = wrap(socket), command; + var remote = wrap(socket), command, path, host; socket.on("error", onDone); remote.read(function (err, line) { if (err) return onDone(err); var match = line.match(/^(git-upload-pack|git-receive-pack) (.+?)\0(?:host=(.+?)\0)$/); if (!match) return onDone(new Error("Invalid connection message: " + line)); command = match[1]; - onReq({ - path: match[2], - host: match[3] - }, onRepo); + path = match[2]; + host = match[3]; + onReq({ path: path, host: host }, onRepo); }); function onRepo(err, repo) { @@ -48,8 +47,14 @@ function connectionHandler(onReq, opts) { } } - function onDone(err) { + function onDone(err, changes) { if (err) console.error(err.stack); + else console.log("DONE", { + command: command, + path: path, + host: host, + changes: changes + }); socket.destroy(); } }; diff --git a/mixins/refs.js b/mixins/refs.js index c077986..af8cdde 100644 --- a/mixins/refs.js +++ b/mixins/refs.js @@ -7,7 +7,8 @@ module.exports = function (repo) { repo.getHead = getHead; // () -> ref repo.setHead = setHead; // (ref) repo.readRef = readRef; // (ref) -> hash - repo.writeRef = writeRef; // (ref, hash) + repo.createRef = createRef; // (ref, hash) + repo.updateRef = updateRef; // (ref, hash) repo.deleteRef = deleteRef; // (ref) repo.listRefs = listRefs; // (prefix) -> refs }; @@ -104,8 +105,15 @@ function readRef(ref, callback) { }); } -function writeRef(ref, hash, callback) { - if (!callback) return writeRef.bind(this, ref, hash); +function createRef(ref, hash, callback) { + if (!callback) return createRef.bind(this, ref, hash); + // TODO: should we check to make sure it doesn't exist first? + return this.db.set(ref, hash + "\n", callback); +} + +function updateRef(ref, hash, callback) { + if (!callback) return updateRef.bind(this, ref, hash); + // TODO: should we check to make sure it does exist first? return this.db.set(ref, hash + "\n", callback); } diff --git a/mixins/server.js b/mixins/server.js index 7db7fa6..77b0bda 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -17,7 +17,8 @@ function receivePack(remote, opts, callback) { Object.keys(refs).forEach(function (ref, i) { var hash = refs[ref]; var line = hash + " " + ref; - if (!i) line += " report-status delete-refs"; + // TODO: Implement report-status below and add here + if (!i) line += "\0delete-refs ofs-delta"; remote.write(line, null); }); remote.write(null, null); @@ -27,7 +28,8 @@ function receivePack(remote, opts, callback) { function onLine(err, line) { if (err) return callback(err); if (line === null) { - return repo.unpack(remote, opts, onUnpack); + if (changes.length) return repo.unpack(remote, opts, onUnpack); + return callback(null, changes); } var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) ([^ ]+)(?: (.+))?$/); changes.push({ @@ -44,14 +46,21 @@ function receivePack(remote, opts, callback) { remote.read(onLine); } - function onUnpack(err, hashes) { + function onUnpack(err) { if (err) return callback(err); - console.log({ - caps: clientCaps, - changes: changes, - numHashes: hashes.length - }); + var i = 0, change; + next(); + function next(err) { + if (err) return callback(err); + change = changes[i++]; + if (!change) return callback(err, changes); + if (change.oldHash === "0000000000000000000000000000000000000000") { + return repo.createRef(change.ref, change.newHash, next); + } + if (change.newHash === "0000000000000000000000000000000000000000") { + return repo.deleteRef(change.ref, next); + } + return repo.updateRef(change.ref, change.newHash, next); + } } - - } \ No newline at end of file From e6207a8d9569c8e4669689b2f6d8066233e0e60b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 22 Nov 2013 17:34:26 -0600 Subject: [PATCH 067/256] Almost get clone working --- examples/pkt-line.js | 43 +++++++--- examples/serve.js | 1 + lib/decoders.js | 7 +- lib/deflate.js | 5 ++ mixins/objects.js | 25 ++++-- mixins/packops.js | 95 ++++++++++++++++++---- mixins/server.js | 187 ++++++++++++++++++++++++++++++++++++++++--- 7 files changed, 319 insertions(+), 44 deletions(-) create mode 100644 lib/deflate.js diff --git a/examples/pkt-line.js b/examples/pkt-line.js index f41a2f7..09cc550 100644 --- a/examples/pkt-line.js +++ b/examples/pkt-line.js @@ -1,4 +1,5 @@ var bops = { + is: require('bops/is.js'), to: require('bops/to.js'), from: require('bops/from.js'), create: require('bops/create.js'), @@ -10,7 +11,8 @@ var PACK = bops.from("PACK"); module.exports = { deframer: deframer, - framer: framer + framer: framer, + frame: frame, }; function deframer(emit) { @@ -72,10 +74,10 @@ function deframer(emit) { emit(bops.subarray(data, 1)); } else if (data[0] === 2) { - emit({progress: bops.to(bops.subarray(data, 1))}); + emit(["progress", bops.to(bops.subarray(data, 1))]); } else if (data[0] === 3) { - emit({error: bops.to(bops.subarray(data, 1))}); + emit(["error", bops.to(bops.subarray(data, 1))]); } else { emit(bops.to(data)); @@ -98,22 +100,41 @@ function deframer(emit) { } + function framer(emit) { return function (item) { if (item === undefined) return emit(); - if (item === null) { - emit(bops.from("0000")); - return; - } + emit(frame(item)); + }; +} + +function frame(item) { + if (item === null) return bops.from("0000"); + if (typeof item === "string") { + item = bops.from(item); + } + if (bops.is(item)) { + return bops.join([frameHead(item.length + 4), item]); + } + if (Array.isArray(item)) { + var type = item[0]; + item = item[1]; + var head = bops.create(5); + if (type === "pack") head[4] = 1; + else if (type === "progress") head[4] = 2; + else if (type === "error") head[4] = 3; + else throw new Error("Invalid channel name: " + type); if (typeof item === "string") { item = bops.from(item); } - emit(bops.join([frameHead(item.length + 4), item])); - }; + return bops.join([frameHead(item.length + 5, head), item]); + } + throw new Error("Invalid input: " + item); } -function frameHead(length) { - var buffer = bops.create(4); + +function frameHead(length, buffer) { + buffer = buffer || bops.create(4); buffer[0] = toHexChar(length >>> 12); buffer[1] = toHexChar((length >>> 8) & 0xf); buffer[2] = toHexChar((length >>> 4) & 0xf); diff --git a/examples/serve.js b/examples/serve.js index 3f36cdf..95d065d 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -132,6 +132,7 @@ function wrap(socket) { } function writeChunk(chunk) { + // console.log(">>", inspect("" + chunk, {colors:true})); if (chunk === undefined) { socket.end(); onDrain(); diff --git a/lib/decoders.js b/lib/decoders.js index 9713e60..1ea962d 100644 --- a/lib/decoders.js +++ b/lib/decoders.js @@ -68,7 +68,7 @@ exports.tree = function decodeTree(body) { var mode; var name; var hash; - var tree = []; + var tree = {}; while (i < length) { start = i; i = indexOf(body, 0x20, start); @@ -78,11 +78,10 @@ exports.tree = function decodeTree(body) { i = indexOf(body, 0x00, start); name = parseAscii(body, start, i++); hash = parseToHex(body, i, i += 20); - tree.push({ + tree[name] = { mode: mode, - name: name, hash: hash - }); + }; } return tree; }; diff --git a/lib/deflate.js b/lib/deflate.js new file mode 100644 index 0000000..a7b797a --- /dev/null +++ b/lib/deflate.js @@ -0,0 +1,5 @@ +var zlib = require('zlib'); +module.exports = function deflate(buffer, callback) { + return zlib.deflate(buffer, callback); +}; +// TODO: make this work in the browser too. \ No newline at end of file diff --git a/mixins/objects.js b/mixins/objects.js index f816cc4..10d166e 100644 --- a/mixins/objects.js +++ b/mixins/objects.js @@ -10,11 +10,14 @@ var isHash = require('../lib/ishash.js'); module.exports = function (repo) { // Add Object store capability to the system - repo.load = load; // (hash-ish) -> object - repo.save = save; // (object) -> hash - repo.loadAs = loadAs; // (type, hash-ish) -> value - repo.saveAs = saveAs; // (type, value) -> hash - repo.remove = remove; // (hash) + repo.load = load; // (hash-ish) -> object + repo.save = save; // (object) -> hash + repo.loadRaw = loadRaw; // (hash) -> buffer + repo.saveRaw = saveRaw; // (hash, buffer) + repo.has = has; // (hash) -> true or false + repo.loadAs = loadAs; // (type, hash-ish) -> value + repo.saveAs = saveAs; // (type, value) -> hash + repo.remove = remove; // (hash) // This is a fallback resolve in case there is no refs system installed. if (!repo.resolve) repo.resolve = function (hash, callback) { @@ -58,6 +61,18 @@ function load(hashish, callback) { } } +function loadRaw(hash, callback) { + return this.db.get(hash, callback); +} + +function saveRaw(hash, buffer, callback) { + return this.db.set(hash, buffer, callback); +} + +function has(hash, callback) { + return this.db.has(hash, callback); +} + function save(object, callback) { if (!callback) return save.bind(this, object); var buffer, hash; diff --git a/mixins/packops.js b/mixins/packops.js index 0d28f44..cfc2770 100644 --- a/mixins/packops.js +++ b/mixins/packops.js @@ -3,15 +3,32 @@ var deframe = require('../lib/deframe.js'); var frame = require('../lib/frame.js'); var sha1 = require('../lib/sha1.js'); var inflate = require('../lib/inflate.js'); +var deflate = require('../lib/deflate.js'); var applyDelta = require('../lib/apply-delta.js'); var pushToPull = require('push-to-pull'); +var typeToNum = { + commit: 1, + tree: 2, + blob: 3, + tag: 4, + "ofs-delta": 5, + "ref-delta": 6 +}; +var numToType = {}; +for (var type in typeToNum) { + var num = typeToNum[type]; + numToType[num] = type; +} + module.exports = function (repo) { // packStream is a simple-stream containing raw packfile binary data // opts can contain "onProgress" or "onError" hook functions. // callback will be called with a list of all unpacked hashes on success. repo.unpack = unpack; // (packStream, opts) -> hashes + // hashes is an array of hashes to pack + // callback will be a simple-stream containing raw packfile binary data repo.pack = pack; // (hashes, opts) -> packStream }; @@ -19,7 +36,7 @@ function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); packStream = pushToPull(decodePack)(packStream); - var db = this.db; + var repo = this; var version, num, numDeltas = 0, count = 0, countDeltas = 0; var done, startDeltaProgress = false; @@ -80,7 +97,7 @@ function unpack(packStream, opts, callback) { function resolveDelta(item) { if (opts.onProgress) deltaProgress(); - return db.get(item.ref, function (err, buffer) { + return repo.loadRaw(item.ref, function (err, buffer) { if (err) return onDone(err); var target = deframe(buffer); item.type = target[0]; @@ -93,7 +110,7 @@ function unpack(packStream, opts, callback) { var hasTarget = has[item.ref]; if (hasTarget === true) return resolveDelta(item); if (hasTarget === false) return enqueueDelta(item); - return db.has(item.ref, function (err, value) { + return repo.has(item.ref, function (err, value) { if (err) return onDone(err); has[item.ref] = value; if (value) return resolveDelta(item); @@ -114,7 +131,7 @@ function unpack(packStream, opts, callback) { }); throw "TODO: pending value was found, resolve it"; } - return db.set(hash, buffer, onSave); + return repo.saveRaw(hash, buffer, onSave); } function onSave(err) { @@ -131,9 +148,66 @@ function unpack(packStream, opts, callback) { } +function packFrame(type, body, callback) { + var length = body.length; + var head = [(typeToNum[type] << 4) | (length & 0xf)]; + var i = 0; + length >>= 4; + while (length) { + head[i++] |= 0x80; + head[i] = length & 0x7f; + length >>= 7; + } + deflate(body, function (err, body) { + if (err) return callback(err); + callback(null, bops.join([bops.from(head), body])); + }); +} + +// TODO: Implement delta refs to reduce stream size function pack(hashes, opts, callback) { if (!callback) return pack.bind(this, hashes, opts); - callback(new Error("TODO: Implement pack")); + var repo = this; + var sha1sum = sha1(); + var i = 0, first = true, done = false; + return callback(null, { read: read, abort: callback }); + + function read(callback) { + if (done) return callback(); + if (first) return readFirst(callback); + var hash = hashes[i++]; + if (hash === undefined) { + var sum = sha1sum.digest(); + done = true; + return callback(null, bops.from(sum, "hex")); + } + repo.loadRaw(hash, function (err, buffer) { + if (err) return callback(err); + if (!buffer) return callback(new Error("Missing hash: " + hash)); + // Reframe with pack format header + var pair = deframe(buffer); + packFrame(pair[0], pair[1], function (err, buffer) { + if (err) return callback(err); + sha1sum.update(buffer); + callback(null, buffer); + }); + }); + } + + function readFirst(callback) { + var length = hashes.length; + var chunk = bops.create([ + 0x50, 0x41, 0x43, 0x4b, // PACK + 0, 0, 0, 2, // version 2 + length >> 24, // Num of objects + (length >> 16) & 0xff, + (length >> 8) & 0xff, + length & 0xff + ]); + first = false; + sha1sum.update(chunk); + callback(null, chunk); + } } function values(object) { @@ -146,15 +220,6 @@ function values(object) { return out; } -var types = { - "1": "commit", - "2": "tree", - "3": "blob", - "4": "tag", - "6": "ofs-delta", - "7": "ref-delta" -}; - function decodePack(emit) { var state = $pack; @@ -290,7 +355,7 @@ function decodePack(emit) { // Common helper for emitting all three object shapes function emitObject() { var item = { - type: types[type], + type: numToType[type], size: length, body: bops.join(parts), offset: start diff --git a/mixins/server.js b/mixins/server.js index 77b0bda..0ecb780 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -1,3 +1,7 @@ +var bops = { + join: require('bops/join.js') +}; + module.exports = function (repo) { repo.uploadPack = uploadPack; repo.receivePack = receivePack; @@ -5,7 +9,119 @@ module.exports = function (repo) { function uploadPack(remote, opts, callback) { if (!callback) return uploadPack.bind(this, remote, opts); - throw "TODO: Implement repo.uploadPack"; + var repo = this, refs, wants = {}, haves = {}, clientCaps = {}; + var packQueue = []; + var queueBytes = 0; + var queueLimit = 0; + repo.listRefs(null, onRefs); + + function onRefs(err, result) { + if (err) return callback(err); + refs = result; + repo.getHead(onHead); + } + + function onHead(err, head) { + if (err) return callback(err); + // TODO: add "multi_ack" once it's implemented + // TODO: add "multi_ack_detailed" once it's implemented + // TODO: add "shallow" once it's implemented + // TODO: add "include-tag" once it's implemented + // TODO: add "thin-pack" once it's implemented + remote.write(refs[head] + " HEAD\0no-progress side-band side-band-64k ofs-delta", null); + Object.keys(refs).forEach(function (ref) { + remote.write(refs[ref] + " " + ref, null); + }); + remote.write(null, null); + remote.read(onWant); + } + + function onWant(err, line) { + if (line === undefined) return callback(err); + if (line === null) { + return remote.read(onHave); + } + var match = line.match(/^want ([0-9a-f]{40})(?: (.+))?\n?$/); + if (!match) { + return callback(new Error("Invalid want: " + line)); + } + var hash = match[1]; + if (match[2]) clientCaps = parseCaps(match[2]); + wants[hash] = true; + remote.read(onWant); + } + + function onHave(err, line) { + if (line === undefined) return callback(err); + var match = line.match(/^(done|have)(?: ([0-9a-f]{40}))?\n?$/); + if (!match) { + return callback(new Error("Unexpected have line: " + line)); + } + if (match[1] === "have") { + haves[match[2]] = true; + return remote.read(onHave); + } + if (Object.keys(haves).length) { + throw new Error("TODO: handle haves"); + } + remote.write("NAK\n", null); + walkRepo(repo, wants, haves, onHashes); + } + + function onHashes(err, hashes) { + if (err) return callback(err); + if (clientCaps["side-band-64k"]) queueLimit = 65519; + else if (clientCaps["size-band"]) queueLimit = 999; + repo.pack(hashes, opts, onPack); + } + + function flush(callback) { + if (!queueBytes) return callback(); + var chunk = bops.join(packQueue, queueBytes); + packQueue.length = 0; + queueBytes = 0; + remote.write(["pack", chunk], callback); + } + + function onPack(err, packStream) { + if (err) return callback(err); + onWrite(); + + function onRead(err, chunk) { + if (err) return callback(err); + if (chunk === undefined) return flush(onFlush); + if (!queueLimit) { + return remote.write(chunk, onWrite); + } + var length = chunk.length; + if (queueBytes + length <= queueLimit) { + packQueue.push(chunk); + queueBytes += length; + return onWrite(); + } + if (queueBytes) { + flush(function (err) { + if (err) return callback(err); + return onRead(null, chunk); + }); + } + remote.write(["pack", bops.subarray(chunk, 0, queueLimit)], function (err) { + if (err) return callback(err); + return onRead(null, bops.subarray(chunk, queueLimit)); + }); + } + function onWrite(err) { + if (err) return callback(err); + packStream.read(onRead); + } + } + + function onFlush(err) { + if (err) return callback(err); + if (queueLimit) remote.write(null, callback); + else callback(); + } + } function receivePack(remote, opts, callback) { @@ -31,18 +147,13 @@ function receivePack(remote, opts, callback) { if (changes.length) return repo.unpack(remote, opts, onUnpack); return callback(null, changes); } - var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) ([^ ]+)(?: (.+))?$/); + var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) ([^ ]+)(?: (.+))?\n?$/); changes.push({ oldHash: match[1], newHash: match[2], ref: match[3] }); - if (match[4]) { - match[4].split(" ").map(function (cap) { - var pair = cap.split("="); - clientCaps[pair[0]] = pair[1] || true; - }); - } + if (match[4]) clientCaps = parseCaps(match[4]); remote.read(onLine); } @@ -63,4 +174,62 @@ function receivePack(remote, opts, callback) { return repo.updateRef(change.ref, change.newHash, next); } } -} \ No newline at end of file +} + +function parseCaps(line) { + var caps = {}; + line.split(" ").map(function (cap) { + var pair = cap.split("="); + caps[pair[0]] = pair[1] || true; + }); + return caps; +} + +// Calculate a list of hashes to be included in a pack file based on have and want lists. +// +function walkRepo(repo, wants, haves, callback) { + var hashes = {}; + var done = false; + var left = 0; + + function onDone(err) { + if (done) return; + done = true; + return callback(err, Object.keys(hashes)); + } + + var keys = Object.keys(wants); + if (!keys.length) return onDone(); + keys.forEach(walkCommit); + + function walkCommit(hash) { + if (hash in hashes || hash in haves) return; + hashes[hash] = true; + left++; + repo.loadAs("commit", hash, function (err, commit) { + if (err) return onDone(err); + commit.parents.forEach(walkCommit); + walkTree(commit.tree); + if (!--left) return onDone(); + }); + } + + function walkTree(hash) { + if (hash in hashes || hash in haves) return; + hashes[hash] = true; + left++; + repo.loadAs("tree", hash, function (err, tree) { + if (err) return onDone(err); + Object.keys(tree).forEach(function (name) { + var item = tree[name]; + if (item.mode === 040000) walkTree(item.hash); + else { + if (item.hash in hashes || item.hash in haves) return; + hashes[item.hash] = true; + } + }); + if (!--left) return onDone(); + }); + } + +} From 55f1ce631a32a83ac936934273716033dbc7240f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 27 Nov 2013 22:14:54 -0600 Subject: [PATCH 068/256] Mix pack encoding into pack codec --- lib/{decode-pack.js => pack-codec.js} | 56 ++++++++++++++++++++------- 1 file changed, 42 insertions(+), 14 deletions(-) rename lib/{decode-pack.js => pack-codec.js} (81%) diff --git a/lib/decode-pack.js b/lib/pack-codec.js similarity index 81% rename from lib/decode-pack.js rename to lib/pack-codec.js index b97af55..8dad19a 100644 --- a/lib/decode-pack.js +++ b/lib/pack-codec.js @@ -1,18 +1,45 @@ - -var types = { - "1": "commit", - "2": "tree", - "3": "blob", - "4": "tag", - "6": "ofs-delta", - "7": "ref-delta" -}; - var inflate = require('./inflate.js'); +var deflate = require('./deflate.js'); var sha1 = require('./sha1.js'); -var bops = require('bops'); +var bops = { + subarray: require('bops/subarray.js'), + join: require('bops/join.js'), + from: require('bops/from.js'), +}; -module.exports = function (emit) { +var typeToNum = { + commit: 1, + tree: 2, + blob: 3, + tag: 4, + "ofs-delta": 6, + "ref-delta": 7 +}; +var numToType = {}; +for (var type in typeToNum) { + var num = typeToNum[type]; + numToType[num] = type; +} + +exports.packFrame = packFrame; +function packFrame(type, body, callback) { + var length = body.length; + var head = [(typeToNum[type] << 4) | (length & 0xf)]; + var i = 0; + length >>= 4; + while (length) { + head[i++] |= 0x80; + head[i] = length & 0x7f; + length >>= 7; + } + deflate(body, function (err, body) { + if (err) return callback(err); + callback(null, bops.join([bops.from(head), body])); + }); +} + +exports.decodePack = decodePack; +function decodePack(emit) { var state = $pack; var sha1sum = sha1(); @@ -147,7 +174,7 @@ module.exports = function (emit) { // Common helper for emitting all three object shapes function emitObject() { var item = { - type: types[type], + type: numToType[type], size: length, body: bops.join(parts), offset: start @@ -166,6 +193,7 @@ module.exports = function (emit) { function $body(byte, i, chunk) { if (inf.write(byte)) return $body; var buf = inf.flush(); + if (buf.length !== length) throw new Error("Length mismatch, expected " + length + " got " + buf.length); inf.recycle(); if (buf.length) { parts.push(buf); @@ -185,4 +213,4 @@ module.exports = function (emit) { if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum); } -}; +} From e7536012fd314530c204d0cd7b2393ec8b8072f3 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 27 Nov 2013 22:15:18 -0600 Subject: [PATCH 069/256] Update serve example and add pack visualizer --- examples/read-pack.js | 130 ++++++++++++++++++++++++++++++++++++++++++ examples/serve.js | 15 +++-- 2 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 examples/read-pack.js diff --git a/examples/read-pack.js b/examples/read-pack.js new file mode 100644 index 0000000..c8d61e5 --- /dev/null +++ b/examples/read-pack.js @@ -0,0 +1,130 @@ +var frame = require('../lib/frame.js'); +var sha1 = require('../lib/sha1.js'); +var decoders = require('../lib/decoders.js'); +var decodePack = require('../lib/pack-codec.js').decodePack; +var applyDelta = require('../lib/apply-delta.js'); +var inspect = require('util').inspect; + +var nodes = {}; +var links = []; +var items = {}; +var hashes = {}; +var left, num; + +var onItem = decodePack(function (item) { + if (left === undefined) { + left = num = item.num; + } + else if (item) { + left--; + console.error("%s/%s left", left, num); + } + else { + if (left) throw new Error(left + " items missing!"); + } + if (item && item.body) { + var hash = item.hash = sha1(frame(item.type, item.body)); + hashes[item.offset] = hash; + items[hash] = item; + + if (item.type === "ofs-delta") { + item.ref = hashes[item.offset - item.ref]; + item.type = "ref-delta"; + } + if (item.type === "ref-delta") { + var target = items[item.ref]; + item.type = target.type; + item.body = applyDelta(item.body, target.body); + delete items[hash]; + hash = item.hash = sha1(frame(item.type, item.body)); + hashes[item.offset] = hash; + items[hash] = item; + } + + var obj = item.obj = decoders[item.type](item.body); + + if (item.type === "commit") { + var label = []; + nodes[hash] = { + color: "deepskyblue4", + shape: "record", + label: label + }; + label.push(" " + shorten(hash)); + label.push(" " + obj.message.split("\n")[0].replace(/"/g, '')); + links.push([ + '"' + hash + '":commit', + '"' + obj.tree + '":hash', + ]); + obj.parents.forEach(function (parent) { + links.push([ + '"' + hash + '":hash', + '"' + parent + '":hash', + ]); + }); + } + else if (item.type === "tree") { + var label = [ + " " + shorten(hash), + ]; + Object.keys(obj).forEach(function (name, i) { + var key = "f" + i; + label.push("<" + key + "> " + name); + links.push([ + '"' + hash + '":' + key, + '"' + obj[name].hash + '":hash', + ]); + }); + nodes[hash] = { + color: "forestgreen", + shape: "record", + label: label + }; + } + else if (item.type === "blob") { + nodes[hash] = { + color: "firebrick4", + shape: "record", + label: [ + " " + shorten(hash), + item.body.length + " bytes data" + ] + }; + } + } + + if (item === undefined) printDot(); + console.error(inspect(item, {colors:true})); +}); +process.stdin.on('data', onItem); +process.stdin.on('end', onItem); +process.stdin.resume(); + +function printDot() { + var dot = []; + Object.keys(nodes).forEach(function (hash) { + var props = nodes[hash]; + dot.push('"' + hash + '" [\n' + Object.keys(props).map(function (name) { + var value = props[name]; + if (Array.isArray(value)) value = value.join("|"); + return ' ' + name + ' = "' + value + '"'; + }).join("\n") + '\n];'); + }); + links.forEach(function (pair) { + if (pair[2]) { + dot.push(pair[0] + ' -> ' + pair[1] + ' [label="' + pair[2] + '"];'); + } + else { + dot.push(pair[0] + ' -> ' + pair[1] + ';'); + } + }); + + dot.unshift('graph [rankdir = "LR" aspect=1];'); + dot.unshift('digraph packfile {'); + dot.push('}'); + console.log(dot.join("\n\n")); +} + +function shorten(hash) { + return hash.substr(0, 6) + "..." + hash.substr(hash.length - 6); +} \ No newline at end of file diff --git a/examples/serve.js b/examples/serve.js index 95d065d..0eca92d 100644 --- a/examples/serve.js +++ b/examples/serve.js @@ -67,18 +67,21 @@ function wrap(socket) { var rcb = null, wcb = null; var onChunk = pktLine.deframer(onFrame); var writeFrame = pktLine.framer(writeChunk); - socket.on("data", function (chunk) { + socket.on("data", onEvent); + socket.on("end", onEvent); + socket.on("drain", onDrain); + return { read: read, write: write }; + + function onEvent(chunk) { try { onChunk(chunk); } catch (err) { + console.error(err.stack); rerr = err; check(); } - }); - socket.on("end", onChunk); - socket.on("drain", onDrain); - return { read: read, write: write }; + } function onFrame(frame) { console.log("<-", inspect(frame, {colors:true})); @@ -183,6 +186,7 @@ function memDb() { }; function get(key, callback) { + console.log("GET", key); return makeAsync(function () { if (isHash.test(key)) { return objects[key]; @@ -192,6 +196,7 @@ function memDb() { } function set(key, value, callback) { + console.log("SET", key); return makeAsync(function () { if (isHash.test(key)) { objects[key] = value; From a23195a805ffa79fca66739d0f6495ecfd07f46e Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 27 Nov 2013 22:16:03 -0600 Subject: [PATCH 070/256] Fix require in client.js --- mixins/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/client.js b/mixins/client.js index 1f33a03..0910bd6 100644 --- a/mixins/client.js +++ b/mixins/client.js @@ -1,5 +1,5 @@ var pushToPull = require('push-to-pull'); -var parse = pushToPull(require('../lib/decode-pack.js')); +var parse = pushToPull(require('../lib/pack-codec.js').decodePack); var agent = require('../lib/agent.js'); module.exports = function (repo) { From dbbedd59d1d026ad84cc0d068279e14a31ef15b0 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 27 Nov 2013 22:16:46 -0600 Subject: [PATCH 071/256] Fix unpack --- mixins/packops.js | 213 ++-------------------------------------------- 1 file changed, 6 insertions(+), 207 deletions(-) diff --git a/mixins/packops.js b/mixins/packops.js index cfc2770..e49b1c3 100644 --- a/mixins/packops.js +++ b/mixins/packops.js @@ -2,24 +2,10 @@ var bops = require('bops'); var deframe = require('../lib/deframe.js'); var frame = require('../lib/frame.js'); var sha1 = require('../lib/sha1.js'); -var inflate = require('../lib/inflate.js'); -var deflate = require('../lib/deflate.js'); var applyDelta = require('../lib/apply-delta.js'); var pushToPull = require('push-to-pull'); - -var typeToNum = { - commit: 1, - tree: 2, - blob: 3, - tag: 4, - "ofs-delta": 5, - "ref-delta": 6 -}; -var numToType = {}; -for (var type in typeToNum) { - var num = typeToNum[type]; - numToType[num] = type; -} +var decodePack = require('../lib/pack-codec.js').decodePack; +var packFrame = require('../lib/pack-codec.js').packFrame; module.exports = function (repo) { // packStream is a simple-stream containing raw packfile binary data @@ -34,6 +20,7 @@ module.exports = function (repo) { function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); + packStream = pushToPull(decodePack)(packStream); var repo = this; @@ -99,6 +86,7 @@ function unpack(packStream, opts, callback) { if (opts.onProgress) deltaProgress(); return repo.loadRaw(item.ref, function (err, buffer) { if (err) return onDone(err); + if (!buffer) return onDone(new Error("Missing base image at " + item.ref)); var target = deframe(buffer); item.type = target[0]; item.body = applyDelta(item.body, target[1]); @@ -120,7 +108,8 @@ function unpack(packStream, opts, callback) { function saveValue(item) { var buffer = frame(item.type, item.body); - var hash = hashes[item.offset] = sha1(buffer); + var hash = sha1(buffer); + hashes[item.offset] = hash; has[hash] = true; if (hash in pending) { // I have yet to come across a pack stream that actually needs this. @@ -148,22 +137,6 @@ function unpack(packStream, opts, callback) { } -function packFrame(type, body, callback) { - var length = body.length; - var head = [(typeToNum[type] << 4) | (length & 0xf)]; - var i = 0; - length >>= 4; - while (length) { - head[i++] |= 0x80; - head[i] = length & 0x7f; - length >>= 7; - } - deflate(body, function (err, body) { - if (err) return callback(err); - callback(null, bops.join([bops.from(head), body])); - }); -} - // TODO: Implement delta refs to reduce stream size function pack(hashes, opts, callback) { if (!callback) return pack.bind(this, hashes, opts); @@ -220,177 +193,3 @@ function values(object) { return out; } -function decodePack(emit) { - - var state = $pack; - var sha1sum = sha1(); - var inf = inflate(); - - var offset = 0; - var position = 0; - var version = 0x4b434150; // PACK reversed - var num = 0; - var type = 0; - var length = 0; - var ref = null; - var checksum = ""; - var start = 0; - var parts = []; - - - return function (chunk) { - if (chunk === undefined) { - if (num || checksum.length < 40) throw new Error("Unexpected end of input stream"); - return emit(); - } - - for (var i = 0, l = chunk.length; i < l; i++) { - // console.log([state, i, chunk[i].toString(16)]); - if (!state) throw new Error("Unexpected extra bytes: " + bops.subarray(chunk, i)); - state = state(chunk[i], i, chunk); - position++; - } - if (!state) return; - if (state !== $checksum) sha1sum.update(chunk); - var buff = inf.flush(); - if (buff.length) { - parts.push(buff); - } - }; - - // The first four bytes in a packfile are the bytes 'PACK' - function $pack(byte) { - if ((version & 0xff) === byte) { - version >>>= 8; - return version ? $pack : $version; - } - throw new Error("Invalid packfile header"); - } - - // The version is stored as an unsigned 32 integer in network byte order. - // It must be version 2 or 3. - function $version(byte) { - version = (version << 8) | byte; - if (++offset < 4) return $version; - if (version >= 2 && version <= 3) { - offset = 0; - return $num; - } - throw new Error("Invalid version number " + num); - } - - // The number of objects in this packfile is also stored as an unsigned 32 bit int. - function $num(byte) { - num = (num << 8) | byte; - if (++offset < 4) return $num; - offset = 0; - emit({version: version, num: num}); - return $header; - } - - // n-byte type and length (3-bit type, (n-1)*7+4-bit length) - // CTTTSSSS - // C is continue bit, TTT is type, S+ is length - function $header(byte) { - if (start === 0) start = position; - type = byte >> 4 & 0x07; - length = byte & 0x0f; - if (byte & 0x80) { - offset = 4; - return $header2; - } - return afterHeader(); - } - - // Second state in the same header parsing. - // CSSSSSSS* - function $header2(byte) { - length |= (byte & 0x7f) << offset; - if (byte & 0x80) { - offset += 7; - return $header2; - } - return afterHeader(); - } - - // Common helper for finishing tiny and normal headers. - function afterHeader() { - offset = 0; - if (type === 6) { - ref = 0; - return $ofsDelta; - } - if (type === 7) { - ref = ""; - return $refDelta; - } - return $body; - } - - // Big-endian modified base 128 number encoded ref offset - function $ofsDelta(byte) { - ref = byte & 0x7f; - if (byte & 0x80) return $ofsDelta2; - return $body; - } - - function $ofsDelta2(byte) { - ref = ((ref + 1) << 7) | (byte & 0x7f); - if (byte & 0x80) return $ofsDelta2; - return $body; - } - - // 20 byte raw sha1 hash for ref - function $refDelta(byte) { - ref += toHex(byte); - if (++offset < 20) return $refDelta; - return $body; - } - - // Common helper for generating 2-character hex numbers - function toHex(num) { - return num < 0x10 ? "0" + num.toString(16) : num.toString(16); - } - - // Common helper for emitting all three object shapes - function emitObject() { - var item = { - type: numToType[type], - size: length, - body: bops.join(parts), - offset: start - }; - if (ref) item.ref = ref; - parts.length = 0; - start = 0; - offset = 0; - type = 0; - length = 0; - ref = null; - emit(item); - } - - // Feed the deflated code to the inflate engine - function $body(byte, i, chunk) { - if (inf.write(byte)) return $body; - var buf = inf.flush(); - inf.recycle(); - if (buf.length) { - parts.push(buf); - } - emitObject(); - // If this was all the objects, start calculating the sha1sum - if (--num) return $header; - sha1sum.update(bops.subarray(chunk, 0, i + 1)); - return $checksum; - } - - // 20 byte checksum - function $checksum(byte) { - checksum += toHex(byte); - if (++offset < 20) return $checksum; - var actual = sha1sum.digest(); - if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum); - } - -} From 16be80836b41d3e191a861033056a9c06d4b9d24 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 27 Nov 2013 22:17:05 -0600 Subject: [PATCH 072/256] Improve error handling in server code --- mixins/server.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mixins/server.js b/mixins/server.js index 0ecb780..950fcb8 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -203,11 +203,14 @@ function walkRepo(repo, wants, haves, callback) { keys.forEach(walkCommit); function walkCommit(hash) { + if (done) return; if (hash in hashes || hash in haves) return; hashes[hash] = true; left++; repo.loadAs("commit", hash, function (err, commit) { + if (done) return; if (err) return onDone(err); + if (!commit) return onDone(new Error("Missing Commit: " + hash)); commit.parents.forEach(walkCommit); walkTree(commit.tree); if (!--left) return onDone(); @@ -215,12 +218,16 @@ function walkRepo(repo, wants, haves, callback) { } function walkTree(hash) { + if (done) return; if (hash in hashes || hash in haves) return; hashes[hash] = true; left++; repo.loadAs("tree", hash, function (err, tree) { + if (done) return; if (err) return onDone(err); + if (tree === undefined) return onDone(new Error("Missing tree: " + hash)); Object.keys(tree).forEach(function (name) { + if (done) return; var item = tree[name]; if (item.mode === 040000) walkTree(item.hash); else { @@ -231,5 +238,4 @@ function walkRepo(repo, wants, haves, callback) { if (!--left) return onDone(); }); } - } From ff93dbe00d4dfeed5b601a16b81f0ce3a8d00715 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 27 Nov 2013 22:18:06 -0600 Subject: [PATCH 073/256] Add path for loading missing objects --- mixins/objects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/objects.js b/mixins/objects.js index 10d166e..e2c545f 100644 --- a/mixins/objects.js +++ b/mixins/objects.js @@ -41,7 +41,7 @@ function load(hashish, callback) { } function onBuffer(err, buffer) { - if (err) return callback(err); + if (buffer === undefined) return callback(err); var type, object; try { if (sha1(buffer) !== hash) { From 4f06a7e31f2ebc6fb4abfc6255290c6883916fe0 Mon Sep 17 00:00:00 2001 From: Aria Stewart Date: Fri, 6 Dec 2013 22:12:27 -0500 Subject: [PATCH 074/256] Change a 'save' to a 'load' Since save makes no sense, this is correct, right? --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 284ba52..3d4cb3e 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Save an object to the database. This will give you back the hash of the cotent ### repo.loadAs(type, hash, [callback]) -> body -This convenience wrapper will call `repo.save` for you and then check if the type is what you expected. If it is, it will return the body directly. If it's not, it will error. +This convenience wrapper will call `repo.load` for you and then check if the type is what you expected. If it is, it will return the body directly. If it's not, it will error. ```js var commit = yield repo.loadAs("commit", "HEAD"); From 31e314371aae333034dd11e50886ff4aea202438 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 11 Dec 2013 22:16:50 +0100 Subject: [PATCH 075/256] Implement proper peeled refs in server ref listing --- examples/create.js | 15 ++++++++-- mixins/server.js | 70 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/examples/create.js b/examples/create.js index 7a52716..81b6a7d 100644 --- a/examples/create.js +++ b/examples/create.js @@ -55,9 +55,20 @@ function create(repo, callback) { }); }); }); - }, callback); + }, function (err) { + if (err) return callback(err); + repo.saveAs("tag", { + object: parent, + type: "commit", + tag: "v0.1.0", + tagger: mock.author, + message: "Details about the v0.1.0 release go here" + }, function (err, hash) { + if (err) return callback(err); + repo.createRef("refs/tags/v0.1.0", hash, callback); + }); + }); }); - } // Mini control-flow library diff --git a/mixins/server.js b/mixins/server.js index 950fcb8..2b3c4e5 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -1,3 +1,24 @@ +// Simple helper for parallel work. +function makeGroup(callback) { + var done = false; + var left = 0; + var results = {}; + return function (name) { + left++; + return function (err, result) { + if (done) return; + if (err) { + done = true; + return callback(err); + } + results[name] = result; + if (--left) return; + done = true; + return callback(null, results); + }; + }; +} + var bops = { join: require('bops/join.js') }; @@ -10,6 +31,7 @@ module.exports = function (repo) { function uploadPack(remote, opts, callback) { if (!callback) return uploadPack.bind(this, remote, opts); var repo = this, refs, wants = {}, haves = {}, clientCaps = {}; + var head; var packQueue = []; var queueBytes = 0; var queueLimit = 0; @@ -21,17 +43,57 @@ function uploadPack(remote, opts, callback) { repo.getHead(onHead); } - function onHead(err, head) { + function onHead(err, result) { if (err) return callback(err); + head = result; + + // The peeled value of a ref (that is "ref^{}") MUST be immediately after + // the ref itself, if presented. A conforming server MUST peel the ref if + // it’s an annotated tag. + var gen = makeGroup(onValues); + for (var ref in refs) { + repo.load(refs[ref], gen(ref)); + } + } + + function onValues(err, values) { + if (err) return callback(err); + // Insert peeled refs. + for (var ref in values) { + var value = values[ref]; + if (value.type === "tag") { + refs[ref+"^{}"] = value.body.object; + } + } + + // The returned response is a pkt-line stream describing each ref and its + // current value. The stream MUST be sorted by name according to the C + // locale ordering. + var keys = Object.keys(refs).sort(); + var lines = keys.map(function (ref) { + return refs[ref] + " " + ref; + }); + + // If HEAD is a valid ref, HEAD MUST appear as the first advertised ref. + // If HEAD is not a valid ref, HEAD MUST NOT appear in the advertisement + // list at all, but other refs may still appear. + if (head) lines.unshift(refs[head] + " HEAD"); + + // The stream MUST include capability declarations behind a NUL on the + // first ref. // TODO: add "multi_ack" once it's implemented // TODO: add "multi_ack_detailed" once it's implemented // TODO: add "shallow" once it's implemented // TODO: add "include-tag" once it's implemented // TODO: add "thin-pack" once it's implemented - remote.write(refs[head] + " HEAD\0no-progress side-band side-band-64k ofs-delta", null); - Object.keys(refs).forEach(function (ref) { - remote.write(refs[ref] + " " + ref, null); + lines[0] += "\0no-progress side-band side-band-64k ofs-delta"; + + // Server SHOULD terminate each non-flush line using LF ("\n") terminator; + // client MUST NOT complain if there is no terminator. + lines.forEach(function (line) { + remote.write(line, null); }); + remote.write(null, null); remote.read(onWant); } From 70f65fe216ff845b30226a88e660b0da530ebb39 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 11 Dec 2013 22:54:46 +0100 Subject: [PATCH 076/256] Use better control-flow libraries --- lib/each.js | 11 +++++++ lib/map.js | 14 +++++++++ lib/parallel.js | 45 +++++++++++++++++++++++++++++ lib/serial.js | 38 +++++++++++++++++++++++++ mixins/server.js | 74 ++++++++++++++++++++---------------------------- 5 files changed, 138 insertions(+), 44 deletions(-) create mode 100644 lib/each.js create mode 100644 lib/map.js create mode 100644 lib/parallel.js create mode 100644 lib/serial.js diff --git a/lib/each.js b/lib/each.js new file mode 100644 index 0000000..94a3db3 --- /dev/null +++ b/lib/each.js @@ -0,0 +1,11 @@ +module.exports = each; + +// A functional forEach that works on both arrays and objects +function each(obj, fn) { + if (Array.isArray(obj)) return obj.forEach(fn); + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + fn(obj[key], key, obj); + } +} diff --git a/lib/map.js b/lib/map.js new file mode 100644 index 0000000..0a1c903 --- /dev/null +++ b/lib/map.js @@ -0,0 +1,14 @@ +module.exports = map; + +// A functional map that works on both arrays and objects +// The returned object has the same shape as the original, but values mapped. +function map(obj, fn) { + if (Array.isArray(obj)) return obj.map(fn); + var result = {}; + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + result[key] = fn(obj[key], key, obj); + } + return result; +} diff --git a/lib/parallel.js b/lib/parallel.js new file mode 100644 index 0000000..f9de195 --- /dev/null +++ b/lib/parallel.js @@ -0,0 +1,45 @@ +module.exports = parallel; + +// Run several continuables in parallel. The results are stored in the same +// shape as the input continuables (array or object). +// Returns a new continuable or accepts a callback. +// This will bail on the first error and ignore all others after it. +function parallel(commands, callback) { + if (!callback) return parallel.bind(this, commands); + var results, length, left, i, done; + + // Handle array shapes + if (Array.isArray(commands)) { + length = commands.length; + left = results = new Array(left); + for (i = 0; i < length; i++) { + run(i, commands[i]); + } + } + + // Otherwise assume it's an object. + else { + var keys = Object.keys(commands); + left = length = keys.length; + results = {}; + for (i = 0; i < length; i++) { + var key = keys[i]; + run(key, commands[key]); + } + } + + // Common logic for both + function run(key, command) { + command(function (err, result) { + if (done) return; + if (err) { + done = true; + return callback(err); + } + results[key] = result; + if (--left) return; + done = true; + callback(null, results); + }); + } +} \ No newline at end of file diff --git a/lib/serial.js b/lib/serial.js new file mode 100644 index 0000000..390b97a --- /dev/null +++ b/lib/serial.js @@ -0,0 +1,38 @@ +module.exports = serial; + +// Run several continuables in serial. The results are stored in the same +// shape as the input continuables (array or object). +// Returns a new continuable or accepts a callback. +// This will bail on the first error. +function serial(commands, callback) { + if (!callback) return serial.bind(this, commands); + var results, keys, index = 0, length, key; + + if (Array.isArray(commands)) { + length = commands.length; + results = new Array(length); + } + else { + results = {}; + keys = Object.keys(commands); + length = keys.length; + } + + index = 0; + return runNext(); + + function runNext() { + if (index >= length) { + return callback(null, results); + } + key = keys ? keys[index] : index; + var command = commands[key]; + command(onResult); + } + + function onResult(err, result) { + if (err) return callback(err); + results[key] = result; + runNext(); + } +} \ No newline at end of file diff --git a/mixins/server.js b/mixins/server.js index 2b3c4e5..288fbab 100644 --- a/mixins/server.js +++ b/mixins/server.js @@ -1,23 +1,6 @@ -// Simple helper for parallel work. -function makeGroup(callback) { - var done = false; - var left = 0; - var results = {}; - return function (name) { - left++; - return function (err, result) { - if (done) return; - if (err) { - done = true; - return callback(err); - } - results[name] = result; - if (--left) return; - done = true; - return callback(null, results); - }; - }; -} +var parallel = require('../lib/parallel.js'); +var map = require('../lib/map.js'); +var each = require('../lib/each.js'); var bops = { join: require('bops/join.js') @@ -31,40 +14,43 @@ module.exports = function (repo) { function uploadPack(remote, opts, callback) { if (!callback) return uploadPack.bind(this, remote, opts); var repo = this, refs, wants = {}, haves = {}, clientCaps = {}; - var head; var packQueue = []; var queueBytes = 0; var queueLimit = 0; - repo.listRefs(null, onRefs); + return parallel({ + head: repo.getHead(), + refs: getRefs() + }, onHeadRef); - function onRefs(err, result) { - if (err) return callback(err); - refs = result; - repo.getHead(onHead); - } + // The peeled value of a ref (that is "ref^{}") MUST be immediately after + // the ref itself, if presented. A conforming server MUST peel the ref if + // it’s an annotated tag. + function getRefs(callback) { + if (!callback) return getRefs; + var refs; + repo.listRefs(null, onRefs); - function onHead(err, result) { - if (err) return callback(err); - head = result; + function onRefs(err, result) { + if (err) return callback(err); + refs = result; + parallel(map(refs, function (hash) { + return repo.load(hash); + }), onValues); + } - // The peeled value of a ref (that is "ref^{}") MUST be immediately after - // the ref itself, if presented. A conforming server MUST peel the ref if - // it’s an annotated tag. - var gen = makeGroup(onValues); - for (var ref in refs) { - repo.load(refs[ref], gen(ref)); + function onValues(err, values) { + each(values, function (value, name) { + if (value.type !== "tag") return; + refs[name + "^{}"] = value.body.object; + }); + callback(null, refs); } } - function onValues(err, values) { + function onHeadRef(err, result) { if (err) return callback(err); - // Insert peeled refs. - for (var ref in values) { - var value = values[ref]; - if (value.type === "tag") { - refs[ref+"^{}"] = value.body.object; - } - } + var head = result.head; + refs = result.refs; // The returned response is a pkt-line stream describing each ref and its // current value. The stream MUST be sorted by name according to the C From 30a3468c1c4710ff65038a16efafcd76ee909ecd Mon Sep 17 00:00:00 2001 From: Peter Jenkins Date: Fri, 27 Dec 2013 18:39:43 -0500 Subject: [PATCH 077/256] Fix a typo. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d4cb3e..3215ae7 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Remove an object or ref from the database. Given a path prefix, give all the keys. This is like a readdir if you treat the keys as paths. -For example, given the keys `refs/heads/master`, `refs/headers/experimental`, `refs/tags/0.1.3` and the prefix `refs/heads/`, the output would be `master` and `experimental`. +For example, given the keys `refs/heads/master`, `refs/heads/experimental`, `refs/tags/0.1.3` and the prefix `refs/heads/`, the output would be `master` and `experimental`. A null prefix returns all non hash keys. From 06a1500a9f2b917e7e553cd66d2b37448946d410 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 2 Jan 2014 23:03:39 -0600 Subject: [PATCH 078/256] Fix array mode for parallel helper --- lib/parallel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/parallel.js b/lib/parallel.js index f9de195..adeb739 100644 --- a/lib/parallel.js +++ b/lib/parallel.js @@ -10,8 +10,8 @@ function parallel(commands, callback) { // Handle array shapes if (Array.isArray(commands)) { - length = commands.length; - left = results = new Array(left); + left = length = commands.length; + results = new Array(left); for (i = 0; i < length; i++) { run(i, commands[i]); } @@ -42,4 +42,4 @@ function parallel(commands, callback) { callback(null, results); }); } -} \ No newline at end of file +} From ae5b8f244fb5245fc3670a65ebb542130de5d9af Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 9 Jan 2014 00:18:34 +0000 Subject: [PATCH 079/256] Bump version to 0.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2e1bd7..303ce91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.6.1", + "version": "0.6.2", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { From 43503204e246c52fe8fab686386e0e67f94f10e8 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 8 Feb 2014 15:27:29 -0600 Subject: [PATCH 080/256] Clean up to get ready for code import --- LICENSE | 2 +- README.md | 220 +--------- examples/clone.js | 31 -- examples/create-harmony.js | 45 -- examples/create.js | 103 ----- examples/ls-remote.js | 13 - examples/mock.js | 18 - examples/pkt-line.js | 152 ------- examples/read-harmony.js | 31 -- examples/read-pack.js | 130 ------ examples/read.js | 39 -- examples/serve.js | 246 ----------- examples/walk.js | 50 --- js-git.js | 34 -- lib/agent.js | 2 - lib/apply-delta.js | 94 ---- lib/assert-type.js | 5 - lib/decoders.js | 104 ----- lib/deflate.js | 5 - lib/deframe.js | 18 - lib/each.js | 11 - lib/encoders.js | 76 ---- lib/frame.js | 8 - lib/indexof.js | 8 - lib/inflate.js | 853 ------------------------------------- lib/ishash.js | 3 - lib/map.js | 14 - lib/pack-codec.js | 216 ---------- lib/parallel.js | 45 -- lib/parseascii.js | 7 - lib/parsedec.js | 7 - lib/parseoct.js | 7 - lib/parsetohex.js | 10 - lib/pathcmp.js | 6 - lib/serial.js | 38 -- lib/sha1.js | 146 ------- lib/trace.js | 1 - lib/tracedb.js | 52 --- lib/walk.js | 42 -- mixins/client.js | 180 -------- mixins/clone.js | 3 - mixins/objects.js | 126 ------ mixins/packops.js | 195 --------- mixins/refs.js | 153 ------- mixins/server.js | 289 ------------- mixins/walkers.js | 88 ---- package.json | 6 +- specs/high/db.md | 0 specs/high/fs.md | 0 specs/high/index.md | 0 specs/high/proto.md | 0 specs/high/trace.md | 0 specs/low/bops.md | 0 specs/low/continuable.md | 0 specs/low/deflate.md | 0 specs/low/http.md | 0 specs/low/inflate.md | 0 specs/low/sha1.md | 0 specs/low/simple-stream.md | 0 specs/low/ssh.md | 0 specs/low/tcp.md | 0 test/test-sha1.js | 51 --- 62 files changed, 4 insertions(+), 3979 deletions(-) delete mode 100644 examples/clone.js delete mode 100644 examples/create-harmony.js delete mode 100644 examples/create.js delete mode 100644 examples/ls-remote.js delete mode 100644 examples/mock.js delete mode 100644 examples/pkt-line.js delete mode 100644 examples/read-harmony.js delete mode 100644 examples/read-pack.js delete mode 100644 examples/read.js delete mode 100644 examples/serve.js delete mode 100644 examples/walk.js delete mode 100644 js-git.js delete mode 100644 lib/agent.js delete mode 100644 lib/apply-delta.js delete mode 100644 lib/assert-type.js delete mode 100644 lib/decoders.js delete mode 100644 lib/deflate.js delete mode 100644 lib/deframe.js delete mode 100644 lib/each.js delete mode 100644 lib/encoders.js delete mode 100644 lib/frame.js delete mode 100644 lib/indexof.js delete mode 100644 lib/inflate.js delete mode 100644 lib/ishash.js delete mode 100644 lib/map.js delete mode 100644 lib/pack-codec.js delete mode 100644 lib/parallel.js delete mode 100644 lib/parseascii.js delete mode 100644 lib/parsedec.js delete mode 100644 lib/parseoct.js delete mode 100644 lib/parsetohex.js delete mode 100644 lib/pathcmp.js delete mode 100644 lib/serial.js delete mode 100644 lib/sha1.js delete mode 100644 lib/trace.js delete mode 100644 lib/tracedb.js delete mode 100644 lib/walk.js delete mode 100644 mixins/client.js delete mode 100644 mixins/clone.js delete mode 100644 mixins/objects.js delete mode 100644 mixins/packops.js delete mode 100644 mixins/refs.js delete mode 100644 mixins/server.js delete mode 100644 mixins/walkers.js delete mode 100644 specs/high/db.md delete mode 100644 specs/high/fs.md delete mode 100644 specs/high/index.md delete mode 100644 specs/high/proto.md delete mode 100644 specs/high/trace.md delete mode 100644 specs/low/bops.md delete mode 100644 specs/low/continuable.md delete mode 100644 specs/low/deflate.md delete mode 100644 specs/low/http.md delete mode 100644 specs/low/inflate.md delete mode 100644 specs/low/sha1.md delete mode 100644 specs/low/simple-stream.md delete mode 100644 specs/low/ssh.md delete mode 100644 specs/low/tcp.md delete mode 100644 test/test-sha1.js diff --git a/LICENSE b/LICENSE index c968f88..a6e8431 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Tim Caswell +Copyright (c) 2013-2014 Tim Caswell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3215ae7..ebf3fdc 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,3 @@ -js-git -====== +# UNDER CONSTRUCTION -Git Implemented in JavaScript. - -This project is very modular and configurable by gluing different components together. - -This repo, `js-git`, is the core implementation of git and consumes various instances of interfaces. This means that your network and persistance stack is completely pluggable. - -If you're looking for a more pre-packaged system, consider packages like `creationix/git-node` that implement all the abstract interfaces using node.js native APIs. The `creationix/jsgit` package is an example of a CLI tool that consumes this. - -The main end-user API as exported by this module for working with local repositories is: - -## Initialize the library - -First you create an instance of the library by injecting the platform dependencies. - -```js -var platform = require('git-node-platform'); -var jsGit = require('js-git')(platform); -``` - -## Wrap a Database - -Then you implement the database interface (or more likely use a library to create it for you). - -```js -var fsDb = require('git-fs-db')(platform); -var db = fsDb("/path/to/repo.git"); -``` - -The database interface is documented later on. - -## Continuables - -In all public async functions you can either pass in a node-style callback last or omit the callback and it will return you a continuable. - -This means you can consume the js-git library using normal ES3 code or if you prefer use [gen-run][] and consume the continuables. - -If the callback is omitted, a continuable is returned. You must pass a callback into this continuable to actually start the action. - -```js -// Callback mode -jsgit.someAction(arg1, arg2, function (err, result) { - ... -}); - -// Continuable mode -var cont = jsgit.someAction(arg1, arg2); -cont(function (err, result) { - ... -}); - -// Continuable mode with gen-run -var result = yield jsgit.someAction(arg1, arg2); -``` - -### db.get(key, [callback]) -> value - -Load a ref or object from the database. - -The database should assume that keys that are 40-character long hex strings are sha1 hashes. The value for these will always be binary (`Buffer` in node, `Uint8Array` in browser) -All other keys are paths like `refs/heads/master` or `HEAD` and the value is a string. - - -### db.set(key, value, [callback]) - -Save a value to the database. Same rules apply about hash keys being binary values and other keys being string values. - -### db.has(key, [callback]) -> hasKey? - -Check if a key is in the database - -### db.del(key, [callback]) - -Remove an object or ref from the database. - -### db.keys(prefix, [callback]) -> keys - -Given a path prefix, give all the keys. This is like a readdir if you treat the keys as paths. - -For example, given the keys `refs/heads/master`, `refs/heads/experimental`, `refs/tags/0.1.3` and the prefix `refs/heads/`, the output would be `master` and `experimental`. - -A null prefix returns all non hash keys. - -### db.init([callback]) - -Initialize a database. This is where you db implementation can setup stuff. - -### db.clear([callback]) - -This is for when the user wants to delete or otherwise reclaim your database's resources. - - -### Wrapping the DataBase - -Now that you have a database instance, you can use the jsGit library created above. - -```js -var repo = jsGit(db); -``` - -### repo.load(hash(ish), [callback]) -> git object - -Load a git object from the database. You can pass in either a hash or a symbolic name like `HEAD` or `refs/tags/v3.1.4`. - -The object will be of the form: - -```js -{ - type: "commit", // Or "tag", "tree", or "blob" - body: { ... } // Or an array for tree and a binary value for blob. -} -``` - -### repo.save(object, [callback]) -> hash - -Save an object to the database. This will give you back the hash of the cotent by which you can retrieve the value back. - -### repo.loadAs(type, hash, [callback]) -> body - -This convenience wrapper will call `repo.load` for you and then check if the type is what you expected. If it is, it will return the body directly. If it's not, it will error. - -```js -var commit = yield repo.loadAs("commit", "HEAD"); -var tree = yield repo.loadAs("tree", commit.tree); -``` - -I'm using yield syntax because it's simpler, you can use callbacks instead if you prefer. - -### repo.saveAs(type, body, [callback]) -> hash - -Another convenience wrapper, this time to save objects as a specefic type. The body must be in the right format. - -```js -var blobHash = yield repo.saveAs("blob", binaryData); -var treeHash = yield repo.saveAs("tree", [ - { mode: 0100644, name: "file.dat", hash: blobHash } -]); -var commitHash = yield repo.saveAs("commit", { - tree: treeHash, - author: { name: "Tim Caswell", email: "tim@creationix.com", date: new Date }, - message: "Save the blob" -}); -``` - -### repo.remove(hash, [callback]) - -Remove an object. - -### repo.unpack(packFileStream, opts, [callback]) - -Import a packfile stream (simple-stream format) into the current database. This is used mostly for clone and fetch operations where the stream comes from a remote repo. - -`opts` is a hash of optional configs. - - - `opts.onProgress(progress)` - listen to the git progress channel by passing in a event listener. - - `opts.onError(error)` - same thing, but for the error channel. - - `opts.deline` - If this is truthy, the progress and error messages will be rechunked to be whole lines. They usually come jumbled in the internal sidechannel. - -### repo.logWalk(hash(ish), [callback]) -> log stream - -This convenience wrapper creates a readable stream of the history sorted by author date. - -If you want full history, pass in `HEAD` for the hash. - -### repo.treeWalk(hash(ish), [callback]) -> file stream - -This helper will return a stream of files suitable for traversing a file tree as a linear stream. The hash can be a ref to a commit, a commit hash or a tree hash directly. - -### repo.walk(seed, scan, loadKey, compare) -> stream - -This is the generic helper that `logWalk` and `treeWalk` use. See `js-git.js` source for usage. - -### repo.resolveHashish(hashish, [callback]) -> hash - -Resolve a ref, branch, or tag to a real hash. - -### repo.updateHead(hash, [callback]) - -Update whatever branch `HEAD` is pointing to so that it points to `hash`. - -You'll usually want to do this after creating a new commint in the HEAD branch. - -### repo.getHead([callback]) -> ref name - -Read the current active branch. - -### repo.setHead(ref, [callback]) - -Set the current active branch. - -### repo.fetch(remote, opts, [callback]) - -Convenience wrapper that fetches from a remote instance and calls `repo.unpack` with the resulting packfile stream for you. - -## Related Packages - -Being that js-git is so modular, here is a list of the most relevent modules that work with js-git: - - - - A generic remote protocol implementation that wraps the platform interfaces and consumes urls. - - Example Applications - - - A multi-platform GUI program that clones and browses git repos. - - - An example of using js-git in node. This is a CLI tool. - - - A packaged version of js-git made for node.js - - Platform Helpers - - - A git-http platform interface adapter that wraps git-tcp platform instances. - - - Just the platform interface for using js-git on node.js. - - - A pure-js implementation of the sha1 part of the platform interface. - - - An implementation of js-git platform for browsers. - - - An implementation of the git-tcp interface that consumes a websocket to tcp proxy server. - - - A pure-js implementation of the zlib parts of the platform interface. - - Storage Backends - - - A database interface adapter that wraps a fs interface. - - - A git-db implementation based on `localStorage`. - - - A git-db implementation that stores data in ram for quick testing. - - - A git-db implementation cased on `indexedDB`. - -[gen-run]: https://github.com/creationix/gen-run +Old code is in [legacy branch](https://github.com/creationix/js-git/tree/legacy) while I integrate the new stuff. diff --git a/examples/clone.js b/examples/clone.js deleted file mode 100644 index 1402d3e..0000000 --- a/examples/clone.js +++ /dev/null @@ -1,31 +0,0 @@ -var platform = require('git-node-platform'); -var jsGit = require('../.'); -var gitRemote = require('git-net')(platform); -var fsDb = require('git-fs-db')(platform); -var fs = platform.fs; -var basename = require('path').basename; - -// Create a remote repo -var url = process.argv[2] || "git://github.com/creationix/conquest.git"; -var remote = gitRemote(url); - -// Create a local repo -var path = process.argv[3] || basename(remote.pathname); -var repo = jsGit(fsDb(fs(path))); - -console.log("Cloning %s to %s", url, path); - -var opts = { - onProgress: function (progress) { - process.stderr.write(progress); - } -}; -if (process.env.DEPTH) { - opts.depth = parseInt(process.env.DEPTH, 10); -} - -repo.fetchPack(remote, opts, function (err) { - if (err) throw err; - console.log("Done"); -}); - diff --git a/examples/create-harmony.js b/examples/create-harmony.js deleted file mode 100644 index 29d821d..0000000 --- a/examples/create-harmony.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -let platform = require('git-node-platform'); -let jsGit = require('../.'); -let fsDb = require('git-fs-db')(platform); -let fs = platform.fs; -let run = require('gen-run'); - -// Create a filesystem backed bare repo -let repo = jsGit(fsDb(fs("test.git"))); - -let mock = require('./mock.js'); - -run(function *() { - yield repo.setHead("master"); - console.log("Git database Initialized"); - - let head; - console.log(yield* map(mock.commits, function* (files, message) { - return head = yield repo.saveAs("commit", { - tree: yield repo.saveAs("tree", yield* map(files, function* (contents) { - return { - mode: 33188, // 0o100644, - hash: yield repo.saveAs("blob", contents) - }; - })), - parent: head, - author: mock.author, - committer: mock.committer, - message: message - }); - })); - - yield repo.updateHead(head); - console.log("Done"); - -}); - -function* map(object, onItem) { - let obj = {}; - for (let key in object) { - let value = object[key]; - obj[key] = yield* onItem(value, key); - } - return obj; -} diff --git a/examples/create.js b/examples/create.js deleted file mode 100644 index 81b6a7d..0000000 --- a/examples/create.js +++ /dev/null @@ -1,103 +0,0 @@ -var platform = require('git-node-platform'); -var jsGit = require('../.'); -var fsDb = require('git-fs-db')(platform); -var fs = platform.fs; - -if (!module.parent) { - // Create a filesystem backed bare repo - var repo = jsGit(fsDb(fs("test.git"))); - create(repo, function (err) { - if (err) throw err; - }); -} -else { - module.exports = create; -} - - -function create(repo, callback) { - - var mock = require('./mock.js'); - - repo.setHead("master", function (err) { - if (err) return callback(err); - console.log("Git database Initialized"); - - var parent; - serialEach(mock.commits, function (message, files, next) { - // Start building a tree object. - var tree = {}; - parallelEach(files, function (name, contents, next) { - repo.saveAs("blob", contents, function (err, hash) { - if (err) return next(err); - tree[name] = { - mode: 0100644, - hash: hash - }; - next(); - }); - }, function (err) { - if (err) return next(err); - repo.saveAs("tree", tree, function (err, hash) { - if (err) return next(err); - var commit = { - tree: hash, - parent: parent, - author: mock.author, - committer: mock.committer, - message: message - }; - if (!parent) delete commit.parent; - repo.saveAs("commit", commit, function (err, hash) { - if (err) return next(err); - parent = hash; - repo.updateHead(hash, next); - }); - }); - }); - }, function (err) { - if (err) return callback(err); - repo.saveAs("tag", { - object: parent, - type: "commit", - tag: "v0.1.0", - tagger: mock.author, - message: "Details about the v0.1.0 release go here" - }, function (err, hash) { - if (err) return callback(err); - repo.createRef("refs/tags/v0.1.0", hash, callback); - }); - }); - }); -} - -// Mini control-flow library -function serialEach(object, fn, callback) { - var keys = Object.keys(object); - next(); - function next(err) { - if (err) return callback(err); - var key = keys.shift(); - if (!key) return callback(); - fn(key, object[key], next); - } -} -function parallelEach(object, fn, callback) { - var keys = Object.keys(object); - var left = keys.length + 1; - var done = false; - keys.forEach(function (key) { - fn(key, object[key], check); - }); - check(); - function check(err) { - if (done) return; - if (err) { - done = true; - return callback(err); - } - if (--left) return; - done = true; - callback(); - } -} diff --git a/examples/ls-remote.js b/examples/ls-remote.js deleted file mode 100644 index f249dc5..0000000 --- a/examples/ls-remote.js +++ /dev/null @@ -1,13 +0,0 @@ -var platform = require('git-node-platform'); -var jsGit = require('../.'); -var gitRemote = require('git-net')(platform); - -var repo = jsGit(); - -var url = process.argv[2] || "git://github.com/creationix/conquest.git"; -repo.lsRemote(gitRemote(url), function (err, refs) { - if (err) throw err; - Object.keys(refs).forEach(function (ref) { - console.log(refs[ref] + "\t" + ref); - }); -}); diff --git a/examples/mock.js b/examples/mock.js deleted file mode 100644 index c3c9d00..0000000 --- a/examples/mock.js +++ /dev/null @@ -1,18 +0,0 @@ -// Mock data for generating some history -exports.author = { name: "Tim Caswell", email: "tim@creationix.com" }; -exports.committer = { name: "JS-Git", email: "js-git@creationix.com" }; -exports.commits = { - "Initial Commit\n": { - "README.md": "# This is a test Repo\n\nIt's generated entirely by JavaScript\n" - }, - "Add package.json and blank module\n": { - "README.md": "# This is a test Repo\n\nIt's generated entirely by JavaScript\n", - "package.json": '{\n "name": "awesome-lib",\n "version": "3.1.3",\n "main": "awesome.js"\n}\n', - "awesome.js": 'module.exports = function () {\n throw new Error("TODO: Implement Awesome");\n};\n' - }, - "Implement awesome and bump version to 3.1.4\n": { - "README.md": "# This is a test Repo\n\nIt's generated entirely by JavaScript\n", - "package.json": '{\n "name": "awesome-lib",\n "version": "3.1.4",\n "main": "awesome.js"\n}\n', - "awesome.js": 'module.exports = function () {\n return 42;\n};\n' - } -}; diff --git a/examples/pkt-line.js b/examples/pkt-line.js deleted file mode 100644 index 09cc550..0000000 --- a/examples/pkt-line.js +++ /dev/null @@ -1,152 +0,0 @@ -var bops = { - is: require('bops/is.js'), - to: require('bops/to.js'), - from: require('bops/from.js'), - create: require('bops/create.js'), - subarray: require('bops/subarray.js'), - join: require('bops/join.js'), -}; - -var PACK = bops.from("PACK"); - -module.exports = { - deframer: deframer, - framer: framer, - frame: frame, -}; - -function deframer(emit) { - var state = 0; - var offset = 4; - var length = 0; - var data; - - return function (item) { - - // Forward the EOS marker - if (item === undefined) return emit(); - - // Once we're in pack mode, everything goes straight through - if (state === 3) return emit(item); - - // Otherwise parse the data using a state machine. - for (var i = 0, l = item.length; i < l; i++) { - var byte = item[i]; - if (state === 0) { - var val = fromHexChar(byte); - if (val === -1) { - if (byte === PACK[0]) { - offset = 1; - state = 2; - continue; - } - state = -1; - throw new SyntaxError("Not a hex char: " + String.fromCharCode(byte)); - } - length |= val << ((--offset) * 4); - if (offset === 0) { - if (length === 4) { - offset = 4; - emit(""); - } - else if (length === 0) { - offset = 4; - emit(null); - } - else if (length > 4) { - length -= 4; - data = bops.create(length); - state = 1; - } - else { - state = -1; - throw new SyntaxError("Invalid length: " + length); - } - } - } - else if (state === 1) { - data[offset++] = byte; - if (offset === length) { - offset = 4; - state = 0; - length = 0; - if (data[0] === 1) { - emit(bops.subarray(data, 1)); - } - else if (data[0] === 2) { - emit(["progress", bops.to(bops.subarray(data, 1))]); - } - else if (data[0] === 3) { - emit(["error", bops.to(bops.subarray(data, 1))]); - } - else { - emit(bops.to(data)); - } - } - } - else if (state === 2) { - if (offset < 4 && byte === PACK[offset++]) { - continue; - } - state = 3; - emit(bops.join([PACK, bops.subarray(item, i)])); - break; - } - else { - throw new Error("pkt-line decoder in invalid state"); - } - } - }; - -} - - -function framer(emit) { - return function (item) { - if (item === undefined) return emit(); - emit(frame(item)); - }; -} - -function frame(item) { - if (item === null) return bops.from("0000"); - if (typeof item === "string") { - item = bops.from(item); - } - if (bops.is(item)) { - return bops.join([frameHead(item.length + 4), item]); - } - if (Array.isArray(item)) { - var type = item[0]; - item = item[1]; - var head = bops.create(5); - if (type === "pack") head[4] = 1; - else if (type === "progress") head[4] = 2; - else if (type === "error") head[4] = 3; - else throw new Error("Invalid channel name: " + type); - if (typeof item === "string") { - item = bops.from(item); - } - return bops.join([frameHead(item.length + 5, head), item]); - } - throw new Error("Invalid input: " + item); -} - - -function frameHead(length, buffer) { - buffer = buffer || bops.create(4); - buffer[0] = toHexChar(length >>> 12); - buffer[1] = toHexChar((length >>> 8) & 0xf); - buffer[2] = toHexChar((length >>> 4) & 0xf); - buffer[3] = toHexChar(length & 0xf); - return buffer; -} - -function fromHexChar(val) { - return (val >= 0x30 && val < 0x40) ? val - 0x30 : - ((val > 0x60 && val <= 0x66) ? val - 0x57 : -1); -} - -function toHexChar(val) { - return val < 0x0a ? val + 0x30 : val + 0x57; -} diff --git a/examples/read-harmony.js b/examples/read-harmony.js deleted file mode 100644 index 16677bf..0000000 --- a/examples/read-harmony.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -let platform = require('git-node-platform'); -let jsGit = require('../.'); -let fsDb = require('git-fs-db')(platform); -let fs = platform.fs; -let run = require('gen-run'); - -// Create a filesystem backed bare repo -let repo = jsGit(fsDb(fs("test.git"))); - -run(start("HEAD")); - -function* start(hashish) { - let hash = yield repo.resolveHashish(hashish); - console.log(hashish, hash); - yield* loadCommit(hash); -} - -function* loadCommit(hash) { - let commit = yield repo.loadAs("commit", hash); - console.log("COMMIT", hash, commit); - let tree = yield repo.loadAs("tree", commit.tree); - console.log("TREE", commit.tree, tree); - for (let entry of tree.values()) { - let blob = yield repo.loadAs("blob", entry.hash); - console.log("BLOB", entry.hash, blob); - } - for (let parent of commit.parents.values()) { - yield* loadCommit(parent); - } -} diff --git a/examples/read-pack.js b/examples/read-pack.js deleted file mode 100644 index c8d61e5..0000000 --- a/examples/read-pack.js +++ /dev/null @@ -1,130 +0,0 @@ -var frame = require('../lib/frame.js'); -var sha1 = require('../lib/sha1.js'); -var decoders = require('../lib/decoders.js'); -var decodePack = require('../lib/pack-codec.js').decodePack; -var applyDelta = require('../lib/apply-delta.js'); -var inspect = require('util').inspect; - -var nodes = {}; -var links = []; -var items = {}; -var hashes = {}; -var left, num; - -var onItem = decodePack(function (item) { - if (left === undefined) { - left = num = item.num; - } - else if (item) { - left--; - console.error("%s/%s left", left, num); - } - else { - if (left) throw new Error(left + " items missing!"); - } - if (item && item.body) { - var hash = item.hash = sha1(frame(item.type, item.body)); - hashes[item.offset] = hash; - items[hash] = item; - - if (item.type === "ofs-delta") { - item.ref = hashes[item.offset - item.ref]; - item.type = "ref-delta"; - } - if (item.type === "ref-delta") { - var target = items[item.ref]; - item.type = target.type; - item.body = applyDelta(item.body, target.body); - delete items[hash]; - hash = item.hash = sha1(frame(item.type, item.body)); - hashes[item.offset] = hash; - items[hash] = item; - } - - var obj = item.obj = decoders[item.type](item.body); - - if (item.type === "commit") { - var label = []; - nodes[hash] = { - color: "deepskyblue4", - shape: "record", - label: label - }; - label.push(" " + shorten(hash)); - label.push(" " + obj.message.split("\n")[0].replace(/"/g, '')); - links.push([ - '"' + hash + '":commit', - '"' + obj.tree + '":hash', - ]); - obj.parents.forEach(function (parent) { - links.push([ - '"' + hash + '":hash', - '"' + parent + '":hash', - ]); - }); - } - else if (item.type === "tree") { - var label = [ - " " + shorten(hash), - ]; - Object.keys(obj).forEach(function (name, i) { - var key = "f" + i; - label.push("<" + key + "> " + name); - links.push([ - '"' + hash + '":' + key, - '"' + obj[name].hash + '":hash', - ]); - }); - nodes[hash] = { - color: "forestgreen", - shape: "record", - label: label - }; - } - else if (item.type === "blob") { - nodes[hash] = { - color: "firebrick4", - shape: "record", - label: [ - " " + shorten(hash), - item.body.length + " bytes data" - ] - }; - } - } - - if (item === undefined) printDot(); - console.error(inspect(item, {colors:true})); -}); -process.stdin.on('data', onItem); -process.stdin.on('end', onItem); -process.stdin.resume(); - -function printDot() { - var dot = []; - Object.keys(nodes).forEach(function (hash) { - var props = nodes[hash]; - dot.push('"' + hash + '" [\n' + Object.keys(props).map(function (name) { - var value = props[name]; - if (Array.isArray(value)) value = value.join("|"); - return ' ' + name + ' = "' + value + '"'; - }).join("\n") + '\n];'); - }); - links.forEach(function (pair) { - if (pair[2]) { - dot.push(pair[0] + ' -> ' + pair[1] + ' [label="' + pair[2] + '"];'); - } - else { - dot.push(pair[0] + ' -> ' + pair[1] + ';'); - } - }); - - dot.unshift('graph [rankdir = "LR" aspect=1];'); - dot.unshift('digraph packfile {'); - dot.push('}'); - console.log(dot.join("\n\n")); -} - -function shorten(hash) { - return hash.substr(0, 6) + "..." + hash.substr(hash.length - 6); -} \ No newline at end of file diff --git a/examples/read.js b/examples/read.js deleted file mode 100644 index 2eb5082..0000000 --- a/examples/read.js +++ /dev/null @@ -1,39 +0,0 @@ -var platform = require('git-node-platform'); -var jsGit = require('../.'); -var fsDb = require('git-fs-db')(platform); -var fs = platform.fs; - -// Create a filesystem backed bare repo -var repo = jsGit(fsDb(fs("test.git"))); - -loadCommit("HEAD"); - -function loadCommit(hashish) { - repo.loadAs("commit", hashish, onCommit); -} - -function onCommit(err, commit, hash) { - if (err) throw err; - console.log("COMMIT", hash, commit); - loadTree(commit.tree); - if (commit.parents) { - commit.parents.forEach(loadCommit); - } -} - -function loadTree(hash) { - repo.loadAs("tree", hash, onTree); -} - -function onTree(err, tree, hash) { - if (err) throw err; - console.log("TREE", hash, tree); - tree.forEach(onEntry); -} - -function onEntry(entry) { - repo.loadAs("blob", entry.hash, function (err, blob) { - if (err) throw err; - console.log("BLOB", entry.hash, blob); - }); -} diff --git a/examples/serve.js b/examples/serve.js deleted file mode 100644 index 0eca92d..0000000 --- a/examples/serve.js +++ /dev/null @@ -1,246 +0,0 @@ -var jsGit = require('../.'); -var net = require('net'); -var inspect = require('util').inspect; - -var db = memDb(); -var repo = jsGit(db); -db.init(function (err) { - if (err) throw err; - require('./create.js')(repo, function (err) { - if (err) throw err; - console.log("Repo Initialized with sample data"); - }); -}); - -var server = net.createServer(connectionHandler(function (req, callback) { - if (req.path !== "/test.git") return callback(new Error("Unknown repo: " + req.path)); - callback(null, repo); -})); -server.listen(9418, "127.0.0.1", function () { - console.log("GIT server listening at", server.address()); -}); - -////////////////////// TCP transport for git:// uris /////////////////////////// - -function connectionHandler(onReq, opts) { - opts = opts || {}; - return function (socket) { - var remote = wrap(socket), command, path, host; - socket.on("error", onDone); - remote.read(function (err, line) { - if (err) return onDone(err); - var match = line.match(/^(git-upload-pack|git-receive-pack) (.+?)\0(?:host=(.+?)\0)$/); - if (!match) return onDone(new Error("Invalid connection message: " + line)); - command = match[1]; - path = match[2]; - host = match[3]; - onReq({ path: path, host: host }, onRepo); - }); - - function onRepo(err, repo) { - if (err) return onDone(err); - if (command === "git-upload-pack") { - return repo.uploadPack(remote, opts, onDone); - } - if (command === "git-receive-pack") { - return repo.receivePack(remote, opts, onDone); - } - } - - function onDone(err, changes) { - if (err) console.error(err.stack); - else console.log("DONE", { - command: command, - path: path, - host: host, - changes: changes - }); - socket.destroy(); - } - }; -} - -var pktLine = require('./pkt-line.js'); -function wrap(socket) { - var queue = []; - var rerr = null; - var rcb = null, wcb = null; - var onChunk = pktLine.deframer(onFrame); - var writeFrame = pktLine.framer(writeChunk); - socket.on("data", onEvent); - socket.on("end", onEvent); - socket.on("drain", onDrain); - return { read: read, write: write }; - - function onEvent(chunk) { - try { - onChunk(chunk); - } - catch (err) { - console.error(err.stack); - rerr = err; - check(); - } - } - - function onFrame(frame) { - console.log("<-", inspect(frame, {colors:true})); - queue.push(frame); - check(); - } - - function read(callback) { - if (!callback) return read; - if (rcb) return callback(new Error("Only one read at a time")); - rcb = callback; - check(); - } - - function check() { - if (rcb && (rerr || queue.length)) { - var callback = rcb; - rcb = null; - if (rerr) { - var err = rerr; - rerr = null; - callback(err); - } - else { - callback(null, queue.shift()); - } - } - if (queue.length) socket.pause(); - else if (rcb) socket.resume(); - } - - function write(frame, callback) { - if (callback === undefined) return write.bind(this, frame); - if (callback) { - if (wcb) return callback(new Error("Only one write at a time")); - wcb = callback; - } - try { - console.log("->", inspect(frame, {colors:true})); - writeFrame(frame); - } - catch (err) { - if (wcb) { - wcb = null; - callback(err); - } - else { - throw err; - } - } - } - - function writeChunk(chunk) { - // console.log(">>", inspect("" + chunk, {colors:true})); - if (chunk === undefined) { - socket.end(); - onDrain(); - } - else if (socket.write(chunk)) { - onDrain(); - } - } - - function onDrain() { - if (wcb) { - var callback = wcb; - wcb = null; - callback(); - } - } - -} - -/////////////////// inMemory database for easy testing ///////////////////////// - -function makeAsync(fn, callback) { - if (!callback) return makeAsync.bind(this, fn); - process.nextTick(function () { - var result; - try { result = fn(); } - catch (err) { return callback(err); } - if (result === undefined) return callback(); - return callback(null, result); - }); -} - -function memDb() { - - // Store everything in ram! - var objects; - var others; - var isHash = /^[a-z0-9]{40}$/; - - return { - get: get, - set: set, - has: has, - del: del, - keys: keys, - init: init, - clear: init, - }; - - function get(key, callback) { - console.log("GET", key); - return makeAsync(function () { - if (isHash.test(key)) { - return objects[key]; - } - return others[key]; - }, callback); - } - - function set(key, value, callback) { - console.log("SET", key); - return makeAsync(function () { - if (isHash.test(key)) { - objects[key] = value; - } - else { - others[key] = value.toString(); - } - }, callback); - } - - function has(key, callback) { - return makeAsync(function () { - if (isHash.test(key)) { - return key in objects; - } - return key in others; - }, callback); - } - - function del(key, callback) { - return makeAsync(function () { - if (isHash.test(key)) { - delete objects[key]; - } - else { - delete others[key]; - } - }, callback); - } - - function keys(prefix, callback) { - return makeAsync(function () { - var length = prefix.length; - return Object.keys(others).filter(function (key) { - return key.substr(0, length) === prefix; - }); - }, callback); - } - - function init(callback) { - return makeAsync(function () { - objects = {}; - others = {}; - }, callback); - } - -} diff --git a/examples/walk.js b/examples/walk.js deleted file mode 100644 index 63f4103..0000000 --- a/examples/walk.js +++ /dev/null @@ -1,50 +0,0 @@ -var platform = require('git-node-platform'); -var jsGit = require('../.'); -var fsDb = require('git-fs-db')(platform); -var fs = platform.fs; - -// Create a filesystem backed bare repo -var repo = jsGit(fsDb(fs(process.argv[2] || "test.git"))); -repo.logWalk("HEAD", function (err, log) { - if (err) throw err; - var shallow; - return log.read(onRead); - - function onRead(err, commit) { - if (err) throw err; - if (!commit) return logEnd(shallow); - if (commit.last) shallow = true; - logCommit(commit); - repo.treeWalk(commit.tree, function (err, tree) { - if (err) throw err; - tree.read(onEntry); - function onEntry(err, entry) { - if (err) throw err; - if (!entry) { - return log.read(onRead); - } - logEntry(entry); - return tree.read(onEntry); - } - }); - } -}); - -function logCommit(commit) { - var author = commit.author; - var message = commit.message; - console.log("\n\x1B[33mcommit %s\x1B[0m", commit.hash); - console.log("Author: %s <%s>", author.name, author.email); - console.log("Date: %s", author.date); - console.log("\n \x1B[32;1m" + message.trim().split("\n").join("\x1B[0m\n \x1B[32m") + "\x1B[0m\n"); -} - -function logEntry(entry) { - var path = entry.path.replace(/\//g, "\x1B[1;34m/\x1B[0;34m") + "\x1B[0m"; - console.log(" %s %s", entry.hash, path); -} - -function logEnd(shallow) { - var message = shallow ? "End of shallow record." : "Beginning of history"; - console.log("\n\x1B[30;1m%s\x1B[0m\n", message); -} \ No newline at end of file diff --git a/js-git.js b/js-git.js deleted file mode 100644 index dc48018..0000000 --- a/js-git.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = newRepo; - -function newRepo(db) { - if (!db) throw new TypeError("A db interface instance is required"); - - // Create a new repo object. - var repo = {}; - - // Auto trace the db if tracing is turned on. - if (require('./lib/trace.js')) db = require('./lib/tracedb.js')(db); - - // Add the db interface (used by objects, refs, and packops mixins) - repo.db = db; - - // Mix in object store interface - require('./mixins/objects.js')(repo); - - // Mix in the references interface - require('./mixins/refs.js')(repo); - - // Mix in the walker helpers - require('./mixins/walkers.js')(repo); - - // Mix in packfile import and export ability - require('./mixins/packops.js')(repo); - - // Mix in git network client ability - require('./mixins/client.js')(repo); - - // Mix in git network client ability - require('./mixins/server.js')(repo); - - return repo; -} diff --git a/lib/agent.js b/lib/agent.js deleted file mode 100644 index 565482d..0000000 --- a/lib/agent.js +++ /dev/null @@ -1,2 +0,0 @@ -var meta = require('../package.json'); -module.exports = meta.name + "/" + meta.version; diff --git a/lib/apply-delta.js b/lib/apply-delta.js deleted file mode 100644 index 20a7f00..0000000 --- a/lib/apply-delta.js +++ /dev/null @@ -1,94 +0,0 @@ -// This is Chris Dickinson's code - -var binary = require('bops') - , Decoder = require('varint/decode.js') - , vi = new Decoder - -// we use writeUint[8|32][LE|BE] instead of indexing -// into buffers so that we get buffer-browserify compat. -var OFFSET_BUFFER = binary.create(4) - , LENGTH_BUFFER = binary.create(4) - -module.exports = apply_delta; -function apply_delta(delta, target) { - var base_size_info = {size: null, buffer: null} - , resized_size_info = {size: null, buffer: null} - , output_buffer - , out_idx - , command - , len - , idx - - delta_header(delta, base_size_info) - delta_header(base_size_info.buffer, resized_size_info) - - delta = resized_size_info.buffer - - idx = - out_idx = 0 - output_buffer = binary.create(resized_size_info.size) - - len = delta.length - - while(idx < len) { - command = delta[idx++] - command & 0x80 ? copy() : insert() - } - - return output_buffer - - function copy() { - binary.writeUInt32LE(OFFSET_BUFFER, 0, 0) - binary.writeUInt32LE(LENGTH_BUFFER, 0, 0) - - var check = 1 - , length - , offset - - for(var x = 0; x < 4; ++x) { - if(command & check) { - OFFSET_BUFFER[3 - x] = delta[idx++] - } - check <<= 1 - } - - for(var x = 0; x < 3; ++x) { - if(command & check) { - LENGTH_BUFFER[3 - x] = delta[idx++] - } - check <<= 1 - } - LENGTH_BUFFER[0] = 0 - - length = binary.readUInt32BE(LENGTH_BUFFER, 0) || 0x10000 - offset = binary.readUInt32BE(OFFSET_BUFFER, 0) - - binary.copy(target, output_buffer, out_idx, offset, offset + length) - out_idx += length - } - - function insert() { - binary.copy(delta, output_buffer, out_idx, idx, command + idx) - idx += command - out_idx += command - } -} - -function delta_header(buf, output) { - var done = false - , idx = 0 - , size = 0 - - vi.ondata = function(s) { - size = s - done = true - } - - do { - vi.write(buf[idx++]) - } while(!done) - - output.size = size - output.buffer = binary.subarray(buf, idx) - -} \ No newline at end of file diff --git a/lib/assert-type.js b/lib/assert-type.js deleted file mode 100644 index c1808db..0000000 --- a/lib/assert-type.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function assertType(object, type) { - if (object.type !== type) { - throw new Error(type + " expected, but found " + object.type); - } -}; diff --git a/lib/decoders.js b/lib/decoders.js deleted file mode 100644 index 1ea962d..0000000 --- a/lib/decoders.js +++ /dev/null @@ -1,104 +0,0 @@ -var indexOf = require('./indexof.js'); -var parseOct = require('./parseoct.js'); -var parseAscii = require('./parseascii.js'); -var parseToHex = require('./parsetohex.js'); - -exports.commit = function decodeCommit(body) { - var i = 0; - var start; - var key; - var parents = []; - var commit = { - tree: "", - parents: parents, - author: "", - committer: "", - message: "" - }; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = parseAscii(body, start, i++); - if (key === "parent") { - parents.push(value); - } - else { - if (key === "author" || key === "committer") { - value = decodePerson(value); - } - commit[key] = value; - } - } - i++; - commit.message = parseAscii(body, i, body.length); - return commit; -}; - -exports.tag = function decodeTag(body) { - var i = 0; - var start; - var key; - var tag = {}; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = parseAscii(body, start, i++); - if (key === "tagger") value = decodePerson(value); - tag[key] = value; - } - i++; - tag.message = parseAscii(body, i, body.length); - return tag; -}; - -exports.tree = function decodeTree(body) { - var i = 0; - var length = body.length; - var start; - var mode; - var name; - var hash; - var tree = {}; - while (i < length) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - mode = parseOct(body, start, i++); - start = i; - i = indexOf(body, 0x00, start); - name = parseAscii(body, start, i++); - hash = parseToHex(body, i, i += 20); - tree[name] = { - mode: mode, - hash: hash - }; - } - return tree; -}; - -exports.blob = function decodeBlob(body) { - return body; -}; - -function decodePerson(string) { - var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); - if (!match) throw new Error("Improperly formatted person string"); - var sec = parseInt(match[3], 10); - var date = new Date(sec * 1000); - date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; - return { - name: match[1], - email: match[2], - date: date - }; -} diff --git a/lib/deflate.js b/lib/deflate.js deleted file mode 100644 index a7b797a..0000000 --- a/lib/deflate.js +++ /dev/null @@ -1,5 +0,0 @@ -var zlib = require('zlib'); -module.exports = function deflate(buffer, callback) { - return zlib.deflate(buffer, callback); -}; -// TODO: make this work in the browser too. \ No newline at end of file diff --git a/lib/deframe.js b/lib/deframe.js deleted file mode 100644 index 30d02e6..0000000 --- a/lib/deframe.js +++ /dev/null @@ -1,18 +0,0 @@ -var bops = require('bops'); -var indexOf = require('./indexof.js'); -var parseDec = require('./parsedec.js'); -var parseAscii = require('./parseascii.js'); - -module.exports = function deframe(buffer) { - var space = indexOf(buffer, 0x20); - if (space < 0) throw new Error("Invalid git object buffer"); - var nil = indexOf(buffer, 0x00, space); - if (nil < 0) throw new Error("Invalid git object buffer"); - var body = bops.subarray(buffer, nil + 1); - var size = parseDec(buffer, space + 1, nil); - if (size !== body.length) throw new Error("Invalid body length."); - return [ - parseAscii(buffer, 0, space), - body - ]; -}; diff --git a/lib/each.js b/lib/each.js deleted file mode 100644 index 94a3db3..0000000 --- a/lib/each.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = each; - -// A functional forEach that works on both arrays and objects -function each(obj, fn) { - if (Array.isArray(obj)) return obj.forEach(fn); - var keys = Object.keys(obj); - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - fn(obj[key], key, obj); - } -} diff --git a/lib/encoders.js b/lib/encoders.js deleted file mode 100644 index 1462efc..0000000 --- a/lib/encoders.js +++ /dev/null @@ -1,76 +0,0 @@ -var bops = require('bops'); -var pathCmp = require('./pathcmp.js'); - -exports.commit = function encodeCommit(commit) { - if (!commit.tree || !commit.author || !commit.message) { - throw new TypeError("Tree, author, and message are require for commits"); - } - var parents = commit.parents || (commit.parent ? [ commit.parent ] : []); - if (!Array.isArray(parents)) { - throw new TypeError("Parents must be an array"); - } - var str = "tree " + commit.tree; - for (var i = 0, l = parents.length; i < l; ++i) { - str += "\nparent " + parents[i]; - } - str += "\nauthor " + encodePerson(commit.author) + - "\ncommitter " + encodePerson(commit.committer || commit.author) + - "\n\n" + commit.message; - return bops.from(str); -}; - -exports.tag = function encodeTag(tag) { - if (!tag.object || !tag.type || !tag.tag || !tag.tagger || !tag.message) { - throw new TypeError("Object, type, tag, tagger, and message required"); - } - var str = "object " + tag.object + - "\ntype " + tag.type + - "\ntag " + tag.tag + - "\ntagger " + encodePerson(tag.tagger) + - "\n\n" + tag.message; - return bops.from(str + "\n" + tag.message); -}; - -exports.tree = function encodeTree(tree) { - var chunks = []; - if (!Array.isArray(tree)) { - tree = Object.keys(tree).map(function (name) { - var entry = tree[name]; - entry.name = name; - return entry; - }); - } - tree.sort(pathCmp).forEach(onEntry); - return bops.join(chunks); - - function onEntry(entry) { - chunks.push( - bops.from(entry.mode.toString(8) + " " + entry.name + "\0"), - bops.from(entry.hash, "hex") - ); - } -}; - -exports.blob = function encodeBlob(blob) { - if (bops.is(blob)) return blob; - return bops.from(blob); -}; - -function encodePerson(person) { - if (!person.name || !person.email) { - throw new TypeError("Name and email are required for person fields"); - } - return safe(person.name) + - " <" + safe(person.email) + "> " + - formatDate(person.date || new Date()); -} - -function safe(string) { - return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); -} - -function formatDate(date) { - var timezone = (date.timeZoneoffset || date.getTimezoneOffset()) / 60; - var seconds = Math.floor(date.getTime() / 1000); - return seconds + " " + (timezone > 0 ? "-0" : "0") + timezone + "00"; -} diff --git a/lib/frame.js b/lib/frame.js deleted file mode 100644 index 3717b83..0000000 --- a/lib/frame.js +++ /dev/null @@ -1,8 +0,0 @@ -var bops = require('bops'); - -module.exports = function frame(type, body) { - return bops.join([ - bops.from(type + " " + body.length + "\0"), - body - ]); -}; diff --git a/lib/indexof.js b/lib/indexof.js deleted file mode 100644 index 18c61a5..0000000 --- a/lib/indexof.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = function indexOf(buffer, byte, i) { - i |= 0; - var length = buffer.length; - for (;;i++) { - if (i >= length) return -1; - if (buffer[i] === byte) return i; - } -}; diff --git a/lib/inflate.js b/lib/inflate.js deleted file mode 100644 index 48dc528..0000000 --- a/lib/inflate.js +++ /dev/null @@ -1,853 +0,0 @@ -var bops = require('bops'); - -// Wrapper for proposed new API to inflate: -// -// var inf = inflate(); -// inf.write(byte) -> more - Write a byte to inflate's state-machine. -// Returns true if more data is expected. -// inf.recycle() - Reset the internal state machine. -// inf.flush() -> data - Flush the output as a binary buffer. -// -// This is quite slow, but could be made fast if baked into inflate itself. -module.exports = function () { - var push = inflate(onEmit, onUnused); - var more = true; - var chunks = []; - var b = bops.create(1); - - return { write: write, recycle: recycle, flush: flush }; - - function write(byte) { - b[0] = byte; - push(null, b); - return more; - } - - function recycle() { - push.recycle(); - more = true; - } - - function flush() { - var buffer = bops.join(chunks); - chunks.length = 0; - return buffer; - } - - function onEmit(err, item) { - if (err) throw err; - if (item === undefined) { - // console.log("onEnd"); - more = false; - return; - } - chunks.push(item); - } - - function onUnused(chunks) { - // console.log("onUnused", chunks); - more = false; - } -}; - -var MAXBITS = 15 - , MAXLCODES = 286 - , MAXDCODES = 30 - , MAXCODES = (MAXLCODES+MAXDCODES) - , FIXLCODES = 288 - -var lens = [ - 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, - 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 -] - -var lext = [ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, - 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 -] - -var dists = [ - 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, - 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, - 8193, 12289, 16385, 24577 -] - -var dext = [ - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, - 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, - 12, 12, 13, 13 -] - -var order = [ - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 -] - -var WINDOW = 32768 - , WINDOW_MINUS_ONE = WINDOW - 1 - -function inflate(emit, on_unused) { - var output = new Uint8Array(WINDOW) - , need_input = false - , buffer_offset = 0 - , bytes_read = 0 - , output_idx = 0 - , ended = false - , state = null - , states = [] - , buffer = [] - , got = 0 - - // buffer up to 128k "output one" bytes - var OUTPUT_ONE_LENGTH = 131070 - , output_one_offs = OUTPUT_ONE_LENGTH - , output_one_buf - - var bitbuf = 0 - , bitcnt = 0 - , is_final = false - , fixed_codes - - var adler_s1 = 1 - , adler_s2 = 0 - - onread.recycle = function recycle() { - var out - buffer.length = 0 - buffer_offset = 0 - output_idx = 0 - bitbuf = 0 - bitcnt = 0 - states.length = 0 - is_final = false - need_input = false - bytes_read = 0 - output_idx = 0 - ended = false - got = 0 - adler_s1 = 1 - adler_s2 = 0 - output_one_offs = 0 - become(noop, {}, noop) - start_stream_header() - // return stream - } - - var bytes_need = 0 - , bytes_value = [] - - var bits_need = 0 - , bits_value = [] - - var codes_distcode = null - , codes_lencode = null - , codes_len = 0 - , codes_dist = 0 - , codes_symbol = 0 - - var dynamic_distcode = {symbol: [], count: []} - , dynamic_lencode = {symbol: [], count: []} - , dynamic_lengths = [] - , dynamic_nlen = 0 - , dynamic_ndist = 0 - , dynamic_ncode = 0 - , dynamic_index = 0 - , dynamic_symbol = 0 - , dynamic_len = 0 - - var decode_huffman = null - , decode_len = 0 - , decode_code = 0 - , decode_first = 0 - , decode_count = 0 - , decode_index = 0 - - var last = null - - become(noop, {}, noop) - start_stream_header() - - return onread - - function onread(err, buf) { - if(buf === undefined) { - return emit(err) - } - - return write(buf) - } - - function noop() { - - } - - function call_header() { - } - - function call_bytes(need) { - bytes_value.length = 0 - bytes_need = need - } - - function call_bits(need) { - bits_value = 0 - bits_need = need - } - - function call_codes(distcode, lencode) { - codes_len = - codes_dist = - codes_symbol = 0 - codes_distcode = distcode - codes_lencode = lencode - } - - function call_dynamic() { - dynamic_distcode.symbol.length = - dynamic_distcode.count.length = - dynamic_lencode.symbol.length = - dynamic_lencode.count.length = - dynamic_lengths.length = 0 - dynamic_nlen = 0 - dynamic_ndist = 0 - dynamic_ncode = 0 - dynamic_index = 0 - dynamic_symbol = 0 - dynamic_len = 0 - } - - function call_decode(h) { - decode_huffman = h - decode_len = 1 - decode_first = - decode_index = - decode_code = 0 - } - - function write(buf) { - buffer.push(buf) - got += buf.length - if(!ended) { - execute() - } - } - - function execute() { - do { - states[0].current() - } while(!need_input && !ended) - - var needed = need_input - need_input = false - } - - function start_stream_header() { - become(bytes, call_bytes(2), got_stream_header) - } - - function got_stream_header() { - var cmf = last[0] - , flg = last[1] - - - if((cmf << 8 | flg) % 31 !== 0) { - emit(new Error( - 'failed header check' - )) - return - } - - - - - if(flg & 32) { - return become(bytes, call_bytes(4), on_got_fdict) - } - return become(bits, call_bits(1), on_got_is_final) - } - - - - - function on_got_fdict() { - return become(bits, call_bits(1), on_got_is_final) - } - - - - - - - - - function on_got_is_final() { - is_final = last - become(bits, call_bits(2), on_got_type) - } - - - - - - - - - - - - - function on_got_type() { - if(last === 0) { - become(bytes, call_bytes(4), on_got_len_nlen) - return - } - - if(last === 1) { - // `fixed` and `dynamic` blocks both eventually delegate - // to the "codes" state -- which reads bits of input, throws - // them into a huffman tree, and produces "symbols" of output. - fixed_codes = fixed_codes || build_fixed() - become(start_codes, call_codes( - fixed_codes.distcode - , fixed_codes.lencode - ), done_with_codes) - return - } - - become(start_dynamic, call_dynamic(), done_with_codes) - return - } - - - - - function on_got_len_nlen() { - var want = last[0] | (last[1] << 8) - , nlen = last[2] | (last[3] << 8) - - if((~nlen & 0xFFFF) !== want) { - emit(new Error( - 'failed len / nlen check' - )) - } - - if(!want) { - become(bits, call_bits(1), on_got_is_final) - return - } - become(bytes, call_bytes(want), on_got_stored) - } - - - - - function on_got_stored() { - output_many(last) - if(is_final) { - become(bytes, call_bytes(4), on_got_adler) - return - } - become(bits, call_bits(1), on_got_is_final) - } - - - - - - - function start_dynamic() { - become(bits, call_bits(5), on_got_nlen) - } - - function on_got_nlen() { - dynamic_nlen = last + 257 - become(bits, call_bits(5), on_got_ndist) - } - - function on_got_ndist() { - dynamic_ndist = last + 1 - become(bits, call_bits(4), on_got_ncode) - } - - function on_got_ncode() { - dynamic_ncode = last + 4 - if(dynamic_nlen > MAXLCODES || dynamic_ndist > MAXDCODES) { - emit(new Error('bad counts')) - return - } - - become(bits, call_bits(3), on_got_lengths_part) - } - - function on_got_lengths_part() { - dynamic_lengths[order[dynamic_index]] = last - - ++dynamic_index - if(dynamic_index === dynamic_ncode) { - for(; dynamic_index < 19; ++dynamic_index) { - dynamic_lengths[order[dynamic_index]] = 0 - } - - // temporarily construct the `lencode` using the - // lengths we've read. we'll actually be using the - // symbols produced by throwing bits into the huffman - // tree to constuct the `lencode` and `distcode` huffman - // trees. - construct(dynamic_lencode, dynamic_lengths, 19) - dynamic_index = 0 - - become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) - return - } - become(bits, call_bits(3), on_got_lengths_part) - } - - function on_got_dynamic_symbol_iter() { - dynamic_symbol = last - - if(dynamic_symbol < 16) { - dynamic_lengths[dynamic_index++] = dynamic_symbol - do_check() - return - } - - dynamic_len = 0 - if(dynamic_symbol === 16) { - become(bits, call_bits(2), on_got_dynamic_symbol_16) - return - } - - if(dynamic_symbol === 17) { - become(bits, call_bits(3), on_got_dynamic_symbol_17) - return - } - - become(bits, call_bits(7), on_got_dynamic_symbol) - } - - function on_got_dynamic_symbol_16() { - dynamic_len = dynamic_lengths[dynamic_index - 1] - on_got_dynamic_symbol_17() - } - - function on_got_dynamic_symbol_17() { - dynamic_symbol = 3 + last - do_dynamic_end_loop() - } - - function on_got_dynamic_symbol() { - dynamic_symbol = 11 + last - do_dynamic_end_loop() - } - - function do_dynamic_end_loop() { - if(dynamic_index + dynamic_symbol > dynamic_nlen + dynamic_ndist) { - emit(new Error('too many lengths')) - return - } - - while(dynamic_symbol--) { - dynamic_lengths[dynamic_index++] = dynamic_len - } - - do_check() - } - - function do_check() { - if(dynamic_index >= dynamic_nlen + dynamic_ndist) { - end_read_dynamic() - return - } - become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) - } - - function end_read_dynamic() { - // okay, we can finally start reading data out of the stream. - construct(dynamic_lencode, dynamic_lengths, dynamic_nlen) - construct(dynamic_distcode, dynamic_lengths.slice(dynamic_nlen), dynamic_ndist) - become(start_codes, call_codes( - dynamic_distcode - , dynamic_lencode - ), done_with_codes) - } - - function start_codes() { - become(decode, call_decode(codes_lencode), on_got_codes_symbol) - } - - function on_got_codes_symbol() { - var symbol = codes_symbol = last - if(symbol < 0) { - emit(new Error('invalid symbol')) - return - } - - if(symbol < 256) { - output_one(symbol) - become(decode, call_decode(codes_lencode), on_got_codes_symbol) - return - } - - if(symbol > 256) { - symbol = codes_symbol -= 257 - if(symbol >= 29) { - emit(new Error('invalid fixed code')) - return - } - - become(bits, call_bits(lext[symbol]), on_got_codes_len) - return - } - - if(symbol === 256) { - unbecome() - return - } - } - - - - - - - function on_got_codes_len() { - codes_len = lens[codes_symbol] + last - become(decode, call_decode(codes_distcode), on_got_codes_dist_symbol) - } - - - function on_got_codes_dist_symbol() { - codes_symbol = last - if(codes_symbol < 0) { - emit(new Error('invalid distance symbol')) - return - } - - become(bits, call_bits(dext[codes_symbol]), on_got_codes_dist_dist) - } - - function on_got_codes_dist_dist() { - var dist = dists[codes_symbol] + last - - // Once we have a "distance" and a "length", we start to output bytes. - // We reach "dist" back from our current output position to get the byte - // we should repeat and output it (thus moving the output window cursor forward). - // Two notes: - // - // 1. Theoretically we could overlap our output and input. - // 2. `X % (2^N) == X & (2^N - 1)` with the distinction that - // the result of the bitwise AND won't be negative for the - // range of values we're feeding it. Spare a modulo, spoil the child. - while(codes_len--) { - output_one(output[(output_idx - dist) & WINDOW_MINUS_ONE]) - } - - become(decode, call_decode(codes_lencode), on_got_codes_symbol) - } - - function done_with_codes() { - if(is_final) { - become(bytes, call_bytes(4), on_got_adler) - return - } - become(bits, call_bits(1), on_got_is_final) - } - - - - - function on_got_adler() { - var check_s1 = last[3] | (last[2] << 8) - , check_s2 = last[1] | (last[0] << 8) - - if(check_s2 !== adler_s2 || check_s1 !== adler_s1) { - emit(new Error( - 'bad adler checksum: '+[check_s2, adler_s2, check_s1, adler_s1] - )) - return - } - - ended = true - - output_one_recycle() - - if(on_unused) { - on_unused( - [bops.subarray(buffer[0], buffer_offset)].concat(buffer.slice(1)) - , bytes_read - ) - } - - output_idx = 0 - ended = true - emit() - } - - function decode() { - _decode() - } - - function _decode() { - if(decode_len > MAXBITS) { - emit(new Error('ran out of codes')) - return - } - - become(bits, call_bits(1), got_decode_bit) - } - - function got_decode_bit() { - decode_code = (decode_code | last) >>> 0 - decode_count = decode_huffman.count[decode_len] - if(decode_code < decode_first + decode_count) { - unbecome(decode_huffman.symbol[decode_index + (decode_code - decode_first)]) - return - } - decode_index += decode_count - decode_first += decode_count - decode_first <<= 1 - decode_code = (decode_code << 1) >>> 0 - ++decode_len - _decode() - } - - - function become(fn, s, then) { - if(typeof then !== 'function') { - throw new Error - } - states.unshift({ - current: fn - , next: then - }) - } - - function unbecome(result) { - if(states.length > 1) { - states[1].current = states[0].next - } - states.shift() - if(!states.length) { - ended = true - - output_one_recycle() - if(on_unused) { - on_unused( - [bops.subarray(buffer[0], buffer_offset)].concat(buffer.slice(1)) - , bytes_read - ) - } - output_idx = 0 - ended = true - emit() - // return - } - else { - last = result - } - } - - function bits() { - var byt - , idx - - idx = 0 - bits_value = bitbuf - while(bitcnt < bits_need) { - // we do this to preserve `bits_value` when - // "need_input" is tripped. - // - // fun fact: if we moved that into the `if` statement - // below, it would trigger a deoptimization of this (very - // hot) function. JITs! - bitbuf = bits_value - byt = take() - if(need_input) { - break - } - ++idx - bits_value = (bits_value | (byt << bitcnt)) >>> 0 - bitcnt += 8 - } - - if(!need_input) { - bitbuf = bits_value >>> bits_need - bitcnt -= bits_need - unbecome((bits_value & ((1 << bits_need) - 1)) >>> 0) - } - } - - - - function bytes() { - var byte_accum = bytes_value - , value - - while(bytes_need--) { - value = take() - - - if(need_input) { - bitbuf = bitcnt = 0 - bytes_need += 1 - break - } - byte_accum[byte_accum.length] = value - } - if(!need_input) { - bitcnt = bitbuf = 0 - unbecome(byte_accum) - } - } - - - - function take() { - if(!buffer.length) { - need_input = true - return - } - - if(buffer_offset === buffer[0].length) { - buffer.shift() - buffer_offset = 0 - return take() - } - - ++bytes_read - - return bitbuf = takebyte() - } - - function takebyte() { - return buffer[0][buffer_offset++] - } - - - - function output_one(val) { - adler_s1 = (adler_s1 + val) % 65521 - adler_s2 = (adler_s2 + adler_s1) % 65521 - output[output_idx++] = val - output_idx &= WINDOW_MINUS_ONE - output_one_pool(val) - } - - function output_one_pool(val) { - if(output_one_offs === OUTPUT_ONE_LENGTH) { - output_one_recycle() - } - - output_one_buf[output_one_offs++] = val - } - - function output_one_recycle() { - if(output_one_offs > 0) { - if(output_one_buf) { - emit(null, bops.subarray(output_one_buf, 0, output_one_offs)) - } else { - } - output_one_buf = bops.create(OUTPUT_ONE_LENGTH) - output_one_offs = 0 - } - } - - function output_many(vals) { - var len - , byt - , olen - - output_one_recycle() - for(var i = 0, len = vals.length; i < len; ++i) { - byt = vals[i] - adler_s1 = (adler_s1 + byt) % 65521 - adler_s2 = (adler_s2 + adler_s1) % 65521 - output[output_idx++] = byt - output_idx &= WINDOW_MINUS_ONE - } - - emit(null, bops.from(vals)) - } -} - -function build_fixed() { - var lencnt = [] - , lensym = [] - , distcnt = [] - , distsym = [] - - var lencode = { - count: lencnt - , symbol: lensym - } - - var distcode = { - count: distcnt - , symbol: distsym - } - - var lengths = [] - , symbol - - for(symbol = 0; symbol < 144; ++symbol) { - lengths[symbol] = 8 - } - for(; symbol < 256; ++symbol) { - lengths[symbol] = 9 - } - for(; symbol < 280; ++symbol) { - lengths[symbol] = 7 - } - for(; symbol < FIXLCODES; ++symbol) { - lengths[symbol] = 8 - } - construct(lencode, lengths, FIXLCODES) - - for(symbol = 0; symbol < MAXDCODES; ++symbol) { - lengths[symbol] = 5 - } - construct(distcode, lengths, MAXDCODES) - return {lencode: lencode, distcode: distcode} -} - -function construct(huffman, lengths, num) { - var symbol - , left - , offs - , len - - offs = [] - - for(len = 0; len <= MAXBITS; ++len) { - huffman.count[len] = 0 - } - - for(symbol = 0; symbol < num; ++symbol) { - huffman.count[lengths[symbol]] += 1 - } - - if(huffman.count[0] === num) { - return - } - - left = 1 - for(len = 1; len <= MAXBITS; ++len) { - left <<= 1 - left -= huffman.count[len] - if(left < 0) { - return left - } - } - - offs[1] = 0 - for(len = 1; len < MAXBITS; ++len) { - offs[len + 1] = offs[len] + huffman.count[len] - } - - for(symbol = 0; symbol < num; ++symbol) { - if(lengths[symbol] !== 0) { - huffman.symbol[offs[lengths[symbol]]++] = symbol - } - } - - return left -} diff --git a/lib/ishash.js b/lib/ishash.js deleted file mode 100644 index 6e46845..0000000 --- a/lib/ishash.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function isHash(hash) { - return (/^[0-9a-f]{40}$/).test(hash); -}; diff --git a/lib/map.js b/lib/map.js deleted file mode 100644 index 0a1c903..0000000 --- a/lib/map.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = map; - -// A functional map that works on both arrays and objects -// The returned object has the same shape as the original, but values mapped. -function map(obj, fn) { - if (Array.isArray(obj)) return obj.map(fn); - var result = {}; - var keys = Object.keys(obj); - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - result[key] = fn(obj[key], key, obj); - } - return result; -} diff --git a/lib/pack-codec.js b/lib/pack-codec.js deleted file mode 100644 index 8dad19a..0000000 --- a/lib/pack-codec.js +++ /dev/null @@ -1,216 +0,0 @@ -var inflate = require('./inflate.js'); -var deflate = require('./deflate.js'); -var sha1 = require('./sha1.js'); -var bops = { - subarray: require('bops/subarray.js'), - join: require('bops/join.js'), - from: require('bops/from.js'), -}; - -var typeToNum = { - commit: 1, - tree: 2, - blob: 3, - tag: 4, - "ofs-delta": 6, - "ref-delta": 7 -}; -var numToType = {}; -for (var type in typeToNum) { - var num = typeToNum[type]; - numToType[num] = type; -} - -exports.packFrame = packFrame; -function packFrame(type, body, callback) { - var length = body.length; - var head = [(typeToNum[type] << 4) | (length & 0xf)]; - var i = 0; - length >>= 4; - while (length) { - head[i++] |= 0x80; - head[i] = length & 0x7f; - length >>= 7; - } - deflate(body, function (err, body) { - if (err) return callback(err); - callback(null, bops.join([bops.from(head), body])); - }); -} - -exports.decodePack = decodePack; -function decodePack(emit) { - - var state = $pack; - var sha1sum = sha1(); - var inf = inflate(); - - var offset = 0; - var position = 0; - var version = 0x4b434150; // PACK reversed - var num = 0; - var type = 0; - var length = 0; - var ref = null; - var checksum = ""; - var start = 0; - var parts = []; - - - return function (chunk) { - if (chunk === undefined) { - if (num || checksum.length < 40) throw new Error("Unexpected end of input stream"); - return emit(); - } - - for (var i = 0, l = chunk.length; i < l; i++) { - // console.log([state, i, chunk[i].toString(16)]); - if (!state) throw new Error("Unexpected extra bytes: " + bops.subarray(chunk, i)); - state = state(chunk[i], i, chunk); - position++; - } - if (!state) return; - if (state !== $checksum) sha1sum.update(chunk); - var buff = inf.flush(); - if (buff.length) { - parts.push(buff); - } - }; - - // The first four bytes in a packfile are the bytes 'PACK' - function $pack(byte) { - if ((version & 0xff) === byte) { - version >>>= 8; - return version ? $pack : $version; - } - throw new Error("Invalid packfile header"); - } - - // The version is stored as an unsigned 32 integer in network byte order. - // It must be version 2 or 3. - function $version(byte) { - version = (version << 8) | byte; - if (++offset < 4) return $version; - if (version >= 2 && version <= 3) { - offset = 0; - return $num; - } - throw new Error("Invalid version number " + num); - } - - // The number of objects in this packfile is also stored as an unsigned 32 bit int. - function $num(byte) { - num = (num << 8) | byte; - if (++offset < 4) return $num; - offset = 0; - emit({version: version, num: num}); - return $header; - } - - // n-byte type and length (3-bit type, (n-1)*7+4-bit length) - // CTTTSSSS - // C is continue bit, TTT is type, S+ is length - function $header(byte) { - if (start === 0) start = position; - type = byte >> 4 & 0x07; - length = byte & 0x0f; - if (byte & 0x80) { - offset = 4; - return $header2; - } - return afterHeader(); - } - - // Second state in the same header parsing. - // CSSSSSSS* - function $header2(byte) { - length |= (byte & 0x7f) << offset; - if (byte & 0x80) { - offset += 7; - return $header2; - } - return afterHeader(); - } - - // Common helper for finishing tiny and normal headers. - function afterHeader() { - offset = 0; - if (type === 6) { - ref = 0; - return $ofsDelta; - } - if (type === 7) { - ref = ""; - return $refDelta; - } - return $body; - } - - // Big-endian modified base 128 number encoded ref offset - function $ofsDelta(byte) { - ref = byte & 0x7f; - if (byte & 0x80) return $ofsDelta2; - return $body; - } - - function $ofsDelta2(byte) { - ref = ((ref + 1) << 7) | (byte & 0x7f); - if (byte & 0x80) return $ofsDelta2; - return $body; - } - - // 20 byte raw sha1 hash for ref - function $refDelta(byte) { - ref += toHex(byte); - if (++offset < 20) return $refDelta; - return $body; - } - - // Common helper for generating 2-character hex numbers - function toHex(num) { - return num < 0x10 ? "0" + num.toString(16) : num.toString(16); - } - - // Common helper for emitting all three object shapes - function emitObject() { - var item = { - type: numToType[type], - size: length, - body: bops.join(parts), - offset: start - }; - if (ref) item.ref = ref; - parts.length = 0; - start = 0; - offset = 0; - type = 0; - length = 0; - ref = null; - emit(item); - } - - // Feed the deflated code to the inflate engine - function $body(byte, i, chunk) { - if (inf.write(byte)) return $body; - var buf = inf.flush(); - if (buf.length !== length) throw new Error("Length mismatch, expected " + length + " got " + buf.length); - inf.recycle(); - if (buf.length) { - parts.push(buf); - } - emitObject(); - // If this was all the objects, start calculating the sha1sum - if (--num) return $header; - sha1sum.update(bops.subarray(chunk, 0, i + 1)); - return $checksum; - } - - // 20 byte checksum - function $checksum(byte) { - checksum += toHex(byte); - if (++offset < 20) return $checksum; - var actual = sha1sum.digest(); - if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum); - } - -} diff --git a/lib/parallel.js b/lib/parallel.js deleted file mode 100644 index adeb739..0000000 --- a/lib/parallel.js +++ /dev/null @@ -1,45 +0,0 @@ -module.exports = parallel; - -// Run several continuables in parallel. The results are stored in the same -// shape as the input continuables (array or object). -// Returns a new continuable or accepts a callback. -// This will bail on the first error and ignore all others after it. -function parallel(commands, callback) { - if (!callback) return parallel.bind(this, commands); - var results, length, left, i, done; - - // Handle array shapes - if (Array.isArray(commands)) { - left = length = commands.length; - results = new Array(left); - for (i = 0; i < length; i++) { - run(i, commands[i]); - } - } - - // Otherwise assume it's an object. - else { - var keys = Object.keys(commands); - left = length = keys.length; - results = {}; - for (i = 0; i < length; i++) { - var key = keys[i]; - run(key, commands[key]); - } - } - - // Common logic for both - function run(key, command) { - command(function (err, result) { - if (done) return; - if (err) { - done = true; - return callback(err); - } - results[key] = result; - if (--left) return; - done = true; - callback(null, results); - }); - } -} diff --git a/lib/parseascii.js b/lib/parseascii.js deleted file mode 100644 index 78f5eb5..0000000 --- a/lib/parseascii.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function parseAscii(buffer, start, end) { - var val = ""; - while (start < end) { - val += String.fromCharCode(buffer[start++]); - } - return val; -}; diff --git a/lib/parsedec.js b/lib/parsedec.js deleted file mode 100644 index e87151d..0000000 --- a/lib/parsedec.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function parseDec(buffer, start, end) { - var val = 0; - while (start < end) { - val = val * 10 + buffer[start++] - 0x30; - } - return val; -}; diff --git a/lib/parseoct.js b/lib/parseoct.js deleted file mode 100644 index d67d8d9..0000000 --- a/lib/parseoct.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function parseOct(buffer, start, end) { - var val = 0; - while (start < end) { - val = (val << 3) + buffer[start++] - 0x30; - } - return val; -}; diff --git a/lib/parsetohex.js b/lib/parsetohex.js deleted file mode 100644 index a2a02af..0000000 --- a/lib/parsetohex.js +++ /dev/null @@ -1,10 +0,0 @@ -var chars = "0123456789abcdef"; - -module.exports = function parseToHex(buffer, start, end) { - var val = ""; - while (start < end) { - var byte = buffer[start++]; - val += chars[byte >> 4] + chars[byte & 0xf]; - } - return val; -}; diff --git a/lib/pathcmp.js b/lib/pathcmp.js deleted file mode 100644 index bc3189d..0000000 --- a/lib/pathcmp.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = function pathCmp(oa, ob) { - var a = oa.name; - var b = ob.name; - a += "/"; b += "/"; - return a < b ? -1 : a > b ? 1 : 0; -}; diff --git a/lib/serial.js b/lib/serial.js deleted file mode 100644 index 390b97a..0000000 --- a/lib/serial.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = serial; - -// Run several continuables in serial. The results are stored in the same -// shape as the input continuables (array or object). -// Returns a new continuable or accepts a callback. -// This will bail on the first error. -function serial(commands, callback) { - if (!callback) return serial.bind(this, commands); - var results, keys, index = 0, length, key; - - if (Array.isArray(commands)) { - length = commands.length; - results = new Array(length); - } - else { - results = {}; - keys = Object.keys(commands); - length = keys.length; - } - - index = 0; - return runNext(); - - function runNext() { - if (index >= length) { - return callback(null, results); - } - key = keys ? keys[index] : index; - var command = commands[key]; - command(onResult); - } - - function onResult(err, result) { - if (err) return callback(err); - results[key] = result; - runNext(); - } -} \ No newline at end of file diff --git a/lib/sha1.js b/lib/sha1.js deleted file mode 100644 index 1438d5c..0000000 --- a/lib/sha1.js +++ /dev/null @@ -1,146 +0,0 @@ -var Array32 = typeof Uint32Array === "function" ? Uint32Array : Array; - -module.exports = function sha1(buffer) { - if (buffer === undefined) return create(); - var shasum = create(); - shasum.update(buffer); - return shasum.digest(); -}; - -// A streaming interface for when nothing is passed in. -function create() { - var h0 = 0x67452301; - var h1 = 0xEFCDAB89; - var h2 = 0x98BADCFE; - var h3 = 0x10325476; - var h4 = 0xC3D2E1F0; - // The first 64 bytes (16 words) is the data chunk - var block = new Array32(80), offset = 0, shift = 24; - var totalLength = 0; - - return { update: update, digest: digest }; - - // The user gave us more data. Store it! - function update(chunk) { - if (typeof chunk === "string") return updateString(chunk); - var length = chunk.length; - totalLength += length * 8; - for (var i = 0; i < length; i++) { - write(chunk[i]); - } - } - - function updateString(string) { - var length = string.length; - totalLength += length * 8; - for (var i = 0; i < length; i++) { - write(string.charCodeAt(i)); - } - } - - function write(byte) { - block[offset] |= (byte & 0xff) << shift; - if (shift) { - shift -= 8; - } - else { - offset++; - shift = 24; - } - if (offset === 16) processBlock(); - } - - // No more data will come, pad the block, process and return the result. - function digest() { - // Pad - write(0x80); - if (offset > 14 || (offset === 14 && shift < 24)) { - processBlock(); - } - offset = 14; - shift = 24; - - // 64-bit length big-endian - write(0x00); // numbers this big aren't accurate in javascript anyway - write(0x00); // ..So just hard-code to zero. - write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); - write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); - for (var s = 24; s >= 0; s -= 8) { - write(totalLength >> s); - } - - // At this point one last processBlock() should trigger and we can pull out the result. - return toHex(h0) - + toHex(h1) - + toHex(h2) - + toHex(h3) - + toHex(h4); - } - - // We have a full block to process. Let's do it! - function processBlock() { - // Extend the sixteen 32-bit words into eighty 32-bit words: - for (var i = 16; i < 80; i++) { - var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; - block[i] = (w << 1) | (w >>> 31); - } - - // log(block); - - // Initialize hash value for this chunk: - var a = h0; - var b = h1; - var c = h2; - var d = h3; - var e = h4; - var f, k; - - // Main loop: - for (i = 0; i < 80; i++) { - if (i < 20) { - f = d ^ (b & (c ^ d)); - k = 0x5A827999; - } - else if (i < 40) { - f = b ^ c ^ d; - k = 0x6ED9EBA1; - } - else if (i < 60) { - f = (b & c) | (d & (b | c)); - k = 0x8F1BBCDC; - } - else { - f = b ^ c ^ d; - k = 0xCA62C1D6; - } - var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); - e = d; - d = c; - c = (b << 30 | b >>> 2); - b = a; - a = temp; - } - - // Add this chunk's hash to result so far: - h0 = (h0 + a) | 0; - h1 = (h1 + b) | 0; - h2 = (h2 + c) | 0; - h3 = (h3 + d) | 0; - h4 = (h4 + e) | 0; - - // The block is now reusable. - offset = 0; - for (i = 0; i < 16; i++) { - block[i] = 0; - } - } - - function toHex(word) { - var hex = ""; - for (var i = 28; i >= 0; i -= 4) { - hex += ((word >> i) & 0xf).toString(16); - } - return hex; - } - -} diff --git a/lib/trace.js b/lib/trace.js deleted file mode 100644 index a5d3020..0000000 --- a/lib/trace.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = false; diff --git a/lib/tracedb.js b/lib/tracedb.js deleted file mode 100644 index 65da059..0000000 --- a/lib/tracedb.js +++ /dev/null @@ -1,52 +0,0 @@ -var trace = require('./trace.js'); - -module.exports = function (db) { - return { - get: wrap1("get", db.get), - set: wrap2("set", db.set), - has: wrap1("has", db.has), - del: wrap1("del", db.del), - keys: wrap1("keys", db.keys), - init: wrap0("init", db.init), - }; -}; - -function wrap0(type, fn) { - return zero; - function zero(callback) { - if (!callback) return zero.bind(this); - return fn.call(this, check); - function check(err) { - if (err) return callback(err); - trace(type, null); - return callback.apply(this, arguments); - } - } -} - -function wrap1(type, fn) { - return one; - function one(arg, callback) { - if (!callback) return one.bind(this, arg); - return fn.call(this, arg, check); - function check(err) { - if (err) return callback(err); - trace(type, null, arg); - return callback.apply(this, arguments); - } - } -} - -function wrap2(type, fn) { - return two; - function two(arg1, arg2, callback) { - if (!callback) return two.bind(this, arg1. arg2); - return fn.call(this, arg1, arg2, check); - function check(err) { - if (err) return callback(err); - trace(type, null, arg1); - return callback.apply(this, arguments); - } - } -} - diff --git a/lib/walk.js b/lib/walk.js deleted file mode 100644 index 677b9dc..0000000 --- a/lib/walk.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = function walk(seed, scan, loadKey, compare) { - var queue = [seed]; - var working = 0, error, cb; - return {read: read, abort: abort}; - - function read(callback) { - if (cb) return callback(new Error("Only one read at a time")); - if (working) { cb = callback; return; } - var item = queue.shift(); - if (!item) return callback(); - try { scan(item).forEach(onKey); } - catch (err) { return callback(err); } - return callback(null, item); - } - - function abort(callback) { return callback(); } - - function onError(err) { - if (cb) { - var callback = cb; cb = null; - return callback(err); - } - error = err; - } - - function onKey(key) { - working++; - loadKey(key, onItem); - } - - function onItem(err, item) { - working--; - if (err) return onError(err); - var index = queue.length; - while (index && compare(item, queue[index - 1])) index--; - queue.splice(index, 0, item); - if (!working && cb) { - var callback = cb; cb = null; - return read(callback); - } - } -}; \ No newline at end of file diff --git a/mixins/client.js b/mixins/client.js deleted file mode 100644 index 0910bd6..0000000 --- a/mixins/client.js +++ /dev/null @@ -1,180 +0,0 @@ -var pushToPull = require('push-to-pull'); -var parse = pushToPull(require('../lib/pack-codec.js').decodePack); -var agent = require('../lib/agent.js'); - -module.exports = function (repo) { - repo.fetchPack = fetchPack; - repo.sendPack = sendPack; -}; - -function fetchPack(remote, opts, callback) { - if (!callback) return fetchPack.bind(this, remote, opts); - var repo = this; - var db = repo.db; - var refs, branch, queue, ref, hash; - return remote.discover(onDiscover); - - function onDiscover(err, serverRefs, serverCaps) { - if (err) return callback(err); - refs = serverRefs; - opts.caps = processCaps(opts, serverCaps); - return processWants(refs, opts.want, onWants); - } - - function onWants(err, wants) { - if (err) return callback(err); - opts.wants = wants; - return remote.fetch(repo, opts, onPackStream); - } - - function onPackStream(err, raw) { - if (err) return callback(err); - if (!raw) return remote.close(onDone); - var packStream = parse(raw); - return repo.unpack(packStream, opts, onUnpack); - } - - function onUnpack(err) { - if (err) return callback(err); - return remote.close(onClose); - } - - function onClose(err) { - if (err) return callback(err); - queue = Object.keys(refs); - return next(); - } - - function next(err) { - if (err) return callback(err); - ref = queue.shift(); - if (!ref) return repo.setHead(branch, onDone); - if (ref === "HEAD" || /{}$/.test(ref)) return next(); - hash = refs[ref]; - if (!branch && (hash === refs.HEAD)) branch = ref.substr(11); - db.has(hash, onHas); - } - - function onHas(err, has) { - if (err) return callback(err); - if (!has) return next(); - return db.set(ref, hash + "\n", next); - } - - function onDone(err) { - if (err) return callback(err); - return callback(null, refs); - } - - function processCaps(opts, serverCaps) { - var caps = []; - if (serverCaps["ofs-delta"]) caps.push("ofs-delta"); - if (serverCaps["thin-pack"]) caps.push("thin-pack"); - if (opts.includeTag && serverCaps["include-tag"]) caps.push("include-tag"); - if ((opts.onProgress || opts.onError) && - (serverCaps["side-band-64k"] || serverCaps["side-band"])) { - caps.push(serverCaps["side-band-64k"] ? "side-band-64k" : "side-band"); - if (!opts.onProgress && serverCaps["no-progress"]) { - caps.push("no-progress"); - } - } - if (serverCaps.agent) caps.push("agent=" + agent); - return caps; - } - - function processWants(refs, filter, callback) { - if (filter === null || filter === undefined) { - return defaultWants(refs, callback); - } - filter = Array.isArray(filter) ? arrayFilter(filter) : - typeof filter === "function" ? filter = filter : - wantFilter(filter); - - var list = Object.keys(refs); - var wants = {}; - var ref, hash; - return shift(); - function shift() { - ref = list.shift(); - if (!ref) return callback(null, Object.keys(wants)); - hash = refs[ref]; - repo.resolve(ref, onResolve); - } - function onResolve(err, oldHash) { - // Skip refs we already have - if (hash === oldHash) return shift(); - filter(ref, onFilter); - } - function onFilter(err, want) { - if (err) return callback(err); - // Skip refs the user doesn't want - if (want) wants[hash] = true; - return shift(); - } - } - - function defaultWants(refs, callback) { - return repo.listRefs("refs/heads", onRefs); - - function onRefs(err, branches) { - if (err) return callback(err); - var wants = Object.keys(branches); - wants.unshift("HEAD"); - return processWants(refs, wants, callback); - } - } - -} - -function wantMatch(ref, want) { - if (want === "HEAD" || want === null || want === undefined) { - return ref === "HEAD"; - } - if (Object.prototype.toString.call(want) === '[object RegExp]') { - return want.test(ref); - } - if (typeof want === "boolean") return want; - if (typeof want !== "string") { - throw new TypeError("Invalid want type: " + typeof want); - } - return (/^refs\//.test(ref) && ref === want) || - (ref === "refs/heads/" + want) || - (ref === "refs/tags/" + want); -} - -function wantFilter(want) { - return filter; - function filter(ref, callback) { - var result; - try { - result = wantMatch(ref, want); - } - catch (err) { - return callback(err); - } - return callback(null, result); - } -} - -function arrayFilter(want) { - var length = want.length; - return filter; - function filter(ref, callback) { - var result; - try { - for (var i = 0; i < length; ++i) { - result = wantMatch(ref, want[i]); - if (result) break; - } - } - catch (err) { - return callback(err); - } - return callback(null, result); - } -} - -function sendPack(remote, opts, callback) { - if (!callback) return sendPack.bind(this, remote, opts); - throw "TODO: Implement repo.sendPack"; -} diff --git a/mixins/clone.js b/mixins/clone.js deleted file mode 100644 index 53b6524..0000000 --- a/mixins/clone.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (repo) { - // TODO: Implement clone -}; \ No newline at end of file diff --git a/mixins/objects.js b/mixins/objects.js deleted file mode 100644 index e2c545f..0000000 --- a/mixins/objects.js +++ /dev/null @@ -1,126 +0,0 @@ -var sha1 = require('../lib/sha1.js'); -var frame = require('../lib/frame.js'); -var deframe = require('../lib/deframe.js'); -var encoders = require('../lib/encoders.js'); -var decoders = require('../lib/decoders.js'); -var parseAscii = require('../lib/parseascii.js'); -var isHash = require('../lib/ishash.js'); - -// Add "objects" capabilities to a repo using db as storage. -module.exports = function (repo) { - - // Add Object store capability to the system - repo.load = load; // (hash-ish) -> object - repo.save = save; // (object) -> hash - repo.loadRaw = loadRaw; // (hash) -> buffer - repo.saveRaw = saveRaw; // (hash, buffer) - repo.has = has; // (hash) -> true or false - repo.loadAs = loadAs; // (type, hash-ish) -> value - repo.saveAs = saveAs; // (type, value) -> hash - repo.remove = remove; // (hash) - - // This is a fallback resolve in case there is no refs system installed. - if (!repo.resolve) repo.resolve = function (hash, callback) { - if (isHash(hash)) return callback(null, hash); - return callback(new Error("This repo only supports direct hashes")); - }; - -}; - -function load(hashish, callback) { - if (!callback) return load.bind(this, hashish); - var hash; - var repo = this; - var db = repo.db; - return repo.resolve(hashish, onHash); - - function onHash(err, result) { - if (result === undefined) return callback(err); - hash = result; - return db.get(hash, onBuffer); - } - - function onBuffer(err, buffer) { - if (buffer === undefined) return callback(err); - var type, object; - try { - if (sha1(buffer) !== hash) { - throw new Error("Hash checksum failed for " + hash); - } - var pair = deframe(buffer); - type = pair[0]; - buffer = pair[1]; - object = { - type: type, - body: decoders[type](buffer) - }; - } catch (err) { - if (err) return callback(err); - } - return callback(null, object, hash); - } -} - -function loadRaw(hash, callback) { - return this.db.get(hash, callback); -} - -function saveRaw(hash, buffer, callback) { - return this.db.set(hash, buffer, callback); -} - -function has(hash, callback) { - return this.db.has(hash, callback); -} - -function save(object, callback) { - if (!callback) return save.bind(this, object); - var buffer, hash; - var repo = this; - var db = repo.db; - try { - buffer = encoders[object.type](object.body); - buffer = frame(object.type, buffer); - hash = sha1(buffer); - } - catch (err) { - return callback(err); - } - return db.set(hash, buffer, onSave); - - function onSave(err) { - if (err) return callback(err); - return callback(null, hash); - } -} - -function remove(hash, callback) { - if (!callback) return remove.bind(this, hash); - if (!isHash(hash)) return callback(new Error("Invalid hash: " + hash)); - var repo = this; - var db = repo.db; - return db.del(hash, callback); -} - -function loadAs(type, hashish, callback) { - if (!callback) return loadAs.bind(this, type, hashish); - return this.load(hashish, onObject); - - function onObject(err, object, hash) { - if (object === undefined) return callback(err); - if (type === "text") { - type = "blob"; - object.body = parseAscii(object.body, 0, object.body.length); - } - if (object.type !== type) { - return new Error("Expected " + type + ", but found " + object.type); - } - return callback(null, object.body, hash); - } -} - -function saveAs(type, body, callback) { - if (!callback) return saveAs.bind(this, type, body); - if (type === "text") type = "blob"; - return this.save({ type: type, body: body }, callback); -} diff --git a/mixins/packops.js b/mixins/packops.js deleted file mode 100644 index e49b1c3..0000000 --- a/mixins/packops.js +++ /dev/null @@ -1,195 +0,0 @@ -var bops = require('bops'); -var deframe = require('../lib/deframe.js'); -var frame = require('../lib/frame.js'); -var sha1 = require('../lib/sha1.js'); -var applyDelta = require('../lib/apply-delta.js'); -var pushToPull = require('push-to-pull'); -var decodePack = require('../lib/pack-codec.js').decodePack; -var packFrame = require('../lib/pack-codec.js').packFrame; - -module.exports = function (repo) { - // packStream is a simple-stream containing raw packfile binary data - // opts can contain "onProgress" or "onError" hook functions. - // callback will be called with a list of all unpacked hashes on success. - repo.unpack = unpack; // (packStream, opts) -> hashes - - // hashes is an array of hashes to pack - // callback will be a simple-stream containing raw packfile binary data - repo.pack = pack; // (hashes, opts) -> packStream -}; - -function unpack(packStream, opts, callback) { - if (!callback) return unpack.bind(this, packStream, opts); - - packStream = pushToPull(decodePack)(packStream); - - var repo = this; - - var version, num, numDeltas = 0, count = 0, countDeltas = 0; - var done, startDeltaProgress = false; - - // hashes keyed by offset for ofs-delta resolving - var hashes = {}; - // key is hash, boolean is cached "has" value of true or false - var has = {}; - // key is hash we're waiting for, value is array of items that are waiting. - var pending = {}; - - return packStream.read(onStats); - - function onDone(err) { - if (done) return; - done = true; - if (err) return callback(err); - return callback(null, values(hashes)); - } - - function onStats(err, stats) { - if (err) return onDone(err); - version = stats.version; - num = stats.num; - packStream.read(onRead); - } - - function objectProgress(more) { - if (!more) startDeltaProgress = true; - var percent = Math.round(count / num * 100); - return opts.onProgress("Receiving objects: " + percent + "% (" + (count++) + "/" + num + ") " + (more ? "\r" : "\n")); - } - - function deltaProgress(more) { - if (!startDeltaProgress) return; - var percent = Math.round(countDeltas / numDeltas * 100); - return opts.onProgress("Applying deltas: " + percent + "% (" + (countDeltas++) + "/" + numDeltas + ") " + (more ? "\r" : "\n")); - } - - function onRead(err, item) { - if (err) return onDone(err); - if (opts.onProgress) objectProgress(item); - if (item === undefined) return onDone(); - if (item.size !== item.body.length) { - return onDone(new Error("Body size mismatch")); - } - if (item.type === "ofs-delta") { - numDeltas++; - item.ref = hashes[item.offset - item.ref]; - return resolveDelta(item); - } - if (item.type === "ref-delta") { - numDeltas++; - return checkDelta(item); - } - return saveValue(item); - } - - function resolveDelta(item) { - if (opts.onProgress) deltaProgress(); - return repo.loadRaw(item.ref, function (err, buffer) { - if (err) return onDone(err); - if (!buffer) return onDone(new Error("Missing base image at " + item.ref)); - var target = deframe(buffer); - item.type = target[0]; - item.body = applyDelta(item.body, target[1]); - return saveValue(item); - }); - } - - function checkDelta(item) { - var hasTarget = has[item.ref]; - if (hasTarget === true) return resolveDelta(item); - if (hasTarget === false) return enqueueDelta(item); - return repo.has(item.ref, function (err, value) { - if (err) return onDone(err); - has[item.ref] = value; - if (value) return resolveDelta(item); - return enqueueDelta(item); - }); - } - - function saveValue(item) { - var buffer = frame(item.type, item.body); - var hash = sha1(buffer); - hashes[item.offset] = hash; - has[hash] = true; - if (hash in pending) { - // I have yet to come across a pack stream that actually needs this. - // So I will only implement it when I have concrete data to test against. - console.error({ - list: pending[hash], - item: item - }); - throw "TODO: pending value was found, resolve it"; - } - return repo.saveRaw(hash, buffer, onSave); - } - - function onSave(err) { - if (err) return callback(err); - packStream.read(onRead); - } - - function enqueueDelta(item) { - var list = pending[item.ref]; - if (!list) pending[item.ref] = [item]; - else list.push(item); - packStream.read(onRead); - } - -} - -// TODO: Implement delta refs to reduce stream size -function pack(hashes, opts, callback) { - if (!callback) return pack.bind(this, hashes, opts); - var repo = this; - var sha1sum = sha1(); - var i = 0, first = true, done = false; - return callback(null, { read: read, abort: callback }); - - function read(callback) { - if (done) return callback(); - if (first) return readFirst(callback); - var hash = hashes[i++]; - if (hash === undefined) { - var sum = sha1sum.digest(); - done = true; - return callback(null, bops.from(sum, "hex")); - } - repo.loadRaw(hash, function (err, buffer) { - if (err) return callback(err); - if (!buffer) return callback(new Error("Missing hash: " + hash)); - // Reframe with pack format header - var pair = deframe(buffer); - packFrame(pair[0], pair[1], function (err, buffer) { - if (err) return callback(err); - sha1sum.update(buffer); - callback(null, buffer); - }); - }); - } - - function readFirst(callback) { - var length = hashes.length; - var chunk = bops.create([ - 0x50, 0x41, 0x43, 0x4b, // PACK - 0, 0, 0, 2, // version 2 - length >> 24, // Num of objects - (length >> 16) & 0xff, - (length >> 8) & 0xff, - length & 0xff - ]); - first = false; - sha1sum.update(chunk); - callback(null, chunk); - } -} - -function values(object) { - var keys = Object.keys(object); - var length = keys.length; - var out = new Array(length); - for (var i = 0; i < length; i++) { - out[i] = object[keys[i]]; - } - return out; -} - diff --git a/mixins/refs.js b/mixins/refs.js deleted file mode 100644 index af8cdde..0000000 --- a/mixins/refs.js +++ /dev/null @@ -1,153 +0,0 @@ -var isHash = require('../lib/ishash.js'); - -module.exports = function (repo) { - // Refs - repo.resolve = resolve; // (hash-ish) -> hash - repo.updateHead = updateHead; // (hash) - repo.getHead = getHead; // () -> ref - repo.setHead = setHead; // (ref) - repo.readRef = readRef; // (ref) -> hash - repo.createRef = createRef; // (ref, hash) - repo.updateRef = updateRef; // (ref, hash) - repo.deleteRef = deleteRef; // (ref) - repo.listRefs = listRefs; // (prefix) -> refs -}; - -function resolve(hashish, callback) { - if (!callback) return resolve.bind(this, hashish); - hashish = hashish.trim(); - var repo = this, db = repo.db; - if (isHash(hashish)) return callback(null, hashish); - if (hashish === "HEAD") return repo.getHead(onBranch); - if ((/^refs\//).test(hashish)) { - return db.get(hashish, checkBranch); - } - return checkBranch(); - - function onBranch(err, ref) { - if (err) return callback(err); - if (!ref) return callback(); - return repo.resolve(ref, callback); - } - - function checkBranch(err, hash) { - if (err && err.code !== "ENOENT") return callback(err); - if (hash) { - return repo.resolve(hash, callback); - } - return db.get("refs/heads/" + hashish, checkTag); - } - - function checkTag(err, hash) { - if (err && err.code !== "ENOENT") return callback(err); - if (hash) { - return repo.resolve(hash, callback); - } - return db.get("refs/tags/" + hashish, final); - } - - function final(err, hash) { - if (err) return callback(err); - if (hash) { - return repo.resolve(hash, callback); - } - err = new Error("ENOENT: Cannot find " + hashish); - err.code = "ENOENT"; - return callback(err); - } -} - -function updateHead(hash, callback) { - if (!callback) return updateHead.bind(this, hash); - var ref; - var repo = this, db = repo.db; - return this.getHead(onBranch); - - function onBranch(err, result) { - if (err) return callback(err); - if (result === undefined) { - return repo.setHead("master", function (err) { - if (err) return callback(err); - onBranch(err, "refs/heads/master"); - }); - } - ref = result; - return db.set(ref, hash + "\n", callback); - } -} - -function getHead(callback) { - if (!callback) return getHead.bind(this); - var repo = this, db = repo.db; - return db.get("HEAD", onRead); - - function onRead(err, ref) { - if (err) return callback(err); - if (!ref) return callback(); - var match = ref.match(/^ref: *(.*)/); - if (!match) return callback(new Error("Invalid HEAD")); - return callback(null, match[1]); - } -} - -function setHead(branchName, callback) { - if (!callback) return setHead.bind(this, branchName); - var ref = "refs/heads/" + branchName; - return this.db.set("HEAD", "ref: " + ref + "\n", callback); -} - -function readRef(ref, callback) { - if (!callback) return readRef.bind(this, ref); - return this.db.get(ref, function (err, result) { - if (err) return callback(err); - if (!result) return callback(); - return callback(null, result.trim()); - }); -} - -function createRef(ref, hash, callback) { - if (!callback) return createRef.bind(this, ref, hash); - // TODO: should we check to make sure it doesn't exist first? - return this.db.set(ref, hash + "\n", callback); -} - -function updateRef(ref, hash, callback) { - if (!callback) return updateRef.bind(this, ref, hash); - // TODO: should we check to make sure it does exist first? - return this.db.set(ref, hash + "\n", callback); -} - -function deleteRef(ref, callback) { - if (!callback) return deleteRef.bind(this, ref); - return this.db.del(ref, callback); -} - -function listRefs(prefix, callback) { - if (!callback) return listRefs.bind(this, prefix); - if (!prefix) prefix = "refs\/"; - else if (!/^refs\//.test(prefix)) { - return callback(new TypeError("Invalid prefix: " + prefix)); - } - var db = this.db; - var refs = {}; - return db.keys(prefix, onKeys); - - function onKeys(err, keys) { - if (err) return callback(err); - var left = keys.length, done = false; - if (!left) return callback(null, refs); - keys.forEach(function (key) { - db.get(key, function (err, value) { - if (done) return; - if (err) { - done = true; - return callback(err); - } - refs[key] = value.trim(); - if (--left) return; - done = true; - callback(null, refs); - }); - }); - } -} diff --git a/mixins/server.js b/mixins/server.js deleted file mode 100644 index 288fbab..0000000 --- a/mixins/server.js +++ /dev/null @@ -1,289 +0,0 @@ -var parallel = require('../lib/parallel.js'); -var map = require('../lib/map.js'); -var each = require('../lib/each.js'); - -var bops = { - join: require('bops/join.js') -}; - -module.exports = function (repo) { - repo.uploadPack = uploadPack; - repo.receivePack = receivePack; -}; - -function uploadPack(remote, opts, callback) { - if (!callback) return uploadPack.bind(this, remote, opts); - var repo = this, refs, wants = {}, haves = {}, clientCaps = {}; - var packQueue = []; - var queueBytes = 0; - var queueLimit = 0; - return parallel({ - head: repo.getHead(), - refs: getRefs() - }, onHeadRef); - - // The peeled value of a ref (that is "ref^{}") MUST be immediately after - // the ref itself, if presented. A conforming server MUST peel the ref if - // it’s an annotated tag. - function getRefs(callback) { - if (!callback) return getRefs; - var refs; - repo.listRefs(null, onRefs); - - function onRefs(err, result) { - if (err) return callback(err); - refs = result; - parallel(map(refs, function (hash) { - return repo.load(hash); - }), onValues); - } - - function onValues(err, values) { - each(values, function (value, name) { - if (value.type !== "tag") return; - refs[name + "^{}"] = value.body.object; - }); - callback(null, refs); - } - } - - function onHeadRef(err, result) { - if (err) return callback(err); - var head = result.head; - refs = result.refs; - - // The returned response is a pkt-line stream describing each ref and its - // current value. The stream MUST be sorted by name according to the C - // locale ordering. - var keys = Object.keys(refs).sort(); - var lines = keys.map(function (ref) { - return refs[ref] + " " + ref; - }); - - // If HEAD is a valid ref, HEAD MUST appear as the first advertised ref. - // If HEAD is not a valid ref, HEAD MUST NOT appear in the advertisement - // list at all, but other refs may still appear. - if (head) lines.unshift(refs[head] + " HEAD"); - - // The stream MUST include capability declarations behind a NUL on the - // first ref. - // TODO: add "multi_ack" once it's implemented - // TODO: add "multi_ack_detailed" once it's implemented - // TODO: add "shallow" once it's implemented - // TODO: add "include-tag" once it's implemented - // TODO: add "thin-pack" once it's implemented - lines[0] += "\0no-progress side-band side-band-64k ofs-delta"; - - // Server SHOULD terminate each non-flush line using LF ("\n") terminator; - // client MUST NOT complain if there is no terminator. - lines.forEach(function (line) { - remote.write(line, null); - }); - - remote.write(null, null); - remote.read(onWant); - } - - function onWant(err, line) { - if (line === undefined) return callback(err); - if (line === null) { - return remote.read(onHave); - } - var match = line.match(/^want ([0-9a-f]{40})(?: (.+))?\n?$/); - if (!match) { - return callback(new Error("Invalid want: " + line)); - } - var hash = match[1]; - if (match[2]) clientCaps = parseCaps(match[2]); - wants[hash] = true; - remote.read(onWant); - } - - function onHave(err, line) { - if (line === undefined) return callback(err); - var match = line.match(/^(done|have)(?: ([0-9a-f]{40}))?\n?$/); - if (!match) { - return callback(new Error("Unexpected have line: " + line)); - } - if (match[1] === "have") { - haves[match[2]] = true; - return remote.read(onHave); - } - if (Object.keys(haves).length) { - throw new Error("TODO: handle haves"); - } - remote.write("NAK\n", null); - walkRepo(repo, wants, haves, onHashes); - } - - function onHashes(err, hashes) { - if (err) return callback(err); - if (clientCaps["side-band-64k"]) queueLimit = 65519; - else if (clientCaps["size-band"]) queueLimit = 999; - repo.pack(hashes, opts, onPack); - } - - function flush(callback) { - if (!queueBytes) return callback(); - var chunk = bops.join(packQueue, queueBytes); - packQueue.length = 0; - queueBytes = 0; - remote.write(["pack", chunk], callback); - } - - function onPack(err, packStream) { - if (err) return callback(err); - onWrite(); - - function onRead(err, chunk) { - if (err) return callback(err); - if (chunk === undefined) return flush(onFlush); - if (!queueLimit) { - return remote.write(chunk, onWrite); - } - var length = chunk.length; - if (queueBytes + length <= queueLimit) { - packQueue.push(chunk); - queueBytes += length; - return onWrite(); - } - if (queueBytes) { - flush(function (err) { - if (err) return callback(err); - return onRead(null, chunk); - }); - } - remote.write(["pack", bops.subarray(chunk, 0, queueLimit)], function (err) { - if (err) return callback(err); - return onRead(null, bops.subarray(chunk, queueLimit)); - }); - } - function onWrite(err) { - if (err) return callback(err); - packStream.read(onRead); - } - } - - function onFlush(err) { - if (err) return callback(err); - if (queueLimit) remote.write(null, callback); - else callback(); - } - -} - -function receivePack(remote, opts, callback) { - if (!callback) return receivePack.bind(this, remote, opts); - var clientCaps = {}, changes = []; - var repo = this; - this.listRefs(null, function (err, refs) { - if (err) return callback(err); - Object.keys(refs).forEach(function (ref, i) { - var hash = refs[ref]; - var line = hash + " " + ref; - // TODO: Implement report-status below and add here - if (!i) line += "\0delete-refs ofs-delta"; - remote.write(line, null); - }); - remote.write(null, null); - remote.read(onLine); - }); - - function onLine(err, line) { - if (err) return callback(err); - if (line === null) { - if (changes.length) return repo.unpack(remote, opts, onUnpack); - return callback(null, changes); - } - var match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) ([^ ]+)(?: (.+))?\n?$/); - changes.push({ - oldHash: match[1], - newHash: match[2], - ref: match[3] - }); - if (match[4]) clientCaps = parseCaps(match[4]); - remote.read(onLine); - } - - function onUnpack(err) { - if (err) return callback(err); - var i = 0, change; - next(); - function next(err) { - if (err) return callback(err); - change = changes[i++]; - if (!change) return callback(err, changes); - if (change.oldHash === "0000000000000000000000000000000000000000") { - return repo.createRef(change.ref, change.newHash, next); - } - if (change.newHash === "0000000000000000000000000000000000000000") { - return repo.deleteRef(change.ref, next); - } - return repo.updateRef(change.ref, change.newHash, next); - } - } -} - -function parseCaps(line) { - var caps = {}; - line.split(" ").map(function (cap) { - var pair = cap.split("="); - caps[pair[0]] = pair[1] || true; - }); - return caps; -} - -// Calculate a list of hashes to be included in a pack file based on have and want lists. -// -function walkRepo(repo, wants, haves, callback) { - var hashes = {}; - var done = false; - var left = 0; - - function onDone(err) { - if (done) return; - done = true; - return callback(err, Object.keys(hashes)); - } - - var keys = Object.keys(wants); - if (!keys.length) return onDone(); - keys.forEach(walkCommit); - - function walkCommit(hash) { - if (done) return; - if (hash in hashes || hash in haves) return; - hashes[hash] = true; - left++; - repo.loadAs("commit", hash, function (err, commit) { - if (done) return; - if (err) return onDone(err); - if (!commit) return onDone(new Error("Missing Commit: " + hash)); - commit.parents.forEach(walkCommit); - walkTree(commit.tree); - if (!--left) return onDone(); - }); - } - - function walkTree(hash) { - if (done) return; - if (hash in hashes || hash in haves) return; - hashes[hash] = true; - left++; - repo.loadAs("tree", hash, function (err, tree) { - if (done) return; - if (err) return onDone(err); - if (tree === undefined) return onDone(new Error("Missing tree: " + hash)); - Object.keys(tree).forEach(function (name) { - if (done) return; - var item = tree[name]; - if (item.mode === 040000) walkTree(item.hash); - else { - if (item.hash in hashes || item.hash in haves) return; - hashes[item.hash] = true; - } - }); - if (!--left) return onDone(); - }); - } -} diff --git a/mixins/walkers.js b/mixins/walkers.js deleted file mode 100644 index 5373043..0000000 --- a/mixins/walkers.js +++ /dev/null @@ -1,88 +0,0 @@ -var walk = require('../lib/walk.js'); -var assertType = require('../lib/assert-type.js'); - -module.exports = function (repo) { - repo.logWalk = logWalk; // (hash-ish) => stream - repo.treeWalk = treeWalk; // (hash-ish) => stream -}; - -function logWalk(hashish, callback) { - if (!callback) return logWalk.bind(this, hashish); - var last, seen = {}; - var repo = this; - return repo.readRef("shallow", onShallow); - - function onShallow(err, shallow) { - last = shallow; - return repo.loadAs("commit", hashish, onLoad); - } - - function onLoad(err, commit, hash) { - if (commit === undefined) return callback(err); - commit.hash = hash; - seen[hash] = true; - return callback(null, walk(commit, scan, loadKey, compare)); - } - - function scan(commit) { - if (last === commit) return []; - return commit.parents.filter(function (hash) { - return !seen[hash]; - }); - } - - function loadKey(hash, callback) { - return repo.loadAs("commit", hash, function (err, commit) { - if (err) return callback(err); - commit.hash = hash; - if (hash === last) commit.last = true; - return callback(null, commit); - }); - } - -} - -function compare(commit, other) { - return commit.author.date < other.author.date; -} - -function treeWalk(hashish, callback) { - if (!callback) return treeWalk.bind(this, hashish); - var repo = this; - return repo.load(hashish, onLoad); - function onLoad(err, item, hash) { - if (err) return callback(err); - if (item.type === "commit") return repo.load(item.body.tree, onLoad); - item.hash = hash; - item.path = "/"; - return callback(null, walk(item, treeScan, treeLoadKey, treeCompare)); - } - - function treeLoadKey(entry, callback) { - return repo.load(entry.hash, function (err, object) { - if (err) return callback(err); - entry.type = object.type; - entry.body = object.body; - return callback(null, entry); - }); - } - -} - -function treeScan(object) { - if (object.type === "blob") return []; - assertType(object, "tree"); - return object.body.filter(function (entry) { - return entry.mode !== 0160000; - }).map(function (entry) { - var path = object.path + entry.name; - if (entry.mode === 040000) path += "/"; - entry.path = path; - return entry; - }); -} - -function treeCompare(first, second) { - return first.path < second.path; -} - diff --git a/package.json b/package.json index 303ce91..246d0ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.6.2", + "version": "0.7.0", "description": "Git Implemented in JavaScript", "main": "js-git.js", "repository": { @@ -8,10 +8,6 @@ "url": "git://github.com/creationix/js-git.git" }, "devDependencies": { - "git-fs-db": "~0.2.0", - "git-net": "~0.0.4", - "git-node-platform": "~0.1.4", - "gen-run": "~0.1.1" }, "keywords": [ "git", diff --git a/specs/high/db.md b/specs/high/db.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/high/fs.md b/specs/high/fs.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/high/index.md b/specs/high/index.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/high/proto.md b/specs/high/proto.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/high/trace.md b/specs/high/trace.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/bops.md b/specs/low/bops.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/continuable.md b/specs/low/continuable.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/deflate.md b/specs/low/deflate.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/http.md b/specs/low/http.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/inflate.md b/specs/low/inflate.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/sha1.md b/specs/low/sha1.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/simple-stream.md b/specs/low/simple-stream.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/ssh.md b/specs/low/ssh.md deleted file mode 100644 index e69de29..0000000 diff --git a/specs/low/tcp.md b/specs/low/tcp.md deleted file mode 100644 index e69de29..0000000 diff --git a/test/test-sha1.js b/test/test-sha1.js deleted file mode 100644 index 7c30561..0000000 --- a/test/test-sha1.js +++ /dev/null @@ -1,51 +0,0 @@ -var sha1 = require('../lib/sha1.js'); - -var tests = [ - "", "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "a", "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8", - "abc", "a9993e364706816aba3e25717850c26c9cd0d89d", - "message digest", "c12252ceda8be8994d5fa0290a47231c1d16aae3", - "abcdefghijklmnopqrstuvwxyz", "32d10c7b8cf96570ca04ce37f2a19d84240d3a89", - "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", - "84983e441c3bd26ebaae4aa1f95129e5e54670f1", - "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabc", - "a6319f25020d5ff8722d40ae750dbab67d94fe4f", - "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZab", - "edb3a03256d1c6d148034ec4795181931c933f46", - "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZa", - "677734f7bf40b2b244cae100bf365598fbf4741d", -]; - -for (var i = 0; i < tests.length; i += 2) { - var input = tests[i]; - console.log("\n" + JSON.stringify(input)); - var expectedHex = tests[i + 1]; - console.log(expectedHex); - var hash = sha1(input); - console.log(hash); - if (hash !== expectedHex) { - throw new Error(hash + " != " + expectedHex + " for '" + input + "'"); - } - var sha1sum = sha1(); - for (var j = 0, l = input.length; j < l; j += 17) { - sha1sum.update(input.substr(j, 17)); - } - hash = sha1sum.digest(); - console.log(hash); - if (hash !== expectedHex) { - throw new Error(hash + " != " + expectedHex + " for '" + input + "'"); - } -} - -console.log("\n1,000,000 repetitions of the character 'a'"); -var expectedHex = "34aa973cd4c4daa4f61eeb2bdbad27316534016f"; -console.log(expectedHex); -var sha1sum = sha1(); -for (var i = 0; i < 100000; i++) { - sha1sum.update("aaaaaaaaaa"); -} -var hash = sha1sum.digest(); -console.log(hash); -if (hash !== expectedHex) { - throw new Error(hash + " != " + expectedHex + " for '" + input + "'"); -} From 4709775dfd4d350d0fc2c1f1a606310367f94ae8 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 8 Feb 2014 16:52:17 -0600 Subject: [PATCH 081/256] Import code from tedit-app --- lib/binary.js | 237 ++++++++++++++++++++++ lib/config-codec.js | 49 +++++ lib/defer.js | 22 ++ lib/encoders.js | 272 +++++++++++++++++++++++++ lib/modes.js | 24 +++ lib/sha1.js | 151 ++++++++++++++ lib/xhr.js | 33 +++ mixins/add-cache.js | 46 +++++ mixins/create-tree.js | 145 ++++++++++++++ mixins/delay.js | 55 +++++ mixins/formats.js | 31 +++ mixins/github-db.js | 433 ++++++++++++++++++++++++++++++++++++++++ mixins/indexed-db.js | 132 ++++++++++++ mixins/mem-db.js | 51 +++++ mixins/path-to-entry.js | 246 +++++++++++++++++++++++ mixins/read-combiner.js | 32 +++ 16 files changed, 1959 insertions(+) create mode 100644 lib/binary.js create mode 100644 lib/config-codec.js create mode 100644 lib/defer.js create mode 100644 lib/encoders.js create mode 100644 lib/modes.js create mode 100644 lib/sha1.js create mode 100644 lib/xhr.js create mode 100644 mixins/add-cache.js create mode 100644 mixins/create-tree.js create mode 100644 mixins/delay.js create mode 100644 mixins/formats.js create mode 100644 mixins/github-db.js create mode 100644 mixins/indexed-db.js create mode 100644 mixins/mem-db.js create mode 100644 mixins/path-to-entry.js create mode 100644 mixins/read-combiner.js diff --git a/lib/binary.js b/lib/binary.js new file mode 100644 index 0000000..4670034 --- /dev/null +++ b/lib/binary.js @@ -0,0 +1,237 @@ +/*global define, escape, unescape*/ + +// This file must be served with UTF-8 encoding for the utf8 codec to work. +define("js-git/lib/binary", function () { + "use strict"; + + return { + // Utility functions + isBinary: isBinary, + create: create, + join: join, + + // Binary input and output + copy: copy, + slice: slice, + + // String input and output + toRaw: toRaw, + fromRaw: fromRaw, + toUnicode: toUnicode, + fromUnicode: fromUnicode, + toHex: toHex, + fromHex: fromHex, + toBase64: toBase64, + fromBase64: fromBase64, + + // Array input and output + toArray: toArray, + fromArray: fromArray, + + // Raw <-> Hex-encoded codec + decodeHex: decodeHex, + encodeHex: encodeHex, + + decodeBase64: decodeBase64, + encodeBase64: encodeBase64, + + // Unicode <-> Utf8-encoded-raw codec + encodeUtf8: encodeUtf8, + decodeUtf8: decodeUtf8, + + // Hex <-> Nibble codec + nibbleToCode: nibbleToCode, + codeToNibble: codeToNibble + }; + + function isBinary(value) { + return value + && typeof value === "object" + && value.constructor.name === "Uint8Array"; + } + + function create(length) { + return new Uint8Array(length); + } + + function join(chunks) { + var length = chunks.length; + var total = 0; + for (var i = 0; i < length; i++) { + total += chunks[i].length; + } + var binary = create(total); + var offset = 0; + for (i = 0; i < length; i++) { + var chunk = chunks[i]; + copy(chunk, binary, offset); + offset += chunk.length; + } + return binary; + } + + function slice(binary, start, end) { + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + return binary.subarray(start, end); + } + + function copy(source, binary, offset) { + var length = source.length; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + for (var i = 0; i < length; i++) { + binary[i + offset] = source[i]; + } + return binary; + } + + // Like slice, but encode as a hex string + function toHex(binary, start, end) { + var hex = ""; + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + for (var i = start; i < end; i++) { + var byte = binary[i]; + hex += String.fromCharCode(nibbleToCode(byte >> 4)) + + String.fromCharCode(nibbleToCode(byte & 0xf)); + } + return hex; + } + + // Like copy, but decode from a hex string + function fromHex(hex, binary, offset) { + var length = hex.length / 2; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + var j = 0; + for (var i = 0; i < length; i++) { + binary[offset + i] = (codeToNibble(hex.charCodeAt(j++)) << 4) + | codeToNibble(hex.charCodeAt(j++)); + } + return binary; + } + + function toBase64(binary, start, end) { + return btoa(toRaw(binary, start, end)); + } + + function fromBase64(base64, binary, offset) { + return fromRaw(atob(base64), binary, offset); + } + + function nibbleToCode(nibble) { + nibble |= 0; + return (nibble + (nibble < 10 ? 0x30 : 0x57))|0; + } + + function codeToNibble(code) { + code |= 0; + return (code - ((code & 0x40) ? 0x57 : 0x30))|0; + } + + function toUnicode(binary, start, end) { + return decodeUtf8(toRaw(binary, start, end)); + } + + function fromUnicode(unicode, binary, offset) { + return fromRaw(encodeUtf8(unicode), binary, offset); + } + + function decodeHex(hex) { + var j = 0, l = hex.length; + var raw = ""; + while (j < l) { + raw += String.fromCharCode( + (codeToNibble(hex.charCodeAt(j++)) << 4) + | codeToNibble(hex.charCodeAt(j++)) + ); + } + return raw; + } + + function encodeHex(raw) { + var hex = ""; + var length = raw.length; + for (var i = 0; i < length; i++) { + var byte = raw.charCodeAt(i); + hex += String.fromCharCode(nibbleToCode(byte >> 4)) + + String.fromCharCode(nibbleToCode(byte & 0xf)); + } + return hex; + } + + function decodeBase64(base64) { + return atob(base64); + } + + function encodeBase64(raw) { + return btoa(raw); + } + + function decodeUtf8(utf8) { + return decodeURIComponent(escape(utf8)); + } + + function encodeUtf8(unicode) { + return unescape(encodeURIComponent(unicode)); + } + + function toRaw(binary, start, end) { + var raw = ""; + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + for (var i = start; i < end; i++) { + raw += String.fromCharCode(binary[i]); + } + return raw; + } + + function fromRaw(raw, binary, offset) { + var length = raw.length; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + for (var i = 0; i < length; i++) { + binary[offset + i] = raw.charCodeAt(i); + } + return binary; + } + + function toArray(binary, start, end) { + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + var length = end - start; + var array = new Array(length); + for (var i = 0; i < length; i++) { + array[i] = binary[i + start]; + } + return array; + } + + function fromArray(array, binary, offset) { + var length = array.length; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + for (var i = 0; i < length; i++) { + binary[offset + i] = array[i]; + } + return binary; + } + +}); \ No newline at end of file diff --git a/lib/config-codec.js b/lib/config-codec.js new file mode 100644 index 0000000..40df004 --- /dev/null +++ b/lib/config-codec.js @@ -0,0 +1,49 @@ +/*global define*/ +define("js-git/lib/config-codec", function () { + + return { + encode: encode, + parse: parse + }; + + function encode(config) { + var lines = []; + Object.keys(config).forEach(function (type) { + var obj = config[type]; + Object.keys(obj).forEach(function (name) { + var item = obj[name]; + lines.push('[' + type + ' "' + name + '"]'); + Object.keys(item).forEach(function (key) { + var value = item[key]; + lines.push("\t" + key + " = " + value); + }); + lines.push(""); + }); + }); + return lines.join("\n"); + } + + function parse(text) { + var config = {}; + var match, offset = 0; + while (match = text.substr(offset).match(/\[([a-z]*) "([^"]*)"\]([^\[]*)/)) { + var type = match[1]; + var section = config[type] || (config[type] = {}); + var name = match[2]; + section[name] = parseBody(match[3]); + offset += match[0].length; + } + return config; + } + + function parseBody(text) { + var entry = {}; + var match, offset = 0; + while (match = text.substr(offset).match(/([^ \t\r\n]*) *= *([^ \t\r\n]*)/)) { + entry[match[1]] = match[2]; + offset += match[0].length; + } + return entry; + } + +}); \ No newline at end of file diff --git a/lib/defer.js b/lib/defer.js new file mode 100644 index 0000000..2a900d2 --- /dev/null +++ b/lib/defer.js @@ -0,0 +1,22 @@ +/*global define*/ +define("js-git/lib/defer", function () { + var timeouts = []; + var messageName = "zero-timeout-message"; + + function handleMessage(event) { + if (event.source == window && event.data == messageName) { + event.stopPropagation(); + if (timeouts.length > 0) { + var fn = timeouts.shift(); + fn(); + } + } + } + + window.addEventListener("message", handleMessage, true); + + return function (fn) { + timeouts.push(fn); + window.postMessage(messageName, "*"); + }; +}); \ No newline at end of file diff --git a/lib/encoders.js b/lib/encoders.js new file mode 100644 index 0000000..064a7ec --- /dev/null +++ b/lib/encoders.js @@ -0,0 +1,272 @@ +/*global define*/ +define("js-git/lib/encoders", function () { + var sha1 = require('js-git/lib/sha1'); + var binary = require('js-git/lib/binary'); + + // Run sanity tests at startup. + test(); + + return { + frame: frame, + normalizeAs: normalizeAs, + normalizeBlob: normalizeBlob, + normalizeTree: normalizeTree, + normalizeCommit: normalizeCommit, + normalizeTag: normalizeTag, + encodeAs: encodeAs, + encodeBlob: encodeBlob, + encodeTree: encodeTree, + encodeCommit: encodeCommit, + encodeTag: encodeTag, + hashAs: hashAs, + pathCmp: pathCmp + }; + + function test() { + // Test blob encoding + var normalized = normalizeBlob("Hello World\n"); + var expected = "557db03de997c86a4a028e1ebd3a1ceb225be238"; + var hash = hashAs("blob", normalized); + if (hash !== expected) { + console.log({expected: expected, actual: hash, normalized: normalized}); + throw new Error("Invalid body hash"); + } + + // Test tree encoding + hash = hashAs("tree", normalizeTree({ "greeting.txt": { mode: 0100644, hash: hash } })); + if (hash !== "648fc86e8557bdabbc2c828a19535f833727fa62") { + throw new Error("Invalid tree hash"); + } + + var date = new Date(1391790884000); + date.timezoneOffset = 7 * 60; + // Test commit encoding + hash = hashAs("commit", normalizeCommit({ + tree: hash, + author: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date + }, + message: "Test Commit\n" + })); + if (hash !== "500c37fc17988b90c82d812a2d6fc25b15354bf2") { + throw new Error("Invalid commit hash"); + } + + // Test annotated tag encoding + date = new Date(1391790910000); + date.timezoneOffset = 7 * 60; + hash = hashAs("tag", normalizeTag({ + object: hash, + type: "commit", + tag: "mytag", + tagger: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date, + }, + message: "Tag it!\n" + })); + if (hash !== "49522787662a0183652dc9cafa5c008b5a0e0c2a") { + throw new Error("Invalid annotated tag hash"); + } + } + + function encodeAs(type, body) { + if (type === "blob") return encodeBlob(body); + if (type === "tree") return encodeTree(body); + if (type === "commit") return encodeCommit(body); + if (type === "tag") return encodeTag(body); + } + + function normalizeAs(type, body) { + if (type === "blob") return normalizeBlob(body); + if (type === "tree") return normalizeTree(body); + if (type === "commit") return normalizeCommit(body); + if (type === "tag") return normalizeTag(body); + } + + // Calculate a git compatable hash by git encoding the body and prepending a + // git style frame header and calculating the sha1 sum of that. + function hashAs(type, body) { + var encoded = encodeAs(type, body); + var sum = sha1(); + sum.update(frame(type, encoded.length)); + sum.update(encoded); + return sum.digest(); + } + + function frame(type, length) { + return type + " " + length + "\0"; + } + + function normalizeBlob(body) { + var type = typeof body; + if (type === "string") { + return binary.fromRaw(body); + } + if (body && type === "object") { + if (body.constructor.name === "ArrayBuffer") body = new Uint8Array(body); + if (typeof body.length === "number") { + return body;//binary.toRaw(body); + } + } + throw new TypeError("Blob body must be raw string, ArrayBuffer or byte array"); + } + + function encodeBlob(body) { + return body; + } + + function normalizeTree(body) { + var type = body && typeof body; + if (type !== "object") { + throw new TypeError("Tree body must be array or object"); + } + var tree = {}, i, l, entry; + // If array form is passed in, convert to object form. + if (Array.isArray(body)) { + for (i = 0, l = body.length; i < l; i++) { + entry = body[i]; + tree[entry.name] = { + mode: entry.mode, + hash: entry.hash + }; + } + } + else { + var names = Object.keys(body); + for (i = 0, l = names.length; i < l; i++) { + var name = names[i]; + entry = body[name]; + tree[name] = { + mode: entry.mode, + hash: entry.hash + }; + } + } + return tree; + } + + function encodeTree(body) { + var tree = ""; + var names = Object.keys(body).sort(pathCmp); + for (var i = 0, l = names.length; i < l; i++) { + var name = names[i]; + var entry = body[name]; + tree += entry.mode.toString(8) + " " + name + + "\0" + binary.decodeHex(entry.hash); + } + return tree; + } + + function normalizeCommit(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Commit body must be an object"); + } + if (!(body.tree && body.author && body.message)) { + throw new TypeError("Tree, author, and message are required for commits"); + } + var parents = body.parents || (body.parent ? [ body.parent ] : []); + if (!Array.isArray(parents)) { + throw new TypeError("Parents must be an array"); + } + var author = normalizePerson(body.author); + var committer = body.committer ? normalizePerson(body.committer) : author; + return { + tree: body.tree, + parents: parents, + author: author, + committer: committer, + message: body.message + }; + } + + function encodeCommit(body) { + var str = "tree " + body.tree; + for (var i = 0, l = body.parents.length; i < l; ++i) { + str += "\nparent " + body.parents[i]; + } + str += "\nauthor " + formatPerson(body.author) + + "\ncommitter " + formatPerson(body.committer) + + "\n\n" + body.message; + return binary.encodeUtf8(str); + } + + function normalizeTag(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Tag body must be an object"); + } + if (!(body.object && body.type && body.tag && body.tagger && body.message)) { + throw new TypeError("Object, type, tag, tagger, and message required"); + } + return { + object: body.object, + type: body.type, + tag: body.tag, + tagger: normalizePerson(body.tagger), + message: body.message + }; + } + + function encodeTag(body) { + var str = "object " + body.object + + "\ntype " + body.type + + "\ntag " + body.tag + + "\ntagger " + formatPerson(body.tagger) + + "\n\n" + body.message; + return binary.encodeUtf8(str); + } + + // Tree entries are sorted by the byte sequence that comprises + // the entry name. However, for the purposes of the sort + // comparison, entries for tree objects are compared as if the + // entry name byte sequence has a trailing ASCII '/' (0x2f). + function pathCmp(a, b) { + // TODO: this spec seems to be wrong. It doesn't match the sort order used + // by real git. + // a = binary.encodeUtf8(a) + "/"; + // b = binary.encodeUtf8(b) + "/"; + return a < b ? -1 : a > b ? 1 : 0; + } + + function normalizePerson(person) { + if (!person || typeof person !== "object") { + throw new TypeError("Person must be an object"); + } + if (!person.name || !person.email) { + throw new TypeError("Name and email are required for person fields"); + } + return { + name: person.name, + email: person.email, + date: person.date || new Date() + }; + } + + function formatPerson(person) { + return safe(person.name) + + " <" + safe(person.email) + "> " + + formatDate(person.date); + } + + function safe(string) { + return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); + } + + function two(num) { + return (num < 10 ? "0" : "") + num; + } + + function formatDate(date) { + var offset = date.timezoneOffset || date.getTimezoneOffset(); + var neg = "+"; + if (offset <= 0) offset = -offset; + else neg = "-"; + offset = neg + two(Math.floor(offset / 60)) + two(offset % 60); + var seconds = Math.floor(date.getTime() / 1000); + return seconds + " " + offset; + } + +}); \ No newline at end of file diff --git a/lib/modes.js b/lib/modes.js new file mode 100644 index 0000000..2288516 --- /dev/null +++ b/lib/modes.js @@ -0,0 +1,24 @@ +/*global define*/ +define("js-git/lib/modes", function () { + // Not strict mode because it uses octal literals all over. + return { + isBlob: function (mode) { + return (mode & 0140000) === 0100000; + }, + isFile: function (mode) { + return (mode & 0160000) === 0100000; + }, + toType: function (mode) { + if (mode === 0160000) return "commit"; + if (mode === 040000) return "tree"; + if ((mode & 0140000) === 0100000) return "blob"; + return "unknown"; + }, + tree: 040000, + blob: 0100644, + file: 0100644, + exec: 0100755, + sym: 0120000, + commit: 0160000 + }; +}); \ No newline at end of file diff --git a/lib/sha1.js b/lib/sha1.js new file mode 100644 index 0000000..a5c9d24 --- /dev/null +++ b/lib/sha1.js @@ -0,0 +1,151 @@ +/*global define*/ +define("js-git/lib/sha1", function () { + "use strict"; + + // Input chunks must be either arrays of bytes or "raw" encoded strings + return function sha1(buffer) { + if (buffer === undefined) return create(); + var shasum = create(); + shasum.update(buffer); + return shasum.digest(); + }; + + // A streaming interface for when nothing is passed in. + function create() { + var h0 = 0x67452301; + var h1 = 0xEFCDAB89; + var h2 = 0x98BADCFE; + var h3 = 0x10325476; + var h4 = 0xC3D2E1F0; + // The first 64 bytes (16 words) is the data chunk + var block = new Uint32Array(80), offset = 0, shift = 24; + var totalLength = 0; + + return { update: update, digest: digest }; + + // The user gave us more data. Store it! + function update(chunk) { + if (typeof chunk === "string") return updateString(chunk); + var length = chunk.length; + totalLength += length * 8; + for (var i = 0; i < length; i++) { + write(chunk[i]); + } + } + + function updateString(string) { + var length = string.length; + totalLength += length * 8; + for (var i = 0; i < length; i++) { + write(string.charCodeAt(i)); + } + } + + function write(byte) { + block[offset] |= (byte & 0xff) << shift; + if (shift) { + shift -= 8; + } + else { + offset++; + shift = 24; + } + if (offset === 16) processBlock(); + } + + // No more data will come, pad the block, process and return the result. + function digest() { + // Pad + write(0x80); + if (offset > 14 || (offset === 14 && shift < 24)) { + processBlock(); + } + offset = 14; + shift = 24; + + // 64-bit length big-endian + write(0x00); // numbers this big aren't accurate in javascript anyway + write(0x00); // ..So just hard-code to zero. + write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); + write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); + for (var s = 24; s >= 0; s -= 8) { + write(totalLength >> s); + } + + // At this point one last processBlock() should trigger and we can pull out the result. + return toHex(h0) + + toHex(h1) + + toHex(h2) + + toHex(h3) + + toHex(h4); + } + + // We have a full block to process. Let's do it! + function processBlock() { + // Extend the sixteen 32-bit words into eighty 32-bit words: + for (var i = 16; i < 80; i++) { + var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; + block[i] = (w << 1) | (w >>> 31); + } + + // log(block); + + // Initialize hash value for this chunk: + var a = h0; + var b = h1; + var c = h2; + var d = h3; + var e = h4; + var f, k; + + // Main loop: + for (i = 0; i < 80; i++) { + if (i < 20) { + f = d ^ (b & (c ^ d)); + k = 0x5A827999; + } + else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (i < 60) { + f = (b & c) | (d & (b | c)); + k = 0x8F1BBCDC; + } + else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); + e = d; + d = c; + c = (b << 30 | b >>> 2); + b = a; + a = temp; + } + + // Add this chunk's hash to result so far: + h0 = (h0 + a) | 0; + h1 = (h1 + b) | 0; + h2 = (h2 + c) | 0; + h3 = (h3 + d) | 0; + h4 = (h4 + e) | 0; + + // The block is now reusable. + offset = 0; + for (i = 0; i < 16; i++) { + block[i] = 0; + } + } + + function toHex(word) { + var hex = ""; + for (var i = 28; i >= 0; i -= 4) { + hex += ((word >> i) & 0xf).toString(16); + } + return hex; + } + + } + +}); \ No newline at end of file diff --git a/lib/xhr.js b/lib/xhr.js new file mode 100644 index 0000000..03247b2 --- /dev/null +++ b/lib/xhr.js @@ -0,0 +1,33 @@ +/*global define*/ +define("js-git/lib/xhr", function () { + return function (root, accessToken) { + return function request(method, url, body, callback) { + if (typeof body === "function" && callback === undefined) { + callback = body; + body = undefined; + } + url = url.replace(":root", root); + if (!callback) return request.bind(this, accessToken, method, url, body); + var json; + var xhr = new XMLHttpRequest(); + xhr.open(method, 'https://api.github.com' + url, true); + xhr.setRequestHeader("Authorization", "token " + accessToken); + if (body) { + xhr.setRequestHeader("Content-Type", "application/json"); + try { json = JSON.stringify(body); } + catch (err) { return callback(err); } + } + xhr.onreadystatechange = onReadyStateChange; + xhr.send(json); + function onReadyStateChange() { + if (xhr.readyState !== 4) return; + var response; + if (xhr.responseText) { + try { response = JSON.parse(xhr.responseText); } + catch (err) { return callback(err, null, xhr, response); } + } + return callback(null, xhr, response); + } + }; + }; +}); \ No newline at end of file diff --git a/mixins/add-cache.js b/mixins/add-cache.js new file mode 100644 index 0000000..a7941a4 --- /dev/null +++ b/mixins/add-cache.js @@ -0,0 +1,46 @@ +/*global define*/ +define("js-git/mixins/add-cache", function () { + + return addCache; + function addCache(repo, cache) { + var loadAs = repo.loadAs; + if (loadAs) repo.loadAs = loadAsCached; + var saveAs = repo.saveAs; + if (saveAs) repo.saveAs = saveAsCached; + var createTree = repo.createTree; + if (createTree) repo.createTree = createTreeCached; + + function loadAsCached(type, hash, callback) { + if (!callback) return loadAsCached.bind(this, type, hash); + cache.loadAs(type, hash, function (err, value) { + if (err) return callback(err); + if (value !== undefined) { + return callback(null, value, hash); + } + loadAs.call(repo, type, hash, function (err, value) { + if (err) return callback(err); + cache.saveAs(type, value, function (err) { + if (err) return callback(err); + callback(null, value, hash); + }, hash); + }); + }); + } + + function saveAsCached(type, value, callback) { + saveAs.call(repo, type, value, function (err, hash) { + if (err) return callback(err); + cache.saveAs(type, value, callback, hash); + }); + } + + function createTreeCached(entries, callback) { + createTree.call(repo, entries, function (err, hash, tree) { + if (err) return callback(err); + cache.saveAs("tree", tree, callback, hash); + }); + } + + } + +}); diff --git a/mixins/create-tree.js b/mixins/create-tree.js new file mode 100644 index 0000000..823f310 --- /dev/null +++ b/mixins/create-tree.js @@ -0,0 +1,145 @@ +/*global define*/ +define("js-git/mixins/create-tree", function () { + "use strict"; + + var modes = require('js-git/lib/modes'); + + return function (repo) { + repo.createTree = createTree; + + function createTree(entries, callback) { + if (!callback) return createTree.bind(null, entries); + callback = singleCall(callback); + + // Tree paths that we need loaded + var toLoad = {}; + function markTree(path) { + while(true) { + if (toLoad[path]) return; + toLoad[path] = true; + trees[path] = { + add: [], + del: [], + tree: {} + }; + if (!path) break; + path = path.substring(0, path.lastIndexOf("/")); + } + } + + // Commands to run organized by tree path + var trees = {}; + + // Counter for parallel I/O operations + var left = 1; // One extra counter to protect again zalgo cache callbacks. + + // First pass, stubs out the trees structure, sorts adds from deletes, + // and saves any inline content blobs. + entries.forEach(function (entry) { + var index = entry.path.lastIndexOf("/"); + var parentPath = entry.path.substr(0, index); + var name = entry.path.substr(index + 1); + markTree(parentPath); + var tree = trees[parentPath]; + var adds = tree.add; + var dels = tree.del; + + if (!entry.mode) { + dels.push(name); + return; + } + var add = { + name: name, + mode: entry.mode, + hash: entry.hash + }; + adds.push(add); + if (entry.hash) return; + left++; + repo.saveAs("blob", entry.content, function (err, hash) { + if (err) return callback(err); + add.hash = hash; + check(); + }); + }); + + // Preload the base trees + if (entries.base) loadTree("", entries.base); + + // Check just in case there was no IO to perform + check(); + + function loadTree(path, hash) { + left++; + delete toLoad[path]; + repo.loadAs("tree", hash, function (err, tree) { + if (err) return callback(err); + trees[path].tree = tree; + Object.keys(tree).forEach(function (name) { + var childPath = path ? path + "/" + name : name; + if (toLoad[childPath]) loadTree(childPath, tree[name].hash); + }); + check(); + }); + } + + function check() { + if (--left) return; + findLeaves().forEach(processLeaf); + } + + function processLeaf(path) { + var entry = trees[path]; + delete trees[path]; + var tree = entry.tree; + entry.del.forEach(function (name) { + delete tree[name]; + }); + entry.add.forEach(function (item) { + tree[item.name] = { + mode: item.mode, + hash: item.hash + }; + }); + left++; + repo.saveAs("tree", tree, function (err, hash, tree) { + if (err) return callback(err); + if (!path) return callback(null, hash, tree); + var index = path.lastIndexOf("/"); + var parentPath = path.substring(0, index); + var name = path.substring(index + 1); + trees[parentPath].add.push({ + name: name, + mode: modes.tree, + hash: hash + }); + if (--left) return; + findLeaves().forEach(processLeaf); + }); + } + + function findLeaves() { + var paths = Object.keys(trees); + var parents = {}; + paths.forEach(function (path) { + if (!path) return; + var parent = path.substring(0, path.lastIndexOf("/")); + parents[parent] = true; + }); + return paths.filter(function (path) { + return !parents[path]; + }); + } + } + }; + + function singleCall(callback) { + var done = false; + return function () { + if (done) return console.warn("Discarding extra callback"); + done = true; + return callback.apply(this, arguments); + }; + } + +}); \ No newline at end of file diff --git a/mixins/delay.js b/mixins/delay.js new file mode 100644 index 0000000..63afc90 --- /dev/null +++ b/mixins/delay.js @@ -0,0 +1,55 @@ +/*global define*/ +define("js-git/mixins/delay", function () { + "use strict"; + + return function (repo, ms) { + var saveAs = repo.saveAs; + var loadAs = repo.loadAs; + var readRef = repo.readRef; + var updateRef = repo.updateRef; + var createTree = repo.createTree; + + repo.saveAs = saveAsDelayed; + repo.loadAs = loadASDelayed; + repo.readRef = readRefDelayed; + repo.updateRed = updateRefDelayed; + if (createTree) repo.createTree = createTreeDelayed; + + function saveAsDelayed(type, value, callback) { + if (!callback) return saveAsDelayed.bind(null, type, value); + setTimeout(function () { + return saveAs.call(repo, type, value, callback); + }, ms); + } + + function loadASDelayed(type, hash, callback) { + if (!callback) return loadASDelayed.bind(null, type, hash); + setTimeout(function () { + return loadAs.call(repo, type, hash, callback); + }, ms); + } + + function readRefDelayed(ref, callback) { + if (!callback) return readRefDelayed.bind(null, ref); + setTimeout(function () { + return readRef.call(repo, ref, callback); + }, ms); + } + + function updateRefDelayed(ref, hash, callback) { + if (!callback) return updateRefDelayed.bind(null, ref, hash); + setTimeout(function () { + return updateRef.call(repo, ref, hash, callback); + }, ms); + } + + function createTreeDelayed(entries, callback) { + if (!callback) return createTreeDelayed.bind(null, entries); + setTimeout(function () { + return createTree.call(repo, entries, callback); + }, ms); + } + + }; + +}); \ No newline at end of file diff --git a/mixins/formats.js b/mixins/formats.js new file mode 100644 index 0000000..6dd35cb --- /dev/null +++ b/mixins/formats.js @@ -0,0 +1,31 @@ +define("js-git/mixins/formats", function () { + + var binary = require('js-git/lib/binary'); + + return function (repo) { + var loadAs = repo.loadAs; + repo.loadAs = newLoadAs; + function newLoadAs(type, hash, callback) { + if (!callback) return newLoadAs.bind(this, type, hash, callback); + var realType = type === "text" ? "blob": + type === "array" ? "tree" : type; + return loadAs.call(this, realType, hash, onLoad); + + function onLoad(err, body, hash) { + if (body === undefined) return callback.call(this, err); + if (type === "text") body = binary.toUnicode(body); + if (type === "array") body = toArray(body); + return callback.call(this, err, body, hash); + } + } + }; + + function toArray(tree) { + return Object.keys(tree).map(function (name) { + var entry = tree[name]; + entry.name = name; + return entry; + }); + } + +}); diff --git a/mixins/github-db.js b/mixins/github-db.js new file mode 100644 index 0000000..3fa55ed --- /dev/null +++ b/mixins/github-db.js @@ -0,0 +1,433 @@ +/*global define*/ +define("js-git/mixins/github-db", function () { + "use strict"; + + var normalizeAs = require('js-git/lib/encoders').normalizeAs; + var modes = require('js-git/lib/modes'); + var hashAs = require('js-git/lib/encoders').hashAs; + var xhr = require('js-git/lib/xhr'); + var binary = require('js-git/lib/binary'); + + var modeToType = { + "040000": "tree", + "100644": "blob", // normal file + "100655": "blob", // executable file + "120000": "blob", // symlink + "160000": "commit" // gitlink + }; + + var encoders = { + commit: encodeCommit, + tag: encodeTag, + tree: encodeTree, + blob: encodeBlob + }; + + var decoders = { + commit: decodeCommit, + tag: decodeTag, + tree: decodeTree, + blob: decodeBlob, + }; + + var emptyBlob = hashAs("blob", ""); + var emptyTree = hashAs("tree", []); + + + // Implement the js-git object interface using github APIs + return function (repo, root, accessToken) { + + var apiRequest = xhr(root, accessToken); + + repo.loadAs = loadAs; // (type, hash) -> value, hash + repo.saveAs = saveAs; // (type, value) -> hash, value + repo.readRef = readRef; // (ref) -> hash + repo.updateRef = updateRef; // (ref, hash) -> hash + repo.createTree = createTree; // (entries) -> hash, tree + + function loadAs(type, hash, callback) { + if (!callback) return loadAs.bind(null, type, hash); + // Github doesn't like empty trees, but we know them already. + if (type === "tree" && hash === emptyTree) return callback(null, {}, hash); + apiRequest("GET", "/repos/:root/git/" + type + "s/" + hash, onValue); + + function onValue(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 500) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + } + if (xhr.status >= 300 && xhr.status < 500) return callback(); + var body; + try { body = decoders[type].call(repo, result); } + catch (err) { return callback(err); } + if (hashAs(type, body) !== hash) { + if (fixDate(type, body, hash)) console.warn(type + " repaired", hash); + else console.error("Unable to repair " + type, hash); + } + return callback(null, body, hash); + } + } + + function saveAs(type, body, callback) { + if (!callback) return saveAs.bind(null, type, body); + var request; + try { + body = normalizeAs(type, body); + request = encoders[type](body); + } + catch (err) { + return callback(err); + } + + // Github doesn't allow creating empty trees. + if (type === "tree" && request.tree.length === 0) { + return callback(null, emptyTree, body); + } + return apiRequest("POST", "/repos/:root/git/" + type + "s", request, onWrite); + + function onWrite(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + } + return callback(null, result.sha, body); + } + } + + // Create a tree with optional deep paths and create new blobs. + // Entries is an array of {mode, path, hash|content} + // Also deltas can be specified by setting entries.base to the hash of a tree + // in delta mode, entries can be removed by specifying just {path} + function createTree(entries, callback) { + if (!callback) return createTree.bind(null, entries); + var toDelete = entries.base && entries.filter(function (entry) { + return !entry.mode; + }).map(function (entry) { + return entry.path; + }); + if (toDelete && toDelete.length) return slowUpdateTree(entries, toDelete, callback); + return fastUpdateTree(entries, callback); + } + + function fastUpdateTree(entries, callback) { + var request = { tree: entries.map(mapTreeEntry) }; + if (entries.base) request.base_tree = entries.base; + + apiRequest("POST", "/repos/:root/git/trees", request, onWrite); + + function onWrite(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + } + return callback(null, result.sha, decoders.tree(result)); + } + } + + // Github doesn't support deleting entries via the createTree API, so we + // need to manually create those affected trees and modify the request. + function slowUpdateTree(entries, toDelete, callback) { + callback = singleCall(callback); + var root = entries.base; + + var left = 0; + + // Calculate trees that need to be re-built and save any provided content. + var parents = {}; + toDelete.forEach(function (path) { + var parentPath = path.substr(0, path.lastIndexOf("/")); + var parent = parents[parentPath] || (parents[parentPath] = { + add: {}, del: [] + }); + var name = path.substr(path.lastIndexOf("/") + 1); + parent.del.push(name); + }); + var other = entries.filter(function (entry) { + if (!entry.mode) return false; + var parentPath = entry.path.substr(0, entry.path.lastIndexOf("/")); + var parent = parents[parentPath]; + if (!parent) return true; + var name = entry.path.substr(entry.path.lastIndexOf("/") + 1); + if (entry.hash) { + parent.add[name] = { + mode: entry.mode, + hash: entry.hash + }; + return false; + } + left++; + repo.saveAs("blob", entry.content, function(err, hash) { + if (err) return callback(err); + parent.add[name] = { + mode: entry.mode, + hash: hash + }; + if (!--left) onParents(); + }); + return false; + }); + if (!left) onParents(); + + function onParents() { + Object.keys(parents).forEach(function (parentPath) { + left++; + // TODO: remove this dependency on pathToEntry + repo.pathToEntry(root, parentPath, function (err, entry) { + if (err) return callback(err); + var tree = entry.tree; + var commands = parents[parentPath]; + commands.del.forEach(function (name) { + delete tree[name]; + }); + for (var name in commands.add) { + tree[name] = commands.add[name]; + } + repo.saveAs("tree", tree, function (err, hash, tree) { + if (err) return callback(err); + other.push({ + path: parentPath, + hash: hash, + mode: modes.tree + }); + if (!--left) { + other.base = entries.base; + if (other.length === 1 && other[0].path === "") { + return callback(null, hash, tree); + } + fastUpdateTree(other, callback); + } + }); + }); + }); + } + } + + + function readRef(ref, callback) { + if (!callback) return readRef.bind(null, ref); + if (!(/^refs\//).test(ref)) { + return callback(new TypeError("Invalid ref: " + ref)); + } + return apiRequest("GET", "/repos/:root/git/" + ref, onRef); + + function onRef(err, xhr, result) { + if (err) return callback(err); + if (xhr.status === 404) return callback(); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + } + return callback(null, result.object.sha); + } + } + + function updateRef(ref, hash, callback) { + if (!callback) return updateRef(null, ref, hash); + if (!(/^refs\//).test(ref)) { + return callback(new Error("Invalid ref: " + ref)); + } + return apiRequest("PATCH", "/repos/:root/git/" + ref, { + sha: hash + }, onResult); + + function onResult(err, xhr, result) { + if (err) return callback(err); + if (xhr.status === 422 && result.message === "Reference does not exist") { + return apiRequest("POST", "/repos/:root/git/refs", { + ref: ref, + sha: hash + }, onResult); + } + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + } + if (err) return callback(err); + callback(null, hash); + } + + } + + }; + + // GitHub has a nasty habit of stripping whitespace from messages and loosing + // the timezone. This information is required to make our hashes match up, so + // we guess it by mutating the value till the hash matches. + // If we're unable to match, we will just force the hash when saving to the cache. + function fixDate(type, value, hash) { + if (type !== "commit" && type !== "tag") return; + // Add up to 2 extra newlines and try all 30-minutes timezone offsets. + for (var x = 0; x < 3; x++) { + for (var i = -720; i < 720; i += 30) { + if (type === "commit") { + value.author.date.timezoneOffset = i; + value.committer.date.timezoneOffset = i; + } + else if (type === "tag") { + value.tagger.date.timezoneOffset = i; + } + if (hash === hashAs(type, value)) return true; + } + value.message += "\n"; + } + // Guessing failed, remove the temporary offset. + if (type === "commit") { + delete value.author.date.timezoneOffset; + delete value.committer.date.timezoneOffset; + } + else if (type === "tag") { + delete value.tagger.date.timezoneOffset; + } + } + + function mapTreeEntry(entry) { + if (!entry.mode) throw new TypeError("Invalid entry"); + var mode = modeToString(entry.mode); + var item = { + path: entry.path, + mode: mode, + type: modeToType[mode] + }; + // Magic hash for empty file since github rejects empty contents. + if (entry.content === "") entry.hash = emptyBlob; + + if (entry.hash) item.sha = entry.hash; + else item.content = entry.content; + return item; + } + + function encodeCommit(commit) { + var out = {}; + out.message = commit.message; + out.tree = commit.tree; + if (commit.parents) out.parents = commit.parents; + else if (commit.parent) out.parents = [commit.parent]; + else commit.parents = []; + if (commit.author) out.author = encodePerson(commit.author); + if (commit.committer) out.committer = encodePerson(commit.committer); + return out; + } + + function encodeTag(tag) { + return { + tag: tag.tag, + message: tag.message, + object: tag.object, + tagger: encodePerson(tag.tagger) + }; + } + + function encodePerson(person) { + return { + name: person.name, + email: person.email, + date: (person.date || new Date()).toISOString() + }; + } + + function encodeTree(tree) { + return { + tree: Object.keys(tree).map(function (name) { + var entry = tree[name]; + var mode = modeToString(entry.mode); + return { + path: name, + mode: mode, + type: modeToType[mode], + sha: entry.hash + }; + }) + }; + } + + function encodeBlob(blob) { + if (typeof blob === "string") return { + content: binary.encodeUtf8(blob), + encoding: "utf-8" + }; + if (binary.isBinary(blob)) return { + content: binary.toBase64(blob), + encoding: "base64" + }; + throw new TypeError("Invalid blob type, must be binary of string"); + } + + function modeToString(mode) { + var string = mode.toString(8); + // Github likes all modes to be 6 chars long + if (string.length === 5) string = "0" + string; + return string; + } + + function decodeCommit(result) { + return { + tree: result.tree.sha, + parents: result.parents.map(function (object) { + return object.sha; + }), + author: pickPerson(result.author), + committer: pickPerson(result.committer), + message: result.message + }; + } + + function decodeTag(result) { + return { + object: result.object.sha, + type: result.object.type, + tag: result.tag, + tagger: pickPerson(result.tagger), + message: result.message + }; + } + + function decodeTree(result) { + var tree = {}; + result.tree.forEach(function (entry) { + tree[entry.path] = { + mode: parseInt(entry.mode, 8), + hash: entry.sha + }; + }); + return tree; + } + + function decodeBlob(result) { + if (result.encoding === 'base64') { + return binary.fromBase64(result.content.replace(/\n/g, '')); + } + if (result.encoding === 'utf-8') { + return binary.fromUtf8(result.content); + } + throw new Error("Unknown blob encoding: " + result.encoding); + } + + function pickPerson(person) { + return { + name: person.name, + email: person.email, + date: parseDate(person.date) + }; + } + + function parseDate(string) { + // TODO: test this once GitHub adds timezone information + var match = string.match(/(-?)([0-9]{2}):([0-9]{2})$/); + var date = new Date(string); + date.timezoneOffset = 0; + if (match) { + date.timezoneOffset = (match[1] === "-" ? 1 : -1) * ( + parseInt(match[2], 10) * 60 + parseInt(match[3], 10) + ); + } + return date; + } + + function singleCall(callback) { + var done = false; + return function () { + if (done) return console.warn("Discarding extra callback"); + done = true; + return callback.apply(this, arguments); + }; + } + +}); diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js new file mode 100644 index 0000000..9829387 --- /dev/null +++ b/mixins/indexed-db.js @@ -0,0 +1,132 @@ +define("js-git/mixins/indexed-db", function () { + "use strict"; + + var encoders = require('js-git/lib/encoders'); + var binary = require('js-git/lib/binary'); + var db; + + mixin.init = init; + + mixin.loadAs = loadAs; + mixin.saveAs = saveAs; + return mixin; + + function init(callback) { + + db = null; + var request = indexedDB.open("tedit", 1); + + // We can only create Object stores in a versionchange transaction. + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + + // A versionchange transaction is started automatically. + evt.target.transaction.onerror = onError; + + if(db.objectStoreNames.contains("objects")) { + db.deleteObjectStore("objects"); + } + if(db.objectStoreNames.contains("refs")) { + db.deleteObjectStore("refs"); + } + + db.createObjectStore("objects", {keyPath: "hash"}); + db.createObjectStore("refs", {keyPath: "path"}); + }; + + request.onsuccess = function (evt) { + db = evt.target.result; + callback(); + }; + request.onerror = onError; + } + + + function mixin(repo, prefix) { + if (!prefix) throw new Error("Prefix required"); + repo.refPrefix = prefix; + repo.saveAs = saveAs; + repo.loadAs = loadAs; + repo.readRef = readRef; + repo.updateRef = updateRef; + } + + function onError(evt) { + console.error("error", evt.target.error); + } + + function saveAs(type, body, callback, forcedHash) { + if (!callback) return saveAs.bind(this, type, body); + var hash; + try { + body = encoders.normalizeAs(type, body); + hash = forcedHash || encoders.hashAs(type, body); + } + catch (err) { return callback(err); } + var trans = db.transaction(["objects"], "readwrite"); + var store = trans.objectStore("objects"); + var entry = { hash: hash, type: type, body: body }; + var request = store.put(entry); + request.onsuccess = function() { + // console.log("SAVE", type, hash); + callback(null, hash, body); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; + } + + function loadAs(type, hash, callback) { + if (!callback) return loadAs.bind(this, type, hash); + // console.log("LOAD", type, hash); + var trans = db.transaction(["objects"], "readwrite"); + var store = trans.objectStore("objects"); + var request = store.get(hash); + request.onsuccess = function(evt) { + var entry = evt.target.result; + if (!entry) return callback(); + if (type !== "blob") { + entry.body = encoders.normalizeAs(type, entry.body); + } + if (type !== entry.type) { + return callback(new TypeError("Type mismatch")); + } + callback(null, entry.body, hash); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; + } + + function readRef(ref, callback) { + if (!callback) return readRef.bind(this, ref); + var key = this.refPrefix + "/" + ref; + var trans = db.transaction(["refs"], "readwrite"); + var store = trans.objectStore("refs"); + var request = store.get(key); + request.onsuccess = function(evt) { + var entry = evt.target.result; + if (!entry) return callback(); + callback(null, entry.hash); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; + } + + function updateRef(ref, hash, callback) { + if (!callback) return updateRef.bind(this, ref, hash); + var key = this.refPrefix + "/" + ref; + var trans = db.transaction(["refs"], "readwrite"); + var store = trans.objectStore("refs"); + var entry = { path: key, hash: hash }; + var request = store.put(entry); + request.onsuccess = function() { + callback(); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; + } + +}); diff --git a/mixins/mem-db.js b/mixins/mem-db.js new file mode 100644 index 0000000..e867513 --- /dev/null +++ b/mixins/mem-db.js @@ -0,0 +1,51 @@ +/*global define*/ +define("js-git/mixins/mem-db", function () { + "use strict"; + + var defer = require('js-git/lib/defer'); + var encoders = require('js-git/lib/encoders'); + var binary = require('js-git/lib/binary'); + var i = 0; + + return mixin; + + function mixin(repo) { + var objects = repo.objects = {}; + var name = i++; + var types = {}; + + repo.saveAs = saveAs; + repo.loadAs = loadAs; + + function saveAs(type, body, callback) { + defer(function () { + var hash; + try { + body = encoders.normalizeAs(type, body); + hash = encoders.hashAs(type, body); + } + catch (err) { return callback(err); } + console.log("SAVE", name, hash); + objects[hash] = body; + types[hash] = type; + callback(null, hash, body); + }); + } + + function loadAs(type, hash, callback) { + console.log("LOAD", name, hash); + defer(function () { + var realType = (type === "text" || type === "raw") ? "blob" : type; + if (!types[hash]) return callback(); + if (realType !== types[hash]) return callback(new TypeError("Type mismatch")); + var result = objects[hash]; + if (type === "text") result = binary.toUnicode(result); + else if (type !== "blob") result = encoders.normalizeAs(type, result); + callback(null, result, hash); + }); + } + + } + + +}); diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js new file mode 100644 index 0000000..bee0472 --- /dev/null +++ b/mixins/path-to-entry.js @@ -0,0 +1,246 @@ +/*global define*/ +define("js-git/mixins/path-to-entry", function () { + "use strict"; + + var modes = require('js-git/lib/modes'); + var encoders = require('js-git/lib/encoders'); + + // Cache the tree entries by hash for faster path lookup. + var cache = {}; + + // Cached compiled directories that contain wildcards. + var dirs = {}; + + return function (repo) { + if (!repo.submodules) repo.submodules = {}; + repo.pathToEntry = pathToEntry; + var loadAs = repo.loadAs; + if (loadAs) repo.loadAs = loadAsCached; + var saveAs = repo.saveAs; + if (saveAs) repo.saveAs = saveAsCached; + var createTree = repo.createTree; + if (createTree) repo.createTree = createTreeCached; + + // Monkeypatch loadAs to cache non-blobs + function loadAsCached(type, hash, callback) { + if (!callback) return loadAsCached.bind(repo, type, hash); + if (hash in cache) { + // console.log("LOAD CACHED", hash); + return callback(null, encoders.normalizeAs(type, cache[hash])); + } + if (type === "blob") { + return loadAs.apply(repo, arguments); + } + loadAs.call(repo, type, hash, function (err, body, hash) { + if (body === undefined) return callback(err); + cache[hash] = body; + callback(null, body, hash); + }); + } + + // Monkeypatch saveAs to cache non-blobs + function saveAsCached(type, body, callback) { + if (!callback) { + return saveAsCached.bind(repo, type, body); + } + if (type === "blob") { + return saveAs.apply(repo, arguments); + } + saveAs.call(repo, type, body, function (err, hash, body) { + if (err) return callback(err); + cache[hash] = body; + callback(null, hash, body); + }); + } + + // Monkeypatch saveAs to cache non-blobs + function createTreeCached(entries, callback) { + if (!callback) { + return createTreeCached.bind(repo, entries); + } + createTree.call(repo, entries, function (err, hash, tree) { + if (err) return callback(err); + cache[hash] = tree; + callback(null, hash, tree); + }); + } + + }; + + + function pathToEntry(root, path, callback) { + var repo = this; + if (!callback) return pathToEntry.bind(repo, root, path); + + // Split path ignoring leading and trailing slashes. + var parts = path.split("/").filter(String); + var length = parts.length; + var index = 0; + + // These contain the hash and mode of the path as we walk the segments. + var mode = modes.tree; + var hash = root; + return walk(); + + function patternCompile(source, target) { + // Escape characters that are dangerous in regular expressions first. + source = source.replace(/[\-\[\]\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + // Extract all the variables in the source and target and replace them. + source.match(/\{[a-z]+\}/g).forEach(function (match, i) { + source = source.replace(match, "(.*)"); + target = target.replace(match, '$' + (i + 1)); + }); + var match = new RegExp("^" + source + "$"); + match.target = target; + return match; + } + + function compileDir(hash, tree, callback) { + var left = 1; + var done = false; + var wilds = Object.keys(tree).filter(function (key) { + return (modes.sym === tree[key].mode) && /\{[a-z]+\}/.test(key); + }); + dirs[hash] = wilds; + wilds.forEach(function (key, i) { + if (done) return; + var hash = tree[key].hash; + var link = cache[hash]; + if (link) { + wilds[i] = patternCompile(key, link); + return; + } + left++; + repo.loadAs("text", hash, function (err, link) { + if (done) return; + if (err) { + done = true; + return callback(err); + } + cache[hash] = link; + wilds[i] = patternCompile(key, link); + if (!--left) { + done = true; + callback(); + } + }); + }); + if (!done && !--left) { + done = true; + callback(); + } + } + + function walk(err) { + if (err) return callback(err); + var cached; + outer: + while (index < length) { + // If the parent is a tree, look for our path segment + if (mode === modes.tree) { + cached = cache[hash]; + // If it's not cached yet, abort and resume later. + if (!cached) return repo.loadAs("tree", hash, onValue); + var name = parts[index]; + var entry = cached[name]; + if (!entry) { + var dir = dirs[hash]; + if (!dir) return compileDir(hash, cached, walk); + for (var i = 0, l = dir.length; i < l; i++) { + var wild = dir[i]; + if (!wild.test(name)) continue; + mode = modes.sym; + hash = hash + "-" + name; + cache[hash] = name.replace(wild, wild.target); + break outer; + } + return callback(); + } + index++; + hash = entry.hash; + mode = entry.mode; + continue; + } + // If the parent is a symlink, adjust the path in-place and start over. + if (mode === modes.sym) { + cached = cache[hash]; + if (!cached) return repo.loadAs("text", hash, onValue); + // Remove the tail and remove the symlink segment from the head. + var tail = parts.slice(index); + parts.length = index - 1; + // Join the target resolving special "." and ".." segments. + cached.split("/").forEach(onPart); + // Add the tail back in. + parts.push.apply(parts, tail); + // Start over. The already passed path will be cached and quite fast. + hash = root; + mode = modes.tree; + index = 0; + continue; + } + // If it's a submodule, jump over to that repo. + if (mode === modes.commit) { + var parentPath = parts.slice(0, index).join("/"); + var submodule = repo.submodules[parentPath]; + if (!submodule) { + return callback(new Error("Missing submodule for path: " + parentPath)); + } + cached = cache[hash]; + if (!cached) return submodule.loadAs("commit", hash, onValue); + var childPath = parts.slice(index).join("/"); + return submodule.pathToEntry(cached.tree, childPath, callback); + } + return callback(new Error("Invalid path segment")); + } + + // We've reached the final segment, let's preload symlinks and trees since + // we don't mind caching those. + + var result; + if (mode === modes.tree) { + cached = cache[hash]; + if (!cached) return repo.loadAs("tree", hash, onValue); + result = { tree: encoders.normalizeAs("tree", cached) }; + } + else if (mode === modes.sym) { + cached = cache[hash]; + if (!cached) return repo.loadAs("text", hash, onValue); + result = { link: cached }; + } + else if (mode === modes.commit) { + cached = cache[hash]; + if (!cached) return repo.loadAs("commit", hash, onValue); + result = { commit: encoders.normalizeAs("commit", cached) }; + } + else { + result = {}; + } + result.mode = mode; + result.hash = hash; + + // In the case of submodule traversal, the caller's repo is different + return callback(null, result, repo); + + // Used by the symlink code to resolve the target against the path. + function onPart(part) { + // Ignore leading and trailing slashes as well as "." segments. + if (!part || part === ".") return; + // ".." pops a path segment from the stack + if (part === "..") parts.pop(); + // New paths segments get pushed on top. + else parts.push(part); + } + + } + + function onValue(err, value) { + if (value === undefined) return callback(err); + // Don't let anyone change this value. + if (typeof value === "object") Object.freeze(value); + cache[hash] = value; + return walk(); + } + + } + +}); \ No newline at end of file diff --git a/mixins/read-combiner.js b/mixins/read-combiner.js new file mode 100644 index 0000000..a3d1936 --- /dev/null +++ b/mixins/read-combiner.js @@ -0,0 +1,32 @@ +/*global define*/ +define('js-git/mixins/read-combiner', function () { + "use strict"; + + // This replaces loadAs with a version that batches concurrent requests for + // the same hash. + return function (repo) { + var pendingReqs = {}; + + var loadAs = repo.loadAs; + repo.loadAs = newLoadAs; + + function newLoadAs(type, hash, callback) { + if (!callback) return newLoadAs.bind(null, type, hash); + var list = pendingReqs[hash]; + if (list) { + if (list.type !== type) callback(new Error("Type mismatch")); + else list.push(callback); + return; + } + list = pendingReqs[hash] = [callback]; + list.type = type; + loadAs.call(repo, type, hash, function () { + delete pendingReqs[hash]; + for (var i = 0, l = list.length; i < l; i++) { + list[i].apply(this, arguments); + } + }); + } + }; + +}); \ No newline at end of file From 87cf936b6a106d668eff0568da5c70bce0ae854d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 8 Feb 2014 20:41:34 -0600 Subject: [PATCH 082/256] Remove jshint config --- .jshintrc | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 953ae25..0000000 --- a/.jshintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - // Enforcing options, without name - set to false to ignore violations - "-W030": false, // 'Expected an assignment or function call and instead saw an expression.' - "-W058": false, // 'Missing '()' invoking a constructor.' - - "loopfunc": true, - - // Environments - set to true to allow environment variables - "browser": true, - "node": true, - "esnext": true -} From 2b386f4d9340c01e43ab931d77c6dcb41c73d14a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 10 Feb 2014 15:10:28 +0000 Subject: [PATCH 083/256] Clean up cache code and remove pathtoentry mixin --- mixins/add-cache.js | 45 ++++++-- mixins/mem-db.js | 16 ++- mixins/path-to-entry.js | 246 ---------------------------------------- 3 files changed, 40 insertions(+), 267 deletions(-) delete mode 100644 mixins/path-to-entry.js diff --git a/mixins/add-cache.js b/mixins/add-cache.js index a7941a4..985d005 100644 --- a/mixins/add-cache.js +++ b/mixins/add-cache.js @@ -1,4 +1,5 @@ /*global define*/ +/*jshint unused:strict,undef:true,trailing:true */ define("js-git/mixins/add-cache", function () { return addCache; @@ -12,33 +13,53 @@ define("js-git/mixins/add-cache", function () { function loadAsCached(type, hash, callback) { if (!callback) return loadAsCached.bind(this, type, hash); - cache.loadAs(type, hash, function (err, value) { + + // Next check in disk cache... + cache.loadAs(type, hash, onCacheLoad); + + function onCacheLoad(err, value) { if (err) return callback(err); + // ...and return if it's there. if (value !== undefined) { return callback(null, value, hash); } - loadAs.call(repo, type, hash, function (err, value) { + + // Otherwise load from real data source... + loadAs.call(repo, type, hash, onLoad); + } + + function onLoad(err, value) { + if (value === undefined) return callback(err); + + // Store it on disk too... + // Force the hash to prevent mismatches. + cache.saveAs(type, value, onSave, hash); + + function onSave(err) { if (err) return callback(err); - cache.saveAs(type, value, function (err) { - if (err) return callback(err); - callback(null, value, hash); - }, hash); - }); - }); + // Finally return the value to caller. + callback(null, value, hash); + } + } } function saveAsCached(type, value, callback) { - saveAs.call(repo, type, value, function (err, hash) { + saveAs.call(repo, type, value, onSave); + + function onSave(err, hash, value) { if (err) return callback(err); + // Store in disk, forcing hash to match. cache.saveAs(type, value, callback, hash); - }); + } } function createTreeCached(entries, callback) { - createTree.call(repo, entries, function (err, hash, tree) { + createTree.call(repo, entries, onTree); + + function onTree(err, hash, tree) { if (err) return callback(err); cache.saveAs("tree", tree, callback, hash); - }); + } } } diff --git a/mixins/mem-db.js b/mixins/mem-db.js index e867513..b13b4ac 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -4,28 +4,27 @@ define("js-git/mixins/mem-db", function () { var defer = require('js-git/lib/defer'); var encoders = require('js-git/lib/encoders'); - var binary = require('js-git/lib/binary'); - var i = 0; + mixin.saveAs = saveAs; + mixin.loadAs = loadAs; return mixin; function mixin(repo) { var objects = repo.objects = {}; - var name = i++; var types = {}; repo.saveAs = saveAs; repo.loadAs = loadAs; - function saveAs(type, body, callback) { + function saveAs(type, body, callback, hashOverride) { + if (!callback) return saveAs.bind(this, type, body); defer(function () { var hash; try { body = encoders.normalizeAs(type, body); - hash = encoders.hashAs(type, body); + hash = hashOverride || encoders.hashAs(type, body); } catch (err) { return callback(err); } - console.log("SAVE", name, hash); objects[hash] = body; types[hash] = type; callback(null, hash, body); @@ -33,14 +32,13 @@ define("js-git/mixins/mem-db", function () { } function loadAs(type, hash, callback) { - console.log("LOAD", name, hash); + if (!callback) return loadAs.bind(this, type, hash); defer(function () { var realType = (type === "text" || type === "raw") ? "blob" : type; if (!types[hash]) return callback(); if (realType !== types[hash]) return callback(new TypeError("Type mismatch")); var result = objects[hash]; - if (type === "text") result = binary.toUnicode(result); - else if (type !== "blob") result = encoders.normalizeAs(type, result); + if (type !== "blob") result = encoders.normalizeAs(type, result); callback(null, result, hash); }); } diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js deleted file mode 100644 index bee0472..0000000 --- a/mixins/path-to-entry.js +++ /dev/null @@ -1,246 +0,0 @@ -/*global define*/ -define("js-git/mixins/path-to-entry", function () { - "use strict"; - - var modes = require('js-git/lib/modes'); - var encoders = require('js-git/lib/encoders'); - - // Cache the tree entries by hash for faster path lookup. - var cache = {}; - - // Cached compiled directories that contain wildcards. - var dirs = {}; - - return function (repo) { - if (!repo.submodules) repo.submodules = {}; - repo.pathToEntry = pathToEntry; - var loadAs = repo.loadAs; - if (loadAs) repo.loadAs = loadAsCached; - var saveAs = repo.saveAs; - if (saveAs) repo.saveAs = saveAsCached; - var createTree = repo.createTree; - if (createTree) repo.createTree = createTreeCached; - - // Monkeypatch loadAs to cache non-blobs - function loadAsCached(type, hash, callback) { - if (!callback) return loadAsCached.bind(repo, type, hash); - if (hash in cache) { - // console.log("LOAD CACHED", hash); - return callback(null, encoders.normalizeAs(type, cache[hash])); - } - if (type === "blob") { - return loadAs.apply(repo, arguments); - } - loadAs.call(repo, type, hash, function (err, body, hash) { - if (body === undefined) return callback(err); - cache[hash] = body; - callback(null, body, hash); - }); - } - - // Monkeypatch saveAs to cache non-blobs - function saveAsCached(type, body, callback) { - if (!callback) { - return saveAsCached.bind(repo, type, body); - } - if (type === "blob") { - return saveAs.apply(repo, arguments); - } - saveAs.call(repo, type, body, function (err, hash, body) { - if (err) return callback(err); - cache[hash] = body; - callback(null, hash, body); - }); - } - - // Monkeypatch saveAs to cache non-blobs - function createTreeCached(entries, callback) { - if (!callback) { - return createTreeCached.bind(repo, entries); - } - createTree.call(repo, entries, function (err, hash, tree) { - if (err) return callback(err); - cache[hash] = tree; - callback(null, hash, tree); - }); - } - - }; - - - function pathToEntry(root, path, callback) { - var repo = this; - if (!callback) return pathToEntry.bind(repo, root, path); - - // Split path ignoring leading and trailing slashes. - var parts = path.split("/").filter(String); - var length = parts.length; - var index = 0; - - // These contain the hash and mode of the path as we walk the segments. - var mode = modes.tree; - var hash = root; - return walk(); - - function patternCompile(source, target) { - // Escape characters that are dangerous in regular expressions first. - source = source.replace(/[\-\[\]\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - // Extract all the variables in the source and target and replace them. - source.match(/\{[a-z]+\}/g).forEach(function (match, i) { - source = source.replace(match, "(.*)"); - target = target.replace(match, '$' + (i + 1)); - }); - var match = new RegExp("^" + source + "$"); - match.target = target; - return match; - } - - function compileDir(hash, tree, callback) { - var left = 1; - var done = false; - var wilds = Object.keys(tree).filter(function (key) { - return (modes.sym === tree[key].mode) && /\{[a-z]+\}/.test(key); - }); - dirs[hash] = wilds; - wilds.forEach(function (key, i) { - if (done) return; - var hash = tree[key].hash; - var link = cache[hash]; - if (link) { - wilds[i] = patternCompile(key, link); - return; - } - left++; - repo.loadAs("text", hash, function (err, link) { - if (done) return; - if (err) { - done = true; - return callback(err); - } - cache[hash] = link; - wilds[i] = patternCompile(key, link); - if (!--left) { - done = true; - callback(); - } - }); - }); - if (!done && !--left) { - done = true; - callback(); - } - } - - function walk(err) { - if (err) return callback(err); - var cached; - outer: - while (index < length) { - // If the parent is a tree, look for our path segment - if (mode === modes.tree) { - cached = cache[hash]; - // If it's not cached yet, abort and resume later. - if (!cached) return repo.loadAs("tree", hash, onValue); - var name = parts[index]; - var entry = cached[name]; - if (!entry) { - var dir = dirs[hash]; - if (!dir) return compileDir(hash, cached, walk); - for (var i = 0, l = dir.length; i < l; i++) { - var wild = dir[i]; - if (!wild.test(name)) continue; - mode = modes.sym; - hash = hash + "-" + name; - cache[hash] = name.replace(wild, wild.target); - break outer; - } - return callback(); - } - index++; - hash = entry.hash; - mode = entry.mode; - continue; - } - // If the parent is a symlink, adjust the path in-place and start over. - if (mode === modes.sym) { - cached = cache[hash]; - if (!cached) return repo.loadAs("text", hash, onValue); - // Remove the tail and remove the symlink segment from the head. - var tail = parts.slice(index); - parts.length = index - 1; - // Join the target resolving special "." and ".." segments. - cached.split("/").forEach(onPart); - // Add the tail back in. - parts.push.apply(parts, tail); - // Start over. The already passed path will be cached and quite fast. - hash = root; - mode = modes.tree; - index = 0; - continue; - } - // If it's a submodule, jump over to that repo. - if (mode === modes.commit) { - var parentPath = parts.slice(0, index).join("/"); - var submodule = repo.submodules[parentPath]; - if (!submodule) { - return callback(new Error("Missing submodule for path: " + parentPath)); - } - cached = cache[hash]; - if (!cached) return submodule.loadAs("commit", hash, onValue); - var childPath = parts.slice(index).join("/"); - return submodule.pathToEntry(cached.tree, childPath, callback); - } - return callback(new Error("Invalid path segment")); - } - - // We've reached the final segment, let's preload symlinks and trees since - // we don't mind caching those. - - var result; - if (mode === modes.tree) { - cached = cache[hash]; - if (!cached) return repo.loadAs("tree", hash, onValue); - result = { tree: encoders.normalizeAs("tree", cached) }; - } - else if (mode === modes.sym) { - cached = cache[hash]; - if (!cached) return repo.loadAs("text", hash, onValue); - result = { link: cached }; - } - else if (mode === modes.commit) { - cached = cache[hash]; - if (!cached) return repo.loadAs("commit", hash, onValue); - result = { commit: encoders.normalizeAs("commit", cached) }; - } - else { - result = {}; - } - result.mode = mode; - result.hash = hash; - - // In the case of submodule traversal, the caller's repo is different - return callback(null, result, repo); - - // Used by the symlink code to resolve the target against the path. - function onPart(part) { - // Ignore leading and trailing slashes as well as "." segments. - if (!part || part === ".") return; - // ".." pops a path segment from the stack - if (part === "..") parts.pop(); - // New paths segments get pushed on top. - else parts.push(part); - } - - } - - function onValue(err, value) { - if (value === undefined) return callback(err); - // Don't let anyone change this value. - if (typeof value === "object") Object.freeze(value); - cache[hash] = value; - return walk(); - } - - } - -}); \ No newline at end of file From 191acfbc83574a9b26e2250a60c402a7682e66e9 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 11 Feb 2014 22:36:09 +0000 Subject: [PATCH 084/256] Add memcache --- mixins/add-cache.js | 16 +++++++-------- mixins/indexed-db.js | 6 +++--- mixins/mem-cache.js | 47 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 mixins/mem-cache.js diff --git a/mixins/add-cache.js b/mixins/add-cache.js index 985d005..b85f2d3 100644 --- a/mixins/add-cache.js +++ b/mixins/add-cache.js @@ -13,28 +13,28 @@ define("js-git/mixins/add-cache", function () { function loadAsCached(type, hash, callback) { if (!callback) return loadAsCached.bind(this, type, hash); - + // Next check in disk cache... cache.loadAs(type, hash, onCacheLoad); - + function onCacheLoad(err, value) { if (err) return callback(err); // ...and return if it's there. if (value !== undefined) { return callback(null, value, hash); } - + // Otherwise load from real data source... loadAs.call(repo, type, hash, onLoad); } - + function onLoad(err, value) { if (value === undefined) return callback(err); - + // Store it on disk too... // Force the hash to prevent mismatches. cache.saveAs(type, value, onSave, hash); - + function onSave(err) { if (err) return callback(err); // Finally return the value to caller. @@ -45,7 +45,7 @@ define("js-git/mixins/add-cache", function () { function saveAsCached(type, value, callback) { saveAs.call(repo, type, value, onSave); - + function onSave(err, hash, value) { if (err) return callback(err); // Store in disk, forcing hash to match. @@ -55,7 +55,7 @@ define("js-git/mixins/add-cache", function () { function createTreeCached(entries, callback) { createTree.call(repo, entries, onTree); - + function onTree(err, hash, tree) { if (err) return callback(err); cache.saveAs("tree", tree, callback, hash); diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index 9829387..9964d6f 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -1,8 +1,8 @@ +/*global indexedDB, define*/ define("js-git/mixins/indexed-db", function () { "use strict"; var encoders = require('js-git/lib/encoders'); - var binary = require('js-git/lib/binary'); var db; mixin.init = init; @@ -68,7 +68,7 @@ define("js-git/mixins/indexed-db", function () { var entry = { hash: hash, type: type, body: body }; var request = store.put(entry); request.onsuccess = function() { - // console.log("SAVE", type, hash); + console.warn("SAVE", type, hash); callback(null, hash, body); }; request.onerror = function(evt) { @@ -78,7 +78,7 @@ define("js-git/mixins/indexed-db", function () { function loadAs(type, hash, callback) { if (!callback) return loadAs.bind(this, type, hash); - // console.log("LOAD", type, hash); + console.warn("LOAD", type, hash); var trans = db.transaction(["objects"], "readwrite"); var store = trans.objectStore("objects"); var request = store.get(hash); diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js new file mode 100644 index 0000000..09e6783 --- /dev/null +++ b/mixins/mem-cache.js @@ -0,0 +1,47 @@ +/*global define*/ +define("js-git/mixins/mem-cache", function () { + "use strict"; + var normalizeAs = require('js-git/lib/encoders').normalizeAs; + var hashAs = require('js-git/lib/encoders').hashAs; + + var cache = {}; + return function (repo) { + var saved = {}; + var loadAs = repo.loadAs; + repo.loadAs = loadAsCached; + function loadAsCached(type, hash, callback) { + if (!callback) return loadAsCached.bind(this, type, hash); + if (hash in cache) return callback(null, dupe(type, cache[hash])); + loadAs.call(repo, type, hash, function (err, value) { + if (err) return callback(err); + if (type !== "blob" || value.length < 100) { + cache[hash] = value; + } + return callback.apply(this, arguments); + }); + } + + var saveAs = repo.saveAs; + repo.saveAs = saveAsCached; + function saveAsCached(type, value, callback) { + if (!callback) return saveAsCached.bind(this, type, value); + // var hash = hashAs(type, value); + // if (saved[hash]) return callback(null, hash, dupe(value)); + saveAs.call(repo, type, value, function (err, hash, value) { + if (err) return callback(err); + if (type !== "blob" || value.length < 100) { + cache[hash] = value; + } + saved[hash] = true; + return callback.apply(this, arguments); + }); + } + }; + + + function dupe(type, value) { + if (type === "blob") return value; + return normalizeAs(type, value); + } + +}); From 2fe9360dc5930bd772751c42ab74f856243cba59 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 11 Feb 2014 22:44:07 +0000 Subject: [PATCH 085/256] Remove noisy logs --- mixins/indexed-db.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index 9964d6f..dc81f64 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -68,7 +68,7 @@ define("js-git/mixins/indexed-db", function () { var entry = { hash: hash, type: type, body: body }; var request = store.put(entry); request.onsuccess = function() { - console.warn("SAVE", type, hash); + // console.warn("SAVE", type, hash); callback(null, hash, body); }; request.onerror = function(evt) { @@ -78,7 +78,7 @@ define("js-git/mixins/indexed-db", function () { function loadAs(type, hash, callback) { if (!callback) return loadAs.bind(this, type, hash); - console.warn("LOAD", type, hash); + // console.warn("LOAD", type, hash); var trans = db.transaction(["objects"], "readwrite"); var store = trans.objectStore("objects"); var request = store.get(hash); From 69b5d7a2ab1904c66415692cfe41913ed9b95021 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 12 Feb 2014 01:30:45 +0000 Subject: [PATCH 086/256] Clean package.json --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 246d0ee..5c9ed34 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "js-git", "version": "0.7.0", "description": "Git Implemented in JavaScript", - "main": "js-git.js", "repository": { "type": "git", "url": "git://github.com/creationix/js-git.git" @@ -19,8 +18,5 @@ "url": "https://github.com/creationix/js-git/issues" }, "dependencies": { - "push-to-pull": "~0.1.0", - "varint": "0.0.3", - "bops": "~0.1.0" } } From 57466ff66d2f842f9c6791bf218da61908407d02 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 12 Feb 2014 21:59:53 +0000 Subject: [PATCH 087/256] Reformat code to cjs modules. --- lib/binary.js | 466 ++++++++++++------------ lib/config-codec.js | 79 ++--- lib/defer.js | 33 +- lib/encoders.js | 467 ++++++++++++------------ lib/modes.js | 45 ++- lib/sha1.js | 261 +++++++------- lib/xhr.js | 61 ++-- mixins/add-cache.js | 98 +++--- mixins/create-tree.js | 261 +++++++------- mixins/delay.js | 101 +++--- mixins/formats.js | 49 ++- mixins/github-db.js | 759 ++++++++++++++++++++-------------------- mixins/indexed-db.js | 235 ++++++------- mixins/mem-cache.js | 74 ++-- mixins/mem-db.js | 84 ++--- mixins/read-combiner.js | 52 ++- 16 files changed, 1522 insertions(+), 1603 deletions(-) diff --git a/lib/binary.js b/lib/binary.js index 4670034..b27052e 100644 --- a/lib/binary.js +++ b/lib/binary.js @@ -1,237 +1,233 @@ -/*global define, escape, unescape*/ +"use strict"; +/*global escape, unescape*/ // This file must be served with UTF-8 encoding for the utf8 codec to work. -define("js-git/lib/binary", function () { - "use strict"; - - return { - // Utility functions - isBinary: isBinary, - create: create, - join: join, - - // Binary input and output - copy: copy, - slice: slice, - - // String input and output - toRaw: toRaw, - fromRaw: fromRaw, - toUnicode: toUnicode, - fromUnicode: fromUnicode, - toHex: toHex, - fromHex: fromHex, - toBase64: toBase64, - fromBase64: fromBase64, - - // Array input and output - toArray: toArray, - fromArray: fromArray, - - // Raw <-> Hex-encoded codec - decodeHex: decodeHex, - encodeHex: encodeHex, - - decodeBase64: decodeBase64, - encodeBase64: encodeBase64, - - // Unicode <-> Utf8-encoded-raw codec - encodeUtf8: encodeUtf8, - decodeUtf8: decodeUtf8, - - // Hex <-> Nibble codec - nibbleToCode: nibbleToCode, - codeToNibble: codeToNibble - }; - - function isBinary(value) { - return value - && typeof value === "object" - && value.constructor.name === "Uint8Array"; - } - - function create(length) { - return new Uint8Array(length); - } - - function join(chunks) { - var length = chunks.length; - var total = 0; - for (var i = 0; i < length; i++) { - total += chunks[i].length; - } - var binary = create(total); - var offset = 0; - for (i = 0; i < length; i++) { - var chunk = chunks[i]; - copy(chunk, binary, offset); - offset += chunk.length; - } - return binary; - } - - function slice(binary, start, end) { - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - return binary.subarray(start, end); - } - - function copy(source, binary, offset) { - var length = source.length; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - for (var i = 0; i < length; i++) { - binary[i + offset] = source[i]; - } - return binary; - } - - // Like slice, but encode as a hex string - function toHex(binary, start, end) { - var hex = ""; - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - for (var i = start; i < end; i++) { - var byte = binary[i]; - hex += String.fromCharCode(nibbleToCode(byte >> 4)) - + String.fromCharCode(nibbleToCode(byte & 0xf)); - } - return hex; - } - - // Like copy, but decode from a hex string - function fromHex(hex, binary, offset) { - var length = hex.length / 2; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - var j = 0; - for (var i = 0; i < length; i++) { - binary[offset + i] = (codeToNibble(hex.charCodeAt(j++)) << 4) - | codeToNibble(hex.charCodeAt(j++)); - } - return binary; - } - - function toBase64(binary, start, end) { - return btoa(toRaw(binary, start, end)); - } - - function fromBase64(base64, binary, offset) { - return fromRaw(atob(base64), binary, offset); - } - - function nibbleToCode(nibble) { - nibble |= 0; - return (nibble + (nibble < 10 ? 0x30 : 0x57))|0; - } - - function codeToNibble(code) { - code |= 0; - return (code - ((code & 0x40) ? 0x57 : 0x30))|0; - } - - function toUnicode(binary, start, end) { - return decodeUtf8(toRaw(binary, start, end)); - } - - function fromUnicode(unicode, binary, offset) { - return fromRaw(encodeUtf8(unicode), binary, offset); - } - - function decodeHex(hex) { - var j = 0, l = hex.length; - var raw = ""; - while (j < l) { - raw += String.fromCharCode( - (codeToNibble(hex.charCodeAt(j++)) << 4) - | codeToNibble(hex.charCodeAt(j++)) - ); - } - return raw; - } - - function encodeHex(raw) { - var hex = ""; - var length = raw.length; - for (var i = 0; i < length; i++) { - var byte = raw.charCodeAt(i); - hex += String.fromCharCode(nibbleToCode(byte >> 4)) - + String.fromCharCode(nibbleToCode(byte & 0xf)); - } - return hex; - } - - function decodeBase64(base64) { - return atob(base64); - } - - function encodeBase64(raw) { - return btoa(raw); - } - - function decodeUtf8(utf8) { - return decodeURIComponent(escape(utf8)); - } - - function encodeUtf8(unicode) { - return unescape(encodeURIComponent(unicode)); - } - - function toRaw(binary, start, end) { - var raw = ""; - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - for (var i = start; i < end; i++) { - raw += String.fromCharCode(binary[i]); - } - return raw; - } - - function fromRaw(raw, binary, offset) { - var length = raw.length; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - for (var i = 0; i < length; i++) { - binary[offset + i] = raw.charCodeAt(i); - } - return binary; - } - - function toArray(binary, start, end) { - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - var length = end - start; - var array = new Array(length); - for (var i = 0; i < length; i++) { - array[i] = binary[i + start]; - } - return array; - } - - function fromArray(array, binary, offset) { - var length = array.length; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - for (var i = 0; i < length; i++) { - binary[offset + i] = array[i]; - } - return binary; - } - -}); \ No newline at end of file +module.exports = { + // Utility functions + isBinary: isBinary, + create: create, + join: join, + + // Binary input and output + copy: copy, + slice: slice, + + // String input and output + toRaw: toRaw, + fromRaw: fromRaw, + toUnicode: toUnicode, + fromUnicode: fromUnicode, + toHex: toHex, + fromHex: fromHex, + toBase64: toBase64, + fromBase64: fromBase64, + + // Array input and output + toArray: toArray, + fromArray: fromArray, + + // Raw <-> Hex-encoded codec + decodeHex: decodeHex, + encodeHex: encodeHex, + + decodeBase64: decodeBase64, + encodeBase64: encodeBase64, + + // Unicode <-> Utf8-encoded-raw codec + encodeUtf8: encodeUtf8, + decodeUtf8: decodeUtf8, + + // Hex <-> Nibble codec + nibbleToCode: nibbleToCode, + codeToNibble: codeToNibble +}; + +function isBinary(value) { + return value && + typeof value === "object" && + value.constructor.name === "Uint8Array"; +} + +function create(length) { + return new Uint8Array(length); +} + +function join(chunks) { + var length = chunks.length; + var total = 0; + for (var i = 0; i < length; i++) { + total += chunks[i].length; + } + var binary = create(total); + var offset = 0; + for (i = 0; i < length; i++) { + var chunk = chunks[i]; + copy(chunk, binary, offset); + offset += chunk.length; + } + return binary; +} + +function slice(binary, start, end) { + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + return binary.subarray(start, end); +} + +function copy(source, binary, offset) { + var length = source.length; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + for (var i = 0; i < length; i++) { + binary[i + offset] = source[i]; + } + return binary; +} + +// Like slice, but encode as a hex string +function toHex(binary, start, end) { + var hex = ""; + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + for (var i = start; i < end; i++) { + var byte = binary[i]; + hex += String.fromCharCode(nibbleToCode(byte >> 4)) + + String.fromCharCode(nibbleToCode(byte & 0xf)); + } + return hex; +} + +// Like copy, but decode from a hex string +function fromHex(hex, binary, offset) { + var length = hex.length / 2; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + var j = 0; + for (var i = 0; i < length; i++) { + binary[offset + i] = (codeToNibble(hex.charCodeAt(j++)) << 4) + | codeToNibble(hex.charCodeAt(j++)); + } + return binary; +} + +function toBase64(binary, start, end) { + return btoa(toRaw(binary, start, end)); +} + +function fromBase64(base64, binary, offset) { + return fromRaw(atob(base64), binary, offset); +} + +function nibbleToCode(nibble) { + nibble |= 0; + return (nibble + (nibble < 10 ? 0x30 : 0x57))|0; +} + +function codeToNibble(code) { + code |= 0; + return (code - ((code & 0x40) ? 0x57 : 0x30))|0; +} + +function toUnicode(binary, start, end) { + return decodeUtf8(toRaw(binary, start, end)); +} + +function fromUnicode(unicode, binary, offset) { + return fromRaw(encodeUtf8(unicode), binary, offset); +} + +function decodeHex(hex) { + var j = 0, l = hex.length; + var raw = ""; + while (j < l) { + raw += String.fromCharCode( + (codeToNibble(hex.charCodeAt(j++)) << 4) + | codeToNibble(hex.charCodeAt(j++)) + ); + } + return raw; +} + +function encodeHex(raw) { + var hex = ""; + var length = raw.length; + for (var i = 0; i < length; i++) { + var byte = raw.charCodeAt(i); + hex += String.fromCharCode(nibbleToCode(byte >> 4)) + + String.fromCharCode(nibbleToCode(byte & 0xf)); + } + return hex; +} + +function decodeBase64(base64) { + return atob(base64); +} + +function encodeBase64(raw) { + return btoa(raw); +} + +function decodeUtf8(utf8) { + return decodeURIComponent(escape(utf8)); +} + +function encodeUtf8(unicode) { + return unescape(encodeURIComponent(unicode)); +} + +function toRaw(binary, start, end) { + var raw = ""; + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + for (var i = start; i < end; i++) { + raw += String.fromCharCode(binary[i]); + } + return raw; +} + +function fromRaw(raw, binary, offset) { + var length = raw.length; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + for (var i = 0; i < length; i++) { + binary[offset + i] = raw.charCodeAt(i); + } + return binary; +} + +function toArray(binary, start, end) { + if (end === undefined) { + end = binary.length; + if (start === undefined) start = 0; + } + var length = end - start; + var array = new Array(length); + for (var i = 0; i < length; i++) { + array[i] = binary[i + start]; + } + return array; +} + +function fromArray(array, binary, offset) { + var length = array.length; + if (offset === undefined) { + offset = 0; + if (binary === undefined) binary = create(length); + } + for (var i = 0; i < length; i++) { + binary[offset + i] = array[i]; + } + return binary; +} diff --git a/lib/config-codec.js b/lib/config-codec.js index 40df004..cee72bf 100644 --- a/lib/config-codec.js +++ b/lib/config-codec.js @@ -1,49 +1,46 @@ -/*global define*/ -define("js-git/lib/config-codec", function () { +"use strict"; - return { - encode: encode, - parse: parse - }; +module.exports = { + encode: encode, + parse: parse +}; - function encode(config) { - var lines = []; - Object.keys(config).forEach(function (type) { - var obj = config[type]; - Object.keys(obj).forEach(function (name) { - var item = obj[name]; - lines.push('[' + type + ' "' + name + '"]'); - Object.keys(item).forEach(function (key) { - var value = item[key]; - lines.push("\t" + key + " = " + value); - }); - lines.push(""); +function encode(config) { + var lines = []; + Object.keys(config).forEach(function (type) { + var obj = config[type]; + Object.keys(obj).forEach(function (name) { + var item = obj[name]; + lines.push('[' + type + ' "' + name + '"]'); + Object.keys(item).forEach(function (key) { + var value = item[key]; + lines.push("\t" + key + " = " + value); }); + lines.push(""); }); - return lines.join("\n"); - } + }); + return lines.join("\n"); +} - function parse(text) { - var config = {}; - var match, offset = 0; - while (match = text.substr(offset).match(/\[([a-z]*) "([^"]*)"\]([^\[]*)/)) { - var type = match[1]; - var section = config[type] || (config[type] = {}); - var name = match[2]; - section[name] = parseBody(match[3]); - offset += match[0].length; - } - return config; +function parse(text) { + var config = {}; + var match, offset = 0; + while ((match = text.substr(offset).match(/\[([a-z]*) "([^"]*)"\]([^\[]*)/))) { + var type = match[1]; + var section = config[type] || (config[type] = {}); + var name = match[2]; + section[name] = parseBody(match[3]); + offset += match[0].length; } + return config; +} - function parseBody(text) { - var entry = {}; - var match, offset = 0; - while (match = text.substr(offset).match(/([^ \t\r\n]*) *= *([^ \t\r\n]*)/)) { - entry[match[1]] = match[2]; - offset += match[0].length; - } - return entry; +function parseBody(text) { + var entry = {}; + var match, offset = 0; + while ((match = text.substr(offset).match(/([^ \t\r\n]*) *= *([^ \t\r\n]*)/))) { + entry[match[1]] = match[2]; + offset += match[0].length; } - -}); \ No newline at end of file + return entry; +} diff --git a/lib/defer.js b/lib/defer.js index 2a900d2..62dd713 100644 --- a/lib/defer.js +++ b/lib/defer.js @@ -1,22 +1,21 @@ -/*global define*/ -define("js-git/lib/defer", function () { - var timeouts = []; - var messageName = "zero-timeout-message"; +"use strict"; - function handleMessage(event) { - if (event.source == window && event.data == messageName) { - event.stopPropagation(); - if (timeouts.length > 0) { - var fn = timeouts.shift(); - fn(); - } +var timeouts = []; +var messageName = "zero-timeout-message"; + +function handleMessage(event) { + if (event.source == window && event.data == messageName) { + event.stopPropagation(); + if (timeouts.length > 0) { + var fn = timeouts.shift(); + fn(); } } +} - window.addEventListener("message", handleMessage, true); +window.addEventListener("message", handleMessage, true); - return function (fn) { - timeouts.push(fn); - window.postMessage(messageName, "*"); - }; -}); \ No newline at end of file +module.exports = function (fn) { + timeouts.push(fn); + window.postMessage(messageName, "*"); +}; diff --git a/lib/encoders.js b/lib/encoders.js index 064a7ec..c11409b 100644 --- a/lib/encoders.js +++ b/lib/encoders.js @@ -1,272 +1,271 @@ -/*global define*/ -define("js-git/lib/encoders", function () { - var sha1 = require('js-git/lib/sha1'); - var binary = require('js-git/lib/binary'); +"use strict"; - // Run sanity tests at startup. - test(); +var sha1 = require('./sha1.js'); +var binary = require('./binary.js'); +var modes = require('./modes.js'); - return { - frame: frame, - normalizeAs: normalizeAs, - normalizeBlob: normalizeBlob, - normalizeTree: normalizeTree, - normalizeCommit: normalizeCommit, - normalizeTag: normalizeTag, - encodeAs: encodeAs, - encodeBlob: encodeBlob, - encodeTree: encodeTree, - encodeCommit: encodeCommit, - encodeTag: encodeTag, - hashAs: hashAs, - pathCmp: pathCmp - }; - - function test() { - // Test blob encoding - var normalized = normalizeBlob("Hello World\n"); - var expected = "557db03de997c86a4a028e1ebd3a1ceb225be238"; - var hash = hashAs("blob", normalized); - if (hash !== expected) { - console.log({expected: expected, actual: hash, normalized: normalized}); - throw new Error("Invalid body hash"); - } +// Run sanity tests at startup. +test(); - // Test tree encoding - hash = hashAs("tree", normalizeTree({ "greeting.txt": { mode: 0100644, hash: hash } })); - if (hash !== "648fc86e8557bdabbc2c828a19535f833727fa62") { - throw new Error("Invalid tree hash"); - } +module.exports = { + frame: frame, + normalizeAs: normalizeAs, + normalizeBlob: normalizeBlob, + normalizeTree: normalizeTree, + normalizeCommit: normalizeCommit, + normalizeTag: normalizeTag, + encodeAs: encodeAs, + encodeBlob: encodeBlob, + encodeTree: encodeTree, + encodeCommit: encodeCommit, + encodeTag: encodeTag, + hashAs: hashAs, + pathCmp: pathCmp +}; - var date = new Date(1391790884000); - date.timezoneOffset = 7 * 60; - // Test commit encoding - hash = hashAs("commit", normalizeCommit({ - tree: hash, - author: { - name: "Tim Caswell", - email: "tim@creationix.com", - date: date - }, - message: "Test Commit\n" - })); - if (hash !== "500c37fc17988b90c82d812a2d6fc25b15354bf2") { - throw new Error("Invalid commit hash"); - } - - // Test annotated tag encoding - date = new Date(1391790910000); - date.timezoneOffset = 7 * 60; - hash = hashAs("tag", normalizeTag({ - object: hash, - type: "commit", - tag: "mytag", - tagger: { - name: "Tim Caswell", - email: "tim@creationix.com", - date: date, - }, - message: "Tag it!\n" - })); - if (hash !== "49522787662a0183652dc9cafa5c008b5a0e0c2a") { - throw new Error("Invalid annotated tag hash"); - } +function test() { + // Test blob encoding + var normalized = normalizeBlob("Hello World\n"); + var expected = "557db03de997c86a4a028e1ebd3a1ceb225be238"; + var hash = hashAs("blob", normalized); + if (hash !== expected) { + console.log({expected: expected, actual: hash, normalized: normalized}); + throw new Error("Invalid body hash"); } - function encodeAs(type, body) { - if (type === "blob") return encodeBlob(body); - if (type === "tree") return encodeTree(body); - if (type === "commit") return encodeCommit(body); - if (type === "tag") return encodeTag(body); + // Test tree encoding + hash = hashAs("tree", normalizeTree({ "greeting.txt": { mode: modes.file, hash: hash } })); + if (hash !== "648fc86e8557bdabbc2c828a19535f833727fa62") { + throw new Error("Invalid tree hash"); } - function normalizeAs(type, body) { - if (type === "blob") return normalizeBlob(body); - if (type === "tree") return normalizeTree(body); - if (type === "commit") return normalizeCommit(body); - if (type === "tag") return normalizeTag(body); + var date = new Date(1391790884000); + date.timezoneOffset = 7 * 60; + // Test commit encoding + hash = hashAs("commit", normalizeCommit({ + tree: hash, + author: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date + }, + message: "Test Commit\n" + })); + if (hash !== "500c37fc17988b90c82d812a2d6fc25b15354bf2") { + throw new Error("Invalid commit hash"); } - // Calculate a git compatable hash by git encoding the body and prepending a - // git style frame header and calculating the sha1 sum of that. - function hashAs(type, body) { - var encoded = encodeAs(type, body); - var sum = sha1(); - sum.update(frame(type, encoded.length)); - sum.update(encoded); - return sum.digest(); + // Test annotated tag encoding + date = new Date(1391790910000); + date.timezoneOffset = 7 * 60; + hash = hashAs("tag", normalizeTag({ + object: hash, + type: "commit", + tag: "mytag", + tagger: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date, + }, + message: "Tag it!\n" + })); + if (hash !== "49522787662a0183652dc9cafa5c008b5a0e0c2a") { + throw new Error("Invalid annotated tag hash"); } +} - function frame(type, length) { - return type + " " + length + "\0"; - } +function encodeAs(type, body) { + if (type === "blob") return encodeBlob(body); + if (type === "tree") return encodeTree(body); + if (type === "commit") return encodeCommit(body); + if (type === "tag") return encodeTag(body); +} - function normalizeBlob(body) { - var type = typeof body; - if (type === "string") { - return binary.fromRaw(body); - } - if (body && type === "object") { - if (body.constructor.name === "ArrayBuffer") body = new Uint8Array(body); - if (typeof body.length === "number") { - return body;//binary.toRaw(body); - } +function normalizeAs(type, body) { + if (type === "blob") return normalizeBlob(body); + if (type === "tree") return normalizeTree(body); + if (type === "commit") return normalizeCommit(body); + if (type === "tag") return normalizeTag(body); +} + +// Calculate a git compatable hash by git encoding the body and prepending a +// git style frame header and calculating the sha1 sum of that. +function hashAs(type, body) { + var encoded = encodeAs(type, body); + var sum = sha1(); + sum.update(frame(type, encoded.length)); + sum.update(encoded); + return sum.digest(); +} + +function frame(type, length) { + return type + " " + length + "\0"; +} + +function normalizeBlob(body) { + var type = typeof body; + if (type === "string") { + return binary.fromRaw(body); + } + if (body && type === "object") { + if (body.constructor.name === "ArrayBuffer") body = new Uint8Array(body); + if (typeof body.length === "number") { + return body;//binary.toRaw(body); } - throw new TypeError("Blob body must be raw string, ArrayBuffer or byte array"); } + throw new TypeError("Blob body must be raw string, ArrayBuffer or byte array"); +} - function encodeBlob(body) { - return body; - } +function encodeBlob(body) { + return body; +} - function normalizeTree(body) { - var type = body && typeof body; - if (type !== "object") { - throw new TypeError("Tree body must be array or object"); - } - var tree = {}, i, l, entry; - // If array form is passed in, convert to object form. - if (Array.isArray(body)) { - for (i = 0, l = body.length; i < l; i++) { - entry = body[i]; - tree[entry.name] = { - mode: entry.mode, - hash: entry.hash - }; - } - } - else { - var names = Object.keys(body); - for (i = 0, l = names.length; i < l; i++) { - var name = names[i]; - entry = body[name]; - tree[name] = { - mode: entry.mode, - hash: entry.hash - }; - } +function normalizeTree(body) { + var type = body && typeof body; + if (type !== "object") { + throw new TypeError("Tree body must be array or object"); + } + var tree = {}, i, l, entry; + // If array form is passed in, convert to object form. + if (Array.isArray(body)) { + for (i = 0, l = body.length; i < l; i++) { + entry = body[i]; + tree[entry.name] = { + mode: entry.mode, + hash: entry.hash + }; } - return tree; } - - function encodeTree(body) { - var tree = ""; - var names = Object.keys(body).sort(pathCmp); - for (var i = 0, l = names.length; i < l; i++) { + else { + var names = Object.keys(body); + for (i = 0, l = names.length; i < l; i++) { var name = names[i]; - var entry = body[name]; - tree += entry.mode.toString(8) + " " + name - + "\0" + binary.decodeHex(entry.hash); + entry = body[name]; + tree[name] = { + mode: entry.mode, + hash: entry.hash + }; } - return tree; } + return tree; +} - function normalizeCommit(body) { - if (!body || typeof body !== "object") { - throw new TypeError("Commit body must be an object"); - } - if (!(body.tree && body.author && body.message)) { - throw new TypeError("Tree, author, and message are required for commits"); - } - var parents = body.parents || (body.parent ? [ body.parent ] : []); - if (!Array.isArray(parents)) { - throw new TypeError("Parents must be an array"); - } - var author = normalizePerson(body.author); - var committer = body.committer ? normalizePerson(body.committer) : author; - return { - tree: body.tree, - parents: parents, - author: author, - committer: committer, - message: body.message - }; +function encodeTree(body) { + var tree = ""; + var names = Object.keys(body).sort(pathCmp); + for (var i = 0, l = names.length; i < l; i++) { + var name = names[i]; + var entry = body[name]; + tree += entry.mode.toString(8) + " " + name + + "\0" + binary.decodeHex(entry.hash); } + return tree; +} - function encodeCommit(body) { - var str = "tree " + body.tree; - for (var i = 0, l = body.parents.length; i < l; ++i) { - str += "\nparent " + body.parents[i]; - } - str += "\nauthor " + formatPerson(body.author) + - "\ncommitter " + formatPerson(body.committer) + - "\n\n" + body.message; - return binary.encodeUtf8(str); +function normalizeCommit(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Commit body must be an object"); } - - function normalizeTag(body) { - if (!body || typeof body !== "object") { - throw new TypeError("Tag body must be an object"); - } - if (!(body.object && body.type && body.tag && body.tagger && body.message)) { - throw new TypeError("Object, type, tag, tagger, and message required"); - } - return { - object: body.object, - type: body.type, - tag: body.tag, - tagger: normalizePerson(body.tagger), - message: body.message - }; + if (!(body.tree && body.author && body.message)) { + throw new TypeError("Tree, author, and message are required for commits"); } - - function encodeTag(body) { - var str = "object " + body.object + - "\ntype " + body.type + - "\ntag " + body.tag + - "\ntagger " + formatPerson(body.tagger) + - "\n\n" + body.message; - return binary.encodeUtf8(str); + var parents = body.parents || (body.parent ? [ body.parent ] : []); + if (!Array.isArray(parents)) { + throw new TypeError("Parents must be an array"); } + var author = normalizePerson(body.author); + var committer = body.committer ? normalizePerson(body.committer) : author; + return { + tree: body.tree, + parents: parents, + author: author, + committer: committer, + message: body.message + }; +} - // Tree entries are sorted by the byte sequence that comprises - // the entry name. However, for the purposes of the sort - // comparison, entries for tree objects are compared as if the - // entry name byte sequence has a trailing ASCII '/' (0x2f). - function pathCmp(a, b) { - // TODO: this spec seems to be wrong. It doesn't match the sort order used - // by real git. - // a = binary.encodeUtf8(a) + "/"; - // b = binary.encodeUtf8(b) + "/"; - return a < b ? -1 : a > b ? 1 : 0; +function encodeCommit(body) { + var str = "tree " + body.tree; + for (var i = 0, l = body.parents.length; i < l; ++i) { + str += "\nparent " + body.parents[i]; } + str += "\nauthor " + formatPerson(body.author) + + "\ncommitter " + formatPerson(body.committer) + + "\n\n" + body.message; + return binary.encodeUtf8(str); +} - function normalizePerson(person) { - if (!person || typeof person !== "object") { - throw new TypeError("Person must be an object"); - } - if (!person.name || !person.email) { - throw new TypeError("Name and email are required for person fields"); - } - return { - name: person.name, - email: person.email, - date: person.date || new Date() - }; +function normalizeTag(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Tag body must be an object"); } - - function formatPerson(person) { - return safe(person.name) + - " <" + safe(person.email) + "> " + - formatDate(person.date); + if (!(body.object && body.type && body.tag && body.tagger && body.message)) { + throw new TypeError("Object, type, tag, tagger, and message required"); } + return { + object: body.object, + type: body.type, + tag: body.tag, + tagger: normalizePerson(body.tagger), + message: body.message + }; +} - function safe(string) { - return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); - } +function encodeTag(body) { + var str = "object " + body.object + + "\ntype " + body.type + + "\ntag " + body.tag + + "\ntagger " + formatPerson(body.tagger) + + "\n\n" + body.message; + return binary.encodeUtf8(str); +} - function two(num) { - return (num < 10 ? "0" : "") + num; - } +// Tree entries are sorted by the byte sequence that comprises +// the entry name. However, for the purposes of the sort +// comparison, entries for tree objects are compared as if the +// entry name byte sequence has a trailing ASCII '/' (0x2f). +function pathCmp(a, b) { + // TODO: this spec seems to be wrong. It doesn't match the sort order used + // by real git. + // a = binary.encodeUtf8(a) + "/"; + // b = binary.encodeUtf8(b) + "/"; + return a < b ? -1 : a > b ? 1 : 0; +} - function formatDate(date) { - var offset = date.timezoneOffset || date.getTimezoneOffset(); - var neg = "+"; - if (offset <= 0) offset = -offset; - else neg = "-"; - offset = neg + two(Math.floor(offset / 60)) + two(offset % 60); - var seconds = Math.floor(date.getTime() / 1000); - return seconds + " " + offset; +function normalizePerson(person) { + if (!person || typeof person !== "object") { + throw new TypeError("Person must be an object"); + } + if (!person.name || !person.email) { + throw new TypeError("Name and email are required for person fields"); } + return { + name: person.name, + email: person.email, + date: person.date || new Date() + }; +} + +function formatPerson(person) { + return safe(person.name) + + " <" + safe(person.email) + "> " + + formatDate(person.date); +} + +function safe(string) { + return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); +} + +function two(num) { + return (num < 10 ? "0" : "") + num; +} -}); \ No newline at end of file +function formatDate(date) { + var offset = date.timezoneOffset || date.getTimezoneOffset(); + var neg = "+"; + if (offset <= 0) offset = -offset; + else neg = "-"; + offset = neg + two(Math.floor(offset / 60)) + two(offset % 60); + var seconds = Math.floor(date.getTime() / 1000); + return seconds + " " + offset; +} diff --git a/lib/modes.js b/lib/modes.js index 2288516..79128e4 100644 --- a/lib/modes.js +++ b/lib/modes.js @@ -1,24 +1,21 @@ -/*global define*/ -define("js-git/lib/modes", function () { - // Not strict mode because it uses octal literals all over. - return { - isBlob: function (mode) { - return (mode & 0140000) === 0100000; - }, - isFile: function (mode) { - return (mode & 0160000) === 0100000; - }, - toType: function (mode) { - if (mode === 0160000) return "commit"; - if (mode === 040000) return "tree"; - if ((mode & 0140000) === 0100000) return "blob"; - return "unknown"; - }, - tree: 040000, - blob: 0100644, - file: 0100644, - exec: 0100755, - sym: 0120000, - commit: 0160000 - }; -}); \ No newline at end of file +// Not strict mode because it uses octal literals all over. +module.exports = { + isBlob: function (mode) { + return (mode & 0140000) === 0100000; + }, + isFile: function (mode) { + return (mode & 0160000) === 0100000; + }, + toType: function (mode) { + if (mode === 0160000) return "commit"; + if (mode === 040000) return "tree"; + if ((mode & 0140000) === 0100000) return "blob"; + return "unknown"; + }, + tree: 040000, + blob: 0100644, + file: 0100644, + exec: 0100755, + sym: 0120000, + commit: 0160000 +}; diff --git a/lib/sha1.js b/lib/sha1.js index a5c9d24..dff5f07 100644 --- a/lib/sha1.js +++ b/lib/sha1.js @@ -1,151 +1,148 @@ -/*global define*/ -define("js-git/lib/sha1", function () { - "use strict"; - - // Input chunks must be either arrays of bytes or "raw" encoded strings - return function sha1(buffer) { - if (buffer === undefined) return create(); - var shasum = create(); - shasum.update(buffer); - return shasum.digest(); - }; - - // A streaming interface for when nothing is passed in. - function create() { - var h0 = 0x67452301; - var h1 = 0xEFCDAB89; - var h2 = 0x98BADCFE; - var h3 = 0x10325476; - var h4 = 0xC3D2E1F0; - // The first 64 bytes (16 words) is the data chunk - var block = new Uint32Array(80), offset = 0, shift = 24; - var totalLength = 0; - - return { update: update, digest: digest }; - - // The user gave us more data. Store it! - function update(chunk) { - if (typeof chunk === "string") return updateString(chunk); - var length = chunk.length; - totalLength += length * 8; - for (var i = 0; i < length; i++) { - write(chunk[i]); - } +"use strict"; + +// Input chunks must be either arrays of bytes or "raw" encoded strings +module.exports = function sha1(buffer) { + if (buffer === undefined) return create(); + var shasum = create(); + shasum.update(buffer); + return shasum.digest(); +}; + +// A streaming interface for when nothing is passed in. +function create() { + var h0 = 0x67452301; + var h1 = 0xEFCDAB89; + var h2 = 0x98BADCFE; + var h3 = 0x10325476; + var h4 = 0xC3D2E1F0; + // The first 64 bytes (16 words) is the data chunk + var block = new Uint32Array(80), offset = 0, shift = 24; + var totalLength = 0; + + return { update: update, digest: digest }; + + // The user gave us more data. Store it! + function update(chunk) { + if (typeof chunk === "string") return updateString(chunk); + var length = chunk.length; + totalLength += length * 8; + for (var i = 0; i < length; i++) { + write(chunk[i]); } + } - function updateString(string) { - var length = string.length; - totalLength += length * 8; - for (var i = 0; i < length; i++) { - write(string.charCodeAt(i)); - } + function updateString(string) { + var length = string.length; + totalLength += length * 8; + for (var i = 0; i < length; i++) { + write(string.charCodeAt(i)); } + } - function write(byte) { - block[offset] |= (byte & 0xff) << shift; - if (shift) { - shift -= 8; - } - else { - offset++; - shift = 24; - } - if (offset === 16) processBlock(); - } - // No more data will come, pad the block, process and return the result. - function digest() { - // Pad - write(0x80); - if (offset > 14 || (offset === 14 && shift < 24)) { - processBlock(); - } - offset = 14; + function write(byte) { + block[offset] |= (byte & 0xff) << shift; + if (shift) { + shift -= 8; + } + else { + offset++; shift = 24; + } + if (offset === 16) processBlock(); + } - // 64-bit length big-endian - write(0x00); // numbers this big aren't accurate in javascript anyway - write(0x00); // ..So just hard-code to zero. - write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); - write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); - for (var s = 24; s >= 0; s -= 8) { - write(totalLength >> s); - } + // No more data will come, pad the block, process and return the result. + function digest() { + // Pad + write(0x80); + if (offset > 14 || (offset === 14 && shift < 24)) { + processBlock(); + } + offset = 14; + shift = 24; + + // 64-bit length big-endian + write(0x00); // numbers this big aren't accurate in javascript anyway + write(0x00); // ..So just hard-code to zero. + write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); + write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); + for (var s = 24; s >= 0; s -= 8) { + write(totalLength >> s); + } - // At this point one last processBlock() should trigger and we can pull out the result. - return toHex(h0) - + toHex(h1) - + toHex(h2) - + toHex(h3) - + toHex(h4); + // At this point one last processBlock() should trigger and we can pull out the result. + return toHex(h0) + + toHex(h1) + + toHex(h2) + + toHex(h3) + + toHex(h4); + } + + // We have a full block to process. Let's do it! + function processBlock() { + // Extend the sixteen 32-bit words into eighty 32-bit words: + for (var i = 16; i < 80; i++) { + var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; + block[i] = (w << 1) | (w >>> 31); } - // We have a full block to process. Let's do it! - function processBlock() { - // Extend the sixteen 32-bit words into eighty 32-bit words: - for (var i = 16; i < 80; i++) { - var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; - block[i] = (w << 1) | (w >>> 31); + // log(block); + + // Initialize hash value for this chunk: + var a = h0; + var b = h1; + var c = h2; + var d = h3; + var e = h4; + var f, k; + + // Main loop: + for (i = 0; i < 80; i++) { + if (i < 20) { + f = d ^ (b & (c ^ d)); + k = 0x5A827999; } - - // log(block); - - // Initialize hash value for this chunk: - var a = h0; - var b = h1; - var c = h2; - var d = h3; - var e = h4; - var f, k; - - // Main loop: - for (i = 0; i < 80; i++) { - if (i < 20) { - f = d ^ (b & (c ^ d)); - k = 0x5A827999; - } - else if (i < 40) { - f = b ^ c ^ d; - k = 0x6ED9EBA1; - } - else if (i < 60) { - f = (b & c) | (d & (b | c)); - k = 0x8F1BBCDC; - } - else { - f = b ^ c ^ d; - k = 0xCA62C1D6; - } - var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); - e = d; - d = c; - c = (b << 30 | b >>> 2); - b = a; - a = temp; + else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; } - - // Add this chunk's hash to result so far: - h0 = (h0 + a) | 0; - h1 = (h1 + b) | 0; - h2 = (h2 + c) | 0; - h3 = (h3 + d) | 0; - h4 = (h4 + e) | 0; - - // The block is now reusable. - offset = 0; - for (i = 0; i < 16; i++) { - block[i] = 0; + else if (i < 60) { + f = (b & c) | (d & (b | c)); + k = 0x8F1BBCDC; + } + else { + f = b ^ c ^ d; + k = 0xCA62C1D6; } + var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); + e = d; + d = c; + c = (b << 30 | b >>> 2); + b = a; + a = temp; } - function toHex(word) { - var hex = ""; - for (var i = 28; i >= 0; i -= 4) { - hex += ((word >> i) & 0xf).toString(16); - } - return hex; + // Add this chunk's hash to result so far: + h0 = (h0 + a) | 0; + h1 = (h1 + b) | 0; + h2 = (h2 + c) | 0; + h3 = (h3 + d) | 0; + h4 = (h4 + e) | 0; + + // The block is now reusable. + offset = 0; + for (i = 0; i < 16; i++) { + block[i] = 0; } + } + function toHex(word) { + var hex = ""; + for (var i = 28; i >= 0; i -= 4) { + hex += ((word >> i) & 0xf).toString(16); + } + return hex; } -}); \ No newline at end of file +} diff --git a/lib/xhr.js b/lib/xhr.js index 03247b2..21bb148 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -1,33 +1,32 @@ -/*global define*/ -define("js-git/lib/xhr", function () { - return function (root, accessToken) { - return function request(method, url, body, callback) { - if (typeof body === "function" && callback === undefined) { - callback = body; - body = undefined; +"use strict"; + +module.exports = function (root, accessToken) { + return function request(method, url, body, callback) { + if (typeof body === "function" && callback === undefined) { + callback = body; + body = undefined; + } + url = url.replace(":root", root); + if (!callback) return request.bind(this, accessToken, method, url, body); + var json; + var xhr = new XMLHttpRequest(); + xhr.open(method, 'https://api.github.com' + url, true); + xhr.setRequestHeader("Authorization", "token " + accessToken); + if (body) { + xhr.setRequestHeader("Content-Type", "application/json"); + try { json = JSON.stringify(body); } + catch (err) { return callback(err); } + } + xhr.onreadystatechange = onReadyStateChange; + xhr.send(json); + function onReadyStateChange() { + if (xhr.readyState !== 4) return; + var response; + if (xhr.responseText) { + try { response = JSON.parse(xhr.responseText); } + catch (err) { return callback(err, null, xhr, response); } } - url = url.replace(":root", root); - if (!callback) return request.bind(this, accessToken, method, url, body); - var json; - var xhr = new XMLHttpRequest(); - xhr.open(method, 'https://api.github.com' + url, true); - xhr.setRequestHeader("Authorization", "token " + accessToken); - if (body) { - xhr.setRequestHeader("Content-Type", "application/json"); - try { json = JSON.stringify(body); } - catch (err) { return callback(err); } - } - xhr.onreadystatechange = onReadyStateChange; - xhr.send(json); - function onReadyStateChange() { - if (xhr.readyState !== 4) return; - var response; - if (xhr.responseText) { - try { response = JSON.parse(xhr.responseText); } - catch (err) { return callback(err, null, xhr, response); } - } - return callback(null, xhr, response); - } - }; + return callback(null, xhr, response); + } }; -}); \ No newline at end of file +}; diff --git a/mixins/add-cache.js b/mixins/add-cache.js index b85f2d3..52e2f09 100644 --- a/mixins/add-cache.js +++ b/mixins/add-cache.js @@ -1,67 +1,61 @@ -/*global define*/ -/*jshint unused:strict,undef:true,trailing:true */ -define("js-git/mixins/add-cache", function () { +"use strict"; + +module.exports = addCache; +function addCache(repo, cache) { + var loadAs = repo.loadAs; + if (loadAs) repo.loadAs = loadAsCached; + var saveAs = repo.saveAs; + if (saveAs) repo.saveAs = saveAsCached; + var createTree = repo.createTree; + if (createTree) repo.createTree = createTreeCached; + + function loadAsCached(type, hash, callback) { + // Next check in disk cache... + cache.loadAs(type, hash, onCacheLoad); + + function onCacheLoad(err, value) { + if (err) return callback(err); + // ...and return if it's there. + if (value !== undefined) { + return callback(null, value, hash); + } - return addCache; - function addCache(repo, cache) { - var loadAs = repo.loadAs; - if (loadAs) repo.loadAs = loadAsCached; - var saveAs = repo.saveAs; - if (saveAs) repo.saveAs = saveAsCached; - var createTree = repo.createTree; - if (createTree) repo.createTree = createTreeCached; + // Otherwise load from real data source... + loadAs.call(repo, type, hash, onLoad); + } - function loadAsCached(type, hash, callback) { - if (!callback) return loadAsCached.bind(this, type, hash); + function onLoad(err, value) { + if (value === undefined) return callback(err); - // Next check in disk cache... - cache.loadAs(type, hash, onCacheLoad); + // Store it on disk too... + // Force the hash to prevent mismatches. + cache.saveAs(type, value, onSave, hash); - function onCacheLoad(err, value) { + function onSave(err) { if (err) return callback(err); - // ...and return if it's there. - if (value !== undefined) { - return callback(null, value, hash); - } - - // Otherwise load from real data source... - loadAs.call(repo, type, hash, onLoad); - } - - function onLoad(err, value) { - if (value === undefined) return callback(err); - - // Store it on disk too... - // Force the hash to prevent mismatches. - cache.saveAs(type, value, onSave, hash); - - function onSave(err) { - if (err) return callback(err); - // Finally return the value to caller. - callback(null, value, hash); - } + // Finally return the value to caller. + callback(null, value, hash); } } + } - function saveAsCached(type, value, callback) { - saveAs.call(repo, type, value, onSave); + function saveAsCached(type, value, callback) { + saveAs.call(repo, type, value, onSave); - function onSave(err, hash, value) { - if (err) return callback(err); - // Store in disk, forcing hash to match. - cache.saveAs(type, value, callback, hash); - } + function onSave(err, hash, value) { + if (err) return callback(err); + // Store in disk, forcing hash to match. + cache.saveAs(type, value, callback, hash); } + } - function createTreeCached(entries, callback) { - createTree.call(repo, entries, onTree); + function createTreeCached(entries, callback) { + createTree.call(repo, entries, onTree); - function onTree(err, hash, tree) { - if (err) return callback(err); - cache.saveAs("tree", tree, callback, hash); - } + function onTree(err, hash, tree) { + if (err) return callback(err); + cache.saveAs("tree", tree, callback, hash); } - } -}); +} diff --git a/mixins/create-tree.js b/mixins/create-tree.js index 823f310..9e2c790 100644 --- a/mixins/create-tree.js +++ b/mixins/create-tree.js @@ -1,145 +1,140 @@ -/*global define*/ -define("js-git/mixins/create-tree", function () { - "use strict"; - - var modes = require('js-git/lib/modes'); - - return function (repo) { - repo.createTree = createTree; - - function createTree(entries, callback) { - if (!callback) return createTree.bind(null, entries); - callback = singleCall(callback); - - // Tree paths that we need loaded - var toLoad = {}; - function markTree(path) { - while(true) { - if (toLoad[path]) return; - toLoad[path] = true; - trees[path] = { - add: [], - del: [], - tree: {} - }; - if (!path) break; - path = path.substring(0, path.lastIndexOf("/")); - } +"use strict"; + +var modes = require('../lib/modes.js'); + +module.exports = function (repo) { + repo.createTree = createTree; + + function createTree(entries, callback) { + callback = singleCall(callback); + + // Tree paths that we need loaded + var toLoad = {}; + function markTree(path) { + while(true) { + if (toLoad[path]) return; + toLoad[path] = true; + trees[path] = { + add: [], + del: [], + tree: {} + }; + if (!path) break; + path = path.substring(0, path.lastIndexOf("/")); } + } - // Commands to run organized by tree path - var trees = {}; - - // Counter for parallel I/O operations - var left = 1; // One extra counter to protect again zalgo cache callbacks. - - // First pass, stubs out the trees structure, sorts adds from deletes, - // and saves any inline content blobs. - entries.forEach(function (entry) { - var index = entry.path.lastIndexOf("/"); - var parentPath = entry.path.substr(0, index); - var name = entry.path.substr(index + 1); - markTree(parentPath); - var tree = trees[parentPath]; - var adds = tree.add; - var dels = tree.del; - - if (!entry.mode) { - dels.push(name); - return; - } - var add = { - name: name, - mode: entry.mode, - hash: entry.hash - }; - adds.push(add); - if (entry.hash) return; - left++; - repo.saveAs("blob", entry.content, function (err, hash) { - if (err) return callback(err); - add.hash = hash; - check(); + // Commands to run organized by tree path + var trees = {}; + + // Counter for parallel I/O operations + var left = 1; // One extra counter to protect again zalgo cache callbacks. + + // First pass, stubs out the trees structure, sorts adds from deletes, + // and saves any inline content blobs. + entries.forEach(function (entry) { + var index = entry.path.lastIndexOf("/"); + var parentPath = entry.path.substr(0, index); + var name = entry.path.substr(index + 1); + markTree(parentPath); + var tree = trees[parentPath]; + var adds = tree.add; + var dels = tree.del; + + if (!entry.mode) { + dels.push(name); + return; + } + var add = { + name: name, + mode: entry.mode, + hash: entry.hash + }; + adds.push(add); + if (entry.hash) return; + left++; + repo.saveAs("blob", entry.content, function (err, hash) { + if (err) return callback(err); + add.hash = hash; + check(); + }); + }); + + // Preload the base trees + if (entries.base) loadTree("", entries.base); + + // Check just in case there was no IO to perform + check(); + + function loadTree(path, hash) { + left++; + delete toLoad[path]; + repo.loadAs("tree", hash, function (err, tree) { + if (err) return callback(err); + trees[path].tree = tree; + Object.keys(tree).forEach(function (name) { + var childPath = path ? path + "/" + name : name; + if (toLoad[childPath]) loadTree(childPath, tree[name].hash); }); + check(); }); + } - // Preload the base trees - if (entries.base) loadTree("", entries.base); - - // Check just in case there was no IO to perform - check(); + function check() { + if (--left) return; + findLeaves().forEach(processLeaf); + } - function loadTree(path, hash) { - left++; - delete toLoad[path]; - repo.loadAs("tree", hash, function (err, tree) { - if (err) return callback(err); - trees[path].tree = tree; - Object.keys(tree).forEach(function (name) { - var childPath = path ? path + "/" + name : name; - if (toLoad[childPath]) loadTree(childPath, tree[name].hash); - }); - check(); + function processLeaf(path) { + var entry = trees[path]; + delete trees[path]; + var tree = entry.tree; + entry.del.forEach(function (name) { + delete tree[name]; + }); + entry.add.forEach(function (item) { + tree[item.name] = { + mode: item.mode, + hash: item.hash + }; + }); + left++; + repo.saveAs("tree", tree, function (err, hash, tree) { + if (err) return callback(err); + if (!path) return callback(null, hash, tree); + var index = path.lastIndexOf("/"); + var parentPath = path.substring(0, index); + var name = path.substring(index + 1); + trees[parentPath].add.push({ + name: name, + mode: modes.tree, + hash: hash }); - } - - function check() { if (--left) return; findLeaves().forEach(processLeaf); - } - - function processLeaf(path) { - var entry = trees[path]; - delete trees[path]; - var tree = entry.tree; - entry.del.forEach(function (name) { - delete tree[name]; - }); - entry.add.forEach(function (item) { - tree[item.name] = { - mode: item.mode, - hash: item.hash - }; - }); - left++; - repo.saveAs("tree", tree, function (err, hash, tree) { - if (err) return callback(err); - if (!path) return callback(null, hash, tree); - var index = path.lastIndexOf("/"); - var parentPath = path.substring(0, index); - var name = path.substring(index + 1); - trees[parentPath].add.push({ - name: name, - mode: modes.tree, - hash: hash - }); - if (--left) return; - findLeaves().forEach(processLeaf); - }); - } - - function findLeaves() { - var paths = Object.keys(trees); - var parents = {}; - paths.forEach(function (path) { - if (!path) return; - var parent = path.substring(0, path.lastIndexOf("/")); - parents[parent] = true; - }); - return paths.filter(function (path) { - return !parents[path]; - }); - } + }); } - }; - function singleCall(callback) { - var done = false; - return function () { - if (done) return console.warn("Discarding extra callback"); - done = true; - return callback.apply(this, arguments); - }; + function findLeaves() { + var paths = Object.keys(trees); + var parents = {}; + paths.forEach(function (path) { + if (!path) return; + var parent = path.substring(0, path.lastIndexOf("/")); + parents[parent] = true; + }); + return paths.filter(function (path) { + return !parents[path]; + }); + } } - -}); \ No newline at end of file +}; + +function singleCall(callback) { + var done = false; + return function () { + if (done) return console.warn("Discarding extra callback"); + done = true; + return callback.apply(this, arguments); + }; +} diff --git a/mixins/delay.js b/mixins/delay.js index 63afc90..18a3650 100644 --- a/mixins/delay.js +++ b/mixins/delay.js @@ -1,55 +1,46 @@ -/*global define*/ -define("js-git/mixins/delay", function () { - "use strict"; - - return function (repo, ms) { - var saveAs = repo.saveAs; - var loadAs = repo.loadAs; - var readRef = repo.readRef; - var updateRef = repo.updateRef; - var createTree = repo.createTree; - - repo.saveAs = saveAsDelayed; - repo.loadAs = loadASDelayed; - repo.readRef = readRefDelayed; - repo.updateRed = updateRefDelayed; - if (createTree) repo.createTree = createTreeDelayed; - - function saveAsDelayed(type, value, callback) { - if (!callback) return saveAsDelayed.bind(null, type, value); - setTimeout(function () { - return saveAs.call(repo, type, value, callback); - }, ms); - } - - function loadASDelayed(type, hash, callback) { - if (!callback) return loadASDelayed.bind(null, type, hash); - setTimeout(function () { - return loadAs.call(repo, type, hash, callback); - }, ms); - } - - function readRefDelayed(ref, callback) { - if (!callback) return readRefDelayed.bind(null, ref); - setTimeout(function () { - return readRef.call(repo, ref, callback); - }, ms); - } - - function updateRefDelayed(ref, hash, callback) { - if (!callback) return updateRefDelayed.bind(null, ref, hash); - setTimeout(function () { - return updateRef.call(repo, ref, hash, callback); - }, ms); - } - - function createTreeDelayed(entries, callback) { - if (!callback) return createTreeDelayed.bind(null, entries); - setTimeout(function () { - return createTree.call(repo, entries, callback); - }, ms); - } - - }; - -}); \ No newline at end of file +"use strict"; + +module.exports = function (repo, ms) { + var saveAs = repo.saveAs; + var loadAs = repo.loadAs; + var readRef = repo.readRef; + var updateRef = repo.updateRef; + var createTree = repo.createTree; + + repo.saveAs = saveAsDelayed; + repo.loadAs = loadASDelayed; + repo.readRef = readRefDelayed; + repo.updateRed = updateRefDelayed; + if (createTree) repo.createTree = createTreeDelayed; + + function saveAsDelayed(type, value, callback) { + setTimeout(function () { + return saveAs.call(repo, type, value, callback); + }, ms); + } + + function loadASDelayed(type, hash, callback) { + setTimeout(function () { + return loadAs.call(repo, type, hash, callback); + }, ms); + } + + function readRefDelayed(ref, callback) { + setTimeout(function () { + return readRef.call(repo, ref, callback); + }, ms); + } + + function updateRefDelayed(ref, hash, callback) { + setTimeout(function () { + return updateRef.call(repo, ref, hash, callback); + }, ms); + } + + function createTreeDelayed(entries, callback) { + setTimeout(function () { + return createTree.call(repo, entries, callback); + }, ms); + } + +}; diff --git a/mixins/formats.js b/mixins/formats.js index 6dd35cb..433947f 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -1,31 +1,28 @@ -define("js-git/mixins/formats", function () { +"use strict"; - var binary = require('js-git/lib/binary'); +var binary = require('../lib/binary.js'); - return function (repo) { - var loadAs = repo.loadAs; - repo.loadAs = newLoadAs; - function newLoadAs(type, hash, callback) { - if (!callback) return newLoadAs.bind(this, type, hash, callback); - var realType = type === "text" ? "blob": - type === "array" ? "tree" : type; - return loadAs.call(this, realType, hash, onLoad); - - function onLoad(err, body, hash) { - if (body === undefined) return callback.call(this, err); - if (type === "text") body = binary.toUnicode(body); - if (type === "array") body = toArray(body); - return callback.call(this, err, body, hash); - } - } - }; +module.exports = function (repo) { + var loadAs = repo.loadAs; + repo.loadAs = newLoadAs; + function newLoadAs(type, hash, callback) { + var realType = type === "text" ? "blob": + type === "array" ? "tree" : type; + return loadAs.call(repo, realType, hash, onLoad); - function toArray(tree) { - return Object.keys(tree).map(function (name) { - var entry = tree[name]; - entry.name = name; - return entry; - }); + function onLoad(err, body, hash) { + if (body === undefined) return callback(err); + if (type === "text") body = binary.toUnicode(body); + if (type === "array") body = toArray(body); + return callback(err, body, hash); + } } +}; -}); +function toArray(tree) { + return Object.keys(tree).map(function (name) { + var entry = tree[name]; + entry.name = name; + return entry; + }); +} diff --git a/mixins/github-db.js b/mixins/github-db.js index 3fa55ed..0b9ad15 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -1,433 +1,424 @@ -/*global define*/ -define("js-git/mixins/github-db", function () { - "use strict"; - - var normalizeAs = require('js-git/lib/encoders').normalizeAs; - var modes = require('js-git/lib/modes'); - var hashAs = require('js-git/lib/encoders').hashAs; - var xhr = require('js-git/lib/xhr'); - var binary = require('js-git/lib/binary'); - - var modeToType = { - "040000": "tree", - "100644": "blob", // normal file - "100655": "blob", // executable file - "120000": "blob", // symlink - "160000": "commit" // gitlink - }; - - var encoders = { - commit: encodeCommit, - tag: encodeTag, - tree: encodeTree, - blob: encodeBlob - }; - - var decoders = { - commit: decodeCommit, - tag: decodeTag, - tree: decodeTree, - blob: decodeBlob, - }; - - var emptyBlob = hashAs("blob", ""); - var emptyTree = hashAs("tree", []); - - - // Implement the js-git object interface using github APIs - return function (repo, root, accessToken) { - - var apiRequest = xhr(root, accessToken); - - repo.loadAs = loadAs; // (type, hash) -> value, hash - repo.saveAs = saveAs; // (type, value) -> hash, value - repo.readRef = readRef; // (ref) -> hash - repo.updateRef = updateRef; // (ref, hash) -> hash - repo.createTree = createTree; // (entries) -> hash, tree - - function loadAs(type, hash, callback) { - if (!callback) return loadAs.bind(null, type, hash); - // Github doesn't like empty trees, but we know them already. - if (type === "tree" && hash === emptyTree) return callback(null, {}, hash); - apiRequest("GET", "/repos/:root/git/" + type + "s/" + hash, onValue); - - function onValue(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 500) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - if (xhr.status >= 300 && xhr.status < 500) return callback(); - var body; - try { body = decoders[type].call(repo, result); } - catch (err) { return callback(err); } - if (hashAs(type, body) !== hash) { - if (fixDate(type, body, hash)) console.warn(type + " repaired", hash); - else console.error("Unable to repair " + type, hash); - } - return callback(null, body, hash); +"use strict"; + +var normalizeAs = require('../lib/encoders.js').normalizeAs; +var modes = require('../lib/modes.js'); +var hashAs = require('../lib/encoders.js').hashAs; +var xhr = require('../lib/xhr.js'); +var binary = require('../lib/binary.js'); + +var modeToType = { + "040000": "tree", + "100644": "blob", // normal file + "100655": "blob", // executable file + "120000": "blob", // symlink + "160000": "commit" // gitlink +}; + +var encoders = { + commit: encodeCommit, + tag: encodeTag, + tree: encodeTree, + blob: encodeBlob +}; + +var decoders = { + commit: decodeCommit, + tag: decodeTag, + tree: decodeTree, + blob: decodeBlob, +}; + +var emptyBlob = hashAs("blob", ""); +var emptyTree = hashAs("tree", []); + + +// Implement the js-git object interface using github APIs +module.exports = function (repo, root, accessToken) { + + var apiRequest = xhr(root, accessToken); + + repo.loadAs = loadAs; // (type, hash) -> value, hash + repo.saveAs = saveAs; // (type, value) -> hash, value + repo.readRef = readRef; // (ref) -> hash + repo.updateRef = updateRef; // (ref, hash) -> hash + repo.createTree = createTree; // (entries) -> hash, tree + + function loadAs(type, hash, callback) { + // Github doesn't like empty trees, but we know them already. + if (type === "tree" && hash === emptyTree) return callback(null, {}, hash); + apiRequest("GET", "/repos/:root/git/" + type + "s/" + hash, onValue); + + function onValue(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 500) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + } + if (xhr.status >= 300 && xhr.status < 500) return callback(); + var body; + try { body = decoders[type].call(repo, result); } + catch (err) { return callback(err); } + if (hashAs(type, body) !== hash) { + if (fixDate(type, body, hash)) console.warn(type + " repaired", hash); + else console.error("Unable to repair " + type, hash); } + return callback(null, body, hash); } + } - function saveAs(type, body, callback) { - if (!callback) return saveAs.bind(null, type, body); - var request; - try { - body = normalizeAs(type, body); - request = encoders[type](body); - } - catch (err) { - return callback(err); - } + function saveAs(type, body, callback) { + var request; + try { + body = normalizeAs(type, body); + request = encoders[type](body); + } + catch (err) { + return callback(err); + } - // Github doesn't allow creating empty trees. - if (type === "tree" && request.tree.length === 0) { - return callback(null, emptyTree, body); - } - return apiRequest("POST", "/repos/:root/git/" + type + "s", request, onWrite); + // Github doesn't allow creating empty trees. + if (type === "tree" && request.tree.length === 0) { + return callback(null, emptyTree, body); + } + return apiRequest("POST", "/repos/:root/git/" + type + "s", request, onWrite); - function onWrite(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - return callback(null, result.sha, body); + function onWrite(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); } + return callback(null, result.sha, body); } + } - // Create a tree with optional deep paths and create new blobs. - // Entries is an array of {mode, path, hash|content} - // Also deltas can be specified by setting entries.base to the hash of a tree - // in delta mode, entries can be removed by specifying just {path} - function createTree(entries, callback) { - if (!callback) return createTree.bind(null, entries); - var toDelete = entries.base && entries.filter(function (entry) { - return !entry.mode; - }).map(function (entry) { - return entry.path; - }); - if (toDelete && toDelete.length) return slowUpdateTree(entries, toDelete, callback); - return fastUpdateTree(entries, callback); - } + // Create a tree with optional deep paths and create new blobs. + // Entries is an array of {mode, path, hash|content} + // Also deltas can be specified by setting entries.base to the hash of a tree + // in delta mode, entries can be removed by specifying just {path} + function createTree(entries, callback) { + var toDelete = entries.base && entries.filter(function (entry) { + return !entry.mode; + }).map(function (entry) { + return entry.path; + }); + if (toDelete && toDelete.length) return slowUpdateTree(entries, toDelete, callback); + return fastUpdateTree(entries, callback); + } - function fastUpdateTree(entries, callback) { - var request = { tree: entries.map(mapTreeEntry) }; - if (entries.base) request.base_tree = entries.base; + function fastUpdateTree(entries, callback) { + var request = { tree: entries.map(mapTreeEntry) }; + if (entries.base) request.base_tree = entries.base; - apiRequest("POST", "/repos/:root/git/trees", request, onWrite); + apiRequest("POST", "/repos/:root/git/trees", request, onWrite); - function onWrite(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - return callback(null, result.sha, decoders.tree(result)); + function onWrite(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); } + return callback(null, result.sha, decoders.tree(result)); } + } - // Github doesn't support deleting entries via the createTree API, so we - // need to manually create those affected trees and modify the request. - function slowUpdateTree(entries, toDelete, callback) { - callback = singleCall(callback); - var root = entries.base; + // Github doesn't support deleting entries via the createTree API, so we + // need to manually create those affected trees and modify the request. + function slowUpdateTree(entries, toDelete, callback) { + callback = singleCall(callback); + var root = entries.base; - var left = 0; + var left = 0; - // Calculate trees that need to be re-built and save any provided content. - var parents = {}; - toDelete.forEach(function (path) { - var parentPath = path.substr(0, path.lastIndexOf("/")); - var parent = parents[parentPath] || (parents[parentPath] = { - add: {}, del: [] - }); - var name = path.substr(path.lastIndexOf("/") + 1); - parent.del.push(name); + // Calculate trees that need to be re-built and save any provided content. + var parents = {}; + toDelete.forEach(function (path) { + var parentPath = path.substr(0, path.lastIndexOf("/")); + var parent = parents[parentPath] || (parents[parentPath] = { + add: {}, del: [] }); - var other = entries.filter(function (entry) { - if (!entry.mode) return false; - var parentPath = entry.path.substr(0, entry.path.lastIndexOf("/")); - var parent = parents[parentPath]; - if (!parent) return true; - var name = entry.path.substr(entry.path.lastIndexOf("/") + 1); - if (entry.hash) { - parent.add[name] = { - mode: entry.mode, - hash: entry.hash - }; - return false; - } - left++; - repo.saveAs("blob", entry.content, function(err, hash) { - if (err) return callback(err); - parent.add[name] = { - mode: entry.mode, - hash: hash - }; - if (!--left) onParents(); - }); + var name = path.substr(path.lastIndexOf("/") + 1); + parent.del.push(name); + }); + var other = entries.filter(function (entry) { + if (!entry.mode) return false; + var parentPath = entry.path.substr(0, entry.path.lastIndexOf("/")); + var parent = parents[parentPath]; + if (!parent) return true; + var name = entry.path.substr(entry.path.lastIndexOf("/") + 1); + if (entry.hash) { + parent.add[name] = { + mode: entry.mode, + hash: entry.hash + }; return false; + } + left++; + repo.saveAs("blob", entry.content, function(err, hash) { + if (err) return callback(err); + parent.add[name] = { + mode: entry.mode, + hash: hash + }; + if (!--left) onParents(); }); - if (!left) onParents(); + return false; + }); + if (!left) onParents(); - function onParents() { - Object.keys(parents).forEach(function (parentPath) { - left++; - // TODO: remove this dependency on pathToEntry - repo.pathToEntry(root, parentPath, function (err, entry) { + function onParents() { + Object.keys(parents).forEach(function (parentPath) { + left++; + // TODO: remove this dependency on pathToEntry + repo.pathToEntry(root, parentPath, function (err, entry) { + if (err) return callback(err); + var tree = entry.tree; + var commands = parents[parentPath]; + commands.del.forEach(function (name) { + delete tree[name]; + }); + for (var name in commands.add) { + tree[name] = commands.add[name]; + } + repo.saveAs("tree", tree, function (err, hash, tree) { if (err) return callback(err); - var tree = entry.tree; - var commands = parents[parentPath]; - commands.del.forEach(function (name) { - delete tree[name]; + other.push({ + path: parentPath, + hash: hash, + mode: modes.tree }); - for (var name in commands.add) { - tree[name] = commands.add[name]; - } - repo.saveAs("tree", tree, function (err, hash, tree) { - if (err) return callback(err); - other.push({ - path: parentPath, - hash: hash, - mode: modes.tree - }); - if (!--left) { - other.base = entries.base; - if (other.length === 1 && other[0].path === "") { - return callback(null, hash, tree); - } - fastUpdateTree(other, callback); + if (!--left) { + other.base = entries.base; + if (other.length === 1 && other[0].path === "") { + return callback(null, hash, tree); } - }); + fastUpdateTree(other, callback); + } }); }); - } + }); } + } - function readRef(ref, callback) { - if (!callback) return readRef.bind(null, ref); - if (!(/^refs\//).test(ref)) { - return callback(new TypeError("Invalid ref: " + ref)); - } - return apiRequest("GET", "/repos/:root/git/" + ref, onRef); + function readRef(ref, callback) { + if (!(/^refs\//).test(ref)) { + return callback(new TypeError("Invalid ref: " + ref)); + } + return apiRequest("GET", "/repos/:root/git/" + ref, onRef); - function onRef(err, xhr, result) { - if (err) return callback(err); - if (xhr.status === 404) return callback(); - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - return callback(null, result.object.sha); + function onRef(err, xhr, result) { + if (err) return callback(err); + if (xhr.status === 404) return callback(); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); } + return callback(null, result.object.sha); } + } - function updateRef(ref, hash, callback) { - if (!callback) return updateRef(null, ref, hash); - if (!(/^refs\//).test(ref)) { - return callback(new Error("Invalid ref: " + ref)); + function updateRef(ref, hash, callback) { + if (!(/^refs\//).test(ref)) { + return callback(new Error("Invalid ref: " + ref)); + } + return apiRequest("PATCH", "/repos/:root/git/" + ref, { + sha: hash + }, onResult); + + function onResult(err, xhr, result) { + if (err) return callback(err); + if (xhr.status === 422 && result.message === "Reference does not exist") { + return apiRequest("POST", "/repos/:root/git/refs", { + ref: ref, + sha: hash + }, onResult); } - return apiRequest("PATCH", "/repos/:root/git/" + ref, { - sha: hash - }, onResult); - - function onResult(err, xhr, result) { - if (err) return callback(err); - if (xhr.status === 422 && result.message === "Reference does not exist") { - return apiRequest("POST", "/repos/:root/git/refs", { - ref: ref, - sha: hash - }, onResult); - } - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - if (err) return callback(err); - callback(null, hash); + if (xhr.status < 200 || xhr.status >= 300) { + return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); } - + if (err) return callback(err); + callback(null, hash); } - }; + } - // GitHub has a nasty habit of stripping whitespace from messages and loosing - // the timezone. This information is required to make our hashes match up, so - // we guess it by mutating the value till the hash matches. - // If we're unable to match, we will just force the hash when saving to the cache. - function fixDate(type, value, hash) { - if (type !== "commit" && type !== "tag") return; - // Add up to 2 extra newlines and try all 30-minutes timezone offsets. - for (var x = 0; x < 3; x++) { - for (var i = -720; i < 720; i += 30) { - if (type === "commit") { - value.author.date.timezoneOffset = i; - value.committer.date.timezoneOffset = i; - } - else if (type === "tag") { - value.tagger.date.timezoneOffset = i; - } - if (hash === hashAs(type, value)) return true; +}; + +// GitHub has a nasty habit of stripping whitespace from messages and loosing +// the timezone. This information is required to make our hashes match up, so +// we guess it by mutating the value till the hash matches. +// If we're unable to match, we will just force the hash when saving to the cache. +function fixDate(type, value, hash) { + if (type !== "commit" && type !== "tag") return; + // Add up to 2 extra newlines and try all 30-minutes timezone offsets. + for (var x = 0; x < 3; x++) { + for (var i = -720; i < 720; i += 30) { + if (type === "commit") { + value.author.date.timezoneOffset = i; + value.committer.date.timezoneOffset = i; } - value.message += "\n"; - } - // Guessing failed, remove the temporary offset. - if (type === "commit") { - delete value.author.date.timezoneOffset; - delete value.committer.date.timezoneOffset; - } - else if (type === "tag") { - delete value.tagger.date.timezoneOffset; + else if (type === "tag") { + value.tagger.date.timezoneOffset = i; + } + if (hash === hashAs(type, value)) return true; } + value.message += "\n"; } - - function mapTreeEntry(entry) { - if (!entry.mode) throw new TypeError("Invalid entry"); - var mode = modeToString(entry.mode); - var item = { - path: entry.path, - mode: mode, - type: modeToType[mode] - }; - // Magic hash for empty file since github rejects empty contents. - if (entry.content === "") entry.hash = emptyBlob; - - if (entry.hash) item.sha = entry.hash; - else item.content = entry.content; - return item; - } - - function encodeCommit(commit) { - var out = {}; - out.message = commit.message; - out.tree = commit.tree; - if (commit.parents) out.parents = commit.parents; - else if (commit.parent) out.parents = [commit.parent]; - else commit.parents = []; - if (commit.author) out.author = encodePerson(commit.author); - if (commit.committer) out.committer = encodePerson(commit.committer); - return out; - } - - function encodeTag(tag) { - return { - tag: tag.tag, - message: tag.message, - object: tag.object, - tagger: encodePerson(tag.tagger) - }; - } - - function encodePerson(person) { - return { - name: person.name, - email: person.email, - date: (person.date || new Date()).toISOString() - }; - } - - function encodeTree(tree) { - return { - tree: Object.keys(tree).map(function (name) { - var entry = tree[name]; - var mode = modeToString(entry.mode); - return { - path: name, - mode: mode, - type: modeToType[mode], - sha: entry.hash - }; - }) - }; - } - - function encodeBlob(blob) { - if (typeof blob === "string") return { - content: binary.encodeUtf8(blob), - encoding: "utf-8" - }; - if (binary.isBinary(blob)) return { - content: binary.toBase64(blob), - encoding: "base64" - }; - throw new TypeError("Invalid blob type, must be binary of string"); + // Guessing failed, remove the temporary offset. + if (type === "commit") { + delete value.author.date.timezoneOffset; + delete value.committer.date.timezoneOffset; } - - function modeToString(mode) { - var string = mode.toString(8); - // Github likes all modes to be 6 chars long - if (string.length === 5) string = "0" + string; - return string; - } - - function decodeCommit(result) { - return { - tree: result.tree.sha, - parents: result.parents.map(function (object) { - return object.sha; - }), - author: pickPerson(result.author), - committer: pickPerson(result.committer), - message: result.message - }; - } - - function decodeTag(result) { - return { - object: result.object.sha, - type: result.object.type, - tag: result.tag, - tagger: pickPerson(result.tagger), - message: result.message - }; + else if (type === "tag") { + delete value.tagger.date.timezoneOffset; } +} + +function mapTreeEntry(entry) { + if (!entry.mode) throw new TypeError("Invalid entry"); + var mode = modeToString(entry.mode); + var item = { + path: entry.path, + mode: mode, + type: modeToType[mode] + }; + // Magic hash for empty file since github rejects empty contents. + if (entry.content === "") entry.hash = emptyBlob; + + if (entry.hash) item.sha = entry.hash; + else item.content = entry.content; + return item; +} + +function encodeCommit(commit) { + var out = {}; + out.message = commit.message; + out.tree = commit.tree; + if (commit.parents) out.parents = commit.parents; + else if (commit.parent) out.parents = [commit.parent]; + else commit.parents = []; + if (commit.author) out.author = encodePerson(commit.author); + if (commit.committer) out.committer = encodePerson(commit.committer); + return out; +} + +function encodeTag(tag) { + return { + tag: tag.tag, + message: tag.message, + object: tag.object, + tagger: encodePerson(tag.tagger) + }; +} - function decodeTree(result) { - var tree = {}; - result.tree.forEach(function (entry) { - tree[entry.path] = { - mode: parseInt(entry.mode, 8), - hash: entry.sha +function encodePerson(person) { + return { + name: person.name, + email: person.email, + date: (person.date || new Date()).toISOString() + }; +} + +function encodeTree(tree) { + return { + tree: Object.keys(tree).map(function (name) { + var entry = tree[name]; + var mode = modeToString(entry.mode); + return { + path: name, + mode: mode, + type: modeToType[mode], + sha: entry.hash }; - }); - return tree; - } - - function decodeBlob(result) { - if (result.encoding === 'base64') { - return binary.fromBase64(result.content.replace(/\n/g, '')); - } - if (result.encoding === 'utf-8') { - return binary.fromUtf8(result.content); - } - throw new Error("Unknown blob encoding: " + result.encoding); - } + }) + }; +} - function pickPerson(person) { - return { - name: person.name, - email: person.email, - date: parseDate(person.date) +function encodeBlob(blob) { + if (typeof blob === "string") return { + content: binary.encodeUtf8(blob), + encoding: "utf-8" + }; + if (binary.isBinary(blob)) return { + content: binary.toBase64(blob), + encoding: "base64" + }; + throw new TypeError("Invalid blob type, must be binary of string"); +} + +function modeToString(mode) { + var string = mode.toString(8); + // Github likes all modes to be 6 chars long + if (string.length === 5) string = "0" + string; + return string; +} + +function decodeCommit(result) { + return { + tree: result.tree.sha, + parents: result.parents.map(function (object) { + return object.sha; + }), + author: pickPerson(result.author), + committer: pickPerson(result.committer), + message: result.message + }; +} + +function decodeTag(result) { + return { + object: result.object.sha, + type: result.object.type, + tag: result.tag, + tagger: pickPerson(result.tagger), + message: result.message + }; +} + +function decodeTree(result) { + var tree = {}; + result.tree.forEach(function (entry) { + tree[entry.path] = { + mode: parseInt(entry.mode, 8), + hash: entry.sha }; - } + }); + return tree; +} - function parseDate(string) { - // TODO: test this once GitHub adds timezone information - var match = string.match(/(-?)([0-9]{2}):([0-9]{2})$/); - var date = new Date(string); - date.timezoneOffset = 0; - if (match) { - date.timezoneOffset = (match[1] === "-" ? 1 : -1) * ( - parseInt(match[2], 10) * 60 + parseInt(match[3], 10) - ); - } - return date; +function decodeBlob(result) { + if (result.encoding === 'base64') { + return binary.fromBase64(result.content.replace(/\n/g, '')); } - - function singleCall(callback) { - var done = false; - return function () { - if (done) return console.warn("Discarding extra callback"); - done = true; - return callback.apply(this, arguments); - }; + if (result.encoding === 'utf-8') { + return binary.fromUtf8(result.content); } - -}); + throw new Error("Unknown blob encoding: " + result.encoding); +} + +function pickPerson(person) { + return { + name: person.name, + email: person.email, + date: parseDate(person.date) + }; +} + +function parseDate(string) { + // TODO: test this once GitHub adds timezone information + var match = string.match(/(-?)([0-9]{2}):([0-9]{2})$/); + var date = new Date(string); + date.timezoneOffset = 0; + if (match) { + date.timezoneOffset = (match[1] === "-" ? 1 : -1) * ( + parseInt(match[2], 10) * 60 + parseInt(match[3], 10) + ); + } + return date; +} + +function singleCall(callback) { + var done = false; + return function () { + if (done) return console.warn("Discarding extra callback"); + done = true; + return callback.apply(this, arguments); + }; +} diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index dc81f64..df1f05c 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -1,132 +1,125 @@ -/*global indexedDB, define*/ -define("js-git/mixins/indexed-db", function () { - "use strict"; +"use strict"; +/*global indexedDB*/ - var encoders = require('js-git/lib/encoders'); - var db; +var encoders = require('js-git/lib/encoders'); +var db; - mixin.init = init; +mixin.init = init; - mixin.loadAs = loadAs; - mixin.saveAs = saveAs; - return mixin; +mixin.loadAs = loadAs; +mixin.saveAs = saveAs; +module.exports = mixin; - function init(callback) { +function init(callback) { - db = null; - var request = indexedDB.open("tedit", 1); + db = null; + var request = indexedDB.open("tedit", 1); - // We can only create Object stores in a versionchange transaction. - request.onupgradeneeded = function(evt) { - var db = evt.target.result; + // We can only create Object stores in a versionchange transaction. + request.onupgradeneeded = function(evt) { + var db = evt.target.result; - // A versionchange transaction is started automatically. - evt.target.transaction.onerror = onError; + // A versionchange transaction is started automatically. + evt.target.transaction.onerror = onError; - if(db.objectStoreNames.contains("objects")) { - db.deleteObjectStore("objects"); - } - if(db.objectStoreNames.contains("refs")) { - db.deleteObjectStore("refs"); - } - - db.createObjectStore("objects", {keyPath: "hash"}); - db.createObjectStore("refs", {keyPath: "path"}); - }; - - request.onsuccess = function (evt) { - db = evt.target.result; - callback(); - }; - request.onerror = onError; - } - - - function mixin(repo, prefix) { - if (!prefix) throw new Error("Prefix required"); - repo.refPrefix = prefix; - repo.saveAs = saveAs; - repo.loadAs = loadAs; - repo.readRef = readRef; - repo.updateRef = updateRef; - } - - function onError(evt) { - console.error("error", evt.target.error); - } - - function saveAs(type, body, callback, forcedHash) { - if (!callback) return saveAs.bind(this, type, body); - var hash; - try { - body = encoders.normalizeAs(type, body); - hash = forcedHash || encoders.hashAs(type, body); + if(db.objectStoreNames.contains("objects")) { + db.deleteObjectStore("objects"); + } + if(db.objectStoreNames.contains("refs")) { + db.deleteObjectStore("refs"); } - catch (err) { return callback(err); } - var trans = db.transaction(["objects"], "readwrite"); - var store = trans.objectStore("objects"); - var entry = { hash: hash, type: type, body: body }; - var request = store.put(entry); - request.onsuccess = function() { - // console.warn("SAVE", type, hash); - callback(null, hash, body); - }; - request.onerror = function(evt) { - callback(new Error(evt.value)); - }; - } - - function loadAs(type, hash, callback) { - if (!callback) return loadAs.bind(this, type, hash); - // console.warn("LOAD", type, hash); - var trans = db.transaction(["objects"], "readwrite"); - var store = trans.objectStore("objects"); - var request = store.get(hash); - request.onsuccess = function(evt) { - var entry = evt.target.result; - if (!entry) return callback(); - if (type !== "blob") { - entry.body = encoders.normalizeAs(type, entry.body); - } - if (type !== entry.type) { - return callback(new TypeError("Type mismatch")); - } - callback(null, entry.body, hash); - }; - request.onerror = function(evt) { - callback(new Error(evt.value)); - }; - } - - function readRef(ref, callback) { - if (!callback) return readRef.bind(this, ref); - var key = this.refPrefix + "/" + ref; - var trans = db.transaction(["refs"], "readwrite"); - var store = trans.objectStore("refs"); - var request = store.get(key); - request.onsuccess = function(evt) { - var entry = evt.target.result; - if (!entry) return callback(); - callback(null, entry.hash); - }; - request.onerror = function(evt) { - callback(new Error(evt.value)); - }; - } - function updateRef(ref, hash, callback) { - if (!callback) return updateRef.bind(this, ref, hash); - var key = this.refPrefix + "/" + ref; - var trans = db.transaction(["refs"], "readwrite"); - var store = trans.objectStore("refs"); - var entry = { path: key, hash: hash }; - var request = store.put(entry); - request.onsuccess = function() { - callback(); - }; - request.onerror = function(evt) { - callback(new Error(evt.value)); - }; + db.createObjectStore("objects", {keyPath: "hash"}); + db.createObjectStore("refs", {keyPath: "path"}); + }; + + request.onsuccess = function (evt) { + db = evt.target.result; + callback(); + }; + request.onerror = onError; +} + + +function mixin(repo, prefix) { + if (!prefix) throw new Error("Prefix required"); + repo.refPrefix = prefix; + repo.saveAs = saveAs; + repo.loadAs = loadAs; + repo.readRef = readRef; + repo.updateRef = updateRef; +} + +function onError(evt) { + console.error("error", evt.target.error); +} + +function saveAs(type, body, callback, forcedHash) { + var hash; + try { + body = encoders.normalizeAs(type, body); + hash = forcedHash || encoders.hashAs(type, body); } - -}); + catch (err) { return callback(err); } + var trans = db.transaction(["objects"], "readwrite"); + var store = trans.objectStore("objects"); + var entry = { hash: hash, type: type, body: body }; + var request = store.put(entry); + request.onsuccess = function() { + // console.warn("SAVE", type, hash); + callback(null, hash, body); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; +} + +function loadAs(type, hash, callback) { + // console.warn("LOAD", type, hash); + var trans = db.transaction(["objects"], "readwrite"); + var store = trans.objectStore("objects"); + var request = store.get(hash); + request.onsuccess = function(evt) { + var entry = evt.target.result; + if (!entry) return callback(); + if (type !== "blob") { + entry.body = encoders.normalizeAs(type, entry.body); + } + if (type !== entry.type) { + return callback(new TypeError("Type mismatch")); + } + callback(null, entry.body, hash); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; +} + +function readRef(ref, callback) { + var key = this.refPrefix + "/" + ref; + var trans = db.transaction(["refs"], "readwrite"); + var store = trans.objectStore("refs"); + var request = store.get(key); + request.onsuccess = function(evt) { + var entry = evt.target.result; + if (!entry) return callback(); + callback(null, entry.hash); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; +} + +function updateRef(ref, hash, callback) { + var key = this.refPrefix + "/" + ref; + var trans = db.transaction(["refs"], "readwrite"); + var store = trans.objectStore("refs"); + var entry = { path: key, hash: hash }; + var request = store.put(entry); + request.onsuccess = function() { + callback(); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; +} diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 09e6783..2161932 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -1,47 +1,35 @@ -/*global define*/ -define("js-git/mixins/mem-cache", function () { - "use strict"; - var normalizeAs = require('js-git/lib/encoders').normalizeAs; - var hashAs = require('js-git/lib/encoders').hashAs; - - var cache = {}; - return function (repo) { - var saved = {}; - var loadAs = repo.loadAs; - repo.loadAs = loadAsCached; - function loadAsCached(type, hash, callback) { - if (!callback) return loadAsCached.bind(this, type, hash); - if (hash in cache) return callback(null, dupe(type, cache[hash])); - loadAs.call(repo, type, hash, function (err, value) { - if (err) return callback(err); - if (type !== "blob" || value.length < 100) { - cache[hash] = value; - } - return callback.apply(this, arguments); - }); - } - - var saveAs = repo.saveAs; - repo.saveAs = saveAsCached; - function saveAsCached(type, value, callback) { - if (!callback) return saveAsCached.bind(this, type, value); - // var hash = hashAs(type, value); - // if (saved[hash]) return callback(null, hash, dupe(value)); - saveAs.call(repo, type, value, function (err, hash, value) { - if (err) return callback(err); - if (type !== "blob" || value.length < 100) { - cache[hash] = value; - } - saved[hash] = true; - return callback.apply(this, arguments); - }); - } - }; +"use strict"; +var normalizeAs = require('../lib/encoders.js').normalizeAs; +var cache = {}; +module.exports = function (repo) { + var loadAs = repo.loadAs; + repo.loadAs = loadAsCached; + function loadAsCached(type, hash, callback) { + if (hash in cache) return callback(null, dupe(type, cache[hash])); + loadAs.call(repo, type, hash, function (err, value) { + if (err) return callback(err); + if (type !== "blob" || value.length < 100) { + cache[hash] = value; + } + return callback.apply(this, arguments); + }); + } - function dupe(type, value) { - if (type === "blob") return value; - return normalizeAs(type, value); + var saveAs = repo.saveAs; + repo.saveAs = saveAsCached; + function saveAsCached(type, value, callback) { + saveAs.call(repo, type, value, function (err, hash, value) { + if (err) return callback(err); + if (type !== "blob" || value.length < 100) { + cache[hash] = value; + } + return callback.apply(this, arguments); + }); } +}; -}); +function dupe(type, value) { + if (type === "blob") return value; + return normalizeAs(type, value); +} diff --git a/mixins/mem-db.js b/mixins/mem-db.js index b13b4ac..49b98d0 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -1,49 +1,39 @@ -/*global define*/ -define("js-git/mixins/mem-db", function () { - "use strict"; - - var defer = require('js-git/lib/defer'); - var encoders = require('js-git/lib/encoders'); - - mixin.saveAs = saveAs; - mixin.loadAs = loadAs; - return mixin; - - function mixin(repo) { - var objects = repo.objects = {}; - var types = {}; - - repo.saveAs = saveAs; - repo.loadAs = loadAs; - - function saveAs(type, body, callback, hashOverride) { - if (!callback) return saveAs.bind(this, type, body); - defer(function () { - var hash; - try { - body = encoders.normalizeAs(type, body); - hash = hashOverride || encoders.hashAs(type, body); - } - catch (err) { return callback(err); } - objects[hash] = body; - types[hash] = type; - callback(null, hash, body); - }); - } - - function loadAs(type, hash, callback) { - if (!callback) return loadAs.bind(this, type, hash); - defer(function () { - var realType = (type === "text" || type === "raw") ? "blob" : type; - if (!types[hash]) return callback(); - if (realType !== types[hash]) return callback(new TypeError("Type mismatch")); - var result = objects[hash]; - if (type !== "blob") result = encoders.normalizeAs(type, result); - callback(null, result, hash); - }); - } - +"use strict"; + +var defer = require('../lib/defer.js'); +var encoders = require('../lib/encoders.js'); + +module.exports = mixin; + +function mixin(repo) { + var objects = repo.objects = {}; + var types = {}; + + repo.saveAs = saveAs; + repo.loadAs = loadAs; + + function saveAs(type, body, callback, hashOverride) { + defer(function () { + var hash; + try { + body = encoders.normalizeAs(type, body); + hash = hashOverride || encoders.hashAs(type, body); + } + catch (err) { return callback(err); } + objects[hash] = body; + types[hash] = type; + callback(null, hash, body); + }); } - -}); + function loadAs(type, hash, callback) { + defer(function () { + var realType = (type === "text" || type === "raw") ? "blob" : type; + if (!types[hash]) return callback(); + if (realType !== types[hash]) return callback(new TypeError("Type mismatch")); + var result = objects[hash]; + if (type !== "blob") result = encoders.normalizeAs(type, result); + callback(null, result, hash); + }); + } +} diff --git a/mixins/read-combiner.js b/mixins/read-combiner.js index a3d1936..39f128d 100644 --- a/mixins/read-combiner.js +++ b/mixins/read-combiner.js @@ -1,32 +1,28 @@ -/*global define*/ -define('js-git/mixins/read-combiner', function () { - "use strict"; +"use strict"; - // This replaces loadAs with a version that batches concurrent requests for - // the same hash. - return function (repo) { - var pendingReqs = {}; +// This replaces loadAs with a version that batches concurrent requests for +// the same hash. +module.exports = function (repo) { + var pendingReqs = {}; - var loadAs = repo.loadAs; - repo.loadAs = newLoadAs; + var loadAs = repo.loadAs; + repo.loadAs = newLoadAs; - function newLoadAs(type, hash, callback) { - if (!callback) return newLoadAs.bind(null, type, hash); - var list = pendingReqs[hash]; - if (list) { - if (list.type !== type) callback(new Error("Type mismatch")); - else list.push(callback); - return; - } - list = pendingReqs[hash] = [callback]; - list.type = type; - loadAs.call(repo, type, hash, function () { - delete pendingReqs[hash]; - for (var i = 0, l = list.length; i < l; i++) { - list[i].apply(this, arguments); - } - }); + function newLoadAs(type, hash, callback) { + if (!callback) return newLoadAs.bind(null, type, hash); + var list = pendingReqs[hash]; + if (list) { + if (list.type !== type) callback(new Error("Type mismatch")); + else list.push(callback); + return; } - }; - -}); \ No newline at end of file + list = pendingReqs[hash] = [callback]; + list.type = type; + loadAs.call(repo, type, hash, function () { + delete pendingReqs[hash]; + for (var i = 0, l = list.length; i < l; i++) { + list[i].apply(this, arguments); + } + }); + } +}; From 3f14ff00628b535a2568bebd4775132fd6a7d1af Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 12 Feb 2014 22:04:39 +0000 Subject: [PATCH 088/256] Fix broken linkage --- mixins/indexed-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index df1f05c..a80dfec 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -1,7 +1,7 @@ "use strict"; /*global indexedDB*/ -var encoders = require('js-git/lib/encoders'); +var encoders = require('../lib/encoders.js'); var db; mixin.init = init; From 0f8d48854e628f791936e973f4d9f80f1f1a7669 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 03:15:55 +0000 Subject: [PATCH 089/256] Make js-github less noisy on the logs --- mixins/github-db.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixins/github-db.js b/mixins/github-db.js index 0b9ad15..e328574 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -58,8 +58,8 @@ module.exports = function (repo, root, accessToken) { try { body = decoders[type].call(repo, result); } catch (err) { return callback(err); } if (hashAs(type, body) !== hash) { - if (fixDate(type, body, hash)) console.warn(type + " repaired", hash); - else console.error("Unable to repair " + type, hash); + if (fixDate(type, body, hash)) console.log(type + " repaired", hash); + else console.warn("Unable to repair " + type, hash); } return callback(null, body, hash); } From ddb80c2679a8285286be505a95dc46a1e01adb5b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 04:15:36 +0000 Subject: [PATCH 090/256] Start to package node version. --- .gitignore | 4 - npm/lib/binary.js | 136 +++++++++++++++++++++++++++++++ npm/lib/defer.js | 3 + npm/lib/sha1.js | 3 + npm/lib/xhr.js | 3 + npm/lib/{name}.js | 1 + npm/mixins | 1 + package.json => npm/package.json | 0 8 files changed, 147 insertions(+), 4 deletions(-) delete mode 100644 .gitignore create mode 100644 npm/lib/binary.js create mode 100644 npm/lib/defer.js create mode 100644 npm/lib/sha1.js create mode 100644 npm/lib/xhr.js create mode 120000 npm/lib/{name}.js create mode 120000 npm/mixins rename package.json => npm/package.json (100%) diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6161444..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.git -node_modules -.zedstate -tags diff --git a/npm/lib/binary.js b/npm/lib/binary.js new file mode 100644 index 0000000..8f70103 --- /dev/null +++ b/npm/lib/binary.js @@ -0,0 +1,136 @@ +"use strict"; + +// This file must be served with UTF-8 encoding for the utf8 codec to work. +module.exports = { + // Utility functions + isBinary: isBinary, + create: create, + join: join, + + // Binary input and output + copy: copy, + slice: slice, + + // String input and output + toRaw: toRaw, + fromRaw: fromRaw, + toUnicode: toUnicode, + fromUnicode: fromUnicode, + toHex: toHex, + fromHex: fromHex, + toBase64: toBase64, + fromBase64: fromBase64, + + // Array input and output + toArray: toArray, + fromArray: fromArray, + + // Raw <-> Hex-encoded codec + decodeHex: decodeHex, + encodeHex: encodeHex, + + decodeBase64: decodeBase64, + encodeBase64: encodeBase64, + + // Unicode <-> Utf8-encoded-raw codec + encodeUtf8: encodeUtf8, + decodeUtf8: decodeUtf8, + + // Hex <-> Nibble codec + nibbleToCode: nibbleToCode, + codeToNibble: codeToNibble +}; + +function isBinary(value) { + return Buffer.isBuffer(value); +} + +function create(length) { + return new Buffer(length); +} + +function join(chunks) { + return Buffer.concat(chunks); +} + +function slice(binary, start, end) { + throw "TODO: Implement"; +} + +function copy(source, binary, offset) { + throw "TODO: Implement"; +} + +// Like slice, but encode as a hex string +function toHex(binary, start, end) { + throw "TODO: Implement"; +} + +// Like copy, but decode from a hex string +function fromHex(hex, binary, offset) { + throw "TODO: Implement"; +} + +function toBase64(binary, start, end) { + throw "TODO: Implement"; +} + +function fromBase64(base64, binary, offset) { + throw "TODO: Implement"; +} + +function nibbleToCode(nibble) { + throw "TODO: Implement"; +} + +function codeToNibble(code) { + throw "TODO: Implement"; +} + +function toUnicode(binary, start, end) { + throw "TODO: Implement"; +} + +function fromUnicode(unicode, binary, offset) { + throw "TODO: Implement"; +} + +function decodeHex(hex) { + throw "TODO: Implement"; +} + +function encodeHex(raw) { + throw "TODO: Implement"; +} + +function decodeBase64(base64) { + throw "TODO: Implement"; +} + +function encodeBase64(raw) { + throw "TODO: Implement"; +} + +function decodeUtf8(utf8) { + throw "TODO: Implement"; +} + +function encodeUtf8(unicode) { + throw "TODO: Implement"; +} + +function toRaw(binary, start, end) { + throw "TODO: Implement"; +} + +function fromRaw(raw, binary, offset) { + throw "TODO: Implement"; +} + +function toArray(binary, start, end) { + throw "TODO: Implement"; +} + +function fromArray(array, binary, offset) { + throw "TODO: Implement"; +} diff --git a/npm/lib/defer.js b/npm/lib/defer.js new file mode 100644 index 0000000..aec8975 --- /dev/null +++ b/npm/lib/defer.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = process.nextTick; diff --git a/npm/lib/sha1.js b/npm/lib/sha1.js new file mode 100644 index 0000000..41a9ac6 --- /dev/null +++ b/npm/lib/sha1.js @@ -0,0 +1,3 @@ +"use strict"; + +throw "TODO: Implement SHA1 shim for node"; diff --git a/npm/lib/xhr.js b/npm/lib/xhr.js new file mode 100644 index 0000000..595ec5a --- /dev/null +++ b/npm/lib/xhr.js @@ -0,0 +1,3 @@ +"use strict"; + +throw "TODO: Implement XHR shim for node"; diff --git a/npm/lib/{name}.js b/npm/lib/{name}.js new file mode 120000 index 0000000..b1eb856 --- /dev/null +++ b/npm/lib/{name}.js @@ -0,0 +1 @@ +../../lib/{name}.js \ No newline at end of file diff --git a/npm/mixins b/npm/mixins new file mode 120000 index 0000000..1328951 --- /dev/null +++ b/npm/mixins @@ -0,0 +1 @@ +mixins \ No newline at end of file diff --git a/package.json b/npm/package.json similarity index 100% rename from package.json rename to npm/package.json From 2df2ba72f94915d6f273faa29c47f78602beafed Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 13 Feb 2014 07:35:30 -0600 Subject: [PATCH 091/256] Fix mode for executable files Signed-off-by: Johannes Schindelin --- mixins/github-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/github-db.js b/mixins/github-db.js index e328574..6a05316 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -9,7 +9,7 @@ var binary = require('../lib/binary.js'); var modeToType = { "040000": "tree", "100644": "blob", // normal file - "100655": "blob", // executable file + "100755": "blob", // executable file "120000": "blob", // symlink "160000": "commit" // gitlink }; From a040674486a5d41f282aee5b272c34224d239f90 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 18:33:03 +0000 Subject: [PATCH 092/256] Add plain package.json --- lib/binary.js | 233 -------------------------------------------------- lib/sha1.js | 148 -------------------------------- package.json | 3 + 3 files changed, 3 insertions(+), 381 deletions(-) delete mode 100644 lib/binary.js delete mode 100644 lib/sha1.js create mode 100644 package.json diff --git a/lib/binary.js b/lib/binary.js deleted file mode 100644 index b27052e..0000000 --- a/lib/binary.js +++ /dev/null @@ -1,233 +0,0 @@ -"use strict"; -/*global escape, unescape*/ - -// This file must be served with UTF-8 encoding for the utf8 codec to work. -module.exports = { - // Utility functions - isBinary: isBinary, - create: create, - join: join, - - // Binary input and output - copy: copy, - slice: slice, - - // String input and output - toRaw: toRaw, - fromRaw: fromRaw, - toUnicode: toUnicode, - fromUnicode: fromUnicode, - toHex: toHex, - fromHex: fromHex, - toBase64: toBase64, - fromBase64: fromBase64, - - // Array input and output - toArray: toArray, - fromArray: fromArray, - - // Raw <-> Hex-encoded codec - decodeHex: decodeHex, - encodeHex: encodeHex, - - decodeBase64: decodeBase64, - encodeBase64: encodeBase64, - - // Unicode <-> Utf8-encoded-raw codec - encodeUtf8: encodeUtf8, - decodeUtf8: decodeUtf8, - - // Hex <-> Nibble codec - nibbleToCode: nibbleToCode, - codeToNibble: codeToNibble -}; - -function isBinary(value) { - return value && - typeof value === "object" && - value.constructor.name === "Uint8Array"; -} - -function create(length) { - return new Uint8Array(length); -} - -function join(chunks) { - var length = chunks.length; - var total = 0; - for (var i = 0; i < length; i++) { - total += chunks[i].length; - } - var binary = create(total); - var offset = 0; - for (i = 0; i < length; i++) { - var chunk = chunks[i]; - copy(chunk, binary, offset); - offset += chunk.length; - } - return binary; -} - -function slice(binary, start, end) { - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - return binary.subarray(start, end); -} - -function copy(source, binary, offset) { - var length = source.length; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - for (var i = 0; i < length; i++) { - binary[i + offset] = source[i]; - } - return binary; -} - -// Like slice, but encode as a hex string -function toHex(binary, start, end) { - var hex = ""; - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - for (var i = start; i < end; i++) { - var byte = binary[i]; - hex += String.fromCharCode(nibbleToCode(byte >> 4)) + - String.fromCharCode(nibbleToCode(byte & 0xf)); - } - return hex; -} - -// Like copy, but decode from a hex string -function fromHex(hex, binary, offset) { - var length = hex.length / 2; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - var j = 0; - for (var i = 0; i < length; i++) { - binary[offset + i] = (codeToNibble(hex.charCodeAt(j++)) << 4) - | codeToNibble(hex.charCodeAt(j++)); - } - return binary; -} - -function toBase64(binary, start, end) { - return btoa(toRaw(binary, start, end)); -} - -function fromBase64(base64, binary, offset) { - return fromRaw(atob(base64), binary, offset); -} - -function nibbleToCode(nibble) { - nibble |= 0; - return (nibble + (nibble < 10 ? 0x30 : 0x57))|0; -} - -function codeToNibble(code) { - code |= 0; - return (code - ((code & 0x40) ? 0x57 : 0x30))|0; -} - -function toUnicode(binary, start, end) { - return decodeUtf8(toRaw(binary, start, end)); -} - -function fromUnicode(unicode, binary, offset) { - return fromRaw(encodeUtf8(unicode), binary, offset); -} - -function decodeHex(hex) { - var j = 0, l = hex.length; - var raw = ""; - while (j < l) { - raw += String.fromCharCode( - (codeToNibble(hex.charCodeAt(j++)) << 4) - | codeToNibble(hex.charCodeAt(j++)) - ); - } - return raw; -} - -function encodeHex(raw) { - var hex = ""; - var length = raw.length; - for (var i = 0; i < length; i++) { - var byte = raw.charCodeAt(i); - hex += String.fromCharCode(nibbleToCode(byte >> 4)) + - String.fromCharCode(nibbleToCode(byte & 0xf)); - } - return hex; -} - -function decodeBase64(base64) { - return atob(base64); -} - -function encodeBase64(raw) { - return btoa(raw); -} - -function decodeUtf8(utf8) { - return decodeURIComponent(escape(utf8)); -} - -function encodeUtf8(unicode) { - return unescape(encodeURIComponent(unicode)); -} - -function toRaw(binary, start, end) { - var raw = ""; - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - for (var i = start; i < end; i++) { - raw += String.fromCharCode(binary[i]); - } - return raw; -} - -function fromRaw(raw, binary, offset) { - var length = raw.length; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - for (var i = 0; i < length; i++) { - binary[offset + i] = raw.charCodeAt(i); - } - return binary; -} - -function toArray(binary, start, end) { - if (end === undefined) { - end = binary.length; - if (start === undefined) start = 0; - } - var length = end - start; - var array = new Array(length); - for (var i = 0; i < length; i++) { - array[i] = binary[i + start]; - } - return array; -} - -function fromArray(array, binary, offset) { - var length = array.length; - if (offset === undefined) { - offset = 0; - if (binary === undefined) binary = create(length); - } - for (var i = 0; i < length; i++) { - binary[offset + i] = array[i]; - } - return binary; -} diff --git a/lib/sha1.js b/lib/sha1.js deleted file mode 100644 index dff5f07..0000000 --- a/lib/sha1.js +++ /dev/null @@ -1,148 +0,0 @@ -"use strict"; - -// Input chunks must be either arrays of bytes or "raw" encoded strings -module.exports = function sha1(buffer) { - if (buffer === undefined) return create(); - var shasum = create(); - shasum.update(buffer); - return shasum.digest(); -}; - -// A streaming interface for when nothing is passed in. -function create() { - var h0 = 0x67452301; - var h1 = 0xEFCDAB89; - var h2 = 0x98BADCFE; - var h3 = 0x10325476; - var h4 = 0xC3D2E1F0; - // The first 64 bytes (16 words) is the data chunk - var block = new Uint32Array(80), offset = 0, shift = 24; - var totalLength = 0; - - return { update: update, digest: digest }; - - // The user gave us more data. Store it! - function update(chunk) { - if (typeof chunk === "string") return updateString(chunk); - var length = chunk.length; - totalLength += length * 8; - for (var i = 0; i < length; i++) { - write(chunk[i]); - } - } - - function updateString(string) { - var length = string.length; - totalLength += length * 8; - for (var i = 0; i < length; i++) { - write(string.charCodeAt(i)); - } - } - - - function write(byte) { - block[offset] |= (byte & 0xff) << shift; - if (shift) { - shift -= 8; - } - else { - offset++; - shift = 24; - } - if (offset === 16) processBlock(); - } - - // No more data will come, pad the block, process and return the result. - function digest() { - // Pad - write(0x80); - if (offset > 14 || (offset === 14 && shift < 24)) { - processBlock(); - } - offset = 14; - shift = 24; - - // 64-bit length big-endian - write(0x00); // numbers this big aren't accurate in javascript anyway - write(0x00); // ..So just hard-code to zero. - write(totalLength > 0xffffffffff ? totalLength / 0x10000000000 : 0x00); - write(totalLength > 0xffffffff ? totalLength / 0x100000000 : 0x00); - for (var s = 24; s >= 0; s -= 8) { - write(totalLength >> s); - } - - // At this point one last processBlock() should trigger and we can pull out the result. - return toHex(h0) + - toHex(h1) + - toHex(h2) + - toHex(h3) + - toHex(h4); - } - - // We have a full block to process. Let's do it! - function processBlock() { - // Extend the sixteen 32-bit words into eighty 32-bit words: - for (var i = 16; i < 80; i++) { - var w = block[i - 3] ^ block[i - 8] ^ block[i - 14] ^ block[i - 16]; - block[i] = (w << 1) | (w >>> 31); - } - - // log(block); - - // Initialize hash value for this chunk: - var a = h0; - var b = h1; - var c = h2; - var d = h3; - var e = h4; - var f, k; - - // Main loop: - for (i = 0; i < 80; i++) { - if (i < 20) { - f = d ^ (b & (c ^ d)); - k = 0x5A827999; - } - else if (i < 40) { - f = b ^ c ^ d; - k = 0x6ED9EBA1; - } - else if (i < 60) { - f = (b & c) | (d & (b | c)); - k = 0x8F1BBCDC; - } - else { - f = b ^ c ^ d; - k = 0xCA62C1D6; - } - var temp = (a << 5 | a >>> 27) + f + e + k + (block[i]|0); - e = d; - d = c; - c = (b << 30 | b >>> 2); - b = a; - a = temp; - } - - // Add this chunk's hash to result so far: - h0 = (h0 + a) | 0; - h1 = (h1 + b) | 0; - h2 = (h2 + c) | 0; - h3 = (h3 + d) | 0; - h4 = (h4 + e) | 0; - - // The block is now reusable. - offset = 0; - for (i = 0; i < 16; i++) { - block[i] = 0; - } - } - - function toHex(word) { - var hex = ""; - for (var i = 28; i >= 0; i -= 4) { - hex += ((word >> i) & 0xf).toString(16); - } - return hex; - } - -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4fd9487 --- /dev/null +++ b/package.json @@ -0,0 +1,3 @@ +{ + "name": "js-git" +} From 3679561e661a9a943b54e6e89f687b3c2e2365ca Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 18:44:22 +0000 Subject: [PATCH 093/256] Declare external dependencies --- lib/encoders.js | 4 ++-- mixins/formats.js | 2 +- mixins/github-db.js | 2 +- package.json | 7 ++++++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/encoders.js b/lib/encoders.js index c11409b..95a11aa 100644 --- a/lib/encoders.js +++ b/lib/encoders.js @@ -1,7 +1,7 @@ "use strict"; -var sha1 = require('./sha1.js'); -var binary = require('./binary.js'); +var sha1 = require('sha1'); +var binary = require('binary'); var modes = require('./modes.js'); // Run sanity tests at startup. diff --git a/mixins/formats.js b/mixins/formats.js index 433947f..f5d2446 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -1,6 +1,6 @@ "use strict"; -var binary = require('../lib/binary.js'); +var binary = require('binary'); module.exports = function (repo) { var loadAs = repo.loadAs; diff --git a/mixins/github-db.js b/mixins/github-db.js index 6a05316..2524062 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -4,7 +4,7 @@ var normalizeAs = require('../lib/encoders.js').normalizeAs; var modes = require('../lib/modes.js'); var hashAs = require('../lib/encoders.js').hashAs; var xhr = require('../lib/xhr.js'); -var binary = require('../lib/binary.js'); +var binary = require('binary'); var modeToType = { "040000": "tree", diff --git a/package.json b/package.json index 4fd9487..0604513 100644 --- a/package.json +++ b/package.json @@ -1,3 +1,8 @@ { - "name": "js-git" + "name": "js-git", + "version": "0.7.0", + "dependencies": { + "binary": "git://github.com/creationix/binary-browser.git", + "sha1": "git://github.com/creationix/git-sha1.git" + } } From 27c2c2f8d258f9e33e5e9dffe3a55b2acebe974d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 18:46:47 +0000 Subject: [PATCH 094/256] Update link to git-sha1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0604513..c9be4be 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,6 @@ "version": "0.7.0", "dependencies": { "binary": "git://github.com/creationix/binary-browser.git", - "sha1": "git://github.com/creationix/git-sha1.git" + "git-sha1": "git://github.com/creationix/git-sha1.git" } } From 53d9f786568eb306fee6b063f083fffbf73c5dcc Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 18:59:04 +0000 Subject: [PATCH 095/256] Update npm packaging --- npm/lib/binary.js | 136 ---------------------------------------------- npm/lib/defer.js | 3 - npm/lib/sha1.js | 3 - npm/lib/xhr.js | 3 - npm/lib/{name}.js | 1 - npm/mixins | 1 - npm/package.json | 22 -------- package.json | 13 ++++- 8 files changed, 12 insertions(+), 170 deletions(-) delete mode 100644 npm/lib/binary.js delete mode 100644 npm/lib/defer.js delete mode 100644 npm/lib/sha1.js delete mode 100644 npm/lib/xhr.js delete mode 120000 npm/lib/{name}.js delete mode 120000 npm/mixins delete mode 100644 npm/package.json diff --git a/npm/lib/binary.js b/npm/lib/binary.js deleted file mode 100644 index 8f70103..0000000 --- a/npm/lib/binary.js +++ /dev/null @@ -1,136 +0,0 @@ -"use strict"; - -// This file must be served with UTF-8 encoding for the utf8 codec to work. -module.exports = { - // Utility functions - isBinary: isBinary, - create: create, - join: join, - - // Binary input and output - copy: copy, - slice: slice, - - // String input and output - toRaw: toRaw, - fromRaw: fromRaw, - toUnicode: toUnicode, - fromUnicode: fromUnicode, - toHex: toHex, - fromHex: fromHex, - toBase64: toBase64, - fromBase64: fromBase64, - - // Array input and output - toArray: toArray, - fromArray: fromArray, - - // Raw <-> Hex-encoded codec - decodeHex: decodeHex, - encodeHex: encodeHex, - - decodeBase64: decodeBase64, - encodeBase64: encodeBase64, - - // Unicode <-> Utf8-encoded-raw codec - encodeUtf8: encodeUtf8, - decodeUtf8: decodeUtf8, - - // Hex <-> Nibble codec - nibbleToCode: nibbleToCode, - codeToNibble: codeToNibble -}; - -function isBinary(value) { - return Buffer.isBuffer(value); -} - -function create(length) { - return new Buffer(length); -} - -function join(chunks) { - return Buffer.concat(chunks); -} - -function slice(binary, start, end) { - throw "TODO: Implement"; -} - -function copy(source, binary, offset) { - throw "TODO: Implement"; -} - -// Like slice, but encode as a hex string -function toHex(binary, start, end) { - throw "TODO: Implement"; -} - -// Like copy, but decode from a hex string -function fromHex(hex, binary, offset) { - throw "TODO: Implement"; -} - -function toBase64(binary, start, end) { - throw "TODO: Implement"; -} - -function fromBase64(base64, binary, offset) { - throw "TODO: Implement"; -} - -function nibbleToCode(nibble) { - throw "TODO: Implement"; -} - -function codeToNibble(code) { - throw "TODO: Implement"; -} - -function toUnicode(binary, start, end) { - throw "TODO: Implement"; -} - -function fromUnicode(unicode, binary, offset) { - throw "TODO: Implement"; -} - -function decodeHex(hex) { - throw "TODO: Implement"; -} - -function encodeHex(raw) { - throw "TODO: Implement"; -} - -function decodeBase64(base64) { - throw "TODO: Implement"; -} - -function encodeBase64(raw) { - throw "TODO: Implement"; -} - -function decodeUtf8(utf8) { - throw "TODO: Implement"; -} - -function encodeUtf8(unicode) { - throw "TODO: Implement"; -} - -function toRaw(binary, start, end) { - throw "TODO: Implement"; -} - -function fromRaw(raw, binary, offset) { - throw "TODO: Implement"; -} - -function toArray(binary, start, end) { - throw "TODO: Implement"; -} - -function fromArray(array, binary, offset) { - throw "TODO: Implement"; -} diff --git a/npm/lib/defer.js b/npm/lib/defer.js deleted file mode 100644 index aec8975..0000000 --- a/npm/lib/defer.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -module.exports = process.nextTick; diff --git a/npm/lib/sha1.js b/npm/lib/sha1.js deleted file mode 100644 index 41a9ac6..0000000 --- a/npm/lib/sha1.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -throw "TODO: Implement SHA1 shim for node"; diff --git a/npm/lib/xhr.js b/npm/lib/xhr.js deleted file mode 100644 index 595ec5a..0000000 --- a/npm/lib/xhr.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -throw "TODO: Implement XHR shim for node"; diff --git a/npm/lib/{name}.js b/npm/lib/{name}.js deleted file mode 120000 index b1eb856..0000000 --- a/npm/lib/{name}.js +++ /dev/null @@ -1 +0,0 @@ -../../lib/{name}.js \ No newline at end of file diff --git a/npm/mixins b/npm/mixins deleted file mode 120000 index 1328951..0000000 --- a/npm/mixins +++ /dev/null @@ -1 +0,0 @@ -mixins \ No newline at end of file diff --git a/npm/package.json b/npm/package.json deleted file mode 100644 index 5c9ed34..0000000 --- a/npm/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "js-git", - "version": "0.7.0", - "description": "Git Implemented in JavaScript", - "repository": { - "type": "git", - "url": "git://github.com/creationix/js-git.git" - }, - "devDependencies": { - }, - "keywords": [ - "git", - "js-git" - ], - "author": "Tim Caswell ", - "license": "MIT", - "bugs": { - "url": "https://github.com/creationix/js-git/issues" - }, - "dependencies": { - } -} diff --git a/package.json b/package.json index c9be4be..ab418d4 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,19 @@ { "name": "js-git", "version": "0.7.0", + "description": "Git Implemented in JavaScript", + "keywords": ["git", "js-git"], + "repository": { + "type": "git", + "url": "git://github.com/creationix/js-git.git" + }, + "author": "Tim Caswell ", + "license": "MIT", + "bugs": { + "url": "https://github.com/creationix/js-git/issues" + }, "dependencies": { - "binary": "git://github.com/creationix/binary-browser.git", + "bodec": "git://github.com/creationix/bodec.git", "git-sha1": "git://github.com/creationix/git-sha1.git" } } From 51f3bd62c71fee75fbd536a47edd20c2b7e6e28d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 19:47:34 +0000 Subject: [PATCH 096/256] Fix a couple require names --- lib/encoders.js | 4 ++-- mixins/formats.js | 2 +- mixins/github-db.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/encoders.js b/lib/encoders.js index 95a11aa..ef700c4 100644 --- a/lib/encoders.js +++ b/lib/encoders.js @@ -1,7 +1,7 @@ "use strict"; -var sha1 = require('sha1'); -var binary = require('binary'); +var sha1 = require('git-sha1'); +var binary = require('bodec'); var modes = require('./modes.js'); // Run sanity tests at startup. diff --git a/mixins/formats.js b/mixins/formats.js index f5d2446..30c7efd 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -1,6 +1,6 @@ "use strict"; -var binary = require('binary'); +var binary = require('bodec'); module.exports = function (repo) { var loadAs = repo.loadAs; diff --git a/mixins/github-db.js b/mixins/github-db.js index 2524062..576da4c 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -4,7 +4,7 @@ var normalizeAs = require('../lib/encoders.js').normalizeAs; var modes = require('../lib/modes.js'); var hashAs = require('../lib/encoders.js').hashAs; var xhr = require('../lib/xhr.js'); -var binary = require('binary'); +var binary = require('bodec'); var modeToType = { "040000": "tree", From e0566a174ed2ef1075ad2ada55eb6922c6a675de Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 20:03:38 +0000 Subject: [PATCH 097/256] Tweak deps --- mixins/github-db.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mixins/github-db.js b/mixins/github-db.js index 576da4c..73b497b 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -1,9 +1,9 @@ "use strict"; -var normalizeAs = require('../lib/encoders.js').normalizeAs; -var modes = require('../lib/modes.js'); -var hashAs = require('../lib/encoders.js').hashAs; -var xhr = require('../lib/xhr.js'); +var normalizeAs = require('../lib/encoders').normalizeAs; +var hashAs = require('../lib/encoders').hashAs; +var modes = require('../lib/modes'); +var xhr = require('../lib/xhr'); var binary = require('bodec'); var modeToType = { From fcf5107fde6c597606330eb719b4e31465910f62 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 13 Feb 2014 22:24:33 +0000 Subject: [PATCH 098/256] Pass hash through in mem-cache callback --- mixins/mem-cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 2161932..e7fcb0e 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -6,7 +6,7 @@ module.exports = function (repo) { var loadAs = repo.loadAs; repo.loadAs = loadAsCached; function loadAsCached(type, hash, callback) { - if (hash in cache) return callback(null, dupe(type, cache[hash])); + if (hash in cache) return callback(null, dupe(type, cache[hash]), hash); loadAs.call(repo, type, hash, function (err, value) { if (err) return callback(err); if (type !== "blob" || value.length < 100) { From fa8e98be3aeb99466e62dcdf315a74e44d4a1e83 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 14 Feb 2014 04:34:57 +0000 Subject: [PATCH 099/256] Protect cached buffers. --- mixins/mem-cache.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index e7fcb0e..97a59e9 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -10,6 +10,7 @@ module.exports = function (repo) { loadAs.call(repo, type, hash, function (err, value) { if (err) return callback(err); if (type !== "blob" || value.length < 100) { + if (type === "blob") value = new Uint8Array(value); cache[hash] = value; } return callback.apply(this, arguments); @@ -22,6 +23,7 @@ module.exports = function (repo) { saveAs.call(repo, type, value, function (err, hash, value) { if (err) return callback(err); if (type !== "blob" || value.length < 100) { + if (type === "blob") value = new Uint8Array(value); cache[hash] = value; } return callback.apply(this, arguments); @@ -30,6 +32,8 @@ module.exports = function (repo) { }; function dupe(type, value) { - if (type === "blob") return value; + if (type === "blob") { + return new Uint8Array(value); + } return normalizeAs(type, value); } From 41cfc1f97bd56c4e26a8abf7d3e182eaeb65d26f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 15 Feb 2014 23:08:44 +0000 Subject: [PATCH 100/256] Expose mem-cache's cache. --- mixins/mem-cache.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 97a59e9..3f49bd4 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -1,8 +1,10 @@ "use strict"; var normalizeAs = require('../lib/encoders.js').normalizeAs; -var cache = {}; -module.exports = function (repo) { +var cache = memCache.cache = {}; +module.exports = memCache; + +function memCache(repo) { var loadAs = repo.loadAs; repo.loadAs = loadAsCached; function loadAsCached(type, hash, callback) { @@ -29,7 +31,7 @@ module.exports = function (repo) { return callback.apply(this, arguments); }); } -}; +} function dupe(type, value) { if (type === "blob") { From 1777dd9701ff094f7079102ad6f9e52e97e239d6 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 17 Feb 2014 16:47:13 +0000 Subject: [PATCH 101/256] Make xhr backend to js-github more robust. --- lib/xhr.js | 20 +++++++++++++++++--- mixins/github-db.js | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/xhr.js b/lib/xhr.js index 21bb148..b9af33c 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -8,8 +8,10 @@ module.exports = function (root, accessToken) { } url = url.replace(":root", root); if (!callback) return request.bind(this, accessToken, method, url, body); + var done = false; var json; var xhr = new XMLHttpRequest(); + xhr.timeout = 4000; xhr.open(method, 'https://api.github.com' + url, true); xhr.setRequestHeader("Authorization", "token " + accessToken); if (body) { @@ -17,16 +19,28 @@ module.exports = function (root, accessToken) { try { json = JSON.stringify(body); } catch (err) { return callback(err); } } + xhr.ontimeout = onTimeout; xhr.onreadystatechange = onReadyStateChange; xhr.send(json); + function onReadyStateChange() { + if (done) return; if (xhr.readyState !== 4) return; - var response; - if (xhr.responseText) { + // Give onTimeout a chance to run first if that's the reason status is 0. + if (!xhr.status) return setTimeout(onReadyStateChange, 0); + done = true; + var response = {message:xhr.responseText}; + if (xhr.responseText){ try { response = JSON.parse(xhr.responseText); } - catch (err) { return callback(err, null, xhr, response); } + catch (err) {} } return callback(null, xhr, response); } + + function onTimeout() { + if (done) return; + done = true; + callback(new Error("Timeout requesting " + url)); + } }; }; diff --git a/mixins/github-db.js b/mixins/github-db.js index 73b497b..00d5fd2 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -51,7 +51,7 @@ module.exports = function (repo, root, accessToken) { function onValue(err, xhr, result) { if (err) return callback(err); if (xhr.status < 200 || xhr.status >= 500) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); + return callback(new Error("Invalid HTTP response: " + xhr.statusCode + " " + result.message)); } if (xhr.status >= 300 && xhr.status < 500) return callback(); var body; From c3a3cdb488089ef187e17c70ae77e6678ef01ce6 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 21 Feb 2014 20:00:02 +0000 Subject: [PATCH 102/256] Add hasHash API that bypasses caches. --- mixins/github-db.js | 14 ++++++++++++++ mixins/indexed-db.js | 18 ++++++++++++++++++ mixins/mem-cache.js | 4 ++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/mixins/github-db.js b/mixins/github-db.js index 00d5fd2..27b4186 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -42,6 +42,7 @@ module.exports = function (repo, root, accessToken) { repo.readRef = readRef; // (ref) -> hash repo.updateRef = updateRef; // (ref, hash) -> hash repo.createTree = createTree; // (entries) -> hash, tree + repo.hasHash = hasHash; function loadAs(type, hash, callback) { // Github doesn't like empty trees, but we know them already. @@ -65,6 +66,19 @@ module.exports = function (repo, root, accessToken) { } } + function hasHash(type, hash, callback) { + apiRequest("GET", "/repos/:root/git/" + type + "s/" + hash, onValue); + + function onValue(err, xhr, result) { + if (err) return callback(err); + if (xhr.status < 200 || xhr.status >= 500) { + return callback(new Error("Invalid HTTP response: " + xhr.statusCode + " " + result.message)); + } + if (xhr.status >= 300 && xhr.status < 500) return callback(null, false); + callback(null, true); + } + } + function saveAs(type, body, callback) { var request; try { diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index a80dfec..a18fa6d 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -48,6 +48,7 @@ function mixin(repo, prefix) { repo.loadAs = loadAs; repo.readRef = readRef; repo.updateRef = updateRef; + repo.hasHash = hasHash; } function onError(evt) { @@ -95,6 +96,23 @@ function loadAs(type, hash, callback) { }; } +function hasHash(type, hash, callback) { + var trans = db.transaction(["objects"], "readwrite"); + var store = trans.objectStore("objects"); + var request = store.get(hash); + request.onsuccess = function(evt) { + var entry = evt.target.result; + if (!entry) return callback(null, false); + if (type !== entry.type) { + return callback(new TypeError("Type mismatch")); + } + callback(null, true); + }; + request.onerror = function(evt) { + callback(new Error(evt.value)); + }; +} + function readRef(ref, callback) { var key = this.refPrefix + "/" + ref; var trans = db.transaction(["refs"], "readwrite"); diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 3f49bd4..642869e 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -10,7 +10,7 @@ function memCache(repo) { function loadAsCached(type, hash, callback) { if (hash in cache) return callback(null, dupe(type, cache[hash]), hash); loadAs.call(repo, type, hash, function (err, value) { - if (err) return callback(err); + if (value === undefined) return callback(err); if (type !== "blob" || value.length < 100) { if (type === "blob") value = new Uint8Array(value); cache[hash] = value; @@ -28,7 +28,7 @@ function memCache(repo) { if (type === "blob") value = new Uint8Array(value); cache[hash] = value; } - return callback.apply(this, arguments); + return callback(null, hash, value); }); } } From 65965fd0e46ab5e7cff1a36becc5a06a1822f359 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 21 Feb 2014 20:21:53 +0000 Subject: [PATCH 103/256] Make hasHash for IDB check full tree since it's often used as a shared cache. --- mixins/indexed-db.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index a18fa6d..eada26d 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -2,6 +2,7 @@ /*global indexedDB*/ var encoders = require('../lib/encoders.js'); +var modes = require('../lib/modes.js'); var db; mixin.init = init; @@ -97,20 +98,23 @@ function loadAs(type, hash, callback) { } function hasHash(type, hash, callback) { - var trans = db.transaction(["objects"], "readwrite"); - var store = trans.objectStore("objects"); - var request = store.get(hash); - request.onsuccess = function(evt) { - var entry = evt.target.result; - if (!entry) return callback(null, false); - if (type !== entry.type) { - return callback(new TypeError("Type mismatch")); + loadAs(type, hash, function (err, value) { + if (err) return callback(err); + if (value === undefined) return callback(null, false); + if (type !== "tree") return callback(null, true); + var names = Object.keys(value); + next(); + function next() { + if (!names.length) return callback(null, true); + var name = names.pop(); + var entry = value[name]; + hasHash(modes.toType(entry.mode), entry.hash, function (err, has) { + if (err) return callback(err); + if (has) return next(); + callback(null, false); + }); } - callback(null, true); - }; - request.onerror = function(evt) { - callback(new Error(evt.value)); - }; + }); } function readRef(ref, callback) { From 8e639bf01a069af1a09deef1a29955dca9ed6102 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 22 Feb 2014 19:39:46 +0000 Subject: [PATCH 104/256] Rewrite codec to parse ini properly. --- lib/config-codec.js | 73 ++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/lib/config-codec.js b/lib/config-codec.js index cee72bf..830e35c 100644 --- a/lib/config-codec.js +++ b/lib/config-codec.js @@ -1,46 +1,51 @@ "use strict"; +// This is for working with git config files like .git/config and .gitmodules. +// I believe this is just INI format. module.exports = { encode: encode, - parse: parse + decode: decode }; function encode(config) { - var lines = []; - Object.keys(config).forEach(function (type) { - var obj = config[type]; - Object.keys(obj).forEach(function (name) { - var item = obj[name]; - lines.push('[' + type + ' "' + name + '"]'); - Object.keys(item).forEach(function (key) { - var value = item[key]; - lines.push("\t" + key + " = " + value); - }); - lines.push(""); - }); - }); - return lines.join("\n"); + return Object.keys(config).map(function (name) { + var obj = config[name]; + for (var key in obj) { + if (typeof obj[key] !== "object") { + return '[' + name + ']\n' + encodeBody(obj); + } + return Object.keys(obj).map(mapSub).join("\n"); + } + return ""; + + function mapSub(sub) { + return '[' + name + ' "' + sub + '"]\n' + encodeBody(obj[sub]); + } + }).join("\n"); } -function parse(text) { - var config = {}; - var match, offset = 0; - while ((match = text.substr(offset).match(/\[([a-z]*) "([^"]*)"\]([^\[]*)/))) { - var type = match[1]; - var section = config[type] || (config[type] = {}); - var name = match[2]; - section[name] = parseBody(match[3]); - offset += match[0].length; - } - return config; +function encodeBody(obj) { + return Object.keys(obj).map(function (name) { + return "\t" + name + " = " + obj[name]; + }).join("\n"); } -function parseBody(text) { - var entry = {}; - var match, offset = 0; - while ((match = text.substr(offset).match(/([^ \t\r\n]*) *= *([^ \t\r\n]*)/))) { - entry[match[1]] = match[2]; - offset += match[0].length; - } - return entry; +function decode(text) { + var config = {}; + var section; + text.split(/[\r\n]+/).forEach(function (line) { + var match = line.match(/\[([^ \t"\]]+) *(?:"([^"]+)")?\]/); + if (match) { + section = config[match[1]] || (config[match[1]] = {}); + if (match[2]) { + section = section[match[2]] = {}; + } + return; + } + match = line.match(/([^ \t=]+)[ \t]*=[ \t]*([^ \t]+)/); + if (match) { + section[match[1]] = match[2]; + } + }); + return config; } From 5ca7e010c4002030d82fe8504ee2082dc8b38fb5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 22 Feb 2014 20:42:07 +0000 Subject: [PATCH 105/256] Slience complaint about invalid github hashes --- mixins/github-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/github-db.js b/mixins/github-db.js index 27b4186..3da63f8 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -60,7 +60,7 @@ module.exports = function (repo, root, accessToken) { catch (err) { return callback(err); } if (hashAs(type, body) !== hash) { if (fixDate(type, body, hash)) console.log(type + " repaired", hash); - else console.warn("Unable to repair " + type, hash); + // else console.warn("Unable to repair " + type, hash); } return callback(null, body, hash); } From a9fcb98615c161945862121a97058ecb022c7484 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 25 Feb 2014 22:01:20 +0000 Subject: [PATCH 106/256] Add trailing newline when writing config files. --- lib/config-codec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config-codec.js b/lib/config-codec.js index 830e35c..2aa1958 100644 --- a/lib/config-codec.js +++ b/lib/config-codec.js @@ -21,7 +21,7 @@ function encode(config) { function mapSub(sub) { return '[' + name + ' "' + sub + '"]\n' + encodeBody(obj[sub]); } - }).join("\n"); + }).join("\n") + "\n"; } function encodeBody(obj) { From d0eca8e3a09f74e35e401f2e036b2068caaedc0b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 28 Feb 2014 22:17:33 +0000 Subject: [PATCH 107/256] DeepFreeze cache to prevent nasty bugs. --- mixins/mem-cache.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 642869e..a1060b2 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -13,6 +13,7 @@ function memCache(repo) { if (value === undefined) return callback(err); if (type !== "blob" || value.length < 100) { if (type === "blob") value = new Uint8Array(value); + else deepFreeze(value); cache[hash] = value; } return callback.apply(this, arguments); @@ -39,3 +40,11 @@ function dupe(type, value) { } return normalizeAs(type, value); } + +function deepFreeze(obj) { + Object.freeze(obj); + Object.keys(obj).forEach(function (key) { + var value = obj[key]; + if (typeof value === "object") deepFreeze(value); + }); +} From e12c2b676012d748beb671a2864084f89b908ed3 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 1 Mar 2014 17:38:23 +0000 Subject: [PATCH 108/256] Make defer and xhr work in node --- lib/defer.js | 30 ++++++--- lib/xhr.js | 168 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 148 insertions(+), 50 deletions(-) diff --git a/lib/defer.js b/lib/defer.js index 62dd713..be50543 100644 --- a/lib/defer.js +++ b/lib/defer.js @@ -1,7 +1,26 @@ "use strict"; -var timeouts = []; -var messageName = "zero-timeout-message"; +var timeouts, messageName; + +// node.js +if (typeof process === "object" && typeof process.nextTick === "function") { + module.exports = process.nextTick; +} +// some browsers +else if (typeof setImmediate === "function") { + module.exports = setImmediate; +} +// most other browsers +else { + timeouts = []; + messageName = "zero-timeout-message"; + window.addEventListener("message", handleMessage, true); + + module.exports = function (fn) { + timeouts.push(fn); + window.postMessage(messageName, "*"); + }; +} function handleMessage(event) { if (event.source == window && event.data == messageName) { @@ -12,10 +31,3 @@ function handleMessage(event) { } } } - -window.addEventListener("message", handleMessage, true); - -module.exports = function (fn) { - timeouts.push(fn); - window.postMessage(messageName, "*"); -}; diff --git a/lib/xhr.js b/lib/xhr.js index b9af33c..354e843 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -1,46 +1,132 @@ "use strict"; -module.exports = function (root, accessToken) { - return function request(method, url, body, callback) { - if (typeof body === "function" && callback === undefined) { - callback = body; - body = undefined; - } - url = url.replace(":root", root); - if (!callback) return request.bind(this, accessToken, method, url, body); - var done = false; - var json; - var xhr = new XMLHttpRequest(); - xhr.timeout = 4000; - xhr.open(method, 'https://api.github.com' + url, true); - xhr.setRequestHeader("Authorization", "token " + accessToken); - if (body) { - xhr.setRequestHeader("Content-Type", "application/json"); - try { json = JSON.stringify(body); } - catch (err) { return callback(err); } - } - xhr.ontimeout = onTimeout; - xhr.onreadystatechange = onReadyStateChange; - xhr.send(json); +// Node.js https module +if (typeof process === 'object' && typeof process.versions === 'object' && process.versions.node) { + var nodeRequire = require; // Prevent mine.js from seeing this require + var https = nodeRequire('https'); + var statusCodes = nodeRequire('http').STATUS_CODES; + module.exports = function (root, accessToken) { + var cache = {}; + return function request(method, url, body, callback) { + if (typeof body === "function" && callback === undefined) { + callback = body; + body = undefined; + } + if (!callback) return request.bind(this, accessToken, method, url, body); + url = url.replace(":root", root); - function onReadyStateChange() { - if (done) return; - if (xhr.readyState !== 4) return; - // Give onTimeout a chance to run first if that's the reason status is 0. - if (!xhr.status) return setTimeout(onReadyStateChange, 0); - done = true; - var response = {message:xhr.responseText}; - if (xhr.responseText){ - try { response = JSON.parse(xhr.responseText); } - catch (err) {} - } - return callback(null, xhr, response); - } + var json; + var headers = { + "User-Agent": "node.js" + }; + if (accessToken) { + headers["Authorization"] = "token " + accessToken; + } + if (body) { + headers["Content-Type"] = "application/json"; + try { json = JSON.stringify(body); } + catch (err) { return callback(err); } + } + if (method === "GET") { + var cached = cache[url]; + if (cached) { + headers["If-None-Match"] = cached.etag; + } + } + var options = { + hostname: "api.github.com", + path: url, + method: method, + headers: headers + }; + var req = https.request(options, function (res) { + var response; + var body = []; + res.on("data", function (chunk) { + body.push(chunk); + }); + res.on("end", function () { + body = Buffer.concat(body).toString(); + console.log(method, url, res.statusCode); + console.log("Rate limit %s/%s left", res.headers['x-ratelimit-remaining'], res.headers['x-ratelimit-limit']); + if (res.statusCode >= 400 && res.statusCode < 500) return callback(); + else if (res.statusCode === 200 && method === "GET" && /\/refs\//.test(url)) { + cache[url] = { + body: body, + etag: res.headers.etag + }; + } + else if (res.statusCode === 304) { + body = cache[url].body; + } + else if (res.statusCode < 200 || res.statusCode >= 300) { + return callback(new Error("Invalid HTTP response: " + res.statusCode)); + } + + var response = {message:body}; + if (body){ + try { response = JSON.parse(body); } + catch (err) {} + } + + // Fake parts of the xhr object using node APIs + var xhr = { + status: res.statusCode, + statusText: res.statusCode + " " + statusCodes[res.statusCode] + }; + return callback(null, xhr, response); + }); + }); + req.end(json); + req.on("error", callback); + }; + }; +} - function onTimeout() { - if (done) return; - done = true; - callback(new Error("Timeout requesting " + url)); - } +// Browser XHR +else { + module.exports = function (root, accessToken) { + return function request(method, url, body, callback) { + if (typeof body === "function" && callback === undefined) { + callback = body; + body = undefined; + } + url = url.replace(":root", root); + if (!callback) return request.bind(this, accessToken, method, url, body); + var done = false; + var json; + var xhr = new XMLHttpRequest(); + xhr.timeout = 4000; + xhr.open(method, 'https://api.github.com' + url, true); + xhr.setRequestHeader("Authorization", "token " + accessToken); + if (body) { + xhr.setRequestHeader("Content-Type", "application/json"); + try { json = JSON.stringify(body); } + catch (err) { return callback(err); } + } + xhr.ontimeout = onTimeout; + xhr.onreadystatechange = onReadyStateChange; + xhr.send(json); + + function onReadyStateChange() { + if (done) return; + if (xhr.readyState !== 4) return; + // Give onTimeout a chance to run first if that's the reason status is 0. + if (!xhr.status) return setTimeout(onReadyStateChange, 0); + done = true; + var response = {message:xhr.responseText}; + if (xhr.responseText){ + try { response = JSON.parse(xhr.responseText); } + catch (err) {} + } + return callback(null, xhr, response); + } + + function onTimeout() { + if (done) return; + done = true; + callback(new Error("Timeout requesting " + url)); + } + }; }; -}; +} \ No newline at end of file From 558ccb05520dbf699995a667d91c37bcad2899b0 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 1 Mar 2014 18:13:16 +0000 Subject: [PATCH 109/256] Mode xhr-node compat to it's own file. --- lib/xhr-node.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/xhr.js | 79 +------------------------------------------------ 2 files changed, 79 insertions(+), 78 deletions(-) create mode 100644 lib/xhr-node.js diff --git a/lib/xhr-node.js b/lib/xhr-node.js new file mode 100644 index 0000000..b24a3dd --- /dev/null +++ b/lib/xhr-node.js @@ -0,0 +1,78 @@ +var https = require('https'); +var statusCodes = require('http').STATUS_CODES; + +module.exports = function (root, accessToken) { + var cache = {}; + return function request(method, url, body, callback) { + if (typeof body === "function" && callback === undefined) { + callback = body; + body = undefined; + } + if (!callback) return request.bind(this, accessToken, method, url, body); + url = url.replace(":root", root); + + var json; + var headers = { + "User-Agent": "node.js" + }; + if (accessToken) { + headers["Authorization"] = "token " + accessToken; + } + if (body) { + headers["Content-Type"] = "application/json"; + try { json = JSON.stringify(body); } + catch (err) { return callback(err); } + } + if (method === "GET") { + var cached = cache[url]; + if (cached) { + headers["If-None-Match"] = cached.etag; + } + } + var options = { + hostname: "api.github.com", + path: url, + method: method, + headers: headers + }; + var req = https.request(options, function (res) { + var body = []; + res.on("data", function (chunk) { + body.push(chunk); + }); + res.on("end", function () { + body = Buffer.concat(body).toString(); + console.log(method, url, res.statusCode); + console.log("Rate limit %s/%s left", res.headers['x-ratelimit-remaining'], res.headers['x-ratelimit-limit']); + if (res.statusCode >= 400 && res.statusCode < 500) return callback(); + else if (res.statusCode === 200 && method === "GET" && /\/refs\//.test(url)) { + cache[url] = { + body: body, + etag: res.headers.etag + }; + } + else if (res.statusCode === 304) { + body = cache[url].body; + } + else if (res.statusCode < 200 || res.statusCode >= 300) { + return callback(new Error("Invalid HTTP response: " + res.statusCode)); + } + + var response = {message:body}; + if (body){ + try { response = JSON.parse(body); } + catch (err) {} + } + + // Fake parts of the xhr object using node APIs + var xhr = { + status: res.statusCode, + statusText: res.statusCode + " " + statusCodes[res.statusCode] + }; + return callback(null, xhr, response); + }); + }); + req.end(json); + req.on("error", callback); + }; +}; diff --git a/lib/xhr.js b/lib/xhr.js index 354e843..740a56e 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -3,84 +3,7 @@ // Node.js https module if (typeof process === 'object' && typeof process.versions === 'object' && process.versions.node) { var nodeRequire = require; // Prevent mine.js from seeing this require - var https = nodeRequire('https'); - var statusCodes = nodeRequire('http').STATUS_CODES; - module.exports = function (root, accessToken) { - var cache = {}; - return function request(method, url, body, callback) { - if (typeof body === "function" && callback === undefined) { - callback = body; - body = undefined; - } - if (!callback) return request.bind(this, accessToken, method, url, body); - url = url.replace(":root", root); - - var json; - var headers = { - "User-Agent": "node.js" - }; - if (accessToken) { - headers["Authorization"] = "token " + accessToken; - } - if (body) { - headers["Content-Type"] = "application/json"; - try { json = JSON.stringify(body); } - catch (err) { return callback(err); } - } - if (method === "GET") { - var cached = cache[url]; - if (cached) { - headers["If-None-Match"] = cached.etag; - } - } - var options = { - hostname: "api.github.com", - path: url, - method: method, - headers: headers - }; - var req = https.request(options, function (res) { - var response; - var body = []; - res.on("data", function (chunk) { - body.push(chunk); - }); - res.on("end", function () { - body = Buffer.concat(body).toString(); - console.log(method, url, res.statusCode); - console.log("Rate limit %s/%s left", res.headers['x-ratelimit-remaining'], res.headers['x-ratelimit-limit']); - if (res.statusCode >= 400 && res.statusCode < 500) return callback(); - else if (res.statusCode === 200 && method === "GET" && /\/refs\//.test(url)) { - cache[url] = { - body: body, - etag: res.headers.etag - }; - } - else if (res.statusCode === 304) { - body = cache[url].body; - } - else if (res.statusCode < 200 || res.statusCode >= 300) { - return callback(new Error("Invalid HTTP response: " + res.statusCode)); - } - - var response = {message:body}; - if (body){ - try { response = JSON.parse(body); } - catch (err) {} - } - - // Fake parts of the xhr object using node APIs - var xhr = { - status: res.statusCode, - statusText: res.statusCode + " " + statusCodes[res.statusCode] - }; - return callback(null, xhr, response); - }); - }); - req.end(json); - req.on("error", callback); - }; - }; + module.exports = nodeRequire('./xhr-node.js'); } // Browser XHR From 112104987aa22f6b374a953ceddae21ad5a6a708 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 1 Mar 2014 18:43:07 +0000 Subject: [PATCH 110/256] Add filesystem based cache for node. --- lib/node-fs-cache.js | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lib/node-fs-cache.js diff --git a/lib/node-fs-cache.js b/lib/node-fs-cache.js new file mode 100644 index 0000000..8cd1c5c --- /dev/null +++ b/lib/node-fs-cache.js @@ -0,0 +1,72 @@ +var encoders = require('../lib/encoders.js'); +var zlib = require('zlib'); +var modes = require('../lib/modes.js'); +var pathJoin = require('path').join; +var dirname = require('path').dirname; + +module.exports = function (root) { + var fs = require('fs'); + return { + loadAs: loadAs, + saveAs: saveAs, + }; + + function loadAs(type, hash, callback) { + var path = toPath(type, hash); + fs.readFile(path, function (err, body) { + if (err) { + if (err.code === 'ENOENT') return callback(); + return callback(err); + } + zlib.gunzip(body, function (err, body) { + if (err) return callback(err); + if (type !== "blob") { + try { body = JSON.parse(body.toString()); } + catch (err) {return callback(err); } + } + callback(null, body, hash); + }); + }); + } + + function saveAs(type, body, callback, forcedHash) { + var hash, data, path; + try { + body = encoders.normalizeAs(type, body); + hash = forcedHash || encoders.hashAs(type, body); + data = type === "blob" ? body : JSON.stringify(body); + path = toPath(type, hash); + } + catch (err) { return callback(err); } + zlib.gzip(data, function (err, data) { + if (err) return callback(err); + mkdirp(dirname(path), function (err) { + if (err) return callback(err); + fs.writeFile(path, data, function (err) { + if (err) return callback(err); + callback(null, hash, body); + }); + }); + }); + } + + function toPath(type, hash) { + return pathJoin(root, hash.substring(0, 2), hash.substring(2) + "." + type + ".gz"); + } + + function mkdirp(path, callback) { + make(); + function make(err) { + if (err) return callback(err); + fs.mkdir(path, onmkdir); + } + function onmkdir(err) { + if (err) { + if (err.code === "ENOENT") return mkdirp(dirname(path), make); + if (err.code === "EEXIST") return callback(); + return callback(err); + } + callback(); + } + } +}; From 8e1ccc4837b81f8758de01082e53752c26ba2a7a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 1 Mar 2014 18:48:41 +0000 Subject: [PATCH 111/256] Use deflate instead of gzip for node cache --- lib/node-fs-cache.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/node-fs-cache.js b/lib/node-fs-cache.js index 8cd1c5c..9e8cb73 100644 --- a/lib/node-fs-cache.js +++ b/lib/node-fs-cache.js @@ -18,7 +18,7 @@ module.exports = function (root) { if (err.code === 'ENOENT') return callback(); return callback(err); } - zlib.gunzip(body, function (err, body) { + zlib.inflate(body, function (err, body) { if (err) return callback(err); if (type !== "blob") { try { body = JSON.parse(body.toString()); } @@ -38,7 +38,7 @@ module.exports = function (root) { path = toPath(type, hash); } catch (err) { return callback(err); } - zlib.gzip(data, function (err, data) { + zlib.deflate(data, function (err, data) { if (err) return callback(err); mkdirp(dirname(path), function (err) { if (err) return callback(err); @@ -51,7 +51,7 @@ module.exports = function (root) { } function toPath(type, hash) { - return pathJoin(root, hash.substring(0, 2), hash.substring(2) + "." + type + ".gz"); + return pathJoin(root, hash.substring(0, 2), hash.substring(2) + "." + type); } function mkdirp(path, callback) { From afe2bc3c8fa9a9dfc987a6d4f6ce7f2b73fff069 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 1 Mar 2014 18:51:47 +0000 Subject: [PATCH 112/256] Remove unused require --- lib/node-fs-cache.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/node-fs-cache.js b/lib/node-fs-cache.js index 9e8cb73..cd560a4 100644 --- a/lib/node-fs-cache.js +++ b/lib/node-fs-cache.js @@ -1,6 +1,5 @@ var encoders = require('../lib/encoders.js'); var zlib = require('zlib'); -var modes = require('../lib/modes.js'); var pathJoin = require('path').join; var dirname = require('path').dirname; From c09dd9817e146a8167d9e15b0339425ebe7d5cab Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 1 Mar 2014 19:56:26 +0000 Subject: [PATCH 113/256] Port walkers from legacy branch. --- lib/walk.js | 42 +++++++++++++++++ mixins/github-db.js | 1 + mixins/walkers.js | 110 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 lib/walk.js create mode 100644 mixins/walkers.js diff --git a/lib/walk.js b/lib/walk.js new file mode 100644 index 0000000..166e0fc --- /dev/null +++ b/lib/walk.js @@ -0,0 +1,42 @@ +module.exports = function walk(seed, scan, loadKey, compare) { + var queue = [seed]; + var working = 0, error, cb; + return {read: read, abort: abort}; + + function read(callback) { + if (cb) return callback(new Error("Only one read at a time")); + if (working) { cb = callback; return; } + var item = queue.shift(); + if (!item) return callback(); + try { scan(item).forEach(onKey); } + catch (err) { return callback(err); } + return callback(null, item); + } + + function abort(callback) { return callback(); } + + function onError(err) { + if (cb) { + var callback = cb; cb = null; + return callback(err); + } + error = err; + } + + function onKey(key) { + working++; + loadKey(key, onItem); + } + + function onItem(err, item) { + working--; + if (err) return onError(err); + var index = queue.length; + while (index && compare(item, queue[index - 1])) index--; + queue.splice(index, 0, item); + if (!working && cb) { + var callback = cb; cb = null; + return read(callback); + } + } +}; diff --git a/mixins/github-db.js b/mixins/github-db.js index 3da63f8..553de37 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -213,6 +213,7 @@ module.exports = function (repo, root, accessToken) { function readRef(ref, callback) { + if (ref === "HEAD") ref = "refs/heads/master"; if (!(/^refs\//).test(ref)) { return callback(new TypeError("Invalid ref: " + ref)); } diff --git a/mixins/walkers.js b/mixins/walkers.js new file mode 100644 index 0000000..791d3df --- /dev/null +++ b/mixins/walkers.js @@ -0,0 +1,110 @@ +var walk = require('../lib/walk.js'); +var modes = require('../lib/modes.js'); + +module.exports = function (repo) { + repo.logWalk = logWalk; // (hash-ish) => stream + repo.treeWalk = treeWalk; // (treeHash) => stream +}; + +function logWalk(hashish, callback) { + if (!callback) return logWalk.bind(this, hashish); + var last, seen = {}; + var repo = this; + return repo.readRef("shallow", onShallow); + + function onShallow(err, shallow) { + last = shallow; + resolveHashish(repo, hashish, onHash); + + } + + function onHash(err, hash) { + if (err) return callback(err); + return repo.loadAs("commit", hash, onLoad); + } + + function onLoad(err, commit, hash) { + if (commit === undefined) return callback(err); + commit.hash = hash; + seen[hash] = true; + return callback(null, walk(commit, scan, loadKey, compare)); + } + + function scan(commit) { + if (last === commit) return []; + return commit.parents.filter(function (hash) { + return !seen[hash]; + }); + } + + function loadKey(hash, callback) { + return repo.loadAs("commit", hash, function (err, commit) { + if (err) return callback(err); + commit.hash = hash; + if (hash === last) commit.last = true; + return callback(null, commit); + }); + } + +} + +function compare(commit, other) { + return commit.author.date < other.author.date; +} + +function treeWalk(hash, callback) { + if (!callback) return treeWalk.bind(this, hash); + var repo = this; + return repo.loadAs("tree", hash, onTree); + + function onTree(err, body, hash) { + if (!body) return callback(err || new Error("Missing tree " + hash)); + var tree = { + mode: modes.tree, + hash: hash, + body: body, + path: "/" + }; + return callback(null, walk(tree, treeScan, treeLoadKey, treeCompare)); + } + + function treeLoadKey(entry, callback) { + if (entry.mode !== modes.tree) return callback(null, entry); + var type = modes.toType(entry.mode); + return repo.loadAs(type, entry.hash, function (err, body) { + if (err) return callback(err); + entry.body = body; + return callback(null, entry); + }); + } + +} + +function treeScan(object) { + if (object.mode !== modes.tree) return []; + var tree = object.body; + return Object.keys(tree).map(function (name) { + var entry = tree[name]; + var path = object.path + name; + if (entry.mode === modes.tree) path += "/"; + return { + mode: entry.mode, + hash: entry.hash, + path: path + }; + }); +} + +function treeCompare(first, second) { + return first.path < second.path; +} + +function resolveHashish(repo, hashish, callback) { + if (/^[0-9a-f]{40}$/.test(hashish)) { + return callback(null, hashish); + } + repo.readRef(hashish, function (err, hash) { + if (!hash) return callback(err || new Error("Bad ref " + hashish)); + callback(null, hash); + }); +} From 4adae5aab54afac31e161e7a790d443597ab02c6 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sun, 2 Mar 2014 02:58:30 +0000 Subject: [PATCH 114/256] Improve error handling in fs-cache --- lib/node-fs-cache.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/node-fs-cache.js b/lib/node-fs-cache.js index cd560a4..7bb1073 100644 --- a/lib/node-fs-cache.js +++ b/lib/node-fs-cache.js @@ -11,7 +11,9 @@ module.exports = function (root) { }; function loadAs(type, hash, callback) { - var path = toPath(type, hash); + var path; + try { path = toPath(type, hash); } + catch (err) { return callback(err); } fs.readFile(path, function (err, body) { if (err) { if (err.code === 'ENOENT') return callback(); @@ -50,6 +52,7 @@ module.exports = function (root) { } function toPath(type, hash) { + if (!type || !hash) throw new TypeError("type and hash required"); return pathJoin(root, hash.substring(0, 2), hash.substring(2) + "." + type); } From a933ab132101b2a6bba7e171105bfba25063f6b8 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sun, 2 Mar 2014 03:10:54 +0000 Subject: [PATCH 115/256] When handling 304 responses, pretend they were 200 internally. --- lib/xhr-node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/xhr-node.js b/lib/xhr-node.js index b24a3dd..5ecd157 100644 --- a/lib/xhr-node.js +++ b/lib/xhr-node.js @@ -53,6 +53,7 @@ module.exports = function (root, accessToken) { } else if (res.statusCode === 304) { body = cache[url].body; + res.statusCode = 200; } else if (res.statusCode < 200 || res.statusCode >= 300) { return callback(new Error("Invalid HTTP response: " + res.statusCode)); From 186ffc434d411483e12b0c04c4437f6343c52ca4 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 4 Mar 2014 23:09:27 +0000 Subject: [PATCH 116/256] Add inflate/deflate using chris's inflate and pako's deflate. --- lib/deflate.js | 13 + lib/inflate-stream.js | 808 ++++++++++++++++++++++++++++++++++++++++++ lib/inflate.js | 35 ++ package.json | 3 +- 4 files changed, 858 insertions(+), 1 deletion(-) create mode 100644 lib/deflate.js create mode 100644 lib/inflate-stream.js create mode 100644 lib/inflate.js diff --git a/lib/deflate.js b/lib/deflate.js new file mode 100644 index 0000000..54a032c --- /dev/null +++ b/lib/deflate.js @@ -0,0 +1,13 @@ +if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { + var nodeRequire = require; + module.exports = nodeRequire("zlib'").deflate; +} +else { + var deflate = require('pako/deflate').deflate; + module.exports = function (buffer, callback) { + var out; + try { out = deflate(buffer); } + catch (err) { return callback(err); } + return callback(null, out); + }; +} diff --git a/lib/inflate-stream.js b/lib/inflate-stream.js new file mode 100644 index 0000000..56c31a2 --- /dev/null +++ b/lib/inflate-stream.js @@ -0,0 +1,808 @@ +// Taken from chrisdickinson/inflate's min.js +// Modified to use bodec and inlined in js-git +// Original code under MIT License +// Copyright Chris Diskinson + +module.exports = inflate + +var binary = require('bodec') + +var MAXBITS = 15 + , MAXLCODES = 286 + , MAXDCODES = 30 + , MAXCODES = (MAXLCODES+MAXDCODES) + , FIXLCODES = 288 + +var lens = [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 +] + +var lext = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 +] + +var dists = [ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 +] + +var dext = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13 +] + +var order = [ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 +] + +var WINDOW = 32768 + , WINDOW_MINUS_ONE = WINDOW - 1 + +function inflate(emit, on_unused) { + var output = new Uint8Array(WINDOW) + , need_input = false + , buffer_offset = 0 + , bytes_read = 0 + , output_idx = 0 + , ended = false + , state = null + , states = [] + , buffer = [] + , got = 0 + + // buffer up to 128k "output one" bytes + var OUTPUT_ONE_LENGTH = 131070 + , output_one_offs = OUTPUT_ONE_LENGTH + , output_one_buf + + var bitbuf = 0 + , bitcnt = 0 + , is_final = false + , fixed_codes + + var adler_s1 = 1 + , adler_s2 = 0 + + onread.recycle = function recycle() { + var out + buffer.length = 0 + buffer_offset = 0 + output_idx = 0 + bitbuf = 0 + bitcnt = 0 + states.length = 0 + is_final = false + need_input = false + bytes_read = 0 + output_idx = 0 + ended = false + got = 0 + adler_s1 = 1 + adler_s2 = 0 + output_one_offs = 0 + become(noop, {}, noop) + start_stream_header() + return stream + } + + var bytes_need = 0 + , bytes_value = [] + + var bits_need = 0 + , bits_value = [] + + var codes_distcode = null + , codes_lencode = null + , codes_len = 0 + , codes_dist = 0 + , codes_symbol = 0 + + var dynamic_distcode = {symbol: [], count: []} + , dynamic_lencode = {symbol: [], count: []} + , dynamic_lengths = [] + , dynamic_nlen = 0 + , dynamic_ndist = 0 + , dynamic_ncode = 0 + , dynamic_index = 0 + , dynamic_symbol = 0 + , dynamic_len = 0 + + var decode_huffman = null + , decode_len = 0 + , decode_code = 0 + , decode_first = 0 + , decode_count = 0 + , decode_index = 0 + + var last = null + + become(noop, {}, noop) + start_stream_header() + + return onread + + function onread(err, buf) { + if(buf === undefined) { + return emit(err) + } + + return write(buf) + } + + function noop() { + + } + + function call_header() { + } + + function call_bytes(need) { + bytes_value.length = 0 + bytes_need = need + } + + function call_bits(need) { + bits_value = 0 + bits_need = need + } + + function call_codes(distcode, lencode) { + codes_len = + codes_dist = + codes_symbol = 0 + codes_distcode = distcode + codes_lencode = lencode + } + + function call_dynamic() { + dynamic_distcode.symbol.length = + dynamic_distcode.count.length = + dynamic_lencode.symbol.length = + dynamic_lencode.count.length = + dynamic_lengths.length = 0 + dynamic_nlen = 0 + dynamic_ndist = 0 + dynamic_ncode = 0 + dynamic_index = 0 + dynamic_symbol = 0 + dynamic_len = 0 + } + + function call_decode(h) { + decode_huffman = h + decode_len = 1 + decode_first = + decode_index = + decode_code = 0 + } + + function write(buf) { + buffer.push(buf) + got += buf.length + if(!ended) { + execute() + } + } + + function execute() { + do { + states[0].current() + } while(!need_input && !ended) + + var needed = need_input + need_input = false + } + + function start_stream_header() { + become(bytes, call_bytes(2), got_stream_header) + } + + function got_stream_header() { + var cmf = last[0] + , flg = last[1] + + + if((cmf << 8 | flg) % 31 !== 0) { + emit(new Error( + 'failed header check' + )) + return + } + + + + + if(flg & 32) { + return become(bytes, call_bytes(4), on_got_fdict) + } + return become(bits, call_bits(1), on_got_is_final) + } + + + + + function on_got_fdict() { + return become(bits, call_bits(1), on_got_is_final) + } + + + + + + + + + function on_got_is_final() { + is_final = last + become(bits, call_bits(2), on_got_type) + } + + + + + + + + + + + + + function on_got_type() { + if(last === 0) { + become(bytes, call_bytes(4), on_got_len_nlen) + return + } + + if(last === 1) { + // `fixed` and `dynamic` blocks both eventually delegate + // to the "codes" state -- which reads bits of input, throws + // them into a huffman tree, and produces "symbols" of output. + fixed_codes = fixed_codes || build_fixed() + become(start_codes, call_codes( + fixed_codes.distcode + , fixed_codes.lencode + ), done_with_codes) + return + } + + become(start_dynamic, call_dynamic(), done_with_codes) + return + } + + + + + function on_got_len_nlen() { + var want = last[0] | (last[1] << 8) + , nlen = last[2] | (last[3] << 8) + + if((~nlen & 0xFFFF) !== want) { + emit(new Error( + 'failed len / nlen check' + )) + } + + if(!want) { + become(bits, call_bits(1), on_got_is_final) + return + } + become(bytes, call_bytes(want), on_got_stored) + } + + + + + function on_got_stored() { + output_many(last) + if(is_final) { + become(bytes, call_bytes(4), on_got_adler) + return + } + become(bits, call_bits(1), on_got_is_final) + } + + + + + + + function start_dynamic() { + become(bits, call_bits(5), on_got_nlen) + } + + function on_got_nlen() { + dynamic_nlen = last + 257 + become(bits, call_bits(5), on_got_ndist) + } + + function on_got_ndist() { + dynamic_ndist = last + 1 + become(bits, call_bits(4), on_got_ncode) + } + + function on_got_ncode() { + dynamic_ncode = last + 4 + if(dynamic_nlen > MAXLCODES || dynamic_ndist > MAXDCODES) { + emit(new Error('bad counts')) + return + } + + become(bits, call_bits(3), on_got_lengths_part) + } + + function on_got_lengths_part() { + dynamic_lengths[order[dynamic_index]] = last + + ++dynamic_index + if(dynamic_index === dynamic_ncode) { + for(; dynamic_index < 19; ++dynamic_index) { + dynamic_lengths[order[dynamic_index]] = 0 + } + + // temporarily construct the `lencode` using the + // lengths we've read. we'll actually be using the + // symbols produced by throwing bits into the huffman + // tree to constuct the `lencode` and `distcode` huffman + // trees. + construct(dynamic_lencode, dynamic_lengths, 19) + dynamic_index = 0 + + become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) + return + } + become(bits, call_bits(3), on_got_lengths_part) + } + + function on_got_dynamic_symbol_iter() { + dynamic_symbol = last + + if(dynamic_symbol < 16) { + dynamic_lengths[dynamic_index++] = dynamic_symbol + do_check() + return + } + + dynamic_len = 0 + if(dynamic_symbol === 16) { + become(bits, call_bits(2), on_got_dynamic_symbol_16) + return + } + + if(dynamic_symbol === 17) { + become(bits, call_bits(3), on_got_dynamic_symbol_17) + return + } + + become(bits, call_bits(7), on_got_dynamic_symbol) + } + + function on_got_dynamic_symbol_16() { + dynamic_len = dynamic_lengths[dynamic_index - 1] + on_got_dynamic_symbol_17() + } + + function on_got_dynamic_symbol_17() { + dynamic_symbol = 3 + last + do_dynamic_end_loop() + } + + function on_got_dynamic_symbol() { + dynamic_symbol = 11 + last + do_dynamic_end_loop() + } + + function do_dynamic_end_loop() { + if(dynamic_index + dynamic_symbol > dynamic_nlen + dynamic_ndist) { + emit(new Error('too many lengths')) + return + } + + while(dynamic_symbol--) { + dynamic_lengths[dynamic_index++] = dynamic_len + } + + do_check() + } + + function do_check() { + if(dynamic_index >= dynamic_nlen + dynamic_ndist) { + end_read_dynamic() + return + } + become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) + } + + function end_read_dynamic() { + // okay, we can finally start reading data out of the stream. + construct(dynamic_lencode, dynamic_lengths, dynamic_nlen) + construct(dynamic_distcode, dynamic_lengths.slice(dynamic_nlen), dynamic_ndist) + become(start_codes, call_codes( + dynamic_distcode + , dynamic_lencode + ), done_with_codes) + } + + function start_codes() { + become(decode, call_decode(codes_lencode), on_got_codes_symbol) + } + + function on_got_codes_symbol() { + var symbol = codes_symbol = last + if(symbol < 0) { + emit(new Error('invalid symbol')) + return + } + + if(symbol < 256) { + output_one(symbol) + become(decode, call_decode(codes_lencode), on_got_codes_symbol) + return + } + + if(symbol > 256) { + symbol = codes_symbol -= 257 + if(symbol >= 29) { + emit(new Error('invalid fixed code')) + return + } + + become(bits, call_bits(lext[symbol]), on_got_codes_len) + return + } + + if(symbol === 256) { + unbecome() + return + } + } + + + + + + + function on_got_codes_len() { + codes_len = lens[codes_symbol] + last + become(decode, call_decode(codes_distcode), on_got_codes_dist_symbol) + } + + + function on_got_codes_dist_symbol() { + codes_symbol = last + if(codes_symbol < 0) { + emit(new Error('invalid distance symbol')) + return + } + + become(bits, call_bits(dext[codes_symbol]), on_got_codes_dist_dist) + } + + function on_got_codes_dist_dist() { + var dist = dists[codes_symbol] + last + + // Once we have a "distance" and a "length", we start to output bytes. + // We reach "dist" back from our current output position to get the byte + // we should repeat and output it (thus moving the output window cursor forward). + // Two notes: + // + // 1. Theoretically we could overlap our output and input. + // 2. `X % (2^N) == X & (2^N - 1)` with the distinction that + // the result of the bitwise AND won't be negative for the + // range of values we're feeding it. Spare a modulo, spoil the child. + while(codes_len--) { + output_one(output[(output_idx - dist) & WINDOW_MINUS_ONE]) + } + + become(decode, call_decode(codes_lencode), on_got_codes_symbol) + } + + function done_with_codes() { + if(is_final) { + become(bytes, call_bytes(4), on_got_adler) + return + } + become(bits, call_bits(1), on_got_is_final) + } + + + + + function on_got_adler() { + var check_s1 = last[3] | (last[2] << 8) + , check_s2 = last[1] | (last[0] << 8) + + if(check_s2 !== adler_s2 || check_s1 !== adler_s1) { + emit(new Error( + 'bad adler checksum: '+[check_s2, adler_s2, check_s1, adler_s1] + )) + return + } + + ended = true + + output_one_recycle() + + if(on_unused) { + on_unused( + [binary.slice(buffer[0], buffer_offset)].concat(buffer.slice(1)) + , bytes_read + ) + } + + output_idx = 0 + ended = true + emit() + } + + function decode() { + _decode() + } + + function _decode() { + if(decode_len > MAXBITS) { + emit(new Error('ran out of codes')) + return + } + + become(bits, call_bits(1), got_decode_bit) + } + + function got_decode_bit() { + decode_code = (decode_code | last) >>> 0 + decode_count = decode_huffman.count[decode_len] + if(decode_code < decode_first + decode_count) { + unbecome(decode_huffman.symbol[decode_index + (decode_code - decode_first)]) + return + } + decode_index += decode_count + decode_first += decode_count + decode_first <<= 1 + decode_code = (decode_code << 1) >>> 0 + ++decode_len + _decode() + } + + + function become(fn, s, then) { + if(typeof then !== 'function') { + throw new Error + } + states.unshift({ + current: fn + , next: then + }) + } + + function unbecome(result) { + if(states.length > 1) { + states[1].current = states[0].next + } + states.shift() + if(!states.length) { + ended = true + + output_one_recycle() + if(on_unused) { + on_unused( + [binary.slice(buffer[0], buffer_offset)].concat(buffer.slice(1)) + , bytes_read + ) + } + output_idx = 0 + ended = true + emit() + return + } + last = result + } + + function bits() { + var byt + , idx + + idx = 0 + bits_value = bitbuf + while(bitcnt < bits_need) { + // we do this to preserve `bits_value` when + // "need_input" is tripped. + // + // fun fact: if we moved that into the `if` statement + // below, it would trigger a deoptimization of this (very + // hot) function. JITs! + bitbuf = bits_value + byt = take() + if(need_input) { + break + } + ++idx + bits_value = (bits_value | (byt << bitcnt)) >>> 0 + bitcnt += 8 + } + + if(!need_input) { + bitbuf = bits_value >>> bits_need + bitcnt -= bits_need + unbecome((bits_value & ((1 << bits_need) - 1)) >>> 0) + } + } + + + + function bytes() { + var byte_accum = bytes_value + , value + + while(bytes_need--) { + value = take() + + + if(need_input) { + bitbuf = bitcnt = 0 + bytes_need += 1 + break + } + byte_accum[byte_accum.length] = value + } + if(!need_input) { + bitcnt = bitbuf = 0 + unbecome(byte_accum) + } + } + + + + function take() { + if(!buffer.length) { + need_input = true + return + } + + if(buffer_offset === buffer[0].length) { + buffer.shift() + buffer_offset = 0 + return take() + } + + ++bytes_read + + return bitbuf = takebyte() + } + + function takebyte() { + return buffer[0][buffer_offset++] + } + + + + function output_one(val) { + adler_s1 = (adler_s1 + val) % 65521 + adler_s2 = (adler_s2 + adler_s1) % 65521 + output[output_idx++] = val + output_idx &= WINDOW_MINUS_ONE + output_one_pool(val) + } + + function output_one_pool(val) { + if(output_one_offs === OUTPUT_ONE_LENGTH) { + output_one_recycle() + } + + output_one_buf[output_one_offs++] = val + } + + function output_one_recycle() { + if(output_one_offs > 0) { + if(output_one_buf) { + emit(null, binary.slice(output_one_buf, 0, output_one_offs)) + } else { + } + output_one_buf = binary.create(OUTPUT_ONE_LENGTH) + output_one_offs = 0 + } + } + + function output_many(vals) { + var len + , byt + , olen + + output_one_recycle() + for(var i = 0, len = vals.length; i < len; ++i) { + byt = vals[i] + adler_s1 = (adler_s1 + byt) % 65521 + adler_s2 = (adler_s2 + adler_s1) % 65521 + output[output_idx++] = byt + output_idx &= WINDOW_MINUS_ONE + } + + emit(binary.fromArray(vals)) + } +} + +function build_fixed() { + var lencnt = [] + , lensym = [] + , distcnt = [] + , distsym = [] + + var lencode = { + count: lencnt + , symbol: lensym + } + + var distcode = { + count: distcnt + , symbol: distsym + } + + var lengths = [] + , symbol + + for(symbol = 0; symbol < 144; ++symbol) { + lengths[symbol] = 8 + } + for(; symbol < 256; ++symbol) { + lengths[symbol] = 9 + } + for(; symbol < 280; ++symbol) { + lengths[symbol] = 7 + } + for(; symbol < FIXLCODES; ++symbol) { + lengths[symbol] = 8 + } + construct(lencode, lengths, FIXLCODES) + + for(symbol = 0; symbol < MAXDCODES; ++symbol) { + lengths[symbol] = 5 + } + construct(distcode, lengths, MAXDCODES) + return {lencode: lencode, distcode: distcode} +} + +function construct(huffman, lengths, num) { + var symbol + , left + , offs + , len + + offs = [] + + for(len = 0; len <= MAXBITS; ++len) { + huffman.count[len] = 0 + } + + for(symbol = 0; symbol < num; ++symbol) { + huffman.count[lengths[symbol]] += 1 + } + + if(huffman.count[0] === num) { + return + } + + left = 1 + for(len = 1; len <= MAXBITS; ++len) { + left <<= 1 + left -= huffman.count[len] + if(left < 0) { + return left + } + } + + offs[1] = 0 + for(len = 1; len < MAXBITS; ++len) { + offs[len + 1] = offs[len] + huffman.count[len] + } + + for(symbol = 0; symbol < num; ++symbol) { + if(lengths[symbol] !== 0) { + huffman.symbol[offs[lengths[symbol]]++] = symbol + } + } + + return left +} \ No newline at end of file diff --git a/lib/inflate.js b/lib/inflate.js new file mode 100644 index 0000000..7856e90 --- /dev/null +++ b/lib/inflate.js @@ -0,0 +1,35 @@ +if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { + var nodeRequire = require; + module.exports = nodeRequire("zlib'").inflate; +} +else { + var inflateStream = require('./inflate-stream.js'); + var binary = require('bodec'); + module.exports = function inflate(buffer, callback) { + var out; + try { + var parts = []; + var done = false; + var push = inflateStream(onEmit, onUnused); + push(null, buffer); + push(); + if (!done) throw new Error("Missing input data"); + out = binary.join(parts); + } catch (err) { + return callback(err); + } + callback(null, out); + + function onEmit(err, chunk) { + if (err) throw err; + if (chunk) parts.push(chunk); + else done = true; + } + + function onUnused(extra) { + if (extra && extra.length && extra[0].length) { + throw new Error("Too much input data"); + } + } + }; +} diff --git a/package.json b/package.json index ab418d4..f1e7667 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "bodec": "git://github.com/creationix/bodec.git", - "git-sha1": "git://github.com/creationix/git-sha1.git" + "git-sha1": "git://github.com/creationix/git-sha1.git", + "pako": "git://github.com/nodeca/pako.git" } } From a598117bbf0ca9dbc48b8f8f4a43dfb75497d2dd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 4 Mar 2014 23:14:41 +0000 Subject: [PATCH 117/256] Fix node zlib require path --- lib/deflate.js | 2 +- lib/inflate.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/deflate.js b/lib/deflate.js index 54a032c..72d333e 100644 --- a/lib/deflate.js +++ b/lib/deflate.js @@ -1,6 +1,6 @@ if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { var nodeRequire = require; - module.exports = nodeRequire("zlib'").deflate; + module.exports = nodeRequire("zlib").deflate; } else { var deflate = require('pako/deflate').deflate; diff --git a/lib/inflate.js b/lib/inflate.js index 7856e90..b69f10d 100644 --- a/lib/inflate.js +++ b/lib/inflate.js @@ -1,6 +1,6 @@ if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { var nodeRequire = require; - module.exports = nodeRequire("zlib'").inflate; + module.exports = nodeRequire("zlib").inflate; } else { var inflateStream = require('./inflate-stream.js'); From 7b5157f5b40584c29a98ac80bd0e30333ae1b1c7 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 5 Mar 2014 02:45:32 +0000 Subject: [PATCH 118/256] Start importing packops and related code. --- lib/apply-delta.js | 109 +++++++++++++++++++++++ lib/deframe.js | 18 ++++ lib/frame.js | 8 ++ lib/indexof.js | 8 ++ lib/pack-codec.js | 212 +++++++++++++++++++++++++++++++++++++++++++++ lib/parseascii.js | 7 ++ lib/parsedec.js | 7 ++ mixins/packops.js | 194 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 563 insertions(+) create mode 100644 lib/apply-delta.js create mode 100644 lib/deframe.js create mode 100644 lib/frame.js create mode 100644 lib/indexof.js create mode 100644 lib/pack-codec.js create mode 100644 lib/parseascii.js create mode 100644 lib/parsedec.js create mode 100644 mixins/packops.js diff --git a/lib/apply-delta.js b/lib/apply-delta.js new file mode 100644 index 0000000..e58d54b --- /dev/null +++ b/lib/apply-delta.js @@ -0,0 +1,109 @@ +// This is Chris Dickinson's code + +var binary = require('bodec') + // , Decoder = require('varint/decode.js') + // , vi = new Decoder + +// we use writeUint[8|32][LE|BE] instead of indexing +// into buffers so that we get buffer-browserify compat. +var OFFSET_BUFFER = binary.create(4) + , LENGTH_BUFFER = binary.create(4) + +module.exports = apply_delta; +function apply_delta(delta, target) { + throw "TODO: fix me" + var base_size_info = {size: null, buffer: null} + , resized_size_info = {size: null, buffer: null} + , output_buffer + , out_idx + , command + , len + , idx + + delta_header(delta, base_size_info) + delta_header(base_size_info.buffer, resized_size_info) + + delta = resized_size_info.buffer + + idx = + out_idx = 0 + output_buffer = binary.create(resized_size_info.size) + + len = delta.length + + while(idx < len) { + command = delta[idx++] + command & 0x80 ? copy() : insert() + } + + return output_buffer + + function copy() { + OFFSET_BUFFER[0] = 0; + OFFSET_BUFFER[1] = 0; + OFFSET_BUFFER[2] = 0; + OFFSET_BUFFER[3] = 0; + LENGTH_BUFFER[0] = 0; + LENGTH_BUFFER[1] = 0; + LENGTH_BUFFER[2] = 0; + LENGTH_BUFFER[3] = 0; + + var check = 1 + , length + , offset + + for(var x = 0; x < 4; ++x) { + if(command & check) { + OFFSET_BUFFER[3 - x] = delta[idx++] + } + check <<= 1 + } + + for(var x = 0; x < 3; ++x) { + if(command & check) { + LENGTH_BUFFER[3 - x] = delta[idx++] + } + check <<= 1 + } + LENGTH_BUFFER[0] = 0 + + length = ( + (LENGTH_BUFFER[0] << 24) | + (LENGTH_BUFFER[1] << 16) | + (LENGTH_BUFFER[2] << 8) | + (LENGTH_BUFFER[3])) || 0x10000; + offset = + (OFFSET_BUFFER[0] << 24) | + (OFFSET_BUFFER[1] << 16) | + (OFFSET_BUFFER[2] << 8) | + (OFFSET_BUFFER[3]); + + binary.copy(target, output_buffer, out_idx, offset, offset + length) + out_idx += length + } + + function insert() { + binary.copy(delta, output_buffer, out_idx, idx, command + idx) + idx += command + out_idx += command + } +} + +function delta_header(buf, output) { + var done = false + , idx = 0 + , size = 0 + + vi.ondata = function(s) { + size = s + done = true + } + + do { + vi.write(buf[idx++]) + } while(!done) + + output.size = size + output.buffer = binary.slice(buf, idx) + +} diff --git a/lib/deframe.js b/lib/deframe.js new file mode 100644 index 0000000..f3284ae --- /dev/null +++ b/lib/deframe.js @@ -0,0 +1,18 @@ +var binary = require('bodec'); +var indexOf = require('./indexof.js'); +var parseDec = require('./parsedec.js'); +var parseAscii = require('./parseascii.js'); + +module.exports = function deframe(buffer) { + var space = indexOf(buffer, 0x20); + if (space < 0) throw new Error("Invalid git object buffer"); + var nil = indexOf(buffer, 0x00, space); + if (nil < 0) throw new Error("Invalid git object buffer"); + var body = binary.slice(buffer, nil + 1); + var size = parseDec(buffer, space + 1, nil); + if (size !== body.length) throw new Error("Invalid body length."); + return [ + parseAscii(buffer, 0, space), + body + ]; +}; diff --git a/lib/frame.js b/lib/frame.js new file mode 100644 index 0000000..5c7fe2c --- /dev/null +++ b/lib/frame.js @@ -0,0 +1,8 @@ +var binary = require('bodec'); + +module.exports = function frame(type, body) { + return binary.join([ + binary.fromRaw(type + " " + body.length + "\0"), + body + ]); +}; diff --git a/lib/indexof.js b/lib/indexof.js new file mode 100644 index 0000000..18c61a5 --- /dev/null +++ b/lib/indexof.js @@ -0,0 +1,8 @@ +module.exports = function indexOf(buffer, byte, i) { + i |= 0; + var length = buffer.length; + for (;;i++) { + if (i >= length) return -1; + if (buffer[i] === byte) return i; + } +}; diff --git a/lib/pack-codec.js b/lib/pack-codec.js new file mode 100644 index 0000000..45d936c --- /dev/null +++ b/lib/pack-codec.js @@ -0,0 +1,212 @@ +var inflate = require('./inflate.js'); +var deflate = require('./deflate.js'); +var sha1 = require('git-sha1'); +var binary = require('bodec'); + +var typeToNum = { + commit: 1, + tree: 2, + blob: 3, + tag: 4, + "ofs-delta": 6, + "ref-delta": 7 +}; +var numToType = {}; +for (var type in typeToNum) { + var num = typeToNum[type]; + numToType[num] = type; +} + +exports.packFrame = packFrame; +function packFrame(type, body, callback) { + var length = body.length; + var head = [(typeToNum[type] << 4) | (length & 0xf)]; + var i = 0; + length >>= 4; + while (length) { + head[i++] |= 0x80; + head[i] = length & 0x7f; + length >>= 7; + } + deflate(body, function (err, body) { + if (err) return callback(err); + callback(null, binary.join([binary.fromArray(head), body])); + }); +} + +exports.decodePack = decodePack; +function decodePack(emit) { + + var state = $pack; + var sha1sum = sha1(); + var inf = inflate(); + + var offset = 0; + var position = 0; + var version = 0x4b434150; // PACK reversed + var num = 0; + var type = 0; + var length = 0; + var ref = null; + var checksum = ""; + var start = 0; + var parts = []; + + + return function (chunk) { + if (chunk === undefined) { + if (num || checksum.length < 40) throw new Error("Unexpected end of input stream"); + return emit(); + } + + for (var i = 0, l = chunk.length; i < l; i++) { + // console.log([state, i, chunk[i].toString(16)]); + if (!state) throw new Error("Unexpected extra bytes: " + binary.slice(chunk, i)); + state = state(chunk[i], i, chunk); + position++; + } + if (!state) return; + if (state !== $checksum) sha1sum.update(chunk); + var buff = inf.flush(); + if (buff.length) { + parts.push(buff); + } + }; + + // The first four bytes in a packfile are the bytes 'PACK' + function $pack(byte) { + if ((version & 0xff) === byte) { + version >>>= 8; + return version ? $pack : $version; + } + throw new Error("Invalid packfile header"); + } + + // The version is stored as an unsigned 32 integer in network byte order. + // It must be version 2 or 3. + function $version(byte) { + version = (version << 8) | byte; + if (++offset < 4) return $version; + if (version >= 2 && version <= 3) { + offset = 0; + return $num; + } + throw new Error("Invalid version number " + num); + } + + // The number of objects in this packfile is also stored as an unsigned 32 bit int. + function $num(byte) { + num = (num << 8) | byte; + if (++offset < 4) return $num; + offset = 0; + emit({version: version, num: num}); + return $header; + } + + // n-byte type and length (3-bit type, (n-1)*7+4-bit length) + // CTTTSSSS + // C is continue bit, TTT is type, S+ is length + function $header(byte) { + if (start === 0) start = position; + type = byte >> 4 & 0x07; + length = byte & 0x0f; + if (byte & 0x80) { + offset = 4; + return $header2; + } + return afterHeader(); + } + + // Second state in the same header parsing. + // CSSSSSSS* + function $header2(byte) { + length |= (byte & 0x7f) << offset; + if (byte & 0x80) { + offset += 7; + return $header2; + } + return afterHeader(); + } + + // Common helper for finishing tiny and normal headers. + function afterHeader() { + offset = 0; + if (type === 6) { + ref = 0; + return $ofsDelta; + } + if (type === 7) { + ref = ""; + return $refDelta; + } + return $body; + } + + // Big-endian modified base 128 number encoded ref offset + function $ofsDelta(byte) { + ref = byte & 0x7f; + if (byte & 0x80) return $ofsDelta2; + return $body; + } + + function $ofsDelta2(byte) { + ref = ((ref + 1) << 7) | (byte & 0x7f); + if (byte & 0x80) return $ofsDelta2; + return $body; + } + + // 20 byte raw sha1 hash for ref + function $refDelta(byte) { + ref += toHex(byte); + if (++offset < 20) return $refDelta; + return $body; + } + + // Common helper for generating 2-character hex numbers + function toHex(num) { + return num < 0x10 ? "0" + num.toString(16) : num.toString(16); + } + + // Common helper for emitting all three object shapes + function emitObject() { + var item = { + type: numToType[type], + size: length, + body: binary.join(parts), + offset: start + }; + if (ref) item.ref = ref; + parts.length = 0; + start = 0; + offset = 0; + type = 0; + length = 0; + ref = null; + emit(item); + } + + // Feed the deflated code to the inflate engine + function $body(byte, i, chunk) { + if (inf.write(byte)) return $body; + var buf = inf.flush(); + if (buf.length !== length) throw new Error("Length mismatch, expected " + length + " got " + buf.length); + inf.recycle(); + if (buf.length) { + parts.push(buf); + } + emitObject(); + // If this was all the objects, start calculating the sha1sum + if (--num) return $header; + sha1sum.update(binary.slice(chunk, 0, i + 1)); + return $checksum; + } + + // 20 byte checksum + function $checksum(byte) { + checksum += toHex(byte); + if (++offset < 20) return $checksum; + var actual = sha1sum.digest(); + if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum); + } + +} diff --git a/lib/parseascii.js b/lib/parseascii.js new file mode 100644 index 0000000..78f5eb5 --- /dev/null +++ b/lib/parseascii.js @@ -0,0 +1,7 @@ +module.exports = function parseAscii(buffer, start, end) { + var val = ""; + while (start < end) { + val += String.fromCharCode(buffer[start++]); + } + return val; +}; diff --git a/lib/parsedec.js b/lib/parsedec.js new file mode 100644 index 0000000..e87151d --- /dev/null +++ b/lib/parsedec.js @@ -0,0 +1,7 @@ +module.exports = function parseDec(buffer, start, end) { + var val = 0; + while (start < end) { + val = val * 10 + buffer[start++] - 0x30; + } + return val; +}; diff --git a/mixins/packops.js b/mixins/packops.js new file mode 100644 index 0000000..3d1d9ce --- /dev/null +++ b/mixins/packops.js @@ -0,0 +1,194 @@ +var binary = require('bodec'); +var deframe = require('../lib/deframe.js'); +var frame = require('../lib/frame.js'); +var sha1 = require('git-sha1'); +var applyDelta = require('../lib/apply-delta.js'); +var pushToPull = require('push-to-pull'); +var decodePack = require('../lib/pack-codec.js').decodePack; +var packFrame = require('../lib/pack-codec.js').packFrame; + +module.exports = function (repo) { + // packStream is a simple-stream containing raw packfile binary data + // opts can contain "onProgress" or "onError" hook functions. + // callback will be called with a list of all unpacked hashes on success. + repo.unpack = unpack; // (packStream, opts) -> hashes + + // hashes is an array of hashes to pack + // callback will be a simple-stream containing raw packfile binary data + repo.pack = pack; // (hashes, opts) -> packStream +}; + +function unpack(packStream, opts, callback) { + if (!callback) return unpack.bind(this, packStream, opts); + + packStream = pushToPull(decodePack)(packStream); + + var repo = this; + + var version, num, numDeltas = 0, count = 0, countDeltas = 0; + var done, startDeltaProgress = false; + + // hashes keyed by offset for ofs-delta resolving + var hashes = {}; + // key is hash, boolean is cached "has" value of true or false + var has = {}; + // key is hash we're waiting for, value is array of items that are waiting. + var pending = {}; + + return packStream.read(onStats); + + function onDone(err) { + if (done) return; + done = true; + if (err) return callback(err); + return callback(null, values(hashes)); + } + + function onStats(err, stats) { + if (err) return onDone(err); + version = stats.version; + num = stats.num; + packStream.read(onRead); + } + + function objectProgress(more) { + if (!more) startDeltaProgress = true; + var percent = Math.round(count / num * 100); + return opts.onProgress("Receiving objects: " + percent + "% (" + (count++) + "/" + num + ") " + (more ? "\r" : "\n")); + } + + function deltaProgress(more) { + if (!startDeltaProgress) return; + var percent = Math.round(countDeltas / numDeltas * 100); + return opts.onProgress("Applying deltas: " + percent + "% (" + (countDeltas++) + "/" + numDeltas + ") " + (more ? "\r" : "\n")); + } + + function onRead(err, item) { + if (err) return onDone(err); + if (opts.onProgress) objectProgress(item); + if (item === undefined) return onDone(); + if (item.size !== item.body.length) { + return onDone(new Error("Body size mismatch")); + } + if (item.type === "ofs-delta") { + numDeltas++; + item.ref = hashes[item.offset - item.ref]; + return resolveDelta(item); + } + if (item.type === "ref-delta") { + numDeltas++; + return checkDelta(item); + } + return saveValue(item); + } + + function resolveDelta(item) { + if (opts.onProgress) deltaProgress(); + return repo.loadRaw(item.ref, function (err, buffer) { + if (err) return onDone(err); + if (!buffer) return onDone(new Error("Missing base image at " + item.ref)); + var target = deframe(buffer); + item.type = target[0]; + item.body = applyDelta(item.body, target[1]); + return saveValue(item); + }); + } + + function checkDelta(item) { + var hasTarget = has[item.ref]; + if (hasTarget === true) return resolveDelta(item); + if (hasTarget === false) return enqueueDelta(item); + return repo.has(item.ref, function (err, value) { + if (err) return onDone(err); + has[item.ref] = value; + if (value) return resolveDelta(item); + return enqueueDelta(item); + }); + } + + function saveValue(item) { + var buffer = frame(item.type, item.body); + var hash = sha1(buffer); + hashes[item.offset] = hash; + has[hash] = true; + if (hash in pending) { + // I have yet to come across a pack stream that actually needs this. + // So I will only implement it when I have concrete data to test against. + console.error({ + list: pending[hash], + item: item + }); + throw "TODO: pending value was found, resolve it"; + } + return repo.saveRaw(hash, buffer, onSave); + } + + function onSave(err) { + if (err) return callback(err); + packStream.read(onRead); + } + + function enqueueDelta(item) { + var list = pending[item.ref]; + if (!list) pending[item.ref] = [item]; + else list.push(item); + packStream.read(onRead); + } + +} + +// TODO: Implement delta refs to reduce stream size +function pack(hashes, opts, callback) { + if (!callback) return pack.bind(this, hashes, opts); + var repo = this; + var sha1sum = sha1(); + var i = 0, first = true, done = false; + return callback(null, { read: read, abort: callback }); + + function read(callback) { + if (done) return callback(); + if (first) return readFirst(callback); + var hash = hashes[i++]; + if (hash === undefined) { + var sum = sha1sum.digest(); + done = true; + return callback(null, binary.fromHex(sum)); + } + repo.loadRaw(hash, function (err, buffer) { + if (err) return callback(err); + if (!buffer) return callback(new Error("Missing hash: " + hash)); + // Reframe with pack format header + var pair = deframe(buffer); + packFrame(pair[0], pair[1], function (err, buffer) { + if (err) return callback(err); + sha1sum.update(buffer); + callback(null, buffer); + }); + }); + } + + function readFirst(callback) { + var length = hashes.length; + var chunk = binary.fromArray([ + 0x50, 0x41, 0x43, 0x4b, // PACK + 0, 0, 0, 2, // version 2 + length >> 24, // Num of objects + (length >> 16) & 0xff, + (length >> 8) & 0xff, + length & 0xff + ]); + first = false; + sha1sum.update(chunk); + callback(null, chunk); + } +} + +function values(object) { + var keys = Object.keys(object); + var length = keys.length; + var out = new Array(length); + for (var i = 0; i < length; i++) { + out[i] = object[keys[i]]; + } + return out; +} From 7d234279bf21c36a5ef33b3511edfe8f67988f54 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 5 Mar 2014 05:20:43 +0000 Subject: [PATCH 119/256] Get unpack working --- lib/apply-delta.js | 35 ++++++++++++-- lib/decoders.js | 104 ++++++++++++++++++++++++++++++++++++++++++ lib/inflate-stream.js | 65 ++++++++++++++++++++++---- lib/pack-codec.js | 2 +- lib/parseoct.js | 7 +++ lib/parsetohex.js | 10 ++++ lib/xhr-node.js | 18 +++----- mixins/mem-db.js | 69 +++++++++++++++++++--------- mixins/packops.js | 1 + 9 files changed, 264 insertions(+), 47 deletions(-) create mode 100644 lib/decoders.js create mode 100644 lib/parseoct.js create mode 100644 lib/parsetohex.js diff --git a/lib/apply-delta.js b/lib/apply-delta.js index e58d54b..3d9738e 100644 --- a/lib/apply-delta.js +++ b/lib/apply-delta.js @@ -1,8 +1,7 @@ // This is Chris Dickinson's code var binary = require('bodec') - // , Decoder = require('varint/decode.js') - // , vi = new Decoder + , vi = new Decoder // we use writeUint[8|32][LE|BE] instead of indexing // into buffers so that we get buffer-browserify compat. @@ -11,7 +10,6 @@ var OFFSET_BUFFER = binary.create(4) module.exports = apply_delta; function apply_delta(delta, target) { - throw "TODO: fix me" var base_size_info = {size: null, buffer: null} , resized_size_info = {size: null, buffer: null} , output_buffer @@ -107,3 +105,34 @@ function delta_header(buf, output) { output.buffer = binary.slice(buf, idx) } + +var MSB = 0x80 + , REST = 0x7F + +function Decoder() { + this.accum = [] +} +Decoder.prototype.write = write; + +function write(byte) { + var msb = byte & MSB + , accum = this.accum + , len + , out + + accum[accum.length] = byte & REST + if(msb) { + return + } + + len = accum.length + out = 0 + + for(var i = 0; i < len; ++i) { + out |= accum[i] << (7 * i) + } + + accum.length = 0 + this.ondata(out) + return +} \ No newline at end of file diff --git a/lib/decoders.js b/lib/decoders.js new file mode 100644 index 0000000..1ea962d --- /dev/null +++ b/lib/decoders.js @@ -0,0 +1,104 @@ +var indexOf = require('./indexof.js'); +var parseOct = require('./parseoct.js'); +var parseAscii = require('./parseascii.js'); +var parseToHex = require('./parsetohex.js'); + +exports.commit = function decodeCommit(body) { + var i = 0; + var start; + var key; + var parents = []; + var commit = { + tree: "", + parents: parents, + author: "", + committer: "", + message: "" + }; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = parseAscii(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = parseAscii(body, start, i++); + if (key === "parent") { + parents.push(value); + } + else { + if (key === "author" || key === "committer") { + value = decodePerson(value); + } + commit[key] = value; + } + } + i++; + commit.message = parseAscii(body, i, body.length); + return commit; +}; + +exports.tag = function decodeTag(body) { + var i = 0; + var start; + var key; + var tag = {}; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = parseAscii(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = parseAscii(body, start, i++); + if (key === "tagger") value = decodePerson(value); + tag[key] = value; + } + i++; + tag.message = parseAscii(body, i, body.length); + return tag; +}; + +exports.tree = function decodeTree(body) { + var i = 0; + var length = body.length; + var start; + var mode; + var name; + var hash; + var tree = {}; + while (i < length) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + mode = parseOct(body, start, i++); + start = i; + i = indexOf(body, 0x00, start); + name = parseAscii(body, start, i++); + hash = parseToHex(body, i, i += 20); + tree[name] = { + mode: mode, + hash: hash + }; + } + return tree; +}; + +exports.blob = function decodeBlob(body) { + return body; +}; + +function decodePerson(string) { + var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); + if (!match) throw new Error("Improperly formatted person string"); + var sec = parseInt(match[3], 10); + var date = new Date(sec * 1000); + date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; + return { + name: match[1], + email: match[2], + date: date + }; +} diff --git a/lib/inflate-stream.js b/lib/inflate-stream.js index 56c31a2..8f03590 100644 --- a/lib/inflate-stream.js +++ b/lib/inflate-stream.js @@ -1,11 +1,54 @@ -// Taken from chrisdickinson/inflate's min.js -// Modified to use bodec and inlined in js-git -// Original code under MIT License -// Copyright Chris Diskinson +var binary = require('bodec'); -module.exports = inflate +// Wrapper for proposed new API to inflate: +// +// var inf = inflate(); +// inf.write(byte) -> more - Write a byte to inflate's state-machine. +// Returns true if more data is expected. +// inf.recycle() - Reset the internal state machine. +// inf.flush() -> data - Flush the output as a binary buffer. +// +// This is quite slow, but could be made fast if baked into inflate itself. +module.exports = function () { + var push = inflate(onEmit, onUnused); + var more = true; + var chunks = []; + var b = binary.create(1); -var binary = require('bodec') + return { write: write, recycle: recycle, flush: flush }; + + function write(byte) { + b[0] = byte; + push(null, b); + return more; + } + + function recycle() { + push.recycle(); + more = true; + } + + function flush() { + var buffer = binary.join(chunks); + chunks.length = 0; + return buffer; + } + + function onEmit(err, item) { + if (err) throw err; + if (item === undefined) { + // console.log("onEnd"); + more = false; + return; + } + chunks.push(item); + } + + function onUnused(chunks) { + // console.log("onUnused", chunks); + more = false; + } +}; var MAXBITS = 15 , MAXLCODES = 286 @@ -86,7 +129,7 @@ function inflate(emit, on_unused) { output_one_offs = 0 become(noop, {}, noop) start_stream_header() - return stream + // return stream } var bytes_need = 0 @@ -596,9 +639,11 @@ function inflate(emit, on_unused) { output_idx = 0 ended = true emit() - return + // return + } + else { + last = result } - last = result } function bits() { @@ -720,7 +765,7 @@ function inflate(emit, on_unused) { output_idx &= WINDOW_MINUS_ONE } - emit(binary.fromArray(vals)) + emit(null, binary.fromArray(vals)) } } diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 45d936c..f01b5dc 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -1,4 +1,4 @@ -var inflate = require('./inflate.js'); +var inflate = require('./inflate-stream.js'); var deflate = require('./deflate.js'); var sha1 = require('git-sha1'); var binary = require('bodec'); diff --git a/lib/parseoct.js b/lib/parseoct.js new file mode 100644 index 0000000..d67d8d9 --- /dev/null +++ b/lib/parseoct.js @@ -0,0 +1,7 @@ +module.exports = function parseOct(buffer, start, end) { + var val = 0; + while (start < end) { + val = (val << 3) + buffer[start++] - 0x30; + } + return val; +}; diff --git a/lib/parsetohex.js b/lib/parsetohex.js new file mode 100644 index 0000000..a2a02af --- /dev/null +++ b/lib/parsetohex.js @@ -0,0 +1,10 @@ +var chars = "0123456789abcdef"; + +module.exports = function parseToHex(buffer, start, end) { + var val = ""; + while (start < end) { + var byte = buffer[start++]; + val += chars[byte >> 4] + chars[byte & 0xf]; + } + return val; +}; diff --git a/lib/xhr-node.js b/lib/xhr-node.js index 5ecd157..f7e2525 100644 --- a/lib/xhr-node.js +++ b/lib/xhr-node.js @@ -44,8 +44,7 @@ module.exports = function (root, accessToken) { body = Buffer.concat(body).toString(); console.log(method, url, res.statusCode); console.log("Rate limit %s/%s left", res.headers['x-ratelimit-remaining'], res.headers['x-ratelimit-limit']); - if (res.statusCode >= 400 && res.statusCode < 500) return callback(); - else if (res.statusCode === 200 && method === "GET" && /\/refs\//.test(url)) { + if (res.statusCode === 200 && method === "GET" && /\/refs\//.test(url)) { cache[url] = { body: body, etag: res.headers.etag @@ -55,21 +54,16 @@ module.exports = function (root, accessToken) { body = cache[url].body; res.statusCode = 200; } - else if (res.statusCode < 200 || res.statusCode >= 300) { - return callback(new Error("Invalid HTTP response: " + res.statusCode)); - } - - var response = {message:body}; - if (body){ - try { response = JSON.parse(body); } - catch (err) {} - } - // Fake parts of the xhr object using node APIs var xhr = { status: res.statusCode, statusText: res.statusCode + " " + statusCodes[res.statusCode] }; + var response = {message:body}; + if (body){ + try { response = JSON.parse(body); } + catch (err) {} + } return callback(null, xhr, response); }); }); diff --git a/mixins/mem-db.js b/mixins/mem-db.js index 49b98d0..ef3157f 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -1,39 +1,66 @@ "use strict"; +var binary = require('bodec'); var defer = require('../lib/defer.js'); var encoders = require('../lib/encoders.js'); +var deframe = require('../lib/deframe.js'); +var decoders = require('../lib/decoders.js'); +var sha1 = require('git-sha1'); module.exports = mixin; function mixin(repo) { - var objects = repo.objects = {}; - var types = {}; + var objects = {}; repo.saveAs = saveAs; repo.loadAs = loadAs; + repo.saveRaw = saveRaw; + repo.loadRaw = loadRaw; function saveAs(type, body, callback, hashOverride) { - defer(function () { - var hash; - try { - body = encoders.normalizeAs(type, body); - hash = hashOverride || encoders.hashAs(type, body); - } - catch (err) { return callback(err); } - objects[hash] = body; - types[hash] = type; - callback(null, hash, body); - }); + makeAsync(function () { + body = encoders.normalizeAs(type, body); + var buffer = encoders.encodeAs(type, body); + buffer = binary.join([ + encoders.frame(type, buffer.length), + buffer + ]); + var hash = hashOverride || sha1(buffer); + objects[hash] = buffer; + return [hash, body]; + }, callback); + } + + function saveRaw(hash, buffer, callback) { + makeAsync(function () { + objects[hash] = buffer; + return []; + }, callback); } function loadAs(type, hash, callback) { - defer(function () { - var realType = (type === "text" || type === "raw") ? "blob" : type; - if (!types[hash]) return callback(); - if (realType !== types[hash]) return callback(new TypeError("Type mismatch")); - var result = objects[hash]; - if (type !== "blob") result = encoders.normalizeAs(type, result); - callback(null, result, hash); - }); + makeAsync(function () { + var buffer = objects[hash]; + if (!buffer) return []; + var parts = deframe(buffer); + if (parts[0] !== type) throw new TypeError("Type mismatch"); + var body = decoders[type](parts[1]); + return [body, hash]; + }, callback); + } + + function loadRaw(hash, callback) { + makeAsync(function () { + return [objects[hash]]; + }, callback); } } + +function makeAsync(fn, callback) { + defer(function () { + var out; + try { out = fn(); } + catch (err) { return callback(err); } + callback.call(null, null, out[0], out[1]); + }); +} diff --git a/mixins/packops.js b/mixins/packops.js index 3d1d9ce..b9ee437 100644 --- a/mixins/packops.js +++ b/mixins/packops.js @@ -16,6 +16,7 @@ module.exports = function (repo) { // hashes is an array of hashes to pack // callback will be a simple-stream containing raw packfile binary data repo.pack = pack; // (hashes, opts) -> packStream + }; function unpack(packStream, opts, callback) { From ed2a24005b39c46895a90e23e8e70a8a7644b71b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 5 Mar 2014 22:48:44 +0000 Subject: [PATCH 120/256] Start documenting and testing js-git public interfaces. --- doc/lib/deflate.md | 0 doc/lib/inflate-stream.md | 0 doc/lib/inflate.md | 0 doc/lib/object-codec.md | 0 doc/mixins/mem-db.md | 27 ++++ lib/decoders.js | 104 ------------ lib/deframe.js | 18 --- lib/encoders.js | 271 -------------------------------- lib/frame.js | 8 - lib/indexof.js | 8 - lib/inflate-stream.js | 50 +----- lib/object-codec.js | 322 ++++++++++++++++++++++++++++++++++++++ lib/pack-codec.js | 52 +++++- lib/parseascii.js | 7 - lib/parsedec.js | 7 - lib/parseoct.js | 7 - lib/parsetohex.js | 10 -- lib/walk.js | 42 ----- mixins/mem-db.js | 23 +-- mixins/walkers.js | 45 +++++- test/run.js | 48 ++++++ test/test-mem-db.js | 58 +++++++ test/test-object-codec.js | 166 ++++++++++++++++++++ test/test-zlib.js | 53 +++++++ 24 files changed, 777 insertions(+), 549 deletions(-) create mode 100644 doc/lib/deflate.md create mode 100644 doc/lib/inflate-stream.md create mode 100644 doc/lib/inflate.md create mode 100644 doc/lib/object-codec.md create mode 100644 doc/mixins/mem-db.md delete mode 100644 lib/decoders.js delete mode 100644 lib/deframe.js delete mode 100644 lib/encoders.js delete mode 100644 lib/frame.js delete mode 100644 lib/indexof.js create mode 100644 lib/object-codec.js delete mode 100644 lib/parseascii.js delete mode 100644 lib/parsedec.js delete mode 100644 lib/parseoct.js delete mode 100644 lib/parsetohex.js delete mode 100644 lib/walk.js create mode 100644 test/run.js create mode 100644 test/test-mem-db.js create mode 100644 test/test-object-codec.js create mode 100644 test/test-zlib.js diff --git a/doc/lib/deflate.md b/doc/lib/deflate.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/lib/inflate-stream.md b/doc/lib/inflate-stream.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/lib/inflate.md b/doc/lib/inflate.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/lib/object-codec.md b/doc/lib/object-codec.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/mixins/mem-db.md b/doc/mixins/mem-db.md new file mode 100644 index 0000000..6b41aba --- /dev/null +++ b/doc/mixins/mem-db.md @@ -0,0 +1,27 @@ +# mem-db mixin + +This mixin implements object store (normal and raw) and stores the data in memory. + +```js +var memDb = require('js-git/mixins/mem-db'); +var repo = {}; +memDb(repo); +repo.saveAs("blob", "Hello World", function (err, hash) { + if (err) throw err; + console.log("Blob saved with hash " + hash); +}); +``` + +This attaches the following interfaces onto the repo object passed in: + + - `saveAs(type, body) => hash` + - `loadAs(type, hash) => body` + - `loadRaw(hash) => raw-binary` + - `saveRaw(hash, raw-binary) =>` + +All these functions are async and accept either a callback last or return a continuable. + +```js +// Example using continuable interface from gen-run generator body. +var commit = yield repo.loadAs("commit", commitHash); +``` \ No newline at end of file diff --git a/lib/decoders.js b/lib/decoders.js deleted file mode 100644 index 1ea962d..0000000 --- a/lib/decoders.js +++ /dev/null @@ -1,104 +0,0 @@ -var indexOf = require('./indexof.js'); -var parseOct = require('./parseoct.js'); -var parseAscii = require('./parseascii.js'); -var parseToHex = require('./parsetohex.js'); - -exports.commit = function decodeCommit(body) { - var i = 0; - var start; - var key; - var parents = []; - var commit = { - tree: "", - parents: parents, - author: "", - committer: "", - message: "" - }; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = parseAscii(body, start, i++); - if (key === "parent") { - parents.push(value); - } - else { - if (key === "author" || key === "committer") { - value = decodePerson(value); - } - commit[key] = value; - } - } - i++; - commit.message = parseAscii(body, i, body.length); - return commit; -}; - -exports.tag = function decodeTag(body) { - var i = 0; - var start; - var key; - var tag = {}; - while (body[i] !== 0x0a) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - key = parseAscii(body, start, i++); - start = i; - i = indexOf(body, 0x0a, start); - if (i < 0) throw new SyntaxError("Missing linefeed"); - var value = parseAscii(body, start, i++); - if (key === "tagger") value = decodePerson(value); - tag[key] = value; - } - i++; - tag.message = parseAscii(body, i, body.length); - return tag; -}; - -exports.tree = function decodeTree(body) { - var i = 0; - var length = body.length; - var start; - var mode; - var name; - var hash; - var tree = {}; - while (i < length) { - start = i; - i = indexOf(body, 0x20, start); - if (i < 0) throw new SyntaxError("Missing space"); - mode = parseOct(body, start, i++); - start = i; - i = indexOf(body, 0x00, start); - name = parseAscii(body, start, i++); - hash = parseToHex(body, i, i += 20); - tree[name] = { - mode: mode, - hash: hash - }; - } - return tree; -}; - -exports.blob = function decodeBlob(body) { - return body; -}; - -function decodePerson(string) { - var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); - if (!match) throw new Error("Improperly formatted person string"); - var sec = parseInt(match[3], 10); - var date = new Date(sec * 1000); - date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; - return { - name: match[1], - email: match[2], - date: date - }; -} diff --git a/lib/deframe.js b/lib/deframe.js deleted file mode 100644 index f3284ae..0000000 --- a/lib/deframe.js +++ /dev/null @@ -1,18 +0,0 @@ -var binary = require('bodec'); -var indexOf = require('./indexof.js'); -var parseDec = require('./parsedec.js'); -var parseAscii = require('./parseascii.js'); - -module.exports = function deframe(buffer) { - var space = indexOf(buffer, 0x20); - if (space < 0) throw new Error("Invalid git object buffer"); - var nil = indexOf(buffer, 0x00, space); - if (nil < 0) throw new Error("Invalid git object buffer"); - var body = binary.slice(buffer, nil + 1); - var size = parseDec(buffer, space + 1, nil); - if (size !== body.length) throw new Error("Invalid body length."); - return [ - parseAscii(buffer, 0, space), - body - ]; -}; diff --git a/lib/encoders.js b/lib/encoders.js deleted file mode 100644 index ef700c4..0000000 --- a/lib/encoders.js +++ /dev/null @@ -1,271 +0,0 @@ -"use strict"; - -var sha1 = require('git-sha1'); -var binary = require('bodec'); -var modes = require('./modes.js'); - -// Run sanity tests at startup. -test(); - -module.exports = { - frame: frame, - normalizeAs: normalizeAs, - normalizeBlob: normalizeBlob, - normalizeTree: normalizeTree, - normalizeCommit: normalizeCommit, - normalizeTag: normalizeTag, - encodeAs: encodeAs, - encodeBlob: encodeBlob, - encodeTree: encodeTree, - encodeCommit: encodeCommit, - encodeTag: encodeTag, - hashAs: hashAs, - pathCmp: pathCmp -}; - -function test() { - // Test blob encoding - var normalized = normalizeBlob("Hello World\n"); - var expected = "557db03de997c86a4a028e1ebd3a1ceb225be238"; - var hash = hashAs("blob", normalized); - if (hash !== expected) { - console.log({expected: expected, actual: hash, normalized: normalized}); - throw new Error("Invalid body hash"); - } - - // Test tree encoding - hash = hashAs("tree", normalizeTree({ "greeting.txt": { mode: modes.file, hash: hash } })); - if (hash !== "648fc86e8557bdabbc2c828a19535f833727fa62") { - throw new Error("Invalid tree hash"); - } - - var date = new Date(1391790884000); - date.timezoneOffset = 7 * 60; - // Test commit encoding - hash = hashAs("commit", normalizeCommit({ - tree: hash, - author: { - name: "Tim Caswell", - email: "tim@creationix.com", - date: date - }, - message: "Test Commit\n" - })); - if (hash !== "500c37fc17988b90c82d812a2d6fc25b15354bf2") { - throw new Error("Invalid commit hash"); - } - - // Test annotated tag encoding - date = new Date(1391790910000); - date.timezoneOffset = 7 * 60; - hash = hashAs("tag", normalizeTag({ - object: hash, - type: "commit", - tag: "mytag", - tagger: { - name: "Tim Caswell", - email: "tim@creationix.com", - date: date, - }, - message: "Tag it!\n" - })); - if (hash !== "49522787662a0183652dc9cafa5c008b5a0e0c2a") { - throw new Error("Invalid annotated tag hash"); - } -} - -function encodeAs(type, body) { - if (type === "blob") return encodeBlob(body); - if (type === "tree") return encodeTree(body); - if (type === "commit") return encodeCommit(body); - if (type === "tag") return encodeTag(body); -} - -function normalizeAs(type, body) { - if (type === "blob") return normalizeBlob(body); - if (type === "tree") return normalizeTree(body); - if (type === "commit") return normalizeCommit(body); - if (type === "tag") return normalizeTag(body); -} - -// Calculate a git compatable hash by git encoding the body and prepending a -// git style frame header and calculating the sha1 sum of that. -function hashAs(type, body) { - var encoded = encodeAs(type, body); - var sum = sha1(); - sum.update(frame(type, encoded.length)); - sum.update(encoded); - return sum.digest(); -} - -function frame(type, length) { - return type + " " + length + "\0"; -} - -function normalizeBlob(body) { - var type = typeof body; - if (type === "string") { - return binary.fromRaw(body); - } - if (body && type === "object") { - if (body.constructor.name === "ArrayBuffer") body = new Uint8Array(body); - if (typeof body.length === "number") { - return body;//binary.toRaw(body); - } - } - throw new TypeError("Blob body must be raw string, ArrayBuffer or byte array"); -} - -function encodeBlob(body) { - return body; -} - -function normalizeTree(body) { - var type = body && typeof body; - if (type !== "object") { - throw new TypeError("Tree body must be array or object"); - } - var tree = {}, i, l, entry; - // If array form is passed in, convert to object form. - if (Array.isArray(body)) { - for (i = 0, l = body.length; i < l; i++) { - entry = body[i]; - tree[entry.name] = { - mode: entry.mode, - hash: entry.hash - }; - } - } - else { - var names = Object.keys(body); - for (i = 0, l = names.length; i < l; i++) { - var name = names[i]; - entry = body[name]; - tree[name] = { - mode: entry.mode, - hash: entry.hash - }; - } - } - return tree; -} - -function encodeTree(body) { - var tree = ""; - var names = Object.keys(body).sort(pathCmp); - for (var i = 0, l = names.length; i < l; i++) { - var name = names[i]; - var entry = body[name]; - tree += entry.mode.toString(8) + " " + name + - "\0" + binary.decodeHex(entry.hash); - } - return tree; -} - -function normalizeCommit(body) { - if (!body || typeof body !== "object") { - throw new TypeError("Commit body must be an object"); - } - if (!(body.tree && body.author && body.message)) { - throw new TypeError("Tree, author, and message are required for commits"); - } - var parents = body.parents || (body.parent ? [ body.parent ] : []); - if (!Array.isArray(parents)) { - throw new TypeError("Parents must be an array"); - } - var author = normalizePerson(body.author); - var committer = body.committer ? normalizePerson(body.committer) : author; - return { - tree: body.tree, - parents: parents, - author: author, - committer: committer, - message: body.message - }; -} - -function encodeCommit(body) { - var str = "tree " + body.tree; - for (var i = 0, l = body.parents.length; i < l; ++i) { - str += "\nparent " + body.parents[i]; - } - str += "\nauthor " + formatPerson(body.author) + - "\ncommitter " + formatPerson(body.committer) + - "\n\n" + body.message; - return binary.encodeUtf8(str); -} - -function normalizeTag(body) { - if (!body || typeof body !== "object") { - throw new TypeError("Tag body must be an object"); - } - if (!(body.object && body.type && body.tag && body.tagger && body.message)) { - throw new TypeError("Object, type, tag, tagger, and message required"); - } - return { - object: body.object, - type: body.type, - tag: body.tag, - tagger: normalizePerson(body.tagger), - message: body.message - }; -} - -function encodeTag(body) { - var str = "object " + body.object + - "\ntype " + body.type + - "\ntag " + body.tag + - "\ntagger " + formatPerson(body.tagger) + - "\n\n" + body.message; - return binary.encodeUtf8(str); -} - -// Tree entries are sorted by the byte sequence that comprises -// the entry name. However, for the purposes of the sort -// comparison, entries for tree objects are compared as if the -// entry name byte sequence has a trailing ASCII '/' (0x2f). -function pathCmp(a, b) { - // TODO: this spec seems to be wrong. It doesn't match the sort order used - // by real git. - // a = binary.encodeUtf8(a) + "/"; - // b = binary.encodeUtf8(b) + "/"; - return a < b ? -1 : a > b ? 1 : 0; -} - -function normalizePerson(person) { - if (!person || typeof person !== "object") { - throw new TypeError("Person must be an object"); - } - if (!person.name || !person.email) { - throw new TypeError("Name and email are required for person fields"); - } - return { - name: person.name, - email: person.email, - date: person.date || new Date() - }; -} - -function formatPerson(person) { - return safe(person.name) + - " <" + safe(person.email) + "> " + - formatDate(person.date); -} - -function safe(string) { - return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); -} - -function two(num) { - return (num < 10 ? "0" : "") + num; -} - -function formatDate(date) { - var offset = date.timezoneOffset || date.getTimezoneOffset(); - var neg = "+"; - if (offset <= 0) offset = -offset; - else neg = "-"; - offset = neg + two(Math.floor(offset / 60)) + two(offset % 60); - var seconds = Math.floor(date.getTime() / 1000); - return seconds + " " + offset; -} diff --git a/lib/frame.js b/lib/frame.js deleted file mode 100644 index 5c7fe2c..0000000 --- a/lib/frame.js +++ /dev/null @@ -1,8 +0,0 @@ -var binary = require('bodec'); - -module.exports = function frame(type, body) { - return binary.join([ - binary.fromRaw(type + " " + body.length + "\0"), - body - ]); -}; diff --git a/lib/indexof.js b/lib/indexof.js deleted file mode 100644 index 18c61a5..0000000 --- a/lib/indexof.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = function indexOf(buffer, byte, i) { - i |= 0; - var length = buffer.length; - for (;;i++) { - if (i >= length) return -1; - if (buffer[i] === byte) return i; - } -}; diff --git a/lib/inflate-stream.js b/lib/inflate-stream.js index 8f03590..c7eff68 100644 --- a/lib/inflate-stream.js +++ b/lib/inflate-stream.js @@ -1,54 +1,6 @@ var binary = require('bodec'); -// Wrapper for proposed new API to inflate: -// -// var inf = inflate(); -// inf.write(byte) -> more - Write a byte to inflate's state-machine. -// Returns true if more data is expected. -// inf.recycle() - Reset the internal state machine. -// inf.flush() -> data - Flush the output as a binary buffer. -// -// This is quite slow, but could be made fast if baked into inflate itself. -module.exports = function () { - var push = inflate(onEmit, onUnused); - var more = true; - var chunks = []; - var b = binary.create(1); - - return { write: write, recycle: recycle, flush: flush }; - - function write(byte) { - b[0] = byte; - push(null, b); - return more; - } - - function recycle() { - push.recycle(); - more = true; - } - - function flush() { - var buffer = binary.join(chunks); - chunks.length = 0; - return buffer; - } - - function onEmit(err, item) { - if (err) throw err; - if (item === undefined) { - // console.log("onEnd"); - more = false; - return; - } - chunks.push(item); - } - - function onUnused(chunks) { - // console.log("onUnused", chunks); - more = false; - } -}; +module.exports = inflate; var MAXBITS = 15 , MAXLCODES = 286 diff --git a/lib/object-codec.js b/lib/object-codec.js new file mode 100644 index 0000000..0351e6d --- /dev/null +++ b/lib/object-codec.js @@ -0,0 +1,322 @@ +var bodec = require('bodec'); + +// (body) -> raw-buffer +var encoders = exports.encoders = { + blob: encodeBlob, + tree: encodeTree, + commit: encodeCommit, + tag: encodeTag +}; + + // ({type:type, body:raw-buffer}) -> buffer +exports.frame = frame; + +// (raw-buffer) -> body +var decoders = exports.decoders ={ + blob: decodeBlob, + tree: decodeTree, + commit: decodeCommit, + tag: decodeTag +}; + +// (buffer) -> {type:type, body:raw-buffer} +exports.deframe = deframe; + +function encodeBlob(body) { + // Assume strings are normal unicode strings + if (typeof body === "string") return bodec.fromUnicode(body); + if (Array.isArray(body)) return bodec.fromArray(body); + if (bodec.isBinary) return body; + throw new Error("Invalid body for blob"); +} + +function encodeTree(body) { + body = normalizeTree(body); + var tree = ""; + var names = Object.keys(body).sort(); + for (var i = 0, l = names.length; i < l; i++) { + var name = names[i]; + var entry = body[name]; + tree += entry.mode.toString(8) + " " + bodec.encodeUtf8(name) + + "\0" + bodec.decodeHex(entry.hash); + } + return bodec.fromRaw(tree); +} + +function encodeTag(body) { + body = normalizeTag(body); + var str = "object " + body.object + + "\ntype " + body.type + + "\ntag " + body.tag + + "\ntagger " + formatPerson(body.tagger) + + "\n\n" + body.message; + return bodec.fromUnicode(str); +} + +function encodeCommit(body) { + body = normalizeCommit(body); + var str = "tree " + body.tree; + for (var i = 0, l = body.parents.length; i < l; ++i) { + str += "\nparent " + body.parents[i]; + } + str += "\nauthor " + formatPerson(body.author) + + "\ncommitter " + formatPerson(body.committer) + + "\n\n" + body.message; + return bodec.fromUnicode(str); +} + +function normalizeTree(body) { + var type = body && typeof body; + if (type !== "object") { + throw new TypeError("Tree body must be array or object"); + } + var tree = {}, i, l, entry; + // If array form is passed in, convert to object form. + if (Array.isArray(body)) { + for (i = 0, l = body.length; i < l; i++) { + entry = body[i]; + tree[entry.name] = { + mode: entry.mode, + hash: entry.hash + }; + } + } + else { + var names = Object.keys(body); + for (i = 0, l = names.length; i < l; i++) { + var name = names[i]; + entry = body[name]; + tree[name] = { + mode: entry.mode, + hash: entry.hash + }; + } + } + return tree; +} + +function normalizeCommit(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Commit body must be an object"); + } + if (!(body.tree && body.author && body.message)) { + throw new TypeError("Tree, author, and message are required for commits"); + } + var parents = body.parents || (body.parent ? [ body.parent ] : []); + if (!Array.isArray(parents)) { + throw new TypeError("Parents must be an array"); + } + var author = normalizePerson(body.author); + var committer = body.committer ? normalizePerson(body.committer) : author; + return { + tree: body.tree, + parents: parents, + author: author, + committer: committer, + message: body.message + }; +} + +function normalizeTag(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Tag body must be an object"); + } + if (!(body.object && body.type && body.tag && body.tagger && body.message)) { + throw new TypeError("Object, type, tag, tagger, and message required"); + } + return { + object: body.object, + type: body.type, + tag: body.tag, + tagger: normalizePerson(body.tagger), + message: body.message + }; +} + +function normalizePerson(person) { + if (!person || typeof person !== "object") { + throw new TypeError("Person must be an object"); + } + if (!person.name || !person.email) { + throw new TypeError("Name and email are required for person fields"); + } + return { + name: person.name, + email: person.email, + date: person.date || new Date() + }; +} + +function formatPerson(person) { + return safe(person.name) + + " <" + safe(person.email) + "> " + + formatDate(person.date); +} + +function safe(string) { + return string.replace(/(?:^[\.,:;<>"']+|[\0\n<>]+|[\.,:;<>"']+$)/gm, ""); +} + +function two(num) { + return (num < 10 ? "0" : "") + num; +} + +function formatDate(date) { + var offset = date.timezoneOffset || date.getTimezoneOffset(); + var neg = "+"; + if (offset <= 0) offset = -offset; + else neg = "-"; + offset = neg + two(Math.floor(offset / 60)) + two(offset % 60); + var seconds = Math.floor(date.getTime() / 1000); + return seconds + " " + offset; +} + +function frame(obj) { + var type = obj.type; + var body = obj.body; + if (!bodec.isBinary(body)) body = encoders[type](body); + return bodec.join([ + bodec.fromRaw(type + " " + body.length + "\0"), + body + ]); +} + +function decodeBlob(body) { + return body; +} + +function decodeTree(body) { + var i = 0; + var length = body.length; + var start; + var mode; + var name; + var hash; + var tree = {}; + while (i < length) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + mode = parseOct(body, start, i++); + start = i; + i = indexOf(body, 0x00, start); + name = bodec.toUnicode(body, start, i++); + hash = bodec.toHex(body, i, i += 20); + tree[name] = { + mode: mode, + hash: hash + }; + } + return tree; +} + +function decodeCommit(body) { + var i = 0; + var start; + var key; + var parents = []; + var commit = { + tree: "", + parents: parents, + author: "", + committer: "", + message: "" + }; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = bodec.toRaw(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = bodec.toUnicode(body, start, i++); + if (key === "parent") { + parents.push(value); + } + else { + if (key === "author" || key === "committer") { + value = decodePerson(value); + } + commit[key] = value; + } + } + i++; + commit.message = bodec.toUnicode(body, i, body.length); + return commit; +} + +function decodeTag(body) { + var i = 0; + var start; + var key; + var tag = {}; + while (body[i] !== 0x0a) { + start = i; + i = indexOf(body, 0x20, start); + if (i < 0) throw new SyntaxError("Missing space"); + key = bodec.toRaw(body, start, i++); + start = i; + i = indexOf(body, 0x0a, start); + if (i < 0) throw new SyntaxError("Missing linefeed"); + var value = bodec.toUnicode(body, start, i++); + if (key === "tagger") value = decodePerson(value); + tag[key] = value; + } + i++; + tag.message = bodec.toUnicode(body, i, body.length); + return tag; +} + +function decodePerson(string) { + var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); + if (!match) throw new Error("Improperly formatted person string"); + var sec = parseInt(match[3], 10); + var date = new Date(sec * 1000); + date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; + return { + name: match[1], + email: match[2], + date: date + }; +} + +function deframe(buffer, decode) { + var space = indexOf(buffer, 0x20); + if (space < 0) throw new Error("Invalid git object buffer"); + var nil = indexOf(buffer, 0x00, space); + if (nil < 0) throw new Error("Invalid git object buffer"); + var body = bodec.slice(buffer, nil + 1); + var size = parseDec(buffer, space + 1, nil); + if (size !== body.length) throw new Error("Invalid body length."); + var type = bodec.toRaw(buffer, 0, space); + return { + type: type, + body: decode ? decoders[type](body) : body + }; +} + +function indexOf(buffer, byte, i) { + i |= 0; + var length = buffer.length; + for (;;i++) { + if (i >= length) return -1; + if (buffer[i] === byte) return i; + } +} + +function parseOct(buffer, start, end) { + var val = 0; + while (start < end) { + val = (val << 3) + buffer[start++] - 0x30; + } + return val; +} + +function parseDec(buffer, start, end) { + var val = 0; + while (start < end) { + val = val * 10 + buffer[start++] - 0x30; + } + return val; +} diff --git a/lib/pack-codec.js b/lib/pack-codec.js index f01b5dc..0ea12b7 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -1,4 +1,4 @@ -var inflate = require('./inflate-stream.js'); +var inflateStream = require('./inflate-stream.js'); var deflate = require('./deflate.js'); var sha1 = require('git-sha1'); var binary = require('bodec'); @@ -210,3 +210,53 @@ function decodePack(emit) { } } + +// Wrapper for proposed new API to inflate: +// +// var inf = inflate(); +// inf.write(byte) -> more - Write a byte to inflate's state-machine. +// Returns true if more data is expected. +// inf.recycle() - Reset the internal state machine. +// inf.flush() -> data - Flush the output as a binary buffer. +// +// This is quite slow, but could be made fast if baked into inflate itself. +function inflate() { + var push = inflateStream(onEmit, onUnused); + var more = true; + var chunks = []; + var b = binary.create(1); + + return { write: write, recycle: recycle, flush: flush }; + + function write(byte) { + b[0] = byte; + push(null, b); + return more; + } + + function recycle() { + push.recycle(); + more = true; + } + + function flush() { + var buffer = binary.join(chunks); + chunks.length = 0; + return buffer; + } + + function onEmit(err, item) { + if (err) throw err; + if (item === undefined) { + // console.log("onEnd"); + more = false; + return; + } + chunks.push(item); + } + + function onUnused(chunks) { + // console.log("onUnused", chunks); + more = false; + } +} diff --git a/lib/parseascii.js b/lib/parseascii.js deleted file mode 100644 index 78f5eb5..0000000 --- a/lib/parseascii.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function parseAscii(buffer, start, end) { - var val = ""; - while (start < end) { - val += String.fromCharCode(buffer[start++]); - } - return val; -}; diff --git a/lib/parsedec.js b/lib/parsedec.js deleted file mode 100644 index e87151d..0000000 --- a/lib/parsedec.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function parseDec(buffer, start, end) { - var val = 0; - while (start < end) { - val = val * 10 + buffer[start++] - 0x30; - } - return val; -}; diff --git a/lib/parseoct.js b/lib/parseoct.js deleted file mode 100644 index d67d8d9..0000000 --- a/lib/parseoct.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function parseOct(buffer, start, end) { - var val = 0; - while (start < end) { - val = (val << 3) + buffer[start++] - 0x30; - } - return val; -}; diff --git a/lib/parsetohex.js b/lib/parsetohex.js deleted file mode 100644 index a2a02af..0000000 --- a/lib/parsetohex.js +++ /dev/null @@ -1,10 +0,0 @@ -var chars = "0123456789abcdef"; - -module.exports = function parseToHex(buffer, start, end) { - var val = ""; - while (start < end) { - var byte = buffer[start++]; - val += chars[byte >> 4] + chars[byte & 0xf]; - } - return val; -}; diff --git a/lib/walk.js b/lib/walk.js deleted file mode 100644 index 166e0fc..0000000 --- a/lib/walk.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = function walk(seed, scan, loadKey, compare) { - var queue = [seed]; - var working = 0, error, cb; - return {read: read, abort: abort}; - - function read(callback) { - if (cb) return callback(new Error("Only one read at a time")); - if (working) { cb = callback; return; } - var item = queue.shift(); - if (!item) return callback(); - try { scan(item).forEach(onKey); } - catch (err) { return callback(err); } - return callback(null, item); - } - - function abort(callback) { return callback(); } - - function onError(err) { - if (cb) { - var callback = cb; cb = null; - return callback(err); - } - error = err; - } - - function onKey(key) { - working++; - loadKey(key, onItem); - } - - function onItem(err, item) { - working--; - if (err) return onError(err); - var index = queue.length; - while (index && compare(item, queue[index - 1])) index--; - queue.splice(index, 0, item); - if (!working && cb) { - var callback = cb; cb = null; - return read(callback); - } - } -}; diff --git a/mixins/mem-db.js b/mixins/mem-db.js index ef3157f..9b516f8 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -1,10 +1,7 @@ "use strict"; -var binary = require('bodec'); var defer = require('../lib/defer.js'); -var encoders = require('../lib/encoders.js'); -var deframe = require('../lib/deframe.js'); -var decoders = require('../lib/decoders.js'); +var codec = require('../lib/object-codec.js'); var sha1 = require('git-sha1'); module.exports = mixin; @@ -17,15 +14,10 @@ function mixin(repo) { repo.saveRaw = saveRaw; repo.loadRaw = loadRaw; - function saveAs(type, body, callback, hashOverride) { + function saveAs(type, body, callback) { makeAsync(function () { - body = encoders.normalizeAs(type, body); - var buffer = encoders.encodeAs(type, body); - buffer = binary.join([ - encoders.frame(type, buffer.length), - buffer - ]); - var hash = hashOverride || sha1(buffer); + var buffer = codec.frame({type:type,body:body}); + var hash = sha1(buffer); objects[hash] = buffer; return [hash, body]; }, callback); @@ -42,10 +34,9 @@ function mixin(repo) { makeAsync(function () { var buffer = objects[hash]; if (!buffer) return []; - var parts = deframe(buffer); - if (parts[0] !== type) throw new TypeError("Type mismatch"); - var body = decoders[type](parts[1]); - return [body, hash]; + var obj = codec.deframe(buffer, true); + if (obj.type !== type) throw new TypeError("Type mismatch"); + return [obj.body, hash]; }, callback); } diff --git a/mixins/walkers.js b/mixins/walkers.js index 791d3df..d95cdbd 100644 --- a/mixins/walkers.js +++ b/mixins/walkers.js @@ -1,10 +1,10 @@ -var walk = require('../lib/walk.js'); var modes = require('../lib/modes.js'); module.exports = function (repo) { repo.logWalk = logWalk; // (hash-ish) => stream repo.treeWalk = treeWalk; // (treeHash) => stream }; +module.exports.walk = walk; function logWalk(hashish, callback) { if (!callback) return logWalk.bind(this, hashish); @@ -108,3 +108,46 @@ function resolveHashish(repo, hashish, callback) { callback(null, hash); }); } + +function walk(seed, scan, loadKey, compare) { + var queue = [seed]; + var working = 0, error, cb; + return {read: read, abort: abort}; + + function read(callback) { + if (cb) return callback(new Error("Only one read at a time")); + if (working) { cb = callback; return; } + var item = queue.shift(); + if (!item) return callback(); + try { scan(item).forEach(onKey); } + catch (err) { return callback(err); } + return callback(null, item); + } + + function abort(callback) { return callback(); } + + function onError(err) { + if (cb) { + var callback = cb; cb = null; + return callback(err); + } + error = err; + } + + function onKey(key) { + working++; + loadKey(key, onItem); + } + + function onItem(err, item) { + working--; + if (err) return onError(err); + var index = queue.length; + while (index && compare(item, queue[index - 1])) index--; + queue.splice(index, 0, item); + if (!working && cb) { + var callback = cb; cb = null; + return read(callback); + } + } +} diff --git a/test/run.js b/test/run.js new file mode 100644 index 0000000..e0655bf --- /dev/null +++ b/test/run.js @@ -0,0 +1,48 @@ +// Ultra simple test runner with TAP output. + +var inspect = require('util').inspect; +var defer = require('../lib/defer.js'); +var log = console.log; +console.log = function () { + var args = [].slice.call(arguments).map(function (arg) { + return inspect(arg, {colors:true}); + }); + log(args.join(" ").split("\n").map(function (line) { + return "# " + line; + }).join("\n")); +}; + +module.exports = function (tests) { + var timeout; + var test; + var index = 0; + log("1.." + (tests.length)); + next(); + function next(err) { + if (timeout) clearTimeout(timeout); + if (index) { + if (err) { + log(err.stack.split("\n").map(function (line) { + return "# " + line; + }).join("\n")); + log("not ok " + index + " - " + test.name); + } + else { + log("ok " + index + " - " + test.name); + } + } + test = tests[index++]; + if (!test) return; + timeout = setTimeout(onTimeout, 1000); + try { + if (test.length) test(next); + else test(); + } + catch (err) { return next(err); } + if (!test.length) defer(next); + } + + function onTimeout() { + next(new Error("Test timeout")); + } +}; \ No newline at end of file diff --git a/test/test-mem-db.js b/test/test-mem-db.js new file mode 100644 index 0000000..b57f233 --- /dev/null +++ b/test/test-mem-db.js @@ -0,0 +1,58 @@ +var run = require('./run.js'); +var bodec = require('bodec'); +var sha1 = require('git-sha1'); +var codec = require('../lib/object-codec.js'); + + +var repo = {}; +require('../mixins/mem-db.js')(repo); + +var blob = "Hello World\n"; +var blobHash = "557db03de997c86a4a028e1ebd3a1ceb225be238"; +run([ + function testSaveAs(end) { + repo.saveAs("blob", blob, function (err, hash) { + if (err) return end(err); + if (hash !== blobHash) { + console.log([hash, blobHash]); + return end(new Error("Hash mismatch")); + } + end(); + }); + }, + function testLoadRaw(end) { + repo.loadRaw(blobHash, function (err, bin) { + if (err) return end(err); + var obj = codec.deframe(bin, true); + if (obj.type !== "blob") return err(new Error("Wrong type")); + if (bodec.toUnicode(obj.body) !== blob) { + return err(new Error("Wrong body")); + } + end(); + }); + }, + function testLoadAs(end) { + repo.loadAs("blob", blobHash, function (err, body) { + if (err) return end(err); + if (bodec.toUnicode(body) !== blob) { + return err(new Error("Wrong body")); + } + end(); + }); + }, + function testSaveRaw(end) { + var newBody = "A new body\n"; + var bin = codec.frame({type:"blob",body:newBody}); + var hash = sha1(bin); + repo.saveRaw(hash, bin, function (err) { + if (err) return end(err); + repo.loadAs("blob", hash, function (err, body) { + if (err) return end(err); + if (bodec.toUnicode(body) !== newBody) { + return end(new Error("Body mismatch")); + } + end(); + }); + }); + } +]); diff --git a/test/test-object-codec.js b/test/test-object-codec.js new file mode 100644 index 0000000..c06fac7 --- /dev/null +++ b/test/test-object-codec.js @@ -0,0 +1,166 @@ +var modes = require('../lib/modes.js'); +var bodec = require('bodec'); +var sha1 = require('git-sha1'); +var run = require('./run.js'); + +// The thing we mean to test. +var codec = require('../lib/object-codec.js'); + +var blobHash, treeHash, commitHash, tagHash; +var blob, tree, commit, tag; +var blobBin, treeBin, commitBin, tagBin; + +run([ + function testEncodeBlob() { + blob = "Hello World\n"; + blobBin = codec.frame({type: "blob", body: blob}); + blobHash = sha1(blobBin); + if (blobHash !== '557db03de997c86a4a028e1ebd3a1ceb225be238') { + throw new Error("Invalid blob hash"); + } + }, + function testEncodeTree() { + tree = { + "greeting.txt": { + mode: modes.file, + hash: blobHash + } + }; + treeBin = codec.frame({type: "tree", body: tree}); + treeHash = sha1(treeBin); + if (treeHash !== "648fc86e8557bdabbc2c828a19535f833727fa62") { + throw new Error("Invalid tree hash"); + } + }, + function testEncodeCommit() { + var date = new Date(1391790884000); + date.timezoneOffset = 7 * 60; + commit = { + tree: treeHash, + author: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date + }, + message: "Test Commit\n" + }; + commitBin = codec.frame({type: "commit", body: commit}); + commitHash = sha1(commitBin); + if (commitHash !== "500c37fc17988b90c82d812a2d6fc25b15354bf2") { + throw new Error("Invalid commit hash"); + } + }, + function testEncodeTag() { + var date = new Date(1391790910000); + date.timezoneOffset = 7 * 60; + tag = { + object: commitHash, + type: "commit", + tag: "mytag", + tagger: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date, + }, + message: "Tag it!\n" + }; + tagBin = codec.frame({type: "tag", body: tag}); + tagHash = sha1(tagBin); + if (tagHash !== "49522787662a0183652dc9cafa5c008b5a0e0c2a") { + throw new Error("Invalid tag hash"); + } + }, + function testDecodeTag() { + var obj = codec.deframe(tagBin, true); + if (obj.type !== "tag") throw new Error("Invalid type"); + if (!(obj.body.object === tag.object && obj.body.message === tag.message)) { + throw new Error("Problem decoding"); + } + }, + function testDecodeCommit() { + var obj = codec.deframe(commitBin, true); + if (obj.type !== "commit") throw new Error("Invalid type"); + if (!(obj.body.tree === commit.tree && obj.body.message === commit.message)) { + throw new Error("Problem decoding"); + } + }, + function testDecodeTree() { + var obj = codec.deframe(treeBin, true); + if (obj.type !== "tree") throw new Error("Invalid type"); + if (obj.body["greeting.txt"].hash !== tree["greeting.txt"].hash) { + throw new Error("Problem decoding"); + } + }, + function testDecodeBlob() { + var obj = codec.deframe(blobBin, true); + if (obj.type !== "blob") throw new Error("Invalid type"); + if (bodec.toUnicode(obj.body) !== blob) { + throw new Error("Problem decoding"); + } + }, + function testUnicodeFilePath() { + var name = "æðelen"; + var tree = {}; + tree[name] = { + mode: modes.file, + hash: blobHash + }; + var bin = codec.frame({type:"tree", body: tree}); + var obj = codec.deframe(bin, true); + var newName = Object.keys(obj.body)[0]; + if (newName !== name) { + console.log(newName + " != " + name); + throw new Error("Problem storing and retrieving utf8 paths"); + } + if (obj.body[name].hash !== tree[name].hash) { + throw new Error("Problem decoding hash hex"); + } + }, + function testUnicodeCommit() { + var commit = { + tree: treeHash, + author: { + name: "Laȝamon", + email: "laȝamon@chronicles-of-england.org" + }, + message: "An preost wes on leoden, Laȝamon was ihoten\nHe wes Leovenaðes sone -- liðe him be Drihten\n" + }; + var bin = codec.frame({type:"commit", body:commit}); + var obj = codec.deframe(bin, true); + if (commit.author.name !== obj.body.author.name || + commit.author.email !== obj.body.author.email || + commit.message !== obj.body.message) { + console.log([obj.body.author, obj.body.message]); + throw new Error("Problem decoding utf8 parts in commit"); + } + }, + function testUnicodeTag() { + var tag = { + object: commitHash, + type: "commit", + tag: "Laȝamon", + tagger: { + name: "Laȝamon", + email: "laȝamon@chronicles-of-england.org" + }, + message: "He wonede at Ernleȝe at æðelen are chirechen,\nUppen Sevarne staþe, sel þar him þuhte,\nOnfest Radestone, þer he bock radde.\n" + }; + var bin = codec.frame({type:"tag", body:tag}); + var obj = codec.deframe(bin, true); + if (tag.tagger.name !== obj.body.tagger.name || + tag.tagger.email !== obj.body.tagger.email || + tag.message !== obj.body.message) { + console.log([obj.body.author, obj.body.message]); + throw new Error("Problem decoding utf8 parts in tag"); + } + }, + function testBinaryBlob() { + var blob = bodec.create(256); + for (var i = 0; i < 256; i++) { blob[i] = i; } + var bin = codec.frame({type:"blob",body:blob}); + var obj = codec.deframe(bin, true); + if (bodec.toRaw(blob) !== bodec.toRaw(obj.body)) { + throw new Error("Problem decoding binary blob"); + } + } +]); diff --git a/test/test-zlib.js b/test/test-zlib.js new file mode 100644 index 0000000..2833ffd --- /dev/null +++ b/test/test-zlib.js @@ -0,0 +1,53 @@ +var run = require('./run.js'); +var bodec = require('bodec'); + +// The thing we mean to test. +var inflate = require('../lib/inflate.js'); +var deflate = require('../lib/deflate.js'); +var inflateStream = require('../lib/inflate-stream.js'); + +var bin = bodec.create(1024); +for (var i = 0; i < 1024; i++) { + bin[i] = i >> 2 | i % 4 & 0x7f; +} + +run([ + function testRoundTrip(end) { + deflate(bin, function (err, deflated) { + if (err) return end(err); + inflate(deflated, function (err, inflated) { + if (err) return end(err); + if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { + console.log([bin, inflated]); + return end(new Error("Problem with roundtrip")); + } + end(); + }); + }); + }, + function testStream(end) { + var chunks = []; + deflate(bin, function (err, deflated) { + if (err) return end(err); + var push = inflateStream(onEmit, onUnused); + for (var i = 0, l = deflated.length; i < l; i += 128) { + push(null, bodec.slice(deflated, i, i + 128)); + } + }); + function onEmit(err, chunk) { + if (err) return end(err); + if (chunk === undefined) { + var inflated = bodec.join(chunks); + if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { + console.log([bin.length, inflated.length]); + return end(new Error("Problem with roundtrip")); + } + return end(); + } + chunks.push(chunk); + } + function onUnused(unused) { + if (unused[0].length) console.log("unused", unused); + } + } +]); From d4432f7f0bc70fa182c6cfc866a854026a8f5eb4 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 5 Mar 2014 23:09:29 +0000 Subject: [PATCH 121/256] Document object-codec --- doc/lib/object-codec.md | 109 ++++++++++++++++++++++++++++++++++++++++ test/test-mem-db.js | 1 - 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/doc/lib/object-codec.md b/doc/lib/object-codec.md index e69de29..efa6c5b 100644 --- a/doc/lib/object-codec.md +++ b/doc/lib/object-codec.md @@ -0,0 +1,109 @@ +# Object Codec + +This module implements a codec for the binary git object format for blobs, trees, tags, and commits. + +This library is useful for writing new storage backends. Normal users will probably +just use one of the existing mixins for object storage. + +## codec.frame({type,body}) -> buffer + +This function accepts an object with `type` and `body` properties. The `type` +property must be one of "blob", "tree", "commit" or "tag". The body can be a +pre-encoded raw-buffer or a plain javascript value. See encoder docs below for +the formats of the different body types. + +The returned binary value is the fully framed git object. The sha1 of this is +the git hash of the object. + +```js +var codec = require('js-git/lib/object-codec'); +var sha1 = require('git-sha1'); + +var bin = codec.frame({ type: "blob", body: "Hello World\n"}); +var hash = sha1(bin); +``` + +## codec.deframe(buffer, decode) -> {type,body} + +This function accepts a binary git buffer and returns the `{type,body}` object. + +If `decode` is true, then the body will also be decoded into a normal javascript +value. If `decode` is false or missing, then the raw-buffer will be in body. + +## codec.encoders + +This is an object containing 4 encoder function Each function has the signature: + + encode(body) -> raw-buffer + +Where body is the JS representation of the type and raw-buffer is the git encoded +version of that value, but without the type and length framing. + +```js +var encoders = require('js-git/lib/object-codec').encoders; +var modes = require('js-git/lib/modes'); +``` + +Blobs can be strings or binary buffers or arrays of bytes. + +```js +rawBin = encoders.blob("Hello World"); +rawBin = encoders.blob([1,2,3,4,5,6]); +``` + +Trees are objects with filename as key and object with {mode,hash} as value. +The modes are integers. It's best to use the modes module to help. + +```js +rawBin = encoders.tree({ "greeting.txt": { + mode: modes.file, + hash: blobHash +}}); +``` + +Commits are objects with required fields {tree,author,message} +Also if there is a single parent, you specify it with `parent`. + +For merge commits, you can use an array of hashes in `parents`. +When decoding, the output is always normalized to an array of `parents`. + +The `author` field is required and contains {name,email} and optionally `date` + +If you want to set a specefic timezone, set the `timezoneOffset` property on the +date object used in the `date` field. + +Commits can also have a `committer` with the same structure as `author`. + +The `message` fiels is mandatory and a simple string. + +```js +var date = new Date(1391790910000); +date.timezoneOffset = 7 * 60; + +rawBin = encoders.commit({ + tree: treeHash, + author: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date + }, + parent: parentCommitHash, + message: "This is a test commit\n" +}); +``` + +Annotated tags are like commits, except they have different fields. + +```js +rawBin = encoders.tag({ + object: commitHash, + type: "commit", + tag: "mytag", + tagger: { + name: "Tim Caswell", + email: "tim@creationix.com", + date: date, + }, + message: "Tag it!\n" +}); +``` diff --git a/test/test-mem-db.js b/test/test-mem-db.js index b57f233..de2e816 100644 --- a/test/test-mem-db.js +++ b/test/test-mem-db.js @@ -3,7 +3,6 @@ var bodec = require('bodec'); var sha1 = require('git-sha1'); var codec = require('../lib/object-codec.js'); - var repo = {}; require('../mixins/mem-db.js')(repo); From 43f85c04e0491251ae48af9a09565e68c008d1a5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 5 Mar 2014 23:12:37 +0000 Subject: [PATCH 122/256] Add quick docs for decoders --- doc/lib/object-codec.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/lib/object-codec.md b/doc/lib/object-codec.md index efa6c5b..6c8c538 100644 --- a/doc/lib/object-codec.md +++ b/doc/lib/object-codec.md @@ -107,3 +107,14 @@ rawBin = encoders.tag({ message: "Tag it!\n" }); ``` + +## codec.decoders + +This is just like `codec.encoders` except these functions do the opposite. +They have the format: + + decode(raw-buffer) -> body + +```js +var commit = decoders.commit(rawCommitBin); +``` From 393259678b2a1be4ef1c6960bf1b6f4de13b1f8b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 16:59:33 +0000 Subject: [PATCH 123/256] Document the zlib wrappers --- doc/lib/deflate.md | 16 ++++++++++++++++ doc/lib/inflate-stream.md | 23 +++++++++++++++++++++++ doc/lib/inflate.md | 16 ++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/doc/lib/deflate.md b/doc/lib/deflate.md index e69de29..984b11e 100644 --- a/doc/lib/deflate.md +++ b/doc/lib/deflate.md @@ -0,0 +1,16 @@ +# Deflate + +This module implements a simple interface that when normal given data, gives +you back the deflated version in a callback. This will use node's native zlib +bindings when available, but otherwise, wraps the pako dependency. + +## deflate(inflated) => deflated + +```js +var deflate = require('js-git/lib/deflate'); + +deflate(original, function (err, deflated) { + if (err) throw err; + // yay +}); +``` diff --git a/doc/lib/inflate-stream.md b/doc/lib/inflate-stream.md index e69de29..4faa15f 100644 --- a/doc/lib/inflate-stream.md +++ b/doc/lib/inflate-stream.md @@ -0,0 +1,23 @@ +# Inflate Stream + +This module implements zlib inflate by hand with a special streaming interface. +This is used in js-git to inflate git object fragments in a pack-stream. + +## inflateStream(onEmit, onUnused) -> onInput + +```js +var onInput = inflateStream(onEmit, onUnused); + +someStream.on("data", function (chunk) { + onInput(null, chunk); +}); + +function onEmit(err, out) { + if (err) throw err; + // out is a chunk of inflated data +} + +function onUnused(chunks) { + // chunks is an array of extra buffers or buffer slices. +} +``` diff --git a/doc/lib/inflate.md b/doc/lib/inflate.md index e69de29..2b9594a 100644 --- a/doc/lib/inflate.md +++ b/doc/lib/inflate.md @@ -0,0 +1,16 @@ +# Inflate + +This module implements a simple interface that when given deflated data, gives +you back the inflated version in a callback. This will use node's native zlib +bindings when available, but otherwise, wrap the included streaming inflate. + +## inflate(deflated) => inflated + +```js +var inflate = require('js-git/lib/inflate'); + +inflate(deflated, function (err, inflated) { + if (err) throw err; + // yay +}); +``` From 2a2e109d9c3b3abd5fe77b14d5abf994c547dcf8 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 17:56:56 +0000 Subject: [PATCH 124/256] Change date format to be plain objects and start testing pack codec. --- doc/lib/object-codec.md | 20 +++++++++------ lib/object-codec.js | 20 ++++++++++----- test/test-object-codec.js | 18 ++++++++----- test/test-pack-codec.js | 54 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 21 deletions(-) create mode 100644 test/test-pack-codec.js diff --git a/doc/lib/object-codec.md b/doc/lib/object-codec.md index 6c8c538..3463945 100644 --- a/doc/lib/object-codec.md +++ b/doc/lib/object-codec.md @@ -69,23 +69,24 @@ When decoding, the output is always normalized to an array of `parents`. The `author` field is required and contains {name,email} and optionally `date` -If you want to set a specefic timezone, set the `timezoneOffset` property on the -date object used in the `date` field. - Commits can also have a `committer` with the same structure as `author`. +The `date` property of `author` and `committer` is in the format {seconds,offset} +Where seconds is a unix timestamp in seconds and offset is the number of minutes +offset for the timezone. (Your local offset can be found with `(new Date).getTimezoneOffset()`) + The `message` fiels is mandatory and a simple string. ```js -var date = new Date(1391790910000); -date.timezoneOffset = 7 * 60; - rawBin = encoders.commit({ tree: treeHash, author: { name: "Tim Caswell", email: "tim@creationix.com", - date: date + date: { + seconds: 1391790910, + offset: 7 * 60 + } }, parent: parentCommitHash, message: "This is a test commit\n" @@ -102,7 +103,10 @@ rawBin = encoders.tag({ tagger: { name: "Tim Caswell", email: "tim@creationix.com", - date: date, + date: { + seconds: 1391790910, + offset: 7 * 60 + } }, message: "Tag it!\n" }); diff --git a/lib/object-codec.js b/lib/object-codec.js index 0351e6d..8588d35 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -162,12 +162,20 @@ function two(num) { } function formatDate(date) { - var offset = date.timezoneOffset || date.getTimezoneOffset(); + var seconds, offset; + if (date.seconds) { + seconds = date.seconds; + offset = date.offset; + } + // Also accept Date instances + else { + seconds = Math.floor(date.getTime() / 1000); + offset = date.getTimezoneOffset(); + } var neg = "+"; if (offset <= 0) offset = -offset; else neg = "-"; offset = neg + two(Math.floor(offset / 60)) + two(offset % 60); - var seconds = Math.floor(date.getTime() / 1000); return seconds + " " + offset; } @@ -271,13 +279,13 @@ function decodeTag(body) { function decodePerson(string) { var match = string.match(/^([^<]*) <([^>]*)> ([^ ]*) (.*)$/); if (!match) throw new Error("Improperly formatted person string"); - var sec = parseInt(match[3], 10); - var date = new Date(sec * 1000); - date.timeZoneoffset = parseInt(match[4], 10) / 100 * -60; return { name: match[1], email: match[2], - date: date + date: { + seconds: parseInt(match[3], 10), + offset: parseInt(match[4], 10) / 100 * -60 + } }; } diff --git a/test/test-object-codec.js b/test/test-object-codec.js index c06fac7..39826fe 100644 --- a/test/test-object-codec.js +++ b/test/test-object-codec.js @@ -33,14 +33,15 @@ run([ } }, function testEncodeCommit() { - var date = new Date(1391790884000); - date.timezoneOffset = 7 * 60; commit = { tree: treeHash, author: { name: "Tim Caswell", email: "tim@creationix.com", - date: date + date: { + seconds: 1391790884, + offset: 7 * 60 + } }, message: "Test Commit\n" }; @@ -51,8 +52,6 @@ run([ } }, function testEncodeTag() { - var date = new Date(1391790910000); - date.timezoneOffset = 7 * 60; tag = { object: commitHash, type: "commit", @@ -60,7 +59,10 @@ run([ tagger: { name: "Tim Caswell", email: "tim@creationix.com", - date: date, + date: { + seconds: 1391790910, + offset: 7 * 60 + } }, message: "Tag it!\n" }; @@ -80,7 +82,9 @@ run([ function testDecodeCommit() { var obj = codec.deframe(commitBin, true); if (obj.type !== "commit") throw new Error("Invalid type"); - if (!(obj.body.tree === commit.tree && obj.body.message === commit.message)) { + if (!(obj.body.tree === commit.tree && + obj.body.message === commit.message && + obj.body.author.date.seconds === commit.author.date.seconds)) { throw new Error("Problem decoding"); } }, diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js new file mode 100644 index 0000000..27e7bcf --- /dev/null +++ b/test/test-pack-codec.js @@ -0,0 +1,54 @@ +var modes = require('../lib/modes.js'); +var bodec = require('bodec'); +var sha1 = require('git-sha1'); +var run = require('./run.js'); +var decoders = require('../lib/object-codec.js').decoders; + +// The thing we mean to test. +var codec = require('../lib/pack-codec.js'); + + +// pack-763df34c046438fd52466d888b46f2e6adb4039d.pack +// var pack = bodec.fromBase64('UEFDSwAAAAIAAAAHlA54nJ3LWw7CIBBG4XdWMRvQDAVKSYwx6RbcwAA/lqQXUzG6fLsGX0++03aAfK+9yRiG4IIDl8ylCDgxXFdyYEmCwaWonrJjbRQPIykytM0uhjJ4a0KM1vTJJxuDNxEZOSh5t2nb6V4XGuX1wTzTpdXllnZIq9tav+e0LVfSJljd9bpzdOKeWR11qa3hn1eNk6wPUJtApc5QP7ZrRo2TC3icncxNCsIwEEDhfU4xF1Dy0yRTkCJ05d4LTGKCA0kLcUSPr3gEtw++J6MUCBPWjKGg9zHdKKVsM1okM3vnKzoXbawUrKKn3PcBV+6w0uNVWoOTcD/nUUh43/h9zHtfwLh5MtY7dHDQQWv1rZ1Fyj9WXTYWpgbrb6I+XRw3AMcJeJw1jEEOwiAQAO+8Yj+gAaGlJMaY+IV+gIVtXVOKqZtofy8evEzmMlPxQUkASU8xoSbjcodhGryzAdHZPvnkMHiLlCkHJfuTINVSWJTEGcre+LOZNhi5wC2+3rQscBYu17RRFK4rf46tuYCxwZlT5/wAB91rrdTYHnIn4JWF4/JffwGK/zCMqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShh2OnOb+K3/Hfb3s+7jrn/7csVXdVzGAAm5xFpqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xtAF4nPNIzcnJVwjPL8pJUXDOSMxLT03hAgBLKQbxPHic80jNyclXCM8vyknhAgAcMgQn30Dgfr8qXI6710Y++2pQNk1UifU='); + +// pack-2746619e21b1b8527bfae6520441ef5ead239e3f.pack +// var pack = bodec.fromBase64('UEFDSwAAAAIAAAAKmA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKogV4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kqBIAxkEfn7UBeJzzSM3JyVcIzy/KSeFyTElRKM7PTeUCAFCMBw6qAnicMzQwMDMxUcitTC9KTS3JzEvXK6koYRB8EOU0XU+mxPrWP/G5c40cq/ZWHAIAMVIQQ6gCeJwzNDAwMzFRSC9KTS3JzEvXK6koYRB8EOU0XU+mxPrWP/G5c40cq/ZWHAIAD7kPXagCeJwzNDAwMzFRSC9KTS3JzEvXK6koYQit3WD7cvqJLC+mPrm9VjKvlaIfWQAAGbwPMTx4nPNIzcnJVwjPL8pJ4QIAHDIEJ9ZeN41OSEIONrXcvdaEN0YnUsHv'); + +// This is a small sample packfile with couple offset deltas +// pack-5851ce932ec42973b51d631afe25da247c3dc49a.pack +var pack = bodec.fromBase64('UEFDSwAAAAIAAAAQnQ54nJ3MWwoCMQxA0f+uIhtQ0nYeKYgobsENZNoEC/OQMTK6e2cN/l4411YRYCo5kseITVLpSmAfOVLSnFJB6kJqSukDuSevMhu0moed9CmrKjKFwpIxtT7TINh2vSqReHX8tseywr1OcOPXJuMIJ6vTJa/CVpe5fo55mc7gY2p86LFBOGCH6PY6VTP5x7prKfAVA54Xe+yLWTbQOor7AZUCSPmRDnicnctRCgIhEADQf08xFyjGUVeFiKIrdAEdZ0lYd8OM7fh1hn4fvNFFQEi8JCcuCoWSmakwY8xoHGMxkdgimZjVM3VZB8wUPMUJLWrRPml0IdspuJl1JHJBSijGRlLpPR5bh3ttcEuvXZYFTqO2C3dJo25r/Rx5a2fQJlpNHgnhgBOi+mmrY8g/V11LgVV2mOsi6guDiEL9mA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKqQl4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kVmVeajlYifjVm28/SzW0d12ZKCB++trFC8ZKOxBKjMBqauylWlkm6kbyCrH0Gp01vHQ9NnMNAFftOrq1AXic80jNyclXCM8vyknhckxJUSjOz03lAgBQjAcOPXicS8zLL8lILVJIy8xJ5QIAI9cEvLEBeJyrTC1RSMzLL8lILVJIy8xJ5QIAOsAGLmWAPnicm8lYOqEUAAX6AhVkEHicKw2aEAQABEABqqoCeJwzNDAwMzFRyK1ML0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAxUhBDqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAPuQ9dqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xPHic80jNyclXCM8vyknhAgAcMgQnuZAj3ZpSLQckQi9VfpQYWt+hefM='); +run([ + function testDecodePack() { + var meta; + var finished = false; + var items = []; + var write = codec.decodePack(function (item) { + if (item === undefined) { + finished = true; + } + else if (!meta) { + meta = item; + } + else { + if (item.type === "tree" || item.type === "tag" || item.type === "commit") { + item.body = decoders[item.type](item.body); + } + else { + item.body = bodec.toRaw(item.body); + } + items.push(item); + } + }); + for (var i = 0, l = pack.length; i < l; i += 128) { + write(bodec.slice(pack, i, i + 128)); + } + write(); + + if (!finished) throw new Error("Codec didn't emit end"); + if (items.length !== meta.num) { + throw new Error("Item count mistmatch"); + } + console.log(meta); + console.log(items); + } +]); From 455011c8ab7dd7857baea026624042af9400c551 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 18:48:33 +0000 Subject: [PATCH 125/256] Make inflate/deflate sync and continue pack-codec tests --- lib/deflate.js | 13 ++++------- lib/inflate.js | 52 ++++++++++++++++------------------------- lib/pack-codec.js | 12 ++++++++++ mixins/packops.js | 10 ++------ test/test-pack-codec.js | 47 ++++++++++++++++++++++++++----------- test/test-zlib.js | 47 +++++++++++++++++++------------------ 6 files changed, 96 insertions(+), 85 deletions(-) diff --git a/lib/deflate.js b/lib/deflate.js index 72d333e..072a130 100644 --- a/lib/deflate.js +++ b/lib/deflate.js @@ -1,13 +1,10 @@ if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { var nodeRequire = require; - module.exports = nodeRequire("zlib").deflate; + var pako = nodeRequire('pako'); + module.exports = function (buffer) { + return new Buffer(pako.deflate(new Uint8Array(buffer))); + }; } else { - var deflate = require('pako/deflate').deflate; - module.exports = function (buffer, callback) { - var out; - try { out = deflate(buffer); } - catch (err) { return callback(err); } - return callback(null, out); - }; + module.exports = nodeRequire('pako/deflate').deflate; } diff --git a/lib/inflate.js b/lib/inflate.js index b69f10d..2cf2826 100644 --- a/lib/inflate.js +++ b/lib/inflate.js @@ -1,35 +1,23 @@ -if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { - var nodeRequire = require; - module.exports = nodeRequire("zlib").inflate; -} -else { - var inflateStream = require('./inflate-stream.js'); - var binary = require('bodec'); - module.exports = function inflate(buffer, callback) { - var out; - try { - var parts = []; - var done = false; - var push = inflateStream(onEmit, onUnused); - push(null, buffer); - push(); - if (!done) throw new Error("Missing input data"); - out = binary.join(parts); - } catch (err) { - return callback(err); - } - callback(null, out); +var inflateStream = require('./inflate-stream.js'); +var binary = require('bodec'); +module.exports = function inflate(buffer) { + var parts = []; + var done = false; + var push = inflateStream(onEmit, onUnused); + push(null, buffer); + push(); + if (!done) throw new Error("Missing input data"); + return binary.join(parts); - function onEmit(err, chunk) { - if (err) throw err; - if (chunk) parts.push(chunk); - else done = true; - } + function onEmit(err, chunk) { + if (err) throw err; + if (chunk) parts.push(chunk); + else done = true; + } - function onUnused(extra) { - if (extra && extra.length && extra[0].length) { - throw new Error("Too much input data"); - } + function onUnused(extra) { + if (extra && extra.length && extra[0].length) { + throw new Error("Too much input data"); } - }; -} + } +}; diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 0ea12b7..950252e 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -17,6 +17,18 @@ for (var type in typeToNum) { numToType[num] = type; } +exports.packHeader = packHeader; +function packHeader(length) { + return binary.fromArray([ + 0x50, 0x41, 0x43, 0x4b, // PACK + 0, 0, 0, 2, // version 2 + length >> 24, // Num of objects + (length >> 16) & 0xff, + (length >> 8) & 0xff, + length & 0xff + ]); +} + exports.packFrame = packFrame; function packFrame(type, body, callback) { var length = body.length; diff --git a/mixins/packops.js b/mixins/packops.js index b9ee437..5698ecb 100644 --- a/mixins/packops.js +++ b/mixins/packops.js @@ -6,6 +6,7 @@ var applyDelta = require('../lib/apply-delta.js'); var pushToPull = require('push-to-pull'); var decodePack = require('../lib/pack-codec.js').decodePack; var packFrame = require('../lib/pack-codec.js').packFrame; +var packHeader = require('../lib/pack-codec.js').packHeader; module.exports = function (repo) { // packStream is a simple-stream containing raw packfile binary data @@ -170,14 +171,7 @@ function pack(hashes, opts, callback) { function readFirst(callback) { var length = hashes.length; - var chunk = binary.fromArray([ - 0x50, 0x41, 0x43, 0x4b, // PACK - 0, 0, 0, 2, // version 2 - length >> 24, // Num of objects - (length >> 16) & 0xff, - (length >> 8) & 0xff, - length & 0xff - ]); + var chunk = packHeader(length); first = false; sha1sum.update(chunk); callback(null, chunk); diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js index 27e7bcf..181e00b 100644 --- a/test/test-pack-codec.js +++ b/test/test-pack-codec.js @@ -8,20 +8,16 @@ var decoders = require('../lib/object-codec.js').decoders; var codec = require('../lib/pack-codec.js'); -// pack-763df34c046438fd52466d888b46f2e6adb4039d.pack -// var pack = bodec.fromBase64('UEFDSwAAAAIAAAAHlA54nJ3LWw7CIBBG4XdWMRvQDAVKSYwx6RbcwAA/lqQXUzG6fLsGX0++03aAfK+9yRiG4IIDl8ylCDgxXFdyYEmCwaWonrJjbRQPIykytM0uhjJ4a0KM1vTJJxuDNxEZOSh5t2nb6V4XGuX1wTzTpdXllnZIq9tav+e0LVfSJljd9bpzdOKeWR11qa3hn1eNk6wPUJtApc5QP7ZrRo2TC3icncxNCsIwEEDhfU4xF1Dy0yRTkCJ05d4LTGKCA0kLcUSPr3gEtw++J6MUCBPWjKGg9zHdKKVsM1okM3vnKzoXbawUrKKn3PcBV+6w0uNVWoOTcD/nUUh43/h9zHtfwLh5MtY7dHDQQWv1rZ1Fyj9WXTYWpgbrb6I+XRw3AMcJeJw1jEEOwiAQAO+8Yj+gAaGlJMaY+IV+gIVtXVOKqZtofy8evEzmMlPxQUkASU8xoSbjcodhGryzAdHZPvnkMHiLlCkHJfuTINVSWJTEGcre+LOZNhi5wC2+3rQscBYu17RRFK4rf46tuYCxwZlT5/wAB91rrdTYHnIn4JWF4/JffwGK/zCMqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShh2OnOb+K3/Hfb3s+7jrn/7csVXdVzGAAm5xFpqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xtAF4nPNIzcnJVwjPL8pJUXDOSMxLT03hAgBLKQbxPHic80jNyclXCM8vyknhAgAcMgQn30Dgfr8qXI6710Y++2pQNk1UifU='); - -// pack-2746619e21b1b8527bfae6520441ef5ead239e3f.pack -// var pack = bodec.fromBase64('UEFDSwAAAAIAAAAKmA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKogV4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kqBIAxkEfn7UBeJzzSM3JyVcIzy/KSeFyTElRKM7PTeUCAFCMBw6qAnicMzQwMDMxUcitTC9KTS3JzEvXK6koYRB8EOU0XU+mxPrWP/G5c40cq/ZWHAIAMVIQQ6gCeJwzNDAwMzFRSC9KTS3JzEvXK6koYRB8EOU0XU+mxPrWP/G5c40cq/ZWHAIAD7kPXagCeJwzNDAwMzFRSC9KTS3JzEvXK6koYQit3WD7cvqJLC+mPrm9VjKvlaIfWQAAGbwPMTx4nPNIzcnJVwjPL8pJ4QIAHDIEJ9ZeN41OSEIONrXcvdaEN0YnUsHv'); - // This is a small sample packfile with couple offset deltas // pack-5851ce932ec42973b51d631afe25da247c3dc49a.pack var pack = bodec.fromBase64('UEFDSwAAAAIAAAAQnQ54nJ3MWwoCMQxA0f+uIhtQ0nYeKYgobsENZNoEC/OQMTK6e2cN/l4411YRYCo5kseITVLpSmAfOVLSnFJB6kJqSukDuSevMhu0moed9CmrKjKFwpIxtT7TINh2vSqReHX8tseywr1OcOPXJuMIJ6vTJa/CVpe5fo55mc7gY2p86LFBOGCH6PY6VTP5x7prKfAVA54Xe+yLWTbQOor7AZUCSPmRDnicnctRCgIhEADQf08xFyjGUVeFiKIrdAEdZ0lYd8OM7fh1hn4fvNFFQEi8JCcuCoWSmakwY8xoHGMxkdgimZjVM3VZB8wUPMUJLWrRPml0IdspuJl1JHJBSijGRlLpPR5bh3ttcEuvXZYFTqO2C3dJo25r/Rx5a2fQJlpNHgnhgBOi+mmrY8g/V11LgVV2mOsi6guDiEL9mA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKqQl4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kVmVeajlYifjVm28/SzW0d12ZKCB++trFC8ZKOxBKjMBqauylWlkm6kbyCrH0Gp01vHQ9NnMNAFftOrq1AXic80jNyclXCM8vyknhckxJUSjOz03lAgBQjAcOPXicS8zLL8lILVJIy8xJ5QIAI9cEvLEBeJyrTC1RSMzLL8lILVJIy8xJ5QIAOsAGLmWAPnicm8lYOqEUAAX6AhVkEHicKw2aEAQABEABqqoCeJwzNDAwMzFRyK1ML0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAxUhBDqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAPuQ9dqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xPHic80jNyclXCM8vyknhAgAcMgQnuZAj3ZpSLQckQi9VfpQYWt+hefM='); +var items = []; + run([ function testDecodePack() { var meta; var finished = false; - var items = []; + var counts = {}; var write = codec.decodePack(function (item) { if (item === undefined) { finished = true; @@ -30,13 +26,12 @@ run([ meta = item; } else { + items.push(item); + counts[item.type] = counts[item.type] || 0; + counts[item.type]++; if (item.type === "tree" || item.type === "tag" || item.type === "commit") { - item.body = decoders[item.type](item.body); + decoders[item.type](item.body); } - else { - item.body = bodec.toRaw(item.body); - } - items.push(item); } }); for (var i = 0, l = pack.length; i < l; i += 128) { @@ -48,7 +43,31 @@ run([ if (items.length !== meta.num) { throw new Error("Item count mistmatch"); } - console.log(meta); - console.log(items); + if (counts.commit !== 6) throw new Error("Wrong number of commits parsed"); + if (counts.tree !== 4) throw new Error("Wrong number of trees parsed"); + if (counts.blob !== 4) throw new Error("Wrong number of blobs parsed"); + if (counts['ofs-delta'] !== 2) throw new Error("Wrong number of offset deltas parsed"); + }, + function testPackFrame(end) { + + var write = codec.decodePack(function (item) { + console.log("WROTE", item); + }); + + var index = 0; + next(); + function next() { + if (index >= items.length) return end(); + var item = items[index++]; + + // Skip delta frames + if (item.type.indexOf("-") > 0) return next(); + + codec.packFrame(item.type, item.body, function (err, packed) { + if (err) return end(err); + console.log(packed.length, item.body.length); + next(); + }) + } } ]); diff --git a/test/test-zlib.js b/test/test-zlib.js index 2833ffd..254dc1f 100644 --- a/test/test-zlib.js +++ b/test/test-zlib.js @@ -12,37 +12,38 @@ for (var i = 0; i < 1024; i++) { } run([ - function testRoundTrip(end) { - deflate(bin, function (err, deflated) { - if (err) return end(err); - inflate(deflated, function (err, inflated) { - if (err) return end(err); - if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { - console.log([bin, inflated]); - return end(new Error("Problem with roundtrip")); - } - end(); - }); - }); + function testRoundTrip() { + var deflated = deflate(bin); + if (!bodec.isBinary(deflated)) { + throw new Error("deflate output should be native binary"); + } + var inflated = inflate(deflated); + if (!bodec.isBinary(inflated)) { + throw new Error("inflate output should be native binary"); + } + if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { + console.log([bin, inflated]); + throw new Error("Problem with roundtrip"); + } }, - function testStream(end) { + function testStream() { + var done = false; var chunks = []; - deflate(bin, function (err, deflated) { - if (err) return end(err); - var push = inflateStream(onEmit, onUnused); - for (var i = 0, l = deflated.length; i < l; i += 128) { - push(null, bodec.slice(deflated, i, i + 128)); - } - }); + var deflated = deflate(bin); + var push = inflateStream(onEmit, onUnused); + for (var i = 0, l = deflated.length; i < l; i += 128) { + push(null, bodec.slice(deflated, i, i + 128)); + } + if (!done) throw new Error("Not finished"); function onEmit(err, chunk) { - if (err) return end(err); + if (err) throw err; if (chunk === undefined) { var inflated = bodec.join(chunks); if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { console.log([bin.length, inflated.length]); - return end(new Error("Problem with roundtrip")); + throw new Error("Problem with roundtrip"); } - return end(); + done = true; } chunks.push(chunk); } From 7ff50f2c75b5cb183937ae609f900808470e12b7 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 18:50:26 +0000 Subject: [PATCH 126/256] Update docs for inflate/deflate. --- doc/lib/deflate.md | 9 ++------- doc/lib/inflate.md | 12 ++++-------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/doc/lib/deflate.md b/doc/lib/deflate.md index 984b11e..b81df87 100644 --- a/doc/lib/deflate.md +++ b/doc/lib/deflate.md @@ -1,16 +1,11 @@ # Deflate -This module implements a simple interface that when normal given data, gives -you back the deflated version in a callback. This will use node's native zlib -bindings when available, but otherwise, wraps the pako dependency. +This module implements a simple interface that when normal given data, returns the deflated version in a callback. This wraps the pako dependency. ## deflate(inflated) => deflated ```js var deflate = require('js-git/lib/deflate'); -deflate(original, function (err, deflated) { - if (err) throw err; - // yay -}); +var deflated = deflate(original); ``` diff --git a/doc/lib/inflate.md b/doc/lib/inflate.md index 2b9594a..31cc102 100644 --- a/doc/lib/inflate.md +++ b/doc/lib/inflate.md @@ -1,16 +1,12 @@ # Inflate -This module implements a simple interface that when given deflated data, gives -you back the inflated version in a callback. This will use node's native zlib -bindings when available, but otherwise, wrap the included streaming inflate. +This module implements a simple interface that when given deflated data returns the inflated version. +This wraps the included streaming inflate. -## inflate(deflated) => inflated +## inflate(deflated) -> inflated ```js var inflate = require('js-git/lib/inflate'); -inflate(deflated, function (err, inflated) { - if (err) throw err; - // yay -}); +var inflated = inflate(deflated); ``` From 1623a373cf2dc0111e7bcd43abe40f5cb80b7d17 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 20:33:19 +0000 Subject: [PATCH 127/256] Refactor pack codec and update failing test case --- lib/pack-codec.js | 95 +++++++++++++++++++++++----------- test/test-pack-codec.js | 111 ++++++++++++++++++++++++---------------- 2 files changed, 132 insertions(+), 74 deletions(-) diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 950252e..36e663b 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -17,35 +17,6 @@ for (var type in typeToNum) { numToType[num] = type; } -exports.packHeader = packHeader; -function packHeader(length) { - return binary.fromArray([ - 0x50, 0x41, 0x43, 0x4b, // PACK - 0, 0, 0, 2, // version 2 - length >> 24, // Num of objects - (length >> 16) & 0xff, - (length >> 8) & 0xff, - length & 0xff - ]); -} - -exports.packFrame = packFrame; -function packFrame(type, body, callback) { - var length = body.length; - var head = [(typeToNum[type] << 4) | (length & 0xf)]; - var i = 0; - length >>= 4; - while (length) { - head[i++] |= 0x80; - head[i] = length & 0x7f; - length >>= 7; - } - deflate(body, function (err, body) { - if (err) return callback(err); - callback(null, binary.join([binary.fromArray(head), body])); - }); -} - exports.decodePack = decodePack; function decodePack(emit) { @@ -151,6 +122,7 @@ function decodePack(emit) { ref = ""; return $refDelta; } + // console.log({type: type,length: length}) return $body; } @@ -268,7 +240,70 @@ function inflate() { } function onUnused(chunks) { - // console.log("onUnused", chunks); + if (chunks[0].length) throw new Error("Extra data after deflate"); more = false; } } + +exports.encodePack = encodePack; +function encodePack(emit) { + var sha1sum = sha1(); + var left; + return function (item) { + if (item === undefined) { + if (left !== 0) throw new Error("Some items were missing"); + return emit(); + } + if (typeof item.num === "number") { + if (left !== undefined) throw new Error("Header already sent"); + left = item.num; + write(packHeader(item.num)); + } + else if (typeof item.type === "string" && binary.isBinary(item.body)) { + // The header must be sent before items. + if (typeof left !== "number") throw new Error("Headers not sent yet"); + + // Make sure we haven't sent all the items already + if (!left) throw new Error("All items already sent"); + + // Send the item in packstream format + write(packFrame(item.type, item.body)); + + // Send the checksum after the last item + if (!--left) { + emit(binary.fromHex(sha1sum.digest())); + } + } + else { + throw new Error("Invalid item"); + } + }; + function write(chunk) { + sha1sum.update(chunk); + emit(chunk); + } +} + +function packHeader(length) { + return binary.fromArray([ + 0x50, 0x41, 0x43, 0x4b, // PACK + 0, 0, 0, 2, // version 2 + length >> 24, // Num of objects + (length >> 16) & 0xff, + (length >> 8) & 0xff, + length & 0xff + ]); +} + +function packFrame(type, body) { + var length = body.length; + var head = [(typeToNum[type] << 4) | (length & 0xf)]; + var i = 0; + length >>= 4; + while (length) { + head[i++] |= 0x80; + head[i] = length & 0x7f; + length >>= 7; + } + return binary.join([binary.fromArray(head), deflate(body)]); +} diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js index 181e00b..d7ce3e9 100644 --- a/test/test-pack-codec.js +++ b/test/test-pack-codec.js @@ -1,8 +1,7 @@ -var modes = require('../lib/modes.js'); var bodec = require('bodec'); -var sha1 = require('git-sha1'); var run = require('./run.js'); var decoders = require('../lib/object-codec.js').decoders; +var encoders = require('../lib/object-codec.js').encoders; // The thing we mean to test. var codec = require('../lib/pack-codec.js'); @@ -12,62 +11,86 @@ var codec = require('../lib/pack-codec.js'); // pack-5851ce932ec42973b51d631afe25da247c3dc49a.pack var pack = bodec.fromBase64('UEFDSwAAAAIAAAAQnQ54nJ3MWwoCMQxA0f+uIhtQ0nYeKYgobsENZNoEC/OQMTK6e2cN/l4411YRYCo5kseITVLpSmAfOVLSnFJB6kJqSukDuSevMhu0moed9CmrKjKFwpIxtT7TINh2vSqReHX8tseywr1OcOPXJuMIJ6vTJa/CVpe5fo55mc7gY2p86LFBOGCH6PY6VTP5x7prKfAVA54Xe+yLWTbQOor7AZUCSPmRDnicnctRCgIhEADQf08xFyjGUVeFiKIrdAEdZ0lYd8OM7fh1hn4fvNFFQEi8JCcuCoWSmakwY8xoHGMxkdgimZjVM3VZB8wUPMUJLWrRPml0IdspuJl1JHJBSijGRlLpPR5bh3ttcEuvXZYFTqO2C3dJo25r/Rx5a2fQJlpNHgnhgBOi+mmrY8g/V11LgVV2mOsi6guDiEL9mA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKqQl4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kVmVeajlYifjVm28/SzW0d12ZKCB++trFC8ZKOxBKjMBqauylWlkm6kbyCrH0Gp01vHQ9NnMNAFftOrq1AXic80jNyclXCM8vyknhckxJUSjOz03lAgBQjAcOPXicS8zLL8lILVJIy8xJ5QIAI9cEvLEBeJyrTC1RSMzLL8lILVJIy8xJ5QIAOsAGLmWAPnicm8lYOqEUAAX6AhVkEHicKw2aEAQABEABqqoCeJwzNDAwMzFRyK1ML0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAxUhBDqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAPuQ9dqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xPHic80jNyclXCM8vyknhAgAcMgQnuZAj3ZpSLQckQi9VfpQYWt+hefM='); var items = []; +var newPack; + +function unpackStream(stream) { + var meta, out = [], finished = false; + var write = codec.decodePack(onItem); + for (var i = 0, l = stream.length; i < l; i += 128) { + var slice = bodec.slice(stream, i, i + 128); + try { + console.log("SLICE", slice); + write(slice); + } + catch (err) { + throw err; + } + } + write(); + + function onItem(item) { + console.log("UNPACK", item); + if (item === undefined) { + finished = true; + } + else if (!meta) { + meta = item; + } + else { + out.push(item); + } + } + if (!finished) throw new Error("unpack stream didn't finish"); + if (out.length !== meta.num) throw new Error("Item num mismatch"); + return out; +} + run([ function testDecodePack() { - var meta; - var finished = false; var counts = {}; - var write = codec.decodePack(function (item) { - if (item === undefined) { - finished = true; - } - else if (!meta) { - meta = item; - } - else { - items.push(item); - counts[item.type] = counts[item.type] || 0; - counts[item.type]++; - if (item.type === "tree" || item.type === "tag" || item.type === "commit") { - decoders[item.type](item.body); - } + items = unpackStream(pack).map(function (item) { + counts[item.type] = counts[item.type] || 0; + counts[item.type]++; + if (item.type === "tree" || item.type === "tag" || item.type === "commit") { + item.body = decoders[item.type](item.body); } + return item; }); - for (var i = 0, l = pack.length; i < l; i += 128) { - write(bodec.slice(pack, i, i + 128)); - } - write(); - - if (!finished) throw new Error("Codec didn't emit end"); - if (items.length !== meta.num) { - throw new Error("Item count mistmatch"); - } if (counts.commit !== 6) throw new Error("Wrong number of commits parsed"); if (counts.tree !== 4) throw new Error("Wrong number of trees parsed"); if (counts.blob !== 4) throw new Error("Wrong number of blobs parsed"); if (counts['ofs-delta'] !== 2) throw new Error("Wrong number of offset deltas parsed"); }, - function testPackFrame(end) { + function testEncodePack() { + var done = false; + var outs = []; - var write = codec.decodePack(function (item) { - console.log("WROTE", item); + var write = codec.encodePack(function (item) { + if (item === undefined) { + done = true; + return; + } + if (!bodec.isBinary(item)) throw new Error("encode output must be buffers"); + outs.push(item); }); + write({num:items.length}); + items.forEach(function (item) { + if (!bodec.isBinary(item.body)) { + item.body = encoders[item.type](item.body); + } + write(item); + }); + write(); - var index = 0; - next(); - function next() { - if (index >= items.length) return end(); - var item = items[index++]; - - // Skip delta frames - if (item.type.indexOf("-") > 0) return next(); + if (!done) throw new Error("Output stream never ended"); - codec.packFrame(item.type, item.body, function (err, packed) { - if (err) return end(err); - console.log(packed.length, item.body.length); - next(); - }) - } + newPack = bodec.join(outs); + }, + function verifyEncodePack() { + var original = unpackStream(pack); + // console.log(original) + var final = unpackStream(newPack); + console.log(original, final); } ]); From 1c5e7d75fe2279e1d8e93b93b0e81b35454677f0 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 20:42:55 +0000 Subject: [PATCH 128/256] Add back note about Chris's inflate code. --- lib/inflate-stream.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/inflate-stream.js b/lib/inflate-stream.js index c7eff68..2a771b5 100644 --- a/lib/inflate-stream.js +++ b/lib/inflate-stream.js @@ -1,5 +1,8 @@ var binary = require('bodec'); +// This code is slightly modified from chrisdickson/inflate min.js +// The original code is under the MIT license copyright Chris Dickinson + module.exports = inflate; var MAXBITS = 15 From c851c3ce127a43d92bdaf66f729100396011534b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 22:49:55 +0000 Subject: [PATCH 129/256] Fix encodePack --- lib/pack-codec.js | 37 +++++++++++++++++++++++++++++++------ test/test-pack-codec.js | 19 +++++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 36e663b..61f70fd 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -153,10 +153,14 @@ function decodePack(emit) { // Common helper for emitting all three object shapes function emitObject() { + var body = binary.join(parts); + if (body.length !== length) { + throw new Error("Body length mismatch"); + } var item = { type: numToType[type], size: length, - body: binary.join(parts), + body: body, offset: start }; if (ref) item.ref = ref; @@ -267,7 +271,7 @@ function encodePack(emit) { if (!left) throw new Error("All items already sent"); // Send the item in packstream format - write(packFrame(item.type, item.body)); + write(packFrame(item)); // Send the checksum after the last item if (!--left) { @@ -295,9 +299,11 @@ function packHeader(length) { ]); } -function packFrame(type, body) { - var length = body.length; - var head = [(typeToNum[type] << 4) | (length & 0xf)]; +function packFrame(item) { + var length = item.body.length; + + // write TYPE_AND_BASE128_SIZE + var head = [(typeToNum[item.type] << 4) | (length & 0xf)]; var i = 0; length >>= 4; while (length) { @@ -305,5 +311,24 @@ function packFrame(type, body) { head[i] = length & 0x7f; length >>= 7; } - return binary.join([binary.fromArray(head), deflate(body)]); + + if (typeof item.ref === "number") { + // write BIG_ENDIAN_MODIFIED_BASE_128_NUMBER + var offset = item.ref; + // Calculate how many digits we need in base 128 and move the pointer + i += Math.floor(Math.log(offset) / Math.log(0x80)) + 1; + // Write the last digit + head[i] = offset & 0x7f; + // Then write the rest + while (offset >>= 7) { + head[--i] = 0x80 | (--offset & 0x7f); + } + } + + var parts = [binary.fromArray(head)]; + if (typeof item.ref === "string") { + parts.push(binary.fromHex(item.ref)); + } + parts.push(deflate(item.body)); + return binary.join(parts); } diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js index d7ce3e9..f5974be 100644 --- a/test/test-pack-codec.js +++ b/test/test-pack-codec.js @@ -19,7 +19,7 @@ function unpackStream(stream) { for (var i = 0, l = stream.length; i < l; i += 128) { var slice = bodec.slice(stream, i, i + 128); try { - console.log("SLICE", slice); + // console.log("SLICE", slice); write(slice); } catch (err) { @@ -29,7 +29,7 @@ function unpackStream(stream) { write(); function onItem(item) { - console.log("UNPACK", item); + // console.log("UNPACK", item); if (item === undefined) { finished = true; } @@ -88,9 +88,16 @@ run([ newPack = bodec.join(outs); }, function verifyEncodePack() { - var original = unpackStream(pack); - // console.log(original) - var final = unpackStream(newPack); - console.log(original, final); + try { + unpackStream(newPack); + if (bodec.toHex(pack) !== bodec.toHex(newPack)) { + throw new Error("Final pack doesn't match original."); + } + } + catch (err) { + console.log(bodec.toHex(pack)); + console.log(bodec.toHex(newPack)); + throw err; + } } ]); From 432ea67ceffe0a0e798eef4d76e6891778307a43 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 22:50:53 +0000 Subject: [PATCH 130/256] Remove tabs --- lib/pack-codec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 61f70fd..14081e8 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -320,9 +320,9 @@ function packFrame(item) { // Write the last digit head[i] = offset & 0x7f; // Then write the rest - while (offset >>= 7) { - head[--i] = 0x80 | (--offset & 0x7f); - } + while (offset >>= 7) { + head[--i] = 0x80 | (--offset & 0x7f); + } } var parts = [binary.fromArray(head)]; From c53aae36700766d300f3c941626e845e8e211700 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 23:25:56 +0000 Subject: [PATCH 131/256] Document Pack Codec --- doc/lib/pack-codec.md | 81 +++++++++++++++++++++++++++++++++++++++++ test/test-pack-codec.js | 1 + 2 files changed, 82 insertions(+) create mode 100644 doc/lib/pack-codec.md diff --git a/doc/lib/pack-codec.md b/doc/lib/pack-codec.md new file mode 100644 index 0000000..d0d2131 --- /dev/null +++ b/doc/lib/pack-codec.md @@ -0,0 +1,81 @@ +# Pack Codec + +This module implements a codec for packfile streams used in the git network +protocols as well as the on-disk packfile format. + +These are a sync stream transforms. It accepts an emit function and returns a +write function. Both of these have the same interface. You signal `end` to the +input side by writing undefined (or nothing) and when emit gets called with +undefined that is `end` on the output. + +Since this is sync, errors are simply thrown. If you want to use this in the +context of an async stream with back-pressure, it's up to the consumer to handle +exceptions and write to the input at the correct rate. Basically to implement +back-pressure, you only need to keep writing values to the input till enough +data comes out the output. It's sync so by the time `write()` returns, `emit()` +will have been called as many times as it ever will (without more writes). + +Here is an example of using the decodePack in a node push stream that ignores +backpressure. + +```js +var decodePack = require('js-git/lib/pack-codec').decodePack; + +var write = decodePack(onItem); +stream.on("data", write); +stream.on("end", write); +var meta; +function onItem(item) { + if (item === undefined) { + // END of Stream + } + else if (meta === undefined) { + meta = item; + } + else { + console.log(item); + } +} +``` + +The first output is the meta object: + +```js +{ + version: 2 + num: 15, +} +``` + +## codec.decodePack(emit) -> write + +Input in this is the raw buffer chunks in the packstream. The chunks can be +broken up at any point so this is ideal for streaming from a disk or network. + + +Version is the git pack protocol version, and num is the number of objects that +will be in this stream. + +All output objects after this will be raw git objects. + +```js +{ + type: type, + size: buffer-size, + body: raw-buffer, + offset: offset-in-stream, + [ref: number-or-hash] +} +``` + +There are two extra types here that aren't seen elsewhere. They are `ofs-delta` +and `ref-delta`. In both cases, these are a diff that applies on top of another +object in the stream. The different is `ofs-delta` stores a number in `ref` +that is the number of bytes to go back in the stream to find the base object. +But `ref-delta` includes the full hash of it's base object. + + +## codec.encodePack(emit) -> write + +This is the reverse. In fact, if you fed this the output from `decodePack`, +it's output should match exactly the original stream. diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js index f5974be..20ec8c0 100644 --- a/test/test-pack-codec.js +++ b/test/test-pack-codec.js @@ -42,6 +42,7 @@ function unpackStream(stream) { } if (!finished) throw new Error("unpack stream didn't finish"); if (out.length !== meta.num) throw new Error("Item num mismatch"); + console.log(out); return out; } From bccd2f9cfa057afba5140366b6e59bf9f4be21a2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 23:26:29 +0000 Subject: [PATCH 132/256] Make meta doc generic --- doc/lib/pack-codec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/lib/pack-codec.md b/doc/lib/pack-codec.md index d0d2131..2e863ba 100644 --- a/doc/lib/pack-codec.md +++ b/doc/lib/pack-codec.md @@ -43,7 +43,7 @@ The first output is the meta object: ```js { version: 2 - num: 15, + num: num-of-objects, } ``` From b36daa23a6a5e956aabc5b28a180acc329bbb301 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 23:28:27 +0000 Subject: [PATCH 133/256] Quiet noisy test --- test/test-pack-codec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js index 20ec8c0..f5974be 100644 --- a/test/test-pack-codec.js +++ b/test/test-pack-codec.js @@ -42,7 +42,6 @@ function unpackStream(stream) { } if (!finished) throw new Error("unpack stream didn't finish"); if (out.length !== meta.num) throw new Error("Item num mismatch"); - console.log(out); return out; } From f43c2292e67c0a2192383ffb3778b1cdefcd35f2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 6 Mar 2014 23:34:44 +0000 Subject: [PATCH 134/256] Document subset of data needed for pack encoder --- doc/lib/pack-codec.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/lib/pack-codec.md b/doc/lib/pack-codec.md index 2e863ba..b08a1f9 100644 --- a/doc/lib/pack-codec.md +++ b/doc/lib/pack-codec.md @@ -79,3 +79,20 @@ But `ref-delta` includes the full hash of it's base object. This is the reverse. In fact, if you fed this the output from `decodePack`, it's output should match exactly the original stream. + +The objects don't need as much data as the parser outputs. In specefic, the meta +object only need contain: + +```js +{ num: num-of-objects } +``` + +And the items only need contain: + +```js +{ + type: type, + body: raw-buffer, + [ref: number-or-hash] +} +``` From 2e1bf388e7c1dd9037c57a6f74a2b2e1ab5e8004 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 7 Mar 2014 04:56:05 +0000 Subject: [PATCH 135/256] Fix and test config-codec --- lib/config-codec.js | 46 ++++++++++++++++++++------------ test/test-config-codec.js | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 test/test-config-codec.js diff --git a/lib/config-codec.js b/lib/config-codec.js index 2aa1958..a049e4e 100644 --- a/lib/config-codec.js +++ b/lib/config-codec.js @@ -8,28 +8,40 @@ module.exports = { }; function encode(config) { - return Object.keys(config).map(function (name) { + var lines = []; + Object.keys(config).forEach(function (name) { var obj = config[name]; - for (var key in obj) { - if (typeof obj[key] !== "object") { - return '[' + name + ']\n' + encodeBody(obj); + var deep = {}; + var values = {}; + Object.keys(obj).forEach(function (key) { + var value = obj[key]; + if (typeof value === 'object') { + deep[key] = value; } - return Object.keys(obj).map(mapSub).join("\n"); - } - return ""; + else { + values[key] = value; + } + }); + lines.push('[' + name + ']'); + encodeBody(values); - function mapSub(sub) { - return '[' + name + ' "' + sub + '"]\n' + encodeBody(obj[sub]); - } - }).join("\n") + "\n"; -} + Object.keys(deep).forEach(function (sub) { + lines.push('[' + name + ' "' + sub + '"]'); + encodeBody(deep[sub]); + }); + }); + + return lines.join("\n") + "\n"; + + function encodeBody(obj) { + Object.keys(obj).forEach(function (name) { + lines.push( "\t" + name + " = " + obj[name]); + }); + } -function encodeBody(obj) { - return Object.keys(obj).map(function (name) { - return "\t" + name + " = " + obj[name]; - }).join("\n"); } + function decode(text) { var config = {}; var section; @@ -42,7 +54,7 @@ function decode(text) { } return; } - match = line.match(/([^ \t=]+)[ \t]*=[ \t]*([^ \t]+)/); + match = line.match(/([^ \t=]+)[ \t]*=[ \t]*(.+)/); if (match) { section[match[1]] = match[2]; } diff --git a/test/test-config-codec.js b/test/test-config-codec.js new file mode 100644 index 0000000..9f93e6a --- /dev/null +++ b/test/test-config-codec.js @@ -0,0 +1,56 @@ +var run = require('./run.js'); + +// The thing we mean to test. +var codec = require('../lib/config-codec.js'); + +var sample = '\ +[user]\n\ +\tname = Tim Caswell\n\ +\temail = tim@creationix.com\n\ +[core]\n\ +\teditor = vim\n\ +\twhitespace = fix,-indent-with-non-tab,trailing-space,cr-at-eol\n\ +[web]\n\ +\tbrowser = google-chrome\n\ +[color]\n\ +\tui = true\n\ +[color "branch"]\n\ +\tcurrent = yellow bold\n\ +\tlocal = green bold\n\ +\tremote = cyan bold\n\ +[color "diff"]\n\ +\tmeta = yellow bold\n\ +\tfrag = magenta bold\n\ +\told = red bold\n\ +\tnew = green bold\n\ +\twhitespace = red reverse\n\ +[github]\n\ +\tuser = creationix\n\ +\ttoken = token'; + +var config; + +run([ + function testDecode() { + config = codec.decode(sample); + if (config.user.name !== "Tim Caswell") { + throw new Error("Failed to parse user.name"); + } + if (config.color.ui != "true") { + throw new Error("Failed to parse color.ui"); + } + if (config.color.diff.meta !== "yellow bold") { + throw new Error("Failed to parse color.diff.meta"); + } + }, + function testEncode() { + var encoded = codec.encode(config); + var config2 = codec.decode(encoded); + if (JSON.stringify(config) !== JSON.stringify(config2)) { + console.log(config); + console.log(encoded); + console.log(config2); + throw new Error("Encode failed"); + } + } +]); From d7adaad4e6f48f14bcd409a725959c512a9be85d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 7 Mar 2014 05:05:09 +0000 Subject: [PATCH 136/256] Document config-codec --- doc/lib/config-codec.md | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 doc/lib/config-codec.md diff --git a/doc/lib/config-codec.md b/doc/lib/config-codec.md new file mode 100644 index 0000000..4a95217 --- /dev/null +++ b/doc/lib/config-codec.md @@ -0,0 +1,46 @@ +# Config Codec + +This module implements a codec for reading and writing git config files (this +includes the .gitmodules file). As far as I can tell, this is a variant of +the INI format. + +## codec.decode(ini) -> config + +Given the text of the config file, return the data as an object. + +The following config: + +```ini +[user] + name = Tim Caswell + email = tim@creationix.com +[color] + ui = true +[color "branch"] + current = yellow bold + local = green bold + remote = cyan bold +``` + +Will parse to this js object + +```js +{ + user: { + name: "Tim Caswell", + email: "tim@creationix.com" + }, + color: { + ui: "true", + branch: { + current: "yellow bold", + local: "green bold", + remote: "cyan bold" + } + } +} +``` + +## codec.encode(config) -> ini + +This reverses the conversion and writes a string from a config object. \ No newline at end of file From 1a39b597faffdf0f74903908e918f4688079024f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 7 Mar 2014 06:11:11 +0000 Subject: [PATCH 137/256] Fix and Test pack-ops. --- mixins/{packops.js => pack-ops.js} | 68 +++++++++++++++++++----------- test/sample-pack.js | 5 +++ test/test-object-codec.js | 2 +- test/test-pack-codec.js | 5 +-- test/test-pack-ops.js | 53 +++++++++++++++++++++++ 5 files changed, 103 insertions(+), 30 deletions(-) rename mixins/{packops.js => pack-ops.js} (80%) create mode 100644 test/sample-pack.js create mode 100644 test/test-pack-ops.js diff --git a/mixins/packops.js b/mixins/pack-ops.js similarity index 80% rename from mixins/packops.js rename to mixins/pack-ops.js index 5698ecb..81d38b3 100644 --- a/mixins/packops.js +++ b/mixins/pack-ops.js @@ -1,12 +1,9 @@ var binary = require('bodec'); -var deframe = require('../lib/deframe.js'); -var frame = require('../lib/frame.js'); var sha1 = require('git-sha1'); var applyDelta = require('../lib/apply-delta.js'); -var pushToPull = require('push-to-pull'); +var codec = require('../lib/object-codec.js'); var decodePack = require('../lib/pack-codec.js').decodePack; -var packFrame = require('../lib/pack-codec.js').packFrame; -var packHeader = require('../lib/pack-codec.js').packHeader; +var encodePack = require('../lib/pack-codec.js').encodePack; module.exports = function (repo) { // packStream is a simple-stream containing raw packfile binary data @@ -23,7 +20,7 @@ module.exports = function (repo) { function unpack(packStream, opts, callback) { if (!callback) return unpack.bind(this, packStream, opts); - packStream = pushToPull(decodePack)(packStream); + packStream = applyParser(packStream, decodePack); var repo = this; @@ -89,9 +86,9 @@ function unpack(packStream, opts, callback) { return repo.loadRaw(item.ref, function (err, buffer) { if (err) return onDone(err); if (!buffer) return onDone(new Error("Missing base image at " + item.ref)); - var target = deframe(buffer); - item.type = target[0]; - item.body = applyDelta(item.body, target[1]); + var target = codec.deframe(buffer); + item.type = target.type; + item.body = applyDelta(item.body, target.body); return saveValue(item); }); } @@ -109,7 +106,7 @@ function unpack(packStream, opts, callback) { } function saveValue(item) { - var buffer = frame(item.type, item.body); + var buffer = codec.frame(item); var hash = sha1(buffer); hashes[item.offset] = hash; has[hash] = true; @@ -143,38 +140,27 @@ function unpack(packStream, opts, callback) { function pack(hashes, opts, callback) { if (!callback) return pack.bind(this, hashes, opts); var repo = this; - var sha1sum = sha1(); var i = 0, first = true, done = false; - return callback(null, { read: read, abort: callback }); + return callback(null, applyParser({ read: read, abort: callback }, encodePack)); function read(callback) { if (done) return callback(); if (first) return readFirst(callback); var hash = hashes[i++]; if (hash === undefined) { - var sum = sha1sum.digest(); - done = true; - return callback(null, binary.fromHex(sum)); + return callback(); } repo.loadRaw(hash, function (err, buffer) { if (err) return callback(err); if (!buffer) return callback(new Error("Missing hash: " + hash)); // Reframe with pack format header - var pair = deframe(buffer); - packFrame(pair[0], pair[1], function (err, buffer) { - if (err) return callback(err); - sha1sum.update(buffer); - callback(null, buffer); - }); + callback(null, codec.deframe(buffer)); }); } function readFirst(callback) { - var length = hashes.length; - var chunk = packHeader(length); first = false; - sha1sum.update(chunk); - callback(null, chunk); + callback(null, {num: hashes.length}); } } @@ -187,3 +173,35 @@ function values(object) { } return out; } + + +function applyParser(stream, parser) { + var write = parser(onData); + var cb = null; + var queue = []; + return { read: read, abort: stream.abort }; + + function read(callback) { + if (queue.length) return callback(null, queue.shift()); + if (cb) return callback(new Error("Only one read at a time.")); + cb = callback; + stream.read(onRead); + } + + function onRead(err, item) { + var callback = cb; + cb = null; + if (err) return callback(err); + try { + write(item); + } + catch (err) { + return callback(err); + } + return read(callback); + } + + function onData(item) { + queue.push(item); + } +} diff --git a/test/sample-pack.js b/test/sample-pack.js new file mode 100644 index 0000000..d90a99c --- /dev/null +++ b/test/sample-pack.js @@ -0,0 +1,5 @@ +var bodec = require('bodec'); + +// This is a small sample packfile with couple offset deltas +// pack-5851ce932ec42973b51d631afe25da247c3dc49a.pack +module.exports = bodec.fromBase64('UEFDSwAAAAIAAAAQnQ54nJ3MWwoCMQxA0f+uIhtQ0nYeKYgobsENZNoEC/OQMTK6e2cN/l4411YRYCo5kseITVLpSmAfOVLSnFJB6kJqSukDuSevMhu0moed9CmrKjKFwpIxtT7TINh2vSqReHX8tseywr1OcOPXJuMIJ6vTJa/CVpe5fo55mc7gY2p86LFBOGCH6PY6VTP5x7prKfAVA54Xe+yLWTbQOor7AZUCSPmRDnicnctRCgIhEADQf08xFyjGUVeFiKIrdAEdZ0lYd8OM7fh1hn4fvNFFQEi8JCcuCoWSmakwY8xoHGMxkdgimZjVM3VZB8wUPMUJLWrRPml0IdspuJl1JHJBSijGRlLpPR5bh3ttcEuvXZYFTqO2C3dJo25r/Rx5a2fQJlpNHgnhgBOi+mmrY8g/V11LgVV2mOsi6guDiEL9mA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKqQl4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kVmVeajlYifjVm28/SzW0d12ZKCB++trFC8ZKOxBKjMBqauylWlkm6kbyCrH0Gp01vHQ9NnMNAFftOrq1AXic80jNyclXCM8vyknhckxJUSjOz03lAgBQjAcOPXicS8zLL8lILVJIy8xJ5QIAI9cEvLEBeJyrTC1RSMzLL8lILVJIy8xJ5QIAOsAGLmWAPnicm8lYOqEUAAX6AhVkEHicKw2aEAQABEABqqoCeJwzNDAwMzFRyK1ML0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAxUhBDqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAPuQ9dqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xPHic80jNyclXCM8vyknhAgAcMgQnuZAj3ZpSLQckQi9VfpQYWt+hefM='); diff --git a/test/test-object-codec.js b/test/test-object-codec.js index 39826fe..73b1409 100644 --- a/test/test-object-codec.js +++ b/test/test-object-codec.js @@ -154,7 +154,7 @@ run([ if (tag.tagger.name !== obj.body.tagger.name || tag.tagger.email !== obj.body.tagger.email || tag.message !== obj.body.message) { - console.log([obj.body.author, obj.body.message]); + console.log([obj.body.tagger, obj.body.message]); throw new Error("Problem decoding utf8 parts in tag"); } }, diff --git a/test/test-pack-codec.js b/test/test-pack-codec.js index f5974be..4f6c1d2 100644 --- a/test/test-pack-codec.js +++ b/test/test-pack-codec.js @@ -6,10 +6,7 @@ var encoders = require('../lib/object-codec.js').encoders; // The thing we mean to test. var codec = require('../lib/pack-codec.js'); - -// This is a small sample packfile with couple offset deltas -// pack-5851ce932ec42973b51d631afe25da247c3dc49a.pack -var pack = bodec.fromBase64('UEFDSwAAAAIAAAAQnQ54nJ3MWwoCMQxA0f+uIhtQ0nYeKYgobsENZNoEC/OQMTK6e2cN/l4411YRYCo5kseITVLpSmAfOVLSnFJB6kJqSukDuSevMhu0moed9CmrKjKFwpIxtT7TINh2vSqReHX8tseywr1OcOPXJuMIJ6vTJa/CVpe5fo55mc7gY2p86LFBOGCH6PY6VTP5x7prKfAVA54Xe+yLWTbQOor7AZUCSPmRDnicnctRCgIhEADQf08xFyjGUVeFiKIrdAEdZ0lYd8OM7fh1hn4fvNFFQEi8JCcuCoWSmakwY8xoHGMxkdgimZjVM3VZB8wUPMUJLWrRPml0IdspuJl1JHJBSijGRlLpPR5bh3ttcEuvXZYFTqO2C3dJo25r/Rx5a2fQJlpNHgnhgBOi+mmrY8g/V11LgVV2mOsi6guDiEL9mA94nJ3PTWrDMBBA4b1OMRdosDT6hRIKvkIuIMkjd6htGXVCkts3Z+j2wbd4MohA+5Cai874uiQXQmuIjagsAWMp3rWS0WCM6syDDgGbDCXEmhz5Zl00iayv2mpyHk2xVLVZlhJUvst3H3DjHeb8+6Btg0/h/asOysL94Oel9v0KGpPVxjtE+Jj8NKl33VmE/mPV3M8XrO8x4WOFkusPSIc+eOUjb9B4I/UHHmNMM5QOeJydy1sKwjAQRuH3rGI2oGQmlzYgIrgDcQNp8hcDTSsxostXt+B5/OD0BpAzMJmzJJs4J5Fh5OiCsB3nMFvoOAakkaHusWHtpJm1y9YYb4KXSawgR/GY9MQ+8OB/TZhVfPbb1uhaKp3j44VloUMv9ZQaYi/bWt77tNUjsQmWxTttaae91uqrtfSOf151wRorqN9Ac1mgPgYNRBeSDnicncvdCcIwEADg90xxCyiXn6YGRBRXcIG75IKBppX2xI6vM/j6waerCGAozFyCiA2Jx+QJh5Rd8l5cHUiSdcVTzeZFq8wKY5TkamYsIWO1Xkau8VRdHNhF5BLJsUWqht76XFZ4tA532j4yTXDW1q95FdK2zG0/5qVfwPoUrIshWThgRDQ/7U1V/rnmVgpsSxdQ2dV8AbwRRT6TC3icnczNCQIxEEDhe6qYBpT8JwuyCHvybgOTmOBAsoE4ouUrluD1wfd4lgLexpqjL9G5kG6YUtY56ohqccbVaEzQoaLXAp98HxOu1GHDx6u0Biemfs6zINPY6X3Mo6+gzGKV9jYEOEgvpfjWTszlHysuOzFhg+03ER9fQDcKqQl4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcMoSoy60kVmVeajlYifjVm28/SzW0d12ZKCB++trFC8ZKOxBKjMBqauylWlkm6kbyCrH0Gp01vHQ9NnMNAFftOrq1AXic80jNyclXCM8vyknhckxJUSjOz03lAgBQjAcOPXicS8zLL8lILVJIy8xJ5QIAI9cEvLEBeJyrTC1RSMzLL8lILVJIy8xJ5QIAOsAGLmWAPnicm8lYOqEUAAX6AhVkEHicKw2aEAQABEABqqoCeJwzNDAwMzFRyK1ML0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAxUhBDqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhEHwQ5TRdT6bE+tY/8blzjRyr9lYcAgAPuQ9dqAJ4nDM0MDAzMVFIL0pNLcnMS9crqShhCK3dYPty+oksL6Y+ub1WMq+Voh9ZAAAZvA8xPHic80jNyclXCM8vyknhAgAcMgQnuZAj3ZpSLQckQi9VfpQYWt+hefM='); +var pack = require('./sample-pack.js'); var items = []; var newPack; diff --git a/test/test-pack-ops.js b/test/test-pack-ops.js new file mode 100644 index 0000000..b0c4068 --- /dev/null +++ b/test/test-pack-ops.js @@ -0,0 +1,53 @@ +var run = require('./run.js'); +var bodec = require('bodec'); +var sha1 = require('git-sha1'); +var codec = require('../lib/object-codec.js'); + +var repo = {}; +require('../mixins/mem-db.js')(repo); + +var pack = require('./sample-pack.js'); +var hashes; + +run([ + function setup() { + require('../mixins/pack-ops.js')(repo); + }, + function testUnpack(end) { + repo.unpack(singleStream(pack), { + onProgress: onProgress + }, function (err, result) { + if (err) return end(err); + hashes = result; + end(); + }); + function onProgress(progress) { + console.log(progress); + } + }, + function testPack(end) { + var stream; + repo.pack(hashes, {}, function (err, result) { + if (err) return end(err); + stream = result; + stream.read(onRead); + }); + function onRead(err, item) { + if (err) return end(err); + console.log(item); + if (item) stream.read(onRead); + else end(); + } + + } +]); + +function singleStream(item) { + var done = false; + return { read: function (callback) { + if (done) return callback(); + done = true; + callback(null, item); + }}; + +} \ No newline at end of file From e7ccaf07cbf6182b6d70d61e41e22a8a151e5a7a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 7 Mar 2014 06:17:59 +0000 Subject: [PATCH 138/256] Make test quieter --- test/test-pack-ops.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/test-pack-ops.js b/test/test-pack-ops.js index b0c4068..f9a1330 100644 --- a/test/test-pack-ops.js +++ b/test/test-pack-ops.js @@ -1,7 +1,5 @@ var run = require('./run.js'); var bodec = require('bodec'); -var sha1 = require('git-sha1'); -var codec = require('../lib/object-codec.js'); var repo = {}; require('../mixins/mem-db.js')(repo); @@ -22,21 +20,26 @@ run([ end(); }); function onProgress(progress) { - console.log(progress); + // console.log(progress); } }, function testPack(end) { var stream; + var parts = []; repo.pack(hashes, {}, function (err, result) { if (err) return end(err); stream = result; stream.read(onRead); }); - function onRead(err, item) { + function onRead(err, chunk) { if (err) return end(err); - console.log(item); - if (item) stream.read(onRead); - else end(); + // console.log(chunk); + if (chunk) { + parts.push(chunk); + return stream.read(onRead); + } + + end(); } } From e0b7c837d5eadb9bcb17847d6faf203215ac8650 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 7 Mar 2014 06:20:15 +0000 Subject: [PATCH 139/256] Clean up packops test --- test/test-pack-ops.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test-pack-ops.js b/test/test-pack-ops.js index f9a1330..523b850 100644 --- a/test/test-pack-ops.js +++ b/test/test-pack-ops.js @@ -1,5 +1,4 @@ var run = require('./run.js'); -var bodec = require('bodec'); var repo = {}; require('../mixins/mem-db.js')(repo); @@ -17,6 +16,9 @@ run([ }, function (err, result) { if (err) return end(err); hashes = result; + if (hashes.length !== 16) { + return end(new Error("Wrong number of objects unpacked")); + } end(); }); function onProgress(progress) { @@ -38,10 +40,8 @@ run([ parts.push(chunk); return stream.read(onRead); } - end(); } - } ]); @@ -52,5 +52,4 @@ function singleStream(item) { done = true; callback(null, item); }}; - } \ No newline at end of file From 7202028db7817e84eac47496b54423e4b2b4f18c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 7 Mar 2014 07:04:22 +0000 Subject: [PATCH 140/256] Add basic docs for pack-ops --- doc/mixins/pack-ops.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 doc/mixins/pack-ops.md diff --git a/doc/mixins/pack-ops.md b/doc/mixins/pack-ops.md new file mode 100644 index 0000000..09d074f --- /dev/null +++ b/doc/mixins/pack-ops.md @@ -0,0 +1,37 @@ +# pack-ops mixin + +This mixin adds the ability to consume or create packfile streams. + +This depends on the repo already having: + + - `loadRaw(hash) => raw-binary` + - `saveRaw(hash, raw-binary) =>` + +And then adds: + + - `unpack(stream, opts) => hashes` + - `pack(hashes, opts) => stream` + +The streams are simple-stream format. This means they have a `.read(callback)` +method for pulling items out of the stream. + +Example: + +```js +var packOps = require('js-git/mixins/pack-ops'); +packOps(repo); + +repo.unpack(stream, opts, function (err, hashes) { + // hashes is imported objects +}); + +repo.pack(hashes, opts, function (err, stream) { + if (err) throw err; + stream.read(onRead); + function onRead(err, item) { + if (err) throw err; + console.log(item); + if (item) stream.read(onRead); + } +}); +``` From 1ca7d165f6fa8d3e3e6b127b3de10d732cab1625 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 2 Apr 2014 18:01:04 +0000 Subject: [PATCH 141/256] Move normalize to formats mixin and fix idb, memcache and githubdb. --- lib/object-codec.js | 84 --------------------------------- mixins/formats.js | 110 ++++++++++++++++++++++++++++++++++++++++++- mixins/github-db.js | 22 ++++++--- mixins/indexed-db.js | 10 ++-- mixins/mem-cache.js | 13 ++--- 5 files changed, 135 insertions(+), 104 deletions(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index 8588d35..70eaf31 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -31,7 +31,6 @@ function encodeBlob(body) { } function encodeTree(body) { - body = normalizeTree(body); var tree = ""; var names = Object.keys(body).sort(); for (var i = 0, l = names.length; i < l; i++) { @@ -44,7 +43,6 @@ function encodeTree(body) { } function encodeTag(body) { - body = normalizeTag(body); var str = "object " + body.object + "\ntype " + body.type + "\ntag " + body.tag + @@ -54,7 +52,6 @@ function encodeTag(body) { } function encodeCommit(body) { - body = normalizeCommit(body); var str = "tree " + body.tree; for (var i = 0, l = body.parents.length; i < l; ++i) { str += "\nparent " + body.parents[i]; @@ -65,87 +62,6 @@ function encodeCommit(body) { return bodec.fromUnicode(str); } -function normalizeTree(body) { - var type = body && typeof body; - if (type !== "object") { - throw new TypeError("Tree body must be array or object"); - } - var tree = {}, i, l, entry; - // If array form is passed in, convert to object form. - if (Array.isArray(body)) { - for (i = 0, l = body.length; i < l; i++) { - entry = body[i]; - tree[entry.name] = { - mode: entry.mode, - hash: entry.hash - }; - } - } - else { - var names = Object.keys(body); - for (i = 0, l = names.length; i < l; i++) { - var name = names[i]; - entry = body[name]; - tree[name] = { - mode: entry.mode, - hash: entry.hash - }; - } - } - return tree; -} - -function normalizeCommit(body) { - if (!body || typeof body !== "object") { - throw new TypeError("Commit body must be an object"); - } - if (!(body.tree && body.author && body.message)) { - throw new TypeError("Tree, author, and message are required for commits"); - } - var parents = body.parents || (body.parent ? [ body.parent ] : []); - if (!Array.isArray(parents)) { - throw new TypeError("Parents must be an array"); - } - var author = normalizePerson(body.author); - var committer = body.committer ? normalizePerson(body.committer) : author; - return { - tree: body.tree, - parents: parents, - author: author, - committer: committer, - message: body.message - }; -} - -function normalizeTag(body) { - if (!body || typeof body !== "object") { - throw new TypeError("Tag body must be an object"); - } - if (!(body.object && body.type && body.tag && body.tagger && body.message)) { - throw new TypeError("Object, type, tag, tagger, and message required"); - } - return { - object: body.object, - type: body.type, - tag: body.tag, - tagger: normalizePerson(body.tagger), - message: body.message - }; -} - -function normalizePerson(person) { - if (!person || typeof person !== "object") { - throw new TypeError("Person must be an object"); - } - if (!person.name || !person.email) { - throw new TypeError("Name and email are required for person fields"); - } - return { - name: person.name, - email: person.email, - date: person.date || new Date() - }; -} function formatPerson(person) { return safe(person.name) + diff --git a/mixins/formats.js b/mixins/formats.js index 30c7efd..44ab90e 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -1,10 +1,13 @@ "use strict"; -var binary = require('bodec'); +var bodec = require('bodec'); module.exports = function (repo) { var loadAs = repo.loadAs; repo.loadAs = newLoadAs; + var saveAs = repo.saveAs; + repo.saveAs = newSaveAs; + function newLoadAs(type, hash, callback) { var realType = type === "text" ? "blob": type === "array" ? "tree" : type; @@ -12,11 +15,32 @@ module.exports = function (repo) { function onLoad(err, body, hash) { if (body === undefined) return callback(err); - if (type === "text") body = binary.toUnicode(body); + if (type === "text") body = bodec.toUnicode(body); if (type === "array") body = toArray(body); return callback(err, body, hash); } } + + function newSaveAs(type, body, callback) { + type = type === "text" ? "blob": + type === "array" ? "tree" : type; + if (type === "blob") { + if (typeof body === "string") { + body = bodec.fromUnicode(body); + } + } + else if (type === "tree") { + body = normalizeTree(body); + } + else if (type === "commit") { + body = normalizeCommit(body); + } + else if (type === "tag") { + body = normalizeTag(body); + } + return saveAs.call(repo, type, body, callback); + } + }; function toArray(tree) { @@ -26,3 +50,85 @@ function toArray(tree) { return entry; }); } + +function normalizeTree(body) { + var type = body && typeof body; + if (type !== "object") { + throw new TypeError("Tree body must be array or object"); + } + var tree = {}, i, l, entry; + // If array form is passed in, convert to object form. + if (Array.isArray(body)) { + for (i = 0, l = body.length; i < l; i++) { + entry = body[i]; + tree[entry.name] = { + mode: entry.mode, + hash: entry.hash + }; + } + } + else { + var names = Object.keys(body); + for (i = 0, l = names.length; i < l; i++) { + var name = names[i]; + entry = body[name]; + tree[name] = { + mode: entry.mode, + hash: entry.hash + }; + } + } + return tree; +} + +function normalizeCommit(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Commit body must be an object"); + } + if (!(body.tree && body.author && body.message)) { + throw new TypeError("Tree, author, and message are required for commits"); + } + var parents = body.parents || (body.parent ? [ body.parent ] : []); + if (!Array.isArray(parents)) { + throw new TypeError("Parents must be an array"); + } + var author = normalizePerson(body.author); + var committer = body.committer ? normalizePerson(body.committer) : author; + return { + tree: body.tree, + parents: parents, + author: author, + committer: committer, + message: body.message + }; +} + +function normalizeTag(body) { + if (!body || typeof body !== "object") { + throw new TypeError("Tag body must be an object"); + } + if (!(body.object && body.type && body.tag && body.tagger && body.message)) { + throw new TypeError("Object, type, tag, tagger, and message required"); + } + return { + object: body.object, + type: body.type, + tag: body.tag, + tagger: normalizePerson(body.tagger), + message: body.message + }; +} + +function normalizePerson(person) { + if (!person || typeof person !== "object") { + throw new TypeError("Person must be an object"); + } + if (!person.name || !person.email) { + throw new TypeError("Name and email are required for person fields"); + } + return { + name: person.name, + email: person.email, + date: person.date || new Date() + }; +} diff --git a/mixins/github-db.js b/mixins/github-db.js index 553de37..900ddda 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -1,10 +1,10 @@ "use strict"; -var normalizeAs = require('../lib/encoders').normalizeAs; -var hashAs = require('../lib/encoders').hashAs; var modes = require('../lib/modes'); var xhr = require('../lib/xhr'); var binary = require('bodec'); +var sha1 = require('git-sha1'); +var frame = require('../lib/object-codec').frame; var modeToType = { "040000": "tree", @@ -28,9 +28,15 @@ var decoders = { blob: decodeBlob, }; -var emptyBlob = hashAs("blob", ""); -var emptyTree = hashAs("tree", []); - +// Precompute hashes for empty blob and empty tree since github won't +var emptyBlob = sha1(frame({ + type: "blob", + body: binary.create(0) +})); +var emptyTree = sha1(frame({ + type: "tree", + body: binary.create(0) +})); // Implement the js-git object interface using github APIs module.exports = function (repo, root, accessToken) { @@ -82,7 +88,6 @@ module.exports = function (repo, root, accessToken) { function saveAs(type, body, callback) { var request; try { - body = normalizeAs(type, body); request = encoders[type](body); } catch (err) { @@ -437,3 +442,8 @@ function singleCall(callback) { return callback.apply(this, arguments); }; } + +function hashAs(type, body) { + var buffer = frame({type: type, body: body}); + return sha1(buffer); +} \ No newline at end of file diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index eada26d..207767b 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -1,7 +1,8 @@ "use strict"; /*global indexedDB*/ -var encoders = require('../lib/encoders.js'); +var codec = require('../lib/object-codec.js'); +var sha1 = require('git-sha1'); var modes = require('../lib/modes.js'); var db; @@ -59,8 +60,8 @@ function onError(evt) { function saveAs(type, body, callback, forcedHash) { var hash; try { - body = encoders.normalizeAs(type, body); - hash = forcedHash || encoders.hashAs(type, body); + var buffer = codec.frame({type:type,body:body}); + hash = forcedHash || sha1(buffer); } catch (err) { return callback(err); } var trans = db.transaction(["objects"], "readwrite"); @@ -84,9 +85,6 @@ function loadAs(type, hash, callback) { request.onsuccess = function(evt) { var entry = evt.target.result; if (!entry) return callback(); - if (type !== "blob") { - entry.body = encoders.normalizeAs(type, entry.body); - } if (type !== entry.type) { return callback(new TypeError("Type mismatch")); } diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index a1060b2..c52221e 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -1,5 +1,7 @@ "use strict"; -var normalizeAs = require('../lib/encoders.js').normalizeAs; + +var encoders = require('../lib/object-codec').encoders; +var decoders = require('../lib/object-codec').decoders; var cache = memCache.cache = {}; module.exports = memCache; @@ -12,9 +14,7 @@ function memCache(repo) { loadAs.call(repo, type, hash, function (err, value) { if (value === undefined) return callback(err); if (type !== "blob" || value.length < 100) { - if (type === "blob") value = new Uint8Array(value); - else deepFreeze(value); - cache[hash] = value; + cache[hash] = dupe(type, value); } return callback.apply(this, arguments); }); @@ -23,10 +23,10 @@ function memCache(repo) { var saveAs = repo.saveAs; repo.saveAs = saveAsCached; function saveAsCached(type, value, callback) { + value = dupe(type, value); saveAs.call(repo, type, value, function (err, hash, value) { if (err) return callback(err); if (type !== "blob" || value.length < 100) { - if (type === "blob") value = new Uint8Array(value); cache[hash] = value; } return callback(null, hash, value); @@ -36,9 +36,10 @@ function memCache(repo) { function dupe(type, value) { if (type === "blob") { + if (type.length >= 100) return value; return new Uint8Array(value); } - return normalizeAs(type, value); + return decoders[type](encoders[type](value)); } function deepFreeze(obj) { From 73a6c740616c6779fe4d1f7db69129b08fc17738 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 2 Apr 2014 18:18:12 +0000 Subject: [PATCH 142/256] Add failing test for #94 --- test/test-config-codec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/test-config-codec.js b/test/test-config-codec.js index 9f93e6a..40c719a 100644 --- a/test/test-config-codec.js +++ b/test/test-config-codec.js @@ -52,5 +52,18 @@ run([ console.log(config2); throw new Error("Encode failed"); } + }, + function testEncode2() { + var encoded = codec.encode({ + foo: { + bar: { + baz: true + } + } + }); + if (encoded !== '[foo "bar"]\tbaz = true\n') { + console.log(encoded); + throw new Error("Invalid encoding of single deep config"); + } } ]); From 7bf8da57b8e56d51ea7fd21faf986f0111bbc1b9 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 3 Apr 2014 19:06:55 +0000 Subject: [PATCH 143/256] Don't invent new empty sections when writing config. --- lib/config-codec.js | 14 +++++++++----- test/test-config-codec.js | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/config-codec.js b/lib/config-codec.js index a049e4e..e69264c 100644 --- a/lib/config-codec.js +++ b/lib/config-codec.js @@ -13,27 +13,31 @@ function encode(config) { var obj = config[name]; var deep = {}; var values = {}; + var hasValues = false; Object.keys(obj).forEach(function (key) { var value = obj[key]; if (typeof value === 'object') { deep[key] = value; } else { + hasValues = true; values[key] = value; } }); - lines.push('[' + name + ']'); - encodeBody(values); + if (hasValues) { + encodeBody('[' + name + ']', values); + } Object.keys(deep).forEach(function (sub) { - lines.push('[' + name + ' "' + sub + '"]'); - encodeBody(deep[sub]); + var child = deep[sub]; + encodeBody('[' + name + ' "' + sub + '"]', child); }); }); return lines.join("\n") + "\n"; - function encodeBody(obj) { + function encodeBody(header, obj) { + lines.push(header); Object.keys(obj).forEach(function (name) { lines.push( "\t" + name + " = " + obj[name]); }); diff --git a/test/test-config-codec.js b/test/test-config-codec.js index 40c719a..22640ba 100644 --- a/test/test-config-codec.js +++ b/test/test-config-codec.js @@ -61,7 +61,7 @@ run([ } } }); - if (encoded !== '[foo "bar"]\tbaz = true\n') { + if (encoded !== '[foo "bar"]\n\tbaz = true\n') { console.log(encoded); throw new Error("Invalid encoding of single deep config"); } From 77f1a82466c23dced0d0a68ccb97213a7edf4092 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 3 Apr 2014 19:14:43 +0000 Subject: [PATCH 144/256] Fix github-db date handling --- mixins/github-db.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mixins/github-db.js b/mixins/github-db.js index 900ddda..3df094d 100644 --- a/mixins/github-db.js +++ b/mixins/github-db.js @@ -332,7 +332,7 @@ function encodePerson(person) { return { name: person.name, email: person.email, - date: (person.date || new Date()).toISOString() + date: (new Date(person.date.seconds * 1000)).toISOString() }; } @@ -425,13 +425,16 @@ function parseDate(string) { // TODO: test this once GitHub adds timezone information var match = string.match(/(-?)([0-9]{2}):([0-9]{2})$/); var date = new Date(string); - date.timezoneOffset = 0; + var timezoneOffset = 0; if (match) { - date.timezoneOffset = (match[1] === "-" ? 1 : -1) * ( + timezoneOffset = (match[1] === "-" ? 1 : -1) * ( parseInt(match[2], 10) * 60 + parseInt(match[3], 10) ); } - return date; + return { + seconds: date.valueOf() / 1000, + offset: timezoneOffset + }; } function singleCall(callback) { From 8d879a826c451780e61cced91c75864e8ba4b57b Mon Sep 17 00:00:00 2001 From: Bryan Burgers Date: Thu, 17 Apr 2014 08:24:00 -0500 Subject: [PATCH 145/256] Fix blob type checking in object-codec In object-codec's encodeBlob method, fix the error message for when an invalid body is sent. It was something cryptic about missing methods; now it is the expected "Invalid body for blob". --- lib/object-codec.js | 2 +- test/test-object-codec.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index 70eaf31..5ac3376 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -26,7 +26,7 @@ function encodeBlob(body) { // Assume strings are normal unicode strings if (typeof body === "string") return bodec.fromUnicode(body); if (Array.isArray(body)) return bodec.fromArray(body); - if (bodec.isBinary) return body; + if (bodec.isBinary(body)) return body; throw new Error("Invalid body for blob"); } diff --git a/test/test-object-codec.js b/test/test-object-codec.js index 73b1409..6570dba 100644 --- a/test/test-object-codec.js +++ b/test/test-object-codec.js @@ -19,6 +19,24 @@ run([ throw new Error("Invalid blob hash"); } }, + function testEncodeBlobInvalidType() { + var correctExceptionThrown = false; + blob = { + thisis: 'Not array, binary, or text' + }; + try { + blobBin = codec.frame({type: "blob", body: blob}); + } + catch (exception) { + if (/invalid body/i.test(exception.message)) { + correctExceptionThrown = true; + } + } + + if (!correctExceptionThrown) { + throw new Error("Expected the correct exception when the blog was an invalid type"); + } + }, function testEncodeTree() { tree = { "greeting.txt": { From 4529d6127120dd75559a2d5987aa29b53a43b2de Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 21 Apr 2014 17:16:43 +0000 Subject: [PATCH 146/256] Make XHR timeout 8s instead of 4s. --- lib/xhr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/xhr.js b/lib/xhr.js index 740a56e..8516912 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -19,7 +19,7 @@ else { var done = false; var json; var xhr = new XMLHttpRequest(); - xhr.timeout = 4000; + xhr.timeout = 8000; xhr.open(method, 'https://api.github.com' + url, true); xhr.setRequestHeader("Authorization", "token " + accessToken); if (body) { From cf99fa58f66d7b5d3448d7ff13cbc5b77e34d475 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 21 Apr 2014 17:28:24 +0000 Subject: [PATCH 147/256] Move github-db to js-github repo. --- lib/xhr-node.js | 73 ------- lib/xhr.js | 55 ------ mixins/github-db.js | 452 -------------------------------------------- 3 files changed, 580 deletions(-) delete mode 100644 lib/xhr-node.js delete mode 100644 lib/xhr.js delete mode 100644 mixins/github-db.js diff --git a/lib/xhr-node.js b/lib/xhr-node.js deleted file mode 100644 index f7e2525..0000000 --- a/lib/xhr-node.js +++ /dev/null @@ -1,73 +0,0 @@ -var https = require('https'); -var statusCodes = require('http').STATUS_CODES; - -module.exports = function (root, accessToken) { - var cache = {}; - return function request(method, url, body, callback) { - if (typeof body === "function" && callback === undefined) { - callback = body; - body = undefined; - } - if (!callback) return request.bind(this, accessToken, method, url, body); - url = url.replace(":root", root); - - var json; - var headers = { - "User-Agent": "node.js" - }; - if (accessToken) { - headers["Authorization"] = "token " + accessToken; - } - if (body) { - headers["Content-Type"] = "application/json"; - try { json = JSON.stringify(body); } - catch (err) { return callback(err); } - } - if (method === "GET") { - var cached = cache[url]; - if (cached) { - headers["If-None-Match"] = cached.etag; - } - } - var options = { - hostname: "api.github.com", - path: url, - method: method, - headers: headers - }; - var req = https.request(options, function (res) { - var body = []; - res.on("data", function (chunk) { - body.push(chunk); - }); - res.on("end", function () { - body = Buffer.concat(body).toString(); - console.log(method, url, res.statusCode); - console.log("Rate limit %s/%s left", res.headers['x-ratelimit-remaining'], res.headers['x-ratelimit-limit']); - if (res.statusCode === 200 && method === "GET" && /\/refs\//.test(url)) { - cache[url] = { - body: body, - etag: res.headers.etag - }; - } - else if (res.statusCode === 304) { - body = cache[url].body; - res.statusCode = 200; - } - // Fake parts of the xhr object using node APIs - var xhr = { - status: res.statusCode, - statusText: res.statusCode + " " + statusCodes[res.statusCode] - }; - var response = {message:body}; - if (body){ - try { response = JSON.parse(body); } - catch (err) {} - } - return callback(null, xhr, response); - }); - }); - req.end(json); - req.on("error", callback); - }; -}; diff --git a/lib/xhr.js b/lib/xhr.js deleted file mode 100644 index 8516912..0000000 --- a/lib/xhr.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -// Node.js https module -if (typeof process === 'object' && typeof process.versions === 'object' && process.versions.node) { - var nodeRequire = require; // Prevent mine.js from seeing this require - module.exports = nodeRequire('./xhr-node.js'); -} - -// Browser XHR -else { - module.exports = function (root, accessToken) { - return function request(method, url, body, callback) { - if (typeof body === "function" && callback === undefined) { - callback = body; - body = undefined; - } - url = url.replace(":root", root); - if (!callback) return request.bind(this, accessToken, method, url, body); - var done = false; - var json; - var xhr = new XMLHttpRequest(); - xhr.timeout = 8000; - xhr.open(method, 'https://api.github.com' + url, true); - xhr.setRequestHeader("Authorization", "token " + accessToken); - if (body) { - xhr.setRequestHeader("Content-Type", "application/json"); - try { json = JSON.stringify(body); } - catch (err) { return callback(err); } - } - xhr.ontimeout = onTimeout; - xhr.onreadystatechange = onReadyStateChange; - xhr.send(json); - - function onReadyStateChange() { - if (done) return; - if (xhr.readyState !== 4) return; - // Give onTimeout a chance to run first if that's the reason status is 0. - if (!xhr.status) return setTimeout(onReadyStateChange, 0); - done = true; - var response = {message:xhr.responseText}; - if (xhr.responseText){ - try { response = JSON.parse(xhr.responseText); } - catch (err) {} - } - return callback(null, xhr, response); - } - - function onTimeout() { - if (done) return; - done = true; - callback(new Error("Timeout requesting " + url)); - } - }; - }; -} \ No newline at end of file diff --git a/mixins/github-db.js b/mixins/github-db.js deleted file mode 100644 index 3df094d..0000000 --- a/mixins/github-db.js +++ /dev/null @@ -1,452 +0,0 @@ -"use strict"; - -var modes = require('../lib/modes'); -var xhr = require('../lib/xhr'); -var binary = require('bodec'); -var sha1 = require('git-sha1'); -var frame = require('../lib/object-codec').frame; - -var modeToType = { - "040000": "tree", - "100644": "blob", // normal file - "100755": "blob", // executable file - "120000": "blob", // symlink - "160000": "commit" // gitlink -}; - -var encoders = { - commit: encodeCommit, - tag: encodeTag, - tree: encodeTree, - blob: encodeBlob -}; - -var decoders = { - commit: decodeCommit, - tag: decodeTag, - tree: decodeTree, - blob: decodeBlob, -}; - -// Precompute hashes for empty blob and empty tree since github won't -var emptyBlob = sha1(frame({ - type: "blob", - body: binary.create(0) -})); -var emptyTree = sha1(frame({ - type: "tree", - body: binary.create(0) -})); - -// Implement the js-git object interface using github APIs -module.exports = function (repo, root, accessToken) { - - var apiRequest = xhr(root, accessToken); - - repo.loadAs = loadAs; // (type, hash) -> value, hash - repo.saveAs = saveAs; // (type, value) -> hash, value - repo.readRef = readRef; // (ref) -> hash - repo.updateRef = updateRef; // (ref, hash) -> hash - repo.createTree = createTree; // (entries) -> hash, tree - repo.hasHash = hasHash; - - function loadAs(type, hash, callback) { - // Github doesn't like empty trees, but we know them already. - if (type === "tree" && hash === emptyTree) return callback(null, {}, hash); - apiRequest("GET", "/repos/:root/git/" + type + "s/" + hash, onValue); - - function onValue(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 500) { - return callback(new Error("Invalid HTTP response: " + xhr.statusCode + " " + result.message)); - } - if (xhr.status >= 300 && xhr.status < 500) return callback(); - var body; - try { body = decoders[type].call(repo, result); } - catch (err) { return callback(err); } - if (hashAs(type, body) !== hash) { - if (fixDate(type, body, hash)) console.log(type + " repaired", hash); - // else console.warn("Unable to repair " + type, hash); - } - return callback(null, body, hash); - } - } - - function hasHash(type, hash, callback) { - apiRequest("GET", "/repos/:root/git/" + type + "s/" + hash, onValue); - - function onValue(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 500) { - return callback(new Error("Invalid HTTP response: " + xhr.statusCode + " " + result.message)); - } - if (xhr.status >= 300 && xhr.status < 500) return callback(null, false); - callback(null, true); - } - } - - function saveAs(type, body, callback) { - var request; - try { - request = encoders[type](body); - } - catch (err) { - return callback(err); - } - - // Github doesn't allow creating empty trees. - if (type === "tree" && request.tree.length === 0) { - return callback(null, emptyTree, body); - } - return apiRequest("POST", "/repos/:root/git/" + type + "s", request, onWrite); - - function onWrite(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - return callback(null, result.sha, body); - } - } - - // Create a tree with optional deep paths and create new blobs. - // Entries is an array of {mode, path, hash|content} - // Also deltas can be specified by setting entries.base to the hash of a tree - // in delta mode, entries can be removed by specifying just {path} - function createTree(entries, callback) { - var toDelete = entries.base && entries.filter(function (entry) { - return !entry.mode; - }).map(function (entry) { - return entry.path; - }); - if (toDelete && toDelete.length) return slowUpdateTree(entries, toDelete, callback); - return fastUpdateTree(entries, callback); - } - - function fastUpdateTree(entries, callback) { - var request = { tree: entries.map(mapTreeEntry) }; - if (entries.base) request.base_tree = entries.base; - - apiRequest("POST", "/repos/:root/git/trees", request, onWrite); - - function onWrite(err, xhr, result) { - if (err) return callback(err); - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - return callback(null, result.sha, decoders.tree(result)); - } - } - - // Github doesn't support deleting entries via the createTree API, so we - // need to manually create those affected trees and modify the request. - function slowUpdateTree(entries, toDelete, callback) { - callback = singleCall(callback); - var root = entries.base; - - var left = 0; - - // Calculate trees that need to be re-built and save any provided content. - var parents = {}; - toDelete.forEach(function (path) { - var parentPath = path.substr(0, path.lastIndexOf("/")); - var parent = parents[parentPath] || (parents[parentPath] = { - add: {}, del: [] - }); - var name = path.substr(path.lastIndexOf("/") + 1); - parent.del.push(name); - }); - var other = entries.filter(function (entry) { - if (!entry.mode) return false; - var parentPath = entry.path.substr(0, entry.path.lastIndexOf("/")); - var parent = parents[parentPath]; - if (!parent) return true; - var name = entry.path.substr(entry.path.lastIndexOf("/") + 1); - if (entry.hash) { - parent.add[name] = { - mode: entry.mode, - hash: entry.hash - }; - return false; - } - left++; - repo.saveAs("blob", entry.content, function(err, hash) { - if (err) return callback(err); - parent.add[name] = { - mode: entry.mode, - hash: hash - }; - if (!--left) onParents(); - }); - return false; - }); - if (!left) onParents(); - - function onParents() { - Object.keys(parents).forEach(function (parentPath) { - left++; - // TODO: remove this dependency on pathToEntry - repo.pathToEntry(root, parentPath, function (err, entry) { - if (err) return callback(err); - var tree = entry.tree; - var commands = parents[parentPath]; - commands.del.forEach(function (name) { - delete tree[name]; - }); - for (var name in commands.add) { - tree[name] = commands.add[name]; - } - repo.saveAs("tree", tree, function (err, hash, tree) { - if (err) return callback(err); - other.push({ - path: parentPath, - hash: hash, - mode: modes.tree - }); - if (!--left) { - other.base = entries.base; - if (other.length === 1 && other[0].path === "") { - return callback(null, hash, tree); - } - fastUpdateTree(other, callback); - } - }); - }); - }); - } - } - - - function readRef(ref, callback) { - if (ref === "HEAD") ref = "refs/heads/master"; - if (!(/^refs\//).test(ref)) { - return callback(new TypeError("Invalid ref: " + ref)); - } - return apiRequest("GET", "/repos/:root/git/" + ref, onRef); - - function onRef(err, xhr, result) { - if (err) return callback(err); - if (xhr.status === 404) return callback(); - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - return callback(null, result.object.sha); - } - } - - function updateRef(ref, hash, callback) { - if (!(/^refs\//).test(ref)) { - return callback(new Error("Invalid ref: " + ref)); - } - return apiRequest("PATCH", "/repos/:root/git/" + ref, { - sha: hash - }, onResult); - - function onResult(err, xhr, result) { - if (err) return callback(err); - if (xhr.status === 422 && result.message === "Reference does not exist") { - return apiRequest("POST", "/repos/:root/git/refs", { - ref: ref, - sha: hash - }, onResult); - } - if (xhr.status < 200 || xhr.status >= 300) { - return callback(new Error("Invalid HTTP response: " + xhr.status + " " + result.message)); - } - if (err) return callback(err); - callback(null, hash); - } - - } - -}; - -// GitHub has a nasty habit of stripping whitespace from messages and loosing -// the timezone. This information is required to make our hashes match up, so -// we guess it by mutating the value till the hash matches. -// If we're unable to match, we will just force the hash when saving to the cache. -function fixDate(type, value, hash) { - if (type !== "commit" && type !== "tag") return; - // Add up to 2 extra newlines and try all 30-minutes timezone offsets. - for (var x = 0; x < 3; x++) { - for (var i = -720; i < 720; i += 30) { - if (type === "commit") { - value.author.date.timezoneOffset = i; - value.committer.date.timezoneOffset = i; - } - else if (type === "tag") { - value.tagger.date.timezoneOffset = i; - } - if (hash === hashAs(type, value)) return true; - } - value.message += "\n"; - } - // Guessing failed, remove the temporary offset. - if (type === "commit") { - delete value.author.date.timezoneOffset; - delete value.committer.date.timezoneOffset; - } - else if (type === "tag") { - delete value.tagger.date.timezoneOffset; - } -} - -function mapTreeEntry(entry) { - if (!entry.mode) throw new TypeError("Invalid entry"); - var mode = modeToString(entry.mode); - var item = { - path: entry.path, - mode: mode, - type: modeToType[mode] - }; - // Magic hash for empty file since github rejects empty contents. - if (entry.content === "") entry.hash = emptyBlob; - - if (entry.hash) item.sha = entry.hash; - else item.content = entry.content; - return item; -} - -function encodeCommit(commit) { - var out = {}; - out.message = commit.message; - out.tree = commit.tree; - if (commit.parents) out.parents = commit.parents; - else if (commit.parent) out.parents = [commit.parent]; - else commit.parents = []; - if (commit.author) out.author = encodePerson(commit.author); - if (commit.committer) out.committer = encodePerson(commit.committer); - return out; -} - -function encodeTag(tag) { - return { - tag: tag.tag, - message: tag.message, - object: tag.object, - tagger: encodePerson(tag.tagger) - }; -} - -function encodePerson(person) { - return { - name: person.name, - email: person.email, - date: (new Date(person.date.seconds * 1000)).toISOString() - }; -} - -function encodeTree(tree) { - return { - tree: Object.keys(tree).map(function (name) { - var entry = tree[name]; - var mode = modeToString(entry.mode); - return { - path: name, - mode: mode, - type: modeToType[mode], - sha: entry.hash - }; - }) - }; -} - -function encodeBlob(blob) { - if (typeof blob === "string") return { - content: binary.encodeUtf8(blob), - encoding: "utf-8" - }; - if (binary.isBinary(blob)) return { - content: binary.toBase64(blob), - encoding: "base64" - }; - throw new TypeError("Invalid blob type, must be binary of string"); -} - -function modeToString(mode) { - var string = mode.toString(8); - // Github likes all modes to be 6 chars long - if (string.length === 5) string = "0" + string; - return string; -} - -function decodeCommit(result) { - return { - tree: result.tree.sha, - parents: result.parents.map(function (object) { - return object.sha; - }), - author: pickPerson(result.author), - committer: pickPerson(result.committer), - message: result.message - }; -} - -function decodeTag(result) { - return { - object: result.object.sha, - type: result.object.type, - tag: result.tag, - tagger: pickPerson(result.tagger), - message: result.message - }; -} - -function decodeTree(result) { - var tree = {}; - result.tree.forEach(function (entry) { - tree[entry.path] = { - mode: parseInt(entry.mode, 8), - hash: entry.sha - }; - }); - return tree; -} - -function decodeBlob(result) { - if (result.encoding === 'base64') { - return binary.fromBase64(result.content.replace(/\n/g, '')); - } - if (result.encoding === 'utf-8') { - return binary.fromUtf8(result.content); - } - throw new Error("Unknown blob encoding: " + result.encoding); -} - -function pickPerson(person) { - return { - name: person.name, - email: person.email, - date: parseDate(person.date) - }; -} - -function parseDate(string) { - // TODO: test this once GitHub adds timezone information - var match = string.match(/(-?)([0-9]{2}):([0-9]{2})$/); - var date = new Date(string); - var timezoneOffset = 0; - if (match) { - timezoneOffset = (match[1] === "-" ? 1 : -1) * ( - parseInt(match[2], 10) * 60 + parseInt(match[3], 10) - ); - } - return { - seconds: date.valueOf() / 1000, - offset: timezoneOffset - }; -} - -function singleCall(callback) { - var done = false; - return function () { - if (done) return console.warn("Discarding extra callback"); - done = true; - return callback.apply(this, arguments); - }; -} - -function hashAs(type, body) { - var buffer = frame({type: type, body: body}); - return sha1(buffer); -} \ No newline at end of file From 3035d3d019083e0eef6b898cd7cb05484b63ff8a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 21 Apr 2014 18:07:57 +0000 Subject: [PATCH 148/256] Use pako for non-streaming inflate --- lib/deflate.js | 2 +- lib/inflate.js | 33 ++++++++++----------------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/lib/deflate.js b/lib/deflate.js index 072a130..e5f68a8 100644 --- a/lib/deflate.js +++ b/lib/deflate.js @@ -6,5 +6,5 @@ if (typeof process === "object" && typeof process.versions === "object" && proce }; } else { - module.exports = nodeRequire('pako/deflate').deflate; + module.exports = require('pako/deflate').deflate; } diff --git a/lib/inflate.js b/lib/inflate.js index 2cf2826..ecf2d03 100644 --- a/lib/inflate.js +++ b/lib/inflate.js @@ -1,23 +1,10 @@ -var inflateStream = require('./inflate-stream.js'); -var binary = require('bodec'); -module.exports = function inflate(buffer) { - var parts = []; - var done = false; - var push = inflateStream(onEmit, onUnused); - push(null, buffer); - push(); - if (!done) throw new Error("Missing input data"); - return binary.join(parts); - - function onEmit(err, chunk) { - if (err) throw err; - if (chunk) parts.push(chunk); - else done = true; - } - - function onUnused(extra) { - if (extra && extra.length && extra[0].length) { - throw new Error("Too much input data"); - } - } -}; +if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { + var nodeRequire = require; + var pako = nodeRequire('pako'); + module.exports = function (buffer) { + return new Buffer(pako.inflate(new Uint8Array(buffer))); + }; +} +else { + module.exports = require('pako/inflate').inflate; +} From 8b3ab90add47814fc884ff4e685b50ff67dfbe2a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 10:51:35 -0500 Subject: [PATCH 149/256] Make mem-db continuable friendly --- mixins/mem-db.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/mixins/mem-db.js b/mixins/mem-db.js index 9b516f8..eefeeb4 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -15,43 +15,45 @@ function mixin(repo) { repo.loadRaw = loadRaw; function saveAs(type, body, callback) { - makeAsync(function () { + return makeAsync(function () { var buffer = codec.frame({type:type,body:body}); var hash = sha1(buffer); objects[hash] = buffer; - return [hash, body]; + return hash; }, callback); } function saveRaw(hash, buffer, callback) { - makeAsync(function () { + return makeAsync(function () { objects[hash] = buffer; - return []; }, callback); } function loadAs(type, hash, callback) { - makeAsync(function () { + return makeAsync(function () { var buffer = objects[hash]; if (!buffer) return []; var obj = codec.deframe(buffer, true); if (obj.type !== type) throw new TypeError("Type mismatch"); - return [obj.body, hash]; + return obj.body; }, callback); } function loadRaw(hash, callback) { - makeAsync(function () { - return [objects[hash]]; + return makeAsync(function () { + return objects[hash]; }, callback); } } function makeAsync(fn, callback) { + if (!callback) return function (callback) { + return makeAsync(fn, callback); + }; defer(function () { var out; try { out = fn(); } catch (err) { return callback(err); } - callback.call(null, null, out[0], out[1]); + callback(null, out); }); } From 7054f1756e440e9c84adff269138ece2af6d1b83 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 11:12:34 -0500 Subject: [PATCH 150/256] Clean up walkers mixin --- mixins/walkers.js | 305 +++++++++++++++++++++++----------------------- 1 file changed, 152 insertions(+), 153 deletions(-) diff --git a/mixins/walkers.js b/mixins/walkers.js index d95cdbd..9752a88 100644 --- a/mixins/walkers.js +++ b/mixins/walkers.js @@ -1,153 +1,152 @@ -var modes = require('../lib/modes.js'); - -module.exports = function (repo) { - repo.logWalk = logWalk; // (hash-ish) => stream - repo.treeWalk = treeWalk; // (treeHash) => stream -}; -module.exports.walk = walk; - -function logWalk(hashish, callback) { - if (!callback) return logWalk.bind(this, hashish); - var last, seen = {}; - var repo = this; - return repo.readRef("shallow", onShallow); - - function onShallow(err, shallow) { - last = shallow; - resolveHashish(repo, hashish, onHash); - - } - - function onHash(err, hash) { - if (err) return callback(err); - return repo.loadAs("commit", hash, onLoad); - } - - function onLoad(err, commit, hash) { - if (commit === undefined) return callback(err); - commit.hash = hash; - seen[hash] = true; - return callback(null, walk(commit, scan, loadKey, compare)); - } - - function scan(commit) { - if (last === commit) return []; - return commit.parents.filter(function (hash) { - return !seen[hash]; - }); - } - - function loadKey(hash, callback) { - return repo.loadAs("commit", hash, function (err, commit) { - if (err) return callback(err); - commit.hash = hash; - if (hash === last) commit.last = true; - return callback(null, commit); - }); - } - -} - -function compare(commit, other) { - return commit.author.date < other.author.date; -} - -function treeWalk(hash, callback) { - if (!callback) return treeWalk.bind(this, hash); - var repo = this; - return repo.loadAs("tree", hash, onTree); - - function onTree(err, body, hash) { - if (!body) return callback(err || new Error("Missing tree " + hash)); - var tree = { - mode: modes.tree, - hash: hash, - body: body, - path: "/" - }; - return callback(null, walk(tree, treeScan, treeLoadKey, treeCompare)); - } - - function treeLoadKey(entry, callback) { - if (entry.mode !== modes.tree) return callback(null, entry); - var type = modes.toType(entry.mode); - return repo.loadAs(type, entry.hash, function (err, body) { - if (err) return callback(err); - entry.body = body; - return callback(null, entry); - }); - } - -} - -function treeScan(object) { - if (object.mode !== modes.tree) return []; - var tree = object.body; - return Object.keys(tree).map(function (name) { - var entry = tree[name]; - var path = object.path + name; - if (entry.mode === modes.tree) path += "/"; - return { - mode: entry.mode, - hash: entry.hash, - path: path - }; - }); -} - -function treeCompare(first, second) { - return first.path < second.path; -} - -function resolveHashish(repo, hashish, callback) { - if (/^[0-9a-f]{40}$/.test(hashish)) { - return callback(null, hashish); - } - repo.readRef(hashish, function (err, hash) { - if (!hash) return callback(err || new Error("Bad ref " + hashish)); - callback(null, hash); - }); -} - -function walk(seed, scan, loadKey, compare) { - var queue = [seed]; - var working = 0, error, cb; - return {read: read, abort: abort}; - - function read(callback) { - if (cb) return callback(new Error("Only one read at a time")); - if (working) { cb = callback; return; } - var item = queue.shift(); - if (!item) return callback(); - try { scan(item).forEach(onKey); } - catch (err) { return callback(err); } - return callback(null, item); - } - - function abort(callback) { return callback(); } - - function onError(err) { - if (cb) { - var callback = cb; cb = null; - return callback(err); - } - error = err; - } - - function onKey(key) { - working++; - loadKey(key, onItem); - } - - function onItem(err, item) { - working--; - if (err) return onError(err); - var index = queue.length; - while (index && compare(item, queue[index - 1])) index--; - queue.splice(index, 0, item); - if (!working && cb) { - var callback = cb; cb = null; - return read(callback); - } - } -} +var modes = require('../lib/modes.js'); + +module.exports = function (repo) { + repo.logWalk = logWalk; // (ref) => stream + repo.treeWalk = treeWalk; // (treeHash) => stream +}; +module.exports.walk = walk; + +function logWalk(ref, callback) { + if (!callback) return logWalk.bind(this, ref); + var last, seen = {}; + var repo = this; + if (!repo.readRef) return onShallow(); + return repo.readRef("shallow", onShallow); + + function onShallow(err, shallow) { + last = shallow; + resolveRef(repo, ref, onHash); + } + + function onHash(err, hash) { + if (err) return callback(err); + return repo.loadAs("commit", hash, function (err, commit) { + if (commit === undefined) return callback(err); + commit.hash = hash; + seen[hash] = true; + return callback(null, walk(commit, scan, loadKey, compare)); + }); + } + + function scan(commit) { + if (last === commit) return []; + return commit.parents.filter(function (hash) { + return !seen[hash]; + }); + } + + function loadKey(hash, callback) { + return repo.loadAs("commit", hash, function (err, commit) { + if (err) return callback(err); + commit.hash = hash; + if (hash === last) commit.last = true; + return callback(null, commit); + }); + } + +} + +function compare(commit, other) { + return commit.author.date < other.author.date; +} + +function treeWalk(hash, callback) { + if (!callback) return treeWalk.bind(this, hash); + var repo = this; + return repo.loadAs("tree", hash, onTree); + + function onTree(err, body) { + if (!body) return callback(err || new Error("Missing tree " + hash)); + var tree = { + mode: modes.tree, + hash: hash, + body: body, + path: "/" + }; + return callback(null, walk(tree, treeScan, treeLoadKey, treeCompare)); + } + + function treeLoadKey(entry, callback) { + if (entry.mode !== modes.tree) return callback(null, entry); + var type = modes.toType(entry.mode); + return repo.loadAs(type, entry.hash, function (err, body) { + if (err) return callback(err); + entry.body = body; + return callback(null, entry); + }); + } + +} + +function treeScan(object) { + if (object.mode !== modes.tree) return []; + var tree = object.body; + return Object.keys(tree).map(function (name) { + var entry = tree[name]; + var path = object.path + name; + if (entry.mode === modes.tree) path += "/"; + return { + mode: entry.mode, + hash: entry.hash, + path: path + }; + }); +} + +function treeCompare(first, second) { + return first.path < second.path; +} + +function resolveRef(repo, hashish, callback) { + if (/^[0-9a-f]{40}$/.test(hashish)) { + return callback(null, hashish); + } + repo.readRef(hashish, function (err, hash) { + if (!hash) return callback(err || new Error("Bad ref " + hashish)); + callback(null, hash); + }); +} + +function walk(seed, scan, loadKey, compare) { + var queue = [seed]; + var working = 0, error, cb; + return {read: read, abort: abort}; + + function read(callback) { + if (!callback) return read; + if (cb) return callback(new Error("Only one read at a time")); + if (working) { cb = callback; return; } + var item = queue.shift(); + if (!item) return callback(); + try { scan(item).forEach(onKey); } + catch (err) { return callback(err); } + return callback(null, item); + } + + function abort(callback) { return callback(); } + + function onError(err) { + if (cb) { + var callback = cb; cb = null; + return callback(err); + } + error = err; + } + + function onKey(key) { + working++; + loadKey(key, onItem); + } + + function onItem(err, item) { + working--; + if (err) return onError(err); + var index = queue.length; + while (index && compare(item, queue[index - 1])) index--; + queue.splice(index, 0, item); + if (!working && cb) { + var callback = cb; cb = null; + return read(callback); + } + } +} From 9f609ee75a4998d53325fd367ef0cd809f04900b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 11:55:33 -0500 Subject: [PATCH 151/256] Remove all auto-formatting logic from object-codec. That belongs in the formats mixin. --- lib/object-codec.js | 8 ++--- test/test-object-codec.js | 65 +++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index 5ac3376..59bef34 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -23,15 +23,13 @@ var decoders = exports.decoders ={ exports.deframe = deframe; function encodeBlob(body) { - // Assume strings are normal unicode strings - if (typeof body === "string") return bodec.fromUnicode(body); - if (Array.isArray(body)) return bodec.fromArray(body); - if (bodec.isBinary(body)) return body; - throw new Error("Invalid body for blob"); + if (!bodec.isBinary(body)) throw new TypeError("Blobs must be binary values"); + return body; } function encodeTree(body) { var tree = ""; + if (Array.isArray(body)) throw new TypeError("Tree must be in object form"); var names = Object.keys(body).sort(); for (var i = 0, l = names.length; i < l; i++) { var name = names[i]; diff --git a/test/test-object-codec.js b/test/test-object-codec.js index 6570dba..eeae487 100644 --- a/test/test-object-codec.js +++ b/test/test-object-codec.js @@ -12,7 +12,7 @@ var blobBin, treeBin, commitBin, tagBin; run([ function testEncodeBlob() { - blob = "Hello World\n"; + blob = bodec.fromUnicode("Hello World\n"); blobBin = codec.frame({type: "blob", body: blob}); blobHash = sha1(blobBin); if (blobHash !== '557db03de997c86a4a028e1ebd3a1ceb225be238') { @@ -20,22 +20,13 @@ run([ } }, function testEncodeBlobInvalidType() { - var correctExceptionThrown = false; - blob = { - thisis: 'Not array, binary, or text' - }; try { - blobBin = codec.frame({type: "blob", body: blob}); + codec.frame({type: "blob", body: "Not a binary value"}); } - catch (exception) { - if (/invalid body/i.test(exception.message)) { - correctExceptionThrown = true; - } - } - - if (!correctExceptionThrown) { - throw new Error("Expected the correct exception when the blog was an invalid type"); + catch (err) { + return; } + throw new Error("Expected an error when passin in a non-binary blob"); }, function testEncodeTree() { tree = { @@ -51,17 +42,20 @@ run([ } }, function testEncodeCommit() { + var person = { + name: "Tim Caswell", + email: "tim@creationix.com", + date: { + seconds: 1391790884, + offset: 7 * 60 + } + }; commit = { tree: treeHash, - author: { - name: "Tim Caswell", - email: "tim@creationix.com", - date: { - seconds: 1391790884, - offset: 7 * 60 - } - }, - message: "Test Commit\n" + author: person, + committer: person, + message: "Test Commit\n", + parents: [] }; commitBin = codec.frame({type: "commit", body: commit}); commitHash = sha1(commitBin); @@ -116,7 +110,7 @@ run([ function testDecodeBlob() { var obj = codec.deframe(blobBin, true); if (obj.type !== "blob") throw new Error("Invalid type"); - if (bodec.toUnicode(obj.body) !== blob) { + if (bodec.toUnicode(obj.body) !== bodec.toUnicode(blob)) { throw new Error("Problem decoding"); } }, @@ -139,13 +133,20 @@ run([ } }, function testUnicodeCommit() { + var person = { + name: "Laȝamon", + email: "laȝamon@chronicles-of-england.org", + date: { + seconds: 1391790910, + offset: 7 * 60 + } + }; var commit = { tree: treeHash, - author: { - name: "Laȝamon", - email: "laȝamon@chronicles-of-england.org" - }, - message: "An preost wes on leoden, Laȝamon was ihoten\nHe wes Leovenaðes sone -- liðe him be Drihten\n" + author: person, + committer: person, + message: "An preost wes on leoden, Laȝamon was ihoten\nHe wes Leovenaðes sone -- liðe him be Drihten\n", + parents: [] }; var bin = codec.frame({type:"commit", body:commit}); var obj = codec.deframe(bin, true); @@ -163,7 +164,11 @@ run([ tag: "Laȝamon", tagger: { name: "Laȝamon", - email: "laȝamon@chronicles-of-england.org" + email: "laȝamon@chronicles-of-england.org", + date: { + seconds: 1391790910, + offset: 7 * 60 + } }, message: "He wonede at Ernleȝe at æðelen are chirechen,\nUppen Sevarne staþe, sel þar him þuhte,\nOnfest Radestone, þer he bock radde.\n" }; From 3b5c9740e536e867bacda2674f7058202a1f7858 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 11:58:18 -0500 Subject: [PATCH 152/256] Fix test for mem-db to honor stricter inputs --- mixins/mem-db.js | 4 +--- test/test-mem-db.js | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mixins/mem-db.js b/mixins/mem-db.js index eefeeb4..f159c97 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -47,9 +47,7 @@ function mixin(repo) { } function makeAsync(fn, callback) { - if (!callback) return function (callback) { - return makeAsync(fn, callback); - }; + if (!callback) return makeAsync.bind(null, fn); defer(function () { var out; try { out = fn(); } diff --git a/test/test-mem-db.js b/test/test-mem-db.js index de2e816..f7db59c 100644 --- a/test/test-mem-db.js +++ b/test/test-mem-db.js @@ -6,7 +6,7 @@ var codec = require('../lib/object-codec.js'); var repo = {}; require('../mixins/mem-db.js')(repo); -var blob = "Hello World\n"; +var blob = bodec.fromUnicode("Hello World\n"); var blobHash = "557db03de997c86a4a028e1ebd3a1ceb225be238"; run([ function testSaveAs(end) { @@ -24,7 +24,7 @@ run([ if (err) return end(err); var obj = codec.deframe(bin, true); if (obj.type !== "blob") return err(new Error("Wrong type")); - if (bodec.toUnicode(obj.body) !== blob) { + if (bodec.toUnicode(obj.body) !== bodec.toUnicode(blob)) { return err(new Error("Wrong body")); } end(); @@ -33,21 +33,21 @@ run([ function testLoadAs(end) { repo.loadAs("blob", blobHash, function (err, body) { if (err) return end(err); - if (bodec.toUnicode(body) !== blob) { + if (bodec.toUnicode(body) !== bodec.toUnicode(blob)) { return err(new Error("Wrong body")); } end(); }); }, function testSaveRaw(end) { - var newBody = "A new body\n"; + var newBody = bodec.fromUnicode("A new body\n"); var bin = codec.frame({type:"blob",body:newBody}); var hash = sha1(bin); repo.saveRaw(hash, bin, function (err) { if (err) return end(err); repo.loadAs("blob", hash, function (err, body) { if (err) return end(err); - if (bodec.toUnicode(body) !== newBody) { + if (bodec.toUnicode(body) !== bodec.toUnicode(newBody)) { return end(new Error("Body mismatch")); } end(); From cc73fb28c82a11cc37c2e409403a1e8321398bde Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 12:01:31 -0500 Subject: [PATCH 153/256] Add test script to package.json --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index f1e7667..e9774e7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "bugs": { "url": "https://github.com/creationix/js-git/issues" }, + "scripts": { + "test": "ls test/test-* | xargs -n1 node" + }, "dependencies": { "bodec": "git://github.com/creationix/bodec.git", "git-sha1": "git://github.com/creationix/git-sha1.git", From a7aa6143125ba03bb81062bd8aa9d722021716b1 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 12:09:42 -0500 Subject: [PATCH 154/256] Update docs to reflect stricter encoders --- doc/lib/object-codec.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/lib/object-codec.md b/doc/lib/object-codec.md index 3463945..69c8cbb 100644 --- a/doc/lib/object-codec.md +++ b/doc/lib/object-codec.md @@ -44,11 +44,14 @@ var encoders = require('js-git/lib/object-codec').encoders; var modes = require('js-git/lib/modes'); ``` -Blobs can be strings or binary buffers or arrays of bytes. +Blobs must be native binary values (Buffer in node, Uint8Array in browser). +It's recommended to either use the `bodec` library to create binary values from +strings directly or configure your system with the `formats` mixin that allows +for unicode strings when working with blobs. ```js -rawBin = encoders.blob("Hello World"); -rawBin = encoders.blob([1,2,3,4,5,6]); +rawBin = encoders.blob(new Uint8Array([1,2,3,4,5,6])); +rawBin = encoders.blob(bodec.fromUnicode("Hello World")); ``` Trees are objects with filename as key and object with {mode,hash} as value. @@ -64,12 +67,12 @@ rawBin = encoders.tree({ "greeting.txt": { Commits are objects with required fields {tree,author,message} Also if there is a single parent, you specify it with `parent`. -For merge commits, you can use an array of hashes in `parents`. -When decoding, the output is always normalized to an array of `parents`. +Since a commit can have zero or more parent commits, you specify the parent +hashes via the `parents` property as an array of hashes. -The `author` field is required and contains {name,email} and optionally `date` +The `author` field is required and contains {name,email,date}. -Commits can also have a `committer` with the same structure as `author`. +Commits also require a `committer` field with the same structure as `author`. The `date` property of `author` and `committer` is in the format {seconds,offset} Where seconds is a unix timestamp in seconds and offset is the number of minutes @@ -88,7 +91,7 @@ rawBin = encoders.commit({ offset: 7 * 60 } }, - parent: parentCommitHash, + parents: [ parentCommitHash ], message: "This is a test commit\n" }); ``` From f389673b678fba75d607a371d9049a199edf4b09 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 12:10:55 -0500 Subject: [PATCH 155/256] Update inflate docs to note that we're not using pako and not the streaming interface --- doc/lib/inflate.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/lib/inflate.md b/doc/lib/inflate.md index 31cc102..d96d36f 100644 --- a/doc/lib/inflate.md +++ b/doc/lib/inflate.md @@ -1,7 +1,6 @@ # Inflate This module implements a simple interface that when given deflated data returns the inflated version. -This wraps the included streaming inflate. ## inflate(deflated) -> inflated From a535d1debc905d9b64f82d44bac84324e8a68395 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 12:32:19 -0500 Subject: [PATCH 156/256] Start writing new README --- LICENSE | 1 - README.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index a6e8431..bfa2ab5 100644 --- a/LICENSE +++ b/LICENSE @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index ebf3fdc..72da9df 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ -# UNDER CONSTRUCTION +# JS-Git -Old code is in [legacy branch](https://github.com/creationix/js-git/tree/legacy) while I integrate the new stuff. +This project is a collection of modules that helps in implementing git powered +applications in JavaScript. The original purpose for this is to enable better +developer tools for authoring code in restricted environments like ChromeBooks +and tablets. It also enables using git at a database to replace SQL and no-SQL +data stores in many applications. + +This project was initially funded by two crowd-sourced fundraisers. See details +in `BACKERS.md` and `BACKERS-2.md`. Thanks to all of you who made this possible! + +## Usage + +Detailed API docs are contained in the [doc subfolder]() of this repository. + +In general the way you use js-git is you create a JS object and then mixin the +functionality you need. Here is an example of creating an in-memory database, +creating some objects, and then walking that tree using the high-level walker +APIs. + +```js +// This provides symbolic names for the octal modes used by git trees. +var modes = require('js-git/lib/modes'); + +// Create a repo by creating a plain object. +var repo = {}; + +// This provides an in-memory storage backend that provides the following APIs: +// - saveAs(type, value) => hash +// - loadAs(type, hash) => hash +// - saveRaw(hash, binary) => +// - loadRaw(hash) => binary +require('../mixins/mem-db')(repo); + +// This provides extra methods for dealing with packfile streams. +// It depends on +// - unpack(packStream, opts) => hashes +// - pack(hashes, opts) => packStream +require('../mixins/pack-ops')(repo); + +// This teaches the repo the client half of git network protocols: +// - fetchPack(remote, opts) => +// - sendPack(remote, opts) => +require('../mixins/client')(repo); + +// This adds in walker algorithms for quickly walking history or a tree. +// - logWalk(ref|hash) => stream +// - treeWalk(hash) => stream +require('../mixins/walkers')(repo); + +// This combines parallel requests for the same resource for effeciency under load. +require('../mixins/read-combiner')(repo); + +// This makes the object interface less strict. See it's docs for details +require('../mixins/formats')(repo); +``` + +Now we have an in-memory git repo useful for testing the network operations or +just getting to know the available APIs. + +In this example, we'll create a blob, create a tree containing that blob, create +a commit containing that tree. This shows how to create git objects manually. + +```js +// I'm using gen-run and ES6 generators to clean up the syntax, but normal +// node callback style also works. All js-git APIs support both. +var run = require('gen-run'); + +run(function*() { + + // First we create a blob from a string. The `formats` mixin allows us to + // use a string directly instead of having to pass in a binary buffer. + var blobHash = yield repo.saveAs("blob", "Hello World\n"); + + // Now we create a tree that is a folder containing the blob as `greeting.txt` + var treeHash = yield repo.saveAs("tree", { + "greeting.txt": { mode: modes.file, hash: blobHash } + }); + + // With that tree, we can create a commit. + // Again the `formats` mixin allows us to omit details like committer, date, + // and parents. It assumes sane defaults for these. + var commitHash = yield repo.saveAs("commit", { + author: { + name: "Tim Caswell", + email: "tim@creationix.com" + }, + tree: treeHash, + message: "Test commit\n" + }); + +``` + +Now that we have a repo with some minimal data in it, we can query it. Since we +included the `walkers` mixin, we can walk the history as a linear stream or walk +the file tree as a depth-first linear stream. + +```js + // Create a log stream starting at the commit we just made. + // You could also use symbolic refs like `refs/heads/master` for repos that + // support them. + var logStream = yield repo.logWalk(commitHash); + + // Looping through the stream is easy by repeatedly calling waiting on `read`. + var commit, object; + while (commit = yield logStream.read(), commit !== undefined) { + + console.log(commit); + + // We can also loop through all the files of each commit version. + var treeStream = yield repo.treeWalk(commit.tree); + while (object = yield treeStream.read(), object !== undefined) { + console.log(object); + } + + } +}); +``` From d53133d98a7e731a73db039e055f8f3ab1620cdb Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 12:34:53 -0500 Subject: [PATCH 157/256] Fix relative links --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72da9df..ba19614 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ and tablets. It also enables using git at a database to replace SQL and no-SQL data stores in many applications. This project was initially funded by two crowd-sourced fundraisers. See details -in `BACKERS.md` and `BACKERS-2.md`. Thanks to all of you who made this possible! +in [BACKERS.md](BACKERS.md) and [BACKERS-2.md](BACKERS.md). Thanks to all of +you who made this possible! ## Usage -Detailed API docs are contained in the [doc subfolder]() of this repository. +Detailed API docs are contained in the [doc](doc) subfolder of this repository. In general the way you use js-git is you create a JS object and then mixin the functionality you need. Here is an example of creating an in-memory database, From 23c5ff0e9f315e19831cd22601d9df0a3c1016a1 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 12:48:07 -0500 Subject: [PATCH 158/256] Allow object style in create-tree mixin --- mixins/create-tree.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mixins/create-tree.js b/mixins/create-tree.js index 9e2c790..5137dae 100644 --- a/mixins/create-tree.js +++ b/mixins/create-tree.js @@ -6,7 +6,15 @@ module.exports = function (repo) { repo.createTree = createTree; function createTree(entries, callback) { + if (!callback) return createTree.bind(null, entries); callback = singleCall(callback); + if (!Array.isArray(entries)) { + entries = Object.keys(entries).map(function (path) { + var entry = entries[path]; + entry.path = path; + return entry; + }); + } // Tree paths that we need loaded var toLoad = {}; From d039dbb764b259d14e58d4683fb00e3bcdb59c35 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:12:25 -0500 Subject: [PATCH 159/256] Add more to README --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index ba19614..ec47b50 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ var repo = {}; // - loadRaw(hash) => binary require('../mixins/mem-db')(repo); +// This adds a high-level API for creating multiple git objects by path. +// - createTree(entries) => hash +require('../mixins/create-tree')(repo); + // This provides extra methods for dealing with packfile streams. // It depends on // - unpack(packStream, opts) => hashes @@ -92,6 +96,34 @@ run(function*() { ``` +We can read objects back one at a time using `loadAs`. + +```js + // Reading the file "greeting.txt" from a commit. + + // We first read the commit. + var commit = yield repo.loadAs("commit", commitHash); + // We then read the tree using `commit.tree`. + var tree = yield repo.loadAs("tree", commit.tree); + // We then read the file using the entry hash in the tree. + var file = yield repo.loadAs("blob", tree["greeting.txt"]; + // file is now a binary buffer. +``` + +When using the `formats` mixin there are two new types for loadAs, they are +`text` and `array`. + +```js + // When you're sure the file contains unicode text, you can load it as text directly. + var fileAsText = yield repo.loadAs("text", blobHash); + + // Also if you prefer array format, you can load a directory as an array. + var entries = yield repo.loadAs("array", treeHash); + entries.forEach(function (entry) { + // entry contains {name, mode, hash} + }); +``` + Now that we have a repo with some minimal data in it, we can query it. Since we included the `walkers` mixin, we can walk the history as a linear stream or walk the file tree as a depth-first linear stream. @@ -117,3 +149,31 @@ the file tree as a depth-first linear stream. } }); ``` + +If you feel that creating a blob, then creating a tree, then creating the parent +tree, etc is a lot of work to save just one file, I agree. While writing the +tedit app, I discovered a nice high-level abstraction that you can mixin to make +this much easier. This is the `create-tree` mixin referenced in the above +config. + +```js +// We wish to create a tree that contains `www/index.html` and `README.me` files. +// This will create these two blobs, create a tree for `www` and then create a +// tree for the root containing `README.md` and the newly created `www` tree. +var treeHash = yield repo.createTree({ + "www/index.html": { + mode: modes.file, + content: "

Hello

\n

This is an HTML page?

\n" + }, + "README.md": { + mode: modes.file, + content: "# Sample repo\n\nThis is a sample\n" + } +}); +``` + +This is great for creating several files at once, but it can also be used to +edit existing trees by adding new files, changing existing files, or deleting +existing entries. + +```js From afa1cbbbbdc2028186d9db7d0993b715df5e7240 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:25:29 -0500 Subject: [PATCH 160/256] Organize README a bit and flesh our note about generators vs callbacks --- README.md | 117 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index ec47b50..75538d5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ functionality you need. Here is an example of creating an in-memory database, creating some objects, and then walking that tree using the high-level walker APIs. +## Creating a repo object. + ```js // This provides symbolic names for the octal modes used by git trees. var modes = require('js-git/lib/modes'); @@ -60,19 +62,50 @@ require('../mixins/read-combiner')(repo); require('../mixins/formats')(repo); ``` -Now we have an in-memory git repo useful for testing the network operations or -just getting to know the available APIs. +## Generators vs Callbacks -In this example, we'll create a blob, create a tree containing that blob, create -a commit containing that tree. This shows how to create git objects manually. +There are two control-flow styles that you can use to consume js-git APIs. All +the examples here use `yield` style and assume the code is contained within a +generator function that's yielding to a tool like `gen-run`. + +This style requires ES6 generators. This feature is currently in stable Firefox, +in stable Chrome behind a user-configurable flag, in node.js 0.11.x or greater +with a command-line flag. + +Also you can use generators on any ES5 platform if you use a source transform +like Facebook's regenerator tool. ```js -// I'm using gen-run and ES6 generators to clean up the syntax, but normal -// node callback style also works. All js-git APIs support both. var run = require('gen-run'); run(function*() { + // Blocking logic goes here. You can use yield + var result = yield someAction(withArgs); + // The generator pauses at yield and resumes when the data is available. + // The rest of your process is not blocked, just this generator body. + // If there was an error, it will throw into this generator. +}); +``` + +If you can't use this new feature or just plain prefer node-style callbacks, all +js-git APIs also support that. + +```js +someAction(withArgs, function (err, value) { + if (err) return handleMyError(err); + // do something with value +}); +``` +## Basic Object Creation + +Now we have an in-memory git repo useful for testing the network operations or +just getting to know the available APIs. + +In this example, we'll create a blob, create a tree containing that blob, create +a commit containing that tree. This shows how to create git objects manually. + +```js // First we create a blob from a string. The `formats` mixin allows us to // use a string directly instead of having to pass in a binary buffer. var blobHash = yield repo.saveAs("blob", "Hello World\n"); @@ -96,60 +129,65 @@ run(function*() { ``` +## Basic Object Loading + We can read objects back one at a time using `loadAs`. ```js - // Reading the file "greeting.txt" from a commit. - - // We first read the commit. - var commit = yield repo.loadAs("commit", commitHash); - // We then read the tree using `commit.tree`. - var tree = yield repo.loadAs("tree", commit.tree); - // We then read the file using the entry hash in the tree. - var file = yield repo.loadAs("blob", tree["greeting.txt"]; - // file is now a binary buffer. +// Reading the file "greeting.txt" from a commit. + +// We first read the commit. +var commit = yield repo.loadAs("commit", commitHash); +// We then read the tree using `commit.tree`. +var tree = yield repo.loadAs("tree", commit.tree); +// We then read the file using the entry hash in the tree. +var file = yield repo.loadAs("blob", tree["greeting.txt"]; +// file is now a binary buffer. ``` -When using the `formats` mixin there are two new types for loadAs, they are -`text` and `array`. +When using the `formats` mixin there are two new types for `loadAs`, they are +`"text"` and `"array"`. ```js - // When you're sure the file contains unicode text, you can load it as text directly. - var fileAsText = yield repo.loadAs("text", blobHash); +// When you're sure the file contains unicode text, you can load it as text directly. +var fileAsText = yield repo.loadAs("text", blobHash); - // Also if you prefer array format, you can load a directory as an array. - var entries = yield repo.loadAs("array", treeHash); - entries.forEach(function (entry) { - // entry contains {name, mode, hash} - }); +// Also if you prefer array format, you can load a directory as an array. +var entries = yield repo.loadAs("array", treeHash); +entries.forEach(function (entry) { + // entry contains {name, mode, hash} +}); ``` +## Using Walkers + Now that we have a repo with some minimal data in it, we can query it. Since we included the `walkers` mixin, we can walk the history as a linear stream or walk the file tree as a depth-first linear stream. ```js - // Create a log stream starting at the commit we just made. - // You could also use symbolic refs like `refs/heads/master` for repos that - // support them. - var logStream = yield repo.logWalk(commitHash); +// Create a log stream starting at the commit we just made. +// You could also use symbolic refs like `refs/heads/master` for repos that +// support them. +var logStream = yield repo.logWalk(commitHash); - // Looping through the stream is easy by repeatedly calling waiting on `read`. - var commit, object; - while (commit = yield logStream.read(), commit !== undefined) { +// Looping through the stream is easy by repeatedly calling waiting on `read`. +var commit, object; +while (commit = yield logStream.read(), commit !== undefined) { - console.log(commit); - - // We can also loop through all the files of each commit version. - var treeStream = yield repo.treeWalk(commit.tree); - while (object = yield treeStream.read(), object !== undefined) { - console.log(object); - } + console.log(commit); + // We can also loop through all the files of each commit version. + var treeStream = yield repo.treeWalk(commit.tree); + while (object = yield treeStream.read(), object !== undefined) { + console.log(object); } -}); + +} ``` +## Filesystem Style Interface + If you feel that creating a blob, then creating a tree, then creating the parent tree, etc is a lot of work to save just one file, I agree. While writing the tedit app, I discovered a nice high-level abstraction that you can mixin to make @@ -177,3 +215,4 @@ edit existing trees by adding new files, changing existing files, or deleting existing entries. ```js +``` From 4aed01094829e924cd527f2975acc1262617926c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:27:32 -0500 Subject: [PATCH 161/256] Add links to gen-run and regenerator --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75538d5..a02fefa 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,14 @@ require('../mixins/formats')(repo); There are two control-flow styles that you can use to consume js-git APIs. All the examples here use `yield` style and assume the code is contained within a -generator function that's yielding to a tool like `gen-run`. +generator function that's yielding to a tool like [gen-run](https://github.com/creationix/gen-run). This style requires ES6 generators. This feature is currently in stable Firefox, in stable Chrome behind a user-configurable flag, in node.js 0.11.x or greater with a command-line flag. Also you can use generators on any ES5 platform if you use a source transform -like Facebook's regenerator tool. +like Facebook's [regenerator](http://facebook.github.io/regenerator/) tool. ```js var run = require('gen-run'); From 2033762fd471f5e3cd79ffeb6dfa8f0d8978e44d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:28:38 -0500 Subject: [PATCH 162/256] Add link to howtonode article on generators --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a02fefa..2190bda 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ with a command-line flag. Also you can use generators on any ES5 platform if you use a source transform like Facebook's [regenerator](http://facebook.github.io/regenerator/) tool. +You read more about how generators work at [Generators vs Fibers](http://howtonode.org/generators-vs-fibers). + ```js var run = require('gen-run'); From e1e6ce8a1be1008143ab6d0d32f21a74c1609913 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:39:51 -0500 Subject: [PATCH 163/256] Flesh out README a bit more --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2190bda..1512a9c 100644 --- a/README.md +++ b/README.md @@ -90,13 +90,21 @@ run(function*() { ``` If you can't use this new feature or just plain prefer node-style callbacks, all -js-git APIs also support that. +js-git APIs also support that. The way this works is actually quite simple. +If you don't pass in the callback, the function will return a partially applied +version of your call expecting just the callback. ```js someAction(withArgs, function (err, value) { if (err) return handleMyError(err); // do something with value }); + +// The function would be implemented to support both style like this. +function someAction(arg, callback) { + if (!callback) return someAction.bind(this, arg); + // We now have callback and arg +} ``` ## Basic Object Creation @@ -217,4 +225,18 @@ edit existing trees by adding new files, changing existing files, or deleting existing entries. ```js +var changes = [ + { + path: "www/index.html" // Leaving out mode means to delete the entry. + }, + { path: "www/app.js", // Create a new file in the existing directory. + mode: modes.file, + content: "// this is a js file\n" + } +]; + +// We need to use array form and specify the base tree hash as `base`. +changes.base = treeHash; + +treeHash = tield repo.createTree(changes); ``` From fdb5affe795dc380fa0b2ee1ae7c573c34bccb57 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:42:19 -0500 Subject: [PATCH 164/256] Fix typo in README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1512a9c..6fb0381 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,8 @@ var changes = [ { path: "www/index.html" // Leaving out mode means to delete the entry. }, - { path: "www/app.js", // Create a new file in the existing directory. + { + path: "www/app.js", // Create a new file in the existing directory. mode: modes.file, content: "// this is a js file\n" } @@ -238,5 +239,5 @@ var changes = [ // We need to use array form and specify the base tree hash as `base`. changes.base = treeHash; -treeHash = tield repo.createTree(changes); +treeHash = yield repo.createTree(changes); ``` From 51e0fe1ab7bf2fcc82385e14e5e2eb892190f72a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 13:56:30 -0500 Subject: [PATCH 165/256] Add notes about git-tree and js-github --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 6fb0381..143a5c2 100644 --- a/README.md +++ b/README.md @@ -241,3 +241,29 @@ changes.base = treeHash; treeHash = yield repo.createTree(changes); ``` + +## Creating Composite Filesystems + +The real fun begins when you create composite filesystems using git submodules. + +The code that handles this is not packaged as a repo mixin since it spans several +independent repos. Instead look to the [git-tree](https://github.com/creationix/git-tree) +repo for the code. It's interface is still slightly unstable and undocumented +but is used in production by tedit and my node hosting service that compliments tedit. + +Basically this module allows you to perform high-level filesystem style commands +on a virtual filesystem that consists of many js-git repos. Until there are +proper docs, you can see how tedit uses it at . + +## Mounting Github Repos + +I've been asking Github to enable CORS headers to their HTTPS git servers, but +they've refused to do it. This means that a browser can never clone from github +because the browser will disallow XHR requests to the domain. + +They do, however, offer a REST interface to the raw [git data](https://developer.github.com/v3/git/). + +Using this I wrote a mixin for js-git that uses github *as* the backend store. + +Code at . Usage in tedit can be seen at +. From ff8c15a825b93759a321bc8273b3f97d58ea14d2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 23 Apr 2014 16:35:30 -0500 Subject: [PATCH 166/256] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 143a5c2..d198773 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ The real fun begins when you create composite filesystems using git submodules. The code that handles this is not packaged as a repo mixin since it spans several independent repos. Instead look to the [git-tree](https://github.com/creationix/git-tree) repo for the code. It's interface is still slightly unstable and undocumented -but is used in production by tedit and my node hosting service that compliments tedit. +but is used in production by tedit and my node hosting service that complements tedit. Basically this module allows you to perform high-level filesystem style commands on a virtual filesystem that consists of many js-git repos. Until there are From edd88959bd487cf00134a52e046667d4a9065ec7 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 24 Apr 2014 16:54:02 -0500 Subject: [PATCH 167/256] Make mem-cache continuable friendly --- mixins/mem-cache.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index c52221e..47a7d06 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -10,6 +10,7 @@ function memCache(repo) { var loadAs = repo.loadAs; repo.loadAs = loadAsCached; function loadAsCached(type, hash, callback) { + if (!callback) return loadAsCached.bind(this, type, hash); if (hash in cache) return callback(null, dupe(type, cache[hash]), hash); loadAs.call(repo, type, hash, function (err, value) { if (value === undefined) return callback(err); @@ -23,6 +24,7 @@ function memCache(repo) { var saveAs = repo.saveAs; repo.saveAs = saveAsCached; function saveAsCached(type, value, callback) { + if (!callback) return saveAsCached.bind(this, type, value); value = dupe(type, value); saveAs.call(repo, type, value, function (err, hash, value) { if (err) return callback(err); From df39d1953dbe7b20c708ace3bb53a10c38529147 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 24 Apr 2014 19:35:38 -0500 Subject: [PATCH 168/256] Fix mem-cache --- mixins/mem-cache.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 47a7d06..72d0b5c 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -26,7 +26,7 @@ function memCache(repo) { function saveAsCached(type, value, callback) { if (!callback) return saveAsCached.bind(this, type, value); value = dupe(type, value); - saveAs.call(repo, type, value, function (err, hash, value) { + saveAs.call(repo, type, value, function (err, hash) { if (err) return callback(err); if (type !== "blob" || value.length < 100) { cache[hash] = value; @@ -35,11 +35,11 @@ function memCache(repo) { }); } } - +var Binary = typeof Buffer === "function" ? Buffer : Uint8Array; function dupe(type, value) { if (type === "blob") { if (type.length >= 100) return value; - return new Uint8Array(value); + return new Binary(value); } return decoders[type](encoders[type](value)); } From b202d073257fd94eb4f0ed61fc49e3b5f2ea5b95 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Apr 2014 14:59:01 -0300 Subject: [PATCH 169/256] Fix tree sort to match real git --- lib/object-codec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index 59bef34..03ceb44 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -30,7 +30,9 @@ function encodeBlob(body) { function encodeTree(body) { var tree = ""; if (Array.isArray(body)) throw new TypeError("Tree must be in object form"); - var names = Object.keys(body).sort(); + var names = Object.keys(body).sort(function (a, b) { + return a + "/" > b + "/"; + }); for (var i = 0, l = names.length; i < l; i++) { var name = names[i]; var entry = body[name]; From baf42162715e2483fa5576a57f803dc805ed5600 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 1 May 2014 19:15:38 +0000 Subject: [PATCH 170/256] Expose numToType --- lib/pack-codec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 14081e8..731d392 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -11,7 +11,7 @@ var typeToNum = { "ofs-delta": 6, "ref-delta": 7 }; -var numToType = {}; +var numToType = exports.numToType = {}; for (var type in typeToNum) { var num = typeToNum[type]; numToType[num] = type; From 95f9ec767b0400265bddcb5f5b3998ddf2fe5582 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 1 May 2014 20:33:18 +0000 Subject: [PATCH 171/256] Move parseEntry to main js-git repo --- lib/pack-codec.js | 67 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 731d392..3746449 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -1,7 +1,8 @@ var inflateStream = require('./inflate-stream.js'); +var staticInflate = require('./inflate.js'); var deflate = require('./deflate.js'); var sha1 = require('git-sha1'); -var binary = require('bodec'); +var bodec = require('bodec'); var typeToNum = { commit: 1, @@ -11,11 +12,51 @@ var typeToNum = { "ofs-delta": 6, "ref-delta": 7 }; -var numToType = exports.numToType = {}; +var numToType = {}; for (var type in typeToNum) { var num = typeToNum[type]; numToType[num] = type; } +exports.parseEntry = parseEntry; +function parseEntry(chunk) { + var offset = 0; + var byte = chunk[offset++]; + var type = numToType[(byte >> 4) & 0x7]; + var size = byte & 0xf; + var left = 4; + while (byte & 0x80) { + byte = chunk[offset++]; + size |= (byte & 0x7f) << left; + left += 7; + } + size = size >>> 0; + var ref; + if (type === "ref-delta") { + ref = bodec.toHex(bodec.slice(chunk, offset, offset += 20)); + } + else if (type === "ofs-delta") { + byte = chunk[offset++]; + ref = byte & 0x75; + while (byte & 0x80) { + byte = chunk[offset++]; + ref = ((ref + 1) << 7) | (byte & 0x7f); + } + } + + var body = staticInflate(bodec.slice(chunk, offset)); + if (body.length !== size) { + throw new Error("Size mismatch"); + } + var result = { + type: type, + body: body + }; + if (typeof ref !== "undefined") { + result.ref = ref; + } + return result; +} + exports.decodePack = decodePack; function decodePack(emit) { @@ -44,7 +85,7 @@ function decodePack(emit) { for (var i = 0, l = chunk.length; i < l; i++) { // console.log([state, i, chunk[i].toString(16)]); - if (!state) throw new Error("Unexpected extra bytes: " + binary.slice(chunk, i)); + if (!state) throw new Error("Unexpected extra bytes: " + bodec.slice(chunk, i)); state = state(chunk[i], i, chunk); position++; } @@ -153,7 +194,7 @@ function decodePack(emit) { // Common helper for emitting all three object shapes function emitObject() { - var body = binary.join(parts); + var body = bodec.join(parts); if (body.length !== length) { throw new Error("Body length mismatch"); } @@ -185,7 +226,7 @@ function decodePack(emit) { emitObject(); // If this was all the objects, start calculating the sha1sum if (--num) return $header; - sha1sum.update(binary.slice(chunk, 0, i + 1)); + sha1sum.update(bodec.slice(chunk, 0, i + 1)); return $checksum; } @@ -212,7 +253,7 @@ function inflate() { var push = inflateStream(onEmit, onUnused); var more = true; var chunks = []; - var b = binary.create(1); + var b = bodec.create(1); return { write: write, recycle: recycle, flush: flush }; @@ -228,7 +269,7 @@ function inflate() { } function flush() { - var buffer = binary.join(chunks); + var buffer = bodec.join(chunks); chunks.length = 0; return buffer; } @@ -263,7 +304,7 @@ function encodePack(emit) { left = item.num; write(packHeader(item.num)); } - else if (typeof item.type === "string" && binary.isBinary(item.body)) { + else if (typeof item.type === "string" && bodec.isBinary(item.body)) { // The header must be sent before items. if (typeof left !== "number") throw new Error("Headers not sent yet"); @@ -275,7 +316,7 @@ function encodePack(emit) { // Send the checksum after the last item if (!--left) { - emit(binary.fromHex(sha1sum.digest())); + emit(bodec.fromHex(sha1sum.digest())); } } else { @@ -289,7 +330,7 @@ function encodePack(emit) { } function packHeader(length) { - return binary.fromArray([ + return bodec.fromArray([ 0x50, 0x41, 0x43, 0x4b, // PACK 0, 0, 0, 2, // version 2 length >> 24, // Num of objects @@ -325,10 +366,10 @@ function packFrame(item) { } } - var parts = [binary.fromArray(head)]; + var parts = [bodec.fromArray(head)]; if (typeof item.ref === "string") { - parts.push(binary.fromHex(item.ref)); + parts.push(bodec.fromHex(item.ref)); } parts.push(deflate(item.body)); - return binary.join(parts); + return bodec.join(parts); } From b39a6b2789b4520d57c931740dcad110958e218f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 1 May 2014 20:34:45 +0000 Subject: [PATCH 172/256] Fix typo --- lib/pack-codec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pack-codec.js b/lib/pack-codec.js index 3746449..f0910a6 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -36,7 +36,7 @@ function parseEntry(chunk) { } else if (type === "ofs-delta") { byte = chunk[offset++]; - ref = byte & 0x75; + ref = byte & 0x7f; while (byte & 0x80) { byte = chunk[offset++]; ref = ((ref + 1) << 7) | (byte & 0x7f); From 5078b25d994194d82e4b0b39fd2207b843edb7b2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 1 May 2014 22:27:53 +0000 Subject: [PATCH 173/256] Rewrite apply-delta. Also use bodec properly. --- lib/apply-delta.js | 173 +++++++++++++-------------------------------- 1 file changed, 48 insertions(+), 125 deletions(-) diff --git a/lib/apply-delta.js b/lib/apply-delta.js index 3d9738e..5357ac3 100644 --- a/lib/apply-delta.js +++ b/lib/apply-delta.js @@ -1,138 +1,61 @@ -// This is Chris Dickinson's code +var bodec = require('bodec'); -var binary = require('bodec') - , vi = new Decoder +module.exports = applyDelta; -// we use writeUint[8|32][LE|BE] instead of indexing -// into buffers so that we get buffer-browserify compat. -var OFFSET_BUFFER = binary.create(4) - , LENGTH_BUFFER = binary.create(4) +function applyDelta(delta, base) { + var deltaOffset = 0; -module.exports = apply_delta; -function apply_delta(delta, target) { - var base_size_info = {size: null, buffer: null} - , resized_size_info = {size: null, buffer: null} - , output_buffer - , out_idx - , command - , len - , idx - - delta_header(delta, base_size_info) - delta_header(base_size_info.buffer, resized_size_info) - - delta = resized_size_info.buffer - - idx = - out_idx = 0 - output_buffer = binary.create(resized_size_info.size) - - len = delta.length - - while(idx < len) { - command = delta[idx++] - command & 0x80 ? copy() : insert() + if (base.length !== readLength()) { + throw new Error("Base length mismatch"); } - return output_buffer - - function copy() { - OFFSET_BUFFER[0] = 0; - OFFSET_BUFFER[1] = 0; - OFFSET_BUFFER[2] = 0; - OFFSET_BUFFER[3] = 0; - LENGTH_BUFFER[0] = 0; - LENGTH_BUFFER[1] = 0; - LENGTH_BUFFER[2] = 0; - LENGTH_BUFFER[3] = 0; - - var check = 1 - , length - , offset - - for(var x = 0; x < 4; ++x) { - if(command & check) { - OFFSET_BUFFER[3 - x] = delta[idx++] - } - check <<= 1 + // Create a new output buffer with length from header. + var outOffset = 0; + var out = bodec.create(readLength()); + + while (deltaOffset < delta.length) { + var byte = delta[deltaOffset++]; + // Copy command. Tells us offset in base and length to copy. + if (byte & 0x80) { + var offset = 0; + var length = 0; + if (byte & 0x01) offset |= delta[deltaOffset++] << 0; + if (byte & 0x02) offset |= delta[deltaOffset++] << 8; + if (byte & 0x04) offset |= delta[deltaOffset++] << 16; + if (byte & 0x08) offset |= delta[deltaOffset++] << 24; + if (byte & 0x10) length |= delta[deltaOffset++] << 0; + if (byte & 0x20) length |= delta[deltaOffset++] << 8; + if (byte & 0x40) length |= delta[deltaOffset++] << 16; + if (length === 0) length = 0x10000; + // copy the data + bodec.copy(bodec.slice(base, offset, offset + length), out, outOffset); + outOffset += length; } - - for(var x = 0; x < 3; ++x) { - if(command & check) { - LENGTH_BUFFER[3 - x] = delta[idx++] - } - check <<= 1 + // Insert command, opcode byte is length itself + else if (byte) { + bodec.copy(bodec.slice(delta, deltaOffset, deltaOffset + byte), out, outOffset); + deltaOffset += byte; + outOffset += byte; } - LENGTH_BUFFER[0] = 0 - - length = ( - (LENGTH_BUFFER[0] << 24) | - (LENGTH_BUFFER[1] << 16) | - (LENGTH_BUFFER[2] << 8) | - (LENGTH_BUFFER[3])) || 0x10000; - offset = - (OFFSET_BUFFER[0] << 24) | - (OFFSET_BUFFER[1] << 16) | - (OFFSET_BUFFER[2] << 8) | - (OFFSET_BUFFER[3]); - - binary.copy(target, output_buffer, out_idx, offset, offset + length) - out_idx += length + else throw new Error('Invalid delta opcode'); } - function insert() { - binary.copy(delta, output_buffer, out_idx, idx, command + idx) - idx += command - out_idx += command + if (outOffset !== out.length) { + throw new Error("Size mismatch in check"); } -} - -function delta_header(buf, output) { - var done = false - , idx = 0 - , size = 0 - vi.ondata = function(s) { - size = s - done = true + return out; + + // Read a variable length number our of delta and move the offset. + function readLength() { + var byte = delta[deltaOffset++]; + var length = byte & 0x7f; + var shift = 7; + while (byte & 0x80) { + byte = delta[deltaOffset++]; + length |= (byte & 0x7f) << shift; + shift += 7; + } + return length; } - - do { - vi.write(buf[idx++]) - } while(!done) - - output.size = size - output.buffer = binary.slice(buf, idx) - } - -var MSB = 0x80 - , REST = 0x7F - -function Decoder() { - this.accum = [] -} -Decoder.prototype.write = write; - -function write(byte) { - var msb = byte & MSB - , accum = this.accum - , len - , out - - accum[accum.length] = byte & REST - if(msb) { - return - } - - len = accum.length - out = 0 - - for(var i = 0; i < len; ++i) { - out |= accum[i] << (7 * i) - } - - accum.length = 0 - this.ondata(out) - return -} \ No newline at end of file From 26576774e14198cce6ddbc1f13c866b18d3c2f00 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 1 May 2014 23:27:06 +0000 Subject: [PATCH 174/256] Import fs-db code from git-chrome-fs. --- mixins/fs-db.js | 293 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 mixins/fs-db.js diff --git a/mixins/fs-db.js b/mixins/fs-db.js new file mode 100644 index 0000000..5d9bac6 --- /dev/null +++ b/mixins/fs-db.js @@ -0,0 +1,293 @@ +"use strict"; +var bodec = require('bodec'); +var inflate = require('../lib/inflate'); +var deflate = require('../lib/deflate'); +var codec = require('../lib/object-codec'); +var parsePackEntry = require('../lib/pack-codec').parseEntry; +var applyDelta = require('../lib/apply-delta'); +var sha1 = require('git-sha1'); +var pathJoin = require('pathjoin'); + +// The fs object has the following interface: +// readFile(path) => binary +// readChunk(path, start, end) => binary +// writeFile(path, binary) => +// readDir(path) => array +module.exports = function (repo, fs, rootPath) { + + var cachedIndexes = {}; + + repo.loadAs = loadAs; + repo.saveAs = saveAs; + repo.loadRaw = loadRaw; + repo.saveRaw = saveRaw; + repo.readRef = readRef; + repo.updateRef = updateRef; + + function updateRef(ref, hash, callback) { + if (!callback) return updateRef.bind(repo, ref, hash); + var path = pathJoin(rootPath, ref); + fs.writeFile(path, bodec.fromRaw(hash + "\n"), callback); + } + + function readRef(ref, callback) { + if (!callback) return readRef.bind(repo, ref); + var path = pathJoin(rootPath, ref); + fs.readFile(path, function (err, binary) { + if (err) return callback(err); + if (binary === undefined) { + return readPackedRef(ref, callback); + } + var hash; + try { hash = bodec.toRaw(binary).trim(); } + catch (err) { return callback(err); } + callback(null, hash); + }); + } + + function readPackedRef(ref, callback) { + var path = pathJoin(rootPath, "packed-refs"); + fs.readFile(path, function (err, binary) { + if (binary === undefined) return callback(err); + var hash; + try { + var text = bodec.toRaw(binary); + var index = text.indexOf(ref); + if (index >= 0) { + hash = text.substring(index - 41, index - 1); + } + } + catch (err) { + return callback(err); + } + callback(null, hash); + }); + } + + function saveAs(type, body, callback) { + if (!callback) return saveAs.bind(repo, type, body); + var raw, hash; + try { + raw = codec.frame({ + type: type, + body: codec.encoders[type](body) + }); + hash = sha1(raw); + } + catch (err) { return callback(err); } + saveRaw(hash, raw, function (err) { + if (err) return callback(err); + callback(null, hash); + }); + } + + function saveRaw(hash, raw, callback) { + if (!callback) return saveRaw.bind(repo, hash, raw); + var buffer, path; + try { + if (sha1(raw) !== hash) { + throw new Error("Save data does not match hash"); + } + buffer = deflate(raw); + path = hashToPath(hash); + } + catch (err) { return callback(err); } + fs.writeFile(path, buffer, callback); + } + + function loadAs(type, hash, callback) { + if (!callback) return loadAs.bind(repo, type, hash); + loadRaw(hash, function (err, raw) { + if (raw === undefined) return callback(err); + var body; + try { + raw = codec.deframe(raw); + if (raw.type !== type) throw new TypeError("Type mismatch"); + body = codec.decoders[raw.type](raw.body); + } + catch (err) { return callback(err); } + callback(null, body); + }); + } + + function loadRaw(hash, callback) { + if (!callback) return loadRaw.bind(repo, hash); + var path = hashToPath(hash); + fs.readFile(path, function (err, buffer) { + if (err) return callback(err); + if (buffer) { + var raw; + try { raw = inflate(buffer); } + catch (err) { return callback(err); } + return callback(null, raw); + } + return loadRawPacked(hash, callback); + }); + } + + function loadRawPacked(hash, callback) { + console.log("loadRawPacked", hash); + var packDir = pathJoin(rootPath, "objects/pack"); + var packHashes = []; + fs.readDir(packDir, function (err, entries) { + if (!entries) return callback(err); + entries.forEach(function (name) { + var match = name.match(/pack-([0-9a-f]{40}).idx/); + if (match) packHashes.push(match[1]); + }); + start(); + }); + + function start() { + var packHash = packHashes.pop(); + var offsets; + if (!packHash) return callback(); + if (!cachedIndexes[packHash]) loadIndex(packHash); + else onIndex(); + + function loadIndex() { + var indexFile = pathJoin(packDir, "pack-" + packHash + ".idx" ); + fs.readFile(indexFile, function (err, buffer) { + if (!buffer) return callback(err); + try { + cachedIndexes[packHash] = parseIndex(buffer); + } + catch (err) { return callback(err); } + onIndex(); + }); + } + + function onIndex() { + var cached = cachedIndexes[packHash]; + console.log("index", cached); + var packFile = pathJoin(packDir, "pack-" + packHash + ".pack" ); + var index = cached.byHash[hash]; + if (!index) return start(); + offsets = cached.offsets; + loadChunk(packFile, index.offset, callback); + } + + function loadChunk(packFile, start, callback) { + var index = offsets.indexOf(start); + var end = index >= 0 ? offsets[index + 1] : -20; + fs.readChunk(packFile, start, end, function (err, chunk) { + if (!chunk) return callback(err); + var raw; + try { + var entry = parsePackEntry(chunk); + if (entry.type === "ref-delta") { + return loadRaw.call(repo, hash, onBase); + } + else if (entry.type === "ofs-delta") { + return loadChunk(packFile, start - entry.ref, onBase); + } + raw = codec.frame(entry); + } + catch (err) { return callback(err); } + callback(null, raw); + + function onBase(err, base) { + if (!base) return callback(err); + var object = codec.deframe(base); + var buffer; + try { + object.body = applyDelta(entry.body, object.body); + buffer = codec.frame(object); + } + catch (err) { return callback(err); } + callback(null, buffer); + } + }); + } + + } + } + + function hashToPath(hash) { + return pathJoin(rootPath, "objects", hash.substring(0, 2), hash.substring(2)); + } + +}; + +function parseIndex(buffer) { + if (readUint32(buffer, 0) !== 0xff744f63 || + readUint32(buffer, 4) !== 0x00000002) { + throw new Error("Only v2 pack indexes supported"); + } + + // Get the number of hashes in index + // This is the value of the last fan-out entry + var hashOffset = 8 + 255 * 4; + var length = readUint32(buffer, hashOffset); + hashOffset += 4; + var crcOffset = hashOffset + 20 * length; + var lengthOffset = crcOffset + 4 * length; + var largeOffset = lengthOffset + 4 * length; + var checkOffset = largeOffset; + var indexes = new Array(length); + for (var i = 0; i < length; i++) { + var start = hashOffset + i * 20; + var hash = bodec.toHex(bodec.slice(buffer, start, start + 20)); + var crc = readUint32(buffer, crcOffset + i * 4); + var offset = readUint32(buffer, lengthOffset + i * 4); + if (offset & 0x80000000) { + offset = largeOffset + (offset &0x7fffffff) * 8; + checkOffset = Math.max(checkOffset, offset + 8); + offset = readUint64(buffer, offset); + } + indexes[i] = { + hash: hash, + offset: offset, + crc: crc + }; + } + var packChecksum = bodec.toHex(bodec.slice(buffer, checkOffset, checkOffset + 20)); + var checksum = bodec.toHex(bodec.slice(buffer, checkOffset + 20, checkOffset + 40)); + if (sha1(bodec.slice(buffer, 0, checkOffset + 20)) !== checksum) { + throw new Error("Checksum mistmatch"); + } + + var byHash = {}; + indexes.sort(function (a, b) { + return a.offset - b.offset; + }); + indexes.forEach(function (data) { + byHash[data.hash] = { + offset: data.offset, + crc: data.crc, + }; + }); + var offsets = indexes.map(function (entry) { + return entry.offset; + }).sort(function (a, b) { + return a - b; + }); + + return { + offsets: offsets, + byHash: byHash, + checksum: packChecksum + }; +} + +function readUint32(buffer, offset) { + return (buffer[offset] << 24 | + buffer[offset + 1] << 16 | + buffer[offset + 2] << 8 | + buffer[offset + 3] << 0) >>> 0; +} + +// Yes this will lose precision over 2^53, but that can't be helped when +// returning a single integer. +// We simply won't support packfiles over 8 petabytes. I'm ok with that. +function readUint64(buffer, offset) { + var hi = (buffer[offset] << 24 | + buffer[offset + 1] << 16 | + buffer[offset + 2] << 8 | + buffer[offset + 3] << 0) >>> 0; + var lo = (buffer[offset + 4] << 24 | + buffer[offset + 5] << 16 | + buffer[offset + 6] << 8 | + buffer[offset + 7] << 0) >>> 0; + return hi * 0x100000000 + lo; +} From 223c9097d9bb42361973a8fc9b0c4fcfb67a15aa Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 1 May 2014 23:59:32 +0000 Subject: [PATCH 175/256] Change interface to fix race condition in git-chrome-fs --- mixins/fs-db.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 5d9bac6..5dcc021 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -9,11 +9,13 @@ var sha1 = require('git-sha1'); var pathJoin = require('pathjoin'); // The fs object has the following interface: -// readFile(path) => binary -// readChunk(path, start, end) => binary -// writeFile(path, binary) => -// readDir(path) => array -module.exports = function (repo, fs, rootPath) { +// - readFile(path) => binary +// - readChunk(path, start, end) => binary +// - writeFile(path, binary) => +// - readDir(path) => array +// The repo is expected to have a rootPath property that points to +// the .git folder within the filesystem. +module.exports = function (repo, fs) { var cachedIndexes = {}; @@ -26,13 +28,13 @@ module.exports = function (repo, fs, rootPath) { function updateRef(ref, hash, callback) { if (!callback) return updateRef.bind(repo, ref, hash); - var path = pathJoin(rootPath, ref); + var path = pathJoin(repo.rootPath, ref); fs.writeFile(path, bodec.fromRaw(hash + "\n"), callback); } function readRef(ref, callback) { if (!callback) return readRef.bind(repo, ref); - var path = pathJoin(rootPath, ref); + var path = pathJoin(repo.rootPath, ref); fs.readFile(path, function (err, binary) { if (err) return callback(err); if (binary === undefined) { @@ -46,7 +48,7 @@ module.exports = function (repo, fs, rootPath) { } function readPackedRef(ref, callback) { - var path = pathJoin(rootPath, "packed-refs"); + var path = pathJoin(repo.rootPath, "packed-refs"); fs.readFile(path, function (err, binary) { if (binary === undefined) return callback(err); var hash; @@ -126,8 +128,7 @@ module.exports = function (repo, fs, rootPath) { } function loadRawPacked(hash, callback) { - console.log("loadRawPacked", hash); - var packDir = pathJoin(rootPath, "objects/pack"); + var packDir = pathJoin(repo.rootPath, "objects/pack"); var packHashes = []; fs.readDir(packDir, function (err, entries) { if (!entries) return callback(err); @@ -159,7 +160,6 @@ module.exports = function (repo, fs, rootPath) { function onIndex() { var cached = cachedIndexes[packHash]; - console.log("index", cached); var packFile = pathJoin(packDir, "pack-" + packHash + ".pack" ); var index = cached.byHash[hash]; if (!index) return start(); @@ -204,7 +204,7 @@ module.exports = function (repo, fs, rootPath) { } function hashToPath(hash) { - return pathJoin(rootPath, "objects", hash.substring(0, 2), hash.substring(2)); + return pathJoin(repo.rootPath, "objects", hash.substring(0, 2), hash.substring(2)); } }; From 362f9f0f22197e4cb26124259655629f27e8ddef Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 2 May 2014 01:05:54 +0000 Subject: [PATCH 176/256] Remove fs-cache in favor of fs-db --- lib/node-fs-cache.js | 74 -------------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 lib/node-fs-cache.js diff --git a/lib/node-fs-cache.js b/lib/node-fs-cache.js deleted file mode 100644 index 7bb1073..0000000 --- a/lib/node-fs-cache.js +++ /dev/null @@ -1,74 +0,0 @@ -var encoders = require('../lib/encoders.js'); -var zlib = require('zlib'); -var pathJoin = require('path').join; -var dirname = require('path').dirname; - -module.exports = function (root) { - var fs = require('fs'); - return { - loadAs: loadAs, - saveAs: saveAs, - }; - - function loadAs(type, hash, callback) { - var path; - try { path = toPath(type, hash); } - catch (err) { return callback(err); } - fs.readFile(path, function (err, body) { - if (err) { - if (err.code === 'ENOENT') return callback(); - return callback(err); - } - zlib.inflate(body, function (err, body) { - if (err) return callback(err); - if (type !== "blob") { - try { body = JSON.parse(body.toString()); } - catch (err) {return callback(err); } - } - callback(null, body, hash); - }); - }); - } - - function saveAs(type, body, callback, forcedHash) { - var hash, data, path; - try { - body = encoders.normalizeAs(type, body); - hash = forcedHash || encoders.hashAs(type, body); - data = type === "blob" ? body : JSON.stringify(body); - path = toPath(type, hash); - } - catch (err) { return callback(err); } - zlib.deflate(data, function (err, data) { - if (err) return callback(err); - mkdirp(dirname(path), function (err) { - if (err) return callback(err); - fs.writeFile(path, data, function (err) { - if (err) return callback(err); - callback(null, hash, body); - }); - }); - }); - } - - function toPath(type, hash) { - if (!type || !hash) throw new TypeError("type and hash required"); - return pathJoin(root, hash.substring(0, 2), hash.substring(2) + "." + type); - } - - function mkdirp(path, callback) { - make(); - function make(err) { - if (err) return callback(err); - fs.mkdir(path, onmkdir); - } - function onmkdir(err) { - if (err) { - if (err.code === "ENOENT") return mkdirp(dirname(path), make); - if (err.code === "EEXIST") return callback(); - return callback(err); - } - callback(); - } - } -}; From 271c1e0d8a7da9b2ed260fdd91af22b017152779 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 5 May 2014 20:44:47 +0000 Subject: [PATCH 177/256] Add missing dep and remove broken docs. --- README.md | 5 ----- package.json | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index d198773..5cb9646 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,6 @@ require('../mixins/create-tree')(repo); // - pack(hashes, opts) => packStream require('../mixins/pack-ops')(repo); -// This teaches the repo the client half of git network protocols: -// - fetchPack(remote, opts) => -// - sendPack(remote, opts) => -require('../mixins/client')(repo); - // This adds in walker algorithms for quickly walking history or a tree. // - logWalk(ref|hash) => stream // - treeWalk(hash) => stream diff --git a/package.json b/package.json index e9774e7..77062f0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "bodec": "git://github.com/creationix/bodec.git", + "pathjoin": "git://github.com/creationix/pathjoin.git", "git-sha1": "git://github.com/creationix/git-sha1.git", "pako": "git://github.com/nodeca/pako.git" } From fecb23dce5ed0ccab18cb95d141e2694dc8b2763 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Tue, 6 May 2014 21:23:51 -0700 Subject: [PATCH 178/256] Document the file system database --- doc/mixins/fs-db.md | 51 +++++++++++++++++++++++++++++++++++++++++++++ mixins/fs-db.js | 4 ++++ 2 files changed, 55 insertions(+) create mode 100644 doc/mixins/fs-db.md diff --git a/doc/mixins/fs-db.md b/doc/mixins/fs-db.md new file mode 100644 index 0000000..f6375b9 --- /dev/null +++ b/doc/mixins/fs-db.md @@ -0,0 +1,51 @@ + +# Filesystem Git Database + +JSGit repositories need `loadAs`, `saveAs`, `loadRaw`, `saveRaw`, `readRef`, and +`updateRef` methods. +Depending on the backing storage, there are various ways to implement these +methods. +The implementation for in-mempory storage is `js-git/mixins/mem-db`, and there +are variants for using Github or IndexDB for storage. + +The `js-git/mixins/fs-db` implementation provides these methods as well, but +depends on a file system interface providing `readFile`, `readChunk`, +`writeFile`, and `readDir`. +These file system methods are implemented by `git-fs-db/mixins/bare-db` and +`git-chrome-db/mixins/bare-db` + +For the purpose of this document, `=>` implies that the function does not block +and accepts a Node.js-style callback. +The arrow points to the type of the result. +None of these methods need to return a continuable if the nodeback is missing. + +The type `binary` stands for whatever binary representation is appropriate for +the underlying platform. +For browsers, binary is a `Uint8Array`. +For Node.js, binary is a `Buffer`. + +## readFile(path) => binary | undefined + +Reads the entirety of the file at the given path and produces the binary. +If the file does not exist, readFile provides `undefined` instead. + +## readChunk(path, start, end) => binary | undefined + +Reads a byte range of the file at the given path. +The byte range is a half open interval, including the byte at the initial index, +and excluding the byte at the terminal index, such that the end minus the start +is the length of the resulting binary data. +If the file does not exist, readChunk provides `undefined` instead. + +## writeFile(path, binary) => undefined + +Writes the given bytes to the file at the given path. +The method creates any directories leading up to the path if they do not already +exist. + +## readDir(path) => array of names | undefined + +Reads the names of the entries in the directory at the given path. +The names are not fully qualified paths, just the name of the entry within the +given directory. + diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 5dcc021..7bbfba2 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -10,9 +10,13 @@ var pathJoin = require('pathjoin'); // The fs object has the following interface: // - readFile(path) => binary +// Must also call callback() with no arguments if the file does not exist. // - readChunk(path, start, end) => binary +// Must also call callback() with no arguments if the file does not exist. // - writeFile(path, binary) => +// Must also make every directory up to parent of path. // - readDir(path) => array +// Must also call callback() with no arguments if the file does not exist. // The repo is expected to have a rootPath property that points to // the .git folder within the filesystem. module.exports = function (repo, fs) { From d3977c324e350dce40956e2eac3fbcbe7725f393 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 7 May 2014 04:29:11 +0000 Subject: [PATCH 179/256] Clarify relationship to fs interface implementations. --- doc/mixins/fs-db.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/mixins/fs-db.md b/doc/mixins/fs-db.md index f6375b9..8cb3d54 100644 --- a/doc/mixins/fs-db.md +++ b/doc/mixins/fs-db.md @@ -11,8 +11,8 @@ are variants for using Github or IndexDB for storage. The `js-git/mixins/fs-db` implementation provides these methods as well, but depends on a file system interface providing `readFile`, `readChunk`, `writeFile`, and `readDir`. -These file system methods are implemented by `git-fs-db/mixins/bare-db` and -`git-chrome-db/mixins/bare-db` +These file system methods are implemented by the `git-fs-db` and +`git-chrome-db` packages. For the purpose of this document, `=>` implies that the function does not block and accepts a Node.js-style callback. @@ -48,4 +48,3 @@ exist. Reads the names of the entries in the directory at the given path. The names are not fully qualified paths, just the name of the entry within the given directory. - From 56b8f1823562f48b3d492f3ad48b17c7f79bb230 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 12 May 2014 19:58:23 +0000 Subject: [PATCH 180/256] Only write objects in fs-db if they don't exists already. --- mixins/fs-db.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 7bbfba2..0dc1ce4 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -98,7 +98,13 @@ module.exports = function (repo, fs) { path = hashToPath(hash); } catch (err) { return callback(err); } - fs.writeFile(path, buffer, callback); + // Try to read the object first. + loadRaw(hash, function (err, data) { + // If it already exists, we're done + if (data) return callback(); + // Otherwise write a new file + fs.writeFile(path, buffer, callback); + }); } function loadAs(type, hash, callback) { From 5f7bcc742c5c3113112af3ddd9631b1efc9faa39 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Wed, 14 May 2014 11:06:42 -0700 Subject: [PATCH 181/256] Specify support for negative end indexes in fs-db --- doc/mixins/fs-db.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/mixins/fs-db.md b/doc/mixins/fs-db.md index 8cb3d54..3029e5a 100644 --- a/doc/mixins/fs-db.md +++ b/doc/mixins/fs-db.md @@ -35,6 +35,9 @@ Reads a byte range of the file at the given path. The byte range is a half open interval, including the byte at the initial index, and excluding the byte at the terminal index, such that the end minus the start is the length of the resulting binary data. +The end offset may be negative, in which case it should count back from the end +of the size of the file at the path, such that the size plus the negative end is +the positive end. If the file does not exist, readChunk provides `undefined` instead. ## writeFile(path, binary) => undefined From 4b7fa6d4f384017f64d47f0aaf68a02fb29fe58c Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Wed, 14 May 2014 11:07:32 -0700 Subject: [PATCH 182/256] Fix bug reading the last chunk of packed index --- mixins/fs-db.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 0dc1ce4..80f6276 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -179,7 +179,11 @@ module.exports = function (repo, fs) { function loadChunk(packFile, start, callback) { var index = offsets.indexOf(start); - var end = index >= 0 ? offsets[index + 1] : -20; + if (index < 0) { + var error = new Error("Can't find chunk starting at " + start); + return callback(error); + } + var end = index + 1 < offsets.length ? offsets[index + 1] : -20; fs.readChunk(packFile, start, end, function (err, chunk) { if (!chunk) return callback(err); var raw; From 2904518b6709b6d8b7e68112ac04babb9681ccca Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 16 May 2014 16:16:02 +0000 Subject: [PATCH 183/256] Implement path-to-entry mixin and git-fs adapter. --- lib/git-fs.js | 112 ++++++++++++++++++++++++++++++++++++++++ mixins/path-to-entry.js | 42 +++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 lib/git-fs.js create mode 100644 mixins/path-to-entry.js diff --git a/lib/git-fs.js b/lib/git-fs.js new file mode 100644 index 0000000..8bde435 --- /dev/null +++ b/lib/git-fs.js @@ -0,0 +1,112 @@ +"use strict"; + +var modes = require('./modes'); + +// options.encrypt(plain) -> encrypted +// options.decrypt(encrypted) -> plain +// options.shouldEncrypt(path) -> boolean +// options.getRootTree() => hash +// options.setRootTree(hash) => +module.exports = function (repo, options) { + var toWrite = {}; + var callbacks = []; + var writing = false; + + return { + readFile: readFile, + writeFile: writeFile, + readDir: readDir + }; + + function readFile(path, callback) { + if (!callback) return readFile.bind(null, path); + options.getRootTree(onRootTree); + + function onRootTree(err, hash) { + if (!hash) return callback(err); + repo.pathToEntry(hash, path, onEntry); + } + + function onEntry(err, entry) { + if (!entry || !modes.isFile(entry.mode)) return callback(err); + repo.loadAs("blob", entry.hash, callback); + } + } + + function writeFile(path, binary, callback) { + if (!callback) return writeFile.bind(null, path, binary); + toWrite[path] = binary; + callbacks.push(callback); + check(); + } + + function readDir(path, callback) { + if (!callback) return readDir.bind(null, path); + + options.getRootTree(onRootTree); + + function onRootTree(err, hash) { + if (!hash) return callback(err); + repo.pathToEntry(hash, path, onEntry); + } + + function onEntry(err, entry) { + if (!entry || entry.mode !== modes.tree) return callback(err); + repo.loadAs("tree", entry.hash, onTree); + } + + function onTree(err, tree) { + if (!tree) return callback(err); + callback(null, Object.keys(tree)); + } + } + + function check() { + if (writing || !callbacks.length) return; + writing = true; + options.getRootTree(onRootTree); + + function onRootTree(err, hash) { + if (err) return callall(err); + var files = pullFiles(); + if (hash) files.base = hash; + repo.createTree(files, onNewTree); + } + + function onNewTree(err, hash) { + if (err) return callall(err); + options.setRootTree(hash, onSaveRoot); + + } + + function onSaveRoot(err) { + if (err) return callall(err); + toWrite = {}; + writing = false; + callall(); + } + } + + function pullFiles() { + var files = Object.keys(toWrite).map(function (path) { + var content = toWrite[path]; + var mode = modes.blob; + if (options.shouldEncrypt && options.shouldEncrypt(path)) { + mode = modes.exec; + content = options.encrypt(content); + } + return { + path: path, + mode: mode, + content: content + }; + }); + return files; + } + + function callall(err) { + callbacks.splice(0, callbacks.length).forEach(function (callback) { + callback(err); + }); + } +}; diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js new file mode 100644 index 0000000..b3404c3 --- /dev/null +++ b/mixins/path-to-entry.js @@ -0,0 +1,42 @@ +var cache = require('./mem-cache').cache; +var modes = require('../lib/modes'); + +module.exports = function (repo) { + repo.pathToEntry = pathToEntry; +}; + +function pathToEntry(rootTree, path, callback) { + if (!callback) return pathToEntry.bind(this, path); + var repo = this; + var mode = modes.tree; + var hash = rootTree; + var parts = path.split("/").filter(Boolean); + var index = 0; + var cached; + function loop() { + while (index < parts.length) { + if (mode === modes.tree) { + cached = cache[hash]; + if (!cached) return repo.loadAs("tree", hash, onLoad); + var entry = cached[parts[index]]; + if (!entry) return callback(); + mode = entry.mode; + hash = entry.hash; + index++; + continue; + } + return callback(); + } + callback({ + mode: mode, + hash: hash + }); + } + + function onLoad(err, value) { + if (!value) return callback(err || new Error("Missing object: " + hash)); + cache[hash] = value; + loop(); + } + +} From af5087ea3c610b4efe1e4bc14aca1555f6c6d4dd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 16 May 2014 16:21:24 +0000 Subject: [PATCH 184/256] Allow reading from write cache. --- lib/git-fs.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/git-fs.js b/lib/git-fs.js index 8bde435..4dd4ffc 100644 --- a/lib/git-fs.js +++ b/lib/git-fs.js @@ -20,6 +20,11 @@ module.exports = function (repo, options) { function readFile(path, callback) { if (!callback) return readFile.bind(null, path); + + // If there is a pending write for this path, pull from the cache. + if (toWrite[path]) return callback(null, toWrite[path]); + + // Otherwise read from the persistent storage options.getRootTree(onRootTree); function onRootTree(err, hash) { From 2eba680bf5e7c9f829c2ec1682a7d9c7b042e021 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 16 May 2014 16:28:15 +0000 Subject: [PATCH 185/256] Fix path-to-entry logic. --- mixins/path-to-entry.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js index b3404c3..574a28a 100644 --- a/mixins/path-to-entry.js +++ b/mixins/path-to-entry.js @@ -13,6 +13,7 @@ function pathToEntry(rootTree, path, callback) { var parts = path.split("/").filter(Boolean); var index = 0; var cached; + loop(); function loop() { while (index < parts.length) { if (mode === modes.tree) { @@ -27,7 +28,7 @@ function pathToEntry(rootTree, path, callback) { } return callback(); } - callback({ + callback(null, { mode: mode, hash: hash }); From b49e1d24dee89c85c23fa09bea6e110279bf48e4 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 16 May 2014 16:35:43 +0000 Subject: [PATCH 186/256] Fix git-fs to actually decrypt data. --- lib/git-fs.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/git-fs.js b/lib/git-fs.js index 4dd4ffc..807a398 100644 --- a/lib/git-fs.js +++ b/lib/git-fs.js @@ -34,7 +34,14 @@ module.exports = function (repo, options) { function onEntry(err, entry) { if (!entry || !modes.isFile(entry.mode)) return callback(err); - repo.loadAs("blob", entry.hash, callback); + + repo.loadAs("blob", entry.hash, function (err, content) { + if (!content) return callback(err); + if (entry.mode === modes.exec) { + content = options.decrypt(content); + } + callback(null, content); + }); } } From 081839e189c37fdccf4cb7c7c368577dcf2ba7b4 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 16 May 2014 16:39:14 +0000 Subject: [PATCH 187/256] Use symlink instead of exec to mark encrypted files in git-fs. --- lib/git-fs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/git-fs.js b/lib/git-fs.js index 807a398..ad7cbbf 100644 --- a/lib/git-fs.js +++ b/lib/git-fs.js @@ -33,11 +33,11 @@ module.exports = function (repo, options) { } function onEntry(err, entry) { - if (!entry || !modes.isFile(entry.mode)) return callback(err); + if (!entry || !modes.isBlob(entry.mode)) return callback(err); repo.loadAs("blob", entry.hash, function (err, content) { if (!content) return callback(err); - if (entry.mode === modes.exec) { + if (entry.mode === modes.sym) { content = options.decrypt(content); } callback(null, content); @@ -104,7 +104,7 @@ module.exports = function (repo, options) { var content = toWrite[path]; var mode = modes.blob; if (options.shouldEncrypt && options.shouldEncrypt(path)) { - mode = modes.exec; + mode = modes.sym; content = options.encrypt(content); } return { From 79e8a5e32576369f322187c03933ecb363b0324e Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 16 May 2014 18:07:43 +0000 Subject: [PATCH 188/256] Optimize git-fs a little using defer. --- lib/git-fs.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/git-fs.js b/lib/git-fs.js index ad7cbbf..1a2ba04 100644 --- a/lib/git-fs.js +++ b/lib/git-fs.js @@ -1,6 +1,7 @@ "use strict"; var modes = require('./modes'); +var defer = require('./defer'); // options.encrypt(plain) -> encrypted // options.decrypt(encrypted) -> plain @@ -49,7 +50,7 @@ module.exports = function (repo, options) { if (!callback) return writeFile.bind(null, path, binary); toWrite[path] = binary; callbacks.push(callback); - check(); + defer(check); } function readDir(path, callback) { @@ -96,6 +97,7 @@ module.exports = function (repo, options) { toWrite = {}; writing = false; callall(); + defer(check); } } From ff690e8d8c2ab3f3ce9c1cde34503c61bf1986cc Mon Sep 17 00:00:00 2001 From: Max Waterman Date: Mon, 19 May 2014 16:14:36 +0100 Subject: [PATCH 189/256] Fixed typo - missing ')'. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cb9646..fa12bfd 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ var commit = yield repo.loadAs("commit", commitHash); // We then read the tree using `commit.tree`. var tree = yield repo.loadAs("tree", commit.tree); // We then read the file using the entry hash in the tree. -var file = yield repo.loadAs("blob", tree["greeting.txt"]; +var file = yield repo.loadAs("blob", tree["greeting.txt"]); // file is now a binary buffer. ``` From 6f322a0a34890b8491309544561ce067cadbb77e Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 20 May 2014 23:34:50 +0000 Subject: [PATCH 190/256] Report IDB data loss --- mixins/indexed-db.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index 207767b..efc8c55 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -21,6 +21,10 @@ function init(callback) { request.onupgradeneeded = function(evt) { var db = evt.target.result; + if (evt.dataLoss) { + return callback(new Error(evt.dataLoss + ": " + evt.dataLossMessage)); + } + // A versionchange transaction is started automatically. evt.target.transaction.onerror = onError; From aaabdd172197cab81ee81912a744471e896f03ad Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 21 May 2014 00:18:43 +0000 Subject: [PATCH 191/256] Fix error check in idb --- mixins/indexed-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index efc8c55..5822aab 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -21,7 +21,7 @@ function init(callback) { request.onupgradeneeded = function(evt) { var db = evt.target.result; - if (evt.dataLoss) { + if (evt.dataLoss && evt.dataLoss !== "none") { return callback(new Error(evt.dataLoss + ": " + evt.dataLossMessage)); } From 9ae2ecec6e99c7b31218a83065efc6ac3603231e Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 23 May 2014 17:30:28 +0000 Subject: [PATCH 192/256] Implement websql backend. --- mixins/websql-db.js | 165 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 mixins/websql-db.js diff --git a/mixins/websql-db.js b/mixins/websql-db.js new file mode 100644 index 0000000..e001073 --- /dev/null +++ b/mixins/websql-db.js @@ -0,0 +1,165 @@ +"use strict"; + +var codec = require('../lib/object-codec.js'); +var bodec = require('bodec'); +var inflate = require('../lib/inflate'); +var deflate = require('../lib/deflate'); + +var sha1 = require('git-sha1'); +var modes = require('../lib/modes.js'); +var db; + +mixin.init = init; + +mixin.loadAs = loadAs; +mixin.saveAs = saveAs; +module.exports = mixin; + +function mixin(repo, prefix) { + if (!prefix) throw new Error("Prefix required"); + repo.refPrefix = prefix; + repo.saveAs = saveAs; + repo.saveRaw = saveRaw; + repo.loadAs = loadAs; + repo.loadRaw = loadRaw; + repo.readRef = readRef; + repo.updateRef = updateRef; + repo.hasHash = hasHash; +} + +function init(callback) { + + db = openDatabase('tedit', '1.0', 'tedit local data', 10 * 1024 * 1024); + db.transaction(function (tx) { + tx.executeSql( + 'CREATE TABLE IF NOT EXISTS objects (hash unique, body blob)' + ); + tx.executeSql( + 'CREATE TABLE IF NOT EXISTS refs (path unique, value text)' + ); + }, function () { + console.error(arguments); + callback(new Error("Problem initializing database")); + }, function () { + callback(); + }); +} + +function saveAs(type, body, callback) { + /*jshint: validthis: true */ + if (!callback) return saveAs.bind(this, type, body); + var hash, buffer; + try { + buffer = codec.frame({type:type,body:body}); + hash = sha1(buffer); + } + catch (err) { return callback(err); } + this.saveRaw(hash, buffer, callback); +} + +function saveRaw(hash, buffer, callback) { + /*jshint: validthis: true */ + if (!callback) return saveRaw.bind(this, hash, buffer); + var sql = 'INSERT INTO objects (hash, body) VALUES (?, ?)'; + db.transaction(function (tx) { + var text; + try { + text = bodec.toBase64(deflate(buffer)); + } + catch (err) { + return callback(err); + } + tx.executeSql(sql, [hash, text], function () { + callback(null, hash); + }); + }); +} + +function loadAs(type, hash, callback) { + /*jshint: validthis: true */ + if (!callback) return loadAs.bind(this, type, hash); + loadRaw(hash, function (err, buffer) { + if (!buffer) return callback(err); + var parts, body; + try { + parts = codec.deframe(buffer); + if (parts.type !== type) throw new Error("Type mismatch"); + body = codec.decoders[type](parts.body); + } + catch (err) { + return callback(err); + } + callback(null, body); + }); +} + +function loadRaw(hash, callback) { + /*jshint: validthis: true */ + if (!callback) return loadRaw.bind(this, hash); + var sql = 'SELECT * FROM objects WHERE hash=?'; + db.readTransaction(function (tx) { + tx.executeSql(sql, [hash], function (tx, result) { + if (!result.rows.length) return callback(); + var item = result.rows.item(0); + var buffer; + try { + buffer = inflate(bodec.fromBase64(item.body)); + } + catch (err) { + return callback(err); + } + callback(null, buffer); + }, function (tx, error) { + callback(new Error(error.message)); + }); + }); +} + +function hasHash(type, hash, callback) { + /*jshint: validthis: true */ + loadAs(type, hash, function (err, value) { + if (err) return callback(err); + if (value === undefined) return callback(null, false); + if (type !== "tree") return callback(null, true); + var names = Object.keys(value); + next(); + function next() { + if (!names.length) return callback(null, true); + var name = names.pop(); + var entry = value[name]; + hasHash(modes.toType(entry.mode), entry.hash, function (err, has) { + if (err) return callback(err); + if (has) return next(); + callback(null, false); + }); + } + }); +} + +function readRef(ref, callback) { + /*jshint: validthis: true */ + var key = this.refPrefix + "/" + ref; + var sql = 'SELECT * FROM refs WHERE path=?'; + db.transaction(function (tx) { + tx.executeSql(sql, [key], function (tx, result) { + if (!result.rows.length) return callback(); + var item = result.rows.item(0); + callback(null, item.value); + }, function (tx, error) { + callback(new Error(error.message)); + }); + }); +} + +function updateRef(ref, hash, callback) { + /*jshint: validthis: true */ + var key = this.refPrefix + "/" + ref; + var sql = 'INSERT INTO refs (path, value) VALUES (?, ?)'; + db.transaction(function (tx) { + tx.executeSql(sql, [key, hash], function () { + callback(); + }, function (tx, error) { + callback(new Error(error.message)); + }); + }); +} From 4438ad9750374bfe249ee3548fbe15aab15dd308 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 23 May 2014 18:17:32 +0000 Subject: [PATCH 193/256] Allow websql to be used as cache. --- mixins/websql-db.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mixins/websql-db.js b/mixins/websql-db.js index e001073..6a25744 100644 --- a/mixins/websql-db.js +++ b/mixins/websql-db.js @@ -13,6 +13,8 @@ mixin.init = init; mixin.loadAs = loadAs; mixin.saveAs = saveAs; +mixin.loadRaw = loadRaw; +mixin.saveRaw = saveRaw; module.exports = mixin; function mixin(repo, prefix) { From 2bc6be3f87656a2cd88708137682b6f30a513f19 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 27 May 2014 04:01:48 +0000 Subject: [PATCH 194/256] Fix tree sort and start to implement sync --- lib/object-codec.js | 4 +- mixins/sync.js | 143 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 mixins/sync.js diff --git a/lib/object-codec.js b/lib/object-codec.js index 03ceb44..11f1954 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -31,7 +31,9 @@ function encodeTree(body) { var tree = ""; if (Array.isArray(body)) throw new TypeError("Tree must be in object form"); var names = Object.keys(body).sort(function (a, b) { - return a + "/" > b + "/"; + var aa = a + "/"; + var bb = b + "/"; + return aa > bb ? 1 : aa < bb ? -1 : 0; }); for (var i = 0, l = names.length; i < l; i++) { var name = names[i]; diff --git a/mixins/sync.js b/mixins/sync.js new file mode 100644 index 0000000..a1ddff3 --- /dev/null +++ b/mixins/sync.js @@ -0,0 +1,143 @@ +"use strict"; + +var carallel = require('carallel'); +var modes = require('../lib/modes'); + +module.exports = function (repo) { + repo.sync = sync; +}; + +function sync(remote, options, callback) { + /*jshint: validthis: true*/ + if (!callback) return sync.bind(this, remote, options); + var local = this; + if (typeof local.readRef !== "function") { + throw new TypeError("local repo is missing readRef method"); + } + if (typeof local.loadDirectAs !== "function") { + throw new TypeError("local repo is missing loadDirectAs method"); + } + if (typeof remote.readRef !== "function") { + throw new TypeError("remote repo is missing readRef method"); + } + console.log({ + local: local, + remote: remote, + options: options + }); + var localRef = options.localRef || "refs/heads/master"; + var remoteRef = options.remoteRef || localRef; + carallel({ + local: local.readRef(localRef), + remote: remote.readRef(remoteRef) + }, function (err, hashes) { + if (err) return callback(err); + if (hashes.local) throw "TODO: Implement local"; + console.log(hashes); + var depth = options.localDepth || Infinity; + downloadCommit(local, remote, hashes.remote, function (err, commit) { + if (err) return callback(err); + console.log("commit", commit); + }); + }); +} + +function downloadCommits(local, remote, hash, depth, callback) { + +} + +function downloadCommit(local, remote, hash, callback) { + local.loadDirectAs("commit", hash, onCheck); + + function onCheck(err, commit) { + if (err || commit) return callback(err, commit); + remote.loadAs("commit", hash, onCommit); + } + + function onCommit(err, commit) { + if (!commit) return callback(err || new Error("Missing remote commit " + hash)); + + downloadTree(local, remote, commit.tree, onTree); + + function onTree(err) { + if (err) return callback(err); + local.saveAs("commit", commit, onSaved); + } + + function onSaved(err, newHash) { + if (err) return callback(err); + if (newHash !== hash) { + return callback(new Error("Hash mismatch for commit")); + } + callback(null, commit); + } + } +} + +function downloadTree(local, remote, hash, callback) { + if (!callback) return downloadTree.bind(null, local, remote, hash); + + local.loadDirectAs("tree", hash, onCheck); + + function onCheck(err, tree) { + if (err || tree) return callback(err); + remote.loadAs("tree", hash, onTree); + } + + function onTree(err, tree) { + if (!tree) return callback(err || new Error("Missing remote tree " + hash)); + carallel(Object.keys(tree).map(function (name) { + return downloadEntry(local, remote, tree[name]); + }).filter(Boolean), function (err) { + if (err) return callback(err); + local.saveAs("tree", tree, onSaved); + }); + + function onSaved(err, newHash) { + if (err) return callback(err); + if (newHash !== hash) { + console.error(tree); + console.error({ + expected: hash, + actual: newHash + }); + return callback(new Error("Hash mismatch for tree")); + } + callback(); + } + + } + + +} + +function downloadBlob(local, remote, hash, callback) { + if (!callback) return downloadBlob.bind(null, local, remote, hash); + + local.loadDirectAs("blob", hash, function (err, blob) { + if (err || blob) return callback(err); + remote.loadAs("blob", hash, onBlob); + }); + + function onBlob(err, blob) { + if (!blob) return callback(err || new Error("Missing remote blob " + hash)); + local.saveAs("blob", blob, onSaved); + } + + function onSaved(err, newHash) { + if (err) return callback(err); + if (newHash !== hash) { + return callback(new Error("Hash mismatch for commit")); + } + callback(); + } +} + +function downloadEntry(local, remote, entry) { + if (entry.mode === modes.tree) return downloadTree(local, remote, entry.hash); + if (modes.isBlob(entry.mode)) return downloadBlob(local, remote, entry.hash); +} + +function findCommon(local, localHash, remote, remoteHash, callback) { + +} From 4c172fbb1154c7aee3bf3e3edab0188c24a6bf68 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 27 May 2014 04:42:04 +0000 Subject: [PATCH 195/256] Progress on generic sync. --- lib/object-codec.js | 15 ++++++++----- mixins/sync.js | 55 +++++++++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index 11f1954..537a65b 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -22,19 +22,24 @@ var decoders = exports.decoders ={ // (buffer) -> {type:type, body:raw-buffer} exports.deframe = deframe; +// Export git style path sort in case it's wanted. +exports.treeSort = treeSort; + function encodeBlob(body) { if (!bodec.isBinary(body)) throw new TypeError("Blobs must be binary values"); return body; } +function treeSort(a, b) { + var aa = a + "/"; + var bb = b + "/"; + return aa > bb ? 1 : aa < bb ? -1 : 0; +} + function encodeTree(body) { var tree = ""; if (Array.isArray(body)) throw new TypeError("Tree must be in object form"); - var names = Object.keys(body).sort(function (a, b) { - var aa = a + "/"; - var bb = b + "/"; - return aa > bb ? 1 : aa < bb ? -1 : 0; - }); + var names = Object.keys(body).sort(treeSort); for (var i = 0, l = names.length; i < l; i++) { var name = names[i]; var entry = body[name]; diff --git a/mixins/sync.js b/mixins/sync.js index a1ddff3..92ab2ab 100644 --- a/mixins/sync.js +++ b/mixins/sync.js @@ -35,42 +35,58 @@ function sync(remote, options, callback) { if (hashes.local) throw "TODO: Implement local"; console.log(hashes); var depth = options.localDepth || Infinity; - downloadCommit(local, remote, hashes.remote, function (err, commit) { + downloadCommit(local, remote, hashes.remote, depth, function (err) { if (err) return callback(err); - console.log("commit", commit); + console.log("DOWNLOADED") }); }); } -function downloadCommits(local, remote, hash, depth, callback) { +function downloadCommit(local, remote, hash, depth, callback) { + if (!callback) return downloadCommit.bind(null, local, remote, hash, depth); -} + var commit; -function downloadCommit(local, remote, hash, callback) { local.loadDirectAs("commit", hash, onCheck); - function onCheck(err, commit) { - if (err || commit) return callback(err, commit); + function onCheck(err, result) { + if (err) return callback(err); + + if (result) { + commit = result; + return onSaved(null, hash); + } remote.loadAs("commit", hash, onCommit); } - function onCommit(err, commit) { - if (!commit) return callback(err || new Error("Missing remote commit " + hash)); + function onCommit(err, result) { + if (!result) return callback(err || new Error("Missing remote commit " + hash)); + commit = result; downloadTree(local, remote, commit.tree, onTree); function onTree(err) { if (err) return callback(err); local.saveAs("commit", commit, onSaved); } + } - function onSaved(err, newHash) { - if (err) return callback(err); - if (newHash !== hash) { - return callback(new Error("Hash mismatch for commit")); - } - callback(null, commit); + function onSaved(err, newHash) { + if (err) return callback(err); + if (newHash !== hash) { + console.error(commit); + console.error({ + expected: hash, + actual: newHash + }); + return callback(new Error("Hash mismatch for commit")); } + + depth--; + if (!depth || !commit.parents.length) return callback(); + carallel(commit.parents.map(function (parent) { + return downloadCommit(local, remote, parent, depth); + }), callback); } } @@ -105,10 +121,7 @@ function downloadTree(local, remote, hash, callback) { } callback(); } - } - - } function downloadBlob(local, remote, hash, callback) { @@ -127,7 +140,11 @@ function downloadBlob(local, remote, hash, callback) { function onSaved(err, newHash) { if (err) return callback(err); if (newHash !== hash) { - return callback(new Error("Hash mismatch for commit")); + console.error({ + expected: hash, + actual: newHash + }); + return callback(new Error("Hash mismatch for blob")); } callback(); } From d23679952eba5b2661b4ed34237fa9b033c58801 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 3 Jun 2014 20:17:22 +0000 Subject: [PATCH 196/256] Document current sponsored features. --- SPONSORS.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SPONSORS.md diff --git a/SPONSORS.md b/SPONSORS.md new file mode 100644 index 0000000..13cced1 --- /dev/null +++ b/SPONSORS.md @@ -0,0 +1,9 @@ +# Sponsored Development + +As a company, you can sponsor development of specefic features to the js-git ecosystem. + +## In Progress Sponsored Features + + - JS-Git Encrypted Filesystem - Anonymous + - Tedit - Web Runtime - Anonymous + - Tedit - Live Export to VFS - Anonymous From 3fbd45bb0c54cd36ce08f5fde52f6baa61ca369d Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 3 Jun 2014 20:17:47 +0000 Subject: [PATCH 197/256] Move live export to completed. --- SPONSORS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SPONSORS.md b/SPONSORS.md index 13cced1..0e4764b 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -6,4 +6,7 @@ As a company, you can sponsor development of specefic features to the js-git eco - JS-Git Encrypted Filesystem - Anonymous - Tedit - Web Runtime - Anonymous + +## Completed Sponsored Features + - Tedit - Live Export to VFS - Anonymous From 9e20d3b682b903fc7226d4a7941e4e1e0835bacd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 3 Jun 2014 20:18:13 +0000 Subject: [PATCH 198/256] Add dash --- SPONSORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.md b/SPONSORS.md index 0e4764b..f40f188 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -4,7 +4,7 @@ As a company, you can sponsor development of specefic features to the js-git eco ## In Progress Sponsored Features - - JS-Git Encrypted Filesystem - Anonymous + - JS-Git - Encrypted Filesystem - Anonymous - Tedit - Web Runtime - Anonymous ## Completed Sponsored Features From f04dbe2e537998fc5f47ef67c595c1c9f36b1736 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 3 Jun 2014 20:19:03 +0000 Subject: [PATCH 199/256] Fix typo --- SPONSORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.md b/SPONSORS.md index f40f188..1c23536 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -1,6 +1,6 @@ # Sponsored Development -As a company, you can sponsor development of specefic features to the js-git ecosystem. +As a company, you can sponsor development of specific features to the js-git ecosystem. ## In Progress Sponsored Features From a6c737c662d9e1d9804a423e7b01fe74e1572a05 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 3 Jun 2014 21:16:34 +0000 Subject: [PATCH 200/256] Fix add-cache to use new interface --- mixins/add-cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/add-cache.js b/mixins/add-cache.js index 52e2f09..6d94285 100644 --- a/mixins/add-cache.js +++ b/mixins/add-cache.js @@ -42,7 +42,7 @@ function addCache(repo, cache) { function saveAsCached(type, value, callback) { saveAs.call(repo, type, value, onSave); - function onSave(err, hash, value) { + function onSave(err, hash) { if (err) return callback(err); // Store in disk, forcing hash to match. cache.saveAs(type, value, callback, hash); From 54a00f000df0f41fffc57072f2fead5e07178744 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 3 Jun 2014 22:14:45 +0000 Subject: [PATCH 201/256] Fix race condition with refs writing. --- lib/git-fs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/git-fs.js b/lib/git-fs.js index 1a2ba04..c8d34d3 100644 --- a/lib/git-fs.js +++ b/lib/git-fs.js @@ -89,12 +89,10 @@ module.exports = function (repo, options) { function onNewTree(err, hash) { if (err) return callall(err); options.setRootTree(hash, onSaveRoot); - } function onSaveRoot(err) { if (err) return callall(err); - toWrite = {}; writing = false; callall(); defer(check); @@ -104,6 +102,7 @@ module.exports = function (repo, options) { function pullFiles() { var files = Object.keys(toWrite).map(function (path) { var content = toWrite[path]; + delete toWrite[path]; var mode = modes.blob; if (options.shouldEncrypt && options.shouldEncrypt(path)) { mode = modes.sym; From 6d4f1276a28b1917ff74ab626e9732b8c2a615e2 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 4 Jun 2014 19:36:41 +0000 Subject: [PATCH 202/256] Implement one-way sync primitive --- mixins/sync.js | 214 ++++++++++++++++++++++--------------------------- 1 file changed, 94 insertions(+), 120 deletions(-) diff --git a/mixins/sync.js b/mixins/sync.js index 92ab2ab..1434efa 100644 --- a/mixins/sync.js +++ b/mixins/sync.js @@ -1,160 +1,134 @@ "use strict"; -var carallel = require('carallel'); var modes = require('../lib/modes'); module.exports = function (repo) { repo.sync = sync; }; -function sync(remote, options, callback) { +// Download remote ref with depth +// Make sure to use Infinity for depth on github mounts or anything that +// doesn't allow shallow clones. +function sync(remote, ref, depth, callback) { /*jshint: validthis: true*/ - if (!callback) return sync.bind(this, remote, options); + if (!callback) return sync.bind(this, remote, ref, depth); var local = this; - if (typeof local.readRef !== "function") { - throw new TypeError("local repo is missing readRef method"); - } - if (typeof local.loadDirectAs !== "function") { - throw new TypeError("local repo is missing loadDirectAs method"); - } - if (typeof remote.readRef !== "function") { - throw new TypeError("remote repo is missing readRef method"); - } - console.log({ - local: local, - remote: remote, - options: options - }); - var localRef = options.localRef || "refs/heads/master"; - var remoteRef = options.remoteRef || localRef; - carallel({ - local: local.readRef(localRef), - remote: remote.readRef(remoteRef) - }, function (err, hashes) { + depth = depth || Infinity; + + var hasCache = {}; + + remote.readRef(ref, function (err, hash) { if (err) return callback(err); - if (hashes.local) throw "TODO: Implement local"; - console.log(hashes); - var depth = options.localDepth || Infinity; - downloadCommit(local, remote, hashes.remote, depth, function (err) { + importCommit(hash, depth, function (err) { if (err) return callback(err); - console.log("DOWNLOADED") + callback(null, hash); }); }); -} - -function downloadCommit(local, remote, hash, depth, callback) { - if (!callback) return downloadCommit.bind(null, local, remote, hash, depth); - - var commit; - - local.loadDirectAs("commit", hash, onCheck); - - function onCheck(err, result) { - if (err) return callback(err); - if (result) { - commit = result; - return onSaved(null, hash); - } - remote.loadAs("commit", hash, onCommit); + // Caching has check. + function check(hash, callback) { + if (hasCache[hash]) return callback(null, true); + local.hasHash(hash, function (err, has) { + if (err) return callback(err); + hasCache[hash] = has; + callback(null, has); + }); } - function onCommit(err, result) { - if (!result) return callback(err || new Error("Missing remote commit " + hash)); + function importCommit(hash, depth, callback) { + check(hash, onCheck); - commit = result; - downloadTree(local, remote, commit.tree, onTree); - - function onTree(err) { - if (err) return callback(err); - local.saveAs("commit", commit, onSaved); + function onCheck(err, has) { + if (err || has) return callback(err); + remote.loadAs("commit", hash, onLoad); } - } - function onSaved(err, newHash) { - if (err) return callback(err); - if (newHash !== hash) { - console.error(commit); - console.error({ - expected: hash, - actual: newHash - }); - return callback(new Error("Hash mismatch for commit")); + function onLoad(err, commit) { + if (!commit) return callback(err || new Error("Missing commit " + hash)); + var i = 0; + importTree(commit.tree, onImport); + + function onImport(err) { + if (err) return callback(err); + if (i >= commit.parents.length || depth <= 1) { + return local.saveAs("commit", commit, onSave); + } + importCommit(commit.parents[i++], depth - 1, onImport); + } } - depth--; - if (!depth || !commit.parents.length) return callback(); - carallel(commit.parents.map(function (parent) { - return downloadCommit(local, remote, parent, depth); - }), callback); + function onSave(err, newHash) { + if (err) return callback(err); + if (newHash !== hash) { + return new Error("Commit hash mismatch " + hash + " != " + newHash); + } + hasCache[hash] = true; + callback(); + } } -} - -function downloadTree(local, remote, hash, callback) { - if (!callback) return downloadTree.bind(null, local, remote, hash); - local.loadDirectAs("tree", hash, onCheck); + function importTree(hash, callback) { + check(hash, onCheck); - function onCheck(err, tree) { - if (err || tree) return callback(err); - remote.loadAs("tree", hash, onTree); - } + function onCheck(err, has) { + if (err || has) return callback(err); + remote.loadAs("tree", hash, onLoad); + } - function onTree(err, tree) { - if (!tree) return callback(err || new Error("Missing remote tree " + hash)); - carallel(Object.keys(tree).map(function (name) { - return downloadEntry(local, remote, tree[name]); - }).filter(Boolean), function (err) { - if (err) return callback(err); - local.saveAs("tree", tree, onSaved); - }); + function onLoad(err, tree) { + if (!tree) return callback(err || new Error("Missing tree " + hash)); + var i = 0; + var names = Object.keys(tree); + onImport(); + + function onImport(err) { + if (err) return callback(err); + if (i >= names.length) { + return local.saveAs("tree", tree, onSave); + } + var name = names[i++]; + var entry = tree[name]; + if (modes.isBlob(entry.mode)) { + return importBlob(entry.hash, onImport); + } + if (entry.mode === modes.tree) { + return importTree(entry.hash, onImport); + } + // Skip others. + onImport(); + } + } - function onSaved(err, newHash) { + function onSave(err, newHash) { if (err) return callback(err); if (newHash !== hash) { - console.error(tree); - console.error({ - expected: hash, - actual: newHash - }); - return callback(new Error("Hash mismatch for tree")); + return new Error("Tree hash mismatch " + hash + " != " + newHash); } + hasCache[hash] = true; callback(); } } -} -function downloadBlob(local, remote, hash, callback) { - if (!callback) return downloadBlob.bind(null, local, remote, hash); + function importBlob(hash, callback) { + check(hash, onCheck); - local.loadDirectAs("blob", hash, function (err, blob) { - if (err || blob) return callback(err); - remote.loadAs("blob", hash, onBlob); - }); + function onCheck(err, has) { + if (err || has) return callback(err); + remote.loadAs("blob", hash, onLoad); + } - function onBlob(err, blob) { - if (!blob) return callback(err || new Error("Missing remote blob " + hash)); - local.saveAs("blob", blob, onSaved); - } + function onLoad(err, blob) { + if (!blob) return callback(err || new Error("Missing blob " + hash)); + local.saveAs("blob", blob, onSave); + } - function onSaved(err, newHash) { - if (err) return callback(err); - if (newHash !== hash) { - console.error({ - expected: hash, - actual: newHash - }); - return callback(new Error("Hash mismatch for blob")); + function onSave(err, newHash) { + if (err) return callback(err); + if (newHash !== hash) { + return new Error("Blob hash mismatch " + hash + " != " + newHash); + } + hasCache[hash] = true; + callback(); } - callback(); } } - -function downloadEntry(local, remote, entry) { - if (entry.mode === modes.tree) return downloadTree(local, remote, entry.hash); - if (modes.isBlob(entry.mode)) return downloadBlob(local, remote, entry.hash); -} - -function findCommon(local, localHash, remote, remoteHash, callback) { - -} From d6f3a56b04412ea0640b7ee3b084b5f4179bacff Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 4 Jun 2014 23:04:14 +0000 Subject: [PATCH 203/256] Fix sync and add find-common helper --- lib/find-common.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ mixins/sync.js | 35 ++++++++++++++++++---------- mixins/walkers.js | 2 +- 3 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 lib/find-common.js diff --git a/lib/find-common.js b/lib/find-common.js new file mode 100644 index 0000000..245a5e2 --- /dev/null +++ b/lib/find-common.js @@ -0,0 +1,58 @@ +function oneCall(fn) { + var done = false; + return function () { + if (done) return; + done = true; + return fn.apply(this, arguments); + }; +} + +module.exports = findCommon; + +function findCommon(repo, a, b, callback) { + callback = oneCall(callback); + var ahead = 0, behind = 0; + var aStream, bStream; + var aCommit, bCommit; + + if (a === b) return callback(null, ahead, behind); + repo.logWalk(a, onAStream); + repo.logWalk(b, onBStream); + + function onAStream(err, stream) { + if (err) return callback(err); + aStream = stream; + aStream.read(onA); + } + + function onBStream(err, stream) { + if (err) return callback(err); + bStream = stream; + bStream.read(onB); + } + + function onA(err, commit) { + if (!commit) return callback(err || new Error("No common commit")); + aCommit = commit; + if (bCommit) compare(); + } + + function onB(err, commit) { + if (!commit) return callback(err || new Error("No common commit")); + bCommit = commit; + if (aCommit) compare(); + } + + function compare() { + if (aCommit.hash === bCommit.hash) return callback(null, ahead, behind); + if (aCommit.author.date.seconds > bCommit.author.date.seconds) { + ahead++; + aStream.read(onA); + } + else { + behind++; + bStream.read(onB); + } + } + +} diff --git a/mixins/sync.js b/mixins/sync.js index 1434efa..895c4b0 100644 --- a/mixins/sync.js +++ b/mixins/sync.js @@ -2,18 +2,29 @@ var modes = require('../lib/modes'); -module.exports = function (repo) { - repo.sync = sync; +module.exports = function (local, remote) { + local.fetch = fetch; + local.send = send; + local.readRemoteRef = remote.readRef.bind(remote); + local.updateRemoteRef = remote.updateRef.bind(remote); + + function fetch(ref, depth, callback) { + if (!callback) return fetch.bind(local, ref, depth); + sync(local, remote, ref, depth, callback); + } + + function send(ref, callback) { + if (!callback) return send.bind(local, ref); + sync(remote, local, ref, Infinity, callback); + } }; // Download remote ref with depth // Make sure to use Infinity for depth on github mounts or anything that // doesn't allow shallow clones. -function sync(remote, ref, depth, callback) { - /*jshint: validthis: true*/ - if (!callback) return sync.bind(this, remote, ref, depth); - var local = this; - depth = depth || Infinity; +function sync(local, remote, ref, depth, callback) { + + depth = depth || 1; var hasCache = {}; @@ -26,9 +37,9 @@ function sync(remote, ref, depth, callback) { }); // Caching has check. - function check(hash, callback) { + function check(type, hash, callback) { if (hasCache[hash]) return callback(null, true); - local.hasHash(hash, function (err, has) { + local.hasHash(type, hash, function (err, has) { if (err) return callback(err); hasCache[hash] = has; callback(null, has); @@ -36,7 +47,7 @@ function sync(remote, ref, depth, callback) { } function importCommit(hash, depth, callback) { - check(hash, onCheck); + check("commit", hash, onCheck); function onCheck(err, has) { if (err || has) return callback(err); @@ -68,7 +79,7 @@ function sync(remote, ref, depth, callback) { } function importTree(hash, callback) { - check(hash, onCheck); + check("tree", hash, onCheck); function onCheck(err, has) { if (err || has) return callback(err); @@ -110,7 +121,7 @@ function sync(remote, ref, depth, callback) { } function importBlob(hash, callback) { - check(hash, onCheck); + check("blob", hash, onCheck); function onCheck(err, has) { if (err || has) return callback(err); diff --git a/mixins/walkers.js b/mixins/walkers.js index 9752a88..59a5475 100644 --- a/mixins/walkers.js +++ b/mixins/walkers.js @@ -37,7 +37,7 @@ function logWalk(ref, callback) { function loadKey(hash, callback) { return repo.loadAs("commit", hash, function (err, commit) { - if (err) return callback(err); + if (!commit) return callback(err || new Error("Missing commit " + hash)); commit.hash = hash; if (hash === last) commit.last = true; return callback(null, commit); From 5b10e38407ee88b340d60cd901f5ca07952b1830 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 5 Jun 2014 03:46:00 +0000 Subject: [PATCH 204/256] Fix error reporting in sync --- mixins/sync.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mixins/sync.js b/mixins/sync.js index 895c4b0..d0fa687 100644 --- a/mixins/sync.js +++ b/mixins/sync.js @@ -23,13 +23,13 @@ module.exports = function (local, remote) { // Make sure to use Infinity for depth on github mounts or anything that // doesn't allow shallow clones. function sync(local, remote, ref, depth, callback) { - - depth = depth || 1; + if (typeof ref !== "string") throw new TypeError("ref must be string"); + if (typeof depth !== "number") throw new TypeError("depth must be number"); var hasCache = {}; remote.readRef(ref, function (err, hash) { - if (err) return callback(err); + if (!hash) return callback(err); importCommit(hash, depth, function (err) { if (err) return callback(err); callback(null, hash); @@ -38,6 +38,8 @@ function sync(local, remote, ref, depth, callback) { // Caching has check. function check(type, hash, callback) { + if (typeof type !== "string") throw new TypeError("type must be string"); + if (typeof hash !== "string") throw new TypeError("hash must be string"); if (hasCache[hash]) return callback(null, true); local.hasHash(type, hash, function (err, has) { if (err) return callback(err); @@ -71,7 +73,7 @@ function sync(local, remote, ref, depth, callback) { function onSave(err, newHash) { if (err) return callback(err); if (newHash !== hash) { - return new Error("Commit hash mismatch " + hash + " != " + newHash); + return callback(new Error("Commit hash mismatch " + hash + " != " + newHash)); } hasCache[hash] = true; callback(); @@ -113,7 +115,7 @@ function sync(local, remote, ref, depth, callback) { function onSave(err, newHash) { if (err) return callback(err); if (newHash !== hash) { - return new Error("Tree hash mismatch " + hash + " != " + newHash); + return callback(new Error("Tree hash mismatch " + hash + " != " + newHash)); } hasCache[hash] = true; callback(); @@ -136,7 +138,7 @@ function sync(local, remote, ref, depth, callback) { function onSave(err, newHash) { if (err) return callback(err); if (newHash !== hash) { - return new Error("Blob hash mismatch " + hash + " != " + newHash); + return callback(new Error("Blob hash mismatch " + hash + " != " + newHash)); } hasCache[hash] = true; callback(); From 46b730d3aa20153149cf251f63cb481d4190b9a8 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 9 Jun 2014 18:48:48 +0000 Subject: [PATCH 205/256] Add simple hasHash --- mixins/fs-db.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 80f6276..5693cb0 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -29,6 +29,7 @@ module.exports = function (repo, fs) { repo.saveRaw = saveRaw; repo.readRef = readRef; repo.updateRef = updateRef; + repo.hasHash = hasHash; function updateRef(ref, hash, callback) { if (!callback) return updateRef.bind(repo, ref, hash); @@ -122,6 +123,14 @@ module.exports = function (repo, fs) { }); } + function hasHash(type, hash, callback) { + if (!callback) return hasHash.bind(repo, type, hash); + loadRaw(hash, function (err, body) { + if (err) return callback(err); + callback(null, !!body); + }); + } + function loadRaw(hash, callback) { if (!callback) return loadRaw.bind(repo, hash); var path = hashToPath(hash); From 55a5929e6fdf93ca89c4941ca0063d331fbab14b Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 9 Jun 2014 19:46:18 +0000 Subject: [PATCH 206/256] Add fall-through --- mixins/fall-through.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 mixins/fall-through.js diff --git a/mixins/fall-through.js b/mixins/fall-through.js new file mode 100644 index 0000000..3953499 --- /dev/null +++ b/mixins/fall-through.js @@ -0,0 +1,26 @@ +var modes = require('../lib/modes'); + +module.exports = function (local, remote) { + var loadAs = local.loadAs; + local.loadAs = newLoadAs; + function newLoadAs(type, hash, callback) { + if (!callback) return newLoadAs.bind(local. type, hash); + loadAs.call(local, type, hash, function (err, body) { + if (err) return callback(err); + if (body === undefined) return remote.loadAs(type, hash, callback); + callback(null, body); + }); + } + + var readRef = local.readRef; + local.readRef = newReadRef; + function newReadRef(ref, callback) { + if (!callback) return newReadRef.bind(local. ref); + readRef.call(local, ref, function (err, body) { + if (err) return callback(err); + if (body === undefined) return remote.readRef(ref, callback); + callback(null, body); + }); + } + +}; From a5879695181270c8493b9be289bdd386e7749061 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 10 Jun 2014 02:34:42 +0000 Subject: [PATCH 207/256] Fix mem-cache to work with atom-shell. --- mixins/mem-cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/mem-cache.js b/mixins/mem-cache.js index 72d0b5c..0434481 100644 --- a/mixins/mem-cache.js +++ b/mixins/mem-cache.js @@ -2,6 +2,7 @@ var encoders = require('../lib/object-codec').encoders; var decoders = require('../lib/object-codec').decoders; +var Binary = require('bodec').Binary; var cache = memCache.cache = {}; module.exports = memCache; @@ -35,7 +36,6 @@ function memCache(repo) { }); } } -var Binary = typeof Buffer === "function" ? Buffer : Uint8Array; function dupe(type, value) { if (type === "blob") { if (type.length >= 100) return value; From 9fea589da0a807cfe1f4238c9ab0a42f4446cf45 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Mon, 23 Jun 2014 11:20:23 -0500 Subject: [PATCH 208/256] Fix formats mixin to be continuable friendly --- mixins/formats.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mixins/formats.js b/mixins/formats.js index 44ab90e..182e27a 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -9,6 +9,7 @@ module.exports = function (repo) { repo.saveAs = newSaveAs; function newLoadAs(type, hash, callback) { + if (!callback) return newLoadAs.bind(repo, type, hash); var realType = type === "text" ? "blob": type === "array" ? "tree" : type; return loadAs.call(repo, realType, hash, onLoad); @@ -22,6 +23,7 @@ module.exports = function (repo) { } function newSaveAs(type, body, callback) { + if (!callback) return newSaveAs.bind(repo, type, body); type = type === "text" ? "blob": type === "array" ? "tree" : type; if (type === "blob") { From 03ea444e55f468d7270e77196701da5350a67c50 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Wed, 2 Jul 2014 11:56:55 -0500 Subject: [PATCH 209/256] Fixes --- mixins/path-to-entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js index 574a28a..3cc419c 100644 --- a/mixins/path-to-entry.js +++ b/mixins/path-to-entry.js @@ -6,7 +6,7 @@ module.exports = function (repo) { }; function pathToEntry(rootTree, path, callback) { - if (!callback) return pathToEntry.bind(this, path); + if (!callback) return pathToEntry.bind(this, rootTree, path); var repo = this; var mode = modes.tree; var hash = rootTree; From 3bbaefa6a88e3014c328390456f029e96d207867 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 00:42:25 -0500 Subject: [PATCH 210/256] Use culvert channel based pack-ops. --- mixins/pack-ops.js | 68 +++++++++++++++++++++------------------------- package.json | 3 +- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/mixins/pack-ops.js b/mixins/pack-ops.js index 81d38b3..baaefc2 100644 --- a/mixins/pack-ops.js +++ b/mixins/pack-ops.js @@ -1,26 +1,28 @@ -var binary = require('bodec'); +"use strict"; + var sha1 = require('git-sha1'); var applyDelta = require('../lib/apply-delta.js'); var codec = require('../lib/object-codec.js'); var decodePack = require('../lib/pack-codec.js').decodePack; var encodePack = require('../lib/pack-codec.js').encodePack; +var makeChannel = require('culvert'); module.exports = function (repo) { - // packStream is a simple-stream containing raw packfile binary data + // packChannel is a writable culvert channel {put,drain} containing raw packfile binary data // opts can contain "onProgress" or "onError" hook functions. // callback will be called with a list of all unpacked hashes on success. - repo.unpack = unpack; // (packStream, opts) -> hashes + repo.unpack = unpack; // (packChannel, opts) => hashes // hashes is an array of hashes to pack - // callback will be a simple-stream containing raw packfile binary data - repo.pack = pack; // (hashes, opts) -> packStream - + // packChannel will be a readable culvert channel {take} containing raw packfile binary data + repo.pack = pack; // (hashes, opts) => packChannel }; -function unpack(packStream, opts, callback) { - if (!callback) return unpack.bind(this, packStream, opts); +function unpack(packChannel, opts, callback) { + /*jshint validthis:true*/ + if (!callback) return unpack.bind(this, packChannel, opts); - packStream = applyParser(packStream, decodePack); + packChannel = applyParser(packChannel, decodePack, callback); var repo = this; @@ -34,7 +36,7 @@ function unpack(packStream, opts, callback) { // key is hash we're waiting for, value is array of items that are waiting. var pending = {}; - return packStream.read(onStats); + return packChannel.take(onStats); function onDone(err) { if (done) return; @@ -47,7 +49,7 @@ function unpack(packStream, opts, callback) { if (err) return onDone(err); version = stats.version; num = stats.num; - packStream.read(onRead); + packChannel.take(onRead); } function objectProgress(more) { @@ -124,20 +126,21 @@ function unpack(packStream, opts, callback) { function onSave(err) { if (err) return callback(err); - packStream.read(onRead); + packChannel.take(onRead); } function enqueueDelta(item) { var list = pending[item.ref]; if (!list) pending[item.ref] = [item]; else list.push(item); - packStream.read(onRead); + packChannel.take(onRead); } } // TODO: Implement delta refs to reduce stream size function pack(hashes, opts, callback) { + /*jshint validthis:true*/ if (!callback) return pack.bind(this, hashes, opts); var repo = this; var i = 0, first = true, done = false; @@ -175,33 +178,24 @@ function values(object) { } -function applyParser(stream, parser) { - var write = parser(onData); - var cb = null; - var queue = []; - return { read: read, abort: stream.abort }; +function applyParser(stream, parser, onError) { + var extra = makeChannel(); + extra.put = parser(extra.put); + stream.take(onData); - function read(callback) { - if (queue.length) return callback(null, queue.shift()); - if (cb) return callback(new Error("Only one read at a time.")); - cb = callback; - stream.read(onRead); + function onData(err, item) { + if (err) return onError(err); + var more; + try { more = extra.put(item); } + catch (err) { return onError(err); } + if (more) stream.take(onData); + else extra.drain(onDrain); } - function onRead(err, item) { - var callback = cb; - cb = null; - if (err) return callback(err); - try { - write(item); - } - catch (err) { - return callback(err); - } - return read(callback); + function onDrain(err) { + if (err) return onError(err); + stream.take(onData); } - function onData(item) { - queue.push(item); - } + return { take: extra.take }; } diff --git a/package.json b/package.json index 77062f0..9516879 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "bodec": "git://github.com/creationix/bodec.git", "pathjoin": "git://github.com/creationix/pathjoin.git", "git-sha1": "git://github.com/creationix/git-sha1.git", - "pako": "git://github.com/nodeca/pako.git" + "pako": "git://github.com/nodeca/pako.git", + "culvert": "git://github.com/creationix/culvert.git" } } From 96909a556176abf005b4d7deef992db6326194aa Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 00:46:53 -0500 Subject: [PATCH 211/256] Bump version to 0.7.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9516879..6c84af0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.0", + "version": "0.7.1", "description": "Git Implemented in JavaScript", "keywords": ["git", "js-git"], "repository": { @@ -20,6 +20,6 @@ "pathjoin": "git://github.com/creationix/pathjoin.git", "git-sha1": "git://github.com/creationix/git-sha1.git", "pako": "git://github.com/nodeca/pako.git", - "culvert": "git://github.com/creationix/culvert.git" + "culvert": "~0.1.1" } } From f8f30def8115dfb8066d12a1ccd8db1297c78a98 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 01:05:05 -0500 Subject: [PATCH 212/256] Add in net code from clone test --- lib/pkt-line.js | 128 +++++++++++++++++++++++++++++ lib/wrap-handler.js | 21 +++++ net/git-fetch-pack.js | 166 ++++++++++++++++++++++++++++++++++++++ net/request-xhr.js | 36 +++++++++ net/tcp-chrome-sockets.js | 108 +++++++++++++++++++++++++ net/tcp-ws-proxy.js | 79 ++++++++++++++++++ net/transport-http.js | 102 +++++++++++++++++++++++ net/transport-tcp.js | 48 +++++++++++ 8 files changed, 688 insertions(+) create mode 100644 lib/pkt-line.js create mode 100644 lib/wrap-handler.js create mode 100644 net/git-fetch-pack.js create mode 100644 net/request-xhr.js create mode 100644 net/tcp-chrome-sockets.js create mode 100644 net/tcp-ws-proxy.js create mode 100644 net/transport-http.js create mode 100644 net/transport-tcp.js diff --git a/lib/pkt-line.js b/lib/pkt-line.js new file mode 100644 index 0000000..4134462 --- /dev/null +++ b/lib/pkt-line.js @@ -0,0 +1,128 @@ +"use strict"; + +var bodec = require('bodec'); +var PACK = bodec.fromRaw("PACK"); + +module.exports = { + deframer: deframer, + framer: framer +}; + +function deframer(emit) { + var state = 0; + var offset = 4; + var length = 0; + var data; + var more = true; + + return function (item) { + + // Forward the EOS marker + if (item === undefined) return emit(); + + // Once we're in pack mode, everything goes straight through + if (state === 3) return emit(item); + + // Otherwise parse the data using a state machine. + for (var i = 0, l = item.length; i < l; i++) { + var byte = item[i]; + if (state === 0) { + var val = fromHexChar(byte); + if (val === -1) { + if (byte === PACK[0]) { + offset = 1; + state = 2; + continue; + } + state = -1; + throw new SyntaxError("Not a hex char: " + String.fromCharCode(byte)); + } + length |= val << ((--offset) * 4); + if (offset === 0) { + if (length === 4) { + offset = 4; + more = emit(""); + } + else if (length === 0) { + offset = 4; + more = emit(null); + } + else if (length > 4) { + length -= 4; + data = bodec.create(length); + state = 1; + } + else { + state = -1; + throw new SyntaxError("Invalid length: " + length); + } + } + } + else if (state === 1) { + data[offset++] = byte; + if (offset === length) { + offset = 4; + state = 0; + length = 0; + if (data[0] === 1) { + more = emit(bodec.slice(data, 1)); + } + else if (data[0] === 2) { + more = emit({progress: bodec.toUnicode(data, 1)}); + } + else if (data[0] === 3) { + more = emit({error: bodec.toUnicode(data, 1)}); + } + else { + more = emit(bodec.toUnicode(data).trim()); + } + } + } + else if (state === 2) { + if (offset < 4 && byte === PACK[offset++]) { + continue; + } + state = 3; + more = emit(bodec.join([PACK, bodec.subarray(item, i)])); + break; + } + else { + throw new Error("pkt-line decoder in invalid state"); + } + } + + return more; + }; + +} + +function framer(emit) { + return function (item) { + if (item === undefined) return emit(); + if (item === null) { + return emit(bodec.fromRaw("0000")); + } + if (typeof item === "string") { + item = bodec.fromUnicode(item); + } + return emit(bodec.join([frameHead(item.length + 4), item])); + }; +} + +function frameHead(length) { + var buffer = bodec.create(4); + buffer[0] = toHexChar(length >>> 12); + buffer[1] = toHexChar((length >>> 8) & 0xf); + buffer[2] = toHexChar((length >>> 4) & 0xf); + buffer[3] = toHexChar(length & 0xf); + return buffer; +} + +function fromHexChar(val) { + return (val >= 0x30 && val < 0x40) ? val - 0x30 : + ((val > 0x60 && val <= 0x66) ? val - 0x57 : -1); +} + +function toHexChar(val) { + return val < 0x0a ? val + 0x30 : val + 0x57; +} diff --git a/lib/wrap-handler.js b/lib/wrap-handler.js new file mode 100644 index 0000000..9a1c050 --- /dev/null +++ b/lib/wrap-handler.js @@ -0,0 +1,21 @@ +"use strict"; + +module.exports = wrapHandler; + +function wrapHandler(fn, onError) { + if (onError) { + return function (err, value) { + if (err) return onError(err); + try { + return fn(value); + } + catch (err) { + return onError(err); + } + }; + } + return function (err, value) { + if (err) throw err; + return fn(value); + }; +} diff --git a/net/git-fetch-pack.js b/net/git-fetch-pack.js new file mode 100644 index 0000000..b00898f --- /dev/null +++ b/net/git-fetch-pack.js @@ -0,0 +1,166 @@ +"use strict"; + +var makeChannel = require('culvert'); +var wrapHandler = require('../lib/wrap-handler'); + +module.exports = fetchPack; + +function fetchPack(transport, onError) { + + if (!onError) onError = throwIt; + + // Wrap our handler functions to route errors properly. + onRef = wrapHandler(onRef, onError); + onWant = wrapHandler(onWant, onError); + onNak = wrapHandler(onNak, onError); + onMore = wrapHandler(onMore, onError); + onReady = wrapHandler(onReady, onError); + + var caps = null; + var capsSent = false; + var refs = {}; + + // Create a duplex channel for talking with the agent. + var libraryChannel = makeChannel(); + var agentChannel = makeChannel(); + var api = { + put: libraryChannel.put, + drain: libraryChannel.drain, + take: agentChannel.take + }; + + // Start the connection and listen for the response. + var socket = transport("git-upload-pack", onError); + socket.take(onRef); + + // Return the other half of the duplex API channel. + return { + put: agentChannel.put, + drain: agentChannel.drain, + take: libraryChannel.take + }; + + function onRef(line) { + if (line === undefined) { + throw new Error("Socket disconnected"); + } + if (line === null) { + api.put(refs); + api.take(onWant); + return; + } + else if (!caps) { + caps = {}; + Object.defineProperty(refs, "caps", {value: caps}); + var index = line.indexOf("\0"); + line.substring(index + 1).split(" ").forEach(function (cap) { + var i = cap.indexOf("="); + if (i >= 0) { + caps[cap.substring(0, i)] = cap.substring(i + 1); + } + else { + caps[cap] = true; + } + }); + line = line.substring(0, index); + } + var match = line.match(/(^[0-9a-f]{40}) (.*)$/); + if (!match) { + throw new Error("Invalid line: " + line); + } + refs[match[2]] = match[1]; + socket.take(onRef); + } + + var packChannel; + var progressChannel; + var errorChannel; + + function onWant(line) { + if (line === null) { + socket.put(null); + return api.take(onWant); + } + if (line.deepen) { + socket.put("deepen " + line.deepen + "\n"); + return api.take(onWant); + } + if (line.want) { + var extra = ""; + if (!capsSent) { + capsSent = true; + if (caps["ofs-delta"]) extra += " ofs-delta"; + if (caps["thin-pack"]) extra += " thin-pack"; + // if (caps["multi_ack_detailed"]) extra += " multi_ack_detailed"; + // else if (caps["multi_ack"]) extra +=" multi_ack"; + if (caps["side-band-64k"]) extra += " side-band-64k"; + else if (caps["side-band"]) extra += " side-band"; + // if (caps["agent"]) extra += " agent=" + agent; + if (caps.agent) extra += " agent=" + caps.agent; + } + extra += "\n"; + socket.put("want " + line.want + extra); + return api.take(onWant); + } + if (line.done) { + socket.put("done\n"); + return socket.take(onNak); + } + throw new Error("Invalid have/want command"); + } + + function onNak(line) { + if (line !== "NAK") { + throw new Error("Expected NAK"); + } + packChannel = makeChannel(); + progressChannel = makeChannel(); + errorChannel = makeChannel(); + api.put({ + pack: packChannel.take, + progress: progressChannel.take, + error: errorChannel.take, + }); + socket.take(onMore); + } + + function onMore(line) { + + if (line === undefined) { + packChannel.put(); + progressChannel.put(); + errorChannel.put(); + return api.put(); + } + if (line === null) { + api.put(line); + } + else { + if (line.progress) { + progressChannel.put(line.progress); + } + else if (line.error) { + errorChannel.put(line.error); + } + else { + if (!packChannel.put(line)) { + return packChannel.drain(onReady); + } + } + } + socket.take(onMore); + } + + function onReady() { + socket.take(onMore); + } + +} + +var defer = require('js-git/lib/defer'); +function throwIt(err) { + defer(function () { + throw err; + }); + // throw err; +} diff --git a/net/request-xhr.js b/net/request-xhr.js new file mode 100644 index 0000000..5bf9064 --- /dev/null +++ b/net/request-xhr.js @@ -0,0 +1,36 @@ +"use strict"; + +module.exports = request; + +function request(method, url, headers, body, callback) { + if (typeof body === "function") { + callback = body; + body = undefined; + } + if (!callback) { + return request.bind(null, method, url, headers, body); + } + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + xhr.responseType = "arraybuffer"; + + Object.keys(headers).forEach(function (name) { + xhr.setRequestHeader(name, headers[name]); + }); + + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + var resHeaders = {}; + xhr.getAllResponseHeaders().trim().split("\r\n").forEach(function (line) { + var index = line.indexOf(":"); + resHeaders[line.substring(0, index).toLowerCase()] = line.substring(index + 1).trim(); + }); + + callback(null, { + statusCode: xhr.status, + headers: resHeaders, + body: xhr.response && new Uint8Array(xhr.response) + }); + }; + xhr.send(body); +} diff --git a/net/tcp-chrome-sockets.js b/net/tcp-chrome-sockets.js new file mode 100644 index 0000000..2a14aa6 --- /dev/null +++ b/net/tcp-chrome-sockets.js @@ -0,0 +1,108 @@ +"use strict"; + +var makeChannel = require('culvert'); +var wrapHandler = require('../lib/wrap-handler'); +var tcp = window.chrome.sockets.tcp; +var runtime = window.chrome.runtime; + +module.exports = connect; + +function connect(host, port, onError) { + port = port|0; + host = String(host); + if (!port || !host) throw new TypeError("host and port are required"); + + onCreate = wrap(onCreate, onError); + onConnect = wrap(onConnect, onError); + onInfo = wrap(onInfo, onError); + onReceive = wrap(onReceive, onError); + onReceiveError = wrap(onReceiveError, onError); + onData = wrapHandler(onData, onError); + onWrite = wrap(onWrite, onError); + + var paused = false; + var open = false; + var socketId; + + var serverChannel = makeChannel(); + var clientChannel = makeChannel(); + var socket = { + put: serverChannel.put, + drain: serverChannel.drain, + take: clientChannel.take + }; + + tcp.onReceive.addListener(onReceive); + tcp.onReceiveError.addListener(onReceiveError); + tcp.create(onCreate); + + return { + put: clientChannel.put, + drain: clientChannel.drain, + take: serverChannel.take + }; + + function onCreate(createInfo) { + socketId = createInfo.socketId; + tcp.connect(socketId, host, port, onConnect); + } + + function onConnect(result) { + if (result < 0) throw new Error(runtime.lastError.message + " Connection error"); + tcp.getInfo(socketId, onInfo); + } + + function onInfo(socketInfo) { + if (!socketInfo.connected) { + throw new Error("Connection failed"); + } + open = true; + socket.take(onData); + } + + function onReceive(info) { + if (info.socketId !== socketId) return; + if (socket.put(new Uint8Array(info.data)) || paused) return; + paused = true; + tcp.setPaused(socketId, true); + socket.drain(onDrain); + } + + function onDrain() { + if (!paused) return; + paused = false; + if (open) tcp.setPaused(socketId, false); + } + + function onReceiveError(info) { + if (info.socketId !== socketId) return; + open = false; + tcp.close(socketId); + socket.put(); + // TODO: find a way to tell close and error apart. + // throw new Error("Code " + info.resultCode + " error while receiving."); + } + + function onData(data) { + tcp.send(socketId, data.buffer, onWrite); + } + + function onWrite(info) { + if (info.resultCode < 0) { + throw new Error(runtime.lastError.message + " Error writing."); + } + socket.take(onData); + } +} + + +function wrap(fn, onError) { + return function () { + try { + return fn.apply(this, arguments); + } + catch (err) { + onError(err); + } + }; +} diff --git a/net/tcp-ws-proxy.js b/net/tcp-ws-proxy.js new file mode 100644 index 0000000..37a5ff7 --- /dev/null +++ b/net/tcp-ws-proxy.js @@ -0,0 +1,79 @@ +"use strict"; + +var makeChannel = require('culvert'); +var wrapHandler = require('../lib/wrap-handler'); + +module.exports = function (proxyUrl) { + if (proxyUrl[proxyUrl.length - 1] !== "/") proxyUrl += "/"; + + return function connect(host, port, onError) { + port = port|0; + host = String(host); + if (!port || !host) throw new TypeError("host and port are required"); + + onData = wrapHandler(onData, onError); + + var serverChannel = makeChannel(); + var clientChannel = makeChannel(); + var socket = { + put: serverChannel.put, + drain: serverChannel.drain, + take: clientChannel.take + }; + + var connected = false; + var ws = new WebSocket(proxyUrl + "tcp/" + host + "/" + port); + ws.binaryType = "arraybuffer"; + + ws.onopen = wrap(onOpen, onError); + ws.onclose = wrap(onClose, onError); + ws.onmessage = wrap(onMessage, onError); + ws.onerror = wrap(onWsError, onError); + + return { + put: clientChannel.put, + drain: clientChannel.drain, + take: serverChannel.take + }; + + function onOpen() { + ws.send("connect"); + } + + function onClose() { + socket.put(); + } + + function onMessage(evt) { + if (!connected && evt.data === "connect") { + connected = true; + socket.take(onData); + return; + } + + socket.put(new Uint8Array(evt.data)); + } + + function onWsError() { + console.error(arguments); + throw new Error("Generic websocket error"); + } + + function onData(chunk) { + ws.send(chunk.buffer); + socket.take(onData); + } + + }; +}; + +function wrap(fn, onError) { + return function () { + try { + return fn.apply(this, arguments); + } + catch (err) { + onError(err); + } + }; +} diff --git a/net/transport-http.js b/net/transport-http.js new file mode 100644 index 0000000..6a68ec2 --- /dev/null +++ b/net/transport-http.js @@ -0,0 +1,102 @@ +"use strict"; + +var makeChannel = require('culvert'); +var bodec = require('bodec'); +var pktLine = require('../lib/pkt-line'); +var wrapHandler = require('../lib/wrap-handler'); + +module.exports = function (request) { + + return function httpTransport(gitUrl, username, password) { + // Send Auth header if username is set + var auth; + if (username) { + auth = "Basic " + btoa(username + ":" + (password || "")); + } + + return function (serviceName, onError) { + + // Wrap our handler functions to route errors properly. + onResponse = wrapHandler(onResponse, onError); + onWrite = wrapHandler(onWrite, onError); + onResult = wrapHandler(onResult, onError); + + // Create a duplex channel with transform for internal use. + var serverChannel = makeChannel();//0, "server"); + var clientChannel = makeChannel();//0, "client"); + var socket = { + put: serverChannel.put, + drain: serverChannel.drain, + take: clientChannel.take + }; + + // Send the initial request to start the connection. + var headers = {}; + if (auth) headers.Authorization = auth; + request("GET", gitUrl + "/info/refs?service=" + serviceName, headers, onResponse); + + // Prep for later requests + var bodyParts = []; + var bodyWrite = pktLine.framer(function (chunk) { + bodyParts.push(chunk); + }); + headers["Content-Type"] = "application/x-" + serviceName + "-request"; + clientChannel.take(onWrite); + + var verified = 0; + var parseResponse = pktLine.deframer(function (line) { + if (verified === 2) { + socket.put(line); + } + else if (verified === 0) { + if (line !== "# service=" + serviceName) { + throw new Error("Illegal service response"); + } + verified = 1; + } + else if (verified === 1) { + if (line !== null) { + throw new Error("Expected null after service name"); + } + verified = 2; + } + }); + + // Return the other half of the duplex channel for the protocol logic to use. + return { + put: clientChannel.put, + drain: clientChannel.drain, + take: serverChannel.take + }; + + function onResponse(res) { + if (res.statusCode !== 200) { + throw new Error("Invalid response: " + res.statusCode); + } + if (res.headers["content-type"] !== "application/x-" + serviceName + "-advertisement") { + throw new Error("Not a smart http git server"); + } + parseResponse(res.body); + } + + function onWrite(item) { + bodyWrite(item); + clientChannel.take(onWrite); + if (item !== "done\n" || !bodyParts.length) return; + var body = bodec.join(bodyParts); + bodyParts.length = 0; + request("POST", gitUrl + "/" + serviceName, headers, body, onResult); + } + + function onResult(res) { + if (res.statusCode !== 200) { + throw new Error("Invalid result: " + res.statusCode); + } + if (res.headers["content-type"] !== "application/x-" + serviceName + "-result") { + throw new Error("Not a smart http git server"); + } + socket.put(res.body); + } + }; + }; +}; diff --git a/net/transport-tcp.js b/net/transport-tcp.js new file mode 100644 index 0000000..d32728e --- /dev/null +++ b/net/transport-tcp.js @@ -0,0 +1,48 @@ +"use strict"; + +var makeChannel = require('culvert'); +var bodec = require('bodec'); +var pktLine = require('../lib/pkt-line'); +var wrapHandler = require('../lib/wrap-handler'); + +module.exports = function (connect) { + + return function tcpTransport(path, host, port) { + port = (port|0) || 9418; + if (!path || !host) throw new Error("path and host are required"); + + return function (serviceName, onError) { + + onData = wrapHandler(onData, onError); + onDrain = wrapHandler(onDrain, onError); + + var socket = connect(host, port, onError); + var inter = makeChannel(); + inter.put = pktLine.deframer(inter.put); + + socket.put = pktLine.framer(socket.put); + var greeting = bodec.fromRaw(serviceName + " " + path + "\0host=" + host + "\0"); + socket.put(greeting); + + // Pipe socket to inter with backpressure + socket.take(onData); + function onData(chunk) { + if (inter.put(chunk)) { + socket.take(onData); + } + else { + inter.drain(onDrain); + } + } + function onDrain() { + socket.take(onData); + } + + return { + put: socket.put, + drain: socket.drain, + take: inter.take + }; + }; + }; +}; From 7e6022791f1227f1d5c6317c0e7b24974d855d99 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 01:05:18 -0500 Subject: [PATCH 213/256] Bump version to 0.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c84af0..1caf4b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.1", + "version": "0.7.2", "description": "Git Implemented in JavaScript", "keywords": ["git", "js-git"], "repository": { From b5897ce2f71374b5e16ef58a5d12e4ad0ea68e39 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 01:15:32 -0500 Subject: [PATCH 214/256] Fix http clone and bump to 0.7.3 --- net/git-fetch-pack.js | 2 +- net/transport-http.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/net/git-fetch-pack.js b/net/git-fetch-pack.js index b00898f..df1f6da 100644 --- a/net/git-fetch-pack.js +++ b/net/git-fetch-pack.js @@ -111,7 +111,7 @@ function fetchPack(transport, onError) { function onNak(line) { if (line !== "NAK") { - throw new Error("Expected NAK"); + throw new Error("Expected NAK, but got " + JSON.stringify(line)); } packChannel = makeChannel(); progressChannel = makeChannel(); diff --git a/net/transport-http.js b/net/transport-http.js index 6a68ec2..608c79f 100644 --- a/net/transport-http.js +++ b/net/transport-http.js @@ -95,7 +95,7 @@ module.exports = function (request) { if (res.headers["content-type"] !== "application/x-" + serviceName + "-result") { throw new Error("Not a smart http git server"); } - socket.put(res.body); + parseResponse(res.body); } }; }; diff --git a/package.json b/package.json index 1caf4b3..22b99a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.2", + "version": "0.7.3", "description": "Git Implemented in JavaScript", "keywords": ["git", "js-git"], "repository": { From e1609e1e78fb0dd7bfdd81dbb3af4dee8c98568c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 01:24:29 -0500 Subject: [PATCH 215/256] More fixes to net stuff --- net/transport-http.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/net/transport-http.js b/net/transport-http.js index 608c79f..fd3b0c3 100644 --- a/net/transport-http.js +++ b/net/transport-http.js @@ -41,7 +41,7 @@ module.exports = function (request) { bodyParts.push(chunk); }); headers["Content-Type"] = "application/x-" + serviceName + "-request"; - clientChannel.take(onWrite); + socket.take(onWrite); var verified = 0; var parseResponse = pktLine.deframer(function (line) { @@ -80,8 +80,9 @@ module.exports = function (request) { } function onWrite(item) { + if (item === undefined) return socket.put(); bodyWrite(item); - clientChannel.take(onWrite); + socket.take(onWrite); if (item !== "done\n" || !bodyParts.length) return; var body = bodec.join(bodyParts); bodyParts.length = 0; From ce4634873c52dcd1e67b98e6a4f316f67e9b2719 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 01:59:42 -0500 Subject: [PATCH 216/256] More fixes --- net/git-fetch-pack.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/net/git-fetch-pack.js b/net/git-fetch-pack.js index df1f6da..1778ba6 100644 --- a/net/git-fetch-pack.js +++ b/net/git-fetch-pack.js @@ -117,9 +117,9 @@ function fetchPack(transport, onError) { progressChannel = makeChannel(); errorChannel = makeChannel(); api.put({ - pack: packChannel.take, - progress: progressChannel.take, - error: errorChannel.take, + pack: { take: packChannel.take }, + progress: { take: progressChannel.take }, + error: { take: errorChannel.take }, }); socket.take(onMore); } From a804fd33763bfe115dd6440b5fc2c5396325f4fb Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 09:36:26 -0500 Subject: [PATCH 217/256] Add basic refs to mem-db --- mixins/mem-db.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mixins/mem-db.js b/mixins/mem-db.js index f159c97..1016c62 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -8,11 +8,33 @@ module.exports = mixin; function mixin(repo) { var objects = {}; + var refs = {}; repo.saveAs = saveAs; repo.loadAs = loadAs; repo.saveRaw = saveRaw; repo.loadRaw = loadRaw; + repo.readRef = readRef; + repo.updateRef = updateRef; + repo.hasHash = hasHash; + + function readRef(ref, callback) { + return makeAsync(function () { + return refs[ref]; + }, callback); + } + + function updateRef(ref, hash, callback) { + return makeAsync(function () { + refs[ref] = hash; + }, callback); + } + + function hasHash(hash, callback) { + return makeAsync(function () { + return hash in objects; + }, callback); + } function saveAs(type, body, callback) { return makeAsync(function () { From ccca651a65da82a6ac0a8b01e9e2cecb0c76cd91 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 10:26:56 -0500 Subject: [PATCH 218/256] Enable shallow clones --- mixins/mem-db.js | 20 ++++++++++++++++++-- net/git-fetch-pack.js | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mixins/mem-db.js b/mixins/mem-db.js index 1016c62..fd9a33d 100644 --- a/mixins/mem-db.js +++ b/mixins/mem-db.js @@ -5,6 +5,7 @@ var codec = require('../lib/object-codec.js'); var sha1 = require('git-sha1'); module.exports = mixin; +var isHash = /^[0-9a-f]{40}$/; function mixin(repo) { var objects = {}; @@ -14,9 +15,10 @@ function mixin(repo) { repo.loadAs = loadAs; repo.saveRaw = saveRaw; repo.loadRaw = loadRaw; + repo.hasHash = hasHash; repo.readRef = readRef; repo.updateRef = updateRef; - repo.hasHash = hasHash; + repo.listRefs = listRefs; function readRef(ref, callback) { return makeAsync(function () { @@ -24,14 +26,27 @@ function mixin(repo) { }, callback); } + function listRefs(prefix, callback) { + return makeAsync(function () { + var regex = prefix && new RegExp("^" + prefix + "[/$]"); + var out = {}; + Object.keys(refs).forEach(function (name) { + if (regex && !regex.test(name)) return; + out[name] = refs[name]; + }); + return out; + }, callback); + } + function updateRef(ref, hash, callback) { return makeAsync(function () { - refs[ref] = hash; + return (refs[ref] = hash); }, callback); } function hasHash(hash, callback) { return makeAsync(function () { + if (!isHash.test(hash)) hash = refs[hash]; return hash in objects; }, callback); } @@ -53,6 +68,7 @@ function mixin(repo) { function loadAs(type, hash, callback) { return makeAsync(function () { + if (!isHash.test(hash)) hash = refs[hash]; var buffer = objects[hash]; if (!buffer) return []; var obj = codec.deframe(buffer, true); diff --git a/net/git-fetch-pack.js b/net/git-fetch-pack.js index 1778ba6..5429c25 100644 --- a/net/git-fetch-pack.js +++ b/net/git-fetch-pack.js @@ -52,6 +52,7 @@ function fetchPack(transport, onError) { else if (!caps) { caps = {}; Object.defineProperty(refs, "caps", {value: caps}); + Object.defineProperty(refs, "shallows", {value:[]}); var index = line.indexOf("\0"); line.substring(index + 1).split(" ").forEach(function (cap) { var i = cap.indexOf("="); @@ -110,6 +111,12 @@ function fetchPack(transport, onError) { } function onNak(line) { + if (line === null) return socket.take(onNak); + var match = line.match(/^shallow ([0-9a-f]{40})$/); + if (match) { + refs.shallows.push(match[1]); + return socket.take(onNak); + } if (line !== "NAK") { throw new Error("Expected NAK, but got " + JSON.stringify(line)); } From bf09a370ae6093da154ac4d8f52999df01d11e3a Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 12:58:31 -0500 Subject: [PATCH 219/256] Switch to pako for streaming parse. --- lib/deflate.js | 14 +- lib/inflate-stream.js | 844 ++---------------------------------------- lib/inflate.js | 14 +- lib/pack-codec.js | 55 +-- 4 files changed, 53 insertions(+), 874 deletions(-) diff --git a/lib/deflate.js b/lib/deflate.js index e5f68a8..512bcc5 100644 --- a/lib/deflate.js +++ b/lib/deflate.js @@ -1,10 +1,10 @@ -if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { - var nodeRequire = require; - var pako = nodeRequire('pako'); - module.exports = function (buffer) { - return new Buffer(pako.deflate(new Uint8Array(buffer))); - }; +var pako = require('pako'); +var Binary = require('bodec').Binary; +if (Binary === Uint8Array) { + module.exports = pako.deflate; } else { - module.exports = require('pako/deflate').deflate; + module.exports = function deflate(value) { + return new Binary(pako.deflate(new Uint8Array(value))); + }; } diff --git a/lib/inflate-stream.js b/lib/inflate-stream.js index 2a771b5..ce8d318 100644 --- a/lib/inflate-stream.js +++ b/lib/inflate-stream.js @@ -1,808 +1,36 @@ -var binary = require('bodec'); - -// This code is slightly modified from chrisdickson/inflate min.js -// The original code is under the MIT license copyright Chris Dickinson - -module.exports = inflate; - -var MAXBITS = 15 - , MAXLCODES = 286 - , MAXDCODES = 30 - , MAXCODES = (MAXLCODES+MAXDCODES) - , FIXLCODES = 288 - -var lens = [ - 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, - 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 -] - -var lext = [ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, - 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 -] - -var dists = [ - 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, - 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, - 8193, 12289, 16385, 24577 -] - -var dext = [ - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, - 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, - 12, 12, 13, 13 -] - -var order = [ - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 -] - -var WINDOW = 32768 - , WINDOW_MINUS_ONE = WINDOW - 1 - -function inflate(emit, on_unused) { - var output = new Uint8Array(WINDOW) - , need_input = false - , buffer_offset = 0 - , bytes_read = 0 - , output_idx = 0 - , ended = false - , state = null - , states = [] - , buffer = [] - , got = 0 - - // buffer up to 128k "output one" bytes - var OUTPUT_ONE_LENGTH = 131070 - , output_one_offs = OUTPUT_ONE_LENGTH - , output_one_buf - - var bitbuf = 0 - , bitcnt = 0 - , is_final = false - , fixed_codes - - var adler_s1 = 1 - , adler_s2 = 0 - - onread.recycle = function recycle() { - var out - buffer.length = 0 - buffer_offset = 0 - output_idx = 0 - bitbuf = 0 - bitcnt = 0 - states.length = 0 - is_final = false - need_input = false - bytes_read = 0 - output_idx = 0 - ended = false - got = 0 - adler_s1 = 1 - adler_s2 = 0 - output_one_offs = 0 - become(noop, {}, noop) - start_stream_header() - // return stream - } - - var bytes_need = 0 - , bytes_value = [] - - var bits_need = 0 - , bits_value = [] - - var codes_distcode = null - , codes_lencode = null - , codes_len = 0 - , codes_dist = 0 - , codes_symbol = 0 - - var dynamic_distcode = {symbol: [], count: []} - , dynamic_lencode = {symbol: [], count: []} - , dynamic_lengths = [] - , dynamic_nlen = 0 - , dynamic_ndist = 0 - , dynamic_ncode = 0 - , dynamic_index = 0 - , dynamic_symbol = 0 - , dynamic_len = 0 - - var decode_huffman = null - , decode_len = 0 - , decode_code = 0 - , decode_first = 0 - , decode_count = 0 - , decode_index = 0 - - var last = null - - become(noop, {}, noop) - start_stream_header() - - return onread - - function onread(err, buf) { - if(buf === undefined) { - return emit(err) - } - - return write(buf) - } - - function noop() { - - } - - function call_header() { - } - - function call_bytes(need) { - bytes_value.length = 0 - bytes_need = need - } - - function call_bits(need) { - bits_value = 0 - bits_need = need - } - - function call_codes(distcode, lencode) { - codes_len = - codes_dist = - codes_symbol = 0 - codes_distcode = distcode - codes_lencode = lencode - } - - function call_dynamic() { - dynamic_distcode.symbol.length = - dynamic_distcode.count.length = - dynamic_lencode.symbol.length = - dynamic_lencode.count.length = - dynamic_lengths.length = 0 - dynamic_nlen = 0 - dynamic_ndist = 0 - dynamic_ncode = 0 - dynamic_index = 0 - dynamic_symbol = 0 - dynamic_len = 0 - } - - function call_decode(h) { - decode_huffman = h - decode_len = 1 - decode_first = - decode_index = - decode_code = 0 - } - - function write(buf) { - buffer.push(buf) - got += buf.length - if(!ended) { - execute() - } - } - - function execute() { - do { - states[0].current() - } while(!need_input && !ended) - - var needed = need_input - need_input = false - } - - function start_stream_header() { - become(bytes, call_bytes(2), got_stream_header) - } - - function got_stream_header() { - var cmf = last[0] - , flg = last[1] - - - if((cmf << 8 | flg) % 31 !== 0) { - emit(new Error( - 'failed header check' - )) - return - } - - - - - if(flg & 32) { - return become(bytes, call_bytes(4), on_got_fdict) - } - return become(bits, call_bits(1), on_got_is_final) - } - - - - - function on_got_fdict() { - return become(bits, call_bits(1), on_got_is_final) - } - - - - - - - - - function on_got_is_final() { - is_final = last - become(bits, call_bits(2), on_got_type) - } - - - - - - - - - - - - - function on_got_type() { - if(last === 0) { - become(bytes, call_bytes(4), on_got_len_nlen) - return - } - - if(last === 1) { - // `fixed` and `dynamic` blocks both eventually delegate - // to the "codes" state -- which reads bits of input, throws - // them into a huffman tree, and produces "symbols" of output. - fixed_codes = fixed_codes || build_fixed() - become(start_codes, call_codes( - fixed_codes.distcode - , fixed_codes.lencode - ), done_with_codes) - return - } - - become(start_dynamic, call_dynamic(), done_with_codes) - return - } - - - - - function on_got_len_nlen() { - var want = last[0] | (last[1] << 8) - , nlen = last[2] | (last[3] << 8) - - if((~nlen & 0xFFFF) !== want) { - emit(new Error( - 'failed len / nlen check' - )) - } - - if(!want) { - become(bits, call_bits(1), on_got_is_final) - return - } - become(bytes, call_bytes(want), on_got_stored) - } - - - - - function on_got_stored() { - output_many(last) - if(is_final) { - become(bytes, call_bytes(4), on_got_adler) - return - } - become(bits, call_bits(1), on_got_is_final) - } - - - - - - - function start_dynamic() { - become(bits, call_bits(5), on_got_nlen) - } - - function on_got_nlen() { - dynamic_nlen = last + 257 - become(bits, call_bits(5), on_got_ndist) - } - - function on_got_ndist() { - dynamic_ndist = last + 1 - become(bits, call_bits(4), on_got_ncode) - } - - function on_got_ncode() { - dynamic_ncode = last + 4 - if(dynamic_nlen > MAXLCODES || dynamic_ndist > MAXDCODES) { - emit(new Error('bad counts')) - return - } - - become(bits, call_bits(3), on_got_lengths_part) - } - - function on_got_lengths_part() { - dynamic_lengths[order[dynamic_index]] = last - - ++dynamic_index - if(dynamic_index === dynamic_ncode) { - for(; dynamic_index < 19; ++dynamic_index) { - dynamic_lengths[order[dynamic_index]] = 0 - } - - // temporarily construct the `lencode` using the - // lengths we've read. we'll actually be using the - // symbols produced by throwing bits into the huffman - // tree to constuct the `lencode` and `distcode` huffman - // trees. - construct(dynamic_lencode, dynamic_lengths, 19) - dynamic_index = 0 - - become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) - return - } - become(bits, call_bits(3), on_got_lengths_part) - } - - function on_got_dynamic_symbol_iter() { - dynamic_symbol = last - - if(dynamic_symbol < 16) { - dynamic_lengths[dynamic_index++] = dynamic_symbol - do_check() - return - } - - dynamic_len = 0 - if(dynamic_symbol === 16) { - become(bits, call_bits(2), on_got_dynamic_symbol_16) - return - } - - if(dynamic_symbol === 17) { - become(bits, call_bits(3), on_got_dynamic_symbol_17) - return - } - - become(bits, call_bits(7), on_got_dynamic_symbol) - } - - function on_got_dynamic_symbol_16() { - dynamic_len = dynamic_lengths[dynamic_index - 1] - on_got_dynamic_symbol_17() - } - - function on_got_dynamic_symbol_17() { - dynamic_symbol = 3 + last - do_dynamic_end_loop() - } - - function on_got_dynamic_symbol() { - dynamic_symbol = 11 + last - do_dynamic_end_loop() - } - - function do_dynamic_end_loop() { - if(dynamic_index + dynamic_symbol > dynamic_nlen + dynamic_ndist) { - emit(new Error('too many lengths')) - return - } - - while(dynamic_symbol--) { - dynamic_lengths[dynamic_index++] = dynamic_len - } - - do_check() - } - - function do_check() { - if(dynamic_index >= dynamic_nlen + dynamic_ndist) { - end_read_dynamic() - return - } - become(decode, call_decode(dynamic_lencode), on_got_dynamic_symbol_iter) - } - - function end_read_dynamic() { - // okay, we can finally start reading data out of the stream. - construct(dynamic_lencode, dynamic_lengths, dynamic_nlen) - construct(dynamic_distcode, dynamic_lengths.slice(dynamic_nlen), dynamic_ndist) - become(start_codes, call_codes( - dynamic_distcode - , dynamic_lencode - ), done_with_codes) - } - - function start_codes() { - become(decode, call_decode(codes_lencode), on_got_codes_symbol) - } - - function on_got_codes_symbol() { - var symbol = codes_symbol = last - if(symbol < 0) { - emit(new Error('invalid symbol')) - return - } - - if(symbol < 256) { - output_one(symbol) - become(decode, call_decode(codes_lencode), on_got_codes_symbol) - return - } - - if(symbol > 256) { - symbol = codes_symbol -= 257 - if(symbol >= 29) { - emit(new Error('invalid fixed code')) - return - } - - become(bits, call_bits(lext[symbol]), on_got_codes_len) - return - } - - if(symbol === 256) { - unbecome() - return - } - } - - - - - - - function on_got_codes_len() { - codes_len = lens[codes_symbol] + last - become(decode, call_decode(codes_distcode), on_got_codes_dist_symbol) - } - - - function on_got_codes_dist_symbol() { - codes_symbol = last - if(codes_symbol < 0) { - emit(new Error('invalid distance symbol')) - return - } - - become(bits, call_bits(dext[codes_symbol]), on_got_codes_dist_dist) - } - - function on_got_codes_dist_dist() { - var dist = dists[codes_symbol] + last - - // Once we have a "distance" and a "length", we start to output bytes. - // We reach "dist" back from our current output position to get the byte - // we should repeat and output it (thus moving the output window cursor forward). - // Two notes: - // - // 1. Theoretically we could overlap our output and input. - // 2. `X % (2^N) == X & (2^N - 1)` with the distinction that - // the result of the bitwise AND won't be negative for the - // range of values we're feeding it. Spare a modulo, spoil the child. - while(codes_len--) { - output_one(output[(output_idx - dist) & WINDOW_MINUS_ONE]) - } - - become(decode, call_decode(codes_lencode), on_got_codes_symbol) - } - - function done_with_codes() { - if(is_final) { - become(bytes, call_bytes(4), on_got_adler) - return - } - become(bits, call_bits(1), on_got_is_final) - } - - - - - function on_got_adler() { - var check_s1 = last[3] | (last[2] << 8) - , check_s2 = last[1] | (last[0] << 8) - - if(check_s2 !== adler_s2 || check_s1 !== adler_s1) { - emit(new Error( - 'bad adler checksum: '+[check_s2, adler_s2, check_s1, adler_s1] - )) - return - } - - ended = true - - output_one_recycle() - - if(on_unused) { - on_unused( - [binary.slice(buffer[0], buffer_offset)].concat(buffer.slice(1)) - , bytes_read - ) - } - - output_idx = 0 - ended = true - emit() - } - - function decode() { - _decode() - } - - function _decode() { - if(decode_len > MAXBITS) { - emit(new Error('ran out of codes')) - return - } - - become(bits, call_bits(1), got_decode_bit) - } - - function got_decode_bit() { - decode_code = (decode_code | last) >>> 0 - decode_count = decode_huffman.count[decode_len] - if(decode_code < decode_first + decode_count) { - unbecome(decode_huffman.symbol[decode_index + (decode_code - decode_first)]) - return - } - decode_index += decode_count - decode_first += decode_count - decode_first <<= 1 - decode_code = (decode_code << 1) >>> 0 - ++decode_len - _decode() - } - - - function become(fn, s, then) { - if(typeof then !== 'function') { - throw new Error - } - states.unshift({ - current: fn - , next: then - }) - } - - function unbecome(result) { - if(states.length > 1) { - states[1].current = states[0].next - } - states.shift() - if(!states.length) { - ended = true - - output_one_recycle() - if(on_unused) { - on_unused( - [binary.slice(buffer[0], buffer_offset)].concat(buffer.slice(1)) - , bytes_read - ) - } - output_idx = 0 - ended = true - emit() - // return - } - else { - last = result - } - } - - function bits() { - var byt - , idx - - idx = 0 - bits_value = bitbuf - while(bitcnt < bits_need) { - // we do this to preserve `bits_value` when - // "need_input" is tripped. - // - // fun fact: if we moved that into the `if` statement - // below, it would trigger a deoptimization of this (very - // hot) function. JITs! - bitbuf = bits_value - byt = take() - if(need_input) { - break - } - ++idx - bits_value = (bits_value | (byt << bitcnt)) >>> 0 - bitcnt += 8 - } - - if(!need_input) { - bitbuf = bits_value >>> bits_need - bitcnt -= bits_need - unbecome((bits_value & ((1 << bits_need) - 1)) >>> 0) - } - } - - - - function bytes() { - var byte_accum = bytes_value - , value - - while(bytes_need--) { - value = take() - - - if(need_input) { - bitbuf = bitcnt = 0 - bytes_need += 1 - break - } - byte_accum[byte_accum.length] = value - } - if(!need_input) { - bitcnt = bitbuf = 0 - unbecome(byte_accum) - } - } - - - - function take() { - if(!buffer.length) { - need_input = true - return - } - - if(buffer_offset === buffer[0].length) { - buffer.shift() - buffer_offset = 0 - return take() - } - - ++bytes_read - - return bitbuf = takebyte() - } - - function takebyte() { - return buffer[0][buffer_offset++] - } - - - - function output_one(val) { - adler_s1 = (adler_s1 + val) % 65521 - adler_s2 = (adler_s2 + adler_s1) % 65521 - output[output_idx++] = val - output_idx &= WINDOW_MINUS_ONE - output_one_pool(val) - } - - function output_one_pool(val) { - if(output_one_offs === OUTPUT_ONE_LENGTH) { - output_one_recycle() - } - - output_one_buf[output_one_offs++] = val - } - - function output_one_recycle() { - if(output_one_offs > 0) { - if(output_one_buf) { - emit(null, binary.slice(output_one_buf, 0, output_one_offs)) - } else { - } - output_one_buf = binary.create(OUTPUT_ONE_LENGTH) - output_one_offs = 0 - } - } - - function output_many(vals) { - var len - , byt - , olen - - output_one_recycle() - for(var i = 0, len = vals.length; i < len; ++i) { - byt = vals[i] - adler_s1 = (adler_s1 + byt) % 65521 - adler_s2 = (adler_s2 + adler_s1) % 65521 - output[output_idx++] = byt - output_idx &= WINDOW_MINUS_ONE - } - - emit(null, binary.fromArray(vals)) - } -} - -function build_fixed() { - var lencnt = [] - , lensym = [] - , distcnt = [] - , distsym = [] - - var lencode = { - count: lencnt - , symbol: lensym - } - - var distcode = { - count: distcnt - , symbol: distsym - } - - var lengths = [] - , symbol - - for(symbol = 0; symbol < 144; ++symbol) { - lengths[symbol] = 8 - } - for(; symbol < 256; ++symbol) { - lengths[symbol] = 9 - } - for(; symbol < 280; ++symbol) { - lengths[symbol] = 7 - } - for(; symbol < FIXLCODES; ++symbol) { - lengths[symbol] = 8 - } - construct(lencode, lengths, FIXLCODES) - - for(symbol = 0; symbol < MAXDCODES; ++symbol) { - lengths[symbol] = 5 - } - construct(distcode, lengths, MAXDCODES) - return {lencode: lencode, distcode: distcode} -} - -function construct(huffman, lengths, num) { - var symbol - , left - , offs - , len - - offs = [] - - for(len = 0; len <= MAXBITS; ++len) { - huffman.count[len] = 0 - } - - for(symbol = 0; symbol < num; ++symbol) { - huffman.count[lengths[symbol]] += 1 - } - - if(huffman.count[0] === num) { - return - } - - left = 1 - for(len = 1; len <= MAXBITS; ++len) { - left <<= 1 - left -= huffman.count[len] - if(left < 0) { - return left - } - } - - offs[1] = 0 - for(len = 1; len < MAXBITS; ++len) { - offs[len + 1] = offs[len] + huffman.count[len] - } - - for(symbol = 0; symbol < num; ++symbol) { - if(lengths[symbol] !== 0) { - huffman.symbol[offs[lengths[symbol]]++] = symbol - } - } - - return left -} \ No newline at end of file +var Inflate = require('pako').Inflate; +var Binary = require('bodec').Binary; + +// Byte oriented inflate stream. Wrapper for pako's Inflate. +// +// var inf = inflate(); +// inf.write(byte) -> more - Write a byte to inflate's state-machine. +// Returns true if more data is expected. +// inf.recycle() - Reset the internal state machine. +// inf.flush() -> data - Flush the output as a binary buffer. +// +module.exports = function inflateStream() { + var inf = new Inflate(); + var b = new Uint8Array(1); + var empty = new Binary(0); + + return { + write: write, + recycle: recycle, + flush: Binary === Uint8Array ? flush : flushConvert + }; + + function write(byte) { + b[0] = byte; + inf.push(b); + return !inf.ended; + } + + function recycle() { inf = new Inflate(); } + + function flush() { return inf.result || empty; } + + function flushConvert() { + return inf.result ? new Binary(inf.result) : empty; + } +}; diff --git a/lib/inflate.js b/lib/inflate.js index ecf2d03..038f8a4 100644 --- a/lib/inflate.js +++ b/lib/inflate.js @@ -1,10 +1,10 @@ -if (typeof process === "object" && typeof process.versions === "object" && process.versions.node) { - var nodeRequire = require; - var pako = nodeRequire('pako'); - module.exports = function (buffer) { - return new Buffer(pako.inflate(new Uint8Array(buffer))); - }; +var pako = require('pako'); +var Binary = require('bodec').Binary; +if (Binary === Uint8Array) { + module.exports = pako.inflate; } else { - module.exports = require('pako/inflate').inflate; + module.exports = function inflate(value) { + return new Binary(pako.inflate(new Uint8Array(value))); + }; } diff --git a/lib/pack-codec.js b/lib/pack-codec.js index f0910a6..93ac7c1 100644 --- a/lib/pack-codec.js +++ b/lib/pack-codec.js @@ -1,5 +1,5 @@ var inflateStream = require('./inflate-stream.js'); -var staticInflate = require('./inflate.js'); +var inflate = require('./inflate.js'); var deflate = require('./deflate.js'); var sha1 = require('git-sha1'); var bodec = require('bodec'); @@ -43,7 +43,7 @@ function parseEntry(chunk) { } } - var body = staticInflate(bodec.slice(chunk, offset)); + var body = inflate(bodec.slice(chunk, offset)); if (body.length !== size) { throw new Error("Size mismatch"); } @@ -63,7 +63,7 @@ function decodePack(emit) { var state = $pack; var sha1sum = sha1(); - var inf = inflate(); + var inf = inflateStream(); var offset = 0; var position = 0; @@ -240,55 +240,6 @@ function decodePack(emit) { } -// Wrapper for proposed new API to inflate: -// -// var inf = inflate(); -// inf.write(byte) -> more - Write a byte to inflate's state-machine. -// Returns true if more data is expected. -// inf.recycle() - Reset the internal state machine. -// inf.flush() -> data - Flush the output as a binary buffer. -// -// This is quite slow, but could be made fast if baked into inflate itself. -function inflate() { - var push = inflateStream(onEmit, onUnused); - var more = true; - var chunks = []; - var b = bodec.create(1); - - return { write: write, recycle: recycle, flush: flush }; - - function write(byte) { - b[0] = byte; - push(null, b); - return more; - } - - function recycle() { - push.recycle(); - more = true; - } - - function flush() { - var buffer = bodec.join(chunks); - chunks.length = 0; - return buffer; - } - - function onEmit(err, item) { - if (err) throw err; - if (item === undefined) { - // console.log("onEnd"); - more = false; - return; - } - chunks.push(item); - } - - function onUnused(chunks) { - if (chunks[0].length) throw new Error("Extra data after deflate"); - more = false; - } -} exports.encodePack = encodePack; function encodePack(emit) { From d6e7ac4177f631fd58df724e61f9a013a23cd156 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 14:28:28 -0500 Subject: [PATCH 220/256] Save changes --- net/request-xhr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/net/request-xhr.js b/net/request-xhr.js index 5bf9064..173cddd 100644 --- a/net/request-xhr.js +++ b/net/request-xhr.js @@ -23,6 +23,7 @@ function request(method, url, headers, body, callback) { var resHeaders = {}; xhr.getAllResponseHeaders().trim().split("\r\n").forEach(function (line) { var index = line.indexOf(":"); + console.log("LINE", line); resHeaders[line.substring(0, index).toLowerCase()] = line.substring(index + 1).trim(); }); From d824f9dc21f1693768867f2f6e95cdef1b529959 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sat, 19 Jul 2014 14:29:27 -0500 Subject: [PATCH 221/256] revert --- net/request-xhr.js | 1 - 1 file changed, 1 deletion(-) diff --git a/net/request-xhr.js b/net/request-xhr.js index 173cddd..5bf9064 100644 --- a/net/request-xhr.js +++ b/net/request-xhr.js @@ -23,7 +23,6 @@ function request(method, url, headers, body, callback) { var resHeaders = {}; xhr.getAllResponseHeaders().trim().split("\r\n").forEach(function (line) { var index = line.indexOf(":"); - console.log("LINE", line); resHeaders[line.substring(0, index).toLowerCase()] = line.substring(index + 1).trim(); }); From 14b5f0de001649b05df4cb93c009cc66afdce3ac Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 24 Jul 2014 19:34:28 -0500 Subject: [PATCH 222/256] Fix shallow clone --- mixins/fs-db.js | 15 ++++++++ net/git-fetch-pack.js | 42 +++++++++++++++------- net/tcp-node.js | 84 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 net/tcp-node.js diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 5693cb0..efab67c 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -30,6 +30,21 @@ module.exports = function (repo, fs) { repo.readRef = readRef; repo.updateRef = updateRef; repo.hasHash = hasHash; + repo.init = init; + repo.setShallow = setShallow; + + function init(ref, callback) { + if (!callback) return init.bind(null, ref); + ref = ref || "refs/heads/master"; + var path = pathJoin(repo.rootPath, "HEAD"); + fs.writeFile(path, "ref: " + ref, callback); + } + + function setShallow(ref, callback) { + if (!callback) return setShallow.bind(null, ref); + var path = pathJoin(repo.rootPath, "shallow"); + fs.writeFile(path, ref, callback); + } function updateRef(ref, hash, callback) { if (!callback) return updateRef.bind(repo, ref, hash); diff --git a/net/git-fetch-pack.js b/net/git-fetch-pack.js index 5429c25..6092a9c 100644 --- a/net/git-fetch-pack.js +++ b/net/git-fetch-pack.js @@ -2,6 +2,7 @@ var makeChannel = require('culvert'); var wrapHandler = require('../lib/wrap-handler'); +var bodec = require('bodec'); module.exports = fetchPack; @@ -19,6 +20,8 @@ function fetchPack(transport, onError) { var caps = null; var capsSent = false; var refs = {}; + var haves = {}; + var havesCount = 0; // Create a duplex channel for talking with the agent. var libraryChannel = makeChannel(); @@ -78,6 +81,7 @@ function fetchPack(transport, onError) { var errorChannel; function onWant(line) { + if (line === undefined) return socket.put(); if (line === null) { socket.put(null); return api.take(onWant); @@ -86,6 +90,12 @@ function fetchPack(transport, onError) { socket.put("deepen " + line.deepen + "\n"); return api.take(onWant); } + if (line.have) { + haves[line.have] = true; + havesCount++; + socket.put("have " + line.have + "\n"); + return api.take(onWant); + } if (line.want) { var extra = ""; if (!capsSent) { @@ -111,24 +121,32 @@ function fetchPack(transport, onError) { } function onNak(line) { + if (line === undefined) return api.put(); if (line === null) return socket.take(onNak); + if (bodec.isBinary(line) || line.progress || line.error) { + packChannel = makeChannel(); + progressChannel = makeChannel(); + errorChannel = makeChannel(); + api.put({ + pack: { take: packChannel.take }, + progress: { take: progressChannel.take }, + error: { take: errorChannel.take }, + }); + return onMore(null, line); + } var match = line.match(/^shallow ([0-9a-f]{40})$/); if (match) { refs.shallows.push(match[1]); return socket.take(onNak); } - if (line !== "NAK") { - throw new Error("Expected NAK, but got " + JSON.stringify(line)); - } - packChannel = makeChannel(); - progressChannel = makeChannel(); - errorChannel = makeChannel(); - api.put({ - pack: { take: packChannel.take }, - progress: { take: progressChannel.take }, - error: { take: errorChannel.take }, - }); - socket.take(onMore); + match = line.match(/^ACK ([0-9a-f]{40})$/); + if (match) { + return socket.take(onNak); + } + if (line === "NAK") { + return socket.take(onNak); + } + throw new Error("Expected NAK, but got " + JSON.stringify(line)); } function onMore(line) { diff --git a/net/tcp-node.js b/net/tcp-node.js new file mode 100644 index 0000000..49c23e9 --- /dev/null +++ b/net/tcp-node.js @@ -0,0 +1,84 @@ +"use strict"; + +var makeChannel = require('culvert'); +var wrapHandler = require('../lib/wrap-handler'); +var net = require('net'); + +module.exports = connect; + +function connect(host, port, onError) { + port = port|0; + host = String(host); + if (!port || !host) throw new TypeError("host and port are required"); + + // Wrap event handlers from node stream + onConnect = wrap(onConnect, onError); + pump = wrap(pump, onError); + onEnd = wrap(onEnd, onError); + onDrain = wrap(onDrain, onError); + + // Wrap event handlers from culvert socket + onTake = wrapHandler(onTake, onError); + + var serverChannel = makeChannel(); + var clientChannel = makeChannel(); + var socket = { + put: serverChannel.put, + drain: serverChannel.drain, + take: clientChannel.take + }; + + var client = net.connect({ host: host, port: port }, onConnect); + if (onError) client.on("error", onError); + + return { + put: clientChannel.put, + drain: clientChannel.drain, + take: serverChannel.take + }; + + function onConnect() { + socket.take(onTake); + client.on("end", onEnd); + client.on("readable", pump); + client.on("drain", onDrain); + } + + function pump() { + var chunk; + do { + chunk = client.read(); + if (!chunk) return; + } while (socket.put(chunk)); + socket.drain(pump); + } + + function onEnd() { + socket.put(); + } + + function onTake(data) { + if (data === undefined) { + client.end(); + } + else if (client.write(data)) { + socket.take(onTake); + } + } + + function onDrain() { + socket.take(onTake); + } + +} + +function wrap(fn, onError) { + return function () { + try { + return fn.apply(this, arguments); + } + catch (err) { + onError(err); + } + }; +} From ad4289c331fae44928279f06bbfaa9e6e87c71a4 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 24 Jul 2014 21:25:20 -0500 Subject: [PATCH 223/256] More fixes --- mixins/path-to-entry.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js index 3cc419c..d15b829 100644 --- a/mixins/path-to-entry.js +++ b/mixins/path-to-entry.js @@ -26,7 +26,13 @@ function pathToEntry(rootTree, path, callback) { index++; continue; } - return callback(); + return callback(null, { + last: { + mode: mode, + hash: hash, + path: parts.slice(0, index).join("/") + } + }); } callback(null, { mode: mode, From 74fa4f76acd8a32a50a8c0092575043dc8f0a8eb Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 24 Jul 2014 22:35:13 -0500 Subject: [PATCH 224/256] Don't send last for files, they can't contain children. --- mixins/path-to-entry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mixins/path-to-entry.js b/mixins/path-to-entry.js index d15b829..3615b7a 100644 --- a/mixins/path-to-entry.js +++ b/mixins/path-to-entry.js @@ -26,11 +26,13 @@ function pathToEntry(rootTree, path, callback) { index++; continue; } + if (modes.isFile(mode)) return callback(); return callback(null, { last: { mode: mode, hash: hash, - path: parts.slice(0, index).join("/") + path: parts.slice(0, index).join("/"), + rest: parts.slice(index).join("/"), } }); } From 6dd92da330671e0b4209c9fcab6660902d0061f7 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Jul 2014 16:24:43 -0500 Subject: [PATCH 225/256] Bump version to 0.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 22b99a2..f95ed23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.3", + "version": "0.7.4", "description": "Git Implemented in JavaScript", "keywords": ["git", "js-git"], "repository": { From a328333476b05ac9249e897606ad37e6ea914371 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Jul 2014 16:26:53 -0500 Subject: [PATCH 226/256] Fix culvert dep version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f95ed23..000656f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ "pathjoin": "git://github.com/creationix/pathjoin.git", "git-sha1": "git://github.com/creationix/git-sha1.git", "pako": "git://github.com/nodeca/pako.git", - "culvert": "~0.1.1" + "culvert": "~0.1.2" } } From 1369459488a5b0a565f10be19b7a6fd07d4436b5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Jul 2014 16:40:04 -0500 Subject: [PATCH 227/256] Bump deps and version to 0.7.5 --- package.json | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 000656f..7f8c38c 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "js-git", - "version": "0.7.4", + "version": "0.7.5", "description": "Git Implemented in JavaScript", - "keywords": ["git", "js-git"], + "keywords": [ + "git", + "js-git" + ], "repository": { "type": "git", "url": "git://github.com/creationix/js-git.git" @@ -16,10 +19,10 @@ "test": "ls test/test-* | xargs -n1 node" }, "dependencies": { - "bodec": "git://github.com/creationix/bodec.git", - "pathjoin": "git://github.com/creationix/pathjoin.git", - "git-sha1": "git://github.com/creationix/git-sha1.git", - "pako": "git://github.com/nodeca/pako.git", - "culvert": "~0.1.2" + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5", + "pathjoin": "git://github.com/creationix/pathjoin.git" } } From b02c799ba9854dee9d81c63105c1e7ffc4415f43 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Jul 2014 21:00:12 -0500 Subject: [PATCH 228/256] Fix sort order for trees. --- lib/object-codec.js | 26 +++++++++++++++++++------- mixins/formats.js | 7 ++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index 537a65b..b7c0c33 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -1,4 +1,6 @@ +"use strict"; var bodec = require('bodec'); +var modes = require('./modes'); // (body) -> raw-buffer var encoders = exports.encoders = { @@ -23,6 +25,7 @@ var decoders = exports.decoders ={ exports.deframe = deframe; // Export git style path sort in case it's wanted. +exports.treeMap = treeMap; exports.treeSort = treeSort; function encodeBlob(body) { @@ -30,20 +33,29 @@ function encodeBlob(body) { return body; } +function treeMap(key) { + /*jshint validthis:true*/ + var entry = this[key]; + return { + name: key, + mode: entry.mode, + hash: entry.hash + }; +} + function treeSort(a, b) { - var aa = a + "/"; - var bb = b + "/"; + var aa = a.mode === modes.tree ? a + "/" : a; + var bb = b.mode === modes.tree ? b + "/" : b; return aa > bb ? 1 : aa < bb ? -1 : 0; } function encodeTree(body) { var tree = ""; if (Array.isArray(body)) throw new TypeError("Tree must be in object form"); - var names = Object.keys(body).sort(treeSort); - for (var i = 0, l = names.length; i < l; i++) { - var name = names[i]; - var entry = body[name]; - tree += entry.mode.toString(8) + " " + bodec.encodeUtf8(name) + + var list = Object.keys(body).map(treeMap, body).sort(treeSort); + for (var i = 0, l = list.length; i < l; i++) { + var entry = list[i]; + tree += entry.mode.toString(8) + " " + bodec.encodeUtf8(entry.name) + "\0" + bodec.decodeHex(entry.hash); } return bodec.fromRaw(tree); diff --git a/mixins/formats.js b/mixins/formats.js index 182e27a..f187fff 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -1,6 +1,7 @@ "use strict"; var bodec = require('bodec'); +var treeMap = require('../lib/object-codec').treeMap; module.exports = function (repo) { var loadAs = repo.loadAs; @@ -46,11 +47,7 @@ module.exports = function (repo) { }; function toArray(tree) { - return Object.keys(tree).map(function (name) { - var entry = tree[name]; - entry.name = name; - return entry; - }); + return Object.keys(tree).map(treeMap); } function normalizeTree(body) { From 36120f3911cd99f1f9232dea25957e4befde0a76 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Jul 2014 23:01:01 -0500 Subject: [PATCH 229/256] Fix unit tests --- mixins/pack-ops.js | 4 ++-- test/test-pack-ops.js | 6 +++--- test/test-zlib.js | 26 ++++++++------------------ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/mixins/pack-ops.js b/mixins/pack-ops.js index baaefc2..a9e91c1 100644 --- a/mixins/pack-ops.js +++ b/mixins/pack-ops.js @@ -144,9 +144,9 @@ function pack(hashes, opts, callback) { if (!callback) return pack.bind(this, hashes, opts); var repo = this; var i = 0, first = true, done = false; - return callback(null, applyParser({ read: read, abort: callback }, encodePack)); + return callback(null, applyParser({ take: take }, encodePack)); - function read(callback) { + function take(callback) { if (done) return callback(); if (first) return readFirst(callback); var hash = hashes[i++]; diff --git a/test/test-pack-ops.js b/test/test-pack-ops.js index 523b850..001d958 100644 --- a/test/test-pack-ops.js +++ b/test/test-pack-ops.js @@ -31,14 +31,14 @@ run([ repo.pack(hashes, {}, function (err, result) { if (err) return end(err); stream = result; - stream.read(onRead); + stream.take(onRead); }); function onRead(err, chunk) { if (err) return end(err); // console.log(chunk); if (chunk) { parts.push(chunk); - return stream.read(onRead); + return stream.take(onRead); } end(); } @@ -47,7 +47,7 @@ run([ function singleStream(item) { var done = false; - return { read: function (callback) { + return { take: function (callback) { if (done) return callback(); done = true; callback(null, item); diff --git a/test/test-zlib.js b/test/test-zlib.js index 254dc1f..78f8090 100644 --- a/test/test-zlib.js +++ b/test/test-zlib.js @@ -30,25 +30,15 @@ run([ var done = false; var chunks = []; var deflated = deflate(bin); - var push = inflateStream(onEmit, onUnused); - for (var i = 0, l = deflated.length; i < l; i += 128) { - push(null, bodec.slice(deflated, i, i + 128)); - } - if (!done) throw new Error("Not finished"); - function onEmit(err, chunk) { - if (err) throw err; - if (chunk === undefined) { - var inflated = bodec.join(chunks); - if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { - console.log([bin.length, inflated.length]); - throw new Error("Problem with roundtrip"); - } - done = true; - } - chunks.push(chunk); + var inf = inflateStream(); + + for (var i = 0, l = deflated.length; i < l; ++i) { + inf.write(deflated[i]); } - function onUnused(unused) { - if (unused[0].length) console.log("unused", unused); + var inflated = inf.flush(); + if (bodec.toRaw(bin) !== bodec.toRaw(inflated)) { + console.log([bin.length, inflated.length]); + throw new Error("Problem with roundtrip"); } } ]); From 7bab6ab1b4d0c64d5919cbabfc6143e20a74148f Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 25 Jul 2014 23:11:42 -0500 Subject: [PATCH 230/256] Test and really fix tree sorting --- lib/object-codec.js | 4 ++-- test/test-object-codec.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/object-codec.js b/lib/object-codec.js index b7c0c33..a1609c3 100644 --- a/lib/object-codec.js +++ b/lib/object-codec.js @@ -44,8 +44,8 @@ function treeMap(key) { } function treeSort(a, b) { - var aa = a.mode === modes.tree ? a + "/" : a; - var bb = b.mode === modes.tree ? b + "/" : b; + var aa = (a.mode === modes.tree) ? a.name + "/" : a.name; + var bb = (b.mode === modes.tree) ? b.name + "/" : b.name; return aa > bb ? 1 : aa < bb ? -1 : 0; } diff --git a/test/test-object-codec.js b/test/test-object-codec.js index eeae487..49fb75e 100644 --- a/test/test-object-codec.js +++ b/test/test-object-codec.js @@ -41,6 +41,25 @@ run([ throw new Error("Invalid tree hash"); } }, + function testTreeSort() { + var tree = { + "README.md": {"mode":modes.blob,"hash":"42bd87a816800cb87646e95b71273983a71a26dc"}, + "a.js": {"mode":modes.blob,"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"}, + "a-js": {"mode":modes.blob,"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"}, + "b": {"mode":modes.blob,"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"}, + "b-js": {"mode":modes.blob,"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"}, + "c": {"mode":modes.blob,"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"}, + "c.js": {"mode":modes.blob,"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"}, + "a": {"mode":modes.tree,"hash":"496d6428b9cf92981dc9495211e6e1120fb6f2ba"}, + "b.js": {"mode":modes.tree,"hash":"496d6428b9cf92981dc9495211e6e1120fb6f2ba"}, + "c-js": {"mode":modes.tree,"hash":"496d6428b9cf92981dc9495211e6e1120fb6f2ba"}, + }; + var treeBin = codec.frame({type: "tree", body: tree}); + var treeHash = sha1(treeBin); + if (treeHash !== "f78893bf52bc695f343372d4210c8c0803c7c4db") { + throw new Error("Invalid tree hash"); + } + }, function testEncodeCommit() { var person = { name: "Tim Caswell", From 4bd600b275e75e81f03c2f432b717104e4a8edfd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 5 Aug 2014 12:49:25 -0500 Subject: [PATCH 231/256] Fix pack ops with fs backend --- mixins/fs-db.js | 6 +++--- mixins/pack-ops.js | 2 +- net/tcp-node.js | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index efab67c..e92305b 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -138,11 +138,11 @@ module.exports = function (repo, fs) { }); } - function hasHash(type, hash, callback) { - if (!callback) return hasHash.bind(repo, type, hash); + function hasHash(hash, callback) { + if (!callback) return hasHash.bind(repo, hash); loadRaw(hash, function (err, body) { if (err) return callback(err); - callback(null, !!body); + return callback(null, !!body); }); } diff --git a/mixins/pack-ops.js b/mixins/pack-ops.js index a9e91c1..dece5ac 100644 --- a/mixins/pack-ops.js +++ b/mixins/pack-ops.js @@ -99,7 +99,7 @@ function unpack(packChannel, opts, callback) { var hasTarget = has[item.ref]; if (hasTarget === true) return resolveDelta(item); if (hasTarget === false) return enqueueDelta(item); - return repo.has(item.ref, function (err, value) { + return repo.hasHash(item.ref, function (err, value) { if (err) return onDone(err); has[item.ref] = value; if (value) return resolveDelta(item); diff --git a/net/tcp-node.js b/net/tcp-node.js index 49c23e9..c7f3967 100644 --- a/net/tcp-node.js +++ b/net/tcp-node.js @@ -42,6 +42,7 @@ function connect(host, port, onError) { client.on("end", onEnd); client.on("readable", pump); client.on("drain", onDrain); + client.on("error", onError); } function pump() { From edd0da74722d7b6c019f4e89247cc1623a1fd0af Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 5 Aug 2014 12:59:39 -0500 Subject: [PATCH 232/256] Improve error reporting in fetch --- net/git-fetch-pack.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/net/git-fetch-pack.js b/net/git-fetch-pack.js index 6092a9c..4e75303 100644 --- a/net/git-fetch-pack.js +++ b/net/git-fetch-pack.js @@ -57,20 +57,25 @@ function fetchPack(transport, onError) { Object.defineProperty(refs, "caps", {value: caps}); Object.defineProperty(refs, "shallows", {value:[]}); var index = line.indexOf("\0"); - line.substring(index + 1).split(" ").forEach(function (cap) { - var i = cap.indexOf("="); - if (i >= 0) { - caps[cap.substring(0, i)] = cap.substring(i + 1); - } - else { - caps[cap] = true; - } - }); - line = line.substring(0, index); + if (index >= 0) { + line.substring(index + 1).split(" ").forEach(function (cap) { + var i = cap.indexOf("="); + if (i >= 0) { + caps[cap.substring(0, i)] = cap.substring(i + 1); + } + else { + caps[cap] = true; + } + }); + line = line.substring(0, index); + } } var match = line.match(/(^[0-9a-f]{40}) (.*)$/); if (!match) { - throw new Error("Invalid line: " + line); + if (typeof line === "string" && /^ERR/i.test(line)) { + throw new Error(line); + } + throw new Error("Invalid line: " + JSON.stringify(line)); } refs[match[2]] = match[1]; socket.take(onRef); From 9c696ae2d1ffdbed22f8bf64745d3d2483f40b3c Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 5 Aug 2014 13:15:17 -0500 Subject: [PATCH 233/256] Bump version to 0.7.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f8c38c..ed8ba03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.5", + "version": "0.7.6", "description": "Git Implemented in JavaScript", "keywords": [ "git", From 4dcd0c53952a42fddfea73281fbf85b963ddb278 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 7 Aug 2014 11:21:41 -0500 Subject: [PATCH 234/256] Save changes --- mixins/delay.js | 9 +++++++-- mixins/indexed-db.js | 40 +++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/mixins/delay.js b/mixins/delay.js index 18a3650..8291224 100644 --- a/mixins/delay.js +++ b/mixins/delay.js @@ -8,36 +8,41 @@ module.exports = function (repo, ms) { var createTree = repo.createTree; repo.saveAs = saveAsDelayed; - repo.loadAs = loadASDelayed; + repo.loadAs = loadAsDelayed; repo.readRef = readRefDelayed; repo.updateRed = updateRefDelayed; if (createTree) repo.createTree = createTreeDelayed; function saveAsDelayed(type, value, callback) { + if (!callback) return saveAsDelayed.bind(repo, type, value); setTimeout(function () { return saveAs.call(repo, type, value, callback); }, ms); } - function loadASDelayed(type, hash, callback) { + function loadAsDelayed(type, hash, callback) { + if (!callback) return loadAsDelayed.bind(repo, type, hash); setTimeout(function () { return loadAs.call(repo, type, hash, callback); }, ms); } function readRefDelayed(ref, callback) { + if (!callback) return readRefDelayed.bind(repo, ref); setTimeout(function () { return readRef.call(repo, ref, callback); }, ms); } function updateRefDelayed(ref, hash, callback) { + if (!callback) return updateRefDelayed.bind(repo, ref, hash); setTimeout(function () { return updateRef.call(repo, ref, hash, callback); }, ms); } function createTreeDelayed(entries, callback) { + if (!callback) return createTreeDelayed.bind(repo, entries); setTimeout(function () { return createTree.call(repo, entries, callback); }, ms); diff --git a/mixins/indexed-db.js b/mixins/indexed-db.js index 5822aab..19b43a5 100644 --- a/mixins/indexed-db.js +++ b/mixins/indexed-db.js @@ -62,6 +62,7 @@ function onError(evt) { } function saveAs(type, body, callback, forcedHash) { + if (!callback) return saveAs.bind(this, type, body); var hash; try { var buffer = codec.frame({type:type,body:body}); @@ -82,44 +83,40 @@ function saveAs(type, body, callback, forcedHash) { } function loadAs(type, hash, callback) { - // console.warn("LOAD", type, hash); + if (!callback) return loadAs.bind(this, type, hash); + loadRaw(hash, function (err, entry) { + if (!entry) return callback(err); + if (type !== entry.type) { + return callback(new TypeError("Type mismatch")); + } + callback(null, entry.body, hash); + }); +} + +function loadRaw(hash, callback) { var trans = db.transaction(["objects"], "readwrite"); var store = trans.objectStore("objects"); var request = store.get(hash); request.onsuccess = function(evt) { var entry = evt.target.result; if (!entry) return callback(); - if (type !== entry.type) { - return callback(new TypeError("Type mismatch")); - } - callback(null, entry.body, hash); + return callback(null, entry); }; request.onerror = function(evt) { callback(new Error(evt.value)); }; } -function hasHash(type, hash, callback) { - loadAs(type, hash, function (err, value) { +function hasHash(hash, callback) { + if (!callback) return hasHash.bind(this, hash); + loadRaw(hash, function (err, body) { if (err) return callback(err); - if (value === undefined) return callback(null, false); - if (type !== "tree") return callback(null, true); - var names = Object.keys(value); - next(); - function next() { - if (!names.length) return callback(null, true); - var name = names.pop(); - var entry = value[name]; - hasHash(modes.toType(entry.mode), entry.hash, function (err, has) { - if (err) return callback(err); - if (has) return next(); - callback(null, false); - }); - } + return callback(null, !!body); }); } function readRef(ref, callback) { + if (!callback) return readRef.bind(this, ref); var key = this.refPrefix + "/" + ref; var trans = db.transaction(["refs"], "readwrite"); var store = trans.objectStore("refs"); @@ -135,6 +132,7 @@ function readRef(ref, callback) { } function updateRef(ref, hash, callback) { + if (!callback) return updateRef.bind(this, ref, hash); var key = this.refPrefix + "/" + ref; var trans = db.transaction(["refs"], "readwrite"); var store = trans.objectStore("refs"); From 0127846b3825133b0280d4f246ac88a8c2327a73 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Aug 2014 12:51:16 -0500 Subject: [PATCH 235/256] hasHash no longer has type argument --- mixins/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/sync.js b/mixins/sync.js index d0fa687..6222c11 100644 --- a/mixins/sync.js +++ b/mixins/sync.js @@ -41,7 +41,7 @@ function sync(local, remote, ref, depth, callback) { if (typeof type !== "string") throw new TypeError("type must be string"); if (typeof hash !== "string") throw new TypeError("hash must be string"); if (hasCache[hash]) return callback(null, true); - local.hasHash(type, hash, function (err, has) { + local.hasHash(hash, function (err, has) { if (err) return callback(err); hasCache[hash] = has; callback(null, has); From 11ee61b4690037be7fcf8736bafaff147d06db7e Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 8 Aug 2014 13:42:40 -0500 Subject: [PATCH 236/256] Fix treemap usage in "array" format --- mixins/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/formats.js b/mixins/formats.js index f187fff..5a4d700 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -47,7 +47,7 @@ module.exports = function (repo) { }; function toArray(tree) { - return Object.keys(tree).map(treeMap); + return Object.keys(tree).map(treeMap, tree); } function normalizeTree(body) { From ab50767ab50c83d2af65be8e6f72baff6ce68d9f Mon Sep 17 00:00:00 2001 From: David Fries Date: Sat, 9 Aug 2014 23:59:00 -0500 Subject: [PATCH 237/256] Data integrity fix, only expose final file fs.writeFile will truncate the file, then write, during which time other readers will get a zero length file or a partial (corrupted) file. Fix by writing to a temporary file as the original git tools do, add .lock to a refs file, and tmp_obj_ for object files. --- mixins/fs-db.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index e92305b..83b7660 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -49,7 +49,11 @@ module.exports = function (repo, fs) { function updateRef(ref, hash, callback) { if (!callback) return updateRef.bind(repo, ref, hash); var path = pathJoin(repo.rootPath, ref); - fs.writeFile(path, bodec.fromRaw(hash + "\n"), callback); + var lock = path + ".lock"; + fs.writeFile(lock, bodec.fromRaw(hash + "\n"), function(err) { + if(err) return callback(err); + fs.rename(lock, path, callback); + }); } function readRef(ref, callback) { @@ -119,7 +123,11 @@ module.exports = function (repo, fs) { // If it already exists, we're done if (data) return callback(); // Otherwise write a new file - fs.writeFile(path, buffer, callback); + var tmp = path.replace(/[0-9a-f]+$/, 'tmp_obj_' + Math.random().toString(36).substr(2)) + fs.writeFile(tmp, buffer, function(err) { + if(err) return callback(err); + fs.rename(tmp, path, callback); + }); }); } From 4ca86be0bf3a5e57f53c44491838955d3201a8ff Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Thu, 21 Aug 2014 21:08:58 -0700 Subject: [PATCH 238/256] Remove pathjoin dep and bump version to 7.7 --- mixins/fs-db.js | 2 +- package.json | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 83b7660..4bbd0c6 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -6,7 +6,7 @@ var codec = require('../lib/object-codec'); var parsePackEntry = require('../lib/pack-codec').parseEntry; var applyDelta = require('../lib/apply-delta'); var sha1 = require('git-sha1'); -var pathJoin = require('pathjoin'); +var pathJoin = require('path').join; // The fs object has the following interface: // - readFile(path) => binary diff --git a/package.json b/package.json index ed8ba03..bec26a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.6", + "version": "0.7.7", "description": "Git Implemented in JavaScript", "keywords": [ "git", @@ -22,7 +22,6 @@ "bodec": "^0.1.0", "culvert": "^0.1.2", "git-sha1": "^0.1.2", - "pako": "^0.2.5", - "pathjoin": "git://github.com/creationix/pathjoin.git" + "pako": "^0.2.5" } } From dabe3683d59f0f92ec77afd0e63a02910c53dfaa Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Tue, 14 Oct 2014 03:16:44 +0000 Subject: [PATCH 239/256] Added Gitter badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fa12bfd..a8c2423 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # JS-Git +[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/creationix/js-git?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) This project is a collection of modules that helps in implementing git powered applications in JavaScript. The original purpose for this is to enable better From 016cb40c4328f56adf1af846713132b231ea54c7 Mon Sep 17 00:00:00 2001 From: Greg Fedirko Date: Thu, 6 Nov 2014 14:30:56 -0800 Subject: [PATCH 240/256] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8c2423..0f23e0a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This project is a collection of modules that helps in implementing git powered applications in JavaScript. The original purpose for this is to enable better developer tools for authoring code in restricted environments like ChromeBooks -and tablets. It also enables using git at a database to replace SQL and no-SQL +and tablets. It also enables using git as a database to replace SQL and no-SQL data stores in many applications. This project was initially funded by two crowd-sourced fundraisers. See details From f1740f50eda2d161524f80399f8e1f2a946f005a Mon Sep 17 00:00:00 2001 From: Greg Fedirko Date: Thu, 6 Nov 2014 17:46:58 -0800 Subject: [PATCH 241/256] Fix typo --- doc/mixins/fs-db.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/mixins/fs-db.md b/doc/mixins/fs-db.md index 3029e5a..c178435 100644 --- a/doc/mixins/fs-db.md +++ b/doc/mixins/fs-db.md @@ -5,7 +5,7 @@ JSGit repositories need `loadAs`, `saveAs`, `loadRaw`, `saveRaw`, `readRef`, and `updateRef` methods. Depending on the backing storage, there are various ways to implement these methods. -The implementation for in-mempory storage is `js-git/mixins/mem-db`, and there +The implementation for in-memory storage is `js-git/mixins/mem-db`, and there are variants for using Github or IndexDB for storage. The `js-git/mixins/fs-db` implementation provides these methods as well, but From 9136d27220f83f75ecf324c4b26e4020fc964eb6 Mon Sep 17 00:00:00 2001 From: "Michael J. Ryan" Date: Tue, 9 Dec 2014 14:42:16 -0700 Subject: [PATCH 242/256] Update BACKERS.md add profile link. --- BACKERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BACKERS.md b/BACKERS.md index 5983003..f84931b 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -294,7 +294,7 @@ Originally JS-Git started at a [kickstarter project][]. This was to enable me t - LSD25 - Nima Gardideh (nemo) - Patrick Collins (pat@burned.com) - - Michael J. Ryan (tracker1) + - Michael J. Ryan ([@tracker1](https://github.com/tracker1)) - technoweenie - David Hayes - Meyer SciTech Solutions, LLC From d4a1d6db1c059eb4f8f951e3c2835a84ad68427d Mon Sep 17 00:00:00 2001 From: Alex Birkett Date: Fri, 26 Dec 2014 22:49:23 +0100 Subject: [PATCH 243/256] Update README.md Fix require paths --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0f23e0a..a3da7b6 100644 --- a/README.md +++ b/README.md @@ -34,28 +34,28 @@ var repo = {}; // - loadAs(type, hash) => hash // - saveRaw(hash, binary) => // - loadRaw(hash) => binary -require('../mixins/mem-db')(repo); +require('js-git/mixins/mem-db')(repo); // This adds a high-level API for creating multiple git objects by path. // - createTree(entries) => hash -require('../mixins/create-tree')(repo); +require('js-git/mixins/create-tree')(repo); // This provides extra methods for dealing with packfile streams. // It depends on // - unpack(packStream, opts) => hashes // - pack(hashes, opts) => packStream -require('../mixins/pack-ops')(repo); +require('js-git/mixins/pack-ops')(repo); // This adds in walker algorithms for quickly walking history or a tree. // - logWalk(ref|hash) => stream // - treeWalk(hash) => stream -require('../mixins/walkers')(repo); +require('js-git/mixins/walkers')(repo); // This combines parallel requests for the same resource for effeciency under load. -require('../mixins/read-combiner')(repo); +require('js-git/mixins/read-combiner')(repo); // This makes the object interface less strict. See it's docs for details -require('../mixins/formats')(repo); +require('js-git/mixins/formats')(repo); ``` ## Generators vs Callbacks From 01abd3194eaacb6ea57abee94d169d01e5960a9a Mon Sep 17 00:00:00 2001 From: Alex Birkett Date: Sat, 27 Dec 2014 00:35:22 +0100 Subject: [PATCH 244/256] Update README.md ```repo.loadAs()``` expects a hash as the second parameter. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f23e0a..7ca3e7c 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ var commit = yield repo.loadAs("commit", commitHash); // We then read the tree using `commit.tree`. var tree = yield repo.loadAs("tree", commit.tree); // We then read the file using the entry hash in the tree. -var file = yield repo.loadAs("blob", tree["greeting.txt"]); +var file = yield repo.loadAs("blob", tree["greeting.txt"].hash); // file is now a binary buffer. ``` From 85e6629458a2e7f58ed58d842684a6d7d3f4d7cd Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 24 Jul 2015 12:14:52 -0500 Subject: [PATCH 245/256] Allow empty email and name fields. It's not good practice, but it's not js-git's job to enforce such things. --- mixins/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/formats.js b/mixins/formats.js index 5a4d700..4678dae 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -122,7 +122,7 @@ function normalizePerson(person) { if (!person || typeof person !== "object") { throw new TypeError("Person must be an object"); } - if (!person.name || !person.email) { + if (typeof person.name !== "string" || typeof person.email === "string") { throw new TypeError("Name and email are required for person fields"); } return { From 0c92c7823b04d5c951d465c93a83dc35bb26cd95 Mon Sep 17 00:00:00 2001 From: Vaughn Iverson Date: Fri, 31 Jul 2015 18:06:49 -0700 Subject: [PATCH 246/256] Fixed bug/typo checking for valid email type recently introduced in normalizePerson() --- mixins/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/formats.js b/mixins/formats.js index 4678dae..88ac21f 100644 --- a/mixins/formats.js +++ b/mixins/formats.js @@ -122,7 +122,7 @@ function normalizePerson(person) { if (!person || typeof person !== "object") { throw new TypeError("Person must be an object"); } - if (typeof person.name !== "string" || typeof person.email === "string") { + if (typeof person.name !== "string" || typeof person.email !== "string") { throw new TypeError("Name and email are required for person fields"); } return { From 126fb88be921a80ee84ff39e98217b4981b017db Mon Sep 17 00:00:00 2001 From: Warren Konkel Date: Sun, 2 Aug 2015 01:18:22 -0700 Subject: [PATCH 247/256] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70dbfda..ebf5d5e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ and tablets. It also enables using git as a database to replace SQL and no-SQL data stores in many applications. This project was initially funded by two crowd-sourced fundraisers. See details -in [BACKERS.md](BACKERS.md) and [BACKERS-2.md](BACKERS.md). Thanks to all of +in [BACKERS.md](BACKERS.md) and [BACKERS-2.md](BACKERS-2.md). Thanks to all of you who made this possible! ## Usage From 221ca1b2a3059b28f036db9166d2457da2baff49 Mon Sep 17 00:00:00 2001 From: Aaron Hammond Date: Wed, 26 Aug 2015 12:50:37 -0400 Subject: [PATCH 248/256] Fixed a typo in documentation --- doc/lib/object-codec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/lib/object-codec.md b/doc/lib/object-codec.md index 69c8cbb..de74a32 100644 --- a/doc/lib/object-codec.md +++ b/doc/lib/object-codec.md @@ -78,7 +78,7 @@ The `date` property of `author` and `committer` is in the format {seconds,offset Where seconds is a unix timestamp in seconds and offset is the number of minutes offset for the timezone. (Your local offset can be found with `(new Date).getTimezoneOffset()`) -The `message` fiels is mandatory and a simple string. +The `message` field is mandatory and a simple string. ```js rawBin = encoders.commit({ From 4380bf9b9ade4908753d1f7616d4f6ea54a1eb5d Mon Sep 17 00:00:00 2001 From: Kevin Malakoff Date: Sun, 15 Nov 2015 03:20:18 -0800 Subject: [PATCH 249/256] Portable octal representation --- lib/modes.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/modes.js b/lib/modes.js index 79128e4..9162c62 100644 --- a/lib/modes.js +++ b/lib/modes.js @@ -1,21 +1,28 @@ -// Not strict mode because it uses octal literals all over. -module.exports = { +"use strict"; + +var masks = { + mask: parseInt('100000', 8), + blob: parseInt('140000', 8), + file: parseInt('160000', 8) +}; + +var modes = module.exports = { isBlob: function (mode) { - return (mode & 0140000) === 0100000; + return (mode & masks.blob) === masks.mask; }, isFile: function (mode) { - return (mode & 0160000) === 0100000; + return (mode & masks.file) === masks.mask; }, toType: function (mode) { - if (mode === 0160000) return "commit"; - if (mode === 040000) return "tree"; - if ((mode & 0140000) === 0100000) return "blob"; + if (mode === modes.commit) return "commit"; + if (mode === modes.tree) return "tree"; + if ((mode & masks.blob) === masks.mask) return "blob"; return "unknown"; }, - tree: 040000, - blob: 0100644, - file: 0100644, - exec: 0100755, - sym: 0120000, - commit: 0160000 + tree: parseInt( '40000', 8), + blob: parseInt('100644', 8), + file: parseInt('100644', 8), + exec: parseInt('100755', 8), + sym: parseInt('120000', 8), + commit: parseInt('160000', 8) }; From 9d27d918508d3438177ff0e5813e73654b2b6ca5 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Fri, 15 Jan 2016 10:46:27 -0600 Subject: [PATCH 250/256] Correct base hash on ref-delta. (closes #128) --- mixins/fs-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/fs-db.js b/mixins/fs-db.js index 83b7660..5298634 100644 --- a/mixins/fs-db.js +++ b/mixins/fs-db.js @@ -222,7 +222,7 @@ module.exports = function (repo, fs) { try { var entry = parsePackEntry(chunk); if (entry.type === "ref-delta") { - return loadRaw.call(repo, hash, onBase); + return loadRaw.call(repo, entry.ref, onBase); } else if (entry.type === "ofs-delta") { return loadChunk(packFile, start - entry.ref, onBase); From d78da0138969a368da74d0dfb9e3c5d88114b152 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 24 Jan 2017 13:54:16 -0600 Subject: [PATCH 251/256] Bump version to 0.7.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bec26a0..9f70c0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-git", - "version": "0.7.7", + "version": "0.7.8", "description": "Git Implemented in JavaScript", "keywords": [ "git", From eb5c152b5ae952a36f35c4f5b5c4b96352ca8aae Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Sun, 23 Jul 2017 11:26:32 -0500 Subject: [PATCH 252/256] Update pack-ops.md Fixed #134 --- doc/mixins/pack-ops.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/mixins/pack-ops.md b/doc/mixins/pack-ops.md index 09d074f..3e24776 100644 --- a/doc/mixins/pack-ops.md +++ b/doc/mixins/pack-ops.md @@ -27,11 +27,11 @@ repo.unpack(stream, opts, function (err, hashes) { repo.pack(hashes, opts, function (err, stream) { if (err) throw err; - stream.read(onRead); - function onRead(err, item) { + stream.take(onRead); + function onRead(err, chunk) { if (err) throw err; - console.log(item); - if (item) stream.read(onRead); + console.log(chunk); + if (item) stream.take(onRead); } }); ``` From a455a8e283092103da87b8c9ce32c628c969edc1 Mon Sep 17 00:00:00 2001 From: Frank Lyder Bredland Date: Thu, 27 Jul 2017 18:17:58 +0200 Subject: [PATCH 253/256] Added some readme and fixed one typo --- doc/lib/readme.md | 3 +++ doc/mixins/pack-ops.md | 2 +- doc/mixins/readme.md | 9 +++++++++ doc/readme.md | 8 ++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 doc/lib/readme.md create mode 100644 doc/mixins/readme.md create mode 100644 doc/readme.md diff --git a/doc/lib/readme.md b/doc/lib/readme.md new file mode 100644 index 0000000..62156f8 --- /dev/null +++ b/doc/lib/readme.md @@ -0,0 +1,3 @@ +# Library + + diff --git a/doc/mixins/pack-ops.md b/doc/mixins/pack-ops.md index 3e24776..71df421 100644 --- a/doc/mixins/pack-ops.md +++ b/doc/mixins/pack-ops.md @@ -12,7 +12,7 @@ And then adds: - `unpack(stream, opts) => hashes` - `pack(hashes, opts) => stream` -The streams are simple-stream format. This means they have a `.read(callback)` +The streams are simple-stream format. This means they have a `.take(callback)` method for pulling items out of the stream. Example: diff --git a/doc/mixins/readme.md b/doc/mixins/readme.md new file mode 100644 index 0000000..5fb958f --- /dev/null +++ b/doc/mixins/readme.md @@ -0,0 +1,9 @@ +# Mixins + +There's three types of mixins thats documented: + +- [fs-db](fs-db.md) + +- [mem-db](mem-db.md) + +- [pack-ops](pack-ops.md) diff --git a/doc/readme.md b/doc/readme.md new file mode 100644 index 0000000..8d68f04 --- /dev/null +++ b/doc/readme.md @@ -0,0 +1,8 @@ +# js-git documentation + +Go to: + +- [Library](lib) + +- [Mixins](mixins) + From 1cce2989c6f870c340b2749082894a250d2560ca Mon Sep 17 00:00:00 2001 From: Ezra Lalonde Date: Fri, 6 Oct 2017 12:20:02 -0600 Subject: [PATCH 254/256] Fix badge in README Fixed incorrect path for the image. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebf5d5e..fcfc863 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JS-Git -[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/creationix/js-git?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/creationix/js-git?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) This project is a collection of modules that helps in implementing git powered applications in JavaScript. The original purpose for this is to enable better From 44d16bd03c13d7dd52a165931be81ebc00fe9071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Ros=C3=A9n?= Date: Fri, 16 Feb 2018 14:27:12 +0100 Subject: [PATCH 255/256] Fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcfc863..d6f54b7 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ require('js-git/mixins/walkers')(repo); // This combines parallel requests for the same resource for effeciency under load. require('js-git/mixins/read-combiner')(repo); -// This makes the object interface less strict. See it's docs for details +// This makes the object interface less strict. See its docs for details require('js-git/mixins/formats')(repo); ``` From 507f53b647cec38c34ef58c95685c3af21d18a77 Mon Sep 17 00:00:00 2001 From: Jason Cooke Date: Sun, 30 Jun 2019 18:25:27 +1200 Subject: [PATCH 256/256] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6f54b7..257315d 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ require('js-git/mixins/pack-ops')(repo); // - treeWalk(hash) => stream require('js-git/mixins/walkers')(repo); -// This combines parallel requests for the same resource for effeciency under load. +// This combines parallel requests for the same resource for efficiency under load. require('js-git/mixins/read-combiner')(repo); // This makes the object interface less strict. See its docs for details