Skip to content

Commit

Permalink
Implement parsing of short issue links
Browse files Browse the repository at this point in the history
  • Loading branch information
maxim-lobanov committed Feb 13, 2023
1 parent 27ef05b commit 2b71df2
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 87 deletions.
77 changes: 48 additions & 29 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -330,35 +330,24 @@ 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;
this.title = title;
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) {
Expand Down Expand Up @@ -447,7 +436,7 @@ ${renderedGraphIssues}
`;
}
renderIssue(issue) {
const title = issue.getWrappedTitle();
const title = issue.getFormattedTitle();
const linkedTitle = issue.url
? `<a href='${issue.url}' style='text-decoration:none;color: inherit;'>${title}</a>`
: title;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;


/***/ }),
Expand Down
6 changes: 3 additions & 3 deletions src/issue-content-parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GitHubIssue, GitHubIssueReference } from "./models";
import { GitHubIssue, GitHubIssueReference, GitHubRepoReference } from "./models";
import { parseIssuesUrls, parseIssueUrl } from "./utils";

export class IssueContentParser {
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const run = async (): Promise<void> => {
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));
}
Expand Down
23 changes: 6 additions & 17 deletions src/mermaid-node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GitHubIssue } from "./models";
import { wrapString } from "./utils";

export type MermaidNodeStatus = "default" | "notstarted" | "started" | "completed";

Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/mermaid-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ ${renderedGraphIssues}
}

private renderIssue(issue: MermaidNode): string {
const title = issue.getWrappedTitle();
const title = issue.getFormattedTitle();
const linkedTitle = issue.url
? `<a href='${issue.url}' style='text-decoration:none;color: inherit;'>${title}</a>`
: title;
Expand Down
5 changes: 4 additions & 1 deletion src/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export type GitHubIssueReference = {
export type GitHubRepoReference = {
repoOwner: string;
repoName: string;
};

export type GitHubIssueReference = GitHubRepoReference & {
issueNumber: number;
};

Expand Down
49 changes: 41 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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<T> = { [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);
Expand All @@ -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;
Expand Down
41 changes: 34 additions & 7 deletions tests/issue-content-parser.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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([]);
});

Expand All @@ -101,15 +103,15 @@ 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([]);
});

it("single dependency line with single issue", () => {
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 }]);
});

Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
]);
});
});
Expand Down
Loading

0 comments on commit 2b71df2

Please sign in to comment.