fix(git): support getting ref in various detached HEAD contexts

Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
This commit is contained in:
Emilien Escalle
2025-11-12 18:23:46 +01:00
parent 747bc1653d
commit ad7ffdebbe
2 changed files with 228 additions and 5 deletions

View File

@@ -122,6 +122,11 @@ export class Git {
private static async getDetachedRef(): Promise<string> {
const res = await Git.exec(['show', '-s', '--pretty=%D']);
core.debug(`detached HEAD ref: ${res}`);
if (res === 'HEAD') {
return await Git.inferRefFromHead();
}
// Can be "HEAD, <tagname>" or "grafted, HEAD, <tagname>"
const refMatch = res.match(/^(grafted, )?HEAD, (.*)$/);
@@ -137,16 +142,22 @@ export class Git {
return `refs/tags/${ref.split(':')[1].trim()}`;
}
// Branch refs are formatted as "<origin>/<branch-name>, <branch-name>"
// Pull request merge refs are formatted as "pull/<number>/<state>"
const prMatch = ref.match(/^pull\/\d+\/(head|merge)$/);
if (prMatch) {
return `refs/${ref}`;
}
// Branch refs can be formatted as "<origin>/<branch-name>, <branch-name>"
const branchMatch = ref.match(/^[^/]+\/[^/]+, (.+)$/);
if (branchMatch) {
return `refs/heads/${branchMatch[1].trim()}`;
}
// Pull request merge refs are formatted as "pull/<number>/<state>"
const prMatch = ref.match(/^pull\/\d+\/(head|merge)$/);
if (prMatch) {
return `refs/${ref}`;
// Branch refs checked out by its latest SHA can be formatted as "<origin>/<branch-name>"
const shaBranchMatch = ref.match(/^[^/]+\/(.+)$/);
if (shaBranchMatch) {
return `refs/heads/${shaBranchMatch[1].trim()}`;
}
throw new Error(`Unsupported detached HEAD ref in "${res}"`);
@@ -164,6 +175,43 @@ export class Git {
});
}
private static async inferRefFromHead(): Promise<string> {
const localRef = await Git.findContainingRef('refs/heads/');
if (localRef) {
return localRef;
}
const remoteRef = await Git.findContainingRef('refs/remotes/');
if (remoteRef) {
const remoteMatch = remoteRef.match(/^refs\/remotes\/[^/]+\/(.+)$/);
if (remoteMatch) {
return `refs/heads/${remoteMatch[1]}`;
}
return remoteRef;
}
const tagRef = await Git.exec(['tag', '--contains', 'HEAD']);
const [firstTag] = tagRef
.split('\n')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
if (firstTag) {
return `refs/tags/${firstTag}`;
}
throw new Error(`Cannot infer ref from detached HEAD`);
}
private static async findContainingRef(scope: string): Promise<string | undefined> {
const refs = await Git.exec(['for-each-ref', '--format=%(refname)', '--contains', 'HEAD', '--sort=-committerdate', scope]);
const [first] = refs
.split('\n')
.map(r => r.trim())
.filter(r => r.length > 0);
return first;
}
public static async commitDate(ref: string): Promise<Date> {
return new Date(await Git.exec(['show', '-s', '--format="%ci"', ref]));
}