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