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