Skip to content

Commit

Permalink
chore: init link pulls logic and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
0x4007 committed Feb 18, 2024
1 parent 1f4ba00 commit 7524e6c
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@cspell/dict-node": "^4.0.3",
"@cspell/dict-software-terms": "^3.3.17",
"@cspell/dict-typescript": "^3.1.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.0",
"@types/yargs": "^17.0.32",
"@typescript-eslint/eslint-plugin": "^6.13.1",
Expand Down
27 changes: 27 additions & 0 deletions src/data-collection/link-pulls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import linkPulls from "./link-pulls";

describe("linkPulls", () => {
it("should return null if there is no link event on the issue", async () => {
const issue = {
/* mock issue object */
};
const result = await linkPulls(issue);
expect(result).toBeNull();
});

it("should find the linked pull request in the current repository", async () => {
const issue = {
/* mock issue object with a link event */
};
const result = await linkPulls(issue);
expect(result).toEqual(/* expected linked pull request */);
});

it("should search across other repositories if linked pull request is not found in the current repository", async () => {
const issue = {
/* mock issue object with a link event */
};
const result = await linkPulls(issue);
expect(result).toEqual(/* expected linked pull request from other repositories */);
});
});
113 changes: 113 additions & 0 deletions src/data-collection/link-pulls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { getOctokitInstance } from "../get-authentication-token";
import { GitHubIssueEvent } from "../github-types";
import { IssueParams } from "../start";

const octokit = getOctokitInstance();

export default async function linkPulls(issue: IssueParams) {
/**
* we need to find the linked pull request.
* * we first start by checking the current repository.
* * for example, if the issue is in repository A and the pull request is opened against repository A,
* * then we can look for the pull request events, based on most recently updated pull requests,
* * for a timestamp match on a connected event.
*/

const issueLinkEvents = await getLinkedEvents(issue);
const latestIssueLinkEvent = getLatestLinkEvent(issueLinkEvents);
if (latestIssueLinkEvent) {
const linkedPullRequest = await findMatchingLinkEventFromPullRequests(issue, latestIssueLinkEvent.created_at);
if (!linkedPullRequest) {
// we need to search across the other repositories in the organization
// get all the repositories in the organization, sorted by most recently updated via pull requests
// then search for the linked pull request event from those pull requests.
return await findMatchingLinkEventFromOtherRepositories(issue, latestIssueLinkEvent.created_at);
}
} else {
// there is no link event on the issue so no need to search.
return null;
}
}

async function findMatchingLinkEventFromOtherRepositories(issue: IssueParams, timestamp: string) {
const orgRepos = await octokit.repos.listForOrg({ org: issue.owner, sort: "updated", direction: "desc" });

for (const repo of orgRepos.data) {
const otherRepoPullRequests = await octokit.pulls.list(
// { owner: issue.owner, repo: repo.name, sort: "updated", direction: "desc", state: "closed" }

{ owner: issue.owner, repo: repo.name, state: "closed", sort: "updated", direction: "desc", per_page: 10 }
);

for (const pullRequest of otherRepoPullRequests.data) {
const pullRequestLinkEvents = await getLinkedEvents({ owner: issue.owner, repo: repo.name, issue_number: pullRequest.number });
const latestPullRequestLinkEvent = getLatestLinkEvent(pullRequestLinkEvents);
if (latestPullRequestLinkEvent && latestPullRequestLinkEvent.created_at === timestamp) {
return pullRequest;
}
}
}

return null;
}

async function getLinkedEvents(params: IssueParams) {
const issueEvents = await fetchEvents(params);
const linkEvents = issueEvents.filter((event) => connectedOrCrossReferenced(event));
if (linkEvents.length === 0) {
return [];
}
return linkEvents;
}

function getLatestLinkEvent(events: GitHubIssueEvent[]) {
if (events.length === 0) {
return null;
} else {
return events.shift();
}
}

