roundabout,
created on Saturday, 2 December 2023, 07:50:08 (1701503408),
received on Wednesday, 31 July 2024, 06:54:38 (1722408878)
Author identity: vlad <vlad.muntoiu@gmail.com>
2b125b919d87db572a6dd3c3ad614963112f7807
app.py
@@ -1,6 +1,7 @@
import os
import random
import subprocess
from functools import wraps
import cairosvg
import flask
@@ -33,6 +34,23 @@ bcrypt = Bcrypt(app)
migrate = Migrate(app, db)
def gitCommand(repo, data, *args):
if not os.path.isdir(repo):
raise FileNotFoundError("Repo not found")
env = os.environ.copy()
command = ["git", *args]
proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
print(command)
if data:
proc.stdin.write(data)
out, err = proc.communicate()
return out
def onlyChars(string, chars):
for i in string:
if i not in chars:
@@ -72,6 +90,7 @@ with app.app_context():
creationDate = db.Column(db.DateTime, default=datetime.utcnow)
repositories = db.relationship("Repo", back_populates="owner")
repoAccess = db.relationship("RepoAccess", back_populates="user")
def __init__(self, username, password, email=None, displayName=None):
self.username = username
@@ -87,7 +106,7 @@ with app.app_context():
avatarName = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH))
if os.path.join(config.DEFAULT_AVATARS_PATH, avatarName).endswith(".svg"):
cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName), write_to="/tmp/gitme-avatar.png")
cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName), write_to="/tmp/roundabout-avatar.png")
avatar = Image.open("/tmp/roundabout-avatar.png")
else:
avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatarName))
@@ -117,10 +136,9 @@ with app.app_context():
self.owner = owner
self.visibility = visibility
# Add repo access for the owner
repoAccess = RepoAccess(user=owner, repo=self, accessLevel=2)
# Add the owner as an admin
repoAccess = RepoAccess(owner, self, 2)
db.session.add(repoAccess)
db.session.commit()
class Commit(db.Model):
@@ -135,7 +153,7 @@ with app.app_context():
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.identifier = f"/{owner.username}/{repo.name}/{sha}"
self.sha = sha
self.repoName = repo.route
self.repo = repo
@@ -145,6 +163,28 @@ with app.app_context():
self.message = message
self.ownerIdentity = ownerIdentity
def getPermissionLevel(loggedIn, username, repository):
user = User.query.filter_by(username=loggedIn).first()
repo = Repo.query.filter_by(route=f"/{username}/{repository}").first()
if user and repo:
permission = RepoAccess.query.filter_by(user=user, repo=repo).first()
if permission:
return permission.accessLevel
return None
def getVisibility(username, repository):
repo = Repo.query.filter_by(route=f"/{username}/{repository}").first()
if repo:
return repo.visibility
return None
import gitHTTP
@@ -192,13 +232,28 @@ def about():
return flask.render_template("about.html", platform=platform)
@app.route("/settings/")
@app.route("/settings/", methods=["GET", "POST"])
def settings():
if not flask.session.get("username"):
flask.abort(401)
user = User.query.filter_by(username=flask.session.get("username")).first()
if flask.request.method == "GET":
if not flask.session.get("username"):
flask.abort(401)
user = User.query.filter_by(username=flask.session.get("username")).first()
return flask.render_template("user-settings.html", user=user)
return flask.render_template("user-settings.html", user=user)
else:
user = User.query.filter_by(username=flask.session.get("username")).first()
user.displayName = flask.request.form["displayname"]
user.URL = flask.request.form["url"]
user.company = flask.request.form["company"]
user.companyURL = flask.request.form["companyurl"]
user.location = flask.request.form["location"]
user.showMail = flask.request.form.get("showmail", user.showMail)
db.session.commit()
flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success")
return flask.redirect(f"/{flask.session.get('username')}", code=303)
@app.route("/accounts/", methods=["GET", "POST"])
@@ -357,6 +412,7 @@ def repositoryTree(username, repository, branch, subpath):
return flask.render_template("not-found.html"), 404
branches = repo.heads
if os.path.isdir(os.path.join(serverRepoLocation, subpath)):
files = []
blobs = []
@@ -371,6 +427,12 @@ def repositoryTree(username, repository, branch, subpath):
path = os.path.join(serverRepoLocation, file)
mimetype = guessMIME(path)
text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode()
sha = text.split("\n")[0]
identifier = f"/{username}/{repository}/{sha}"
lastCommit = Commit.query.filter_by(identifier=identifier).first()
info = {
"name": os.path.basename(file),
"serverPath": path,
@@ -378,6 +440,8 @@ def repositoryTree(username, repository, branch, subpath):
"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file),
"size": humanSize(os.path.getsize(path)),
"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}",
"commit": lastCommit,
"shaSize": 7,
}
specialIcon = config.matchIcon(os.path.basename(file))
config.py
@@ -3,7 +3,7 @@ from dotenv import load_dotenv
load_dotenv("secrets.env")
DB_PASSWORD = os.environ.get("DB_PASSWORD")
DB_URI = f"postgresql://root:{DB_PASSWORD}@localhost/gitme"
DB_URI = f"postgresql://root:{DB_PASSWORD}@localhost/roundabout"
REPOS_PATH = "./repos"
USERDATA_PATH = "./userdata"
@@ -14,7 +14,7 @@ AUTH_REALM = "roundabout"
AVATAR_SIZE = (192, 192)
HASHING_ROUNDS = 16
HASHING_ROUNDS = 11
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, Commit, db, bcrypt
from app import app, gitCommand, User, Repo, Commit, RepoAccess, getPermissionLevel, getVisibility, db, bcrypt
import os
import shutil
import config
@@ -26,26 +26,12 @@ def verifyPassword(username, password):
return False
def gitCommand(repo, data, *args):
if not os.path.isdir(repo):
raise FileNotFoundError("Repo not found")
env = os.environ.copy()
command = ["git", *args]
proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
print(command)
if data:
proc.stdin.write(data)
out, err = proc.communicate()
return out
@app.route("/git/<username>/<repository>/git-upload-pack", methods=["POST"])
@auth.login_required
def gitUploadPack(username, repository):
if not (getVisibility(username, repository) or getPermissionLevel(flask.g.user, username, repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository, ".git")
text = gitCommand(serverRepoLocation, flask.request.data, "upload-pack", "--stateless-rpc", ".")
@@ -55,16 +41,18 @@ def gitUploadPack(username, repository):
@app.route("/git/<username>/<repository>/git-receive-pack", methods=["POST"])
@auth.login_required
def gitReceivePack(username, repository):
if not getPermissionLevel(flask.g.user, username, repository):
flask.abort(403)
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()
info = gitCommand(serverRepoLocation, None, "show", "-s", "--format='%H%n%at%n%cn <%ce>%n%B'", sha).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")
if re.match("^[0-9a-fA-F]{40}$", info[:40]):
print(info.split("\n", 3))
sha, time, identity, body = info.split("\n", 3)
login = flask.g.user
user = User.query.filter_by(username=login).first()
@@ -80,13 +68,19 @@ def gitReceivePack(username, repository):
@app.route("/git/<username>/<repository>/info/refs", methods=["GET"])
@auth.login_required
def gitInfoRefs(username, repository):
if not (getVisibility(username, repository) or getPermissionLevel(flask.g.user, username, repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository, ".git")
service = flask.request.args.get("service")
if service.startswith("git"):
service = service[4:]
print(service)
if service == "receive-pack":
print(getPermissionLevel(flask.g.user, username, repository))
if not getPermissionLevel(flask.g.user, username, repository):
flask.abort(403)
serviceLine = f"# service=git-{service}\n"
serviceLine = (f"{len(serviceLine) + 4:04x}" + serviceLine).encode()
post-receive
@@ -1,16 +0,0 @@
#!/bin/bash
while read oldrev newrev refname
do
# Check if the refname starts with "refs/heads/"
if [[ $refname == refs/heads/* ]]; then
# Extract the branch name from refname
branchname="${refname#refs/heads/}"
# Change to the repository's working tree
cd /path/to/your/repo/working/tree
# Update the working tree for the branch
git checkout -f "$branchname"
fi
done
templates/tree-view.html
@@ -37,8 +37,8 @@
<td style="text-align: right;">{{ file.size[0] }} {{ file.size[1] }}</td>
<td>
<x-hbox style="align-items: baseline; gap: 0.5ch;">
<code>DEADBEEF</code>
<span class="commit-message">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</span>
<code>{{ file.commit.sha[:file.shaSize] }}</code>
<span class="commit-message">{{ file.commit.message }}</span>
</x-hbox>
</td>
</tr>
templates/user-profile.html
@@ -10,12 +10,12 @@
<x-frame style="--width: 768px;">
<x-vbox>
<x-hbox style="align-items: center;">
<img src="/info/{{ user.username }}/avatar" class="avatar">
<img src="/info/{{ user.username }}/avatar" class="avatar" style="width: 128px; height: 128px;">
<x-vbox class="nopad" style="flex: 1 0 auto;">
<x-vbox class="nopad">
{% if user.displayName and user.displayName != user.username %}
<hgroup id="username">
<h1 class="headline">{{ user.displayName }}</h1>
<h1>{{ user.displayName }}</h1>
<p>{{ user.username }}</p>
</hgroup>
{% else %}
@@ -35,31 +35,39 @@
{% if user.location %}
<li><x-hbox><iconify-icon icon="mdi:map-marker-radius"></iconify-icon>{{ user.location }}</x-hbox></li>
{% endif %}
{% if user.showMail %}
<li><a href="mailto:{{ user.email }}"><x-hbox><iconify-icon icon="mdi:email"></iconify-icon>{{ user.email }}</x-hbox></a></li>
{% endif %}
</ul>
</x-hbox>
{% if user.bio %}
<x-vbox>
{% if user.bio %}
<article class="card" style="flex: 0 1 auto;">
<section class="card-main">
<p>
{{ user.bio }}
</p>
</section>
</article>
{% endif %}
<h2>Repositories</h2>
<article class="card" style="flex: 0 1 auto;">
<section class="card-main">
<p>
{{ user.bio }}
</p>
<ul style="list-style: none;" class="noindent">
{% for repo in repos %}
<li>
<article>
<a href="{{ repo.route }}"><h3>{{ repo.name }}</h3></a>
{% if repo.info %}
<p>{{ repo.info }}</p>
{% endif %}
</article>
</li>
{% endfor %}
</ul>
</section>
</article>
{% endif %}
<article class="card" style="flex: 0 1 auto;">
<section class="card-main">
<ul style="list-style: none;" class="noindent">
{% for repo in repos %}
<li>
<article>
<a href="{{ repo.route }}"><h3>{{ repo.name }}</h3></a>
<p>{{ repo.info }}</p>
</article>
</li>
{% endfor %}
</ul>
</section>
</article>
</x-vbox>
</x-vbox>
</x-frame>
{% endblock %}
templates/user-settings.html
@@ -36,7 +36,7 @@
<x-hbox style="width: 100%; align-items: space-between;">
<x-vbox style="flex-grow: 1;" class="nopad">
<label for="company">Company or school</label>
<input id="company" name="companyurl" value="{% if user.company %}{{ user.company }}{% endif %}">
<input id="company" name="company" value="{% if user.company %}{{ user.company }}{% endif %}">
</x-vbox>
<x-vbox style="flex-grow: 1;" class="nopad">
<label for="companyurl">Link to the company's website</label>
@@ -45,11 +45,15 @@
</x-hbox>
</x-vbox>
<x-vbox class="nopad">
<label><input name="showmail" value="{% if user.showMail %}{{ user.showMail }}{% endif %}" type="checkbox">Show email on my profile</label>
<label><input name="showmail" value="showmail" {% if user.showMail %}checked{% endif %} type="checkbox">Show email on my profile</label>
</x-vbox>
<x-vbox class="nopad">
<label for="location">Location</label>
<input id="location" name="location" value="{% if user.location %}{{ user.location }}{% endif %}" type="text">
</x-vbox>
<x-vbox class="nopad">
<label for="bio">Bio</label>
<textarea id="bio" rows="4">{% if user.bio %}{{ user.bio }}{% endif %}</textarea>
<textarea id="bio" name="bio" rows="4">{% if user.bio %}{{ user.bio }}{% endif %}</textarea>
</x-vbox>
<button type="submit">Update profile</button>
</x-vbox>