FossilRepo

Add GitHub/GitLab OAuth, sync error handling, cleanup old code

ragelink 2026-04-07 03:19 trunk
Commit 70fa9579745a8a25ac92893e0bb6bb53b8f0a3148aa6439e13055fe80134613a
--- a/fossil/oauth.py
+++ b/fossil/oauth.py
@@ -0,0 +1,61 @@
1
+"""Lightweight OAuth2 flows for GitHub and GitLab.
2
+
3
+No dependency on django-allauth — just requests + constance config.
4
+Stores tokens on GitMirror.auth_crede
5
+import requests
6
+
7
+logger = logging.getLogger(__name__)
8
+
9
+GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
10
+GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
11
+GITHUB_USER_URL = "https://api.github.com/user"
12
+
13
+GITLAB_AUTHORIZE_URL = "https://gitlab.com/oauth/authorize"
14
+GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"
15
+
16
+
17
+def github_authorize_url(request, slug, mirror_id=None):
18
+ """Build GitHub OAuth authorization URL."""
19
+ from constance import config
20
+
21
+ client_id = config.GITHUB_OAUTH_CLIENT_ID
22
+ if not client_id:
23
+ return None
24
+
25
+ callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/github/")
26
+ nce"] = no"
27
+
28
+ return f"{GITHUB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&scope=repo&state={state}"
29
+
30
+
31
+def github_exchange_token(request, slug):
32
+ """Exchange GitHub OAuth code for access token. Returns {token, username, error}."""
33
+ from constance import config
34
+
35
+ code = request.GET.get("code", "")
36
+ if not code:
37
+ return {"token": "", "username": "", "error": "No code received"}
38
+
39
+ client_id = config.GITHUB_OAUTH_CLIENT_ID
40
+ client_secret = config.GITHUB_OAUTH_CLIENT_SECRET
41
+
42
+ try:
43
+ resp = requests.post(
44
+ GITHUB_TOKEN_URL,
45
+ data={"client_id": client_id, "client_secret": client_secret, "code": code},
46
+ headers={"Accept": "application/json"},
47
+ timeout=15,
48
+ )
49
+ data = resp.json()
50
+ token = data.get("access_token", "")
51
+ if not token:
52
+ return {"token": "", "username": "", "error": data.get("error_description", "Token exchange failed")}
53
+
54
+ # Get username
55
+ user_resp = requests.get(GITHUB_USER_URL, headers={"Authorization": f"Bearer {token}"}, timeout=10)
56
+ username = user_resp.json().get("login", "") if user_resp.ok else ""
57
+
58
+ return {"token": token, "username": username, "error": ""}
59
+ except Exception as e:
60
+ logger.exception("Gif"/projects/{slug}/fossil/sync/gitub/")
61
+ nce"] = no
--- a/fossil/oauth.py
+++ b/fossil/oauth.py
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/fossil/oauth.py
+++ b/fossil/oauth.py
@@ -0,0 +1,61 @@
1 """Lightweight OAuth2 flows for GitHub and GitLab.
2
3 No dependency on django-allauth — just requests + constance config.
4 Stores tokens on GitMirror.auth_crede
5 import requests
6
7 logger = logging.getLogger(__name__)
8
9 GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
10 GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
11 GITHUB_USER_URL = "https://api.github.com/user"
12
13 GITLAB_AUTHORIZE_URL = "https://gitlab.com/oauth/authorize"
14 GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"
15
16
17 def github_authorize_url(request, slug, mirror_id=None):
18 """Build GitHub OAuth authorization URL."""
19 from constance import config
20
21 client_id = config.GITHUB_OAUTH_CLIENT_ID
22 if not client_id:
23 return None
24
25 callback = request.build_absolute_uri(f"/projects/{slug}/fossil/sync/git/callback/github/")
26 nce"] = no"
27
28 return f"{GITHUB_AUTHORIZE_URL}?client_id={client_id}&redirect_uri={callback}&scope=repo&state={state}"
29
30
31 def github_exchange_token(request, slug):
32 """Exchange GitHub OAuth code for access token. Returns {token, username, error}."""
33 from constance import config
34
35 code = request.GET.get("code", "")
36 if not code:
37 return {"token": "", "username": "", "error": "No code received"}
38
39 client_id = config.GITHUB_OAUTH_CLIENT_ID
40 client_secret = config.GITHUB_OAUTH_CLIENT_SECRET
41
42 try:
43 resp = requests.post(
44 GITHUB_TOKEN_URL,
45 data={"client_id": client_id, "client_secret": client_secret, "code": code},
46 headers={"Accept": "application/json"},
47 timeout=15,
48 )
49 data = resp.json()
50 token = data.get("access_token", "")
51 if not token:
52 return {"token": "", "username": "", "error": data.get("error_description", "Token exchange failed")}
53
54 # Get username
55 user_resp = requests.get(GITHUB_USER_URL, headers={"Authorization": f"Bearer {token}"}, timeout=10)
56 username = user_resp.json().get("login", "") if user_resp.ok else ""
57
58 return {"token": token, "username": username, "error": ""}
59 except Exception as e:
60 logger.exception("Gif"/projects/{slug}/fossil/sync/gitub/")
61 nce"] = no
--- fossil/urls.py
+++ fossil/urls.py
@@ -29,10 +29,14 @@
2929
path("stats/", views.repo_stats, name="stats"),
3030
path("compare/", views.compare_checkins, name="compare"),
3131
path("sync/", views.sync_pull, name="sync"),
3232
path("sync/git/", views.git_mirror_config, name="git_mirror"),
3333
path("sync/git/<int:mirror_id>/run/", views.git_mirror_run, name="git_mirror_run"),
34
+ path("sync/git/connect/github/", views.oauth_github_start, name="oauth_github"),
35
+ path("sync/git/connect/gitlab/", views.oauth_gitlab_start, name="oauth_gitlab"),
36
+ path("sync/git/callback/github/", views.oauth_github_callback, name="oauth_github_callback"),
37
+ path("sync/git/callback/gitlab/", views.oauth_gitlab_callback, name="oauth_gitlab_callback"),
3438
path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
3539
path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
3640
path("code/history/<path:filepath>", views.file_history, name="file_history"),
3741
path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
3842
path("tickets/export/", views.tickets_csv, name="tickets_csv"),
3943
--- fossil/urls.py
+++ fossil/urls.py
@@ -29,10 +29,14 @@
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/urls.py
+++ fossil/urls.py
@@ -29,10 +29,14 @@
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("sync/git/connect/github/", views.oauth_github_start, name="oauth_github"),
35 path("sync/git/connect/gitlab/", views.oauth_gitlab_start, name="oauth_gitlab"),
36 path("sync/git/callback/github/", views.oauth_github_callback, name="oauth_github_callback"),
37 path("sync/git/callback/gitlab/", views.oauth_gitlab_callback, name="oauth_gitlab_callback"),
38 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
39 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
40 path("code/history/<path:filepath>", views.file_history, name="file_history"),
41 path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
42 path("tickets/export/", views.tickets_csv, name="tickets_csv"),
43
+84 -4
--- fossil/views.py
+++ fossil/views.py
@@ -2,11 +2,11 @@
22
import re
33
44
import markdown as md
55
from django.contrib.auth.decorators import login_required
66
from django.http import Http404
7
-from django.shortcuts import get_object_or_404, render
7
+from django.shortcuts import get_object_or_404, redirect, render
88
from django.utils.safestring import mark_safe
99
1010
from projects.models import Project
1111
1212
from .models import FossilRepository
@@ -1049,10 +1049,16 @@
10491049
action = request.POST.get("action", "")
10501050
if action == "create":
10511051
git_url = request.POST.get("git_remote_url", "").strip()
10521052
auth_method = request.POST.get("auth_method", "token")
10531053
auth_credential = request.POST.get("auth_credential", "").strip()
1054
+ # Use OAuth token from session if available and no manual credential provided
1055
+ if not auth_credential:
1056
+ if auth_method == "oauth_github" and request.session.get("github_oauth_token"):
1057
+ auth_credential = request.session.pop("github_oauth_token")
1058
+ elif auth_method == "oauth_gitlab" and request.session.get("gitlab_oauth_token"):
1059
+ auth_credential = request.session.pop("gitlab_oauth_token")
10541060
sync_mode = request.POST.get("sync_mode", "scheduled")
10551061
sync_schedule = request.POST.get("sync_schedule", "*/15 * * * *").strip()
10561062
git_branch = request.POST.get("git_branch", "main").strip()
10571063
10581064
if git_url:
@@ -1100,17 +1106,91 @@
11001106
project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin")
11011107
11021108
if request.method == "POST":
11031109
from fossil.tasks import run_git_sync
11041110
1105
- run_git_sync.delay(mirror_id)
1106
- from django.contrib import messages
1111
+ try:
1112
+ run_git_sync.delay(mirror_id)
1113
+ from django.contrib import messages
11071114
1108
- messages.info(request, "Git sync triggered. Check back shortly for results.")
1115
+ messages.info(request, "Git sync triggered in background.")
1116
+ except Exception:
1117
+ # Celery not available — run synchronously
1118
+ run_git_sync(mirror_id)
1119
+ from django.contrib import messages
1120
+
1121
+ messages.success(request, "Git sync completed.")
11091122
11101123
from django.shortcuts import redirect
11111124
1125
+ return redirect("fossil:git_mirror", slug=slug)
1126
+
1127
+
1128
+# --- OAuth ---
1129
+
1130
+
1131
+@login_required
1132
+def oauth_github_start(request, slug):
1133
+ """Start GitHub OAuth flow."""
1134
+ from fossil.oauth import github_authorize_url
1135
+
1136
+ url = github_authorize_url(request, slug)
1137
+ if not url:
1138
+ from django.contrib import messages
1139
+
1140
+ messages.error(request, "GitHub OAuth not configured. Set GITHUB_OAUTH_CLIENT_ID in admin settings.")
1141
+ return redirect("fossil:git_mirror", slug=slug)
1142
+ return redirect(url)
1143
+
1144
+
1145
+@login_required
1146
+def oauth_gitlab_start(request, slug):
1147
+ """Start GitLab OAuth flow."""
1148
+ from fossil.oauth import gitlab_authorize_url
1149
+
1150
+ url = gitlab_authorize_url(request, slug)
1151
+ if not url:
1152
+ from django.contrib import messages
1153
+
1154
+ messages.error(request, "GitLab OAuth not configured. Set GITLAB_OAUTH_CLIENT_ID in admin settings.")
1155
+ return redirect("fossil:git_mirror", slug=slug)
1156
+ return redirect(url)
1157
+
1158
+
1159
+@login_required
1160
+def oauth_github_callback(request, slug):
1161
+ """Handle GitHub OAuth callback."""
1162
+ from fossil.oauth import github_exchange_token
1163
+
1164
+ result = github_exchange_token(request, slug)
1165
+ from django.contrib import messages
1166
+
1167
+ if result["token"]:
1168
+ # Store token in session for the mirror config form to pick up
1169
+ request.session["github_oauth_token"] = result["token"]
1170
+ request.session["github_oauth_user"] = result.get("username", "")
1171
+ messages.success(request, f"Connected to GitHub as {result.get('username', 'unknown')}. Now configure your mirror.")
1172
+ else:
1173
+ messages.error(request, f"GitHub OAuth failed: {result.get('error', 'Unknown error')}")
1174
+
1175
+ return redirect("fossil:git_mirror", slug=slug)
1176
+
1177
+
1178
+@login_required
1179
+def oauth_gitlab_callback(request, slug):
1180
+ """Handle GitLab OAuth callback."""
1181
+ from fossil.oauth import gitlab_exchange_token
1182
+
1183
+ result = gitlab_exchange_token(request, slug)
1184
+ from django.contrib import messages
1185
+
1186
+ if result["token"]:
1187
+ request.session["gitlab_oauth_token"] = result["token"]
1188
+ messages.success(request, "Connected to GitLab. Now configure your mirror.")
1189
+ else:
1190
+ messages.error(request, f"GitLab OAuth failed: {result.get('error', 'Unknown error')}")
1191
+
11121192
return redirect("fossil:git_mirror", slug=slug)
11131193
11141194
11151195
# --- Technotes ---
11161196
11171197
--- fossil/views.py
+++ fossil/views.py
@@ -2,11 +2,11 @@
2 import re
3
4 import markdown as md
5 from django.contrib.auth.decorators import login_required
6 from django.http import Http404
7 from django.shortcuts import get_object_or_404, render
8 from django.utils.safestring import mark_safe
9
10 from projects.models import Project
11
12 from .models import FossilRepository
@@ -1049,10 +1049,16 @@
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:
@@ -1100,17 +1106,91 @@
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
--- fossil/views.py
+++ fossil/views.py
@@ -2,11 +2,11 @@
2 import re
3
4 import markdown as md
5 from django.contrib.auth.decorators import login_required
6 from django.http import Http404
7 from django.shortcuts import get_object_or_404, redirect, render
8 from django.utils.safestring import mark_safe
9
10 from projects.models import Project
11
12 from .models import FossilRepository
@@ -1049,10 +1049,16 @@
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 # Use OAuth token from session if available and no manual credential provided
1055 if not auth_credential:
1056 if auth_method == "oauth_github" and request.session.get("github_oauth_token"):
1057 auth_credential = request.session.pop("github_oauth_token")
1058 elif auth_method == "oauth_gitlab" and request.session.get("gitlab_oauth_token"):
1059 auth_credential = request.session.pop("gitlab_oauth_token")
1060 sync_mode = request.POST.get("sync_mode", "scheduled")
1061 sync_schedule = request.POST.get("sync_schedule", "*/15 * * * *").strip()
1062 git_branch = request.POST.get("git_branch", "main").strip()
1063
1064 if git_url:
@@ -1100,17 +1106,91 @@
1106 project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin")
1107
1108 if request.method == "POST":
1109 from fossil.tasks import run_git_sync
1110
1111 try:
1112 run_git_sync.delay(mirror_id)
1113 from django.contrib import messages
1114
1115 messages.info(request, "Git sync triggered in background.")
1116 except Exception:
1117 # Celery not available — run synchronously
1118 run_git_sync(mirror_id)
1119 from django.contrib import messages
1120
1121 messages.success(request, "Git sync completed.")
1122
1123 from django.shortcuts import redirect
1124
1125 return redirect("fossil:git_mirror", slug=slug)
1126
1127
1128 # --- OAuth ---
1129
1130
1131 @login_required
1132 def oauth_github_start(request, slug):
1133 """Start GitHub OAuth flow."""
1134 from fossil.oauth import github_authorize_url
1135
1136 url = github_authorize_url(request, slug)
1137 if not url:
1138 from django.contrib import messages
1139
1140 messages.error(request, "GitHub OAuth not configured. Set GITHUB_OAUTH_CLIENT_ID in admin settings.")
1141 return redirect("fossil:git_mirror", slug=slug)
1142 return redirect(url)
1143
1144
1145 @login_required
1146 def oauth_gitlab_start(request, slug):
1147 """Start GitLab OAuth flow."""
1148 from fossil.oauth import gitlab_authorize_url
1149
1150 url = gitlab_authorize_url(request, slug)
1151 if not url:
1152 from django.contrib import messages
1153
1154 messages.error(request, "GitLab OAuth not configured. Set GITLAB_OAUTH_CLIENT_ID in admin settings.")
1155 return redirect("fossil:git_mirror", slug=slug)
1156 return redirect(url)
1157
1158
1159 @login_required
1160 def oauth_github_callback(request, slug):
1161 """Handle GitHub OAuth callback."""
1162 from fossil.oauth import github_exchange_token
1163
1164 result = github_exchange_token(request, slug)
1165 from django.contrib import messages
1166
1167 if result["token"]:
1168 # Store token in session for the mirror config form to pick up
1169 request.session["github_oauth_token"] = result["token"]
1170 request.session["github_oauth_user"] = result.get("username", "")
1171 messages.success(request, f"Connected to GitHub as {result.get('username', 'unknown')}. Now configure your mirror.")
1172 else:
1173 messages.error(request, f"GitHub OAuth failed: {result.get('error', 'Unknown error')}")
1174
1175 return redirect("fossil:git_mirror", slug=slug)
1176
1177
1178 @login_required
1179 def oauth_gitlab_callback(request, slug):
1180 """Handle GitLab OAuth callback."""
1181 from fossil.oauth import gitlab_exchange_token
1182
1183 result = gitlab_exchange_token(request, slug)
1184 from django.contrib import messages
1185
1186 if result["token"]:
1187 request.session["gitlab_oauth_token"] = result["token"]
1188 messages.success(request, "Connected to GitLab. Now configure your mirror.")
1189 else:
1190 messages.error(request, f"GitLab OAuth failed: {result.get('error', 'Unknown error')}")
1191
1192 return redirect("fossil:git_mirror", slug=slug)
1193
1194
1195 # --- Technotes ---
1196
1197
--- pyproject.toml
+++ pyproject.toml
@@ -24,10 +24,11 @@
2424
"boto3>=1.35",
2525
"sentry-sdk[django]>=2.14",
2626
"click>=8.1",
2727
"rich>=13.0",
2828
"markdown>=3.6",
29
+ "requests>=2.31",
2930
]
3031
3132
[project.scripts]
3233
fossilrepo-ctl = "ctl.main:cli"
3334
3435
--- pyproject.toml
+++ pyproject.toml
@@ -24,10 +24,11 @@
24 "boto3>=1.35",
25 "sentry-sdk[django]>=2.14",
26 "click>=8.1",
27 "rich>=13.0",
28 "markdown>=3.6",
 
