Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ dist/

# macOS
.DS_Store

# AI
superpowers/
178 changes: 178 additions & 0 deletions src/git/gitCleanCodeContextCollector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
collectPreparedEvidence,
type PreparedCodeContextEvidence
} from "./gitCodeContextEvidenceBuilder.js"
import {
changedFiles,
diffHunks,
pairCodeContext
} from "./gitCodeContextReader.js"
import {
boundedContextRange,
functionRangeFor,
newRange,
oldRange,
overlapRegions,
unionRange
} from "./mergeCodeContextParser.js"
import type {
GitDiffHunk,
GitMergeTreePairResult,
MergeCodeContextEvidence,
MergeCodeContextLineRange
} from "./types.js"

// clean 조합에서 양쪽 exact hunk가 merge-base 좌표로 겹치는 문맥만 수집
export async function collectCleanOverlapEvidence(
mergeResult: GitMergeTreePairResult,
pairIndex: number,
repositoryPath: string
): Promise<MergeCodeContextEvidence[]> {
const context = await pairCodeContext(mergeResult, repositoryPath)
const [leftFiles, rightFiles] = await Promise.all([
changedFiles(repositoryPath, context.mergeBaseOid, context.leftOid),
changedFiles(repositoryPath, context.mergeBaseOid, context.rightOid)
])
const rightFileSet = new Set(rightFiles)
const commonFiles = leftFiles
.filter(filePath => rightFileSet.has(filePath))
.sort(compareText)
const prepared: PreparedCodeContextEvidence[] = []

for (const [fileIndex, filePath] of commonFiles.entries()) {
const [leftHunks, rightHunks] = await Promise.all([
diffHunks(
repositoryPath,
context.mergeBaseOid,
context.leftOid,
filePath,
"exact"
),
diffHunks(
repositoryPath,
context.mergeBaseOid,
context.rightOid,
filePath,
"exact"
)
])
const regions = overlapRegions(filePath, leftHunks, rightHunks)

if (!regions.length) {
continue
}

const [
mergedHunks,
leftFunctionHunks,
rightFunctionHunks,
mergedFunctionHunks
] = await Promise.all([
diffHunks(
repositoryPath,
context.mergeBaseOid,
context.mergedTreeOid,
filePath,
"exact"
),
diffHunks(
repositoryPath,
context.mergeBaseOid,
context.leftOid,
filePath,
"function"
),
diffHunks(
repositoryPath,
context.mergeBaseOid,
context.rightOid,
filePath,
"function"
),
diffHunks(
repositoryPath,
context.mergeBaseOid,
context.mergedTreeOid,
filePath,
"function"
)
])

for (const [regionIndex, region] of regions.entries()) {
const mergedTarget = mappedNewRangeFor(
mergedHunks,
region.baseRange
) ?? unionRange(region.leftRange, region.rightRange)
const key = `${pairIndex}:${fileIndex}:${regionIndex}`

prepared.push({
filePath,
baseRange: boundedContextRange(
region.baseRange,
functionRangeFor(
[...leftFunctionHunks, ...rightFunctionHunks],
"old",
region.baseRange
)
),
leftRange: boundedContextRange(
region.leftRange,
functionRangeFor(leftFunctionHunks, "new", region.leftRange)
),
rightRange: boundedContextRange(
region.rightRange,
functionRangeFor(rightFunctionHunks, "new", region.rightRange)
),
mergedRange: boundedContextRange(
mergedTarget,
functionRangeFor(mergedFunctionHunks, "new", mergedTarget)
),
baseKey: `${key}:base`,
leftKey: `${key}:left`,
rightKey: `${key}:right`,
mergedKey: `${key}:merged`
})
}
}

return collectPreparedEvidence(
mergeResult,
"clean_hunk_overlap",
context,
prepared,
repositoryPath
)
}

// base 좌표와 겹치는 merged hunk의 변경 후 좌표를 선택
function mappedNewRangeFor(
hunks: GitDiffHunk[],
baseRange: MergeCodeContextLineRange
): MergeCodeContextLineRange | undefined {
return hunks
.map(hunk => ({
oldRange: oldRange(hunk),
newRange: newRange(hunk)
}))
.filter(item =>
item.oldRange !== undefined &&
item.newRange !== undefined &&
Math.max(item.oldRange.startLine, baseRange.startLine) <=
Math.min(item.oldRange.endLine, baseRange.endLine)
)
.sort((item, other) => {
const range = item.oldRange!
const otherRange = other.oldRange!

return range.endLine - range.startLine -
(otherRange.endLine - otherRange.startLine)
})[0]?.newRange
}

