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