Skip to content

Commit 580ad9c

Browse files
pvdlggr2m
authored andcommitted
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
1 parent 8e9d9f7 commit 580ad9c

5 files changed

Lines changed: 364 additions & 109 deletions

File tree

src/lib/get-commits.js

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ const getVersionHead = require('./get-version-head');
1414
* 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.
1515
*
1616
* The commit correspoding to the last released version is determined as follow:
17-
* - Use `lastRelease.gitHead` is defined and present in `config.options.branch` history.
18-
* - Search for a tag named `v<version>` or `<version>` and it's associated commit sha if present in `config.options.branch` history.
19-
*
20-
* 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.
17+
* - Use `lastRelease.gitHead` if defined and present in `config.options.branch` history.
18+
* - If `lastRelease.gitHead` is not in the `config.options.branch` history, unshallow the repository and try again.
19+
* - 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.
2120
*
2221
* @param {Object} config
2322
* @param {Object} config.lastRelease The lastRelease object obtained from the getLastRelease plugin.
@@ -34,20 +33,12 @@ const getVersionHead = require('./get-version-head');
3433
module.exports = async ({lastRelease: {version, gitHead}, options: {branch}}) => {
3534
if (gitHead || version) {
3635
try {
37-
gitHead = await getVersionHead(version, branch, gitHead);
38-
} catch (err) {
39-
// Unshallow the repository if the gitHead cannot be found and the branch for the last release version
40-
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
41-
}
42-
43-
// Try to find the gitHead on the branch again with an unshallowed repository
44-
try {
45-
gitHead = await getVersionHead(version, branch, gitHead);
36+
gitHead = await getVersionHead(gitHead, version, branch);
4637
} catch (err) {
4738
if (err.code === 'ENOTINHISTORY') {
48-
log.error('commits', notInHistoryMessage(gitHead, branch, version, err.branches));
49-
} else if (err.code === 'ENOGITHEAD') {
50-
log.error('commits', noGitHeadMessage());
39+
log.error('commits', notInHistoryMessage(err.gitHead, branch, version));
40+
} else {
41+
log.error('commits', noGitHeadMessage(branch, version));
5142
}
5243
throw err;
5344
}
@@ -73,20 +64,24 @@ module.exports = async ({lastRelease: {version, gitHead}, options: {branch}}) =>
7364
}
7465
};
7566

76-
function noGitHeadMessage(version) {
67+
function noGitHeadMessage(branch, version) {
7768
return `The commit the last release of this package was derived from cannot be determined from the release metadata not from the repository tags.
78-
This means semantic-release can not extract the commits between now and then.
79-
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
80-
You can recover from this error by publishing manually.`;
69+
This means semantic-release can not extract the commits between now and then.
70+
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
71+
72+
You can recover from this error by creating a tag for the version "${version}" on the commit corresponding to this release:
73+
$ git tag -f v${version} <commit sha1 corresponding to last release>
74+
$ git push -f --tags origin ${branch}
75+
`;
8176
}
8277

83-
function notInHistoryMessage(gitHead, branch, version, branches) {
78+
function notInHistoryMessage(gitHead, branch, version) {
8479
return `The commit the last release of this package was derived from is not in the direct history of the "${branch}" branch.
85-
This means semantic-release can not extract the commits between now and then.
86-
This is usually caused by force pushing, releasing from an unrelated branch, or using an already existing package name.
87-
You can recover from this error by publishing manually or restoring the commit "${gitHead}".
88-
89-
${branches && branches.length
90-
? `Here is a list of branches that still contain the commit in question: \n * ${branches.join('\n * ')}`
91-
: ''}`;
80+
This means semantic-release can not extract the commits between now and then.
81+
This is usually caused by force pushing, releasing from an unrelated branch, or using an already existing package name.
82+
83+
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:
84+
$ git tag -f v${version || '<version>'} <commit sha1 corresponding to last release>
85+
$ git push -f --tags origin ${branch}
86+
`;
9287
}

src/lib/get-version-head.js

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,51 +17,59 @@ async function gitTagHead(tagName) {
1717
}
1818

1919
/**
20-
* Get the list of branches that contains the given commit.
21-
*
20+
* Verify if the commist `sha` is in the direct history of the current branch.
21+
*
2222
* @param {string} sha The sha of the commit to look for.
2323
*
24-
* @return {Array<string>} The list of branches that contains the commit sha in parameter.
24+
* @return {boolean} `true` if the commit `sha` is in the history of the current branch, `false` otherwise.
2525
*/
26-
async function getCommitBranches(sha) {
27-
try {
28-
return (await execa('git', ['branch', '--no-color', '--contains', sha])).stdout
29-
.split('\n')
30-
.map(branch => branch.replace('*', '').trim())
31-
.filter(branch => !!branch);
32-
} catch (err) {
33-
return [];
34-
}
26+
async function isCommitInHistory(sha) {
27+
return (await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false})).code === 0;
3528
}
3629

