From 6b5b02ea755b74e1c2ea9a2dfff6576f5f15e870 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Tue, 14 Jan 2020 18:22:39 -0500 Subject: [PATCH] fix: fetch tags on repo cached by the CI --- lib/git.js | 17 ++++++++++++----- test/git.test.js | 31 ++++++++++++++++++++++++++++++- test/helpers/git-utils.js | 10 ++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/git.js b/lib/git.js index dc75018a..7a2400ca 100644 --- a/lib/git.js +++ b/lib/git.js @@ -86,15 +86,22 @@ async function isRefExists(ref, execaOpts) { } /** - * Unshallow the git repository if necessary and fetch all the tags. + * 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, execaOpts) { - const isLocalExists = - (await execa('git', ['rev-parse', '--verify', branch], {...execaOpts, reject: false})).exitCode === 0; + const isDetachedHead = + (await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {...execaOpts, reject: false})).stdout === 'HEAD'; try { await execa( @@ -103,7 +110,7 @@ async function fetch(repositoryUrl, branch, ciBranch, execaOpts) { 'fetch', '--unshallow', '--tags', - ...(branch === ciBranch && isLocalExists + ...(branch === ciBranch && !isDetachedHead ? [repositoryUrl] : ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), ], @@ -115,7 +122,7 @@ async function fetch(repositoryUrl, branch, ciBranch, execaOpts) { [ 'fetch', '--tags', - ...(branch === ciBranch && isLocalExists + ...(branch === ciBranch && !isDetachedHead ? [repositoryUrl] : ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), ], diff --git a/test/git.test.js b/test/git.test.js index 781994f6..5640dcdc 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -32,6 +32,7 @@ import { gitDetachedHeadFromBranch, gitAddNote, gitGetNote, + gitFetch, } from './helpers/git-utils'; test('Get the last commit sha', async t => { @@ -99,7 +100,7 @@ test('Fetch all tags on a detached head repository', async t => { t.deepEqual((await getTags('master', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort()); }); -test('Fetch all tags on a repository with a detached head from branch', async t => { +test('Fetch all tags on a repository with a detached head from branch (CircleCI)', async t => { let {cwd, repositoryUrl} = await gitRepo(); await gitCommits(['First'], {cwd}); @@ -124,6 +125,34 @@ test('Fetch all tags on a repository with a detached head from branch', async t t.deepEqual((await getTags('master', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0', 'v2.0.0'].sort()); }); +test('Fetch all tags on a detached head repository with outdated cached repo (GitLab CI)', async t => { + const {cwd, repositoryUrl} = await gitRepo(); + + await gitCommits(['First'], {cwd}); + await gitTagVersion('v1.0.0', undefined, {cwd}); + await gitCommits(['Second'], {cwd}); + await gitTagVersion('v1.0.1', undefined, {cwd}); + let [commit] = await gitCommits(['Third'], {cwd}); + await gitTagVersion('v1.1.0', undefined, {cwd}); + await gitPush(repositoryUrl, 'master', {cwd}); + + // Create a clone (as first CI run would) + const cloneCwd = await gitShallowClone(repositoryUrl); + await gitFetch(repositoryUrl, {cwd: cloneCwd}); + await gitCheckout(commit.hash, false, {cwd: cloneCwd}); + + // Push tag to remote + [commit] = await gitCommits(['Fourth'], {cwd}); + await gitTagVersion('v1.2.0', undefined, {cwd}); + await gitPush(repositoryUrl, 'master', {cwd}); + + // Fetch on the cached repo and make detached head, leaving master outdated + await fetch(repositoryUrl, 'master', 'master', {cwd: cloneCwd}); + await gitCheckout(commit.hash, false, {cwd: cloneCwd}); + + t.deepEqual((await getTags('master', {cwd: cloneCwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0', 'v1.2.0'].sort()); +}); + test('Verify if a branch exists', async t => { // Create a git repository, set the current working directory at the root of the repo const {cwd} = await gitRepo(); diff --git a/test/helpers/git-utils.js b/test/helpers/git-utils.js index c474801b..40a08dd1 100644 --- a/test/helpers/git-utils.js +++ b/test/helpers/git-utils.js @@ -109,6 +109,16 @@ export async function gitCheckout(branch, create, execaOpts) { await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch], execaOpts); } +/** + * Fetch current git repository. + * + * @param {String} repositoryUrl The repository remote URL. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +export async function gitFetch(repositoryUrl, execaOpts) { + await execa('git', ['fetch', repositoryUrl], execaOpts); +} + /** * Get the HEAD sha. *