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