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} 630) 631 632 633@repositories.route("/<username>/<repository>/forum/") 634def repository_forum(username, repository): 635server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 636if not os.path.exists(server_repo_location): 637app.logger.error(f"Cannot load {server_repo_location}") 638flask.abort(404) 639if not (get_visibility(username, repository) or get_permission_level( 640flask.session.get("username"), username, 641repository) is not None): 642flask.abort(403) 643 644app.logger.info(f"Loading {server_repo_location}") 645 646if not os.path.exists(server_repo_location): 647app.logger.error(f"Cannot load {server_repo_location}") 648return flask.render_template("not-found.html"), 404 649 650repo = git.Repo(server_repo_location) 651repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 652user = User.query.filter_by(username=flask.session.get("username")).first() 653relationships = RepoAccess.query.filter_by(repo=repo_data) 654user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 655 656return flask.render_template( 657"repo-forum.html", 658username=username, 659repository=repository, 660repo_data=repo_data, 661relationships=relationships, 662repo=repo, 663user_relationship=user_relationship, 664Post=Post, 665remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 666is_favourite=get_favourite(flask.session.get("username"), username, repository), 667default_branch=repo_data.default_branch 668) 669 670 671@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 672def repository_forum_topic(username, repository, id): 673server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 674if not os.path.exists(server_repo_location): 675app.logger.error(f"Cannot load {server_repo_location}") 676flask.abort(404) 677if not (get_visibility(username, repository) or get_permission_level( 678flask.session.get("username"), username, 679repository) is not None): 680flask.abort(403) 681 682app.logger.info(f"Loading {server_repo_location}") 683 684if not os.path.exists(server_repo_location): 685app.logger.error(f"Cannot load {server_repo_location}") 686return flask.render_template("not-found.html"), 404 687 688repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 689user = User.query.filter_by(username=flask.session.get("username")).first() 690relationships = RepoAccess.query.filter_by(repo=repo_data) 691user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 692 693post = Post.query.filter_by(id=id).first() 694 695return flask.render_template( 696"repo-topic.html", 697username=username, 698repository=repository, 699repo_data=repo_data, 700relationships=relationships, 701user_relationship=user_relationship, 702post=post, 703remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 704is_favourite=get_favourite(flask.session.get("username"), username, repository), 705default_branch=repo_data.default_branch 706) 707 708 709@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 710def repository_forum_new(username, repository): 711server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 712if not os.path.exists(server_repo_location): 713app.logger.error(f"Cannot load {server_repo_location}") 714flask.abort(404) 715if not (get_visibility(username, repository) or get_permission_level( 716flask.session.get("username"), username, 717repository) is not None): 718flask.abort(403) 719 720app.logger.info(f"Loading {server_repo_location}") 721 722if not os.path.exists(server_repo_location): 723app.logger.error(f"Cannot load {server_repo_location}") 724return flask.render_template("not-found.html"), 404 725 726repo = git.Repo(server_repo_location) 727repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 728user = User.query.filter_by(username=flask.session.get("username")).first() 729relationships = RepoAccess.query.filter_by(repo=repo_data) 730user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 731 732post = Post(user, repo_data, None, flask.request.form["subject"], 733flask.request.form["message"]) 734 735db.session.add(post) 736db.session.commit() 737 738return flask.redirect( 739flask.url_for(".repository_forum_thread", username=username, repository=repository, 740post_id=post.number), 741code=303) 742 743 744@repositories.route("/<username>/<repository>/forum/<int:post_id>") 745def repository_forum_thread(username, repository, post_id): 746server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 747if not os.path.exists(server_repo_location): 748app.logger.error(f"Cannot load {server_repo_location}") 749flask.abort(404) 750if not (get_visibility(username, repository) or get_permission_level( 751flask.session.get("username"), username, 752repository) is not None): 753flask.abort(403) 754 755app.logger.info(f"Loading {server_repo_location}") 756 757if not os.path.exists(server_repo_location): 758app.logger.error(f"Cannot load {server_repo_location}") 759return flask.render_template("not-found.html"), 404 760 761repo = git.Repo(server_repo_location) 762repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 763user = User.query.filter_by(username=flask.session.get("username")).first() 764relationships = RepoAccess.query.filter_by(repo=repo_data) 765user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 766 767return flask.render_template( 768"repo-forum-thread.html", 769username=username, 770repository=repository, 771repo_data=repo_data, 772relationships=relationships, 773repo=repo, 774Post=Post, 775user_relationship=user_relationship, 776post_id=post_id, 777max_post_nesting=4, 778remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 779is_favourite=get_favourite(flask.session.get("username"), username, repository), 780parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 781) 782 783 784@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 785def repository_forum_reply(username, repository, post_id): 786server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 787if not os.path.exists(server_repo_location): 788app.logger.error(f"Cannot load {server_repo_location}") 789flask.abort(404) 790if not (get_visibility(username, repository) or get_permission_level( 791flask.session.get("username"), username, 792repository) is not None): 793flask.abort(403) 794 795app.logger.info(f"Loading {server_repo_location}") 796 797if not os.path.exists(server_repo_location): 798app.logger.error(f"Cannot load {server_repo_location}") 799return flask.render_template("not-found.html"), 404 800 801repo = git.Repo(server_repo_location) 802repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 803user = User.query.filter_by(username=flask.session.get("username")).first() 804relationships = RepoAccess.query.filter_by(repo=repo_data) 805user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 806if not user: 807flask.abort(401) 808 809parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 810post = Post(user, repo_data, parent, flask.request.form["subject"], 811flask.request.form["message"]) 812 813db.session.add(post) 814post.update_date() 815db.session.commit() 816 817return flask.redirect( 818flask.url_for(".repository_forum_thread", username=username, repository=repository, 819post_id=post_id), 820code=303) 821 822 823@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 824defaults={"score": 1}) 825@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 826defaults={"score": -1}) 827@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 828def repository_forum_vote(username, repository, post_id, score): 829server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 830if not os.path.exists(server_repo_location): 831app.logger.error(f"Cannot load {server_repo_location}") 832flask.abort(404) 833if not (get_visibility(username, repository) or get_permission_level( 834flask.session.get("username"), username, 835repository) is not None): 836flask.abort(403) 837 838app.logger.info(f"Loading {server_repo_location}") 839 840if not os.path.exists(server_repo_location): 841app.logger.error(f"Cannot load {server_repo_location}") 842return flask.render_template("not-found.html"), 404 843 844repo = git.Repo(server_repo_location) 845repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 846user = User.query.filter_by(username=flask.session.get("username")).first() 847relationships = RepoAccess.query.filter_by(repo=repo_data) 848user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 849if not user: 850flask.abort(401) 851 852post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 853 854if score: 855old_relationship = PostVote.query.filter_by(user_username=user.username, 856post_identifier=post.identifier).first() 857if old_relationship: 858if score == old_relationship.vote_score: 859db.session.delete(old_relationship) 860post.vote_sum -= old_relationship.vote_score 861else: 862post.vote_sum -= old_relationship.vote_score 863post.vote_sum += score 864old_relationship.vote_score = score 865else: 866relationship = PostVote(user, post, score) 867post.vote_sum += score 868db.session.add(relationship) 869 870db.session.commit() 871 872user_vote = PostVote.query.filter_by(user_username=user.username, 873post_identifier=post.identifier).first() 874response = flask.make_response( 875str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 876response.content_type = "text/plain" 877 878return response 879 880 881@repositories.route("/<username>/<repository>/favourite") 882def repository_favourite(username, repository): 883server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 884if not os.path.exists(server_repo_location): 885app.logger.error(f"Cannot load {server_repo_location}") 886flask.abort(404) 887if not (get_visibility(username, repository) or get_permission_level( 888flask.session.get("username"), username, 889repository) is not None): 890flask.abort(403) 891 892app.logger.info(f"Loading {server_repo_location}") 893 894if not os.path.exists(server_repo_location): 895app.logger.error(f"Cannot load {server_repo_location}") 896return flask.render_template("not-found.html"), 404 897 898repo = git.Repo(server_repo_location) 899repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 900user = User.query.filter_by(username=flask.session.get("username")).first() 901relationships = RepoAccess.query.filter_by(repo=repo_data) 902user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 903if not user: 904flask.abort(401) 905 906old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 907repo_route=repo_data.route).first() 908if old_relationship: 909db.session.delete(old_relationship) 910else: 911relationship = RepoFavourite(user, repo_data) 912db.session.add(relationship) 913 914db.session.commit() 915 916return flask.redirect(flask.url_for("favourites"), code=303) 917 918 919@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 920def repository_users(username, repository): 921server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 922if not os.path.exists(server_repo_location): 923app.logger.error(f"Cannot load {server_repo_location}") 924flask.abort(404) 925if not (get_visibility(username, repository) or get_permission_level( 926flask.session.get("username"), username, 927repository) is not None): 928flask.abort(403) 929 930app.logger.info(f"Loading {server_repo_location}") 931 932if not os.path.exists(server_repo_location): 933app.logger.error(f"Cannot load {server_repo_location}") 934return flask.render_template("not-found.html"), 404 935 936repo = git.Repo(server_repo_location) 937repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 938user = User.query.filter_by(username=flask.session.get("username")).first() 939relationships = RepoAccess.query.filter_by(repo=repo_data) 940user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 941 942if flask.request.method == "GET": 943return flask.render_template( 944"repo-users.html", 945username=username, 946repository=repository, 947repo_data=repo_data, 948relationships=relationships, 949repo=repo, 950user_relationship=user_relationship, 951remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 952is_favourite=get_favourite(flask.session.get("username"), username, repository) 953) 954else: 955if get_permission_level(flask.session.get("username"), username, repository) != 2: 956flask.abort(401) 957 958if flask.request.form.get("new-username"): 959# Create new relationship 960new_user = User.query.filter_by( 961username=flask.request.form.get("new-username")).first() 962relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 963db.session.add(relationship) 964db.session.commit() 965if flask.request.form.get("update-username"): 966# Create new relationship 967updated_user = User.query.filter_by( 968username=flask.request.form.get("update-username")).first() 969relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 970if flask.request.form.get("update-level") == -1: 971relationship.delete() 972else: 973relationship.access_level = flask.request.form.get("update-level") 974db.session.commit() 975 976return flask.redirect( 977app.url_for(".repository_users", username=username, repository=repository)) 978 979 980@repositories.route("/<username>/<repository>/branches/") 981def repository_branches(username, repository): 982server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 983if not os.path.exists(server_repo_location): 984app.logger.error(f"Cannot load {server_repo_location}") 985flask.abort(404) 986if not (get_visibility(username, repository) or get_permission_level( 987flask.session.get("username"), username, 988repository) is not None): 989flask.abort(403) 990 991app.logger.info(f"Loading {server_repo_location}") 992 993if not os.path.exists(server_repo_location): 994app.logger.error(f"Cannot load {server_repo_location}") 995return flask.render_template("not-found.html"), 404 996 997repo = git.Repo(server_repo_location) 998repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 999 1000return flask.render_template( 1001"repo-branches.html", 1002username=username, 1003repository=repository, 1004repo_data=repo_data, 1005repo=repo, 1006remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1007is_favourite=get_favourite(flask.session.get("username"), username, repository) 1008) 1009 1010 1011@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1012@repositories.route("/<username>/<repository>/log/<branch>/") 1013def repository_log(username, repository, branch): 1014server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1015if not os.path.exists(server_repo_location): 1016app.logger.error(f"Cannot load {server_repo_location}") 1017flask.abort(404) 1018if not (get_visibility(username, repository) or get_permission_level( 1019flask.session.get("username"), username, 1020repository) is not None): 1021flask.abort(403) 1022 1023app.logger.info(f"Loading {server_repo_location}") 1024 1025if not os.path.exists(server_repo_location): 1026app.logger.error(f"Cannot load {server_repo_location}") 1027return flask.render_template("not-found.html"), 404 1028 1029repo = git.Repo(server_repo_location) 1030repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1031if not repo_data.default_branch: 1032if repo.heads: 1033repo_data.default_branch = repo.heads[0].name 1034else: 1035return flask.render_template("empty.html", 1036remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1037if not branch: 1038branch = repo_data.default_branch 1039return flask.redirect(f"./{branch}", code=302) 1040 1041if branch.startswith("tag:"): 1042ref = f"tags/{branch[4:]}" 1043elif branch.startswith("~"): 1044ref = branch[1:] 1045else: 1046ref = f"heads/{branch}" 1047 1048ref = ref.replace("~", "/") # encode slashes for URL support 1049 1050try: 1051repo.git.checkout("-f", ref) 1052except git.exc.GitCommandError: 1053return flask.render_template("not-found.html"), 404 1054 1055branches = repo.heads 1056 1057all_refs = [] 1058for ref in repo.heads: 1059all_refs.append((ref, "head")) 1060for ref in repo.tags: 1061all_refs.append((ref, "tag")) 1062 1063commit_list = [f"/{username}/{repository}/{sha}" for sha in 1064git_command(server_repo_location, None, "log", 1065"--format='%H'").decode().split("\n")] 1066 1067commits = Commit.query.filter(Commit.identifier.in_(commit_list)) 1068 1069return flask.render_template( 1070"repo-log.html", 1071username=username, 1072repository=repository, 1073branches=all_refs, 1074current=branch, 1075repo_data=repo_data, 1076repo=repo, 1077commits=commits, 1078remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1079is_favourite=get_favourite(flask.session.get("username"), username, repository) 1080) 1081 1082 1083@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1084def repository_prs(username, repository): 1085server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1086if not os.path.exists(server_repo_location): 1087app.logger.error(f"Cannot load {server_repo_location}") 1088flask.abort(404) 1089if not (get_visibility(username, repository) or get_permission_level( 1090flask.session.get("username"), username, 1091repository) is not None): 1092flask.abort(403) 1093 1094app.logger.info(f"Loading {server_repo_location}") 1095 1096if not os.path.exists(server_repo_location): 1097app.logger.error(f"Cannot load {server_repo_location}") 1098return flask.render_template("not-found.html"), 404 1099 1100if flask.request.method == "GET": 1101repo = git.Repo(server_repo_location) 1102repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1103user = User.query.filter_by(username=flask.session.get("username")).first() 1104 1105return flask.render_template( 1106"repo-prs.html", 1107username=username, 1108repository=repository, 1109repo_data=repo_data, 1110repo=repo, 1111PullRequest=PullRequest, 1112remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1113is_favourite=get_favourite(flask.session.get("username"), username, repository), 1114default_branch=repo_data.default_branch, 1115branches=repo.branches 1116) 1117 1118else: 1119repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1120head = flask.request.form.get("head") 1121head_route = flask.request.form.get("headroute") 1122base = flask.request.form.get("base") 1123 1124if not head and base and head_route: 1125return flask.redirect(".", 400) 1126 1127head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1128base_repo = git.Repo(server_repo_location) 1129print(head_repo) 1130 1131if head not in head_repo.branches or base not in base_repo.branches: 1132flask.flash(Markup( 1133"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1134category="error") 1135return flask.redirect(".", 303) 1136 1137head_data = db.session.get(Repo, head_route) 1138if not head_data.visibility: 1139flask.flash(Markup( 1140"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1141"Head can't be restricted")), 1142category="error") 1143return flask.redirect(".", 303) 1144 1145pull_request = PullRequest(repo_data, head, head_data, base, 1146db.session.get(User, flask.session["username"])) 1147 1148db.session.add(pull_request) 1149db.session.commit() 1150 1151return flask.redirect(".", 303) 1152 1153 1154@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1155def repository_prs_merge(username, repository): 1156server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1157if not os.path.exists(server_repo_location): 1158app.logger.error(f"Cannot load {server_repo_location}") 1159flask.abort(404) 1160if not (get_visibility(username, repository) or get_permission_level( 1161flask.session.get("username"), username, 1162repository) is not None): 1163flask.abort(403) 1164 1165if not get_permission_level(flask.session.get("username"), username, repository): 1166flask.abort(401) 1167 1168repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1169repo = git.Repo(server_repo_location) 1170id = flask.request.form.get("id") 1171 1172pull_request = db.session.get(PullRequest, id) 1173 1174if pull_request: 1175result = celery_tasks.merge_heads.delay( 1176pull_request.head_route, 1177pull_request.head_branch, 1178pull_request.base_route, 1179pull_request.base_branch, 1180simulate=True 1181) 1182task_result = worker.AsyncResult(result.id) 1183 1184return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1185# db.session.delete(pull_request) 1186# db.session.commit() 1187else: 1188flask.abort(400) 1189 1190 1191@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1192def repository_prs_merge_stage_two(username, repository, id): 1193server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1194if not os.path.exists(server_repo_location): 1195app.logger.error(f"Cannot load {server_repo_location}") 1196flask.abort(404) 1197if not (get_visibility(username, repository) or get_permission_level( 1198flask.session.get("username"), username, 1199repository) is not None): 1200flask.abort(403) 1201 1202if not get_permission_level(flask.session.get("username"), username, repository): 1203flask.abort(401) 1204 1205repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1206repo = git.Repo(server_repo_location) 1207 1208pull_request = db.session.get(PullRequest, id) 1209 1210if pull_request: 1211result = celery_tasks.merge_heads.delay( 1212pull_request.head_route, 1213pull_request.head_branch, 1214pull_request.base_route, 1215pull_request.base_branch, 1216simulate=False 1217) 1218task_result = worker.AsyncResult(result.id) 1219 1220return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1221# db.session.delete(pull_request) 1222# db.session.commit() 1223else: 1224flask.abort(400) 1225 1226 1227@app.route("/task/<task_id>") 1228def task_monitor(task_id): 1229task_result = worker.AsyncResult(task_id) 1230print(task_result.status) 1231 1232return flask.render_template("task-monitor.html", result=task_result) 1233 1234 1235@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1236def repository_prs_delete(username, repository): 1237server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1238if not os.path.exists(server_repo_location): 1239app.logger.error(f"Cannot load {server_repo_location}") 1240flask.abort(404) 1241if not (get_visibility(username, repository) or get_permission_level( 1242flask.session.get("username"), username, 1243repository) is not None): 1244flask.abort(403) 1245 1246if not get_permission_level(flask.session.get("username"), username, repository): 1247flask.abort(401) 1248 1249repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1250repo = git.Repo(server_repo_location) 1251id = flask.request.form.get("id") 1252 1253pull_request = db.session.get(PullRequest, id) 1254 1255if pull_request: 1256db.session.delete(pull_request) 1257db.session.commit() 1258 1259return flask.redirect(".", 303) 1260 1261 1262@repositories.route("/<username>/<repository>/settings/") 1263def repository_settings(username, repository): 1264if get_permission_level(flask.session.get("username"), username, repository) != 2: 1265flask.abort(401) 1266 1267return flask.render_template("repo-settings.html", username=username, repository=repository) 1268 1269 1270@app.errorhandler(404) 1271def e404(error): 1272return flask.render_template("not-found.html"), 404 1273 1274 1275@app.errorhandler(401) 1276def e401(error): 1277return flask.render_template("unauthorised.html"), 401 1278 1279 1280@app.errorhandler(403) 1281def e403(error): 1282return flask.render_template("forbidden.html"), 403 1283 1284 1285@app.errorhandler(418) 1286def e418(error): 1287return flask.render_template("teapot.html"), 418 1288 1289 1290@app.errorhandler(405) 1291def e405(error): 1292return flask.render_template("method-not-allowed.html"), 405 1293 1294 1295if __name__ == "__main__": 1296app.run(debug=True, port=8080, host="0.0.0.0") 1297 1298app.register_blueprint(repositories)