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 in config.RESERVED_NAMES: 364flask.flash( 365Markup( 366_("Sorry, {username} is a system path").format( 367username=username)), 368category="error") 369return flask.render_template("login.html") 370 371display_name = None 372if not username.islower(): 373display_name = username 374username = username.lower() 375flask.flash(Markup( 376_("Usernames must be lowercase, so it's been converted automatically")), 377category="info") 378 379user_check = User.query.filter_by(username=username).first() 380if user_check or email2: # make the honeypot look like a normal error 381flask.flash( 382Markup( 383_( 384"The username {username} is taken").format( 385username=username)), 386category="error") 387return flask.render_template("login.html") 388 389if password2 != password: 390flask.flash(Markup(_( 391"Make sure the passwords match")), 392category="error") 393return flask.render_template("login.html") 394 395user = User(username, password, email, name) 396if display_name: 397user.display_name = display_name 398db.session.add(user) 399db.session.commit() 400flask.session["username"] = user.username 401flask.flash(Markup( 402_( 403"Successfully created and logged in as {username}").format( 404username=username)), 405category="success") 406 407notification = Notification({"type": "welcome"}) 408db.session.add(notification) 409db.session.commit() 410 411return flask.redirect("/", code=303) 412 413 414@app.route("/newrepo/", methods=["GET", "POST"]) 415def new_repo(): 416if not flask.session.get("username"): 417flask.abort(401) 418if flask.request.method == "GET": 419return flask.render_template("new-repo.html") 420else: 421name = flask.request.form["name"] 422visibility = int(flask.request.form["visibility"]) 423 424if not only_chars(name, 425"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 426flask.flash(Markup( 427"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 428"Repository names may only contain Latin alphabet, numbers, '-' and '_'")), 429category="error") 430return flask.render_template("new-repo.html") 431 432user = User.query.filter_by(username=flask.session.get("username")).first() 433 434repo = Repo(user, name, visibility) 435db.session.add(repo) 436db.session.commit() 437 438flask.flash(Markup(_("Successfully created repository {name}").format(name=name)), 439category="success") 440return flask.redirect(repo.route, code=303) 441 442 443@app.route("/logout") 444def logout(): 445flask.session.clear() 446flask.flash(Markup( 447"<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")), 448category="info") 449return flask.redirect("/", code=303) 450 451 452@app.route("/<username>/", methods=["GET", "POST"]) 453def user_profile(username): 454if db.session.get(User, username) is None: 455flask.abort(404) 456old_relationship = UserFollow.query.filter_by( 457follower_username=flask.session.get("username"), 458followed_username=username).first() 459if flask.request.method == "GET": 460user = User.query.filter_by(username=username).first() 461match flask.request.args.get("action"): 462case "repositories": 463repos = Repo.query.filter_by(owner_name=username, visibility=2) 464return flask.render_template("user-profile-repositories.html", user=user, 465repos=repos, 466relationship=old_relationship) 467case "followers": 468return flask.render_template("user-profile-followers.html", user=user, 469relationship=old_relationship) 470case "follows": 471return flask.render_template("user-profile-follows.html", user=user, 472relationship=old_relationship) 473case _: 474return flask.render_template("user-profile-overview.html", user=user, 475relationship=old_relationship) 476 477elif flask.request.method == "POST": 478match flask.request.args.get("action"): 479case "follow": 480if username == flask.session.get("username"): 481flask.abort(403) 482if old_relationship: 483db.session.delete(old_relationship) 484else: 485relationship = UserFollow( 486flask.session.get("username"), 487username 488) 489db.session.add(relationship) 490db.session.commit() 491 492user = db.session.get(User, username) 493author = db.session.get(User, flask.session.get("username")) 494notification = Notification({"type": "update", "version": "0.0.0"}) 495db.session.add(notification) 496db.session.commit() 497 498db.session.commit() 499return flask.redirect("?", code=303) 500 501 502@app.route("/<username>/<repository>/") 503def repository_index(username, repository): 504return flask.redirect("./tree", code=302) 505 506 507@app.route("/info/<username>/avatar") 508def user_avatar(username): 509server_userdata_location = os.path.join(config.USERDATA_PATH, username) 510if not os.path.exists(server_userdata_location): 511return flask.render_template("not-found.html"), 404 512 513return flask.send_from_directory(server_userdata_location, "avatar.png") 514 515 516@app.route("/info/<username>/avatar", methods=["POST"]) 517def user_avatar_upload(username): 518server_userdata_location = os.path.join(config.USERDATA_PATH, username) 519 520if not os.path.exists(server_userdata_location): 521flask.abort(404) 522if not flask.session.get("username") == username: 523flask.abort(403) 524 525# Convert image to PNG 526try: 527image = Image.open(flask.request.files["avatar"]) 528except PIL.UnidentifiedImageError: 529flask.abort(400) 530image.save(os.path.join(server_userdata_location, "avatar.png")) 531 532return flask.redirect(f"/{username}", code=303) 533 534 535@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 536def repository_raw(username, repository, branch, subpath): 537server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 538if not os.path.exists(server_repo_location): 539app.logger.error(f"Cannot load {server_repo_location}") 540flask.abort(404) 541if not (get_visibility(username, repository) or get_permission_level( 542flask.session.get("username"), username, 543repository) is not None): 544flask.abort(403) 545 546app.logger.info(f"Loading {server_repo_location}") 547 548if not os.path.exists(server_repo_location): 549app.logger.error(f"Cannot load {server_repo_location}") 550return flask.render_template("not-found.html"), 404 551 552repo = git.Repo(server_repo_location) 553repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 554if not repo_data.default_branch: 555if repo.heads: 556repo_data.default_branch = repo.heads[0].name 557else: 558return flask.render_template("empty.html", 559remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 560if not branch: 561branch = repo_data.default_branch 562return flask.redirect(f"./{branch}", code=302) 563 564if branch.startswith("tag:"): 565ref = f"tags/{branch[4:]}" 566elif branch.startswith("~"): 567ref = branch[1:] 568else: 569ref = f"heads/{branch}" 570 571ref = ref.replace("~", "/") # encode slashes for URL support 572 573try: 574repo.git.checkout("-f", ref) 575except git.exc.GitCommandError: 576return flask.render_template("not-found.html"), 404 577 578return flask.send_from_directory(config.REPOS_PATH, 579os.path.join(username, repository, subpath)) 580 581 582@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 583@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 584@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 585def repository_tree(username, repository, branch, subpath): 586server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 587if not os.path.exists(server_repo_location): 588app.logger.error(f"Cannot load {server_repo_location}") 589flask.abort(404) 590if not (get_visibility(username, repository) or get_permission_level( 591flask.session.get("username"), username, 592repository) is not None): 593flask.abort(403) 594 595app.logger.info(f"Loading {server_repo_location}") 596 597repo = git.Repo(server_repo_location) 598repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 599if not repo_data.default_branch: 600if repo.heads: 601repo_data.default_branch = repo.heads[0].name 602else: 603return flask.render_template("empty.html", 604remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 605if not branch: 606branch = repo_data.default_branch 607return flask.redirect(f"./{branch}", code=302) 608 609if branch.startswith("tag:"): 610ref = f"tags/{branch[4:]}" 611elif branch.startswith("~"): 612ref = branch[1:] 613else: 614ref = f"heads/{branch}" 615 616ref = ref.replace("~", "/") # encode slashes for URL support 617 618try: 619repo.git.checkout("-f", ref) 620except git.exc.GitCommandError: 621return flask.render_template("not-found.html"), 404 622 623branches = repo.heads 624 625all_refs = [] 626for ref in repo.heads: 627all_refs.append((ref, "head")) 628for ref in repo.tags: 629all_refs.append((ref, "tag")) 630 631if os.path.isdir(os.path.join(server_repo_location, subpath)): 632files = [] 633blobs = [] 634 635for entry in os.listdir(os.path.join(server_repo_location, subpath)): 636if not os.path.basename(entry) == ".git": 637files.append(os.path.join(subpath, entry)) 638 639infos = [] 640 641for file in files: 642path = os.path.join(server_repo_location, file) 643mimetype = guess_mime(path) 644 645text = git_command(server_repo_location, None, "log", "--format='%H\n'", 646shlex.quote(file)).decode() 647 648sha = text.split("\n")[0] 649identifier = f"/{username}/{repository}/{sha}" 650 651last_commit = db.session.get(Commit, identifier) 652 653info = { 654"name": os.path.basename(file), 655"serverPath": path, 656"relativePath": file, 657"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 658"size": human_size(os.path.getsize(path)), 659"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 660"commit": last_commit, 661"shaSize": 7, 662} 663 664special_icon = config.match_icon(os.path.basename(file)) 665if special_icon: 666info["icon"] = special_icon 667elif os.path.isdir(path): 668info["icon"] = config.folder_icon 669elif mimetypes.guess_type(path)[0] in config.file_icons: 670info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]] 671else: 672info["icon"] = config.unknown_icon 673 674if os.path.isdir(path): 675infos.insert(0, info) 676else: 677infos.append(info) 678 679return flask.render_template( 680"repo-tree.html", 681username=username, 682repository=repository, 683files=infos, 684subpath=os.path.join("/", subpath), 685branches=all_refs, 686current=branch, 687remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 688is_favourite=get_favourite(flask.session.get("username"), username, repository), 689repo_data=repo_data, 690) 691else: 692path = os.path.join(server_repo_location, subpath) 693 694if not os.path.exists(path): 695return flask.render_template("not-found.html"), 404 696 697mimetype = guess_mime(path) 698mode = mimetype.split("/", 1)[0] 699size = human_size(os.path.getsize(path)) 700 701special_icon = config.match_icon(os.path.basename(path)) 702if special_icon: 703icon = special_icon 704elif os.path.isdir(path): 705icon = config.folder_icon 706elif mimetypes.guess_type(path)[0] in config.file_icons: 707icon = config.file_icons[mimetypes.guess_type(path)[0]] 708else: 709icon = config.unknown_icon 710 711contents = None 712if mode == "text": 713contents = convert_to_html(path) 714 715return flask.render_template( 716"repo-file.html", 717username=username, 718repository=repository, 719file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 720branches=all_refs, 721current=branch, 722mode=mode, 723mimetype=mimetype, 724detailedtype=magic.from_file(path), 725size=size, 726icon=icon, 727subpath=os.path.join("/", subpath), 728extension=pathlib.Path(path).suffix, 729basename=os.path.basename(path), 730contents=contents, 731remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 732is_favourite=get_favourite(flask.session.get("username"), username, repository), 733repo_data=repo_data, 734) 735 736 737@repositories.route("/<username>/<repository>/commit/<sha>") 738def repository_commit(username, repository, sha): 739server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 740if not os.path.exists(server_repo_location): 741app.logger.error(f"Cannot load {server_repo_location}") 742flask.abort(404) 743if not (get_visibility(username, repository) or get_permission_level( 744flask.session.get("username"), username, 745repository) is not None): 746flask.abort(403) 747 748app.logger.info(f"Loading {server_repo_location}") 749 750if not os.path.exists(server_repo_location): 751app.logger.error(f"Cannot load {server_repo_location}") 752return flask.render_template("not-found.html"), 404 753 754repo = git.Repo(server_repo_location) 755repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 756 757files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r", 758"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1] 759 760print(files) 761 762return flask.render_template( 763"repo-commit.html", 764username=username, 765repository=repository, 766remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 767is_favourite=get_favourite(flask.session.get("username"), username, repository), 768diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff", 769str(sha) + "^!", "--", file).decode().split("\n") for 770file in files}, 771data=db.session.get(Commit, f"/{username}/{repository}/{sha}"), 772) 773 774 775@repositories.route("/<username>/<repository>/forum/") 776def repository_forum(username, repository): 777server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 778if not os.path.exists(server_repo_location): 779app.logger.error(f"Cannot load {server_repo_location}") 780flask.abort(404) 781if not (get_visibility(username, repository) or get_permission_level( 782flask.session.get("username"), username, 783repository) is not None): 784flask.abort(403) 785 786app.logger.info(f"Loading {server_repo_location}") 787 788if not os.path.exists(server_repo_location): 789app.logger.error(f"Cannot load {server_repo_location}") 790return flask.render_template("not-found.html"), 404 791 792repo = git.Repo(server_repo_location) 793repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 794user = User.query.filter_by(username=flask.session.get("username")).first() 795relationships = RepoAccess.query.filter_by(repo=repo_data) 796user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 797 798return flask.render_template( 799"repo-forum.html", 800username=username, 801repository=repository, 802repo_data=repo_data, 803relationships=relationships, 804repo=repo, 805user_relationship=user_relationship, 806Post=Post, 807remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 808is_favourite=get_favourite(flask.session.get("username"), username, repository), 809default_branch=repo_data.default_branch 810) 811 812 813@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 814def repository_forum_topic(username, repository, id): 815server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 816if not os.path.exists(server_repo_location): 817app.logger.error(f"Cannot load {server_repo_location}") 818flask.abort(404) 819if not (get_visibility(username, repository) or get_permission_level( 820flask.session.get("username"), username, 821repository) is not None): 822flask.abort(403) 823 824app.logger.info(f"Loading {server_repo_location}") 825 826if not os.path.exists(server_repo_location): 827app.logger.error(f"Cannot load {server_repo_location}") 828return flask.render_template("not-found.html"), 404 829 830repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 831user = User.query.filter_by(username=flask.session.get("username")).first() 832relationships = RepoAccess.query.filter_by(repo=repo_data) 833user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 834 835post = Post.query.filter_by(id=id).first() 836 837return flask.render_template( 838"repo-topic.html", 839username=username, 840repository=repository, 841repo_data=repo_data, 842relationships=relationships, 843user_relationship=user_relationship, 844post=post, 845remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 846is_favourite=get_favourite(flask.session.get("username"), username, repository), 847default_branch=repo_data.default_branch 848) 849 850 851@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 852def repository_forum_new(username, repository): 853server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 854if not os.path.exists(server_repo_location): 855app.logger.error(f"Cannot load {server_repo_location}") 856flask.abort(404) 857if not (get_visibility(username, repository) or get_permission_level( 858flask.session.get("username"), username, 859repository) is not None): 860flask.abort(403) 861 862app.logger.info(f"Loading {server_repo_location}") 863 864if not os.path.exists(server_repo_location): 865app.logger.error(f"Cannot load {server_repo_location}") 866return flask.render_template("not-found.html"), 404 867 868repo = git.Repo(server_repo_location) 869repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 870user = User.query.filter_by(username=flask.session.get("username")).first() 871relationships = RepoAccess.query.filter_by(repo=repo_data) 872user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 873 874post = Post(user, repo_data, None, flask.request.form["subject"], 875flask.request.form["message"]) 876 877db.session.add(post) 878db.session.commit() 879 880return flask.redirect( 881flask.url_for(".repository_forum_thread", username=username, repository=repository, 882post_id=post.number), 883code=303) 884 885 886@repositories.route("/<username>/<repository>/forum/<int:post_id>") 887def repository_forum_thread(username, repository, post_id): 888server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 889if not os.path.exists(server_repo_location): 890app.logger.error(f"Cannot load {server_repo_location}") 891flask.abort(404) 892if not (get_visibility(username, repository) or get_permission_level( 893flask.session.get("username"), username, 894repository) is not None): 895flask.abort(403) 896 897app.logger.info(f"Loading {server_repo_location}") 898 899if not os.path.exists(server_repo_location): 900app.logger.error(f"Cannot load {server_repo_location}") 901return flask.render_template("not-found.html"), 404 902 903repo = git.Repo(server_repo_location) 904repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 905user = User.query.filter_by(username=flask.session.get("username")).first() 906relationships = RepoAccess.query.filter_by(repo=repo_data) 907user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 908 909if user: 910max_post_nesting = user.max_post_nesting 911else: 912max_post_nesting = 2 913 914return flask.render_template( 915"repo-forum-thread.html", 916username=username, 917repository=repository, 918repo_data=repo_data, 919relationships=relationships, 920repo=repo, 921Post=Post, 922user_relationship=user_relationship, 923post_id=post_id, 924max_post_nesting=max_post_nesting, 925remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 926is_favourite=get_favourite(flask.session.get("username"), username, repository), 927parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 928has_permission=not ((not get_permission_level(flask.session.get("username"), username, 929repository)) and db.session.get(Post, 930f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")), 931) 932 933 934@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state", 935methods=["POST"]) 936def repository_forum_change_state(username, repository, post_id): 937server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 938if not os.path.exists(server_repo_location): 939app.logger.error(f"Cannot load {server_repo_location}") 940flask.abort(404) 941if (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"): 942flask.abort(403) 943 944app.logger.info(f"Loading {server_repo_location}") 945 946repo = git.Repo(server_repo_location) 947repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 948user = User.query.filter_by(username=flask.session.get("username")).first() 949relationships = RepoAccess.query.filter_by(repo=repo_data) 950user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 951 952post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 953 954if not post: 955flask.abort(404) 956 957post.state = int(flask.request.form["new-state"]) 958 959db.session.commit() 960 961return flask.redirect( 962flask.url_for(".repository_forum_thread", username=username, repository=repository, 963post_id=post_id), 964code=303) 965 966 967@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 968def repository_forum_reply(username, repository, post_id): 969server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 970if not os.path.exists(server_repo_location): 971app.logger.error(f"Cannot load {server_repo_location}") 972flask.abort(404) 973if not (get_visibility(username, repository) or get_permission_level( 974flask.session.get("username"), username, 975repository) is not None): 976flask.abort(403) 977 978app.logger.info(f"Loading {server_repo_location}") 979 980if not os.path.exists(server_repo_location): 981app.logger.error(f"Cannot load {server_repo_location}") 982return flask.render_template("not-found.html"), 404 983 984repo = git.Repo(server_repo_location) 985repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 986user = User.query.filter_by(username=flask.session.get("username")).first() 987relationships = RepoAccess.query.filter_by(repo=repo_data) 988user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 989if not user: 990flask.abort(401) 991 992parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 993post = Post(user, repo_data, parent, flask.request.form["subject"], 994flask.request.form["message"]) 995 996db.session.add(post) 997post.update_date() 998db.session.commit() 999 1000return flask.redirect( 1001flask.url_for(".repository_forum_thread", username=username, repository=repository, 1002post_id=post_id), 1003code=303) 1004 1005 1006@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 1007defaults={"score": 1}) 1008@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 1009defaults={"score": -1}) 1010@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 1011def repository_forum_vote(username, repository, post_id, score): 1012server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1013if not os.path.exists(server_repo_location): 1014app.logger.error(f"Cannot load {server_repo_location}") 1015flask.abort(404) 1016if not (get_visibility(username, repository) or get_permission_level( 1017flask.session.get("username"), username, 1018repository) is not None): 1019flask.abort(403) 1020 1021app.logger.info(f"Loading {server_repo_location}") 1022 1023if not os.path.exists(server_repo_location): 1024app.logger.error(f"Cannot load {server_repo_location}") 1025return flask.render_template("not-found.html"), 404 1026 1027repo = git.Repo(server_repo_location) 1028repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1029user = User.query.filter_by(username=flask.session.get("username")).first() 1030relationships = RepoAccess.query.filter_by(repo=repo_data) 1031user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1032if not user: 1033flask.abort(401) 1034 1035post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1036 1037if score: 1038old_relationship = PostVote.query.filter_by(user_username=user.username, 1039post_identifier=post.identifier).first() 1040if old_relationship: 1041if score == old_relationship.vote_score: 1042db.session.delete(old_relationship) 1043post.vote_sum -= old_relationship.vote_score 1044else: 1045post.vote_sum -= old_relationship.vote_score 1046post.vote_sum += score 1047old_relationship.vote_score = score 1048else: 1049relationship = PostVote(user, post, score) 1050post.vote_sum += score 1051db.session.add(relationship) 1052 1053db.session.commit() 1054 1055user_vote = PostVote.query.filter_by(user_username=user.username, 1056post_identifier=post.identifier).first() 1057response = flask.make_response( 1058str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 1059response.content_type = "text/plain" 1060 1061return response 1062 1063 1064@repositories.route("/<username>/<repository>/favourite") 1065def repository_favourite(username, repository): 1066server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1067if not os.path.exists(server_repo_location): 1068app.logger.error(f"Cannot load {server_repo_location}") 1069flask.abort(404) 1070if not (get_visibility(username, repository) or get_permission_level( 1071flask.session.get("username"), username, 1072repository) is not None): 1073flask.abort(403) 1074 1075app.logger.info(f"Loading {server_repo_location}") 1076 1077if not os.path.exists(server_repo_location): 1078app.logger.error(f"Cannot load {server_repo_location}") 1079return flask.render_template("not-found.html"), 404 1080 1081repo = git.Repo(server_repo_location) 1082repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1083user = User.query.filter_by(username=flask.session.get("username")).first() 1084relationships = RepoAccess.query.filter_by(repo=repo_data) 1085user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1086if not user: 1087flask.abort(401) 1088 1089old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 1090repo_route=repo_data.route).first() 1091if old_relationship: 1092db.session.delete(old_relationship) 1093else: 1094relationship = RepoFavourite(user, repo_data) 1095db.session.add(relationship) 1096 1097db.session.commit() 1098 1099return flask.redirect(flask.url_for("favourites"), code=303) 1100 1101 1102@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 1103def repository_users(username, repository): 1104server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1105if not os.path.exists(server_repo_location): 1106app.logger.error(f"Cannot load {server_repo_location}") 1107flask.abort(404) 1108if not (get_visibility(username, repository) or get_permission_level( 1109flask.session.get("username"), username, 1110repository) is not None): 1111flask.abort(403) 1112 1113app.logger.info(f"Loading {server_repo_location}") 1114 1115if not os.path.exists(server_repo_location): 1116app.logger.error(f"Cannot load {server_repo_location}") 1117return flask.render_template("not-found.html"), 404 1118 1119repo = git.Repo(server_repo_location) 1120repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1121user = User.query.filter_by(username=flask.session.get("username")).first() 1122relationships = RepoAccess.query.filter_by(repo=repo_data) 1123user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1124 1125if flask.request.method == "GET": 1126return flask.render_template( 1127"repo-users.html", 1128username=username, 1129repository=repository, 1130repo_data=repo_data, 1131relationships=relationships, 1132repo=repo, 1133user_relationship=user_relationship, 1134remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1135is_favourite=get_favourite(flask.session.get("username"), username, repository) 1136) 1137else: 1138if get_permission_level(flask.session.get("username"), username, repository) != 2: 1139flask.abort(401) 1140 1141if flask.request.form.get("new-username"): 1142# Create new relationship 1143new_user = User.query.filter_by( 1144username=flask.request.form.get("new-username")).first() 1145relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1146db.session.add(relationship) 1147db.session.commit() 1148if flask.request.form.get("update-username"): 1149# Create new relationship 1150updated_user = User.query.filter_by( 1151username=flask.request.form.get("update-username")).first() 1152relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1153if flask.request.form.get("update-level") == -1: 1154relationship.delete() 1155else: 1156relationship.access_level = flask.request.form.get("update-level") 1157db.session.commit() 1158 1159return flask.redirect( 1160app.url_for(".repository_users", username=username, repository=repository)) 1161 1162 1163@repositories.route("/<username>/<repository>/branches/") 1164def repository_branches(username, repository): 1165server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1166if not os.path.exists(server_repo_location): 1167app.logger.error(f"Cannot load {server_repo_location}") 1168flask.abort(404) 1169if not (get_visibility(username, repository) or get_permission_level( 1170flask.session.get("username"), username, 1171repository) is not None): 1172flask.abort(403) 1173 1174app.logger.info(f"Loading {server_repo_location}") 1175 1176if not os.path.exists(server_repo_location): 1177app.logger.error(f"Cannot load {server_repo_location}") 1178return flask.render_template("not-found.html"), 404 1179 1180repo = git.Repo(server_repo_location) 1181repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1182 1183return flask.render_template( 1184"repo-branches.html", 1185username=username, 1186repository=repository, 1187repo_data=repo_data, 1188repo=repo, 1189remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1190is_favourite=get_favourite(flask.session.get("username"), username, repository) 1191) 1192 1193 1194@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1195@repositories.route("/<username>/<repository>/log/<branch>/") 1196def repository_log(username, repository, branch): 1197server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1198if not os.path.exists(server_repo_location): 1199app.logger.error(f"Cannot load {server_repo_location}") 1200flask.abort(404) 1201if not (get_visibility(username, repository) or get_permission_level( 1202flask.session.get("username"), username, 1203repository) is not None): 1204flask.abort(403) 1205 1206app.logger.info(f"Loading {server_repo_location}") 1207 1208if not os.path.exists(server_repo_location): 1209app.logger.error(f"Cannot load {server_repo_location}") 1210return flask.render_template("not-found.html"), 404 1211 1212repo = git.Repo(server_repo_location) 1213repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1214if not repo_data.default_branch: 1215if repo.heads: 1216repo_data.default_branch = repo.heads[0].name 1217else: 1218return flask.render_template("empty.html", 1219remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1220if not branch: 1221branch = repo_data.default_branch 1222return flask.redirect(f"./{branch}", code=302) 1223 1224if branch.startswith("tag:"): 1225ref = f"tags/{branch[4:]}" 1226elif branch.startswith("~"): 1227ref = branch[1:] 1228else: 1229ref = f"heads/{branch}" 1230 1231ref = ref.replace("~", "/") # encode slashes for URL support 1232 1233try: 1234repo.git.checkout("-f", ref) 1235except git.exc.GitCommandError: 1236return flask.render_template("not-found.html"), 404 1237 1238branches = repo.heads 1239 1240all_refs = [] 1241for ref in repo.heads: 1242all_refs.append((ref, "head")) 1243for ref in repo.tags: 1244all_refs.append((ref, "tag")) 1245 1246commit_list = [f"/{username}/{repository}/{sha}" for sha in 1247git_command(server_repo_location, None, "log", 1248"--format='%H'").decode().split("\n")] 1249 1250commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc()) 1251page_number = flask.request.args.get("page", 1, type=int) 1252if flask.session.get("username"): 1253default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1254else: 1255default_page_length = 16 1256page_length = flask.request.args.get("per_page", default_page_length, type=int) 1257page_listing = db.paginate(commits, page=page_number, per_page=page_length) 1258 1259if page_listing.has_next: 1260next_page = page_listing.next_num 1261else: 1262next_page = None 1263 1264if page_listing.has_prev: 1265prev_page = page_listing.prev_num 1266else: 1267prev_page = None 1268 1269return flask.render_template( 1270"repo-log.html", 1271username=username, 1272repository=repository, 1273branches=all_refs, 1274current=branch, 1275repo_data=repo_data, 1276repo=repo, 1277commits=page_listing, 1278remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1279is_favourite=get_favourite(flask.session.get("username"), username, repository), 1280page_number=page_number, 1281page_length=page_length, 1282next_page=next_page, 1283prev_page=prev_page, 1284num_pages=page_listing.pages 1285) 1286 1287 1288@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1289def repository_prs(username, repository): 1290server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1291if not os.path.exists(server_repo_location): 1292app.logger.error(f"Cannot load {server_repo_location}") 1293flask.abort(404) 1294if not (get_visibility(username, repository) or get_permission_level( 1295flask.session.get("username"), username, 1296repository) is not None): 1297flask.abort(403) 1298 1299app.logger.info(f"Loading {server_repo_location}") 1300 1301if not os.path.exists(server_repo_location): 1302app.logger.error(f"Cannot load {server_repo_location}") 1303return flask.render_template("not-found.html"), 404 1304 1305if flask.request.method == "GET": 1306repo = git.Repo(server_repo_location) 1307repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1308user = User.query.filter_by(username=flask.session.get("username")).first() 1309 1310return flask.render_template( 1311"repo-prs.html", 1312username=username, 1313repository=repository, 1314repo_data=repo_data, 1315repo=repo, 1316PullRequest=PullRequest, 1317remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1318is_favourite=get_favourite(flask.session.get("username"), username, repository), 1319default_branch=repo_data.default_branch, 1320branches=repo.branches 1321) 1322 1323else: 1324repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1325head = flask.request.form.get("head") 1326head_route = flask.request.form.get("headroute") 1327base = flask.request.form.get("base") 1328 1329if not head and base and head_route: 1330return flask.redirect(".", 400) 1331 1332head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1333base_repo = git.Repo(server_repo_location) 1334print(head_repo) 1335 1336if head not in head_repo.branches or base not in base_repo.branches: 1337flask.flash(Markup( 1338"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1339category="error") 1340return flask.redirect(".", 303) 1341 1342head_data = db.session.get(Repo, head_route) 1343if not head_data.visibility: 1344flask.flash(Markup( 1345"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1346"Head can't be restricted")), 1347category="error") 1348return flask.redirect(".", 303) 1349 1350pull_request = PullRequest(repo_data, head, head_data, base, 1351db.session.get(User, flask.session["username"])) 1352 1353db.session.add(pull_request) 1354db.session.commit() 1355 1356return flask.redirect(".", 303) 1357 1358 1359@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1360def repository_prs_merge(username, repository): 1361server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1362if not os.path.exists(server_repo_location): 1363app.logger.error(f"Cannot load {server_repo_location}") 1364flask.abort(404) 1365if not (get_visibility(username, repository) or get_permission_level( 1366flask.session.get("username"), username, 1367repository) is not None): 1368flask.abort(403) 1369 1370if not get_permission_level(flask.session.get("username"), username, repository): 1371flask.abort(401) 1372 1373repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1374repo = git.Repo(server_repo_location) 1375id = flask.request.form.get("id") 1376 1377pull_request = db.session.get(PullRequest, id) 1378 1379if pull_request: 1380result = celery_tasks.merge_heads.delay( 1381pull_request.head_route, 1382pull_request.head_branch, 1383pull_request.base_route, 1384pull_request.base_branch, 1385simulate=True 1386) 1387task_result = worker.AsyncResult(result.id) 1388 1389return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1390# db.session.delete(pull_request) 1391# db.session.commit() 1392else: 1393flask.abort(400) 1394 1395 1396@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1397def repository_prs_merge_stage_two(username, repository, id): 1398server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1399if not os.path.exists(server_repo_location): 1400app.logger.error(f"Cannot load {server_repo_location}") 1401flask.abort(404) 1402if not (get_visibility(username, repository) or get_permission_level( 1403flask.session.get("username"), username, 1404repository) is not None): 1405flask.abort(403) 1406 1407if not get_permission_level(flask.session.get("username"), username, repository): 1408flask.abort(401) 1409 1410repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1411repo = git.Repo(server_repo_location) 1412 1413pull_request = db.session.get(PullRequest, id) 1414 1415if pull_request: 1416result = celery_tasks.merge_heads.delay( 1417pull_request.head_route, 1418pull_request.head_branch, 1419pull_request.base_route, 1420pull_request.base_branch, 1421simulate=False 1422) 1423task_result = worker.AsyncResult(result.id) 1424 1425pull_request.state = 1 1426db.session.commit() 1427 1428return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1429# db.session.delete(pull_request) 1430else: 1431flask.abort(400) 1432 1433 1434@app.route("/task/<task_id>") 1435def task_monitor(task_id): 1436task_result = worker.AsyncResult(task_id) 1437if task_result.status == "FAILURE": 1438app.logger.error(f"Task {task_id} failed") 1439return flask.render_template("task-monitor.html", result=task_result), 500 1440 1441return flask.render_template("task-monitor.html", result=task_result) 1442 1443 1444@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1445def repository_prs_delete(username, repository): 1446server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1447if not os.path.exists(server_repo_location): 1448app.logger.error(f"Cannot load {server_repo_location}") 1449flask.abort(404) 1450if not (get_visibility(username, repository) or get_permission_level( 1451flask.session.get("username"), username, 1452repository) is not None): 1453flask.abort(403) 1454 1455if not get_permission_level(flask.session.get("username"), username, repository): 1456flask.abort(401) 1457 1458repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1459repo = git.Repo(server_repo_location) 1460id = flask.request.form.get("id") 1461 1462pull_request = db.session.get(PullRequest, id) 1463 1464if pull_request: 1465pull_request.state = 2 1466db.session.commit() 1467 1468return flask.redirect(".", 303) 1469 1470 1471@repositories.route("/<username>/<repository>/settings/") 1472def repository_settings(username, repository): 1473if get_permission_level(flask.session.get("username"), username, repository) != 2: 1474flask.abort(401) 1475 1476repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository)) 1477 1478site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{repository}.{username}.{config.BASE_DOMAIN}/</code>") 1479primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>") 1480 1481return flask.render_template("repo-settings.html", username=username, repository=repository, 1482repo_data=db.session.get(Repo, f"/{username}/{repository}"), 1483branches=[branch.name for branch in repo.branches], 1484site_link=site_link, primary_site_link=primary_site_link) 1485 1486 1487@repositories.route("/<username>/<repository>/settings/", methods=["POST"]) 1488def repository_settings_post(username, repository): 1489if get_permission_level(flask.session.get("username"), username, repository) != 2: 1490flask.abort(401) 1491 1492repo = db.session.get(Repo, f"/{username}/{repository}") 1493 1494repo.visibility = flask.request.form.get("visibility", type=int) 1495repo.info = flask.request.form.get("description") 1496repo.default_branch = flask.request.form.get("default_branch") 1497 1498# Update site settings 1499had_site = repo.has_site 1500old_branch = repo.site_branch 1501if flask.request.form.get("site_branch"): 1502repo.site_branch = flask.request.form.get("site_branch") 1503if flask.request.form.get("primary_site") and had_site != 2: 1504# Remove primary site from other repos 1505for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2): 1506other_repo.has_site = 1 # switch it to a regular site 1507flask.flash(Markup( 1508_("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( 1509repository=other_repo.route 1510)), category="warning") 1511repo.has_site = 2 1512else: 1513repo.has_site = 1 1514else: 1515repo.site_branch = None 1516repo.has_site = 0 1517 1518db.session.commit() 1519 1520if not (had_site, old_branch) == (repo.has_site, repo.site_branch): 1521# Deploy the newly activated site 1522result = celery_tasks.copy_site.delay(repo.route) 1523 1524if had_site and not repo.has_site: 1525# Remove the site 1526result = celery_tasks.delete_site.delay(repo.route) 1527 1528if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site): 1529# Deploy all other sites which were destroyed by the primary site 1530for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1): 1531result = celery_tasks.copy_site.delay(other_repo.route) 1532 1533return flask.redirect(f"/{username}/{repository}/settings", 303) 1534 1535 1536@app.errorhandler(404) 1537def e404(error): 1538return flask.render_template("not-found.html"), 404 1539 1540 1541@app.errorhandler(401) 1542def e401(error): 1543return flask.render_template("unauthorised.html"), 401 1544 1545 1546@app.errorhandler(403) 1547def e403(error): 1548return flask.render_template("forbidden.html"), 403 1549 1550 1551@app.errorhandler(418) 1552def e418(error): 1553return flask.render_template("teapot.html"), 418 1554 1555 1556@app.errorhandler(405) 1557def e405(error): 1558return flask.render_template("method-not-allowed.html"), 405 1559 1560 1561if __name__ == "__main__": 1562app.run(debug=True, port=8080, host="0.0.0.0") 1563 1564app.register_blueprint(repositories) 1565