radicle-cob: port collaborative objects
This is a minimal port of the collaborative objects code from radicle-link[0]. A `CollaborativeObject` is made up of a `History` of `Change`s that are expected to be stored in a `git` backend. The library exposes a CRU series of functions `create`, `get`, `list`, and `update`. This implementation differs, to previous one, in that it does not assume any kind of identity system -- its only assumption is that identities have an identifier and can be addressed in the `git` database. This means that it does not require an `author` to be passed in and it only uses the `resource` (previously `authorizing_identity`) to store its content-address in the `Change`. It also foregoes any caching to simplify the port and not make any early optimisation assumptions. [0]: https://github.com/radicle-dev/radicle-link/tree/master/cob Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
parent
58c26e1c9d
commit
f3c4a53e1e
|
|
@ -30,27 +30,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.65"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.57"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
|
||||
checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -76,9 +64,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.5.16"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
|
||||
checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
|
|
@ -105,9 +93,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b"
|
||||
checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
|
|
@ -148,9 +136,9 @@ checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
|
|||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
|
|
@ -175,35 +163,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2s_simd"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
|
|
@ -216,12 +181,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
|
||||
|
||||
[[package]]
|
||||
name = "bloomy"
|
||||
version = "1.2.0"
|
||||
|
|
@ -243,9 +202,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.0"
|
||||
version = "3.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
|
||||
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
|
|
@ -261,9 +220,9 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
version = "1.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
|
@ -299,6 +258,16 @@ dependencies = [
|
|||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "1.9.3"
|
||||
|
|
@ -316,12 +285,6 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
|
|
@ -415,6 +378,50 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -496,12 +503,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ed25519-compact"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee9df587982575886a8682edcee11877894349a805f25629c27f63abe3e9ae8"
|
||||
checksum = "e18997d4604542d0736fae2c5ad6de987f0a50530cbcc14a7ce5a685328a252d"
|
||||
dependencies = [
|
||||
"ct-codecs",
|
||||
"getrandom 0.2.7",
|
||||
"getrandom 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -556,6 +563,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.24"
|
||||
|
|
@ -583,36 +596,36 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
|
||||
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
|
||||
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
|
||||
checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
|
||||
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.24"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
|
||||
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
|
|
@ -643,15 +656,26 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
|
||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-commit"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e5120c3ada84aa4bc54602ab0c02c223a5a569b5cf28ae59b2ac423d5b3f0e9"
|
||||
dependencies = [
|
||||
"git-trailers",
|
||||
"git2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-ref-format"
|
||||
version = "0.1.0"
|
||||
|
|
@ -684,6 +708,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-trailers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0934f57135449b88bea0e28efd80aab0c1b53692f8207c7e232086db824c7a8"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.15.0"
|
||||
|
|
@ -710,9 +744,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
|
||||
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
|
|
@ -793,9 +827,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
|||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.20"
|
||||
version = "0.14.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
|
||||
checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
|
@ -817,17 +851,28 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.50"
|
||||
version = "0.1.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
|
||||
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
|
|
@ -868,9 +913,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
||||
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
|
|
@ -890,12 +935,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
|
@ -913,9 +952,9 @@ checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.134"
|
||||
version = "0.2.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
|
|
@ -947,6 +986,15 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
|
|
@ -983,6 +1031,12 @@ version = "0.3.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.4"
|
||||
|
|
@ -994,9 +1048,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
|
||||
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
|
|
@ -1015,21 +1069,6 @@ dependencies = [
|
|||
"data-encoding-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multihash"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567122ab6492f49b59def14ecc36e13e64dca4188196dd0cd41f9f3f979f3df6"
|
||||
dependencies = [
|
||||
"blake2b_simd",
|
||||
"blake2s_simd",
|
||||
"digest 0.9.0",
|
||||
"sha-1",
|
||||
"sha2 0.9.9",
|
||||
"sha3",
|
||||
"unsigned-varint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nakamoto-net"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1054,6 +1093,16 @@ dependencies = [
|
|||
"socket2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.8.0"
|
||||
|
|
@ -1153,9 +1202,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
|
|
@ -1215,6 +1264,16 @@ version = "2.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.12"
|
||||
|
|
@ -1271,9 +1330,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.25"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "popol"
|
||||
|
|
@ -1316,9 +1375,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.46"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -1383,6 +1442,26 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radicle-cob"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ed25519-compact",
|
||||
"git-commit",
|
||||
"git-ref-format",
|
||||
"git-trailers",
|
||||
"git2",
|
||||
"log",
|
||||
"petgraph",
|
||||
"quickcheck",
|
||||
"radicle-crypto",
|
||||
"radicle-git-ext",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radicle-crypto"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1406,13 +1485,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "radicle-git-ext"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0906d0de75de1e19d595b9fa2a8a1478a8d75e057399e9795ffbe9b6864be4c"
|
||||
checksum = "25ed92fcf331d19b3110bbed8d3fe2bd99dc75f0059fd135727d24ef829de507"
|
||||
dependencies = [
|
||||
"git-ref-format",
|
||||
"git2",
|
||||
"multihash",
|
||||
"percent-encoding",
|
||||
"radicle-std-ext",
|
||||
"serde",
|
||||
|
|
@ -1560,7 +1638,7 @@ version = "0.6.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.7",
|
||||
"getrandom 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1661,6 +1739,12 @@ dependencies = [
|
|||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.10.0"
|
||||
|
|
@ -1689,18 +1773,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.145"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.145"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1709,9 +1793,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.85"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
|
||||
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
|
|
@ -1719,19 +1803,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.9"
|
||||
|
|
@ -1756,18 +1827,6 @@ dependencies = [
|
|||
"digest 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"digest 0.9.0",
|
||||
"keccak",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
|
|
@ -1836,9 +1895,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sqlite"
|
||||
version = "0.27.0"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2df8edd55685048550daaaf2be9024182f3523086cc86f7d50c136e55173e8c"
|
||||
checksum = "e66cb949f931ece6201d72bffad3f3601b94998a345793713dd13af70a77c185"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sqlite3-sys",
|
||||
|
|
@ -1904,9 +1963,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.101"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1945,6 +2004,15 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
|
|
@ -1987,13 +2055,31 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.15"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
|
||||
checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2153,7 +2239,7 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08aacc136419ba433b3f9bfd434a1bb62fe385328935e6ac11d952122b8a8cb"
|
||||
dependencies = [
|
||||
"time 0.3.15",
|
||||
"time 0.3.16",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
|
|
@ -2197,9 +2283,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
||||
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
|
|
@ -2210,18 +2296,18 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "unsigned-varint"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fdeedbf205afadfe39ae559b75c3240f24e257d0ca27e85f85cb82aa19ac35"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
|
|
@ -2349,6 +2435,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
@ -2357,46 +2452,60 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"radicle",
|
||||
"radicle-cob",
|
||||
"radicle-crypto",
|
||||
"radicle-httpd",
|
||||
"radicle-node",
|
||||
"radicle-tools",
|
||||
"radicle-ssh",
|
||||
"radicle-remote-helper",
|
||||
"radicle-httpd"
|
||||
"radicle-ssh",
|
||||
"radicle-tools",
|
||||
]
|
||||
default-members = [
|
||||
"radicle",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "radicle-cob"
|
||||
version = "0.1.0"
|
||||
authors = [
|
||||
"Alex Good <alex@memoryandthought.me>",
|
||||
"Fintan Halpenny <fintan.halpenny@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Library for implementing Radicle Collaborative Objects"
|
||||
keywords = ["radicle", "collaborative objects", "cob", "cobs"]
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
git-commit = { version = "0.2" }
|
||||
git-ref-format = { version = "0.1" }
|
||||
git-trailers = { version = "0.1" }
|
||||
log = { version = "0.4.17" }
|
||||
petgraph = { version = "0.5" }
|
||||
radicle-git-ext = { version = "0.2" }
|
||||
serde_json = { version = "1.0" }
|
||||
thiserror = { version = "1.0" }
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.15.0"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dependencies.radicle-crypto]
|
||||
path = "../radicle-crypto"
|
||||
version = "0.1"
|
||||
features = ["ssh"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dev-dependencies]
|
||||
ed25519-compact = { version = "1.0.12", features = ["pem"] }
|
||||
tempfile = { version = "3" }
|
||||
quickcheck = { version = "1", default-features = false }
|
||||
|
||||
[dev-dependencies.radicle-crypto]
|
||||
path = "../radicle-crypto"
|
||||
version = "0.1"
|
||||
features = ["test"]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub mod git;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub mod change;
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use git_commit::{self as commit, Commit};
|
||||
use git_ext::Oid;
|
||||
use git_trailers::OwnedTrailer;
|
||||
|
||||
use crate::{
|
||||
change::{self, store, Change},
|
||||
history::entry,
|
||||
signatures::{Signature, Signatures},
|
||||
trailers, HistoryType,
|
||||
};
|
||||
|
||||
const MANIFEST_BLOB_NAME: &str = "manifest";
|
||||
const CHANGE_BLOB_NAME: &str = "change";
|
||||
|
||||
pub mod error {
|
||||
use std::str::Utf8Error;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use git_ext::Oid;
|
||||
use git_trailers::Error as TrailerError;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::signatures::error::Signatures;
|
||||
use crate::trailers;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Create {
|
||||
#[error(transparent)]
|
||||
FromUtf8(#[from] FromUtf8Error),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Signer(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] Utf8Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Load {
|
||||
#[error(transparent)]
|
||||
Read(#[from] git_commit::error::Read),
|
||||
#[error(transparent)]
|
||||
Signatures(#[from] Signatures),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("a 'manifest' file was expected be found in '{0}'")]
|
||||
NoManifest(Oid),
|
||||
#[error("the 'manifest' found at '{0}' was not a blob")]
|
||||
ManifestIsNotBlob(Oid),
|
||||
#[error("the 'manifest' found at '{id}' was invalid: {err}")]
|
||||
InvalidManifest {
|
||||
id: Oid,
|
||||
#[source]
|
||||
err: serde_json::Error,
|
||||
},
|
||||
#[error("a 'change' file was expected be found in '{0}'")]
|
||||
NoChange(Oid),
|
||||
#[error("the 'change' found at '{0}' was not a blob")]
|
||||
ChangeNotBlob(Oid),
|
||||
#[error(transparent)]
|
||||
AuthorTrailer(#[from] trailers::error::InvalidAuthorTrailer),
|
||||
#[error(transparent)]
|
||||
ResourceTrailer(#[from] super::trailers::error::InvalidResourceTrailer),
|
||||
#[error("non utf-8 characters in commit message")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
#[error(transparent)]
|
||||
Trailer(#[from] TrailerError),
|
||||
}
|
||||
}
|
||||
|
||||
impl change::Storage for git2::Repository {
|
||||
type CreateError = error::Create;
|
||||
type LoadError = error::Load;
|
||||
|
||||
type ObjectId = Oid;
|
||||
type Author = Oid;
|
||||
type Resource = Oid;
|
||||
type Signatures = Signatures;
|
||||
|
||||
fn create<Signer>(
|
||||
&self,
|
||||
author: Option<Self::Author>,
|
||||
resource: Self::Resource,
|
||||
signer: &Signer,
|
||||
spec: store::Create<Self::ObjectId>,
|
||||
) -> Result<Change, Self::CreateError>
|
||||
where
|
||||
Signer: crypto::Signer,
|
||||
{
|
||||
let change::Create {
|
||||
typename,
|
||||
tips,
|
||||
message,
|
||||
contents,
|
||||
} = spec;
|
||||
let manifest = store::Manifest {
|
||||
typename,
|
||||
history_type: (&contents).into(),
|
||||
};
|
||||
|
||||
let revision = write_manifest(self, &manifest, &contents)?;
|
||||
let tree = self.find_tree(revision)?;
|
||||
|
||||
let signature = {
|
||||
let sig = signer.sign(revision.as_bytes());
|
||||
let key = signer.public_key();
|
||||
Signature::from((*key, sig))
|
||||
};
|
||||
|
||||
let id = write_commit(
|
||||
self,
|
||||
author,
|
||||
resource,
|
||||
tips,
|
||||
message,
|
||||
signature.clone(),
|
||||
tree,
|
||||
)?;
|
||||
Ok(Change {
|
||||
id,
|
||||
revision: revision.into(),
|
||||
signatures: signature.into(),
|
||||
author,
|
||||
resource,
|
||||
manifest,
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
fn load(&self, id: Self::ObjectId) -> Result<Change, Self::LoadError> {
|
||||
let commit = Commit::read(self, id.into())?;
|
||||
let (author, resource) = parse_trailers(commit.trailers())?;
|
||||
let signatures = Signatures::try_from(&commit)?;
|
||||
|
||||
let tree = self.find_tree(commit.tree())?;
|
||||
let manifest = load_manifest(self, &tree)?;
|
||||
let contents = load_contents(self, &tree, &manifest)?;
|
||||
|
||||
Ok(Change {
|
||||
id,
|
||||
revision: tree.id().into(),
|
||||
signatures,
|
||||
author,
|
||||
resource,
|
||||
manifest,
|
||||
contents,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_trailers<'a>(
|
||||
mut trailers: impl Iterator<Item = &'a OwnedTrailer>,
|
||||
) -> Result<(Option<Oid>, Oid), error::Load> {
|
||||
let (author, resource) = trailers.try_fold((None, None), |(author, resource), trailer| {
|
||||
match trailers::AuthorCommitTrailer::try_from(trailer) {
|
||||
Ok(trailer) => Ok((Some(trailer.oid().into()), resource)),
|
||||
Err(err) => match err {
|
||||
trailers::error::InvalidAuthorTrailer::NoTrailer
|
||||
| trailers::error::InvalidAuthorTrailer::NoValue => Ok((author, resource)),
|
||||
trailers::error::InvalidAuthorTrailer::WrongToken => {
|
||||
let resource = trailers::ResourceCommitTrailer::try_from(trailer)?;
|
||||
Ok((author, Some(resource.oid().into())))
|
||||
}
|
||||
err => Err(error::Load::from(err)),
|
||||
},
|
||||
}
|
||||
})?;
|
||||
let resource = resource
|
||||
.ok_or_else(|| error::Load::from(trailers::error::InvalidResourceTrailer::NoTrailer))?;
|
||||
Ok((author, resource))
|
||||
}
|
||||
|
||||
fn load_manifest(
|
||||
repo: &git2::Repository,
|
||||
tree: &git2::Tree,
|
||||
) -> Result<store::Manifest, error::Load> {
|
||||
let manifest_tree_entry = tree
|
||||
.get_name(MANIFEST_BLOB_NAME)
|
||||
.ok_or_else(|| error::Load::NoManifest(tree.id().into()))?;
|
||||
let manifest_object = manifest_tree_entry.to_object(repo)?;
|
||||
let manifest_blob = manifest_object
|
||||
.as_blob()
|
||||
.ok_or_else(|| error::Load::ManifestIsNotBlob(tree.id().into()))?;
|
||||
serde_json::from_slice(manifest_blob.content()).map_err(|err| error::Load::InvalidManifest {
|
||||
id: tree.id().into(),
|
||||
err,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_contents(
|
||||
repo: &git2::Repository,
|
||||
tree: &git2::Tree,
|
||||
manifest: &store::Manifest,
|
||||
) -> Result<entry::Contents, error::Load> {
|
||||
Ok(match manifest.history_type {
|
||||
HistoryType::Automerge => {
|
||||
let contents_tree_entry = tree
|
||||
.get_name(CHANGE_BLOB_NAME)
|
||||
.ok_or_else(|| error::Load::NoChange(tree.id().into()))?;
|
||||
let contents_object = contents_tree_entry.to_object(repo)?;
|
||||
let contents_blob = contents_object
|
||||
.as_blob()
|
||||
.ok_or_else(|| error::Load::ChangeNotBlob(tree.id().into()))?;
|
||||
entry::Contents::automerge(contents_blob.content())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn write_commit<O>(
|
||||
repo: &git2::Repository,
|
||||
author: Option<O>,
|
||||
resource: O,
|
||||
tips: Vec<O>,
|
||||
message: String,
|
||||
signature: Signature,
|
||||
tree: git2::Tree,
|
||||
) -> Result<Oid, error::Create>
|
||||
where
|
||||
O: AsRef<git2::Oid>,
|
||||
{
|
||||
let author = author.map(|author| *author.as_ref());
|
||||
let resource = *resource.as_ref();
|
||||
|
||||
let mut parents = tips.iter().map(|o| *o.as_ref()).collect::<Vec<_>>();
|
||||
parents.push(resource);
|
||||
parents.extend(author);
|
||||
|
||||
let mut trailers: Vec<OwnedTrailer> =
|
||||
vec![trailers::ResourceCommitTrailer::from(resource).into()];
|
||||
trailers.extend(author.map(|author| trailers::AuthorCommitTrailer::from(author).into()));
|
||||
|
||||
{
|
||||
let author = repo.signature()?;
|
||||
let mut headers = commit::Headers::new();
|
||||
headers.push(
|
||||
"gpgsig",
|
||||
&String::from_utf8(crypto::ssh::ExtendedSignature::from(signature).to_armored())?,
|
||||
);
|
||||
let author = commit::Author::try_from(&author)?;
|
||||
|
||||
let commit = Commit::new(
|
||||
tree.id(),
|
||||
parents,
|
||||
author.clone(),
|
||||
author,
|
||||
headers,
|
||||
message,
|
||||
trailers,
|
||||
);
|
||||
commit
|
||||
.write(repo)
|
||||
.map(Oid::from)
|
||||
.map_err(error::Create::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_manifest(
|
||||
repo: &git2::Repository,
|
||||
manifest: &store::Manifest,
|
||||
contents: &entry::Contents,
|
||||
) -> Result<git2::Oid, git2::Error> {
|
||||
let mut tb = repo.treebuilder(None)?;
|
||||
// SAFETY: we're serializing to an in memory buffer so the only source of
|
||||
// errors here is a programming error, which we can't recover from
|
||||
let serialized_manifest = serde_json::to_vec(manifest).unwrap();
|
||||
let manifest_oid = repo.blob(&serialized_manifest)?;
|
||||
tb.insert(
|
||||
MANIFEST_BLOB_NAME,
|
||||
manifest_oid,
|
||||
git2::FileMode::Blob.into(),
|
||||
)?;
|
||||
|
||||
let change_blob = repo.blob(contents.as_ref())?;
|
||||
tb.insert(CHANGE_BLOB_NAME, change_blob, git2::FileMode::Blob.into())?;
|
||||
|
||||
tb.write()
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use git_ext::Oid;
|
||||
|
||||
pub mod store;
|
||||
pub use store::{Create, Storage};
|
||||
|
||||
use crate::signatures::Signatures;
|
||||
|
||||
/// A single change in the change graph. The layout of changes in the repository
|
||||
/// is specified in the RFC (docs/rfc/0662-collaborative-objects.adoc)
|
||||
/// under "Change Commits".
|
||||
pub type Change = store::Change<Oid, Oid, Oid, Signatures>;
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
history::{Contents, HistoryType},
|
||||
signatures, TypeName,
|
||||
};
|
||||
|
||||
pub trait Storage {
|
||||
type CreateError: Error + Send + Sync + 'static;
|
||||
type LoadError: Error + Send + Sync + 'static;
|
||||
|
||||
type ObjectId;
|
||||
type Author;
|
||||
type Resource;
|
||||
type Signatures;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn create<Signer>(
|
||||
&self,
|
||||
author: Option<Self::Author>,
|
||||
authority: Self::Resource,
|
||||
signer: &Signer,
|
||||
spec: Create<Self::ObjectId>,
|
||||
) -> Result<
|
||||
Change<Self::Author, Self::Resource, Self::ObjectId, Self::Signatures>,
|
||||
Self::CreateError,
|
||||
>
|
||||
where
|
||||
Signer: crypto::Signer;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn load(
|
||||
&self,
|
||||
id: Self::ObjectId,
|
||||
) -> Result<
|
||||
Change<Self::Author, Self::Resource, Self::ObjectId, Self::Signatures>,
|
||||
Self::LoadError,
|
||||
>;
|
||||
}
|
||||
|
||||
pub struct Create<Id> {
|
||||
pub typename: TypeName,
|
||||
pub tips: Vec<Id>,
|
||||
pub message: String,
|
||||
pub contents: Contents,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Change<Author, Resource, Id, Signatures> {
|
||||
/// The content address of the `Change` itself.
|
||||
pub id: Id,
|
||||
/// The content address of the tree of the `Change`.
|
||||
pub revision: Id,
|
||||
/// The cryptographic signatures and their public keys of the
|
||||
/// authors.
|
||||
pub signatures: Signatures,
|
||||
/// The author of this change. The `Author` is expected to be a
|
||||
/// content address to look up the identity of the author.
|
||||
pub author: Option<Author>,
|
||||
/// The parent resource that this change lives under. For example,
|
||||
/// this change could be for a patch of a project.
|
||||
pub resource: Resource,
|
||||
/// The manifest describing the type of object as well as the type
|
||||
/// of history for this `Change`.
|
||||
pub manifest: Manifest,
|
||||
/// The contents that describe `Change`.
|
||||
pub contents: Contents,
|
||||
}
|
||||
|
||||
impl<Author, Resource, Id, S> fmt::Display for Change<Author, Resource, Id, S>
|
||||
where
|
||||
Id: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Change {{ id: {} }}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Author, Resource, Id, Signatures> Change<Author, Resource, Id, Signatures> {
|
||||
pub fn id(&self) -> &Id {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn author(&self) -> &Option<Author> {
|
||||
&self.author
|
||||
}
|
||||
|
||||
pub fn typename(&self) -> &TypeName {
|
||||
&self.manifest.typename
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> &Contents {
|
||||
&self.contents
|
||||
}
|
||||
|
||||
pub fn resource(&self) -> &Resource {
|
||||
&self.resource
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, R, Id> Change<A, R, Id, signatures::Signatures>
|
||||
where
|
||||
Id: AsRef<[u8]>,
|
||||
{
|
||||
pub fn valid_signatures(&self) -> bool {
|
||||
self.signatures
|
||||
.iter()
|
||||
.all(|(key, sig)| key.verify(self.revision.as_ref(), sig).is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
/// The name given to the type of collaborative object.
|
||||
pub typename: TypeName,
|
||||
/// The type of history for the collaborative oject.
|
||||
pub history_type: HistoryType,
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, BTreeSet, HashMap},
|
||||
convert::TryInto,
|
||||
};
|
||||
|
||||
use git_ext::Oid;
|
||||
use petgraph::{
|
||||
visit::{EdgeRef, Topo, Walker},
|
||||
EdgeDirection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
change, object, signatures::Signatures, Change, CollaborativeObject, ObjectId, TypeName,
|
||||
};
|
||||
|
||||
mod evaluation;
|
||||
use evaluation::evaluate;
|
||||
|
||||
/// The graph of changes for a particular collaborative object
|
||||
pub(super) struct ChangeGraph {
|
||||
object_id: ObjectId,
|
||||
graph: petgraph::Graph<Change, ()>,
|
||||
}
|
||||
|
||||
impl ChangeGraph {
|
||||
/// Load the change graph from the underlying git store by walking
|
||||
/// backwards from references to the object
|
||||
pub(crate) fn load<'a, S>(
|
||||
storage: &S,
|
||||
tip_refs: impl Iterator<Item = &'a object::Reference> + 'a,
|
||||
typename: &TypeName,
|
||||
oid: &ObjectId,
|
||||
) -> Option<ChangeGraph>
|
||||
where
|
||||
S: change::Storage<ObjectId = Oid, Author = Oid, Resource = Oid, Signatures = Signatures>,
|
||||
{
|
||||
log::info!("loading object '{}' '{}'", typename, oid);
|
||||
let mut builder = GraphBuilder::default();
|
||||
let mut edges_to_process: Vec<(object::Commit, Oid)> = Vec::new();
|
||||
|
||||
// Populate the initial set of edges_to_process from the refs we have
|
||||
for reference in tip_refs {
|
||||
log::trace!("loading object from reference '{}'", reference.name);
|
||||
match storage.load(reference.target.id) {
|
||||
Ok(change) => {
|
||||
let commit = reference.target.clone();
|
||||
let new_edges = builder.add_change(commit, change);
|
||||
edges_to_process.extend(new_edges);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"unable to load change from reference '{}->{}', error '{}'",
|
||||
reference.name,
|
||||
reference.target.id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process edges until we have no more to process
|
||||
while let Some((parent_commit, child_commit_id)) = edges_to_process.pop() {
|
||||
log::trace!(
|
||||
"loading change parent='{}', child='{}'",
|
||||
parent_commit.id,
|
||||
child_commit_id
|
||||
);
|
||||
match storage.load(parent_commit.id) {
|
||||
Ok(change) => {
|
||||
let parent_commit_id = parent_commit.id;
|
||||
let new_edges = builder.add_change(parent_commit, change);
|
||||
edges_to_process.extend(new_edges);
|
||||
builder.add_edge(child_commit_id, parent_commit_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"unable to load changetree from commit '{}', error '{}'",
|
||||
parent_commit.id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.build(*oid)
|
||||
}
|
||||
|
||||
/// Given a graph evaluate it to produce a collaborative object. This will
|
||||
/// filter out branches of the graph which do not have valid signatures,
|
||||
/// or which do not have permission to make a change, or which make a
|
||||
/// change which invalidates the schema of the object
|
||||
pub(crate) fn evaluate(&self) -> CollaborativeObject {
|
||||
let mut roots: Vec<petgraph::graph::NodeIndex<u32>> = self
|
||||
.graph
|
||||
.externals(petgraph::Direction::Incoming)
|
||||
.collect();
|
||||
roots.sort();
|
||||
// This is okay because we check that the graph has a root node in
|
||||
// GraphBuilder::build
|
||||
let root = roots.first().unwrap();
|
||||
let typename = {
|
||||
let first_node = &self.graph[*root];
|
||||
first_node.typename().clone()
|
||||
};
|
||||
let topo = Topo::new(&self.graph);
|
||||
let items = topo.iter(&self.graph).map(|idx| {
|
||||
let node = &self.graph[idx];
|
||||
let outgoing_edges = self.graph.edges_directed(idx, EdgeDirection::Outgoing);
|
||||
let child_commits = outgoing_edges
|
||||
.map(|e| *self.graph[e.target()].id())
|
||||
.collect::<Vec<_>>();
|
||||
(node, child_commits)
|
||||
});
|
||||
let history = {
|
||||
let root_change = &self.graph[*root];
|
||||
evaluate(*root_change.id(), items)
|
||||
};
|
||||
CollaborativeObject {
|
||||
typename,
|
||||
history,
|
||||
id: self.object_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the tips of the collaborative object
|
||||
pub(crate) fn tips(&self) -> BTreeSet<Oid> {
|
||||
self.graph
|
||||
.externals(petgraph::Direction::Outgoing)
|
||||
.map(|n| {
|
||||
let change = &self.graph[n];
|
||||
*change.id()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn number_of_nodes(&self) -> u64 {
|
||||
self.graph.node_count().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn graphviz(&self) -> String {
|
||||
let for_display = self.graph.map(|_ix, n| n.to_string(), |_ix, _e| "");
|
||||
petgraph::dot::Dot::new(&for_display).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct GraphBuilder {
|
||||
node_indices: HashMap<Oid, petgraph::graph::NodeIndex<u32>>,
|
||||
graph: petgraph::Graph<Change, ()>,
|
||||
}
|
||||
|
||||
impl Default for GraphBuilder {
|
||||
fn default() -> Self {
|
||||
GraphBuilder {
|
||||
node_indices: HashMap::new(),
|
||||
graph: petgraph::graph::Graph::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphBuilder {
|
||||
/// Add a change to the graph which we are building up, returning any edges
|
||||
/// corresponding to the parents of this node in the change graph
|
||||
fn add_change(
|
||||
&mut self,
|
||||
commit: object::Commit,
|
||||
change: Change,
|
||||
) -> impl Iterator<Item = (object::Commit, Oid)> + '_ {
|
||||
let author_commit = *change.author();
|
||||
let resource_commit = *change.resource();
|
||||
let commit_id = commit.id;
|
||||
if let Entry::Vacant(e) = self.node_indices.entry(commit_id) {
|
||||
let ix = self.graph.add_node(change);
|
||||
e.insert(ix);
|
||||
}
|
||||
commit.parents.into_iter().filter_map(move |parent| {
|
||||
if Some(parent.id) != author_commit
|
||||
&& parent.id != resource_commit
|
||||
&& !self.has_edge(parent.id, commit_id)
|
||||
{
|
||||
Some((parent, commit_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn has_edge(&mut self, parent_id: Oid, child_id: Oid) -> bool {
|
||||
let parent_ix = self.node_indices.get(&parent_id);
|
||||
let child_ix = self.node_indices.get(&child_id);
|
||||
match (parent_ix, child_ix) {
|
||||
(Some(parent_ix), Some(child_ix)) => self.graph.contains_edge(*parent_ix, *child_ix),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_edge(&mut self, child: Oid, parent: Oid) {
|
||||
// This panics if the child or parent ids are not in the graph already
|
||||
let child_id = self
|
||||
.node_indices
|
||||
.get(&child)
|
||||
.expect("BUG: child id expected to be in graph");
|
||||
let parent_id = self
|
||||
.node_indices
|
||||
.get(&parent)
|
||||
.expect("BUG: parent id expected to in graph");
|
||||
self.graph.update_edge(*parent_id, *child_id, ());
|
||||
}
|
||||
|
||||
fn build(self, object_id: ObjectId) -> Option<ChangeGraph> {
|
||||
if self
|
||||
.graph
|
||||
.externals(petgraph::Direction::Incoming)
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
Some(ChangeGraph {
|
||||
object_id,
|
||||
graph: self.graph,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{collections::HashMap, ops::ControlFlow};
|
||||
|
||||
use git_ext::Oid;
|
||||
|
||||
use crate::{change::Change, history, pruning_fold};
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// If the change corresponding to the root OID is not in `items`
|
||||
pub fn evaluate<'b>(
|
||||
root: Oid,
|
||||
items: impl Iterator<Item = (&'b Change, Vec<Oid>)>,
|
||||
) -> history::History {
|
||||
let entries = pruning_fold::pruning_fold(
|
||||
HashMap::new(),
|
||||
items.map(|(change, children)| ChangeWithChildren {
|
||||
change,
|
||||
child_commits: children,
|
||||
}),
|
||||
|mut entries, c| match evaluate_change(c.change, &c.child_commits) {
|
||||
Err(RejectionReason::InvalidSignatures) => {
|
||||
log::warn!(
|
||||
"rejecting change '{}' because its signatures were invalid",
|
||||
c.change.id(),
|
||||
);
|
||||
ControlFlow::Break(entries)
|
||||
}
|
||||
Ok(entry) => {
|
||||
log::trace!("change '{}' accepted", c.change.id());
|
||||
entries.insert((*c.change.id()).into(), entry);
|
||||
ControlFlow::Continue(entries)
|
||||
}
|
||||
},
|
||||
);
|
||||
// SAFETY: The caller must guarantee that `root` is in `items`
|
||||
history::History::new(root, entries).unwrap()
|
||||
}
|
||||
|
||||
fn evaluate_change(
|
||||
change: &Change,
|
||||
child_commits: &[Oid],
|
||||
) -> Result<history::Entry, RejectionReason> {
|
||||
// Check the change signatures are valid
|
||||
if !change.valid_signatures() {
|
||||
return Err(RejectionReason::InvalidSignatures);
|
||||
};
|
||||
|
||||
Ok(history::Entry::new(
|
||||
*change.id(),
|
||||
*change.author(),
|
||||
change.resource,
|
||||
child_commits.iter().cloned(),
|
||||
change.contents().clone(),
|
||||
))
|
||||
}
|
||||
|
||||
struct ChangeWithChildren<'a> {
|
||||
change: &'a Change,
|
||||
child_commits: Vec<Oid>,
|
||||
}
|
||||
|
||||
impl<'a> pruning_fold::GraphNode for ChangeWithChildren<'a> {
|
||||
type Id = Oid;
|
||||
|
||||
fn id(&self) -> &Self::Id {
|
||||
self.change.id()
|
||||
}
|
||||
|
||||
fn child_ids(&self) -> &[Self::Id] {
|
||||
&self.child_commits
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RejectionReason {
|
||||
InvalidSignatures,
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
ops::ControlFlow,
|
||||
};
|
||||
|
||||
use git_ext::Oid;
|
||||
use petgraph::visit::Walker as _;
|
||||
|
||||
use crate::pruning_fold;
|
||||
|
||||
pub mod entry;
|
||||
pub use entry::{Contents, Entry, EntryId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum HistoryType {
|
||||
Automerge,
|
||||
}
|
||||
|
||||
/// The DAG of changes making up the history of a collaborative object.
|
||||
///
|
||||
/// The `Author` represents the content address for the author of the change entry.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct History {
|
||||
graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>,
|
||||
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>,
|
||||
}
|
||||
|
||||
impl PartialEq for History {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.tips() == other.tips()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for History {}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CreateError {
|
||||
#[error("no entry for the root ID in the entries")]
|
||||
MissingRoot,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub(crate) fn new_from_root<Id>(
|
||||
id: Id,
|
||||
author: Option<Oid>,
|
||||
resource: Oid,
|
||||
contents: Contents,
|
||||
) -> Self
|
||||
where
|
||||
Id: Into<EntryId>,
|
||||
{
|
||||
let id = id.into();
|
||||
let root_entry = Entry {
|
||||
id,
|
||||
author,
|
||||
resource,
|
||||
children: vec![],
|
||||
contents,
|
||||
};
|
||||
let mut entries = HashMap::new();
|
||||
entries.insert(id, root_entry.clone());
|
||||
let NewGraph { graph, indices } = create_petgraph(&root_entry.id, &entries);
|
||||
Self { graph, indices }
|
||||
}
|
||||
|
||||
pub fn new<Id>(root: Id, entries: HashMap<EntryId, Entry>) -> Result<Self, CreateError>
|
||||
where
|
||||
Id: Into<EntryId>,
|
||||
{
|
||||
let root = root.into();
|
||||
if !entries.contains_key(&root) {
|
||||
Err(CreateError::MissingRoot)
|
||||
} else {
|
||||
let NewGraph { graph, indices } = create_petgraph(&root, &entries);
|
||||
Ok(Self { graph, indices })
|
||||
}
|
||||
}
|
||||
|
||||
/// A topological (parents before children) traversal of the dependency
|
||||
/// graph of this history. This is analagous to
|
||||
/// [`std::iter::Iterator::fold`] in that it folds every change into an
|
||||
/// accumulator value of type `A`. However, unlike `fold` the function `f`
|
||||
/// may prune branches from the dependency graph by returning
|
||||
/// `ControlFlow::Break`.
|
||||
pub fn traverse<F, A>(&self, init: A, f: F) -> A
|
||||
where
|
||||
F: for<'r> FnMut(A, &'r Entry) -> ControlFlow<A, A>,
|
||||
{
|
||||
let topo = petgraph::visit::Topo::new(&self.graph);
|
||||
#[allow(clippy::let_and_return)]
|
||||
let items = topo.iter(&self.graph).map(|idx| {
|
||||
let node = &self.graph[idx];
|
||||
node
|
||||
});
|
||||
pruning_fold::pruning_fold(init, items, f)
|
||||
}
|
||||
|
||||
pub(crate) fn tips(&self) -> BTreeSet<Oid> {
|
||||
self.graph
|
||||
.externals(petgraph::Direction::Outgoing)
|
||||
.map(|n| {
|
||||
let entry = &self.graph[n];
|
||||
(*entry.id()).into()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn extend<Id>(
|
||||
&mut self,
|
||||
new_id: Id,
|
||||
new_author: Option<Oid>,
|
||||
new_resource: Oid,
|
||||
new_contents: Contents,
|
||||
) where
|
||||
Id: Into<EntryId>,
|
||||
{
|
||||
let tips = self.tips();
|
||||
let new_id = new_id.into();
|
||||
let new_entry = Entry::new(
|
||||
new_id,
|
||||
new_author,
|
||||
new_resource,
|
||||
std::iter::empty::<git2::Oid>(),
|
||||
new_contents,
|
||||
);
|
||||
let new_ix = self.graph.add_node(new_entry);
|
||||
for tip in tips {
|
||||
let tip_ix = self.indices.get(&tip.into()).unwrap();
|
||||
self.graph.update_edge(*tip_ix, new_ix, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NewGraph {
|
||||
graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>,
|
||||
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>,
|
||||
}
|
||||
|
||||
fn create_petgraph<'a>(root: &'a EntryId, entries: &'a HashMap<EntryId, Entry>) -> NewGraph {
|
||||
let mut graph = petgraph::Graph::new();
|
||||
let mut indices = HashMap::<EntryId, petgraph::graph::NodeIndex<u32>>::new();
|
||||
let root = entries.get(root).unwrap().clone();
|
||||
let root_ix = graph.add_node(root.clone());
|
||||
indices.insert(root.id, root_ix);
|
||||
let mut to_process = vec![root];
|
||||
while let Some(entry) = to_process.pop() {
|
||||
let entry_ix = indices[&entry.id];
|
||||
for child_id in entry.children {
|
||||
let child = entries[&child_id].clone();
|
||||
let child_ix = graph.add_node(child.clone());
|
||||
indices.insert(child.id, child_ix);
|
||||
graph.update_edge(entry_ix, child_ix, ());
|
||||
to_process.push(child.clone());
|
||||
}
|
||||
}
|
||||
NewGraph { graph, indices }
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use git_ext::Oid;
|
||||
|
||||
use crate::pruning_fold;
|
||||
|
||||
use super::HistoryType;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Eq)]
|
||||
pub enum Contents {
|
||||
Automerge(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Contents {
|
||||
pub fn automerge(bytes: &[u8]) -> Self {
|
||||
Self::Automerge(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Contents> for HistoryType {
|
||||
fn from(c: &Contents) -> Self {
|
||||
match c {
|
||||
Contents::Automerge(..) => HistoryType::Automerge,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Contents {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Automerge(bytes) => bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique identifier for a history entry.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
pub struct EntryId(Oid);
|
||||
|
||||
impl From<git2::Oid> for EntryId {
|
||||
fn from(id: git2::Oid) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oid> for EntryId {
|
||||
fn from(id: Oid) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntryId> for Oid {
|
||||
fn from(EntryId(id): EntryId) -> Self {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry in the dependency graph for a change
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Entry {
|
||||
/// The identifier for this entry
|
||||
pub(super) id: EntryId,
|
||||
/// The content-address for this entry's author.
|
||||
pub(super) author: Option<Oid>,
|
||||
/// The content-address for the resource this entry lives under.
|
||||
pub(super) resource: Oid,
|
||||
/// The child entries for this entry.
|
||||
pub(super) children: Vec<EntryId>,
|
||||
/// The contents of this entry.
|
||||
pub(super) contents: Contents,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn new<Id1, Id2, ChildIds>(
|
||||
id: Id1,
|
||||
author: Option<Oid>,
|
||||
resource: Oid,
|
||||
children: ChildIds,
|
||||
contents: Contents,
|
||||
) -> Self
|
||||
where
|
||||
Id1: Into<EntryId>,
|
||||
Id2: Into<EntryId>,
|
||||
ChildIds: IntoIterator<Item = Id2>,
|
||||
{
|
||||
Self {
|
||||
id: id.into(),
|
||||
author,
|
||||
resource,
|
||||
children: children.into_iter().map(|id| id.into()).collect(),
|
||||
contents,
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the changes this change depends on
|
||||
pub fn children(&self) -> impl Iterator<Item = &EntryId> {
|
||||
self.children.iter()
|
||||
}
|
||||
|
||||
/// The `Oid` of the resource this change lives under.
|
||||
pub fn resource(&self) -> Oid {
|
||||
self.resource
|
||||
}
|
||||
|
||||
/// The `Oid` of the author that made this change.
|
||||
pub fn author(&self) -> &Option<Oid> {
|
||||
&self.author
|
||||
}
|
||||
|
||||
/// The contents of this change
|
||||
pub fn contents(&self) -> &Contents {
|
||||
&self.contents
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &EntryId {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl pruning_fold::GraphNode for Entry {
|
||||
type Id = EntryId;
|
||||
|
||||
fn id(&self) -> &Self::Id {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn child_ids(&self) -> &[Self::Id] {
|
||||
&self.children
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use git_ext::Oid;
|
||||
|
||||
/// An [`Identity`] represents a content addressed identity
|
||||
/// (i.e. expected to be stored in a git backend).
|
||||
///
|
||||
/// It should have:
|
||||
/// * A delegate system
|
||||
/// * A content addressable identifier
|
||||
/// * A unique, stable identifier
|
||||
pub trait Identity {
|
||||
type Identifier;
|
||||
|
||||
/// Confirm that the given [`crypto::PublicKey`] is a delegate for
|
||||
/// the identity.
|
||||
fn is_delegate(&self, delegation: &crypto::PublicKey) -> bool;
|
||||
|
||||
/// Provide the content address for the given identity. This is
|
||||
/// expected to be the latest address for the identity at the time
|
||||
/// of use.
|
||||
fn content_id(&self) -> Oid;
|
||||
|
||||
/// The unique, stable identifier for the identity.
|
||||
fn identifier(&self) -> Self::Identifier;
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
//! # Collaborative Objects
|
||||
//!
|
||||
//! Collaborative objects are graphs of CRDTs. The current CRDTs that
|
||||
//! is intended to be used are specifically [automerge] CRDTs.
|
||||
//!
|
||||
//! The initial design is proposed at [RFC-0662], and this
|
||||
//! implementation keeps to most of its design principle.
|
||||
//!
|
||||
//! ## Basic Types
|
||||
//!
|
||||
//! The basic types that are found in `radicle-cob` are:
|
||||
//! * [`CollaborativeObject`] -- the computed object itself.
|
||||
//! * [`ObjectId`] -- the content-address for a single collaborative
|
||||
//! object.
|
||||
//! * [`TypeName`] -- the name for a collection of collaborative objects.
|
||||
//! * [`History`] -- the traversable history of the changes made to
|
||||
//! a single collaborative object.
|
||||
//!
|
||||
//! ## CRU Interface (No Delete)
|
||||
//!
|
||||
//! The main entry for manipulating [`CollaborativeObject`]s is by
|
||||
//! using the CRU like functions:
|
||||
//! * [`create`]
|
||||
//! * [`get`]
|
||||
//! * [`list`]
|
||||
//! * [`update`]
|
||||
//!
|
||||
//! ## Storage
|
||||
//!
|
||||
//! The storing of collaborative objects is based on a git
|
||||
//! backend. The previously mentioned functions all accept a [`Store`]
|
||||
//! as parameter. The `Store` itself is an accumulation of different
|
||||
//! storage capabilities:
|
||||
//! * [`object::Storage`]
|
||||
//! * [`change::Storage`] -- **Note**: there is already an
|
||||
//! implementation for this for [`git2::Repository`] for convenience.
|
||||
//!
|
||||
//! The `Store` also takes a generic parameter to indicate the type of
|
||||
//! resource the collaborative object is living inside, for example a
|
||||
//! software project. This `Resource` must also implement
|
||||
//! [`identity::Identity`] to allow the internal logic to reference
|
||||
//! the resource's content-address in `git` as well as the stable
|
||||
//! identifier used for the resource.
|
||||
//!
|
||||
//! ## History Traversal
|
||||
//!
|
||||
//! The [`History`] of a [`CollaborativeObject`] -- accessed via
|
||||
//! [`CollaborativeObject::history`] -- has a method
|
||||
//! [`History::traverse`] which provides a way of inspecting each
|
||||
//! [`Entry`] and building up a final value.
|
||||
//!
|
||||
//! This mechanism would be used in tandem with [automerge] to load an
|
||||
//! automerge document and deserialize into an application defined
|
||||
//! object.
|
||||
//!
|
||||
//! This traversal is also the point at which the [`Entry::author`]
|
||||
//! and [`Entry::resource`] can be retrieved to apply any kind of
|
||||
//! filtering logic. For example, a specific `author`'s change may be
|
||||
//! egregious, spouting terrible libel about Radicle. It is at this
|
||||
//! point that the `author`'s change can be filtered out from the
|
||||
//! final product of the traversal.
|
||||
//!
|
||||
//! [automerge]: https://automerge.org
|
||||
//! [RFC-0662]: https://github.com/radicle-dev/radicle-link/blob/master/docs/rfc/0662-collaborative-objects.adoc
|
||||
|
||||
extern crate radicle_crypto as crypto;
|
||||
extern crate radicle_git_ext as git_ext;
|
||||
|
||||
mod backend;
|
||||
pub use backend::git;
|
||||
|
||||
mod change_graph;
|
||||
mod trailers;
|
||||
|
||||
pub mod change;
|
||||
pub use change::Change;
|
||||
|
||||
pub mod identity;
|
||||
|
||||
pub mod history;
|
||||
pub use history::{Contents, Entry, History, HistoryType};
|
||||
|
||||
mod pruning_fold;
|
||||
|
||||
pub mod signatures;
|
||||
use signatures::Signatures;
|
||||
|
||||
pub mod type_name;
|
||||
pub use type_name::TypeName;
|
||||
|
||||
pub mod object;
|
||||
pub use object::{create, get, info, list, update, CollaborativeObject, Create, ObjectId, Update};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// The `Store` is an aggregation of the different types of storage
|
||||
/// traits required for editing [`CollaborativeObject`]s.
|
||||
///
|
||||
/// The backing store being used is expected to be a `git` backend.
|
||||
///
|
||||
/// To get started using this trait, you must implement the following
|
||||
/// for the specific `git` storage:
|
||||
///
|
||||
/// * [`object::Storage`]
|
||||
/// * [`identity::Identity`] for `Resource`
|
||||
///
|
||||
/// **Note**: [`change::Storage`] is already implemented for
|
||||
/// [`git2::Repository`]. It is expected that the underlying storage
|
||||
/// for `object::Storage` will also be `git2::Repository`, but if not
|
||||
/// please open an issue to change the definition of `Store` :)
|
||||
pub trait Store<Resource>
|
||||
where
|
||||
Resource: identity::Identity,
|
||||
Self: object::Storage<Identifier = Resource::Identifier>
|
||||
+ change::Storage<
|
||||
CreateError = git::change::error::Create,
|
||||
LoadError = git::change::error::Load,
|
||||
ObjectId = git_ext::Oid,
|
||||
Author = git_ext::Oid,
|
||||
Resource = git_ext::Oid,
|
||||
Signatures = Signatures,
|
||||
>,
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{convert::TryFrom as _, fmt, str::FromStr};
|
||||
|
||||
use git_ext::Oid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod collaboration;
|
||||
pub use collaboration::{create, get, info, list, update, CollaborativeObject, Create, Update};
|
||||
|
||||
pub mod storage;
|
||||
pub use storage::{Commit, Objects, Reference, Storage};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseObjectId {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
|
||||
/// The id of an object
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ObjectId(Oid);
|
||||
|
||||
impl FromStr for ObjectId {
|
||||
type Err = ParseObjectId;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let oid = Oid::try_from(s.as_bytes())?;
|
||||
Ok(ObjectId(oid))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oid> for ObjectId {
|
||||
fn from(oid: Oid) -> Self {
|
||||
ObjectId(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Oid> for ObjectId {
|
||||
fn from(oid: &Oid) -> Self {
|
||||
(*oid).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git2::Oid> for ObjectId {
|
||||
fn from(oid: git2::Oid) -> Self {
|
||||
Oid::from(oid).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git2::Oid> for ObjectId {
|
||||
fn from(oid: &git2::Oid) -> Self {
|
||||
ObjectId(Oid::from(*oid))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ObjectId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ObjectId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(self.0.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ObjectId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw = <&[u8]>::deserialize(deserializer)?;
|
||||
let oid = Oid::try_from(raw).map_err(serde::de::Error::custom)?;
|
||||
Ok(ObjectId(oid))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use git_ext::Oid;
|
||||
|
||||
use crate::{change, identity::Identity, Contents, History, ObjectId, TypeName};
|
||||
|
||||
pub mod error;
|
||||
|
||||
mod create;
|
||||
pub use create::{create, Create};
|
||||
|
||||
mod get;
|
||||
pub use get::get;
|
||||
|
||||
pub mod info;
|
||||
|
||||
mod list;
|
||||
pub use list::list;
|
||||
|
||||
mod update;
|
||||
pub use update::{update, Update};
|
||||
|
||||
/// A collaborative object
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CollaborativeObject {
|
||||
/// The typename of this object
|
||||
pub(crate) typename: TypeName,
|
||||
/// The CRDT history we know about for this object
|
||||
pub(crate) history: History,
|
||||
/// The id of the object
|
||||
pub(crate) id: ObjectId,
|
||||
}
|
||||
|
||||
impl CollaborativeObject {
|
||||
pub fn history(&self) -> &History {
|
||||
&self.history
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &ObjectId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn typename(&self) -> &TypeName {
|
||||
&self.typename
|
||||
}
|
||||
|
||||
fn tips(&self) -> BTreeSet<Oid> {
|
||||
self.history.tips().into_iter().map(Oid::from).collect()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use crate::Store;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The metadata required for creating a new [`CollaborativeObject`].
|
||||
pub struct Create<Author> {
|
||||
/// The identity of the author for this object's first change.
|
||||
pub author: Option<Author>,
|
||||
/// The CRDT history to initialize this object with.
|
||||
pub contents: Contents,
|
||||
/// The typename for this object.
|
||||
pub typename: TypeName,
|
||||
/// The message to add when creating this object.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl<Author> Create<Author> {
|
||||
fn create_spec(&self) -> change::Create<git_ext::Oid> {
|
||||
change::Create {
|
||||
typename: self.typename.clone(),
|
||||
tips: Vec::new(),
|
||||
message: self.message.clone(),
|
||||
contents: self.contents.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`CollaborativeObject`].
|
||||
///
|
||||
/// The `storage` is the backing storage for storing
|
||||
/// [`crate::Change`]s at content-addressable locations. Please see
|
||||
/// [`Store`] for further information.
|
||||
///
|
||||
/// The `signer` is expected to be a cryptographic signing key. This
|
||||
/// ensures that the objects origin is cryptographically verifiable.
|
||||
///
|
||||
/// The `resource` is the parent of this object, for example a
|
||||
/// software project.
|
||||
///
|
||||
/// The `args` are the metadata for this [`CollaborativeObject`]. See
|
||||
/// [`Create`] for further information.
|
||||
pub fn create<S, Signer, Author, Resource>(
|
||||
storage: &S,
|
||||
signer: Signer,
|
||||
resource: &Resource,
|
||||
args: Create<Author>,
|
||||
) -> Result<CollaborativeObject, error::Create>
|
||||
where
|
||||
S: Store<Resource>,
|
||||
Author: Identity,
|
||||
Author::Identifier: Clone + PartialEq,
|
||||
Resource: Identity,
|
||||
Signer: crypto::Signer,
|
||||
{
|
||||
let Create {
|
||||
author,
|
||||
ref contents,
|
||||
ref typename,
|
||||
..
|
||||
} = &args;
|
||||
|
||||
let content = match author {
|
||||
None => None,
|
||||
Some(author) => {
|
||||
if !author.is_delegate(signer.public_key()) {
|
||||
return Err(error::Create::SignerIsNotAuthor);
|
||||
} else {
|
||||
Some(author.content_id())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let init_change = storage
|
||||
.create(content, resource.content_id(), &signer, args.create_spec())
|
||||
.map_err(error::Create::from)?;
|
||||
|
||||
let history = History::new_from_root(
|
||||
*init_change.id(),
|
||||
content,
|
||||
resource.content_id(),
|
||||
contents.clone(),
|
||||
);
|
||||
|
||||
let object_id = init_change.id().into();
|
||||
storage
|
||||
.update(&resource.identifier(), typename, &object_id, &init_change)
|
||||
.map_err(|err| error::Create::Refs { err: Box::new(err) })?;
|
||||
|
||||
Ok(CollaborativeObject {
|
||||
typename: args.typename,
|
||||
history,
|
||||
id: init_change.id().into(),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::git;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Create {
|
||||
#[error("Invalid automerge history")]
|
||||
InvalidAutomergeHistory,
|
||||
#[error(transparent)]
|
||||
CreateChange(#[from] git::change::error::Create),
|
||||
#[error("failed to updated references for during object creation")]
|
||||
Refs {
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("signer must belong to the author")]
|
||||
SignerIsNotAuthor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Retrieve {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("failed to get references during object retrieval")]
|
||||
Refs {
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Update {
|
||||
#[error("no object found")]
|
||||
NoSuchObject,
|
||||
#[error(transparent)]
|
||||
CreateChange(#[from] git::change::error::Create),
|
||||
#[error("failed to get references during object update")]
|
||||
Refs {
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("signer must belong to the author")]
|
||||
SignerIsNotAuthor,
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use crate::{change_graph::ChangeGraph, identity, CollaborativeObject, ObjectId, Store, TypeName};
|
||||
|
||||
use super::error;
|
||||
|
||||
/// Get a [`CollaborativeObject`], if it exists.
|
||||
///
|
||||
/// The `storage` is the backing storage for storing
|
||||
/// [`crate::Change`]s at content-addressable locations. Please see
|
||||
/// [`Store`] for further information.
|
||||
///
|
||||
/// The `resource` is the parent of this object, for example a
|
||||
/// software project.
|
||||
///
|
||||
/// The `typename` is the type of object to be found, while the `oid`
|
||||
/// is the identifier for the particular object under that type.
|
||||
pub fn get<S, Resource>(
|
||||
storage: &S,
|
||||
resource: &Resource,
|
||||
typename: &TypeName,
|
||||
oid: &ObjectId,
|
||||
) -> Result<Option<CollaborativeObject>, error::Retrieve>
|
||||
where
|
||||
S: Store<Resource>,
|
||||
Resource: identity::Identity,
|
||||
{
|
||||
let tip_refs = storage
|
||||
.objects(&resource.identifier(), typename, oid)
|
||||
.map_err(|err| error::Retrieve::Refs { err: Box::new(err) })?;
|
||||
Ok(ChangeGraph::load(storage, tip_refs.iter(), typename, oid).map(|graph| graph.evaluate()))
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
//! [`ChangeGraphInfo`] provides a useful debugging structure for
|
||||
//! represnting a single [`crate::CollaborativeObject`]'s underlying
|
||||
//! change graph. This includes a [`ChangeGraphInfo::dotviz`] for
|
||||
//! describing the graph via [graphviz].
|
||||
//!
|
||||
//! [graphviz]: https://graphviz.org/
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use git_ext::Oid;
|
||||
|
||||
use crate::{change_graph::ChangeGraph, identity::Identity, ObjectId, Store, TypeName};
|
||||
|
||||
use super::error;
|
||||
|
||||
/// Additional information about the change graph of an object
|
||||
pub struct ChangeGraphInfo {
|
||||
/// The ID of the object
|
||||
pub object_id: ObjectId,
|
||||
/// A graphviz description of the changegraph of the object
|
||||
pub dotviz: String,
|
||||
/// The number of nodes in the change graph of the object
|
||||
pub number_of_nodes: u64,
|
||||
/// The "tips" of the change graph, i.e the object IDs pointed to by
|
||||
/// references to the object
|
||||
pub tips: BTreeSet<Oid>,
|
||||
}
|
||||
|
||||
/// Retrieve additional information about the change graph of an object. This
|
||||
/// is mostly useful for debugging and testing
|
||||
///
|
||||
/// The `storage` is the backing storage for storing
|
||||
/// [`crate::Change`]s at content-addressable locations. Please see
|
||||
/// [`Store`] for further information.
|
||||
///
|
||||
/// The `resource` is the parent of this object, for example a
|
||||
/// software project.
|
||||
///
|
||||
/// The `typename` is the type of object to be found, while the `oid`
|
||||
/// is the identifier for the particular object under that type.
|
||||
pub fn changegraph<S, Resource>(
|
||||
storage: &S,
|
||||
resource: &Resource,
|
||||
typename: &TypeName,
|
||||
oid: &ObjectId,
|
||||
) -> Result<Option<ChangeGraphInfo>, error::Retrieve>
|
||||
where
|
||||
S: Store<Resource>,
|
||||
Resource: Identity,
|
||||
{
|
||||
let tip_refs = storage
|
||||
.objects(&resource.identifier(), typename, oid)
|
||||
.map_err(|err| error::Retrieve::Refs { err: Box::new(err) })?;
|
||||
Ok(
|
||||
ChangeGraph::load(storage, tip_refs.iter(), typename, oid).map(|graph| ChangeGraphInfo {
|
||||
object_id: *oid,
|
||||
dotviz: graph.graphviz(),
|
||||
number_of_nodes: graph.number_of_nodes(),
|
||||
tips: graph.tips(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use crate::{change_graph::ChangeGraph, identity::Identity, CollaborativeObject, Store, TypeName};
|
||||
|
||||
use super::error;
|
||||
|
||||
/// List a set of [`CollaborativeObject`].
|
||||
///
|
||||
/// The `storage` is the backing storage for storing
|
||||
/// [`crate::Change`]s at content-addressable locations. Please see
|
||||
/// [`Store`] for further information.
|
||||
///
|
||||
/// The `resource` is the parent of this object, for example a
|
||||
/// software project.
|
||||
///
|
||||
/// The `typename` is the type of objects to listed.
|
||||
pub fn list<S, Resource>(
|
||||
storage: &S,
|
||||
resource: &Resource,
|
||||
typename: &TypeName,
|
||||
) -> Result<Vec<CollaborativeObject>, error::Retrieve>
|
||||
where
|
||||
S: Store<Resource>,
|
||||
Resource: Identity,
|
||||
{
|
||||
let references = storage
|
||||
.types(&resource.identifier(), typename)
|
||||
.map_err(|err| error::Retrieve::Refs { err: Box::new(err) })?;
|
||||
log::trace!("loaded {} references", references.len());
|
||||
let mut result = Vec::new();
|
||||
for (oid, tip_refs) in references {
|
||||
log::trace!("loading object '{}'", oid);
|
||||
let loaded = ChangeGraph::load(storage, tip_refs.iter(), typename, &oid)
|
||||
.map(|graph| graph.evaluate());
|
||||
|
||||
match loaded {
|
||||
Some(obj) => {
|
||||
log::trace!("object '{}' found", oid);
|
||||
result.push(obj);
|
||||
}
|
||||
None => {
|
||||
log::trace!("object '{}' not found", oid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use crate::{
|
||||
change, change_graph::ChangeGraph, identity::Identity, CollaborativeObject, Contents, ObjectId,
|
||||
Store, TypeName,
|
||||
};
|
||||
|
||||
use super::error;
|
||||
|
||||
/// The data required to update an object
|
||||
pub struct Update<Author> {
|
||||
/// The identity of the author for this object's first change.
|
||||
pub author: Option<Author>,
|
||||
/// The CRDT changes to add to the object.
|
||||
pub changes: Contents,
|
||||
/// The object ID of the object to be updated.
|
||||
pub object_id: ObjectId,
|
||||
/// The typename of the object to be updated.
|
||||
pub typename: TypeName,
|
||||
/// The message to add when updating this object.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Update an existing [`CollaborativeObject`].
|
||||
///
|
||||
/// The `storage` is the backing storage for storing
|
||||
/// [`crate::Change`]s at content-addressable locations. Please see
|
||||
/// [`Store`] for further information.
|
||||
///
|
||||
/// The `signer` is expected to be a cryptographic signing key. This
|
||||
/// ensures that the objects origin is cryptographically verifiable.
|
||||
///
|
||||
/// The `resource` is the parent of this object, for example a
|
||||
/// software project.
|
||||
///
|
||||
/// The `args` are the metadata for this [`CollaborativeObject`]
|
||||
/// udpate. See [`Update`] for further information.
|
||||
pub fn update<S, Signer, Resource, Author>(
|
||||
storage: &S,
|
||||
signer: Signer,
|
||||
resource: &Resource,
|
||||
args: Update<Author>,
|
||||
) -> Result<CollaborativeObject, error::Update>
|
||||
where
|
||||
S: Store<Resource>,
|
||||
Author: Identity,
|
||||
Author::Identifier: Clone + PartialEq,
|
||||
Resource: Identity,
|
||||
Signer: crypto::Signer,
|
||||
{
|
||||
let Update {
|
||||
author,
|
||||
ref typename,
|
||||
object_id,
|
||||
changes,
|
||||
message,
|
||||
} = args;
|
||||
|
||||
let content = match author {
|
||||
None => None,
|
||||
Some(author) => {
|
||||
if !author.is_delegate(signer.public_key()) {
|
||||
return Err(error::Update::SignerIsNotAuthor);
|
||||
} else {
|
||||
Some(author.content_id())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let existing_refs = storage
|
||||
.objects(&resource.identifier(), typename, &object_id)
|
||||
.map_err(|err| error::Update::Refs { err: Box::new(err) })?;
|
||||
|
||||
let mut object = ChangeGraph::load(storage, existing_refs.iter(), typename, &object_id)
|
||||
.map(|graph| graph.evaluate())
|
||||
.ok_or(error::Update::NoSuchObject)?;
|
||||
|
||||
let change = storage.create(
|
||||
content,
|
||||
resource.content_id(),
|
||||
&signer,
|
||||
change::Create {
|
||||
tips: object.tips().iter().cloned().collect(),
|
||||
contents: changes.clone(),
|
||||
typename: typename.clone(),
|
||||
message,
|
||||
},
|
||||
)?;
|
||||
|
||||
object
|
||||
.history
|
||||
.extend(change.id, content, change.resource, changes);
|
||||
storage
|
||||
.update(&resource.identifier(), typename, &object_id, &change)
|
||||
.map_err(|err| error::Update::Refs { err: Box::new(err) })?;
|
||||
|
||||
Ok(object)
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{collections::HashMap, error::Error};
|
||||
|
||||
use git_ext::Oid;
|
||||
use git_ref_format::RefString;
|
||||
|
||||
use crate::change::Change;
|
||||
use crate::{ObjectId, TypeName};
|
||||
|
||||
/// The [`Reference`]s that refer to the commits that make up a
|
||||
/// [`crate::CollaborativeObject`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Objects {
|
||||
/// If the local peer has a [`Reference`] for this particular
|
||||
/// object, then `local` should be set.
|
||||
pub local: Option<Reference>,
|
||||
/// The `remotes` are the entries for each remote peer's version
|
||||
/// of the particular object.
|
||||
pub remotes: Vec<Reference>,
|
||||
}
|
||||
|
||||
impl Objects {
|
||||
/// Return an iterator over the `local` and `remotes` of the given
|
||||
/// [`Objects`].
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Reference> {
|
||||
self.local.iter().chain(self.remotes.iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Reference`] that must directly point to the [`Commit`] for a
|
||||
/// [`crate::CollaborativeObject`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Reference {
|
||||
/// The `name` of the reference.
|
||||
pub name: RefString,
|
||||
/// The [`Commit`] that this reference points to.
|
||||
pub target: Commit,
|
||||
}
|
||||
|
||||
/// A [`Commit`] that holds the data for a given [`crate::CollaborativeObject`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Commit {
|
||||
/// The content identifier of the commit.
|
||||
pub id: Oid,
|
||||
/// The parents of the commit.
|
||||
pub parents: Vec<Commit>,
|
||||
}
|
||||
|
||||
pub trait Storage {
|
||||
type ObjectsError: Error + Send + Sync + 'static;
|
||||
type TypesError: Error + Send + Sync + 'static;
|
||||
type UpdateError: Error + Send + Sync + 'static;
|
||||
|
||||
type Identifier;
|
||||
|
||||
/// Get all references which point to a head of the change graph for a
|
||||
/// particular object
|
||||
fn objects(
|
||||
&self,
|
||||
identifier: &Self::Identifier,
|
||||
typename: &TypeName,
|
||||
object_id: &ObjectId,
|
||||
) -> Result<Objects, Self::ObjectsError>;
|
||||
|
||||
/// Get all references to objects of a given type within a particular
|
||||
/// identity
|
||||
fn types(
|
||||
&self,
|
||||
identifier: &Self::Identifier,
|
||||
typename: &TypeName,
|
||||
) -> Result<HashMap<ObjectId, Objects>, Self::TypesError>;
|
||||
|
||||
/// Update a ref to a particular collaborative object
|
||||
fn update(
|
||||
&self,
|
||||
identifier: &Self::Identifier,
|
||||
typename: &TypeName,
|
||||
object_id: &ObjectId,
|
||||
change: &Change,
|
||||
) -> Result<(), Self::UpdateError>;
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeSet, HashMap},
|
||||
ops::ControlFlow,
|
||||
};
|
||||
|
||||
pub(crate) trait GraphNode {
|
||||
type Id: Clone + Eq + Ord + std::hash::Hash + std::fmt::Debug;
|
||||
|
||||
fn id(&self) -> &Self::Id;
|
||||
fn child_ids(&self) -> &[Self::Id];
|
||||
}
|
||||
|
||||
/// Fold a topological sort of a directed acyclic graph, pruning some branches.
|
||||
///
|
||||
/// `items` must be an iterator over the nodes of the graph in topological
|
||||
/// order. Assuming this is the case `fold` will only be called with nodes whose
|
||||
/// ancestors have already been evaluated. Returning `ControlFlow::Break(..)`
|
||||
/// from `fold` will omit evaluation of the current node and consequently omit
|
||||
/// processing of any nodes who have the current node as an ancestor.
|
||||
pub(crate) fn pruning_fold<'a, BN, Node, It, F, O>(init: O, items: It, mut f: F) -> O
|
||||
where
|
||||
BN: Borrow<Node> + 'a,
|
||||
Node: 'a + GraphNode,
|
||||
It: Iterator<Item = BN>,
|
||||
F: for<'r> FnMut(O, &'r Node) -> std::ops::ControlFlow<O, O>,
|
||||
{
|
||||
let mut rejected = RejectedNodes::new();
|
||||
let mut state = init;
|
||||
for node in items {
|
||||
// There can be multiple paths to a change so in a topological traversal we
|
||||
// might encounter a change which we have already rejected
|
||||
// previously
|
||||
if rejected.is_rejected(node.borrow().id()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(rejected_ancestor) = rejected.rejected_ancestor(node.borrow().id()) {
|
||||
let ancestor = rejected_ancestor.clone();
|
||||
log::warn!(
|
||||
"rejecting node because an ancestor change was rejected id='{:?}', ancestor='{:?}'",
|
||||
node.borrow().id(),
|
||||
rejected_ancestor
|
||||
);
|
||||
for child in node.borrow().child_ids() {
|
||||
rejected.transitively_reject(child, &ancestor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
state = match f(state, node.borrow()) {
|
||||
ControlFlow::Continue(state) => state,
|
||||
ControlFlow::Break(state) => {
|
||||
rejected.directly_reject(node.borrow().id(), node.borrow().child_ids());
|
||||
state
|
||||
}
|
||||
};
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
struct RejectedNodes<NodeId> {
|
||||
/// Changes which are directly rejected by the fold function
|
||||
direct: BTreeSet<NodeId>,
|
||||
/// A map from node IDs to the IDs of ancestor nodes which are
|
||||
/// direct rejections
|
||||
transitive: HashMap<NodeId, NodeId>,
|
||||
}
|
||||
|
||||
impl<NodeId: Clone + Eq + Ord + std::hash::Hash> RejectedNodes<NodeId> {
|
||||
fn new() -> RejectedNodes<NodeId> {
|
||||
RejectedNodes {
|
||||
direct: BTreeSet::new(),
|
||||
transitive: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rejected_ancestor(&self, node: &NodeId) -> Option<&NodeId> {
|
||||
self.transitive.get(node)
|
||||
}
|
||||
|
||||
fn is_rejected(&self, node: &NodeId) -> bool {
|
||||
self.direct.contains(node)
|
||||
}
|
||||
|
||||
fn directly_reject(&mut self, node: &NodeId, children: &[NodeId]) {
|
||||
self.direct.insert(node.clone());
|
||||
for child in children {
|
||||
self.transitive.insert(child.clone(), node.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn transitively_reject(&mut self, child: &NodeId, rejected_ancestor: &NodeId) {
|
||||
self.transitive
|
||||
.insert(child.clone(), rejected_ancestor.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub mod signatures;
|
||||
pub use signatures::{Signature, Signatures};
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
convert::TryFrom,
|
||||
iter::FromIterator,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crypto::{ssh::ExtendedSignature, PublicKey};
|
||||
use git_commit::{
|
||||
Commit,
|
||||
Signature::{Pgp, Ssh},
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Signature {
|
||||
key: PublicKey,
|
||||
sig: crypto::Signature,
|
||||
}
|
||||
|
||||
impl From<Signature> for ExtendedSignature {
|
||||
fn from(sig: Signature) -> Self {
|
||||
Self::new(sig.key, sig.sig)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExtendedSignature> for Signature {
|
||||
fn from(ex: ExtendedSignature) -> Self {
|
||||
let (key, sig) = ex.into();
|
||||
Self { key, sig }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(PublicKey, crypto::Signature)> for Signature {
|
||||
fn from((key, sig): (PublicKey, crypto::Signature)) -> Self {
|
||||
Self { key, sig }
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME(kim): This should really be a HashMap with a no-op Hasher -- PublicKey
|
||||
// collisions are catastrophic
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Signatures(BTreeMap<PublicKey, crypto::Signature>);
|
||||
|
||||
impl Deref for Signatures {
|
||||
type Target = BTreeMap<PublicKey, crypto::Signature>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Signatures {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signature> for Signatures {
|
||||
fn from(Signature { key, sig }: Signature) -> Self {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(key, sig);
|
||||
map.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<PublicKey, crypto::Signature>> for Signatures {
|
||||
fn from(map: BTreeMap<PublicKey, crypto::Signature>) -> Self {
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signatures> for BTreeMap<PublicKey, crypto::Signature> {
|
||||
fn from(s: Signatures) -> Self {
|
||||
s.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Commit> for Signatures {
|
||||
type Error = error::Signatures;
|
||||
|
||||
fn try_from(value: &Commit) -> Result<Self, Self::Error> {
|
||||
value
|
||||
.signatures()
|
||||
.filter_map(|signature| {
|
||||
match signature {
|
||||
// Skip PGP signatures
|
||||
Pgp(_) => None,
|
||||
Ssh(armored) => Some(
|
||||
ExtendedSignature::from_armored(armored.as_bytes())
|
||||
.map_err(error::Signatures::from),
|
||||
),
|
||||
}
|
||||
})
|
||||
.map(|ex| ex.map(|ex| ex.into()))
|
||||
.collect::<Result<_, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(PublicKey, crypto::Signature)> for Signatures {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (PublicKey, crypto::Signature)>,
|
||||
{
|
||||
Self(BTreeMap::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Signatures {
|
||||
type Item = (PublicKey, crypto::Signature);
|
||||
type IntoIter = <BTreeMap<PublicKey, crypto::Signature> as IntoIterator>::IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Signature> for Signatures {
|
||||
fn extend<T>(&mut self, iter: T)
|
||||
where
|
||||
T: IntoIterator<Item = Signature>,
|
||||
{
|
||||
for Signature { key, sig } in iter {
|
||||
self.insert(key, sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<(PublicKey, crypto::Signature)> for Signatures {
|
||||
fn extend<T>(&mut self, iter: T)
|
||||
where
|
||||
T: IntoIterator<Item = (PublicKey, crypto::Signature)>,
|
||||
{
|
||||
for (key, sig) in iter {
|
||||
self.insert(key, sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use radicle_crypto::ssh::ExtendedSignatureError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Signature {
|
||||
#[error("missing {0}")]
|
||||
Missing(&'static str),
|
||||
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde::de::value::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Signatures {
|
||||
#[error(transparent)]
|
||||
ExtendedSignature(#[from] ExtendedSignatureError),
|
||||
|
||||
#[error(transparent)]
|
||||
Signature(#[from] Signature),
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pub mod identity;
|
||||
pub use identity::{Name, Person, Project, RemoteProject};
|
||||
|
||||
pub mod storage;
|
||||
pub use storage::Storage;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
pub mod project;
|
||||
pub use project::{Project, RemoteProject};
|
||||
|
||||
pub mod person;
|
||||
pub use person::Person;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Urn {
|
||||
pub name: Name,
|
||||
pub remote: Option<Name>,
|
||||
}
|
||||
|
||||
impl Urn {
|
||||
pub fn to_path(&self) -> String {
|
||||
match &self.remote {
|
||||
Some(remote) => format!("{}/{}", self.name.as_str(), remote.as_str()),
|
||||
None => self.name.0.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Name(String);
|
||||
|
||||
impl Name {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
use git_ext::Oid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::identity::Identity;
|
||||
use crate::test::storage::{self, Storage};
|
||||
|
||||
use super::{Name, Urn};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Person {
|
||||
pub payload: Payload,
|
||||
pub content_id: Oid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Payload {
|
||||
name: Name,
|
||||
key: crypto::PublicKey,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
pub fn new(
|
||||
repo: &Storage,
|
||||
name: &str,
|
||||
key: crypto::PublicKey,
|
||||
) -> Result<Self, storage::error::Identity> {
|
||||
let repo = repo.as_raw();
|
||||
let refname = format!("refs/rad/identities/{}", name);
|
||||
let payload = Payload {
|
||||
name: Name(name.to_owned()),
|
||||
key,
|
||||
};
|
||||
let blob = serde_json::to_vec(&payload)?;
|
||||
let oid = repo.blob(&blob)?;
|
||||
let mut tree = repo.treebuilder(None)?;
|
||||
tree.insert("identity", oid, git2::FileMode::Blob.into())?;
|
||||
let oid = tree.write()?;
|
||||
let tree = repo.find_tree(oid)?;
|
||||
let signature = git2::Signature::now(name, name)?;
|
||||
let content_id = repo
|
||||
.commit(
|
||||
Some(&refname),
|
||||
&signature,
|
||||
&signature,
|
||||
"persisted identity",
|
||||
&tree,
|
||||
&[],
|
||||
)?
|
||||
.into();
|
||||
Ok(Self {
|
||||
payload,
|
||||
content_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn key(&self) -> crypto::PublicKey {
|
||||
self.payload.key
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &Name {
|
||||
&self.payload.name
|
||||
}
|
||||
|
||||
pub fn find_by_oid(
|
||||
repo: &git2::Repository,
|
||||
id: Oid,
|
||||
) -> Result<Option<Person>, storage::error::Identity> {
|
||||
match repo.find_commit(id.into()) {
|
||||
Ok(commit) => from_commit(repo, commit),
|
||||
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_commit(
|
||||
repo: &git2::Repository,
|
||||
commit: git2::Commit,
|
||||
) -> Result<Option<Person>, storage::error::Identity> {
|
||||
let tree = commit.tree()?;
|
||||
let entry = tree
|
||||
.get_name("identity")
|
||||
.ok_or_else(|| storage::error::Identity::NotFound(tree.id().into()))?;
|
||||
let blob = match entry.to_object(repo)?.into_blob() {
|
||||
Ok(blob) => blob,
|
||||
Err(other) => return Err(storage::error::Identity::NotBlob(other.kind())),
|
||||
};
|
||||
let payload = serde_json::de::from_slice(blob.content())?;
|
||||
Ok(Some(Person {
|
||||
payload,
|
||||
content_id: commit.id().into(),
|
||||
}))
|
||||
}
|
||||
|
||||
impl Identity for Person {
|
||||
type Identifier = Urn;
|
||||
|
||||
fn is_delegate(&self, delegation: &crypto::PublicKey) -> bool {
|
||||
self.key() == *delegation
|
||||
}
|
||||
|
||||
fn content_id(&self) -> Oid {
|
||||
self.content_id
|
||||
}
|
||||
|
||||
fn identifier(&self) -> Self::Identifier {
|
||||
Urn {
|
||||
name: self.name().clone(),
|
||||
remote: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use git_ext::Oid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::identity::Identity;
|
||||
use crate::test;
|
||||
use crate::test::storage::{self, Storage};
|
||||
|
||||
use super::{Name, Urn};
|
||||
|
||||
pub struct RemoteProject {
|
||||
pub project: Project,
|
||||
pub person: test::Person,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Project {
|
||||
pub payload: Payload,
|
||||
pub content_id: Oid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Payload {
|
||||
name: Name,
|
||||
delegates: BTreeSet<crypto::PublicKey>,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new(
|
||||
repo: &Storage,
|
||||
name: &str,
|
||||
delegate: crypto::PublicKey,
|
||||
) -> Result<Self, storage::error::Identity> {
|
||||
let repo = repo.as_raw();
|
||||
let refname = format!("refs/rad/identities/{}", name);
|
||||
let payload = Payload {
|
||||
name: Name(name.to_owned()),
|
||||
delegates: Some(delegate).into_iter().collect(),
|
||||
};
|
||||
let blob = serde_json::to_vec(&payload)?;
|
||||
let oid = repo.blob(&blob)?;
|
||||
let mut tree = repo.treebuilder(None)?;
|
||||
tree.insert("identity", oid, git2::FileMode::Blob.into())?;
|
||||
let oid = tree.write()?;
|
||||
let tree = repo.find_tree(oid)?;
|
||||
let signature = git2::Signature::now(name, name)?;
|
||||
let content_id = repo
|
||||
.commit(
|
||||
Some(&refname),
|
||||
&signature,
|
||||
&signature,
|
||||
"persisted identity",
|
||||
&tree,
|
||||
&[],
|
||||
)?
|
||||
.into();
|
||||
Ok(Self {
|
||||
payload,
|
||||
content_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delegates(&self) -> &BTreeSet<crypto::PublicKey> {
|
||||
&self.payload.delegates
|
||||
}
|
||||
|
||||
pub fn delegate_check(&self, person: &test::Person) -> bool {
|
||||
self.payload.delegates.contains(&person.key())
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &Name {
|
||||
&self.payload.name
|
||||
}
|
||||
|
||||
pub fn find_by_oid(
|
||||
repo: &git2::Repository,
|
||||
id: Oid,
|
||||
) -> Result<Option<Self>, storage::error::Identity> {
|
||||
match repo.find_commit(id.into()) {
|
||||
Ok(commit) => from_commit(repo, commit),
|
||||
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_commit(
|
||||
repo: &git2::Repository,
|
||||
commit: git2::Commit,
|
||||
) -> Result<Option<Project>, storage::error::Identity> {
|
||||
let tree = commit.tree()?;
|
||||
let entry = tree
|
||||
.get_name("identity")
|
||||
.ok_or_else(|| storage::error::Identity::NotFound(tree.id().into()))?;
|
||||
let blob = match entry.to_object(repo)?.into_blob() {
|
||||
Ok(blob) => blob,
|
||||
Err(other) => return Err(storage::error::Identity::NotBlob(other.kind())),
|
||||
};
|
||||
let payload = serde_json::de::from_slice(blob.content())?;
|
||||
Ok(Some(Project {
|
||||
payload,
|
||||
content_id: commit.id().into(),
|
||||
}))
|
||||
}
|
||||
|
||||
impl Identity for Project {
|
||||
type Identifier = Urn;
|
||||
|
||||
fn is_delegate(&self, delegation: &crypto::PublicKey) -> bool {
|
||||
self.delegates().contains(delegation)
|
||||
}
|
||||
|
||||
fn content_id(&self) -> Oid {
|
||||
self.content_id
|
||||
}
|
||||
|
||||
fn identifier(&self) -> Self::Identifier {
|
||||
Urn {
|
||||
name: self.name().clone(),
|
||||
remote: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Identity for RemoteProject {
|
||||
type Identifier = Urn;
|
||||
|
||||
fn is_delegate(&self, delegation: &crypto::PublicKey) -> bool {
|
||||
self.project.delegates().contains(delegation)
|
||||
}
|
||||
|
||||
fn content_id(&self) -> Oid {
|
||||
self.project.content_id
|
||||
}
|
||||
|
||||
fn identifier(&self) -> Self::Identifier {
|
||||
Urn {
|
||||
name: self.project.name().clone(),
|
||||
remote: Some(self.person.name().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
use std::{collections::HashMap, convert::TryFrom as _};
|
||||
|
||||
use git_ref_format::RefString;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{
|
||||
change,
|
||||
object::{self, Commit, Reference},
|
||||
ObjectId, Store,
|
||||
};
|
||||
|
||||
use super::identity::{RemoteProject, Urn};
|
||||
|
||||
pub mod error {
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Identity {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("'identity' was not a blob, found '{0:?}'")]
|
||||
NotBlob(Option<git2::ObjectType>),
|
||||
#[error("could not find 'identity' in the tree '{0}'")]
|
||||
NotFound(git_ext::Oid),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Objects {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Format(#[from] git_ref_format::Error),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
raw: git2::Repository,
|
||||
_temp: TempDir,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new() -> Self {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let raw = git2::Repository::init(temp.path()).unwrap();
|
||||
let mut config = raw.config().unwrap();
|
||||
config.set_str("user.name", "Terry Pratchett").unwrap();
|
||||
config
|
||||
.set_str("user.email", "http://www.gnuterrypratchett.com")
|
||||
.unwrap();
|
||||
Self { raw, _temp: temp }
|
||||
}
|
||||
|
||||
pub fn as_raw(&self) -> &git2::Repository {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
impl Store<RemoteProject> for Storage {}
|
||||
|
||||
impl change::Storage for Storage {
|
||||
type CreateError = <git2::Repository as change::Storage>::CreateError;
|
||||
type LoadError = <git2::Repository as change::Storage>::LoadError;
|
||||
|
||||
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
|
||||
type Author = <git2::Repository as change::Storage>::Author;
|
||||
type Resource = <git2::Repository as change::Storage>::Resource;
|
||||
type Signatures = <git2::Repository as change::Storage>::Signatures;
|
||||
|
||||
fn create<Signer>(
|
||||
&self,
|
||||
author: Option<Self::Author>,
|
||||
authority: Self::Resource,
|
||||
signer: &Signer,
|
||||
spec: change::Create<Self::ObjectId>,
|
||||
) -> Result<
|
||||
change::store::Change<Self::Author, Self::Resource, Self::ObjectId, Self::Signatures>,
|
||||
Self::CreateError,
|
||||
>
|
||||
where
|
||||
Signer: crypto::Signer,
|
||||
{
|
||||
self.as_raw().create(author, authority, signer, spec)
|
||||
}
|
||||
|
||||
fn load(
|
||||
&self,
|
||||
id: Self::ObjectId,
|
||||
) -> Result<
|
||||
change::store::Change<Self::Author, Self::Resource, Self::ObjectId, Self::Signatures>,
|
||||
Self::LoadError,
|
||||
> {
|
||||
self.as_raw().load(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl object::Storage for Storage {
|
||||
type ObjectsError = error::Objects;
|
||||
type TypesError = error::Objects;
|
||||
type UpdateError = git2::Error;
|
||||
|
||||
type Identifier = Urn;
|
||||
|
||||
fn objects(
|
||||
&self,
|
||||
identifier: &Self::Identifier,
|
||||
typename: &crate::TypeName,
|
||||
object_id: &ObjectId,
|
||||
) -> Result<object::Objects, Self::ObjectsError> {
|
||||
let name = format!(
|
||||
"refs/rad/{}/cobs/{}/{}",
|
||||
identifier.to_path(),
|
||||
typename,
|
||||
object_id
|
||||
);
|
||||
let glob = format!(
|
||||
"refs/rad/{}/*/cobs/{}/{}",
|
||||
identifier.name.as_str(),
|
||||
typename,
|
||||
object_id
|
||||
);
|
||||
let local = {
|
||||
let r = self.raw.find_reference(&name)?;
|
||||
Some(resolve_reference(r)?)
|
||||
};
|
||||
let remotes = self
|
||||
.raw
|
||||
.references_glob(&glob)?
|
||||
.map(|r| r.map_err(error::Objects::from).and_then(resolve_reference))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(object::Objects { local, remotes })
|
||||
}
|
||||
|
||||
fn types(
|
||||
&self,
|
||||
identifier: &Self::Identifier,
|
||||
typename: &crate::TypeName,
|
||||
) -> Result<HashMap<ObjectId, object::Objects>, Self::TypesError> {
|
||||
let mut objects = HashMap::new();
|
||||
let prefix = format!("refs/rad/{}/cobs/{}", identifier.to_path(), typename);
|
||||
for r in self.raw.references()? {
|
||||
let r = r?;
|
||||
let name = r.name().unwrap();
|
||||
let oid = r
|
||||
.target()
|
||||
.map(ObjectId::from)
|
||||
.expect("BUG: the cob references should be direct");
|
||||
if name.starts_with(&prefix) {
|
||||
objects.insert(
|
||||
oid,
|
||||
object::Objects {
|
||||
local: Some(resolve_reference(r)?),
|
||||
remotes: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
identifier: &Self::Identifier,
|
||||
typename: &crate::TypeName,
|
||||
object_id: &ObjectId,
|
||||
change: &change::Change,
|
||||
) -> Result<(), Self::UpdateError> {
|
||||
let name = format!(
|
||||
"refs/rad/{}/cobs/{}/{}",
|
||||
identifier.to_path(),
|
||||
typename,
|
||||
object_id
|
||||
);
|
||||
let id = *change.id();
|
||||
self.raw.reference(&name, id.into(), true, "new change")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_reference(r: git2::Reference) -> Result<Reference, error::Objects> {
|
||||
let commit = r.peel_to_commit()?;
|
||||
let target = resolve_parents(commit)?;
|
||||
Ok(Reference {
|
||||
name: RefString::try_from(r.name().unwrap().to_owned())?,
|
||||
target,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_parents(commit: git2::Commit) -> Result<Commit, git2::Error> {
|
||||
let parents = commit
|
||||
.parents()
|
||||
.map(resolve_parents)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Commit {
|
||||
id: commit.id().into(),
|
||||
parents,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
use std::ops::ControlFlow;
|
||||
|
||||
use crypto::test::signer::MockSigner;
|
||||
use quickcheck::Arbitrary;
|
||||
use radicle_crypto::Signer;
|
||||
|
||||
use crate::{
|
||||
create, get, history, identity::Identity, list, update, Create, ObjectId, TypeName, Update,
|
||||
};
|
||||
|
||||
use super::test;
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let storage = test::Storage::new();
|
||||
let signer = gen::<MockSigner>(1);
|
||||
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
|
||||
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
|
||||
let proj = test::RemoteProject {
|
||||
project: proj,
|
||||
person: terry.clone(),
|
||||
};
|
||||
let contents = history::Contents::Automerge(Vec::new());
|
||||
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
|
||||
let cob = create(
|
||||
&storage,
|
||||
signer,
|
||||
&proj,
|
||||
Create {
|
||||
author: Some(terry),
|
||||
contents,
|
||||
typename: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expected = get(&storage, &proj, &typename, cob.id())
|
||||
.unwrap()
|
||||
.expect("BUG: cob was missing");
|
||||
|
||||
assert_eq!(cob, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_cobs() {
|
||||
let storage = test::Storage::new();
|
||||
let signer = gen::<MockSigner>(1);
|
||||
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
|
||||
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
|
||||
let proj = test::RemoteProject {
|
||||
project: proj,
|
||||
person: terry.clone(),
|
||||
};
|
||||
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
|
||||
let issue_1 = create(
|
||||
&storage,
|
||||
signer.clone(),
|
||||
&proj,
|
||||
Create {
|
||||
author: Some(terry.clone()),
|
||||
contents: history::Contents::Automerge(b"issue 1".to_vec()),
|
||||
typename: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let issue_2 = create(
|
||||
&storage,
|
||||
signer,
|
||||
&proj,
|
||||
Create {
|
||||
author: Some(terry),
|
||||
contents: history::Contents::Automerge(b"issue 2".to_vec()),
|
||||
typename: typename.clone(),
|
||||
message: "commenting xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut expected = list(&storage, &proj, &typename).unwrap();
|
||||
expected.sort_by(|x, y| x.id().cmp(y.id()));
|
||||
|
||||
let mut actual = vec![issue_1, issue_2];
|
||||
actual.sort_by(|x, y| x.id().cmp(y.id()));
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_cob() {
|
||||
let storage = test::Storage::new();
|
||||
let signer = gen::<MockSigner>(1);
|
||||
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
|
||||
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
|
||||
let proj = test::RemoteProject {
|
||||
project: proj,
|
||||
person: terry.clone(),
|
||||
};
|
||||
let contents = history::Contents::Automerge(Vec::new());
|
||||
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
|
||||
let cob = create(
|
||||
&storage,
|
||||
signer.clone(),
|
||||
&proj,
|
||||
Create {
|
||||
author: Some(terry.clone()),
|
||||
contents,
|
||||
typename: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let not_expected = get(&storage, &proj, &typename, cob.id())
|
||||
.unwrap()
|
||||
.expect("BUG: cob was missing");
|
||||
|
||||
let updated = update(
|
||||
&storage,
|
||||
signer,
|
||||
&proj,
|
||||
Update {
|
||||
author: Some(terry),
|
||||
changes: history::Contents::Automerge(b"issue 1".to_vec()),
|
||||
object_id: *cob.id(),
|
||||
typename: typename.clone(),
|
||||
message: "commenting xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expected = get(&storage, &proj, &typename, updated.id())
|
||||
.unwrap()
|
||||
.expect("BUG: cob was missing");
|
||||
|
||||
assert_ne!(updated, not_expected);
|
||||
assert_eq!(updated, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traverse_cobs() {
|
||||
let storage = test::Storage::new();
|
||||
let neil_signer = gen::<MockSigner>(2);
|
||||
let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
|
||||
let terry_signer = gen::<MockSigner>(1);
|
||||
let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
|
||||
let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
|
||||
let terry_proj = test::RemoteProject {
|
||||
project: proj.clone(),
|
||||
person: terry.clone(),
|
||||
};
|
||||
let neil_proj = test::RemoteProject {
|
||||
project: proj,
|
||||
person: neil.clone(),
|
||||
};
|
||||
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
|
||||
let cob = create(
|
||||
&storage,
|
||||
terry_signer,
|
||||
&terry_proj,
|
||||
Create {
|
||||
author: Some(terry),
|
||||
contents: history::Contents::Automerge(b"issue 1".to_vec()),
|
||||
typename: typename.clone(),
|
||||
message: "creating xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
copy_to(
|
||||
storage.as_raw(),
|
||||
&terry_proj,
|
||||
&neil_proj,
|
||||
&typename,
|
||||
*cob.id(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let updated = update(
|
||||
&storage,
|
||||
neil_signer,
|
||||
&neil_proj,
|
||||
Update {
|
||||
author: Some(neil),
|
||||
changes: history::Contents::Automerge(b"issue 2".to_vec()),
|
||||
object_id: *cob.id(),
|
||||
typename,
|
||||
message: "commenting on xyz.rad.issue".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// traverse over the history and filter by changes that were only authorized by terry
|
||||
let contents = updated.history().traverse(Vec::new(), |mut acc, entry| {
|
||||
let author = match entry.author() {
|
||||
Some(author) => test::Person::find_by_oid(storage.as_raw(), *author).unwrap(),
|
||||
None => None,
|
||||
};
|
||||
let project = test::Project::find_by_oid(storage.as_raw(), entry.resource()).unwrap();
|
||||
|
||||
if let (Some(author), Some(project)) = (author, project) {
|
||||
if project.delegate_check(&author) {
|
||||
acc.push(entry.contents().as_ref().to_vec())
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue(acc)
|
||||
});
|
||||
|
||||
assert_eq!(contents, vec![b"issue 1".to_vec()]);
|
||||
|
||||
// traverse over the history and filter by changes that were only authorized by neil
|
||||
let contents = updated.history().traverse(Vec::new(), |mut acc, entry| {
|
||||
acc.push(entry.contents().as_ref().to_vec());
|
||||
ControlFlow::Continue(acc)
|
||||
});
|
||||
|
||||
assert_eq!(contents, vec![b"issue 1".to_vec(), b"issue 2".to_vec()]);
|
||||
}
|
||||
|
||||
fn gen<T: Arbitrary>(size: usize) -> T {
|
||||
let mut gen = quickcheck::Gen::new(size);
|
||||
|
||||
T::arbitrary(&mut gen)
|
||||
}
|
||||
|
||||
fn copy_to(
|
||||
repo: &git2::Repository,
|
||||
from: &test::RemoteProject,
|
||||
to: &test::RemoteProject,
|
||||
typename: &TypeName,
|
||||
object: ObjectId,
|
||||
) -> Result<(), git2::Error> {
|
||||
let original = {
|
||||
let name = format!(
|
||||
"refs/rad/{}/cobs/{}/{}",
|
||||
from.identifier().to_path(),
|
||||
typename,
|
||||
object
|
||||
);
|
||||
let r = repo.find_reference(&name)?;
|
||||
r.target().unwrap()
|
||||
};
|
||||
|
||||
let name = format!(
|
||||
"refs/rad/{}/cobs/{}/{}",
|
||||
to.identifier().to_path(),
|
||||
typename,
|
||||
object
|
||||
);
|
||||
repo.reference(&name, original, false, "copying object reference")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
mod author_commit {
|
||||
super::oid_trailer! {AuthorCommitTrailer, "Rad-Author"}
|
||||
}
|
||||
mod resource_identity {
|
||||
super::oid_trailer! {ResourceCommitTrailer, "Rad-Resource"}
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
pub use super::author_commit::Error as InvalidAuthorTrailer;
|
||||
|
||||
pub use super::resource_identity::Error as InvalidResourceTrailer;
|
||||
}
|
||||
|
||||
pub use author_commit::AuthorCommitTrailer;
|
||||
pub use resource_identity::ResourceCommitTrailer;
|
||||
|
||||
/// A macro for generating boilerplate From and TryFrom impls for trailers which
|
||||
/// have git object IDs as their values
|
||||
#[macro_export]
|
||||
macro_rules! oid_trailer {
|
||||
($typename:ident, $trailer:literal) => {
|
||||
use git_trailers::{OwnedTrailer, Token, Trailer};
|
||||
use radicle_git_ext as ext;
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
WrongToken,
|
||||
NoTrailer,
|
||||
NoValue,
|
||||
InvalidOid,
|
||||
}
|
||||
|
||||
// We can't use `derive(thiserror::Error)` as we need to concat strings with
|
||||
// $trailer and macros are not allowed in non-key-value attributes
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::WrongToken => {
|
||||
write!(f, concat!("found wrong token for ", $trailer, " trailer"))
|
||||
}
|
||||
Self::NoTrailer => write!(f, concat!("no ", $trailer)),
|
||||
Self::NoValue => write!(f, concat!("no value for ", $trailer, " trailer")),
|
||||
Self::InvalidOid => write!(f, "invalid git OID"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub struct $typename(git2::Oid);
|
||||
|
||||
impl $typename {
|
||||
pub fn oid(&self) -> git2::Oid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git2::Oid> for $typename {
|
||||
fn from(oid: git2::Oid) -> Self {
|
||||
$typename(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$typename> for Trailer<'_> {
|
||||
fn from(containing: $typename) -> Self {
|
||||
Trailer {
|
||||
token: Token::try_from($trailer).unwrap(),
|
||||
values: vec![containing.0.to_string().into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$typename> for OwnedTrailer {
|
||||
fn from(containing: $typename) -> Self {
|
||||
Trailer::from(containing).to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Trailer<'_>> for $typename {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(Trailer { values, token }: &Trailer<'_>) -> Result<Self, Self::Error> {
|
||||
let val = values.first().ok_or(Error::NoValue)?;
|
||||
let ext_oid =
|
||||
radicle_git_ext::Oid::try_from(val.as_ref()).map_err(|_| Error::InvalidOid)?;
|
||||
if Some(token) == Token::try_from($trailer).ok().as_ref() {
|
||||
Ok($typename(ext_oid.into()))
|
||||
} else {
|
||||
Err(Error::WrongToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&OwnedTrailer> for $typename {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(trailer: &OwnedTrailer) -> Result<Self, Self::Error> {
|
||||
(&Trailer::from(trailer)).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ext::Oid> for $typename {
|
||||
fn from(oid: ext::Oid) -> Self {
|
||||
$typename(oid.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use oid_trailer;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
/// The typename of an object. Valid typenames MUST be sequences of
|
||||
/// alphanumeric characters separated by a period. The name must start
|
||||
/// and end with an alphanumeric character
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// * `abc.def`
|
||||
/// * `xyz.rad.issues`
|
||||
/// * `xyz.rad.patches.releases`
|
||||
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct TypeName(String);
|
||||
|
||||
impl fmt::Display for TypeName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("the type name '{invalid}' is invalid")]
|
||||
pub struct TypeNameParse {
|
||||
invalid: String,
|
||||
}
|
||||
|
||||
impl FromStr for TypeName {
|
||||
type Err = TypeNameParse;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let split = s.split('.');
|
||||
for component in split {
|
||||
if component.is_empty() {
|
||||
return Err(TypeNameParse {
|
||||
invalid: s.to_string(),
|
||||
});
|
||||
}
|
||||
if !component.chars().all(char::is_alphanumeric) {
|
||||
return Err(TypeNameParse {
|
||||
invalid: s.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(TypeName(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use super::TypeName;
|
||||
|
||||
#[test]
|
||||
fn valid_typenames() {
|
||||
assert!(TypeName::from_str("abc.def.ghi").is_ok());
|
||||
assert!(TypeName::from_str("abc.123.ghi").is_ok());
|
||||
assert!(TypeName::from_str("1bc.123.ghi").is_ok());
|
||||
assert!(TypeName::from_str(".abc.123.ghi").is_err());
|
||||
assert!(TypeName::from_str("abc.123.ghi.").is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue