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 }}
+
+
{% 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 #}
@@ -136,30 +193,112 @@
{% endif %}
{% for hunk in patch.hunks if not patch.delta.is_binary %}
-
- |
- {% for line in hunk.lines %}
-
-
- {% if line.old_lineno >= 0 %}
- {{ line.old_lineno }}
- {% endif %}
- |
-
- {% if line.new_lineno >= 0 %}
- {{ line.new_lineno }}
- {% endif %}
- |
- {{ line.origin }} |
- {{ line.content }} |
+ {#### UDIFF mode ####}
+
+ {% if mode == 'udiff' %}
+
- {% endfor %}
+
+ {% for line in hunk.lines %}
+
+
+ {% if line.old_lineno >= 0 %}
+ {{ line.old_lineno }}
+ {% endif %}
+ |
+
+ {% if line.new_lineno >= 0 %}
+ {{ line.new_lineno }}
+ {% endif %}
+ |
+ {{ line.origin }} |
+ {{ line.content }} |
+
+ {% 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 %}
+
+ {{ buffer_del.old_lineno }}
+ |
+ {{ buffer_del.content }} |
+ {% else %}
+
+ |
+ |
+ {% endif %}
+
+ {% if buffer_ins %}
+
+ {{ buffer_ins.new_lineno }}
+ |
+ {{ buffer_ins.content }} |
+ {% 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) }}
+
+
+
+ {{ line.old_lineno }}
+ |
+ {{ line.content }} |
+
+
+ {{ line.new_lineno }}
+ |
+ {{ line.content }} |
+
+ {% 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):