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