ggdgsdbsdbbb / diff.v
194 lines · 178 sloc · 4.32 KB · cbcf1476fe0844712412e7fa72a04a7c5e03b64c
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import veb
6import highlight
7
8// render_diff_line is a template helper that returns the diff line's
9// content with single-line syntax highlighting applied.
10fn render_diff_line(content string, file_path string) veb.RawHtml {
11 return veb.RawHtml(highlight.highlight_line(content, file_path))
12}
13
14struct FileDiff {
15mut:
16 path string
17 old_path string
18 is_new bool
19 is_deleted bool
20 is_renamed bool
21 is_binary bool
22 additions int
23 deletions int
24 hunks []DiffHunk
25}
26
27struct DiffHunk {
28mut:
29 header string
30 old_start int
31 old_count int
32 new_start int
33 new_count int
34 lines []DiffLine
35}
36
37struct DiffLine {
38mut:
39 kind string // 'context', 'add', 'del'
40 old_line int // 0 if not applicable
41 new_line int // 0 if not applicable
42 content string
43}
44
45// parse_unified_diff parses a `git diff` unified diff into FileDiff structs.
46fn parse_unified_diff(raw string) []FileDiff {
47 mut files := []FileDiff{}
48 mut cur := FileDiff{}
49 mut cur_hunk := DiffHunk{}
50 mut in_file := false
51 mut in_hunk := false
52 mut old_l := 0
53 mut new_l := 0
54
55 for line in raw.split_into_lines() {
56 if line.starts_with('diff --git') {
57 if in_file {
58 if in_hunk {
59 cur.hunks << cur_hunk
60 }
61 files << cur
62 }
63 cur = FileDiff{}
64 cur_hunk = DiffHunk{}
65 in_file = true
66 in_hunk = false
67 parts := line.split(' ')
68 if parts.len >= 4 {
69 a_path := strip_diff_prefix(parts[2], 'a/')
70 b_path := strip_diff_prefix(parts[3], 'b/')
71 cur.old_path = a_path
72 cur.path = b_path
73 }
74 } else if line.starts_with('new file') {
75 cur.is_new = true
76 } else if line.starts_with('deleted file') {
77 cur.is_deleted = true
78 } else if line.starts_with('rename from') || line.starts_with('rename to') {
79 cur.is_renamed = true
80 } else if line.starts_with('Binary files') {
81 cur.is_binary = true
82 } else if line.starts_with('--- ') || line.starts_with('+++ ') {
83 // skip header lines
84 } else if line.starts_with('@@') {
85 if in_hunk {
86 cur.hunks << cur_hunk
87 }
88 cur_hunk = DiffHunk{
89 header: line
90 }
91 in_hunk = true
92 parse_hunk_header(line, mut cur_hunk)
93 old_l = cur_hunk.old_start
94 new_l = cur_hunk.new_start
95 } else if in_hunk && line.len > 0 {
96 first := line[0]
97 content := line[1..]
98 if first == ` ` {
99 cur_hunk.lines << DiffLine{
100 kind: 'context'
101 old_line: old_l
102 new_line: new_l
103 content: content
104 }
105 old_l++
106 new_l++
107 } else if first == `+` {
108 cur_hunk.lines << DiffLine{
109 kind: 'add'
110 new_line: new_l
111 content: content
112 }
113 new_l++
114 cur.additions++
115 } else if first == `-` {
116 cur_hunk.lines << DiffLine{
117 kind: 'del'
118 old_line: old_l
119 content: content
120 }
121 old_l++
122 cur.deletions++
123 } else if first == `\\` {
124 // "\ No newline at end of file" — ignore
125 }
126 }
127 }
128 if in_file {
129 if in_hunk {
130 cur.hunks << cur_hunk
131 }
132 files << cur
133 }
134 return files
135}
136
137fn (d &DiffLine) sign() string {
138 return match d.kind {
139 'add' { '+' }
140 'del' { '-' }
141 else { ' ' }
142 }
143}
144
145fn (d &DiffLine) side() string {
146 return if d.kind == 'add' { 'new' } else { 'old' }
147}
148
149fn (d &DiffLine) effective_line() int {
150 return if d.kind == 'add' { d.new_line } else { d.old_line }
151}
152
153fn (d &DiffLine) comment_field_name(file_path string) string {
154 return 'rc::${file_path}::${d.side()}::${d.effective_line()}'
155}
156
157fn (d &DiffLine) old_line_str() string {
158 return if d.old_line > 0 { d.old_line.str() } else { '' }
159}
160
161fn (d &DiffLine) new_line_str() string {
162 return if d.new_line > 0 { d.new_line.str() } else { '' }
163}
164
165fn strip_diff_prefix(s string, prefix string) string {
166 if s.starts_with(prefix) {
167 return s[prefix.len..]
168 }
169 return s
170}
171
172// parse_hunk_header parses lines like "@@ -1,3 +1,4 @@ optional context"
173fn parse_hunk_header(line string, mut hunk DiffHunk) {
174 parts := line.split(' ')
175 for p in parts {
176 if p.len < 2 {
177 continue
178 }
179 if p[0] == `-` {
180 start, count := parse_range(p[1..])
181 hunk.old_start = start
182 hunk.old_count = count
183 } else if p[0] == `+` {
184 start, count := parse_range(p[1..])
185 hunk.new_start = start
186 hunk.new_count = count
187 }
188 }
189}
190
191fn parse_range(s string) (int, int) {
192 idx := s.index(',') or { return s.int(), 1 }
193 return s[..idx].int(), s[idx + 1..].int()
194}
195