Files
Sebastian Poxhofer 9defb36404 refactor(manager): share repo status to file change conversion (#46090)
Add `collectFileChanges()` next to the other repository helpers, plus a
thin `fileChangesToArtifactResults()` in `lib/modules/manager/util.ts`,
and migrate the managers which re-implemented the same status loops:
apm, cocoapods, composer, copier, gomod, helmv3, jsonnet-bundler,
kustomize and vendir.

Each site keeps its own status bucket selection and path filter, so the
emitted artifacts are unchanged. Two dead pieces of logic are dropped:
vendir's `vendorDir || isFileInVendorDir` condition, which was always
true, and kustomize's optional chaining on a non-nullable status, which
only ever mattered for an unset test mock (now set explicitly).
2026-09-16 13:13:40 +00:00

94 lines
2.6 KiB
TypeScript

import { coerceArray } from '../array.ts';
import { readLocalFile } from '../fs/index.ts';
import type { FileAddition, FileChange, StatusResult } from './types.ts';
/**
* The `git status` buckets which can be turned into {@link FileChange}s.
*
* All of them describe additions, except `deleted` which describes deletions
* and `renamed` which describes a deletion followed by an addition.
*/
export type RepoStatusBucket =
'modified' | 'not_added' | 'created' | 'conflicted' | 'renamed' | 'deleted';
/**
* Extra metadata which can be attached to a {@link FileAddition}.
*/
export type FileAdditionMetadata = Partial<
Omit<FileAddition, 'type' | 'path' | 'contents'>
>;
export interface CollectFileChangesOptions {
/**
* The `git status` buckets to collect, in output order.
*
* @default ['modified', 'not_added', 'deleted']
*/
include?: RepoStatusBucket[];
/**
* Only paths for which this returns `true` are collected.
*/
filter?: (path: string) => boolean;
/**
* Called for every addition to attach extra metadata, e.g. the executable bit.
*/
additionMetadata?: (path: string) => Promise<FileAdditionMetadata>;
}
const defaultInclude: RepoStatusBucket[] = ['modified', 'not_added', 'deleted'];
async function toAddition(
path: string,
additionMetadata: CollectFileChangesOptions['additionMetadata'],
): Promise<FileAddition> {
return {
type: 'addition',
path,
contents: await readLocalFile(path),
...(await additionMetadata?.(path)),
};
}
/**
* Converts the result of `getRepoStatus()` into the {@link FileChange}s which
* managers return as artifact updates. The contents of every addition are read
* from the local directory.
*/
export async function collectFileChanges(
status: StatusResult,
options: CollectFileChangesOptions = {},
): Promise<FileChange[]> {
const { include = defaultInclude, filter, additionMetadata } = options;
const changes: FileChange[] = [];
for (const bucket of include) {
if (bucket === 'renamed') {
for (const { from, to } of coerceArray(status.renamed)) {
if (!filter || filter(from)) {
changes.push({ type: 'deletion', path: from });
}
if (!filter || filter(to)) {
changes.push(await toAddition(to, additionMetadata));
}
}
continue;
}
for (const path of coerceArray(status[bucket])) {
if (filter && !filter(path)) {
continue;
}
if (bucket === 'deleted') {
changes.push({ type: 'deletion', path });
} else {
changes.push(await toAddition(path, additionMetadata));
}
}
}
return changes;
}