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, 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) 576db.session.add(relationship) 577db.session.commit() 578 579user = db.session.get(User, username) 580author = db.session.get(User, flask.session.get("username")) 581notification = Notification({"type": "update", "version": "0.0.0"}) 582db.session.add(notification) 583db.session.commit() 584 585db.session.commit() 586return flask.redirect("?", code=303) 587 588 589@app.route("/<username>/<repository>/") 590def repository_index(username, repository): 591return flask.redirect("./tree", code=302) 592 593 594@app.route("/info/<username>/avatar") 595def user_avatar(username): 596server_userdata_location = os.path.join(config.USERDATA_PATH, username) 597if not os.path.exists(server_userdata_location): 598return flask.render_template("errors/not-found.html"), 404 599 600return flask.send_from_directory(server_userdata_location, "avatar.png") 601 602 603@app.route("/info/<username>/avatar", methods=["POST"]) 604def user_avatar_upload(username): 605server_userdata_location = os.path.join(config.USERDATA_PATH, username) 606 607if not os.path.exists(server_userdata_location): 608flask.abort(404) 609if not flask.session.get("username") == username: 610flask.abort(403) 611 612# Convert image to PNG 613try: 614image = Image.open(flask.request.files["avatar"]) 615except PIL.UnidentifiedImageError: 616flask.abort(400) 617image.save(os.path.join(server_userdata_location, "avatar.png")) 618 619return flask.redirect(f"/{username}", code=303) 620 621 622@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 623def repository_raw(username, repository, branch, subpath): 624server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 625if not os.path.exists(server_repo_location): 626flask.abort(404) 627if not (get_visibility(username, repository) or get_permission_level( 628flask.session.get("username"), username, 629repository) is not None): 630flask.abort(403) 631 632if not os.path.exists(server_repo_location): 633return flask.render_template("errors/not-found.html"), 404 634 635repo = git.Repo(server_repo_location) 636repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 637if not repo_data.default_branch: 638if repo.heads: 639repo_data.default_branch = repo.heads[0].name 640else: 641return flask.render_template("empty.html", 642remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 643if not branch: 644branch = repo_data.default_branch 645return flask.redirect(f"./{branch}", code=302) 646 647if branch.startswith("tag:"): 648ref = f"tags/{branch[4:]}" 649elif branch.startswith("~"): 650ref = branch[1:] 651else: 652ref = f"heads/{branch}" 653 654ref = ref.replace("~", "/") # encode slashes for URL support 655 656try: 657repo.git.checkout("-f", ref) 658except git.exc.GitCommandError: 659return flask.render_template("errors/not-found.html"), 404 660 661return flask.send_from_directory(config.REPOS_PATH, 662os.path.join(username, repository, subpath)) 663 664 665@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 666@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 667@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 668def repository_tree(username, repository, branch, subpath): 669server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 670if not os.path.exists(server_repo_location): 671flask.abort(404) 672if not (get_visibility(username, repository) or get_permission_level( 673flask.session.get("username"), username, 674repository) is not None): 675flask.abort(403) 676 677repo = git.Repo(server_repo_location) 678repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 679if not repo_data.default_branch: 680if repo.heads: 681repo_data.default_branch = repo.heads[0].name 682else: 683return flask.render_template("empty.html", 684remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 685if not branch: 686branch = repo_data.default_branch 687return flask.redirect(f"./{branch}", code=302) 688 689if branch.startswith("tag:"): 690ref = f"tags/{branch[4:]}" 691elif branch.startswith("~"): 692ref = branch[1:] 693else: 694ref = f"heads/{branch}" 695 696ref = ref.replace("~", "/") # encode slashes for URL support 697 698try: 699repo.git.checkout("-f", ref) 700except git.exc.GitCommandError: 701return flask.render_template("errors/not-found.html"), 404 702 703branches = repo.heads 704 705all_refs = [] 706for ref in repo.heads: 707all_refs.append((ref, "head")) 708for ref in repo.tags: 709all_refs.append((ref, "tag")) 710 711if os.path.isdir(os.path.join(server_repo_location, subpath)): 712files = [] 713blobs = [] 714 715for entry in os.listdir(os.path.join(server_repo_location, subpath)): 716if not os.path.basename(entry) == ".git": 717files.append(os.path.join(subpath, entry)) 718 719infos = [] 720 721for file in files: 722path = os.path.join(server_repo_location, file) 723mimetype = guess_mime(path) 724 725text = git_command(server_repo_location, None, "log", "--format='%H\n'", 726shlex.quote(file)).decode() 727 728sha = text.split("\n")[0] 729identifier = f"/{username}/{repository}/{sha}" 730 731last_commit = db.session.get(Commit, identifier) 732 733info = { 734"name": os.path.basename(file), 735"serverPath": path, 736"relativePath": file, 737"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 738"size": human_size(os.path.getsize(path)), 739"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 740"commit": last_commit, 741"shaSize": 7, 742} 743 744special_icon = config.match_icon(os.path.basename(file)) 745if special_icon: 746info["icon"] = special_icon 747elif os.path.isdir(path): 748info["icon"] = config.folder_icon 749info["size"] = _("{} files").format(len(os.listdir(path))) 750elif mimetypes.guess_type(path)[0] in config.file_icons: 751info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]] 752else: 753info["icon"] = config.unknown_icon 754 755if os.path.isdir(path): 756infos.insert(0, info) 757else: 758infos.append(info) 759 760return flask.render_template( 761"repo-tree.html", 762username=username, 763repository=repository, 764files=infos, 765subpath=os.path.join("/", subpath), 766branches=all_refs, 767current=branch, 768remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 769is_favourite=get_favourite(flask.session.get("username"), username, repository), 770repo_data=repo_data, 771) 772else: 773path = os.path.join(server_repo_location, subpath) 774 775if not os.path.exists(path): 776return flask.render_template("errors/not-found.html"), 404 777 778mimetype = guess_mime(path) 779mode = mimetype.split("/", 1)[0] 780size = human_size(os.path.getsize(path)) 781 782special_icon = config.match_icon(os.path.basename(path)) 783if special_icon: 784icon = special_icon 785elif os.path.isdir(path): 786icon = config.folder_icon 787elif mimetypes.guess_type(path)[0] in config.file_icons: 788icon = config.file_icons[mimetypes.guess_type(path)[0]] 789else: 790icon = config.unknown_icon 791 792contents = None 793if mode == "text": 794contents = convert_to_html(path) 795 796return flask.render_template( 797"repo-file.html", 798username=username, 799repository=repository, 800file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 801branches=all_refs, 802current=branch, 803mode=mode, 804mimetype=mimetype, 805detailedtype=magic.from_file(path), 806size=size, 807icon=icon, 808subpath=os.path.join("/", subpath), 809extension=pathlib.Path(path).suffix, 810basename=os.path.basename(path), 811contents=contents, 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), 814repo_data=repo_data, 815) 816 817 818@repositories.route("/<username>/<repository>/commit/<sha>") 819def repository_commit(username, repository, sha): 820server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 821if not os.path.exists(server_repo_location): 822flask.abort(404) 823if not (get_visibility(username, repository) or get_permission_level( 824flask.session.get("username"), username, 825repository) is not None): 826flask.abort(403) 827 828if not os.path.exists(server_repo_location): 829return flask.render_template("errors/not-found.html"), 404 830 831repo = git.Repo(server_repo_location) 832repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 833 834files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r", 835"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1] 836 837return flask.render_template( 838"repo-commit.html", 839username=username, 840repository=repository, 841remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 842is_favourite=get_favourite(flask.session.get("username"), username, repository), 843diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff", 844str(sha) + "^!", "--", file).decode().split("\n") for 845file in files}, 846data=db.session.get(Commit, f"/{username}/{repository}/{sha}"), 847repo_data=repo_data, 848comment_query=Comment.query, 849permission_level=get_permission_level(flask.session.get("username"), username, repository), 850) 851 852 853@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"]) 854def repository_commit_add_comment(username, repository, sha): 855server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 856if not os.path.exists(server_repo_location): 857flask.abort(404) 858if not (get_visibility(username, repository) or get_permission_level( 859flask.session.get("username"), username, 860repository) is not None): 861flask.abort(403) 862 863comment = Comment( 864db.session.get(User, flask.session.get("username")), 865db.session.get(Repo, f"/{username}/{repository}"), 866db.session.get(Commit, f"/{username}/{repository}/{sha}"), 867flask.request.form["comment"], 868flask.request.form["file"], 869flask.request.form["line"], 870) 871 872db.session.add(comment) 873db.session.commit() 874 875return flask.redirect( 876flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 877code=303 878) 879 880 881@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"]) 882def repository_commit_delete_comment(username, repository, sha, id): 883repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 884comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 885commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first() 886if ( 887comment.owner.username == flask.session.get("username") 888or get_permission_level(flask.session.get("username"), username, repository) >= 2 889or comment.commit.owner.username == flask.session.get("username") 890): 891db.session.delete(comment) 892db.session.commit() 893 894return flask.redirect( 895flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 896code=303 897) 898 899 900@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"]) 901def repository_commit_resolve_comment(username, repository, sha, id): 902comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 903if ( 904comment.commit.owner.username == flask.session.get("username") 905or get_permission_level(flask.session.get("username"), username, repository) >= 2 906or comment.owner.username == flask.session.get("username") 907): 908comment.state = int(not comment.state) 909db.session.commit() 910 911return flask.redirect( 912flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 913code=303 914) 915 916 917@repositories.route("/<username>/<repository>/forum/") 918def repository_forum(username, repository): 919server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 920if not os.path.exists(server_repo_location): 921flask.abort(404) 922if not (get_visibility(username, repository) or get_permission_level( 923flask.session.get("username"), username, 924repository) is not None): 925flask.abort(403) 926 927if not os.path.exists(server_repo_location): 928return flask.render_template("errors/not-found.html"), 404 929 930repo = git.Repo(server_repo_location) 931repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 932user = User.query.filter_by(username=flask.session.get("username")).first() 933relationships = RepoAccess.query.filter_by(repo=repo_data) 934user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 935 936return flask.render_template( 937"repo-forum.html", 938username=username, 939repository=repository, 940repo_data=repo_data, 941relationships=relationships, 942repo=repo, 943user_relationship=user_relationship, 944Post=Post, 945remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 946is_favourite=get_favourite(flask.session.get("username"), username, repository), 947default_branch=repo_data.default_branch 948) 949 950 951@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 952def repository_forum_topic(username, repository, id): 953server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 954if not os.path.exists(server_repo_location): 955flask.abort(404) 956if not (get_visibility(username, repository) or get_permission_level( 957flask.session.get("username"), username, 958repository) is not None): 959flask.abort(403) 960 961if not os.path.exists(server_repo_location): 962return flask.render_template("errors/not-found.html"), 404 963 964repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 965user = User.query.filter_by(username=flask.session.get("username")).first() 966relationships = RepoAccess.query.filter_by(repo=repo_data) 967user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 968 969post = Post.query.filter_by(id=id).first() 970 971return flask.render_template( 972"repo-topic.html", 973username=username, 974repository=repository, 975repo_data=repo_data, 976relationships=relationships, 977user_relationship=user_relationship, 978post=post, 979remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 980is_favourite=get_favourite(flask.session.get("username"), username, repository), 981default_branch=repo_data.default_branch 982) 983 984 985@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 986def repository_forum_new(username, repository): 987server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 988if not os.path.exists(server_repo_location): 989flask.abort(404) 990if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level( 991flask.session.get("username"), username, 992repository) is not None): 993flask.abort(403) 994 995if not os.path.exists(server_repo_location): 996return flask.render_template("errors/not-found.html"), 404 997 998repo = git.Repo(server_repo_location) 999repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1000user = User.query.filter_by(username=flask.session.get("username")).first() 1001relationships = RepoAccess.query.filter_by(repo=repo_data) 1002user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1003 1004post = Post(user, repo_data, None, flask.request.form["subject"], 1005flask.request.form["message"]) 1006 1007db.session.add(post) 1008db.session.commit() 1009 1010return flask.redirect( 1011flask.url_for(".repository_forum_thread", username=username, repository=repository, 1012post_id=post.number), 1013code=303) 1014 1015 1016@repositories.route("/<username>/<repository>/forum/<int:post_id>") 1017def repository_forum_thread(username, repository, post_id): 1018server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1019if not os.path.exists(server_repo_location): 1020flask.abort(404) 1021if not (get_visibility(username, repository) or get_permission_level( 1022flask.session.get("username"), username, 1023repository) is not None): 1024flask.abort(403) 1025 1026if not os.path.exists(server_repo_location): 1027return flask.render_template("errors/not-found.html"), 404 1028 1029repo = git.Repo(server_repo_location) 1030repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1031user = User.query.filter_by(username=flask.session.get("username")).first() 1032relationships = RepoAccess.query.filter_by(repo=repo_data) 1033user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1034 1035if user: 1036max_post_nesting = user.max_post_nesting 1037else: 1038max_post_nesting = 2 1039 1040return flask.render_template( 1041"repo-forum-thread.html", 1042username=username, 1043repository=repository, 1044repo_data=repo_data, 1045relationships=relationships, 1046repo=repo, 1047Post=Post, 1048user_relationship=user_relationship, 1049post_id=post_id, 1050max_post_nesting=max_post_nesting, 1051remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1052is_favourite=get_favourite(flask.session.get("username"), username, repository), 1053parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 1054has_permission=not ((not get_permission_level(flask.session.get("username"), username, 1055repository)) and db.session.get(Post, 1056f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")), 1057) 1058 1059 1060@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state", 1061methods=["POST"]) 1062def repository_forum_change_state(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_permission_level(flask.session.get("username"), username, repository)) and db.session.get(Post, f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username"): 1067flask.abort(403) 1068 1069repo = git.Repo(server_repo_location) 1070repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1071user = User.query.filter_by(username=flask.session.get("username")).first() 1072relationships = RepoAccess.query.filter_by(repo=repo_data) 1073user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1074 1075post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1076 1077if not post: 1078flask.abort(404) 1079 1080post.state = int(flask.request.form["new-state"]) 1081 1082db.session.commit() 1083 1084return flask.redirect( 1085flask.url_for(".repository_forum_thread", username=username, repository=repository, 1086post_id=post_id), 1087code=303) 1088 1089 1090@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 1091def repository_forum_reply(username, repository, post_id): 1092server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1093if not os.path.exists(server_repo_location): 1094flask.abort(404) 1095if not ((flask.session.get("username") and get_visibility(username, repository)) or get_permission_level( 1096flask.session.get("username"), username, 1097repository) is not None): 1098flask.abort(403) 1099 1100if not os.path.exists(server_repo_location): 1101return flask.render_template("errors/not-found.html"), 404 1102 1103repo = git.Repo(server_repo_location) 1104repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1105user = User.query.filter_by(username=flask.session.get("username")).first() 1106relationships = RepoAccess.query.filter_by(repo=repo_data) 1107user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1108if not user: 1109flask.abort(401) 1110 1111parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1112post = Post(user, repo_data, parent, flask.request.form["subject"], 1113flask.request.form["message"]) 1114 1115db.session.add(post) 1116post.update_date() 1117db.session.commit() 1118 1119return flask.redirect( 1120flask.url_for(".repository_forum_thread", username=username, repository=repository, 1121post_id=post_id), 1122code=303) 1123 1124 1125@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["POST"]) 1126def repository_forum_edit(username, repository, post_id): 1127server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1128if not os.path.exists(server_repo_location): 1129flask.abort(404) 1130if not (get_visibility(username, repository) or get_permission_level( 1131flask.session.get("username"), username, 1132repository) is not None): 1133flask.abort(403) 1134 1135if not os.path.exists(server_repo_location): 1136return flask.render_template("errors/not-found.html"), 404 1137 1138repo = git.Repo(server_repo_location) 1139repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1140user = User.query.filter_by(username=flask.session.get("username")).first() 1141relationships = RepoAccess.query.filter_by(repo=repo_data) 1142user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1143if not user: 1144flask.abort(401) 1145post = db.session.get(Post, f"/{username}/{repository}/{post_id}") 1146if user != post.owner: 1147flask.abort(403) 1148 1149post.subject = flask.request.form["subject"] 1150post.message = flask.request.form["message"] 1151post.html = markdown.markdown2html(post.message).prettify() 1152post.update_date() 1153db.session.commit() 1154 1155return flask.redirect( 1156flask.url_for(".repository_forum_thread", username=username, repository=repository, 1157post_id=post_id), 1158code=303) 1159 1160 1161@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["GET"]) 1162def repository_forum_edit_form(username, repository, post_id): 1163server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1164if not os.path.exists(server_repo_location): 1165flask.abort(404) 1166if not (get_visibility(username, repository) or get_permission_level( 1167flask.session.get("username"), username, 1168repository) is not None): 1169flask.abort(403) 1170 1171if not os.path.exists(server_repo_location): 1172return flask.render_template("errors/not-found.html"), 404 1173 1174repo = git.Repo(server_repo_location) 1175repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1176user = User.query.filter_by(username=flask.session.get("username")).first() 1177relationships = RepoAccess.query.filter_by(repo=repo_data) 1178user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1179if not user: 1180flask.abort(401) 1181post = db.session.get(Post, f"/{username}/{repository}/{post_id}") 1182if user != post.owner: 1183flask.abort(403) 1184 1185return flask.render_template( 1186"repo-forum-edit.html", 1187username=username, 1188repository=repository, 1189repo_data=repo_data, 1190relationships=relationships, 1191repo=repo, 1192user_relationship=user_relationship, 1193post=post, 1194remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1195is_favourite=get_favourite(flask.session.get("username"), username, repository), 1196default_branch=repo_data.default_branch 1197) 1198 1199@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 1200defaults={"score": 1}) 1201@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 1202defaults={"score": -1}) 1203@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 1204def repository_forum_vote(username, repository, post_id, score): 1205server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1206if not os.path.exists(server_repo_location): 1207flask.abort(404) 1208if not (get_visibility(username, repository) or get_permission_level( 1209flask.session.get("username"), username, 1210repository) is not None): 1211flask.abort(403) 1212 1213if not os.path.exists(server_repo_location): 1214return flask.render_template("errors/not-found.html"), 404 1215 1216repo = git.Repo(server_repo_location) 1217repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1218user = User.query.filter_by(username=flask.session.get("username")).first() 1219relationships = RepoAccess.query.filter_by(repo=repo_data) 1220user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1221if not user: 1222flask.abort(401) 1223 1224post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1225 1226if score: 1227old_relationship = PostVote.query.filter_by(user_username=user.username, 1228post_identifier=post.identifier).first() 1229if old_relationship: 1230if score == old_relationship.vote_score: 1231db.session.delete(old_relationship) 1232post.vote_sum -= old_relationship.vote_score 1233else: 1234post.vote_sum -= old_relationship.vote_score 1235post.vote_sum += score 1236old_relationship.vote_score = score 1237else: 1238relationship = PostVote(user, post, score) 1239post.vote_sum += score 1240db.session.add(relationship) 1241 1242db.session.commit() 1243 1244user_vote = PostVote.query.filter_by(user_username=user.username, 1245post_identifier=post.identifier).first() 1246response = flask.make_response( 1247str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 1248response.content_type = "text/plain" 1249 1250return response 1251 1252 1253@repositories.route("/<username>/<repository>/forum/<int:post_id>/label", methods=["POST"]) 1254def repository_forum_label(username, repository, post_id): 1255server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1256if not os.path.exists(server_repo_location): 1257flask.abort(404) 1258if not get_permission_level(flask.session.get("username"), username, repository): 1259flask.abort(403) 1260 1261repo = git.Repo(server_repo_location) 1262repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1263user = User.query.filter_by(username=flask.session.get("username")).first() 1264relationships = RepoAccess.query.filter_by(repo=repo_data) 1265user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1266 1267post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1268 1269if not post: 1270flask.abort(404) 1271if post.parent: 1272flask.abort(400) 1273 1274label = db.session.get(Label, flask.request.form["label"]) 1275 1276if PostLabel.query.filter_by(post=post, label=label).first(): 1277return flask.redirect( 1278flask.url_for(".repository_forum_thread", username=username, repository=repository, 1279post_id=post_id), 1280code=303) 1281 1282post_label = PostLabel(post, label) 1283db.session.add(post_label) 1284 1285db.session.commit() 1286 1287return flask.redirect( 1288flask.url_for(".repository_forum_thread", username=username, repository=repository, 1289post_id=post_id), 1290code=303) 1291 1292 1293@repositories.route("/<username>/<repository>/forum/<int:post_id>/remove-label") 1294def repository_forum_remove_label(username, repository, post_id): 1295server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1296if not os.path.exists(server_repo_location): 1297flask.abort(404) 1298if not get_permission_level(flask.session.get("username"), username, repository): 1299flask.abort(403) 1300 1301repo = git.Repo(server_repo_location) 1302repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1303user = User.query.filter_by(username=flask.session.get("username")).first() 1304relationships = RepoAccess.query.filter_by(repo=repo_data) 1305user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1306 1307post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1308 1309if not post: 1310flask.abort(404) 1311if post.parent: 1312flask.abort(400) 1313 1314label = db.session.get(Label, flask.request.args["label"]) 1315 1316post_label = PostLabel.query.filter_by(post=post, label=label).first() 1317db.session.delete(post_label) 1318 1319db.session.commit() 1320 1321return flask.redirect( 1322flask.url_for(".repository_forum_thread", username=username, repository=repository, 1323post_id=post_id), 1324code=303) 1325 1326 1327@repositories.route("/<username>/<repository>/favourite") 1328def repository_favourite(username, repository): 1329server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1330if not os.path.exists(server_repo_location): 1331flask.abort(404) 1332if not (get_visibility(username, repository) or get_permission_level( 1333flask.session.get("username"), username, 1334repository) is not None): 1335flask.abort(403) 1336 1337if not os.path.exists(server_repo_location): 1338return flask.render_template("errors/not-found.html"), 404 1339 1340repo = git.Repo(server_repo_location) 1341repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1342user = User.query.filter_by(username=flask.session.get("username")).first() 1343relationships = RepoAccess.query.filter_by(repo=repo_data) 1344user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1345if not user: 1346flask.abort(401) 1347 1348old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 1349repo_route=repo_data.route).first() 1350if old_relationship: 1351db.session.delete(old_relationship) 1352else: 1353relationship = RepoFavourite(user, repo_data) 1354db.session.add(relationship) 1355 1356db.session.commit() 1357 1358return flask.redirect(flask.url_for("favourites"), code=303) 1359 1360 1361@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 1362def repository_users(username, repository): 1363server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1364if not os.path.exists(server_repo_location): 1365flask.abort(404) 1366if not (get_visibility(username, repository) or get_permission_level( 1367flask.session.get("username"), username, 1368repository) is not None): 1369flask.abort(403) 1370 1371if not os.path.exists(server_repo_location): 1372return flask.render_template("errors/not-found.html"), 404 1373 1374repo = git.Repo(server_repo_location) 1375repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1376user = User.query.filter_by(username=flask.session.get("username")).first() 1377relationships = RepoAccess.query.filter_by(repo=repo_data) 1378user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1379 1380if flask.request.method == "GET": 1381return flask.render_template( 1382"repo-users.html", 1383username=username, 1384repository=repository, 1385repo_data=repo_data, 1386relationships=relationships, 1387repo=repo, 1388user_relationship=user_relationship, 1389remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1390is_favourite=get_favourite(flask.session.get("username"), username, repository) 1391) 1392else: 1393if get_permission_level(flask.session.get("username"), username, repository) != 2: 1394flask.abort(401) 1395 1396if flask.request.form.get("new-username"): 1397# Create new relationship 1398new_user = User.query.filter_by( 1399username=flask.request.form.get("new-username")).first() 1400relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1401db.session.add(relationship) 1402db.session.commit() 1403if flask.request.form.get("update-username"): 1404# Create new relationship 1405updated_user = User.query.filter_by( 1406username=flask.request.form.get("update-username")).first() 1407relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1408if flask.request.form.get("update-level") == -1: 1409relationship.delete() 1410else: 1411relationship.access_level = flask.request.form.get("update-level") 1412db.session.commit() 1413 1414return flask.redirect( 1415app.url_for(".repository_users", username=username, repository=repository)) 1416 1417 1418@repositories.route("/<username>/<repository>/branches/") 1419def repository_branches(username, repository): 1420server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1421if not os.path.exists(server_repo_location): 1422flask.abort(404) 1423if not (get_visibility(username, repository) or get_permission_level( 1424flask.session.get("username"), username, 1425repository) is not None): 1426flask.abort(403) 1427 1428if not os.path.exists(server_repo_location): 1429return flask.render_template("errors/not-found.html"), 404 1430 1431repo = git.Repo(server_repo_location) 1432repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1433 1434return flask.render_template( 1435"repo-branches.html", 1436username=username, 1437repository=repository, 1438repo_data=repo_data, 1439repo=repo, 1440remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1441is_favourite=get_favourite(flask.session.get("username"), username, repository) 1442) 1443 1444 1445@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1446@repositories.route("/<username>/<repository>/log/<branch>/") 1447def repository_log(username, repository, branch): 1448server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1449if not os.path.exists(server_repo_location): 1450flask.abort(404) 1451if not (get_visibility(username, repository) or get_permission_level( 1452flask.session.get("username"), username, 1453repository) is not None): 1454flask.abort(403) 1455 1456if not os.path.exists(server_repo_location): 1457return flask.render_template("errors/not-found.html"), 404 1458 1459repo = git.Repo(server_repo_location) 1460repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1461if not repo_data.default_branch: 1462if repo.heads: 1463repo_data.default_branch = repo.heads[0].name 1464else: 1465return flask.render_template("empty.html", 1466remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1467if not branch: 1468branch = repo_data.default_branch 1469return flask.redirect(f"./{branch}", code=302) 1470 1471if branch.startswith("tag:"): 1472ref = f"tags/{branch[4:]}" 1473elif branch.startswith("~"): 1474ref = branch[1:] 1475else: 1476ref = f"heads/{branch}" 1477 1478ref = ref.replace("~", "/") # encode slashes for URL support 1479 1480try: 1481repo.git.checkout("-f", ref) 1482except git.exc.GitCommandError: 1483return flask.render_template("errors/not-found.html"), 404 1484 1485branches = repo.heads 1486 1487all_refs = [] 1488for ref in repo.heads: 1489all_refs.append((ref, "head")) 1490for ref in repo.tags: 1491all_refs.append((ref, "tag")) 1492 1493commit_list = [f"/{username}/{repository}/{sha}" for sha in 1494git_command(server_repo_location, None, "log", 1495"--format='%H'").decode().split("\n")] 1496 1497commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc()) 1498page_number = flask.request.args.get("page", 1, type=int) 1499if flask.session.get("username"): 1500default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1501else: 1502default_page_length = 16 1503page_length = flask.request.args.get("per_page", default_page_length, type=int) 1504page_listing = db.paginate(commits, page=page_number, per_page=page_length) 1505 1506if page_listing.has_next: 1507next_page = page_listing.next_num 1508else: 1509next_page = None 1510 1511if page_listing.has_prev: 1512prev_page = page_listing.prev_num 1513else: 1514prev_page = None 1515 1516return flask.render_template( 1517"repo-log.html", 1518username=username, 1519repository=repository, 1520branches=all_refs, 1521current=branch, 1522repo_data=repo_data, 1523repo=repo, 1524commits=page_listing, 1525remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1526is_favourite=get_favourite(flask.session.get("username"), username, repository), 1527page_number=page_number, 1528page_length=page_length, 1529next_page=next_page, 1530prev_page=prev_page, 1531num_pages=page_listing.pages 1532) 1533 1534 1535@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1536def repository_prs(username, repository): 1537server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1538if not os.path.exists(server_repo_location): 1539flask.abort(404) 1540if not (get_visibility(username, repository) or get_permission_level( 1541flask.session.get("username"), username, 1542repository) is not None): 1543flask.abort(403) 1544 1545if not os.path.exists(server_repo_location): 1546return flask.render_template("errors/not-found.html"), 404 1547 1548if flask.request.method == "GET": 1549repo = git.Repo(server_repo_location) 1550repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1551user = User.query.filter_by(username=flask.session.get("username")).first() 1552 1553return flask.render_template( 1554"repo-prs.html", 1555username=username, 1556repository=repository, 1557repo_data=repo_data, 1558repo=repo, 1559PullRequest=PullRequest, 1560remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1561is_favourite=get_favourite(flask.session.get("username"), username, repository), 1562default_branch=repo_data.default_branch, 1563branches=repo.branches 1564) 1565 1566elif "id" not in flask.request.form: 1567repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1568head = flask.request.form.get("head") 1569head_route = flask.request.form.get("headroute") 1570base = flask.request.form.get("base") 1571 1572if not head and base and head_route: 1573return flask.redirect(".", 400) 1574 1575head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1576base_repo = git.Repo(server_repo_location) 1577 1578if head not in head_repo.branches or base not in base_repo.branches: 1579flask.flash(Markup( 1580"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1581category="error") 1582return flask.redirect(".", 303) 1583 1584head_data = db.session.get(Repo, head_route) 1585if not head_data.visibility: 1586flask.flash(Markup( 1587"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1588"Head can't be restricted")), 1589category="error") 1590return flask.redirect(".", 303) 1591 1592pull_request = PullRequest(head_data, head, repo_data, base, 1593db.session.get(User, flask.session["username"])) 1594 1595db.session.add(pull_request) 1596db.session.commit() 1597 1598# Create the notification 1599notification = Notification({"type": "pr", "head": pull_request.head.route, "base": pull_request.base.route, "pr": pull_request.id}) 1600db.session.add(notification) 1601db.session.commit() 1602 1603# Send a notification to all users who have enabled PR notifications for this repo 1604for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all(): 1605user = relationship.user 1606user_notification = UserNotification(user, notification, 1) 1607db.session.add(user_notification) 1608db.session.commit() 1609celery_tasks.send_notification.apply_async(args=[user_notification.id]) 1610 1611return flask.redirect(".", 303) 1612else: 1613id = flask.request.form.get("id") 1614pull_request = db.session.get(PullRequest, id) 1615 1616if not pull_request: 1617flask.abort(404) 1618 1619if not (get_visibility(username, repository) or get_permission_level( 1620flask.session.get("username"), username, 1621repository) >= 1 or pull_request.owner.username == flask.session.get("username")): 1622flask.abort(403) 1623 1624if not get_permission_level(flask.session.get("username"), username, repository): 1625flask.abort(401) 1626 1627repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1628 1629if pull_request: 1630pull_request.resolves_list = flask.request.form.get("resolves") 1631db.session.commit() 1632 1633return flask.redirect(".", 303) 1634 1635 1636@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1637def repository_prs_merge(username, repository): 1638server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1639if not os.path.exists(server_repo_location): 1640flask.abort(404) 1641if not (get_visibility(username, repository) or get_permission_level( 1642flask.session.get("username"), username, 1643repository) is not None): 1644flask.abort(403) 1645 1646if not get_permission_level(flask.session.get("username"), username, repository): 1647flask.abort(401) 1648 1649repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1650repo = git.Repo(server_repo_location) 1651id = flask.request.form.get("id") 1652 1653pull_request = db.session.get(PullRequest, id) 1654 1655if pull_request: 1656result = celery_tasks.merge_heads.delay( 1657pull_request.head_route, 1658pull_request.head_branch, 1659pull_request.base_route, 1660pull_request.base_branch, 1661pull_request.id, 1662simulate=True 1663) 1664task_result = worker.AsyncResult(result.id) 1665 1666return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) # should be 202 Accepted but we must use a redirect 1667# db.session.delete(pull_request) 1668# db.session.commit() 1669else: 1670flask.abort(400) 1671 1672 1673@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1674def repository_prs_merge_stage_two(username, repository, id): 1675server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1676if not os.path.exists(server_repo_location): 1677flask.abort(404) 1678if not (get_visibility(username, repository) or get_permission_level( 1679flask.session.get("username"), username, 1680repository) is not None): 1681flask.abort(403) 1682 1683if not get_permission_level(flask.session.get("username"), username, repository): 1684flask.abort(401) 1685 1686repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1687repo = git.Repo(server_repo_location) 1688 1689pull_request = db.session.get(PullRequest, id) 1690 1691if pull_request: 1692result = celery_tasks.merge_heads.delay( 1693pull_request.head_route, 1694pull_request.head_branch, 1695pull_request.base_route, 1696pull_request.base_branch, 1697pull_request.id, 1698simulate=False 1699) 1700task_result = worker.AsyncResult(result.id) 1701 1702db.session.commit() 1703 1704return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1705# db.session.delete(pull_request) 1706else: 1707flask.abort(400) 1708 1709 1710@app.route("/task/<task_id>") 1711def task_monitor(task_id): 1712task_result = worker.AsyncResult(task_id) 1713 1714if flask.request.args.get("partial"): 1715# htmx partial update 1716return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000) 1717 1718# Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long 1719return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125) 1720 1721 1722@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1723def repository_prs_delete(username, repository): 1724server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1725if not os.path.exists(server_repo_location): 1726flask.abort(404) 1727if not (get_visibility(username, repository) or get_permission_level( 1728flask.session.get("username"), username, 1729repository) is not None): 1730flask.abort(403) 1731 1732if not get_permission_level(flask.session.get("username"), username, repository): 1733flask.abort(401) 1734 1735repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1736repo = git.Repo(server_repo_location) 1737id = flask.request.form.get("id") 1738 1739pull_request = db.session.get(PullRequest, id) 1740 1741if pull_request: 1742pull_request.state = 2 1743db.session.commit() 1744 1745return flask.redirect(".", 303) 1746 1747 1748@repositories.route("/<username>/<repository>/settings/") 1749def repository_settings(username, repository): 1750if get_permission_level(flask.session.get("username"), username, repository) != 2: 1751flask.abort(401) 1752 1753repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository)) 1754 1755site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>") 1756primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>") 1757 1758return flask.render_template("repo-settings.html", username=username, repository=repository, 1759repo_data=db.session.get(Repo, f"/{username}/{repository}"), 1760branches=[branch.name for branch in repo.branches], 1761site_link=site_link, primary_site_link=primary_site_link, 1762remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1763is_favourite=get_favourite(flask.session.get("username"), username, repository), 1764) 1765 1766 1767@repositories.route("/<username>/<repository>/settings/", methods=["POST"]) 1768def repository_settings_post(username, repository): 1769if get_permission_level(flask.session.get("username"), username, repository) != 2: 1770flask.abort(401) 1771 1772repo = db.session.get(Repo, f"/{username}/{repository}") 1773 1774repo.visibility = flask.request.form.get("visibility", type=int) 1775repo.info = flask.request.form.get("description") 1776repo.default_branch = flask.request.form.get("default_branch") 1777repo.url = flask.request.form.get("url") 1778 1779# Update site settings 1780had_site = repo.has_site 1781old_branch = repo.site_branch 1782if flask.request.form.get("site_branch"): 1783repo.site_branch = flask.request.form.get("site_branch") 1784if flask.request.form.get("primary_site"): 1785if had_site != 2: 1786# Remove primary site from other repos 1787for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2): 1788other_repo.has_site = 1 # switch it to a regular site 1789flask.flash(Markup( 1790_("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( 1791repository=other_repo.route 1792)), category="warning") 1793repo.has_site = 2 1794else: 1795repo.has_site = 1 1796else: 1797repo.site_branch = None 1798repo.has_site = 0 1799 1800db.session.commit() 1801 1802if not (had_site, old_branch) == (repo.has_site, repo.site_branch): 1803# Deploy the newly activated site 1804result = celery_tasks.copy_site.delay(repo.route) 1805 1806if had_site and not repo.has_site: 1807# Remove the site 1808result = celery_tasks.delete_site.delay(repo.route) 1809 1810if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site): 1811# Deploy all other sites which were destroyed by the primary site 1812for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1): 1813result = celery_tasks.copy_site.delay(other_repo.route) 1814 1815return flask.redirect(f"/{username}/{repository}/settings", 303) 1816 1817 1818@repositories.route("/<username>/<repository>/settings/add-label", methods=["POST"]) 1819def repository_settings_add_label(username, repository): 1820if get_permission_level(flask.session.get("username"), username, repository) != 2: 1821flask.abort(401) 1822 1823repo_data = db.session.get(Repo, f"/{username}/{repository}") 1824 1825label = Label(repo_data, flask.request.form.get("label"), flask.request.form.get("colour")) 1826db.session.add(label) 1827db.session.commit() 1828 1829return flask.redirect(f"/{username}/{repository}/settings", 303) 1830 1831 1832@repositories.route("/<username>/<repository>/settings/delete-label", methods=["POST"]) 1833def repository_settings_delete_label(username, repository): 1834if get_permission_level(flask.session.get("username"), username, repository) != 2: 1835flask.abort(401) 1836 1837repo_data = db.session.get(Repo, f"/{username}/{repository}") 1838 1839label = db.session.get(Label, flask.request.form.get("id")) 1840 1841db.session.delete(label) 1842db.session.commit() 1843 1844return flask.redirect(f"/{username}/{repository}/settings", 303) 1845 1846 1847@repositories.route("/<username>/<repository>/settings/edit-label", methods=["POST"]) 1848def repository_settings_edit_label(username, repository): 1849if get_permission_level(flask.session.get("username"), username, repository) != 2: 1850flask.abort(401) 1851 1852repo_data = db.session.get(Repo, f"/{username}/{repository}") 1853 1854label = db.session.get(Label, flask.request.form.get("id")) 1855 1856label.name = flask.request.form.get("label") 1857label.colour_hex = flask.request.form.get("colour") 1858 1859db.session.commit() 1860 1861return flask.redirect(f"/{username}/{repository}/settings", 303) 1862 1863 1864@repositories.route("/<username>/<repository>/settings/delete", methods=["POST"]) 1865def repository_settings_delete(username, repository): 1866if username != flask.session.get("username"): 1867flask.abort(401) 1868 1869repo = db.session.get(Repo, f"/{username}/{repository}") 1870 1871if not repo: 1872flask.abort(404) 1873 1874user = db.session.get(User, flask.session.get("username")) 1875 1876if not bcrypt.check_password_hash(user.password_hashed, flask.request.form.get("password")): 1877flask.flash(_("Incorrect password"), category="error") 1878flask.abort(401) 1879 1880if repo.has_site: 1881celery_tasks.delete_site.delay(repo.route) 1882 1883db.session.delete(repo) 1884db.session.commit() 1885 1886shutil.rmtree(os.path.join(config.REPOS_PATH, username, repository)) 1887 1888return flask.redirect(f"/{username}", 303) 1889 1890 1891@app.errorhandler(404) 1892def e404(error): 1893return flask.render_template("errors/not-found.html"), 404 1894 1895 1896@app.errorhandler(401) 1897def e401(error): 1898return flask.render_template("errors/unauthorised.html"), 401 1899 1900 1901@app.errorhandler(403) 1902def e403(error): 1903return flask.render_template("errors/forbidden.html"), 403 1904 1905 1906@app.errorhandler(418) 1907def e418(error): 1908return flask.render_template("errors/teapot.html"), 418 1909 1910 1911@app.errorhandler(405) 1912def e405(error): 1913return flask.render_template("errors/method-not-allowed.html"), 405 1914 1915 1916@app.errorhandler(500) 1917def e500(error): 1918return flask.render_template("errors/server-error.html"), 500 1919 1920 1921@app.errorhandler(400) 1922def e400(error): 1923return flask.render_template("errors/bad-request.html"), 400 1924 1925 1926@app.errorhandler(410) 1927def e410(error): 1928return flask.render_template("errors/gone.html"), 410 1929 1930 1931@app.errorhandler(415) 1932def e415(error): 1933return flask.render_template("errors/media-type.html"), 415 1934 1935 1936if __name__ == "__main__": 1937app.run(debug=True, port=8080, host="0.0.0.0") 1938 1939app.register_blueprint(repositories) 1940