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