Files
github--github-mcp-server/pkg/github/diff_integration.go
Sam Morrow 88a6a37c51 Apply semantic diffs to get_commit, get_diff, and get_files
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.
2026-02-10 00:09:41 +01:00

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
}