Skip to content
This repository has been archived by the owner on Sep 19, 2024. It is now read-only.

Blame #778

Open
wants to merge 7 commits into
base: development
Choose a base branch
from
Open

Blame #778

Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions src/handlers/comment/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import {
getTokenSymbol,
getAllIssueAssignEvents,
calculateWeight,
getAllPullRequests,
} from "../../../helpers";
import { getBotConfig, getBotContext, getLogger } from "../../../bindings";
import { handleIssueClosed } from "../../payout";
import { query } from "./query";
import { autoPay } from "./payout";
import { getTargetPriceLabel } from "../../shared";
import { ErrorDiff } from "../../../utils/helpers";
import { lastActivityTime } from "../../wildcard";

export * from "./assign";
export * from "./wallet";
Expand Down Expand Up @@ -117,6 +119,185 @@ export const issueCreatedCallback = async (): Promise<void> => {
}
};

/**
* Callback for issues reopened - Blame Processor
* @notice Identifies the changes in main that broke the features of the issue
* @notice This is to assign responsibility to the person who broke the feature
* @dev The person in fault will be penalized...
*/
export const issueReopenedBlameCallback = async (): Promise<void> => {
const logger = getLogger();
const context = getBotContext();
// const config = getBotConfig();
const payload = context.payload as Payload;
const issue = payload.issue;
const repository = payload.repository;

if (!issue) return;
if (!repository) return;

const allRepoCommits = await context.octokit.repos
.listCommits({
owner: repository.owner.login,
repo: repository.name,
})
.then((res) => res.data);

const currentCommit = allRepoCommits[0];
const currentCommitSha = currentCommit.sha;
const lastActivity = await lastActivityTime(issue, await getAllIssueComments(issue.number));

const allClosedPulls = await getAllPullRequests(context, "closed");
const mergedPulls = allClosedPulls.filter((pull) => pull.merged_at && pull.merged_at > lastActivity.toISOString());
const mergedSHAs = mergedPulls.map((pull) => pull.merge_commit_sha);
const commitsThatMatch = allRepoCommits.filter((commit) => mergedSHAs.includes(commit.sha)).reverse();

const pullsThatCommitsMatch = await Promise.all(
commitsThatMatch.map((commit) =>
context.octokit.repos
.listPullRequestsAssociatedWithCommit({
owner: repository.owner.login,
repo: repository.name,
commit_sha: commit.sha,
})
.then((res) => res.data)
)
);

const onlyPRsNeeded = pullsThatCommitsMatch.map((pulls) => pulls.map((pull) => pull.number)).reduce((acc, val) => acc.concat(val), []);

const issueRegex = new RegExp(`#${issue.number}`, "g");
const matchingPull = mergedPulls.find((pull) => pull.body?.match(issueRegex));

if (!matchingPull) {
logger.info(`No matching pull found for issue #${issue.number}`);
return;
}

const pullDiff = await context.octokit.repos
.compareCommitsWithBasehead({
owner: repository.owner.login,
repo: repository.name,
basehead: matchingPull?.merge_commit_sha + "..." + currentCommitSha,
mediaType: {
format: "diff",
},
})
.then((res) => res.data);

const diffs = [];
const fileLens: number[] = [];

for (const sha of mergedSHAs) {
if (!sha) continue;
const diff = await context.octokit.repos
.compareCommitsWithBasehead({
owner: repository.owner.login,
repo: repository.name,
basehead: sha + "..." + currentCommitSha,
})
.then((res) => res.data);

const fileLen = diff.files?.length;

fileLens.push(fileLen || 0);
diffs.push(diff);

if (diff.files && diff.files.length > 0) {
logger.info(`Found ${diff.files.length} files changed in commit ${sha}`);
} else {
logger.info(`No files changed in commit ${sha}`);
}
}

if (pullDiff.files && pullDiff.files.length > 0) {
logger.info(`Found ${pullDiff.files.length} files changed in commit ${matchingPull?.merge_commit_sha}`);
}

const matchingSlice = matchingPull?.merge_commit_sha?.slice(0, 8);
const currentSlice = currentCommitSha.slice(0, 8);

const twoDotUrl = `<code>[${matchingSlice}..${currentSlice}](${repository.html_url}/compare/${matchingPull?.merge_commit_sha}..${currentCommitSha})</code>`;

interface Blamer {
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
author: string;
count: number;
}

const blamers: Blamer[] = [];

for (const diff of diffs) {
if (!diff.files) continue;
for (const file of diff.files) {
const linesChanged = file.patch?.split("\n").filter((line) => line.startsWith("+")).length;
const author = diff.base_commit?.author?.login;
const blamer = blamers.find((b) => b.author === author);
if (blamer) {
blamer.count += linesChanged || 0;
} else {
blamers.push({ author: author || "", count: linesChanged || 0 });
}
}
}

const advancedBlameTable = `
| **Blame** | **Count** | **%** |
| --- | --- | --- |
${Array.from(new Set(blamers))
.filter((blamer) => blamer.count > 0)
.sort((a, b) => b.count - a.count)
.map((blamer) => {
const linesChanged = blamer.count;
const totalLinesChanged = blamers.reduce((acc, val) => acc + val.count, 0);
const percent = Math.round((linesChanged / totalLinesChanged) * 100);
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
return `| ${blamer.author} | ${blamer.count} | ${percent}% |`;
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
})
.join("\n")}
`;

const blameQuantifier = blamers.length > 1 ? "suspects" : "suspect";

const comment = `
<details>
<summary>Merged Pulls Since Issue Close</summary><br/>

${onlyPRsNeeded
.sort()
.map((pullNumber) => `\n<ul><li>#${pullNumber}</li></ul>`)
.join("\n")}
</details>


