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