diff --git a/src/git.js b/src/git.js index b335c22c..c116f46e 100644 --- a/src/git.js +++ b/src/git.js @@ -20,8 +20,17 @@ async function git(args, options) { let { cwd, cached, + shouldUseExitCode, } = options; + /** + * Git is inverted. A zero exit code is true. + * @param {number} exitCode + */ + function toBool(exitCode) { + return exitCode === 0; + } + let cacheKey; let lockFilePath; @@ -59,9 +68,17 @@ async function git(args, options) { if (_cache !== null) { debug('Git cache hit.'); - cache[cacheKey] = _cache; + let normalized = (() => { + if (shouldUseExitCode) { + return _cache === 'true'; + } + + return _cache; + })(); + + cache[cacheKey] = normalized; - return _cache; + return normalized; } } @@ -70,21 +87,34 @@ async function git(args, options) { debug(args, options); - let { stdout } = await execa('git', args, { + let { + stdout, + all, + exitCode, + } = await execa('git', args, { cwd, + reject: !shouldUseExitCode, }); + debug(all); + + let result = (() => { + if (shouldUseExitCode) { + return toBool(exitCode); + } + + return stdout; + })(); + if (cached) { - cache[cacheKey] = stdout; + cache[cacheKey] = result; if (cached !== true) { - await ensureWriteFile(cachedFilePath, stdout); + await ensureWriteFile(cachedFilePath, result.toString()); } } - debug(stdout); - - return stdout; + return result; } finally { if (lockFilePath) { await unlock(lockFilePath); @@ -117,16 +147,10 @@ function getLinesFromOutput(output) { } async function isCommitAncestorOf(ancestorCommit, descendantCommit, options) { - try { - await git(['merge-base', '--is-ancestor', ancestorCommit, descendantCommit], options); - } catch (err) { - let missingCommit = 128; - if (![1, missingCommit].includes(err.exitCode)) { - throw err; - } - return false; - } - return true; + return await git(['merge-base', '--is-ancestor', ancestorCommit, descendantCommit], { + shouldUseExitCode: true, + ...options, + }); } async function getCommonAncestor(commit1, commit2, options) { @@ -164,6 +188,7 @@ async function getCurrentCommit(cwd) { } module.exports = { + getCacheKey, git, getCurrentBranch, getWorkspaceCwd, diff --git a/test/git-test.js b/test/git-test.js index d9971209..fc52d7c6 100644 --- a/test/git-test.js +++ b/test/git-test.js @@ -2,17 +2,17 @@ const { describe, it, setUpTmpDir } = require('./helpers/mocha'); const { expect } = require('./helpers/chai'); -const { git } = require('../src/git'); +const { git, getCacheKey } = require('../src/git'); const execa = require('execa'); const { gitInit } = require('git-fixtures'); const path = require('path'); const fs = require('fs'); describe(function() { - let tmpPath; + let cwd; beforeEach(async function() { - tmpPath = await gitInit({ + cwd = await gitInit({ defaultBranchName: 'master', }); }); @@ -22,24 +22,99 @@ describe(function() { describe('in memory', function () { it('works', async function() { let oldSha = await git(['rev-parse', 'HEAD'], { - cwd: tmpPath, + cwd, cached: true, }); - await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd: tmpPath }); + await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd }); let cachedSha = await git(['rev-parse', 'HEAD'], { - cwd: tmpPath, + cwd, cached: true, }); let newSha = await execa('git', ['rev-parse', 'HEAD'], { - cwd: tmpPath, + cwd, }); expect(cachedSha).to.equal(oldSha); expect(newSha).to.not.equal(oldSha); }); + + describe('exit code commands', function () { + const shouldUseExitCode = true; + + it('true', async function() { + let oldSha = await git(['rev-parse', 'HEAD'], { + cwd, + }); + + await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd }); + + let newSha = await execa('git', ['rev-parse', 'HEAD'], { + cwd, + }); + + let cachedArgs = ['merge-base', '--is-ancestor', oldSha, newSha.stdout]; + + let result = await git(cachedArgs, { + cwd, + cached: true, + shouldUseExitCode, + }); + + expect(result).to.equal(true); + + await execa('git', ['reset', '--hard', oldSha], { cwd }); + await execa('git', ['reflog', 'expire', '--expire=now', '--all'], { cwd }); + await execa('git', ['gc', '--prune=now'], { cwd }); + + result = await git(cachedArgs, { + cwd, + cached: true, + shouldUseExitCode, + }); + + expect(result).to.equal(true); + + result = await git(cachedArgs, { + cwd, + shouldUseExitCode, + }); + + expect(result).to.equal(false); + + }); + + it('false', async function() { + let cachedArgs = ['rev-parse', 'non-existent-branch']; + + let result = await git(cachedArgs, { + cwd, + cached: true, + shouldUseExitCode, + }); + + expect(result).to.equal(false); + + await execa('git', ['branch', 'non-existent-branch'], { cwd }); + + result = await git(cachedArgs, { + cwd, + cached: true, + shouldUseExitCode, + }); + + expect(result).to.equal(false); + + result = await git(cachedArgs, { + cwd, + shouldUseExitCode, + }); + + expect(result).to.equal(true); + }); + }); }); describe('on disk', function () { @@ -48,19 +123,19 @@ describe(function() { it('works', async function() { let oldSha = await git(['rev-parse', 'HEAD'], { - cwd: tmpPath, + cwd, cached: this.tmpPath, }); - await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd: tmpPath }); + await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd }); let cachedSha = await git(['rev-parse', 'HEAD'], { - cwd: tmpPath, + cwd, cached: this.tmpPath, }); let newSha = await execa('git', ['rev-parse', 'HEAD'], { - cwd: tmpPath, + cwd, }); expect(cachedSha).to.equal(oldSha); @@ -68,9 +143,82 @@ describe(function() { let [cachedFilePath] = await fs.promises.readdir(this.tmpPath); - expect(cachedFilePath).to.not.be.undefined; + expect(cachedFilePath).to.equal(getCacheKey(['rev-parse', 'HEAD'], cwd)); expect(path.join(this.tmpPath, cachedFilePath)).to.be.a.file().with.content(oldSha); }); + + describe('exit code commands', function () { + const shouldUseExitCode = true; + + it('true', async function() { + let oldSha = await git(['rev-parse', 'HEAD'], { + cwd, + }); + + await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd }); + + let newSha = await execa('git', ['rev-parse', 'HEAD'], { + cwd, + }); + + let args = ['merge-base', '--is-ancestor', oldSha, newSha.stdout]; + + await git(args, { + cwd, + cached: this.tmpPath, + shouldUseExitCode, + }); + + let [cachedFilePath] = await fs.promises.readdir(this.tmpPath); + + expect(cachedFilePath).to.equal(getCacheKey(args, cwd)); + expect(path.join(this.tmpPath, cachedFilePath)).to.be.a.file().with.content('true'); + }); + + it('false', async function() { + let oldSha = await git(['rev-parse', 'HEAD'], { + cwd, + }); + + await execa('git', ['commit', '-m', 'test', '--allow-empty'], { cwd }); + + let newSha = await execa('git', ['rev-parse', 'HEAD'], { + cwd, + }); + + let args = ['merge-base', '--is-ancestor', newSha.stdout, oldSha]; + + await git(args, { + cwd, + cached: this.tmpPath, + shouldUseExitCode, + }); + + let [cachedFilePath] = await fs.promises.readdir(this.tmpPath); + + expect(cachedFilePath).to.equal(getCacheKey(args, cwd)); + expect(path.join(this.tmpPath, cachedFilePath)).to.be.a.file().with.content('false'); + }); + + it('error', async function() { + let sha = await git(['rev-parse', 'HEAD'], { + cwd, + }); + + let args = ['merge-base', '--is-ancestor', 'missing-commit', sha]; + + await git(args, { + cwd, + cached: this.tmpPath, + shouldUseExitCode, + }); + + let [cachedFilePath] = await fs.promises.readdir(this.tmpPath); + + expect(cachedFilePath).to.equal(getCacheKey(args, cwd)); + expect(path.join(this.tmpPath, cachedFilePath)).to.be.a.file().with.content('false'); + }); + }); }); }); });