diff --git a/cli/git-user/git-user.test.ts b/cli/git-user/git-user.test.ts index 9c872bd..2a681e1 100644 --- a/cli/git-user/git-user.test.ts +++ b/cli/git-user/git-user.test.ts @@ -22,6 +22,13 @@ describe("getLocalGitUser", () => { expect(getLocalGitUser()).toEqual({ name: "tomhagen" }); }); + it("returns an user with a trimmed username", () => { + (execSync as jest.Mock).mockReturnValueOnce("tomhagen "); + expect(getLocalGitUser()).toEqual({ name: "tomhagen" }); + (execSync as jest.Mock).mockReturnValueOnce("tomhagen\n\n"); + expect(getLocalGitUser()).toEqual({ name: "tomhagen" }); + }); + it("throws an error if execSync fails", () => { (execSync as jest.Mock).mockImplementationOnce(() => { throw new Error(); diff --git a/cli/git-user/index.ts b/cli/git-user/index.ts index 655c8e9..0e04357 100644 --- a/cli/git-user/index.ts +++ b/cli/git-user/index.ts @@ -10,7 +10,7 @@ export function getLocalGitUser(): GitUser { if (!name) { throw new Error(); } - return { name }; + return { name: name.trim().replaceAll("\n", "") }; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_: unknown) { throw new Error(getRandomMessage(MessageType.NoGitConfig)); diff --git a/scripts/init/index.ts b/scripts/init/index.ts index fc844f4..4b457e8 100644 --- a/scripts/init/index.ts +++ b/scripts/init/index.ts @@ -33,7 +33,7 @@ function informFileCreated( `- The following crews were detected:\n${crews .map((crew) => ` - ${crew}`) .join("\n")} -Please specify their members in the codefather config for CLI enforcement.` +\n⚙️ Please specify their members in the codefather config for CLI enforcement.\n` ); } } diff --git a/shared/file-matcher/file-matcher.test.ts b/shared/file-matcher/file-matcher.test.ts index 5081fae..2165ca6 100644 --- a/shared/file-matcher/file-matcher.test.ts +++ b/shared/file-matcher/file-matcher.test.ts @@ -109,4 +109,69 @@ describe("matchFilesAgainstRule", () => { const matched = matchFilesAgainstRule([file], rule.match, configRoot); expect(matched).toContain(file); }); + test("matches rule with leading slash", () => { + const result = matchFilesAgainstRule(updatedFiles, ["/src/core/**"], root); + expect(result).toEqual([resolve("project/src/core/index.ts")]); + }); + test("matches rule with trailing slash", () => { + const result = matchFilesAgainstRule( + updatedFiles, + ["src/core/"], // Should auto-expand to src/core/** + root + ); + expect(result).toEqual([resolve("project/src/core/index.ts")]); + }); + test("matches rule with whitespace", () => { + const result = matchFilesAgainstRule( + updatedFiles, + [" src/core/** "], + root + ); + expect(result).toEqual([resolve("project/src/core/index.ts")]); + }); + test("matches dotfiles", () => { + const result = matchFilesAgainstRule(updatedFiles, [".env"], root); + expect(result).toEqual([resolve("project/.env")]); + }); + test("matches deeply nested file", () => { + const result = matchFilesAgainstRule( + updatedFiles, + ["src/models/searches/**"], + root + ); + expect(result).toEqual([resolve("project/src/models/searches/utils.ts")]); + }); + test("matches pattern starting with ./", () => { + const result = matchFilesAgainstRule(updatedFiles, ["./src/core/**"], root); + expect(result).toEqual([resolve("project/src/core/index.ts")]); + }); + test("matches pattern ending with /**/", () => { + const result = matchFilesAgainstRule(updatedFiles, ["src/**/"], root); + expect(result).toContain(resolve("project/src/core/index.ts")); + expect(result).toContain(resolve("project/src/utils/helpers.ts")); + expect(result).toContain(resolve("project/src/models/searches/utils.ts")); + }); + test("matches pattern with regex metacharacters in path", () => { + const specialFiles = [ + resolve("project/src/utils/file[1].ts"), + resolve("project/src/utils/file(2).ts"), + ]; + const result = matchFilesAgainstRule(specialFiles, ["src/utils/**"], root); + expect(result).toEqual(specialFiles); + }); + test("matches hidden file in subdirectory", () => { + const hiddenFile = resolve("project/src/.hidden/config.ts"); + const result = matchFilesAgainstRule([hiddenFile], ["src/**"], root); + expect(result).toEqual([hiddenFile]); + }); + test("matches file with unicode characters in path", () => { + const unicodeFile = resolve("project/src/设计/模块.ts"); + const result = matchFilesAgainstRule([unicodeFile], ["src/**"], root); + expect(result).toEqual([unicodeFile]); + }); + test("matches file with spaces and parentheses in path", () => { + const spacedFile = resolve("project/src/utils/my file (copy).ts"); + const result = matchFilesAgainstRule([spacedFile], ["src/utils/**"], root); + expect(result).toEqual([spacedFile]); + }); }); diff --git a/shared/file-matcher/index.ts b/shared/file-matcher/index.ts index b0e01f6..2c8f146 100644 --- a/shared/file-matcher/index.ts +++ b/shared/file-matcher/index.ts @@ -5,7 +5,13 @@ const normalizePath = (filePath: string) => filePath.replace(/\\/g, "/"); function globToRegExp(glob: string | RegExp): RegExp { if (glob instanceof RegExp) return glob; - const escaped = glob + + let normalizedGlob = normalizePath(glob.trim().replace(/^\.?\/+/, "")); + + // Auto-expand trailing slash to recursive match + if (normalizedGlob.endsWith("/")) normalizedGlob += "**"; + + const escaped = normalizedGlob .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*\*/g, "§§") .replace(/\*/g, "[^/]*") @@ -22,7 +28,7 @@ export function matchFilesAgainstRule( const regexes = rulePattern.map(globToRegExp); const normalizedRootDir = normalizePath(rootDir); return files.filter((file) => { - const normalizedFile = normalizePath(file); + const normalizedFile = normalizePath(file.trim()); const relativePath = normalizePath( relative(normalizedRootDir, normalizedFile) ); diff --git a/shared/messages/index.ts b/shared/messages/index.ts index a5e049e..b44347a 100644 --- a/shared/messages/index.ts +++ b/shared/messages/index.ts @@ -50,7 +50,6 @@ const sharedErrors = [ "𐄂 _committers_: That commit screams betrayal. _goodfellas_ heard it loud and clear.", "𐄂 _committers_: You broke something built on trust. Now it needs fixing.", "𐄂 _committers_, this act smells like ambition. Careful—ambition got Fredo killed.", - "𐄂 _committers_: You messed up. Big. _goodfellas_ are already talking.", "𐄂 _committers_: You acted like a enemy. We don’t deal with enemies.", "𐄂 _committers_: That move lacked honor. Fix it or fade out.", "𐄂 _committers_: You weren’t subtle. You weren’t careful. Call _goodfella_ to clear things out.", @@ -75,10 +74,12 @@ const messagesMap: Record = { ], [MessageType.Error]: [ "𐄂 _committers_! You need permission from my trusted associate: _goodfellas_. Nobody touches this without approval.", + "𐄂 _committers_: You messed up. Big. _goodfellas_ is already talking.", ...sharedErrors, ], [MessageType.MultiErrors]: [ "𐄂 _committers_! You need permission from my trusted associates: _goodfellas_. Nobody touches this without approval.", + "𐄂 _committers_: You messed up. Big. _goodfellas_ are already talking.", ...sharedErrors, ], [MessageType.Warning]: [