rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood756a5e442991434f0f9c2c88d79b862fa46261bb
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "184ed1a999f29c917b5c96b53b42c5c7e4f67c51",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"title": "httpd: Allow fetching of patches or issues without defining status",
"state": {
"status": "archived",
"conflicts": []
},
"before": "bd8e0ebcda8f6f06dc20641a71614e3778a43fea",
"after": "756a5e442991434f0f9c2c88d79b862fa46261bb",
"commits": [
"756a5e442991434f0f9c2c88d79b862fa46261bb"
],
"target": "6cfed884bf37cba1e0d8e97fa8b0e94df4a04b1f",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "184ed1a999f29c917b5c96b53b42c5c7e4f67c51",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"description": "Renames the `state` query variable into `status` since we define it as\ne.g. `status: \"draft\"`\nAnd we allow consumers to get all the issues without querying for a specific status.",
"base": "18fc41c53f181673189dcca0213aafc38229c0f9",
"oid": "072a5acc2ce31455c430d610e5090721eee37e48",
"timestamp": 1710193025
},
{
"id": "3984b6c2601eb990c4c43c2f06c9934e6d700024",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"description": "Rebase and use cache filtering methods",
"base": "51e64cfa8b291d87cd055d46759be1d7f2c164b8",
"oid": "5444f9c13dc31e28550b0cd4a1b038a6af28b336",
"timestamp": 1712268484
},
{
"id": "328a1cf6150ae610f57024f2a031e505848e3e51",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"description": "Fix issue list_by_status db statement",
"base": "51e64cfa8b291d87cd055d46759be1d7f2c164b8",
"oid": "92095f5d9c164e7e89a06844fb34b4b1a5a57a53",
"timestamp": 1712269250
},
{
"id": "a62b8f94ad10de22faa234e2f81d0bb8296cde0c",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"description": "Rename state query variable to status, and add issue list_by_status test",
"base": "bc247dff2400ed4f454f1d76e6a95bd82f05b792",
"oid": "04577cec789b39590e38b0904821431a932bad53",
"timestamp": 1712324748
},
{
"id": "7d5a749fa109cd2154ab02d7da1f8add953ccb32",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"description": "Test listing of open and closed issue lists in cache",
"base": "bd8e0ebcda8f6f06dc20641a71614e3778a43fea",
"oid": "756a5e442991434f0f9c2c88d79b862fa46261bb",
"timestamp": 1712579667
},
{
"id": "d803fda2dec0525ddc8446a3008e7c8613e182f1",
"author": {
"id": "did:key:z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
"alias": "sebastinez"
},
"description": "Changes:\n- Introduce Status type for issue filtering by status\n- Clean up test code",
"base": "bd8e0ebcda8f6f06dc20641a71614e3778a43fea",
"oid": "583592bcd9d48c7bd80e68265704b15e9cea5780",
"timestamp": 1712582001
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "d911a27f-a24c-4ed6-bebf-3f9b8c135ad5"
},
"info_url": "https://cci.rad.levitte.org//d911a27f-a24c-4ed6-bebf-3f9b8c135ad5.html"
}
Started at: 2025-10-21 18:05:21.857013+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 125 issues · 15 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 184ed1a999f29c917b5c96b53b42c5c7e4f67c51
✓ Switched to branch patch/184ed1a at revision 7d5a749
✓ Branch patch/184ed1a setup to track rad/patches/184ed1a999f29c917b5c96b53b42c5c7e4f67c51
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 756a5e442991434f0f9c2c88d79b862fa46261bb
HEAD is now at 756a5e44 httpd: Allow fetching of patches or issues without defining status
Exit code: 0
$ git show 756a5e442991434f0f9c2c88d79b862fa46261bb
commit 756a5e442991434f0f9c2c88d79b862fa46261bb
Author: Sebastian Martinez <me@sebastinez.dev>
Date: Mon Mar 11 22:35:28 2024 +0100
httpd: Allow fetching of patches or issues without defining status
Renames the `state` query variable into `status` since we define it as
e.g. `status: "draft"`
And we allow consumers to get all the issues without querying for a specific status.
diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs
index 498b052b..8a2212ac 100644
--- a/radicle-httpd/src/api.rs
+++ b/radicle-httpd/src/api.rs
@@ -17,8 +17,6 @@ use serde_json::json;
use tokio::sync::RwLock;
use tower_http::cors::{self, CorsLayer};
-use radicle::cob::issue;
-use radicle::cob::patch;
use radicle::identity::{DocAt, RepoId};
use radicle::node::policy::Scope;
use radicle::node::routing::Store;
@@ -167,7 +165,7 @@ pub struct RawQuery {
pub struct CobsQuery<T> {
pub page: Option<usize>,
pub per_page: Option<usize>,
- pub state: Option<T>,
+ pub status: Option<T>,
}
#[derive(Serialize, Deserialize, Clone)]
@@ -178,44 +176,6 @@ pub struct PoliciesQuery {
pub scope: Option<Scope>,
}
-#[derive(Default, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub enum IssueState {
- Closed,
- #[default]
- Open,
-}
-
-impl IssueState {
- pub fn matches(&self, issue: &issue::State) -> bool {
- match self {
- Self::Open => matches!(issue, issue::State::Open),
- Self::Closed => matches!(issue, issue::State::Closed { .. }),
- }
- }
-}
-
-#[derive(Default, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub enum PatchState {
- #[default]
- Open,
- Draft,
- Archived,
- Merged,
-}
-
-impl PatchState {
- pub fn matches(&self, patch: &patch::State) -> bool {
- match self {
- Self::Open => matches!(patch, patch::State::Open { .. }),
- Self::Draft => matches!(patch, patch::State::Draft),
- Self::Archived => matches!(patch, patch::State::Archived),
- Self::Merged => matches!(patch, patch::State::Merged { .. }),
- }
- }
-}
-
mod project {
use nonempty::NonEmpty;
use serde::Serialize;
diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs
index 19965d06..0e468bb0 100644
--- a/radicle-httpd/src/api/v1/projects.rs
+++ b/radicle-httpd/src/api/v1/projects.rs
@@ -580,26 +580,25 @@ async fn readme_handler(
async fn issues_handler(
State(ctx): State<Context>,
Path(project): Path<RepoId>,
- Query(qs): Query<CobsQuery<api::IssueState>>,
+ Query(qs): Query<CobsQuery<issue::State>>,
) -> impl IntoResponse {
let (repo, _) = ctx.repo(project)?;
let CobsQuery {
page,
per_page,
- state,
+ status,
} = qs;
let page = page.unwrap_or(0);
let per_page = per_page.unwrap_or(10);
- let state = state.unwrap_or_default();
let issues = ctx.profile.issues(&repo)?;
- let mut issues: Vec<_> = issues
- .list()?
- .filter_map(|r| {
- let (id, issue) = r.ok()?;
- (state.matches(issue.state())).then_some((id, issue))
- })
- .collect::<Vec<_>>();
-
+ let mut issues = if let Some(status) = status {
+ issues
+ .list_by_status(&status)?
+ .filter_map(Result::ok)
+ .collect::<Vec<_>>()
+ } else {
+ issues.list()?.filter_map(Result::ok).collect::<Vec<_>>()
+ };
issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
let aliases = &ctx.profile.aliases();
let issues = issues
@@ -932,26 +931,26 @@ async fn patch_update_handler(
/// `GET /projects/:project/patches`
async fn patches_handler(
State(ctx): State<Context>,
- Path(rid): Path<RepoId>,
- Query(qs): Query<CobsQuery<api::PatchState>>,
+ Path(project): Path<RepoId>,
+ Query(qs): Query<CobsQuery<patch::Status>>,
) -> impl IntoResponse {
- let (repo, _) = ctx.repo(rid)?;
+ let (repo, _) = ctx.repo(project)?;
let CobsQuery {
page,
per_page,
- state,
+ status,
} = qs;
let page = page.unwrap_or(0);
let per_page = per_page.unwrap_or(10);
- let state = state.unwrap_or_default();
let patches = ctx.profile.patches(&repo)?;
- let mut patches = patches
- .list()?
- .filter_map(|r| {
- let (id, patch) = r.ok()?;
- (state.matches(patch.state())).then_some((id, patch))
- })
- .collect::<Vec<_>>();
+ let mut patches = if let Some(status) = status {
+ patches
+ .list_by_status(&status)?
+ .filter_map(Result::ok)
+ .collect::<Vec<_>>()
+ } else {
+ patches.list()?.filter_map(Result::ok).collect::<Vec<_>>()
+ };
patches.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
let aliases = ctx.profile.aliases();
let patches = patches
diff --git a/radicle/src/cob/issue/cache.rs b/radicle/src/cob/issue/cache.rs
index b5272dbc..7f23a1a7 100644
--- a/radicle/src/cob/issue/cache.rs
+++ b/radicle/src/cob/issue/cache.rs
@@ -32,6 +32,10 @@ pub trait Issues {
/// List all issues that are in the store.
fn list(&self) -> Result<Self::Iter<'_>, Self::Error>;
+ /// List all issues in the store that match the provided
+ /// `status`.
+ fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error>;
+
/// Get the [`IssueCounts`] of all the issues in the store.
fn counts(&self) -> Result<IssueCounts, Self::Error>;
@@ -355,6 +359,23 @@ where
.map_err(super::Error::from)
}
+ fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error> {
+ let status = *status;
+ self.store
+ .all()
+ .map(move |inner| NoCacheIter {
+ inner: Box::new(inner.into_iter().filter_map(move |res| {
+ match res {
+ Ok((id, issue)) => (status == State::from(issue.state))
+ .then_some((id, issue))
+ .map(Ok),
+ Err(e) => Some(Err(e.into())),
+ }
+ })),
+ })
+ .map_err(super::Error::from)
+ }
+
fn counts(&self) -> Result<IssueCounts, Self::Error> {
self.store.counts().map_err(super::Error::from)
}
@@ -412,6 +433,10 @@ where
query::list(&self.cache.db, &self.rid())
}
+ fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error> {
+ query::list_by_status(&self.cache.db, &self.rid(), status)
+ }
+
fn counts(&self) -> Result<IssueCounts, Self::Error> {
query::counts(&self.cache.db, &self.rid())
}
@@ -432,6 +457,10 @@ where
query::list(&self.cache.db, &self.rid())
}
+ fn list_by_status(&self, status: &State) -> Result<Self::Iter<'_>, Self::Error> {
+ query::list_by_status(&self.cache.db, &self.rid(), status)
+ }
+
fn counts(&self) -> Result<IssueCounts, Self::Error> {
query::counts(&self.cache.db, &self.rid())
}
@@ -484,6 +513,26 @@ mod query {
})
}
+ pub(super) fn list_by_status<'a>(
+ db: &'a sql::ConnectionThreadSafe,
+ rid: &RepoId,
+ filter: &State,
+ ) -> Result<IssuesIter<'a>, Error> {
+ let mut stmt = db.prepare(
+ "SELECT id, issue
+ FROM issues
+ WHERE repo = ?1
+ AND issue->>'$.state.status' = ?2
+ ORDER BY id
+ ",
+ )?;
+ stmt.bind((1, rid))?;
+ stmt.bind((2, sql::Value::String(filter.to_string())))?;
+ Ok(IssuesIter {
+ inner: stmt.into_iter(),
+ })
+ }
+
pub(super) fn counts(
db: &sql::ConnectionThreadSafe,
rid: &RepoId,
@@ -661,6 +710,69 @@ mod tests {
assert_eq!(issues, list);
}
+ fn create_random_issue_list(state: State) -> Vec<(ObjectId, Issue)> {
+ let ids = (0..arbitrary::gen::<u8>(1))
+ .map(|_| IssueId::from(arbitrary::oid()))
+ .collect::<BTreeSet<IssueId>>();
+
+ ids.into_iter()
+ .map(|id| {
+ (
+ id,
+ Issue {
+ title: id.to_string(),
+ state,
+ ..Issue::new(Thread::default())
+ },
+ )
+ })
+ .collect::<Vec<_>>()
+ }
+
+ #[test]
+ fn test_list_by_status() {
+ let repo = arbitrary::gen::<MockRepository>(1);
+ let mut cache = memory(repo);
+ let issues =
+ create_random_issue_list(State::Open)
+ .into_iter()
+ .zip(create_random_issue_list(State::Closed {
+ reason: CloseReason::Solved,
+ }));
+
+ issues
+ .clone()
+ .for_each(|((open_id, open_issue), (closed_id, closed_issue))| {
+ let rid = cache.rid();
+ cache.update(&rid, &open_id, &open_issue).unwrap();
+ cache.update(&rid, &closed_id, &closed_issue).unwrap();
+ });
+
+ let mut open_list = cache
+ .list_by_status(&State::Open)
+ .unwrap()
+ .collect::<Result<Vec<_>, _>>()
+ .unwrap();
+ let mut closed_list = cache
+ .list_by_status(&State::Closed {
+ reason: CloseReason::Solved,
+ })
+ .unwrap()
+ .collect::<Result<Vec<_>, _>>()
+ .unwrap();
+
+ let (mut open_issues, mut closed_issues): (Vec<(ObjectId, Issue)>, Vec<(ObjectId, Issue)>) =
+ issues.unzip();
+
+ open_list.sort_by_key(|(id, _)| *id);
+ open_issues.sort_by_key(|(id, _)| *id);
+ assert_eq!(open_list, open_issues);
+
+ closed_list.sort_by_key(|(id, _)| *id);
+ closed_issues.sort_by_key(|(id, _)| *id);
+ assert_eq!(closed_list, closed_issues);
+ }
+
#[test]
fn test_remove() {
let repo = arbitrary::gen::<MockRepository>(1);
diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs
index 3e40d389..c1a2d115 100644
--- a/radicle/src/cob/patch.rs
+++ b/radicle/src/cob/patch.rs
@@ -1488,7 +1488,8 @@ impl From<&State> for Status {
/// A simplified enumeration of a [`State`] that can be used for
/// filtering purposes.
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase", tag = "status")]
pub enum Status {
Draft,
#[default]
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name d911a27f-a24c-4ed6-bebf-3f9b8c135ad5 -v /opt/radcis/ci.rad.levitte.org/cci/state/d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/s:/d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/w:/d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/w -w /d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /d911a27f-a24c-4ed6-bebf-3f9b8c135ad5/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}