Created
July 24, 2020 18:19
-
-
Save lucianmachado/810bc35daa17a65cba8a0b09189b7335 to your computer and use it in GitHub Desktop.
git utils javascript
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
const gitLogParser = require('git-log-parser'); | |
const getStream = require('get-stream'); | |
const execa = require('execa'); | |
const {GIT_NOTE_REF} = "" | |
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}}); | |
/** | |
* Get the commit sha for a given tag. | |
* | |
* @param {String} tagName Tag name for which to retrieve the commit sha. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {String} The commit sha of the tag in parameter or `null`. | |
*/ | |
async function getTagHead(tagName, execaOptions) { | |
return (await execa('git', ['rev-list', '-1', tagName], execaOptions)).stdout; | |
} | |
/** | |
* Get all the tags for a given branch. | |
* | |
* @param {String} branch The branch for which to retrieve the tags. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Array<String>} List of git tags. | |
* @throws {Error} If the `git` command fails. | |
*/ | |
async function getTags(branch, execaOptions) { | |
return (await execa('git', ['tag', '--merged', branch], execaOptions)).stdout | |
.split('\n') | |
.map((tag) => tag.trim()) | |
.filter(Boolean); | |
} | |
/** | |
* Retrieve a range of commits. | |
* | |
* @param {String} from to includes all commits made after this sha (does not include this sha). | |
* @param {String} to to includes all commits made before this sha (also include this sha). | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* @return {Promise<Array<Object>>} The list of commits between `from` and `to`. | |
*/ | |
async function getCommits(from, to, execaOptions) { | |
return ( | |
await getStream.array( | |
gitLogParser.parse( | |
{_: `${from ? from + '..' : ''}${to}`}, | |
{cwd: execaOptions.cwd, env: {...process.env, ...execaOptions.env}} | |
) | |
) | |
).map(({message, gitTags, ...commit}) => ({...commit, message: message.trim(), gitTags: gitTags.trim()})); | |
} | |
/** | |
* Get all the repository branches. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Array<String>} List of git branches. | |
* @throws {Error} If the `git` command fails. | |
*/ | |
async function getBranches(repositoryUrl, execaOptions) { | |
return (await execa('git', ['ls-remote', '--heads', repositoryUrl], execaOptions)).stdout | |
.split('\n') | |
.filter(Boolean) | |
.map((branch) => branch.match(/^.+refs\/heads\/(?<branch>.+)$/)[1]); | |
} | |
/** | |
* Verify if the `ref` exits | |
* | |
* @param {String} ref The reference to verify. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Boolean} `true` if the reference exists, falsy otherwise. | |
*/ | |
async function isRefExists(ref, execaOptions) { | |
try { | |
return (await execa('git', ['rev-parse', '--verify', ref], execaOptions)).exitCode === 0; | |
} catch (error) { | |
log.error(error) | |
} | |
} | |
/** | |
* Fetch all the tags from a branch. Unshallow if necessary. | |
* This will update the local branch from the latest on the remote if: | |
* - The branch is not the one that triggered the CI | |
* - The CI created a detached head | |
* | |
* Otherwise it just calls `git fetch` without specifying the `refspec` option to avoid overwritting the head commit set by the CI. | |
* | |
* The goal is to retrieve the informations on all the release branches without "disturbing" the CI, leaving the trigger branch or the detached head intact. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {String} branch The repository branch to fetch. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
*/ | |
async function fetch(repositoryUrl, branch, ciBranch, execaOptions) { | |
const isDetachedHead = | |
(await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {...execaOptions, reject: false})).stdout === 'HEAD'; | |
try { | |
await execa( | |
'git', | |
[ | |
'fetch', | |
'--unshallow', | |
'--tags', | |
...(branch === ciBranch && !isDetachedHead | |
? [repositoryUrl] | |
: ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), | |
], | |
execaOptions | |
); | |
} catch (_) { | |
await execa( | |
'git', | |
[ | |
'fetch', | |
'--tags', | |
...(branch === ciBranch && !isDetachedHead | |
? [repositoryUrl] | |
: ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), | |
], | |
execaOptions | |
); | |
} | |
} | |
/** | |
* Unshallow the git repository if necessary and fetch all the notes. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
*/ | |
async function fetchNotes(repositoryUrl, execaOptions) { | |
try { | |
await execa( | |
'git', | |
['fetch', '--unshallow', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`], | |
execaOptions | |
); | |
} catch (_) { | |
await execa('git', ['fetch', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`], { | |
...execaOptions, | |
reject: false, | |
}); | |
} | |
} | |
/** | |
* Get the HEAD sha. | |
* | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {String} the sha of the HEAD commit. | |
*/ | |
async function getGitHead(execaOptions) { | |
return (await execa('git', ['rev-parse', 'HEAD'], execaOptions)).stdout; | |
} | |
/** | |
* Get the repository remote URL. | |
* | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {string} The value of the remote git URL. | |
*/ | |
async function repoUrl(execaOptions) { | |
try { | |
return (await execa('git', ['config', '--get', 'remote.origin.url'], execaOptions)).stdout; | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
/** | |
* Test if the current working directory is a Git repository. | |
* | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Boolean} `true` if the current working directory is in a git repository, falsy otherwise. | |
*/ | |
async function isGitRepo(execaOptions) { | |
try { | |
return (await execa('git', ['rev-parse', '--git-dir'], execaOptions)).exitCode === 0; | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
/** | |
* Verify the write access authorization to remote repository with push dry-run. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {String} branch The repository branch for which to verify write access. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @throws {Error} if not authorized to push. | |
*/ | |
async function verifyAuth(repositoryUrl, branch, execaOptions) { | |
try { | |
await execa('git', ['push', '--dry-run', '--no-verify', repositoryUrl, `HEAD:${branch}`], execaOptions); | |
} catch (error) { | |
console.error(error); | |
throw error; | |
} | |
} | |
/** | |
* Tag the commit head on the local repository. | |
* | |
* @param {String} tagName The name of the tag. | |
* @param {String} ref The Git reference to tag. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @throws {Error} if the tag creation failed. | |
*/ | |
async function tag(tagName, ref, execaOptions) { | |
await execa('git', ['tag', tagName, ref], execaOptions); | |
} | |
/** | |
* Push to the remote repository. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @throws {Error} if the push failed. | |
*/ | |
async function push(repositoryUrl, execaOptions) { | |
await execa('git', ['push', '--tags', repositoryUrl], execaOptions); | |
} | |
/** | |
* Push notes to the remote repository. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @throws {Error} if the push failed. | |
*/ | |
async function pushNotes(repositoryUrl, execaOptions) { | |
await execa('git', ['push', repositoryUrl, `refs/notes/${GIT_NOTE_REF}`], execaOptions); | |
} | |
/** | |
* Verify a tag name is a valid Git reference. | |
* | |
* @param {String} tagName the tag name to verify. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Boolean} `true` if valid, falsy otherwise. | |
*/ | |
async function verifyTagName(tagName, execaOptions) { | |
try { | |
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOptions)).exitCode === 0; | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
/** | |
* Verify a branch name is a valid Git reference. | |
* | |
* @param {String} branch the branch name to verify. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Boolean} `true` if valid, falsy otherwise. | |
*/ | |
async function verifyBranchName(branch, execaOptions) { | |
try { | |
return (await execa('git', ['check-ref-format', `refs/heads/${branch}`], execaOptions)).exitCode === 0; | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
/** | |
* Verify the local branch is up to date with the remote one. | |
* | |
* @param {String} repositoryUrl The remote repository URL. | |
* @param {String} branch The repository branch for which to verify status. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, falsy otherwise. | |
*/ | |
async function isBranchUpToDate(repositoryUrl, branch, execaOptions) { | |
return ( | |
(await getGitHead(execaOptions)) === | |
(await execa('git', ['ls-remote', '--heads', repositoryUrl, branch], execaOptions)).stdout.match(/^(?<ref>\w+)?/)[1] | |
); | |
} | |
/** | |
* Get and parse the JSON note of a given reference. | |
* | |
* @param {String} ref The Git reference for which to retrieve the note. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
* | |
* @return {Object} the parsed JSON note if there is one, an empty object otherwise. | |
*/ | |
async function getNote(ref, execaOptions) { | |
try { | |
return JSON.parse((await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'show', ref], execaOptions)).stdout); | |
} catch (error) { | |
if (error.exitCode === 1) { | |
return {}; | |
} | |
console.error(error); | |
throw error; | |
} | |
} | |
/** | |
* Get and parse the JSON note of a given reference. | |
* | |
* @param {Object} note The object to save in the reference note. | |
* @param {String} ref The Git reference to add the note to. | |
* @param {Object} [execaOpts] Options to pass to `execa`. | |
*/ | |
async function addNote(note, ref, execaOptions) { | |
await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-f', '-m', JSON.stringify(note), ref], execaOptions); | |
} | |
module.exports = { | |
getTagHead, | |
getTags, | |
getCommits, | |
getBranches, | |
isRefExists, | |
fetch, | |
fetchNotes, | |
getGitHead, | |
repoUrl, | |
isGitRepo, | |
verifyAuth, | |
tag, | |
push, | |
pushNotes, | |
verifyTagName, | |
isBranchUpToDate, | |
verifyBranchName, | |
getNote, | |
addNote, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment