by roundabout, Thursday, 30 November 2023, 12:21:53 (1701346913), pushed by roundabout, 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 platform
import 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 = level
class 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 = message
self.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, bcrypt
from app import app, User, Repo, Commit, db, bcrypt
import os
import shutil
import config
@@ -9,6 +9,8 @@ import git
import subprocess
from flask_httpauth import HTTPBasicAuth
import zlib
import re
import datetime
auth = 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 %}