Commit Graph

71 Commits

Author SHA1 Message Date
Lorenz Leutgeb b5e6d82769
radicle/identity: Tidy up
Small improvements:
 - Remove confusing comments in test
 - Rename `has_active_sibling_accept`
     to   `has_accepted_active_sibling`
 - Small refactoring to avoid `… } else { None }`
2026-07-09 16:26:25 +02:00
Fintan Halpenny 2f1aaa45b2 radicle/cob/identity: Add sibling-accept invariant to property tests 2026-07-08 15:31:45 +01:00
Fintan Halpenny a63f91885f radicle/cob/identity: Guard `IdentityMut::update` against sibling proposals
Ensure that the `IdentityMut::update` API returns an error when it encounters an
active, sibling revision by the same delegate.
2026-07-08 15:31:45 +01:00
Fintan Halpenny b6f07074d9 radicle/cob/identity: Guard `IdentityMut::accept` against sibling accepts
Ensure that the `IdentityMut::accept` API returns an error when it encounters an
active, sibling revision by the same delegate.
2026-07-08 15:31:45 +01:00
Fintan Halpenny 7daeb3135b radicle/cob/identity: Add SiblingAccepted error variant
Add an `ApplyError` for variant, `SiblingAccepted`, to use to enforce the new
invariant: delegates must not accept active, sibling revisions.
2026-07-08 15:31:45 +01:00
Fintan Halpenny 654218e6a5 radicle/cob/identity: Strip implicit author accept for sibling revisions in `action`
When a delegate creates a new `Revision`, their signature is implicitly added to the revision.
Prevent the delegate from an accept verdict on two active, sibling revisions by:
1. Checking that the `state` is `Active`, and then checking `has_active_sibling_accept`
2. If there is an existing verdict, then it the verdict from the new revision is stripped.

The creation of the `Revision` must still happen, as opposed to throwing an
error, in case of existing histories.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 82dc3fe20c radicle/cob/identity: Enforce sibling-accept invariant for `RevisionAccept` in `action`
On `Action::RevisionAccept` use `has_active_sibling_accept` to check for a sibling accept.
If one is found, the `RevisionAccept` is logged and skipped.

This preserves the invariant of a single accept per delegate on active, sibling
revisions without throwing an error on existing histories.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 8e677a9d90 radicle/cob/identity: Add has_active_sibling_accept helper
Add a helper method to check if a delegate has already voted on a sibling
revision.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 62f64744ad radicle/cob/identity: Test accept-before-redact convergence
Companion to the redact-vs-accept test, with reversed timestamp ordering so
accept wins.
2026-07-03 15:22:13 +01:00
Fintan Halpenny f720526fc9 radicle/cob/identity: Test concurrent redact-vs-accept convergence
Verifies that when Alice redacts a revision and Bob concurrently
accepts it, both nodes converge after sync. The CRDT evaluation
order (timestamp-based) determines which operation takes effect.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 21700e4962 radicle/cob/identity: Fix cascading_rejections test to test actual cascading
The test previously had B2 as a sibling of B1 (both children of A1),
because update() always uses self.current as parent. This meant
the test was testing multi-sibling rejection, not cascading.

Now B2 is a child of B1 via a manual transaction, so when B1 is
rejected (sibling E1 accepted), B2 is rejected via RejectedBy::Parent
(cascade), which is what the test name promises.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb ebf0f417f7 radicle/cob/identity: Deprecate `Identity::id`
In the heartwood workspace, there is just a single use of this function.
Also, the repository ID can be computed on demand, saving memory.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 97f279fe41 radicle/cob/identity: Test redacting accepted revisions
Add a test to ensure that a previously accepted revision cannot be redacted.
2026-07-03 15:22:13 +01:00
Fintan Halpenny e0e231813f radicle/cob/identity: Test `authorization_based_on_parent_not_current`
To reflect that autorization is governed by the causal parent of a
revision, not the currently accepted revision.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 108686e299 radicle/cob/identity: Test queued children with delegate change
Ensure that adopting queued children takes into account that the delegate set
can change.
2026-07-03 15:22:13 +01:00
Adrian Duke da2c523d41 radicle/cob/identity: Add Property Tests
Introduce a property testing harness for asserting a set of properties expected
of the repository identity.

The harness uses the `Network` fixture for providing four nodes that can
interact with the repository identity. The state of the harness is advanced by
providing an actor and operation on the identity document. Invariants are then
asserted about the identity.

The current invariants are:
- The `current` revision is the one and only `Accepted` revision.
- The chain of revisions is valid. That is, `Active` revisions only have a
  parent that is `Accepted`, and not `Rejected` or `Redacted`.
