2024-02-21 00:39:58 +00:00
|
|
|
import { determineValidHunks, fileDiffsToMap, isDiffContentsWellFormed } from "../diff_utils"
|
|
|
|
|
import { userNickname } from "../auth0-mgmt"
|
2024-06-27 22:29:21 +00:00
|
|
|
import {
|
|
|
|
|
buildDependentPrTitle,
|
|
|
|
|
getInstallationOctokitByOwner,
|
|
|
|
|
isUserCollaborator,
|
|
|
|
|
} from "../github/github-utils"
|
2024-02-21 00:39:58 +00:00
|
|
|
import { githubApp } from "../github/github-app"
|
|
|
|
|
import {
|
|
|
|
|
buildResultDetails,
|
|
|
|
|
buildResultHeader,
|
|
|
|
|
buildResultTestReport,
|
|
|
|
|
createDependentPullRequest,
|
|
|
|
|
createNewBranchFromDiffContents,
|
|
|
|
|
} from "../github/create-pr-from-diffcontents"
|
|
|
|
|
import { posthog } from "../analytics"
|
|
|
|
|
import suggester from "code-suggester"
|
|
|
|
|
import { type FileDiffContent } from "code-suggester/build/src/types"
|
2024-07-13 01:05:09 +00:00
|
|
|
import { PrismaClient } from "@prisma/client"
|
2024-09-14 03:12:16 +00:00
|
|
|
import { sendSlackMessage } from "github/slack_util"
|
2024-07-13 01:05:09 +00:00
|
|
|
|
|
|
|
|
const prisma = new PrismaClient()
|
2024-02-21 00:39:58 +00:00
|
|
|
|
|
|
|
|
export async function suggestPrChanges(req, res): Promise<void> {
|
|
|
|
|
try {
|
2024-02-21 23:37:50 +00:00
|
|
|
const {
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
pullNumber,
|
|
|
|
|
diffContents,
|
|
|
|
|
prCommentFields,
|
|
|
|
|
existingTests,
|
|
|
|
|
generatedTests,
|
2024-11-27 01:01:00 +00:00
|
|
|
coverage_message,
|
2024-02-21 23:37:50 +00:00
|
|
|
} = req.body
|
2024-02-21 00:39:58 +00:00
|
|
|
const userId = req.userId
|
2024-07-13 01:05:09 +00:00
|
|
|
//traceId is optional to allow for backwards compatibility, can make this required in the future
|
|
|
|
|
const traceId = req.body.traceId || ""
|
|
|
|
|
console.log(`traceId: ${traceId}`)
|
2024-02-21 00:39:58 +00:00
|
|
|
|
|
|
|
|
if (!repo || !owner || !pullNumber || !isDiffContentsWellFormed(diffContents)) {
|
|
|
|
|
return res.status(400).send("Missing or malformed fields")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nickname = await userNickname(userId)
|
|
|
|
|
if (nickname == null) {
|
|
|
|
|
return res.status(401).send("Unauthorized") // Error getting user nickname
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const installationOctokit = await getInstallationOctokitByOwner(githubApp, owner, repo)
|
|
|
|
|
if (installationOctokit instanceof Error) {
|
|
|
|
|
return res.status(401).send(installationOctokit.message)
|
|
|
|
|
}
|
|
|
|
|
const isCollaborator = await isUserCollaborator(installationOctokit, owner, repo, nickname)
|
|
|
|
|
if (!isCollaborator) {
|
|
|
|
|
console.log(`${nickname} is not a collaborator on ${owner}/${repo}`)
|
|
|
|
|
return res.status(401).send("Unauthorized") // User is not a collaborator
|
|
|
|
|
}
|
|
|
|
|
console.log(`${nickname} is a collaborator on ${owner}/${repo}`)
|
|
|
|
|
|
|
|
|
|
// Suggest changes
|
|
|
|
|
|
|
|
|
|
const diffContentsMap: Map<string, FileDiffContent> = fileDiffsToMap(diffContents)
|
|
|
|
|
|
|
|
|
|
const { validHunks, invalidHunks } = await determineValidHunks(
|
|
|
|
|
installationOctokit.rest,
|
|
|
|
|
{ owner, repo },
|
|
|
|
|
pullNumber,
|
|
|
|
|
100,
|
|
|
|
|
diffContentsMap,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (invalidHunks.size > 0) {
|
|
|
|
|
// we can't suggest all of the hunks for this PR, because some of them are invalid (out of scope for this PR).
|
|
|
|
|
// so instead, let's make a new branch, commit the changes,
|
|
|
|
|
// and make a new PR from that branch onto the PR's branch
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`Can't suggest all of the hunks for this PR, because some of them are invalid (out of scope for this PR).`,
|
|
|
|
|
)
|
|
|
|
|
console.log(`Making a new dependent PR...`)
|
|
|
|
|
|
|
|
|
|
// a timestamp format like 2024-01-31-12.59.48
|
|
|
|
|
const timestamp = new Date()
|
|
|
|
|
.toISOString()
|
|
|
|
|
.replace(/:/g, ".")
|
|
|
|
|
.replace(/\.\d+Z$/, "")
|
|
|
|
|
|
|
|
|
|
// If you change this, please also change the regex in github-app.ts in the pull_request.closed event
|
|
|
|
|
const newBranchName = `codeflash/optimize-pr${pullNumber}-${timestamp}`
|
|
|
|
|
|
|
|
|
|
// Get the head branch of the original pull request
|
|
|
|
|
const originalPrData = await installationOctokit.rest.pulls.get({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
pull_number: pullNumber,
|
|
|
|
|
})
|
|
|
|
|
const baseBranch = originalPrData.data.head.ref
|
|
|
|
|
|
2024-06-27 22:29:21 +00:00
|
|
|
const commitMessage =
|
|
|
|
|
buildDependentPrTitle(
|
|
|
|
|
prCommentFields.function_name,
|
|
|
|
|
prCommentFields.speedup_pct,
|
|
|
|
|
pullNumber,
|
|
|
|
|
baseBranch,
|
|
|
|
|
) + `\n${prCommentFields.optimization_explanation}`
|
2024-02-21 00:39:58 +00:00
|
|
|
const branchCreated = await createNewBranchFromDiffContents(
|
|
|
|
|
installationOctokit,
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
newBranchName,
|
|
|
|
|
baseBranch,
|
|
|
|
|
diffContentsMap,
|
2024-06-27 22:29:21 +00:00
|
|
|
commitMessage,
|
2024-02-21 00:39:58 +00:00
|
|
|
)
|
|
|
|
|
if (!branchCreated) {
|
|
|
|
|
throw new Error(`Failed to create branch ${newBranchName}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newPrData = await createDependentPullRequest(
|
|
|
|
|
installationOctokit,
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
pullNumber,
|
|
|
|
|
newBranchName,
|
|
|
|
|
baseBranch,
|
|
|
|
|
prCommentFields,
|
2024-02-21 23:37:50 +00:00
|
|
|
existingTests,
|
2024-02-21 00:39:58 +00:00
|
|
|
generatedTests,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Respond with the new PR details
|
|
|
|
|
console.log(
|
|
|
|
|
`Created new dependent PR #${newPrData.data.number} from branch ${newPrData.data.head.ref}`,
|
|
|
|
|
)
|
2024-09-14 03:12:16 +00:00
|
|
|
|
2024-11-15 00:02:03 +00:00
|
|
|
if (owner === "Future-House" && (repo === "aviary" || repo === "paper-qa")) {
|
2024-11-14 23:58:34 +00:00
|
|
|
await sendSlackMessage(
|
|
|
|
|
`new dependent PR created: ${newPrData.data.html_url} for ${owner}/${repo}`,
|
|
|
|
|
)
|
2024-09-14 03:12:16 +00:00
|
|
|
}
|
2024-11-14 23:58:34 +00:00
|
|
|
|
2024-02-21 00:39:58 +00:00
|
|
|
posthog.capture({
|
|
|
|
|
distinctId: userId,
|
|
|
|
|
event: `cfapi-suggest-pr-changes-success-dependent-pr-created`,
|
|
|
|
|
properties: {
|
2024-03-01 01:10:15 +00:00
|
|
|
owner,
|
|
|
|
|
repo,
|
2024-02-21 00:39:58 +00:00
|
|
|
newPrNumber: newPrData.data.number,
|
|
|
|
|
newPrBranch: newPrData.data.head.ref,
|
2024-06-07 19:26:05 +00:00
|
|
|
PRURL: newPrData.data.html_url,
|
2024-02-21 00:39:58 +00:00
|
|
|
},
|
|
|
|
|
})
|
2024-07-13 01:05:09 +00:00
|
|
|
if (traceId !== "") {
|
|
|
|
|
let pull_request_db = await prisma.optimization_features.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
trace_id: traceId,
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
pull_request: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
2024-07-20 05:15:57 +00:00
|
|
|
if (pull_request_db) {
|
|
|
|
|
if (pull_request_db.pull_request === null || pull_request_db.pull_request === undefined) {
|
|
|
|
|
pull_request_db.pull_request = {}
|
|
|
|
|
}
|
2024-07-13 01:05:09 +00:00
|
|
|
pull_request_db.pull_request.dependent_pr_url = newPrData.data.html_url
|
|
|
|
|
|
|
|
|
|
await prisma.optimization_features.update({
|
|
|
|
|
where: {
|
|
|
|
|
trace_id: traceId,
|
|
|
|
|
},
|
|
|
|
|
data: pull_request_db,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-21 00:39:58 +00:00
|
|
|
res.json(newPrData.data.number)
|
|
|
|
|
} else {
|
|
|
|
|
// all suggestions are valid and can be made on this PR.
|
|
|
|
|
// let's make the suggestions
|
|
|
|
|
|
|
|
|
|
console.log(`All suggestions are valid and can be made on this PR.`)
|
|
|
|
|
console.log(`Making suggestions...`)
|
|
|
|
|
|
|
|
|
|
const reviewNumber = await suggester.reviewPullRequest(
|
|
|
|
|
installationOctokit.rest,
|
|
|
|
|
diffContentsMap,
|
|
|
|
|
{
|
|
|
|
|
repo,
|
|
|
|
|
owner,
|
|
|
|
|
pullNumber,
|
|
|
|
|
pageSize: 100,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if (reviewNumber == null) {
|
|
|
|
|
console.log(`Failed to create a suggester review for PR #${pullNumber}.`)
|
|
|
|
|
console.log(`This may be because there are no changes to be made.`)
|
|
|
|
|
console.log(`diffContentsMap: ${JSON.stringify(diffContentsMap)}`)
|
|
|
|
|
posthog.capture({
|
|
|
|
|
distinctId: userId,
|
|
|
|
|
event: `cfapi-suggest-pr-changes-failed-no-changes`,
|
|
|
|
|
})
|
|
|
|
|
return res.status(500).send("Failed creating suggestions for review.")
|
|
|
|
|
}
|
|
|
|
|
console.log(`Created suggestions for review #${reviewNumber} for PR #${pullNumber}`)
|
|
|
|
|
|
|
|
|
|
const prCommentBody =
|
2024-03-19 04:13:28 +00:00
|
|
|
`## ⚡️ Codeflash found optimizations for this PR\n` +
|
2024-02-21 00:39:58 +00:00
|
|
|
`${buildResultHeader(prCommentFields)}\n` +
|
|
|
|
|
`${buildResultDetails(prCommentFields)}\n` +
|
2024-11-27 01:01:00 +00:00
|
|
|
`${buildResultTestReport(prCommentFields, existingTests, generatedTests, coverage_message)}\n`
|
2024-02-21 00:39:58 +00:00
|
|
|
|
|
|
|
|
// Add explanation comment
|
|
|
|
|
const result = await installationOctokit.rest.issues.createComment({
|
|
|
|
|
owner,
|
|
|
|
|
repo,
|
|
|
|
|
issue_number: pullNumber,
|
|
|
|
|
body: prCommentBody,
|
|
|
|
|
})
|
|
|
|
|
console.log(`Added review comment to PR #${pullNumber}: ${result.data.html_url}`)
|
|
|
|
|
|
2024-11-15 00:02:03 +00:00
|
|
|
if (owner === "Future-House" && (repo === "aviary" || repo === "paper-qa")) {
|
2024-09-14 03:12:16 +00:00
|
|
|
await sendSlackMessage(`Suggestions made for PR #${pullNumber} in ${owner}/${repo}`)
|
|
|
|
|
}
|
2024-02-21 00:39:58 +00:00
|
|
|
posthog.capture({
|
|
|
|
|
distinctId: userId,
|
|
|
|
|
event: `cfapi-suggest-pr-changes-success-suggestions-made`,
|
|
|
|
|
properties: {
|
2024-03-01 01:10:15 +00:00
|
|
|
owner,
|
|
|
|
|
repo,
|
2024-02-21 00:39:58 +00:00
|
|
|
reviewNumber,
|
2024-06-07 19:26:05 +00:00
|
|
|
PRURL: result.data.html_url,
|
2024-02-21 00:39:58 +00:00
|
|
|
},
|
|
|
|
|
})
|
2024-09-14 03:12:16 +00:00
|
|
|
|
2024-07-13 01:05:09 +00:00
|
|
|
if (traceId !== "") {
|
|
|
|
|
let pull_request_db = await prisma.optimization_features.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
trace_id: traceId,
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
pull_request: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
2024-07-20 05:15:57 +00:00
|
|
|
if (pull_request_db) {
|
2024-07-13 01:05:09 +00:00
|
|
|
// the trace_id is not in the database then ignore it, because it should already exist by this stage.
|
2024-07-20 05:15:57 +00:00
|
|
|
if (pull_request_db.pull_request === null || pull_request_db.pull_request === undefined) {
|
|
|
|
|
pull_request_db.pull_request = {}
|
|
|
|
|
}
|
2024-07-13 01:05:09 +00:00
|
|
|
|
2024-08-13 04:38:07 +00:00
|
|
|
pull_request_db.pull_request.review_suggestion_pr_url = result.data.html_url
|
2024-07-13 01:05:09 +00:00
|
|
|
|
|
|
|
|
await prisma.optimization_features.update({
|
|
|
|
|
where: {
|
|
|
|
|
trace_id: traceId,
|
|
|
|
|
},
|
|
|
|
|
data: pull_request_db,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-21 00:39:58 +00:00
|
|
|
res.json(reviewNumber)
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.log(`Error in /cfapi/suggest-pr-changes: ${error}`)
|
|
|
|
|
console.log(`Error message: ${error.message}`)
|
|
|
|
|
console.log(`Error stack: ${error.stack}`)
|
|
|
|
|
posthog.capture({
|
|
|
|
|
distinctId: req.userId,
|
|
|
|
|
event: `cfapi-suggest-pr-changes-failed-error`,
|
|
|
|
|
properties: {
|
|
|
|
|
error: error.message,
|
|
|
|
|
stack: error.stack,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
res.status(500).send(`Error creating pull request: ${error.message}`)
|
|
|
|
|
}
|
|
|
|
|
}
|