-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff.go
More file actions
122 lines (118 loc) · 3.43 KB
/
diff.go
File metadata and controls
122 lines (118 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Package review implements the MR-review pipeline: git diff → graph
// evidence → LLM review. Phase 3 of the optimization plan.
//
// Two entry points:
// - CLI: `codeiq review --base <ref> --head <ref>` (cli/review.go)
// - MCP: `review_changes` tool (mcp/tools_review.go)
//
// Both call ReviewService.Review which:
// 1. Shells out to `git diff` for the file list + hunks.
// 2. For each changed file, queries the graph for nodes-in-file,
// blast radius, and evidence packs.
// 3. Renders a single prompt and calls the configured LLM (default:
// Ollama Cloud gpt-oss).
// 4. Returns a structured review report (markdown for CLI, JSON for MCP).
package review
import (
"bufio"
"fmt"
"os/exec"
"strings"
)
// ChangedFile is one entry in the diff.
type ChangedFile struct {
Path string
Hunks []string
AddedLines int
RemovedLines int
}
// ParseDiff parses raw unified `git diff` output into a per-file slice.
// File renames are reported under the new path. Binary diffs are recorded
// with empty hunks and zero line counts.
func ParseDiff(raw string) ([]ChangedFile, error) {
if raw == "" {
return nil, nil
}
var files []ChangedFile
var cur *ChangedFile
var hunkBuf strings.Builder
flushHunk := func() {
if cur != nil && hunkBuf.Len() > 0 {
cur.Hunks = append(cur.Hunks, hunkBuf.String())
hunkBuf.Reset()
}
}
scanner := bufio.NewScanner(strings.NewReader(raw))
scanner.Buffer(make([]byte, 1024*1024), 16*1024*1024) // 16MB max line for big patches
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "diff --git "):
flushHunk()
if cur != nil {
files = append(files, *cur)
}
// `diff --git a/path b/path` — pick the b/ path (new name on
// rename, same path otherwise).
parts := strings.SplitN(line, " ", 4)
path := ""
if len(parts) == 4 {
bSide := strings.TrimPrefix(parts[3], "b/")
path = bSide
}
cur = &ChangedFile{Path: path}
case strings.HasPrefix(line, "+++ b/"):
// File rename or initial add: prefer the +++ b/ side over the
// `diff --git` header parse.
if cur != nil {
cur.Path = strings.TrimPrefix(line, "+++ b/")
}
case strings.HasPrefix(line, "@@"):
flushHunk()
hunkBuf.WriteString(line)
hunkBuf.WriteString("\n")
case strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++"):
if cur != nil {
cur.AddedLines++
}
hunkBuf.WriteString(line)
hunkBuf.WriteString("\n")
case strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---"):
if cur != nil {
cur.RemovedLines++
}
hunkBuf.WriteString(line)
hunkBuf.WriteString("\n")
default:
if hunkBuf.Len() > 0 {
hunkBuf.WriteString(line)
hunkBuf.WriteString("\n")
}
}
}
flushHunk()
if cur != nil {
files = append(files, *cur)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan diff: %w", err)
}
return files, nil
}
// GitDiff shells out to `git diff --unified=3 <base>..<head>` from cwd
// and returns the parsed result. Mirrors the Java DiffParser shellout.
func GitDiff(cwd, base, head string) ([]ChangedFile, error) {
args := []string{"diff", "--unified=3"}
if base != "" && head != "" {
args = append(args, base+".."+head)
} else if base != "" {
args = append(args, base)
}
cmd := exec.Command("git", args...)
cmd.Dir = cwd
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git diff %v: %w", args, err)
}
return ParseDiff(string(out))
}