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