home » zplus/clif.git
Author zPlus <zplus@peers.community> 2022-08-13 11:59:56
Committer zPlus <zplus@peers.community> 2022-08-13 11:59:56
Commit 3404a95 (patch)
Tree 47b6629
Parent(s)

Show diff between arbitrary commits. Allow a commit to be diffed with any arbitrary commit (for example great-grandparent) instead of just its first parent. This commit also adds the options for downloading .diff and .patch files for a commit.


commits diff: b517ecb..3404a95
3 files changed, 333 insertions, 245 deletionsdownload


Diffstat
-rw-r--r-- static/css/clif.css 46
-rw-r--r-- templates/repository/commit.html 477
-rw-r--r-- web.py 55

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+34/-12 M   static/css/clif.css
index 5cbebc0..c31ea7a
old size: 10K - new size: 10K
@@ -44,6 +44,28 @@ pre {
44 44 line-height: 1.5rem;
45 45 }
46 46
47 + .separator {
48 + align-items: center;
49 + display: flex;
50 + margin: 1rem 0;
51 + text-align: center;
52 + }
53 +
54 + .separator::before,
55 + .separator::after {
56 + border-bottom: 1px solid #aaa;
57 + content: '';
58 + flex: 1;
59 + }
60 +
61 + .separator:not(:empty)::before {
62 + margin-right: 1rem;
63 + }
64 +
65 + .separator:not(:empty)::after {
66 + margin-left: 1rem;
67 + }
68 +
47 69 .top_bar {
48 70 display: flex;
49 71 width: 100%;
@@ -329,6 +351,18 @@ div.commit {
329 351 white-space: pre-wrap;
330 352 }
331 353
354 + div.commit .accumulated {
355 + margin-top: .5rem;
356 + }
357 +
358 + div.commit .accumulated .insertions {
359 + color: darkgreen;
360 + }
361 +
362 + div.commit .accumulated .deletions {
363 + color: darkred;
364 + }
365 +
332 366 div.commit details.diffstat {
333 367 }
334 368
@@ -372,18 +406,6 @@ div.commit {
372 406 background: red;
373 407 }
374 408
375 - div.commit details.diffstat .accumulated {
376 - margin-top: .5rem;
377 - }
378 -
379 - div.commit details.diffstat .accumulated .insertions {
380 - color: darkgreen;
381 - }
382 -
383 - div.commit details.diffstat .accumulated .deletions {
384 - color: darkred;
385 - }
386 -
387 409 div.commit details.diff_options {
388 410 margin-bottom: 2rem;
389 411 }

+252/-225 M   templates/repository/commit.html
index 56667ce..a917fc1
old size: 19K - new size: 22K
@@ -30,7 +30,8 @@
30 30 <b>Commit</b>
31 31 </td>
32 32 <td>
33 - <a href="{{ url('commit', repository=repository[:-4], commit_id=commit.id) }}">{{ commit.id }}</a>
33 + <a href="{{ url('commit', repository=repository[:-4], commit_id=commit.id) }}">{{ commit.short_id }}</a>
34 + (<a href="{{ url('commit_patch', repository=repository[:-4], commit_id=commit.id, **request.query) }}">patch</a>)
34 35 </td>
35 36 </tr>
36 37 <tr>
@@ -38,7 +39,7 @@
38 39 <b>Tree</b>
39 40 </td>
40 41 <td>
41 - <a href="{{ url('tree', repository=repository[:-4], revision=commit.tree.id) }}">{{ commit.tree.id }}</a>
42 + <a href="{{ url('tree', repository=repository[:-4], revision=commit.tree.id) }}">{{ commit.tree.short_id }}</a>
42 43 </td>
43 44 </tr>
44 45 <tr>
@@ -47,7 +48,10 @@
47 48 </td>
48 49 <td>
49 50 {% for parent in commit.parents %}
50 - <a href="{{ url('commit', repository=repository[:-4], commit_id=parent.id) }}">{{ parent.short_id }}</a>
51 + <div>
52 + <a href="{{ url('commit', repository=repository[:-4], commit_id=parent.id) }}">{{ parent.short_id }}</a>
53 + (<a href="{{ url('commit2', repository=repository[:-4], commit_id=commit.id, commit_id2=parent.id) }}">diff</a>)
54 + </div>
51 55 {% endfor %}
52 56 </td>
53 57 </tr>
@@ -58,14 +62,33 @@
58 62
59 63 <div class="message">{{ commit.message }}</div>
60 64
61 - <br /><br /><br /><br />
65 + <br /><br />
62 66
63 - <details class="diffstat" {{ 'open' if defaults.DIFF_EXPAND_DIFFSTAT }}>
64 - {# pygit2 appears to recompute all the stats every time we use diff.stats,
65 - # therefore we set this variable in order to compute it only once.
66 - #}
67 - {% set diff_stats = diff.stats %}
67 + <div class="separator">
68 + commits diff:
69 + <b>{{ commit2.short_id if commit2 else '0000000' }}</b>..<b>{{ commit.short_id }}</b>
70 + </div>
71 +
72 + {# pygit2 appears to recompute all the stats every time we use diff.stats,
73 + # therefore we set this variable in order to compute it only once.
74 + #}
75 + {% set diff_stats = diff.stats %}
76 +
77 + <div class="accumulated">
78 + <b>{{ diff_stats.files_changed }}</b> file{{ 's' if diff_stats.files_changed != 1 }} changed,
79 + <span class="insertions"><b>{{ diff_stats.insertions }}</b> insertion{{ 's' if diff_stats.insertions != 1 }}</span>,
80 + <span class="deletions"><b>{{ diff_stats.deletions }}</b> deletion{{ 's' if diff_stats.deletions != 1 }}</span>
81 +
82 + {% if commit2 %}
83 + <a href="{{ url('commit_diff2', repository=repository[:-4], commit_id=commit.id, commit_id2=commit2.id, **request.query) }}">download</a>
84 + {% else %}
85 + <a href="{{ url('commit_diff', repository=repository[:-4], commit_id=commit.id, **request.query) }}">download</a>
86 + {% endif %}
87 + </div>
88 +
89 + <br /><br />
68 90
91 + <details class="diffstat" {{ 'open' if defaults.DIFF_EXPAND_DIFFSTAT }}>
69 92 <summary>Diffstat</summary>
70 93 <table>
71 94 {% for patch in diff %}
@@ -85,12 +108,6 @@
85 108 </tr>
86 109 {% endfor %}
87 110 </table>
88 -
89 - <div class="accumulated">
90 - <b>{{ diff_stats.files_changed }}</b> file{{ 's' if diff_stats.files_changed != 1 }} changed,
91 - <span class="insertions"><b>{{ diff_stats.insertions }}</b> insertion{{ 's' if diff_stats.insertions != 1 }}</span>,
92 - <span class="deletions"><b>{{ diff_stats.deletions }}</b> deletion{{ 's' if diff_stats.deletions != 1 }}</span>
93 - </div>
94 111 </details>
95 112
96 113 <br />
@@ -111,38 +128,38 @@
111 128 </tr>
112 129 <tr>
113 130 <td>
114 - Context lines
131 + Side
115 132 </td>
116 133 <td>
117 - <input type="number" min=0 max=1000 name="context_lines" value="{{ context_lines }}" />
134 + <label><input type="radio" name="side" value="normal" {{ 'checked' if side == 'normal' }}>Normal</label>
135 + <label><input type="radio" name="side" value="reverse" {{ 'checked' if side == 'reverse' }}>Reverse</label>
118 136 </td>
119 137 </tr>
120 138 <tr>
121 139 <td>
122 - Inter-hunk lines
140 + Whitespace
123 141 </td>
124 142 <td>
125 - <input type="number" min=0 max=1000 name="inter_hunk_lines" value="{{ inter_hunk_lines }}" />
143 + <label><input type="radio" name="whitespace" value="include" {{ 'checked' if whitespace == 'include' }}>Include</label>
144 + <label><input type="radio" name="whitespace" value="ignore_all" {{ 'checked' if whitespace == 'ignore_all' }}>Ignore all</label>
145 + <label><input type="radio" name="whitespace" value="ignore_change" {{ 'checked' if whitespace == 'ignore_change' }}>Ignore amount changes</label>
146 + <label><input type="radio" name="whitespace" value="ignore_eol" {{ 'checked' if whitespace == 'ignore_eol' }}>Ignore at end of line</label>
126 147 </td>
127 148 </tr>
128 149 <tr>
129 150 <td>
130 - Side
151 + Context lines
131 152 </td>
132 153 <td>
133 - <label><input type="radio" name="side" value="normal" {{ 'checked' if side == 'normal' }}>Normal</label>
134 - <label><input type="radio" name="side" value="reverse" {{ 'checked' if side == 'reverse' }}>Reverse</label>
154 + <input type="number" min=0 max=1000 name="context_lines" value="{{ context_lines }}" />
135 155 </td>
136 156 </tr>
137 157 <tr>
138 158 <td>
139 - Whitespace
159 + Inter-hunk lines
140 160 </td>
141 161 <td>
142 - <label><input type="radio" name="whitespace" value="include" {{ 'checked' if whitespace == 'include' }}>Include</label>
143 - <label><input type="radio" name="whitespace" value="ignore_all" {{ 'checked' if whitespace == 'ignore_all' }}>Ignore all</label>
144 - <label><input type="radio" name="whitespace" value="ignore_change" {{ 'checked' if whitespace == 'ignore_change' }}>Ignore amount changes</label>
145 - <label><input type="radio" name="whitespace" value="ignore_eol" {{ 'checked' if whitespace == 'ignore_eol' }}>Ignore at end of line</label>
162 + <input type="number" min=0 max=1000 name="inter_hunk_lines" value="{{ inter_hunk_lines }}" />
146 163 </td>
147 164 </tr>
148 165 </table>
@@ -151,225 +168,235 @@
151 168 </details>
152 169
153 170 {% if mode == 'udiff_raw' %}
154 -
171 +
155 172 <div class="raw_diff">{{ diff.patch|highlight_udiff|safe }}</div>
156 173
157 174 {% else %}
158 -
159 - {% for patch in diff %}
160 -
161 - {# The following status values are defined in the git_delta_t enum
162 - # in libgit2. See https://github.com/libgit2/libgit2/blob/main/include/git2/diff.h
163 - # Looks like pygit2 also has a pygit2.DiffDelta.status_char() functions that
164 - # returns the single-char abbreviation of the delta status, so we can use this
165 - # instead of the raw integer.
166 - # 0 = UNCHANGED
167 - # 1 = ADDED (does not exist in old version)
168 - # 2 = DELETED (does not exist in new version)
169 - # 3 = MODIFIED (content changed between old and new versions)
170 - # 4 = RENAMED
171 - # 5 = COPIED
172 - # ... (there are other codes that we don't use)
173 - #}
174 175
175 - {% set delta_char = patch.delta.status_char() %}
176 -
177 - <details class="diff_view" id="{{ (patch.delta.new_file.id|string)[:7] }}" {{ 'open' if defaults.DIFF_EXPAND }}>
178 -
179 - <summary>
180 - {% if patch.line_stats[1] + patch.line_stats[2] > 0 %}
181 - {% set color_border = patch.line_stats[1] / ( patch.line_stats[1] + patch.line_stats[2] ) * 100 %}
182 - {% else %}
183 - {% set color_border = 0 %}
184 - {% endif %}
185 -
186 - <span class="histogram" style="border-image: linear-gradient(to right, lightgreen {{ color_border }}%, red {{ color_border }}%) 1;">
187 - +{{ patch.line_stats[1] }}/-{{ patch.line_stats[2] }}
188 - </span>
189 -
190 - {% if delta_char == 'A' %}
191 - <b title="Added">A</b> &nbsp; {{ patch.delta.new_file.path }}
192 - {% endif %}
193 -
194 - {% if delta_char == 'D' %}
195 - <b title="Deleted">D</b> &nbsp; {{ patch.delta.old_file.path }}
196 - {% endif %}
197 -
198 - {% if delta_char == 'M' %}
199 - <b title="Modified">M</b> &nbsp; {{ patch.delta.new_file.path }}
200 - {% endif %}
201 -
202 - {% if delta_char == 'R' %}
203 - <b title="Renamed">R</b> &nbsp; {{ patch.delta.old_file.path }} -> {{ patch.delta.new_file.path }}
204 - {% endif %}
205 -
206 - {% if delta_char == 'C' %}
207 - <b title="Copied">C</b> &nbsp; {{ patch.delta.old_file.path }} -> {{ patch.delta.new_file.path }}
208 - {% endif %}
209 -
210 - <table>
211 - <tr>
212 - <td>
213 - index <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.parents[0].id, tree_path=patch.delta.old_file.path) }}">{{ (patch.delta.old_file.id|string)[:7] }}</a>..<a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}">{{ (patch.delta.new_file.id|string)[:7] }}</a>
214 - </td>
215 - </tr>
216 - <tr>
217 - <td colspan="4">
218 - old size: {{ patch.delta.old_file.size|human_size(B=true) }}
219 - -
220 - new size: {{ patch.delta.new_file.size|human_size(B=true) }}
221 - </td>
222 - </tr>
223 - <tr>
224 - <td colspan="4">
225 - {% if delta_char == 'A' %}
226 - new file mode: {{ patch.delta.new_file.mode|filemode }}
227 - {% elif delta_char == 'D' %}
228 - deleted file mode: {{ patch.delta.old_file.mode|filemode }}
229 - {% elif patch.delta.old_file.mode != patch.delta.new_file.mode %}
230 - old mode: {{ patch.delta.old_file.mode|filemode }}
231 - <br />
232 - new mode: {{ patch.delta.new_file.mode|filemode }}
233 - {% endif %}
234 - </td>
235 - </tr>
236 - </table>
237 - </summary>
238 -
239 - <table class="diff">
240 - {% if patch.delta.is_binary %}
241 - <tr>
242 - <td colspan="4">
243 - <i>Binary file</i>
244 - </td>
245 - </tr>
246 - {% endif %}
247 -
248 - {% for hunk in patch.hunks if not patch.delta.is_binary %}
176 + {% for patch in diff %}
177 +
178 + {# The following status values are defined in the git_delta_t enum
179 + # in libgit2. See https://github.com/libgit2/libgit2/blob/main/include/git2/diff.h
180 + # Looks like pygit2 also has a pygit2.DiffDelta.status_char() functions that
181 + # returns the single-char abbreviation of the delta status, so we can use this
182 + # instead of the raw integer.
183 + # 0 = UNCHANGED
184 + # 1 = ADDED (does not exist in old version)
185 + # 2 = DELETED (does not exist in new version)
186 + # 3 = MODIFIED (content changed between old and new versions)
187 + # 4 = RENAMED
188 + # 5 = COPIED
189 + # ... (there are other codes that we don't use)
190 + #}
191 +
192 + {% set delta_char = patch.delta.status_char() %}
193 +
194 + <details class="diff_view" id="{{ (patch.delta.new_file.id|string)[:7] }}" {{ 'open' if defaults.DIFF_EXPAND }}>
195 +
196 + <summary>
197 + {% if patch.line_stats[1] + patch.line_stats[2] > 0 %}
198 + {% set color_border = patch.line_stats[1] / ( patch.line_stats[1] + patch.line_stats[2] ) * 100 %}
199 + {% else %}
200 + {% set color_border = 0 %}
201 + {% endif %}
249 202
250 - {#### UDIFF mode ####}
251 - {# In this mode the lines are printed one after the other. #}
203 + <span class="histogram" style="border-image: linear-gradient(to right, lightgreen {{ color_border }}%, red {{ color_border }}%) 1;">
204 + +{{ patch.line_stats[1] }}/-{{ patch.line_stats[2] }}
205 + </span>
252 206
253 - {% if mode == 'udiff' %}
254 - <tr class="header">
255 - <td></td>
256 - <td></td>
257 - <td></td>
258 - <td>{{ hunk.header }}</td>
259 - </tr>
260 -
261 - {% for line in hunk.lines %}
262 - <tr class="udiff {{ 'insertion' if line.old_lineno < 0 }} {{ 'deletion' if line.new_lineno < 0 }}">
263 - <td class="linenos">
264 - {% if line.old_lineno >= 0 %}
265 - <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.parents[0].id, tree_path=patch.delta.old_file.path) }}#line-{{ line.old_lineno }}">{{ line.old_lineno }}</a>
266 - {% endif %}
267 - </td>
268 - <td class="linenos">
269 - {% if line.new_lineno >= 0 %}
270 - <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}#line-{{ line.new_lineno }}">{{ line.new_lineno }}</a>
271 - {% endif %}
272 - </td>
273 - <td class="origin">{{ line.origin }}</td>
274 - <td class="content">{{ line.content }}</td>
275 - </tr>
276 - {% endfor %}
207 + {% if delta_char == 'A' %}
208 + <b title="Added">A</b> &nbsp; {{ patch.delta.new_file.path }}
277 209 {% endif %}
278 210
279 - {#### SSDIFF mode ####}
280 - {# In this mode, changed lines are buffered until we find an
281 - # unchanged line. When an unchanged line has been found, the
282 - # buffer is emptied.
283 - #}
211 + {% if delta_char == 'D' %}
212 + <b title="Deleted">D</b> &nbsp; {{ patch.delta.old_file.path }}
213 + {% endif %}
284 214
285 - {% if mode == 'ssdiff' %}
286 - {% set buffer_deletions = [] %}
287 - {% set buffer_insertions = [] %}
288 -
289 - {% macro print_buffer(buffer_deletions, buffer_insertions) %}
290 - {% for buffer_del, buffer_ins in zip_longest(buffer_deletions, buffer_insertions) %}
291 - <tr class="ssdiff">
292 - {% if buffer_del %}
293 - <td class="linenos">
294 - <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.parents[0].id, tree_path=patch.delta.old_file.path) }}#line-{{ buffer_del.old_lineno }}">{{ buffer_del.old_lineno }}</a>
295 - </td>
296 - <td class="content {{ 'deletion' if buffer_insertions|length == 0 else 'change' }}">{{ buffer_del.content }}</td>
297 - {% else %}
298 - <td class="linenos">
299 - </td>
300 - <td class="content"></td>
301 - {% endif %}
302 -
303 - {% if buffer_ins %}
304 - <td class="linenos">
305 - <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}#line-{{ buffer_ins.new_lineno }}">{{ buffer_ins.new_lineno }}</a>
306 - </td>
307 - <td class="content {{ 'insertion' if buffer_deletions|length == 0 else 'change' }}">{{ buffer_ins.content }}</td>
308 - {% else %}
309 - <td class="linenos">
310 - </td>
311 - <td class="content"></td>
312 - {% endif %}
313 - </tr>
314 - {% endfor %}
315 -
316 - {# .clear() empties the buffer. Since this requires {{}}
317 - # brackets instead of {%%}, this line will print the value
318 - # of "buffer_deletions" which is "None".
319 - # "or ''" is a hack for printing an empty line instead of "None".
320 - #}
321 - {{ buffer_deletions.clear() or '' }}
322 - {{ buffer_insertions.clear() or '' }}
323 - {% endmacro %}
324 -
325 - <tr class="header">
326 - <td colspan=4>{{ hunk.header }}</td>
215 + {% if delta_char == 'M' %}
216 + <b title="Modified">M</b> &nbsp; {{ patch.delta.new_file.path }}
217 + {% endif %}
218 +
219 + {% if delta_char == 'R' %}
220 + <b title="Renamed">R</b> &nbsp; {{ patch.delta.old_file.path }} -> {{ patch.delta.new_file.path }}
221 + {% endif %}
222 +
223 + {% if delta_char == 'C' %}
224 + <b title="Copied">C</b> &nbsp; {{ patch.delta.old_file.path }} -> {{ patch.delta.new_file.path }}
225 + {% endif %}
226 +
227 + <table>
228 + <tr>
229 + <td>
230 + index
231 + {% if delta_char == 'A' %}
232 + {{ (patch.delta.old_file.id|string)[:7] }}
233 + {%- else -%}
234 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit2.id, tree_path=patch.delta.old_file.path) }}">{{ (patch.delta.old_file.id|string)[:7] }}</a>
235 + {%- endif -%}
236 + ..
237 + {%- if delta_char == 'D' -%}
238 + {{ (patch.delta.new_file.id|string)[:7] }}
239 + {%- else -%}
240 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}">{{ (patch.delta.new_file.id|string)[:7] }}</a>
241 + {% endif %}
242 + </td>
243 + </tr>
244 + <tr>
245 + <td colspan="4">
246 + old size: {{ patch.delta.old_file.size|human_size(B=true) }}
247 + -
248 + new size: {{ patch.delta.new_file.size|human_size(B=true) }}
249 + </td>
250 + </tr>
251 + <tr>
252 + <td colspan="4">
253 + {% if delta_char == 'A' %}
254 + new file mode: {{ patch.delta.new_file.mode|filemode }}
255 + {% elif delta_char == 'D' %}
256 + deleted file mode: {{ patch.delta.old_file.mode|filemode }}
257 + {% elif patch.delta.old_file.mode != patch.delta.new_file.mode %}
258 + old mode: {{ patch.delta.old_file.mode|filemode }}
259 + <br />
260 + new mode: {{ patch.delta.new_file.mode|filemode }}
261 + {% endif %}
262 + </td>
327 263 </tr>
264 + </table>
265 + </summary>
266 +
267 + <table class="diff">
268 + {% if patch.delta.is_binary %}
269 + <tr>
270 + <td colspan="4">
271 + <i>Binary file</i>
272 + </td>
273 + </tr>
274 + {% endif %}
275 +
276 + {% for hunk in patch.hunks if not patch.delta.is_binary %}
328 277
329 - {% for line in hunk.lines %}
330 - {% if line.old_lineno < 0 %}
331 - {{ buffer_insertions.append(line) or '' }}
332 - {% endif %}
333 -
334 - {% if line.new_lineno < 0 %}
335 - {{ buffer_deletions.append(line) or '' }}
336 - {% endif %}
337 -
338 - {% if line.old_lineno >= 0 and line.new_lineno >= 0 %}
278 + {#### UDIFF mode ####}
279 + {# In this mode the lines are printed one after the other. #}
280 +
281 + {% if mode == 'udiff' %}
282 + <tr class="header">
283 + <td></td>
284 + <td></td>
285 + <td></td>
286 + <td>{{ hunk.header }}</td>
287 + </tr>
339 288
340 - {# Unload buffer #}
341 - {{ print_buffer(buffer_deletions, buffer_insertions) }}
342 -
343 - {# Insert the unchanged line. #}
344 - <tr class="ssdiff">
289 + {% for line in hunk.lines %}
290 + <tr class="udiff {{ 'insertion' if line.old_lineno < 0 }} {{ 'deletion' if line.new_lineno < 0 }}">
345 291 <td class="linenos">
346 - <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.parents[0].id, tree_path=patch.delta.old_file.path) }}#line-{{ line.old_lineno }}">{{ line.old_lineno }}</a>
292 + {% if line.old_lineno >= 0 %}
293 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit2.id, tree_path=patch.delta.old_file.path) }}#line-{{ line.old_lineno }}">{{ line.old_lineno }}</a>
294 + {% endif %}
347 295 </td>
348 - <td class="content">{{ line.content }}</td>
349 -
350 296 <td class="linenos">
351 - <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}#line-{{ line.new_lineno }}">{{ line.new_lineno }}</a>
297 + {% if line.new_lineno >= 0 %}
298 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}#line-{{ line.new_lineno }}">{{ line.new_lineno }}</a>
299 + {% endif %}
352 300 </td>
301 + <td class="origin">{{ line.origin }}</td>
353 302 <td class="content">{{ line.content }}</td>
354 303 </tr>
355 - {% endif %}
356 - {% endfor %}
304 + {% endfor %}
305 + {% endif %}
357 306
358 - {# Empty remaining buffer. There is a non-empty buffer if we did
359 - # not find an unchanged line, for example when context_lines=0.
307 + {#### SSDIFF mode ####}
308 + {# In this mode, changed lines are buffered until we find an
309 + # unchanged line. When an unchanged line has been found, the
310 + # buffer is emptied.
360 311 #}
361 - {{ print_buffer(buffer_deletions, buffer_insertions) }}
362 - {% endif %}
363 -
364 - {% endfor %}
365 -
366 - </table>
367 - </details>
312 +
313 + {% if mode == 'ssdiff' %}
314 + {% set buffer_deletions = [] %}
315 + {% set buffer_insertions = [] %}
316 +
317 + {% macro print_buffer(buffer_deletions, buffer_insertions) %}
318 + {% for buffer_del, buffer_ins in zip_longest(buffer_deletions, buffer_insertions) %}
319 + <tr class="ssdiff">
320 + {% if buffer_del %}
321 + <td class="linenos">
322 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit2.id, tree_path=patch.delta.old_file.path) }}#line-{{ buffer_del.old_lineno }}">{{ buffer_del.old_lineno }}</a>
323 + </td>
324 + <td class="content {{ 'deletion' if buffer_insertions|length == 0 else 'change' }}">{{ buffer_del.content }}</td>
325 + {% else %}
326 + <td class="linenos">
327 + </td>
328 + <td class="content"></td>
329 + {% endif %}
330 +
331 + {% if buffer_ins %}
332 + <td class="linenos">
333 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}#line-{{ buffer_ins.new_lineno }}">{{ buffer_ins.new_lineno }}</a>
334 + </td>
335 + <td class="content {{ 'insertion' if buffer_deletions|length == 0 else 'change' }}">{{ buffer_ins.content }}</td>
336 + {% else %}
337 + <td class="linenos">
338 + </td>
339 + <td class="content"></td>
340 + {% endif %}
341 + </tr>
342 + {% endfor %}
343 +
344 + {# .clear() empties the buffer. Since this requires {{}}
345 + # brackets instead of {%%}, this line will print the value
346 + # of "buffer_deletions" which is "None".
347 + # "or ''" is a hack for printing an empty line instead of "None".
348 + #}
349 + {{ buffer_deletions.clear() or '' }}
350 + {{ buffer_insertions.clear() or '' }}
351 + {% endmacro %}
352 +
353 + <tr class="header">
354 + <td colspan=4>{{ hunk.header }}</td>
355 + </tr>
356 +
357 + {% for line in hunk.lines %}
358 + {% if line.old_lineno < 0 %}
359 + {{ buffer_insertions.append(line) or '' }}
360 + {% endif %}
361 +
362 + {% if line.new_lineno < 0 %}
363 + {{ buffer_deletions.append(line) or '' }}
364 + {% endif %}
365 +
366 + {% if line.old_lineno >= 0 and line.new_lineno >= 0 %}
367 +
368 + {# Unload buffer #}
369 + {{ print_buffer(buffer_deletions, buffer_insertions) }}
370 +
371 + {# Insert the unchanged line. #}
372 + <tr class="ssdiff">
373 + <td class="linenos">
374 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit2.id, tree_path=patch.delta.old_file.path) }}#line-{{ line.old_lineno }}">{{ line.old_lineno }}</a>
375 + </td>
376 + <td class="content">{{ line.content }}</td>
377 +
378 + <td class="linenos">
379 + <a href="{{ url('tree_path', repository=repository[:-4], revision=commit.id, tree_path=patch.delta.new_file.path) }}#line-{{ line.new_lineno }}">{{ line.new_lineno }}</a>
380 + </td>
381 + <td class="content">{{ line.content }}</td>
382 + </tr>
383 + {% endif %}
384 + {% endfor %}
385 +
386 + {# Empty remaining buffer. There is a non-empty buffer if we did
387 + # not find an unchanged line, for example when context_lines=0.
388 + #}
389 + {{ print_buffer(buffer_deletions, buffer_insertions) }}
390 + {% endif %}
391 +
392 + {% endfor %}
393 +
394 + </table>
395 + </details>
396 +
397 + <hr/>
368 398
369 - <hr/>
370 -
371 - {% endfor %}
372 -
399 + {% endfor %}
373 400 {% endif %}
374 401 </div>
375 402

+47/-8 M   web.py
index 487dbdf..823b44e
old size: 29K - new size: 31K
@@ -546,7 +546,11 @@ def log_change(repository):
546 546 revision=revision))
547 547
548 548 @bottle.get('/<repository:path>.git/commit/<commit_id>', name='commit')
549 - def commit(repository, commit_id):
549 + @bottle.get('/<repository:path>.git/commit/<commit_id2>..<commit_id>', name='commit2')
550 + @bottle.get('/<repository:path>.git/commit/<commit_id>.patch', name='commit_patch')
551 + @bottle.get('/<repository:path>.git/commit/<commit_id>.diff', name='commit_diff')
552 + @bottle.get('/<repository:path>.git/commit/<commit_id2>..<commit_id>.diff', name='commit_diff2')
553 + def commit(repository, commit_id, commit_id2=None):
550 554 """
551 555 Show a commit.
552 556 """
@@ -562,6 +566,15 @@ def commit(repository, commit_id):
562 566 try:
563 567 commit = repo.get(commit_id)
564 568 assert commit.type == pygit2.GIT_OBJ_COMMIT
569 +
570 + if commit_id2:
571 + commit2 = repo.get(commit_id2)
572 + assert commit2.type == pygit2.GIT_OBJ_COMMIT
573 + elif len(commit.parents) > 0:
574 + commit2 = commit.parents[0]
575 + assert commit2.type == pygit2.GIT_OBJ_COMMIT
576 + else:
577 + commit2 = None
565 578 except:
566 579 bottle.abort(404, 'Not a valid commit.')
567 580
@@ -609,13 +622,12 @@ def commit(repository, commit_id):
609 622
610 623 # Compute diff with parent
611 624
612 - if len(commit.parents) > 0:
613 - diff = commit.parents[0].tree.diff_to_tree(
614 - commit.tree,
615 - context_lines = diff_context_lines,
616 - interhunk_lines = diff_inter_hunk_lines,
617 - flags = diff_flags)
625 + if commit2:
626 + diff = repo.diff(
627 + a=commit2, b=commit, flags=diff_flags,
628 + context_lines=diff_context_lines, interhunk_lines=diff_inter_hunk_lines)
618 629 else:
630 + # diff with empty tree
619 631 diff = commit.tree.diff_to_tree(
620 632 context_lines = diff_context_lines,
621 633 interhunk_lines = diff_inter_hunk_lines,
@@ -625,9 +637,36 @@ def commit(repository, commit_id):
625 637 # Compute the similarity index. This is used to decide which files are "renamed".
626 638 diff.find_similar()
627 639
640 + if request.route.name in [ 'commit_diff', 'commit_diff2' ]:
641 + response.content_type = 'text/plain'
642 + return diff.patch
643 +
644 + if request.route.name == 'commit_patch':
645 + """
646 + Looks like pygit2 doesn't have a way for creating a patch file from a commit.
647 + So, exclusively for this task, we fork a new subprocess and read the output.
648 + TODO see if this subprocess call can be replaced with pygit2.
649 + """
650 +
651 + try:
652 + output = subprocess.check_output(
653 + args=[ 'git', 'format-patch',
654 + '--no-signature',
655 + '--stdout',
656 + '--unified={}'.format(diff_context_lines),
657 + '--inter-hunk-context={}'.format(diff_inter_hunk_lines),
658 + '-1', str(commit.id) ],
659 + cwd=repository_path)
660 +
661 + response.content_type = 'text/plain'
662 + return output
663 + except Exception as e:
664 + print(e)
665 + bottle.abort(500, 'Cannot create patch.')
666 +
628 667 return template(
629 668 'repository/commit.html',
630 - repository=repository, commit=commit, diff=diff,
669 + repository=repository, commit=commit, commit2=commit2, diff=diff,
631 670 context_lines=diff_context_lines, inter_hunk_lines=diff_inter_hunk_lines,
632 671 mode=diff_mode, side=diff_side, whitespace=diff_whitespace)
633 672