Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions packages/aws-cdk-lib/core/lib/fs/fingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions
return hash.digest('hex');

function _processFileOrDirectory(symbolicPath: string, isRootDir: boolean = false, realPath = symbolicPath) {
if (!isRootDir && ignoreStrategy.ignores(symbolicPath)) {
const stat = fs.lstatSync(realPath);

if (_shouldIgnore(isRootDir, symbolicPath, realPath, stat)) {
return;
}

const stat = fs.lstatSync(realPath);

// Use relative path as hash component. Normalize it with forward slashes to ensure
// same hash on Windows and Linux.
const hashComponent = path.relative(fileOrDirectory, symbolicPath).replace(/\\/g, '/');
Expand All @@ -94,6 +94,32 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions
throw new UnscopedValidationError(`Unable to hash ${symbolicPath}: it is neither a file nor a directory`);
}
}

function _shouldIgnore(isRootDir: boolean, symbolicPath: string, realPath: string, stat: fs.Stats) {
if (isRootDir) {
return false;
}

if (stat.isDirectory()) {
return ignoreStrategy.completelyIgnores(symbolicPath);
}

if (stat.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(symbolicPath);
const resolvedLinkTarget = path.resolve(path.dirname(realPath), linkTarget);

if (shouldFollow(follow, rootDirectory, resolvedLinkTarget)) {
const targetStat = fs.statSync(resolvedLinkTarget);

// If we are following a directory symlink, we should use `completelyIgnores`.
if (targetStat.isDirectory()) {
return ignoreStrategy.completelyIgnores(symbolicPath);
}
}
}

return ignoreStrategy.ignores(symbolicPath);
}
}

export function contentFingerprint(file: string): string {
Expand Down
39 changes: 27 additions & 12 deletions packages/aws-cdk-lib/core/lib/fs/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,13 @@ export abstract class IgnoreStrategy {
public abstract ignores(absoluteFilePath: string): boolean;

/**
* Determines whether a given file path should be ignored and have all of its children ignored
* if its a directory.
* Determines whether a given directory path should be ignored and have all of its children ignored.
*
* @param absoluteFilePath absolute file path to be assessed against the pattern
* @returns `true` if the file should be ignored
* @param absoluteDirectoryPath absolute directory path to be assessed against the pattern
* @returns `true` if the directory and all of its children should be ignored
*/
public completelyIgnores(absoluteFilePath: string): boolean {
return this.ignores(absoluteFilePath);
public completelyIgnores(absoluteDirectoryPath: string): boolean {
return this.ignores(absoluteDirectoryPath);
}
}

Expand Down Expand Up @@ -189,6 +188,23 @@ export class GitIgnoreStrategy extends IgnoreStrategy {

return this.ignore.ignores(relativePath);
}

/**
* Determines whether a given directory path should be ignored and have all of its children ignored.
*
* @param absoluteDirectoryPath absolute directory path to be assessed against the pattern
* @returns `true` if the directory and all of its children should be ignored
*/
public completelyIgnores(absoluteDirectoryPath: string): boolean {
if (!path.isAbsolute(absoluteDirectoryPath)) {
throw new UnscopedValidationError('GitIgnoreStrategy.completelyIgnores() expects an absolute path');
}

const relativePath = path.relative(this.absoluteRootPath, absoluteDirectoryPath);
const relativePathWithSep = relativePath.endsWith(path.sep) ? relativePath : relativePath + path.sep;

return this.ignore.ignores(relativePathWithSep);
}
}

/**
Expand Down Expand Up @@ -268,14 +284,13 @@ export class DockerIgnoreStrategy extends IgnoreStrategy {
}

/**
* Determines whether a given file path should be ignored and have all of its children ignored
* if its a directory.
* Determines whether a given directory path should be ignored and have all of its children ignored.
*
* @param absoluteFilePath absolute file path to be assessed against the pattern
* @returns `true` if the file should be ignored
* @param absoluteDirectoryPath absolute directory path to be assessed against the pattern
* @returns `true` if the directory and all of its children should be ignored
*/
public completelyIgnores(absoluteFilePath: string): boolean {
const relativePath = this.getRelativePath(absoluteFilePath);
public completelyIgnores(absoluteDirectoryPath: string): boolean {
const relativePath = this.getRelativePath(absoluteDirectoryPath);
return this.ignore.ignores(relativePath) && this.completeIgnore.ignores(relativePath);
}
}
22 changes: 22 additions & 0 deletions packages/aws-cdk-lib/core/test/fs/fs-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ describe('fs copy', () => {
' file3.txt',
]);
});

