rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood36fc031da22c7ccbfc94545747203bc8da9d17c0
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "c9dfabe3daac1a1eeeba438f64e3d660e490e432",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "Adopt `radicle-surf` from `radicle-git` workspace",
"state": {
"status": "draft",
"conflicts": []
},
"before": "474b7f36933fa56646e8983414074514a3dc8f78",
"after": "36fc031da22c7ccbfc94545747203bc8da9d17c0",
"commits": [
"36fc031da22c7ccbfc94545747203bc8da9d17c0",
"f8b5bde40a6de76318aca6a6972ac67391943e6c",
"e880810e48d7f7d740aafdd33898d43b143a7022",
"0ef1f0714dc940787cc74a04daee57eec06d5def",
"64a028353b4b5a657370b9c6056454a8c44747e1",
"e9b999e2dd1950daab2b1405dccd9a99d5e8ffae"
],
"target": "474b7f36933fa56646e8983414074514a3dc8f78",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "c9dfabe3daac1a1eeeba438f64e3d660e490e432",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "",
"base": "02318f199c6f29a2eede1f282e1f9b99927d27ec",
"oid": "d83fc686a80adabb1f37bdd841b622865b65edc7",
"timestamp": 1768252986
},
{
"id": "40a79eb4724e3ad0e49001efbf3b0f364d5c0408",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "",
"base": "02318f199c6f29a2eede1f282e1f9b99927d27ec",
"oid": "aebddc9857bdad8ea26151fd3f09c20fcaf1c70d",
"timestamp": 1768313737
},
{
"id": "4fe4d5badd9d8c1c8d9a98e8bd35d3f86036bbb9",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Rebase, handle `git-platinum`.",
"base": "474b7f36933fa56646e8983414074514a3dc8f78",
"oid": "36fc031da22c7ccbfc94545747203bc8da9d17c0",
"timestamp": 1784904360
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a"
},
"info_url": "https://cci.rad.levitte.org//c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a.html"
}
Started at: 2026-07-24 16:47:12.813297+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 187 issues · 39 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout c9dfabe3daac1a1eeeba438f64e3d660e490e432
✓ Switched to branch patch/c9dfabe at revision 4fe4d5b
✓ Branch patch/c9dfabe setup to track rad/patches/c9dfabe3daac1a1eeeba438f64e3d660e490e432
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 36fc031da22c7ccbfc94545747203bc8da9d17c0
HEAD is now at 36fc031d surf: Switch to crate in workspace
Exit code: 0
$ rad patch show c9dfabe3daac1a1eeeba438f64e3d660e490e432 -p
╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Adopt `radicle-surf` from `radicle-git` workspace │
│ Patch c9dfabe3daac1a1eeeba438f64e3d660e490e432 │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 36fc031da22c7ccbfc94545747203bc8da9d17c0 │
│ Base 474b7f36933fa56646e8983414074514a3dc8f78 │
│ Branches patch/c9dfabe │
│ Commits ahead 6, behind 0 │
│ Status draft │
├──────────────────────────────────────────────────────────────────────────────┤
│ 36fc031 surf: Switch to crate in workspace │
│ f8b5bde surf: Refactor │
│ e880810 surf: Move test files into place │
│ 0ef1f07 surf: Copy over from radicle-git │
│ 64a0283 scripts/just/check-ellipses: Replace less │
│ e9b999e protocol: Calm `typos` │
├──────────────────────────────────────────────────────────────────────────────┤
│ ● Revision c9dfabe @ 02318f1..d83fc68 by lorenz z6MkkPv…WX5sTEz 6 months ago │
│ ↑ Revision 40a79eb @ 02318f1..aebddc9 by lorenz z6MkkPv…WX5sTEz 6 months ago │
│ ↑ Revision 4fe4d5b @ 474b7f3..36fc031 by lorenz z6MkkPv…WX5sTEz 1 minute ago │
╰──────────────────────────────────────────────────────────────────────────────╯
commit 36fc031da22c7ccbfc94545747203bc8da9d17c0
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sun Jan 11 15:16:09 2026 +0100
surf: Switch to crate in workspace
Now that `radicle-surf` lives in this workspace, use it.
diff --git a/Cargo.lock b/Cargo.lock
index 122824204..13ee0127b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1089,17 +1089,6 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
-[[package]]
-name = "filetime"
-version = "0.2.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
-dependencies = [
- "cfg-if",
- "libc",
- "libredox",
-]
-
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1217,16 +1206,6 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
-[[package]]
-name = "git-ref-format"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56b3fd79d4f8678e804bf9e60ef31bc810889d702df1e7280d800b3c907e1af"
-dependencies = [
- "git-ref-format-core",
- "git-ref-format-macro",
-]
-
[[package]]
name = "git-ref-format-core"
version = "0.6.0"
@@ -1238,18 +1217,6 @@ dependencies = [
"thiserror 1.0.69",
]
-[[package]]
-name = "git-ref-format-macro"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d38795f40abc04aed7473ce282be6a5da97ae58a1445a014193d3bf302b034cf"
-dependencies = [
- "git-ref-format-core",
- "proc-macro-error3",
- "quote",
- "syn 2.0.117",
-]
-
[[package]]
name = "git2"
version = "0.21.0"
@@ -2186,18 +2153,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
-[[package]]
-name = "libredox"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
-dependencies = [
- "bitflags",
- "libc",
- "plain",
- "redox_syscall 0.7.3",
-]
-
[[package]]
name = "libz-sys"
version = "1.1.25"
@@ -2602,7 +2557,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall 0.5.18",
+ "redox_syscall",
"smallvec",
"windows-link 0.2.1",
]
@@ -2689,12 +2644,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
-[[package]]
-name = "plain"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
-
[[package]]
name = "poly1305"
version = "0.8.0"
@@ -2770,28 +2719,6 @@ dependencies = [
"elliptic-curve",
]
-[[package]]
-name = "proc-macro-error-attr3"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34e4dd828515431dd6c4a030d26f7eaed7dd4778226e9d2bb968d65ca4ec3d4d"
-dependencies = [
- "proc-macro2",
- "quote",
-]
-
-[[package]]
-name = "proc-macro-error3"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ee475e440453418ff1335189eddf7101ba502cd818ab7ae04209bc83aa925aa"
-dependencies = [
- "proc-macro-error-attr3",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -2939,7 +2866,7 @@ dependencies = [
"radicle-localtime",
"radicle-log",
"radicle-node",
- "radicle-surf 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "radicle-surf",
"radicle-term",
"schemars",
"serde",
@@ -3067,20 +2994,6 @@ dependencies = [
"thiserror 2.0.18",
]
-[[package]]
-name = "radicle-git-ext"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a30b3153c4db6898f21ad8b0d7f2d6fe04434ec9b66764ef34e23b0b05167ba"
-dependencies = [
- "git-ref-format",
- "git2",
- "percent-encoding",
- "radicle-std-ext",
- "serde",
- "thiserror 1.0.69",
-]
-
[[package]]
name = "radicle-git-metadata"
version = "0.2.0"
@@ -3218,12 +3131,6 @@ dependencies = [
"windows 0.62.2",
]
-[[package]]
-name = "radicle-std-ext"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb935931bdd2a2966f3b584f3031d9d54ec0713ddbc563a0193d54e62a88ec73"
-
[[package]]
name = "radicle-surf"
version = "0.28.0"
@@ -3246,25 +3153,6 @@ dependencies = [
"url",
]
-[[package]]
-name = "radicle-surf"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee7a801291a59d24a533fa4923721181e2ca1d588c8418cdbf619a81b726d273"
-dependencies = [
- "anyhow",
- "base64 0.21.7",
- "flate2",
- "git2",
- "log",
- "nonempty 0.9.0",
- "radicle-git-ext",
- "radicle-std-ext",
- "tar",
- "thiserror 1.0.69",
- "url",
-]
-
[[package]]
name = "radicle-systemd"
version = "0.13.0"
@@ -3378,15 +3266,6 @@ dependencies = [
"bitflags",
]
-[[package]]
-name = "redox_syscall"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
-dependencies = [
- "bitflags",
-]
-
[[package]]
name = "ref-cast"
version = "1.0.25"
@@ -4211,17 +4090,6 @@ dependencies = [
"rustix",
]
-[[package]]
-name = "tar"
-version = "0.4.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
-dependencies = [
- "filetime",
- "libc",
- "xattr",
-]
-
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5274,16 +5142,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
-[[package]]
-name = "xattr"
-version = "1.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
-dependencies = [
- "libc",
- "rustix",
-]
-
[[package]]
name = "yansi"
version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 3c25da833..9053ca589 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,7 @@ radicle-node = { version = "0.21", path = "crates/radicle-node" }
radicle-oid = { version = "0.2.1", path = "crates/radicle-oid", default-features = false }
radicle-protocol = { version = "0.9", path = "crates/radicle-protocol" }
radicle-signals = { version = "0.12", path = "crates/radicle-signals" }
+radicle-surf = { version = "0.28.0", path = "crates/radicle-surf", default-features = false }
radicle-systemd = { version = "0.13", path = "crates/radicle-systemd" }
radicle-term = { version = "0.19", path = "crates/radicle-term" }
radicle-windows = { version = "0.1", path = "crates/radicle-windows" }
@@ -75,12 +76,8 @@ winpipe = "0.1.1"
winsplit = "0.1.0"
zeroize = { version = "1.5.7", default-features = false }
-# Crates from the "radicle-git" workspace. These should be synced manually.
-# When updating, start from `radicle-surf`:
-# `radicle-surf` → `radicle-git-ext` → `git-ref-format` → `git-ref-format-core`
-# Also note that `radicle-surf → git2` so try to also sync with `git2`.
+# Crates from the "radicle-git" workspace.
git-ref-format-core = { version = "0.6.0", default-features = false }
-radicle-surf = "0.28.0"
[workspace.lints.clippy]
fallible_impl_from = "deny"
diff --git a/crates/radicle-cli/src/commands/patch/review/builder.rs b/crates/radicle-cli/src/commands/patch/review/builder.rs
index 09f7e4150..4b48d2668 100644
--- a/crates/radicle-cli/src/commands/patch/review/builder.rs
+++ b/crates/radicle-cli/src/commands/patch/review/builder.rs
@@ -200,28 +200,25 @@ impl ReviewItem {
#[allow(clippy::type_complexity)]
fn paths(&self) -> (Option<(&Path, Oid)>, Option<(&Path, Oid)>) {
match self {
- Self::FileAdded { path, new, .. } => (None, Some((path, Oid::from(*new.oid)))),
- Self::FileDeleted { path, old, .. } => (Some((path, Oid::from(*old.oid))), None),
+ Self::FileAdded { path, new, .. } => (None, Some((path, new.oid))),
+ Self::FileDeleted { path, old, .. } => (Some((path, old.oid)), None),
Self::FileMoved { moved } => (
- Some((&moved.old_path, Oid::from(*moved.old.oid))),
- Some((&moved.new_path, Oid::from(*moved.new.oid))),
+ Some((&moved.old_path, moved.old.oid)),
+ Some((&moved.new_path, moved.new.oid)),
),
Self::FileCopied { copied } => (
- Some((&copied.old_path, Oid::from(*copied.old.oid))),
- Some((&copied.new_path, Oid::from(*copied.new.oid))),
- ),
- Self::FileModified { path, old, new, .. } => (
- Some((path, Oid::from(*old.oid))),
- Some((path, Oid::from(*new.oid))),
- ),
- Self::FileEofChanged { path, old, new, .. } => (
- Some((path, Oid::from(*old.oid))),
- Some((path, Oid::from(*new.oid))),
- ),
- Self::FileModeChanged { path, old, new, .. } => (
- Some((path, Oid::from(*old.oid))),
- Some((path, Oid::from(*new.oid))),
+ Some((&copied.old_path, copied.old.oid)),
+ Some((&copied.new_path, copied.new.oid)),
),
+ Self::FileModified { path, old, new, .. } => {
+ (Some((path, old.oid)), Some((path, new.oid)))
+ }
+ Self::FileEofChanged { path, old, new, .. } => {
+ (Some((path, old.oid)), Some((path, new.oid)))
+ }
+ Self::FileModeChanged { path, old, new, .. } => {
+ (Some((path, old.oid)), Some((path, new.oid)))
+ }
}
}
diff --git a/crates/radicle-cli/src/git/pretty_diff.rs b/crates/radicle-cli/src/git/pretty_diff.rs
index d7eaae92a..53bb7052c 100644
--- a/crates/radicle-cli/src/git/pretty_diff.rs
+++ b/crates/radicle-cli/src/git/pretty_diff.rs
@@ -338,7 +338,7 @@ impl ToPretty for Added {
repo: &R,
) -> Self::Output {
let old = None;
- let new = Some((self.path.as_path(), Oid::from(*self.new.oid)));
+ let new = Some((self.path.as_path(), self.new.oid));
pretty_modification(header, &self.diff, old, new, repo, hi)
}
@@ -354,7 +354,7 @@ impl ToPretty for Deleted {
header: &Self::Context,
repo: &R,
) -> Self::Output {
- let old = Some((self.path.as_path(), Oid::from(*self.old.oid)));
+ let old = Some((self.path.as_path(), self.old.oid));
let new = None;
pretty_modification(header, &self.diff, old, new, repo, hi)
@@ -371,8 +371,8 @@ impl ToPretty for Modified {
header: &Self::Context,
repo: &R,
) -> Self::Output {
- let old = Some((self.path.as_path(), Oid::from(*self.old.oid)));
- let new = Some((self.path.as_path(), Oid::from(*self.new.oid)));
+ let old = Some((self.path.as_path(), self.old.oid));
+ let new = Some((self.path.as_path(), self.new.oid));
pretty_modification(header, &self.diff, old, new, repo, hi)
}
diff --git a/crates/radicle-cli/src/git/unified_diff.rs b/crates/radicle-cli/src/git/unified_diff.rs
index 3b4fc8f78..24fb8f8df 100644
--- a/crates/radicle-cli/src/git/unified_diff.rs
+++ b/crates/radicle-cli/src/git/unified_diff.rs
@@ -309,8 +309,8 @@ impl Encode for FileHeader {
if old.mode == new.mode {
w.meta(format!(
"index {}..{} {:o}",
- term::format::oid(*old.oid),
- term::format::oid(*new.oid),
+ term::format::oid(old.oid),
+ term::format::oid(new.oid),
u32::from(old.mode.clone()),
))?;
} else {
@@ -318,8 +318,8 @@ impl Encode for FileHeader {
w.meta(format!("new mode {:o}", u32::from(new.mode.clone())))?;
w.meta(format!(
"index {}..{}",
- term::format::oid(*old.oid),
- term::format::oid(*new.oid)
+ term::format::oid(old.oid),
+ term::format::oid(new.oid)
))?;
}
@@ -337,7 +337,7 @@ impl Encode for FileHeader {
w.meta(format!(
"index {}..{}",
term::format::oid(git::Oid::ZERO_SHA1),
- term::format::oid(*new.oid),
+ term::format::oid(new.oid),
))?;
w.meta("--- /dev/null")?;
@@ -357,7 +357,7 @@ impl Encode for FileHeader {
))?;
w.meta(format!(
"index {}..{}",
- term::format::oid(*old.oid),
+ term::format::oid(old.oid),
term::format::oid(git::Oid::ZERO_SHA1)
))?;
commit f8b5bde40a6de76318aca6a6972ac67391943e6c
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sat Jan 10 15:31:57 2026 +0100
surf: Refactor
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.
diff --git a/.codespellrc b/.codespellrc
index 11bb829ed..a20229636 100644
--- a/.codespellrc
+++ b/.codespellrc
@@ -1,5 +1,5 @@
# See: https://github.com/codespell-project/codespell#using-a-config-file
[codespell]
-skip = .git*,*.lock,.codespellrc,target,.jj,.direnv,simulation/modules/radicle-node/cue.mod/*
+skip = .git*,*.lock,.codespellrc,target,.jj,.direnv,simulation/modules/radicle-node/cue.mod/*,crates/radicle-surf/src/test/platinum/*
check-hidden = true
ignore-words-list = ser,set,noes
diff --git a/.typos.toml b/.typos.toml
index 060f83629..1be401dcf 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -34,6 +34,7 @@ extend-ignore-re = [
[files]
extend-exclude = [
+ "crates/radicle-surf/src/test/platinum",
"simulation/modules/radicle-node/cue.mod"
]
diff --git a/Cargo.lock b/Cargo.lock
index bd7cc8809..122824204 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1818,6 +1818,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hex-literal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1"
+
[[package]]
name = "hmac"
version = "0.12.1"
@@ -2296,6 +2302,12 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "minicbor"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "124d887cb82f0b1469bdac3d1b65764a381eed1a54fdab0070e5772b13114521"
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2927,7 +2939,7 @@ dependencies = [
"radicle-localtime",
"radicle-log",
"radicle-node",
- "radicle-surf",
+ "radicle-surf 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)",
"radicle-term",
"schemars",
"serde",
@@ -3073,6 +3085,7 @@ dependencies = [
name = "radicle-git-metadata"
version = "0.2.0"
dependencies = [
+ "git2",
"thiserror 2.0.18",
]
@@ -3211,6 +3224,28 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb935931bdd2a2966f3b584f3031d9d54ec0713ddbc563a0193d54e62a88ec73"
+[[package]]
+name = "radicle-surf"
+version = "0.28.0"
+dependencies = [
+ "base64 0.21.7",
+ "flate2",
+ "git2",
+ "hex-literal",
+ "log",
+ "minicbor",
+ "nonempty 0.9.0",
+ "proptest",
+ "radicle-git-metadata",
+ "radicle-git-ref-format",
+ "radicle-oid",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 1.0.69",
+ "url",
+]
+
[[package]]
name = "radicle-surf"
version = "0.28.0"
@@ -4637,6 +4672,7 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
+ "serde_derive",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ff5b6c0fc..3c25da833 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,8 +2,6 @@
members = ["crates/*"]
resolver = "2"
-exclude = ["crates/radicle-surf"] # TODO: Remove exclusion.
-
[workspace.package]
edition = "2024"
license = "MIT OR Apache-2.0"
diff --git a/crates/radicle-git-metadata/Cargo.toml b/crates/radicle-git-metadata/Cargo.toml
index 05e237915..92578e314 100644
--- a/crates/radicle-git-metadata/Cargo.toml
+++ b/crates/radicle-git-metadata/Cargo.toml
@@ -11,4 +11,5 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
-thiserror = { workspace = true, default-features = true }
\ No newline at end of file
+thiserror = { workspace = true, default-features = true }
+git2 = { workspace = true, optional = true }
\ No newline at end of file
diff --git a/crates/radicle-git-metadata/src/author.rs b/crates/radicle-git-metadata/src/author.rs
index e0c133658..f172cb63a 100644
--- a/crates/radicle-git-metadata/src/author.rs
+++ b/crates/radicle-git-metadata/src/author.rs
@@ -123,3 +123,16 @@ impl FromStr for Author {
})
}
}
+
+#[cfg(feature = "git2")]
+impl TryFrom<&Author> for git2::Signature<'_> {
+ type Error = git2::Error;
+
+ fn try_from(author: &Author) -> Result<Self, Self::Error> {
+ git2::Signature::new(
+ &author.name,
+ &author.email,
+ &git2::Time::new(author.time.seconds(), author.time.offset()),
+ )
+ }
+}
diff --git a/crates/radicle-surf/CHANGELOG.md b/crates/radicle-surf/CHANGELOG.md
index 7399d3b7d..666e1637a 100644
--- a/crates/radicle-surf/CHANGELOG.md
+++ b/crates/radicle-surf/CHANGELOG.md
@@ -27,7 +27,7 @@ changes include:
- `Browser` is removed. Its methods are implemented directly with `Repository`.
- Git will be the only supported VCS. Any extension points for other VCSes were
removed.
-- `Ref` and `RefScope` are removed. Re-use the `git-ref-format` crate and a new
+- `Ref` and `RefScope` are removed. Reuse the `git-ref-format` crate and a new
`Glob` type for the refspec patterns.
- Added support of `Tree` and `Blob` that correspond to their definitions in
Git.
diff --git a/crates/radicle-surf/Cargo.toml b/crates/radicle-surf/Cargo.toml
index d9029d0b6..94a112d17 100644
--- a/crates/radicle-surf/Cargo.toml
+++ b/crates/radicle-surf/Cargo.toml
@@ -1,59 +1,39 @@
[package]
name = "radicle-surf"
+version = "0.28.0"
description = "A code surfing library for Git repositories"
+
readme = "README.md"
-version = "0.28.0"
-authors = ["The Radicle Team <dev@radicle.xyz>"]
-homepage.workspace = true
-repository.workspace = true
+
edition.workspace = true
license.workspace = true
+repository.workspace = true
rust-version.workspace = true
-include = [
- "**/*.rs",
- "Cargo.toml",
- "data/git-platinum.tgz",
-]
-
-[lib]
-test = false
-doctest = false
-
[features]
# NOTE: testing `test_submodule_failure` on GH actions
# is painful since it uses this specific repo and expects
# certain branches to be setup. So we use this feature flag
# to ignore the test on CI.
gh-actions = []
-minicbor = ["radicle-git-ext/minicbor"]
-serde = ["dep:serde", "url/serde"]
+serde = ["dep:serde", "radicle-git-ref-format/serde", "radicle-oid/serde", "url/serde"]
[dependencies]
base64 = "0.21"
+git2 = { workspace = true, features = ["vendored-libgit2"] }
log = "0.4"
+minicbor = { version = "0.13", features = ["std"], optional = true }
nonempty = "0.9"
+radicle-git-metadata = { workspace = true, features = ["git2"] }
+radicle-git-ref-format = { workspace = true, features = ["macro"] }
+radicle-oid = { workspace = true, features = ["git2", "sha1"] }
+serde = { workspace = true, optional = true, features = ["derive"] }
thiserror = "1.0"
url = "2.5.4"
-[dependencies.git2]
-workspace = true
-
-[dependencies.radicle-git-ext]
-version = "0.13.0"
-path = "../radicle-git-ext"
-features = ["serde"]
-
-[dependencies.radicle-std-ext]
-version = "0.2.0"
-path = "../radicle-std-ext"
-
-[dependencies.serde]
-version = "1"
-features = ["serde_derive"]
-optional = true
-
-[build-dependencies]
-anyhow = "1.0"
+[dev-dependencies]
flate2 = "1.1"
-tar = "0.4.45"
+hex-literal = "1.1.0"
+proptest = { workspace = true }
+serde_json = { workspace = true }
+tempfile = { workspace = true }
\ No newline at end of file
diff --git a/crates/radicle-surf/DEVELOPMENT.md b/crates/radicle-surf/DEVELOPMENT.md
index a3df0fa74..6610737c9 100644
--- a/crates/radicle-surf/DEVELOPMENT.md
+++ b/crates/radicle-surf/DEVELOPMENT.md
@@ -5,7 +5,7 @@ Thanks for wanting to contribute to `radicle-surf`!
## Building & Testing 🏗️
-We try to make development as seemless as possible so we can get down to the real work. We supply
+We try to make development as seamless as possible so we can get down to the real work. We supply
the toolchain via the `rust-toolchain` file, and the formatting rules `.rustmt.toml` file.
For the [Nix](https://nixos.org/) inclined there is a `default.nix` file to get all the necessary
@@ -52,7 +52,7 @@ of development.
If more tests are needed then we should add them under `mod tests` in the relevant module. We strive
to find properties of our programs so that we can use tools like `proptest` to extensively prove our
-programs are correct. As well as this, we add unit tests to esnure the examples in our heads are
+programs are correct. As well as this, we add unit tests to ensure the examples in our heads are
correct, and testing out the ergonomics of our API first-hand.
## CI files 🤖
diff --git a/crates/radicle-surf/README.md b/crates/radicle-surf/README.md
index 405e75585..33de830ec 100644
--- a/crates/radicle-surf/README.md
+++ b/crates/radicle-surf/README.md
@@ -2,8 +2,6 @@
A code surfing library for Git repositories 🏄♀️🏄♂️
-Welcome to `radicle-surf`!
-
`radicle-surf` is a library to describe a Git repository as a file system. It
aims to provide an easy-to-use API to browse a repository via the concept of
files and directories for any given revision. It also allows the user to diff
@@ -11,12 +9,3 @@ any two different revisions.
One of the use cases would be to create a web GUI for interacting with a Git
repository (thinking GitHub, GitLab or similar systems).
-
-## Contributing
-
-To get started on contributing you can check out our [developing guide](../DEVELOPMENT.md), and also
-our [LICENSE](../LICENSE) file.
-
-## The Community
-
-Join our community disccussions at [radicle.community](https://radicle.community)!
diff --git a/crates/radicle-surf/docs/denotational-design.md b/crates/radicle-surf/docs/denotational-design.md
index 88a2c41e6..779c37133 100644
--- a/crates/radicle-surf/docs/denotational-design.md
+++ b/crates/radicle-surf/docs/denotational-design.md
@@ -11,7 +11,7 @@ and view their differences.
The stream of consciousness that gave birth to this document started with thinking how the user would interact with
the system, identifying the key components. This is captured in [User Flow](#user-flow). From there we found nouns that
represent objects in our system and verbs that represent functions over those objects. This iteratively informed us as
-to what other actions we would need to supply. We would occassionally look at [GitHub](todo) and [Pijul Nest](todo) for
+to what other actions we would need to supply. We would occasionally look at [GitHub](todo) and [Pijul Nest](todo) for
inspiration, since we would like to imitate the features that they supply, and we ultimately want use one or both of
these for our backends.
@@ -33,7 +33,7 @@ repl = do
But then we thought about what happens when we are in `browse` but we would like to change the history and see that
file or directory at a different snapshot. This was captured in the pseudo-code below:
```haskell
- src_foo_bar <- find...
+ src_foo_bar <- find…
history' <- historyOf src_foo_bar
```
@@ -54,7 +54,7 @@ type Label
-- |-- bar.hs
--
-- Would look like:
--- @("foo", Right ("bar.hs", "module Banana ...") :| [])@
+-- @("foo", Right ("bar.hs", "module Banana …") :| [])@
type Directory
μ Directory = (Label, NonEmpty (Either Directory File))
@@ -64,7 +64,7 @@ type DirectoryContents
μ DirectoryContents = IsRepo | Directory | File
-- Opaque representation of repository state directories (e.g. `.git`, `.pijul`)
--- Those are not browseable, but have to be present at the repo root 'Directory'.
+-- Those are not browsable, but have to be present at the repo root 'Directory'.
type IsRepo
-- A Directory captures its own Label followed by 1 or more DirectoryContents
@@ -91,7 +91,7 @@ type SystemType
= IsFile
| IsDirectory
--- A Chnage is an enumeration of how a file has changed.
+-- A Change is an enumeration of how a file has changed.
-- This is simply used for getting the difference between two
-- directories.
type Change
diff --git a/crates/radicle-surf/docs/refactor-design.md b/crates/radicle-surf/docs/refactor-design.md
index aac67c084..b861c93d6 100644
--- a/crates/radicle-surf/docs/refactor-design.md
+++ b/crates/radicle-surf/docs/refactor-design.md
@@ -1,7 +1,7 @@
# An updated design for radicle-surf
This is a design blueprint for the new `radicle-git/radicle-surf` crate. The
-actual design details and implemenation are described and updated in its
+actual design details and implementation are described and updated in its
documentation comments, viewable via `cargo doc`.
## Introduction
@@ -16,7 +16,7 @@ UI for a git repo:
2. Diff between two revisions that resolve into two commits.
3. Retrieve the history of commits with a given head, and optionally a file.
4. List refs and retrieve their metadata: Branches, Tags, Remotes,
-Notes and user-defined "categories", where a category is: `refs/<category>/<...>`.
+Notes and user-defined "categories", where a category is: `refs/<category>/<…>`.
## Motivation
@@ -107,7 +107,7 @@ Currently we have multiple types to identify a `Commit` or `Revision`.
The relations between them are: all `Rev` and `Commit` can resolve into `Oid`,
and in most cases `Rev` can resolve into `Commit`.
-On one hand, `Oid` is the ultimate unique identifer but it is more machine-
+On one hand, `Oid` is the ultimate unique identifier but it is more machine-
friendly than human-friendly. On the other hand, `Revision` is most human-
friendly and better suited in the API interface. A conversion from `Revision`
to `Oid` will be useful.
@@ -138,7 +138,7 @@ Our API will use these traits where we expect a `Revision` or a `Commit`.
The current `History` is generic over VCS types and also retrieves the full list
of commits when the history is created. The VCS part can be removed and the
-history can lazy-load the list of commits by implmenting `Iterator` to support
+history can lazy-load the list of commits by implementing `Iterator` to support
potentially very long histories.
We can also store the head commit with the history so that it's easy to get
@@ -288,7 +288,7 @@ pub struct History<'a> {
}
impl<'a> History<'a> {
- /// This method creats a new `RevWalk` internally and return an
+ /// This method creates a new `RevWalk` internally and return an
/// iterator for all commits in a history.
pub fn iter(&self) -> impl Iterator<Item = Commit>;
}
diff --git a/crates/radicle-surf/examples/browsing.rs b/crates/radicle-surf/examples/browsing.rs
index a5206bbdf..bd00ec651 100644
--- a/crates/radicle-surf/examples/browsing.rs
+++ b/crates/radicle-surf/examples/browsing.rs
@@ -8,8 +8,8 @@
//! the directories in a tree-like structure.
use radicle_surf::{
- fs::{self, Directory},
Repository,
+ fs::{self, Directory},
};
use std::{env, time::Instant};
@@ -33,12 +33,12 @@ fn main() {
fn print_directory(d: &Directory, repo: &Repository, indent_level: usize) {
let indent = " ".repeat(indent_level * 4);
- println!("{}{}/", &indent, d.name());
+ println!("{}{}/", indent, d.name());
for entry in d.entries(repo).unwrap() {
match entry {
- fs::Entry::File(f) => println!(" {}{}", &indent, f.name()),
+ fs::Entry::File(f) => println!(" {}{}", indent, f.name()),
fs::Entry::Directory(d) => print_directory(&d, repo, indent_level + 1),
- fs::Entry::Submodule(s) => println!(" {}{}", &indent, s.name()),
+ fs::Entry::Submodule(s) => println!(" {}{}", indent, s.name()),
}
}
}
diff --git a/crates/radicle-surf/examples/diff.rs b/crates/radicle-surf/examples/diff.rs
index 9454589af..d15a6c7f6 100644
--- a/crates/radicle-surf/examples/diff.rs
+++ b/crates/radicle-surf/examples/diff.rs
@@ -2,8 +2,8 @@ extern crate radicle_surf;
use std::{env::Args, str::FromStr, time::Instant};
-use radicle_git_ext::Oid;
-use radicle_surf::{diff::Diff, Repository};
+use radicle_oid::Oid;
+use radicle_surf::{Repository, diff::Diff};
fn main() {
let options = get_options_or_exit();
@@ -80,7 +80,8 @@ impl Options {
\tpath-to-repo: Path to the directory containing .git subdirectory\n\
\tbase-revision: Git commit ID of the base revision (one that will be considered less recent)\n\
\thead-revision: Git commit ID of the head revision (one that will be considered more recent) or 'HEAD' to use current git HEAD\n",
- args[0]));
+ args[0]
+ ));
}
let path_to_repo = args[1].clone();
diff --git a/crates/radicle-surf/src/blob.rs b/crates/radicle-surf/src/blob.rs
index 427d0ca70..5ad236a3c 100644
--- a/crates/radicle-surf/src/blob.rs
+++ b/crates/radicle-surf/src/blob.rs
@@ -3,12 +3,12 @@
use std::ops::Deref;
-use radicle_git_ext::Oid;
+use radicle_oid::Oid;
#[cfg(feature = "serde")]
use serde::{
- ser::{SerializeStruct as _, Serializer},
Serialize,
+ ser::{SerializeStruct as _, Serializer},
};
use crate::Commit;
diff --git a/crates/radicle-surf/src/branch.rs b/crates/radicle-surf/src/branch.rs
index 73fe8fa2b..dac4d3b2f 100644
--- a/crates/radicle-surf/src/branch.rs
+++ b/crates/radicle-surf/src/branch.rs
@@ -3,7 +3,7 @@ use std::{
str::{self, FromStr},
};
-use git_ext::ref_format::{component, lit, Component, Qualified, RefStr, RefString};
+use radicle_git_ref_format::{Component, Qualified, RefStr, RefString, lit, name::component};
use crate::refs::refstr_join;
@@ -295,7 +295,7 @@ impl FromStr for Remote {
}
pub mod error {
- use radicle_git_ext::ref_format::{self, RefString};
+ use radicle_git_ref_format::{self, RefString};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -305,7 +305,7 @@ pub mod error {
#[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
NotQualified(String),
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
}
@@ -317,7 +317,7 @@ pub mod error {
#[error("the refname '{0}' did not begin with 'refs/heads'")]
NotQualified(String),
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
}
@@ -329,7 +329,7 @@ pub mod error {
#[error("the refname '{0}' did not begin with 'refs/remotes'")]
NotRemotes(RefString),
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
}
diff --git a/crates/radicle-surf/src/commit.rs b/crates/radicle-surf/src/commit.rs
index c2ee708bd..6c2225279 100644
--- a/crates/radicle-surf/src/commit.rs
+++ b/crates/radicle-surf/src/commit.rs
@@ -1,10 +1,10 @@
use std::{convert::TryFrom, str};
-use radicle_git_ext::Oid;
+use radicle_oid::Oid;
use thiserror::Error;
#[cfg(feature = "serde")]
-use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct};
#[derive(Debug, Error)]
pub enum Error {
diff --git a/crates/radicle-surf/src/diff.rs b/crates/radicle-surf/src/diff.rs
index f0043aed0..071180942 100644
--- a/crates/radicle-surf/src/diff.rs
+++ b/crates/radicle-surf/src/diff.rs
@@ -8,9 +8,9 @@ use std::{
};
#[cfg(feature = "serde")]
-use serde::{ser, ser::SerializeStruct, Serialize, Serializer};
+use serde::{Serialize, Serializer, ser, ser::SerializeStruct};
-use git_ext::Oid;
+use radicle_oid::Oid;
pub mod git;
@@ -257,20 +257,15 @@ impl Serialize for Copied {
}
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum EofNewLine {
OldMissing,
NewMissing,
BothMissing,
+ #[default]
NoneMissing,
}
-impl Default for EofNewLine {
- fn default() -> Self {
- Self::NoneMissing
- }
-}
-
/// A file that was modified within a [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
diff --git a/crates/radicle-surf/src/error.rs b/crates/radicle-surf/src/error.rs
index 1a790dc17..fbe887f47 100644
--- a/crates/radicle-surf/src/error.rs
+++ b/crates/radicle-surf/src/error.rs
@@ -27,7 +27,7 @@ pub enum Error {
#[error(transparent)]
Namespace(#[from] namespace::Error),
#[error(transparent)]
- RefFormat(#[from] git_ext::ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Revision(Box<dyn std::error::Error + Send + Sync + 'static>),
#[error(transparent)]
@@ -36,4 +36,6 @@ pub enum Error {
Tags(#[from] refs::error::Tag),
#[error(transparent)]
Repo(#[from] repo::error::Repo),
+ #[error(transparent)]
+ Oid(#[from] radicle_oid::str::error::ParseOidError),
}
diff --git a/crates/radicle-surf/src/fs.rs b/crates/radicle-surf/src/fs.rs
index 2cbdf8f67..cf7b16f6a 100644
--- a/crates/radicle-surf/src/fs.rs
+++ b/crates/radicle-surf/src/fs.rs
@@ -11,8 +11,7 @@ use std::{
};
use git2::Blob;
-use radicle_git_ext::{is_not_found_err, Oid};
-use radicle_std_ext::result::ResultExt as _;
+use radicle_oid::Oid;
use url::Url;
use crate::{Repository, Revision};
@@ -186,9 +185,9 @@ impl Iterator for Entries {
fn next(&mut self) -> Option<Self::Item> {
// Can be improved when `pop_first()` is stable for BTreeMap.
- let next_key = match self.listing.keys().next() {
- Some(k) => k.clone(),
- None => return None,
+ let next_key = {
+ let k = self.listing.keys().next()?;
+ k.clone()
};
self.listing.remove(&next_key)
}
@@ -228,7 +227,7 @@ impl Ord for Entry {
}
impl Entry {
- /// Get a label for the `Entriess`, either the name of the [`File`],
+ /// Get a label for the `Entries`, either the name of the [`File`],
/// the name of the [`Directory`], or the name of the [`Submodule`].
pub fn name(&self) -> &String {
match self {
@@ -300,7 +299,7 @@ impl Entry {
/// [git-tree]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Directory {
- /// The name of the directoy.
+ /// The name of the directory.
name: String,
/// The relative path of the directory, not including the `name`,
/// in respect to the root of the git repository.
@@ -414,11 +413,13 @@ impl Directory {
// Search the path in git2 tree.
let path = path.as_ref();
let git2_tree = repo.find_tree(self.id)?;
- let entry = git2_tree
- .get_path(path)
- .or_matches::<error::Directory, _, _>(is_not_found_err, || {
- Err(error::Directory::PathNotFound(path.to_path_buf()))
- })?;
+ let entry = git2_tree.get_path(path).map_err(|err| {
+ if err.code() == git2::ErrorCode::NotFound {
+ error::Directory::PathNotFound(path.to_path_buf())
+ } else {
+ err.into()
+ }
+ })?;
let parent = path
.parent()
.ok_or_else(|| error::Directory::InvalidPath(path.to_path_buf()))?;
diff --git a/crates/radicle-surf/src/glob.rs b/crates/radicle-surf/src/glob.rs
index c5b0eb632..5a4b8ad4a 100644
--- a/crates/radicle-surf/src/glob.rs
+++ b/crates/radicle-surf/src/glob.rs
@@ -1,9 +1,8 @@
use std::marker::PhantomData;
-use git_ext::ref_format::{
- self, refname,
- refspec::{self, PatternString, QualifiedPattern},
- Qualified, RefStr, RefString,
+use radicle_git_ref_format::{
+ self, Qualified, RefStr, RefString, pattern, refname,
+ refspec::{PatternString, QualifiedPattern},
};
use thiserror::Error;
@@ -12,7 +11,7 @@ use crate::{Branch, Local, Namespace, Remote, Tag};
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
}
/// A collection of globs for a git reference type.
@@ -51,7 +50,7 @@ impl<T> Glob<T> {
impl Glob<Namespace> {
/// Creates the `Glob` that matches all `refs/namespaces`.
pub fn all_namespaces() -> Self {
- Self::namespaces(refspec::pattern!("*"))
+ Self::namespaces(pattern!("*"))
}
/// Creates a `Glob` for `refs/namespaces`, starting with `glob`.
@@ -101,7 +100,7 @@ impl Extend<PatternString> for Glob<Namespace> {
impl Glob<Tag> {
/// Creates a `Glob` that matches all `refs/tags`.
pub fn all_tags() -> Self {
- Self::tags(refspec::pattern!("*"))
+ Self::tags(pattern!("*"))
}
/// Creates a `Glob` for `refs/tags`, starting with `glob`.
@@ -150,7 +149,7 @@ impl Extend<PatternString> for Glob<Tag> {
impl Glob<Local> {
/// Creates the `Glob` that matches all `refs/heads`.
pub fn all_heads() -> Self {
- Self::heads(refspec::pattern!("*"))
+ Self::heads(pattern!("*"))
}
/// Creates a `Glob` for `refs/heads`, starting with `glob`.
@@ -224,7 +223,7 @@ impl From<Glob<Local>> for Glob<Branch> {
impl Glob<Remote> {
/// Creates the `Glob` that matches all `refs/remotes`.
pub fn all_remotes() -> Self {
- Self::remotes(refspec::pattern!("*"))
+ Self::remotes(pattern!("*"))
}
/// Creates a `Glob` for `refs/remotes`, starting with `glob`.
@@ -298,7 +297,7 @@ impl From<Glob<Remote>> for Glob<Branch> {
impl Glob<Qualified<'_>> {
pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
Self {
- globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
+ globs: vec![Self::qualify_category(category, pattern!("*"))],
glob_type: PhantomData,
}
}
diff --git a/crates/radicle-surf/src/lib.rs b/crates/radicle-surf/src/lib.rs
index c577b0651..33472c979 100644
--- a/crates/radicle-surf/src/lib.rs
+++ b/crates/radicle-surf/src/lib.rs
@@ -15,13 +15,11 @@
//!
//! [serde]: https://crates.io/crates/serde
-extern crate radicle_git_ext as git_ext;
-
/// Re-exports.
-pub use radicle_git_ext::ref_format;
+pub use radicle_git_ref_format;
/// Represents an object id in Git. Re-exported from `radicle-git-ext`.
-pub type Oid = radicle_git_ext::Oid;
+pub type Oid = radicle_oid::Oid;
pub mod blob;
pub mod diff;
@@ -60,3 +58,6 @@ mod refs;
mod error;
pub use error::Error;
+
+#[cfg(test)]
+pub mod test;
diff --git a/crates/radicle-surf/src/namespace.rs b/crates/radicle-surf/src/namespace.rs
index 590ff03f1..364e42542 100644
--- a/crates/radicle-surf/src/namespace.rs
+++ b/crates/radicle-surf/src/namespace.rs
@@ -4,12 +4,11 @@ use std::{
str::{self, FromStr},
};
-use git_ext::ref_format::{
- self,
+use nonempty::NonEmpty;
+use radicle_git_ref_format::{
+ self, Component, Namespaced, Qualified, RefStr, RefString,
refspec::{NamespacedPattern, PatternString, QualifiedPattern},
- Component, Namespaced, Qualified, RefStr, RefString,
};
-use nonempty::NonEmpty;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -19,7 +18,7 @@ pub enum Error {
#[error("namespaces must not be empty")]
EmptyNamespace,
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
diff --git a/crates/radicle-surf/src/refs.rs b/crates/radicle-surf/src/refs.rs
index e0b9d71c4..3d94398fd 100644
--- a/crates/radicle-surf/src/refs.rs
+++ b/crates/radicle-surf/src/refs.rs
@@ -2,13 +2,13 @@
// using Generic associated types supported in Rust 1.65.0.
use std::{
- collections::{btree_set, BTreeSet},
+ collections::{BTreeSet, btree_set},
convert::TryFrom as _,
};
-use git_ext::ref_format::{self, lit, name::Components, Component, Qualified, RefString};
+use radicle_git_ref_format::{self, Component, Qualified, RefString, lit, name::Components};
-use crate::{tag, Branch, Namespace, Tag};
+use crate::{Branch, Namespace, Tag, tag};
/// Iterator over [`Tag`]s.
#[derive(Default)]
@@ -66,7 +66,7 @@ impl Iterator for TagNames<'_> {
tag::reference_name(&r)
.map(|name| lit::refs_tags(name).into())
.map_err(error::Tag::from)
- }))
+ }));
}
None => self.inner.current += 1,
},
@@ -110,7 +110,7 @@ impl Iterator for Branches<'_> {
return Some(
res.map_err(error::Branch::from)
.and_then(|r| Branch::try_from(&r).map_err(error::Branch::from)),
- )
+ );
}
None => self.current += 1,
},
@@ -133,7 +133,7 @@ impl Iterator for BranchNames<'_> {
Branch::try_from(&r)
.map(|branch| branch.refname().into_owned())
.map_err(error::Branch::from)
- }))
+ }));
}
None => self.inner.current += 1,
},
@@ -187,7 +187,7 @@ impl Iterator for Categories<'_> {
Some(res) => {
return Some(res.map_err(error::Category::from).and_then(|r| {
let name = std::str::from_utf8(r.name_bytes())?;
- let name = ref_format::RefStr::try_from_str(name)?;
+ let name = radicle_git_ref_format::RefStr::try_from_str(name)?;
let name = name.qualified().ok_or_else(|| {
error::Category::NotQualified(name.to_ref_string())
})?;
@@ -207,7 +207,7 @@ impl Iterator for Categories<'_> {
pub mod error {
use std::str;
- use radicle_git_ext::ref_format::{self, RefString};
+ use radicle_git_ref_format::{self, RefString};
use thiserror::Error;
use crate::{branch, tag};
@@ -227,7 +227,7 @@ pub mod error {
#[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
NotQualified(RefString),
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
diff --git a/crates/radicle-surf/src/repo.rs b/crates/radicle-surf/src/repo.rs
index 162d24876..cf52cb821 100644
--- a/crates/radicle-surf/src/repo.rs
+++ b/crates/radicle-surf/src/repo.rs
@@ -5,18 +5,16 @@ use std::{
str,
};
-use git_ext::{
- ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString},
- Oid,
-};
+use radicle_git_ref_format::{Qualified, RefStr, RefString, refspec::QualifiedPattern};
+use radicle_oid::Oid;
use crate::{
+ Branch, Commit, Error, Glob, History, Namespace, Revision, Signature, Stats, Tag, ToCommit,
blob::{Blob, BlobRef},
diff::{Diff, FileDiff},
fs::{Directory, File, FileContent},
refs::{BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
tree::{Entry, Tree},
- Branch, Commit, Error, Glob, History, Namespace, Revision, Signature, Stats, Tag, ToCommit,
};
/// Enumeration of errors that can occur in repo operations.
@@ -373,7 +371,7 @@ impl Repository {
.to_commit(self)
.map_err(|e| Error::ToCommit(e.into()))?;
- match self.inner.extract_signature(&commit.id, field) {
+ match self.inner.extract_signature(&commit.id.into(), field) {
Err(error) => {
if error.code() == git2::ErrorCode::NotFound {
Ok(None)
diff --git a/crates/radicle-surf/src/revision.rs b/crates/radicle-surf/src/revision.rs
index 4e34f63f9..b9ab8b4a1 100644
--- a/crates/radicle-surf/src/revision.rs
+++ b/crates/radicle-surf/src/revision.rs
@@ -1,9 +1,7 @@
use std::convert::Infallible;
-use git_ext::{
- ref_format::{Qualified, RefString},
- Oid,
-};
+use radicle_git_ref_format::{Qualified, RefString};
+use radicle_oid::Oid;
use crate::{Branch, Commit, Error, Repository, Tag};
@@ -50,10 +48,13 @@ impl Revision for Oid {
}
impl Revision for &str {
- type Error = git2::Error;
+ type Error = radicle_oid::str::error::ParseOidError;
- fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
- Oid::from_str_ext(self, repo.object_format())
+ fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+ use std::str::FromStr as _;
+
+ // TODO: Support for SHA-256.
+ Oid::from_str(self)
}
}
@@ -75,10 +76,10 @@ impl Revision for Tag {
}
impl Revision for String {
- type Error = git2::Error;
+ type Error = radicle_oid::str::error::ParseOidError;
fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
- Oid::from_str_ext(self, repo.object_format())
+ self.as_str().object_id(repo)
}
}
diff --git a/crates/radicle-surf/src/tag.rs b/crates/radicle-surf/src/tag.rs
index daad8b729..ed1f54e09 100644
--- a/crates/radicle-surf/src/tag.rs
+++ b/crates/radicle-surf/src/tag.rs
@@ -1,11 +1,9 @@
use std::{convert::TryFrom, str};
-use git_ext::{
- ref_format::{component, lit, Qualified, RefStr, RefString},
- Oid,
-};
+use radicle_git_ref_format::{Qualified, RefStr, RefString, lit, name::component};
+use radicle_oid::Oid;
-use crate::{refs::refstr_join, Author};
+use crate::{Author, refs::refstr_join};
/// The metadata of a [`Git tag`][git-tag].
///
@@ -62,13 +60,13 @@ impl Tag {
pub mod error {
use std::str;
- use radicle_git_ext::ref_format::{self, RefString};
+ use radicle_git_ref_format::{self, RefString};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FromTag {
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
@@ -84,7 +82,7 @@ pub mod error {
#[error("the refname '{0}' did not begin with 'refs/tags'")]
NotTag(RefString),
#[error(transparent)]
- RefFormat(#[from] ref_format::Error),
+ RefFormat(#[from] radicle_git_ref_format::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
diff --git a/crates/radicle-surf/src/test/branch.rs b/crates/radicle-surf/src/test/branch.rs
index ae4e62d4f..ef8322e11 100644
--- a/crates/radicle-surf/src/test/branch.rs
+++ b/crates/radicle-surf/src/test/branch.rs
@@ -1,20 +1,19 @@
+use super::r#gen;
+use crate::Branch;
use proptest::prelude::*;
-use radicle_git_ext::ref_format::{RefStr, RefString};
-use radicle_git_ext_test::git_ref_format::gen;
-use radicle_surf::Branch;
-use test_helpers::roundtrip;
+use radicle_git_ref_format::{RefStr, RefString};
proptest! {
#[test]
fn prop_test_branch(branch in gen_branch()) {
- roundtrip::json(branch)
+ super::roundtrip::json(branch)
}
}
fn gen_branch() -> impl Strategy<Value = Branch> {
prop_oneof![
- gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
- (gen::valid(), gen::valid()).prop_map(|(remote, name): (String, String)| {
+ r#gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
+ (r#gen::valid(), r#gen::valid()).prop_map(|(remote, name): (String, String)| {
let remote =
RefStr::try_from_str(&remote).expect("BUG: reference strings should be valid");
let name = RefStr::try_from_str(&name).expect("BUG: reference strings should be valid");
diff --git a/crates/radicle-surf/src/test/code_browsing.rs b/crates/radicle-surf/src/test/code_browsing.rs
index 3cafcde3c..8760eb9e5 100644
--- a/crates/radicle-surf/src/test/code_browsing.rs
+++ b/crates/radicle-surf/src/test/code_browsing.rs
@@ -1,16 +1,16 @@
use std::path::Path;
-use radicle_git_ext::ref_format::refname;
-use radicle_surf::{
- fs::{self, Directory},
+use crate::{
Branch, Repository,
+ fs::{self, Directory},
};
+use radicle_git_ref_format::refname;
-use super::GIT_PLATINUM;
+use super::platinum;
#[test]
fn iterate_root_dir_recursive() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let root_dir = repo.root_dir(Branch::local(refname!("master"))).unwrap();
let count = println_dir(&root_dir, &repo);
@@ -41,7 +41,7 @@ fn iterate_root_dir_recursive() {
#[test]
fn browse_repo_lazily() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let root_dir = repo.root_dir(Branch::local(refname!("master"))).unwrap();
let count = root_dir.entries(&repo).unwrap().entries().count();
@@ -57,7 +57,7 @@ fn browse_repo_lazily() {
#[test]
fn test_file_history() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let history = repo.history(Branch::local(refname!("dev"))).unwrap();
let path = Path::new("README.md");
let mut file_history = history.by_path(&path);
@@ -68,7 +68,7 @@ fn test_file_history() {
#[test]
fn test_commit_history() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let head = "a0dd9122d33dff2a35f564d564db127152c88e02";
// verify `&str` works.
@@ -83,7 +83,7 @@ fn test_commit_history() {
#[test]
fn test_commit_signature() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let commit_with_signature = "e24124b7538658220b5aaf3b6ef53758f0a106dc";
let signature = repo.extract_signature(commit_with_signature, None).unwrap();
assert!(signature.is_some());
diff --git a/crates/radicle-surf/src/test/commit.rs b/crates/radicle-surf/src/test/commit.rs
index b0a722264..073cc24e7 100644
--- a/crates/radicle-surf/src/test/commit.rs
+++ b/crates/radicle-surf/src/test/commit.rs
@@ -1,14 +1,13 @@
use std::str::FromStr;
+use crate::{Author, Commit, Time};
use proptest::prelude::*;
-use radicle_git_ext::Oid;
-use radicle_surf::{Author, Commit, Time};
-use test_helpers::roundtrip;
+use radicle_oid::Oid;
proptest! {
#[test]
fn prop_test_commits(commit in commits_strategy()) {
- roundtrip::json(commit)
+ super::roundtrip::json(commit)
}
}
diff --git a/crates/radicle-surf/src/test/diff.rs b/crates/radicle-surf/src/test/diff.rs
index 292df46f3..263c8e3c5 100644
--- a/crates/radicle-surf/src/test/diff.rs
+++ b/crates/radicle-surf/src/test/diff.rs
@@ -1,19 +1,21 @@
-use pretty_assertions::assert_eq;
-use radicle_git_ext::{ref_format::refname, Oid};
-use radicle_surf::{
+use std::{path::Path, str::FromStr};
+
+use radicle_git_ref_format::refname;
+use radicle_oid::Oid;
+
+use crate::{
+ Branch, Error, Repository,
diff::{
Added, Diff, DiffContent, DiffFile, EofNewLine, FileDiff, FileMode, FileStats, Hunk, Line,
Modification, Modified, Stats,
},
- Branch, Error, Repository,
};
-use std::{path::Path, str::FromStr};
-use super::GIT_PLATINUM;
+use super::platinum;
#[test]
fn test_initial_diff() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
let commit = repo.commit(oid).unwrap();
assert!(commit.parents.is_empty());
@@ -62,7 +64,7 @@ fn test_initial_diff() -> Result<(), Error> {
#[test]
fn test_diff_of_rev() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let diff = repo.diff_commit("80bacafba303bf0cdf6142921f430ff265f25095")?;
assert_eq!(diff.files().count(), 1);
Ok(())
@@ -70,7 +72,7 @@ fn test_diff_of_rev() -> Result<(), Error> {
#[test]
fn test_diff_file() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let path_buf = Path::new("README.md").to_path_buf();
let diff = repo.diff_file(
&path_buf,
@@ -113,7 +115,7 @@ fn test_diff_file() -> Result<(), Error> {
#[test]
fn test_diff() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let oid = "80bacafba303bf0cdf6142921f430ff265f25095";
let commit = repo.commit(oid).unwrap();
let parent_oid = commit.parents.first().unwrap();
@@ -163,7 +165,7 @@ fn test_diff() -> Result<(), Error> {
#[test]
fn test_branch_diff() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let rev_from = Branch::local(refname!("master"));
let rev_to = Branch::local(refname!("dev"));
let diff = repo.diff(&rev_from, &rev_to)?;
@@ -181,16 +183,16 @@ fn test_branch_diff() -> Result<(), Error> {
assert_eq!(diff.moved().count(), 1);
assert_eq!(diff.modified().count(), 2);
for c in diff.added() {
- println!("added: {:?}", &c.path);
+ println!("added: {:?}", c.path);
}
for d in diff.deleted() {
- println!("deleted: {:?}", &d.path);
+ println!("deleted: {:?}", d.path);
}
for m in diff.moved() {
- println!("moved: {:?} -> {:?}", &m.old_path, &m.new_path);
+ println!("moved: {:?} -> {:?}", m.old_path, m.new_path);
}
for m in diff.modified() {
- println!("modified: {:?}", &m.path);
+ println!("modified: {:?}", m.path);
}
// Verify moved.
@@ -198,11 +200,11 @@ fn test_branch_diff() -> Result<(), Error> {
// We can find a `FileDiff` for the old_path in a move.
let file_diff = repo.diff_file(&diff_moved.old_path, &rev_from, &rev_to)?;
- println!("old path file diff: {:?}", &file_diff);
+ println!("old path file diff: {:?}", file_diff);
// We can find a `FileDiff` for the new_path in a move.
let file_diff = repo.diff_file(&diff_moved.new_path, &rev_from, &rev_to)?;
- println!("new path file diff: {:?}", &file_diff);
+ println!("new path file diff: {:?}", file_diff);
// We can find a `FileDiff` if given a directory name.
let dir_diff = repo.diff_file(&"special/", &rev_from, &rev_to)?;
@@ -211,9 +213,10 @@ fn test_branch_diff() -> Result<(), Error> {
Ok(())
}
+#[cfg(feature = "serde")]
#[test]
fn test_diff_serde() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let rev_from = Branch::local(refname!("master"));
let rev_to = Branch::local(refname!("diff-test"));
let diff = repo.diff(rev_from, rev_to)?;
@@ -380,6 +383,7 @@ fn test_diff_serde() -> Result<(), Error> {
Ok(())
}
+#[cfg(feature = "serde")]
#[test]
fn test_rename_with_changes() {
let buf = r"
diff --git a/crates/radicle-surf/src/test/file_system.rs b/crates/radicle-surf/src/test/file_system.rs
index bf6150599..5368bdc17 100644
--- a/crates/radicle-surf/src/test/file_system.rs
+++ b/crates/radicle-surf/src/test/file_system.rs
@@ -1,18 +1,16 @@
-//! Unit tests for radicle_surf::file_system
-
-mod directory {
- use radicle_git_ext::ref_format::refname;
- use radicle_surf::{
- fs::{self, Entry},
+pub mod directory {
+ use crate::{
Branch, Oid, Repository,
+ fs::{self, Entry},
+ test::platinum,
};
+ use git2::RepositoryInitOptions;
+ use radicle_git_ref_format::refname;
use std::path::Path;
- const GIT_PLATINUM: &str = "../data/git-platinum";
-
#[test]
fn directory_find_entry() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let root = repo.root_dir(Branch::local(refname!("master"))).unwrap();
// find_entry for a file.
@@ -53,7 +51,7 @@ mod directory {
#[test]
fn directory_find_file_and_directory() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
// Get the snapshot of the directory for a given commit.
let root = repo
.root_dir("80ded66281a4de2889cc07293a8f10947c6d57fe")
@@ -91,7 +89,7 @@ mod directory {
#[test]
fn directory_size() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let root = repo.root_dir(Branch::local(refname!("master"))).unwrap();
/*
@@ -112,7 +110,7 @@ mod directory {
#[test]
fn directory_last_commit() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let branch = Branch::local(refname!("dev"));
let root = repo.root_dir(&branch).unwrap();
let dir = root.find_directory(&"this/is", &repo).unwrap();
@@ -125,7 +123,7 @@ mod directory {
#[test]
fn file_last_commit() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let branch = Branch::local(refname!("master"));
let root = repo.root_dir(&branch).unwrap();
@@ -145,9 +143,11 @@ mod directory {
/// interpreted as glob patterns by git's pathspec, causing errors.
#[test]
fn directory_with_bracket_in_name() {
- let repo = test_helpers::tempdir::WithTmpDir::new(|path| {
- git2::Repository::init(path).map_err(std::io::Error::other)
- })
+ let path = tempfile::TempDir::new().unwrap();
+ let repo = git2::Repository::init_opts(
+ path.path().join("repo"),
+ RepositoryInitOptions::new().initial_head("master"),
+ )
.unwrap();
// Initialize the repo and create test structure:
diff --git a/crates/radicle-surf/src/test/gen.rs b/crates/radicle-surf/src/test/gen.rs
new file mode 100644
index 000000000..c43160186
--- /dev/null
+++ b/crates/radicle-surf/src/test/gen.rs
@@ -0,0 +1,22 @@
+pub(crate) mod commit;
+
+use proptest::prelude::*;
+
+/// Any unicode "word" is trivially a valid refname.
+#[cfg(feature = "serde")]
+pub fn trivial() -> impl Strategy<Value = String> {
+ "\\w+"
+}
+
+#[cfg(feature = "serde")]
+pub fn valid() -> impl Strategy<Value = String> {
+ prop::collection::vec(trivial(), 1..20).prop_map(|xs| xs.join("/"))
+}
+
+pub fn alphanumeric() -> impl Strategy<Value = String> {
+ "[a-zA-Z0-9_]+"
+}
+
+pub fn alpha() -> impl Strategy<Value = String> {
+ "[a-zA-Z]+"
+}
diff --git a/crates/radicle-surf/src/test/gen/commit.rs b/crates/radicle-surf/src/test/gen/commit.rs
new file mode 100644
index 000000000..96416ef7b
--- /dev/null
+++ b/crates/radicle-surf/src/test/gen/commit.rs
@@ -0,0 +1,102 @@
+use std::convert::Infallible;
+
+use proptest::strategy::Strategy;
+use radicle_git_metadata::commit::CommitData;
+
+mod author;
+mod headers;
+mod trailers;
+
+pub use author::author;
+pub use headers::headers;
+pub use trailers::trailers;
+
+use super::alphanumeric;
+
+pub fn commit() -> impl Strategy<Value = CommitData<TreeData, Infallible>> {
+ (
+ TreeData::r#gen(),
+ author(),
+ author(),
+ headers(),
+ alphanumeric(),
+ trailers(3),
+ )
+ .prop_map(|(tree, author, committer, headers, message, trailers)| {
+ CommitData::new(tree, vec![], author, committer, headers, message, trailers)
+ })
+}
+
+pub fn write_commits(
+ repo: &git2::Repository,
+ linear: Vec<CommitData<TreeData, Infallible>>,
+) -> Result<Vec<git2::Oid>, git2::Error> {
+ let mut parent = None;
+ let mut commits = Vec::new();
+ for commit in linear {
+ let commit = commit.map_tree(|tree| tree.write(repo))?;
+ let commit = match parent {
+ Some(parent) => commit
+ .map_parents::<git2::Oid, Infallible, _>(|_| Ok(parent))
+ .unwrap(),
+ None => commit
+ .map_parents::<git2::Oid, Infallible, _>(|_| unreachable!("no parents"))
+ .unwrap(),
+ };
+ let tree = repo.find_tree(*commit.tree())?;
+ let oid = repo.commit(
+ None,
+ &git2::Signature::try_from(commit.author()).unwrap(),
+ &git2::Signature::try_from(commit.committer()).unwrap(),
+ commit.message(),
+ &tree,
+ &[],
+ )?;
+ commits.push(oid);
+ parent = Some(oid);
+ }
+ Ok(commits)
+}
+
+#[derive(Clone, Debug)]
+pub enum TreeData {
+ Blob { name: String, data: String },
+ Tree { name: String, inner: Vec<TreeData> },
+}
+
+impl TreeData {
+ fn r#gen() -> impl Strategy<Value = Self> {
+ let leaf =
+ (alphanumeric(), alphanumeric()).prop_map(|(name, data)| Self::Blob { name, data });
+ leaf.prop_recursive(8, 16, 5, |inner| {
+ (proptest::collection::vec(inner, 1..5), alphanumeric())
+ .prop_map(|(inner, name)| Self::Tree { name, inner })
+ })
+ }
+
+ fn write(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error> {
+ let mut builder = repo.treebuilder(None)?;
+ self.write_(repo, &mut builder)?;
+ builder.write()
+ }
+
+ fn write_(
+ &self,
+ repo: &git2::Repository,
+ builder: &mut git2::TreeBuilder,
+ ) -> Result<git2::Oid, git2::Error> {
+ match self {
+ Self::Blob { name, data } => {
+ let oid = repo.blob(data.as_bytes())?;
+ builder.insert(name, oid, git2::FileMode::Blob.into())?;
+ }
+ Self::Tree { name, inner } => {
+ for data in inner {
+ let oid = data.write_(repo, builder)?;
+ builder.insert(name, oid, git2::FileMode::Tree.into())?;
+ }
+ }
+ }
+ builder.write()
+ }
+}
diff --git a/crates/radicle-surf/src/test/gen/commit/author.rs b/crates/radicle-surf/src/test/gen/commit/author.rs
new file mode 100644
index 000000000..fb38bd236
--- /dev/null
+++ b/crates/radicle-surf/src/test/gen/commit/author.rs
@@ -0,0 +1,19 @@
+use proptest::strategy::{Just, Strategy};
+use radicle_git_metadata::author::{Author, Time};
+
+use crate::test::r#gen;
+
+pub fn author() -> impl Strategy<Value = Author> {
+ r#gen::alphanumeric().prop_flat_map(move |name| {
+ (Just(name), r#gen::alphanumeric()).prop_flat_map(|(name, domain)| {
+ (Just(name), Just(domain), (0..1000i64)).prop_map(move |(name, domain, time)| {
+ let email = format!("{name}@{domain}");
+ Author {
+ name,
+ email,
+ time: Time::new(time, 0),
+ }
+ })
+ })
+ })
+}
diff --git a/crates/radicle-surf/src/test/gen/commit/headers.rs b/crates/radicle-surf/src/test/gen/commit/headers.rs
new file mode 100644
index 000000000..80ecfee4a
--- /dev/null
+++ b/crates/radicle-surf/src/test/gen/commit/headers.rs
@@ -0,0 +1,30 @@
+use proptest::{collection, prop_oneof, strategy::Strategy};
+use radicle_git_metadata::commit::headers::Headers;
+
+use crate::test::r#gen;
+
+pub fn headers() -> impl Strategy<Value = Headers> {
+ collection::vec(prop_oneof![header(), signature()], 0..5).prop_map(|hs| {
+ let mut headers = Headers::new();
+ for (k, v) in hs {
+ headers.push(&k, &v);
+ }
+ headers
+ })
+}
+
+fn header() -> impl Strategy<Value = (String, String)> {
+ (prop_oneof!["test", "foo", "foobar"], r#gen::alphanumeric())
+}
+
+pub fn signature() -> impl Strategy<Value = (String, String)> {
+ ("gpgsig", prop_oneof![pgp(), ssh()])
+}
+
+pub fn pgp() -> impl Strategy<Value = String> {
+ "-----BEGIN PGP SIGNATURE-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END PGP SIGNATURE-----"
+}
+
+pub fn ssh() -> impl Strategy<Value = String> {
+ "-----BEGIN SSH SIGNATURE-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END SSH SIGNATURE-----"
+}
diff --git a/crates/radicle-surf/src/test/gen/commit/trailers.rs b/crates/radicle-surf/src/test/gen/commit/trailers.rs
new file mode 100644
index 000000000..2881a729c
--- /dev/null
+++ b/crates/radicle-surf/src/test/gen/commit/trailers.rs
@@ -0,0 +1,18 @@
+use proptest::{collection, strategy::Strategy};
+use radicle_git_metadata::commit::trailers::{OwnedTrailer, Token, Trailer};
+
+use crate::test::r#gen;
+
+pub fn trailers(n: usize) -> impl Strategy<Value = Vec<OwnedTrailer>> {
+ collection::vec(trailer(), 0..n)
+}
+
+pub fn trailer() -> impl Strategy<Value = OwnedTrailer> {
+ (r#gen::alpha(), r#gen::alphanumeric()).prop_map(|(token, value)| {
+ Trailer {
+ token: Token::try_from(format!("X-{}", token).as_str()).unwrap(),
+ value: value.into(),
+ }
+ .to_owned()
+ })
+}
diff --git a/crates/radicle-surf/src/test/last_commit.rs b/crates/radicle-surf/src/test/last_commit.rs
index 009893ee9..30669c2d4 100644
--- a/crates/radicle-surf/src/test/last_commit.rs
+++ b/crates/radicle-surf/src/test/last_commit.rs
@@ -1,18 +1,19 @@
use std::{path::PathBuf, str::FromStr};
-use radicle_git_ext::{ref_format::refname, Oid};
-use radicle_surf::{Branch, Repository};
+use radicle_git_ref_format::refname;
+use radicle_oid::Oid;
-use super::GIT_PLATINUM;
+use crate::{Branch, Repository};
+
+use super::platinum;
#[test]
fn readme_missing_and_memory() {
- let repo = Repository::open(GIT_PLATINUM)
- .expect("Could not retrieve ./data/git-platinum as git repository");
+ let repo = Repository::open(platinum::get()).unwrap();
let oid =
Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
- // memory.rs is commited later so it should not exist here.
+ // memory.rs is committed later so it should not exist here.
let memory_last_commit_oid = repo
.last_commit(&"src/memory.rs", oid)
.expect("Failed to get last commit")
@@ -31,8 +32,7 @@ fn readme_missing_and_memory() {
#[test]
fn folder_svelte() {
- let repo = Repository::open(GIT_PLATINUM)
- .expect("Could not retrieve ./data/git-platinum as git repository");
+ let repo = Repository::open(platinum::get()).unwrap();
// Check that last commit is the actual last commit even if head commit differs.
let oid =
Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
@@ -49,8 +49,7 @@ fn folder_svelte() {
#[test]
fn nest_directory() {
- let repo = Repository::open(GIT_PLATINUM)
- .expect("Could not retrieve ./data/git-platinum as git repository");
+ let repo = Repository::open(platinum::get()).unwrap();
// Check that last commit is the actual last commit even if head commit differs.
let oid =
Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
@@ -68,8 +67,7 @@ fn nest_directory() {
#[test]
#[cfg(not(windows))]
fn can_get_last_commit_for_special_filenames() {
- let repo = Repository::open(GIT_PLATINUM)
- .expect("Could not retrieve ./data/git-platinum as git repository");
+ let repo = Repository::open(platinum::get()).unwrap();
// Check that last commit is the actual last commit even if head commit differs.
let oid =
@@ -92,8 +90,7 @@ fn can_get_last_commit_for_special_filenames() {
#[test]
fn root() {
- let repo = Repository::open(GIT_PLATINUM)
- .expect("Could not retrieve ./data/git-platinum as git repository");
+ let repo = Repository::open(platinum::get()).unwrap();
let rev = Branch::local(refname!("master"));
let root_last_commit_id = repo
.last_commit(&PathBuf::new(), rev)
@@ -110,10 +107,9 @@ fn root() {
#[test]
fn binary_file() {
- let repo = Repository::open(GIT_PLATINUM)
- .expect("Could not retrieve ./data/git-platinum as git repository");
+ let repo = Repository::open(platinum::get()).unwrap();
let history = repo.history(Branch::local(refname!("dev"))).unwrap();
let file_commit = history.by_path(&"bin/cat").next();
assert!(file_commit.is_some());
- println!("file commit: {:?}", &file_commit);
+ println!("file commit: {:?}", file_commit);
}
diff --git a/crates/radicle-surf/src/test/mod.rs b/crates/radicle-surf/src/test/mod.rs
new file mode 100644
index 000000000..f63186925
--- /dev/null
+++ b/crates/radicle-surf/src/test/mod.rs
@@ -0,0 +1,20 @@
+#[cfg(feature = "serde")]
+mod branch;
+mod code_browsing;
+#[cfg(feature = "serde")]
+mod commit;
+mod diff;
+mod file_system;
+mod r#gen;
+mod last_commit;
+mod namespace;
+mod platinum;
+mod reference;
+mod repository;
+mod rev;
+#[cfg(feature = "serde")]
+mod roundtrip;
+#[cfg(feature = "serde")]
+mod source;
+mod submodule;
+mod threading;
diff --git a/crates/radicle-surf/src/test/namespace.rs b/crates/radicle-surf/src/test/namespace.rs
index 0fe7b74c1..0e8a3dc31 100644
--- a/crates/radicle-surf/src/test/namespace.rs
+++ b/crates/radicle-surf/src/test/namespace.rs
@@ -1,12 +1,12 @@
-use pretty_assertions::{assert_eq, assert_ne};
-use radicle_git_ext::ref_format::{name::component, refname, refspec};
-use radicle_surf::{Branch, Error, Glob, Repository};
+use radicle_git_ref_format::{component, pattern, refname};
-use super::GIT_PLATINUM;
+use crate::{Branch, Error, Glob, Repository};
+
+use super::platinum;
#[test]
fn switch_to_banana() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let history_master = repo.history(Branch::local(refname!("master")))?;
repo.switch_namespace(&refname!("golden"))?;
let history_banana = repo.history(Branch::local(refname!("banana")))?;
@@ -18,7 +18,7 @@ fn switch_to_banana() -> Result<(), Error> {
#[test]
fn me_namespace() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let history = repo.history(Branch::local(refname!("master")))?;
assert_eq!(repo.which_namespace().unwrap(), None);
@@ -42,7 +42,7 @@ fn me_namespace() -> Result<(), Error> {
refname!("heads/feature/#1194"),
)];
let mut branches = repo
- .branches(Glob::remotes(refspec::pattern!("fein/*")))?
+ .branches(Glob::remotes(pattern!("fein/*")))?
.collect::<Result<Vec<_>, _>>()?;
branches.sort();
@@ -53,7 +53,7 @@ fn me_namespace() -> Result<(), Error> {
#[test]
fn golden_namespace() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let history = repo.history(Branch::local(refname!("master")))?;
assert_eq!(repo.which_namespace().unwrap(), None);
@@ -85,7 +85,7 @@ fn golden_namespace() -> Result<(), Error> {
Branch::remote(remote, refname!("tags/v0.1.0")),
];
let mut branches = repo
- .branches(Glob::remotes(refspec::pattern!("kickflip/*")))?
+ .branches(Glob::remotes(pattern!("kickflip/*")))?
.collect::<Result<Vec<_>, _>>()?;
branches.sort();
@@ -96,7 +96,7 @@ fn golden_namespace() -> Result<(), Error> {
#[test]
fn silver_namespace() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let history = repo.history(Branch::local(refname!("master")))?;
assert_eq!(repo.which_namespace().unwrap(), None);
diff --git a/crates/radicle-surf/src/test/platinum.rs b/crates/radicle-surf/src/test/platinum.rs
new file mode 100644
index 000000000..73144f12a
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum.rs
@@ -0,0 +1,708 @@
+use std::{io::Read as _, mem, path::Path, str, sync::LazyLock};
+
+use flate2::read::GzDecoder;
+use git2::{Oid, Repository, Signature, Time, build::TreeUpdateBuilder};
+use hex_literal::hex;
+use tempfile::TempDir;
+
+static LOCK: LazyLock<tempfile::TempDir> = LazyLock::new(init);
+
+pub(crate) fn get() -> &'static Path {
+ LOCK.path()
+}
+
+macro_rules! oid {
+ ($bytes:expr) => {
+ radicle_oid::Oid::Sha1(hex!($bytes))
+ };
+}
+
+#[rustfmt::skip]
+mod author {
+ use super::*;
+
+ pub(super) struct Author {
+ name: &'static str,
+ email: &'static str,
+ }
+
+ impl Author {
+ const fn new(name: &'static str, email: &'static str) -> Self {
+ Self { name, email }
+ }
+
+ pub(super) fn signature(&self, time: &Time) -> git2::Signature<'static> {
+ Signature::new(self.name, self.email, time).unwrap()
+ }
+ }
+
+ pub(super) const ALEXANDER: Author = Author::new("Alexander Simmerl", "a.simmerl@gmail.com" );
+ pub(super) const FINTAN_1: Author = Author::new("FintanH", "fintan.halpenny@gmail.com" );
+ pub(super) const FINTAN_2: Author = Author::new("Fintan Halpenny", "fintan.halpenny@gmail.com" );
+ pub(super) const GITHUB: Author = Author::new("GitHub", "noreply@github.com" );
+ const HAN: Author = Author::new("Han Xu", "keepsimple@gmail.com" );
+ pub(super) const RUDOLFS: Author = Author::new("Rūdolfs Ošiņš", "rudolfs@osins.org" );
+ pub(super) const SEBASTIAN: Author = Author::new("Sebastian Martinez", "me@sebastinez.dev");
+ pub(super) const THOMAS: Author = Author::new("Thomas Scholtes", "thomas@monadic.xyz" );
+
+ pub(super) fn rudolfs(seconds: i64) -> git2::Signature<'static> {
+ RUDOLFS.signature(&Time::new(seconds, 60))
+ }
+
+ pub(super) fn han(seconds: i64) -> git2::Signature<'static> {
+ HAN.signature(&Time::new(seconds, -480))
+ }
+}
+
+use author::*;
+
+/// Git blobs, represented by their contents and expected object ID, i.e.,
+/// expected SHA-1 digest of the contents.
+///
+/// [`Self::write`] asserts that the actual object ID computed matches
+/// [`Blob::oid`], which helps to catch errors in construction/loading of
+/// contents early.
+struct Blob<'a> {
+ oid: radicle_oid::Oid,
+ contents: &'a [u8],
+}
+
+impl<'a> Blob<'a> {
+ const EMPTY: Blob<'static> = Blob {
+ oid: oid!("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
+ contents: &[],
+ };
+
+ /// Writes this blob to the object database of `repo`, and asserts that
+ /// the returned object ID matches [`Self::oid`].
+ ///
+ /// To obtain a handle to the blob in the object database of `repo`,
+ /// use [`Repository::find_blob`] after [`Self::write`] has returned.
+ fn write(&self, repo: &Repository) -> Oid {
+ let oid = repo.blob(self.contents).unwrap();
+ assert_eq!(radicle_oid::Oid::from(oid), self.oid);
+ oid
+ }
+
+ fn to_vec(&self) -> Vec<u8> {
+ self.contents.to_vec()
+ }
+}
+
+const README_1: Blob = Blob {
+ oid: oid!("7f48df0118b1674f4ab0ed1717c1368091a5dddc"),
+ contents: b"This repository is a data source for the Upstream front-end tests.\n",
+};
+
+const README_2: Blob = Blob {
+ oid: oid!("5e07534cd74a6a9b2ccd2729b181c4ef26173a5e"),
+ contents: b"This repository is a data source for the Upstream front-end tests and the\n[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
+};
+
+const README_3: Blob = Blob {
+ oid: oid!("b033ecf407a44922b28c942c696922a7d7daf06e"),
+ contents: b"This repository is a data source for the upstream front-end tests and the\n[`radicle-surf`](https://github.com/radicle-dev/radicle-surf) unit tests.\n",
+};
+
+const LICENSE: Blob = Blob {
+ oid: oid!("02f70f56ec62396ceaf38804c37e169e875ab291"),
+ contents: include_bytes!("platinum/LICENSE"),
+};
+
+const EVAL: Blob = Blob {
+ oid: oid!("8c7447d13b907aa994ac3a38317c1e9633bf0732"),
+ contents: include_bytes!("platinum/Eval.hs"),
+};
+
+const FOLDER: Blob = Blob {
+ oid: oid!("a50cdb374f3a16da6cc6056a5f2818b53efdb745"),
+ contents: include_bytes!("platinum/Folder.svelte"),
+};
+
+const MEMORY: Blob = Blob {
+ oid: oid!("b84992d24be67536837f5ab45a943f1b3f501878"),
+ contents: include_bytes!("platinum/memory.rs"),
+};
+
+const ARROWS: Blob = Blob {
+ oid: oid!("95418c04010a3cc758fb3a37f9918465f147566f"),
+ contents: include_bytes!("platinum/arrows.txt"),
+};
+
+const EMOJI: Blob = Blob {
+ oid: oid!("1570277532948712fea9029d100a4208f9e34241"),
+ contents: include_bytes!("platinum/emoji.txt"),
+};
+
+const GARDEN: Blob = Blob {
+ oid: oid!("859f93b3b6a687a961ea7dd54277ae12d93567bf"),
+ contents: include_bytes!("platinum/garden.txt"),
+};
+
+pub(super) fn init() -> TempDir {
+ use git2::FileMode::{Blob as Regular, BlobExecutable as Executable};
+
+ let dir = tempfile::tempdir().unwrap();
+ let repo = Repository::init(dir.path()).unwrap();
+ let empty_tree = repo.treebuilder(None).unwrap().write().unwrap();
+ let mut tree_oid = empty_tree;
+ let mut tree = TreeUpdateBuilder::new();
+
+ let empty = Blob::EMPTY.write(&repo);
+ let readme_initial = README_1.write(&repo);
+ let readme_updated = README_2.write(&repo);
+ let readme_diff_test = README_3.write(&repo);
+ let license = LICENSE.write(&repo);
+ let eval_initial = EVAL.write(&repo);
+ let folder = FOLDER.write(&repo);
+ let memory = MEMORY.write(&repo);
+ let arrows = ARROWS.write(&repo);
+ let emoji = EMOJI.write(&repo);
+ let garden = GARDEN.write(&repo);
+
+ // The second version of `Eval.hs` used in tests only differs from
+ // the first version by a difference of 9 bytes inserted. Instead
+ // of duplicating the file, we compute the second version from the first
+ // version by splicing 9 bytes.
+ let eval_updated = {
+ let mut contents = EVAL.to_vec();
+
+ const INSERT: &[u8; 9] = b", the MVP";
+
+ /// The index at which we need to insert [`INSERT`] into [`contents`].
+ ///
+ /// This was computed via a naive linear search:
+ /// ```no_run
+ /// const NEEDLE: &[u8; 16] = b", original, eval";
+ /// let index = contents
+ /// .array_windows()
+ /// .enumerate()
+ /// .find_map(|(i, window)| (window == NEEDLE).then_some(i))
+ /// .unwrap()
+ /// + NEEDLE.len();
+ /// ```
+ const INDEX: usize = 623;
+
+ contents.splice(INDEX..INDEX, INSERT.to_vec());
+
+ let oid = repo.blob(&contents).unwrap();
+ assert_eq!(
+ radicle_oid::Oid::from(oid),
+ oid!("7d6240123a8d8ea8a8376610168a0a4bcb96afd0")
+ );
+ oid
+ };
+
+ let ls = gunzip(
+ &repo,
+ oid!("87c2d5149737b266cfe35ca6fa8d2362048613ae"),
+ include_bytes!("platinum/ls.gz"),
+ );
+ let test = gunzip(
+ &repo,
+ oid!("0cced79cda4babfd382621200cb6fdc6470e3c47"),
+ include_bytes!("platinum/test.gz"),
+ );
+ let cat = gunzip(
+ &repo,
+ oid!("0bbb14b0f0d3b6e658879748a720ec601108d26d"),
+ include_bytes!("platinum/cat.gz"),
+ );
+
+ let initial = {
+ tree.upsert("README.md", readme_initial, Regular);
+
+ let sign = rudolfs(1_575_282_266);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[],
+ sign.clone(),
+ sign,
+ "Initial commit FTW!\n",
+ Some(include_str!("platinum/signatures/initial.sig")),
+ oid!("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3"),
+ )
+ };
+
+ for name in [
+ "refs/tags/v0.1.0",
+ "refs/namespaces/golden/refs/tags/v0.1.0",
+ "refs/namespaces/golden/refs/remotes/kickflip/tags/v0.1.0",
+ ] {
+ repo.reference(name, initial.into(), false, "").unwrap();
+ }
+
+ let nested = {
+ tree.upsert(
+ "this/is/a/really/deeply/nested/directory/tree/.gitkeep",
+ empty,
+ Regular,
+ );
+
+ let sign = rudolfs(1_575_282_370);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[initial],
+ sign.clone(),
+ sign,
+ "Add fixture for deeply nested directories\n",
+ Some(include_str!("platinum/signatures/nested.sig")),
+ oid!("2429f097664f9af0c5b7b389ab998b2199ffa977"),
+ )
+ };
+
+ for name in [
+ "refs/tags/v0.2.0",
+ "refs/namespaces/golden/refs/tags/v0.2.0",
+ ] {
+ repo.reference(name, nested.into(), false, "").unwrap();
+ }
+
+ let sources = {
+ tree.upsert("examples/Eval.hs", eval_initial, Regular);
+ tree.upsert("examples/Folder.svelte", folder, Regular);
+ tree.upsert("examples/memory.rs", memory, Regular);
+
+ let sign = rudolfs(1_575_282_874);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[nested],
+ sign.clone(),
+ sign,
+ "Add some source code example files\n",
+ Some(include_str!("platinum/signatures/sources.sig")),
+ oid!("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2"),
+ )
+ };
+ let binaries = {
+ tree.upsert("bin/cat", cat, Executable);
+ tree.upsert("bin/ls", ls, Executable);
+ tree.upsert("bin/test", test, Executable);
+
+ let sign = rudolfs(1_575_282_964);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[sources],
+ sign.clone(),
+ sign,
+ "Add some binary files\n",
+ Some(include_str!("platinum/signatures/binaries.sig")),
+ oid!("19bec071db6474af89c866a1bd0e4b1ff76e2b97"),
+ )
+ };
+
+ repo.reference("refs/tags/v0.3.0", binaries.into(), false, "")
+ .unwrap();
+
+ let moved = {
+ tree.remove("examples/Eval.hs");
+ tree.remove("examples/Folder.svelte");
+ tree.remove("examples/memory.rs");
+ tree.upsert("src/Eval.hs", eval_initial, Regular);
+ tree.upsert("src/Folder.svelte", folder, Regular);
+ tree.upsert("src/memory.rs", memory, Regular);
+
+ let sign = rudolfs(1_575_283_266);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[binaries],
+ sign.clone(),
+ sign,
+ "Move examples to \"src\"\n",
+ Some(include_str!("platinum/signatures/moved.sig")),
+ oid!("e24124b7538658220b5aaf3b6ef53758f0a106dc"),
+ )
+ };
+
+ let texts = {
+ tree.upsert("text/arrows.txt", arrows, Regular);
+ tree.upsert("text/emoji.txt", emoji, Regular);
+ tree.upsert("text/garden.txt", garden, Regular);
+
+ let sign = rudolfs(1_575_283_425);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[moved],
+ sign.clone(),
+ sign,
+ "Add text files\n",
+ Some(include_str!("platinum/signatures/texts.sig")),
+ oid!("1e0206da8571ca71c51c91154e2fee376e09b4e7"),
+ )
+ };
+
+ let common = {
+ tree.upsert(".i-am-well-hidden", empty, Regular);
+ tree.upsert(".i-too-am-hidden", empty, Regular);
+
+ let sign = rudolfs(1_575_283_503);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[texts],
+ sign.clone(),
+ sign,
+ "Add dotfiles\n",
+ Some(include_str!("platinum/signatures/common.sig")),
+ oid!("1820cb07c1a890016ca5578aa652fd4d4c38967e"),
+ )
+ };
+
+ let common_tree = tree_oid;
+ let dev = {
+ tree.upsert("here-we-are-on-a-dev-branch.lol", empty, Regular);
+ let sign = rudolfs(1_575_283_616);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[common],
+ sign.clone(),
+ sign,
+ "Commit on the dev branch\n",
+ Some(include_str!("platinum/signatures/dev.sig")),
+ oid!("27acd68c7504755aa11023300890bb85bbd69d45"),
+ )
+ };
+
+ for name in [
+ "refs/heads/dev",
+ "refs/namespaces/golden/refs/heads/banana",
+ "refs/namespaces/golden/refs/namespaces/silver/refs/heads/master",
+ "refs/namespaces/golden/refs/remotes/kickflip/heads/fakie/bigspin",
+ "refs/namespaces/golden/refs/remotes/kickflip/heads/heelflip",
+ "refs/namespaces/me/refs/remotes/fein/heads/feature/#1194",
+ "refs/remotes/origin/dev",
+ ] {
+ repo.reference(name, dev.into(), false, "").unwrap();
+ }
+
+ tree_oid = common_tree;
+
+ let deletion_added = {
+ tree.upsert("test-file-deletion.txt", empty, Regular);
+
+ let sign = rudolfs(1_575_468_360);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[common],
+ sign.clone(),
+ sign,
+ "Add file which will be deleted later\n",
+ Some(include_str!("platinum/signatures/deletion-added.sig")),
+ oid!("91b69e00cd8e5a07e20942e9e4457d83ce7a3ff1"),
+ )
+ };
+
+ repo.reference("refs/tags/v0.4.0", deletion_added.into(), false, "")
+ .unwrap();
+
+ let deletion_removed = {
+ tree.remove("test-file-deletion.txt");
+
+ let sign = rudolfs(1_575_468_397);
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[deletion_added],
+ sign.clone(),
+ sign,
+ "Delete unneeded file\n",
+ Some(include_str!("platinum/signatures/deletion-removed.sig")),
+ oid!("80ded66281a4de2889cc07293a8f10947c6d57fe"),
+ )
+ };
+
+ repo.reference("refs/tags/v0.5.0", deletion_removed.into(), false, "")
+ .unwrap();
+
+ let long_message = {
+ let sign = |author: Author| author.signature(&Time::new(1_576_170_713, 60));
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[deletion_removed],
+ sign(ALEXANDER),
+ sign(GITHUB),
+ "Add a long commit message to commit message body (#1)\n\nIn order to test the correct delivery of the message part of the commit\r\nwe add this commit which has both by expanding beyond the summary.",
+ Some(include_str!("platinum/signatures/long-message.sig")),
+ oid!("d6880352fc7fda8f521ae9b7357668b17bb5bad5"),
+ )
+ };
+
+ {
+ let target = repo.find_object(long_message.into(), None).unwrap();
+ let tagger = THOMAS.signature(&Time::new(1_620_740_737, 120));
+ let tag = repo
+ .tag(
+ "v0.6.0",
+ &target,
+ &tagger,
+ "An annotated tag message for v0.6.0\n",
+ false,
+ )
+ .unwrap();
+ assert_eq!(
+ radicle_oid::Oid::from(tag),
+ oid!("4d1f4af2703074d37cb877f4fdbe36322c8e541d")
+ );
+ }
+
+ let long_message_tree = tree_oid;
+ let readme = {
+ tree.upsert("README.md", readme_updated, Regular);
+
+ let sign = |seconds| FINTAN_1.signature(&Time::new(seconds, 0));
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[long_message],
+ sign(1_584_362_521),
+ sign(1_584_362_684),
+ "Updated README with radicle-surf link\n",
+ None,
+ oid!("80bacafba303bf0cdf6142921f430ff265f25095"),
+ )
+ };
+ tree_oid = long_message_tree;
+
+ let docs = {
+ tree.upsert("src/Eval.hs", eval_updated, Regular);
+
+ let sign = |author: Author| author.signature(&Time::new(1_578_309_972, 0));
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[long_message],
+ sign(FINTAN_2),
+ sign(GITHUB),
+ "Extend the docs (#2)\n\nI want to have files under src that have separate commits.\r\nThat way src's latest commit isn't the same as all its files, instead it's the file that was touched last.",
+ Some(include_str!("platinum/signatures/docs.sig")),
+ oid!("3873745c8f6ffb45c990eb23b491d4b4b6182f95"),
+ )
+ };
+
+ let folder_removed = {
+ tree.remove("src/Folder.svelte");
+
+ let sign = |author: Author| author.signature(&Time::new(1_582_198_877, 60));
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[docs],
+ sign(RUDOLFS),
+ sign(GITHUB),
+ "Remove src/Folder.svelte (#3)\n\nIt was a bad idea to have an actual source file which is used by\r\nradicle-upstream in the fixtures repository. It gets in the way of\r\nlinting and editors pick it up as a regular source file by accident.",
+ Some(include_str!("platinum/signatures/folder-removed.sig")),
+ oid!("a57846bbc8ced6587bf8329fc4bce970eb7b757e"),
+ )
+ };
+
+ let merged = {
+ tree.upsert("README.md", readme_updated, Regular);
+
+ let sign = |author: Author| author.signature(&Time::new(1_584_367_899, 60));
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[folder_removed, readme],
+ sign(ALEXANDER),
+ sign(GITHUB),
+ "Merge pull request #4 from FintanH/fintan/update-readme-no-sig\n\nUpdated README",
+ Some(include_str!("platinum/signatures/merged.sig")),
+ oid!("223aaf87d6ea62eef0014857640fd7c8dd0f80b5"),
+ )
+ };
+
+ let master = {
+ for path in [
+ "special/-dash-",
+ "special/...",
+ "special/:colon:",
+ "special/;semicolon;",
+ "special/@at@",
+ "special/_underscore_",
+ "special/c++",
+ "special/faux\\path",
+ "special/i need some space",
+ "special/qs?param1=value?param2=value2#hash",
+ "special/~tilde~",
+ "special/👹👹👹",
+ ] {
+ tree.upsert(path, empty, Regular);
+ }
+
+ let sign = |author: Author| author.signature(&Time::new(1_602_778_504, 120));
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[merged],
+ sign(RUDOLFS),
+ sign(GITHUB),
+ "Add files with special characters in their filenames (#5)\n\n",
+ Some(include_str!("platinum/signatures/master.sig")),
+ oid!("a0dd9122d33dff2a35f564d564db127152c88e02"),
+ )
+ };
+
+ for name in [
+ "refs/heads/master",
+ "refs/namespaces/golden/refs/heads/master",
+ "refs/namespaces/me/refs/heads/feature/#1194",
+ "refs/remotes/banana/orange/pineapple",
+ "refs/remotes/banana/pineapple",
+ "refs/remotes/origin/master",
+ ] {
+ repo.reference(name, master.into(), false, "").unwrap();
+ }
+
+ let master_tree = tree_oid;
+ tree_oid = empty_tree;
+
+ let empty_branch = {
+ let sign = han(1_674_618_111);
+ let removed_all = commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[master],
+ sign.clone(),
+ sign,
+ "remove all files\n",
+ None,
+ oid!("13a866a47591625c3fb7895e454369cd5874badc"),
+ );
+
+ let sign = han(1_674_618_130);
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[removed_all],
+ sign.clone(),
+ sign,
+ "an empty commit.\n",
+ None,
+ oid!("e972683fe8136bf8a5cb2378cf50303554008049"),
+ )
+ };
+
+ for name in [
+ "refs/heads/empty-branch",
+ "refs/remotes/origin/empty-branch",
+ ] {
+ repo.reference(name, empty_branch.into(), false, "")
+ .unwrap();
+ }
+
+ let diff_test = {
+ tree_oid = master_tree;
+ tree.upsert("LICENSE", license, Regular);
+ tree.upsert("README.md", readme_diff_test, Regular);
+ tree.remove("text/emoji.txt");
+ tree.upsert("emoji.txt", emoji, Regular);
+ tree.upsert("file_operations/copied.md", readme_updated, Regular);
+ tree.remove("text/arrows.txt");
+
+ commit(
+ &repo,
+ write_tree(&repo, &mut tree, &mut tree_oid),
+ &[master],
+ han(1_675_087_389),
+ SEBASTIAN.signature(&Time::new(1_687_938_471, 120)),
+ "One commit to include add, delete, modify, copy and move\n\nFor copies detection, we copy the README file which is also being\nmodified in the same changeset.\nDue to unmodified copies detection being to expensive.\n",
+ Some(include_str!("platinum/signatures/diff-test.sig")),
+ oid!("29b78a041bffb955b597719b27c51134a49555c1"),
+ )
+ };
+
+ for name in ["refs/heads/diff-test", "refs/remotes/origin/diff-test"] {
+ repo.reference(name, diff_test.into(), false, "").unwrap();
+ }
+
+ repo.reference_symbolic(
+ "refs/remotes/origin/HEAD",
+ "refs/remotes/origin/master",
+ false,
+ "create platinum fixture",
+ )
+ .unwrap();
+
+ repo.set_head("refs/heads/dev").unwrap();
+
+ dir
+}
+
+fn gunzip(repo: &Repository, expected: radicle_oid::Oid, bytes: &[u8]) -> Oid {
+ let mut decoded = Vec::new();
+ GzDecoder::new(bytes).read_to_end(&mut decoded).unwrap();
+ let oid = repo.blob(&decoded).unwrap();
+ assert_eq!(radicle_oid::Oid::from(oid), expected);
+ oid
+}
+
+fn write_tree(
+ repo: &Repository,
+ builder: &mut TreeUpdateBuilder,
+ baseline: &mut Oid,
+) -> radicle_oid::Oid {
+ let tree = repo.find_tree(*baseline).unwrap();
+ *baseline = mem::take(builder).create_updated(repo, &tree).unwrap();
+ (*baseline).into()
+}
+
+#[allow(clippy::too_many_arguments)]
+fn commit(
+ repo: &Repository,
+ tree: radicle_oid::Oid,
+ parents: &[radicle_oid::Oid],
+ author: Signature<'_>,
+ committer: Signature<'_>,
+ message: &str,
+ signature: Option<&str>,
+ expected: radicle_oid::Oid,
+) -> radicle_oid::Oid {
+ let tree = repo.find_tree(tree.into()).unwrap();
+ let parents = parents
+ .iter()
+ .map(|oid| repo.find_commit(oid.into()).unwrap())
+ .collect::<Vec<_>>();
+ let parent_refs = parents.iter().collect::<Vec<_>>();
+
+ let oid = match signature {
+ Some(signature) => {
+ let buffer = repo
+ .commit_create_buffer(&author, &committer, message, &tree, &parent_refs)
+ .unwrap();
+ let content = str::from_utf8(&buffer).unwrap();
+ repo.commit_signed(content, signature, None).unwrap()
+ }
+ None => repo
+ .commit(None, &author, &committer, message, &tree, &parent_refs)
+ .unwrap(),
+ };
+
+ let oid = oid.into();
+ assert_eq!(oid, expected);
+ oid
+}
diff --git a/crates/radicle-surf/src/test/platinum/Eval.hs b/crates/radicle-surf/src/test/platinum/Eval.hs
new file mode 100644
index 000000000..8c7447d13
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/Eval.hs
@@ -0,0 +1,253 @@
+module Radicle.Lang.Eval
+ ( eval
+ , baseEval
+ , callFn
+ , ($$)
+ , createModule
+ ) where
+
+import Protolude hiding (Constructor, Handle, TypeError, (<>))
+
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map as Map
+import Data.Semigroup ((<>))
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
+import qualified GHC.Exts as GhcExts
+
+import Radicle.Lang.Core
+import qualified Radicle.Lang.Doc as Doc
+import Radicle.Lang.Identifier (Ident(..))
+import Radicle.Lang.Internal.Orphans ()
+
+-- | The built-in, original, eval.
+baseEval :: Monad m => Value -> Lang m Value
+baseEval val = logValPos val $ case val of
+ Atom i -> lookupAtom i
+ List (f:vs) -> f $$ vs
+ List xs -> throwErrorHere
+ $ WrongNumberOfArgs ("application: " <> show xs)
+ 2
+ (length xs)
+ Vec xs -> Vec <$> traverse baseEval xs
+ Dict mp -> do
+ let evalBoth (a,b) = (,) <$> baseEval a <*> baseEval b
+ kvs <- traverse evalBoth (Map.toList mp)
+ dict $ Map.fromList kvs
+ autoquote -> pure autoquote
+
+
+-- | The buck-passing eval. Uses whatever 'eval' is in scope.
+eval :: Monad m => Value -> Lang m Value
+eval val = do
+ e <- lookupAtom (Ident "eval")
+ logValPos e $ do
+ st <- gets bindingsToRadicle
+ result <- callFn e [val, st]
+ case result of
+ List [val', newSt] -> do
+ setBindings newSt
+ pure val'
+ _ -> throwErrorHere
+ $ OtherError "eval: should return list with value and new env"
+
+
+
+specialForms :: forall m. (Monad m) => Map Ident ([Value] -> Lang m Value)
+specialForms = Map.fromList $ first Ident <$>
+ [ ( "fn"
+ , \case
+ args : b : bs -> do
+ e <- gets bindingsEnv
+ let toLambda argNames = pure (Lambda argNames (b :| bs) e)
+ case args of
+ Vec atoms_ -> do
+ atoms <- traverse isAtom (toList atoms_) ?? toLangError (SpecialForm "fn" "One of the arguments was not an atom")
+ toLambda (PosArgs atoms)
+ Atom name -> do
+ toLambda (VarArgs name)
+ _ -> throwErrorHere $ SpecialForm "fn" "Function parameters must be one atom or a vector of atoms"
+ _ -> throwErrorHere $ SpecialForm "fn" "Function needs parameters (one atom or a vector of atoms) and a body"
+ )
+ , ( "module", createModule )
+ , ("quote", \case
+ [v] -> pure v
+ xs -> throwErrorHere $ WrongNumberOfArgs "quote" 1 (length xs))
+ , ("def", \case
+ [Atom name, val] -> def name Nothing val
+ [_, _] -> throwErrorHere $ OtherError "def expects atom for first arg"
+ [Atom name, String d, val] -> def name (Just d) val
+ xs -> throwErrorHere $ WrongNumberOfArgs "def" 2 (length xs))
+ , ( "def-rec"
+ , \case
+ [Atom name, val] -> defRec name Nothing val
+ [_, _] -> throwErrorHere $ OtherError "def-rec expects atom for first arg"
+ [Atom name, String d, val] -> defRec name (Just d) val
+ xs -> throwErrorHere $ WrongNumberOfArgs "def-rec" 2 (length xs)
+ )
+ , ("do", (lastDef nil <$>) . traverse baseEval)
+ , ("catch", \case
+ [l, form, handler] -> do
+ mlabel <- baseEval l
+ s <- get
+ case mlabel of
+ -- TODO reify stack
+ Atom label -> baseEval form `catchError` \err@(LangError _stack e) -> do
+ (thrownLabel, thrownValue) <- errorDataToValue e
+ if thrownLabel == label || label == Ident "any"
+ then do
+ put s
+ handlerclo <- baseEval handler
+ callFn handlerclo [thrownValue]
+ else throwError err
+ _ -> throwErrorHere $ SpecialForm "catch" "first argument must be atom"
+ xs -> throwErrorHere $ WrongNumberOfArgs "catch" 3 (length xs))
+ , ("if", \case
+ [condition, t, f] -> do
+ b <- baseEval condition
+ -- I hate this as much as everyone that might ever read Haskell, but
+ -- in Lisps a lot of things that one might object to are True...
+ if b == Boolean False then baseEval f else baseEval t
+ xs -> throwErrorHere $ WrongNumberOfArgs "if" 3 (length xs))
+ , ( "cond", (cond =<<) . evenArgs "cond" )
+ , ( "match", match )
+ ]
+ where
+ cond = \case
+ [] -> pure nil
+ (c,e):ps -> do
+ b <- baseEval c
+ if b /= Boolean False
+ then baseEval e
+ else cond ps
+
+ match = \case
+ v : cases -> do
+ cs <- evenArgs "match" cases
+ v' <- baseEval v
+ goMatches v' cs
+ _ -> throwErrorHere $ PatternMatchError NoValue
+
+ goMatches _ [] = throwErrorHere (PatternMatchError NoMatch)
+ goMatches v ((m, body):cases) = do
+ patFn <- baseEval m
+ matchPat <- lookupAtom (Ident "match-pat")
+ res <- callFn matchPat [patFn, v]
+ let res_ = fromRad res
+ case res_ of
+ Right (Just (Dict binds)) -> do
+ b <- bindsToEnv m binds
+ addBinds b *> baseEval body
+ Right Nothing -> goMatches v cases
+ _ -> throwErrorHere $ PatternMatchError (BadBindings m)
+
+ bindsToEnv pat m = do
+ is <- traverse isBind (Map.toList m)
+ pure $ Env (Map.fromList is)
+ where
+ isBind (Atom x, v) = pure (x, Doc.Docd Nothing v)
+ isBind _ = throwErrorHere $ PatternMatchError (BadBindings pat)
+
+ addBinds e = modify (\s -> s { bindingsEnv = e <> bindingsEnv s })
+
+ def name doc_ val = do
+ val' <- baseEval val
+ defineAtom name doc_ val'
+ pure nil
+
+ defRec name doc_ val = do
+ val' <- baseEval val
+ case val' of
+ Lambda is b e -> do
+ defineAtom name doc_ $ LambdaRec name is b e
+ pure nil
+ LambdaRec{} -> throwErrorHere $
+ OtherError "'def-rec' cannot be used to alias functions. Use 'def' instead"
+ _ -> throwErrorHere $ OtherError "'def-rec' can only be used to define functions"
+
+data ModuleMeta = ModuleMeta
+ { name :: Ident
+ , exports :: [Ident]
+ , doc :: Text
+ }
+
+-- | Given a list of forms, the first of which should be module declaration,
+-- runs the rest of the forms in a new scope, and then defs the module value
+-- according to the name in the declaration.
+createModule :: forall m. Monad m => [Value] -> Lang m Value
+createModule = \case
+ (m : forms) -> do
+ m' <- baseEval m >>= meta
+ e <- withEnv identity $ traverse_ eval forms *> gets bindingsEnv
+ let exportsSet = Set.fromList (exports m')
+ let undefinedExports = Set.difference exportsSet (Map.keysSet (fromEnv e))
+ env <- if null undefinedExports
+ then pure . VEnv . Env $ Map.restrictKeys (fromEnv e) exportsSet
+ else throwErrorHere (ModuleError (UndefinedExports (name m') (Set.toList undefinedExports)))
+ let modu = Dict $ Map.fromList
+ [ (Keyword (Ident "module"), Atom (name m'))
+ , (Keyword (Ident "env"), env)
+ , (Keyword (Ident "exports"), Vec (Seq.fromList (Atom <$> exports m')))
+ ]
+ defineAtom (name m') (Just (doc m')) modu
+ pure modu
+ _ -> throwErrorHere (ModuleError MissingDeclaration)
+ where
+ meta v@(Dict d) = do
+ name <- modLookup v "module" d "missing `:module` key"
+ doc <- modLookup v "doc" d "missing `:doc` key"
+ exports <- modLookup v "exports" d "Missing `:exports` key"
+ case (name, doc, exports) of
+ (Atom n, String ds, Vec es) -> do
+ is <- traverse isAtom es ?? toLangError (ModuleError (InvalidDeclaration "`:exports` must be a vector of atoms" v))
+ pure $ ModuleMeta n (toList is) ds
+ _ -> throwErrorHere (ModuleError (InvalidDeclaration "`:module` must be an atom, `:doc` must be a string and `:exports` must be a vector" v))
+ meta v = throwErrorHere (ModuleError (InvalidDeclaration "must be dict" v))
+ modLookup v k d e = kwLookup k d ?? toLangError (ModuleError (InvalidDeclaration e v))
+
+
+-- | Call a lambda or primitive function @f@ with @arguments@. @f@ and
+-- @argumenst@ are not evaluated.
+callFn :: forall m. Monad m => Value -> [Value] -> Lang m Value
+callFn f arguments = case f of
+ Lambda argNames body closure -> do
+ args <- argumentBindings argNames
+ evalManyWithEnv (args <> closure) body
+ LambdaRec self argNames body closure -> do
+ args <- argumentBindings argNames
+ let selfBinding = GhcExts.fromList (Doc.noDocs [(self, f)])
+ evalManyWithEnv (args <> selfBinding <> closure) body
+ PrimFn i -> do
+ fn <- lookupPrimop i
+ fn arguments
+ _ -> throwErrorHere $ NonFunctionCalled f
+ where
+ evalManyWithEnv :: Env Value -> NonEmpty Value -> Lang m Value
+ evalManyWithEnv env exprs =
+ NonEmpty.last <$> withEnv (const env) (traverse baseEval exprs)
+ argumentBindings :: LambdaArgs -> Lang m (Env Value)
+ argumentBindings argNames = case argNames of
+ PosArgs names ->
+ if length names /= length arguments
+ then throwErrorHere $ WrongNumberOfArgs "lambda" (length names) (length arguments)
+ else toArgs $ zip names arguments
+ VarArgs name ->
+ toArgs [(name, Vec $ Seq.fromList arguments)]
+ where toArgs = pure . GhcExts.fromList . Doc.noDocs
+
+-- | Process special forms or call function. If @f@ is an atom that
+-- indicates a special form that special form is processed. Otherwise
+-- @f@ and @vs@ are evaluated in 'callFn' is called.
+infixr 1 $$
+($$) :: Monad m => Value -> [Value] -> Lang m Value
+f $$ vs = case f of
+ Atom i ->
+ case Map.lookup i specialForms of
+ Just form -> form vs
+ Nothing -> fnApp
+ _ -> fnApp
+ where
+ fnApp = do
+ f' <- baseEval f
+ vs' <- traverse baseEval vs
+ callFn f' vs'
diff --git a/crates/radicle-surf/src/test/platinum/Folder.svelte b/crates/radicle-surf/src/test/platinum/Folder.svelte
new file mode 100644
index 000000000..a50cdb374
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/Folder.svelte
@@ -0,0 +1,99 @@
+<script>
+ import ApolloClient from "apollo-boost";
+ import { gql } from "apollo-boost";
+ import { query } from "svelte-apollo";
+ import { TREE } from "../types.js";
+ import { getContext } from "svelte";
+ import { link } from "svelte-spa-router";
+ import { Icon } from "../DesignSystem";
+ import File from "./File.svelte";
+ import { revision, objectPath } from "../stores.js";
+ import * as path from "../path.js";
+ export let prefix = ""; // start sidebar tree from repository root
+ export let name = null;
+ export let expanded = false;
+ export let firstEntry = true;
+ const projectId = getContext("projectId");
+ const client = new ApolloClient({
+ uri: "http://127.0.0.1:4000"
+ });
+ const QUERY = gql`
+ query Query($projectId: IdInput!, $revision: String!, $prefix: String!) {
+ tree(projectId: $projectId, revision: $revision, prefix: $prefix) {
+ entries {
+ path
+ info {
+ objectType
+ name
+ }
+ }
+ }
+ }
+ `;
+ $: sourceTree = query(client, {
+ query: QUERY,
+ variables: { projectId: projectId, revision: $revision, prefix: prefix }
+ });
+ let toggle = () => {
+ expanded = !expanded;
+ };
+ $: active = prefix === $objectPath;
+</script>
+
+<style>
+ .folder {
+ display: flex;
+ cursor: pointer;
+ margin: 0 4px 12px 0;
+ color: var(--color-darkgray);
+ user-select: none;
+ }
+ .expanded :global(svg) {
+ transform: rotate(90deg);
+ }
+ .folder :global(svg:hover) {
+ background-color: #eeeeef;
+ border-radius: 2px;
+ }
+ .container {
+ margin: 0 0 0 8px;
+ }
+ a {
+ display: flex;
+ }
+ .active a {
+ color: var(--color-purple);
+ font-family: "GT America Medium";
+ }
+ .active :global(svg) {
+ fill: var(--color-purple);
+ }
+</style>
+
+{#await $sourceTree then result}
+ <div class="container">
+ {#if !firstEntry}
+ <div class="folder" class:expanded class:active>
+ <Icon.CarretBig on:click={toggle} />
+ <a
+ href={path.projectSource(projectId.domain, projectId.name, $revision, TREE, prefix)}
+ use:link>
+ {name}
+ </a>
+ </div>
+ {/if}
+
+ {#if expanded || firstEntry}
+ {#each result.data.tree.entries as entry}
+ {#if entry.info.objectType === TREE}
+ <svelte:self
+ prefix={entry.path + '/'}
+ name={entry.info.name}
+ firstEntry={false} />
+ {:else}
+ <File name={entry.info.name} filePath={entry.path} />
+ {/if}
+ {/each}
+ {/if}
+ </div>
+{/await}
diff --git a/crates/radicle-surf/src/test/platinum/LICENSE b/crates/radicle-surf/src/test/platinum/LICENSE
new file mode 100644
index 000000000..02f70f56e
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/LICENSE
@@ -0,0 +1,2 @@
+This is a license file.
+
diff --git a/crates/radicle-surf/src/test/platinum/arrows.txt b/crates/radicle-surf/src/test/platinum/arrows.txt
new file mode 100644
index 000000000..95418c040
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/arrows.txt
@@ -0,0 +1,7 @@
+ ;;;;; ;;;;; ;;;;;
+ ;;;;; ;;;;; ;;;;;
+ ;;;;; ;;;;; ;;;;;
+ ;;;;; ;;;;; ;;;;;
+..;;;;;.. ..;;;;;.. ..;;;;;..
+ ':::::' ':::::' ':::::'
+ ':` ':` ':`
diff --git a/crates/radicle-surf/src/test/platinum/cat.gz b/crates/radicle-surf/src/test/platinum/cat.gz
new file mode 100644
index 000000000..d3e231557
Binary files /dev/null and b/crates/radicle-surf/src/test/platinum/cat.gz differ
diff --git a/crates/radicle-surf/src/test/platinum/emoji.txt b/crates/radicle-surf/src/test/platinum/emoji.txt
new file mode 100644
index 000000000..157027753
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/emoji.txt
@@ -0,0 +1,8 @@
+ 🌞
+ ☁
+ ____🐓_
+---| |---------
+ / \ 🌵
+
+ ~~
+ 🐍 ~~
diff --git a/crates/radicle-surf/src/test/platinum/garden.txt b/crates/radicle-surf/src/test/platinum/garden.txt
new file mode 100644
index 000000000..859f93b3b
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/garden.txt
@@ -0,0 +1,8 @@
+ _(_)_ wWWWw _
+ @@@@ (_)@(_) vVVVv _ @@@@ (___) _(_)_
+ @@()@@ wWWWw (_)\ (___) _(_)_ @@()@@ Y (_)@(_)
+ @@@@ (___) `|/ Y (_)@(_) @@@@ \|/ (_)\
+ / Y \| \|/ /(_) \| |/ |
+ \ | \ |/ | / \ | / \|/ |/ \| \|/
+jgs \\|// \\|/// \\\|//\\\|/// \|/// \\\|// \\|// \\\|//
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/crates/radicle-surf/src/test/platinum/ls.gz b/crates/radicle-surf/src/test/platinum/ls.gz
new file mode 100644
index 000000000..03bb245f0
Binary files /dev/null and b/crates/radicle-surf/src/test/platinum/ls.gz differ
diff --git a/crates/radicle-surf/src/test/platinum/memory.rs b/crates/radicle-surf/src/test/platinum/memory.rs
new file mode 100644
index 000000000..b84992d24
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/memory.rs
@@ -0,0 +1,172 @@
+//! Provides [MemoryClient] to run the registry ledger in memory.
+
+use futures01::{future, prelude::*};
+use std::sync::{Arc, Mutex};
+
+use paint_support::storage::generator::{StorageMap, StorageValue};
+use parity_scale_codec::{Decode, FullCodec};
+use sr_primitives::{traits::Hash as _, BuildStorage as _};
+
+use radicle_registry_runtime::{
+ balances, registry, BalancesConfig, Executive, GenesisConfig, Hash, Hashing, Runtime,
+};
+
+use crate::interface::*;
+
+/// [Client] implementation using native runtime code and in memory state through
+/// [sr_io::TestExternalities].
+///
+/// # Differences with real [Client]
+///
+/// The [MemoryClient] does not produce blocks. In particular the `blocks` field in
+/// [TransactionApplied]` always equals `Hash::default` when returned from [Client::submit].
+///
+/// The responses returned from the client never result in an [Error].
+#[derive(Clone)]
+pub struct MemoryClient {
+ test_ext: Arc<Mutex<sr_io::TestExternalities>>,
+ genesis_hash: Hash,
+}
+
+impl MemoryClient {
+ pub fn new() -> Self {
+ let genesis_config = GenesisConfig {
+ paint_aura: None,
+ paint_balances: Some(BalancesConfig {
+ balances: vec![(
+ ed25519::Pair::from_string("//Alice", None)
+ .unwrap()
+ .public(),
+ 1 << 60,
+ )],
+ vesting: vec![],
+ }),
+ paint_sudo: None,
+ system: None,
+ };
+ let mut test_ext = sr_io::TestExternalities::new(genesis_config.build_storage().unwrap());
+ let genesis_hash = test_ext.execute_with(|| {
+ paint_system::Module::<Runtime>::initialize(
+ &1,
+ &[0u8; 32].into(),
+ &[0u8; 32].into(),
+ &Default::default(),
+ );
+ paint_system::Module::<Runtime>::block_hash(0)
+ });
+ MemoryClient {
+ test_ext: Arc::new(Mutex::new(test_ext)),
+ genesis_hash,
+ }
+ }
+
+ pub fn fetch_value<S: StorageValue<Value>, Value: FullCodec>(&self) -> Response<S::Query, Error>
+ where
+ S::Query: Send + 'static,
+ {
+ let test_ext = &mut self.test_ext.lock().unwrap();
+ let result = storage_lookup(test_ext, S::storage_value_final_key());
+ Box::new(
+ result
+ .map(S::from_optional_value_to_query)
+ .map_err(Error::Codec)
+ .into_future(),
+ )
+ }
+
+ pub fn fetch_map_value<S: StorageMap<Key, Value>, Key: FullCodec, Value: FullCodec>(
+ &self,
+ key: Key,
+ ) -> Response<S::Query, Error>
+ where
+ S::Query: Send + 'static,
+ {
+ let test_ext = &mut self.test_ext.lock().unwrap();
+ let result = storage_lookup(test_ext, S::storage_map_final_key(key));
+ Box::new(
+ result
+ .map(S::from_optional_value_to_query)
+ .map_err(Error::Codec)
+ .into_future(),
+ )
+ }
+}
+
+impl Client for MemoryClient {
+ fn submit<Call_: Call>(
+ &self,
+ author: &ed25519::Pair,
+ call: Call_,
+ ) -> Response<TransactionApplied<Call_>, Error> {
+ let account_id = author.public();
+ let client = self.clone();
+ let key_pair = author.clone();
+ Box::new(
+ self.get_transaction_extra(&account_id)
+ .and_then(move |extra| {
+ let extrinsic = crate::extrinsic::signed_extrinsic(
+ &key_pair,
+ call.into_runtime_call(),
+ extra.nonce,
+ extra.genesis_hash,
+ );
+ let tx_hash = Hashing::hash_of(&extrinsic);
+ let test_ext = &mut client.test_ext.lock().unwrap();
+ let events = test_ext.execute_with(move || {
+ let event_start_index = paint_system::Module::<Runtime>::event_count();
+ let _apply_outcome = Executive::apply_extrinsic(extrinsic).unwrap();
+ paint_system::Module::<Runtime>::events()
+ .into_iter()
+ .skip(event_start_index as usize)
+ .map(|event_record| event_record.event)
+ .collect::<Vec<Event>>()
+ });
+ let result = Call_::result_from_events(events.clone())?;
+ Ok(TransactionApplied {
+ tx_hash,
+ block: Default::default(),
+ events,
+ result,
+ })
+ }),
+ )
+ }
+
+ fn get_transaction_extra(&self, account_id: &AccountId) -> Response<TransactionExtra, Error> {
+ let test_ext = &mut self.test_ext.lock().unwrap();
+ let nonce =
+ test_ext.execute_with(|| paint_system::Module::<Runtime>::account_nonce(account_id));
+ Box::new(future::ok(TransactionExtra {
+ nonce,
+ genesis_hash: self.genesis_hash,
+ }))
+ }
+
+ fn free_balance(&self, account_id: &AccountId) -> Response<Balance, Error> {
+ Box::new(self.fetch_map_value::<balances::FreeBalance<Runtime>, _, _>(account_id.clone()))
+ }
+
+ fn get_project(&self, id: ProjectId) -> Response<Option<Project>, Error> {
+ Box::new(self.fetch_map_value::<registry::store::Projects, _, _>(id))
+ }
+
+ fn list_projects(&self) -> Response<Vec<ProjectId>, Error> {
+ Box::new(self.fetch_value::<registry::store::ProjectIds, _>())
+ }
+
+ fn get_checkpoint(&self, id: CheckpointId) -> Response<Option<Checkpoint>, Error> {
+ Box::new(self.fetch_map_value::<registry::store::Checkpoints, _, _>(id))
+ }
+}
+
+/// Lookup and decode a storage value in the [TestExternalities] context.
+fn storage_lookup<Value: Decode>(
+ test_ext: &mut sr_io::TestExternalities,
+ key: impl AsRef<[u8]>,
+) -> Result<Option<Value>, parity_scale_codec::Error> {
+ let maybe_data = test_ext.execute_with(|| sr_io::storage::get(key.as_ref()));
+ match maybe_data {
+ Some(data) => Value::decode(&mut data.as_slice()).map(Some),
+ None => Ok(None),
+ }
+}
diff --git a/crates/radicle-surf/src/test/platinum/signatures/binaries.sig b/crates/radicle-surf/src/test/platinum/signatures/binaries.sig
new file mode 100644
index 000000000..83aab0de9
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/binaries.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k6RoACgkQaDIsu74N
+Arre+RAAwTEQgVJWhU9OCNTUCOjgbs51NlAALyhLNSWENdJ/zNqWHk4XwX8YEsPU
+fKtzjwqFzImBSYOz4rje1s0xcqCM+MGaonaCb8bHl2G5lq/rf1LRplVQvFf6WhTR
+UxZSOtXKc702m3iWLsOQq45DDd//EoHLse7ISbhh0kh4+p55//PGpm23LllVzpY0
+UuGD7dCFFgHuE+p4gDMSMrwV+AgukjfMHam3wwQePrZzjgG8yO0Z04a87X+CcHUT
+zYVw0G3M8aJ26D72ZsA6oYia/cSCJG7kY61iih6TEm13Y2R2UGxRqOvsdNlm5Bpw
++kFOvvuKTs1y0FsQ8Zipe6BqVyxF45+ybEvgLM4cSB9iW0+g1sxKM5PmEmTFlpeW
+mwAJ7wVlpH+FwMY+m+lX47BPXo7E1SYzUNfR+8zjm8QlZci05s5Lo0cIKrIAulHN
+YLf6YpzKgRp5p+Fhp75jrMCGvZ/7jlX6shhoUe5Y17N2X8TMUHAys/qSN88Qslu2
+LeuQ7eG6eSAgNaTwekBMVSM1yTHZwXbGa1/Vj9zWEu6BgM9U2zeryz9ATTARY+/Y
+nzSVYajtPwZLEEo8LUdAWF6P919/wdfPdB/6AaYu0eFiy8jr1a4RN4I3mZjZP76w
++sziMIWMKg63CPvyYA0dqul3u4fWZ2ymiFjwNunEp8MQ2oVSH80=
+=1/L7
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/common.sig b/crates/radicle-surf/src/test/platinum/signatures/common.sig
new file mode 100644
index 000000000..05f171026
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/common.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k6zYACgkQaDIsu74N
+ArrZ5hAAwjx6uL0hhTeZ/w6zGocS9I1DeQAJVMFQVeqVBi3Z4+H0TEY14qEexDVn
+U3k6RvfRE1TcyXYtO7SzPKCqcLuXpZWjSfXuxjFDRVObEFrfSqKx2Wc89Nxn0xb1
+ltO89TNmchYPTzqEGhYUAsxp7ff/piIYzc6GG5DOuy8vggQzujpEA5ujg3iKXILQ
+3K2dXMBEnEqf5S9SHDcTVCTXqcrNA2kjW2TiiYkxFFJoqwNjpxj22yRfAGt/2Jq5
+srjZVxW50UK9zLhsiYEcy49Md98NcG9if0QKitKVpo6GpyM1qqKFJsXZbroAzf6R
+U7PZwfY24DqN9lveYQomnUTNkRxwBgR3di4Z0YNROhyLcjfkzbGMU8v4SYBfv5fG
+SQThyo6HMCJLZnTe+7yzNXncpHVaiEn5s4SH98W6thRtZgs2QKbwcM15ZG01XUrD
+0cEvGQv+H8J+q9Pt3UnuL6i39iLcfWXHtXRtObV1st6inGbuykxa8lSBpfTAaWYu
+Hdqzh3gOnXsKXhwGpgyxPqoUBGM5uyxMYfz1fXba0NUEl3efGN5gZskrl5Wjvx5/
+B7W2D1A+KiRwUHABN9BAORUH5sJLmcaFIyd2c2NXC6DCO7QAlLRcuir6kfS0mnJw
+KuyDD7Aron+ZTDUgcRWnSfc1GuCKnI9ToCk3kgW8l/uU8FXow6g=
+=0Guy
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/deletion-added.sig b/crates/radicle-surf/src/test/platinum/signatures/deletion-added.sig
new file mode 100644
index 000000000..5de0cd35b
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/deletion-added.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3nvVUACgkQaDIsu74N
+Arrmcg//fulK6vqrAQiZSLmh0qf4Vvm8NLp18k0JYkrT+QbEltwpNSL1U8Z5uO+X
+QDtZoCgWN+OhypvihJahhk/QSeD6Zs8RqeR0j91nlgh88t4NEx2K3oU89J4bok1I
+TK/7OJgZiSfSQrHPJEC/wZJsGzxG606HgV8yY3maKo958V+/g3ttThzVqk4neVzS
+Qb7nWX+/Be3mg2612JDjYmVvzce8q+44B6HWHhjwSkvDx6sB2T8wc2mabnU9TEMJ
+jYtmJRUCWWd09mMOLoUQgGlwG/2jwD6b8x2Sx4nEOydVewk89Dem7Vc5WE1ot8ae
+q1EIIELxubmXzHotepHlszgstHqjAEOouqnx/Wn5hWdo7uKfUlALkbJc1LyG8Sw+
+y69CbIX0H3w0G3L0+YHdkXbiCwl3EK5aWKaoIHA3DTIs11rmpWvkvncYs8krRkrm
+4b2HTVCM7+f5SGqaTz3kTeaTwPfAYQPwvXNMrAVGIO9FYgSSSXuBrxxgQ13LYbvW
+pvtlhhZ/TzFsagVVl4Za51dnt+hT0v9fgG9XRey7gGbsWFQwOz12NnlccvDJ4HSg
+qAKq3qtQS3GKzAUFcxAczBcxbt0Tx+3VM7gEQKAnzVp3FpXGbYwaxx18lFMqGIJ8
+OzhjMPZDzuZ/SShui3xpuyd+lVn1a83rQ4/9W5BgBxIWhdgNOz4=
+=44E7
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/deletion-removed.sig b/crates/radicle-surf/src/test/platinum/signatures/deletion-removed.sig
new file mode 100644
index 000000000..73f9fe573
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/deletion-removed.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3nvXQACgkQaDIsu74N
+ArqZvg/9Fp5mxNqyb5nwsTDixSfF4yXDE4R+vGKCids+sxnETepS4Ssnwl2Do/hN
+RTItTU09ZE0jXZKoc7qZI/n0b36kca0kfowvmAomgQLqh0yaSb7wkZcDivAvjs5Q
+iHfopvLNeI6OG8STVrVx4W8+6TNmq0jlet782rHDZVWpig8UrpbZ59q7yaIC3BDu
+2QB9NWj2IJFKJOxCABd0IY71zeH9xYlbcRXwU/8isipjIlTLfdBpaSxTbUjj5Eum
+M3EmvGVncDToD4wXijMIKT7qPeVkwfBiZb3RuFgqgI/cOentjKD9xBUuCvGyLSF3
+M5l2Olx0ufAhUaN5IYwkh7fyYB2tUMTBE4Xtamst0HiiKaUoda6UTRMJIF6DZVbP
++Sq4GGcRDuZypCNMLx6gkvrx7rFL3Dzkq/6kcixEqQ8Bq5NOBlCPDg372ERQnU9c
+0BCVPI1uIuZf6bwWwUBYuzP09aNJeYtbymTKe2r33VNziANkA7r48mM2g7xA6X1B
+SzZTNMHbfqK8ibYZhAfGNI0fbiSXRlmwOpVXEglpXMMZjDWJzyINyXtHrJYdqmSR
+ZzFwlCKY4HVrDhsCP+OflQuMbGXdzBQGFs/ksIwOvFsdtgqM2k8ioZMOTE8mBCT4
+9ZfUvnAF3X+opRDIrf+VhESgeck+ZQoNhr2K0WRB605X8W3mN8Y=
+=+2J4
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/dev.sig b/crates/radicle-surf/src/test/platinum/signatures/dev.sig
new file mode 100644
index 000000000..b8cb8aa04
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/dev.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k67AACgkQaDIsu74N
+ArrDVw//c85BqWoryW0sqoze5+RRC2pLiKknQgI7x0kue91kJVZ/bSYhePKwuxyM
+6lgRScZp7gEyT9AyOZWr7Fyrd+V53dIrYQMW48FlSt5KdqPq3tOclxOzmURxsy9p
+Okpv909VesYb8WNyLOBVbHE69su/gh0qYEn1X9kfm1c3dYiAG3s3JuzqRjtSFWV2
+G/KFDl4csflrsbs+iGzfBPwqsIPRk476LdR6gaR17kdy41CnluAksuJZ8Uw4YsGv
+tIvsyT08wDd3GqyHrzZ1rcMCh9DlD/ERGeylA2URNmQE/cxttBV+lHjCpF7GZiH1
+UkGA5ln/oHQtw1U7Tye5NDCGOS43tUiy7yWBX5v9S/w1PuCHf8F0ejXGmFqGEOyd
+ZSNywhg+p9VJV9/UD1pm1s14DuRJzKhbrja22qVhg9qZJxTm1GWfnRC8met4/BjN
+Qyffo4fd9JPmrfYAtY2tEh+VS3S9BAMmhVk6D0thB5DAOAd4M+MIzm5j5ZDJIs2c
+w0xqiLH/Ic4XYvvG1egKSMUWjexgwQj/ja9jIGr4UhDKmEz3cbZVw7WJBfiYfdGN
+PqgvgW8KHDv4FZIZUu8IuLaB6wPdg/9MSLaNkbpq0/JhNtovqsrLva8OQJPLZVyM
+kXz/AXSc9jL03NtpwtCTGV6be4taeoyhcLoSMGrY8APJiJ2n25A=
+=VCJq
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/diff-test.sig b/crates/radicle-surf/src/test/platinum/signatures/diff-test.sig
new file mode 100644
index 000000000..95d84f99a
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/diff-test.sig
@@ -0,0 +1,6 @@
+-----BEGIN SSH SIGNATURE-----
+U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgXD7AI9lSHw6d2GBq8kxJmW6itW
+/MmPPbY8fzVeuS8i4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+AAAAQFVcxAVjTRp3x0mYSjcBb10Ywjb+UyZ9Zi8GTofzTeVL+W/2D422qKCsPkbVgQOuaG
+OxKsKBwrSBTSOEf8RWKAg=
+-----END SSH SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/docs.sig b/crates/radicle-surf/src/test/platinum/signatures/docs.sig
new file mode 100644
index 000000000..195912c53
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/docs.sig
@@ -0,0 +1,10 @@
+-----BEGIN PGP SIGNATURE-----
+
+wsBcBAABCAAQBQJeExlUCRBK7hj4Ov3rIwAAdHIIAHnXU+wVSLDL4EQQHxQtWVJA
+qCRGExLRqqfXZNWgTpNbYXy7eK1TcqshZFUvUY9uy/AIwjnIMeBz4CbQuB5+2Te4
+RhK7hFtiPjSMhmpIpvL9izsKZCJzIMtx4f7CC8PWsa61mfgy7mnt5td5X42PyhJs
+xlkcBWSAGgCH8FfEV1FOAUFQQzztv4VwwvY8myYRLCnjBRghnV7iQsXXqNSmB+Mo
+E/vZ6hdt/ylhOs5Bpy9ft4NlArJl8gTsfJrs2RMtybbU+DRFMJAazcSkqM1dhfo+
+hvjIeF758OB7OBftcrpG6JVDNajvkIokxbU//Mz/6OOgktBA8vwaw6NXSgmcYa8=
+=qH1p
+-----END PGP SIGNATURE-----
diff --git a/crates/radicle-surf/src/test/platinum/signatures/folder-removed.sig b/crates/radicle-surf/src/test/platinum/signatures/folder-removed.sig
new file mode 100644
index 000000000..f08e2e04f
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/folder-removed.sig
@@ -0,0 +1,10 @@
+-----BEGIN PGP SIGNATURE-----
+
+wsBcBAABCAAQBQJeTnBdCRBK7hj4Ov3rIwAAdHIIAFedCAtnlo8BtZGRyBpTf975
+Ne7jNqzzA52YPTcZpp4hV+ccO9igHUCyeHlIV79puKbFa353Kq7VKqUYkK33NYXL
+hzHaxvE+HpfDmIt6GWdXeout/IVWlAKEMqVXZTZlKC+OmRyVIg1s0rPryHiCVPNL
+6WzFtIEp39rVkZLosPPcIg0T859gZqcU3osBQKKhqg1gnZRK0OvYIRTwfDf0FGAN
+tGikY7XpOlEAavA9c/9eXZBV0RZdIDk2+4jZHUO+AZLUw5cR+Jp9f2YQG31FFjPb
+iZY0EMx19Usle/4Mm67/kaHPK4I+cJ9VAmZXMCnRKkYHTVszb6DZWYz85oNjh1o=
+=FYia
+-----END PGP SIGNATURE-----
diff --git a/crates/radicle-surf/src/test/platinum/signatures/initial.sig b/crates/radicle-surf/src/test/platinum/signatures/initial.sig
new file mode 100644
index 000000000..0ee1f74ae
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/initial.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k5mIACgkQaDIsu74N
+ArrxuA//e9riSTSzZaYcayEykaGgYxm4FAiEcs/LONQp2wJg1srs6xCEjcYHdWg0
+0eP4qE7KdUtMiXyCv4ma4+BKsM+OpH0KP58ju/c6a7AD8AMQbYoT6c9Gp7Wwkly/
+Xn2lXzM19SDBZUTfEEc6ejZiWd3DGJJs88tTstgoIdFjzHe/BEarVWDT2PBducIm
+FDz/dvzoi9I97lPyCiLRP2LzCDG8vyN0T0xFJpF3aPSebVSW6CZuZdkkwDWqf22D
+bFuEXtotnl+cUnYJXxTFEj0vdOLfBbeiVdtDeGRxKWNTyRmGrQrZPzRP1/WG2kPP
+RMVRgonUEJMt/JzFILH14tYv1bc9ji+7xLNPRn3Z+A7KQfdphfw6/VZS5vY0wH6V
+XkJEqJd6i4k/PeRwcFX3X1i7npla/pY79KtjGXDcLoa5O42JlezEzJFMN0mNxLR9
+BXenBACsP40ifsaPHl4maXCt/JqbvtLdGtths6LK/U3nIIXxRz+i/rW2ZzFc+zEF
+msYzWWSj/MNSuMyfRwGmtk8F+dyTr11w+X7VrKTi5OHLkqwArdnMnLsywEsLa/lB
+9QZAM+8mN3zIdr3QpBWGP/kzUC+3XaxCs4ZR5H6ILItnWHNTRY3j11cruzIFH7WV
+HQAMBl95HdJebDzvS0CY7P/fntzrdnNH+/gvF1ZA3DBLuKZFA5I=
+=WzFk
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/long-message.sig b/crates/radicle-surf/src/test/platinum/signatures/long-message.sig
new file mode 100644
index 000000000..08f8b1e98
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/long-message.sig
@@ -0,0 +1,10 @@
+-----BEGIN PGP SIGNATURE-----
+
+wsBcBAABCAAQBQJd8nTZCRBK7hj4Ov3rIwAAdHIIAFbhHXu4yTmibrAKiRLo52/4
+3jxGoVoWvJuZIrHq1KINdwifV8FcRgFvGMwHtd/ACo7QemaDiFHF4cd3/jB3FVv1
+o+ugassmwtETkl5soI7KvM9fmw3DsMBu/Kx1XPGZvSMs8VuU+bRH/5PVnrF9ck/m
+P6aX2rRmf/HngxqnTPt9pHvi9/u3BEhyaQgrdVxBclzW3e1pjYUi4mPgwipNoHme
+OqwhRdW0o/e2I/J50elluWULKJD+I9XfbBugNw47wzywf23OX4ex4ViO+tRua1mB
+W1NWw6sFjTbra1gGgUaWMtTzxWOezbjazp/uPsNx9KscoNsnqaapkZHqZYLh8Po=
+=Atpd
+-----END PGP SIGNATURE-----
diff --git a/crates/radicle-surf/src/test/platinum/signatures/master.sig b/crates/radicle-surf/src/test/platinum/signatures/master.sig
new file mode 100644
index 000000000..81f5bfa11
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/master.sig
@@ -0,0 +1,10 @@
+-----BEGIN PGP SIGNATURE-----
+
+wsBcBAABCAAQBQJfiHWICRBK7hj4Ov3rIwAAdHIIAEvxxPkgNdFOC3uF3w59CpkG
+6Z/DGOpDrP6P8qQQ4nJ6TZZHFvIs6+4i9XyvzPflrnNPgbWJhxnLA2ylPuu5+Cft
+CxwlmRuxFBKeAfFrVPBK/XtoWCnnTUCQMMcKEgO7xoBaFiRjiu3LaMI/X3u6HPEo
+hTBDx4A/MBTJhphHTnJ8w9COpRoejNJLTuja7+6Q6Mz/ckwK6tzttyE9vnfHRbjL
+YbhGLJL6uYnbK3yp8WbwVMKYMnfPdW5Hw5trwqVuXljMzci9ik1LWgmpnVxIkf/x
+jIXElzrjbUrh9qiWpevVd5p5RwnI7bdJG4rQVvz2HPcz1tDgiV6ZNQ1G/OcwneA=
+=YLNW
+-----END PGP SIGNATURE-----
diff --git a/crates/radicle-surf/src/test/platinum/signatures/merged.sig b/crates/radicle-surf/src/test/platinum/signatures/merged.sig
new file mode 100644
index 000000000..8bf75427e
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/merged.sig
@@ -0,0 +1,10 @@
+-----BEGIN PGP SIGNATURE-----
+
+wsBcBAABCAAQBQJeb4kbCRBK7hj4Ov3rIwAAdHIIAJWI1IccDC4VfHYs8MeeGXgn
+9CSM5dh6QAzILYCFPX82alftJ+wxxCccWmIJ1YltFEgy1nCvRwYp4fbeIpqxjZCc
+IZwwerPIHckGPEpcDkTdQ/ER8z84HWNIqXb8GikzRZCDyRBqHNdufZ3YsjLxVfbQ
+hstD4Y1t9ocn43QDc7N7pZmxqMPfFFnHwZmX5aSrkzXJ7W3sQ8pCTsGr/odKP79L
+hRB9dmmmGIiB9TvnFD+2Slx1Z8frelOm9RClZ6NjPERl5ZKyHj5VqxfMi6Mo0KC4
+Qky9AZ2j9872yUEqUXD6xmBuxyYWVwnPzVe8oxk4paOLpd3x1jbS6sbtPrimaRY=
+=vclq
+-----END PGP SIGNATURE-----
diff --git a/crates/radicle-surf/src/test/platinum/signatures/moved.sig b/crates/radicle-surf/src/test/platinum/signatures/moved.sig
new file mode 100644
index 000000000..a098125be
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/moved.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k6lMACgkQaDIsu74N
+Arrl/A/7BH95H9m44+86RTsm0rZZvQ40KV6vADEVEXlzqsN/Nqmkij46prjPni+k
+BWypnfSZO3pmCXTzLmJ5nUEVOL+aHhIwanVeFS15OqpTKNLVzMMTXsRcsHzyXmkS
+fp0AUOzurgMoeDJ11u16/Q0CiKWr0PPjwO0DxrHxkwvWXQ2NupuPdb07c+e1c+Oz
+2sn+7V7HyZ9cvZSfoxJJDUiLRezQ2xx5EIygTdKlY/1wrQQ/MMYy+VSeZv60+oYf
+ACrVN3X2dQMEgWEW6CTOKqeQVeGsJtgwkJqtRi/7Z3pi3hLymuIxVL1CCODq0b6Q
+tw/KyYF6RXO2a9rgaY4ghEd5rTj2tpz/FvQ/3kIjOtNMDW6vaCJNNsZeJneb+5xq
+yfNQ7Z7iGk+SGreLG23XhuP36LWxqQq6zFyAiRzuqKf96a3ZI0pw7gXR0AAqHzFI
+eUa6B5LrmE4RNM3zKjttuvZysE8Qh18sgNrCnjxdZZyfmXIr1E38Y/drvePuLBux
+ioWDTgXc1lK1MLy46p/1ub3/UxoTps4fX+iOpKWd2fRn/nE16is3y5Q5u6+dF+YW
+djCe30AsDoufRsp52UrWXLFrqzqZRILKUkvk0obZwLaP+gZascv7ME7ysdMyYPzg
+WVmqIk3ew/0bacqPDJZfoGTI1yYl2rTAiKxwzKOZA7oAVD51HJE=
+=WX/k
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/nested.sig b/crates/radicle-surf/src/test/platinum/signatures/nested.sig
new file mode 100644
index 000000000..a629c64c9
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/nested.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k5s8ACgkQaDIsu74N
+ArqknQ//XWUKF6n5Dl9tv8lLIGTTGzyeF0ZcI5dSE9WF6x3YHH3wSawE84KenRwE
+L1PrpnQUrFw9JOeT+0aoNIvqGGj/6G82evz0bcqkyOanJiHr1SlRaZFuNwvFkbMS
+4aM+pqDdzPRUynRCQmswMOPhybucvmv2NTRU4Vufh7YvaCVOXyFTR1gIprw+CtfV
+Q5IWXfPeI5oQ6LLLKBnhStNK3NukyaJAkmzDkr63Z9pnFAZ48A/QqBCjDXVMkAvU
+eS6WkaFSVXmtEIcLYFbB8L5a62DYjMdzfhX+LzDvUE0DADUqM5NnJW9OjIBJdrYq
+Xn6pA4qLPZk0aE9lD7vJ13/nZTgouVGQNFPNhyJpasfES2SPEGWtx2TTpHxWVBC/
+BVxfIvmVZz4oPsRS1jCmbL8wB33bxAq/MqV4Qs23vTlkwInmBvuZfzLP339u+wNT
+XKkCCgGkrl8UDiWNR6USPhlylJyKYhbCrZnTUPVMe/pMhp089DZyiiHPpgeT623S
+U4XwYneoZmQEht5SIygiph/HqKA1RiP9Wmq2jnvLBidZ+ag3yqY10Nn1VxVvFAGb
+EAbSQr2FJpaeyro87/dWJnWbAY1uBS84jFoS/3RWpV+/bqPkP7SPA/M8sCIM479z
+zF9ovzyXczB4H2RlA6/vIW31Jq8jHSw4HwtCn5JKArB9eL0kVMM=
+=3aUn
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/sources.sig b/crates/radicle-surf/src/test/platinum/signatures/sources.sig
new file mode 100644
index 000000000..74531e1c9
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/sources.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k6MQACgkQaDIsu74N
+ArqgZBAAkjJdTCQzLZX/sthB3lqVatFLU+RzcEeF5Np+yMxwzMunA/VheZZffHsT
+j/tEjQw/hehPb3YS2TxA8JREWqccbBgeCF9b4ZPvmEW1Hdp6S6Hj/bek4frg9oZi
+rSOzjEg1wZDmGp3uIYiOD0e8p1kkMyaWZamg41HmSRBEbQTD2VANkVYCwUZ+lIHM
+SZkZ4lRp5Tpg4u4r9TlS4UqWNh53S5GVdIgjAZks/THfJscPfu7AOPQF+G855DLU
+6upSHni3tIBDTMkhU1PdDdsG278HglsHKWVl7Xi7XPlW+NX/bb7T8ffqUE2nKENH
+MvOBMX8uEss5/c+5sq5HxgW9zGJFfVvBMvI34FlT/2HY3m+X8aEXPTssF5vvZteP
+q8Mj6L6RkfpRad/9jpc1wsIN37FdudzGZ51Yqx+nuBGV7KaCVTH7YrvA0yRZ+8+P
+zWfNvAFvbtdsYEVPo2J7smC9DJndA3wKgoF05BkwlYm7JHzD1yCXIixEq7Vu8Mq0
+h12E5jKmGFenTbqOWCd8FsAqWgVAgxcpTh2Pe2h6YYikZ4C2bImvmHNYXOTs5y/C
+/5MggXvSy2qzA7B1j7Etwb1jDoVztrbSCRB0HQ35MJ7NAy7byxuE0ofsPJjZXZRf
+sdwjJILgPx8URUVjgAD7Uowwye5ZdHTA26YL5vnu76BtwLZl5cU=
+=gyZi
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/signatures/texts.sig b/crates/radicle-surf/src/test/platinum/signatures/texts.sig
new file mode 100644
index 000000000..5b9bceb7b
--- /dev/null
+++ b/crates/radicle-surf/src/test/platinum/signatures/texts.sig
@@ -0,0 +1,16 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAABCAAdFiEEime/6MCYr8AgSm0yaDIsu74NAroFAl3k6ugACgkQaDIsu74N
+AromTQ//eNQv4HPfCs4Q/zrWPnBgvk5JgwEaCJNBWHvKsHIWMeMeS4KQ3rTD9d1z
+xicWcV5JHORe4+OdJHCcFbPFfKhUR99JxXY+OoQK8qYpT/nQsTgURciw3Ef+bbcn
+WGAdlvFDpsSjfipxp2xSFwU5FGwZ9FjGG9gKhDKbEzmqiU5tp3TpnVdJej55FAtM
+TdD3I00JNKmmOGPZw7DKi3Re7FqajG1FsqXsqwsq0S25TLcrPdO+hbaHHxZvCidG
+1oH1p4TC/a5XWB+uIcO5Pyrk7z+HNfDtCCebKG+JCJJYH4PtdxdWI2ZKaROJshMU
+JnKMBaVacGVnBjl/G9SzmYTQs3lsbT+quMgXk1lQ1R8eGJo/nuc4LOevyEAlcnxU
+WOfJMHFXqCwgfJz42JezJXFTCVDoBZ7OPGUHpee5UoIpBAEDANeDAopAZhOPp1RJ
+wY0YaWmn3HTZe9LXg0M1n/BUfDZgY5ZUOtXuT1Lla3ReZPGYxK4bSr6YRLhcz2Vp
+20OJBDzDsZihcU2PBp7R1XwhhQGUf9mliuBBEXLD3jek3BTl9fwA+tfb0CJ7mfR4
+9i5i5PGrY32FLiBwdpAmfRhTLYh3y49EuQl14tMiRPB+5YFi1plhYzghpsgUDkpJ
+NdddcQW+S4EyjxTUURPhQovlkFGW8OSJCf5CxPfMizOtaJlcFUU=
+=GUKI
+-----END PGP SIGNATURE-----
\ No newline at end of file
diff --git a/crates/radicle-surf/src/test/platinum/test.gz b/crates/radicle-surf/src/test/platinum/test.gz
new file mode 100644
index 000000000..8d38a7290
Binary files /dev/null and b/crates/radicle-surf/src/test/platinum/test.gz differ
diff --git a/crates/radicle-surf/src/test/reference.rs b/crates/radicle-surf/src/test/reference.rs
index 6625feb97..cb3490bed 100644
--- a/crates/radicle-surf/src/test/reference.rs
+++ b/crates/radicle-surf/src/test/reference.rs
@@ -1,22 +1,18 @@
-use radicle_git_ext::ref_format::refspec;
-use radicle_surf::{Glob, Repository};
+use crate::{Glob, Repository};
+use radicle_git_ref_format::pattern;
-use super::GIT_PLATINUM;
+use super::platinum;
#[test]
fn test_branches() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let heads = Glob::all_heads();
let branches = repo.branches(heads.clone()).unwrap();
for b in branches {
println!("{}", b.unwrap().refname());
}
let branches = repo
- .branches(
- heads
- .branches()
- .and(Glob::remotes(refspec::pattern!("banana/*"))),
- )
+ .branches(heads.branches().and(Glob::remotes(pattern!("banana/*"))))
.unwrap();
for b in branches {
println!("{}", b.unwrap().refname());
@@ -25,7 +21,7 @@ fn test_branches() {
#[test]
fn test_tag_snapshot() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let tags = repo
.tags(&Glob::all_tags())
.unwrap()
@@ -38,18 +34,16 @@ fn test_tag_snapshot() {
#[test]
fn test_namespaces() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let namespaces = repo.namespaces(&Glob::all_namespaces()).unwrap();
assert_eq!(namespaces.count(), 3);
let namespaces = repo
- .namespaces(&Glob::namespaces(refspec::pattern!("golden/*")))
+ .namespaces(&Glob::namespaces(pattern!("golden/*")))
.unwrap();
assert_eq!(namespaces.count(), 2);
let namespaces = repo
- .namespaces(
- &Glob::namespaces(refspec::pattern!("golden/*")).insert(refspec::pattern!("me/*")),
- )
+ .namespaces(&Glob::namespaces(pattern!("golden/*")).insert(pattern!("me/*")))
.unwrap();
assert_eq!(namespaces.count(), 3);
}
diff --git a/crates/radicle-surf/src/test/repository.rs b/crates/radicle-surf/src/test/repository.rs
new file mode 100644
index 000000000..5cd55d356
--- /dev/null
+++ b/crates/radicle-surf/src/test/repository.rs
@@ -0,0 +1,103 @@
+use std::{convert::Infallible, path::Path};
+
+use git2::{Oid, RepositoryInitOptions};
+use radicle_git_metadata::commit::CommitData;
+use radicle_git_ref_format::RefString;
+
+use crate::test::r#gen::commit::{self, TreeData};
+pub struct Fixture {
+ #[allow(unused)] // Prevent early removal of the temporary directory.
+ dir: tempfile::TempDir,
+
+ pub inner: git2::Repository,
+ pub head: Option<git2::Oid>,
+}
+
+/// Initialise a [`git2::Repository`] in a temporary directory.
+///
+/// The provided `commits` will be added to the repository, and the
+/// head commit will be returned.
+pub fn fixture(refname: &RefString, commits: Vec<CommitData<TreeData, Infallible>>) -> Fixture {
+ let dir = tempfile::tempdir().unwrap();
+ let repo = git2::Repository::init_opts(
+ dir.path(),
+ RepositoryInitOptions::new().initial_head(refname),
+ )
+ .unwrap();
+ let commits = commit::write_commits(&repo, commits).unwrap();
+ let head = commits.last().copied();
+
+ if let Some(head) = head {
+ repo.reference(refname.as_str(), head, false, "Initialise repository")
+ .unwrap();
+ }
+
+ Fixture {
+ dir,
+ inner: repo,
+ head,
+ }
+}
+
+pub fn bare_fixture(
+ refname: &RefString,
+ commits: Vec<CommitData<TreeData, Infallible>>,
+) -> Fixture {
+ let dir = tempfile::tempdir().unwrap();
+ let repo = git2::Repository::init_opts(
+ dir.path(),
+ RepositoryInitOptions::new()
+ .initial_head(refname)
+ .bare(true),
+ )
+ .unwrap();
+ let commits = commit::write_commits(&repo, commits).unwrap();
+ let head = commits.last().copied();
+
+ if let Some(head) = head {
+ repo.reference(refname.as_str(), head, false, "Initialise repository")
+ .unwrap();
+ }
+
+ Fixture {
+ dir,
+ inner: repo,
+ head,
+ }
+}
+
+pub fn submodule<'a>(
+ parent: &'a git2::Repository,
+ child: &'a git2::Repository,
+ refname: &RefString,
+ head: Oid,
+ author: &git2::Signature,
+) -> git2::Submodule<'a> {
+ let url = format!("file://{}", child.path().canonicalize().unwrap().display());
+ let mut sub = parent
+ .submodule(url.as_str(), Path::new("submodule"), true)
+ .unwrap();
+ let _ = sub.open().unwrap();
+ let _ = sub
+ .clone(Some(&mut git2::SubmoduleUpdateOptions::default()))
+ .unwrap();
+ sub.add_to_index(true).unwrap();
+ sub.add_finalize().unwrap();
+ {
+ let mut ix = parent.index().unwrap();
+ let tree = ix.write_tree_to(parent).unwrap();
+ let tree = parent.find_tree(tree).unwrap();
+ let head = parent.find_commit(head).unwrap();
+ parent
+ .commit(
+ Some(refname.as_str()),
+ author,
+ author,
+ "Commit submodule",
+ &tree,
+ &[&head],
+ )
+ .unwrap();
+ }
+ sub
+}
diff --git a/crates/radicle-surf/src/test/rev.rs b/crates/radicle-surf/src/test/rev.rs
index 60294a0f0..81d7a23ee 100644
--- a/crates/radicle-surf/src/test/rev.rs
+++ b/crates/radicle-surf/src/test/rev.rs
@@ -1,9 +1,10 @@
use std::str::FromStr;
-use radicle_git_ext::ref_format::{name::component, refname};
-use radicle_surf::{Branch, Error, Oid, Repository};
+use radicle_git_ref_format::{component, refname};
-use super::GIT_PLATINUM;
+use crate::{Branch, Error, Oid, Repository};
+
+use super::platinum;
// **FIXME**: This seems to break occasionally on
// buildkite. For some reason the commit
@@ -18,7 +19,7 @@ use super::GIT_PLATINUM;
// * Expand "Options" and select "clean checkout".
#[test]
fn _master() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let mut history = repo.history(Branch::remote(component!("origin"), refname!("master")))?;
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
@@ -26,7 +27,7 @@ fn _master() -> Result<(), Error> {
history.any(|commit| commit.unwrap().id == commit1),
"commit_id={}, history =\n{:#?}",
commit1,
- &history
+ history
);
let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
@@ -34,7 +35,7 @@ fn _master() -> Result<(), Error> {
history.any(|commit| commit.unwrap().id == commit2),
"commit_id={}, history =\n{:#?}",
commit2,
- &history
+ history
);
Ok(())
@@ -42,7 +43,7 @@ fn _master() -> Result<(), Error> {
#[test]
fn commit() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
let mut history = repo.history(rev)?;
@@ -54,7 +55,7 @@ fn commit() -> Result<(), Error> {
#[test]
fn commit_parents() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
let history = repo.history(rev)?;
let commit = history.head();
@@ -69,7 +70,7 @@ fn commit_parents() -> Result<(), Error> {
#[test]
fn commit_short() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let rev = repo.oid("3873745c8")?;
let mut history = repo.history(rev)?;
@@ -81,7 +82,7 @@ fn commit_short() -> Result<(), Error> {
#[test]
fn tag() -> Result<(), Error> {
- let repo = Repository::open(GIT_PLATINUM)?;
+ let repo = Repository::open(platinum::get())?;
let rev = refname!("refs/tags/v0.2.0");
let history = repo.history(&rev)?;
diff --git a/crates/radicle-surf/src/test/roundtrip.rs b/crates/radicle-surf/src/test/roundtrip.rs
new file mode 100644
index 000000000..3a054d6e8
--- /dev/null
+++ b/crates/radicle-surf/src/test/roundtrip.rs
@@ -0,0 +1,12 @@
+use std::fmt::Debug;
+
+#[cfg(feature = "serde")]
+pub fn json<A>(a: A)
+where
+ for<'de> A: Debug + PartialEq + serde::Serialize + serde::Deserialize<'de>,
+{
+ assert_eq!(
+ a,
+ serde_json::from_str(&serde_json::to_string(&a).unwrap()).unwrap()
+ )
+}
diff --git a/crates/radicle-surf/src/test/source.rs b/crates/radicle-surf/src/test/source.rs
index 4b97714ab..6b8918016 100644
--- a/crates/radicle-surf/src/test/source.rs
+++ b/crates/radicle-surf/src/test/source.rs
@@ -1,14 +1,12 @@
use std::path::PathBuf;
-use radicle_git_ext::ref_format::refname;
-use radicle_surf::{Branch, Glob, Repository};
+use crate::{Branch, Glob, Repository, test::platinum};
+use radicle_git_ref_format::refname;
use serde_json::json;
-const GIT_PLATINUM: &str = "../data/git-platinum";
-
#[test]
fn tree_serialization() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let tree = repo.tree(refname!("refs/heads/master"), &"src").unwrap();
let expected = json!({
@@ -59,7 +57,7 @@ fn tree_serialization() {
#[test]
fn test_tree_last_commit() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let tree = repo.tree(refname!("refs/heads/master"), &"src").unwrap();
let last_commit = tree.last_commit(&repo).unwrap();
assert_ne!(*tree.commit(), last_commit);
@@ -71,7 +69,7 @@ fn test_tree_last_commit() {
#[test]
fn repo_tree_empty_branch() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let rev = Branch::local(refname!("empty-branch"));
let tree = repo.tree(rev, &"").unwrap();
assert_eq!(tree.entries().len(), 0);
@@ -85,7 +83,7 @@ fn repo_tree_empty_branch() {
#[test]
fn repo_tree() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let tree = repo
.tree("27acd68c7504755aa11023300890bb85bbd69d45", &"src")
.unwrap();
@@ -132,7 +130,7 @@ fn repo_tree() {
#[test]
fn repo_blob() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let blob = repo
.blob("27acd68c7504755aa11023300890bb85bbd69d45", &"src/memory.rs")
.unwrap();
@@ -171,7 +169,7 @@ fn repo_blob() {
#[test]
fn tree_ordering() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let tree = repo
.tree(refname!("refs/heads/master"), &PathBuf::new())
.unwrap();
@@ -195,7 +193,7 @@ fn tree_ordering() {
#[test]
fn commit_branches() {
- let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let repo = Repository::open(platinum::get()).unwrap();
let init_commit = "d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3";
let glob = Glob::all_heads().branches().and(Glob::all_remotes());
let branches = repo.revision_branches(init_commit, glob).unwrap();
diff --git a/crates/radicle-surf/src/test/submodule.rs b/crates/radicle-surf/src/test/submodule.rs
index 62129b8f6..a2dd637a7 100644
--- a/crates/radicle-surf/src/test/submodule.rs
+++ b/crates/radicle-surf/src/test/submodule.rs
@@ -1,17 +1,19 @@
use std::{convert::Infallible, path::Path};
use proptest::{collection, proptest};
-use radicle_git_ext::commit::CommitData;
-use radicle_git_ext::ref_format::refname;
-use radicle_git_ext_test::gen;
-use radicle_surf::tree::EntryKind;
-use radicle_surf::{fs, Branch, Repository};
+use radicle_git_metadata::commit::CommitData;
+use radicle_git_ref_format::refname;
+
+use crate::tree::EntryKind;
+use crate::{Branch, Repository, fs};
+
+use super::r#gen;
proptest! {
#[test]
fn test_submodule(
- initial in gen::commit::commit(),
- commits in collection::vec(gen::commit::commit(), 1..5)
+ initial in r#gen::commit::commit(),
+ commits in collection::vec(r#gen::commit::commit(), 1..5)
) {
prop::test_submodule(initial, commits)
}
@@ -19,8 +21,8 @@ proptest! {
#[ignore = "segfault"]
#[test]
fn test_submodule_bare(
- initial in gen::commit::commit(),
- commits in collection::vec(gen::commit::commit(), 1..5)
+ initial in r#gen::commit::commit(),
+ commits in collection::vec(r#gen::commit::commit(), 1..5)
) {
prop::test_submodule_bare(initial, commits)
}
@@ -28,7 +30,8 @@ proptest! {
}
mod prop {
- use radicle_git_ext_test::{gen::commit, repository};
+ use crate::test::r#gen::commit;
+ use crate::test::repository;
use super::*;
@@ -39,12 +42,11 @@ mod prop {
let refname = refname!("refs/heads/master");
let author = git2::Signature::try_from(initial.author()).unwrap();
- let submodule = repository::fixture(&refname, commits).unwrap();
- let repo = repository::fixture(&refname, vec![initial]).unwrap();
+ let submodule = repository::fixture(&refname, commits);
+ let repo = repository::fixture(&refname, vec![initial]);
let head = repo.head.expect("missing initial commit");
- let sub =
- repository::submodule(&repo.inner, &submodule.inner, &refname, head, &author).unwrap();
+ let sub = repository::submodule(&repo.inner, &submodule.inner, &refname, head, &author);
let repo = Repository::open(repo.inner.path()).unwrap();
let branch = Branch::local(refname);
@@ -65,12 +67,11 @@ mod prop {
let refname = refname!("refs/heads/master");
let author = git2::Signature::try_from(initial.author()).unwrap();
- let submodule = repository::fixture(&refname, commits).unwrap();
- let repo = repository::bare_fixture(&refname, vec![initial]).unwrap();
+ let submodule = repository::fixture(&refname, commits);
+ let repo = repository::bare_fixture(&refname, vec![initial]);
let head = repo.head.expect("missing initial commit");
- let sub =
- repository::submodule(&repo.inner, &submodule.inner, &refname, head, &author).unwrap();
+ let sub = repository::submodule(&repo.inner, &submodule.inner, &refname, head, &author);
let repo = Repository::open(repo.inner.path()).unwrap();
let branch = Branch::local(refname);
diff --git a/crates/radicle-surf/src/test/threading.rs b/crates/radicle-surf/src/test/threading.rs
index 47cca4859..d3c798c1d 100644
--- a/crates/radicle-surf/src/test/threading.rs
+++ b/crates/radicle-surf/src/test/threading.rs
@@ -1,13 +1,14 @@
use std::sync::{Mutex, MutexGuard};
-use radicle_git_ext::ref_format::{name::component, refname};
-use radicle_surf::{Branch, Error, Glob, Repository};
+use radicle_git_ref_format::{component, refname};
-use super::GIT_PLATINUM;
+use crate::{Branch, Error, Glob, Repository};
+
+use super::platinum;
#[test]
fn basic_test() -> Result<(), Error> {
- let shared_repo = Mutex::new(Repository::open(GIT_PLATINUM)?);
+ let shared_repo = Mutex::new(Repository::open(platinum::get())?);
let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
let mut branches = locked_repo
.branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
diff --git a/crates/radicle-surf/src/tree.rs b/crates/radicle-surf/src/tree.rs
index 2f691d71c..8e65ad349 100644
--- a/crates/radicle-surf/src/tree.rs
+++ b/crates/radicle-surf/src/tree.rs
@@ -4,15 +4,15 @@
use std::cmp::Ordering;
use std::path::PathBuf;
-use radicle_git_ext::Oid;
+use radicle_oid::Oid;
#[cfg(feature = "serde")]
use serde::{
- ser::{SerializeStruct as _, Serializer},
Serialize,
+ ser::{SerializeStruct as _, Serializer},
};
use url::Url;
-use crate::{fs, Commit, Error, Repository};
+use crate::{Commit, Error, Repository, fs};
/// Represents a tree object as in git. It is essentially the content of
/// one directory. Note that multiple directories can have the same content,
commit e880810e48d7f7d740aafdd33898d43b143a7022
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Thu Jul 23 18:28:50 2026 +0200
surf: Move test files into place
In the radicle-git workspace, crates brought their own testing crates
along with them, usually in a subdirectory named "t". In the heartwood
workspace we use tests within the same crate.
This commit only moves files, so that it is easier to follow changes to
them in the upcoming commit.
diff --git a/crates/radicle-surf/t/src/branch.rs b/crates/radicle-surf/src/test/branch.rs
similarity index 100%
rename from crates/radicle-surf/t/src/branch.rs
rename to crates/radicle-surf/src/test/branch.rs
diff --git a/crates/radicle-surf/t/src/code_browsing.rs b/crates/radicle-surf/src/test/code_browsing.rs
similarity index 100%
rename from crates/radicle-surf/t/src/code_browsing.rs
rename to crates/radicle-surf/src/test/code_browsing.rs
diff --git a/crates/radicle-surf/t/src/commit.rs b/crates/radicle-surf/src/test/commit.rs
similarity index 100%
rename from crates/radicle-surf/t/src/commit.rs
rename to crates/radicle-surf/src/test/commit.rs
diff --git a/crates/radicle-surf/t/src/diff.rs b/crates/radicle-surf/src/test/diff.rs
similarity index 100%
rename from crates/radicle-surf/t/src/diff.rs
rename to crates/radicle-surf/src/test/diff.rs
diff --git a/crates/radicle-surf/t/src/file_system.rs b/crates/radicle-surf/src/test/file_system.rs
similarity index 100%
rename from crates/radicle-surf/t/src/file_system.rs
rename to crates/radicle-surf/src/test/file_system.rs
diff --git a/crates/radicle-surf/t/src/last_commit.rs b/crates/radicle-surf/src/test/last_commit.rs
similarity index 100%
rename from crates/radicle-surf/t/src/last_commit.rs
rename to crates/radicle-surf/src/test/last_commit.rs
diff --git a/crates/radicle-surf/t/src/lib.rs b/crates/radicle-surf/src/test/lib.rs
similarity index 100%
rename from crates/radicle-surf/t/src/lib.rs
rename to crates/radicle-surf/src/test/lib.rs
diff --git a/crates/radicle-surf/t/src/namespace.rs b/crates/radicle-surf/src/test/namespace.rs
similarity index 100%
rename from crates/radicle-surf/t/src/namespace.rs
rename to crates/radicle-surf/src/test/namespace.rs
diff --git a/crates/radicle-surf/t/src/reference.rs b/crates/radicle-surf/src/test/reference.rs
similarity index 100%
rename from crates/radicle-surf/t/src/reference.rs
rename to crates/radicle-surf/src/test/reference.rs
diff --git a/crates/radicle-surf/t/src/rev.rs b/crates/radicle-surf/src/test/rev.rs
similarity index 100%
rename from crates/radicle-surf/t/src/rev.rs
rename to crates/radicle-surf/src/test/rev.rs
diff --git a/crates/radicle-surf/t/src/source.rs b/crates/radicle-surf/src/test/source.rs
similarity index 100%
rename from crates/radicle-surf/t/src/source.rs
rename to crates/radicle-surf/src/test/source.rs
diff --git a/crates/radicle-surf/t/src/submodule.rs b/crates/radicle-surf/src/test/submodule.rs
similarity index 100%
rename from crates/radicle-surf/t/src/submodule.rs
rename to crates/radicle-surf/src/test/submodule.rs
diff --git a/crates/radicle-surf/t/src/threading.rs b/crates/radicle-surf/src/test/threading.rs
similarity index 100%
rename from crates/radicle-surf/t/src/threading.rs
rename to crates/radicle-surf/src/test/threading.rs
diff --git a/crates/radicle-surf/t/Cargo.toml b/crates/radicle-surf/t/Cargo.toml
deleted file mode 100644
index 7f9470947..000000000
--- a/crates/radicle-surf/t/Cargo.toml
+++ /dev/null
@@ -1,37 +0,0 @@
-[package]
-name = "radicle-surf-test"
-version = "0.1.0"
-edition.workspace = true
-license.workspace = true
-
-publish = false
-
-[lib]
-test = true
-
-[features]
-test = []
-
-[dev-dependencies]
-nonempty = "0.5"
-pretty_assertions = "1.3.0"
-proptest = "1"
-serde_json = "1"
-url = "2.5"
-
-[dev-dependencies.git2]
-workspace = true
-
-[dev-dependencies.radicle-git-ext]
-path = "../../radicle-git-ext"
-
-[dev-dependencies.radicle-git-ext-test]
-path = "../../radicle-git-ext/t"
-features = ["test"]
-
-[dev-dependencies.radicle-surf]
-path = ".."
-features = ["serde"]
-
-[dev-dependencies.test-helpers]
-path = "../../test/test-helpers"
commit 0ef1f0714dc940787cc74a04daee57eec06d5def
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sat Jan 10 15:28:52 2026 +0100
surf: Copy over from radicle-git
This is the result of a simple
cp -r ../radicle-git/radicle-surf/ crates
rm -r crates/radicle-surf/{data,scripts,build.rs}
with the radicle-git workspace at commit
8c5df5ecbd3f2507d323608b52e1fbf7b177d864.
diff --git a/Cargo.toml b/Cargo.toml
index 3c25da833..ff5b6c0fc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,8 @@
members = ["crates/*"]
resolver = "2"
+exclude = ["crates/radicle-surf"] # TODO: Remove exclusion.
+
[workspace.package]
edition = "2024"
license = "MIT OR Apache-2.0"
diff --git a/crates/radicle-surf/CHANGELOG.md b/crates/radicle-surf/CHANGELOG.md
new file mode 100644
index 000000000..7399d3b7d
--- /dev/null
+++ b/crates/radicle-surf/CHANGELOG.md
@@ -0,0 +1,37 @@
+# CHANGELOG
+
+## 0.28.0
+
+- [`c92d2898`] Update `Oid` parsing to use the new `git2::ObjectFormat` type.
+
+[`c92d2898`]: https://app.radicle.xyz/nodes/seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt/commits/c92d2898a84b0d35f5a36d6af8c1b51757fd6436
+
+## 0.27.0
+
+### Fixed
+
+- `radicle-surf` has learned how to handle directories that use `[]` in their
+ name. This was a limitation in how the `git2` library was used for searching
+ "pathspecs".
+
+## 0.25.0
+
+* Update to `radicle-git-ext-0.10.0` [6422fd5](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt/commits/6422fd580b1c9c96ba40620197e29d7b9fbe2824)
+
+## 0.9.0
+
+This release consists of a major rewrite of this crate. Its API is overall
+simplified and is not compatible with the previous version (v0.8.0). The main
+changes include:
+
+- `Browser` is removed. Its methods are implemented directly with `Repository`.
+- Git will be the only supported VCS. Any extension points for other VCSes were
+removed.
+- `Ref` and `RefScope` are removed. Re-use the `git-ref-format` crate and a new
+`Glob` type for the refspec patterns.
+- Added support of `Tree` and `Blob` that correspond to their definitions in
+Git.
+- Added two new traits `Revision` and `ToCommit` that make methods flexible and
+still simple to use.
+
+For more details, please check out the crate's documentation.
diff --git a/crates/radicle-surf/Cargo.toml b/crates/radicle-surf/Cargo.toml
new file mode 100644
index 000000000..d9029d0b6
--- /dev/null
+++ b/crates/radicle-surf/Cargo.toml
@@ -0,0 +1,59 @@
+[package]
+name = "radicle-surf"
+description = "A code surfing library for Git repositories"
+readme = "README.md"
+version = "0.28.0"
+authors = ["The Radicle Team <dev@radicle.xyz>"]
+homepage.workspace = true
+repository.workspace = true
+edition.workspace = true
+license.workspace = true
+rust-version.workspace = true
+
+include = [
+ "**/*.rs",
+ "Cargo.toml",
+ "data/git-platinum.tgz",
+]
+
+[lib]
+test = false
+doctest = false
+
+[features]
+# NOTE: testing `test_submodule_failure` on GH actions
+# is painful since it uses this specific repo and expects
+# certain branches to be setup. So we use this feature flag
+# to ignore the test on CI.
+gh-actions = []
+minicbor = ["radicle-git-ext/minicbor"]
+serde = ["dep:serde", "url/serde"]
+
+[dependencies]
+base64 = "0.21"
+log = "0.4"
+nonempty = "0.9"
+thiserror = "1.0"
+url = "2.5.4"
+
+[dependencies.git2]
+workspace = true
+
+[dependencies.radicle-git-ext]
+version = "0.13.0"
+path = "../radicle-git-ext"
+features = ["serde"]
+
+[dependencies.radicle-std-ext]
+version = "0.2.0"
+path = "../radicle-std-ext"
+
+[dependencies.serde]
+version = "1"
+features = ["serde_derive"]
+optional = true
+
+[build-dependencies]
+anyhow = "1.0"
+flate2 = "1.1"
+tar = "0.4.45"
diff --git a/crates/radicle-surf/DEVELOPMENT.md b/crates/radicle-surf/DEVELOPMENT.md
new file mode 100644
index 000000000..a3df0fa74
--- /dev/null
+++ b/crates/radicle-surf/DEVELOPMENT.md
@@ -0,0 +1,78 @@
+
+# Radicle Surfing 🏄
+
+Thanks for wanting to contribute to `radicle-surf`!
+
+## Building & Testing 🏗️
+
+We try to make development as seemless as possible so we can get down to the real work. We supply
+the toolchain via the `rust-toolchain` file, and the formatting rules `.rustmt.toml` file.
+
+For the [Nix](https://nixos.org/) inclined there is a `default.nix` file to get all the necessary
+dependencies and it also uses the `rust-toolchain` file to pin to that version of Rust.
+
+You can build the project the usual way:
+```
+cargo build
+```
+
+To run all the tests:
+```
+cargo test
+```
+
+For the full list of checks that get executed in CI you can checkout the [ci/run](./ci/run) script.
+
+If any of this _isn't_ working, then let's work through it together and get it Working on Your
+Machine™.
+
+## Structure 🏛️
+
+The design of `radicle-surf` is to have an in-memory representation of a project's directory which
+can be generated by a VCS's backend. The directory system is modeled under `file_system`, the VCS
+functionality is naturally under `vcs`, and `diff` logic is held under `diff`.
+
+```
+src/
+├── diff
+├── file_system
+└── vcs
+```
+
+## Testing & Documentation 📚
+
+We ensure that the crate is well documented. `cargo clippy` will argue with you anytime a public
+facing piece of the library is undocumented. We should always provide an explanation of what
+something is or does, and also provide examples to allow our users to get up and running as quick
+and easy as possible.
+
+When writing documentation we should try provide one or two examples (if they make sense). This
+provides us with some simple unit tests as well as something our users can copy and paste for ease
+of development.
+
+If more tests are needed then we should add them under `mod tests` in the relevant module. We strive
+to find properties of our programs so that we can use tools like `proptest` to extensively prove our
+programs are correct. As well as this, we add unit tests to esnure the examples in our heads are
+correct, and testing out the ergonomics of our API first-hand.
+
+## CI files 🤖
+
+Our CI infrastructure runs on Buildkite. The build process is run for every commit which is pushed
+to GitHub.
+
+All relevant configuration can be found here:
+
+```
+radicle-surf/.buildkite/
+├── docker
+│ ├── build
+│ │ └── Dockerfile
+│ └── rust-nightly
+│ └── Dockerfile
+└── pipeline.yaml
+```
+
+## Releases 📅
+
+TODO: Once we get the API into a good shape we will keep track of releases via a `CHANGELOG.md` and
+tag the releases via `git tag`.
diff --git a/crates/radicle-surf/README.md b/crates/radicle-surf/README.md
new file mode 100644
index 000000000..405e75585
--- /dev/null
+++ b/crates/radicle-surf/README.md
@@ -0,0 +1,22 @@
+# radicle-surf
+
+A code surfing library for Git repositories 🏄♀️🏄♂️
+
+Welcome to `radicle-surf`!
+
+`radicle-surf` is a library to describe a Git repository as a file system. It
+aims to provide an easy-to-use API to browse a repository via the concept of
+files and directories for any given revision. It also allows the user to diff
+any two different revisions.
+
+One of the use cases would be to create a web GUI for interacting with a Git
+repository (thinking GitHub, GitLab or similar systems).
+
+## Contributing
+
+To get started on contributing you can check out our [developing guide](../DEVELOPMENT.md), and also
+our [LICENSE](../LICENSE) file.
+
+## The Community
+
+Join our community disccussions at [radicle.community](https://radicle.community)!
diff --git a/crates/radicle-surf/docs/denotational-design.md b/crates/radicle-surf/docs/denotational-design.md
new file mode 100644
index 000000000..88a2c41e6
--- /dev/null
+++ b/crates/radicle-surf/docs/denotational-design.md
@@ -0,0 +1,265 @@
+# Design Documentation
+
+In this document we will describe the design of `radicle-surf`. The design of the system will rely
+heavily on [denotational design](todo) and use Haskell syntax (because types are easy to reason about, I'm sorry).
+
+`radicle-surf` is a system to describe a file-system in a VCS world. We have the concept of files and directories,
+but these objects can change over time while people iterate on them. Thus, it is a file-system within history and
+we, the user, are viewing the file-system at a particular snapshot. Alongside this, we will wish to take two snapshots
+and view their differences.
+
+The stream of consciousness that gave birth to this document started with thinking how the user would interact with
+the system, identifying the key components. This is captured in [User Flow](#user-flow). From there we found nouns that
+represent objects in our system and verbs that represent functions over those objects. This iteratively informed us as
+to what other actions we would need to supply. We would occassionally look at [GitHub](todo) and [Pijul Nest](todo) for
+inspiration, since we would like to imitate the features that they supply, and we ultimately want use one or both of
+these for our backends.
+
+## User Flow
+
+For the user flow we imagined what it would be like if the user was using a [REPL](todo) to interact with `radicle-surf`.
+The general concept was that the user would enter the repository, build a view of the directory structure, and then
+interact with the directories and files from there (called `browse`).
+```haskell
+repl :: IO ()
+repl = do
+ repo <- getRepo
+ history <- getHistory label repo -- head is SHA1, tail is rest
+ directory <- buildDirectory history
+
+ forever browse directory
+```
+
+But then we thought about what happens when we are in `browse` but we would like to change the history and see that
+file or directory at a different snapshot. This was captured in the pseudo-code below:
+```haskell
+ src_foo_bar <- find...
+ history' <- historyOf src_foo_bar
+```
+
+This information was enough for us to begin the [denotational design](#denotational-design) below.
+
+## Denotational Design
+
+```haskell
+-- A Label is a name for a directory or a file
+type Label
+μ Label = Text
+
+-- A Directory captures its own Label followed by 1 or more
+-- artefacts which can either be sub-directories or files.
+--
+-- An example of "foo/bar.hs" structure:
+-- foo
+-- |-- bar.hs
+--
+-- Would look like:
+-- @("foo", Right ("bar.hs", "module Banana ...") :| [])@
+type Directory
+μ Directory = (Label, NonEmpty (Either Directory File))
+
+-- DirectoryContents can either be the special IsRepo object,
+-- a Directory, or a File.
+type DirectoryContents
+μ DirectoryContents = IsRepo | Directory | File
+
+-- Opaque representation of repository state directories (e.g. `.git`, `.pijul`)
+-- Those are not browseable, but have to be present at the repo root 'Directory'.
+type IsRepo
+
+-- A Directory captures its own Label followed by 1 or more DirectoryContents
+--
+-- An example of "foo/bar.hs" structure:
+-- foo
+-- |-- bar.hs
+--
+-- Would look like:
+-- @("~", IsRepo :| [Directory ("foo", File ("bar.hs", "module Banana ..") :| [])]
+-- where IsRepo is the implicit root of the repository.
+type Directory
+μ Directory = (Label, NonEmpty DirectoryContents)
+
+-- A File is its Label and its contents
+type File
+μ File = (Label, ByteString)
+
+-- An enumeration of what file-system artefact we're looking at.
+-- Useful for listing a directory and denoting what the label is
+-- corresponding to.
+type SystemType
+μ SystemType
+ = IsFile
+ | IsDirectory
+
+-- A Chnage is an enumeration of how a file has changed.
+-- This is simply used for getting the difference between two
+-- directories.
+type Change
+
+-- Constructors of Change - think GADT
+AddLineToFile :: NonEmpty Label -> Location -> ByteString -> Change
+RemoveLineFromFile :: NonEmpty Label -> Location -> Change
+MoveFile :: NonEmpty Label -> NonEmpty Label -> Change
+CreateFile :: NonEmpty Label -> Change
+DeleteFile :: NonEmpty Label -> Change
+
+-- A Diff is a set of Changes that were made
+type Diff
+μ Diff = [Change]
+
+-- History is an ordered set of @a@s. The reason for it being
+-- polymorphic is that it allows us to choose what set artefact we
+-- want to carry around.
+--
+-- For example:
+-- * In `git` this would be a `Commit`.
+-- * In `pijul` it would be a `Patch`.
+type History a
+μ History = NonEmpty a
+
+-- A Repo is a collection of multiple histories.
+-- This would essentially boil down to branches and tags.
+type Repo
+μ Repo a = [History a]
+
+-- A Snapshot is a way of converting a History into a Directory.
+-- In other words it gives us a snapshot of the history in the form of a directory.
+type Snapshot a
+μ Snapshot a = History a -> Directory
+
+-- For example, we have a `git` snapshot or a `pjul` snapshot.
+type Commit
+type GitSnapshot = Snapshot Commit
+
+type Patch
+type PijulSnapshot = Snapshot Patch
+
+-- This is piece de resistance of the design! It turns out,
+-- everything is just a Monad after all.
+--
+-- Our code Browser is a stateful computation of what History
+-- we are currently working with and how to get a Snapshot of it.
+type Browser a b
+μ type Browser a b = ReaderT (Snapshot a) (State (History a) b)
+
+-- A function that will retrieve a repository given an
+-- identifier. In our case the identifier is opaque to the system.
+getRepo :: Repo -> Repo
+
+-- Find a particular History in the Repo. Again, how these things
+-- are equated and found is opaque, but we can think of them as
+-- branch or tag labels.
+getHistory :: Eq a => History a -> Repo a -> Maybe (History a)
+μ getHistory history repo =
+ find history (μ repo)
+
+-- Find if a particular artefact occurs in 0 or more histories.
+findInHistories :: a -> [History a] -> [History a]
+μ findInHistories a histories =
+ filterMaybe (findInHistory a) histories
+
+-- Find a particular artefact is in a history.
+findInHistory :: Eq a => a -> History a -> Maybe a
+μ findInHistory a history = find (== a) (μ history)
+
+-- A special Label that guarantees a starting point, i.e. ~
+rootLabel :: Label
+μ rootLabel = "~"
+
+emptyRepoRoot :: Directory
+μ emptyRepoRoot = (rootLabel, IsRepo :| [])
+
+-- Get the difference between two directory views.
+diff :: Directory -> Directory -> Diff
+
+-- List the current file or directories in a given Directory view.
+listDirectory :: Directory -> [Label, SystemType]
+μ listDirectory directory = foldMap toLabel $ snd (μ directory)
+ where
+ toLabel content = case content of
+ File (label, _) -> [(label, IsFile)]
+ Directory (label, _) -> [(label, IsDirectory)]
+ IsRepo -> []
+
+fileName :: File -> Label
+μ fileName file = fst (μ file)
+
+findFile :: NonEmpty Label -> Directory -> Maybe File
+μ findFile (label :| labels) (Directory (label', contents)) =
+ if label == label' then go labels contents else Nothing
+ where
+ findFileWithLabel :: Foldable f => Label -> f DirectoryContents -> Maybe File
+ findFileWithLabel label = find (\artefact -> case content of
+ File (fileLabel, _) -> fileLabel == label
+ Directory _ -> False
+ IsRepo -> False)
+
+ go :: [Label] -> NonEmpty DirectoryContents -> Just File
+ go [] _ = Nothing
+ go [label] contents = findMaybe (fileWithLabel label) contents
+ go (label:labels) contents = (go labels . snd) <$> find ((label ==) . fst) onlyDirectories contents
+
+onlyDirectories :: Foldable f => f DirectoryContents -> [Directory]
+μ onlyDirectories = fmapMaybe (\content -> case content of
+ d@(Directory _) -> Just d
+ File _ -> Nothing
+ IsRepo -> Nothing) . toList
+
+getSubDirectories :: Directory -> [Directory]
+μ getSubDirectories directory = foldMap f $ snd (μ directory)
+ where
+ f :: DirectoryContents -> [Directory]
+ f = \case
+ d@(Directory _) -> [d]
+ File _ -> []
+ IsRepo -> []
+
+-- Definition elided
+findDirectory :: NonEmpty Label -> Directory -> Maybe Directory
+
+-- Definition elided
+fuzzyFind :: Label -> [Directory]
+
+-- A Git Snapshot is grabbing the HEAD commit of your History
+-- and turning it into a Directory
+gitSnapshot :: Snapshot Commit
+μ gitSnapshot commits = second (\root -> root <> getDirectoryPtr $ Nel.head commits) emptyRepoRoot
+
+-- Opaque and defined by the backend
+getDirectoryPtr :: Commit -> Directory
+
+-- A Pijul history is semantically applying the patches in a
+-- topological order and achieving the Directory view.
+pijulHistory :: Snapshot Patch
+μ pijulHistory = foldl pijulMagic emptyRepoRoot
+
+-- Opaque and defined by the backend
+pijulMagic :: Patch -> Directory -> Directory
+
+-- Get the current History we are working with.
+getHistory :: Browser a (History a)
+μ getHistory = get
+
+setHistory :: History a -> Browser a ()
+μ setHistory = put
+
+-- Get the current Directory in the Browser
+getDirectory :: Browser a Directory
+μ getDirectory = do
+ hist <- get
+ applySnapshot <- ask
+ pure $ applySnapshot hist
+
+-- We modify the history by changing the internal history state.
+switchHistory :: (History a -> History a) -> Browser a b
+μ switchHistory f = modify f
+
+-- | Find the suffix of a History.
+findSuffix :: Eq a => a -> History a -> Maybe (History a)
+μ findSuffix a = nonEmpty . Nel.dropWhile (/= a)
+
+-- View the history up to a given point by supplying a function to modify
+-- the state. If this operation fails, then the default value is used.
+viewAt :: (History a -> Maybe (History a)) -> History a -> Browser a b
+μ viewAt f def = switchHistory (fromMaybe def . f)
+```
diff --git a/crates/radicle-surf/docs/refactor-design.md b/crates/radicle-surf/docs/refactor-design.md
new file mode 100644
index 000000000..aac67c084
--- /dev/null
+++ b/crates/radicle-surf/docs/refactor-design.md
@@ -0,0 +1,341 @@
+# An updated design for radicle-surf
+
+This is a design blueprint for the new `radicle-git/radicle-surf` crate. The
+actual design details and implemenation are described and updated in its
+documentation comments, viewable via `cargo doc`.
+
+## Introduction
+
+In September 2022, we have ported the [`radicle-surf` crate](https://github.com/radicle-dev/radicle-surf)
+from its own github repo to be part of the [`radicle-git` repo](https://github.com/radicle-dev/radicle-git).
+We are taking this opportunity to refactor its design as well. Intuitively,
+`radicle-surf` provides an API so that one can use it to create a GitHub-like
+UI for a git repo:
+
+1. Code browsing: given a specific commit/ref, browse files and directories.
+2. Diff between two revisions that resolve into two commits.
+3. Retrieve the history of commits with a given head, and optionally a file.
+4. List refs and retrieve their metadata: Branches, Tags, Remotes,
+Notes and user-defined "categories", where a category is: `refs/<category>/<...>`.
+
+## Motivation
+
+The `radicle-surf` crate aims to provide a safe and easy-to-use API that
+supports the features listed in [Introduction](#introduction). Based on the
+existing API, the main goals of the refactoring are:
+
+- API review: identify the issues with the current API.
+- Updated API: propose an updated API that reuses parts of the existing API.
+- Address open issues in the original `radicle-surf` repo.
+- Be `git` specific. (i.e. no need to support other VCS systems)
+- Remove `git2` from the public API. The use of `git2` should be an
+implementation detail.
+
+## API review
+
+In this section, we review some core types in the current API and propose
+changes to them. The main theme is to make the API simpler and easier to use.
+
+### Remove the `Browser` type
+
+The type [`Browser`](https://github.com/radicle-dev/radicle-surf/blob/b85d2183d786e5fa447aab9d2f420a32f1061bfa/surf/src/vcs.rs#L145) is awkward:
+
+- it is not a source of truth of any information. For example, `list_branches`
+method is just a wrapper of `Repository::list_branches`.
+- it takes in `History`, but really operates at the `Snapshot` level.
+- it is mutable but its state mutations are not used much.
+
+Can we just remove `Browser` and implement its functionalities using other
+types?
+
+- For iteratoring the history, use `History`.
+- For generating `Directory`, use `Repository` directly given a `Rev`.
+- For accessing `Branch`, `Tag` or `Commit`, use `Repository`.
+
+### Remove the `Snapshot` type
+
+A [`Snapshot`](https://github.com/radicle-dev/radicle-surf/blob/b85d2183d786e5fa447aab9d2f420a32f1061bfa/surf/src/vcs.rs#L140) should be really
+just a tree (or `Directory`) of a `Commit` in git. Currently it is a function
+that returns a `Directory`. As we are moving to be git specific, we don't need
+to have this generic function to create a snapshot across different VCS systems.
+
+The snapshot function can be easily implement as a method of `RepositoryRef`.
+
+### Simplify `Directory` and remove the `Tree` and `Forest` types
+
+The [`Directory`](https://github.com/radicle-dev/radicle-surf/blob/b85d2183d786e5fa447aab9d2f420a32f1061bfa/surf/src/file_system/directory.rs#L144)
+type represents the file system view of a snapshot. Its field
+`sub_directories` is defined a `Forest` based on `Tree`. The types are
+over-engineered from such a simple concept. We could refactor `Directory` to
+use `DirectoryContents` for its items and not to use `Tree` or `Forest` at all.
+
+We also found the `list_directory()` method duplicates with `iter()` method.
+Hence `list_directory()` is removed, together with `SystemType` type.
+
+### Remove `Vcs` trait
+
+The `Vcs` trait was introduced to support different version control backends,
+for example both Git and Pijul, and potentially others. However, since this
+port is part of `radicle-git` repo, we are only supporting Git going forward.
+We no longer need another layer of indirection defined by `Vcs` trait.
+
+## The new API
+
+With the changes proposed in the previous section, we describe what the new API
+would look like and how they meet the requirements.
+
+### Basic types
+
+#### Repository
+
+`Repository` is kept as the entry point of the API, even though its methods
+would change due to changes in other types. Also, we would like to consolidate
+[`Repository`](https://github.com/radicle-dev/radicle-surf/blob/b85d2183d786e5fa447aab9d2f420a32f1061bfa/surf/src/vcs/git/repo.rs#L53) and
+ [`RepositoryRef`](https://github.com/radicle-dev/radicle-surf/blob/b85d2183d786e5fa447aab9d2f420a32f1061bfa/surf/src/vcs/git/repo.rs#L63) to
+ simplify the API.
+
+#### Revision and Commit
+
+In Git, `Revision` commonly resolves into a `Commit` but could refer to other
+objects for example a `Blob`. Hence we need to keep both concepts in the API.
+Currently we have multiple types to identify a `Commit` or `Revision`.
+
+- Commit
+- Oid
+- Rev
+
+The relations between them are: all `Rev` and `Commit` can resolve into `Oid`,
+and in most cases `Rev` can resolve into `Commit`.
+
+On one hand, `Oid` is the ultimate unique identifer but it is more machine-
+friendly than human-friendly. On the other hand, `Revision` is most human-
+friendly and better suited in the API interface. A conversion from `Revision`
+to `Oid` will be useful.
+
+For the places where `Commit` is required, we should explicitly ask for
+`Commit` instead of `Revision`.
+
+In conclusion, we define two new traits to support the use of `Revision` and
+`Commit`:
+
+```Rust
+pub trait Revision {
+ /// Resolves a revision into an object id in `repo`.
+ fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
+}
+
+pub trait ToCommit {
+ /// Converts to a commit in `repo`.
+ fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error>;
+}
+```
+
+These two traits will be implemented for most common representations of
+`Revision` and `Commit`, for example `&str`, refs like `Branch`, `Tag`, etc.
+Our API will use these traits where we expect a `Revision` or a `Commit`.
+
+#### History
+
+The current `History` is generic over VCS types and also retrieves the full list
+of commits when the history is created. The VCS part can be removed and the
+history can lazy-load the list of commits by implmenting `Iterator` to support
+ potentially very long histories.
+
+We can also store the head commit with the history so that it's easy to get
+the start point and it helps to identify the history.
+
+To support getting the history of a file, we provide methods to modify a
+`History` to filter by a file path.
+
+The new `History` type would look like this:
+
+```Rust
+pub struct History<'a> {
+ repo: RepositoryRef<'a>,
+ head: Commit,
+ revwalk: git2::Revwalk<'a>,
+ filter_by: Option<FilterBy>,
+}
+
+enum FilterBy {
+ File { path: file_system::Path },
+}
+```
+
+For the methods provided by `History`, please see section [Retrieve the history](#retrieve-the-history) below.
+
+#### Commit
+
+`Commit` is a central concept in Git. In `radicle-surf` we define `Commit` type
+to represent its metadata:
+
+```Rust
+pub struct Commit {
+ /// Object Id
+ pub id: Oid,
+ /// The author of the commit.
+ pub author: Author,
+ /// The actor who committed this commit.
+ pub committer: Author,
+ /// The long form message of the commit.
+ pub message: String,
+ /// The summary message of the commit.
+ pub summary: String,
+ /// The parents of this commit.
+ pub parents: Vec<Oid>,
+}
+```
+
+To get the file system snapshot of the commit, the user should use
+`root_dir` method described in [Code browsing](#code-browsing) section.
+
+To get the diff of the commit, the user should use `diff_commit` method
+described in [Diffs](#diffs) section. Note that we might move that method to
+`Commit` type itself.
+
+### Code browsing
+
+The user should be able to browse the files and directories for any given
+commit. The core API is:
+
+- Create a root Directory:
+```Rust
+impl RepositoryRef {
+ pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error>;
+}
+```
+
+- Browse a Directory's contents:
+```Rust
+impl Directory {
+ pub fn contents(&self) -> impl Iterator<Item = &DirectoryContents>;
+}
+```
+where `DirectoryContents` supports both files and sub-directories:
+```Rust
+pub enum DirectoryContents {
+ /// The `File` variant contains the file's name and the [`File`] itself.
+ File {
+ /// The name of the file.
+ name: Label,
+ /// The file data.
+ file: File,
+ },
+ /// The `Directory` variant contains a sub-directory to the current one.
+ Directory(Directory),
+}
+```
+
+### Diffs
+
+The user would be able to create a diff between any two revisions. In the first
+implementation, these revisions have to resolve into commits. But in future,
+the revisions could refer to other objects, e.g. files (blobs).
+
+The core API is:
+
+```Rust
+impl RepositoryRef {
+ /// Returns the diff between two revisions.
+ pub fn diff<R: Revision>(&self, from: R, to: R) -> Result<Diff, Error>;
+}
+```
+
+To obtain the diff of a particular commit, we will have:
+```Rust
+impl RepositoryRef {
+ pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error>;
+}
+```
+
+### Retrieve the history
+
+The user would be able to get the list of previous commits reachable from a
+particular commit.
+
+To create a `History` from a repo with a given head:
+```Rust
+impl RepositoryRef {
+ pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error>;
+}
+```
+
+To access the `History`, the user simply iterates `History` as it implements the
+`Iterator` trait that produces `Result<Commit, Error>`.
+
+`History` provides a method to filter based on a path and other helper methods:
+
+```Rust
+impl<'a> History<'a> {
+ /// Returns the head commit of a history.
+ pub fn head(&self) -> &Commit;
+
+ // Modifies a history with a filter by `path`.
+ // This is to support getting the history of a file.
+ pub fn by_path(mut self, path: file_system::Path) -> Self;
+```
+
+- Alternative design:
+
+One potential downside of define `History` as an iterator is that:
+`history.next()` takes a mutable history object. A different design is to use
+`History` as immutable object that produces an iterator on-demand:
+
+```Rust
+pub struct History<'a> {
+ repo: RepositoryRef<'a>,
+ head: Commit,
+}
+
+impl<'a> History<'a> {
+ /// This method creats a new `RevWalk` internally and return an
+ /// iterator for all commits in a history.
+ pub fn iter(&self) -> impl Iterator<Item = Commit>;
+}
+```
+
+In this design, `History` does not keep `RevWalk` in its state. It will create
+a new one when `iter()` is called. I like the immutable interface of this design
+but did not implement it in the current code mainly because the libgit2 doc says
+[creating a new `RevWalk` is relatively expensive](https://libgit2.org/libgit2/#HEAD/group/revwalk/git_revwalk_new).
+
+### List refs and retrieve their metadata
+
+Git refs are simple names that point to objects using object IDs. In this new
+design, we no longer group different refs into a single enum. Instead, each kind
+of ref would be their own type, e.g. `Tag`, `Branch`, `Namespace`, etc.
+
+To retrieve the refs, the user would call the `<ref_type>s()` method of a
+repo, e.g. `branches()`, `tags()`. The result is an iterator of available refs,
+e.g. `Branches`.
+
+```Rust
+impl RepositoryRef {
+ /// Lists branch names with `filter`.
+ pub fn branches<G>(&self, pattern: G) -> Result<Branches, Error>
+ where
+ G: Into<Glob<Branch>>
+}
+```
+
+### Git Tree and Blob
+
+In Git, a `Blob` object represents the content of a file, and a `Tree` object
+represents the content of a listing (in a directory). A `Tree` or a `Blob` does
+not have its own name because there could be different names (paths) pointing
+to the same `Tree` or `Blob`. In contrast, a `Directory` or a `File` has names
+included and possible other attributes.
+
+In `radicle-surf` API, we would expose `Tree` and `Blob` as defined in Git, i.e.
+as the content only, with some extra helper methods, for example `commit()` that
+returns the commit that created the object.
+
+The `Repository` provides methods to retrieve `Tree` or `Blob` for a given path.
+
+## Summary
+
+This design document was created as a guideline for refactoring the
+`radicle-surf` crate when we move it into the `radicle-git` repo. As the code
+evolves, this document would not be matching the actual code exactly. However
+we can still come back to this document to learn about the history of the
+design and update it when needed.
diff --git a/crates/radicle-surf/examples/browsing.rs b/crates/radicle-surf/examples/browsing.rs
new file mode 100644
index 000000000..a5206bbdf
--- /dev/null
+++ b/crates/radicle-surf/examples/browsing.rs
@@ -0,0 +1,49 @@
+//! An example of browsing a git repo using `radicle-surf`.
+//!
+//! How to run:
+//!
+//! cargo run --example browsing <git_repo_path>
+//!
+//! This program browses the given repo and prints out the files and
+//! the directories in a tree-like structure.
+
+use radicle_surf::{
+ fs::{self, Directory},
+ Repository,
+};
+use std::{env, time::Instant};
+
+fn main() {
+ let repo_path = match env::args().nth(1) {
+ Some(path) => path,
+ None => {
+ print_usage();
+ return;
+ }
+ };
+ let repo = Repository::discover(repo_path).unwrap();
+ let now = Instant::now();
+ let head = repo.head().unwrap();
+ let root = repo.root_dir(head).unwrap();
+ print_directory(&root, &repo, 0);
+
+ let elapsed_millis = now.elapsed().as_millis();
+ println!("browse with print: {elapsed_millis} ms");
+}
+
+fn print_directory(d: &Directory, repo: &Repository, indent_level: usize) {
+ let indent = " ".repeat(indent_level * 4);
+ println!("{}{}/", &indent, d.name());
+ for entry in d.entries(repo).unwrap() {
+ match entry {
+ fs::Entry::File(f) => println!(" {}{}", &indent, f.name()),
+ fs::Entry::Directory(d) => print_directory(&d, repo, indent_level + 1),
+ fs::Entry::Submodule(s) => println!(" {}{}", &indent, s.name()),
+ }
+ }
+}
+
+fn print_usage() {
+ println!("Usage:");
+ println!("cargo run --example browsing <repo_path>");
+}
diff --git a/crates/radicle-surf/examples/diff.rs b/crates/radicle-surf/examples/diff.rs
new file mode 100644
index 000000000..9454589af
--- /dev/null
+++ b/crates/radicle-surf/examples/diff.rs
@@ -0,0 +1,102 @@
+extern crate radicle_surf;
+
+use std::{env::Args, str::FromStr, time::Instant};
+
+use radicle_git_ext::Oid;
+use radicle_surf::{diff::Diff, Repository};
+
+fn main() {
+ let options = get_options_or_exit();
+ let repo = init_repository_or_exit(&options.path_to_repo);
+ let head_oid = match options.head_revision {
+ HeadRevision::Head => repo.head().unwrap(),
+ HeadRevision::Commit(id) => Oid::from_str(&id).unwrap(),
+ };
+ let base_oid = Oid::from_str(&options.base_revision).unwrap();
+ let now = Instant::now();
+ let elapsed_nanos = now.elapsed().as_nanos();
+ let diff = repo.diff(base_oid, head_oid).unwrap();
+ print_diff_summary(&diff, elapsed_nanos);
+}
+
+fn get_options_or_exit() -> Options {
+ match Options::parse(std::env::args()) {
+ Ok(options) => options,
+ Err(message) => {
+ println!("{message}");
+ std::process::exit(1);
+ }
+ }
+}
+
+fn init_repository_or_exit(path_to_repo: &str) -> Repository {
+ match Repository::open(path_to_repo) {
+ Ok(repo) => repo,
+ Err(e) => {
+ println!("Failed to create repository: {e:?}");
+ std::process::exit(1);
+ }
+ }
+}
+
+fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
+ diff.added().for_each(|created| {
+ println!("+++ {:?}", created.path);
+ });
+ diff.deleted().for_each(|deleted| {
+ println!("--- {:?}", deleted.path);
+ });
+ diff.modified().for_each(|modified| {
+ println!("mod {:?}", modified.path);
+ });
+
+ println!(
+ "created {} / deleted {} / modified {} / total {}",
+ diff.added().count(),
+ diff.deleted().count(),
+ diff.modified().count(),
+ diff.added().count() + diff.deleted().count() + diff.modified().count()
+ );
+ println!("diff took {elapsed_nanos} nanos ");
+}
+
+struct Options {
+ path_to_repo: String,
+ base_revision: String,
+ head_revision: HeadRevision,
+}
+
+enum HeadRevision {
+ Head,
+ Commit(String),
+}
+
+impl Options {
+ fn parse(args: Args) -> Result<Self, String> {
+ let args: Vec<String> = args.collect();
+ if args.len() != 4 {
+ return Err(format!(
+ "Usage: {} <path-to-repo> <base-revision> <head-revision>\n\
+ \tpath-to-repo: Path to the directory containing .git subdirectory\n\
+ \tbase-revision: Git commit ID of the base revision (one that will be considered less recent)\n\
+ \thead-revision: Git commit ID of the head revision (one that will be considered more recent) or 'HEAD' to use current git HEAD\n",
+ args[0]));
+ }
+
+ let path_to_repo = args[1].clone();
+ let base_revision = args[2].clone();
+ let head_revision = {
+ if args[3].eq_ignore_ascii_case("HEAD") {
+ HeadRevision::Head
+ } else {
+ HeadRevision::Commit(args[3].clone())
+ }
+ };
+
+ Ok(Options {
+ path_to_repo,
+ base_revision,
+ head_revision,
+ })
+ }
+}
diff --git a/crates/radicle-surf/src/blob.rs b/crates/radicle-surf/src/blob.rs
new file mode 100644
index 000000000..427d0ca70
--- /dev/null
+++ b/crates/radicle-surf/src/blob.rs
@@ -0,0 +1,157 @@
+//! Represents git object type 'blob', i.e. actual file contents.
+//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+
+use std::ops::Deref;
+
+use radicle_git_ext::Oid;
+
+#[cfg(feature = "serde")]
+use serde::{
+ ser::{SerializeStruct as _, Serializer},
+ Serialize,
+};
+
+use crate::Commit;
+
+/// Represents a git blob object.
+///
+/// The type parameter `T` can be fulfilled by [`BlobRef`] or a
+/// [`Vec`] of bytes.
+pub struct Blob<T> {
+ id: Oid,
+ is_binary: bool,
+ commit: Commit,
+ content: T,
+}
+
+impl<T> Blob<T> {
+ pub fn object_id(&self) -> Oid {
+ self.id
+ }
+
+ pub fn is_binary(&self) -> bool {
+ self.is_binary
+ }
+
+ /// Returns the commit that created this blob.
+ pub fn commit(&self) -> &Commit {
+ &self.commit
+ }
+
+ pub fn content(&self) -> &[u8]
+ where
+ T: AsRef<[u8]>,
+ {
+ self.content.as_ref()
+ }
+
+ pub fn size(&self) -> usize
+ where
+ T: AsRef<[u8]>,
+ {
+ self.content.as_ref().len()
+ }
+}
+
+impl<'a> Blob<BlobRef<'a>> {
+ /// Returns the [`Blob`] wrapping around an underlying [`git2::Blob`].
+ pub(crate) fn new(id: Oid, git2_blob: git2::Blob<'a>, commit: Commit) -> Self {
+ let is_binary = git2_blob.is_binary();
+ let content = BlobRef { inner: git2_blob };
+ Self {
+ id,
+ is_binary,
+ content,
+ commit,
+ }
+ }
+
+ /// Converts into a `Blob` with owned content bytes.
+ pub fn to_owned(&self) -> Blob<Vec<u8>> {
+ Blob {
+ id: self.id,
+ content: self.content.to_vec(),
+ commit: self.commit.clone(),
+ is_binary: self.is_binary,
+ }
+ }
+}
+
+/// Represents a blob with borrowed content bytes.
+pub struct BlobRef<'a> {
+ pub(crate) inner: git2::Blob<'a>,
+}
+
+impl BlobRef<'_> {
+ pub fn id(&self) -> Oid {
+ self.inner.id().into()
+ }
+}
+
+impl AsRef<[u8]> for BlobRef<'_> {
+ fn as_ref(&self) -> &[u8] {
+ self.inner.content()
+ }
+}
+
+impl Deref for BlobRef<'_> {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ self.inner.content()
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<T> Serialize for Blob<T>
+where
+ T: AsRef<[u8]>,
+{
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ use base64::Engine as _;
+
+ const FIELDS: usize = 4;
+ let mut state = serializer.serialize_struct("Blob", FIELDS)?;
+ state.serialize_field("id", &self.id)?;
+ state.serialize_field("binary", &self.is_binary())?;
+
+ let bytes = self.content.as_ref();
+ match std::str::from_utf8(bytes) {
+ Ok(s) => state.serialize_field("content", s)?,
+ Err(_) => {
+ let encoded = base64::prelude::BASE64_STANDARD.encode(bytes);
+ state.serialize_field("content", &encoded)?
+ }
+ };
+ state.serialize_field("lastCommit", &self.commit)?;
+ state.end()
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for BlobRef<'_> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ use base64::Engine as _;
+
+ const FIELDS: usize = 3;
+ let mut state = serializer.serialize_struct("BlobRef", FIELDS)?;
+ state.serialize_field("id", &self.id())?;
+ state.serialize_field("binary", &self.inner.is_binary())?;
+
+ let bytes = self.as_ref();
+ match std::str::from_utf8(bytes) {
+ Ok(s) => state.serialize_field("content", s)?,
+ Err(_) => {
+ let encoded = base64::prelude::BASE64_STANDARD.encode(bytes);
+ state.serialize_field("content", &encoded)?
+ }
+ };
+ state.end()
+ }
+}
diff --git a/crates/radicle-surf/src/branch.rs b/crates/radicle-surf/src/branch.rs
new file mode 100644
index 000000000..73fe8fa2b
--- /dev/null
+++ b/crates/radicle-surf/src/branch.rs
@@ -0,0 +1,336 @@
+use std::{
+ convert::TryFrom,
+ str::{self, FromStr},
+};
+
+use git_ext::ref_format::{component, lit, Component, Qualified, RefStr, RefString};
+
+use crate::refs::refstr_join;
+
+/// A `Branch` represents any git branch. It can be [`Local`] or [`Remote`].
+///
+/// Note that if a `Branch` is created from a [`git2::Reference`] then
+/// any `refs/namespaces` will be stripped.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum Branch {
+ Local(Local),
+ Remote(Remote),
+}
+
+impl Branch {
+ /// Construct a [`Local`] branch.
+ pub fn local<R>(name: R) -> Self
+ where
+ R: AsRef<RefStr>,
+ {
+ Self::Local(Local::new(name))
+ }
+
+ /// Construct a [`Remote`] branch.
+ /// The `remote` is the remote name of the reference name while
+ /// the `name` is the suffix, i.e. `refs/remotes/<remote>/<name>`.
+ pub fn remote<R>(remote: Component<'_>, name: R) -> Self
+ where
+ R: AsRef<RefStr>,
+ {
+ Self::Remote(Remote::new(remote, name))
+ }
+
+ /// Return the short `Branch` refname,
+ /// e.g. `fix/ref-format`.
+ pub fn short_name(&self) -> &RefString {
+ match self {
+ Branch::Local(local) => local.short_name(),
+ Branch::Remote(remote) => remote.short_name(),
+ }
+ }
+
+ /// Give back the fully qualified `Branch` refname,
+ /// e.g. `refs/remotes/origin/fix/ref-format`,
+ /// `refs/heads/fix/ref-format`.
+ pub fn refname<'a>(&'a self) -> Qualified<'a> {
+ match self {
+ Branch::Local(local) => local.refname(),
+ Branch::Remote(remote) => remote.refname(),
+ }
+ }
+}
+
+impl TryFrom<&git2::Reference<'_>> for Branch {
+ type Error = error::Branch;
+
+ fn try_from(reference: &git2::Reference<'_>) -> Result<Self, Self::Error> {
+ let name = str::from_utf8(reference.name_bytes())?;
+ Self::from_str(name)
+ }
+}
+
+impl TryFrom<&str> for Branch {
+ type Error = error::Branch;
+
+ fn try_from(name: &str) -> Result<Self, Self::Error> {
+ Self::from_str(name)
+ }
+}
+
+impl FromStr for Branch {
+ type Err = error::Branch;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ let name = RefStr::try_from_str(name)?;
+ let name = match name.to_namespaced() {
+ None => name
+ .qualified()
+ .ok_or_else(|| error::Branch::NotQualified(name.to_string()))?,
+ Some(name) => name.strip_namespace_recursive(),
+ };
+
+ let (_ref, category, c, cs) = name.non_empty_components();
+
+ if category == component::HEADS {
+ Ok(Self::Local(Local::new(refstr_join(c, cs))))
+ } else if category == component::REMOTES {
+ Ok(Self::Remote(Remote::new(c, cs.collect::<RefString>())))
+ } else {
+ Err(error::Branch::InvalidName(name.into()))
+ }
+ }
+}
+
+/// A `Local` represents a local branch, i.e. it is a reference under
+/// `refs/heads`.
+///
+/// Note that if a `Local` is created from a [`git2::Reference`] then
+/// any `refs/namespaces` will be stripped.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct Local {
+ name: RefString,
+}
+
+impl Local {
+ /// Construct a new `Local` with the given `name`.
+ ///
+ /// If the name is qualified with `refs/heads`, this will be
+ /// shortened to the suffix. To get the `Qualified` name again,
+ /// use [`Local::refname`].
+ pub(crate) fn new<R>(name: R) -> Self
+ where
+ R: AsRef<RefStr>,
+ {
+ match name.as_ref().qualified() {
+ None => Self {
+ name: name.as_ref().to_ref_string(),
+ },
+ Some(qualified) => {
+ let (_refs, heads, c, cs) = qualified.non_empty_components();
+ if heads == component::HEADS {
+ Self {
+ name: refstr_join(c, cs),
+ }
+ } else {
+ Self {
+ name: name.as_ref().to_ref_string(),
+ }
+ }
+ }
+ }
+ }
+
+ /// Return the short `Local` refname,
+ /// e.g. `fix/ref-format`.
+ pub fn short_name(&self) -> &RefString {
+ &self.name
+ }
+
+ /// Return the fully qualified `Local` refname,
+ /// e.g. `refs/heads/fix/ref-format`.
+ pub fn refname<'a>(&'a self) -> Qualified<'a> {
+ lit::refs_heads(&self.name).into()
+ }
+}
+
+impl TryFrom<&git2::Reference<'_>> for Local {
+ type Error = error::Local;
+
+ fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+ let name = str::from_utf8(reference.name_bytes())?;
+ Self::from_str(name)
+ }
+}
+
+impl TryFrom<&str> for Local {
+ type Error = error::Local;
+
+ fn try_from(name: &str) -> Result<Self, Self::Error> {
+ Self::from_str(name)
+ }
+}
+
+impl FromStr for Local {
+ type Err = error::Local;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ let name = RefStr::try_from_str(name)?;
+ let name = match name.to_namespaced() {
+ None => name
+ .qualified()
+ .ok_or_else(|| error::Local::NotQualified(name.to_string()))?,
+ Some(name) => name.strip_namespace_recursive(),
+ };
+
+ let (_ref, heads, c, cs) = name.non_empty_components();
+ if heads == component::HEADS {
+ Ok(Self::new(refstr_join(c, cs)))
+ } else {
+ Err(error::Local::NotHeads(name.into()))
+ }
+ }
+}
+
+/// A `Remote` represents a remote branch, i.e. it is a reference under
+/// `refs/remotes`.
+///
+/// Note that if a `Remote` is created from a [`git2::Reference`] then
+/// any `refs/namespaces` will be stripped.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct Remote {
+ remote: RefString,
+ name: RefString,
+}
+
+impl Remote {
+ /// Construct a new `Remote` with the given `name` and `remote`.
+ ///
+ /// ## Note
+ /// `name` is expected to be in short form, i.e. not begin with
+ /// `refs`.
+ ///
+ /// If you are creating a `Remote` with a name that begins with
+ /// `refs/remotes`, use [`Remote::from_refs_remotes`] instead.
+ ///
+ /// To get the `Qualified` name, use [`Remote::refname`].
+ pub(crate) fn new<R>(remote: Component, name: R) -> Self
+ where
+ R: AsRef<RefStr>,
+ {
+ Self {
+ name: name.as_ref().to_ref_string(),
+ remote: remote.to_ref_string(),
+ }
+ }
+
+ /// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
+ ///
+ /// If the `name` is not of this form, then `None` is returned.
+ pub fn from_refs_remotes<R>(name: R) -> Option<Self>
+ where
+ R: AsRef<RefStr>,
+ {
+ let qualified = name.as_ref().qualified()?;
+ let (_refs, remotes, remote, cs) = qualified.non_empty_components();
+ (remotes == component::REMOTES).then_some(Self {
+ name: cs.collect(),
+ remote: remote.to_ref_string(),
+ })
+ }
+
+ /// Return the short `Remote` refname,
+ /// e.g. `fix/ref-format`.
+ pub fn short_name(&self) -> &RefString {
+ &self.name
+ }
+
+ /// Return the remote of the `Remote`'s refname,
+ /// e.g. `origin`.
+ pub fn remote(&self) -> &RefString {
+ &self.remote
+ }
+
+ /// Give back the fully qualified `Remote` refname,
+ /// e.g. `refs/remotes/origin/fix/ref-format`.
+ pub fn refname<'a>(&'a self) -> Qualified<'a> {
+ lit::refs_remotes(self.remote.join(&self.name)).into()
+ }
+}
+
+impl TryFrom<&git2::Reference<'_>> for Remote {
+ type Error = error::Remote;
+
+ fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+ let name = str::from_utf8(reference.name_bytes())?;
+ Self::from_str(name)
+ }
+}
+
+impl TryFrom<&str> for Remote {
+ type Error = error::Remote;
+
+ fn try_from(name: &str) -> Result<Self, Self::Error> {
+ Self::from_str(name)
+ }
+}
+
+impl FromStr for Remote {
+ type Err = error::Remote;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ let name = RefStr::try_from_str(name)?;
+ let name = match name.to_namespaced() {
+ None => name
+ .qualified()
+ .ok_or_else(|| error::Remote::NotQualified(name.to_string()))?,
+ Some(name) => name.strip_namespace_recursive(),
+ };
+
+ let (_ref, remotes, remote, cs) = name.non_empty_components();
+ if remotes == component::REMOTES {
+ Ok(Self::new(remote, cs.collect::<RefString>()))
+ } else {
+ Err(error::Remote::NotRemotes(name.into()))
+ }
+ }
+}
+
+pub mod error {
+ use radicle_git_ext::ref_format::{self, RefString};
+ use thiserror::Error;
+
+ #[derive(Debug, Error)]
+ pub enum Branch {
+ #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
+ InvalidName(RefString),
+ #[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
+ NotQualified(String),
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] std::str::Utf8Error),
+ }
+
+ #[derive(Debug, Error)]
+ pub enum Local {
+ #[error("the refname '{0}' did not begin with 'refs/heads'")]
+ NotHeads(RefString),
+ #[error("the refname '{0}' did not begin with 'refs/heads'")]
+ NotQualified(String),
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] std::str::Utf8Error),
+ }
+
+ #[derive(Debug, Error)]
+ pub enum Remote {
+ #[error("the refname '{0}' did not begin with 'refs/remotes'")]
+ NotQualified(String),
+ #[error("the refname '{0}' did not begin with 'refs/remotes'")]
+ NotRemotes(RefString),
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] std::str::Utf8Error),
+ }
+}
diff --git a/crates/radicle-surf/src/commit.rs b/crates/radicle-surf/src/commit.rs
new file mode 100644
index 000000000..c2ee708bd
--- /dev/null
+++ b/crates/radicle-surf/src/commit.rs
@@ -0,0 +1,190 @@
+use std::{convert::TryFrom, str};
+
+use radicle_git_ext::Oid;
+use thiserror::Error;
+
+#[cfg(feature = "serde")]
+use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
+
+#[derive(Debug, Error)]
+pub enum Error {
+ /// When trying to get the summary for a [`git2::Commit`] some action
+ /// failed.
+ #[error("an error occurred trying to get a commit's summary")]
+ MissingSummary,
+ #[error(transparent)]
+ Utf8Error(#[from] str::Utf8Error),
+}
+
+/// Represents the authorship of actions in a git repo.
+#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Author {
+ /// Name of the author.
+ pub name: String,
+ /// Email of the author.
+ pub email: String,
+ /// Time the action was taken, e.g. time of commit.
+ #[cfg_attr(
+ feature = "serde",
+ serde(
+ serialize_with = "serialize_time",
+ deserialize_with = "deserialize_time"
+ )
+ )]
+ pub time: Time,
+}
+
+/// Time used in the authorship of an action in a git repo.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Time {
+ inner: git2::Time,
+}
+
+impl From<git2::Time> for Time {
+ fn from(inner: git2::Time) -> Self {
+ Self { inner }
+ }
+}
+
+impl Time {
+ pub fn new(epoch_seconds: i64, offset_minutes: i32) -> Self {
+ git2::Time::new(epoch_seconds, offset_minutes).into()
+ }
+
+ /// Returns the seconds since UNIX epoch.
+ pub fn seconds(&self) -> i64 {
+ self.inner.seconds()
+ }
+
+ /// Returns the timezone offset in minutes.
+ pub fn offset_minutes(&self) -> i32 {
+ self.inner.offset_minutes()
+ }
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_time<'de, D>(deserializer: D) -> Result<Time, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ let seconds: i64 = Deserialize::deserialize(deserializer)?;
+ Ok(Time::new(seconds, 0))
+}
+
+#[cfg(feature = "serde")]
+fn serialize_time<S>(t: &Time, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ serializer.serialize_i64(t.seconds())
+}
+
+impl std::fmt::Debug for Author {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use std::cmp::Ordering;
+ let time = match self.time.offset_minutes().cmp(&0) {
+ Ordering::Equal => format!("{}", self.time.seconds()),
+ Ordering::Greater => format!("{}+{}", self.time.seconds(), self.time.offset_minutes()),
+ Ordering::Less => format!("{}{}", self.time.seconds(), self.time.offset_minutes()),
+ };
+ f.debug_struct("Author")
+ .field("name", &self.name)
+ .field("email", &self.email)
+ .field("time", &time)
+ .finish()
+ }
+}
+
+impl TryFrom<git2::Signature<'_>> for Author {
+ type Error = str::Utf8Error;
+
+ fn try_from(signature: git2::Signature) -> Result<Self, Self::Error> {
+ let name = str::from_utf8(signature.name_bytes())?.into();
+ let email = str::from_utf8(signature.email_bytes())?.into();
+ let time = signature.when().into();
+
+ Ok(Author { name, email, time })
+ }
+}
+
+/// `Commit` is the metadata of a [Git commit][git-commit].
+///
+/// [git-commit]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Commit {
+ /// Object Id
+ pub id: Oid,
+ /// The author of the commit.
+ pub author: Author,
+ /// The actor who committed this commit.
+ pub committer: Author,
+ /// The long form message of the commit.
+ pub message: String,
+ /// The summary message of the commit.
+ pub summary: String,
+ /// The parents of this commit.
+ pub parents: Vec<Oid>,
+}
+
+impl Commit {
+ /// Returns the commit description text. This is the text after the one-line
+ /// summary.
+ #[must_use]
+ pub fn description(&self) -> &str {
+ self.message
+ .strip_prefix(&self.summary)
+ .unwrap_or(&self.message)
+ .trim()
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Commit {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut state = serializer.serialize_struct("Commit", 7)?;
+ state.serialize_field("id", &self.id.to_string())?;
+ state.serialize_field("author", &self.author)?;
+ state.serialize_field("committer", &self.committer)?;
+ state.serialize_field("summary", &self.summary)?;
+ state.serialize_field("message", &self.message)?;
+ state.serialize_field("description", &self.description())?;
+ state.serialize_field(
+ "parents",
+ &self
+ .parents
+ .iter()
+ .map(|oid| oid.to_string())
+ .collect::<Vec<String>>(),
+ )?;
+ state.end()
+ }
+}
+
+impl TryFrom<git2::Commit<'_>> for Commit {
+ type Error = Error;
+
+ fn try_from(commit: git2::Commit) -> Result<Self, Self::Error> {
+ let id = commit.id().into();
+ let author = Author::try_from(commit.author())?;
+ let committer = Author::try_from(commit.committer())?;
+ let message_raw = commit.message_bytes();
+ let message = str::from_utf8(message_raw)?.into();
+ let summary_raw = commit.summary_bytes().ok_or(Error::MissingSummary)?;
+ let summary = str::from_utf8(summary_raw)?.into();
+ let parents = commit.parent_ids().map(|oid| oid.into()).collect();
+
+ Ok(Commit {
+ id,
+ author,
+ committer,
+ message,
+ summary,
+ parents,
+ })
+ }
+}
diff --git a/crates/radicle-surf/src/diff.rs b/crates/radicle-surf/src/diff.rs
new file mode 100644
index 000000000..f0043aed0
--- /dev/null
+++ b/crates/radicle-surf/src/diff.rs
@@ -0,0 +1,612 @@
+//! Types that represent diff(s) in a Git repo.
+
+use std::{
+ borrow::Cow,
+ ops::Range,
+ path::{Path, PathBuf},
+ string::FromUtf8Error,
+};
+
+#[cfg(feature = "serde")]
+use serde::{ser, ser::SerializeStruct, Serialize, Serializer};
+
+use git_ext::Oid;
+
+pub mod git;
+
+/// The serializable representation of a `git diff`.
+///
+/// A [`Diff`] can be retrieved by the following functions:
+/// * [`crate::Repository::diff`]
+/// * [`crate::Repository::diff_commit`]
+#[cfg_attr(feature = "serde", derive(Serialize))]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct Diff {
+ files: Vec<FileDiff>,
+ stats: Stats,
+}
+
+impl Diff {
+ /// Creates an empty diff.
+ pub(crate) fn new() -> Self {
+ Diff::default()
+ }
+
+ /// Returns an iterator of the file in the diff.
+ pub fn files(&self) -> impl Iterator<Item = &FileDiff> {
+ self.files.iter()
+ }
+
+ /// Returns owned files in the diff.
+ pub fn into_files(self) -> Vec<FileDiff> {
+ self.files
+ }
+
+ pub fn added(&self) -> impl Iterator<Item = &Added> {
+ self.files().filter_map(|x| match x {
+ FileDiff::Added(a) => Some(a),
+ _ => None,
+ })
+ }
+
+ pub fn deleted(&self) -> impl Iterator<Item = &Deleted> {
+ self.files().filter_map(|x| match x {
+ FileDiff::Deleted(a) => Some(a),
+ _ => None,
+ })
+ }
+
+ pub fn moved(&self) -> impl Iterator<Item = &Moved> {
+ self.files().filter_map(|x| match x {
+ FileDiff::Moved(a) => Some(a),
+ _ => None,
+ })
+ }
+
+ pub fn modified(&self) -> impl Iterator<Item = &Modified> {
+ self.files().filter_map(|x| match x {
+ FileDiff::Modified(a) => Some(a),
+ _ => None,
+ })
+ }
+
+ pub fn copied(&self) -> impl Iterator<Item = &Copied> {
+ self.files().filter_map(|x| match x {
+ FileDiff::Copied(a) => Some(a),
+ _ => None,
+ })
+ }
+
+ pub fn stats(&self) -> &Stats {
+ &self.stats
+ }
+
+ fn update_stats(&mut self, diff: &DiffContent) {
+ self.stats.files_changed += 1;
+ if let DiffContent::Plain { hunks, .. } = diff {
+ for h in hunks.iter() {
+ for l in &h.lines {
+ match l {
+ Modification::Addition(_) => self.stats.insertions += 1,
+ Modification::Deletion(_) => self.stats.deletions += 1,
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+
+ pub fn insert_modified(
+ &mut self,
+ path: PathBuf,
+ diff: DiffContent,
+ old: DiffFile,
+ new: DiffFile,
+ ) {
+ self.update_stats(&diff);
+ let diff = FileDiff::Modified(Modified {
+ path,
+ diff,
+ old,
+ new,
+ });
+ self.files.push(diff);
+ }
+
+ pub fn insert_moved(
+ &mut self,
+ old_path: PathBuf,
+ new_path: PathBuf,
+ old: DiffFile,
+ new: DiffFile,
+ content: DiffContent,
+ ) {
+ self.update_stats(&DiffContent::Empty);
+ let diff = FileDiff::Moved(Moved {
+ old_path,
+ new_path,
+ old,
+ new,
+ diff: content,
+ });
+ self.files.push(diff);
+ }
+
+ pub fn insert_copied(
+ &mut self,
+ old_path: PathBuf,
+ new_path: PathBuf,
+ old: DiffFile,
+ new: DiffFile,
+ content: DiffContent,
+ ) {
+ self.update_stats(&DiffContent::Empty);
+ let diff = FileDiff::Copied(Copied {
+ old_path,
+ new_path,
+ old,
+ new,
+ diff: content,
+ });
+ self.files.push(diff);
+ }
+
+ pub fn insert_added(&mut self, path: PathBuf, diff: DiffContent, new: DiffFile) {
+ self.update_stats(&diff);
+ let diff = FileDiff::Added(Added { path, diff, new });
+ self.files.push(diff);
+ }
+
+ pub fn insert_deleted(&mut self, path: PathBuf, diff: DiffContent, old: DiffFile) {
+ self.update_stats(&diff);
+ let diff = FileDiff::Deleted(Deleted { path, diff, old });
+ self.files.push(diff);
+ }
+}
+
+/// A file that was added within a [`Diff`].
+#[cfg_attr(feature = "serde", derive(Serialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Added {
+ /// The path to this file, relative to the repository root.
+ pub path: PathBuf,
+ pub diff: DiffContent,
+ pub new: DiffFile,
+}
+
+/// A file that was deleted within a [`Diff`].
+#[cfg_attr(feature = "serde", derive(Serialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Deleted {
+ /// The path to this file, relative to the repository root.
+ pub path: PathBuf,
+ pub diff: DiffContent,
+ pub old: DiffFile,
+}
+
+/// A file that was moved within a [`Diff`].
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Moved {
+ /// The old path to this file, relative to the repository root.
+ pub old_path: PathBuf,
+ pub old: DiffFile,
+ /// The new path to this file, relative to the repository root.
+ pub new_path: PathBuf,
+ pub new: DiffFile,
+ pub diff: DiffContent,
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Moved {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ if self.old == self.new {
+ let mut state = serializer.serialize_struct("Moved", 3)?;
+ state.serialize_field("oldPath", &self.old_path)?;
+ state.serialize_field("newPath", &self.new_path)?;
+ state.serialize_field("current", &self.new)?;
+ state.end()
+ } else {
+ let mut state = serializer.serialize_struct("Moved", 5)?;
+ state.serialize_field("oldPath", &self.old_path)?;
+ state.serialize_field("newPath", &self.new_path)?;
+ state.serialize_field("old", &self.old)?;
+ state.serialize_field("new", &self.new)?;
+ state.serialize_field("diff", &self.diff)?;
+ state.end()
+ }
+ }
+}
+
+/// A file that was copied within a [`Diff`].
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Copied {
+ /// The old path to this file, relative to the repository root.
+ pub old_path: PathBuf,
+ /// The new path to this file, relative to the repository root.
+ pub new_path: PathBuf,
+ pub old: DiffFile,
+ pub new: DiffFile,
+ pub diff: DiffContent,
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Copied {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ if self.old == self.new {
+ let mut state = serializer.serialize_struct("Copied", 3)?;
+ state.serialize_field("oldPath", &self.old_path)?;
+ state.serialize_field("newPath", &self.new_path)?;
+ state.serialize_field("current", &self.new)?;
+ state.end()
+ } else {
+ let mut state = serializer.serialize_struct("Copied", 5)?;
+ state.serialize_field("oldPath", &self.old_path)?;
+ state.serialize_field("newPath", &self.new_path)?;
+ state.serialize_field("old", &self.old)?;
+ state.serialize_field("new", &self.new)?;
+ state.serialize_field("diff", &self.diff)?;
+ state.end()
+ }
+ }
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum EofNewLine {
+ OldMissing,
+ NewMissing,
+ BothMissing,
+ NoneMissing,
+}
+
+impl Default for EofNewLine {
+ fn default() -> Self {
+ Self::NoneMissing
+ }
+}
+
+/// A file that was modified within a [`Diff`].
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Modified {
+ pub path: PathBuf,
+ pub diff: DiffContent,
+ pub old: DiffFile,
+ pub new: DiffFile,
+}
+
+/// The set of changes for a given file.
+#[cfg_attr(
+ feature = "serde",
+ derive(Serialize),
+ serde(tag = "type", rename_all = "camelCase")
+)]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum DiffContent {
+ /// The file is a binary file and so no set of changes can be provided.
+ Binary,
+ /// The set of changes, as [`Hunks`] for a plaintext file.
+ #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+ Plain {
+ hunks: Hunks<Modification>,
+ stats: FileStats,
+ eof: EofNewLine,
+ },
+ Empty,
+}
+
+impl DiffContent {
+ pub fn eof(&self) -> Option<EofNewLine> {
+ match self {
+ Self::Plain { eof, .. } => Some(eof.clone()),
+ _ => None,
+ }
+ }
+
+ pub fn stats(&self) -> Option<&FileStats> {
+ match &self {
+ DiffContent::Plain { stats, .. } => Some(stats),
+ DiffContent::Empty => None,
+ DiffContent::Binary => None,
+ }
+ }
+}
+
+/// File mode in a diff.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+pub enum FileMode {
+ /// For regular files.
+ Blob,
+ /// For regular files that are executable.
+ BlobExecutable,
+ /// For directories.
+ Tree,
+ /// For symbolic links.
+ Link,
+ /// Used for Git submodules.
+ Commit,
+}
+
+impl From<FileMode> for u32 {
+ fn from(m: FileMode) -> Self {
+ git2::FileMode::from(m).into()
+ }
+}
+
+impl From<FileMode> for i32 {
+ fn from(m: FileMode) -> Self {
+ git2::FileMode::from(m).into()
+ }
+}
+
+/// A modified file.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+pub struct DiffFile {
+ /// File blob id.
+ pub oid: Oid,
+ /// File mode.
+ pub mode: FileMode,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(
+ feature = "serde",
+ derive(Serialize),
+ serde(tag = "status", rename_all = "camelCase")
+)]
+pub enum FileDiff {
+ Added(Added),
+ Deleted(Deleted),
+ Modified(Modified),
+ Moved(Moved),
+ Copied(Copied),
+}
+
+impl FileDiff {
+ pub fn path(&self) -> &Path {
+ match self {
+ FileDiff::Added(x) => x.path.as_path(),
+ FileDiff::Deleted(x) => x.path.as_path(),
+ FileDiff::Modified(x) => x.path.as_path(),
+ FileDiff::Moved(x) => x.new_path.as_path(),
+ FileDiff::Copied(x) => x.new_path.as_path(),
+ }
+ }
+}
+
+/// Statistics describing a particular [`FileDiff`].
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct FileStats {
+ /// Get the total number of additions in a [`FileDiff`].
+ pub additions: usize,
+ /// Get the total number of deletions in a [`FileDiff`].
+ pub deletions: usize,
+}
+
+/// Statistics describing a particular [`Diff`].
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct Stats {
+ /// Get the total number of files changed in a [`Diff`]
+ pub files_changed: usize,
+ /// Get the total number of insertions in a [`Diff`].
+ pub insertions: usize,
+ /// Get the total number of deletions in a [`Diff`].
+ pub deletions: usize,
+}
+
+/// A set of changes across multiple lines.
+///
+/// The parameter `T` can be an [`Addition`], [`Deletion`], or
+/// [`Modification`].
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Hunk<T> {
+ pub header: Line,
+ pub lines: Vec<T>,
+ /// Old line range.
+ pub old: Range<u32>,
+ /// New line range.
+ pub new: Range<u32>,
+}
+
+/// A set of [`Hunk`] changes.
+#[cfg_attr(feature = "serde", derive(Serialize))]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Hunks<T>(pub Vec<Hunk<T>>);
+
+impl<T> Default for Hunks<T> {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl<T> Hunks<T> {
+ pub fn iter(&self) -> impl Iterator<Item = &Hunk<T>> {
+ self.0.iter()
+ }
+}
+
+impl<T> From<Vec<Hunk<T>>> for Hunks<T> {
+ fn from(hunks: Vec<Hunk<T>>) -> Self {
+ Self(hunks)
+ }
+}
+
+/// The content of a single line.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Line(pub(crate) Vec<u8>);
+
+impl Line {
+ pub fn as_bytes(&self) -> &[u8] {
+ self.0.as_slice()
+ }
+
+ pub fn from_utf8(self) -> Result<String, FromUtf8Error> {
+ String::from_utf8(self.0)
+ }
+
+ pub fn from_utf8_lossy<'a>(&'a self) -> Cow<'a, str> {
+ String::from_utf8_lossy(&self.0)
+ }
+}
+
+impl From<Vec<u8>> for Line {
+ fn from(v: Vec<u8>) -> Self {
+ Self(v)
+ }
+}
+
+impl From<String> for Line {
+ fn from(s: String) -> Self {
+ Self(s.into_bytes())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Line {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let s = std::str::from_utf8(&self.0).map_err(ser::Error::custom)?;
+
+ serializer.serialize_str(s)
+ }
+}
+
+/// Either the modification of a single [`Line`], or just contextual
+/// information.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Modification {
+ /// A line is an addition in a file.
+ Addition(Addition),
+
+ /// A line is a deletion in a file.
+ Deletion(Deletion),
+
+ /// A contextual line in a file, i.e. there were no changes to the line.
+ Context {
+ line: Line,
+ line_no_old: u32,
+ line_no_new: u32,
+ },
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Modification {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ use serde::ser::SerializeMap as _;
+
+ match self {
+ Modification::Addition(addition) => {
+ let mut map = serializer.serialize_map(Some(3))?;
+ map.serialize_entry("line", &addition.line)?;
+ map.serialize_entry("lineNo", &addition.line_no)?;
+ map.serialize_entry("type", "addition")?;
+ map.end()
+ }
+ Modification::Deletion(deletion) => {
+ let mut map = serializer.serialize_map(Some(3))?;
+ map.serialize_entry("line", &deletion.line)?;
+ map.serialize_entry("lineNo", &deletion.line_no)?;
+ map.serialize_entry("type", "deletion")?;
+ map.end()
+ }
+ Modification::Context {
+ line,
+ line_no_old,
+ line_no_new,
+ } => {
+ let mut map = serializer.serialize_map(Some(4))?;
+ map.serialize_entry("line", line)?;
+ map.serialize_entry("lineNoOld", line_no_old)?;
+ map.serialize_entry("lineNoNew", line_no_new)?;
+ map.serialize_entry("type", "context")?;
+ map.end()
+ }
+ }
+ }
+}
+
+/// A addition of a [`Line`] at the `line_no`.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Addition {
+ pub line: Line,
+ pub line_no: u32,
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Addition {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ use serde::ser::SerializeStruct as _;
+
+ let mut s = serializer.serialize_struct("Addition", 3)?;
+ s.serialize_field("line", &self.line)?;
+ s.serialize_field("lineNo", &self.line_no)?;
+ s.serialize_field("type", "addition")?;
+ s.end()
+ }
+}
+
+/// A deletion of a [`Line`] at the `line_no`.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Deletion {
+ pub line: Line,
+ pub line_no: u32,
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Deletion {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ use serde::ser::SerializeStruct as _;
+
+ let mut s = serializer.serialize_struct("Deletion", 3)?;
+ s.serialize_field("line", &self.line)?;
+ s.serialize_field("lineNo", &self.line_no)?;
+ s.serialize_field("type", "deletion")?;
+ s.end()
+ }
+}
+
+impl Modification {
+ pub fn addition(line: impl Into<Line>, line_no: u32) -> Self {
+ Self::Addition(Addition {
+ line: line.into(),
+ line_no,
+ })
+ }
+
+ pub fn deletion(line: impl Into<Line>, line_no: u32) -> Self {
+ Self::Deletion(Deletion {
+ line: line.into(),
+ line_no,
+ })
+ }
+
+ pub fn context(line: impl Into<Line>, line_no_old: u32, line_no_new: u32) -> Self {
+ Self::Context {
+ line: line.into(),
+ line_no_old,
+ line_no_new,
+ }
+ }
+}
diff --git a/crates/radicle-surf/src/diff/git.rs b/crates/radicle-surf/src/diff/git.rs
new file mode 100644
index 000000000..4adf8d7e5
--- /dev/null
+++ b/crates/radicle-surf/src/diff/git.rs
@@ -0,0 +1,377 @@
+use std::convert::TryFrom;
+
+use super::{
+ Diff, DiffContent, DiffFile, EofNewLine, FileMode, FileStats, Hunk, Hunks, Line, Modification,
+ Stats,
+};
+
+pub mod error {
+ use std::path::PathBuf;
+
+ use thiserror::Error;
+
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum Addition {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error("the new line number was missing for an added line")]
+ MissingNewLineNo,
+ }
+
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum Deletion {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error("the new line number was missing for an deleted line")]
+ MissingOldLineNo,
+ }
+
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum FileMode {
+ #[error("unknown file mode `{0:?}`")]
+ Unknown(git2::FileMode),
+ }
+
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum Modification {
+ /// A Git `DiffLine` is invalid.
+ #[error(
+ "invalid `git2::DiffLine` which contains no line numbers for either side of the diff"
+ )]
+ Invalid,
+ }
+
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum Hunk {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error(transparent)]
+ Line(#[from] Modification),
+ }
+
+ /// A Git diff error.
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum Diff {
+ #[error(transparent)]
+ Addition(#[from] Addition),
+ #[error(transparent)]
+ Deletion(#[from] Deletion),
+ /// A Git delta type isn't currently handled.
+ #[error("git delta type is not handled")]
+ DeltaUnhandled(git2::Delta),
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error(transparent)]
+ FileMode(#[from] FileMode),
+ #[error(transparent)]
+ Hunk(#[from] Hunk),
+ #[error(transparent)]
+ Line(#[from] Modification),
+ /// A patch is unavailable.
+ #[error("couldn't retrieve patch for {0}")]
+ PatchUnavailable(PathBuf),
+ /// A The path of a file isn't available.
+ #[error("couldn't retrieve file path")]
+ PathUnavailable,
+ }
+}
+
+impl TryFrom<git2::DiffFile<'_>> for DiffFile {
+ type Error = error::FileMode;
+
+ fn try_from(value: git2::DiffFile) -> Result<Self, Self::Error> {
+ Ok(Self {
+ mode: value.mode().try_into()?,
+ oid: value.id().into(),
+ })
+ }
+}
+
+impl TryFrom<git2::FileMode> for FileMode {
+ type Error = error::FileMode;
+
+ fn try_from(value: git2::FileMode) -> Result<Self, Self::Error> {
+ match value {
+ git2::FileMode::Blob => Ok(Self::Blob),
+ git2::FileMode::BlobExecutable => Ok(Self::BlobExecutable),
+ git2::FileMode::Commit => Ok(Self::Commit),
+ git2::FileMode::Tree => Ok(Self::Tree),
+ git2::FileMode::Link => Ok(Self::Link),
+ _ => Err(error::FileMode::Unknown(value)),
+ }
+ }
+}
+
+impl From<FileMode> for git2::FileMode {
+ fn from(m: FileMode) -> Self {
+ match m {
+ FileMode::Blob => git2::FileMode::Blob,
+ FileMode::BlobExecutable => git2::FileMode::BlobExecutable,
+ FileMode::Tree => git2::FileMode::Tree,
+ FileMode::Link => git2::FileMode::Link,
+ FileMode::Commit => git2::FileMode::Commit,
+ }
+ }
+}
+
+impl TryFrom<git2::Patch<'_>> for DiffContent {
+ type Error = error::Hunk;
+
+ fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
+ let mut hunks = Vec::new();
+ let mut old_missing_eof = false;
+ let mut new_missing_eof = false;
+ let mut additions = 0;
+ let mut deletions = 0;
+
+ for h in 0..patch.num_hunks() {
+ let (hunk, hunk_lines) = patch.hunk(h)?;
+ let header = Line(hunk.header().to_owned());
+ let mut lines: Vec<Modification> = Vec::new();
+
+ for l in 0..hunk_lines {
+ let line = patch.line_in_hunk(h, l)?;
+ match line.origin_value() {
+ git2::DiffLineType::ContextEOFNL => {
+ new_missing_eof = true;
+ old_missing_eof = true;
+ continue;
+ }
+ git2::DiffLineType::Addition => {
+ additions += 1;
+ }
+ git2::DiffLineType::Deletion => {
+ deletions += 1;
+ }
+ git2::DiffLineType::AddEOFNL => {
+ additions += 1;
+ old_missing_eof = true;
+ continue;
+ }
+ git2::DiffLineType::DeleteEOFNL => {
+ deletions += 1;
+ new_missing_eof = true;
+ continue;
+ }
+ _ => {}
+ }
+ let line = Modification::try_from(line)?;
+ lines.push(line);
+ }
+ hunks.push(Hunk {
+ header,
+ lines,
+ old: hunk.old_start()..hunk.old_start() + hunk.old_lines(),
+ new: hunk.new_start()..hunk.new_start() + hunk.new_lines(),
+ });
+ }
+ let eof = match (old_missing_eof, new_missing_eof) {
+ (true, true) => EofNewLine::BothMissing,
+ (true, false) => EofNewLine::OldMissing,
+ (false, true) => EofNewLine::NewMissing,
+ (false, false) => EofNewLine::NoneMissing,
+ };
+ Ok(DiffContent::Plain {
+ hunks: Hunks(hunks),
+ stats: FileStats {
+ additions,
+ deletions,
+ },
+ eof,
+ })
+ }
+}
+
+impl TryFrom<git2::DiffLine<'_>> for Modification {
+ type Error = error::Modification;
+
+ fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
+ match (line.old_lineno(), line.new_lineno()) {
+ (None, Some(n)) => Ok(Self::addition(line.content().to_owned(), n)),
+ (Some(n), None) => Ok(Self::deletion(line.content().to_owned(), n)),
+ (Some(l), Some(r)) => Ok(Self::context(line.content().to_owned(), l, r)),
+ (None, None) => Err(error::Modification::Invalid),
+ }
+ }
+}
+
+impl From<git2::DiffStats> for Stats {
+ fn from(stats: git2::DiffStats) -> Self {
+ Self {
+ files_changed: stats.files_changed(),
+ insertions: stats.insertions(),
+ deletions: stats.deletions(),
+ }
+ }
+}
+
+impl TryFrom<git2::Diff<'_>> for Diff {
+ type Error = error::Diff;
+
+ fn try_from(git_diff: git2::Diff) -> Result<Diff, Self::Error> {
+ use git2::Delta;
+
+ let mut diff = Diff::new();
+
+ // This allows libgit2 to run the binary detection.
+ // Reference: <https://github.com/libgit2/libgit2/issues/6637>
+ git_diff.foreach(&mut |_, _| true, None, None, None)?;
+
+ for (idx, delta) in git_diff.deltas().enumerate() {
+ match delta.status() {
+ Delta::Added => created(&mut diff, &git_diff, idx, &delta)?,
+ Delta::Deleted => deleted(&mut diff, &git_diff, idx, &delta)?,
+ Delta::Modified => modified(&mut diff, &git_diff, idx, &delta)?,
+ Delta::Renamed => renamed(&mut diff, &git_diff, idx, &delta)?,
+ Delta::Copied => copied(&mut diff, &git_diff, idx, &delta)?,
+ status => {
+ return Err(error::Diff::DeltaUnhandled(status));
+ }
+ }
+ }
+
+ Ok(diff)
+ }
+}
+
+fn created(
+ diff: &mut Diff,
+ git_diff: &git2::Diff<'_>,
+ idx: usize,
+ delta: &git2::DiffDelta<'_>,
+) -> Result<(), error::Diff> {
+ let diff_file = delta.new_file();
+ let is_binary = diff_file.is_binary();
+ let path = diff_file
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let new = DiffFile::try_from(diff_file)?;
+
+ let patch = git2::Patch::from_diff(git_diff, idx)?;
+ if is_binary {
+ diff.insert_added(path, DiffContent::Binary, new);
+ } else if let Some(patch) = patch {
+ diff.insert_added(path, DiffContent::try_from(patch)?, new);
+ } else {
+ return Err(error::Diff::PatchUnavailable(path));
+ }
+ Ok(())
+}
+
+fn deleted(
+ diff: &mut Diff,
+ git_diff: &git2::Diff<'_>,
+ idx: usize,
+ delta: &git2::DiffDelta<'_>,
+) -> Result<(), error::Diff> {
+ let diff_file = delta.old_file();
+ let is_binary = diff_file.is_binary();
+ let path = diff_file
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let patch = git2::Patch::from_diff(git_diff, idx)?;
+ let old = DiffFile::try_from(diff_file)?;
+
+ if is_binary {
+ diff.insert_deleted(path, DiffContent::Binary, old);
+ } else if let Some(patch) = patch {
+ diff.insert_deleted(path, DiffContent::try_from(patch)?, old);
+ } else {
+ return Err(error::Diff::PatchUnavailable(path));
+ }
+ Ok(())
+}
+
+fn modified(
+ diff: &mut Diff,
+ git_diff: &git2::Diff<'_>,
+ idx: usize,
+ delta: &git2::DiffDelta<'_>,
+) -> Result<(), error::Diff> {
+ let diff_file = delta.new_file();
+ let path = diff_file
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let patch = git2::Patch::from_diff(git_diff, idx)?;
+ let old = DiffFile::try_from(delta.old_file())?;
+ let new = DiffFile::try_from(delta.new_file())?;
+
+ if diff_file.is_binary() {
+ diff.insert_modified(path, DiffContent::Binary, old, new);
+ Ok(())
+ } else if let Some(patch) = patch {
+ diff.insert_modified(path, DiffContent::try_from(patch)?, old, new);
+ Ok(())
+ } else {
+ Err(error::Diff::PatchUnavailable(path))
+ }
+}
+
+fn renamed(
+ diff: &mut Diff,
+ git_diff: &git2::Diff<'_>,
+ idx: usize,
+ delta: &git2::DiffDelta<'_>,
+) -> Result<(), error::Diff> {
+ let old_path = delta
+ .old_file()
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let new_path = delta
+ .new_file()
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let patch = git2::Patch::from_diff(git_diff, idx)?;
+ let old = DiffFile::try_from(delta.old_file())?;
+ let new = DiffFile::try_from(delta.new_file())?;
+
+ if delta.new_file().is_binary() {
+ diff.insert_moved(old_path, new_path, old, new, DiffContent::Binary);
+ } else if let Some(patch) = patch {
+ diff.insert_moved(old_path, new_path, old, new, DiffContent::try_from(patch)?);
+ } else {
+ diff.insert_moved(old_path, new_path, old, new, DiffContent::Empty);
+ }
+ Ok(())
+}
+
+fn copied(
+ diff: &mut Diff,
+ git_diff: &git2::Diff<'_>,
+ idx: usize,
+ delta: &git2::DiffDelta<'_>,
+) -> Result<(), error::Diff> {
+ let old_path = delta
+ .old_file()
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let new_path = delta
+ .new_file()
+ .path()
+ .ok_or(error::Diff::PathUnavailable)?
+ .to_path_buf();
+ let patch = git2::Patch::from_diff(git_diff, idx)?;
+ let old = DiffFile::try_from(delta.old_file())?;
+ let new = DiffFile::try_from(delta.new_file())?;
+
+ if delta.new_file().is_binary() {
+ diff.insert_copied(old_path, new_path, old, new, DiffContent::Binary);
+ } else if let Some(patch) = patch {
+ diff.insert_copied(old_path, new_path, old, new, DiffContent::try_from(patch)?);
+ } else {
+ diff.insert_copied(old_path, new_path, old, new, DiffContent::Empty);
+ }
+ Ok(())
+}
diff --git a/crates/radicle-surf/src/error.rs b/crates/radicle-surf/src/error.rs
new file mode 100644
index 000000000..1a790dc17
--- /dev/null
+++ b/crates/radicle-surf/src/error.rs
@@ -0,0 +1,39 @@
+//! Definition for a crate level error type, which wraps up module level
+//! error types transparently.
+
+use crate::{commit, diff, fs, glob, namespace, refs, repo};
+use thiserror::Error;
+
+/// The crate level error type that wraps up module level error types.
+#[derive(Debug, Error)]
+#[non_exhaustive]
+pub enum Error {
+ #[error(transparent)]
+ Branches(#[from] refs::error::Branch),
+ #[error(transparent)]
+ Categories(#[from] refs::error::Category),
+ #[error(transparent)]
+ Commit(#[from] commit::Error),
+ #[error(transparent)]
+ Diff(#[from] diff::git::error::Diff),
+ #[error(transparent)]
+ Directory(#[from] fs::error::Directory),
+ #[error(transparent)]
+ File(#[from] fs::error::File),
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error(transparent)]
+ Glob(#[from] glob::Error),
+ #[error(transparent)]
+ Namespace(#[from] namespace::Error),
+ #[error(transparent)]
+ RefFormat(#[from] git_ext::ref_format::Error),
+ #[error(transparent)]
+ Revision(Box<dyn std::error::Error + Send + Sync + 'static>),
+ #[error(transparent)]
+ ToCommit(Box<dyn std::error::Error + Send + Sync + 'static>),
+ #[error(transparent)]
+ Tags(#[from] refs::error::Tag),
+ #[error(transparent)]
+ Repo(#[from] repo::error::Repo),
+}
diff --git a/crates/radicle-surf/src/fs.rs b/crates/radicle-surf/src/fs.rs
new file mode 100644
index 000000000..2cbdf8f67
--- /dev/null
+++ b/crates/radicle-surf/src/fs.rs
@@ -0,0 +1,608 @@
+//! Definition for a file system consisting of `Directory` and `File`.
+//!
+//! A `Directory` is expected to be a non-empty tree of directories and files.
+//! See [`Directory`] for more information.
+
+use std::{
+ cmp::Ordering,
+ collections::BTreeMap,
+ convert::{Infallible, Into as _},
+ path::{Path, PathBuf},
+};
+
+use git2::Blob;
+use radicle_git_ext::{is_not_found_err, Oid};
+use radicle_std_ext::result::ResultExt as _;
+use url::Url;
+
+use crate::{Repository, Revision};
+
+pub mod error {
+ use std::path::PathBuf;
+
+ use thiserror::Error;
+
+ #[derive(Debug, Error, PartialEq)]
+ pub enum Directory {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error(transparent)]
+ File(#[from] File),
+ #[error("the path {0} is not valid")]
+ InvalidPath(PathBuf),
+ #[error("the entry at '{0}' must be of type {1}")]
+ InvalidType(PathBuf, &'static str),
+ #[error("the entry name was not valid UTF-8")]
+ Utf8Error,
+ #[error("the path {0} not found")]
+ PathNotFound(PathBuf),
+ #[error(transparent)]
+ Submodule(#[from] Submodule),
+ }
+
+ #[derive(Debug, Error, PartialEq)]
+ pub enum File {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ }
+
+ #[derive(Debug, Error, PartialEq)]
+ pub enum Submodule {
+ #[error("URL is invalid utf-8 for submodule '{name}': {err}")]
+ Utf8 {
+ name: String,
+ #[source]
+ err: std::str::Utf8Error,
+ },
+ #[error("failed to parse URL '{url}' for submodule '{name}': {err}")]
+ ParseUrl {
+ name: String,
+ url: String,
+ #[source]
+ err: url::ParseError,
+ },
+ }
+}
+
+/// A `File` in a git repository.
+///
+/// The representation is lightweight and contains the [`Oid`] that
+/// points to the git blob which is this file.
+///
+/// The name of a file can be retrieved via [`File::name`].
+///
+/// The [`FileContent`] of a file can be retrieved via
+/// [`File::content`].
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct File {
+ /// The name of the file.
+ name: String,
+ /// The relative path of the file, not including the `name`,
+ /// in respect to the root of the git repository.
+ prefix: PathBuf,
+ /// The object identifier of the git blob of this file.
+ id: Oid,
+}
+
+impl File {
+ /// Construct a new `File`.
+ ///
+ /// The `path` must be the prefix location of the directory, and
+ /// so should not end in `name`.
+ ///
+ /// The `id` must point to a git blob.
+ pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
+ debug_assert!(
+ !prefix.ends_with(&name),
+ "prefix = {prefix:?}, name = {name}",
+ );
+ Self { name, prefix, id }
+ }
+
+ /// The name of this `File`.
+ pub fn name(&self) -> &str {
+ self.name.as_str()
+ }
+
+ /// The object identifier of this `File`.
+ pub fn id(&self) -> Oid {
+ self.id
+ }
+
+ /// Return the exact path for this `File`, including the `name` of
+ /// the directory itself.
+ ///
+ /// The path is relative to the git repository root.
+ pub fn path(&self) -> PathBuf {
+ self.prefix.join(&self.name)
+ }
+
+ /// Return the [`Path`] where this `File` is located, relative to the
+ /// git repository root.
+ pub fn location(&self) -> &Path {
+ &self.prefix
+ }
+
+ /// Get the [`FileContent`] for this `File`.
+ ///
+ /// # Errors
+ ///
+ /// This function will fail if it could not find the `git` blob
+ /// for the `Oid` of this `File`.
+ pub fn content<'a>(&self, repo: &'a Repository) -> Result<FileContent<'a>, error::File> {
+ let blob = repo.find_blob(self.id)?;
+ Ok(FileContent { blob })
+ }
+}
+
+/// The contents of a [`File`].
+///
+/// To construct a `FileContent` use [`File::content`].
+pub struct FileContent<'a> {
+ blob: Blob<'a>,
+}
+
+impl<'a> FileContent<'a> {
+ /// Return the file contents as a byte slice.
+ pub fn as_bytes(&self) -> &[u8] {
+ self.blob.content()
+ }
+
+ /// Return the size of the file contents.
+ pub fn size(&self) -> usize {
+ self.blob.size()
+ }
+
+ /// Creates a `FileContent` using a blob.
+ pub(crate) fn new(blob: Blob<'a>) -> Self {
+ Self { blob }
+ }
+}
+
+/// A representations of a [`Directory`]'s entries.
+pub struct Entries {
+ listing: BTreeMap<String, Entry>,
+}
+
+impl Entries {
+ /// Return the name of each [`Entry`].
+ pub fn names(&self) -> impl Iterator<Item = &String> {
+ self.listing.keys()
+ }
+
+ /// Return each [`Entry`].
+ pub fn entries(&self) -> impl Iterator<Item = &Entry> {
+ self.listing.values()
+ }
+
+ /// Return each [`Entry`] and its name.
+ pub fn iter(&self) -> impl Iterator<Item = (&String, &Entry)> {
+ self.listing.iter()
+ }
+}
+
+impl Iterator for Entries {
+ type Item = Entry;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // Can be improved when `pop_first()` is stable for BTreeMap.
+ let next_key = match self.listing.keys().next() {
+ Some(k) => k.clone(),
+ None => return None,
+ };
+ self.listing.remove(&next_key)
+ }
+}
+
+/// An `Entry` is either a [`File`] entry or a [`Directory`] entry.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Entry {
+ /// A file entry within a [`Directory`].
+ File(File),
+ /// A sub-directory of a [`Directory`].
+ Directory(Directory),
+ /// An entry points to a submodule.
+ Submodule(Submodule),
+}
+
+impl PartialOrd for Entry {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for Entry {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ match (self, other) {
+ (Entry::File(x), Entry::File(y)) => x.name().cmp(y.name()),
+ (Entry::File(_), Entry::Directory(_)) => Ordering::Less,
+ (Entry::File(_), Entry::Submodule(_)) => Ordering::Less,
+ (Entry::Directory(_), Entry::File(_)) => Ordering::Greater,
+ (Entry::Submodule(_), Entry::File(_)) => Ordering::Less,
+ (Entry::Directory(x), Entry::Directory(y)) => x.name().cmp(y.name()),
+ (Entry::Directory(x), Entry::Submodule(y)) => x.name().cmp(y.name()),
+ (Entry::Submodule(x), Entry::Directory(y)) => x.name().cmp(y.name()),
+ (Entry::Submodule(x), Entry::Submodule(y)) => x.name().cmp(y.name()),
+ }
+ }
+}
+
+impl Entry {
+ /// Get a label for the `Entriess`, either the name of the [`File`],
+ /// the name of the [`Directory`], or the name of the [`Submodule`].
+ pub fn name(&self) -> &String {
+ match self {
+ Entry::File(file) => &file.name,
+ Entry::Directory(directory) => directory.name(),
+ Entry::Submodule(submodule) => submodule.name(),
+ }
+ }
+
+ pub fn path(&self) -> PathBuf {
+ match self {
+ Entry::File(file) => file.path(),
+ Entry::Directory(directory) => directory.path(),
+ Entry::Submodule(submodule) => submodule.path(),
+ }
+ }
+
+ pub fn location(&self) -> &Path {
+ match self {
+ Entry::File(file) => file.location(),
+ Entry::Directory(directory) => directory.location(),
+ Entry::Submodule(submodule) => submodule.location(),
+ }
+ }
+
+ /// Returns `true` if the `Entry` is a file.
+ pub fn is_file(&self) -> bool {
+ matches!(self, Entry::File(_))
+ }
+
+ /// Returns `true` if the `Entry` is a directory.
+ pub fn is_directory(&self) -> bool {
+ matches!(self, Entry::Directory(_))
+ }
+
+ pub(crate) fn from_entry(
+ entry: &git2::TreeEntry,
+ path: PathBuf,
+ repo: &Repository,
+ ) -> Result<Self, error::Directory> {
+ let name = entry
+ .name()
+ .map_err(|_| error::Directory::Utf8Error)?
+ .to_string();
+ let id = entry.id().into();
+
+ match entry.kind() {
+ Some(git2::ObjectType::Tree) => Ok(Self::Directory(Directory::new(name, path, id))),
+ Some(git2::ObjectType::Blob) => Ok(Self::File(File::new(name, path, id))),
+ Some(git2::ObjectType::Commit) => {
+ let submodule = (!repo.is_bare())
+ .then(|| repo.find_submodule(&name))
+ .transpose()?;
+ Ok(Self::Submodule(Submodule::new(name, path, submodule, id)?))
+ }
+ _ => Err(error::Directory::InvalidType(path, "tree or blob")),
+ }
+ }
+}
+
+/// A `Directory` is the representation of a file system directory, for a given
+/// [`git` tree][git-tree].
+///
+/// The name of a directory can be retrieved via [`File::name`].
+///
+/// The [`Entries`] of a directory can be retrieved via
+/// [`Directory::entries`].
+///
+/// [git-tree]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Directory {
+ /// The name of the directoy.
+ name: String,
+ /// The relative path of the directory, not including the `name`,
+ /// in respect to the root of the git repository.
+ prefix: PathBuf,
+ /// The object identifier of the git tree of this directory.
+ id: Oid,
+}
+
+const ROOT_DIR: &str = "";
+
+impl Directory {
+ /// Creates a directory given its `tree_id`.
+ ///
+ /// The `name` and `prefix` are both set to be empty.
+ pub(crate) fn root(id: Oid) -> Self {
+ Self::new(ROOT_DIR.to_string(), PathBuf::new(), id)
+ }
+
+ /// Creates a directory given its `name` and `id`.
+ ///
+ /// The `path` must be the prefix location of the directory, and
+ /// so should not end in `name`.
+ ///
+ /// The `id` must point to a `git` tree.
+ pub(crate) fn new(name: String, prefix: PathBuf, id: Oid) -> Self {
+ debug_assert!(
+ name.is_empty() || !prefix.ends_with(&name),
+ "prefix = {prefix:?}, name = {name}",
+ );
+ Self { name, prefix, id }
+ }
+
+ /// Get the name of the current `Directory`.
+ pub fn name(&self) -> &String {
+ &self.name
+ }
+
+ /// The object identifier of this `[Directory]`.
+ pub fn id(&self) -> Oid {
+ self.id
+ }
+
+ /// Return the exact path for this `Directory`, including the `name` of the
+ /// directory itself.
+ ///
+ /// The path is relative to the git repository root.
+ pub fn path(&self) -> PathBuf {
+ self.prefix.join(&self.name)
+ }
+
+ /// Return the [`Path`] where this `Directory` is located, relative to the
+ /// git repository root.
+ pub fn location(&self) -> &Path {
+ &self.prefix
+ }
+
+ /// Return the [`Entries`] for this `Directory`'s `Oid`.
+ ///
+ /// The resulting `Entries` will only resolve to this
+ /// `Directory`'s entries. Any sub-directories will need to be
+ /// resolved independently.
+ ///
+ /// # Errors
+ ///
+ /// This function will fail if it could not find the `git` tree
+ /// for the `Oid`.
+ pub fn entries(&self, repo: &Repository) -> Result<Entries, error::Directory> {
+ let tree = repo.find_tree(self.id)?;
+
+ let mut entries = BTreeMap::new();
+ let mut error = None;
+ let path = self.path();
+
+ // Walks only the first level of entries. And `_entry_path` is always
+ // empty for the first level.
+ tree.walk(git2::TreeWalkMode::PreOrder, |_entry_path, entry| {
+ match Entry::from_entry(entry, path.clone(), repo) {
+ Ok(entry) => match entry {
+ Entry::File(_) => {
+ entries.insert(entry.name().clone(), entry);
+ git2::TreeWalkResult::Ok
+ }
+ Entry::Directory(_) => {
+ entries.insert(entry.name().clone(), entry);
+ // Skip nested directories
+ git2::TreeWalkResult::Skip
+ }
+ Entry::Submodule(_) => {
+ entries.insert(entry.name().clone(), entry);
+ git2::TreeWalkResult::Ok
+ }
+ },
+ Err(err) => {
+ error = Some(err);
+ git2::TreeWalkResult::Abort
+ }
+ }
+ })?;
+
+ match error {
+ Some(err) => Err(err),
+ None => Ok(Entries { listing: entries }),
+ }
+ }
+
+ /// Find the [`Entry`] found at a non-empty `path`, if it exists.
+ pub fn find_entry<P>(&self, path: &P, repo: &Repository) -> Result<Entry, error::Directory>
+ where
+ P: AsRef<Path>,
+ {
+ // Search the path in git2 tree.
+ let path = path.as_ref();
+ let git2_tree = repo.find_tree(self.id)?;
+ let entry = git2_tree
+ .get_path(path)
+ .or_matches::<error::Directory, _, _>(is_not_found_err, || {
+ Err(error::Directory::PathNotFound(path.to_path_buf()))
+ })?;
+ let parent = path
+ .parent()
+ .ok_or_else(|| error::Directory::InvalidPath(path.to_path_buf()))?;
+ let root_path = self.path().join(parent);
+
+ Entry::from_entry(&entry, root_path, repo)
+ }
+
+ /// Find the `Oid`, for a [`File`], found at `path`, if it exists.
+ pub fn find_file<P>(&self, path: &P, repo: &Repository) -> Result<File, error::Directory>
+ where
+ P: AsRef<Path>,
+ {
+ match self.find_entry(path, repo)? {
+ Entry::File(file) => Ok(file),
+ _ => Err(error::Directory::InvalidType(
+ path.as_ref().to_path_buf(),
+ "file",
+ )),
+ }
+ }
+
+ /// Find the `Directory` found at `path`, if it exists.
+ ///
+ /// If `path` is `ROOT_DIR` (i.e. an empty path), returns self.
+ pub fn find_directory<P>(&self, path: &P, repo: &Repository) -> Result<Self, error::Directory>
+ where
+ P: AsRef<Path>,
+ {
+ if path.as_ref() == Path::new(ROOT_DIR) {
+ return Ok(self.clone());
+ }
+
+ match self.find_entry(path, repo)? {
+ Entry::Directory(d) => Ok(d),
+ _ => Err(error::Directory::InvalidType(
+ path.as_ref().to_path_buf(),
+ "directory",
+ )),
+ }
+ }
+
+ // TODO(fintan): This is going to be a bit trickier so going to leave it out for
+ // now
+ #[allow(dead_code)]
+ fn fuzzy_find(_label: &Path) -> Vec<Self> {
+ unimplemented!()
+ }
+
+ /// Get the total size, in bytes, of a `Directory`. The size is
+ /// the sum of all files that can be reached from this `Directory`.
+ pub fn size(&self, repo: &Repository) -> Result<usize, error::Directory> {
+ self.traverse(repo, 0, &mut |size, entry| match entry {
+ Entry::File(file) => Ok(size + file.content(repo)?.size()),
+ Entry::Directory(dir) => Ok(size + dir.size(repo)?),
+ Entry::Submodule(_) => Ok(size),
+ })
+ }
+
+ /// Traverse the entire `Directory` using the `initial`
+ /// accumulator and the function `f`.
+ ///
+ /// For each [`Entry::Directory`] this will recursively call
+ /// [`Directory::traverse`] and obtain its [`Entries`].
+ ///
+ /// `Error` is the error type of the fallible function.
+ /// `B` is the type of the accumulator.
+ /// `F` is the fallible function that takes the accumulator and
+ /// the next [`Entry`], possibly providing the next accumulator
+ /// value.
+ pub fn traverse<Error, B, F>(
+ &self,
+ repo: &Repository,
+ initial: B,
+ f: &mut F,
+ ) -> Result<B, Error>
+ where
+ Error: From<error::Directory>,
+ F: FnMut(B, &Entry) -> Result<B, Error>,
+ {
+ self.entries(repo)?
+ .entries()
+ .try_fold(initial, |acc, entry| match entry {
+ Entry::File(_) => f(acc, entry),
+ Entry::Directory(directory) => {
+ let acc = directory.traverse(repo, acc, f)?;
+ f(acc, entry)
+ }
+ Entry::Submodule(_) => f(acc, entry),
+ })
+ }
+}
+
+impl Revision for Directory {
+ type Error = Infallible;
+
+ fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+ Ok(self.id)
+ }
+}
+
+/// A representation of a Git [submodule] when encountered in a Git
+/// repository.
+///
+/// [submodule]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Submodule {
+ name: String,
+ prefix: PathBuf,
+ id: Oid,
+ url: Option<Url>,
+}
+
+impl Submodule {
+ /// Construct a new `Submodule`.
+ ///
+ /// The `path` must be the prefix location of the directory, and
+ /// so should not end in `name`.
+ ///
+ /// The `id` is the commit pointer that Git provides when listing
+ /// a submodule.
+ pub fn new(
+ name: String,
+ prefix: PathBuf,
+ submodule: Option<git2::Submodule>,
+ id: Oid,
+ ) -> Result<Self, error::Submodule> {
+ let url = submodule
+ .and_then(|module| {
+ module
+ .opt_url_bytes()
+ .map(|bs| std::str::from_utf8(bs).map(|url| url.to_string()))
+ })
+ .transpose()
+ .map_err(|err| error::Submodule::Utf8 {
+ name: name.clone(),
+ err,
+ })?;
+ let url = url
+ .map(|url| {
+ Url::parse(&url).map_err(|err| error::Submodule::ParseUrl {
+ name: name.clone(),
+ url,
+ err,
+ })
+ })
+ .transpose()?;
+ Ok(Self {
+ name,
+ prefix,
+ id,
+ url,
+ })
+ }
+
+ /// The name of this `Submodule`.
+ pub fn name(&self) -> &String {
+ &self.name
+ }
+
+ /// Return the [`Path`] where this `Submodule` is located, relative to the
+ /// git repository root.
+ pub fn location(&self) -> &Path {
+ &self.prefix
+ }
+
+ /// Return the exact path for this `Submodule`, including the
+ /// `name` of the submodule itself.
+ ///
+ /// The path is relative to the git repository root.
+ pub fn path(&self) -> PathBuf {
+ self.prefix.join(&self.name)
+ }
+
+ /// The object identifier of this `Submodule`.
+ ///
+ /// Note that this does not exist in the parent `Repository`. A
+ /// new `Repository` should be opened for the submodule.
+ pub fn id(&self) -> Oid {
+ self.id
+ }
+
+ /// The URL for the submodule, if it is defined.
+ pub fn url(&self) -> &Option<Url> {
+ &self.url
+ }
+}
diff --git a/crates/radicle-surf/src/glob.rs b/crates/radicle-surf/src/glob.rs
new file mode 100644
index 000000000..c5b0eb632
--- /dev/null
+++ b/crates/radicle-surf/src/glob.rs
@@ -0,0 +1,338 @@
+use std::marker::PhantomData;
+
+use git_ext::ref_format::{
+ self, refname,
+ refspec::{self, PatternString, QualifiedPattern},
+ Qualified, RefStr, RefString,
+};
+use thiserror::Error;
+
+use crate::{Branch, Local, Namespace, Remote, Tag};
+
+#[derive(Debug, Error)]
+pub enum Error {
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+}
+
+/// A collection of globs for a git reference type.
+#[derive(Clone, Debug)]
+pub struct Glob<T> {
+ globs: Vec<QualifiedPattern<'static>>,
+ glob_type: PhantomData<T>, // To support different methods for different T.
+}
+
+impl<T> Default for Glob<T> {
+ fn default() -> Self {
+ Self {
+ globs: Default::default(),
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl<T> Glob<T> {
+ /// Return the [`QualifiedPattern`] globs of this `Glob`.
+ pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
+ self.globs.iter()
+ }
+
+ /// Combine two `Glob`s together by combining their glob lists together.
+ ///
+ /// Note that the `Glob`s must result in the same type,
+ /// e.g. `Glob<Tag>` can only combine with `Glob<Tag>`,
+ /// `Glob<Local>` can combine with `Glob<Remote>`, etc.
+ pub fn and(mut self, other: impl Into<Self>) -> Self {
+ self.globs.extend(other.into().globs);
+ self
+ }
+}
+
+impl Glob<Namespace> {
+ /// Creates the `Glob` that matches all `refs/namespaces`.
+ pub fn all_namespaces() -> Self {
+ Self::namespaces(refspec::pattern!("*"))
+ }
+
+ /// Creates a `Glob` for `refs/namespaces`, starting with `glob`.
+ pub fn namespaces(glob: PatternString) -> Self {
+ let globs = vec![Self::qualify(glob)];
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+
+ /// Adds a `refs/namespaces` pattern to this `Glob`.
+ pub fn insert(mut self, glob: PatternString) -> Self {
+ self.globs.push(Self::qualify(glob));
+ self
+ }
+
+ fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
+ qualify(&refname!("refs/namespaces"), glob).expect("BUG: pattern is qualified")
+ }
+}
+
+impl FromIterator<PatternString> for Glob<Namespace> {
+ fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+ let globs = iter
+ .into_iter()
+ .map(|pat| {
+ qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
+ })
+ .collect();
+
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl Extend<PatternString> for Glob<Namespace> {
+ fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+ self.globs.extend(iter.into_iter().map(|pat| {
+ qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
+ }))
+ }
+}
+
+impl Glob<Tag> {
+ /// Creates a `Glob` that matches all `refs/tags`.
+ pub fn all_tags() -> Self {
+ Self::tags(refspec::pattern!("*"))
+ }
+
+ /// Creates a `Glob` for `refs/tags`, starting with `glob`.
+ pub fn tags(glob: PatternString) -> Self {
+ let globs = vec![Self::qualify(glob)];
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+
+ /// Adds a `refs/tags` pattern to this `Glob`.
+ pub fn insert(mut self, glob: PatternString) -> Self {
+ self.globs.push(Self::qualify(glob));
+ self
+ }
+
+ fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
+ qualify(&refname!("refs/tags"), glob).expect("BUG: pattern is qualified")
+ }
+}
+
+impl FromIterator<PatternString> for Glob<Tag> {
+ fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+ let globs = iter
+ .into_iter()
+ .map(|pat| qualify(&refname!("refs/tags"), pat).expect("BUG: pattern is qualified"))
+ .collect();
+
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl Extend<PatternString> for Glob<Tag> {
+ fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+ self.globs.extend(
+ iter.into_iter()
+ .map(|pat| qualify(&refname!("refs/tag"), pat).expect("BUG: pattern is qualified")),
+ )
+ }
+}
+
+impl Glob<Local> {
+ /// Creates the `Glob` that matches all `refs/heads`.
+ pub fn all_heads() -> Self {
+ Self::heads(refspec::pattern!("*"))
+ }
+
+ /// Creates a `Glob` for `refs/heads`, starting with `glob`.
+ pub fn heads(glob: PatternString) -> Self {
+ let globs = vec![Self::qualify_heads(glob)];
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+
+ /// Adds a `refs/heads` pattern to this `Glob`.
+ pub fn insert(mut self, glob: PatternString) -> Self {
+ self.globs.push(Self::qualify_heads(glob));
+ self
+ }
+
+ /// When chaining `Glob<Local>` with `Glob<Remote>`, use
+ /// `branches` to convert this `Glob<Local>` into a
+ /// `Glob<Branch>`.
+ ///
+ /// # Example
+ /// ```no_run
+ /// Glob::heads(pattern!("features/*"))
+ /// .insert(pattern!("qa/*"))
+ /// .branches()
+ /// .and(Glob::remotes(pattern!("origin/features/*")))
+ /// ```
+ pub fn branches(self) -> Glob<Branch> {
+ self.into()
+ }
+
+ fn qualify_heads(glob: PatternString) -> QualifiedPattern<'static> {
+ qualify(&refname!("refs/heads"), glob).expect("BUG: pattern is qualified")
+ }
+}
+
+impl FromIterator<PatternString> for Glob<Local> {
+ fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+ let globs = iter
+ .into_iter()
+ .map(|pat| qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified"))
+ .collect();
+
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl Extend<PatternString> for Glob<Local> {
+ fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+ self.globs.extend(
+ iter.into_iter().map(|pat| {
+ qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified")
+ }),
+ )
+ }
+}
+
+impl From<Glob<Local>> for Glob<Branch> {
+ fn from(Glob { globs, .. }: Glob<Local>) -> Self {
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl Glob<Remote> {
+ /// Creates the `Glob` that matches all `refs/remotes`.
+ pub fn all_remotes() -> Self {
+ Self::remotes(refspec::pattern!("*"))
+ }
+
+ /// Creates a `Glob` for `refs/remotes`, starting with `glob`.
+ pub fn remotes(glob: PatternString) -> Self {
+ let globs = vec![Self::qualify_remotes(glob)];
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+
+ /// Adds a `refs/remotes` pattern to this `Glob`.
+ pub fn insert(mut self, glob: PatternString) -> Self {
+ self.globs.push(Self::qualify_remotes(glob));
+ self
+ }
+
+ /// When chaining `Glob<Remote>` with `Glob<Local>`, use
+ /// `branches` to convert this `Glob<Remote>` into a
+ /// `Glob<Branch>`.
+ ///
+ /// # Example
+ /// ```no_run
+ /// Glob::remotes(pattern!("origin/features/*"))
+ /// .insert(pattern!("origin/qa/*"))
+ /// .branches()
+ /// .and(Glob::heads(pattern!("features/*")))
+ /// ```
+ pub fn branches(self) -> Glob<Branch> {
+ self.into()
+ }
+
+ fn qualify_remotes(glob: PatternString) -> QualifiedPattern<'static> {
+ qualify(&refname!("refs/remotes"), glob).expect("BUG: pattern is qualified")
+ }
+}
+
+impl FromIterator<PatternString> for Glob<Remote> {
+ fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
+ let globs = iter
+ .into_iter()
+ .map(|pat| qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified"))
+ .collect();
+
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl Extend<PatternString> for Glob<Remote> {
+ fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
+ self.globs.extend(
+ iter.into_iter().map(|pat| {
+ qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified")
+ }),
+ )
+ }
+}
+
+impl From<Glob<Remote>> for Glob<Branch> {
+ fn from(Glob { globs, .. }: Glob<Remote>) -> Self {
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+}
+
+impl Glob<Qualified<'_>> {
+ pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
+ Self {
+ globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
+ glob_type: PhantomData,
+ }
+ }
+
+ /// Creates a `Glob` for `refs/<category>`, starting with `glob`.
+ pub fn categories<R>(category: R, glob: PatternString) -> Self
+ where
+ R: AsRef<RefStr>,
+ {
+ let globs = vec![Self::qualify_category(category, glob)];
+ Self {
+ globs,
+ glob_type: PhantomData,
+ }
+ }
+
+ /// Adds a `refs/<category>` pattern to this `Glob`.
+ pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
+ where
+ R: AsRef<RefStr>,
+ {
+ self.globs.push(Self::qualify_category(category, glob));
+ self
+ }
+
+ fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
+ where
+ R: AsRef<RefStr>,
+ {
+ let prefix = refname!("refs").and(category);
+ qualify(&prefix, glob).expect("BUG: pattern is qualified")
+ }
+}
+
+fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
+ prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
+}
diff --git a/crates/radicle-surf/src/history.rs b/crates/radicle-surf/src/history.rs
new file mode 100644
index 000000000..2ad41e13f
--- /dev/null
+++ b/crates/radicle-surf/src/history.rs
@@ -0,0 +1,98 @@
+use std::{
+ convert::TryFrom,
+ path::{Path, PathBuf},
+};
+
+use crate::{Commit, Error, Repository, ToCommit};
+
+/// An iterator that produces the history of commits for a given `head`.
+///
+/// The lifetime of this struct is attached to the underlying [`Repository`].
+pub struct History<'a> {
+ repo: &'a Repository,
+ head: Commit,
+ revwalk: git2::Revwalk<'a>,
+ filter_by: Option<FilterBy>,
+}
+
+/// Internal implementation, subject to refactoring.
+enum FilterBy {
+ File { path: PathBuf },
+}
+
+impl<'a> History<'a> {
+ /// Creates a new history starting from `head`, in `repo`.
+ pub(crate) fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
+ let head = head
+ .to_commit(repo)
+ .map_err(|err| Error::ToCommit(err.into()))?;
+ let mut revwalk = repo.revwalk()?;
+ revwalk.push(head.id.into())?;
+ let history = Self {
+ repo,
+ head,
+ revwalk,
+ filter_by: None,
+ };
+ Ok(history)
+ }
+
+ /// Returns the first commit (i.e. the head) in the history.
+ pub fn head(&self) -> &Commit {
+ &self.head
+ }
+
+ /// Returns a modified `History` filtered by `path`.
+ ///
+ /// Note that it is possible that a filtered History becomes empty,
+ /// even though calling `.head()` still returns the original head.
+ pub fn by_path<P>(mut self, path: &P) -> Self
+ where
+ P: AsRef<Path>,
+ {
+ self.filter_by = Some(FilterBy::File {
+ path: path.as_ref().to_path_buf(),
+ });
+ self
+ }
+}
+
+impl Iterator for History<'_> {
+ type Item = Result<Commit, Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // Loop through the commits with the optional filtering.
+ while let Some(oid) = self.revwalk.next() {
+ let found = oid
+ .map_err(Error::Git)
+ .and_then(|oid| {
+ let commit = self.repo.find_commit(oid.into())?;
+
+ // Handles the optional filter_by.
+ if let Some(FilterBy::File { path }) = &self.filter_by {
+ // Only check the commit diff if the path is not empty.
+ if !path.as_os_str().is_empty() {
+ let path_opt = self.repo.diff_commit_and_parents(path, &commit)?;
+ if path_opt.is_none() {
+ return Ok(None); // Filter out this commit.
+ }
+ }
+ }
+
+ let commit = Commit::try_from(commit)?;
+ Ok(Some(commit))
+ })
+ .transpose();
+ if found.is_some() {
+ return found;
+ }
+ }
+ None
+ }
+}
+
+impl std::fmt::Debug for History<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "History of {}", self.head.id)
+ }
+}
diff --git a/crates/radicle-surf/src/lib.rs b/crates/radicle-surf/src/lib.rs
new file mode 100644
index 000000000..c577b0651
--- /dev/null
+++ b/crates/radicle-surf/src/lib.rs
@@ -0,0 +1,62 @@
+//! `radicle-surf` is a library to help users explore a Git repository with
+//! ease. It supports browsing a repository via the concept of files and
+//! directories, or via blobs and trees in a git fashion. With the additional
+//! support of [`diff::Diff`] and [`History`], this library can be used to build
+//! an intuitive UI for any Git repository.
+//!
+//! The main entry point of the library API is [`Repository`].
+//!
+//! Let's start surfing!
+//!
+//! ## Serialization with feature `serde`
+//!
+//! Many types in this crate support serialization using [`Serde`][serde]
+//! through the `serde` feature flag for this crate.
+//!
+//! [serde]: https://crates.io/crates/serde
+
+extern crate radicle_git_ext as git_ext;
+
+/// Re-exports.
+pub use radicle_git_ext::ref_format;
+
+/// Represents an object id in Git. Re-exported from `radicle-git-ext`.
+pub type Oid = radicle_git_ext::Oid;
+
+pub mod blob;
+pub mod diff;
+pub mod fs;
+pub mod tree;
+
+/// Private modules with their public types.
+mod repo;
+pub use repo::Repository;
+
+mod glob;
+pub use glob::Glob;
+
+mod history;
+pub use history::History;
+
+mod branch;
+pub use branch::{Branch, Local, Remote};
+
+mod tag;
+pub use tag::Tag;
+
+mod commit;
+pub use commit::{Author, Commit, Time};
+
+mod namespace;
+pub use namespace::Namespace;
+
+mod stats;
+pub use stats::Stats;
+
+mod revision;
+pub use revision::{Revision, Signature, ToCommit};
+
+mod refs;
+
+mod error;
+pub use error::Error;
diff --git a/crates/radicle-surf/src/namespace.rs b/crates/radicle-surf/src/namespace.rs
new file mode 100644
index 000000000..590ff03f1
--- /dev/null
+++ b/crates/radicle-surf/src/namespace.rs
@@ -0,0 +1,169 @@
+use std::{
+ convert::TryFrom,
+ fmt,
+ str::{self, FromStr},
+};
+
+use git_ext::ref_format::{
+ self,
+ refspec::{NamespacedPattern, PatternString, QualifiedPattern},
+ Component, Namespaced, Qualified, RefStr, RefString,
+};
+use nonempty::NonEmpty;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum Error {
+ /// When parsing a namespace we may come across one that was an empty
+ /// string.
+ #[error("namespaces must not be empty")]
+ EmptyNamespace,
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] str::Utf8Error),
+}
+
+/// A `Namespace` value allows us to switch the git namespace of
+/// a repo.
+///
+/// A `Namespace` is one or more name components separated by `/`, e.g. `surf`,
+/// `surf/git`.
+///
+/// For each `Namespace`, the reference name will add a single `refs/namespaces`
+/// prefix, e.g. `refs/namespaces/surf`,
+/// `refs/namespaces/surf/refs/namespaces/git`.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Namespace {
+ // XXX: we rely on RefString being non-empty here, which
+ // git-ref-format ensures that there's no way to construct one.
+ pub(super) namespaces: RefString,
+}
+
+impl Namespace {
+ /// Take a `Qualified` reference name and convert it to a `Namespaced` using
+ /// this `Namespace`.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// let ns = "surf/git".parse::<Namespace>();
+ /// let name = ns.to_namespaced(qualified!("refs/heads/main"));
+ /// assert_eq!(
+ /// name.as_str(),
+ /// "refs/namespaces/surf/refs/namespaces/git/refs/heads/main"
+ /// );
+ /// ```
+ pub(crate) fn to_namespaced<'a>(&self, name: &Qualified<'a>) -> Namespaced<'a> {
+ let mut components = self.namespaces.components().rev();
+ let mut namespaced = name.with_namespace(
+ components
+ .next()
+ .expect("BUG: 'namespaces' cannot be empty"),
+ );
+ for ns in components {
+ let qualified = namespaced.into_qualified();
+ namespaced = qualified.with_namespace(ns);
+ }
+ namespaced
+ }
+
+ /// Take a `QualifiedPattern` reference name and convert it to a
+ /// `NamespacedPattern` using this `Namespace`.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// let ns = "surf/git".parse::<Namespace>();
+ /// let name = ns.to_namespaced(pattern!("refs/heads/*").to_qualified().unwrap());
+ /// assert_eq!(
+ /// name.as_str(),
+ /// "refs/namespaces/surf/refs/namespaces/git/refs/heads/*"
+ /// );
+ /// ```
+ pub(crate) fn to_namespaced_pattern<'a>(
+ &self,
+ pat: &QualifiedPattern<'a>,
+ ) -> NamespacedPattern<'a> {
+ let pattern = PatternString::from(self.namespaces.clone());
+ let mut components = pattern.components().rev();
+ let mut namespaced = pat
+ .with_namespace(
+ components
+ .next()
+ .expect("BUG: 'namespaces' cannot be empty"),
+ )
+ .expect("BUG: 'namespace' cannot have globs");
+ for ns in components {
+ let qualified = namespaced.into_qualified();
+ namespaced = qualified
+ .with_namespace(ns)
+ .expect("BUG: 'namespaces' cannot have globs");
+ }
+ namespaced
+ }
+}
+
+impl fmt::Display for Namespace {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.namespaces)
+ }
+}
+
+impl<'a> From<NonEmpty<Component<'a>>> for Namespace {
+ fn from(cs: NonEmpty<Component<'a>>) -> Self {
+ Self {
+ namespaces: cs.into_iter().collect::<RefString>(),
+ }
+ }
+}
+
+impl TryFrom<&str> for Namespace {
+ type Error = Error;
+
+ fn try_from(name: &str) -> Result<Self, Self::Error> {
+ Self::from_str(name)
+ }
+}
+
+impl TryFrom<&[u8]> for Namespace {
+ type Error = Error;
+
+ fn try_from(namespace: &[u8]) -> Result<Self, Self::Error> {
+ str::from_utf8(namespace)
+ .map_err(Error::from)
+ .and_then(Self::from_str)
+ }
+}
+
+impl FromStr for Namespace {
+ type Err = Error;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ let namespaces = RefStr::try_from_str(name)?.to_ref_string();
+ Ok(Self { namespaces })
+ }
+}
+
+impl From<Namespaced<'_>> for Namespace {
+ fn from(namespaced: Namespaced<'_>) -> Self {
+ let mut namespaces = namespaced.namespace().to_ref_string();
+ let mut qualified = namespaced.strip_namespace();
+ while let Some(namespaced) = qualified.to_namespaced() {
+ namespaces.push(namespaced.namespace());
+ qualified = namespaced.strip_namespace();
+ }
+ Self { namespaces }
+ }
+}
+
+impl TryFrom<&git2::Reference<'_>> for Namespace {
+ type Error = Error;
+
+ fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+ let name = RefStr::try_from_str(str::from_utf8(reference.name_bytes())?)?;
+ name.to_namespaced()
+ .ok_or(Error::EmptyNamespace)
+ .map(Self::from)
+ }
+}
diff --git a/crates/radicle-surf/src/refs.rs b/crates/radicle-surf/src/refs.rs
new file mode 100644
index 000000000..e0b9d71c4
--- /dev/null
+++ b/crates/radicle-surf/src/refs.rs
@@ -0,0 +1,246 @@
+// I think the following `Tags` and `Branches` would be merged
+// using Generic associated types supported in Rust 1.65.0.
+
+use std::{
+ collections::{btree_set, BTreeSet},
+ convert::TryFrom as _,
+};
+
+use git_ext::ref_format::{self, lit, name::Components, Component, Qualified, RefString};
+
+use crate::{tag, Branch, Namespace, Tag};
+
+/// Iterator over [`Tag`]s.
+#[derive(Default)]
+pub struct Tags<'a> {
+ references: Vec<git2::References<'a>>,
+ current: usize,
+}
+
+/// Iterator over the [`Qualified`] names of [`Tag`]s.
+pub struct TagNames<'a> {
+ inner: Tags<'a>,
+}
+
+impl<'a> Tags<'a> {
+ pub(super) fn push(&mut self, references: git2::References<'a>) {
+ self.references.push(references)
+ }
+
+ pub fn names(self) -> TagNames<'a> {
+ TagNames { inner: self }
+ }
+}
+
+impl Iterator for Tags<'_> {
+ type Item = Result<Tag, error::Tag>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while self.current < self.references.len() {
+ match self.references.get_mut(self.current) {
+ Some(refs) => match refs.next() {
+ Some(res) => {
+ return Some(
+ res.map_err(error::Tag::from)
+ .and_then(|r| Tag::try_from(&r).map_err(error::Tag::from)),
+ );
+ }
+ None => self.current += 1,
+ },
+ None => break,
+ }
+ }
+ None
+ }
+}
+
+impl Iterator for TagNames<'_> {
+ type Item = Result<Qualified<'static>, error::Tag>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while self.inner.current < self.inner.references.len() {
+ match self.inner.references.get_mut(self.inner.current) {
+ Some(refs) => match refs.next() {
+ Some(res) => {
+ return Some(res.map_err(error::Tag::from).and_then(|r| {
+ tag::reference_name(&r)
+ .map(|name| lit::refs_tags(name).into())
+ .map_err(error::Tag::from)
+ }))
+ }
+ None => self.inner.current += 1,
+ },
+ None => break,
+ }
+ }
+ None
+ }
+}
+
+/// Iterator over [`Branch`]es.
+#[derive(Default)]
+pub struct Branches<'a> {
+ references: Vec<git2::References<'a>>,
+ current: usize,
+}
+
+/// Iterator over the [`Qualified`] names of [`Branch`]es.
+pub struct BranchNames<'a> {
+ inner: Branches<'a>,
+}
+
+impl<'a> Branches<'a> {
+ pub(super) fn push(&mut self, references: git2::References<'a>) {
+ self.references.push(references)
+ }
+
+ pub fn names(self) -> BranchNames<'a> {
+ BranchNames { inner: self }
+ }
+}
+
+impl Iterator for Branches<'_> {
+ type Item = Result<Branch, error::Branch>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while self.current < self.references.len() {
+ match self.references.get_mut(self.current) {
+ Some(refs) => match refs.next() {
+ Some(res) => {
+ return Some(
+ res.map_err(error::Branch::from)
+ .and_then(|r| Branch::try_from(&r).map_err(error::Branch::from)),
+ )
+ }
+ None => self.current += 1,
+ },
+ None => break,
+ }
+ }
+ None
+ }
+}
+
+impl Iterator for BranchNames<'_> {
+ type Item = Result<Qualified<'static>, error::Branch>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while self.inner.current < self.inner.references.len() {
+ match self.inner.references.get_mut(self.inner.current) {
+ Some(refs) => match refs.next() {
+ Some(res) => {
+ return Some(res.map_err(error::Branch::from).and_then(|r| {
+ Branch::try_from(&r)
+ .map(|branch| branch.refname().into_owned())
+ .map_err(error::Branch::from)
+ }))
+ }
+ None => self.inner.current += 1,
+ },
+ None => break,
+ }
+ }
+ None
+ }
+}
+
+// TODO: not sure this buys us much
+/// An iterator for namespaces.
+pub struct Namespaces {
+ namespaces: btree_set::IntoIter<Namespace>,
+}
+
+impl Namespaces {
+ pub(super) fn new(namespaces: BTreeSet<Namespace>) -> Self {
+ Self {
+ namespaces: namespaces.into_iter(),
+ }
+ }
+}
+
+impl Iterator for Namespaces {
+ type Item = Namespace;
+ fn next(&mut self) -> Option<Self::Item> {
+ self.namespaces.next()
+ }
+}
+
+#[derive(Default)]
+pub struct Categories<'a> {
+ references: Vec<git2::References<'a>>,
+ current: usize,
+}
+
+impl<'a> Categories<'a> {
+ pub(super) fn push(&mut self, references: git2::References<'a>) {
+ self.references.push(references)
+ }
+}
+
+impl Iterator for Categories<'_> {
+ type Item = Result<(RefString, RefString), error::Category>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while self.current < self.references.len() {
+ match self.references.get_mut(self.current) {
+ Some(refs) => match refs.next() {
+ Some(res) => {
+ return Some(res.map_err(error::Category::from).and_then(|r| {
+ let name = std::str::from_utf8(r.name_bytes())?;
+ let name = ref_format::RefStr::try_from_str(name)?;
+ let name = name.qualified().ok_or_else(|| {
+ error::Category::NotQualified(name.to_ref_string())
+ })?;
+ let (_refs, category, c, cs) = name.non_empty_components();
+ Ok((category.to_ref_string(), refstr_join(c, cs)))
+ }));
+ }
+ None => self.current += 1,
+ },
+ None => break,
+ }
+ }
+ None
+ }
+}
+
+pub mod error {
+ use std::str;
+
+ use radicle_git_ext::ref_format::{self, RefString};
+ use thiserror::Error;
+
+ use crate::{branch, tag};
+
+ #[derive(Debug, Error)]
+ pub enum Branch {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error(transparent)]
+ Branch(#[from] branch::error::Branch),
+ }
+
+ #[derive(Debug, Error)]
+ pub enum Category {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error("the reference '{0}' was expected to be qualified, i.e. 'refs/<category>/<path>'")]
+ NotQualified(RefString),
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] str::Utf8Error),
+ }
+
+ #[derive(Debug, Error)]
+ pub enum Tag {
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error(transparent)]
+ Tag(#[from] tag::error::FromReference),
+ }
+}
+
+pub(crate) fn refstr_join<'a>(c: Component<'a>, cs: Components<'a>) -> RefString {
+ std::iter::once(c).chain(cs).collect::<RefString>()
+}
diff --git a/crates/radicle-surf/src/repo.rs b/crates/radicle-surf/src/repo.rs
new file mode 100644
index 000000000..162d24876
--- /dev/null
+++ b/crates/radicle-surf/src/repo.rs
@@ -0,0 +1,569 @@
+use std::{
+ collections::BTreeSet,
+ convert::TryFrom,
+ path::{Path, PathBuf},
+ str,
+};
+
+use git_ext::{
+ ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString},
+ Oid,
+};
+
+use crate::{
+ blob::{Blob, BlobRef},
+ diff::{Diff, FileDiff},
+ fs::{Directory, File, FileContent},
+ refs::{BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
+ tree::{Entry, Tree},
+ Branch, Commit, Error, Glob, History, Namespace, Revision, Signature, Stats, Tag, ToCommit,
+};
+
+/// Enumeration of errors that can occur in repo operations.
+pub mod error {
+ use std::path::PathBuf;
+ use thiserror::Error;
+
+ #[derive(Debug, Error)]
+ #[non_exhaustive]
+ pub enum Repo {
+ #[error("path not found for: {0}")]
+ PathNotFound(PathBuf),
+ }
+}
+
+/// Represents the state associated with a Git repository.
+///
+/// Many other types in this crate are derived from methods in this struct.
+pub struct Repository {
+ /// Wrapper around the `git2`'s `git2::Repository` type.
+ /// This is to to limit the functionality that we can do
+ /// on the underlying object.
+ inner: git2::Repository,
+}
+
+////////////////////////////////////////////
+// Public API, ONLY add `pub fn` in here. //
+////////////////////////////////////////////
+impl Repository {
+ /// Open a git repository given its exact URI.
+ ///
+ /// # Errors
+ ///
+ /// * [`Error::Git`]
+ pub fn open(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
+ let repo = git2::Repository::open(repo_uri)?;
+ Ok(Self { inner: repo })
+ }
+
+ /// Attempt to open a git repository at or above `repo_uri` in the file
+ /// system.
+ pub fn discover(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
+ let repo = git2::Repository::discover(repo_uri)?;
+ Ok(Self { inner: repo })
+ }
+
+ /// What is the current namespace we're browsing in.
+ pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
+ self.inner
+ .namespace_bytes()
+ .map(|ns| Namespace::try_from(ns).map_err(Error::from))
+ .transpose()
+ }
+
+ /// Switch to a `namespace`
+ pub fn switch_namespace(&self, namespace: &RefString) -> Result<(), Error> {
+ Ok(self.inner.set_namespace(namespace.as_str())?)
+ }
+
+ pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
+ where
+ F: FnOnce() -> Result<T, Error>,
+ {
+ self.switch_namespace(namespace)?;
+ let res = f();
+ self.inner.remove_namespace()?;
+ res
+ }
+
+ /// Returns an iterator of branches that match `pattern`.
+ pub fn branches<'a, G>(&'a self, pattern: G) -> Result<Branches<'a>, Error>
+ where
+ G: Into<Glob<Branch>>,
+ {
+ let pattern = pattern.into();
+ let mut branches = Branches::default();
+ for glob in pattern.globs() {
+ let namespaced = self.namespaced_pattern(glob)?;
+ let references = self.inner.references_glob(&namespaced)?;
+ branches.push(references);
+ }
+ Ok(branches)
+ }
+
+ /// Lists branch names with `filter`.
+ pub fn branch_names<'a, G>(&'a self, filter: G) -> Result<BranchNames<'a>, Error>
+ where
+ G: Into<Glob<Branch>>,
+ {
+ Ok(self.branches(filter)?.names())
+ }
+
+ /// Returns an iterator of tags that match `pattern`.
+ pub fn tags<'a>(&'a self, pattern: &Glob<Tag>) -> Result<Tags<'a>, Error> {
+ let mut tags = Tags::default();
+ for glob in pattern.globs() {
+ let namespaced = self.namespaced_pattern(glob)?;
+ let references = self.inner.references_glob(&namespaced)?;
+ tags.push(references);
+ }
+ Ok(tags)
+ }
+
+ /// Lists tag names in the local RefScope.
+ pub fn tag_names<'a>(&'a self, filter: &Glob<Tag>) -> Result<TagNames<'a>, Error> {
+ Ok(self.tags(filter)?.names())
+ }
+
+ pub fn categories<'a>(
+ &'a self,
+ pattern: &Glob<Qualified<'_>>,
+ ) -> Result<Categories<'a>, Error> {
+ let mut cats = Categories::default();
+ for glob in pattern.globs() {
+ let namespaced = self.namespaced_pattern(glob)?;
+ let references = self.inner.references_glob(&namespaced)?;
+ cats.push(references);
+ }
+ Ok(cats)
+ }
+
+ /// Returns an iterator of namespaces that match `pattern`.
+ pub fn namespaces(&self, pattern: &Glob<Namespace>) -> Result<Namespaces, Error> {
+ let mut set = BTreeSet::new();
+ for glob in pattern.globs() {
+ let new_set = self
+ .inner
+ .references_glob(glob)?
+ .map(|reference| {
+ reference
+ .map_err(Error::Git)
+ .and_then(|r| Namespace::try_from(&r).map_err(Error::from))
+ })
+ .collect::<Result<BTreeSet<Namespace>, Error>>()?;
+ set.extend(new_set);
+ }
+ Ok(Namespaces::new(set))
+ }
+
+ /// Get the [`Diff`] between two commits.
+ pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
+ let from_commit = self.find_commit(self.object_id(&from)?)?;
+ let to_commit = self.find_commit(self.object_id(&to)?)?;
+ self.diff_commits(None, Some(&from_commit), &to_commit)
+ .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
+ }
+
+ /// Get the [`Diff`] of a `commit`.
+ ///
+ /// If the `commit` has a parent, then it the diff will be a
+ /// comparison between itself and that parent. Otherwise, the left
+ /// hand side of the diff will pass nothing.
+ pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error> {
+ let commit = commit
+ .to_commit(self)
+ .map_err(|err| Error::ToCommit(err.into()))?;
+ match commit.parents.first() {
+ Some(parent) => self.diff(*parent, commit.id),
+ None => self.initial_diff(commit.id),
+ }
+ }
+
+ /// Get the [`FileDiff`] between two revisions for a file at `path`.
+ ///
+ /// If `path` is only a directory name, not a file, returns
+ /// a [`FileDiff`] for any file under `path`.
+ pub fn diff_file<P: AsRef<Path>, R: Revision>(
+ &self,
+ path: &P,
+ from: R,
+ to: R,
+ ) -> Result<FileDiff, Error> {
+ let from_commit = self.find_commit(self.object_id(&from)?)?;
+ let to_commit = self.find_commit(self.object_id(&to)?)?;
+ let diff = self
+ .diff_commits(Some(path.as_ref()), Some(&from_commit), &to_commit)
+ .and_then(|diff| Diff::try_from(diff).map_err(Error::from))?;
+ let file_diff = diff
+ .into_files()
+ .pop()
+ .ok_or(error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
+ Ok(file_diff)
+ }
+
+ /// Parse an [`Oid`] from the given string.
+ pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
+ Ok(self.inner.revparse_single(oid)?.id().into())
+ }
+
+ /// Returns a top level `Directory` without nested sub-directories.
+ ///
+ /// To visit inside any nested sub-directories, call `directory.get(&repo)`
+ /// on the sub-directory.
+ pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
+ let commit = commit
+ .to_commit(self)
+ .map_err(|err| Error::ToCommit(err.into()))?;
+ let git2_commit = self.inner.find_commit((commit.id).into())?;
+ let tree = git2_commit.as_object().peel_to_tree()?;
+ Ok(Directory::root(tree.id().into()))
+ }
+
+ /// Returns a [`Directory`] for `path` in `commit`.
+ pub fn directory<C: ToCommit, P: AsRef<Path>>(
+ &self,
+ commit: C,
+ path: &P,
+ ) -> Result<Directory, Error> {
+ let root = self.root_dir(commit)?;
+ Ok(root.find_directory(path, self)?)
+ }
+
+ /// Returns a [`File`] for `path` in `commit`.
+ pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
+ let root = self.root_dir(commit)?;
+ Ok(root.find_file(path, self)?)
+ }
+
+ /// Returns a [`Tree`] for `path` in `commit`.
+ pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
+ let commit = commit
+ .to_commit(self)
+ .map_err(|e| Error::ToCommit(e.into()))?;
+ let dir = self.directory(commit.id, path)?;
+ let mut entries = dir
+ .entries(self)?
+ .map(|en| {
+ let name = en.name().to_string();
+ let path = en.path();
+ Ok(Entry::new(name, path, en.into(), commit.clone()))
+ })
+ .collect::<Result<Vec<Entry>, Error>>()?;
+ entries.sort();
+
+ Ok(Tree::new(
+ dir.id(),
+ entries,
+ commit,
+ path.as_ref().to_path_buf(),
+ ))
+ }
+
+ /// Returns a [`Blob`] for `path` in `commit`.
+ pub fn blob<'a, C: ToCommit, P: AsRef<Path>>(
+ &'a self,
+ commit: C,
+ path: &P,
+ ) -> Result<Blob<BlobRef<'a>>, Error> {
+ let commit = commit
+ .to_commit(self)
+ .map_err(|e| Error::ToCommit(e.into()))?;
+ let file = self.file(commit.id, path)?;
+ let last_commit = self
+ .last_commit(path, commit)?
+ .ok_or_else(|| error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
+ let git2_blob = self.find_blob(file.id())?;
+ Ok(Blob::<BlobRef<'a>>::new(file.id(), git2_blob, last_commit))
+ }
+
+ pub fn blob_ref(&self, oid: Oid) -> Result<BlobRef<'_>, Error> {
+ Ok(BlobRef {
+ inner: self.find_blob(oid)?,
+ })
+ }
+
+ /// Returns the last commit, if exists, for a `path` in the history of
+ /// `rev`.
+ pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
+ where
+ P: AsRef<Path>,
+ C: ToCommit,
+ {
+ let history = self.history(rev)?;
+ history.by_path(path).next().transpose()
+ }
+
+ /// Returns a commit for `rev`, if it exists.
+ pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
+ rev.to_commit(self)
+ }
+
+ /// Gets the [`Stats`] of this repository starting from the
+ /// `HEAD` (see [`Repository::head`]) of the repository.
+ pub fn stats(&self) -> Result<Stats, Error> {
+ self.stats_from(&self.head()?)
+ }
+
+ /// Gets the [`Stats`] of this repository starting from the given
+ /// `rev`.
+ pub fn stats_from<R>(&self, rev: &R) -> Result<Stats, Error>
+ where
+ R: Revision,
+ {
+ let branches = self.branches(Glob::all_heads())?.count();
+ let mut history = self.history(rev)?;
+ let (commits, contributors) = history.try_fold(
+ (0, BTreeSet::new()),
+ |(commits, mut contributors), commit| {
+ let commit = commit?;
+ contributors.insert((commit.author.name, commit.author.email));
+ Ok::<_, Error>((commits + 1, contributors))
+ },
+ )?;
+ Ok(Stats {
+ branches,
+ commits,
+ contributors: contributors.len(),
+ })
+ }
+
+ // TODO(finto): I think this can be removed in favour of using
+ // `source::Blob::new`
+ /// Retrieves the file with `path` in this commit.
+ pub fn get_commit_file<'a, P, R>(&'a self, rev: &R, path: &P) -> Result<FileContent<'a>, Error>
+ where
+ P: AsRef<Path>,
+ R: Revision,
+ {
+ let path = path.as_ref();
+ let id = self.object_id(rev)?;
+ let commit = self.find_commit(id)?;
+ let tree = commit.tree()?;
+ let entry = tree.get_path(path)?;
+ let object = entry.to_object(&self.inner)?;
+ let blob = object
+ .into_blob()
+ .map_err(|_| error::Repo::PathNotFound(path.to_path_buf()))?;
+ Ok(FileContent::new(blob))
+ }
+
+ /// Returns the [`Oid`] of the current `HEAD`.
+ pub fn head(&self) -> Result<Oid, Error> {
+ let head = self.inner.head()?;
+ let head_commit = head.peel_to_commit()?;
+ Ok(head_commit.id().into())
+ }
+
+ /// Extract the signature from a commit
+ ///
+ /// # Arguments
+ ///
+ /// `field` - the name of the header field containing the signature block;
+ /// pass `None` to extract the default 'gpgsig'
+ pub fn extract_signature(
+ &self,
+ commit: impl ToCommit,
+ field: Option<&str>,
+ ) -> Result<Option<Signature>, Error> {
+ // Match is necessary here because according to the documentation for
+ // git_commit_extract_signature at
+ // https://libgit2.org/libgit2/#HEAD/group/commit/git_commit_extract_signature
+ // the return value for a commit without a signature will be GIT_ENOTFOUND
+ let commit = commit
+ .to_commit(self)
+ .map_err(|e| Error::ToCommit(e.into()))?;
+
+ match self.inner.extract_signature(&commit.id, field) {
+ Err(error) => {
+ if error.code() == git2::ErrorCode::NotFound {
+ Ok(None)
+ } else {
+ Err(error.into())
+ }
+ }
+ Ok(sig) => Ok(Some(Signature::from(sig.0))),
+ }
+ }
+
+ /// Returns the history with the `head` commit.
+ pub fn history<'a, C: ToCommit>(&'a self, head: C) -> Result<History<'a>, Error> {
+ History::new(self, head)
+ }
+
+ /// Lists branches that are reachable from `rev`.
+ pub fn revision_branches(
+ &self,
+ rev: impl Revision,
+ glob: Glob<Branch>,
+ ) -> Result<Vec<Branch>, Error> {
+ let oid = self.object_id(&rev)?;
+ let mut contained_branches = vec![];
+ for branch in self.branches(glob)? {
+ let branch = branch?;
+ let namespaced = self.namespaced_refname(&branch.refname())?;
+ let reference = self.inner.find_reference(namespaced.as_str())?;
+ if self.reachable_from(&reference, &oid)? {
+ contained_branches.push(branch);
+ }
+ }
+
+ Ok(contained_branches)
+ }
+
+ pub fn object_format(&self) -> git2::ObjectFormat {
+ self.inner.object_format()
+ }
+}
+
+////////////////////////////////////////////////////////////
+// Private API, ONLY add `pub(crate) fn` or `fn` in here. //
+////////////////////////////////////////////////////////////
+impl Repository {
+ pub(crate) fn is_bare(&self) -> bool {
+ self.inner.is_bare()
+ }
+
+ pub(crate) fn find_submodule<'a>(
+ &'a self,
+ name: &str,
+ ) -> Result<git2::Submodule<'a>, git2::Error> {
+ self.inner.find_submodule(name)
+ }
+
+ pub(crate) fn find_blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git2::Error> {
+ self.inner.find_blob(oid.into())
+ }
+
+ pub(crate) fn find_commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git2::Error> {
+ self.inner.find_commit(oid.into())
+ }
+
+ pub(crate) fn find_tree(&self, oid: Oid) -> Result<git2::Tree<'_>, git2::Error> {
+ self.inner.find_tree(oid.into())
+ }
+
+ pub(crate) fn refname_to_id<R>(&self, name: &R) -> Result<Oid, git2::Error>
+ where
+ R: AsRef<RefStr>,
+ {
+ self.inner
+ .refname_to_id(name.as_ref().as_str())
+ .map(Oid::from)
+ }
+
+ pub(crate) fn revwalk(&self) -> Result<git2::Revwalk<'_>, git2::Error> {
+ self.inner.revwalk()
+ }
+
+ pub(super) fn object_id<R: Revision>(&self, r: &R) -> Result<Oid, Error> {
+ r.object_id(self).map_err(|err| Error::Revision(err.into()))
+ }
+
+ /// Get the [`Diff`] of a commit with no parents.
+ fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
+ let commit = self.find_commit(self.object_id(&rev)?)?;
+ self.diff_commits(None, None, &commit)
+ .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
+ }
+
+ fn reachable_from(&self, reference: &git2::Reference, oid: &Oid) -> Result<bool, Error> {
+ let git2_oid = (*oid).into();
+ let other = reference.peel_to_commit()?.id();
+ let is_descendant = self.inner.graph_descendant_of(other, git2_oid)?;
+
+ Ok(other == git2_oid || is_descendant)
+ }
+
+ pub(crate) fn diff_commit_and_parents<P>(
+ &self,
+ path: &P,
+ commit: &git2::Commit,
+ ) -> Result<Option<PathBuf>, Error>
+ where
+ P: AsRef<Path>,
+ {
+ let mut parents = commit.parents();
+
+ let diff = self.diff_commits(Some(path.as_ref()), parents.next().as_ref(), commit)?;
+ if let Some(_delta) = diff.deltas().next() {
+ Ok(Some(path.as_ref().to_path_buf()))
+ } else {
+ Ok(None)
+ }
+ }
+
+ /// Create a diff with the difference between two tree objects.
+ ///
+ /// Defines some options and flags that are passed to git2.
+ ///
+ /// Note:
+ /// libgit2 optimizes around not loading the content when there's no content
+ /// callbacks configured. Be aware that binaries aren't detected as
+ /// expected.
+ ///
+ /// Reference: <https://github.com/libgit2/libgit2/issues/6637>
+ fn diff_commits<'a>(
+ &'a self,
+ path: Option<&Path>,
+ from: Option<&git2::Commit>,
+ to: &git2::Commit,
+ ) -> Result<git2::Diff<'a>, Error> {
+ let new_tree = to.tree()?;
+ let old_tree = from.map_or(Ok(None), |c| c.tree().map(Some))?;
+
+ let mut opts = git2::DiffOptions::new();
+ if let Some(path) = path {
+ opts.pathspec(path.to_string_lossy().to_string());
+ opts.disable_pathspec_match(true);
+ opts.skip_binary_check(false);
+ }
+
+ let mut diff =
+ self.inner
+ .diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut opts))?;
+
+ // Detect renames by default.
+ let mut find_opts = git2::DiffFindOptions::new();
+ find_opts.renames(true);
+ find_opts.copies(true);
+ diff.find_similar(Some(&mut find_opts))?;
+
+ Ok(diff)
+ }
+
+ /// Returns a full reference name with namespace(s) included.
+ pub(crate) fn namespaced_refname<'a>(
+ &'a self,
+ refname: &Qualified<'a>,
+ ) -> Result<Qualified<'a>, Error> {
+ let fullname = match self.which_namespace()? {
+ Some(namespace) => namespace.to_namespaced(refname).into_qualified(),
+ None => refname.clone(),
+ };
+ Ok(fullname)
+ }
+
+ /// Returns a full reference name with namespace(s) included.
+ fn namespaced_pattern<'a>(
+ &'a self,
+ refname: &QualifiedPattern<'a>,
+ ) -> Result<QualifiedPattern<'a>, Error> {
+ let fullname = match self.which_namespace()? {
+ Some(namespace) => namespace.to_namespaced_pattern(refname).into_qualified(),
+ None => refname.clone(),
+ };
+ Ok(fullname)
+ }
+}
+
+impl From<git2::Repository> for Repository {
+ fn from(repo: git2::Repository) -> Self {
+ Repository { inner: repo }
+ }
+}
+
+impl std::fmt::Debug for Repository {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, ".git")
+ }
+}
diff --git a/crates/radicle-surf/src/revision.rs b/crates/radicle-surf/src/revision.rs
new file mode 100644
index 000000000..4e34f63f9
--- /dev/null
+++ b/crates/radicle-surf/src/revision.rs
@@ -0,0 +1,125 @@
+use std::convert::Infallible;
+
+use git_ext::{
+ ref_format::{Qualified, RefString},
+ Oid,
+};
+
+use crate::{Branch, Commit, Error, Repository, Tag};
+
+/// The signature of a commit
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Signature(Vec<u8>);
+
+impl From<git2::Buf> for Signature {
+ fn from(other: git2::Buf) -> Self {
+ Signature((*other).into())
+ }
+}
+
+/// Supports various ways to specify a revision used in Git.
+pub trait Revision {
+ type Error: std::error::Error + Send + Sync + 'static;
+
+ /// Returns the object id of this revision in `repo`.
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error>;
+}
+
+impl Revision for RefString {
+ type Error = git2::Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ repo.refname_to_id(self)
+ }
+}
+
+impl Revision for Qualified<'_> {
+ type Error = git2::Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ repo.refname_to_id(self)
+ }
+}
+
+impl Revision for Oid {
+ type Error = Infallible;
+
+ fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+ Ok(*self)
+ }
+}
+
+impl Revision for &str {
+ type Error = git2::Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ Oid::from_str_ext(self, repo.object_format())
+ }
+}
+
+impl Revision for Branch {
+ type Error = Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ let refname = repo.namespaced_refname(&self.refname())?;
+ Ok(repo.refname_to_id(&refname)?)
+ }
+}
+
+impl Revision for Tag {
+ type Error = Infallible;
+
+ fn object_id(&self, _repo: &Repository) -> Result<Oid, Self::Error> {
+ Ok(self.id())
+ }
+}
+
+impl Revision for String {
+ type Error = git2::Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ Oid::from_str_ext(self, repo.object_format())
+ }
+}
+
+impl<R: Revision> Revision for &R {
+ type Error = R::Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ (*self).object_id(repo)
+ }
+}
+
+impl<R: Revision> Revision for Box<R> {
+ type Error = R::Error;
+
+ fn object_id(&self, repo: &Repository) -> Result<Oid, Self::Error> {
+ self.as_ref().object_id(repo)
+ }
+}
+
+/// A common trait for anything that can convert to a `Commit`.
+pub trait ToCommit {
+ type Error: std::error::Error + Send + Sync + 'static;
+
+ /// Converts to a commit in `repo`.
+ fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error>;
+}
+
+impl ToCommit for Commit {
+ type Error = Infallible;
+
+ fn to_commit(self, _repo: &Repository) -> Result<Commit, Self::Error> {
+ Ok(self)
+ }
+}
+
+impl<R: Revision> ToCommit for R {
+ type Error = Error;
+
+ fn to_commit(self, repo: &Repository) -> Result<Commit, Self::Error> {
+ let oid = repo.object_id(&self)?;
+ let commit = repo.find_commit(oid)?;
+ Ok(Commit::try_from(commit)?)
+ }
+}
diff --git a/crates/radicle-surf/src/stats.rs b/crates/radicle-surf/src/stats.rs
new file mode 100644
index 000000000..f7f257c17
--- /dev/null
+++ b/crates/radicle-surf/src/stats.rs
@@ -0,0 +1,14 @@
+#[cfg(feature = "serde")]
+use serde::Serialize;
+
+/// Stats for a repository
+#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct Stats {
+ /// Number of commits
+ pub commits: usize,
+ /// Number of local branches
+ pub branches: usize,
+ /// Number of contributors
+ pub contributors: usize,
+}
diff --git a/crates/radicle-surf/src/tag.rs b/crates/radicle-surf/src/tag.rs
new file mode 100644
index 000000000..daad8b729
--- /dev/null
+++ b/crates/radicle-surf/src/tag.rs
@@ -0,0 +1,160 @@
+use std::{convert::TryFrom, str};
+
+use git_ext::{
+ ref_format::{component, lit, Qualified, RefStr, RefString},
+ Oid,
+};
+
+use crate::{refs::refstr_join, Author};
+
+/// The metadata of a [`Git tag`][git-tag].
+///
+/// [git-tag]: https://git-scm.com/book/en/v2/Git-Basics-Tagging
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Tag {
+ /// A light-weight git tag.
+ Light {
+ /// The Object ID for the `Tag`, i.e the SHA1 digest.
+ id: Oid,
+ /// The reference name for this `Tag`.
+ name: RefString,
+ },
+ /// An annotated git tag.
+ Annotated {
+ /// The Object ID for the `Tag`, i.e the SHA1 digest.
+ id: Oid,
+ /// The Object ID for the object that is tagged.
+ target: Oid,
+ /// The reference name for this `Tag`.
+ name: RefString,
+ /// The named author of this `Tag`, if the `Tag` was annotated.
+ tagger: Option<Author>,
+ /// The message with this `Tag`, if the `Tag` was annotated.
+ message: Option<String>,
+ },
+}
+
+impl Tag {
+ /// Get the `Oid` of the tag, regardless of its type.
+ pub fn id(&self) -> Oid {
+ match self {
+ Self::Light { id, .. } => *id,
+ Self::Annotated { id, .. } => *id,
+ }
+ }
+
+ /// Return the short `Tag` refname,
+ /// e.g. `release/v1`.
+ pub fn short_name(&self) -> &RefString {
+ match &self {
+ Tag::Light { name, .. } => name,
+ Tag::Annotated { name, .. } => name,
+ }
+ }
+
+ /// Return the fully qualified `Tag` refname,
+ /// e.g. `refs/tags/release/v1`.
+ pub fn refname<'a>(&'a self) -> Qualified<'a> {
+ lit::refs_tags(self.short_name()).into()
+ }
+}
+
+pub mod error {
+ use std::str;
+
+ use radicle_git_ext::ref_format::{self, RefString};
+ use thiserror::Error;
+
+ #[derive(Debug, Error)]
+ pub enum FromTag {
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] str::Utf8Error),
+ }
+
+ #[derive(Debug, Error)]
+ pub enum FromReference {
+ #[error(transparent)]
+ FromTag(#[from] FromTag),
+ #[error(transparent)]
+ Git(#[from] git2::Error),
+ #[error("the refname '{0}' did not begin with 'refs/tags'")]
+ NotQualified(String),
+ #[error("the refname '{0}' did not begin with 'refs/tags'")]
+ NotTag(RefString),
+ #[error(transparent)]
+ RefFormat(#[from] ref_format::Error),
+ #[error(transparent)]
+ Utf8(#[from] str::Utf8Error),
+ }
+}
+
+impl TryFrom<&git2::Tag<'_>> for Tag {
+ type Error = error::FromTag;
+
+ fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
+ let id = tag.id().into();
+ let target = tag.target_id().into();
+ let name = {
+ let name = str::from_utf8(tag.name_bytes())?;
+ RefStr::try_from_str(name)?.to_ref_string()
+ };
+ let tagger = tag.tagger().map(Author::try_from).transpose()?;
+ let message = tag
+ .message_bytes()
+ .map(str::from_utf8)
+ .transpose()?
+ .map(|message| message.into());
+
+ Ok(Tag::Annotated {
+ id,
+ target,
+ name,
+ tagger,
+ message,
+ })
+ }
+}
+
+impl TryFrom<&git2::Reference<'_>> for Tag {
+ type Error = error::FromReference;
+
+ fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
+ let name = reference_name(reference)?;
+ match reference.peel_to_tag() {
+ Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
+ // If we get an error peeling to a tag _BUT_ we also have confirmed the
+ // reference is a tag, that means we have a lightweight tag,
+ // i.e. a commit SHA and name.
+ Err(err)
+ if err.class() == git2::ErrorClass::Object
+ && err.code() == git2::ErrorCode::InvalidSpec =>
+ {
+ let commit = reference.peel_to_commit()?;
+ Ok(Tag::Light {
+ id: commit.id().into(),
+ name,
+ })
+ }
+ Err(err) => Err(err.into()),
+ }
+ }
+}
+
+pub(crate) fn reference_name(
+ reference: &git2::Reference,
+) -> Result<RefString, error::FromReference> {
+ let name = str::from_utf8(reference.name_bytes())?;
+ let name = RefStr::try_from_str(name)?
+ .qualified()
+ .ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?;
+
+ let (_refs, tags, c, cs) = name.non_empty_components();
+
+ if tags == component::TAGS {
+ Ok(refstr_join(c, cs))
+ } else {
+ Err(error::FromReference::NotTag(name.into()))
+ }
+}
diff --git a/crates/radicle-surf/src/tree.rs b/crates/radicle-surf/src/tree.rs
new file mode 100644
index 000000000..2f691d71c
--- /dev/null
+++ b/crates/radicle-surf/src/tree.rs
@@ -0,0 +1,290 @@
+//! Represents git object type 'tree', i.e. like directory entries in Unix.
+//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
+
+use std::cmp::Ordering;
+use std::path::PathBuf;
+
+use radicle_git_ext::Oid;
+#[cfg(feature = "serde")]
+use serde::{
+ ser::{SerializeStruct as _, Serializer},
+ Serialize,
+};
+use url::Url;
+
+use crate::{fs, Commit, Error, Repository};
+
+/// Represents a tree object as in git. It is essentially the content of
+/// one directory. Note that multiple directories can have the same content,
+/// i.e. have the same tree object. Hence this struct does not embed its path.
+#[derive(Clone, Debug)]
+pub struct Tree {
+ /// The object id of this tree.
+ id: Oid,
+ /// The first descendant entries for this tree.
+ entries: Vec<Entry>,
+ /// The commit object that created this tree object.
+ commit: Commit,
+ /// The root path this tree was constructed from.
+ root: PathBuf,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum LastCommitError {
+ #[error(transparent)]
+ Repo(#[from] Error),
+ #[error("could not get the last commit for this entry")]
+ Missing,
+}
+
+impl Tree {
+ /// Creates a new tree, ensuring the `entries` are sorted.
+ pub(crate) fn new(id: Oid, mut entries: Vec<Entry>, commit: Commit, root: PathBuf) -> Self {
+ entries.sort();
+ Self {
+ id,
+ entries,
+ commit,
+ root,
+ }
+ }
+
+ pub fn object_id(&self) -> Oid {
+ self.id
+ }
+
+ /// Returns the commit for which this [`Tree`] was constructed from.
+ pub fn commit(&self) -> &Commit {
+ &self.commit
+ }
+
+ /// Returns the commit that last touched this [`Tree`].
+ pub fn last_commit(&self, repo: &Repository) -> Result<Commit, LastCommitError> {
+ repo.last_commit(&self.root, self.commit().clone())?
+ .ok_or(LastCommitError::Missing)
+ }
+
+ /// Returns the entries of the tree.
+ pub fn entries(&self) -> &Vec<Entry> {
+ &self.entries
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Tree {
+ /// Sample output:
+ /// (for `<entry_1>` and `<entry_2>` sample output, see [`Entry`])
+ /// ```
+ /// {
+ /// "entries": [
+ /// { <entry_1> },
+ /// { <entry_2> },
+ /// ],
+ /// "root": "src/foo",
+ /// "commit": {
+ /// "author": {
+ /// "email": "foobar@gmail.com",
+ /// "name": "Foo Bar"
+ /// },
+ /// "committer": {
+ /// "email": "noreply@github.com",
+ /// "name": "GitHub"
+ /// },
+ /// "committerTime": 1582198877,
+ /// "description": "A sample commit.",
+ /// "sha1": "b57846bbc8ced6587bf8329fc4bce970eb7b757e",
+ /// "summary": "Add a new sample"
+ /// },
+ /// "oid": "dd52e9f8dfe1d8b374b2a118c25235349a743dd2"
+ /// }
+ /// ```
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ const FIELDS: usize = 4;
+ let mut state = serializer.serialize_struct("Tree", FIELDS)?;
+ state.serialize_field("oid", &self.id)?;
+ state.serialize_field("entries", &self.entries)?;
+ state.serialize_field("commit", &self.commit)?;
+ state.serialize_field("root", &self.root)?;
+ state.end()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum EntryKind {
+ Tree(Oid),
+ Blob(Oid),
+ Submodule { id: Oid, url: Option<Url> },
+}
+
+impl PartialOrd for EntryKind {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for EntryKind {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match (self, other) {
+ (EntryKind::Submodule { .. }, EntryKind::Submodule { .. }) => Ordering::Equal,
+ (EntryKind::Submodule { .. }, EntryKind::Tree(_)) => Ordering::Equal,
+ (EntryKind::Tree(_), EntryKind::Submodule { .. }) => Ordering::Equal,
+ (EntryKind::Tree(_), EntryKind::Tree(_)) => Ordering::Equal,
+ (EntryKind::Tree(_), EntryKind::Blob(_)) => Ordering::Less,
+ (EntryKind::Blob(_), EntryKind::Tree(_)) => Ordering::Greater,
+ (EntryKind::Submodule { .. }, EntryKind::Blob(_)) => Ordering::Less,
+ (EntryKind::Blob(_), EntryKind::Submodule { .. }) => Ordering::Greater,
+ (EntryKind::Blob(_), EntryKind::Blob(_)) => Ordering::Equal,
+ }
+ }
+}
+
+/// An entry that can be found in a tree.
+///
+/// # Ordering
+///
+/// The ordering of a [`Entry`] is first by its `entry` where
+/// [`EntryKind::Tree`]s come before [`EntryKind::Blob`]. If both kinds
+/// are equal then they are next compared by the lexicographical ordering
+/// of their `name`s.
+#[derive(Clone, Debug)]
+pub struct Entry {
+ name: String,
+ entry: EntryKind,
+ path: PathBuf,
+ /// The commit from which this entry was constructed from.
+ commit: Commit,
+}
+
+impl Entry {
+ pub(crate) fn new(name: String, path: PathBuf, entry: EntryKind, commit: Commit) -> Self {
+ Self {
+ name,
+ entry,
+ path,
+ commit,
+ }
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ /// The full path to this entry from the root of the Git repository
+ pub fn path(&self) -> &PathBuf {
+ &self.path
+ }
+
+ pub fn entry(&self) -> &EntryKind {
+ &self.entry
+ }
+
+ pub fn is_tree(&self) -> bool {
+ matches!(self.entry, EntryKind::Tree(_))
+ }
+
+ pub fn commit(&self) -> &Commit {
+ &self.commit
+ }
+
+ pub fn object_id(&self) -> Oid {
+ match self.entry {
+ EntryKind::Blob(id) => id,
+ EntryKind::Tree(id) => id,
+ EntryKind::Submodule { id, .. } => id,
+ }
+ }
+
+ /// Returns the commit that last touched this [`Entry`].
+ pub fn last_commit(&self, repo: &Repository) -> Result<Commit, LastCommitError> {
+ repo.last_commit(&self.path, self.commit.clone())?
+ .ok_or(LastCommitError::Missing)
+ }
+}
+
+// To support `sort`.
+impl Ord for Entry {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.entry
+ .cmp(&other.entry)
+ .then(self.name.cmp(&other.name))
+ }
+}
+
+impl PartialOrd for Entry {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl PartialEq for Entry {
+ fn eq(&self, other: &Self) -> bool {
+ self.entry == other.entry && self.name == other.name
+ }
+}
+
+impl Eq for Entry {}
+
+impl From<fs::Entry> for EntryKind {
+ fn from(entry: fs::Entry) -> Self {
+ match entry {
+ fs::Entry::File(f) => EntryKind::Blob(f.id()),
+ fs::Entry::Directory(d) => EntryKind::Tree(d.id()),
+ fs::Entry::Submodule(u) => EntryKind::Submodule {
+ id: u.id(),
+ url: u.url().clone(),
+ },
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Entry {
+ /// Sample output:
+ /// ```json
+ /// {
+ /// "kind": "blob",
+ /// "commit": {
+ /// "author": {
+ /// "email": "foobar@gmail.com",
+ /// "name": "Foo Bar"
+ /// },
+ /// "committer": {
+ /// "email": "noreply@github.com",
+ /// "name": "GitHub"
+ /// },
+ /// "committerTime": 1578309972,
+ /// "description": "This is a sample file",
+ /// "sha1": "2873745c8f6ffb45c990eb23b491d4b4b6182f95",
+ /// "summary": "Add a new sample"
+ /// },
+ /// "path": "src/foo/Sample.rs",
+ /// "name": "Sample.rs",
+ /// "oid": "6d6240123a8d8ea8a8376610168a0a4bcb96afd0"
+ /// },
+ /// ```
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ const FIELDS: usize = 5;
+ let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
+ state.serialize_field("name", &self.name)?;
+ state.serialize_field(
+ "kind",
+ match self.entry {
+ EntryKind::Blob(_) => "blob",
+ EntryKind::Tree(_) => "tree",
+ EntryKind::Submodule { .. } => "submodule",
+ },
+ )?;
+ if let EntryKind::Submodule { url: Some(url), .. } = &self.entry {
+ state.serialize_field("url", url)?;
+ };
+ state.serialize_field("oid", &self.object_id())?;
+ state.serialize_field("commit", &self.path)?;
+ state.end()
+ }
+}
diff --git a/crates/radicle-surf/t/Cargo.toml b/crates/radicle-surf/t/Cargo.toml
new file mode 100644
index 000000000..7f9470947
--- /dev/null
+++ b/crates/radicle-surf/t/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "radicle-surf-test"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+
+publish = false
+
+[lib]
+test = true
+
+[features]
+test = []
+
+[dev-dependencies]
+nonempty = "0.5"
+pretty_assertions = "1.3.0"
+proptest = "1"
+serde_json = "1"
+url = "2.5"
+
+[dev-dependencies.git2]
+workspace = true
+
+[dev-dependencies.radicle-git-ext]
+path = "../../radicle-git-ext"
+
+[dev-dependencies.radicle-git-ext-test]
+path = "../../radicle-git-ext/t"
+features = ["test"]
+
+[dev-dependencies.radicle-surf]
+path = ".."
+features = ["serde"]
+
+[dev-dependencies.test-helpers]
+path = "../../test/test-helpers"
diff --git a/crates/radicle-surf/t/src/branch.rs b/crates/radicle-surf/t/src/branch.rs
new file mode 100644
index 000000000..ae4e62d4f
--- /dev/null
+++ b/crates/radicle-surf/t/src/branch.rs
@@ -0,0 +1,24 @@
+use proptest::prelude::*;
+use radicle_git_ext::ref_format::{RefStr, RefString};
+use radicle_git_ext_test::git_ref_format::gen;
+use radicle_surf::Branch;
+use test_helpers::roundtrip;
+
+proptest! {
+ #[test]
+ fn prop_test_branch(branch in gen_branch()) {
+ roundtrip::json(branch)
+ }
+}
+
+fn gen_branch() -> impl Strategy<Value = Branch> {
+ prop_oneof![
+ gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
+ (gen::valid(), gen::valid()).prop_map(|(remote, name): (String, String)| {
+ let remote =
+ RefStr::try_from_str(&remote).expect("BUG: reference strings should be valid");
+ let name = RefStr::try_from_str(&name).expect("BUG: reference strings should be valid");
+ Branch::remote(remote.head(), name)
+ })
+ ]
+}
diff --git a/crates/radicle-surf/t/src/code_browsing.rs b/crates/radicle-surf/t/src/code_browsing.rs
new file mode 100644
index 000000000..3cafcde3c
--- /dev/null
+++ b/crates/radicle-surf/t/src/code_browsing.rs
@@ -0,0 +1,100 @@
+use std::path::Path;
+
+use radicle_git_ext::ref_format::refname;
+use radicle_surf::{
+ fs::{self, Directory},
+ Branch, Repository,
+};
+
+use super::GIT_PLATINUM;
+
+#[test]
+fn iterate_root_dir_recursive() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+
+ let root_dir = repo.root_dir(Branch::local(refname!("master"))).unwrap();
+ let count = println_dir(&root_dir, &repo);
+
+ assert_eq!(count, 36); // Check total file count.
+
+ /// Prints items in `dir` with `indent_level`.
+ /// For sub-directories, will do Depth-First-Search and print
+ /// recursively.
+ /// Returns the number of items visited (i.e. printed)
+ fn println_dir(dir: &Directory, repo: &Repository) -> i32 {
+ dir.traverse::<fs::error::Directory, _, _>(
+ repo,
+ (0, 0),
+ &mut |(count, indent_level), entry| {
+ println!("> {}{}", " ".repeat(indent_level * 4), entry.name());
+ match entry {
+ fs::Entry::File(_) => Ok((count + 1, indent_level)),
+ fs::Entry::Directory(_) => Ok((count + 1, indent_level + 1)),
+ fs::Entry::Submodule(_) => Ok((count + 1, indent_level)),
+ }
+ },
+ )
+ .unwrap()
+ .0
+ }
+}
+
+#[test]
+fn browse_repo_lazily() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+
+ let root_dir = repo.root_dir(Branch::local(refname!("master"))).unwrap();
+ let count = root_dir.entries(&repo).unwrap().entries().count();
+ assert_eq!(count, 8);
+ let count = traverse(&root_dir, &repo);
+ assert_eq!(count, 36);
+
+ fn traverse(dir: &Directory, repo: &Repository) -> i32 {
+ dir.traverse::<fs::error::Directory, _, _>(repo, 0, &mut |count, _| Ok(count + 1))
+ .unwrap()
+ }
+}
+
+#[test]
+fn test_file_history() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let history = repo.history(Branch::local(refname!("dev"))).unwrap();
+ let path = Path::new("README.md");
+ let mut file_history = history.by_path(&path);
+ let commit = file_history.next().unwrap().unwrap();
+ let file = repo.get_commit_file(&commit.id, &path).unwrap();
+ assert_eq!(file.size(), 67);
+}
+
+#[test]
+fn test_commit_history() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let head = "a0dd9122d33dff2a35f564d564db127152c88e02";
+
+ // verify `&str` works.
+ let h1 = repo.history(head).unwrap();
+
+ // verify `&String` works.
+ let head_string = head.to_string();
+ let h2 = repo.history(&head_string).unwrap();
+
+ assert_eq!(h1.head().id, h2.head().id);
+}
+
+#[test]
+fn test_commit_signature() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let commit_with_signature = "e24124b7538658220b5aaf3b6ef53758f0a106dc";
+ let signature = repo.extract_signature(commit_with_signature, None).unwrap();
+ assert!(signature.is_some());
+
+ let commit_without_signature = "80bacafba303bf0cdf6142921f430ff265f25095";
+ let signature = repo
+ .extract_signature(commit_without_signature, None)
+ .unwrap();
+ assert!(signature.is_none());
+
+ let commit_nonexist = "8080808080";
+ let signature = repo.extract_signature(commit_nonexist, None);
+ assert!(signature.is_err());
+}
diff --git a/crates/radicle-surf/t/src/commit.rs b/crates/radicle-surf/t/src/commit.rs
new file mode 100644
index 000000000..b0a722264
--- /dev/null
+++ b/crates/radicle-surf/t/src/commit.rs
@@ -0,0 +1,32 @@
+use std::str::FromStr;
+
+use proptest::prelude::*;
+use radicle_git_ext::Oid;
+use radicle_surf::{Author, Commit, Time};
+use test_helpers::roundtrip;
+
+proptest! {
+ #[test]
+ fn prop_test_commits(commit in commits_strategy()) {
+ roundtrip::json(commit)
+ }
+}
+
+fn commits_strategy() -> impl Strategy<Value = Commit> {
+ ("[a-fA-F0-9]{40}", any::<String>(), any::<i64>()).prop_map(|(id, text, time)| Commit {
+ id: Oid::from_str(&id).unwrap(),
+ author: Author {
+ name: text.clone(),
+ email: text.clone(),
+ time: Time::new(time, 0),
+ },
+ committer: Author {
+ name: text.clone(),
+ email: text.clone(),
+ time: Time::new(time, 0),
+ },
+ message: text.clone(),
+ summary: text,
+ parents: vec![Oid::from_str(&id).unwrap(), Oid::from_str(&id).unwrap()],
+ })
+}
diff --git a/crates/radicle-surf/t/src/diff.rs b/crates/radicle-surf/t/src/diff.rs
new file mode 100644
index 000000000..292df46f3
--- /dev/null
+++ b/crates/radicle-surf/t/src/diff.rs
@@ -0,0 +1,671 @@
+use pretty_assertions::assert_eq;
+use radicle_git_ext::{ref_format::refname, Oid};
+use radicle_surf::{
+ diff::{
+ Added, Diff, DiffContent, DiffFile, EofNewLine, FileDiff, FileMode, FileStats, Hunk, Line,
+ Modification, Modified, Stats,
+ },
+ Branch, Error, Repository,
+};
+use std::{path::Path, str::FromStr};
+
+use super::GIT_PLATINUM;
+
+#[test]
+fn test_initial_diff() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?;
+ let commit = repo.commit(oid).unwrap();
+ assert!(commit.parents.is_empty());
+
+ let diff = repo.diff_commit(oid)?;
+ let diff_stats = *diff.stats();
+ let diff_files = diff.into_files();
+
+ let expected_files = vec![FileDiff::Added(Added {
+ path: Path::new("README.md").to_path_buf(),
+ diff: DiffContent::Plain {
+ hunks: vec![Hunk {
+ header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
+ lines: vec![Modification::addition(
+ b"This repository is a data source for the Upstream front-end tests.\n"
+ .to_vec(),
+ 1,
+ )],
+ old: 0..0,
+ new: 1..2,
+ }]
+ .into(),
+ stats: FileStats {
+ additions: 1,
+ deletions: 0,
+ },
+ eof: EofNewLine::default(),
+ },
+ new: DiffFile {
+ oid: Oid::from_str("7f48df0118b1674f4ab0ed1717c1368091a5dddc").unwrap(),
+ mode: FileMode::Blob,
+ },
+ })];
+
+ let expected_stats = Stats {
+ files_changed: 1,
+ insertions: 1,
+ deletions: 0,
+ };
+
+ assert_eq!(expected_files, diff_files);
+ assert_eq!(expected_stats, diff_stats);
+
+ Ok(())
+}
+
+#[test]
+fn test_diff_of_rev() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let diff = repo.diff_commit("80bacafba303bf0cdf6142921f430ff265f25095")?;
+ assert_eq!(diff.files().count(), 1);
+ Ok(())
+}
+
+#[test]
+fn test_diff_file() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let path_buf = Path::new("README.md").to_path_buf();
+ let diff = repo.diff_file(
+ &path_buf,
+ "d6880352fc7fda8f521ae9b7357668b17bb5bad5",
+ "223aaf87d6ea62eef0014857640fd7c8dd0f80b5",
+ )?;
+ let expected_diff = FileDiff::Modified(Modified {
+ path: path_buf,
+ diff: DiffContent::Plain {
+ hunks: vec unit tests.\n".to_vec(), 2),
+ ],
+ old: 1..2,
+ new: 1..3,
+ }]
+ .into(),
+ stats: FileStats {
+ additions: 2,
+ deletions: 1
+ },
+ eof: EofNewLine::default(),
+ },
+ old: DiffFile {
+ oid: Oid::from_str("7f48df0118b1674f4ab0ed1717c1368091a5dddc").unwrap(),
+ mode: FileMode::Blob,
+ },
+ new: DiffFile {
+ oid: Oid::from_str("5e07534cd74a6a9b2ccd2729b181c4ef26173a5e").unwrap(),
+ mode: FileMode::Blob,
+ },
+ });
+ assert_eq!(expected_diff, diff);
+
+ Ok(())
+}
+
+#[test]
+fn test_diff() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let oid = "80bacafba303bf0cdf6142921f430ff265f25095";
+ let commit = repo.commit(oid).unwrap();
+ let parent_oid = commit.parents.first().unwrap();
+ let diff = repo.diff(*parent_oid, oid)?;
+
+ let expected_files = vec unit tests.\n".to_vec(), 2),
+ ],
+ old: 1..2,
+ new: 1..3,
+ }]
+ .into(),
+ stats: FileStats {
+ additions: 2,
+ deletions: 1
+ },
+ eof: EofNewLine::default(),
+ },
+ old: DiffFile {
+ oid: Oid::from_str("7f48df0118b1674f4ab0ed1717c1368091a5dddc").unwrap(),
+ mode: FileMode::Blob,
+ },
+ new: DiffFile {
+ oid: Oid::from_str("5e07534cd74a6a9b2ccd2729b181c4ef26173a5e").unwrap(),
+ mode: FileMode::Blob,
+ },
+ })];
+ let expected_stats = Stats {
+ files_changed: 1,
+ insertions: 2,
+ deletions: 1,
+ };
+ let diff_stats = *diff.stats();
+ let diff_files = diff.into_files();
+ assert_eq!(expected_files, diff_files);
+ assert_eq!(expected_stats, diff_stats);
+
+ Ok(())
+}
+
+#[test]
+fn test_branch_diff() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let rev_from = Branch::local(refname!("master"));
+ let rev_to = Branch::local(refname!("dev"));
+ let diff = repo.diff(&rev_from, &rev_to)?;
+
+ println!("Diff two branches: master -> dev");
+ println!(
+ "result: added {} deleted {} moved {} modified {}",
+ diff.added().count(),
+ diff.deleted().count(),
+ diff.moved().count(),
+ diff.modified().count()
+ );
+ assert_eq!(diff.added().count(), 1);
+ assert_eq!(diff.deleted().count(), 11);
+ assert_eq!(diff.moved().count(), 1);
+ assert_eq!(diff.modified().count(), 2);
+ for c in diff.added() {
+ println!("added: {:?}", &c.path);
+ }
+ for d in diff.deleted() {
+ println!("deleted: {:?}", &d.path);
+ }
+ for m in diff.moved() {
+ println!("moved: {:?} -> {:?}", &m.old_path, &m.new_path);
+ }
+ for m in diff.modified() {
+ println!("modified: {:?}", &m.path);
+ }
+
+ // Verify moved.
+ let diff_moved = diff.moved().next().unwrap();
+
+ // We can find a `FileDiff` for the old_path in a move.
+ let file_diff = repo.diff_file(&diff_moved.old_path, &rev_from, &rev_to)?;
+ println!("old path file diff: {:?}", &file_diff);
+
+ // We can find a `FileDiff` for the new_path in a move.
+ let file_diff = repo.diff_file(&diff_moved.new_path, &rev_from, &rev_to)?;
+ println!("new path file diff: {:?}", &file_diff);
+
+ // We can find a `FileDiff` if given a directory name.
+ let dir_diff = repo.diff_file(&"special/", &rev_from, &rev_to)?;
+ println!("dir diff: {dir_diff:?}");
+
+ Ok(())
+}
+
+#[test]
+fn test_diff_serde() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let rev_from = Branch::local(refname!("master"));
+ let rev_to = Branch::local(refname!("diff-test"));
+ let diff = repo.diff(rev_from, rev_to)?;
+
+ let json = serde_json::json!({
+ "files": [{
+ "path": "LICENSE",
+ "diff": {
+ "type": "plain",
+ "hunks": [{
+ "header": "@@ -0,0 +1,2 @@\n",
+ "lines": [{
+ "line": "This is a license file.\n",
+ "lineNo": 1,
+ "type": "addition",
+ },
+ {
+ "line": "\n",
+ "lineNo": 2,
+ "type": "addition",
+ }],
+ "old": { "start": 0, "end": 0 },
+ "new": { "start": 1, "end": 3 },
+ }],
+ "stats": {
+ "additions": 2,
+ "deletions": 0,
+ },
+ "eof": "noneMissing",
+ },
+ "new": {
+ "mode": "blob",
+ "oid": "02f70f56ec62396ceaf38804c37e169e875ab291",
+ },
+ "status": "added"
+ },
+ {
+ "path": "README.md",
+ "diff": {
+ "type": "plain",
+ "hunks": [{
+ "header": "@@ -1,2 +1,2 @@\n",
+ "lines": [
+ { "lineNo": 1,
+ "line": "This repository is a data source for the Upstream front-end tests and the\n",
+ "type": "deletion"
+ },
+ { "lineNo": 2,
+ "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
+ "type": "deletion"
+ },
+ { "lineNo": 1,
+ "line": "This repository is a data source for the upstream front-end tests and the\n",
+ "type": "addition"
+ },
+ { "lineNo": 2,
+ "line": "[`radicle-surf`](https://github.com/radicle-dev/radicle-surf) unit tests.\n",
+ "type": "addition"
+ },
+ ],
+ "old": { "start": 1, "end": 3 },
+ "new": { "start": 1, "end": 3 },
+ }],
+ "stats": {
+ "additions": 2,
+ "deletions": 2
+ },
+ "eof": "noneMissing",
+ },
+ "new": {
+ "mode": "blob",
+ "oid": "b033ecf407a44922b28c942c696922a7d7daf06e",
+ },
+ "old": {
+ "mode": "blob",
+ "oid": "5e07534cd74a6a9b2ccd2729b181c4ef26173a5e",
+ },
+ "status": "modified",
+ },
+ {
+ "current": {
+ "mode": "blob",
+ "oid": "1570277532948712fea9029d100a4208f9e34241",
+ },
+ "oldPath": "text/emoji.txt",
+ "newPath": "emoji.txt",
+ "status": "moved"
+ },
+ {
+ "current": {
+ "mode": "blob",
+ "oid": "5e07534cd74a6a9b2ccd2729b181c4ef26173a5e",
+ },
+ "newPath": "file_operations/copied.md",
+ "oldPath": "README.md",
+ "status": "copied"
+ },
+ {
+ "path": "text/arrows.txt",
+ "status": "deleted",
+ "old": {
+ "mode": "blob",
+ "oid": "95418c04010a3cc758fb3a37f9918465f147566f",
+ },
+ "diff": {
+ "type": "plain",
+ "hunks": [{
+ "header": "@@ -1,7 +0,0 @@\n",
+ "lines": [
+ {
+ "line": " ;;;;; ;;;;; ;;;;;\n",
+ "lineNo": 1,
+ "type": "deletion",
+ },
+ {
+ "line": " ;;;;; ;;;;; ;;;;;\n",
+ "lineNo": 2,
+ "type": "deletion",
+ },
+ {
+ "line": " ;;;;; ;;;;; ;;;;;\n",
+ "lineNo": 3,
+ "type": "deletion",
+ },
+ {
+ "line": " ;;;;; ;;;;; ;;;;;\n",
+ "lineNo": 4,
+ "type": "deletion",
+ },
+ {
+ "line": "..;;;;;.. ..;;;;;.. ..;;;;;..\n",
+ "lineNo": 5,
+ "type": "deletion",
+ },
+ {
+ "line": " ':::::' ':::::' ':::::'\n",
+ "lineNo": 6,
+ "type": "deletion",
+ },
+ {
+ "line": " ':` ':` ':`\n",
+ "lineNo": 7,
+ "type": "deletion",
+ },
+ ],
+ "old": { "start": 1, "end": 8 },
+ "new": { "start": 0, "end": 0 },
+ }],
+ "stats": {
+ "additions": 0,
+ "deletions": 7,
+ },
+ "eof": "noneMissing",
+ },
+ }],
+ "stats": {
+ "deletions": 9,
+ "filesChanged": 5,
+ "insertions": 4,
+ }
+ });
+ assert_eq!(serde_json::to_value(diff).unwrap(), json);
+
+ Ok(())
+}
+
+#[test]
+fn test_rename_with_changes() {
+ let buf = r"
+diff --git a/radicle/src/node/tracking/config.rs b/radicle-node/src/service/tracking.rs
+similarity index 96%
+rename from radicle/src/node/tracking/config.rs
+rename to radicle-node/src/service/tracking.rs
+index 3f69208f3..cbc843c82 100644
+--- a/radicle/src/node/tracking/config.rs
++++ b/radicle-node/src/service/tracking.rs
+@@ -5,15 +5,17 @@ use std::ops;
+ use log::error;
+ use thiserror::Error;
+
+-use crate::crypto::PublicKey;
+-use crate::identity::IdentityError;
++use radicle::crypto::PublicKey;
++use radicle::identity::IdentityError;
++use radicle::storage::{Namespaces, ReadRepository as _, ReadStorage};
++
++use crate::prelude::Id;
++use crate::service::NodeId;
++
+ pub use crate::node::tracking::store;
+ pub use crate::node::tracking::store::Config as Store;
+ pub use crate::node::tracking::store::Error;
+ pub use crate::node::tracking::{Alias, Node, Policy, Repo, Scope};
+-use crate::node::NodeId;
+-use crate::prelude::Id;
+-use crate::storage::{Namespaces, ReadRepository as _, ReadStorage};
+
+ #[derive(Debug, Error)]
+ pub enum NamespacesError {
+";
+ let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+ let diff = Diff::try_from(diff).unwrap();
+ let json = serde_json::json!(
+ {
+ "files": [
+ {
+ "diff": {
+ "eof": "noneMissing",
+ "hunks": [
+ {
+ "header": "@@ -5,15 +5,17 @@ use std::ops;\n",
+ "lines": [
+ {
+ "line": "use log::error;\n",
+ "lineNoNew": 5,
+ "lineNoOld": 5,
+ "type": "context",
+ },
+ {
+ "line": "use thiserror::Error;\n",
+ "lineNoNew": 6,
+ "lineNoOld": 6,
+ "type": "context",
+ },
+ {
+ "line": "\n",
+ "lineNoNew": 7,
+ "lineNoOld": 7,
+ "type": "context",
+ },
+ {
+ "line": "use crate::crypto::PublicKey;\n",
+ "lineNo": 8,
+ "type": "deletion",
+ },
+ {
+ "line": "use crate::identity::IdentityError;\n",
+ "lineNo": 9,
+ "type": "deletion",
+ },
+ {
+ "line": "use radicle::crypto::PublicKey;\n",
+ "lineNo": 8,
+ "type": "addition",
+ },
+ {
+ "line": "use radicle::identity::IdentityError;\n",
+ "lineNo": 9,
+ "type": "addition",
+ },
+ {
+ "line": "use radicle::storage::{Namespaces, ReadRepository as _, ReadStorage};\n",
+ "lineNo": 10,
+ "type": "addition",
+ },
+ {
+ "line": "\n",
+ "lineNo": 11,
+ "type": "addition",
+ },
+ {
+ "line": "use crate::prelude::Id;\n",
+ "lineNo": 12,
+ "type": "addition",
+ },
+ {
+ "line": "use crate::service::NodeId;\n",
+ "lineNo": 13,
+ "type": "addition",
+ },
+ {
+ "line": "\n",
+ "lineNo": 14,
+ "type": "addition",
+ },
+ {
+ "line": "pub use crate::node::tracking::store;\n",
+ "lineNoNew": 15,
+ "lineNoOld": 10,
+ "type": "context",
+ },
+ {
+ "line": "pub use crate::node::tracking::store::Config as Store;\n",
+ "lineNoNew": 16,
+ "lineNoOld": 11,
+ "type": "context",
+ },
+ {
+ "line": "pub use crate::node::tracking::store::Error;\n",
+ "lineNoNew": 17,
+ "lineNoOld": 12,
+ "type": "context",
+ },
+ {
+ "line": "pub use crate::node::tracking::{Alias, Node, Policy, Repo, Scope};\n",
+ "lineNoNew": 18,
+ "lineNoOld": 13,
+ "type": "context",
+ },
+ {
+ "line": "use crate::node::NodeId;\n",
+ "lineNo": 14,
+ "type": "deletion",
+ },
+ {
+ "line": "use crate::prelude::Id;\n",
+ "lineNo": 15,
+ "type": "deletion",
+ },
+ {
+ "line": "use crate::storage::{Namespaces, ReadRepository as _, ReadStorage};\n",
+ "lineNo": 16,
+ "type": "deletion",
+ },
+ {
+ "line": "\n",
+ "lineNoNew": 19,
+ "lineNoOld": 17,
+ "type": "context",
+ },
+ {
+ "line": "#[derive(Debug, Error)]\n",
+ "lineNoNew": 20,
+ "lineNoOld": 18,
+ "type": "context",
+ },
+ {
+ "line": "pub enum NamespacesError {\n",
+ "lineNoNew": 21,
+ "lineNoOld": 19,
+ "type": "context",
+ },
+ ],
+ "new": {
+ "end": 22,
+ "start": 5,
+ },
+ "old": {
+ "end": 20,
+ "start": 5,
+ },
+ },
+ ],
+ "stats": {
+ "additions": 7,
+ "deletions": 5
+ },
+ "type": "plain",
+ },
+ "new": {
+ "mode": "blob",
+ "oid": "cbc843c820000000000000000000000000000000",
+ },
+ "newPath": "radicle-node/src/service/tracking.rs",
+ "old": {
+ "mode": "blob",
+ "oid": "3f69208f30000000000000000000000000000000",
+ },
+ "oldPath": "radicle/src/node/tracking/config.rs",
+ "status": "moved"
+ },
+ ],
+ "stats": {
+ "deletions": 0,
+ "filesChanged": 1,
+ "insertions": 0,
+ },
+ });
+
+ assert_eq!(serde_json::to_value(diff).unwrap(), json);
+}
+
+// A possible false positive is being hit here for this clippy
+// warning. Tracking issue:
+// https://github.com/rust-lang/rust-clippy/issues/11402
+#[allow(clippy::needless_raw_string_hashes)]
+#[test]
+fn test_both_missing_eof_newline() {
+ let buf = r#"
+diff --git a/.env b/.env
+index f89e4c0..7c56eb7 100644
+--- a/.env
++++ b/.env
+@@ -1 +1 @@
+-hello=123
+\ No newline at end of file
++hello=1234
+\ No newline at end of file
+"#;
+ let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+ let diff = Diff::try_from(diff).unwrap();
+ assert_eq!(
+ diff.modified().next().unwrap().diff.eof(),
+ Some(EofNewLine::BothMissing)
+ );
+}
+
+#[test]
+fn test_none_missing_eof_newline() {
+ let buf = r#"
+diff --git a/.env b/.env
+index f89e4c0..7c56eb7 100644
+--- a/.env
++++ b/.env
+@@ -1 +1 @@
+-hello=123
++hello=1234
+"#;
+ let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+ let diff = Diff::try_from(diff).unwrap();
+ assert_eq!(
+ diff.modified().next().unwrap().diff.eof(),
+ Some(EofNewLine::NoneMissing)
+ );
+}
+
+#[test]
+fn test_old_missing_eof_newline() {
+ let buf = r#"
+diff --git a/.env b/.env
+index f89e4c0..7c56eb7 100644
+--- a/.env
++++ b/.env
+@@ -1 +1 @@
+-hello=123
+\ No newline at end of file
++hello=1234
+"#;
+ let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+ let diff = Diff::try_from(diff).unwrap();
+ assert_eq!(
+ diff.modified().next().unwrap().diff.eof(),
+ Some(EofNewLine::OldMissing)
+ );
+}
+
+#[test]
+fn test_new_missing_eof_newline() {
+ let buf = r#"
+diff --git a/.env b/.env
+index f89e4c0..7c56eb7 100644
+--- a/.env
++++ b/.env
+@@ -1 +1 @@
+-hello=123
++hello=1234
+\ No newline at end of file
+"#;
+ let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+ let diff = Diff::try_from(diff).unwrap();
+ assert_eq!(
+ diff.modified().next().unwrap().diff.eof(),
+ Some(EofNewLine::NewMissing)
+ );
+}
diff --git a/crates/radicle-surf/t/src/file_system.rs b/crates/radicle-surf/t/src/file_system.rs
new file mode 100644
index 000000000..bf6150599
--- /dev/null
+++ b/crates/radicle-surf/t/src/file_system.rs
@@ -0,0 +1,202 @@
+//! Unit tests for radicle_surf::file_system
+
+mod directory {
+ use radicle_git_ext::ref_format::refname;
+ use radicle_surf::{
+ fs::{self, Entry},
+ Branch, Oid, Repository,
+ };
+ use std::path::Path;
+
+ const GIT_PLATINUM: &str = "../data/git-platinum";
+
+ #[test]
+ fn directory_find_entry() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let root = repo.root_dir(Branch::local(refname!("master"))).unwrap();
+
+ // find_entry for a file.
+ let path = Path::new("src/memory.rs");
+ let entry = root.find_entry(&path, &repo).unwrap();
+ assert!(matches!(entry, fs::Entry::File(_)));
+
+ // find_entry for a directory.
+ let path = Path::new("this/is/a/really/deeply/nested/directory/tree");
+ let entry = root.find_entry(&path, &repo).unwrap();
+ assert!(matches!(entry, fs::Entry::Directory(_)));
+
+ // find_entry for a non-leaf directory and its relative path.
+ let path = Path::new("text");
+ let entry = root.find_entry(&path, &repo).unwrap();
+ assert!(matches!(entry, fs::Entry::Directory(_)));
+ if let fs::Entry::Directory(sub_dir) = entry {
+ let inner_path = Path::new("garden.txt");
+ let inner_entry = sub_dir.find_entry(&inner_path, &repo).unwrap();
+ assert!(matches!(inner_entry, fs::Entry::File(_)));
+ }
+
+ // find_entry for non-existing file
+ let path = Path::new("this/is/a/really/missing_file");
+ let result = root.find_entry(&path, &repo);
+ assert!(matches!(result, Err(fs::error::Directory::PathNotFound(_))));
+
+ // find_entry for absolute path: fail.
+ let path = Path::new("/src/memory.rs");
+ let result = root.find_entry(&path, &repo);
+ assert!(result.is_err());
+
+ // find entry for an empty path
+ let path = Path::new("");
+ let result = root.find_entry(&path, &repo);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn directory_find_file_and_directory() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ // Get the snapshot of the directory for a given commit.
+ let root = repo
+ .root_dir("80ded66281a4de2889cc07293a8f10947c6d57fe")
+ .unwrap();
+
+ // Assert that we can find the memory.rs file!
+ assert!(root.find_file(&Path::new("src/memory.rs"), &repo).is_ok());
+
+ let root_contents: Vec<Entry> = root.entries(&repo).unwrap().collect();
+ assert_eq!(root_contents.len(), 7);
+ assert!(root_contents[0].is_file());
+ assert!(root_contents[1].is_file());
+ assert!(root_contents[2].is_file());
+ assert_eq!(root_contents[0].name(), ".i-am-well-hidden");
+ assert_eq!(root_contents[1].name(), ".i-too-am-hidden");
+ assert_eq!(root_contents[2].name(), "README.md");
+
+ assert!(root_contents[3].is_directory());
+ assert!(root_contents[4].is_directory());
+ assert!(root_contents[5].is_directory());
+ assert!(root_contents[6].is_directory());
+ assert_eq!(root_contents[3].name(), "bin");
+ assert_eq!(root_contents[4].name(), "src");
+ assert_eq!(root_contents[5].name(), "text");
+ assert_eq!(root_contents[6].name(), "this");
+
+ let src = root.find_directory(&Path::new("src"), &repo).unwrap();
+ assert_eq!(src.path(), Path::new("src").to_path_buf());
+ let src_contents: Vec<Entry> = src.entries(&repo).unwrap().collect();
+ assert_eq!(src_contents.len(), 3);
+ assert_eq!(src_contents[0].name(), "Eval.hs");
+ assert_eq!(src_contents[1].name(), "Folder.svelte");
+ assert_eq!(src_contents[2].name(), "memory.rs");
+ }
+
+ #[test]
+ fn directory_size() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let root = repo.root_dir(Branch::local(refname!("master"))).unwrap();
+
+ /*
+ git-platinum (master) $ ls -l src
+ -rw-r--r-- 1 pi pi 10044 Oct 31 11:32 Eval.hs
+ -rw-r--r-- 1 pi pi 6253 Oct 31 11:27 memory.rs
+
+ 10044 + 6253 = 16297
+ */
+
+ let path = Path::new("src");
+ let entry = root.find_entry(&path, &repo).unwrap();
+ assert!(matches!(entry, fs::Entry::Directory(_)));
+ if let fs::Entry::Directory(d) = entry {
+ assert_eq!(16297, d.size(&repo).unwrap());
+ }
+ }
+
+ #[test]
+ fn directory_last_commit() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let branch = Branch::local(refname!("dev"));
+ let root = repo.root_dir(&branch).unwrap();
+ let dir = root.find_directory(&"this/is", &repo).unwrap();
+ let last_commit = repo.last_commit(&dir.path(), &branch).unwrap().unwrap();
+ assert_eq!(
+ last_commit.id.to_string(),
+ "2429f097664f9af0c5b7b389ab998b2199ffa977"
+ );
+ }
+
+ #[test]
+ fn file_last_commit() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let branch = Branch::local(refname!("master"));
+ let root = repo.root_dir(&branch).unwrap();
+
+ // Find a file with "\" in its name.
+ let f = root.find_file(&"special/faux\\path", &repo).unwrap();
+ let last_commit = repo.last_commit(&f.path(), &branch).unwrap().unwrap();
+ assert_eq!(
+ last_commit.id.to_string(),
+ "a0dd9122d33dff2a35f564d564db127152c88e02"
+ );
+ }
+
+ /// Test that directories and files with glob metacharacters in their names
+ /// can be browsed and have their history retrieved correctly.
+ ///
+ /// This is a regression test for a bug where paths containing `[` were
+ /// interpreted as glob patterns by git's pathspec, causing errors.
+ #[test]
+ fn directory_with_bracket_in_name() {
+ let repo = test_helpers::tempdir::WithTmpDir::new(|path| {
+ git2::Repository::init(path).map_err(std::io::Error::other)
+ })
+ .unwrap();
+
+ // Initialize the repo and create test structure:
+ // src/
+ // [special-dir]/
+ // file.txt
+ // normal-file.txt
+ let commit = {
+ let mut tb = repo.treebuilder(None).unwrap();
+ let hello = repo.blob(b"hello world").unwrap();
+ tb.insert("file.txt", hello, git2::FileMode::Blob.into())
+ .unwrap();
+ let inner = tb.write().unwrap();
+ let mut tb = repo.treebuilder(None).unwrap();
+ let normal = repo.blob(b"normal content").unwrap();
+ tb.insert("normal-file.txt", normal, git2::FileMode::Blob.into())
+ .unwrap();
+ tb.insert("[special-dir]", inner, git2::FileMode::Tree.into())
+ .unwrap();
+ let id = tb.write().unwrap();
+ let mut tb = repo.treebuilder(None).unwrap();
+ tb.insert("src", id, git2::FileMode::Tree.into()).unwrap();
+ let tree = tb.write().unwrap();
+ let tree = repo.find_tree(tree).unwrap();
+ let sig = git2::Signature::now("Test", "test@test.com").unwrap();
+ Oid::from(
+ repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
+ .unwrap(),
+ )
+ };
+
+ let repo = Repository::open(repo.path()).unwrap();
+ let branch = Branch::local(refname!("master"));
+ let root = repo.root_dir(&branch).unwrap();
+
+ let src = root.find_directory(&"src", &repo).unwrap();
+ let src_entries: Vec<Entry> = src.entries(&repo).unwrap().collect();
+ assert_eq!(src_entries.len(), 2);
+
+ let special_dir = src.find_directory(&"[special-dir]", &repo).unwrap();
+ assert_eq!(special_dir.name(), "[special-dir]");
+
+ let dir_path = special_dir.path();
+ let dir_last_commit = repo.last_commit(&dir_path, &branch).unwrap();
+ assert_eq!(dir_last_commit.map(|c| c.id), Some(commit));
+
+ let file = special_dir.find_file(&"file.txt", &repo).unwrap();
+ let file_path = file.path();
+ let file_last_commit = repo.last_commit(&file_path, &branch).unwrap();
+ assert_eq!(file_last_commit.map(|c| c.id), Some(commit));
+ }
+}
diff --git a/crates/radicle-surf/t/src/last_commit.rs b/crates/radicle-surf/t/src/last_commit.rs
new file mode 100644
index 000000000..009893ee9
--- /dev/null
+++ b/crates/radicle-surf/t/src/last_commit.rs
@@ -0,0 +1,119 @@
+use std::{path::PathBuf, str::FromStr};
+
+use radicle_git_ext::{ref_format::refname, Oid};
+use radicle_surf::{Branch, Repository};
+
+use super::GIT_PLATINUM;
+
+#[test]
+fn readme_missing_and_memory() {
+ let repo = Repository::open(GIT_PLATINUM)
+ .expect("Could not retrieve ./data/git-platinum as git repository");
+ let oid =
+ Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
+
+ // memory.rs is commited later so it should not exist here.
+ let memory_last_commit_oid = repo
+ .last_commit(&"src/memory.rs", oid)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+
+ assert_eq!(memory_last_commit_oid, None);
+
+ // README.md exists in this commit.
+ let readme_last_commit = repo
+ .last_commit(&"README.md", oid)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+
+ assert_eq!(readme_last_commit, Some(oid));
+}
+
+#[test]
+fn folder_svelte() {
+ let repo = Repository::open(GIT_PLATINUM)
+ .expect("Could not retrieve ./data/git-platinum as git repository");
+ // Check that last commit is the actual last commit even if head commit differs.
+ let oid =
+ Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
+
+ let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();
+
+ let folder_svelte = repo
+ .last_commit(&"examples/Folder.svelte", oid)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+
+ assert_eq!(folder_svelte, Some(expected_commit_id));
+}
+
+#[test]
+fn nest_directory() {
+ let repo = Repository::open(GIT_PLATINUM)
+ .expect("Could not retrieve ./data/git-platinum as git repository");
+ // Check that last commit is the actual last commit even if head commit differs.
+ let oid =
+ Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
+
+ let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap();
+
+ let nested_directory_tree_commit_id = repo
+ .last_commit(&"this/is/a/really/deeply/nested/directory/tree", oid)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+
+ assert_eq!(nested_directory_tree_commit_id, Some(expected_commit_id));
+}
+
+#[test]
+#[cfg(not(windows))]
+fn can_get_last_commit_for_special_filenames() {
+ let repo = Repository::open(GIT_PLATINUM)
+ .expect("Could not retrieve ./data/git-platinum as git repository");
+
+ // Check that last commit is the actual last commit even if head commit differs.
+ let oid =
+ Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
+
+ let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();
+
+ let backslash_commit_id = repo
+ .last_commit(&"special/faux\\path", oid)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+ assert_eq!(backslash_commit_id, Some(expected_commit_id));
+
+ let ogre_commit_id = repo
+ .last_commit(&"special/👹👹👹", oid)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+ assert_eq!(ogre_commit_id, Some(expected_commit_id));
+}
+
+#[test]
+fn root() {
+ let repo = Repository::open(GIT_PLATINUM)
+ .expect("Could not retrieve ./data/git-platinum as git repository");
+ let rev = Branch::local(refname!("master"));
+ let root_last_commit_id = repo
+ .last_commit(&PathBuf::new(), rev)
+ .expect("Failed to get last commit")
+ .map(|commit| commit.id);
+
+ let expected_oid = repo
+ .history(Branch::local(refname!("master")))
+ .unwrap()
+ .head()
+ .id;
+ assert_eq!(root_last_commit_id, Some(expected_oid));
+}
+
+#[test]
+fn binary_file() {
+ let repo = Repository::open(GIT_PLATINUM)
+ .expect("Could not retrieve ./data/git-platinum as git repository");
+ let history = repo.history(Branch::local(refname!("dev"))).unwrap();
+ let file_commit = history.by_path(&"bin/cat").next();
+ assert!(file_commit.is_some());
+ println!("file commit: {:?}", &file_commit);
+}
diff --git a/crates/radicle-surf/t/src/lib.rs b/crates/radicle-surf/t/src/lib.rs
new file mode 100644
index 000000000..812646e6c
--- /dev/null
+++ b/crates/radicle-surf/t/src/lib.rs
@@ -0,0 +1,38 @@
+#[cfg(test)]
+const GIT_PLATINUM: &str = "../data/git-platinum";
+
+#[cfg(test)]
+mod file_system;
+
+#[cfg(test)]
+mod source;
+
+#[cfg(test)]
+mod branch;
+
+#[cfg(test)]
+mod code_browsing;
+
+#[cfg(test)]
+mod commit;
+
+#[cfg(test)]
+mod diff;
+
+#[cfg(test)]
+mod last_commit;
+
+#[cfg(test)]
+mod namespace;
+
+#[cfg(test)]
+mod reference;
+
+#[cfg(test)]
+mod rev;
+
+#[cfg(test)]
+mod submodule;
+
+#[cfg(test)]
+mod threading;
diff --git a/crates/radicle-surf/t/src/namespace.rs b/crates/radicle-surf/t/src/namespace.rs
new file mode 100644
index 000000000..0fe7b74c1
--- /dev/null
+++ b/crates/radicle-surf/t/src/namespace.rs
@@ -0,0 +1,121 @@
+use pretty_assertions::{assert_eq, assert_ne};
+use radicle_git_ext::ref_format::{name::component, refname, refspec};
+use radicle_surf::{Branch, Error, Glob, Repository};
+
+use super::GIT_PLATINUM;
+
+#[test]
+fn switch_to_banana() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let history_master = repo.history(Branch::local(refname!("master")))?;
+ repo.switch_namespace(&refname!("golden"))?;
+ let history_banana = repo.history(Branch::local(refname!("banana")))?;
+
+ assert_ne!(history_master.head(), history_banana.head());
+
+ Ok(())
+}
+
+#[test]
+fn me_namespace() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let history = repo.history(Branch::local(refname!("master")))?;
+
+ assert_eq!(repo.which_namespace().unwrap(), None);
+
+ repo.switch_namespace(&refname!("me"))?;
+ assert_eq!(repo.which_namespace().unwrap(), Some("me".parse()?));
+
+ let history_feature = repo.history(Branch::local(refname!("feature/#1194")))?;
+ assert_eq!(history.head(), history_feature.head());
+
+ let expected_branches: Vec<Branch> = vec![Branch::local(refname!("feature/#1194"))];
+ let mut branches = repo
+ .branches(Glob::all_heads())?
+ .collect::<Result<Vec<_>, _>>()?;
+ branches.sort();
+
+ assert_eq!(expected_branches, branches);
+
+ let expected_branches: Vec<Branch> = vec![Branch::remote(
+ component!("fein"),
+ refname!("heads/feature/#1194"),
+ )];
+ let mut branches = repo
+ .branches(Glob::remotes(refspec::pattern!("fein/*")))?
+ .collect::<Result<Vec<_>, _>>()?;
+ branches.sort();
+
+ assert_eq!(expected_branches, branches);
+
+ Ok(())
+}
+
+#[test]
+fn golden_namespace() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let history = repo.history(Branch::local(refname!("master")))?;
+
+ assert_eq!(repo.which_namespace().unwrap(), None);
+
+ repo.switch_namespace(&refname!("golden"))?;
+
+ assert_eq!(repo.which_namespace().unwrap(), Some("golden".parse()?));
+
+ let golden_history = repo.history(Branch::local(refname!("master")))?;
+ assert_eq!(history.head(), golden_history.head());
+
+ let expected_branches: Vec<Branch> = vec![
+ Branch::local(refname!("banana")),
+ Branch::local(refname!("master")),
+ ];
+ let mut branches = repo
+ .branches(Glob::all_heads())?
+ .collect::<Result<Vec<_>, _>>()?;
+ branches.sort();
+
+ assert_eq!(expected_branches, branches);
+
+ // NOTE: these tests used to remove the categories, i.e. heads & tags, but that
+ // was specialised logic based on the radicle-link storage layout.
+ let remote = component!("kickflip");
+ let expected_branches: Vec<Branch> = vec![
+ Branch::remote(remote.clone(), refname!("heads/fakie/bigspin")),
+ Branch::remote(remote.clone(), refname!("heads/heelflip")),
+ Branch::remote(remote, refname!("tags/v0.1.0")),
+ ];
+ let mut branches = repo
+ .branches(Glob::remotes(refspec::pattern!("kickflip/*")))?
+ .collect::<Result<Vec<_>, _>>()?;
+ branches.sort();
+
+ assert_eq!(expected_branches, branches);
+
+ Ok(())
+}
+
+#[test]
+fn silver_namespace() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let history = repo.history(Branch::local(refname!("master")))?;
+
+ assert_eq!(repo.which_namespace().unwrap(), None);
+
+ repo.switch_namespace(&refname!("golden/silver"))?;
+ assert_eq!(
+ repo.which_namespace().unwrap(),
+ Some("golden/silver".parse()?)
+ );
+ let silver_history = repo.history(Branch::local(refname!("master")))?;
+ assert_ne!(history.head(), silver_history.head());
+
+ let expected_branches: Vec<Branch> = vec![Branch::local(refname!("master"))];
+ let mut branches = repo
+ .branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
+ .collect::<Result<Vec<_>, _>>()?;
+ branches.sort();
+
+ assert_eq!(expected_branches, branches);
+
+ Ok(())
+}
diff --git a/crates/radicle-surf/t/src/reference.rs b/crates/radicle-surf/t/src/reference.rs
new file mode 100644
index 000000000..6625feb97
--- /dev/null
+++ b/crates/radicle-surf/t/src/reference.rs
@@ -0,0 +1,55 @@
+use radicle_git_ext::ref_format::refspec;
+use radicle_surf::{Glob, Repository};
+
+use super::GIT_PLATINUM;
+
+#[test]
+fn test_branches() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let heads = Glob::all_heads();
+ let branches = repo.branches(heads.clone()).unwrap();
+ for b in branches {
+ println!("{}", b.unwrap().refname());
+ }
+ let branches = repo
+ .branches(
+ heads
+ .branches()
+ .and(Glob::remotes(refspec::pattern!("banana/*"))),
+ )
+ .unwrap();
+ for b in branches {
+ println!("{}", b.unwrap().refname());
+ }
+}
+
+#[test]
+fn test_tag_snapshot() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let tags = repo
+ .tags(&Glob::all_tags())
+ .unwrap()
+ .collect::<Result<Vec<_>, _>>()
+ .unwrap();
+ assert_eq!(tags.len(), 6);
+ let root_dir = repo.root_dir(&tags[0]).unwrap();
+ assert_eq!(root_dir.entries(&repo).unwrap().entries().count(), 1);
+}
+
+#[test]
+fn test_namespaces() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+
+ let namespaces = repo.namespaces(&Glob::all_namespaces()).unwrap();
+ assert_eq!(namespaces.count(), 3);
+ let namespaces = repo
+ .namespaces(&Glob::namespaces(refspec::pattern!("golden/*")))
+ .unwrap();
+ assert_eq!(namespaces.count(), 2);
+ let namespaces = repo
+ .namespaces(
+ &Glob::namespaces(refspec::pattern!("golden/*")).insert(refspec::pattern!("me/*")),
+ )
+ .unwrap();
+ assert_eq!(namespaces.count(), 3);
+}
diff --git a/crates/radicle-surf/t/src/rev.rs b/crates/radicle-surf/t/src/rev.rs
new file mode 100644
index 000000000..60294a0f0
--- /dev/null
+++ b/crates/radicle-surf/t/src/rev.rs
@@ -0,0 +1,92 @@
+use std::str::FromStr;
+
+use radicle_git_ext::ref_format::{name::component, refname};
+use radicle_surf::{Branch, Error, Oid, Repository};
+
+use super::GIT_PLATINUM;
+
+// **FIXME**: This seems to break occasionally on
+// buildkite. For some reason the commit
+// 3873745c8f6ffb45c990eb23b491d4b4b6182f95, which is on master
+// (currently HEAD), is not found. It seems to load the history
+// with d6880352fc7fda8f521ae9b7357668b17bb5bad5 as the HEAD.
+//
+// To temporarily fix this, we need to select "New Build" from the build kite
+// build page that's failing.
+// * Under "Message" put whatever you want.
+// * Under "Branch" put in the branch you're working on.
+// * Expand "Options" and select "clean checkout".
+#[test]
+fn _master() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let mut history = repo.history(Branch::remote(component!("origin"), refname!("master")))?;
+
+ let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+ assert!(
+ history.any(|commit| commit.unwrap().id == commit1),
+ "commit_id={}, history =\n{:#?}",
+ commit1,
+ &history
+ );
+
+ let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?;
+ assert!(
+ history.any(|commit| commit.unwrap().id == commit2),
+ "commit_id={}, history =\n{:#?}",
+ commit2,
+ &history
+ );
+
+ Ok(())
+}
+
+#[test]
+fn commit() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+ let mut history = repo.history(rev)?;
+
+ let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+ assert!(history.any(|commit| commit.unwrap().id == commit1));
+
+ Ok(())
+}
+
+#[test]
+fn commit_parents() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+ let history = repo.history(rev)?;
+ let commit = history.head();
+
+ assert_eq!(
+ commit.parents,
+ vec![Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?]
+ );
+
+ Ok(())
+}
+
+#[test]
+fn commit_short() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let rev = repo.oid("3873745c8")?;
+ let mut history = repo.history(rev)?;
+
+ let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
+ assert!(history.any(|commit| commit.unwrap().id == commit1));
+
+ Ok(())
+}
+
+#[test]
+fn tag() -> Result<(), Error> {
+ let repo = Repository::open(GIT_PLATINUM)?;
+ let rev = refname!("refs/tags/v0.2.0");
+ let history = repo.history(&rev)?;
+
+ let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
+ assert_eq!(history.head().id, commit1);
+
+ Ok(())
+}
diff --git a/crates/radicle-surf/t/src/source.rs b/crates/radicle-surf/t/src/source.rs
new file mode 100644
index 000000000..4b97714ab
--- /dev/null
+++ b/crates/radicle-surf/t/src/source.rs
@@ -0,0 +1,222 @@
+use std::path::PathBuf;
+
+use radicle_git_ext::ref_format::refname;
+use radicle_surf::{Branch, Glob, Repository};
+use serde_json::json;
+
+const GIT_PLATINUM: &str = "../data/git-platinum";
+
+#[test]
+fn tree_serialization() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let tree = repo.tree(refname!("refs/heads/master"), &"src").unwrap();
+
+ let expected = json!({
+ "oid": "ed52e9f8dfe1d8b374b2a118c25235349a743dd2",
+ "entries": [
+ {
+ "name": "Eval.hs",
+ "kind": "blob",
+ "oid": "7d6240123a8d8ea8a8376610168a0a4bcb96afd0",
+ "commit": "src/Eval.hs"
+ },
+ {
+ "name": "memory.rs",
+ "kind": "blob",
+ "oid": "b84992d24be67536837f5ab45a943f1b3f501878",
+ "commit": "src/memory.rs"
+ }
+ ],
+ "commit": {
+ "id": "a0dd9122d33dff2a35f564d564db127152c88e02",
+ "author": {
+ "name": "Rūdolfs Ošiņš",
+ "email": "rudolfs@osins.org",
+ "time": 1602778504
+ },
+ "committer": {
+ "name": "GitHub",
+ "email": "noreply@github.com",
+ "time": 1602778504
+ },
+ "summary": "Add files with special characters in their filenames (#5)",
+ "message": "Add files with special characters in their filenames (#5)\n\n",
+ "description": "",
+ "parents": [
+ "223aaf87d6ea62eef0014857640fd7c8dd0f80b5"
+ ]
+ },
+ "root": "src"
+ });
+
+ assert_eq!(
+ serde_json::to_value(&tree).unwrap(),
+ expected,
+ "Got:\n{}",
+ serde_json::to_string_pretty(&tree).unwrap()
+ )
+}
+
+#[test]
+fn test_tree_last_commit() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let tree = repo.tree(refname!("refs/heads/master"), &"src").unwrap();
+ let last_commit = tree.last_commit(&repo).unwrap();
+ assert_ne!(*tree.commit(), last_commit);
+ assert_eq!(
+ last_commit.id.to_string(),
+ "a57846bbc8ced6587bf8329fc4bce970eb7b757e"
+ )
+}
+
+#[test]
+fn repo_tree_empty_branch() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let rev = Branch::local(refname!("empty-branch"));
+ let tree = repo.tree(rev, &"").unwrap();
+ assert_eq!(tree.entries().len(), 0);
+
+ // Verify the last commit is the empty commit.
+ assert_eq!(
+ tree.commit().id.to_string(),
+ "e972683fe8136bf8a5cb2378cf50303554008049"
+ );
+}
+
+#[test]
+fn repo_tree() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let tree = repo
+ .tree("27acd68c7504755aa11023300890bb85bbd69d45", &"src")
+ .unwrap();
+ assert_eq!(tree.entries().len(), 3);
+
+ let commit_header = tree.commit();
+ assert_eq!(
+ commit_header.id.to_string(),
+ "27acd68c7504755aa11023300890bb85bbd69d45"
+ );
+
+ let tree_oid = tree.object_id();
+ assert_eq!(
+ tree_oid.to_string(),
+ "dbd5d80c64a00969f521b96401a315e9481e9561"
+ );
+
+ let entries = tree.entries();
+ assert_eq!(entries.len(), 3);
+ let entry = &entries[0];
+ assert!(!entry.is_tree());
+ assert_eq!(entry.name(), "Eval.hs");
+ assert_eq!(
+ entry.object_id().to_string(),
+ "8c7447d13b907aa994ac3a38317c1e9633bf0732"
+ );
+ let commit = entry.commit();
+ assert_eq!(
+ commit.id.to_string(),
+ "27acd68c7504755aa11023300890bb85bbd69d45"
+ );
+ let last_commit = entry.last_commit(&repo).unwrap();
+ assert_eq!(
+ last_commit.id.to_string(),
+ "e24124b7538658220b5aaf3b6ef53758f0a106dc"
+ );
+
+ // Verify that an empty path works for getting the root tree.
+ let root_tree = repo
+ .tree("27acd68c7504755aa11023300890bb85bbd69d45", &"")
+ .unwrap();
+ assert_eq!(root_tree.entries().len(), 8);
+}
+
+#[test]
+fn repo_blob() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let blob = repo
+ .blob("27acd68c7504755aa11023300890bb85bbd69d45", &"src/memory.rs")
+ .unwrap();
+
+ let blob_oid = blob.object_id();
+ assert_eq!(
+ blob_oid.to_string(),
+ "b84992d24be67536837f5ab45a943f1b3f501878"
+ );
+
+ let commit_header = blob.commit();
+ assert_eq!(
+ commit_header.id.to_string(),
+ "e24124b7538658220b5aaf3b6ef53758f0a106dc"
+ );
+
+ assert!(!blob.is_binary());
+
+ // Verify the blob content size matches with the file size of "memory.rs"
+ let content = blob.content();
+ assert_eq!(blob.size(), 6253);
+
+ // Verify to_owned().
+ let blob_owned = blob.to_owned();
+ assert_eq!(blob_owned.size(), 6253);
+ assert_eq!(blob.content(), blob_owned.content());
+
+ // Verify JSON output is the same.
+ let json_ref = json!({ "content": content }).to_string();
+ let json_owned = json!( {
+ "content": blob_owned.content()
+ })
+ .to_string();
+ assert_eq!(json_ref, json_owned);
+}
+
+#[test]
+fn tree_ordering() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let tree = repo
+ .tree(refname!("refs/heads/master"), &PathBuf::new())
+ .unwrap();
+ assert_eq!(
+ tree.entries()
+ .iter()
+ .map(|entry| entry.name().to_string())
+ .collect::<Vec<_>>(),
+ vec![
+ "bin".to_string(),
+ "special".to_string(),
+ "src".to_string(),
+ "text".to_string(),
+ "this".to_string(),
+ ".i-am-well-hidden".to_string(),
+ ".i-too-am-hidden".to_string(),
+ "README.md".to_string(),
+ ]
+ );
+}
+
+#[test]
+fn commit_branches() {
+ let repo = Repository::open(GIT_PLATINUM).unwrap();
+ let init_commit = "d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3";
+ let glob = Glob::all_heads().branches().and(Glob::all_remotes());
+ let branches = repo.revision_branches(init_commit, glob).unwrap();
+
+ assert_eq!(branches.len(), 11);
+
+ let refnames: Vec<_> = branches.iter().map(|b| b.refname().to_string()).collect();
+ assert_eq!(
+ refnames,
+ vec![
+ "refs/heads/dev",
+ "refs/heads/diff-test",
+ "refs/heads/empty-branch",
+ "refs/heads/master",
+ "refs/remotes/banana/orange/pineapple",
+ "refs/remotes/banana/pineapple",
+ "refs/remotes/origin/HEAD",
+ "refs/remotes/origin/dev",
+ "refs/remotes/origin/diff-test",
+ "refs/remotes/origin/empty-branch",
+ "refs/remotes/origin/master"
+ ]
+ );
+}
diff --git a/crates/radicle-surf/t/src/submodule.rs b/crates/radicle-surf/t/src/submodule.rs
new file mode 100644
index 000000000..62129b8f6
--- /dev/null
+++ b/crates/radicle-surf/t/src/submodule.rs
@@ -0,0 +1,86 @@
+use std::{convert::Infallible, path::Path};
+
+use proptest::{collection, proptest};
+use radicle_git_ext::commit::CommitData;
+use radicle_git_ext::ref_format::refname;
+use radicle_git_ext_test::gen;
+use radicle_surf::tree::EntryKind;
+use radicle_surf::{fs, Branch, Repository};
+
+proptest! {
+ #[test]
+ fn test_submodule(
+ initial in gen::commit::commit(),
+ commits in collection::vec(gen::commit::commit(), 1..5)
+ ) {
+ prop::test_submodule(initial, commits)
+ }
+
+ #[ignore = "segfault"]
+ #[test]
+ fn test_submodule_bare(
+ initial in gen::commit::commit(),
+ commits in collection::vec(gen::commit::commit(), 1..5)
+ ) {
+ prop::test_submodule_bare(initial, commits)
+ }
+
+}
+
+mod prop {
+ use radicle_git_ext_test::{gen::commit, repository};
+
+ use super::*;
+
+ pub fn test_submodule(
+ initial: CommitData<commit::TreeData, Infallible>,
+ commits: Vec<CommitData<commit::TreeData, Infallible>>,
+ ) {
+ let refname = refname!("refs/heads/master");
+ let author = git2::Signature::try_from(initial.author()).unwrap();
+
+ let submodule = repository::fixture(&refname, commits).unwrap();
+ let repo = repository::fixture(&refname, vec![initial]).unwrap();
+
+ let head = repo.head.expect("missing initial commit");
+ let sub =
+ repository::submodule(&repo.inner, &submodule.inner, &refname, head, &author).unwrap();
+
+ let repo = Repository::open(repo.inner.path()).unwrap();
+ let branch = Branch::local(refname);
+ let dir = repo.root_dir(&branch).unwrap();
+
+ let platinum = dir.find_entry(&sub.path(), &repo).unwrap();
+ assert!(matches!(&platinum, fs::Entry::Submodule(module) if module.url().is_some()));
+
+ let root = repo.tree(&branch, &Path::new("")).unwrap();
+ let kind = EntryKind::from(platinum);
+ assert!(root.entries().iter().any(|e| e.entry() == &kind));
+ }
+
+ pub fn test_submodule_bare(
+ initial: CommitData<commit::TreeData, Infallible>,
+ commits: Vec<CommitData<commit::TreeData, Infallible>>,
+ ) {
+ let refname = refname!("refs/heads/master");
+ let author = git2::Signature::try_from(initial.author()).unwrap();
+
+ let submodule = repository::fixture(&refname, commits).unwrap();
+ let repo = repository::bare_fixture(&refname, vec![initial]).unwrap();
+
+ let head = repo.head.expect("missing initial commit");
+ let sub =
+ repository::submodule(&repo.inner, &submodule.inner, &refname, head, &author).unwrap();
+
+ let repo = Repository::open(repo.inner.path()).unwrap();
+ let branch = Branch::local(refname);
+ let dir = repo.root_dir(&branch).unwrap();
+
+ let platinum = dir.find_entry(&sub.path(), &repo).unwrap();
+ assert!(matches!(&platinum, fs::Entry::Submodule(module) if module.url().is_some()));
+
+ let root = repo.tree(&branch, &Path::new("")).unwrap();
+ let kind = EntryKind::from(platinum);
+ assert!(root.entries().iter().any(|e| e.entry() == &kind));
+ }
+}
diff --git a/crates/radicle-surf/t/src/threading.rs b/crates/radicle-surf/t/src/threading.rs
new file mode 100644
index 000000000..47cca4859
--- /dev/null
+++ b/crates/radicle-surf/t/src/threading.rs
@@ -0,0 +1,37 @@
+use std::sync::{Mutex, MutexGuard};
+
+use radicle_git_ext::ref_format::{name::component, refname};
+use radicle_surf::{Branch, Error, Glob, Repository};
+
+use super::GIT_PLATINUM;
+
+#[test]
+fn basic_test() -> Result<(), Error> {
+ let shared_repo = Mutex::new(Repository::open(GIT_PLATINUM)?);
+ let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
+ let mut branches = locked_repo
+ .branches(Glob::all_heads().branches().and(Glob::all_remotes()))?
+ .collect::<Result<Vec<_>, _>>()?;
+ branches.sort();
+
+ let origin = component!("origin");
+ let banana = component!("banana");
+ assert_eq!(
+ branches,
+ vec![
+ Branch::local(refname!("dev")),
+ Branch::local(refname!("diff-test")),
+ Branch::local(refname!("empty-branch")),
+ Branch::local(refname!("master")),
+ Branch::remote(banana.clone(), refname!("orange/pineapple")),
+ Branch::remote(banana, refname!("pineapple")),
+ Branch::remote(origin.clone(), refname!("HEAD")),
+ Branch::remote(origin.clone(), refname!("dev")),
+ Branch::remote(origin.clone(), refname!("diff-test")),
+ Branch::remote(origin.clone(), refname!("empty-branch")),
+ Branch::remote(origin, refname!("master")),
+ ]
+ );
+
+ Ok(())
+}
commit 64a028353b4b5a657370b9c6056454a8c44747e1
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Thu Jul 23 21:51:32 2026 +0200
scripts/just/check-ellipses: Replace less
Replacing all "..." with "…" is too aggressive.
diff --git a/scripts/just/check-ellipses.sh b/scripts/just/check-ellipses.sh
index 8bbacd96e..85bcbe0dd 100755
--- a/scripts/just/check-ellipses.sh
+++ b/scripts/just/check-ellipses.sh
@@ -40,7 +40,6 @@ printf '%s\0' "${files[@]}" | xargs -0 "${sed_cmd[@]}" \
-e "/${git_hex_range}/b" \
-e "/${git_ref_range}/b" \
-e "/${bracket_wildcard}/b" \
- -e 's/\.\.\./…/g'
after=$(printf '%s\0' "${files[@]}" | xargs -0 md5sum)
commit e9b999e2dd1950daab2b1405dccd9a99d5e8ffae
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
Date: Thu Jul 23 22:12:29 2026 +0200
protocol: Calm `typos`
The `typos` spell-checker complains about "[T]ype" and "[I]nitiator".
Reformulate the comment to work around the issue.
diff --git a/crates/radicle-protocol/src/wire/frame.rs b/crates/radicle-protocol/src/wire/frame.rs
index 78b11cc9d..387b385ed 100644
--- a/crates/radicle-protocol/src/wire/frame.rs
+++ b/crates/radicle-protocol/src/wire/frame.rs
@@ -189,11 +189,16 @@ impl From<StreamType> for u8 {
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | 'r' | 'a' | 'd' | 0x1 | Version
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-/// | Stream ID |TTT|I| Stream ID with Stream [T]ype and [I]nitiator bits
+/// | |T T T I| Stream ID
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-/// | Data …| Data (variable size)
+/// | …| Data (variable size)
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// ```
+///
+/// Note that the last four bits of the Stream ID encode the Stream Type
+/// and Initiator.
+/// The first three bits of the last four bits encode the Stream Type,
+/// while the last bit encodes the Initiator.
#[derive(Debug, PartialEq, Eq)]
pub struct Frame<M = Message> {
/// The protocol version.
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a -v /opt/radcis/ci.rad.levitte.org/cci/state/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/s:/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w:/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w -w /c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.97.1-x86_64-unknown-linux-gnu'
info: latest update on 2026-07-16, rust version 1.97.1 (8bab26f4f 2026-07-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.97.1 (c980f4866 2026-06-30)
+ rustc --version
rustc 1.97.1 (8bab26f4f 2026-07-14)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- --deny warnings
Updating crates.io index
Downloading crates ...
Downloaded either v1.15.0
Downloaded clap_derive v4.6.0
Downloaded clap_builder v4.6.0
Downloaded clap v4.6.0
Downloaded chrono v0.4.44
Downloaded clap_complete v4.6.0
Downloaded gix-glob v0.26.0
Downloaded addr2line v0.25.1
Downloaded gix-odb v0.80.0
Downloaded scopeguard v1.2.0
Downloaded siphasher v0.3.11
Downloaded gix-revwalk v0.31.0
Downloaded cipher v0.4.4
Downloaded thiserror-impl v1.0.69
Downloaded crossbeam-channel v0.5.15
Downloaded gix-command v0.9.0
Downloaded ed25519-dalek v2.2.0
Downloaded fast-glob v0.3.3
Downloaded equivalent v1.0.2
Downloaded const-str v0.4.3
Downloaded ff v0.13.1
Downloaded env_filter v1.0.0
Downloaded env_logger v0.11.9
Downloaded crossterm v0.29.0
Downloaded rusty-fork v0.3.1
Downloaded cypheraddr v0.4.1
Downloaded crossbeam-utils v0.8.21
Downloaded gix-features v0.48.0
Downloaded const-oid v0.9.6
Downloaded sem_safe v0.2.1
Downloaded form_urlencoded v1.2.2
Downloaded ed25519 v2.2.3
Downloaded crypto-bigint v0.5.5
Downloaded rustc_version v0.4.1
Downloaded serde-untagged v0.1.9
Downloaded escargot v0.5.15
Downloaded getrandom v0.4.3
Downloaded derive_more-impl v2.1.1
Downloaded gix-revision v0.45.0
Downloaded qcheck v1.0.0
Downloaded gix-diff v0.63.0
Downloaded gix-refspec v0.41.0
Downloaded getrandom v0.2.17
Downloaded data-encoding-macro-internal v0.1.17
Downloaded gix-chunk v0.7.1
Downloaded serde_spanned v1.0.4
Downloaded signature v1.6.4
Downloaded simd-adler32 v0.3.8
Downloaded similar v3.1.1
Downloaded fastrand v2.3.0
Downloaded gix-hashtable v0.15.0
Downloaded ctr v0.9.2
Downloaded fraction v0.15.3
Downloaded colored v2.2.0
Downloaded regex-syntax v0.8.10
Downloaded der v0.7.10
Downloaded email_address v0.2.9
Downloaded find-msvc-tools v0.1.9
Downloaded gix-config-value v0.18.0
Downloaded signals_receipts v0.2.5
Downloaded getrandom v0.3.4
Downloaded quick-error v1.2.3
Downloaded sval_json v2.17.0
Downloaded serde_derive_internals v0.29.1
Downloaded rand_core v0.9.5
Downloaded elliptic-curve v0.13.8
Downloaded serde v1.0.228
Downloaded shlex v1.3.0
Downloaded gix-quote v0.7.1
Downloaded gix-trace v0.1.19
Downloaded quote v1.0.45
Downloaded colorchoice v1.0.5
Downloaded gix-date v0.15.3
Downloaded data-encoding-macro v0.1.19
Downloaded fancy-regex v0.14.0
Downloaded data-encoding v2.10.0
Downloaded derive_more v2.1.1
Downloaded diff v0.1.13
Downloaded structured-logger v1.0.5
Downloaded cyphergraphy v0.3.1
Downloaded rand v0.8.5
Downloaded convert_case v0.10.0
Downloaded gimli v0.32.3
Downloaded scrypt v0.11.0
Downloaded shell-words v1.1.1
Downloaded console v0.16.3
Downloaded clap_lex v1.1.0
Downloaded fnv v1.0.7
Downloaded gix-ref v0.63.0
Downloaded document-features v0.2.12
Downloaded faster-hex v0.10.0
Downloaded sha1 v0.10.6
Downloaded gix-actor v0.41.0
Downloaded cpufeatures v0.2.17
Downloaded erased-serde v0.4.10
Downloaded crypto-common v0.1.7
Downloaded ghash v0.5.1
Downloaded gix-fs v0.21.1
Downloaded sval_fmt v2.17.0
Downloaded digest v0.10.7
Downloaded gix-pack v0.70.0
Downloaded heapless v0.8.0
Downloaded dyn-clone v1.0.20
Downloaded generic-array v0.14.7
Downloaded crc32fast v1.5.0
Downloaded systemd-journal-logger v2.2.2
Downloaded rand_chacha v0.3.1
Downloaded idna_adapter v1.2.1
Downloaded rfc6979 v0.4.0
Downloaded lock_api v0.4.14
Downloaded rustversion v1.0.22
Downloaded sval v2.17.0
Downloaded gix-lock v23.0.0
Downloaded memmap2 v0.9.10
Downloaded serde_fmt v1.1.0
Downloaded subtle v2.6.1
Downloaded signature v2.2.0
Downloaded pbkdf2 v0.12.2
Downloaded signal-hook-mio v0.2.5
Downloaded num-cmp v0.1.0
Downloaded schemars_derive v1.2.1
Downloaded sha2 v0.10.9
Downloaded sval_ref v2.17.0
Downloaded dunce v1.0.5
Downloaded ec25519 v0.1.0
Downloaded curve25519-dalek-derive v0.1.1
Downloaded keccak v0.1.6
Downloaded qcheck-macros v1.0.0
Downloaded sval_dynamic v2.17.0
Downloaded rsa v0.9.10
Downloaded signal-hook-registry v1.4.8
Downloaded git-ref-format-core v0.6.0
Downloaded gix-prompt v0.15.0
Downloaded ppv-lite86 v0.2.21
Downloaded gix-commitgraph v0.37.0
Downloaded num-rational v0.4.2
Downloaded fluent-uri v0.3.2
Downloaded strsim v0.11.1
Downloaded bytecount v0.6.9
Downloaded displaydoc v0.2.5
Downloaded pkg-config v0.3.32
Downloaded ssh-encoding v0.2.0
Downloaded gix-negotiate v0.31.0
Downloaded secrecy v0.10.3
Downloaded toml v0.9.12+spec-1.1.0
Downloaded base32 v0.4.0
Downloaded siphasher v1.0.2
Downloaded sval_serde v2.17.0
Downloaded ed25519 v1.5.3
Downloaded gix-error v0.2.3
Downloaded same-file v1.0.6
Downloaded ssh-cipher v0.2.0
Downloaded smallvec v1.15.1
Downloaded test-log-macros v0.2.19
Downloaded thiserror-impl v2.0.18
Downloaded rand_chacha v0.9.0
Downloaded thiserror v1.0.69
Downloaded sqlite v0.37.0
Downloaded sqlite3-sys v0.18.0
Downloaded gix-hash v0.25.0
Downloaded ref-cast v1.0.25
Downloaded salsa20 v0.10.2
Downloaded socket2 v0.5.10
Downloaded snapbox-macros v1.1.0
Downloaded sval_nested v2.17.0
Downloaded wait-timeout v0.2.1
Downloaded emojis v0.6.4
Downloaded errno v0.3.14
Downloaded socks5-client v0.4.3
Downloaded spin v0.9.8
Downloaded referencing v0.30.0
Downloaded ecdsa v0.16.9
Downloaded gix-traverse v0.57.0
Downloaded stable_deref_trait v1.2.1
Downloaded flate2 v1.1.9
Downloaded semver v1.0.27
Downloaded rustc-demangle v0.1.27
Downloaded bcrypt-pbkdf v0.10.0
Downloaded bit-set v0.8.0
Downloaded rand_xorshift v0.4.0
Downloaded ref-cast-impl v1.0.25
Downloaded rand_core v0.6.4
Downloaded cyphernet v0.5.4
Downloaded gix-credentials v0.38.0
Downloaded p384 v0.13.1
Downloaded serde_core v1.0.228
Downloaded spki v0.7.3
Downloaded serde_derive v1.0.228
Downloaded value-bag-serde1 v1.12.0
Downloaded streaming-iterator v0.1.9
Downloaded thiserror v2.0.18
Downloaded utf8parse v0.2.2
Downloaded gix-protocol v0.61.0
Downloaded tree-sitter v0.24.7
Downloaded sharded-slab v0.1.7
Downloaded cbc v0.1.2
Downloaded ssh-agent-lib v0.6.0
Downloaded signal-hook v0.3.18
Downloaded base-x v0.2.11
Downloaded pin-project-lite v0.2.17
Downloaded test-log v0.2.19
Downloaded tree-sitter-html v0.23.2
Downloaded sval_buffer v2.17.0
Downloaded tempfile v3.27.0
Downloaded gix-packetline v0.21.3
Downloaded gix-url v0.36.0
Downloaded hashbrown v0.16.1
Downloaded tinystr v0.8.2
Downloaded potential_utf v0.1.4
Downloaded lazy_static v1.5.0
Downloaded miniz_oxide v0.8.9
Downloaded uuid-simd v0.8.0
Downloaded itoa v1.0.17
Downloaded ssh-key v0.6.7
Downloaded synstructure v0.13.2
Downloaded unit-prefix v0.5.2
Downloaded nu-ansi-term v0.50.3
Downloaded git2 v0.21.0
Downloaded gix-path v0.12.0
Downloaded icu_collections v2.1.1
Downloaded gix-object v0.60.0
Downloaded hex-literal v1.1.0
Downloaded sec1 v0.7.3
Downloaded zerotrie v0.2.3
Downloaded snapbox v1.2.2
Downloaded tree-sitter-json v0.24.8
Downloaded tree-sitter-highlight v0.24.7
Downloaded inout v0.1.4
Downloaded gix-utils v0.3.2
Downloaded blowfish v0.9.1
Downloaded litemap v0.8.1
Downloaded thread_local v1.1.9
Downloaded ryu v1.0.23
Downloaded gix-shallow v0.12.0
Downloaded phf v0.11.3
Downloaded adler2 v2.0.1
Downloaded opaque-debug v0.3.1
Downloaded phf_shared v0.11.3
Downloaded anstyle-parse v0.2.7
Downloaded bytesize v2.3.1
Downloaded zerovec-derive v0.11.2
Downloaded tree-sitter-go v0.23.4
Downloaded anyhow v1.0.102
Downloaded tracing-log v0.2.0
Downloaded num-iter v0.1.45
Downloaded chacha20 v0.9.1
Downloaded yoke v0.8.1
Downloaded typeid v1.0.3
Downloaded schemars v1.2.1
Downloaded pem-rfc7468 v0.7.0
Downloaded multibase v0.9.2
Downloaded anstyle-parse v1.0.0
Downloaded block-padding v0.3.3
Downloaded gix-validate v0.11.1
Downloaded chacha20poly1305 v0.10.1
Downloaded jiff-static v0.2.23
Downloaded num-integer v0.1.46
Downloaded zerofrom-derive v0.1.6
Downloaded anstyle-query v1.1.5
Downloaded match-lookup v0.1.2
Downloaded value-bag-sval2 v1.12.0
Downloaded nonempty v0.9.0
Downloaded lexopt v0.3.2
Downloaded yoke-derive v0.8.1
Downloaded matchers v0.2.0
Downloaded percent-encoding v2.3.2
Downloaded poly1305 v0.8.0
Downloaded rand v0.9.2
Downloaded num-traits v0.2.19
Downloaded universal-hash v0.5.1
Downloaded unicode-normalization v0.1.25
Downloaded tinyvec_macros v0.1.1
Downloaded autocfg v1.5.0
Downloaded amplify v4.9.0
Downloaded group v0.13.0
Downloaded polyval v0.6.2
Downloaded primeorder v0.13.6
Downloaded amplify_derive v4.0.1
Downloaded hmac v0.12.1
Downloaded outref v0.5.2
Downloaded byteorder v1.5.0
Downloaded icu_properties v2.1.2
Downloaded num-complex v0.4.6
Downloaded parking_lot_core v0.9.12
Downloaded block-buffer v0.10.4
Downloaded aead v0.5.2
Downloaded uuid v1.22.0
Downloaded indicatif v0.18.4
Downloaded utf8_iter v1.0.4
Downloaded human-panic v2.0.6
Downloaded amplify_syn v2.0.1
Downloaded nonempty v0.12.0
Downloaded amplify_num v0.5.3
Downloaded gix-tempfile v23.0.0
Downloaded pkcs8 v0.10.2
Downloaded base64ct v1.8.3
Downloaded icu_normalizer_data v2.1.1
Downloaded anstream v1.0.0
Downloaded memchr v2.8.0
Downloaded iana-time-zone v0.1.65
Downloaded is_terminal_polyfill v1.70.2
Downloaded gix-sec v0.14.0
Downloaded base256emoji v1.0.2
Downloaded vsimd v0.8.0
Downloaded num v0.4.3
Downloaded heck v0.5.0
Downloaded proc-macro2 v1.0.106
Downloaded cc v1.2.57
Downloaded curve25519-dalek v4.1.3
Downloaded humantime v2.3.0
Downloaded unicode-ident v1.0.24
Downloaded hash32 v0.3.1
Downloaded base16ct v0.2.0
Downloaded normalize-line-endings v0.3.0
Downloaded zerofrom v0.1.6
Downloaded borrow-or-share v0.2.4
Downloaded value-bag v1.12.0
Downloaded icu_locale_core v2.1.1
Downloaded walkdir v2.5.0
Downloaded tree-sitter-language v0.1.7
Downloaded cfg-if v1.0.4
Downloaded pastey v0.2.1
Downloaded zeroize v1.9.0
Downloaded noise-framework v0.4.1
Downloaded url v2.5.8
Downloaded bitflags v2.11.0
Downloaded regex v1.12.3
Downloaded anstream v0.6.21
Downloaded version_check v0.9.5
Downloaded mio v1.1.1
Downloaded bytes v1.11.1
Downloaded unarray v0.1.4
Downloaded once_cell v1.21.4
Downloaded maybe-async v0.2.10
Downloaded ahash v0.8.12
Downloaded unicode-display-width v0.3.0
Downloaded zmij v1.0.21
Downloaded base64 v0.21.7
Downloaded p256 v0.13.2
Downloaded indexmap v2.13.0
Downloaded base64 v0.22.1
Downloaded ascii v1.1.0
Downloaded anstyle v1.0.14
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded bit-vec v0.8.0
Downloaded yansi v1.0.1
Downloaded parking_lot v0.12.5
Downloaded pretty_assertions v1.4.1
Downloaded unicode-segmentation v1.12.0
Downloaded arc-swap v1.9.1
Downloaded serde_json v1.0.149
Downloaded litrs v1.0.0
Downloaded jobserver v0.1.34
Downloaded toml_writer v1.0.7+spec-1.1.0
Downloaded num-bigint v0.4.6
Downloaded tinyvec v1.11.0
Downloaded num-bigint-dig v0.8.6
Downloaded tree-sitter-css v0.23.2
Downloaded writeable v0.6.2
Downloaded zerovec v0.11.5
Downloaded log v0.4.29
Downloaded tracing-core v0.1.36
Downloaded pkcs1 v0.7.5
Downloaded zlib-rs v0.6.5
Downloaded tracing v0.1.44
Downloaded icu_normalizer v2.1.1
Downloaded tracing-subscriber v0.3.23
Downloaded icu_provider v2.1.1
Downloaded jsonschema v0.30.0
Downloaded tree-sitter-c v0.23.4
Downloaded gix-transport v0.57.0
Downloaded backtrace v0.3.76
Downloaded prodash v31.0.0
Downloaded proptest v1.10.0
Downloaded unicode-width v0.2.2
Downloaded zerocopy v0.8.42
Downloaded aho-corasick v1.1.4
Downloaded aes-gcm v0.10.3
Downloaded sysinfo v0.37.2
Downloaded libm v0.2.16
Downloaded itertools v0.14.0
Downloaded typenum v1.20.1
Downloaded bloomy v1.2.0
Downloaded idna v1.1.0
Downloaded object v0.37.3
Downloaded inquire v0.9.4
Downloaded syn v1.0.109
Downloaded tree-sitter-rust v0.23.3
Downloaded p521 v0.13.3
Downloaded tree-sitter-python v0.23.6
Downloaded tree-sitter-ruby v0.23.1
Downloaded tree-sitter-typescript v0.23.2
Downloaded aes v0.8.4
Downloaded icu_properties_data v2.1.2
Downloaded rustix v1.1.4
Downloaded portable-atomic v1.13.1
Downloaded syn v2.0.117
Downloaded jiff v0.2.23
Downloaded libz-sys v1.1.25
Downloaded vcpkg v0.2.15
Downloaded sha1-checked v0.10.0
Downloaded bstr v1.12.1
Downloaded linux-raw-sys v0.12.1
Downloaded tree-sitter-bash v0.23.3
Downloaded regex-automata v0.4.14
Downloaded sha3 v0.10.8
Downloaded libc v0.2.183
Downloaded tokio v1.50.0
Downloaded libgit2-sys v0.18.4+1.9.3
Downloaded sqlite3-src v0.7.0
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded timeago v0.4.2
Downloaded tree-sitter-md v0.3.2
Compiling libc v0.2.183
Compiling proc-macro2 v1.0.106
Compiling quote v1.0.45
Compiling unicode-ident v1.0.24
Compiling serde_core v1.0.228
Checking cfg-if v1.0.4
Compiling serde v1.0.228
Compiling version_check v0.9.5
Compiling syn v2.0.117
Compiling generic-array v0.14.7
Checking getrandom v0.2.17
Checking typenum v1.20.1
Checking rand_core v0.6.4
Checking memchr v2.8.0
Compiling shlex v1.3.0
Compiling find-msvc-tools v0.1.9
Checking subtle v2.6.1
Checking const-oid v0.9.6
Checking smallvec v1.15.1
Checking regex-syntax v0.8.10
Checking aho-corasick v1.1.4
Compiling jobserver v0.1.34
Compiling cc v1.2.57
Checking cpufeatures v0.2.17
Compiling thiserror v2.0.18
Checking fastrand v2.3.0
Checking regex-automata v0.4.14
Compiling parking_lot_core v0.9.12
Checking scopeguard v1.2.0
Checking lock_api v0.4.14
Checking parking_lot v0.12.5
Checking bitflags v2.11.0
Checking stable_deref_trait v1.2.1
Compiling typeid v1.0.3
Compiling erased-serde v0.4.10
Checking tinyvec_macros v0.1.1
Checking itoa v1.0.17
Checking gix-trace v0.1.19
Checking tinyvec v1.11.0
Checking serde_fmt v1.1.0
Checking hashbrown v0.16.1
Checking unicode-normalization v0.1.25
Checking value-bag-serde1 v1.12.0
Checking bstr v1.12.1
Checking byteorder v1.5.0
Checking value-bag v1.12.0
Checking gix-utils v0.3.2
Compiling serde_derive v1.0.228
Compiling thiserror-impl v2.0.18
Checking log v0.4.29
Checking gix-validate v0.11.1
Compiling crc32fast v1.5.0
Checking same-file v1.0.6
Checking walkdir v2.5.0
Checking prodash v31.0.0
Checking zlib-rs v0.6.5
Compiling heapless v0.8.0
Compiling libm v0.2.16
Compiling pkg-config v0.3.32
Checking gix-path v0.12.0
Compiling autocfg v1.5.0
Checking gix-features v0.48.0
Compiling num-traits v0.2.19
Checking hash32 v0.3.1
Compiling rustix v1.1.4
Checking linux-raw-sys v0.12.1
Checking faster-hex v0.10.0
Compiling zerocopy v0.8.42
Compiling getrandom v0.4.3
Compiling zmij v1.0.21
Checking zeroize v1.9.0
Compiling serde_json v1.0.149
Checking equivalent v1.0.2
Checking indexmap v2.13.0
Checking crypto-common v0.1.7
Checking block-buffer v0.10.4
Checking digest v0.10.7
Checking sha1 v0.10.6
Checking sha1-checked v0.10.0
Checking once_cell v1.21.4
Compiling ref-cast v1.0.25
Checking gix-hash v0.25.0
Checking block-padding v0.3.3
Compiling ref-cast-impl v1.0.25
Checking inout v0.1.4
Checking sha2 v0.10.9
Compiling vcpkg v0.2.15
Checking dyn-clone v1.0.20
Checking cipher v0.4.4
Checking tempfile v3.27.0
Compiling serde_derive_internals v0.29.1
Checking der v0.7.10
Compiling libz-sys v1.1.25
Checking ppv-lite86 v0.2.21
Compiling libgit2-sys v0.18.4+1.9.3
Checking spin v0.9.8
Compiling schemars_derive v1.2.1
Checking lazy_static v1.5.0
Checking rand_chacha v0.3.1
Compiling syn v1.0.109
Checking rand v0.8.5
Compiling tree-sitter-language v0.1.7
Checking hmac v0.12.1
Checking universal-hash v0.5.1
Checking num-integer v0.1.46
Checking opaque-debug v0.3.1
Compiling thiserror v1.0.69
Checking spki v0.7.3
Checking signature v2.2.0
Checking schemars v1.2.1
Compiling thiserror-impl v1.0.69
Checking ff v0.13.1
Checking base16ct v0.2.0
Checking sec1 v0.7.3
Checking group v0.13.0
Checking crypto-bigint v0.5.5
Compiling data-encoding v2.10.0
Compiling data-encoding-macro-internal v0.1.17
Checking elliptic-curve v0.13.8
Compiling amplify_syn v2.0.1
Checking num-iter v0.1.45
Checking aead v0.5.2
Compiling match-lookup v0.1.2
Checking signature v1.6.4
Compiling semver v1.0.27
Checking const-str v0.4.3
Compiling rustc_version v0.4.1
Checking base256emoji v1.0.2
Checking data-encoding-macro v0.1.19
Compiling amplify_derive v4.0.1
Checking ed25519 v1.5.3
Checking git-ref-format-core v0.6.0
Checking poly1305 v0.8.0
Checking rfc6979 v0.4.0
Checking qcheck v1.0.0
Checking chacha20 v0.9.1
Checking gix-error v0.2.3
Checking ascii v1.1.0
Checking amplify_num v0.5.3
Checking base-x v0.2.11
Checking multibase v0.9.2
Checking ecdsa v0.16.9
Checking ec25519 v0.1.0
Compiling curve25519-dalek v4.1.3
Checking primeorder v0.13.6
Checking amplify v4.9.0
Checking polyval v0.6.2
Checking base64ct v1.8.3
Compiling num-bigint-dig v0.8.6
Checking cyphergraphy v0.3.1
Checking pem-rfc7468 v0.7.0
Checking ghash v0.5.1
Checking pkcs8 v0.10.2
Checking pbkdf2 v0.12.2
Checking ctr v0.9.2
Checking aes v0.8.4
Compiling sqlite3-src v0.7.0
Compiling curve25519-dalek-derive v0.1.1
Compiling synstructure v0.13.2
Checking keccak v0.1.6
Checking sha3 v0.10.8
Checking aes-gcm v0.10.3
Checking pkcs1 v0.7.5
Checking ssh-encoding v0.2.0
Checking ed25519 v2.2.3
Checking cbc v0.1.2
Checking blowfish v0.9.1
Checking percent-encoding v2.3.2
Checking base32 v0.4.0
Checking cypheraddr v0.4.1
Checking rsa v0.9.10
Checking bcrypt-pbkdf v0.10.0
Checking ssh-cipher v0.2.0
Checking ed25519-dalek v2.2.0
Compiling zerofrom-derive v0.1.6
Checking p256 v0.13.2
Checking p384 v0.13.1
Checking p521 v0.13.3
Checking chacha20poly1305 v0.10.1
Checking noise-framework v0.4.1
Checking ssh-key v0.6.7
Checking zerofrom v0.1.6
Checking socks5-client v0.4.3
Compiling yoke-derive v0.8.1
Checking secrecy v0.10.3
Checking ssh-agent-lib v0.6.0
Checking cyphernet v0.5.4
Checking nonempty v0.9.0
Checking yoke v0.8.1
Checking anstyle-query v1.1.5
Checking utf8parse v0.2.2
Checking jiff v0.2.23
Checking radicle-git-ref-format v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-ref-format)
Compiling zerovec-derive v0.11.2
Checking zerovec v0.11.5
Checking radicle-localtime v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-localtime)
Compiling displaydoc v0.2.5
Checking errno v0.3.14
Checking is_terminal_polyfill v1.70.2
Checking colorchoice v1.0.5
Checking base64 v0.21.7
Checking anstyle v1.0.14
Checking gix-hashtable v0.15.0
Checking siphasher v1.0.2
Checking gix-date v0.15.3
Checking radicle-dag v0.10.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-dag)
Checking gix-actor v0.41.0
Checking gix-object v0.60.0
Checking memmap2 v0.9.10
Checking nonempty v0.12.0
Checking tinystr v0.8.2
Checking gix-chunk v0.7.1
Checking litemap v0.8.1
Checking writeable v0.6.2
Compiling radicle v0.25.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle)
Checking icu_locale_core v2.1.1
Checking gix-commitgraph v0.37.0
Checking zerotrie v0.2.3
Checking potential_utf v0.1.4
Checking anstyle-parse v1.0.0
Checking serde-untagged v0.1.9
Checking bytesize v2.3.1
Checking dunce v1.0.5
Compiling icu_properties_data v2.1.2
Compiling icu_normalizer_data v2.1.1
Checking fast-glob v0.3.3
Checking anstream v1.0.0
Checking icu_collections v2.1.1
Checking icu_provider v2.1.1
Checking gix-revwalk v0.31.0
Checking gix-fs v0.21.1
Checking mio v1.1.1
Checking sem_safe v0.2.1
Checking simd-adler32 v0.3.8
Checking adler2 v2.0.1
Checking miniz_oxide v0.8.9
Checking signals_receipts v0.2.5
Checking gix-tempfile v23.0.0
Checking gix-quote v0.7.1
Checking icu_properties v2.1.2
Checking icu_normalizer v2.1.1
Checking shell-words v1.1.1
Compiling signal-hook v0.3.18
Compiling unicode-segmentation v1.12.0
Checking either v1.15.0
Checking iana-time-zone v0.1.65
Checking chrono v0.4.44
Checking idna_adapter v1.2.1
Compiling convert_case v0.10.0
Checking gix-command v0.9.0
Checking radicle-signals v0.12.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-signals)
Checking signal-hook-registry v1.4.8
Checking colored v2.2.0
Checking utf8_iter v1.0.4
Compiling object v0.37.3
Compiling getrandom v0.3.4
Compiling rustversion v1.0.22
Checking idna v1.1.0
Compiling derive_more-impl v2.1.1
Checking gix-lock v23.0.0
Checking gix-url v0.36.0
Checking gix-config-value v0.18.0
Checking gix-sec v0.14.0
Checking gimli v0.32.3
Compiling litrs v1.0.0
Compiling portable-atomic v1.13.1
Checking unicode-width v0.2.2
Compiling document-features v0.2.12
Checking gix-prompt v0.15.0
Checking derive_more v2.1.1
Checking signal-hook-mio v0.2.5
Checking addr2line v0.25.1
Checking radicle-log v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-log)
Checking gix-revision v0.45.0
Checking gix-traverse v0.57.0
Checking gix-diff v0.63.0
Checking anstyle-parse v0.2.7
Checking gix-packetline v0.21.3
Checking gix-glob v0.26.0
Checking rustc-demangle v0.1.27
Checking gix-refspec v0.41.0
Checking gix-transport v0.57.0
Checking anstream v0.6.21
Checking gix-pack v0.70.0
Checking backtrace v0.3.76
Checking arc-swap v1.9.1
Checking crossterm v0.29.0
Checking gix-credentials v0.38.0
Checking console v0.16.3
Checking gix-shallow v0.12.0
Checking gix-ref v0.63.0
Checking gix-negotiate v0.31.0
Checking form_urlencoded v1.2.2
Compiling maybe-async v0.2.10
Checking regex v1.12.3
Compiling tree-sitter v0.24.7
Checking unit-prefix v0.5.2
Checking indicatif v0.18.4
Checking gix-protocol v0.61.0
Checking url v2.5.8
Checking inquire v0.9.4
Checking gix-odb v0.80.0
Checking unicode-display-width v0.3.0
Checking uuid v1.22.0
Checking bytes v1.11.1
Checking snapbox-macros v1.1.0
Checking salsa20 v0.10.2
Compiling heck v0.5.0
Compiling crossbeam-utils v0.8.21
Checking similar v3.1.1
Checking streaming-iterator v0.1.9
Checking strsim v0.11.1
Checking siphasher v0.3.11
Checking clap_lex v1.1.0
Checking normalize-line-endings v0.3.0
Checking snapbox v1.2.2
Checking clap_builder v4.6.0
Checking bloomy v1.2.0
Compiling clap_derive v4.6.0
Checking scrypt v0.11.0
Checking systemd-journal-logger v2.2.2
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-css v0.23.2
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-python v0.23.6
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-html v0.23.2
Checking serde_spanned v1.0.4
Checking toml_datetime v0.7.5+spec-1.1.0
Checking toml_writer v1.0.7+spec-1.1.0
Compiling anyhow v1.0.102
Checking bit-vec v0.8.0
Checking pin-project-lite v0.2.17
Checking tokio v1.50.0
Checking bit-set v0.8.0
Checking toml v0.9.12+spec-1.1.0
Checking sqlite3-sys v0.18.0
Checking sqlite v0.37.0
Checking radicle-crypto v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-crypto)
Checking clap v4.6.0
Checking rand_core v0.9.5
Checking sysinfo v0.37.2
Compiling radicle-node v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-node)
Compiling radicle-cli v0.22.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli)
Checking yansi v1.0.1
Checking diff v0.1.13
Checking pretty_assertions v1.4.1
Checking human-panic v2.0.6
Checking crossbeam-channel v0.5.15
Checking clap_complete v4.6.0
Checking structured-logger v1.0.5
Checking radicle-systemd v0.13.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.7
Checking itertools v0.14.0
Checking socket2 v0.5.10
Compiling escargot v0.5.15
Checking humantime v2.3.0
Checking timeago v0.4.2
Checking lexopt v0.3.2
Checking wait-timeout v0.2.1
Checking quick-error v1.2.3
Checking fnv v1.0.7
Checking rusty-fork v0.3.1
Checking rand v0.9.2
Checking rand_chacha v0.9.0
Checking rand_xorshift v0.4.0
Compiling qcheck-macros v1.0.0
Checking num-bigint v0.4.6
Compiling ahash v0.8.12
Checking unarray v0.1.4
Checking proptest v1.10.0
Checking num-complex v0.4.6
Checking env_filter v1.0.0
Checking borrow-or-share v0.2.4
Checking fluent-uri v0.3.2
Checking num-rational v0.4.2
Checking env_logger v0.11.9
Checking num v0.4.3
Checking flate2 v1.1.9
Checking phf_shared v0.11.3
Compiling test-log-macros v0.2.19
Compiling radicle-remote-helper v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-remote-helper)
Checking hex-literal v1.1.0
Checking vsimd v0.8.0
Checking outref v0.5.2
Checking test-log v0.2.19
Checking phf v0.11.3
Checking referencing v0.30.0
Checking uuid-simd v0.8.0
Checking fraction v0.15.3
Checking fancy-regex v0.14.0
Checking email_address v0.2.9
Checking num-cmp v0.1.0
Checking base64 v0.22.1
Checking bytecount v0.6.9
Checking emojis v0.6.4
Compiling pastey v0.2.1
Checking jsonschema v0.30.0
Checking radicle-windows v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-windows)
Checking git2 v0.21.0
Checking radicle-oid v0.2.1 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-oid)
Checking radicle-git-metadata v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-metadata)
Checking radicle-term v0.19.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-term)
Checking radicle-core v0.4.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-core)
Checking radicle-cob v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cob)
Checking radicle-surf v0.28.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-surf)
Checking radicle-cli-test v0.14.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli-test)
Checking radicle-fetch v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-fetch)
Checking radicle-schemars v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-schemars)
Checking radicle-protocol v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-protocol)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 41.29s
+ cargo build --all-targets --workspace
Compiling libc v0.2.183
Compiling cfg-if v1.0.4
Compiling serde_core v1.0.228
Compiling typenum v1.20.1
Compiling memchr v2.8.0
Compiling shlex v1.3.0
Compiling getrandom v0.2.17
Compiling jobserver v0.1.34
Compiling subtle v2.6.1
Compiling rand_core v0.6.4
Compiling const-oid v0.9.6
Compiling cc v1.2.57
Compiling regex-syntax v0.8.10
Compiling smallvec v1.15.1
Compiling aho-corasick v1.1.4
Compiling serde v1.0.228
Compiling zeroize v1.9.0
Compiling regex-automata v0.4.14
Compiling generic-array v0.14.7
Compiling crypto-common v0.1.7
Compiling block-buffer v0.10.4
Compiling digest v0.10.7
Compiling cpufeatures v0.2.17
Compiling thiserror v2.0.18
Compiling fastrand v2.3.0
Compiling scopeguard v1.2.0
Compiling lock_api v0.4.14
Compiling parking_lot_core v0.9.12
Compiling bitflags v2.11.0
Compiling parking_lot v0.12.5
Compiling stable_deref_trait v1.2.1
Compiling typeid v1.0.3
Compiling erased-serde v0.4.10
Compiling tinyvec_macros v0.1.1
Compiling gix-trace v0.1.19
Compiling itoa v1.0.17
Compiling tinyvec v1.11.0
Compiling serde_fmt v1.1.0
Compiling hashbrown v0.16.1
Compiling value-bag-serde1 v1.12.0
Compiling byteorder v1.5.0
Compiling unicode-normalization v0.1.25
Compiling value-bag v1.12.0
Compiling gix-utils v0.3.2
Compiling log v0.4.29
Compiling crc32fast v1.5.0
Compiling same-file v1.0.6
Compiling walkdir v2.5.0
Compiling prodash v31.0.0
Compiling bstr v1.12.1
Compiling zlib-rs v0.6.5
Compiling gix-validate v0.11.1
Compiling hash32 v0.3.1
Compiling heapless v0.8.0
Compiling gix-path v0.12.0
Compiling libm v0.2.16
Compiling linux-raw-sys v0.12.1
Compiling num-traits v0.2.19
Compiling rustix v1.1.4
Compiling faster-hex v0.10.0
Compiling sha1 v0.10.6
Compiling getrandom v0.4.3
Compiling sha1-checked v0.10.0
Compiling zerocopy v0.8.42
Compiling equivalent v1.0.2
Compiling indexmap v2.13.0
Compiling zmij v1.0.21
Compiling gix-features v0.48.0
Compiling gix-hash v0.25.0
Compiling once_cell v1.21.4
Compiling serde_json v1.0.149
Compiling block-padding v0.3.3
Compiling ref-cast v1.0.25
Compiling inout v0.1.4
Compiling sha2 v0.10.9
Compiling dyn-clone v1.0.20
Compiling libz-sys v1.1.25
Compiling cipher v0.4.4
Compiling tempfile v3.27.0
Compiling libgit2-sys v0.18.4+1.9.3
Compiling ppv-lite86 v0.2.21
Compiling der v0.7.10
Compiling spin v0.9.8
Compiling lazy_static v1.5.0
Compiling rand_chacha v0.3.1
Compiling schemars v1.2.1
Compiling rand v0.8.5
Compiling num-integer v0.1.46
Compiling hmac v0.12.1
Compiling universal-hash v0.5.1
Compiling opaque-debug v0.3.1
Compiling spki v0.7.3
Compiling signature v2.2.0
Compiling ff v0.13.1
Compiling base16ct v0.2.0
Compiling group v0.13.0
Compiling sec1 v0.7.3
Compiling thiserror v1.0.69
Compiling crypto-bigint v0.5.5
Compiling num-iter v0.1.45
Compiling aead v0.5.2
Compiling data-encoding v2.10.0
Compiling const-str v0.4.3
Compiling signature v1.6.4
Compiling ed25519 v1.5.3
Compiling elliptic-curve v0.13.8
Compiling base256emoji v1.0.2
Compiling data-encoding-macro v0.1.19
Compiling git-ref-format-core v0.6.0
Compiling poly1305 v0.8.0
Compiling rfc6979 v0.4.0
Compiling qcheck v1.0.0
Compiling chacha20 v0.9.1
Compiling gix-error v0.2.3
Compiling base-x v0.2.11
Compiling ascii v1.1.0
Compiling amplify_num v0.5.3
Compiling multibase v0.9.2
Compiling ecdsa v0.16.9
Compiling primeorder v0.13.6
Compiling ec25519 v0.1.0
Compiling polyval v0.6.2
Compiling base64ct v1.8.3
Compiling pem-rfc7468 v0.7.0
Compiling ghash v0.5.1
Compiling amplify v4.9.0
Compiling pkcs8 v0.10.2
Compiling cyphergraphy v0.3.1
Compiling pbkdf2 v0.12.2
Compiling ctr v0.9.2
Compiling aes v0.8.4
Compiling sqlite3-src v0.7.0
Compiling keccak v0.1.6
Compiling aes-gcm v0.10.3
Compiling sha3 v0.10.8
Compiling curve25519-dalek v4.1.3
Compiling pkcs1 v0.7.5
Compiling ssh-encoding v0.2.0
Compiling num-bigint-dig v0.8.6
Compiling ed25519 v2.2.3
Compiling blowfish v0.9.1
Compiling cbc v0.1.2
Compiling percent-encoding v2.3.2
Compiling base32 v0.4.0
Compiling cypheraddr v0.4.1
Compiling ssh-cipher v0.2.0
Compiling bcrypt-pbkdf v0.10.0
Compiling ed25519-dalek v2.2.0
Compiling rsa v0.9.10
Compiling p521 v0.13.3
Compiling p384 v0.13.1
Compiling p256 v0.13.2
Compiling chacha20poly1305 v0.10.1
Compiling noise-framework v0.4.1
Compiling ssh-key v0.6.7
Compiling zerofrom v0.1.6
Compiling socks5-client v0.4.3
Compiling secrecy v0.10.3
Compiling yoke v0.8.1
Compiling cyphernet v0.5.4
Compiling nonempty v0.9.0
Compiling ssh-agent-lib v0.6.0
Compiling utf8parse v0.2.2
Compiling anstyle-query v1.1.5
Compiling jiff v0.2.23
Compiling radicle-git-ref-format v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-ref-format)
Compiling zerovec v0.11.5
Compiling radicle-localtime v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-localtime)
Compiling errno v0.3.14
Compiling is_terminal_polyfill v1.70.2
Compiling colorchoice v1.0.5
Compiling base64 v0.21.7
Compiling anstyle v1.0.14
Compiling gix-hashtable v0.15.0
Compiling siphasher v1.0.2
Compiling tree-sitter-language v0.1.7
Compiling radicle-dag v0.10.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-dag)
Compiling gix-date v0.15.3
Compiling gix-actor v0.41.0
Compiling gix-object v0.60.0
Compiling memmap2 v0.9.10
Compiling nonempty v0.12.0
Compiling tinystr v0.8.2
Compiling gix-chunk v0.7.1
Compiling radicle v0.25.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle)
Compiling writeable v0.6.2
Compiling litemap v0.8.1
Compiling icu_locale_core v2.1.1
Compiling gix-commitgraph v0.37.0
Compiling zerotrie v0.2.3
Compiling potential_utf v0.1.4
Compiling anstyle-parse v1.0.0
Compiling serde-untagged v0.1.9
Compiling bytesize v2.3.1
Compiling fast-glob v0.3.3
Compiling dunce v1.0.5
Compiling anstream v1.0.0
Compiling icu_collections v2.1.1
Compiling icu_provider v2.1.1
Compiling gix-revwalk v0.31.0
Compiling gix-fs v0.21.1
Compiling mio v1.1.1
Compiling sem_safe v0.2.1
Compiling simd-adler32 v0.3.8
Compiling adler2 v2.0.1
Compiling miniz_oxide v0.8.9
Compiling signals_receipts v0.2.5
Compiling gix-tempfile v23.0.0
Compiling icu_properties_data v2.1.2
Compiling icu_normalizer_data v2.1.1
Compiling gix-quote v0.7.1
Compiling unicode-segmentation v1.12.0
Compiling icu_normalizer v2.1.1
Compiling icu_properties v2.1.2
Compiling iana-time-zone v0.1.65
Compiling shell-words v1.1.1
Compiling either v1.15.0
Compiling idna_adapter v1.2.1
Compiling gix-command v0.9.0
Compiling chrono v0.4.44
Compiling convert_case v0.10.0
Compiling radicle-signals v0.12.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-signals)
Compiling signal-hook-registry v1.4.8
Compiling colored v2.2.0
Compiling utf8_iter v1.0.4
Compiling idna v1.1.0
Compiling signal-hook v0.3.18
Compiling derive_more-impl v2.1.1
Compiling gix-lock v23.0.0
Compiling gix-url v0.36.0
Compiling gix-config-value v0.18.0
Compiling gix-sec v0.14.0
Compiling unicode-width v0.2.2
Compiling gimli v0.32.3
Compiling gix-prompt v0.15.0
Compiling derive_more v2.1.1
Compiling signal-hook-mio v0.2.5
Compiling object v0.37.3
Compiling addr2line v0.25.1
Compiling getrandom v0.3.4
Compiling radicle-log v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-log)
Compiling gix-traverse v0.57.0
Compiling gix-revision v0.45.0
Compiling gix-diff v0.63.0
Compiling anstyle-parse v0.2.7
Compiling gix-glob v0.26.0
Compiling gix-packetline v0.21.3
Compiling rustc-demangle v0.1.27
Compiling gix-transport v0.57.0
Compiling backtrace v0.3.76
Compiling gix-refspec v0.41.0
Compiling anstream v0.6.21
Compiling sqlite3-sys v0.18.0
Compiling gix-pack v0.70.0
Compiling sqlite v0.37.0
Compiling arc-swap v1.9.1
Compiling radicle-crypto v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-crypto)
Compiling crossterm v0.29.0
Compiling gix-credentials v0.38.0
Compiling portable-atomic v1.13.1
Compiling console v0.16.3
Compiling gix-ref v0.63.0
Compiling gix-shallow v0.12.0
Compiling gix-negotiate v0.31.0
Compiling form_urlencoded v1.2.2
Compiling regex v1.12.3
Compiling tree-sitter v0.24.7
Compiling unit-prefix v0.5.2
Compiling indicatif v0.18.4
Compiling gix-protocol v0.61.0
Compiling url v2.5.8
Compiling gix-odb v0.80.0
Compiling inquire v0.9.4
Compiling unicode-display-width v0.3.0
Compiling uuid v1.22.0
Compiling bytes v1.11.1
Compiling snapbox-macros v1.1.0
Compiling salsa20 v0.10.2
Compiling clap_lex v1.1.0
Compiling siphasher v0.3.11
Compiling strsim v0.11.1
Compiling streaming-iterator v0.1.9
Compiling similar v3.1.1
Compiling normalize-line-endings v0.3.0
Compiling clap_builder v4.6.0
Compiling snapbox v1.2.2
Compiling bloomy v1.2.0
Compiling scrypt v0.11.0
Compiling systemd-journal-logger v2.2.2
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-css v0.23.2
Compiling tree-sitter-c v0.23.4
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-python v0.23.6
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-rust v0.23.3
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-typescript v0.23.2
Compiling toml_datetime v0.7.5+spec-1.1.0
Compiling serde_spanned v1.0.4
Compiling pin-project-lite v0.2.17
Compiling toml_writer v1.0.7+spec-1.1.0
Compiling bit-vec v0.8.0
Compiling toml v0.9.12+spec-1.1.0
Compiling bit-set v0.8.0
Compiling tokio v1.50.0
Compiling clap v4.6.0
Compiling crossbeam-utils v0.8.21
Compiling rand_core v0.9.5
Compiling sysinfo v0.37.2
Compiling radicle-cli v0.22.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli)
Compiling radicle-node v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-node)
Compiling diff v0.1.13
Compiling yansi v1.0.1
Compiling human-panic v2.0.6
Compiling pretty_assertions v1.4.1
Compiling crossbeam-channel v0.5.15
Compiling clap_complete v4.6.0
Compiling structured-logger v1.0.5
Compiling anyhow v1.0.102
Compiling radicle-systemd v0.13.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-systemd)
Compiling tree-sitter-highlight v0.24.7
Compiling itertools v0.14.0
Compiling socket2 v0.5.10
Compiling lexopt v0.3.2
Compiling timeago v0.4.2
Compiling humantime v2.3.0
Compiling wait-timeout v0.2.1
Compiling fnv v1.0.7
Compiling quick-error v1.2.3
Compiling escargot v0.5.15
Compiling rusty-fork v0.3.1
Compiling rand_chacha v0.9.0
Compiling git2 v0.21.0
Compiling rand v0.9.2
Compiling rand_xorshift v0.4.0
Compiling num-bigint v0.4.6
Compiling unarray v0.1.4
Compiling proptest v1.10.0
Compiling num-complex v0.4.6
Compiling num-rational v0.4.2
Compiling env_filter v1.0.0
Compiling borrow-or-share v0.2.4
Compiling env_logger v0.11.9
Compiling fluent-uri v0.3.2
Compiling num v0.4.3
Compiling ahash v0.8.12
Compiling radicle-oid v0.2.1 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-oid)
Compiling radicle-git-metadata v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-metadata)
Compiling radicle-core v0.4.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-core)
Compiling radicle-cob v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cob)
Compiling radicle-surf v0.28.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-surf)
Compiling radicle-term v0.19.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-term)
Compiling radicle-cli-test v0.14.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli-test)
Compiling flate2 v1.1.9
Compiling phf_shared v0.11.3
Compiling vsimd v0.8.0
Compiling outref v0.5.2
Compiling hex-literal v1.1.0
Compiling radicle-remote-helper v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-remote-helper)
Compiling uuid-simd v0.8.0
Compiling test-log v0.2.19
Compiling phf v0.11.3
Compiling referencing v0.30.0
Compiling fraction v0.15.3
Compiling fancy-regex v0.14.0
Compiling email_address v0.2.9
Compiling bytecount v0.6.9
Compiling num-cmp v0.1.0
Compiling base64 v0.22.1
Compiling emojis v0.6.4
Compiling jsonschema v0.30.0
Compiling radicle-fetch v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-fetch)
Compiling radicle-protocol v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-protocol)
Compiling radicle-schemars v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-schemars)
Compiling radicle-windows v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-windows)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 57.57s
+ cargo doc --workspace --no-deps --all-features
Downloading crates ...
Downloaded dhat v0.3.3
Downloaded mintex v0.1.4
Downloaded minicbor v0.13.2
Downloaded rustc-hash v1.1.0
Downloaded thousands v0.2.0
Checking regex-automata v0.4.14
Compiling num-traits v0.2.19
Checking once_cell v1.21.4
Compiling syn v1.0.109
Checking tempfile v3.27.0
Checking radicle-git-metadata v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-metadata)
Checking rusty-fork v0.3.1
Checking num-integer v0.1.46
Checking radicle-localtime v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-localtime)
Checking proptest v1.10.0
Checking num-iter v0.1.45
Checking num-bigint-dig v0.8.6
Compiling amplify_syn v2.0.1
Checking bstr v1.12.1
Checking rsa v0.9.10
Compiling amplify_derive v4.0.1
Checking gix-validate v0.11.1
Checking gix-path v0.12.0
Checking gix-error v0.2.3
Checking gix-features v0.48.0
Checking git-ref-format-core v0.6.0
Checking ssh-key v0.6.7
Checking gix-hash v0.25.0
Checking radicle-git-ref-format v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-ref-format)
Checking gix-date v0.15.3
Checking radicle-oid v0.2.1 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-oid)
Checking gix-actor v0.41.0
Checking gix-hashtable v0.15.0
Checking gix-chunk v0.7.1
Checking gix-object v0.60.0
Checking radicle-dag v0.10.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-dag)
Checking gix-commitgraph v0.37.0
Checking ssh-agent-lib v0.6.0
Checking gix-fs v0.21.1
Checking gix-tempfile v23.0.0
Checking gix-quote v0.7.1
Checking gix-revwalk v0.31.0
Checking amplify v4.9.0
Checking gix-command v0.9.0
Checking chrono v0.4.44
Checking miniz_oxide v0.8.9
Checking gix-lock v23.0.0
Checking radicle-signals v0.12.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-signals)
Checking gix-url v0.36.0
Checking cyphergraphy v0.3.1
Checking gix-config-value v0.18.0
Checking cypheraddr v0.4.1
Checking noise-framework v0.4.1
Compiling minicbor v0.13.2
Checking backtrace v0.3.76
Checking idna v1.1.0
Checking socks5-client v0.4.3
Checking gix-prompt v0.15.0
Checking inquire v0.9.4
Checking gix-traverse v0.57.0
Checking gix-revision v0.45.0
Checking cyphernet v0.5.4
Checking radicle-crypto v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-crypto)
Checking gix-diff v0.63.0
Checking gix-glob v0.26.0
Checking gix-packetline v0.21.3
Checking radicle-core v0.4.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-core)
Checking radicle-cob v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cob)
Checking regex v1.12.3
Checking url v2.5.8
Checking gix-transport v0.57.0
Checking tree-sitter v0.24.7
Checking gix-refspec v0.41.0
Checking radicle v0.25.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle)
Checking gix-pack v0.70.0
Checking gix-credentials v0.38.0
Checking radicle-log v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-log)
Checking gix-shallow v0.12.0
Checking gix-ref v0.63.0
Checking gix-negotiate v0.31.0
Checking radicle-term v0.19.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-term)
Checking gix-protocol v0.61.0
Checking gix-odb v0.80.0
Checking uuid v1.22.0
Checking tree-sitter-toml-ng v0.6.0
Checking radicle-surf v0.28.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-surf)
Checking human-panic v2.0.6
Checking tree-sitter-highlight v0.24.7
Checking thousands v0.2.0
Compiling radicle-node v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-node)
Checking rustc-hash v1.1.0
Checking mintex v0.1.4
Checking dhat v0.3.3
Checking radicle-systemd v0.13.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-systemd)
Documenting radicle-cli-test v0.14.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli-test)
Documenting radicle-systemd v0.13.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-systemd)
Documenting radicle-surf v0.28.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-surf)
Documenting radicle v0.25.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle)
Documenting radicle-term v0.19.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-term)
Documenting radicle-core v0.4.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-core)
Documenting radicle-cob v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cob)
Documenting radicle-crypto v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-crypto)
Documenting radicle-log v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-log)
Documenting radicle-signals v0.12.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-signals)
Documenting radicle-oid v0.2.1 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-oid)
Documenting radicle-git-ref-format v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-ref-format)
Documenting radicle-git-metadata v0.2.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-git-metadata)
Documenting radicle-localtime v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-localtime)
Documenting radicle-dag v0.10.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-dag)
Documenting radicle-windows v0.1.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-windows)
Checking radicle-fetch v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-fetch)
Checking radicle-cli v0.22.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli)
Documenting radicle-cli v0.22.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-cli)
Documenting radicle-schemars v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-schemars)
Checking radicle-protocol v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-protocol)
Documenting radicle-node v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-node)
Documenting radicle-protocol v0.9.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-protocol)
Documenting radicle-fetch v0.21.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-fetch)
Documenting radicle-remote-helper v0.18.0 (/c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/crates/radicle-remote-helper)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.08s
Generated /c69ddadd-9f6b-41ab-85aa-bfac30dbaf0a/w/target/doc/radicle/index.html and 22 other files
+ cargo test --workspace --no-fail-fast
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.22s
Running unittests src/lib.rs (target/debug/deps/radicle-e94edd723df57b0d)
running 400 tests
test cob::cache::migrations::_2::tests::test_migration_2 ... ok
test canonical::formatter::test::ascii_control_characters ... ok
test canonical::formatter::test::securesystemslib_asserts ... ok
test canonical::formatter::test::ordered_nested_object ... ok
test cob::common::test::test_color ... ok
test cob::cache::tests::test_check_version ... ok
test cob::common::test::test_emojis ... ok
test cob::cache::migrations::_2::tests::test_patch_json_deserialization ... ok
test cob::cache::tests::test_migrate_to ... ok
test cob::common::test::test_title ... ok
test cob::identity::test::accepted_sibling_causes_rejection ... ok
test cob::identity::test::accept_rejects_sibling_accept ... ok
test cob::identity::test::authorization_based_on_parent_not_current ... ok
test cob::identity::test::cannot_redact_previously_accepted_revision ... ok
test cob::identity::test::concurrent_accept_before_redact_converge ... ok
test cob::identity::test::concurrent_redact_and_accept_converge ... ok
test cob::identity::test::evaluates_queued_children ... ok
test cob::identity::test::cascading_rejections ... ok
test cob::identity::test::evaluation_strips_author_accept_for_sibling_creation ... ok
test cob::identity::test::prop_json_eq_str ... ok
test cob::identity::test::evaluation_skips_sibling_accept_in_old_history ... ok
test cob::identity::test::has_accepted_active_sibling_returns_true_when_sibling_accepted ... ok
test cob::identity::test::evaluates_queued_children_with_new_delegate ... ok
test cob::identity::test::redact_parent_cascades ... ok
test cob::identity::test::reject_concurrent ... ok
test cob::identity::test::remove_delegate_concurrent ... ok
test cob::identity::test::test_identity_redact_revision ... ok
test cob::identity::test::terminal_states_concurrent ... ok
test cob::identity::test::test_identity_updates ... ok
test cob::identity::test::test_identity_update_rejected ... ok
test cob::identity::test::test_identity_cannot_redact_terminal_states ... ok
test cob::identity::test::test_valid_identity ... ok
test cob::identity::test::test_identity_updates_concurrent ... ok
test cob::issue::cache::tests::test_counts ... ok
test cob::issue::cache::tests::test_get ... ok
test cob::issue::cache::tests::test_is_empty ... ok
test cob::issue::cache::tests::test_list ... ok
test cob::issue::cache::tests::test_list_by_status ... ok
test cob::issue::cache::tests::test_remove ... ok
test cob::identity::test::test_identity_updates_concurrent_outdated ... ok
test cob::issue::test::test_embeds ... ok
test cob::identity::test::update_rejects_second_sibling_proposal ... ok
test cob::issue::test::test_embeds_edit ... ok
test cob::issue::test::test_invalid_actions ... ok
test cob::issue::test::test_invalid_tx ... ok
test cob::issue::test::test_invalid_cob ... ok
test cob::issue::test::test_invalid_tx_reference ... ok
test cob::issue::test::test_issue_all ... ok
test cob::issue::test::test_concurrency ... ok
test cob::issue::test::test_issue_comment ... ok
test cob::issue::test::test_issue_comment_redact ... ok
test cob::issue::test::test_issue_create_and_assign ... ok
test cob::issue::test::test_issue_create_and_change_state ... ok
test cob::issue::test::test_issue_create_and_get ... ok
test cob::issue::test::test_issue_create_and_reassign ... ok
test cob::issue::test::test_issue_create_and_unassign ... ok
test cob::issue::test::test_issue_edit ... ok
test cob::issue::test::test_issue_edit_description ... ok
test cob::issue::test::test_issue_multilines ... ok
test cob::issue::test::test_issue_label ... ok
test cob::issue::test::test_issue_state_serde ... ok
test cob::issue::test::test_ordering ... ok
test cob::patch::actions::test::test_review_edit ... ok
test cob::issue::test::test_issue_react ... ok
test cob::issue::test::test_issue_reply ... ok
test cob::patch::cache::tests::test_get ... ok
test cob::patch::cache::tests::test_is_empty ... ok
test cob::patch::cache::tests::test_counts ... ok
test cob::patch::cache::tests::test_list_by_status ... ok
test cob::patch::cache::tests::test_list ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_migration_null_summary ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_migration_with_summary ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_migration_without_summary ... ok
test cob::patch::encoding::review::test::test_review_deserialize_summary_v2 ... ok
test cob::patch::encoding::review::test::test_review_summary ... ok
test cob::patch::test::test_json ... ok
test cob::patch::test::test_json_serialisation_target ... ok
test cob::patch::test::test_json_serialization ... ok
test cob::patch::test::test_merge_target_resolution ... ok
test cob::patch::cache::tests::test_remove ... ok
test cob::patch::test::test_patch_create_and_get ... ok
test cob::patch::test::test_patch_discussion ... ok
test cob::patch::test::test_patch_merge_authorization_ref_formats ... ok
test cob::patch::test::test_patch_merge_custom_destination_authorized ... ok
test cob::patch::test::test_patch_merge_custom_destination_unauthorized ... ok
test cob::patch::test::test_patch_merge ... ok
test cob::patch::test::test_patch_redact ... ok
test cob::patch::test::test_patch_review ... ok
test cob::patch::test::test_patch_review_comment ... ok
test cob::patch::cache::tests::test_find_by_revision ... ok
test cob::patch::test::test_patch_review_duplicate ... ok
test cob::patch::test::test_patch_review_edit ... ok
test cob::patch::test::test_patch_review_edit_comment ... ok
test cob::patch::test::test_patch_review_remove_summary ... ok
test cob::patch::test::test_reactions_json_serialization ... ok
test cob::patch::test::test_revision_edit_redact ... ok
test cob::patch::test::test_revision_reaction ... ok
test cob::patch::test::test_revision_review_merge_redacted ... ok
test cob::patch::test::test_target_branch ... ok
test cob::stream::tests::test_all_from ... ok
test cob::stream::tests::test_all_from_until ... ok
test cob::patch::test::test_patch_review_revision_redact ... ok
test cob::stream::tests::test_all_until ... ok
test cob::stream::tests::test_from_until ... ok
test cob::stream::tests::test_regression_from_until ... ok
test cob::thread::tests::test_comment_edit_missing ... ok
test cob::thread::tests::test_comment_edit_redacted ... ok
test cob::thread::tests::test_comment_redact_missing ... ok
test cob::thread::tests::test_duplicate_comments ... ok
test cob::thread::tests::test_edit_comment ... ok
test cob::thread::tests::test_redact_comment ... ok
test cob::thread::tests::test_timeline ... ok
test git::canonical::protect::tests::refs_rad ... ok
test git::canonical::protect::tests::refs_rad_id ... ok
test git::canonical::protect::tests::refs_radieschen ... ok
test git::canonical::quorum::test::merge_base_commutative ... ok
test git::canonical::quorum::test::test_merge_bases ... ok
test cob::patch::test::test_patch_update ... ok
test git::canonical::rules::test::deserialization ... ok
test git::canonical::rules::test::deserialize_extensions ... ok
test git::canonical::rules::test::matches_exactly_curly_braces ... ok
test git::canonical::rules::test::matches_expands_globs_appropriately ... ok
test git::canonical::rules::test::ordering ... ok
test git::canonical::rules::test::property::identity ... ok
test git::canonical::rules::test::property::prefix ... ok
test git::canonical::rules::test::property::prefix_negative ... ok
test git::canonical::rules::test::canonical ... ok
test cob::thread::tests::prop_ordering ... ok
test git::canonical::rules::test::property::suffix ... ok
test git::canonical::rules::test::roundtrip ... ok
test git::canonical::rules::test::property::suffix_negative ... ok
test git::canonical::rules::test::rule_validate_success ... ok
test git::canonical::rules::test::special_branches ... ok
test git::canonical::symbolic::test::deserialize_infinite ... ok
test git::canonical::symbolic::test::deserialize_order ... ok
test git::canonical::symbolic::test::deserialize_valid ... ok
test git::canonical::symbolic::test::infinite_extend ... ok
test git::canonical::symbolic::test::infinite_multi ... ok
test git::canonical::symbolic::test::infinite_single ... ok
test git::canonical::rules::test::property::trailing_asterisk_partial_component ... ok
test git::canonical::symbolic::test::reclassification_combine ... ok
test git::canonical::symbolic::test::reclassification_combine_reverse ... ok
test git::canonical::symbolic::test::reclassification_diamond ... ok
test git::canonical::symbolic::test::reclassification_order_invariant ... ok
test git::canonical::symbolic::test::reclassification_reverse_chain ... ok
test git::canonical::symbolic::test::resolve_two_hop_chain ... ok
test git::canonical::symbolic::test::target_classification ... ok
test git::canonical::symbolic::test::target_classification_symbolic ... ok
test git::canonical::symbolic::test::target_reclassification ... ok
test git::canonical::symbolic::test::target_reclassification_commutative ... ok
test git::canonical::tests::test_commit_quorum_fork_of_a_fork ... ok
test git::canonical::tests::test_commit_quorum_forked_merge_commits ... ok
test git::canonical::tests::test_commit_quorum_groups ... ok
test git::canonical::tests::test_commit_quorum_linear ... ok
test git::canonical::tests::test_commit_quorum_merges ... ok
test git::canonical::tests::test_commit_quorum_single ... ok
test git::canonical::tests::test_commit_quorum_three_way_fork ... ok
test git::canonical::tests::test_commit_quorum_two_way_fork ... ok
test git::canonical::rules::test::rule_validate_failures ... ok
test git::canonical::tests::test_quorum_different_types ... ok
test git::canonical::tests::test_tag_quorum ... ok
test git::test::test_version_from_str ... ok
test git::test::test_version_ord ... ok
test identity::crefs::tests::invalid_clash ... ok
test identity::crefs::tests::invalid_clash_asterisk_name ... ok
test identity::crefs::tests::invalid_dangling ... ok
test identity::crefs::tests::omit_symbolic ... ok
test identity::crefs::tests::valid_asterisk_target ... ok
test identity::did::test::test_did_encode_decode ... ok
test identity::crefs::tests::valid ... ok
test identity::did::test::test_did_vectors ... ok
test identity::doc::test::default_branch_clash ... ok
test identity::doc::test::default_branch_without_project ... ok
test identity::doc::test::test_canonical_doc ... ok
test identity::doc::test::test_canonical_example ... ok
test identity::doc::test::test_duplicate_dids ... ok
test identity::doc::test::test_future_version_error ... ok
test identity::doc::test::test_is_valid_version ... ok
test identity::doc::test::test_max_delegates ... ok
test identity::doc::test::test_not_found ... ok
test identity::doc::test::test_parse_version ... ok
test identity::doc::test::test_visibility_json ... ok
test identity::doc::update::test::test_can_update_crefs ... ok
test identity::doc::update::test::test_cannot_include_default_branch_rule ... ok
test identity::doc::update::test::test_default_branch_rule_exists_after_verification ... ok
test identity::project::test::test_project_name ... ok
test node::address::store::test::skip_invalid_address_type ... ok
test node::address::store::test::skip_mismatched_address_type ... ok
test node::address::store::test::test_alias ... ok
test node::address::store::test::test_disconnected ... ok
test node::address::store::test::test_disconnected_ban ... ok
test node::address::store::test::test_empty ... ok
test node::address::store::test::test_entries ... ok
test node::address::store::test::test_entries_skips_unparsable_address ... ok
test node::address::store::test::test_get_none ... ok
test node::address::store::test::test_insert_and_get ... ok
test identity::doc::test::prop_encode_decode ... ok
test node::address::store::test::test_insert_and_remove ... ok
test node::address::store::test::test_insert_and_update ... ok
test node::address::store::test::test_insert_duplicate ... ok
test node::address::store::test::test_node_aliases ... ok
test node::command::test::command_result ... ok
test node::config::test::deserialize_migrating_scope ... ok
test node::config::test::fetch_level_min ... ok
test node::config::test::onion_absent ... ok
test node::address::store::test::test_remove_nothing ... ok
test node::config::test::onion_null ... ok
test node::config::test::regression_ipv6_address_brackets ... ok
test node::config::test::regression_ipv6_address_no_brackets ... ok
test node::config::test::serialize_migrating_scope ... ok
test node::config::test::user_agent_custom ... ok
test node::config::test::user_agent_default ... ok
test node::config::test::user_agent_default_explicit ... ok
test node::config::test::user_agent_opt_out ... ok
test node::db::config::test::database_config_valid_combinations ... ok
test node::db::config::test::invalid ... ok
test node::config::test::partial ... ok
test node::db::test::migration_8::all_ipv6_formatted_dns_addresses_are_retyped ... ok
test node::db::test::migration_8::dns_address_starting_with_bracket_but_missing_closing_bracket_colon_is_unaffected ... ok
test node::db::test::migration_8::ipv4_address_is_unaffected ... ok
test node::db::test::migration_8::dns_address_with_bracket_not_at_start_is_unaffected ... ok
test node::db::test::migration_8::ipv6_formatted_dns_address_is_deleted_when_correct_ipv6_row_already_exists ... ok
test node::db::test::migration_8::ipv6_formatted_dns_address_is_retyped_to_ipv6 ... ok
test node::db::test::migration_8::migration_applies_to_all_nodes ... ok
test node::db::test::migration_8::plain_dns_hostname_without_brackets_is_unaffected ... ok
test node::db::test::migration_8::retype_preserves_address_metadata ... ok
test node::db::test::migration_9::bracketed_non_ipv6_garbage_is_deleted ... ok
test node::db::test::migration_9::dns_row_is_unaffected_even_when_inner_part_has_no_colon ... ok
test node::db::test::migration_9::empty_brackets_ipv6_row_is_deleted ... ok
test git::canonical::tests::test_quorum_properties ... ok
test node::db::test::migration_9::ipv4_row_is_unaffected ... ok
test node::db::test::migration_9::full_ipv6_address_is_kept ... ok
test node::db::test::migration_9::loopback_address_is_kept ... ok
test node::features::test::test_operations ... ok
test node::notifications::store::test::test_branch_notifications ... ok
test node::db::test::migration_9::unspecified_address_is_kept ... ok
test node::notifications::store::test::test_clear ... ok
test node::notifications::store::test::test_cob_notifications ... ok
test node::notifications::store::test::test_duplicate_notifications ... ok
test node::db::test::test_version ... ok
test node::notifications::store::test::test_notification_status ... ok
test node::notifications::store::test::test_counts_by_repo ... ok
test node::policy::store::test::test_follow_and_unfollow_node ... ok
test node::policy::store::test::test_node_policies ... ok
test node::policy::store::test::test_node_aliases ... ok
test node::policy::store::test::test_repo_policies ... ok
test node::policy::store::test::test_repo_policy ... ok
test node::policy::store::test::test_node_policy ... ok
test node::policy::store::test::test_seed_and_unseed_repo ... ok
test node::policy::store::test::test_update_alias ... ok
test node::policy::store::test::test_update_scope ... ok
test node::refs::store::test::test_set_and_delete ... ok
test node::refs::store::test::test_set_and_get ... ok
test node::refs::store::test::test_count ... ok
test node::routing::test::test_count ... ok
test node::routing::test::test_entries ... ok
test node::routing::test::test_insert_and_get ... ok
test node::routing::test::test_insert_and_get_resources ... ok
test node::routing::test::test_insert_and_remove ... ok
test node::routing::test::test_insert_existing_updated_time ... ok
test node::routing::test::test_insert_duplicate ... ok
test node::routing::test::test_len ... ok
test node::routing::test::test_remove_many ... ok
test node::routing::test::test_remove_redundant ... ok
test node::sync::announce::test::all_synced_nodes_are_preferred_seeds ... ok
test node::sync::announce::test::announcer_adapts_target_to_reach ... ok
test node::sync::announce::test::announcer_preferred_seeds_or_replica_factor ... ok
test node::sync::announce::test::announcer_reached_max_replication_target ... ok
test node::routing::test::test_update_existing_multi ... ok
test node::sync::announce::test::announcer_reached_min_replication_target ... ok
test node::sync::announce::test::announcer_synced_with_unknown_node ... ok
test node::routing::test::test_prune ... ok
test node::sync::announce::test::announcer_reached_preferred_seeds ... ok
test node::sync::announce::test::announcer_timed_out ... ok
test node::sync::announce::test::announcer_with_replication_factor_zero_and_preferred_seeds ... ok
test node::sync::announce::test::construct_node_appears_in_multiple_input_sets ... ok
test node::sync::announce::test::cannot_construct_announcer ... ok
test node::sync::announce::test::construct_only_preferred_seeds_provided ... ok
test node::sync::announce::test::local_node_in_multiple_sets ... ok
test node::sync::announce::test::invariant_progress_should_match_state ... ok
test node::sync::announce::test::local_node_in_preferred_seeds ... ok
test node::sync::announce::test::local_node_in_synced_set ... ok
test node::sync::announce::test::local_node_in_unsynced_set ... ok
test node::sync::announce::test::local_node_only_in_all_sets_results_in_no_seeds_error ... ok
test node::sync::announce::test::synced_with_local_node_is_ignored ... ok
test node::sync::announce::test::preferred_seeds_already_synced ... ok
test node::sync::announce::test::synced_with_same_node_multiple_times ... ok
test node::sync::announce::test::timed_out_after_reaching_success ... ok
test node::sync::fetch::test::all_nodes_are_candidates ... ok
test node::sync::fetch::test::could_not_reach_target ... ok
test node::sync::fetch::test::all_nodes_are_fetchable ... ok
test node::sync::fetch::test::ignores_duplicates_and_local_node ... ok
test node::sync::fetch::test::preferred_seeds_target_returned_over_replicas ... ok
test node::sync::fetch::test::reaches_target_of_max_replicas ... ok
test node::sync::fetch::test::reaches_target_of_preferred_seeds ... ok
test node::sync::fetch::test::reaches_target_of_replicas ... ok
test node::sync::test::ensure_replicas_construction ... ok
test node::sync::test::replicas_constrain_to ... ok
test node::test::test_address ... ok
test node::test::test_alias ... ok
test node::test::test_user_agent ... ok
test node::timestamp::tests::test_timestamp_max ... ok
test node::test::test_command_result ... ok
test profile::test::test_config ... ok
test profile::test::canonicalize_home ... ok
test rad::tests::test_checkout ... ok
test rad::tests::test_fork ... ok
test rad::tests::test_init ... ok
test storage::git::tests::test_references_of ... ok
test storage::git::transport::local::url::test::test_url_parse ... ok
test storage::git::transport::local::url::test::test_url_to_string ... ok
test storage::git::transport::remote::url::test::test_url_parse ... ok
test storage::git::tests::test_sign_refs ... ok
test profile::config::test::schema ... ok
test storage::refs::sigrefs::git::properties::idempotent_write ... ok
test storage::refs::sigrefs::git::properties::initial_commit_roundtrip ... ok
test cob::identity::test::property::prop_invariants ... ok
test storage::refs::sigrefs::read::test::commit_reader::identity_root_error ... ok
test storage::refs::sigrefs::read::test::commit_reader::missing_commit ... ok
test storage::refs::sigrefs::read::test::commit_reader::read_ok ... ok
test storage::refs::sigrefs::read::test::commit_reader::too_many_parents ... ok
test storage::refs::sigrefs::read::test::commit_reader::tree_error ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::doc_blob_error ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::missing_identity ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::read_ok_none ... ok
test storage::refs::sigrefs::read::test::identity_root_reader::read_ok_some ... ok
test storage::refs::sigrefs::read::test::resolve_tip::find_reference_error ... ok
test storage::refs::sigrefs::read::test::resolve_tip::missing_sigrefs ... ok
test storage::refs::sigrefs::read::test::resolve_tip::resolve_tip_ok ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::detect_parent::root_without_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::detect_parent::root_without_root ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::restore ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::root ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::downgrade::root_with_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::head_commit_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::head_verify_mismatched_identity_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::head_verify_signature_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::invalid_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::read_ok_no_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::read_ok_parent ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::read_ok_root ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::alternating ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::chain ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::multiple ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::replay::root_at_head ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::single_commit ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::two_commits ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::walk_commit_error ... ok
test storage::refs::sigrefs::read::test::signed_refs_reader::walk_verify_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::missing_both ... ok
test storage::refs::sigrefs::read::test::tree_reader::missing_refs ... ok
test storage::refs::sigrefs::read::test::tree_reader::missing_signature ... ok
test storage::refs::sigrefs::read::test::tree_reader::parse_refs_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::parse_signature_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::read_ok ... ok
test storage::refs::sigrefs::read::test::tree_reader::read_refs_error ... ok
test storage::refs::sigrefs::read::test::tree_reader::read_signature_error ... ok
test storage::refs::sigrefs::write::test::commit_writer::tree_error ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_commit_error ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_empty_refs ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_root_ok ... ok
test storage::refs::sigrefs::write::test::commit_writer::write_with_parent_ok ... ok
test storage::refs::sigrefs::write::test::head_reader::no_head ... ok
test storage::refs::sigrefs::write::test::head_reader::read_ok ... ok
test storage::refs::sigrefs::write::test::head_reader::reference_error ... ok
test storage::refs::sigrefs::write::test::head_reader::refs_blob_error ... ok
test storage::refs::sigrefs::write::test::head_reader::refs_blob_missing ... ok
test storage::refs::sigrefs::write::test::head_reader::refs_parse_error ... ok
test storage::refs::sigrefs::write::test::head_reader::signature_blob_error ... ok
test storage::refs::sigrefs::write::test::head_reader::signature_blob_missing ... ok
test storage::refs::sigrefs::write::test::head_reader::signature_parse_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::commit_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::head_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::never_write_rad_sigrefs ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::reference_error ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::unchanged ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::unchanged_force_writes_new_commit ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::write_empty_refs ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::write_root_ok ... ok
test storage::refs::sigrefs::write::test::signed_refs_writer::write_with_parent_ok ... ok
test storage::refs::sigrefs::write::test::tree_writer::sign_error ... ok
test storage::refs::sigrefs::write::test::tree_writer::write_ok ... ok
test storage::refs::sigrefs::write::test::tree_writer::write_tree_error ... ok
test storage::refs::tests::prop_canonical_roundtrip ... ok
test storage::refs::sigrefs::property::idempotent ... ok
test storage::tests::test_storage ... ok
test test::assert::test::assert_with_message ... ok
test test::assert::test::test_assert_no_move ... ok
test test::assert::test::test_assert_panic_0 - should panic ... ok
test test::assert::test::test_assert_panic_1 - should panic ... ok
test test::assert::test::test_assert_panic_2 - should panic ... ok
test test::assert::test::test_assert_succeed ... ok
test test::assert::test::test_panic_message ... ok
test version::test::test_version ... ok
test web::test::description_only ... ok
test web::test::pinned_empty ... ok
test storage::refs::tests::test_rid_verification ... ok
test storage::refs::sigrefs::git::properties::chain_roundtrip ... ok
test storage::refs::sigrefs::property::roundtrip ... ok
test result: ok. 400 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.32s
Running unittests src/lib.rs (target/debug/deps/radicle_cli-c7b2ca137c595b1f)
running 46 tests
test commands::block::args::test::should_parse_nid ... ok
test commands::block::args::test::should_parse_rid ... ok
test commands::block::args::test::should_not_parse ... ok
test commands::clone::args::test::should_parse_rid_non_urn ... ok
test commands::clone::args::test::should_parse_rid_urn ... ok
test commands::cob::args::test::should_allow_log_json_format ... ok
test commands::clone::args::test::should_parse_rid_url ... ok
test commands::cob::args::test::should_allow_show_json_format ... ok
test commands::cob::args::test::should_not_allow_show_pretty_format ... ok
test commands::cob::args::test::should_allow_log_pretty_format ... ok
test commands::fork::args::test::should_not_parse_rid_url ... ok
test commands::cob::args::test::should_allow_update_json_format ... ok
test commands::fork::args::test::should_parse_rid_non_urn ... ok
test commands::cob::args::test::should_not_allow_update_pretty_format ... ok
test commands::fork::args::test::should_parse_rid_urn ... ok
test commands::id::args::test::should_not_clobber_payload_args ... ok
test commands::id::args::test::should_not_parse_single_payloads ... ok
test commands::id::args::test::should_not_parse_into_payload - should panic ... ok
test commands::id::args::test::should_parse_into_payload ... ok
test commands::id::args::test::should_parse_multiple_payloads ... ok
test commands::id::args::test::should_not_parse_single_payload ... ok
test commands::init::args::test::should_parse_rid_non_urn ... ok
test commands::id::args::test::should_parse_single_payload ... ok
test commands::init::args::test::should_parse_rid_urn ... ok
test commands::init::args::test::should_not_parse_rid_url ... ok
test commands::patch::review::builder::tests::test_review_comments_before ... ok
test commands::inspect::test::test_tree ... ok
test commands::patch::review::builder::tests::test_review_comments_basic ... ok
test commands::patch::review::builder::tests::test_review_comments_multiline ... ok
test commands::patch::review::builder::tests::test_review_comments_split_hunk ... ok
test commands::publish::args::test::should_not_parse_rid_url ... ok
test git::ddiff::tests::diff_encode_decode_ddiff_hunk ... ok
test git::pretty_diff::test::test_pretty ... ignored
test git::unified_diff::test::test_diff_content_encode_decode_content ... ok
test commands::publish::args::test::should_parse_rid_urn ... ok
test commands::publish::args::test::should_parse_rid_non_urn ... ok
test terminal::args::test::should_parse_nid ... ok
test terminal::args::test::should_not_parse ... ok
test git::unified_diff::test::test_diff_encode_decode_diff ... ok
test commands::watch::args::test::should_parse_ref_str ... ok
test terminal::format::test::test_bytes ... ok
test terminal::format::test::test_strip_comments ... ok
test terminal::patch::test::test_edit_display_message ... ok
test terminal::args::test::should_parse_rid ... ok
test terminal::patch::test::test_create_display_message ... ok
test terminal::patch::test::test_update_display_message ... ok
test result: ok. 45 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/main.rs (target/debug/deps/rad-6f5aef478c8ff379)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/commands.rs (target/debug/deps/commands-11a7c49401933fe5)
running 126 tests
test commands::checkout::rad_checkout ... ok
test commands::clone::rad_clone_bare ... ok
test commands::clone::rad_clone ... ok
test commands::clone::rad_clone_all ... ok
test commands::clone::rad_clone_scope ... ok
test commands::clone::rad_clone_directory ... ok
test commands::clone::rad_clone_connect ... ok
test commands::clone::rad_clone_unknown ... ok
test commands::clone::rad_clone_partial_fail ... ok
test commands::cob::rad_cob_multiset ... ok
test commands::clone::test_clone_without_seeds ... ok
test commands::cob::rad_cob_log ... ok
test commands::cob::rad_cob_migrate ... ok
test commands::cob::rad_cob_operations ... ok
test commands::cob::rad_cob_show ... ok
test commands::cob::rad_cob_update_identity ... ok
test commands::cob::rad_cob_update ... ok
test commands::cob::test_cob_deletion ... ok
test commands::cob::test_cob_replication ... ok
test commands::git::git_push_amend ... ok
test commands::git::git_push_and_fetch ... ok
test commands::git::git_push_canonical_lightweight_tags ... ok
test commands::git::git_push_diverge ... ok
test commands::git::git_push_force_with_lease ... ok
test commands::git::git_push_canonical ... ok
test commands::git::git_push_converge ... ok
test commands::id::rad_id_collaboration ... ignored, slow
test commands::id::rad_id ... ok
test commands::git::git_push_rollback ... ok
test commands::git::git_tag ... ok
test commands::id::rad_id_private ... ok
test commands::id::rad_id_conflict ... ok
test commands::id::rad_id_threshold_soft_fork ... ok
test commands::id::rad_id_unknown_field ... ok
test commands::id::rad_id_threshold ... ok
test commands::id::rad_id_update_delete_field ... ok
test commands::init::rad_init ... ignored, part of many other tests
test commands::id::rad_id_unauthorized_delegate ... ok
test commands::init::rad_init_bare ... ok
test commands::id::rad_id_multi_delegate ... ok
test commands::init::rad_init_detached_head ... ok
test commands::init::rad_init_existing ... ok
test commands::init::rad_init_existing_bare ... ok
test commands::init::rad_init_no_git ... ok
test commands::init::rad_init_no_seed ... ok
test commands::init::rad_init_private ... ok
test commands::init::rad_init_private_no_seed ... ok
test commands::init::rad_init_private_clone ... ok
test commands::inbox::rad_inbox ... ok
test commands::init::rad_init_private_clone_seed ... ok
test commands::init::rad_init_private_seed ... ok
test commands::init::rad_init_sync_not_connected ... ok
test commands::init::rad_init_sync_preferred ... ok
test commands::init::rad_init_with_existing_remote ... ok
test commands::init::rad_publish ... ok
test commands::issue::rad_issue ... ok
test commands::jj::rad_jj_bare ... ignored, the bare repository does not have a `rad` remote, and so it cannot determine the RID of the repository
test commands::jj::rad_jj_colocated_patch ... ok
test commands::issue::rad_issue_list ... ok
test commands::node::rad_node_connect ... ok
test commands::node::rad_node_connect_without_address ... ok
test commands::patch::rad_merge_after_update ... ok
test commands::patch::rad_merge_no_ff ... ok
test commands::node::rad_node ... ok
test commands::patch::rad_merge_via_push ... ok
test commands::patch::rad_patch_ahead_behind ... ok
test commands::patch::rad_patch ... ok
test commands::patch::rad_patch_change_base ... ok
test commands::patch::rad_patch_checkout ... ok
test commands::patch::rad_patch_checkout_revision ... ok
test commands::patch::rad_patch_checkout_force ... ok
test commands::init::rad_init_sync_and_clone ... ok
test commands::init::rad_init_sync_timeout ... ok
test commands::patch::rad_patch_detached_head ... ok
test commands::patch::rad_patch_diff ... ok
test commands::patch::rad_patch_draft ... ok
test commands::patch::rad_patch_edit ... ok
test commands::patch::rad_patch_fetch_2 ... ok
test commands::patch::rad_patch_merge_draft ... ok
test commands::patch::rad_patch_fetch_1 ... ok
test commands::patch::rad_patch_magic_push ... ok
test commands::patch::rad_patch_delete ... ok
test commands::patch::rad_patch_merge_into_canonical_ref_branch ... ok
test commands::patch::rad_patch_merge_wrong_branch ... ok
test commands::patch::rad_patch_merge_unauthorized_branch ... ok
test commands::patch::rad_patch_merge_on_first_push ... ok
test commands::patch::rad_patch_open_explore ... ok
test commands::patch::rad_patch_revert_merge ... ok
test commands::patch::rad_patch_revert_custom_branch ... ok
test commands::patch::rad_patch_update ... ok
test commands::patch::rad_patch_review_no_options ... ok
test commands::patch::rad_patch_via_push ... ok
test commands::policy::rad_block ... ok
test commands::patch::rad_review_by_hunk ... ok
test commands::policy::rad_seed_and_follow ... ok
test commands::policy::rad_seed_policy_allow_no_scope ... ok
test commands::policy::rad_seed_scope ... ok
test commands::policy::rad_unseed ... ok
test commands::policy::rad_seed_many ... ok
test commands::policy::rad_unseed_many ... ok
test commands::sigpipe::config ... ok
test commands::sigpipe::help ... ok
test commands::sigpipe::rad_self ... ok
test commands::patch::rad_push_and_pull_patches ... ok
test commands::remote::rad_remote ... ok
test commands::sync::rad_sync_without_node ... ok
test commands::sync::rad_sync ... ok
test commands::utility::framework_home ... ok
test commands::utility::rad_auth ... ok
test commands::utility::rad_auth_errors ... ok
test commands::patch::rad_patch_pull_update ... ok
test commands::utility::rad_clean ... ok
test commands::utility::rad_config ... ok
test commands::utility::rad_diff ... ok
test commands::utility::rad_help ... ok
test commands::utility::rad_inspect ... ok
test commands::utility::rad_key_mismatch ... ok
test commands::utility::rad_self ... ok
test commands::utility::rad_warn_ipv6 ... ok
test commands::utility::rad_warn_old_nodes ... ok
test commands::watch::rad_watch ... ok
test commands::sync::rad_fetch ... ok
test rad_remote ... ok
test commands::workflow::rad_workflow ... ok
test commands::sync::test_replication_via_seed ... ok
test commands::utility::rad_fork ... ok
test result: ok. 123 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 73.32s
Running unittests src/lib.rs (target/debug/deps/radicle_cli_test-43cf8cd336c16765)
running 3 tests
test tests::test_parse ... ok
test tests::test_run ... ok
test tests::test_example_spaced_brackets ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/lib.rs (target/debug/deps/radicle_cob-945435690bdd3f69)
running 9 tests
test object::tests::test_serde ... ok
test tests::git::roundtrip ... ok
test tests::git::list_cobs ... ok
test tests::git::update_cob ... ok
test tests::git::traverse_cobs ... ok
test type_name::test::valid_typenames ... ok
test tests::invalid_parse_refstr ... ok
test tests::parse_refstr ... ok
test type_name::test::invalid_typenames ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/lib.rs (target/debug/deps/radicle_core-fc4f4fdb88cdc03f)
running 4 tests
test repo::test::invalid ... ok
test repo::test::valid ... ok
test repo::test::assert_prop_roundtrip_parse ... ok
test repo::serde_impls::test::assert_prop_roundtrip_serde_json ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running unittests src/lib.rs (target/debug/deps/radicle_crypto-e9fcade94bee2e01)
running 12 tests
test ssh::agent::test::test_agent_encoding_remove ... ok
test ssh::fmt::test::test_key ... ok
test ssh::fmt::test::test_fingerprint ... ok
test ssh::agent::test::test_agent_encoding_sign ... ok
test tests::did_key_sample ... ok
test ssh::keystore::tests::test_init_no_passphrase ... ok
test tests::diffie_hellman_fixture ... ok
test tests::prop_encode_decode ... ok
test tests::diffie_hellman ... ok
test tests::prop_key_equality ... ok
test ssh::keystore::tests::test_signer ... ok
test ssh::keystore::tests::test_init_passphrase ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.80s
Running unittests src/lib.rs (target/debug/deps/radicle_dag-cba03ea2f40c887a)
running 20 tests
test tests::test_contains ... ok
test tests::test_cycle ... ok
test tests::test_diamond ... ok
test tests::test_dependencies ... ok
test tests::test_complex ... ok
test tests::test_fold_diamond ... ok
test tests::test_fold_multiple_roots ... ok
test tests::test_fold_sorting_1 ... ok
test tests::test_fold_reject ... ok
test tests::test_is_empty ... ok
test tests::test_len ... ok
test tests::test_merge_1 ... ok
test tests::test_fold_sorting_2 ... ok
test tests::test_get ... ok
test tests::test_prune_1 ... ok
test tests::test_merge_2 ... ok
test tests::test_prune_2 ... ok
test tests::test_prune_by_sorting ... ok
test tests::test_remove ... ok
test tests::test_siblings ... ok
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_fetch-136fd7e52b4edc38)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_git_metadata-9aa751837329bb45)
running 24 tests
test commit::parse::test::error::invalid_author ... ok
test commit::parse::test::error::invalid_committer ... ok
test commit::parse::test::error::invalid_format_continuation_without_preceding_header ... ok
test commit::parse::test::error::invalid_tree ... ok
test commit::parse::test::error::invalid_parent ... ok
test commit::parse::test::error::missing_author ... ok
test commit::parse::test::error::missing_committer ... ok
test commit::parse::test::error::missing_tree_empty_header ... ok
test commit::parse::test::error::missing_tree_wrong_first_line ... ok
test commit::parse::test::error::missing_header_body_separator ... ok
test commit::parse::test::success::commit_gpgsig_is_preserved_and_strip_removes_it ... ok
test commit::parse::test::success::commit_last_paragraph_kept_in_message_when_not_all_trailers ... ok
test commit::parse::test::success::commit_with_extra_headers ... ok
test commit::parse::test::success::commit_with_multiline_gpgsig ... ok
test commit::parse::test::success::commit_with_single_parent ... ok
test commit::parse::test::success::commit_with_trailers ... ok
test commit::parse::test::success::merge_commit ... ok
test commit::parse::test::success::root_commit ... ok
test commit::parse::test::success::roundtrip ... ok
test commit::parse::test::unit::body_last_paragraph_not_trailers_stays_in_message ... ok
test commit::parse::test::unit::body_no_paragraph_separator_means_no_trailers ... ok
test commit::parse::test::unit::trailers_accepts_empty_input ... ok
test commit::parse::test::unit::trailers_rejects_invalid_token_chars ... ok
test commit::parse::test::unit::trailers_rejects_line_without_separator ... ok
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_git_ref_format-53cacdde7021555b)
running 9 tests
test test::component ... ok
test test::pattern ... ok
test test::component_invalid - should panic ... ok
test test::qualified ... ok
test test::qualified_invalid - should panic ... ok
test test::qualified_pattern ... ok
test test::qualified_pattern_invalid - should panic ... ok
test test::refname ... ok
test test::refname_invalid - should panic ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_localtime-834cec7348dd66f7)
running 1 test
test serde_impls::test::test_localtime ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_log-27383ba511356d2a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_node-ada1870f5f3dafe6)
running 84 tests
test reactor::timer::tests::test_next ... ok
test reactor::timer::tests::test_wake ... ok
test reactor::timer::tests::test_wake_exact ... ok
test control::tests::test_control_socket ... ok
test control::tests::test_seed_unseed ... ok
test fingerprint::tests::matching ... ok
test tests::announcement_rebroadcast ... ok
test tests::announcement_rebroadcast_timestamp_filtered ... ok
test tests::announcement_rebroadcast_duplicates ... ok
test tests::connection_kept_alive ... ok
test tests::announcement_relay ... ok
test tests::disconnecting_unresponsive_peer ... ok
test tests::e2e::fetch_does_not_contain_rad_sigrefs_parent ... ok
test tests::e2e::missing_default_branch ... ok
test tests::e2e::missing_delegate_default_branch ... ok
test tests::e2e::test_block_active_connection ... ok
test tests::e2e::test_block_prevents_connection ... ok
test tests::e2e::test_background_foreground_fetch ... ok
test tests::e2e::test_block_prevents_fetch ... ok
test tests::e2e::test_catchup_on_refs_announcements ... ok
test tests::e2e::test_channel_reader_limit ... ok
test tests::e2e::test_clone ... ok
test tests::e2e::test_connection_crossing ... ok
test tests::e2e::test_dont_fetch_owned_refs ... ok
test tests::e2e::test_fetch_emits_canonical_ref_update_partial_glob ... ok
test tests::e2e::test_fetch_followed_remotes ... ok
test tests::e2e::test_concurrent_fetches ... ok
test tests::announcement_message_amplification ... ok
test tests::e2e::test_fetch_preserve_owned_refs ... ok
test tests::e2e::test_fetch_unseeded ... ok
test tests::e2e::test_inventory_sync_basic ... ok
test tests::e2e::test_fetch_up_to_date ... ok
test tests::e2e::test_fetch_emits_canonical_ref_update ... ok
test tests::e2e::test_large_fetch ... ok
test tests::e2e::test_migrated_clone ... ok
test tests::e2e::test_missing_remote ... ok
test tests::e2e::test_multiple_offline_inits ... ok
test tests::e2e::test_non_fast_forward_identity_doc ... ok
test tests::e2e::test_non_fast_forward_sigrefs ... ok
test tests::e2e::test_outdated_delegate_sigrefs ... ok
test tests::e2e::test_outdated_sigrefs ... ok
test tests::e2e::test_replication ... ok
test tests::e2e::test_replication_invalid ... ok
test tests::e2e::test_inventory_sync_bridge ... ok
test tests::e2e::test_replication_ref_in_sigrefs ... ok
test tests::fetch_missing_inventory_on_gossip ... ok
test tests::fetch_missing_inventory_on_schedule ... ok
test tests::inbound_connection ... ok
test tests::inventory_decode ... ok
test tests::init_and_seed ... ok
test tests::e2e::test_inventory_sync_ring ... ok
test tests::inventory_relay ... ok
test tests::e2e::test_inventory_sync_star ... ok
test tests::inventory_relay_bad_timestamp ... ok
test tests::inventory_sync ... ok
test tests::maintain_connections_failed_attempt ... ok
test tests::maintain_connections_same_second_loop ... ok
test tests::maintain_connections_transient ... ok
test tests::orphaned_fetch_blocks_repo_from_all_nodes ... ok
test tests::maintain_connections ... ok
test tests::outbound_connection ... ok
test tests::persistent_peer_connect ... ok
test tests::persistent_peer_reconnect_attempt ... ok
test tests::persistent_peer_reconnect_success ... ok
test tests::ping_response ... ok
test tests::queued_fetch_from_ann_same_rid ... ok
test tests::queued_fetch_from_command_same_rid ... ok
test tests::queued_fetch_max_capacity ... ok
test tests::redundant_connect ... ok
test tests::refs_announcement_fetch_trusted_no_inventory ... ok
test tests::refs_announcement_followed ... ok
test tests::inventory_pruning ... ok
test tests::refs_announcement_no_subscribe ... ok
test tests::refs_announcement_offline ... ok
test tests::refs_announcement_relay_private ... ok
test tests::refs_synced_event ... ok
test tests::refs_announcement_relay_public ... ok
test wire::test::test_inventory_ann_with_extension ... ok
test wire::test::test_pong_message_with_extension ... ok
test tests::seeding ... ok
test tests::seed_repo_subscribe ... ok
test wire::test::worker_result_clears_active_when_peer_connected ... ok
test wire::test::worker_result_clears_active_when_peer_disconnecting ... ok
test tests::prop_inventory_exchange_dense ... ok
test result: ok. 84 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.13s
Running unittests src/main.rs (target/debug/deps/radicle_node-2598235cb18e06fc)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_oid-b31c81253d824b1f)
running 10 tests
test fmt::test::zero ... ok
test fmt::test::gix ... ok
test fmt::test::fixture ... ok
test fmt::test::git2 ... ok
test git2::test::zero ... ok
test gix::test::zero ... ok
test str::test::fixture ... ok
test str::test::zero ... ok
test str::test::gix_roundtrip ... ok
test str::test::git2_roundtrip ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_protocol-81a53ba1cc026eec)
running 100 tests
test deserializer::test::test_decode_next ... ok
test deserializer::test::test_unparsed ... ok
test deserializer::test::prop_decode_next ... ok
test fetcher::service::tests::test_fetch_coalescing_different_refs ... ok
test fetcher::test::queue::properties::capacity::capacity_reached_returns_same_item ... ok
test fetcher::test::queue::properties::dequeue::drained_queue_returns_none ... ok
test fetcher::test::queue::properties::dequeue::empty_queue_returns_none ... ok
test fetcher::test::queue::properties::dequeue::enables_reenqueue ... ok
test fetcher::test::queue::properties::capacity::bounded ... ok
test fetcher::test::queue::properties::equality::reflexive ... ok
test fetcher::test::queue::properties::equality::symmetric ... ok
test fetcher::test::queue::properties::capacity::rejection ... ok
test fetcher::test::queue::properties::fifo::interleaved_operations ... ok
test fetcher::test::queue::properties::fifo::ordering ... ok
test fetcher::test::queue::properties::merge::different_rid_accepted ... ok
test fetcher::test::queue::properties::merge::combines_refs ... ok
test fetcher::test::queue::properties::equality::transitive ... ok
test fetcher::test::queue::properties::merge::longer_timeout_preserved ... ok
test fetcher::test::queue::properties::merge::empty_refs_fetches_all ... ok
test fetcher::test::queue::properties::merge::does_not_increase_queue_length ... ok
test fetcher::test::queue::unit::capacity_takes_precedence_over_merge_for_new_items ... ok
test fetcher::test::queue::unit::empty_refs_items_can_be_equal ... ok
test fetcher::test::queue::unit::max_timeout_accepted ... ok
test fetcher::test::queue::unit::merge_preserves_position_in_queue ... ok
test fetcher::test::queue::unit::zero_timeout_accepted ... ok
test fetcher::test::state::command::cancel::cancellation_is_isolated ... ok
test fetcher::test::state::command::cancel::non_existent_returns_unexpected ... ok
test fetcher::test::state::command::cancel::ongoing_and_queued ... ok
test fetcher::test::state::command::cancel::single_ongoing ... ok
test fetcher::test::state::command::fetch::fetch_after_previous_completed ... ok
test fetcher::test::state::command::fetch::fetch_at_capacity_enqueues ... ok
test fetcher::test::state::command::fetch::fetch_different_repo_same_node_within_capacity ... ok
test fetcher::test::state::command::fetch::fetch_duplicate_returns_already_fetching ... ok
test fetcher::test::state::command::fetch::fetch_queue_merge_empty_refs_fetches_all ... ok
test fetcher::test::state::command::fetch::fetch_queue_merge_takes_longer_timeout ... ok
test fetcher::test::state::command::fetch::fetch_queue_merges_already_queued ... ok
test fetcher::test::state::command::fetch::fetch_queue_rejected_capacity_reached ... ok
test fetcher::test::state::command::fetch::fetch_same_repo_different_nodes_queues_second ... ok
test fetcher::test::state::command::fetch::fetch_same_repo_different_refs_enqueues ... ok
test fetcher::test::state::command::fetch::fetch_start_first_fetch_for_node ... ok
test fetcher::test::state::command::fetched::complete_one_of_multiple ... ok
test fetcher::test::state::command::fetched::complete_single_ongoing ... ok
test fetcher::test::queue::properties::merge::succeed_when_at_capacity ... ok
test fetcher::test::state::command::fetched::non_existent_returns_not_found ... ok
test fetcher::test::state::command::fetched::complete_then_dequeue_fifo ... ok
test fetcher::test::state::command::fetched::stale_from_does_not_clear_active ... ok
test fetcher::test::state::concurrent::fetched_then_cancel ... ok
test fetcher::test::state::concurrent::interleaved_operations ... ok
test fetcher::test::state::config::min_queue_size ... ok
test fetcher::test::state::dequeue::cannot_dequeue_while_node_at_capacity ... ok
test fetcher::test::state::dequeue::empty_queue_returns_none ... ok
test fetcher::test::state::dequeue::maintains_fifo_order ... ok
test fetcher::test::state::invariant::queue_integrity_after_merge ... ok
test fetcher::test::state::config::high_concurrency ... ok
test fetcher::test::state::multinode::independent_queues ... ok
test service::filter::test::compatible ... ok
test service::filter::test::test_parameters ... ok
test fetcher::test::state::multinode::high_count ... ok
test service::gossip::store::test::test_announced ... ok
test service::limiter::test::test_limiter_different_rates ... ok
test service::limiter::test::test_limiter_multi ... ok
test service::limiter::test::test_limiter_refill ... ok
test service::filter::test::test_sizes ... ok
test fetcher::test::queue::properties::capacity::restored_after_dequeue ... ok
test service::message::tests::test_inventory_limit ... ok
test service::message::tests::prop_refs_announcement_signing ... ok
test wire::frame::test::test_encode_git_large ... ok
test wire::frame::test::test_stream_id ... ok
test service::message::tests::test_ref_remote_limit ... ok
test wire::message::tests::prop_roundtrip_address ... ok
test fetcher::test::queue::properties::merge::same_rid_merges_anywhere_in_queue ... ok
test wire::message::tests::prop_roundtrip_message ... ok
test wire::message::tests::prop_zero_bytes_encode_decode ... ok
test wire::message::tests::test_node_ann_max_size ... ok
test wire::message::tests::test_ping_encode_size_overflow - should panic ... ok
test wire::message::tests::test_pingpong_encode_max_size ... ok
test wire::message::tests::test_pong_encode_size_overflow - should panic ... ok
test wire::message::tests::test_inv_ann_max_size ... ok
test wire::tests::prop_oid ... ok
test wire::tests::prop_roundtrip_filter ... ok
test wire::tests::prop_roundtrip_publickey ... ok
test wire::tests::prop_roundtrip_refs ... ok
test wire::tests::prop_roundtrip_repoid ... ok
test wire::tests::prop_roundtrip_tuple ... ok
test wire::tests::prop_roundtrip_u16 ... ok
test wire::tests::prop_roundtrip_u32 ... ok
test wire::tests::prop_roundtrip_u64 ... ok
test wire::tests::prop_roundtrip_vec ... ok
test wire::tests::prop_signature ... ok
test wire::tests::prop_string ... ok
test wire::tests::test_alias ... ok
test wire::tests::test_bounded_vec_limit ... ok
test wire::tests::test_filter_invalid ... ok
test wire::tests::test_string ... ok
test wire::varint::test::prop_roundtrip_varint ... ok
test wire::varint::test::test_encode_overflow - should panic ... ok
test wire::varint::test::test_encoding ... ok
test wire::message::tests::test_refs_ann_max_size ... ok
test service::message::tests::test_node_announcement_validate ... ok
test wire::message::tests::prop_message_decoder ... ok
test result: ok. 100 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.36s
Running unittests src/main.rs (target/debug/deps/git_remote_rad-c6408418f3fe90cf)
running 12 tests
test protocol::tests::test_empty ... ok
test protocol::tests::test_fetch ... ok
test protocol::tests::test_fetch_whitespace ... ok
test protocol::tests::test_capabilities ... ok
test protocol::tests::test_invalid ... ok
test protocol::tests::test_list ... ok
test protocol::tests::test_list_for_push ... ok
test protocol::tests::test_option ... ok
test protocol::tests::test_option_whitespace_preservation ... ok
test protocol::tests::test_push_force ... ok
test protocol::tests::test_push ... ok
test protocol::tests::test_push_delete ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/radicle_schemars-111d634e518315c8)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_signals-eaafc2a972879cc9)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_surf-2322e8eb18a712d2)
running 41 tests
test test::code_browsing::test_commit_history ... ok
test test::code_browsing::test_commit_signature ... ok
test test::code_browsing::iterate_root_dir_recursive ... ok
test test::diff::test_both_missing_eof_newline ... ok
test test::code_browsing::browse_repo_lazily ... ok
test test::diff::test_diff ... ok
test test::code_browsing::test_file_history ... ok
test test::diff::test_diff_file ... ok
test test::diff::test_diff_of_rev ... ok
test test::diff::test_new_missing_eof_newline ... ok
test test::diff::test_none_missing_eof_newline ... ok
test test::diff::test_old_missing_eof_newline ... ok
test test::file_system::directory::directory_find_entry ... ok
test test::diff::test_initial_diff ... ok
test test::file_system::directory::directory_find_file_and_directory ... ok
test test::file_system::directory::directory_last_commit ... ok
test test::diff::test_branch_diff ... ok
test test::file_system::directory::directory_size ... ok
test test::last_commit::can_get_last_commit_for_special_filenames ... ok
test test::file_system::directory::file_last_commit ... ok
test test::last_commit::binary_file ... ok
test test::last_commit::folder_svelte ... ok
test test::last_commit::readme_missing_and_memory ... ok
test test::last_commit::nest_directory ... ok
test test::last_commit::root ... ok
test test::namespace::silver_namespace ... ok
test test::namespace::me_namespace ... ok
test test::namespace::golden_namespace ... ok
test test::namespace::switch_to_banana ... ok
test test::reference::test_branches ... ok
test test::reference::test_tag_snapshot ... ok
test test::rev::_master ... ok
test test::rev::commit_parents ... ok
test test::rev::commit ... ok
test test::rev::tag ... ok
test test::rev::commit_short ... ok
test test::submodule::test_submodule_bare ... ignored, segfault
test test::reference::test_namespaces ... ok
test test::threading::basic_test ... ok
test test::file_system::directory::directory_with_bracket_in_name ... ok
test test::submodule::test_submodule ... ok
test result: ok. 40 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 4.64s
Running unittests src/lib.rs (target/debug/deps/radicle_systemd-e493431dcbdbcb38)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_term-900a6989d3dc6fd5)
running 22 tests
test ansi::tests::colors_disabled ... ok
test ansi::tests::colors_enabled ... ok
test ansi::tests::no_color_disables_colors_not_styles ... ok
test element::test::test_spaced ... ok
test ansi::tests::wrapping ... ok
test element::test::test_width ... ok
test element::test::test_truncate ... ok
test table::test::test_table ... ok
test table::test::test_table_border ... ok
test table::test::test_table_border_maximized ... ok
test table::test::test_table_truncate ... ok
test cell::test::test_width ... ok
test table::test::test_table_border_truncated ... ok
test table::test::test_table_unicode ... ok
test table::test::test_truncate ... ok
test table::test::test_table_unicode_truncate ... ok
test textarea::test::test_wrapping ... ok
test textarea::test::test_wrapping_code_block ... ok
test vstack::test::test_vstack ... ok
test vstack::test::test_vstack_maximize ... ok
test textarea::test::test_wrapping_fenced_block ... ok
test textarea::test::test_wrapping_paragraphs ... ok
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/radicle_windows-73a4d59102760b64)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle
running 1 test
test crates/radicle/src/cob/patch/encoding/review.rs - cob::patch::encoding::review::Review (line 23) ... ignored
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_cli
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_cli_test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_cob
running 1 test
test crates/radicle-cob/src/backend/stable.rs - backend::stable::with_advanced_timestamp (line 56) ... ignored
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.08s; merged doctests compilation took 0.08s
Doc-tests radicle_core
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_crypto
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_dag
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_fetch
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_git_metadata
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_git_ref_format
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_localtime
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_log
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_node
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_oid
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_protocol
running 6 tests
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::push (line 122) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::truncate (line 50) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::collect_from (line 30) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::max (line 96) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::unbound (line 149) ... ok
test crates/radicle-protocol/src/bounded.rs - bounded::BoundedVec<T,N>::with_capacity (line 66) ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.37s; merged doctests compilation took 0.36s
Doc-tests radicle_signals
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_surf
running 4 tests
test crates/radicle-surf/src/namespace.rs - namespace::Namespace::to_namespaced_pattern (line 75) - compile ... FAILED
test crates/radicle-surf/src/namespace.rs - namespace::Namespace::to_namespaced (line 48) - compile ... FAILED
test crates/radicle-surf/src/glob.rs - glob::Glob<Remote>::branches (line 249) - compile ... FAILED
test crates/radicle-surf/src/glob.rs - glob::Glob<Local>::branches (line 175) - compile ... FAILED
failures:
---- crates/radicle-surf/src/namespace.rs - namespace::Namespace::to_namespaced_pattern (line 75) stdout ----
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/namespace.rs:77:29
|
77 | let name = ns.to_namespaced(pattern!("refs/heads/*").to_qualified().unwrap());
| ^^^^^^^
error[E0425]: cannot find type `Namespace` in this scope
--> crates/radicle-surf/src/namespace.rs:76:29
|
76 | let ns = "surf/git".parse::<Namespace>();
| ^^^^^^^^^ not found in this scope
|
help: you might be missing a type parameter
|
75 | fn main() { #[allow(non_snake_case)] fn _doctest_main_crates_radicle_surf_src_namespace_rs_75_0<Namespace>() {
| +++++++++++
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0425`.
Couldn't compile the test.
---- crates/radicle-surf/src/namespace.rs - namespace::Namespace::to_namespaced (line 48) stdout ----
error: cannot find macro `qualified` in this scope
--> crates/radicle-surf/src/namespace.rs:50:29
|
50 | let name = ns.to_namespaced(qualified!("refs/heads/main"));
| ^^^^^^^^^
error[E0425]: cannot find type `Namespace` in this scope
--> crates/radicle-surf/src/namespace.rs:49:29
|
49 | let ns = "surf/git".parse::<Namespace>();
| ^^^^^^^^^ not found in this scope
|
help: you might be missing a type parameter
|
48 | fn main() { #[allow(non_snake_case)] fn _doctest_main_crates_radicle_surf_src_namespace_rs_48_0<Namespace>() {
| +++++++++++
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0425`.
Couldn't compile the test.
---- crates/radicle-surf/src/glob.rs - glob::Glob<Remote>::branches (line 249) stdout ----
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/glob.rs:250:15
|
250 | Glob::remotes(pattern!("origin/features/*"))
| ^^^^^^^
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/glob.rs:251:13
|
251 | .insert(pattern!("origin/qa/*"))
| ^^^^^^^
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/glob.rs:253:22
|
253 | .and(Glob::heads(pattern!("features/*")))
| ^^^^^^^
error[E0433]: cannot find type `Glob` in this scope
--> crates/radicle-surf/src/glob.rs:250:1
|
250 | Glob::remotes(pattern!("origin/features/*"))
| ^^^^ use of undeclared type `Glob`
error[E0433]: cannot find type `Glob` in this scope
--> crates/radicle-surf/src/glob.rs:253:10
|
253 | .and(Glob::heads(pattern!("features/*")))
| ^^^^ use of undeclared type `Glob`
error: aborting due to 5 previous errors
For more information about this error, try `rustc --explain E0433`.
Couldn't compile the test.
---- crates/radicle-surf/src/glob.rs - glob::Glob<Local>::branches (line 175) stdout ----
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/glob.rs:176:13
|
176 | Glob::heads(pattern!("features/*"))
| ^^^^^^^
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/glob.rs:177:13
|
177 | .insert(pattern!("qa/*"))
| ^^^^^^^
error: cannot find macro `pattern` in this scope
--> crates/radicle-surf/src/glob.rs:179:24
|
179 | .and(Glob::remotes(pattern!("origin/features/*")))
| ^^^^^^^
error[E0433]: cannot find type `Glob` in this scope
--> crates/radicle-surf/src/glob.rs:176:1
|
176 | Glob::heads(pattern!("features/*"))
| ^^^^ use of undeclared type `Glob`
error[E0433]: cannot find type `Glob` in this scope
--> crates/radicle-surf/src/glob.rs:179:10
|
179 | .and(Glob::remotes(pattern!("origin/features/*")))
| ^^^^ use of undeclared type `Glob`
error: aborting due to 5 previous errors
For more information about this error, try `rustc --explain E0433`.
Couldn't compile the test.
failures:
crates/radicle-surf/src/glob.rs - glob::Glob<Local>::branches (line 175)
crates/radicle-surf/src/glob.rs - glob::Glob<Remote>::branches (line 249)
crates/radicle-surf/src/namespace.rs - namespace::Namespace::to_namespaced (line 48)
crates/radicle-surf/src/namespace.rs - namespace::Namespace::to_namespaced_pattern (line 75)
test result: FAILED. 0 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
all doctests ran in 0.10s; merged doctests compilation took 0.06s
error: doctest failed, to rerun pass `-p radicle-surf --doc`
Doc-tests radicle_systemd
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests radicle_term
running 1 test
test crates/radicle-term/src/table.rs - table (line 4) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.16s; merged doctests compilation took 0.15s
Doc-tests radicle_windows
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: 1 target failed:
`-p radicle-surf --doc`
Exit code: 101
{
"response": "finished",
"result": "failure"
}