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