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.98 kiB
Python script, Unicode text, UTF-8 text executable
        
            
1
__version__ = "0.5.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
if post.parent:
1083
flask.abort(400)
1084
1085
post.state = int(flask.request.form["new-state"])
1086
1087
db.session.commit()
1088
1089
return flask.redirect(
1090
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1091
post_id=post_id),
1092
code=303)
1093
1094
1095
@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"])
1096
def repository_forum_reply(username, repository, post_id):
1097
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1098
if not os.path.exists(server_repo_location):
1099
flask.abort(404)
1100
if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level(
1101
flask.session.get("username"), username,
1102
repository) is not None):
1103
flask.abort(403)
1104
1105
if not os.path.exists(server_repo_location):
1106
return flask.render_template("errors/not-found.html"), 404
1107
1108
repo = git.Repo(server_repo_location)
1109
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1110
user = User.query.filter_by(username=flask.session.get("username")).first()
1111
relationships = RepoAccess.query.filter_by(repo=repo_data)
1112
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1113
if not user:
1114
flask.abort(401)
1115
1116
parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1117
post = Post(user, repo_data, parent, flask.request.form["subject"],
1118
flask.request.form["message"])
1119
1120
db.session.add(post)
1121
post.update_date()
1122
db.session.commit()
1123
1124
return flask.redirect(
1125
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1126
post_id=post_id),
1127
code=303)
1128
1129
1130
@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["POST"])
1131
def repository_forum_edit(username, repository, post_id):
1132
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1133
if not os.path.exists(server_repo_location):
1134
flask.abort(404)
1135
if not (get_visibility(username, repository) or get_permission_level(
1136
flask.session.get("username"), username,
1137
repository) is not None):
1138
flask.abort(403)
1139
1140
if not os.path.exists(server_repo_location):
1141
return flask.render_template("errors/not-found.html"), 404
1142
1143
repo = git.Repo(server_repo_location)
1144
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1145
user = User.query.filter_by(username=flask.session.get("username")).first()
1146
relationships = RepoAccess.query.filter_by(repo=repo_data)
1147
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1148
if not user:
1149
flask.abort(401)
1150
post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
1151
if user != post.owner:
1152
flask.abort(403)
1153
1154
post.subject = flask.request.form["subject"]
1155
post.message = flask.request.form["message"]
1156
post.html = markdown.markdown2html(post.message).prettify()
1157
post.update_date()
1158
db.session.commit()
1159
1160
return flask.redirect(
1161
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1162
post_id=post_id),
1163
code=303)
1164
1165
1166
@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["GET"])
1167
def repository_forum_edit_form(username, repository, post_id):
1168
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1169
if not os.path.exists(server_repo_location):
1170
flask.abort(404)
1171
if not (get_visibility(username, repository) or get_permission_level(
1172
flask.session.get("username"), username,
1173
repository) is not None):
1174
flask.abort(403)
1175
1176
if not os.path.exists(server_repo_location):
1177
return flask.render_template("errors/not-found.html"), 404
1178
1179
repo = git.Repo(server_repo_location)
1180
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1181
user = User.query.filter_by(username=flask.session.get("username")).first()
1182
relationships = RepoAccess.query.filter_by(repo=repo_data)
1183
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1184
if not user:
1185
flask.abort(401)
1186
post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
1187
if user != post.owner:
1188
flask.abort(403)
1189
1190
return flask.render_template(
1191
"repo-forum-edit.html",
1192
username=username,
1193
repository=repository,
1194
repo_data=repo_data,
1195
relationships=relationships,
1196
repo=repo,
1197
user_relationship=user_relationship,
1198
post=post,
1199
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1200
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1201
default_branch=repo_data.default_branch
1202
)
1203
1204
@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup",
1205
defaults={"score": 1})
1206
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown",
1207
defaults={"score": -1})
1208
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0})
1209
def repository_forum_vote(username, repository, post_id, score):
1210
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1211
if not os.path.exists(server_repo_location):
1212
flask.abort(404)
1213
if not (get_visibility(username, repository) or get_permission_level(
1214
flask.session.get("username"), username,
1215
repository) is not None):
1216
flask.abort(403)
1217
1218
if not os.path.exists(server_repo_location):
1219
return flask.render_template("errors/not-found.html"), 404
1220
1221
repo = git.Repo(server_repo_location)
1222
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1223
user = User.query.filter_by(username=flask.session.get("username")).first()
1224
relationships = RepoAccess.query.filter_by(repo=repo_data)
1225
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1226
if not user:
1227
flask.abort(401)
1228
1229
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1230
1231
if score:
1232
old_relationship = PostVote.query.filter_by(user_username=user.username,
1233
post_identifier=post.identifier).first()
1234
if old_relationship:
1235
if score == old_relationship.vote_score:
1236
db.session.delete(old_relationship)
1237
post.vote_sum -= old_relationship.vote_score
1238
else:
1239
post.vote_sum -= old_relationship.vote_score
1240
post.vote_sum += score
1241
old_relationship.vote_score = score
1242
else:
1243
relationship = PostVote(user, post, score)
1244
post.vote_sum += score
1245
db.session.add(relationship)
1246
1247
db.session.commit()
1248
1249
user_vote = PostVote.query.filter_by(user_username=user.username,
1250
post_identifier=post.identifier).first()
1251
response = flask.make_response(
1252
str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0))
1253
response.content_type = "text/plain"
1254
1255
return response
1256
1257
1258
@repositories.route("/<username>/<repository>/forum/<int:post_id>/label", methods=["POST"])
1259
def repository_forum_label(username, repository, post_id):
1260
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1261
if not os.path.exists(server_repo_location):
1262
flask.abort(404)
1263
if not get_permission_level(flask.session.get("username"), username, repository):
1264
flask.abort(403)
1265
1266
repo = git.Repo(server_repo_location)
1267
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1268
user = User.query.filter_by(username=flask.session.get("username")).first()
1269
relationships = RepoAccess.query.filter_by(repo=repo_data)
1270
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1271
1272
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1273
1274
if not post:
1275
flask.abort(404)
1276
if post.parent:
1277
flask.abort(400)
1278
1279
label = db.session.get(Label, flask.request.form["label"])
1280
1281
if PostLabel.query.filter_by(post=post, label=label).first():
1282
return flask.redirect(
1283
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1284
post_id=post_id),
1285
code=303)
1286
1287
post_label = PostLabel(post, label)
1288
db.session.add(post_label)
1289
1290
db.session.commit()
1291
1292
return flask.redirect(
1293
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1294
post_id=post_id),
1295
code=303)
1296
1297
1298
@repositories.route("/<username>/<repository>/forum/<int:post_id>/remove-label")
1299
def repository_forum_remove_label(username, repository, post_id):
1300
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1301
if not os.path.exists(server_repo_location):
1302
flask.abort(404)
1303
if not get_permission_level(flask.session.get("username"), username, repository):
1304
flask.abort(403)
1305
1306
repo = git.Repo(server_repo_location)
1307
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1308
user = User.query.filter_by(username=flask.session.get("username")).first()
1309
relationships = RepoAccess.query.filter_by(repo=repo_data)
1310
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1311
1312
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1313
1314
if not post:
1315
flask.abort(404)
1316
if post.parent:
1317
flask.abort(400)
1318
1319
label = db.session.get(Label, flask.request.args["label"])
1320
1321
post_label = PostLabel.query.filter_by(post=post, label=label).first()
1322
db.session.delete(post_label)
1323
1324
db.session.commit()
1325
1326
return flask.redirect(
1327
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1328
post_id=post_id),
1329
code=303)
1330
1331
1332
@repositories.route("/<username>/<repository>/favourite")
1333
def repository_favourite(username, repository):
1334
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1335
if not os.path.exists(server_repo_location):
1336
flask.abort(404)
1337
if not (get_visibility(username, repository) or get_permission_level(
1338
flask.session.get("username"), username,
1339
repository) is not None):
1340
flask.abort(403)
1341
1342
if not os.path.exists(server_repo_location):
1343
return flask.render_template("errors/not-found.html"), 404
1344
1345
repo = git.Repo(server_repo_location)
1346
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1347
user = User.query.filter_by(username=flask.session.get("username")).first()
1348
relationships = RepoAccess.query.filter_by(repo=repo_data)
1349
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1350
if not user:
1351
flask.abort(401)
1352
1353
old_relationship = RepoFavourite.query.filter_by(user_username=user.username,
1354
repo_route=repo_data.route).first()
1355
if old_relationship:
1356
db.session.delete(old_relationship)
1357
else:
1358
relationship = RepoFavourite(user, repo_data)
1359
db.session.add(relationship)
1360
1361
db.session.commit()
1362
1363
return flask.redirect(flask.url_for("favourites"), code=303)
1364
1365
1366
@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"])
1367
def repository_users(username, repository):
1368
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1369
if not os.path.exists(server_repo_location):
1370
flask.abort(404)
1371
if not (get_visibility(username, repository) or get_permission_level(
1372
flask.session.get("username"), username,
1373
repository) is not None):
1374
flask.abort(403)
1375
1376
if not os.path.exists(server_repo_location):
1377
return flask.render_template("errors/not-found.html"), 404
1378
1379
repo = git.Repo(server_repo_location)
1380
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1381
user = User.query.filter_by(username=flask.session.get("username")).first()
1382
relationships = RepoAccess.query.filter_by(repo=repo_data)
1383
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1384
1385
if flask.request.method == "GET":
1386
return flask.render_template(
1387
"repo-users.html",
1388
username=username,
1389
repository=repository,
1390
repo_data=repo_data,
1391
relationships=relationships,
1392
repo=repo,
1393
user_relationship=user_relationship,
1394
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1395
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1396
)
1397
else:
1398
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1399
flask.abort(401)
1400
1401
if flask.request.form.get("new-username"):
1402
# Create new relationship
1403
new_user = User.query.filter_by(
1404
username=flask.request.form.get("new-username")).first()
1405
relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level"))
1406
db.session.add(relationship)
1407
db.session.commit()
1408
if flask.request.form.get("update-username"):
1409
# Create new relationship
1410
updated_user = User.query.filter_by(
1411
username=flask.request.form.get("update-username")).first()
1412
relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first()
1413
if flask.request.form.get("update-level") == -1:
1414
relationship.delete()
1415
else:
1416
relationship.access_level = flask.request.form.get("update-level")
1417
db.session.commit()
1418
1419
return flask.redirect(
1420
app.url_for(".repository_users", username=username, repository=repository))
1421
1422
1423
@repositories.route("/<username>/<repository>/branches/")
1424
def repository_branches(username, repository):
1425
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1426
if not os.path.exists(server_repo_location):
1427
flask.abort(404)
1428
if not (get_visibility(username, repository) or get_permission_level(
1429
flask.session.get("username"), username,
1430
repository) is not None):
1431
flask.abort(403)
1432
1433
if not os.path.exists(server_repo_location):
1434
return flask.render_template("errors/not-found.html"), 404
1435
1436
repo = git.Repo(server_repo_location)
1437
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1438
1439
return flask.render_template(
1440
"repo-branches.html",
1441
username=username,
1442
repository=repository,
1443
repo_data=repo_data,
1444
repo=repo,
1445
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1446
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1447
)
1448
1449
1450
@repositories.route("/<username>/<repository>/log/", defaults={"branch": None})
1451
@repositories.route("/<username>/<repository>/log/<branch>/")
1452
def repository_log(username, repository, branch):
1453
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1454
if not os.path.exists(server_repo_location):
1455
flask.abort(404)
1456
if not (get_visibility(username, repository) or get_permission_level(
1457
flask.session.get("username"), username,
1458
repository) is not None):
1459
flask.abort(403)
1460
1461
if not os.path.exists(server_repo_location):
1462
return flask.render_template("errors/not-found.html"), 404
1463
1464
repo = git.Repo(server_repo_location)
1465
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1466
if not repo_data.default_branch:
1467
if repo.heads:
1468
repo_data.default_branch = repo.heads[0].name
1469
else:
1470
return flask.render_template("empty.html",
1471
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
1472
if not branch:
1473
branch = repo_data.default_branch
1474
return flask.redirect(f"./{branch}", code=302)
1475
1476
if branch.startswith("tag:"):
1477
ref = f"tags/{branch[4:]}"
1478
elif branch.startswith("~"):
1479
ref = branch[1:]
1480
else:
1481
ref = f"heads/{branch}"
1482
1483
ref = ref.replace("~", "/") # encode slashes for URL support
1484
1485
try:
1486
repo.git.checkout("-f", ref)
1487
except git.exc.GitCommandError:
1488
return flask.render_template("errors/not-found.html"), 404
1489
1490
branches = repo.heads
1491
1492
all_refs = []
1493
for ref in repo.heads:
1494
all_refs.append((ref, "head"))
1495
for ref in repo.tags:
1496
all_refs.append((ref, "tag"))
1497
1498
commit_list = [f"/{username}/{repository}/{sha}" for sha in
1499
git_command(server_repo_location, None, "log",
1500
"--format='%H'").decode().split("\n")]
1501
1502
commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc())
1503
page_number = flask.request.args.get("page", 1, type=int)
1504
if flask.session.get("username"):
1505
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1506
else:
1507
default_page_length = 16
1508
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1509
page_listing = db.paginate(commits, page=page_number, per_page=page_length)
1510
1511
if page_listing.has_next:
1512
next_page = page_listing.next_num
1513
else:
1514
next_page = None
1515
1516
if page_listing.has_prev:
1517
prev_page = page_listing.prev_num
1518
else:
1519
prev_page = None
1520
1521
return flask.render_template(
1522
"repo-log.html",
1523
username=username,
1524
repository=repository,
1525
branches=all_refs,
1526
current=branch,
1527
repo_data=repo_data,
1528
repo=repo,
1529
commits=page_listing,
1530
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1531
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1532
page_number=page_number,
1533
page_length=page_length,
1534
next_page=next_page,
1535
prev_page=prev_page,
1536
num_pages=page_listing.pages
1537
)
1538
1539
1540
@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"])
1541
def repository_prs(username, repository):
1542
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1543
if not os.path.exists(server_repo_location):
1544
flask.abort(404)
1545
if not (get_visibility(username, repository) or get_permission_level(
1546
flask.session.get("username"), username,
1547
repository) is not None):
1548
flask.abort(403)
1549
1550
if not os.path.exists(server_repo_location):
1551
return flask.render_template("errors/not-found.html"), 404
1552
1553
if flask.request.method == "GET":
1554
repo = git.Repo(server_repo_location)
1555
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1556
user = User.query.filter_by(username=flask.session.get("username")).first()
1557
1558
return flask.render_template(
1559
"repo-prs.html",
1560
username=username,
1561
repository=repository,
1562
repo_data=repo_data,
1563
repo=repo,
1564
PullRequest=PullRequest,
1565
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1566
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1567
default_branch=repo_data.default_branch,
1568
branches=repo.branches
1569
)
1570
1571
elif "id" not in flask.request.form:
1572
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1573
head = flask.request.form.get("head")
1574
head_route = flask.request.form.get("headroute")
1575
base = flask.request.form.get("base")
1576
1577
if not head and base and head_route:
1578
return flask.redirect(".", 400)
1579
1580
head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/")))
1581
base_repo = git.Repo(server_repo_location)
1582
1583
if head not in head_repo.branches or base not in base_repo.branches:
1584
flask.flash(Markup(
1585
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")),
1586
category="error")
1587
return flask.redirect(".", 303)
1588
1589
head_data = db.session.get(Repo, head_route)
1590
if not head_data.visibility:
1591
flask.flash(Markup(
1592
"<iconify-icon icon='mdi:error'></iconify-icon>" + _(
1593
"Head can't be restricted")),
1594
category="error")
1595
return flask.redirect(".", 303)
1596
1597
pull_request = PullRequest(head_data, head, repo_data, base,
1598
db.session.get(User, flask.session["username"]))
1599
1600
db.session.add(pull_request)
1601
db.session.commit()
1602
1603
# Create the notification
1604
notification = Notification({"type": "pr", "head": pull_request.head.route, "base": pull_request.base.route, "pr": pull_request.id})
1605
db.session.add(notification)
1606
db.session.commit()
1607
1608
# Send a notification to all users who have enabled PR notifications for this repo
1609
for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all():
1610
user = relationship.user
1611
user_notification = UserNotification(user, notification, 1)
1612
db.session.add(user_notification)
1613
db.session.commit()
1614
celery_tasks.send_notification.apply_async(args=[user_notification.id])
1615
1616
return flask.redirect(".", 303)
1617
else:
1618
id = flask.request.form.get("id")
1619
pull_request = db.session.get(PullRequest, id)
1620
1621
if not pull_request:
1622
flask.abort(404)
1623
1624
if not (get_visibility(username, repository) or get_permission_level(
1625
flask.session.get("username"), username,
1626
repository) >= 1 or pull_request.owner.username == flask.session.get("username")):
1627
flask.abort(403)
1628
1629
if not get_permission_level(flask.session.get("username"), username, repository):
1630
flask.abort(401)
1631
1632
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1633
1634
if pull_request:
1635
pull_request.resolves_list = flask.request.form.get("resolves")
1636
db.session.commit()
1637
1638
return flask.redirect(".", 303)
1639
1640
1641
@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"])
1642
def repository_prs_merge(username, repository):
1643
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1644
if not os.path.exists(server_repo_location):
1645
flask.abort(404)
1646
if not (get_visibility(username, repository) or get_permission_level(
1647
flask.session.get("username"), username,
1648
repository) is not None):
1649
flask.abort(403)
1650
1651
if not get_permission_level(flask.session.get("username"), username, repository):
1652
flask.abort(401)
1653
1654
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1655
repo = git.Repo(server_repo_location)
1656
id = flask.request.form.get("id")
1657
1658
pull_request = db.session.get(PullRequest, id)
1659
1660
if pull_request:
1661
result = celery_tasks.merge_heads.delay(
1662
pull_request.head_route,
1663
pull_request.head_branch,
1664
pull_request.base_route,
1665
pull_request.base_branch,
1666
pull_request.id,
1667
simulate=True
1668
)
1669
task_result = worker.AsyncResult(result.id)
1670
1671
return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) # should be 202 Accepted but we must use a redirect
1672
# db.session.delete(pull_request)
1673
# db.session.commit()
1674
else:
1675
flask.abort(400)
1676
1677
1678
@repositories.route("/<username>/<repository>/prs/<int:id>/merge")
1679
def repository_prs_merge_stage_two(username, repository, id):
1680
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1681
if not os.path.exists(server_repo_location):
1682
flask.abort(404)
1683
if not (get_visibility(username, repository) or get_permission_level(
1684
flask.session.get("username"), username,
1685
repository) is not None):
1686
flask.abort(403)
1687
1688
if not get_permission_level(flask.session.get("username"), username, repository):
1689
flask.abort(401)
1690
1691
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1692
repo = git.Repo(server_repo_location)
1693
1694
pull_request = db.session.get(PullRequest, id)
1695
1696
if pull_request:
1697
result = celery_tasks.merge_heads.delay(
1698
pull_request.head_route,
1699
pull_request.head_branch,
1700
pull_request.base_route,
1701
pull_request.base_branch,
1702
pull_request.id,
1703
simulate=False
1704
)
1705
task_result = worker.AsyncResult(result.id)
1706
1707
db.session.commit()
1708
1709
return flask.redirect(f"/task/{result.id}?pr-id={id}", 303)
1710
# db.session.delete(pull_request)
1711
else:
1712
flask.abort(400)
1713
1714
1715
@app.route("/task/<task_id>")
1716
def task_monitor(task_id):
1717
task_result = worker.AsyncResult(task_id)
1718
1719
if flask.request.args.get("partial"):
1720
# htmx partial update
1721
return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000)
1722
1723
# Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long
1724
return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125)
1725
1726
1727
@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"])
1728
def repository_prs_delete(username, repository):
1729
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1730
if not os.path.exists(server_repo_location):
1731
flask.abort(404)
1732
if not (get_visibility(username, repository) or get_permission_level(
1733
flask.session.get("username"), username,
1734
repository) is not None):
1735
flask.abort(403)
1736
1737
if not get_permission_level(flask.session.get("username"), username, repository):
1738
flask.abort(401)
1739
1740
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1741
repo = git.Repo(server_repo_location)
1742
id = flask.request.form.get("id")
1743
1744
pull_request = db.session.get(PullRequest, id)
1745
1746
if pull_request:
1747
pull_request.state = 2
1748
db.session.commit()
1749
1750
return flask.redirect(".", 303)
1751
1752
1753
@repositories.route("/<username>/<repository>/settings/")
1754
def repository_settings(username, repository):
1755
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1756
flask.abort(401)
1757
1758
repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository))
1759
1760
site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>")
1761
primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>")
1762
1763
return flask.render_template("repo-settings.html", username=username, repository=repository,
1764
repo_data=db.session.get(Repo, f"/{username}/{repository}"),
1765
branches=[branch.name for branch in repo.branches],
1766
site_link=site_link, primary_site_link=primary_site_link,
1767
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1768
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1769
)
1770
1771
1772
@repositories.route("/<username>/<repository>/settings/", methods=["POST"])
1773
def repository_settings_post(username, repository):
1774
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1775
flask.abort(401)
1776
1777
repo = db.session.get(Repo, f"/{username}/{repository}")
1778
1779
repo.visibility = flask.request.form.get("visibility", type=int)
1780
repo.info = flask.request.form.get("description")
1781
repo.default_branch = flask.request.form.get("default_branch")
1782
repo.url = flask.request.form.get("url")
1783
1784
# Update site settings
1785
had_site = repo.has_site
1786
old_branch = repo.site_branch
1787
if flask.request.form.get("site_branch"):
1788
repo.site_branch = flask.request.form.get("site_branch")
1789
if flask.request.form.get("primary_site"):
1790
if had_site != 2:
1791
# Remove primary site from other repos
1792
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2):
1793
other_repo.has_site = 1 # switch it to a regular site
1794
flask.flash(Markup(
1795
_("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(
1796
repository=other_repo.route
1797
)), category="warning")
1798
repo.has_site = 2
1799
else:
1800
repo.has_site = 1
1801
else:
1802
repo.site_branch = None
1803
repo.has_site = 0
1804
1805
db.session.commit()
1806
1807
if not (had_site, old_branch) == (repo.has_site, repo.site_branch):
1808
# Deploy the newly activated site
1809
result = celery_tasks.copy_site.delay(repo.route)
1810
1811
if had_site and not repo.has_site:
1812
# Remove the site
1813
result = celery_tasks.delete_site.delay(repo.route)
1814
1815
if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site):
1816
# Deploy all other sites which were destroyed by the primary site
1817
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1):
1818
result = celery_tasks.copy_site.delay(other_repo.route)
1819
1820
return flask.redirect(f"/{username}/{repository}/settings", 303)
1821
1822
1823
@repositories.route("/<username>/<repository>/settings/add-label", methods=["POST"])
1824
def repository_settings_add_label(username, repository):
1825
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1826
flask.abort(401)
1827
1828
repo_data = db.session.get(Repo, f"/{username}/{repository}")
1829
1830
label = Label(repo_data, flask.request.form.get("label"), flask.request.form.get("colour"))
1831
db.session.add(label)
1832
db.session.commit()
1833
1834
return flask.redirect(f"/{username}/{repository}/settings", 303)
1835
1836
1837
@repositories.route("/<username>/<repository>/settings/delete-label", methods=["POST"])
1838
def repository_settings_delete_label(username, repository):
1839
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1840
flask.abort(401)
1841
1842
repo_data = db.session.get(Repo, f"/{username}/{repository}")
1843
1844
label = db.session.get(Label, flask.request.form.get("id"))
1845
1846
db.session.delete(label)
1847
db.session.commit()
1848
1849
return flask.redirect(f"/{username}/{repository}/settings", 303)
1850
1851
1852
@repositories.route("/<username>/<repository>/settings/edit-label", methods=["POST"])
1853
def repository_settings_edit_label(username, repository):
1854
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1855
flask.abort(401)
1856
1857
repo_data = db.session.get(Repo, f"/{username}/{repository}")
1858
1859
label = db.session.get(Label, flask.request.form.get("id"))
1860
1861
label.name = flask.request.form.get("label")
1862
label.colour_hex = flask.request.form.get("colour")
1863
1864
db.session.commit()
1865
1866
return flask.redirect(f"/{username}/{repository}/settings", 303)
1867
1868
1869
@repositories.route("/<username>/<repository>/settings/delete", methods=["POST"])
1870
def repository_settings_delete(username, repository):
1871
if username != flask.session.get("username"):
1872
flask.abort(401)
1873
1874
repo = db.session.get(Repo, f"/{username}/{repository}")
1875
1876
if not repo:
1877
flask.abort(404)
1878
1879
user = db.session.get(User, flask.session.get("username"))
1880
1881
if not bcrypt.check_password_hash(user.password_hashed, flask.request.form.get("password")):
1882
flask.flash(_("Incorrect password"), category="error")
1883
flask.abort(401)
1884
1885
if repo.has_site:
1886
celery_tasks.delete_site.delay(repo.route)
1887
1888
db.session.delete(repo)
1889
db.session.commit()
1890
1891
shutil.rmtree(os.path.join(config.REPOS_PATH, username, repository))
1892
1893
return flask.redirect(f"/{username}", 303)
1894
1895
1896
@app.errorhandler(404)
1897
def e404(error):
1898
return flask.render_template("errors/not-found.html"), 404
1899
1900
1901
@app.errorhandler(401)
1902
def e401(error):
1903
return flask.render_template("errors/unauthorised.html"), 401
1904
1905
1906
@app.errorhandler(403)
1907
def e403(error):
1908
return flask.render_template("errors/forbidden.html"), 403
1909
1910
1911
@app.errorhandler(418)
1912
def e418(error):
1913
return flask.render_template("errors/teapot.html"), 418
1914
1915
1916
@app.errorhandler(405)
1917
def e405(error):
1918
return flask.render_template("errors/method-not-allowed.html"), 405
1919
1920
1921
@app.errorhandler(500)
1922
def e500(error):
1923
return flask.render_template("errors/server-error.html"), 500
1924
1925
1926
@app.errorhandler(400)
1927
def e400(error):
1928
return flask.render_template("errors/bad-request.html"), 400
1929
1930
1931
@app.errorhandler(410)
1932
def e410(error):
1933
return flask.render_template("errors/gone.html"), 410
1934
1935
1936
@app.errorhandler(415)
1937
def e415(error):
1938
return flask.render_template("errors/media-type.html"), 415
1939
1940
1941
if __name__ == "__main__":
1942
app.run(debug=True, port=8080, host="0.0.0.0")
1943
1944
app.register_blueprint(repositories)
1945