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

Dismiss

 app.py

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