summary refs log tree commit diff stats
path: root/pkgs/by-name/ba/back/src
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ba/back/src')
-rw-r--r--pkgs/by-name/ba/back/src/issues/format/mod.rs88
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue/mod.rs328
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue/raw.rs145
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue_show.rs34
-rw-r--r--pkgs/by-name/ba/back/src/issues/mod.rs129
-rw-r--r--pkgs/by-name/ba/back/src/main.rs49
6 files changed, 773 insertions, 0 deletions
diff --git a/pkgs/by-name/ba/back/src/issues/format/mod.rs b/pkgs/by-name/ba/back/src/issues/format/mod.rs
new file mode 100644
index 0000000..f78d3b3
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/issues/format/mod.rs
@@ -0,0 +1,88 @@
+// Back - An extremely simple git issue tracking system. Inspired by tvix's
+// panettone
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This file is part of Back.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+use std::fmt::Display;
+
+use markdown::to_html;
+
+#[derive(Debug, Default, Clone)]
+pub struct Markdown {
+    value: String,
+}
+
+impl From<String> for Markdown {
+    fn from(value: String) -> Self {
+        Self { value }
+    }
+}
+impl Display for Markdown {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(to_html(&self.value).as_str())
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct BackString {
+    value: String,
+}
+
+impl From<Markdown> for BackString {
+    fn from(value: Markdown) -> Self {
+        Self { value: value.value }
+    }
+}
+
+impl From<String> for BackString {
+    fn from(value: String) -> Self {
+        Self { value }
+    }
+}
+impl Display for BackString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(escape_html(&self.value).as_str())
+    }
+}
+
+// From `tera::escape_html`
+/// Escape HTML following [OWASP](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)
+///
+/// Escape the following characters with HTML entity encoding to prevent switching
+/// into any execution context, such as script, style, or event handlers. Using
+/// hex entities is recommended in the spec. In addition to the 5 characters
+/// significant in XML (&, <, >, ", '), the forward slash is included as it helps
+/// to end an HTML entity.
+///
+/// ```text
+/// & --> &amp;
+/// < --> &lt;
+/// > --> &gt;
+/// " --> &quot;
+/// ' --> &#x27;     &apos; is not recommended
+/// / --> &#x2F;     forward slash is included as it helps end an HTML entity
+/// ```
+#[inline]
+pub fn escape_html(input: &str) -> String {
+    let mut output = String::with_capacity(input.len() * 2);
+    for c in input.chars() {
+        match c {
+            '&' => output.push_str("&amp;"),
+            '<' => output.push_str("&lt;"),
+            '>' => output.push_str("&gt;"),
+            '"' => output.push_str("&quot;"),
+            '\'' => output.push_str("&#x27;"),
+            '/' => output.push_str("&#x2F;"),
+            _ => output.push(c),
+        }
+    }
+
+    // Not using shrink_to_fit() on purpose
+    output
+}
diff --git a/pkgs/by-name/ba/back/src/issues/issue/mod.rs b/pkgs/by-name/ba/back/src/issues/issue/mod.rs
new file mode 100644
index 0000000..ada7593
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/issues/issue/mod.rs
@@ -0,0 +1,328 @@
+// Back - An extremely simple git issue tracking system. Inspired by tvix's
+// panettone
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This file is part of Back.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+use std::fmt::Display;
+
+use chrono::DateTime;
+use gix::{bstr::ByteSlice, Commit, Id, ObjectId, Repository};
+use raw::{Operation, RawIssue};
+use rocket::response::content::RawHtml;
+
+use super::format::{BackString, Markdown};
+
+mod raw;
+
+#[derive(Debug, Default)]
+pub struct TimeStamp {
+    value: u64,
+}
+impl TimeStamp {
+    pub fn new(val: u64) -> Self {
+        Self { value: val }
+    }
+}
+impl Display for TimeStamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let date =
+            DateTime::from_timestamp(self.value as i64, 0).expect("This timestamp should be vaild");
+
+        let newdate = date.format("%Y-%m-%d %H:%M:%S");
+        f.write_str(newdate.to_string().as_str())
+    }
+}
+
+#[derive(Debug)]
+pub struct Comment<'a> {
+    pub id: Id<'a>,
+    pub author: Author,
+    pub message: Markdown,
+    pub timestamp: TimeStamp,
+}
+
+#[derive(Debug, Default)]
+pub struct Author {
+    name: BackString,
+    email: BackString,
+}
+
+#[derive(Debug)]
+pub struct IssueId<'a> {
+    value: Id<'a>,
+}
+impl<'a> IssueId<'a> {
+    pub fn new(id: Id<'a>) -> Self {
+        Self { value: id }
+    }
+}
+impl Display for IssueId<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let shortend = self.value.shorten().expect("This should work.");
+        f.write_str(shortend.to_string().as_str())
+    }
+}
+
+#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
+pub enum Status {
+    #[default]
+    Open,
+    Closed,
+}
+impl Display for Status {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Status::Open => f.write_str("Open"),
+            Status::Closed => f.write_str("Closed"),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct Issue<'a> {
+    pub id: IssueId<'a>,
+    pub author: Author,
+    pub timestamp: TimeStamp,
+    pub title: Markdown,
+    pub message: Markdown,
+    pub comments: Vec<Comment<'a>>,
+    pub status: Status,
+    pub last_status_change: Option<TimeStamp>,
+}
+impl<'a> Issue<'a> {
+    pub fn default_with_id(id: Id<'a>) -> Self {
+        Self {
+            id: IssueId::new(id),
+            author: Author::default(),
+            timestamp: TimeStamp::default(),
+            title: Markdown::default(),
+            message: Markdown::default(),
+            comments: <Vec<Comment>>::default(),
+            status: Status::default(),
+            last_status_change: <Option<TimeStamp>>::default(),
+        }
+    }
+
+    pub fn from_commit_id(repo: &'a Repository, commit_id: ObjectId) -> Self {
+        fn unwrap_id<'b>(repo: &Repository, id: &Commit<'b>) -> (RawIssue, Id<'b>) {
+            let tree_obj = repo
+                .find_object(id.tree_id().unwrap())
+                .expect("The object with this id should exist.")
+                .try_into_tree()
+                .expect("The git-bug's data model enforces this.");
+
+            let ops_ref = tree_obj.find_entry("ops").unwrap();
+
+            let issue_data = repo
+                .find_object(ops_ref.object_id())
+                .expect("The object with this id should exist.")
+                .try_into_blob()
+                .expect("The git-bug's data model enforces this.")
+                .data
+                .clone();
+
+            let raw_issue = serde_json::from_str(
+                issue_data
+                    .to_str()
+                    .expect("git-bug's ensures, that this is valid json."),
+            )
+            .expect("The returned json should be valid");
+
+            (raw_issue, id.id())
+        }
+
+        let commit_obj = repo
+            .find_object(commit_id)
+            .expect("The object with this id should exist.")
+            .try_into_commit()
+            .expect("The git-bug's data model enforces this.");
+
+        let mut issues = vec![unwrap_id(repo, &commit_obj)];
+
+        let mut current_commit_obj = commit_obj;
+        while current_commit_obj.parent_ids().count() != 0 {
+            assert_eq!(
+                current_commit_obj.parent_ids().count(),
+                1,
+                "There should be only one parent"
+            );
+            let parent = current_commit_obj
+                .parent_ids()
+                .last()
+                .expect("One does exist");
+
+            let parent_id = parent.object().expect("The object exists").id;
+            let parent_commit = repo
+                .find_object(parent_id)
+                .expect("This is a valid id")
+                .try_into_commit()
+                .expect("This should be a commit");
+
+            issues.push(unwrap_id(repo, &parent_commit));
+            current_commit_obj = parent_commit;
+        }
+
+        let mut final_issue = Self::default_with_id(current_commit_obj.id());
+        for (issue, id) in issues {
+            for op in issue.operations {
+                match op {
+                    Operation::AddComment { timestamp, message } => {
+                        final_issue.comments.push(Comment {
+                            id,
+                            author: issue.author.load_identity(repo),
+                            message: Markdown::from(message),
+                            timestamp: TimeStamp::new(timestamp),
+                        })
+                    }
+                    Operation::Create {
+                        timestamp,
+                        title,
+                        message,
+                    } => {
+                        final_issue.author = issue.author.load_identity(repo);
+                        final_issue.title = Markdown::from(title);
+                        final_issue.message = Markdown::from(message);
+                        final_issue.timestamp = TimeStamp::new(timestamp);
+                    }
+                    Operation::SetStatus { timestamp, status } => {
+                        final_issue.status = status;
+                        final_issue.last_status_change = Some(TimeStamp::new(timestamp));
+                    }
+                }
+            }
+        }
+        final_issue
+    }
+
+    pub fn to_list_entry(&self) -> RawHtml<String> {
+        let comment_list = if self.comments.is_empty() {
+            String::new()
+        } else {
+            format!(
+                r#"
+                <span class="comment-count"> - {} comments</span>
+            "#,
+                self.comments.len()
+            )
+        };
+        let Issue {
+            id,
+            title,
+            message: _,
+            author,
+            timestamp,
+            comments: _,
+            status: _,
+            last_status_change: _,
+        } = self;
+        let Author { name, email } = author;
+        RawHtml(format!(
+            r#"
+               <li>
+                  <a href="/issue/{id}">
+                     <p>
+                        <span class="issue-subject">{title}</span>
+                     </p>
+                     <span class="issue-number">{id}</span> - <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email">&lt;{email}&gt;</span> at <span class="timestamp">{timestamp}</span></span>{comment_list}                  </a>
+               </li>
+"#,
+        ))
+    }
+
+    pub fn to_html(&self) -> RawHtml<String> {
+        let fmt_comments: String = self
+            .comments
+            .iter()
+            .map(|val| {
+                let Comment {
+                    id,
+                    author,
+                    message,
+                    timestamp,
+                } = val;
+                let Author { name, email: _ } = author;
+
+                format!(
+                    r#"
+               <li class="comment" id="{id}">
+                  {message}
+                  <p class="comment-info"><span class="user-name">{name} at {timestamp}</span></p>
+               </li>
+                "#,
+                )
+            })
+            .collect::<Vec<String>>()
+            .join("\n");
+
+        let maybe_comments = if fmt_comments.is_empty() {
+            String::new()
+        } else {
+            format!(
+                r#"
+            <ol class="issue-history">
+            {fmt_comments}
+            </ol>
+            "#
+            )
+        };
+
+        {
+            let Issue {
+                id,
+                title,
+                message,
+                author,
+                timestamp,
+                comments: _,
+                status: _,
+                last_status_change: _,
+            } = self;
+            let Author { name, email } = author;
+            let html_title = BackString::from(title.clone());
+
+            RawHtml(format!(
+                r#"
+<!DOCTYPE html>
+<html lang="en">
+   <head>
+      <title>{html_title} | Back</title>
+      <link href="/style.css" rel="stylesheet" type="text/css">
+      <meta content="width=device-width,initial-scale=1" name="viewport">
+   </head>
+   <body>
+      <div class="content">
+         <nav>
+         <a href="/issues/open">Open Issues</a>
+         <a href="/issues/closed">Closed Issues</a>
+         </nav>
+         <header>
+            <h1>{title}</h1>
+            <div class="issue-number">{id}</div>
+         </header>
+         <main>
+            <div class="issue-info">
+                <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email">&lt;{email}&gt;</span> at <span class="timestamp">{timestamp}</span></span>
+            </div>
+            {message}
+            {maybe_comments}
+         </main>
+         <footer>
+            <nav>
+            <a href="/issues/open">Open Issues</a>
+            <a href="/issues/closed">Closed Issues</a>
+            </nav>
+         </footer>
+      </div>
+   </body>
+</html>
+"#,
+            ))
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/issues/issue/raw.rs b/pkgs/by-name/ba/back/src/issues/issue/raw.rs
new file mode 100644
index 0000000..48d2a9f
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/issues/issue/raw.rs
@@ -0,0 +1,145 @@
+// Back - An extremely simple git issue tracking system. Inspired by tvix's
+// panettone
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This file is part of Back.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+use gix::{bstr::ByteSlice, Repository};
+use serde::Deserialize;
+use serde_json::Value;
+
+use crate::issues::format::BackString;
+
+use super::{Author, Status};
+
+macro_rules! get {
+    ($value:expr, $name:expr, $type_fun:ident) => {
+        $value
+            .get($name)
+            .expect(concat!(
+                "Expected field ",
+                stringify!($name),
+                "to exists, but was missing."
+            ))
+            .$type_fun()
+            .expect(concat!(
+                "Failed to interpret field ",
+                stringify!($name),
+                " as ",
+                stringify!($type),
+                "!"
+            ))
+    };
+}
+
+#[derive(Deserialize)]
+pub(super) struct RawIssue {
+    pub(super) author: RawAuthor,
+
+    #[serde(alias = "ops")]
+    pub(super) operations: Vec<Operation>,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub(super) struct RawAuthor {
+    id: String,
+}
+
+impl RawAuthor {
+    pub fn load_identity(&self, repo: &Repository) -> Author {
+        let commit_obj = repo
+            .find_reference(&format!("refs/identities/{}", self.id))
+            .expect("All authors should also have identities")
+            .peel_to_commit()
+            .expect("All identities should be commits");
+        let tree_obj = repo
+            .find_tree(
+                commit_obj
+                    .tree()
+                    .expect("The commit should have an tree associated with it")
+                    .id,
+            )
+            .expect("This should be a tree");
+        let data = repo
+            .find_blob(
+                tree_obj
+                    .find_entry("version")
+                    .expect("This entry should exist")
+                    .object()
+                    .expect("This should point to a blob entry")
+                    .id,
+            )
+            .expect("This blob should exist")
+            .data
+            .clone();
+
+        let json: Value = serde_json::from_str(data.to_str().expect("This is encoded json"))
+            .expect("This is valid json");
+
+        Author {
+            name: BackString::from(get! {json, "name", as_str}.to_owned()),
+            email: BackString::from(get! {json, "email", as_str}.to_owned()),
+        }
+    }
+}
+
+#[derive(Deserialize)]
+#[serde(from = "Value")]
+pub(super) enum Operation {
+    AddComment {
+        timestamp: u64,
+        message: String,
+        // files: Option<String>, TODO
+    },
+    SetStatus {
+        timestamp: u64,
+        status: Status,
+    },
+    Create {
+        timestamp: u64,
+        title: String,
+        message: String,
+        // files: Option<String>, TODO
+    },
+}
+
+impl From<u64> for Status {
+    fn from(value: u64) -> Self {
+        match value {
+            1 => Status::Open,
+            2 => Status::Closed,
+            other => todo!("The status ({other}) is not yet implemented."),
+        }
+    }
+}
+
+impl From<Value> for Operation {
+    fn from(value: Value) -> Self {
+        match value
+            .get("type")
+            .expect("Should exist")
+            .as_u64()
+            .expect("This should work")
+        {
+            1 => Self::Create {
+                title: get! {value, "title", as_str}.to_owned(),
+                message: get! {value, "message", as_str}.to_owned(),
+                timestamp: get! {value, "timestamp", as_u64},
+            },
+            3 => Self::AddComment {
+                message: get! {value, "message", as_str}.to_owned(),
+                timestamp: get! {value, "timestamp", as_u64},
+            },
+            4 => Self::SetStatus {
+                status: Status::from(get! {value, "status", as_u64}),
+                timestamp: get! {value, "timestamp", as_u64},
+            },
+            other => todo!("The type ({other}) is not yet added as a a valid operation. It's value is: '{value}''"),
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/issues/issue_show.rs b/pkgs/by-name/ba/back/src/issues/issue_show.rs
new file mode 100644
index 0000000..638840e
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/issues/issue_show.rs
@@ -0,0 +1,34 @@
+// Back - An extremely simple git issue tracking system. Inspired by tvix's
+// panettone
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This file is part of Back.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+use std::fmt::Display;
+
+use gix::hash::Prefix;
+use rocket::request::FromParam;
+
+pub struct BackPrefix {
+    prefix: Prefix,
+}
+impl Display for BackPrefix {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.prefix.fmt(f)
+    }
+}
+
+impl<'a> FromParam<'a> for BackPrefix {
+    type Error = gix::hash::prefix::from_hex::Error;
+
+    fn from_param(param: &'a str) -> Result<Self, Self::Error> {
+        let prefix = Prefix::from_hex(param)?;
+
+        Ok(Self { prefix })
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/issues/mod.rs b/pkgs/by-name/ba/back/src/issues/mod.rs
new file mode 100644
index 0000000..e2af5de
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/issues/mod.rs
@@ -0,0 +1,129 @@
+// Back - An extremely simple git issue tracking system. Inspired by tvix's
+// panettone
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This file is part of Back.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+use std::path::Path;
+
+use crate::issues::issue::{Issue, Status};
+use issue_show::BackPrefix;
+use gix::{refs::Target, Repository};
+use rocket::{
+    get,
+    response::content::{RawCss, RawHtml},
+};
+
+use crate::REPOSITORY;
+
+mod issue;
+mod issue_show;
+mod format;
+
+#[get("/style.css")]
+pub fn styles() -> RawCss<String> {
+    RawCss(include_str!("../../assets/style.css").to_owned())
+}
+
+fn list_all_issues(repo: &'_ Repository) -> Vec<Issue<'_>> {
+    repo.refs
+        .iter()
+        .expect("We should be able to iterate over references")
+        .prefixed(Path::new("refs/bugs/"))
+        .expect("The 'refs/bugs/' namespace should exist")
+        .map(|val| {
+            let reference = val.expect("'val' should be an object?");
+            if let Target::Object(commit_id) = reference.target {
+                Issue::from_commit_id(repo, commit_id)
+            } else {
+                unreachable!("All 'refs/bugs/' should contain a clear target.");
+            }
+        })
+        .collect()
+}
+
+pub fn issue_list_boilerplate(wanted_status: Status, counter_status: Status) -> RawHtml<String> {
+    let repository = REPOSITORY.get().expect("This should have been set");
+
+    let issue_list = list_all_issues(&repository.to_thread_local())
+        .iter()
+        .fold(String::new(), |acc, val| {
+            format!("{}{}", acc, &issue_to_string(val, wanted_status).0)
+        });
+
+    let counter_status_lower = counter_status.to_string().to_lowercase();
+    RawHtml(format!(
+        r#"
+<!DOCTYPE html>
+<html lang="en">
+   <head>
+      <title>Back</title>
+      <link href="/style.css" rel="stylesheet" type="text/css">
+      <meta content="width=device-width,initial-scale=1" name="viewport">
+   </head>
+   <body>
+      <div class="content">
+         <header>
+            <h1>{wanted_status} Issues</h1>
+         </header>
+         <main>
+            <div class="issue-links">
+               <a href="/issues/{counter_status_lower}/">View {counter_status} issues</a>
+               <!--
+               <form class="issue-search" method="get">
+                   <input name="search" title="Issue search query" type="search">
+                   <input class="sr-only" type="submit" value="Search Issues">
+               </form>
+               -->
+            </div>
+            <ol class="issue-list">
+            {}
+            </ol>
+         </main>
+      </div>
+   </body>
+</html>
+"#,
+        issue_list
+    ))
+}
+
+#[get("/issues/open")]
+pub fn open() -> RawHtml<String> {
+    issue_list_boilerplate(Status::Open, Status::Closed)
+}
+#[get("/issues/closed")]
+pub fn closed() -> RawHtml<String> {
+    issue_list_boilerplate(Status::Closed, Status::Open)
+}
+
+#[get("/issue/<prefix>")]
+pub fn show_issue(prefix: BackPrefix) -> RawHtml<String> {
+    let repository = REPOSITORY
+        .get()
+        .expect("This should have been set")
+        .to_thread_local();
+
+    let all_issues = list_all_issues(&repository);
+    let maybe_issue = all_issues
+        .iter()
+        .find(|issue| issue.id.to_string().starts_with(&prefix.to_string()));
+
+    match maybe_issue {
+        Some(issue) => issue.to_html(),
+        None => RawHtml(format!("Issue with id '{prefix}' not found!")),
+    }
+}
+
+fn issue_to_string(issue: &Issue<'_>, status: Status) -> RawHtml<String> {
+    if issue.status == status {
+        issue.to_list_entry()
+    } else {
+        RawHtml(String::default())
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/main.rs b/pkgs/by-name/ba/back/src/main.rs
new file mode 100644
index 0000000..d0b0f81
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/main.rs
@@ -0,0 +1,49 @@
+// Back - An extremely simple git issue tracking system. Inspired by tvix's
+// panettone
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This file is part of Back.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/agpl.txt>.
+
+use std::{env::args, path::PathBuf, process, sync::OnceLock};
+
+use gix::ThreadSafeRepository;
+use rocket::{launch, routes};
+
+use crate::issues::{closed, open, show_issue, styles};
+
+mod issues;
+
+static REPOSITORY: OnceLock<ThreadSafeRepository> = OnceLock::new();
+
+#[launch]
+fn rocket() -> _ {
+    let repository_path = {
+        let maybe_path = args().skip(1).rev().last();
+        if let Some(path) = maybe_path {
+            PathBuf::from(path)
+        } else {
+            eprintln!("Usage: back <issue repoitory>");
+            process::exit(1);
+        }
+    };
+
+    REPOSITORY
+        .set(
+            ThreadSafeRepository::open(&repository_path).unwrap_or_else(|err| {
+                eprintln!(
+                    "Error while opening repository ('{}'): {} ",
+                    repository_path.display(),
+                    err
+                );
+                process::exit(1);
+            }),
+        )
+        .expect("There should be only one thread accessing this right now");
+
+    rocket::build().mount("/", routes![open, closed, show_issue, styles])
+}