test('negated pattern inside subdirectory with git ignore mode', () => {
// GIVEN
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests'));

// WHEN
FileSystem.copyDirectory(path.join(__dirname, 'fixtures', 'test1'), outdir, {
exclude: [
'*',
'!.hidden',
'!*/',
],
ignoreMode: IgnoreMode.GIT,
});

// THEN
expect(tree(outdir)).toEqual([
'subdir2 (D)',
' empty-subdir (D)',
' .hidden',
]);
});
});

function tree(dir: string, depth = ''): string[] {
Expand Down
59 changes: 58 additions & 1 deletion packages/aws-cdk-lib/core/test/fs/fs-fingerprint.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { FileSystem, SymlinkFollowMode } from '../../lib/fs';
import { FileSystem, IgnoreMode, SymlinkFollowMode } from '../../lib/fs';
import { contentFingerprint } from '../../lib/fs/fingerprint';

describe('fs fingerprint', () => {
Expand Down Expand Up @@ -91,6 +91,25 @@ describe('fs fingerprint', () => {
// THEN
expect(hashSrc).not.toEqual(hashCopy);
});

test('changes with negated gitignore patterns inside ignored directories', () => {
// GIVEN
const srcdir = path.join(__dirname, 'fixtures', 'symlinks');
const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
FileSystem.copyDirectory(srcdir, cpydir);

// Add a new file that is inside an ignored directory, but has a negated pattern that includes it.
const newFile = path.join(cpydir, 'ignored-dir', 'not-ignored-anymore.html');
fs.mkdirSync(path.dirname(newFile), { recursive: true });
fs.writeFileSync(newFile, '<h1>Do not ignore me!</h1>');

// WHEN
const hashSrc = FileSystem.fingerprint(srcdir, { exclude: ['*', '!*.html', '!*/'], ignoreMode: IgnoreMode.GIT });
const hashCopy = FileSystem.fingerprint(cpydir, { exclude: ['*', '!*.html', '!*/'], ignoreMode: IgnoreMode.GIT });

// THEN
expect(hashSrc).not.toEqual(hashCopy);
});
});

describe('symlinks', () => {
Expand Down Expand Up @@ -147,6 +166,44 @@ describe('fs fingerprint', () => {
expect(original).toEqual(afterChange);
expect(afterRevert).toEqual(original);
});

test('changes when following directory symlinks with negated gitignore patterns', () => {
// GIVEN
const srcdir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
fs.mkdirSync(path.join(srcdir, 'subdir'), { recursive: true });
fs.writeFileSync(path.join(srcdir, 'subdir', 'page.html'), '<h1>Hello, world!</h1>');

const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
FileSystem.copyDirectory(srcdir, cpydir);
fs.symlinkSync(path.join(cpydir, 'subdir'), path.join(cpydir, 'link-to-dir'));

// WHEN
const options = { exclude: ['*', '!*.html', '!*/'], ignoreMode: IgnoreMode.GIT, follow: SymlinkFollowMode.ALWAYS };
const hashSrc = FileSystem.fingerprint(srcdir, options);
const hashCpy = FileSystem.fingerprint(cpydir, options);

// THEN
expect(hashSrc).not.toEqual(hashCpy);
});

test('does not change when not following directory symlinks with negated gitignore patterns', () => {
// GIVEN
const srcdir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
fs.mkdirSync(path.join(srcdir, 'subdir'), { recursive: true });
fs.writeFileSync(path.join(srcdir, 'subdir', 'page.html'), '<h1>Hello, world!</h1>');

const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
FileSystem.copyDirectory(srcdir, cpydir);
fs.symlinkSync(path.join(cpydir, 'subdir'), path.join(cpydir, 'link-to-dir'));

// WHEN
const options = { exclude: ['*', '!*.html', '!*/'], ignoreMode: IgnoreMode.GIT, follow: SymlinkFollowMode.NEVER };
const hashSrc = FileSystem.fingerprint(srcdir, options);
const hashCpy = FileSystem.fingerprint(cpydir, options);

// THEN
expect(hashSrc).toEqual(hashCpy);
});
});

describe('eol', () => {
Expand Down
Loading