- Allow to run semantic-release (via API) from anywhere passing the current working directory. - Allows to simplify the tests and to run them in parallel in both the core and plugins.
228 lines
8.1 KiB
JavaScript
228 lines
8.1 KiB
JavaScript
import tempy from 'tempy';
|
|
import execa from 'execa';
|
|
import fileUrl from 'file-url';
|
|
import pReduce from 'p-reduce';
|
|
import gitLogParser from 'git-log-parser';
|
|
import getStream from 'get-stream';
|
|
|
|
/**
|
|
* Commit message informations.
|
|
*
|
|
* @typedef {Object} Commit
|
|
* @property {String} branch The commit branch.
|
|
* @property {String} hash The commit hash.
|
|
* @property {String} message The commit message.
|
|
*/
|
|
|
|
/**
|
|
* Create a temporary git repository.
|
|
* If `withRemote` is `true`, creates a bare repository, initialize it and create a shallow clone. Change the current working directory to the clone root.
|
|
* If `withRemote` is `false`, creates a regular repository and initialize it. Change the current working directory to the repository root.
|
|
*
|
|
* @param {Boolean} withRemote `true` to create a shallow clone of a bare repository.
|
|
* @param {String} [branch='master'] The branch to initialize.
|
|
* @return {String} The path of the clone if `withRemote` is `true`, the path of the repository otherwise.
|
|
*/
|
|
export async function gitRepo(withRemote, branch = 'master') {
|
|
let cwd = tempy.directory();
|
|
|
|
await execa('git', ['init'].concat(withRemote ? ['--bare'] : []), {cwd});
|
|
|
|
const repositoryUrl = fileUrl(cwd);
|
|
if (withRemote) {
|
|
await initBareRepo(repositoryUrl, branch);
|
|
cwd = await gitShallowClone(repositoryUrl, branch);
|
|
} else {
|
|
await gitCheckout(branch, true, {cwd});
|
|
}
|
|
|
|
await execa('git', ['config', 'commit.gpgsign', false], {cwd});
|
|
|
|
return {cwd, repositoryUrl};
|
|
}
|
|
|
|
/**
|
|
* Initialize an existing bare repository:
|
|
* - Clone the repository
|
|
* - Change the current working directory to the clone root
|
|
* - Create a default branch
|
|
* - Create an initial commits
|
|
* - Push to origin
|
|
*
|
|
* @param {String} repositoryUrl The URL of the bare repository.
|
|
* @param {String} [branch='master'] the branch to initialize.
|
|
*/
|
|
export async function initBareRepo(repositoryUrl, branch = 'master') {
|
|
const cwd = tempy.directory();
|
|
await execa('git', ['clone', '--no-hardlinks', repositoryUrl, cwd], {cwd});
|
|
await gitCheckout(branch, true, {cwd});
|
|
await gitCommits(['Initial commit'], {cwd});
|
|
await execa('git', ['push', repositoryUrl, branch], {cwd});
|
|
}
|
|
|
|
/**
|
|
* Create commits on the current git repository.
|
|
*
|
|
* @param {Array<string>} messages Commit messages.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
|
|
*/
|
|
export async function gitCommits(messages, execaOpts) {
|
|
await pReduce(messages, async (_, message) =>
|
|
execa.stdout('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)
|
|
);
|
|
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
|
|
}
|
|
|
|
/**
|
|
* Get the list of parsed commits since a git reference.
|
|
*
|
|
* @param {String} [from] Git reference from which to seach commits.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @return {Array<Object>} The list of parsed commits.
|
|
*/
|
|
export async function gitGetCommits(from, execaOpts) {
|
|
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
|
|
return (await getStream.array(
|
|
gitLogParser.parse({_: `${from ? from + '..' : ''}HEAD`}, {...execaOpts, env: {...process.env, ...execaOpts.env}})
|
|
)).map(commit => {
|
|
commit.message = commit.message.trim();
|
|
commit.gitTags = commit.gitTags.trim();
|
|
return commit;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checkout a branch on the current git repository.
|
|
*
|
|
* @param {String} branch Branch name.
|
|
* @param {Boolean} create `true` to create the branch, `false` to checkout an existing branch.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*/
|
|
export async function gitCheckout(branch, create = true, execaOpts) {
|
|
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch], execaOpts);
|
|
}
|
|
|
|
/**
|
|
* Get the HEAD sha.
|
|
*
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @return {String} The sha of the head commit in the current git repository.
|
|
*/
|
|
export async function gitHead(execaOpts) {
|
|
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*/
|
|
export async function gitTagVersion(tagName, sha, execaOpts) {
|
|
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName], execaOpts);
|
|
}
|
|
|
|
/**
|
|
* Create a shallow clone of a git repository and change the current working directory to the cloned repository root.
|
|
* The shallow will contain a limited number of commit and no tags.
|
|
*
|
|
* @param {String} repositoryUrl The path of the repository to clone.
|
|
* @param {String} [branch='master'] the branch to clone.
|
|
* @param {Number} [depth=1] The number of commit to clone.
|
|
* @return {String} The path of the cloned repository.
|
|
*/
|
|
export async function gitShallowClone(repositoryUrl, branch = 'master', depth = 1) {
|
|
const cwd = tempy.directory();
|
|
|
|
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, repositoryUrl, cwd], {
|
|
cwd,
|
|
});
|
|
return cwd;
|
|
}
|
|
|
|
/**
|
|
* 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} repositoryUrl The path of the repository to clone.
|
|
* @param {Number} head A commit sha of the remote repo that will become the detached head of the new one.
|
|
* @return {String} The path of the new repository.
|
|
*/
|
|
export async function gitDetachedHead(repositoryUrl, head) {
|
|
const cwd = tempy.directory();
|
|
|
|
await execa('git', ['init'], {cwd});
|
|
await execa('git', ['remote', 'add', 'origin', repositoryUrl], {cwd});
|
|
await execa('git', ['fetch', repositoryUrl], {cwd});
|
|
await execa('git', ['checkout', head], {cwd});
|
|
return cwd;
|
|
}
|
|
|
|
/**
|
|
* Add a new Git configuration.
|
|
*
|
|
* @param {String} name Config name.
|
|
* @param {String} value Config value.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*/
|
|
export async function gitAddConfig(name, value, execaOpts) {
|
|
await execa('git', ['config', '--add', name, value], execaOpts);
|
|
}
|
|
|
|
/**
|
|
* Get the first commit sha referenced by the tag `tagName` in the local repository.
|
|
*
|
|
* @param {String} tagName Tag name for which to retrieve the commit sha.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @return {String} The sha of the commit associated with `tagName` on the local repository.
|
|
*/
|
|
export async function gitTagHead(tagName, execaOpts) {
|
|
return execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
|
}
|
|
|
|
/**
|
|
* Get the first commit sha referenced by the tag `tagName` in the remote repository.
|
|
*
|
|
* @param {String} repositoryUrl The repository remote URL.
|
|
* @param {String} tagName The tag name to seach for.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @return {String} The sha of the commit associated with `tagName` on the remote repository.
|
|
*/
|
|
export async function gitRemoteTagHead(repositoryUrl, tagName, execaOpts) {
|
|
return (await execa.stdout('git', ['ls-remote', '--tags', repositoryUrl, tagName], execaOpts))
|
|
.split('\n')
|
|
.filter(tag => Boolean(tag))
|
|
.map(tag => tag.match(/^(\S+)/)[1])[0];
|
|
}
|
|
|
|
/**
|
|
* Get the tag associated with a commit sha.
|
|
*
|
|
* @param {String} gitHead The commit sha for which to retrieve the associated tag.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @return {String} The tag associatedwith the sha in parameter or `null`.
|
|
*/
|
|
export async function gitCommitTag(gitHead, execaOpts) {
|
|
return execa.stdout('git', ['describe', '--tags', '--exact-match', gitHead], execaOpts);
|
|
}
|
|
|
|
/**
|
|
* Push to the remote repository.
|
|
*
|
|
* @param {String} repositoryUrl The remote repository URL.
|
|
* @param {String} branch The branch to push.
|
|
* @param {Object} [execaOpts] Options to pass to `execa`.
|
|
*
|
|
* @throws {Error} if the push failed.
|
|
*/
|
|
export async function gitPush(repositoryUrl = 'origin', branch = 'master', execaOpts) {
|
|
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`], execaOpts);
|
|
}
|