diff --git a/package.json b/package.json index e6d23fe..6d0a2db 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,13 @@ }, "homepage": "https://github.com/ipld/js-ipld-git", "dependencies": { - "async": "^2.3.0", - "cids": "~0.5.0", - "multihashes": "~0.4.5", - "multihashing-async": "~0.4.5", + "async": "^2.5.0", + "cids": "~0.5.1", + "multicodec": "~0.1.9", + "multihashes": "~0.4.8", + "multihashing-async": "~0.4.6", + "smart-buffer": "^3.0.3", + "traverse": "^~0.6.6" }, "devDependencies": { "aegir": "^11.0.1", diff --git a/src/index.js b/src/index.js index 7dc585b..adc8a1b 100644 --- a/src/index.js +++ b/src/index.js @@ -2,4 +2,3 @@ exports.util = require('./util.js') exports.resolver = require('./resolver.js') - diff --git a/src/resolver.js b/src/resolver.js index 84e28d6..27e8de8 100644 --- a/src/resolver.js +++ b/src/resolver.js @@ -1,16 +1,152 @@ 'use strict' const util = require('./util') -const setImmediate = require('async/setImmediate') +const traverse = require('traverse') exports = module.exports -exports.multicodec = 'git' +exports.multicodec = 'git-raw' + +const personInfoPaths = [ + 'original', + 'name', + 'email', + 'date' +] exports.resolve = (block, path, callback) => { - setImmediate(() => callback(new Error("not implemented"), null)) + if (typeof path === 'function') { + callback = path + path = undefined + } + + util.deserialize(block.data, (err, node) => { + if (err) { + return callback(err) + } + + if (!path || path === '/') { + return callback(null, { + value: node, + remainderPath: '' + }) + } + + if (Buffer.isBuffer(node)) { // git blob + return callback(null, { + value: node, + remainderPath: path + }) + } + + const parts = path.split('/') + const val = traverse(node).get(parts) + + if (val) { + return callback(null, { + value: val, + remainderPath: '' + }) + } + + let value + let len = parts.length + + for (let i = 0; i < len; i++) { + const partialPath = parts.shift() + + if (Array.isArray(node)) { + value = node[Number(partialPath)] + } if (node[partialPath]) { + value = node[partialPath] + } else { + // can't traverse more + if (!value) { + return callback(new Error('path not available at root')) + } else { + parts.unshift(partialPath) + return callback(null, { + value: value, + remainderPath: parts.join('/') + }) + } + } + node = value + } + }) } exports.tree = (block, options, callback) => { - setImmediate(() => callback(new Error("not implemented"), null)) + if (typeof options === 'function') { + callback = options + options = undefined + } + + options = options || {} + + util.deserialize(block.data, (err, node) => { + if (err) { + return callback(err) + } + + if (Buffer.isBuffer(node)) { // git blob + return callback(null, []) + } + + let paths = [] + switch (node.gitType) { + case 'commit': + paths = [ + 'message', + 'tree' + ] + + paths = paths.concat(personInfoPaths.map(e => 'author/' + e)) + paths = paths.concat(personInfoPaths.map(e => 'committer/' + e)) + paths = paths.concat([...node.parents.keys()].map(e => 'parents/' + e)) + + if (node.encoding) { + paths.push('encoding') + } + break + case 'tag': + paths = [ + 'object', + 'type', + 'tag', + 'message' + ] + + if (node.tagger) { + paths = paths.concat(personInfoPaths.map((e) => 'tagger/' + e)) + } + + break + default: // tree + Object.keys(node).forEach(dir => { + paths.push(dir) + paths.push(dir + '/hash') + paths.push(dir + '/mode') + }) + } + callback(null, paths) + }) +} + +exports.isLink = (block, path, callback) => { + exports.resolve(block, path, (err, result) => { + if (err) { + return callback(err) + } + + if (result.remainderPath.length > 0) { + return callback(new Error('path out of scope')) + } + + if (typeof result.value === 'object' && result.value['/']) { + callback(null, result.value) + } else { + callback(null, false) + } + }) } diff --git a/src/util.js b/src/util.js index e065879..3b12aeb 100644 --- a/src/util.js +++ b/src/util.js @@ -1,19 +1,78 @@ 'use strict' const setImmediate = require('async/setImmediate') +const waterfall = require('async/waterfall') +const multihashing = require('multihashing-async') +const CID = require('cids') -const CID_GIT_TAG = 0x78 +const resolver = require('./resolver') +const gitUtil = require('./util/util') + +const commit = require('./util/commit') +const tag = require('./util/tag') +const tree = require('./util/tree') exports = module.exports exports.serialize = (dagNode, callback) => { - setImmediate(() => callback(new Error("not implemented"), null)) -}) + if (dagNode === null) { + setImmediate(() => callback(new Error('dagNode passed to serialize was null'), null)) + return + } + + if (Buffer.isBuffer(dagNode)) { + if (dagNode.slice(0, 4).toString() === 'blob') { + setImmediate(() => callback(null, dagNode)) + } else { + setImmediate(() => callback(new Error('unexpected dagNode passed to serialize'), null)) + } + return + } + + switch (dagNode.gitType) { + case 'commit': + commit.serialize(dagNode, callback) + break + case 'tag': + tag.serialize(dagNode, callback) + break + default: + // assume tree as a file named 'type' is legal + tree.serialize(dagNode, callback) + } +} + +exports.deserialize = (data, callback) => { + let headLen = gitUtil.find(data, 0) + let head = data.slice(0, headLen).toString() + let typeLen = head.match(/([^ ]+) (\d+)/) + if (!typeLen) { + setImmediate(() => callback(new Error('invalid object header'), null)) + return + } -exports.deserialize = (dagNode, callback) => { - setImmediate(() => callback(new Error("not implemented"), null)) -}) + switch (typeLen[1]) { + case 'blob': + callback(null, data) + break + case 'commit': + commit.deserialize(data.slice(headLen + 1), callback) + break + case 'tag': + tag.deserialize(data.slice(headLen + 1), callback) + break + case 'tree': + tree.deserialize(data.slice(headLen + 1), callback) + break + default: + setImmediate(() => callback(new Error('unknown object type ' + typeLen[1]), null)) + } +} exports.cid = (dagNode, callback) => { - setImmediate(() => callback(new Error("not implemented"), null)) -}) + waterfall([ + (cb) => exports.serialize(dagNode, cb), + (serialized, cb) => multihashing(serialized, 'sha1', cb), + (mh, cb) => cb(null, new CID(1, resolver.multicodec, mh)) + ], callback) +} diff --git a/src/util/commit.js b/src/util/commit.js new file mode 100644 index 0000000..0f93a42 --- /dev/null +++ b/src/util/commit.js @@ -0,0 +1,89 @@ +'use strict' + +const setImmediate = require('async/setImmediate') +const SmartBuffer = require('smart-buffer').SmartBuffer +const gitUtil = require('./util') + +exports = module.exports + +exports.serialize = (dagNode, callback) => { + let lines = [] + lines.push('tree ' + gitUtil.cidToSha(dagNode.tree['/']).toString('hex')) + dagNode.parents.forEach((parent) => { + lines.push('parent ' + gitUtil.cidToSha(parent['/']).toString('hex')) + }) + lines.push('author ' + gitUtil.serializePersonLine(dagNode.author)) + lines.push('committer ' + gitUtil.serializePersonLine(dagNode.committer)) + if (dagNode.encoding) { + lines.push('encoding ' + dagNode.encoding) + } + if (dagNode.signature) { + lines.push('gpgsig -----BEGIN PGP SIGNATURE-----') + lines.push(dagNode.signature.text) + lines.push(' -----END PGP SIGNATURE-----') + } + lines.push('') + lines.push(dagNode.message) + + let data = lines.join('\n') + + let outBuf = new SmartBuffer() + outBuf.writeString('commit ') + outBuf.writeString(data.length.toString()) + outBuf.writeUInt8(0) + outBuf.writeString(data) + setImmediate(() => callback(null, outBuf.toBuffer())) +} + +exports.deserialize = (data, callback) => { + let lines = data.toString().split('\n') + let res = {gitType: 'commit', parents: []} + + for (let line = 0; line < lines.length; line++) { + let m = lines[line].match(/^([^ ]+) (.+)$/) + if (!m) { + if (lines[line] !== '') { + setImmediate(() => callback(new Error('Invalid commit line ' + line))) + } + res.message = lines.slice(line + 1).join('\n') + break + } + + let key = m[1] + let value = m[2] + switch (key) { + case 'tree': + res.tree = {'/': gitUtil.shaToCid(new Buffer(value, 'hex'))} + break + case 'committer': + res.committer = gitUtil.parsePersonLine(value) + break + case 'author': + res.author = gitUtil.parsePersonLine(value) + break + case 'parent': + res.parents.push({'/': gitUtil.shaToCid(new Buffer(value, 'hex'))}) + break + case 'gpgsig': { + if (value !== '-----BEGIN PGP SIGNATURE-----') { + setImmediate(() => callback(new Error('Invalid commit line ' + line))) + } + res.signature = {} + + let startLine = line + for (; line < lines.length - 1; line++) { + if (lines[line + 1] === ' -----END PGP SIGNATURE-----') { + res.signature.text = lines.slice(startLine + 1, line + 1).join('\n') + break + } + } + line++ + break + } + default: + res[key] = value + } + } + + setImmediate(() => callback(null, res)) +} diff --git a/src/util/tag.js b/src/util/tag.js new file mode 100644 index 0000000..1fbb159 --- /dev/null +++ b/src/util/tag.js @@ -0,0 +1,65 @@ +'use strict' + +const setImmediate = require('async/setImmediate') +const SmartBuffer = require('smart-buffer').SmartBuffer +const gitUtil = require('./util') + +exports = module.exports + +exports.serialize = (dagNode, callback) => { + let lines = [] + lines.push('object ' + gitUtil.cidToSha(dagNode.object['/']).toString('hex')) + lines.push('type ' + dagNode.type) + lines.push('tag ' + dagNode.tag) + if (dagNode.tagger !== null) { + lines.push('tagger ' + gitUtil.serializePersonLine(dagNode.tagger)) + } + lines.push('') + lines.push(dagNode.message) + + let data = lines.join('\n') + + let outBuf = new SmartBuffer() + outBuf.writeString('tag ') + outBuf.writeString(data.length.toString()) + outBuf.writeUInt8(0) + outBuf.writeString(data) + setImmediate(() => callback(null, outBuf.toBuffer())) +} + +exports.deserialize = (data, callback) => { + let lines = data.toString().split('\n') + let res = {gitType: 'tag'} + + for (let line = 0; line < lines.length; line++) { + let m = lines[line].match(/^([^ ]+) (.+)$/) + if (m === null) { + if (lines[line] !== '') { + setImmediate(() => callback(new Error('Invalid tag line ' + line))) + } + res.message = lines.slice(line + 1).join('\n') + break + } + + let key = m[1] + let value = m[2] + switch (key) { + case 'object': + res.object = {'/': gitUtil.shaToCid(new Buffer(value, 'hex'))} + break + case 'tagger': + res.tagger = gitUtil.parsePersonLine(value) + break + case 'tag': + res.tag = value + break + case 'type': + res.type = value + break + default: + res[key] = value + } + } + + setImmediate(() => callback(null, res)) +} diff --git a/src/util/tree.js b/src/util/tree.js new file mode 100644 index 0000000..41520c2 --- /dev/null +++ b/src/util/tree.js @@ -0,0 +1,56 @@ +'use strict' + +const setImmediate = require('async/setImmediate') +const SmartBuffer = require('smart-buffer').SmartBuffer +const gitUtil = require('./util') + +exports = module.exports + +exports.serialize = (dagNode, callback) => { + let entries = [] + Object.keys(dagNode).forEach((name) => { + entries.push([name, dagNode[name]]) + }) + entries.sort((a, b) => a[0] > b[0] ? 1 : -1) + let buf = new SmartBuffer() + entries.forEach((entry) => { + buf.writeStringNT(entry[1].mode + ' ' + entry[0]) + buf.writeBuffer(gitUtil.cidToSha(entry[1].hash['/'])) + }) + + let outBuf = new SmartBuffer() + outBuf.writeString('tree ') + outBuf.writeString(buf.length.toString()) + outBuf.writeUInt8(0) + outBuf.writeBuffer(buf.toBuffer()) + setImmediate(() => callback(null, outBuf.toBuffer())) +} + +exports.deserialize = (data, callback) => { + let res = {} + let buf = SmartBuffer.fromBuffer(data, 'utf8') + + for (;;) { + let modeName = buf.readStringNT() + if (modeName === '') { + break + } + + let hash = buf.readBuffer(gitUtil.SHA1_LENGTH) + let modNameMatched = modeName.match(/^(\d+) (.+)$/) + if (!modNameMatched) { + setImmediate(() => callback(new Error('invalid file mode/name'))) + } + + if (res[modNameMatched[2]]) { + setImmediate(() => callback(new Error('duplicate file in tree'))) + } + + res[modNameMatched[2]] = { + mode: modNameMatched[1], + hash: {'/': gitUtil.shaToCid(hash)} + } + } + + setImmediate(() => callback(null, res)) +} diff --git a/src/util/util.js b/src/util/util.js new file mode 100644 index 0000000..a4b4a71 --- /dev/null +++ b/src/util/util.js @@ -0,0 +1,65 @@ +'use strict' + +const SmartBuffer = require('smart-buffer').SmartBuffer +const multihashes = require('multihashes/src/constants') +const multicodecs = require('multicodec/src/base-table') +const multihash = require('multihashes') +const CID = require('cids') + +exports = module.exports + +exports.SHA1_LENGTH = multihashes.defaultLengths[multihashes.names.sha1] + +exports.find = (buf, byte) => { + for (let i = 0; i < buf.length; i++) { + if (buf[i] === byte) { + return i + } + } + return -1 +} + +exports.parsePersonLine = (line) => { + let matched = line.match(/^(([^<]+)\s)?\s?<([^>]+)>\s?(\d+\s[+\-\d]+)?$/) + if (matched === null) { + return null + } + + return { + name: matched[2], + email: matched[3], + date: matched[4] + } +} + +exports.serializePersonLine = (node) => { + let parts = [] + if (node.name) { + parts.push(node.name) + } + parts.push('<' + node.email + '>') + if (node.date) { + parts.push(node.date) + } + + return parts.join(' ') +} + +exports.shaToCid = (buf) => { + let mhashBuf = new SmartBuffer() + mhashBuf.writeUInt8(1) + mhashBuf.writeBuffer(multicodecs['git-raw']) + mhashBuf.writeUInt8(multihashes.names.sha1) + mhashBuf.writeUInt8(exports.SHA1_LENGTH) + mhashBuf.writeBuffer(buf) + return mhashBuf.toBuffer() +} + +exports.cidToSha = (cidBuf) => { + let mh = multihash.decode(new CID(cidBuf).multihash) + if (mh.name !== 'sha1') { + return null + } + + return mh.digest +} diff --git a/test/fixtures/0a1690e0640a212aafed1824eb208ffddab1789b b/test/fixtures/0a1690e0640a212aafed1824eb208ffddab1789b new file mode 100644 index 0000000..31f6b18 Binary files /dev/null and b/test/fixtures/0a1690e0640a212aafed1824eb208ffddab1789b differ diff --git a/test/fixtures/19f0805d8de36b6442e8c573074112ba72ad6780 b/test/fixtures/19f0805d8de36b6442e8c573074112ba72ad6780 new file mode 100644 index 0000000..8a37516 Binary files /dev/null and b/test/fixtures/19f0805d8de36b6442e8c573074112ba72ad6780 differ diff --git a/test/fixtures/1e2cb60e9e29de3320459f02cb156b66a86925aa b/test/fixtures/1e2cb60e9e29de3320459f02cb156b66a86925aa new file mode 100644 index 0000000..dbe7872 Binary files /dev/null and b/test/fixtures/1e2cb60e9e29de3320459f02cb156b66a86925aa differ diff --git a/test/fixtures/4bbc624c5e1d7e13fac32698c20cf47bc7df3ffa b/test/fixtures/4bbc624c5e1d7e13fac32698c20cf47bc7df3ffa new file mode 100644 index 0000000..d32213e Binary files /dev/null and b/test/fixtures/4bbc624c5e1d7e13fac32698c20cf47bc7df3ffa differ diff --git a/test/fixtures/5af4dc18899e8ac95904eaf2b4abf1a2ca5e1507 b/test/fixtures/5af4dc18899e8ac95904eaf2b4abf1a2ca5e1507 new file mode 100644 index 0000000..34be597 Binary files /dev/null and b/test/fixtures/5af4dc18899e8ac95904eaf2b4abf1a2ca5e1507 differ diff --git a/test/fixtures/672ef117424f54b71e5e058d1184de6a07450d0e b/test/fixtures/672ef117424f54b71e5e058d1184de6a07450d0e new file mode 100644 index 0000000..1491802 Binary files /dev/null and b/test/fixtures/672ef117424f54b71e5e058d1184de6a07450d0e differ diff --git a/test/fixtures/6860d53002bcd8f9a2e371462743340abc1c47a8 b/test/fixtures/6860d53002bcd8f9a2e371462743340abc1c47a8 new file mode 100644 index 0000000..d0b5b56 Binary files /dev/null and b/test/fixtures/6860d53002bcd8f9a2e371462743340abc1c47a8 differ diff --git a/test/fixtures/70ce33c808791776ea89c98bb28bdab6352fc6e3 b/test/fixtures/70ce33c808791776ea89c98bb28bdab6352fc6e3 new file mode 100644 index 0000000..9a9d497 --- /dev/null +++ b/test/fixtures/70ce33c808791776ea89c98bb28bdab6352fc6e3 @@ -0,0 +1,2 @@ +x +!;sRԩKϠ.bR^39[i"Z}:Bˣ2Qj5PIhfWhBQt Jx=:EYu.y ֟x#ri~sUW 6\rp6?}{3^J \ No newline at end of file diff --git a/test/fixtures/788506cb0585ff19bfe9a5add28ab03d1b6db165 b/test/fixtures/788506cb0585ff19bfe9a5add28ab03d1b6db165 new file mode 100644 index 0000000..344f616 Binary files /dev/null and b/test/fixtures/788506cb0585ff19bfe9a5add28ab03d1b6db165 differ diff --git a/test/fixtures/7ffd1401f599c70364eda431d29363e037b2c92c b/test/fixtures/7ffd1401f599c70364eda431d29363e037b2c92c new file mode 100644 index 0000000..2168694 Binary files /dev/null and b/test/fixtures/7ffd1401f599c70364eda431d29363e037b2c92c differ diff --git a/test/fixtures/802992c4220de19a90767f3000a79a31b98d0df7 b/test/fixtures/802992c4220de19a90767f3000a79a31b98d0df7 new file mode 100644 index 0000000..6a172c1 Binary files /dev/null and b/test/fixtures/802992c4220de19a90767f3000a79a31b98d0df7 differ diff --git a/test/fixtures/832b4a8497de78248f70c06e0f06e785a74fea4c b/test/fixtures/832b4a8497de78248f70c06e0f06e785a74fea4c new file mode 100644 index 0000000..ec23575 Binary files /dev/null and b/test/fixtures/832b4a8497de78248f70c06e0f06e785a74fea4c differ diff --git a/test/fixtures/933b7583b7767b07ea4cf242c1be29162eb8bb85 b/test/fixtures/933b7583b7767b07ea4cf242c1be29162eb8bb85 new file mode 100644 index 0000000..d109799 Binary files /dev/null and b/test/fixtures/933b7583b7767b07ea4cf242c1be29162eb8bb85 differ diff --git a/test/fixtures/9f358a4addefcab294b83e4282bfef1f9625a249 b/test/fixtures/9f358a4addefcab294b83e4282bfef1f9625a249 new file mode 100644 index 0000000..d427286 Binary files /dev/null and b/test/fixtures/9f358a4addefcab294b83e4282bfef1f9625a249 differ diff --git a/test/fixtures/README.md b/test/fixtures/README.md new file mode 100644 index 0000000..ce0f10d --- /dev/null +++ b/test/fixtures/README.md @@ -0,0 +1 @@ +Generated with https://github.com/ipfs/go-ipld-git/blob/master/make-test-repo.sh diff --git a/test/fixtures/a0f06ca3cdb1e6e7f603794ef1cd9f9867f85551 b/test/fixtures/a0f06ca3cdb1e6e7f603794ef1cd9f9867f85551 new file mode 100644 index 0000000..b4cc032 Binary files /dev/null and b/test/fixtures/a0f06ca3cdb1e6e7f603794ef1cd9f9867f85551 differ diff --git a/test/fixtures/a847fad8424bf2fed664658d215921a9cf0275a5 b/test/fixtures/a847fad8424bf2fed664658d215921a9cf0275a5 new file mode 100644 index 0000000..9e748a3 Binary files /dev/null and b/test/fixtures/a847fad8424bf2fed664658d215921a9cf0275a5 differ diff --git a/test/fixtures/aa9080345b7881124ee5a605b72d1d7b6892d1dc b/test/fixtures/aa9080345b7881124ee5a605b72d1d7b6892d1dc new file mode 100644 index 0000000..4551572 --- /dev/null +++ b/test/fixtures/aa9080345b7881124ee5a605b72d1d7b6892d1dc @@ -0,0 +1 @@ +xj0EW}iqhJȈL2̴5ڿtwupׅq#(YŅXjc#*qV;Wi3.., J?q~ta|]hp$/ܮ} ?)>X7suhB<3׾ĕ3} 㶰rǸ!P5įjD5@Le \ No newline at end of file diff --git a/test/fixtures/adf239853d9b280be483592b40c4bd8b5d9fb524 b/test/fixtures/adf239853d9b280be483592b40c4bd8b5d9fb524 new file mode 100644 index 0000000..1b0a590 Binary files /dev/null and b/test/fixtures/adf239853d9b280be483592b40c4bd8b5d9fb524 differ diff --git a/test/fixtures/af99300d66a5078022b14c388d214046cb2647ca b/test/fixtures/af99300d66a5078022b14c388d214046cb2647ca new file mode 100644 index 0000000..a5b6e4d Binary files /dev/null and b/test/fixtures/af99300d66a5078022b14c388d214046cb2647ca differ diff --git a/test/fixtures/bcd6c57873ddd85c30e7758a5c34d5ffa37496f8 b/test/fixtures/bcd6c57873ddd85c30e7758a5c34d5ffa37496f8 new file mode 100644 index 0000000..c811391 --- /dev/null +++ b/test/fixtures/bcd6c57873ddd85c30e7758a5c34d5ffa37496f8 @@ -0,0 +1,2 @@ +xAJD1]I'q1+CtΌ x|{U},TnY1,&1Zo1*1)AzJ1t\ki) +iy>~t}S xHRb.zYҽ~6Xzo~dR \ No newline at end of file diff --git a/test/fixtures/bdc9b0f98b578af9015359103503d5de3218efdd b/test/fixtures/bdc9b0f98b578af9015359103503d5de3218efdd new file mode 100644 index 0000000..09ff978 --- /dev/null +++ b/test/fixtures/bdc9b0f98b578af9015359103503d5de3218efdd @@ -0,0 +1,2 @@ +x= +1@abzQI@d,Dooy|kie LA4 =~(=%GmuVH:ADXE뇗ڍB?O 83a1ZhA9iuGJ-W** \ No newline at end of file diff --git a/test/fixtures/ce717819ba52636f465c805794cef0c3103770c4 b/test/fixtures/ce717819ba52636f465c805794cef0c3103770c4 new file mode 100644 index 0000000..cc22d08 --- /dev/null +++ b/test/fixtures/ce717819ba52636f465c805794cef0c3103770c4 @@ -0,0 +1,2 @@ +x5̱0`>FsWc\}?b=Bo/nv$9T&1D: CDjMZ81\̠`:dv@iU2n|" n46eo +S* \ No newline at end of file diff --git a/test/fixtures/d31ec9422b3048f104b4c090cf1c0f5be0963cd9 b/test/fixtures/d31ec9422b3048f104b4c090cf1c0f5be0963cd9 new file mode 100644 index 0000000..1efa5e5 Binary files /dev/null and b/test/fixtures/d31ec9422b3048f104b4c090cf1c0f5be0963cd9 differ diff --git a/test/fixtures/d94c1dbabf13cff8bed80d8818c41b20bca4ee39 b/test/fixtures/d94c1dbabf13cff8bed80d8818c41b20bca4ee39 new file mode 100644 index 0000000..05ce579 --- /dev/null +++ b/test/fixtures/d94c1dbabf13cff8bed80d8818c41b20bca4ee39 @@ -0,0 +1,3 @@ +xK +0@]$@ąE2ҊH=<Ikk)Cߙa Z +XK2>VFhlI7*"; objects.json +paste <(find git/objects -type f) <(find git/objects -type f | cut -d'/' -f3- | sed 's/\///g') | xargs -L 1 mv -v +rm -rf git diff --git a/test/parse.spec.js b/test/parse.spec.js new file mode 100644 index 0000000..4d8792e --- /dev/null +++ b/test/parse.spec.js @@ -0,0 +1,107 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const loadFixture = require('aegir/fixtures') +const zlib = require('zlib') +const ipldGit = require('../src') +const util = require('../src/util/util') +const waterfall = require('async/waterfall') + +const testObjectsJSON = require('./fixtures/objects.json') + +describe('utils', () => { + describe('person line parsing', () => { + it('parses generic line', (done) => { + let info = util.parsePersonLine('Someone 123456 +0123') + expect(info).to.exist() + expect(info.name).to.equal('Someone') + expect(info.email).to.equal('some@one.somewhere') + expect(info.date).to.equal('123456 +0123') + done() + }) + + it('parses 3 segment name', (done) => { + let info = util.parsePersonLine('So Me One 123456 +0123') + expect(info).to.exist() + expect(info.name).to.equal('So Me One') + expect(info.email).to.equal('some@one.somewhere') + expect(info.date).to.equal('123456 +0123') + done() + }) + + it('parses no name line', (done) => { + let info = util.parsePersonLine(' 123456 +0123') + expect(info).to.exist() + expect(info.name).to.not.exist() + expect(info.email).to.equal('some@one.somewhere') + expect(info.date).to.equal('123456 +0123') + done() + }) + + it('parses no name line with space in front', (done) => { + let info = util.parsePersonLine(' 123456 +0123') + expect(info).to.exist() + expect(info.name).to.not.exist() + expect(info.email).to.equal('some@one.somewhere') + expect(info.date).to.equal('123456 +0123') + done() + }) + + it('parses line with nonstandard info', (done) => { + let info = util.parsePersonLine('Some One & Other One 987654 +4321') + expect(info).to.exist() + expect(info.name).to.equal('Some One & Other One') + expect(info.email).to.equal('some@one.somewhere, other@one.elsewhere') + expect(info.date).to.equal('987654 +4321') + done() + }) + + it('parses line without date info', (done) => { + let info = util.parsePersonLine('Someone ') + expect(info).to.exist() + expect(info.name).to.equal('Someone') + expect(info.email).to.equal('some.one@some.where') + expect(info.date).to.not.exist() + done() + }) + }) +}) + +describe('git object parsing', () => { + let objects + + before((done) => { + objects = testObjectsJSON.map(o => [o, zlib.inflateSync(loadFixture(__dirname, '/fixtures/' + o))]) + done() + }) + + it('is parsing and serializing properly', (done) => { + waterfall(objects.map((object) => { + return (cb) => { + ipldGit.util.deserialize(object[1], (err, node) => { + expect(err).to.not.exist() + expect(node).to.exist() + + let expCid = util.shaToCid(new Buffer(object[0], 'hex')) + + ipldGit.util.cid(node, (err, cid) => { + expect(err).to.not.exist() + expect(cid).to.exist() + + expect(cid.buffer.toString('hex')).to.equal(expCid.toString('hex'), 'expected ' + + object[0] + ', got ' + cid.toBaseEncodedString('base16') + ', objtype ' + + node._objtype + ', blob:' + Buffer.isBuffer(node)) + + cb(null) + }) + }) + } + }), done) + }) +}) diff --git a/test/resolver.spec.js b/test/resolver.spec.js new file mode 100644 index 0000000..140134f --- /dev/null +++ b/test/resolver.spec.js @@ -0,0 +1,308 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +const Block = require('ipfs-block') +const map = require('async/map') +const waterfall = require('async/waterfall') +const parallel = require('async/parallel') +const CID = require('cids') +const multihashing = require('multihashing-async') + +const ipldGit = require('../src') +const resolver = ipldGit.resolver + +describe('IPLD format resolver (local)', () => { + let commitBlock + let tagBlock + let treeBlock + let blobBlock + + before((done) => { + const commitNode = { + gitType: 'commit', + tree: {'/': new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr').buffer}, + parents: [ + {'/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer} + ], + author: { + name: 'John Doe', + email: 'johndoe@example.com', + date: '1497302532 +0200' + }, + committer: { + name: 'John Doe', + email: 'johndoe@example.com', + date: '1497302532 +0200' + }, + encoding: 'ISO-8859-1', + message: 'Encoded\n' + } + + const tagNode = { + gitType: 'tag', + object: {'/': new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e').buffer}, + type: 'commit', + tag: 'v0.0.0', + tagger: { + name: 'John Doe', + email: 'johndoe@example.com', + date: '1497302532 +0200' + }, + message: 'A message\n' + } + + const treeNode = { + somefile: { + hash: {'/': new CID('z8mWaJNVTadD7oum3m7f1dmarHvYhFV5b').buffer}, + mode: '100644' + }, + somedir: { + hash: {'/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer}, + mode: '40000' + } + } + + const blobNode = new Buffer('626c6f62203800736f6d6564617461', 'hex') // blob 8\0somedata + + waterfall([ + (cb) => parallel([ + (cb) => ipldGit.util.serialize(commitNode, cb), + (cb) => ipldGit.util.serialize(tagNode, cb), + (cb) => ipldGit.util.serialize(treeNode, cb), + (cb) => ipldGit.util.serialize(blobNode, cb) + ], cb), + (res, cb) => map(res, (s, cb) => { + multihashing(s, 'sha1', (err, multihash) => { + expect(err).to.not.exist() + cb(null, new Block(s, new CID(multihash))) + }) + }, cb), + (blocks, cb) => { + commitBlock = blocks[0] + tagBlock = blocks[1] + treeBlock = blocks[2] + blobBlock = blocks[3] + cb() + } + ], done) + }) + + describe('commit', () => { + it('resolver.tree', (done) => { + resolver.tree(commitBlock, (err, paths) => { + expect(err).to.not.exist() + + expect(paths).to.eql([ + 'message', + 'tree', + 'author/original', + 'author/name', + 'author/email', + 'author/date', + 'committer/original', + 'committer/name', + 'committer/email', + 'committer/date', + 'parents/0', + 'encoding' + ]) + + done() + }) + }) + + it('resolver.isLink with valid Link', (done) => { + resolver.isLink(commitBlock, 'tree', (err, link) => { + expect(err).to.not.exist() + const linkCID = new CID(link['/']) + expect(CID.isCID(linkCID)).to.equal(true) + done() + }) + }) + + it('resolver.isLink with invalid Link', (done) => { + resolver.isLink(commitBlock, '', (err, link) => { + expect(err).to.not.exist() + expect(link).to.equal(false) + done() + }) + }) + + describe('resolver.resolve', () => { + it('path within scope', (done) => { + resolver.resolve(commitBlock, 'message', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.equal('Encoded\n') + done() + }) + }) + + it('path within scope, but nested', (done) => { + resolver.resolve(commitBlock, 'author/name', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.equal('John Doe') + done() + }) + }) + + it('path out of scope', (done) => { + resolver.resolve(commitBlock, 'tree/foo/hash/bar/mode', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.eql({ + '/': new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr').buffer + }) + expect(result.remainderPath).to.equal('foo/hash/bar/mode') + done() + }) + }) + }) + }) + + describe('tag', () => { + it('resolver.tree', (done) => { + resolver.tree(tagBlock, (err, paths) => { + expect(err).to.not.exist() + + expect(paths).to.eql([ + 'object', + 'type', + 'tag', + 'message', + 'tagger/original', + 'tagger/name', + 'tagger/email', + 'tagger/date' + ]) + + done() + }) + }) + + it('resolver.isLink with valid Link', (done) => { + resolver.isLink(tagBlock, 'object', (err, link) => { + expect(err).to.not.exist() + const linkCID = new CID(link['/']) + expect(CID.isCID(linkCID)).to.equal(true) + done() + }) + }) + + it('resolver.isLink with invalid Link', (done) => { + resolver.isLink(tagBlock, '', (err, link) => { + expect(err).to.not.exist() + expect(link).to.equal(false) + done() + }) + }) + + describe('resolver.resolve', () => { + it('path within scope', (done) => { + resolver.resolve(tagBlock, 'message', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.equal('A message\n') + done() + }) + }) + + it('path within scope, but nested', (done) => { + resolver.resolve(tagBlock, 'tagger/name', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.equal('John Doe') + done() + }) + }) + + it('path out of scope', (done) => { + resolver.resolve(tagBlock, 'object/tree/foo/mode', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.eql({ + '/': new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e').buffer + }) + expect(result.remainderPath).to.equal('tree/foo/mode') + done() + }) + }) + }) + }) + + describe('tree', () => { + it('resolver.tree', (done) => { + resolver.tree(treeBlock, (err, paths) => { + expect(err).to.not.exist() + + expect(paths).to.eql([ + 'somedir', + 'somedir/hash', + 'somedir/mode', + 'somefile', + 'somefile/hash', + 'somefile/mode' + ]) + + done() + }) + }) + + it('resolver.isLink with valid Link', (done) => { + resolver.isLink(treeBlock, 'somefile/hash', (err, link) => { + expect(err).to.not.exist() + const linkCID = new CID(link['/']) + expect(CID.isCID(linkCID)).to.equal(true) + done() + }) + }) + + it('resolver.isLink with invalid Link', (done) => { + resolver.isLink(treeBlock, '', (err, link) => { + expect(err).to.not.exist() + expect(link).to.equal(false) + done() + }) + }) + + describe('resolver.resolve', () => { + it('path within scope, nested', (done) => { + resolver.resolve(treeBlock, 'somedir/mode', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.equal('40000') + done() + }) + }) + + it('path out of scope', (done) => { + resolver.resolve(treeBlock, 'somedir/hash/subfile/mode', (err, result) => { + expect(err).to.not.exist() + expect(result.value).to.eql({ + '/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer + }) + expect(result.remainderPath).to.equal('subfile/mode') + done() + }) + }) + }) + }) + + describe('blob', () => { + it('resolver.tree', (done) => { + resolver.tree(blobBlock, (err, paths) => { + expect(err).to.not.exist() + expect(paths).to.eql([]) + done() + }) + }) + + it('resolver.isLink with invalid Link', (done) => { + resolver.isLink(treeBlock, '', (err, link) => { + expect(err).to.not.exist() + expect(link).to.equal(false) + done() + }) + }) + }) +})