29 ]
30
31 [project.scripts]
32 fossilrepo-ctl = "ctl.main:cli"
33
34
--- pyproject.toml
+++ pyproject.toml
@@ -24,10 +24,11 @@
24 "boto3>=1.35",
25 "sentry-sdk[django]>=2.14",
26 "click>=8.1",
27 "rich>=13.0",
28 "markdown>=3.6",
29 "requests>=2.31",
30 ]
31
32 [project.scripts]
33 fossilrepo-ctl = "ctl.main:cli"
34
35
--- templates/fossil/git_mirror.html
+++ templates/fossil/git_mirror.html
@@ -55,10 +55,33 @@
5555
</div>
5656
</div>
5757
{% endfor %}
5858
</div>
5959
{% endif %}
60
+
61
+ <!-- OAuth connect buttons -->
62
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-5 mb-6">
63
+ <h3 class="text-sm font-semibold text-gray-200 mb-3">Quick Connect</h3>
64
+ <div class="flex items-center gap-3">
65
+ <a href="{% url 'fossil:oauth_github' slug=project.slug %}"
66
+ class="inline-flex items-center gap-2 rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-gray-200 ring-1 ring-inset ring-gray-600 hover:bg-gray-700">
67
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
68
+ Connect GitHub
69
+ </a>
70
+ <a href="{% url 'fossil:oauth_gitlab' slug=project.slug %}"
71
+ class="inline-flex items-center gap-2 rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-gray-200 ring-1 ring-inset ring-gray-600 hover:bg-gray-700">
72
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"><path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/></svg>
73
+ Connect GitLab
74
+ </a>
75
+ </div>
76
+ {% if request.session.github_oauth_token %}
77
+ <p class="mt-2 text-xs text-green-400">GitHub connected as {{ request.session.github_oauth_user }}. Token will be used for new mirrors.</p>
78
+ {% endif %}
79
+ {% if request.session.gitlab_oauth_token %}
80
+ <p class="mt-2 text-xs text-green-400">GitLab connected. Token will be used for new mirrors.</p>
81
+ {% endif %}
82
+ </div>
6083
6184
<!-- Add new mirror -->
6285
<div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
6386
<h3 class="text-base font-semibold text-gray-200 mb-4">Add Git Mirror</h3>
6487
6588
--- templates/fossil/git_mirror.html
+++ templates/fossil/git_mirror.html
@@ -55,10 +55,33 @@
55 </div>
56 </div>
57 {% endfor %}
58 </div>
59 {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
61 <!-- Add new mirror -->
62 <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
63 <h3 class="text-base font-semibold text-gray-200 mb-4">Add Git Mirror</h3>
64
65
--- templates/fossil/git_mirror.html
+++ templates/fossil/git_mirror.html
@@ -55,10 +55,33 @@
55 </div>
56 </div>
57 {% endfor %}
58 </div>
59 {% endif %}
60
61 <!-- OAuth connect buttons -->
62 <div class="rounded-lg bg-gray-800 border border-gray-700 p-5 mb-6">
63 <h3 class="text-sm font-semibold text-gray-200 mb-3">Quick Connect</h3>
64 <div class="flex items-center gap-3">
65 <a href="{% url 'fossil:oauth_github' slug=project.slug %}"
66 class="inline-flex items-center gap-2 rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-gray-200 ring-1 ring-inset ring-gray-600 hover:bg-gray-700">
67 <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
68 Connect GitHub
69 </a>
70 <a href="{% url 'fossil:oauth_gitlab' slug=project.slug %}"
71 class="inline-flex items-center gap-2 rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-gray-200 ring-1 ring-inset ring-gray-600 hover:bg-gray-700">
72 <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"><path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/></svg>
73 Connect GitLab
74 </a>
75 </div>
76 {% if request.session.github_oauth_token %}
77 <p class="mt-2 text-xs text-green-400">GitHub connected as {{ request.session.github_oauth_user }}. Token will be used for new mirrors.</p>
78 {% endif %}
79 {% if request.session.gitlab_oauth_token %}
80 <p class="mt-2 text-xs text-green-400">GitLab connected. Token will be used for new mirrors.</p>
81 {% endif %}
82 </div>
83
84 <!-- Add new mirror -->
85 <div class="rounded-lg bg-gray-800 border border-gray-700 p-6">
86 <h3 class="text-base font-semibold text-gray-200 mb-4">Add Git Mirror</h3>
87
88
+2
--- uv.lock
+++ uv.lock
@@ -574,10 +574,11 @@
574574
{ name = "django-storages", extra = ["s3"] },
575575
{ name = "gunicorn" },
576576
{ name = "markdown" },
577577
{ name = "psycopg2-binary" },
578578
{ name = "redis" },
579
+ { name = "requests" },
579580
{ name = "rich" },
580581
{ name = "sentry-sdk", extra = ["django"] },
581582
{ name = "whitenoise" },
582583
]
583584
@@ -616,10 +617,11 @@
616617
{ name = "psycopg2-binary", specifier = ">=2.9" },
617618
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
618619
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
619620
{ name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
620621
{ name = "redis", specifier = ">=5.0" },
622
+ { name = "requests", specifier = ">=2.31" },
621623
{ name = "rich", specifier = ">=13.0" },
622624
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
623625
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
624626
{ name = "whitenoise", specifier = ">=6.7" },
625627
]
626628
--- uv.lock
+++ uv.lock
@@ -574,10 +574,11 @@
574 { name = "django-storages", extra = ["s3"] },
575 { name = "gunicorn" },
576 { name = "markdown" },
577 { name = "psycopg2-binary" },
578 { name = "redis" },
 
