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