diff --git a/index.js b/index.js index 29a79f7d6d..8696d8b254 100644 --- a/index.js +++ b/index.js @@ -53,7 +53,7 @@ async function run(context, plugins) { await verify(context); options.repositoryUrl = await getGitAuthUrl(context); - context.branches = await getBranches(options.repositoryUrl, context); + context.branches = await getBranches(options.repositoryUrl, ciBranch, context); context.branch = context.branches.find(({name}) => name === ciBranch); if (!context.branch) { diff --git a/lib/branches/index.js b/lib/branches/index.js index b3781f0905..b94e76f23b 100644 --- a/lib/branches/index.js +++ b/lib/branches/index.js @@ -8,7 +8,7 @@ const expand = require('./expand'); const getTags = require('./get-tags'); const normalize = require('./normalize'); -module.exports = async (repositoryUrl, context) => { +module.exports = async (repositoryUrl, ciBranch, context) => { const {cwd, env} = context; const remoteBranches = await expand( @@ -18,7 +18,7 @@ module.exports = async (repositoryUrl, context) => { ); await pEachSeries(remoteBranches, async ({name}) => { - await fetch(repositoryUrl, name, {cwd, env}); + await fetch(repositoryUrl, name, ciBranch, {cwd, env}); }); await fetchNotes(repositoryUrl, {cwd, env}); diff --git a/lib/git.js b/lib/git.js index 0c2d7836a6..efaecadba2 100644 --- a/lib/git.js +++ b/lib/git.js @@ -119,7 +119,7 @@ async function isRefExists(ref, execaOpts) { * @param {String} branch The repository branch to fetch. * @param {Object} [execaOpts] Options to pass to `execa`. */ -async function fetch(repositoryUrl, branch, execaOpts) { +async function fetch(repositoryUrl, branch, ciBranch, execaOpts) { const isLocalExists = (await execa('git', ['rev-parse', '--verify', branch], {...execaOpts, reject: false})).exitCode === 0; @@ -130,7 +130,9 @@ async function fetch(repositoryUrl, branch, execaOpts) { 'fetch', '--unshallow', '--tags', - ...(isLocalExists ? [repositoryUrl] : [repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), + ...(branch === ciBranch && isLocalExists + ? [repositoryUrl] + : ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), ], execaOpts ); @@ -140,7 +142,9 @@ async function fetch(repositoryUrl, branch, execaOpts) { [ 'fetch', '--tags', - ...(isLocalExists ? [repositoryUrl] : [repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), + ...(branch === ciBranch && isLocalExists + ? [repositoryUrl] + : ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]), ], execaOpts ); diff --git a/test/branches/branches.test.js b/test/branches/branches.test.js index 76ed0dd10b..a658aa2bd0 100644 --- a/test/branches/branches.test.js +++ b/test/branches/branches.test.js @@ -24,7 +24,10 @@ test('Enforce ranges with branching release workflow', async t => { ]; const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []}); - let result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + let result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, '1.0.x').range, '>=1.0.0 <1.0.0', 'Cannot release on 1.0.x before a releasing on master'); t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.0', 'Cannot release on 1.x before a releasing on master'); t.is(getBranch(result, 'master').range, '>=1.0.0 <1.1.0', 'Can release only patch on master'); @@ -32,43 +35,64 @@ test('Enforce ranges with branching release workflow', async t => { t.is(getBranch(result, 'next-major').range, '>=2.0.0', 'Can release only major on next-major'); release(branches, 'master', '1.0.0'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, '1.0.x').range, '>=1.0.0 <1.0.0', 'Cannot release on 1.0.x before a releasing on master'); t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.0', 'Cannot release on 1.x before a releasing on master'); t.is(getBranch(result, 'master').range, '>=1.0.0 <1.1.0', 'Can release only patch on master'); release(branches, 'master', '1.0.1'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master'); merge(branches, 'master', 'next'); merge(branches, 'master', 'next-major'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master'); t.is(getBranch(result, 'next').range, '>=1.1.0 <2.0.0', 'Can release only minor on next'); t.is(getBranch(result, 'next-major').range, '>=2.0.0', 'Can release only major on next-major'); release(branches, 'next', '1.1.0'); release(branches, 'next', '1.1.1'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'next').range, '>=1.1.1 <2.0.0', 'Can release only patch or minor, > than 1.1.0 on next'); release(branches, 'next-major', '2.0.0'); release(branches, 'next-major', '2.0.1'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major'); merge(branches, 'next-major', 'beta'); release(branches, 'beta', '3.0.0-beta.1'); merge(branches, 'beta', 'alpha'); release(branches, 'alpha', '4.0.0-alpha.1'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major'); merge(branches, 'master', '1.0.x'); merge(branches, 'master', '1.x'); release(branches, 'master', '1.0.1'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master'); t.is( getBranch(result, '1.0.x').range, @@ -80,7 +104,10 @@ test('Enforce ranges with branching release workflow', async t => { release(branches, 'master', '1.0.2'); release(branches, 'master', '1.0.3'); release(branches, 'master', '1.0.4'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.0.4 <1.1.0', 'Can release only patch, > than 1.0.4 on master'); t.is( getBranch(result, '1.0.x').range, @@ -90,7 +117,10 @@ test('Enforce ranges with branching release workflow', async t => { t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.2', 'Cannot release on 1.x before >= 1.2.0 is released on master'); merge(branches, 'next', 'master'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master'); t.is(getBranch(result, 'next').range, '>=1.2.0 <2.0.0', 'Can release only patch or minor, > than 1.2.0 on next'); t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major'); @@ -102,34 +132,49 @@ test('Enforce ranges with branching release workflow', async t => { t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.2', 'Cannot release on 1.x before >= 2.0.0 is released on master'); merge(branches, 'master', '1.0.x', '1.0.4'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master'); t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range'); t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.1.0', 'Cannot release on 1.x before >= 2.0.0 is released on master'); merge(branches, 'master', '1.x'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master'); t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range'); t.is(getBranch(result, '1.x').range, '>=1.1.1 <1.1.1', 'Cannot release on 1.x before >= 2.0.0 is released on master'); merge(branches, 'next-major', 'next'); merge(branches, 'next', 'master'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=2.0.1 <2.1.0', 'Can release only patch, > than 2.0.1 on master'); t.is(getBranch(result, 'next').range, '>=2.1.0 <3.0.0', 'Can release only minor on next'); t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only major on next-major'); t.is(getBranch(result, '1.x').range, '>=1.1.1 <2.0.0', 'Can release on 1.x only within range'); merge(branches, 'beta', 'master'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, 'master').range, '>=2.0.1 <2.1.0', 'Can release only patch, > than 2.0.1 on master'); t.is(getBranch(result, 'next').range, '>=2.1.0 <3.0.0', 'Can release only minor on next'); t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only major on next-major'); branches.push({name: '1.1.x', tags: []}); merge(branches, '1.x', '1.1.x'); - result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range})); + result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({ + name, + range, + })); t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range'); t.is(getBranch(result, '1.1.x').range, '>=1.1.1 <1.2.0', 'Can release on 1.1.x only within range'); t.is(getBranch(result, '1.x').range, '>=1.2.0 <2.0.0', 'Can release on 1.x only within range'); @@ -146,7 +191,7 @@ test('Throw SemanticReleaseError for invalid configurations', async t => { {name: 'preview', prerelease: 'alpha', tags: []}, ]; const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []}); - const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', {options: {branches}})))]; + const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', 'master', {options: {branches}})))]; t.is(errors[0].name, 'SemanticReleaseError'); t.is(errors[0].code, 'EMAINTENANCEBRANCH'); @@ -177,7 +222,7 @@ test('Throw a SemanticReleaseError if there is duplicate branches', async t => { ]; const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []}); - const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', {options: {branches}})))]; + const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', 'master', {options: {branches}})))]; t.is(errors[0].name, 'SemanticReleaseError'); t.is(errors[0].code, 'EDUPLICATEBRANCHES'); @@ -192,7 +237,7 @@ test('Throw a SemanticReleaseError for each invalid branch name', async t => { ]; const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []}); - const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', {options: {branches}})))]; + const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', 'master', {options: {branches}})))]; t.is(errors[0].name, 'SemanticReleaseError'); t.is(errors[0].code, 'EINVALIDBRANCHNAME'); diff --git a/test/git.test.js b/test/git.test.js index 9df6d03cfe..eb320d1a38 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -30,6 +30,7 @@ import { gitRemoteTagHead, gitPush, gitDetachedHead, + gitDetachedHeadFromBranch, gitAddNote, gitGetNote, } from './helpers/git-utils'; @@ -63,7 +64,7 @@ test('Unshallow and fetch repository', async t => { // Verify the shallow clone contains only one commit t.is((await gitGetCommits(undefined, {cwd})).length, 1); - await fetch(repositoryUrl, 'master', {cwd}); + await fetch(repositoryUrl, 'master', 'master', {cwd}); // Verify the shallow clone contains all the commits t.is((await gitGetCommits(undefined, {cwd})).length, 2); @@ -78,8 +79,8 @@ test('Do not throw error when unshallow a complete repository', async t => { await gitCommits(['Second'], {cwd}); await gitPush(repositoryUrl, 'second-branch', {cwd}); - await t.notThrowsAsync(fetch(repositoryUrl, 'master', {cwd})); - await t.notThrowsAsync(fetch(repositoryUrl, 'second-branch', {cwd})); + await t.notThrowsAsync(fetch(repositoryUrl, 'master', 'master', {cwd})); + await t.notThrowsAsync(fetch(repositoryUrl, 'second-branch', 'master', {cwd})); }); test('Fetch all tags on a detached head repository', async t => { @@ -94,11 +95,36 @@ test('Fetch all tags on a detached head repository', async t => { await gitPush(repositoryUrl, 'master', {cwd}); cwd = await gitDetachedHead(repositoryUrl, commit.hash); - await fetch(repositoryUrl, 'master', {cwd}); + await fetch(repositoryUrl, 'master', 'master', {cwd}); 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 => { + let {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}); + const [commit] = await gitCommits(['Third'], {cwd}); + await gitTagVersion('v1.1.0', undefined, {cwd}); + await gitPush(repositoryUrl, 'master', {cwd}); + await gitCheckout('other-branch', true, {cwd}); + await gitPush(repositoryUrl, 'other-branch', {cwd}); + await gitCheckout('master', false, {cwd}); + await gitCommits(['Fourth'], {cwd}); + await gitTagVersion('v2.0.0', undefined, {cwd}); + await gitPush(repositoryUrl, 'master', {cwd}); + cwd = await gitDetachedHeadFromBranch(repositoryUrl, 'other-branch', commit.hash); + + await fetch(repositoryUrl, 'master', 'other-branch', {cwd}); + await fetch(repositoryUrl, 'other-branch', 'other-branch', {cwd}); + + t.deepEqual((await getTags('other-branch', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort()); + t.deepEqual((await getTags('master', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0', 'v2.0.0'].sort()); +}); + test('Verify if the commit `sha` is in the direct history of the current branch', 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 b5ea65b34e..2dfa4ba289 100644 --- a/test/helpers/git-utils.js +++ b/test/helpers/git-utils.js @@ -166,6 +166,17 @@ export async function gitDetachedHead(repositoryUrl, head) { return cwd; } +export async function gitDetachedHeadFromBranch(repositoryUrl, branch, head) { + const cwd = tempy.directory(); + + await execa('git', ['init'], {cwd}); + await execa('git', ['remote', 'add', 'origin', repositoryUrl], {cwd}); + await execa('git', ['fetch', '--force', repositoryUrl, `${branch}:remotes/origin/${branch}`], {cwd}); + await execa('git', ['reset', '--hard', head], {cwd}); + await execa('git', ['checkout', '-q', '-B', branch], {cwd}); + return cwd; +} + /** * Add a new Git configuration. *