88a6a37c51
Integrates the semantic diff engine into existing tools that return file patches: - get_commit: Adds Patch field to MinimalCommitFile and applies semantic diff for supported formats (JSON, YAML, CSV, TOML) and structural diff for code files - pull_request_read (get_diff): Splits multi-file raw diff by file and applies semantic diff per-file where beneficial - pull_request_read (get_files): Applies semantic diff to each file's Patch field before returning Unsupported file types keep their original unified diff patches unchanged. Patches are reconstructed from hunks to feed the semantic diff engine — this works well for structured data where the full content is typically in the diff.
168 lines
4.4 KiB
Go
168 lines
4.4 KiB
Go
package github
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// applySemanticDiffToUnifiedPatch takes a unified diff patch for a single file
|
|
// and attempts to produce a semantic diff. It reconstructs the base and head
|
|
// content from the patch hunks, then runs them through SemanticDiff.
|
|
// Returns the original patch unchanged if the file type doesn't benefit from
|
|
// semantic diffing or if reconstruction fails.
|
|
func applySemanticDiffToUnifiedPatch(filename, patch string) string {
|
|
if patch == "" {
|
|
return patch
|
|
}
|
|
|
|
format := DetectDiffFormat(filename)
|
|
if format == DiffFormatUnified {
|
|
// Not a structured data or code file — keep the original patch
|
|
return patch
|
|
}
|
|
|
|
base, head, ok := reconstructFromPatch(patch)
|
|
if !ok {
|
|
return patch
|
|
}
|
|
|
|
result := SemanticDiff(filename, base, head)
|
|
if result.Format == DiffFormatFallback {
|
|
return patch
|
|
}
|
|
|
|
return result.Diff
|
|
}
|
|
|
|
// reconstructFromPatch extracts the base and head file content from a unified
|
|
// diff patch. Returns the reconstructed contents and true if successful.
|
|
// This only works well for complete file diffs — partial context diffs will
|
|
// produce incomplete content, which is fine for semantic comparison of
|
|
// structured data where the full structure is usually in the diff.
|
|
func reconstructFromPatch(patch string) (base, head []byte, ok bool) {
|
|
lines := strings.Split(patch, "\n")
|
|
|
|
var baseLines, headLines []string
|
|
inHunk := false
|
|
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "@@") {
|
|
inHunk = true
|
|
continue
|
|
}
|
|
if !inHunk {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(line, "-"):
|
|
baseLines = append(baseLines, line[1:])
|
|
case strings.HasPrefix(line, "+"):
|
|
headLines = append(headLines, line[1:])
|
|
case strings.HasPrefix(line, " "):
|
|
baseLines = append(baseLines, line[1:])
|
|
headLines = append(headLines, line[1:])
|
|
case line == "":
|
|
// Could be end of patch or an empty context line
|
|
baseLines = append(baseLines, "")
|
|
headLines = append(headLines, "")
|
|
}
|
|
}
|
|
|
|
if len(baseLines) == 0 && len(headLines) == 0 {
|
|
return nil, nil, false
|
|
}
|
|
|
|
return []byte(strings.Join(baseLines, "\n")),
|
|
[]byte(strings.Join(headLines, "\n")),
|
|
true
|
|
}
|
|
|
|
// processMultiFileDiff splits a multi-file unified diff into per-file sections
|
|
// and applies semantic diffing to each file where applicable. Returns a
|
|
// combined result with structural diffs for supported formats and original
|
|
// patches for unsupported ones.
|
|
func processMultiFileDiff(rawDiff string) string {
|
|
sections := splitDiffByFile(rawDiff)
|
|
if len(sections) == 0 {
|
|
return rawDiff
|
|
}
|
|
|
|
var result strings.Builder
|
|
for i, section := range sections {
|
|
if i > 0 {
|
|
result.WriteString("\n")
|
|
}
|
|
|
|
semanticPatch := applySemanticDiffToUnifiedPatch(section.filename, section.patch)
|
|
if semanticPatch != section.patch {
|
|
result.WriteString(fmt.Sprintf("--- %s (semantic diff) ---\n", section.filename))
|
|
result.WriteString(semanticPatch)
|
|
} else {
|
|
result.WriteString(section.header)
|
|
if section.patch != "" {
|
|
result.WriteString("\n")
|
|
result.WriteString(section.patch)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
type diffSection struct {
|
|
filename string
|
|
header string
|
|
patch string
|
|
}
|
|
|
|
// splitDiffByFile splits a raw multi-file unified diff into per-file sections.
|
|
func splitDiffByFile(rawDiff string) []diffSection {
|
|
lines := strings.Split(rawDiff, "\n")
|
|
var sections []diffSection
|
|
var current *diffSection
|
|
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "diff --git ") {
|
|
if current != nil {
|
|
sections = append(sections, *current)
|
|
}
|
|
// Extract filename from "diff --git a/path b/path"
|
|
parts := strings.SplitN(line, " b/", 2)
|
|
filename := ""
|
|
if len(parts) == 2 {
|
|
filename = parts[1]
|
|
}
|
|
current = &diffSection{
|
|
filename: filename,
|
|
header: line,
|
|
}
|
|
continue
|
|
}
|
|
|
|
if current == nil {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(line, "---") || strings.HasPrefix(line, "+++") ||
|
|
strings.HasPrefix(line, "index ") || strings.HasPrefix(line, "new file") ||
|
|
strings.HasPrefix(line, "deleted file") || strings.HasPrefix(line, "old mode") ||
|
|
strings.HasPrefix(line, "new mode") || strings.HasPrefix(line, "similarity") ||
|
|
strings.HasPrefix(line, "rename ") || strings.HasPrefix(line, "Binary") {
|
|
current.header += "\n" + line
|
|
} else {
|
|
if current.patch != "" {
|
|
current.patch += "\n" + line
|
|
} else {
|
|
current.patch = line
|
|
}
|
|
}
|
|
}
|
|
|
|
if current != nil {
|
|
sections = append(sections, *current)
|
|
}
|
|
|
|
return sections
|
|
}
|