app.py
Python script, Unicode text, UTF-8 text executable
1""" 2Roundabout - git hosting for everyone <https://roundabout-host.com> 3Copyright (C) 2023-2025 Roundabout developers <root@roundabout-host.com> 4 5This program is free software: you can redistribute it and/or modify 6it under the terms of the GNU Affero General Public License as published by 7the Free Software Foundation, either version 3 of the License, or 8(at your option) any later version. 9 10This program is distributed in the hope that it will be useful, 11but WITHOUT ANY WARRANTY; without even the implied warranty of 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13GNU Affero General Public License for more details. 14 15You should have received a copy of the GNU Affero General Public License 16along with this program. If not, see <http://www.gnu.org/licenses/>. 17""" 18 19__version__ = "0.6.1" 20 21import os 22import shutil 23import random 24import subprocess 25import platform 26 27import PIL 28import git 29import mimetypes 30import magic 31import flask 32import cairosvg 33import celery 34import shlex 35import natsort 36from functools import wraps 37from datetime import datetime 38from enum import Enum 39from cairosvg import svg2png 40from flask_sqlalchemy import SQLAlchemy 41from flask_bcrypt import Bcrypt 42from markupsafe import escape, Markup 43from flask_migrate import Migrate 44from PIL import Image 45from flask_httpauth import HTTPBasicAuth 46import config 47import markdown 48from common import git_command 49from flask_babel import Babel, gettext, ngettext, force_locale 50from jinja2_fragments.flask import render_block 51import logging 52 53 54class No304(logging.Filter): 55def filter(self, record): 56return not record.getMessage().strip().endswith("304 -") 57 58 59logging.getLogger("werkzeug").addFilter(No304()) 60 61_ = gettext 62n_ = ngettext 63 64app = flask.Flask(__name__) 65app.config.from_mapping( 66CELERY=dict( 67broker_url=config.REDIS_URI, 68result_backend=config.REDIS_URI, 69task_ignore_result=True, 70), 71) 72 73auth = HTTPBasicAuth() 74 75app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 76app.config["SECRET_KEY"] = config.DB_PASSWORD 77app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 78app.config["BABEL_TRANSLATION_DIRECTORIES"] = "i18n" 79app.config["MAX_CONTENT_LENGTH"] = config.MAX_PAYLOAD_SIZE 80app.config["SESSION_COOKIE_SAMESITE"] = "Lax" 81app.config["SESSION_COOKIE_SECURE"] = config.suggest_https # only send cookies over HTTPS if the server is configured for it 82app.config["SESSION_COOKIE_HTTPONLY"] = True # don't allow JS to access the cookie 83if config.restrict_cookie_domain: 84app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN # don't share across subdomains, since user content is hosted there 85 86db = SQLAlchemy(app) 87bcrypt = Bcrypt(app) 88migrate = Migrate(app, db) 89 90from misc_utils import * 91 92import git_http 93import api 94import jinja_utils 95import celery_tasks 96from celery import Celery, Task 97import celery_integration 98import pathlib 99 100from models import * 101 102babel = Babel(app) 103 104 105def get_locale(): 106if flask.request.cookies.get("language"): 107return flask.request.cookies.get("language") 108return flask.request.accept_languages.best_match(config.available_locales) 109 110 111babel.init_app(app, locale_selector=get_locale) 112 113with app.app_context(): 114locale_names = {} 115for language in config.available_locales: 116with force_locale(language): 117# NOTE: Translate this to the language's name in that language, for example in French you would use français 118locale_names[language] = gettext("English") 119 120worker = celery_integration.init_celery_app(app) 121 122repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/") 123 124app.jinja_env.add_extension("jinja2.ext.do") 125app.jinja_env.add_extension("jinja2.ext.loopcontrols") 126app.jinja_env.add_extension("jinja2.ext.debug") 127 128 129natural_sorting_key = natsort.natsort_keygen(key=lambda x: x["name"]) 130 131 132@app.context_processor 133def default(): 134username = flask.session.get("username") 135 136user_object = User.query.filter_by(username=username).first() 137 138return { 139"logged_in_user": username, 140"user_object": user_object, 141"Notification": Notification, 142"unread": UserNotification.query.filter_by(user_username=username).filter( 143UserNotification.attention_level > 0).count(), 144"config": config, 145"Markup": Markup, 146"locale_names": locale_names, 147"set": set, # since using {} is impossible in Jinja 148"request": flask.request, 149"get_visibility": get_visibility, 150"get_permission_level": get_permission_level, 151} 152 153 154@app.route("/") 155def main(): 156if flask.session.get("username"): 157return flask.render_template("home.html") 158else: 159return flask.render_template("no-home.html") 160 161 162@app.route("/userstyle") 163def userstyle(): 164if flask.session.get("username") and os.path.exists( 165os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config", 166"theme.css")): 167return flask.send_from_directory( 168os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config"), 169"theme.css") 170else: 171return flask.Response("", mimetype="text/css") 172 173 174@app.route("/about/") 175def about(): 176return flask.render_template("about.html", platform=platform, version=__version__) 177 178 179@app.route("/search") 180def search(): 181query = flask.request.args.get("q") 182sorting = flask.request.args.get("sort", "default") 183page_number = flask.request.args.get("page", 1, type=int) 184if flask.session.get("username"): 185default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 186else: 187default_page_length = 16 188page_length = flask.request.args.get("per_page", default_page_length, type=int) 189 190results = Repo.query.filter(Repo.name.ilike(f"%{query}%")).filter_by(visibility=2) 191 192match sorting: 193case "popularity": 194results = results.outerjoin(RepoFavourite).group_by(Repo.route).order_by( 195db.func.count(RepoFavourite.id).desc()) 196case "newest": 197results = results.order_by(Repo.creation_date.desc()) 198case "oldest": 199results = results.order_by(Repo.creation_date) 200 201results = results.paginate(page=page_number, per_page=page_length) 202 203if results.has_next: 204next_page = results.next_num 205else: 206next_page = None 207 208if results.has_prev: 209prev_page = results.prev_num 210else: 211prev_page = None 212 213return flask.render_template("search.html", results=results, query=query, 214page_number=page_number, 215page_length=page_length, 216next_page=next_page, 217prev_page=prev_page, 218num_pages=results.pages) 219 220 221@app.route("/user-search") 222def user_search(): 223query = flask.request.args.get("q") 224sorting = flask.request.args.get("sort", "default") 225page_number = flask.request.args.get("page", 1, type=int) 226if flask.session.get("username"): 227default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 228else: 229default_page_length = 16 230page_length = flask.request.args.get("per_page", default_page_length, type=int) 231 232results = User.query.filter(User.username.ilike(f"%{query}%")) 233 234match sorting: 235case "popularity": 236results = results.outerjoin(UserFollow, User.username == UserFollow.followed_username).group_by( 237User.username).order_by(db.func.count(UserFollow.id).desc()) 238case "newest": 239results = results.order_by(User.creation_date.desc()) 240case "oldest": 241results = results.order_by(User.creation_date) 242 243results = results.paginate(page=page_number, per_page=page_length) 244 245if results.has_next: 246next_page = results.next_num 247else: 248next_page = None 249 250if results.has_prev: 251prev_page = results.prev_num 252else: 253prev_page = None 254 255return flask.render_template("user-search.html", results=results, query=query, 256page_number=page_number, 257page_length=page_length, 258next_page=next_page, 259prev_page=prev_page, 260num_pages=results.pages) 261 262 263@app.route("/language", methods=["POST"]) 264def set_locale(): 265response = flask.redirect(flask.request.referrer if flask.request.referrer else "/", 266code=303) 267if not flask.request.form.get("language"): 268response.delete_cookie("language") 269else: 270response.set_cookie("language", flask.request.form.get("language")) 271 272return response 273 274 275@app.route("/help/") 276def help_redirect(): 277return flask.redirect(config.help_url, code=302) 278 279 280@app.route("/settings/") 281def settings(): 282if not flask.session.get("username"): 283flask.abort(401) 284user = User.query.filter_by(username=flask.session.get("username")).first() 285 286return flask.render_template("user-settings.html", user=user) 287 288 289@app.route("/settings/confirm-email/<code>") 290def confirm_email(code): 291request = EmailChangeRequest.query.filter_by(code=code).first() 292if not request: 293flask.abort(404) 294 295user = db.session.get(User, request.user_username) 296user.email = request.new_email 297db.session.delete(request) 298db.session.commit() 299 300return flask.redirect("/settings", code=303) 301 302 303@app.route("/settings/profile", methods=["POST"]) 304def settings_profile(): 305user = User.query.filter_by(username=flask.session.get("username")).first() 306 307user.display_name = flask.request.form["displayname"] 308user.URL = flask.request.form["url"] 309user.company = flask.request.form["company"] 310user.company_URL = flask.request.form["companyurl"] 311if not flask.request.form.get("email"): 312# Deleting the email can be instant; no need to confirm 313user.email = "" 314elif flask.request.form.get("email") != user.email: 315# Changing the email requires confirmation from the address holder 316celery_tasks.request_email_change.delay(user.username, flask.request.form["email"]) 317user.location = flask.request.form["location"] 318user.show_mail = True if flask.request.form.get("showmail") else False 319user.bio = flask.request.form.get("bio") 320 321db.session.commit() 322 323flask.flash( 324Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")), 325category="success") 326return flask.redirect(f"/{flask.session.get('username')}", code=303) 327 328 329@app.route("/settings/preferences", methods=["POST"]) 330def settings_prefs(): 331user = User.query.filter_by(username=flask.session.get("username")).first() 332 333user.default_page_length = int(flask.request.form["page_length"]) 334user.max_post_nesting = int(flask.request.form["max_post_nesting"]) 335 336db.session.commit() 337 338flask.flash( 339Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")), 340category="success") 341return flask.redirect(f"/{flask.session.get('username')}", code=303) 342 343 344@app.route("/favourites/", methods=["GET", "POST"]) 345def favourites(): 346if not flask.session.get("username"): 347flask.abort(401) 348if flask.request.method == "GET": 349relationships = RepoFavourite.query.filter_by( 350user_username=flask.session.get("username")) 351 352return flask.render_template("favourites.html", favourites=relationships) 353 354 355@app.route("/favourites/<int:id>", methods=["POST"]) 356def favourite_edit(id): 357if not flask.session.get("username"): 358flask.abort(401) 359favourite = db.session.get(RepoFavourite, id) 360if favourite.user_username != flask.session.get("username"): 361flask.abort(403) 362data = flask.request.form 363favourite.notify_commit = js_to_bool(data.get("commit")) 364favourite.notify_forum = js_to_bool(data.get("forum")) 365favourite.notify_pr = js_to_bool(data.get("pull_request")) 366favourite.notify_admin = js_to_bool(data.get("administrative")) 367db.session.commit() 368return flask.render_template_string( 369""" 370<tr hx-post="/favourites/{{ favourite.id }}" hx-trigger="change" hx-include="#commit-{{ favourite.id }}, #forum-{{ favourite.id }}, #pull_request-{{ favourite.id }}, #administrative-{{ favourite.id }}" hx-headers='{"Content-Type": "application/json"}' hx-swap="outerHTML"> 371<td><a href="{{ favourite.repo.route }}">{{ favourite.repo.owner.username }}/{{ favourite.repo.name }}</a></td> 372<td style="text-align: center;"><input type="checkbox" name="commit" id="commit-{{ favourite.id }}" value="true" {% if favourite.notify_commit %}checked{% endif %}></td> 373<td style="text-align: center;"><input type="checkbox" name="forum" id="forum-{{ favourite.id }}" value="true" {% if favourite.notify_forum %}checked{% endif %}></td> 374<td style="text-align: center;"><input type="checkbox" name="pull_request" id="pull_request-{{ favourite.id }}" value="true" {% if favourite.notify_pr %}checked{% endif %}></td> 375<td style="text-align: center;"><input type="checkbox" name="administrative" id="administrative-{{ favourite.id }}" value="true" {% if favourite.notify_admin %}checked{% endif %}></td> 376</tr> 377""", 378favourite=favourite 379) 380 381 382@app.route("/notifications/", methods=["GET", "POST"]) 383def notifications(): 384if not flask.session.get("username"): 385flask.abort(401) 386if flask.request.method == "GET": 387page_number = flask.request.args.get("page", 1, type=int) 388if flask.session.get("username"): 389default_page_length = db.session.get(User, flask.session.get( 390"username")).default_page_length 391else: 392default_page_length = 16 393page_length = flask.request.args.get("per_page", default_page_length, type=int) 394 395results = UserNotification.query.filter_by( 396user_username=flask.session.get("username")).order_by(UserNotification.id.desc()).paginate( 397page=page_number, per_page=page_length) 398 399if results.has_next: 400next_page = results.next_num 401else: 402next_page = None 403 404if results.has_prev: 405prev_page = results.prev_num 406else: 407prev_page = None 408 409return flask.render_template("notifications.html", 410notifications=results, 411db=db, Commit=Commit, Post=Post, PullRequest=PullRequest, User=User, 412page_number=page_number, 413page_length=page_length, 414next_page=next_page, 415prev_page=prev_page, 416num_pages=results.pages 417) 418 419 420@app.route("/notifications/<int:notification_id>/read", methods=["POST"]) 421def mark_read(notification_id): 422if not flask.session.get("username"): 423flask.abort(401) 424notification = UserNotification.query.filter_by(id=notification_id).first() 425if notification.user_username != flask.session.get("username"): 426flask.abort(403) 427notification.mark_read() 428db.session.commit() 429return flask.render_template_string( 430"<button hx-post='/notifications/{{ notification.id }}/unread' hx-swap='outerHTML'>Mark as unread</button>", 431notification=notification), 200 432 433 434@app.route("/notifications/<int:notification_id>/unread", methods=["POST"]) 435def mark_unread(notification_id): 436if not flask.session.get("username"): 437flask.abort(401) 438notification = UserNotification.query.filter_by(id=notification_id).first() 439if notification.user_username != flask.session.get("username"): 440flask.abort(403) 441notification.mark_unread() 442db.session.commit() 443return flask.render_template_string( 444"<button hx-post='/notifications/{{ notification.id }}/read' hx-swap='outerHTML'>Mark as read</button>", 445notification=notification), 200 446 447 448@app.route("/notifications/mark-all-read", methods=["POST"]) 449def mark_all_read(): 450if not flask.session.get("username"): 451flask.abort(401) 452 453notifications = UserNotification.query.filter_by( 454user_username=flask.session.get("username")) 455for notification in notifications: 456notification.mark_read() 457db.session.commit() 458return flask.redirect("/notifications/", code=303) 459 460 461@app.route("/accounts/", methods=["GET", "POST"]) 462def login(): 463if flask.request.method == "GET": 464return flask.render_template("login.html") 465else: 466if "login" in flask.request.form: 467username = flask.request.form["username"] 468password = flask.request.form["password"] 469 470user = User.query.filter_by(username=username).first() 471 472if user and bcrypt.check_password_hash(user.password_hashed, password): 473flask.session["username"] = user.username 474flask.flash( 475Markup("<iconify-icon icon='mdi:account'></iconify-icon>" + _( 476"Successfully logged in as {username}").format( 477username=username)), 478category="success") 479return flask.redirect("/", code=303) 480elif not user: 481flask.flash(Markup( 482"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _( 483"User not found")), 484category="alert") 485return flask.render_template("login.html") 486else: 487flask.flash(Markup( 488"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _( 489"Invalid password")), 490category="error") 491return flask.render_template("login.html") 492if "signup" in flask.request.form: 493username = flask.request.form["username"] 494password = flask.request.form["password"] 495password2 = flask.request.form["password2"] 496email = flask.request.form.get("email") 497email2 = flask.request.form.get("email2") # repeat email is a honeypot 498name = flask.request.form.get("name") 499 500if not only_chars(username, 501"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"): 502flask.flash(Markup( 503_("Usernames may only contain Latin alphabet, numbers and '-'")), 504category="error") 505return flask.render_template("login.html") 506if "--" in username: 507flask.flash(Markup( 508_("Usernames may not contain consecutive hyphens")), 509category="error") 510return flask.render_template("login.html") 511if username.startswith("-") or username.endswith("-"): 512flask.flash(Markup( 513_("Usernames may not start or end with a hyphen")), 514category="error") 515return flask.render_template("login.html") 516if username in config.RESERVED_NAMES: 517flask.flash( 518Markup( 519_("Sorry, {username} is a system path").format( 520username=username)), 521category="error") 522return flask.render_template("login.html") 523 524if not username.islower(): 525if not name: # infer display name from the wanted username if not customised 526display_name = username 527username = username.lower() 528flask.flash(Markup( 529_("Usernames must be lowercase, so it's been converted automatically")), 530category="info") 531 532user_check = User.query.filter_by(username=username).first() 533if user_check or email2: # make the honeypot look like a normal error 534flask.flash( 535Markup( 536_( 537"The username {username} is taken").format( 538username=username)), 539category="error") 540return flask.render_template("login.html") 541 542if password2 != password: 543flask.flash(Markup(_( 544"Make sure the passwords match")), 545category="error") 546return flask.render_template("login.html") 547 548user = User(username, password, email, name) 549db.session.add(user) 550db.session.commit() 551flask.session["username"] = user.username 552flask.flash(Markup( 553_( 554"Successfully created and logged in as {username}").format( 555username=username)), 556category="success") 557 558notification = Notification({"type": "welcome"}) 559db.session.add(notification) 560db.session.commit() 561 562return flask.redirect("/", code=303) 563 564 565@app.route("/newrepo/", methods=["GET", "POST"]) 566def new_repo(): 567if not flask.session.get("username"): 568flask.abort(401) 569if flask.request.method == "GET": 570return flask.render_template("new-repo.html") 571else: 572name = flask.request.form["name"] 573visibility = int(flask.request.form["visibility"]) 574 575if not only_chars(name, 576"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."): 577flask.flash(Markup( 578"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 579"Repository names may only contain Latin alphabet, numbers, '-', '_' and '.'")), 580category="error") 581return flask.render_template("new-repo.html") 582 583username = flask.request.form["account"] 584if not username: 585username = flask.session.get("username") 586if username != flask.session.get("username"): 587trust_relationship = UserTrust.query.filter_by( 588host_username=username, 589trusted_username=flask.session.get("username") 590).first() 591 592if not trust_relationship: 593flask.flash(Markup( 594"<iconify-icon icon='mdi:error'></iconify-icon>" + _("{username} doesn't trust you").format(username=username)), 595category="error") 596return flask.render_template("new-repo.html") 597elif trust_relationship.trust_level < 1: 598flask.flash(Markup( 599"<iconify-icon icon='mdi:error'></iconify-icon>" + _("{username} doesn't trust you to modify").format(username=username)), 600category="error") 601return flask.render_template("new-repo.html") 602 603user = User.query.filter_by(username=username).first() 604 605repo = Repo(user, name, visibility) 606db.session.add(repo) 607db.session.commit() 608 609flask.flash(Markup(_("Successfully created repository {name}").format(name=name)), 610category="success") 611return flask.redirect(repo.route, code=303) 612 613 614@app.route("/logout") 615def logout(): 616flask.session.clear() 617flask.flash(Markup( 618"<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")), 619category="info") 620return flask.redirect("/", code=303) 621 622 623@app.route("/<username>/", methods=["GET", "POST"]) 624def user_profile(username): 625if db.session.get(User, username) is None: 626flask.abort(404) 627old_relationship = UserFollow.query.filter_by( 628follower_username=flask.session.get("username"), 629followed_username=username).first() 630if flask.request.method == "GET": 631user = User.query.filter_by(username=username).first() 632match flask.request.args.get("action"): 633case "repositories": 634if user.username == flask.session.get("username"): 635repos = Repo.query.filter_by(owner_name=username) 636else: 637repos = Repo.query.filter_by(owner_name=username, visibility=2) 638return flask.render_template("user-profile-repositories.html", user=user, 639repos=repos, 640relationship=old_relationship) 641case "followers": 642return flask.render_template("user-profile-followers.html", user=user, 643relationship=old_relationship) 644case "follows": 645return flask.render_template("user-profile-follows.html", user=user, 646relationship=old_relationship) 647case "organisation": 648return flask.render_template("user-profile-organisation.html", user=user, 649relationship=old_relationship) 650case "trust": 651return flask.render_template("user-profile-trust.html", user=user, 652relationship=old_relationship) 653case _: 654return flask.render_template("user-profile-overview.html", user=user, 655relationship=old_relationship) 656 657elif flask.request.method == "POST": 658match flask.request.args.get("action"): 659case "follow": 660if username == flask.session.get("username"): 661flask.abort(403) 662if old_relationship: 663db.session.delete(old_relationship) 664else: 665relationship = UserFollow( 666flask.session.get("username"), 667username 668) 669print(f"Following {username}") 670db.session.add(relationship) 671 672user = db.session.get(User, username) 673author = db.session.get(User, flask.session.get("username")) 674notification = Notification({"type": "follow", "author": author.username, "user": user.username}) 675db.session.add(notification) 676db.session.commit() 677user_notification = UserNotification(user, notification, 1) 678db.session.add(user_notification) 679db.session.commit() 680 681db.session.commit() 682return flask.redirect("?", code=303) 683case "trust-confirm": 684trust_level = int(flask.request.form["trust-level"]) 685existing_trust = UserTrust.query.filter_by( 686host_username=flask.session.get("username"), 687trusted_username=username).first() 688password = flask.request.form.get("password") 689if not bcrypt.check_password_hash(db.session.get(User, flask.session.get("username")).password_hashed, 690password): 691flask.flash(Markup(_("Invalid password")), category="error") 692return flask.redirect("?action=trust", code=303) 693if existing_trust: 694existing_trust.cancel() 695if 0 <= trust_level <= 2: 696trust = UserTrust(db.session.get(User, flask.session.get("username")), db.session.get(User, username), 697trust_level) 698db.session.add(trust) 699elif trust_level == -1: 700existing_trust.cancel() 701db.session.commit() 702 703return flask.redirect("?", code=303) 704 705 706@app.route("/<username>/<repository>/") 707def repository_index(username, repository): 708return flask.redirect("./tree", code=302) 709 710 711@app.route("/info/<username>/avatar") 712def user_avatar(username): 713server_userdata_location = os.path.join(config.USERDATA_PATH, username) 714if not os.path.exists(server_userdata_location): 715return flask.render_template("errors/not-found.html"), 404 716 717return flask.send_from_directory(server_userdata_location, "avatar.png") 718 719 720@app.route("/info/<username>/avatar", methods=["POST"]) 721def user_avatar_upload(username): 722server_userdata_location = os.path.join(config.USERDATA_PATH, username) 723 724if not os.path.exists(server_userdata_location): 725flask.abort(404) 726if not flask.session.get("username") == username: 727flask.abort(403) 728 729# Convert image to PNG 730try: 731image = Image.open(flask.request.files["avatar"]) 732except PIL.UnidentifiedImageError: 733flask.abort(400) 734image.save(os.path.join(server_userdata_location, "avatar.png")) 735 736return flask.redirect(f"/{username}", code=303) 737 738 739@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 740def repository_raw(username, repository, branch, subpath): 741server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 742if not os.path.exists(server_repo_location): 743flask.abort(404) 744if not (get_visibility(username, repository) or get_permission_level( 745flask.session.get("username"), username, 746repository) is not None): 747flask.abort(403) 748 749if not os.path.exists(server_repo_location): 750return flask.render_template("errors/not-found.html"), 404 751 752repo = git.Repo(server_repo_location) 753repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 754if not repo_data.default_branch: 755if repo.heads: 756repo_data.default_branch = repo.heads[0].name 757else: 758return flask.render_template("empty.html", 759remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 760if not branch: 761branch = repo_data.default_branch 762return flask.redirect(f"./{branch}", code=302) 763 764if branch.startswith("tag:"): 765ref = f"tags/{branch[4:]}" 766elif branch.startswith("~"): 767ref = branch[1:] 768else: 769ref = f"heads/{branch}" 770 771ref = ref.replace("~", "/") # encode slashes for URL support 772 773try: 774repo.git.checkout("-f", ref) 775except git.exc.GitCommandError: 776return flask.render_template("errors/not-found.html"), 404 777 778response = flask.send_from_directory(config.REPOS_PATH, 779os.path.join(username, repository, subpath)) 780 781if repo_data.visibility < 2: 782response.headers["X-Robots-Tag"] = "noindex" 783 784return response 785 786 787@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 788@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 789@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 790def repository_tree(username, repository, branch, subpath): 791server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 792if not os.path.exists(server_repo_location): 793flask.abort(404) 794if not (get_visibility(username, repository) or get_permission_level( 795flask.session.get("username"), username, 796repository) is not None): 797flask.abort(403) 798 799repo = git.Repo(server_repo_location) 800repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 801if not repo_data.default_branch: 802if repo.heads: 803repo_data.default_branch = repo.heads[0].name 804else: 805return flask.render_template("empty.html", 806remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 807if not branch: 808branch = repo_data.default_branch 809return flask.redirect(f"./{branch}", code=302) 810 811if branch.startswith("tag:"): 812ref = f"tags/{branch[4:]}" 813elif branch.startswith("~"): 814ref = branch[1:] 815else: 816ref = f"heads/{branch}" 817 818ref = ref.replace("~", "/") # encode slashes for URL support 819 820try: 821repo.git.checkout("-f", ref) 822except git.exc.GitCommandError: 823return flask.render_template("errors/not-found.html"), 404 824 825branches = repo.heads 826 827all_refs = [] 828for ref in repo.heads: 829all_refs.append((ref, "head")) 830for ref in repo.tags: 831all_refs.append((ref, "tag")) 832 833if os.path.isdir(os.path.join(server_repo_location, subpath)): 834files = [] 835blobs = [] 836 837for entry in os.listdir(os.path.join(server_repo_location, subpath)): 838if not os.path.basename(entry) == ".git": 839files.append(os.path.join(subpath, entry)) 840 841infos = [] 842 843for file in files: 844path = os.path.join(server_repo_location, file) 845mimetype = guess_mime(path) 846 847text = git_command(server_repo_location, None, "log", "--format='%H\n'", 848shlex.quote(file)).decode() 849 850sha = text.split("\n")[0] 851identifier = f"/{username}/{repository}/{sha}" 852 853last_commit = db.session.get(Commit, identifier) 854 855quantity, unit = human_size(os.path.getsize(path)) 856 857info = { 858"name": os.path.basename(file), 859"serverPath": path, 860"relativePath": file, 861"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 862"size": str(quantity) + "\u202f" + unit, # nnbsp 863"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 864"commit": last_commit, 865"shaSize": 7, 866} 867 868special_icon = config.match_icon(os.path.basename(file)) 869if special_icon: 870info["icon"] = special_icon 871elif os.path.isdir(path): 872info["icon"] = config.folder_icon 873info["size"] = ngettext("%(num)d file", "%(num)d files", len(os.listdir(path))) 874elif mimetypes.guess_type(path)[0] in config.file_icons: 875info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]] 876else: 877info["icon"] = config.unknown_icon 878 879if os.path.isdir(path): 880infos.insert(0, info) 881else: 882infos.append(info) 883 884infos.sort(key=natural_sorting_key) 885 886return flask.render_template( 887"repo-tree.html", 888username=username, 889repository=repository, 890files=infos, 891subpath=os.path.join("/", subpath), 892branches=all_refs, 893current=branch, 894remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 895is_favourite=get_favourite(flask.session.get("username"), username, repository), 896repo_data=repo_data, 897) 898else: 899path = os.path.join(server_repo_location, subpath) 900 901if not os.path.exists(path): 902return flask.render_template("errors/not-found.html"), 404 903 904mimetype = guess_mime(path) 905mode = mimetype.split("/", 1)[0] 906size = human_size(os.path.getsize(path)) 907 908special_icon = config.match_icon(os.path.basename(path)) 909if special_icon: 910icon = special_icon 911elif os.path.isdir(path): 912icon = config.folder_icon 913elif mimetypes.guess_type(path)[0] in config.file_icons: 914icon = config.file_icons[mimetypes.guess_type(path)[0]] 915else: 916icon = config.unknown_icon 917 918contents = None 919if mode == "text": 920contents = convert_to_html(path) 921 922return flask.render_template( 923"repo-file.html", 924username=username, 925repository=repository, 926file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 927branches=all_refs, 928current=branch, 929mode=mode, 930mimetype=mimetype, 931detailedtype=magic.from_file(path), 932size=size, 933icon=icon, 934subpath=os.path.join("/", subpath), 935extension=pathlib.Path(path).suffix, 936basename=os.path.basename(path), 937contents=contents, 938remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 939is_favourite=get_favourite(flask.session.get("username"), username, repository), 940repo_data=repo_data, 941) 942 943 944@repositories.route("/<username>/<repository>/commit/<sha>") 945def repository_commit(username, repository, sha): 946server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 947if not os.path.exists(server_repo_location): 948flask.abort(404) 949if not (get_visibility(username, repository) or get_permission_level( 950flask.session.get("username"), username, 951repository) is not None): 952flask.abort(403) 953 954if not os.path.exists(server_repo_location): 955return flask.render_template("errors/not-found.html"), 404 956 957repo = git.Repo(server_repo_location) 958repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 959 960files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r", 961"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1] 962 963return flask.render_template( 964"repo-commit.html", 965username=username, 966repository=repository, 967remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 968is_favourite=get_favourite(flask.session.get("username"), username, repository), 969diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff", 970str(sha) + "^!", "--", file).decode().split("\n") for 971file in files}, 972data=db.session.get(Commit, f"/{username}/{repository}/{sha}"), 973repo_data=repo_data, 974comment_query=Comment.query, 975permission_level=get_permission_level(flask.session.get("username"), username, repository), 976) 977 978 979@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"]) 980def repository_commit_add_comment(username, repository, sha): 981server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 982if not os.path.exists(server_repo_location): 983flask.abort(404) 984if not flask.session.get("username"): 985flask.abort(401) 986if not (get_visibility(username, repository) or get_permission_level( 987flask.session.get("username"), username, 988repository) is not None): 989flask.abort(403) 990 991comment = Comment( 992db.session.get(User, flask.session.get("username")), 993db.session.get(Repo, f"/{username}/{repository}"), 994db.session.get(Commit, f"/{username}/{repository}/{sha}"), 995flask.request.form["comment"], 996flask.request.form["file"], 997flask.request.form["line"], 998) 999 1000db.session.add(comment) 1001db.session.commit() 1002 1003return flask.redirect( 1004flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 1005code=303 1006) 1007 1008 1009@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"]) 1010def repository_commit_delete_comment(username, repository, sha, id): 1011repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1012comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 1013commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first() 1014if ( 1015comment.owner.username == flask.session.get("username") 1016or get_permission_level(flask.session.get("username"), username, repository) >= 2 1017or comment.commit.owner_name == flask.session.get("username") 1018): 1019db.session.delete(comment) 1020db.session.commit() 1021 1022return flask.redirect( 1023flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 1024code=303 1025) 1026 1027 1028@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"]) 1029def repository_commit_resolve_comment(username, repository, sha, id): 1030comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 1031if ( 1032comment.commit.owner_name == flask.session.get("username") 1033or get_permission_level(flask.session.get("username"), username, repository) >= 2 1034or comment.owner.username == flask.session.get("username") 1035): 1036comment.state = int(not comment.state) 1037db.session.commit() 1038 1039return flask.redirect( 1040flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 1041code=303 1042) 1043 1044 1045@repositories.route("/<username>/<repository>/forum/") 1046def repository_forum(username, repository): 1047server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1048if not os.path.exists(server_repo_location): 1049flask.abort(404) 1050if not (get_visibility(username, repository) or get_permission_level( 1051flask.session.get("username"), username, 1052repository) is not None): 1053flask.abort(403) 1054 1055repo = git.Repo(server_repo_location) 1056repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1057user = User.query.filter_by(username=flask.session.get("username")).first() 1058relationships = RepoAccess.query.filter_by(repo=repo_data) 1059user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1060 1061page_number = flask.request.args.get("page", 1, type=int) 1062if flask.session.get("username"): 1063default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1064else: 1065default_page_length = 16 1066 1067page_length = flask.request.args.get("per_page", default_page_length, type=int) 1068 1069posts = Post.query.filter_by(repo=repo_data, parent=None).order_by(Post.last_updated.desc()).paginate( 1070page=page_number, per_page=page_length 1071) 1072 1073if posts.has_next: 1074next_page = posts.next_num 1075else: 1076next_page = None 1077 1078if posts.has_prev: 1079prev_page = posts.prev_num 1080else: 1081prev_page = None 1082 1083return flask.render_template( 1084"repo-forum.html", 1085username=username, 1086repository=repository, 1087repo_data=repo_data, 1088relationships=relationships, 1089repo=repo, 1090user_relationship=user_relationship, 1091Post=Post, 1092remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1093is_favourite=get_favourite(flask.session.get("username"), username, repository), 1094default_branch=repo_data.default_branch, 1095page_number=page_number, 1096page_length=page_length, 1097next_page=next_page, 1098prev_page=prev_page, 1099num_pages=posts.pages, 1100posts=posts 1101) 1102 1103 1104@repositories.route("/<username>/<repository>/forum/search") 1105def repository_forum_search(username, repository): 1106server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1107if not os.path.exists(server_repo_location): 1108flask.abort(404) 1109if not (get_visibility(username, repository) or get_permission_level( 1110flask.session.get("username"), username, 1111repository) is not None): 1112flask.abort(403) 1113 1114repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1115user = User.query.filter_by(username=flask.session.get("username")).first() 1116relationships = RepoAccess.query.filter_by(repo=repo_data) 1117user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1118 1119query = flask.request.args.get("q") 1120 1121page_number = flask.request.args.get("page", 1, type=int) 1122if flask.session.get("username"): 1123default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1124else: 1125default_page_length = 16 1126 1127page_length = flask.request.args.get("per_page", default_page_length, type=int) 1128 1129all_posts = Post.query.filter(Post.repo == repo_data) 1130 1131results = (all_posts 1132.filter(Post.subject.ilike(f"%{query}%") | Post.message.ilike(f"%{query}%")) 1133.order_by(Post.last_updated.desc())) 1134 1135if flask.request.args.get("state"): 1136try: 1137results = results.filter(Post.state == int(flask.request.args.get("state"))) 1138except ValueError: 1139pass # if state is not an integer, ignore it 1140 1141if flask.request.args.get("label"): 1142results = results.filter(Post.labels.any(Label.identifier == flask.request.args.get("label"))) 1143 1144results = results.paginate(page=page_number, per_page=page_length) 1145 1146if results.has_next: 1147next_page = results.next_num 1148else: 1149next_page = None 1150 1151if results.has_prev: 1152prev_page = results.prev_num 1153else: 1154prev_page = None 1155 1156return flask.render_template( 1157"repo-forum-search.html", 1158username=username, 1159repository=repository, 1160repo_data=repo_data, 1161relationships=relationships, 1162user_relationship=user_relationship, 1163query=query, 1164results=results, 1165Post=Post, 1166remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1167is_favourite=get_favourite(flask.session.get("username"), username, repository), 1168default_branch=repo_data.default_branch, 1169page_number=page_number, 1170page_length=page_length, 1171next_page=next_page, 1172prev_page=prev_page, 1173num_pages=results.pages, 1174require_state=flask.request.args.get("state"), 1175require_label=flask.request.args.get("label"), 1176) 1177 1178 1179@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 1180def repository_forum_topic(username, repository, id): 1181server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1182if not os.path.exists(server_repo_location): 1183flask.abort(404) 1184if not (get_visibility(username, repository) or get_permission_level( 1185flask.session.get("username"), username, 1186repository) is not None): 1187flask.abort(403) 1188 1189if not os.path.exists(server_repo_location): 1190return flask.render_template("errors/not-found.html"), 404 1191 1192repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1193user = User.query.filter_by(username=flask.session.get("username")).first() 1194relationships = RepoAccess.query.filter_by(repo=repo_data) 1195user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1196 1197post = Post.query.filter_by(id=id).first() 1198 1199return flask.render_template( 1200"repo-topic.html", 1201username=username, 1202repository=repository, 1203repo_data=repo_data, 1204relationships=relationships, 1205user_relationship=user_relationship, 1206post=post, 1207remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1208is_favourite=get_favourite(flask.session.get("username"), username, repository), 1209default_branch=repo_data.default_branch 1210) 1211 1212 1213@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 1214def repository_forum_new(username, repository): 1215server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1216if not os.path.exists(server_repo_location): 1217flask.abort(404) 1218if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level( 1219flask.session.get("username"), username, 1220repository) is not None): 1221flask.abort(403) 1222 1223if not os.path.exists(server_repo_location): 1224return flask.render_template("errors/not-found.html"), 404 1225 1226repo = git.Repo(server_repo_location) 1227repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1228user = User.query.filter_by(username=flask.session.get("username")).first() 1229relationships = RepoAccess.query.filter_by(repo=repo_data) 1230user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1231 1232post = Post(user, repo_data, None, flask.request.form["subject"], 1233flask.request.form["message"]) 1234 1235db.session.add(post) 1236db.session.commit() 1237 1238return flask.redirect( 1239flask.url_for(".repository_forum_thread", username=username, repository=repository, 1240post_id=post.number), 1241code=303) 1242 1243 1244@repositories.route("/<username>/<repository>/forum/<int:post_id>") 1245def repository_forum_thread(username, repository, post_id): 1246server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1247if not os.path.exists(server_repo_location): 1248flask.abort(404) 1249if not (get_visibility(username, repository) or get_permission_level( 1250flask.session.get("username"), username, 1251repository) is not None): 1252flask.abort(403) 1253 1254if not os.path.exists(server_repo_location): 1255return flask.render_template("errors/not-found.html"), 404 1256 1257repo = git.Repo(server_repo_location) 1258repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1259user = User.query.filter_by(username=flask.session.get("username")).first() 1260relationships = RepoAccess.query.filter_by(repo=repo_data) 1261user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1262 1263if user: 1264max_post_nesting = user.max_post_nesting 1265else: 1266max_post_nesting = 2 1267 1268return flask.render_template( 1269"repo-forum-thread.html", 1270username=username, 1271repository=repository, 1272repo_data=repo_data, 1273relationships=relationships, 1274repo=repo, 1275Post=Post, 1276user_relationship=user_relationship, 1277post_id=post_id, 1278max_post_nesting=max_post_nesting, 1279remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1280is_favourite=get_favourite(flask.session.get("username"), username, repository), 1281parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 1282has_permission=not ((not get_permission_level(flask.session.get("username"), username, 1283repository)) and db.session.get(Post, 1284f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")), 1285) 1286 1287 1288@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state", 1289methods=["POST"]) 1290def repository_forum_change_state(username, repository, post_id): 1291server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1292if not os.path.exists(server_repo_location): 1293flask.abort(404) 1294if (not get_permission_level(flask.session.get("username"), username, repository)) and db.session.get(Post, f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username"): 1295flask.abort(403) 1296 1297repo = git.Repo(server_repo_location) 1298repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1299user = User.query.filter_by(username=flask.session.get("username")).first() 1300relationships = RepoAccess.query.filter_by(repo=repo_data) 1301user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1302 1303post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1304 1305if not post: 1306flask.abort(404) 1307if post.parent: 1308flask.abort(400) 1309 1310post.state = int(flask.request.form["new-state"]) 1311 1312db.session.commit() 1313 1314return flask.redirect( 1315flask.url_for(".repository_forum_thread", username=username, repository=repository, 1316post_id=post_id), 1317code=303) 1318 1319 1320@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 1321def repository_forum_reply(username, repository, post_id): 1322server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1323if not os.path.exists(server_repo_location): 1324flask.abort(404) 1325if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level( 1326flask.session.get("username"), username, 1327repository) is not None): 1328flask.abort(403) 1329 1330if not os.path.exists(server_repo_location): 1331return flask.render_template("errors/not-found.html"), 404 1332 1333repo = git.Repo(server_repo_location) 1334repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1335user = User.query.filter_by(username=flask.session.get("username")).first() 1336relationships = RepoAccess.query.filter_by(repo=repo_data) 1337user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1338if not user: 1339flask.abort(401) 1340 1341parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1342post = Post(user, repo_data, parent, flask.request.form["subject"], 1343flask.request.form["message"]) 1344 1345db.session.add(post) 1346post.update_date() 1347db.session.commit() 1348 1349return flask.redirect( 1350flask.url_for(".repository_forum_thread", username=username, repository=repository, 1351post_id=post_id), 1352code=303) 1353 1354 1355@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["POST"]) 1356def repository_forum_edit(username, repository, post_id): 1357server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1358if not os.path.exists(server_repo_location): 1359flask.abort(404) 1360if not (get_visibility(username, repository) or get_permission_level( 1361flask.session.get("username"), username, 1362repository) is not None): 1363flask.abort(403) 1364 1365if not os.path.exists(server_repo_location): 1366return flask.render_template("errors/not-found.html"), 404 1367 1368repo = git.Repo(server_repo_location) 1369repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1370user = User.query.filter_by(username=flask.session.get("username")).first() 1371relationships = RepoAccess.query.filter_by(repo=repo_data) 1372user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1373if not user: 1374flask.abort(401) 1375post = db.session.get(Post, f"/{username}/{repository}/{post_id}") 1376if user != post.owner: 1377flask.abort(403) 1378 1379post.subject = flask.request.form["subject"] 1380post.message = flask.request.form["message"] 1381post.html = markdown.markdown2html(post.message).prettify() 1382post.update_date() 1383db.session.commit() 1384 1385return flask.redirect( 1386flask.url_for(".repository_forum_thread", username=username, repository=repository, 1387post_id=post_id), 1388code=303) 1389 1390 1391@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["GET"]) 1392def repository_forum_edit_form(username, repository, post_id): 1393server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1394if not os.path.exists(server_repo_location): 1395flask.abort(404) 1396if not (get_visibility(username, repository) or get_permission_level( 1397flask.session.get("username"), username, 1398repository) is not None): 1399flask.abort(403) 1400 1401if not os.path.exists(server_repo_location): 1402return flask.render_template("errors/not-found.html"), 404 1403 1404repo = git.Repo(server_repo_location) 1405repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1406user = User.query.filter_by(username=flask.session.get("username")).first() 1407relationships = RepoAccess.query.filter_by(repo=repo_data) 1408user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1409if not user: 1410flask.abort(401) 1411post = db.session.get(Post, f"/{username}/{repository}/{post_id}") 1412if user != post.owner: 1413flask.abort(403) 1414 1415return flask.render_template( 1416"repo-forum-edit.html", 1417username=username, 1418repository=repository, 1419repo_data=repo_data, 1420relationships=relationships, 1421repo=repo, 1422user_relationship=user_relationship, 1423post=post, 1424remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1425is_favourite=get_favourite(flask.session.get("username"), username, repository), 1426default_branch=repo_data.default_branch 1427) 1428 1429@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 1430defaults={"score": 1}) 1431@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 1432defaults={"score": -1}) 1433@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 1434def repository_forum_vote(username, repository, post_id, score): 1435server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1436if not os.path.exists(server_repo_location): 1437flask.abort(404) 1438if not (get_visibility(username, repository) or get_permission_level( 1439flask.session.get("username"), username, 1440repository) is not None): 1441flask.abort(403) 1442 1443if not os.path.exists(server_repo_location): 1444return flask.render_template("errors/not-found.html"), 404 1445 1446repo = git.Repo(server_repo_location) 1447repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1448user = User.query.filter_by(username=flask.session.get("username")).first() 1449relationships = RepoAccess.query.filter_by(repo=repo_data) 1450user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1451if not user: 1452flask.abort(401) 1453 1454post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1455 1456if score: 1457old_relationship = PostVote.query.filter_by(user_username=user.username, 1458post_identifier=post.identifier).first() 1459if old_relationship: 1460if score == old_relationship.vote_score: 1461db.session.delete(old_relationship) 1462post.vote_sum -= old_relationship.vote_score 1463else: 1464post.vote_sum -= old_relationship.vote_score 1465post.vote_sum += score 1466old_relationship.vote_score = score 1467else: 1468relationship = PostVote(user, post, score) 1469post.vote_sum += score 1470db.session.add(relationship) 1471 1472db.session.commit() 1473 1474user_vote = PostVote.query.filter_by(user_username=user.username, 1475post_identifier=post.identifier).first() 1476response = flask.make_response( 1477str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 1478response.content_type = "text/plain" 1479 1480return response 1481 1482 1483@repositories.route("/<username>/<repository>/forum/<int:post_id>/label", methods=["POST"]) 1484def repository_forum_label(username, repository, post_id): 1485server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1486if not os.path.exists(server_repo_location): 1487flask.abort(404) 1488if not get_permission_level(flask.session.get("username"), username, repository): 1489flask.abort(403) 1490 1491repo = git.Repo(server_repo_location) 1492repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1493user = User.query.filter_by(username=flask.session.get("username")).first() 1494relationships = RepoAccess.query.filter_by(repo=repo_data) 1495user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1496 1497post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1498 1499if not post: 1500flask.abort(404) 1501if post.parent: 1502flask.abort(400) 1503 1504label = db.session.get(Label, flask.request.form["label"]) 1505 1506if PostLabel.query.filter_by(post=post, label=label).first(): 1507return flask.redirect( 1508flask.url_for(".repository_forum_thread", username=username, repository=repository, 1509post_id=post_id), 1510code=303) 1511 1512post_label = PostLabel(post, label) 1513db.session.add(post_label) 1514 1515db.session.commit() 1516 1517return flask.redirect( 1518flask.url_for(".repository_forum_thread", username=username, repository=repository, 1519post_id=post_id), 1520code=303) 1521 1522 1523@repositories.route("/<username>/<repository>/forum/<int:post_id>/remove-label") 1524def repository_forum_remove_label(username, repository, post_id): 1525server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1526if not os.path.exists(server_repo_location): 1527flask.abort(404) 1528if not get_permission_level(flask.session.get("username"), username, repository): 1529flask.abort(403) 1530 1531repo = git.Repo(server_repo_location) 1532repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1533user = User.query.filter_by(username=flask.session.get("username")).first() 1534relationships = RepoAccess.query.filter_by(repo=repo_data) 1535user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1536 1537post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1538 1539if not post: 1540flask.abort(404) 1541if post.parent: 1542flask.abort(400) 1543 1544label = db.session.get(Label, flask.request.args["label"]) 1545 1546post_label = PostLabel.query.filter_by(post=post, label=label).first() 1547db.session.delete(post_label) 1548 1549db.session.commit() 1550 1551return flask.redirect( 1552flask.url_for(".repository_forum_thread", username=username, repository=repository, 1553post_id=post_id), 1554code=303) 1555 1556 1557@repositories.route("/<username>/<repository>/favourite") 1558def repository_favourite(username, repository): 1559server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1560if not os.path.exists(server_repo_location): 1561flask.abort(404) 1562if not (get_visibility(username, repository) or get_permission_level( 1563flask.session.get("username"), username, 1564repository) is not None): 1565flask.abort(403) 1566 1567if not os.path.exists(server_repo_location): 1568return flask.render_template("errors/not-found.html"), 404 1569 1570repo = git.Repo(server_repo_location) 1571repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1572user = User.query.filter_by(username=flask.session.get("username")).first() 1573relationships = RepoAccess.query.filter_by(repo=repo_data) 1574user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1575if not user: 1576flask.abort(401) 1577 1578old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 1579repo_route=repo_data.route).first() 1580if old_relationship: 1581db.session.delete(old_relationship) 1582else: 1583relationship = RepoFavourite(user, repo_data) 1584db.session.add(relationship) 1585 1586db.session.commit() 1587 1588return flask.redirect(flask.url_for("favourites"), code=303) 1589 1590 1591@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 1592def repository_users(username, repository): 1593server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1594if not os.path.exists(server_repo_location): 1595flask.abort(404) 1596if not (get_visibility(username, repository) or get_permission_level( 1597flask.session.get("username"), username, 1598repository) is not None): 1599flask.abort(403) 1600 1601if not os.path.exists(server_repo_location): 1602return flask.render_template("errors/not-found.html"), 404 1603 1604repo = git.Repo(server_repo_location) 1605repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1606user = User.query.filter_by(username=flask.session.get("username")).first() 1607relationships = RepoAccess.query.filter_by(repo=repo_data) 1608user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1609 1610if flask.request.method == "GET": 1611return flask.render_template( 1612"repo-users.html", 1613username=username, 1614repository=repository, 1615repo_data=repo_data, 1616relationships=relationships, 1617repo=repo, 1618user_relationship=user_relationship, 1619remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1620is_favourite=get_favourite(flask.session.get("username"), username, repository) 1621) 1622else: 1623if get_permission_level(flask.session.get("username"), username, repository) != 2: 1624flask.abort(401) 1625 1626if flask.request.form.get("new-username"): 1627# Create new relationship 1628new_user = User.query.filter_by( 1629username=flask.request.form.get("new-username")).first() 1630relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1631db.session.add(relationship) 1632db.session.commit() 1633if flask.request.form.get("update-username"): 1634# Create new relationship 1635updated_user = User.query.filter_by( 1636username=flask.request.form.get("update-username")).first() 1637relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1638if relationship.automatic: 1639flask.abort(400) 1640if flask.request.form.get("update-level") == -1: 1641relationship.delete() 1642else: 1643relationship.access_level = flask.request.form.get("update-level") 1644db.session.commit() 1645 1646return flask.redirect( 1647app.url_for(".repository_users", username=username, repository=repository)) 1648 1649 1650@repositories.route("/<username>/<repository>/branches/") 1651def repository_branches(username, repository): 1652server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1653if not os.path.exists(server_repo_location): 1654flask.abort(404) 1655if not (get_visibility(username, repository) or get_permission_level( 1656flask.session.get("username"), username, 1657repository) is not None): 1658flask.abort(403) 1659 1660if not os.path.exists(server_repo_location): 1661return flask.render_template("errors/not-found.html"), 404 1662 1663repo = git.Repo(server_repo_location) 1664repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1665 1666return flask.render_template( 1667"repo-branches.html", 1668username=username, 1669repository=repository, 1670repo_data=repo_data, 1671repo=repo, 1672remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1673is_favourite=get_favourite(flask.session.get("username"), username, repository) 1674) 1675 1676 1677@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1678@repositories.route("/<username>/<repository>/log/<branch>/") 1679def repository_log(username, repository, branch): 1680server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1681if not os.path.exists(server_repo_location): 1682flask.abort(404) 1683if not (get_visibility(username, repository) or get_permission_level( 1684flask.session.get("username"), username, 1685repository) is not None): 1686flask.abort(403) 1687 1688if not os.path.exists(server_repo_location): 1689return flask.render_template("errors/not-found.html"), 404 1690 1691repo = git.Repo(server_repo_location) 1692repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1693if not repo_data.default_branch: 1694if repo.heads: 1695repo_data.default_branch = repo.heads[0].name 1696else: 1697return flask.render_template("empty.html", 1698remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1699if not branch: 1700branch = repo_data.default_branch 1701return flask.redirect(f"./{branch}", code=302) 1702 1703if branch.startswith("tag:"): 1704ref = f"tags/{branch[4:]}" 1705elif branch.startswith("~"): 1706ref = branch[1:] 1707else: 1708ref = f"heads/{branch}" 1709 1710ref = ref.replace("~", "/") # encode slashes for URL support 1711 1712try: 1713repo.git.checkout("-f", ref) 1714except git.exc.GitCommandError: 1715return flask.render_template("errors/not-found.html"), 404 1716 1717branches = repo.heads 1718 1719all_refs = [] 1720for ref in repo.heads: 1721all_refs.append((ref, "head")) 1722for ref in repo.tags: 1723all_refs.append((ref, "tag")) 1724 1725commit_list = [f"/{username}/{repository}/{sha}" for sha in 1726git_command(server_repo_location, None, "log", 1727"--format='%H'").decode().split("\n")] 1728 1729commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc()) 1730page_number = flask.request.args.get("page", 1, type=int) 1731if flask.session.get("username"): 1732default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1733else: 1734default_page_length = 16 1735page_length = flask.request.args.get("per_page", default_page_length, type=int) 1736page_listing = db.paginate(commits, page=page_number, per_page=page_length) 1737 1738if page_listing.has_next: 1739next_page = page_listing.next_num 1740else: 1741next_page = None 1742 1743if page_listing.has_prev: 1744prev_page = page_listing.prev_num 1745else: 1746prev_page = None 1747 1748return flask.render_template( 1749"repo-log.html", 1750username=username, 1751repository=repository, 1752branches=all_refs, 1753current=branch, 1754repo_data=repo_data, 1755repo=repo, 1756commits=page_listing, 1757remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1758is_favourite=get_favourite(flask.session.get("username"), username, repository), 1759page_number=page_number, 1760page_length=page_length, 1761next_page=next_page, 1762prev_page=prev_page, 1763num_pages=page_listing.pages 1764) 1765 1766 1767@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1768def repository_prs(username, repository): 1769server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1770if not os.path.exists(server_repo_location): 1771flask.abort(404) 1772if not (get_visibility(username, repository) or get_permission_level( 1773flask.session.get("username"), username, 1774repository) is not None): 1775flask.abort(403) 1776 1777if not os.path.exists(server_repo_location): 1778return flask.render_template("errors/not-found.html"), 404 1779 1780if flask.request.method == "GET": 1781repo = git.Repo(server_repo_location) 1782repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1783user = User.query.filter_by(username=flask.session.get("username")).first() 1784 1785return flask.render_template( 1786"repo-prs.html", 1787username=username, 1788repository=repository, 1789repo_data=repo_data, 1790repo=repo, 1791PullRequest=PullRequest, 1792remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1793is_favourite=get_favourite(flask.session.get("username"), username, repository), 1794default_branch=repo_data.default_branch, 1795branches=repo.branches 1796) 1797 1798elif "id" not in flask.request.form: 1799repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1800head = flask.request.form.get("head") 1801head_route = flask.request.form.get("headroute") 1802base = flask.request.form.get("base") 1803 1804if not head and base and head_route: 1805return flask.redirect(".", 400) 1806 1807repo_user = head_route.rpartition("/")[0].removeprefix("/") 1808print(repo_user) 1809if "@" not in repo_user: 1810# Same server; do some additional checks 1811try: 1812head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1813except git.exc.NoSuchPathError: 1814flask.flash(Markup( 1815"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Repository doesn't exist")), 1816category="error") 1817return flask.redirect(".", 303) 1818base_repo = git.Repo(server_repo_location) 1819 1820if head not in head_repo.branches or base not in base_repo.branches: 1821flask.flash(Markup( 1822"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1823category="error") 1824return flask.redirect(".", 303) 1825 1826head_data = db.session.get(Repo, head_route) 1827if not head_data.visibility: 1828flask.flash(Markup( 1829"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1830"Head can't be restricted")), 1831category="error") 1832return flask.redirect(".", 303) 1833 1834pull_request = PullRequest(head_route, head, repo_data, base, 1835db.session.get(User, flask.session["username"])) 1836 1837db.session.add(pull_request) 1838db.session.commit() 1839 1840# Create the notification 1841notification = Notification({"type": "pr", "head": pull_request.head_route, "base": pull_request.base.route, "pr": pull_request.id}) 1842db.session.add(notification) 1843db.session.commit() 1844 1845# Send a notification to all users who have enabled PR notifications for this repo 1846for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all(): 1847user = relationship.user 1848user_notification = UserNotification(user, notification, 1) 1849db.session.add(user_notification) 1850db.session.commit() 1851celery_tasks.send_notification.apply_async(args=[user_notification.id]) 1852 1853return flask.redirect(".", 303) 1854else: 1855id = flask.request.form.get("id") 1856pull_request = db.session.get(PullRequest, id) 1857 1858if not pull_request: 1859flask.abort(404) 1860 1861if not (get_visibility(username, repository) or get_permission_level( 1862flask.session.get("username"), username, 1863repository) >= 1 or pull_request.owner.username == flask.session.get("username")): 1864flask.abort(403) 1865 1866repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1867 1868if pull_request: 1869pull_request.resolves_list = flask.request.form.get("resolves") 1870db.session.commit() 1871 1872return flask.redirect(".", 303) 1873 1874 1875@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1876def repository_prs_merge(username, repository): 1877server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1878if not os.path.exists(server_repo_location): 1879flask.abort(404) 1880if not (get_visibility(username, repository) or get_permission_level( 1881flask.session.get("username"), username, 1882repository) is not None): 1883flask.abort(403) 1884 1885if not get_permission_level(flask.session.get("username"), username, repository): 1886flask.abort(401) 1887 1888repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1889repo = git.Repo(server_repo_location) 1890id = flask.request.form.get("id") 1891 1892pull_request = db.session.get(PullRequest, id) 1893 1894if pull_request.state != 0: 1895flask.abort(400) 1896 1897if pull_request: 1898result = celery_tasks.merge_heads.delay( 1899pull_request.head_route, 1900pull_request.head_branch, 1901pull_request.base_route, 1902pull_request.base_branch, 1903pull_request.id, 1904simulate=True, 1905method=flask.request.form.get("method"), # like merge, fast-forward, rebase 1906username=flask.session.get("username") 1907) 1908task_result = worker.AsyncResult(result.id) 1909 1910return flask.redirect(f"/task/{result.id}?pr-id={id}&method={flask.request.form.get('method')}", 303) # should be 202 Accepted but we must use a redirect 1911# db.session.delete(pull_request) 1912# db.session.commit() 1913else: 1914flask.abort(400) 1915 1916 1917@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1918def repository_prs_merge_stage_two(username, repository, id): 1919server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1920if not os.path.exists(server_repo_location): 1921flask.abort(404) 1922if not (get_visibility(username, repository) or get_permission_level( 1923flask.session.get("username"), username, 1924repository) is not None): 1925flask.abort(403) 1926 1927if not get_permission_level(flask.session.get("username"), username, repository): 1928flask.abort(401) 1929 1930repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1931repo = git.Repo(server_repo_location) 1932 1933pull_request = db.session.get(PullRequest, id) 1934 1935if pull_request: 1936result = celery_tasks.merge_heads.delay( 1937pull_request.head_route, 1938pull_request.head_branch, 1939pull_request.base_route, 1940pull_request.base_branch, 1941pull_request.id, 1942simulate=False, 1943username=flask.session.get("username") 1944) 1945task_result = worker.AsyncResult(result.id) 1946 1947db.session.commit() 1948 1949return flask.redirect(f"/task/{result.id}?pr-id={id}&method={flask.request.args.get('method')}", 303) # should be 202 Accepted but we must use a redirect 1950# db.session.delete(pull_request) 1951else: 1952flask.abort(400) 1953 1954 1955@app.route("/task/<task_id>") 1956def task_monitor(task_id): 1957task_result = worker.AsyncResult(task_id) 1958 1959if flask.request.args.get("partial"): 1960# htmx partial update 1961return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000) 1962 1963# Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long 1964return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125) 1965 1966 1967@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1968def repository_prs_delete(username, repository): 1969server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1970if not os.path.exists(server_repo_location): 1971flask.abort(404) 1972if not (get_visibility(username, repository) or get_permission_level( 1973flask.session.get("username"), username, 1974repository) is not None): 1975flask.abort(403) 1976 1977if not get_permission_level(flask.session.get("username"), username, repository): 1978flask.abort(401) 1979 1980repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1981repo = git.Repo(server_repo_location) 1982id = flask.request.form.get("id") 1983 1984pull_request = db.session.get(PullRequest, id) 1985 1986if pull_request: 1987pull_request.state = 2 1988db.session.commit() 1989 1990return flask.redirect(".", 303) 1991 1992 1993@repositories.route("/<username>/<repository>/settings/") 1994def repository_settings(username, repository): 1995if get_permission_level(flask.session.get("username"), username, repository) != 2: 1996flask.abort(401) 1997 1998repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository)) 1999 2000site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>") 2001primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>") 2002 2003return flask.render_template("repo-settings.html", username=username, repository=repository, 2004repo_data=db.session.get(Repo, f"/{username}/{repository}"), 2005branches=[branch.name for branch in repo.branches], 2006site_link=site_link, primary_site_link=primary_site_link, 2007remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 2008is_favourite=get_favourite(flask.session.get("username"), username, repository), 2009) 2010 2011 2012@repositories.route("/<username>/<repository>/settings/", methods=["POST"]) 2013def repository_settings_post(username, repository): 2014if get_permission_level(flask.session.get("username"), username, repository) != 2: 2015flask.abort(401) 2016 2017repo = db.session.get(Repo, f"/{username}/{repository}") 2018 2019repo.visibility = flask.request.form.get("visibility", type=int) 2020repo.info = flask.request.form.get("description") 2021repo.default_branch = flask.request.form.get("default_branch") 2022repo.url = flask.request.form.get("url") 2023 2024# Update site settings 2025had_site = repo.has_site 2026old_branch = repo.site_branch 2027if flask.request.form.get("site_branch"): 2028repo.site_branch = flask.request.form.get("site_branch") 2029if flask.request.form.get("primary_site"): 2030if had_site != 2: 2031# Remove primary site from other repos 2032for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2): 2033other_repo.has_site = 1 # switch it to a regular site 2034flask.flash(Markup( 2035_("Your repository {repository} has been demoted from a primary site to a regular site because there can only be one primary site per user.").format( 2036repository=other_repo.route 2037)), category="warning") 2038repo.has_site = 2 2039else: 2040repo.has_site = 1 2041else: 2042repo.site_branch = None 2043repo.has_site = 0 2044 2045db.session.commit() 2046 2047if not (had_site, old_branch) == (repo.has_site, repo.site_branch): 2048# Deploy the newly activated site 2049result = celery_tasks.copy_site.delay(repo.route) 2050 2051if had_site and not repo.has_site: 2052# Remove the site 2053result = celery_tasks.delete_site.delay(repo.route) 2054 2055if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site): 2056# Deploy all other sites which were destroyed by the primary site 2057for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1): 2058result = celery_tasks.copy_site.delay(other_repo.route) 2059 2060return flask.redirect(f"/{username}/{repository}/settings", 303) 2061 2062 2063@repositories.route("/<username>/<repository>/settings/add-label", methods=["POST"]) 2064def repository_settings_add_label(username, repository): 2065if get_permission_level(flask.session.get("username"), username, repository) != 2: 2066flask.abort(401) 2067 2068repo_data = db.session.get(Repo, f"/{username}/{repository}") 2069 2070label = Label(repo_data, flask.request.form.get("label"), flask.request.form.get("colour")) 2071db.session.add(label) 2072db.session.commit() 2073 2074return flask.redirect(f"/{username}/{repository}/settings", 303) 2075 2076 2077@repositories.route("/<username>/<repository>/settings/delete-label", methods=["POST"]) 2078def repository_settings_delete_label(username, repository): 2079if get_permission_level(flask.session.get("username"), username, repository) != 2: 2080flask.abort(401) 2081 2082repo_data = db.session.get(Repo, f"/{username}/{repository}") 2083 2084label = db.session.get(Label, flask.request.form.get("id")) 2085 2086db.session.delete(label) 2087db.session.commit() 2088 2089return flask.redirect(f"/{username}/{repository}/settings", 303) 2090 2091 2092@repositories.route("/<username>/<repository>/settings/edit-label", methods=["POST"]) 2093def repository_settings_edit_label(username, repository): 2094if get_permission_level(flask.session.get("username"), username, repository) != 2: 2095flask.abort(401) 2096 2097repo_data = db.session.get(Repo, f"/{username}/{repository}") 2098 2099label = db.session.get(Label, flask.request.form.get("id")) 2100 2101label.name = flask.request.form.get("label") 2102label.colour_hex = flask.request.form.get("colour") 2103 2104db.session.commit() 2105 2106return flask.redirect(f"/{username}/{repository}/settings", 303) 2107 2108 2109@repositories.route("/<username>/<repository>/settings/delete", methods=["POST"]) 2110def repository_settings_delete(username, repository): 2111if username != flask.session.get("username"): 2112flask.abort(401) 2113 2114repo = db.session.get(Repo, f"/{username}/{repository}") 2115 2116if not repo: 2117flask.abort(404) 2118 2119user = db.session.get(User, flask.session.get("username")) 2120 2121if not bcrypt.check_password_hash(user.password_hashed, flask.request.form.get("password")): 2122flask.flash(_("Incorrect password"), category="error") 2123flask.abort(401) 2124 2125if repo.has_site: 2126celery_tasks.delete_site.delay(repo.route) 2127 2128db.session.delete(repo) 2129db.session.commit() 2130 2131shutil.rmtree(os.path.join(config.REPOS_PATH, username, repository)) 2132 2133return flask.redirect(f"/{username}", 303) 2134 2135 2136@app.errorhandler(404) 2137def e404(error): 2138return flask.render_template("errors/not-found.html"), 404 2139 2140 2141@app.errorhandler(401) 2142def e401(error): 2143return flask.render_template("errors/unauthorised.html"), 401 2144 2145 2146@app.errorhandler(403) 2147def e403(error): 2148return flask.render_template("errors/forbidden.html"), 403 2149 2150 2151@app.errorhandler(418) 2152def e418(error): 2153return flask.render_template("errors/teapot.html"), 418 2154 2155 2156@app.errorhandler(405) 2157def e405(error): 2158return flask.render_template("errors/method-not-allowed.html"), 405 2159 2160 2161@app.errorhandler(500) 2162def e500(error): 2163return flask.render_template("errors/server-error.html"), 500 2164 2165 2166@app.errorhandler(400) 2167def e400(error): 2168return flask.render_template("errors/bad-request.html"), 400 2169 2170 2171@app.errorhandler(410) 2172def e410(error): 2173return flask.render_template("errors/gone.html"), 410 2174 2175 2176@app.errorhandler(415) 2177def e415(error): 2178return flask.render_template("errors/media-type.html"), 415 2179 2180 2181if __name__ == "__main__": 2182app.run(debug=True, port=8080, host="0.0.0.0") 2183 2184app.register_blueprint(repositories) 2185