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