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