Files
renovate/lib/modules/datasource/git-refs/base.ts
T
Sergei Zharinov cca6227dfa fix(git): resolve child env before adding authentication (#45184)
* fix(git): preserve approved runtime config with authentication

* test(git): remove redundant authentication cases

* test(git): update post-upgrade environment assertions

* test(hermit): cover uninstall setup failure

* refactor(git): clarify authentication host types

* refactor(git): coerce exec options

* refactor(git): coerce docker env vars

* refactor(exec): coerce docker env vars
2026-08-10 11:54:18 +00:00

104 lines
3.1 KiB
TypeScript

import { isTruthy } from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import { withCache } from '../../../util/cache/package/with-cache.ts';
import { createSimpleGit } from '../../../util/git/index.ts';
import { getRemoteUrlWithToken } from '../../../util/git/url.ts';
import { newlineRegex, regEx } from '../../../util/regex.ts';
import { Datasource } from '../datasource.ts';
import type { GetReleasesConfig } from '../types.ts';
import type { RawRefs } from './types.ts';
const refMatch = regEx(/(?<hash>.*?)\s+refs\/(?<type>.*?)\/(?<value>.*)/);
const headMatch = regEx(/(?<hash>.*?)\s+HEAD/);
const gitId = 'git';
// TODO: extract to a separate directory structure (#10532)
export abstract class GitDatasource extends Datasource {
static id = gitId;
constructor(id: string) {
super(id);
}
private async _getRawRefs({
packageName,
}: GetReleasesConfig): Promise<RawRefs[] | null> {
const git = createSimpleGit({
authentication: { hostTypes: [this.id] },
});
// fetch remote tags
const lsRemote = await git.listRemote([
getRemoteUrlWithToken(packageName, this.id),
]);
if (!lsRemote) {
return null;
}
const allRefs = lsRemote
.trim()
.split(newlineRegex)
.map((line) => line.trim())
.map((line) => {
let match = refMatch.exec(line);
if (match?.groups) {
return {
type: match.groups.type,
value: match.groups.value,
hash: match.groups.hash,
};
}
match = headMatch.exec(line);
if (match?.groups) {
return {
type: '',
value: 'HEAD',
hash: match.groups.hash,
};
}
logger.trace(`malformed ref: ${line}`);
return null;
})
.filter(isTruthy)
.filter((ref) => ref.type !== 'pull');
// For annotated tags, git ls-remote returns two entries:
// 1. The tag object hash: refs/tags/v1.0.0
// 2. The dereferenced commit hash: refs/tags/v1.0.0^{}
// We need to use the dereferenced commit hash (^{}) for annotated tags
// to match what `git submodule status` returns (the actual commit hash).
// This prevents false-positive updates that result in empty commits.
const dereferencedTags: Record<string, string> = {};
for (const ref of allRefs) {
if (ref.value.endsWith('^{}')) {
// Store the commit hash for the base tag name (without ^{})
dereferencedTags[ref.value.slice(0, -3)] = ref.hash;
}
}
const refs = allRefs
.filter((ref) => !ref.value.endsWith('^{}'))
.map((ref) => {
// For annotated tags, use the dereferenced commit hash
const dereferencedHash = dereferencedTags[ref.value];
if (dereferencedHash) {
return { ...ref, hash: dereferencedHash };
}
return ref;
});
return refs;
}
getRawRefs(config: GetReleasesConfig): Promise<RawRefs[] | null> {
return withCache(
{
namespace: `datasource-${gitId}`,
key: config.packageName,
},
() => this._getRawRefs(config),
);
}
}