- `Active` revisions do not contain a majority approval.
- `Accepted` revisions contain a majority approval.
- `Rejected` revisions do not contain a majority approval.
- For each revision, at most one child is `Accepted`.
- For each revision, if any of its children is `Accepted`, all other
  children are `Rejected`.
- A revision which is `Rejected(RejectedBy::Parent)` has a parent that
  is `Rejected`.
- A revision which is `Redacted(RedactedBy::Parent)` has a parent that
  is `Redacted`.
- For each revision that is `Rejected` or `Redacted`, none of its
  children is `Active`.
- A sibling or ancestor revision's rejected state applies to its sibling or
  descendant.
- The repository identity documents converge when all nodes have applied all
  operations.
2026-07-03 15:22:13 +01:00
Adrian Duke 94a24a7283 radicle/cob/identity: Fix comments in test `reject_concurrent`
This incorrectly listed the number of votes and delegates.
2026-07-03 15:22:13 +01:00
Adrian Duke 2768fe79d2 radicle/cob/identity: Test evaluating children of accepted revision
Add a test case to show that when a revision becomes accepted that its children,
which reach a majority vote, are also marked as accepted.
2026-07-03 15:22:13 +01:00
Adrian Duke c174efccb9 radicle/cob/identity: Test cascading of redaction
Add a test case to show that when a revision is redacted that its children also
become redacted, tracking `RedactedBy::Parent`.
2026-07-03 15:22:13 +01:00
Adrian Duke 2349eebf5d radicle/cob/identity: Test cascading rejection
When a revision reaches qourum and is adopted, the state all competing sibling
revisions as `Rejected`.

However, the test suite lacked coverage for deeper proposal branches,
specifically testing that this rejection correctly cascades to the
children of those rejected siblings.

Introduces a test to simulate the scenario where a delegate eagerly
proposes a chain of revisions (a child branching off a sibling) while
others concurrently accept a competing branch.
2026-07-03 15:22:13 +01:00
Adrian Duke 39972b0c46 radicle/cob/identity: Test terminal states
The identity evaluation rewrite changed how revision redactions are
handled. Instead of removing the revision entirely, it explicitly
transitions the revision to `State::Redacted`.

Add a test to cover two redaction scenarios:
 - Attempting to redact a revision that has already been `Accepted`.
 - Attempting to redact a revision that has already been `Rejected`.
2026-07-03 15:22:13 +01:00
Adrian Duke 489360d0e5 radicle/cob/identity: Add concurrent terminal state test
The new identity evaluation logic was updated to ignore `Accept` or `Reject`
actions if the revision has already reached a terminal state (`Accepted` or
`Rejected`), however this handling was not explicitly covered by the test suite.

This test ensures that the state machine correctly short-circuits the
late-arriving vote, proving that the new logic succesfully ensures
terminal states remain immutable under concurrent network conditions.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb ba4a79af90 radicle/cob/identity: Test rejected sibling
Add a test case to show that when Bob creates two sibling changes `b1` and `b2`,
that the acceptance of one means the other gets rejected.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb d8c14222a2 radicle/cob/identity: Rewrite Evaluation
The evaluation of the reposioty identity is rewritten to better handle cases
where there are child and sibling revisions that are active at the same time.

In particular, `fn Identity::action` is now free of any references to
`self.current`.

This is achieved through two main improvements. The first is that the `State`
of revisions changes to improve clarity:
 1. `Active` and `Accepted` remain, and their meanings also remain the same.
 2. `Stale` is removed entirely.
 3. `Rejected` is improved to also contain `RejectedBy`, to keep track of the
    reason for rejection.
 4. `Redacted` is promoted to a state. Similarly, the reason for
    redaction is tracked by `RedactedBy`.

The transition of a revision from `Active` to one of the other states now
influences siblings and children.

If a revision transitions to `Accepted`, then the sibling revisions can no
longer transition to `Accepted`. They are considered `Rejected` where the reason
is `Sibling`. This cascades: Children of siblings are `Rejected`
recursively, tracking `RejectedBy::Parent`.
Any children of the `Accepted` revision are also evaluated to see if they can be
similarly transitioned to the `Accepted` state; since they may have already been
voted on.

If a revision was rejected by a majority, then the it transitions to `Rejected`
with `RejectedBy::Vote`. Dually to acceptance, the children of this revision
are also rejected with the reason of `Ancestor`.

Finally, when the author of a revision redacts a revision, it transitions
to `Redacted`, tracking `RedactedBy::Author`, and its children are redacted
tracking `RedactedBy::Parent`.

