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