package gitcmd import ( "bytes" "strconv" "strings" "time" ) type Commit struct { ID string `json:"id"` ShortSHA string `json:"short_sha"` Message string `json:"message"` Author string `json:"author"` AuthorEmail string `json:"author_email"` When time.Time `json:"when"` Parents []string `json:"parents"` } type CommitDetail struct { Commit Stats DiffStats `json:"stats"` Files []FileDiff `json:"files"` } type DiffStats struct { Additions int `json:"additions"` Deletions int `json:"deletions"` Total int `json:"total"` Files int `json:"files"` } type FileDiff struct { Path string `json:"path"` Additions int `json:"additions"` Deletions int `json:"deletions"` Status string `json:"status"` } func (r *Repository) GetCommit(ref string) (*Commit, error) { out, _, err := NewCommand("show", "-s"). Add("--format=%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%aI%x1f%P"). AddDynamicArguments(ref). WithDir(r.Path). RunStdString(r.ctx) if err != nil { return nil, err } return parseCommitLine(strings.TrimSpace(out)), nil } func (r *Repository) ListCommits(ref string, limit int) ([]Commit, error) { if empty, err := r.IsEmpty(); err != nil { return nil, err } else if empty { return []Commit{}, nil } cmd := NewCommand("log"). Add("--format=%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%aI%x1f%P"). WithDir(r.Path) if limit > 0 { cmd.Add("-n", strconv.Itoa(limit)) } cmd.AddDynamicArguments(ref) out, _, err := cmd.RunStdString(r.ctx) if err != nil { return nil, err } var commits []Commit for _, line := range strings.Split(strings.TrimSpace(out), "\n") { if line == "" { continue } commits = append(commits, *parseCommitLine(line)) } return commits, nil } func (r *Repository) GetCommitDetail(sha string) (*CommitDetail, error) { commit, err := r.GetCommit(sha) if err != nil { return nil, err } out, _, err := NewCommand("show", "--numstat", "--format="). AddDynamicArguments(sha). WithDir(r.Path). RunStdString(r.ctx) if err != nil { return nil, err } var files []FileDiff var totalAdd, totalDel int for _, line := range strings.Split(strings.TrimSpace(out), "\n") { if line == "" { continue } parts := strings.SplitN(line, "\t", 3) if len(parts) < 3 { continue } add, _ := strconv.Atoi(parts[0]) del, _ := strconv.Atoi(parts[1]) path := parts[2] status := "M" if add == 0 && del > 0 { status = "D" } else if del == 0 && add > 0 { status = "A" } files = append(files, FileDiff{ Path: path, Additions: add, Deletions: del, Status: status, }) totalAdd += add totalDel += del } return &CommitDetail{ Commit: *commit, Stats: DiffStats{ Additions: totalAdd, Deletions: totalDel, Total: totalAdd + totalDel, Files: len(files), }, Files: files, }, nil } func parseCommitLine(line string) *Commit { fields := bytes.SplitN([]byte(line), []byte{0x1f}, 7) if len(fields) < 6 { return &Commit{Message: line} } when, _ := time.Parse(time.RFC3339, string(fields[5])) parents := []string{} if len(fields) >= 7 && len(fields[6]) > 0 { parents = strings.Fields(string(fields[6])) } return &Commit{ ID: string(fields[0]), ShortSHA: string(fields[1]), Message: string(fields[2]), Author: string(fields[3]), AuthorEmail: string(fields[4]), When: when, Parents: parents, } }