FossilRepo

Add Git mirror UI: config page, add/remove mirrors, manual sync trigger Views: - /fossil/sync/git/ — Git mirror configuration page Add mirror: URL, auth method, token, sync mode, schedule, branch Remove mirror (soft delete) Shows status: last sync, success/fail, total syncs - /fossil/sync/git/<id>/run/ — manual sync trigger via Celery task Templates: - fossil/git_mirror.html — full config UI with add form - Sync page links to "Git Mirrors" button and "configure Git Mirror" link Requires admin project role (require_project_admin). Ref: #13

lmata 2026-04-07 02:54 trunk
Commit d9f28de75dc31d1286531bcf7b6aa9a357da3721f2348fe056cdd715be65156e
--- fossil/urls.py
+++ fossil/urls.py
@@ -27,10 +27,12 @@
2727
path("technotes/", views.technote_list, name="technotes"),
2828
path("search/", views.search, name="search"),
2929
path("stats/", views.repo_stats, name="stats"),
3030
path("compare/", views.compare_checkins, name="compare"),
3131
path("sync/", views.sync_pull, name="sync"),
32
+ path("sync/git/", views.git_mirror_config, name="git_mirror"),
33
+ path("sync/git/<int:mirror_id>/run/", views.git_mirror_run, name="git_mirror_run"),
3234
path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
3335
path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
3436
path("code/history/<path:filepath>", views.file_history, name="file_history"),
3537
path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
3638
path("tickets/export/", views.tickets_csv, name="tickets_csv"),
3739
--- fossil/urls.py
+++ fossil/urls.py
@@ -27,10 +27,12 @@
27 path("technotes/", views.technote_list, name="technotes"),
28 path("search/", views.search, name="search"),
29 path("stats/", views.repo_stats, name="stats"),
30 path("compare/", views.compare_checkins, name="compare"),
31 path("sync/", views.sync_pull, name="sync"),
 
 
32 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
33 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
34 path("code/history/<path:filepath>", views.file_history, name="file_history"),
35 path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
36 path("tickets/export/", views.tickets_csv, name="tickets_csv"),
37
--- fossil/urls.py
+++ fossil/urls.py
@@ -27,10 +27,12 @@
27 path("technotes/", views.technote_list, name="technotes"),
28 path("search/", views.search, name="search"),
29 path("stats/", views.repo_stats, name="stats"),
30 path("compare/", views.compare_checkins, name="compare"),
31 path("sync/", views.sync_pull, name="sync"),
32 path("sync/git/", views.git_mirror_config, name="git_mirror"),
33 path("sync/git/<int:mirror_id>/run/", views.git_mirror_run, name="git_mirror_run"),
34 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
35 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
36 path("code/history/<path:filepath>", views.file_history, name="file_history"),
37 path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
38 path("tickets/export/", views.tickets_csv, name="tickets_csv"),
39
--- fossil/views.py
+++ fossil/views.py
@@ -1030,10 +1030,89 @@
10301030
"result": result,
10311031
"active_tab": "sync",
10321032
},
10331033
)
10341034
1035
+
1036
+# --- Git Mirror ---
1037
+
1038
+
1039
+@login_required
1040
+def git_mirror_config(request, slug):
1041
+ """Configure Git mirror sync for a project."""
1042
+ project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin")
1043
+
1044
+ from fossil.sync_models import GitMirror
1045
+
1046
+ mirrors = GitMirror.objects.filter(repository=fossil_repo, deleted_at__isnull=True)
1047
+
1048
+ if request.method == "POST":
1049
+ action = request.POST.get("action", "")
1050
+ if action == "create":
1051
+ git_url = request.POST.get("git_remote_url", "").strip()
1052
+ auth_method = request.POST.get("auth_method", "token")
1053
+ auth_credential = request.POST.get("auth_credential", "").strip()
1054
+ sync_mode = request.POST.get("sync_mode", "scheduled")
1055
+ sync_schedule = request.POST.get("sync_schedule", "*/15 * * * *").strip()
1056
+ git_branch = request.POST.get("git_branch", "main").strip()
1057
+
1058
+ if git_url:
1059
+ GitMirror.objects.create(
1060
+ repository=fossil_repo,
1061
+ git_remote_url=git_url,
1062
+ auth_method=auth_method,
1063
+ auth_credential=auth_credential,
1064
+ sync_mode=sync_mode,
1065
+ sync_schedule=sync_schedule,
1066
+ git_branch=git_branch,
1067
+ created_by=request.user,
1068
+ )
1069
+ from django.contrib import messages
1070
+
1071
+ messages.success(request, f"Git mirror configured: {git_url}")
1072
+ from django.shortcuts import redirect
1073
+
1074
+ return redirect("fossil:git_mirror", slug=slug)
1075
+
1076
+ elif action == "delete":
1077
+ mirror_id = request.POST.get("mirror_id")
1078
+ mirror = GitMirror.objects.filter(pk=mirror_id, repository=fossil_repo).first()
1079
+ if mirror:
1080
+ mirror.soft_delete(user=request.user)
1081
+ from django.contrib import messages
1082
+
1083
+ messages.info(request, "Git mirror removed.")
1084
+
1085
+ return render(
1086
+ request,
1087
+ "fossil/git_mirror.html",
1088
+ {
1089
+ "project": project,
1090
+ "fossil_repo": fossil_repo,
1091
+ "mirrors": mirrors,
1092
+ "active_tab": "sync",
1093
+ },
1094
+ )
1095
+
1096
+
1097
+@login_required
1098
+def git_mirror_run(request, slug, mirror_id):
1099
+ """Manually trigger a Git sync for a specific mirror."""
1100
+ project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin")
1101
+
1102
+ if request.method == "POST":
1103
+ from fossil.tasks import run_git_sync
1104
+
1105
+ run_git_sync.delay(mirror_id)
1106
+ from django.contrib import messages
1107
+
1108
+ messages.info(request, "Git sync triggered. Check back shortly for results.")
1109
+
1110
+ from django.shortcuts import redirect
1111
+
1112
+ return redirect("fossil:git_mirror", slug=slug)
1113
+
10351114
10361115
# --- Technotes ---
10371116
10381117
10391118
def technote_list(request, slug):
10401119
10411120
ADDED templates/fossil/git_mirror.html
--- fossil/views.py
+++ fossil/views.py
@@ -1030,10 +1030,89 @@
1030 "result": result,
1031 "active_tab": "sync",
1032 },
1033 )
1034
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1035
1036 # --- Technotes ---
1037
1038
1039 def technote_list(request, slug):
1040
1041 DDED templates/fossil/git_mirror.html
--- fossil/views.py
+++ fossil/views.py
@@ -1030,10 +1030,89 @@
1030 "result": result,
1031 "active_tab": "sync",
1032 },
1033 )
1034
1035
1036 # --- Git Mirror ---
1037
1038
1039 @login_required
1040 def git_mirror_config(request, slug):
1041 """Configure Git mirror sync for a project."""
1042 project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin")
1043
1044 from fossil.sync_models import GitMirror
1045
1046 mirrors = GitMirror.objects.filter(repository=fossil_repo, deleted_at__isnull=True)
1047
1048 if request.method == "POST":
1049 action = request.POST.get("action", "")
1050 if action == "create":
1051 git_url = request.POST.get("git_remote_url", "").strip()
1052 auth_method = request.POST.get("auth_method", "token")
1053 auth_credential = request.POST.get("auth_credential", "").strip()
1054 sync_mode = request.POST.get("sync_mode", "scheduled")
1055 sync_schedule = request.POST.get("sync_schedule", "*/15 * * * *").strip()
1056 git_branch = request.POST.get("git_branch", "main").strip()
1057
1058 if git_url:
1059 GitMirror.objects.create(
1060 repository=fossil_repo,
1061 git_remote_url=git_url,
1062 auth_method=auth_method,
1063 auth_credential=auth_credential,
1064 sync_mode=sync_mode,
1065 sync_schedule=sync_schedule,
1066 git_branch=git_branch,
1067 created_by=request.user,
1068 )
1069 from django.contrib import messages
1070
1071 messages.success(request, f"Git mirror configured: {git_url}")
1072 from django.shortcuts import redirect
1073
1074 return redirect("fossil:git_mirror", slug=slug)
1075
1076 elif action == "delete":
1077 mirror_id = request.POST.get("mirror_id")
1078 mirror = GitMirror.objects.filter(pk=mirror_id, repository=fossil_repo).first()
1079 if mirror:
1080 mirror.soft_delete(user=request.user)
1081 from django.contrib import messages
1082
1083 messages.info(request, "Git mirror removed.")
1084
1085 return render(
1086 request,
1087 "fossil/git_mirror.html",
1088 {
1089 "project": project,
1090 "fossil_repo": fossil_repo,
1091 "mirrors": mirrors,
1092 "active_tab": "sync",
1093 },
1094 )
1095
1096
1097 @login_required
1098 def git_mirror_run(request, slug, mirror_id):
1099 """Manually trigger a Git sync for a specific mirror."""
1100 project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin")
1101
1102 if request.method == "POST":
1103 from fossil.tasks import run_git_sync
1104
1105 run_git_sync.delay(mirror_id)
1106 from django.contrib import messages
1107
1108 messages.info(request, "Git sync triggered. Check back shortly for results.")
1109
1110 from django.shortcuts import redirect
1111
1112 return redirect("fossil:git_mirror", slug=slug)
1113
1114
1115 # --- Technotes ---
1116
1117
1118 def technote_list(request, slug):
1119
1120 DDED templates/fossil/git_mirror.html
--- a/templates/fossil/git_mirror.html
+++ b/templates/fossil/git_mirror.html
@@ -0,0 +1,55 @@
1
+{% extends "baseGit Mirror%}Add Git Mirror{% endif %} — {{ project.name }} — Fossilrepo{% endblock %}
2
+
3
+{% block content %}
4
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
5
+{% include "fossil/_project_nav.html" %}
6
+
7
+<div class="max-w-3xl">
8
+ <div class="flex items-center justify-between mb-6">
9
+ <h2 class="text-lg foGit MirrorsFossil Sync</a>
10
+ </div>
11
+
12
+ <!-- Existing mirrors -->
13
+ {% if mirrors %}
14
+ <div class="space-y-4 mb-8">
15
+ {% for mirror in mirrors %}
16
+ <div class="7 1.834 2.807 1.304 35">and wiki.</p>
17
+ </div>
18
+
19
+ start justify-between"%}Add Git Mirror{% endif %}{% extends "base.html" %}
20
+{% block title %}{% if editing_mirror %}Edit Mirrobel in auth_method_choices %}
21
+ <option value="{{ value }}" {% if editing_mirror and editing_mirror.auth_method == value %}selected{% endif %}>{{ label }}</option>
22
+ {% endfor %}
23
+ </select>
24
+ </div>
25
+ </div>
26
+
27
+ <div>
28
+ <label class="block text-sm font-medium text-gray-300 mb-1">Token / Credential</label>
29
+ <input type="password" name="auth_credential"
30
+ placeholder="{% if editing_mirror %}Leave blank {{ mirror.get_auth_me value="{{ editinginline-flex rounded-full bg-gray-700 px-2 py-0.5">{{ mirror.get_sync_mode_display }}</span>
31
+ value="{{ editinginline-flex rounded-full bg-gray-700 px-2 py-0.5">{{ mirror.get_sync_direction_display }}</span>
32
+ <span>→ {{ mirl in auth_method_choi <div class="<svg class="h-5 w-5"ay-500"% else %}Add Git {% extends "base.html" %}
33
+{% block title %}{% if editing_mirror %}Edit Mirror{% else %}Add Gi {{ mirror.last_sync_at|timesince }} ago
34
+ L@1ui,1_:{% if mirror.last_sync_status == 'success' %}text-green-400{% else %}text-red-400{% endif %}">
35
+ 9@21R,b:({{ mirror.last_sync_status }})
36
+ 8@WF,v:span>
37
+ — {{ mirror.total_syncs }} total syncsH@1F0,V:lse %}
38
+ Never syncedJ@1F0,8:if %}
39
+ L@1cl,d: {% if mirror.last_sync_message %}
40
+ 8@218,4:<preV@27G,1L:600 max-h-20 overflow-hidden">{{ mirror.last_sync_message|truncatechars:200 }}</pre>
41
+A@23V,M@2Ev,7:div>
42
+ _@28~,E:gap-2">
43
+ O@o~,a:action="{% url 'fossil:git_mirror_runK@8W,N:mirror_id=mirror.id %}"H@1E~,E:csrf_token %}
44
+8@1yx,t@2Bs,G:3 py-1.5 text-xsQ@2Cy,d:hover:bg-brand-hover">Sync Now</button>
45
+A@1Ij,E:</form>
46
+ N@o~,H@1E~,G:csrf_token %}
47
+ N@1ZW,T@qK,6:deleteS@22W,k:hidden" name="mirror_id" value="{{ mirror.id }}G@22W,f@2Bx,K:gray-700 px-3 py-1.5J@1si,K:400 hover:text-red-4Y@IW,I:">Remove</button>
48
+A@1Ij,6:</formN@1ll,I:</div>
49
+ </div>
50
+J@1lD,r:</div>
51
+ {% endif %}
52
+
53
+ <!-- OAuth connect buttons -->~@nx,U9@DL,3G@gf,1L@k6,Q:
54
+ <!-- Add new mirror -->18@nx,J:h3 class="text-baseK@2Cy,Z:gray-200 mb-4">Add Git Mirror</h3>
55
+1o@o
--- a/templates/fossil/git_mirror.html
+++ b/templates/fossil/git_mirror.html
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/git_mirror.html
+++ b/templates/fossil/git_mirror.html
@@ -0,0 +1,55 @@
1 {% extends "baseGit Mirror%}Add Git Mirror{% endif %} — {{ project.name }} — Fossilrepo{% endblock %}
2
3 {% block content %}
4 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
5 {% include "fossil/_project_nav.html" %}
6
7 <div class="max-w-3xl">
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg foGit MirrorsFossil Sync</a>
10 </div>
11
12 <!-- Existing mirrors -->
13 {% if mirrors %}
14 <div class="space-y-4 mb-8">
15 {% for mirror in mirrors %}
16 <div class="7 1.834 2.807 1.304 35">and wiki.</p>
17 </div>
18
19 start justify-between"%}Add Git Mirror{% endif %}{% extends "base.html" %}
20 {% block title %}{% if editing_mirror %}Edit Mirrobel in auth_method_choices %}
21 <option value="{{ value }}" {% if editing_mirror and editing_mirror.auth_method == value %}selected{% endif %}>{{ label }}</option>
22 {% endfor %}
23 </select>
24 </div>
25 </div>
26
27 <div>
28 <label class="block text-sm font-medium text-gray-300 mb-1">Token / Credential</label>
29 <input type="password" name="auth_credential"
30 placeholder="{% if editing_mirror %}Leave blank {{ mirror.get_auth_me value="{{ editinginline-flex rounded-full bg-gray-700 px-2 py-0.5">{{ mirror.get_sync_mode_display }}</span>
31 value="{{ editinginline-flex rounded-full bg-gray-700 px-2 py-0.5">{{ mirror.get_sync_direction_display }}</span>
32 <span>→ {{ mirl in auth_method_choi <div class="<svg class="h-5 w-5"ay-500"% else %}Add Git {% extends "base.html" %}
33 {% block title %}{% if editing_mirror %}Edit Mirror{% else %}Add Gi {{ mirror.last_sync_at|timesince }} ago
34 L@1ui,1_:{% if mirror.last_sync_status == 'success' %}text-green-400{% else %}text-red-400{% endif %}">
35 9@21R,b:({{ mirror.last_sync_status }})
36 8@WF,v:span>
37 — {{ mirror.total_syncs }} total syncsH@1F0,V:lse %}
38 Never syncedJ@1F0,8:if %}
39 L@1cl,d: {% if mirror.last_sync_message %}
40 8@218,4:<preV@27G,1L:600 max-h-20 overflow-hidden">{{ mirror.last_sync_message|truncatechars:200 }}</pre>
41 A@23V,M@2Ev,7:div>
42 _@28~,E:gap-2">
43 O@o~,a:action="{% url 'fossil:git_mirror_runK@8W,N:mirror_id=mirror.id %}"H@1E~,E:csrf_token %}
44 8@1yx,t@2Bs,G:3 py-1.5 text-xsQ@2Cy,d:hover:bg-brand-hover">Sync Now</button>
45 A@1Ij,E:</form>
46 N@o~,H@1E~,G:csrf_token %}
47 N@1ZW,T@qK,6:deleteS@22W,k:hidden" name="mirror_id" value="{{ mirror.id }}G@22W,f@2Bx,K:gray-700 px-3 py-1.5J@1si,K:400 hover:text-red-4Y@IW,I:">Remove</button>
48 A@1Ij,6:</formN@1ll,I:</div>
49 </div>
50 J@1lD,r:</div>
51 {% endif %}
52
53 <!-- OAuth connect buttons -->~@nx,U9@DL,3G@gf,1L@k6,Q:
54 <!-- Add new mirror -->18@nx,J:h3 class="text-baseK@2Cy,Z:gray-200 mb-4">Add Git Mirror</h3>
55 1o@o
--- templates/fossil/sync.html
+++ templates/fossil/sync.html
@@ -48,10 +48,13 @@
4848
<input type="hidden" name="action" value="disable">
4949
<button type="submit" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
5050
Disable Sync
5151
</button>
5252
</form>
53
+ <a href="{% url 'fossil:git_mirror' slug=project.slug %}" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
54
+ Git Mirrors
55
+ </a>
5356
</div>
5457
</div>
5558
5659
{% if result %}
5760
<div class="rounded-lg {% if result.success %}bg-green-900/20 border border-green-800{% else %}bg-red-900/20 border border-red-800{% endif %} p-4">
@@ -102,15 +105,16 @@
102105
<li>You can also pull manually at any time</li>
103106
<li>Your local data is never overwritten — only new artifacts are added</li>
104107
</ul>
105108
</div>
106109
107
- <div class="flex justify-end">
110
+ <div class="flex items-center justify-between">
111
+ <a href="{% url 'fossil:git_mirror' slug=project.slug %}" class="text-sm text-brand-light hover:text-brand">Or configure Git Mirror (GitHub/GitLab) &rarr;</a>
108112
<button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
109113
Enable Sync
110114
</button>
111115
</div>
112116
</form>
113117
</div>
114118
{% endif %}
115119
</div>
116120
{% endblock %}
117121
--- templates/fossil/sync.html
+++ templates/fossil/sync.html
@@ -48,10 +48,13 @@
48 <input type="hidden" name="action" value="disable">
49 <button type="submit" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
50 Disable Sync
51 </button>
52 </form>
 
 
 
