By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 app.py

View raw Download
text/x-script.python • 79.62 kiB
Python script, Unicode text, UTF-8 text executable
        
            
1
__version__ = "0.5.0"
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
info = {
742
"name": os.path.basename(file),
743
"serverPath": path,
744
"relativePath": file,
745
"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file),
746
"size": human_size(os.path.getsize(path)),
747
"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}",
748
"commit": last_commit,
749
"shaSize": 7,
750
}
751
752
special_icon = config.match_icon(os.path.basename(file))
753
if special_icon:
754
info["icon"] = special_icon
755
elif os.path.isdir(path):
756
info["icon"] = config.folder_icon
757
info["size"] = _("{} files").format(len(os.listdir(path)))
758
elif mimetypes.guess_type(path)[0] in config.file_icons:
759
info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]]
760
else:
761
info["icon"] = config.unknown_icon
762
763
if os.path.isdir(path):
764
infos.insert(0, info)
765
else:
766
infos.append(info)
767
768
return flask.render_template(
769
"repo-tree.html",
770
username=username,
771
repository=repository,
772
files=infos,
773
subpath=os.path.join("/", subpath),
774
branches=all_refs,
775
current=branch,
776
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
777
is_favourite=get_favourite(flask.session.get("username"), username, repository),
778
repo_data=repo_data,
779
)
780
else:
781
path = os.path.join(server_repo_location, subpath)
782
783
if not os.path.exists(path):
784
return flask.render_template("errors/not-found.html"), 404
785
786
mimetype = guess_mime(path)
787
mode = mimetype.split("/", 1)[0]
788
size = human_size(os.path.getsize(path))
789
790
special_icon = config.match_icon(os.path.basename(path))
791
if special_icon:
792
icon = special_icon
793
elif os.path.isdir(path):
794
icon = config.folder_icon
795
elif mimetypes.guess_type(path)[0] in config.file_icons:
796
icon = config.file_icons[mimetypes.guess_type(path)[0]]
797
else:
798
icon = config.unknown_icon
799
800
contents = None
801
if mode == "text":
802
contents = convert_to_html(path)
803
804
return flask.render_template(
805
"repo-file.html",
806
username=username,
807
repository=repository,
808
file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath),
809
branches=all_refs,
810
current=branch,
811
mode=mode,
812
mimetype=mimetype,
813
detailedtype=magic.from_file(path),
814
size=size,
815
icon=icon,
816
subpath=os.path.join("/", subpath),
817
extension=pathlib.Path(path).suffix,
818
basename=os.path.basename(path),
819
contents=contents,
820
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
821
is_favourite=get_favourite(flask.session.get("username"), username, repository),
822
repo_data=repo_data,
823
)
824
825
826
@repositories.route("/<username>/<repository>/commit/<sha>")
827
def repository_commit(username, repository, sha):
828
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
829
if not os.path.exists(server_repo_location):
830
flask.abort(404)
831
if not (get_visibility(username, repository) or get_permission_level(
832
flask.session.get("username"), username,
833
repository) is not None):
834
flask.abort(403)
835
836
if not os.path.exists(server_repo_location):
837
return flask.render_template("errors/not-found.html"), 404
838
839
repo = git.Repo(server_repo_location)
840
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
841
842
files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r",
843
"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1]
844
845
return flask.render_template(
846
"repo-commit.html",
847
username=username,
848
repository=repository,
849
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
850
is_favourite=get_favourite(flask.session.get("username"), username, repository),
851
diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff",
852
str(sha) + "^!", "--", file).decode().split("\n") for
853
file in files},
854
data=db.session.get(Commit, f"/{username}/{repository}/{sha}"),
855
repo_data=repo_data,
856
comment_query=Comment.query,
857
permission_level=get_permission_level(flask.session.get("username"), username, repository),
858
)
859
860
861
@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"])
862
def repository_commit_add_comment(username, repository, sha):
863
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
864
if not os.path.exists(server_repo_location):
865
flask.abort(404)
866
if not (get_visibility(username, repository) or get_permission_level(
867
flask.session.get("username"), username,
868
repository) is not None):
869
flask.abort(403)
870
871
comment = Comment(
872
db.session.get(User, flask.session.get("username")),
873
db.session.get(Repo, f"/{username}/{repository}"),
874
db.session.get(Commit, f"/{username}/{repository}/{sha}"),
875
flask.request.form["comment"],
876
flask.request.form["file"],
877
flask.request.form["line"],
878
)
879
880
db.session.add(comment)
881
db.session.commit()
882
883
return flask.redirect(
884
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
885
code=303
886
)
887
888
889
@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"])
890
def repository_commit_delete_comment(username, repository, sha, id):
891
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
892
comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first()
893
commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first()
894
if (
895
comment.owner.username == flask.session.get("username")
896
or get_permission_level(flask.session.get("username"), username, repository) >= 2
897
or comment.commit.owner.username == flask.session.get("username")
898
):
899
db.session.delete(comment)
900
db.session.commit()
901
902
return flask.redirect(
903
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
904
code=303
905
)
906
907
908
@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"])
909
def repository_commit_resolve_comment(username, repository, sha, id):
910
comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first()
911
if (
912
comment.commit.owner.username == flask.session.get("username")
913
or get_permission_level(flask.session.get("username"), username, repository) >= 2
914
or comment.owner.username == flask.session.get("username")
915
):
916
comment.state = int(not comment.state)
917
db.session.commit()
918
919
return flask.redirect(
920
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
921
code=303
922
)
923
924
925
@repositories.route("/<username>/<repository>/forum/")
926
def repository_forum(username, repository):
927
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
928
if not os.path.exists(server_repo_location):
929
flask.abort(404)
930
if not (get_visibility(username, repository) or get_permission_level(
931
flask.session.get("username"), username,
932
repository) is not None):
933
flask.abort(403)
934
935
repo = git.Repo(server_repo_location)
936
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
937
user = User.query.filter_by(username=flask.session.get("username")).first()
938
relationships = RepoAccess.query.filter_by(repo=repo_data)
939
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
940
941
page_number = flask.request.args.get("page", 1, type=int)
942
if flask.session.get("username"):
943
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
944
else:
945
default_page_length = 16
946
947
page_length = flask.request.args.get("per_page", default_page_length, type=int)
948
949
posts = Post.query.filter_by(repo=repo_data).order_by(Post.last_updated.desc()).paginate(
950
page=page_number, per_page=page_length
951
)
952
953
if posts.has_next:
954
next_page = posts.next_num
955
else:
956
next_page = None
957
958
if posts.has_prev:
959
prev_page = posts.prev_num
960
else:
961
prev_page = None
962
963
return flask.render_template(
964
"repo-forum.html",
965
username=username,
966
repository=repository,
967
repo_data=repo_data,
968
relationships=relationships,
969
repo=repo,
970
user_relationship=user_relationship,
971
Post=Post,
972
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
973
is_favourite=get_favourite(flask.session.get("username"), username, repository),
974
default_branch=repo_data.default_branch,
975
page_number=page_number,
976
page_length=page_length,
977
next_page=next_page,
978
prev_page=prev_page,
979
num_pages=posts.pages,
980
posts=posts
981
)
982
983
984
@repositories.route("/<username>/<repository>/forum/search")
985
def repository_forum_search(username, repository):
986
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
987
if not os.path.exists(server_repo_location):
988
flask.abort(404)
989
if not (get_visibility(username, repository) or get_permission_level(
990
flask.session.get("username"), username,
991
repository) is not None):
992
flask.abort(403)
993
994
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
995
user = User.query.filter_by(username=flask.session.get("username")).first()
996
relationships = RepoAccess.query.filter_by(repo=repo_data)
997
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
998
999
query = flask.request.args.get("q")
1000
1001
page_number = flask.request.args.get("page", 1, type=int)
1002
if flask.session.get("username"):
1003
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1004
else:
1005
default_page_length = 16
1006
1007
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1008
1009
all_posts = Post.query.filter(Post.repo == repo_data)
1010
1011
results = (all_posts
1012
.filter(Post.subject.ilike(f"%{query}%") | Post.message.ilike(f"%{query}%"))
1013
.order_by(Post.last_updated.desc()))
1014
1015
if flask.request.args.get("state"):
1016
try:
1017
results = results.filter(Post.state == int(flask.request.args.get("state")))
1018
except ValueError:
1019
pass # if state is not an integer, ignore it
1020
1021
if flask.request.args.get("label"):
1022
results = results.filter(Post.labels.any(Label.identifier == flask.request.args.get("label")))
1023
1024
results = results.paginate(page=page_number, per_page=page_length)
1025
1026
if results.has_next:
1027
next_page = results.next_num
1028
else:
1029
next_page = None
1030
1031
if results.has_prev:
1032
prev_page = results.prev_num
1033
else:
1034
prev_page = None
1035
1036
return flask.render_template(
1037
"repo-forum-search.html",
1038
username=username,
1039
repository=repository,
1040
repo_data=repo_data,
1041
relationships=relationships,
1042
user_relationship=user_relationship,
1043
query=query,
1044
results=results,
1045
Post=Post,
1046
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1047
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1048
default_branch=repo_data.default_branch,
1049
page_number=page_number,
1050
page_length=page_length,
1051
next_page=next_page,
1052
prev_page=prev_page,
1053
num_pages=results.pages,
1054
require_state=flask.request.args.get("state"),
1055
require_label=flask.request.args.get("label"),
1056
)
1057
1058
1059
@repositories.route("/<username>/<repository>/forum/topic/<int:id>")
1060
def repository_forum_topic(username, repository, id):
1061
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1062
if not os.path.exists(server_repo_location):
1063
flask.abort(404)
1064
if not (get_visibility(username, repository) or get_permission_level(
1065
flask.session.get("username"), username,
1066
repository) is not None):
1067
flask.abort(403)
1068
1069
if not os.path.exists(server_repo_location):
1070
return flask.render_template("errors/not-found.html"), 404
1071
1072
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1073
user = User.query.filter_by(username=flask.session.get("username")).first()
1074
relationships = RepoAccess.query.filter_by(repo=repo_data)
1075
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1076
1077
post = Post.query.filter_by(id=id).first()
1078
1079
return flask.render_template(
1080
"repo-topic.html",
1081
username=username,
1082
repository=repository,
1083
repo_data=repo_data,
1084
relationships=relationships,
1085
user_relationship=user_relationship,
1086
post=post,
1087
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1088
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1089
default_branch=repo_data.default_branch
1090
)
1091
1092
1093
@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"])
1094
def repository_forum_new(username, repository):
1095
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1096
if not os.path.exists(server_repo_location):
1097
flask.abort(404)
1098
if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level(
1099
flask.session.get("username"), username,
1100
repository) is not None):
1101
flask.abort(403)
1102
1103
if not os.path.exists(server_repo_location):
1104
return flask.render_template("errors/not-found.html"), 404
1105
1106
repo = git.Repo(server_repo_location)
1107
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1108
user = User.query.filter_by(username=flask.session.get("username")).first()
1109
relationships = RepoAccess.query.filter_by(repo=repo_data)
1110
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1111
1112
post = Post(user, repo_data, None, flask.request.form["subject"],
1113
flask.request.form["message"])
1114
1115
db.session.add(post)
1116
db.session.commit()
1117
1118
return flask.redirect(
1119
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1120
post_id=post.number),
1121
code=303)
1122
1123
1124
@repositories.route("/<username>/<repository>/forum/<int:post_id>")
1125
def repository_forum_thread(username, repository, post_id):
1126
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1127
if not os.path.exists(server_repo_location):
1128
flask.abort(404)
1129
if not (get_visibility(username, repository) or get_permission_level(
1130
flask.session.get("username"), username,
1131
repository) is not None):
1132
flask.abort(403)
1133
1134
if not os.path.exists(server_repo_location):
1135
return flask.render_template("errors/not-found.html"), 404
1136
1137
repo = git.Repo(server_repo_location)
1138
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1139
user = User.query.filter_by(username=flask.session.get("username")).first()
1140
relationships = RepoAccess.query.filter_by(repo=repo_data)
1141
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1142
1143
if user:
1144
max_post_nesting = user.max_post_nesting
1145
else:
1146
max_post_nesting = 2
1147
1148
return flask.render_template(
1149
"repo-forum-thread.html",
1150
username=username,
1151
repository=repository,
1152
repo_data=repo_data,
1153
relationships=relationships,
1154
repo=repo,
1155
Post=Post,
1156
user_relationship=user_relationship,
1157
post_id=post_id,
1158
max_post_nesting=max_post_nesting,
1159
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1160
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1161
parent=Post.query.filter_by(repo=repo_data, number=post_id).first(),
1162
has_permission=not ((not get_permission_level(flask.session.get("username"), username,
1163
repository)) and db.session.get(Post,
1164
f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")),
1165
)
1166
1167
1168
@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state",
1169
methods=["POST"])
1170
def repository_forum_change_state(username, repository, post_id):
1171
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1172
if not os.path.exists(server_repo_location):
1173
flask.abort(404)
1174
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"):
1175
flask.abort(403)
1176
1177
repo = git.Repo(server_repo_location)
1178
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1179
user = User.query.filter_by(username=flask.session.get("username")).first()
1180
relationships = RepoAccess.query.filter_by(repo=repo_data)
1181
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1182
1183
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1184
1185
if not post:
1186
flask.abort(404)
1187
if post.parent:
1188
flask.abort(400)
1189
1190
post.state = int(flask.request.form["new-state"])
1191
1192
db.session.commit()
1193
1194
return flask.redirect(
1195
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1196
post_id=post_id),
1197
code=303)
1198
1199
1200
@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"])
1201
def repository_forum_reply(username, repository, post_id):
1202
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1203
if not os.path.exists(server_repo_location):
1204
flask.abort(404)
1205
if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level(
1206
flask.session.get("username"), username,
1207
repository) is not None):
1208
flask.abort(403)
1209
1210
if not os.path.exists(server_repo_location):
1211
return flask.render_template("errors/not-found.html"), 404
1212
1213
repo = git.Repo(server_repo_location)
1214
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1215
user = User.query.filter_by(username=flask.session.get("username")).first()
1216
relationships = RepoAccess.query.filter_by(repo=repo_data)
1217
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1218
if not user:
1219
flask.abort(401)
1220
1221
parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1222
post = Post(user, repo_data, parent, flask.request.form["subject"],
1223
flask.request.form["message"])
1224
1225
db.session.add(post)
1226
post.update_date()
1227
db.session.commit()
1228
1229
return flask.redirect(
1230
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1231
post_id=post_id),
1232
code=303)
1233
1234
1235
@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["POST"])
1236
def repository_forum_edit(username, repository, post_id):
1237
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1238
if not os.path.exists(server_repo_location):
1239
flask.abort(404)
1240
if not (get_visibility(username, repository) or get_permission_level(
1241
flask.session.get("username"), username,
1242
repository) is not None):
1243
flask.abort(403)
1244
1245
if not os.path.exists(server_repo_location):
1246
return flask.render_template("errors/not-found.html"), 404
1247
1248
repo = git.Repo(server_repo_location)
1249
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1250
user = User.query.filter_by(username=flask.session.get("username")).first()
1251
relationships = RepoAccess.query.filter_by(repo=repo_data)
1252
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1253
if not user:
1254
flask.abort(401)
1255
post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
1256
if user != post.owner:
1257
flask.abort(403)
1258
1259
post.subject = flask.request.form["subject"]
1260
post.message = flask.request.form["message"]
1261
post.html = markdown.markdown2html(post.message).prettify()
1262
post.update_date()
1263
db.session.commit()
1264
1265
return flask.redirect(
1266
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1267
post_id=post_id),
1268
code=303)
1269
1270
1271
@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["GET"])
1272
def repository_forum_edit_form(username, repository, post_id):
1273
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1274
if not os.path.exists(server_repo_location):
1275
flask.abort(404)
1276
if not (get_visibility(username, repository) or get_permission_level(
1277
flask.session.get("username"), username,
1278
repository) is not None):
1279
flask.abort(403)
1280
1281
if not os.path.exists(server_repo_location):
1282
return flask.render_template("errors/not-found.html"), 404
1283
1284
repo = git.Repo(server_repo_location)
1285
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1286
user = User.query.filter_by(username=flask.session.get("username")).first()
1287
relationships = RepoAccess.query.filter_by(repo=repo_data)
1288
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1289
if not user:
1290
flask.abort(401)
1291
post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
1292
if user != post.owner:
1293
flask.abort(403)
1294
1295
return flask.render_template(
1296
"repo-forum-edit.html",
1297
username=username,
1298
repository=repository,
1299
repo_data=repo_data,
1300
relationships=relationships,
1301
repo=repo,
1302
user_relationship=user_relationship,
1303
post=post,
1304
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1305
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1306
default_branch=repo_data.default_branch
1307
)
1308
1309
@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup",
1310
defaults={"score": 1})
1311
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown",
1312
defaults={"score": -1})
1313
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0})
1314
def repository_forum_vote(username, repository, post_id, score):
1315
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1316
if not os.path.exists(server_repo_location):
1317
flask.abort(404)
1318
if not (get_visibility(username, repository) or get_permission_level(
1319
flask.session.get("username"), username,
1320
repository) is not None):
1321
flask.abort(403)
1322
1323
if not os.path.exists(server_repo_location):
1324
return flask.render_template("errors/not-found.html"), 404
1325
1326
repo = git.Repo(server_repo_location)
1327
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1328
user = User.query.filter_by(username=flask.session.get("username")).first()
1329
relationships = RepoAccess.query.filter_by(repo=repo_data)
1330
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1331
if not user:
1332
flask.abort(401)
1333
1334
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1335
1336
if score:
1337
old_relationship = PostVote.query.filter_by(user_username=user.username,
1338
post_identifier=post.identifier).first()
1339
if old_relationship:
1340
if score == old_relationship.vote_score:
1341
db.session.delete(old_relationship)
1342
post.vote_sum -= old_relationship.vote_score
1343
else:
1344
post.vote_sum -= old_relationship.vote_score
1345
post.vote_sum += score
1346
old_relationship.vote_score = score
1347
else:
1348
relationship = PostVote(user, post, score)
1349
post.vote_sum += score
1350
db.session.add(relationship)
1351
1352
db.session.commit()
1353
1354
user_vote = PostVote.query.filter_by(user_username=user.username,
1355
post_identifier=post.identifier).first()
1356
response = flask.make_response(
1357
str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0))
1358
response.content_type = "text/plain"
1359
1360
return response
1361
1362
1363
@repositories.route("/<username>/<repository>/forum/<int:post_id>/label", methods=["POST"])
1364
def repository_forum_label(username, repository, post_id):
1365
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1366
if not os.path.exists(server_repo_location):
1367
flask.abort(404)
1368
if not get_permission_level(flask.session.get("username"), username, repository):
1369
flask.abort(403)
1370
1371
repo = git.Repo(server_repo_location)
1372
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1373
user = User.query.filter_by(username=flask.session.get("username")).first()
1374
relationships = RepoAccess.query.filter_by(repo=repo_data)
1375
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1376
1377
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1378
1379
if not post:
1380
flask.abort(404)
1381
if post.parent:
1382
flask.abort(400)
1383
1384
label = db.session.get(Label, flask.request.form["label"])
1385
1386
if PostLabel.query.filter_by(post=post, label=label).first():
1387
return flask.redirect(
1388
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1389
post_id=post_id),
1390
code=303)
1391
1392
post_label = PostLabel(post, label)
1393
db.session.add(post_label)
1394
1395
db.session.commit()
1396
1397
return flask.redirect(
1398
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1399
post_id=post_id),
1400
code=303)
1401
1402
1403
@repositories.route("/<username>/<repository>/forum/<int:post_id>/remove-label")
1404
def repository_forum_remove_label(username, repository, post_id):
1405
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1406
if not os.path.exists(server_repo_location):
1407
flask.abort(404)
1408
if not get_permission_level(flask.session.get("username"), username, repository):
1409
flask.abort(403)
1410
1411
repo = git.Repo(server_repo_location)
1412
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1413
user = User.query.filter_by(username=flask.session.get("username")).first()
1414
relationships = RepoAccess.query.filter_by(repo=repo_data)
1415
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1416
1417
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1418
1419
if not post:
1420
flask.abort(404)
1421
if post.parent:
1422
flask.abort(400)
1423
1424
label = db.session.get(Label, flask.request.args["label"])
1425
1426
post_label = PostLabel.query.filter_by(post=post, label=label).first()
1427
db.session.delete(post_label)
1428
1429
db.session.commit()
1430
1431
return flask.redirect(
1432
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1433
post_id=post_id),
1434
code=303)
1435
1436
1437
@repositories.route("/<username>/<repository>/favourite")
1438
def repository_favourite(username, repository):
1439
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1440
if not os.path.exists(server_repo_location):
1441
flask.abort(404)
1442
if not (get_visibility(username, repository) or get_permission_level(
1443
flask.session.get("username"), username,
1444
repository) is not None):
1445
flask.abort(403)
1446
1447
if not os.path.exists(server_repo_location):
1448
return flask.render_template("errors/not-found.html"), 404
1449
1450
repo = git.Repo(server_repo_location)
1451
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1452
user = User.query.filter_by(username=flask.session.get("username")).first()
1453
relationships = RepoAccess.query.filter_by(repo=repo_data)
1454
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1455
if not user:
1456
flask.abort(401)
1457
1458
old_relationship = RepoFavourite.query.filter_by(user_username=user.username,
1459
repo_route=repo_data.route).first()
1460
if old_relationship:
1461
db.session.delete(old_relationship)
1462
else:
1463
relationship = RepoFavourite(user, repo_data)
1464
db.session.add(relationship)
1465
1466
db.session.commit()
1467
1468
return flask.redirect(flask.url_for("favourites"), code=303)
1469
1470
1471
@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"])
1472
def repository_users(username, repository):
1473
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1474
if not os.path.exists(server_repo_location):
1475
flask.abort(404)
1476
if not (get_visibility(username, repository) or get_permission_level(
1477
flask.session.get("username"), username,
1478
repository) is not None):
1479
flask.abort(403)
1480
1481
if not os.path.exists(server_repo_location):
1482
return flask.render_template("errors/not-found.html"), 404
1483
1484
repo = git.Repo(server_repo_location)
1485
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1486
user = User.query.filter_by(username=flask.session.get("username")).first()
1487
relationships = RepoAccess.query.filter_by(repo=repo_data)
1488
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1489
1490
if flask.request.method == "GET":
1491
return flask.render_template(
1492
"repo-users.html",
1493
username=username,
1494
repository=repository,
1495
repo_data=repo_data,
1496
relationships=relationships,
1497
repo=repo,
1498
user_relationship=user_relationship,
1499
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1500
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1501
)
1502
else:
1503
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1504
flask.abort(401)
1505
1506
if flask.request.form.get("new-username"):
1507
# Create new relationship
1508
new_user = User.query.filter_by(
1509
username=flask.request.form.get("new-username")).first()
1510
relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level"))
1511
db.session.add(relationship)
1512
db.session.commit()
1513
if flask.request.form.get("update-username"):
1514
# Create new relationship
1515
updated_user = User.query.filter_by(
1516
username=flask.request.form.get("update-username")).first()
1517
relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first()
1518
if flask.request.form.get("update-level") == -1:
1519
relationship.delete()
1520
else:
1521
relationship.access_level = flask.request.form.get("update-level")
1522
db.session.commit()
1523
1524
return flask.redirect(
1525
app.url_for(".repository_users", username=username, repository=repository))
1526
1527
1528
@repositories.route("/<username>/<repository>/branches/")
1529
def repository_branches(username, repository):
1530
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1531
if not os.path.exists(server_repo_location):
1532
flask.abort(404)
1533
if not (get_visibility(username, repository) or get_permission_level(
1534
flask.session.get("username"), username,
1535
repository) is not None):
1536
flask.abort(403)
1537
1538
if not os.path.exists(server_repo_location):
1539
return flask.render_template("errors/not-found.html"), 404
1540
1541
repo = git.Repo(server_repo_location)
1542
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1543
1544
return flask.render_template(
1545
"repo-branches.html",
1546
username=username,
1547
repository=repository,
1548
repo_data=repo_data,
1549
repo=repo,
1550
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1551
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1552
)
1553
1554
1555
@repositories.route("/<username>/<repository>/log/", defaults={"branch": None})
1556
@repositories.route("/<username>/<repository>/log/<branch>/")
1557
def repository_log(username, repository, branch):
1558
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1559
if not os.path.exists(server_repo_location):
1560
flask.abort(404)
1561
if not (get_visibility(username, repository) or get_permission_level(
1562
flask.session.get("username"), username,
1563
repository) is not None):
1564
flask.abort(403)
1565
1566
if not os.path.exists(server_repo_location):
1567
return flask.render_template("errors/not-found.html"), 404
1568
1569
repo = git.Repo(server_repo_location)
1570
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1571
if not repo_data.default_branch:
1572
if repo.heads:
1573
repo_data.default_branch = repo.heads[0].name
1574
else:
1575
return flask.render_template("empty.html",
1576
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
1577
if not branch:
1578
branch = repo_data.default_branch
1579
return flask.redirect(f"./{branch}", code=302)
1580
1581
if branch.startswith("tag:"):
1582
ref = f"tags/{branch[4:]}"
1583
elif branch.startswith("~"):
1584
ref = branch[1:]
1585
else:
1586
ref = f"heads/{branch}"
1587
1588
ref = ref.replace("~", "/") # encode slashes for URL support
1589
1590
try:
1591
repo.git.checkout("-f", ref)
1592
except git.exc.GitCommandError:
1593
return flask.render_template("errors/not-found.html"), 404
1594
1595
branches = repo.heads
1596
1597
all_refs = []
1598
for ref in repo.heads:
1599
all_refs.append((ref, "head"))
1600
for ref in repo.tags:
1601
all_refs.append((ref, "tag"))
1602
1603
commit_list = [f"/{username}/{repository}/{sha}" for sha in
1604
git_command(server_repo_location, None, "log",
1605
"--format='%H'").decode().split("\n")]
1606
1607
commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc())
1608
page_number = flask.request.args.get("page", 1, type=int)
1609
if flask.session.get("username"):
1610
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1611
else:
1612
default_page_length = 16
1613
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1614
page_listing = db.paginate(commits, page=page_number, per_page=page_length)
1615
1616
if page_listing.has_next:
1617
next_page = page_listing.next_num
1618
else:
1619
next_page = None
1620
1621
if page_listing.has_prev:
1622
prev_page = page_listing.prev_num
1623
else:
1624
prev_page = None
1625
1626
return flask.render_template(
1627
"repo-log.html",
1628
username=username,
1629
repository=repository,
1630
branches=all_refs,
1631
current=branch,
1632
repo_data=repo_data,
1633
repo=repo,
1634
commits=page_listing,
1635
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1636
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1637
page_number=page_number,
1638
page_length=page_length,
1639
next_page=next_page,
1640
prev_page=prev_page,
1641
num_pages=page_listing.pages
1642
)
1643
1644
1645
@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"])
1646
def repository_prs(username, repository):
1647
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1648
if not os.path.exists(server_repo_location):
1649
flask.abort(404)
1650
if not (get_visibility(username, repository) or get_permission_level(
1651
flask.session.get("username"), username,
1652
repository) is not None):
1653
flask.abort(403)
1654
1655
if not os.path.exists(server_repo_location):
1656
return flask.render_template("errors/not-found.html"), 404
1657
1658
if flask.request.method == "GET":
1659
repo = git.Repo(server_repo_location)
1660
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1661
user = User.query.filter_by(username=flask.session.get("username")).first()
1662
1663
return flask.render_template(
1664
"repo-prs.html",
1665
username=username,
1666
repository=repository,
1667
repo_data=repo_data,
1668
repo=repo,
1669
PullRequest=PullRequest,
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
default_branch=repo_data.default_branch,
1673
branches=repo.branches
1674
)
1675
1676
elif "id" not in flask.request.form:
1677
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1678
head = flask.request.form.get("head")
1679
head_route = flask.request.form.get("headroute")
1680
base = flask.request.form.get("base")
1681
1682
if not head and base and head_route:
1683
return flask.redirect(".", 400)
1684
1685
head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/")))
1686
base_repo = git.Repo(server_repo_location)
1687
1688
if head not in head_repo.branches or base not in base_repo.branches:
1689
flask.flash(Markup(
1690
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")),
1691
category="error")
1692
return flask.redirect(".", 303)
1693
1694
head_data = db.session.get(Repo, head_route)
1695
if not head_data.visibility:
1696
flask.flash(Markup(
1697
"<iconify-icon icon='mdi:error'></iconify-icon>" + _(
1698
"Head can't be restricted")),
1699
category="error")
1700
return flask.redirect(".", 303)
1701
1702
pull_request = PullRequest(head_data, head, repo_data, base,
1703
db.session.get(User, flask.session["username"]))
1704
1705
db.session.add(pull_request)
1706
db.session.commit()
1707
1708
# Create the notification
1709
notification = Notification({"type": "pr", "head": pull_request.head.route, "base": pull_request.base.route, "pr": pull_request.id})
1710
db.session.add(notification)
1711
db.session.commit()
1712
1713
# Send a notification to all users who have enabled PR notifications for this repo
1714
for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all():
1715
user = relationship.user
1716
user_notification = UserNotification(user, notification, 1)
1717
db.session.add(user_notification)
1718
db.session.commit()
1719
celery_tasks.send_notification.apply_async(args=[user_notification.id])
1720
1721
return flask.redirect(".", 303)
1722
else:
1723
id = flask.request.form.get("id")
1724
pull_request = db.session.get(PullRequest, id)
1725
1726
if not pull_request:
1727
flask.abort(404)
1728
1729
if not (get_visibility(username, repository) or get_permission_level(
1730
flask.session.get("username"), username,
1731
repository) >= 1 or pull_request.owner.username == flask.session.get("username")):
1732
flask.abort(403)
1733
1734
if not get_permission_level(flask.session.get("username"), username, repository):
1735
flask.abort(401)
1736
1737
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1738
1739
if pull_request:
1740
pull_request.resolves_list = flask.request.form.get("resolves")
1741
db.session.commit()
1742
1743
return flask.redirect(".", 303)
1744
1745
1746
@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"])
1747
def repository_prs_merge(username, repository):
1748
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1749
if not os.path.exists(server_repo_location):
1750
flask.abort(404)
1751
if not (get_visibility(username, repository) or get_permission_level(
1752
flask.session.get("username"), username,
1753
repository) is not None):
1754
flask.abort(403)
1755
1756
if not get_permission_level(flask.session.get("username"), username, repository):
1757
flask.abort(401)
1758
1759
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1760
repo = git.Repo(server_repo_location)
1761
id = flask.request.form.get("id")
1762
1763
pull_request = db.session.get(PullRequest, id)
1764
1765
if pull_request:
1766
result = celery_tasks.merge_heads.delay(
1767
pull_request.head_route,
1768
pull_request.head_branch,
1769
pull_request.base_route,
1770
pull_request.base_branch,
1771
pull_request.id,
1772
simulate=True
1773
)
1774
task_result = worker.AsyncResult(result.id)
1775
1776
return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) # should be 202 Accepted but we must use a redirect
1777
# db.session.delete(pull_request)
1778
# db.session.commit()
1779
else:
1780
flask.abort(400)
1781
1782
1783
@repositories.route("/<username>/<repository>/prs/<int:id>/merge")
1784
def repository_prs_merge_stage_two(username, repository, id):
1785
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1786
if not os.path.exists(server_repo_location):
1787
flask.abort(404)
1788
if not (get_visibility(username, repository) or get_permission_level(
1789
flask.session.get("username"), username,
1790
repository) is not None):
1791
flask.abort(403)
1792
1793
if not get_permission_level(flask.session.get("username"), username, repository):
1794
flask.abort(401)
1795
1796
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1797
repo = git.Repo(server_repo_location)
1798
1799
pull_request = db.session.get(PullRequest, id)
1800
1801
if pull_request:
1802
result = celery_tasks.merge_heads.delay(
1803
pull_request.head_route,
1804
pull_request.head_branch,
1805
pull_request.base_route,
1806
pull_request.base_branch,
1807
pull_request.id,
1808
simulate=False
1809
)
1810
task_result = worker.AsyncResult(result.id)
1811
1812
db.session.commit()
1813
1814
return flask.redirect(f"/task/{result.id}?pr-id={id}", 303)
1815
# db.session.delete(pull_request)
1816
else:
1817
flask.abort(400)
1818
1819
1820
@app.route("/task/<task_id>")
1821
def task_monitor(task_id):
1822
task_result = worker.AsyncResult(task_id)
1823
1824
if flask.request.args.get("partial"):
1825
# htmx partial update
1826
return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000)
1827
1828
# Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long
1829
return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125)
1830
1831
1832
@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"])
1833
def repository_prs_delete(username, repository):
1834
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1835
if not os.path.exists(server_repo_location):
1836
flask.abort(404)
1837
if not (get_visibility(username, repository) or get_permission_level(
1838
flask.session.get("username"), username,
1839
repository) is not None):
1840
flask.abort(403)
1841
1842
if not get_permission_level(flask.session.get("username"), username, repository):
1843
flask.abort(401)
1844
1845
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1846
repo = git.Repo(server_repo_location)
1847
id = flask.request.form.get("id")
1848
1849
pull_request = db.session.get(PullRequest, id)
1850
1851
if pull_request:
1852
pull_request.state = 2
1853
db.session.commit()
1854
1855
return flask.redirect(".", 303)
1856
1857
1858
@repositories.route("/<username>/<repository>/settings/")
1859
def repository_settings(username, repository):
1860
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1861
flask.abort(401)
1862
1863
repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository))
1864
1865
site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>")
1866
primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>")
1867
1868
return flask.render_template("repo-settings.html", username=username, repository=repository,
1869
repo_data=db.session.get(Repo, f"/{username}/{repository}"),
1870
branches=[branch.name for branch in repo.branches],
1871
site_link=site_link, primary_site_link=primary_site_link,
1872
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1873
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1874
)
1875
1876
1877
@repositories.route("/<username>/<repository>/settings/", methods=["POST"])
1878
def repository_settings_post(username, repository):
1879
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1880
flask.abort(401)
1881
1882
repo = db.session.get(Repo, f"/{username}/{repository}")
1883
1884
repo.visibility = flask.request.form.get("visibility", type=int)
1885
repo.info = flask.request.form.get("description")
1886
repo.default_branch = flask.request.form.get("default_branch")
1887
repo.url = flask.request.form.get("url")
1888
1889
# Update site settings
1890
had_site = repo.has_site
1891
old_branch = repo.site_branch
1892
if flask.request.form.get("site_branch"):
1893
repo.site_branch = flask.request.form.get("site_branch")
1894
if flask.request.form.get("primary_site"):
1895
if had_site != 2:
1896
# Remove primary site from other repos
1897
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2):
1898
other_repo.has_site = 1 # switch it to a regular site
1899
flask.flash(Markup(
1900
_("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(
1901
repository=other_repo.route
1902
)), category="warning")
1903
repo.has_site = 2
1904
else:
1905
repo.has_site = 1
1906
else:
1907
repo.site_branch = None
1908
repo.has_site = 0
1909
1910
db.session.commit()
1911
1912
if not (had_site, old_branch) == (repo.has_site, repo.site_branch):
1913
# Deploy the newly activated site
1914
result = celery_tasks.copy_site.delay(repo.route)
1915
1916
if had_site and not repo.has_site:
1917
# Remove the site
1918
result = celery_tasks.delete_site.delay(repo.route)
1919
1920
if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site):
1921
# Deploy all other sites which were destroyed by the primary site
1922
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1):
1923
result = celery_tasks.copy_site.delay(other_repo.route)
1924
1925
return flask.redirect(f"/{username}/{repository}/settings", 303)
1926
1927
1928
@repositories.route("/<username>/<repository>/settings/add-label", methods=["POST"])
1929
def repository_settings_add_label(username, repository):
1930
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1931
flask.abort(401)
1932
1933
repo_data = db.session.get(Repo, f"/{username}/{repository}")
1934
1935
label = Label(repo_data, flask.request.form.get("label"), flask.request.form.get("colour"))
1936
db.session.add(label)
1937
db.session.commit()
1938
1939
return flask.redirect(f"/{username}/{repository}/settings", 303)
1940
1941
1942
@repositories.route("/<username>/<repository>/settings/delete-label", methods=["POST"])
1943
def repository_settings_delete_label(username, repository):
1944
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1945
flask.abort(401)
1946
1947
repo_data = db.session.get(Repo, f"/{username}/{repository}")
1948
1949
label = db.session.get(Label, flask.request.form.get("id"))
1950
1951
db.session.delete(label)
1952
db.session.commit()
1953
1954
return flask.redirect(f"/{username}/{repository}/settings", 303)
1955
1956
1957
@repositories.route("/<username>/<repository>/settings/edit-label", methods=["POST"])
1958
def repository_settings_edit_label(username, repository):
1959
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1960
flask.abort(401)
1961
1962
repo_data = db.session.get(Repo, f"/{username}/{repository}")
1963
1964
label = db.session.get(Label, flask.request.form.get("id"))
1965
1966
label.name = flask.request.form.get("label")
1967
label.colour_hex = flask.request.form.get("colour")
1968
1969
db.session.commit()
1970
1971
return flask.redirect(f"/{username}/{repository}/settings", 303)
1972
1973
1974
@repositories.route("/<username>/<repository>/settings/delete", methods=["POST"])
1975
def repository_settings_delete(username, repository):
1976
if username != flask.session.get("username"):
1977
flask.abort(401)
1978
1979
repo = db.session.get(Repo, f"/{username}/{repository}")
1980
1981
if not repo:
1982
flask.abort(404)
1983
1984
user = db.session.get(User, flask.session.get("username"))
1985
1986
if not bcrypt.check_password_hash(user.password_hashed, flask.request.form.get("password")):
1987
flask.flash(_("Incorrect password"), category="error")
1988
flask.abort(401)
1989
1990
if repo.has_site:
1991
celery_tasks.delete_site.delay(repo.route)
1992
1993
db.session.delete(repo)
1994
db.session.commit()
1995
1996
shutil.rmtree(os.path.join(config.REPOS_PATH, username, repository))
1997
1998
return flask.redirect(f"/{username}", 303)
1999
2000
2001
@app.errorhandler(404)
2002
def e404(error):
2003
return flask.render_template("errors/not-found.html"), 404
2004
2005
2006
@app.errorhandler(401)
2007
def e401(error):
2008
return flask.render_template("errors/unauthorised.html"), 401
2009
2010
2011
@app.errorhandler(403)
2012
def e403(error):
2013
return flask.render_template("errors/forbidden.html"), 403
2014
2015
2016
@app.errorhandler(418)
2017
def e418(error):
2018
return flask.render_template("errors/teapot.html"), 418
2019
2020
2021
@app.errorhandler(405)
2022
def e405(error):
2023
return flask.render_template("errors/method-not-allowed.html"), 405
2024
2025
2026
@app.errorhandler(500)
2027
def e500(error):
2028
return flask.render_template("errors/server-error.html"), 500
2029
2030
2031
@app.errorhandler(400)
2032
def e400(error):
2033
return flask.render_template("errors/bad-request.html"), 400
2034
2035
2036
@app.errorhandler(410)
2037
def e410(error):
2038
return flask.render_template("errors/gone.html"), 410
2039
2040
2041
@app.errorhandler(415)
2042
def e415(error):
2043
return flask.render_template("errors/media-type.html"), 415
2044
2045
2046
if __name__ == "__main__":
2047
app.run(debug=True, port=8080, host="0.0.0.0")
2048
2049
app.register_blueprint(repositories)
2050