Compare commits
9 Commits
20e6d7328a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e42baf3ca9 | |||
| cf3f8b0aea | |||
| 331387a56a | |||
|
d17d64876a
|
|||
|
ac7d7a44c7
|
|||
|
e59322c3cf
|
|||
|
6e1859943a
|
|||
|
0daf300060
|
|||
|
27d82a2df5
|
1847
Cargo.lock
generated
1847
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,17 @@ edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "frontend"
|
||||
path = "src/frontend.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "test_render"
|
||||
path = "src/test-render.rs"
|
||||
|
||||
[dependencies]
|
||||
git2 = { version = "0.20.2", default-features = false }
|
||||
gix = "0.78.0"
|
||||
serde = { version = "1.0.219", default-features = false, features = ["derive"] }
|
||||
#inkjet = "0.11.1"
|
||||
#markdown = "1.0.0"
|
||||
tera = { version = "1.20.0", default-features = false, features = ["builtins"] }
|
||||
httparse = "1.10.1"
|
||||
futures = { version = "0.3.32", default-features = false, features = ["std", "thread-pool", "executor"] }
|
||||
typed-path = "=0.12.3" # pinned to avoid slop
|
||||
itertools = "0.14.0"
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<!--
|
||||
Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||
Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This file is part of Mintee.
|
||||
|
||||
Mintee is free software: you can redistribute it and/or modify it under the
|
||||
terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option) any
|
||||
later version.
|
||||
|
||||
Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>mintea</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<link rel="stylesheet" href="res/style.css">
|
||||
<link rel="stylesheet" href="res/mint.css">
|
||||
<script>
|
||||
/*
|
||||
@licstart The following is the entire license notice for the
|
||||
JavaScript code in this page.
|
||||
|
||||
Copyright (C) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||
|
||||
The JavaScript code in this page is free software: you can
|
||||
redistribute it and/or modify it under the terms of the GNU
|
||||
General Public License (GNU GPL) as published by the Free Software
|
||||
Foundation, either version 3 of the License, or (at your option)
|
||||
any later version. The code is distributed WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
|
||||
|
||||
As additional permission under GNU GPL version 3 section 7, you
|
||||
may distribute non-source (e.g., minimized or compacted) forms of
|
||||
that code without the copy of the GNU GPL normally required by
|
||||
section 4, provided you include this license notice and a URL
|
||||
through which recipients can access the Corresponding Source.
|
||||
|
||||
|
||||
@licend The above is the entire license notice
|
||||
for the JavaScript code in this page.
|
||||
*/
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id=stickyWrap>
|
||||
<nav>
|
||||
<div class=buttonListWrap>
|
||||
<ul class=buttonList>
|
||||
<li><a href="/">TiBM</a></li>
|
||||
<li><a class=dashboardButton href="/"><span class=collapse>dashboard</span></a></li>
|
||||
<li><a class=profileButton href="/"><span class=collapse>profile</span></a></li>
|
||||
<li><a class=notificationsButton href="/"><span class=collapse>notifications</span> (17)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class=buttonListWrap>
|
||||
<div class="tabList end"><span>code</span>
|
||||
<a href="/">history</a>
|
||||
<a href="/">tags</a>
|
||||
<a href="/">tickets (17)</a>
|
||||
<a href="/">releases</a>
|
||||
<a href="/">settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<span>
|
||||
viewing
|
||||
<span class=linkedPath>
|
||||
<a href="/">owner</a>/<a href="/">repo</a>/<a href="/">branch</a>
|
||||
</span>
|
||||
</span>
|
||||
<ul class=buttonList>
|
||||
<li><a class=watchButton href="/">watch</a></li>
|
||||
<li><a class=starButton href="/">star</a></li>
|
||||
<li><a class=forkButton href="/">fork</a></li>
|
||||
<li><a class=cloneButton href="/">clone</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<div id=contentWrap>
|
||||
<aside>
|
||||
<p class=sidebarLabel>
|
||||
<a href="/">/</a><a href="/">name/</a>
|
||||
</p>
|
||||
<table class=files border=1>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a class=file href="/">file name</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a class=directory href="/">directory/</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a class=file href="/">file name</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a class=file href="/">file name</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<p class=previewLabel>README.md</p>
|
||||
<article class=preview>
|
||||
<h1>Some Repository</h1>
|
||||
<p>Ratione quis totam tempora sit magnam voluptas. Recusandae qui illo nulla eligendi. Perspiciatis iusto numquam suscipit aspernatur quibusdam ex.</p>
|
||||
<p>Consequuntur atque rerum culpa numquam et et possimus dolor. Quidem possimus quia et consectetur. Debitis autem cupiditate vero maxime et libero quae. Tenetur vero architecto iure dolores numquam assumenda enim.</p>
|
||||
<p>Dolorem facere animi quis repudiandae rerum dolor. Reprehenderit consequatur quae quia quos illum. Labore ducimus ut quaerat et corrupti cupiditate tenetur.</p>
|
||||
<p>Nisi ut aut sunt dignissimos. Alias magnam itaque deleniti alias quibusdam id possimus eos. Reiciendis et eos placeat. Vero eligendi occaecati quaerat vitae voluptatem deserunt.</p>
|
||||
<p>Dolor aut corrupti et officia id. Minus ipsam assumenda fugiat neque. Neque et saepe maiores iusto maiores. Dolor et at reprehenderit exercitationem totam neque.</p>
|
||||
<h2>Heading</h2>
|
||||
<p>Ratione quis totam tempora sit magnam voluptas. Recusandae qui illo nulla eligendi. Perspiciatis iusto numquam suscipit aspernatur quibusdam ex.</p>
|
||||
<p>Consequuntur atque rerum culpa numquam et et possimus dolor. Quidem possimus quia et consectetur. Debitis autem cupiditate vero maxime et libero quae. Tenetur vero architecto iure dolores numquam assumenda enim.</p>
|
||||
<p>Dolorem facere animi quis repudiandae rerum dolor. Reprehenderit consequatur quae quia quos illum. Labore ducimus ut quaerat et corrupti cupiditate tenetur.</p>
|
||||
<p>Nisi ut aut sunt dignissimos. Alias magnam itaque deleniti alias quibusdam id possimus eos. Reiciendis et eos placeat. Vero eligendi occaecati quaerat vitae voluptatem deserunt.</p>
|
||||
<p>Dolor aut corrupti et officia id. Minus ipsam assumenda fugiat neque. Neque et saepe maiores iusto maiores. Dolor et at reprehenderit exercitationem totam neque.</p>
|
||||
<h2>Heading</h2>
|
||||
<p>Ratione quis totam tempora sit magnam voluptas. Recusandae qui illo nulla eligendi. Perspiciatis iusto numquam suscipit aspernatur quibusdam ex.</p>
|
||||
<p>Consequuntur atque rerum culpa numquam et et possimus dolor. Quidem possimus quia et consectetur. Debitis autem cupiditate vero maxime et libero quae. Tenetur vero architecto iure dolores numquam assumenda enim.</p>
|
||||
<p>Dolorem facere animi quis repudiandae rerum dolor. Reprehenderit consequatur quae quia quos illum. Labore ducimus ut quaerat et corrupti cupiditate tenetur.</p>
|
||||
<p>Nisi ut aut sunt dignissimos. Alias magnam itaque deleniti alias quibusdam id possimus eos. Reiciendis et eos placeat. Vero eligendi occaecati quaerat vitae voluptatem deserunt.</p>
|
||||
<p>Dolor aut corrupti et officia id. Minus ipsam assumenda fugiat neque. Neque et saepe maiores iusto maiores. Dolor et at reprehenderit exercitationem totam neque.</p>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let stickyWrap = document.querySelector("#stickyWrap")
|
||||
document.addEventListener ("scroll", () => {
|
||||
if (document.documentElement.scrollTop > 0) {
|
||||
stickyWrap.className = "lifted"
|
||||
} else {
|
||||
stickyWrap.className = ""
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<a href="https://git.tebibyte.media/meta/mintee">Mintee</a>,
|
||||
the fresh and tasty git frontend. page: #s. template: #s.
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -66,7 +66,7 @@
|
||||
<li><a class=dashboardButton href="/">
|
||||
<span class=collapse>dashboard</span>
|
||||
</a></li>
|
||||
<li><a class=profileButton href="/{{ user }}">
|
||||
<li><a class=profileButton href="/~{{ user }}">
|
||||
<span class=collapse>profile</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -93,9 +93,9 @@
|
||||
<span>
|
||||
viewing
|
||||
<span class=linkedPath>
|
||||
{% block nav %}
|
||||
<a href="/">{{ owner }}</a>/<a href="/">{{ repo }}</a>
|
||||
{% endblock nav %}
|
||||
{% block breadcrumbs %}
|
||||
<a href="/~{{ owner }}">{{ owner }}</a>/<a href="/~{{ owner }}/{{ repo }}">{{ repo }}</a>
|
||||
{% endblock breadcrumbs %}
|
||||
</span>
|
||||
</span>
|
||||
<ul class=buttonList>
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
|
||||
{% block page %}code{% endblock page %}
|
||||
|
||||
|
||||
{% block nav %}
|
||||
<a href="/">{{ owner }}</a>/<a href="/">{{ repo }}</a>/<a href="/">{{ branch }}</a>
|
||||
{% endblock nav %}
|
||||
{% block breadcrumbs %}
|
||||
<a href="/~{{ owner }}">{{ owner }}</a>/<a href="/~{{ owner }}/{{ repo }}">{{ repo }}</a>/<a href="/">{{ branch }}</a>
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block content %}
|
||||
<div id=contentWrap>
|
||||
<aside>
|
||||
<p class=sidebarLabel>
|
||||
<a href="/"></a><a href="/">{{ directory }}</a>
|
||||
{% for part in directory | split(pat="/") -%}
|
||||
<a href="/">{{ part }}/</a>
|
||||
{%- endfor %}
|
||||
</p>
|
||||
<table class=files border=1>
|
||||
<tbody>
|
||||
{% block entries_list %}
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td><a class={{ entry.class }} href="/">{{ entry.path }}</a></td>
|
||||
<td><a class=commit href="./commit/{{ entry.last_commit }}">
|
||||
<td><a class={{ entry.class }} href="./{{ entry.path }}">{{ entry.path }}</a></td>
|
||||
<td><a class=commit href="./?commit={{ entry.last_commit }}">
|
||||
{{ entry.last_commit_message }}
|
||||
</a></td>
|
||||
<td><time datetime="{{ entry.last_commit_time }}">
|
||||
|
||||
42
assets/templates/repo/tickets.html
Normal file
42
assets/templates/repo/tickets.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block page %}tickets{% endblock page %}
|
||||
|
||||
{% block content %}
|
||||
<div id=contentWrap>
|
||||
<aside>
|
||||
<div class=sidebarLabel>
|
||||
tickets
|
||||
<div class=tabList style="display: inline-flex">
|
||||
<span>open</span>
|
||||
<a href="/">closed</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class=files border=1>
|
||||
<tbody>
|
||||
{% block entries_list %}
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td><a class={{ entry.class }} href="/">{{ entry.path }}</a></td>
|
||||
<td><a class=commit href="./commit/{{ entry.last_commit }}">
|
||||
{{ entry.last_commit_message }}
|
||||
</a></td>
|
||||
<td><time datetime="{{ entry.last_commit_time }}">
|
||||
{{ entry.last_commit_time | date(format="%Y-%m-%d %H:%M")}}
|
||||
</time></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock entries %}
|
||||
</tbody>
|
||||
</table>
|
||||
</aside>
|
||||
{% block readme %}
|
||||
<main>
|
||||
<article class=preview>
|
||||
<h1>Ticket title</h1>
|
||||
{{ readme_content }}
|
||||
</article>
|
||||
</main>
|
||||
{% endblock readme %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,173 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<!--
|
||||
Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||
Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This file is part of Mintee.
|
||||
|
||||
Mintee is free software: you can redistribute it and/or modify it under the
|
||||
terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option) any
|
||||
later version.
|
||||
|
||||
Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>mintea</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<link rel="stylesheet" href="res/style.css">
|
||||
<link rel="stylesheet" href="res/mint.css">
|
||||
<script>
|
||||
/*
|
||||
@licstart The following is the entire license notice for the
|
||||
JavaScript code in this page.
|
||||
|
||||
Copyright (C) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||
|
||||
The JavaScript code in this page is free software: you can
|
||||
redistribute it and/or modify it under the terms of the GNU
|
||||
General Public License (GNU GPL) as published by the Free Software
|
||||
Foundation, either version 3 of the License, or (at your option)
|
||||
any later version. The code is distributed WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
|
||||
|
||||
As additional permission under GNU GPL version 3 section 7, you
|
||||
may distribute non-source (e.g., minimized or compacted) forms of
|
||||
that code without the copy of the GNU GPL normally required by
|
||||
section 4, provided you include this license notice and a URL
|
||||
through which recipients can access the Corresponding Source.
|
||||
|
||||
|
||||
@licend The above is the entire license notice
|
||||
for the JavaScript code in this page.
|
||||
*/
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id=stickyWrap>
|
||||
<nav>
|
||||
<div class=buttonListWrap>
|
||||
<ul class=buttonList>
|
||||
<li><a href="/">TiBM</a></li>
|
||||
<li><a class=dashboardButton href="/"><span class=collapse>dashboard</span></a></li>
|
||||
<li><a class=profileButton href="/"><span class=collapse>profile</span></a></li>
|
||||
<li><a class=notificationsButton href="/"><span class=collapse>notifications</span> (17)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class=buttonListWrap>
|
||||
<div class="tabList end"><span>code</span>
|
||||
<a href="/">history</a>
|
||||
<a href="/">tags</a>
|
||||
<a href="/">tickets (17)</a>
|
||||
<a href="/">releases</a>
|
||||
<a href="/">settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<span>
|
||||
viewing
|
||||
<span class=linkedPath>
|
||||
<a href="/">owner</a>/<a href="/">repo</a>
|
||||
</span>
|
||||
</span>
|
||||
<ul class=buttonList>
|
||||
<li><a class=watchButton href="/">watch</a></li>
|
||||
<li><a class=starButton href="/">star</a></li>
|
||||
<li><a class=forkButton href="/">fork</a></li>
|
||||
<li><a class=cloneButton href="/">clone</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<div id=contentWrap>
|
||||
<aside>
|
||||
<div class=sidebarLabel>
|
||||
tickets
|
||||
<div class=tabList style="display: inline-flex">
|
||||
<span>open</span>
|
||||
<a href="/">closed</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class=files border=1>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a class=file href="/">file name</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a class=directory href="/">directory/</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a class=file href="/">file name</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a class=file href="/">file name</a></td>
|
||||
<td><a class=commit href="/">most recent commit</a></td>
|
||||
<td><time datetime="2023-01-01">in 2 months</time></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<article class=preview>
|
||||
<h1>Ticket title</h1>
|
||||
<p>Ratione quis totam tempora sit magnam voluptas. Recusandae qui illo nulla eligendi. Perspiciatis iusto numquam suscipit aspernatur quibusdam ex.</p>
|
||||
<p>Consequuntur atque rerum culpa numquam et et possimus dolor. Quidem possimus quia et consectetur. Debitis autem cupiditate vero maxime et libero quae. Tenetur vero architecto iure dolores numquam assumenda enim.</p>
|
||||
<p>Dolorem facere animi quis repudiandae rerum dolor. Reprehenderit consequatur quae quia quos illum. Labore ducimus ut quaerat et corrupti cupiditate tenetur.</p>
|
||||
<p>Nisi ut aut sunt dignissimos. Alias magnam itaque deleniti alias quibusdam id possimus eos. Reiciendis et eos placeat. Vero eligendi occaecati quaerat vitae voluptatem deserunt.</p>
|
||||
<p>Dolor aut corrupti et officia id. Minus ipsam assumenda fugiat neque. Neque et saepe maiores iusto maiores. Dolor et at reprehenderit exercitationem totam neque.</p>
|
||||
<h2>Heading</h2>
|
||||
<p>Ratione quis totam tempora sit magnam voluptas. Recusandae qui illo nulla eligendi. Perspiciatis iusto numquam suscipit aspernatur quibusdam ex.</p>
|
||||
<p>Consequuntur atque rerum culpa numquam et et possimus dolor. Quidem possimus quia et consectetur. Debitis autem cupiditate vero maxime et libero quae. Tenetur vero architecto iure dolores numquam assumenda enim.</p>
|
||||
<p>Dolorem facere animi quis repudiandae rerum dolor. Reprehenderit consequatur quae quia quos illum. Labore ducimus ut quaerat et corrupti cupiditate tenetur.</p>
|
||||
<p>Nisi ut aut sunt dignissimos. Alias magnam itaque deleniti alias quibusdam id possimus eos. Reiciendis et eos placeat. Vero eligendi occaecati quaerat vitae voluptatem deserunt.</p>
|
||||
<p>Dolor aut corrupti et officia id. Minus ipsam assumenda fugiat neque. Neque et saepe maiores iusto maiores. Dolor et at reprehenderit exercitationem totam neque.</p>
|
||||
<h2>Heading</h2>
|
||||
<p>Ratione quis totam tempora sit magnam voluptas. Recusandae qui illo nulla eligendi. Perspiciatis iusto numquam suscipit aspernatur quibusdam ex.</p>
|
||||
<p>Consequuntur atque rerum culpa numquam et et possimus dolor. Quidem possimus quia et consectetur. Debitis autem cupiditate vero maxime et libero quae. Tenetur vero architecto iure dolores numquam assumenda enim.</p>
|
||||
<p>Dolorem facere animi quis repudiandae rerum dolor. Reprehenderit consequatur quae quia quos illum. Labore ducimus ut quaerat et corrupti cupiditate tenetur.</p>
|
||||
<p>Nisi ut aut sunt dignissimos. Alias magnam itaque deleniti alias quibusdam id possimus eos. Reiciendis et eos placeat. Vero eligendi occaecati quaerat vitae voluptatem deserunt.</p>
|
||||
<p>Dolor aut corrupti et officia id. Minus ipsam assumenda fugiat neque. Neque et saepe maiores iusto maiores. Dolor et at reprehenderit exercitationem totam neque.</p>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let stickyWrap = document.querySelector("#stickyWrap")
|
||||
document.addEventListener ("scroll", () => {
|
||||
if (document.documentElement.scrollTop > 0) {
|
||||
stickyWrap.className = "lifted"
|
||||
} else {
|
||||
stickyWrap.className = ""
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<a href="https://git.tebibyte.media/meta/mintee">Mintee</a>,
|
||||
the fresh and tasty git frontend. page: #s. template: #s.
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
74
src/backend/git.rs
Normal file
74
src/backend/git.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::error::Error;
|
||||
|
||||
use gix::{ Repository, object::tree::EntryKind };
|
||||
use serde::Serialize;
|
||||
use tera::Context;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Entry {
|
||||
class: String,
|
||||
last_commit: String,
|
||||
last_commit_message: String,
|
||||
last_commit_time: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let branches = r.clone().branch_names()
|
||||
.iter()
|
||||
.map(|x| x.to_owned().to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
||||
|
||||
/* replace with configurable branch name when we have a database */
|
||||
let current_branch = r.head()?.referent_name().unwrap().to_string();
|
||||
|
||||
for e in tree.iter() {
|
||||
let entry = e.unwrap();
|
||||
|
||||
let last_commit = "TODO".to_string();
|
||||
let last_commit_message = "TODO".to_string();
|
||||
let last_commit_time = "1970-01-01T00:00:00".to_string();
|
||||
|
||||
let class = match entry.kind() {
|
||||
EntryKind::Tree => "directory",
|
||||
EntryKind::Blob => "file",
|
||||
_ => "",
|
||||
}.to_owned();
|
||||
|
||||
entries.push(Entry {
|
||||
class,
|
||||
last_commit,
|
||||
last_commit_message,
|
||||
last_commit_time,
|
||||
path: entry.filename().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let dir = r.git_dir();
|
||||
let name = dir.file_name().unwrap().to_str().unwrap();
|
||||
let directory = dir.as_os_str().to_str().unwrap();
|
||||
|
||||
let mut ctx = Context::new();
|
||||
|
||||
/* stubs til we have a real database */
|
||||
ctx.insert("user", "anon");
|
||||
ctx.insert("site", "TiB.");
|
||||
ctx.insert("notif_count", "");
|
||||
ctx.insert("ticket_count", "(47)");
|
||||
|
||||
ctx.insert("readme_content", "test readme");
|
||||
|
||||
ctx.insert("branches", &branches);
|
||||
ctx.insert("entries", &entries);
|
||||
ctx.insert("owner", "anon");
|
||||
ctx.insert("branch", ¤t_branch);
|
||||
ctx.insert("repo", name);
|
||||
ctx.insert("directory", directory);
|
||||
ctx.insert("entries", &entries);
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
2
src/backend/mod.rs
Normal file
2
src/backend/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod git;
|
||||
pub mod render;
|
||||
25
src/backend/test.rs
Normal file
25
src/backend/test.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use std::{ env::current_dir, error::Error };
|
||||
|
||||
use gix::{ traverse::tree::Recorder, prelude::* };
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let r = gix::open(current_dir()?)?;
|
||||
|
||||
let mut rec = Recorder::default();
|
||||
|
||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
||||
|
||||
tree.traverse().breadthfirst(&mut rec)?;
|
||||
|
||||
for e in rec.records.iter() {
|
||||
println!("{}", e.filepath)
|
||||
}
|
||||
|
||||
/*
|
||||
for e in tree.iter() {
|
||||
println!("{}", e?.filename())
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
62
src/bin/backend/git.rs
Normal file
62
src/bin/backend/git.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::error::Error;
|
||||
|
||||
use gix::{ Repository, traverse::tree::Recorder, prelude::* };
|
||||
use serde::Serialize;
|
||||
use tera::Context;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Entry {
|
||||
mode: String,
|
||||
hash: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let branches = r.clone().branch_names()
|
||||
.iter()
|
||||
.map(|x| x.to_owned().to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut rec = Recorder::default();
|
||||
|
||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
||||
|
||||
/* replace with configurable branch name when we have a database */
|
||||
let current_branch = r.head()?.referent_name().unwrap().to_string();
|
||||
|
||||
tree.traverse().breadthfirst(&mut rec)?;
|
||||
|
||||
for e in rec.records.iter() {
|
||||
entries.push(Entry {
|
||||
mode: e.mode.value().to_string(),
|
||||
hash: e.oid.to_string(),
|
||||
path: e.filepath.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let dir = r.git_dir();
|
||||
let name = dir.file_name().unwrap().to_str().unwrap();
|
||||
let directory = dir.as_os_str().to_str().unwrap();
|
||||
|
||||
let mut ctx = Context::new();
|
||||
|
||||
/* stubs til we have a real database */
|
||||
ctx.insert("user", "anon");
|
||||
ctx.insert("site", "TiB.");
|
||||
ctx.insert("notif_count", "");
|
||||
ctx.insert("ticket_count", "(47)");
|
||||
|
||||
ctx.insert("readme_content", "test readme");
|
||||
|
||||
ctx.insert("branches", &branches);
|
||||
ctx.insert("entries", &entries);
|
||||
ctx.insert("owner", "anon");
|
||||
ctx.insert("branch", ¤t_branch);
|
||||
ctx.insert("repo", name);
|
||||
ctx.insert("directory", directory);
|
||||
ctx.insert("entries", &entries);
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
65
src/bin/backend/render.rs
Normal file
65
src/bin/backend/render.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Emma Tebibyte <emma@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{ self, Display, Formatter },
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use tera::{ Context, Tera };
|
||||
|
||||
#[non_exhaustive]
|
||||
pub enum PageKind {
|
||||
Code,
|
||||
Dashboard,
|
||||
Tickets,
|
||||
User,
|
||||
}
|
||||
|
||||
impl PageKind {
|
||||
pub fn render_page(&self, ctx: Context) -> Result<String, Box<dyn Error>> {
|
||||
let page_dir = self.to_string();
|
||||
let template = String::from_utf8(Path::new(&page_dir)
|
||||
.to_path_buf()
|
||||
.as_mut_os_string()
|
||||
.as_encoded_bytes()
|
||||
.to_vec()
|
||||
)?;
|
||||
|
||||
let tera = Tera::new(&page_dir)?;
|
||||
Ok(tera.render(&template, &ctx)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PageKind {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
use PageKind::*;
|
||||
|
||||
let path = match self {
|
||||
Code => "repo/code.html",
|
||||
Dashboard => "dashboard.html",
|
||||
Tickets => "repo/tickets.html",
|
||||
User => "user.html",
|
||||
};
|
||||
|
||||
write!(f, "./assets/templates/{}", path)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Emma Tebibyte <emma@tebibyte.media>
|
||||
* Copyright (c) 2025–2026 Emma Tebibyte <emma@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
@@ -23,23 +23,14 @@ use std::{
|
||||
error::Error,
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use gix::open;
|
||||
use tera::{ Context, Tera };
|
||||
|
||||
#[path="git.rs"]
|
||||
mod git;
|
||||
|
||||
use git::GitRepo;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SampleEntry {
|
||||
class: String,
|
||||
last_commit_message: String,
|
||||
last_commit: String,
|
||||
last_commit_time: i64,
|
||||
path: String,
|
||||
}
|
||||
use git::repo_to_context;
|
||||
|
||||
/*
|
||||
impl From<GitRepo> for Context {
|
||||
fn from(repo: GitRepo) -> Self {
|
||||
let mut ctx = Context::new();
|
||||
@@ -85,13 +76,12 @@ impl From<GitRepo> for Context {
|
||||
ctx
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
if let Some(templates) = args().collect::<Vec<_>>().get(1) {
|
||||
let tera = Tera::new(templates.as_str())?;
|
||||
let ctx = Context::from(
|
||||
GitRepo::open(current_dir()?.to_path_buf(), None)?
|
||||
);
|
||||
let ctx = repo_to_context(open(current_dir()?)?)?;
|
||||
|
||||
println!("{}", tera.render("code.html", &ctx)?);
|
||||
|
||||
25
src/bin/backend/test.rs
Normal file
25
src/bin/backend/test.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use std::{ env::current_dir, error::Error };
|
||||
|
||||
use gix::{ traverse::tree::Recorder, prelude::* };
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let r = gix::open(current_dir()?)?;
|
||||
|
||||
let mut rec = Recorder::default();
|
||||
|
||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
||||
|
||||
tree.traverse().breadthfirst(&mut rec)?;
|
||||
|
||||
for e in rec.records.iter() {
|
||||
println!("{}", e.filepath)
|
||||
}
|
||||
|
||||
/*
|
||||
for e in tree.iter() {
|
||||
println!("{}", e?.filename())
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
465
src/bin/frontend/http_fe/mod.rs
Normal file
465
src/bin/frontend/http_fe/mod.rs
Normal file
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* Copyright (c) 2025, 2026 silt <silt@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use httparse::{self};
|
||||
use itertools::Itertools;
|
||||
use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf};
|
||||
use std::{
|
||||
error::Error, fmt, io::{self, BufRead, BufReader, Read}, net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, ops::Deref, pin::Pin, process::exit, str::FromStr, time::Duration
|
||||
};
|
||||
|
||||
use mintee::util::yapper::{yap, eyap};
|
||||
|
||||
pub use super::manager::{Frontend, FrontendImpl};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
HEAD,
|
||||
PUT,
|
||||
DELETE,
|
||||
CONNECT,
|
||||
OPTIONS,
|
||||
TRACE,
|
||||
PATCH,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&str> for HttpMethod {
|
||||
fn from(val: &str) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
"GET" => GET,
|
||||
"POST" => POST,
|
||||
"HEAD" => HEAD,
|
||||
"PUT" => PUT,
|
||||
"DELETE" => DELETE,
|
||||
"CONNECT" => CONNECT,
|
||||
"OPTIONS" => OPTIONS,
|
||||
"TRACE" => TRACE,
|
||||
"PATCH" => PATCH,
|
||||
_ => Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpMethod> for &'static str {
|
||||
fn from(val: HttpMethod) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
GET => "GET",
|
||||
POST => "POST",
|
||||
HEAD => "HEAD",
|
||||
PUT => "PUT",
|
||||
DELETE => "DELETE",
|
||||
CONNECT => "CONNECT",
|
||||
OPTIONS => "OPTIONS",
|
||||
TRACE => "TRACE",
|
||||
PATCH => "PATCH",
|
||||
Unknown => "?",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for HttpMethod {
|
||||
fn from(val: String) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val.as_str() {
|
||||
"GET" => GET,
|
||||
"POST" => POST,
|
||||
"HEAD" => HEAD,
|
||||
"PUT" => PUT,
|
||||
"DELETE" => DELETE,
|
||||
"CONNECT" => CONNECT,
|
||||
"OPTIONS" => OPTIONS,
|
||||
"TRACE" => TRACE,
|
||||
"PATCH" => PATCH,
|
||||
_ => Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpMethod> for String {
|
||||
fn from(val: HttpMethod) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
GET => "GET".to_string(),
|
||||
POST => "POST".to_string(),
|
||||
HEAD => "HEAD".to_string(),
|
||||
PUT => "PUT".to_string(),
|
||||
DELETE => "DELETE".to_string(),
|
||||
CONNECT => "CONNECT".to_string(),
|
||||
OPTIONS => "OPTIONS".to_string(),
|
||||
TRACE => "TRACE".to_string(),
|
||||
PATCH => "PATCH".to_string(),
|
||||
Unknown => "?".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum ResponseStatus {
|
||||
Okay,
|
||||
Created,
|
||||
MovedPermanently {
|
||||
location: String,
|
||||
},
|
||||
SeeOther {
|
||||
location: String,
|
||||
},
|
||||
TemporaryRedirect {
|
||||
location: String,
|
||||
},
|
||||
PermanentRedirect {
|
||||
location: String,
|
||||
},
|
||||
BadRequest,
|
||||
Unauthorized,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
MethodNotAllowed {
|
||||
allow: Vec<HttpMethod>,
|
||||
},
|
||||
UriTooLong,
|
||||
ImATeapot,
|
||||
InternalServerError,
|
||||
NotImplemented,
|
||||
HttpVersionNotSupported,
|
||||
}
|
||||
|
||||
impl ResponseStatus {
|
||||
fn as_code(&self) -> usize {
|
||||
use ResponseStatus::*;
|
||||
match self {
|
||||
Okay => 200,
|
||||
Created => 201,
|
||||
MovedPermanently { .. } => 301,
|
||||
SeeOther { .. } => 303,
|
||||
TemporaryRedirect { .. } => 307,
|
||||
PermanentRedirect { .. } => 308,
|
||||
BadRequest => 400,
|
||||
Unauthorized => 401,
|
||||
Forbidden => 403,
|
||||
NotFound => 404,
|
||||
MethodNotAllowed { .. } => 405,
|
||||
UriTooLong => 414,
|
||||
ImATeapot => 418,
|
||||
InternalServerError => 500,
|
||||
NotImplemented => 501,
|
||||
HttpVersionNotSupported => 505,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_description(&self) -> &'static str {
|
||||
use ResponseStatus::*;
|
||||
match self {
|
||||
Okay => "OK",
|
||||
Created => "Created",
|
||||
MovedPermanently { .. } => "Moved Permanently",
|
||||
SeeOther { .. } => "See Other",
|
||||
TemporaryRedirect { .. } => "Temporary Redirect",
|
||||
PermanentRedirect { .. } => "Permanent Redirect",
|
||||
BadRequest => "Bad Request",
|
||||
Unauthorized => "Unauthorized",
|
||||
Forbidden => "Forbidden",
|
||||
NotFound => "Not Found",
|
||||
MethodNotAllowed { .. } => "Method Not Allowed",
|
||||
UriTooLong => "URI Too Long",
|
||||
ImATeapot => "I'm A Teapot",
|
||||
InternalServerError => "Internal Server Error",
|
||||
NotImplemented => "Not Implemented",
|
||||
HttpVersionNotSupported => "HTTP Version Not Supported",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HttpError {
|
||||
kind: ResponseStatus,
|
||||
}
|
||||
|
||||
impl HttpError {
|
||||
pub fn new(kind: ResponseStatus) -> Self {
|
||||
Self { kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HttpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"HTTP/1.1 {} {}",
|
||||
self.kind.as_code(),
|
||||
self.kind.as_description()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HttpError {}
|
||||
|
||||
impl From<HttpError> for io::Error {
|
||||
fn from(val: HttpError) -> Self {
|
||||
io::Error::other(val.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<httparse::Error> for HttpError {
|
||||
fn from(_: httparse::Error) -> Self {
|
||||
HttpError::new(ResponseStatus::BadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Response<'a> {
|
||||
pub status: ResponseStatus,
|
||||
pub headers: Vec<(&'a str, String)>,
|
||||
pub body: Option<&'a [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> From<Response<'a>> for Vec<u8> {
|
||||
fn from(val: Response<'a>) -> Self {
|
||||
[
|
||||
"HTTP/1.1 ".as_bytes(),
|
||||
val.status.as_code().to_string().as_bytes(),
|
||||
b" ",
|
||||
val.status.as_description().as_bytes(),
|
||||
b"\r\n",
|
||||
&val.headers.into_iter().fold(
|
||||
Default::default(),
|
||||
|mut acc: Vec<u8>, e: (&str, String)| -> Vec<u8> {
|
||||
acc.append(&mut [e.0.as_bytes(), b": ", e.1.as_bytes(), b"\r\n"].concat());
|
||||
acc
|
||||
}
|
||||
),
|
||||
b"\r\n",
|
||||
val.body.unwrap_or_default(),
|
||||
].concat()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<HttpError> for Response<'a> {
|
||||
fn from(err: HttpError) -> Self {
|
||||
let status = err.kind.clone();
|
||||
let headers = match err.kind {
|
||||
ResponseStatus::MovedPermanently { location }
|
||||
| ResponseStatus::SeeOther { location }
|
||||
| ResponseStatus::TemporaryRedirect { location }
|
||||
| ResponseStatus::PermanentRedirect { location } => vec![("location", location)],
|
||||
ResponseStatus::MethodNotAllowed { allow } => vec![(
|
||||
"allow",
|
||||
allow.iter().map(|x| Into::<String>::into(*x)).join(", ")
|
||||
)],
|
||||
_ => vec![],
|
||||
};
|
||||
Response {
|
||||
status,
|
||||
headers,
|
||||
body: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeConfig {
|
||||
bind_address: SocketAddr,
|
||||
read_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
}
|
||||
|
||||
impl FeConfig {
|
||||
pub fn init<A: ToSocketAddrs>(
|
||||
bind_address: A,
|
||||
read_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Ok(FeConfig {
|
||||
bind_address: bind_address.to_socket_addrs()?.collect::<Vec<_>>()[0],
|
||||
read_timeout,
|
||||
write_timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeStorage {
|
||||
listener: TcpListener,
|
||||
// TODO: tera template store
|
||||
}
|
||||
|
||||
impl Frontend<FeStorage, FeConfig> {
|
||||
fn router(
|
||||
method: HttpMethod,
|
||||
path: Utf8UnixPathBuf,
|
||||
params: Option<Vec<(&str, &str)>>,
|
||||
headers: &[httparse::Header],
|
||||
) -> Result<<Frontend<FeStorage, FeConfig> as FrontendImpl<TcpStream>>::Response, Box<dyn Error>> {
|
||||
use HttpMethod::*;
|
||||
use ResponseStatus::*;
|
||||
// unwrapping is safe here because the resource path it came from is a valid UTF-8 &str
|
||||
match (method, path.components().map(|c| c.as_str()).collect::<Vec<&str>>().deref(), params, headers) {
|
||||
(method, ["/", "index.html"], _, _) => {
|
||||
if matches!(method, GET) {
|
||||
Ok(Response {
|
||||
status: ResponseStatus::Okay,
|
||||
headers: vec![("x-test1", "test1".to_string()), ("x-test2", "test2".to_string())],
|
||||
body: Some(b"totally cool and swag homepage"),
|
||||
}.into())
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
||||
}
|
||||
}
|
||||
(method, ["/", "login"], _, _) => {
|
||||
if matches!(method, GET | POST) {
|
||||
todo!()
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET, POST] })))
|
||||
}
|
||||
}
|
||||
// oh how i long for inline const patterns
|
||||
(method, ["/", user], _, _) if let Some(user) = user.strip_prefix('~') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
||||
}
|
||||
}
|
||||
(method, ["/", user, repo], _, _) if let Some(user) = user.strip_prefix('~') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
||||
}
|
||||
}
|
||||
(method, ["/", project], _, _) if let Some(project) = project.strip_prefix('+') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
||||
}
|
||||
}
|
||||
(method, ["/", project, repo], _, _) if let Some(project) = project.strip_prefix('+') => {
|
||||
if matches!(method, GET) {
|
||||
todo!()
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
||||
}
|
||||
}
|
||||
_ => Err(Box::new(HttpError::new(ResponseStatus::NotFound))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Frontend<FeStorage, FeConfig> {
|
||||
type Item = Incoming<'static>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FrontendImpl<TcpStream> for Frontend<FeStorage, FeConfig> {
|
||||
type FeConfig = FeConfig;
|
||||
type Request = TcpStream;
|
||||
type Response = Vec<u8>;
|
||||
|
||||
fn init(config: FeConfig) -> Self {
|
||||
// TODO: load tera templates into FeStorage
|
||||
Frontend {
|
||||
storage: self::FeStorage {
|
||||
listener: TcpListener::bind(config.bind_address).unwrap_or_else(|e| {
|
||||
eyap!(&e);
|
||||
exit(1)
|
||||
}),
|
||||
},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(&self, subj: Self::Request) -> Result<Self::Response, Box<dyn Error>> {
|
||||
subj.set_read_timeout(Some(self.config.read_timeout))
|
||||
.and_then(|_| subj.set_write_timeout(Some(self.config.write_timeout)))?;
|
||||
|
||||
let stream_read = BufReader::new(subj);
|
||||
let mut headers = [httparse::EMPTY_HEADER; 32];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
let buf: &mut Vec<u8> = &mut vec![];
|
||||
|
||||
// TODO: validate more of the request before sending to the router
|
||||
stream_read.take(8192).read_until(b'\n', buf)?;
|
||||
let res = req.parse(buf);
|
||||
|
||||
Ok(match (res, req) {
|
||||
|
||||
// Presumably well-formed enough to get sent off to the route handler
|
||||
(
|
||||
Ok(httparse::Status::Partial),
|
||||
httparse::Request {
|
||||
method: Some(method),
|
||||
path: Some(path),
|
||||
version: Some(1),
|
||||
headers,
|
||||
},
|
||||
) => {
|
||||
// separate path containing get params into path and kv vec
|
||||
let (path, params) = path.split_once("?").map_or_else(
|
||||
|| (Utf8UnixPathBuf::from_str(path).unwrap(), None),
|
||||
|(path, args)| {
|
||||
(
|
||||
Utf8UnixPathBuf::from_str(path).unwrap(),
|
||||
Some(
|
||||
args.split('&')
|
||||
.filter_map(|e| e.split_once('='))
|
||||
.collect::<Vec<(&str, &str)>>(),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
if path.is_absolute() {
|
||||
// context-valid lexical normalization without da feature
|
||||
let path = Utf8UnixPathBuf::from_iter(path.components().try_fold(Vec::<Utf8UnixComponent>::new(), |mut acc, item| -> Result<Vec<Utf8UnixComponent<'_>>, Box<dyn Error>> {
|
||||
match item {
|
||||
Utf8UnixComponent::CurDir => Ok(acc),
|
||||
Utf8UnixComponent::RootDir => {acc.push(item); Ok(acc)},
|
||||
Utf8UnixComponent::Normal(_) => {acc.push(item); Ok(acc)},
|
||||
Utf8UnixComponent::ParentDir => {acc.pop_if(|c| c != &Utf8UnixComponent::RootDir); Ok(acc)},
|
||||
}
|
||||
})?);
|
||||
|
||||
Self::router(method.into(), path, params, headers)
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(ResponseStatus::BadRequest)) as Box<dyn Error>)
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed request lines and HTTP/1.1 requests without a Host header
|
||||
(Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => {
|
||||
Err(Box::new(HttpError::new(ResponseStatus::BadRequest)) as Box<dyn Error>)
|
||||
}
|
||||
|
||||
// Fatal parsing error; obvious bad request
|
||||
(Err(e), _) => Err(Box::new(e) as Box<dyn Error>),
|
||||
}?)
|
||||
}
|
||||
|
||||
fn handle_error(&mut self, res: Result<Self::Response, Box<dyn Error>>) -> Vec<u8> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,12 @@
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use std::{error::Error, time::Duration};
|
||||
use std::{error::Error, thread::available_parallelism, time::Duration};
|
||||
|
||||
mod server;
|
||||
use server::Pool;
|
||||
use futures::{self, channel::mpsc, executor::ThreadPool};
|
||||
|
||||
mod manager;
|
||||
use manager::Pool;
|
||||
|
||||
|
||||
mod http_fe;
|
||||
@@ -29,12 +31,32 @@ use http_fe::FrontendImpl;
|
||||
|
||||
mod gem_fe;
|
||||
|
||||
mod yapper;
|
||||
// mod util;
|
||||
// use crate::;
|
||||
use mintee::util::yapper::eyap;
|
||||
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let http_fe = http_fe::Frontend::init(http_fe::FeConfig::init("0.0.0.0:8080", Duration::new(2, 0), Duration::new(2, 0))?);
|
||||
// let http_fe = http_fe::Frontend::init(http_fe::FeConfig::init("0.0.0.0:8080", Duration::new(2, 0), Duration::new(2, 0))?);
|
||||
|
||||
let pool = Pool::<32>::new();
|
||||
// let pool = Pool::<32>::new();
|
||||
|
||||
|
||||
let pool = ThreadPool::builder()
|
||||
.pool_size(available_parallelism()?.into()) // TODO: or optional value from config
|
||||
.name_prefix("mintfe-worker:")
|
||||
.after_start(|i| {
|
||||
eyap!("Spawned mintfe-worker:{i}");
|
||||
})
|
||||
.before_stop(|i| {
|
||||
eyap!("Stopping mintfe-worker:{i}");
|
||||
})
|
||||
.create()?;
|
||||
|
||||
// let (tx, rx) = mpsc::unbounded::<todo!()>();
|
||||
|
||||
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 silt <silt@tebibyte.media>
|
||||
* Copyright (c) 2025, 2026 silt <silt@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
@@ -24,8 +24,9 @@ use std::{
|
||||
io::{Read, Write},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use futures::executor;
|
||||
|
||||
use crate::{eyap, yap};
|
||||
use mintee::util::yapper::{yap, eyap};
|
||||
|
||||
pub struct Frontend<S, C> {
|
||||
/// Holds data necessary for and private to the implementor.
|
||||
@@ -35,15 +36,15 @@ pub struct Frontend<S, C> {
|
||||
pub config: C,
|
||||
}
|
||||
|
||||
pub trait FrontendImpl: Iterator {
|
||||
pub trait FrontendImpl<Request>: Iterator where Request: Read, Self::Response: Into<Vec<u8>> {
|
||||
type FeConfig: ?Sized;
|
||||
type RequestSubject: Read;
|
||||
type ReplySubject: Write;
|
||||
type Request: Read;
|
||||
type Response: Write;
|
||||
fn init(storage: Self::FeConfig) -> Self;
|
||||
fn handle_request(&self, subj: Self::RequestSubject) -> Result<(), Box<dyn Error>>;
|
||||
fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box<dyn Error>>;
|
||||
// NOTE: handle_request.or_else(handle_error)
|
||||
fn handle_error(&self, res: Result<(), Box<dyn Error>>);
|
||||
fn handle_request(&self, subj: Request) -> Result<Self::Response, Box<dyn Error>>;
|
||||
// fn send_reply(&self, subj: Self::Response) -> Result<(), Box<dyn Error>>;
|
||||
// NOTE: handle_request().or_else(handle_error())
|
||||
fn handle_error(&mut self, res: Result<Self::Response, Box<dyn Error>>) -> Self::Response;
|
||||
}
|
||||
|
||||
// TODO: split frontend management code and Frontend trait stuff into diff files
|
||||
91
src/bin/test_render.rs
Normal file
91
src/bin/test_render.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2025–2026 Emma Tebibyte <emma@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
env::{ args, current_dir },
|
||||
error::Error,
|
||||
};
|
||||
|
||||
use gix::open;
|
||||
use tera::{ Context, Tera };
|
||||
|
||||
use mintee::backend::git::repo_to_context;
|
||||
|
||||
/*
|
||||
impl From<GitRepo> for Context {
|
||||
fn from(repo: GitRepo) -> Self {
|
||||
let mut ctx = Context::new();
|
||||
|
||||
let directory = format!("{}/{}", repo.owner, repo.name);
|
||||
|
||||
let main_branch = repo.branches
|
||||
.iter()
|
||||
.find(|b| b.name == "main")
|
||||
.unwrap();
|
||||
|
||||
let latest_commit = &main_branch.commits[0];
|
||||
let hash = latest_commit.hash.clone();
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for e in latest_commit.entries.iter() {
|
||||
let entry = SampleEntry {
|
||||
class: e.kind.to_string(),
|
||||
last_commit: hash.clone(),
|
||||
last_commit_message: latest_commit.message.clone().unwrap(),
|
||||
last_commit_time: latest_commit.time,
|
||||
path: e.path.clone(),
|
||||
};
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
/* stubs til we have a real database */
|
||||
ctx.insert("user", "anon");
|
||||
ctx.insert("site", "TiB.");
|
||||
ctx.insert("notif_count", "");
|
||||
ctx.insert("ticket_count", "(47)");
|
||||
|
||||
ctx.insert("readme_content", "this is a readme");
|
||||
|
||||
ctx.insert("owner", &repo.owner);
|
||||
ctx.insert("branch", &main_branch.name);
|
||||
ctx.insert("repo", &repo.name);
|
||||
ctx.insert("directory", &directory);
|
||||
ctx.insert("entries", &entries);
|
||||
|
||||
ctx
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
if let Some(templates) = args().collect::<Vec<_>>().get(1) {
|
||||
let tera = Tera::new(templates.as_str())?;
|
||||
let ctx = repo_to_context(open(current_dir()?)?)?;
|
||||
|
||||
println!("{}", tera.render("code.html", &ctx)?);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
eprintln!("Usage: {} template_glob", args().collect::<Vec<_>>()[0]);
|
||||
std::process::exit(64);
|
||||
}
|
||||
}
|
||||
247
src/git.rs
247
src/git.rs
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Emma Tebibyte <emma@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021 Sergey "Shnatsel" Davidoff <shnatsel@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use git2::{ Branch, Commit, FileMode, Repository, Sort, TreeEntry };
|
||||
use tera::Context;
|
||||
|
||||
/* TODO: implement From<T> for Context where T: all Git* types */
|
||||
|
||||
pub enum GitEntryKind {
|
||||
Directory,
|
||||
File,
|
||||
Link,
|
||||
}
|
||||
|
||||
impl ToString for GitEntryKind {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
GitEntryKind::Directory => "directory".to_owned(),
|
||||
GitEntryKind::File => "file".to_owned(),
|
||||
GitEntryKind::Link => "link".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitEntry {
|
||||
pub entries: Vec<GitEntry>,
|
||||
pub kind: GitEntryKind,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl GitEntry {
|
||||
fn new(entries: Vec<GitEntry>, kind: GitEntryKind, path: String) -> Self {
|
||||
GitEntry { entries, kind, path }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TreeEntry<'_>> for GitEntry {
|
||||
type Error = std::string::FromUtf8Error;
|
||||
|
||||
fn try_from(entry: TreeEntry) -> Result<Self, Self::Error> {
|
||||
let entries = Vec::new();
|
||||
let path = String::from_utf8(entry.name_bytes().to_vec())?;
|
||||
let kind = match entry.filemode() {
|
||||
m if m == <i32>::from(FileMode::Blob) => GitEntryKind::File,
|
||||
m if m == <i32>::from(FileMode::BlobExecutable) => GitEntryKind::File,
|
||||
m if m == <i32>::from(FileMode::BlobGroupWritable) => unreachable!(),
|
||||
m if m == <i32>::from(FileMode::Link) => GitEntryKind::File,
|
||||
m if m == <i32>::from(FileMode::Tree) => GitEntryKind::Directory,
|
||||
m if m == <i32>::from(FileMode::Commit) => GitEntryKind::File,
|
||||
m if m == <i32>::from(FileMode::Unreadable) => todo!(),
|
||||
_ => GitEntryKind::Directory,
|
||||
};
|
||||
|
||||
Ok(Self::new(entries, kind, path))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitCommit {
|
||||
pub author: (Option<String>, Option<String>), // name, e-mail
|
||||
pub entries: Vec<GitEntry>,
|
||||
pub hash: String,
|
||||
pub message: Option<String>,
|
||||
pub short_hash: Option<String>,
|
||||
pub time: i64, // seconds since Unix epoch
|
||||
}
|
||||
|
||||
impl From<GitCommit> for Context {
|
||||
fn from(value: GitCommit) -> Self {
|
||||
let mut ctx = Context::new();
|
||||
|
||||
todo!("proc_macro for populating");
|
||||
|
||||
ctx
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Commit<'_>> for GitCommit {
|
||||
type Error = Box<dyn Error>;
|
||||
|
||||
fn try_from(commit: Commit) -> Result<Self, Self::Error> {
|
||||
let hash = commit.id().to_string();
|
||||
let short_hash = commit.as_object().short_id()?.as_str().map(|f| f.to_owned());
|
||||
let message = commit.message().map(|f| f.to_owned());
|
||||
let commit_signature = commit.author();
|
||||
let name = commit_signature.name().map(|f| f.to_owned());
|
||||
let address = commit_signature.email().map(|f| f.to_owned());
|
||||
let author = (name, address);
|
||||
let time = commit.time().seconds();
|
||||
let entries = commit
|
||||
.tree()?
|
||||
.iter()
|
||||
.map(|c| -> Result<GitEntry, std::string::FromUtf8Error> {
|
||||
Ok(GitEntry::try_from(c)?)
|
||||
}).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(GitCommit { author, entries, hash, message, short_hash, time })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitBranch {
|
||||
pub commits: Vec<GitCommit>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
struct GitBranchWrapper<'a> {
|
||||
branch: Branch<'a>,
|
||||
repo: &'a Repository,
|
||||
}
|
||||
|
||||
impl<'a> GitBranchWrapper<'a> {
|
||||
fn new(branch: Branch<'a>, repo: &'a Repository) -> Self {
|
||||
Self { branch, repo }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<GitBranchWrapper<'_>> for GitBranch {
|
||||
type Error = Box<dyn Error>;
|
||||
|
||||
fn try_from(branch: GitBranchWrapper) -> Result<Self, Self::Error> {
|
||||
let name = String::from_utf8(branch.branch.name_bytes()?.to_vec())?;
|
||||
let repo = branch.repo;
|
||||
let branch_oid = branch.branch.get().resolve()?.target().unwrap();
|
||||
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
|
||||
revwalk.push(branch_oid)?;
|
||||
|
||||
let commits = revwalk
|
||||
.into_iter()
|
||||
.map(|o| -> Result<GitCommit, Box<dyn Error>> {
|
||||
Ok(GitCommit::try_from(repo.find_commit(o?)?)?)
|
||||
}).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(GitBranch { commits, name })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct GitRepo {
|
||||
pub branches: Vec<GitBranch>,
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
impl GitRepo {
|
||||
pub fn open(
|
||||
path: PathBuf, prefix: Option<String>
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let repo = Repository::open(path.clone())?;
|
||||
|
||||
let branches = repo
|
||||
.branches(None)?
|
||||
.map(|b| -> Result<GitBranch, Box<dyn Error>> {
|
||||
Ok(GitBranch::try_from(GitBranchWrapper::new(b?.0, &repo))?)
|
||||
}).collect::<Result<Vec<_>, _>>()?;
|
||||
/*
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
|
||||
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::TIME)?;
|
||||
revwalk.push_head()?;
|
||||
let commits = revwalk
|
||||
.into_iter()
|
||||
.map(|o| -> Result<GitCommit, Box<dyn Error>> {
|
||||
Ok(GitCommit::try_from(repo.find_commit(o?)?)?)
|
||||
}).collect::<Result<Vec<_>, _>>()?;
|
||||
*/
|
||||
|
||||
let full_path = path.clone().as_path().canonicalize()?;
|
||||
let relative_path = match prefix {
|
||||
Some(p) => full_path.strip_prefix(p)?.canonicalize()?,
|
||||
None => full_path,
|
||||
};
|
||||
|
||||
let owner = String::from_utf8(
|
||||
relative_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf()
|
||||
.as_mut_os_string()
|
||||
.as_encoded_bytes()
|
||||
.to_vec()
|
||||
)?;
|
||||
|
||||
let name = String::from_utf8(
|
||||
relative_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.as_encoded_bytes()
|
||||
.to_vec()
|
||||
)?;
|
||||
|
||||
Ok(GitRepo { branches, name, owner })
|
||||
}
|
||||
|
||||
pub fn get_remote_repo() { // for AP repos
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
332
src/http_fe.rs
332
src/http_fe.rs
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 silt <silt@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use httparse;
|
||||
use std::{
|
||||
error::Error, fmt, io::{self, BufRead, BufReader, Read}, net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, ops::Deref, path::{Component, PathBuf}, process::exit, str::FromStr, time::Duration
|
||||
};
|
||||
|
||||
use crate::{eyap, yap};
|
||||
|
||||
pub use super::server::{Frontend, FrontendImpl};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
HEAD,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for HttpMethod {
|
||||
type Error = HttpError;
|
||||
fn try_from(val: &str) -> Result<Self, Self::Error> {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
"GET" => Ok(GET),
|
||||
"POST" => Ok(POST),
|
||||
"HEAD" => Ok(HEAD),
|
||||
_ => Err(HttpError {
|
||||
kind: ResponseCode::MethodNotAllowed,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpMethod> for &str {
|
||||
fn from(val: HttpMethod) -> Self {
|
||||
use HttpMethod::*;
|
||||
match val {
|
||||
GET => "GET",
|
||||
POST => "POST",
|
||||
HEAD => "HEAD",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum ResponseCode {
|
||||
Okay,
|
||||
Created,
|
||||
MovedPermanently,
|
||||
SeeOther,
|
||||
TemporaryRedirect,
|
||||
PermanentRedirect,
|
||||
BadRequest,
|
||||
Unauthorized,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
MethodNotAllowed,
|
||||
UriTooLong,
|
||||
ImATeapot,
|
||||
InternalServerError,
|
||||
NotImplemented,
|
||||
HttpVersionNotSupported,
|
||||
}
|
||||
|
||||
impl From<ResponseCode> for usize {
|
||||
fn from(val: ResponseCode) -> Self {
|
||||
use ResponseCode::*;
|
||||
match val {
|
||||
Okay => 200,
|
||||
Created => 201,
|
||||
MovedPermanently => 301,
|
||||
SeeOther => 303,
|
||||
TemporaryRedirect => 307,
|
||||
PermanentRedirect => 308,
|
||||
BadRequest => 400,
|
||||
Unauthorized => 401,
|
||||
Forbidden => 403,
|
||||
NotFound => 404,
|
||||
MethodNotAllowed => 405,
|
||||
UriTooLong => 414,
|
||||
ImATeapot => 418,
|
||||
InternalServerError => 500,
|
||||
NotImplemented => 501,
|
||||
HttpVersionNotSupported => 505,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseCode> for &str {
|
||||
fn from(val: ResponseCode) -> Self {
|
||||
use ResponseCode::*;
|
||||
match val {
|
||||
Okay => "OK",
|
||||
Created => "Created",
|
||||
MovedPermanently => "Moved Permanently",
|
||||
SeeOther => "See Other",
|
||||
TemporaryRedirect => "Temporary Redirect",
|
||||
PermanentRedirect => "Permanent Redirect",
|
||||
BadRequest => "Bad Request",
|
||||
Unauthorized => "Unauthorized",
|
||||
Forbidden => "Forbidden",
|
||||
NotFound => "NotFound",
|
||||
MethodNotAllowed => "Method Not Allowed",
|
||||
UriTooLong => "URI Too Long",
|
||||
ImATeapot => "I'm A Teapot",
|
||||
InternalServerError => "Internal Server Error",
|
||||
NotImplemented => "Not Implemented",
|
||||
HttpVersionNotSupported => "HTTP Version Not Supported",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HttpError {
|
||||
kind: ResponseCode,
|
||||
}
|
||||
|
||||
impl HttpError {
|
||||
pub fn new(kind: ResponseCode) -> Self {
|
||||
Self { kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HttpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"HTTP/1.1 {} {}",
|
||||
Into::<usize>::into(self.kind),
|
||||
Into::<&str>::into(self.kind)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HttpError {}
|
||||
|
||||
impl From<HttpError> for io::Error {
|
||||
fn from(val: HttpError) -> Self {
|
||||
io::Error::other(val.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<httparse::Error> for HttpError {
|
||||
fn from(value: httparse::Error) -> Self {
|
||||
HttpError {
|
||||
kind: match value {
|
||||
_ => ResponseCode::BadRequest,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeConfig {
|
||||
bind_address: SocketAddr,
|
||||
read_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
}
|
||||
|
||||
impl FeConfig {
|
||||
pub fn init<A: ToSocketAddrs>(
|
||||
bind_address: A,
|
||||
read_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Ok(FeConfig {
|
||||
bind_address: bind_address.to_socket_addrs()?.collect::<Vec<_>>()[0],
|
||||
read_timeout,
|
||||
write_timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeStorage {
|
||||
listener: TcpListener,
|
||||
// TODO: tera template store
|
||||
}
|
||||
|
||||
impl Frontend<FeStorage, FeConfig> {
|
||||
fn router(
|
||||
method: HttpMethod,
|
||||
path: PathBuf,
|
||||
params: Option<Vec<(&str, &str)>>,
|
||||
headers: &[httparse::Header],
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
use HttpMethod::*;
|
||||
// unwrapping is safe here because the resource path it came from is a valid UTF-8 &str
|
||||
match (method, path.components().map(|c| c.as_os_str().to_str().unwrap()).collect::<Vec<&str>>().deref(), params, headers) {
|
||||
(GET, ["/", "index.html"], _, _) => {
|
||||
todo!()
|
||||
}
|
||||
(GET, ["/", "login.html"], _, _) => {
|
||||
todo!()
|
||||
}
|
||||
// can be cleaned up with rust/rust-lang #76001 or maybe #87599
|
||||
(GET, ["/", user], _, _) if user.starts_with('~') => {
|
||||
todo!()
|
||||
}
|
||||
(GET, ["/", user, repo], _, _) if user.starts_with('~') => {
|
||||
todo!()
|
||||
}
|
||||
(GET, ["/", project], _, _) if project.starts_with('+') => {
|
||||
todo!()
|
||||
}
|
||||
(GET, ["/", project, repo], _, _) if project.starts_with('+') => {
|
||||
todo!()
|
||||
}
|
||||
_ => Err(Box::new(HttpError::new(ResponseCode::NotFound))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Frontend<FeStorage, FeConfig> {
|
||||
type Item = Incoming<'static>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FrontendImpl for Frontend<FeStorage, FeConfig> {
|
||||
type FeConfig = FeConfig;
|
||||
type RequestSubject = TcpStream;
|
||||
type ReplySubject = TcpStream;
|
||||
|
||||
fn init(config: FeConfig) -> Self {
|
||||
// TODO: load tera templates into FeStorage
|
||||
Frontend {
|
||||
storage: self::FeStorage {
|
||||
listener: TcpListener::bind(config.bind_address).unwrap_or_else(|e| {
|
||||
eyap!(&e);
|
||||
exit(1)
|
||||
}),
|
||||
},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(&self, subj: Self::RequestSubject) -> Result<(), Box<dyn Error>> {
|
||||
subj.set_read_timeout(Some(self.config.read_timeout))
|
||||
.and_then(|_| subj.set_write_timeout(Some(self.config.write_timeout)))?;
|
||||
|
||||
let stream_read = BufReader::new(subj);
|
||||
let mut headers = [httparse::EMPTY_HEADER; 32];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
let buf: &mut Vec<u8> = &mut vec![];
|
||||
|
||||
// TODO: parse the rest of the request before sending to the router
|
||||
stream_read.take(8192).read_until(b'\n', buf)?;
|
||||
let res = req.parse(buf);
|
||||
|
||||
match (res, req) {
|
||||
// Presumably well-formed enough to get sent off to the route handler
|
||||
(
|
||||
Ok(httparse::Status::Partial),
|
||||
httparse::Request {
|
||||
method: Some(method),
|
||||
path: Some(path),
|
||||
version: Some(1),
|
||||
headers,
|
||||
},
|
||||
) => {
|
||||
// separate path containing get params into path and kv vec
|
||||
let (path, params) = path.split_once("?").map_or_else(
|
||||
|| (PathBuf::from_str(path).unwrap(), None),
|
||||
|(path, args)| {
|
||||
(
|
||||
PathBuf::from_str(path).unwrap(),
|
||||
Some(
|
||||
args.split('&')
|
||||
.filter_map(|e| e.split_once('='))
|
||||
.collect::<Vec<(&str, &str)>>(),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
if path.is_absolute() {
|
||||
// context-valid lexical normalization without da feature
|
||||
let path = PathBuf::from_iter(path.components().try_fold(Vec::<Component>::new(), |mut acc, item| -> Result<Vec<Component<'_>>, Box<dyn Error>> {
|
||||
match item {
|
||||
Component::CurDir => Ok(acc),
|
||||
Component::RootDir => {acc.push(item); Ok(acc)},
|
||||
Component::Normal(_) => {acc.push(item); Ok(acc)},
|
||||
Component::ParentDir => {acc.pop_if(|c| c != &Component::RootDir); Ok(acc)},
|
||||
Component::Prefix(_) => Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box<dyn Error>),
|
||||
}
|
||||
})?);
|
||||
Self::router(method.try_into()?, path, params, headers)
|
||||
} else {
|
||||
Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box<dyn Error>)
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed request lines and HTTP/1.1 requests without a Host header
|
||||
(Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => {
|
||||
Err(Box::new(HttpError::new(ResponseCode::BadRequest)) as Box<dyn Error>)
|
||||
}
|
||||
|
||||
// Fatal parsing error; obvious bad request
|
||||
(Err(e), _) => Err(Box::new(e) as Box<dyn Error>),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_reply(&self, subj: Self::ReplySubject) -> Result<(), Box<dyn Error>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn handle_error(&self, res: Result<(), Box<dyn Error>>) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod backend;
|
||||
pub mod util;
|
||||
1
src/util/mod.rs
Normal file
1
src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod yapper;
|
||||
114
src/util/yapper.rs
Normal file
114
src/util/yapper.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2025 silt <silt@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
pub mod _inner {
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! _yap_inner {
|
||||
($m:expr) => {{
|
||||
println!("[{}:{}] {}", file!(), line!(), $m);
|
||||
}};
|
||||
}
|
||||
pub use _yap_inner;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! _eyap_inner {
|
||||
($m:expr) => {{
|
||||
eprintln!("[{}:{}] {}", file!(), line!(), $m);
|
||||
}};
|
||||
}
|
||||
pub use _eyap_inner;
|
||||
}
|
||||
|
||||
macro_rules! mk_yap {() => (
|
||||
#[macro_export]
|
||||
macro_rules! yap {
|
||||
($m:ident) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_yap_inner!(format!("{}", $m));
|
||||
v
|
||||
}};
|
||||
($f:literal , $m:ident) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_yap_inner!(format!($f, $m));
|
||||
v
|
||||
}};
|
||||
|
||||
($m:literal) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_yap_inner!(format!($m));
|
||||
v
|
||||
}};
|
||||
|
||||
($f:literal , $m:expr) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_yap_inner!(format!($f, v));
|
||||
v
|
||||
}};
|
||||
($m:expr) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_yap_inner!(format!("{}", v));
|
||||
v
|
||||
}};
|
||||
}
|
||||
|
||||
pub use yap;
|
||||
)}
|
||||
|
||||
mk_yap!();
|
||||
|
||||
|
||||
macro_rules! mk_eyap {() => (
|
||||
#[macro_export]
|
||||
macro_rules! eyap {
|
||||
($m:ident) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_eyap_inner!(format!("{}", $m));
|
||||
v
|
||||
}};
|
||||
($f:literal , $m:ident) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_eyap_inner!(format!($f, $m));
|
||||
v
|
||||
}};
|
||||
|
||||
($m:literal) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_eyap_inner!(format!($m));
|
||||
v
|
||||
}};
|
||||
|
||||
($f:literal , $m:expr) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_eyap_inner!(format!($f, v));
|
||||
v
|
||||
}};
|
||||
($m:expr) => {{
|
||||
let v = $m;
|
||||
$crate::util::yapper::_inner::_eyap_inner!(format!("{}", v));
|
||||
v
|
||||
}};
|
||||
}
|
||||
|
||||
pub use eyap;
|
||||
)}
|
||||
|
||||
mk_eyap!();
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 silt <silt@tebibyte.media>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This file is part of Mintee.
|
||||
*
|
||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
pub mod _inner {
|
||||
macro_rules! _yap_inner {
|
||||
($m:expr) => {{
|
||||
println!("[{}:{}] {}", file!(), line!(), $m);
|
||||
}};
|
||||
}
|
||||
pub(crate) use _yap_inner;
|
||||
|
||||
macro_rules! _eyap_inner {
|
||||
($m:expr) => {{
|
||||
println!("[{}:{}] {}", file!(), line!(), $m);
|
||||
}};
|
||||
}
|
||||
pub(crate) use _eyap_inner;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! yap {
|
||||
($m:ident) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_yap_inner!(format!("{}", $m));
|
||||
v
|
||||
}};
|
||||
($f:literal , $m:ident) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_yap_inner!(format!($f, $m));
|
||||
v
|
||||
}};
|
||||
|
||||
($m:literal) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_yap_inner!(format!($m));
|
||||
v
|
||||
}};
|
||||
|
||||
($f:literal , $m:expr) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_yap_inner!(format!($f, v));
|
||||
v
|
||||
}};
|
||||
($m:expr) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_yap_inner!(format!("{}", v));
|
||||
v
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! eyap {
|
||||
($m:ident) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_eyap_inner!(format!("{}", $m));
|
||||
v
|
||||
}};
|
||||
($f:literal , $m:ident) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_eyap_inner!(format!($f, $m));
|
||||
v
|
||||
}};
|
||||
|
||||
($m:literal) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_eyap_inner!(format!($m));
|
||||
v
|
||||
}};
|
||||
|
||||
($f:literal , $m:expr) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_eyap_inner!(format!($f, v));
|
||||
v
|
||||
}};
|
||||
($m:expr) => {{
|
||||
let v = $m;
|
||||
crate::yapper::_inner::_eyap_inner!(format!("{}", v));
|
||||
v
|
||||
}};
|
||||
}
|
||||
Reference in New Issue
Block a user