app.py
Python script, Unicode text, UTF-8 text executable
1__version__ = "0.2.0" 2 3import os 4import shutil 5import random 6import subprocess 7import platform 8 9import PIL 10import git 11import mimetypes 12import magic 13import flask 14import cairosvg 15import celery 16import shlex 17from functools import wraps 18from datetime import datetime 19from enum import Enum 20from cairosvg import svg2png 21from flask_sqlalchemy import SQLAlchemy 22from flask_bcrypt import Bcrypt 23from markupsafe import escape, Markup 24from flask_migrate import Migrate 25from PIL import Image 26from flask_httpauth import HTTPBasicAuth 27import config 28from common import git_command 29from flask_babel import Babel, gettext, ngettext, force_locale 30 31_ = gettext 32n_ = ngettext 33 34app = flask.Flask(__name__) 35app.config.from_mapping( 36CELERY=dict( 37broker_url=config.REDIS_URI, 38result_backend=config.REDIS_URI, 39task_ignore_result=True, 40), 41) 42 43auth = HTTPBasicAuth() 44 45app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 46app.config["SECRET_KEY"] = config.DB_PASSWORD 47app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 48app.config["BABEL_TRANSLATION_DIRECTORIES"] = "i18n" 49app.config["MAX_CONTENT_LENGTH"] = config.MAX_PAYLOAD_SIZE 50app.config["SESSION_COOKIE_SAMESITE"] = "Lax" 51app.config["SESSION_COOKIE_SECURE"] = config.suggest_https # only send cookies over HTTPS if the server is configured for it 52app.config["SESSION_COOKIE_HTTPONLY"] = True # don't allow JS to access the cookie 53app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN # don't share across subdomains, since user content is hosted there 54 55db = SQLAlchemy(app) 56bcrypt = Bcrypt(app) 57migrate = Migrate(app, db) 58 59from misc_utils import * 60 61import git_http 62import jinja_utils 63import celery_tasks 64from celery import Celery, Task 65import celery_integration 66import pathlib 67 68from models import * 69 70babel = Babel(app) 71 72 73def get_locale(): 74if flask.request.cookies.get("language"): 75return flask.request.cookies.get("language") 76return flask.request.accept_languages.best_match(config.available_locales) 77 78 79babel.init_app(app, locale_selector=get_locale) 80 81with app.app_context(): 82locale_names = {} 83for language in config.available_locales: 84with force_locale(language): 85# NOTE: Translate this to the language's name in that language, for example in French you would use français 86locale_names[language] = gettext("English") 87 88worker = celery_integration.init_celery_app(app) 89 90repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/") 91 92app.jinja_env.add_extension("jinja2.ext.do") 93app.jinja_env.add_extension("jinja2.ext.loopcontrols") 94app.jinja_env.add_extension("jinja2.ext.debug") 95 96 97@app.context_processor 98def default(): 99username = flask.session.get("username") 100 101user_object = User.query.filter_by(username=username).first() 102 103return { 104"logged_in_user": username, 105"user_object": user_object, 106"Notification": Notification, 107"unread": UserNotification.query.filter_by(user_username=username).filter( 108UserNotification.attention_level > 0).count(), 109"config": config, 110"Markup": Markup, 111"locale_names": locale_names, 112} 113 114 115@app.route("/") 116def main(): 117if flask.session.get("username"): 118return flask.render_template("home.html") 119else: 120return flask.render_template("no-home.html") 121 122 123@app.route("/userstyle") 124def userstyle(): 125if flask.session.get("username") and os.path.exists( 126os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config", 127"theme.css")): 128return flask.send_from_directory( 129os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config"), 130"theme.css") 131else: 132return flask.Response("", mimetype="text/css") 133 134 135@app.route("/about/") 136def about(): 137return flask.render_template("about.html", platform=platform, version=__version__) 138 139 140@app.route("/search") 141def search(): 142query = flask.request.args.get("q") 143if not query: 144query = "" 145 146results = Repo.query.filter(Repo.name.ilike(f"%{query}%")).filter_by(visibility=2).all() 147 148return flask.render_template("search.html", results=results, query=query) 149 150 151@app.route("/language", methods=["POST"]) 152def set_locale(): 153response = flask.redirect(flask.request.referrer if flask.request.referrer else "/", 154code=303) 155if not flask.request.form.get("language"): 156response.delete_cookie("language") 157else: 158response.set_cookie("language", flask.request.form.get("language")) 159 160return response 161 162 163@app.route("/cookie-dismiss") 164def dismiss_banner(): 165response = flask.redirect(flask.request.referrer if flask.request.referrer else "/", 166code=303) 167response.set_cookie("cookie-banner", "1") 168return response 169 170 171@app.route("/help/") 172def help_redirect(): 173return flask.redirect(config.help_url, code=302) 174 175 176@app.route("/settings/") 177def settings(): 178if not flask.session.get("username"): 179flask.abort(401) 180user = User.query.filter_by(username=flask.session.get("username")).first() 181 182return flask.render_template("user-settings.html", user=user) 183 184 185@app.route("/settings/profile", methods=["POST"]) 186def settings_profile(): 187user = User.query.filter_by(username=flask.session.get("username")).first() 188 189user.display_name = flask.request.form["displayname"] 190user.URL = flask.request.form["url"] 191user.company = flask.request.form["company"] 192user.company_URL = flask.request.form["companyurl"] 193user.email = flask.request.form.get("email") if flask.request.form.get( 194"email") else None 195user.location = flask.request.form["location"] 196user.show_mail = True if flask.request.form.get("showmail") else False 197user.bio = flask.request.form.get("bio") 198 199db.session.commit() 200 201flask.flash( 202Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")), 203category="success") 204return flask.redirect(f"/{flask.session.get('username')}", code=303) 205 206 207@app.route("/settings/preferences", methods=["POST"]) 208def settings_prefs(): 209user = User.query.filter_by(username=flask.session.get("username")).first() 210 211user.default_page_length = int(flask.request.form["page_length"]) 212user.max_post_nesting = int(flask.request.form["max_post_nesting"]) 213 214db.session.commit() 215 216flask.flash( 217Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")), 218category="success") 219return flask.redirect(f"/{flask.session.get('username')}", code=303) 220 221 222@app.route("/favourites/", methods=["GET", "POST"]) 223def favourites(): 224if not flask.session.get("username"): 225flask.abort(401) 226if flask.request.method == "GET": 227relationships = RepoFavourite.query.filter_by( 228user_username=flask.session.get("username")) 229 230return flask.render_template("favourites.html", favourites=relationships) 231 232 233@app.route("/favourites/<int:id>", methods=["POST"]) 234def favourite_edit(id): 235if not flask.session.get("username"): 236flask.abort(401) 237favourite = db.session.get(RepoFavourite, id) 238if favourite.user_username != flask.session.get("username"): 239flask.abort(403) 240data = flask.request.form 241print(data) 242favourite.notify_commit = js_to_bool(data.get("commit")) 243favourite.notify_forum = js_to_bool(data.get("forum")) 244favourite.notify_pr = js_to_bool(data.get("pull_request")) 245favourite.notify_admin = js_to_bool(data.get("administrative")) 246print(favourite.notify_commit, favourite.notify_forum, favourite.notify_pr, 247favourite.notify_admin) 248db.session.commit() 249return flask.render_template_string( 250""" 251<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"> 252<td><a href="{{ favourite.repo.route }}">{{ favourite.repo.owner.username }}/{{ favourite.repo.name }}</a></td> 253<td style="text-align: center;"><input type="checkbox" name="commit" id="commit-{{ favourite.id }}" value="true" {% if favourite.notify_commit %}checked{% endif %}></td> 254<td style="text-align: center;"><input type="checkbox" name="forum" id="forum-{{ favourite.id }}" value="true" {% if favourite.notify_forum %}checked{% endif %}></td> 255<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> 256<td style="text-align: center;"><input type="checkbox" name="administrative" id="administrative-{{ favourite.id }}" value="true" {% if favourite.notify_admin %}checked{% endif %}></td> 257</tr> 258""", 259favourite=favourite 260) 261 262 263@app.route("/notifications/", methods=["GET", "POST"]) 264def notifications(): 265if not flask.session.get("username"): 266flask.abort(401) 267if flask.request.method == "GET": 268return flask.render_template("notifications.html", 269notifications=UserNotification.query.filter_by( 270user_username=flask.session.get("username") 271).order_by(UserNotification.id.desc()), 272db=db, Commit=Commit 273) 274 275 276@app.route("/notifications/<int:notification_id>/read", methods=["POST"]) 277def mark_read(notification_id): 278if not flask.session.get("username"): 279flask.abort(401) 280notification = UserNotification.query.filter_by(id=notification_id).first() 281if notification.user_username != flask.session.get("username"): 282flask.abort(403) 283notification.mark_read() 284db.session.commit() 285return flask.render_template_string( 286"<button hx-post='/notifications/{{ notification.id }}/unread' hx-swap='outerHTML'>Mark as unread</button>", 287notification=notification), 200 288 289 290@app.route("/notifications/<int:notification_id>/unread", methods=["POST"]) 291def mark_unread(notification_id): 292if not flask.session.get("username"): 293flask.abort(401) 294notification = UserNotification.query.filter_by(id=notification_id).first() 295if notification.user_username != flask.session.get("username"): 296flask.abort(403) 297notification.mark_unread() 298db.session.commit() 299return flask.render_template_string( 300"<button hx-post='/notifications/{{ notification.id }}/read' hx-swap='outerHTML'>Mark as read</button>", 301notification=notification), 200 302 303 304@app.route("/notifications/mark-all-read", methods=["POST"]) 305def mark_all_read(): 306if not flask.session.get("username"): 307flask.abort(401) 308 309notifications = UserNotification.query.filter_by( 310user_username=flask.session.get("username")) 311for notification in notifications: 312notification.mark_read() 313db.session.commit() 314return flask.redirect("/notifications/", code=303) 315 316 317@app.route("/accounts/", methods=["GET", "POST"]) 318def login(): 319if flask.request.method == "GET": 320return flask.render_template("login.html") 321else: 322if "login" in flask.request.form: 323username = flask.request.form["username"] 324password = flask.request.form["password"] 325 326user = User.query.filter_by(username=username).first() 327 328if user and bcrypt.check_password_hash(user.password_hashed, password): 329flask.session["username"] = user.username 330flask.flash( 331Markup("<iconify-icon icon='mdi:account'></iconify-icon>" + _( 332"Successfully logged in as {username}").format( 333username=username)), 334category="success") 335return flask.redirect("/", code=303) 336elif not user: 337flask.flash(Markup( 338"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _( 339"User not found")), 340category="alert") 341return flask.render_template("login.html") 342else: 343flask.flash(Markup( 344"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _( 345"Invalid password")), 346category="error") 347return flask.render_template("login.html") 348if "signup" in flask.request.form: 349username = flask.request.form["username"] 350password = flask.request.form["password"] 351password2 = flask.request.form["password2"] 352email = flask.request.form.get("email") 353email2 = flask.request.form.get("email2") # repeat email is a honeypot 354name = flask.request.form.get("name") 355 356if not only_chars(username, 357"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"): 358flask.flash(Markup( 359_("Usernames may only contain Latin alphabet, numbers and '-'")), 360category="error") 361return flask.render_template("login.html") 362if "--" in username: 363flask.flash(Markup( 364_("Usernames may not contain consecutive hyphens")), 365category="error") 366return flask.render_template("login.html") 367if username.startswith("-") or username.endswith("-"): 368flask.flash(Markup( 369_("Usernames may not start or end with a hyphen")), 370category="error") 371return flask.render_template("login.html") 372if username in config.RESERVED_NAMES: 373flask.flash( 374Markup( 375_("Sorry, {username} is a system path").format( 376username=username)), 377category="error") 378return flask.render_template("login.html") 379 380if not username.islower(): 381if not name: # infer display name from the wanted username if not customised 382display_name = username 383username = username.lower() 384flask.flash(Markup( 385_("Usernames must be lowercase, so it's been converted automatically")), 386category="info") 387 388user_check = User.query.filter_by(username=username).first() 389if user_check or email2: # make the honeypot look like a normal error 390flask.flash( 391Markup( 392_( 393"The username {username} is taken").format( 394username=username)), 395category="error") 396return flask.render_template("login.html") 397 398if password2 != password: 399flask.flash(Markup(_( 400"Make sure the passwords match")), 401category="error") 402return flask.render_template("login.html") 403 404user = User(username, password, email, name) 405db.session.add(user) 406db.session.commit() 407flask.session["username"] = user.username 408flask.flash(Markup( 409_( 410"Successfully created and logged in as {username}").format( 411username=username)), 412category="success") 413 414notification = Notification({"type": "welcome"}) 415db.session.add(notification) 416db.session.commit() 417 418return flask.redirect("/", code=303) 419 420 421@app.route("/newrepo/", methods=["GET", "POST"]) 422def new_repo(): 423if not flask.session.get("username"): 424flask.abort(401) 425if flask.request.method == "GET": 426return flask.render_template("new-repo.html") 427else: 428name = flask.request.form["name"] 429visibility = int(flask.request.form["visibility"]) 430 431if not only_chars(name, 432"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 433flask.flash(Markup( 434"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 435"Repository names may only contain Latin alphabet, numbers, '-' and '_'")), 436category="error") 437return flask.render_template("new-repo.html") 438 439user = User.query.filter_by(username=flask.session.get("username")).first() 440 441repo = Repo(user, name, visibility) 442db.session.add(repo) 443db.session.commit() 444 445flask.flash(Markup(_("Successfully created repository {name}").format(name=name)), 446category="success") 447return flask.redirect(repo.route, code=303) 448 449 450@app.route("/logout") 451def logout(): 452flask.session.clear() 453flask.flash(Markup( 454"<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")), 455category="info") 456return flask.redirect("/", code=303) 457 458 459@app.route("/<username>/", methods=["GET", "POST"]) 460def user_profile(username): 461if db.session.get(User, username) is None: 462flask.abort(404) 463old_relationship = UserFollow.query.filter_by( 464follower_username=flask.session.get("username"), 465followed_username=username).first() 466if flask.request.method == "GET": 467user = User.query.filter_by(username=username).first() 468match flask.request.args.get("action"): 469case "repositories": 470repos = Repo.query.filter_by(owner_name=username, visibility=2) 471return flask.render_template("user-profile-repositories.html", user=user, 472repos=repos, 473relationship=old_relationship) 474case "followers": 475return flask.render_template("user-profile-followers.html", user=user, 476relationship=old_relationship) 477case "follows": 478return flask.render_template("user-profile-follows.html", user=user, 479relationship=old_relationship) 480case _: 481return flask.render_template("user-profile-overview.html", user=user, 482relationship=old_relationship) 483 484elif flask.request.method == "POST": 485match flask.request.args.get("action"): 486case "follow": 487if username == flask.session.get("username"): 488flask.abort(403) 489if old_relationship: 490db.session.delete(old_relationship) 491else: 492relationship = UserFollow( 493flask.session.get("username"), 494username 495) 496db.session.add(relationship) 497db.session.commit() 498 499user = db.session.get(User, username) 500author = db.session.get(User, flask.session.get("username")) 501notification = Notification({"type": "update", "version": "0.0.0"}) 502db.session.add(notification) 503db.session.commit() 504 505db.session.commit() 506return flask.redirect("?", code=303) 507 508 509@app.route("/<username>/<repository>/") 510def repository_index(username, repository): 511return flask.redirect("./tree", code=302) 512 513 514@app.route("/info/<username>/avatar") 515def user_avatar(username): 516server_userdata_location = os.path.join(config.USERDATA_PATH, username) 517if not os.path.exists(server_userdata_location): 518return flask.render_template("not-found.html"), 404 519 520return flask.send_from_directory(server_userdata_location, "avatar.png") 521 522 523@app.route("/info/<username>/avatar", methods=["POST"]) 524def user_avatar_upload(username): 525server_userdata_location = os.path.join(config.USERDATA_PATH, username) 526 527if not os.path.exists(server_userdata_location): 528flask.abort(404) 529if not flask.session.get("username") == username: 530flask.abort(403) 531 532# Convert image to PNG 533try: 534image = Image.open(flask.request.files["avatar"]) 535except PIL.UnidentifiedImageError: 536flask.abort(400) 537image.save(os.path.join(server_userdata_location, "avatar.png")) 538 539return flask.redirect(f"/{username}", code=303) 540 541 542@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 543def repository_raw(username, repository, branch, subpath): 544server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 545if not os.path.exists(server_repo_location): 546app.logger.error(f"Cannot load {server_repo_location}") 547flask.abort(404) 548if not (get_visibility(username, repository) or get_permission_level( 549flask.session.get("username"), username, 550repository) is not None): 551flask.abort(403) 552 553app.logger.info(f"Loading {server_repo_location}") 554 555if not os.path.exists(server_repo_location): 556app.logger.error(f"Cannot load {server_repo_location}") 557return flask.render_template("not-found.html"), 404 558 559repo = git.Repo(server_repo_location) 560repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 561if not repo_data.default_branch: 562if repo.heads: 563repo_data.default_branch = repo.heads[0].name 564else: 565return flask.render_template("empty.html", 566remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 567if not branch: 568branch = repo_data.default_branch 569return flask.redirect(f"./{branch}", code=302) 570 571if branch.startswith("tag:"): 572ref = f"tags/{branch[4:]}" 573elif branch.startswith("~"): 574ref = branch[1:] 575else: 576ref = f"heads/{branch}" 577 578ref = ref.replace("~", "/") # encode slashes for URL support 579 580try: 581repo.git.checkout("-f", ref) 582except git.exc.GitCommandError: 583return flask.render_template("not-found.html"), 404 584 585return flask.send_from_directory(config.REPOS_PATH, 586os.path.join(username, repository, subpath)) 587 588 589@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 590@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 591@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 592def repository_tree(username, repository, branch, subpath): 593server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 594if not os.path.exists(server_repo_location): 595app.logger.error(f"Cannot load {server_repo_location}") 596flask.abort(404) 597if not (get_visibility(username, repository) or get_permission_level( 598flask.session.get("username"), username, 599repository) is not None): 600flask.abort(403) 601 602app.logger.info(f"Loading {server_repo_location}") 603 604repo = git.Repo(server_repo_location) 605repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 606if not repo_data.default_branch: 607if repo.heads: 608repo_data.default_branch = repo.heads[0].name 609else: 610return flask.render_template("empty.html", 611remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 612if not branch: 613branch = repo_data.default_branch 614return flask.redirect(f"./{branch}", code=302) 615 616if branch.startswith("tag:"): 617ref = f"tags/{branch[4:]}" 618elif branch.startswith("~"): 619ref = branch[1:] 620else: 621ref = f"heads/{branch}" 622 623ref = ref.replace("~", "/") # encode slashes for URL support 624 625try: 626repo.git.checkout("-f", ref) 627except git.exc.GitCommandError: 628return flask.render_template("not-found.html"), 404 629 630branches = repo.heads 631 632all_refs = [] 633for ref in repo.heads: 634all_refs.append((ref, "head")) 635for ref in repo.tags: 636all_refs.append((ref, "tag")) 637 638if os.path.isdir(os.path.join(server_repo_location, subpath)): 639files = [] 640blobs = [] 641 642for entry in os.listdir(os.path.join(server_repo_location, subpath)): 643if not os.path.basename(entry) == ".git": 644files.append(os.path.join(subpath, entry)) 645 646infos = [] 647 648for file in files: 649path = os.path.join(server_repo_location, file) 650mimetype = guess_mime(path) 651 652text = git_command(server_repo_location, None, "log", "--format='%H\n'", 653shlex.quote(file)).decode() 654 655sha = text.split("\n")[0] 656identifier = f"/{username}/{repository}/{sha}" 657 658last_commit = db.session.get(Commit, identifier) 659 660info = { 661"name": os.path.basename(file), 662"serverPath": path, 663"relativePath": file, 664"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 665"size": human_size(os.path.getsize(path)), 666"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 667"commit": last_commit, 668"shaSize": 7, 669} 670 671special_icon = config.match_icon(os.path.basename(file)) 672if special_icon: 673info["icon"] = special_icon 674elif os.path.isdir(path): 675info["icon"] = config.folder_icon 676elif mimetypes.guess_type(path)[0] in config.file_icons: 677info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]] 678else: 679info["icon"] = config.unknown_icon 680 681if os.path.isdir(path): 682infos.insert(0, info) 683else: 684infos.append(info) 685 686return flask.render_template( 687"repo-tree.html", 688username=username, 689repository=repository, 690files=infos, 691subpath=os.path.join("/", subpath), 692branches=all_refs, 693current=branch, 694remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 695is_favourite=get_favourite(flask.session.get("username"), username, repository), 696repo_data=repo_data, 697) 698else: 699path = os.path.join(server_repo_location, subpath) 700 701if not os.path.exists(path): 702return flask.render_template("not-found.html"), 404 703 704mimetype = guess_mime(path) 705mode = mimetype.split("/", 1)[0] 706size = human_size(os.path.getsize(path)) 707 708special_icon = config.match_icon(os.path.basename(path)) 709if special_icon: 710icon = special_icon 711elif os.path.isdir(path): 712icon = config.folder_icon 713elif mimetypes.guess_type(path)[0] in config.file_icons: 714icon = config.file_icons[mimetypes.guess_type(path)[0]] 715else: 716icon = config.unknown_icon 717 718contents = None 719if mode == "text": 720contents = convert_to_html(path) 721 722return flask.render_template( 723"repo-file.html", 724username=username, 725repository=repository, 726file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 727branches=all_refs, 728current=branch, 729mode=mode, 730mimetype=mimetype, 731detailedtype=magic.from_file(path), 732size=size, 733icon=icon, 734subpath=os.path.join("/", subpath), 735extension=pathlib.Path(path).suffix, 736basename=os.path.basename(path), 737contents=contents, 738remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 739is_favourite=get_favourite(flask.session.get("username"), username, repository), 740repo_data=repo_data, 741) 742 743 744@repositories.route("/<username>/<repository>/commit/<sha>") 745def repository_commit(username, repository, sha): 746server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 747if not os.path.exists(server_repo_location): 748app.logger.error(f"Cannot load {server_repo_location}") 749flask.abort(404) 750if not (get_visibility(username, repository) or get_permission_level( 751flask.session.get("username"), username, 752repository) is not None): 753flask.abort(403) 754 755app.logger.info(f"Loading {server_repo_location}") 756 757if not os.path.exists(server_repo_location): 758app.logger.error(f"Cannot load {server_repo_location}") 759return flask.render_template("not-found.html"), 404 760 761repo = git.Repo(server_repo_location) 762repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 763 764files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r", 765"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1] 766 767print(files) 768 769return flask.render_template( 770"repo-commit.html", 771username=username, 772repository=repository, 773remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 774is_favourite=get_favourite(flask.session.get("username"), username, repository), 775diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff", 776str(sha) + "^!", "--", file).decode().split("\n") for 777file in files}, 778data=db.session.get(Commit, f"/{username}/{repository}/{sha}"), 779repo_data=repo_data, 780) 781 782 783@repositories.route("/<username>/<repository>/forum/") 784def repository_forum(username, repository): 785server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 786if not os.path.exists(server_repo_location): 787app.logger.error(f"Cannot load {server_repo_location}") 788flask.abort(404) 789if not (get_visibility(username, repository) or get_permission_level( 790flask.session.get("username"), username, 791repository) is not None): 792flask.abort(403) 793 794app.logger.info(f"Loading {server_repo_location}") 795 796if not os.path.exists(server_repo_location): 797app.logger.error(f"Cannot load {server_repo_location}") 798return flask.render_template("not-found.html"), 404 799 800repo = git.Repo(server_repo_location) 801repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 802user = User.query.filter_by(username=flask.session.get("username")).first() 803relationships = RepoAccess.query.filter_by(repo=repo_data) 804user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 805 806return flask.render_template( 807"repo-forum.html", 808username=username, 809repository=repository, 810repo_data=repo_data, 811relationships=relationships, 812repo=repo, 813user_relationship=user_relationship, 814Post=Post, 815remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 816is_favourite=get_favourite(flask.session.get("username"), username, repository), 817default_branch=repo_data.default_branch 818) 819 820 821@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 822def repository_forum_topic(username, repository, id): 823server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 824if not os.path.exists(server_repo_location): 825app.logger.error(f"Cannot load {server_repo_location}") 826flask.abort(404) 827if not (get_visibility(username, repository) or get_permission_level( 828flask.session.get("username"), username, 829repository) is not None): 830flask.abort(403) 831 832app.logger.info(f"Loading {server_repo_location}") 833 834if not os.path.exists(server_repo_location): 835app.logger.error(f"Cannot load {server_repo_location}") 836return flask.render_template("not-found.html"), 404 837 838repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 839user = User.query.filter_by(username=flask.session.get("username")).first() 840relationships = RepoAccess.query.filter_by(repo=repo_data) 841user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 842 843post = Post.query.filter_by(id=id).first() 844 845return flask.render_template( 846"repo-topic.html", 847username=username, 848repository=repository, 849repo_data=repo_data, 850relationships=relationships, 851user_relationship=user_relationship, 852post=post, 853remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 854is_favourite=get_favourite(flask.session.get("username"), username, repository), 855default_branch=repo_data.default_branch 856) 857 858 859@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 860def repository_forum_new(username, repository): 861server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 862if not os.path.exists(server_repo_location): 863app.logger.error(f"Cannot load {server_repo_location}") 864flask.abort(404) 865if not (get_visibility(username, repository) or get_permission_level( 866flask.session.get("username"), username, 867repository) is not None): 868flask.abort(403) 869 870app.logger.info(f"Loading {server_repo_location}") 871 872if not os.path.exists(server_repo_location): 873app.logger.error(f"Cannot load {server_repo_location}") 874return flask.render_template("not-found.html"), 404 875 876repo = git.Repo(server_repo_location) 877repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 878user = User.query.filter_by(username=flask.session.get("username")).first() 879relationships = RepoAccess.query.filter_by(repo=repo_data) 880user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 881 882post = Post(user, repo_data, None, flask.request.form["subject"], 883flask.request.form["message"]) 884 885db.session.add(post) 886db.session.commit() 887 888return flask.redirect( 889flask.url_for(".repository_forum_thread", username=username, repository=repository, 890post_id=post.number), 891code=303) 892 893 894@repositories.route("/<username>/<repository>/forum/<int:post_id>") 895def repository_forum_thread(username, repository, post_id): 896server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 897if not os.path.exists(server_repo_location): 898app.logger.error(f"Cannot load {server_repo_location}") 899flask.abort(404) 900if not (get_visibility(username, repository) or get_permission_level( 901flask.session.get("username"), username, 902repository) is not None): 903flask.abort(403) 904 905app.logger.info(f"Loading {server_repo_location}") 906 907if not os.path.exists(server_repo_location): 908app.logger.error(f"Cannot load {server_repo_location}") 909return flask.render_template("not-found.html"), 404 910 911repo = git.Repo(server_repo_location) 912repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 913user = User.query.filter_by(username=flask.session.get("username")).first() 914relationships = RepoAccess.query.filter_by(repo=repo_data) 915user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 916 917if user: 918max_post_nesting = user.max_post_nesting 919else: 920max_post_nesting = 2 921 922return flask.render_template( 923"repo-forum-thread.html", 924username=username, 925repository=repository, 926repo_data=repo_data, 927relationships=relationships, 928repo=repo, 929Post=Post, 930user_relationship=user_relationship, 931post_id=post_id, 932max_post_nesting=max_post_nesting, 933remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 934is_favourite=get_favourite(flask.session.get("username"), username, repository), 935parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 936has_permission=not ((not get_permission_level(flask.session.get("username"), username, 937repository)) and db.session.get(Post, 938f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")), 939) 940 941 942@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state", 943methods=["POST"]) 944def repository_forum_change_state(username, repository, post_id): 945server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 946if not os.path.exists(server_repo_location): 947app.logger.error(f"Cannot load {server_repo_location}") 948flask.abort(404) 949if (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"): 950flask.abort(403) 951 952app.logger.info(f"Loading {server_repo_location}") 953 954repo = git.Repo(server_repo_location) 955repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 956user = User.query.filter_by(username=flask.session.get("username")).first() 957relationships = RepoAccess.query.filter_by(repo=repo_data) 958user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 959 960post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 961 962if not post: 963flask.abort(404) 964 965post.state = int(flask.request.form["new-state"]) 966 967db.session.commit() 968 969return flask.redirect( 970flask.url_for(".repository_forum_thread", username=username, repository=repository, 971post_id=post_id), 972code=303) 973 974 975@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 976def repository_forum_reply(username, repository, post_id): 977server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 978if not os.path.exists(server_repo_location): 979app.logger.error(f"Cannot load {server_repo_location}") 980flask.abort(404) 981if not (get_visibility(username, repository) or get_permission_level( 982flask.session.get("username"), username, 983repository) is not None): 984flask.abort(403) 985 986app.logger.info(f"Loading {server_repo_location}") 987 988if not os.path.exists(server_repo_location): 989app.logger.error(f"Cannot load {server_repo_location}") 990return flask.render_template("not-found.html"), 404 991 992repo = git.Repo(server_repo_location) 993repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 994user = User.query.filter_by(username=flask.session.get("username")).first() 995relationships = RepoAccess.query.filter_by(repo=repo_data) 996user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 997if not user: 998flask.abort(401) 999 1000parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1001post = Post(user, repo_data, parent, flask.request.form["subject"], 1002flask.request.form["message"]) 1003 1004db.session.add(post) 1005post.update_date() 1006db.session.commit() 1007 1008return flask.redirect( 1009flask.url_for(".repository_forum_thread", username=username, repository=repository, 1010post_id=post_id), 1011code=303) 1012 1013 1014@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 1015defaults={"score": 1}) 1016@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 1017defaults={"score": -1}) 1018@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 1019def repository_forum_vote(username, repository, post_id, score): 1020server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1021if not os.path.exists(server_repo_location): 1022app.logger.error(f"Cannot load {server_repo_location}") 1023flask.abort(404) 1024if not (get_visibility(username, repository) or get_permission_level( 1025flask.session.get("username"), username, 1026repository) is not None): 1027flask.abort(403) 1028 1029app.logger.info(f"Loading {server_repo_location}") 1030 1031if not os.path.exists(server_repo_location): 1032app.logger.error(f"Cannot load {server_repo_location}") 1033return flask.render_template("not-found.html"), 404 1034 1035repo = git.Repo(server_repo_location) 1036repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1037user = User.query.filter_by(username=flask.session.get("username")).first() 1038relationships = RepoAccess.query.filter_by(repo=repo_data) 1039user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1040if not user: 1041flask.abort(401) 1042 1043post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1044 1045if score: 1046old_relationship = PostVote.query.filter_by(user_username=user.username, 1047post_identifier=post.identifier).first() 1048if old_relationship: 1049if score == old_relationship.vote_score: 1050db.session.delete(old_relationship) 1051post.vote_sum -= old_relationship.vote_score 1052else: 1053post.vote_sum -= old_relationship.vote_score 1054post.vote_sum += score 1055old_relationship.vote_score = score 1056else: 1057relationship = PostVote(user, post, score) 1058post.vote_sum += score 1059db.session.add(relationship) 1060 1061db.session.commit() 1062 1063user_vote = PostVote.query.filter_by(user_username=user.username, 1064post_identifier=post.identifier).first() 1065response = flask.make_response( 1066str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 1067response.content_type = "text/plain" 1068 1069return response 1070 1071 1072@repositories.route("/<username>/<repository>/favourite") 1073def repository_favourite(username, repository): 1074server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1075if not os.path.exists(server_repo_location): 1076app.logger.error(f"Cannot load {server_repo_location}") 1077flask.abort(404) 1078if not (get_visibility(username, repository) or get_permission_level( 1079flask.session.get("username"), username, 1080repository) is not None): 1081flask.abort(403) 1082 1083app.logger.info(f"Loading {server_repo_location}") 1084 1085if not os.path.exists(server_repo_location): 1086app.logger.error(f"Cannot load {server_repo_location}") 1087return flask.render_template("not-found.html"), 404 1088 1089repo = git.Repo(server_repo_location) 1090repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1091user = User.query.filter_by(username=flask.session.get("username")).first() 1092relationships = RepoAccess.query.filter_by(repo=repo_data) 1093user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1094if not user: 1095flask.abort(401) 1096 1097old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 1098repo_route=repo_data.route).first() 1099if old_relationship: 1100db.session.delete(old_relationship) 1101else: 1102relationship = RepoFavourite(user, repo_data) 1103db.session.add(relationship) 1104 1105db.session.commit() 1106 1107return flask.redirect(flask.url_for("favourites"), code=303) 1108 1109 1110@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 1111def repository_users(username, repository): 1112server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1113if not os.path.exists(server_repo_location): 1114app.logger.error(f"Cannot load {server_repo_location}") 1115flask.abort(404) 1116if not (get_visibility(username, repository) or get_permission_level( 1117flask.session.get("username"), username, 1118repository) is not None): 1119flask.abort(403) 1120 1121app.logger.info(f"Loading {server_repo_location}") 1122 1123if not os.path.exists(server_repo_location): 1124app.logger.error(f"Cannot load {server_repo_location}") 1125return flask.render_template("not-found.html"), 404 1126 1127repo = git.Repo(server_repo_location) 1128repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1129user = User.query.filter_by(username=flask.session.get("username")).first() 1130relationships = RepoAccess.query.filter_by(repo=repo_data) 1131user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1132 1133if flask.request.method == "GET": 1134return flask.render_template( 1135"repo-users.html", 1136username=username, 1137repository=repository, 1138repo_data=repo_data, 1139relationships=relationships, 1140repo=repo, 1141user_relationship=user_relationship, 1142remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1143is_favourite=get_favourite(flask.session.get("username"), username, repository) 1144) 1145else: 1146if get_permission_level(flask.session.get("username"), username, repository) != 2: 1147flask.abort(401) 1148 1149if flask.request.form.get("new-username"): 1150# Create new relationship 1151new_user = User.query.filter_by( 1152username=flask.request.form.get("new-username")).first() 1153relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1154db.session.add(relationship) 1155db.session.commit() 1156if flask.request.form.get("update-username"): 1157# Create new relationship 1158updated_user = User.query.filter_by( 1159username=flask.request.form.get("update-username")).first() 1160relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1161if flask.request.form.get("update-level") == -1: 1162relationship.delete() 1163else: 1164relationship.access_level = flask.request.form.get("update-level") 1165db.session.commit() 1166 1167return flask.redirect( 1168app.url_for(".repository_users", username=username, repository=repository)) 1169 1170 1171@repositories.route("/<username>/<repository>/branches/") 1172def repository_branches(username, repository): 1173server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1174if not os.path.exists(server_repo_location): 1175app.logger.error(f"Cannot load {server_repo_location}") 1176flask.abort(404) 1177if not (get_visibility(username, repository) or get_permission_level( 1178flask.session.get("username"), username, 1179repository) is not None): 1180flask.abort(403) 1181 1182app.logger.info(f"Loading {server_repo_location}") 1183 1184if not os.path.exists(server_repo_location): 1185app.logger.error(f"Cannot load {server_repo_location}") 1186return flask.render_template("not-found.html"), 404 1187 1188repo = git.Repo(server_repo_location) 1189repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1190 1191return flask.render_template( 1192"repo-branches.html", 1193username=username, 1194repository=repository, 1195repo_data=repo_data, 1196repo=repo, 1197remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1198is_favourite=get_favourite(flask.session.get("username"), username, repository) 1199) 1200 1201 1202@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1203@repositories.route("/<username>/<repository>/log/<branch>/") 1204def repository_log(username, repository, branch): 1205server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1206if not os.path.exists(server_repo_location): 1207app.logger.error(f"Cannot load {server_repo_location}") 1208flask.abort(404) 1209if not (get_visibility(username, repository) or get_permission_level( 1210flask.session.get("username"), username, 1211repository) is not None): 1212flask.abort(403) 1213 1214app.logger.info(f"Loading {server_repo_location}") 1215 1216if not os.path.exists(server_repo_location): 1217app.logger.error(f"Cannot load {server_repo_location}") 1218return flask.render_template("not-found.html"), 404 1219 1220repo = git.Repo(server_repo_location) 1221repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1222if not repo_data.default_branch: 1223if repo.heads: 1224repo_data.default_branch = repo.heads[0].name 1225else: 1226return flask.render_template("empty.html", 1227remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1228if not branch: 1229branch = repo_data.default_branch 1230return flask.redirect(f"./{branch}", code=302) 1231 1232if branch.startswith("tag:"): 1233ref = f"tags/{branch[4:]}" 1234elif branch.startswith("~"): 1235ref = branch[1:] 1236else: 1237ref = f"heads/{branch}" 1238 1239ref = ref.replace("~", "/") # encode slashes for URL support 1240 1241try: 1242repo.git.checkout("-f", ref) 1243except git.exc.GitCommandError: 1244return flask.render_template("not-found.html"), 404 1245 1246branches = repo.heads 1247 1248all_refs = [] 1249for ref in repo.heads: 1250all_refs.append((ref, "head")) 1251for ref in repo.tags: 1252all_refs.append((ref, "tag")) 1253 1254commit_list = [f"/{username}/{repository}/{sha}" for sha in 1255git_command(server_repo_location, None, "log", 1256"--format='%H'").decode().split("\n")] 1257 1258commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc()) 1259page_number = flask.request.args.get("page", 1, type=int) 1260if flask.session.get("username"): 1261default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1262else: 1263default_page_length = 16 1264page_length = flask.request.args.get("per_page", default_page_length, type=int) 1265page_listing = db.paginate(commits, page=page_number, per_page=page_length) 1266 1267if page_listing.has_next: 1268next_page = page_listing.next_num 1269else: 1270next_page = None 1271 1272if page_listing.has_prev: 1273prev_page = page_listing.prev_num 1274else: 1275prev_page = None 1276 1277return flask.render_template( 1278"repo-log.html", 1279username=username, 1280repository=repository, 1281branches=all_refs, 1282current=branch, 1283repo_data=repo_data, 1284repo=repo, 1285commits=page_listing, 1286remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1287is_favourite=get_favourite(flask.session.get("username"), username, repository), 1288page_number=page_number, 1289page_length=page_length, 1290next_page=next_page, 1291prev_page=prev_page, 1292num_pages=page_listing.pages 1293) 1294 1295 1296@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1297def repository_prs(username, repository): 1298server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1299if not os.path.exists(server_repo_location): 1300app.logger.error(f"Cannot load {server_repo_location}") 1301flask.abort(404) 1302if not (get_visibility(username, repository) or get_permission_level( 1303flask.session.get("username"), username, 1304repository) is not None): 1305flask.abort(403) 1306 1307app.logger.info(f"Loading {server_repo_location}") 1308 1309if not os.path.exists(server_repo_location): 1310app.logger.error(f"Cannot load {server_repo_location}") 1311return flask.render_template("not-found.html"), 404 1312 1313if flask.request.method == "GET": 1314repo = git.Repo(server_repo_location) 1315repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1316user = User.query.filter_by(username=flask.session.get("username")).first() 1317 1318return flask.render_template( 1319"repo-prs.html", 1320username=username, 1321repository=repository, 1322repo_data=repo_data, 1323repo=repo, 1324PullRequest=PullRequest, 1325remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1326is_favourite=get_favourite(flask.session.get("username"), username, repository), 1327default_branch=repo_data.default_branch, 1328branches=repo.branches 1329) 1330 1331else: 1332repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1333head = flask.request.form.get("head") 1334head_route = flask.request.form.get("headroute") 1335base = flask.request.form.get("base") 1336 1337if not head and base and head_route: 1338return flask.redirect(".", 400) 1339 1340head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1341base_repo = git.Repo(server_repo_location) 1342print(head_repo) 1343 1344if head not in head_repo.branches or base not in base_repo.branches: 1345flask.flash(Markup( 1346"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1347category="error") 1348return flask.redirect(".", 303) 1349 1350head_data = db.session.get(Repo, head_route) 1351if not head_data.visibility: 1352flask.flash(Markup( 1353"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1354"Head can't be restricted")), 1355category="error") 1356return flask.redirect(".", 303) 1357 1358pull_request = PullRequest(head_data, head, repo_data, base, 1359db.session.get(User, flask.session["username"])) 1360 1361db.session.add(pull_request) 1362db.session.commit() 1363 1364return flask.redirect(".", 303) 1365 1366 1367@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1368def repository_prs_merge(username, repository): 1369server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1370if not os.path.exists(server_repo_location): 1371app.logger.error(f"Cannot load {server_repo_location}") 1372flask.abort(404) 1373if not (get_visibility(username, repository) or get_permission_level( 1374flask.session.get("username"), username, 1375repository) is not None): 1376flask.abort(403) 1377 1378if not get_permission_level(flask.session.get("username"), username, repository): 1379flask.abort(401) 1380 1381repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1382repo = git.Repo(server_repo_location) 1383id = flask.request.form.get("id") 1384 1385pull_request = db.session.get(PullRequest, id) 1386 1387if pull_request: 1388result = celery_tasks.merge_heads.delay( 1389pull_request.head_route, 1390pull_request.head_branch, 1391pull_request.base_route, 1392pull_request.base_branch, 1393simulate=True 1394) 1395task_result = worker.AsyncResult(result.id) 1396 1397return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1398# db.session.delete(pull_request) 1399# db.session.commit() 1400else: 1401flask.abort(400) 1402 1403 1404@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1405def repository_prs_merge_stage_two(username, repository, id): 1406server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1407if not os.path.exists(server_repo_location): 1408app.logger.error(f"Cannot load {server_repo_location}") 1409flask.abort(404) 1410if not (get_visibility(username, repository) or get_permission_level( 1411flask.session.get("username"), username, 1412repository) is not None): 1413flask.abort(403) 1414 1415if not get_permission_level(flask.session.get("username"), username, repository): 1416flask.abort(401) 1417 1418repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1419repo = git.Repo(server_repo_location) 1420 1421pull_request = db.session.get(PullRequest, id) 1422 1423if pull_request: 1424result = celery_tasks.merge_heads.delay( 1425pull_request.head_route, 1426pull_request.head_branch, 1427pull_request.base_route, 1428pull_request.base_branch, 1429simulate=False 1430) 1431task_result = worker.AsyncResult(result.id) 1432 1433pull_request.state = 1 1434db.session.commit() 1435 1436return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1437# db.session.delete(pull_request) 1438else: 1439flask.abort(400) 1440 1441 1442@app.route("/task/<task_id>") 1443def task_monitor(task_id): 1444task_result = worker.AsyncResult(task_id) 1445if task_result.status == "FAILURE": 1446app.logger.error(f"Task {task_id} failed") 1447return flask.render_template("task-monitor.html", result=task_result), 500 1448 1449return flask.render_template("task-monitor.html", result=task_result) 1450 1451 1452@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1453def repository_prs_delete(username, repository): 1454server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1455if not os.path.exists(server_repo_location): 1456app.logger.error(f"Cannot load {server_repo_location}") 1457flask.abort(404) 1458if not (get_visibility(username, repository) or get_permission_level( 1459flask.session.get("username"), username, 1460repository) is not None): 1461flask.abort(403) 1462 1463if not get_permission_level(flask.session.get("username"), username, repository): 1464flask.abort(401) 1465 1466repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1467repo = git.Repo(server_repo_location) 1468id = flask.request.form.get("id") 1469 1470pull_request = db.session.get(PullRequest, id) 1471 1472if pull_request: 1473pull_request.state = 2 1474db.session.commit() 1475 1476return flask.redirect(".", 303) 1477 1478 1479@repositories.route("/<username>/<repository>/settings/") 1480def repository_settings(username, repository): 1481if get_permission_level(flask.session.get("username"), username, repository) != 2: 1482flask.abort(401) 1483 1484repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository)) 1485 1486site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>") 1487primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>") 1488 1489return flask.render_template("repo-settings.html", username=username, repository=repository, 1490repo_data=db.session.get(Repo, f"/{username}/{repository}"), 1491branches=[branch.name for branch in repo.branches], 1492site_link=site_link, primary_site_link=primary_site_link, 1493remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1494is_favourite=get_favourite(flask.session.get("username"), username, repository), 1495) 1496 1497 1498@repositories.route("/<username>/<repository>/settings/", methods=["POST"]) 1499def repository_settings_post(username, repository): 1500if get_permission_level(flask.session.get("username"), username, repository) != 2: 1501flask.abort(401) 1502 1503repo = db.session.get(Repo, f"/{username}/{repository}") 1504 1505repo.visibility = flask.request.form.get("visibility", type=int) 1506repo.info = flask.request.form.get("description") 1507repo.default_branch = flask.request.form.get("default_branch") 1508 1509# Update site settings 1510had_site = repo.has_site 1511old_branch = repo.site_branch 1512if flask.request.form.get("site_branch"): 1513repo.site_branch = flask.request.form.get("site_branch") 1514if flask.request.form.get("primary_site"): 1515if had_site != 2: 1516# Remove primary site from other repos 1517for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2): 1518other_repo.has_site = 1 # switch it to a regular site 1519flask.flash(Markup( 1520_("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( 1521repository=other_repo.route 1522)), category="warning") 1523repo.has_site = 2 1524else: 1525repo.has_site = 1 1526else: 1527repo.site_branch = None 1528repo.has_site = 0 1529 1530db.session.commit() 1531 1532if not (had_site, old_branch) == (repo.has_site, repo.site_branch): 1533# Deploy the newly activated site 1534result = celery_tasks.copy_site.delay(repo.route) 1535 1536if had_site and not repo.has_site: 1537# Remove the site 1538result = celery_tasks.delete_site.delay(repo.route) 1539 1540if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site): 1541# Deploy all other sites which were destroyed by the primary site 1542for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1): 1543result = celery_tasks.copy_site.delay(other_repo.route) 1544 1545return flask.redirect(f"/{username}/{repository}/settings", 303) 1546 1547 1548@app.errorhandler(404) 1549def e404(error): 1550return flask.render_template("not-found.html"), 404 1551 1552 1553@app.errorhandler(401) 1554def e401(error): 1555return flask.render_template("unauthorised.html"), 401 1556 1557 1558@app.errorhandler(403) 1559def e403(error): 1560return flask.render_template("forbidden.html"), 403 1561 1562 1563@app.errorhandler(418) 1564def e418(error): 1565return flask.render_template("teapot.html"), 418 1566 1567 1568@app.errorhandler(405) 1569def e405(error): 1570return flask.render_template("method-not-allowed.html"), 405 1571 1572 1573if __name__ == "__main__": 1574app.run(debug=True, port=8080, host="0.0.0.0") 1575 1576app.register_blueprint(repositories) 1577