-
- {# pygit2 appears to recompute all the stats every time we use diff.stats,
- # therefore we set this variable in order to compute it only once.
- #}
- {% set diff_stats = diff.stats %}
+
+
+ {# pygit2 appears to recompute all the stats every time we use diff.stats,
+ # therefore we set this variable in order to compute it only once.
+ #}
+ {% set diff_stats = diff.stats %}
+
+
- {% endif %}
-
- {% for hunk in patch.hunks if not patch.delta.is_binary %}
+ {% 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
+ # Looks like pygit2 also has a pygit2.DiffDelta.status_char() functions that
+ # returns the single-char abbreviation of the delta status, so we can use this
+ # instead of the raw integer.
+ # 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)
+ #}
+
+ {% set delta_char = patch.delta.status_char() %}
+
+
+
+
+ {% if patch.line_stats[1] + patch.line_stats[2] > 0 %}
+ {% set color_border = patch.line_stats[1] / ( patch.line_stats[1] + patch.line_stats[2] ) * 100 %}
+ {% else %}
+ {% set color_border = 0 %}
+ {% endif %}
- {#### UDIFF mode ####}
- {# In this mode the lines are printed one after the other. #}
+
+ +{{ patch.line_stats[1] }}/-{{ patch.line_stats[2] }}
+
- {% if mode == 'udiff' %}
-
- {% endfor %}
+ {% if delta_char == 'A' %}
+ A {{ patch.delta.new_file.path }}
{% endif %}
- {#### SSDIFF mode ####}
- {# In this mode, changed lines are buffered until we find an
- # unchanged line. When an unchanged line has been found, the
- # buffer is emptied.
- #}
+ {% if delta_char == 'D' %}
+ D {{ patch.delta.old_file.path }}
+ {% endif %}
- {% if mode == 'ssdiff' %}
- {% set buffer_deletions = [] %}
- {% set buffer_insertions = [] %}
-
- {% macro print_buffer(buffer_deletions, buffer_insertions) %}
- {% for buffer_del, buffer_ins in zip_longest(buffer_deletions, buffer_insertions) %}
-
- {% endfor %}
-
- {# .clear() empties the buffer. Since this 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 %}
-
-
-
{{ hunk.header }}
+ {% if delta_char == 'M' %}
+ M {{ patch.delta.new_file.path }}
+ {% endif %}
+
+ {% if delta_char == 'R' %}
+ R {{ patch.delta.old_file.path }} -> {{ patch.delta.new_file.path }}
+ {% endif %}
+
+ {% if delta_char == 'C' %}
+ C {{ patch.delta.old_file.path }} -> {{ patch.delta.new_file.path }}
+ {% endif %}
+
+
+ {% endif %}
+
+ {% for hunk in patch.hunks if not patch.delta.is_binary %}
- {% 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 %}
+ {#### UDIFF mode ####}
+ {# In this mode the lines are printed one after the other. #}
+
+ {% if mode == 'udiff' %}
+
- {% endif %}
- {% endfor %}
+ {% endfor %}
+ {% endif %}
- {# Empty remaining buffer. There is a non-empty buffer if we did
- # not find an unchanged line, for example when context_lines=0.
+ {#### SSDIFF mode ####}
+ {# In this mode, changed lines are buffered until we find an
+ # unchanged line. When an unchanged line has been found, the
+ # buffer is emptied.
#}
- {{ print_buffer(buffer_deletions, buffer_insertions) }}
- {% endif %}
-
- {% endfor %}
-
-
-
+
+ {% if mode == 'ssdiff' %}
+ {% set buffer_deletions = [] %}
+ {% set buffer_insertions = [] %}
+
+ {% macro print_buffer(buffer_deletions, buffer_insertions) %}
+ {% for buffer_del, buffer_ins in zip_longest(buffer_deletions, buffer_insertions) %}
+
+ {% endfor %}
+
+ {# .clear() empties the buffer. Since this 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 %}
+
+
+
{{ hunk.header }}
+
+
+ {% 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) }}
+
+ {# Insert the unchanged line. #}
+
+ {% endif %}
+ {% endfor %}
+
+ {# Empty remaining buffer. There is a non-empty buffer if we did
+ # not find an unchanged line, for example when context_lines=0.
+ #}
+ {{ print_buffer(buffer_deletions, buffer_insertions) }}
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
-
-
- {% endfor %}
-
+ {% endfor %}
{% endif %}
diff --git a/web.py b/web.py
index 487dbdf..823b44e 100644
--- a/web.py
+++ b/web.py
@@ -546,7 +546,11 @@ def log_change(repository):
revision=revision))
@bottle.get('/.git/commit/', name='commit')
-def commit(repository, commit_id):
+@bottle.get('/.git/commit/..', name='commit2')
+@bottle.get('/.git/commit/.patch', name='commit_patch')
+@bottle.get('/.git/commit/.diff', name='commit_diff')
+@bottle.get('/.git/commit/...diff', name='commit_diff2')
+def commit(repository, commit_id, commit_id2=None):
"""
Show a commit.
"""
@@ -562,6 +566,15 @@ def commit(repository, commit_id):
try:
commit = repo.get(commit_id)
assert commit.type == pygit2.GIT_OBJ_COMMIT
+
+ if commit_id2:
+ commit2 = repo.get(commit_id2)
+ assert commit2.type == pygit2.GIT_OBJ_COMMIT
+ elif len(commit.parents) > 0:
+ commit2 = commit.parents[0]
+ assert commit2.type == pygit2.GIT_OBJ_COMMIT
+ else:
+ commit2 = None
except:
bottle.abort(404, 'Not a valid commit.')
@@ -609,13 +622,12 @@ def commit(repository, commit_id):
# Compute diff with parent
- if len(commit.parents) > 0:
- diff = commit.parents[0].tree.diff_to_tree(
- commit.tree,
- context_lines = diff_context_lines,
- interhunk_lines = diff_inter_hunk_lines,
- flags = diff_flags)
+ if commit2:
+ diff = repo.diff(
+ a=commit2, b=commit, flags=diff_flags,
+ context_lines=diff_context_lines, interhunk_lines=diff_inter_hunk_lines)
else:
+ # diff with empty tree
diff = commit.tree.diff_to_tree(
context_lines = diff_context_lines,
interhunk_lines = diff_inter_hunk_lines,
@@ -625,9 +637,36 @@ def commit(repository, commit_id):
# Compute the similarity index. This is used to decide which files are "renamed".
diff.find_similar()
+ if request.route.name in [ 'commit_diff', 'commit_diff2' ]:
+ response.content_type = 'text/plain'
+ return diff.patch
+
+ if request.route.name == 'commit_patch':
+ """
+ Looks like pygit2 doesn't have a way for creating a patch file from a commit.
+ So, exclusively for this task, we fork a new subprocess and read the output.
+ TODO see if this subprocess call can be replaced with pygit2.
+ """
+
+ try:
+ output = subprocess.check_output(
+ args=[ 'git', 'format-patch',
+ '--no-signature',
+ '--stdout',
+ '--unified={}'.format(diff_context_lines),
+ '--inter-hunk-context={}'.format(diff_inter_hunk_lines),
+ '-1', str(commit.id) ],
+ cwd=repository_path)
+
+ response.content_type = 'text/plain'
+ return output
+ except Exception as e:
+ print(e)
+ bottle.abort(500, 'Cannot create patch.')
+
return template(
'repository/commit.html',
- repository=repository, commit=commit, diff=diff,
+ repository=repository, commit=commit, commit2=commit2, diff=diff,
context_lines=diff_context_lines, inter_hunk_lines=diff_inter_hunk_lines,
mode=diff_mode, side=diff_side, whitespace=diff_whitespace)