package gitcmd import ( "regexp" "strconv" "strings" ) type DiffLineType string const ( DiffLineContext DiffLineType = "context" DiffLineAdd DiffLineType = "add" DiffLineDelete DiffLineType = "delete" ) type DiffLine struct { Type DiffLineType `json:"type"` Text string `json:"text"` } type Hunk struct { OldStart int `json:"old_start"` OldCount int `json:"old_count"` NewStart int `json:"new_start"` NewCount int `json:"new_count"` Lines []DiffLine `json:"lines"` } type FileChange struct { Path string `json:"path"` OldPath string `json:"old_path"` Status string `json:"status"` IsBinary bool `json:"is_binary"` Additions int `json:"additions"` Deletions int `json:"deletions"` Hunks []Hunk `json:"hunks"` } type DiffResult struct { Files []FileChange `json:"files"` } var hunkHeaderRe = regexp.MustCompile(`^@+ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @+`) func (r *Repository) Diff(a, b string, paths []string) (*DiffResult, error) { cmd := NewCommand("diff", "--unified=3", "--no-color", "--no-prefix"). AddDynamicArguments(a, b). WithDir(r.Path) if len(paths) > 0 { cmd.AddDashesAndList(paths...) } out, _, err := cmd.RunStdString(r.ctx) if err != nil { return nil, err } return parseDiff(out), nil } func (r *Repository) CommitDiff(sha string) (*DiffResult, error) { out, _, err := NewCommand("show", "--unified=3", "--no-color", "--no-prefix", "--format="). AddDynamicArguments(sha). WithDir(r.Path). RunStdString(r.ctx) if err != nil { return nil, err } return parseDiff(out), nil } func parseDiff(out string) *DiffResult { res := &DiffResult{} if out == "" { return res } var cur *FileChange var curHunk *Hunk finishHunk := func() { if cur != nil && curHunk != nil { cur.Hunks = append(cur.Hunks, *curHunk) curHunk = nil } } finishFile := func() { finishHunk() if cur != nil { if cur.Status == "" { cur.Status = "M" } if cur.Status != "R" { cur.OldPath = "" } res.Files = append(res.Files, *cur) cur = nil } } for _, line := range strings.Split(out, "\n") { switch { case strings.HasPrefix(line, "diff --git "): finishFile() cur = &FileChange{} rest := strings.TrimPrefix(line, "diff --git ") if parts := strings.SplitN(rest, " ", 2); len(parts) == 2 { cur.OldPath, cur.Path = parts[0], parts[1] } else { cur.Path = rest } case cur == nil: continue case strings.HasPrefix(line, "new file mode"): cur.Status = "A" case strings.HasPrefix(line, "deleted file mode"): cur.Status = "D" case strings.HasPrefix(line, "rename from "): cur.OldPath = strings.TrimPrefix(line, "rename from ") cur.Status = "R" case strings.HasPrefix(line, "rename to "): cur.Path = strings.TrimPrefix(line, "rename to ") cur.Status = "R" case strings.HasPrefix(line, "copy from "): cur.OldPath = strings.TrimPrefix(line, "copy from ") cur.Status = "R" case strings.HasPrefix(line, "copy to "): cur.Path = strings.TrimPrefix(line, "copy to ") cur.Status = "R" case strings.HasPrefix(line, "similarity index"): if cur.Status == "" { cur.Status = "R" } case strings.HasPrefix(line, "Binary files "): cur.IsBinary = true case strings.HasPrefix(line, "--- "): p := strings.TrimPrefix(line, "--- ") if p != "/dev/null" { cur.OldPath = p } if p == "/dev/null" && cur.Status == "" { cur.Status = "A" } case strings.HasPrefix(line, "+++ "): p := strings.TrimPrefix(line, "+++ ") if p != "/dev/null" { cur.Path = p } if p == "/dev/null" && cur.Status == "" { cur.Status = "D" } case hunkHeaderRe.MatchString(line): finishHunk() m := hunkHeaderRe.FindStringSubmatch(line) os, _ := strconv.Atoi(m[1]) oc := 1 if m[2] != "" { oc, _ = strconv.Atoi(m[2]) } ns, _ := strconv.Atoi(m[3]) nc := 1 if m[4] != "" { nc, _ = strconv.Atoi(m[4]) } curHunk = &Hunk{OldStart: os, OldCount: oc, NewStart: ns, NewCount: nc} case strings.HasPrefix(line, "\\ No newline"): default: if curHunk == nil { continue } if line == "" { continue } switch line[0] { case ' ': curHunk.Lines = append(curHunk.Lines, DiffLine{Type: DiffLineContext, Text: line[1:]}) case '+': curHunk.Lines = append(curHunk.Lines, DiffLine{Type: DiffLineAdd, Text: line[1:]}) cur.Additions++ case '-': curHunk.Lines = append(curHunk.Lines, DiffLine{Type: DiffLineDelete, Text: line[1:]}) cur.Deletions++ default: curHunk.Lines = append(curHunk.Lines, DiffLine{Type: DiffLineContext, Text: line}) } } } finishFile() return res }