app.py
Python script, Unicode text, UTF-8 text executable
1__version__ = "0.3.1" 2 3import os 4import shutil 5import random 6import subprocess 7import platform 8 9import PIL 10import git 11import mimetypes 12import magic 13import flask 14import cairosvg 15import celery 16import shlex 17from functools import wraps 18from datetime import datetime 19from enum import Enum 20from cairosvg import svg2png 21from flask_sqlalchemy import SQLAlchemy 22from flask_bcrypt import Bcrypt 23from markupsafe import escape, Markup 24from flask_migrate import Migrate 25from PIL import Image 26from flask_httpauth import HTTPBasicAuth 27import config 28from common import git_command 29from flask_babel import Babel, gettext, ngettext, force_locale 30from jinja2_fragments.flask import render_block 31 32import logging 33 34 35class No304(logging.Filter): 36def filter(self, record): 37return not record.getMessage().strip().endswith("304 -") 38 39 40logging.getLogger("werkzeug").addFilter(No304()) 41 42_ = gettext 43n_ = ngettext 44 45app = flask.Flask(__name__) 46app.config.from_mapping( 47CELERY=dict( 48broker_url=config.REDIS_URI, 49result_backend=config.REDIS_URI, 50task_ignore_result=True, 51), 52) 53 54auth = HTTPBasicAuth() 55 56app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 57app.config["SECRET_KEY"] = config.DB_PASSWORD 58app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 59app.config["BABEL_TRANSLATION_DIRECTORIES"] = "i18n" 60app.config["MAX_CONTENT_LENGTH"] = config.MAX_PAYLOAD_SIZE 61app.config["SESSION_COOKIE_SAMESITE"] = "Lax" 62app.config["SESSION_COOKIE_SECURE"] = config.suggest_https # only send cookies over HTTPS if the server is configured for it 63app.config["SESSION_COOKIE_HTTPONLY"] = True # don't allow JS to access the cookie 64if config.restrict_cookie_domain: 65app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN # don't share across subdomains, since user content is hosted there 66 67db = SQLAlchemy(app) 68bcrypt = Bcrypt(app) 69migrate = Migrate(app, db) 70 71from misc_utils import * 72 73import git_http 74import api 75import jinja_utils 76import celery_tasks 77from celery import Celery, Task 78import celery_integration 79import pathlib 80 81from models import * 82 83babel = Babel(app) 84 85 86def get_locale(): 87if flask.request.cookies.get("language"): 88return flask.request.cookies.get("language") 89return flask.request.accept_languages.best_match(config.available_locales) 90 91 92babel.init_app(app, locale_selector=get_locale) 93 94with app.app_context(): 95locale_names = {} 96for language in config.available_locales: 97with force_locale(language): 98# NOTE: Translate this to the language's name in that language, for example in French you would use français 99locale_names[language] = gettext("English") 100 101worker = celery_integration.init_celery_app(app) 102 103repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/") 104 105app.jinja_env.add_extension("jinja2.ext.do") 106app.jinja_env.add_extension("jinja2.ext.loopcontrols") 107app.jinja_env.add_extension("jinja2.ext.debug") 108 109 110@app.context_processor 111def default(): 112username = flask.session.get("username") 113 114user_object = User.query.filter_by(username=username).first() 115 116return { 117"logged_in_user": username, 118"user_object": user_object, 119"Notification": Notification, 120"unread": UserNotification.query.filter_by(user_username=username).filter( 121UserNotification.attention_level > 0).count(), 122"config": config, 123"Markup": Markup, 124"locale_names": locale_names, 125"set": set, # since using {} is impossible in Jinja 126"request": flask.request, 127} 128 129 130@app.route("/") 131def main(): 132if flask.session.get("username"): 133return flask.render_template("home.html") 134else: 135return flask.render_template("no-home.html") 136 137 138@app.route("/userstyle") 139def userstyle(): 140if flask.session.get("username") and os.path.exists( 141os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config", 142"theme.css")): 143return flask.send_from_directory( 144os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config"), 145"theme.css") 146else: 147return flask.Response("", mimetype="text/css") 148 149 150@app.route("/about/") 151def about(): 152return flask.render_template("about.html", platform=platform, version=__version__) 153 154 155@app.route("/search") 156def search(): 157query = flask.request.args.get("q") 158page_number = flask.request.args.get("page", 1, type=int) 159if flask.session.get("username"): 160default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 161else: 162default_page_length = 16 163page_length = flask.request.args.get("per_page", default_page_length, type=int) 164if not query: 165flask.abort(400) 166 167results = Repo.query.filter(Repo.name.ilike(f"%{query}%")).filter_by(visibility=2).paginate( 168page=page_number, per_page=page_length) 169 170if results.has_next: 171next_page = results.next_num 172else: 173next_page = None 174 175if results.has_prev: 176prev_page = results.prev_num 177else: 178prev_page = None 179 180return flask.render_template("search.html", results=results, query=query, 181page_number=page_number, 182page_length=page_length, 183next_page=next_page, 184prev_page=prev_page, 185num_pages=results.pages) 186 187 188@app.route("/language", methods=["POST"]) 189def set_locale(): 190response = flask.redirect(flask.request.referrer if flask.request.referrer else "/", 191code=303) 192if not flask.request.form.get("language"): 193response.delete_cookie("language") 194else: 195response.set_cookie("language", flask.request.form.get("language")) 196 197return response 198 199 200@app.route("/cookie-dismiss") 201def dismiss_banner(): 202response = flask.redirect(flask.request.referrer if flask.request.referrer else "/", 203code=303) 204response.set_cookie("cookie-banner", "1") 205return response 206 207 208@app.route("/help/") 209def help_redirect(): 210return flask.redirect(config.help_url, code=302) 211 212 213@app.route("/settings/") 214def settings(): 215if not flask.session.get("username"): 216flask.abort(401) 217user = User.query.filter_by(username=flask.session.get("username")).first() 218 219return flask.render_template("user-settings.html", user=user) 220 221 222@app.route("/settings/confirm-email/<code>") 223def confirm_email(code): 224request = EmailChangeRequest.query.filter_by(code=code).first() 225if not request: 226flask.abort(404) 227 228user = db.session.get(User, request.user_username) 229user.email = request.new_email 230db.session.delete(request) 231db.session.commit() 232 233return flask.redirect("/settings", code=303) 234 235 236@app.route("/settings/profile", methods=["POST"]) 237def settings_profile(): 238user = User.query.filter_by(username=flask.session.get("username")).first() 239 240user.display_name = flask.request.form["displayname"] 241user.URL = flask.request.form["url"] 242user.company = flask.request.form["company"] 243user.company_URL = flask.request.form["companyurl"] 244if not flask.request.form.get("email"): 245# Deleting the email can be instant; no need to confirm 246user.email = "" 247elif flask.request.form.get("email") != user.email: 248# Changing the email requires confirmation from the address holder 249celery_tasks.request_email_change.delay(user.username, flask.request.form["email"]) 250user.location = flask.request.form["location"] 251user.show_mail = True if flask.request.form.get("showmail") else False 252user.bio = flask.request.form.get("bio") 253 254db.session.commit() 255 256flask.flash( 257Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")), 258category="success") 259return flask.redirect(f"/{flask.session.get('username')}", code=303) 260 261 262@app.route("/settings/preferences", methods=["POST"]) 263def settings_prefs(): 264user = User.query.filter_by(username=flask.session.get("username")).first() 265 266user.default_page_length = int(flask.request.form["page_length"]) 267user.max_post_nesting = int(flask.request.form["max_post_nesting"]) 268 269db.session.commit() 270 271flask.flash( 272Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")), 273category="success") 274return flask.redirect(f"/{flask.session.get('username')}", code=303) 275 276 277@app.route("/favourites/", methods=["GET", "POST"]) 278def favourites(): 279if not flask.session.get("username"): 280flask.abort(401) 281if flask.request.method == "GET": 282relationships = RepoFavourite.query.filter_by( 283user_username=flask.session.get("username")) 284 285return flask.render_template("favourites.html", favourites=relationships) 286 287 288@app.route("/favourites/<int:id>", methods=["POST"]) 289def favourite_edit(id): 290if not flask.session.get("username"): 291flask.abort(401) 292favourite = db.session.get(RepoFavourite, id) 293if favourite.user_username != flask.session.get("username"): 294flask.abort(403) 295data = flask.request.form 296# print(data) 297favourite.notify_commit = js_to_bool(data.get("commit")) 298favourite.notify_forum = js_to_bool(data.get("forum")) 299favourite.notify_pr = js_to_bool(data.get("pull_request")) 300favourite.notify_admin = js_to_bool(data.get("administrative")) 301print(favourite.notify_commit, favourite.notify_forum, favourite.notify_pr, 302favourite.notify_admin) 303db.session.commit() 304return flask.render_template_string( 305""" 306<tr hx-post="/favourites/{{ favourite.id }}" hx-trigger="change" hx-include="#commit-{{ favourite.id }}, #forum-{{ favourite.id }}, #pull_request-{{ favourite.id }}, #administrative-{{ favourite.id }}" hx-headers='{"Content-Type": "application/json"}' hx-swap="outerHTML"> 307<td><a href="{{ favourite.repo.route }}">{{ favourite.repo.owner.username }}/{{ favourite.repo.name }}</a></td> 308<td style="text-align: center;"><input type="checkbox" name="commit" id="commit-{{ favourite.id }}" value="true" {% if favourite.notify_commit %}checked{% endif %}></td> 309<td style="text-align: center;"><input type="checkbox" name="forum" id="forum-{{ favourite.id }}" value="true" {% if favourite.notify_forum %}checked{% endif %}></td> 310<td style="text-align: center;"><input type="checkbox" name="pull_request" id="pull_request-{{ favourite.id }}" value="true" {% if favourite.notify_pr %}checked{% endif %}></td> 311<td style="text-align: center;"><input type="checkbox" name="administrative" id="administrative-{{ favourite.id }}" value="true" {% if favourite.notify_admin %}checked{% endif %}></td> 312</tr> 313""", 314favourite=favourite 315) 316 317 318@app.route("/notifications/", methods=["GET", "POST"]) 319def notifications(): 320if not flask.session.get("username"): 321flask.abort(401) 322if flask.request.method == "GET": 323return flask.render_template("notifications.html", 324notifications=UserNotification.query.filter_by( 325user_username=flask.session.get("username") 326).order_by(UserNotification.id.desc()), 327db=db, Commit=Commit, Post=Post, PullRequest=PullRequest 328) 329 330 331@app.route("/notifications/<int:notification_id>/read", methods=["POST"]) 332def mark_read(notification_id): 333if not flask.session.get("username"): 334flask.abort(401) 335notification = UserNotification.query.filter_by(id=notification_id).first() 336if notification.user_username != flask.session.get("username"): 337flask.abort(403) 338notification.mark_read() 339db.session.commit() 340return flask.render_template_string( 341"<button hx-post='/notifications/{{ notification.id }}/unread' hx-swap='outerHTML'>Mark as unread</button>", 342notification=notification), 200 343 344 345@app.route("/notifications/<int:notification_id>/unread", methods=["POST"]) 346def mark_unread(notification_id): 347if not flask.session.get("username"): 348flask.abort(401) 349notification = UserNotification.query.filter_by(id=notification_id).first() 350if notification.user_username != flask.session.get("username"): 351flask.abort(403) 352notification.mark_unread() 353db.session.commit() 354return flask.render_template_string( 355"<button hx-post='/notifications/{{ notification.id }}/read' hx-swap='outerHTML'>Mark as read</button>", 356notification=notification), 200 357 358 359@app.route("/notifications/mark-all-read", methods=["POST"]) 360def mark_all_read(): 361if not flask.session.get("username"): 362flask.abort(401) 363 364notifications = UserNotification.query.filter_by( 365user_username=flask.session.get("username")) 366for notification in notifications: 367notification.mark_read() 368db.session.commit() 369return flask.redirect("/notifications/", code=303) 370 371 372@app.route("/accounts/", methods=["GET", "POST"]) 373def login(): 374if flask.request.method == "GET": 375return flask.render_template("login.html") 376else: 377if "login" in flask.request.form: 378username = flask.request.form["username"] 379password = flask.request.form["password"] 380 381user = User.query.filter_by(username=username).first() 382 383if user and bcrypt.check_password_hash(user.password_hashed, password): 384flask.session["username"] = user.username 385flask.flash( 386Markup("<iconify-icon icon='mdi:account'></iconify-icon>" + _( 387"Successfully logged in as {username}").format( 388username=username)), 389category="success") 390return flask.redirect("/", code=303) 391elif not user: 392flask.flash(Markup( 393"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _( 394"User not found")), 395category="alert") 396return flask.render_template("login.html") 397else: 398flask.flash(Markup( 399"<iconify-icon icon='mdi:account-question'></iconify-icon>" + _( 400"Invalid password")), 401category="error") 402return flask.render_template("login.html") 403if "signup" in flask.request.form: 404username = flask.request.form["username"] 405password = flask.request.form["password"] 406password2 = flask.request.form["password2"] 407email = flask.request.form.get("email") 408email2 = flask.request.form.get("email2") # repeat email is a honeypot 409name = flask.request.form.get("name") 410 411if not only_chars(username, 412"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"): 413flask.flash(Markup( 414_("Usernames may only contain Latin alphabet, numbers and '-'")), 415category="error") 416return flask.render_template("login.html") 417if "--" in username: 418flask.flash(Markup( 419_("Usernames may not contain consecutive hyphens")), 420category="error") 421return flask.render_template("login.html") 422if username.startswith("-") or username.endswith("-"): 423flask.flash(Markup( 424_("Usernames may not start or end with a hyphen")), 425category="error") 426return flask.render_template("login.html") 427if username in config.RESERVED_NAMES: 428flask.flash( 429Markup( 430_("Sorry, {username} is a system path").format( 431username=username)), 432category="error") 433return flask.render_template("login.html") 434 435if not username.islower(): 436if not name: # infer display name from the wanted username if not customised 437display_name = username 438username = username.lower() 439flask.flash(Markup( 440_("Usernames must be lowercase, so it's been converted automatically")), 441category="info") 442 443user_check = User.query.filter_by(username=username).first() 444if user_check or email2: # make the honeypot look like a normal error 445flask.flash( 446Markup( 447_( 448"The username {username} is taken").format( 449username=username)), 450category="error") 451return flask.render_template("login.html") 452 453if password2 != password: 454flask.flash(Markup(_( 455"Make sure the passwords match")), 456category="error") 457return flask.render_template("login.html") 458 459user = User(username, password, email, name) 460db.session.add(user) 461db.session.commit() 462flask.session["username"] = user.username 463flask.flash(Markup( 464_( 465"Successfully created and logged in as {username}").format( 466username=username)), 467category="success") 468 469notification = Notification({"type": "welcome"}) 470db.session.add(notification) 471db.session.commit() 472 473return flask.redirect("/", code=303) 474 475 476@app.route("/newrepo/", methods=["GET", "POST"]) 477def new_repo(): 478if not flask.session.get("username"): 479flask.abort(401) 480if flask.request.method == "GET": 481return flask.render_template("new-repo.html") 482else: 483name = flask.request.form["name"] 484visibility = int(flask.request.form["visibility"]) 485 486if not only_chars(name, 487"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 488flask.flash(Markup( 489"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 490"Repository names may only contain Latin alphabet, numbers, '-' and '_'")), 491category="error") 492return flask.render_template("new-repo.html") 493 494user = User.query.filter_by(username=flask.session.get("username")).first() 495 496repo = Repo(user, name, visibility) 497db.session.add(repo) 498db.session.commit() 499 500flask.flash(Markup(_("Successfully created repository {name}").format(name=name)), 501category="success") 502return flask.redirect(repo.route, code=303) 503 504 505@app.route("/logout") 506def logout(): 507flask.session.clear() 508# print("Logged out") 509flask.flash(Markup( 510"<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")), 511category="info") 512return flask.redirect("/", code=303) 513 514 515@app.route("/<username>/", methods=["GET", "POST"]) 516def user_profile(username): 517if db.session.get(User, username) is None: 518flask.abort(404) 519old_relationship = UserFollow.query.filter_by( 520follower_username=flask.session.get("username"), 521followed_username=username).first() 522if flask.request.method == "GET": 523user = User.query.filter_by(username=username).first() 524match flask.request.args.get("action"): 525case "repositories": 526repos = Repo.query.filter_by(owner_name=username, visibility=2) 527return flask.render_template("user-profile-repositories.html", user=user, 528repos=repos, 529relationship=old_relationship) 530case "followers": 531return flask.render_template("user-profile-followers.html", user=user, 532relationship=old_relationship) 533case "follows": 534return flask.render_template("user-profile-follows.html", user=user, 535relationship=old_relationship) 536case _: 537return flask.render_template("user-profile-overview.html", user=user, 538relationship=old_relationship) 539 540elif flask.request.method == "POST": 541match flask.request.args.get("action"): 542case "follow": 543if username == flask.session.get("username"): 544flask.abort(403) 545if old_relationship: 546db.session.delete(old_relationship) 547else: 548relationship = UserFollow( 549flask.session.get("username"), 550username 551) 552db.session.add(relationship) 553db.session.commit() 554 555user = db.session.get(User, username) 556author = db.session.get(User, flask.session.get("username")) 557notification = Notification({"type": "update", "version": "0.0.0"}) 558db.session.add(notification) 559db.session.commit() 560 561db.session.commit() 562return flask.redirect("?", code=303) 563 564 565@app.route("/<username>/<repository>/") 566def repository_index(username, repository): 567return flask.redirect("./tree", code=302) 568 569 570@app.route("/info/<username>/avatar") 571def user_avatar(username): 572server_userdata_location = os.path.join(config.USERDATA_PATH, username) 573if not os.path.exists(server_userdata_location): 574return flask.render_template("errors/not-found.html"), 404 575 576return flask.send_from_directory(server_userdata_location, "avatar.png") 577 578 579@app.route("/info/<username>/avatar", methods=["POST"]) 580def user_avatar_upload(username): 581server_userdata_location = os.path.join(config.USERDATA_PATH, username) 582 583if not os.path.exists(server_userdata_location): 584flask.abort(404) 585if not flask.session.get("username") == username: 586flask.abort(403) 587 588# Convert image to PNG 589try: 590image = Image.open(flask.request.files["avatar"]) 591except PIL.UnidentifiedImageError: 592flask.abort(400) 593image.save(os.path.join(server_userdata_location, "avatar.png")) 594 595return flask.redirect(f"/{username}", code=303) 596 597 598@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 599def repository_raw(username, repository, branch, subpath): 600server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 601if not os.path.exists(server_repo_location): 602app.logger.error(f"Cannot load {server_repo_location}") 603flask.abort(404) 604if not (get_visibility(username, repository) or get_permission_level( 605flask.session.get("username"), username, 606repository) is not None): 607flask.abort(403) 608 609app.logger.info(f"Loading {server_repo_location}") 610 611if not os.path.exists(server_repo_location): 612app.logger.error(f"Cannot load {server_repo_location}") 613return flask.render_template("errors/not-found.html"), 404 614 615repo = git.Repo(server_repo_location) 616repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 617if not repo_data.default_branch: 618if repo.heads: 619repo_data.default_branch = repo.heads[0].name 620else: 621return flask.render_template("empty.html", 622remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 623if not branch: 624branch = repo_data.default_branch 625return flask.redirect(f"./{branch}", code=302) 626 627if branch.startswith("tag:"): 628ref = f"tags/{branch[4:]}" 629elif branch.startswith("~"): 630ref = branch[1:] 631else: 632ref = f"heads/{branch}" 633 634ref = ref.replace("~", "/") # encode slashes for URL support 635 636try: 637repo.git.checkout("-f", ref) 638except git.exc.GitCommandError: 639return flask.render_template("errors/not-found.html"), 404 640 641return flask.send_from_directory(config.REPOS_PATH, 642os.path.join(username, repository, subpath)) 643 644 645@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 646@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 647@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 648def repository_tree(username, repository, branch, subpath): 649server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 650if not os.path.exists(server_repo_location): 651app.logger.error(f"Cannot load {server_repo_location}") 652flask.abort(404) 653if not (get_visibility(username, repository) or get_permission_level( 654flask.session.get("username"), username, 655repository) is not None): 656flask.abort(403) 657 658app.logger.info(f"Loading {server_repo_location}") 659 660repo = git.Repo(server_repo_location) 661repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 662if not repo_data.default_branch: 663if repo.heads: 664repo_data.default_branch = repo.heads[0].name 665else: 666return flask.render_template("empty.html", 667remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 668if not branch: 669branch = repo_data.default_branch 670return flask.redirect(f"./{branch}", code=302) 671 672if branch.startswith("tag:"): 673ref = f"tags/{branch[4:]}" 674elif branch.startswith("~"): 675ref = branch[1:] 676else: 677ref = f"heads/{branch}" 678 679ref = ref.replace("~", "/") # encode slashes for URL support 680 681try: 682repo.git.checkout("-f", ref) 683except git.exc.GitCommandError: 684return flask.render_template("errors/not-found.html"), 404 685 686branches = repo.heads 687 688all_refs = [] 689for ref in repo.heads: 690all_refs.append((ref, "head")) 691for ref in repo.tags: 692all_refs.append((ref, "tag")) 693 694if os.path.isdir(os.path.join(server_repo_location, subpath)): 695files = [] 696blobs = [] 697 698for entry in os.listdir(os.path.join(server_repo_location, subpath)): 699if not os.path.basename(entry) == ".git": 700files.append(os.path.join(subpath, entry)) 701 702infos = [] 703 704for file in files: 705path = os.path.join(server_repo_location, file) 706mimetype = guess_mime(path) 707 708text = git_command(server_repo_location, None, "log", "--format='%H\n'", 709shlex.quote(file)).decode() 710 711sha = text.split("\n")[0] 712identifier = f"/{username}/{repository}/{sha}" 713 714last_commit = db.session.get(Commit, identifier) 715 716info = { 717"name": os.path.basename(file), 718"serverPath": path, 719"relativePath": file, 720"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 721"size": human_size(os.path.getsize(path)), 722"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 723"commit": last_commit, 724"shaSize": 7, 725} 726 727special_icon = config.match_icon(os.path.basename(file)) 728if special_icon: 729info["icon"] = special_icon 730elif os.path.isdir(path): 731info["icon"] = config.folder_icon 732elif mimetypes.guess_type(path)[0] in config.file_icons: 733info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]] 734else: 735info["icon"] = config.unknown_icon 736 737if os.path.isdir(path): 738infos.insert(0, info) 739else: 740infos.append(info) 741 742return flask.render_template( 743"repo-tree.html", 744username=username, 745repository=repository, 746files=infos, 747subpath=os.path.join("/", subpath), 748branches=all_refs, 749current=branch, 750remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 751is_favourite=get_favourite(flask.session.get("username"), username, repository), 752repo_data=repo_data, 753) 754else: 755path = os.path.join(server_repo_location, subpath) 756 757if not os.path.exists(path): 758return flask.render_template("errors/not-found.html"), 404 759 760mimetype = guess_mime(path) 761mode = mimetype.split("/", 1)[0] 762size = human_size(os.path.getsize(path)) 763 764special_icon = config.match_icon(os.path.basename(path)) 765if special_icon: 766icon = special_icon 767elif os.path.isdir(path): 768icon = config.folder_icon 769elif mimetypes.guess_type(path)[0] in config.file_icons: 770icon = config.file_icons[mimetypes.guess_type(path)[0]] 771else: 772icon = config.unknown_icon 773 774contents = None 775if mode == "text": 776contents = convert_to_html(path) 777 778return flask.render_template( 779"repo-file.html", 780username=username, 781repository=repository, 782file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 783branches=all_refs, 784current=branch, 785mode=mode, 786mimetype=mimetype, 787detailedtype=magic.from_file(path), 788size=size, 789icon=icon, 790subpath=os.path.join("/", subpath), 791extension=pathlib.Path(path).suffix, 792basename=os.path.basename(path), 793contents=contents, 794remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 795is_favourite=get_favourite(flask.session.get("username"), username, repository), 796repo_data=repo_data, 797) 798 799 800@repositories.route("/<username>/<repository>/commit/<sha>") 801def repository_commit(username, repository, sha): 802server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 803if not os.path.exists(server_repo_location): 804app.logger.error(f"Cannot load {server_repo_location}") 805flask.abort(404) 806if not (get_visibility(username, repository) or get_permission_level( 807flask.session.get("username"), username, 808repository) is not None): 809flask.abort(403) 810 811app.logger.info(f"Loading {server_repo_location}") 812 813if not os.path.exists(server_repo_location): 814app.logger.error(f"Cannot load {server_repo_location}") 815return flask.render_template("errors/not-found.html"), 404 816 817repo = git.Repo(server_repo_location) 818repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 819 820files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r", 821"--name-only", "--no-commit-id", sha).decode().split("\n")[:-1] 822 823# print(files) 824 825return flask.render_template( 826"repo-commit.html", 827username=username, 828repository=repository, 829remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 830is_favourite=get_favourite(flask.session.get("username"), username, repository), 831diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff", 832str(sha) + "^!", "--", file).decode().split("\n") for 833file in files}, 834data=db.session.get(Commit, f"/{username}/{repository}/{sha}"), 835repo_data=repo_data, 836comment_query=Comment.query, 837permission_level=get_permission_level(flask.session.get("username"), username, repository), 838) 839 840 841@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"]) 842def repository_commit_add_comment(username, repository, sha): 843server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 844if not os.path.exists(server_repo_location): 845app.logger.error(f"Cannot load {server_repo_location}") 846flask.abort(404) 847if not (get_visibility(username, repository) or get_permission_level( 848flask.session.get("username"), username, 849repository) is not None): 850flask.abort(403) 851 852comment = Comment( 853db.session.get(User, flask.session.get("username")), 854db.session.get(Repo, f"/{username}/{repository}"), 855db.session.get(Commit, f"/{username}/{repository}/{sha}"), 856flask.request.form["comment"], 857flask.request.form["file"], 858flask.request.form["line"], 859) 860 861db.session.add(comment) 862db.session.commit() 863 864return flask.redirect( 865flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 866code=303 867) 868 869 870@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"]) 871def repository_commit_delete_comment(username, repository, sha, id): 872repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 873# print(f"/{username}/{repository}/{flask.request.form.get('id')}") 874comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 875commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first() 876if ( 877comment.owner.username == flask.session.get("username") 878or get_permission_level(flask.session.get("username"), username, repository) >= 2 879or comment.commit.owner.username == flask.session.get("username") 880): 881db.session.delete(comment) 882db.session.commit() 883 884return flask.redirect( 885flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 886code=303 887) 888 889 890@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"]) 891def repository_commit_resolve_comment(username, repository, sha, id): 892comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first() 893if ( 894comment.commit.owner.username == flask.session.get("username") 895or get_permission_level(flask.session.get("username"), username, repository) >= 2 896or comment.owner.username == flask.session.get("username") 897): 898comment.state = int(not comment.state) 899db.session.commit() 900 901return flask.redirect( 902flask.url_for(".repository_commit", username=username, repository=repository, sha=sha), 903code=303 904) 905 906 907@repositories.route("/<username>/<repository>/forum/") 908def repository_forum(username, repository): 909server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 910if not os.path.exists(server_repo_location): 911app.logger.error(f"Cannot load {server_repo_location}") 912flask.abort(404) 913if not (get_visibility(username, repository) or get_permission_level( 914flask.session.get("username"), username, 915repository) is not None): 916flask.abort(403) 917 918app.logger.info(f"Loading {server_repo_location}") 919 920if not os.path.exists(server_repo_location): 921app.logger.error(f"Cannot load {server_repo_location}") 922return flask.render_template("errors/not-found.html"), 404 923 924repo = git.Repo(server_repo_location) 925repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 926user = User.query.filter_by(username=flask.session.get("username")).first() 927relationships = RepoAccess.query.filter_by(repo=repo_data) 928user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 929 930return flask.render_template( 931"repo-forum.html", 932username=username, 933repository=repository, 934repo_data=repo_data, 935relationships=relationships, 936repo=repo, 937user_relationship=user_relationship, 938Post=Post, 939remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 940is_favourite=get_favourite(flask.session.get("username"), username, repository), 941default_branch=repo_data.default_branch 942) 943 944 945@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 946def repository_forum_topic(username, repository, id): 947server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 948if not os.path.exists(server_repo_location): 949app.logger.error(f"Cannot load {server_repo_location}") 950flask.abort(404) 951if not (get_visibility(username, repository) or get_permission_level( 952flask.session.get("username"), username, 953repository) is not None): 954flask.abort(403) 955 956app.logger.info(f"Loading {server_repo_location}") 957 958if not os.path.exists(server_repo_location): 959app.logger.error(f"Cannot load {server_repo_location}") 960return flask.render_template("errors/not-found.html"), 404 961 962repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 963user = User.query.filter_by(username=flask.session.get("username")).first() 964relationships = RepoAccess.query.filter_by(repo=repo_data) 965user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 966 967post = Post.query.filter_by(id=id).first() 968 969return flask.render_template( 970"repo-topic.html", 971username=username, 972repository=repository, 973repo_data=repo_data, 974relationships=relationships, 975user_relationship=user_relationship, 976post=post, 977remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 978is_favourite=get_favourite(flask.session.get("username"), username, repository), 979default_branch=repo_data.default_branch 980) 981 982 983@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"]) 984def repository_forum_new(username, repository): 985server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 986if not os.path.exists(server_repo_location): 987app.logger.error(f"Cannot load {server_repo_location}") 988flask.abort(404) 989if not (get_visibility(username, repository) or get_permission_level( 990flask.session.get("username"), username, 991repository) is not None): 992flask.abort(403) 993 994app.logger.info(f"Loading {server_repo_location}") 995 996if not os.path.exists(server_repo_location): 997app.logger.error(f"Cannot load {server_repo_location}") 998return flask.render_template("errors/not-found.html"), 404 999 1000repo = git.Repo(server_repo_location) 1001repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1002user = User.query.filter_by(username=flask.session.get("username")).first() 1003relationships = RepoAccess.query.filter_by(repo=repo_data) 1004user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1005 1006post = Post(user, repo_data, None, flask.request.form["subject"], 1007flask.request.form["message"]) 1008 1009db.session.add(post) 1010db.session.commit() 1011 1012return flask.redirect( 1013flask.url_for(".repository_forum_thread", username=username, repository=repository, 1014post_id=post.number), 1015code=303) 1016 1017 1018@repositories.route("/<username>/<repository>/forum/<int:post_id>") 1019def repository_forum_thread(username, repository, post_id): 1020server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1021if not os.path.exists(server_repo_location): 1022app.logger.error(f"Cannot load {server_repo_location}") 1023flask.abort(404) 1024if not (get_visibility(username, repository) or get_permission_level( 1025flask.session.get("username"), username, 1026repository) is not None): 1027flask.abort(403) 1028 1029app.logger.info(f"Loading {server_repo_location}") 1030 1031if not os.path.exists(server_repo_location): 1032app.logger.error(f"Cannot load {server_repo_location}") 1033return flask.render_template("errors/not-found.html"), 404 1034 1035repo = git.Repo(server_repo_location) 1036repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1037user = User.query.filter_by(username=flask.session.get("username")).first() 1038relationships = RepoAccess.query.filter_by(repo=repo_data) 1039user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1040 1041if user: 1042max_post_nesting = user.max_post_nesting 1043else: 1044max_post_nesting = 2 1045 1046return flask.render_template( 1047"repo-forum-thread.html", 1048username=username, 1049repository=repository, 1050repo_data=repo_data, 1051relationships=relationships, 1052repo=repo, 1053Post=Post, 1054user_relationship=user_relationship, 1055post_id=post_id, 1056max_post_nesting=max_post_nesting, 1057remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1058is_favourite=get_favourite(flask.session.get("username"), username, repository), 1059parent=Post.query.filter_by(repo=repo_data, number=post_id).first(), 1060has_permission=not ((not get_permission_level(flask.session.get("username"), username, 1061repository)) and db.session.get(Post, 1062f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")), 1063) 1064 1065 1066@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state", 1067methods=["POST"]) 1068def repository_forum_change_state(username, repository, post_id): 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_permission_level(flask.session.get("username"), username, repository)) and db.session.get(Post, f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username"): 1074flask.abort(403) 1075 1076app.logger.info(f"Loading {server_repo_location}") 1077 1078repo = git.Repo(server_repo_location) 1079repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1080user = User.query.filter_by(username=flask.session.get("username")).first() 1081relationships = RepoAccess.query.filter_by(repo=repo_data) 1082user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1083 1084post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1085 1086if not post: 1087flask.abort(404) 1088 1089post.state = int(flask.request.form["new-state"]) 1090 1091db.session.commit() 1092 1093return flask.redirect( 1094flask.url_for(".repository_forum_thread", username=username, repository=repository, 1095post_id=post_id), 1096code=303) 1097 1098 1099@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 1100def repository_forum_reply(username, repository, post_id): 1101server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1102if not os.path.exists(server_repo_location): 1103app.logger.error(f"Cannot load {server_repo_location}") 1104flask.abort(404) 1105if not (get_visibility(username, repository) or get_permission_level( 1106flask.session.get("username"), username, 1107repository) is not None): 1108flask.abort(403) 1109 1110app.logger.info(f"Loading {server_repo_location}") 1111 1112if not os.path.exists(server_repo_location): 1113app.logger.error(f"Cannot load {server_repo_location}") 1114return flask.render_template("errors/not-found.html"), 404 1115 1116repo = git.Repo(server_repo_location) 1117repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1118user = User.query.filter_by(username=flask.session.get("username")).first() 1119relationships = RepoAccess.query.filter_by(repo=repo_data) 1120user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1121if not user: 1122flask.abort(401) 1123 1124parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1125post = Post(user, repo_data, parent, flask.request.form["subject"], 1126flask.request.form["message"]) 1127 1128db.session.add(post) 1129post.update_date() 1130db.session.commit() 1131 1132return flask.redirect( 1133flask.url_for(".repository_forum_thread", username=username, repository=repository, 1134post_id=post_id), 1135code=303) 1136 1137 1138@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", 1139defaults={"score": 1}) 1140@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", 1141defaults={"score": -1}) 1142@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 1143def repository_forum_vote(username, repository, post_id, score): 1144server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1145if not os.path.exists(server_repo_location): 1146app.logger.error(f"Cannot load {server_repo_location}") 1147flask.abort(404) 1148if not (get_visibility(username, repository) or get_permission_level( 1149flask.session.get("username"), username, 1150repository) is not None): 1151flask.abort(403) 1152 1153app.logger.info(f"Loading {server_repo_location}") 1154 1155if not os.path.exists(server_repo_location): 1156app.logger.error(f"Cannot load {server_repo_location}") 1157return flask.render_template("errors/not-found.html"), 404 1158 1159repo = git.Repo(server_repo_location) 1160repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1161user = User.query.filter_by(username=flask.session.get("username")).first() 1162relationships = RepoAccess.query.filter_by(repo=repo_data) 1163user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1164if not user: 1165flask.abort(401) 1166 1167post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 1168 1169if score: 1170old_relationship = PostVote.query.filter_by(user_username=user.username, 1171post_identifier=post.identifier).first() 1172if old_relationship: 1173if score == old_relationship.vote_score: 1174db.session.delete(old_relationship) 1175post.vote_sum -= old_relationship.vote_score 1176else: 1177post.vote_sum -= old_relationship.vote_score 1178post.vote_sum += score 1179old_relationship.vote_score = score 1180else: 1181relationship = PostVote(user, post, score) 1182post.vote_sum += score 1183db.session.add(relationship) 1184 1185db.session.commit() 1186 1187user_vote = PostVote.query.filter_by(user_username=user.username, 1188post_identifier=post.identifier).first() 1189response = flask.make_response( 1190str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 1191response.content_type = "text/plain" 1192 1193return response 1194 1195 1196@repositories.route("/<username>/<repository>/favourite") 1197def repository_favourite(username, repository): 1198server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1199if not os.path.exists(server_repo_location): 1200app.logger.error(f"Cannot load {server_repo_location}") 1201flask.abort(404) 1202if not (get_visibility(username, repository) or get_permission_level( 1203flask.session.get("username"), username, 1204repository) is not None): 1205flask.abort(403) 1206 1207app.logger.info(f"Loading {server_repo_location}") 1208 1209if not os.path.exists(server_repo_location): 1210app.logger.error(f"Cannot load {server_repo_location}") 1211return flask.render_template("errors/not-found.html"), 404 1212 1213repo = git.Repo(server_repo_location) 1214repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1215user = User.query.filter_by(username=flask.session.get("username")).first() 1216relationships = RepoAccess.query.filter_by(repo=repo_data) 1217user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1218if not user: 1219flask.abort(401) 1220 1221old_relationship = RepoFavourite.query.filter_by(user_username=user.username, 1222repo_route=repo_data.route).first() 1223if old_relationship: 1224db.session.delete(old_relationship) 1225else: 1226relationship = RepoFavourite(user, repo_data) 1227db.session.add(relationship) 1228 1229db.session.commit() 1230 1231return flask.redirect(flask.url_for("favourites"), code=303) 1232 1233 1234@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 1235def repository_users(username, repository): 1236server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1237if not os.path.exists(server_repo_location): 1238app.logger.error(f"Cannot load {server_repo_location}") 1239flask.abort(404) 1240if not (get_visibility(username, repository) or get_permission_level( 1241flask.session.get("username"), username, 1242repository) is not None): 1243flask.abort(403) 1244 1245app.logger.info(f"Loading {server_repo_location}") 1246 1247if not os.path.exists(server_repo_location): 1248app.logger.error(f"Cannot load {server_repo_location}") 1249return flask.render_template("errors/not-found.html"), 404 1250 1251repo = git.Repo(server_repo_location) 1252repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1253user = User.query.filter_by(username=flask.session.get("username")).first() 1254relationships = RepoAccess.query.filter_by(repo=repo_data) 1255user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 1256 1257if flask.request.method == "GET": 1258return flask.render_template( 1259"repo-users.html", 1260username=username, 1261repository=repository, 1262repo_data=repo_data, 1263relationships=relationships, 1264repo=repo, 1265user_relationship=user_relationship, 1266remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1267is_favourite=get_favourite(flask.session.get("username"), username, repository) 1268) 1269else: 1270if get_permission_level(flask.session.get("username"), username, repository) != 2: 1271flask.abort(401) 1272 1273if flask.request.form.get("new-username"): 1274# Create new relationship 1275new_user = User.query.filter_by( 1276username=flask.request.form.get("new-username")).first() 1277relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 1278db.session.add(relationship) 1279db.session.commit() 1280if flask.request.form.get("update-username"): 1281# Create new relationship 1282updated_user = User.query.filter_by( 1283username=flask.request.form.get("update-username")).first() 1284relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 1285if flask.request.form.get("update-level") == -1: 1286relationship.delete() 1287else: 1288relationship.access_level = flask.request.form.get("update-level") 1289db.session.commit() 1290 1291return flask.redirect( 1292app.url_for(".repository_users", username=username, repository=repository)) 1293 1294 1295@repositories.route("/<username>/<repository>/branches/") 1296def repository_branches(username, repository): 1297server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1298if not os.path.exists(server_repo_location): 1299app.logger.error(f"Cannot load {server_repo_location}") 1300flask.abort(404) 1301if not (get_visibility(username, repository) or get_permission_level( 1302flask.session.get("username"), username, 1303repository) is not None): 1304flask.abort(403) 1305 1306app.logger.info(f"Loading {server_repo_location}") 1307 1308if not os.path.exists(server_repo_location): 1309app.logger.error(f"Cannot load {server_repo_location}") 1310return flask.render_template("errors/not-found.html"), 404 1311 1312repo = git.Repo(server_repo_location) 1313repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1314 1315return flask.render_template( 1316"repo-branches.html", 1317username=username, 1318repository=repository, 1319repo_data=repo_data, 1320repo=repo, 1321remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1322is_favourite=get_favourite(flask.session.get("username"), username, repository) 1323) 1324 1325 1326@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 1327@repositories.route("/<username>/<repository>/log/<branch>/") 1328def repository_log(username, repository, branch): 1329server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1330if not os.path.exists(server_repo_location): 1331app.logger.error(f"Cannot load {server_repo_location}") 1332flask.abort(404) 1333if not (get_visibility(username, repository) or get_permission_level( 1334flask.session.get("username"), username, 1335repository) is not None): 1336flask.abort(403) 1337 1338app.logger.info(f"Loading {server_repo_location}") 1339 1340if not os.path.exists(server_repo_location): 1341app.logger.error(f"Cannot load {server_repo_location}") 1342return flask.render_template("errors/not-found.html"), 404 1343 1344repo = git.Repo(server_repo_location) 1345repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1346if not repo_data.default_branch: 1347if repo.heads: 1348repo_data.default_branch = repo.heads[0].name 1349else: 1350return flask.render_template("empty.html", 1351remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1352if not branch: 1353branch = repo_data.default_branch 1354return flask.redirect(f"./{branch}", code=302) 1355 1356if branch.startswith("tag:"): 1357ref = f"tags/{branch[4:]}" 1358elif branch.startswith("~"): 1359ref = branch[1:] 1360else: 1361ref = f"heads/{branch}" 1362 1363ref = ref.replace("~", "/") # encode slashes for URL support 1364 1365try: 1366repo.git.checkout("-f", ref) 1367except git.exc.GitCommandError: 1368return flask.render_template("errors/not-found.html"), 404 1369 1370branches = repo.heads 1371 1372all_refs = [] 1373for ref in repo.heads: 1374all_refs.append((ref, "head")) 1375for ref in repo.tags: 1376all_refs.append((ref, "tag")) 1377 1378commit_list = [f"/{username}/{repository}/{sha}" for sha in 1379git_command(server_repo_location, None, "log", 1380"--format='%H'").decode().split("\n")] 1381 1382commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc()) 1383page_number = flask.request.args.get("page", 1, type=int) 1384if flask.session.get("username"): 1385default_page_length = db.session.get(User, flask.session.get("username")).default_page_length 1386else: 1387default_page_length = 16 1388page_length = flask.request.args.get("per_page", default_page_length, type=int) 1389page_listing = db.paginate(commits, page=page_number, per_page=page_length) 1390 1391if page_listing.has_next: 1392next_page = page_listing.next_num 1393else: 1394next_page = None 1395 1396if page_listing.has_prev: 1397prev_page = page_listing.prev_num 1398else: 1399prev_page = None 1400 1401return flask.render_template( 1402"repo-log.html", 1403username=username, 1404repository=repository, 1405branches=all_refs, 1406current=branch, 1407repo_data=repo_data, 1408repo=repo, 1409commits=page_listing, 1410remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1411is_favourite=get_favourite(flask.session.get("username"), username, repository), 1412page_number=page_number, 1413page_length=page_length, 1414next_page=next_page, 1415prev_page=prev_page, 1416num_pages=page_listing.pages 1417) 1418 1419 1420@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"]) 1421def repository_prs(username, repository): 1422server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1423if not os.path.exists(server_repo_location): 1424app.logger.error(f"Cannot load {server_repo_location}") 1425flask.abort(404) 1426if not (get_visibility(username, repository) or get_permission_level( 1427flask.session.get("username"), username, 1428repository) is not None): 1429flask.abort(403) 1430 1431app.logger.info(f"Loading {server_repo_location}") 1432 1433if not os.path.exists(server_repo_location): 1434app.logger.error(f"Cannot load {server_repo_location}") 1435return flask.render_template("errors/not-found.html"), 404 1436 1437if flask.request.method == "GET": 1438repo = git.Repo(server_repo_location) 1439repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1440user = User.query.filter_by(username=flask.session.get("username")).first() 1441 1442return flask.render_template( 1443"repo-prs.html", 1444username=username, 1445repository=repository, 1446repo_data=repo_data, 1447repo=repo, 1448PullRequest=PullRequest, 1449remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1450is_favourite=get_favourite(flask.session.get("username"), username, repository), 1451default_branch=repo_data.default_branch, 1452branches=repo.branches 1453) 1454 1455else: 1456repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1457head = flask.request.form.get("head") 1458head_route = flask.request.form.get("headroute") 1459base = flask.request.form.get("base") 1460 1461if not head and base and head_route: 1462return flask.redirect(".", 400) 1463 1464head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/"))) 1465base_repo = git.Repo(server_repo_location) 1466# print(head_repo) 1467 1468if head not in head_repo.branches or base not in base_repo.branches: 1469flask.flash(Markup( 1470"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")), 1471category="error") 1472return flask.redirect(".", 303) 1473 1474head_data = db.session.get(Repo, head_route) 1475if not head_data.visibility: 1476flask.flash(Markup( 1477"<iconify-icon icon='mdi:error'></iconify-icon>" + _( 1478"Head can't be restricted")), 1479category="error") 1480return flask.redirect(".", 303) 1481 1482pull_request = PullRequest(head_data, head, repo_data, base, 1483db.session.get(User, flask.session["username"])) 1484 1485db.session.add(pull_request) 1486db.session.commit() 1487 1488# Create the notification 1489notification = Notification({"type": "pr", "head": pull_request.head.route, "base": pull_request.base.route, "pr": pull_request.id}) 1490db.session.add(notification) 1491db.session.commit() 1492 1493# Send a notification to all users who have enabled PR notifications for this repo 1494for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all(): 1495user = relationship.user 1496user_notification = UserNotification(user, notification, 1) 1497db.session.add(user_notification) 1498db.session.commit() 1499celery_tasks.send_notification.apply_async(args=[user_notification.id]) 1500 1501return flask.redirect(".", 303) 1502 1503 1504@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"]) 1505def repository_prs_merge(username, repository): 1506server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1507if not os.path.exists(server_repo_location): 1508app.logger.error(f"Cannot load {server_repo_location}") 1509flask.abort(404) 1510if not (get_visibility(username, repository) or get_permission_level( 1511flask.session.get("username"), username, 1512repository) is not None): 1513flask.abort(403) 1514 1515if not get_permission_level(flask.session.get("username"), username, repository): 1516flask.abort(401) 1517 1518repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1519repo = git.Repo(server_repo_location) 1520id = flask.request.form.get("id") 1521 1522pull_request = db.session.get(PullRequest, id) 1523 1524if pull_request: 1525result = celery_tasks.merge_heads.delay( 1526pull_request.head_route, 1527pull_request.head_branch, 1528pull_request.base_route, 1529pull_request.base_branch, 1530simulate=True 1531) 1532task_result = worker.AsyncResult(result.id) 1533 1534return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) # should be 202 Accepted but we must use a redirect 1535# db.session.delete(pull_request) 1536# db.session.commit() 1537else: 1538flask.abort(400) 1539 1540 1541@repositories.route("/<username>/<repository>/prs/<int:id>/merge") 1542def repository_prs_merge_stage_two(username, repository, id): 1543server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1544if not os.path.exists(server_repo_location): 1545app.logger.error(f"Cannot load {server_repo_location}") 1546flask.abort(404) 1547if not (get_visibility(username, repository) or get_permission_level( 1548flask.session.get("username"), username, 1549repository) is not None): 1550flask.abort(403) 1551 1552if not get_permission_level(flask.session.get("username"), username, repository): 1553flask.abort(401) 1554 1555repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1556repo = git.Repo(server_repo_location) 1557 1558pull_request = db.session.get(PullRequest, id) 1559 1560if pull_request: 1561result = celery_tasks.merge_heads.delay( 1562pull_request.head_route, 1563pull_request.head_branch, 1564pull_request.base_route, 1565pull_request.base_branch, 1566simulate=False 1567) 1568task_result = worker.AsyncResult(result.id) 1569 1570pull_request.state = 1 1571db.session.commit() 1572 1573return flask.redirect(f"/task/{result.id}?pr-id={id}", 303) 1574# db.session.delete(pull_request) 1575else: 1576flask.abort(400) 1577 1578 1579@app.route("/task/<task_id>") 1580def task_monitor(task_id): 1581task_result = worker.AsyncResult(task_id) 1582 1583if flask.request.args.get("partial"): 1584# htmx partial update 1585return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000) 1586 1587# Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long 1588return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125) 1589 1590 1591@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"]) 1592def repository_prs_delete(username, repository): 1593server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 1594if not os.path.exists(server_repo_location): 1595app.logger.error(f"Cannot load {server_repo_location}") 1596flask.abort(404) 1597if not (get_visibility(username, repository) or get_permission_level( 1598flask.session.get("username"), username, 1599repository) is not None): 1600flask.abort(403) 1601 1602if not get_permission_level(flask.session.get("username"), username, repository): 1603flask.abort(401) 1604 1605repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 1606repo = git.Repo(server_repo_location) 1607id = flask.request.form.get("id") 1608 1609pull_request = db.session.get(PullRequest, id) 1610 1611if pull_request: 1612pull_request.state = 2 1613db.session.commit() 1614 1615return flask.redirect(".", 303) 1616 1617 1618@repositories.route("/<username>/<repository>/settings/") 1619def repository_settings(username, repository): 1620if get_permission_level(flask.session.get("username"), username, repository) != 2: 1621flask.abort(401) 1622 1623repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository)) 1624 1625site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>") 1626primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>") 1627 1628return flask.render_template("repo-settings.html", username=username, repository=repository, 1629repo_data=db.session.get(Repo, f"/{username}/{repository}"), 1630branches=[branch.name for branch in repo.branches], 1631site_link=site_link, primary_site_link=primary_site_link, 1632remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1633is_favourite=get_favourite(flask.session.get("username"), username, repository), 1634) 1635 1636 1637@repositories.route("/<username>/<repository>/settings/", methods=["POST"]) 1638def repository_settings_post(username, repository): 1639if get_permission_level(flask.session.get("username"), username, repository) != 2: 1640flask.abort(401) 1641 1642repo = db.session.get(Repo, f"/{username}/{repository}") 1643 1644repo.visibility = flask.request.form.get("visibility", type=int) 1645repo.info = flask.request.form.get("description") 1646repo.default_branch = flask.request.form.get("default_branch") 1647repo.url = flask.request.form.get("url") 1648 1649# Update site settings 1650had_site = repo.has_site 1651old_branch = repo.site_branch 1652if flask.request.form.get("site_branch"): 1653repo.site_branch = flask.request.form.get("site_branch") 1654if flask.request.form.get("primary_site"): 1655if had_site != 2: 1656# Remove primary site from other repos 1657for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2): 1658other_repo.has_site = 1 # switch it to a regular site 1659flask.flash(Markup( 1660_("Your repository {repository} has been demoted from a primary site to a regular site because there can only be one primary site per user.").format( 1661repository=other_repo.route 1662)), category="warning") 1663repo.has_site = 2 1664else: 1665repo.has_site = 1 1666else: 1667repo.site_branch = None 1668repo.has_site = 0 1669 1670db.session.commit() 1671 1672if not (had_site, old_branch) == (repo.has_site, repo.site_branch): 1673# Deploy the newly activated site 1674result = celery_tasks.copy_site.delay(repo.route) 1675 1676if had_site and not repo.has_site: 1677# Remove the site 1678result = celery_tasks.delete_site.delay(repo.route) 1679 1680if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site): 1681# Deploy all other sites which were destroyed by the primary site 1682for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1): 1683result = celery_tasks.copy_site.delay(other_repo.route) 1684 1685return flask.redirect(f"/{username}/{repository}/settings", 303) 1686 1687 1688@app.errorhandler(404) 1689def e404(error): 1690return flask.render_template("errors/not-found.html"), 404 1691 1692 1693@app.errorhandler(401) 1694def e401(error): 1695return flask.render_template("errors/unauthorised.html"), 401 1696 1697 1698@app.errorhandler(403) 1699def e403(error): 1700return flask.render_template("errors/forbidden.html"), 403 1701 1702 1703@app.errorhandler(418) 1704def e418(error): 1705return flask.render_template("errors/teapot.html"), 418 1706 1707 1708@app.errorhandler(405) 1709def e405(error): 1710return flask.render_template("errors/method-not-allowed.html"), 405 1711 1712 1713@app.errorhandler(500) 1714def e500(error): 1715return flask.render_template("errors/server-error.html"), 500 1716 1717 1718@app.errorhandler(400) 1719def e400(error): 1720return flask.render_template("errors/bad-request.html"), 400 1721 1722 1723@app.errorhandler(410) 1724def e410(error): 1725return flask.render_template("errors/gone.html"), 410 1726 1727 1728@app.errorhandler(415) 1729def e415(error): 1730return flask.render_template("errors/media-type.html"), 415 1731 1732 1733if __name__ == "__main__": 1734app.run(debug=True, port=8080, host="0.0.0.0") 1735 1736app.register_blueprint(repositories) 1737