Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: update radicle-git-ext
Fintan Halpenny committed 3 years ago
commit 8508eab8faddb8d02148db6255b1d52479c78b4a
parent 293a76948fb2d72a85c5902e8ecca681fd5db9ff
37 files changed +400 -394
modified Cargo.lock
@@ -913,21 +913,10 @@ dependencies = [
]

[[package]]
-
name = "git-commit"
-
version = "0.3.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "42480682cb6d05a34304eaea858ff0008fe3ac8167e31eda360931c0b51a35c1"
-
dependencies = [
-
 "git-trailers",
-
 "git2",
-
 "thiserror",
-
]
-

-
[[package]]
name = "git-ref-format"
-
version = "0.2.0"
+
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "9c0eb2e3e4837359b843ab8cc5ad482bb84d4aa528153dc882dbbba44a2f4f47"
+
checksum = "dee7296543085890905b49ee720902b25133a8d878b7221488d0450bbd965741"
dependencies = [
 "git-ref-format-core",
 "git-ref-format-macro",
@@ -935,19 +924,20 @@ dependencies = [

[[package]]
name = "git-ref-format-core"
-
version = "0.2.0"
+
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "1cbf4ccf1d4cd0ac87a1461d6e59be34aec975bc2097850d7c3e164e3ff66436"
+
checksum = "54d2d2c6b5f0b38d20c51883abe9ee5879fb50e56a8255bb35ac0b0c679504c5"
dependencies = [
+
 "minicbor",
 "serde",
 "thiserror",
]

[[package]]
name = "git-ref-format-macro"
-
version = "0.2.0"
+
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "3058716dac23a95ef68bd44c855c8e8368ee36feefc875bd8bdcb42ae582d98e"
+
checksum = "f6b7367435d968adc55ba8cd0bb3c12a3cb517270f110b5300df9c6f0726c762"
dependencies = [
 "git-ref-format-core",
 "proc-macro-error",
@@ -956,20 +946,10 @@ dependencies = [
]

[[package]]
-
name = "git-trailers"
-
version = "0.1.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e0934f57135449b88bea0e28efd80aab0c1b53692f8207c7e232086db824c7a8"
-
dependencies = [
-
 "nom",
-
 "thiserror",
-
]
-

-
[[package]]
name = "git2"
-
version = "0.16.1"
+
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
+
checksum = "8b7905cdfe33d31a88bb2e8419ddd054451f5432d1da9eaf2ac7804ee1ea12d5"
dependencies = [
 "bitflags",
 "libc",
@@ -1345,9 +1325,9 @@ checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"

[[package]]
name = "libgit2-sys"
-
version = "0.14.2+1.5.1"
+
version = "0.15.1+1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
+
checksum = "fb4577bde8cdfc7d6a2a4bcb7b049598597de33ffd337276e9c7db6cd4a2cee7"
dependencies = [
 "cc",
 "libc",
@@ -1437,10 +1417,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"

[[package]]
-
name = "minimal-lexical"
-
version = "0.2.1"
+
name = "minicbor"
+
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
checksum = "124d887cb82f0b1469bdac3d1b65764a381eed1a54fdab0070e5772b13114521"

[[package]]
name = "miniz_oxide"
@@ -1509,16 +1489,6 @@ dependencies = [
]

[[package]]
-
name = "nom"
-
version = "7.1.3"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-
dependencies = [
-
 "memchr",
-
 "minimal-lexical",
-
]
-

-
[[package]]
name = "nonempty"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1872,7 +1842,6 @@ dependencies = [
 "crossbeam-channel",
 "cyphernet",
 "fastrand",
-
 "git-ref-format",
 "git2",
 "localtime",
 "log",
@@ -1912,6 +1881,7 @@ dependencies = [
 "radicle-cli-test",
 "radicle-cob",
 "radicle-crypto",
+
 "radicle-git-ext",
 "radicle-node",
 "radicle-term",
 "serde",
@@ -1941,9 +1911,6 @@ name = "radicle-cob"
version = "0.1.0"
dependencies = [
 "fastrand",
-
 "git-commit",
-
 "git-ref-format",
-
 "git-trailers",
 "git2",
 "log",
 "nonempty 0.8.1",
@@ -1980,10 +1947,10 @@ dependencies = [
 "cyphernet",
 "ec25519",
 "fastrand",
-
 "git-ref-format",
 "multibase",
 "qcheck",
 "qcheck-macros",
+
 "radicle-git-ext",
 "radicle-ssh",
 "serde",
 "sqlite",
@@ -2002,9 +1969,9 @@ dependencies = [

[[package]]
name = "radicle-git-ext"
-
version = "0.2.1"
+
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c68edd5604b96b0ede289513ce523b9cef82a9c4ab07f9e059a24036fc77a2da"
+
checksum = "69d38277eb376789c3ed50a673c99b7fead98c172dd976060b029b527e605148"
dependencies = [
 "git-ref-format",
 "git2",
@@ -2060,7 +2027,6 @@ dependencies = [
 "crossbeam-channel",
 "cyphernet",
 "fastrand",
-
 "git-ref-format",
 "io-reactor",
 "lexopt",
 "libc",
@@ -2072,6 +2038,7 @@ dependencies = [
 "qcheck-macros",
 "radicle",
 "radicle-crypto",
+
 "radicle-git-ext",
 "radicle-term",
 "scrypt",
 "serde",
@@ -2110,14 +2077,13 @@ checksum = "db20136bbc9ae63f3fec8e5a6c369f4902fac2244501b5dfc6d668e43475aaa4"

[[package]]
name = "radicle-surf"
-
version = "0.9.0"
+
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "10168a9ee60ee17511fe0062cc2fa3115654f0c1f2d1692d8109a3f254c855de"
+
checksum = "7607a6c93d4cc399479cc0da0d259a42a0ed0b7ae2b051ed80ea8ed35f1246b1"
dependencies = [
 "anyhow",
 "base64",
 "flate2",
-
 "git-ref-format",
 "git2",
 "log",
 "nonempty 0.5.0",
@@ -2149,9 +2115,9 @@ name = "radicle-tools"
version = "0.2.0"
dependencies = [
 "anyhow",
-
 "git-ref-format",
 "radicle",
 "radicle-cli",
+
 "radicle-git-ext",
]

[[package]]
modified radicle-cli/Cargo.toml
@@ -18,6 +18,9 @@ json-color = { version = "0.7" }
lexopt = { version = "0.2" }
log = { version = "0.4", features = ["std"] }
nonempty = { version = "0.8" }
+
# N.b. this is required to use macros, even though it's re-exported
+
# through radicle
+
radicle-git-ext = { version = "0", features = ["serde"] }
serde = { version = "1.0" }
serde_json = { version = "1" }
serde_yaml = { version = "0.8" }
modified radicle-cli/examples/rad-id-rebase.md
@@ -6,7 +6,7 @@ delegates creating proposals concurrently.

```
$ rad id edit --title "Add Alice" --description "Add Alice as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
-
✓ Identity proposal 'f4579d0a9a068453cb24da1ad048ca1bfdcb6d98' created
+
✓ Identity proposal '04603c0d3ea4d137487024a51c9360adfc511114' created
title: Add Alice
description: Add Alice as a delegate
status: ❲open❳
@@ -48,7 +48,7 @@ Quorum Reached

```
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG --no-confirm
-
✓ Identity proposal '5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f' created
+
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' created
title: Add Bob
description: Add Bob as a delegate
status: ❲open❳
@@ -93,7 +93,7 @@ second proposal, then the identity would be out of date. So let's run
through that and see what happens.

```
-
$ rad id accept f4579d0a9a068453cb24da1ad048ca1bfdcb6d98 --no-confirm
+
$ rad id accept 04603c0d3ea4d137487024a51c9360adfc511114 --no-confirm
✓ Accepted proposal ✓
title: Add Alice
description: Add Alice as a delegate
@@ -137,7 +137,7 @@ Quorum Reached
```

```
-
$ rad id commit f4579d0a9a068453cb24da1ad048ca1bfdcb6d98 --no-confirm
+
$ rad id commit 04603c0d3ea4d137487024a51c9360adfc511114 --no-confirm
✓ Committed new identity '29ae4b72f5a315328f06fbd68dc1c396a2d5c45e'
title: Add Alice
description: Add Alice as a delegate
@@ -183,7 +183,7 @@ Quorum Reached
Now, when we go to accept the second proposal:

```
-
$ rad id accept 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --no-confirm
+
$ rad id accept 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm
! Warning: Revision is out of date
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
👉 Consider using 'rad id rebase' to update the proposal to the latest identity
@@ -238,19 +238,19 @@ Note that a warning was emitted:
If we attempt to commit this revision, the command will fail:

```
-
$ rad id commit 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --no-confirm
+
$ rad id commit 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm
! Warning: Revision is out of date
! Warning: d96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f
👉 Consider using 'rad id rebase' to update the proposal to the latest identity
-
✗ Id failed: the identity hashes do match 'd96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f' for the revision '5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f'
+
✗ Id failed: the identity hashes do match 'd96f425412c9f8ad5d9a9a05c9831d0728e2338d =/= 475cdfbc8662853dd132ec564e4f5eb0f152dd7f' for the revision '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774'
```

So, let's fix this by running a rebase on the proposal's revision:

```
-
$ rad id rebase 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --no-confirm
-
✓ Identity proposal '5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f' rebased
-
✓ Revision '45477572abd02ea6fc3d3e710e034bb60c74038b'
+
$ rad id rebase 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --no-confirm
+
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' rebased
+
✓ Revision '42b9428df59ad349f706b1397750b75ea3b42574'
title: Add Bob
description: Add Bob as a delegate
status: ❲open❳
@@ -293,9 +293,9 @@ Quorum Reached
We can now update the proposal to have both keys in the delegates set:

```
-
$ rad id update 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --rev 45477572abd02ea6fc3d3e710e034bb60c74038b --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
-
✓ Identity proposal '5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f' updated
-
✓ Revision '66f68597d031f9678e704204bea5ce7f020c98e7'
+
$ rad id update 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --rev 42b9428df59ad349f706b1397750b75ea3b42574 --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
+
✓ Identity proposal '3f6ae4f8645c8b0cbcd35ea924df7b13aca52774' updated
+
✓ Revision '1b4ded759249e4f76d19c3e580b4736bf2a2d1c4'
title: Add Bob
description: Add Bob as a delegate
status: ❲open❳
@@ -338,10 +338,10 @@ Quorum Reached
Finally, we can accept and commit this proposal, creating the final
state of our new Radicle identity:

-
$ rad id show 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --revisions
+
$ rad id show 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --revisions

```
-
$ rad id accept 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --rev 66f68597d031f9678e704204bea5ce7f020c98e7 --no-confirm
+
$ rad id accept 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --rev 1b4ded759249e4f76d19c3e580b4736bf2a2d1c4 --no-confirm
✓ Accepted proposal ✓
title: Add Bob
description: Add Bob as a delegate
@@ -385,7 +385,7 @@ Quorum Reached
```

```
-
$ rad id commit 5d6c0faeeb5ed5e0d66c72d853a6131a35228d3f --rev 66f68597d031f9678e704204bea5ce7f020c98e7 --no-confirm
+
$ rad id commit 3f6ae4f8645c8b0cbcd35ea924df7b13aca52774 --rev 1b4ded759249e4f76d19c3e580b4736bf2a2d1c4 --no-confirm
✓ Committed new identity '60de897bc24898f6908fd1272633c0b15aa4096f'
title: Add Bob
description: Add Bob as a delegate
modified radicle-cli/examples/rad-id.md
@@ -14,7 +14,7 @@ Let's add Bob as a delegate using their DID

```
$ rad id edit --title "Add Bob" --description "Add Bob as a delegate" --delegates did:key:z6MkedTZGJGqgQ2py2b8kGecfxdt2yRdHWF6JpaZC47fovFn --no-confirm
-
✓ Identity proposal 'c2fe23ff387d5e040d0ed8eb2651942f5d0c2aa5' created
+
✓ Identity proposal '0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8' created
title: Add Bob
description: Add Bob as a delegate
status: ❲open❳
@@ -89,7 +89,7 @@ Finally, we can see whether the `Quorum` was reached:
Let's see what happens when we reject the change:

```
-
$ rad id reject c2fe23ff387d5e040d0ed8eb2651942f5d0c2aa5 --no-confirm
+
$ rad id reject 0d396a --no-confirm
✓ Rejected proposal 👎
title: Add Bob
description: Add Bob as a delegate
@@ -145,7 +145,7 @@ increased to `1`.
Instead, let's accept the proposal:

```
-
$ rad id accept c2fe23f --no-confirm
+
$ rad id accept 0d396a --no-confirm
✓ Accepted proposal ✓
title: Add Bob
description: Add Bob as a delegate
@@ -207,7 +207,7 @@ As well as that, the `Quorum` has now been reached:
At this point, we can commit the proposal and update the identity:

```
-
$ rad id commit c2fe23ff387d5e040d0ed8eb2651942f5d0c2aa5 --no-confirm
+
$ rad id commit 0d396a --no-confirm
✓ Committed new identity 'c96e764965aaeff1c6ea3e5b97e2b9828773c8b0'
title: Add Bob
description: Add Bob as a delegate
@@ -255,7 +255,7 @@ the `--threshold` option:

```
$ rad id edit --title "Update threshold" --description "Update to safer threshold" --threshold 2 --no-confirm
-
✓ Identity proposal '27864167e392d63e77df2a7413c988857bd32839' created
+
✓ Identity proposal 'f435d6e89c8f922ede691287c0d8b7f82afa591e' created
title: Update threshold
description: Update to safer threshold
status: ❲open❳
@@ -298,8 +298,8 @@ Quorum Reached
But we change our minds and decide to close the proposal instead:

```
-
$ rad id close 27864167e392d63e77df2a7413c988857bd32839 --no-confirm
-
✓ Closed identity proposal '27864167e392d63e77df2a7413c988857bd32839'
+
$ rad id close f435d6 --no-confirm
+
✓ Closed identity proposal 'f435d6e89c8f922ede691287c0d8b7f82afa591e'
title: Update threshold
description: Update to safer threshold
status: ❲closed❳
@@ -348,15 +348,15 @@ Radicle identity, then we can use the list command:

```
$ rad id list
-
27864167e392d63e77df2a7413c988857bd32839 "Update threshold" ❲closed❳
-
c2fe23ff387d5e040d0ed8eb2651942f5d0c2aa5 "Add Bob"          ❲committed❳
+
0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8 "Add Bob"          ❲committed❳
+
f435d6e89c8f922ede691287c0d8b7f82afa591e "Update threshold" ❲closed❳
```

And if we want to view the latest state of any proposal we can use the
show command:

```
-
$ rad id show 27864167e392d63e77df2a7413c988857bd32839
+
$ rad id show f435d6
title: Update threshold
description: Update to safer threshold
status: ❲closed❳
modified radicle-cli/tests/commands.rs
@@ -513,6 +513,8 @@ fn test_clone_without_seeds() {

#[test]
fn test_cob_replication() {
+
    logger::init(log::Level::Debug);
+

    let mut environment = Environment::new();
    let working = tempfile::tempdir().unwrap();
    let mut alice = environment.node("alice");
modified radicle-cob/Cargo.toml
@@ -14,17 +14,14 @@ keywords = ["radicle", "collaborative objects", "cob", "cobs"]

[dependencies]
fastrand = { version = "1.9.0" }
-
git-commit = { version = "0.3" }
-
git-ref-format = { version = "0.2" }
-
git-trailers = { version = "0.1" }
log = { version = "0.4.17" }
nonempty = { version = "0.8.1", features = ["serialize"] }
-
radicle-git-ext = { version = "0" }
+
radicle-git-ext = { version = "0", features = ["serde"] }
serde_json = { version = "1.0" }
thiserror = { version = "1.0" }

[dependencies.git2]
-
version = "0.16.1"
+
version = "0.17.0"
default-features = false
features = ["vendored-libgit2"]

@@ -43,7 +40,6 @@ features = ["derive"]

[dev-dependencies]
fastrand = { version = "1.9.0", default-features = false }
-
git-ref-format = { version = "0.2", features = ["macro"] }
tempfile = { version = "3" }
qcheck = { version = "1", default-features = false }
qcheck-macros = { version = "1", default-features = false }
modified radicle-cob/src/backend/git/change.rs
@@ -3,10 +3,12 @@
use std::collections::BTreeMap;
use std::convert::TryFrom;

-
use git_commit::{self as commit, Commit};
+
use git_ext::author;
+
use git_ext::author::Author;
+
use git_ext::commit::{headers::Headers, Commit};
use git_ext::Oid;
-
use git_trailers::OwnedTrailer;
use nonempty::NonEmpty;
+
use radicle_git_ext::commit::trailers::OwnedTrailer;

use crate::history::entry::Timestamp;
use crate::signatures;
@@ -23,8 +25,8 @@ pub mod error {
    use std::str::Utf8Error;
    use std::string::FromUtf8Error;

+
    use git_ext::commit;
    use git_ext::Oid;
-
    use git_trailers::Error as TrailerError;
    use thiserror::Error;

    use crate::signatures::error::Signatures;
@@ -32,6 +34,8 @@ pub mod error {
    #[derive(Debug, Error)]
    pub enum Create {
        #[error(transparent)]
+
        WriteCommit(#[from] commit::error::Write),
+
        #[error(transparent)]
        FromUtf8(#[from] FromUtf8Error),
        #[error(transparent)]
        Git(#[from] git2::Error),
@@ -46,7 +50,7 @@ pub mod error {
    #[derive(Debug, Error)]
    pub enum Load {
        #[error(transparent)]
-
        Read(#[from] git_commit::error::Read),
+
        Read(#[from] commit::error::Read),
        #[error(transparent)]
        Signatures(#[from] Signatures),
        #[error(transparent)]
@@ -73,8 +77,6 @@ pub mod error {
        ResourceTrailer(#[from] super::trailers::error::InvalidResourceTrailer),
        #[error("non utf-8 characters in commit message")]
        Utf8(#[from] FromUtf8Error),
-
        #[error(transparent)]
-
        Trailer(#[from] TrailerError),
    }
}

@@ -262,7 +264,7 @@ where
    let author = repo.signature()?;
    let timestamp = author.when().seconds();

-
    let mut headers = commit::Headers::new();
+
    let mut headers = Headers::new();
    headers.push(
        "gpgsig",
        signature
@@ -270,13 +272,13 @@ where
            .map_err(signatures::error::Signatures::from)?
            .as_str(),
    );
-
    let author = commit::Author::try_from(&author)?;
+
    let author = Author::try_from(&author)?;

    #[cfg(debug_assertions)]
    let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::RAD_COMMIT_TIME) {
        let timestamp = s.trim().parse::<i64>().unwrap();
-
        let author = commit::Author {
-
            time: git_commit::author::Time::new(timestamp, 0),
+
        let author = Author {
+
            time: author::Time::new(timestamp, 0),
            ..author
        };
        (author, timestamp)
modified radicle-cob/src/object.rs
@@ -2,8 +2,8 @@

use std::{convert::TryFrom as _, fmt, ops::Deref, str::FromStr};

+
use git_ext::ref_format::{Component, RefString};
use git_ext::Oid;
-
use git_ref_format::{Component, RefString};
use serde::{Deserialize, Serialize};
use thiserror::Error;

modified radicle-cob/src/object/collaboration.rs
@@ -82,9 +82,9 @@ impl CollaborativeObject {
///   * The parsing of the [`TypeName`] fails
pub fn parse_refstr<R>(name: &R) -> Option<(TypeName, ObjectId)>
where
-
    R: AsRef<git_ref_format::RefStr>,
+
    R: AsRef<git_ext::ref_format::RefStr>,
{
-
    use git_ref_format::Qualified;
+
    use git_ext::ref_format::Qualified;
    let name = name.as_ref();
    let refs_cobs = match name.to_namespaced() {
        None => Qualified::from_refstr(name)?,
modified radicle-cob/src/object/storage.rs
@@ -2,8 +2,8 @@

use std::{collections::HashMap, error::Error};

+
use git_ext::ref_format::RefString;
use git_ext::Oid;
-
use git_ref_format::RefString;

use crate::change::Change;
use crate::{ObjectId, TypeName};
@@ -95,7 +95,7 @@ pub trait Storage {
pub mod convert {
    use std::str;

-
    use git_ref_format::RefString;
+
    use git_ext::ref_format::RefString;
    use thiserror::Error;

    use super::{Commit, Reference};
@@ -109,7 +109,7 @@ pub mod convert {
            err: git2::Error,
        },
        #[error(transparent)]
-
        Ref(#[from] git_ref_format::Error),
+
        Ref(#[from] git_ext::ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }
modified radicle-cob/src/signatures.rs
@@ -8,9 +8,9 @@ use std::{
};

use crypto::{ssh, PublicKey};
-
use git_commit::{
+
use git_ext::commit::{
+
    headers::Signature::{Pgp, Ssh},
    Commit,
-
    Signature::{Pgp, Ssh},
};

pub use ssh::ExtendedSignature;
modified radicle-cob/src/test/storage.rs
@@ -30,7 +30,7 @@ pub mod error {
        #[error(transparent)]
        Git(#[from] git2::Error),
        #[error(transparent)]
-
        Format(#[from] git_ref_format::Error),
+
        Format(#[from] git_ext::ref_format::Error),
    }
}

modified radicle-cob/src/tests.rs
@@ -1,7 +1,7 @@
use std::ops::ControlFlow;

use crypto::test::signer::MockSigner;
-
use git_ref_format::{refname, Component, RefString};
+
use git_ext::ref_format::{refname, Component, RefString};
use nonempty::nonempty;
use qcheck::Arbitrary;
use radicle_crypto::Signer;
modified radicle-cob/src/trailers.rs
@@ -1,7 +1,7 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>

-
use git_trailers::{OwnedTrailer, Token, Trailer};
-
use radicle_git_ext as ext;
+
use git_ext::commit::trailers::{OwnedTrailer, Token, Trailer};
+
use std::ops::Deref as _;

pub mod error {
    use thiserror::Error;
@@ -30,11 +30,10 @@ impl ResourceCommitTrailer {
impl TryFrom<&Trailer<'_>> for ResourceCommitTrailer {
    type Error = error::InvalidResourceTrailer;

-
    fn try_from(Trailer { values, token }: &Trailer<'_>) -> Result<Self, Self::Error> {
-
        let val = values.first().ok_or(Self::Error::NoValue)?;
+
    fn try_from(Trailer { value, token }: &Trailer<'_>) -> Result<Self, Self::Error> {
        let ext_oid =
-
            radicle_git_ext::Oid::try_from(val.as_ref()).map_err(|_| Self::Error::InvalidOid)?;
-
        if Some(token) == Token::try_from("Rad-Resource").ok().as_ref() {
+
            git_ext::Oid::try_from(value.as_ref()).map_err(|_| Self::Error::InvalidOid)?;
+
        if token.deref() == "Rad-Resource" {
            Ok(ResourceCommitTrailer(ext_oid.into()))
        } else {
            Err(Self::Error::WrongToken)
@@ -60,7 +59,7 @@ impl From<ResourceCommitTrailer> for Trailer<'_> {
    fn from(containing: ResourceCommitTrailer) -> Self {
        Trailer {
            token: Token::try_from("Rad-Resource").unwrap(),
-
            values: vec![containing.0.to_string().into()],
+
            value: containing.0.to_string().into(),
        }
    }
}
@@ -71,8 +70,8 @@ impl From<ResourceCommitTrailer> for OwnedTrailer {
    }
}

-
impl From<ext::Oid> for ResourceCommitTrailer {
-
    fn from(oid: ext::Oid) -> Self {
+
impl From<git_ext::Oid> for ResourceCommitTrailer {
+
    fn from(oid: git_ext::Oid) -> Self {
        Self(oid.into())
    }
}
modified radicle-cob/src/type_name.rs
@@ -2,7 +2,7 @@

use std::{fmt, str::FromStr};

-
use git_ref_format::{Component, RefString};
+
use git_ext::ref_format::{Component, RefString};
use serde::{Deserialize, Serialize};
use thiserror::Error;

modified radicle-crypto/Cargo.toml
@@ -27,8 +27,8 @@ version = "1.8.0"
default-features = false
optional = true

-
[dependencies.git-ref-format]
-
version = "0.2"
+
[dependencies.radicle-git-ext]
+
version = "0.4"
optional = true

[dependencies.ssh-key]
modified radicle-crypto/src/lib.rs
@@ -360,13 +360,16 @@ impl PublicKey {
        multibase::encode(multibase::Base::Base58Btc, buf)
    }

-
    #[cfg(feature = "git-ref-format")]
-
    pub fn to_namespace(&self) -> git_ref_format::RefString {
-
        git_ref_format::refname!("refs/namespaces").join(git_ref_format::Component::from(self))
+
    #[cfg(feature = "radicle-git-ext")]
+
    pub fn to_namespace(&self) -> radicle_git_ext::ref_format::RefString {
+
        use radicle_git_ext::ref_format::{refname, Component};
+
        refname!("refs/namespaces").join(Component::from(self))
    }

-
    #[cfg(feature = "git-ref-format")]
-
    pub fn from_namespaced(refstr: &git_ref_format::Namespaced) -> Result<Self, PublicKeyError> {
+
    #[cfg(feature = "radicle-git-ext")]
+
    pub fn from_namespaced(
+
        refstr: &radicle_git_ext::ref_format::Namespaced,
+
    ) -> Result<Self, PublicKeyError> {
        let name = refstr.namespace().into_inner();

        Self::from_str(name.deref().as_str())
@@ -405,10 +408,10 @@ impl Deref for PublicKey {
    }
}

-
#[cfg(feature = "git-ref-format")]
-
impl<'a> From<&PublicKey> for git_ref_format::Component<'a> {
+
#[cfg(feature = "radicle-git-ext")]
+
impl<'a> From<&PublicKey> for radicle_git_ext::ref_format::Component<'a> {
    fn from(id: &PublicKey) -> Self {
-
        use git_ref_format::{Component, RefString};
+
        use radicle_git_ext::ref_format::{Component, RefString};
        let refstr =
            RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
        Component::from_refstr(refstr).expect("encoded public keys are valid refname components")
modified radicle-httpd/Cargo.toml
@@ -25,7 +25,7 @@ flate2 = { version = "1" }
hyper = { version = "0.14.17", default-features = false }
lexopt = { version = "0.2.1" }
nonempty = { version = "0.8.1", features = ["serialize"] }
-
radicle-surf = { version = "0.9.0", default-features = false, features = ["serde"] }
+
radicle-surf = { version = "0.10.3", default-features = false, features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
thiserror = { version = "1" }
modified radicle-node/Cargo.toml
@@ -19,7 +19,6 @@ colored = { version = "1.9.0" }
crossbeam-channel = { version = "0.5.6" }
cyphernet = { version = "0.2.0", features = ["tor", "dns", "ed25519", "p2p-ed25519"] }
fastrand = { version = "1.9.0" }
-
git-ref-format = { version = "0.2", features = ["serde", "macro"] }
io-reactor = { version = "0.1.2", features = ["popol"] }
lexopt = { version = "0.2.1" }
libc = { version = "0.2.137" }
@@ -28,6 +27,9 @@ localtime = { version = "1.2.0" }
netservices = { version = "0.2.2", features = ["io-reactor", "socket2"] }
nonempty = { version = "0.8.1", features = ["serialize"] }
qcheck = { version = "1", default-features = false, optional = true }
+
# N.b. this is required to use macros, even though it's re-exported
+
# through radicle
+
radicle-git-ext = { version = "0", features = ["serde"] }
sqlite = { version = "0.30.3" }
sqlite3-src = { version = "0.4.0", features = ["bundled"] } # Ensures static linking
scrypt = { version = "0.10.0", default-features = false }
modified radicle-node/src/worker.rs
@@ -478,7 +478,7 @@ impl Worker {
        channels: &mut Channels,
    ) -> Result<(), FetchError>
    where
-
        S: fetch::AsRefspecs,
+
        S: IntoIterator<Item = fetch::Refspec>,
    {
        let mut tunnel = Tunnel::with(channels, stream, self.nid, remote, self.handle.clone())?;
        let tunnel_addr = tunnel.local_addr();
@@ -498,7 +498,6 @@ impl Worker {

        let namespace = self.nid.to_namespace();
        let mut fetchspecs = specs
-
            .into_refspecs()
            .into_iter()
            // Filter out our own refs, if we aren't cloning.
            .filter(|fs| is_cloning || !fs.dst.starts_with(namespace.as_str()))
modified radicle-node/src/worker/fetch.rs
@@ -1,5 +1,5 @@
mod refspecs;
-
pub use refspecs::{AsRefspecs, Refspec, SpecialRefs};
+
pub use refspecs::SpecialRefs;

pub mod error;

@@ -7,6 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::ops::Deref;

use radicle::crypto::{PublicKey, Unverified, Verified};
+
use radicle::git::refspec;
use radicle::git::{url, Namespaced};
use radicle::prelude::{Doc, Id, NodeId};
use radicle::storage::git::Repository;
@@ -15,6 +16,8 @@ use radicle::storage::{Namespaces, RefUpdate, Remote, RemoteId};
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository, WriteStorage};
use radicle::{git, Storage};

+
pub type Refspec = refspec::Refspec<git::PatternString, git::PatternString>;
+

/// The initial phase of staging a fetch from a remote.
///
/// The [`StagingPhaseInitial::refpsecs`] generated are to fetch the
@@ -146,15 +149,14 @@ impl<'a> StagingPhaseInitial<'a> {

    /// Return the fetch refspecs for fetching the necessary `rad`
    /// references.
-
    pub fn refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
+
    pub fn refspecs(&self) -> Vec<Refspec> {
        let id = git::PatternString::from(IDENTITY_BRANCH.clone().into_refstring());
        match self.repo {
-
            StagedRepository::Cloning(_) => Refspec {
+
            StagedRepository::Cloning(_) => vec![Refspec {
                src: id.clone(),
                dst: id,
                force: false,
-
            }
-
            .into_refspecs(),
+
            }],
            StagedRepository::Fetching(_) => SpecialRefs(self.namespaces.clone()).into_refspecs(),
        }
    }
@@ -229,7 +231,7 @@ impl<'a> StagingPhaseInitial<'a> {
                    // don't need to be re-fetched from the remote.
                    let mut remote = copy.remote_anonymous(&url)?;
                    let refspecs: Vec<_> = Namespaces::All
-
                        .into_refspecs()
+
                        .to_refspecs()
                        .into_iter()
                        .map(|s| s.to_string())
                        .collect();
@@ -256,12 +258,19 @@ impl<'a> StagingPhaseInitial<'a> {
impl<'a> StagingPhaseFinal<'a> {
    /// Return the fetch refspecs for fetching the necessary
    /// references.
-
    pub fn refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
+
    pub fn refspecs(&self) -> Vec<Refspec> {
        match &self.repo {
            FinalStagedRepository::Cloning { trusted, .. } => {
-
                Namespaces::Trusted(trusted.clone()).as_refspecs()
+
                Namespaces::Trusted(trusted.clone()).to_refspecs()
            }
-
            FinalStagedRepository::Fetching { refs, .. } => refs.as_refspecs(),
+
            FinalStagedRepository::Fetching { refs, .. } => refs
+
                .iter()
+
                .map(|r| Refspec {
+
                    src: r.clone().to_ref_string().into(),
+
                    dst: r.clone().to_ref_string().into(),
+
                    force: true,
+
                })
+
                .collect(),
        }
    }

@@ -358,8 +367,8 @@ impl<'a> StagingPhaseFinal<'a> {
                        refspecs.push((
                            remote.id,
                            Refspec {
-
                                src: id.clone(),
-
                                dst: id,
+
                                src: id.clone().into(),
+
                                dst: id.into(),
                                // Nb. The identity branch is allowed to be force-updated.
                                force: true,
                            }
@@ -368,8 +377,8 @@ impl<'a> StagingPhaseFinal<'a> {
                        refspecs.push((
                            remote.id,
                            Refspec {
-
                                src: sigrefs.clone(),
-
                                dst: sigrefs,
+
                                src: sigrefs.clone().into(),
+
                                dst: sigrefs.into(),
                                // Nb. Sigrefs are never force-updated.
                                force: false,
                            }
modified radicle-node/src/worker/fetch/refspecs.rs
@@ -1,43 +1,16 @@
-
use std::collections::BTreeSet;
-
use std::fmt;
-
use std::fmt::Write as _;
-

use radicle::crypto::PublicKey;
use radicle::git;
use radicle::git::refs::storage::{IDENTITY_BRANCH, SIGREFS_BRANCH};
-
use radicle::storage;
use radicle::storage::git::NAMESPACES_GLOB;
-
use radicle::storage::{Namespaces, Remote};
+
use radicle::storage::Namespaces;

-
/// A Git [refspec].
-
///
-
/// [refspec]: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
-
// TODO(finto): this should go into radicle-git-ext/git-ref-format
-
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct Refspec<T, U> {
-
    pub src: T,
-
    pub dst: U,
-
    pub force: bool,
-
}
-

-
impl<T, U> fmt::Display for Refspec<T, U>
-
where
-
    T: fmt::Display,
-
    U: fmt::Display,
-
{
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        if self.force {
-
            f.write_char('+')?;
-
        }
-
        write!(f, "{}:{}", self.src, self.dst)
-
    }
-
}
+
use super::Refspec;

/// Radicle special refs, i.e. `refs/rad/*`.
pub struct SpecialRefs(pub(super) Namespaces);

-
impl AsRefspecs for SpecialRefs {
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
+
impl SpecialRefs {
+
    pub fn into_refspecs(self) -> Vec<Refspec> {
        match &self.0 {
            Namespaces::All => {
                let id = NAMESPACES_GLOB.join(&*IDENTITY_BRANCH);
@@ -55,13 +28,9 @@ impl AsRefspecs for SpecialRefs {
            Namespaces::Trusted(pks) => pks.iter().flat_map(rad_refs).collect(),
        }
    }
-

-
    fn into_refspecs(self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        self.as_refspecs()
-
    }
}

-
fn rad_refs(pk: &PublicKey) -> Vec<Refspec<git::PatternString, git::PatternString>> {
+
fn rad_refs(pk: &PublicKey) -> Vec<Refspec> {
    let ns = pk.to_namespace();
    let id = git::PatternString::from(ns.join(&*IDENTITY_BRANCH));
    let id = Refspec {
@@ -77,95 +46,3 @@ fn rad_refs(pk: &PublicKey) -> Vec<Refspec<git::PatternString, git::PatternStrin
    };
    vec![id, sigrefs]
}
-

-
/// A conversion trait for producing a set of Git [`Refspec`]s.
-
pub trait AsRefspecs
-
where
-
    Self: Sized,
-
{
-
    /// Convert the borrowed data into a set of [`Refspec`]s.
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>>;
-

-
    /// Convert the owned data into a set of [`Refspec`]s.
-
    ///
-
    /// Nb. The default implementation uses
-
    /// [`AsRefspecs::as_refspecs`], which may clone data.
-
    fn into_refspecs(self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        self.as_refspecs()
-
    }
-
}
-

-
impl AsRefspecs for Namespaces {
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        match self {
-
            Namespaces::All => vec![Refspec {
-
                src: (*storage::git::NAMESPACES_GLOB).clone(),
-
                dst: (*storage::git::NAMESPACES_GLOB).clone(),
-
                force: true,
-
            }],
-
            Namespaces::Trusted(pks) => pks
-
                .iter()
-
                .map(|pk| {
-
                    let ns = pk.to_namespace().with_pattern(git::refspec::STAR);
-
                    Refspec {
-
                        src: ns.clone(),
-
                        dst: ns,
-
                        force: true,
-
                    }
-
                })
-
                .collect(),
-
        }
-
    }
-
}
-

-
impl AsRefspecs for Refspec<git::PatternString, git::PatternString> {
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        vec![self.clone()]
-
    }
-

-
    fn into_refspecs(self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        vec![self]
-
    }
-
}
-

-
impl<T: AsRefspecs> AsRefspecs for Vec<T> {
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        self.iter().flat_map(AsRefspecs::as_refspecs).collect()
-
    }
-

-
    fn into_refspecs(self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        self.into_iter()
-
            .flat_map(AsRefspecs::into_refspecs)
-
            .collect()
-
    }
-
}
-

-
impl AsRefspecs for Remote {
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        let ns = self.id.to_namespace();
-
        // Nb. the references in Refs are expected to be Qualified
-
        self.refs
-
            .iter()
-
            .map(|(name, _)| {
-
                let name = git::PatternString::from(ns.join(name));
-
                Refspec {
-
                    src: name.clone(),
-
                    dst: name,
-
                    force: true,
-
                }
-
            })
-
            .collect()
-
    }
-
}
-

-
impl<'a> AsRefspecs for BTreeSet<git::Namespaced<'a>> {
-
    fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
-
        self.iter()
-
            .map(|r| Refspec {
-
                src: r.clone().to_ref_string().into(),
-
                dst: r.clone().to_ref_string().into(),
-
                force: true,
-
            })
-
            .collect()
-
    }
-
}
modified radicle-tools/Cargo.toml
@@ -7,7 +7,9 @@ edition = "2021"

[dependencies]
anyhow = { version = "1" }
-
git-ref-format = { version = "0", features = ["serde", "macro"] }
+
# N.b. this is required to use macros, even though it's re-exported
+
# through radicle
+
radicle-git-ext = { version = "0", features = ["serde"] }

[dependencies.radicle]
version = "0"
modified radicle/Cargo.toml
@@ -14,7 +14,6 @@ amplify = { version = "4.0.0-beta.7", default-features = false, features = ["std
crossbeam-channel = { version = "0.5.6" }
cyphernet = { version = "0.2.0", features = ["tor", "dns", "ed25519"] }
fastrand = { version = "1.9.0" }
-
git-ref-format = { version = "0", features = ["serde", "macro"] }
multibase = { version = "0.9.1" }
localtime = { version = "1.2.0" }
log = { version = "0.4.17", features = ["std"] }
@@ -30,7 +29,7 @@ thiserror = { version = "1" }
unicode-normalization = { version = "0.1" }

[dependencies.git2]
-
version = "0.16.1"
+
version = "0.17.0"
default-features = false
features = ["vendored-libgit2"]

@@ -45,7 +44,7 @@ version = "0"
[dependencies.radicle-crypto]
path = "../radicle-crypto"
version = "0"
-
features = ["git-ref-format", "ssh", "sqlite", "cyphernet"]
+
features = ["radicle-git-ext", "ssh", "sqlite", "cyphernet"]

[dependencies.radicle-ssh]
path = "../radicle-ssh"
modified radicle/src/cob/identity.rs
@@ -59,6 +59,8 @@ pub enum Action {
        revision: RevisionId,
    },
    Revision {
+
        // N.b. the `Oid` is a blob identifier and not a commit, so we
+
        // do not need to propagate it via HistoryAction.
        current: Oid,
        proposed: Doc<Verified>,
    },
@@ -68,16 +70,7 @@ pub enum Action {
    },
}

-
impl HistoryAction for Action {
-
    fn parents(&self) -> Vec<Oid> {
-
        match self {
-
            Self::Revision { current, .. } => {
-
                vec![*current]
-
            }
-
            _ => vec![],
-
        }
-
    }
-
}
+
impl HistoryAction for Action {}

/// Error applying an operation onto a state.
#[derive(Error, Debug)]
modified radicle/src/cob/issue.rs
@@ -575,7 +575,9 @@ mod test {
    #[test]
    fn test_issue_create_and_assign() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();

        let assignee: ActorId = arbitrary::gen(1);
@@ -612,7 +614,9 @@ mod test {
    #[test]
    fn test_issue_create_and_reassign() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();

        let assignee: ActorId = arbitrary::gen(1);
@@ -642,7 +646,9 @@ mod test {
    #[test]
    fn test_issue_create_and_get() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let created = issues
            .create("My first issue", "Blah blah blah.", &[], &[], &signer)
@@ -664,7 +670,9 @@ mod test {
    #[test]
    fn test_issue_create_and_change_state() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let mut issue = issues
            .create("My first issue", "Blah blah blah.", &[], &[], &signer)
@@ -698,7 +706,9 @@ mod test {
    #[test]
    fn test_issue_create_and_unassign() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();

        let assignee: ActorId = arbitrary::gen(1);
@@ -726,7 +736,9 @@ mod test {
    #[test]
    fn test_issue_edit_title() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let mut issue = issues
            .create("My first issue", "Blah blah blah.", &[], &[], &signer)
@@ -744,7 +756,9 @@ mod test {
    #[test]
    fn test_issue_react() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let mut issue = issues
            .create("My first issue", "Blah blah blah.", &[], &[], &signer)
@@ -767,7 +781,9 @@ mod test {
    #[test]
    fn test_issue_reply() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let mut issue = issues
            .create("My first issue", "Blah blah blah.", &[], &[], &signer)
@@ -805,7 +821,9 @@ mod test {
    #[test]
    fn test_issue_tag() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let bug_tag = Tag::new("bug").unwrap();
        let ux_tag = Tag::new("ux").unwrap();
@@ -835,7 +853,9 @@ mod test {
    #[test]
    fn test_issue_comment() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let author = *signer.public_key();
        let mut issues = Issues::open(&project).unwrap();
        let mut issue = issues
@@ -886,7 +906,9 @@ mod test {
    #[test]
    fn test_issue_all() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();

        issues.create("First", "Blah", &[], &[], &signer).unwrap();
@@ -910,7 +932,9 @@ mod test {
    #[test]
    fn test_issue_multilines() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
+
        let test::setup::Context {
+
            signer, project, ..
+
        } = test::setup::Context::new(&tmp);
        let mut issues = Issues::open(&project).unwrap();
        let created = issues
            .create(
modified radicle/src/cob/patch.rs
@@ -1391,21 +1391,21 @@ mod test {
    #[test]
    fn test_patch_create_and_get() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let author: Did = signer.public_key().into();
        let target = MergeTarget::Delegates;
-
        let oid = git::Oid::from_str("e2a85016a458cd809c0ecee81f8c99613b0b0945").unwrap();
-
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
        let patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                target,
-
                base,
-
                oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1426,8 +1426,8 @@ mod test {
        assert_eq!(revision.author.id(), &author);
        assert_eq!(revision.description(), "");
        assert_eq!(revision.discussion.len(), 0);
-
        assert_eq!(revision.oid, oid);
-
        assert_eq!(revision.base, base);
+
        assert_eq!(revision.oid, pr.oid);
+
        assert_eq!(revision.base, pr.base);

        let (id, _, _) = patches.find_by_revision(rev_id).unwrap().unwrap();
        assert_eq!(id, patch_id);
@@ -1436,17 +1436,19 @@ mod test {
    #[test]
    fn test_patch_discussion() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                git::Oid::try_from("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap(),
-
                git::Oid::try_from("e2a85016a458cd809c0ecee81f8c99613b0b0945").unwrap(),
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1455,7 +1457,7 @@ mod test {
        let (revision_id, _) = patch.revisions().last().unwrap();
        assert!(
            patch
-
                .comment(*revision_id, "patch comment", None, &signer)
+
                .comment(*revision_id, "patch comment", None, signer)
                .is_ok(),
            "can comment on patch"
        );
@@ -1468,25 +1470,25 @@ mod test {
    #[test]
    fn test_patch_merge() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let oid = git::Oid::from_str("e2a85016a458cd809c0ecee81f8c99613b0b0945").unwrap();
-
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let mut patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                base,
-
                oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

        let id = patch.id;
        let (rid, _) = patch.revisions().next().unwrap();
-
        let _merge = patch.merge(*rid, base, &signer).unwrap();
+
        let _merge = patch.merge(*rid, pr.base, signer).unwrap();

        let patch = patches.get(&id).unwrap().unwrap();

@@ -1496,25 +1498,25 @@ mod test {

        let merge = merges.first().unwrap();
        assert_eq!(merge.node, *signer.public_key());
-
        assert_eq!(merge.commit, base);
+
        assert_eq!(merge.commit, pr.base);
    }

    #[test]
    fn test_patch_review() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let mut patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                base,
-
                oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1525,7 +1527,7 @@ mod test {
                Some(Verdict::Accept),
                Some("LGTM".to_owned()),
                vec![],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1626,20 +1628,20 @@ mod test {
    #[test]
    fn test_patch_review_edit() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
        let blob = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d133999").unwrap();
-
        let mut patches = Patches::open(&project).unwrap();
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let mut patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                base,
-
                oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1650,7 +1652,7 @@ mod test {
            location: CodeLocation {
                blob,
                path: Path::new("file.rs").to_path_buf(),
-
                commit: oid,
+
                commit: pr.oid,
                lines: 1..3,
            },
            comment: "Nice!".to_owned(),
@@ -1662,7 +1664,7 @@ mod test {
                Some(Verdict::Accept),
                Some("LGTM".to_owned()),
                inline.clone(),
-
                &signer,
+
                signer,
            )
            .unwrap();
        patch
@@ -1671,7 +1673,7 @@ mod test {
                Some(Verdict::Reject),
                Some("LGTM".to_owned()),
                vec![],
-
                &signer,
+
                signer,
            )
            .unwrap(); // Overwrite the verdict.

@@ -1691,7 +1693,7 @@ mod test {
                Some(Verdict::Reject),
                Some("Whoops!".to_owned()),
                vec![],
-
                &signer,
+
                signer,
            )
            .unwrap(); // Overwrite the comment.
        let (_, revision) = patch.latest().unwrap();
@@ -1704,19 +1706,19 @@ mod test {
    #[test]
    fn test_patch_reject_to_accept() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let mut patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                base,
-
                oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1729,7 +1731,7 @@ mod test {
                Some(Verdict::Reject),
                Some("Nah".to_owned()),
                vec![],
-
                &signer,
+
                signer,
            )
            .unwrap();
        patch
@@ -1738,7 +1740,7 @@ mod test {
                Some(Verdict::Accept),
                Some("LGTM".to_owned()),
                vec![],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1755,19 +1757,19 @@ mod test {
    #[test]
    fn test_patch_review_remove_fields() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let mut patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                base,
-
                oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1780,10 +1782,10 @@ mod test {
                Some(Verdict::Reject),
                Some("Nah".to_owned()),
                vec![],
-
                &signer,
+
                signer,
            )
            .unwrap();
-
        patch.review(rid, None, None, vec![], &signer).unwrap();
+
        patch.review(rid, None, None, vec![], signer).unwrap();

        let id = patch.id;
        let patch = patches.get_mut(&id).unwrap();
@@ -1797,20 +1799,19 @@ mod test {
    #[test]
    fn test_patch_update() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, project) = test::setup::context(&tmp);
-
        let base = git::Oid::from_str("af08e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let rev0_oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
        let rev1_oid = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
-
        let mut patches = Patches::open(&project).unwrap();
+
        let ctx = test::setup::Context::new(&tmp);
+
        let signer = &ctx.signer;
+
        let pr = ctx.branch_with(test::setup::initial_blobs());
+
        let mut patches = Patches::open(&ctx.project).unwrap();
        let mut patch = patches
            .create(
                "My first patch",
                "Blah blah blah.",
                MergeTarget::Delegates,
-
                base,
-
                rev0_oid,
+
                pr.base,
+
                pr.oid,
                &[],
-
                &signer,
+
                signer,
            )
            .unwrap();

@@ -1818,8 +1819,9 @@ mod test {
        assert_eq!(patch.description(), "Blah blah blah.");
        assert_eq!(patch.version(), 0);

+
        let update = ctx.branch_with(test::setup::update_blobs());
        let _ = patch
-
            .update("I've made changes.", base, rev1_oid, &signer)
+
            .update("I've made changes.", pr.base, update.oid, signer)
            .unwrap();
        assert_eq!(patch.clock.get(), 2);

@@ -1837,7 +1839,7 @@ mod test {
        let (_, revision) = patch.latest().unwrap();

        assert_eq!(patch.version(), 1);
-
        assert_eq!(revision.oid, rev1_oid);
+
        assert_eq!(revision.oid, update.oid);
        assert_eq!(revision.description(), "I've made changes.");
    }
}
modified radicle/src/cob/thread.rs
@@ -497,7 +497,7 @@ mod tests {
    #[test]
    fn test_redact_comment() {
        let tmp = tempfile::tempdir().unwrap();
-
        let (_, signer, _) = radicle::test::setup::context(&tmp);
+
        let radicle::test::setup::Context { signer, .. } = radicle::test::setup::Context::new(&tmp);
        let repo = gen::<MockRepository>(1);
        let mut alice = Actor::new(signer);
        let mut thread = Thread::default();
modified radicle/src/git.rs
@@ -3,7 +3,7 @@ use std::path::Path;
use std::process::Command;
use std::str::FromStr;

-
use git_ref_format as format;
+
use git_ext::ref_format as format;
use once_cell::sync::Lazy;

use crate::collections::HashMap;
@@ -17,8 +17,8 @@ pub use ext::Error;
pub use ext::NotFound;
pub use ext::Oid;
pub use git2 as raw;
-
pub use git_ref_format as fmt;
-
pub use git_ref_format::{
+
pub use git_ext::ref_format as fmt;
+
pub use git_ext::ref_format::{
    component, lit, name, qualified, refname, refspec,
    refspec::{PatternStr, PatternString},
    Component, Namespaced, Qualified, RefStr, RefString,
@@ -152,6 +152,7 @@ pub mod refs {

    pub mod storage {
        use format::{
+
            lit,
            name::component,
            refspec::{self, PatternString},
        };
@@ -181,7 +182,7 @@ pub mod refs {
        /// `refs/namespaces/<remote>/refs/heads/<branch>`
        ///
        pub fn branch<'a>(remote: &RemoteId, branch: &RefStr) -> Namespaced<'a> {
-
            Qualified::from(git_ref_format::lit::refs_heads(branch)).with_namespace(remote.into())
+
            Qualified::from(lit::refs_heads(branch)).with_namespace(remote.into())
        }

        /// Get the branch where the project's identity document is stored.
modified radicle/src/identity/doc/id.rs
@@ -1,7 +1,7 @@
use std::ops::Deref;
use std::{ffi::OsString, fmt, str::FromStr};

-
use git_ref_format::{Component, RefString};
+
use git_ext::ref_format::{Component, RefString};
use thiserror::Error;

use crate::crypto;
modified radicle/src/lib.rs
@@ -7,6 +7,7 @@ pub extern crate radicle_crypto as crypto;

#[macro_use]
extern crate amplify;
+
extern crate radicle_git_ext as git_ext;

mod canonical;
pub mod cob;
modified radicle/src/storage.rs
@@ -16,11 +16,12 @@ pub use radicle_git_ext::Oid;

use crate::collections::HashMap;
use crate::git::ext as git_ext;
-
use crate::git::{Qualified, RefError, RefString};
+
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefString};
use crate::identity;
use crate::identity::doc::DocError;
use crate::identity::Did;
use crate::identity::{Id, IdentityError};
+
use crate::storage::git::NAMESPACES_GLOB;
use crate::storage::refs::Refs;

use self::refs::SignedRefs;
@@ -38,6 +39,29 @@ pub enum Namespaces {
    Trusted(HashSet<PublicKey>),
}

+
impl Namespaces {
+
    pub fn to_refspecs(&self) -> Vec<Refspec<PatternString, PatternString>> {
+
        match self {
+
            Namespaces::All => vec![Refspec {
+
                src: (*NAMESPACES_GLOB).clone(),
+
                dst: (*NAMESPACES_GLOB).clone(),
+
                force: true,
+
            }],
+
            Namespaces::Trusted(pks) => pks
+
                .iter()
+
                .map(|pk| {
+
                    let ns = pk.to_namespace().with_pattern(git::refspec::STAR);
+
                    Refspec {
+
                        src: ns.clone(),
+
                        dst: ns,
+
                        force: true,
+
                    }
+
                })
+
                .collect(),
+
        }
+
    }
+
}
+

impl FromIterator<PublicKey> for Namespaces {
    fn from_iter<T: IntoIterator<Item = PublicKey>>(iter: T) -> Self {
        Self::Trusted(iter.into_iter().collect())
@@ -236,6 +260,22 @@ impl Remote<Verified> {
            refs: self.refs.unverified(),
        }
    }
+

+
    pub fn to_refspecs(&self) -> Vec<Refspec<PatternString, PatternString>> {
+
        let ns = self.id.to_namespace();
+
        // Nb. the references in Refs are expected to be Qualified
+
        self.refs
+
            .iter()
+
            .map(|(name, _)| {
+
                let name = PatternString::from(ns.join(name));
+
                Refspec {
+
                    src: name.clone(),
+
                    dst: name,
+
                    force: true,
+
                }
+
            })
+
            .collect()
+
    }
}

impl<V> Deref for Remote<V> {
modified radicle/src/storage/git.rs
@@ -6,7 +6,6 @@ use std::path::{Path, PathBuf};
use std::{fs, io};

use crypto::{Signer, Unverified, Verified};
-
use git_ref_format::refspec;
use once_cell::sync::Lazy;

use crate::git;
@@ -25,10 +24,10 @@ pub use crate::storage::Error;

use super::RemoteId;

-
pub static NAMESPACES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
+
pub static NAMESPACES_GLOB: Lazy<git::refspec::PatternString> =
+
    Lazy::new(|| git::refspec::pattern!("refs/namespaces/*"));
pub static SIGREFS_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/namespaces/*/rad/sigrefs"));
+
    Lazy::new(|| git::refspec::pattern!("refs/namespaces/*/rad/sigrefs"));
pub static CANONICAL_IDENTITY: Lazy<git::Qualified> = Lazy::new(|| {
    git::Qualified::from_components(
        git::name::component!("rad"),
modified radicle/src/storage/git/cob.rs
@@ -31,7 +31,7 @@ pub enum TypesError {
    #[error(transparent)]
    ParseObjectId(#[from] cob::object::ParseObjectId),
    #[error(transparent)]
-
    RefFormat(#[from] git_ref_format::Error),
+
    RefFormat(#[from] git::fmt::Error),
}

impl cob::Store for Repository {}
modified radicle/src/storage/refs.rs
@@ -361,7 +361,7 @@ pub mod canonical {
    #[derive(Debug, thiserror::Error)]
    pub enum Error {
        #[error(transparent)]
-
        InvalidRef(#[from] git_ref_format::Error),
+
        InvalidRef(#[from] git::fmt::Error),
        #[error("invalid canonical format")]
        InvalidFormat,
        #[error(transparent)]
modified radicle/src/test.rs
@@ -10,20 +10,108 @@ pub mod setup {
    use crate::crypto::test::signer::MockSigner;
    use crate::prelude::*;
    use crate::{
+
        git,
        profile::Home,
+
        rad::REMOTE_NAME,
        test::{fixtures, storage::git::Repository},
        Storage,
    };

-
    pub fn context(tmp: &TempDir) -> (Storage, MockSigner, Repository) {
-
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
-
        let home = tmp.path().join("home");
-
        let paths = Home::new(home.as_path()).unwrap();
-
        let storage = Storage::open(paths.storage()).unwrap();
-
        let (id, _, _, _) = fixtures::project(tmp.path().join("copy"), &storage, &signer).unwrap();
-
        let project = storage.repository(id).unwrap();
+
    #[derive(Debug)]
+
    pub struct BranchWith {
+
        pub base: git::Oid,
+
        pub oid: git::Oid,
+
    }
+

+
    pub struct Context {
+
        pub storage: Storage,
+
        pub signer: MockSigner,
+
        pub project: Repository,
+
        pub working: git2::Repository,
+
    }
+

+
    impl Context {
+
        pub fn new(tmp: &TempDir) -> Self {
+
            let mut rng = fastrand::Rng::new();
+
            let signer = MockSigner::new(&mut rng);
+
            let home = tmp.path().join("home");
+
            let paths = Home::new(home.as_path()).unwrap();
+
            let storage = Storage::open(paths.storage()).unwrap();
+
            let (id, _, working, _) =
+
                fixtures::project(tmp.path().join("copy"), &storage, &signer).unwrap();
+
            let project = storage.repository(id).unwrap();
+

+
            Self {
+
                storage,
+
                signer,
+
                project,
+
                working,
+
            }
+
        }
+

+
        pub fn branch_with(
+
            &self,
+
            blobs: impl IntoIterator<Item = (String, Vec<u8>)>,
+
        ) -> BranchWith {
+
            let refname = git::Qualified::from(git::lit::refs_heads(git::refname!("master")));
+
            let base = self.working.refname_to_id(refname.as_str()).unwrap();
+
            let parent = self.working.find_commit(base).unwrap();
+
            let oid = commit(&self.working, &refname, blobs, &[&parent]);
+

+
            git::push(&self.working, &REMOTE_NAME, [(&refname, &refname)]).unwrap();
+
            BranchWith {
+
                base: base.into(),
+
                oid,
+
            }
+
        }
+
    }
+

+
    pub fn initial_blobs() -> Vec<(String, Vec<u8>)> {
+
        vec![
+
            ("README.md".to_string(), b"Hello, World!".to_vec()),
+
            (
+
                "CONTRIBUTING".to_string(),
+
                b"Please follow the rules".to_vec(),
+
            ),
+
        ]
+
    }
+

+
    pub fn update_blobs() -> Vec<(String, Vec<u8>)> {
+
        vec![
+
            ("README.md".to_string(), b"Hello, Radicle!".to_vec()),
+
            (
+
                "CONTRIBUTING".to_string(),
+
                b"Please follow the rules".to_vec(),
+
            ),
+
        ]
+
    }
+

+
    pub fn commit(
+
        repo: &git2::Repository,
+
        refname: &git::Qualified,
+
        blobs: impl IntoIterator<Item = (String, Vec<u8>)>,
+
        parents: &[&git2::Commit<'_>],
+
    ) -> git::Oid {
+
        let tree = {
+
            let mut tb = repo.treebuilder(None).unwrap();
+
            for (name, blob) in blobs.into_iter() {
+
                let oid = repo.blob(&blob).unwrap();
+
                tb.insert(name, oid, git2::FileMode::Blob.into()).unwrap();
+
            }
+
            tb.write().unwrap()
+
        };
+
        let tree = repo.find_tree(tree).unwrap();
+
        let author = git2::Signature::now("anonymous", "anonymous@example.com").unwrap();

-
        (storage, signer, project)
+
        repo.commit(
+
            Some(refname.as_str()),
+
            &author,
+
            &author,
+
            "test commit",
+
            &tree,
+
            parents,
+
        )
+
        .unwrap()
+
        .into()
    }
}
modified radicle/src/test/storage.rs
@@ -2,8 +2,7 @@ use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};

-
use git_ref_format as fmt;
-
use radicle_git_ext as git_ext;
+
use git_ext::ref_format as fmt;

use crate::crypto::{Signer, Verified};
use crate::identity::doc::{Doc, DocError, Id};