app.py
Python script, Unicode text, UTF-8 text executable
1
"""
2
Roundabout - git hosting for everyone <https://roundabout-host.com>
3
Copyright (C) 2023-2025 Roundabout developers <root@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU Affero General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU Affero General Public License for more details.
14
15
You should have received a copy of the GNU Affero General Public License
16
along with this program. If not, see <http://www.gnu.org/licenses/>.
17
"""
18
19
__version__ = "0.6.0"
20
21
import os
22
import shutil
23
import random
24
import subprocess
25
import platform
26
27
import PIL
28
import git
29
import mimetypes
30
import magic
31
import flask
32
import cairosvg
33
import celery
34
import shlex
35
from functools import wraps
36
from datetime import datetime
37
from enum import Enum
38
from cairosvg import svg2png
39
from flask_sqlalchemy import SQLAlchemy
40
from flask_bcrypt import Bcrypt
41
from markupsafe import escape, Markup
42
from flask_migrate import Migrate
43
from PIL import Image
44
from flask_httpauth import HTTPBasicAuth
45
import config
46
import markdown
47
from common import git_command
48
from flask_babel import Babel, gettext, ngettext, force_locale
49
from jinja2_fragments.flask import render_block
50
51
import logging
52
53
54
class No304(logging.Filter):
55
def filter(self, record):
56
return not record.getMessage().strip().endswith("304 -")
57
58
59
logging.getLogger("werkzeug").addFilter(No304())
60
61
_ = gettext
62
n_ = ngettext
63
64
app = flask.Flask(__name__)
65
app.config.from_mapping(
66
CELERY=dict(
67
broker_url=config.REDIS_URI,
68
result_backend=config.REDIS_URI,
69
task_ignore_result=True,
70
),
71
)
72
73
auth = HTTPBasicAuth()
74
75
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
76
app.config["SECRET_KEY"] = config.DB_PASSWORD
77
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
78
app.config["BABEL_TRANSLATION_DIRECTORIES"] = "i18n"
79
app.config["MAX_CONTENT_LENGTH"] = config.MAX_PAYLOAD_SIZE
80
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
81
app.config["SESSION_COOKIE_SECURE"] = config.suggest_https # only send cookies over HTTPS if the server is configured for it
82
app.config["SESSION_COOKIE_HTTPONLY"] = True # don't allow JS to access the cookie
83
if config.restrict_cookie_domain:
84
app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN # don't share across subdomains, since user content is hosted there
85
86
db = SQLAlchemy(app)
87
bcrypt = Bcrypt(app)
88
migrate = Migrate(app, db)
89
90
from misc_utils import *
91
92
import git_http
93
import api
94
import jinja_utils
95
import celery_tasks
96
from celery import Celery, Task
97
import celery_integration
98
import pathlib
99
100
from models import *
101
102
babel = Babel(app)
103
104
105
def get_locale():
106
if flask.request.cookies.get("language"):
107
return flask.request.cookies.get("language")
108
return flask.request.accept_languages.best_match(config.available_locales)
109
110
111
babel.init_app(app, locale_selector=get_locale)
112
113
with app.app_context():
114
locale_names = {}
115
for language in config.available_locales:
116
with force_locale(language):
117
# NOTE: Translate this to the language's name in that language, for example in French you would use français
118
locale_names[language] = gettext("English")
119
120
worker = celery_integration.init_celery_app(app)
121
122
repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/")
123
124
app.jinja_env.add_extension("jinja2.ext.do")
125
app.jinja_env.add_extension("jinja2.ext.loopcontrols")
126
app.jinja_env.add_extension("jinja2.ext.debug")
127
128
129
@app.context_processor
130
def default():
131
username = flask.session.get("username")
132
133
user_object = User.query.filter_by(username=username).first()
134
135
return {
136
"logged_in_user": username,
137
"user_object": user_object,
138
"Notification": Notification,
139
"unread": UserNotification.query.filter_by(user_username=username).filter(
140
UserNotification.attention_level > 0).count(),
141
"config": config,
142
"Markup": Markup,
143
"locale_names": locale_names,
144
"set": set, # since using {} is impossible in Jinja
145
"request": flask.request,
146
"get_visibility": get_visibility,
147
"get_permission_level": get_permission_level,
148
}
149
150
151
@app.route("/")
152
def main():
153
if flask.session.get("username"):
154
return flask.render_template("home.html")
155
else:
156
return flask.render_template("no-home.html")
157
158
159
@app.route("/userstyle")
160
def userstyle():
161
if flask.session.get("username") and os.path.exists(
162
os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config",
163
"theme.css")):
164
return flask.send_from_directory(
165
os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config"),
166
"theme.css")
167
else:
168
return flask.Response("", mimetype="text/css")
169
170
171
@app.route("/about/")
172
def about():
173
return flask.render_template("about.html", platform=platform, version=__version__)
174
175
176
@app.route("/search")
177
def search():
178
query = flask.request.args.get("q")
179
sorting = flask.request.args.get("sort", "default")
180
page_number = flask.request.args.get("page", 1, type=int)
181
if flask.session.get("username"):
182
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
183
else:
184
default_page_length = 16
185
page_length = flask.request.args.get("per_page", default_page_length, type=int)
186
187
results = Repo.query.filter(Repo.name.ilike(f"%{query}%")).filter_by(visibility=2)
188
189
match sorting:
190
case "popularity":
191
results = results.outerjoin(RepoFavourite).group_by(Repo.route).order_by(
192
db.func.count(RepoFavourite.id).desc())
193
case "newest":
194
results = results.order_by(Repo.creation_date.desc())
195
case "oldest":
196
results = results.order_by(Repo.creation_date)
197
198
results = results.paginate(page=page_number, per_page=page_length)
199
200
if results.has_next:
201
next_page = results.next_num
202
else:
203
next_page = None
204
205
if results.has_prev:
206
prev_page = results.prev_num
207
else:
208
prev_page = None
209
210
return flask.render_template("search.html", results=results, query=query,
211
page_number=page_number,
212
page_length=page_length,
213
next_page=next_page,
214
prev_page=prev_page,
215
num_pages=results.pages)
216
217
218
@app.route("/user-search")
219
def user_search():
220
query = flask.request.args.get("q")
221
sorting = flask.request.args.get("sort", "default")
222
page_number = flask.request.args.get("page", 1, type=int)
223
if flask.session.get("username"):
224
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
225
else:
226
default_page_length = 16
227
page_length = flask.request.args.get("per_page", default_page_length, type=int)
228
229
results = User.query.filter(User.username.ilike(f"%{query}%"))
230
231
match sorting:
232
case "popularity":
233
results = results.outerjoin(UserFollow, User.username == UserFollow.followed_username).group_by(
234
User.username).order_by(db.func.count(UserFollow.id).desc())
235
case "newest":
236
results = results.order_by(User.creation_date.desc())
237
case "oldest":
238
results = results.order_by(User.creation_date)
239
240
results = results.paginate(page=page_number, per_page=page_length)
241
242
if results.has_next:
243
next_page = results.next_num
244
else:
245
next_page = None
246
247
if results.has_prev:
248
prev_page = results.prev_num
249
else:
250
prev_page = None
251
252
return flask.render_template("user-search.html", results=results, query=query,
253
page_number=page_number,
254
page_length=page_length,
255
next_page=next_page,
256
prev_page=prev_page,
257
num_pages=results.pages)
258
259
260
@app.route("/language", methods=["POST"])
261
def set_locale():
262
response = flask.redirect(flask.request.referrer if flask.request.referrer else "/",
263
code=303)
264
if not flask.request.form.get("language"):
265
response.delete_cookie("language")
266
else:
267
response.set_cookie("language", flask.request.form.get("language"))
268
269
return response
270
271
272
@app.route("/cookie-dismiss")
273
def dismiss_banner():
274
response = flask.redirect(flask.request.referrer if flask.request.referrer else "/",
275
code=303)
276
response.set_cookie("cookie-banner", "1")
277
return response
278
279
280
@app.route("/help/")
281
def help_redirect():
282
return flask.redirect(config.help_url, code=302)
283
284
285
@app.route("/settings/")
286
def settings():
287
if not flask.session.get("username"):
288
flask.abort(401)
289
user = User.query.filter_by(username=flask.session.get("username")).first()
290
291
return flask.render_template("user-settings.html", user=user)
292
293
294
@app.route("/settings/confirm-email/<code>")
295
def confirm_email(code):
296
request = EmailChangeRequest.query.filter_by(code=code).first()
297
if not request:
298
flask.abort(404)
299
300
user = db.session.get(User, request.user_username)
301
user.email = request.new_email
302
db.session.delete(request)
303
db.session.commit()
304
305
return flask.redirect("/settings", code=303)
306
307
308
@app.route("/settings/profile", methods=["POST"])
309
def settings_profile():
310
user = User.query.filter_by(username=flask.session.get("username")).first()
311
312
user.display_name = flask.request.form["displayname"]
313
user.URL = flask.request.form["url"]
314
user.company = flask.request.form["company"]
315
user.company_URL = flask.request.form["companyurl"]
316
if not flask.request.form.get("email"):
317
# Deleting the email can be instant; no need to confirm
318
user.email = ""
319
elif flask.request.form.get("email") != user.email:
320
# Changing the email requires confirmation from the address holder
321
celery_tasks.request_email_change.delay(user.username, flask.request.form["email"])
322
user.location = flask.request.form["location"]
323
user.show_mail = True if flask.request.form.get("showmail") else False
324
user.bio = flask.request.form.get("bio")
325
326
db.session.commit()
327
328
flask.flash(
329
Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")),
330
category="success")
331
return flask.redirect(f"/{flask.session.get('username')}", code=303)
332
333
334
@app.route("/settings/preferences", methods=["POST"])
335
def settings_prefs():
336
user = User.query.filter_by(username=flask.session.get("username")).first()
337
338
user.default_page_length = int(flask.request.form["page_length"])
339
user.max_post_nesting = int(flask.request.form["max_post_nesting"])
340
341
db.session.commit()
342
343
flask.flash(
344
Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")),
345
category="success")
346
return flask.redirect(f"/{flask.session.get('username')}", code=303)
347
348
349
@app.route("/favourites/", methods=["GET", "POST"])
350
def favourites():
351
if not flask.session.get("username"):
352
flask.abort(401)
353
if flask.request.method == "GET":
354
relationships = RepoFavourite.query.filter_by(
355
user_username=flask.session.get("username"))
356
357
return flask.render_template("favourites.html", favourites=relationships)
358
359
360
@app.route("/favourites/<int:id>", methods=["POST"])
361
def favourite_edit(id):
362
if not flask.session.get("username"):
363
flask.abort(401)
364
favourite = db.session.get(RepoFavourite, id)
365
if favourite.user_username != flask.session.get("username"):
366
flask.abort(403)
367
data = flask.request.form
368
favourite.notify_commit = js_to_bool(data.get("commit"))
369
favourite.notify_forum = js_to_bool(data.get("forum"))
370
favourite.notify_pr = js_to_bool(data.get("pull_request"))
371
favourite.notify_admin = js_to_bool(data.get("administrative"))
372
db.session.commit()
373
return flask.render_template_string(
374
"""
375
<tr hx-post="/favourites/{{ favourite.id }}" hx-trigger="change" hx-include="#commit-{{ favourite.id }}, #forum-{{ favourite.id }}, #pull_request-{{ favourite.id }}, #administrative-{{ favourite.id }}" hx-headers='{"Content-Type": "application/json"}' hx-swap="outerHTML">
376
<td><a href="{{ favourite.repo.route }}">{{ favourite.repo.owner.username }}/{{ favourite.repo.name }}</a></td>
377
<td style="text-align: center;"><input type="checkbox" name="commit" id="commit-{{ favourite.id }}" value="true" {% if favourite.notify_commit %}checked{% endif %}></td>
378
<td style="text-align: center;"><input type="checkbox" name="forum" id="forum-{{ favourite.id }}" value="true" {% if favourite.notify_forum %}checked{% endif %}></td>
379
<td style="text-align: center;"><input type="checkbox" name="pull_request" id="pull_request-{{ favourite.id }}" value="true" {% if favourite.notify_pr %}checked{% endif %}></td>
380
<td style="text-align: center;"><input type="checkbox" name="administrative" id="administrative-{{ favourite.id }}" value="true" {% if favourite.notify_admin %}checked{% endif %}></td>
381
</tr>
382
""",
383
favourite=favourite
384
)
385
386
387
@app.route("/notifications/", methods=["GET", "POST"])
388
def notifications():
389
if not flask.session.get("username"):
390
flask.abort(401)
391
if flask.request.method == "GET":
392
page_number = flask.request.args.get("page", 1, type=int)
393
if flask.session.get("username"):
394
default_page_length = db.session.get(User, flask.session.get(
395
"username")).default_page_length
396
else:
397
default_page_length = 16
398
page_length = flask.request.args.get("per_page", default_page_length, type=int)
399
400
results = UserNotification.query.filter_by(
401
user_username=flask.session.get("username")).order_by(UserNotification.id.desc()).paginate(
402
page=page_number, per_page=page_length)
403
404
if results.has_next:
405
next_page = results.next_num
406
else:
407
next_page = None
408
409
if results.has_prev:
410
prev_page = results.prev_num
411
else:
412
prev_page = None
413
414
return flask.render_template("notifications.html",
415
notifications=results,
416
db=db, Commit=Commit, Post=Post, PullRequest=PullRequest, User=User,
417
page_number=page_number,
418
page_length=page_length,
419
next_page=next_page,
420
prev_page=prev_page,
421
num_pages=results.pages
422
)
423
424
425
@app.route("/notifications/<int:notification_id>/read", methods=["POST"])
426
def mark_read(notification_id):
427
if not flask.session.get("username"):
428
flask.abort(401)
429
notification = UserNotification.query.filter_by(id=notification_id).first()
430
if notification.user_username != flask.session.get("username"):
431
flask.abort(403)
432
notification.mark_read()
433
db.session.commit()
434
return flask.render_template_string(
435
"<button hx-post='/notifications/{{ notification.id }}/unread' hx-swap='outerHTML'>Mark as unread</button>",
436
notification=notification), 200
437
438
439
@app.route("/notifications/<int:notification_id>/unread", methods=["POST"])
440
def mark_unread(notification_id):
441
if not flask.session.get("username"):
442
flask.abort(401)
443
notification = UserNotification.query.filter_by(id=notification_id).first()
444
if notification.user_username != flask.session.get("username"):
445
flask.abort(403)
446
notification.mark_unread()
447
db.session.commit()
448
return flask.render_template_string(
449
"<button hx-post='/notifications/{{ notification.id }}/read' hx-swap='outerHTML'>Mark as read</button>",
450
notification=notification), 200
451
452
453
@app.route("/notifications/mark-all-read", methods=["POST"])
454
def mark_all_read():
455
if not flask.session.get("username"):
456
flask.abort(401)
457
458
notifications = UserNotification.query.filter_by(
459
user_username=flask.session.get("username"))
460
for notification in notifications:
461
notification.mark_read()
462
db.session.commit()
463
return flask.redirect("/notifications/", code=303)
464
465
466
@app.route("/accounts/", methods=["GET", "POST"])
467
def login():
468
if flask.request.method == "GET":
469
return flask.render_template("login.html")
470
else:
471
if "login" in flask.request.form:
472
username = flask.request.form["username"]
473
password = flask.request.form["password"]
474
475
user = User.query.filter_by(username=username).first()
476
477
if user and bcrypt.check_password_hash(user.password_hashed, password):
478
flask.session["username"] = user.username
479
flask.flash(
480
Markup("<iconify-icon icon='mdi:account'></iconify-icon>" + _(
481
"Successfully logged in as {username}").format(
482
username=username)),
483
category="success")
484
return flask.redirect("/", code=303)
485
elif not user:
486
flask.flash(Markup(
487
"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _(
488
"User not found")),
489
category="alert")
490
return flask.render_template("login.html")
491
else:
492
flask.flash(Markup(
493
"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _(
494
"Invalid password")),
495
category="error")
496
return flask.render_template("login.html")
497
if "signup" in flask.request.form:
498
username = flask.request.form["username"]
499
password = flask.request.form["password"]
500
password2 = flask.request.form["password2"]
501
email = flask.request.form.get("email")
502
email2 = flask.request.form.get("email2") # repeat email is a honeypot
503
name = flask.request.form.get("name")
504
505
if not only_chars(username,
506
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"):
507
flask.flash(Markup(
508
_("Usernames may only contain Latin alphabet, numbers and '-'")),
509
category="error")
510
return flask.render_template("login.html")
511
if "--" in username:
512
flask.flash(Markup(
513
_("Usernames may not contain consecutive hyphens")),
514
category="error")
515
return flask.render_template("login.html")
516
if username.startswith("-") or username.endswith("-"):
517
flask.flash(Markup(
518
_("Usernames may not start or end with a hyphen")),
519
category="error")
520
return flask.render_template("login.html")
521
if username in config.RESERVED_NAMES:
522
flask.flash(
523
Markup(
524
_("Sorry, {username} is a system path").format(
525
username=username)),
526
category="error")
527
return flask.render_template("login.html")
528
529
if not username.islower():
530
if not name: # infer display name from the wanted username if not customised
531
display_name = username
532
username = username.lower()
533
flask.flash(Markup(
534
_("Usernames must be lowercase, so it's been converted automatically")),
535
category="info")
536
537
user_check = User.query.filter_by(username=username).first()
538
if user_check or email2: # make the honeypot look like a normal error
539
flask.flash(
540
Markup(
541
_(
542
"The username {username} is taken").format(
543
username=username)),
544
category="error")
545
return flask.render_template("login.html")
546
547
if password2 != password:
548
flask.flash(Markup(_(
549
"Make sure the passwords match")),
550
category="error")
551
return flask.render_template("login.html")
552
553
user = User(username, password, email, name)
554
db.session.add(user)
555
db.session.commit()
556
flask.session["username"] = user.username
557
flask.flash(Markup(
558
_(
559
"Successfully created and logged in as {username}").format(
560
username=username)),
561
category="success")
562
563
notification = Notification({"type": "welcome"})
564
db.session.add(notification)
565
db.session.commit()
566
567
return flask.redirect("/", code=303)
568
569
570
@app.route("/newrepo/", methods=["GET", "POST"])
571
def new_repo():
572
if not flask.session.get("username"):
573
flask.abort(401)
574
if flask.request.method == "GET":
575
return flask.render_template("new-repo.html")
576
else:
577
name = flask.request.form["name"]
578
visibility = int(flask.request.form["visibility"])
579
580
if not only_chars(name,
581
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."):
582
flask.flash(Markup(
583
"<iconify-icon icon='mdi:error'></iconify-icon>" + _(
584
"Repository names may only contain Latin alphabet, numbers, '-', '_' and '.'")),
585
category="error")
586
return flask.render_template("new-repo.html")
587
588
username = flask.request.form["account"]
589
if not username:
590
username = flask.session.get("username")
591
if username != flask.session.get("username"):
592
trust_relationship = UserTrust.query.filter_by(
593
host_username=username,
594
trusted_username=flask.session.get("username")
595
).first()
596
597
if not trust_relationship:
598
flask.flash(Markup(
599
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("{username} doesn't trust you").format(username=username)),
600
category="error")
601
return flask.render_template("new-repo.html")
602
elif trust_relationship.trust_level < 1:
603
flask.flash(Markup(
604
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("{username} doesn't trust you to modify").format(username=username)),
605
category="error")
606
return flask.render_template("new-repo.html")
607
608
user = User.query.filter_by(username=username).first()
609
610
repo = Repo(user, name, visibility)
611
db.session.add(repo)
612
db.session.commit()
613
614
flask.flash(Markup(_("Successfully created repository {name}").format(name=name)),
615
category="success")
616
return flask.redirect(repo.route, code=303)
617
618
619
@app.route("/logout")
620
def logout():
621
flask.session.clear()
622
flask.flash(Markup(
623
"<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")),
624
category="info")
625
return flask.redirect("/", code=303)
626
627
628
@app.route("/<username>/", methods=["GET", "POST"])
629
def user_profile(username):
630
if db.session.get(User, username) is None:
631
flask.abort(404)
632
old_relationship = UserFollow.query.filter_by(
633
follower_username=flask.session.get("username"),
634
followed_username=username).first()
635
if flask.request.method == "GET":
636
user = User.query.filter_by(username=username).first()
637
match flask.request.args.get("action"):
638
case "repositories":
639
repos = Repo.query.filter_by(owner_name=username, visibility=2)
640
return flask.render_template("user-profile-repositories.html", user=user,
641
repos=repos,
642
relationship=old_relationship)
643
case "followers":
644
return flask.render_template("user-profile-followers.html", user=user,
645
relationship=old_relationship)
646
case "follows":
647
return flask.render_template("user-profile-follows.html", user=user,
648
relationship=old_relationship)
649
case "organisation":
650
return flask.render_template("user-profile-organisation.html", user=user,
651
relationship=old_relationship)
652
case "trust":
653
return flask.render_template("user-profile-trust.html", user=user,
654
relationship=old_relationship)
655
case _:
656
return flask.render_template("user-profile-overview.html", user=user,
657
relationship=old_relationship)
658
659
elif flask.request.method == "POST":
660
match flask.request.args.get("action"):
661
case "follow":
662
if username == flask.session.get("username"):
663
flask.abort(403)
664
if old_relationship:
665
db.session.delete(old_relationship)
666
else:
667
relationship = UserFollow(
668
flask.session.get("username"),
669
username
670
)
671
print(f"Following {username}")
672
db.session.add(relationship)
673
674
user = db.session.get(User, username)
675
author = db.session.get(User, flask.session.get("username"))
676
notification = Notification({"type": "follow", "author": author.username, "user": user.username})
677
db.session.add(notification)
678
db.session.commit()
679
user_notification = UserNotification(user, notification, 1)
680
db.session.add(user_notification)
681
db.session.commit()
682
683
db.session.commit()
684
return flask.redirect("?", code=303)
685
case "trust-confirm":
686
trust_level = int(flask.request.form["trust-level"])
687
existing_trust = UserTrust.query.filter_by(
688
host_username=flask.session.get("username"),
689
trusted_username=username).first()
690
password = flask.request.form.get("password")
691
if not bcrypt.check_password_hash(db.session.get(User, flask.session.get("username")).password_hashed,
692
password):
693
flask.flash(Markup(_("Invalid password")), category="error")
694
return flask.redirect("?action=trust", code=303)
695
if existing_trust:
696
existing_trust.cancel()
697
if 0 <= trust_level <= 2:
698
trust = UserTrust(db.session.get(User, flask.session.get("username")), db.session.get(User, username),
699
trust_level)
700
db.session.add(trust)
701
elif trust_level == -1:
702
existing_trust.cancel()
703
db.session.commit()
704
705
return flask.redirect("?", code=303)
706
707
708
@app.route("/<username>/<repository>/")
709
def repository_index(username, repository):
710
return flask.redirect("./tree", code=302)
711
712
713
@app.route("/info/<username>/avatar")
714
def user_avatar(username):
715
server_userdata_location = os.path.join(config.USERDATA_PATH, username)
716
if not os.path.exists(server_userdata_location):
717
return flask.render_template("errors/not-found.html"), 404
718
719
return flask.send_from_directory(server_userdata_location, "avatar.png")
720
721
722
@app.route("/info/<username>/avatar", methods=["POST"])
723
def user_avatar_upload(username):
724
server_userdata_location = os.path.join(config.USERDATA_PATH, username)
725
726
if not os.path.exists(server_userdata_location):
727
flask.abort(404)
728
if not flask.session.get("username") == username:
729
flask.abort(403)
730
731
# Convert image to PNG
732
try:
733
image = Image.open(flask.request.files["avatar"])
734
except PIL.UnidentifiedImageError:
735
flask.abort(400)
736
image.save(os.path.join(server_userdata_location, "avatar.png"))
737
738
return flask.redirect(f"/{username}", code=303)
739
740
741
@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>")
742
def repository_raw(username, repository, branch, subpath):
743
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
744
if not os.path.exists(server_repo_location):
745
flask.abort(404)
746
if not (get_visibility(username, repository) or get_permission_level(
747
flask.session.get("username"), username,
748
repository) is not None):
749
flask.abort(403)
750
751
if not os.path.exists(server_repo_location):
752
return flask.render_template("errors/not-found.html"), 404
753
754
repo = git.Repo(server_repo_location)
755
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
756
if not repo_data.default_branch:
757
if repo.heads:
758
repo_data.default_branch = repo.heads[0].name
759
else:
760
return flask.render_template("empty.html",
761
remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
762
if not branch:
763
branch = repo_data.default_branch
764
return flask.redirect(f"./{branch}", code=302)
765
766
if branch.startswith("tag:"):
767
ref = f"tags/{branch[4:]}"
768
elif branch.startswith("~"):
769
ref = branch[1:]
770
else:
771
ref = f"heads/{branch}"
772
773
ref = ref.replace("~", "/") # encode slashes for URL support
774
775
try:
776
repo.git.checkout("-f", ref)
777
except git.exc.GitCommandError:
778
return flask.render_template("errors/not-found.html"), 404
779
780
response = flask.send_from_directory(config.REPOS_PATH,
781
os.path.join(username, repository, subpath))
782
783
if repo_data.visibility < 2:
784
response.headers["X-Robots-Tag"] = "noindex"
785
786
return response
787
788
789
@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""})
790
@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""})
791
@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>")
792
def repository_tree(username, repository, branch, subpath):
793
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
794
if not os.path.exists(server_repo_location):
795
flask.abort(404)
796
if not (get_visibility(username, repository) or get_permission_level(
797
flask.session.get("username"), username,
798
repository) is not None):
799
flask.abort(403)
800
801
repo = git.Repo(server_repo_location)
802
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
803
if not repo_data.default_branch:
804
if repo.heads:
805
repo_data.default_branch = repo.heads[0].name
806
else:
807
return flask.render_template("empty.html",
808
remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
809
if not branch:
810
branch = repo_data.default_branch
811
return flask.redirect(f"./{branch}", code=302)
812
813
if branch.startswith("tag:"):
814
ref = f"tags/{branch[4:]}"
815
elif branch.startswith("~"):
816
ref = branch[1:]
817
else:
818
ref = f"heads/{branch}"
819
820
ref = ref.replace("~", "/") # encode slashes for URL support
821
822
try:
823
repo.git.checkout("-f", ref)
824
except git.exc.GitCommandError:
825
return flask.render_template("errors/not-found.html"), 404
826
827
branches = repo.heads
828
829
all_refs = []
830
for ref in repo.heads:
831
all_refs.append((ref, "head"))
832
for ref in repo.tags:
833
all_refs.append((ref, "tag"))
834
835
if os.path.isdir(os.path.join(server_repo_location, subpath)):
836
files = []
837
blobs = []
838
839
for entry in os.listdir(os.path.join(server_repo_location, subpath)):
840
if not os.path.basename(entry) == ".git":
841
files.append(os.path.join(subpath, entry))
842
843
infos = []
844
845
for file in files:
846
path = os.path.join(server_repo_location, file)
847
mimetype = guess_mime(path)
848
849
text = git_command(server_repo_location, None, "log", "--format='%H\n'",
850
shlex.quote(file)).decode()
851
852
sha = text.split("\n")[0]
853
identifier = f"/{username}/{repository}/{sha}"
854
855
last_commit = db.session.get(Commit, identifier)
856
857
quantity, unit = human_size(os.path.getsize(path))
858
859
info = {
860
"name": os.path.basename(file),
861
"serverPath": path,
862
"relativePath": file,
863
"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file),
864
"size": str(quantity) + "\u202f" + unit, # nnbsp
865
"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}",
866
"commit": last_commit,
867
"shaSize": 7,
868
}
869
870
special_icon = config.match_icon(os.path.basename(file))
871
if special_icon:
872
info["icon"] = special_icon
873
elif os.path.isdir(path):
874
info["icon"] = config.folder_icon
875
info["size"] = ngettext("%(num)d file", "%(num)d files", len(os.listdir(path)))
876
elif mimetypes.guess_type(path)[0] in config.file_icons:
877
info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]]
878
else:
879
info["icon"] = config.unknown_icon
880
881
if os.path.isdir(path):
882
infos.insert(0, info)
883
else:
884
infos.append(info)
885
886
return flask.render_template(
887
"repo-tree.html",
888
username=username,
889
repository=repository,
890
files=infos,
891
subpath=os.path.join("/", subpath),
892
branches=all_refs,
893
current=branch,
894
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
895
is_favourite=get_favourite(flask.session.get("username"), username, repository),
896
repo_data=repo_data,
897
)
898
else:
899
path = os.path.join(server_repo_location, subpath)
900
901
if not os.path.exists(path):
902
return flask.render_template("errors/not-found.html"), 404
903
904
mimetype = guess_mime(path)
905
mode = mimetype.split("/", 1)[0]
906
size = human_size(os.path.getsize(path))
907
908
special_icon = config.match_icon(os.path.basename(path))
909
if special_icon:
910
icon = special_icon
911
elif os.path.isdir(path):
912
icon = config.folder_icon
913
elif mimetypes.guess_type(path)[0] in config.file_icons:
914
icon = config.file_icons[mimetypes.guess_type(path)[0]]
915
else:
916
icon = config.unknown_icon
917
918
contents = None
919
if mode == "text":
920
contents = convert_to_html(path)
921
922
return flask.render_template(
923
"repo-file.html",
924
username=username,
925
repository=repository,
926
file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath),
927
branches=all_refs,
928
current=branch,
929
mode=mode,
930
mimetype=mimetype,
931
detailedtype=magic.from_file(path),
932
size=size,
933
icon=icon,
934
subpath=os.path.join("/", subpath),
935
extension=pathlib.Path(path).suffix,
936
basename=os.path.basename(path),
937
contents=contents,
938
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
939
is_favourite=get_favourite(flask.session.get("username"), username, repository),
940
repo_data=repo_data,
941
)
942
943
944
@repositories.route("/<username>/<repository>/commit/<sha>")
945
def repository_commit(username, repository, sha):
946
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
947
if not os.path.exists(server_repo_location):
948
flask.abort(404)
949
if not (get_visibility(username, repository) or get_permission_level(
950
flask.session.get("username"), username,
951
repository) is not None):
952
flask.abort(403)
953
954
if not os.path.exists(server_repo_location):
955
return flask.render_template("errors/not-found.html"), 404
956
957
repo = git.Repo(server_repo_location)
958
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
959
960
files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r",
961
"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1]
962
963
return flask.render_template(
964
"repo-commit.html",
965
username=username,
966
repository=repository,
967
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
968
is_favourite=get_favourite(flask.session.get("username"), username, repository),
969
diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff",
970
str(sha) + "^!", "--", file).decode().split("\n") for
971
file in files},
972
data=db.session.get(Commit, f"/{username}/{repository}/{sha}"),
973
repo_data=repo_data,
974
comment_query=Comment.query,
975
permission_level=get_permission_level(flask.session.get("username"), username, repository),
976
)
977
978
979
@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"])
980
def repository_commit_add_comment(username, repository, sha):
981
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
982
if not os.path.exists(server_repo_location):
983
flask.abort(404)
984
if not (get_visibility(username, repository) or get_permission_level(
985
flask.session.get("username"), username,
986
repository) is not None):
987
flask.abort(403)
988
989
comment = Comment(
990
db.session.get(User, flask.session.get("username")),
991
db.session.get(Repo, f"/{username}/{repository}"),
992
db.session.get(Commit, f"/{username}/{repository}/{sha}"),
993
flask.request.form["comment"],
994
flask.request.form["file"],
995
flask.request.form["line"],
996
)
997
998
db.session.add(comment)
999
db.session.commit()
1000
1001
return flask.redirect(
1002
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
1003
code=303
1004
)
1005
1006
1007
@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"])
1008
def repository_commit_delete_comment(username, repository, sha, id):
1009
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1010
comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first()
1011
commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first()
1012
if (
1013
comment.owner.username == flask.session.get("username")
1014
or get_permission_level(flask.session.get("username"), username, repository) >= 2
1015
or comment.commit.owner_name == flask.session.get("username")
1016
):
1017
db.session.delete(comment)
1018
db.session.commit()
1019
1020
return flask.redirect(
1021
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
1022
code=303
1023
)
1024
1025
1026
@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"])
1027
def repository_commit_resolve_comment(username, repository, sha, id):
1028
comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first()
1029
if (
1030
comment.commit.owner_name == flask.session.get("username")
1031
or get_permission_level(flask.session.get("username"), username, repository) >= 2
1032
or comment.owner.username == flask.session.get("username")
1033
):
1034
comment.state = int(not comment.state)
1035
db.session.commit()
1036
1037
return flask.redirect(
1038
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
1039
code=303
1040
)
1041
1042
1043
@repositories.route("/<username>/<repository>/forum/")
1044
def repository_forum(username, repository):
1045
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1046
if not os.path.exists(server_repo_location):
1047
flask.abort(404)
1048
if not (get_visibility(username, repository) or get_permission_level(
1049
flask.session.get("username"), username,
1050
repository) is not None):
1051
flask.abort(403)
1052
1053
repo = git.Repo(server_repo_location)
1054
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1055
user = User.query.filter_by(username=flask.session.get("username")).first()
1056
relationships = RepoAccess.query.filter_by(repo=repo_data)
1057
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1058
1059
page_number = flask.request.args.get("page", 1, type=int)
1060
if flask.session.get("username"):
1061
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1062
else:
1063
default_page_length = 16
1064
1065
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1066
1067
posts = Post.query.filter_by(repo=repo_data, parent=None).order_by(Post.last_updated.desc()).paginate(
1068
page=page_number, per_page=page_length
1069
)
1070
1071
if posts.has_next:
1072
next_page = posts.next_num
1073
else:
1074
next_page = None
1075
1076
if posts.has_prev:
1077
prev_page = posts.prev_num
1078
else:
1079
prev_page = None
1080
1081
return flask.render_template(
1082
"repo-forum.html",
1083
username=username,
1084
repository=repository,
1085
repo_data=repo_data,
1086
relationships=relationships,
1087
repo=repo,
1088
user_relationship=user_relationship,
1089
Post=Post,
1090
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1091
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1092
default_branch=repo_data.default_branch,
1093
page_number=page_number,
1094
page_length=page_length,
1095
next_page=next_page,
1096
prev_page=prev_page,
1097
num_pages=posts.pages,
1098
posts=posts
1099
)
1100
1101
1102
@repositories.route("/<username>/<repository>/forum/search")
1103
def repository_forum_search(username, repository):
1104
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1105
if not os.path.exists(server_repo_location):
1106
flask.abort(404)
1107
if not (get_visibility(username, repository) or get_permission_level(
1108
flask.session.get("username"), username,
1109
repository) is not None):
1110
flask.abort(403)
1111
1112
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1113
user = User.query.filter_by(username=flask.session.get("username")).first()
1114
relationships = RepoAccess.query.filter_by(repo=repo_data)
1115
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1116
1117
query = flask.request.args.get("q")
1118
1119
page_number = flask.request.args.get("page", 1, type=int)
1120
if flask.session.get("username"):
1121
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1122
else:
1123
default_page_length = 16
1124
1125
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1126
1127
all_posts = Post.query.filter(Post.repo == repo_data)
1128
1129
results = (all_posts
1130
.filter(Post.subject.ilike(f"%{query}%") | Post.message.ilike(f"%{query}%"))
1131
.order_by(Post.last_updated.desc()))
1132
1133
if flask.request.args.get("state"):
1134
try:
1135
results = results.filter(Post.state == int(flask.request.args.get("state")))
1136
except ValueError:
1137
pass # if state is not an integer, ignore it
1138
1139
if flask.request.args.get("label"):
1140
results = results.filter(Post.labels.any(Label.identifier == flask.request.args.get("label")))
1141
1142
results = results.paginate(page=page_number, per_page=page_length)
1143
1144
if results.has_next:
1145
next_page = results.next_num
1146
else:
1147
next_page = None
1148
1149
if results.has_prev:
1150
prev_page = results.prev_num
1151
else:
1152
prev_page = None
1153
1154
return flask.render_template(
1155
"repo-forum-search.html",
1156
username=username,
1157
repository=repository,
1158
repo_data=repo_data,
1159
relationships=relationships,
1160
user_relationship=user_relationship,
1161
query=query,
1162
results=results,
1163
Post=Post,
1164
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1165
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1166
default_branch=repo_data.default_branch,
1167
page_number=page_number,
1168
page_length=page_length,
1169
next_page=next_page,
1170
prev_page=prev_page,
1171
num_pages=results.pages,
1172
require_state=flask.request.args.get("state"),
1173
require_label=flask.request.args.get("label"),
1174
)
1175
1176
1177
@repositories.route("/<username>/<repository>/forum/topic/<int:id>")
1178
def repository_forum_topic(username, repository, id):
1179
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1180
if not os.path.exists(server_repo_location):
1181
flask.abort(404)
1182
if not (get_visibility(username, repository) or get_permission_level(
1183
flask.session.get("username"), username,
1184
repository) is not None):
1185
flask.abort(403)
1186
1187
if not os.path.exists(server_repo_location):
1188
return flask.render_template("errors/not-found.html"), 404
1189
1190
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1191
user = User.query.filter_by(username=flask.session.get("username")).first()
1192
relationships = RepoAccess.query.filter_by(repo=repo_data)
1193
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1194
1195
post = Post.query.filter_by(id=id).first()
1196
1197
return flask.render_template(
1198
"repo-topic.html",
1199
username=username,
1200
repository=repository,
1201
repo_data=repo_data,
1202
relationships=relationships,
1203
user_relationship=user_relationship,
1204
post=post,
1205
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1206
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1207
default_branch=repo_data.default_branch
1208
)
1209
1210
1211
@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"])
1212
def repository_forum_new(username, repository):
1213
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1214
if not os.path.exists(server_repo_location):
1215
flask.abort(404)
1216
if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level(
1217
flask.session.get("username"), username,
1218
repository) is not None):
1219
flask.abort(403)
1220
1221
if not os.path.exists(server_repo_location):
1222
return flask.render_template("errors/not-found.html"), 404
1223
1224
repo = git.Repo(server_repo_location)
1225
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1226
user = User.query.filter_by(username=flask.session.get("username")).first()
1227
relationships = RepoAccess.query.filter_by(repo=repo_data)
1228
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1229
1230
post = Post(user, repo_data, None, flask.request.form["subject"],
1231
flask.request.form["message"])
1232
1233
db.session.add(post)
1234
db.session.commit()
1235
1236
return flask.redirect(
1237
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1238
post_id=post.number),
1239
code=303)
1240
1241
1242
@repositories.route("/<username>/<repository>/forum/<int:post_id>")
1243
def repository_forum_thread(username, repository, post_id):
1244
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1245
if not os.path.exists(server_repo_location):
1246
flask.abort(404)
1247
if not (get_visibility(username, repository) or get_permission_level(
1248
flask.session.get("username"), username,
1249
repository) is not None):
1250
flask.abort(403)
1251
1252
if not os.path.exists(server_repo_location):
1253
return flask.render_template("errors/not-found.html"), 404
1254
1255
repo = git.Repo(server_repo_location)
1256
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1257
user = User.query.filter_by(username=flask.session.get("username")).first()
1258
relationships = RepoAccess.query.filter_by(repo=repo_data)
1259
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1260
1261
if user:
1262
max_post_nesting = user.max_post_nesting
1263
else:
1264
max_post_nesting = 2
1265
1266
return flask.render_template(
1267
"repo-forum-thread.html",
1268
username=username,
1269
repository=repository,
1270
repo_data=repo_data,
1271
relationships=relationships,
1272
repo=repo,
1273
Post=Post,
1274
user_relationship=user_relationship,
1275
post_id=post_id,
1276
max_post_nesting=max_post_nesting,
1277
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1278
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1279
parent=Post.query.filter_by(repo=repo_data, number=post_id).first(),
1280
has_permission=not ((not get_permission_level(flask.session.get("username"), username,
1281
repository)) and db.session.get(Post,
1282
f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")),
1283
)
1284
1285
1286
@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state",
1287
methods=["POST"])
1288
def repository_forum_change_state(username, repository, post_id):
1289
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1290
if not os.path.exists(server_repo_location):
1291
flask.abort(404)
1292
if (not get_permission_level(flask.session.get("username"), username, repository)) and db.session.get(Post, f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username"):
1293
flask.abort(403)
1294
1295
repo = git.Repo(server_repo_location)
1296
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1297
user = User.query.filter_by(username=flask.session.get("username")).first()
1298
relationships = RepoAccess.query.filter_by(repo=repo_data)
1299
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1300
1301
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1302
1303
if not post:
1304
flask.abort(404)
1305
if post.parent:
1306
flask.abort(400)
1307
1308
post.state = int(flask.request.form["new-state"])
1309
1310
db.session.commit()
1311
1312
return flask.redirect(
1313
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1314
post_id=post_id),
1315
code=303)
1316
1317
1318
@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"])
1319
def repository_forum_reply(username, repository, post_id):
1320
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1321
if not os.path.exists(server_repo_location):
1322
flask.abort(404)
1323
if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level(
1324
flask.session.get("username"), username,
1325
repository) is not None):
1326
flask.abort(403)
1327
1328
if not os.path.exists(server_repo_location):
1329
return flask.render_template("errors/not-found.html"), 404
1330
1331
repo = git.Repo(server_repo_location)
1332
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1333
user = User.query.filter_by(username=flask.session.get("username")).first()
1334
relationships = RepoAccess.query.filter_by(repo=repo_data)
1335
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1336
if not user:
1337
flask.abort(401)
1338
1339
parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1340
post = Post(user, repo_data, parent, flask.request.form["subject"],
1341
flask.request.form["message"])
1342
1343
db.session.add(post)
1344
post.update_date()
1345
db.session.commit()
1346
1347
return flask.redirect(
1348
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1349
post_id=post_id),
1350
code=303)
1351
1352
1353
@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["POST"])
1354
def repository_forum_edit(username, repository, post_id):
1355
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1356
if not os.path.exists(server_repo_location):
1357
flask.abort(404)
1358
if not (get_visibility(username, repository) or get_permission_level(
1359
flask.session.get("username"), username,
1360
repository) is not None):
1361
flask.abort(403)
1362
1363
if not os.path.exists(server_repo_location):
1364
return flask.render_template("errors/not-found.html"), 404
1365
1366
repo = git.Repo(server_repo_location)
1367
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1368
user = User.query.filter_by(username=flask.session.get("username")).first()
1369
relationships = RepoAccess.query.filter_by(repo=repo_data)
1370
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1371
if not user:
1372
flask.abort(401)
1373
post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
1374
if user != post.owner:
1375
flask.abort(403)
1376
1377
post.subject = flask.request.form["subject"]
1378
post.message = flask.request.form["message"]
1379
post.html = markdown.markdown2html(post.message).prettify()
1380
post.update_date()
1381
db.session.commit()
1382
1383
return flask.redirect(
1384
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1385
post_id=post_id),
1386
code=303)
1387
1388
1389
@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["GET"])
1390
def repository_forum_edit_form(username, repository, post_id):
1391
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1392
if not os.path.exists(server_repo_location):
1393
flask.abort(404)
1394
if not (get_visibility(username, repository) or get_permission_level(
1395
flask.session.get("username"), username,
1396
repository) is not None):
1397
flask.abort(403)
1398
1399
if not os.path.exists(server_repo_location):
1400
return flask.render_template("errors/not-found.html"), 404
1401
1402
repo = git.Repo(server_repo_location)
1403
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1404
user = User.query.filter_by(username=flask.session.get("username")).first()
1405
relationships = RepoAccess.query.filter_by(repo=repo_data)
1406
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1407
if not user:
1408
flask.abort(401)
1409
post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
1410
if user != post.owner:
1411
flask.abort(403)
1412
1413
return flask.render_template(
1414
"repo-forum-edit.html",
1415
username=username,
1416
repository=repository,
1417
repo_data=repo_data,
1418
relationships=relationships,
1419
repo=repo,
1420
user_relationship=user_relationship,
1421
post=post,
1422
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1423
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1424
default_branch=repo_data.default_branch
1425
)
1426
1427
@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup",
1428
defaults={"score": 1})
1429
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown",
1430
defaults={"score": -1})
1431
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0})
1432
def repository_forum_vote(username, repository, post_id, score):
1433
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1434
if not os.path.exists(server_repo_location):
1435
flask.abort(404)
1436
if not (get_visibility(username, repository) or get_permission_level(
1437
flask.session.get("username"), username,
1438
repository) is not None):
1439
flask.abort(403)
1440
1441
if not os.path.exists(server_repo_location):
1442
return flask.render_template("errors/not-found.html"), 404
1443
1444
repo = git.Repo(server_repo_location)
1445
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1446
user = User.query.filter_by(username=flask.session.get("username")).first()
1447
relationships = RepoAccess.query.filter_by(repo=repo_data)
1448
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1449
if not user:
1450
flask.abort(401)
1451
1452
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1453
1454
if score:
1455
old_relationship = PostVote.query.filter_by(user_username=user.username,
1456
post_identifier=post.identifier).first()
1457
if old_relationship:
1458
if score == old_relationship.vote_score:
1459
db.session.delete(old_relationship)
1460
post.vote_sum -= old_relationship.vote_score
1461
else:
1462
post.vote_sum -= old_relationship.vote_score
1463
post.vote_sum += score
1464
old_relationship.vote_score = score
1465
else:
1466
relationship = PostVote(user, post, score)
1467
post.vote_sum += score
1468
db.session.add(relationship)
1469
1470
db.session.commit()
1471
1472
user_vote = PostVote.query.filter_by(user_username=user.username,
1473
post_identifier=post.identifier).first()
1474
response = flask.make_response(
1475
str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0))
1476
response.content_type = "text/plain"
1477
1478
return response
1479
1480
1481
@repositories.route("/<username>/<repository>/forum/<int:post_id>/label", methods=["POST"])
1482
def repository_forum_label(username, repository, post_id):
1483
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1484
if not os.path.exists(server_repo_location):
1485
flask.abort(404)
1486
if not get_permission_level(flask.session.get("username"), username, repository):
1487
flask.abort(403)
1488
1489
repo = git.Repo(server_repo_location)
1490
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1491
user = User.query.filter_by(username=flask.session.get("username")).first()
1492
relationships = RepoAccess.query.filter_by(repo=repo_data)
1493
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1494
1495
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1496
1497
if not post:
1498
flask.abort(404)
1499
if post.parent:
1500
flask.abort(400)
1501
1502
label = db.session.get(Label, flask.request.form["label"])
1503
1504
if PostLabel.query.filter_by(post=post, label=label).first():
1505
return flask.redirect(
1506
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1507
post_id=post_id),
1508
code=303)
1509
1510
post_label = PostLabel(post, label)
1511
db.session.add(post_label)
1512
1513
db.session.commit()
1514
1515
return flask.redirect(
1516
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1517
post_id=post_id),
1518
code=303)
1519
1520
1521
@repositories.route("/<username>/<repository>/forum/<int:post_id>/remove-label")
1522
def repository_forum_remove_label(username, repository, post_id):
1523
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1524
if not os.path.exists(server_repo_location):
1525
flask.abort(404)
1526
if not get_permission_level(flask.session.get("username"), username, repository):
1527
flask.abort(403)
1528
1529
repo = git.Repo(server_repo_location)
1530
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1531
user = User.query.filter_by(username=flask.session.get("username")).first()
1532
relationships = RepoAccess.query.filter_by(repo=repo_data)
1533
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1534
1535
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1536
1537
if not post:
1538
flask.abort(404)
1539
if post.parent:
1540
flask.abort(400)
1541
1542
label = db.session.get(Label, flask.request.args["label"])
1543
1544
post_label = PostLabel.query.filter_by(post=post, label=label).first()
1545
db.session.delete(post_label)
1546
1547
db.session.commit()
1548
1549
return flask.redirect(
1550
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1551
post_id=post_id),
1552
code=303)
1553
1554
1555
@repositories.route("/<username>/<repository>/favourite")
1556
def repository_favourite(username, repository):
1557
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1558
if not os.path.exists(server_repo_location):
1559
flask.abort(404)
1560
if not (get_visibility(username, repository) or get_permission_level(
1561
flask.session.get("username"), username,
1562
repository) is not None):
1563
flask.abort(403)
1564
1565
if not os.path.exists(server_repo_location):
1566
return flask.render_template("errors/not-found.html"), 404
1567
1568
repo = git.Repo(server_repo_location)
1569
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1570
user = User.query.filter_by(username=flask.session.get("username")).first()
1571
relationships = RepoAccess.query.filter_by(repo=repo_data)
1572
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1573
if not user:
1574
flask.abort(401)
1575
1576
old_relationship = RepoFavourite.query.filter_by(user_username=user.username,
1577
repo_route=repo_data.route).first()
1578
if old_relationship:
1579
db.session.delete(old_relationship)
1580
else:
1581
relationship = RepoFavourite(user, repo_data)
1582
db.session.add(relationship)
1583
1584
db.session.commit()
1585
1586
return flask.redirect(flask.url_for("favourites"), code=303)
1587
1588
1589
@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"])
1590
def repository_users(username, repository):
1591
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1592
if not os.path.exists(server_repo_location):
1593
flask.abort(404)
1594
if not (get_visibility(username, repository) or get_permission_level(
1595
flask.session.get("username"), username,
1596
repository) is not None):
1597
flask.abort(403)
1598
1599
if not os.path.exists(server_repo_location):
1600
return flask.render_template("errors/not-found.html"), 404
1601
1602
repo = git.Repo(server_repo_location)
1603
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1604
user = User.query.filter_by(username=flask.session.get("username")).first()
1605
relationships = RepoAccess.query.filter_by(repo=repo_data)
1606
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1607
1608
if flask.request.method == "GET":
1609
return flask.render_template(
1610
"repo-users.html",
1611
username=username,
1612
repository=repository,
1613
repo_data=repo_data,
1614
relationships=relationships,
1615
repo=repo,
1616
user_relationship=user_relationship,
1617
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1618
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1619
)
1620
else:
1621
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1622
flask.abort(401)
1623
1624
if flask.request.form.get("new-username"):
1625
# Create new relationship
1626
new_user = User.query.filter_by(
1627
username=flask.request.form.get("new-username")).first()
1628
relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level"))
1629
db.session.add(relationship)
1630
db.session.commit()
1631
if flask.request.form.get("update-username"):
1632
# Create new relationship
1633
updated_user = User.query.filter_by(
1634
username=flask.request.form.get("update-username")).first()
1635
relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first()
1636
if relationship.automatic:
1637
flask.abort(400)
1638
if flask.request.form.get("update-level") == -1:
1639
relationship.delete()
1640
else:
1641
relationship.access_level = flask.request.form.get("update-level")
1642
db.session.commit()
1643
1644
return flask.redirect(
1645
app.url_for(".repository_users", username=username, repository=repository))
1646
1647
1648
@repositories.route("/<username>/<repository>/branches/")
1649
def repository_branches(username, repository):
1650
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1651
if not os.path.exists(server_repo_location):
1652
flask.abort(404)
1653
if not (get_visibility(username, repository) or get_permission_level(
1654
flask.session.get("username"), username,
1655
repository) is not None):
1656
flask.abort(403)
1657
1658
if not os.path.exists(server_repo_location):
1659
return flask.render_template("errors/not-found.html"), 404
1660
1661
repo = git.Repo(server_repo_location)
1662
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1663
1664
return flask.render_template(
1665
"repo-branches.html",
1666
username=username,
1667
repository=repository,
1668
repo_data=repo_data,
1669
repo=repo,
1670
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1671
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1672
)
1673
1674
1675
@repositories.route("/<username>/<repository>/log/", defaults={"branch": None})
1676
@repositories.route("/<username>/<repository>/log/<branch>/")
1677
def repository_log(username, repository, branch):
1678
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1679
if not os.path.exists(server_repo_location):
1680
flask.abort(404)
1681
if not (get_visibility(username, repository) or get_permission_level(
1682
flask.session.get("username"), username,
1683
repository) is not None):
1684
flask.abort(403)
1685
1686
if not os.path.exists(server_repo_location):
1687
return flask.render_template("errors/not-found.html"), 404
1688
1689
repo = git.Repo(server_repo_location)
1690
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1691
if not repo_data.default_branch:
1692
if repo.heads:
1693
repo_data.default_branch = repo.heads[0].name
1694
else:
1695
return flask.render_template("empty.html",
1696
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
1697
if not branch:
1698
branch = repo_data.default_branch
1699
return flask.redirect(f"./{branch}", code=302)
1700
1701
if branch.startswith("tag:"):
1702
ref = f"tags/{branch[4:]}"
1703
elif branch.startswith("~"):
1704
ref = branch[1:]
1705
else:
1706
ref = f"heads/{branch}"
1707
1708
ref = ref.replace("~", "/") # encode slashes for URL support
1709
1710
try:
1711
repo.git.checkout("-f", ref)
1712
except git.exc.GitCommandError:
1713
return flask.render_template("errors/not-found.html"), 404
1714
1715
branches = repo.heads
1716
1717
all_refs = []
1718
for ref in repo.heads:
1719
all_refs.append((ref, "head"))
1720
for ref in repo.tags:
1721
all_refs.append((ref, "tag"))
1722
1723
commit_list = [f"/{username}/{repository}/{sha}" for sha in
1724
git_command(server_repo_location, None, "log",
1725
"--format='%H'").decode().split("\n")]
1726
1727
commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc())
1728
page_number = flask.request.args.get("page", 1, type=int)
1729
if flask.session.get("username"):
1730
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1731
else:
1732
default_page_length = 16
1733
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1734
page_listing = db.paginate(commits, page=page_number, per_page=page_length)
1735
1736
if page_listing.has_next:
1737
next_page = page_listing.next_num
1738
else:
1739
next_page = None
1740
1741
if page_listing.has_prev:
1742
prev_page = page_listing.prev_num
1743
else:
1744
prev_page = None
1745
1746
return flask.render_template(
1747
"repo-log.html",
1748
username=username,
1749
repository=repository,
1750
branches=all_refs,
1751
current=branch,
1752
repo_data=repo_data,
1753
repo=repo,
1754
commits=page_listing,
1755
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1756
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1757
page_number=page_number,
1758
page_length=page_length,
1759
next_page=next_page,
1760
prev_page=prev_page,
1761
num_pages=page_listing.pages
1762
)
1763
1764
1765
@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"])
1766
def repository_prs(username, repository):
1767
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1768
if not os.path.exists(server_repo_location):
1769
flask.abort(404)
1770
if not (get_visibility(username, repository) or get_permission_level(
1771
flask.session.get("username"), username,
1772
repository) is not None):
1773
flask.abort(403)
1774
1775
if not os.path.exists(server_repo_location):
1776
return flask.render_template("errors/not-found.html"), 404
1777
1778
if flask.request.method == "GET":
1779
repo = git.Repo(server_repo_location)
1780
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1781
user = User.query.filter_by(username=flask.session.get("username")).first()
1782
1783
return flask.render_template(
1784
"repo-prs.html",
1785
username=username,
1786
repository=repository,
1787
repo_data=repo_data,
1788
repo=repo,
1789
PullRequest=PullRequest,
1790
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1791
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1792
default_branch=repo_data.default_branch,
1793
branches=repo.branches
1794
)
1795
1796
elif "id" not in flask.request.form:
1797
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1798
head = flask.request.form.get("head")
1799
head_route = flask.request.form.get("headroute")
1800
base = flask.request.form.get("base")
1801
1802
if not head and base and head_route:
1803
return flask.redirect(".", 400)
1804
1805
repo_user = head_route.rpartition("/")[0].removeprefix("/")
1806
print(repo_user)
1807
if "@" not in repo_user:
1808
# Same server; do some additional checks
1809
try:
1810
head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/")))
1811
except git.exc.NoSuchPathError:
1812
flask.flash(Markup(
1813
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Repository doesn't exist")),
1814
category="error")
1815
return flask.redirect(".", 303)
1816
base_repo = git.Repo(server_repo_location)
1817
1818
if head not in head_repo.branches or base not in base_repo.branches:
1819
flask.flash(Markup(
1820
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")),
1821
category="error")
1822
return flask.redirect(".", 303)
1823
1824
head_data = db.session.get(Repo, head_route)
1825
if not head_data.visibility:
1826
flask.flash(Markup(
1827
"<iconify-icon icon='mdi:error'></iconify-icon>" + _(
1828
"Head can't be restricted")),
1829
category="error")
1830
return flask.redirect(".", 303)
1831
1832
pull_request = PullRequest(head_route, head, repo_data, base,
1833
db.session.get(User, flask.session["username"]))
1834
1835
db.session.add(pull_request)
1836
db.session.commit()
1837
1838
# Create the notification
1839
notification = Notification({"type": "pr", "head": pull_request.head_route, "base": pull_request.base.route, "pr": pull_request.id})
1840
db.session.add(notification)
1841
db.session.commit()
1842
1843
# Send a notification to all users who have enabled PR notifications for this repo
1844
for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all():
1845
user = relationship.user
1846
user_notification = UserNotification(user, notification, 1)
1847
db.session.add(user_notification)
1848
db.session.commit()
1849
celery_tasks.send_notification.apply_async(args=[user_notification.id])
1850
1851
return flask.redirect(".", 303)
1852
else:
1853
id = flask.request.form.get("id")
1854
pull_request = db.session.get(PullRequest, id)
1855
1856
if not pull_request:
1857
flask.abort(404)
1858
1859
if not (get_visibility(username, repository) or get_permission_level(
1860
flask.session.get("username"), username,
1861
repository) >= 1 or pull_request.owner.username == flask.session.get("username")):
1862
flask.abort(403)
1863
1864
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1865
1866
if pull_request:
1867
pull_request.resolves_list = flask.request.form.get("resolves")
1868
db.session.commit()
1869
1870
return flask.redirect(".", 303)
1871
1872
1873
@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"])
1874
def repository_prs_merge(username, repository):
1875
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1876
if not os.path.exists(server_repo_location):
1877
flask.abort(404)
1878
if not (get_visibility(username, repository) or get_permission_level(
1879
flask.session.get("username"), username,
1880
repository) is not None):
1881
flask.abort(403)
1882
1883
if not get_permission_level(flask.session.get("username"), username, repository):
1884
flask.abort(401)
1885
1886
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1887
repo = git.Repo(server_repo_location)
1888
id = flask.request.form.get("id")
1889
1890
pull_request = db.session.get(PullRequest, id)
1891
1892
if pull_request.state != 0:
1893
flask.abort(400)
1894
1895
if pull_request:
1896
result = celery_tasks.merge_heads.delay(
1897
pull_request.head_route,
1898
pull_request.head_branch,
1899
pull_request.base_route,
1900
pull_request.base_branch,
1901
pull_request.id,
1902
simulate=True,
1903
method=flask.request.form.get("method"), # like merge, fast-forward, rebase
1904
username=flask.session.get("username")
1905
)
1906
task_result = worker.AsyncResult(result.id)
1907
1908
return flask.redirect(f"/task/{result.id}?pr-id={id}&method={flask.request.form.get('method')}", 303) # should be 202 Accepted but we must use a redirect
1909
# db.session.delete(pull_request)
1910
# db.session.commit()
1911
else:
1912
flask.abort(400)
1913
1914
1915
@repositories.route("/<username>/<repository>/prs/<int:id>/merge")
1916
def repository_prs_merge_stage_two(username, repository, id):
1917
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1918
if not os.path.exists(server_repo_location):
1919
flask.abort(404)
1920
if not (get_visibility(username, repository) or get_permission_level(
1921
flask.session.get("username"), username,
1922
repository) is not None):
1923
flask.abort(403)
1924
1925
if not get_permission_level(flask.session.get("username"), username, repository):
1926
flask.abort(401)
1927
1928
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1929
repo = git.Repo(server_repo_location)
1930
1931
pull_request = db.session.get(PullRequest, id)
1932
1933
if pull_request:
1934
result = celery_tasks.merge_heads.delay(
1935
pull_request.head_route,
1936
pull_request.head_branch,
1937
pull_request.base_route,
1938
pull_request.base_branch,
1939
pull_request.id,
1940
simulate=False,
1941
username=flask.session.get("username")
1942
)
1943
task_result = worker.AsyncResult(result.id)
1944
1945
db.session.commit()
1946
1947
return flask.redirect(f"/task/{result.id}?pr-id={id}&method={flask.request.args.get('method')}", 303) # should be 202 Accepted but we must use a redirect
1948
# db.session.delete(pull_request)
1949
else:
1950
flask.abort(400)
1951
1952
1953
@app.route("/task/<task_id>")
1954
def task_monitor(task_id):
1955
task_result = worker.AsyncResult(task_id)
1956
1957
if flask.request.args.get("partial"):
1958
# htmx partial update
1959
return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000)
1960
1961
# Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long
1962
return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125)
1963
1964
1965
@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"])
1966
def repository_prs_delete(username, repository):
1967
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1968
if not os.path.exists(server_repo_location):
1969
flask.abort(404)
1970
if not (get_visibility(username, repository) or get_permission_level(
1971
flask.session.get("username"), username,
1972
repository) is not None):
1973
flask.abort(403)
1974
1975
if not get_permission_level(flask.session.get("username"), username, repository):
1976
flask.abort(401)
1977
1978
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1979
repo = git.Repo(server_repo_location)
1980
id = flask.request.form.get("id")
1981
1982
pull_request = db.session.get(PullRequest, id)
1983
1984
if pull_request:
1985
pull_request.state = 2
1986
db.session.commit()
1987
1988
return flask.redirect(".", 303)
1989
1990
1991
@repositories.route("/<username>/<repository>/settings/")
1992
def repository_settings(username, repository):
1993
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1994
flask.abort(401)
1995
1996
repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository))
1997
1998
site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>")
1999
primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>")
2000
2001
return flask.render_template("repo-settings.html", username=username, repository=repository,
2002
repo_data=db.session.get(Repo, f"/{username}/{repository}"),
2003
branches=[branch.name for branch in repo.branches],
2004
site_link=site_link, primary_site_link=primary_site_link,
2005
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
2006
is_favourite=get_favourite(flask.session.get("username"), username, repository),
2007
)
2008
2009
2010
@repositories.route("/<username>/<repository>/settings/", methods=["POST"])
2011
def repository_settings_post(username, repository):
2012
if get_permission_level(flask.session.get("username"), username, repository) != 2:
2013
flask.abort(401)
2014
2015
repo = db.session.get(Repo, f"/{username}/{repository}")
2016
2017
repo.visibility = flask.request.form.get("visibility", type=int)
2018
repo.info = flask.request.form.get("description")
2019
repo.default_branch = flask.request.form.get("default_branch")
2020
repo.url = flask.request.form.get("url")
2021
2022
# Update site settings
2023
had_site = repo.has_site
2024
old_branch = repo.site_branch
2025
if flask.request.form.get("site_branch"):
2026
repo.site_branch = flask.request.form.get("site_branch")
2027
if flask.request.form.get("primary_site"):
2028
if had_site != 2:
2029
# Remove primary site from other repos
2030
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2):
2031
other_repo.has_site = 1 # switch it to a regular site
2032
flask.flash(Markup(
2033
_("Your repository {repository} has been demoted from a primary site to a regular site because there can only be one primary site per user.").format(
2034
repository=other_repo.route
2035
)), category="warning")
2036
repo.has_site = 2
2037
else:
2038
repo.has_site = 1
2039
else:
2040
repo.site_branch = None
2041
repo.has_site = 0
2042
2043
db.session.commit()
2044
2045
if not (had_site, old_branch) == (repo.has_site, repo.site_branch):
2046
# Deploy the newly activated site
2047
result = celery_tasks.copy_site.delay(repo.route)
2048
2049
if had_site and not repo.has_site:
2050
# Remove the site
2051
result = celery_tasks.delete_site.delay(repo.route)
2052
2053
if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site):
2054
# Deploy all other sites which were destroyed by the primary site
2055
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1):
2056
result = celery_tasks.copy_site.delay(other_repo.route)
2057
2058
return flask.redirect(f"/{username}/{repository}/settings", 303)
2059
2060
2061
@repositories.route("/<username>/<repository>/settings/add-label", methods=["POST"])
2062
def repository_settings_add_label(username, repository):
2063
if get_permission_level(flask.session.get("username"), username, repository) != 2:
2064
flask.abort(401)
2065
2066
repo_data = db.session.get(Repo, f"/{username}/{repository}")
2067
2068
label = Label(repo_data, flask.request.form.get("label"), flask.request.form.get("colour"))
2069
db.session.add(label)
2070
db.session.commit()
2071
2072
return flask.redirect(f"/{username}/{repository}/settings", 303)
2073
2074
2075
@repositories.route("/<username>/<repository>/settings/delete-label", methods=["POST"])
2076
def repository_settings_delete_label(username, repository):
2077
if get_permission_level(flask.session.get("username"), username, repository) != 2:
2078
flask.abort(401)
2079
2080
repo_data = db.session.get(Repo, f"/{username}/{repository}")
2081
2082
label = db.session.get(Label, flask.request.form.get("id"))
2083
2084
db.session.delete(label)
2085
db.session.commit()
2086
2087
return flask.redirect(f"/{username}/{repository}/settings", 303)
2088
2089
2090
@repositories.route("/<username>/<repository>/settings/edit-label", methods=["POST"])
2091
def repository_settings_edit_label(username, repository):
2092
if get_permission_level(flask.session.get("username"), username, repository) != 2:
2093
flask.abort(401)
2094
2095
repo_data = db.session.get(Repo, f"/{username}/{repository}")
2096
2097
label = db.session.get(Label, flask.request.form.get("id"))
2098
2099
label.name = flask.request.form.get("label")
2100
label.colour_hex = flask.request.form.get("colour")
2101
2102
db.session.commit()
2103
2104
return flask.redirect(f"/{username}/{repository}/settings", 303)
2105
2106
2107
@repositories.route("/<username>/<repository>/settings/delete", methods=["POST"])
2108
def repository_settings_delete(username, repository):
2109
if username != flask.session.get("username"):
2110
flask.abort(401)
2111
2112
repo = db.session.get(Repo, f"/{username}/{repository}")
2113
2114
if not repo:
2115
flask.abort(404)
2116
2117
user = db.session.get(User, flask.session.get("username"))
2118
2119
if not bcrypt.check_password_hash(user.password_hashed, flask.request.form.get("password")):
2120
flask.flash(_("Incorrect password"), category="error")
2121
flask.abort(401)
2122
2123
if repo.has_site:
2124
celery_tasks.delete_site.delay(repo.route)
2125
2126
db.session.delete(repo)
2127
db.session.commit()
2128
2129
shutil.rmtree(os.path.join(config.REPOS_PATH, username, repository))
2130
2131
return flask.redirect(f"/{username}", 303)
2132
2133
2134
@app.errorhandler(404)
2135
def e404(error):
2136
return flask.render_template("errors/not-found.html"), 404
2137
2138
2139
@app.errorhandler(401)
2140
def e401(error):
2141
return flask.render_template("errors/unauthorised.html"), 401
2142
2143
2144
@app.errorhandler(403)
2145
def e403(error):
2146
return flask.render_template("errors/forbidden.html"), 403
2147
2148
2149
@app.errorhandler(418)
2150
def e418(error):
2151
return flask.render_template("errors/teapot.html"), 418
2152
2153
2154
@app.errorhandler(405)
2155
def e405(error):
2156
return flask.render_template("errors/method-not-allowed.html"), 405
2157
2158
2159
@app.errorhandler(500)
2160
def e500(error):
2161
return flask.render_template("errors/server-error.html"), 500
2162
2163
2164
@app.errorhandler(400)
2165
def e400(error):
2166
return flask.render_template("errors/bad-request.html"), 400
2167
2168
2169
@app.errorhandler(410)
2170
def e410(error):
2171
return flask.render_template("errors/gone.html"), 410
2172
2173
2174
@app.errorhandler(415)
2175
def e415(error):
2176
return flask.render_template("errors/media-type.html"), 415
2177
2178
2179
if __name__ == "__main__":
2180
app.run(debug=True, port=8080, host="0.0.0.0")
2181
2182
app.register_blueprint(repositories)
2183