diff.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package git
  2. import (
  3. "bytes"
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. "github.com/go-git/go-git/v5"
  11. "github.com/go-git/go-git/v5/plumbing/object"
  12. )
  13. type DiffStats struct {
  14. Additions int
  15. Removals int
  16. }
  17. func GenerateGitDiff(filePath string, contentBefore string, contentAfter string) (string, error) {
  18. tempDir, err := os.MkdirTemp("", "git-diff-temp")
  19. if err != nil {
  20. return "", fmt.Errorf("failed to create temp dir: %w", err)
  21. }
  22. defer os.RemoveAll(tempDir)
  23. repo, err := git.PlainInit(tempDir, false)
  24. if err != nil {
  25. return "", fmt.Errorf("failed to initialize git repo: %w", err)
  26. }
  27. wt, err := repo.Worktree()
  28. if err != nil {
  29. return "", fmt.Errorf("failed to get worktree: %w", err)
  30. }
  31. fullPath := filepath.Join(tempDir, filePath)
  32. if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
  33. return "", fmt.Errorf("failed to create directories: %w", err)
  34. }
  35. if err = os.WriteFile(fullPath, []byte(contentBefore), 0o644); err != nil {
  36. return "", fmt.Errorf("failed to write 'before' content: %w", err)
  37. }
  38. _, err = wt.Add(filePath)
  39. if err != nil {
  40. return "", fmt.Errorf("failed to add file to git: %w", err)
  41. }
  42. beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
  43. Author: &object.Signature{
  44. Name: "OpenCode",
  45. Email: "[email protected]",
  46. When: time.Now(),
  47. },
  48. })
  49. if err != nil {
  50. return "", fmt.Errorf("failed to commit 'before' version: %w", err)
  51. }
  52. if err = os.WriteFile(fullPath, []byte(contentAfter), 0o644); err != nil {
  53. return "", fmt.Errorf("failed to write 'after' content: %w", err)
  54. }
  55. _, err = wt.Add(filePath)
  56. if err != nil {
  57. return "", fmt.Errorf("failed to add updated file to git: %w", err)
  58. }
  59. afterCommit, err := wt.Commit("After", &git.CommitOptions{
  60. Author: &object.Signature{
  61. Name: "OpenCode",
  62. Email: "[email protected]",
  63. When: time.Now(),
  64. },
  65. })
  66. if err != nil {
  67. return "", fmt.Errorf("failed to commit 'after' version: %w", err)
  68. }
  69. beforeCommitObj, err := repo.CommitObject(beforeCommit)
  70. if err != nil {
  71. return "", fmt.Errorf("failed to get 'before' commit: %w", err)
  72. }
  73. afterCommitObj, err := repo.CommitObject(afterCommit)
  74. if err != nil {
  75. return "", fmt.Errorf("failed to get 'after' commit: %w", err)
  76. }
  77. patch, err := beforeCommitObj.Patch(afterCommitObj)
  78. if err != nil {
  79. return "", fmt.Errorf("failed to generate patch: %w", err)
  80. }
  81. return patch.String(), nil
  82. }
  83. func GenerateGitDiffWithStats(filePath string, contentBefore string, contentAfter string) (string, DiffStats, error) {
  84. tempDir, err := os.MkdirTemp("", "git-diff-temp")
  85. if err != nil {
  86. return "", DiffStats{}, fmt.Errorf("failed to create temp dir: %w", err)
  87. }
  88. defer os.RemoveAll(tempDir)
  89. repo, err := git.PlainInit(tempDir, false)
  90. if err != nil {
  91. return "", DiffStats{}, fmt.Errorf("failed to initialize git repo: %w", err)
  92. }
  93. wt, err := repo.Worktree()
  94. if err != nil {
  95. return "", DiffStats{}, fmt.Errorf("failed to get worktree: %w", err)
  96. }
  97. fullPath := filepath.Join(tempDir, filePath)
  98. if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
  99. return "", DiffStats{}, fmt.Errorf("failed to create directories: %w", err)
  100. }
  101. if err = os.WriteFile(fullPath, []byte(contentBefore), 0o644); err != nil {
  102. return "", DiffStats{}, fmt.Errorf("failed to write 'before' content: %w", err)
  103. }
  104. _, err = wt.Add(filePath)
  105. if err != nil {
  106. return "", DiffStats{}, fmt.Errorf("failed to add file to git: %w", err)
  107. }
  108. beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
  109. Author: &object.Signature{
  110. Name: "OpenCode",
  111. Email: "[email protected]",
  112. When: time.Now(),
  113. },
  114. })
  115. if err != nil {
  116. return "", DiffStats{}, fmt.Errorf("failed to commit 'before' version: %w", err)
  117. }
  118. if err = os.WriteFile(fullPath, []byte(contentAfter), 0o644); err != nil {
  119. return "", DiffStats{}, fmt.Errorf("failed to write 'after' content: %w", err)
  120. }
  121. _, err = wt.Add(filePath)
  122. if err != nil {
  123. return "", DiffStats{}, fmt.Errorf("failed to add updated file to git: %w", err)
  124. }
  125. afterCommit, err := wt.Commit("After", &git.CommitOptions{
  126. Author: &object.Signature{
  127. Name: "OpenCode",
  128. Email: "[email protected]",
  129. When: time.Now(),
  130. },
  131. })
  132. if err != nil {
  133. return "", DiffStats{}, fmt.Errorf("failed to commit 'after' version: %w", err)
  134. }
  135. beforeCommitObj, err := repo.CommitObject(beforeCommit)
  136. if err != nil {
  137. return "", DiffStats{}, fmt.Errorf("failed to get 'before' commit: %w", err)
  138. }
  139. afterCommitObj, err := repo.CommitObject(afterCommit)
  140. if err != nil {
  141. return "", DiffStats{}, fmt.Errorf("failed to get 'after' commit: %w", err)
  142. }
  143. patch, err := beforeCommitObj.Patch(afterCommitObj)
  144. if err != nil {
  145. return "", DiffStats{}, fmt.Errorf("failed to generate patch: %w", err)
  146. }
  147. stats := DiffStats{}
  148. for _, fileStat := range patch.Stats() {
  149. stats.Additions += fileStat.Addition
  150. stats.Removals += fileStat.Deletion
  151. }
  152. return patch.String(), stats, nil
  153. }
  154. func FormatDiff(diffText string, width int) (string, error) {
  155. if isSplitDiffsAvailable() {
  156. return formatWithSplitDiffs(diffText, width)
  157. }
  158. return formatSimple(diffText), nil
  159. }
  160. func isSplitDiffsAvailable() bool {
  161. _, err := exec.LookPath("node")
  162. return err == nil
  163. }
  164. func formatWithSplitDiffs(diffText string, width int) (string, error) {
  165. args := []string{
  166. "--color",
  167. }
  168. var diffCmd *exec.Cmd
  169. if _, err := exec.LookPath("git-split-diffs-opencode"); err == nil {
  170. fullArgs := append([]string{"git-split-diffs-opencode"}, args...)
  171. diffCmd = exec.Command(fullArgs[0], fullArgs[1:]...)
  172. } else {
  173. npxArgs := append([]string{"git-split-diffs-opencode"}, args...)
  174. diffCmd = exec.Command("npx", npxArgs...)
  175. }
  176. diffCmd.Env = append(os.Environ(), fmt.Sprintf("DIFF_COLUMNS=%d", width))
  177. diffCmd.Stdin = strings.NewReader(diffText)
  178. var out bytes.Buffer
  179. diffCmd.Stdout = &out
  180. var stderr bytes.Buffer
  181. diffCmd.Stderr = &stderr
  182. if err := diffCmd.Run(); err != nil {
  183. return "", fmt.Errorf("git-split-diffs-opencode error: %w, stderr: %s", err, stderr.String())
  184. }
  185. return out.String(), nil
  186. }
  187. func formatSimple(diffText string) string {
  188. lines := strings.Split(diffText, "\n")
  189. var result strings.Builder
  190. for _, line := range lines {
  191. if len(line) == 0 {
  192. result.WriteString("\n")
  193. continue
  194. }
  195. switch line[0] {
  196. case '+':
  197. result.WriteString("\033[32m" + line + "\033[0m\n")
  198. case '-':
  199. result.WriteString("\033[31m" + line + "\033[0m\n")
  200. case '@':
  201. result.WriteString("\033[36m" + line + "\033[0m\n")
  202. case 'd':
  203. if strings.HasPrefix(line, "diff --git") {
  204. result.WriteString("\033[1m" + line + "\033[0m\n")
  205. } else {
  206. result.WriteString(line + "\n")
  207. }
  208. default:
  209. result.WriteString(line + "\n")
  210. }
  211. }
  212. if !strings.HasSuffix(diffText, "\n") {
  213. output := result.String()
  214. return output[:len(output)-1]
  215. }
  216. return result.String()
  217. }