aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ba
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ba')
-rw-r--r--pkgs/by-name/ba/back/Cargo.lock66
-rw-r--r--pkgs/by-name/ba/back/Cargo.toml1
-rw-r--r--pkgs/by-name/ba/back/flake.nix2
-rw-r--r--pkgs/by-name/ba/back/src/error/mod.rs44
-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.rs (renamed from pkgs/by-name/ba/back/src/web/format/mod.rs)63
-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/main.rs1
-rw-r--r--pkgs/by-name/ba/back/src/web/issue/mod.rs337
-rw-r--r--pkgs/by-name/ba/back/src/web/issue/raw.rs145
-rw-r--r--pkgs/by-name/ba/back/src/web/issue_html.rs166
-rw-r--r--pkgs/by-name/ba/back/src/web/mod.rs148
-rw-r--r--pkgs/by-name/ba/back/src/web/prefix.rs (renamed from pkgs/by-name/ba/back/src/web/issue_show.rs)1
20 files changed, 1187 insertions, 575 deletions
diff --git a/pkgs/by-name/ba/back/Cargo.lock b/pkgs/by-name/ba/back/Cargo.lock
index b72f23c..3965cfc 100644
--- a/pkgs/by-name/ba/back/Cargo.lock
+++ b/pkgs/by-name/ba/back/Cargo.lock
@@ -196,6 +196,7 @@ dependencies = [
"rocket",
"serde",
"serde_json",
+ "sha2",
"thiserror",
"url",
]
@@ -228,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"
@@ -367,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"
@@ -391,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"
@@ -447,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"
@@ -639,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"
@@ -2557,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"
@@ -2944,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"
diff --git a/pkgs/by-name/ba/back/Cargo.toml b/pkgs/by-name/ba/back/Cargo.toml
index 2fc0600..8a472e6 100644
--- a/pkgs/by-name/ba/back/Cargo.toml
+++ b/pkgs/by-name/ba/back/Cargo.toml
@@ -29,6 +29,7 @@ markdown = "1.0.0-alpha.21"
rocket = "0.5.1"
serde = "1.0.216"
serde_json = "1.0.134"
+sha2 = "0.10.8"
thiserror = "2.0.9"
url = { version = "2.5.4", features = ["serde"] }
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/error/mod.rs b/pkgs/by-name/ba/back/src/error/mod.rs
index 7e1c9cf..8b71700 100644
--- a/pkgs/by-name/ba/back/src/error/mod.rs
+++ b/pkgs/by-name/ba/back/src/error/mod.rs
@@ -1,9 +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::{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 {
@@ -14,11 +29,19 @@ pub enum Error {
file: PathBuf,
error: io::Error,
},
+ RocketLaunch(#[from] rocket::Error),
+
RepoOpen {
repository_path: PathBuf,
error: Box<gix::open::Error>,
},
- RocketLaunch(#[from] rocket::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 {
@@ -38,6 +61,9 @@ impl Display for Error {
file.display()
)
}
+ Error::RocketLaunch(error) => {
+ write!(f, "while trying to start back: {error}")
+ }
Error::RepoOpen {
repository_path,
error,
@@ -48,8 +74,20 @@ impl Display for Error {
repository_path.display()
)
}
- Error::RocketLaunch(error) => {
- write!(f, "while trying to start back: {error}")
+ 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/web/format/mod.rs b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
index f78d3b3..4ebf6d4 100644
--- a/pkgs/by-name/ba/back/src/web/format/mod.rs
+++ b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
@@ -11,41 +11,76 @@
use std::fmt::Display;
+use chrono::DateTime;
use markdown::to_html;
+use serde::Deserialize;
+use serde_json::Value;
#[derive(Debug, Default, Clone)]
-pub struct Markdown {
+/// Markdown content.
+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())
+ }
+}
+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 Markdown {
+impl Display for TimeStamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(to_html(&self.value).as_str())
+ 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, Default)]
-pub struct BackString {
+#[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 BackString {
- fn from(value: Markdown) -> Self {
+impl From<MarkDown> for HtmlString {
+ fn from(value: MarkDown) -> Self {
Self { value: value.value }
}
}
-impl From<String> for BackString {
- fn from(value: String) -> Self {
- Self { 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 BackString {
+impl Display for HtmlString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(escape_html(&self.value).as_str())
}
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/main.rs b/pkgs/by-name/ba/back/src/main.rs
index e8f36d2..009bdb6 100644
--- a/pkgs/by-name/ba/back/src/main.rs
+++ b/pkgs/by-name/ba/back/src/main.rs
@@ -18,6 +18,7 @@ use crate::web::{closed, open, show_issue, styles};
mod cli;
pub mod config;
mod error;
+pub mod git_bug;
mod web;
#[rocket::main]
diff --git a/pkgs/by-name/ba/back/src/web/issue/mod.rs b/pkgs/by-name/ba/back/src/web/issue/mod.rs
deleted file mode 100644
index 79ef70f..0000000
--- a/pkgs/by-name/ba/back/src/web/issue/mod.rs
+++ /dev/null
@@ -1,337 +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::config::BackConfig;
-
-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()
- .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("The 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 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, config: &BackConfig) -> 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>
-"#,
- config.source_code_repository_url
- ))
- }
- }
-}
diff --git a/pkgs/by-name/ba/back/src/web/issue/raw.rs b/pkgs/by-name/ba/back/src/web/issue/raw.rs
deleted file mode 100644
index bb447ec..0000000
--- a/pkgs/by-name/ba/back/src/web/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::web::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/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
index ed91e7e..1e6a5af 100644
--- a/pkgs/by-name/ba/back/src/web/mod.rs
+++ b/pkgs/by-name/ba/back/src/web/mod.rs
@@ -9,125 +9,121 @@
// 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::{
config::BackConfig,
- web::issue::{Issue, Status},
+ error::{self, Error},
+ git_bug::{
+ dag::issues_from_repository,
+ issue::{CollapsedIssue, Status},
+ },
};
-use gix::{refs::Target, Repository};
-use issue_show::BackPrefix;
+use prefix::BackPrefix;
use rocket::{
get,
response::content::{RawCss, RawHtml},
State,
};
-mod format;
-mod issue;
-mod issue_show;
+mod issue_html;
+pub mod prefix;
#[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(
config: &State<BackConfig>,
wanted_status: Status,
counter_status: Status,
-) -> RawHtml<String> {
+) -> error::Result<RawHtml<String>> {
let repository = &config.repository;
- let issue_list = list_all_issues(&repository.to_thread_local())
- .iter()
+ let issue_list = issues_from_repository(&repository.to_thread_local())?
+ .into_iter()
.fold(String::new(), |acc, val| {
- format!("{}{}", acc, &issue_to_string(val, wanted_status).0)
+ let issue = val.collaps();
+
+ 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();
- RawHtml(format!(
+ 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}
- </ol>
- </main>
- </div>
- </body>
-</html>
-"#,
+ <!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>
+ "#,
config.source_code_repository_url
- ))
+ )))
}
#[get("/issues/open")]
-pub fn open(config: &State<BackConfig>) -> RawHtml<String> {
+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>) -> RawHtml<String> {
+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: BackPrefix) -> RawHtml<String> {
+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 = list_all_issues(&repository);
+ 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) => issue.to_html(config),
- 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())
+ Some(issue) => Ok(issue.to_html(config)),
+ None => Err(Error::IssuesPrefixMissing { prefix }),
}
}
diff --git a/pkgs/by-name/ba/back/src/web/issue_show.rs b/pkgs/by-name/ba/back/src/web/prefix.rs
index 638840e..5143799 100644
--- a/pkgs/by-name/ba/back/src/web/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,
}