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>/reply", methods=["POST"]) 823def repository_forum_reply(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 835if not os.path.exists(server_repo_location): 836app.logger.error(f"Cannot load {server_repo_location}") 837return flask.render_template("not-found.html"), 404 838 839repo = git.Repo(server_repo_location) 840repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 841user = User.query.filter_by(username=flask.session.get("username")).first() 842relationships = RepoAccess.query.filter_by(repo=repo_data) 843user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 844if not user: 845flask.abort(401) 846 847parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 848post = Post(user, repo_data, parent, flask.request.form["subject"], 849flask.request.form["message"]) 850 851db.session.add(post) 852post.update_date() 853db.session.commit() 854 855return flask.redirect( 856flask.url_for(".repository_forum_thread", username=username, repository=repository, 857post_id=post_id), 858code=303) 859 860 861@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 862defaults={"score": 1}) 863@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 864defaults={"score": -1}) 865@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 866def repository_forum_vote(username, repository, post_id, score): 867server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 868if not os.path.exists(server_repo_location): 869app.logger.error(f"Cannot load {server_repo_location}") 870flask.abort(404) 871if not (get_visibility(username, repository) or get_permission_level( 872flask.session.get("username"), username, 873repository) is not None): 874flask.abort(403) 875 876app.logger.info(f"Loading {server_repo_location}") 877 878if not os.path.exists(server_repo_location): 879app.logger.error(f"Cannot load {server_repo_location}") 880return flask.render_template("not-found.html"), 404 881 882repo = git.Repo(server_repo_location) 883repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 884user = User.query.filter_by(username=flask.session.get("username")).first() 885relationships = RepoAccess.query.filter_by(repo=repo_data) 886user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 887if not user: 888flask.abort(401) 889 890post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 891 892if score: 893old_relationship = PostVote.query.filter_by(user_username=user.username, 894post_identifier=post.identifier).first() 895if old_relationship: 896if score == old_relationship.vote_score: 897db.session.delete(old_relationship) 898post.vote_sum -= old_relationship.vote_score 899else: 900post.vote_sum -= old_relationship.vote_score 901post.vote_sum += score 902old_relationship.vote_score = score 903else: 904relationship = PostVote(user, post, score) 905post.vote_sum += score 906db.session.add(relationship) 907 908db.session.commit() 909 910user_vote = PostVote.query.filter_by(user_username=user.username, 911post_identifier=post.identifier).first() 912response = flask.make_response( 913str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 914response.content_type = "text/plain" 915 916return response 917 918 919@repositories.route("/<username>/<repository>/favourite") 920def repository_favourite(username, repository): 921server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 922if not os.path.exists(server_repo_location): 923app.logger.error(f"Cannot load {server_repo_location}") 924flask.abort(404) 925if not (get_visibility(username, repository) or get_permission_level( 926flask.session.get("username"), username, 927repository) is not None): 928flask.abort(403) 929 930app.logger.info(f"Loading {server_repo_location}") 931 932if not os.path.exists(server_repo_location): 933app.logger.error(f"Cannot load {server_repo_location}") 934return flask.render_template("not-found.html"), 404 935 936repo = git.Repo(server_repo_location) 937repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 938user = User.query.filter_by(username=flask.session.get("username")).first() 939relationships = RepoAccess.query.filter_by(repo=repo_data) 940user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 941if not user: 942flask.abort(401) 943 944old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 945repo_route=repo_data.route).first() 946if old_relationship: 947db.session.delete(old_relationship) 948else: 949relationship = RepoFavourite(user, repo_data) 950db.session.add(relationship) 951 952db.session.commit() 953 954return flask.redirect(flask.url_for("favourites"), code=303) 955 956 957@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 958def repository_users(username, repository): 959server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 960if not os.path.exists(server_repo_location): 961app.logger.error(f"Cannot load {server_repo_location}") 962flask.abort(404) 963if not (get_visibility(username, repository) or get_permission_level( 964flask.session.get("username"), username, 965repository) is not None): 966flask.abort(403) 967 968app.logger.info(f"Loading {server_repo_location}") 969 970if not os.path.exists(server_repo_location): 971app.logger.error(f"Cannot load {server_repo_location}") 972return flask.render_template("not-found.html"), 404 973 974repo = git.Repo(server_repo_location) 975repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 976user = User.query.filter_by(username=flask.session.get("username")).first() 977relationships = RepoAccess.query.filter_by(repo=repo_data) 978user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 979 980if flask.request.method == "GET": 981return flask.render_template( 982"repo-users.html", 983username=username, 984repository=repository, 985repo_data=repo_data, 986relationships=relationships, 987repo=repo, 988user_relationship=user_relationship, 989remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 990is_favourite=get_favourite(flask.session.get("username"), username, repository) 991) 992else: 993if get_permission_level(flask.session.get("username"), username, repository) != 2: 994flask.abort(401) 995 996if flask.request.form.get("new-username"): 997# Create new relationship 998new_user = User.query.filter_by( 999username=flask.request.form.get("new-username")).first() 1000relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1001db.session.add(relationship) 1002db.session.commit() 1003if flask.request.form.get("update-username"): 1004# Create new relationship 1005updated_user = User.query.filter_by( 1006username=flask.request.form.get("update-username")).first() 1007relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1008if flask.request.form.get("update-level") == -1: 1009relationship.delete() 1010else: 1011relationship.access_level = flask.request.form.get("update-level") 1012db.session.commit() 1013 1014return flask.redirect( 1015app.url_for(".repository_users", username=username, repository=repository)) 1016 1017 1018@repositories.route("/<username>/<repository>/branches/") 1019def repository_branches(username, repository): 1020server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1021if not os.path.exists(server_repo_location): 1022app.logger.error(f"Cannot load {server_repo_location}") 1023flask.abort(404) 1024if not (get_visibility(username, repository) or get_permission_level( 1025flask.session.get("username"), username, 1026repository) is not None): 1027flask.abort(403) 1028 1029app.logger.info(f"Loading {server_repo_location}") 1030 1031if not os.path.exists(server_repo_location): 1032app.logger.error(f"Cannot load {server_repo_location}") 1033return flask.render_template("not-found.html"), 404 1034 1035repo = git.Repo(server_repo_location) 1036repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1037 1038return flask.render_template( 1039"repo-branches.html", 1040username=username, 1041repository=repository, 1042repo_data=repo_data, 1043repo=repo, 1044remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1045is_favourite=get_favourite(flask.session.get("username"), username, repository) 1046) 1047 1048 1049@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1050@repositories.route("/<username>/<repository>/log/<branch>/") 1051def repository_log(username, repository, branch): 1052server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1053if not os.path.exists(server_repo_location): 1054app.logger.error(f"Cannot load {server_repo_location}") 1055flask.abort(404) 1056if not (get_visibility(username, repository) or get_permission_level( 1057flask.session.get("username"), username, 1058repository) is not None): 1059flask.abort(403) 1060 1061app.logger.info(f"Loading {server_repo_location}") 1062 1063if not os.path.exists(server_repo_location): 1064app.logger.error(f"Cannot load {server_repo_location}") 1065return flask.render_template("not-found.html"), 404 1066 1067repo = git.Repo(server_repo_location) 1068repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1069if not repo_data.default_branch: 1070if repo.heads: 1071repo_data.default_branch = repo.heads[0].name 1072else: 1073return flask.render_template("empty.html", 1074remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1075if not branch: 1076branch = repo_data.default_branch 1077return flask.redirect(f"./{branch}", code=302) 1078 1079if branch.startswith("tag:"): 1080ref = f"tags/{branch[4:]}" 1081elif branch.startswith("~"): 1082ref = branch[1:] 1083else: 1084ref = f"heads/{branch}" 1085 1086ref = ref.replace("~", "/") # encode slashes for URL support 1087 1088try: 1089repo.git.checkout("-f", ref) 1090except git.exc.GitCommandError: 1091return flask.render_template("not-found.html"), 404 1092 1093branches = repo.heads 1094 1095all_refs = [] 1096for ref in repo.heads: 1097all_refs.append((ref, "head")) 1098for ref in repo.tags: 1099all_refs.append((ref, "tag")) 1100 1101commit_list = [f"/{username}/{repository}/{sha}" for sha in 1102git_command(server_repo_location, None, "log", 1103"--format='%H'").decode().split("\n")] 1104 1105commits = Commit.query.filter(Commit.identifier.in_(commit_list)) 1106 1107return flask.render_template( 1108"repo-log.html", 1109username=username, 1110repository=repository, 1111branches=all_refs, 1112current=branch, 1113repo_data=repo_data, 1114repo=repo, 1115commits=commits, 1116remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1117is_favourite=get_favourite(flask.session.get("username"), username, repository) 1118) 1119 1120 1121@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1122def repository_prs(username, repository): 1123server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1124if not os.path.exists(server_repo_location): 1125app.logger.error(f"Cannot load {server_repo_location}") 1126flask.abort(404) 1127if not (get_visibility(username, repository) or get_permission_level( 1128flask.session.get("username"), username, 1129repository) is not None): 1130flask.abort(403) 1131 1132app.logger.info(f"Loading {server_repo_location}") 1133 1134if not os.path.exists(server_repo_location): 1135app.logger.error(f"Cannot load {server_repo_location}") 1136return flask.render_template("not-found.html"), 404 1137 1138if flask.request.method == "GET": 1139repo = git.Repo(server_repo_location) 1140repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1141user = User.query.filter_by(username=flask.session.get("username")).first() 1142 1143return flask.render_template( 1144"repo-prs.html", 1145username=username, 1146repository=repository, 1147repo_data=repo_data, 1148repo=repo, 1149PullRequest=PullRequest, 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), 1152default_branch=repo_data.default_branch, 1153branches=repo.branches 1154) 1155 1156else: 1157repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1158head = flask.request.form.get("head") 1159head_route = flask.request.form.get("headroute") 1160base = flask.request.form.get("base") 1161 1162if not head and base and head_route: 1163return flask.redirect(".", 400) 1164 1165head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1166base_repo = git.Repo(server_repo_location) 1167print(head_repo) 1168 1169if head not in head_repo.branches or base not in base_repo.branches: 1170flask.flash(Markup( 1171"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1172category="error") 1173return flask.redirect(".", 303) 1174 1175head_data = db.session.get(Repo, head_route) 1176if not head_data.visibility: 1177flask.flash(Markup( 1178"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1179"Head can't be restricted")), 1180category="error") 1181return flask.redirect(".", 303) 1182 1183pull_request = PullRequest(repo_data, head, head_data, base, 1184db.session.get(User, flask.session["username"])) 1185 1186db.session.add(pull_request) 1187db.session.commit() 1188 1189return flask.redirect(".", 303) 1190 1191 1192@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1193def repository_prs_merge(username, repository): 1194server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1195if not os.path.exists(server_repo_location): 1196app.logger.error(f"Cannot load {server_repo_location}") 1197flask.abort(404) 1198if not (get_visibility(username, repository) or get_permission_level( 1199flask.session.get("username"), username, 1200repository) is not None): 1201flask.abort(403) 1202 1203if not get_permission_level(flask.session.get("username"), username, repository): 1204flask.abort(401) 1205 1206repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1207repo = git.Repo(server_repo_location) 1208id = flask.request.form.get("id") 1209 1210pull_request = db.session.get(PullRequest, id) 1211 1212if pull_request: 1213result = celery_tasks.merge_heads.delay( 1214pull_request.head_route, 1215pull_request.head_branch, 1216pull_request.base_route, 1217pull_request.base_branch, 1218simulate=True 1219) 1220task_result = worker.AsyncResult(result.id) 1221 1222return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1223# db.session.delete(pull_request) 1224# db.session.commit() 1225else: 1226flask.abort(400) 1227 1228 1229@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1230def repository_prs_merge_stage_two(username, repository, id): 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) 1245 1246pull_request = db.session.get(PullRequest, id) 1247 1248if pull_request: 1249result = celery_tasks.merge_heads.delay( 1250pull_request.head_route, 1251pull_request.head_branch, 1252pull_request.base_route, 1253pull_request.base_branch, 1254simulate=False 1255) 1256task_result = worker.AsyncResult(result.id) 1257 1258return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1259# db.session.delete(pull_request) 1260# db.session.commit() 1261else: 1262flask.abort(400) 1263 1264 1265@app.route("/task/<task_id>") 1266def task_monitor(task_id): 1267task_result = worker.AsyncResult(task_id) 1268print(task_result.status) 1269 1270return flask.render_template("task-monitor.html", result=task_result) 1271 1272 1273@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1274def repository_prs_delete(username, repository): 1275server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1276if not os.path.exists(server_repo_location): 1277app.logger.error(f"Cannot load {server_repo_location}") 1278flask.abort(404) 1279if not (get_visibility(username, repository) or get_permission_level( 1280flask.session.get("username"), username, 1281repository) is not None): 1282flask.abort(403) 1283 1284if not get_permission_level(flask.session.get("username"), username, repository): 1285flask.abort(401) 1286 1287repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1288repo = git.Repo(server_repo_location) 1289id = flask.request.form.get("id") 1290 1291pull_request = db.session.get(PullRequest, id) 1292 1293if pull_request: 1294db.session.delete(pull_request) 1295db.session.commit() 1296 1297return flask.redirect(".", 303) 1298 1299 1300@repositories.route("/<username>/<repository>/settings/") 1301def repository_settings(username, repository): 1302if get_permission_level(flask.session.get("username"), username, repository) != 2: 1303flask.abort(401) 1304 1305return flask.render_template("repo-settings.html", username=username, repository=repository) 1306 1307 1308@app.errorhandler(404) 1309def e404(error): 1310return flask.render_template("not-found.html"), 404 1311 1312 1313@app.errorhandler(401) 1314def e401(error): 1315return flask.render_template("unauthorised.html"), 401 1316 1317 1318@app.errorhandler(403) 1319def e403(error): 1320return flask.render_template("forbidden.html"), 403 1321 1322 1323@app.errorhandler(418) 1324def e418(error): 1325return flask.render_template("teapot.html"), 418 1326 1327 1328@app.errorhandler(405) 1329def e405(error): 1330return flask.render_template("method-not-allowed.html"), 405 1331 1332 1333if __name__ == "__main__": 1334app.run(debug=True, port=8080, host="0.0.0.0") 1335 1336app.register_blueprint(repositories)