Merge pull request #173 from hoverkraft-tech/fix/handle-detached-ref
fix(git): handle properly detached HEAD ref
This commit is contained in:
@@ -34,6 +34,9 @@ describe('context', () => {
|
|||||||
case 'git show --format=%H HEAD --quiet --':
|
case 'git show --format=%H HEAD --quiet --':
|
||||||
result = 'test-sha';
|
result = 'test-sha';
|
||||||
break;
|
break;
|
||||||
|
case 'git branch --show-current':
|
||||||
|
result = 'test';
|
||||||
|
break;
|
||||||
case 'git symbolic-ref HEAD':
|
case 'git symbolic-ref HEAD':
|
||||||
result = 'refs/heads/test';
|
result = 'refs/heads/test';
|
||||||
break;
|
break;
|
||||||
@@ -90,17 +93,76 @@ describe('remoteURL', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('ref', () => {
|
describe('ref', () => {
|
||||||
it('have been called', async () => {
|
it('returns mocked ref', async () => {
|
||||||
const execSpy = jest.spyOn(Exec, 'getExecOutput');
|
jest.spyOn(Exec, 'getExecOutput').mockImplementation((cmd, args): Promise<ExecOutput> => {
|
||||||
try {
|
const fullCmd = `${cmd} ${args?.join(' ')}`;
|
||||||
await Git.ref();
|
let result = '';
|
||||||
} catch (err) {
|
switch (fullCmd) {
|
||||||
// noop
|
case 'git branch --show-current':
|
||||||
}
|
result = 'test';
|
||||||
expect(execSpy).toHaveBeenCalledWith(`git`, ['symbolic-ref', 'HEAD'], {
|
break;
|
||||||
silent: true,
|
case 'git symbolic-ref HEAD':
|
||||||
ignoreReturnCode: true
|
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<ExecOutput> => {
|
||||||
|
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<ExecOutput> => {
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
44
src/git.ts
44
src/git.ts
@@ -89,13 +89,12 @@ export class Git {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async ref(): Promise<string> {
|
public static async ref(): Promise<string> {
|
||||||
return await Git.exec(['symbolic-ref', 'HEAD']).catch(() => {
|
const isHeadDetached = await Git.isHeadDetached();
|
||||||
// if it fails (for example in a detached HEAD state), falls back to
|
if (isHeadDetached) {
|
||||||
// using git tag or describe to get the exact matching tag name.
|
return await Git.getDetachedRef();
|
||||||
return Git.tag().then(tag => {
|
}
|
||||||
return `refs/tags/${tag}`;
|
|
||||||
});
|
return await Git.exec(['symbolic-ref', 'HEAD']);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fullCommit(): Promise<string> {
|
public static async fullCommit(): Promise<string> {
|
||||||
@@ -115,6 +114,37 @@ export class Git {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async isHeadDetached(): Promise<boolean> {
|
||||||
|
return await Git.exec(['branch', '--show-current']).then(res => {
|
||||||
|
return res.length == 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getDetachedRef(): Promise<string> {
|
||||||
|
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: <tagname>"
|
||||||
|
if (ref.startsWith('tag: ')) {
|
||||||
|
return `refs/tags/${ref.split(':')[1].trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, it's a branch "<origin>/<branch-name>, <branch-name>"
|
||||||
|
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<string> {
|
private static async exec(args: string[] = []): Promise<string> {
|
||||||
return await Exec.getExecOutput(`git`, args, {
|
return await Exec.getExecOutput(`git`, args, {
|
||||||
ignoreReturnCode: true,
|
ignoreReturnCode: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user