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