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 • 64.06 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
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("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("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("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("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("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("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
)
812
813
814
@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"])
815
def repository_commit_add_comment(username, repository, sha):
816
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
817
if not os.path.exists(server_repo_location):
818
app.logger.error(f"Cannot load {server_repo_location}")
819
flask.abort(404)
820
if not (get_visibility(username, repository) or get_permission_level(
821
flask.session.get("username"), username,
822
repository) is not None):
823
flask.abort(403)
824
825
comment = Comment(
826
db.session.get(User, flask.session.get("username")),
827
db.session.get(Repo, f"/{username}/{repository}"),
828
db.session.get(Commit, f"/{username}/{repository}/{sha}"),
829
flask.request.form["comment"],
830
flask.request.form["file"],
831
flask.request.form["line"],
832
)
833
834
db.session.add(comment)
835
db.session.commit()
836
837
return flask.redirect(
838
flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
839
code=303
840
)
841
842
843
@repositories.route("/<username>/<repository>/forum/")
844
def repository_forum(username, repository):
845
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
846
if not os.path.exists(server_repo_location):
847
app.logger.error(f"Cannot load {server_repo_location}")
848
flask.abort(404)
849
if not (get_visibility(username, repository) or get_permission_level(
850
flask.session.get("username"), username,
851
repository) is not None):
852
flask.abort(403)
853
854
app.logger.info(f"Loading {server_repo_location}")
855
856
if not os.path.exists(server_repo_location):
857
app.logger.error(f"Cannot load {server_repo_location}")
858
return flask.render_template("not-found.html"), 404
859
860
repo = git.Repo(server_repo_location)
861
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
862
user = User.query.filter_by(username=flask.session.get("username")).first()
863
relationships = RepoAccess.query.filter_by(repo=repo_data)
864
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
865
866
return flask.render_template(
867
"repo-forum.html",
868
username=username,
869
repository=repository,
870
repo_data=repo_data,
871
relationships=relationships,
872
repo=repo,
873
user_relationship=user_relationship,
874
Post=Post,
875
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
876
is_favourite=get_favourite(flask.session.get("username"), username, repository),
877
default_branch=repo_data.default_branch
878
)
879
880
881
@repositories.route("/<username>/<repository>/forum/topic/<int:id>")
882
def repository_forum_topic(username, repository, id):
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("not-found.html"), 404
897
898
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
899
user = User.query.filter_by(username=flask.session.get("username")).first()
900
relationships = RepoAccess.query.filter_by(repo=repo_data)
901
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
902
903
post = Post.query.filter_by(id=id).first()
904
905
return flask.render_template(
906
"repo-topic.html",
907
username=username,
908
repository=repository,
909
repo_data=repo_data,
910
relationships=relationships,
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/new", methods=["POST", "GET"])
920
def repository_forum_new(username, repository):
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("not-found.html"), 404
935
936
repo = git.Repo(server_repo_location)
937
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
938
user = User.query.filter_by(username=flask.session.get("username")).first()
939
relationships = RepoAccess.query.filter_by(repo=repo_data)
940
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
941
942
post = Post(user, repo_data, None, flask.request.form["subject"],
943
flask.request.form["message"])
944
945
db.session.add(post)
946
db.session.commit()
947
948
return flask.redirect(
949
flask.url_for(".repository_forum_thread", username=username, repository=repository,
950
post_id=post.number),
951
code=303)
952
953
954
@repositories.route("/<username>/<repository>/forum/<int:post_id>")
955
def repository_forum_thread(username, repository, post_id):
956
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
957
if not os.path.exists(server_repo_location):
958
app.logger.error(f"Cannot load {server_repo_location}")
959
flask.abort(404)
960
if not (get_visibility(username, repository) or get_permission_level(
961
flask.session.get("username"), username,
962
repository) is not None):
963
flask.abort(403)
964
965
app.logger.info(f"Loading {server_repo_location}")
966
967
if not os.path.exists(server_repo_location):
968
app.logger.error(f"Cannot load {server_repo_location}")
969
return flask.render_template("not-found.html"), 404
970
971
repo = git.Repo(server_repo_location)
972
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
973
user = User.query.filter_by(username=flask.session.get("username")).first()
974
relationships = RepoAccess.query.filter_by(repo=repo_data)
975
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
976
977
if user:
978
max_post_nesting = user.max_post_nesting
979
else:
980
max_post_nesting = 2
981
982
return flask.render_template(
983
"repo-forum-thread.html",
984
username=username,
985
repository=repository,
986
repo_data=repo_data,
987
relationships=relationships,
988
repo=repo,
989
Post=Post,
990
user_relationship=user_relationship,
991
post_id=post_id,
992
max_post_nesting=max_post_nesting,
993
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
994
is_favourite=get_favourite(flask.session.get("username"), username, repository),
995
parent=Post.query.filter_by(repo=repo_data, number=post_id).first(),
996
has_permission=not ((not get_permission_level(flask.session.get("username"), username,
997
repository)) and db.session.get(Post,
998
f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")),
999
)
1000
1001
1002
@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state",
1003
methods=["POST"])
1004
def repository_forum_change_state(username, repository, post_id):
1005
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1006
if not os.path.exists(server_repo_location):
1007
app.logger.error(f"Cannot load {server_repo_location}")
1008
flask.abort(404)
1009
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"):
1010
flask.abort(403)
1011
1012
app.logger.info(f"Loading {server_repo_location}")
1013
1014
repo = git.Repo(server_repo_location)
1015
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1016
user = User.query.filter_by(username=flask.session.get("username")).first()
1017
relationships = RepoAccess.query.filter_by(repo=repo_data)
1018
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1019
1020
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1021
1022
if not post:
1023
flask.abort(404)
1024
1025
post.state = int(flask.request.form["new-state"])
1026
1027
db.session.commit()
1028
1029
return flask.redirect(
1030
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1031
post_id=post_id),
1032
code=303)
1033
1034
1035
@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"])
1036
def repository_forum_reply(username, repository, post_id):
1037
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1038
if not os.path.exists(server_repo_location):
1039
app.logger.error(f"Cannot load {server_repo_location}")
1040
flask.abort(404)
1041
if not (get_visibility(username, repository) or get_permission_level(
1042
flask.session.get("username"), username,
1043
repository) is not None):
1044
flask.abort(403)
1045
1046
app.logger.info(f"Loading {server_repo_location}")
1047
1048
if not os.path.exists(server_repo_location):
1049
app.logger.error(f"Cannot load {server_repo_location}")
1050
return flask.render_template("not-found.html"), 404
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
if not user:
1058
flask.abort(401)
1059
1060
parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1061
post = Post(user, repo_data, parent, flask.request.form["subject"],
1062
flask.request.form["message"])
1063
1064
db.session.add(post)
1065
post.update_date()
1066
db.session.commit()
1067
1068
return flask.redirect(
1069
flask.url_for(".repository_forum_thread", username=username, repository=repository,
1070
post_id=post_id),
1071
code=303)
1072
1073
1074
@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup",
1075
defaults={"score": 1})
1076
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown",
1077
defaults={"score": -1})
1078
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0})
1079
def repository_forum_vote(username, repository, post_id, score):
1080
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1081
if not os.path.exists(server_repo_location):
1082
app.logger.error(f"Cannot load {server_repo_location}")
1083
flask.abort(404)
1084
if not (get_visibility(username, repository) or get_permission_level(
1085
flask.session.get("username"), username,
1086
repository) is not None):
1087
flask.abort(403)
1088
1089
app.logger.info(f"Loading {server_repo_location}")
1090
1091
if not os.path.exists(server_repo_location):
1092
app.logger.error(f"Cannot load {server_repo_location}")
1093
return flask.render_template("not-found.html"), 404
1094
1095
repo = git.Repo(server_repo_location)
1096
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1097
user = User.query.filter_by(username=flask.session.get("username")).first()
1098
relationships = RepoAccess.query.filter_by(repo=repo_data)
1099
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1100
if not user:
1101
flask.abort(401)
1102
1103
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
1104
1105
if score:
1106
old_relationship = PostVote.query.filter_by(user_username=user.username,
1107
post_identifier=post.identifier).first()
1108
if old_relationship:
1109
if score == old_relationship.vote_score:
1110
db.session.delete(old_relationship)
1111
post.vote_sum -= old_relationship.vote_score
1112
else:
1113
post.vote_sum -= old_relationship.vote_score
1114
post.vote_sum += score
1115
old_relationship.vote_score = score
1116
else:
1117
relationship = PostVote(user, post, score)
1118
post.vote_sum += score
1119
db.session.add(relationship)
1120
1121
db.session.commit()
1122
1123
user_vote = PostVote.query.filter_by(user_username=user.username,
1124
post_identifier=post.identifier).first()
1125
response = flask.make_response(
1126
str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0))
1127
response.content_type = "text/plain"
1128
1129
return response
1130
1131
1132
@repositories.route("/<username>/<repository>/favourite")
1133
def repository_favourite(username, repository):
1134
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1135
if not os.path.exists(server_repo_location):
1136
app.logger.error(f"Cannot load {server_repo_location}")
1137
flask.abort(404)
1138
if not (get_visibility(username, repository) or get_permission_level(
1139
flask.session.get("username"), username,
1140
repository) is not None):
1141
flask.abort(403)
1142
1143
app.logger.info(f"Loading {server_repo_location}")
1144
1145
if not os.path.exists(server_repo_location):
1146
app.logger.error(f"Cannot load {server_repo_location}")
1147
return flask.render_template("not-found.html"), 404
1148
1149
repo = git.Repo(server_repo_location)
1150
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1151
user = User.query.filter_by(username=flask.session.get("username")).first()
1152
relationships = RepoAccess.query.filter_by(repo=repo_data)
1153
user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
1154
if not user:
1155
flask.abort(401)
1156
1157
old_relationship = RepoFavourite.query.filter_by(user_username=user.username,
1158
repo_route=repo_data.route).first()
1159
if old_relationship:
1160
db.session.delete(old_relationship)
1161
else:
1162
relationship = RepoFavourite(user, repo_data)
1163
db.session.add(relationship)
1164
1165
db.session.commit()
1166
1167
return flask.redirect(flask.url_for("favourites"), code=303)
1168
1169
1170
@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"])
1171
def repository_users(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("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
1193
if flask.request.method == "GET":
1194
return flask.render_template(
1195
"repo-users.html",
1196
username=username,
1197
repository=repository,
1198
repo_data=repo_data,
1199
relationships=relationships,
1200
repo=repo,
1201
user_relationship=user_relationship,
1202
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1203
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1204
)
1205
else:
1206
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1207
flask.abort(401)
1208
1209
if flask.request.form.get("new-username"):
1210
# Create new relationship
1211
new_user = User.query.filter_by(
1212
username=flask.request.form.get("new-username")).first()
1213
relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level"))
1214
db.session.add(relationship)
1215
db.session.commit()
1216
if flask.request.form.get("update-username"):
1217
# Create new relationship
1218
updated_user = User.query.filter_by(
1219
username=flask.request.form.get("update-username")).first()
1220
relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first()
1221
if flask.request.form.get("update-level") == -1:
1222
relationship.delete()
1223
else:
1224
relationship.access_level = flask.request.form.get("update-level")
1225
db.session.commit()
1226
1227
return flask.redirect(
1228
app.url_for(".repository_users", username=username, repository=repository))
1229
1230
1231
@repositories.route("/<username>/<repository>/branches/")
1232
def repository_branches(username, repository):
1233
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1234
if not os.path.exists(server_repo_location):
1235
app.logger.error(f"Cannot load {server_repo_location}")
1236
flask.abort(404)
1237
if not (get_visibility(username, repository) or get_permission_level(
1238
flask.session.get("username"), username,
1239
repository) is not None):
1240
flask.abort(403)
1241
1242
app.logger.info(f"Loading {server_repo_location}")
1243
1244
if not os.path.exists(server_repo_location):
1245
app.logger.error(f"Cannot load {server_repo_location}")
1246
return flask.render_template("not-found.html"), 404
1247
1248
repo = git.Repo(server_repo_location)
1249
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1250
1251
return flask.render_template(
1252
"repo-branches.html",
1253
username=username,
1254
repository=repository,
1255
repo_data=repo_data,
1256
repo=repo,
1257
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1258
is_favourite=get_favourite(flask.session.get("username"), username, repository)
1259
)
1260
1261
1262
@repositories.route("/<username>/<repository>/log/", defaults={"branch": None})
1263
@repositories.route("/<username>/<repository>/log/<branch>/")
1264
def repository_log(username, repository, branch):
1265
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1266
if not os.path.exists(server_repo_location):
1267
app.logger.error(f"Cannot load {server_repo_location}")
1268
flask.abort(404)
1269
if not (get_visibility(username, repository) or get_permission_level(
1270
flask.session.get("username"), username,
1271
repository) is not None):
1272
flask.abort(403)
1273
1274
app.logger.info(f"Loading {server_repo_location}")
1275
1276
if not os.path.exists(server_repo_location):
1277
app.logger.error(f"Cannot load {server_repo_location}")
1278
return flask.render_template("not-found.html"), 404
1279
1280
repo = git.Repo(server_repo_location)
1281
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1282
if not repo_data.default_branch:
1283
if repo.heads:
1284
repo_data.default_branch = repo.heads[0].name
1285
else:
1286
return flask.render_template("empty.html",
1287
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
1288
if not branch:
1289
branch = repo_data.default_branch
1290
return flask.redirect(f"./{branch}", code=302)
1291
1292
if branch.startswith("tag:"):
1293
ref = f"tags/{branch[4:]}"
1294
elif branch.startswith("~"):
1295
ref = branch[1:]
1296
else:
1297
ref = f"heads/{branch}"
1298
1299
ref = ref.replace("~", "/") # encode slashes for URL support
1300
1301
try:
1302
repo.git.checkout("-f", ref)
1303
except git.exc.GitCommandError:
1304
return flask.render_template("not-found.html"), 404
1305
1306
branches = repo.heads
1307
1308
all_refs = []
1309
for ref in repo.heads:
1310
all_refs.append((ref, "head"))
1311
for ref in repo.tags:
1312
all_refs.append((ref, "tag"))
1313
1314
commit_list = [f"/{username}/{repository}/{sha}" for sha in
1315
git_command(server_repo_location, None, "log",
1316
"--format='%H'").decode().split("\n")]
1317
1318
commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc())
1319
page_number = flask.request.args.get("page", 1, type=int)
1320
if flask.session.get("username"):
1321
default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
1322
else:
1323
default_page_length = 16
1324
page_length = flask.request.args.get("per_page", default_page_length, type=int)
1325
page_listing = db.paginate(commits, page=page_number, per_page=page_length)
1326
1327
if page_listing.has_next:
1328
next_page = page_listing.next_num
1329
else:
1330
next_page = None
1331
1332
if page_listing.has_prev:
1333
prev_page = page_listing.prev_num
1334
else:
1335
prev_page = None
1336
1337
return flask.render_template(
1338
"repo-log.html",
1339
username=username,
1340
repository=repository,
1341
branches=all_refs,
1342
current=branch,
1343
repo_data=repo_data,
1344
repo=repo,
1345
commits=page_listing,
1346
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1347
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1348
page_number=page_number,
1349
page_length=page_length,
1350
next_page=next_page,
1351
prev_page=prev_page,
1352
num_pages=page_listing.pages
1353
)
1354
1355
1356
@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"])
1357
def repository_prs(username, repository):
1358
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1359
if not os.path.exists(server_repo_location):
1360
app.logger.error(f"Cannot load {server_repo_location}")
1361
flask.abort(404)
1362
if not (get_visibility(username, repository) or get_permission_level(
1363
flask.session.get("username"), username,
1364
repository) is not None):
1365
flask.abort(403)
1366
1367
app.logger.info(f"Loading {server_repo_location}")
1368
1369
if not os.path.exists(server_repo_location):
1370
app.logger.error(f"Cannot load {server_repo_location}")
1371
return flask.render_template("not-found.html"), 404
1372
1373
if flask.request.method == "GET":
1374
repo = git.Repo(server_repo_location)
1375
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1376
user = User.query.filter_by(username=flask.session.get("username")).first()
1377
1378
return flask.render_template(
1379
"repo-prs.html",
1380
username=username,
1381
repository=repository,
1382
repo_data=repo_data,
1383
repo=repo,
1384
PullRequest=PullRequest,
1385
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1386
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1387
default_branch=repo_data.default_branch,
1388
branches=repo.branches
1389
)
1390
1391
else:
1392
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1393
head = flask.request.form.get("head")
1394
head_route = flask.request.form.get("headroute")
1395
base = flask.request.form.get("base")
1396
1397
if not head and base and head_route:
1398
return flask.redirect(".", 400)
1399
1400
head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/")))
1401
base_repo = git.Repo(server_repo_location)
1402
print(head_repo)
1403
1404
if head not in head_repo.branches or base not in base_repo.branches:
1405
flask.flash(Markup(
1406
"<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")),
1407
category="error")
1408
return flask.redirect(".", 303)
1409
1410
head_data = db.session.get(Repo, head_route)
1411
if not head_data.visibility:
1412
flask.flash(Markup(
1413
"<iconify-icon icon='mdi:error'></iconify-icon>" + _(
1414
"Head can't be restricted")),
1415
category="error")
1416
return flask.redirect(".", 303)
1417
1418
pull_request = PullRequest(head_data, head, repo_data, base,
1419
db.session.get(User, flask.session["username"]))
1420
1421
db.session.add(pull_request)
1422
db.session.commit()
1423
1424
return flask.redirect(".", 303)
1425
1426
1427
@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"])
1428
def repository_prs_merge(username, repository):
1429
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1430
if not os.path.exists(server_repo_location):
1431
app.logger.error(f"Cannot load {server_repo_location}")
1432
flask.abort(404)
1433
if not (get_visibility(username, repository) or get_permission_level(
1434
flask.session.get("username"), username,
1435
repository) is not None):
1436
flask.abort(403)
1437
1438
if not get_permission_level(flask.session.get("username"), username, repository):
1439
flask.abort(401)
1440
1441
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1442
repo = git.Repo(server_repo_location)
1443
id = flask.request.form.get("id")
1444
1445
pull_request = db.session.get(PullRequest, id)
1446
1447
if pull_request:
1448
result = celery_tasks.merge_heads.delay(
1449
pull_request.head_route,
1450
pull_request.head_branch,
1451
pull_request.base_route,
1452
pull_request.base_branch,
1453
simulate=True
1454
)
1455
task_result = worker.AsyncResult(result.id)
1456
1457
return flask.redirect(f"/task/{result.id}?pr-id={id}", 303)
1458
# db.session.delete(pull_request)
1459
# db.session.commit()
1460
else:
1461
flask.abort(400)
1462
1463
1464
@repositories.route("/<username>/<repository>/prs/<int:id>/merge")
1465
def repository_prs_merge_stage_two(username, repository, id):
1466
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1467
if not os.path.exists(server_repo_location):
1468
app.logger.error(f"Cannot load {server_repo_location}")
1469
flask.abort(404)
1470
if not (get_visibility(username, repository) or get_permission_level(
1471
flask.session.get("username"), username,
1472
repository) is not None):
1473
flask.abort(403)
1474
1475
if not get_permission_level(flask.session.get("username"), username, repository):
1476
flask.abort(401)
1477
1478
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1479
repo = git.Repo(server_repo_location)
1480
1481
pull_request = db.session.get(PullRequest, id)
1482
1483
if pull_request:
1484
result = celery_tasks.merge_heads.delay(
1485
pull_request.head_route,
1486
pull_request.head_branch,
1487
pull_request.base_route,
1488
pull_request.base_branch,
1489
simulate=False
1490
)
1491
task_result = worker.AsyncResult(result.id)
1492
1493
pull_request.state = 1
1494
db.session.commit()
1495
1496
return flask.redirect(f"/task/{result.id}?pr-id={id}", 303)
1497
# db.session.delete(pull_request)
1498
else:
1499
flask.abort(400)
1500
1501
1502
@app.route("/task/<task_id>")
1503
def task_monitor(task_id):
1504
task_result = worker.AsyncResult(task_id)
1505
if task_result.status == "FAILURE":
1506
app.logger.error(f"Task {task_id} failed")
1507
return flask.render_template("task-monitor.html", result=task_result), 500
1508
1509
return flask.render_template("task-monitor.html", result=task_result)
1510
1511
1512
@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"])
1513
def repository_prs_delete(username, repository):
1514
server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
1515
if not os.path.exists(server_repo_location):
1516
app.logger.error(f"Cannot load {server_repo_location}")
1517
flask.abort(404)
1518
if not (get_visibility(username, repository) or get_permission_level(
1519
flask.session.get("username"), username,
1520
repository) is not None):
1521
flask.abort(403)
1522
1523
if not get_permission_level(flask.session.get("username"), username, repository):
1524
flask.abort(401)
1525
1526
repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
1527
repo = git.Repo(server_repo_location)
1528
id = flask.request.form.get("id")
1529
1530
pull_request = db.session.get(PullRequest, id)
1531
1532
if pull_request:
1533
pull_request.state = 2
1534
db.session.commit()
1535
1536
return flask.redirect(".", 303)
1537
1538
1539
@repositories.route("/<username>/<repository>/settings/")
1540
def repository_settings(username, repository):
1541
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1542
flask.abort(401)
1543
1544
repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository))
1545
1546
site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>")
1547
primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>")
1548
1549
return flask.render_template("repo-settings.html", username=username, repository=repository,
1550
repo_data=db.session.get(Repo, f"/{username}/{repository}"),
1551
branches=[branch.name for branch in repo.branches],
1552
site_link=site_link, primary_site_link=primary_site_link,
1553
remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
1554
is_favourite=get_favourite(flask.session.get("username"), username, repository),
1555
)
1556
1557
1558
@repositories.route("/<username>/<repository>/settings/", methods=["POST"])
1559
def repository_settings_post(username, repository):
1560
if get_permission_level(flask.session.get("username"), username, repository) != 2:
1561
flask.abort(401)
1562
1563
repo = db.session.get(Repo, f"/{username}/{repository}")
1564
1565
repo.visibility = flask.request.form.get("visibility", type=int)
1566
repo.info = flask.request.form.get("description")
1567
repo.default_branch = flask.request.form.get("default_branch")
1568
1569
# Update site settings
1570
had_site = repo.has_site
1571
old_branch = repo.site_branch
1572
if flask.request.form.get("site_branch"):
1573
repo.site_branch = flask.request.form.get("site_branch")
1574
if flask.request.form.get("primary_site"):
1575
if had_site != 2:
1576
# Remove primary site from other repos
1577
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2):
1578
other_repo.has_site = 1 # switch it to a regular site
1579
flask.flash(Markup(
1580
_("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(
1581
repository=other_repo.route
1582
)), category="warning")
1583
repo.has_site = 2
1584
else:
1585
repo.has_site = 1
1586
else:
1587
repo.site_branch = None
1588
repo.has_site = 0
1589
1590
db.session.commit()
1591
1592
if not (had_site, old_branch) == (repo.has_site, repo.site_branch):
1593
# Deploy the newly activated site
1594
result = celery_tasks.copy_site.delay(repo.route)
1595
1596
if had_site and not repo.has_site:
1597
# Remove the site
1598
result = celery_tasks.delete_site.delay(repo.route)
1599
1600
if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site):
1601
# Deploy all other sites which were destroyed by the primary site
1602
for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1):
1603
result = celery_tasks.copy_site.delay(other_repo.route)
1604
1605
return flask.redirect(f"/{username}/{repository}/settings", 303)
1606
1607
1608
@app.errorhandler(404)
1609
def e404(error):
1610
return flask.render_template("not-found.html"), 404
1611
1612
1613
@app.errorhandler(401)
1614
def e401(error):
1615
return flask.render_template("unauthorised.html"), 401
1616
1617
1618
@app.errorhandler(403)
1619
def e403(error):
1620
return flask.render_template("forbidden.html"), 403
1621
1622
1623
@app.errorhandler(418)
1624
def e418(error):
1625
return flask.render_template("teapot.html"), 418
1626
1627
1628
@app.errorhandler(405)
1629
def e405(error):
1630
return flask.render_template("method-not-allowed.html"), 405
1631
1632
1633
if __name__ == "__main__":
1634
app.run(debug=True, port=8080, host="0.0.0.0")
1635
1636
app.register_blueprint(repositories)
1637