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