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) 637) 638 639 640@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 641def repository_forum_reply(username, repository, post_id): 642if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 643repository) is not None): 644flask.abort(403) 645 646server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 647 648app.logger.info(f"Loading {server_repo_location}") 649 650if not os.path.exists(server_repo_location): 651app.logger.error(f"Cannot load {server_repo_location}") 652return flask.render_template("not-found.html"), 404 653 654repo = git.Repo(server_repo_location) 655repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 656user = User.query.filter_by(username=flask.session.get("username")).first() 657relationships = RepoAccess.query.filter_by(repo=repo_data) 658user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 659if not user: 660flask.abort(401) 661 662parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 663post = Post(user, repo_data, parent, flask.request.form["subject"], flask.request.form["message"]) 664 665db.session.add(post) 666post.update_date() 667db.session.commit() 668 669return flask.redirect( 670flask.url_for(".repository_forum_thread", username=username, repository=repository, post_id=post_id), 671code=303) 672 673 674@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", defaults={"score": 1}) 675@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", defaults={"score": -1}) 676@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 677def repository_forum_vote(username, repository, post_id, score): 678if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 679repository) is not None): 680flask.abort(403) 681 682server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 683 684app.logger.info(f"Loading {server_repo_location}") 685 686if not os.path.exists(server_repo_location): 687app.logger.error(f"Cannot load {server_repo_location}") 688return flask.render_template("not-found.html"), 404 689 690repo = git.Repo(server_repo_location) 691repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 692user = User.query.filter_by(username=flask.session.get("username")).first() 693relationships = RepoAccess.query.filter_by(repo=repo_data) 694user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 695if not user: 696flask.abort(401) 697 698post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 699 700if score: 701old_relationship = PostVote.query.filter_by(user_username=user.username, 702post_identifier=post.identifier).first() 703if old_relationship: 704if score == old_relationship.vote_score: 705db.session.delete(old_relationship) 706post.vote_sum -= old_relationship.vote_score 707else: 708post.vote_sum -= old_relationship.vote_score 709post.vote_sum += score 710old_relationship.vote_score = score 711else: 712relationship = PostVote(user, post, score) 713post.vote_sum += score 714db.session.add(relationship) 715 716db.session.commit() 717 718user_vote = PostVote.query.filter_by(user_username=user.username, post_identifier=post.identifier).first() 719response = flask.make_response(str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 720response.content_type = "text/plain" 721 722return response 723 724 725@repositories.route("/<username>/<repository>/favourite") 726def repository_favourite(username, repository): 727if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 728repository) is not None): 729flask.abort(403) 730 731server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 732 733app.logger.info(f"Loading {server_repo_location}") 734 735if not os.path.exists(server_repo_location): 736app.logger.error(f"Cannot load {server_repo_location}") 737return flask.render_template("not-found.html"), 404 738 739repo = git.Repo(server_repo_location) 740repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 741user = User.query.filter_by(username=flask.session.get("username")).first() 742relationships = RepoAccess.query.filter_by(repo=repo_data) 743user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 744if not user: 745flask.abort(401) 746 747old_relationship = RepoFavourite.query.filter_by(user_username=user.username, repo_route=repo_data.route).first() 748if old_relationship: 749db.session.delete(old_relationship) 750else: 751relationship = RepoFavourite(user, repo_data) 752db.session.add(relationship) 753 754db.session.commit() 755 756return flask.redirect(flask.url_for("favourites"), code=303) 757 758 759@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 760def repository_users(username, repository): 761if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 762repository) is not None): 763flask.abort(403) 764 765server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 766 767app.logger.info(f"Loading {server_repo_location}") 768 769if not os.path.exists(server_repo_location): 770app.logger.error(f"Cannot load {server_repo_location}") 771return flask.render_template("not-found.html"), 404 772 773repo = git.Repo(server_repo_location) 774repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 775user = User.query.filter_by(username=flask.session.get("username")).first() 776relationships = RepoAccess.query.filter_by(repo=repo_data) 777user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 778 779if flask.request.method == "GET": 780return flask.render_template( 781"repo-users.html", 782username=username, 783repository=repository, 784repo_data=repo_data, 785relationships=relationships, 786repo=repo, 787user_relationship=user_relationship, 788remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 789is_favourite=get_favourite(flask.session.get("username"), username, repository) 790) 791else: 792if get_permission_level(flask.session.get("username"), username, repository) != 2: 793flask.abort(401) 794 795if flask.request.form.get("new-username"): 796# Create new relationship 797new_user = User.query.filter_by(username=flask.request.form.get("new-username")).first() 798relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 799db.session.add(relationship) 800db.session.commit() 801if flask.request.form.get("update-username"): 802# Create new relationship 803updated_user = User.query.filter_by(username=flask.request.form.get("update-username")).first() 804relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 805if flask.request.form.get("update-level") == -1: 806relationship.delete() 807else: 808relationship.access_level = flask.request.form.get("update-level") 809db.session.commit() 810 811return flask.redirect(app.url_for(".repository_users", username=username, repository=repository)) 812 813 814@repositories.route("/<username>/<repository>/branches/") 815def repository_branches(username, repository): 816if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 817repository) is not None): 818flask.abort(403) 819 820server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 821 822app.logger.info(f"Loading {server_repo_location}") 823 824if not os.path.exists(server_repo_location): 825app.logger.error(f"Cannot load {server_repo_location}") 826return flask.render_template("not-found.html"), 404 827 828repo = git.Repo(server_repo_location) 829repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 830 831return flask.render_template( 832"repo-branches.html", 833username=username, 834repository=repository, 835repo_data=repo_data, 836repo=repo, 837remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 838is_favourite=get_favourite(flask.session.get("username"), username, repository) 839) 840 841 842@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 843@repositories.route("/<username>/<repository>/log/<branch>/") 844def repository_log(username, repository, branch): 845if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 846repository) is not None): 847flask.abort(403) 848 849server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 850 851app.logger.info(f"Loading {server_repo_location}") 852 853if not os.path.exists(server_repo_location): 854app.logger.error(f"Cannot load {server_repo_location}") 855return flask.render_template("not-found.html"), 404 856 857repo = git.Repo(server_repo_location) 858repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 859if not repo_data.default_branch: 860if repo.heads: 861repo_data.default_branch = repo.heads[0].name 862else: 863return flask.render_template("empty.html", 864remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 865if not branch: 866branch = repo_data.default_branch 867return flask.redirect(f"./{branch}", code=302) 868 869if branch.startswith("tag:"): 870ref = f"tags/{branch[4:]}" 871elif branch.startswith("~"): 872ref = branch[1:] 873else: 874ref = f"heads/{branch}" 875 876ref = ref.replace("~", "/") # encode slashes for URL support 877 878try: 879repo.git.checkout("-f", ref) 880except git.exc.GitCommandError: 881return flask.render_template("not-found.html"), 404 882 883branches = repo.heads 884 885all_refs = [] 886for ref in repo.heads: 887all_refs.append((ref, "head")) 888for ref in repo.tags: 889all_refs.append((ref, "tag")) 890 891commit_list = [f"/{username}/{repository}/{sha}" for sha in 892git_command(server_repo_location, None, "log", "--format='%H'").decode().split("\n")] 893 894commits = Commit.query.filter(Commit.identifier.in_(commit_list)) 895 896return flask.render_template( 897"repo-log.html", 898username=username, 899repository=repository, 900branches=all_refs, 901current=branch, 902repo_data=repo_data, 903repo=repo, 904commits=commits, 905remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 906is_favourite=get_favourite(flask.session.get("username"), username, repository) 907) 908 909 910@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 911def repository_prs(username, repository): 912if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 913repository) is not None): 914flask.abort(403) 915 916server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 917 918app.logger.info(f"Loading {server_repo_location}") 919 920if not os.path.exists(server_repo_location): 921app.logger.error(f"Cannot load {server_repo_location}") 922return flask.render_template("not-found.html"), 404 923 924if flask.request.method == "GET": 925repo = git.Repo(server_repo_location) 926repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 927user = User.query.filter_by(username=flask.session.get("username")).first() 928 929return flask.render_template( 930"repo-prs.html", 931username=username, 932repository=repository, 933repo_data=repo_data, 934repo=repo, 935PullRequest=PullRequest, 936remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 937is_favourite=get_favourite(flask.session.get("username"), username, repository), 938default_branch=repo_data.default_branch, 939branches=repo.branches 940) 941 942else: 943repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 944head = flask.request.form.get("head") 945head_route = flask.request.form.get("headroute") 946base = flask.request.form.get("base") 947 948if not head and base and head_route: 949return flask.redirect(".", 400) 950 951head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 952base_repo = git.Repo(server_repo_location) 953print(head_repo) 954 955if head not in head_repo.branches or base not in base_repo.branches: 956flask.flash(Markup( 957"<iconify-icon icon='mdi:error'></iconify-icon>Bad branch name"), 958category="error") 959return flask.redirect(".", 303) 960 961head_data = db.session.get(Repo, head_route) 962if not head_data.visibility: 963flask.flash(Markup( 964"<iconify-icon icon='mdi:error'></iconify-icon>Head can't be restricted"), 965category="error") 966return flask.redirect(".", 303) 967 968pull_request = PullRequest(repo_data, head, head_data, base, db.session.get(User, flask.session["username"])) 969 970db.session.add(pull_request) 971db.session.commit() 972 973return flask.redirect(".", 303) 974 975 976@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 977def repository_prs_merge(username, repository): 978if not get_permission_level(flask.session.get("username"), username, repository): 979flask.abort(401) 980 981server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 982repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 983repo = git.Repo(server_repo_location) 984id = flask.request.form.get("id") 985 986pull_request = db.session.get(PullRequest, id) 987 988if pull_request: 989result = celery_tasks.merge_heads.delay( 990pull_request.head_route, 991pull_request.head_branch, 992pull_request.base_route, 993pull_request.base_branch, 994simulate=True 995) 996task_result = worker.AsyncResult(result.id) 997 998return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 999# db.session.delete(pull_request) 1000# db.session.commit() 1001else: 1002flask.abort(400) 1003 1004 1005@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1006def repository_prs_merge_stage_two(username, repository, id): 1007if not get_permission_level(flask.session.get("username"), username, repository): 1008flask.abort(401) 1009 1010server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1011repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1012repo = git.Repo(server_repo_location) 1013 1014pull_request = db.session.get(PullRequest, id) 1015 1016if pull_request: 1017result = celery_tasks.merge_heads.delay( 1018pull_request.head_route, 1019pull_request.head_branch, 1020pull_request.base_route, 1021pull_request.base_branch, 1022simulate=False 1023) 1024task_result = worker.AsyncResult(result.id) 1025 1026return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1027# db.session.delete(pull_request) 1028# db.session.commit() 1029else: 1030flask.abort(400) 1031 1032 1033@app.route("/task/<task_id>") 1034def task_monitor(task_id): 1035task_result = worker.AsyncResult(task_id) 1036print(task_result.status) 1037 1038return flask.render_template("task-monitor.html", result=task_result) 1039 1040 1041@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1042def repository_prs_delete(username, repository): 1043if not get_permission_level(flask.session.get("username"), username, repository): 1044flask.abort(401) 1045 1046server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1047repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1048repo = git.Repo(server_repo_location) 1049id = flask.request.form.get("id") 1050 1051pull_request = db.session.get(PullRequest, id) 1052 1053if pull_request: 1054db.session.delete(pull_request) 1055db.session.commit() 1056 1057return flask.redirect(".", 303) 1058 1059 1060@repositories.route("/<username>/<repository>/settings/") 1061def repository_settings(username, repository): 1062if get_permission_level(flask.session.get("username"), username, repository) != 2: 1063flask.abort(401) 1064 1065return flask.render_template("repo-settings.html", username=username, repository=repository) 1066 1067 1068@app.errorhandler(404) 1069def e404(error): 1070return flask.render_template("not-found.html"), 404 1071 1072 1073@app.errorhandler(401) 1074def e401(error): 1075return flask.render_template("unauthorised.html"), 401 1076 1077 1078@app.errorhandler(403) 1079def e403(error): 1080return flask.render_template("forbidden.html"), 403 1081 1082 1083@app.errorhandler(418) 1084def e418(error): 1085return flask.render_template("teapot.html"), 418 1086 1087 1088@app.errorhandler(405) 1089def e405(error): 1090return flask.render_template("method-not-allowed.html"), 405 1091 1092 1093if __name__ == "__main__": 1094app.run(debug=True, port=8080, host="0.0.0.0") 1095 1096app.register_blueprint(repositories) 1097