579 { name = "rich" },
580 { name = "sentry-sdk", extra = ["django"] },
581 { name = "whitenoise" },
582 ]
583
@@ -616,10 +617,11 @@
616 { name = "psycopg2-binary", specifier = ">=2.9" },
617 { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
618 { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
619 { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
620 { name = "redis", specifier = ">=5.0" },
 
621 { name = "rich", specifier = ">=13.0" },
622 { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
623 { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
624 { name = "whitenoise", specifier = ">=6.7" },
625 ]
626
--- uv.lock
+++ uv.lock
@@ -574,10 +574,11 @@
574 { name = "django-storages", extra = ["s3"] },
575 { name = "gunicorn" },
576 { name = "markdown" },
577 { name = "psycopg2-binary" },
578 { name = "redis" },
579 { name = "requests" },
580 { name = "rich" },
581 { name = "sentry-sdk", extra = ["django"] },
582 { name = "whitenoise" },
583 ]
584
@@ -616,10 +617,11 @@
617 { name = "psycopg2-binary", specifier = ">=2.9" },
618 { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" },
619 { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
620 { name = "pytest-django", marker = "extra == 'dev'", specifier = ">=4.9" },
621 { name = "redis", specifier = ">=5.0" },
622 { name = "requests", specifier = ">=2.31" },
623 { name = "rich", specifier = ">=13.0" },
624 { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7" },
625 { name = "sentry-sdk", extras = ["django"], specifier = ">=2.14" },
626 { name = "whitenoise", specifier = ">=6.7" },
627 ]
628

Keyboard Shortcuts

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