on cdc5f20 help sign in
crush/internal/git/diff.go
264 lines 6.8 kB history raw
1package git
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10 "time"
12 "github.com/go-git/go-git/v5"
13 "github.com/go-git/go-git/v5/plumbing/object"
16type DiffStats struct {
17 Additions int
18 Removals int
21func GenerateGitDiff(filePath string, contentBefore string, contentAfter string) (string, error) {
22 tempDir, err := os.MkdirTemp("", "git-diff-temp")
23 if err != nil {
24 return "", fmt.Errorf("failed to create temp dir: %w", err)
25 }
26 defer os.RemoveAll(tempDir)
28 repo, err := git.PlainInit(tempDir, false)
29 if err != nil {
30 return "", fmt.Errorf("failed to initialize git repo: %w", err)
31 }
33 wt, err := repo.Worktree()
34 if err != nil {
35 return "", fmt.Errorf("failed to get worktree: %w", err)
36 }
38 fullPath := filepath.Join(tempDir, filePath)
39 if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
40 return "", fmt.Errorf("failed to create directories: %w", err)
41 }
42 if err = os.WriteFile(fullPath, []byte(contentBefore), 0o644); err != nil {
43 return "", fmt.Errorf("failed to write 'before' content: %w", err)
44 }
46 _, err = wt.Add(filePath)
47 if err != nil {
48 return "", fmt.Errorf("failed to add file to git: %w", err)
49 }
51 beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
52 Author: &object.Signature{
53 Name: "OpenCode",
54 Email: "coder@opencode.ai",
55 When: time.Now(),
56 },
57 })
58 if err != nil {
59 return "", fmt.Errorf("failed to commit 'before' version: %w", err)
60 }
62 if err = os.WriteFile(fullPath, []byte(contentAfter), 0o644); err != nil {
63 return "", fmt.Errorf("failed to write 'after' content: %w", err)
64 }
66 _, err = wt.Add(filePath)
67 if err != nil {
68 return "", fmt.Errorf("failed to add updated file to git: %w", err)
69 }
71 afterCommit, err := wt.Commit("After", &git.CommitOptions{
72 Author: &object.Signature{
73 Name: "OpenCode",
74 Email: "coder@opencode.ai",
75 When: time.Now(),
76 },
77 })
78 if err != nil {
79 return "", fmt.Errorf("failed to commit 'after' version: %w", err)
80 }
82 beforeCommitObj, err := repo.CommitObject(beforeCommit)
83 if err != nil {
84 return "", fmt.Errorf("failed to get 'before' commit: %w", err)
85 }
87 afterCommitObj, err := repo.CommitObject(afterCommit)
88 if err != nil {
89 return "", fmt.Errorf("failed to get 'after' commit: %w", err)
90 }
92 patch, err := beforeCommitObj.Patch(afterCommitObj)
93 if err != nil {
94 return "", fmt.Errorf("failed to generate patch: %w", err)
95 }
97 return patch.String(), nil
100func GenerateGitDiffWithStats(filePath string, contentBefore string, contentAfter string) (string, DiffStats, error) {
101 tempDir, err := os.MkdirTemp("", "git-diff-temp")
102 if err != nil {
103 return "", DiffStats{}, fmt.Errorf("failed to create temp dir: %w", err)
104 }
105 defer os.RemoveAll(tempDir)
107 repo, err := git.PlainInit(tempDir, false)
108 if err != nil {
109 return "", DiffStats{}, fmt.Errorf("failed to initialize git repo: %w", err)
110 }
112 wt, err := repo.Worktree()
113 if err != nil {
114 return "", DiffStats{}, fmt.Errorf("failed to get worktree: %w", err)
115 }
117 fullPath := filepath.Join(tempDir, filePath)
118 if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
119 return "", DiffStats{}, fmt.Errorf("failed to create directories: %w", err)
120 }
121 if err = os.WriteFile(fullPath, []byte(contentBefore), 0o644); err != nil {
122 return "", DiffStats{}, fmt.Errorf("failed to write 'before' content: %w", err)
123 }
125 _, err = wt.Add(filePath)
126 if err != nil {
127 return "", DiffStats{}, fmt.Errorf("failed to add file to git: %w", err)
128 }
130 beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
131 Author: &object.Signature{
132 Name: "OpenCode",
133 Email: "coder@opencode.ai",
134 When: time.Now(),
135 },
136 })
137 if err != nil {
138 return "", DiffStats{}, fmt.Errorf("failed to commit 'before' version: %w", err)
139 }
141 if err = os.WriteFile(fullPath, []byte(contentAfter), 0o644); err != nil {
142 return "", DiffStats{}, fmt.Errorf("failed to write 'after' content: %w", err)
143 }
145 _, err = wt.Add(filePath)
146 if err != nil {
147 return "", DiffStats{}, fmt.Errorf("failed to add updated file to git: %w", err)
148 }
150 afterCommit, err := wt.Commit("After", &git.CommitOptions{
151 Author: &object.Signature{
152 Name: "OpenCode",
153 Email: "coder@opencode.ai",
154 When: time.Now(),
155 },
156 })
157 if err != nil {
158 return "", DiffStats{}, fmt.Errorf("failed to commit 'after' version: %w", err)
159 }
161 beforeCommitObj, err := repo.CommitObject(beforeCommit)
162 if err != nil {
163 return "", DiffStats{}, fmt.Errorf("failed to get 'before' commit: %w", err)
164 }
166 afterCommitObj, err := repo.CommitObject(afterCommit)
167 if err != nil {
168 return "", DiffStats{}, fmt.Errorf("failed to get 'after' commit: %w", err)
169 }
171 patch, err := beforeCommitObj.Patch(afterCommitObj)
172 if err != nil {
173 return "", DiffStats{}, fmt.Errorf("failed to generate patch: %w", err)
174 }
176 stats := DiffStats{}
177 for _, fileStat := range patch.Stats() {
178 stats.Additions += fileStat.Addition
179 stats.Removals += fileStat.Deletion
180 }
182 return patch.String(), stats, nil
185func FormatDiff(diffText string, width int) (string, error) {
186 if isSplitDiffsAvailable() {
187 return formatWithSplitDiffs(diffText, width)
188 }
190 return formatSimple(diffText), nil
193func isSplitDiffsAvailable() bool {
194 _, err := exec.LookPath("node")
195 return err == nil
198func formatWithSplitDiffs(diffText string, width int) (string, error) {
199 args := []string{
200 "--color",
201 }
203 var diffCmd *exec.Cmd
205 if _, err := exec.LookPath("git-split-diffs-opencode"); err == nil {
206 fullArgs := append([]string{"git-split-diffs-opencode"}, args...)
207 diffCmd = exec.Command(fullArgs[0], fullArgs[1:]...)
208 } else {
209 npxArgs := append([]string{"git-split-diffs-opencode"}, args...)
210 diffCmd = exec.Command("npx", npxArgs...)
211 }
213 diffCmd.Env = append(os.Environ(), fmt.Sprintf("DIFF_COLUMNS=%d", width))
215 diffCmd.Stdin = strings.NewReader(diffText)
217 var out bytes.Buffer
218 diffCmd.Stdout = &out
220 var stderr bytes.Buffer
221 diffCmd.Stderr = &stderr
223 if err := diffCmd.Run(); err != nil {
224 return "", fmt.Errorf("git-split-diffs-opencode error: %w, stderr: %s", err, stderr.String())
225 }
227 return out.String(), nil
230func formatSimple(diffText string) string {
231 lines := strings.Split(diffText, "\n")
232 var result strings.Builder
234 for _, line := range lines {
235 if len(line) == 0 {
236 result.WriteString("\n")
237 continue
238 }
240 switch line[0] {
241 case '+':
242 result.WriteString("\033[32m" + line + "\033[0m\n")
243 case '-':
244 result.WriteString("\033[31m" + line + "\033[0m\n")
245 case '@':
246 result.WriteString("\033[36m" + line + "\033[0m\n")
247 case 'd':
248 if strings.HasPrefix(line, "diff --git") {
249 result.WriteString("\033[1m" + line + "\033[0m\n")
250 } else {
251 result.WriteString(line + "\n")
252 }
253 default:
254 result.WriteString(line + "\n")
255 }
256 }
258 if !strings.HasSuffix(diffText, "\n") {
259 output := result.String()
260 return output[:len(output)-1]
261 }
263 return result.String()