aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregg Caines <[email protected]>2013-11-15 13:08:29 -0800
committerGregg Caines <[email protected]>2013-11-15 13:08:29 -0800
commit097ce4e175e701efb95fd8aeaeb81a9d929d90ba (patch)
tree97bf43b178f0f21b168c10474afbae7627ecc652
parentd52c62b04c5c11fd931ca967f09621d03c330f88 (diff)
parenta639b7dcb280f3b1ab6c8357fb3c8b9057543e79 (diff)
downloadnode-coveralls-097ce4e175e701efb95fd8aeaeb81a9d929d90ba.tar.xz
node-coveralls-097ce4e175e701efb95fd8aeaeb81a9d929d90ba.zip
Merge pull request #25 from tdd/master
Fix direct dev use on a local Git repo + improve Git metadata fetching
-rw-r--r--Makefile11
-rw-r--r--lib/detectLocalGit.js26
-rw-r--r--lib/fetchGitData.js135
-rw-r--r--lib/getOptions.js8
-rw-r--r--lib/handleInput.js6
-rw-r--r--test/fetchGitData.js6
-rw-r--r--test/getOptions.js191
7 files changed, 302 insertions, 81 deletions
diff --git a/Makefile b/Makefile
index 938b2c0..6d761a3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,10 @@
REPORTER = spec
test:
- $(MAKE) lint
- echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID)
+ @$(MAKE) lint
+ @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID)
+ @$(MAKE) core_test
+
+core_test:
@NODE_ENV=test ./node_modules/.bin/mocha -b --require blanket --reporter $(REPORTER)
lint:
@@ -9,11 +12,11 @@ lint:
test-cov:
$(MAKE) test REPORTER=spec
- $(MAKE) test REPORTER=html-cov 1> coverage.html
+ $(MAKE) core_test REPORTER=html-cov > coverage.html
test-coveralls:
$(MAKE) test REPORTER=spec
- $(MAKE) test REPORTER=mocha-lcov-reporter | ./bin/coveralls.js --verbose
+ $(MAKE) core_test REPORTER=mocha-lcov-reporter | ./bin/coveralls.js --verbose
rm -rf lib-cov
.PHONY: test
diff --git a/lib/detectLocalGit.js b/lib/detectLocalGit.js
new file mode 100644
index 0000000..4fd8d1b
--- /dev/null
+++ b/lib/detectLocalGit.js
@@ -0,0 +1,26 @@
+var fs = require('fs');
+var path = require('path');
+
+var REGEX_BRANCH = /^ref: refs\/heads\/(\w+)$/;
+
+module.exports = function detectLocalGit(knownCommit, knownBranch) {
+ var dir = process.cwd(), gitDir;
+ while ('/' !== dir) {
+ gitDir = path.join(dir, '.git');
+ if (fs.existsSync(path.join(gitDir, 'HEAD')))
+ break;
+
+ dir = path.dirname(dir);
+ }
+
+ if ('/' === dir)
+ return;
+
+ var head = fs.readFileSync(path.join(dir, '.git', 'HEAD'), 'utf-8').trim();
+ var branch = (head.match(REGEX_BRANCH) || [])[1];
+ if (!branch)
+ return { git_commit: head };
+
+ var commit = fs.readFileSync(path.join(dir, '.git', 'refs', 'heads', branch), 'utf-8').trim();
+ return { git_commit: commit, git_branch: branch };
+};
diff --git a/lib/fetchGitData.js b/lib/fetchGitData.js
index a77b4a8..1633bd5 100644
--- a/lib/fetchGitData.js
+++ b/lib/fetchGitData.js
@@ -1,55 +1,20 @@
var exec = require('child_process').exec;
var logger = require('./logger')();
-var fetchGitData = function(git, cb) {
+function fetchGitData(git, cb) {
if (!cb){
throw new Error("fetchGitData requires a callback");
}
- var i;
- var execGit = true;
- var head = {
- "author_name": {
- "format": "'%aN'",
- },
- "author_email": {
- "format": "'%ae'",
- },
- "committer_name": {
- "format": "'%cN'",
- },
- "committer_email": {
- "format": "'%ce'",
- },
- "message": {
- "format": "'%s'",
- }
- };
- var remotes = {};
-
//-- Malformed/undefined git object
if ('undefined' === typeof git) {
return cb(new Error('No options passed'));
- } else if (!git.hasOwnProperty('head')) {
+ }
+ if (!git.hasOwnProperty('head')) {
return cb(new Error('You must provide the head'));
- } else if (!git.head.hasOwnProperty('id')) {
- return cb(new Error('You must provide the head.id'));
}
-
- function saveRemote(name, url, push) {
- var key = name + "-" + url;
- if ("undefined" === typeof push || "boolean" !== typeof push) {
- push = true;
- }
- if (!remotes.hasOwnProperty(key)) {
- remotes[key] = true;
- if (push) {
- git.remotes.push({
- "name": name,
- "url": url
- });
- }
- }
+ if (!git.head.hasOwnProperty('id')) {
+ return cb(new Error('You must provide the head.id'));
}
//-- Set required properties of git if they weren"t provided
@@ -69,7 +34,7 @@ var fetchGitData = function(git, cb) {
}
//-- Use git?
- exec("git log -1 " + git.head.id + " --pretty=format:'%H'", function(err, response){
+ exec("git rev-parse --verify " + git.head.id, function(err, response){
if (err){
// git is not available...
git.head.author_name = git.head.author_name || "Unknown Author";
@@ -80,42 +45,64 @@ var fetchGitData = function(git, cb) {
return cb(null, git);
}
- //-- Head
- var commands = [];
- var fields = [];
- for (var field in head) {
- fields.push(field);
- var command = "git log -1 " + git.head.id + " --pretty=format:" + head[field].format;
- commands.push(command);
+ fetchHeadDetails(git, cb);
+ });
+}
+
+function fetchBranch(git, cb) {
+ exec("git branch", function(err, branches){
+ if (err)
+ return cb(err);
+
+ git.branch = (branches.match(/^\* (\w+)/) || [])[1];
+ fetchRemotes(git, cb);
+ });
+}
+
+var REGEX_COMMIT_DETAILS = /\nauthor (.+?) <(.+?)>.+\ncommitter (.+?) <(.+?)>.+\n\n(.*)/m;
+
+function fetchHeadDetails(git, cb) {
+ exec('git cat-file -p ' + git.head.id, function(err, response) {
+ if (err)
+ return cb(err);
+
+ var items = response.match(REGEX_COMMIT_DETAILS).slice(1);
+ var fields = ['author_name', 'author_email', 'committer_name', 'committer_email', 'message'];
+ fields.forEach(function(field, index) {
+ git.head[field] = items[index];
+ });
+
+ if (git.branch) {
+ fetchRemotes(git, cb);
+ } else {
+ fetchBranch(git, cb);
}
- var i = 0;
- var remaining = commands.length;
- commands.forEach(function(command){
- var field = fields[i];
- i++;
- exec(command, function(err, response){
- if (err) return cb(err);
- git.head[field] = response;
- remaining--;
- if (remaining === 0){
- //-- Branch
- exec("git branch", function(err, branches){
- if (err) return cb(err);
- git.branch = branches.split("\n")[0].replace(/^\*\ /, "").trim();
- exec("git remote -v", function(err, remotes){
- if (err) return cb(err);
- remotes.split("\n").forEach(function(remote) {
- remote = remote.split(/\s/);
- saveRemote(remote[0], remote[1]);
- });
- return cb(null, git);
- });
- });
- }
- });
+ });
+}
+
+function fetchRemotes(git, cb) {
+ exec("git remote -v", function(err, remotes){
+ if (err)
+ return cb(err);
+
+ var processed = {};
+ remotes.split("\n").forEach(function(remote) {
+ if (!/\s\(push\)$/.test(remote))
+ return;
+ remote = remote.split(/\s+/);
+ saveRemote(processed, git, remote[0], remote[1]);
});
+ cb(null, git);
});
-};
+}
+
+function saveRemote(processed, git, name, url) {
+ var key = name + "-" + url;
+ if (processed.hasOwnProperty(key))
+ return;
+ processed[key] = true;
+ git.remotes.push({ name: name, url: url });
+}
module.exports = fetchGitData;
diff --git a/lib/getOptions.js b/lib/getOptions.js
index 35187b4..abd2f2e 100644
--- a/lib/getOptions.js
+++ b/lib/getOptions.js
@@ -43,6 +43,14 @@ var getBaseOptions = function(cb){
options.service_job_id = process.env.COVERALLS_SERVICE_JOB_ID;
}
+ if (!git_commit || !git_branch) {
+ var data = require('./detectLocalGit')(git_commit, git_branch);
+ if (data) {
+ git_commit = git_commit || data.git_commit;
+ git_branch = git_branch || data.git_branch;
+ }
+ }
+
// try to get the repo token as an environment variable
if (process.env.COVERALLS_REPO_TOKEN) {
options.repo_token = process.env.COVERALLS_REPO_TOKEN;
diff --git a/lib/handleInput.js b/lib/handleInput.js
index 1435c1c..807e31d 100644
--- a/lib/handleInput.js
+++ b/lib/handleInput.js
@@ -1,9 +1,10 @@
var index = require('../index');
var logger = require('./logger')();
-var handleInput = function(input){
+function handleInput(input) {
logger.debug(input);
var options = index.getOptions(function(err, options){
+
if (err){
logger.error("error from getOptions");
throw err;
@@ -28,7 +29,6 @@ var handleInput = function(input){
});
});
});
-
-};
+}
module.exports = handleInput;
diff --git a/test/fetchGitData.js b/test/fetchGitData.js
index de5c474..9c00828 100644
--- a/test/fetchGitData.js
+++ b/test/fetchGitData.js
@@ -9,6 +9,12 @@ describe("fetchGitData", function(){
it("should throw an error when no data is passed", function() {
fetchGitData.should.throw(/fetchGitData requires a callback/);
});
+ it('should throw an error when no git context is provided', function(done) {
+ fetchGitData(undefined, function(err){
+ err.should.match(/No options passed/);
+ done();
+ });
+ });
it("should throw an error if no head is provided", function(done) {
fetchGitData({
}, function(err){
diff --git a/test/getOptions.js b/test/getOptions.js
index 939cedd..0639a3c 100644
--- a/test/getOptions.js
+++ b/test/getOptions.js
@@ -16,9 +16,24 @@ describe("getBaseOptions", function(){
it ("should set git branch if it exists", function(done){
testGitBranch(getBaseOptions, done);
});
+ it ("should detect current git hash if not passed in", function(done) {
+ testGitHashDetection(getBaseOptions, done);
+ });
+ it ("should detect current git branch if not passed in", function(done) {
+ testGitBranchDetection(getBaseOptions, done);
+ });
+ it ("should detect detached git head if no hash passed in", function(done) {
+ testGitDetachedHeadDetection(getBaseOptions, done);
+ });
+ it ("should fail local Git detection if no .git directory", function(done) {
+ testNoLocalGit(getBaseOptions, done);
+ });
it ("should set repo_token if it exists", function(done){
testRepoToken(getBaseOptions, done);
});
+ it ("should detect repo_token if not passed in", function(done){
+ testRepoTokenDetection(getBaseOptions, done);
+ });
it ("should set service_name if it exists", function(done){
testServiceName(getBaseOptions, done);
});
@@ -31,12 +46,21 @@ describe("getBaseOptions", function(){
it ("should set service_name and service_job_id if it's running on circleci", function(done){
testCircleCi(getBaseOptions, done);
});
+ it ("should set service_name and service_job_id if it's running on codeship", function(done){
+ testCodeship(getBaseOptions, done);
+ });
});
describe("getOptions", function(){
beforeEach(function(){
process.env = {};
});
+ it ("should require a callback", function(done) {
+ (function() {
+ getOptions();
+ }).should.throw();
+ done();
+ });
it ("should get a filepath if there is one", function(done){
process.argv[2] = "somepath";
getOptions(function(err, options){
@@ -62,9 +86,24 @@ describe("getOptions", function(){
it ("should set git branch if it exists", function(done){
testGitBranch(getOptions, done);
});
+ it ("should detect current git hash if not passed in", function(done) {
+ testGitHashDetection(getOptions, done);
+ });
+ it ("should detect current git branch if not passed in", function(done) {
+ testGitBranchDetection(getOptions, done);
+ });
+ it ("should detect detached git head if no hash passed in", function(done) {
+ testGitDetachedHeadDetection(getOptions, done);
+ });
+ it ("should fail local Git detection if no .git directory", function(done) {
+ testNoLocalGit(getOptions, done);
+ });
it ("should set repo_token if it exists", function(done){
testRepoToken(getOptions, done);
});
+ it ("should detect repo_token if not passed in", function(done){
+ testRepoTokenDetection(getOptions, done);
+ });
it ("should set service_name if it exists", function(done){
testServiceName(getOptions, done);
});
@@ -77,6 +116,9 @@ describe("getOptions", function(){
it ("should set service_name and service_job_id if it's running on circleci", function(done){
testCircleCi(getOptions, done);
});
+ it ("should set service_name and service_job_id if it's running on codeship", function(done){
+ testCodeship(getOptions, done);
+ });
});
var testServiceJobId = function(sut, done){
@@ -95,6 +137,24 @@ var testGitHash = function(sut, done){
});
};
+var testGitDetachedHeadDetection = function(sut, done){
+ var localGit = ensureLocalGitContext({ detached: true });
+ sut(function(err, options) {
+ options.git.head.id.should.equal(localGit.id);
+ localGit.wrapUp();
+ done();
+ });
+};
+
+var testGitHashDetection = function(sut, done){
+ var localGit = ensureLocalGitContext();
+ sut(function(err, options) {
+ options.git.head.id.should.equal(localGit.id);
+ localGit.wrapUp();
+ done();
+ });
+};
+
var testGitBranch = function(sut, done){
process.env.COVERALLS_GIT_COMMIT = "e3e3e3e3e3e3e3e3e";
process.env.COVERALLS_GIT_BRANCH = "master";
@@ -104,6 +164,27 @@ var testGitBranch = function(sut, done){
});
};
+var testGitBranchDetection = function(sut, done){
+ var localGit = ensureLocalGitContext();
+ sut(function(err, options) {
+ if (localGit.branch)
+ options.git.branch.should.equal(localGit.branch);
+ else
+ options.git.should.not.have.property('branch');
+ localGit.wrapUp();
+ done();
+ });
+};
+
+var testNoLocalGit = function(sut, done){
+ var localGit = ensureLocalGitContext({ noGit: true });
+ sut(function(err, options) {
+ options.should.not.have.property('git');
+ localGit.wrapUp();
+ done();
+ });
+};
+
var testRepoToken = function(sut, done){
process.env.COVERALLS_REPO_TOKEN = "REPO_TOKEN";
sut(function(err, options){
@@ -112,6 +193,28 @@ var testRepoToken = function(sut, done){
});
};
+var testRepoTokenDetection = function(sut, done) {
+ var fs = require('fs');
+ var path = require('path');
+
+ var file = path.join(process.cwd(), '.coveralls.yml'), token, synthetic = false;
+ if (fs.exists(file)) {
+ var yaml = require('yaml');
+ /* jshint evil:true */
+ token = yaml.eval(fs.readFileSync(yml, 'utf8')).repo_token;
+ } else {
+ token = 'REPO_TOKEN';
+ fs.writeFileSync(file, 'repo_token: ' + token);
+ synthetic = true;
+ }
+ sut(function(err, options) {
+ options.repo_token.should.equal(token);
+ if (synthetic)
+ fs.unlink(file);
+ done();
+ });
+};
+
var testServiceName = function(sut, done){
process.env.COVERALLS_SERVICE_NAME = "SERVICE_NAME";
sut(function(err, options){
@@ -171,3 +274,91 @@ var testCircleCi = function(sut, done){
done();
});
};
+
+var testCodeship = function(sut, done) {
+ process.env.CI_NAME = 'codeship';
+ process.env.CI_BUILD_NUMBER = '1234';
+ process.env.CI_COMMIT_ID = "e3e3e3e3e3e3e3e3e";
+ process.env.CI_BRANCH = "master";
+ sut(function(err, options){
+ options.service_name.should.equal("codeship");
+ options.service_job_id.should.equal("1234");
+ options.git.should.eql({ head:
+ { id: 'e3e3e3e3e3e3e3e3e',
+ author_name: 'Unknown Author',
+ author_email: '',
+ committer_name: 'Unknown Committer',
+ committer_email: '',
+ message: 'Unknown Commit Message' },
+ branch: 'master',
+ remotes: [] });
+ done();
+ });
+};
+
+function ensureLocalGitContext(options) {
+ var path = require('path');
+ var fs = require('fs');
+
+ var baseDir = process.cwd(), dir = baseDir, gitDir;
+ while ('/' !== dir) {
+ gitDir = path.join(dir, '.git');
+ if (fs.existsSync(path.join(gitDir, 'HEAD')))
+ break;
+
+ dir = path.dirname(dir);
+ }
+
+ options = options || {};
+ var synthetic = '/' === dir;
+ var gitHead, content, branch, id, wrapUp = function() {};
+
+ if (synthetic) {
+ branch = 'synthetic';
+ id = '424242424242424242';
+ gitHead = path.join('.git', 'HEAD');
+ var gitBranch = path.join('.git', 'refs', 'heads', branch);
+ fs.mkdirSync('.git');
+ if (options.detached) {
+ fs.writeFileSync(gitHead, id, { encoding: 'utf8' });
+ } else {
+ fs.mkdirSync(path.join('.git', 'refs'));
+ fs.mkdirSync(path.join('.git', 'refs', 'heads'));
+ fs.writeFileSync(gitHead, "ref: refs/heads/" + branch, { encoding: 'utf8' });
+ fs.writeFileSync(gitBranch, id, { encoding: 'utf8' });
+ }
+ wrapUp = function() {
+ fs.unlinkSync(gitHead);
+ if (!options.detached) {
+ fs.unlinkSync(gitBranch);
+ fs.rmdirSync(path.join('.git', 'refs', 'heads'));
+ fs.rmdirSync(path.join('.git', 'refs'));
+ }
+ fs.rmdirSync('.git');
+ };
+ } else if (options.noGit) {
+ fs.renameSync(gitDir, gitDir + '.bak');
+ wrapUp = function() {
+ fs.renameSync(gitDir + '.bak', gitDir);
+ };
+ } else if (options.detached) {
+ gitHead = path.join(gitDir, 'HEAD');
+ content = fs.readFileSync(gitHead, 'utf8').trim();
+ var b = (content.match(/^ref: refs\/heads\/(\S+)$/) || [])[1];
+ if (!b) {
+ id = content;
+ } else {
+ id = fs.readFileSync(path.join(gitDir, 'refs', 'heads', b), 'utf8').trim();
+ fs.writeFileSync(gitHead, id, 'utf8');
+ wrapUp = function() {
+ fs.writeFileSync(gitHead, content, 'utf8');
+ };
+ }
+ } else {
+ content = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
+ branch = (content.match(/^ref: refs\/heads\/(\S+)$/) || [])[1];
+ id = branch ? fs.readFileSync(path.join(gitDir, 'refs', 'heads', branch), 'utf8').trim() : content;
+ }
+
+ return { id: id, branch: branch, wrapUp: wrapUp };
+}