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