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:
committed by
Gregor Martynus
parent
8e9d9f77f3
commit
580ad9c3d2
+23
-28
@@ -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}
|
||||
`;
|
||||
}
|
||||
|
||||
+40
-32
@@ -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');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user