From d548a4cce21583ea79a3ca272a9d46d34738cb23 Mon Sep 17 00:00:00 2001 From: zPlus Date: Sun, 7 Aug 2022 11:10:08 +0200 Subject: [PATCH] Add diff options in commit page. Allow diff options to be changed when viewing a commit: view (udiff, ssdiff), context lines, interhunk lines, side (normal, reverse), whitespace. --- static/css/clif.css | 9 +- templates/repository/commit.html | 195 ++++++++++++++++++++++++++----- web.py | 66 ++++++++++- 3 files changed, 235 insertions(+), 35 deletions(-) diff --git a/static/css/clif.css b/static/css/clif.css index 98a2bf1..20fd6fb 100644 --- a/static/css/clif.css +++ b/static/css/clif.css @@ -326,7 +326,7 @@ div.commit { } div.commit .message { - margin-top: 2rem; + margin: 2rem 0; white-space: pre-wrap; } @@ -376,7 +376,12 @@ div.commit { background-color: #e6ffed; } - div.commit .diff .content { + div.commit .diff .change { + background-color: #feffbe; + } + + div.commit .diff .ssdiff .content { + width: 50%; white-space: pre-wrap; } diff --git a/templates/repository/commit.html b/templates/repository/commit.html index 39f2ca7..f814f2d 100644 --- a/templates/repository/commit.html +++ b/templates/repository/commit.html @@ -66,19 +66,76 @@
{{ commit.message }}
+
+
+ Diff options +
+ + + + + + + + + + + + + + + + + + + + + +
+ View + + + +
+ Context lines + + +
+ Inter-hunk lines + + +
+ Side + + + +
+ Whitespace + + + + + +
+ +
+
+
+ {% for patch in diff %} {# The following status values are defined in the git_delta_t enum # in libgit2. See https://github.com/libgit2/libgit2/blob/main/include/git2/diff.h + # 0 = UNCHANGED + # 1 = ADDED (does not exist in old version) + # 2 = DELETED (does not exist in new version) + # 3 = MODIFIED (content changed between old and new versions) + # 4 = RENAMED + # 5 = COPIED + # ... (there are other codes that we don't use) #} - - {# 1=ADDED (does not exist in old version) #} - {# 2=DELETED (does not exist in new version) #} - {# 3=MODIFIED (content changed between old and new versions) #} - {# 4=RENAMED #} - {# 5=COPIED #} - - - - - - {% for line in hunk.lines %} - - - - - + {#### UDIFF mode ####} + + {% if mode == 'udiff' %} + + + + + - {% endfor %} + + {% for line in hunk.lines %} + + + + + + + {% endfor %} + {% endif %} + + {#### SSDIFF mode ####} + + {% if mode == 'ssdiff' %} + {% macro print_buffer(buffer_deletions, buffer_insertions) %} + {% for buffer_del, buffer_ins in zip_longest(buffer_deletions, buffer_insertions) %} + + {% if buffer_del %} + + + {% else %} + + + {% endif %} + + {% if buffer_ins %} + + + {% else %} + + + {% endif %} + + {% endfor %} + + {# .clear() empties the buffer. However since it requires + # {{}} brackets instead of {%%}, this line will print the + # value of "buffer_deletions" which is "None". + # "or ''" is a hack for printing an empty line instead of "None". + #} + {{ buffer_deletions.clear() or '' }} + {{ buffer_insertions.clear() or '' }} + {% endmacro %} + + + + + + {% set buffer_deletions = [] %} + {% set buffer_insertions = [] %} + + {% for line in hunk.lines %} + {% if line.old_lineno < 0 %} + {{ buffer_insertions.append(line) or '' }} + {% endif %} + + {% if line.new_lineno < 0 %} + {{ buffer_deletions.append(line) or '' }} + {% endif %} + + {% if line.old_lineno >= 0 and line.new_lineno >= 0 %} + + {# Unload buffer #} + {{ print_buffer(buffer_deletions, buffer_insertions) }} + + + + + + + + + {% endif %} + {% endfor %} + + {# Empty remaining buffer #} + {{ print_buffer(buffer_deletions, buffer_insertions) }} + {% endif %} {% endfor %} diff --git a/web.py b/web.py index d007ed6..26ac0c5 100644 --- a/web.py +++ b/web.py @@ -7,6 +7,7 @@ import email.policy import functools import glob import hashlib +import itertools import magic import os import pathlib @@ -150,7 +151,7 @@ def humanct(commit_time, commit_time_offset = 0): dt = datetime.datetime.fromtimestamp(commit_time, tz) return dt.astimezone(pytz.utc).strftime('%Y-%m-%d %H:%M:%S') - + template = functools.partial(template, template_settings = { 'filters': { 'ago': timeago.format, @@ -166,6 +167,7 @@ template = functools.partial(template, template_settings = { 'now': lambda: datetime.datetime.now(datetime.timezone.utc), 'request': request, 'url': application.get_url, + 'zip_longest': itertools.zip_longest }, 'autoescape': True }) @@ -541,17 +543,71 @@ def commit(repository, commit_id): except: bottle.abort(404, 'Not a valid commit.') - # Find diff wih parent + # Diff options + + diff_mode = 'udiff' + if 'mode' in request.query: + if request.query.get('mode') in [ 'udiff', 'ssdiff' ]: + diff_mode = request.query.get('mode') + else: + bottle.abort(400, 'Bad request: mode') + + try: diff_context_lines = int(request.query.get('context_lines', 3)) + except: bottle.abort(400, 'Bad request: context_lines') + + try: diff_inter_hunk_lines = int(request.query.get('inter_hunk_lines', 0)) + except: bottle.abort(400, 'Bad request: inter_hunk_lines') + + diff_flags = pygit2.GIT_DIFF_NORMAL + diff_side = 'normal' + if 'side' in request.query: + if request.query.get('side') == 'normal': + diff_side = 'normal' + elif request.query.get('side') == 'reverse': + diff_flags |= pygit2.GIT_DIFF_REVERSE + diff_side = 'reverse' + else: + bottle.abort(400, 'Bad request: side') + + diff_whitespace = 'include' + if 'whitespace' in request.query: + if request.query.get('whitespace') == 'include': + diff_whitespace = 'include' + elif request.query.get('whitespace') == 'ignore_all': + diff_flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE + diff_whitespace = 'ignore_all' + elif request.query.get('whitespace') == 'ignore_change': + diff_flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE_CHANGE + diff_whitespace = 'ignore_change' + elif request.query.get('whitespace') == 'ignore_eol': + diff_flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE_EOL + diff_whitespace = 'ignore_eol' + else: + bottle.abort(400, 'Bad request: whitespace') + + # Compute diff with parent + if len(commit.parents) > 0: - diff = commit.parents[0].tree.diff_to_tree(commit.tree) + diff = commit.parents[0].tree.diff_to_tree( + commit.tree, + context_lines = diff_context_lines, + interhunk_lines = diff_inter_hunk_lines, + flags = diff_flags) else: - diff = commit.tree.diff_to_tree(swap=True) + diff = commit.tree.diff_to_tree( + context_lines = diff_context_lines, + interhunk_lines = diff_inter_hunk_lines, + flags = diff_flags, + swap = True) + # Compute the similarity index. This is used to decide which files are "renamed". diff.find_similar() return template( 'repository/commit.html', - repository=repository, commit=commit, diff=diff) + repository=repository, commit=commit, diff=diff, + context_lines=diff_context_lines, inter_hunk_lines=diff_inter_hunk_lines, + mode=diff_mode, side=diff_side, whitespace=diff_whitespace) @bottle.get('/.git/raw//', name='raw') def raw(repository, revision, tree_path):
@@ -136,30 +193,112 @@ {% endif %} {% for hunk in patch.hunks if not patch.delta.is_binary %} - -
{{ hunk.header }}
- {% if line.old_lineno >= 0 %} - {{ line.old_lineno }} - {% endif %} - - {% if line.new_lineno >= 0 %} - {{ line.new_lineno }} - {% endif %} - {{ line.origin }}{{ line.content }}
{{ hunk.header }}
+ {% if line.old_lineno >= 0 %} + {{ line.old_lineno }} + {% endif %} + + {% if line.new_lineno >= 0 %} + {{ line.new_lineno }} + {% endif %} + {{ line.origin }}{{ line.content }}
+ {{ buffer_del.old_lineno }} + {{ buffer_del.content }} + + {{ buffer_ins.new_lineno }} + {{ buffer_ins.content }} +
{{ hunk.header }}
+ {{ line.old_lineno }} + {{ line.content }} + {{ line.new_lineno }} + {{ line.content }}