diff options
| author | Gregg Caines <[email protected]> | 2013-11-15 13:08:29 -0800 |
|---|---|---|
| committer | Gregg Caines <[email protected]> | 2013-11-15 13:08:29 -0800 |
| commit | 097ce4e175e701efb95fd8aeaeb81a9d929d90ba (patch) | |
| tree | 97bf43b178f0f21b168c10474afbae7627ecc652 | |
| parent | d52c62b04c5c11fd931ca967f09621d03c330f88 (diff) | |
| parent | a639b7dcb280f3b1ab6c8357fb3c8b9057543e79 (diff) | |
| download | node-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-- | Makefile | 11 | ||||
| -rw-r--r-- | lib/detectLocalGit.js | 26 | ||||
| -rw-r--r-- | lib/fetchGitData.js | 135 | ||||
| -rw-r--r-- | lib/getOptions.js | 8 | ||||
| -rw-r--r-- | lib/handleInput.js | 6 | ||||
| -rw-r--r-- | test/fetchGitData.js | 6 | ||||
| -rw-r--r-- | test/getOptions.js | 191 |
7 files changed, 302 insertions, 81 deletions
@@ -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 }; +} |
