mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 23:54:13 +08:00
Initial commit
This commit is contained in:
194
src/github/data/fetcher.ts
Normal file
194
src/github/data/fetcher.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { execSync } from "child_process";
|
||||
import type {
|
||||
GitHubPullRequest,
|
||||
GitHubIssue,
|
||||
GitHubComment,
|
||||
GitHubFile,
|
||||
GitHubReview,
|
||||
PullRequestQueryResponse,
|
||||
IssueQueryResponse,
|
||||
} from "../types";
|
||||
import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github";
|
||||
import type { Octokits } from "../api/client";
|
||||
import { downloadCommentImages } from "../utils/image-downloader";
|
||||
import type { CommentWithImages } from "../utils/image-downloader";
|
||||
|
||||
type FetchDataParams = {
|
||||
octokits: Octokits;
|
||||
repository: string;
|
||||
prNumber: string;
|
||||
isPR: boolean;
|
||||
};
|
||||
|
||||
export type GitHubFileWithSHA = GitHubFile & {
|
||||
sha: string;
|
||||
};
|
||||
|
||||
export type FetchDataResult = {
|
||||
contextData: GitHubPullRequest | GitHubIssue;
|
||||
comments: GitHubComment[];
|
||||
changedFiles: GitHubFile[];
|
||||
changedFilesWithSHA: GitHubFileWithSHA[];
|
||||
reviewData: { nodes: GitHubReview[] } | null;
|
||||
imageUrlMap: Map<string, string>;
|
||||
};
|
||||
|
||||
export async function fetchGitHubData({
|
||||
octokits,
|
||||
repository,
|
||||
prNumber,
|
||||
isPR,
|
||||
}: FetchDataParams): Promise<FetchDataResult> {
|
||||
const [owner, repo] = repository.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error("Invalid repository format. Expected 'owner/repo'.");
|
||||
}
|
||||
|
||||
let contextData: GitHubPullRequest | GitHubIssue | null = null;
|
||||
let comments: GitHubComment[] = [];
|
||||
let changedFiles: GitHubFile[] = [];
|
||||
let reviewData: { nodes: GitHubReview[] } | null = null;
|
||||
|
||||
try {
|
||||
if (isPR) {
|
||||
// Fetch PR data with all comments and file information
|
||||
const prResult = await octokits.graphql<PullRequestQueryResponse>(
|
||||
PR_QUERY,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
number: parseInt(prNumber),
|
||||
},
|
||||
);
|
||||
|
||||
if (prResult.repository.pullRequest) {
|
||||
const pullRequest = prResult.repository.pullRequest;
|
||||
contextData = pullRequest;
|
||||
changedFiles = pullRequest.files.nodes || [];
|
||||
comments = pullRequest.comments?.nodes || [];
|
||||
reviewData = pullRequest.reviews || [];
|
||||
|
||||
console.log(`Successfully fetched PR #${prNumber} data`);
|
||||
} else {
|
||||
throw new Error(`PR #${prNumber} not found`);
|
||||
}
|
||||
} else {
|
||||
// Fetch issue data
|
||||
const issueResult = await octokits.graphql<IssueQueryResponse>(
|
||||
ISSUE_QUERY,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
number: parseInt(prNumber),
|
||||
},
|
||||
);
|
||||
|
||||
if (issueResult.repository.issue) {
|
||||
contextData = issueResult.repository.issue;
|
||||
comments = contextData?.comments?.nodes || [];
|
||||
|
||||
console.log(`Successfully fetched issue #${prNumber} data`);
|
||||
} else {
|
||||
throw new Error(`Issue #${prNumber} not found`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch ${isPR ? "PR" : "issue"} data:`, error);
|
||||
throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`);
|
||||
}
|
||||
|
||||
// Compute SHAs for changed files
|
||||
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
||||
if (isPR && changedFiles.length > 0) {
|
||||
changedFilesWithSHA = changedFiles.map((file) => {
|
||||
try {
|
||||
// Use git hash-object to compute the SHA for the current file content
|
||||
const sha = execSync(`git hash-object "${file.path}"`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
return {
|
||||
...file,
|
||||
sha,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to compute SHA for ${file.path}:`, error);
|
||||
// Return original file without SHA if computation fails
|
||||
return {
|
||||
...file,
|
||||
sha: "unknown",
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare all comments for image processing
|
||||
const issueComments: CommentWithImages[] = comments
|
||||
.filter((c) => c.body)
|
||||
.map((c) => ({
|
||||
type: "issue_comment" as const,
|
||||
id: c.databaseId,
|
||||
body: c.body,
|
||||
}));
|
||||
|
||||
const reviewBodies: CommentWithImages[] =
|
||||
reviewData?.nodes
|
||||
?.filter((r) => r.body)
|
||||
.map((r) => ({
|
||||
type: "review_body" as const,
|
||||
id: r.databaseId,
|
||||
pullNumber: prNumber,
|
||||
body: r.body,
|
||||
})) ?? [];
|
||||
|
||||
const reviewComments: CommentWithImages[] =
|
||||
reviewData?.nodes
|
||||
?.flatMap((r) => r.comments?.nodes ?? [])
|
||||
.filter((c) => c.body)
|
||||
.map((c) => ({
|
||||
type: "review_comment" as const,
|
||||
id: c.databaseId,
|
||||
body: c.body,
|
||||
})) ?? [];
|
||||
|
||||
// Add the main issue/PR body if it has content
|
||||
const mainBody: CommentWithImages[] = contextData.body
|
||||
? [
|
||||
{
|
||||
...(isPR
|
||||
? {
|
||||
type: "pr_body" as const,
|
||||
pullNumber: prNumber,
|
||||
body: contextData.body,
|
||||
}
|
||||
: {
|
||||
type: "issue_body" as const,
|
||||
issueNumber: prNumber,
|
||||
body: contextData.body,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const allComments = [
|
||||
...mainBody,
|
||||
...issueComments,
|
||||
...reviewBodies,
|
||||
...reviewComments,
|
||||
];
|
||||
|
||||
const imageUrlMap = await downloadCommentImages(
|
||||
octokits,
|
||||
owner,
|
||||
repo,
|
||||
allComments,
|
||||
);
|
||||
|
||||
return {
|
||||
contextData,
|
||||
comments,
|
||||
changedFiles,
|
||||
changedFilesWithSHA,
|
||||
reviewData,
|
||||
imageUrlMap,
|
||||
};
|
||||
}
|
||||
123
src/github/data/formatter.ts
Normal file
123
src/github/data/formatter.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
GitHubPullRequest,
|
||||
GitHubIssue,
|
||||
GitHubComment,
|
||||
GitHubFile,
|
||||
GitHubReview,
|
||||
} from "../types";
|
||||
import type { GitHubFileWithSHA } from "./fetcher";
|
||||
|
||||
export function formatContext(
|
||||
contextData: GitHubPullRequest | GitHubIssue,
|
||||
isPR: boolean,
|
||||
): string {
|
||||
if (isPR) {
|
||||
const prData = contextData as GitHubPullRequest;
|
||||
return `PR Title: ${prData.title}
|
||||
PR Author: ${prData.author.login}
|
||||
PR Branch: ${prData.headRefName} -> ${prData.baseRefName}
|
||||
PR State: ${prData.state}
|
||||
PR Additions: ${prData.additions}
|
||||
PR Deletions: ${prData.deletions}
|
||||
Total Commits: ${prData.commits.totalCount}
|
||||
Changed Files: ${prData.files.nodes.length} files`;
|
||||
} else {
|
||||
const issueData = contextData as GitHubIssue;
|
||||
return `Issue Title: ${issueData.title}
|
||||
Issue Author: ${issueData.author.login}
|
||||
Issue State: ${issueData.state}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBody(
|
||||
body: string,
|
||||
imageUrlMap: Map<string, string>,
|
||||
): string {
|
||||
let processedBody = body;
|
||||
|
||||
// Replace image URLs with local paths
|
||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
||||
processedBody = processedBody.replaceAll(originalUrl, localPath);
|
||||
}
|
||||
|
||||
return processedBody;
|
||||
}
|
||||
|
||||
export function formatComments(
|
||||
comments: GitHubComment[],
|
||||
imageUrlMap?: Map<string, string>,
|
||||
): string {
|
||||
return comments
|
||||
.map((comment) => {
|
||||
let body = comment.body;
|
||||
|
||||
// Replace image URLs with local paths if we have a mapping
|
||||
if (imageUrlMap && body) {
|
||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
||||
body = body.replaceAll(originalUrl, localPath);
|
||||
}
|
||||
}
|
||||
|
||||
return `[${comment.author.login} at ${comment.createdAt}]: ${body}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
export function formatReviewComments(
|
||||
reviewData: { nodes: GitHubReview[] } | null,
|
||||
imageUrlMap?: Map<string, string>,
|
||||
): string {
|
||||
if (!reviewData || !reviewData.nodes) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const formattedReviews = reviewData.nodes.map((review) => {
|
||||
let reviewOutput = `[Review by ${review.author.login} at ${review.submittedAt}]: ${review.state}`;
|
||||
|
||||
if (
|
||||
review.comments &&
|
||||
review.comments.nodes &&
|
||||
review.comments.nodes.length > 0
|
||||
) {
|
||||
const comments = review.comments.nodes
|
||||
.map((comment) => {
|
||||
let body = comment.body;
|
||||
|
||||
// Replace image URLs with local paths if we have a mapping
|
||||
if (imageUrlMap) {
|
||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
||||
body = body.replaceAll(originalUrl, localPath);
|
||||
}
|
||||
}
|
||||
|
||||
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
||||
})
|
||||
.join("\n");
|
||||
reviewOutput += `\n${comments}`;
|
||||
}
|
||||
|
||||
return reviewOutput;
|
||||
});
|
||||
|
||||
return formattedReviews.join("\n\n");
|
||||
}
|
||||
|
||||
export function formatChangedFiles(changedFiles: GitHubFile[]): string {
|
||||
return changedFiles
|
||||
.map(
|
||||
(file) =>
|
||||
`- ${file.path} (${file.changeType}) +${file.additions}/-${file.deletions}`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function formatChangedFilesWithSHA(
|
||||
changedFiles: GitHubFileWithSHA[],
|
||||
): string {
|
||||
return changedFiles
|
||||
.map(
|
||||
(file) =>
|
||||
`- ${file.path} (${file.changeType}) +${file.additions}/-${file.deletions} SHA: ${file.sha}`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user