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