53 </div>
54 </div>
55
56 {% if result %}
57 <div class="rounded-lg {% if result.success %}bg-green-900/20 border border-green-800{% else %}bg-red-900/20 border border-red-800{% endif %} p-4">
@@ -102,15 +105,16 @@
102 <li>You can also pull manually at any time</li>
103 <li>Your local data is never overwritten — only new artifacts are added</li>
104 </ul>
105 </div>
106
107 <div class="flex justify-end">
 
108 <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
109 Enable Sync
110 </button>
111 </div>
112 </form>
113 </div>
114 {% endif %}
115 </div>
116 {% endblock %}
117
--- templates/fossil/sync.html
+++ templates/fossil/sync.html
@@ -48,10 +48,13 @@
48 <input type="hidden" name="action" value="disable">
49 <button type="submit" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
50 Disable Sync
51 </button>
52 </form>
53 <a href="{% url 'fossil:git_mirror' slug=project.slug %}" class="rounded-md bg-gray-700 px-3 py-2 text-sm text-gray-400 hover:text-white ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
54 Git Mirrors
55 </a>
56 </div>
57 </div>
58
59 {% if result %}
60 <div class="rounded-lg {% if result.success %}bg-green-900/20 border border-green-800{% else %}bg-red-900/20 border border-red-800{% endif %} p-4">
@@ -102,15 +105,16 @@
105 <li>You can also pull manually at any time</li>
106 <li>Your local data is never overwritten — only new artifacts are added</li>
107 </ul>
108 </div>
109
110 <div class="flex items-center justify-between">
111 <a href="{% url 'fossil:git_mirror' slug=project.slug %}" class="text-sm text-brand-light hover:text-brand">Or configure Git Mirror (GitHub/GitLab) &rarr;</a>
112 <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
113 Enable Sync
114 </button>
115 </div>
116 </form>
117 </div>
118 {% endif %}
119 </div>
120 {% endblock %}
121

Keyboard Shortcuts

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