The test is adjusted to `remove_delegate_concurrent` reflect that concurrently
proposed revisions are retained in the timeline and explicitly marked as rejected,
rather than being dropped entirely (as before).
2026-07-02 09:56:22 +01:00
Lorenz Leutgeb b145b95ee4 radicle/cob/identity: Use verdicts to count votes
Evaluation of this COB is implemented under the assumption that
delegates accept at most one revision.

This can lead to issues, if some delegates create new revisions eagerly,
without waiting for the others to vote on earlier revisions. As the
eager delegates start accepting newer and newer revisions, this shadows
their acceptance of earlier revisions, which leads to failure to
recognize that these earlier revisions were actually accepted by a
majority.

To avoid such situations, remove `heads`, and always count the number of
"accept" verdicts explicitly.
2026-07-02 09:56:22 +01:00
Lorenz Leutgeb 442df34be7 radicle/cob/identity: Strengthen test assumptions
Not only assert that the revision is active, but
also its parent.
2026-07-02 09:56:22 +01:00
Daniel Silverstone 07f748475b radicle/cob: Include command name on spawn fail
To improve the ability to work out what went wrong, should an external
cob helper not be found, report the command name if the spawn fails.

Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-06-17 23:22:10 +02:00
Adrian Duke f0c6abc6ea radicle/cob/patch: Extend MergeTarget
Extend `enum MergeTarget` to include a new variant, `Branch`.
The intended use of this new variant is to allow a `Patch` to have a
target branch other than the default branch.

The `Branch` variant holds a `TargetBranch` which, in turn, is ensured
to be a `Qualified` reference that begins with `refs/heads`, i.e. a
Git branch.
2026-06-09 11:21:33 +02:00
Adrian Duke c7339615a7 cob: Replace 'git2::Oid' -> 'git::raw::Oid' 2026-06-09 11:21:33 +02:00
Lorenz Leutgeb 420af3b710 workspace/rust: 1.90 → 1.95
The update to `flake.lock` is a simple

    nix flake update rust-overlay

to be able to reach 1.95.
2026-05-11 12:09:04 +01:00
Lorenz Leutgeb 9ea040ccd0 rust/msrv: 1.85.0 → 1.88.0
`cargo check` fails because `human-panic` and `sysinfo` require at least
1.88.0.

    +++ command cargo check --release --locked --all-targets
    error: rustc 1.85.0 is not supported by the following packages:
      human-panic@2.0.6 requires rustc 1.88
      sysinfo@0.37.2 requires rustc 1.88

Bump MSRV to fix this.

1.88.0 introduced [let chains], which in turn has `clippy` warn about
nested if statements. All of these sites are fixed in this change.

1.87.0 introduced [`is_multiple_of`], which is a more readable version
of `x % y == 0`.

[let chains]: https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/#let-chains
[`is_multiple_of`]: https://doc.rust-lang.org/std/primitive.usize.html#method.is_multiple_of
2026-05-11 11:23:18 +01:00
Josh Soref 5dae2a8b58 treewide: Spelling
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2026-04-30 15:50:29 -04:00
Lorenz Leutgeb cc70d7dad4 radicle/cob: Do not return `BrokenPipe` from external COB invocation
A handle for standard input can always be expected here.
2026-04-29 10:57:24 +02:00
Lorenz Leutgeb c23a257f3c
clippy: Configure lint `unwrap_used`
By setting `allow-unwrap-in-tests`, we can avoid repeating
`allow(clippy::unwrap_used)` many times over.
2026-04-15 10:06:05 +02:00
Lorenz Leutgeb dbec748c47
clippy: Disallow unused lints
The following clippy lints were allowed in the past but are not
required anymore:
 - box_default
 - large_enum_variant
 - let_unit_value
 - or_fun_call
2026-04-15 10:06:05 +02:00
Lorenz Leutgeb 63f9607423
clippy: Disallow lint `iter_nth_zero` 2026-04-15 10:06:05 +02:00
Lorenz Leutgeb 44244dc00f
clippy: Disallow lint `type_complexity`
We just need two exceptions.
2026-04-15 10:06:05 +02:00
Lorenz Leutgeb f223afd9d7 radicle: Refactor COB Storage Access
Instead of passing the signer as an argument to many methods on `Store`,
scope the `Store` itself to a signer.

This further allows to differentiate two different access modes on the
store in `radicle::cob::store::access`: `WriteAs` (which requires signer)
and `ReadOnly` (which does not require a signer).

The caches for issues and patches in `radicle::cob::{issue,patch}::Cache`
are concretised by removing the first type parameter, since it was
specific to issues and patches anyway. This was done in this commit as
it touches very similar usage sites.

