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