Skip to content

Commit 8864fde

Browse files
authored
멀티라인 리뷰에서 파일 리뷰로 롤백 및 ReviewContext 추가
멀티라인 리뷰에서 파일 리뷰로 롤백 및 ReviewContext 추가
2 parents 148d3b1 + 6bfaa95 commit 8864fde

11 files changed

Lines changed: 276 additions & 284 deletions

File tree

src/main/kotlin/com/project/codereview/batch/FailedTaskManager.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package com.project.codereview.batch
22

3-
import com.project.codereview.client.github.GithubDiffUtils
3+
import com.project.codereview.client.github.dto.ReviewContext
44
import com.project.codereview.core.dto.GithubPayload
5-
import com.project.codereview.core.dto.GithubReviewDto
65
import org.springframework.stereotype.Component
76
import java.util.concurrent.ConcurrentLinkedQueue
87
import java.util.concurrent.atomic.AtomicInteger
@@ -11,12 +10,8 @@ import java.util.concurrent.atomic.AtomicInteger
1110
class FailedTaskManager {
1211
data class OriginalTask(
1312
val payload: GithubPayload,
14-
val diff: GithubDiffUtils.DiffInfo
15-
) {
16-
fun toGithubReviewDto(review: String): GithubReviewDto {
17-
return GithubReviewDto(payload.pull_request, diff, payload.installation.id, review)
18-
}
19-
}
13+
val reviewContext: ReviewContext
14+
)
2015

2116
data class FailedTask(
2217
val task: OriginalTask,

src/main/kotlin/com/project/codereview/batch/FailedTaskRetryScheduler.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,38 @@ class FailedTaskRetryScheduler(
3232
val retryCount = task.retryCount
3333
val cmd = ReviewCommand(
3434
payload = original.payload,
35-
diff = original.diff,
35+
reviewContext = original.reviewContext,
3636
promptOverride = task.prompt // 재시도는 기존 프롬프트 유지
3737
)
3838

39+
val path = original.reviewContext.type.path()
40+
if (path.isBlank()) {
41+
// path가 빈 경우는 Summary 뿐임
42+
logger.warn("[Retry Aborted] File path is empty")
43+
return@forEach
44+
}
45+
3946
if (retryCount >= maxRetry) {
40-
logger.error("[Give Up] file={} after {} retries", original.diff.path, retryCount)
47+
logger.error("[Give Up] file={} after {} retries", path, retryCount)
4148
return@forEach
4249
}
4350

4451
when (val outcome = executor.execute(cmd)) {
4552
is ReviewOutcome.Success -> {
46-
logger.info("[Retry Success] file={}, retry={}", original.diff.path, retryCount)
53+
logger.info("[Retry Success] file={}, retry={}", path, retryCount)
4754
}
4855
is ReviewOutcome.Retryable -> {
4956
val next = computeBackoffMillis(retryCount)
5057
failedTaskManager.add(original, outcome.promptUsed, retryCount + 1)
51-
logger.warn("[Retry Requeued] file={}, retry={}, next={}ms, reason={}",
52-
original.diff.path, retryCount + 1, next, outcome.reason)
58+
logger.warn("[Retry Requeued] file={}, retry={}, next={}ms, reason={}", path, retryCount + 1, next, outcome.reason)
5359
}
5460
is ReviewOutcome.NonRetryable -> {
55-
logger.error("[Retry Aborted] file={}, retry={}, reason={}",
56-
original.diff.path, retryCount, outcome.reason)
61+
logger.error("[Retry Aborted] file={}, retry={}, reason={}", path, retryCount, outcome.reason)
5762
}
5863
}
5964
}
6065

61-
logger.info("[Retry End] processed = {}", batch.size)
66+
logger.info("[Retry End] processed count = {}, Remaining whole process count = {}", batch.size, failedTaskManager.size())
6267
}
6368
}
6469

src/main/kotlin/com/project/codereview/client/github/GithubDiffUtils.kt

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
package com.project.codereview.client.github
22

