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