Skip to content

Commit e0c969f

Browse files
committed
feat: add Diff Processor(save tokens by filtering meaningless change logs)
- 불필요한 커밋 세부 내역 걸러주는 processor 생성 후 적용함
1 parent c2df68c commit e0c969f

6 files changed

Lines changed: 279 additions & 22 deletions

File tree

cli/src/main/kotlin/com/commitchronicle/cli/Main.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -473,19 +473,23 @@ class PR : CliktCommand(
473473
val templateDetector = TemplateEngineFactory.createGitHubTemplateDetector()
474474
val githubTemplatePath = templateDetector.findPRTemplate(path.absolutePathString(), Locale.fromCode(config.locale ?: "en"))
475475

476-
val prDraft = if (githubTemplatePath != null) {
477-
echo("GitHub PR template detected: $githubTemplatePath")
478-
val templateParser = TemplateEngineFactory.createGitHubTemplateParser(Locale.fromCode(config.locale ?: "en"))
479-
val templateContent = templateDetector.readTemplateContent(githubTemplatePath)
480-
481-
if (templateContent != null) {
482-
templateParser.parseAndRender(templateContent, commitsForPR, null)
483-
} else {
484-
echo("Unable to read template file. Using AI generation instead.")
485-
aiSummarizer.generatePRDraft(commitsForPR, null)
476+
val prDraft = when {
477+
githubTemplatePath != null -> {
478+
echo("GitHub PR template detected: $githubTemplatePath")
479+
val templateContent = templateDetector.readTemplateContent(githubTemplatePath)
480+
481+
when (templateContent) {
482+
null -> {
483+
echo("Unable to read template file. Using AI generation instead.")
484+
aiSummarizer.generatePRDraft(commitsForPR, null)
485+
}
486+
else -> {
487+
// AI에게 템플릿 형태로 상세한 내용을 작성하도록 요청
488+
aiSummarizer.generatePRDraftWithTemplate(commitsForPR, templateContent)
489+
}
490+
}
486491
}
487-
} else {
488-
aiSummarizer.generatePRDraft(commitsForPR, null)
492+
else -> aiSummarizer.generatePRDraft(commitsForPR, null)
489493
}
490494

491495
echo("\n=== PR Draft ===\n")

core/api/src/main/kotlin/com/commitchronicle/ai/AISummarizer.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,13 @@ interface AISummarizer {
3131
* @return 생성된 변경 로그
3232
*/
3333
suspend fun generateChangelog(commits: List<Commit>, groupByType: Boolean = false): String
34+
35+
/**
36+
* 주어진 템플릿 형태로 PR 초안을 생성합니다.
37+
*
38+
* @param commits 요약할 커밋 목록
39+
* @param template 사용할 템플릿 내용
40+
* @return 템플릿 형태로 생성된 PR 초안
41+
*/
42+
suspend fun generatePRDraftWithTemplate(commits: List<Commit>, template: String): String
3443
}

core/impl/src/main/kotlin/com/commitchronicle/ai/base/BaseSummarizer.kt

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,17 @@ abstract class BaseSummarizer(protected val config: AIProviderConfig) : AISummar
2828
if (commit.changes.isNotEmpty()) {
2929
append("Changed files:\n")
3030
commit.changes.forEach { change ->
31-
append("- ${change.path} (${change.changeType.name}, +${change.additions}, -${change.deletions})\n")
31+
val processedDiff = DiffProcessor.processDiff(change.diff)
32+
val changeSummary = DiffProcessor.generateChangeSummary(change.diff)
33+
34+
append("- ${change.path} (${change.changeType.name}, $changeSummary)\n")
35+
36+
if (processedDiff.isNotEmpty()) {
37+
append(" Key changes:\n")
38+
processedDiff.lines().forEach { line ->
39+
append(" $line\n")
40+
}
41+
}
3242
}
3343
}
3444
}
@@ -58,22 +68,132 @@ abstract class BaseSummarizer(protected val config: AIProviderConfig) : AISummar
5868
*/
5969
protected fun buildPRPrompt(commits: List<Commit>, title: String?): String {
6070
val commitsText = formatCommitsForPrompt(commits)
71+
val changesSummary = generateChangesSummaryForPR(commits)
6172
val language = locale.languageName
6273

6374
return """
64-
Please generate a PR description based on the following git commits.
75+
Please generate a comprehensive PR description based on the following git commits and code changes.
6576
${title?.let { "PR Title: $it" } ?: ""}
6677
6778
Focus on:
68-
1. Changes made
69-
2. Impact of changes
70-
3. Testing done
71-
4. Additional notes
79+
1. Overview of changes made (based on actual code changes below)
80+
2. Technical implementation details
81+
3. Impact and benefits of these changes
82+
4. Breaking changes (if any)
83+
5. Testing considerations
7284
7385
IMPORTANT: Please provide the response in $language language.
86+
Use the actual code changes to provide specific details about what was modified.
7487
75-
Commits:
88+
## Changes Summary
89+
$changesSummary
90+
91+
## Detailed Commits
92+
$commitsText
93+
""".trimIndent()
94+
}
95+
96+
/**
97+
* PR용 변경사항 요약 생성
98+
*/
99+
private fun generateChangesSummaryForPR(commits: List<Commit>): String {
100+
val allChanges = commits.flatMap { it.changes }
101+
val filesByType = allChanges.groupBy { getFileType(it.path) }
102+
103+
val summary = StringBuilder()
104+
105+
// 파일 타입별 변경 통계
106+
summary.append("### Files Changed by Type\n")
107+
filesByType.forEach { (fileType, changes) ->
108+
val fileCount = changes.size
109+
val meaningfulChanges = changes.count { DiffProcessor.processDiff(it.diff).isNotEmpty() }
110+
summary.append("- **$fileType**: $fileCount files ($meaningfulChanges with meaningful changes)\n")
111+
}
112+
113+
// 주요 변경사항 하이라이트
114+
summary.append("\n### Key Changes\n")
115+
allChanges.forEach { change ->
116+
val processedDiff = DiffProcessor.processDiff(change.diff)
117+
if (processedDiff.isNotEmpty()) {
118+
val changeSummary = DiffProcessor.generateChangeSummary(change.diff)
119+
summary.append("**${change.path}** (${change.changeType.name}): $changeSummary\n")
120+
121+
// 핵심 변경사항 몇 줄만 보여주기
122+
val keyLines = processedDiff.lines().take(3)
123+
if (keyLines.isNotEmpty()) {
124+
summary.append("```\n")
125+
keyLines.forEach { summary.append("$it\n") }
126+
if (processedDiff.lines().size > 3) {
127+
summary.append("... (${processedDiff.lines().size - 3} more lines)\n")
128+
}
129+
summary.append("```\n\n")
130+
}
131+
}
132+
}
133+
134+
return summary.toString()
135+
}
136+
137+
/**
138+
* 파일 확장자를 기반으로 파일 타입 결정
139+
*/
140+
private fun getFileType(filePath: String): String {
141+
return when (filePath.substringAfterLast('.', "")) {
142+
"kt" -> "Kotlin"
143+
"java" -> "Java"
144+
"js", "ts" -> "JavaScript/TypeScript"
145+
"py" -> "Python"
146+
"go" -> "Go"
147+
"rs" -> "Rust"
148+
"cpp", "c", "h" -> "C/C++"
149+
"md" -> "Documentation"
150+
"yml", "yaml" -> "Configuration"
151+
"json" -> "JSON"
152+
"xml" -> "XML"
153+
"gradle", "kts" -> "Build Script"
154+
"properties" -> "Properties"
155+
else -> "Other"
156+
}
157+
}
158+
159+
/**
160+
* 템플릿 기반 PR 초안 생성 (기본 구현)
161+
*/
162+
override suspend fun generatePRDraftWithTemplate(commits: List<Commit>, template: String): String {
163+
return callAIModel(buildPRPromptWithTemplate(commits, template))
164+
}
165+
166+
/**
167+
* 템플릿 기반 PR 초안용 프롬프트 생성
168+
*/
169+
protected fun buildPRPromptWithTemplate(commits: List<Commit>, template: String): String {
170+
val commitsText = formatCommitsForPrompt(commits)
171+
val changesSummary = generateChangesSummaryForPR(commits)
172+
val language = locale.languageName
173+
174+
return """
175+
Please generate a comprehensive PR description based on the following git commits and code changes.
176+
You MUST follow the provided template structure exactly and fill in detailed, meaningful content.
177+
178+
TEMPLATE TO FOLLOW:
179+
$template
180+
181+
INSTRUCTIONS:
182+
1. Use the exact template structure and sections
183+
2. Fill each section with detailed, technical information based on the actual code changes
184+
3. Be specific about what was changed, why it was changed, and the impact
185+
4. Include technical implementation details where relevant
186+
5. For checklists, mark items as checked [x] or unchecked [ ] based on the changes
187+
6. Write in $language language
188+
7. DO NOT include template variable syntax like {{}} in your response
189+
190+
## Changes Summary
191+
$changesSummary
192+
193+
## Detailed Commits
76194
$commitsText
195+
196+
Generate the PR description following the template structure above.
77197
""".trimIndent()
78198
}
79199

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.commitchronicle.ai.base
2+
3+
/**
4+
* Diff 내용을 가공하여 의미 있는 변경사항만 추출하는 클래스
5+
*/
6+
object DiffProcessor {
7+
8+
/**
9+
* Raw diff를 처리하여 의미 있는 변경사항만 추출
10+
*/
11+
fun processDiff(diff: String): String {
12+
if (diff.isBlank()) return ""
13+
14+
val meaningfulLines = diff.lines()
15+
.filter { line -> isMeaningfulChange(line) }
16+
.take(20) // 토큰 절약을 위해 최대 20줄로 제한
17+
18+
return if (meaningfulLines.isEmpty()) {
19+
""
20+
} else {
21+
meaningfulLines.joinToString("\n")
22+
}
23+
}
24+
25+
/**
26+
* 해당 라인이 의미 있는 변경사항인지 판단
27+
*/
28+
private fun isMeaningfulChange(line: String): Boolean {
29+
val trimmedLine = line.trim()
30+
31+
// 빈 줄은 제외
32+
if (trimmedLine.isEmpty()) return false
33+
34+
// diff 헤더는 제외 (@@, +++, ---, index 등)
35+
if (trimmedLine.startsWith("@@") ||
36+
trimmedLine.startsWith("+++") ||
37+
trimmedLine.startsWith("---") ||
38+
trimmedLine.startsWith("index ") ||
39+
trimmedLine.startsWith("diff --git")) {
40+
return false
41+
}
42+
43+
// 변경되지 않은 컨텍스트 라인은 제외 (+ 또는 -로 시작하지 않는 라인)
44+
if (!trimmedLine.startsWith("+") && !trimmedLine.startsWith("-")) {
45+
return false
46+
}
47+
48+
// + 또는 - 제거 후 실제 내용 확인
49+
val actualContent = trimmedLine.substring(1).trim()
50+
51+
// 빈 줄 변경은 제외
52+
if (actualContent.isEmpty()) return false
53+
54+
// 단순 주석 변경은 제외
55+
if (isOnlyCommentChange(actualContent)) return false
56+
57+
// 단순 들여쓰기/공백 변경은 제외
58+
if (isOnlyWhitespaceChange(line)) return false
59+
60+
// 단순 import 정렬은 제외 (연속된 import 변경)
61+
if (isImportReordering(actualContent)) return false
62+
63+
return true
64+
}
65+
66+
/**
67+
* 주석만 변경된 라인인지 확인
68+
*/
69+
private fun isOnlyCommentChange(content: String): Boolean {
70+
val trimmed = content.trim()
71+
return trimmed.startsWith("//") ||
72+
trimmed.startsWith("/*") ||
73+
trimmed.startsWith("*") ||
74+
trimmed.startsWith("*/") ||
75+
trimmed.startsWith("#") ||
76+
trimmed.startsWith("<!--") ||
77+
trimmed.startsWith("-->")
78+
}
79+
80+
/**
81+
* 들여쓰기/공백만 변경된 라인인지 확인
82+
*/
83+
private fun isOnlyWhitespaceChange(line: String): Boolean {
84+
if (!line.startsWith("+") && !line.startsWith("-")) return false
85+
86+
val content = line.substring(1)
87+
// 공백만 있거나 탭만 있는 경우
88+
return content.isBlank() || content.all { it.isWhitespace() }
89+
}
90+
91+
/**
92+
* import 재정렬인지 확인
93+
*/
94+
private fun isImportReordering(content: String): Boolean {
95+
val trimmed = content.trim()
96+
return trimmed.startsWith("import ") &&
97+
!trimmed.contains("new ") &&
98+
!trimmed.contains("=") &&
99+
!trimmed.contains("{") &&
100+
!trimmed.contains("(")
101+
}
102+
103+
/**
104+
* 변경사항 요약 생성
105+
*/
106+
fun generateChangeSummary(diff: String): String {
107+
if (diff.isBlank()) return "No meaningful changes"
108+
109+
val lines = diff.lines()
110+
val additions = lines.count { it.trim().startsWith("+") && isMeaningfulChange(it) }
111+
val deletions = lines.count { it.trim().startsWith("-") && isMeaningfulChange(it) }
112+
113+
return when {
114+
additions > 0 && deletions > 0 -> "Modified (+$additions, -$deletions lines)"
115+
additions > 0 -> "Added (+$additions lines)"
116+
deletions > 0 -> "Removed (-$deletions lines)"
117+
else -> "No meaningful changes"
118+
}
119+
}
120+
}

core/impl/src/main/kotlin/com/commitchronicle/language/MessageBundleImpl.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ class MessageBundleImpl : MessageBundle {
99
init {
1010
loadBundle(Locale.ENGLISH)
1111
loadBundle(Locale.KOREAN)
12+
loadBundle(Locale.CHINESE)
13+
loadBundle(Locale.JAPANESE)
1214
}
1315

1416
private fun loadBundle(locale: Locale) {
1517
val properties = Properties()
1618
val resourceName = "/messages_${locale.code}.properties"
17-
MessageBundleImpl::class.java.getResourceAsStream(resourceName)?.use {
18-
properties.load(it)
19+
MessageBundleImpl::class.java.getResourceAsStream(resourceName)?.use { inputStream ->
20+
inputStream.reader(Charsets.UTF_8).use { reader ->
21+
properties.load(reader)
22+
}
1923
}
2024
bundles[locale.code] = properties
2125
}

core/impl/src/main/kotlin/com/commitchronicle/template/GitHubTemplateDetectorImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class GitHubTemplateDetectorImpl : GitHubTemplateDetector {
139139
override fun readTemplateContent(templatePath: String): String? {
140140
return try {
141141
if (isValidTemplate(templatePath)) {
142-
File(templatePath).readText()
142+
File(templatePath).readText(Charsets.UTF_8)
143143
} else {
144144
null
145145
}

0 commit comments

Comments
 (0)