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