rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodee4f8a6152dcb904753f3d3f170fe7848175b81c
{
"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": "79daf0e6dbb18c54b8e8a6a7bdaeeaef764c5183",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"title": "cli: use a pager on list commands",
"state": {
"status": "open",
"conflicts": []
},
"before": "ed8b086045ee5d7bd1327f579de7861a1cf49e3b",
"after": "ee4f8a6152dcb904753f3d3f170fe7848175b81c",
"commits": [
"ee4f8a6152dcb904753f3d3f170fe7848175b81c"
],
"target": "6cfed884bf37cba1e0d8e97fa8b0e94df4a04b1f",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "79daf0e6dbb18c54b8e8a6a7bdaeeaef764c5183",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "",
"base": "55cdd880bfee08124d5b6a38cc05036402c7ab6e",
"oid": "03f4733394f67bb142b12f91bf70747a44c47950",
"timestamp": 1756655004
},
{
"id": "9665554130b558d6eccc78ebfea54c095f6c83b4",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "rebased on master",
"base": "f00d1d67432882bef11fc940601f071efe55c88d",
"oid": "8ab3e770b97ef8a52d31fbe02e60df9973332206",
"timestamp": 1757007944
},
{
"id": "d8d3d678e8544ea8c71315a7d7977d3261ee5e30",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "Some small fixups.",
"base": "f00d1d67432882bef11fc940601f071efe55c88d",
"oid": "b883ed105515f64d7db5a5aeb1b691f255377cd5",
"timestamp": 1757074236
},
{
"id": "20cc1897a7203e3d05f05da3a812ab1b961232ad",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "rebased on master",
"base": "11fc98c9c9c4c681d265e765df05c2f9d503ddc9",
"oid": "25712ceb0423c05f48f27588b95b769d6f6b1edd",
"timestamp": 1757424133
},
{
"id": "7c5fa64cc7eb917c63cb84448f25cd101efcb1b4",
"author": {
"id": "did:key:z6Mku263TDxoptFHqPNaz8aFeuc3kyJhcQKJPPVB9wkBgR76",
"alias": "flepied"
},
"description": "rebased",
"base": "ed8b086045ee5d7bd1327f579de7861a1cf49e3b",
"oid": "ee4f8a6152dcb904753f3d3f170fe7848175b81c",
"timestamp": 1758997431
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "30c2e2d6-93ac-4cc6-9458-14c09dd79427"
},
"info_url": "https://cci.rad.levitte.org//30c2e2d6-93ac-4cc6-9458-14c09dd79427.html"
}
Started at: 2025-10-21 20:25:42.971258+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/30c2e2d6-93ac-4cc6-9458-14c09dd79427/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 125 issues · 15 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 79daf0e6dbb18c54b8e8a6a7bdaeeaef764c5183
✓ Switched to branch patch/79daf0e at revision 7c5fa64
✓ Branch patch/79daf0e setup to track rad/patches/79daf0e6dbb18c54b8e8a6a7bdaeeaef764c5183
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout ee4f8a6152dcb904753f3d3f170fe7848175b81c
HEAD is now at ee4f8a61 cli: use a pager on list commands
Exit code: 0
$ git show ee4f8a6152dcb904753f3d3f170fe7848175b81c
commit ee4f8a6152dcb904753f3d3f170fe7848175b81c
Author: Frederic Lepied <flepied@gmail.com>
Date: Sat Sep 13 19:14:06 2025 +0200
cli: use a pager on list commands
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..ca497dec
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,39 @@
+# Repository Guidelines
+
+## Project Structure & Modules
+- `crates/`: Rust workspace members (CLI, node, helpers). Treat each crate as an isolated package.
+- `build.rs`, `build/`: build-time artifacts and helpers.
+- `scripts/`: maintenance utilities (e.g., `scripts/build-man-pages.sh`).
+- `systemd/`, `debian/`: packaging/service files.
+- `README.md`, `ARCHITECTURE.md`, `*.adoc`: docs and man pages.
+
+## Build, Test, and Dev
+- Build: `cargo build --workspace --all-features`
+- Check: `cargo check --workspace`
+- Test (std): `cargo test --workspace`
+- Test (faster): `cargo nextest run --workspace` (if installed)
+- Lint: `cargo clippy --workspace --all-targets -- -D warnings`
+- Format: `cargo fmt --all -- --check`
+- Security: `cargo deny check` and `cargo audit`
+- Nix dev shell: `nix develop` then run the above. CI parity: `nix flake check`.
+
+## Coding Style & Naming
+- Rust 1.88 (see `rust-toolchain.toml`). Run `cargo fmt` before pushing.
+- Prefer module paths over `#[path]` includes; crate-local `mod` layout under `src/`.
+- Naming: modules `snake_case`, types/traits `PascalCase`, functions/vars `snake_case`.
+- Deny warnings in CI; fix all Clippy findings before submitting.
+
+## Testing Guidelines
+- Use Rust’s built-in test framework; place unit tests in `src/*` with `#[cfg(test)]` and integration tests under `crates/<name>/tests/>`.
+- Keep tests deterministic; avoid network and time flakiness.
+- Run: `cargo nextest run --workspace` (or `cargo test`). Aim for meaningful coverage across public APIs.
+
+## Commit & PR Guidelines
+- Commit messages: imperative mood with a scoped prefix when helpful (e.g., `cli/patch: ...`, `term/table: ...`). Keep subject ≤72 chars; add a concise body when needed.
+- PRs must include: clear description, rationale, and testing notes. Link related issues. Update docs/man pages when flags or behavior change.
+- Pre-submit checklist: `cargo fmt`, `cargo clippy -D warnings`, tests green, `cargo deny check`.
+
+## Agent-Specific Notes
+- Make minimal, focused changes; follow existing patterns in the touched crate.
+- Do not refactor unrelated code. Keep file moves atomic and justified.
+- If unsure about behavior, search usage via `rg '<symbol>'` and update call sites.
diff --git a/crates/radicle-cli/build.rs b/crates/radicle-cli/build.rs
deleted file mode 120000
index c6b197af..00000000
--- a/crates/radicle-cli/build.rs
+++ /dev/null
@@ -1 +0,0 @@
-../../build.rs
\ No newline at end of file
diff --git a/crates/radicle-cli/build.rs b/crates/radicle-cli/build.rs
new file mode 100644
index 00000000..34d54253
--- /dev/null
+++ b/crates/radicle-cli/build.rs
@@ -0,0 +1,54 @@
+use std::env;
+use std::process::Command;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Set a build-time `GIT_HEAD` env var which includes the commit id;
+ // such that we can tell which code is running.
+ let hash = env::var("GIT_HEAD").unwrap_or_else(|_| {
+ Command::new("git")
+ .arg("rev-parse")
+ .arg("--short")
+ .arg("HEAD")
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .unwrap_or("unknown".into())
+ });
+
+ let version = if let Ok(version) = env::var("RADICLE_VERSION") {
+ version
+ } else {
+ "pre-release".to_owned()
+ };
+
+ // Set a build-time `SOURCE_DATE_EPOCH` env var which includes the commit time.
+ let commit_time = env::var("SOURCE_DATE_EPOCH").unwrap_or_else(|_| {
+ Command::new("git")
+ .arg("log")
+ .arg("-1")
+ .arg("--pretty=%ct")
+ .arg("HEAD")
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .unwrap_or(0.to_string())
+ });
+
+ println!("cargo::rustc-env=RADICLE_VERSION={version}");
+ println!("cargo::rustc-env=SOURCE_DATE_EPOCH={commit_time}");
+ println!("cargo::rustc-env=GIT_HEAD={hash}");
+
+ Ok(())
+}
diff --git a/crates/radicle-cli/examples/rad-patch-ahead-behind.md b/crates/radicle-cli/examples/rad-patch-ahead-behind.md
index cf8c5b7b..2efa699a 100644
--- a/crates/radicle-cli/examples/rad-patch-ahead-behind.md
+++ b/crates/radicle-cli/examples/rad-patch-ahead-behind.md
@@ -14,7 +14,7 @@ Alice Jones
Then we create a feature branch which adds another entry:
```
$ git checkout -q -b feature/1
-$ sed -i '$a Alan K' CONTRIBUTORS
+$ sh -c "printf '%s\\n' 'Alan K' >> CONTRIBUTORS"
$ git commit -a -q -m "Add Alan"
```
@@ -22,7 +22,7 @@ We go back to master, and add a different second entry, essentially forking
the history:
```
$ git checkout -q master
-$ sed -i '$a Jason Bourne' CONTRIBUTORS
+$ sh -c "printf '%s\\n' 'Jason Bourne' >> CONTRIBUTORS"
$ git commit -a -q -m "Add Jason"
$ git push rad master
$ git log --graph --decorate --abbrev-commit --pretty=oneline --all
@@ -90,7 +90,7 @@ index 3f60d25..6829c43 100644
Then, we stack another change onto `feature/1`, adding another contributor:
``` (stderr)
$ git checkout -q -b feature/2 feature/1
-$ sed -i '$a Mel Farna' CONTRIBUTORS
+$ sh -c "printf '%s\\n' 'Mel Farna' >> CONTRIBUTORS"
$ git commit -a -q -m "Add Mel"
$ git push -o patch.message="Add Mel" rad HEAD:refs/patches
✓ Patch e22ff008e2a0ed47262890d13263031d7555b555 opened
diff --git a/crates/radicle-cli/src/commands/issue.rs b/crates/radicle-cli/src/commands/issue.rs
index db8a50f6..1316b330 100644
--- a/crates/radicle-cli/src/commands/issue.rs
+++ b/crates/radicle-cli/src/commands/issue.rs
@@ -801,8 +801,7 @@ where
mk_issue_row(id, issue, assigned, labels, alias, did)
}));
-
- table.print();
+ term::print_with_pager(table)?;
Ok(())
}
diff --git a/crates/radicle-cli/src/commands/ls.rs b/crates/radicle-cli/src/commands/ls.rs
index e4398ec6..036165c1 100644
--- a/crates/radicle-cli/src/commands/ls.rs
+++ b/crates/radicle-cli/src/commands/ls.rs
@@ -5,8 +5,6 @@ use radicle::storage::{ReadStorage, RepositoryInfo};
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
-use term::Element;
-
pub const HELP: Help = Help {
name: "ls",
description: "List repositories",
@@ -157,7 +155,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
]);
table.divider();
table.extend(rows);
- table.print();
+ term::print_with_pager(table)?;
}
Ok(())
diff --git a/crates/radicle-cli/src/commands/node.rs b/crates/radicle-cli/src/commands/node.rs
index 1b217f7d..54b6b24e 100644
--- a/crates/radicle-cli/src/commands/node.rs
+++ b/crates/radicle-cli/src/commands/node.rs
@@ -14,7 +14,6 @@ use radicle::prelude::RepoId;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
-use crate::terminal::Element as _;
mod commands;
pub mod control;
@@ -323,7 +322,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Operation::Sessions => {
let sessions = control::sessions(&node)?;
if let Some(table) = sessions {
- table.print();
+ term::print_with_pager(table)?;
}
}
Operation::Events { timeout, count } => {
diff --git a/crates/radicle-cli/src/commands/node/control.rs b/crates/radicle-cli/src/commands/node/control.rs
index 6e70f908..9688129a 100644
--- a/crates/radicle-cli/src/commands/node/control.rs
+++ b/crates/radicle-cli/src/commands/node/control.rs
@@ -302,7 +302,7 @@ pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
let sessions = sessions(node)?;
if let Some(table) = sessions {
term::blank();
- table.print();
+ term::print_with_pager(table)?;
}
if profile.hints() {
diff --git a/crates/radicle-cli/src/commands/patch/list.rs b/crates/radicle-cli/src/commands/patch/list.rs
index ff9f2d45..c37cd535 100644
--- a/crates/radicle-cli/src/commands/patch/list.rs
+++ b/crates/radicle-cli/src/commands/patch/list.rs
@@ -9,7 +9,6 @@ use radicle::storage::git::Repository;
use term::format::Author;
use term::table::{Table, TableOptions};
-use term::Element as _;
use crate::terminal as term;
use crate::terminal::patch as common;
@@ -89,7 +88,7 @@ pub fn run(
.partition_result();
table.extend(rows);
- table.print();
+ term::print_with_pager(table)?;
if !errors.is_empty() {
for (title, id, error) in errors {
diff --git a/crates/radicle-cli/src/commands/patch/review/builder.rs b/crates/radicle-cli/src/commands/patch/review/builder.rs
index fe7f2448..3604f5a2 100644
--- a/crates/radicle-cli/src/commands/patch/review/builder.rs
+++ b/crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -452,7 +452,7 @@ impl FileReviewBuilder {
}
}
- fn item_diff(&mut self, item: ReviewItem) -> Result<git::raw::Diff, Error> {
+ fn item_diff(&mut self, item: ReviewItem) -> Result<git::raw::Diff<'_>, Error> {
let mut buf = Vec::new();
let mut writer = unified_diff::Writer::new(&mut buf);
writer.encode(&self.header)?;
diff --git a/crates/radicle-cli/src/commands/remote.rs b/crates/radicle-cli/src/commands/remote.rs
index 3cae7441..b180bfa5 100644
--- a/crates/radicle-cli/src/commands/remote.rs
+++ b/crates/radicle-cli/src/commands/remote.rs
@@ -207,20 +207,20 @@ pub fn run(options: Options, ctx: impl Context) -> anyhow::Result<()> {
// Only include a blank line if we're printing both tracked and untracked
let include_blank_line = !tracked.is_empty() && !untracked.is_empty();
- list::print_tracked(tracked.iter());
+ list::print_tracked(tracked.iter())?;
if include_blank_line {
term::blank();
}
- list::print_untracked(untracked.iter());
+ list::print_untracked(untracked.iter())?;
}
ListOption::Tracked => {
let tracked = list::tracked(&working)?;
- list::print_tracked(tracked.iter());
+ list::print_tracked(tracked.iter())?;
}
ListOption::Untracked => {
let tracked = list::tracked(&working)?;
let untracked = list::untracked(rid, &profile, tracked.iter())?;
- list::print_untracked(untracked.iter());
+ list::print_untracked(untracked.iter())?;
}
},
};
diff --git a/crates/radicle-cli/src/commands/remote/list.rs b/crates/radicle-cli/src/commands/remote/list.rs
index 7356cdd1..ebbe07fd 100644
--- a/crates/radicle-cli/src/commands/remote/list.rs
+++ b/crates/radicle-cli/src/commands/remote/list.rs
@@ -5,7 +5,7 @@ use radicle::identity::{Did, RepoId};
use radicle::node::{Alias, AliasStore as _, NodeId};
use radicle::storage::ReadStorage as _;
use radicle::Profile;
-use radicle_term::{Element, Table};
+use radicle_term::Table;
use crate::git;
use crate::terminal as term;
@@ -80,8 +80,8 @@ pub fn untracked<'a>(
.collect::<Result<Vec<_>, _>>()?)
}
-pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) {
- Table::from_iter(tracked.into_iter().flat_map(|Tracked { direction, name }| {
+pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) -> anyhow::Result<()> {
+ let table = Table::from_iter(tracked.into_iter().flat_map(|Tracked { direction, name }| {
let Some(direction) = direction else {
return None;
};
@@ -99,12 +99,13 @@ pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) {
description,
term::format::parens(term::format::secondary(dir.to_owned())),
])
- }))
- .print();
+ }));
+ term::print_with_pager(table)?;
+ Ok(())
}
-pub fn print_untracked<'a>(untracked: impl Iterator<Item = &'a Untracked>) {
- Table::from_iter(untracked.into_iter().map(|Untracked { remote, alias }| {
+pub fn print_untracked<'a>(untracked: impl Iterator<Item = &'a Untracked>) -> anyhow::Result<()> {
+ let table = Table::from_iter(untracked.into_iter().map(|Untracked { remote, alias }| {
[
match alias {
None => term::format::secondary("n/a".to_string()),
@@ -112,6 +113,7 @@ pub fn print_untracked<'a>(untracked: impl Iterator<Item = &'a Untracked>) {
},
term::format::highlight(Did::from(remote).to_string()),
]
- }))
- .print();
+ }));
+ term::print_with_pager(table)?;
+ Ok(())
}
diff --git a/crates/radicle-cli/src/commands/self.rs b/crates/radicle-cli/src/commands/self.rs
index 04cb11f6..8725caeb 100644
--- a/crates/radicle-cli/src/commands/self.rs
+++ b/crates/radicle-cli/src/commands/self.rs
@@ -2,11 +2,11 @@ use std::ffi::OsString;
use radicle::crypto::ssh;
use radicle::node::Handle as _;
-use radicle::{Node, Profile};
+use radicle::node::Node;
+use radicle::profile::Profile;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
-use crate::terminal::Element as _;
pub const HELP: Help = Help {
name: "self",
@@ -208,7 +208,7 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
term::format::tertiary(profile.home.node().display()).into(),
]);
- table.print();
+ term::print_with_pager(table)?;
Ok(())
}
diff --git a/crates/radicle-cli/src/commands/sync.rs b/crates/radicle-cli/src/commands/sync.rs
index 73b2cd52..a0ed1cd8 100644
--- a/crates/radicle-cli/src/commands/sync.rs
+++ b/crates/radicle-cli/src/commands/sync.rs
@@ -18,7 +18,6 @@ use radicle::prelude::{NodeId, Profile, RepoId};
use radicle::storage::ReadRepository;
use radicle::storage::RefUpdate;
use radicle::storage::{ReadStorage, RemoteRepository};
-use radicle_term::Element;
use crate::node::SyncReporting;
use crate::node::SyncSettings;
@@ -406,7 +405,7 @@ fn sync_status(
});
table.extend(seeds);
- table.print();
+ term::print_with_pager(table)?;
if profile.hints() {
const COLUMN_WIDTH: usize = 16;
diff --git a/crates/radicle-cli/src/git.rs b/crates/radicle-cli/src/git.rs
index 199f60d5..76c3b4aa 100644
--- a/crates/radicle-cli/src/git.rs
+++ b/crates/radicle-cli/src/git.rs
@@ -250,7 +250,7 @@ pub fn is_signing_configured(repo: &Path) -> Result<bool, anyhow::Error> {
}
/// Return the list of radicle remotes for the given repository.
-pub fn rad_remotes(repo: &git2::Repository) -> anyhow::Result<Vec<Remote>> {
+pub fn rad_remotes(repo: &git2::Repository) -> anyhow::Result<Vec<Remote<'_>>> {
let remotes: Vec<_> = repo
.remotes()?
.iter()
@@ -272,7 +272,7 @@ pub fn is_remote(repo: &git2::Repository, alias: &str) -> anyhow::Result<bool> {
}
/// Get the repository's "rad" remote.
-pub fn rad_remote(repo: &Repository) -> anyhow::Result<(git2::Remote, RepoId)> {
+pub fn rad_remote(repo: &Repository) -> anyhow::Result<(git2::Remote<'_>, RepoId)> {
match radicle::rad::remote(repo) {
Ok((remote, id)) => Ok((remote, id)),
Err(radicle::rad::RemoteError::NotFound(_)) => Err(anyhow!(
diff --git a/crates/radicle-cli/src/project.rs b/crates/radicle-cli/src/project.rs
index 37708d13..5be086ee 100644
--- a/crates/radicle-cli/src/project.rs
+++ b/crates/radicle-cli/src/project.rs
@@ -22,7 +22,7 @@ impl SetupRemote<'_> {
&self,
name: impl AsRef<RefStr>,
node: NodeId,
- ) -> anyhow::Result<(git::Remote, Option<BranchName>)> {
+ ) -> anyhow::Result<(git::Remote<'_>, Option<BranchName>)> {
let remote_url = radicle::git::Url::from(self.rid).with_namespace(node);
let remote_name = name.as_ref();
diff --git a/crates/radicle-cli/src/terminal.rs b/crates/radicle-cli/src/terminal.rs
index 86712223..4c65e81c 100644
--- a/crates/radicle-cli/src/terminal.rs
+++ b/crates/radicle-cli/src/terminal.rs
@@ -161,3 +161,11 @@ pub fn fail(_name: &str, error: &anyhow::Error) {
io::hint(hint);
}
}
+
+/// Print an element with automatic paging when there are too many lines.
+/// This function automatically detects if the content is too long for the terminal
+/// and uses a pager if necessary.
+pub fn print_with_pager(elem: impl Element) -> anyhow::Result<()> {
+ elem.print_with_pager()?;
+ Ok(())
+}
diff --git a/crates/radicle-cli/src/terminal/patch.rs b/crates/radicle-cli/src/terminal/patch.rs
index 75f70423..b5784f96 100644
--- a/crates/radicle-cli/src/terminal/patch.rs
+++ b/crates/radicle-cli/src/terminal/patch.rs
@@ -307,21 +307,19 @@ pub fn get_update_message(
/// List the given commits in a table.
pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> {
- commits
+ let table: term::Table<2, _> = commits
.iter()
.map(|commit| {
let message = commit
.summary_bytes()
.unwrap_or_else(|| commit.message_bytes());
-
[
term::format::secondary(term::format::oid(commit.id()).into()),
term::format::italic(String::from_utf8_lossy(message).to_string()),
]
})
- .collect::<term::Table<2, _>>()
- .print();
-
+ .collect();
+ term::print_with_pager(table)?;
Ok(())
}
diff --git a/crates/radicle-cli/src/terminal/patch/common.rs b/crates/radicle-cli/src/terminal/patch/common.rs
index 35e1dcf3..8d2074e8 100644
--- a/crates/radicle-cli/src/terminal/patch/common.rs
+++ b/crates/radicle-cli/src/terminal/patch/common.rs
@@ -97,7 +97,7 @@ pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result<Vec
}
#[inline]
-pub fn try_branch(reference: git::raw::Reference<'_>) -> anyhow::Result<git::raw::Branch> {
+pub fn try_branch(reference: git::raw::Reference<'_>) -> anyhow::Result<git::raw::Branch<'_>> {
let branch = if reference.is_branch() {
git::raw::Branch::wrap(reference)
} else {
diff --git a/crates/radicle-cli/tests/commands.rs b/crates/radicle-cli/tests/commands.rs
index 5a8b8f3a..0f2fa4c3 100644
--- a/crates/radicle-cli/tests/commands.rs
+++ b/crates/radicle-cli/tests/commands.rs
@@ -1790,6 +1790,12 @@ fn rad_sync() {
[],
)
.unwrap();
+
+ // Explicit cleanup order to prevent panic during teardown
+ // Shutdown nodes in reverse order of creation
+ drop(eve);
+ drop(bob);
+ drop(alice);
}
#[test]
diff --git a/crates/radicle-cli/tests/util/formula.rs b/crates/radicle-cli/tests/util/formula.rs
index fe44a0de..02d10c9b 100644
--- a/crates/radicle-cli/tests/util/formula.rs
+++ b/crates/radicle-cli/tests/util/formula.rs
@@ -24,6 +24,8 @@ pub(crate) fn formula(
.env("TZ", "UTC")
.env("LANG", "C")
.env("USER", "alice")
+ .env("COLUMNS", "120")
+ .env("LINES", "30")
.env(env::RAD_PASSPHRASE, "radicle")
.env(env::RAD_KEYGEN_SEED, RAD_SEED)
.env(env::RAD_RNG_SEED, "0")
diff --git a/crates/radicle-crypto/src/lib.rs b/crates/radicle-crypto/src/lib.rs
index 5a60a4b1..17a25df7 100644
--- a/crates/radicle-crypto/src/lib.rs
+++ b/crates/radicle-crypto/src/lib.rs
@@ -357,7 +357,7 @@ impl PublicKey {
}
#[cfg(feature = "radicle-git-ext")]
- pub fn to_component(&self) -> radicle_git_ext::ref_format::Component {
+ pub fn to_component(&self) -> radicle_git_ext::ref_format::Component<'_> {
radicle_git_ext::ref_format::Component::from(self)
}
diff --git a/crates/radicle-fetch/src/git/repository/error.rs b/crates/radicle-fetch/src/git/repository/error.rs
index 6c2d546c..dd680a66 100644
--- a/crates/radicle-fetch/src/git/repository/error.rs
+++ b/crates/radicle-fetch/src/git/repository/error.rs
@@ -40,6 +40,7 @@ pub struct Resolve {
#[derive(Debug, Error)]
#[error("failed to scan for refs matching {pattern}")]
+#[allow(dead_code)]
pub struct Scan {
pub pattern: radicle::git::PatternString,
#[source]
diff --git a/crates/radicle-node/build.rs b/crates/radicle-node/build.rs
deleted file mode 120000
index c6b197af..00000000
--- a/crates/radicle-node/build.rs
+++ /dev/null
@@ -1 +0,0 @@
-../../build.rs
\ No newline at end of file
diff --git a/crates/radicle-node/build.rs b/crates/radicle-node/build.rs
new file mode 100644
index 00000000..34d54253
--- /dev/null
+++ b/crates/radicle-node/build.rs
@@ -0,0 +1,54 @@
+use std::env;
+use std::process::Command;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Set a build-time `GIT_HEAD` env var which includes the commit id;
+ // such that we can tell which code is running.
+ let hash = env::var("GIT_HEAD").unwrap_or_else(|_| {
+ Command::new("git")
+ .arg("rev-parse")
+ .arg("--short")
+ .arg("HEAD")
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .unwrap_or("unknown".into())
+ });
+
+ let version = if let Ok(version) = env::var("RADICLE_VERSION") {
+ version
+ } else {
+ "pre-release".to_owned()
+ };
+
+ // Set a build-time `SOURCE_DATE_EPOCH` env var which includes the commit time.
+ let commit_time = env::var("SOURCE_DATE_EPOCH").unwrap_or_else(|_| {
+ Command::new("git")
+ .arg("log")
+ .arg("-1")
+ .arg("--pretty=%ct")
+ .arg("HEAD")
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .unwrap_or(0.to_string())
+ });
+
+ println!("cargo::rustc-env=RADICLE_VERSION={version}");
+ println!("cargo::rustc-env=SOURCE_DATE_EPOCH={commit_time}");
+ println!("cargo::rustc-env=GIT_HEAD={hash}");
+
+ Ok(())
+}
diff --git a/crates/radicle-node/src/test/environment.rs b/crates/radicle-node/src/test/environment.rs
index edf98c13..8994f4e5 100644
--- a/crates/radicle-node/src/test/environment.rs
+++ b/crates/radicle-node/src/test/environment.rs
@@ -206,13 +206,23 @@ impl<G: crypto::signature::Signer<crypto::Signature> + cyphernet::Ecdh + 'static
fn drop(&mut self) {
log::debug!(target: "test", "Node {} shutting down..", self.id);
- unsafe { ManuallyDrop::take(&mut self.handle) }
- .shutdown()
- .unwrap();
- unsafe { ManuallyDrop::take(&mut self.thread) }
- .join()
- .unwrap()
- .unwrap();
+ // Shutdown the handle with error handling to prevent panic during cleanup
+ if let Err(e) = unsafe { ManuallyDrop::take(&mut self.handle) }.shutdown() {
+ log::warn!(target: "test", "Failed to shutdown node {}: {}", self.id, e);
+ }
+
+ // Join the thread with error handling to prevent panic during cleanup
+ match unsafe { ManuallyDrop::take(&mut self.thread) }.join() {
+ Ok(Ok(())) => {
+ log::debug!(target: "test", "Node {} shutdown completed", self.id);
+ }
+ Ok(Err(e)) => {
+ log::warn!(target: "test", "Node {} runtime error during shutdown: {}", self.id, e);
+ }
+ Err(_) => {
+ log::warn!(target: "test", "Node {} thread panicked during shutdown", self.id);
+ }
+ }
}
}
diff --git a/crates/radicle-node/src/test/node.rs b/crates/radicle-node/src/test/node.rs
index 95e6f916..a96e1f8b 100644
--- a/crates/radicle-node/src/test/node.rs
+++ b/crates/radicle-node/src/test/node.rs
@@ -85,13 +85,23 @@ impl<G: 'static> Drop for NodeHandle<G> {
fn drop(&mut self) {
log::debug!(target: "test", "Node {} shutting down..", self.id);
- unsafe { ManuallyDrop::take(&mut self.handle) }
- .shutdown()
- .unwrap();
- unsafe { ManuallyDrop::take(&mut self.thread) }
- .join()
- .unwrap()
- .unwrap();
+ // Shutdown the handle with error handling to prevent panic during cleanup
+ if let Err(e) = unsafe { ManuallyDrop::take(&mut self.handle) }.shutdown() {
+ log::warn!(target: "test", "Failed to shutdown node {}: {}", self.id, e);
+ }
+
+ // Join the thread with error handling to prevent panic during cleanup
+ match unsafe { ManuallyDrop::take(&mut self.thread) }.join() {
+ Ok(Ok(())) => {
+ log::debug!(target: "test", "Node {} shutdown completed", self.id);
+ }
+ Ok(Err(e)) => {
+ log::warn!(target: "test", "Node {} runtime error during shutdown: {}", self.id, e);
+ }
+ Err(_) => {
+ log::warn!(target: "test", "Node {} thread panicked during shutdown", self.id);
+ }
+ }
}
}
diff --git a/crates/radicle-node/src/wire.rs b/crates/radicle-node/src/wire.rs
index d8dd07fd..418ac98d 100644
--- a/crates/radicle-node/src/wire.rs
+++ b/crates/radicle-node/src/wire.rs
@@ -258,7 +258,7 @@ impl Peers {
self.0.get_mut(id)
}
- fn entry(&mut self, id: ResourceId) -> Entry<ResourceId, Peer> {
+ fn entry(&mut self, id: ResourceId) -> Entry<'_, ResourceId, Peer> {
self.0.entry(id)
}
diff --git a/crates/radicle-protocol/src/bounded.rs b/crates/radicle-protocol/src/bounded.rs
index 1687de4b..06e51aa0 100644
--- a/crates/radicle-protocol/src/bounded.rs
+++ b/crates/radicle-protocol/src/bounded.rs
@@ -160,7 +160,7 @@ impl<T, const N: usize> BoundedVec<T, N> {
}
/// Calls [`std::vec::Drain`].
- pub fn drain<R: RangeBounds<usize>>(&mut self, range: R) -> std::vec::Drain<T> {
+ pub fn drain<R: RangeBounds<usize>>(&mut self, range: R) -> std::vec::Drain<'_, T> {
self.v.drain(range)
}
}
diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs
index 558ab056..3aa8fa08 100644
--- a/crates/radicle-protocol/src/service.rs
+++ b/crates/radicle-protocol/src/service.rs
@@ -1089,7 +1089,7 @@ where
from: &NodeId,
refs_at: Vec<RefsAt>,
timeout: time::Duration,
- ) -> Result<&mut FetchState, TryFetchError> {
+ ) -> Result<&mut FetchState, TryFetchError<'_>> {
let from = *from;
let Some(session) = self.sessions.get_mut(&from) else {
return Err(TryFetchError::SessionNotConnected);
diff --git a/crates/radicle-remote-helper/build.rs b/crates/radicle-remote-helper/build.rs
deleted file mode 120000
index c6b197af..00000000
--- a/crates/radicle-remote-helper/build.rs
+++ /dev/null
@@ -1 +0,0 @@
-../../build.rs
\ No newline at end of file
diff --git a/crates/radicle-remote-helper/build.rs b/crates/radicle-remote-helper/build.rs
new file mode 100644
index 00000000..34d54253
--- /dev/null
+++ b/crates/radicle-remote-helper/build.rs
@@ -0,0 +1,54 @@
+use std::env;
+use std::process::Command;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Set a build-time `GIT_HEAD` env var which includes the commit id;
+ // such that we can tell which code is running.
+ let hash = env::var("GIT_HEAD").unwrap_or_else(|_| {
+ Command::new("git")
+ .arg("rev-parse")
+ .arg("--short")
+ .arg("HEAD")
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .unwrap_or("unknown".into())
+ });
+
+ let version = if let Ok(version) = env::var("RADICLE_VERSION") {
+ version
+ } else {
+ "pre-release".to_owned()
+ };
+
+ // Set a build-time `SOURCE_DATE_EPOCH` env var which includes the commit time.
+ let commit_time = env::var("SOURCE_DATE_EPOCH").unwrap_or_else(|_| {
+ Command::new("git")
+ .arg("log")
+ .arg("-1")
+ .arg("--pretty=%ct")
+ .arg("HEAD")
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .unwrap_or(0.to_string())
+ });
+
+ println!("cargo::rustc-env=RADICLE_VERSION={version}");
+ println!("cargo::rustc-env=SOURCE_DATE_EPOCH={commit_time}");
+ println!("cargo::rustc-env=GIT_HEAD={hash}");
+
+ Ok(())
+}
diff --git a/crates/radicle-ssh/src/encoding.rs b/crates/radicle-ssh/src/encoding.rs
index a4599437..b3ebf647 100644
--- a/crates/radicle-ssh/src/encoding.rs
+++ b/crates/radicle-ssh/src/encoding.rs
@@ -167,11 +167,11 @@ impl Encoding for Buffer {
/// A cursor-like trait to read SSH-encoded things.
pub trait Reader {
/// Create an SSH reader for `self`.
- fn reader(&self, starting_at: usize) -> Cursor;
+ fn reader(&self, starting_at: usize) -> Cursor<'_>;
}
impl Reader for Buffer {
- fn reader(&self, starting_at: usize) -> Cursor {
+ fn reader(&self, starting_at: usize) -> Cursor<'_> {
Cursor {
s: self,
position: starting_at,
@@ -180,7 +180,7 @@ impl Reader for Buffer {
}
impl Reader for [u8] {
- fn reader(&self, starting_at: usize) -> Cursor {
+ fn reader(&self, starting_at: usize) -> Cursor<'_> {
Cursor {
s: self,
position: starting_at,
diff --git a/crates/radicle-term/src/element.rs b/crates/radicle-term/src/element.rs
index feee19a9..813dd394 100644
--- a/crates/radicle-term/src/element.rs
+++ b/crates/radicle-term/src/element.rs
@@ -56,7 +56,13 @@ impl Constraint {
/// Returns [`None`] if the output device is not a terminal.
pub fn from_env() -> Option<Self> {
if io::stdout().is_terminal() {
- Some(Self::max(viewport().unwrap_or(Size::MAX)))
+ // Use our more reliable terminal size detection instead of the broken viewport() function
+ if let Some((cols, rows)) = get_terminal_size() {
+ Some(Self::max(Size::new(cols, rows)))
+ } else {
+ // Fallback to viewport if our detection fails
+ Some(Self::max(viewport().unwrap_or(Size::MAX)))
+ }
} else {
None
}
@@ -89,6 +95,32 @@ pub trait Element: fmt::Debug + Send + Sync {
}
}
+ /// Print this element to stdout, automatically using a pager if there are too many lines.
+ /// This method is preferred over `print()` when you want automatic paging behavior.
+ fn print_with_pager(&self) -> io::Result<()> {
+ // Only use pager if we're on a terminal and not in tests
+ if !io::stdout().is_terminal() || cfg!(test) {
+ // Not on a terminal or running in tests, just print normally
+ self.print();
+ return Ok(());
+ }
+
+ // Get the actual content size using UNBOUNDED constraint
+ let element_rows = self.size(Constraint::UNBOUNDED).rows;
+
+ // Try to get terminal size to determine if paging is needed
+ if let Some((_, terminal_rows)) = get_terminal_size() {
+ // Use pager if the element is too tall for the terminal
+ if element_rows > terminal_rows {
+ return crate::pager::run(self);
+ }
+ }
+
+ // Otherwise, just print normally
+ self.print();
+ Ok(())
+ }
+
/// Write using the given constraints to `stdout`.
fn write(&self, constraints: Constraint) -> io::Result<()>
where
@@ -121,6 +153,10 @@ impl Element for Box<dyn Element + '_> {
fn print(&self) {
self.deref().print()
}
+
+ fn print_with_pager(&self) -> io::Result<()> {
+ self.deref().print_with_pager()
+ }
}
impl<T: Element> Element for &T {
@@ -135,11 +171,15 @@ impl<T: Element> Element for &T {
fn print(&self) {
(*self).print()
}
+
+ fn print_with_pager(&self) -> io::Result<()> {
+ (*self).print_with_pager()
+ }
}
/// Write using the given constraints, to a writer.
pub fn write_to(
- elem: &impl Element,
+ elem: &(impl Element + ?Sized),
writer: &mut impl io::Write,
constraints: Constraint,
) -> io::Result<()> {
@@ -369,6 +409,45 @@ impl Size {
}
}
+/// Get terminal size with fallback methods
+pub fn get_terminal_size() -> Option<(usize, usize)> {
+ // First try crossterm
+ if let Some(size) = crate::viewport() {
+ // Validate the size - if it's too small, it's probably wrong
+ if size.rows >= 10 && size.cols >= 20 {
+ return Some((size.cols, size.rows));
+ }
+ }
+
+ // Fallback to environment variables
+ if let (Ok(lines), Ok(cols)) = (std::env::var("LINES"), std::env::var("COLUMNS")) {
+ if let (Ok(lines), Ok(cols)) = (lines.parse::<usize>(), cols.parse::<usize>()) {
+ if lines >= 10 && cols >= 20 {
+ return Some((cols, lines));
+ }
+ }
+ }
+
+ // Fallback to stty command
+ if let Ok(output) = std::process::Command::new("stty").arg("size").output() {
+ if let Ok(output_str) = String::from_utf8(output.stdout) {
+ let parts: Vec<&str> = output_str.split_whitespace().collect();
+ if parts.len() == 2 {
+ if let (Ok(lines), Ok(cols)) =
+ (parts[1].parse::<usize>(), parts[0].parse::<usize>())
+ {
+ if lines >= 10 && cols >= 20 {
+ return Some((cols, lines));
+ }
+ }
+ }
+ }
+ }
+
+ // Final fallback: assume reasonable defaults for modern terminals
+ Some((80, 24))
+}
+
#[cfg(test)]
mod test {
use super::*;
diff --git a/crates/radicle-term/src/lib.rs b/crates/radicle-term/src/lib.rs
index 2113c6f7..2fe10bee 100644
--- a/crates/radicle-term/src/lib.rs
+++ b/crates/radicle-term/src/lib.rs
@@ -7,6 +7,7 @@ pub mod format;
pub mod hstack;
pub mod io;
pub mod label;
+pub mod pager;
pub mod spinner;
pub mod table;
pub mod textarea;
diff --git a/crates/radicle-term/src/pager.rs b/crates/radicle-term/src/pager.rs
new file mode 100644
index 00000000..9f92b809
--- /dev/null
+++ b/crates/radicle-term/src/pager.rs
@@ -0,0 +1,115 @@
+use std::io::{self, IsTerminal};
+use std::process::{Command, Stdio};
+
+use crate::element::{self, get_terminal_size, Constraint, Element};
+
+/// Output the given element through a pager, if necessary.
+/// If it fits within the screen, don't run it through a pager.
+pub fn run(elem: &(impl Element + ?Sized)) -> io::Result<()> {
+ // Only use pager if we're on a terminal and not in tests
+ if !io::stdout().is_terminal() || cfg!(test) {
+ // Not on a terminal or running in tests, just write directly to stdout
+ return element::write_to(elem, &mut io::stdout(), Constraint::UNBOUNDED);
+ }
+
+ // Note: We don't need to create a constraint here since we use UNBOUNDED when rendering to pager
+ // The pager itself handles width constraints
+
+ let Some((_, _rows)) = get_terminal_size() else {
+ return element::write_to(elem, &mut io::stdout(), Constraint::UNBOUNDED);
+ };
+
+ // Get the actual content size using UNBOUNDED constraint
+ let element_rows = elem.size(Constraint::UNBOUNDED).rows;
+
+ // Check if the element fits within the terminal
+ if let Some((_, terminal_rows)) = get_terminal_size() {
+ // If the element fits within the terminal, don't use pager
+ if element_rows <= terminal_rows {
+ return element::write_to(elem, &mut io::stdout(), Constraint::UNBOUNDED);
+ }
+ }
+
+ // Get the pager command, but validate it's a real pager
+ let pager = std::env::var("PAGER")
+ .ok()
+ .or_else(|| std::env::var("LESS").ok())
+ .or_else(|| Some("more".to_string()));
+
+ // Validate that the pager is a real pager, not a shell command
+ let pager = if let Some(ref pager_cmd) = pager {
+ if pager_cmd.contains("sh -c") || pager_cmd.contains("head") || pager_cmd.contains("cat") {
+ // PAGER appears to be a shell command, fall back to 'more' (safer with colors)
+ "more".to_string()
+ } else {
+ pager_cmd.clone()
+ }
+ } else {
+ "more".to_string()
+ };
+ let Some(parts) = shlex::split(&pager) else {
+ // Fallback to 'more' if pager parsing fails
+ let mut child = Command::new("more")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .spawn()?;
+
+ let writer = child.stdin.as_mut().unwrap();
+ let result = element::write_to(elem, writer, Constraint::UNBOUNDED);
+
+ child.wait()?;
+
+ match result {
+ Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {}
+ Err(e) => return Err(e),
+ Ok(_) => {}
+ }
+
+ return Ok(());
+ };
+ let Some((program, args)) = parts.split_first() else {
+ // Fallback to 'more' if pager parsing fails
+ let mut child = Command::new("more")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .spawn()?;
+
+ let writer = child.stdin.as_mut().unwrap();
+ let result = element::write_to(elem, writer, Constraint::UNBOUNDED);
+
+ child.wait()?;
+
+ match result {
+ Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {}
+ Err(e) => return Err(e),
+ Ok(_) => {}
+ }
+
+ return Ok(());
+ };
+
+ let mut child = Command::new(program)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .args(args)
+ .spawn()?;
+
+ let writer = child.stdin.as_mut().unwrap();
+ // Use UNBOUNDED constraint when rendering to pager to preserve table formatting
+ // The pager itself will handle width constraints
+ let result = element::write_to(elem, writer, Constraint::UNBOUNDED);
+
+ child.wait()?;
+
+ match result {
+ // This error is expected when the pager is exited.
+ Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {}
+ Err(e) => return Err(e),
+ Ok(_) => {}
+ }
+
+ Ok(())
+}
diff --git a/crates/radicle-term/src/table.rs b/crates/radicle-term/src/table.rs
index 6e4e1aef..ed1a2f4c 100644
--- a/crates/radicle-term/src/table.rs
+++ b/crates/radicle-term/src/table.rs
@@ -233,6 +233,20 @@ impl<const W: usize, T: Cell> Table<W, T> {
cols += 2 + padding;
rows += 2;
}
+
+ // If the constraint is effectively UNBOUNDED (max is Size::MAX), use a reasonable maximum width
+ // This preserves table formatting when rendering to pager without making it excessively wide
+ if c.max == Size::MAX {
+ // Use a reasonable maximum width (e.g., 120 columns) to preserve formatting
+ // but avoid making the table excessively wide
+ let max_width = 120;
+ if cols > max_width {
+ return Size::new(max_width, rows);
+ }
+ return Size::new(cols, rows);
+ }
+
+ // Otherwise, apply the constraint
Size::new(cols, rows).constrain(c)
}
}
diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs
index 938936fd..6c4772af 100644
--- a/crates/radicle/src/cob/identity.rs
+++ b/crates/radicle/src/cob/identity.rs
@@ -273,7 +273,7 @@ impl Identity {
pub fn load_mut<R: WriteRepository + cob::Store<Namespace = NodeId>>(
repo: &R,
- ) -> Result<IdentityMut<R>, RepositoryError> {
+ ) -> Result<IdentityMut<'_, R>, RepositoryError> {
let oid = repo.identity_root()?;
let oid = ObjectId::from(oid);
diff --git a/crates/radicle/src/cob/issue.rs b/crates/radicle/src/cob/issue.rs
index f771301c..26167efc 100644
--- a/crates/radicle/src/cob/issue.rs
+++ b/crates/radicle/src/cob/issue.rs
@@ -1390,7 +1390,7 @@ mod test {
.create(
cob::Title::new("My first issue").unwrap(),
"Blah blah blah.",
- &[ux_label.clone()],
+ std::slice::from_ref(&ux_label),
&[],
[],
&node.signer,
diff --git a/crates/radicle/src/cob/patch.rs b/crates/radicle/src/cob/patch.rs
index 64422550..ad6e5c0c 100644
--- a/crates/radicle/src/cob/patch.rs
+++ b/crates/radicle/src/cob/patch.rs
@@ -2419,7 +2419,7 @@ where
revision: RevisionId,
commit: git::Oid,
signer: &Device<G>,
- ) -> Result<Merged<R>, Error>
+ ) -> Result<Merged<'_, R>, Error>
where
G: crypto::signature::Signer<crypto::Signature>,
{
diff --git a/crates/radicle/src/git.rs b/crates/radicle/src/git.rs
index 9c1769dc..b829065c 100644
--- a/crates/radicle/src/git.rs
+++ b/crates/radicle/src/git.rs
@@ -298,7 +298,7 @@ pub mod refs {
///
/// `refs/namespaces/<remote>/refs/rad/id`
///
- pub fn id(remote: &RemoteId) -> Namespaced {
+ pub fn id(remote: &RemoteId) -> Namespaced<'_> {
IDENTITY_BRANCH.with_namespace(remote.into())
}
@@ -306,7 +306,7 @@ pub mod refs {
///
/// `refs/namespaces/<remote>/refs/rad/root`
///
- pub fn id_root(remote: &RemoteId) -> Namespaced {
+ pub fn id_root(remote: &RemoteId) -> Namespaced<'_> {
IDENTITY_ROOT.with_namespace(remote.into())
}
@@ -315,7 +315,7 @@ pub mod refs {
///
/// `refs/namespaces/<remote>/refs/rad/sigrefs`
///
- pub fn sigrefs(remote: &RemoteId) -> Namespaced {
+ pub fn sigrefs(remote: &RemoteId) -> Namespaced<'_> {
SIGREFS_BRANCH.with_namespace(remote.into())
}
@@ -497,7 +497,7 @@ pub fn remote_refs(url: &Url) -> Result<RandomMap<RemoteId, Refs>, ListRefsError
/// The `T` can be specified when calling the function. For example, if you
/// wanted to parse the namespace as a `PublicKey`, then you would the function
/// like so, `parse_ref_namespaced::<PublicKey>(s)`.
-pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, format::Qualified), RefError>
+pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, format::Qualified<'_>), RefError>
where
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
@@ -526,7 +526,7 @@ where
/// The `T` can be specified when calling the function. For example, if you
/// wanted to parse the namespace as a `PublicKey`, then you would the function
/// like so, `parse_ref::<PublicKey>(s)`.
-pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, format::Qualified), RefError>
+pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, format::Qualified<'_>), RefError>
where
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
@@ -599,7 +599,7 @@ pub fn empty_commit<'a>(
}
/// Get the repository head.
-pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
+pub fn head(repo: &git2::Repository) -> Result<git2::Commit<'_>, git2::Error> {
let head = repo.head()?.peel_to_commit()?;
Ok(head)
diff --git a/crates/radicle/src/git/canonical/rules.rs b/crates/radicle/src/git/canonical/rules.rs
index ed0f6271..e81810ec 100644
--- a/crates/radicle/src/git/canonical/rules.rs
+++ b/crates/radicle/src/git/canonical/rules.rs
@@ -511,7 +511,7 @@ pub struct MatchedRule<'a> {
impl MatchedRule<'_> {
/// Return the reference name that was used for checking if it was a match.
- pub fn refname(&self) -> &Qualified {
+ pub fn refname(&self) -> &Qualified<'_> {
&self.refname
}
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index db27c47d..b505fafa 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -830,7 +830,7 @@ impl Doc {
pub(crate) fn blob_at<R: ReadRepository>(
commit: Oid,
repo: &R,
- ) -> Result<git2::Blob, DocError> {
+ ) -> Result<git2::Blob<'_>, DocError> {
let path = Path::new("embeds").join(*PATH);
repo.blob_at(commit, path.as_path()).map_err(DocError::from)
}
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index 92892959..1d453c5e 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -509,10 +509,10 @@ pub trait ReadRepository: Sized + ValidateRepository {
fn path(&self) -> &Path;
/// Get a blob in this repository at the given commit and path.
- fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob, git_ext::Error>;
+ fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob<'_>, git_ext::Error>;
/// Get a blob in this repository, given its id.
- fn blob(&self, oid: Oid) -> Result<git2::Blob, git_ext::Error>;
+ fn blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git_ext::Error>;
/// Get the head of this repository.
///
@@ -520,14 +520,14 @@ pub trait ReadRepository: Sized + ValidateRepository {
/// head using [`ReadRepository::canonical_head`].
///
/// Returns the [`Oid`] as well as the qualified reference name.
- fn head(&self) -> Result<(Qualified, Oid), RepositoryError>;
+ fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError>;
/// Compute the canonical head of this repository.
///
/// Ignores any existing `HEAD` reference.
///
/// Returns the [`Oid`] as well as the qualified reference name.
- fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError>;
+ fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError>;
/// Get the head of the `rad/id` reference in this repository.
///
@@ -572,15 +572,15 @@ pub trait ReadRepository: Sized + ValidateRepository {
&self,
remote: &RemoteId,
reference: &Qualified,
- ) -> Result<git2::Reference, git_ext::Error>;
+ ) -> Result<git2::Reference<'_>, git_ext::Error>;
/// Get the [`git2::Commit`] found using its `oid`.
///
/// Returns `Err` if the commit did not exist.
- fn commit(&self, oid: Oid) -> Result<git2::Commit, git::ext::Error>;
+ fn commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git::ext::Error>;
/// Perform a revision walk of a commit history starting from the given head.
- fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
+ fn revwalk(&self, head: Oid) -> Result<git2::Revwalk<'_>, git2::Error>;
/// Check if the underlying ODB contains the given `oid`.
fn contains(&self, oid: Oid) -> Result<bool, git2::Error>;
@@ -606,7 +606,7 @@ pub trait ReadRepository: Sized + ValidateRepository {
fn references_glob(
&self,
pattern: &git::PatternStr,
- ) -> Result<Vec<(Qualified, Oid)>, git::ext::Error>;
+ ) -> Result<Vec<(Qualified<'_>, Oid)>, git::ext::Error>;
/// Get repository delegates.
fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 4bc313d1..4918aec6 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -657,7 +657,7 @@ impl ReadRepository for Repository {
self.backend.path()
}
- fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob, git::Error> {
+ fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob<'_>, git::Error> {
let commit = self.backend.find_commit(*commit)?;
let tree = commit.tree()?;
let entry = tree.get_path(path.as_ref())?;
@@ -671,7 +671,7 @@ impl ReadRepository for Repository {
Ok(blob)
}
- fn blob(&self, oid: Oid) -> Result<git2::Blob, git::Error> {
+ fn blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git::Error> {
self.backend.find_blob(oid.into()).map_err(git::Error::from)
}
@@ -679,7 +679,7 @@ impl ReadRepository for Repository {
&self,
remote: &RemoteId,
name: &git::Qualified,
- ) -> Result<git2::Reference, git::Error> {
+ ) -> Result<git2::Reference<'_>, git::Error> {
let name = name.with_namespace(remote.into());
self.backend.find_reference(&name).map_err(git::Error::from)
}
@@ -695,13 +695,13 @@ impl ReadRepository for Repository {
Ok(oid.into())
}
- fn commit(&self, oid: Oid) -> Result<git2::Commit, git::Error> {
+ fn commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git::Error> {
self.backend
.find_commit(oid.into())
.map_err(git::Error::from)
}
- fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
+ fn revwalk(&self, head: Oid) -> Result<git2::Revwalk<'_>, git2::Error> {
let mut revwalk = self.backend.revwalk()?;
revwalk.push(head.into())?;
@@ -749,7 +749,7 @@ impl ReadRepository for Repository {
fn references_glob(
&self,
pattern: &PatternStr,
- ) -> Result<Vec<(Qualified, Oid)>, git::ext::Error> {
+ ) -> Result<Vec<(Qualified<'_>, Oid)>, git::ext::Error> {
let mut refs = Vec::new();
for r in self.backend.references_glob(pattern)? {
@@ -774,7 +774,7 @@ impl ReadRepository for Repository {
Doc::load_at(head, self)
}
- fn head(&self) -> Result<(Qualified, Oid), RepositoryError> {
+ fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
// If `HEAD` is already set locally, just return that.
if let Ok(head) = self.backend.head() {
if let Ok((name, oid)) = git::refs::qualified_from(&head) {
@@ -784,7 +784,7 @@ impl ReadRepository for Repository {
self.canonical_head()
}
- fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> {
+ fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
let doc = self.identity_doc()?;
let refname = git::refs::branch(doc.project()?.default_branch());
let crefs = match doc.canonical_refs()? {
diff --git a/crates/radicle/src/storage/git/cob.rs b/crates/radicle/src/storage/git/cob.rs
index 6f8fee88..49959207 100644
--- a/crates/radicle/src/storage/git/cob.rs
+++ b/crates/radicle/src/storage/git/cob.rs
@@ -278,11 +278,11 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
self.repo.is_empty()
}
- fn head(&self) -> Result<(Qualified, Oid), RepositoryError> {
+ fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
self.repo.head()
}
- fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> {
+ fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
self.repo.canonical_head()
}
@@ -290,11 +290,11 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
self.repo.path()
}
- fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error> {
+ fn commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git_ext::Error> {
self.repo.commit(oid)
}
- fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
+ fn revwalk(&self, head: Oid) -> Result<git2::Revwalk<'_>, git2::Error> {
self.repo.revwalk(head)
}
@@ -310,11 +310,11 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
&self,
oid: git_ext::Oid,
path: P,
- ) -> Result<git2::Blob, git_ext::Error> {
+ ) -> Result<git2::Blob<'_>, git_ext::Error> {
self.repo.blob_at(oid, path)
}
- fn blob(&self, oid: git_ext::Oid) -> Result<raw::Blob, ext::Error> {
+ fn blob(&self, oid: git_ext::Oid) -> Result<raw::Blob<'_>, ext::Error> {
self.repo.blob(oid)
}
@@ -322,7 +322,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
&self,
remote: &RemoteId,
reference: &git::Qualified,
- ) -> Result<git2::Reference, git_ext::Error> {
+ ) -> Result<git2::Reference<'_>, git_ext::Error> {
self.repo.reference(remote, reference)
}
@@ -341,7 +341,7 @@ impl<R: storage::ReadRepository> ReadRepository for DraftStore<'_, R> {
fn references_glob(
&self,
pattern: &git::PatternStr,
- ) -> Result<Vec<(fmt::Qualified, Oid)>, git::ext::Error> {
+ ) -> Result<Vec<(fmt::Qualified<'_>, Oid)>, git::ext::Error> {
self.repo.references_glob(pattern)
}
diff --git a/crates/radicle/src/storage/refs.rs b/crates/radicle/src/storage/refs.rs
index be5b6afa..856cc3c8 100644
--- a/crates/radicle/src/storage/refs.rs
+++ b/crates/radicle/src/storage/refs.rs
@@ -400,7 +400,7 @@ impl RefsAt {
SignedRefsAt::load_at(self.at, self.remote, repo)
}
- pub fn path(&self) -> &git::Qualified {
+ pub fn path(&self) -> &git::Qualified<'_> {
&SIGREFS_BRANCH
}
}
diff --git a/crates/radicle/src/test/fixtures.rs b/crates/radicle/src/test/fixtures.rs
index 67da9412..9e46a326 100644
--- a/crates/radicle/src/test/fixtures.rs
+++ b/crates/radicle/src/test/fixtures.rs
@@ -178,7 +178,7 @@ pub fn tag(name: &str, message: &str, commit: git2::Oid, repo: &git2::Repository
}
/// Populate a repository with commits, branches and blobs.
-pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
+pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified<'_>> {
assert!(
scale <= 8,
"Scale parameter must be less than or equal to 8"
diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs
index a2ea5db6..7205d886 100644
--- a/crates/radicle/src/test/storage.rs
+++ b/crates/radicle/src/test/storage.rs
@@ -203,11 +203,11 @@ impl ReadRepository for MockRepository {
Ok(self.remotes.is_empty())
}
- fn head(&self) -> Result<(fmt::Qualified, Oid), RepositoryError> {
+ fn head(&self) -> Result<(fmt::Qualified<'_>, Oid), RepositoryError> {
Ok((fmt::qualified!("refs/heads/master"), arbitrary::oid()))
}
- fn canonical_head(&self) -> Result<(fmt::Qualified, Oid), RepositoryError> {
+ fn canonical_head(&self) -> Result<(fmt::Qualified<'_>, Oid), RepositoryError> {
todo!()
}
@@ -215,13 +215,13 @@ impl ReadRepository for MockRepository {
todo!()
}
- fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error> {
+ fn commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git_ext::Error> {
Err(git_ext::Error::NotFound(git_ext::NotFound::NoSuchObject(
*oid,
)))
}
- fn revwalk(&self, _head: Oid) -> Result<git2::Revwalk, git2::Error> {
+ fn revwalk(&self, _head: Oid) -> Result<git2::Revwalk<'_>, git2::Error> {
todo!()
}
@@ -236,7 +236,7 @@ impl ReadRepository for MockRepository {
Ok(true)
}
- fn blob(&self, _oid: Oid) -> Result<git2::Blob, git_ext::Error> {
+ fn blob(&self, _oid: Oid) -> Result<git2::Blob<'_>, git_ext::Error> {
todo!()
}
@@ -244,7 +244,7 @@ impl ReadRepository for MockRepository {
&self,
_oid: git_ext::Oid,
_path: P,
- ) -> Result<git2::Blob, git_ext::Error> {
+ ) -> Result<git2::Blob<'_>, git_ext::Error> {
todo!()
}
@@ -252,7 +252,7 @@ impl ReadRepository for MockRepository {
&self,
_remote: &RemoteId,
_reference: &git::Qualified,
- ) -> Result<git2::Reference, git_ext::Error> {
+ ) -> Result<git2::Reference<'_>, git_ext::Error> {
todo!()
}
@@ -284,7 +284,7 @@ impl ReadRepository for MockRepository {
fn references_glob(
&self,
_pattern: &git::PatternStr,
- ) -> Result<Vec<(fmt::Qualified, Oid)>, git::ext::Error> {
+ ) -> Result<Vec<(fmt::Qualified<'_>, Oid)>, git::ext::Error> {
todo!()
}
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 30c2e2d6-93ac-4cc6-9458-14c09dd79427 -v /opt/radcis/ci.rad.levitte.org/cci/state/30c2e2d6-93ac-4cc6-9458-14c09dd79427/s:/30c2e2d6-93ac-4cc6-9458-14c09dd79427/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/30c2e2d6-93ac-4cc6-9458-14c09dd79427/w:/30c2e2d6-93ac-4cc6-9458-14c09dd79427/w -w /30c2e2d6-93ac-4cc6-9458-14c09dd79427/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /30c2e2d6-93ac-4cc6-9458-14c09dd79427/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.88-x86_64-unknown-linux-gnu'
info: latest update on 2025-06-26, rust version 1.88.0 (6b00bc388 2025-06-23)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.88.0 (873a06493 2025-05-10)
+ rustc --version
rustc 1.88.0 (6b00bc388 2025-06-23)
+ cargo fmt --check
Diff in /30c2e2d6-93ac-4cc6-9458-14c09dd79427/w/crates/radicle/src/storage.rs:509:
fn path(&self) -> &Path;
/// Get a blob in this repository at the given commit and path.
- fn blob_at<P: AsRef<Path>>(&self, commit: Oid, path: P) -> Result<git2::Blob<'_>, git_ext::Error>;
+ fn blob_at<P: AsRef<Path>>(
+ &self,
+ commit: Oid,
+ path: P,
+ ) -> Result<git2::Blob<'_>, git_ext::Error>;
/// Get a blob in this repository, given its id.
fn blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git_ext::Error>;
Diff in /30c2e2d6-93ac-4cc6-9458-14c09dd79427/w/crates/radicle-node/src/test/node.rs:89:
if let Err(e) = unsafe { ManuallyDrop::take(&mut self.handle) }.shutdown() {
log::warn!(target: "test", "Failed to shutdown node {}: {}", self.id, e);
}
-
+
// Join the thread with error handling to prevent panic during cleanup
match unsafe { ManuallyDrop::take(&mut self.thread) }.join() {
Ok(Ok(())) => {
Exit code: 1
{
"response": "finished",
"result": "failure"
}