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