diff --git a/dist/index.js b/dist/index.js index b27604a..bd93d46 100644 --- a/dist/index.js +++ b/dist/index.js @@ -173,11 +173,11 @@ class IssueContentParser { .map(x => (0, utils_1.parseIssueUrl)(x)) .filter((x) => x !== null); } - extractIssueDependencies(issue) { + extractIssueDependencies(issue, repoRef) { const contentLines = issue.body?.split("\n") ?? []; return contentLines .filter(x => this.isDependencyLine(x)) - .map(x => (0, utils_1.parseIssuesUrls)(x)) + .map(x => (0, utils_1.parseIssuesUrls)(x, repoRef)) .flat() .filter((x) => x !== null); } @@ -291,7 +291,7 @@ const run = async () => { for (const issueRef of rootIssueTasklist) { const issue = await githubApiClient.getIssue(issueRef); const issueDetails = mermaid_node_1.MermaidNode.createFromGitHubIssue(issue); - const issueDependencies = issueContentParser.extractIssueDependencies(issue); + const issueDependencies = issueContentParser.extractIssueDependencies(issue, issueRef); graphBuilder.addIssue(issueRef, issueDetails); issueDependencies.forEach(x => graphBuilder.addDependency(x, issueRef)); } @@ -330,12 +330,13 @@ run(); /***/ }), /***/ 235: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.MermaidNode = void 0; +const utils_1 = __nccwpck_require__(918); class MermaidNode { constructor(nodeId, title, status, url) { this.nodeId = nodeId; @@ -343,22 +344,10 @@ class MermaidNode { this.status = status; this.url = url; } - getWrappedTitle() { - const maxWidth = 40; - const words = this.title.split(/\s+/); - let result = words[0]; - let lastLength = result.length; - for (let wordIndex = 1; wordIndex < words.length; wordIndex++) { - if (lastLength + words[wordIndex].length >= maxWidth) { - result += "\n"; - lastLength = 0; - } - else { - result += " "; - } - result += words[wordIndex]; - lastLength += words[wordIndex].length; - } + getFormattedTitle() { + let result = this.title; + result = result.replaceAll('"', "'"); + result = (0, utils_1.wrapString)(result, 40); return result; } static createFromGitHubIssue(issue) { @@ -447,7 +436,7 @@ ${renderedGraphIssues} `; } renderIssue(issue) { - const title = issue.getWrappedTitle(); + const title = issue.getFormattedTitle(); const linkedTitle = issue.url ? `${title}` : title; @@ -478,9 +467,10 @@ exports.MermaidRender = MermaidRender; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.parseIssuesUrls = exports.parseIssueUrl = void 0; +exports.wrapString = exports.parseIssuesUrls = exports.parseIssueNumber = exports.parseIssueUrl = void 0; const issueUrlRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)$/i; -const issueUrlsRegex = /github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/gi; +const issueNumberRegex = /^#(\d+)$/; +const issueUrlsRegex = /https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)|#\d+/gi; const parseIssueUrl = (str) => { const found = str.trim().match(issueUrlRegex); if (!found) { @@ -493,18 +483,47 @@ const parseIssueUrl = (str) => { }; }; exports.parseIssueUrl = parseIssueUrl; -const parseIssuesUrls = (str) => { +const parseIssueNumber = (str, repoRef) => { + const found = str.trim().match(issueNumberRegex); + if (!found) { + return null; + } + return { + repoOwner: repoRef.repoOwner, + repoName: repoRef.repoName, + issueNumber: parseInt(found[1]), + }; +}; +exports.parseIssueNumber = parseIssueNumber; +const parseIssuesUrls = (str, repoRef) => { const result = []; for (const match of str.matchAll(issueUrlsRegex)) { - result.push({ - repoOwner: match[1], - repoName: match[2], - issueNumber: parseInt(match[3]), - }); + const parsedIssue = (0, exports.parseIssueUrl)(match[0]) || (0, exports.parseIssueNumber)(match[0], repoRef); + if (parsedIssue) { + result.push(parsedIssue); + } } return result; }; exports.parseIssuesUrls = parseIssuesUrls; +const wrapString = (str, maxWidth) => { + const words = str.split(/\s+/); + let result = words[0]; + let lastLength = result.length; + for (let wordIndex = 1; wordIndex < words.length; wordIndex++) { + if (lastLength + words[wordIndex].length >= maxWidth) { + result += "\n"; + lastLength = 0; + } + else { + result += " "; + } + result += words[wordIndex]; + lastLength += words[wordIndex].length; + } + return result; +}; +exports.wrapString = wrapString; /***/ }), diff --git a/src/issue-content-parser.ts b/src/issue-content-parser.ts index 8b05ab7..88ba9cc 100644 --- a/src/issue-content-parser.ts +++ b/src/issue-content-parser.ts @@ -1,4 +1,4 @@ -import { GitHubIssue, GitHubIssueReference } from "./models"; +import { GitHubIssue, GitHubIssueReference, GitHubRepoReference } from "./models"; import { parseIssuesUrls, parseIssueUrl } from "./utils"; export class IssueContentParser { @@ -12,12 +12,12 @@ export class IssueContentParser { .filter((x): x is GitHubIssueReference => x !== null); } - public extractIssueDependencies(issue: GitHubIssue): GitHubIssueReference[] { + public extractIssueDependencies(issue: GitHubIssue, repoRef: GitHubRepoReference): GitHubIssueReference[] { const contentLines = issue.body?.split("\n") ?? []; return contentLines .filter(x => this.isDependencyLine(x)) - .map(x => parseIssuesUrls(x)) + .map(x => parseIssuesUrls(x, repoRef)) .flat() .filter((x): x is GitHubIssueReference => x !== null); } diff --git a/src/main.ts b/src/main.ts index b81185c..9a91d7c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,7 +31,7 @@ const run = async (): Promise => { for (const issueRef of rootIssueTasklist) { const issue = await githubApiClient.getIssue(issueRef); const issueDetails = MermaidNode.createFromGitHubIssue(issue); - const issueDependencies = issueContentParser.extractIssueDependencies(issue); + const issueDependencies = issueContentParser.extractIssueDependencies(issue, issueRef); graphBuilder.addIssue(issueRef, issueDetails); issueDependencies.forEach(x => graphBuilder.addDependency(x, issueRef)); } diff --git a/src/mermaid-node.ts b/src/mermaid-node.ts index 39fd3b0..0863e5e 100644 --- a/src/mermaid-node.ts +++ b/src/mermaid-node.ts @@ -1,4 +1,5 @@ import { GitHubIssue } from "./models"; +import { wrapString } from "./utils"; export type MermaidNodeStatus = "default" | "notstarted" | "started" | "completed"; @@ -10,23 +11,11 @@ export class MermaidNode { public readonly url?: string ) {} - public getWrappedTitle(): string { - const maxWidth = 40; - const words = this.title.split(/\s+/); - - let result = words[0]; - let lastLength = result.length; - for (let wordIndex = 1; wordIndex < words.length; wordIndex++) { - if (lastLength + words[wordIndex].length >= maxWidth) { - result += "\n"; - lastLength = 0; - } else { - result += " "; - } - - result += words[wordIndex]; - lastLength += words[wordIndex].length; - } + public getFormattedTitle(): string { + let result = this.title; + + result = result.replaceAll('"', "'"); + result = wrapString(result, 40); return result; } diff --git a/src/mermaid-render.ts b/src/mermaid-render.ts index 0143099..2815c68 100644 --- a/src/mermaid-render.ts +++ b/src/mermaid-render.ts @@ -61,7 +61,7 @@ ${renderedGraphIssues} } private renderIssue(issue: MermaidNode): string { - const title = issue.getWrappedTitle(); + const title = issue.getFormattedTitle(); const linkedTitle = issue.url ? `${title}` : title; diff --git a/src/models.ts b/src/models.ts index 755a567..83e3a2d 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,6 +1,9 @@ -export type GitHubIssueReference = { +export type GitHubRepoReference = { repoOwner: string; repoName: string; +}; + +export type GitHubIssueReference = GitHubRepoReference & { issueNumber: number; }; diff --git a/src/utils.ts b/src/utils.ts index b0f48d9..0c634a2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,11 @@ -import { GitHubIssueReference } from "./models"; +import { GitHubIssueReference, GitHubRepoReference } from "./models"; // Analogue of TypeScript "Partial" type but for null values export type NullablePartial = { [P in keyof T]: T[P] | null | undefined }; const issueUrlRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)$/i; -const issueUrlsRegex = /github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/gi; +const issueNumberRegex = /^#(\d+)$/; +const issueUrlsRegex = /https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)|#\d+/gi; export const parseIssueUrl = (str: string): GitHubIssueReference | null => { const found = str.trim().match(issueUrlRegex); @@ -19,15 +20,47 @@ export const parseIssueUrl = (str: string): GitHubIssueReference | null => { }; }; -export const parseIssuesUrls = (str: string): GitHubIssueReference[] => { +export const parseIssueNumber = (str: string, repoRef: GitHubRepoReference): GitHubIssueReference | null => { + const found = str.trim().match(issueNumberRegex); + if (!found) { + return null; + } + + return { + repoOwner: repoRef.repoOwner, + repoName: repoRef.repoName, + issueNumber: parseInt(found[1]), + }; +}; + +export const parseIssuesUrls = (str: string, repoRef: GitHubRepoReference): GitHubIssueReference[] => { const result: GitHubIssueReference[] = []; for (const match of str.matchAll(issueUrlsRegex)) { - result.push({ - repoOwner: match[1], - repoName: match[2], - issueNumber: parseInt(match[3]), - }); + const parsedIssue = parseIssueUrl(match[0]) || parseIssueNumber(match[0], repoRef); + if (parsedIssue) { + result.push(parsedIssue); + } + } + + return result; +}; + +export const wrapString = (str: string, maxWidth: number): string => { + const words = str.split(/\s+/); + + let result = words[0]; + let lastLength = result.length; + for (let wordIndex = 1; wordIndex < words.length; wordIndex++) { + if (lastLength + words[wordIndex].length >= maxWidth) { + result += "\n"; + lastLength = 0; + } else { + result += " "; + } + + result += words[wordIndex]; + lastLength += words[wordIndex].length; } return result; diff --git a/tests/issue-content-parser.test.ts b/tests/issue-content-parser.test.ts index aa3d65d..360c33a 100644 --- a/tests/issue-content-parser.test.ts +++ b/tests/issue-content-parser.test.ts @@ -1,5 +1,5 @@ import { IssueContentParser } from "../src/issue-content-parser"; -import { GitHubIssue } from "../src/models"; +import { GitHubIssue, GitHubRepoReference } from "../src/models"; describe("IssueContentParser", () => { const issueContentParser = new IssueContentParser(); @@ -83,9 +83,11 @@ Test content 2 }); describe("extractIssueDependencies", () => { + const repoRef: GitHubRepoReference = { repoOwner: "testOwner", repoName: "testRepo" }; + it("empty body", () => { const issue = { body: undefined } as GitHubIssue; - const actual = issueContentParser.extractIssueDependencies(issue); + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); expect(actual).toEqual([]); }); @@ -101,7 +103,7 @@ https://github.com/actions/setup-node/issues/4 Test content 3 `, } as GitHubIssue; - const actual = issueContentParser.extractIssueDependencies(issue); + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); expect(actual).toEqual([]); }); @@ -109,7 +111,7 @@ Test content 3 const issue = { body: "## Hello\nDepends on https://github.com/actions/setup-node/issues/5663\nTest", } as GitHubIssue; - const actual = issueContentParser.extractIssueDependencies(issue); + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); expect(actual).toEqual([{ repoOwner: "actions", repoName: "setup-node", issueNumber: 5663 }]); }); @@ -123,7 +125,7 @@ Depends on https://github.com/actions/setup-node/issues/105, https://github.com/ Test content `, } as GitHubIssue; - const actual = issueContentParser.extractIssueDependencies(issue); + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); expect(actual).toEqual([ { repoOwner: "actions", repoName: "setup-node", issueNumber: 105 }, { repoOwner: "actions", repoName: "setup-python", issueNumber: 115 }, @@ -143,7 +145,7 @@ Depends on https://github.com/actions/setup-ruby/issues/105 & https://github.com Test content `, } as GitHubIssue; - const actual = issueContentParser.extractIssueDependencies(issue); + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); expect(actual).toEqual([ { repoOwner: "actions", repoName: "setup-node", issueNumber: 101 }, { repoOwner: "actions", repoName: "setup-node", issueNumber: 102 }, @@ -166,11 +168,36 @@ Dependencies: https://github.com/actions/setup-node/issues/103 Test content `, } as GitHubIssue; - const actual = issueContentParser.extractIssueDependencies(issue); + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); + expect(actual).toEqual([ + { repoOwner: "actions", repoName: "setup-node", issueNumber: 101 }, + { repoOwner: "actions", repoName: "setup-node", issueNumber: 102 }, + { repoOwner: "actions", repoName: "setup-node", issueNumber: 103 }, + ]); + }); + + it("diffent types of issues referencing", () => { + const issue = { + body: ` +Hello + +Depends on https://github.com/actions/setup-node/issues/101 +depends on: https://github.com/actions/setup-node/issues/102 +Dependencies: https://github.com/actions/setup-node/issues/103 +Depends on: #123, #456, https://github.com/actions/setup-node/issues/105, #701 + +Test content +`, + } as GitHubIssue; + const actual = issueContentParser.extractIssueDependencies(issue, repoRef); expect(actual).toEqual([ { repoOwner: "actions", repoName: "setup-node", issueNumber: 101 }, { repoOwner: "actions", repoName: "setup-node", issueNumber: 102 }, { repoOwner: "actions", repoName: "setup-node", issueNumber: 103 }, + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 123 }, + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 456 }, + { repoOwner: "actions", repoName: "setup-node", issueNumber: 105 }, + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 701 }, ]); }); }); diff --git a/tests/mermaid-node.test.ts b/tests/mermaid-node.test.ts index 471366e..0bcb4c1 100644 --- a/tests/mermaid-node.test.ts +++ b/tests/mermaid-node.test.ts @@ -55,29 +55,18 @@ describe("MermaidNode", () => { }); }); - describe("getWrappedTitle", () => { + describe("getFormattedTitle", () => { it.each([ ["", ""], ["hello world", "hello world"], - [ - "Integrate software report diff module into macOS Monterey pipeline and validate deployment pipeline end-to-end", - "Integrate software report diff module into\nmacOS Monterey pipeline and validate\ndeployment pipeline end-to-end", - ], [ "Onboard Linux image generation to new software report module", "Onboard Linux image generation to new\nsoftware report module", ], - [ - "Integrate auxiliary release scripts into Windows / Linux deployment pipelines", - "Integrate auxiliary release scripts into\nWindows / Linux deployment pipelines", - ], - [ - "Implement unit tests and e2e tests for software report diff module", - "Implement unit tests and e2e tests for\nsoftware report diff module", - ], + ['Update link "Learn more" with new link', "Update link 'Learn more' with new link"], ])("case %#", (input: string, expected: string) => { const node = new MermaidNode("issue", input, "notstarted"); - const actual = node.getWrappedTitle(); + const actual = node.getFormattedTitle(); expect(actual).toBe(expected); }); }); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index a6c65d6..a2d8423 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,4 +1,5 @@ -import { parseIssuesUrls, parseIssueUrl } from "../src/utils"; +import { GitHubRepoReference } from "../src/models"; +import { parseIssueNumber, parseIssuesUrls, parseIssueUrl, wrapString } from "../src/utils"; describe("parseIssueUrl", () => { it("invalid site host", () => { @@ -46,15 +47,41 @@ describe("parseIssueUrl", () => { }); }); +describe("parseIssueNumber", () => { + const repoRef: GitHubRepoReference = { repoOwner: "testOwner", repoName: "testRepo" }; + + it("empty string", () => { + const actual = parseIssueNumber("", repoRef); + expect(actual).toBeNull(); + }); + + it("invalid format", () => { + const actual = parseIssueNumber("#abc", repoRef); + expect(actual).toBeNull(); + }); + + it("parses issue number correctly", () => { + const actual = parseIssueNumber("#123", repoRef); + expect(actual).toStrictEqual({ + repoOwner: "testOwner", + repoName: "testRepo", + issueNumber: 123, + }); + }); +}); + describe("parseIssuesUrls", () => { + const repoRef: GitHubRepoReference = { repoOwner: "testOwner", repoName: "testRepo" }; + it("parses single issue url", () => { - const actual = parseIssuesUrls("https://github.com/actions/setup-node/issues/5663"); + const actual = parseIssuesUrls("https://github.com/actions/setup-node/issues/5663", repoRef); expect(actual).toStrictEqual([{ repoOwner: "actions", repoName: "setup-node", issueNumber: 5663 }]); }); it("parses multiple issues urls", () => { const actual = parseIssuesUrls( - "https://github.com/actions/setup-node/issues/110 https://github.com/actions/setup-go/issues/115 https://github.com/actions/setup-go/issues/120" + "https://github.com/actions/setup-node/issues/110 https://github.com/actions/setup-go/issues/115 https://github.com/actions/setup-go/issues/120", + repoRef ); expect(actual).toStrictEqual([ { repoOwner: "actions", repoName: "setup-node", issueNumber: 110 }, @@ -65,7 +92,8 @@ describe("parseIssuesUrls", () => { it("parses multiple comma separated issues urls", () => { const actual = parseIssuesUrls( - "https://github.com/actions/setup-node/issues/110, https://github.com/actions/setup-go/issues/115" + "https://github.com/actions/setup-node/issues/110, https://github.com/actions/setup-go/issues/115", + repoRef ); expect(actual).toStrictEqual([ { repoOwner: "actions", repoName: "setup-node", issueNumber: 110 }, @@ -75,18 +103,71 @@ describe("parseIssuesUrls", () => { it("parses multiple issues urls with additional words", () => { const actual = parseIssuesUrls( - "Depends on: https://github.com/actions/setup-node/issues/110, https://github.com/actions/setup-go/issues/115" + "Depends on: https://github.com/actions/setup-node/issues/110, https://github.com/actions/setup-go/issues/115", + repoRef + ); + expect(actual).toStrictEqual([ + { repoOwner: "actions", repoName: "setup-node", issueNumber: 110 }, + { repoOwner: "actions", repoName: "setup-go", issueNumber: 115 }, + ]); + }); + + it("parses multiple issue numbers with additional words", () => { + const actual = parseIssuesUrls("Depends on: #123, gawgaw #213 afaaw", repoRef); + expect(actual).toStrictEqual([ + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 123 }, + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 213 }, + ]); + }); + + it("parses multiple issues in different formats", () => { + const actual = parseIssuesUrls( + "Depends on: https://github.com/actions/setup-node/issues/110, #123, gawgaw #213 afaaw, https://github.com/actions/setup-go/issues/115", + repoRef ); expect(actual).toStrictEqual([ { repoOwner: "actions", repoName: "setup-node", issueNumber: 110 }, + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 123 }, + { repoOwner: "testOwner", repoName: "testRepo", issueNumber: 213 }, { repoOwner: "actions", repoName: "setup-go", issueNumber: 115 }, ]); }); it("no valid urls found", () => { const actual = parseIssuesUrls( - "https://github.com/actions/setup-node/, https://github.com/actions/setup-go/issues/ https://github.com/actions/setup-go/issues/fake" + "https://github.com/actions/setup-node/, https://github.com/actions/setup-go/issues/ https://github.com/actions/setup-go/issues/fake", + repoRef ); expect(actual).toStrictEqual([]); }); }); + +describe("wrapString", () => { + it.each([ + ["", 40, ""], + ["hello world", 40, "hello world"], + [ + "Integrate software report diff module into macOS Monterey pipeline and validate deployment pipeline end-to-end", + 40, + "Integrate software report diff module into\nmacOS Monterey pipeline and validate\ndeployment pipeline end-to-end", + ], + [ + "Onboard Linux image generation to new software report module", + 40, + "Onboard Linux image generation to new\nsoftware report module", + ], + [ + "Integrate auxiliary release scripts into Windows / Linux deployment pipelines", + 40, + "Integrate auxiliary release scripts into\nWindows / Linux deployment pipelines", + ], + [ + "Implement unit tests and e2e tests for software report diff module", + 40, + "Implement unit tests and e2e tests for\nsoftware report diff module", + ], + ])("case %#", (input: string, maxWidth: number, expected: string) => { + const actual = wrapString(input, maxWidth); + expect(actual).toBe(expected); + }); +});