Skip to content

Commit

Permalink
Generate links to changes, if possible.
Browse files Browse the repository at this point in the history
  • Loading branch information
totherik committed Feb 22, 2015
1 parent b2d07de commit d5bc10d
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 48 deletions.
49 changes: 40 additions & 9 deletions lib/mailer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Semver from 'semver';
import Nodemailer from 'nodemailer';
import { dedent } from './utils';
import template from './template';
import { git2http, compare } from './utils';


export default class Mailer {
Expand All @@ -18,18 +19,48 @@ export default class Mailer {
send(pkg, cb) {
let { id: name, doc } = pkg;
let { 'dist-tags': { latest: version } } = doc;
let { homepage, repository, description, author, _npmUser: publisher } = doc.versions[version];
let {
homepage,
repository = { },
description,
author,
_npmUser: publisher,
gitHead: currentGitHead
} = doc.versions[version];

if (typeof repository === 'object') {
repository = repository.url;
}
// Generate link to changes since last publish.
let changes = undefined;
let { url: repo = 'n/a' } = repository;

// Hamfisted way of ensuring the repo is a GitHub repo so that we generate
// correct links. These links wouldn't make sense outside GitHub anyway.
if (repository.type === 'git' && repo.indexOf('github') !== -1 && currentGitHead) {
// Do our best to convert the provided URL to an SSL git checkout URL.
repo = git2http(repo);
if (typeof repo === 'string') {
// Remove the extension to (hopefully) derive a GitHub URL.
repo = repo.replace(/\.git$/, '');

let subject = `npm publish ${name}@${version}`;
let text = template({ name, version, homepage, repository, description, author, publisher });
// Grab the keys of all published versions and sort using a
// semver comparator. The result *should* be a correctly ordered
// version array, from which we can grab the previous version. It
// may not make sense to grab versions across major release boundaries,
// but this can be fine-tuned in the future, if necessary.
let versions = Object.keys(doc.versions).sort(Semver.compare);
let previous = versions[versions.indexOf(version) - 1];
let { gitHead: previousGitHead } = (doc.versions[previous] || {});

if (previousGitHead && previousGitHead !== currentGitHead) {
changes = `${repo}/compare/${previousGitHead}...${currentGitHead}`;
} else {
changes = `${repo}/commit/${currentGitHead}`;
}
}
}

let message = Object.assign({
subject,
text
subject: `npm publish ${name}@${version}`,
text: template({ name, version, homepage, repo, description, author, publisher, changes })
}, this.message);

this.transport.sendMail(message, cb);
Expand Down
27 changes: 15 additions & 12 deletions lib/template.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { dedent } from './utils'
import cool from 'cool-ascii-faces';


export default ({ name, version, description, author = {}, publisher = {}, homepage = 'n/a', repository = 'Not found.' }) => {
export default ({ name, version, description, author = {}, publisher = {}, homepage = 'n/a', repo = 'n/a', changes = '' }) => {

let { name: authorName = 'anonymous', email: authorEmail = '' } = author;
let { name: publisherName = 'anonymous', email: publisherEmail = '' } = publisher;

return dedent`
Module: ${name}
Version: ${version}
Author: ${authorName}${authorEmail && ` <${authorEmail}>`}
Published By: ${publisherName}${publisherEmail && ` <${publisherEmail}>`}
Homepage: ${homepage}
Repository: ${repository}
Description: ${description}
`;
}
return `
${name} ${version}
${description}
Author: ${authorName}${authorEmail && ` <${authorEmail}>`}
Publisher: ${publisherName}${publisherEmail && ` <${publisherEmail}>`}
Homepage: ${homepage}
Repository: ${repo}
${changes}
${cool()}`;
};
58 changes: 33 additions & 25 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
import Url from 'url';

// From: https://gist.github.com/zenparsing/5dffde82d9acef19e43c
function dedent(callsite, ...args) {

function format(str) {
let size = -1;

return str.replace(/\n(\s+)/g, (m, m1) => {
if (size < 0) {
size = m1.replace(/\t/g, ' ').length;
}

return '\n' + m1.slice(Math.min(m1.length, size));
});
function git2http(url) {
// Discard non-strings
if (typeof url !== 'string') {
return undefined;
}

if (typeof callsite === 'string') {
return format(callsite);
// Try parsing the URL, if that fails one symptom is
// a missing protocol. If the protocol is missing,
// prepend one and try again. That way we don't have to
// try to write url parsing rules here, we just test
// for what we need.
let parsed = Url.parse(url);
if (!parsed.protocol && !parsed.hostname) {
// If there's no protocol, we assume the parse failed. Will
// happen, for example, on [email protected]:org/repository.git.
parsed = Url.parse('https://' + url);
if (!parsed.protocol && !parsed.hostname) {
// Adding a protocol didn't help, so not a valid uri for our needs.
return undefined;
}
}

if (typeof callsite === 'function') {
return (...args) => format(callsite(...args));
}

let output = callsite
.slice(0, args.length + 1)
.map((text, i) => (i === 0 ? '' : args[i - 1]) + text)
.join('');

return format(output);
// Git paths like github.com:org/repo produce the pathname
// '/:org/repo', so the colon needs to be removed. Also,
// remove the optional `.git` extension.
parsed.pathname = parsed.pathname.replace(/^\/\:/, '/');
parsed.protocol = 'https:';
parsed.slashes = true;
parsed.auth = null;
parsed.host = null;
parsed.path = null;
parsed.search = null;
parsed.hash = null;
parsed.query = null;
return Url.format(parsed);
}

export default {

dedent
git2http

};
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"prepublish": "npm run compile",
"compile": "babel --optional selfContained --modules common --out-dir dist index.js lib/*.js bin/*.js",
"compile": "babel --optional runtime --modules common --out-dir dist index.js lib/*.js bin/*.js",
"test": "npm run compile && babel-node test/harness.js test/*-test.js"
},
"keywords": [
Expand All @@ -23,14 +23,18 @@
"license": "ISC",
"dependencies": {
"babel-runtime": "^4.3.0",
"cool-ascii-faces": "^1.3.3",
"dbrickashaw": "^5.0.2",
"disclose": "^1.0.0",
"minimist": "^1.1.0",
"nodemailer": "^1.3.0",
"semver": "^4.3.0",
"zmq": "^2.10.0"
},
"devDependencies": {
"babel": "^4.3.0"
"babel": "^4.3.0",
"glob": "^4.4.0",
"tape": "^3.5.0"
},
"repository": {
"type": "git",
Expand Down
21 changes: 21 additions & 0 deletions test/harness.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import tape from 'tape';
import glob from 'glob';
import Path from 'path';

// Kick things off, but only after the module has completed loading,
// hence the setImmediate. If the load the modules synchronously,
// the exported object isn't yet available (since tests import this
// module) and we get into a weird state.
setImmediate(() => {
// All this mess for npm < 2. With 2.x this can be removed
// and npm script argument globbing can be used.
process.argv.slice(2).forEach(arg => {
glob.sync(arg).forEach(file => {
require(Path.resolve(process.cwd(), file));
});
});

// Get a handle on the root test harness so we
// can forcefull kill the process (THANKS TIMERS!)
//tape().on('end', function () { setImmediate(process.exit, 0) });
});
37 changes: 37 additions & 0 deletions test/utils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from 'tape'
import Path from 'path';
import { git2http } from '../dist/lib/utils';


test('git2http', function (t) {

let uris = [
['https://github.com/org/repository', 'https://github.com/org/repository'],
['https://github.com/org/repository.git', 'https://github.com/org/repository.git'],
['git://github.com/org/repository.git', 'https://github.com/org/repository.git'],
['git://github.com:org/repository.git', 'https://github.com/org/repository.git'],
['[email protected]:org/repository', 'https://github.com/org/repository'],
['[email protected]:org/repository.git', 'https://github.com/org/repository.git'],
['[email protected]/org/repository', 'https://github.com/org/repository'],
['[email protected]/org/repository.git', 'https://github.com/org/repository.git'],
['git://[email protected]:org/repository', 'https://github.com/org/repository'],
['git://[email protected]:org/repository.git', 'https://github.com/org/repository.git'],
['git://[email protected]/org/repository', 'https://github.com/org/repository'],
['git://[email protected]/org/repository.git', 'https://github.com/org/repository.git'],
['git+ssh://[email protected]:org/repository', 'https://github.com/org/repository'],
['git+ssh://[email protected]:org/repository.git', 'https://github.com/org/repository.git'],
['git+ssh://[email protected]/org/repository', 'https://github.com/org/repository'],
['git+ssh://[email protected]/org/repository.git', 'https://github.com/org/repository.git']
];

t.test('uri', function () {
//let expected = 'https://github.com/org/repository';

for (let [uri, expected] of uris) {
t.equal(git2http(uri), expected);
}

t.end();
});

});

0 comments on commit d5bc10d

Please sign in to comment.