From 98c156610905e04a2d3ee862b64e7974ea21383f Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Thu, 8 Dec 2022 15:38:09 +0100 Subject: [PATCH] Make `Doc` type payload generic --- Cargo.lock | 131 +++++++------ Cargo.toml | 2 +- radicle-cli/examples/rad-init.md | 4 +- radicle-cli/src/commands/checkout.rs | 10 +- radicle-cli/src/commands/clone.rs | 7 +- radicle-cli/src/commands/init.rs | 12 +- radicle-cli/src/commands/ls.rs | 6 +- radicle-cli/src/commands/merge.rs | 2 +- radicle-cli/src/commands/review.rs | 2 +- radicle-cli/src/commands/track.rs | 7 +- radicle-cli/src/commands/untrack.rs | 6 +- radicle-cob/Cargo.toml | 2 +- radicle-httpd/src/api.rs | 8 +- radicle-httpd/src/api/v1/delegates.rs | 7 +- radicle-httpd/src/api/v1/projects.rs | 4 +- radicle/src/identity/project.rs | 253 ++++++++++++++++---------- radicle/src/lib.rs | 2 +- radicle/src/rad.rs | 30 +-- radicle/src/storage/git.rs | 25 ++- radicle/src/test/arbitrary.rs | 40 ++-- 20 files changed, 336 insertions(+), 224 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 222ed026..d7111040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "amplify" -version = "4.0.0-beta.4" +version = "4.0.0-beta.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c3b7fe483c11f7a434ec6923a68749b3415c1328857f1a8e37bb2c792a25c0" +checksum = "4ff4d466b0cb5124991d3a2085c28f2aed6d86264c3c0bdbd7d1b64a49b4726b" dependencies = [ "amplify_derive", "amplify_num", @@ -543,9 +543,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" dependencies = [ "cc", "cxxbridge-flags", @@ -555,9 +555,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" dependencies = [ "cc", "codespan-reporting", @@ -570,15 +570,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ "proc-macro2", "quote", @@ -597,9 +597,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "data-encoding-macro" @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "der" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", "pem-rfc7468", @@ -704,9 +704,9 @@ dependencies = [ [[package]] name = "ed25519-compact" -version = "2.0.2" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2d21333b679bbbac680b3eb45c86937e42f69277028f4e97b599b80b86c253" +checksum = "6a3d382e8464107391c8706b4c14b087808ecb909f6c15c34114bc42e53a9e4c" dependencies = [ "ct-codecs", "getrandom 0.2.8", @@ -873,9 +873,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if", "libc", @@ -1020,13 +1020,31 @@ dependencies = [ "thiserror", ] +[[package]] +name = "git-ref-format" +version = "0.1.0" +source = "git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481#39ce2f934915a563f9420ac9c85480df8a591481" +dependencies = [ + "git-ref-format-core 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", + "git-ref-format-macro 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", +] + [[package]] name = "git-ref-format" version = "0.1.0" source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d" dependencies = [ - "git-ref-format-core", - "git-ref-format-macro", + "git-ref-format-core 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)", + "git-ref-format-macro 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)", +] + +[[package]] +name = "git-ref-format-core" +version = "0.1.0" +source = "git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481#39ce2f934915a563f9420ac9c85480df8a591481" +dependencies = [ + "serde", + "thiserror", ] [[package]] @@ -1038,12 +1056,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "git-ref-format-macro" +version = "0.1.0" +source = "git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481#39ce2f934915a563f9420ac9c85480df8a591481" +dependencies = [ + "git-ref-format-core 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", + "proc-macro-error", + "quote", + "syn", +] + [[package]] name = "git-ref-format-macro" version = "0.1.0" source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d" dependencies = [ - "git-ref-format-core", + "git-ref-format-core 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)", "proc-macro-error", "quote", "syn", @@ -1467,9 +1496,9 @@ checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libgit2-sys" @@ -2067,7 +2096,7 @@ dependencies = [ "cyphernet", "ed25519-compact", "fastrand", - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", "git2", "log", "multibase", @@ -2126,7 +2155,7 @@ dependencies = [ "ed25519-compact", "fastrand", "git-commit", - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", "git-trailers", "git2", "log", @@ -2164,7 +2193,7 @@ dependencies = [ "cyphernet", "ed25519-compact", "fastrand", - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", "multibase", "qcheck", "qcheck-macros", @@ -2183,7 +2212,7 @@ name = "radicle-git-ext" version = "0.2.0" source = "git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d#d3115a22158c8395705babefdc89049f7510d32d" dependencies = [ - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)", "git2", "percent-encoding", "radicle-std-ext", @@ -2229,7 +2258,7 @@ dependencies = [ "colored", "crossbeam-channel", "fastrand", - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", "lexopt", "log", "nakamoto-net", @@ -2281,7 +2310,7 @@ dependencies = [ "base64", "either", "flate2", - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=d3115a22158c8395705babefdc89049f7510d32d)", "git2", "nom 6.1.2", "nonempty 0.5.0", @@ -2297,7 +2326,7 @@ name = "radicle-tools" version = "0.2.0" dependencies = [ "anyhow", - "git-ref-format", + "git-ref-format 0.1.0 (git+https://github.com/radicle-dev/radicle-git?rev=39ce2f934915a563f9420ac9c85480df8a591481)", "radicle", ] @@ -2489,9 +2518,9 @@ checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" [[package]] name = "rustix" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" dependencies = [ "bitflags", "errno", @@ -2524,9 +2553,9 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d8a765117b237ef233705cc2cc4c6a27fccd46eea6ef0c8c6dae5f3ef407f8" +checksum = "001cf62ece89779fd16105b5f515ad0e5cedcd5440d3dd806bb067978e7c3608" dependencies = [ "cfg-if", "derive_more", @@ -2536,9 +2565,9 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcd47b380d8c4541044e341dcd9475f55ba37ddc50c908d945fc036a8642496" +checksum = "303959cf613a6f6efd19ed4b4ad5bf79966a13352716299ad532cfb115f4205c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2580,18 +2609,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -2901,9 +2930,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -3077,9 +3106,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg", "bytes", @@ -3090,7 +3119,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "winapi", + "windows-sys", ] [[package]] @@ -3145,9 +3174,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags", "bytes", @@ -3258,9 +3287,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "uint" @@ -3556,9 +3585,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b9c3e798..634b5224 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,4 +42,4 @@ rev = "d3115a22158c8395705babefdc89049f7510d32d" [patch.crates-io.git-ref-format] git = "https://github.com/radicle-dev/radicle-git" -rev = "d3115a22158c8395705babefdc89049f7510d32d" +rev = "39ce2f934915a563f9420ac9c85480df8a591481" diff --git a/radicle-cli/examples/rad-init.md b/radicle-cli/examples/rad-init.md index b8719090..6844e0cc 100644 --- a/radicle-cli/examples/rad-init.md +++ b/radicle-cli/examples/rad-init.md @@ -15,7 +15,7 @@ ok Project heartwood created } -Your project id is rad:z2TBtGrJKGsremYAPec6vN4n77Ba7. You can show it any time by running: +Your project id is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji. You can show it any time by running: rad . To publish your project to the network, run: @@ -27,5 +27,5 @@ Projects can be listed with the `ls` command: ``` $ rad ls -heartwood rad:z2TBtGrJKGsremYAPec6vN4n77Ba7 cdf76ce Radicle Heartwood Protocol & Stack +heartwood rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji cdf76ce Radicle Heartwood Protocol & Stack ``` diff --git a/radicle-cli/src/commands/checkout.rs b/radicle-cli/src/commands/checkout.rs index 09e5e222..cf3ad749 100644 --- a/radicle-cli/src/commands/checkout.rs +++ b/radicle-cli/src/commands/checkout.rs @@ -79,12 +79,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { pub fn execute(options: Options, profile: &Profile) -> anyhow::Result { let id = options.id; let storage = &profile.storage; - let Doc { - payload, delegates, .. - } = storage + let doc = storage .repository(id)? - .project_of(profile.id()) + .identity_of(profile.id()) .context("project could not be found in local storage")?; + let payload = doc.project()?; let path = PathBuf::from(payload.name.clone()); if path.exists() { @@ -109,7 +108,8 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result { }; spinner.finish(); - let remotes = delegates + let remotes = doc + .delegates .into_iter() .map(|did| *did) .filter(|id| id != profile.id()) diff --git a/radicle-cli/src/commands/clone.rs b/radicle-cli/src/commands/clone.rs index 748d2189..3d3fb98a 100644 --- a/radicle-cli/src/commands/clone.rs +++ b/radicle-cli/src/commands/clone.rs @@ -93,10 +93,11 @@ pub fn clone(id: Id, _interactive: Interactive, ctx: impl term::Context) -> anyh let doc = profile .storage .repository(id)? - .project_of(profile.id()) + .identity_of(profile.id()) .map_err(|_e| anyhow!("couldn't load project {} from local state", id))?; + let proj = doc.project()?; - let path = Path::new(&doc.name); + let path = Path::new(&proj.name); let repo = rad::checkout(id, profile.id(), path, &profile.storage)?; let delegates = doc .delegates @@ -104,7 +105,7 @@ pub fn clone(id: Id, _interactive: Interactive, ctx: impl term::Context) -> anyh .map(|d| **d) .filter(|id| id != profile.id()) .collect::>(); - let default_branch = doc.payload.default_branch.clone(); + let default_branch = proj.default_branch.clone(); // Setup tracking for project delegates. setup_remotes( diff --git a/radicle-cli/src/commands/init.rs b/radicle-cli/src/commands/init.rs index 736ee8c8..99287076 100644 --- a/radicle-cli/src/commands/init.rs +++ b/radicle-cli/src/commands/init.rs @@ -201,24 +201,26 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> &profile.storage, ) { Ok((id, doc, _)) => { + let proj = doc.project()?; + spinner.message(format!( "Project {} created", - term::format::highlight(&doc.name) + term::format::highlight(&proj.name) )); spinner.finish(); if interactive.no() { - term::blob(json::to_string_pretty(&doc.payload)?); + term::blob(json::to_string_pretty(&proj)?); term::blank(); } - if options.set_upstream || git::branch_remote(&repo, &doc.default_branch).is_err() { + if options.set_upstream || git::branch_remote(&repo, &proj.default_branch).is_err() { // Setup eg. `master` -> `rad/master` radicle::git::set_upstream( &repo, &radicle::rad::REMOTE_NAME, - &doc.default_branch, - &radicle::git::refs::workdir::branch(&doc.default_branch), + &proj.default_branch, + &radicle::git::refs::workdir::branch(&proj.default_branch), )?; } diff --git a/radicle-cli/src/commands/ls.rs b/radicle-cli/src/commands/ls.rs index 85c346d0..11ca3c88 100644 --- a/radicle-cli/src/commands/ls.rs +++ b/radicle-cli/src/commands/ls.rs @@ -50,13 +50,13 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> { storage.projects()?.into_iter().for_each(|id| { let Ok(repo) = storage.repository(id) else { return }; let Ok((_, head)) = repo.head() else { return }; - let Ok(Doc { payload, .. }) = repo.project_of(profile.id()) else { return }; + let Ok(Project { name, description, .. }) = repo.project_of(profile.id()) else { return }; let head = term::format::oid(head); table.push([ - term::format::bold(payload.name), + term::format::bold(name), term::format::tertiary(id), term::format::secondary(head), - term::format::italic(payload.description), + term::format::italic(description), ]); }); table.render(); diff --git a/radicle-cli/src/commands/merge.rs b/radicle-cli/src/commands/merge.rs index 6315a33e..52c7c304 100644 --- a/radicle-cli/src/commands/merge.rs +++ b/radicle-cli/src/commands/merge.rs @@ -137,7 +137,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let signer = term::signer(&profile)?; let repository = profile.storage.repository(id)?; let _project = repository - .project_of(profile.id()) + .identity_of(profile.id()) .context(format!("couldn't load project {} from local state", id))?; let repository = profile.storage.repository(id)?; let mut patches = Patches::open(*profile.id(), &repository)?; diff --git a/radicle-cli/src/commands/review.rs b/radicle-cli/src/commands/review.rs index ff41fd72..8925ca55 100644 --- a/radicle-cli/src/commands/review.rs +++ b/radicle-cli/src/commands/review.rs @@ -137,7 +137,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let signer = term::signer(&profile)?; let repository = profile.storage.repository(id)?; let _project = repository - .project_of(profile.id()) + .identity_of(profile.id()) .context(format!("couldn't load project {} from local state", id))?; let mut patches = Patches::open(*profile.id(), &repository)?; diff --git a/radicle-cli/src/commands/track.rs b/radicle-cli/src/commands/track.rs index ded8a1a5..4b07c0d4 100644 --- a/radicle-cli/src/commands/track.rs +++ b/radicle-cli/src/commands/track.rs @@ -3,8 +3,7 @@ use std::str::FromStr; use anyhow::{anyhow, Context as _}; -use radicle::node::Handle; -use radicle::prelude::*; +use radicle::node::{Handle, NodeId}; use radicle::storage::WriteStorage; use crate::terminal as term; @@ -94,12 +93,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let profile = ctx.profile()?; let storage = &profile.storage; let (_, rid) = radicle::rad::cwd().context("this command must be run within a project")?; - let Doc { payload, .. } = storage.repository(rid)?.project_of(profile.id())?; + let project = storage.repository(rid)?.project_of(profile.id())?; let mut node = radicle::node::connect(&profile.node())?; term::info!( "Establishing 🌱 tracking relationship for {}", - term::format::highlight(&payload.name) + term::format::highlight(&project.name) ); term::blank(); diff --git a/radicle-cli/src/commands/untrack.rs b/radicle-cli/src/commands/untrack.rs index 90ed49d8..e1e5dcc0 100644 --- a/radicle-cli/src/commands/untrack.rs +++ b/radicle-cli/src/commands/untrack.rs @@ -68,18 +68,18 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { .context("current directory is not a git repository; please supply an ``")?; let profile = ctx.profile()?; let storage = &profile.storage; - let Doc { payload, .. } = storage.repository(id)?.project_of(profile.id())?; + let project = storage.repository(id)?.project_of(profile.id())?; if untrack(id, &profile)? { term::success!( "Tracking relationships for {} ({}) removed", - term::format::highlight(payload.name), + term::format::highlight(project.name), &id.to_human() ); } else { term::info!( "Tracking relationships for {} ({}) doesn't exist", - term::format::highlight(payload.name), + term::format::highlight(project.name), &id.to_human() ); } diff --git a/radicle-cob/Cargo.toml b/radicle-cob/Cargo.toml index 480e31b5..7d2d5526 100644 --- a/radicle-cob/Cargo.toml +++ b/radicle-cob/Cargo.toml @@ -19,7 +19,7 @@ git-trailers = { version = "0.1" } log = { version = "0.4.17" } nonempty = { version = "0.8.1", features = ["serialize"] } petgraph = { version = "0.5" } -radicle-git-ext = { version = "0.2" } +radicle-git-ext = { version = "0" } serde_json = { version = "1.0" } thiserror = { version = "1.0" } diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index b06dd8cd..f87142d8 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -17,7 +17,7 @@ use tower_http::trace::TraceLayer; use tracing::Span; use radicle::cob::issue::Issues; -use radicle::identity::{Doc, Id}; +use radicle::identity::Id; use radicle::storage::{ReadRepository, WriteStorage}; use radicle::Profile; @@ -49,7 +49,7 @@ impl Context { let storage = &self.profile.storage; let repo = storage.repository(id)?; let (_, head) = repo.head()?; - let Doc { payload, .. } = repo.project_of(self.profile.id())?; + let payload = repo.project_of(self.profile.id())?; let issues = (Issues::open(self.profile.public_key, &repo)?).count()?; Ok(project::Info { @@ -142,7 +142,7 @@ pub struct PaginationQuery { mod project { use radicle::git::Oid; - use radicle::identity::project::Payload; + use radicle::identity::project::Project; use radicle::identity::Id; use serde::Serialize; @@ -152,7 +152,7 @@ mod project { pub struct Info { /// Project metadata. #[serde(flatten)] - pub payload: Payload, + pub payload: Project, pub head: Oid, pub patches: usize, pub issues: usize, diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 89364523..d578504d 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -3,7 +3,7 @@ use axum::routing::get; use axum::{Extension, Json, Router}; use radicle::cob::issue::Issues; -use radicle::identity::{Did, Doc}; +use radicle::identity::Did; use radicle::storage::{ReadRepository, WriteStorage}; use crate::api::axum_extra::{Path, Query}; @@ -38,9 +38,10 @@ async fn delegates_projects_handler( .filter_map(|id| { let Ok(repo) = storage.repository(id) else { return None }; let Ok((_, head)) = repo.head() else { return None }; - let Ok(Doc { payload, delegates, .. }) = repo.project_of(ctx.profile.id()) else { return None }; + let Ok(doc) = repo.identity_of(ctx.profile.id()) else { return None }; + let Ok(payload) = doc.project() else { return None }; - if !delegates.iter().any(|d| *d == delegate) { + if !doc.delegates.iter().any(|d| *d == delegate) { return None; } diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index c8b24f9d..f8141017 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -14,7 +14,7 @@ use radicle::cob::issue::Issues; use radicle::cob::thread::{self, CommentId}; use radicle::cob::Timestamp; use radicle::git::raw::BranchType; -use radicle::identity::{Doc, Id, PublicKey}; +use radicle::identity::{Id, PublicKey}; use radicle::node::NodeId; use radicle::storage::{Oid, ReadRepository, WriteRepository, WriteStorage}; use radicle_surf::git::History; @@ -68,7 +68,7 @@ async fn project_root_handler( .filter_map(|id| { let Ok(repo) = storage.repository(id) else { return None }; let Ok((_, head)) = repo.head() else { return None }; - let Ok(Doc { payload, .. }) = repo.project_of(ctx.profile.id()) else { return None }; + let Ok(payload) = repo.project_of(ctx.profile.id()) else { return None }; let Ok(issues) = Issues::open(ctx.profile.public_key, &repo) else { return None }; let Ok(issues) = (*issues).count() else { return None }; diff --git a/radicle/src/identity/project.rs b/radicle/src/identity/project.rs index 2cd2fe3f..1f369e74 100644 --- a/radicle/src/identity/project.rs +++ b/radicle/src/identity/project.rs @@ -1,7 +1,7 @@ mod id; use std::collections::{BTreeMap, HashMap}; -use std::fmt::Write as _; +use std::fmt::{self, Write as _}; use std::io; use std::marker::PhantomData; use std::ops::Deref; @@ -46,6 +46,8 @@ pub enum DocError { Io(#[from] io::Error), #[error("verification: {0}")] Verification(#[from] VerificationError), + #[error("payload: {0}")] + Payload(#[from] PayloadError), #[error("git: {0}")] Git(#[from] git::Error), #[error("git: {0}")] @@ -68,26 +70,120 @@ impl DocError { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Payload { - pub name: String, - pub description: String, - pub default_branch: git::RefString, +#[derive(Debug, Error)] +pub enum ProjectError { + #[error("invalid name: {0}")] + Name(&'static str), + #[error("invalid description: {0}")] + Description(&'static str), + #[error("invalid default branch: {0}")] + DefaultBranch(&'static str), + #[error("json: {0}")] + Json(#[from] serde_json::Error), + #[error("project payload not found in identity document")] + NotFound, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Project { + pub name: String, + pub description: String, + pub default_branch: BranchName, +} + +impl From for Payload { + fn from(proj: Project) -> Self { + let value = serde_json::to_value(proj) + .expect("Payload::from: could not convert project into value"); + + Self { value } + } +} + +impl Project { + /// Validate the project data. + pub fn validate(&self) -> Result<(), ProjectError> { + if self.name.is_empty() { + return Err(ProjectError::Name("name cannot be empty")); + } + if self.name.len() > MAX_STRING_LENGTH { + return Err(ProjectError::Name("name cannot exceed 255 bytes")); + } + if self.description.len() > MAX_STRING_LENGTH { + return Err(ProjectError::Description( + "description cannot exceed 255 bytes", + )); + } + if self.default_branch.is_empty() { + return Err(ProjectError::DefaultBranch( + "default branch cannot be empty", + )); + } + if self.default_branch.len() > MAX_STRING_LENGTH { + return Err(ProjectError::DefaultBranch( + "default branch cannot exceed 255 bytes", + )); + } + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum PayloadError { + #[error("json: {0}")] + Json(#[from] serde_json::Error), + #[error("payload '{0}' not found in identity document")] + NotFound(PayloadId), +} + +/// Identifies an identity document payload type. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(transparent)] // TODO: Restrict values. -pub struct Namespace(String); +pub struct PayloadId(String); + +impl fmt::Display for PayloadId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl PayloadId +where + PayloadId: Clone, +{ + /// Project payload type. + pub fn project() -> Self { + Self(String::from("xyz.radicle.project")) + } +} + +/// Payload value. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Payload { + value: serde_json::Value, +} + +impl From for Payload { + fn from(value: serde_json::Value) -> Self { + Self { value } + } +} + +impl Deref for Payload { + type Target = serde_json::Value; + + fn deref(&self) -> &Self::Target { + &self.value + } +} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Doc { - #[serde(rename = "xyz.radicle.project")] - pub payload: Payload, - #[serde(flatten)] - pub extensions: BTreeMap, + pub payload: BTreeMap, pub delegates: NonEmpty, pub threshold: usize, @@ -117,6 +213,17 @@ impl Doc { false } + /// Get the project payload, if it exists and is valid, out of this document. + pub fn project(&self) -> Result { + let value = self + .payload + .get(&PayloadId::project()) + .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?; + let proj: Project = serde_json::from_value((**value).clone())?; + + Ok(proj) + } + pub fn sign(&self, signer: &G) -> Result<(git::Oid, Signature), DocError> { let (oid, bytes) = self.encode()?; let sig = signer.sign(&bytes); @@ -181,66 +288,27 @@ impl Doc { } } -impl Deref for Doc { - type Target = Payload; - - fn deref(&self) -> &Self::Target { - &self.payload - } -} - #[derive(Error, Debug)] pub enum VerificationError { - #[error("invalid name: {0}")] - Name(&'static str), - #[error("invalid description: {0}")] - Description(&'static str), - #[error("invalid default branch: {0}")] - DefaultBranch(&'static str), #[error("invalid delegates: {0}")] Delegates(&'static str), #[error("invalid version `{0}`")] Version(u32), - #[error("invalid parent: {0}")] - Parent(&'static str), #[error("invalid threshold `{0}`: {1}")] Threshold(usize, &'static str), } impl Doc { - pub fn initial( - name: String, - description: String, - default_branch: BranchName, - delegate: Did, - ) -> Self { - Self { - payload: Payload { - name, - description, - default_branch, - }, - extensions: BTreeMap::new(), - delegates: NonEmpty::new(delegate), - threshold: 1, - verified: PhantomData, - } + pub fn initial(project: Project, delegate: Did) -> Self { + Self::new(project, NonEmpty::new(delegate), 1) } - pub fn new( - name: String, - description: String, - default_branch: BranchName, - delegates: NonEmpty, - threshold: usize, - ) -> Self { + pub fn new(project: Project, delegates: NonEmpty, threshold: usize) -> Self { + let project = + serde_json::to_value(project).expect("Doc::initial: payload must be serializable"); + Self { - payload: Payload { - name, - description, - default_branch, - }, - extensions: BTreeMap::new(), + payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]), delegates, threshold, verified: PhantomData, @@ -252,17 +320,6 @@ impl Doc { } pub fn verified(self) -> Result, VerificationError> { - if self.name.is_empty() { - return Err(VerificationError::Name("name cannot be empty")); - } - if self.name.len() > MAX_STRING_LENGTH { - return Err(VerificationError::Name("name cannot exceed 255 bytes")); - } - if self.description.len() > MAX_STRING_LENGTH { - return Err(VerificationError::Description( - "description cannot exceed 255 bytes", - )); - } if self.delegates.len() > MAX_DELEGATES { return Err(VerificationError::Delegates( "number of delegates cannot exceed 255", @@ -273,16 +330,6 @@ impl Doc { "delegate list cannot be empty", )); } - if self.default_branch.is_empty() { - return Err(VerificationError::DefaultBranch( - "default branch cannot be empty", - )); - } - if self.default_branch.len() > MAX_STRING_LENGTH { - return Err(VerificationError::DefaultBranch( - "default branch cannot exceed 255 bytes", - )); - } if self.threshold > self.delegates.len() { return Err(VerificationError::Threshold( self.threshold, @@ -298,7 +345,6 @@ impl Doc { Ok(Doc { payload: self.payload, - extensions: self.extensions, delegates: self.delegates, threshold: self.threshold, verified: PhantomData, @@ -500,9 +546,13 @@ mod test { delegate.public_key().to_human(), String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi") ); + assert_eq!( + (*id).to_string(), + "d96f425412c9f8ad5d9a9a05c9831d0728e2338d" + ); assert_eq!( id.to_human(), - String::from("rad:z2TBtGrJKGsremYAPec6vN4n77Ba7") + String::from("rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji") ); } @@ -542,14 +592,16 @@ mod test { // TODO: In some cases we want to get the repo and the project, but don't // want to have to create a repository object twice. Perhaps there should // be a way of getting a project from a repo. - let mut proj = storage.get(alice.public_key(), id).unwrap().unwrap(); + let mut doc = storage.get(alice.public_key(), id).unwrap().unwrap(); + let mut prj = doc.project().unwrap(); let repo = storage.repository(id).unwrap(); // Make a change to the description and sign it. - proj.payload.description += "!"; - proj.sign(&alice) + prj.description += "!"; + doc.payload.insert(PayloadId::project(), prj.clone().into()); + doc.sign(&alice) .and_then(|(_, sig)| { - proj.update( + doc.update( alice.public_key(), "Update description", &[(alice.public_key(), sig)], @@ -559,11 +611,11 @@ mod test { .unwrap(); // Add Bob as a delegate, and sign it. - proj.delegate(*bob.public_key()); - proj.threshold = 2; - proj.sign(&alice) + doc.delegate(*bob.public_key()); + doc.threshold = 2; + doc.sign(&alice) .and_then(|(_, sig)| { - proj.update( + doc.update( alice.public_key(), "Add bob", &[(alice.public_key(), sig)], @@ -573,11 +625,11 @@ mod test { .unwrap(); // Add Eve as a delegate, and sign it. - proj.delegate(*eve.public_key()); - proj.sign(&alice) + doc.delegate(*eve.public_key()); + doc.sign(&alice) .and_then(|(_, alice_sig)| { - proj.sign(&bob).and_then(|(_, bob_sig)| { - proj.update( + doc.sign(&bob).and_then(|(_, bob_sig)| { + doc.update( alice.public_key(), "Add eve", &[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)], @@ -588,12 +640,13 @@ mod test { .unwrap(); // Update description again with signatures by Eve and Bob. - proj.payload.description += "?"; - let (current, head) = proj + prj.description += "?"; + doc.payload.insert(PayloadId::project(), prj.into()); + let (current, head) = doc .sign(&bob) .and_then(|(_, bob_sig)| { - proj.sign(&eve).and_then(|(blob_id, eve_sig)| { - proj.update( + doc.sign(&eve).and_then(|(blob_id, eve_sig)| { + doc.update( alice.public_key(), "Update description", &[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)], @@ -614,10 +667,10 @@ mod test { assert_eq!(identity.root, id); assert_eq!(identity.current, current); assert_eq!(identity.head, head); - assert_eq!(identity.doc, proj); + assert_eq!(identity.doc, doc); - let proj = storage.get(alice.public_key(), id).unwrap().unwrap(); - assert_eq!(proj.description, "Acme's repository!?"); + let doc = storage.get(alice.public_key(), id).unwrap().unwrap(); + assert_eq!(doc.project().unwrap().description, "Acme's repository!?"); } #[quickcheck] diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index e292629f..901b1014 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -26,7 +26,7 @@ pub mod prelude { use super::*; pub use crypto::{PublicKey, Signer, Verified}; - pub use identity::{Did, Doc, Id}; + pub use identity::{project::Project, Did, Doc, Id}; pub use node::NodeId; pub use profile::Profile; pub use storage::{BranchName, ReadRepository, ReadStorage, WriteRepository, WriteStorage}; diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 7b74faaf..e5bb3fe1 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::crypto::{Signer, Verified}; use crate::git; -use crate::identity::project::DocError; +use crate::identity::project::{self, DocError, Project}; use crate::identity::Id; use crate::node; use crate::node::NodeId; @@ -60,13 +60,12 @@ pub fn init( // TODO: Better error when project id already exists in storage, but remote doesn't. let pk = signer.public_key(); let delegate = identity::Did::from(*pk); - let doc = identity::Doc::initial( - name.to_owned(), - description.to_owned(), - default_branch.clone(), - delegate, - ) - .verified()?; + let proj = Project { + name: name.to_owned(), + description: description.to_owned(), + default_branch: default_branch.clone(), + }; + let doc = identity::Doc::initial(proj, delegate).verified()?; let (id, _, project) = doc.create(pk, "Initialize Radicle\n", storage)?; let url = git::Url::from(id).with_namespace(*pk); @@ -96,6 +95,8 @@ pub enum ForkError { Git(#[from] git::Error), #[error("storage: {0}")] Storage(#[from] storage::Error), + #[error("payload: {0}")] + Payload(#[from] project::PayloadError), #[error("project `{0}` was not found in storage")] NotFound(Id), #[error("project identity error: {0}")] @@ -123,9 +124,10 @@ pub fn fork_remote( // refs/namespaces//refs/tags/* let me = signer.public_key(); - let project = storage + let doc = storage .get(remote, proj)? .ok_or(ForkError::NotFound(proj))?; + let project = doc.project()?; let repository = storage.repository(proj)?; let raw = repository.raw(); @@ -247,6 +249,8 @@ pub enum CheckoutError { Git(#[from] git2::Error), #[error("storage: {0}")] Storage(#[from] storage::Error), + #[error("payload: {0}")] + Payload(#[from] project::PayloadError), #[error("project `{0}` was not found in storage")] NotFound(Id), #[error("project error: {0}")] @@ -263,9 +267,10 @@ pub fn checkout, S: storage::ReadStorage>( ) -> Result { // TODO: Decide on whether we can use `clone_local` // TODO: Look into sharing object databases. - let project = storage + let doc = storage .get(remote, proj)? .ok_or(CheckoutError::NotFound(proj))?; + let project = doc.project()?; let mut opts = git2::RepositoryInitOptions::new(); opts.no_reinit(true).description(&project.description); @@ -375,7 +380,8 @@ mod tests { ) .unwrap(); - let project = storage.get(&public_key, proj).unwrap().unwrap(); + let doc = storage.get(&public_key, proj).unwrap().unwrap(); + let project = doc.project().unwrap(); let remotes: HashMap<_, _> = storage .repository(proj) .unwrap() @@ -402,7 +408,7 @@ mod tests { assert_eq!(project.name, "acme"); assert_eq!(project.description, "Acme's repo"); assert_eq!(project.default_branch, git::refname!("master")); - assert_eq!(project.delegates.first(), &Did::from(public_key)); + assert_eq!(doc.delegates.first(), &Did::from(public_key)); } #[test] diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 44dc22c2..dd2823d5 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -11,7 +11,7 @@ use radicle_cob::{self as cob, change}; use crate::git; use crate::identity; -use crate::identity::project::{Identity, IdentityError}; +use crate::identity::project::{Identity, IdentityError, Project}; use crate::identity::{Doc, Id}; use crate::storage::refs; use crate::storage::refs::{Refs, SignedRefs}; @@ -41,6 +41,8 @@ pub enum ProjectError { Storage(#[from] Error), #[error("identity document error: {0}")] Doc(#[from] identity::project::DocError), + #[error("payload error: {0}")] + Payload(#[from] identity::project::PayloadError), #[error("identity verification error: {0}")] Verify(#[from] identity::project::VerificationError), #[error("git: {0}")] @@ -96,7 +98,7 @@ impl ReadStorage for Storage { fn get(&self, remote: &RemoteId, proj: Id) -> Result>, ProjectError> { // TODO: Don't create a repo here if it doesn't exist? // Perhaps for checking we could have a `contains` method? - match self.repository(proj)?.project_of(remote) { + match self.repository(proj)?.identity_of(remote) { Ok(doc) => Ok(Some(doc)), Err(err) if err.is_not_found() => Ok(None), @@ -271,7 +273,14 @@ impl Repository { Identity::load(remote, self) } - pub fn project_of(&self, remote: &RemoteId) -> Result, ProjectError> { + pub fn project_of(&self, remote: &RemoteId) -> Result { + let doc = self.identity_of(remote)?; + let proj = doc.project()?; + + Ok(proj) + } + + pub fn identity_of(&self, remote: &RemoteId) -> Result, ProjectError> { let (doc, _) = identity::Doc::load(remote, self)?; let verified = doc.verified()?; @@ -279,7 +288,7 @@ impl Repository { } /// Return the canonical identity [`git::Oid`] and document. - pub fn project(&self) -> Result<(Oid, identity::Doc), ProjectError> { + pub fn identity_doc(&self) -> Result<(Oid, identity::Doc), ProjectError> { let mut heads = Vec::new(); for remote in self.remote_ids()? { let remote = remote?; @@ -478,7 +487,7 @@ impl ReadRepository for Repository { } fn project_identity(&self) -> Result<(Oid, identity::Doc), ProjectError> { - Repository::project(self) + Repository::identity_doc(self) } fn head(&self) -> Result<(Qualified, Oid), ProjectError> { @@ -494,12 +503,14 @@ impl ReadRepository for Repository { fn canonical_head(&self) -> Result<(Qualified, Oid), ProjectError> { // TODO: In the `fork` function for example, we call Repository::project_identity again, // This should only be necessary once. - let (_, project) = self.project_identity()?; + let (_, doc) = self.project_identity()?; + let doc = doc.verified()?; + let project = doc.project()?; let branch_ref = Qualified::from(lit::refs_heads(&project.default_branch)); let raw = self.raw(); let mut heads = Vec::new(); - for delegate in project.delegates.iter() { + for delegate in doc.delegates.iter() { let r = self.reference_oid(delegate, &branch_ref)?.into(); heads.push(r); diff --git a/radicle/src/test/arbitrary.rs b/radicle/src/test/arbitrary.rs index 6104e088..16fee756 100644 --- a/radicle/src/test/arbitrary.rs +++ b/radicle/src/test/arbitrary.rs @@ -10,7 +10,7 @@ use qcheck::Arbitrary; use crate::collections::HashMap; use crate::git; -use crate::identity::{project::Doc, Did, Id}; +use crate::identity::{project::Doc, project::Project, Did, Id}; use crate::storage; use crate::storage::refs::{Refs, SignedRefs}; use crate::test::storage::MockStorage; @@ -74,18 +74,7 @@ impl Arbitrary for Did { } } -impl Arbitrary for Doc { - fn arbitrary(g: &mut qcheck::Gen) -> Self { - let name = String::arbitrary(g); - let description = String::arbitrary(g); - let default_branch = git::RefString::try_from(String::arbitrary(g)).unwrap(); - let delegate = Did::arbitrary(g); - - Self::initial(name, description, default_branch, delegate) - } -} - -impl Arbitrary for Doc { +impl Arbitrary for Project { fn arbitrary(g: &mut qcheck::Gen) -> Self { let rng = fastrand::Rng::with_seed(u64::arbitrary(g)); let name = iter::repeat_with(|| rng.alphanumeric()) @@ -99,14 +88,35 @@ impl Arbitrary for Doc { .collect::() .try_into() .unwrap(); + + Project { + name, + description, + default_branch, + } + } +} + +impl Arbitrary for Doc { + fn arbitrary(g: &mut qcheck::Gen) -> Self { + let proj = Project::arbitrary(g); + let delegate = Did::arbitrary(g); + + Self::initial(proj, delegate) + } +} + +impl Arbitrary for Doc { + fn arbitrary(g: &mut qcheck::Gen) -> Self { + let rng = fastrand::Rng::with_seed(u64::arbitrary(g)); + let project = Project::arbitrary(g); let delegates: NonEmpty<_> = iter::repeat_with(|| Did::arbitrary(g)) .take(rng.usize(1..6)) .collect::>() .try_into() .unwrap(); let threshold = delegates.len() / 2 + 1; - let doc: Doc = - Doc::new(name, description, default_branch, delegates, threshold); + let doc: Doc = Doc::new(project, delegates, threshold); doc.verified().unwrap() }