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 270# print(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() 482# print("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 797# print(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, 811permission_level=get_permission_level(flask.session.get("username"), username, repository), 812) 813 814 815@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"]) 816def repository_commit_add_comment(username, repository, sha): 817server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 818if not os.path.exists(server_repo_location): 819app.logger.error(f"Cannot load {server_repo_location}") 820flask.abort(404) 821if not (get_visibility(username, repository) or get_permission_level( 822flask.session.get("username"), username, 823repository) is not None): 824flask.abort(403) 825 826comment = Comment( 827db.session.get(User, flask.session.get("username")), 828db.session.get(Repo, f"/{username}/{repository}"), 829db.session.get(Commit, f"/{username}/{repository}/{sha}"), 830flask.request.form["comment"], 831flask.request.form["file"], 832flask.request.form["line"], 833) 834 835db.session.add(comment) 836db.session.commit() 837 838return flask.redirect( 839flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 840code=303 841) 842 843 844@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"]) 845def repository_commit_delete_comment(username, repository, sha, id): 846repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 847# print(f"/{username}/{repository}/{flask.request.form.get('id')}") 848comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 849commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first() 850if ( 851comment.owner.username == flask.session.get("username") 852or get_permission_level(flask.session.get("username"), username, repository) >= 2 853or comment.commit.owner.username == flask.session.get("username") 854): 855db.session.delete(comment) 856db.session.commit() 857 858return flask.redirect( 859flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 860code=303 861) 862 863 864@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"]) 865def repository_commit_resolve_comment(username, repository, sha, id): 866comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 867if ( 868comment.commit.owner.username == flask.session.get("username") 869or get_permission_level(flask.session.get("username"), username, repository) >= 2 870or comment.owner.username == flask.session.get("username") 871): 872comment.state = int(not comment.state) 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>/forum/") 882def repository_forum(username, repository): 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 = git.Repo(server_repo_location) 899repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 900user = User.query.filter_by(username=flask.session.get("username")).first() 901relationships = RepoAccess.query.filter_by(repo=repo_data) 902user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 903 904return flask.render_template( 905"repo-forum.html", 906username=username, 907repository=repository, 908repo_data=repo_data, 909relationships=relationships, 910repo=repo, 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/topic/<int:id>") 920def repository_forum_topic(username, repository, id): 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_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 937user = User.query.filter_by(username=flask.session.get("username")).first() 938relationships = RepoAccess.query.filter_by(repo=repo_data) 939user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 940 941post = Post.query.filter_by(id=id).first() 942 943return flask.render_template( 944"repo-topic.html", 945username=username, 946repository=repository, 947repo_data=repo_data, 948relationships=relationships, 949user_relationship=user_relationship, 950post=post, 951remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 952is_favourite=get_favourite(flask.session.get("username"), username, repository), 953default_branch=repo_data.default_branch 954) 955 956 957@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 958def repository_forum_new(username, repository): 959server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 960if not os.path.exists(server_repo_location): 961app.logger.error(f"Cannot load {server_repo_location}") 962flask.abort(404) 963if not (get_visibility(username, repository) or get_permission_level( 964flask.session.get("username"), username, 965repository) is not None): 966flask.abort(403) 967 968app.logger.info(f"Loading {server_repo_location}") 969 970if not os.path.exists(server_repo_location): 971app.logger.error(f"Cannot load {server_repo_location}") 972return flask.render_template("not-found.html"), 404 973 974repo = git.Repo(server_repo_location) 975repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 976user = User.query.filter_by(username=flask.session.get("username")).first() 977relationships = RepoAccess.query.filter_by(repo=repo_data) 978user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 979 980post = Post(user, repo_data, None, flask.request.form["subject"], 981flask.request.form["message"]) 982 983db.session.add(post) 984db.session.commit() 985 986return flask.redirect( 987flask.url_for(".repository_forum_thread", username=username, repository=repository, 988post_id=post.number), 989code=303) 990 991 992@repositories.route("/<username>/<repository>/forum/<int:post_id>") 993def repository_forum_thread(username, repository, post_id): 994server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 995if not os.path.exists(server_repo_location): 996app.logger.error(f"Cannot load {server_repo_location}") 997flask.abort(404) 998if not (get_visibility(username, repository) or get_permission_level( 999flask.session.get("username"), username, 1000repository) is not None): 1001flask.abort(403) 1002 1003app.logger.info(f"Loading {server_repo_location}") 1004 1005if not os.path.exists(server_repo_location): 1006app.logger.error(f"Cannot load {server_repo_location}") 1007return flask.render_template("not-found.html"), 404 1008 1009repo = git.Repo(server_repo_location) 1010repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1011user = User.query.filter_by(username=flask.session.get("username")).first() 1012relationships = RepoAccess.query.filter_by(repo=repo_data) 1013user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1014 1015if user: 1016max_post_nesting = user.max_post_nesting 1017else: 1018max_post_nesting = 2 1019 1020return flask.render_template( 1021"repo-forum-thread.html", 1022username=username, 1023repository=repository, 1024repo_data=repo_data, 1025relationships=relationships, 1026repo=repo, 1027Post=Post, 1028user_relationship=user_relationship, 1029post_id=post_id, 1030max_post_nesting=max_post_nesting, 1031remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1032is_favourite=get_favourite(flask.session.get("username"), username, repository), 1033parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 1034has_permission=not ((not get_permission_level(flask.session.get("username"), username, 1035repository)) and db.session.get(Post, 1036f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")), 1037) 1038 1039 1040@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state", 1041methods=["POST"]) 1042def repository_forum_change_state(username, repository, post_id): 1043server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1044if not os.path.exists(server_repo_location): 1045app.logger.error(f"Cannot load {server_repo_location}") 1046flask.abort(404) 1047if (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"): 1048flask.abort(403) 1049 1050app.logger.info(f"Loading {server_repo_location}") 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() 1057 1058post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1059 1060if not post: 1061flask.abort(404) 1062 1063post.state = int(flask.request.form["new-state"]) 1064 1065db.session.commit() 1066 1067return flask.redirect( 1068flask.url_for(".repository_forum_thread", username=username, repository=repository, 1069post_id=post_id), 1070code=303) 1071 1072 1073@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 1074def repository_forum_reply(username, repository, post_id): 1075server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1076if not os.path.exists(server_repo_location): 1077app.logger.error(f"Cannot load {server_repo_location}") 1078flask.abort(404) 1079if not (get_visibility(username, repository) or get_permission_level( 1080flask.session.get("username"), username, 1081repository) is not None): 1082flask.abort(403) 1083 1084app.logger.info(f"Loading {server_repo_location}") 1085 1086if not os.path.exists(server_repo_location): 1087app.logger.error(f"Cannot load {server_repo_location}") 1088return flask.render_template("not-found.html"), 404 1089 1090repo = git.Repo(server_repo_location) 1091repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1092user = User.query.filter_by(username=flask.session.get("username")).first() 1093relationships = RepoAccess.query.filter_by(repo=repo_data) 1094user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1095if not user: 1096flask.abort(401) 1097 1098parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1099post = Post(user, repo_data, parent, flask.request.form["subject"], 1100flask.request.form["message"]) 1101 1102db.session.add(post) 1103post.update_date() 1104db.session.commit() 1105 1106return flask.redirect( 1107flask.url_for(".repository_forum_thread", username=username, repository=repository, 1108post_id=post_id), 1109code=303) 1110 1111 1112@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 1113defaults={"score": 1}) 1114@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 1115defaults={"score": -1}) 1116@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 1117def repository_forum_vote(username, repository, post_id, score): 1118server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1119if not os.path.exists(server_repo_location): 1120app.logger.error(f"Cannot load {server_repo_location}") 1121flask.abort(404) 1122if not (get_visibility(username, repository) or get_permission_level( 1123flask.session.get("username"), username, 1124repository) is not None): 1125flask.abort(403) 1126 1127app.logger.info(f"Loading {server_repo_location}") 1128 1129if not os.path.exists(server_repo_location): 1130app.logger.error(f"Cannot load {server_repo_location}") 1131return flask.render_template("not-found.html"), 404 1132 1133repo = git.Repo(server_repo_location) 1134repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1135user = User.query.filter_by(username=flask.session.get("username")).first() 1136relationships = RepoAccess.query.filter_by(repo=repo_data) 1137user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1138if not user: 1139flask.abort(401) 1140 1141post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1142 1143if score: 1144old_relationship = PostVote.query.filter_by(user_username=user.username, 1145post_identifier=post.identifier).first() 1146if old_relationship: 1147if score == old_relationship.vote_score: 1148db.session.delete(old_relationship) 1149post.vote_sum -= old_relationship.vote_score 1150else: 1151post.vote_sum -= old_relationship.vote_score 1152post.vote_sum += score 1153old_relationship.vote_score = score 1154else: 1155relationship = PostVote(user, post, score) 1156post.vote_sum += score 1157db.session.add(relationship) 1158 1159db.session.commit() 1160 1161user_vote = PostVote.query.filter_by(user_username=user.username, 1162post_identifier=post.identifier).first() 1163response = flask.make_response( 1164str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 1165response.content_type = "text/plain" 1166 1167return response 1168 1169 1170@repositories.route("/<username>/<repository>/favourite") 1171def repository_favourite(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() 1192if not user: 1193flask.abort(401) 1194 1195old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 1196repo_route=repo_data.route).first() 1197if old_relationship: 1198db.session.delete(old_relationship) 1199else: 1200relationship = RepoFavourite(user, repo_data) 1201db.session.add(relationship) 1202 1203db.session.commit() 1204 1205return flask.redirect(flask.url_for("favourites"), code=303) 1206 1207 1208@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 1209def repository_users(username, repository): 1210server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1211if not os.path.exists(server_repo_location): 1212app.logger.error(f"Cannot load {server_repo_location}") 1213flask.abort(404) 1214if not (get_visibility(username, repository) or get_permission_level( 1215flask.session.get("username"), username, 1216repository) is not None): 1217flask.abort(403) 1218 1219app.logger.info(f"Loading {server_repo_location}") 1220 1221if not os.path.exists(server_repo_location): 1222app.logger.error(f"Cannot load {server_repo_location}") 1223return flask.render_template("not-found.html"), 404 1224 1225repo = git.Repo(server_repo_location) 1226repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1227user = User.query.filter_by(username=flask.session.get("username")).first() 1228relationships = RepoAccess.query.filter_by(repo=repo_data) 1229user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1230 1231if flask.request.method == "GET": 1232return flask.render_template( 1233"repo-users.html", 1234username=username, 1235repository=repository, 1236repo_data=repo_data, 1237relationships=relationships, 1238repo=repo, 1239user_relationship=user_relationship, 1240remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1241is_favourite=get_favourite(flask.session.get("username"), username, repository) 1242) 1243else: 1244if get_permission_level(flask.session.get("username"), username, repository) != 2: 1245flask.abort(401) 1246 1247if flask.request.form.get("new-username"): 1248# Create new relationship 1249new_user = User.query.filter_by( 1250username=flask.request.form.get("new-username")).first() 1251relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1252db.session.add(relationship) 1253db.session.commit() 1254if flask.request.form.get("update-username"): 1255# Create new relationship 1256updated_user = User.query.filter_by( 1257username=flask.request.form.get("update-username")).first() 1258relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1259if flask.request.form.get("update-level") == -1: 1260relationship.delete() 1261else: 1262relationship.access_level = flask.request.form.get("update-level") 1263db.session.commit() 1264 1265return flask.redirect( 1266app.url_for(".repository_users", username=username, repository=repository)) 1267 1268 1269@repositories.route("/<username>/<repository>/branches/") 1270def repository_branches(username, repository): 1271server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1272if not os.path.exists(server_repo_location): 1273app.logger.error(f"Cannot load {server_repo_location}") 1274flask.abort(404) 1275if not (get_visibility(username, repository) or get_permission_level( 1276flask.session.get("username"), username, 1277repository) is not None): 1278flask.abort(403) 1279 1280app.logger.info(f"Loading {server_repo_location}") 1281 1282if not os.path.exists(server_repo_location): 1283app.logger.error(f"Cannot load {server_repo_location}") 1284return flask.render_template("not-found.html"), 404 1285 1286repo = git.Repo(server_repo_location) 1287repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1288 1289return flask.render_template( 1290"repo-branches.html", 1291username=username, 1292repository=repository, 1293repo_data=repo_data, 1294repo=repo, 1295remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1296is_favourite=get_favourite(flask.session.get("username"), username, repository) 1297) 1298 1299 1300@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1301@repositories.route("/<username>/<repository>/log/<branch>/") 1302def repository_log(username, repository, branch): 1303server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1304if not os.path.exists(server_repo_location): 1305app.logger.error(f"Cannot load {server_repo_location}") 1306flask.abort(404) 1307if not (get_visibility(username, repository) or get_permission_level( 1308flask.session.get("username"), username, 1309repository) is not None): 1310flask.abort(403) 1311 1312app.logger.info(f"Loading {server_repo_location}") 1313 1314if not os.path.exists(server_repo_location): 1315app.logger.error(f"Cannot load {server_repo_location}") 1316return flask.render_template("not-found.html"), 404 1317 1318repo = git.Repo(server_repo_location) 1319repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1320if not repo_data.default_branch: 1321if repo.heads: 1322repo_data.default_branch = repo.heads[0].name 1323else: 1324return flask.render_template("empty.html", 1325remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1326if not branch: 1327branch = repo_data.default_branch 1328return flask.redirect(f"./{branch}", code=302) 1329 1330if branch.startswith("tag:"): 1331ref = f"tags/{branch[4:]}" 1332elif branch.startswith("~"): 1333ref = branch[1:] 1334else: 1335ref = f"heads/{branch}" 1336 1337ref = ref.replace("~", "/") # encode slashes for URL support 1338 1339try: 1340repo.git.checkout("-f", ref) 1341except git.exc.GitCommandError: 1342return flask.render_template("not-found.html"), 404 1343 1344branches = repo.heads 1345 1346all_refs = [] 1347for ref in repo.heads: 1348all_refs.append((ref, "head")) 1349for ref in repo.tags: 1350all_refs.append((ref, "tag")) 1351 1352commit_list = [f"/{username}/{repository}/{sha}" for sha in 1353git_command(server_repo_location, None, "log", 1354"--format='%H'").decode().split("\n")] 1355 1356commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc()) 1357page_number = flask.request.args.get("page", 1, type=int) 1358if flask.session.get("username"): 1359default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1360else: 1361default_page_length = 16 1362page_length = flask.request.args.get("per_page", default_page_length, type=int) 1363page_listing = db.paginate(commits, page=page_number, per_page=page_length) 1364 1365if page_listing.has_next: 1366next_page = page_listing.next_num 1367else: 1368next_page = None 1369 1370if page_listing.has_prev: 1371prev_page = page_listing.prev_num 1372else: 1373prev_page = None 1374 1375return flask.render_template( 1376"repo-log.html", 1377username=username, 1378repository=repository, 1379branches=all_refs, 1380current=branch, 1381repo_data=repo_data, 1382repo=repo, 1383commits=page_listing, 1384remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1385is_favourite=get_favourite(flask.session.get("username"), username, repository), 1386page_number=page_number, 1387page_length=page_length, 1388next_page=next_page, 1389prev_page=prev_page, 1390num_pages=page_listing.pages 1391) 1392 1393 1394@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1395def repository_prs(username, repository): 1396server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1397if not os.path.exists(server_repo_location): 1398app.logger.error(f"Cannot load {server_repo_location}") 1399flask.abort(404) 1400if not (get_visibility(username, repository) or get_permission_level( 1401flask.session.get("username"), username, 1402repository) is not None): 1403flask.abort(403) 1404 1405app.logger.info(f"Loading {server_repo_location}") 1406 1407if not os.path.exists(server_repo_location): 1408app.logger.error(f"Cannot load {server_repo_location}") 1409return flask.render_template("not-found.html"), 404 1410 1411if flask.request.method == "GET": 1412repo = git.Repo(server_repo_location) 1413repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1414user = User.query.filter_by(username=flask.session.get("username")).first() 1415 1416return flask.render_template( 1417"repo-prs.html", 1418username=username, 1419repository=repository, 1420repo_data=repo_data, 1421repo=repo, 1422PullRequest=PullRequest, 1423remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1424is_favourite=get_favourite(flask.session.get("username"), username, repository), 1425default_branch=repo_data.default_branch, 1426branches=repo.branches 1427) 1428 1429else: 1430repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1431head = flask.request.form.get("head") 1432head_route = flask.request.form.get("headroute") 1433base = flask.request.form.get("base") 1434 1435if not head and base and head_route: 1436return flask.redirect(".", 400) 1437 1438head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1439base_repo = git.Repo(server_repo_location) 1440# print(head_repo) 1441 1442if head not in head_repo.branches or base not in base_repo.branches: 1443flask.flash(Markup( 1444"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1445category="error") 1446return flask.redirect(".", 303) 1447 1448head_data = db.session.get(Repo, head_route) 1449if not head_data.visibility: 1450flask.flash(Markup( 1451"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1452"Head can't be restricted")), 1453category="error") 1454return flask.redirect(".", 303) 1455 1456pull_request = PullRequest(head_data, head, repo_data, base, 1457db.session.get(User, flask.session["username"])) 1458 1459db.session.add(pull_request) 1460db.session.commit() 1461 1462return flask.redirect(".", 303) 1463 1464 1465@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1466def repository_prs_merge(username, repository): 1467server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1468if not os.path.exists(server_repo_location): 1469app.logger.error(f"Cannot load {server_repo_location}") 1470flask.abort(404) 1471if not (get_visibility(username, repository) or get_permission_level( 1472flask.session.get("username"), username, 1473repository) is not None): 1474flask.abort(403) 1475 1476if not get_permission_level(flask.session.get("username"), username, repository): 1477flask.abort(401) 1478 1479repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1480repo = git.Repo(server_repo_location) 1481id = flask.request.form.get("id") 1482 1483pull_request = db.session.get(PullRequest, id) 1484 1485if pull_request: 1486result = celery_tasks.merge_heads.delay( 1487pull_request.head_route, 1488pull_request.head_branch, 1489pull_request.base_route, 1490pull_request.base_branch, 1491simulate=True 1492) 1493task_result = worker.AsyncResult(result.id) 1494 1495return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1496# db.session.delete(pull_request) 1497# db.session.commit() 1498else: 1499flask.abort(400) 1500 1501 1502@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1503def repository_prs_merge_stage_two(username, repository, id): 1504server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1505if not os.path.exists(server_repo_location): 1506app.logger.error(f"Cannot load {server_repo_location}") 1507flask.abort(404) 1508if not (get_visibility(username, repository) or get_permission_level( 1509flask.session.get("username"), username, 1510repository) is not None): 1511flask.abort(403) 1512 1513if not get_permission_level(flask.session.get("username"), username, repository): 1514flask.abort(401) 1515 1516repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1517repo = git.Repo(server_repo_location) 1518 1519pull_request = db.session.get(PullRequest, id) 1520 1521if pull_request: 1522result = celery_tasks.merge_heads.delay( 1523pull_request.head_route, 1524pull_request.head_branch, 1525pull_request.base_route, 1526pull_request.base_branch, 1527simulate=False 1528) 1529task_result = worker.AsyncResult(result.id) 1530 1531pull_request.state = 1 1532db.session.commit() 1533 1534return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1535# db.session.delete(pull_request) 1536else: 1537flask.abort(400) 1538 1539 1540@app.route("/task/<task_id>") 1541def task_monitor(task_id): 1542task_result = worker.AsyncResult(task_id) 1543if task_result.status == "FAILURE": 1544app.logger.error(f"Task {task_id} failed") 1545return flask.render_template("task-monitor.html", result=task_result), 500 1546 1547return flask.render_template("task-monitor.html", result=task_result) 1548 1549 1550@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1551def repository_prs_delete(username, repository): 1552server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1553if not os.path.exists(server_repo_location): 1554app.logger.error(f"Cannot load {server_repo_location}") 1555flask.abort(404) 1556if not (get_visibility(username, repository) or get_permission_level( 1557flask.session.get("username"), username, 1558repository) is not None): 1559flask.abort(403) 1560 1561if not get_permission_level(flask.session.get("username"), username, repository): 1562flask.abort(401) 1563 1564repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1565repo = git.Repo(server_repo_location) 1566id = flask.request.form.get("id") 1567 1568pull_request = db.session.get(PullRequest, id) 1569 1570if pull_request: 1571pull_request.state = 2 1572db.session.commit() 1573 1574return flask.redirect(".", 303) 1575 1576 1577@repositories.route("/<username>/<repository>/settings/") 1578def repository_settings(username, repository): 1579if get_permission_level(flask.session.get("username"), username, repository) != 2: 1580flask.abort(401) 1581 1582repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository)) 1583 1584site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>") 1585primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>") 1586 1587return flask.render_template("repo-settings.html", username=username, repository=repository, 1588repo_data=db.session.get(Repo, f"/{username}/{repository}"), 1589branches=[branch.name for branch in repo.branches], 1590site_link=site_link, primary_site_link=primary_site_link, 1591remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1592is_favourite=get_favourite(flask.session.get("username"), username, repository), 1593) 1594 1595 1596@repositories.route("/<username>/<repository>/settings/", methods=["POST"]) 1597def repository_settings_post(username, repository): 1598if get_permission_level(flask.session.get("username"), username, repository) != 2: 1599flask.abort(401) 1600 1601repo = db.session.get(Repo, f"/{username}/{repository}") 1602 1603repo.visibility = flask.request.form.get("visibility", type=int) 1604repo.info = flask.request.form.get("description") 1605repo.default_branch = flask.request.form.get("default_branch") 1606 1607# Update site settings 1608had_site = repo.has_site 1609old_branch = repo.site_branch 1610if flask.request.form.get("site_branch"): 1611repo.site_branch = flask.request.form.get("site_branch") 1612if flask.request.form.get("primary_site"): 1613if had_site != 2: 1614# Remove primary site from other repos 1615for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2): 1616other_repo.has_site = 1 # switch it to a regular site 1617flask.flash(Markup( 1618_("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( 1619repository=other_repo.route 1620)), category="warning") 1621repo.has_site = 2 1622else: 1623repo.has_site = 1 1624else: 1625repo.site_branch = None 1626repo.has_site = 0 1627 1628db.session.commit() 1629 1630if not (had_site, old_branch) == (repo.has_site, repo.site_branch): 1631# Deploy the newly activated site 1632result = celery_tasks.copy_site.delay(repo.route) 1633 1634if had_site and not repo.has_site: 1635# Remove the site 1636result = celery_tasks.delete_site.delay(repo.route) 1637 1638if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site): 1639# Deploy all other sites which were destroyed by the primary site 1640for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1): 1641result = celery_tasks.copy_site.delay(other_repo.route) 1642 1643return flask.redirect(f"/{username}/{repository}/settings", 303) 1644 1645 1646@app.errorhandler(404) 1647def e404(error): 1648return flask.render_template("not-found.html"), 404 1649 1650 1651@app.errorhandler(401) 1652def e401(error): 1653return flask.render_template("unauthorised.html"), 401 1654 1655 1656@app.errorhandler(403) 1657def e403(error): 1658return flask.render_template("forbidden.html"), 403 1659 1660 1661@app.errorhandler(418) 1662def e418(error): 1663return flask.render_template("teapot.html"), 418 1664 1665 1666@app.errorhandler(405) 1667def e405(error): 1668return flask.render_template("method-not-allowed.html"), 405 1669 1670 1671if __name__ == "__main__": 1672app.run(debug=True, port=8080, host="0.0.0.0") 1673 1674app.register_blueprint(repositories) 1675