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