multi: add ents-testutil, the shared dev-only fixture harness
commit
1003cdamulti: add ents-testutil, the shared dev-only fixture harness
The integration harness the engineering conventions call for, landed
now because phase 3’s two crates both need it: an in-memory
RefStoreRead (MemRefStore), the facet-git-tree in-memory ObjectStore
re-exported as the object side, deterministic ed25519 keys (Keypair),
a signed-commit builder that signs the gpgsig-less serialization in
git’s SSHSIG namespace exactly like git commit -S, seeding helpers
for members/results/meta entities/code-ref chains, and a CountingFind
wrapper for asserting incremental-evaluation read bounds.
Adds the ssh-key workspace dependency (pure-Rust SSHSIG) rather than shelling out to ssh-keygen as the pre-redo tree did.
Assisted-by: Claude:claude-fable-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
Cargo.lock
@@ -38,6 +38,18 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -109,6 +121,16 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
[[package]]
name = "clru"
version = "0.6.3"
@@ -124,6 +146,12 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca"
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -157,6 +185,18 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -167,6 +207,32 @@
"typenum",
]
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "dashmap"
version = "6.2.1"
@@ -212,6 +278,16 @@
"thiserror",
]
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -219,7 +295,9 @@
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
+ "subtle",
]
[[package]]
@@ -228,6 +306,60 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "sha2",
+ "subtle",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -251,6 +383,21 @@
"thiserror",
]
+[[package]]
+name = "ents-testutil"
+version = "0.0.0"
+dependencies = [
+ "ents-model",
+ "facet",
+ "facet-git-tree",
+ "gix",
+ "gix-hash",
+ "gix-object",
+ "gix-ref-store",
+ "rstest",
+ "ssh-key",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -385,6 +532,22 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
[[package]]
name = "filetime"
version = "0.2.29"
@@ -457,6 +620,18 @@
dependencies = [
"typenum",
"version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
]
[[package]]
@@ -1393,6 +1568,17 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
[[package]]
name = "hash32"
version = "0.3.1"
@@ -1440,6 +1626,15 @@
"stable_deref_trait",
]
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
[[package]]
name = "human_format"
version = "1.2.1"
@@ -1474,6 +1669,15 @@
"hashbrown 0.17.1",
]
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "io-close"
version = "0.3.7"
@@ -1541,12 +1745,27 @@
"static_assertions",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -1606,6 +1825,41 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6"
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
+dependencies = [
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.6",
+ "smallvec 1.15.2",
+ "zeroize",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1613,6 +1867,7 @@
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
+ "libm",
]
[[package]]
@@ -1621,6 +1876,44 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p521"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
+dependencies = [
+ "base16ct",
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "rand_core 0.6.4",
+ "sha2",
+]
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -1644,6 +1937,15 @@
"windows-link",
]
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1656,6 +1958,27 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -1680,6 +2003,15 @@
"zerocopy",
]
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
@@ -1719,8 +2051,8 @@
"bit-vec",
"bitflags 2.13.0",
"num-traits",
- "rand",
- "rand_chacha",
+ "rand 0.9.4",
+ "rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
@@ -1755,14 +2087,34 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
]
[[package]]
@@ -1772,7 +2124,16 @@
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
]
[[package]]
@@ -1790,7 +2151,7 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
dependencies = [
- "rand_core",
+ "rand_core 0.9.5",
]
[[package]]
@@ -1837,6 +2198,37 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sha2",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "rstest"
version = "0.26.1"
@@ -1927,6 +2319,20 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "semver"
version = "1.0.28"
@@ -1983,6 +2389,17 @@
"sha1",
]
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "shell-words"
version = "1.1.1"
@@ -2009,6 +2426,16 @@
"libc",
]
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "slab"
version = "0.4.12"
@@ -2027,6 +2454,64 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857"
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
+dependencies = [
+ "cipher",
+ "ssh-encoding",
+]
+
+[[package]]
+name = "ssh-encoding"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
+dependencies = [
+ "base64ct",
+ "pem-rfc7468",
+ "sha2",
+]
+
+[[package]]
+name = "ssh-key"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
+dependencies = [
+ "ed25519-dalek",
+ "p256",
+ "p384",
+ "p521",
+ "rand_core 0.6.4",
+ "rsa",
+ "sec1",
+ "sha2",
+ "signature",
+ "ssh-cipher",
+ "ssh-encoding",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -2045,6 +2530,12 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "syn"
version = "2.0.118"
@@ -2212,6 +2703,12 @@
"winapi-util",
]
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
@@ -2302,6 +2799,12 @@
"syn",
]
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
[[package]]
name = "zlib-rs"
version = "0.6.6"
Cargo.toml
@@ -1,6 +1,10 @@
[workspace]
resolver = "3"
-members = ["crates/ents-model", "crates/gix-ref-store"]
+members = [
+ "crates/ents-model",
+ "crates/ents-testutil",
+ "crates/gix-ref-store",
+]
[workspace.package]
edition = "2024"
@@ -12,6 +16,9 @@
missing_docs = "warn"
[workspace.dependencies]
+ents-model = { path = "crates/ents-model" }
+ents-testutil = { path = "crates/ents-testutil" }
+gix-ref-store = { path = "crates/gix-ref-store" }
arborium = { version = "2.18", default-features = false, features = [
"lang-rust",
"lang-toml",
@@ -63,6 +70,7 @@
rstest = "0.26"
semver = "1"
spdx = "0.13"
+ssh-key = { version = "0.6", features = ["ed25519"] }
target-lexicon = "0.13"
tempfile = "3"
thiserror = "2"
crates/ents-testutil/Cargo.toml
@@ -1,0 +1,22 @@
+[package]
+name = "ents-testutil"
+version = "0.0.0"
+edition.workspace = true
+publish.workspace = true
+license.workspace = true
+
+[dependencies]
+ents-model = { workspace = true }
+facet = { workspace = true }
+facet-git-tree = { workspace = true }
+gix = { workspace = true }
+gix-hash = { workspace = true }
+gix-object = { workspace = true }
+gix-ref-store = { workspace = true }
+ssh-key = { workspace = true }
+
+[dev-dependencies]
+rstest = { workspace = true }
+
+[lints]
+workspace = true
crates/ents-testutil/src/commit.rs
@@ -1,0 +1,107 @@
+//! A commit builder that signs the way git's SSH signing does.
+
+use gix_hash::ObjectId;
+use gix_object::{Commit, Kind, Write, WriteTo as _};
+
+use crate::keys::Keypair;
+
+/// The inputs for one fixture commit; see [`write_commit`].
+///
+/// Author and committer are fixed fixture identities — only the timestamp
+/// varies, because timestamps are what revocation-boundary logic keys on.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::{CommitSpec, ObjectStore, empty_tree, write_commit};
+///
+/// let objects = ObjectStore::default();
+/// let tree = empty_tree(&objects);
+/// let spec = CommitSpec {
+/// tree,
+/// parents: vec![],
+/// message: "Initial".into(),
+/// seconds: 1_000,
+/// };
+/// let oid = write_commit(&objects, &spec, None);
+/// assert!(objects.get(&oid).is_some());
+/// ```
+#[derive(Debug, Clone)]
+pub struct CommitSpec {
+ /// The tree this commit records.
+ pub tree: ObjectId,
+ /// Parent commits, in order; empty for a root commit.
+ pub parents: Vec<ObjectId>,
+ /// The full commit message, trailers included.
+ pub message: String,
+ /// Author and committer timestamp, in seconds since the Unix epoch.
+ pub seconds: i64,
+}
+
+fn actor(seconds: i64) -> gix::actor::Signature {
+ gix::actor::Signature {
+ name: "Fixture".into(),
+ email: "fixture@ents.test".into(),
+ time: gix::date::Time { seconds, offset: 0 },
+ }
+}
+
+/// Write the commit described by `spec` into `objects`, signing it with
+/// `key` when one is given.
+///
+/// Signing works exactly the way `git commit -S` with an SSH key does: the
+/// SSHSIG (namespace `git`) is computed over the commit object serialized
+/// *without* its `gpgsig` header, then stored as that header's value — so
+/// the signature replicates with the repository and verifies offline in
+/// every clone.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::{CommitSpec, Keypair, ObjectStore, empty_tree, write_commit};
+///
+/// let objects = ObjectStore::default();
+/// let key = Keypair::from_seed(1);
+/// let spec = CommitSpec {
+/// tree: empty_tree(&objects),
+/// parents: vec![],
+/// message: "Signed".into(),
+/// seconds: 1_000,
+/// };
+/// let oid = write_commit(&objects, &spec, Some(&key));
+///
+/// // The stored bytes carry the signature as repository data.
+/// let raw = objects.get(&oid).expect("stored");
+/// # let gix_object::Object::Commit(commit) = raw else { panic!("not a commit") };
+/// assert!(commit.extra_headers.iter().any(|(k, _)| k == "gpgsig"));
+/// ```
+pub fn write_commit(objects: &impl Write, spec: &CommitSpec, key: Option<&Keypair>) -> ObjectId {
+ let mut commit = Commit {
+ tree: spec.tree,
+ parents: spec.parents.clone().into(),
+ author: actor(spec.seconds),
+ committer: actor(spec.seconds),
+ encoding: None,
+ message: spec.message.clone().into(),
+ extra_headers: Vec::new(),
+ };
+
+ if let Some(key) = key {
+ let mut payload = Vec::new();
+ commit
+ .write_to(&mut payload)
+ .expect("serializing a commit to a Vec cannot fail");
+ let pem = key.sign(&payload);
+ commit
+ .extra_headers
+ .push(("gpgsig".into(), pem.trim_end().into()));
+ }
+
+ let mut raw = Vec::new();
+ commit
+ .write_to(&mut raw)
+ .expect("serializing a commit to a Vec cannot fail");
+ objects
+ .write_buf(Kind::Commit, &raw)
+ .expect("in-memory object write cannot fail")
+}
crates/ents-testutil/src/counting.rs
@@ -1,0 +1,69 @@
+//! An object-read counter, for asserting incremental-evaluation bounds.
+
+use std::cell::Cell;
+
+use gix_object::Find;
+use gix_object::find;
+
+/// Wraps any [`Find`] and counts `try_find` calls, so a test can assert
+/// that an "incremental" code path really is bounded — for example, that
+/// evaluating a query entry set after a one-commit advance does not
+/// re-walk a three-hundred-commit history (`query.incremental`).
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::{CountingFind, ObjectStore, empty_tree};
+/// use gix_object::Find as _;
+///
+/// let objects = ObjectStore::default();
+/// let tree = empty_tree(&objects);
+///
+/// let counting = CountingFind::new(&objects);
+/// let mut buf = Vec::new();
+/// counting.try_find(&tree, &mut buf).expect("readable");
+/// assert_eq!(counting.reads(), 1);
+///
+/// counting.reset();
+/// assert_eq!(counting.reads(), 0);
+/// ```
+#[derive(Debug)]
+pub struct CountingFind<'a, F: Find> {
+ inner: &'a F,
+ reads: Cell<usize>,
+}
+
+impl<'a, F: Find> CountingFind<'a, F> {
+ /// Wrap `inner`, starting the counter at zero.
+ #[must_use]
+ pub fn new(inner: &'a F) -> Self {
+ Self {
+ inner,
+ reads: Cell::new(0),
+ }
+ }
+
+ /// How many `try_find` calls have happened since the last
+ /// [`CountingFind::reset`].
+ #[must_use]
+ pub fn reads(&self) -> usize {
+ self.reads.get()
+ }
+
+ /// Zero the counter — typically after warming caches, so an assertion
+ /// covers only the increment under test.
+ pub fn reset(&self) {
+ self.reads.set(0);
+ }
+}
+
+impl<F: Find> Find for CountingFind<'_, F> {
+ fn try_find<'b>(
+ &self,
+ id: &gix_hash::oid,
+ buffer: &'b mut Vec<u8>,
+ ) -> Result<Option<gix_object::Data<'b>>, find::Error> {
+ self.reads.set(self.reads.get().saturating_add(1));
+ self.inner.try_find(id, buffer)
+ }
+}
crates/ents-testutil/src/keys.rs
@@ -1,0 +1,79 @@
+//! Deterministic SSH signing keys for fixtures.
+
+use ssh_key::private::{Ed25519Keypair, KeypairData};
+use ssh_key::{HashAlg, LineEnding, PrivateKey};
+
+/// The SSHSIG namespace git uses when signing commits with an SSH key.
+pub const GIT_SIGN_NAMESPACE: &str = "git";
+
+/// A deterministic ed25519 keypair for signing fixture commits.
+///
+/// Deterministic (seeded) rather than random so property tests and
+/// build-the-fixture-twice "clone" tests reproduce byte-identical objects.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::Keypair;
+///
+/// let a = Keypair::from_seed(1);
+/// let b = Keypair::from_seed(1);
+/// assert_eq!(a.public_openssh(), b.public_openssh());
+/// assert_ne!(a.public_openssh(), Keypair::from_seed(2).public_openssh());
+/// ```
+#[derive(Debug)]
+pub struct Keypair {
+ private: PrivateKey,
+}
+
+impl Keypair {
+ /// Derive a keypair from a one-byte seed (repeated to fill the ed25519
+ /// seed), so tests can name keys `1`, `2`, ... and always get the same
+ /// key back.
+ #[must_use]
+ pub fn from_seed(seed: u8) -> Self {
+ let pair = Ed25519Keypair::from_seed(&[seed; 32]);
+ let private = PrivateKey::new(KeypairData::from(pair), "ents-testutil")
+ .expect("a well-formed ed25519 keypair is always accepted");
+ Self { private }
+ }
+
+ /// The public half in OpenSSH single-line format — exactly what a
+ /// [`ents_model::Member`]'s `key` field carries.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ents_testutil::Keypair;
+ ///
+ /// let key = Keypair::from_seed(1).public_openssh();
+ /// assert!(key.starts_with("ssh-ed25519 "));
+ /// ```
+ #[must_use]
+ pub fn public_openssh(&self) -> String {
+ self.private
+ .public_key()
+ .to_openssh()
+ .expect("an ed25519 public key always renders")
+ }
+
+ /// Sign `payload` in git's SSHSIG namespace, returning the armored
+ /// signature block git would store in a commit's `gpgsig` header.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ents_testutil::Keypair;
+ ///
+ /// let pem = Keypair::from_seed(1).sign(b"payload");
+ /// assert!(pem.starts_with("-----BEGIN SSH SIGNATURE-----"));
+ /// ```
+ #[must_use]
+ pub fn sign(&self, payload: &[u8]) -> String {
+ self.private
+ .sign(GIT_SIGN_NAMESPACE, HashAlg::Sha512, payload)
+ .expect("ed25519 signing is infallible for any payload")
+ .to_pem(LineEnding::LF)
+ .expect("an SSHSIG always renders as PEM")
+ }
+}
crates/ents-testutil/src/lib.rs
@@ -1,0 +1,66 @@
+//! Shared test fixtures for `git-ents` crates: an in-memory ref store, an
+//! in-memory object store, deterministic signing keys, a signed-commit
+//! builder, and seeding helpers for members, results, and code-ref history.
+//!
+//! This is the integration harness the engineering conventions call for —
+//! a dev-dependency-only crate, never linked into a shipping binary. It
+//! exists so `ents-gate` and `ents-query` (and later `ents-receive`,
+//! `ents-sync`, `ents-effect`) exercise the same fixture vocabulary instead
+//! of each reinventing repos, keys, and commit plumbing.
+//!
+//! Everything here panics on setup failure (via `expect`) rather than
+//! returning `Result`: a fixture that cannot be built is a broken test,
+//! not a condition under test.
+//!
+//! # Examples
+//!
+//! A complete miniature forge: one member enrolled under
+//! `refs/meta/member/*`, one signed mutation, both readable back through
+//! the same read-half trait production code uses.
+//!
+//! ```
+//! use ents_model::Provenance;
+//! use ents_testutil::{Keypair, MemRefStore, ObjectStore, enroll_member};
+//! use gix_ref_store::RefStoreRead;
+//!
+//! let refs = MemRefStore::default();
+//! let objects = ObjectStore::default();
+//! let key = Keypair::from_seed(1);
+//!
+//! let tip = enroll_member(&refs, &objects, "jdc", &key, Provenance::AdminRegistered, 1_000);
+//!
+//! let name: gix::refs::FullName = "refs/meta/member/jdc".try_into().expect("valid");
+//! assert_eq!(refs.get(name.as_ref()).expect("readable"), Some(tip));
+//! ```
+
+#![expect(
+ clippy::expect_used,
+ reason = "test-support crate: fixtures panic on setup failure rather than returning Result"
+)]
+
+pub use facet_git_tree::ObjectStore;
+
+mod commit;
+mod counting;
+mod keys;
+mod refs;
+mod seed;
+
+pub use commit::{CommitSpec, write_commit};
+pub use counting::CountingFind;
+pub use keys::Keypair;
+pub use refs::MemRefStore;
+pub use seed::{
+ advance_ref, empty_tree, enroll_member, record_result, write_member, write_meta_entity,
+};
+
+/// Read a ref from a fixture store, panicking on the (impossible for
+/// [`MemRefStore`]) backend error.
+pub(crate) fn refs_get(
+ refs: &MemRefStore,
+ name: &gix::refs::FullName,
+) -> Option<gix_hash::ObjectId> {
+ use gix_ref_store::RefStoreRead as _;
+ refs.get(name.as_ref())
+ .expect("in-memory ref read cannot fail")
+}
crates/ents-testutil/src/refs.rs
@@ -1,0 +1,122 @@
+//! An in-memory [`RefStoreRead`] implementation for fixtures.
+
+use std::collections::BTreeMap;
+use std::sync::Mutex;
+
+use gix::refs::{FullName, FullNameRef};
+use gix_hash::ObjectId;
+use gix_ref_store::{RefIter, RefStoreRead, Result};
+
+/// An in-memory ref store: a name-to-oid map behind the same
+/// [`RefStoreRead`] trait production code consumes.
+///
+/// Mutation happens through [`MemRefStore::set`] and
+/// [`MemRefStore::remove`] — deliberately *not* through the `RefStore`
+/// write half, because fixtures seed state directly; the CAS discipline
+/// itself is `gix-ref-store`'s to test.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::MemRefStore;
+/// use gix_ref_store::RefStoreRead;
+///
+/// let store = MemRefStore::default();
+/// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
+/// let oid = gix_hash::ObjectId::null(gix_hash::Kind::Sha1);
+///
+/// store.set(name.as_ref(), oid);
+/// assert_eq!(store.get(name.as_ref()).expect("readable"), Some(oid));
+///
+/// store.remove(name.as_ref());
+/// assert_eq!(store.get(name.as_ref()).expect("readable"), None);
+/// ```
+#[derive(Debug, Default)]
+pub struct MemRefStore {
+ refs: Mutex<BTreeMap<String, ObjectId>>,
+}
+
+impl MemRefStore {
+ /// Set `name` to `oid`, creating or overwriting it.
+ pub fn set(&self, name: &FullNameRef, oid: ObjectId) {
+ self.locked().insert(name.as_bstr().to_string(), oid);
+ }
+
+ /// Set the ref named by `name` (a full refname string) to `oid`.
+ ///
+ /// Panics if `name` is not a valid full refname.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ents_testutil::MemRefStore;
+ ///
+ /// let store = MemRefStore::default();
+ /// store.set_str("refs/heads/main", gix_hash::ObjectId::null(gix_hash::Kind::Sha1));
+ /// ```
+ pub fn set_str(&self, name: &str, oid: ObjectId) {
+ let name: FullName = name.try_into().expect("valid refname in fixture");
+ self.set(name.as_ref(), oid);
+ }
+
+ /// Delete `name` if present.
+ pub fn remove(&self, name: &FullNameRef) {
+ let key = name.as_bstr().to_string();
+ let _removed = self.locked().remove(&key);
+ }
+
+ /// A deep copy of this store's current refs — the fixture analogue of
+ /// a fetch, for pre-flight call-site tests that evaluate against a
+ /// clone's ref state.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ents_testutil::MemRefStore;
+ /// use gix_ref_store::RefStoreRead;
+ ///
+ /// let store = MemRefStore::default();
+ /// store.set_str("refs/heads/main", gix_hash::ObjectId::null(gix_hash::Kind::Sha1));
+ ///
+ /// let fetched = store.fetched_copy();
+ /// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
+ /// assert_eq!(
+ /// fetched.get(name.as_ref()).expect("readable"),
+ /// store.get(name.as_ref()).expect("readable"),
+ /// );
+ /// ```
+ #[must_use]
+ pub fn fetched_copy(&self) -> Self {
+ Self {
+ refs: Mutex::new(self.locked().clone()),
+ }
+ }
+
+ fn locked(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, ObjectId>> {
+ self.refs
+ .lock()
+ .expect("ref-store mutex poisoned in fixture")
+ }
+}
+
+impl RefStoreRead for MemRefStore {
+ fn get(&self, name: &FullNameRef) -> Result<Option<ObjectId>> {
+ Ok(self.locked().get(&name.as_bstr().to_string()).copied())
+ }
+
+ fn iter_prefix(&self, prefix: &str) -> Result<RefIter> {
+ let snapshot: Vec<_> = self
+ .locked()
+ .range(prefix.to_owned()..)
+ .take_while(|(name, _)| name.starts_with(prefix))
+ .map(|(name, oid)| {
+ let full: FullName = name
+ .as_str()
+ .try_into()
+ .expect("only valid refnames are ever inserted");
+ Ok((full, *oid))
+ })
+ .collect();
+ Ok(RefIter::new(snapshot.into_iter()))
+ }
+}
crates/ents-testutil/src/seed.rs
@@ -1,0 +1,222 @@
+//! Seeding helpers: members, meta entities, results, and code-ref history.
+
+use ents_model::trailer::Trailers;
+use ents_model::{Member, MemberId, Provenance, Status, namespace};
+use gix::refs::FullName;
+use gix_hash::ObjectId;
+use gix_object::{Find, Kind, Write};
+
+use crate::commit::{CommitSpec, write_commit};
+use crate::keys::Keypair;
+use crate::refs::MemRefStore;
+
+/// Write the empty tree into `objects` and return its id.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::{ObjectStore, empty_tree};
+///
+/// let objects = ObjectStore::default();
+/// let tree = empty_tree(&objects);
+/// assert_eq!(tree.to_string(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
+/// ```
+pub fn empty_tree(objects: &impl Write) -> ObjectId {
+ objects
+ .write_buf(Kind::Tree, b"")
+ .expect("in-memory object write cannot fail")
+}
+
+/// Serialize `entity` as its typed tree and land it on `refname` as a
+/// mutation commit carrying the `Ents-Ref:` trailer, signed by `signer`
+/// when one is given. Parents come from `refname`'s current tip. Returns
+/// the new tip.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::{Keypair, MemRefStore, ObjectStore, write_meta_entity};
+/// use gix_ref_store::RefStoreRead;
+///
+/// let refs = MemRefStore::default();
+/// let objects = ObjectStore::default();
+/// let name: gix::refs::FullName = "refs/meta/issues/1".try_into().expect("valid");
+///
+/// let issue = ents_model::Issue {
+/// title: "t".into(),
+/// body: "b".into(),
+/// state: "open".into(),
+/// assignees: vec![],
+/// labels: vec![],
+/// };
+/// let tip = write_meta_entity(&refs, &objects, name.clone(), &issue, None, 1_000);
+/// assert_eq!(refs.get(name.as_ref()).expect("readable"), Some(tip));
+/// ```
+pub fn write_meta_entity<T: for<'facet> facet::Facet<'facet>>(
+ refs: &MemRefStore,
+ objects: &(impl Write + Find),
+ refname: FullName,
+ entity: &T,
+ signer: Option<&Keypair>,
+ seconds: i64,
+) -> ObjectId {
+ let tree =
+ facet_git_tree::serialize_into(entity, objects).expect("fixture entity always serializes");
+ let trailers = Trailers {
+ ents_ref: Some(refname.clone()),
+ schema_version: None,
+ };
+ let message = format!("Mutate {}\n\n{}", refname.as_bstr(), trailers.render());
+ let parents = crate::refs_get(refs, &refname).into_iter().collect();
+ let tip = write_commit(
+ objects,
+ &CommitSpec {
+ tree,
+ parents,
+ message,
+ seconds,
+ },
+ signer,
+ );
+ refs.set(refname.as_ref(), tip);
+ tip
+}
+
+/// Enroll `id` as a member whose key is `key`'s public half, active, with
+/// the given provenance, and return the member ref's new tip.
+///
+/// The enrollment commit is signed by the member's own key — the
+/// self-admitting shape the bootstrap window admits.
+///
+/// # Examples
+///
+/// ```
+/// use ents_model::Provenance;
+/// use ents_testutil::{Keypair, MemRefStore, ObjectStore, enroll_member};
+///
+/// let refs = MemRefStore::default();
+/// let objects = ObjectStore::default();
+/// enroll_member(&refs, &objects, "jdc", &Keypair::from_seed(1), Provenance::AdminRegistered, 500);
+/// ```
+pub fn enroll_member(
+ refs: &MemRefStore,
+ objects: &(impl Write + Find),
+ id: &str,
+ key: &Keypair,
+ provenance: Provenance,
+ seconds: i64,
+) -> ObjectId {
+ let member = Member::new(key.public_openssh(), provenance);
+ write_member(refs, objects, id, &member, Some(key), seconds)
+}
+
+/// Land an arbitrary [`Member`] state on `refs/meta/member/<id>` — the
+/// general form of [`enroll_member`], for revocations, unrevocations, and
+/// promotions, signed by whoever `signer` is (an admin, usually).
+///
+/// # Examples
+///
+/// ```
+/// use ents_model::{Member, Provenance};
+/// use ents_testutil::{Keypair, MemRefStore, ObjectStore, write_member};
+///
+/// let refs = MemRefStore::default();
+/// let objects = ObjectStore::default();
+/// let key = Keypair::from_seed(1);
+///
+/// let mut member = Member::new(key.public_openssh(), Provenance::AdminRegistered);
+/// member.revoke();
+/// write_member(&refs, &objects, "jdc", &member, Some(&key), 900);
+/// ```
+pub fn write_member(
+ refs: &MemRefStore,
+ objects: &(impl Write + Find),
+ id: &str,
+ member: &Member,
+ signer: Option<&Keypair>,
+ seconds: i64,
+) -> ObjectId {
+ let refname = namespace::member_ref(&MemberId::new(id)).expect("valid member id in fixture");
+ write_meta_entity(refs, objects, refname, member, signer, seconds)
+}
+
+/// Record a result of `status` for `effect` on the commit whose hex id
+/// starts with `short_oid`, at the canonical
+/// `refs/meta/results/<effect>/<short_oid>` ref (`effect.results-writeback`).
+///
+/// The result commit is unsigned unless `signer` is given — query tests
+/// exercise scan semantics, gate tests exercise signatures.
+///
+/// # Examples
+///
+/// ```
+/// use ents_model::Status;
+/// use ents_testutil::{MemRefStore, ObjectStore, record_result};
+///
+/// let refs = MemRefStore::default();
+/// let objects = ObjectStore::default();
+/// record_result(&refs, &objects, "unit", "abc123", Status::Pass, None, 1_000);
+/// ```
+pub fn record_result(
+ refs: &MemRefStore,
+ objects: &(impl Write + Find),
+ effect: &str,
+ short_oid: &str,
+ status: Status,
+ signer: Option<&Keypair>,
+ seconds: i64,
+) -> ObjectId {
+ let refname =
+ namespace::result_ref(effect, short_oid).expect("valid result segments in fixture");
+ write_meta_entity(refs, objects, refname, &status, signer, seconds)
+}
+
+/// Append `count` empty-tree commits on top of `refname`'s current tip
+/// (creating the ref if absent), one second apart starting at
+/// `start_seconds`, and return the new commits oldest-first.
+///
+/// # Examples
+///
+/// ```
+/// use ents_testutil::{MemRefStore, ObjectStore, advance_ref};
+/// use gix_ref_store::RefStoreRead;
+///
+/// let refs = MemRefStore::default();
+/// let objects = ObjectStore::default();
+/// let commits = advance_ref(&refs, &objects, "refs/heads/main", 3, 100);
+/// assert_eq!(commits.len(), 3);
+///
+/// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
+/// assert_eq!(refs.get(name.as_ref()).expect("readable"), commits.last().copied());
+/// ```
+pub fn advance_ref(
+ refs: &MemRefStore,
+ objects: &(impl Write + Find),
+ refname: &str,
+ count: usize,
+ start_seconds: i64,
+) -> Vec<ObjectId> {
+ let name: FullName = refname.try_into().expect("valid refname in fixture");
+ let tree = empty_tree(objects);
+ let mut tip = crate::refs_get(refs, &name);
+ let mut out = Vec::with_capacity(count);
+ for i in 0..count {
+ let seconds = start_seconds.saturating_add(i64::try_from(i).unwrap_or(i64::MAX));
+ let commit = write_commit(
+ objects,
+ &CommitSpec {
+ tree,
+ parents: tip.into_iter().collect(),
+ message: format!("{refname} commit {i} at {seconds}"),
+ seconds,
+ },
+ None,
+ );
+ tip = Some(commit);
+ out.push(commit);
+ }
+ if let Some(tip) = tip {
+ refs.set(name.as_ref(), tip);
+ }
+ out
+}