ParseDiffText splits the unified diff text into per-file Diff structs. ref, if non-empty, is a git ref used to read new-file content via git show instead of reading from the working tree. runner, if non-nil, is used to execute git subprocesses through a shared concurrency limiter.
(ctx context.Context, diffText string, repoDir string, ref string, runner *gitcmd.Runner)
| 28 | // runner, if non-nil, is used to execute git subprocesses through a |
| 29 | // shared concurrency limiter. |
| 30 | func ParseDiffText(ctx context.Context, diffText string, repoDir string, ref string, runner *gitcmd.Runner) ([]model.Diff, error) { |
| 31 | lines := strings.Split(diffText, "\n") |
| 32 | var diffs []model.Diff |
| 33 | var current *model.Diff |
| 34 | var buf strings.Builder |
| 35 | // inHunk tracks whether the current line sits inside a "@@" hunk of the |
| 36 | // current file's section. Only hunk content lines carry a leading |
| 37 | // "+"/"-"/" " marker, so insertion/deletion counting and the binary |
| 38 | // marker must look at hunk state: outside a hunk, "+++ b/file" and |
| 39 | // "--- a/file" are headers, not content; inside a hunk, an added line |
| 40 | // like "++i" renders as "+++i" and still counts as an insertion. |
| 41 | inHunk := false |
| 42 | |
| 43 | ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) |
| 44 | defer cancel() |
| 45 | |
| 46 | for _, line := range lines { |
| 47 | if m := diffHeaderRe.FindStringSubmatch(line); m != nil { |
| 48 | // Flush previous diff |
| 49 | if current != nil { |
| 50 | current.Diff = strings.TrimSuffix(buf.String(), "\n") |
| 51 | finalizeDiff(ctx, current, repoDir, ref, runner) |
| 52 | diffs = append(diffs, *current) |
| 53 | buf.Reset() |
| 54 | } |
| 55 | current = &model.Diff{ |
| 56 | OldPath: m[1], |
| 57 | NewPath: m[2], |
| 58 | } |
| 59 | inHunk = false |
| 60 | } |
| 61 | if current == nil { |
| 62 | continue |
| 63 | } |
| 64 | |
| 65 | switch { |
| 66 | case strings.HasPrefix(line, "@@"): |
| 67 | inHunk = true |
| 68 | // The object IDs and mode in Git's extended "index" header are not |
| 69 | // useful review context. Keep index text in hunks, where it is file |
| 70 | // content and therefore carries a diff prefix. |
| 71 | case !inHunk && strings.HasPrefix(line, "index "): |
| 72 | continue |
| 73 | case !inHunk && binaryRe.MatchString(line): |
| 74 | current.IsBinary = true |
| 75 | // Extended header lines (unambiguous: content lines always carry a |
| 76 | // leading "+", "-" or " " prefix, so a bare prefix match is safe). |
| 77 | case strings.HasPrefix(line, "new file mode "): |
| 78 | current.IsNew = true |
| 79 | case strings.HasPrefix(line, "deleted file mode "): |
| 80 | current.IsDeleted = true |
| 81 | case strings.HasPrefix(line, "rename from "): |
| 82 | // Authoritative old path for renames; more reliable than the |
| 83 | // "diff --git" header when paths contain spaces. |
| 84 | current.OldPath = strings.TrimPrefix(line, "rename from ") |
| 85 | current.IsRenamed = true |
| 86 | case strings.HasPrefix(line, "rename to "): |
| 87 | current.NewPath = strings.TrimPrefix(line, "rename to ") |