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