| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- package git
- import (
- "bytes"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "time"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing/object"
- )
- type DiffStats struct {
- Additions int
- Removals int
- }
- func GenerateGitDiff(filePath string, contentBefore string, contentAfter string) (string, error) {
- tempDir, err := os.MkdirTemp("", "git-diff-temp")
- if err != nil {
- return "", fmt.Errorf("failed to create temp dir: %w", err)
- }
- defer os.RemoveAll(tempDir)
- repo, err := git.PlainInit(tempDir, false)
- if err != nil {
- return "", fmt.Errorf("failed to initialize git repo: %w", err)
- }
- wt, err := repo.Worktree()
- if err != nil {
- return "", fmt.Errorf("failed to get worktree: %w", err)
- }
- fullPath := filepath.Join(tempDir, filePath)
- if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
- return "", fmt.Errorf("failed to create directories: %w", err)
- }
- if err = os.WriteFile(fullPath, []byte(contentBefore), 0o644); err != nil {
- return "", fmt.Errorf("failed to write 'before' content: %w", err)
- }
- _, err = wt.Add(filePath)
- if err != nil {
- return "", fmt.Errorf("failed to add file to git: %w", err)
- }
- beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
- Author: &object.Signature{
- Name: "OpenCode",
- Email: "[email protected]",
- When: time.Now(),
- },
- })
- if err != nil {
- return "", fmt.Errorf("failed to commit 'before' version: %w", err)
- }
- if err = os.WriteFile(fullPath, []byte(contentAfter), 0o644); err != nil {
- return "", fmt.Errorf("failed to write 'after' content: %w", err)
- }
- _, err = wt.Add(filePath)
- if err != nil {
- return "", fmt.Errorf("failed to add updated file to git: %w", err)
- }
- afterCommit, err := wt.Commit("After", &git.CommitOptions{
- Author: &object.Signature{
- Name: "OpenCode",
- Email: "[email protected]",
- When: time.Now(),
- },
- })
- if err != nil {
- return "", fmt.Errorf("failed to commit 'after' version: %w", err)
- }
- beforeCommitObj, err := repo.CommitObject(beforeCommit)
- if err != nil {
- return "", fmt.Errorf("failed to get 'before' commit: %w", err)
- }
- afterCommitObj, err := repo.CommitObject(afterCommit)
- if err != nil {
- return "", fmt.Errorf("failed to get 'after' commit: %w", err)
- }
- patch, err := beforeCommitObj.Patch(afterCommitObj)
- if err != nil {
- return "", fmt.Errorf("failed to generate patch: %w", err)
- }
- return patch.String(), nil
- }
- func GenerateGitDiffWithStats(filePath string, contentBefore string, contentAfter string) (string, DiffStats, error) {
- tempDir, err := os.MkdirTemp("", "git-diff-temp")
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to create temp dir: %w", err)
- }
- defer os.RemoveAll(tempDir)
- repo, err := git.PlainInit(tempDir, false)
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to initialize git repo: %w", err)
- }
- wt, err := repo.Worktree()
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to get worktree: %w", err)
- }
- fullPath := filepath.Join(tempDir, filePath)
- if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to create directories: %w", err)
- }
- if err = os.WriteFile(fullPath, []byte(contentBefore), 0o644); err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to write 'before' content: %w", err)
- }
- _, err = wt.Add(filePath)
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to add file to git: %w", err)
- }
- beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
- Author: &object.Signature{
- Name: "OpenCode",
- Email: "[email protected]",
- When: time.Now(),
- },
- })
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to commit 'before' version: %w", err)
- }
- if err = os.WriteFile(fullPath, []byte(contentAfter), 0o644); err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to write 'after' content: %w", err)
- }
- _, err = wt.Add(filePath)
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to add updated file to git: %w", err)
- }
- afterCommit, err := wt.Commit("After", &git.CommitOptions{
- Author: &object.Signature{
- Name: "OpenCode",
- Email: "[email protected]",
- When: time.Now(),
- },
- })
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to commit 'after' version: %w", err)
- }
- beforeCommitObj, err := repo.CommitObject(beforeCommit)
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to get 'before' commit: %w", err)
- }
- afterCommitObj, err := repo.CommitObject(afterCommit)
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to get 'after' commit: %w", err)
- }
- patch, err := beforeCommitObj.Patch(afterCommitObj)
- if err != nil {
- return "", DiffStats{}, fmt.Errorf("failed to generate patch: %w", err)
- }
- stats := DiffStats{}
- for _, fileStat := range patch.Stats() {
- stats.Additions += fileStat.Addition
- stats.Removals += fileStat.Deletion
- }
- return patch.String(), stats, nil
- }
- func FormatDiff(diffText string, width int) (string, error) {
- if isSplitDiffsAvailable() {
- return formatWithSplitDiffs(diffText, width)
- }
- return formatSimple(diffText), nil
- }
- func isSplitDiffsAvailable() bool {
- _, err := exec.LookPath("node")
- return err == nil
- }
- func formatWithSplitDiffs(diffText string, width int) (string, error) {
- args := []string{
- "--color",
- }
- var diffCmd *exec.Cmd
- if _, err := exec.LookPath("git-split-diffs-opencode"); err == nil {
- fullArgs := append([]string{"git-split-diffs-opencode"}, args...)
- diffCmd = exec.Command(fullArgs[0], fullArgs[1:]...)
- } else {
- npxArgs := append([]string{"git-split-diffs-opencode"}, args...)
- diffCmd = exec.Command("npx", npxArgs...)
- }
- diffCmd.Env = append(os.Environ(), fmt.Sprintf("DIFF_COLUMNS=%d", width))
- diffCmd.Stdin = strings.NewReader(diffText)
- var out bytes.Buffer
- diffCmd.Stdout = &out
- var stderr bytes.Buffer
- diffCmd.Stderr = &stderr
- if err := diffCmd.Run(); err != nil {
- return "", fmt.Errorf("git-split-diffs-opencode error: %w, stderr: %s", err, stderr.String())
- }
- return out.String(), nil
- }
- func formatSimple(diffText string) string {
- lines := strings.Split(diffText, "\n")
- var result strings.Builder
- for _, line := range lines {
- if len(line) == 0 {
- result.WriteString("\n")
- continue
- }
- switch line[0] {
- case '+':
- result.WriteString("\033[32m" + line + "\033[0m\n")
- case '-':
- result.WriteString("\033[31m" + line + "\033[0m\n")
- case '@':
- result.WriteString("\033[36m" + line + "\033[0m\n")
- case 'd':
- if strings.HasPrefix(line, "diff --git") {
- result.WriteString("\033[1m" + line + "\033[0m\n")
- } else {
- result.WriteString(line + "\n")
- }
- default:
- result.WriteString(line + "\n")
- }
- }
- if !strings.HasSuffix(diffText, "\n") {
- output := result.String()
- return output[:len(output)-1]
- }
- return result.String()
- }
|