Files
renovate/lib/util/git/file-changes.spec.ts
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

135 lines
3.6 KiB
TypeScript

import { fs, partial } from '~test/util.ts';
import { collectFileChanges } from './file-changes.ts';
import type { StatusResult } from './types.ts';
vi.mock('../fs/index.ts');
describe('util/git/file-changes', () => {
beforeEach(() => {
fs.readLocalFile.mockImplementation((file) =>
Promise.resolve(`content of ${file}`),
);
});
it('collects modified, not added and deleted files by default', async () => {
const status = partial<StatusResult>({
modified: ['modified.txt'],
not_added: ['added.txt'],
created: ['created.txt'],
deleted: ['deleted.txt'],
});
await expect(collectFileChanges(status)).resolves.toEqual([
{
type: 'addition',
path: 'modified.txt',
contents: 'content of modified.txt',
},
{
type: 'addition',
path: 'added.txt',
contents: 'content of added.txt',
},
{ type: 'deletion', path: 'deleted.txt' },
]);
});
it('returns changes in the order of the included buckets', async () => {
const status = partial<StatusResult>({
modified: ['modified.txt'],
created: ['created.txt'],
conflicted: ['conflicted.txt'],
deleted: ['deleted.txt'],
});
await expect(
collectFileChanges(status, {
include: ['deleted', 'conflicted', 'created'],
}),
).resolves.toEqual([
{ type: 'deletion', path: 'deleted.txt' },
{
type: 'addition',
path: 'conflicted.txt',
contents: 'content of conflicted.txt',
},
{
type: 'addition',
path: 'created.txt',
contents: 'content of created.txt',
},
]);
});
it('turns renames into a deletion and an addition', async () => {
const status = partial<StatusResult>({
renamed: [{ from: 'old.txt', to: 'new.txt' }],
});
await expect(
collectFileChanges(status, { include: ['renamed'] }),
).resolves.toEqual([
{ type: 'deletion', path: 'old.txt' },
{ type: 'addition', path: 'new.txt', contents: 'content of new.txt' },
]);
});
it('applies the filter to every bucket', async () => {
const status = partial<StatusResult>({
modified: ['sub/modified.txt', 'other/modified.txt'],
deleted: ['sub/deleted.txt', 'other/deleted.txt'],
renamed: [
{ from: 'other/old.txt', to: 'sub/new.txt' },
{ from: 'sub/old.txt', to: 'other/new.txt' },
],
});
await expect(
collectFileChanges(status, {
include: ['modified', 'deleted', 'renamed'],
filter: (path) => path.startsWith('sub/'),
}),
).resolves.toEqual([
{
type: 'addition',
path: 'sub/modified.txt',
contents: 'content of sub/modified.txt',
},
{ type: 'deletion', path: 'sub/deleted.txt' },
{
type: 'addition',
path: 'sub/new.txt',
contents: 'content of sub/new.txt',
},
{ type: 'deletion', path: 'sub/old.txt' },
]);
});
it('attaches addition metadata', async () => {
const status = partial<StatusResult>({ not_added: ['bin/tool'] });
await expect(
collectFileChanges(status, {
include: ['not_added'],
additionMetadata: (path) =>
Promise.resolve({ isExecutable: path.startsWith('bin/') }),
}),
).resolves.toEqual([
{
type: 'addition',
path: 'bin/tool',
contents: 'content of bin/tool',
isExecutable: true,
},
]);
});
it('tolerates missing buckets', async () => {
await expect(
collectFileChanges(partial<StatusResult>({}), {
include: ['modified', 'deleted', 'renamed'],
}),
).resolves.toEqual([]);
});
});