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