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