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

Dismiss

 app.py

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