function compareText(text: string, other: string): number {
if (text === other) {
return 0
}

return text < other ? -1 : 1
}
156 changes: 156 additions & 0 deletions src/git/gitCodeContextEvidenceBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { readGitObjectSnippets } from "./gitObjectBatchReader.js"
import type { PairCodeContext } from "./gitCodeContextReader.js"
import type {
GitMergeTreePairResult,
GitObjectSnippetRequest,
MergeCodeContextEvidence,
MergeCodeContextKind,
MergeCodeContextLineRange,
MergeCodeContextSnippet
} from "./types.js"

export type PreparedCodeContextEvidence = {
filePath: string
baseRange: MergeCodeContextLineRange
leftRange: MergeCodeContextLineRange
rightRange: MergeCodeContextLineRange
mergedRange: MergeCodeContextLineRange
baseKey: string
leftKey: string
rightKey: string
mergedKey: string
}

// 준비된 네 version 요청을 evidence model로 변환
export async function collectPreparedEvidence(
mergeResult: GitMergeTreePairResult,
kind: MergeCodeContextKind,
context: PairCodeContext,
prepared: PreparedCodeContextEvidence[],
repositoryPath: string
): Promise<MergeCodeContextEvidence[]> {
const snippets = await readGitObjectSnippets(
snippetRequests(
prepared,
context.mergeBaseOid,
context.leftOid,
context.rightOid,
context.mergedTreeOid
),
{ repositoryPath }
)

return prepared.map(item => {
const baseSnippet = requiredSnippet(snippets, item.baseKey)
const leftSnippet = deletedVersionSnippet(
baseSnippet,
requiredSnippet(snippets, item.leftKey)
)
const rightSnippet = deletedVersionSnippet(
baseSnippet,
requiredSnippet(snippets, item.rightKey)
)
const mergedSnippet = deletedMergedSnippet(
baseSnippet,
leftSnippet,
rightSnippet,
requiredSnippet(snippets, item.mergedKey)
)

return {
pair: mergeResult.pair,
kind,
filePath: item.filePath,
mergeBaseOid: context.mergeBaseOid,
mergedTreeOid: context.mergedTreeOid,
baseCommit: context.baseCommit,
leftCommit: context.leftCommit,
rightCommit: context.rightCommit,
baseSnippet,
leftSnippet,
rightSnippet,
mergedSnippet
}
})
}

// 준비된 evidence를 Git object batch 요청 네 개로 변환
function snippetRequests(
prepared: PreparedCodeContextEvidence[],
mergeBaseOid: string,
leftOid: string,
rightOid: string,
mergedTreeOid: string
): GitObjectSnippetRequest[] {
return prepared.flatMap(item => [{
key: item.baseKey,
objectOid: mergeBaseOid,
filePath: item.filePath,
range: item.baseRange
}, {
key: item.leftKey,
objectOid: leftOid,
filePath: item.filePath,
range: item.leftRange
}, {
key: item.rightKey,
objectOid: rightOid,
filePath: item.filePath,
range: item.rightRange
}, {
key: item.mergedKey,
objectOid: mergedTreeOid,
filePath: item.filePath,
range: item.mergedRange
}])
}

function requiredSnippet(
snippets: ReadonlyMap<string, MergeCodeContextSnippet>,
key: string
): MergeCodeContextSnippet {
const snippet = snippets.get(key)

if (!snippet) {
throw new Error(`Missing Git object snippet ${key}`)
}

return snippet
}

// base에 존재하던 file이 side object에 없으면 deleted로 분류
function deletedVersionSnippet(
baseSnippet: MergeCodeContextSnippet,
snippet: MergeCodeContextSnippet
): MergeCodeContextSnippet {
if (snippet.status !== "missing" || baseSnippet.status === "missing") {
return snippet
}

return {
...snippet,
status: "deleted"
}
}

// 어느 version에든 존재한 file이 merge tree에서 없으면 deleted로 분류
function deletedMergedSnippet(
baseSnippet: MergeCodeContextSnippet,
leftSnippet: MergeCodeContextSnippet,
rightSnippet: MergeCodeContextSnippet,
mergedSnippet: MergeCodeContextSnippet
): MergeCodeContextSnippet {
if (
mergedSnippet.status !== "missing" ||
[baseSnippet, leftSnippet, rightSnippet].every(
snippet => snippet.status === "missing"
)
) {
return mergedSnippet
}

return {
...mergedSnippet,
status: "deleted"
}
}
Loading