<details>
<summary>Merged Commits Since Issue Close</summary><br/>
${diffs
// @ts-expect-error - diff is unused
.map((diff, i) => {
const fileLen = fileLens[i];
const sha = mergedSHAs[i];
const slice = sha?.slice(0, 7);
const url = `${repository.html_url}/commit/${sha}`;
return `\n<code><a href="${url}">${slice}</a></code> - ${fileLen} files changed`;
})
.join("\n")}
</details>

<details>
<summary>Assigned Blame</summary><br/>

The following ${blameQuantifier} may be responsible for breaking this issue:
${advancedBlameTable}
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
</details>

<hr>

2 dot: ${twoDotUrl}
3 dot: ${repository.html_url}/compare/${matchingPull?.merge_commit_sha}...${currentCommitSha}
`;

await addCommentToIssue(comment, issue.number);
};

/**
* Callback for issues reopened - Processor
*/
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { closePullRequestForAnIssue, commentWithAssignMessage } from "./assign";
import { pricingLabelLogic, validatePriceLabels } from "./pricing";
import { checkBountiesToUnassign, collectAnalytics, checkWeeklyUpdate } from "./wildcard";
import { nullHandler } from "./shared";
import { handleComment, issueClosedCallback, issueCreatedCallback, issueReopenedCallback } from "./comment";
import { handleComment, issueClosedCallback, issueCreatedCallback, issueReopenedBlameCallback, issueReopenedCallback } from "./comment";
import { checkPullRequests } from "./assign/auto";
import { createDevPoolPR } from "./pull-request";
import { incentivizeComments, incentivizeCreatorComment, incentivizePullRequestReviews } from "./payout";
Expand All @@ -18,7 +18,7 @@ export const processors: Record<string, Handler> = {
},
[GithubEvent.ISSUES_REOPENED]: {
pre: [nullHandler],
action: [issueReopenedCallback],
action: [issueReopenedBlameCallback, issueReopenedCallback],
post: [nullHandler],
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
},
[GithubEvent.ISSUES_LABELED]: {
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/wildcard/unassign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const checkBountyToUnassign = async (issue: Issue): Promise<boolean> => {
return false;
};

const lastActivityTime = async (issue: Issue, comments: Comment[]): Promise<Date> => {
export const lastActivityTime = async (issue: Issue, comments: Comment[]): Promise<Date> => {
const logger = getLogger();
logger.info(`Checking the latest activity for the issue, issue_number: ${issue.number}`);
const assignees = issue.assignees.map((i) => i.login);
Expand Down