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