diff --git a/generate/input/descriptor.json b/generate/input/descriptor.json index ae8e0b1c4..de5eebf74 100644 --- a/generate/input/descriptor.json +++ b/generate/input/descriptor.json @@ -3597,7 +3597,16 @@ "isAsync": true }, "git_tag_create_frombuffer": { - "ignore": true + "jsFunctionName": "createFromBuffer", + "args": { + "oid": { + "isReturn": true + } + }, + "return": { + "isErrorCode": true + }, + "isAsync": true }, "git_tag_create_lightweight": { "args": { @@ -3698,11 +3707,6 @@ "dependencies": [ "git2/sys/time.h" ], - "fields": { - "sign": { - "ignore": true - } - }, "functions": { "git_time_sign": { "ignore": true diff --git a/generate/templates/partials/convert_to_v8.cc b/generate/templates/partials/convert_to_v8.cc index f3adc3c9b..1227932a6 100644 --- a/generate/templates/partials/convert_to_v8.cc +++ b/generate/templates/partials/convert_to_v8.cc @@ -5,6 +5,9 @@ to = Nan::New({{= parsedName =}}, {{ size }}).ToLocalChecked(); {% elsif cType == 'char **' %} to = Nan::New(*{{= parsedName =}}).ToLocalChecked(); + {% elsif cType == 'char' %} + char convertToNullTerminated[2] = { {{= parsedName =}}, '\0' }; + to = Nan::New(convertToNullTerminated).ToLocalChecked(); {% else %} to = Nan::New({{= parsedName =}}).ToLocalChecked(); {% endif %} diff --git a/lib/signature.js b/lib/signature.js index 7fc8e274d..4dfe84a36 100644 --- a/lib/signature.js +++ b/lib/signature.js @@ -1,11 +1,38 @@ var NodeGit = require("../"); var Signature = NodeGit.Signature; +const toPaddedDoubleDigitString = (number) => { + if (number < 10) { + return `0${number}`; + } + + return `${number}`; +}; + /** * Standard string representation of an author. - * - * @return {string} Representation of the author. + * @param {Boolean} withTime Whether or not to include timestamp + * @return {String} Representation of the author. */ -Signature.prototype.toString = function() { - return this.name().toString() + " <" + this.email().toString() + ">"; +Signature.prototype.toString = function(withTime) { + const name = this.name().toString(); + const email = this.email().toString(); + + let stringifiedSignature = `${name} <${email}>`; + + if (!withTime) { + return stringifiedSignature; + } + + const when = this.when(); + const offset = when.offset(); + const offsetMagnitude = Math.abs(offset); + const time = when.time(); + + const sign = (offset < 0 || when.sign() === "-") ? "-" : "+"; + const hours = toPaddedDoubleDigitString(Math.floor(offsetMagnitude / 60)); + const minutes = toPaddedDoubleDigitString(offsetMagnitude % 60); + + stringifiedSignature += ` ${time} ${sign}${hours}${minutes}`; + return stringifiedSignature; }; diff --git a/lib/tag.js b/lib/tag.js index bf8ddff49..b94872160 100644 --- a/lib/tag.js +++ b/lib/tag.js @@ -2,11 +2,114 @@ var NodeGit = require("../"); var LookupWrapper = NodeGit.Utils.lookupWrapper; var Tag = NodeGit.Tag; +const signatureRegexesBySignatureType = { + gpgsig: [ + /-----BEGIN PGP SIGNATURE-----[\s\S]+?-----END PGP SIGNATURE-----/gm, + /-----BEGIN PGP MESSAGE-----[\s\S]+?-----END PGP MESSAGE-----/gm, + ], + x509: [ + /-----BEGIN SIGNED MESSAGE-----[\s\S]+?-----END SIGNED MESSAGE-----/gm, + ] +}; + /** -* Retrieves the tag pointed to by the oid -* @async -* @param {Repository} repo The repo that the tag lives in -* @param {String|Oid|Tag} id The tag to lookup -* @return {Tag} -*/ + * Retrieves the tag pointed to by the oid + * @async + * @param {Repository} repo The repo that the tag lives in + * @param {String|Oid|Tag} id The tag to lookup + * @return {Tag} + */ Tag.lookup = LookupWrapper(Tag); + +/** + * @async + * @param {Repository} repo + * @param {String} tagName + * @param {Oid} target + * @param {Signature} tagger + * @return {String} + */ +Tag.createBuffer = function(repo, tagName, target, tagger, message) { + return NodeGit.Object.lookup(repo, target, NodeGit.Object.TYPE.ANY) + .then((object) => { + if (!NodeGit.Object.typeisloose(object.type())) { + throw new Error("Object must be a loose type"); + } + + const id = object.id().toString(); + const objectType = NodeGit.Object.type2String(object.type()); + const lines = [ + `object ${id}`, + `type ${objectType}`, + `tag ${tagName}`, + `tagger ${tagger.toString(true)}\n`, + `${message}${message.endsWith("\n") ? "" : "\n"}` + ]; + return lines.join("\n"); + }); +}; + +/** + * @async + * @param {Repository} repo + * @param {String} tagName + * @param {Oid} target + * @param {Signature} tagger + * @param {String} message + * @param {Number} force + * @param {Function} signingCallback Takes a string and returns a string + * representing the signed message + * @return {Oid} + */ +Tag.createWithSignature = function( + repo, + tagName, + target, + tagger, + message, + force, + signingCallback +) { + let tagBuffer; + return Tag.createBuffer(repo, tagName, target, tagger, message) + .then((tagBufferResult) => { + tagBuffer = tagBufferResult; + return signingCallback(tagBuffer); + }) + .then((tagSignature) => { + const normalizedEnding = tagSignature.endsWith("\n") ? "" : "\n"; + const signedTagString = tagBuffer + tagSignature + normalizedEnding; + return Tag.createFromBuffer(repo, signedTagString, force); + }); +}; + +/** + * Retrieves the signature of an annotated tag + * @async + * @param {String} signatureType + * @return {String|null} + */ +Tag.prototype.extractSignature = function(signatureType = "gpgsig") { + const id = this.id(); + const repo = this.repo; + const signatureRegexes = signatureRegexesBySignatureType[signatureType]; + if (!signatureRegexes) { + throw new Error("Unsupported signature type"); + } + + return repo.odb().then((odb) => { + return odb.read(id); + }).then((odbObject) => { + const odbData = odbObject.toString(); + + for (const regex of signatureRegexes) { + const matchResult = regex.exec(odbData); + + if (matchResult !== null) { + return matchResult[0]; + } + } + + return null; + }); +}; diff --git a/test/tests/signature.js b/test/tests/signature.js index bf5f99ef7..9a6b2119b 100644 --- a/test/tests/signature.js +++ b/test/tests/signature.js @@ -104,4 +104,29 @@ describe("Signature", function() { // the self-freeing time should get freed assert.equal(startSelfFreeingCount, endSelfFreeingCount); }); + + it("toString does not provide a timestamp by default", function () { + const signature = Signature.create( + "Shaggy Rogers", + "shaggy@mystery.com", + 987654321, + 90 + ); + + assert.equal(signature.toString(), "Shaggy Rogers "); + }); + + it("toString provides the correct timestamp when requested", function() { + const signature = Signature.create( + "Shaggy Rogers", + "shaggy@mystery.com", + 987654321, + 90 + ); + + assert.equal( + signature.toString(true), + "Shaggy Rogers 987654321 +0130" + ); + }); }); diff --git a/test/tests/tag.js b/test/tests/tag.js index 8d2c48f20..c43a618a1 100644 --- a/test/tests/tag.js +++ b/test/tests/tag.js @@ -207,6 +207,229 @@ describe("Tag", function() { }); }); + it("can create a Tag buffer", function() { + const targetOid = Oid.fromString(commitPointedTo); + const name = "created-signed-tag-annotationCreate"; + const repository = this.repository; + const signature = Signature.create( + "Shaggy Rogers", + "shaggy@mystery.com", + 987654321, + 90 + ); + const message = "I'm a teapot"; + + return Tag.createBuffer(repository, name, targetOid, signature, message) + .then((tagBuffer) => { + const lines = tagBuffer.split("\n"); + assert.equal(7, lines.length); + assert.equal(lines[0], `object ${commitPointedTo}`); + assert.equal(lines[1], "type commit"); + assert.equal(lines[2], `tag ${name}`); + assert.equal( + lines[3], + "tagger Shaggy Rogers 987654321 +0130" + ); + assert.equal(lines[4], ""); + assert.equal(lines[5], message); + assert.equal(lines[6], ""); + }); + }); + + it("can create a Tag from a Tag buffer", function() { + const targetOid = Oid.fromString(commitPointedTo); + const otherTargetOid = Oid.fromString(commitPointedTo2); + const name = "created-signed-tag-annotationCreate"; + const repository = this.repository; + const signature = Signature.create( + "Shaggy Rogers", + "shaggy@mystery.com", + 987654321, + 90 + ); + const message = "I'm a teapot"; + + let odb; + let buffer; + let otherBuffer; + + return repository.odb() + .then((odbResult) => { + odb = odbResult; + return Tag.createBuffer( + repository, + name, + targetOid, + signature, + message + ); + }) + .then((bufferResult) => { + buffer = bufferResult; + return Tag.createBuffer( + repository, + name, + otherTargetOid, + signature, + message + ); + }) + .then((bufferResult) => { + otherBuffer = bufferResult; + return Tag.createFromBuffer(repository, buffer, 1); + }) + .then((oid) => { + return odb.read(oid); + }) + .then((object) => { + const lines = object.toString().split("\n"); + assert(object.type(), Obj.TYPE.TAG); + assert.equal(7, lines.length); + assert.equal(lines[0], `object ${commitPointedTo}`); + assert.equal(lines[1], "type commit"); + assert.equal(lines[2], `tag ${name}`); + assert.equal( + lines[3], + "tagger Shaggy Rogers 987654321 +0130" + ); + assert.equal(lines[4], ""); + assert.equal(lines[5], message); + assert.equal(lines[6], ""); + }) + .then(() => { + // overwriting is okay + return Tag.createFromBuffer(repository, otherBuffer, 1); + }) + .then(() => { + // overwriting is not okay + return Tag.createFromBuffer(repository, buffer, 0); + }) + .then(() => { + return Promise.reject( + new Error("should not be able to create the '" + name + "' tag twice") + ); + }, + () => { + return Promise.resolve(); + }); + }); + + it("can create a tag with a signature and extract the signature", function() { + const targetOid = Oid.fromString(commitPointedTo); + const otherTargetOid = Oid.fromString(commitPointedTo2); + const name = "created-signed-tag-annotationCreate"; + const repository = this.repository; + const signature = Signature.create( + "Shaggy Rogers", + "shaggy@mystery.com", + 987654321, + 90 + ); + const signatureLines = [ + "-----BEGIN PGP SIGNATURE-----", + "iQIzBAABCAAdFiEEKdxGpJ93wnkLaBKfURjJKedOfEMFAlxR4JUACgkQURjJKedO", + "fEN+8A//cXmkRmhzQMdTEdrxty7tVKQ7lVhL7r7e+cB84hO7WrDn8549c7/Puflu", + "idanWfyoAEMSNWDgY84lx/t3I3YYKXsLDPT93HiMhCXmPVZcfLxlARRL1rrNZV4q", + "L9hhqb9bFrRNBn6YebhygeLXLHlDKEZzx8W9jnDLU8Px8UTkwdQIDnPDfT7UOPPU", + "MYDgP3OwWwoG8dUlZXaHjtFz29wPlJo177MwdLYwn4zpEIysoY1ev5IKWD+LPW4g", + "vdQnaK1x3dozmG8YLUZw5iW7ap9DpahbAGQgdy1z1ypiNUjNuhaP8zkG1ci6X88N", + "6MIoQ+YqfowRJJTIr1lzssxsRI1syjfS6smnI4ZNE6S+6mIKN96ES2OZF+rn4xnD", + "PofR9Qh2gPq++ULriPE/cX7ZkZ0/ZDZGDfIGvricB8JEJhISZn/VMX/KScJs+rFq", + "KWN5Au6Uc2pEqeq5OP4y2k0QUmKQT9sh9OepnPmfqF8hG6wI8nM67jT/FEOcpr0v", + "qoN2NRXrcq3iZAp07AGq9IdpYhBcEW7MFmOcNt+Zb8SbTMp6DawnREg9xzz1SIkZ", + "Cdp1XoJ6mkVvzBB4T/Esp7j1VztinTX2PpX7C1CE5LC76UfCiEjEWOmWrVuPuA5a", + "oRrJvgPJg8gpVj04r2m8nvUK1gwhxg9ZB+SK+nd3OAd0dnbJwTE=", + "=dW3g", + "-----END PGP SIGNATURE-----" + ]; + const message = "I'm a teapot"; + const signingCallback = (message) => { + return signatureLines.join("\n"); + }; + + let odb; + let oid; + let object; + + return repository.odb() + .then((odbResult) => { + odb = odbResult; + + return Tag.createWithSignature( + repository, + name, + targetOid, + signature, + message, + 1, + signingCallback + ); + }) + .then((oidResult) => { + oid = oidResult; + return odb.read(oid); + }) + .then((objectResult) => { + object = objectResult; + const lines = object.toString().split("\n"); + assert(object.type(), Obj.TYPE.TAG); + assert.equal(signatureLines.length + 7, lines.length); + assert.equal(lines[0], `object ${commitPointedTo}`); + assert.equal(lines[1], "type commit"); + assert.equal(lines[2], `tag ${name}`); + assert.equal( + lines[3], + "tagger Shaggy Rogers 987654321 +0130" + ); + assert.equal(lines[4], ""); + assert.equal(lines[5], message); + for (let i = 6; i < 6 + signatureLines.length; i++) { + assert.equal(lines[i], signatureLines[i - 6]); + } + assert.equal(lines[6 + signatureLines.length], ""); + + return Tag.lookup(repository, oid); + }) + .then((tag) => { + return tag.extractSignature(); + }) + .then((tagSignature) => { + assert.equal(tagSignature, signatureLines.join("\n")); + }) + .then(() => { + // overwriting is okay + return Tag.createWithSignature( + repository, + name, + targetOid, + signature, + message, + 1, + signingCallback + ); + }) + .then(() => { + // overwriting is not okay + return Tag.createWithSignature( + repository, + name, + otherTargetOid, + signature, + message, + 0, + signingCallback + ); + }) + .then(() => { + return Promise.reject( + new Error("should not be able to create the '" + name + "' tag twice") + ); + }, + () => { + return Promise.resolve(); + }); + }); + it("can create a new signed tag with Tag.annotationCreate", function() { var oid = Oid.fromString(commitPointedTo); var name = "created-signed-tag-annotationCreate";