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