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