Make `Device` less prominent, and instead lean more heavily towards
traits from the `signature` crate, such as `Keypair` and `Verifier`
in addition to `Signer`. Trait bounds regarding `Signer` could be
simplified, but this is left for the future.

In `radicle-cli`, the function `term::cob::patches_mut`, which generates
errors with a hint is used instead of the lower-level `Profile::patches_mut`.

Commands `rad issue cache` and `rad patch cache` now construct a
writeable cache on top of a read-only store.

Many knock-on changes are handled as well, to arrive at a clean state.
2026-03-31 17:57:14 +01:00
Lorenz Leutgeb 8bac24d6ab
rust/edition/fmt: 2021 → 2024
Reformat code with `rustfmt`, now that the edition changed from
Rust 2021 to Rust 2024.
2026-03-30 16:57:51 +02:00
Lorenz Leutgeb 0494227e75
rust/edition: 2021 → 2024
Switch from Rust 2021 to Rust 2024.

See <https://doc.rust-lang.org/edition-guide/rust-2024/>.

Regarding the added bound `use<>`, see
<https://blog.rust-lang.org/2024/09/05/impl-trait-capture-rules/>.
2026-03-30 16:56:58 +02:00
Fintan Halpenny 1566646762 radcile/cob/identity: mark ApplyError as non_exhaustive 2026-02-19 21:06:43 +01:00
Adrian Duke 67a4a712e4 radicle: add new error type for unauthorized non-delegates
The previous error `UnexpectedState` was opaque, and ended up in a
confusing message when a user would try to edit the identity document
while not being a deleagte themselves.

Instead, provide a more specific error that mentions the `Did`
performing the action, and the action itself.

Note that the `UnexpectedState` variant was matched on during
evaluation, meaning that operations would still appear in the timeline
of the COB. With the change to the other variant, the timeline does
not record the operation anymore.
2026-01-22 10:24:26 +00:00
Fintan Halpenny d98033a1ff localtime: localise the localtime dependency
The `localtime` crate was defined by cloudhead, and is a minimal
repository with a single `lib.rs`.

Instead of using it as an external dependency, copy the code directly
into a new workspace crate `radicle-localtime`.

The default `serde` implementation goes through the `LocalTime`'s
seconds values rather than milliseconds, since this is the more common
format. This allows the removal of the `serde_ext` functions.
The one place milliseconds was used was for the
`radicle::cob::common::Timestamp` type, where the `Serialize` and
`Deserialize` implementations are manually written.

It also adds a `schemars` feature to remove `schemars_ext` functions
in `radicle` as well.
2026-01-08 14:58:41 +01:00
Fintan Halpenny b8a6e1a52f radicle/cob/stream: skip commits that do not have a manifest
For some COBs, parent commits can be specified for actions, for
example, patches will bundle their base and head commits as parents.

When the COB stream performs the revwalk, these commits will be
included, and will not have the manifest file – resulting in an error.

Instead, this error can be matched against, and the commit is skipped
instead. The other errors should still be resurfaced, since a commit
with a manifest should be expected to load the operation correctly,
and the commit should exist in the repository when attempting to load
it.

One might argue that a valid operation with a missing manifest could
occur, but that would mean that `radicle-cob` has performed an invalid
write. This is undetectable by the stream API, and is ambiguous with
the case of non-Op commits.
2025-12-28 14:01:01 +00:00
Yaroslav Halchenko 32d77641ab chore: Fix spelling errors with codespell 2025-11-01 12:11:02 +01:00
Johannes Kühlewindt 5741bafa3b radicle/cob/db: Add index for issues and patches
To improve query performance for issues and patches, add a composite
index on "repo" and "id" to the SQLite database schema via a
migration.
2025-10-25 10:22:50 +02:00
Lorenz Leutgeb b0beef4391
workspace/rust/clippy: Fix all warnings
Fix clippy warnings following the upgrade of the Rust toolchain to
version 1.90. Almost all are "mismatched_lifetime_syntaxes".
2025-10-17 12:56:43 +02:00
Lorenz Leutgeb b7cfcfff75
workspace: A Little Less `git2`
Make `git2` an *optional* dependency of `radicle-cob`, and refactor
`radicle` to depend on crates that in turn do not depend on `git2`
*non-optionally*

The main offending dependency of `radicle-cob` is `radicle-git-ext`
from the `radicle-git` workspace in repository
(rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt) which *non-optionally* depends on
`git2`.

