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