diff --git a/jest.config.js b/jest.config.js index b99eeed..5190dce 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,7 +5,7 @@ module.exports = { transformIgnorePatterns: ['/node_modules/'], roots: ["/test/", "/src/"], collectCoverage: true, - collectCoverageFrom: ["**/src/**", "!**/node_modules/**"], + collectCoverageFrom: ["src/**", "!**/node_modules/**"], coverageDirectory: './coverage', coverageReporters: ['json', 'lcovonly', 'text', 'clover'], testPathIgnorePatterns: ['/_utils/'], diff --git a/src/Commands/runVerifyUnpushedCommitsCommand.ts b/src/Commands/runVerifyUnpushedCommitsCommand.ts index b620e91..fa5d002 100644 --- a/src/Commands/runVerifyUnpushedCommitsCommand.ts +++ b/src/Commands/runVerifyUnpushedCommitsCommand.ts @@ -72,11 +72,20 @@ export async function verifyUnpushedCommits(args: Array, root: string, u return VerificationStatus.VERIFIED; } - if ([...commitsVerificationInfo.values()].some(info => !info.isSkipped && !info.isMergeCommit && !info.includesOnlyIgnoredFiles && !info.hasRelevancy)) { - console.log(`[Scope tags] All commits are verified, but found no relevancy data. To add relevancy use:\n`); - console.log("\nTo add relevancy use\n\n\tnpx scope --add\n\n"); - return VerificationStatus.NOT_VERIFIED; - } + // if ([...commitsVerificationInfo.entries()].some(([commitSHA, info]) => { + + for (const [commitSHA, info] of [...commitsVerificationInfo.entries()]) { + if (!info.isSkipped && !info.isMergeCommit && !info.includesOnlyIgnoredFiles) { + const commit = await repository.getCommitByHash(commitSHA); + + const filesWithoutRelevancy = info.filesToTag.filter(file => !info.relevancy.some(relevancyInfo => relevancyInfo.path === file.newPath)); + + console.log(`Commit '${commit.summary()}' not verified, found no relevancy for required files:\n`); + filesWithoutRelevancy.forEach(file => console.log(`- ${file.newPath}`)); + + return VerificationStatus.NOT_VERIFIED; + } + }; return VerificationStatus.VERIFIED; } \ No newline at end of file diff --git a/src/Git/GitRepository.ts b/src/Git/GitRepository.ts index b03aafd..8c0a7d9 100644 --- a/src/Git/GitRepository.ts +++ b/src/Git/GitRepository.ts @@ -118,15 +118,8 @@ export class GitRepository { * https://git-scm.com/docs/git-diff */ - - // exec(`pwd && echo $PAGER && cd ${this._root} && git diff ${commit.sha()}^ ${commit.sha()} --name-status`, (error: any, stdout: any, stderr: any) => { - // console.debug("STDOUT:", stdout, ", STDERR:", stderr); - // }); - const nameStatusOutput = execSync(`cd ${this._root} && git --no-pager diff ${commit.sha()}~ ${commit.sha()} --name-status`).toString().trim().split('\n'); - console.debug(nameStatusOutput); - /** * Has the following format: * 1 1 .vscode/launch.json @@ -312,6 +305,7 @@ export class GitRepository { includesOnlyIgnoredFiles: false, isMergeCommit: false, hasRelevancy: false, + relevancy: [], }; // Check if commit should be skipped @@ -350,6 +344,10 @@ export class GitRepository { // Check relevancy commitInfo.hasRelevancy = relevancyManager.doesCommitMessageHaveRelevancyData(commit.message()); + if (commitInfo.hasRelevancy) { + commitInfo.relevancy = relevancyManager.convertCommitMessageToRelevancyData(commit) + } + return commitInfo; } diff --git a/src/Git/Types.ts b/src/Git/Types.ts index b87c923..1fdc897 100644 --- a/src/Git/Types.ts +++ b/src/Git/Types.ts @@ -1,4 +1,5 @@ import { Commit } from "nodegit/commit"; +import { CommitMessageRelevancyInfo } from "../Relevancy/RelevancyManager"; export type FilePath = string; @@ -48,4 +49,5 @@ export type VerificationInfo = { hasRelevancy: boolean, isMergeCommit: boolean, includesOnlyIgnoredFiles: boolean, + relevancy: Array, } \ No newline at end of file diff --git a/src/Relevancy/RelevancyManager.ts b/src/Relevancy/RelevancyManager.ts index 298ac0e..a5b134b 100644 --- a/src/Relevancy/RelevancyManager.ts +++ b/src/Relevancy/RelevancyManager.ts @@ -101,7 +101,7 @@ export class RelevancyManager { } as CommitMessageRelevancyInfo; }); - // Merge relevancies - if some are duplicates, select those with higher relevancy + // Merge relevancies - if some are duplicates, select those with higher relevancy - TODO: This should be testable -> add test const allRelevancies: CommitMessageRelevancyInfo[] = relevancyArray.concat(relevancyArrayFromCurrentCommit); const mergedRelevancies: CommitMessageRelevancyInfo[] = []; @@ -139,34 +139,47 @@ export class RelevancyManager { } public convertCommitMessageToRelevancyData(commit: Commit, replaceCurrentCommitSHA = true): Array { - const commitMessage = commit.message(); - const prefixStartIndex = commitMessage.indexOf(RelevancyManager.COMMIT_MSG_PREFIX); - const relevancyEndIndex = commitMessage.lastIndexOf(RelevancyManager.COMMIT_MSG_PREFIX); - - if (prefixStartIndex === -1) { - throw new Error(`Commit message '${commitMessage}' does not include relevancy info`); + if (!this.doesCommitMessageHaveRelevancyData(commit.message())) { + throw new Error(`Commit message '${commit.message()}' does not include correct relevancy data`); } - const relevancyJSON = commitMessage.substring(prefixStartIndex + RelevancyManager.COMMIT_MSG_PREFIX.length, relevancyEndIndex); + const info: CommitMessageRelevancyInfo[] = []; - let relevancyInfo = []; + // Check every line, as commit could have multiple relevancies - try { - relevancyInfo = JSON.parse(relevancyJSON) as Array; - } catch (e) { - throw new Error(`Could not parse relevancy data from commit message: '${commitMessage}', found relevancy data: '${relevancyJSON}'`); - } + let currentLine = 0; - // Replace current commit' sha - if (replaceCurrentCommitSHA) { - relevancyInfo.forEach(info => { - if (info.commit === RelevancyManager.CURRENT_COMMIT) { - info.commit = commit.sha(); - } - }); + for (const line of commit.message().split("\n")) { + currentLine++; + + const prefixStartIndex = line.indexOf(RelevancyManager.COMMIT_MSG_PREFIX); + const relevancyEndIndex = line.lastIndexOf(RelevancyManager.COMMIT_MSG_PREFIX); + + if (prefixStartIndex === -1 || relevancyEndIndex === -1 || prefixStartIndex === relevancyEndIndex) { + continue; + } + + const relevancyJSON = line.substring(prefixStartIndex + RelevancyManager.COMMIT_MSG_PREFIX.length, relevancyEndIndex); + + try { + const parsedRelevancy = JSON.parse(relevancyJSON) as Array; + + parsedRelevancy.forEach(relevancy => { + // Replace current commit' sha + if (replaceCurrentCommitSHA) { + if (relevancy.commit === RelevancyManager.CURRENT_COMMIT) { + relevancy.commit = commit.sha(); + } + } + info.push(relevancy) + }); + } catch (e) { + throw new Error(`Could not parse relevancy data from line ${currentLine}: '${line}', found relevancy data: '${relevancyJSON}'`); + } } - return relevancyInfo; + + return info; } // public addRelevancyFromCommit(fileDataRelevancy: Map, commit: Commit) { @@ -177,22 +190,30 @@ export class RelevancyManager { // } public doesCommitMessageHaveRelevancyData(commitMessage: string): boolean { - const prefixStartIndex = commitMessage.indexOf(RelevancyManager.COMMIT_MSG_PREFIX); - const relevancyEndIndex = commitMessage.lastIndexOf(RelevancyManager.COMMIT_MSG_PREFIX); - if (prefixStartIndex === -1 || relevancyEndIndex === -1 || prefixStartIndex === relevancyEndIndex) { - return false; - } + // Check every line, as commit could have multiple relevancies + + let commitMessageHasRelevancy = false; + for (const line of commitMessage.split("\n")) { + const prefixStartIndex = line.indexOf(RelevancyManager.COMMIT_MSG_PREFIX); + const relevancyEndIndex = line.lastIndexOf(RelevancyManager.COMMIT_MSG_PREFIX); - const relevancyJSON = commitMessage.substring(prefixStartIndex + RelevancyManager.COMMIT_MSG_PREFIX.length, relevancyEndIndex); + if (prefixStartIndex === -1 || relevancyEndIndex === -1 || prefixStartIndex === relevancyEndIndex) { + continue; + } + + const relevancyJSON = line.substring(prefixStartIndex + RelevancyManager.COMMIT_MSG_PREFIX.length, relevancyEndIndex); - try { - JSON.parse(relevancyJSON); - return true; - } catch (e) { - return false; + try { + JSON.parse(relevancyJSON); + commitMessageHasRelevancy = true; + } catch (e) { + return false; + } } + + return commitMessageHasRelevancy; } public loadRelevancyMapFromCommits(commits: Commit[]): RelevancyMap { diff --git a/test/commits/relevancy.test.ts b/test/commits/relevancy.test.ts index 2047700..4cc8058 100644 --- a/test/commits/relevancy.test.ts +++ b/test/commits/relevancy.test.ts @@ -12,6 +12,45 @@ const checkRelevancyAndFileData = (info: CommitMessageRelevancyInfo, fileData: F expect(info.commit).toBe(expectedCommitSHA); } +const mockFileData1: FileData = { + oldPath: "src/basic/commands/command.ts", + newPath: "src/basic/commands/command.ts", + change: GitDeltaType.ADDED, + linesAdded: 120, + linesRemoved: 10, + commitedIn: { + sha: () => "sha" + } as Commit +}; + +const mockFileData2: FileData = { + oldPath: "assets/basic/assets/asset.jpg", + newPath: "assets/basic/assets/asset.jpg", + change: GitDeltaType.MODIFIED, + linesAdded: 5, + linesRemoved: 0, + commitedIn: { + sha: () => "sha" + } as Commit +}; + +const mockFileData3: FileData = { + oldPath: "config/basic/config/data.log", + newPath: "config/basic/config/data.log", + change: GitDeltaType.DELETED, + linesAdded: 0, + linesRemoved: 1050, + commitedIn: { + sha: () => "sha" + } as Commit +}; + +const mockRelevancyData = new Map([ + [mockFileData1, Relevancy.LOW], + [mockFileData2, Relevancy.MEDIUM], + [mockFileData3, Relevancy.HIGH], +]); + describe("Relevancy manager tests", () => { it("Reports no relevancy when there is no relevancy in commit message", async () => { @@ -47,41 +86,40 @@ describe("Relevancy manager tests", () => { }) }) - it("Correctly encodes relevancy data in a commit", async () => { + it("Reports relevancy when there is correct relevancy encoded", async () => { const relevancyManager = new RelevancyManager(); - const mockFileData1: FileData = { - oldPath: "src/basic/commands/command.ts", - newPath: "src/basic/commands/command.ts", - change: GitDeltaType.ADDED, - linesAdded: 120, - linesRemoved: 10, - commitedIn: { - sha: () => "sha" - } as Commit - }; - - const mockFileData2: FileData = { - oldPath: "assets/basic/assets/asset.jpg", - newPath: "assets/basic/assets/asset.jpg", - change: GitDeltaType.MODIFIED, - linesAdded: 5, - linesRemoved: 0, - commitedIn: { - sha: () => "sha" - } as Commit - }; - - const mockFileData3: FileData = { - oldPath: "config/basic/config/data.log", - newPath: "config/basic/config/data.log", - change: GitDeltaType.DELETED, - linesAdded: 0, - linesRemoved: 1050, - commitedIn: { - sha: () => "sha" - } as Commit - }; + const commitMessagesWithRelevancy = [ + `[TEST-1234] There is correct relevancy data in this commit message + + __relevancy__[]__relevancy__`, + `[TEST - 1234] There is correct relevancy data in this commit message + + __relevancy__[{"path":"src/basic/commands/command.ts","relevancy":"LOW","commit":"__current__"}]__relevancy__ + `, + `[TEST-1234] This is a commit message with multiple relevancies + + + __relevancy__[{"path":"src/basic/commands/command.ts","relevancy":"LOW","commit":"__current__"}]__relevancy__ + + __relevancy__[{"path":"assets/basic/assets/asset.jpg","relevancy":"MEDIUM","commit":"__current__"},{"path":"config/basic/config/data.log","relevancy":"HIGH","commit":"__current__"}]__relevancy__ + `, + ]; + + commitMessagesWithRelevancy.forEach(message => { + const hasRelevancy = relevancyManager.doesCommitMessageHaveRelevancyData(message); + + if (!hasRelevancy) { + console.debug(message); + } + + expect(hasRelevancy).toBe(true); + }) + }) + + + it("Correctly encodes relevancy data in a commit", async () => { + const relevancyManager = new RelevancyManager(); const mockRelevancyData = new Map([ [mockFileData1, Relevancy.LOW], @@ -96,12 +134,8 @@ describe("Relevancy manager tests", () => { sha: () => "sha", } as Commit; - mockCommit.message - const generatedMessage = relevancyManager.convertRelevancyDataToCommitMessage(mockRelevancyData, mockCommit); - console.debug(generatedMessage); - expect(relevancyManager.doesCommitMessageHaveRelevancyData(generatedMessage)).toBe(true); // Read relevancy back @@ -117,4 +151,33 @@ describe("Relevancy manager tests", () => { checkRelevancyAndFileData(generatedRelevancyData[1], mockFileData2, mockRelevancyData.get(mockFileData2), "sha"); checkRelevancyAndFileData(generatedRelevancyData[2], mockFileData3, mockRelevancyData.get(mockFileData3), "sha"); }) + + it("Correctly reads multiple relevancies from a merge commit", async () => { + const relevancyManager = new RelevancyManager(); + + const mockCommitWithGeneratedRelevancy: Commit = { + message: () => `[TEST-1234] This is a commit message with multiple relevancies + + + + + __relevancy__[{"path":"src/basic/commands/command.ts","relevancy":"LOW","commit":"__current__"}]__relevancy__ + + __relevancy__[{"path":"assets/basic/assets/asset.jpg","relevancy":"MEDIUM","commit":"__current__"},{"path":"config/basic/config/data.log","relevancy":"HIGH","commit":"__current__"}]__relevancy__ + `, + sha: () => "sha", + } as Commit; + + expect(relevancyManager.doesCommitMessageHaveRelevancyData(mockCommitWithGeneratedRelevancy.message())).toBe(true); + + // Read relevancy back + + const generatedRelevancyData: CommitMessageRelevancyInfo[] = relevancyManager.convertCommitMessageToRelevancyData(mockCommitWithGeneratedRelevancy, true); + + expect(generatedRelevancyData.length).toBe(3); + + checkRelevancyAndFileData(generatedRelevancyData[0], mockFileData1, mockRelevancyData.get(mockFileData1), "sha"); + checkRelevancyAndFileData(generatedRelevancyData[1], mockFileData2, mockRelevancyData.get(mockFileData2), "sha"); + checkRelevancyAndFileData(generatedRelevancyData[2], mockFileData3, mockRelevancyData.get(mockFileData3), "sha"); + }) });