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