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