diff.go 6.5 KB

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