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