3730
/**
38-
* Get the commit sha for a given version, if it is contained in the given branch.
31+
* Get the commit sha for a given version, if it's contained in the given branch.
3932
*
33+
* @param {string} gitHead The commit sha to look for.
4034
* @param {string} version The version corresponding to the commit sha to look for. Used to search in git tags.
41-
* @param {string} branch The branch that must have the commit in its direct history.
42-
* @param {string} gitHead The commit sha to verify.
4335
*
44-
* @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`.
36+
* @return {Promise<string>} A Promise that resolves to the commit sha of the version, either `gitHead` of the commit associated with the `version` tag.
4537
*
46-
* @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.
38+
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `gitHead` or the commit sha dereived from `version` is not in the direct history of `branch`.
4739
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `gitHead` is undefined and no commit sha can be found for the `version`.
4840
*/
49-
module.exports = async (version, branch, gitHead) => {
50-
if (!gitHead && version) {
51-
// Look for the version tag only if no gitHead exists
52-
gitHead = (await gitTagHead(`v${version}`)) || (await gitTagHead(version));
41+
module.exports = async (gitHead, version) => {
42+
// Check if gitHead is defined and exists in release branch
43+
if (gitHead && (await isCommitInHistory(gitHead))) {
44+
return gitHead;
45+
}
46+
47+
// Ushallow the repository
48+
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
49+
50+
// Check if gitHead is defined and exists in release branch again
51+
if (gitHead && (await isCommitInHistory(gitHead))) {
52+
return gitHead;
5353
}
5454

55-
if (gitHead) {
56-
// Retrieve the branches containing the gitHead and verify one of them is the branch in param
57-
const branches = await getCommitBranches(gitHead);
58-
if (!branches.includes(branch)) {
59-
const error = new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY');
60-
error.branches = branches;
61-
throw error;
55+
let tagHead;
56+
if (version) {
57+
// If a version is defined search a corresponding tag
58+
tagHead = (await gitTagHead(`v${version}`)) || (await gitTagHead(version));
59+
60+
// Check if tagHead is found and exists in release branch again
61+
if (tagHead && (await isCommitInHistory(tagHead))) {
62+
return tagHead;
6263
}
63-
} else {
64-
throw new SemanticReleaseError('There is no commit associated with last release', 'ENOGITHEAD');
6564
}
66-
return gitHead;
65+
66+
// Either gitHead is defined or a tagHead has been found but none is in the branch history
67+
if (gitHead || tagHead) {
68+
const error = new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY');
69+
error.gitHead = gitHead || tagHead;
70+
throw error;
71+
}
72+
73+
// There is no gitHead in the last release and there is no tags correponsing to the last release version
74+
throw new SemanticReleaseError('There is no commit associated with last release', 'ENOGITHEAD');
6775
};

0 commit comments

Comments
 (0)