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