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