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