-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Retrieve version gitHead from git tags and unshallow the repo i…
…f necessary Add several fixes and improvements in the identification of the last release gitHead: - If there is no last release, unshallow the repo in order to retrieve all existing commits - If git head is not present in last release, try to retrieve it from git tag with format ‘v\<version\>’ or ‘\<version\>’ - If the last release git head cannot be determined and found in commit history, unshallow the repo and try again - Throw a ENOGITHEAD error if the gitHead for the last release cannot be found in the npm metadata nor in the git tags, preventing to make release based on the all the commits in the repo as before - Add integration test for the scenario with a packed repo from which `npm republish` fails to read the git head Fix #447, Fix #393, Fix #280, Fix #276
- Loading branch information
Showing
6 changed files
with
390 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,56 +1,92 @@ | ||
const execa = require('execa'); | ||
const log = require('npmlog'); | ||
const SemanticReleaseError = require('@semantic-release/error'); | ||
const getVersionHead = require('./get-version-head'); | ||
|
||
module.exports = async ({lastRelease, options}) => { | ||
let stdout; | ||
if (lastRelease.gitHead) { | ||
/** | ||
* Commit message. | ||
* | ||
* @typedef {Object} Commit | ||
* @property {string} hash The commit hash. | ||
* @property {string} message The commit message. | ||
*/ | ||
|
||
/** | ||
* 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. | ||
* | ||
* @param {Object} config | ||
* @param {Object} config.lastRelease The lastRelease object obtained from the getLastRelease plugin. | ||
* @param {string} [config.lastRelease.version] The version number of the last release. | ||
* @param {string} [config.lastRelease.gitHead] The commit sha used to make the last release. | ||
* @param {Object} config.options The semantic-relese options. | ||
* @param {string} config.options.branch The branch to release from. | ||
* | ||
* @return {Promise<Array<Commit>>} The list of commits on the branch `config.options.branch` since the last release. | ||
* | ||
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `config.lastRelease.gitHead` or the commit sha derived from `config.lastRelease.version` is not in the direct history of `config.options.branch`. | ||
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `config.lastRelease.gitHead` is undefined and no commit sha can be found for the `config.lastRelease.version`. | ||
*/ | ||
module.exports = async ({lastRelease: {version, gitHead}, options: {branch}}) => { | ||
if (gitHead || version) { | ||
try { | ||
({stdout} = await execa('git', ['branch', '--no-color', '--contains', lastRelease.gitHead])); | ||
gitHead = await getVersionHead(version, branch, gitHead); | ||
} catch (err) { | ||
throw notInHistoryError(lastRelease.gitHead, options.branch); | ||
// 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}); | ||
} | ||
const branches = stdout | ||
.split('\n') | ||
.map(branch => branch.replace('*', '').trim()) | ||
.filter(branch => !!branch); | ||
|
||
if (!branches.includes(options.branch)) { | ||
throw notInHistoryError(lastRelease.gitHead, options.branch, branches); | ||
// Try to find the gitHead on the branch again with an unshallowed repository | ||
try { | ||
gitHead = await getVersionHead(version, branch, gitHead); | ||
} catch (err) { | ||
if (err.code === 'ENOTINHISTORY') { | ||
log.error('commits', notInHistoryMessage(gitHead, branch, version, err.branches)); | ||
} else if (err.code === 'ENOGITHEAD') { | ||
log.error('commits', noGitHeadMessage()); | ||
} | ||
throw err; | ||
} | ||
} else { | ||
// If there is no gitHead nor a version, there is no previous release. Unshallow the repo in order to retrieve all commits | ||
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false}); | ||
} | ||
|
||
try { | ||
({stdout} = await execa('git', [ | ||
return (await execa('git', [ | ||
'log', | ||
'--format=%H==SPLIT==%B==END==', | ||
`${lastRelease.gitHead ? lastRelease.gitHead + '..' : ''}HEAD`, | ||
])); | ||
'--format=format:%H==SPLIT==%B==END==', | ||
`${gitHead ? gitHead + '..' : ''}HEAD`, | ||
])).stdout | ||
.split('==END==') | ||
.filter(raw => !!raw.trim()) | ||
.map(raw => { | ||
const [hash, message] = raw.trim().split('==SPLIT=='); | ||
return {hash, message}; | ||
}); | ||
} catch (err) { | ||
return []; | ||
} | ||
|
||
return String(stdout) | ||
.split('==END==') | ||
.filter(raw => !!raw.trim()) | ||
.map(raw => { | ||
const [hash, message] = raw.trim().split('==SPLIT=='); | ||
return {hash, message}; | ||
}); | ||
}; | ||
|
||
function notInHistoryError(gitHead, branch, branches) { | ||
log.error( | ||
'commits', | ||
` | ||
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 | ||
? `\nHere is a list of branches that still contain the commit in question: \n * ${branches.join('\n * ')}` | ||
: ''} | ||
` | ||
); | ||
return new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY'); | ||
function noGitHeadMessage(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.`; | ||
} | ||
|
||
function notInHistoryMessage(gitHead, branch, version, branches) { | ||
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
const SemanticReleaseError = require('@semantic-release/error'); | ||
const execa = require('execa'); | ||
|
||
/** | ||
* Get the commit sha for a given tag. | ||
* | ||
* @param {string} tagName Tag name for which to retrieve the commit sha. | ||
* | ||
* @return {string} The commit sha of the tag in parameter or `null`. | ||
*/ | ||
async function gitTagHead(tagName) { | ||
try { | ||
return (await execa('git', ['rev-list', '-1', '--tags', tagName])).stdout; | ||
} catch (err) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Get the list of branches that contains the given commit. | ||
* | ||
* @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. | ||
*/ | ||
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 []; | ||
} | ||
} | ||
|
||
/** | ||
* Get the commit sha for a given version, if it is contained in the given branch. | ||
* | ||
* @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`. | ||
* | ||
* @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 `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)); | ||
} | ||
|
||
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'); | ||
} | ||
return gitHead; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.