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