Skip to content

Commit

Permalink
feat: add lockfile direct dep affected detection
Browse files Browse the repository at this point in the history
  • Loading branch information
EladBezalel committed Nov 27, 2023
1 parent 161c86c commit 7bd8bf0
Show file tree
Hide file tree
Showing 12 changed files with 656 additions and 181 deletions.
95 changes: 95 additions & 0 deletions libs/core/src/assets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { fastFindInFiles } from 'fast-find-in-files';
import { existsSync } from 'fs';
import * as path from 'path';
import { findNonSourceAffectedFiles } from './assets';

jest.mock('fast-find-in-files');
jest.mock('fs');

describe('findNonSourceAffectedFiles', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should return relevant files', () => {
const cwd = '/project';
const changedFilePath = '/project/src/file.ts';
const excludeFolderPaths = ['node_modules', 'dist', '.git'];

(fastFindInFiles as jest.Mock).mockReturnValue([
{
filePath: '/project/src/file.ts',
queryHits: [{ lineNumber: 1, line: `"file.ts"` }],
},
]);
(existsSync as jest.Mock).mockReturnValue(true);

const result = findNonSourceAffectedFiles(
cwd,
changedFilePath,
excludeFolderPaths
);

expect(result).toEqual([{ filePath: 'src/file.ts', changedLines: [1] }]);
expect(fastFindInFiles).toHaveBeenCalledWith({
directory: cwd,
needle: path.basename(changedFilePath),
excludeFolderPaths: excludeFolderPaths.map((folder) =>
path.join(cwd, folder)
),
});
});

it('should return empty array if no relevant files found', () => {
const cwd = '/project';
const changedFilePath = '/project/src/file.ts';
const excludeFolderPaths = ['node_modules', 'dist', '.git'];

(fastFindInFiles as jest.Mock).mockReturnValue([]);
(existsSync as jest.Mock).mockReturnValue(true);

const result = findNonSourceAffectedFiles(
cwd,
changedFilePath,
excludeFolderPaths
);

expect(result).toEqual([]);
expect(fastFindInFiles).toHaveBeenCalledWith({
directory: cwd,
needle: path.basename(changedFilePath),
excludeFolderPaths: excludeFolderPaths.map((folder) =>
path.join(cwd, folder)
),
});
});

it("should still work even if found file didn't have a match", () => {
const cwd = '/project';
const changedFilePath = '/project/src/file.ts';
const excludeFolderPaths = ['node_modules', 'dist', '.git'];

(fastFindInFiles as jest.Mock).mockReturnValue([
{
filePath: '/project/src/file.ts',
queryHits: [{ lineNumber: 1, line: `console.log('hi')` }],
},
]);
(existsSync as jest.Mock).mockReturnValue(true);

const result = findNonSourceAffectedFiles(
cwd,
changedFilePath,
excludeFolderPaths
);

expect(result).toEqual([]);
expect(fastFindInFiles).toHaveBeenCalledWith({
directory: cwd,
needle: path.basename(changedFilePath),
excludeFolderPaths: excludeFolderPaths.map((folder) =>
path.join(cwd, folder)
),
});
});
});
63 changes: 63 additions & 0 deletions libs/core/src/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { basename, dirname, join, relative, resolve } from 'path';
import { ChangedFiles } from './git';
import { FastFindInFiles, fastFindInFiles } from 'fast-find-in-files';
import { existsSync } from 'fs';

export function findNonSourceAffectedFiles(
cwd: string,
changedFilePath: string,
excludeFolderPaths: string[]
): ChangedFiles[] {
const fileName = basename(changedFilePath);

const files = fastFindInFiles({
directory: cwd,
needle: fileName,
excludeFolderPaths: excludeFolderPaths.map((path) => join(cwd, path)),
});

const relevantFiles = filterRelevantFiles(cwd, files, changedFilePath);

return relevantFiles;
}

function filterRelevantFiles(
cwd: string,
files: FastFindInFiles[],
changedFilePath: string
): ChangedFiles[] {
const fileName = basename(changedFilePath);
const regExp = new RegExp(`['"\`](?<relFilePath>.*${fileName})['"\`]`);

return files
.map(({ filePath: foundFilePath, queryHits }) => ({
filePath: relative(cwd, foundFilePath),
changedLines: queryHits
.filter(({ line }) =>
isRelevantLine(line, regExp, cwd, foundFilePath, changedFilePath)
)
.map(({ lineNumber }) => lineNumber),
}))
.filter(({ changedLines }) => changedLines.length > 0);
}

function isRelevantLine(
line: string,
regExp: RegExp,
cwd: string,
foundFilePath: string,
changedFilePath: string
): boolean {
const match = regExp.exec(line);
const { relFilePath } = match?.groups ?? {};

if (relFilePath == null) return false;

const changedFile = resolve(cwd, changedFilePath);
const relatedFilePath = resolve(
cwd,
relative(cwd, join(dirname(foundFilePath), relFilePath))
);

return relatedFilePath === changedFile && existsSync(relatedFilePath);
}
32 changes: 31 additions & 1 deletion libs/core/src/git.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { getChangedFiles, getMergeBase, getDiff } from './git';
import {
getChangedFiles,
getMergeBase,
getDiff,
getFileFromRevision,
} from './git';
import { resolve } from 'path';
import { readFile, writeFile } from 'fs/promises';
import * as childProcess from 'node:child_process';
Expand Down Expand Up @@ -184,4 +189,29 @@ describe('git', () => {
expect(changedFiles).toEqual([]);
});
});

describe('getFileFromRevision', () => {
it('should return the file content from the specified revision', () => {
const fileContent = getFileFromRevision({
base: branch,
filePath: './index.ts',
cwd,
});

expect(fileContent).toEqual(expect.any(String));
});

it('should throw an error if unable to get the file content', () => {
const filePath = './missing.ts';
expect(() =>
getFileFromRevision({
base: branch,
filePath,
cwd,
})
).toThrow(
`Unable to get file "${filePath}" for base: "${branch}". are you using the correct base?`
);
});
});
});
28 changes: 26 additions & 2 deletions libs/core/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,39 @@ export function getDiff({ base, cwd }: BaseGitActionArgs): string {
}
}

export interface GetChangedFiles {
interface FileFromRevisionArgs extends BaseGitActionArgs {
filePath: string;
}

export function getFileFromRevision({
base,
filePath,
cwd,
}: FileFromRevisionArgs): string {
try {
return execSync(`git show ${base}:${filePath}`, {
maxBuffer: TEN_MEGABYTES,
cwd,
stdio: 'pipe',
})
.toString()
.trim();
} catch (e) {
throw new Error(
`Unable to get file "${filePath}" for base: "${base}". are you using the correct base?`
);
}
}

export interface ChangedFiles {
filePath: string;
changedLines: number[];
}

export function getChangedFiles({
base,
cwd,
}: BaseGitActionArgs): GetChangedFiles[] {
}: BaseGitActionArgs): ChangedFiles[] {
const mergeBase = getMergeBase({ base, cwd });
const diff = getDiff({ base: mergeBase, cwd });

Expand Down
Loading

0 comments on commit 7bd8bf0

Please sign in to comment.