gitlyx / diff.v
231 lines · 210 sloc · 5.21 KB · 3e6e1621f046018306c4254719b20acd01c1a8da
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
7import strings
8
9fn render_diff_table(fd FileDiff) veb.RawHtml {
10 mut out := strings.new_builder(1024)
11 out.write_string('<div class=pr-diff__table>')
12 for hunk in fd.hunks {
13 out.write_string(diff_hunk_header_html(hunk.header))
14 for dline in hunk.lines {
15 out.write_string(diff_line_row_html(fd.path, dline))
16 }
17 }
18 out.write_string('</div>')
19 return veb.RawHtml(out.str())
20}
21
22fn diff_hunk_header_html(header string) string {
23 return '<p class=h><code>${html_escape_text(header)}</code></p>'
24}
25
26fn diff_line_row_html(file_path string, dline DiffLine) string {
27 return diff_line_row_html_with_attrs(file_path, dline, '')
28}
29
30fn diff_line_row_html_with_attrs(file_path string, dline DiffLine, attrs string) string {
31 return '<p class=${dline.compact_class()}${attrs}><u>${dline.old_line_str()}</u><u>${dline.new_line_str()}</u><i>${dline.compact_sign()}</i><s>${highlight.highlight_line(dline.content,
32 file_path)}</s></p>'
33}
34
35struct FileDiff {
36mut:
37 path string
38 old_path string
39 is_new bool
40 is_deleted bool
41 is_renamed bool
42 is_binary bool
43 additions int
44 deletions int
45 hunks []DiffHunk
46}
47
48struct DiffHunk {
49mut:
50 header string
51 old_start int
52 old_count int
53 new_start int
54 new_count int
55 lines []DiffLine
56}
57
58struct DiffLine {
59mut:
60 kind string // 'context', 'add', 'del'
61 old_line int // 0 if not applicable
62 new_line int // 0 if not applicable
63 content string
64}
65
66// parse_unified_diff parses a `git diff` unified diff into FileDiff structs.
67fn parse_unified_diff(raw string) []FileDiff {
68 mut files := []FileDiff{}
69 mut cur := FileDiff{}
70 mut cur_hunk := DiffHunk{}
71 mut in_file := false
72 mut in_hunk := false
73 mut old_l := 0
74 mut new_l := 0
75
76 for line in raw.split_into_lines() {
77 if line.starts_with('diff --git') {
78 if in_file {
79 if in_hunk {
80 cur.hunks << cur_hunk
81 }
82 files << cur
83 }
84 cur = FileDiff{}
85 cur_hunk = DiffHunk{}
86 in_file = true
87 in_hunk = false
88 parts := line.split(' ')
89 if parts.len >= 4 {
90 a_path := strip_diff_prefix(parts[2], 'a/')
91 b_path := strip_diff_prefix(parts[3], 'b/')
92 cur.old_path = a_path
93 cur.path = b_path
94 }
95 } else if line.starts_with('new file') {
96 cur.is_new = true
97 } else if line.starts_with('deleted file') {
98 cur.is_deleted = true
99 } else if line.starts_with('rename from') || line.starts_with('rename to') {
100 cur.is_renamed = true
101 } else if line.starts_with('Binary files') {
102 cur.is_binary = true
103 } else if line.starts_with('--- ') || line.starts_with('+++ ') {
104 // skip header lines
105 } else if line.starts_with('@@') {
106 if in_hunk {
107 cur.hunks << cur_hunk
108 }
109 cur_hunk = DiffHunk{
110 header: line
111 }
112 in_hunk = true
113 parse_hunk_header(line, mut cur_hunk)
114 old_l = cur_hunk.old_start
115 new_l = cur_hunk.new_start
116 } else if in_hunk && line.len > 0 {
117 first := line[0]
118 content := line[1..]
119 if first == ` ` {
120 cur_hunk.lines << DiffLine{
121 kind: 'context'
122 old_line: old_l
123 new_line: new_l
124 content: content
125 }
126 old_l++
127 new_l++
128 } else if first == `+` {
129 cur_hunk.lines << DiffLine{
130 kind: 'add'
131 new_line: new_l
132 content: content
133 }
134 new_l++
135 cur.additions++
136 } else if first == `-` {
137 cur_hunk.lines << DiffLine{
138 kind: 'del'
139 old_line: old_l
140 content: content
141 }
142 old_l++
143 cur.deletions++
144 } else if first == `\\` {
145 // "\ No newline at end of file" — ignore
146 }
147 }
148 }
149 if in_file {
150 if in_hunk {
151 cur.hunks << cur_hunk
152 }
153 files << cur
154 }
155 return files
156}
157
158fn (d &DiffLine) compact_sign() string {
159 return match d.kind {
160 'add' { '+' }
161 'del' { '-' }
162 else { '' }
163 }
164}
165
166fn (d &DiffLine) compact_class() string {
167 return match d.kind {
168 'add' { 'a' }
169 'del' { 'd' }
170 else { 'c' }
171 }
172}
173
174fn (d &DiffLine) compact_side() string {
175 return match d.kind {
176 'add' { 'n' }
177 'del' { 'o' }
178 else { '' }
179 }
180}
181
182fn (d &DiffLine) side() string {
183 return if d.kind == 'add' { 'new' } else { 'old' }
184}
185
186fn (d &DiffLine) effective_line() int {
187 return if d.kind == 'add' { d.new_line } else { d.old_line }
188}
189
190fn (d &DiffLine) comment_field_name(file_path string) string {
191 return 'rc::${file_path}::${d.side()}::${d.effective_line()}'
192}
193
194fn (d &DiffLine) old_line_str() string {
195 return if d.old_line > 0 { d.old_line.str() } else { '' }
196}
197
198fn (d &DiffLine) new_line_str() string {
199 return if d.new_line > 0 { d.new_line.str() } else { '' }
200}
201
202fn strip_diff_prefix(s string, prefix string) string {
203 if s.starts_with(prefix) {
204 return s[prefix.len..]
205 }
206 return s
207}
208
209// parse_hunk_header parses lines like "@@ -1,3 +1,4 @@ optional context"
210fn parse_hunk_header(line string, mut hunk DiffHunk) {
211 parts := line.split(' ')
212 for p in parts {
213 if p.len < 2 {
214 continue
215 }
216 if p[0] == `-` {
217 start, count := parse_range(p[1..])
218 hunk.old_start = start
219 hunk.old_count = count
220 } else if p[0] == `+` {
221 start, count := parse_range(p[1..])
222 hunk.new_start = start
223 hunk.new_count = count
224 }
225 }
226}
227
228fn parse_range(s string) (int, int) {
229 idx := s.index(',') or { return s.int(), 1 }
230 return s[..idx].int(), s[idx + 1..].int()
231}
232