// make sure to find a matching event in the list of pull requests. go through each one at a time.
// if the pull request is not in the same repository, we need to find the repository where the pull request was opened.
async function findMatchingLinkEventFromPullRequests(params: IssueParams, timestamp: string) {
// this searches the entire first page of results for matching link events.
const pullRequests = await getClosedPullRequests(params);

for (const pullRequest of pullRequests) {
const pullRequestEvents = await fetchEvents({
owner: pullRequest.base.repo.owner.login,
repo: pullRequest.base.repo.name,
issue_number: pullRequest.number,
});
const connectedEvents = pullRequestEvents.filter((event) => connectedOrCrossReferenced(event) && event.created_at === timestamp);
const disconnectedEvents = pullRequestEvents.filter((event) => event.event === "disconnected");
if (connectedEvents.length > 0 && disconnectedEvents.length === 0) {
return pullRequest;
}
}

return null;
}

function connectedOrCrossReferenced(event: GitHubIssueEvent) {
return event.event === "connected" || event.event === "cross-referenced";
}

async function fetchEvents(params: IssueParams): Promise<GitHubIssueEvent[]> {
const response = await octokit.rest.issues.listEvents(params);
return response.data;
}

async function getClosedPullRequests(params: IssueParams) {
// when the linked pull request is merged, it is automatically closed
const response = await octokit.rest.pulls.list({
owner: params.owner,
repo: params.repo,
state: "closed",
sort: "updated",
direction: "desc",
per_page: 10,
});
return response.data;
}
3 changes: 3 additions & 0 deletions src/get-authentication-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ let octokitInstance: Octokit | null = null;

function getAuthenticationToken(): string {
const argv = parse(process.argv.slice(2));
if (!argv.auth) {
throw new Error("No authentication token provided");
}
return argv.auth as string;
}

Expand Down
4 changes: 3 additions & 1 deletion src/github-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RestEndpointMethodTypes } from "@octokit/rest";

export type GitHubIssue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"];
export type GitHubPullRequest = RestEndpointMethodTypes["pulls"]["get"]["response"]["data"];
export type GitHubComment = RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][0];
export type GitHubLabel = RestEndpointMethodTypes["issues"]["listLabelsOnIssue"]["response"]["data"][0];
export type GitHubLabel = RestEndpointMethodTypes["issues"]["listLabelsOnIssue"]["response"]["data"][0];
export type GitHubIssueEvent = RestEndpointMethodTypes["issues"]["listEvents"]["response"]["data"][0];
28 changes: 23 additions & 5 deletions src/start.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getOctokitInstance } from "./get-authentication-token";
import { GitHubIssue } from "./github-types";

function main(gitHubIssueId: GitHubIssue["id"]) {
const issue = getIssue(gitHubIssueId);
async function main(gitHubIssueUrl: GitHubIssue["html_url"]) {
const issueParams = parseGitHubUrl(gitHubIssueUrl);
const issue = await getIssue(issueParams);
const pullRequest = getLinkedPullRequest(issue);
const users = getUsers(issue, pullRequest);

Expand Down Expand Up @@ -33,12 +35,28 @@ function main(gitHubIssueId: GitHubIssue["id"]) {
*/
}

function getIssue(issueId: GitHubIssue["id"]) {}
async function getIssue(params: IssueParams): Promise<GitHubIssue> {
const octokit = getOctokitInstance();
return (await octokit.issues.get(params)).data;
}

function getLinkedPullRequest(issue: GitHubIssue) {
// ...
// this needs to see all of the events that happened on the issue and filter for "connected" or "cross-referenced" events


}

function getUsers(issue: GitHubIssue, pullRequest: GitHubIssue) {
function getUsers(issue: GitHubIssue, pullRequest: null | GitHubIssue) {
// ...
}

function parseGitHubUrl(url: string): { owner: string; repo: string; issue_number: number } {
const path = new URL(url).pathname.split("/");
return {
owner: path[1],
repo: path[2],
issue_number: Number(path[4]),
};
}

export type IssueParams = ReturnType<typeof parseGitHubUrl>;
Loading

0 comments on commit 7524e6c

Please sign in to comment.