diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts index 1ba2f7e..2454600 100644 --- a/__tests__/git.test.ts +++ b/__tests__/git.test.ts @@ -34,6 +34,9 @@ describe('context', () => { case 'git show --format=%H HEAD --quiet --': result = 'test-sha'; break; + case 'git branch --show-current': + result = 'test'; + break; case 'git symbolic-ref HEAD': result = 'refs/heads/test'; break; @@ -90,17 +93,76 @@ describe('remoteURL', () => { }); describe('ref', () => { - it('have been called', async () => { - const execSpy = jest.spyOn(Exec, 'getExecOutput'); - try { - await Git.ref(); - } catch (err) { - // noop - } - expect(execSpy).toHaveBeenCalledWith(`git`, ['symbolic-ref', 'HEAD'], { - silent: true, - ignoreReturnCode: true + it('returns mocked ref', async () => { + jest.spyOn(Exec, 'getExecOutput').mockImplementation((cmd, args): Promise => { + const fullCmd = `${cmd} ${args?.join(' ')}`; + let result = ''; + switch (fullCmd) { + case 'git branch --show-current': + result = 'test'; + break; + case 'git symbolic-ref HEAD': + result = 'refs/heads/test'; + break; + } + return Promise.resolve({ + stdout: result, + stderr: '', + exitCode: 0 + }); }); + + const ref = await Git.ref(); + + expect(ref).toEqual('refs/heads/test'); + }); + + it('returns mocked detached tag ref', async () => { + jest.spyOn(Exec, 'getExecOutput').mockImplementation((cmd, args): Promise => { + const fullCmd = `${cmd} ${args?.join(' ')}`; + let result = ''; + switch (fullCmd) { + case 'git branch --show-current': + result = ''; + break; + case 'git show -s --pretty=%D': + result = 'HEAD, tag: 8.0.0'; + break; + } + return Promise.resolve({ + stdout: result, + stderr: '', + exitCode: 0 + }); + }); + + const ref = await Git.ref(); + + expect(ref).toEqual('refs/tags/8.0.0'); + }); + + it('returns mocked detached branch ref', async () => { + jest.spyOn(Exec, 'getExecOutput').mockImplementation((cmd, args): Promise => { + const fullCmd = `${cmd} ${args?.join(' ')}`; + let result = ''; + switch (fullCmd) { + case 'git branch --show-current': + result = ''; + break; + case 'git show -s --pretty=%D': + result = 'HEAD, origin/test, test'; + break; + } + return Promise.resolve({ + stdout: result, + stderr: '', + exitCode: 0 + }); + }); + + const ref = await Git.ref(); + + expect(ref).toEqual('refs/heads/test'); }); }); diff --git a/src/git.ts b/src/git.ts index 1ed9c55..7845300 100644 --- a/src/git.ts +++ b/src/git.ts @@ -89,13 +89,12 @@ export class Git { } public static async ref(): Promise { - return await Git.exec(['symbolic-ref', 'HEAD']).catch(() => { - // if it fails (for example in a detached HEAD state), falls back to - // using git tag or describe to get the exact matching tag name. - return Git.tag().then(tag => { - return `refs/tags/${tag}`; - }); - }); + const isHeadDetached = await Git.isHeadDetached(); + if (isHeadDetached) { + return await Git.getDetachedRef(); + } + + return await Git.exec(['symbolic-ref', 'HEAD']); } public static async fullCommit(): Promise { @@ -115,6 +114,37 @@ export class Git { }); } + private static async isHeadDetached(): Promise { + return await Git.exec(['branch', '--show-current']).then(res => { + return res.length == 0; + }); + } + + private static async getDetachedRef(): Promise { + const res = await Git.exec(['show', '-s', '--pretty=%D']); + + const refMatch = res.match(/^HEAD, (.*)$/); + + if (!refMatch) { + throw new Error(`Cannot find detached HEAD ref in "${res}"`); + } + + const ref = refMatch[1].trim(); + + // Tag refs are formatted as "tag: " + if (ref.startsWith('tag: ')) { + return `refs/tags/${ref.split(':')[1].trim()}`; + } + + // Otherwise, it's a branch "/, " + const branchMatch = ref.match(/^[^/]+\/[^/]+, (.+)$/); + if (branchMatch) { + return `refs/heads/${branchMatch[1].trim()}`; + } + + throw new Error(`Unsupported detached HEAD ref in "${res}"`); + } + private static async exec(args: string[] = []): Promise { return await Exec.getExecOutput(`git`, args, { ignoreReturnCode: true,