Add reaction to one-comment PRs
This commit is contained in:
committed by
pullfrog[bot]
parent
c3ac7d9ff0
commit
1edeaa0f4c
@@ -121270,11 +121270,20 @@ query ($owner: String!, $repo: String!, $pullNumber: Int!) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
var MAX_BODY_PREVIEW = 80;
|
var MAX_BODY_PREVIEW = 80;
|
||||||
|
var MAX_THREAD_DEPTH = 10;
|
||||||
function truncateBody(body) {
|
function truncateBody(body) {
|
||||||
const oneLine = body.replace(/\n/g, " ").trim();
|
const oneLine = body.replace(/\n/g, " ").trim();
|
||||||
if (oneLine.length <= MAX_BODY_PREVIEW) return oneLine;
|
if (oneLine.length <= MAX_BODY_PREVIEW) return oneLine;
|
||||||
return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "...";
|
return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "...";
|
||||||
}
|
}
|
||||||
|
function getReplyChain(comment, commentMap, depth = 0) {
|
||||||
|
if (depth >= MAX_THREAD_DEPTH) return [];
|
||||||
|
const parentId = comment.replyTo?.databaseId;
|
||||||
|
if (!parentId) return [];
|
||||||
|
const parent = commentMap.get(parentId);
|
||||||
|
if (!parent) return [];
|
||||||
|
return [...getReplyChain(parent, commentMap, depth + 1), parent];
|
||||||
|
}
|
||||||
function escapeXml(text) {
|
function escapeXml(text) {
|
||||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
}
|
}
|
||||||
@@ -121295,13 +121304,6 @@ function GetReviewCommentsTool(ctx) {
|
|||||||
description: "Get review comments for a pull request review, including thread context. When approved_by is provided, only returns comments that user approved with \u{1F44D}. Returns commentsPath pointing to a file with full comment details in XML format.",
|
description: "Get review comments for a pull request review, including thread context. When approved_by is provided, only returns comments that user approved with \u{1F44D}. Returns commentsPath pointing to a file with full comment details in XML format.",
|
||||||
parameters: GetReviewComments,
|
parameters: GetReviewComments,
|
||||||
execute: execute(async ({ pull_number, review_id, approved_by }) => {
|
execute: execute(async ({ pull_number, review_id, approved_by }) => {
|
||||||
const review = await ctx.octokit.rest.pulls.getReview({
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
pull_number,
|
|
||||||
review_id
|
|
||||||
});
|
|
||||||
const reviewer = review.data.user?.login ?? "unknown";
|
|
||||||
const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
|
const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
repo: ctx.repo.name,
|
repo: ctx.repo.name,
|
||||||
@@ -121312,12 +121314,19 @@ function GetReviewCommentsTool(ctx) {
|
|||||||
return {
|
return {
|
||||||
review_id,
|
review_id,
|
||||||
pull_number,
|
pull_number,
|
||||||
reviewer,
|
reviewer: "unknown",
|
||||||
count: 0,
|
count: 0,
|
||||||
commentsPath: null,
|
commentsPath: null,
|
||||||
message: "No review threads found"
|
message: "No review threads found"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const commentMap = /* @__PURE__ */ new Map();
|
||||||
|
for (const thread of pullRequest.reviewThreads.nodes) {
|
||||||
|
if (!thread?.comments?.nodes) continue;
|
||||||
|
for (const comment of thread.comments.nodes) {
|
||||||
|
if (comment) commentMap.set(comment.databaseId, comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
const leafComments = [];
|
const leafComments = [];
|
||||||
for (const thread of pullRequest.reviewThreads.nodes) {
|
for (const thread of pullRequest.reviewThreads.nodes) {
|
||||||
if (!thread?.comments?.nodes) continue;
|
if (!thread?.comments?.nodes) continue;
|
||||||
@@ -121328,12 +121337,10 @@ function GetReviewCommentsTool(ctx) {
|
|||||||
for (const comment of threadComments) {
|
for (const comment of threadComments) {
|
||||||
if (comment.pullRequestReview?.databaseId !== review_id) continue;
|
if (comment.pullRequestReview?.databaseId !== review_id) continue;
|
||||||
if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue;
|
if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue;
|
||||||
const threadContext = threadComments.filter(
|
const replyChain = getReplyChain(comment, commentMap);
|
||||||
(c) => c.createdAt < comment.createdAt || c.createdAt === comment.createdAt && c.databaseId < comment.databaseId
|
|
||||||
);
|
|
||||||
leafComments.push({
|
leafComments.push({
|
||||||
comment,
|
comment,
|
||||||
thread: threadContext,
|
thread: replyChain,
|
||||||
side: thread.diffSide
|
side: thread.diffSide
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121342,12 +121349,13 @@ function GetReviewCommentsTool(ctx) {
|
|||||||
return {
|
return {
|
||||||
review_id,
|
review_id,
|
||||||
pull_number,
|
pull_number,
|
||||||
reviewer,
|
reviewer: "unknown",
|
||||||
count: 0,
|
count: 0,
|
||||||
commentsPath: null,
|
commentsPath: null,
|
||||||
message: approved_by ? `No comments with \u{1F44D} from ${approved_by}` : "No comments found for this review"
|
message: approved_by ? `No comments with \u{1F44D} from ${approved_by}` : "No comments found for this review"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const reviewer = leafComments[0].comment.author?.login ?? "unknown";
|
||||||
const lines = [];
|
const lines = [];
|
||||||
lines.push(
|
lines.push(
|
||||||
`<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
|
`<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
|
||||||
|
|||||||
+32
-18
@@ -101,6 +101,7 @@ type GraphQLResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_BODY_PREVIEW = 80;
|
const MAX_BODY_PREVIEW = 80;
|
||||||
|
const MAX_THREAD_DEPTH = 10;
|
||||||
|
|
||||||
function truncateBody(body: string): string {
|
function truncateBody(body: string): string {
|
||||||
const oneLine = body.replace(/\n/g, " ").trim();
|
const oneLine = body.replace(/\n/g, " ").trim();
|
||||||
@@ -108,6 +109,20 @@ function truncateBody(body: string): string {
|
|||||||
return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "...";
|
return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// walk up the replyTo chain to get actual conversation (oldest first)
|
||||||
|
function getReplyChain(
|
||||||
|
comment: GraphQLReviewComment,
|
||||||
|
commentMap: Map<number, GraphQLReviewComment>,
|
||||||
|
depth = 0
|
||||||
|
): GraphQLReviewComment[] {
|
||||||
|
if (depth >= MAX_THREAD_DEPTH) return [];
|
||||||
|
const parentId = comment.replyTo?.databaseId;
|
||||||
|
if (!parentId) return [];
|
||||||
|
const parent = commentMap.get(parentId);
|
||||||
|
if (!parent) return [];
|
||||||
|
return [...getReplyChain(parent, commentMap, depth + 1), parent];
|
||||||
|
}
|
||||||
|
|
||||||
function escapeXml(text: string): string {
|
function escapeXml(text: string): string {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
@@ -140,15 +155,6 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
|||||||
"Returns commentsPath pointing to a file with full comment details in XML format.",
|
"Returns commentsPath pointing to a file with full comment details in XML format.",
|
||||||
parameters: GetReviewComments,
|
parameters: GetReviewComments,
|
||||||
execute: execute(async ({ pull_number, review_id, approved_by }) => {
|
execute: execute(async ({ pull_number, review_id, approved_by }) => {
|
||||||
// fetch the review to get reviewer username
|
|
||||||
const review = await ctx.octokit.rest.pulls.getReview({
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
pull_number,
|
|
||||||
review_id,
|
|
||||||
});
|
|
||||||
const reviewer = review.data.user?.login ?? "unknown";
|
|
||||||
|
|
||||||
// fetch all review threads using graphql (single API call)
|
// fetch all review threads using graphql (single API call)
|
||||||
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
|
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
@@ -161,13 +167,22 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
|||||||
return {
|
return {
|
||||||
review_id,
|
review_id,
|
||||||
pull_number,
|
pull_number,
|
||||||
reviewer,
|
reviewer: "unknown",
|
||||||
count: 0,
|
count: 0,
|
||||||
commentsPath: null,
|
commentsPath: null,
|
||||||
message: "No review threads found",
|
message: "No review threads found",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build a map of all comments for O(1) lookup when walking replyTo chains
|
||||||
|
const commentMap = new Map<number, GraphQLReviewComment>();
|
||||||
|
for (const thread of pullRequest.reviewThreads.nodes) {
|
||||||
|
if (!thread?.comments?.nodes) continue;
|
||||||
|
for (const comment of thread.comments.nodes) {
|
||||||
|
if (comment) commentMap.set(comment.databaseId, comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// collect leaf comments (from target review) with their thread context
|
// collect leaf comments (from target review) with their thread context
|
||||||
type LeafComment = {
|
type LeafComment = {
|
||||||
comment: GraphQLReviewComment;
|
comment: GraphQLReviewComment;
|
||||||
@@ -191,16 +206,12 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
|||||||
// filter by approved_by if specified
|
// filter by approved_by if specified
|
||||||
if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue;
|
if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue;
|
||||||
|
|
||||||
// get thread context (all comments before this one in the thread)
|
// get thread context by walking up the replyTo chain (not just chronological)
|
||||||
const threadContext = threadComments.filter(
|
const replyChain = getReplyChain(comment, commentMap);
|
||||||
(c) =>
|
|
||||||
c.createdAt < comment.createdAt ||
|
|
||||||
(c.createdAt === comment.createdAt && c.databaseId < comment.databaseId)
|
|
||||||
);
|
|
||||||
|
|
||||||
leafComments.push({
|
leafComments.push({
|
||||||
comment,
|
comment,
|
||||||
thread: threadContext,
|
thread: replyChain,
|
||||||
side: thread.diffSide,
|
side: thread.diffSide,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -210,7 +221,7 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
|||||||
return {
|
return {
|
||||||
review_id,
|
review_id,
|
||||||
pull_number,
|
pull_number,
|
||||||
reviewer,
|
reviewer: "unknown",
|
||||||
count: 0,
|
count: 0,
|
||||||
commentsPath: null,
|
commentsPath: null,
|
||||||
message: approved_by
|
message: approved_by
|
||||||
@@ -219,6 +230,9 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// derive reviewer from first comment (all comments in a review are from the same user)
|
||||||
|
const reviewer = leafComments[0].comment.author?.login ?? "unknown";
|
||||||
|
|
||||||
// build XML output
|
// build XML output
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|||||||
Reference in New Issue
Block a user