Skip to content

Commit 081c054

Browse files
authored
Merge pull request #982 from Kyoungwoong/feature/embedded-engine
refac[vibe]: Analysis-Engine 의존성 제거
2 parents ea82361 + f3fa86b commit 081c054

14 files changed

Lines changed: 754 additions & 6 deletions

File tree

packages/mcp/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
"author": "",
1616
"license": "ISC",
1717
"dependencies": {
18-
"@githru-vscode-ext/analysis-engine": "^0.7.2",
1918
"@modelcontextprotocol/sdk": "^1.12.1",
20-
"@octokit/rest": "^22.0.0",
19+
"@octokit/core": "^4.0.4",
20+
"@octokit/plugin-retry": "^3.0.9",
21+
"@octokit/plugin-throttling": "^4.1.0",
22+
"@octokit/rest": "^19.0.3",
23+
"@octokit/types": "^13.5.0",
2124
"cors": "^2.8.5",
2225
"d3": "^7.8.5",
2326
"dayjs": "^1.11.18",
2427
"express": "^5.1.0",
25-
"puppeteer": "^21.0.0",
28+
"puppeteer": "^24.15.0",
29+
"glob": "^10.0.0",
2630
"react": "^18.2.0",
2731
"react-dom": "^18.2.0",
2832
"sharp": "^0.34.4",
@@ -31,6 +35,7 @@
3135
"devDependencies": {
3236
"@smithery/cli": "^1.2.4",
3337
"@types/d3": "^7.4.3",
38+
"@types/node": "^24.7.1",
3439
"@types/react": "^18.2.0",
3540
"@types/react-dom": "^18.2.0",
3641
"copyfiles": "^2.4.1",

packages/mcp/src/common/parser.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { getCommitMessageType } from '../engine/commit.js';
2+
import type { CommitRaw, DifferenceStatistic } from "./types.js";
3+
4+
interface ParsedRefs {
5+
branches: string[];
6+
tags: string[];
7+
}
8+
9+
interface MessageAndDiffStats {
10+
message: string;
11+
diffStats: DifferenceStatistic;
12+
}
13+
14+
const EOL_REGEX = /\r?\n/;
15+
const COMMIT_SEPARATOR = new RegExp(`${EOL_REGEX.source}{4}`);
16+
const INDENTATION = " ";
17+
18+
export function splitLogIntoCommits(log: string): string[] {
19+
return log.substring(2).split(COMMIT_SEPARATOR);
20+
}
21+
22+
export function extractCommitData(commit: string) {
23+
const [
24+
id,
25+
parents,
26+
refs,
27+
authorName,
28+
authorEmail,
29+
authorDate,
30+
committerName,
31+
committerEmail,
32+
committerDate,
33+
...messageAndDiffStats
34+
] = commit.split(EOL_REGEX);
35+
36+
return {
37+
id,
38+
parents,
39+
refs,
40+
authorName,
41+
authorEmail,
42+
authorDate,
43+
committerName,
44+
committerEmail,
45+
committerDate,
46+
messageAndDiffStats,
47+
};
48+
}
49+
50+
export function extractBranchesAndTags(refs: string): ParsedRefs {
51+
if (!refs) return { branches: [], tags: [] };
52+
53+
const refsArray = refs.replace(" -> ", ", ").split(", ");
54+
return refsArray.reduce<ParsedRefs>(
55+
(acc, ref) => {
56+
if (ref === "") return acc;
57+
if (ref.startsWith("tag: ")) {
58+
acc.tags.push(ref.replace("tag: ", ""));
59+
} else {
60+
acc.branches.push(ref);
61+
}
62+
return acc;
63+
},
64+
{ branches: [], tags: [] }
65+
);
66+
}
67+
68+
export function processDiffStatLine(line: string, diffStats: DifferenceStatistic) {
69+
const [insertions, deletions, path] = line.split("\t");
70+
const numberedInsertions = insertions === "-" ? 0 : Number(insertions);
71+
const numberedDeletions = deletions === "-" ? 0 : Number(deletions);
72+
73+
diffStats.totalInsertionCount += numberedInsertions;
74+
diffStats.totalDeletionCount += numberedDeletions;
75+
diffStats.fileDictionary[path] = {
76+
insertionCount: numberedInsertions,
77+
deletionCount: numberedDeletions,
78+
};
79+
}
80+
81+
export function extractMessageAndDiffStats(messageAndDiffStats: string[]): MessageAndDiffStats {
82+
let messageSubject = "";
83+
let messageBody = "";
84+
const diffStats: DifferenceStatistic = {
85+
totalInsertionCount: 0,
86+
totalDeletionCount: 0,
87+
fileDictionary: {},
88+
};
89+
90+
for (const [idx, line] of messageAndDiffStats.entries()) {
91+
if (idx === 0) {
92+
messageSubject = line;
93+
continue;
94+
}
95+
96+
if (line.startsWith(INDENTATION)) {
97+
messageBody += idx === 1 ? line.trim() : `\n${line.trim()}`;
98+
continue;
99+
}
100+
101+
if (line === "") continue;
102+
103+
processDiffStatLine(line, diffStats);
104+
}
105+
106+
const message = messageBody === "" ? messageSubject : `${messageSubject}\n${messageBody}`;
107+
return { message, diffStats };
108+
}
109+
110+
export function createCommitRaw(
111+
commitIdx: number,
112+
commitData: ReturnType<typeof extractCommitData>,
113+
parsedRefs: ParsedRefs,
114+
parsedMessage: ReturnType<typeof extractMessageAndDiffStats>
115+
): CommitRaw {
116+
const { id, parents, authorName, authorEmail, authorDate, committerName, committerEmail, committerDate } = commitData;
117+
const { branches, tags } = parsedRefs;
118+
const { message, diffStats } = parsedMessage;
119+
120+
return {
121+
sequence: commitIdx,
122+
id,
123+
parents: parents.length === 0 ? [] : parents.split(" "),
124+
branches,
125+
tags,
126+
author: {
127+
name: authorName,
128+
email: authorEmail,
129+
},
130+
authorDate: new Date(authorDate),
131+
committer: {
132+
name: committerName,
133+
email: committerEmail,
134+
},
135+
committerDate: new Date(committerDate),
136+
message,
137+
commitMessageType: getCommitMessageType(message),
138+
differenceStatistic: diffStats,
139+
};
140+
}
141+
142+
export default function getCommitRaws(log: string): CommitRaw[] {
143+
if (!log) return [];
144+
145+
const commits = splitLogIntoCommits(log);
146+
return commits.map((commit, idx) => {
147+
const commitData = extractCommitData(commit);
148+
const parsedRefs = extractBranchesAndTags(commitData.refs);
149+
const parsedMessage = extractMessageAndDiffStats(commitData.messageAndDiffStats);
150+
return createCommitRaw(idx, commitData, parsedRefs, parsedMessage);
151+
});
152+
}

packages/mcp/src/common/queue.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export default class Queue<T> {
2+
private queue: Array<T> = [];
3+
4+
private readonly compareFn: (a: T, b: T) => number;
5+
6+
constructor(compareFn: (a: T, b: T) => number) {
7+
this.compareFn = compareFn;
8+
}
9+
10+
push(node: T): void {
11+
this.queue.push(node);
12+
this.queue.sort(this.compareFn);
13+
}
14+
15+
pop(): T | undefined {
16+
return this.queue.shift();
17+
}
18+
19+
isEmpty(): boolean {
20+
return this.queue.length === 0;
21+
}
22+
23+
pushFront(node: T): void {
24+
this.queue.unshift(node);
25+
}
26+
27+
pushBack(node: T): void {
28+
this.queue.push(node);
29+
}
30+
}

packages/mcp/src/common/summary.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { CommitRaw } from "./types.js";
2+
3+
const apiKey = process.env.GEMENI_API_KEY || '';
4+
const apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=";
5+
6+
export async function getSummary(csmNodes: CommitRaw[]) {
7+
const commitMessages = csmNodes.map((csmNode) => csmNode.message.split('\n')[0]).join(', ');
8+
9+
try {
10+
const response = await fetch(apiUrl + apiKey, {
11+
method: "POST",
12+
headers: {
13+
"Content-Type": "application/json",
14+
},
15+
body: JSON.stringify({
16+
contents: [{parts: [{text: `${prompt} \n${commitMessages}`}]}],
17+
}),
18+
});
19+
20+
if (!response.ok) {
21+
throw new Error(`Error: ${response.status} ${response.statusText}`);
22+
}
23+
24+
const data = await response.json();
25+
return data.candidates[0].content.parts[0].text.trim();
26+
} catch (error) {
27+
console.error("Error fetching summary:", error);
28+
return undefined;
29+
}
30+
}
31+
32+
const prompt = `Proceed with the task of summarising the contents of the commit message provided.
33+
34+
Procedure:
35+
1. Separate the commits based on , .
36+
2. Extract only the commits given, excluding the merge commits.
37+
3. Summarise the commits based on the most common words. Keep the shape of your commit message.
38+
39+
Example Merge commits:
40+
- Merge pull request #633 from HIITMEMARIO/main
41+
- Merge branch ‘githru:main’ into main
42+
43+
Rules:
44+
- Summarize in 3 to 5 lines.
45+
- Combine similar or overlapping content. (e.g. feat: add button, feat: add button to header -> feat: add button)
46+
- Include prefixes if present (e.g. feat, fix, refactor)
47+
- Please preserve the stylistic style of the commit.
48+
49+
Output format:
50+
‘’
51+
- {prefix (if any)}:{Commit summary1}
52+
- {prefix (if any)}:{Commit summary2}
53+
- {prefix (if any)}:{commit summary3}
54+
‘’
55+
56+
Commits:`

packages/mcp/src/common/types.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { RestEndpointMethodTypes } from "@octokit/rest";
2+
13
export interface GitHubRepoInfo {
24
owner: string;
35
repo: string;
@@ -78,6 +80,26 @@ export interface CommitNode {
7880
commit: CommitRaw;
7981
}
8082

83+
export type CommitDict = Map<string, CommitNode>;
84+
85+
export const CommitMessageTypeList = [
86+
"build",
87+
"chore",
88+
"ci",
89+
"docs",
90+
"feat",
91+
"fix",
92+
"pert",
93+
"refactor",
94+
"revert",
95+
"style",
96+
"test",
97+
];
98+
99+
const COMMIT_MESSAGE_TYPE = [...CommitMessageTypeList] as const;
100+
101+
export type CommitMessageType = (typeof COMMIT_MESSAGE_TYPE)[number];
102+
81103
export interface CSMNode {
82104
base: CommitNode;
83105
source: CommitNode[];
@@ -173,3 +195,16 @@ export interface AuthorWorkPatternPayload {
173195
typeMix: Array<{ label: string; value: number }>;
174196
locale?: "en" | "ko";
175197
}
198+
199+
export interface Stem {
200+
nodes: CommitNode[];
201+
}
202+
203+
export type StemDict = Map<string, Stem>;
204+
205+
export interface PullRequest {
206+
detail: RestEndpointMethodTypes["pulls"]["get"]["response"];
207+
commitDetails: RestEndpointMethodTypes["pulls"]["listCommits"]["response"];
208+
}
209+
210+
export type PullRequestDict = Map<string, PullRequest>;

packages/mcp/src/engine/Engine.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import "reflect-metadata";
2+
3+
import PluginOctokit from "./pluginOctokit.js";
4+
import getCommitRaws from "../common/parser.js";
5+
import { buildCommitDict } from "./commit.js";
6+
import { buildStemDict } from "./stem.js";
7+
import { buildCSMDict } from "./csm.js";
8+
import { getSummary } from "../common/summary.js";
9+
10+
type AnalysisEngineArgs = {
11+
isDebugMode?: boolean;
12+
gitLog: string;
13+
owner: string;
14+
repo: string;
15+
baseBranchName: string;
16+
auth?: string;
17+
};
18+
19+
export class AnalysisEngine {
20+
private gitLog!: string;
21+
22+
private isDebugMode?: boolean;
23+
24+
private octokit!: PluginOctokit;
25+
26+
private baseBranchName!: string;
27+
28+
constructor(args: AnalysisEngineArgs) {
29+
this.insertArgs(args);
30+
}
31+
32+
private insertArgs = (args: AnalysisEngineArgs) => {
33+
const { isDebugMode, gitLog, owner, repo, auth, baseBranchName } = args;
34+
this.gitLog = gitLog;
35+
this.baseBranchName = baseBranchName;
36+
this.isDebugMode = isDebugMode;
37+
this.octokit = new PluginOctokit({
38+
owner,
39+
repo,
40+
options: {
41+
auth,
42+
},
43+
});
44+
};
45+
46+
public analyzeGit = async () => {
47+
let isPRSuccess = true;
48+
49+
const commitRaws = getCommitRaws(this.gitLog);
50+
51+
const commitDict = buildCommitDict(commitRaws);
52+
53+
const pullRequests = await this.octokit
54+
.getPullRequests()
55+
.catch((err) => {
56+
console.error(err);
57+
isPRSuccess = false;
58+
return [];
59+
})
60+
.then((pullRequests) => {
61+
return pullRequests;
62+
});
63+
64+
const stemDict = buildStemDict(commitDict, this.baseBranchName);
65+
const csmDict = buildCSMDict(commitDict, stemDict, this.baseBranchName, pullRequests);
66+
const nodes = stemDict.get(this.baseBranchName)?.nodes?.map(({ commit }) => commit);
67+
const geminiCommitSummary = await getSummary(nodes ? nodes?.slice(-10) : []);
68+
69+
return {
70+
isPRSuccess,
71+
csmDict,
72+
};
73+
}
74+
}

0 commit comments

Comments
 (0)