3+
import com.project.codereview.client.github.dto.ReviewContext
4+
import com.project.codereview.client.github.dto.ReviewType
5+
import com.project.codereview.core.dto.GithubPayload
6+
37
object GithubDiffUtils {
48

9+
// 멀티라인 앵커 정보
510
data class DiffInfo(
611
val path: String,
712
val startLine: Int,
813
val endLine: Int,
9-
val side: String,
14+
val side: String, // "RIGHT" | "LEFT"
1015
val snippet: String
1116
) {
12-
fun toGithubReviewRequest(
13-
commitId: String,
14-
body: String
15-
) = GithubReviewClient.ReviewCommentRequest(
16-
body = body,
17-
path = path,
18-
commit_id = commitId,
19-
start_line = startLine,
20-
start_side = side,
21-
line = endLine,
22-
side = side
23-
)
17+
fun toReviewType(): ReviewType.ByMultiline =
18+
ReviewType.ByMultiline(
19+
path = path,
20+
line = endLine,
21+
side = side,
22+
start_line = startLine,
23+
start_side = side
24+
)
2425
}
2526

27+
// 파일 단위 컨텍스트(파일별로 묶기)
2628
data class FileContext(
2729
val path: String,
2830
val originSnippet: String,
@@ -35,6 +37,7 @@ object GithubDiffUtils {
3537

3638
private fun isImportLineInDiff(line: String): Boolean = importLineRegex.containsMatchIn(line)
3739

40+
// hunk 파싱 → 멀티라인 범위(DiffInfo) 추출
3841
private fun buildRequests(diffText: String): List<DiffInfo> {
3942
val text = diffText.replace("\r\n", "\n")
4043
val lines = text.lineSequence().toList()
@@ -143,7 +146,7 @@ object GithubDiffUtils {
143146
}
144147

145148
inHunk && currentPath != null -> {
146-
if (raw.isEmpty()) return@forEach
149+
if (raw.isBlank()) return@forEach
147150
when (raw[0]) {
148151
' ' -> {
149152
if (collectingRight) {
@@ -185,6 +188,7 @@ object GithubDiffUtils {
185188
return ranges
186189
}
187190

191+
// 파일별 원본 스니펫 생성(선택적 import 필터링)
188192
private fun buildOrigins(
189193
diffText: String,
190194
filterImportsInOrigin: Boolean = false
@@ -215,18 +219,13 @@ object GithubDiffUtils {
215219
}
216220

217221
inHunk && currentPath != null -> {
218-
if (raw.isEmpty()) return@forEach
219-
// origin은 + / - 만 수집 (원하면 ' '도 포함 가능)
222+
if (raw.isBlank()) return@forEach
220223
if (raw[0] == '+' || raw[0] == '-') {
221224
if (!filterImportsInOrigin || !isImportLineInDiff(raw)) {
222225
originByFile[currentPath]!!.add(raw)
223226
}
224227
}
225-
if (raw.startsWith("diff --git ") || raw.startsWith("index ") || raw.startsWith("--- ") || raw.startsWith(
226-
"+++ "
227-
)
228-
) {
229-
// 안전장치: 예외적 케이스 방지
228+
if (raw.startsWith("diff --git ") || raw.startsWith("index ") || raw.startsWith("--- ") || raw.startsWith("+++ ")) {
230229
inHunk = false
231230
}
232231
}
@@ -238,13 +237,13 @@ object GithubDiffUtils {
238237
return originByFile.mapValues { (_, v) -> v.joinToString("\n") }
239238
}
240239

241-
fun buildFileContexts(
240+
// 내부 공통: 파일 컨텍스트 구성
241+
private fun buildFileContextsInternal(
242242
diffText: String,
243-
filterImportsInOrigin: Boolean = false
243+
filterImportsInOrigin: Boolean
244244
): List<FileContext> {
245245
val diffs = buildRequests(diffText)
246246
val originMap = buildOrigins(diffText, filterImportsInOrigin)
247-
248247
return diffs.groupBy { it.path }
249248
.map { (path, list) ->
250249
FileContext(
@@ -254,4 +253,36 @@ object GithubDiffUtils {
254253
)
255254
}
256255
}
256+
257+
fun buildReviewContextsByMultiline(
258+
diffText: String,
259+
payload: GithubPayload,
260+
filterImportsInOrigin: Boolean = true,
261+
): List<ReviewContext> {
262+
val fileContexts = buildFileContextsInternal(diffText, filterImportsInOrigin)
263+
return fileContexts.flatMap { file ->
264+
file.diffs.map { d ->
265+
ReviewContext(
266+
body = d.snippet,
267+
payload = payload,
268+
type = d.toReviewType()
269+
)
270+
}
271+
}
272+
}
273+
274+
fun buildReviewContextsByFile(
275+
diffText: String,
276+
payload: GithubPayload,
277+
filterImportsInOrigin: Boolean = false,
278+
): List<ReviewContext> {
279+
val fileContexts = buildFileContextsInternal(diffText, filterImportsInOrigin)
280+
return fileContexts.map { file ->
281+
ReviewContext(
282+
body = file.originSnippet,
283+
payload = payload,
284+
type = ReviewType.ByFile(path = file.path)
285+
)
286+
}
287+
}
257288
}

src/main/kotlin/com/project/codereview/client/github/GithubReviewClient.kt

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
11
package com.project.codereview.client.github
22

3-
import com.project.codereview.core.dto.GithubReviewDto
4-
import com.project.codereview.core.dto.PullRequestPayload
5-
import org.springframework.beans.factory.annotation.Value
3+
import com.project.codereview.client.github.dto.ReviewContext
64
import org.springframework.stereotype.Service
75
import org.springframework.web.reactive.function.client.WebClient
86

97
@Service
108
class GithubReviewClient(
119
private val tokenProvider: GithubAppTokenProvider
1210
) {
13-
data class ReviewCommentRequest(
14-
val body: String,
15-
val path: String,
16-
val commit_id: String,
17-
val line: Int? = null,
18-
val side: String? = null,
19-
val start_line: Int? = null,
20-
val start_side: String? = null
21-
)
22-
2311
val client = WebClient.builder()
2412
.baseUrl("https://api.github.com")
2513
.defaultHeader("Accept", "application/vnd.github+json")
2614
.build()
2715

2816
suspend fun addReviewSummaryComment(
29-
payload: PullRequestPayload,
30-
installationId: String,
31-
commitId: String,
32-
comment: String
17+
ctx: ReviewContext
3318
) {
34-
val uri = payload.run {
19+
val uri = ctx.run {
3520
"/repos/$owner/$repo/pulls/$prNumber/reviews"
3621
}
37-
val payload = mutableMapOf(
38-
"event" to "COMMENT",
39-
"commit_id" to commitId,
40-
"body" to comment
41-
)
22+
val payload = ctx.type.toPayloadMap(ctx.body, ctx.commitId)
4223

43-
val token = tokenProvider.getInstallationToken(installationId)
24+
val token = tokenProvider.getInstallationToken(ctx.installationId)
4425

4526
client.post()
4627
.uri(uri)
@@ -52,20 +33,18 @@ class GithubReviewClient(
5233
}
5334

5435
suspend fun addReviewComment(
55-
dto: GithubReviewDto
36+
ctx: ReviewContext
5637
) {
57-
val uri = dto.payload.run {
38+
val uri = ctx.run {
5839
"/repos/$owner/$repo/pulls/$prNumber/comments"
5940
}
6041

61-
println("dto.toReviewCommentRequest() = ${dto.toReviewCommentRequest()}")
62-
6342
client.post()
6443
.uri(uri)
6544
.headers {
66-
it.setBearerAuth(tokenProvider.getInstallationToken(dto.installationId))
45+
it.setBearerAuth(tokenProvider.getInstallationToken(ctx.installationId))
6746
}
68-
.bodyValue(dto.toReviewCommentRequest())
47+
.bodyValue(ctx.type.toPayloadMap(ctx.body, ctx.installationId))
6948
.retrieve()
7049
.bodyToMono(String::class.java)
7150
.block()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.project.codereview.client.github.dto
2+
3+
import com.project.codereview.core.dto.GithubPayload
4+
import kotlin.String
5+
6+
data class ReviewContext(
7+
val body: String,
8+
val payload: GithubPayload,
9+
val type: ReviewType
10+
) {
11+
val commitId get() = payload.pull_request.head.sha
12+
val owner get() = payload.pull_request.owner
13+
val repo get() = payload.pull_request.repo
14+
val prNumber get() = payload.pull_request.prNumber
15+
val installationId get() = payload.installation.id
16+
}
17+
18+
sealed class ReviewType {
19+
data class ByComment(
20+
val event: String = "COMMENT",
21+
) : ReviewType() {
22+
override fun toPayloadMap(
23+
body: String,
24+
commitId: String
25+
): Map<String, String> = mapOf(
26+
"event" to "COMMENT",
27+
"commit_id" to commitId,
28+
"body" to body
29+
)
30+
}
31+
32+
data class ByMultiline(
33+
val path: String,
34+
val line: Int,
35+
val side: String,
36+
val start_line: Int,
37+
val start_side: String,
38+
) : ReviewType() {
39+
override fun toPayloadMap(
40+
body: String,
41+
commitId: String
42+
) = mapOf(
43+
"body" to body,
44+
"path" to path,
45+
"commit_id" to commitId,
46+
"line" to "$line",
47+
"side" to side,
48+
"start_line" to "$start_line",
49+
"start_side" to start_side,
50+
)
51+
}
52+
53+
data class ByFile(
54+
val path: String,
55+
) : ReviewType() {
56+
override fun toPayloadMap(
57+
body: String,
58+
commitId: String
59+
): Map<String, String> = mapOf(
60+
"body" to body,
61+
"path" to path,
62+
"commit_id" to commitId,
63+
"subject_type" to "file"
64+
)
65+
}
66+
67+
abstract fun toPayloadMap(
68+
body: String,
69+
commitId: String
70+
): Map<String, String>
71+
72+
fun path() = when(this) {
73+
is ByComment -> ""
74+
is ByFile -> this.path
75+
is ByMultiline -> this.path
76+
}
77+
}

0 commit comments

Comments
 (0)