Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: use a pager on list commands
Draft did:key:z6Mku263...gR76 opened 8 months ago
49 files changed +550 -112 ed8b0860 ee4f8a61
added 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.
deleted crates/radicle-cli/build.rs
@@ -1 +0,0 @@
-
../../build.rs

\ No newline at end of file
added 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(())
+
}
modified 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
modified 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(())
}
modified 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(())
modified 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 } => {
modified 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() {
modified 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 {
modified 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)?;
modified 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())?;
            }
        },
    };
modified 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(())
}
modified 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(())
}
modified 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;
modified 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!(
modified 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();

modified 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(())
+
}
modified 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(())
}

modified 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 {
modified 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]
modified 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")
modified 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)
    }

modified 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]
deleted crates/radicle-node/build.rs
@@ -1 +0,0 @@
-
../../build.rs

\ No newline at end of file
added 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(())
+
}
modified 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);
+
            }
+
        }
    }
}

modified 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);
+
            }
+
        }
    }
}

modified 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)
    }

modified 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)
    }
}
modified 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);
deleted crates/radicle-remote-helper/build.rs
@@ -1 +0,0 @@
-
../../build.rs

\ No newline at end of file
added 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(())
+
}
modified 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,
modified 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::*;
modified 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;
added 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(())
+
}
modified 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)
    }
}
modified 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);

modified 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,
modified 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>,
    {
modified 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)
modified 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
    }

modified 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)
    }
modified 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> {
modified 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()? {
modified 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)
    }

modified 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
    }
}
modified 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"
modified 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!()
    }