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