package git import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "strings" ) type Object struct { Path string `json:"path,omitempty" xml:"path,omitempty" text:"Path,omitempty"` Commit Commit `json:"commit,omitempty" xml:"commit,omitempty" text:"Commit,omitempty"` Commits []IncludedCommits `json:"includedCommits,omitempty" xml:"includedcommits>commit,omitempty" text:"Included Commits,omitempty"` Diff string `json:"diff,omitempty" xml:"diff,omitempty" text:"Diff,omitempty"` File string `json:"file,omitempty" xml:"file,omitempty" text:"File,omitempty"` } type IncludedCommits struct { Hash string Included bool } // Commit represents a Git commit with relevant details. type Commit struct { Hash string `json:"commit,omitempty" xml:"hash,omitempty" text:"Hash,omitempty"` Author string `json:"author,omitempty" xml:"author,omitempty" text:"Author,omitempty"` Date string `json:"date,omitempty" xml:"date,omitempty" text:"Date,omitempty"` Email string `json:"email,omitempty" xml:"email,omitempty" text:"Email,omitempty"` Message string `json:"message,omitempty" xml:"message,omitempty" text:"Message,omitempty"` Files []File `json:"files,omitempty" xml:"files>file,omitempty" text:"Files,omitempty"` } type File struct { Status string `json:"status" xml:"status" text:"Status"` File string `json:"file" xml:"file" text:"File"` } // GetCommits retrieves the commit log in JSON format. func GetCommits(path string) ([]Commit, error) { cmd := exec.Command("git", "log", "--pretty=format:%H|%an|%aD|%ae|%s", "--date=short") cmd.Dir = path output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to get commits: %w", err) } lines := strings.Split(strings.TrimSpace(string(output)), "\n") commits := []Commit{} for _, line := range lines { parts := strings.SplitN(line, "|", 5) if len(parts) < 5 { continue } // Ensure proper JSON escaping message := strings.ReplaceAll(parts[4], `"`, `\"`) files, err := GetFilesInCommit(path, parts[0]) if err != nil { return commits, err } changes, err := GetChangesInCommit(path, parts[0]) if err != nil { return commits, err } filesWithChanges := []File{} for _, v := range files { status := "" for _, c := range changes { if c.File == v { status = c.Status break } } filesWithChanges = append(filesWithChanges, File{ File: v, Status: status, }) } commit := Commit{ Hash: parts[0], Author: parts[1], Date: parts[2], Email: parts[3], Message: message, Files: filesWithChanges, } commits = append(commits, commit) } return commits, nil } // GetCommitsForFile returns all commits that modified a given file. func GetCommitsForFile(repoPath, filePath string) ([]string, error) { cmd := exec.Command("git", "log", "--follow", "--format=%H", "--", filePath) cmd.Dir = repoPath // Ensure command runs in the correct Git repository output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to get commit history for %s in %s: %w", filePath, repoPath, err) } // Split the output into commit hashes commits := strings.Split(strings.TrimSpace(string(output)), "\n") return commits, nil } // GetFilesInCommit retrieves a list of files for a given commit hash. func GetFilesInCommit(repoPath, commitHash string) ([]string, error) { cmd := exec.Command("git", "ls-tree", "-r", "--name-only", commitHash) cmd.Dir = repoPath output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to list files for commit %s: %w", commitHash, err) } files := strings.Split(strings.TrimSpace(string(output)), "\n") return files, nil } // GetFileContent retrieves the content of a file at a specific commit. func GetFileContent(repoPath, commitHash, filePath string) (string, error) { cmd := exec.Command("git", "show", fmt.Sprintf("%s:%s", commitHash, filePath)) cmd.Dir = repoPath var output bytes.Buffer cmd.Stdout = &output err := cmd.Run() if err != nil { return "", fmt.Errorf("failed to get file content for %s at commit %s: %w", filePath, commitHash, err) } return output.String(), nil } func GetDiff(repoPath, commit1, commit2, filePath string) (string, error) { cmd := exec.Command("git", "diff", "--unified=0", commit1, commit2, "--", filePath) cmd.Dir = repoPath var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { return "", err } return out.String(), nil } // GetChangesInCommit returns the files changed in a given commit along with their change type. func GetChangesInCommit(repoPath, commitHash string) ([]File, error) { cmd := exec.Command("git", "show", "--pretty=format:", "--name-status", commitHash) cmd.Dir = repoPath output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to get changes for commit %s: %w", commitHash, err) } lines := strings.Split(strings.TrimSpace(string(output)), "\n") changes := []File{} for _, line := range lines { if line == "" { continue } // Split the line into status and file parts := strings.Fields(line) if len(parts) < 2 { continue } status := parts[0] // A, M, D, etc. file := parts[1] // File path changes = append(changes, File{ Status: status, File: file, }) } return changes, nil } func Read(path string, handle func(interface{})) error { path = filepath.Join(path, ".git") file, err := os.Open(path) if err != nil { return err } fileInfo, err := file.Stat() if err != nil { return err } if fileInfo.IsDir() { commits, err := GetCommits(path) if err != nil { return err } for i, commit := range commits { for _, file := range commit.Files { ic := []IncludedCommits{} fileCommits, err := GetCommitsForFile(path, file.File) if err != nil { return err } for _, commit := range commits { included := false for _, fc := range fileCommits { if fc == commit.Hash { included = true break } } ic = append(ic, IncludedCommits{ Hash: commit.Hash, Included: included, }) } content, err := GetFileContent(path, commit.Hash, file.File) if err != nil { return err } diff := "" if i < len(commits)-1 { diff, err = GetDiff(path, commit.Hash, commits[i+1].Hash, file.File) if err != nil { return err } } handle(Object{ Path: file.File, Commit: commit, File: content, Diff: diff, Commits: ic, }) } } } else { return fmt.Errorf("Please set the src to a directory that contains a .git directory: %s", path) } return nil }