By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 app.py

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