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