So, to achieve removal of this dependency:

 1. The crate is refactored to depend on the new crates
    `radicle-git-ref-format` `radicle-git-metadata`, and
    `radicle-oid` introduced in the previous commits, instead of
    `radicle-git-ext`.
 2. Some code from the `radicle-git-ext` crate in the `radicle-git`
    workspace in repository (rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt) is
    copied. See `crates/radicle-cob/src/backend/git/commit.rs`.

This cascades to `radicle` and its dependents.

  Firstly, the there is an
`impl Deref<Target=git2::Oid> for radicle_git_ext::Oid`. This made
it very convenient to just deref to obtain the wrapped `git2::Oid`,
so there are many expressions of the shape `*oid` in `radicle` and
its dependents. However `radicle-oid` does not provide
`impl Deref<Target=git2::Oid> for radicle_oid::Oid`, as notably,
`Target` is an associated type, and not a type parameter, so an
implementation of `Deref` would tie the new `Oid` too tightly to a
particular implementation.
Instead, work with `impl From<radicle_oid::Oid> for git2::Oid`
(which can be enabled using the feature flag `radicle-oid/git2`).
This explains the changes from `*oid` to `oid.into()` at every
transition to "`git2` land".

  Secondly, `radicle` and its dependents are refactored to also depend
on `radicle-git-ref-format` and (much less prominently)
`radicle-git-metadata` instead of `radicle-git-ext`
This is to avoid pulling in `git2` via these dependencies.

  Thirdly, as the re-exports in `crates/radicle/src/git.rs` change,
they are at the same time also cleaned up. Notably, the types from
`radicle-git-ref-format` are re-exported under `fmt` only, not "twice".

While overall this obviously is very much a breaking change, these
changes should mostly amount to changing from `Deref` to `Into`, i.e.,
`*oid` → `oid.into()` and rewriting imports for `radicle::git::fmt`.
This is indicated by the mostly mechanical nature of the changes to
`crates/radicle-{cli,node,remote-helper}`.
2025-10-13 16:14:23 +02:00
Lorenz Leutgeb 3780f908a2 radicle/identity: Stabilize tests
The following tests generated an unstable object ID, thus linearizing
history in a different order, causing failures. Fix this by using the
`stable-commits` feature of `radicle-cob`.

    thread 'cob::identity::test::test_identity_redact_revision'
    panicked at crates\radicle\src\cob\identity.rs:1338:9:
    assertion `left == right` failed
      left: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(a04165e3d3717c5a1413ec78e852bd8e1bb049d4),
             Oid(820d3faf5b507888173b26ccd1a2a81666bd2573),
             Oid(30ca6f078a401bf542049594fbb1c8d2371c9819),
             Oid(9e9c46971014b123a31cd1195078f95c2e319419)]
     right: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(a04165e3d3717c5a1413ec78e852bd8e1bb049d4),
             Oid(820d3faf5b507888173b26ccd1a2a81666bd2573),
             Oid(9e9c46971014b123a31cd1195078f95c2e319419),
             Oid(30ca6f078a401bf542049594fbb1c8d2371c9819)]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

    thread 'cob::identity::test::test_identity_reject_concurrent'
    panicked at crates\radicle\src\cob\identity.rs:1494:9:
    assertion `left == right` failed
      left: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(bce4233f6a09d6e9c021ba436e3c6923a1a78f31),
             Oid(7cacb23be7079c16c8ad1b2458ae5bac95a1295b),
             Oid(12b00e2e871b699c84c20427a64e029934c15ec4),
             Oid(07431c8d80c11492ef248e07126370e874de3435),
             Oid(98b42450159a359c2f482652c66d79b05d0af624)]
     right: [Oid(b4307ded046befba374bf8cd9fd787592ceb615c),
             Oid(bce4233f6a09d6e9c021ba436e3c6923a1a78f31),
             Oid(7cacb23be7079c16c8ad1b2458ae5bac95a1295b),
             Oid(98b42450159a359c2f482652c66d79b05d0af624),
             Oid(12b00e2e871b699c84c20427a64e029934c15ec4),
             Oid(07431c8d80c11492ef248e07126370e874de3435)]
2025-10-13 08:49:24 +01:00
Lorenz Leutgeb 880634acd4
radicle/git/raw: Capture all `git2` re-exports
A refactoring internal to the `radicle` crate, with the goal of
making dependency on `git2` clearer and more controlled.
`radicle::git::raw` is changed from a complete re-export of `git2`
to a module that selectively re-exports (many) members of `git2`.
required to build the workspace (potentially breaking dependents
outside the workspace, but given just how many types are
re-exported, this seems unlikely).

In the future, no more `use git2::…` statements should be added
outside of `crates/radicle/src/git/raw.rs`. This is in an effort to
decrease dependence on `git2` in the future.
2025-10-10 11:29:24 +02:00