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