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