feat: Allow to recover from ENOTINHISTORY with a tag and handle detached head repo
- Tag sha will now be used also if there is a gitHead in last release and it's not in the history - Use `git merge-base` to determine if a commit is in history, allowing to use CI creating detached head repo - Mention recovery solution by creating a version tag in `ENOTINHISTORY` and `ENOGITHEAD` error messages - Do not mention branches containing missing commit in `ENOTINHISTORY` and `ENOGITHEAD` error messages as it's not available by default on most CI
This commit is contained in:
parent
8e9d9f77f3
commit
580ad9c3d2
@ -14,10 +14,9 @@ const getVersionHead = require('./get-version-head');
|
||||
* Retrieve the list of commits on the current branch since the last released version, or all the commits of the current branch if there is no last released version.
|
||||
*
|
||||
* The commit correspoding to the last released version is determined as follow:
|
||||
* - Use `lastRelease.gitHead` is defined and present in `config.options.branch` history.
|
||||
* - Search for a tag named `v<version>` or `<version>` and it's associated commit sha if present in `config.options.branch` history.
|
||||
*
|
||||
* If a commit corresponding to the last released is not found, unshallow the repository (as most CI create a shallow clone with limited number of commits and no tags) and try again.
|
||||
* - Use `lastRelease.gitHead` if defined and present in `config.options.branch` history.
|
||||
* - If `lastRelease.gitHead` is not in the `config.options.branch` history, unshallow the repository and try again.
|
||||
* - If `lastRelease.gitHead` is still not in the `config.options.branch` history, search for a tag named `v<version>` or `<version>` and verify if it's associated commit sha is present in `config.options.branch` history.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Object} config.lastRelease The lastRelease object obtained from the getLastRelease plugin.
|
||||
@ -34,20 +33,12 @@ const getVersionHead = require('./get-version-head');
|
||||
module.exports = async ({lastRelease: {version, gitHead}, options: {branch}}) => {
|
||||
if (gitHead || version) {
|
||||
try {
|
||||
gitHead = await getVersionHead(version, branch, gitHead);
|
||||
} catch (err) {
|
||||
// Unshallow the repository if the gitHead cannot be found and the branch for the last release version
|
||||
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
|
||||
}
|
||||
|
||||
// Try to find the gitHead on the branch again with an unshallowed repository
|
||||
try {
|
||||
gitHead = await getVersionHead(version, branch, gitHead);
|
||||
gitHead = await getVersionHead(gitHead, version, branch);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOTINHISTORY') {
|
||||
log.error('commits', notInHistoryMessage(gitHead, branch, version, err.branches));
|
||||
} else if (err.code === 'ENOGITHEAD') {
|
||||
log.error('commits', noGitHeadMessage());
|
||||
log.error('commits', notInHistoryMessage(err.gitHead, branch, version));
|
||||
} else {
|
||||
log.error('commits', noGitHeadMessage(branch, version));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
@ -73,20 +64,24 @@ module.exports = async ({lastRelease: {version, gitHead}, options: {branch}}) =>
|
||||
}
|
||||
};
|
||||
|
||||
function noGitHeadMessage(version) {
|
||||
function noGitHeadMessage(branch, version) {
|
||||
return `The commit the last release of this package was derived from cannot be determined from the release metadata not from the repository tags.
|
||||
This means semantic-release can not extract the commits between now and then.
|
||||
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
|
||||
You can recover from this error by publishing manually.`;
|
||||
This means semantic-release can not extract the commits between now and then.
|
||||
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
|
||||
|
||||
You can recover from this error by creating a tag for the version "${version}" on the commit corresponding to this release:
|
||||
$ git tag -f v${version} <commit sha1 corresponding to last release>
|
||||
$ git push -f --tags origin ${branch}
|
||||
`;
|
||||
}
|
||||
|
||||
function notInHistoryMessage(gitHead, branch, version, branches) {
|
||||
function notInHistoryMessage(gitHead, branch, version) {
|
||||
return `The commit the last release of this package was derived from is not in the direct history of the "${branch}" branch.
|
||||
This means semantic-release can not extract the commits between now and then.
|
||||
This is usually caused by force pushing, releasing from an unrelated branch, or using an already existing package name.
|
||||
You can recover from this error by publishing manually or restoring the commit "${gitHead}".
|
||||
|
||||
${branches && branches.length
|
||||
? `Here is a list of branches that still contain the commit in question: \n * ${branches.join('\n * ')}`
|
||||
: ''}`;
|
||||
This means semantic-release can not extract the commits between now and then.
|
||||
This is usually caused by force pushing, releasing from an unrelated branch, or using an already existing package name.
|
||||
|
||||
You can recover from this error by restoring the commit "${gitHead}" or by creating a tag for the version "${version}" on the commit corresponding to this release:
|
||||
$ git tag -f v${version || '<version>'} <commit sha1 corresponding to last release>
|
||||
$ git push -f --tags origin ${branch}
|
||||
`;
|
||||
}
|
||||
|
@ -17,51 +17,59 @@ async function gitTagHead(tagName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of branches that contains the given commit.
|
||||
*
|
||||
* Verify if the commist `sha` is in the direct history of the current branch.
|
||||
*
|
||||
* @param {string} sha The sha of the commit to look for.
|
||||
*
|
||||
* @return {Array<string>} The list of branches that contains the commit sha in parameter.
|
||||
* @return {boolean} `true` if the commit `sha` is in the history of the current branch, `false` otherwise.
|
||||
*/
|
||||
async function getCommitBranches(sha) {
|
||||
try {
|
||||
return (await execa('git', ['branch', '--no-color', '--contains', sha])).stdout
|
||||
.split('\n')
|
||||
.map(branch => branch.replace('*', '').trim())
|
||||
.filter(branch => !!branch);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
async function isCommitInHistory(sha) {
|
||||
return (await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false})).code === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the commit sha for a given version, if it is contained in the given branch.
|
||||
* Get the commit sha for a given version, if it's contained in the given branch.
|
||||
*
|
||||
* @param {string} gitHead The commit sha to look for.
|
||||
* @param {string} version The version corresponding to the commit sha to look for. Used to search in git tags.
|
||||
* @param {string} branch The branch that must have the commit in its direct history.
|
||||
* @param {string} gitHead The commit sha to verify.
|
||||
*
|
||||
* @return {Promise<string>} A Promise that resolves to `gitHead` if defined and if present in branch direct history or the commit sha corresponding to `version`.
|
||||
* @return {Promise<string>} A Promise that resolves to the commit sha of the version, either `gitHead` of the commit associated with the `version` tag.
|
||||
*
|
||||
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `gitHead` or the commit sha dereived from `version` is not in the direct history of `branch`. The Error will have a `branches` attributes with the list of branches containing the commit.
|
||||
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `gitHead` or the commit sha dereived from `version` is not in the direct history of `branch`.
|
||||
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `gitHead` is undefined and no commit sha can be found for the `version`.
|
||||
*/
|
||||
module.exports = async (version, branch, gitHead) => {
|
||||
if (!gitHead && version) {
|
||||
// Look for the version tag only if no gitHead exists
|
||||
gitHead = (await gitTagHead(`v${version}`)) || (await gitTagHead(version));
|
||||
module.exports = async (gitHead, version) => {
|
||||
// Check if gitHead is defined and exists in release branch
|
||||
if (gitHead && (await isCommitInHistory(gitHead))) {
|
||||
return gitHead;
|
||||
}
|
||||
|
||||
if (gitHead) {
|
||||
// Retrieve the branches containing the gitHead and verify one of them is the branch in param
|
||||
const branches = await getCommitBranches(gitHead);
|
||||
if (!branches.includes(branch)) {
|
||||
const error = new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY');
|
||||
error.branches = branches;
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw new SemanticReleaseError('There is no commit associated with last release', 'ENOGITHEAD');
|
||||
// Ushallow the repository
|
||||
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
|
||||
|
||||
// Check if gitHead is defined and exists in release branch again
|
||||
if (gitHead && (await isCommitInHistory(gitHead))) {
|
||||
return gitHead;
|
||||
}
|
||||
return gitHead;
|
||||
|
||||
let tagHead;
|
||||
if (version) {
|
||||
// If a version is defined search a corresponding tag
|
||||
tagHead = (await gitTagHead(`v${version}`)) || (await gitTagHead(version));
|
||||
|
||||
// Check if tagHead is found and exists in release branch again
|
||||
if (tagHead && (await isCommitInHistory(tagHead))) {
|
||||
return tagHead;
|
||||
}
|
||||
}
|
||||
|
||||
// Either gitHead is defined or a tagHead has been found but none is in the branch history
|
||||
if (gitHead || tagHead) {
|
||||
const error = new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY');
|
||||
error.gitHead = gitHead || tagHead;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// There is no gitHead in the last release and there is no tags correponsing to the last release version
|
||||
throw new SemanticReleaseError('There is no commit associated with last release', 'ENOGITHEAD');
|
||||
};
|
||||
|
@ -1,5 +1,14 @@
|
||||
import test from 'ava';
|
||||
import {gitRepo, gitCommits, gitCheckout, gitTagVersion, gitShallowClone, gitTags, gitLog} from './helpers/git-utils';
|
||||
import {
|
||||
gitRepo,
|
||||
gitCommits,
|
||||
gitCheckout,
|
||||
gitTagVersion,
|
||||
gitShallowClone,
|
||||
gitTags,
|
||||
gitLog,
|
||||
gitDetachedHead,
|
||||
} from './helpers/git-utils';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
@ -25,7 +34,7 @@ test.serial('Get all commits when there is no last release', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(['fix: First fix', 'feat: Second feature']);
|
||||
const commits = await gitCommits(['First', 'Second']);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const result = await getCommits({lastRelease: {}, options: {branch: 'master'}});
|
||||
@ -42,7 +51,7 @@ test.serial('Get all commits when there is no last release, including the ones n
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repo = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(['fix: First fix', 'feat: Second feature']);
|
||||
const commits = await gitCommits(['First', 'Second']);
|
||||
// Create a shallow clone with only 1 commit
|
||||
await gitShallowClone(repo);
|
||||
|
||||
@ -64,9 +73,9 @@ test.serial('Get all commits since gitHead (from lastRelease)', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(['fix: First fix', 'feat: Second feature', 'feat: Third feature']);
|
||||
const commits = await gitCommits(['First', 'Second', 'Third']);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
// Retrieve the commits with the commits module, since commit 'First'
|
||||
const result = await getCommits({
|
||||
lastRelease: {gitHead: commits[commits.length - 1].hash},
|
||||
options: {branch: 'master'},
|
||||
@ -80,18 +89,38 @@ test.serial('Get all commits since gitHead (from lastRelease)', async t => {
|
||||
t.is(result[1].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Get all commits since gitHead (from lastRelease) on a detached head repo', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repo = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(['First', 'Second', 'Third']);
|
||||
// Create a detached head repo at commit 'feat: Second'
|
||||
await gitDetachedHead(repo, commits[1].hash);
|
||||
|
||||
// Retrieve the commits with the commits module, since commit 'First'
|
||||
const result = await getCommits({
|
||||
lastRelease: {gitHead: commits[commits.length - 1].hash},
|
||||
options: {branch: 'master'},
|
||||
});
|
||||
|
||||
// Verify the module retrieved only the commit 'feat: Second' (included in the detached and after 'fix: First')
|
||||
t.is(result.length, 1);
|
||||
t.is(result[0].hash.substring(0, 7), commits[1].hash);
|
||||
t.is(result[0].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Get all commits since gitHead (from tag) ', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
let commits = await gitCommits(['fix: First fix']);
|
||||
let commits = await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('1.0.0');
|
||||
// Add new commits to the master branch
|
||||
commits = (await gitCommits(['feat: Second feature', 'feat: Third feature'])).concat(commits);
|
||||
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const result = await getCommits({lastRelease: {version: `1.0.0`}, options: {branch: 'master'}});
|
||||
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
|
||||
const result = await getCommits({lastRelease: {version: '1.0.0'}, options: {branch: 'master'}});
|
||||
|
||||
// Verify the commits created and retrieved by the module are identical
|
||||
t.is(result.length, 2);
|
||||
@ -101,18 +130,82 @@ test.serial('Get all commits since gitHead (from tag) ', async t => {
|
||||
t.is(result[1].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Get all commits since gitHead (from tag) on a detached head repo', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repo = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
let commits = await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('1.0.0');
|
||||
// Add new commits to the master branch
|
||||
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
|
||||
// Create a detached head repo at commit 'feat: Second'
|
||||
await gitDetachedHead(repo, commits[1].hash);
|
||||
|
||||
// Retrieve the commits with the commits module, since commit 'First' (associated with tag 1.0.0)
|
||||
const result = await getCommits({lastRelease: {version: '1.0.0'}, options: {branch: 'master'}});
|
||||
|
||||
// Verify the module retrieved only the commit 'feat: Second' (included in the detached and after 'fix: First')
|
||||
t.is(result.length, 1);
|
||||
t.is(result[0].hash.substring(0, 7), commits[1].hash);
|
||||
t.is(result[0].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Get all commits since gitHead (from tag formatted like v<version>) ', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
let commits = await gitCommits(['fix: First fix']);
|
||||
let commits = await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
commits = (await gitCommits(['feat: Second feature', 'feat: Third feature'])).concat(commits);
|
||||
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const result = await getCommits({lastRelease: {version: `1.0.0`}, options: {branch: 'master'}});
|
||||
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
|
||||
const result = await getCommits({lastRelease: {version: '1.0.0'}, options: {branch: 'master'}});
|
||||
|
||||
// Verify the commits created and retrieved by the module are identical
|
||||
t.is(result.length, 2);
|
||||
t.is(result[0].hash.substring(0, 7), commits[0].hash);
|
||||
t.is(result[0].message, commits[0].message);
|
||||
t.is(result[1].hash.substring(0, 7), commits[1].hash);
|
||||
t.is(result[1].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Get commits when last release gitHead is missing but a tag match the version', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
let commits = await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
|
||||
|
||||
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
|
||||
const result = await getCommits({lastRelease: {version: '1.0.0', gitHead: 'missing'}, options: {branch: 'master'}});
|
||||
|
||||
// Verify the commits created and retrieved by the module are identical
|
||||
t.is(result.length, 2);
|
||||
t.is(result[0].hash.substring(0, 7), commits[0].hash);
|
||||
t.is(result[0].message, commits[0].message);
|
||||
t.is(result[1].hash.substring(0, 7), commits[1].hash);
|
||||
t.is(result[1].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Get all commits since gitHead, when gitHead are mising from the shallow clone', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repo = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(['First', 'Second', 'Third']);
|
||||
// Create a shallow clone with only 1 commit and no tags
|
||||
await gitShallowClone(repo);
|
||||
|
||||
// Retrieve the commits with the commits module, since commit 'First'
|
||||
const result = await getCommits({
|
||||
lastRelease: {version: '1.0.0', gitHead: commits[commits.length - 1].hash},
|
||||
options: {branch: 'master'},
|
||||
});
|
||||
|
||||
// Verify the commits created and retrieved by the module are identical
|
||||
t.is(result.length, 2);
|
||||
@ -126,19 +219,19 @@ test.serial('Get all commits since gitHead from tag, when tags are mising from t
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repo = await gitRepo();
|
||||
// Add commits to the master branch
|
||||
let commits = await gitCommits(['fix: First fix']);
|
||||
let commits = await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
commits = (await gitCommits(['feat: Second feature', 'feat: Third feature'])).concat(commits);
|
||||
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
|
||||
// Create a shallow clone with only 1 commit and no tags
|
||||
await gitShallowClone(repo);
|
||||
|
||||
// Verify the shallow clone does not contains any tags
|
||||
t.is((await gitTags()).length, 0);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const result = await getCommits({lastRelease: {version: `1.0.0`}, options: {branch: 'master'}});
|
||||
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
|
||||
const result = await getCommits({lastRelease: {version: '1.0.0'}, options: {branch: 'master'}});
|
||||
|
||||
// Verify the commits created and retrieved by the module are identical
|
||||
t.is(result.length, 2);
|
||||
@ -148,20 +241,20 @@ test.serial('Get all commits since gitHead from tag, when tags are mising from t
|
||||
t.is(result[1].message, commits[1].message);
|
||||
});
|
||||
|
||||
test.serial('Return empty array if there is no commits', async t => {
|
||||
test.serial('Return empty array if lastRelease.gitHead is the last commit', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
const commits = await gitCommits(['fix: First fix', 'feat: Second feature']);
|
||||
const commits = await gitCommits(['First', 'Second']);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
// Retrieve the commits with the commits module, since commit 'Second' (therefore none)
|
||||
const result = await getCommits({lastRelease: {gitHead: commits[0].hash}, options: {branch: 'master'}});
|
||||
|
||||
// Verify no commit is retrieved
|
||||
t.deepEqual(result, []);
|
||||
});
|
||||
|
||||
test.serial('Return empty array if lastRelease.gitHead is the last commit', async t => {
|
||||
test.serial('Return empty array if there is no commits', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
|
||||
@ -176,7 +269,7 @@ test.serial('Throws ENOGITHEAD error if the gitHead of the last release cannot b
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['fix: First fix', 'feat: Second feature']);
|
||||
await gitCommits(['First', 'Second']);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const error = await t.throws(getCommits({lastRelease: {version: '1.0.0'}, options: {branch: 'master'}}));
|
||||
@ -195,7 +288,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in history', async t =
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['fix: First fix', 'feat: Second feature']);
|
||||
await gitCommits(['First', 'Second']);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const error = await t.throws(getCommits({lastRelease: {gitHead: 'notinhistory'}, options: {branch: 'master'}}));
|
||||
@ -218,8 +311,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in branch history but
|
||||
await gitCheckout('other-branch');
|
||||
// Add commits to the 'other-branch' branch
|
||||
const commitsBranch = await gitCommits(['Third', 'Fourth']);
|
||||
// Create the new branch 'another-branch' from 'other-branch'
|
||||
await gitCheckout('another-branch');
|
||||
await gitCheckout('master', false);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const error = await t.throws(
|
||||
@ -233,6 +325,59 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in branch history but
|
||||
t.regex(errorLog.firstCall.args[1], /history of the "master" branch/);
|
||||
// Verify the log function has been called with a message mentionning the missing gitHead
|
||||
t.regex(errorLog.firstCall.args[1], new RegExp(`restoring the commit "${commitsBranch[0].hash}"`));
|
||||
// Verify the log function has been called with a message mentionning the branches that contains the gitHead
|
||||
t.regex(errorLog.firstCall.args[1], /\* another-branch\s+\* other-branch/);
|
||||
});
|
||||
|
||||
test.serial('Throws ENOTINHISTORY error if gitHead is not in detached head but present in other branch', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repo = await gitRepo();
|
||||
// Add commit to the master branch
|
||||
await gitCommits(['First']);
|
||||
// Create the new branch 'other-branch' from master
|
||||
await gitCheckout('other-branch');
|
||||
// Add commits to the 'other-branch' branch
|
||||
const commitsBranch = await gitCommits(['Second', 'Third']);
|
||||
await gitCheckout('master', false);
|
||||
// Add new commit to master branch
|
||||
const commitsMaster = await gitCommits(['Fourth']);
|
||||
// Create a detached head repo at commit 'Fourth'
|
||||
await gitDetachedHead(repo, commitsMaster[0].hash);
|
||||
|
||||
// Retrieve the commits with the commits module, since commit 'Second'
|
||||
const error = await t.throws(
|
||||
getCommits({lastRelease: {version: '1.0.1', gitHead: commitsBranch[0].hash}, options: {branch: 'master'}})
|
||||
);
|
||||
|
||||
// Verify error code and message
|
||||
t.is(error.code, 'ENOTINHISTORY');
|
||||
t.true(error instanceof SemanticReleaseError);
|
||||
// Verify the log function has been called with a message mentionning the branch
|
||||
t.regex(errorLog.firstCall.args[1], /history of the "master" branch/);
|
||||
// Verify the log function has been called with a message mentionning the missing gitHead
|
||||
t.regex(errorLog.firstCall.args[1], new RegExp(`restoring the commit "${commitsBranch[0].hash}"`));
|
||||
});
|
||||
|
||||
test.serial('Throws ENOTINHISTORY error when a tag is not in branch history but present in others', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First', 'Second']);
|
||||
// Create the new branch 'other-branch' from master
|
||||
await gitCheckout('other-branch');
|
||||
// Add commits to the 'other-branch' branch
|
||||
await gitCommits(['Third']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
const shaTag = await gitTagVersion('v1.0.0');
|
||||
await gitCheckout('master', false);
|
||||
// Add new commit to the master branch
|
||||
await gitCommits(['Forth']);
|
||||
|
||||
// Retrieve the commits with the commits module
|
||||
const error = await t.throws(getCommits({lastRelease: {version: '1.0.0'}, options: {branch: 'master'}}));
|
||||
// Verify error code and message
|
||||
t.is(error.code, 'ENOTINHISTORY');
|
||||
t.true(error instanceof SemanticReleaseError);
|
||||
// Verify the log function has been called with a message mentionning the branch
|
||||
t.regex(errorLog.firstCall.args[1], /history of the "master" branch/);
|
||||
// Verify the log function has been called with a message mentionning the missing gitHead
|
||||
t.regex(errorLog.firstCall.args[1], new RegExp(`restoring the commit "${shaTag}"`));
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ export async function gitRepo() {
|
||||
*
|
||||
* @param {Array<string>} messages commit messages.
|
||||
*
|
||||
* @returns {Array<Commit>} commits the created commits, in reverse order (to match `git log` order).
|
||||
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
|
||||
*/
|
||||
export async function gitCommits(messages) {
|
||||
return (await pMapSeries(messages, async msg => {
|
||||
@ -45,6 +45,19 @@ export async function gitCommits(messages) {
|
||||
})).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Amend a commit (rewriting the sha) on the current git repository.
|
||||
*
|
||||
* @param {string} messages commit message.
|
||||
*
|
||||
* @returns {Array<Commit>} the created commits.
|
||||
*/
|
||||
export async function gitAmmendCommit(msg) {
|
||||
const {stdout} = await execa('git', ['commit', '--amend', '-m', msg, '--allow-empty']);
|
||||
const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)(.|\s)+$/.exec(stdout);
|
||||
return {branch, hash, message};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout a branch on the current git repository.
|
||||
*
|
||||
@ -52,7 +65,7 @@ export async function gitCommits(messages) {
|
||||
* @param {boolean} create `true` to create the branche ans switch, `false` to only switch.
|
||||
*/
|
||||
export async function gitCheckout(branch, create = true) {
|
||||
await execa('git', ['checkout', create ? '-b' : null, branch]);
|
||||
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,9 +79,13 @@ export async function gitHead() {
|
||||
* Create a tag on the head commit in the current git repository.
|
||||
*
|
||||
* @param {string} tagName The tag name to create.
|
||||
* @param {string} [sha] The commit on which to create the tag. If undefined the tag is created on the last commit.
|
||||
*
|
||||
* @return {string} The commit sha of the created tag.
|
||||
*/
|
||||
export async function gitTagVersion(tagName) {
|
||||
await execa('git', ['tag', tagName]);
|
||||
export async function gitTagVersion(tagName, sha) {
|
||||
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName]);
|
||||
return (await execa('git', ['rev-list', '-1', '--tags', tagName])).stdout;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,11 +110,29 @@ export async function gitLog() {
|
||||
* @param {number} [depth=1] The number of commit to clone.
|
||||
* @return {string} The path of the cloned repository.
|
||||
*/
|
||||
export async function gitShallowClone(origin, depth = 1) {
|
||||
export async function gitShallowClone(origin, branch = 'master', depth = 1) {
|
||||
const dir = tempy.directory();
|
||||
|
||||
process.chdir(dir);
|
||||
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '--depth', depth, `file://${origin}`, dir]);
|
||||
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, `file://${origin}`, dir]);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a git repo with a detached head from another git repository and change the current working directory to the new repository root.
|
||||
*
|
||||
* @param {string} origin The path of the repository to clone.
|
||||
* @param {number} head A commit sha of the origin repo that will become the detached head of the new one.
|
||||
* @return {string} The path of the new repository.
|
||||
*/
|
||||
export async function gitDetachedHead(origin, head) {
|
||||
const dir = tempy.directory();
|
||||
|
||||
process.chdir(dir);
|
||||
await execa('git', ['init']);
|
||||
await execa('git', ['remote', 'add', 'origin', origin]);
|
||||
await execa('git', ['fetch']);
|
||||
await execa('git', ['checkout', head]);
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,18 @@
|
||||
import test from 'ava';
|
||||
import {writeJson, readJson} from 'fs-extra';
|
||||
import {start, stop, uri} from './helpers/registry';
|
||||
import {gitRepo, gitCommits, gitHead, gitTagVersion, gitPackRefs} from './helpers/git-utils';
|
||||
import {gitRepo, gitCommits, gitHead, gitTagVersion, gitPackRefs, gitAmmendCommit} from './helpers/git-utils';
|
||||
import execa from 'execa';
|
||||
|
||||
// Environment variables used with cli
|
||||
const env = {
|
||||
CI: true,
|
||||
npm_config_registry: uri,
|
||||
GH_TOKEN: 'github_token',
|
||||
NPM_OLD_TOKEN: 'aW50ZWdyYXRpb246c3VjaHNlY3VyZQ==',
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
};
|
||||
|
||||
test.before(async t => {
|
||||
// Start the local NPM registry
|
||||
await start();
|
||||
@ -25,14 +34,6 @@ test.after.always(async t => {
|
||||
});
|
||||
|
||||
test.serial('Release patch, minor and major versions', async t => {
|
||||
// Environment variables used with cli
|
||||
const env = {
|
||||
CI: true,
|
||||
npm_config_registry: uri,
|
||||
GH_TOKEN: 'github_token',
|
||||
NPM_OLD_TOKEN: 'aW50ZWdyYXRpb246c3VjaHNlY3VyZQ==',
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
};
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
t.log('Create git repository');
|
||||
await gitRepo();
|
||||
@ -146,14 +147,6 @@ test.serial('Release patch, minor and major versions', async t => {
|
||||
});
|
||||
|
||||
test.serial('Release versions from a packed git repository, using tags to determine last release gitHead', async t => {
|
||||
// Environment variables used with cli
|
||||
const env = {
|
||||
CI: true,
|
||||
npm_config_registry: uri,
|
||||
GH_TOKEN: 'github_token',
|
||||
NPM_OLD_TOKEN: 'aW50ZWdyYXRpb246c3VjaHNlY3VyZQ==',
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
};
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
t.log('Create git repository');
|
||||
await gitRepo();
|
||||
@ -230,3 +223,82 @@ test.serial('Exit with 1 in a plugin is not found', async t => {
|
||||
const {code} = await t.throws(execa(require.resolve('../bin/semantic-release'), ['pre'], {env}));
|
||||
t.is(code, 1);
|
||||
});
|
||||
|
||||
test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
t.log('Create git repository');
|
||||
await gitRepo();
|
||||
|
||||
// Create package.json in repository root
|
||||
await writeJson('./package.json', {
|
||||
name: 'test-module-4',
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: 'git+https://github.com/semantic-release/test-module-2'},
|
||||
release: {verifyConditions: require.resolve('../src/lib/plugin-noop')},
|
||||
});
|
||||
|
||||
/** Minor release **/
|
||||
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit']);
|
||||
t.log('$ semantic-release pre');
|
||||
let {stdout, stderr, code} = await execa(require.resolve('../bin/semantic-release'), ['pre'], {env});
|
||||
// Verify package.json has been updated
|
||||
t.is((await readJson('./package.json')).version, '1.0.0');
|
||||
t.log('$ npm publish');
|
||||
({stdout, code} = await execa('npm', ['publish'], {env}));
|
||||
// Verify output of npm publish
|
||||
t.regex(stdout, /test-module-4@1.0.0/);
|
||||
t.is(code, 0);
|
||||
// Retrieve the published package from the registry and check version and gitHead
|
||||
let [, version, releaseGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
|
||||
(await execa('npm', ['show', 'test-module-4', 'version', 'gitHead'], {env})).stdout
|
||||
);
|
||||
const head = await gitHead();
|
||||
t.is(releaseGitHead, head);
|
||||
t.log(`+ released ${version}`);
|
||||
t.is(version, '1.0.0');
|
||||
// Create a tag version so the tag can be used later to determine the commit associated with the version
|
||||
await gitTagVersion('v1.0.0');
|
||||
t.log('Create git tag v1.0.0');
|
||||
|
||||
/** Rewrite sha of commit used for release **/
|
||||
|
||||
t.log('Amend release commit');
|
||||
const {hash} = await gitAmmendCommit('feat: Initial commit');
|
||||
|
||||
/** Patch release **/
|
||||
|
||||
t.log('Commit a fix');
|
||||
await gitCommits(['fix: bar']);
|
||||
t.log('$ semantic-release pre');
|
||||
({stderr, stdout, code} = await execa(require.resolve('../bin/semantic-release'), ['pre'], {env, reject: false}));
|
||||
|
||||
t.log('Fail with "ENOTINHISTORY" error');
|
||||
t.is(code, 1);
|
||||
t.regex(
|
||||
stderr,
|
||||
new RegExp(
|
||||
`You can recover from this error by restoring the commit "${head}" or by creating a tag for the version "1.0.0" on the commit corresponding to this release`
|
||||
)
|
||||
);
|
||||
|
||||
/** Create a tag to recover and redo release **/
|
||||
|
||||
t.log('Create git tag v1.0.0 to recover');
|
||||
await gitTagVersion('v1.0.0', hash);
|
||||
|
||||
t.log('$ semantic-release pre');
|
||||
({stderr, stdout, code} = await execa(require.resolve('../bin/semantic-release'), ['pre'], {env}));
|
||||
// Verify package.json has been updated
|
||||
t.is((await readJson('./package.json')).version, '1.0.1');
|
||||
t.log('$ npm publish');
|
||||
({stdout, code} = await execa('npm', ['publish'], {env}));
|
||||
// Verify output of npm publish
|
||||
t.regex(stdout, /test-module-4@1.0.1/);
|
||||
t.is(code, 0);
|
||||
// Retrieve the published package from the registry and check version and gitHead
|
||||
version = (await execa('npm', ['show', 'test-module-4', 'version'], {env})).stdout;
|
||||
t.is(version, '1.0.1');
|
||||
t.log(`+ released ${version}`);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user