FossilRepo

Add compare checkins view for arbitrary diff between two commits - /fossil/compare/?from=HASH&to=HASH shows side-by-side checkin info and unified diff for all changed files between the two checkins - Form with two hash inputs for selecting commits to compare - Color-coded diff output (add/del/hunk) reusing diff styles - Limited to 20 files per comparison for performance

lmata 2026-04-07 00:34 trunk
Commit 2d30fd873a2031aad0d5f5f75fcb2792513788bc5c66f5ad5fc42414e296c2a9
--- fossil/urls.py
+++ fossil/urls.py
@@ -23,10 +23,11 @@
2323
path("branches/", views.branch_list, name="branches"),
2424
path("tags/", views.tag_list, name="tags"),
2525
path("technotes/", views.technote_list, name="technotes"),
2626
path("search/", views.search, name="search"),
2727
path("stats/", views.repo_stats, name="stats"),
28
+ path("compare/", views.compare_checkins, name="compare"),
2829
path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
2930
path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
3031
path("code/history/<path:filepath>", views.file_history, name="file_history"),
3132
path("docs/", views.fossil_docs, name="docs"),
3233
path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
3334
--- fossil/urls.py
+++ fossil/urls.py
@@ -23,10 +23,11 @@
23 path("branches/", views.branch_list, name="branches"),
24 path("tags/", views.tag_list, name="tags"),
25 path("technotes/", views.technote_list, name="technotes"),
26 path("search/", views.search, name="search"),
27 path("stats/", views.repo_stats, name="stats"),
 
28 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
29 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
30 path("code/history/<path:filepath>", views.file_history, name="file_history"),
31 path("docs/", views.fossil_docs, name="docs"),
32 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
33
--- fossil/urls.py
+++ fossil/urls.py
@@ -23,10 +23,11 @@
23 path("branches/", views.branch_list, name="branches"),
24 path("tags/", views.tag_list, name="tags"),
25 path("technotes/", views.technote_list, name="technotes"),
26 path("search/", views.search, name="search"),
27 path("stats/", views.repo_stats, name="stats"),
28 path("compare/", views.compare_checkins, name="compare"),
29 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
30 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
31 path("code/history/<path:filepath>", views.file_history, name="file_history"),
32 path("docs/", views.fossil_docs, name="docs"),
33 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
34
--- fossil/views.py
+++ fossil/views.py
@@ -1,5 +1,6 @@
1
+import contextlib
12
import re
23
34
import markdown as md
45
from django.contrib.auth.decorators import login_required
56
from django.http import Http404
@@ -885,10 +886,91 @@
885886
request,
886887
"fossil/technote_list.html",
887888
{"project": project, "notes": notes, "active_tab": "wiki"},
888889
)
889890
891
+
892
+# --- Compare Checkins ---
893
+
894
+
895
+@login_required
896
+def compare_checkins(request, slug):
897
+ """Compare two checkins side by side."""
898
+ P.PROJECT_VIEW.check(request.user)
899
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
900
+
901
+ from_uuid = request.GET.get("from", "")
902
+ to_uuid = request.GET.get("to", "")
903
+
904
+ from_detail = None
905
+ to_detail = None
906
+ file_diffs = []
907
+
908
+ if from_uuid and to_uuid:
909
+ with reader:
910
+ from_detail = reader.get_checkin_detail(from_uuid)
911
+ to_detail = reader.get_checkin_detail(to_uuid)
912
+
913
+ if from_detail and to_detail:
914
+ # Get all files from both checkins and compute diffs
915
+ from_files = {f["name"]: f for f in from_detail.files_changed}
916
+ to_files = {f["name"]: f for f in to_detail.files_changed}
917
+ all_files = sorted(set(list(from_files.keys()) + list(to_files.keys())))
918
+
919
+ import difflib
920
+
921
+ for fname in all_files[:20]: # Limit to 20 files for performance
922
+ old_text = ""
923
+ new_text = ""
924
+ f_from = from_files.get(fname, {})
925
+ f_to = to_files.get(fname, {})
926
+
927
+ if f_from.get("uuid"):
928
+ with contextlib.suppress(Exception):
929
+ old_text = reader.get_file_content(f_from["uuid"]).decode("utf-8", errors="replace")
930
+ if f_to.get("uuid"):
931
+ with contextlib.suppress(Exception):
932
+ new_text = reader.get_file_content(f_to["uuid"]).decode("utf-8", errors="replace")
933
+
934
+ if old_text != new_text:
935
+ diff = difflib.unified_diff(
936
+ old_text.splitlines(keepends=True),
937
+ new_text.splitlines(keepends=True),
938
+ fromfile=f"a/{fname}",
939
+ tofile=f"b/{fname}",
940
+ n=3,
941
+ )
942
+ diff_lines = []
943
+ for line in diff:
944
+ line_type = "context"
945
+ if line.startswith("+++") or line.startswith("---"):
946
+ line_type = "header"
947
+ elif line.startswith("@@"):
948
+ line_type = "hunk"
949
+ elif line.startswith("+"):
950
+ line_type = "add"
951
+ elif line.startswith("-"):
952
+ line_type = "del"
953
+ diff_lines.append({"text": line, "type": line_type})
954
+
955
+ if diff_lines:
956
+ file_diffs.append({"name": fname, "diff_lines": diff_lines})
957
+
958
+ return render(
959
+ request,
960
+ "fossil/compare.html",
961
+ {
962
+ "project": project,
963
+ "from_uuid": from_uuid,
964
+ "to_uuid": to_uuid,
965
+ "from_detail": from_detail,
966
+ "to_detail": to_detail,
967
+ "file_diffs": file_diffs,
968
+ "active_tab": "timeline",
969
+ },
970
+ )
971
+
890972
891973
# --- Search ---
892974
893975
894976
@login_required
895977
896978
ADDED templates/fossil/compare.html
--- fossil/views.py
+++ fossil/views.py
@@ -1,5 +1,6 @@
 
