Skip to content

Commit

Permalink
feat(spire): Do not use git binary and enable installing git hooks again
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Git hooks are now installed again.
  • Loading branch information
danez committed Jul 28, 2020
1 parent bebde71 commit 67cb751
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 76 deletions.
9 changes: 4 additions & 5 deletions packages/spire/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,10 @@ dependencies it replaces. At this point you're done!

### Using without Git

At the moment Spire has a hard dependency on a Git repository to resolve
monorepo project root. This most likely to be reviewed in the future, but
meanwhile it is part of the core, you can export `SKIP_PREFLIGHT_CHECK=true`
environment variable to disable this check. The idea and name of this flag is
borrowed from [create-react-app].
You do not need to have git installed to run spire itself. Be aware though that
certain plugins might need to have git installed to work. For example the
semantic-release and lerna-release plugin obviously need git because they create
tags and commits.

### Using in monorepos

Expand Down
8 changes: 2 additions & 6 deletions packages/spire/lib/create-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const {
readFile,
writeFile,
} = require('fs-extra');
const execa = require('execa');
const detectIndent = require('detect-indent');
const gitRemoteOriginUrl = require('git-remote-origin-url');

function createCore({ cwd, logger }, { setState, getState }) {
async function hasFile(file) {
Expand Down Expand Up @@ -37,11 +37,7 @@ function createCore({ cwd, logger }, { setState, getState }) {
async function getProvider() {
let remoteUrl;
try {
remoteUrl = (
await execa('git', ['remote', 'get-url', '--push', 'origin'], {
cwd,
})
).stdout;
remoteUrl = await gitRemoteOriginUrl(cwd);
} catch (error) {
logger.warn(
'Unable to determine git provider. Using "none" instead.',
Expand Down
121 changes: 56 additions & 65 deletions packages/spire/lib/plugins/git.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const isCI = require('is-ci');
const { join, dirname, relative } = require('path');
const execa = require('execa');
const { pathExists, readFile, outputFile, chmod, remove } = require('fs-extra');
const SpireError = require('spire/error');
const SpireError = require('../../error');
const getGitRoot = require('../utils/getGitRoot');

const SPIRE_COMMENT_MARK = '# spire';

Expand All @@ -15,87 +15,78 @@ ${command}
}

function git(
{ getCommand, setState, getState },
{ setState, getState },
{
gitHooks = {
'pre-commit': '<spireBin> hook precommit',
'post-merge': '<spireBin> hook postmerge',
},
}
) {
function shouldSkip(logger) {
if (isCI) {
logger.info('Skipped processing git hooks on CI');
return true;
}
const { root } = getState();
if (!root) {
logger.warn(
'Cannot process git hooks because root directory not detected'
);
return true;
}

return false;
}

return {
name: 'spire-git-support',
async setup({ cwd, env }) {
try {
setState({
root: (
await execa('git', ['rev-parse', '--show-toplevel'], {
cwd,
})
).stdout,
});
} catch (reason) {
if (env.SKIP_PREFLIGHT_CHECK) {
setState({ root: cwd });
} else {
async setup({ cwd }) {
setState({
root: await getGitRoot(cwd),
});
},
async postinstall({ logger, resolve }) {
if (shouldSkip(logger)) {
return;
}
const { root: gitRoot } = getState();
const spireDir = dirname(resolve('spire/package.json'));
const spireBin = join(relative(gitRoot, spireDir), 'bin/spire.js');
logger.debug('Using spire bin for hooks: %s', spireBin);
for (const hook of Object.keys(gitHooks)) {
const hookPath = join(gitRoot, '.git/hooks', hook);
if (await pathExists(hookPath)) {
const hookContents = await readFile(hookPath, 'utf8');
if (hookContents.includes(SPIRE_COMMENT_MARK)) {
continue;
}
throw new SpireError(
[
'Project is not in a Git repository.',
'Set `SKIP_PREFLIGHT_CHECK=true` to disable this check,',
'but be advised that some plugins may fail.\n',
'Caused by:\n',
reason.message,
`Git ${hook} hook is already installed.`,
"Make sure you're not using husky or any other similar tools.",
'To continue, remove `.git/hooks/` folder and try again.',
].join(' ')
);
}
const hookCommand = gitHooks[hook].replace(/<spireBin>/g, spireBin);
await outputFile(hookPath, hookToString(hookCommand), 'utf8');
await chmod(hookPath, '744'); // a+x
}
},
async skip({ logger }) {
if (isCI) {
logger.debug('Skipping installing hooks on CI');
return true;
async preuninstall({ logger }) {
if (shouldSkip(logger)) {
return;
}
},
async run({ logger, resolve }) {
const gitRoot = getState().root;
const spireDir = dirname(resolve('spire/package.json'));
const spireBin = join(relative(gitRoot, spireDir), 'bin/spire.js');
logger.debug('Using spire bin for hooks: %s', spireBin);
switch (getCommand()) {
case Symbol.for('postinstall'):
for (const hook of Object.keys(gitHooks)) {
const hookPath = join(gitRoot, '.git/hooks', hook);
if (await pathExists(hookPath)) {
const hookContents = await readFile(hookPath, 'utf8');
if (hookContents.includes(SPIRE_COMMENT_MARK)) {
continue;
}
throw new SpireError(
[
`Git ${hook} hook is already installed.`,
"Make sure you're not using husky or any other similar tools.",
'To continue, remove `.git/hooks/` folder and try again.',
].join(' ')
);
}
const hookCommand = gitHooks[hook].replace(/<spireBin>/g, spireBin);
await outputFile(hookPath, hookToString(hookCommand), 'utf8');
await chmod(hookPath, '744'); // a+x
}
break;
case Symbol.for('preuninstall'):
for (const hook of Object.keys(gitHooks)) {
const hookPath = join(gitRoot, '.git/hooks', hook);
if (await pathExists(hookPath)) {
const hookContents = await readFile(hookPath, 'utf8');
if (hookContents.includes(SPIRE_COMMENT_MARK)) {
await remove(hookPath);
}
}
const { root: gitRoot } = getState();
for (const hook of Object.keys(gitHooks)) {
const hookPath = join(gitRoot, '.git/hooks', hook);
if (await pathExists(hookPath)) {
const hookContents = await readFile(hookPath, 'utf8');
if (hookContents.includes(SPIRE_COMMENT_MARK)) {
await remove(hookPath);
}
break;
default:
return;
}
}
},
};
Expand Down
25 changes: 25 additions & 0 deletions packages/spire/lib/utils/__tests__/getGitRoot.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { createFixture } = require('spire-test-utils');
const { join } = require('path');
const getGitRoot = require('../getGitRoot');

describe('getGitRoot', () => {
it('resolves root directory if .git directory exists', async () => {
const fixture = await createFixture(
{
'.git/config': `[remote "origin"]
url = https://github.com/researchgate/spire.git`,
},
{ git: false }
);
const subdir = join(fixture.cwd, 'some/other/sub/folder');
expect(await getGitRoot(subdir)).toBe(fixture.cwd);
await fixture.clean();
});

it('does not resolve root directory if .git directory does not exists', async () => {
const fixture = await createFixture({}, { git: false });
const subdir = join(fixture.cwd, 'some/other/sub/folder');
expect(await getGitRoot(subdir)).toBeNull();
await fixture.clean();
});
});
19 changes: 19 additions & 0 deletions packages/spire/lib/utils/getGitRoot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { resolve, dirname } = require('path');
const { pathExists } = require('fs-extra');

async function getGitRoot(dir) {
var gitFolder = resolve(dir, '.git');
const exists = await pathExists(gitFolder);
if (exists) {
return dir;
}
const newDir = dirname(dir);
// Reached top
if (dir === newDir) {
return null;
}

return getGitRoot(newDir);
}

module.exports = getGitRoot;
1 change: 1 addition & 0 deletions packages/spire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"cosmiconfig": "^6.0.0",
"detect-indent": "^6.0.0",
"fs-extra": "^9.0.0",
"git-remote-origin-url": "^3.1.0",
"import-from": "^3.0.0",
"invariant": "^2.2.4",
"pretty-format": "^26.0.0",
Expand Down
14 changes: 14 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4216,6 +4216,13 @@ git-remote-origin-url@^2.0.0:
gitconfiglocal "^1.0.0"
pify "^2.3.0"

git-remote-origin-url@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-3.1.0.tgz#c90c1cb0f66658566bbc900509ab093a1522d2b3"
integrity sha512-yVSfaTMO7Bqk6Xx3696ufNfjdrajX7Ig9GuAeO2V3Ji7stkDoBNFldnWIAsy0qviUd0Z+X2P6ziJENKztW7cBQ==
dependencies:
gitconfiglocal "^2.1.0"

git-semver-tags@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-2.0.3.tgz#48988a718acf593800f99622a952a77c405bfa34"
Expand Down Expand Up @@ -4246,6 +4253,13 @@ gitconfiglocal@^1.0.0:
dependencies:
ini "^1.3.2"

gitconfiglocal@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz#07c28685c55cc5338b27b5acbcfe34aeb92e43d1"
integrity sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==
dependencies:
ini "^1.3.2"

glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
Expand Down

0 comments on commit 67cb751

Please sign in to comment.