summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--hosts/by-name/server2/configuration.nix2
-rw-r--r--pkgs/by-name/ba/back/Cargo.lock188
-rw-r--r--pkgs/by-name/ba/back/Cargo.toml5
-rw-r--r--pkgs/by-name/ba/back/contrib/config.json4
-rw-r--r--pkgs/by-name/ba/back/contrib/config.json.license10
-rw-r--r--pkgs/by-name/ba/back/flake.nix2
-rw-r--r--pkgs/by-name/ba/back/src/cli.rs24
-rw-r--r--pkgs/by-name/ba/back/src/config/mod.rs69
-rw-r--r--pkgs/by-name/ba/back/src/error/mod.rs94
-rw-r--r--pkgs/by-name/ba/back/src/error/responder.rs23
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/dag/mod.rs143
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/format/mod.rs144
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs78
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs71
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs85
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/mod.rs185
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs124
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs51
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/mod.rs28
-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.rs332
-rw-r--r--pkgs/by-name/ba/back/src/issues/issue/raw.rs145
-rw-r--r--pkgs/by-name/ba/back/src/issues/mod.rs134
-rw-r--r--pkgs/by-name/ba/back/src/main.rs86
-rw-r--r--pkgs/by-name/ba/back/src/web/issue_html.rs166
-rw-r--r--pkgs/by-name/ba/back/src/web/mod.rs134
-rw-r--r--pkgs/by-name/ba/back/src/web/prefix.rs (renamed from pkgs/by-name/ba/back/src/issues/issue_show.rs)1
27 files changed, 1657 insertions, 759 deletions
diff --git a/hosts/by-name/server2/configuration.nix b/hosts/by-name/server2/configuration.nix
index 6d412fa..07b78c3 100644
--- a/hosts/by-name/server2/configuration.nix
+++ b/hosts/by-name/server2/configuration.nix
@@ -8,7 +8,7 @@
     back = {
       enable = true;
       repositories = {
-        "${config.services.gitolite.dataDir}/vhack.eu/nixos-server.git" = {
+        "${config.services.gitolite.dataDir}/repositories/vhack.eu/nixos-server.git" = {
           domain = "issues.foss-syndicate.org";
           port = 9220;
         };
diff --git a/pkgs/by-name/ba/back/Cargo.lock b/pkgs/by-name/ba/back/Cargo.lock
index ed7f120..3965cfc 100644
--- a/pkgs/by-name/ba/back/Cargo.lock
+++ b/pkgs/by-name/ba/back/Cargo.lock
@@ -71,6 +71,55 @@ dependencies = [
 ]
 
 [[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "arc-swap"
 version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -141,11 +190,14 @@ name = "back"
 version = "0.1.0"
 dependencies = [
  "chrono",
+ "clap",
  "gix",
  "markdown",
  "rocket",
  "serde",
  "serde_json",
+ "sha2",
+ "thiserror",
  "url",
 ]
 
@@ -177,6 +229,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
 [[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "bstr"
 version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -247,12 +308,58 @@ dependencies = [
 ]
 
 [[package]]
+name = "clap"
+version = "4.5.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
 name = "clru"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59"
 
 [[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
 name = "cookie"
 version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -270,6 +377,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
+name = "cpufeatures"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "crc32fast"
 version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -294,6 +410,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
 name = "dashmap"
 version = "6.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -350,6 +476,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
 name = "displaydoc"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -542,6 +678,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
 name = "getrandom"
 version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1470,6 +1616,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
 
 [[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
 name = "hermit-abi"
 version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1776,6 +1928,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
 name = "itoa"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2448,6 +2606,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
 
 [[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
 name = "sharded-slab"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2549,6 +2718,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
 name = "syn"
 version = "2.0.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2829,6 +3004,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
 [[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
 name = "ubyte"
 version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2898,6 +3079,7 @@ dependencies = [
  "form_urlencoded",
  "idna",
  "percent-encoding",
+ "serde",
 ]
 
 [[package]]
@@ -2913,6 +3095,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
 [[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
 name = "valuable"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/pkgs/by-name/ba/back/Cargo.toml b/pkgs/by-name/ba/back/Cargo.toml
index f697511..8a472e6 100644
--- a/pkgs/by-name/ba/back/Cargo.toml
+++ b/pkgs/by-name/ba/back/Cargo.toml
@@ -23,12 +23,15 @@ license = "AGPL-3.0-or-later"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
 chrono = "0.4.39"
+clap = { version = "4.5.23", features = ["derive"] }
 gix = "0.69.1"
 markdown = "1.0.0-alpha.21"
 rocket = "0.5.1"
 serde = "1.0.216"
 serde_json = "1.0.134"
-url = "2.5.4"
+sha2 = "0.10.8"
+thiserror = "2.0.9"
+url = { version = "2.5.4", features = ["serde"] }
 
 [profile.release]
 lto = true
diff --git a/pkgs/by-name/ba/back/contrib/config.json b/pkgs/by-name/ba/back/contrib/config.json
new file mode 100644
index 0000000..10173fb
--- /dev/null
+++ b/pkgs/by-name/ba/back/contrib/config.json
@@ -0,0 +1,4 @@
+{
+    "source_code_repository_url": "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back",
+    "repository_path": "/path/to/your/repository"
+}
diff --git a/pkgs/by-name/ba/back/contrib/config.json.license b/pkgs/by-name/ba/back/contrib/config.json.license
new file mode 100644
index 0000000..9c92e8d
--- /dev/null
+++ b/pkgs/by-name/ba/back/contrib/config.json.license
@@ -0,0 +1,10 @@
+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>.
diff --git a/pkgs/by-name/ba/back/flake.nix b/pkgs/by-name/ba/back/flake.nix
index b7e158e..2553cdf 100644
--- a/pkgs/by-name/ba/back/flake.nix
+++ b/pkgs/by-name/ba/back/flake.nix
@@ -31,6 +31,8 @@
         rust-analyzer
         cargo-edit
 
+        git-bug
+
         reuse
       ];
     };
diff --git a/pkgs/by-name/ba/back/src/cli.rs b/pkgs/by-name/ba/back/src/cli.rs
new file mode 100644
index 0000000..79f0d63
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/cli.rs
@@ -0,0 +1,24 @@
+// 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::PathBuf;
+
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+#[allow(clippy::module_name_repetitions)]
+/// An extremely simple git issue tracking system.
+/// Inspired by tvix's panettone
+pub struct Cli {
+    /// The path to the configuration file. The file should be written in JSON.
+    pub config_file: PathBuf,
+}
diff --git a/pkgs/by-name/ba/back/src/config/mod.rs b/pkgs/by-name/ba/back/src/config/mod.rs
new file mode 100644
index 0000000..a680b90
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/config/mod.rs
@@ -0,0 +1,69 @@
+// 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::{
+    fs,
+    path::{Path, PathBuf},
+};
+
+use gix::ThreadSafeRepository;
+use serde::Deserialize;
+use url::Url;
+
+use crate::error::{self, Error};
+
+pub struct BackConfig {
+    // NOTE(@bpeetz): We do not need to html escape this, as the value must be a valid url. As such
+    // `<tags>` of all kinds _should_ be invalid.  <2024-12-26>
+    pub source_code_repository_url: Url,
+    pub repository: ThreadSafeRepository,
+}
+
+#[derive(Deserialize)]
+struct RawBackConfig {
+    source_code_repository_url: Url,
+    repository_path: PathBuf,
+}
+
+impl BackConfig {
+    pub fn from_config_file(path: &Path) -> error::Result<Self> {
+        let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead {
+            file: path.to_owned(),
+            error: err,
+        })?;
+
+        let raw: RawBackConfig =
+            serde_json::from_str(&value).map_err(|err| Error::ConfigParse {
+                file: path.to_owned(),
+                error: err,
+            })?;
+
+        Self::try_from(raw)
+    }
+}
+
+impl TryFrom<RawBackConfig> for BackConfig {
+    type Error = error::Error;
+
+    fn try_from(value: RawBackConfig) -> Result<Self, Self::Error> {
+        let repository = {
+            ThreadSafeRepository::open(&value.repository_path).map_err(|err| Error::RepoOpen {
+                repository_path: value.repository_path,
+                error: Box::new(err),
+            })
+        }?;
+
+        Ok(Self {
+            repository,
+            source_code_repository_url: value.source_code_repository_url,
+        })
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/error/mod.rs b/pkgs/by-name/ba/back/src/error/mod.rs
new file mode 100644
index 0000000..8b71700
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/error/mod.rs
@@ -0,0 +1,94 @@
+// 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, io, path::PathBuf};
+
+use thiserror::Error;
+
+use crate::web::prefix::BackPrefix;
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+pub mod responder;
+
+#[derive(Error, Debug)]
+pub enum Error {
+    ConfigParse {
+        file: PathBuf,
+        error: serde_json::Error,
+    },
+    ConfigRead {
+        file: PathBuf,
+        error: io::Error,
+    },
+    RocketLaunch(#[from] rocket::Error),
+
+    RepoOpen {
+        repository_path: PathBuf,
+        error: Box<gix::open::Error>,
+    },
+    RepoRefsIter(#[from] gix::refs::packed::buffer::open::Error),
+    RepoRefsPrefixed(#[from] std::io::Error),
+
+    IssuesPrefixMissing {
+        prefix: BackPrefix,
+    },
+    IssuesPrefixParse(#[from] gix::hash::prefix::from_hex::Error),
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::ConfigParse { file, error } => {
+                write!(
+                    f,
+                    "while trying to parse the config file ({}): {error}",
+                    file.display()
+                )
+            }
+            Error::ConfigRead { file, error } => {
+                write!(
+                    f,
+                    "while trying to read the config file ({}): {error}",
+                    file.display()
+                )
+            }
+            Error::RocketLaunch(error) => {
+                write!(f, "while trying to start back: {error}")
+            }
+            Error::RepoOpen {
+                repository_path,
+                error,
+            } => {
+                write!(
+                    f,
+                    "while trying to open the repository ({}): {error}",
+                    repository_path.display()
+                )
+            }
+            Error::RepoRefsIter(error) => {
+                write!(f, "while iteration over the refs in a repository: {error}",)
+            }
+            Error::RepoRefsPrefixed(error) => {
+                write!(f, "while prefixing the refs with a path: {error}")
+            }
+            Error::IssuesPrefixMissing { prefix } => {
+                write!(
+                    f,
+                    "There is no 'issue' associated with the prefix: {prefix}"
+                )
+            }
+            Error::IssuesPrefixParse(error) => {
+                write!(f, "The given prefix can not be parsed as prefix: {error}")
+            }
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/error/responder.rs b/pkgs/by-name/ba/back/src/error/responder.rs
new file mode 100644
index 0000000..7bea961
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/error/responder.rs
@@ -0,0 +1,23 @@
+// 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 rocket::{
+    response::{self, Responder, Response},
+    Request,
+};
+
+use super::Error;
+
+impl<'r> Responder<'r, 'static> for Error {
+    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
+        Response::build_from(self.to_string().respond_to(req)?).ok()
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs b/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs
new file mode 100644
index 0000000..9c158a7
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs
@@ -0,0 +1,143 @@
+// 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 gix::{bstr::ByteSlice, refs::Target, Commit, Id, ObjectId, Repository};
+
+use crate::error;
+
+use super::issue::{
+    entity::{Entity, RawEntity},
+    CollapsedIssue, RawCollapsedIssue,
+};
+
+#[derive(Debug)]
+pub struct Dag {
+    entities: Vec<Entity>,
+}
+
+impl Dag {
+    pub fn collapse(self) -> CollapsedIssue {
+        let raw_collapsed_issue = self.entities.into_iter().rev().fold(
+            RawCollapsedIssue::default(),
+            |mut collapsed_issue, entity| {
+                collapsed_issue.append_entity(entity);
+                collapsed_issue
+            },
+        );
+
+        CollapsedIssue::from(raw_collapsed_issue)
+    }
+
+    /// Construct a DAG from the root child upwards.
+    pub fn construct(repo: &Repository, id: ObjectId) -> Self {
+        let mut entities = vec![];
+
+        let base_commit = repo
+            .find_object(id)
+            .expect("The object with this id should exist.")
+            .try_into_commit()
+            .expect("The git-bug's data model enforces this.");
+
+        entities.push(Self::commit_to_operations(repo, &base_commit));
+
+        let mut current_commit = base_commit;
+        while let Some(parent_id) = Self::try_get_parent(repo, &current_commit) {
+            entities.push(Self::commit_to_operations(repo, &parent_id));
+            current_commit = parent_id;
+        }
+
+        Self {
+            entities: {
+                entities
+                    .into_iter()
+                    .map(|(raw_entity, id)| Entity::from_raw(repo, raw_entity, id))
+                    .collect()
+            },
+        }
+    }
+
+    fn commit_to_operations<'b>(repo: &Repository, id: &Commit<'b>) -> (RawEntity, Id<'b>) {
+        let tree_obj = repo
+            .find_object(
+                id.tree_id()
+                    .expect("All of git-bug's commits should have trees attached to them'"),
+            )
+            .expect("The object with this id should exist.")
+            .try_into_tree()
+            .expect("git-bug's data model enforces this.");
+
+        let ops_ref = tree_obj
+            .find_entry("ops")
+            .expect("All of git-bug's trees should contain a 'ops' json file");
+
+        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 operations = 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");
+
+        (operations, id.id())
+    }
+
+    fn try_get_parent<'a>(repo: &'a Repository, base_commit: &Commit<'a>) -> Option<Commit<'a>> {
+        let count = base_commit.parent_ids().count();
+
+        match count {
+            0 => None,
+            1 => {
+                let parent = base_commit.parent_ids().last().expect("One does exist");
+
+                let parent_id = parent.object().expect("The object exists").id;
+                Some(
+                    repo.find_object(parent_id)
+                        .expect("This is a valid id")
+                        .try_into_commit()
+                        .expect("This should be a commit"),
+                )
+            }
+            other => {
+                unreachable!(
+                    "Each commit, used by git-bug should only have one parent, but found: {other}"
+                );
+            }
+        }
+    }
+}
+
+pub fn issues_from_repository(repo: &Repository) -> error::Result<Vec<Dag>> {
+    let dags = repo
+        .refs
+        .iter()?
+        .prefixed(Path::new("refs/bugs/"))?
+        .map(|val| {
+            let reference = val.expect("All `git-bug` references in 'refs/bugs' should be objects");
+
+            if let Target::Object(id) = reference.target {
+                Dag::construct(repo, id)
+            } else {
+                unreachable!("All 'refs/bugs/' should contain a clear target.");
+            }
+        })
+        .collect::<Vec<Dag>>();
+
+    Ok(dags)
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/format/mod.rs b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
new file mode 100644
index 0000000..b3b6bcc
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
@@ -0,0 +1,144 @@
+// 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 markdown::to_html;
+use serde::Deserialize;
+use serde_json::Value;
+
+#[derive(Debug, Default, Clone)]
+/// Markdown content.
+pub struct MarkDown {
+    value: String,
+}
+
+impl Display for MarkDown {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(to_html(&self.value).as_str())
+    }
+}
+impl From<&Value> for MarkDown {
+    fn from(value: &Value) -> Self {
+        Self {
+            value: value.as_str().expect("This will exist").to_owned(),
+        }
+    }
+}
+
+/// An UNIX time stamp.
+///
+/// These should only ever be used for human-display, because timestamps are unreliably in a
+/// distributed system.
+/// Because of this reason, there is no `value()` function.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct TimeStamp {
+    value: u64,
+}
+impl From<&Value> for TimeStamp {
+    fn from(value: &Value) -> Self {
+        Self {
+            value: value.as_u64().expect("This must exist"),
+        }
+    }
+}
+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())
+    }
+}
+
+/// An UNIX time stamp.
+///
+/// These should only ever be used for human-display, because timestamps are unreliably in a
+/// distributed system.
+///
+/// This one allows underlying access to it's value and is only obtainable via `unsafe` code.
+/// The reason behind this is, that you might need to access this to improve the display for humans
+/// (i.e., sorting by date).
+#[derive(Debug, Default, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
+pub struct UnsafeTimeStamp {
+    value: u64,
+}
+impl TimeStamp {
+    /// # Safety
+    /// This is not really unsafe, but there is just no way your can trust a time stamp in a
+    /// distributed system. As such access to the raw value could lead to bugs.
+    pub unsafe fn to_unsafe(self) -> UnsafeTimeStamp {
+        UnsafeTimeStamp { value: self.value }
+    }
+}
+
+#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)]
+/// A string that should be escaped when injected into html content.
+pub struct HtmlString {
+    value: String,
+}
+
+impl From<MarkDown> for HtmlString {
+    fn from(value: MarkDown) -> Self {
+        Self { value: value.value }
+    }
+}
+
+impl From<&Value> for HtmlString {
+    fn from(value: &Value) -> Self {
+        Self {
+            value: value.as_str().expect("This will exist").to_owned(),
+        }
+    }
+}
+impl Display for HtmlString {
+    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/git_bug/issue/entity/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs
new file mode 100644
index 0000000..f2e9af0
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/entity/mod.rs
@@ -0,0 +1,78 @@
+// 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::Repository;
+use serde::Deserialize;
+use serde_json::Value;
+
+use super::{
+    identity::{Author, RawAuthor},
+    operation::Operation,
+};
+
+#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
+#[serde(from = "Value")]
+pub struct Id {
+    value: String,
+}
+impl From<Value> for Id {
+    fn from(value: Value) -> Self {
+        Self::from(&value)
+    }
+}
+impl From<&Value> for Id {
+    fn from(value: &Value) -> Self {
+        Self {
+            value: value.as_str().expect("This should  be a string").to_owned(),
+        }
+    }
+}
+impl From<gix::Id<'_>> for Id {
+    fn from(value: gix::Id<'_>) -> Self {
+        Self {
+            value: value.shorten().expect("This should work?").to_string(),
+        }
+    }
+}
+impl Display for Id {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.value.fmt(f)
+        // let shortend = self.value.shorten().expect("This should work.");
+        // f.write_str(shortend.to_string().as_str())
+    }
+}
+
+#[derive(Debug)]
+pub struct Entity {
+    pub id: Id,
+    pub author: Author,
+    pub operations: Vec<Operation>,
+}
+
+impl Entity {
+    pub fn from_raw<'a>(repo: &'a Repository, raw: RawEntity, id: gix::Id<'a>) -> Self {
+        Self {
+            id: Id::from(id),
+            author: Author::construct(repo, raw.author),
+            operations: raw.operations,
+        }
+    }
+}
+
+#[derive(Deserialize)]
+pub struct RawEntity {
+    pub author: RawAuthor,
+
+    #[serde(alias = "ops")]
+    pub operations: Vec<Operation>,
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs
new file mode 100644
index 0000000..0c2f426
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/identity/mod.rs
@@ -0,0 +1,71 @@
+// 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::{get, git_bug::format::HtmlString};
+
+use super::entity::Id;
+
+#[derive(Debug, Clone)]
+pub struct Author {
+    pub name: HtmlString,
+    pub email: HtmlString,
+    pub id: Id,
+}
+
+impl Author {
+    pub fn construct(repo: &Repository, raw: RawAuthor) -> Self {
+        let commit_obj = repo
+            .find_reference(&format!("refs/identities/{}", raw.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: get! {json, "name"},
+            email: get! {json, "email"},
+            id: raw.id,
+        }
+    }
+}
+
+#[derive(Deserialize)]
+pub struct RawAuthor {
+    id: Id,
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs
new file mode 100644
index 0000000..a971234
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/label/mod.rs
@@ -0,0 +1,85 @@
+// 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 serde::Deserialize;
+use sha2::{Digest, Sha256};
+
+use crate::git_bug::format::HtmlString;
+
+#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
+pub struct Label {
+    value: HtmlString,
+}
+
+impl Display for Label {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.value.fmt(f)
+    }
+}
+
+impl Label {
+    /// RGBA from a Label computed in a deterministic way
+    /// This is taken completely from `git_bug`
+    pub fn associate_color(&self) -> Color {
+        // colors from: https://material-ui.com/style/color/
+        let colors = vec![
+            Color::from_rgba(244, 67, 54, 255),   // red
+            Color::from_rgba(233, 30, 99, 255),   // pink
+            Color::from_rgba(156, 39, 176, 255),  // purple
+            Color::from_rgba(103, 58, 183, 255),  // deepPurple
+            Color::from_rgba(63, 81, 181, 255),   // indigo
+            Color::from_rgba(33, 150, 243, 255),  // blue
+            Color::from_rgba(3, 169, 244, 255),   // lightBlue
+            Color::from_rgba(0, 188, 212, 255),   // cyan
+            Color::from_rgba(0, 150, 136, 255),   // teal
+            Color::from_rgba(76, 175, 80, 255),   // green
+            Color::from_rgba(139, 195, 74, 255),  // lightGreen
+            Color::from_rgba(205, 220, 57, 255),  // lime
+            Color::from_rgba(255, 235, 59, 255),  // yellow
+            Color::from_rgba(255, 193, 7, 255),   // amber
+            Color::from_rgba(255, 152, 0, 255),   // orange
+            Color::from_rgba(255, 87, 34, 255),   // deepOrange
+            Color::from_rgba(121, 85, 72, 255),   // brown
+            Color::from_rgba(158, 158, 158, 255), // grey
+            Color::from_rgba(96, 125, 139, 255),  // blueGrey
+        ];
+
+        let hash = Sha256::digest(self.to_string().as_bytes());
+
+        let id: usize = hash
+            .into_iter()
+            .map(|val| val as usize)
+            .fold(0, |acc, val| (acc + val) % colors.len());
+
+        colors[id]
+    }
+}
+
+#[derive(Default, Clone, Copy, Debug)]
+pub struct Color {
+    pub red: u32,
+    pub green: u32,
+    pub blue: u32,
+    pub alpha: u32,
+}
+
+impl Color {
+    pub fn from_rgba(red: u32, green: u32, blue: u32, alpha: u32) -> Self {
+        Self {
+            red,
+            green,
+            blue,
+            alpha,
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs
new file mode 100644
index 0000000..f27bfec
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs
@@ -0,0 +1,185 @@
+// 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 entity::{Entity, Id};
+use identity::Author;
+use label::Label;
+use operation::Operation;
+use serde_json::Value;
+
+use super::format::{MarkDown, TimeStamp};
+
+pub mod entity;
+pub mod identity;
+pub mod label;
+pub mod operation;
+
+#[derive(Debug, Eq, PartialEq, Copy, Clone)]
+pub enum Status {
+    Open,
+    Closed,
+}
+impl From<&Value> for Status {
+    fn from(value: &Value) -> Self {
+        match value.as_u64().expect("This should be a integer") {
+            1 => Self::Open,
+            2 => Self::Closed,
+            other => unimplemented!("Invalid status string: '{other}'"),
+        }
+    }
+}
+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 CollapsedIssue {
+    pub id: Id,
+    pub author: Author,
+    pub timestamp: TimeStamp,
+    pub title: MarkDown,
+    pub message: MarkDown,
+    pub comments: Vec<Comment>,
+    pub status: Status,
+    pub last_status_change: TimeStamp,
+    pub labels: Vec<Label>,
+}
+impl From<RawCollapsedIssue> for CollapsedIssue {
+    fn from(r: RawCollapsedIssue) -> Self {
+        macro_rules! get {
+            ($name:ident) => {
+                r.$name.expect(concat!(
+                    "'",
+                    stringify!($name),
+                    "' is unset, when trying to collapes an issue! (This is likely a bug)"
+                ))
+            };
+        }
+
+        Self {
+            id: get! {id},
+            author: get! {author},
+            timestamp: get! {timestamp},
+            title: get! {title},
+            message: get! {message},
+            comments: r.comments,
+            status: get! {status},
+            last_status_change: get! {last_status_change},
+            labels: r.labels,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct Comment {
+    pub id: Id,
+    pub author: Author,
+    pub timestamp: TimeStamp,
+    pub message: MarkDown,
+}
+
+#[derive(Debug, Default)]
+pub struct RawCollapsedIssue {
+    pub id: Option<Id>,
+    pub author: Option<Author>,
+    pub timestamp: Option<TimeStamp>,
+    pub title: Option<MarkDown>,
+    pub message: Option<MarkDown>,
+    pub status: Option<Status>,
+    pub last_status_change: Option<TimeStamp>,
+
+    // NOTE(@bpeetz): These values set here already, because an issue without these
+    // would be perfectly valid. <2024-12-26>
+    pub labels: Vec<Label>,
+    pub comments: Vec<Comment>,
+}
+
+impl RawCollapsedIssue {
+    pub fn append_entity(&mut self, entity: Entity) {
+        for op in entity.operations {
+            match op {
+                Operation::AddComment { timestamp, message } => {
+                    self.comments.push(Comment {
+                        id: entity.id.clone(),
+                        author: entity.author.clone(),
+                        timestamp,
+                        message,
+                    });
+                }
+                Operation::Create {
+                    timestamp,
+                    title,
+                    message,
+                } => {
+                    self.id = Some(entity.id.clone());
+                    self.author = Some(entity.author.clone());
+                    self.timestamp = Some(timestamp.clone());
+                    self.title = Some(title);
+                    self.message = Some(message);
+                    self.status = Some(Status::Open); // This is the default in git_bug
+                    self.last_status_change = Some(timestamp);
+                }
+                Operation::EditComment {
+                    timestamp,
+                    target,
+                    message,
+                } => {
+                    let comments = &mut self.comments;
+
+                    let target_comment = comments
+                        .iter_mut()
+                        .find(|comment| comment.id == target)
+                        .expect("The target must be a valid comment");
+
+                    // TODO(@bpeetz): We should probably set a `edited = true` flag here. <2024-12-26>
+                    // TODO(@bpeetz): Should we also change the author? <2024-12-26>
+
+                    target_comment.timestamp = timestamp;
+                    target_comment.message = message;
+                }
+                Operation::LabelChange {
+                    timestamp: _,
+                    added,
+                    removed,
+                } => {
+                    let labels = self.labels.clone();
+
+                    self.labels = labels
+                        .into_iter()
+                        .filter(|val| !removed.contains(val))
+                        .chain(added.into_iter())
+                        .collect();
+                }
+                Operation::SetStatus { timestamp, status } => {
+                    self.status = Some(status);
+                    self.last_status_change = Some(timestamp);
+                }
+                Operation::SetTitle {
+                    timestamp: _,
+                    title,
+                    was: _,
+                } => {
+                    self.title = Some(title);
+                }
+
+                Operation::NoOp {} => unimplemented!(),
+                Operation::SetMetadata {} => unimplemented!(),
+            }
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs
new file mode 100644
index 0000000..7f861a7
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/operation/mod.rs
@@ -0,0 +1,124 @@
+// 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::convert::Infallible;
+
+use operation_type::OperationType;
+use serde::Deserialize;
+use serde_json::Value;
+
+use crate::{
+    get,
+    git_bug::format::{MarkDown, TimeStamp},
+};
+
+use super::{entity, label::Label, Status};
+
+pub mod operation_type;
+
+#[derive(Deserialize, Debug)]
+#[serde(try_from = "Value")]
+pub enum Operation {
+    AddComment {
+        timestamp: TimeStamp,
+        message: MarkDown,
+    },
+    Create {
+        timestamp: TimeStamp,
+        title: MarkDown,
+        message: MarkDown,
+    },
+    EditComment {
+        timestamp: TimeStamp,
+        target: entity::Id,
+        message: MarkDown,
+    },
+    LabelChange {
+        timestamp: TimeStamp,
+        added: Vec<Label>,
+        removed: Vec<Label>,
+    },
+    SetStatus {
+        timestamp: TimeStamp,
+        status: Status,
+    },
+    SetTitle {
+        timestamp: TimeStamp,
+        title: MarkDown,
+        was: MarkDown,
+    },
+
+    // These seem to be just weird non-operation, operations.
+    // defined in: git-bug/entities/bug/operation.go
+    NoOp {},
+    SetMetadata {},
+}
+
+impl TryFrom<Value> for Operation {
+    type Error = Infallible;
+
+    fn try_from(value: Value) -> Result<Self, Self::Error> {
+        let operation_type = OperationType::from_json_int(
+            value
+                .get("type")
+                .expect("Should exist")
+                .as_u64()
+                .expect("This should work"),
+        );
+
+        let op = match operation_type {
+            OperationType::AddComment => Self::AddComment {
+                timestamp: get! {value, "timestamp" },
+                message: get! {value, "message"},
+            },
+            OperationType::Create => Self::Create {
+                timestamp: get! {value, "timestamp"},
+                title: get! {value, "title"},
+                message: get! {value, "message"},
+            },
+            OperationType::EditComment => Self::EditComment {
+                timestamp: get! {value, "timestamp"},
+                target: get! {value, "target"},
+                message: get! {value, "message"},
+            },
+            OperationType::LabelChange => Self::LabelChange {
+                timestamp: get! {value, "timestamp"},
+                added: serde_json::from_value(
+                    value
+                        .get("added")
+                        .expect("This should be available")
+                        .to_owned(),
+                )
+                .expect("This should be parsable"),
+                removed: serde_json::from_value(
+                    value
+                        .get("removed")
+                        .expect("This should be available")
+                        .to_owned(),
+                )
+                .expect("This should be parsable"),
+            },
+            OperationType::SetStatus => Self::SetStatus {
+                timestamp: get! {value, "timestamp"},
+                status: get! {value, "status"},
+            },
+            OperationType::SetTitle => Self::SetTitle {
+                timestamp: get! {value, "timestamp"},
+                title: get! {value, "title"},
+                was: get! {value, "was"},
+            },
+            OperationType::NoOp => Self::NoOp {},
+            OperationType::SetMetadata => Self::SetMetadata {},
+        };
+
+        Ok(op)
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs b/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs
new file mode 100644
index 0000000..69d272f
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/operation/operation_type.rs
@@ -0,0 +1,51 @@
+// 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>.
+
+pub enum OperationType {
+    AddComment,
+    Create,
+    EditComment,
+    LabelChange,
+    NoOp,
+    SetMetadata,
+    SetStatus,
+    SetTitle,
+}
+
+impl OperationType {
+    // NOTE(@bpeetz): This mapping should always be the same as `git_bug`'s.
+    // The mapping is defined in `git-bug/entities/bug/operation.go`. <2024-12-26>
+    pub fn to_json_int(self) -> u64 {
+        match self {
+            OperationType::Create => 1,
+            OperationType::SetTitle => 2,
+            OperationType::AddComment => 3,
+            OperationType::SetStatus => 4,
+            OperationType::LabelChange => 5,
+            OperationType::EditComment => 6,
+            OperationType::NoOp => 7,
+            OperationType::SetMetadata => 8,
+        }
+    }
+    pub fn from_json_int(value: u64) -> Self {
+        match value {
+            1 => OperationType::Create,
+            2 => OperationType::SetTitle,
+            3 => OperationType::AddComment,
+            4 => OperationType::SetStatus,
+            5 => OperationType::LabelChange,
+            6 => OperationType::EditComment,
+            7 => OperationType::NoOp,
+            8 => OperationType::SetMetadata,
+            other => unimplemented!("The operation type {other} is not recognized."),
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/git_bug/mod.rs b/pkgs/by-name/ba/back/src/git_bug/mod.rs
new file mode 100644
index 0000000..c0a5372
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/git_bug/mod.rs
@@ -0,0 +1,28 @@
+// 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>.
+
+pub mod dag;
+pub mod format;
+pub mod issue;
+
+#[macro_export]
+macro_rules! get {
+    ($value:expr, $name:expr) => {
+        $value
+            .get($name)
+            .expect(concat!(
+                "Expected field ",
+                stringify!($name),
+                "to exists, but was missing."
+            ))
+            .into()
+    };
+}
diff --git a/pkgs/by-name/ba/back/src/issues/format/mod.rs b/pkgs/by-name/ba/back/src/issues/format/mod.rs
deleted file mode 100644
index f78d3b3..0000000
--- a/pkgs/by-name/ba/back/src/issues/format/mod.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-// 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
deleted file mode 100644
index b78f473..0000000
--- a/pkgs/by-name/ba/back/src/issues/issue/mod.rs
+++ /dev/null
@@ -1,332 +0,0 @@
-// 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 crate::SOURCE_CODE_REPOSITORY;
-
-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="{}">Source code</a>
-            <a href="/issues/closed">Closed Issues</a>
-            </nav>
-         </footer>
-      </div>
-   </body>
-</html>
-"#,
-                SOURCE_CODE_REPOSITORY.get().expect("This should be set")
-            ))
-        }
-    }
-}
diff --git a/pkgs/by-name/ba/back/src/issues/issue/raw.rs b/pkgs/by-name/ba/back/src/issues/issue/raw.rs
deleted file mode 100644
index 48d2a9f..0000000
--- a/pkgs/by-name/ba/back/src/issues/issue/raw.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-// 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/mod.rs b/pkgs/by-name/ba/back/src/issues/mod.rs
deleted file mode 100644
index 744d5ba..0000000
--- a/pkgs/by-name/ba/back/src/issues/mod.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-// 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},
-    SOURCE_CODE_REPOSITORY,
-};
-use format::BackString;
-use gix::{refs::Target, Repository};
-use issue_show::BackPrefix;
-use rocket::{
-    get,
-    response::content::{RawCss, RawHtml},
-};
-
-use crate::REPOSITORY;
-
-mod format;
-mod issue;
-mod issue_show;
-
-#[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>
-               <a href="{}">Source code</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">
-            {issue_list}
-            </ol>
-         </main>
-      </div>
-   </body>
-</html>
-"#,
-        SOURCE_CODE_REPOSITORY.get().expect("This should be set")
-    ))
-}
-
-#[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
index 86fe196..009bdb6 100644
--- a/pkgs/by-name/ba/back/src/main.rs
+++ b/pkgs/by-name/ba/back/src/main.rs
@@ -9,62 +9,32 @@
 // 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 url::Url;
-
-use crate::issues::{closed, open, show_issue, styles};
-
-mod issues;
-
-static REPOSITORY: OnceLock<ThreadSafeRepository> = OnceLock::new();
-static SOURCE_CODE_REPOSITORY: OnceLock<Url> = 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);
-        }
-    };
-    let source_code_url = {
-        match std::env::var("BACK_SOURCE_CODE_REPOSITORY_URL") {
-            Ok(value) => match Url::parse(&value) {
-                Ok(url) => url,
-                Err(err) => {
-                    eprintln!("Can't parse `BACK_SOURCE_CODE_REPOSITORY_URL` value as url: {err}");
-                    process::exit(1);
-                }
-            },
-            Err(err) => {
-                eprintln!("back needs you to specify a source code repositiory as `BACK_SOURCE_CODE_REPOSITORY_URL`: {err}");
-                process::exit(1);
-            }
-        }
-    };
-
-    SOURCE_CODE_REPOSITORY
-        .set(source_code_url)
-        .expect("This should be unset by this stage");
-
-    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])
+use clap::Parser;
+use config::BackConfig;
+use rocket::routes;
+
+use crate::web::{closed, open, show_issue, styles};
+
+mod cli;
+pub mod config;
+mod error;
+pub mod git_bug;
+mod web;
+
+#[rocket::main]
+async fn main() -> Result<(), error::Error> {
+    let args = cli::Cli::parse();
+
+    let config = BackConfig::from_config_file(&args.config_file)?;
+
+    rocket::build()
+        .mount("/", routes![open, closed, show_issue, styles])
+        .manage(config)
+        .ignite()
+        .await
+        .expect("This error should only happen on a miss-configuration.")
+        .launch()
+        .await?;
+
+    Ok(())
 }
diff --git a/pkgs/by-name/ba/back/src/web/issue_html.rs b/pkgs/by-name/ba/back/src/web/issue_html.rs
new file mode 100644
index 0000000..45c0281
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/web/issue_html.rs
@@ -0,0 +1,166 @@
+// 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 rocket::response::content::RawHtml;
+
+use crate::{
+    config::BackConfig,
+    git_bug::{
+        format::HtmlString,
+        issue::{identity::Author, CollapsedIssue, Comment},
+    },
+};
+
+impl CollapsedIssue {
+    pub fn to_list_entry(&self) -> RawHtml<String> {
+        let comment_list = if self.comments.is_empty() {
+            String::new()
+        } else {
+            let comments_string = if self.comments.len() > 1 {
+                "comments"
+            } else {
+                "comment"
+            };
+
+            format!(
+                r#"
+                <span class="comment-count"> - {} {}</span>
+            "#,
+                self.comments.len(),
+                comments_string
+            )
+        };
+
+        let CollapsedIssue {
+            id,
+            title,
+            message: _,
+            author,
+            timestamp,
+            comments: _,
+            status: _,
+            last_status_change: _,
+            labels: _,
+        } = self;
+
+        let Author { name, email, id: _ } = 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, config: &BackConfig) -> RawHtml<String> {
+        let comments = if self.comments.is_empty() {
+            String::new()
+        } else {
+            let fmt_comments: String = self
+                .comments
+                .iter()
+                .map(|val| {
+                    let Comment {
+                        id,
+                        author,
+                        message,
+                        timestamp,
+                    } = val;
+                    let Author {
+                        name,
+                        email: _,
+                        id: _,
+                    } = 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");
+
+            format!(
+                r#"
+            <ol class="issue-history">
+            {fmt_comments}
+            </ol>
+            "#
+            )
+        };
+
+        {
+            let CollapsedIssue {
+                id,
+                title,
+                message,
+                author,
+                timestamp,
+                comments: _,
+                status: _,
+                last_status_change: _,
+                labels: _,
+            } = self;
+            let Author { name, email, id: _ } = author;
+            let html_title = HtmlString::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}
+            {comments}
+         </main>
+         <footer>
+            <nav>
+            <a href="/issues/open">Open Issues</a>
+            <a href="{}">Source code</a>
+            <a href="/issues/closed">Closed Issues</a>
+            </nav>
+         </footer>
+      </div>
+   </body>
+</html>
+"#,
+                config.source_code_repository_url
+            ))
+        }
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/web/mod.rs b/pkgs/by-name/ba/back/src/web/mod.rs
new file mode 100644
index 0000000..35dc59f
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/web/mod.rs
@@ -0,0 +1,134 @@
+// 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 crate::{
+    config::BackConfig,
+    error::{self, Error},
+    git_bug::{
+        dag::issues_from_repository,
+        issue::{CollapsedIssue, Status},
+    },
+};
+use prefix::BackPrefix;
+use rocket::{
+    get,
+    response::content::{RawCss, RawHtml},
+    State,
+};
+
+mod issue_html;
+pub mod prefix;
+
+#[get("/style.css")]
+pub fn styles() -> RawCss<String> {
+    RawCss(include_str!("../../assets/style.css").to_owned())
+}
+
+pub fn issue_list_boilerplate(
+    config: &State<BackConfig>,
+    wanted_status: Status,
+    counter_status: Status,
+) -> error::Result<RawHtml<String>> {
+    let repository = &config.repository;
+
+    let mut issue_list = issues_from_repository(&repository.to_thread_local())?
+        .into_iter()
+        .map(|issue| issue.collapse())
+        .collect::<Vec<CollapsedIssue>>();
+
+    // Sort by date descending.
+    issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() });
+    issue_list.reverse();
+
+    let issue_list_str = issue_list.into_iter().fold(String::new(), |acc, issue| {
+        format!("{}{}", acc, {
+            if issue.status == wanted_status {
+                let issue_entry = issue.to_list_entry();
+                issue_entry.0
+            } else {
+                String::new()
+            }
+        })
+    });
+
+    let counter_status_lower = counter_status.to_string().to_lowercase();
+    Ok(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>
+                   <a href="{}">Source code</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">
+                {issue_list_str}
+                </ol>
+             </main>
+          </div>
+       </body>
+    </html>
+    "#,
+        config.source_code_repository_url
+    )))
+}
+
+#[get("/issues/open")]
+pub fn open(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
+    issue_list_boilerplate(config, Status::Open, Status::Closed)
+}
+#[get("/issues/closed")]
+pub fn closed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
+    issue_list_boilerplate(config, Status::Closed, Status::Open)
+}
+
+#[get("/issue/<prefix>")]
+pub fn show_issue(
+    config: &State<BackConfig>,
+    prefix: Result<BackPrefix, gix::hash::prefix::from_hex::Error>,
+) -> error::Result<RawHtml<String>> {
+    // NOTE(@bpeetz): Explicitly unwrap the `prefix` here (instead of taking the unwrapped value as
+    // argument), to avoid triggering rockets "errors forward to the next route" feature.
+    // This ensures, that our error message actually reaches the user. <2024-12-26>
+    let prefix = prefix?;
+
+    let repository = config.repository.to_thread_local();
+
+    let all_issues: Vec<CollapsedIssue> = issues_from_repository(&repository)?
+        .into_iter()
+        .map(|val| val.collapse())
+        .collect();
+
+    let maybe_issue = all_issues
+        .iter()
+        .find(|issue| issue.id.to_string().starts_with(&prefix.to_string()));
+
+    match maybe_issue {
+        Some(issue) => Ok(issue.to_html(config)),
+        None => Err(Error::IssuesPrefixMissing { prefix }),
+    }
+}
diff --git a/pkgs/by-name/ba/back/src/issues/issue_show.rs b/pkgs/by-name/ba/back/src/web/prefix.rs
index 638840e..5143799 100644
--- a/pkgs/by-name/ba/back/src/issues/issue_show.rs
+++ b/pkgs/by-name/ba/back/src/web/prefix.rs
@@ -14,6 +14,7 @@ use std::fmt::Display;
 use gix::hash::Prefix;
 use rocket::request::FromParam;
 
+#[derive(Debug)]
 pub struct BackPrefix {
     prefix: Prefix,
 }