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