When executing programs on Windows, we suppress creating a new window,
and, if the process should continue to run in the background, we
explicitly create a new process group and detach.
Refactor the `radicle-surf` crate to use OIDs from `radicle-oid` instead
of `radicle-git-ext`, and `radicle-git-ref-format` instead of re-exports
from `radicle-git-ext`. Also remove the dependency `radicle-std-ext`.
This requires changing `use`s, and an ad-hoc reimplementation of
`ResultExt::or_matches`.
The remaining changes (excl. tests) are automatic via `rustfmt` and
`cargo clippy`.
For testing, implement reconstruction of
https://github.com/radicle-dev/git-platinum
in particular of the commits
- a0dd9122d33dff2a35f564d564db127152c88e02
- 27acd68c7504755aa11023300890bb85bbd69d45
- e972683fe8136bf8a5cb2378cf50303554008049
and some tags.
`…/src/test/roundtrip.rs` was copied from the `test-helpers` crate in
the radicle-git workspace.
`…/src/test/{repository.rs,gen*}` was copied from the `radicle-git-ext`
crate in the radicle-git workspace.
In `radicle-cli`, which is a binary crate (!) a few `pub use`s can be
removed. This forces `radicle-remote-helper` to depend on the library
crate `radicle-term` to access some of the types that were re-exported
from `radicle-cli`, making the dependency more obvious, and minimizing
the public symbols on `radicle-cli`.
The dependency on `radicle-cli` cannot be removed from
`radicle-remote-helper` easily, so is kept.
Reasons for the rewrite:
- Migrate away from `ec25519` as our implementation of Ed25519,
because that crate is unmaintained.
Instead of `ec25519` we use `ed25519-dalek` which is the most popular
implementation of Ed25519 in Rust.
- Migrate away from `amplify`, because we make very little use of its
features. Here, we only use `amplify::Bytes32`, which is just a thin
wrapper of `[u8; 32]`.
Instead of `amplify::Bytes32` we use `type PublicKeyBytes = [u8; 32]`.
The realization that `ExtendedSignature` is not really tied to SSH, but
is merely a representation of a key together with a signature, allows to
relax the bounds on traits in `radicle-cob`.
Separating out uses of "public key" (a rather simple model of a key,
just 32 bytes) and "verifying key" (an actual point on the Edwards
curve, ready to verify signatures, using more memory than just 32 bytes)
allows a more sensible definition of `crypto::Signer`, which propagates
to `radicle-cob`, `radicle`, `radicle-cli`, `radicle-node` and
simplifies trait bounds.
In `radicle-node`, make the fact clearer that actually the secret key
itself is required (for our implementation of Diffie-Hellman) not
"just" signing. This results in removal of the generic argument
(usually called `G`) of multiple types.
Concretizing the types in `radicle-node` this way, i.e. by (morally)
setting `G = radicle_crypto::SigningKey`, leads to removal of
`MemorySigner` and `MockSigner`, as they really just wrap a signing
key.
Re-exporting `ed25519::Signature` (via re-export in `ed25519-dalek`)
is hopefully more future-proof than using our own type for signatures.
However, the fact that `ed25519::Signature` is not serializable and
does not implement `std::hash::Hash` means that we have to resort to
ad-hoc solutions in `radicle`:
1. The Identity COB requires serialization of signatures. These are
generally questionable, since the security of the Identity COB
should rely on the security of Signed References. In the medium
term we might even want to consider ignoring signatures in actions.
2. When reading Signed References, detection of Replay Attacks uses
a `HashMap` keyed by signatures. This detection only exists for
backwards compatibility.
These two use cases are exotic enough to warrant ad-hoc implementations.
By not wrapping `ed25519::Signature`, we encourage writing more widely
compatible code in the future.
Co-authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
In preparation of a rewrite of `radicle-crypto`, move the
responsibility for generating a cryptographically secure
seed from `radicle` (transitively via dependency on
`radicle-crypto` and `ec25519`) to the dependent of `radicle`.
Applications should decide how to obtain randomness, not `radicle` via
some far-removed dependency.
Within the workspace, the only dependent using `Profile::seed` is
`radicle-cli`, in particular for the `rad auth` subcommand. To obtain
random bytes, `radicle-cli` depends on `getrandom` and uses
`getrandom::fill`.
For most use-cases, i.e., except some in `radicle-node`, the channels
in `std::sync::mpsc` suffice, allowing us to get rid of the dependency
`crossbeam-channel`.
Crossbeam is sometimes mentioned to offer higher performance compared
to `std::sync::mpsc`.
For usage in `radicle-signals` (and the counterparts in `radicle-node`
and `radicle-term`) performance is irrelevant, as these channels are
used to send a singular message (to the effect of shutting down the
node or stopping the spinner). These could potentially use
`std::sync::oneshot` in the future, which is nightly-only as of
2026-06-17, see <https://github.com/rust-lang/rust/issues/143674>.
Usage in `radicle-protocol` and `radicle`, is a heavier duty.
The channels pass commands to the service (arriving e.g. via the
control socket in `radicle-node`), events from the service to other
listeners (e.g. the control socket), and fetch results from workers to
the service. For user interaction, performance is irrelevant, since
the messages are in the tens or hundreds per second. Even for delivering
events and programmatic usage of commands, thousands to hundreds of
thousands of messages per second are plenty.
Note that these channel implementations are benchmarked in the millions
of operations per second, see e.g.:
- <https://github.com/crossbeam-rs/crossbeam/tree/b23b7e8eca2efdad9bdc1ceb1aee1207a852c03b/crossbeam-channel/benchmarks#results>
- <https://github.com/fereidani/rust-channel-benchmarks/blob/b4c979a94482e5012734657c2ee153b528bc56d6/README.md>
Also, `crossbeam-channel` was merged into `std` in 2022-11, and we do
not believe that performance of the implementations has siginifcantly
drifted since then, see <https://github.com/rust-lang/rust/pull/93563>.
One minor downside is that the channels in the standard library do not
expose the number of pending messages.
The `amplify` crate is only used in a handful places to derive `impl`s
that are not very complex.
Provide the required `impl`s directly, and remove the dependency.
Fetching a large repository looped with
Failed to consume the pack sent by the remote
even though the pack appears valid (Git indexes it in full).
The cause appears to be a bug in zlib-rs 0.6.3 according to
<https://github.com/trifectatechfoundation/zlib-rs/pull/504>.
Move logging-related module `radicle::logging` into its own crate.
While at it, remove the "logger" feature flag from `radicle`.
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Prevent `rad` from panicking when its stdout is a closed pipe, e.g.
when running `rad config | head`.
The broken pipes are handled at the output boundaries using three
layers of defence:
1. Inner: Route all stdout output through `term::print()` and
`term::print_inline()`, which use explicit `writeln!`/`write!` to
a locked stdout and silently ignore write errors. All `println!`
calls in `radicle-cli` and `radicle-term` are converted.
2. Middle: Catch `BrokenPipe` errors that propagate up via `anyhow`
from subcommands in `main::run()`, and exit cleanly with code 0.
3. Outer: Install a panic hook that intercepts `println!` panics
containing "Broken pipe" (from dependencies like clap) and exits
cleanly, chained in front of human_panic's hook.
To prevent regressions, `#![deny(clippy::print_stdout)]` is added to
`radicle-cli`, requiring all future stdout output to go through the
safe `term::` functions.
Note: `eprintln!` calls (stderr) are not handled, as broken stderr
pipes are extremely rare in practice.
With `connect: std::collections::HashSet`, we suffer unpredictable reordering
of values in our test cases.
Use `connect: indexmap::IndexSet` instead, which preserves insertion
order for iteration.
The main change that needs adjustment on our side is that
`impl EcPk for ec25519::PublicKey` changed from `Compressed = [u8; 32]`
to `Compressed = amplify::Bytes32`.
Also remove `impl Deref for PublicKey` to avoid dependents of
`radicle-crypto` depend on a particular implementation, and make the
anonymous member private.
See
<https://github.com/cyphernet-labs/cyphernet.rs/commit/f42396139a6321615dd651bc09073052add14a05>.
The `fxhash` crate has become unmaintained and was a transitive
dependency of `inquire`.
The `inquire` crate moved to use `stdlib` instead of `fxhash`[^0].
Update `inquire` to 0.9.4 to get the latest version of the crate.
`cargo deny check` output:
```
error[unmaintained]: fxhash - no longer maintained
┌─ /home/fintohaps/Developer/heartwood/Cargo.lock:111:1
│
111 │ fxhash 0.2.1 registry+https://github.com/rust-lang/crates.io-index
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unmaintained advisory detected
│
├ ID: RUSTSEC-2025-0057
├ Advisory: https://rustsec.org/advisories/RUSTSEC-2025-0057
├ The fxhash crate is no longer maintained.
The repository is stale and owner is no longer active on GitHub.
Please take a look at [rustc-hash](https://github.com/rust-lang/rustc-hash) instead.
```
├ Announcement: https://github.com/cbreeden/fxhash/issues/20
├ Solution: No safe upgrade is available!
├ fxhash v0.2.1
└── inquire v0.7.5
└── radicle-term v0.17.0
└── radicle-cli v0.19.0
└── radicle-remote-helper v0.15.0
```
[^0]: https://github.com/mikaelmello/inquire/blob/3d5b65422a247acc773d767558e34fcb69ab04bf/CHANGELOG.md?plain=1#L72
This version resolves the vulnerability from the `tar-rs` transitive
dependency.
`cargo deny check` output:
```
error[vulnerability]: tar-rs incorrectly ignores PAX size headers if header size is nonzero
┌─ /home/fintohaps/Developer/heartwood/Cargo.lock:362:1
│
362 │ tar 0.4.44 registry+https://github.com/rust-lang/crates.io-index
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
│
├ ID: RUSTSEC-2026-0068
├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0068
├ Versions 0.4.44 and below of tar-rs have conditional logic that skips the PAX
size header in cases where the base header size is nonzero.
As part of [CVE-2025-62518][astral-cve], the [astral-tokio-tar]
project was changed to correctly honor PAX size headers in the case where it
was different from the base header. This is almost the inverse of the
astral-tokio-tar issue.
Any discrepancy in how tar parsers honor file size can be used to create
archives that appear differently when unpacked by different archivers. In this
case, the tar-rs (Rust tar) crate is an outlier in checking for the header size
— other tar parsers (including e.g. Go [`archive/tar`][go-tar]) unconditionally
use the PAX size override. This can affect anything that uses the tar crate to
parse archives and expects to have a consistent view with other parsers.
This issue has been fixed in version 0.4.45.
[astral-cve]: https://www.cve.org/CVERecord?id=CVE-2025-62518
[astral-tokio-tar]: https://github.com/astral-sh/tokio-tar
[go-tar]: https://pkg.go.dev/archive/tar
├ Solution: Upgrade to >=0.4.45 (try `cargo update -p tar`)
├ tar v0.4.44
└── (build) radicle-surf v0.27.0
└── radicle-cli v0.19.0
└── radicle-remote-helper v0.15.0
```
The `paste` crate is no longer maintained, and the `pastey` fork is a
drop-in replacement.
`cargo deny check` output:
```
error[unmaintained]: paste - no longer maintained
┌─ /home/fintohaps/Developer/heartwood/Cargo.lock:239:1
│
239 │ paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unmaintained advisory detected
│
├ ID: RUSTSEC-2024-0436
├ Advisory: https://rustsec.org/advisories/RUSTSEC-2024-0436
├ The creator of the crate `paste` has stated in the [`README.md`](https://github.com/dtolnay/paste/blob/master/README.md)
that this project is not longer maintained as well as archived the repository
## Possible Alternative(s)
- [`pastey`]: a fork of paste and is aimed to be a drop-in replacement with additional features for paste crate
- [`with_builtin_macros`]: crate providing a [superset of `paste`'s functionality including general `macro_rules!` eager expansions](https://docs.rs/with_builtin_macros/0.1.0/with_builtin_macros/macro.with_eager_expansions.html) and `concat!`/`concat_idents!` macros
[`pastey`]: https://crates.io/crates/pastey
[`with_builtin_macros`]: https://crates.io/crates/with_builtin_macros
├ Announcement: https://github.com/dtolnay/paste
├ Solution: No safe upgrade is available!
├ paste v1.0.15
└── (dev) radicle-protocol v0.6.0
└── radicle-node v0.18.0
└── (dev) radicle-cli v0.19.0
└── radicle-remote-helper v0.15.0
```