You're looking at it

Homepage: https://roundabout-host.com

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