1 import re
2
3 import markdown as md
4 from django.contrib.auth.decorators import login_required
5 from django.http import Http404
@@ -885,10 +886,91 @@
885 request,
886 "fossil/technote_list.html",
887 {"project": project, "notes": notes, "active_tab": "wiki"},
888 )
889
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
891 # --- Search ---
892
893
894 @login_required
895
896 DDED templates/fossil/compare.html
--- fossil/views.py
+++ fossil/views.py
@@ -1,5 +1,6 @@
1 import contextlib
2 import re
3
4 import markdown as md
5 from django.contrib.auth.decorators import login_required
6 from django.http import Http404
@@ -885,10 +886,91 @@
886 request,
887 "fossil/technote_list.html",
888 {"project": project, "notes": notes, "active_tab": "wiki"},
889 )
890
891
892 # --- Compare Checkins ---
893
894
895 @login_required
896 def compare_checkins(request, slug):
897 """Compare two checkins side by side."""
898 P.PROJECT_VIEW.check(request.user)
899 project, fossil_repo, reader = _get_repo_and_reader(slug)
900
901 from_uuid = request.GET.get("from", "")
902 to_uuid = request.GET.get("to", "")
903
904 from_detail = None
905 to_detail = None
906 file_diffs = []
907
908 if from_uuid and to_uuid:
909 with reader:
910 from_detail = reader.get_checkin_detail(from_uuid)
911 to_detail = reader.get_checkin_detail(to_uuid)
912
913 if from_detail and to_detail:
914 # Get all files from both checkins and compute diffs
915 from_files = {f["name"]: f for f in from_detail.files_changed}
916 to_files = {f["name"]: f for f in to_detail.files_changed}
917 all_files = sorted(set(list(from_files.keys()) + list(to_files.keys())))
918
919 import difflib
920
921 for fname in all_files[:20]: # Limit to 20 files for performance
922 old_text = ""
923 new_text = ""
924 f_from = from_files.get(fname, {})
925 f_to = to_files.get(fname, {})
926
927 if f_from.get("uuid"):
928 with contextlib.suppress(Exception):
929 old_text = reader.get_file_content(f_from["uuid"]).decode("utf-8", errors="replace")
930 if f_to.get("uuid"):
931 with contextlib.suppress(Exception):
932 new_text = reader.get_file_content(f_to["uuid"]).decode("utf-8", errors="replace")
933
934 if old_text != new_text:
935 diff = difflib.unified_diff(
936 old_text.splitlines(keepends=True),
937 new_text.splitlines(keepends=True),
938 fromfile=f"a/{fname}",
939 tofile=f"b/{fname}",
940 n=3,
941 )
942 diff_lines = []
943 for line in diff:
944 line_type = "context"
945 if line.startswith("+++") or line.startswith("---"):
946 line_type = "header"
947 elif line.startswith("@@"):
948 line_type = "hunk"
949 elif line.startswith("+"):
950 line_type = "add"
951 elif line.startswith("-"):
952 line_type = "del"
953 diff_lines.append({"text": line, "type": line_type})
954
955 if diff_lines:
956 file_diffs.append({"name": fname, "diff_lines": diff_lines})
957
958 return render(
959 request,
960 "fossil/compare.html",
961 {
962 "project": project,
963 "from_uuid": from_uuid,
964 "to_uuid": to_uuid,
965 "from_detail": from_detail,
966 "to_detail": to_detail,
967 "file_diffs": file_diffs,
968 "active_tab": "timeline",
969 },
970 )
971
972
973 # --- Search ---
974
975
976 @login_required
977
978 DDED templates/fossil/compare.html
--- a/templates/fossil/compare.html
+++ b/templates/fossil/compare.html
@@ -0,0 +1,39 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}Compare — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block extra_head %}
6
+<style>
7
+ .diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
8
+ .diff-table td { padding: 0 8px; white-space: pre; vertical-align: top; line-height: 1.4rem; }
9
+ .diff-line-add { background: rgba(34, 197, 94, 0.1); }
10
+ .diff-line-add td:last-child { color: #86efac; }
11
+ .diff-line-del { background: rgba(239, 68, 68, 0.1); }
12
+ .diff-line-del td:last-child { color: #fca5a5; }
13
+ .diff-line-hunk { background: rgba(96, 165, 250, 0.08); }
14
+ .diff-line-hunk td { color: #93c5fd; }
15
+ .diff-line-heat %}
16
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
17
+{% include "fossil/_project_nav.html" %}
18
+
19
+<div class="mb-6">
20
+ <h2 class="text-lg font-semibold text-gray-200 mb-3">Compare Checkins</h2>
21
+ <form method="get" class="flex items-end gap-3">
22
+ <div class="flex-1">
23
+ <label class="block </label>
24
+ <input type="text" name="from" value="{{ from_uuid }}" placeholder="Checkin hash..."
25
+ aria-label="From checkin hash"
26
+ class="w-frder-gray-700 bg-gray-800 text-gray-100 text-sm px-3 py-2 font-mono focus:border-brand focus:ring-brand">
27
+ </div>
28
+ <div class="flex-1">
29
+ <label class="block text-xs text-gray-500 mb-1">To (newer)</label>
30
+ <input type="text" name="to" value="{{ to_uuid }}" placeholder="Checkin hash..."
31
+ aria-label="To checkin hashid #374151; white-space: nowrap; }
32
+ /* Split diff view */
33
+ .split-diff { display: grid; grid-template-columns: 1fr 1fr; }
34
+ .split-diff-si.split-diffright: 1px solid #374151; }
35
+ .split-diff-side .diff-table td:last-child { width: 100%; }
36
+ .split-line-add { background: rgba(34, 197, 94, 0.1); }
37
+ .split-line-add td:last-child { color: #86efac; }
38
+ .split-line-del { background: rgba(239, 68, 68, 0.1); }
39
+ .spli
--- a/templates/fossil/compare.html
+++ b/templates/fossil/compare.html
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/compare.html
+++ b/templates/fossil/compare.html
@@ -0,0 +1,39 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Compare — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <style>
7 .diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
8 .diff-table td { padding: 0 8px; white-space: pre; vertical-align: top; line-height: 1.4rem; }
9 .diff-line-add { background: rgba(34, 197, 94, 0.1); }
10 .diff-line-add td:last-child { color: #86efac; }
11 .diff-line-del { background: rgba(239, 68, 68, 0.1); }
12 .diff-line-del td:last-child { color: #fca5a5; }
13 .diff-line-hunk { background: rgba(96, 165, 250, 0.08); }
14 .diff-line-hunk td { color: #93c5fd; }
15 .diff-line-heat %}
16 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
17 {% include "fossil/_project_nav.html" %}
18
19 <div class="mb-6">
20 <h2 class="text-lg font-semibold text-gray-200 mb-3">Compare Checkins</h2>
21 <form method="get" class="flex items-end gap-3">
22 <div class="flex-1">
23 <label class="block </label>
24 <input type="text" name="from" value="{{ from_uuid }}" placeholder="Checkin hash..."
25 aria-label="From checkin hash"
26 class="w-frder-gray-700 bg-gray-800 text-gray-100 text-sm px-3 py-2 font-mono focus:border-brand focus:ring-brand">
27 </div>
28 <div class="flex-1">
29 <label class="block text-xs text-gray-500 mb-1">To (newer)</label>
30 <input type="text" name="to" value="{{ to_uuid }}" placeholder="Checkin hash..."
31 aria-label="To checkin hashid #374151; white-space: nowrap; }
32 /* Split diff view */
33 .split-diff { display: grid; grid-template-columns: 1fr 1fr; }
34 .split-diff-si.split-diffright: 1px solid #374151; }
35 .split-diff-side .diff-table td:last-child { width: 100%; }
36 .split-line-add { background: rgba(34, 197, 94, 0.1); }
37 .split-line-add td:last-child { color: #86efac; }
38 .split-line-del { background: rgba(239, 68, 68, 0.1); }
39 .spli

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button