roundabout,
created on Thursday, 23 May 2024, 05:50:50 (1716443450),
received on Wednesday, 31 July 2024, 06:54:48 (1722408888)
Author identity: vlad <vlad.muntoiu@gmail.com>
f978bd32c4ae575b402c858e1f48515c6536698e
app.py
@@ -28,6 +28,16 @@ import config
from common import git_command
from flask_babel import Babel, gettext, ngettext, force_locale
import logging
class No304(logging.Filter):
def filter(self, record):
return not record.getMessage().strip().endswith("304 -")
logging.getLogger("werkzeug").addFilter(No304())
_ = gettext
n_ = ngettext
@@ -50,7 +60,8 @@ app.config["MAX_CONTENT_LENGTH"] = config.MAX_PAYLOAD_SIZE
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = config.suggest_https # only send cookies over HTTPS if the server is configured for it
app.config["SESSION_COOKIE_HTTPONLY"] = True # don't allow JS to access the cookie
app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN # don't share across subdomains, since user content is hosted there
if config.restrict_cookie_domain:
app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN # don't share across subdomains, since user content is hosted there
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
@@ -468,6 +479,7 @@ def new_repo():
@app.route("/logout")
def logout():
flask.session.clear()
print("Logged out")
flask.flash(Markup(
"<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")),
category="info")
@@ -795,6 +807,36 @@ def repository_commit(username, repository, sha):
file in files},
data=db.session.get(Commit, f"/{username}/{repository}/{sha}"),
repo_data=repo_data,
comment_query=Comment.query,
)
@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"])
def repository_commit_add_comment(username, repository, sha):
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
if not os.path.exists(server_repo_location):
app.logger.error(f"Cannot load {server_repo_location}")
flask.abort(404)
if not (get_visibility(username, repository) or get_permission_level(
flask.session.get("username"), username,
repository) is not None):
flask.abort(403)
comment = Comment(
db.session.get(User, flask.session.get("username")),
db.session.get(Repo, f"/{username}/{repository}"),
db.session.get(Commit, f"/{username}/{repository}/{sha}"),
flask.request.form["comment"],
flask.request.form["file"],
flask.request.form["line"],
)
db.session.add(comment)
db.session.commit()
return flask.redirect(
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
code=303
)
config.py
@@ -28,6 +28,7 @@ HASHING_ROUNDS: int = 11
RESERVED_NAMES: tuple = ("git", "settings", "logout", "accounts", "info", "notifications", "about", "newrepo", "favourites",)
suggest_https: bool = False # this config is intended for a test server
restrict_cookie_domain: bool = False # ditto
available_locales: list[str] = ["ro_RO", "en_GB"]
git_http.py
@@ -60,7 +60,7 @@ def get_commit_identity(identity, pusher, repo):
if relationship.permission_level == 1:
return user
# If no user has a higher permission level, attribute the commit to the pusher.:(
# If no user has a higher permission level, attribute the commit to the pusher :(
return pusher
markdown.py
@@ -9,7 +9,7 @@ def only_chars(string, chars):
return all_chars.issubset(chars)
inlineRegex = r"""
inline_regex = r"""
(?P<imageFlag>!?) \[ (?P<urlText>[^\[\]]*) \] \((?P<urlDestination>[^\(\)]*)\) # hyperlink or media
|
(?P<em>\*{1,7}) (?P<textEm>(?:\\\*|[^*])*) (?P=em) # emphasis with * not requiring space on either side
@@ -261,7 +261,7 @@ def parse_line(source):
hard_break = False
tokens = []
pattern = re.compile(inlineRegex, re.MULTILINE | re.DOTALL | re.VERBOSE)
pattern = re.compile(inline_regex, re.MULTILINE | re.DOTALL | re.VERBOSE)
matches = pattern.finditer(source)
lookup = 0
models.py
@@ -11,6 +11,7 @@ __all__ = [
"Commit",
"PullRequest",
"EmailChangeRequest",
"Comment",
]
import secrets
@@ -107,6 +108,7 @@ with (app.app_context()):
commits = db.relationship("Commit", back_populates="owner")
posts = db.relationship("Post", back_populates="owner")
comments = db.relationship("Comment", back_populates="owner")
prs = db.relationship("PullRequest", back_populates="owner")
notifications = db.relationship("UserNotification", back_populates="user")
@@ -163,6 +165,7 @@ with (app.app_context()):
commits = db.relationship("Commit", back_populates="repo")
posts = db.relationship("Post", back_populates="repo")
comments = db.relationship("Comment", back_populates="repo")
repo_access = db.relationship("RepoAccess", back_populates="repo")
favourites = db.relationship("RepoFavourite", back_populates="repo")
heads = db.relationship("PullRequest", back_populates="head", foreign_keys="[PullRequest.head_route]")
@@ -172,7 +175,8 @@ with (app.app_context()):
server_default="0") # (the one accessible at username.localhost)
site_branch = db.Column(db.String(64), nullable=True)
last_post_id = db.Column(db.Integer, nullable=False, default=0)
last_post_id = db.Column(db.Integer, nullable=False, default=0, server_default="0")
last_comment_id = db.Column(db.Integer, nullable=False, default=0, server_default="0")
def __init__(self, owner, name, visibility):
self.route = f"/{owner.username}/{name}"
@@ -203,6 +207,8 @@ with (app.app_context()):
repo = db.relationship("Repo", back_populates="commits")
owner = db.relationship("User", back_populates="commits")
comments = db.relationship("Comment", back_populates="commit")
def __init__(self, sha, owner, repo, date, message, owner_identity):
self.identifier = f"{repo.route}/{sha}"
self.sha = sha
@@ -279,6 +285,44 @@ with (app.app_context()):
with db.session.no_autoflush:
if self.parent is not None:
self.parent.update_date()
class Comment(db.Model):
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
number = db.Column(db.Integer, nullable=False)
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
commit_identifier = db.Column(db.String(227), db.ForeignKey("commit.identifier"), nullable=False)
pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=True)
file = db.Column(db.String(256), nullable=True)
line_number = db.Column(db.Integer, nullable=True)
state = db.Column(db.SmallInteger, nullable=True, default=1)
review = db.Column(db.SmallInteger, nullable=True, default=0)
date = db.Column(db.DateTime, default=datetime.now)
message = db.Column(db.UnicodeText)
repo = db.relationship("Repo", back_populates="comments")
owner = db.relationship("User", back_populates="comments")
commit = db.relationship("Commit", back_populates="comments")
def __init__(self, owner, repo, commit, message, file, line_number, pr=None):
self.identifier = f"{repo.route}/{repo.last_comment_id}"
self.number = repo.last_comment_id
self.repo_name = repo.route
self.repo = repo
self.owner_name = owner.username
self.owner = owner
self.commit_identifier = commit.identifier
self.commit = commit
self.message = message
self.file = file
self.line_number = line_number
self.pr_id = pr
repo.last_comment_id += 1
class UserNotification(db.Model):
static/style.css
@@ -437,23 +437,31 @@ header {
.code-view > .line-number:first-child,
.code-view > .line-number:first-child + :is(code, ins, del, x-codeline) {
padding-top: 1ch;
align-items: flex-end;
}
.code-view > .line-number:nth-last-child(2),
.code-view > :is(code, ins, del, x-codeline):last-child {
padding-bottom: 1ch;
align-items: flex-start;
}
.line-number {
display: inline-block;
box-sizing: content-box;
box-sizing: border-box;
text-align: right;
padding: 0 1ch;
padding: 0 1ch !important;
background: var(--color-code-line-number);
position: sticky;
left: 0;
user-select: none;
font-weight: 500;
font: var(--mono-font);
box-shadow: none;
width: 100%;
border-radius: 0;
min-height: 0;
cursor: cell;
}
.line-number:has(+ ins) {
@@ -577,3 +585,16 @@ strong, em {
gap: 1em;
width: 100%;
}
.comment {
/* Span all columns */
grid-column: 1 / -1;
padding: 1em;
background: var(--color-card);
color: var(--color-card-text);
font: var(--body-font);
box-shadow: var(--shadow-card);
z-index: 2;
border-radius: var(--radius-card);
border: var(--border-card);
}
templates/repo.html
@@ -12,7 +12,7 @@
<header class="card-top">
<div class="navbar navbar-mini">
<ul>
<li><b>{% trans %}Information{% endtrans %}</b></li>
<li><h4>{% trans %}Information{% endtrans %}</h4></li>
</ul>
<x-buttonbox class="dialog-tools">
<button class="button-flat button-neutral big-button" type="submit" form="info-form"><iconify-icon icon="mdi:close"></iconify-icon></button>
templates/repository/repo-commit.html
@@ -47,16 +47,16 @@
<pre class="code-view">
{% elif vars.hunk_started %}
{% if line.startswith("+") %}
<span class="line-number">{{ vars.modified_line }} +</span>
<button class="line-number" data-file="{{ file }}" data-line="{{ vars.modified_line }}">{{ vars.modified_line }} +</button>
<ins>{{ line[1:] }}</ins>
{% set vars.modified_line = vars.modified_line + 1 %}
{% elif line.startswith("-") %}
<span class="line-number">{{ vars.original_line }} -</span>
<button class="line-number" data-file="{{ file }}" data-line="{{ vars.original_line }}">{{ vars.original_line }} -</button>
<del>{{ line[1:] }}</del>
{% set vars.original_line = vars.original_line + 1 %}
{% elif not line.startswith("\\") %}
{% if line %}
<span class="line-number">{{ vars.modified_line }} </span>
<button class="line-number" data-file="{{ file }}" data-line="{{ vars.modified_line }}">{{ vars.modified_line }} </button>
<x-codeline>{{ line[1:] }}</x-codeline>
{% endif %}
{% if not line.startswith("@@") %}
@@ -65,6 +65,11 @@
{% endif %}
{% endif %}
{% endif %}
{% for comment in comment_query.filter_by(commit=data, file=file, line_number=vars.original_line).all() %}
<div class="comment">
{{ comment.message }}
</div>
{% endfor %}
{% endfor %}
{% if vars.hunk_started %}
</pre> <!-- close the last hunk -->
@@ -76,4 +81,40 @@
</x-vbox>
</x-frame>
</x-vbox>
<dialog id="add-comment-dialog">
<article class="card">
<header class="card-top">
<div class="navbar navbar-mini">
<ul>
<li><h4>{% trans %}Add comment{% endtrans %}</h4></li>
</ul>
<x-buttonbox class="dialog-tools">
<button class="button-flat button-neutral big-button" type="submit" form="info-form"><iconify-icon icon="mdi:close"></iconify-icon></button>
</x-buttonbox>
</div>
</header>
<section class="card-main" style="padding-top: var(--padding-card-top);">
<form method="post" action="{{ repo_data.route }}/commit/{{ data.sha }}/add_comment">
<input type="hidden" name="file" value="">
<input type="hidden" name="line" value="">
<textarea name="comment" required></textarea>
<button type="submit">Post</button>
</form>
</section>
</article>
</dialog>
{% endblock %}
{% block scripts %}
<script>
document.querySelectorAll(".line-number").forEach(function (element) {
element.addEventListener("click", function () {
let file = element.getAttribute("data-file");
let line = element.getAttribute("data-line");
let dialog = document.getElementById("add-comment-dialog");
dialog.querySelector("input[name=file]").value = file;
dialog.querySelector("input[name=line]").value = line;
dialog.showModal();
});
});
</script>
{% endblock %}