roundabout,
created on Thursday, 30 November 2023, 12:21:53 (1701346913),
received on Wednesday, 31 July 2024, 06:54:38 (1722408878)
Author identity: vlad <vlad.muntoiu@gmail.com>
d680ea8d686c2fb31d2bb161edbd3e9e9a004ca8
app.py
@@ -16,6 +16,7 @@ from enum import Enum
import shutil from PIL import Image from cairosvg import svg2png import platformimport config
@@ -40,6 +41,23 @@ def onlyChars(string, chars):
with app.app_context(): class RepoAccess(db.Model): id = db.Column(db.Integer, primary_key=True) userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) repoRoute = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) accessLevel = db.Column(db.SmallInteger(), nullable=False) # 0 read-only, 1 read-write, 2 admin user = db.relationship("User", back_populates="repoAccess") repo = db.relationship("Repo", back_populates="repoAccess") __table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),) def __init__(self, user, repo, level): self.userUsername = user.username self.repoRoute = repo.route self.accessLevel = levelclass User(db.Model): username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) displayName = db.Column(db.Unicode(128), unique=False, nullable=True)
@@ -53,7 +71,7 @@ with app.app_context():
location = db.Column(db.Unicode(64), nullable=True) creationDate = db.Column(db.DateTime, default=datetime.utcnow) repositories = db.relationship("Repo", backref="owner")repositories = db.relationship("Repo", back_populates="owner")def __init__(self, username, password, email=None, displayName=None): self.username = username
@@ -81,7 +99,7 @@ with app.app_context():
route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True) ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) name = db.Column(db.String(64), nullable=False) # owner = db.relationship("User", back_populates="repositories")owner = db.relationship("User", back_populates="repositories")visibility = db.Column(db.SmallInteger(), nullable=False) info = db.Column(db.Unicode(512), nullable=True) URL = db.Column(db.String(256), nullable=True)
@@ -89,7 +107,8 @@ with app.app_context():
defaultBranch = db.Column(db.String(64), nullable=True, default="master") commits = db.relationship("Commit", backref="repo")commits = db.relationship("Commit", back_populates="repo") repoAccess = db.relationship("RepoAccess", back_populates="repo")def __init__(self, owner, name, visibility): self.route = f"/{owner.username}/{name}"
@@ -98,6 +117,11 @@ with app.app_context():
self.owner = owner self.visibility = visibility # Add repo access for the owner repoAccess = RepoAccess(user=owner, repo=self, accessLevel=2) db.session.add(repoAccess) db.session.commit() class Commit(db.Model): identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True)
@@ -108,14 +132,17 @@ with app.app_context():
receiveDate = db.Column(db.DateTime, default=datetime.utcnow) authorDate = db.Column(db.DateTime) message = db.Column(db.UnicodeText) repo = db.relationship("Repo", back_populates="commits")def __init__(self, sha, owner, repo, date, message, ownerIdentity): self.identifier = f"/{repo.route}/{owner.username}/{sha}"self.sha = sha self.repoName = repo.route self.repo = repo self.ownerName = owner.username self.owner = owner self.authorDate = db.Column(db.DateTime)self.authorDate = datetime.fromtimestamp(int(date)) self.message = messageself.ownerIdentity = ownerIdentity import gitHTTP
@@ -160,6 +187,11 @@ def main():
return flask.render_template("home.html") @app.route("/about") def about(): return flask.render_template("about.html", platform=platform) @app.route("/settings/") def settings(): if not flask.session.get("username"):
config.py
@@ -15,7 +15,7 @@ AUTH_REALM = "roundabout"
AVATAR_SIZE = (192, 192) HASHING_ROUNDS = 16 RESERVED_NAMES = ("git", "settings", "logout", "accounts", "info",)RESERVED_NAMES = ("git", "settings", "logout", "accounts", "info", "alerts", "notifications", "about",)locking = False
gitHTTP.py
@@ -1,6 +1,6 @@
import uuid from app import app, User, Repo, db, bcryptfrom app import app, User, Repo, Commit, db, bcryptimport os import shutil import config
@@ -9,6 +9,8 @@ import git
import subprocess from flask_httpauth import HTTPBasicAuth import zlib import re import datetimeauth = HTTPBasicAuth(realm=config.AUTH_REALM)
@@ -30,8 +32,7 @@ def gitCommand(repo, data, *args):
env = os.environ.copy() command = ["git", *args] print("RUNNING GIT COMMAND")print(" ".join(command))proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE) print(command)
@@ -57,6 +58,22 @@ def gitReceivePack(username, repository):
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository, ".git") text = gitCommand(serverRepoLocation, flask.request.data, "receive-pack", "--stateless-rpc", ".") sha = flask.request.data.split(b" ", 2)[1].decode() info = gitCommand(serverRepoLocation, None, "show", "-s", "--format='%H%n%at%n%cn <%ce>%n%B'").decode() if re.match("[0-9a-fA-F]", info[:40]): print(info.split("\n", 4)) sha, time, identity, *body = info.split("\n", 4) body = "\n".join("body") login = flask.g.user user = User.query.filter_by(username=login).first() repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() commit = Commit(sha, user, repo, time, body, identity) db.session.add(commit) db.session.commit() return flask.Response(text, content_type="application/x-git-receive-pack-result")
@@ -78,6 +95,8 @@ def gitInfoRefs(username, repository):
text = serviceLine + b"0000" + gitCommand(serverRepoLocation, None, "upload-pack", "--stateless-rpc", "--advertise-refs", "--http-backend-info-refs", ".") elif service == "receive-pack": text = serviceLine + b"0000" + gitCommand(serverRepoLocation, None, "receive-pack", "--http-backend-info-refs", ".") else: flask.abort(403)response = flask.Response(text, content_type=f"application/x-git-{service}-advertisement") response.headers["Cache-Control"] = "no-cache"
static/efficient-ui/THEME.css
@@ -56,7 +56,7 @@
normal /* variant */ 700 /* weight */ normal /* stretch */ 15px/1 /* size / line height */13.2px/1 /* size / line height */"Roboto Mono", "Noto Sans", system-ui, apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, "Arial", sans-serif /* family */ ; --gui-font: /* everything not covered by something else */
@@ -401,11 +401,11 @@
--border-badge: none; --shadow-badge: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 3px 6px 0 rgba(0, 0, 0, 0.24), 0 1px 4px 0 rgba(0, 0, 0, 0.12); --x-badge: 100%; --y-badge: 0;--width-badge: 3ch;--height-badge: 3ch;--y-badge: 25%; --width-badge: 2.25ch; --height-badge: 2.25ch;--radius-badge: 3ch; --padding-badge: 0.5ch;--padding-badge: 0.25ch;--color-badge: var(--color-accent); --color-badge-text: var(--color-accent-text); /* AVATARS */
static/style.css
@@ -107,4 +107,12 @@ button, input, .button, select {
#username p { font-size: 1.5em; font-weight: 300; } .box-center { align-items: center; } #global-nav > x-hbox > a > x-hbox { height: 100%;}
templates/about.html
@@ -0,0 +1,142 @@
{% extends "default.html" %} {% block title %} About {% endblock %} {% block breadcrumbs %} <li><a href="/about">Information</a></li> {% endblock %} {% block content %} <style> main { height: 100%; flex: 1 0 auto; } #about-page { background-image: radial-gradient(circle, #2196F3 0%, #1976D2 50%, #0D47A1 100%); height: 100vh; width: 100vw; top: 0; position: fixed; color: #ffffff; padding: 96px; align-items: center; justify-content: center; text-align: center; } body { display: flex; flex-direction: column; } #global-nav { position: fixed; z-index: 2; background: #00000040; } #logo { position: absolute; width: 100%; height: 100%; background-image: url("/static/logo.svg"); background-size: contain; z-index: 1; } #logo-shadow { width: clamp(240px, 15vw, 512px); aspect-ratio: 1/1; position: relative; filter: drop-shadow(0 48px 64px #00000040) drop-shadow(0 16px 16px #00000020) drop-shadow(0 0 4px #00000030); } #logo.spinning { animation: spin 625ms cubic-bezier(0.16, 1, 0.3, 1); } @keyframes spin { 0% { transform: rotate(120deg); } 100% { transform: rotate(0deg); } } #infos { position: relative; z-index: 1; } html { scroll-behavior: smooth; } #tagline { font-style: italic; font-size: 1.25em; } </style> <x-vbox id="about-page"> <div id="logo-shadow"> <div id="logo" onmousedown="waitForAnimation(this, 'spinning');"></div> </div> <h1 class="headline"><b>Roundabout</b> alpha testing</h1> <span id="tagline">Repository hosting for everyone.</span> </x-vbox> <x-frame style="--width: 100vw; --height: calc(100vh - 48px);" id="clear"></x-frame> <x-frame style="--width: 768px; --height: auto;" id="infos"> <x-hbox class="box-center" style="justify-content: center;"> <a href="#infos"> <iconify-icon icon="mdi-light:chevron-double-up" style="color: #ffffff; font-size: 48px;"></iconify-icon> </a> </x-hbox> <article class="card"> <section class="card-main"> <h2>Licensing</h2> <p> Roundabout is free software available under the GNU AGPLv3. You are free to use, study, modify and/or distribute both the server and client programs, and to host your own instance. </p> <h5 style="display: inline;">Copyright 2023, Roundabout contributors</h5> <p> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. </p> <p> This program is distributed in the hope that it will be useful, but <b>without any warranty</b>; without even the implied warranty of <b>merchantability</b> or <b>fitness for a particular purpose</b>. See the GNU General Public License for more details. </p> <p> You should have received a copy of the GNU General Public License along with this program. If not, see <a href="https://www.gnu.org/licenses/">the GNU website</a>. </p> <a href="https://www.gnu.org/licenses/agpl-3.0.html">Click to view a copy of the GNU AGPL</a> </section> </article> <article class="card"> <section class="card-main"> <h2>Server information</h2> <ul> <li><b>Operating System:</b> {{ platform.platform() }}</li> </ul> </section> </article> </x-frame> <script> function waitForAnimation(element, className) { element.classList.add(className); const stop = () => { element.classList.remove(className); element.removeEventListener("animationend", stop); }; element.addEventListener("animationend", stop); } </script> {% endblock %}
templates/default.html
@@ -25,10 +25,38 @@
</ul> <x-hbox style="--gap: 2ch;"> {% if loggedInUser %} <a href="/{{ loggedInUser }}">{{ loggedInUser }}</a><a href="/newrepo">Create</a><a href="/logout">Logout</a><a href="/settings">Preferences</a><a href="/newrepo"> <x-hbox style="gap: 0.75ch;" class="box-center"> <iconify-icon icon="mdi:folder-plus"></iconify-icon> Create </x-hbox> </a> <a href="/notifications"> <x-hbox style="gap: 0.75ch;" class="box-center"> <iconify-icon icon="ic:baseline-inbox"></iconify-icon> </x-hbox> </a> <a href="/alerts"> <x-hbox style="gap: 0.75ch;" class="box-center"> <iconify-icon icon="ic:baseline-info"></iconify-icon> </x-hbox> </a> <a href="/{{ loggedInUser }}"> <x-hbox style="gap: 0.75ch;" class="box-center"> <img src="/info/{{ loggedInUser }}/avatar" class="avatar" style="width: 1em; height: 1em;"> {{ loggedInUser }} </x-hbox> </a> <a href="/logout"> <x-hbox style="gap: 0.75ch;" class="box-center"> <iconify-icon icon="mdi:logout" title="Log out"></iconify-icon> </x-hbox> </a> <a href="/settings"> <x-hbox style="gap: 0.75ch;" class="box-center"> <iconify-icon icon="mdi:cog" title="User settings"></iconify-icon> </x-hbox> </a>{% else %} <a href="/accounts">Log in or sign up</a> {% endif %}
templates/repo.html
@@ -21,22 +21,24 @@
<li><a href="/{{ username }}/{{ repository }}/settings">Settings</a></li> </ul> <x-buttonbox> <button><iconify-icon icon="mdi:star-plus"></iconify-icon>star</button><button><iconify-icon icon="mdi:chat-alert"></iconify-icon>follow</button><button><iconify-icon icon="mdi:briefcase-download"></iconify-icon>clone</button><button><iconify-icon icon="mdi:directions-fork"></iconify-icon>fork</button></x-buttonbox>{% if loggedInUser %} <button> <iconify-icon icon="mdi:star-plus"></iconify-icon> star </button> <button> <iconify-icon icon="mdi:chat-alert"></iconify-icon> follow </button> <button> <iconify-icon icon="mdi:briefcase-download"></iconify-icon> clone </button> <button> <iconify-icon icon="mdi:directions-fork"></iconify-icon> fork </button> </x-buttonbox> {% endif %}</nav> {% endblock %}