Unbound hosted repositories adopt the first pusher's id 75167672ebcd
A bare repository created with init_bare_unbound reports an all-zero id. Clients refuse to clone or fetch from one and send Oxy-Repo-Id on every request; the server binds the repository to that id under the push lock, so an existing repository moved to a hub keeps its identity (protocol §2).
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-27 17:15 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
75167672ebcdfc798181db598f9e72a4987236c9c376e5db1f8fde30ec031fc7- Depends on
- 02f1b14af1e2 1196b29b5fa9 132b884b9a8d 135212de5370 1983686fb7a7 20247a03a596 23b217d75848 252e8cb23fbc 2863456a0f6d 28fc23d7706a 2b5ee371060e 2b872d074ddb 30ea9c1e70fa 4dee277c327a 4e88e774777a 50a046e439e9 5b1114a04905 5b9c6ad45f6e 614541c3cebd 6706bdcfb63e 694805cca585 6d3eefbce2f0 6e723b81f5b9 728da0f92161 7421309f5372 750251b21a96 79ab8528ff62 79d03f826ecd 7b9809e67c3d 83e0677b07fe 86e682c242c6 8b678b298fee 8f10a07a1dbe 94084087a876 957fee30826e 96f7610fa666 982864068a59 9ac1fbe211b4 a269bee11bb2 b3c5a59a875a b7f102fcbb17 b837d2b55853 ba160f60219e c07e0e7d2d85 c3069537d7c5 c6aa5bd9d1c8 c792033af344 cdb5beba7f05 d02e545dc265 d8c086ca3eb2 e02bf0cd0533 ee06a6c557b7 f9c98deef326 fce44ae9ad29 ff0af1ee299d
- Depended on by
- 232aabb5452d 284fe47d98b9 aded4110f0bd 2f0b4b0651ea c397a8c36a9f 6f4226bfb87f 0588de69a84d 64dc5197f9df b5e98094eb8f 642fa213de80 80b069facc69 4afe288b7946 9bd98cfb4f7e 544d0541a7e5 4476cdbb0443 570e35310f31 e30c081a5064 f3811eb20bea 7bd805ffb418 d3b270909c78 b63fc7a77fc6 ee130dd9eef2 1ec6fdbc6bcb ef7d4ea6f33b d36ba2f79f95 028545dfa7a4 3652104ae7d3 c2e1d09047db 0299076f09a2 733ea16a9701 767ac6cd0545 4b9d06778581 68dcf21fe055 f53cbe7a204e f0a517d28237 3a6effc74835 c6e69e4b8d8f 74b4a3c62ea4 092457020692 a39754d02c58 e5d9b0513f3e b582948f1912 68a924846379 aa4939e07a09 9b723f69b032 b689779e9e74 530a0fc650a2 9d580f2132f3 d6e0d5b8b339 cb8b8082fa73 ac03130a2b95 e8e6e0cac26a 79b5a90ffd43 7505194116e7 19303b4a06fb 170bb436e1d5 f6dccc4533c8 6eae06b6ec04 42ccf504aa50 54e2e6309aa3 bf3a67819b59 d069701a4997 dd21fd82687d 614f55e14eee 5cde7882d767 ff1a64f81c1c 648189388f1a 58dfe0e134eb 2c9ddac3c387 47ce8b8e2d4b 7ba155f20aa0 9ae4d15a0e4b 9aa399044d9d 9f9e72b93e4e a46f3e4f50b1 b35df36b7dc5 631fc8970bd0 5b4c51307b14 360aa74a6277 236f35c0baba 0389c3e82f6c e96a430c78f3 33472ccc08cc b30a880fa409 1ec3f008e96a 77445673174b 3604179679eb c07406bcc237 98a1620a3840 2bf22305bc48 14edbaaf5291 ff1f62fee654 d4dbab97e948 a17fc622ca43 e266b7d4065c 073a2115995f f9466021286a ce14f0d68b15 4cce912a0953 36e51aa806c6 1966f5db15ee 76740263089c 06d16dc2bc13 05059d434aca 992467d31d26 d0990c2bff74 f9575918477f 40a40bb7b798 dbf06d342e1d e83354cdac22 2306a4d24779 d0d52107b074 4d6c9c1b358c 0f93f49ce059 da652d6392d8 a87c7b9d563a 2c75672fb2fe 519c7c061bcb 598009a75f6c 01a47cd7f0c8 192fad1e46a2 8b276e0106d1 69816d688300 0554aaa7ddef eaf3e9068180 d3cd7ef95da6 c5ce0367eddf 57eaac9107df da868d5d2646 86e1824f2d0b 7d9dab131937 1a3b62032ed9 4fe7d6045860 6caacfff1d4f 6f8aeb996558 a9f686edf60b e293c90dca62 dfb23a3a8cfa 958fb27dda7c fc1a466a1e66 83d61e803ccd f7510b63cceb 8ab265dc4a51 e7ac6c2e84e4 6a81dbab471d 1c2bd9abdd4f be451356e3ef 61e3bce7ea5b 0912a0337a95 8c30d814f2cc f8c695764c95 cc7a00565161 e8d38e47e9d7 60a1223c894f 0955469ac0e1 7df9007b163c afaba5f1306d 696bbf29d3eb 0bdc3f91a39f bc489b6c7f69 c863c7a7a2d1 62794ecc7f74 d42369abf180 3e5772ccf9da 19555025a2ac e7ddd552b619 07eb99be126e 4dcb8c0916b7 3526d339519f eae595568832 84b9e9ac0ab4 be0161a88dd7 76f752503e7e 119bbf50cb86 7aa2dc9369e3 05e71ed6cdf1
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 1686 atoms, 0 tree operations, 49 files
Browse the tree at this change
modified crates/oxy-core/src/attest.rs +104 −26
| … | 1 unchanged lines |
||
| 2 | 2 | //! of changes. Only the `ref` and `seal` kinds exist so far. |
|
| 3 | 3 | ||
| 4 | 4 | use crate::cbor::{self, Value}; |
|
| 5 | - | use crate::hash::{KeyId, hash_object, ChangeHash, ObjectRef, ObjectType}; |
|
| 6 | - | use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, Hlc, ObjectError, FORMAT_VERSION}; |
|
| 5 | + | use crate::hash::{hash_object, ChangeHash, KeyId, ObjectRef, ObjectType}; |
|
| 6 | + | use crate::objects::{ |
|
| 7 | + | array, bytes, check_version, field, map, oref, text, uint, Hlc, ObjectError, FORMAT_VERSION, |
|
| 8 | + | }; |
|
| 7 | 9 | ||
| 8 | 10 | pub const MAX_SEAL_HASHES: usize = 4096; |
|
| 9 | 11 | ||
| 10 | 12 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 11 | 13 | pub enum AttestBody { |
|
| 12 | 14 | /// A bookmark moved from `old_state` to `new_state` (spec §10.2). |
|
| 13 | - | Ref { name: String, old_state: ObjectRef, new_state: ObjectRef, serial: u64 }, |
|
| 15 | + | Ref { |
|
| 16 | + | name: String, |
|
| 17 | + | old_state: ObjectRef, |
|
| 18 | + | new_state: ObjectRef, |
|
| 19 | + | serial: u64, |
|
| 20 | + | }, |
|
| 14 | 21 | /// One signature covering a batch of changes (spec §14.4). |
|
| 15 | 22 | Seal { hashes: Vec<ChangeHash> }, |
|
| 16 | 23 | /// An immutable named state (spec §10.1). |
|
| 17 | - | Tag { name: String, state: ObjectRef, message: String }, |
|
| 24 | + | Tag { |
|
| 25 | + | name: String, |
|
| 26 | + | state: ObjectRef, |
|
| 27 | + | message: String, |
|
| 28 | + | }, |
|
| 18 | 29 | /// The signing key's identity was replaced by `new` (spec §14.2). |
|
| 19 | 30 | Rotate { new: KeyId }, |
|
| 20 | 31 | /// `key` must no longer be trusted (spec §14.2). |
|
| … | 22 unchanged lines |
||
| 43 | 54 | pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest { |
|
| 44 | 55 | hashes.sort(); |
|
| 45 | 56 | hashes.dedup(); |
|
| 46 | - | Attest { repo, hlc: Hlc::now(), body: AttestBody::Seal { hashes } } |
|
| 57 | + | Attest { |
|
| 58 | + | repo, |
|
| 59 | + | hlc: Hlc::now(), |
|
| 60 | + | body: AttestBody::Seal { hashes }, |
|
| 61 | + | } |
|
| 47 | 62 | } |
|
| 48 | 63 | ||
| 49 | 64 | pub fn to_value(&self) -> Value { |
|
| 50 | 65 | let body = match &self.body { |
|
| 51 | - | AttestBody::Ref { name, old_state, new_state, serial } => Value::map([ |
|
| 66 | + | AttestBody::Ref { |
|
| 67 | + | name, |
|
| 68 | + | old_state, |
|
| 69 | + | new_state, |
|
| 70 | + | serial, |
|
| 71 | + | } => Value::map([ |
|
| 52 | 72 | (0, Value::Text(name.clone())), |
|
| 53 | 73 | (1, Value::Bytes(old_state.0.to_vec())), |
|
| 54 | 74 | (2, Value::Bytes(new_state.0.to_vec())), |
|
| 55 | 75 | (3, Value::Uint(*serial)), |
|
| 56 | 76 | ]), |
|
| 57 | - | AttestBody::Seal { hashes } => { |
|
| 58 | - | Value::map([(0, Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()))]) |
|
| 59 | - | } |
|
| 60 | - | AttestBody::Tag { name, state, message } => { |
|
| 61 | - | Value::map([(0, Value::Text(name.clone())), (1, Value::Bytes(state.0.to_vec())), (2, Value::Text(message.clone()))]) |
|
| 62 | - | } |
|
| 77 | + | AttestBody::Seal { hashes } => Value::map([( |
|
| 78 | + | 0, |
|
| 79 | + | Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()), |
|
| 80 | + | )]), |
|
| 81 | + | AttestBody::Tag { |
|
| 82 | + | name, |
|
| 83 | + | state, |
|
| 84 | + | message, |
|
| 85 | + | } => Value::map([ |
|
| 86 | + | (0, Value::Text(name.clone())), |
|
| 87 | + | (1, Value::Bytes(state.0.to_vec())), |
|
| 88 | + | (2, Value::Text(message.clone())), |
|
| 89 | + | ]), |
|
| 63 | 90 | AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]), |
|
| 64 | - | AttestBody::Revoke { key, reason } => Value::map([(0, Value::Bytes(key.0.to_vec())), (1, Value::Text(reason.clone()))]), |
|
| 91 | + | AttestBody::Revoke { key, reason } => Value::map([ |
|
| 92 | + | (0, Value::Bytes(key.0.to_vec())), |
|
| 93 | + | (1, Value::Text(reason.clone())), |
|
| 94 | + | ]), |
|
| 65 | 95 | }; |
|
| 66 | 96 | Value::map([ |
|
| 67 | 97 | (0, Value::Uint(FORMAT_VERSION)), |
|
| … | 30 unchanged lines |
||
| 98 | 128 | serial: uint(field(b, 3, "serial")?, "serial")?, |
|
| 99 | 129 | }, |
|
| 100 | 130 | "seal" => { |
|
| 101 | - | let hashes = array(field(b, 0, "hashes")?, "hashes")?.iter().map(|h| oref(h, "hash")).collect::<Result<Vec<_>, _>>()?; |
|
| 102 | - | if hashes.is_empty() || hashes.len() > MAX_SEAL_HASHES || !hashes.windows(2).all(|w| w[0] < w[1]) { |
|
| 103 | - | return Err(ObjectError::Schema("seal: hashes must be 1..4096, sorted and unique".into())); |
|
| 131 | + | let hashes = array(field(b, 0, "hashes")?, "hashes")? |
|
| 132 | + | .iter() |
|
| 133 | + | .map(|h| oref(h, "hash")) |
|
| 134 | + | .collect::<Result<Vec<_>, _>>()?; |
|
| 135 | + | if hashes.is_empty() |
|
| 136 | + | || hashes.len() > MAX_SEAL_HASHES |
|
| 137 | + | || !hashes.windows(2).all(|w| w[0] < w[1]) |
|
| 138 | + | { |
|
| 139 | + | return Err(ObjectError::Schema( |
|
| 140 | + | "seal: hashes must be 1..4096, sorted and unique".into(), |
|
| 141 | + | )); |
|
| 104 | 142 | } |
|
| 105 | 143 | AttestBody::Seal { hashes } |
|
| 106 | 144 | } |
|
| … | 2 unchanged lines |
||
| 109 | 147 | state: oref(field(b, 1, "state")?, "state")?, |
|
| 110 | 148 | message: text(field(b, 2, "message")?, "message")?.to_owned(), |
|
| 111 | 149 | }, |
|
| 112 | - | "rotate" => AttestBody::Rotate { new: oref(field(b, 0, "new")?, "new")? }, |
|
| 113 | - | "revoke" => AttestBody::Revoke { key: oref(field(b, 0, "key")?, "key")?, reason: text(field(b, 1, "reason")?, "reason")?.to_owned() }, |
|
| 114 | - | other => return Err(ObjectError::Schema(format!("unknown attestation kind {other:?}"))), |
|
| 150 | + | "rotate" => AttestBody::Rotate { |
|
| 151 | + | new: oref(field(b, 0, "new")?, "new")?, |
|
| 152 | + | }, |
|
| 153 | + | "revoke" => AttestBody::Revoke { |
|
| 154 | + | key: oref(field(b, 0, "key")?, "key")?, |
|
| 155 | + | reason: text(field(b, 1, "reason")?, "reason")?.to_owned(), |
|
| 156 | + | }, |
|
| 157 | + | other => { |
|
| 158 | + | return Err(ObjectError::Schema(format!( |
|
| 159 | + | "unknown attestation kind {other:?}" |
|
| 160 | + | ))) |
|
| 161 | + | } |
|
| 115 | 162 | }; |
|
| 116 | 163 | Ok(Attest { repo, hlc, body }) |
|
| 117 | 164 | } |
|
| … | 7 unchanged lines |
||
| 125 | 172 | fn round_trips() { |
|
| 126 | 173 | let r = Attest { |
|
| 127 | 174 | repo: [1; 16], |
|
| 128 | - | hlc: Hlc { wall_ms: 3, logical: 0 }, |
|
| 129 | - | body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 7 }, |
|
| 175 | + | hlc: Hlc { |
|
| 176 | + | wall_ms: 3, |
|
| 177 | + | logical: 0, |
|
| 178 | + | }, |
|
| 179 | + | body: AttestBody::Ref { |
|
| 180 | + | name: "main".into(), |
|
| 181 | + | old_state: ObjectRef([1; 32]), |
|
| 182 | + | new_state: ObjectRef([2; 32]), |
|
| 183 | + | serial: 7, |
|
| 184 | + | }, |
|
| 130 | 185 | }; |
|
| 131 | 186 | assert_eq!(Attest::decode(&r.encode()).unwrap(), r); |
|
| 132 | 187 | assert_eq!(r.kind(), "ref"); |
|
| 133 | - | let s = Attest::seal([1; 16], vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])]); |
|
| 188 | + | let s = Attest::seal( |
|
| 189 | + | [1; 16], |
|
| 190 | + | vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])], |
|
| 191 | + | ); |
|
| 134 | 192 | let d = Attest::decode(&s.encode()).unwrap(); |
|
| 135 | - | assert!(matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])])); |
|
| 193 | + | assert!( |
|
| 194 | + | matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])]) |
|
| 195 | + | ); |
|
| 136 | 196 | assert_ne!(s.hash(), r.hash()); |
|
| 137 | - | let t = Attest { repo: [1; 16], hlc: Hlc::default(), body: AttestBody::Tag { name: "v1".into(), state: ObjectRef([5; 32]), message: "release".into() } }; |
|
| 197 | + | let t = Attest { |
|
| 198 | + | repo: [1; 16], |
|
| 199 | + | hlc: Hlc::default(), |
|
| 200 | + | body: AttestBody::Tag { |
|
| 201 | + | name: "v1".into(), |
|
| 202 | + | state: ObjectRef([5; 32]), |
|
| 203 | + | message: "release".into(), |
|
| 204 | + | }, |
|
| 205 | + | }; |
|
| 138 | 206 | assert_eq!(Attest::decode(&t.encode()).unwrap(), t); |
|
| 139 | 207 | assert_eq!(t.kind(), "tag"); |
|
| 140 | 208 | } |
|
| 141 | 209 | ||
| 142 | 210 | #[test] |
|
| 143 | 211 | fn rejects_bad_seals() { |
|
| 144 | - | let empty = Attest { repo: [0; 16], hlc: Hlc::default(), body: AttestBody::Seal { hashes: vec![] } }; |
|
| 212 | + | let empty = Attest { |
|
| 213 | + | repo: [0; 16], |
|
| 214 | + | hlc: Hlc::default(), |
|
| 215 | + | body: AttestBody::Seal { hashes: vec![] }, |
|
| 216 | + | }; |
|
| 145 | 217 | assert!(Attest::decode(&empty.encode()).is_err()); |
|
| 146 | - | let unsorted = Attest { repo: [0; 16], hlc: Hlc::default(), body: AttestBody::Seal { hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])] } }; |
|
| 218 | + | let unsorted = Attest { |
|
| 219 | + | repo: [0; 16], |
|
| 220 | + | hlc: Hlc::default(), |
|
| 221 | + | body: AttestBody::Seal { |
|
| 222 | + | hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])], |
|
| 223 | + | }, |
|
| 224 | + | }; |
|
| 147 | 225 | assert!(Attest::decode(&unsorted.encode()).is_err()); |
|
| 148 | 226 | } |
|
| 149 | 227 | } |
modified crates/oxy-core/src/attributes.rs +20 −4
| … | 42 unchanged lines |
||
| 43 | 43 | continue; |
|
| 44 | 44 | } |
|
| 45 | 45 | let mut parts = line.split_whitespace(); |
|
| 46 | - | let Some(pattern) = parts.next() else { continue }; |
|
| 46 | + | let Some(pattern) = parts.next() else { |
|
| 47 | + | continue; |
|
| 48 | + | }; |
|
| 47 | 49 | let attrs: Vec<(String, Option<String>)> = parts |
|
| 48 | 50 | .map(|a| match a.split_once('=') { |
|
| 49 | 51 | Some((k, v)) => (k.to_string(), Some(v.to_string())), |
|
| … | 9 unchanged lines |
||
| 59 | 61 | (false, false) => format!("{dir}/**/{core}"), |
|
| 60 | 62 | }; |
|
| 61 | 63 | if let Ok(g) = Glob::new(&full) { |
|
| 62 | - | self.rules.push(Rule { matcher: g.compile_matcher(), attrs }); |
|
| 64 | + | self.rules.push(Rule { |
|
| 65 | + | matcher: g.compile_matcher(), |
|
| 66 | + | attrs, |
|
| 67 | + | }); |
|
| 63 | 68 | } |
|
| 64 | 69 | } |
|
| 65 | 70 | } |
|
| … | 11 unchanged lines |
||
| 77 | 82 | let mut entries: Vec<(&str, &[u8])> = sorted.into_iter().collect(); |
|
| 78 | 83 | entries.sort_by_key(|(p, _)| p.matches('/').count()); |
|
| 79 | 84 | for (p, c) in entries { |
|
| 80 | - | let dir = p.strip_suffix(".oxyattributes").unwrap_or("").trim_end_matches('/'); |
|
| 85 | + | let dir = p |
|
| 86 | + | .strip_suffix(".oxyattributes") |
|
| 87 | + | .unwrap_or("") |
|
| 88 | + | .trim_end_matches('/'); |
|
| 81 | 89 | out.add_file(dir, &String::from_utf8_lossy(c)); |
|
| 82 | 90 | } |
|
| 83 | 91 | out |
|
| … | 68 unchanged lines |
||
| 152 | 160 | ("src/.oxyattributes", &b"*.dat binary\ngen.txt -eol\n"[..]), |
|
| 153 | 161 | ("README.md", &b"not attributes"[..]), |
|
| 154 | 162 | ]); |
|
| 155 | - | assert_eq!(a.lookup("a/b/logo.png"), Attrs { binary: Some(true), eol: None, nocompress: true, lockable: false }); |
|
| 163 | + | assert_eq!( |
|
| 164 | + | a.lookup("a/b/logo.png"), |
|
| 165 | + | Attrs { |
|
| 166 | + | binary: Some(true), |
|
| 167 | + | eol: None, |
|
| 168 | + | nocompress: true, |
|
| 169 | + | lockable: false |
|
| 170 | + | } |
|
| 171 | + | ); |
|
| 156 | 172 | assert_eq!(a.lookup("notes.txt").eol, Some(Eol::Lf)); |
|
| 157 | 173 | assert_eq!(a.lookup("docs/notes.txt").eol, Some(Eol::Crlf)); |
|
| 158 | 174 | assert!(a.lookup("models/x/y.bin").lockable); |
|
| … | 14 unchanged lines |
modified crates/oxy-core/src/cbor.rs +109 −25
| … | 38 unchanged lines |
||
| 39 | 39 | Value::Map(pairs.into_iter().collect()) |
|
| 40 | 40 | } |
|
| 41 | 41 | pub fn as_uint(&self) -> Option<u64> { |
|
| 42 | - | if let Value::Uint(n) = self { Some(*n) } else { None } |
|
| 42 | + | if let Value::Uint(n) = self { |
|
| 43 | + | Some(*n) |
|
| 44 | + | } else { |
|
| 45 | + | None |
|
| 46 | + | } |
|
| 43 | 47 | } |
|
| 44 | 48 | pub fn as_bytes(&self) -> Option<&[u8]> { |
|
| 45 | - | if let Value::Bytes(b) = self { Some(b) } else { None } |
|
| 49 | + | if let Value::Bytes(b) = self { |
|
| 50 | + | Some(b) |
|
| 51 | + | } else { |
|
| 52 | + | None |
|
| 53 | + | } |
|
| 46 | 54 | } |
|
| 47 | 55 | pub fn as_text(&self) -> Option<&str> { |
|
| 48 | - | if let Value::Text(t) = self { Some(t) } else { None } |
|
| 56 | + | if let Value::Text(t) = self { |
|
| 57 | + | Some(t) |
|
| 58 | + | } else { |
|
| 59 | + | None |
|
| 60 | + | } |
|
| 49 | 61 | } |
|
| 50 | 62 | pub fn as_array(&self) -> Option<&[Value]> { |
|
| 51 | - | if let Value::Array(a) = self { Some(a) } else { None } |
|
| 63 | + | if let Value::Array(a) = self { |
|
| 64 | + | Some(a) |
|
| 65 | + | } else { |
|
| 66 | + | None |
|
| 67 | + | } |
|
| 52 | 68 | } |
|
| 53 | 69 | pub fn as_map(&self) -> Option<&BTreeMap<u64, Value>> { |
|
| 54 | - | if let Value::Map(m) = self { Some(m) } else { None } |
|
| 70 | + | if let Value::Map(m) = self { |
|
| 71 | + | Some(m) |
|
| 72 | + | } else { |
|
| 73 | + | None |
|
| 74 | + | } |
|
| 55 | 75 | } |
|
| 56 | 76 | pub fn as_bool(&self) -> Option<bool> { |
|
| 57 | - | if let Value::Bool(b) = self { Some(*b) } else { None } |
|
| 77 | + | if let Value::Bool(b) = self { |
|
| 78 | + | Some(*b) |
|
| 79 | + | } else { |
|
| 80 | + | None |
|
| 81 | + | } |
|
| 58 | 82 | } |
|
| 59 | 83 | pub fn is_null(&self) -> bool { |
|
| 60 | 84 | matches!(self, Value::Null) |
|
| … | 56 unchanged lines |
||
| 117 | 141 | } |
|
| 118 | 142 | ||
| 119 | 143 | pub fn decode(bytes: &[u8]) -> Result<Value, CborError> { |
|
| 120 | - | let mut d = Decoder { b: bytes, pos: 0, depth: 0 }; |
|
| 144 | + | let mut d = Decoder { |
|
| 145 | + | b: bytes, |
|
| 146 | + | pos: 0, |
|
| 147 | + | depth: 0, |
|
| 148 | + | }; |
|
| 121 | 149 | let v = d.value()?; |
|
| 122 | 150 | if d.pos != bytes.len() { |
|
| 123 | 151 | return Err(CborError::TrailingBytes); |
|
| … | 34 unchanged lines |
||
| 158 | 186 | let major = ib >> 5; |
|
| 159 | 187 | let ai = ib & 0x1f; |
|
| 160 | 188 | if major == 7 { |
|
| 161 | - | return Err(CborError::Unsupported("simple value or float where a length or integer was expected")); |
|
| 189 | + | return Err(CborError::Unsupported( |
|
| 190 | + | "simple value or float where a length or integer was expected", |
|
| 191 | + | )); |
|
| 162 | 192 | } |
|
| 163 | 193 | let n = match ai { |
|
| 164 | 194 | 0..=23 => ai as u64, |
|
| … | 43 unchanged lines |
||
| 208 | 238 | 0xf4 => Value::Bool(false), |
|
| 209 | 239 | 0xf5 => Value::Bool(true), |
|
| 210 | 240 | 0xf6 => Value::Null, |
|
| 211 | - | _ => return Err(CborError::Unsupported("simple value, float, undefined, or break")), |
|
| 241 | + | _ => { |
|
| 242 | + | return Err(CborError::Unsupported( |
|
| 243 | + | "simple value, float, undefined, or break", |
|
| 244 | + | )) |
|
| 245 | + | } |
|
| 212 | 246 | } |
|
| 213 | 247 | } else { |
|
| 214 | 248 | let (major, n) = self.head()?; |
|
| … | 64 unchanged lines |
||
| 279 | 313 | ||
| 280 | 314 | #[test] |
|
| 281 | 315 | fn rejects_non_shortest_integers() { |
|
| 282 | - | assert_eq!(decode(&[0x18, 5]), Err(CborError::NonCanonical("argument fits in the initial byte"))); |
|
| 283 | - | assert_eq!(decode(&[0x19, 0, 5]), Err(CborError::NonCanonical("argument fits in one byte"))); |
|
| 284 | - | assert_eq!(decode(&[0x1a, 0, 0, 0, 5]), Err(CborError::NonCanonical("argument fits in two bytes"))); |
|
| 285 | - | assert_eq!(decode(&[0x1b, 0, 0, 0, 0, 0, 0, 0, 5]), Err(CborError::NonCanonical("argument fits in four bytes"))); |
|
| 316 | + | assert_eq!( |
|
| 317 | + | decode(&[0x18, 5]), |
|
| 318 | + | Err(CborError::NonCanonical("argument fits in the initial byte")) |
|
| 319 | + | ); |
|
| 320 | + | assert_eq!( |
|
| 321 | + | decode(&[0x19, 0, 5]), |
|
| 322 | + | Err(CborError::NonCanonical("argument fits in one byte")) |
|
| 323 | + | ); |
|
| 324 | + | assert_eq!( |
|
| 325 | + | decode(&[0x1a, 0, 0, 0, 5]), |
|
| 326 | + | Err(CborError::NonCanonical("argument fits in two bytes")) |
|
| 327 | + | ); |
|
| 328 | + | assert_eq!( |
|
| 329 | + | decode(&[0x1b, 0, 0, 0, 0, 0, 0, 0, 5]), |
|
| 330 | + | Err(CborError::NonCanonical("argument fits in four bytes")) |
|
| 331 | + | ); |
|
| 286 | 332 | } |
|
| 287 | 333 | ||
| 288 | 334 | #[test] |
|
| 289 | 335 | fn rejects_unsupported_items() { |
|
| 290 | 336 | assert!(matches!(decode(&[0x20]), Err(CborError::Unsupported(_)))); // -1 |
|
| 291 | - | assert!(matches!(decode(&[0xc0, 0x00]), Err(CborError::Unsupported(_)))); // tag |
|
| 292 | - | assert!(matches!(decode(&[0xf9, 0, 0]), Err(CborError::Unsupported(_)))); // float16 |
|
| 293 | - | assert!(matches!(decode(&[0x9f, 0xff]), Err(CborError::Unsupported(_)))); // indefinite array |
|
| 337 | + | assert!(matches!( |
|
| 338 | + | decode(&[0xc0, 0x00]), |
|
| 339 | + | Err(CborError::Unsupported(_)) |
|
| 340 | + | )); // tag |
|
| 341 | + | assert!(matches!( |
|
| 342 | + | decode(&[0xf9, 0, 0]), |
|
| 343 | + | Err(CborError::Unsupported(_)) |
|
| 344 | + | )); // float16 |
|
| 345 | + | assert!(matches!( |
|
| 346 | + | decode(&[0x9f, 0xff]), |
|
| 347 | + | Err(CborError::Unsupported(_)) |
|
| 348 | + | )); // indefinite array |
|
| 294 | 349 | assert!(matches!(decode(&[0xf7]), Err(CborError::Unsupported(_)))); // undefined |
|
| 295 | 350 | } |
|
| 296 | 351 | ||
| 297 | 352 | #[test] |
|
| 298 | 353 | fn maps_are_sorted_and_unique() { |
|
| 299 | - | let v = Value::map([(2, Value::Uint(1)), (0, Value::Null), (1, Value::Bool(true))]); |
|
| 354 | + | let v = Value::map([ |
|
| 355 | + | (2, Value::Uint(1)), |
|
| 356 | + | (0, Value::Null), |
|
| 357 | + | (1, Value::Bool(true)), |
|
| 358 | + | ]); |
|
| 300 | 359 | let b = rt(v); |
|
| 301 | 360 | assert_eq!(b, [0xa3, 0x00, 0xf6, 0x01, 0xf5, 0x02, 0x01]); |
|
| 302 | - | assert_eq!(decode(&[0xa2, 0x01, 0x00, 0x00, 0x00]), Err(CborError::BadMapKey)); |
|
| 303 | - | assert_eq!(decode(&[0xa2, 0x01, 0x00, 0x01, 0x00]), Err(CborError::BadMapKey)); |
|
| 304 | - | assert_eq!(decode(&[0xa1, 0x61, 0x61, 0x00]), Err(CborError::BadMapKey)); // text key |
|
| 361 | + | assert_eq!( |
|
| 362 | + | decode(&[0xa2, 0x01, 0x00, 0x00, 0x00]), |
|
| 363 | + | Err(CborError::BadMapKey) |
|
| 364 | + | ); |
|
| 365 | + | assert_eq!( |
|
| 366 | + | decode(&[0xa2, 0x01, 0x00, 0x01, 0x00]), |
|
| 367 | + | Err(CborError::BadMapKey) |
|
| 368 | + | ); |
|
| 369 | + | assert_eq!(decode(&[0xa1, 0x61, 0x61, 0x00]), Err(CborError::BadMapKey)); |
|
| 370 | + | // text key |
|
| 305 | 371 | } |
|
| 306 | 372 | ||
| 307 | 373 | #[test] |
|
| 308 | 374 | fn strings_arrays_and_trailing() { |
|
| 309 | 375 | rt(Value::Bytes(vec![1, 2, 3])); |
|
| 310 | 376 | rt(Value::Text("héllo".into())); |
|
| 311 | - | rt(Value::Array(vec![Value::Uint(1), Value::Text("x".into()), Value::Array(vec![])])); |
|
| 377 | + | rt(Value::Array(vec![ |
|
| 378 | + | Value::Uint(1), |
|
| 379 | + | Value::Text("x".into()), |
|
| 380 | + | Value::Array(vec![]), |
|
| 381 | + | ])); |
|
| 312 | 382 | assert_eq!(decode(&[0x00, 0x00]), Err(CborError::TrailingBytes)); |
|
| 313 | 383 | assert_eq!(decode(&[0x62, 0xff, 0xfe]), Err(CborError::InvalidUtf8)); |
|
| 314 | 384 | assert_eq!(decode(&[0x43, 1, 2]), Err(CborError::UnexpectedEnd)); |
|
| … | 1 unchanged lines |
||
| 316 | 386 | ||
| 317 | 387 | #[test] |
|
| 318 | 388 | fn huge_length_does_not_allocate() { |
|
| 319 | - | assert_eq!(decode(&[0x5b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), Err(CborError::LengthOverflow)); |
|
| 389 | + | assert_eq!( |
|
| 390 | + | decode(&[0x5b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), |
|
| 391 | + | Err(CborError::LengthOverflow) |
|
| 392 | + | ); |
|
| 320 | 393 | assert!(decode(&[0x9b, 0x00, 0x00, 0x00, 0x01, 0, 0, 0, 0]).is_err()); |
|
| 321 | 394 | } |
|
| 322 | 395 | ||
| … | 20 unchanged lines |
||
| 343 | 416 | } |
|
| 344 | 417 | 4 => { |
|
| 345 | 418 | let n = (xorshift(seed) % 12) as usize; |
|
| 346 | - | Value::Text((0..n).map(|_| char::from_u32((xorshift(seed) % 0x24f) as u32 + 1).unwrap_or('x')).collect()) |
|
| 419 | + | Value::Text( |
|
| 420 | + | (0..n) |
|
| 421 | + | .map(|_| char::from_u32((xorshift(seed) % 0x24f) as u32 + 1).unwrap_or('x')) |
|
| 422 | + | .collect(), |
|
| 423 | + | ) |
|
| 347 | 424 | } |
|
| 348 | 425 | 5 => { |
|
| 349 | 426 | let n = (xorshift(seed) % 5) as usize; |
|
| … | 32 unchanged lines |
||
| 382 | 459 | let n = (xorshift(&mut seed) % 64) as usize; |
|
| 383 | 460 | let bytes: Vec<u8> = (0..n).map(|_| (xorshift(&mut seed) & 0xff) as u8).collect(); |
|
| 384 | 461 | if let Ok(v) = decode(&bytes) { |
|
| 385 | - | assert_eq!(encode(&v), bytes, "accepted input must be canonical: {bytes:02x?}"); |
|
| 462 | + | assert_eq!( |
|
| 463 | + | encode(&v), |
|
| 464 | + | bytes, |
|
| 465 | + | "accepted input must be canonical: {bytes:02x?}" |
|
| 466 | + | ); |
|
| 386 | 467 | } |
|
| 387 | 468 | } |
|
| 388 | 469 | for _ in 0..3000 { |
|
| … | 25 unchanged lines |
||
| 414 | 495 | deep.push(0x00); |
|
| 415 | 496 | assert!(matches!(decode(&deep), Err(CborError::Unsupported(_)))); |
|
| 416 | 497 | // Lengths beyond the input are refused without allocating them. |
|
| 417 | - | assert!(matches!(decode(&[0x5b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), Err(CborError::LengthOverflow) | Err(CborError::UnexpectedEnd))); |
|
| 498 | + | assert!(matches!( |
|
| 499 | + | decode(&[0x5b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), |
|
| 500 | + | Err(CborError::LengthOverflow) | Err(CborError::UnexpectedEnd) |
|
| 501 | + | )); |
|
| 418 | 502 | } |
|
| 419 | 503 | } |
modified crates/oxy-core/src/chunk.rs +21 −5
| … | 20 unchanged lines |
||
| 21 | 21 | static GEAR: OnceLock<[u64; 256]> = OnceLock::new(); |
|
| 22 | 22 | GEAR.get_or_init(|| { |
|
| 23 | 23 | let mut out = [0u8; 256 * 8]; |
|
| 24 | - | blake3::Hasher::new().update(GEAR_SEED).finalize_xof().fill(&mut out); |
|
| 24 | + | blake3::Hasher::new() |
|
| 25 | + | .update(GEAR_SEED) |
|
| 26 | + | .finalize_xof() |
|
| 27 | + | .fill(&mut out); |
|
| 25 | 28 | let mut g = [0u64; 256]; |
|
| 26 | 29 | for (i, v) in g.iter_mut().enumerate() { |
|
| 27 | 30 | *v = u64::from_le_bytes(out[i * 8..i * 8 + 8].try_into().unwrap()); |
|
| … | 78 unchanged lines |
||
| 106 | 109 | assert_eq!(MASK_S & (1 << 63), 1 << 63); |
|
| 107 | 110 | // Pin the first gear entry so the table cannot drift silently. |
|
| 108 | 111 | let mut out = [0u8; 8]; |
|
| 109 | - | blake3::Hasher::new().update(GEAR_SEED).finalize_xof().fill(&mut out); |
|
| 112 | + | blake3::Hasher::new() |
|
| 113 | + | .update(GEAR_SEED) |
|
| 114 | + | .finalize_xof() |
|
| 115 | + | .fill(&mut out); |
|
| 110 | 116 | assert_eq!(gear()[0], u64::from_le_bytes(out)); |
|
| 111 | 117 | assert_ne!(gear()[1], gear()[2]); |
|
| 112 | 118 | } |
|
| … | 30 unchanged lines |
||
| 143 | 149 | let d = random(2 * 1024 * 1024, 3); |
|
| 144 | 150 | let mut e = d.clone(); |
|
| 145 | 151 | e.splice(1_000_000..1_000_000, random(100, 4)); |
|
| 146 | - | let a: std::collections::HashSet<[u8; 32]> = chunks(&d).iter().map(|c| *blake3::hash(c).as_bytes()).collect(); |
|
| 147 | - | let b: Vec<[u8; 32]> = chunks(&e).iter().map(|c| *blake3::hash(c).as_bytes()).collect(); |
|
| 152 | + | let a: std::collections::HashSet<[u8; 32]> = chunks(&d) |
|
| 153 | + | .iter() |
|
| 154 | + | .map(|c| *blake3::hash(c).as_bytes()) |
|
| 155 | + | .collect(); |
|
| 156 | + | let b: Vec<[u8; 32]> = chunks(&e) |
|
| 157 | + | .iter() |
|
| 158 | + | .map(|c| *blake3::hash(c).as_bytes()) |
|
| 159 | + | .collect(); |
|
| 148 | 160 | let shared = b.iter().filter(|h| a.contains(*h)).count(); |
|
| 149 | - | assert!(shared * 10 >= b.len() * 8, "only {shared} of {} chunks shared", b.len()); |
|
| 161 | + | assert!( |
|
| 162 | + | shared * 10 >= b.len() * 8, |
|
| 163 | + | "only {shared} of {} chunks shared", |
|
| 164 | + | b.len() |
|
| 165 | + | ); |
|
| 150 | 166 | } |
|
| 151 | 167 | ||
| 152 | 168 | #[test] |
|
| … | 10 unchanged lines |
modified crates/oxy-core/src/diff.rs +123 −19
| … | 30 unchanged lines |
||
| 31 | 31 | for &(c, i) in keys { |
|
| 32 | 32 | match out.last_mut() { |
|
| 33 | 33 | Some(r) if r.change == c && r.to == i => r.to = i + 1, |
|
| 34 | - | _ => out.push(AtomRange { change: c, from: i, to: i + 1 }), |
|
| 34 | + | _ => out.push(AtomRange { |
|
| 35 | + | change: c, |
|
| 36 | + | from: i, |
|
| 37 | + | to: i + 1, |
|
| 38 | + | }), |
|
| 35 | 39 | } |
|
| 36 | 40 | } |
|
| 37 | 41 | out |
|
| … | 6 unchanged lines |
||
| 44 | 48 | let ops = capture_diff_slices(Algorithm::Myers, &old_lines, &new_lines); |
|
| 45 | 49 | ||
| 46 | 50 | let anchor = |i: usize| -> (AtomRef, AtomRef) { |
|
| 47 | - | let after = if i == 0 { AtomRef::Start } else { AtomRef::Atom { change: old[i - 1].0 .0, index: old[i - 1].0 .1 } }; |
|
| 48 | - | let before = if i >= old.len() { AtomRef::End } else { AtomRef::Atom { change: old[i].0 .0, index: old[i].0 .1 } }; |
|
| 51 | + | let after = if i == 0 { |
|
| 52 | + | AtomRef::Start |
|
| 53 | + | } else { |
|
| 54 | + | AtomRef::Atom { |
|
| 55 | + | change: old[i - 1].0 .0, |
|
| 56 | + | index: old[i - 1].0 .1, |
|
| 57 | + | } |
|
| 58 | + | }; |
|
| 59 | + | let before = if i >= old.len() { |
|
| 60 | + | AtomRef::End |
|
| 61 | + | } else { |
|
| 62 | + | AtomRef::Atom { |
|
| 63 | + | change: old[i].0 .0, |
|
| 64 | + | index: old[i].0 .1, |
|
| 65 | + | } |
|
| 66 | + | }; |
|
| 49 | 67 | (after, before) |
|
| 50 | 68 | }; |
|
| 51 | 69 | ||
| … | 2 unchanged lines |
||
| 54 | 72 | for op in ops { |
|
| 55 | 73 | match op { |
|
| 56 | 74 | DiffOp::Equal { .. } => {} |
|
| 57 | - | DiffOp::Delete { old_index, old_len, .. } => { |
|
| 75 | + | DiffOp::Delete { |
|
| 76 | + | old_index, old_len, .. |
|
| 77 | + | } => { |
|
| 58 | 78 | deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k)); |
|
| 59 | 79 | } |
|
| 60 | - | DiffOp::Insert { old_index, new_index, new_len } => { |
|
| 80 | + | DiffOp::Insert { |
|
| 81 | + | old_index, |
|
| 82 | + | new_index, |
|
| 83 | + | new_len, |
|
| 84 | + | } => { |
|
| 61 | 85 | let (after, before) = anchor(old_index); |
|
| 62 | 86 | inserts.push(Edit::Insert { |
|
| 63 | 87 | node, |
|
| 64 | 88 | after, |
|
| 65 | 89 | before, |
|
| 66 | - | lines: new_lines[new_index..new_index + new_len].iter().map(|l| l.to_vec()).collect(), |
|
| 90 | + | lines: new_lines[new_index..new_index + new_len] |
|
| 91 | + | .iter() |
|
| 92 | + | .map(|l| l.to_vec()) |
|
| 93 | + | .collect(), |
|
| 67 | 94 | }); |
|
| 68 | 95 | } |
|
| 69 | - | DiffOp::Replace { old_index, old_len, new_index, new_len } => { |
|
| 96 | + | DiffOp::Replace { |
|
| 97 | + | old_index, |
|
| 98 | + | old_len, |
|
| 99 | + | new_index, |
|
| 100 | + | new_len, |
|
| 101 | + | } => { |
|
| 70 | 102 | deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k)); |
|
| 71 | 103 | let (after, before) = anchor(old_index); |
|
| 72 | 104 | inserts.push(Edit::Insert { |
|
| 73 | 105 | node, |
|
| 74 | 106 | after, |
|
| 75 | 107 | before, |
|
| 76 | - | lines: new_lines[new_index..new_index + new_len].iter().map(|l| l.to_vec()).collect(), |
|
| 108 | + | lines: new_lines[new_index..new_index + new_len] |
|
| 109 | + | .iter() |
|
| 110 | + | .map(|l| l.to_vec()) |
|
| 111 | + | .collect(), |
|
| 77 | 112 | }); |
|
| 78 | 113 | } |
|
| 79 | 114 | } |
|
| 80 | 115 | } |
|
| 81 | 116 | let mut edits = Vec::new(); |
|
| 82 | 117 | if !deleted.is_empty() { |
|
| 83 | - | edits.push(Edit::Delete { node, atoms: ranges(&deleted) }); |
|
| 118 | + | edits.push(Edit::Delete { |
|
| 119 | + | node, |
|
| 120 | + | atoms: ranges(&deleted), |
|
| 121 | + | }); |
|
| 84 | 122 | } |
|
| 85 | 123 | edits.extend(inserts); |
|
| 86 | 124 | edits |
|
| … | 22 unchanged lines |
||
| 109 | 147 | assert_eq!( |
|
| 110 | 148 | r, |
|
| 111 | 149 | vec![ |
|
| 112 | - | AtomRange { change: h(1), from: 0, to: 2 }, |
|
| 113 | - | AtomRange { change: h(1), from: 3, to: 4 }, |
|
| 114 | - | AtomRange { change: h(2), from: 0, to: 1 } |
|
| 150 | + | AtomRange { |
|
| 151 | + | change: h(1), |
|
| 152 | + | from: 0, |
|
| 153 | + | to: 2 |
|
| 154 | + | }, |
|
| 155 | + | AtomRange { |
|
| 156 | + | change: h(1), |
|
| 157 | + | from: 3, |
|
| 158 | + | to: 4 |
|
| 159 | + | }, |
|
| 160 | + | AtomRange { |
|
| 161 | + | change: h(2), |
|
| 162 | + | from: 0, |
|
| 163 | + | to: 1 |
|
| 164 | + | } |
|
| 115 | 165 | ] |
|
| 116 | 166 | ); |
|
| 117 | 167 | } |
|
| … | 1 unchanged lines |
||
| 119 | 169 | #[test] |
|
| 120 | 170 | fn edits_for_typical_changes() { |
|
| 121 | 171 | let node = NodeId::random(); |
|
| 122 | - | let old: Vec<(AtomKey, Vec<u8>)> = ["a\n", "b\n", "c\n"].iter().enumerate().map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec())).collect(); |
|
| 172 | + | let old: Vec<(AtomKey, Vec<u8>)> = ["a\n", "b\n", "c\n"] |
|
| 173 | + | .iter() |
|
| 174 | + | .enumerate() |
|
| 175 | + | .map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec())) |
|
| 176 | + | .collect(); |
|
| 123 | 177 | // No change. |
|
| 124 | 178 | assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty()); |
|
| 125 | 179 | // Replace middle line. |
|
| … | 1 unchanged lines |
||
| 127 | 181 | assert_eq!( |
|
| 128 | 182 | e, |
|
| 129 | 183 | vec![ |
|
| 130 | - | Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 1, to: 2 }] }, |
|
| 131 | - | Edit::Insert { node, after: AtomRef::Atom { change: h(1), index: 0 }, before: AtomRef::Atom { change: h(1), index: 1 }, lines: vec![b"B\n".to_vec()] }, |
|
| 184 | + | Edit::Delete { |
|
| 185 | + | node, |
|
| 186 | + | atoms: vec![AtomRange { |
|
| 187 | + | change: h(1), |
|
| 188 | + | from: 1, |
|
| 189 | + | to: 2 |
|
| 190 | + | }] |
|
| 191 | + | }, |
|
| 192 | + | Edit::Insert { |
|
| 193 | + | node, |
|
| 194 | + | after: AtomRef::Atom { |
|
| 195 | + | change: h(1), |
|
| 196 | + | index: 0 |
|
| 197 | + | }, |
|
| 198 | + | before: AtomRef::Atom { |
|
| 199 | + | change: h(1), |
|
| 200 | + | index: 1 |
|
| 201 | + | }, |
|
| 202 | + | lines: vec![b"B\n".to_vec()] |
|
| 203 | + | }, |
|
| 132 | 204 | ] |
|
| 133 | 205 | ); |
|
| 134 | 206 | // Prepend and append. |
|
| 135 | 207 | let e = text_edits(node, &old, b"top\na\nb\nc\nend\n"); |
|
| 136 | 208 | assert_eq!(e.len(), 2); |
|
| 137 | - | assert!(matches!(&e[0], Edit::Insert { after: AtomRef::Start, before: AtomRef::Atom { index: 0, .. }, .. })); |
|
| 138 | - | assert!(matches!(&e[1], Edit::Insert { after: AtomRef::Atom { index: 2, .. }, before: AtomRef::End, .. })); |
|
| 209 | + | assert!(matches!( |
|
| 210 | + | &e[0], |
|
| 211 | + | Edit::Insert { |
|
| 212 | + | after: AtomRef::Start, |
|
| 213 | + | before: AtomRef::Atom { index: 0, .. }, |
|
| 214 | + | .. |
|
| 215 | + | } |
|
| 216 | + | )); |
|
| 217 | + | assert!(matches!( |
|
| 218 | + | &e[1], |
|
| 219 | + | Edit::Insert { |
|
| 220 | + | after: AtomRef::Atom { index: 2, .. }, |
|
| 221 | + | before: AtomRef::End, |
|
| 222 | + | .. |
|
| 223 | + | } |
|
| 224 | + | )); |
|
| 139 | 225 | // Delete everything. |
|
| 140 | 226 | let e = text_edits(node, &old, b""); |
|
| 141 | - | assert_eq!(e, vec![Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 0, to: 3 }] }]); |
|
| 227 | + | assert_eq!( |
|
| 228 | + | e, |
|
| 229 | + | vec![Edit::Delete { |
|
| 230 | + | node, |
|
| 231 | + | atoms: vec![AtomRange { |
|
| 232 | + | change: h(1), |
|
| 233 | + | from: 0, |
|
| 234 | + | to: 3 |
|
| 235 | + | }] |
|
| 236 | + | }] |
|
| 237 | + | ); |
|
| 142 | 238 | // From empty. |
|
| 143 | 239 | let e = text_edits(node, &[], b"x\n"); |
|
| 144 | - | assert_eq!(e, vec![Edit::Insert { node, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }]); |
|
| 240 | + | assert_eq!( |
|
| 241 | + | e, |
|
| 242 | + | vec![Edit::Insert { |
|
| 243 | + | node, |
|
| 244 | + | after: AtomRef::Start, |
|
| 245 | + | before: AtomRef::End, |
|
| 246 | + | lines: vec![b"x\n".to_vec()] |
|
| 247 | + | }] |
|
| 248 | + | ); |
|
| 145 | 249 | } |
|
| 146 | 250 | } |
modified crates/oxy-core/src/graph.rs +396 −65
| … | 58 unchanged lines |
||
| 59 | 59 | Conflict(Vec<Vec<AtomKey>>), |
|
| 60 | 60 | /// Delete/insert conflict: `inserted` was anchored to `deleted` by a change |
|
| 61 | 61 | /// that commutes with the deletion. |
|
| 62 | - | Zombie { deleted: Vec<AtomKey>, inserted: Vec<AtomKey> }, |
|
| 62 | + | Zombie { |
|
| 63 | + | deleted: Vec<AtomKey>, |
|
| 64 | + | inserted: Vec<AtomKey>, |
|
| 65 | + | }, |
|
| 63 | 66 | } |
|
| 64 | 67 | ||
| 65 | 68 | #[derive(Clone, Debug, PartialEq, Eq, Default)] |
|
| … | 3 unchanged lines |
||
| 69 | 72 | ||
| 70 | 73 | impl Materialized { |
|
| 71 | 74 | pub fn is_conflicted(&self) -> bool { |
|
| 72 | - | self.segments.iter().any(|s| !matches!(s, Segment::Lines(_))) |
|
| 75 | + | self.segments |
|
| 76 | + | .iter() |
|
| 77 | + | .any(|s| !matches!(s, Segment::Lines(_))) |
|
| 73 | 78 | } |
|
| 74 | 79 | pub fn conflict_count(&self) -> usize { |
|
| 75 | - | self.segments.iter().filter(|s| !matches!(s, Segment::Lines(_))).count() |
|
| 80 | + | self.segments |
|
| 81 | + | .iter() |
|
| 82 | + | .filter(|s| !matches!(s, Segment::Lines(_))) |
|
| 83 | + | .count() |
|
| 76 | 84 | } |
|
| 77 | 85 | /// Every live atom in materialized order, conflict sides concatenated. |
|
| 78 | 86 | pub fn live_atoms(&self) -> Vec<AtomKey> { |
|
| … | 11 unchanged lines |
||
| 90 | 98 | ||
| 91 | 99 | impl FileGraph { |
|
| 92 | 100 | pub fn new() -> Self { |
|
| 93 | - | let mut g = FileGraph { atoms: HashMap::new(), succ: HashMap::new(), pred: HashMap::new() }; |
|
| 101 | + | let mut g = FileGraph { |
|
| 102 | + | atoms: HashMap::new(), |
|
| 103 | + | succ: HashMap::new(), |
|
| 104 | + | pred: HashMap::new(), |
|
| 105 | + | }; |
|
| 94 | 106 | g.edge(Vertex::Start, Vertex::End); |
|
| 95 | 107 | g |
|
| 96 | 108 | } |
|
| … | 6 unchanged lines |
||
| 103 | 115 | } |
|
| 104 | 116 | ||
| 105 | 117 | /// Apply an `Insert` by `change` whose first new atom has index `first_index`. |
|
| 106 | - | pub fn insert(&mut self, change: ChangeHash, first_index: u32, after: Vertex, before: Vertex, lines: &[Vec<u8>]) { |
|
| 118 | + | pub fn insert( |
|
| 119 | + | &mut self, |
|
| 120 | + | change: ChangeHash, |
|
| 121 | + | first_index: u32, |
|
| 122 | + | after: Vertex, |
|
| 123 | + | before: Vertex, |
|
| 124 | + | lines: &[Vec<u8>], |
|
| 125 | + | ) { |
|
| 107 | 126 | let mut prev = after; |
|
| 108 | 127 | for (i, line) in lines.iter().enumerate() { |
|
| 109 | 128 | let key = (change, first_index + i as u32); |
|
| … | 8 unchanged lines |
||
| 118 | 137 | /// Apply a `Delete` of `range` by change `by`. |
|
| 119 | 138 | pub fn delete(&mut self, by: ChangeHash, range: &AtomRange) { |
|
| 120 | 139 | for i in range.from..range.to { |
|
| 121 | - | self.atoms.entry((range.change, i)).or_default().deleted_by.insert(by); |
|
| 140 | + | self.atoms |
|
| 141 | + | .entry((range.change, i)) |
|
| 142 | + | .or_default() |
|
| 143 | + | .deleted_by |
|
| 144 | + | .insert(by); |
|
| 122 | 145 | } |
|
| 123 | 146 | } |
|
| 124 | 147 | ||
| 125 | 148 | pub fn is_live(&self, k: &AtomKey) -> bool { |
|
| 126 | - | self.atoms.get(k).is_some_and(|a| a.content.is_some() && a.deleted_by.is_empty()) |
|
| 149 | + | self.atoms |
|
| 150 | + | .get(k) |
|
| 151 | + | .is_some_and(|a| a.content.is_some() && a.deleted_by.is_empty()) |
|
| 127 | 152 | } |
|
| 128 | 153 | ||
| 129 | 154 | pub fn content(&self, k: &AtomKey) -> Option<&[u8]> { |
|
| … | 1 unchanged lines |
||
| 131 | 156 | } |
|
| 132 | 157 | ||
| 133 | 158 | pub fn deleters(&self, k: &AtomKey) -> impl Iterator<Item = &ChangeHash> { |
|
| 134 | - | self.atoms.get(k).into_iter().flat_map(|a| a.deleted_by.iter()) |
|
| 159 | + | self.atoms |
|
| 160 | + | .get(k) |
|
| 161 | + | .into_iter() |
|
| 162 | + | .flat_map(|a| a.deleted_by.iter()) |
|
| 135 | 163 | } |
|
| 136 | 164 | ||
| 137 | 165 | /// Number of atoms introduced by `change` in this graph (for index bookkeeping). |
|
| … | 8 unchanged lines |
||
| 146 | 174 | // deletion, or the deleter saw the insertion, there is no conflict. |
|
| 147 | 175 | self.succ.get(&Vertex::Atom(*k)).is_some_and(|ss| { |
|
| 148 | 176 | ss.iter().any(|s| match s { |
|
| 149 | - | Vertex::Atom(y) if self.is_live(y) => !info.deleted_by.iter().any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)), |
|
| 177 | + | Vertex::Atom(y) if self.is_live(y) => !info |
|
| 178 | + | .deleted_by |
|
| 179 | + | .iter() |
|
| 180 | + | .any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)), |
|
| 150 | 181 | _ => false, |
|
| 151 | 182 | }) |
|
| 152 | 183 | }) |
|
| 153 | 184 | } |
|
| 154 | 185 | ||
| 155 | 186 | /// Successors of `v` in the graph with non-kept vertices contracted away. |
|
| 156 | - | fn reduced_succ(&self, v: Vertex, kept: &HashSet<Vertex>, memo: &mut HashMap<Vertex, BTreeSet<Vertex>>) -> BTreeSet<Vertex> { |
|
| 187 | + | fn reduced_succ( |
|
| 188 | + | &self, |
|
| 189 | + | v: Vertex, |
|
| 190 | + | kept: &HashSet<Vertex>, |
|
| 191 | + | memo: &mut HashMap<Vertex, BTreeSet<Vertex>>, |
|
| 192 | + | ) -> BTreeSet<Vertex> { |
|
| 157 | 193 | let mut out = BTreeSet::new(); |
|
| 158 | - | let mut stack: Vec<Vertex> = self.succ.get(&v).map(|s| s.iter().copied().collect()).unwrap_or_default(); |
|
| 194 | + | let mut stack: Vec<Vertex> = self |
|
| 195 | + | .succ |
|
| 196 | + | .get(&v) |
|
| 197 | + | .map(|s| s.iter().copied().collect()) |
|
| 198 | + | .unwrap_or_default(); |
|
| 159 | 199 | let mut seen = HashSet::new(); |
|
| 160 | 200 | while let Some(s) = stack.pop() { |
|
| 161 | 201 | if kept.contains(&s) { |
|
| … | 42 unchanged lines |
||
| 204 | 244 | } |
|
| 205 | 245 | ||
| 206 | 246 | // 3. Kahn's algorithm with side labelling for conflict regions. |
|
| 207 | - | let mut ready: BTreeSet<Vertex> = indeg.iter().filter(|(_, &d)| d == 0).map(|(v, _)| *v).collect(); |
|
| 247 | + | let mut ready: BTreeSet<Vertex> = indeg |
|
| 248 | + | .iter() |
|
| 249 | + | .filter(|(_, &d)| d == 0) |
|
| 250 | + | .map(|(v, _)| *v) |
|
| 251 | + | .collect(); |
|
| 208 | 252 | let mut side: HashMap<Vertex, u32> = HashMap::new(); |
|
| 209 | 253 | let mut joins: HashSet<Vertex> = HashSet::new(); |
|
| 210 | 254 | let mut next_side = 0u32; |
|
| … | 2 unchanged lines |
||
| 213 | 257 | let mut segments: Vec<Segment> = Vec::new(); |
|
| 214 | 258 | ||
| 215 | 259 | let flush_plain = |plain: &mut Vec<Vertex>, segments: &mut Vec<Segment>| { |
|
| 216 | - | let atoms: Vec<AtomKey> = plain.iter().filter_map(|v| if let Vertex::Atom(k) = v { Some(*k) } else { None }).collect(); |
|
| 260 | + | let atoms: Vec<AtomKey> = plain |
|
| 261 | + | .iter() |
|
| 262 | + | .filter_map(|v| { |
|
| 263 | + | if let Vertex::Atom(k) = v { |
|
| 264 | + | Some(*k) |
|
| 265 | + | } else { |
|
| 266 | + | None |
|
| 267 | + | } |
|
| 268 | + | }) |
|
| 269 | + | .collect(); |
|
| 217 | 270 | plain.clear(); |
|
| 218 | 271 | if !atoms.is_empty() { |
|
| 219 | 272 | segments.extend(split_zombies(atoms, &ghosts)); |
|
| … | 108 unchanged lines |
||
| 328 | 381 | push_atoms(&mut out, s); |
|
| 329 | 382 | } |
|
| 330 | 383 | newline_if_needed(&mut out); |
|
| 331 | - | out.extend_from_slice(format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes()); |
|
| 384 | + | out.extend_from_slice( |
|
| 385 | + | format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(), |
|
| 386 | + | ); |
|
| 332 | 387 | } |
|
| 333 | 388 | Segment::Zombie { deleted, inserted } => { |
|
| 334 | 389 | n += 1; |
|
| … | 9 unchanged lines |
||
| 344 | 399 | ); |
|
| 345 | 400 | push_atoms(&mut out, deleted); |
|
| 346 | 401 | newline_if_needed(&mut out); |
|
| 347 | - | out.extend_from_slice(format!("======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n").as_bytes()); |
|
| 402 | + | out.extend_from_slice( |
|
| 403 | + | format!( |
|
| 404 | + | "======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n" |
|
| 405 | + | ) |
|
| 406 | + | .as_bytes(), |
|
| 407 | + | ); |
|
| 348 | 408 | push_atoms(&mut out, inserted); |
|
| 349 | 409 | newline_if_needed(&mut out); |
|
| 350 | - | out.extend_from_slice(format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes()); |
|
| 410 | + | out.extend_from_slice( |
|
| 411 | + | format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(), |
|
| 412 | + | ); |
|
| 351 | 413 | } |
|
| 352 | 414 | } |
|
| 353 | 415 | } |
|
| … | 6 unchanged lines |
||
| 360 | 422 | match v { |
|
| 361 | 423 | Vertex::Start => Value::Uint(0), |
|
| 362 | 424 | Vertex::End => Value::Uint(1), |
|
| 363 | - | Vertex::Atom((c, i)) => Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)]), |
|
| 425 | + | Vertex::Atom((c, i)) => { |
|
| 426 | + | Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)]) |
|
| 427 | + | } |
|
| 364 | 428 | } |
|
| 365 | 429 | } |
|
| 366 | 430 | ||
| … | 3 unchanged lines |
||
| 370 | 434 | match v { |
|
| 371 | 435 | Value::Uint(0) => Ok(Vertex::Start), |
|
| 372 | 436 | Value::Uint(1) => Ok(Vertex::End), |
|
| 373 | - | Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom((crate::objects::oref(&a[0], "vertex")?, crate::objects::u32_(&a[1], "vertex")?))), |
|
| 374 | - | _ => Err(ObjectError::Schema("vertex: expected 0, 1, or [hash, index]".into())), |
|
| 437 | + | Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom(( |
|
| 438 | + | crate::objects::oref(&a[0], "vertex")?, |
|
| 439 | + | crate::objects::u32_(&a[1], "vertex")?, |
|
| 440 | + | ))), |
|
| 441 | + | _ => Err(ObjectError::Schema( |
|
| 442 | + | "vertex: expected 0, 1, or [hash, index]".into(), |
|
| 443 | + | )), |
|
| 375 | 444 | } |
|
| 376 | 445 | } |
|
| 377 | 446 | ||
| … | 3 unchanged lines |
||
| 381 | 450 | use crate::cbor::Value; |
|
| 382 | 451 | let mut atoms: Vec<(&AtomKey, &AtomInfo)> = self.atoms.iter().collect(); |
|
| 383 | 452 | atoms.sort_by_key(|(k, _)| **k); |
|
| 384 | - | let mut edges: Vec<(Vertex, Vertex)> = self.succ.iter().flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b))).collect(); |
|
| 453 | + | let mut edges: Vec<(Vertex, Vertex)> = self |
|
| 454 | + | .succ |
|
| 455 | + | .iter() |
|
| 456 | + | .flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b))) |
|
| 457 | + | .collect(); |
|
| 385 | 458 | edges.sort(); |
|
| 386 | 459 | Value::map([ |
|
| 387 | 460 | ( |
|
| … | 5 unchanged lines |
||
| 393 | 466 | Value::Array(vec![ |
|
| 394 | 467 | Value::Bytes(c.0.to_vec()), |
|
| 395 | 468 | Value::Uint(*i as u64), |
|
| 396 | - | info.content.as_ref().map(|b| Value::Bytes(b.clone())).unwrap_or(Value::Null), |
|
| 397 | - | Value::Array(info.deleted_by.iter().map(|d| Value::Bytes(d.0.to_vec())).collect()), |
|
| 469 | + | info.content |
|
| 470 | + | .as_ref() |
|
| 471 | + | .map(|b| Value::Bytes(b.clone())) |
|
| 472 | + | .unwrap_or(Value::Null), |
|
| 473 | + | Value::Array( |
|
| 474 | + | info.deleted_by |
|
| 475 | + | .iter() |
|
| 476 | + | .map(|d| Value::Bytes(d.0.to_vec())) |
|
| 477 | + | .collect(), |
|
| 478 | + | ), |
|
| 398 | 479 | ]) |
|
| 399 | 480 | }) |
|
| 400 | 481 | .collect(), |
|
| 401 | 482 | ), |
|
| 402 | 483 | ), |
|
| 403 | - | (1, Value::Array(edges.iter().map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)])).collect())), |
|
| 484 | + | ( |
|
| 485 | + | 1, |
|
| 486 | + | Value::Array( |
|
| 487 | + | edges |
|
| 488 | + | .iter() |
|
| 489 | + | .map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)])) |
|
| 490 | + | .collect(), |
|
| 491 | + | ), |
|
| 492 | + | ), |
|
| 404 | 493 | ]) |
|
| 405 | 494 | } |
|
| 406 | 495 | ||
| 407 | 496 | pub fn from_value(v: &crate::cbor::Value) -> Result<FileGraph, crate::objects::ObjectError> { |
|
| 408 | 497 | use crate::objects::{array, bytes, field, map, oref, u32_, ObjectError}; |
|
| 409 | 498 | let m = map(v, "file graph")?; |
|
| 410 | - | let mut g = FileGraph { atoms: HashMap::new(), succ: HashMap::new(), pred: HashMap::new() }; |
|
| 499 | + | let mut g = FileGraph { |
|
| 500 | + | atoms: HashMap::new(), |
|
| 501 | + | succ: HashMap::new(), |
|
| 502 | + | pred: HashMap::new(), |
|
| 503 | + | }; |
|
| 411 | 504 | for a in array(field(m, 0, "atoms")?, "atoms")? { |
|
| 412 | 505 | let e = array(a, "atom")?; |
|
| 413 | 506 | if e.len() != 4 { |
|
| 414 | 507 | return Err(ObjectError::Schema("atom: expected 4 fields".into())); |
|
| 415 | 508 | } |
|
| 416 | 509 | let key = (oref(&e[0], "atom change")?, u32_(&e[1], "atom index")?); |
|
| 417 | - | let content = if e[2].is_null() { None } else { Some(bytes(&e[2], "atom content")?.to_vec()) }; |
|
| 418 | - | let deleted_by = array(&e[3], "deleters")?.iter().map(|d| oref(d, "deleter")).collect::<Result<BTreeSet<_>, _>>()?; |
|
| 419 | - | g.atoms.insert(key, AtomInfo { content, deleted_by }); |
|
| 510 | + | let content = if e[2].is_null() { |
|
| 511 | + | None |
|
| 512 | + | } else { |
|
| 513 | + | Some(bytes(&e[2], "atom content")?.to_vec()) |
|
| 514 | + | }; |
|
| 515 | + | let deleted_by = array(&e[3], "deleters")? |
|
| 516 | + | .iter() |
|
| 517 | + | .map(|d| oref(d, "deleter")) |
|
| 518 | + | .collect::<Result<BTreeSet<_>, _>>()?; |
|
| 519 | + | g.atoms.insert( |
|
| 520 | + | key, |
|
| 521 | + | AtomInfo { |
|
| 522 | + | content, |
|
| 523 | + | deleted_by, |
|
| 524 | + | }, |
|
| 525 | + | ); |
|
| 420 | 526 | } |
|
| 421 | 527 | for e in array(field(m, 1, "edges")?, "edges")? { |
|
| 422 | 528 | let pair = array(e, "edge")?; |
|
| … | 83 unchanged lines |
||
| 506 | 612 | #[test] |
|
| 507 | 613 | fn sequential_inserts_and_deletes() { |
|
| 508 | 614 | let mut g = FileGraph::new(); |
|
| 509 | - | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n", "c\n"])); |
|
| 615 | + | g.insert( |
|
| 616 | + | h(1), |
|
| 617 | + | 0, |
|
| 618 | + | Vertex::Start, |
|
| 619 | + | Vertex::End, |
|
| 620 | + | &lines(&["a\n", "b\n", "c\n"]), |
|
| 621 | + | ); |
|
| 510 | 622 | let m = g.materialize(&NoDeps); |
|
| 511 | 623 | assert!(!m.is_conflicted()); |
|
| 512 | 624 | assert_eq!(text(&g, &m), "a\nb\nc\n"); |
|
| 513 | 625 | ||
| 514 | 626 | // Change 2 depends on 1, inserts between b and c and deletes a. |
|
| 515 | - | g.insert(h(2), 0, Vertex::Atom((h(1), 1)), Vertex::Atom((h(1), 2)), &lines(&["x\n"])); |
|
| 516 | - | g.delete(h(2), &AtomRange { change: h(1), from: 0, to: 1 }); |
|
| 627 | + | g.insert( |
|
| 628 | + | h(2), |
|
| 629 | + | 0, |
|
| 630 | + | Vertex::Atom((h(1), 1)), |
|
| 631 | + | Vertex::Atom((h(1), 2)), |
|
| 632 | + | &lines(&["x\n"]), |
|
| 633 | + | ); |
|
| 634 | + | g.delete( |
|
| 635 | + | h(2), |
|
| 636 | + | &AtomRange { |
|
| 637 | + | change: h(1), |
|
| 638 | + | from: 0, |
|
| 639 | + | to: 1, |
|
| 640 | + | }, |
|
| 641 | + | ); |
|
| 517 | 642 | let deps = Deps([(h(2), vec![h(1)])].into_iter().collect()); |
|
| 518 | 643 | let m = g.materialize(&deps); |
|
| 519 | 644 | assert!(!m.is_conflicted()); |
|
| … | 5 unchanged lines |
||
| 525 | 650 | fn concurrent_inserts_at_same_anchor_conflict() { |
|
| 526 | 651 | let mut g = FileGraph::new(); |
|
| 527 | 652 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"])); |
|
| 528 | - | g.insert(h(2), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["from-2\n"])); |
|
| 529 | - | g.insert(h(3), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["from-3\n", "more-3\n"])); |
|
| 530 | - | let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect()); |
|
| 653 | + | g.insert( |
|
| 654 | + | h(2), |
|
| 655 | + | 0, |
|
| 656 | + | Vertex::Atom((h(1), 0)), |
|
| 657 | + | Vertex::Atom((h(1), 1)), |
|
| 658 | + | &lines(&["from-2\n"]), |
|
| 659 | + | ); |
|
| 660 | + | g.insert( |
|
| 661 | + | h(3), |
|
| 662 | + | 0, |
|
| 663 | + | Vertex::Atom((h(1), 0)), |
|
| 664 | + | Vertex::Atom((h(1), 1)), |
|
| 665 | + | &lines(&["from-3\n", "more-3\n"]), |
|
| 666 | + | ); |
|
| 667 | + | let deps = Deps( |
|
| 668 | + | [(h(2), vec![h(1)]), (h(3), vec![h(1)])] |
|
| 669 | + | .into_iter() |
|
| 670 | + | .collect(), |
|
| 671 | + | ); |
|
| 531 | 672 | let m = g.materialize(&deps); |
|
| 532 | 673 | assert!(m.is_conflicted()); |
|
| 533 | 674 | assert_eq!(m.conflict_count(), 1); |
|
| … | 12 unchanged lines |
||
| 546 | 687 | #[test] |
|
| 547 | 688 | fn concurrent_inserts_at_different_anchors_do_not_conflict() { |
|
| 548 | 689 | let mut g = FileGraph::new(); |
|
| 549 | - | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n", "c\n"])); |
|
| 550 | - | g.insert(h(2), 0, Vertex::Start, Vertex::Atom((h(1), 0)), &lines(&["top\n"])); |
|
| 551 | - | g.insert(h(3), 0, Vertex::Atom((h(1), 2)), Vertex::End, &lines(&["bottom\n"])); |
|
| 552 | - | let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect()); |
|
| 690 | + | g.insert( |
|
| 691 | + | h(1), |
|
| 692 | + | 0, |
|
| 693 | + | Vertex::Start, |
|
| 694 | + | Vertex::End, |
|
| 695 | + | &lines(&["a\n", "b\n", "c\n"]), |
|
| 696 | + | ); |
|
| 697 | + | g.insert( |
|
| 698 | + | h(2), |
|
| 699 | + | 0, |
|
| 700 | + | Vertex::Start, |
|
| 701 | + | Vertex::Atom((h(1), 0)), |
|
| 702 | + | &lines(&["top\n"]), |
|
| 703 | + | ); |
|
| 704 | + | g.insert( |
|
| 705 | + | h(3), |
|
| 706 | + | 0, |
|
| 707 | + | Vertex::Atom((h(1), 2)), |
|
| 708 | + | Vertex::End, |
|
| 709 | + | &lines(&["bottom\n"]), |
|
| 710 | + | ); |
|
| 711 | + | let deps = Deps( |
|
| 712 | + | [(h(2), vec![h(1)]), (h(3), vec![h(1)])] |
|
| 713 | + | .into_iter() |
|
| 714 | + | .collect(), |
|
| 715 | + | ); |
|
| 553 | 716 | let m = g.materialize(&deps); |
|
| 554 | 717 | assert!(!m.is_conflicted()); |
|
| 555 | 718 | assert_eq!(text(&g, &m), "top\na\nb\nc\nbottom\n"); |
|
| … | 3 unchanged lines |
||
| 559 | 722 | fn concurrent_deletes_of_the_same_line_agree() { |
|
| 560 | 723 | let mut g = FileGraph::new(); |
|
| 561 | 724 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"])); |
|
| 562 | - | g.delete(h(2), &AtomRange { change: h(1), from: 0, to: 1 }); |
|
| 563 | - | g.delete(h(3), &AtomRange { change: h(1), from: 0, to: 1 }); |
|
| 564 | - | let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect()); |
|
| 725 | + | g.delete( |
|
| 726 | + | h(2), |
|
| 727 | + | &AtomRange { |
|
| 728 | + | change: h(1), |
|
| 729 | + | from: 0, |
|
| 730 | + | to: 1, |
|
| 731 | + | }, |
|
| 732 | + | ); |
|
| 733 | + | g.delete( |
|
| 734 | + | h(3), |
|
| 735 | + | &AtomRange { |
|
| 736 | + | change: h(1), |
|
| 737 | + | from: 0, |
|
| 738 | + | to: 1, |
|
| 739 | + | }, |
|
| 740 | + | ); |
|
| 741 | + | let deps = Deps( |
|
| 742 | + | [(h(2), vec![h(1)]), (h(3), vec![h(1)])] |
|
| 743 | + | .into_iter() |
|
| 744 | + | .collect(), |
|
| 745 | + | ); |
|
| 565 | 746 | let m = g.materialize(&deps); |
|
| 566 | 747 | assert!(!m.is_conflicted()); |
|
| 567 | 748 | assert_eq!(text(&g, &m), "b\n"); |
|
| … | 4 unchanged lines |
||
| 572 | 753 | let mut g = FileGraph::new(); |
|
| 573 | 754 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"])); |
|
| 574 | 755 | // 2 deletes a; 3 (concurrent) inserts after a. |
|
| 575 | - | g.delete(h(2), &AtomRange { change: h(1), from: 0, to: 1 }); |
|
| 576 | - | g.insert(h(3), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["after-a\n"])); |
|
| 577 | - | let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect()); |
|
| 756 | + | g.delete( |
|
| 757 | + | h(2), |
|
| 758 | + | &AtomRange { |
|
| 759 | + | change: h(1), |
|
| 760 | + | from: 0, |
|
| 761 | + | to: 1, |
|
| 762 | + | }, |
|
| 763 | + | ); |
|
| 764 | + | g.insert( |
|
| 765 | + | h(3), |
|
| 766 | + | 0, |
|
| 767 | + | Vertex::Atom((h(1), 0)), |
|
| 768 | + | Vertex::Atom((h(1), 1)), |
|
| 769 | + | &lines(&["after-a\n"]), |
|
| 770 | + | ); |
|
| 771 | + | let deps = Deps( |
|
| 772 | + | [(h(2), vec![h(1)]), (h(3), vec![h(1)])] |
|
| 773 | + | .into_iter() |
|
| 774 | + | .collect(), |
|
| 775 | + | ); |
|
| 578 | 776 | let m = g.materialize(&deps); |
|
| 579 | 777 | assert!(m.is_conflicted()); |
|
| 580 | 778 | assert_eq!( |
|
| 581 | 779 | m.segments, |
|
| 582 | 780 | vec![ |
|
| 583 | - | Segment::Zombie { deleted: vec![(h(1), 0)], inserted: vec![(h(3), 0)] }, |
|
| 781 | + | Segment::Zombie { |
|
| 782 | + | deleted: vec![(h(1), 0)], |
|
| 783 | + | inserted: vec![(h(3), 0)] |
|
| 784 | + | }, |
|
| 584 | 785 | Segment::Lines(vec![(h(1), 1)]), |
|
| 585 | 786 | ] |
|
| 586 | 787 | ); |
|
| … | 2 unchanged lines |
||
| 589 | 790 | assert!(t.contains("after-a\n>>>>>>>"), "{t}"); |
|
| 590 | 791 | ||
| 591 | 792 | // If the deleter saw the insert (depends on 3), there is no conflict. |
|
| 592 | - | let deps = Deps([(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])].into_iter().collect()); |
|
| 793 | + | let deps = Deps( |
|
| 794 | + | [(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])] |
|
| 795 | + | .into_iter() |
|
| 796 | + | .collect(), |
|
| 797 | + | ); |
|
| 593 | 798 | let m = g.materialize(&deps); |
|
| 594 | 799 | assert!(!m.is_conflicted()); |
|
| 595 | 800 | assert_eq!(text(&g, &m), "after-a\nb\n"); |
|
| … | 5 unchanged lines |
||
| 601 | 806 | // after the dead b (a rewritten change may do this): no conflict. |
|
| 602 | 807 | let mut g = FileGraph::new(); |
|
| 603 | 808 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"])); |
|
| 604 | - | g.delete(h(2), &AtomRange { change: h(1), from: 1, to: 2 }); |
|
| 605 | - | g.insert(h(3), 0, Vertex::Atom((h(1), 1)), Vertex::End, &lines(&["c\n"])); |
|
| 606 | - | let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(2)])].into_iter().collect()); |
|
| 809 | + | g.delete( |
|
| 810 | + | h(2), |
|
| 811 | + | &AtomRange { |
|
| 812 | + | change: h(1), |
|
| 813 | + | from: 1, |
|
| 814 | + | to: 2, |
|
| 815 | + | }, |
|
| 816 | + | ); |
|
| 817 | + | g.insert( |
|
| 818 | + | h(3), |
|
| 819 | + | 0, |
|
| 820 | + | Vertex::Atom((h(1), 1)), |
|
| 821 | + | Vertex::End, |
|
| 822 | + | &lines(&["c\n"]), |
|
| 823 | + | ); |
|
| 824 | + | let deps = Deps( |
|
| 825 | + | [(h(2), vec![h(1)]), (h(3), vec![h(2)])] |
|
| 826 | + | .into_iter() |
|
| 827 | + | .collect(), |
|
| 828 | + | ); |
|
| 607 | 829 | let m = g.materialize(&deps); |
|
| 608 | 830 | assert!(!m.is_conflicted(), "{:?}", m.segments); |
|
| 609 | 831 | assert_eq!(text(&g, &m), "a\nc\n"); |
|
| … | 1 unchanged lines |
||
| 611 | 833 | // anchoring after it, is likewise fine. |
|
| 612 | 834 | let mut g = FileGraph::new(); |
|
| 613 | 835 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["x\n"])); |
|
| 614 | - | g.delete(h(1), &AtomRange { change: h(1), from: 0, to: 1 }); |
|
| 615 | - | g.insert(h(2), 0, Vertex::Atom((h(1), 0)), Vertex::End, &lines(&["y\n"])); |
|
| 836 | + | g.delete( |
|
| 837 | + | h(1), |
|
| 838 | + | &AtomRange { |
|
| 839 | + | change: h(1), |
|
| 840 | + | from: 0, |
|
| 841 | + | to: 1, |
|
| 842 | + | }, |
|
| 843 | + | ); |
|
| 844 | + | g.insert( |
|
| 845 | + | h(2), |
|
| 846 | + | 0, |
|
| 847 | + | Vertex::Atom((h(1), 0)), |
|
| 848 | + | Vertex::End, |
|
| 849 | + | &lines(&["y\n"]), |
|
| 850 | + | ); |
|
| 616 | 851 | let deps = Deps([(h(2), vec![h(1)])].into_iter().collect()); |
|
| 617 | 852 | let m = g.materialize(&deps); |
|
| 618 | 853 | assert!(!m.is_conflicted()); |
|
| … | 4 unchanged lines |
||
| 623 | 858 | fn resolution_collapses_a_conflict() { |
|
| 624 | 859 | let mut g = FileGraph::new(); |
|
| 625 | 860 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"])); |
|
| 626 | - | g.insert(h(2), 0, Vertex::Start, Vertex::Atom((h(1), 0)), &lines(&["x\n"])); |
|
| 627 | - | g.insert(h(3), 0, Vertex::Start, Vertex::Atom((h(1), 0)), &lines(&["y\n"])); |
|
| 628 | - | let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect(); |
|
| 861 | + | g.insert( |
|
| 862 | + | h(2), |
|
| 863 | + | 0, |
|
| 864 | + | Vertex::Start, |
|
| 865 | + | Vertex::Atom((h(1), 0)), |
|
| 866 | + | &lines(&["x\n"]), |
|
| 867 | + | ); |
|
| 868 | + | g.insert( |
|
| 869 | + | h(3), |
|
| 870 | + | 0, |
|
| 871 | + | Vertex::Start, |
|
| 872 | + | Vertex::Atom((h(1), 0)), |
|
| 873 | + | &lines(&["y\n"]), |
|
| 874 | + | ); |
|
| 875 | + | let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])] |
|
| 876 | + | .into_iter() |
|
| 877 | + | .collect(); |
|
| 629 | 878 | assert!(g.materialize(&Deps(deps.clone())).is_conflicted()); |
|
| 630 | 879 | // Resolution keeps y, drops x. |
|
| 631 | - | g.delete(h(4), &AtomRange { change: h(2), from: 0, to: 1 }); |
|
| 880 | + | g.delete( |
|
| 881 | + | h(4), |
|
| 882 | + | &AtomRange { |
|
| 883 | + | change: h(2), |
|
| 884 | + | from: 0, |
|
| 885 | + | to: 1, |
|
| 886 | + | }, |
|
| 887 | + | ); |
|
| 632 | 888 | deps.insert(h(4), vec![h(2), h(3)]); |
|
| 633 | 889 | let m = g.materialize(&Deps(deps)); |
|
| 634 | 890 | assert!(!m.is_conflicted()); |
|
| … | 15 unchanged lines |
||
| 650 | 906 | fn serialization_round_trip() { |
|
| 651 | 907 | let mut g = FileGraph::new(); |
|
| 652 | 908 | g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"])); |
|
| 653 | - | g.insert(h(2), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["x\n"])); |
|
| 654 | - | g.delete(h(3), &AtomRange { change: h(1), from: 1, to: 2 }); |
|
| 909 | + | g.insert( |
|
| 910 | + | h(2), |
|
| 911 | + | 0, |
|
| 912 | + | Vertex::Atom((h(1), 0)), |
|
| 913 | + | Vertex::Atom((h(1), 1)), |
|
| 914 | + | &lines(&["x\n"]), |
|
| 915 | + | ); |
|
| 916 | + | g.delete( |
|
| 917 | + | h(3), |
|
| 918 | + | &AtomRange { |
|
| 919 | + | change: h(1), |
|
| 920 | + | from: 1, |
|
| 921 | + | to: 2, |
|
| 922 | + | }, |
|
| 923 | + | ); |
|
| 655 | 924 | let v = g.to_value(); |
|
| 656 | 925 | let bytes = crate::cbor::encode(&v); |
|
| 657 | 926 | let back = FileGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap(); |
|
| 658 | - | let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect()); |
|
| 927 | + | let deps = Deps( |
|
| 928 | + | [(h(2), vec![h(1)]), (h(3), vec![h(1)])] |
|
| 929 | + | .into_iter() |
|
| 930 | + | .collect(), |
|
| 931 | + | ); |
|
| 659 | 932 | assert_eq!(back.materialize(&deps), g.materialize(&deps)); |
|
| 660 | 933 | assert_eq!(text(&back, &back.materialize(&deps)), "a\nx\n"); |
|
| 661 | - | assert_eq!(crate::cbor::encode(&back.to_value()), bytes, "deterministic"); |
|
| 934 | + | assert_eq!( |
|
| 935 | + | crate::cbor::encode(&back.to_value()), |
|
| 936 | + | bytes, |
|
| 937 | + | "deterministic" |
|
| 938 | + | ); |
|
| 662 | 939 | } |
|
| 663 | 940 | ||
| 664 | 941 | /// Spec §7.3: the graph is a function of the set of changes, not their order. |
|
| 665 | 942 | #[test] |
|
| 666 | 943 | fn application_order_is_irrelevant() { |
|
| 667 | 944 | use rand::seq::SliceRandom; |
|
| 668 | - | type Op = (ChangeHash, Vec<(u32, Vertex, Vertex, Vec<Vec<u8>>)>, Vec<AtomRange>); |
|
| 945 | + | type Op = ( |
|
| 946 | + | ChangeHash, |
|
| 947 | + | Vec<(u32, Vertex, Vertex, Vec<Vec<u8>>)>, |
|
| 948 | + | Vec<AtomRange>, |
|
| 949 | + | ); |
|
| 669 | 950 | let ops: Vec<Op> = vec![ |
|
| 670 | - | (h(1), vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))], vec![]), |
|
| 671 | - | (h(2), vec![(0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), lines(&["p\n"]))], vec![]), |
|
| 672 | - | (h(3), vec![(0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), lines(&["q\n", "r\n"]))], vec![AtomRange { change: h(1), from: 2, to: 3 }]), |
|
| 673 | - | (h(4), vec![(0, Vertex::Atom((h(3), 1)), Vertex::Atom((h(1), 1)), lines(&["s\n"]))], vec![AtomRange { change: h(1), from: 1, to: 2 }]), |
|
| 674 | - | (h(5), vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))], vec![]), |
|
| 951 | + | ( |
|
| 952 | + | h(1), |
|
| 953 | + | vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))], |
|
| 954 | + | vec![], |
|
| 955 | + | ), |
|
| 956 | + | ( |
|
| 957 | + | h(2), |
|
| 958 | + | vec![( |
|
| 959 | + | 0, |
|
| 960 | + | Vertex::Atom((h(1), 0)), |
|
| 961 | + | Vertex::Atom((h(1), 1)), |
|
| 962 | + | lines(&["p\n"]), |
|
| 963 | + | )], |
|
| 964 | + | vec![], |
|
| 965 | + | ), |
|
| 966 | + | ( |
|
| 967 | + | h(3), |
|
| 968 | + | vec![( |
|
| 969 | + | 0, |
|
| 970 | + | Vertex::Atom((h(1), 0)), |
|
| 971 | + | Vertex::Atom((h(1), 1)), |
|
| 972 | + | lines(&["q\n", "r\n"]), |
|
| 973 | + | )], |
|
| 974 | + | vec![AtomRange { |
|
| 975 | + | change: h(1), |
|
| 976 | + | from: 2, |
|
| 977 | + | to: 3, |
|
| 978 | + | }], |
|
| 979 | + | ), |
|
| 980 | + | ( |
|
| 981 | + | h(4), |
|
| 982 | + | vec![( |
|
| 983 | + | 0, |
|
| 984 | + | Vertex::Atom((h(3), 1)), |
|
| 985 | + | Vertex::Atom((h(1), 1)), |
|
| 986 | + | lines(&["s\n"]), |
|
| 987 | + | )], |
|
| 988 | + | vec![AtomRange { |
|
| 989 | + | change: h(1), |
|
| 990 | + | from: 1, |
|
| 991 | + | to: 2, |
|
| 992 | + | }], |
|
| 993 | + | ), |
|
| 994 | + | ( |
|
| 995 | + | h(5), |
|
| 996 | + | vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))], |
|
| 997 | + | vec![], |
|
| 998 | + | ), |
|
| 675 | 999 | ]; |
|
| 676 | 1000 | let deps = Deps( |
|
| 677 | - | [(h(2), vec![h(1)]), (h(3), vec![h(1)]), (h(4), vec![h(3), h(1)]), (h(5), vec![h(1)])].into_iter().collect(), |
|
| 1001 | + | [ |
|
| 1002 | + | (h(2), vec![h(1)]), |
|
| 1003 | + | (h(3), vec![h(1)]), |
|
| 1004 | + | (h(4), vec![h(3), h(1)]), |
|
| 1005 | + | (h(5), vec![h(1)]), |
|
| 1006 | + | ] |
|
| 1007 | + | .into_iter() |
|
| 1008 | + | .collect(), |
|
| 678 | 1009 | ); |
|
| 679 | 1010 | let build = |order: &[usize]| { |
|
| 680 | 1011 | let mut g = FileGraph::new(); |
|
| … | 22 unchanged lines |
modified crates/oxy-core/src/identity.rs +153 −34
| … | 5 unchanged lines |
||
| 6 | 6 | ||
| 7 | 7 | use crate::cbor::{self, Value}; |
|
| 8 | 8 | use crate::hash::{hash_object, KeyId, ObjectRef, ObjectType}; |
|
| 9 | - | use crate::objects::{array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION}; |
|
| 9 | + | use crate::objects::{ |
|
| 10 | + | array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION, |
|
| 11 | + | }; |
|
| 10 | 12 | use ed25519_dalek::ed25519::signature::Signer as _; |
|
| 11 | - | use ed25519_dalek::{Signature as EdSignature, SigningKey as EdSigningKey, VerifyingKey as EdVerifyingKey}; |
|
| 12 | - | use ml_dsa::{EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed, Signature as MlSignature, SigningKey as MlSigningKey, VerifyingKey as MlVerifyingKey}; |
|
| 13 | + | use ed25519_dalek::{ |
|
| 14 | + | Signature as EdSignature, SigningKey as EdSigningKey, VerifyingKey as EdVerifyingKey, |
|
| 15 | + | }; |
|
| 16 | + | use ml_dsa::{ |
|
| 17 | + | EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed, Signature as MlSignature, |
|
| 18 | + | SigningKey as MlSigningKey, VerifyingKey as MlVerifyingKey, |
|
| 19 | + | }; |
|
| 13 | 20 | use rand::RngCore; |
|
| 14 | 21 | ||
| 15 | 22 | pub const ED25519_PK_LEN: usize = 32; |
|
| … | 25 unchanged lines |
||
| 41 | 48 | (4, Value::Text(self.email.clone())), |
|
| 42 | 49 | (5, Value::Uint(self.created)), |
|
| 43 | 50 | (6, self.expires.map(Value::Uint).unwrap_or(Value::Null)), |
|
| 44 | - | (7, self.previous.map(|p| Value::Bytes(p.0.to_vec())).unwrap_or(Value::Null)), |
|
| 45 | - | (8, Value::Array(self.delegates.iter().map(|d| Value::Bytes(d.0.to_vec())).collect())), |
|
| 51 | + | ( |
|
| 52 | + | 7, |
|
| 53 | + | self.previous |
|
| 54 | + | .map(|p| Value::Bytes(p.0.to_vec())) |
|
| 55 | + | .unwrap_or(Value::Null), |
|
| 56 | + | ), |
|
| 57 | + | ( |
|
| 58 | + | 8, |
|
| 59 | + | Value::Array( |
|
| 60 | + | self.delegates |
|
| 61 | + | .iter() |
|
| 62 | + | .map(|d| Value::Bytes(d.0.to_vec())) |
|
| 63 | + | .collect(), |
|
| 64 | + | ), |
|
| 65 | + | ), |
|
| 46 | 66 | ]) |
|
| 47 | 67 | } |
|
| 48 | 68 | pub fn encode(&self) -> Vec<u8> { |
|
| … | 4 unchanged lines |
||
| 53 | 73 | } |
|
| 54 | 74 | pub fn decode(b: &[u8]) -> Result<Identity, ObjectError> { |
|
| 55 | 75 | let v = cbor::decode(b)?; |
|
| 56 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("identity: expected map".into()))?; |
|
| 76 | + | let m = v |
|
| 77 | + | .as_map() |
|
| 78 | + | .ok_or_else(|| ObjectError::Schema("identity: expected map".into()))?; |
|
| 57 | 79 | check_version(m)?; |
|
| 58 | 80 | let ed = bytes(field(m, 1, "ed25519_pk")?, "ed25519_pk")?; |
|
| 59 | 81 | let ml = bytes(field(m, 2, "mldsa65_pk")?, "mldsa65_pk")?; |
|
| 60 | 82 | if ed.len() != ED25519_PK_LEN || ml.len() != MLDSA65_PK_LEN { |
|
| 61 | - | return Err(ObjectError::Schema("identity: wrong public key length".into())); |
|
| 83 | + | return Err(ObjectError::Schema( |
|
| 84 | + | "identity: wrong public key length".into(), |
|
| 85 | + | )); |
|
| 62 | 86 | } |
|
| 63 | - | let opt_u = |v: &Value| -> Result<Option<u64>, ObjectError> { if v.is_null() { Ok(None) } else { uint(v, "expires").map(Some) } }; |
|
| 64 | - | let opt_r = |v: &Value| -> Result<Option<KeyId>, ObjectError> { if v.is_null() { Ok(None) } else { oref(v, "previous").map(Some) } }; |
|
| 65 | - | let mut delegates = array(field(m, 8, "delegates")?, "delegates")?.iter().map(|d| oref(d, "delegate")).collect::<Result<Vec<_>, _>>()?; |
|
| 87 | + | let opt_u = |v: &Value| -> Result<Option<u64>, ObjectError> { |
|
| 88 | + | if v.is_null() { |
|
| 89 | + | Ok(None) |
|
| 90 | + | } else { |
|
| 91 | + | uint(v, "expires").map(Some) |
|
| 92 | + | } |
|
| 93 | + | }; |
|
| 94 | + | let opt_r = |v: &Value| -> Result<Option<KeyId>, ObjectError> { |
|
| 95 | + | if v.is_null() { |
|
| 96 | + | Ok(None) |
|
| 97 | + | } else { |
|
| 98 | + | oref(v, "previous").map(Some) |
|
| 99 | + | } |
|
| 100 | + | }; |
|
| 101 | + | let mut delegates = array(field(m, 8, "delegates")?, "delegates")? |
|
| 102 | + | .iter() |
|
| 103 | + | .map(|d| oref(d, "delegate")) |
|
| 104 | + | .collect::<Result<Vec<_>, _>>()?; |
|
| 66 | 105 | delegates.sort(); |
|
| 67 | 106 | delegates.dedup(); |
|
| 68 | 107 | Ok(Identity { |
|
| … | 19 unchanged lines |
||
| 88 | 127 | let mut delegates = delegates; |
|
| 89 | 128 | delegates.sort(); |
|
| 90 | 129 | delegates.dedup(); |
|
| 91 | - | Identity { previous: Some(self.hash()), delegates, expires, created, ..self.clone() } |
|
| 130 | + | Identity { |
|
| 131 | + | previous: Some(self.hash()), |
|
| 132 | + | delegates, |
|
| 133 | + | expires, |
|
| 134 | + | created, |
|
| 135 | + | ..self.clone() |
|
| 136 | + | } |
|
| 92 | 137 | } |
|
| 93 | 138 | ||
| 94 | 139 | fn ed_key(&self) -> Result<EdVerifyingKey, VerifyError> { |
|
| 95 | 140 | EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| VerifyError::BadKey) |
|
| 96 | 141 | } |
|
| 97 | 142 | fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, VerifyError> { |
|
| 98 | - | let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| VerifyError::BadKey)?; |
|
| 143 | + | let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()) |
|
| 144 | + | .map_err(|_| VerifyError::BadKey)?; |
|
| 99 | 145 | Ok(MlVerifyingKey::decode(&enc)) |
|
| 100 | 146 | } |
|
| 101 | 147 | } |
|
| … | 49 unchanged lines |
||
| 151 | 197 | ]) |
|
| 152 | 198 | } |
|
| 153 | 199 | pub fn from_value(v: &Value) -> Result<Signature, ObjectError> { |
|
| 154 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?; |
|
| 200 | + | let m = v |
|
| 201 | + | .as_map() |
|
| 202 | + | .ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?; |
|
| 155 | 203 | let alg = uint(field(m, 1, "alg")?, "alg")?; |
|
| 156 | 204 | if alg != ALG_COMPOSITE { |
|
| 157 | - | return Err(ObjectError::Schema(format!("unknown signature algorithm {alg}"))); |
|
| 205 | + | return Err(ObjectError::Schema(format!( |
|
| 206 | + | "unknown signature algorithm {alg}" |
|
| 207 | + | ))); |
|
| 158 | 208 | } |
|
| 159 | 209 | let ed = bytes(field(m, 2, "ed")?, "ed")?; |
|
| 160 | 210 | let ml = bytes(field(m, 3, "ml")?, "ml")?; |
|
| 161 | 211 | if ed.len() != ED25519_SIG_LEN || ml.len() != MLDSA65_SIG_LEN { |
|
| 162 | 212 | return Err(ObjectError::Schema("signature: wrong length".into())); |
|
| 163 | 213 | } |
|
| 164 | - | Ok(Signature { key: oref(field(m, 0, "key")?, "key")?, ed: ed.try_into().unwrap(), ml: ml.to_vec() }) |
|
| 214 | + | Ok(Signature { |
|
| 215 | + | key: oref(field(m, 0, "key")?, "key")?, |
|
| 216 | + | ed: ed.try_into().unwrap(), |
|
| 217 | + | ml: ml.to_vec(), |
|
| 218 | + | }) |
|
| 165 | 219 | } |
|
| 166 | 220 | pub fn encode_list(sigs: &[Signature]) -> Vec<u8> { |
|
| 167 | 221 | cbor::encode(&Value::Array(sigs.iter().map(|s| s.to_value()).collect())) |
|
| 168 | 222 | } |
|
| 169 | 223 | pub fn decode_list(b: &[u8]) -> Result<Vec<Signature>, ObjectError> { |
|
| 170 | 224 | let v = cbor::decode(b)?; |
|
| 171 | - | array(&v, "signatures")?.iter().map(Signature::from_value).collect() |
|
| 225 | + | array(&v, "signatures")? |
|
| 226 | + | .iter() |
|
| 227 | + | .map(Signature::from_value) |
|
| 228 | + | .collect() |
|
| 172 | 229 | } |
|
| 173 | 230 | } |
|
| 174 | 231 | ||
| … | 10 unchanged lines |
||
| 185 | 242 | } |
|
| 186 | 243 | ||
| 187 | 244 | /// Verify a composite signature by `identity` over `target` (spec §14.3). |
|
| 188 | - | pub fn verify(identity: &Identity, sig: &Signature, domain: Domain, target: &ObjectRef) -> Result<(), VerifyError> { |
|
| 245 | + | pub fn verify( |
|
| 246 | + | identity: &Identity, |
|
| 247 | + | sig: &Signature, |
|
| 248 | + | domain: Domain, |
|
| 249 | + | target: &ObjectRef, |
|
| 250 | + | ) -> Result<(), VerifyError> { |
|
| 189 | 251 | let key = identity.hash(); |
|
| 190 | 252 | if sig.key != key { |
|
| 191 | 253 | return Err(VerifyError::KeyMismatch); |
|
| 192 | 254 | } |
|
| 193 | 255 | let msg = signing_message(domain, &key, target); |
|
| 194 | 256 | let ed_sig = EdSignature::from_bytes(&sig.ed); |
|
| 195 | - | identity.ed_key()?.verify_strict(&msg, &ed_sig).map_err(|_| VerifyError::Ed25519)?; |
|
| 196 | - | let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?; |
|
| 257 | + | identity |
|
| 258 | + | .ed_key()? |
|
| 259 | + | .verify_strict(&msg, &ed_sig) |
|
| 260 | + | .map_err(|_| VerifyError::Ed25519)?; |
|
| 261 | + | let enc = |
|
| 262 | + | EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?; |
|
| 197 | 263 | let ml_sig = MlSignature::<MlDsa65>::decode(&enc).ok_or(VerifyError::MlDsa)?; |
|
| 198 | 264 | if !identity.ml_key()?.verify_with_context(&msg, b"", &ml_sig) { |
|
| 199 | 265 | return Err(VerifyError::MlDsa); |
|
| … | 34 unchanged lines |
||
| 234 | 300 | pub fn identity(&self, name: &str, email: &str, created: u64) -> Identity { |
|
| 235 | 301 | Identity { |
|
| 236 | 302 | ed25519_pk: self.ed().verifying_key().to_bytes(), |
|
| 237 | - | mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(), |
|
| 303 | + | mldsa65_pk: self |
|
| 304 | + | .ml() |
|
| 305 | + | .expanded_key() |
|
| 306 | + | .verifying_key() |
|
| 307 | + | .encode() |
|
| 308 | + | .as_slice() |
|
| 309 | + | .to_vec(), |
|
| 238 | 310 | name: name.into(), |
|
| 239 | 311 | email: email.into(), |
|
| 240 | 312 | created, |
|
| … | 6 unchanged lines |
||
| 247 | 319 | pub fn sign(&self, domain: Domain, key: &KeyId, target: &ObjectRef) -> Signature { |
|
| 248 | 320 | let msg = signing_message(domain, key, target); |
|
| 249 | 321 | let ed = self.ed().sign(&msg).to_bytes(); |
|
| 250 | - | let ml = self.ml().expanded_key().sign_deterministic(&msg, b"").expect("empty context is valid").encode().as_slice().to_vec(); |
|
| 322 | + | let ml = self |
|
| 323 | + | .ml() |
|
| 324 | + | .expanded_key() |
|
| 325 | + | .sign_deterministic(&msg, b"") |
|
| 326 | + | .expect("empty context is valid") |
|
| 327 | + | .encode() |
|
| 328 | + | .as_slice() |
|
| 329 | + | .to_vec(); |
|
| 251 | 330 | Signature { key: *key, ed, ml } |
|
| 252 | 331 | } |
|
| 253 | 332 | ||
| … | 8 unchanged lines |
||
| 262 | 341 | ||
| 263 | 342 | pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), ObjectError> { |
|
| 264 | 343 | let v = cbor::decode(b)?; |
|
| 265 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?; |
|
| 344 | + | let m = v |
|
| 345 | + | .as_map() |
|
| 346 | + | .ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?; |
|
| 266 | 347 | check_version(m)?; |
|
| 267 | 348 | let seed = |k: u64| -> Result<[u8; 32], ObjectError> { |
|
| 268 | - | bytes(field(m, k, "seed")?, "seed")?.try_into().map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into())) |
|
| 349 | + | bytes(field(m, k, "seed")?, "seed")? |
|
| 350 | + | .try_into() |
|
| 351 | + | .map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into())) |
|
| 269 | 352 | }; |
|
| 270 | - | Ok((oref(field(m, 1, "key")?, "key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? })) |
|
| 353 | + | Ok(( |
|
| 354 | + | oref(field(m, 1, "key")?, "key")?, |
|
| 355 | + | SecretKey { |
|
| 356 | + | ed_seed: seed(2)?, |
|
| 357 | + | ml_seed: seed(3)?, |
|
| 358 | + | }, |
|
| 359 | + | )) |
|
| 271 | 360 | } |
|
| 272 | 361 | } |
|
| 273 | 362 | ||
| … | 2 unchanged lines |
||
| 276 | 365 | use super::*; |
|
| 277 | 366 | ||
| 278 | 367 | fn fixed_key() -> SecretKey { |
|
| 279 | - | SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] } |
|
| 368 | + | SecretKey { |
|
| 369 | + | ed_seed: [1; 32], |
|
| 370 | + | ml_seed: [2; 32], |
|
| 371 | + | } |
|
| 280 | 372 | } |
|
| 281 | 373 | ||
| 282 | 374 | #[test] |
|
| … | 5 unchanged lines |
||
| 288 | 380 | assert_eq!(d, id); |
|
| 289 | 381 | assert_eq!(d.hash(), id.hash()); |
|
| 290 | 382 | // Deterministic from seeds. |
|
| 291 | - | assert_eq!(fixed_key().identity("Ada", "[email protected]", 1_700_000_000_000).hash(), id.hash()); |
|
| 383 | + | assert_eq!( |
|
| 384 | + | fixed_key() |
|
| 385 | + | .identity("Ada", "[email protected]", 1_700_000_000_000) |
|
| 386 | + | .hash(), |
|
| 387 | + | id.hash() |
|
| 388 | + | ); |
|
| 292 | 389 | let mut other = id.clone(); |
|
| 293 | 390 | other.delegates.push(ObjectRef([9; 32])); |
|
| 294 | 391 | assert_ne!(other.hash(), id.hash()); |
|
| … | 9 unchanged lines |
||
| 304 | 401 | assert_eq!(sig.ml.len(), MLDSA65_SIG_LEN); |
|
| 305 | 402 | assert_eq!(verify(&id, &sig, Domain::Change, &target), Ok(())); |
|
| 306 | 403 | // Different target, different domain: invalid. |
|
| 307 | - | assert_eq!(verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])), Err(VerifyError::Ed25519)); |
|
| 308 | - | assert_eq!(verify(&id, &sig, Domain::Attest, &target), Err(VerifyError::Ed25519)); |
|
| 404 | + | assert_eq!( |
|
| 405 | + | verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])), |
|
| 406 | + | Err(VerifyError::Ed25519) |
|
| 407 | + | ); |
|
| 408 | + | assert_eq!( |
|
| 409 | + | verify(&id, &sig, Domain::Attest, &target), |
|
| 410 | + | Err(VerifyError::Ed25519) |
|
| 411 | + | ); |
|
| 309 | 412 | // Deterministic signatures. |
|
| 310 | 413 | assert_eq!(sk.sign(Domain::Change, &key, &target), sig); |
|
| 311 | 414 | // Signature list round trip. |
|
| 312 | - | let l = Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap(); |
|
| 415 | + | let l = |
|
| 416 | + | Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap(); |
|
| 313 | 417 | assert_eq!(l, vec![sig]); |
|
| 314 | 418 | } |
|
| 315 | 419 | ||
| … | 7 unchanged lines |
||
| 323 | 427 | ||
| 324 | 428 | let mut no_ml = good.clone(); |
|
| 325 | 429 | no_ml.ml = vec![0; MLDSA65_SIG_LEN]; |
|
| 326 | - | assert_eq!(verify(&id, &no_ml, Domain::Change, &target), Err(VerifyError::MlDsa)); |
|
| 430 | + | assert_eq!( |
|
| 431 | + | verify(&id, &no_ml, Domain::Change, &target), |
|
| 432 | + | Err(VerifyError::MlDsa) |
|
| 433 | + | ); |
|
| 327 | 434 | ||
| 328 | 435 | let mut bad_ed = good.clone(); |
|
| 329 | 436 | bad_ed.ed[10] ^= 1; |
|
| 330 | - | assert_eq!(verify(&id, &bad_ed, Domain::Change, &target), Err(VerifyError::Ed25519)); |
|
| 437 | + | assert_eq!( |
|
| 438 | + | verify(&id, &bad_ed, Domain::Change, &target), |
|
| 439 | + | Err(VerifyError::Ed25519) |
|
| 440 | + | ); |
|
| 331 | 441 | ||
| 332 | 442 | // A signature made for another KeyId over the same target does not transfer. |
|
| 333 | - | let other = SecretKey { ed_seed: [3; 32], ml_seed: [4; 32] }; |
|
| 443 | + | let other = SecretKey { |
|
| 444 | + | ed_seed: [3; 32], |
|
| 445 | + | ml_seed: [4; 32], |
|
| 446 | + | }; |
|
| 334 | 447 | let other_id = other.identity("Bob", "b@b", 1); |
|
| 335 | 448 | let theirs = other.sign(Domain::Change, &other_id.hash(), &target); |
|
| 336 | - | assert_eq!(verify(&id, &theirs, Domain::Change, &target), Err(VerifyError::KeyMismatch)); |
|
| 449 | + | assert_eq!( |
|
| 450 | + | verify(&id, &theirs, Domain::Change, &target), |
|
| 451 | + | Err(VerifyError::KeyMismatch) |
|
| 452 | + | ); |
|
| 337 | 453 | let mut relabelled = theirs.clone(); |
|
| 338 | 454 | relabelled.key = key; |
|
| 339 | - | assert_eq!(verify(&id, &relabelled, Domain::Change, &target), Err(VerifyError::Ed25519)); |
|
| 455 | + | assert_eq!( |
|
| 456 | + | verify(&id, &relabelled, Domain::Change, &target), |
|
| 457 | + | Err(VerifyError::Ed25519) |
|
| 458 | + | ); |
|
| 340 | 459 | ||
| 341 | 460 | // Wrong length is rejected at decode time. |
|
| 342 | 461 | let mut short = good.to_value(); |
|
| … | 17 unchanged lines |
modified crates/oxy-core/src/ids.rs +8 −2
| … | 49 unchanged lines |
||
| 50 | 50 | }; |
|
| 51 | 51 | } |
|
| 52 | 52 | ||
| 53 | - | id128!(ChangeId, "Stable identity of a change across rewrites (spec §6.2)."); |
|
| 53 | + | id128!( |
|
| 54 | + | ChangeId, |
|
| 55 | + | "Stable identity of a change across rewrites (spec §6.2)." |
|
| 56 | + | ); |
|
| 54 | 57 | id128!(NodeId, "Stable identity of a file, directory, or symlink across renames (spec §7.1). `NodeId::ZERO` is the root directory."); |
|
| 55 | - | id128!(WorkspaceId, "Identity of a checkout directory (spec §12.2)."); |
|
| 58 | + | id128!( |
|
| 59 | + | WorkspaceId, |
|
| 60 | + | "Identity of a checkout directory (spec §12.2)." |
|
| 61 | + | ); |
|
| 56 | 62 | ||
| 57 | 63 | impl NodeId { |
|
| 58 | 64 | pub const ROOT: NodeId = NodeId::ZERO; |
|
| … | 35 unchanged lines |
modified crates/oxy-core/src/objects.rs +315 −72
| … | 28 unchanged lines |
||
| 29 | 29 | pub(crate) type Map = BTreeMap<u64, Value>; |
|
| 30 | 30 | ||
| 31 | 31 | pub(crate) fn field<'a>(m: &'a Map, k: u64, what: &str) -> Result<&'a Value, ObjectError> { |
|
| 32 | - | m.get(&k).ok_or_else(|| ObjectError::Schema(format!("missing field {k} ({what})"))) |
|
| 32 | + | m.get(&k) |
|
| 33 | + | .ok_or_else(|| ObjectError::Schema(format!("missing field {k} ({what})"))) |
|
| 33 | 34 | } |
|
| 34 | 35 | pub(crate) fn uint(v: &Value, what: &str) -> Result<u64, ObjectError> { |
|
| 35 | - | v.as_uint().ok_or_else(|| ObjectError::Schema(format!("{what}: expected unsigned integer"))) |
|
| 36 | + | v.as_uint() |
|
| 37 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected unsigned integer"))) |
|
| 36 | 38 | } |
|
| 37 | 39 | pub(crate) fn u32_(v: &Value, what: &str) -> Result<u32, ObjectError> { |
|
| 38 | 40 | u32::try_from(uint(v, what)?).map_err(|_| ObjectError::Schema(format!("{what}: exceeds u32"))) |
|
| 39 | 41 | } |
|
| 40 | 42 | pub(crate) fn text<'a>(v: &'a Value, what: &str) -> Result<&'a str, ObjectError> { |
|
| 41 | - | v.as_text().ok_or_else(|| ObjectError::Schema(format!("{what}: expected text"))) |
|
| 43 | + | v.as_text() |
|
| 44 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected text"))) |
|
| 42 | 45 | } |
|
| 43 | 46 | pub(crate) fn array<'a>(v: &'a Value, what: &str) -> Result<&'a [Value], ObjectError> { |
|
| 44 | - | v.as_array().ok_or_else(|| ObjectError::Schema(format!("{what}: expected array"))) |
|
| 47 | + | v.as_array() |
|
| 48 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected array"))) |
|
| 45 | 49 | } |
|
| 46 | 50 | pub(crate) fn map<'a>(v: &'a Value, what: &str) -> Result<&'a Map, ObjectError> { |
|
| 47 | - | v.as_map().ok_or_else(|| ObjectError::Schema(format!("{what}: expected map"))) |
|
| 51 | + | v.as_map() |
|
| 52 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected map"))) |
|
| 48 | 53 | } |
|
| 49 | 54 | pub(crate) fn bytes<'a>(v: &'a Value, what: &str) -> Result<&'a [u8], ObjectError> { |
|
| 50 | - | v.as_bytes().ok_or_else(|| ObjectError::Schema(format!("{what}: expected bytes"))) |
|
| 55 | + | v.as_bytes() |
|
| 56 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected bytes"))) |
|
| 51 | 57 | } |
|
| 52 | 58 | pub(crate) fn oref(v: &Value, what: &str) -> Result<ObjectRef, ObjectError> { |
|
| 53 | - | ObjectRef::from_slice(bytes(v, what)?).ok_or_else(|| ObjectError::Schema(format!("{what}: expected 32 bytes"))) |
|
| 59 | + | ObjectRef::from_slice(bytes(v, what)?) |
|
| 60 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected 32 bytes"))) |
|
| 54 | 61 | } |
|
| 55 | 62 | pub(crate) fn node_id(v: &Value, what: &str) -> Result<NodeId, ObjectError> { |
|
| 56 | - | NodeId::from_slice(bytes(v, what)?).ok_or_else(|| ObjectError::Schema(format!("{what}: expected 16 bytes"))) |
|
| 63 | + | NodeId::from_slice(bytes(v, what)?) |
|
| 64 | + | .ok_or_else(|| ObjectError::Schema(format!("{what}: expected 16 bytes"))) |
|
| 57 | 65 | } |
|
| 58 | 66 | pub(crate) fn check_version(m: &Map) -> Result<(), ObjectError> { |
|
| 59 | 67 | let v = uint(field(m, 0, "version")?, "version")?; |
|
| … | 18 unchanged lines |
||
| 78 | 86 | .duration_since(std::time::UNIX_EPOCH) |
|
| 79 | 87 | .map(|d| d.as_millis() as u64) |
|
| 80 | 88 | .unwrap_or(0); |
|
| 81 | - | Hlc { wall_ms, logical: 0 } |
|
| 89 | + | Hlc { |
|
| 90 | + | wall_ms, |
|
| 91 | + | logical: 0, |
|
| 92 | + | } |
|
| 82 | 93 | } |
|
| 83 | 94 | pub(crate) fn to_value(self) -> Value { |
|
| 84 | - | Value::map([(0, Value::Uint(self.wall_ms)), (1, Value::Uint(self.logical as u64))]) |
|
| 95 | + | Value::map([ |
|
| 96 | + | (0, Value::Uint(self.wall_ms)), |
|
| 97 | + | (1, Value::Uint(self.logical as u64)), |
|
| 98 | + | ]) |
|
| 85 | 99 | } |
|
| 86 | 100 | pub(crate) fn from_value(v: &Value) -> Result<Hlc, ObjectError> { |
|
| 87 | 101 | let m = map(v, "hlc")?; |
|
| … | 43 unchanged lines |
||
| 131 | 145 | } |
|
| 132 | 146 | ||
| 133 | 147 | impl Mode { |
|
| 134 | - | pub const TEXT: Mode = Mode { exec: false, opaque: false }; |
|
| 135 | - | pub const OPAQUE: Mode = Mode { exec: false, opaque: true }; |
|
| 148 | + | pub const TEXT: Mode = Mode { |
|
| 149 | + | exec: false, |
|
| 150 | + | opaque: false, |
|
| 151 | + | }; |
|
| 152 | + | pub const OPAQUE: Mode = Mode { |
|
| 153 | + | exec: false, |
|
| 154 | + | opaque: true, |
|
| 155 | + | }; |
|
| 136 | 156 | pub(crate) fn bits(self) -> u64 { |
|
| 137 | 157 | (self.exec as u64) | ((self.opaque as u64) << 1) |
|
| 138 | 158 | } |
|
| … | 1 unchanged lines |
||
| 140 | 160 | if b > 3 { |
|
| 141 | 161 | return schema(format!("unknown mode bits {b}")); |
|
| 142 | 162 | } |
|
| 143 | - | Ok(Mode { exec: b & 1 != 0, opaque: b & 2 != 0 }) |
|
| 163 | + | Ok(Mode { |
|
| 164 | + | exec: b & 1 != 0, |
|
| 165 | + | opaque: b & 2 != 0, |
|
| 166 | + | }) |
|
| 144 | 167 | } |
|
| 145 | 168 | } |
|
| 146 | 169 | ||
| … | 7 unchanged lines |
||
| 154 | 177 | ||
| 155 | 178 | impl OpRef { |
|
| 156 | 179 | fn to_value(self) -> Value { |
|
| 157 | - | Value::Array(vec![Value::Bytes(self.change.0.to_vec()), Value::Uint(self.index as u64)]) |
|
| 180 | + | Value::Array(vec![ |
|
| 181 | + | Value::Bytes(self.change.0.to_vec()), |
|
| 182 | + | Value::Uint(self.index as u64), |
|
| 183 | + | ]) |
|
| 158 | 184 | } |
|
| 159 | 185 | fn from_value(v: &Value, what: &str) -> Result<OpRef, ObjectError> { |
|
| 160 | 186 | let a = array(v, what)?; |
|
| 161 | 187 | if a.len() != 2 { |
|
| 162 | 188 | return schema(format!("{what}: expected [hash, index]")); |
|
| 163 | 189 | } |
|
| 164 | - | Ok(OpRef { change: oref(&a[0], what)?, index: u32_(&a[1], what)? }) |
|
| 190 | + | Ok(OpRef { |
|
| 191 | + | change: oref(&a[0], what)?, |
|
| 192 | + | index: u32_(&a[1], what)?, |
|
| 193 | + | }) |
|
| 165 | 194 | } |
|
| 166 | 195 | /// Replace a self reference with the concrete change hash. |
|
| 167 | 196 | pub fn resolve(self, me: ChangeHash) -> OpRef { |
|
| 168 | - | if self.change.is_zero() { OpRef { change: me, ..self } } else { self } |
|
| 197 | + | if self.change.is_zero() { |
|
| 198 | + | OpRef { change: me, ..self } |
|
| 199 | + | } else { |
|
| 200 | + | self |
|
| 201 | + | } |
|
| 169 | 202 | } |
|
| 170 | 203 | } |
|
| 171 | 204 | ||
| 172 | 205 | #[derive(Debug, Clone, PartialEq, Eq)] |
|
| 173 | 206 | pub enum TreeOp { |
|
| 174 | - | AddNode { node: NodeId, kind: NodeKind, mode: Mode, parent: NodeId, name: String }, |
|
| 175 | - | RemoveNode { node: NodeId, placement: OpRef }, |
|
| 176 | - | Move { node: NodeId, old: OpRef, parent: NodeId, name: String }, |
|
| 177 | - | SetMode { node: NodeId, old: OpRef, mode: Mode }, |
|
| 207 | + | AddNode { |
|
| 208 | + | node: NodeId, |
|
| 209 | + | kind: NodeKind, |
|
| 210 | + | mode: Mode, |
|
| 211 | + | parent: NodeId, |
|
| 212 | + | name: String, |
|
| 213 | + | }, |
|
| 214 | + | RemoveNode { |
|
| 215 | + | node: NodeId, |
|
| 216 | + | placement: OpRef, |
|
| 217 | + | }, |
|
| 218 | + | Move { |
|
| 219 | + | node: NodeId, |
|
| 220 | + | old: OpRef, |
|
| 221 | + | parent: NodeId, |
|
| 222 | + | name: String, |
|
| 223 | + | }, |
|
| 224 | + | SetMode { |
|
| 225 | + | node: NodeId, |
|
| 226 | + | old: OpRef, |
|
| 227 | + | mode: Mode, |
|
| 228 | + | }, |
|
| 178 | 229 | } |
|
| 179 | 230 | ||
| 180 | 231 | /// Names must be valid, non-empty, not `.`/`..`, and free of `/` and NUL (spec §7.1). |
|
| … | 17 unchanged lines |
||
| 198 | 249 | fn to_value(&self) -> Value { |
|
| 199 | 250 | let nid = |n: &NodeId| Value::Bytes(n.0.to_vec()); |
|
| 200 | 251 | match self { |
|
| 201 | - | TreeOp::AddNode { node, kind, mode, parent, name } => Value::map([ |
|
| 252 | + | TreeOp::AddNode { |
|
| 253 | + | node, |
|
| 254 | + | kind, |
|
| 255 | + | mode, |
|
| 256 | + | parent, |
|
| 257 | + | name, |
|
| 258 | + | } => Value::map([ |
|
| 202 | 259 | (0, Value::Uint(0)), |
|
| 203 | 260 | (1, nid(node)), |
|
| 204 | 261 | (2, Value::Uint(kind.tag())), |
|
| … | 1 unchanged lines |
||
| 206 | 263 | (4, nid(parent)), |
|
| 207 | 264 | (5, Value::Text(name.clone())), |
|
| 208 | 265 | ]), |
|
| 209 | - | TreeOp::RemoveNode { node, placement } => { |
|
| 210 | - | Value::map([(0, Value::Uint(1)), (1, nid(node)), (2, placement.to_value())]) |
|
| 211 | - | } |
|
| 212 | - | TreeOp::Move { node, old, parent, name } => Value::map([ |
|
| 266 | + | TreeOp::RemoveNode { node, placement } => Value::map([ |
|
| 267 | + | (0, Value::Uint(1)), |
|
| 268 | + | (1, nid(node)), |
|
| 269 | + | (2, placement.to_value()), |
|
| 270 | + | ]), |
|
| 271 | + | TreeOp::Move { |
|
| 272 | + | node, |
|
| 273 | + | old, |
|
| 274 | + | parent, |
|
| 275 | + | name, |
|
| 276 | + | } => Value::map([ |
|
| 213 | 277 | (0, Value::Uint(2)), |
|
| 214 | 278 | (1, nid(node)), |
|
| 215 | 279 | (2, old.to_value()), |
|
| … | 25 unchanged lines |
||
| 241 | 305 | name, |
|
| 242 | 306 | } |
|
| 243 | 307 | } |
|
| 244 | - | 1 => TreeOp::RemoveNode { node, placement: OpRef::from_value(field(m, 2, "placement")?, "placement")? }, |
|
| 308 | + | 1 => TreeOp::RemoveNode { |
|
| 309 | + | node, |
|
| 310 | + | placement: OpRef::from_value(field(m, 2, "placement")?, "placement")?, |
|
| 311 | + | }, |
|
| 245 | 312 | 2 => { |
|
| 246 | 313 | let name = text(field(m, 5, "name")?, "name")?.to_owned(); |
|
| 247 | 314 | validate_name(&name)?; |
|
| … | 32 unchanged lines |
||
| 280 | 347 | match self { |
|
| 281 | 348 | AtomRef::Start => Value::Uint(0), |
|
| 282 | 349 | AtomRef::End => Value::Uint(1), |
|
| 283 | - | AtomRef::Atom { change, index } => { |
|
| 284 | - | Value::Array(vec![Value::Bytes(change.0.to_vec()), Value::Uint(index as u64)]) |
|
| 285 | - | } |
|
| 350 | + | AtomRef::Atom { change, index } => Value::Array(vec![ |
|
| 351 | + | Value::Bytes(change.0.to_vec()), |
|
| 352 | + | Value::Uint(index as u64), |
|
| 353 | + | ]), |
|
| 286 | 354 | } |
|
| 287 | 355 | } |
|
| 288 | 356 | fn from_value(v: &Value, what: &str) -> Result<AtomRef, ObjectError> { |
|
| 289 | 357 | match v { |
|
| 290 | 358 | Value::Uint(0) => Ok(AtomRef::Start), |
|
| 291 | 359 | Value::Uint(1) => Ok(AtomRef::End), |
|
| 292 | - | Value::Array(a) if a.len() == 2 => { |
|
| 293 | - | Ok(AtomRef::Atom { change: oref(&a[0], what)?, index: u32_(&a[1], what)? }) |
|
| 294 | - | } |
|
| 360 | + | Value::Array(a) if a.len() == 2 => Ok(AtomRef::Atom { |
|
| 361 | + | change: oref(&a[0], what)?, |
|
| 362 | + | index: u32_(&a[1], what)?, |
|
| 363 | + | }), |
|
| 295 | 364 | _ => schema(format!("{what}: expected atom reference")), |
|
| 296 | 365 | } |
|
| 297 | 366 | } |
|
| 298 | 367 | pub fn resolve(self, me: ChangeHash) -> AtomRef { |
|
| 299 | 368 | match self { |
|
| 300 | - | AtomRef::Atom { change, index } if change.is_zero() => AtomRef::Atom { change: me, index }, |
|
| 369 | + | AtomRef::Atom { change, index } if change.is_zero() => { |
|
| 370 | + | AtomRef::Atom { change: me, index } |
|
| 371 | + | } |
|
| 301 | 372 | other => other, |
|
| 302 | 373 | } |
|
| 303 | 374 | } |
|
| … | 20 unchanged lines |
||
| 324 | 395 | if a.len() != 3 { |
|
| 325 | 396 | return schema("atom range: expected [hash, from, to]"); |
|
| 326 | 397 | } |
|
| 327 | - | let r = AtomRange { change: oref(&a[0], "range.change")?, from: u32_(&a[1], "range.from")?, to: u32_(&a[2], "range.to")? }; |
|
| 398 | + | let r = AtomRange { |
|
| 399 | + | change: oref(&a[0], "range.change")?, |
|
| 400 | + | from: u32_(&a[1], "range.from")?, |
|
| 401 | + | to: u32_(&a[2], "range.to")?, |
|
| 402 | + | }; |
|
| 328 | 403 | if r.from >= r.to { |
|
| 329 | 404 | return schema("atom range: empty or inverted"); |
|
| 330 | 405 | } |
|
| 331 | 406 | Ok(r) |
|
| 332 | 407 | } |
|
| 333 | 408 | pub fn resolve(self, me: ChangeHash) -> AtomRange { |
|
| 334 | - | if self.change.is_zero() { AtomRange { change: me, ..self } } else { self } |
|
| 409 | + | if self.change.is_zero() { |
|
| 410 | + | AtomRange { change: me, ..self } |
|
| 411 | + | } else { |
|
| 412 | + | self |
|
| 413 | + | } |
|
| 335 | 414 | } |
|
| 336 | 415 | } |
|
| 337 | 416 | ||
| 338 | 417 | #[derive(Debug, Clone, PartialEq, Eq)] |
|
| 339 | 418 | pub enum Edit { |
|
| 340 | - | Insert { node: NodeId, after: AtomRef, before: AtomRef, lines: Vec<Vec<u8>> }, |
|
| 341 | - | Delete { node: NodeId, atoms: Vec<AtomRange> }, |
|
| 342 | - | Replace { node: NodeId, old: Option<ObjectRef>, new: Option<ObjectRef> }, |
|
| 419 | + | Insert { |
|
| 420 | + | node: NodeId, |
|
| 421 | + | after: AtomRef, |
|
| 422 | + | before: AtomRef, |
|
| 423 | + | lines: Vec<Vec<u8>>, |
|
| 424 | + | }, |
|
| 425 | + | Delete { |
|
| 426 | + | node: NodeId, |
|
| 427 | + | atoms: Vec<AtomRange>, |
|
| 428 | + | }, |
|
| 429 | + | Replace { |
|
| 430 | + | node: NodeId, |
|
| 431 | + | old: Option<ObjectRef>, |
|
| 432 | + | new: Option<ObjectRef>, |
|
| 433 | + | }, |
|
| 343 | 434 | } |
|
| 344 | 435 | ||
| 345 | 436 | impl Edit { |
|
| 346 | 437 | pub fn node(&self) -> NodeId { |
|
| 347 | 438 | match self { |
|
| 348 | - | Edit::Insert { node, .. } | Edit::Delete { node, .. } | Edit::Replace { node, .. } => *node, |
|
| 439 | + | Edit::Insert { node, .. } | Edit::Delete { node, .. } | Edit::Replace { node, .. } => { |
|
| 440 | + | *node |
|
| 441 | + | } |
|
| 349 | 442 | } |
|
| 350 | 443 | } |
|
| 351 | 444 | ||
| … | 4 unchanged lines |
||
| 356 | 449 | None => Value::Null, |
|
| 357 | 450 | }; |
|
| 358 | 451 | match self { |
|
| 359 | - | Edit::Insert { node, after, before, lines } => Value::map([ |
|
| 452 | + | Edit::Insert { |
|
| 453 | + | node, |
|
| 454 | + | after, |
|
| 455 | + | before, |
|
| 456 | + | lines, |
|
| 457 | + | } => Value::map([ |
|
| 360 | 458 | (0, Value::Uint(0)), |
|
| 361 | 459 | (1, nid(node)), |
|
| 362 | 460 | (2, after.to_value()), |
|
| 363 | 461 | (3, before.to_value()), |
|
| 364 | - | (4, Value::Array(lines.iter().map(|l| Value::Bytes(l.clone())).collect())), |
|
| 462 | + | ( |
|
| 463 | + | 4, |
|
| 464 | + | Value::Array(lines.iter().map(|l| Value::Bytes(l.clone())).collect()), |
|
| 465 | + | ), |
|
| 365 | 466 | ]), |
|
| 366 | 467 | Edit::Delete { node, atoms } => Value::map([ |
|
| 367 | 468 | (0, Value::Uint(1)), |
|
| 368 | 469 | (1, nid(node)), |
|
| 369 | - | (2, Value::Array(atoms.iter().map(|r| r.to_value()).collect())), |
|
| 470 | + | ( |
|
| 471 | + | 2, |
|
| 472 | + | Value::Array(atoms.iter().map(|r| r.to_value()).collect()), |
|
| 473 | + | ), |
|
| 370 | 474 | ]), |
|
| 371 | - | Edit::Replace { node, old, new } => { |
|
| 372 | - | Value::map([(0, Value::Uint(2)), (1, nid(node)), (2, opt(old)), (3, opt(new))]) |
|
| 373 | - | } |
|
| 475 | + | Edit::Replace { node, old, new } => Value::map([ |
|
| 476 | + | (0, Value::Uint(2)), |
|
| 477 | + | (1, nid(node)), |
|
| 478 | + | (2, opt(old)), |
|
| 479 | + | (3, opt(new)), |
|
| 480 | + | ]), |
|
| 374 | 481 | } |
|
| 375 | 482 | } |
|
| 376 | 483 | ||
| … | 2 unchanged lines |
||
| 379 | 486 | let tag = uint(field(m, 0, "edit tag")?, "edit tag")?; |
|
| 380 | 487 | let node = node_id(field(m, 1, "edit.node")?, "edit.node")?; |
|
| 381 | 488 | let opt = |v: &Value, what: &str| -> Result<Option<ObjectRef>, ObjectError> { |
|
| 382 | - | if v.is_null() { Ok(None) } else { Ok(Some(oref(v, what)?)) } |
|
| 489 | + | if v.is_null() { |
|
| 490 | + | Ok(None) |
|
| 491 | + | } else { |
|
| 492 | + | Ok(Some(oref(v, what)?)) |
|
| 493 | + | } |
|
| 383 | 494 | }; |
|
| 384 | 495 | Ok(match tag { |
|
| 385 | 496 | 0 => { |
|
| … | 21 unchanged lines |
||
| 407 | 518 | } |
|
| 408 | 519 | Edit::Delete { node, atoms } |
|
| 409 | 520 | } |
|
| 410 | - | 2 => Edit::Replace { node, old: opt(field(m, 2, "old")?, "old")?, new: opt(field(m, 3, "new")?, "new")? }, |
|
| 521 | + | 2 => Edit::Replace { |
|
| 522 | + | node, |
|
| 523 | + | old: opt(field(m, 2, "old")?, "old")?, |
|
| 524 | + | new: opt(field(m, 3, "new")?, "new")?, |
|
| 525 | + | }, |
|
| 411 | 526 | _ => return schema(format!("unknown edit tag {tag}")), |
|
| 412 | 527 | }) |
|
| 413 | 528 | } |
|
| … | 49 unchanged lines |
||
| 463 | 578 | Value::map([ |
|
| 464 | 579 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 465 | 580 | (1, Value::Bytes(self.change_id.0.to_vec())), |
|
| 466 | - | (2, Value::Array(self.deps.iter().map(|d| Value::Bytes(d.0.to_vec())).collect())), |
|
| 581 | + | ( |
|
| 582 | + | 2, |
|
| 583 | + | Value::Array( |
|
| 584 | + | self.deps |
|
| 585 | + | .iter() |
|
| 586 | + | .map(|d| Value::Bytes(d.0.to_vec())) |
|
| 587 | + | .collect(), |
|
| 588 | + | ), |
|
| 589 | + | ), |
|
| 467 | 590 | (3, Value::Bytes(self.author.0.to_vec())), |
|
| 468 | 591 | (4, self.hlc.to_value()), |
|
| 469 | 592 | (5, Value::Text(self.message.clone())), |
|
| 470 | - | (6, Value::Array(self.tree_ops.iter().map(|t| t.to_value()).collect())), |
|
| 471 | - | (7, Value::Array(self.edits.iter().map(|e| e.to_value()).collect())), |
|
| 593 | + | ( |
|
| 594 | + | 6, |
|
| 595 | + | Value::Array(self.tree_ops.iter().map(|t| t.to_value()).collect()), |
|
| 596 | + | ), |
|
| 597 | + | ( |
|
| 598 | + | 7, |
|
| 599 | + | Value::Array(self.edits.iter().map(|e| e.to_value()).collect()), |
|
| 600 | + | ), |
|
| 472 | 601 | (8, Value::Uint(self.kind.tag())), |
|
| 473 | 602 | (9, Value::Map(BTreeMap::new())), |
|
| 474 | 603 | ]) |
|
| 475 | 604 | } |
|
| 476 | 605 | ||
| 477 | 606 | pub fn encode(&self) -> Vec<u8> { |
|
| 478 | - | debug_assert!(self.deps.windows(2).all(|w| w[0] < w[1]), "deps must be normalized"); |
|
| 607 | + | debug_assert!( |
|
| 608 | + | self.deps.windows(2).all(|w| w[0] < w[1]), |
|
| 609 | + | "deps must be normalized" |
|
| 610 | + | ); |
|
| 479 | 611 | cbor::encode(&self.to_value()) |
|
| 480 | 612 | } |
|
| 481 | 613 | ||
| … | 58 unchanged lines |
||
| 540 | 672 | cbor::encode(&Value::map([ |
|
| 541 | 673 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 542 | 674 | (1, Value::Uint(self.length)), |
|
| 543 | - | (2, Value::Array(self.chunks.iter().map(|c| Value::Bytes(c.0.to_vec())).collect())), |
|
| 675 | + | ( |
|
| 676 | + | 2, |
|
| 677 | + | Value::Array( |
|
| 678 | + | self.chunks |
|
| 679 | + | .iter() |
|
| 680 | + | .map(|c| Value::Bytes(c.0.to_vec())) |
|
| 681 | + | .collect(), |
|
| 682 | + | ), |
|
| 683 | + | ), |
|
| 544 | 684 | ])) |
|
| 545 | 685 | } |
|
| 546 | 686 | pub fn hash(&self) -> ObjectRef { |
|
| … | 28 unchanged lines |
||
| 575 | 715 | pub fn encode(&self) -> Vec<u8> { |
|
| 576 | 716 | cbor::encode(&Value::map([ |
|
| 577 | 717 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 578 | - | (1, Value::Array(self.heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())), |
|
| 718 | + | ( |
|
| 719 | + | 1, |
|
| 720 | + | Value::Array( |
|
| 721 | + | self.heads |
|
| 722 | + | .iter() |
|
| 723 | + | .map(|h| Value::Bytes(h.0.to_vec())) |
|
| 724 | + | .collect(), |
|
| 725 | + | ), |
|
| 726 | + | ), |
|
| 579 | 727 | (2, Value::Uint(self.count)), |
|
| 580 | 728 | (3, Value::Bytes(self.payload.0.to_vec())), |
|
| 581 | 729 | ])) |
|
| … | 6 unchanged lines |
||
| 588 | 736 | let m = map(&v, "checkpoint")?; |
|
| 589 | 737 | check_version(m)?; |
|
| 590 | 738 | Ok(Checkpoint { |
|
| 591 | - | heads: array(field(m, 1, "heads")?, "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?, |
|
| 739 | + | heads: array(field(m, 1, "heads")?, "heads")? |
|
| 740 | + | .iter() |
|
| 741 | + | .map(|h| oref(h, "head")) |
|
| 742 | + | .collect::<Result<Vec<_>, _>>()?, |
|
| 592 | 743 | count: uint(field(m, 2, "count")?, "count")?, |
|
| 593 | 744 | payload: oref(field(m, 3, "payload")?, "payload")?, |
|
| 594 | 745 | }) |
|
| … | 18 unchanged lines |
||
| 613 | 764 | debug_assert!(self.heads.windows(2).all(|w| w[0] < w[1])); |
|
| 614 | 765 | cbor::encode(&Value::map([ |
|
| 615 | 766 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 616 | - | (1, Value::Array(self.heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())), |
|
| 767 | + | ( |
|
| 768 | + | 1, |
|
| 769 | + | Value::Array( |
|
| 770 | + | self.heads |
|
| 771 | + | .iter() |
|
| 772 | + | .map(|h| Value::Bytes(h.0.to_vec())) |
|
| 773 | + | .collect(), |
|
| 774 | + | ), |
|
| 775 | + | ), |
|
| 617 | 776 | ])) |
|
| 618 | 777 | } |
|
| 619 | 778 | pub fn hash(&self) -> ObjectRef { |
|
| … | 3 unchanged lines |
||
| 623 | 782 | let v = cbor::decode(bytes)?; |
|
| 624 | 783 | let m = map(&v, "state")?; |
|
| 625 | 784 | check_version(m)?; |
|
| 626 | - | let heads = array(field(m, 1, "heads")?, "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?; |
|
| 785 | + | let heads = array(field(m, 1, "heads")?, "heads")? |
|
| 786 | + | .iter() |
|
| 787 | + | .map(|h| oref(h, "head")) |
|
| 788 | + | .collect::<Result<Vec<_>, _>>()?; |
|
| 627 | 789 | if !heads.windows(2).all(|w| w[0] < w[1]) { |
|
| 628 | 790 | return schema("state heads must be sorted and unique"); |
|
| 629 | 791 | } |
|
| … | 12 unchanged lines |
||
| 642 | 804 | change_id: ChangeId([9; 16]), |
|
| 643 | 805 | deps: vec![ObjectRef([2; 32]), dep], |
|
| 644 | 806 | author: ObjectRef([3; 32]), |
|
| 645 | - | hlc: Hlc { wall_ms: 1_700_000_000_000, logical: 4 }, |
|
| 807 | + | hlc: Hlc { |
|
| 808 | + | wall_ms: 1_700_000_000_000, |
|
| 809 | + | logical: 4, |
|
| 810 | + | }, |
|
| 646 | 811 | message: "Add greeting".into(), |
|
| 647 | 812 | tree_ops: vec![ |
|
| 648 | - | TreeOp::AddNode { node: n, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "hello.txt".into() }, |
|
| 649 | - | TreeOp::SetMode { node: n, old: OpRef { change: ObjectRef::ZERO, index: 0 }, mode: Mode { exec: true, opaque: false } }, |
|
| 813 | + | TreeOp::AddNode { |
|
| 814 | + | node: n, |
|
| 815 | + | kind: NodeKind::File, |
|
| 816 | + | mode: Mode::TEXT, |
|
| 817 | + | parent: NodeId::ROOT, |
|
| 818 | + | name: "hello.txt".into(), |
|
| 819 | + | }, |
|
| 820 | + | TreeOp::SetMode { |
|
| 821 | + | node: n, |
|
| 822 | + | old: OpRef { |
|
| 823 | + | change: ObjectRef::ZERO, |
|
| 824 | + | index: 0, |
|
| 825 | + | }, |
|
| 826 | + | mode: Mode { |
|
| 827 | + | exec: true, |
|
| 828 | + | opaque: false, |
|
| 829 | + | }, |
|
| 830 | + | }, |
|
| 650 | 831 | ], |
|
| 651 | 832 | edits: vec![ |
|
| 652 | - | Edit::Insert { node: n, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"hello\n".to_vec(), b"world\n".to_vec()] }, |
|
| 653 | - | Edit::Delete { node: n, atoms: vec![AtomRange { change: dep, from: 0, to: 2 }] }, |
|
| 654 | - | Edit::Replace { node: n, old: None, new: Some(ObjectRef([5; 32])) }, |
|
| 833 | + | Edit::Insert { |
|
| 834 | + | node: n, |
|
| 835 | + | after: AtomRef::Start, |
|
| 836 | + | before: AtomRef::End, |
|
| 837 | + | lines: vec![b"hello\n".to_vec(), b"world\n".to_vec()], |
|
| 838 | + | }, |
|
| 839 | + | Edit::Delete { |
|
| 840 | + | node: n, |
|
| 841 | + | atoms: vec![AtomRange { |
|
| 842 | + | change: dep, |
|
| 843 | + | from: 0, |
|
| 844 | + | to: 2, |
|
| 845 | + | }], |
|
| 846 | + | }, |
|
| 847 | + | Edit::Replace { |
|
| 848 | + | node: n, |
|
| 849 | + | old: None, |
|
| 850 | + | new: Some(ObjectRef([5; 32])), |
|
| 851 | + | }, |
|
| 655 | 852 | ], |
|
| 656 | 853 | kind: ChangeKind::Normal, |
|
| 657 | 854 | }; |
|
| … | 22 unchanged lines |
||
| 680 | 877 | if let Value::Map(m) = &mut v { |
|
| 681 | 878 | m.insert(0, Value::Uint(2)); |
|
| 682 | 879 | } |
|
| 683 | - | assert!(matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("version"))); |
|
| 880 | + | assert!( |
|
| 881 | + | matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("version")) |
|
| 882 | + | ); |
|
| 684 | 883 | ||
| 685 | 884 | let mut v = c.to_value(); |
|
| 686 | 885 | if let Value::Map(m) = &mut v { |
|
| 687 | - | m.insert(2, Value::Array(vec![Value::Bytes(vec![2; 32]), Value::Bytes(vec![1; 32])])); |
|
| 886 | + | m.insert( |
|
| 887 | + | 2, |
|
| 888 | + | Value::Array(vec![Value::Bytes(vec![2; 32]), Value::Bytes(vec![1; 32])]), |
|
| 889 | + | ); |
|
| 688 | 890 | } |
|
| 689 | - | assert!(matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("sorted"))); |
|
| 891 | + | assert!( |
|
| 892 | + | matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("sorted")) |
|
| 893 | + | ); |
|
| 690 | 894 | ||
| 691 | 895 | let mut v = c.to_value(); |
|
| 692 | 896 | if let Value::Map(m) = &mut v { |
|
| 693 | 897 | m.insert(9, Value::map([(0, Value::Null)])); |
|
| 694 | 898 | } |
|
| 695 | - | assert!(matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("extra"))); |
|
| 899 | + | assert!( |
|
| 900 | + | matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("extra")) |
|
| 901 | + | ); |
|
| 696 | 902 | } |
|
| 697 | 903 | ||
| 698 | 904 | #[test] |
|
| … | 7 unchanged lines |
||
| 706 | 912 | ||
| 707 | 913 | #[test] |
|
| 708 | 914 | fn mode_and_kind_tags() { |
|
| 709 | - | for m in [Mode::TEXT, Mode::OPAQUE, Mode { exec: true, opaque: true }] { |
|
| 915 | + | for m in [ |
|
| 916 | + | Mode::TEXT, |
|
| 917 | + | Mode::OPAQUE, |
|
| 918 | + | Mode { |
|
| 919 | + | exec: true, |
|
| 920 | + | opaque: true, |
|
| 921 | + | }, |
|
| 922 | + | ] { |
|
| 710 | 923 | assert_eq!(Mode::from_bits(m.bits()).unwrap(), m); |
|
| 711 | 924 | } |
|
| 712 | 925 | assert!(Mode::from_bits(4).is_err()); |
|
| 713 | - | for k in [NodeKind::File, NodeKind::Dir, NodeKind::Symlink, NodeKind::Subrepo] { |
|
| 926 | + | for k in [ |
|
| 927 | + | NodeKind::File, |
|
| 928 | + | NodeKind::Dir, |
|
| 929 | + | NodeKind::Symlink, |
|
| 930 | + | NodeKind::Subrepo, |
|
| 931 | + | ] { |
|
| 714 | 932 | assert_eq!(NodeKind::from_tag(k.tag()).unwrap(), k); |
|
| 715 | 933 | } |
|
| 716 | 934 | } |
|
| … | 1 unchanged lines |
||
| 718 | 936 | #[test] |
|
| 719 | 937 | fn self_references_resolve() { |
|
| 720 | 938 | let me = ObjectRef([8; 32]); |
|
| 721 | - | assert_eq!(AtomRef::Atom { change: ObjectRef::ZERO, index: 3 }.resolve(me), AtomRef::Atom { change: me, index: 3 }); |
|
| 939 | + | assert_eq!( |
|
| 940 | + | AtomRef::Atom { |
|
| 941 | + | change: ObjectRef::ZERO, |
|
| 942 | + | index: 3 |
|
| 943 | + | } |
|
| 944 | + | .resolve(me), |
|
| 945 | + | AtomRef::Atom { |
|
| 946 | + | change: me, |
|
| 947 | + | index: 3 |
|
| 948 | + | } |
|
| 949 | + | ); |
|
| 722 | 950 | assert_eq!(AtomRef::Start.resolve(me), AtomRef::Start); |
|
| 723 | 951 | let other = ObjectRef([1; 32]); |
|
| 724 | - | assert_eq!(OpRef { change: other, index: 1 }.resolve(me).change, other); |
|
| 952 | + | assert_eq!( |
|
| 953 | + | OpRef { |
|
| 954 | + | change: other, |
|
| 955 | + | index: 1 |
|
| 956 | + | } |
|
| 957 | + | .resolve(me) |
|
| 958 | + | .change, |
|
| 959 | + | other |
|
| 960 | + | ); |
|
| 725 | 961 | } |
|
| 726 | 962 | ||
| 727 | 963 | #[test] |
|
| 728 | 964 | fn state_object_round_trip() { |
|
| 729 | - | let st = StateObject::new(vec![ObjectRef([2; 32]), ObjectRef([1; 32]), ObjectRef([2; 32])]); |
|
| 965 | + | let st = StateObject::new(vec![ |
|
| 966 | + | ObjectRef([2; 32]), |
|
| 967 | + | ObjectRef([1; 32]), |
|
| 968 | + | ObjectRef([2; 32]), |
|
| 969 | + | ]); |
|
| 730 | 970 | assert_eq!(st.heads, vec![ObjectRef([1; 32]), ObjectRef([2; 32])]); |
|
| 731 | 971 | assert_eq!(StateObject::decode(&st.encode()).unwrap(), st); |
|
| 732 | 972 | assert_eq!(StateObject::new(vec![]).heads, vec![]); |
|
| … | 2 unchanged lines |
||
| 735 | 975 | ||
| 736 | 976 | #[test] |
|
| 737 | 977 | fn blob_round_trip() { |
|
| 738 | - | let b = Blob { length: 10, chunks: vec![ObjectRef([1; 32]), ObjectRef([2; 32])] }; |
|
| 978 | + | let b = Blob { |
|
| 979 | + | length: 10, |
|
| 980 | + | chunks: vec![ObjectRef([1; 32]), ObjectRef([2; 32])], |
|
| 981 | + | }; |
|
| 739 | 982 | assert_eq!(Blob::decode(&b.encode()).unwrap(), b); |
|
| 740 | 983 | } |
|
| 741 | 984 | } |
modified crates/oxy-core/src/oplog.rs +147 −33
| … | 9 unchanged lines |
||
| 10 | 10 | use crate::cbor::{self, Value}; |
|
| 11 | 11 | use crate::hash::{hash_object, ChangeHash, ObjectRef, ObjectType}; |
|
| 12 | 12 | use crate::ids::WorkspaceId; |
|
| 13 | - | use crate::objects::{array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION}; |
|
| 13 | + | use crate::objects::{ |
|
| 14 | + | array, bytes, check_version, field, oref, text, Hlc, ObjectError, FORMAT_VERSION, |
|
| 15 | + | }; |
|
| 14 | 16 | use std::collections::BTreeMap; |
|
| 15 | 17 | ||
| 16 | 18 | #[derive(Clone, Debug, Default, PartialEq, Eq)] |
|
| … | 32 unchanged lines |
||
| 49 | 51 | Value::Array( |
|
| 50 | 52 | self.bookmarks |
|
| 51 | 53 | .iter() |
|
| 52 | - | .map(|(n, st)| Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])) |
|
| 54 | + | .map(|(n, st)| { |
|
| 55 | + | Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())]) |
|
| 56 | + | }) |
|
| 53 | 57 | .collect(), |
|
| 54 | 58 | ), |
|
| 55 | 59 | ), |
|
| … | 2 unchanged lines |
||
| 58 | 62 | Value::Array( |
|
| 59 | 63 | self.workspaces |
|
| 60 | 64 | .iter() |
|
| 61 | - | .map(|(w, h)| Value::Array(vec![Value::Bytes(w.0.to_vec()), Value::Bytes(h.0.to_vec())])) |
|
| 65 | + | .map(|(w, h)| { |
|
| 66 | + | Value::Array(vec![ |
|
| 67 | + | Value::Bytes(w.0.to_vec()), |
|
| 68 | + | Value::Bytes(h.0.to_vec()), |
|
| 69 | + | ]) |
|
| 70 | + | }) |
|
| 62 | 71 | .collect(), |
|
| 63 | 72 | ), |
|
| 64 | 73 | ), |
|
| … | 6 unchanged lines |
||
| 71 | 80 | Value::Array(vec![ |
|
| 72 | 81 | Value::Text(r.clone()), |
|
| 73 | 82 | Value::Array( |
|
| 74 | - | bms.iter().map(|(n, st)| Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])).collect(), |
|
| 83 | + | bms.iter() |
|
| 84 | + | .map(|(n, st)| { |
|
| 85 | + | Value::Array(vec![ |
|
| 86 | + | Value::Text(n.clone()), |
|
| 87 | + | Value::Bytes(st.0.to_vec()), |
|
| 88 | + | ]) |
|
| 89 | + | }) |
|
| 90 | + | .collect(), |
|
| 75 | 91 | ), |
|
| 76 | 92 | ]) |
|
| 77 | 93 | }) |
|
| 78 | 94 | .collect(), |
|
| 79 | 95 | ), |
|
| 80 | 96 | ), |
|
| 81 | - | (3, self.policy.map(|p| Value::Bytes(p.0.to_vec())).unwrap_or(Value::Null)), |
|
| 82 | - | (4, Value::Array(self.tags.iter().map(|(n, st)| Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())])).collect())), |
|
| 97 | + | ( |
|
| 98 | + | 3, |
|
| 99 | + | self.policy |
|
| 100 | + | .map(|p| Value::Bytes(p.0.to_vec())) |
|
| 101 | + | .unwrap_or(Value::Null), |
|
| 102 | + | ), |
|
| 103 | + | ( |
|
| 104 | + | 4, |
|
| 105 | + | Value::Array( |
|
| 106 | + | self.tags |
|
| 107 | + | .iter() |
|
| 108 | + | .map(|(n, st)| { |
|
| 109 | + | Value::Array(vec![Value::Text(n.clone()), Value::Bytes(st.0.to_vec())]) |
|
| 110 | + | }) |
|
| 111 | + | .collect(), |
|
| 112 | + | ), |
|
| 113 | + | ), |
|
| 83 | 114 | ]) |
|
| 84 | 115 | } |
|
| 85 | 116 | ||
| 86 | 117 | fn from_value(v: &Value) -> Result<View, ObjectError> { |
|
| 87 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("view: expected map".into()))?; |
|
| 118 | + | let m = v |
|
| 119 | + | .as_map() |
|
| 120 | + | .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?; |
|
| 88 | 121 | let mut view = View::default(); |
|
| 89 | 122 | for b in array(field(m, 0, "bookmarks")?, "bookmarks")? { |
|
| 90 | 123 | let pair = array(b, "bookmark")?; |
|
| 91 | 124 | if pair.len() != 2 { |
|
| 92 | - | return Err(ObjectError::Schema("bookmark: expected [name, heads]".into())); |
|
| 125 | + | return Err(ObjectError::Schema( |
|
| 126 | + | "bookmark: expected [name, heads]".into(), |
|
| 127 | + | )); |
|
| 93 | 128 | } |
|
| 94 | 129 | let name = text(&pair[0], "bookmark name")?.to_owned(); |
|
| 95 | - | view.bookmarks.insert(name, oref(&pair[1], "bookmark state")?); |
|
| 130 | + | view.bookmarks |
|
| 131 | + | .insert(name, oref(&pair[1], "bookmark state")?); |
|
| 96 | 132 | } |
|
| 97 | 133 | view.workspaces = workspaces_from_value(field(m, 1, "workspaces")?)?; |
|
| 98 | 134 | if let Some(rs) = m.get(&2) { |
|
| 99 | 135 | for r in array(rs, "remotes")? { |
|
| 100 | 136 | let pair = array(r, "remote")?; |
|
| 101 | 137 | if pair.len() != 2 { |
|
| 102 | - | return Err(ObjectError::Schema("remote: expected [name, bookmarks]".into())); |
|
| 138 | + | return Err(ObjectError::Schema( |
|
| 139 | + | "remote: expected [name, bookmarks]".into(), |
|
| 140 | + | )); |
|
| 103 | 141 | } |
|
| 104 | 142 | let name = text(&pair[0], "remote name")?.to_owned(); |
|
| 105 | 143 | let mut bms = BTreeMap::new(); |
|
| 106 | 144 | for b in array(&pair[1], "remote bookmarks")? { |
|
| 107 | 145 | let bp = array(b, "remote bookmark")?; |
|
| 108 | 146 | if bp.len() != 2 { |
|
| 109 | - | return Err(ObjectError::Schema("remote bookmark: expected [name, state]".into())); |
|
| 147 | + | return Err(ObjectError::Schema( |
|
| 148 | + | "remote bookmark: expected [name, state]".into(), |
|
| 149 | + | )); |
|
| 110 | 150 | } |
|
| 111 | - | bms.insert(text(&bp[0], "bookmark name")?.to_owned(), oref(&bp[1], "bookmark state")?); |
|
| 151 | + | bms.insert( |
|
| 152 | + | text(&bp[0], "bookmark name")?.to_owned(), |
|
| 153 | + | oref(&bp[1], "bookmark state")?, |
|
| 154 | + | ); |
|
| 112 | 155 | } |
|
| 113 | 156 | view.remotes.insert(name, bms); |
|
| 114 | 157 | } |
|
| 115 | 158 | } |
|
| 116 | 159 | if let Some(p) = m.get(&3) { |
|
| 117 | - | view.policy = if p.is_null() { None } else { Some(oref(p, "policy")?) }; |
|
| 160 | + | view.policy = if p.is_null() { |
|
| 161 | + | None |
|
| 162 | + | } else { |
|
| 163 | + | Some(oref(p, "policy")?) |
|
| 164 | + | }; |
|
| 118 | 165 | } |
|
| 119 | 166 | if let Some(ts) = m.get(&4) { |
|
| 120 | 167 | for t in array(ts, "tags")? { |
|
| … | 1 unchanged lines |
||
| 122 | 169 | if pair.len() != 2 { |
|
| 123 | 170 | return Err(ObjectError::Schema("tag: expected [name, state]".into())); |
|
| 124 | 171 | } |
|
| 125 | - | view.tags.insert(text(&pair[0], "tag name")?.to_owned(), oref(&pair[1], "tag state")?); |
|
| 172 | + | view.tags.insert( |
|
| 173 | + | text(&pair[0], "tag name")?.to_owned(), |
|
| 174 | + | oref(&pair[1], "tag state")?, |
|
| 175 | + | ); |
|
| 126 | 176 | } |
|
| 127 | 177 | } |
|
| 128 | 178 | Ok(view) |
|
| … | 5 unchanged lines |
||
| 134 | 184 | for w in array(v, "workspaces")? { |
|
| 135 | 185 | let pair = array(w, "workspace")?; |
|
| 136 | 186 | if pair.len() != 2 { |
|
| 137 | - | return Err(ObjectError::Schema("workspace: expected [id, change]".into())); |
|
| 187 | + | return Err(ObjectError::Schema( |
|
| 188 | + | "workspace: expected [id, change]".into(), |
|
| 189 | + | )); |
|
| 138 | 190 | } |
|
| 139 | 191 | let id = WorkspaceId::from_slice(bytes(&pair[0], "workspace id")?) |
|
| 140 | 192 | .ok_or_else(|| ObjectError::Schema("workspace id: expected 16 bytes".into()))?; |
|
| … | 4 unchanged lines |
||
| 145 | 197 | ||
| 146 | 198 | impl Format1View { |
|
| 147 | 199 | fn from_value(v: &Value) -> Result<Format1View, ObjectError> { |
|
| 148 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("view: expected map".into()))?; |
|
| 200 | + | let m = v |
|
| 201 | + | .as_map() |
|
| 202 | + | .ok_or_else(|| ObjectError::Schema("view: expected map".into()))?; |
|
| 149 | 203 | let mut view = Format1View::default(); |
|
| 150 | 204 | for b in array(field(m, 0, "bookmarks")?, "bookmarks")? { |
|
| 151 | 205 | let pair = array(b, "bookmark")?; |
|
| 152 | 206 | if pair.len() != 2 { |
|
| 153 | - | return Err(ObjectError::Schema("bookmark: expected [name, heads]".into())); |
|
| 207 | + | return Err(ObjectError::Schema( |
|
| 208 | + | "bookmark: expected [name, heads]".into(), |
|
| 209 | + | )); |
|
| 154 | 210 | } |
|
| 155 | 211 | let name = text(&pair[0], "bookmark name")?.to_owned(); |
|
| 156 | - | let mut heads = array(&pair[1], "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?; |
|
| 212 | + | let mut heads = array(&pair[1], "heads")? |
|
| 213 | + | .iter() |
|
| 214 | + | .map(|h| oref(h, "head")) |
|
| 215 | + | .collect::<Result<Vec<_>, _>>()?; |
|
| 157 | 216 | heads.sort(); |
|
| 158 | 217 | heads.dedup(); |
|
| 159 | 218 | view.bookmarks.insert(name, heads); |
|
| … | 14 unchanged lines |
||
| 174 | 233 | .map(|(n, hs)| { |
|
| 175 | 234 | Value::Array(vec![ |
|
| 176 | 235 | Value::Text(n.clone()), |
|
| 177 | - | Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()), |
|
| 236 | + | Value::Array( |
|
| 237 | + | hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect(), |
|
| 238 | + | ), |
|
| 178 | 239 | ]) |
|
| 179 | 240 | }) |
|
| 180 | 241 | .collect(), |
|
| … | 4 unchanged lines |
||
| 185 | 246 | Value::Array( |
|
| 186 | 247 | self.workspaces |
|
| 187 | 248 | .iter() |
|
| 188 | - | .map(|(w, h)| Value::Array(vec![Value::Bytes(w.0.to_vec()), Value::Bytes(h.0.to_vec())])) |
|
| 249 | + | .map(|(w, h)| { |
|
| 250 | + | Value::Array(vec![ |
|
| 251 | + | Value::Bytes(w.0.to_vec()), |
|
| 252 | + | Value::Bytes(h.0.to_vec()), |
|
| 253 | + | ]) |
|
| 254 | + | }) |
|
| 189 | 255 | .collect(), |
|
| 190 | 256 | ), |
|
| 191 | 257 | ), |
|
| … | 15 unchanged lines |
||
| 207 | 273 | pub(crate) fn encode(&self) -> Vec<u8> { |
|
| 208 | 274 | cbor::encode(&Value::map([ |
|
| 209 | 275 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 210 | - | (1, Value::Array(self.parents.iter().map(|p| Value::Bytes(p.0.to_vec())).collect())), |
|
| 276 | + | ( |
|
| 277 | + | 1, |
|
| 278 | + | Value::Array( |
|
| 279 | + | self.parents |
|
| 280 | + | .iter() |
|
| 281 | + | .map(|p| Value::Bytes(p.0.to_vec())) |
|
| 282 | + | .collect(), |
|
| 283 | + | ), |
|
| 284 | + | ), |
|
| 211 | 285 | (2, self.hlc.to_value()), |
|
| 212 | 286 | (3, Value::Text(self.command.clone())), |
|
| 213 | 287 | (4, self.view.to_value()), |
|
| … | 14 unchanged lines |
||
| 228 | 302 | pub fn to_value(&self) -> Value { |
|
| 229 | 303 | Value::map([ |
|
| 230 | 304 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 231 | - | (1, Value::Array(self.parents.iter().map(|p| Value::Bytes(p.0.to_vec())).collect())), |
|
| 305 | + | ( |
|
| 306 | + | 1, |
|
| 307 | + | Value::Array( |
|
| 308 | + | self.parents |
|
| 309 | + | .iter() |
|
| 310 | + | .map(|p| Value::Bytes(p.0.to_vec())) |
|
| 311 | + | .collect(), |
|
| 312 | + | ), |
|
| 313 | + | ), |
|
| 232 | 314 | (2, self.hlc.to_value()), |
|
| 233 | 315 | (3, Value::Text(self.command.clone())), |
|
| 234 | 316 | (4, self.view.to_value()), |
|
| … | 7 unchanged lines |
||
| 242 | 324 | } |
|
| 243 | 325 | pub fn decode(b: &[u8]) -> Result<Op, ObjectError> { |
|
| 244 | 326 | let v = cbor::decode(b)?; |
|
| 245 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("op: expected map".into()))?; |
|
| 327 | + | let m = v |
|
| 328 | + | .as_map() |
|
| 329 | + | .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?; |
|
| 246 | 330 | check_version(m)?; |
|
| 247 | 331 | Ok(Op { |
|
| 248 | - | parents: array(field(m, 1, "parents")?, "parents")?.iter().map(|p| oref(p, "parent")).collect::<Result<Vec<_>, _>>()?, |
|
| 332 | + | parents: array(field(m, 1, "parents")?, "parents")? |
|
| 333 | + | .iter() |
|
| 334 | + | .map(|p| oref(p, "parent")) |
|
| 335 | + | .collect::<Result<Vec<_>, _>>()?, |
|
| 249 | 336 | hlc: Hlc::from_value(field(m, 2, "hlc")?)?, |
|
| 250 | 337 | command: text(field(m, 3, "command")?, "command")?.to_owned(), |
|
| 251 | 338 | view: View::from_value(field(m, 4, "view")?)?, |
|
| … | 3 unchanged lines |
||
| 255 | 342 | /// Read an operation written by repository format 1. |
|
| 256 | 343 | pub fn decode_format1(b: &[u8]) -> Result<Format1Op, ObjectError> { |
|
| 257 | 344 | let v = cbor::decode(b)?; |
|
| 258 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("op: expected map".into()))?; |
|
| 345 | + | let m = v |
|
| 346 | + | .as_map() |
|
| 347 | + | .ok_or_else(|| ObjectError::Schema("op: expected map".into()))?; |
|
| 259 | 348 | check_version(m)?; |
|
| 260 | 349 | Ok(Format1Op { |
|
| 261 | - | parents: array(field(m, 1, "parents")?, "parents")?.iter().map(|p| oref(p, "parent")).collect::<Result<Vec<_>, _>>()?, |
|
| 350 | + | parents: array(field(m, 1, "parents")?, "parents")? |
|
| 351 | + | .iter() |
|
| 352 | + | .map(|p| oref(p, "parent")) |
|
| 353 | + | .collect::<Result<Vec<_>, _>>()?, |
|
| 262 | 354 | hlc: Hlc::from_value(field(m, 2, "hlc")?)?, |
|
| 263 | 355 | command: text(field(m, 3, "command")?, "command")?.to_owned(), |
|
| 264 | 356 | view: Format1View::from_value(field(m, 4, "view")?)?, |
|
| … | 10 unchanged lines |
||
| 275 | 367 | let mut view = View::default(); |
|
| 276 | 368 | view.bookmarks.insert("main".into(), ObjectRef([2; 32])); |
|
| 277 | 369 | view.bookmarks.insert("dev".into(), ObjectRef([3; 32])); |
|
| 278 | - | view.workspaces.insert(WorkspaceId([5; 16]), ObjectRef([9; 32])); |
|
| 279 | - | view.remotes.entry("origin".into()).or_default().insert("main".into(), ObjectRef([4; 32])); |
|
| 370 | + | view.workspaces |
|
| 371 | + | .insert(WorkspaceId([5; 16]), ObjectRef([9; 32])); |
|
| 372 | + | view.remotes |
|
| 373 | + | .entry("origin".into()) |
|
| 374 | + | .or_default() |
|
| 375 | + | .insert("main".into(), ObjectRef([4; 32])); |
|
| 280 | 376 | view.policy = Some(ObjectRef([6; 32])); |
|
| 281 | 377 | view.tags.insert("v1".into(), ObjectRef([8; 32])); |
|
| 282 | - | let op = Op { parents: vec![ObjectRef([7; 32])], hlc: Hlc { wall_ms: 5, logical: 1 }, command: "oxy commit".into(), view }; |
|
| 378 | + | let op = Op { |
|
| 379 | + | parents: vec![ObjectRef([7; 32])], |
|
| 380 | + | hlc: Hlc { |
|
| 381 | + | wall_ms: 5, |
|
| 382 | + | logical: 1, |
|
| 383 | + | }, |
|
| 384 | + | command: "oxy commit".into(), |
|
| 385 | + | view, |
|
| 386 | + | }; |
|
| 283 | 387 | let d = Op::decode(&op.encode()).unwrap(); |
|
| 284 | 388 | assert_eq!(d, op); |
|
| 285 | 389 | assert_eq!(d.hash(), Op::decode(&d.encode()).unwrap().hash()); |
|
| 286 | 390 | // A format-1 op does not decode as format 2, but does through the migration reader. |
|
| 287 | 391 | let mut v1 = Format1View::default(); |
|
| 288 | - | v1.bookmarks.insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]); |
|
| 289 | - | v1.workspaces.insert(WorkspaceId([5; 16]), ObjectRef([9; 32])); |
|
| 290 | - | let old = Format1Op { parents: vec![], hlc: Hlc::default(), command: "oxy init".into(), view: v1 }; |
|
| 392 | + | v1.bookmarks |
|
| 393 | + | .insert("main".into(), vec![ObjectRef([2; 32]), ObjectRef([1; 32])]); |
|
| 394 | + | v1.workspaces |
|
| 395 | + | .insert(WorkspaceId([5; 16]), ObjectRef([9; 32])); |
|
| 396 | + | let old = Format1Op { |
|
| 397 | + | parents: vec![], |
|
| 398 | + | hlc: Hlc::default(), |
|
| 399 | + | command: "oxy init".into(), |
|
| 400 | + | view: v1, |
|
| 401 | + | }; |
|
| 291 | 402 | assert!(Op::decode(&old.encode()).is_err()); |
|
| 292 | 403 | let back = Op::decode_format1(&old.encode()).unwrap(); |
|
| 293 | - | assert_eq!(back.view.bookmarks["main"], vec![ObjectRef([1; 32]), ObjectRef([2; 32])]); |
|
| 404 | + | assert_eq!( |
|
| 405 | + | back.view.bookmarks["main"], |
|
| 406 | + | vec![ObjectRef([1; 32]), ObjectRef([2; 32])] |
|
| 407 | + | ); |
|
| 294 | 408 | } |
|
| 295 | 409 | } |
modified crates/oxy-core/src/policy.rs +97 −20
| … | 8 unchanged lines |
||
| 9 | 9 | use crate::cbor::{self, Value}; |
|
| 10 | 10 | use crate::hash::{hash_object, KeyId, ObjectRef, ObjectType}; |
|
| 11 | 11 | use crate::identity::{self, Domain, Identity, Signature}; |
|
| 12 | - | use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, ObjectError, FORMAT_VERSION}; |
|
| 12 | + | use crate::objects::{ |
|
| 13 | + | array, bytes, check_version, field, map, oref, text, uint, ObjectError, FORMAT_VERSION, |
|
| 14 | + | }; |
|
| 13 | 15 | use std::collections::BTreeMap; |
|
| 14 | 16 | ||
| 15 | 17 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
| … | 68 unchanged lines |
||
| 84 | 86 | Value::Array(keys.iter().map(|k| Value::Bytes(k.0.to_vec())).collect()) |
|
| 85 | 87 | } |
|
| 86 | 88 | fn keys_from(v: &Value, what: &str) -> Result<Vec<KeyId>, ObjectError> { |
|
| 87 | - | let mut ks = array(v, what)?.iter().map(|k| oref(k, what)).collect::<Result<Vec<_>, _>>()?; |
|
| 89 | + | let mut ks = array(v, what)? |
|
| 90 | + | .iter() |
|
| 91 | + | .map(|k| oref(k, what)) |
|
| 92 | + | .collect::<Result<Vec<_>, _>>()?; |
|
| 88 | 93 | ks.sort(); |
|
| 89 | 94 | ks.dedup(); |
|
| 90 | 95 | Ok(ks) |
|
| … | 4 unchanged lines |
||
| 95 | 100 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 96 | 101 | (1, Value::Bytes(self.repo.to_vec())), |
|
| 97 | 102 | (2, Value::Uint(self.serial)), |
|
| 98 | - | (3, Value::map([(0, Self::keys_value(&self.roots)), (1, Value::Uint(self.root_threshold as u64))])), |
|
| 103 | + | 3, |
|
| 104 | + | Value::map([ |
|
| 105 | + | (0, Self::keys_value(&self.roots)), |
|
| 106 | + | (1, Value::Uint(self.root_threshold as u64)), |
|
| 107 | + | ]), |
|
| 108 | + | ), |
|
| 109 | + | ( |
|
| 99 | 110 | ( |
|
| 100 | 111 | 4, |
|
| 101 | 112 | Value::Array( |
|
| … | 11 unchanged lines |
||
| 113 | 124 | .collect(), |
|
| 114 | 125 | ), |
|
| 115 | 126 | ), |
|
| 116 | - | (5, Value::Array(self.identities.iter().map(|i| Value::Bytes(i.encode())).collect())), |
|
| 127 | + | ( |
|
| 128 | + | 5, |
|
| 129 | + | Value::Array( |
|
| 130 | + | self.identities |
|
| 131 | + | .iter() |
|
| 132 | + | .map(|i| Value::Bytes(i.encode())) |
|
| 133 | + | .collect(), |
|
| 134 | + | ), |
|
| 135 | + | ), |
|
| 117 | 136 | (6, Self::keys_value(&self.revoked)), |
|
| 118 | 137 | (7, Value::Uint(self.expires)), |
|
| 119 | 138 | ]) |
|
| … | 20 unchanged lines |
||
| 140 | 159 | keys: Self::keys_from(field(rm, 1, "rule keys")?, "rule keys")?, |
|
| 141 | 160 | threshold: uint(field(rm, 2, "threshold")?, "threshold")? as u32, |
|
| 142 | 161 | require: Require::from_tag(uint(field(rm, 3, "require")?, "require")?)?, |
|
| 143 | - | allow_conflicts: field(rm, 4, "allow_conflicts")?.as_bool().ok_or_else(|| ObjectError::Schema("allow_conflicts: expected bool".into()))?, |
|
| 162 | + | allow_conflicts: field(rm, 4, "allow_conflicts")? |
|
| 163 | + | .as_bool() |
|
| 164 | + | .ok_or_else(|| ObjectError::Schema("allow_conflicts: expected bool".into()))?, |
|
| 144 | 165 | }); |
|
| 145 | 166 | } |
|
| 146 | 167 | let identities = array(field(m, 5, "identities")?, "identities")? |
|
| … | 1 unchanged lines |
||
| 148 | 169 | .map(|i| Identity::decode(bytes(i, "identity")?)) |
|
| 149 | 170 | .collect::<Result<Vec<_>, _>>()?; |
|
| 150 | 171 | let p = Policy { |
|
| 151 | - | repo: bytes(field(m, 1, "repo")?, "repo")?.try_into().map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?, |
|
| 172 | + | repo: bytes(field(m, 1, "repo")?, "repo")? |
|
| 173 | + | .try_into() |
|
| 174 | + | .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?, |
|
| 152 | 175 | serial: uint(field(m, 2, "serial")?, "serial")?, |
|
| 153 | 176 | roots: Self::keys_from(field(roots, 0, "root keys")?, "root keys")?, |
|
| 154 | 177 | root_threshold: uint(field(roots, 1, "root threshold")?, "root threshold")? as u32, |
|
| … | 3 unchanged lines |
||
| 158 | 181 | expires: uint(field(m, 7, "expires")?, "expires")?, |
|
| 159 | 182 | }; |
|
| 160 | 183 | if p.root_threshold == 0 || p.root_threshold as usize > p.roots.len() { |
|
| 161 | - | return Err(ObjectError::Schema("root threshold must be between 1 and the number of root keys".into())); |
|
| 184 | + | return Err(ObjectError::Schema( |
|
| 185 | + | "root threshold must be between 1 and the number of root keys".into(), |
|
| 186 | + | )); |
|
| 162 | 187 | } |
|
| 163 | 188 | for r in &p.rules { |
|
| 164 | 189 | if r.threshold == 0 || r.threshold as usize > r.keys.len() { |
|
| 165 | - | return Err(ObjectError::Schema(format!("rule {:?}: threshold must be between 1 and the number of keys", r.pattern))); |
|
| 190 | + | return Err(ObjectError::Schema(format!( |
|
| 191 | + | "rule {:?}: threshold must be between 1 and the number of keys", |
|
| 192 | + | r.pattern |
|
| 193 | + | ))); |
|
| 166 | 194 | } |
|
| 167 | 195 | } |
|
| 168 | 196 | Ok(p) |
|
| … | 13 unchanged lines |
||
| 182 | 210 | ||
| 183 | 211 | /// The first rule whose pattern matches `reference`. |
|
| 184 | 212 | pub fn rule_for(&self, reference: &str) -> Option<&Rule> { |
|
| 185 | - | self.rules.iter().find(|r| glob_matches(&r.pattern, reference)) |
|
| 213 | + | self.rules |
|
| 214 | + | .iter() |
|
| 215 | + | .find(|r| glob_matches(&r.pattern, reference)) |
|
| 186 | 216 | } |
|
| 187 | 217 | ||
| 188 | 218 | /// Whether `key` may update `reference`: listed in the matching rule and not revoked. |
|
| 189 | 219 | pub fn authorizes(&self, key: &KeyId, reference: &str) -> bool { |
|
| 190 | - | !self.is_revoked(key) && self.rule_for(reference).is_some_and(|r| r.keys.contains(key)) |
|
| 220 | + | !self.is_revoked(key) |
|
| 221 | + | && self |
|
| 222 | + | .rule_for(reference) |
|
| 223 | + | .is_some_and(|r| r.keys.contains(key)) |
|
| 191 | 224 | } |
|
| 192 | 225 | ||
| 193 | 226 | /// Verify that `sigs` over this policy's hash meet `trusted`'s root |
|
| 194 | 227 | /// threshold (`trusted` is the previous policy, or this one when |
|
| 195 | 228 | /// bootstrapping). Returns the signing root keys. |
|
| 196 | - | pub fn verify_signatures(&self, sigs: &[Signature], trusted: &Policy) -> Result<Vec<KeyId>, String> { |
|
| 229 | + | pub fn verify_signatures( |
|
| 230 | + | &self, |
|
| 231 | + | sigs: &[Signature], |
|
| 232 | + | trusted: &Policy, |
|
| 233 | + | ) -> Result<Vec<KeyId>, String> { |
|
| 197 | 234 | let target = self.hash(); |
|
| 198 | 235 | let mut signers = Vec::new(); |
|
| 199 | 236 | for sig in sigs { |
|
| 200 | 237 | if !trusted.roots.contains(&sig.key) || trusted.is_revoked(&sig.key) { |
|
| 201 | 238 | continue; |
|
| 202 | 239 | } |
|
| 203 | - | let Some(id) = trusted.identity(&sig.key).or_else(|| self.identity(&sig.key)) else { continue }; |
|
| 204 | - | if identity::verify(id, sig, Domain::Policy, &target).is_ok() && !signers.contains(&sig.key) { |
|
| 240 | + | let Some(id) = trusted |
|
| 241 | + | .identity(&sig.key) |
|
| 242 | + | .or_else(|| self.identity(&sig.key)) |
|
| 243 | + | else { |
|
| 244 | + | continue; |
|
| 245 | + | }; |
|
| 246 | + | if identity::verify(id, sig, Domain::Policy, &target).is_ok() |
|
| 247 | + | && !signers.contains(&sig.key) |
|
| 248 | + | { |
|
| 205 | 249 | signers.push(sig.key); |
|
| 206 | 250 | } |
|
| 207 | 251 | } |
|
| 208 | 252 | if (signers.len() as u32) < trusted.root_threshold { |
|
| 209 | - | return Err(format!("policy needs {} root signature(s), has {}", trusted.root_threshold, signers.len())); |
|
| 253 | + | return Err(format!( |
|
| 254 | + | "policy needs {} root signature(s), has {}", |
|
| 255 | + | trusted.root_threshold, |
|
| 256 | + | signers.len() |
|
| 257 | + | )); |
|
| 210 | 258 | } |
|
| 211 | 259 | Ok(signers) |
|
| 212 | 260 | } |
|
| … | 25 unchanged lines |
||
| 238 | 286 | serial: 1, |
|
| 239 | 287 | roots: vec![key], |
|
| 240 | 288 | root_threshold: 1, |
|
| 241 | - | rules: vec![Rule { pattern: "*".into(), keys: vec![key], threshold: 1, require: Require::Any, allow_conflicts: true }], |
|
| 289 | + | rules: vec![Rule { |
|
| 290 | + | pattern: "*".into(), |
|
| 291 | + | keys: vec![key], |
|
| 292 | + | threshold: 1, |
|
| 293 | + | require: Require::Any, |
|
| 294 | + | allow_conflicts: true, |
|
| 295 | + | }], |
|
| 242 | 296 | identities: vec![root], |
|
| 243 | 297 | revoked: vec![], |
|
| 244 | 298 | expires, |
|
| … | 2 unchanged lines |
||
| 247 | 301 | ||
| 248 | 302 | /// Whether `key` appears anywhere in the policy (roots, rules, identities). |
|
| 249 | 303 | pub fn mentions(&self, key: &KeyId) -> bool { |
|
| 250 | - | self.roots.contains(key) || self.rules.iter().any(|r| r.keys.contains(key)) || self.identity(key).is_some() |
|
| 304 | + | self.roots.contains(key) |
|
| 305 | + | || self.rules.iter().any(|r| r.keys.contains(key)) |
|
| 306 | + | || self.identity(key).is_some() |
|
| 251 | 307 | } |
|
| 252 | 308 | ||
| 253 | 309 | /// Whether `key` is `rule_key` or one of its predecessors in the |
|
| … | 20 unchanged lines |
||
| 274 | 330 | /// not revoked. |
|
| 275 | 331 | pub fn substitute_key(&mut self, old: &KeyId, new: Identity) { |
|
| 276 | 332 | let nk = new.hash(); |
|
| 277 | - | for k in self.roots.iter_mut().chain(self.rules.iter_mut().flat_map(|r| r.keys.iter_mut())) { |
|
| 333 | + | for k in self |
|
| 334 | + | .roots |
|
| 335 | + | .iter_mut() |
|
| 336 | + | .chain(self.rules.iter_mut().flat_map(|r| r.keys.iter_mut())) |
|
| 337 | + | { |
|
| 278 | 338 | if k == old { |
|
| 279 | 339 | *k = nk; |
|
| 280 | 340 | } |
|
| … | 21 unchanged lines |
||
| 302 | 362 | use crate::identity::SecretKey; |
|
| 303 | 363 | ||
| 304 | 364 | fn sk(n: u8) -> SecretKey { |
|
| 305 | - | SecretKey { ed_seed: [n; 32], ml_seed: [n + 100; 32] } |
|
| 365 | + | SecretKey { |
|
| 366 | + | ed_seed: [n; 32], |
|
| 367 | + | ml_seed: [n + 100; 32], |
|
| 368 | + | } |
|
| 306 | 369 | } |
|
| 307 | 370 | ||
| 308 | 371 | #[test] |
|
| … | 12 unchanged lines |
||
| 321 | 384 | let root = sk(1).identity("Root", "r@x", 0); |
|
| 322 | 385 | let dev = sk(2).identity("Dev", "d@x", 0); |
|
| 323 | 386 | let mut p = Policy::single_root([9; 16], root.clone(), 10); |
|
| 324 | - | p.rules.insert(0, Rule { pattern: "release/*".into(), keys: vec![root.hash()], threshold: 1, require: Require::Signed, allow_conflicts: false }); |
|
| 387 | + | p.rules.insert( |
|
| 388 | + | 0, |
|
| 389 | + | Rule { |
|
| 390 | + | pattern: "release/*".into(), |
|
| 391 | + | keys: vec![root.hash()], |
|
| 392 | + | threshold: 1, |
|
| 393 | + | require: Require::Signed, |
|
| 394 | + | allow_conflicts: false, |
|
| 395 | + | }, |
|
| 396 | + | ); |
|
| 325 | 397 | p.rules[1].keys.push(dev.hash()); |
|
| 326 | 398 | p.identities.push(dev.clone()); |
|
| 327 | 399 | p.revoked.push(ObjectRef([7; 32])); |
|
| … | 27 unchanged lines |
||
| 355 | 427 | let s1 = r1.sign(Domain::Policy, &i1.hash(), &h); |
|
| 356 | 428 | let s2 = r2.sign(Domain::Policy, &i2.hash(), &h); |
|
| 357 | 429 | assert!(p.verify_signatures(std::slice::from_ref(&s1), &p).is_err()); |
|
| 358 | - | assert_eq!(p.verify_signatures(&[s1.clone(), s2.clone()], &p).unwrap().len(), 2); |
|
| 430 | + | assert_eq!( |
|
| 431 | + | p.verify_signatures(&[s1.clone(), s2.clone()], &p) |
|
| 432 | + | .unwrap() |
|
| 433 | + | .len(), |
|
| 434 | + | 2 |
|
| 435 | + | ); |
|
| 359 | 436 | // Duplicates do not count twice; a non-root signer does not count. |
|
| 360 | 437 | assert!(p.verify_signatures(&[s1.clone(), s1.clone()], &p).is_err()); |
|
| 361 | 438 | let stranger = sk(3); |
|
| … | 13 unchanged lines |
modified crates/oxy-core/src/protocol.rs +353 −73
| … | 38 unchanged lines |
||
| 39 | 39 | crate::cbor::encode(&wire::policy_value(self)) |
|
| 40 | 40 | } |
|
| 41 | 41 | pub fn decode(b: &[u8]) -> Result<SignedPolicy> { |
|
| 42 | - | wire::policy_from(&crate::cbor::decode(b).map_err(|e| RepoError::Object(ObjectError::from(e)))?) |
|
| 42 | + | wire::policy_from( |
|
| 43 | + | &crate::cbor::decode(b).map_err(|e| RepoError::Object(ObjectError::from(e)))?, |
|
| 44 | + | ) |
|
| 43 | 45 | } |
|
| 44 | 46 | } |
|
| 45 | 47 | ||
| … | 82 unchanged lines |
||
| 128 | 130 | /// Push changes, bookmark updates, new tags, and optionally a newer |
|
| 129 | 131 | /// policy. The policy is applied first; a result named `policy` reports |
|
| 130 | 132 | /// it, and each tag reports as `tags/<name>`. |
|
| 131 | - | fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>, tags: Vec<TagRef>) -> Result<Vec<RefResult>>; |
|
| 133 | + | fn push( |
|
| 134 | + | &self, |
|
| 135 | + | bundle: Bundle, |
|
| 136 | + | updates: Vec<RefUpdate>, |
|
| 137 | + | policy: Option<SignedPolicy>, |
|
| 138 | + | tags: Vec<TagRef>, |
|
| 139 | + | ) -> Result<Vec<RefResult>>; |
|
| 132 | 140 | } |
|
| 133 | 141 | ||
| 134 | 142 | // ---------------------------------------------------------------- local |
|
| … | 5 unchanged lines |
||
| 140 | 148 | ||
| 141 | 149 | impl LocalRemote { |
|
| 142 | 150 | pub fn open(path: &Path) -> Result<LocalRemote> { |
|
| 143 | - | Ok(LocalRemote { repo: Repo::open(path)? }) |
|
| 151 | + | Ok(LocalRemote { |
|
| 152 | + | repo: Repo::open(path)?, |
|
| 153 | + | }) |
|
| 144 | 154 | } |
|
| 145 | 155 | } |
|
| 146 | 156 | ||
| … | 19 unchanged lines |
||
| 166 | 176 | fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> { |
|
| 167 | 177 | self.repo.shallow_changes(heads, depth) |
|
| 168 | 178 | } |
|
| 169 | - | fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>, tags: Vec<TagRef>) -> Result<Vec<RefResult>> { |
|
| 179 | + | fn push( |
|
| 180 | + | &self, |
|
| 181 | + | bundle: Bundle, |
|
| 182 | + | updates: Vec<RefUpdate>, |
|
| 183 | + | policy: Option<SignedPolicy>, |
|
| 184 | + | tags: Vec<TagRef>, |
|
| 185 | + | ) -> Result<Vec<RefResult>> { |
|
| 170 | 186 | self.repo.receive_push(bundle, updates, policy, tags) |
|
| 171 | 187 | } |
|
| 172 | 188 | } |
|
| … | 8 unchanged lines |
||
| 181 | 197 | /// PEM file with extra root certificates to trust for `https://` remotes |
|
| 182 | 198 | /// (self-hosted servers); the system web roots are always trusted. |
|
| 183 | 199 | pub ca: Option<PathBuf>, |
|
| 200 | + | /// The local repository's id, sent as `Oxy-Repo-Id` so an unbound |
|
| 201 | + | /// hosted repository can adopt it on the first push (spec §16.1). |
|
| 202 | + | pub repo_id: Option<[u8; 16]>, |
|
| 184 | 203 | } |
|
| 185 | 204 | ||
| 205 | + | /// The request header carrying the client's repository id. |
|
| 206 | + | pub const REPO_ID_HEADER: &str = "Oxy-Repo-Id"; |
|
| 207 | + | ||
| 208 | + | /// The id of a hosted repository that has not adopted one yet. |
|
| 209 | + | pub const UNBOUND_ID: [u8; 16] = [0; 16]; |
|
| 210 | + | ||
| 186 | 211 | /// A remote served by `oxy-server` at `<base>/oxy/v1/...`. |
|
| 187 | 212 | pub struct HttpRemote { |
|
| 188 | 213 | base: String, |
|
| 189 | 214 | token: Option<String>, |
|
| 215 | + | repo_id: Option<[u8; 16]>, |
|
| 190 | 216 | agent: ureq::Agent, |
|
| 191 | 217 | } |
|
| 192 | 218 | ||
| … | 2 unchanged lines |
||
| 195 | 221 | let mut roots = rustls::RootCertStore::empty(); |
|
| 196 | 222 | roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); |
|
| 197 | 223 | if let Some(path) = ca { |
|
| 198 | - | let pem = std::fs::read(path).map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?; |
|
| 224 | + | let pem = std::fs::read(path) |
|
| 225 | + | .map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?; |
|
| 199 | 226 | let certs = rustls_pemfile::certs(&mut pem.as_slice()) |
|
| 200 | 227 | .collect::<std::result::Result<Vec<_>, _>>() |
|
| 201 | 228 | .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?; |
|
| 202 | 229 | if certs.is_empty() { |
|
| 203 | - | return Err(RepoError::Msg(format!("CA file {} holds no certificates", path.display()))); |
|
| 230 | + | return Err(RepoError::Msg(format!( |
|
| 231 | + | "CA file {} holds no certificates", |
|
| 232 | + | path.display() |
|
| 233 | + | ))); |
|
| 204 | 234 | } |
|
| 205 | 235 | for c in certs { |
|
| 206 | - | roots.add(c).map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?; |
|
| 236 | + | roots |
|
| 237 | + | .add(c) |
|
| 238 | + | .map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?; |
|
| 207 | 239 | } |
|
| 208 | 240 | } |
|
| 209 | - | let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) |
|
| 210 | - | .with_safe_default_protocol_versions() |
|
| 211 | - | .map_err(|e| RepoError::Msg(e.to_string()))? |
|
| 212 | - | .with_root_certificates(roots) |
|
| 213 | - | .with_no_client_auth(); |
|
| 241 | + | let cfg = rustls::ClientConfig::builder_with_provider(Arc::new( |
|
| 242 | + | rustls::crypto::ring::default_provider(), |
|
| 243 | + | )) |
|
| 244 | + | .with_safe_default_protocol_versions() |
|
| 245 | + | .map_err(|e| RepoError::Msg(e.to_string()))? |
|
| 246 | + | .with_root_certificates(roots) |
|
| 247 | + | .with_no_client_auth(); |
|
| 214 | 248 | Ok(Arc::new(cfg)) |
|
| 215 | 249 | } |
|
| 216 | 250 | ||
| 217 | 251 | impl HttpRemote { |
|
| 218 | 252 | pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> { |
|
| 219 | - | let agent = ureq::AgentBuilder::new().tls_config(tls_client_config(options.ca.as_deref())?).build(); |
|
| 220 | - | Ok(HttpRemote { base: base.trim_end_matches('/').to_string(), token: options.token.clone(), agent }) |
|
| 253 | + | let agent = ureq::AgentBuilder::new() |
|
| 254 | + | .tls_config(tls_client_config(options.ca.as_deref())?) |
|
| 255 | + | .build(); |
|
| 256 | + | Ok(HttpRemote { |
|
| 257 | + | base: base.trim_end_matches('/').to_string(), |
|
| 258 | + | token: options.token.clone(), |
|
| 259 | + | repo_id: options.repo_id, |
|
| 260 | + | agent, |
|
| 261 | + | }) |
|
| 221 | 262 | } |
|
| 222 | 263 | ||
| 223 | 264 | fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> { |
|
| 224 | 265 | let url = format!("{}/oxy/v1/{endpoint}", self.base); |
|
| 225 | - | let mut req = if post { self.agent.post(&url) } else { self.agent.get(&url) }; |
|
| 266 | + | let mut req = if post { |
|
| 267 | + | self.agent.post(&url) |
|
| 268 | + | } else { |
|
| 269 | + | self.agent.get(&url) |
|
| 270 | + | }; |
|
| 226 | 271 | req = req.set("Accept", CONTENT_TYPE); |
|
| 227 | 272 | if let Some(t) = &self.token { |
|
| 228 | 273 | req = req.set("Authorization", &format!("Bearer {t}")); |
|
| 229 | 274 | } |
|
| 275 | + | if let Some(id) = &self.repo_id { |
|
| 276 | + | req = req.set(REPO_ID_HEADER, &hex::encode(id)); |
|
| 277 | + | } |
|
| 230 | 278 | let resp = match body { |
|
| 231 | 279 | Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b), |
|
| 232 | 280 | None => req.call(), |
|
| … | 1 unchanged lines |
||
| 234 | 282 | match resp { |
|
| 235 | 283 | Ok(r) => { |
|
| 236 | 284 | let mut out = Vec::new(); |
|
| 237 | - | r.into_reader().read_to_end(&mut out).map_err(|e| RepoError::Msg(format!("{url}: {e}")))?; |
|
| 285 | + | r.into_reader() |
|
| 286 | + | .read_to_end(&mut out) |
|
| 287 | + | .map_err(|e| RepoError::Msg(format!("{url}: {e}")))?; |
|
| 238 | 288 | Ok(out) |
|
| 239 | 289 | } |
|
| 240 | 290 | Err(ureq::Error::Status(code, r)) => { |
|
| 241 | 291 | let text = r.into_string().unwrap_or_default(); |
|
| 242 | - | Err(RepoError::Msg(format!("{url}: HTTP {code}: {}", text.trim()))) |
|
| 292 | + | Err(RepoError::Msg(format!( |
|
| 293 | + | "{url}: HTTP {code}: {}", |
|
| 294 | + | text.trim() |
|
| 295 | + | ))) |
|
| 243 | 296 | } |
|
| 244 | 297 | Err(e) => Err(RepoError::Msg(format!("{url}: {e}"))), |
|
| 245 | 298 | } |
|
| … | 8 unchanged lines |
||
| 254 | 307 | Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1) |
|
| 255 | 308 | } |
|
| 256 | 309 | fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> { |
|
| 257 | - | wire::decode_hashes(&self.call(true, "negotiate", Some(&wire::encode_negotiate(have, want)))?) |
|
| 310 | + | wire::decode_hashes(&self.call( |
|
| 311 | + | true, |
|
| 312 | + | "negotiate", |
|
| 313 | + | Some(&wire::encode_negotiate(have, want)), |
|
| 314 | + | )?) |
|
| 258 | 315 | } |
|
| 259 | 316 | fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> { |
|
| 260 | - | wire::decode_bundle(&self.call(true, "fetch", Some(&wire::encode_fetch(changes, max_blob)))?) |
|
| 317 | + | wire::decode_bundle(&self.call( |
|
| 318 | + | true, |
|
| 319 | + | "fetch", |
|
| 320 | + | Some(&wire::encode_fetch(changes, max_blob)), |
|
| 321 | + | )?) |
|
| 261 | 322 | } |
|
| 262 | 323 | fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> { |
|
| 263 | - | Ok(wire::decode_bundle(&self.call(true, "objects", Some(&wire::encode_hashes(refs)))?)?.objects) |
|
| 324 | + | Ok( |
|
| 325 | + | wire::decode_bundle(&self.call(true, "objects", Some(&wire::encode_hashes(refs)))?)? |
|
| 326 | + | .objects, |
|
| 327 | + | ) |
|
| 264 | 328 | } |
|
| 265 | 329 | fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 266 | 330 | wire::decode_bundle(&self.call(true, "checkpoint", Some(&wire::encode_hashes(heads)))?) |
|
| 267 | 331 | } |
|
| 268 | 332 | fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> { |
|
| 269 | - | wire::decode_hashes(&self.call(true, "shallow", Some(&wire::encode_shallow(heads, depth)))?) |
|
| 333 | + | wire::decode_hashes(&self.call( |
|
| 334 | + | true, |
|
| 335 | + | "shallow", |
|
| 336 | + | Some(&wire::encode_shallow(heads, depth)), |
|
| 337 | + | )?) |
|
| 270 | 338 | } |
|
| 271 | - | fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>, tags: Vec<TagRef>) -> Result<Vec<RefResult>> { |
|
| 272 | - | wire::decode_push_response(&self.call(true, "push", Some(&wire::encode_push(&bundle, &updates, policy.as_ref(), &tags)))?) |
|
| 339 | + | fn push( |
|
| 340 | + | &self, |
|
| 341 | + | bundle: Bundle, |
|
| 342 | + | updates: Vec<RefUpdate>, |
|
| 343 | + | policy: Option<SignedPolicy>, |
|
| 344 | + | tags: Vec<TagRef>, |
|
| 345 | + | ) -> Result<Vec<RefResult>> { |
|
| 346 | + | wire::decode_push_response(&self.call( |
|
| 347 | + | true, |
|
| 348 | + | "push", |
|
| 349 | + | Some(&wire::encode_push( |
|
| 350 | + | &bundle, |
|
| 351 | + | &updates, |
|
| 352 | + | policy.as_ref(), |
|
| 353 | + | &tags, |
|
| 354 | + | )), |
|
| 355 | + | )?) |
|
| 273 | 356 | } |
|
| 274 | 357 | } |
|
| 275 | 358 | ||
| … | 22 unchanged lines |
||
| 298 | 381 | Value::Array(sigs.iter().map(|s| s.to_value()).collect()) |
|
| 299 | 382 | } |
|
| 300 | 383 | fn sigs_from(v: &Value) -> Result<Vec<Signature>> { |
|
| 301 | - | Ok(o::array(v, "signatures")?.iter().map(Signature::from_value).collect::<std::result::Result<Vec<_>, _>>()?) |
|
| 384 | + | Ok(o::array(v, "signatures")? |
|
| 385 | + | .iter() |
|
| 386 | + | .map(Signature::from_value) |
|
| 387 | + | .collect::<std::result::Result<Vec<_>, _>>()?) |
|
| 302 | 388 | } |
|
| 303 | 389 | fn hashes_value(hs: &[ObjectRef]) -> Value { |
|
| 304 | 390 | Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()) |
|
| 305 | 391 | } |
|
| 306 | 392 | fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> { |
|
| 307 | - | Ok(o::array(v, "hashes")?.iter().map(|h| o::oref(h, "hash")).collect::<std::result::Result<Vec<_>, _>>()?) |
|
| 393 | + | Ok(o::array(v, "hashes")? |
|
| 394 | + | .iter() |
|
| 395 | + | .map(|h| o::oref(h, "hash")) |
|
| 396 | + | .collect::<std::result::Result<Vec<_>, _>>()?) |
|
| 308 | 397 | } |
|
| 309 | 398 | fn attest_value(sa: &SignedAttest) -> Value { |
|
| 310 | 399 | Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)]) |
|
| … | 3 unchanged lines |
||
| 314 | 403 | if a.len() != 2 { |
|
| 315 | 404 | return Err(schema("signed attest: expected [attest, sigs]")); |
|
| 316 | 405 | } |
|
| 317 | - | Ok(SignedAttest { attest: Attest::decode(o::bytes(&a[0], "attest")?)?, sigs: sigs_from(&a[1])? }) |
|
| 406 | + | Ok(SignedAttest { |
|
| 407 | + | attest: Attest::decode(o::bytes(&a[0], "attest")?)?, |
|
| 408 | + | sigs: sigs_from(&a[1])?, |
|
| 409 | + | }) |
|
| 318 | 410 | } |
|
| 319 | 411 | fn opt_attest_value(sa: &Option<SignedAttest>) -> Value { |
|
| 320 | 412 | sa.as_ref().map(attest_value).unwrap_or(Value::Null) |
|
| 321 | 413 | } |
|
| 322 | 414 | fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> { |
|
| 323 | - | if v.is_null() { Ok(None) } else { attest_from(v).map(Some) } |
|
| 415 | + | if v.is_null() { |
|
| 416 | + | Ok(None) |
|
| 417 | + | } else { |
|
| 418 | + | attest_from(v).map(Some) |
|
| 419 | + | } |
|
| 324 | 420 | } |
|
| 325 | 421 | /// `[policy bytes, [signature]]`: also the format of a policy proposal |
|
| 326 | 422 | /// file exchanged between root holders (spec §14.5). |
|
| … | 5 unchanged lines |
||
| 332 | 428 | if a.len() != 2 { |
|
| 333 | 429 | return Err(schema("signed policy: expected [policy, sigs]")); |
|
| 334 | 430 | } |
|
| 335 | - | Ok(SignedPolicy { policy: Policy::decode(o::bytes(&a[0], "policy")?)?, sigs: sigs_from(&a[1])? }) |
|
| 431 | + | Ok(SignedPolicy { |
|
| 432 | + | policy: Policy::decode(o::bytes(&a[0], "policy")?)?, |
|
| 433 | + | sigs: sigs_from(&a[1])?, |
|
| 434 | + | }) |
|
| 336 | 435 | } |
|
| 337 | 436 | fn opt_policy_value(sp: Option<&SignedPolicy>) -> Value { |
|
| 338 | 437 | sp.map(policy_value).unwrap_or(Value::Null) |
|
| 339 | 438 | } |
|
| 340 | 439 | fn opt_policy_from(v: &Value) -> Result<Option<SignedPolicy>> { |
|
| 341 | - | if v.is_null() { Ok(None) } else { policy_from(v).map(Some) } |
|
| 440 | + | if v.is_null() { |
|
| 441 | + | Ok(None) |
|
| 442 | + | } else { |
|
| 443 | + | policy_from(v).map(Some) |
|
| 444 | + | } |
|
| 342 | 445 | } |
|
| 343 | 446 | fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> { |
|
| 344 | 447 | Ok(o::map(v, what)?) |
|
| … | 6 unchanged lines |
||
| 351 | 454 | Value::map([ |
|
| 352 | 455 | ( |
|
| 353 | 456 | 0, |
|
| 354 | - | Value::Array(b.changes.iter().map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)])).collect()), |
|
| 457 | + | Value::Array( |
|
| 458 | + | b.changes |
|
| 459 | + | .iter() |
|
| 460 | + | .map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)])) |
|
| 461 | + | .collect(), |
|
| 462 | + | ), |
|
| 355 | 463 | ), |
|
| 356 | - | (1, Value::Array(b.attests.iter().map(attest_value).collect())), |
|
| 357 | 464 | ( |
|
| 465 | + | 1, |
|
| 466 | + | Value::Array(b.attests.iter().map(attest_value).collect()), |
|
| 467 | + | ), |
|
| 468 | + | ( |
|
| 358 | 469 | 2, |
|
| 359 | 470 | Value::Array( |
|
| 360 | - | b.objects.iter().map(|(t, body)| Value::Array(vec![Value::Text(t.name().into()), Value::Bytes(body.clone())])).collect(), |
|
| 471 | + | b.objects |
|
| 472 | + | .iter() |
|
| 473 | + | .map(|(t, body)| { |
|
| 474 | + | Value::Array(vec![ |
|
| 475 | + | Value::Text(t.name().into()), |
|
| 476 | + | Value::Bytes(body.clone()), |
|
| 477 | + | ]) |
|
| 478 | + | }) |
|
| 479 | + | .collect(), |
|
| 361 | 480 | ), |
|
| 362 | 481 | ), |
|
| 363 | - | (3, Value::Array(b.policies.iter().map(policy_value).collect())), |
|
| 482 | + | ( |
|
| 483 | + | 3, |
|
| 484 | + | Value::Array(b.policies.iter().map(policy_value).collect()), |
|
| 485 | + | ), |
|
| 364 | 486 | ]) |
|
| 365 | 487 | } |
|
| 366 | 488 | ||
| … | 5 unchanged lines |
||
| 372 | 494 | if pair.len() != 2 { |
|
| 373 | 495 | return Err(schema("change entry: expected [change, sigs]")); |
|
| 374 | 496 | } |
|
| 375 | - | b.changes.push((Change::decode(o::bytes(&pair[0], "change")?)?, sigs_from(&pair[1])?)); |
|
| 497 | + | b.changes.push(( |
|
| 498 | + | Change::decode(o::bytes(&pair[0], "change")?)?, |
|
| 499 | + | sigs_from(&pair[1])?, |
|
| 500 | + | )); |
|
| 376 | 501 | } |
|
| 377 | 502 | for a in o::array(field(m, 1, "attests")?, "attests")? { |
|
| 378 | 503 | b.attests.push(attest_from(a)?); |
|
| … | 3 unchanged lines |
||
| 382 | 507 | if pair.len() != 2 { |
|
| 383 | 508 | return Err(schema("object: expected [type, body]")); |
|
| 384 | 509 | } |
|
| 385 | - | let ty = ObjectType::from_name(o::text(&pair[0], "object type")?).ok_or_else(|| schema("unknown object type"))?; |
|
| 386 | - | b.objects.push((ty, o::bytes(&pair[1], "object body")?.to_vec())); |
|
| 510 | + | let ty = ObjectType::from_name(o::text(&pair[0], "object type")?) |
|
| 511 | + | .ok_or_else(|| schema("unknown object type"))?; |
|
| 512 | + | b.objects |
|
| 513 | + | .push((ty, o::bytes(&pair[1], "object body")?.to_vec())); |
|
| 387 | 514 | } |
|
| 388 | 515 | if let Some(ps) = m.get(&3) { |
|
| 389 | 516 | for p in o::array(ps, "policies")? { |
|
| … | 21 unchanged lines |
||
| 411 | 538 | let v = cbor::decode(bytes).map_err(ObjectError::from)?; |
|
| 412 | 539 | let m = map(&v, "capabilities")?; |
|
| 413 | 540 | let versions = o::array(field(m, 0, "versions")?, "versions")?; |
|
| 414 | - | if !versions.iter().any(|x| x.as_uint() == Some(PROTOCOL_VERSION)) { |
|
| 415 | - | return Err(RepoError::Msg("remote does not speak protocol version 1".into())); |
|
| 541 | + | if !versions |
|
| 542 | + | .iter() |
|
| 543 | + | .any(|x| x.as_uint() == Some(PROTOCOL_VERSION)) |
|
| 544 | + | { |
|
| 545 | + | return Err(RepoError::Msg( |
|
| 546 | + | "remote does not speak protocol version 1".into(), |
|
| 547 | + | )); |
|
| 416 | 548 | } |
|
| 417 | - | o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes")) |
|
| 549 | + | o::bytes(field(m, 1, "repo id")?, "repo id")? |
|
| 550 | + | .try_into() |
|
| 551 | + | .map_err(|_| schema("repo id: expected 16 bytes")) |
|
| 418 | 552 | } |
|
| 419 | 553 | ||
| 420 | 554 | fn tags_value(tags: &[TagRef]) -> Value { |
|
| … | 46 unchanged lines |
||
| 467 | 601 | (1, Value::Bytes(id.to_vec())), |
|
| 468 | 602 | (2, opt_policy_value(resp.policy.as_ref())), |
|
| 469 | 603 | (3, tags_value(&resp.tags)), |
|
| 470 | - | (4, Value::Array(resp.history.iter().map(policy_value).collect())), |
|
| 604 | + | ( |
|
| 605 | + | 4, |
|
| 606 | + | Value::Array(resp.history.iter().map(policy_value).collect()), |
|
| 607 | + | ), |
|
| 471 | 608 | ])) |
|
| 472 | 609 | } |
|
| 473 | 610 | pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], RefsResponse)> { |
|
| 474 | 611 | let v = cbor::decode(bytes).map_err(ObjectError::from)?; |
|
| 475 | 612 | let m = map(&v, "refs")?; |
|
| 476 | - | let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))?; |
|
| 613 | + | let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")? |
|
| 614 | + | .try_into() |
|
| 615 | + | .map_err(|_| schema("repo id: expected 16 bytes"))?; |
|
| 477 | 616 | let mut refs = Vec::new(); |
|
| 478 | 617 | for r in o::array(field(m, 0, "refs")?, "refs")? { |
|
| 479 | 618 | let rm = map(r, "ref")?; |
|
| … | 3 unchanged lines |
||
| 483 | 622 | state: o::oref(field(rm, 1, "state")?, "state")?, |
|
| 484 | 623 | heads: hashes_from(field(rm, 2, "heads")?)?, |
|
| 485 | 624 | attestation: opt_attest_from(field(rm, 3, "attestation")?)?, |
|
| 486 | - | last_serial: if ls.is_null() { None } else { Some(o::uint(ls, "last serial")?) }, |
|
| 625 | + | last_serial: if ls.is_null() { |
|
| 626 | + | None |
|
| 627 | + | } else { |
|
| 628 | + | Some(o::uint(ls, "last serial")?) |
|
| 629 | + | }, |
|
| 487 | 630 | }); |
|
| 488 | 631 | } |
|
| 489 | 632 | let policy = match m.get(&2) { |
|
| … | 10 unchanged lines |
||
| 500 | 643 | history.push(policy_from(p)?); |
|
| 501 | 644 | } |
|
| 502 | 645 | } |
|
| 503 | - | Ok((id, RefsResponse { refs, policy, tags, history })) |
|
| 646 | + | Ok(( |
|
| 647 | + | id, |
|
| 648 | + | RefsResponse { |
|
| 649 | + | refs, |
|
| 650 | + | policy, |
|
| 651 | + | tags, |
|
| 652 | + | history, |
|
| 653 | + | }, |
|
| 654 | + | )) |
|
| 504 | 655 | } |
|
| 505 | 656 | ||
| 506 | 657 | pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> { |
|
| 507 | - | cbor::encode(&Value::map([(0, hashes_value(have)), (1, hashes_value(want))])) |
|
| 658 | + | cbor::encode(&Value::map([ |
|
| 659 | + | (0, hashes_value(have)), |
|
| 660 | + | (1, hashes_value(want)), |
|
| 661 | + | ])) |
|
| 508 | 662 | } |
|
| 509 | 663 | pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> { |
|
| 510 | 664 | let v = cbor::decode(bytes).map_err(ObjectError::from)?; |
|
| 511 | 665 | let m = map(&v, "negotiate")?; |
|
| 512 | - | Ok((hashes_from(field(m, 0, "have")?)?, hashes_from(field(m, 1, "want")?)?)) |
|
| 666 | + | Ok(( |
|
| 667 | + | hashes_from(field(m, 0, "have")?)?, |
|
| 668 | + | hashes_from(field(m, 1, "want")?)?, |
|
| 669 | + | )) |
|
| 513 | 670 | } |
|
| 514 | 671 | ||
| 515 | 672 | /// `fetch` request: `{0: [changes], 1: max blob size or null}`. |
|
| 516 | 673 | pub fn encode_fetch(hs: &[ChangeHash], max_blob: Option<u64>) -> Vec<u8> { |
|
| 517 | - | cbor::encode(&Value::map([(0, hashes_value(hs)), (1, max_blob.map(Value::Uint).unwrap_or(Value::Null))])) |
|
| 674 | + | cbor::encode(&Value::map([ |
|
| 675 | + | (0, hashes_value(hs)), |
|
| 676 | + | (1, max_blob.map(Value::Uint).unwrap_or(Value::Null)), |
|
| 677 | + | ])) |
|
| 518 | 678 | } |
|
| 519 | 679 | pub fn decode_fetch(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Option<u64>)> { |
|
| 520 | 680 | let v = cbor::decode(bytes).map_err(ObjectError::from)?; |
|
| … | 7 unchanged lines |
||
| 528 | 688 | ||
| 529 | 689 | /// `shallow` request: `{0: [heads], 1: depth}`. |
|
| 530 | 690 | pub fn encode_shallow(hs: &[ChangeHash], depth: u32) -> Vec<u8> { |
|
| 531 | - | cbor::encode(&Value::map([(0, hashes_value(hs)), (1, Value::Uint(depth as u64))])) |
|
| 691 | + | cbor::encode(&Value::map([ |
|
| 692 | + | (0, hashes_value(hs)), |
|
| 693 | + | (1, Value::Uint(depth as u64)), |
|
| 694 | + | ])) |
|
| 532 | 695 | } |
|
| 533 | 696 | pub fn decode_shallow(bytes: &[u8]) -> Result<(Vec<ChangeHash>, u32)> { |
|
| 534 | 697 | let v = cbor::decode(bytes).map_err(ObjectError::from)?; |
|
| 535 | 698 | let m = map(&v, "shallow")?; |
|
| 536 | - | Ok((hashes_from(field(m, 0, "heads")?)?, o::u32_(field(m, 1, "depth")?, "depth")?)) |
|
| 699 | + | Ok(( |
|
| 700 | + | hashes_from(field(m, 0, "heads")?)?, |
|
| 701 | + | o::u32_(field(m, 1, "depth")?, "depth")?, |
|
| 702 | + | )) |
|
| 537 | 703 | } |
|
| 538 | 704 | ||
| 539 | 705 | pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> { |
|
| … | 4 unchanged lines |
||
| 544 | 710 | hashes_from(field(map(&v, "hashes")?, 0, "hashes")?) |
|
| 545 | 711 | } |
|
| 546 | 712 | ||
| 547 | - | pub fn encode_push(bundle: &Bundle, updates: &[RefUpdate], policy: Option<&SignedPolicy>, tags: &[TagRef]) -> Vec<u8> { |
|
| 713 | + | pub fn encode_push( |
|
| 714 | + | bundle: &Bundle, |
|
| 715 | + | updates: &[RefUpdate], |
|
| 716 | + | policy: Option<&SignedPolicy>, |
|
| 717 | + | tags: &[TagRef], |
|
| 718 | + | ) -> Vec<u8> { |
|
| 548 | 719 | cbor::encode(&Value::map([ |
|
| 549 | 720 | (0, bundle_value(bundle)), |
|
| 550 | 721 | (2, opt_policy_value(policy)), |
|
| … | 31 unchanged lines |
||
| 582 | 753 | name: o::text(field(um, 0, "name")?, "name")?.to_owned(), |
|
| 583 | 754 | old_state: o::oref(field(um, 1, "old_state")?, "old_state")?, |
|
| 584 | 755 | new_state: o::oref(field(um, 2, "new_state")?, "new_state")?, |
|
| 585 | - | force: field(um, 3, "force")?.as_bool().ok_or_else(|| schema("force: expected bool"))?, |
|
| 756 | + | force: field(um, 3, "force")? |
|
| 757 | + | .as_bool() |
|
| 758 | + | .ok_or_else(|| schema("force: expected bool"))?, |
|
| 586 | 759 | attestation: opt_attest_from(field(um, 4, "attestation")?)?, |
|
| 587 | 760 | }); |
|
| 588 | 761 | } |
|
| … | 14 unchanged lines |
||
| 603 | 776 | Value::Array( |
|
| 604 | 777 | results |
|
| 605 | 778 | .iter() |
|
| 606 | - | .map(|r| Value::map([(0, Value::Text(r.name.clone())), (1, Value::Bool(r.ok)), (2, Value::Text(r.reason.clone()))])) |
|
| 779 | + | .map(|r| { |
|
| 780 | + | Value::map([ |
|
| 781 | + | (0, Value::Text(r.name.clone())), |
|
| 782 | + | (1, Value::Bool(r.ok)), |
|
| 783 | + | (2, Value::Text(r.reason.clone())), |
|
| 784 | + | ]) |
|
| 785 | + | }) |
|
| 607 | 786 | .collect(), |
|
| 608 | 787 | ), |
|
| 609 | 788 | )])) |
|
| … | 6 unchanged lines |
||
| 616 | 795 | let rm = map(r, "result")?; |
|
| 617 | 796 | out.push(RefResult { |
|
| 618 | 797 | name: o::text(field(rm, 0, "name")?, "name")?.to_owned(), |
|
| 619 | - | ok: field(rm, 1, "ok")?.as_bool().ok_or_else(|| schema("ok: expected bool"))?, |
|
| 798 | + | ok: field(rm, 1, "ok")? |
|
| 799 | + | .as_bool() |
|
| 800 | + | .ok_or_else(|| schema("ok: expected bool"))?, |
|
| 620 | 801 | reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(), |
|
| 621 | 802 | }); |
|
| 622 | 803 | } |
|
| … | 10 unchanged lines |
||
| 633 | 814 | #[test] |
|
| 634 | 815 | fn wire_round_trips() { |
|
| 635 | 816 | let c = crate::objects::tests::sample_change(); |
|
| 636 | - | let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] }; |
|
| 817 | + | let sk = crate::identity::SecretKey { |
|
| 818 | + | ed_seed: [1; 32], |
|
| 819 | + | ml_seed: [2; 32], |
|
| 820 | + | }; |
|
| 637 | 821 | let id = sk.identity("a", "b", 0); |
|
| 638 | 822 | let key = id.hash(); |
|
| 639 | 823 | let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash()); |
|
| … | 3 unchanged lines |
||
| 643 | 827 | let psig = sk.sign(crate::identity::Domain::Policy, &key, &policy.hash()); |
|
| 644 | 828 | let bundle = Bundle { |
|
| 645 | 829 | changes: vec![(c.clone(), vec![sig.clone()])], |
|
| 646 | - | attests: vec![SignedAttest { attest: seal.clone(), sigs: vec![seal_sig] }], |
|
| 647 | - | objects: vec![(ObjectType::Identity, id.encode()), (ObjectType::Chunk, b"xyz".to_vec())], |
|
| 648 | - | policies: vec![SignedPolicy { policy: policy.clone(), sigs: vec![psig.clone()] }], |
|
| 830 | + | attests: vec![SignedAttest { |
|
| 831 | + | attest: seal.clone(), |
|
| 832 | + | sigs: vec![seal_sig], |
|
| 833 | + | }], |
|
| 834 | + | objects: vec![ |
|
| 835 | + | (ObjectType::Identity, id.encode()), |
|
| 836 | + | (ObjectType::Chunk, b"xyz".to_vec()), |
|
| 837 | + | ], |
|
| 838 | + | policies: vec![SignedPolicy { |
|
| 839 | + | policy: policy.clone(), |
|
| 840 | + | sigs: vec![psig.clone()], |
|
| 841 | + | }], |
|
| 649 | 842 | }; |
|
| 650 | 843 | let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap(); |
|
| 651 | 844 | assert_eq!(d.changes, bundle.changes); |
|
| … | 1 unchanged lines |
||
| 653 | 846 | assert_eq!(d.objects, bundle.objects); |
|
| 654 | 847 | assert_eq!(d.policies, bundle.policies); |
|
| 655 | 848 | ||
| 656 | - | let att = Attest { repo: [3; 16], hlc: Hlc::default(), body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 4 } }; |
|
| 849 | + | let att = Attest { |
|
| 850 | + | repo: [3; 16], |
|
| 851 | + | hlc: Hlc::default(), |
|
| 852 | + | body: AttestBody::Ref { |
|
| 853 | + | name: "main".into(), |
|
| 854 | + | old_state: ObjectRef([1; 32]), |
|
| 855 | + | new_state: ObjectRef([2; 32]), |
|
| 856 | + | serial: 4, |
|
| 857 | + | }, |
|
| 858 | + | }; |
|
| 657 | 859 | let refs = vec![ |
|
| 658 | - | RemoteRef { name: "main".into(), state: ObjectRef([2; 32]), heads: vec![c.hash()], attestation: Some(SignedAttest { attest: att.clone(), sigs: vec![sig.clone()] }), last_serial: Some(4) }, |
|
| 659 | - | RemoteRef { name: "dev".into(), state: ObjectRef([5; 32]), heads: vec![], attestation: None, last_serial: None }, |
|
| 860 | + | RemoteRef { |
|
| 861 | + | name: "main".into(), |
|
| 862 | + | state: ObjectRef([2; 32]), |
|
| 863 | + | heads: vec![c.hash()], |
|
| 864 | + | attestation: Some(SignedAttest { |
|
| 865 | + | attest: att.clone(), |
|
| 866 | + | sigs: vec![sig.clone()], |
|
| 867 | + | }), |
|
| 868 | + | last_serial: Some(4), |
|
| 869 | + | }, |
|
| 870 | + | RemoteRef { |
|
| 871 | + | name: "dev".into(), |
|
| 872 | + | state: ObjectRef([5; 32]), |
|
| 873 | + | heads: vec![], |
|
| 874 | + | attestation: None, |
|
| 875 | + | last_serial: None, |
|
| 876 | + | }, |
|
| 660 | 877 | ]; |
|
| 661 | - | let tag = TagRef { name: "v1".into(), state: ObjectRef([2; 32]), heads: vec![c.hash()], attestation: None }; |
|
| 662 | - | let resp = RefsResponse { refs, policy: Some(SignedPolicy { policy: policy.clone(), sigs: vec![psig.clone()] }), tags: vec![tag.clone()], history: vec![SignedPolicy { policy: policy.clone(), sigs: vec![] }] }; |
|
| 878 | + | let tag = TagRef { |
|
| 879 | + | name: "v1".into(), |
|
| 880 | + | state: ObjectRef([2; 32]), |
|
| 881 | + | heads: vec![c.hash()], |
|
| 882 | + | attestation: None, |
|
| 883 | + | }; |
|
| 884 | + | let resp = RefsResponse { |
|
| 885 | + | refs, |
|
| 886 | + | policy: Some(SignedPolicy { |
|
| 887 | + | policy: policy.clone(), |
|
| 888 | + | sigs: vec![psig.clone()], |
|
| 889 | + | }), |
|
| 890 | + | tags: vec![tag.clone()], |
|
| 891 | + | history: vec![SignedPolicy { |
|
| 892 | + | policy: policy.clone(), |
|
| 893 | + | sigs: vec![], |
|
| 894 | + | }], |
|
| 895 | + | }; |
|
| 663 | 896 | let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &resp)).unwrap(); |
|
| 664 | 897 | assert_eq!(id_back, [3; 16]); |
|
| 665 | 898 | assert_eq!(back.refs.len(), 2); |
|
| … | 3 unchanged lines |
||
| 669 | 902 | assert_eq!(back.history, resp.history); |
|
| 670 | 903 | assert_eq!(back.tags, vec![tag.clone()]); |
|
| 671 | 904 | ||
| 672 | - | let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(&[ObjectRef([1; 32])], &[ObjectRef([2; 32])])).unwrap(); |
|
| 905 | + | let (h, w) = wire::decode_negotiate(&wire::encode_negotiate( |
|
| 906 | + | &[ObjectRef([1; 32])], |
|
| 907 | + | &[ObjectRef([2; 32])], |
|
| 908 | + | )) |
|
| 909 | + | .unwrap(); |
|
| 673 | 910 | assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])])); |
|
| 674 | - | assert_eq!(wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(), vec![c.hash()]); |
|
| 675 | - | assert_eq!(wire::decode_fetch(&wire::encode_fetch(&[c.hash()], Some(4096))).unwrap(), (vec![c.hash()], Some(4096))); |
|
| 676 | - | assert_eq!(wire::decode_fetch(&wire::encode_fetch(&[], None)).unwrap(), (vec![], None)); |
|
| 677 | - | assert_eq!(wire::decode_shallow(&wire::encode_shallow(&[c.hash()], 3)).unwrap(), (vec![c.hash()], 3)); |
|
| 911 | + | assert_eq!( |
|
| 912 | + | wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(), |
|
| 913 | + | vec![c.hash()] |
|
| 914 | + | ); |
|
| 915 | + | assert_eq!( |
|
| 916 | + | wire::decode_fetch(&wire::encode_fetch(&[c.hash()], Some(4096))).unwrap(), |
|
| 917 | + | (vec![c.hash()], Some(4096)) |
|
| 918 | + | ); |
|
| 919 | + | assert_eq!( |
|
| 920 | + | wire::decode_fetch(&wire::encode_fetch(&[], None)).unwrap(), |
|
| 921 | + | (vec![], None) |
|
| 922 | + | ); |
|
| 923 | + | assert_eq!( |
|
| 924 | + | wire::decode_shallow(&wire::encode_shallow(&[c.hash()], 3)).unwrap(), |
|
| 925 | + | (vec![c.hash()], 3) |
|
| 926 | + | ); |
|
| 678 | 927 | // An old-style fetch request without the filter field still decodes. |
|
| 679 | - | assert_eq!(wire::decode_fetch(&wire::encode_hashes(&[c.hash()])).unwrap(), (vec![c.hash()], None)); |
|
| 928 | + | assert_eq!( |
|
| 929 | + | wire::decode_fetch(&wire::encode_hashes(&[c.hash()])).unwrap(), |
|
| 930 | + | (vec![c.hash()], None) |
|
| 931 | + | ); |
|
| 680 | 932 | ||
| 681 | - | let up = RefUpdate { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), force: true, attestation: Some(SignedAttest { attest: att, sigs: vec![sig] }) }; |
|
| 682 | - | let sp = SignedPolicy { policy, sigs: vec![psig] }; |
|
| 683 | - | let (b2, ups, pol, tags) = wire::decode_push(&wire::encode_push(&bundle, std::slice::from_ref(&up), Some(&sp), std::slice::from_ref(&tag))).unwrap(); |
|
| 933 | + | let up = RefUpdate { |
|
| 934 | + | name: "main".into(), |
|
| 935 | + | old_state: ObjectRef([1; 32]), |
|
| 936 | + | new_state: ObjectRef([2; 32]), |
|
| 937 | + | force: true, |
|
| 938 | + | attestation: Some(SignedAttest { |
|
| 939 | + | attest: att, |
|
| 940 | + | sigs: vec![sig], |
|
| 941 | + | }), |
|
| 942 | + | }; |
|
| 943 | + | let sp = SignedPolicy { |
|
| 944 | + | policy, |
|
| 945 | + | sigs: vec![psig], |
|
| 946 | + | }; |
|
| 947 | + | let (b2, ups, pol, tags) = wire::decode_push(&wire::encode_push( |
|
| 948 | + | &bundle, |
|
| 949 | + | std::slice::from_ref(&up), |
|
| 950 | + | Some(&sp), |
|
| 951 | + | std::slice::from_ref(&tag), |
|
| 952 | + | )) |
|
| 953 | + | .unwrap(); |
|
| 684 | 954 | assert_eq!(pol, Some(sp)); |
|
| 685 | 955 | assert_eq!(tags, vec![tag]); |
|
| 686 | 956 | assert_eq!(b2.changes.len(), 1); |
|
| 687 | 957 | assert_eq!(ups[0].name, "main"); |
|
| 688 | 958 | assert!(ups[0].force); |
|
| 689 | 959 | assert_eq!(ups[0].attestation, up.attestation); |
|
| 690 | - | let res = vec![RefResult { name: "main".into(), ok: false, reason: "stale".into() }]; |
|
| 691 | - | assert_eq!(wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(), res); |
|
| 692 | - | assert_eq!(wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(), [7; 16]); |
|
| 960 | + | let res = vec![RefResult { |
|
| 961 | + | name: "main".into(), |
|
| 962 | + | ok: false, |
|
| 963 | + | reason: "stale".into(), |
|
| 964 | + | }]; |
|
| 965 | + | assert_eq!( |
|
| 966 | + | wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(), |
|
| 967 | + | res |
|
| 968 | + | ); |
|
| 969 | + | assert_eq!( |
|
| 970 | + | wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(), |
|
| 971 | + | [7; 16] |
|
| 972 | + | ); |
|
| 693 | 973 | } |
|
| 694 | 974 | } |
modified crates/oxy-core/src/repo/bookmarks.rs +68 −15
| … | 37 unchanged lines |
||
| 38 | 38 | ||
| 39 | 39 | impl Repo { |
|
| 40 | 40 | fn workspace_bookmark_path(&self) -> PathBuf { |
|
| 41 | - | self.oxy.join("workspaces").join(self.workspace.to_hex()).join("bookmark") |
|
| 41 | + | self.oxy |
|
| 42 | + | .join("workspaces") |
|
| 43 | + | .join(self.workspace.to_hex()) |
|
| 44 | + | .join("bookmark") |
|
| 42 | 45 | } |
|
| 43 | 46 | ||
| 44 | 47 | /// Every bookmark with its heads and remote-tracking difference. |
|
| … | 5 unchanged lines |
||
| 50 | 53 | let (ahead, behind) = match view.remotes.get("origin").and_then(|b| b.get(name)) { |
|
| 51 | 54 | Some(rs) => { |
|
| 52 | 55 | let remote_heads = self.store.get_state(rs)?.heads; |
|
| 53 | - | (self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(), self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok()) |
|
| 56 | + | ( |
|
| 57 | + | self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(), |
|
| 58 | + | self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok(), |
|
| 59 | + | ) |
|
| 54 | 60 | } |
|
| 55 | 61 | None => (None, None), |
|
| 56 | 62 | }; |
|
| 57 | - | out.push(BookmarkInfo { name: name.clone(), heads, current: *name == self.config.bookmark, ahead, behind }); |
|
| 63 | + | out.push(BookmarkInfo { |
|
| 64 | + | name: name.clone(), |
|
| 65 | + | heads, |
|
| 66 | + | current: *name == self.config.bookmark, |
|
| 67 | + | ahead, |
|
| 68 | + | behind, |
|
| 69 | + | }); |
|
| 58 | 70 | } |
|
| 59 | 71 | Ok(out) |
|
| 60 | 72 | } |
|
| 61 | 73 | ||
| 62 | 74 | /// Create `name` at `heads` (default: the working copy's base heads). |
|
| 63 | - | pub fn bookmark_create(&self, name: &str, heads: Option<&[ChangeHash]>) -> Result<Vec<ChangeHash>> { |
|
| 75 | + | pub fn bookmark_create( |
|
| 76 | + | &self, |
|
| 77 | + | name: &str, |
|
| 78 | + | heads: Option<&[ChangeHash]>, |
|
| 79 | + | ) -> Result<Vec<ChangeHash>> { |
|
| 64 | 80 | if !valid_reference_name(name) { |
|
| 65 | 81 | return fail(format!("{name:?} is not a valid bookmark name")); |
|
| 66 | 82 | } |
|
| … | 9 unchanged lines |
||
| 76 | 92 | } |
|
| 77 | 93 | }; |
|
| 78 | 94 | let mut view = op.view.clone(); |
|
| 79 | - | view.bookmarks.insert(name.to_string(), self.store.put_state(&StateObject::new(heads.clone()))?); |
|
| 95 | + | view.bookmarks.insert( |
|
| 96 | + | name.to_string(), |
|
| 97 | + | self.store.put_state(&StateObject::new(heads.clone()))?, |
|
| 98 | + | ); |
|
| 80 | 99 | self.record_op(&[op_ref], &format!("oxy bookmark create {name}"), view)?; |
|
| 81 | 100 | Ok(heads) |
|
| 82 | 101 | } |
|
| … | 2 unchanged lines |
||
| 85 | 104 | pub fn bookmark_set(&self, name: &str, heads: &[ChangeHash]) -> Result<Vec<ChangeHash>> { |
|
| 86 | 105 | let (op_ref, op) = self.head_op()?; |
|
| 87 | 106 | if !op.view.bookmarks.contains_key(name) { |
|
| 88 | - | return fail(format!("no bookmark {name}; create it with `oxy bookmark create`")); |
|
| 107 | + | return fail(format!( |
|
| 108 | + | "no bookmark {name}; create it with `oxy bookmark create`" |
|
| 109 | + | )); |
|
| 89 | 110 | } |
|
| 90 | 111 | let heads = self.load_state(heads)?.maximal(heads); |
|
| 91 | 112 | let mut view = op.view.clone(); |
|
| 92 | - | view.bookmarks.insert(name.to_string(), self.store.put_state(&StateObject::new(heads.clone()))?); |
|
| 113 | + | view.bookmarks.insert( |
|
| 114 | + | name.to_string(), |
|
| 115 | + | self.store.put_state(&StateObject::new(heads.clone()))?, |
|
| 116 | + | ); |
|
| 93 | 117 | self.record_op(&[op_ref], &format!("oxy bookmark set {name}"), view)?; |
|
| 94 | 118 | if name == self.config.bookmark { |
|
| 95 | 119 | self.drop_manifest(); |
|
| … | 4 unchanged lines |
||
| 100 | 124 | /// Remove a bookmark; its changes stay until unreachable (spec §21). |
|
| 101 | 125 | pub fn bookmark_delete(&self, name: &str) -> Result<()> { |
|
| 102 | 126 | if name == self.config.bookmark { |
|
| 103 | - | return fail(format!("{name} is this workspace's current bookmark; switch to another first")); |
|
| 127 | + | return fail(format!( |
|
| 128 | + | "{name} is this workspace's current bookmark; switch to another first" |
|
| 129 | + | )); |
|
| 104 | 130 | } |
|
| 105 | 131 | for w in self.workspaces()? { |
|
| 106 | 132 | if !w.current && self.workspace_bookmark_of(&w.id).as_deref() == Some(name) { |
|
| 107 | - | return fail(format!("{name} is the current bookmark of workspace {} at {}", w.id.short(8), w.path.display())); |
|
| 133 | + | return fail(format!( |
|
| 134 | + | "{name} is the current bookmark of workspace {} at {}", |
|
| 135 | + | w.id.short(8), |
|
| 136 | + | w.path.display() |
|
| 137 | + | )); |
|
| 108 | 138 | } |
|
| 109 | 139 | } |
|
| 110 | 140 | let (op_ref, op) = self.head_op()?; |
|
| … | 6 unchanged lines |
||
| 117 | 147 | } |
|
| 118 | 148 | ||
| 119 | 149 | fn workspace_bookmark_of(&self, id: &WorkspaceId) -> Option<String> { |
|
| 120 | - | fs::read_to_string(self.oxy.join("workspaces").join(id.to_hex()).join("bookmark")).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) |
|
| 150 | + | fs::read_to_string( |
|
| 151 | + | self.oxy |
|
| 152 | + | .join("workspaces") |
|
| 153 | + | .join(id.to_hex()) |
|
| 154 | + | .join("bookmark"), |
|
| 155 | + | ) |
|
| 156 | + | .ok() |
|
| 157 | + | .map(|s| s.trim().to_string()) |
|
| 158 | + | .filter(|s| !s.is_empty()) |
|
| 121 | 159 | } |
|
| 122 | 160 | ||
| 123 | 161 | /// Make `name` this workspace's current bookmark and check it out. |
|
| 124 | 162 | pub fn bookmark_switch(&mut self, name: &str, discard: bool) -> Result<CheckoutReport> { |
|
| 125 | 163 | let view = self.view()?; |
|
| 126 | - | let Some(st) = view.bookmarks.get(name) else { return fail(format!("no bookmark {name}")) }; |
|
| 164 | + | let Some(st) = view.bookmarks.get(name) else { |
|
| 165 | + | return fail(format!("no bookmark {name}")); |
|
| 166 | + | }; |
|
| 127 | 167 | let heads = self.store.get_state(st)?.heads; |
|
| 128 | 168 | let report = self.checkout(&heads, discard, &format!("oxy bookmark switch {name}"))?; |
|
| 129 | 169 | fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?; |
|
| … | 17 unchanged lines |
||
| 147 | 187 | if name == current { |
|
| 148 | 188 | return fail(format!("{name} is the current bookmark")); |
|
| 149 | 189 | } |
|
| 150 | - | let Some(st) = op.view.bookmarks.get(name) else { return fail(format!("no bookmark {name}")) }; |
|
| 190 | + | let Some(st) = op.view.bookmarks.get(name) else { |
|
| 191 | + | return fail(format!("no bookmark {name}")); |
|
| 192 | + | }; |
|
| 151 | 193 | let other = self.store.get_state(st)?.heads; |
|
| 152 | 194 | let local = self.heads_of(&op.view, ¤t)?; |
|
| 153 | 195 | let mut all = local.clone(); |
|
| … | 3 unchanged lines |
||
| 157 | 199 | let mut sorted_local = local.clone(); |
|
| 158 | 200 | sorted_local.sort(); |
|
| 159 | 201 | if heads == sorted_local { |
|
| 160 | - | return Ok(MergeReport { heads, changed: false, checked_out: false }); |
|
| 202 | + | return Ok(MergeReport { |
|
| 203 | + | heads, |
|
| 204 | + | changed: false, |
|
| 205 | + | checked_out: false, |
|
| 206 | + | }); |
|
| 161 | 207 | } |
|
| 162 | 208 | let mut view = op.view.clone(); |
|
| 163 | - | view.bookmarks.insert(current, self.store.put_state(&StateObject::new(heads.clone()))?); |
|
| 209 | + | view.bookmarks.insert( |
|
| 210 | + | current, |
|
| 211 | + | self.store.put_state(&StateObject::new(heads.clone()))?, |
|
| 212 | + | ); |
|
| 164 | 213 | self.record_op(&[op_ref], &format!("oxy merge {name}"), view)?; |
|
| 165 | 214 | let snap = self.snapshot("oxy merge")?; |
|
| 166 | 215 | let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty(); |
|
| … | 4 unchanged lines |
||
| 171 | 220 | self.checkout(&heads, false, "oxy merge (checkout)")?; |
|
| 172 | 221 | checked_out = true; |
|
| 173 | 222 | } |
|
| 174 | - | Ok(MergeReport { heads, changed: true, checked_out }) |
|
| 223 | + | Ok(MergeReport { |
|
| 224 | + | heads, |
|
| 225 | + | changed: true, |
|
| 226 | + | checked_out, |
|
| 227 | + | }) |
|
| 175 | 228 | } |
|
| 176 | 229 | } |
modified crates/oxy-core/src/repo/checkout.rs +47 −11
| … | 14 unchanged lines |
||
| 15 | 15 | /// Move the working copy to the state whose heads are `heads` (spec §12.3): |
|
| 16 | 16 | /// the working directory is rewritten to that state and a fresh |
|
| 17 | 17 | /// working-copy change is started on it. The bookmark does not move. |
|
| 18 | - | pub fn checkout(&self, heads: &[ChangeHash], discard: bool, command: &str) -> Result<CheckoutReport> { |
|
| 18 | + | pub fn checkout( |
|
| 19 | + | &self, |
|
| 20 | + | heads: &[ChangeHash], |
|
| 21 | + | discard: bool, |
|
| 22 | + | command: &str, |
|
| 23 | + | ) -> Result<CheckoutReport> { |
|
| 19 | 24 | let snap = self.snapshot(command)?; |
|
| 20 | 25 | if !discard && (!snap.wc.tree_ops.is_empty() || !snap.wc.edits.is_empty()) { |
|
| 21 | 26 | return fail("the working copy has changes; commit them first or pass --discard"); |
|
| … | 11 unchanged lines |
||
| 33 | 38 | for (path, entry) in ¤t { |
|
| 34 | 39 | if !target.files.contains_key(path) { |
|
| 35 | 40 | if entry.kind == NodeKind::Subrepo { |
|
| 36 | - | report.notes.push(format!("{path}: nested repository left in place")); |
|
| 41 | + | report |
|
| 42 | + | .notes |
|
| 43 | + | .push(format!("{path}: nested repository left in place")); |
|
| 37 | 44 | continue; |
|
| 38 | 45 | } |
|
| 39 | 46 | let full = self.safe_path(path)?; |
|
| … | 13 unchanged lines |
||
| 53 | 60 | FileContent::Dir => continue, |
|
| 54 | 61 | FileContent::Text { bytes, .. } => { |
|
| 55 | 62 | let a = attrs.lookup(path); |
|
| 56 | - | let on_disk: std::borrow::Cow<[u8]> = if a.eol == Some(crate::attributes::Eol::Crlf) { crate::attributes::to_crlf(bytes).into() } else { bytes.as_slice().into() }; |
|
| 57 | - | let same = current.get(path).is_some_and(|c| c.kind == NodeKind::File && c.content == *on_disk && c.exec == f.mode.exec); |
|
| 63 | + | let on_disk: std::borrow::Cow<[u8]> = |
|
| 64 | + | if a.eol == Some(crate::attributes::Eol::Crlf) { |
|
| 65 | + | crate::attributes::to_crlf(bytes).into() |
|
| 66 | + | } else { |
|
| 67 | + | bytes.as_slice().into() |
|
| 68 | + | }; |
|
| 69 | + | let same = current.get(path).is_some_and(|c| { |
|
| 70 | + | c.kind == NodeKind::File && c.content == *on_disk && c.exec == f.mode.exec |
|
| 71 | + | }); |
|
| 58 | 72 | if !same { |
|
| 59 | 73 | self.write_file(path, &on_disk, f.mode.exec)?; |
|
| 60 | 74 | report.written += 1; |
|
| … | 17 unchanged lines |
||
| 78 | 92 | continue; |
|
| 79 | 93 | } |
|
| 80 | 94 | if f.kind == NodeKind::Symlink { |
|
| 81 | - | let same = current.get(path).is_some_and(|c| c.kind == NodeKind::Symlink && c.content == bytes); |
|
| 95 | + | let same = current |
|
| 96 | + | .get(path) |
|
| 97 | + | .is_some_and(|c| c.kind == NodeKind::Symlink && c.content == bytes); |
|
| 82 | 98 | if !same { |
|
| 83 | 99 | self.write_symlink(path, &bytes)?; |
|
| 84 | 100 | report.written += 1; |
|
| 85 | 101 | } |
|
| 86 | 102 | } else { |
|
| 87 | - | let same = current.get(path).is_some_and(|c| c.kind == NodeKind::File && c.content == bytes && c.exec == f.mode.exec); |
|
| 103 | + | let same = current.get(path).is_some_and(|c| { |
|
| 104 | + | c.kind == NodeKind::File && c.content == bytes && c.exec == f.mode.exec |
|
| 105 | + | }); |
|
| 88 | 106 | if !same { |
|
| 89 | 107 | self.write_file(path, &bytes, f.mode.exec)?; |
|
| 90 | 108 | report.written += 1; |
|
| … | 4 unchanged lines |
||
| 95 | 113 | } |
|
| 96 | 114 | if blobs.len() > 1 { |
|
| 97 | 115 | for (i, b) in blobs.iter().enumerate().skip(1) { |
|
| 98 | - | self.write_file(&format!("{path}.oxy-conflict-{}", i + 1), &self.read_blob(b)?, f.mode.exec)?; |
|
| 116 | + | self.write_file( |
|
| 117 | + | &format!("{path}.oxy-conflict-{}", i + 1), |
|
| 118 | + | &self.read_blob(b)?, |
|
| 119 | + | f.mode.exec, |
|
| 120 | + | )?; |
|
| 99 | 121 | } |
|
| 100 | 122 | } |
|
| 101 | 123 | } |
|
| … | 2 unchanged lines |
||
| 104 | 126 | // Prune directories left empty, deepest first. |
|
| 105 | 127 | for d in dirs_to_prune.iter().rev() { |
|
| 106 | 128 | let mut d = d.clone(); |
|
| 107 | - | while d != self.root && fs::read_dir(&d).map(|mut i| i.next().is_none()).unwrap_or(false) { |
|
| 129 | + | while d != self.root |
|
| 130 | + | && fs::read_dir(&d) |
|
| 131 | + | .map(|mut i| i.next().is_none()) |
|
| 132 | + | .unwrap_or(false) |
|
| 133 | + | { |
|
| 108 | 134 | fs::remove_dir(&d)?; |
|
| 109 | 135 | let Some(p) = d.parent() else { break }; |
|
| 110 | 136 | d = p.to_path_buf(); |
|
| … | 11 unchanged lines |
||
| 122 | 148 | .files |
|
| 123 | 149 | .iter() |
|
| 124 | 150 | .filter_map(|(p, f)| match &f.content { |
|
| 125 | - | FileContent::Opaque { blobs } if f.kind == NodeKind::File && blobs.len() == 1 => Some((p.clone(), blobs[0])), |
|
| 151 | + | FileContent::Opaque { blobs } if f.kind == NodeKind::File && blobs.len() == 1 => { |
|
| 152 | + | Some((p.clone(), blobs[0])) |
|
| 153 | + | } |
|
| 126 | 154 | _ => None, |
|
| 127 | 155 | }) |
|
| 128 | 156 | .collect(); |
|
| … | 1 unchanged lines |
||
| 130 | 158 | self.write_manifest_entries(report.conflicted, false, &entries, &blobs)?; |
|
| 131 | 159 | let _ = self.run_hook( |
|
| 132 | 160 | "post-checkout", |
|
| 133 | - | &format!("{{\"event\":\"post-checkout\",\"heads\":{},\"written\":{},\"removed\":{}}}", json_array(heads.iter().map(|h| json_str(&h.to_hex()))), report.written, report.removed), |
|
| 161 | + | &format!( |
|
| 162 | + | "{{\"event\":\"post-checkout\",\"heads\":{},\"written\":{},\"removed\":{}}}", |
|
| 163 | + | json_array(heads.iter().map(|h| json_str(&h.to_hex()))), |
|
| 164 | + | report.written, |
|
| 165 | + | report.removed |
|
| 166 | + | ), |
|
| 134 | 167 | ); |
|
| 135 | 168 | Ok(report) |
|
| 136 | 169 | } |
|
| … | 14 unchanged lines |
||
| 151 | 184 | full.push(comp); |
|
| 152 | 185 | if let Ok(m) = fs::symlink_metadata(&full) { |
|
| 153 | 186 | if m.file_type().is_symlink() { |
|
| 154 | - | return fail(format!("refusing to write through symlink {}", full.display())); |
|
| 187 | + | return fail(format!( |
|
| 188 | + | "refusing to write through symlink {}", |
|
| 189 | + | full.display() |
|
| 190 | + | )); |
|
| 155 | 191 | } |
|
| 156 | 192 | } |
|
| 157 | 193 | } else { |
|
| … | 43 unchanged lines |
modified crates/oxy-core/src/repo/checkpoint.rs +21 −5
| … | 18 unchanged lines |
||
| 19 | 19 | pub fn latest_checkpoint(&self, bookmark: &str) -> Result<Option<(ObjectRef, Checkpoint)>> { |
|
| 20 | 20 | match fs::read_to_string(self.checkpoint_pointer(bookmark)) { |
|
| 21 | 21 | Ok(h) => { |
|
| 22 | - | let r = ObjectRef::from_hex(h.trim()).ok_or_else(|| RepoError::Msg("corrupt checkpoint pointer".into()))?; |
|
| 22 | + | let r = ObjectRef::from_hex(h.trim()) |
|
| 23 | + | .ok_or_else(|| RepoError::Msg("corrupt checkpoint pointer".into()))?; |
|
| 23 | 24 | match self.store.get_typed(&r, ObjectType::Checkpoint) { |
|
| 24 | 25 | Ok(bytes) => Ok(Some((r, Checkpoint::decode(&bytes)?))), |
|
| 25 | 26 | Err(StoreError::NotFound(_)) => Ok(None), |
|
| … | 22 unchanged lines |
||
| 48 | 49 | } |
|
| 49 | 50 | ||
| 50 | 51 | /// Write a checkpoint of `state` (whose heads are `heads`) for `bookmark`. |
|
| 51 | - | pub(crate) fn write_checkpoint(&self, bookmark: &str, state: &State, heads: &[ChangeHash]) -> Result<(ObjectRef, usize)> { |
|
| 52 | + | pub(crate) fn write_checkpoint( |
|
| 53 | + | &self, |
|
| 54 | + | bookmark: &str, |
|
| 55 | + | state: &State, |
|
| 56 | + | heads: &[ChangeHash], |
|
| 57 | + | ) -> Result<(ObjectRef, usize)> { |
|
| 52 | 58 | let payload = state.checkpoint_payload(); |
|
| 53 | 59 | let batch = self.store.begin_batch()?; |
|
| 54 | 60 | let blob = self.store_blob(&payload)?; |
|
| 55 | - | let cp = Checkpoint { heads: StateObject::new(heads.to_vec()).heads, count: state.len() as u64, payload: blob }; |
|
| 61 | + | let cp = Checkpoint { |
|
| 62 | + | heads: StateObject::new(heads.to_vec()).heads, |
|
| 63 | + | count: state.len() as u64, |
|
| 64 | + | payload: blob, |
|
| 65 | + | }; |
|
| 56 | 66 | let r = self.store.put(ObjectType::Checkpoint, &cp.encode())?; |
|
| 57 | 67 | batch.finish()?; |
|
| 58 | 68 | fs::create_dir_all(self.oxy.join("checkpoints"))?; |
|
| … | 33 unchanged lines |
||
| 92 | 102 | } else { |
|
| 93 | 103 | let payload = state.checkpoint_payload(); |
|
| 94 | 104 | let blob = self.store_blob(&payload)?; |
|
| 95 | - | let cp = Checkpoint { heads: wanted.clone(), count: state.len() as u64, payload: blob }; |
|
| 105 | + | let cp = Checkpoint { |
|
| 106 | + | heads: wanted.clone(), |
|
| 107 | + | count: state.len() as u64, |
|
| 108 | + | payload: blob, |
|
| 109 | + | }; |
|
| 96 | 110 | self.store.put(ObjectType::Checkpoint, &cp.encode())? |
|
| 97 | 111 | } |
|
| 98 | 112 | } |
|
| … | 20 unchanged lines |
||
| 119 | 133 | } |
|
| 120 | 134 | } |
|
| 121 | 135 | for b in bookmarks { |
|
| 122 | - | let Some((_, cp)) = self.latest_checkpoint(&b)? else { continue }; |
|
| 136 | + | let Some((_, cp)) = self.latest_checkpoint(&b)? else { |
|
| 137 | + | continue; |
|
| 138 | + | }; |
|
| 123 | 139 | let payload = self.read_blob(&cp.payload)?; |
|
| 124 | 140 | let base = State::from_checkpoint_payload(&payload)?; |
|
| 125 | 141 | let cp_heads: BTreeSet<ChangeHash> = cp.heads.iter().copied().collect(); |
|
| … | 31 unchanged lines |
modified crates/oxy-core/src/repo/config.rs +35 −10
| … | 42 unchanged lines |
||
| 43 | 43 | Config { |
|
| 44 | 44 | repo_layer: BTreeMap::new(), |
|
| 45 | 45 | author_name: std::env::var("OXY_AUTHOR_NAME").unwrap_or_else(|_| user.clone()), |
|
| 46 | - | author_email: std::env::var("OXY_AUTHOR_EMAIL").unwrap_or_else(|_| format!("{user}@localhost")), |
|
| 46 | + | author_email: std::env::var("OXY_AUTHOR_EMAIL") |
|
| 47 | + | .unwrap_or_else(|_| format!("{user}@localhost")), |
|
| 47 | 48 | author_key: None, |
|
| 48 | 49 | sign_changes: false, |
|
| 49 | 50 | bookmark: "main".into(), |
|
| … | 44 unchanged lines |
||
| 94 | 95 | self.author_key = if value.is_empty() { |
|
| 95 | 96 | None |
|
| 96 | 97 | } else { |
|
| 97 | - | Some(KeyId::from_hex(value).ok_or_else(|| RepoError::Msg("author.key must be a 64-character hex KeyId".into()))?) |
|
| 98 | + | Some(KeyId::from_hex(value).ok_or_else(|| { |
|
| 99 | + | RepoError::Msg("author.key must be a 64-character hex KeyId".into()) |
|
| 100 | + | })?) |
|
| 98 | 101 | } |
|
| 99 | 102 | } |
|
| 100 | 103 | "core.sign" => { |
|
| … | 6 unchanged lines |
||
| 107 | 110 | "workspace.bookmark" => self.bookmark = value.into(), |
|
| 108 | 111 | "core.op-retention" => self.op_retention_days = parse_days(value)?, |
|
| 109 | 112 | "core.gc-grace" => self.gc_grace_days = parse_days(value)?, |
|
| 110 | - | "filter.max-blob" => self.max_blob = if value.is_empty() { None } else { Some(parse_size(value)?) }, |
|
| 113 | + | "filter.max-blob" => { |
|
| 114 | + | self.max_blob = if value.is_empty() { |
|
| 115 | + | None |
|
| 116 | + | } else { |
|
| 117 | + | Some(parse_size(value)?) |
|
| 118 | + | } |
|
| 119 | + | } |
|
| 111 | 120 | "hooks.semantic-deps" => self.semantic_deps = matches!(value, "true" | "1" | "yes"), |
|
| 112 | 121 | "filter.paths" => { |
|
| 113 | 122 | self.sparse_paths = value |
|
| 114 | 123 | .split(',') |
|
| 115 | - | .map(|p| p.trim().trim_start_matches("./").trim_matches('/').to_string()) |
|
| 124 | + | .map(|p| { |
|
| 125 | + | p.trim() |
|
| 126 | + | .trim_start_matches("./") |
|
| 127 | + | .trim_matches('/') |
|
| 128 | + | .to_string() |
|
| 129 | + | }) |
|
| 116 | 130 | .filter(|p| !p.is_empty()) |
|
| 117 | 131 | .collect(); |
|
| 118 | 132 | } |
|
| 119 | 133 | "core.checkpoint-interval" => { |
|
| 120 | - | self.checkpoint_interval = value.parse().map_err(|_| RepoError::Msg(format!("expected a number, got {value:?}")))?; |
|
| 134 | + | self.checkpoint_interval = value |
|
| 135 | + | .parse() |
|
| 136 | + | .map_err(|_| RepoError::Msg(format!("expected a number, got {value:?}")))?; |
|
| 121 | 137 | } |
|
| 122 | 138 | k if k.starts_with("remote.") => { |
|
| 123 | 139 | let rest = &k["remote.".len()..]; |
|
| … | 59 unchanged lines |
||
| 183 | 199 | Some('G' | 'g') => (&v[..v.len() - 1], 1u64 << 30), |
|
| 184 | 200 | _ => (v, 1), |
|
| 185 | 201 | }; |
|
| 186 | - | let n: u64 = num.trim().parse().map_err(|_| RepoError::Msg(format!("expected a size like 1M, got {v:?}")))?; |
|
| 202 | + | let n: u64 = num |
|
| 203 | + | .trim() |
|
| 204 | + | .parse() |
|
| 205 | + | .map_err(|_| RepoError::Msg(format!("expected a size like 1M, got {v:?}")))?; |
|
| 187 | 206 | Ok(n * mult) |
|
| 188 | 207 | } |
|
| 189 | 208 | ||
| 190 | 209 | /// A count of days, optionally suffixed `d`. |
|
| 191 | 210 | fn parse_days(v: &str) -> Result<u64> { |
|
| 192 | - | v.trim_end_matches('d').parse().map_err(|_| RepoError::Msg(format!("expected a number of days, got {v:?}"))) |
|
| 211 | + | v.trim_end_matches('d') |
|
| 212 | + | .parse() |
|
| 213 | + | .map_err(|_| RepoError::Msg(format!("expected a number of days, got {v:?}"))) |
|
| 193 | 214 | } |
|
| 194 | 215 | ||
| 195 | 216 | /// Read `key = "value"` lines; a missing file is empty. |
|
| … | 9 unchanged lines |
||
| 205 | 226 | if line.is_empty() || line.starts_with('#') { |
|
| 206 | 227 | continue; |
|
| 207 | 228 | } |
|
| 208 | - | let Some((k, v)) = line.split_once('=') else { continue }; |
|
| 229 | + | let Some((k, v)) = line.split_once('=') else { |
|
| 230 | + | continue; |
|
| 231 | + | }; |
|
| 209 | 232 | out.insert(k.trim().to_string(), v.trim().trim_matches('"').to_string()); |
|
| 210 | 233 | } |
|
| 211 | 234 | Ok(out) |
|
| 212 | 235 | } |
|
| 213 | 236 | ||
| 214 | 237 | fn write_config_file(path: &Path, entries: &BTreeMap<String, String>) -> Result<()> { |
|
| 215 | - | let text: String = entries.iter().map(|(k, v)| format!("{k} = \"{v}\"\n")).collect(); |
|
| 238 | + | let text: String = entries |
|
| 239 | + | .iter() |
|
| 240 | + | .map(|(k, v)| format!("{k} = \"{v}\"\n")) |
|
| 241 | + | .collect(); |
|
| 216 | 242 | write_atomic(path, text.as_bytes()) |
|
| 217 | 243 | } |
|
| 218 | - |
modified crates/oxy-core/src/repo/gc.rs +14 −3
| … | 25 unchanged lines |
||
| 26 | 26 | pub fn gc(&self, now: bool) -> Result<GcReport> { |
|
| 27 | 27 | let packed = self.store.pack_loose()?; |
|
| 28 | 28 | let keep = self.reachable()?; |
|
| 29 | - | let grace = if now { std::time::Duration::ZERO } else { std::time::Duration::from_secs(self.config.gc_grace_days * 86_400) }; |
|
| 29 | + | let grace = if now { |
|
| 30 | + | std::time::Duration::ZERO |
|
| 31 | + | } else { |
|
| 32 | + | std::time::Duration::from_secs(self.config.gc_grace_days * 86_400) |
|
| 33 | + | }; |
|
| 30 | 34 | let pruned = self.store.prune(&keep, grace)?; |
|
| 31 | 35 | let merged_packs = self.store.merge_packs()?; |
|
| 32 | - | Ok(GcReport { packed, pruned, reachable: keep.len(), merged_packs }) |
|
| 36 | + | Ok(GcReport { |
|
| 37 | + | packed, |
|
| 38 | + | pruned, |
|
| 39 | + | reachable: keep.len(), |
|
| 40 | + | merged_packs, |
|
| 41 | + | }) |
|
| 33 | 42 | } |
|
| 34 | 43 | ||
| 35 | 44 | /// Every object that must survive garbage collection. |
|
| … | 5 unchanged lines |
||
| 41 | 50 | let mut policies: Vec<ObjectRef> = Vec::new(); |
|
| 42 | 51 | ||
| 43 | 52 | // Operations: always kept; views within retention are roots. |
|
| 44 | - | let cutoff = Hlc::now().wall_ms.saturating_sub(self.config.op_retention_days * 86_400_000); |
|
| 53 | + | let cutoff = Hlc::now() |
|
| 54 | + | .wall_ms |
|
| 55 | + | .saturating_sub(self.config.op_retention_days * 86_400_000); |
|
| 45 | 56 | for (i, (r, op)) in self.op_log()?.into_iter().enumerate() { |
|
| 46 | 57 | keep.insert(r); |
|
| 47 | 58 | if i == 0 || op.hlc.wall_ms >= cutoff { |
|
| … | 89 unchanged lines |
modified crates/oxy-core/src/repo/git.rs +287 −55
| … | 38 unchanged lines |
||
| 39 | 39 | .map_err(|e| RepoError::Msg(format!("running git cat-file: {e}")))?; |
|
| 40 | 40 | let stdin = child.stdin.take().unwrap(); |
|
| 41 | 41 | let stdout = std::io::BufReader::new(child.stdout.take().unwrap()); |
|
| 42 | - | Ok(CatFile { child, stdin, stdout }) |
|
| 42 | + | Ok(CatFile { |
|
| 43 | + | child, |
|
| 44 | + | stdin, |
|
| 45 | + | stdout, |
|
| 46 | + | }) |
|
| 43 | 47 | } |
|
| 44 | 48 | ||
| 45 | 49 | fn read(&mut self, sha: &str) -> Result<Vec<u8>> { |
|
| … | 6 unchanged lines |
||
| 52 | 56 | if parts.len() != 3 { |
|
| 53 | 57 | return fail(format!("git cat-file: object {sha} not found")); |
|
| 54 | 58 | } |
|
| 55 | - | let size: usize = parts[2].parse().map_err(|_| RepoError::Msg("git cat-file: bad size".into()))?; |
|
| 59 | + | let size: usize = parts[2] |
|
| 60 | + | .parse() |
|
| 61 | + | .map_err(|_| RepoError::Msg("git cat-file: bad size".into()))?; |
|
| 56 | 62 | let mut body = vec![0u8; size]; |
|
| 57 | 63 | self.stdout.read_exact(&mut body)?; |
|
| 58 | 64 | let mut nl = [0u8; 1]; |
|
| … | 22 unchanged lines |
||
| 81 | 87 | let out = Command::new("git") |
|
| 82 | 88 | .arg("-C") |
|
| 83 | 89 | .arg(git_dir) |
|
| 84 | - | .args(["log", "--reverse", "--topo-order", "--format=%H%x00%P%x00%at%x00%an <%ae>%x00%B%x1e", branch]) |
|
| 90 | + | .args([ |
|
| 91 | + | "log", |
|
| 92 | + | "--reverse", |
|
| 93 | + | "--topo-order", |
|
| 94 | + | "--format=%H%x00%P%x00%at%x00%an <%ae>%x00%B%x1e", |
|
| 95 | + | branch, |
|
| 96 | + | ]) |
|
| 85 | 97 | .output() |
|
| 86 | 98 | .map_err(|e| RepoError::Msg(format!("running git log: {e}")))?; |
|
| 87 | 99 | if !out.status.success() { |
|
| 88 | - | return fail(format!("git log {branch} failed: {}", String::from_utf8_lossy(&out.stderr).trim())); |
|
| 100 | + | return fail(format!( |
|
| 101 | + | "git log {branch} failed: {}", |
|
| 102 | + | String::from_utf8_lossy(&out.stderr).trim() |
|
| 103 | + | )); |
|
| 89 | 104 | } |
|
| 90 | 105 | let text = String::from_utf8_lossy(&out.stdout); |
|
| 91 | 106 | let mut commits = Vec::new(); |
|
| … | 25 unchanged lines |
||
| 117 | 132 | if entry.is_empty() { |
|
| 118 | 133 | continue; |
|
| 119 | 134 | } |
|
| 120 | - | let Some((meta, path)) = entry.split_once('\t') else { continue }; |
|
| 135 | + | let Some((meta, path)) = entry.split_once('\t') else { |
|
| 136 | + | continue; |
|
| 137 | + | }; |
|
| 121 | 138 | let m: Vec<&str> = meta.split(' ').collect(); |
|
| 122 | 139 | if m.len() == 3 { |
|
| 123 | 140 | out.insert(path.to_string(), (m[0].to_string(), m[2].to_string())); |
|
| … | 35 unchanged lines |
||
| 159 | 176 | } |
|
| 160 | 177 | ||
| 161 | 178 | fn git(dir: &Path, args: &[&str]) -> Result<String> { |
|
| 162 | - | let out = Command::new("git").arg("-C").arg(dir).args(args).output().map_err(|e| RepoError::Msg(format!("running git: {e}")))?; |
|
| 179 | + | let out = Command::new("git") |
|
| 180 | + | .arg("-C") |
|
| 181 | + | .arg(dir) |
|
| 182 | + | .args(args) |
|
| 183 | + | .output() |
|
| 184 | + | .map_err(|e| RepoError::Msg(format!("running git: {e}")))?; |
|
| 163 | 185 | if !out.status.success() { |
|
| 164 | - | return fail(format!("git {} failed: {}", args.join(" "), String::from_utf8_lossy(&out.stderr).trim())); |
|
| 186 | + | return fail(format!( |
|
| 187 | + | "git {} failed: {}", |
|
| 188 | + | args.join(" "), |
|
| 189 | + | String::from_utf8_lossy(&out.stderr).trim() |
|
| 190 | + | )); |
|
| 165 | 191 | } |
|
| 166 | 192 | Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) |
|
| 167 | 193 | } |
|
| 168 | 194 | ||
| 169 | 195 | impl Repo { |
|
| 170 | 196 | fn git_marks_dir(&self, git_dir: &Path) -> Result<PathBuf> { |
|
| 171 | - | let canonical = git_dir.canonicalize().unwrap_or_else(|_| git_dir.to_path_buf()); |
|
| 197 | + | let canonical = git_dir |
|
| 198 | + | .canonicalize() |
|
| 199 | + | .unwrap_or_else(|_| git_dir.to_path_buf()); |
|
| 172 | 200 | let key = hash_object(ObjectType::Chunk, canonical.to_string_lossy().as_bytes()).short(16); |
|
| 173 | 201 | let dir = self.oxy.join("git").join(key); |
|
| 174 | 202 | fs::create_dir_all(&dir)?; |
|
| … | 20 unchanged lines |
||
| 195 | 223 | let path = self.git_marks_dir(git_dir)?.join("git-states"); |
|
| 196 | 224 | let mut states = Self::read_git_states(&path)?; |
|
| 197 | 225 | states.insert(*state, sha.to_string()); |
|
| 198 | - | let text: String = states.iter().map(|(h, s)| format!("{} {s}\n", h.to_hex())).collect(); |
|
| 226 | + | let text: String = states |
|
| 227 | + | .iter() |
|
| 228 | + | .map(|(h, s)| format!("{} {s}\n", h.to_hex())) |
|
| 229 | + | .collect(); |
|
| 199 | 230 | write_atomic(&path, text.as_bytes()) |
|
| 200 | 231 | } |
|
| 201 | 232 | ||
| … | 15 unchanged lines |
||
| 217 | 248 | if e.is_working_copy { |
|
| 218 | 249 | continue; |
|
| 219 | 250 | } |
|
| 220 | - | if let Some(sha) = e.change.message.lines().rev().find_map(|l| l.strip_prefix("Git-Commit: ")) { |
|
| 251 | + | if let Some(sha) = e |
|
| 252 | + | .change |
|
| 253 | + | .message |
|
| 254 | + | .lines() |
|
| 255 | + | .rev() |
|
| 256 | + | .find_map(|l| l.strip_prefix("Git-Commit: ")) |
|
| 257 | + | { |
|
| 221 | 258 | map.insert(sha.trim().to_string(), e.hash); |
|
| 222 | 259 | } |
|
| 223 | 260 | } |
|
| … | 8 unchanged lines |
||
| 232 | 269 | Ok(_) => Ok(()), |
|
| 233 | 270 | Err(e) => { |
|
| 234 | 271 | let ls = git(mirror, &["ls-remote", "--heads", url, branch]).unwrap_or_default(); |
|
| 235 | - | if ls.trim().is_empty() { Ok(()) } else { Err(e) } |
|
| 272 | + | if ls.trim().is_empty() { |
|
| 273 | + | Ok(()) |
|
| 274 | + | } else { |
|
| 275 | + | Err(e) |
|
| 276 | + | } |
|
| 236 | 277 | } |
|
| 237 | 278 | } |
|
| 238 | 279 | } |
|
| … | 5 unchanged lines |
||
| 244 | 285 | pub fn git_fetch(&self, url: &str, branch: &str) -> Result<GitImportReport> { |
|
| 245 | 286 | let mirror = self.git_mirror(url)?; |
|
| 246 | 287 | self.git_mirror_fetch(&mirror, url, branch)?; |
|
| 247 | - | if git(&mirror, &["rev-parse", "--verify", "-q", &format!("refs/heads/{branch}")]).is_err() { |
|
| 288 | + | if git( |
|
| 289 | + | &mirror, |
|
| 290 | + | &[ |
|
| 291 | + | "rev-parse", |
|
| 292 | + | "--verify", |
|
| 293 | + | "-q", |
|
| 294 | + | &format!("refs/heads/{branch}"), |
|
| 295 | + | ], |
|
| 296 | + | ) |
|
| 297 | + | .is_err() |
|
| 298 | + | { |
|
| 248 | 299 | return fail(format!("{url} has no branch {branch:?}")); |
|
| 249 | 300 | } |
|
| 250 | 301 | self.import_git_commits(&mirror, branch, &format!("oxy git fetch {url} {branch}")) |
|
| … | 5 unchanged lines |
||
| 256 | 307 | pub fn git_push(&self, url: &str, branch: &str) -> Result<GitExportReport> { |
|
| 257 | 308 | let mirror = self.git_mirror(url)?; |
|
| 258 | 309 | self.git_mirror_fetch(&mirror, url, branch)?; |
|
| 259 | - | if let Ok(tip) = git(&mirror, &["rev-parse", "--verify", "-q", &format!("refs/heads/{branch}")]) { |
|
| 310 | + | if let Ok(tip) = git( |
|
| 311 | + | &mirror, |
|
| 312 | + | &[ |
|
| 313 | + | "rev-parse", |
|
| 314 | + | "--verify", |
|
| 315 | + | "-q", |
|
| 316 | + | &format!("refs/heads/{branch}"), |
|
| 317 | + | ], |
|
| 318 | + | ) { |
|
| 260 | 319 | let known = self.git_known(&mirror)?; |
|
| 261 | 320 | if !known.imported.contains_key(&tip) && !known.exported.contains_key(&tip) { |
|
| 262 | - | return fail(format!("{url} {branch} has commits not yet fetched; run `oxy git fetch {url}` first")); |
|
| 321 | + | return fail(format!( |
|
| 322 | + | "{url} {branch} has commits not yet fetched; run `oxy git fetch {url}` first" |
|
| 323 | + | )); |
|
| 263 | 324 | } |
|
| 264 | 325 | } |
|
| 265 | 326 | let report = self.git_export_as(&mirror, branch)?; |
|
| … | 42 unchanged lines |
||
| 308 | 369 | ||
| 309 | 370 | let bookmark = self.config.bookmark.clone(); |
|
| 310 | 371 | let mut stream: Vec<u8> = Vec::new(); |
|
| 311 | - | let mut report = GitExportReport { branch: branch.to_string(), heads: self.bookmark_heads()?.len(), ..Default::default() }; |
|
| 372 | + | let mut report = GitExportReport { |
|
| 373 | + | branch: branch.to_string(), |
|
| 374 | + | heads: self.bookmark_heads()?.len(), |
|
| 375 | + | ..Default::default() |
|
| 376 | + | }; |
|
| 312 | 377 | ||
| 313 | 378 | // States along the bookmark, oldest first, skipping unchanged and pruned ones. |
|
| 314 | 379 | let mut ops = self.op_log()?; |
|
| … | 2 unchanged lines |
||
| 317 | 382 | // The fast-import parent of the next commit: `:mark` or a commit sha. |
|
| 318 | 383 | let mut last_parent: Option<String> = None; |
|
| 319 | 384 | for (_, op) in &ops { |
|
| 320 | - | let Some(state_ref) = op.view.bookmarks.get(&bookmark).copied() else { continue }; |
|
| 385 | + | let Some(state_ref) = op.view.bookmarks.get(&bookmark).copied() else { |
|
| 386 | + | continue; |
|
| 387 | + | }; |
|
| 321 | 388 | if prev.as_ref().is_some_and(|(p, _)| *p == state_ref) { |
|
| 322 | 389 | continue; |
|
| 323 | 390 | } |
|
| 324 | - | let Ok(state) = self.store.get_state(&state_ref) else { continue }; |
|
| 391 | + | let Ok(state) = self.store.get_state(&state_ref) else { |
|
| 392 | + | continue; |
|
| 393 | + | }; |
|
| 325 | 394 | if state.heads.is_empty() { |
|
| 326 | 395 | prev = Some((state_ref, vec![])); |
|
| 327 | 396 | continue; |
|
| … | 11 unchanged lines |
||
| 339 | 408 | continue; |
|
| 340 | 409 | } |
|
| 341 | 410 | let prev_heads = prev.as_ref().map(|(_, h)| h.clone()).unwrap_or_default(); |
|
| 342 | - | let added = self.negotiate(&prev_heads, &state.heads).unwrap_or_default(); |
|
| 411 | + | let added = self |
|
| 412 | + | .negotiate(&prev_heads, &state.heads) |
|
| 413 | + | .unwrap_or_default(); |
|
| 343 | 414 | let loaded = self.load_state(&state.heads)?; |
|
| 344 | 415 | let snapshot = loaded.materialize(); |
|
| 345 | 416 | let (author, secs, mut message) = match added.as_slice() { |
|
| … | 3 unchanged lines |
||
| 349 | 420 | if !m.ends_with('\n') { |
|
| 350 | 421 | m.push('\n'); |
|
| 351 | 422 | } |
|
| 352 | - | m.push_str(&format!("\nChange-Id: {}\nOxy-Hash: {}\n", c.change_id.to_hex(), h.to_hex())); |
|
| 423 | + | m.push_str(&format!( |
|
| 424 | + | "\nChange-Id: {}\nOxy-Hash: {}\n", |
|
| 425 | + | c.change_id.to_hex(), |
|
| 426 | + | h.to_hex() |
|
| 427 | + | )); |
|
| 353 | 428 | (self.git_author(&c), c.hlc.wall_ms / 1000, m) |
|
| 354 | 429 | } |
|
| 355 | - | [] => ("oxy <oxy@localhost>".to_string(), op.hlc.wall_ms / 1000, format!("oxy: {}\n\n", op.command)), |
|
| 430 | + | [] => ( |
|
| 431 | + | "oxy <oxy@localhost>".to_string(), |
|
| 432 | + | op.hlc.wall_ms / 1000, |
|
| 433 | + | format!("oxy: {}\n\n", op.command), |
|
| 434 | + | ), |
|
| 356 | 435 | many => { |
|
| 357 | 436 | let mut m = format!("oxy: {} ({} changes)\n\n", op.command, many.len()); |
|
| 358 | 437 | for h in many { |
|
| 359 | 438 | let c = self.store.get_change(h)?; |
|
| 360 | - | m.push_str(&format!("- {} {}\n", c.change_id.short(8), c.message.lines().next().unwrap_or(""))); |
|
| 439 | + | m.push_str(&format!( |
|
| 440 | + | "- {} {}\n", |
|
| 441 | + | c.change_id.short(8), |
|
| 442 | + | c.message.lines().next().unwrap_or("") |
|
| 443 | + | )); |
|
| 361 | 444 | } |
|
| 362 | - | m.push_str(&format!("\nOxy-Changes: {}\n", many.iter().map(|h| h.to_hex()).collect::<Vec<_>>().join(" "))); |
|
| 445 | + | m.push_str(&format!( |
|
| 446 | + | "\nOxy-Changes: {}\n", |
|
| 447 | + | many.iter() |
|
| 448 | + | .map(|h| h.to_hex()) |
|
| 449 | + | .collect::<Vec<_>>() |
|
| 450 | + | .join(" ") |
|
| 451 | + | )); |
|
| 363 | 452 | ("oxy <oxy@localhost>".to_string(), op.hlc.wall_ms / 1000, m) |
|
| 364 | 453 | } |
|
| 365 | 454 | }; |
|
| … | 4 unchanged lines |
||
| 370 | 459 | let mark = next_mark; |
|
| 371 | 460 | next_mark += 1; |
|
| 372 | 461 | let parents: Vec<String> = last_parent.clone().into_iter().collect(); |
|
| 373 | - | self.write_commit(&mut stream, "refs/oxy/export", mark, &author, secs, &message, &parents, &snapshot)?; |
|
| 462 | + | self.write_commit( |
|
| 463 | + | &mut stream, |
|
| 464 | + | "refs/oxy/export", |
|
| 465 | + | mark, |
|
| 466 | + | &author, |
|
| 467 | + | secs, |
|
| 468 | + | &message, |
|
| 469 | + | &parents, |
|
| 470 | + | &snapshot, |
|
| 471 | + | )?; |
|
| 374 | 472 | marks.insert(state_ref, mark); |
|
| 375 | 473 | last_parent = Some(format!(":{mark}")); |
|
| 376 | 474 | prev = Some((state_ref, state.heads.clone())); |
|
| … | 13 unchanged lines |
||
| 390 | 488 | let snapshot = loaded.materialize(); |
|
| 391 | 489 | let mark = next_mark; |
|
| 392 | 490 | next_mark += 1; |
|
| 393 | - | let message = format!("oxy: tag {}\n\n{}Oxy-State: {}\n", t.name, if t.message.is_empty() { String::new() } else { format!("{}\n\n", t.message) }, t.state.to_hex()); |
|
| 394 | - | self.write_commit(&mut stream, "refs/oxy/export", mark, "oxy <oxy@localhost>", Hlc::now().wall_ms / 1000, &message, &[], &snapshot)?; |
|
| 491 | + | let message = format!( |
|
| 492 | + | "oxy: tag {}\n\n{}Oxy-State: {}\n", |
|
| 493 | + | t.name, |
|
| 494 | + | if t.message.is_empty() { |
|
| 495 | + | String::new() |
|
| 496 | + | } else { |
|
| 497 | + | format!("{}\n\n", t.message) |
|
| 498 | + | }, |
|
| 499 | + | t.state.to_hex() |
|
| 500 | + | ); |
|
| 501 | + | self.write_commit( |
|
| 502 | + | &mut stream, |
|
| 503 | + | "refs/oxy/export", |
|
| 504 | + | mark, |
|
| 505 | + | "oxy <oxy@localhost>", |
|
| 506 | + | Hlc::now().wall_ms / 1000, |
|
| 507 | + | &message, |
|
| 508 | + | &[], |
|
| 509 | + | &snapshot, |
|
| 510 | + | )?; |
|
| 395 | 511 | marks.insert(t.state, mark); |
|
| 396 | 512 | report.exported += 1; |
|
| 397 | 513 | format!(":{mark}") |
|
| 398 | 514 | } |
|
| 399 | 515 | }; |
|
| 400 | - | stream.extend_from_slice(format!("reset refs/tags/{}\nfrom {target}\n\n", t.name).as_bytes()); |
|
| 516 | + | stream.extend_from_slice( |
|
| 517 | + | format!("reset refs/tags/{}\nfrom {target}\n\n", t.name).as_bytes(), |
|
| 518 | + | ); |
|
| 401 | 519 | } |
|
| 402 | 520 | stream.extend_from_slice(b"done\n"); |
|
| 403 | 521 | ||
| 404 | 522 | let mut cmd = Command::new("git"); |
|
| 405 | 523 | // `--force`: the scratch ref may jump between unrelated states. |
|
| 406 | - | cmd.arg("-C").arg(git_dir).args(["fast-import", "--quiet", "--done", "--force"]).arg(format!("--export-marks={}", git_marks.display())); |
|
| 524 | + | cmd.arg("-C") |
|
| 525 | + | .arg(git_dir) |
|
| 526 | + | .args(["fast-import", "--quiet", "--done", "--force"]) |
|
| 527 | + | .arg(format!("--export-marks={}", git_marks.display())); |
|
| 407 | 528 | if git_marks.exists() { |
|
| 408 | 529 | cmd.arg(format!("--import-marks={}", git_marks.display())); |
|
| 409 | 530 | } |
|
| 410 | - | let mut child = cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::piped()).spawn().map_err(|e| RepoError::Msg(format!("running git fast-import: {e}")))?; |
|
| 531 | + | let mut child = cmd |
|
| 532 | + | .stdin(Stdio::piped()) |
|
| 533 | + | .stdout(Stdio::null()) |
|
| 534 | + | .stderr(Stdio::piped()) |
|
| 535 | + | .spawn() |
|
| 536 | + | .map_err(|e| RepoError::Msg(format!("running git fast-import: {e}")))?; |
|
| 411 | 537 | child.stdin.take().unwrap().write_all(&stream)?; |
|
| 412 | 538 | let out = child.wait_with_output()?; |
|
| 413 | 539 | if !out.status.success() { |
|
| 414 | - | return fail(format!("git fast-import failed: {}", String::from_utf8_lossy(&out.stderr).trim())); |
|
| 540 | + | return fail(format!( |
|
| 541 | + | "git fast-import failed: {}", |
|
| 542 | + | String::from_utf8_lossy(&out.stderr).trim() |
|
| 543 | + | )); |
|
| 415 | 544 | } |
|
| 416 | - | let text: String = marks.iter().map(|(h, m)| format!("{} {m}\n", h.to_hex())).collect(); |
|
| 545 | + | let text: String = marks |
|
| 546 | + | .iter() |
|
| 547 | + | .map(|(h, m)| format!("{} {m}\n", h.to_hex())) |
|
| 548 | + | .collect(); |
|
| 417 | 549 | write_atomic(&oxy_marks_path, text.as_bytes())?; |
|
| 418 | 550 | let _ = git(git_dir, &["update-ref", "-d", "refs/oxy/export"]); |
|
| 419 | 551 | report.skipped_subrepos = self.git_export_skipped.replace(0); |
|
| … | 8 unchanged lines |
||
| 428 | 560 | if !self.bookmark_heads()?.is_empty() { |
|
| 429 | 561 | return fail("git import needs an empty bookmark; import into a fresh repository"); |
|
| 430 | 562 | } |
|
| 431 | - | self.import_git_commits(git_dir, branch, &format!("oxy git import {} {branch}", git_dir.display())) |
|
| 563 | + | self.import_git_commits( |
|
| 564 | + | git_dir, |
|
| 565 | + | branch, |
|
| 566 | + | &format!("oxy git import {} {branch}", git_dir.display()), |
|
| 567 | + | ) |
|
| 432 | 568 | } |
|
| 433 | 569 | ||
| 434 | 570 | /// Import the commits of `branch` that the bookmark does not have yet, |
|
| 435 | 571 | /// then move the bookmark to the union of its heads and theirs. |
|
| 436 | - | fn import_git_commits(&self, git_dir: &Path, branch: &str, command: &str) -> Result<GitImportReport> { |
|
| 572 | + | fn import_git_commits( |
|
| 573 | + | &self, |
|
| 574 | + | git_dir: &Path, |
|
| 575 | + | branch: &str, |
|
| 576 | + | command: &str, |
|
| 577 | + | ) -> Result<GitImportReport> { |
|
| 437 | 578 | let batch = self.store.begin_batch()?; |
|
| 438 | 579 | let result = self.import_git_commits_batched(git_dir, branch, command); |
|
| 439 | 580 | batch.finish()?; |
|
| 440 | 581 | result |
|
| 441 | 582 | } |
|
| 442 | 583 | ||
| 443 | - | fn import_git_commits_batched(&self, git_dir: &Path, branch: &str, command: &str) -> Result<GitImportReport> { |
|
| 584 | + | fn import_git_commits_batched( |
|
| 585 | + | &self, |
|
| 586 | + | git_dir: &Path, |
|
| 587 | + | branch: &str, |
|
| 588 | + | command: &str, |
|
| 589 | + | ) -> Result<GitImportReport> { |
|
| 444 | 590 | let commits = list_commits(git_dir, branch)?; |
|
| 445 | 591 | let mut cat = CatFile::start(git_dir)?; |
|
| 446 | 592 | let mut report = GitImportReport::default(); |
|
| … | 63 unchanged lines |
||
| 510 | 656 | // pins, and `.gitmodules` also yields `.oxymodules`. |
|
| 511 | 657 | if !gitlinks.is_empty() { |
|
| 512 | 658 | report.submodules += gitlinks.len(); |
|
| 513 | - | let gm = entries.get(".gitmodules").map(|e| crate::subrepo::Modules::from_gitmodules(&String::from_utf8_lossy(&e.content))).unwrap_or_default(); |
|
| 514 | - | let mut modules = entries.get(".oxymodules").map(|e| crate::subrepo::Modules::parse(&String::from_utf8_lossy(&e.content))).unwrap_or_default(); |
|
| 659 | + | let gm = entries |
|
| 660 | + | .get(".gitmodules") |
|
| 661 | + | .map(|e| { |
|
| 662 | + | crate::subrepo::Modules::from_gitmodules(&String::from_utf8_lossy( |
|
| 663 | + | &e.content, |
|
| 664 | + | )) |
|
| 665 | + | }) |
|
| 666 | + | .unwrap_or_default(); |
|
| 667 | + | let mut modules = entries |
|
| 668 | + | .get(".oxymodules") |
|
| 669 | + | .map(|e| crate::subrepo::Modules::parse(&String::from_utf8_lossy(&e.content))) |
|
| 670 | + | .unwrap_or_default(); |
|
| 515 | 671 | for (path, sha) in &gitlinks { |
|
| 516 | 672 | let m = gm.entries.get(path); |
|
| 517 | - | let branch = m.and_then(|m| m.bookmark.clone()).unwrap_or_else(|| "HEAD".into()); |
|
| 518 | - | entries.insert(path.clone(), FsEntry::new(NodeKind::Subrepo, false, crate::subrepo::Pin::Git { sha: sha.clone(), branch }.encode())); |
|
| 673 | + | let branch = m |
|
| 674 | + | .and_then(|m| m.bookmark.clone()) |
|
| 675 | + | .unwrap_or_else(|| "HEAD".into()); |
|
| 676 | + | entries.insert( |
|
| 677 | + | path.clone(), |
|
| 678 | + | FsEntry::new( |
|
| 679 | + | NodeKind::Subrepo, |
|
| 680 | + | false, |
|
| 681 | + | crate::subrepo::Pin::Git { |
|
| 682 | + | sha: sha.clone(), |
|
| 683 | + | branch, |
|
| 684 | + | } |
|
| 685 | + | .encode(), |
|
| 686 | + | ), |
|
| 687 | + | ); |
|
| 519 | 688 | if let Some(m) = m { |
|
| 520 | 689 | modules.entries.insert(path.clone(), m.clone()); |
|
| 521 | 690 | } |
|
| 522 | 691 | } |
|
| 523 | 692 | if !modules.entries.is_empty() { |
|
| 524 | - | entries.insert(".oxymodules".into(), FsEntry::new(NodeKind::File, false, modules.to_text().into_bytes())); |
|
| 693 | + | entries.insert( |
|
| 694 | + | ".oxymodules".into(), |
|
| 695 | + | FsEntry::new(NodeKind::File, false, modules.to_text().into_bytes()), |
|
| 696 | + | ); |
|
| 525 | 697 | } |
|
| 526 | 698 | } |
|
| 527 | 699 | let built = self.build_from_entries(&base, &base_snapshot, HashMap::new(), &entries)?; |
|
| … | 2 unchanged lines |
||
| 530 | 702 | if !message.ends_with('\n') { |
|
| 531 | 703 | message.push('\n'); |
|
| 532 | 704 | } |
|
| 533 | - | message.push_str(&format!("\nGit-Commit: {}\nGit-Author: {}\n", gc.sha, gc.author)); |
|
| 705 | + | message.push_str(&format!( |
|
| 706 | + | "\nGit-Commit: {}\nGit-Author: {}\n", |
|
| 707 | + | gc.sha, gc.author |
|
| 708 | + | )); |
|
| 534 | 709 | let mut change = Change { |
|
| 535 | 710 | change_id: change_id_for(&gc.sha, &gc.message), |
|
| 536 | 711 | deps: vec![], |
|
| 537 | 712 | author: ObjectRef::ZERO, |
|
| 538 | - | hlc: Hlc { wall_ms: gc.author_secs * 1000, logical: 0 }, |
|
| 713 | + | hlc: Hlc { |
|
| 714 | + | wall_ms: gc.author_secs * 1000, |
|
| 715 | + | logical: 0, |
|
| 716 | + | }, |
|
| 539 | 717 | message, |
|
| 540 | 718 | tree_ops: built.tree_ops, |
|
| 541 | 719 | edits: built.edits, |
|
| 542 | - | kind: if built.resolves { ChangeKind::Resolution } else { ChangeKind::Normal }, |
|
| 720 | + | kind: if built.resolves { |
|
| 721 | + | ChangeKind::Resolution |
|
| 722 | + | } else { |
|
| 723 | + | ChangeKind::Normal |
|
| 724 | + | }, |
|
| 543 | 725 | }; |
|
| 544 | 726 | deps.extend(referenced_changes(&change, &base)); |
|
| 545 | 727 | change.deps = deps.into_iter().collect(); |
|
| … | 22 unchanged lines |
||
| 568 | 750 | let mut view = op.view.clone(); |
|
| 569 | 751 | let old_state_ref = view.bookmarks.get(&self.config.bookmark).copied(); |
|
| 570 | 752 | let state_ref = self.store.put_state(&StateObject::new(heads.clone()))?; |
|
| 571 | - | view.bookmarks.insert(self.config.bookmark.clone(), state_ref); |
|
| 753 | + | view.bookmarks |
|
| 754 | + | .insert(self.config.bookmark.clone(), state_ref); |
|
| 572 | 755 | self.record_op(&[op_ref], command, view)?; |
|
| 573 | 756 | // The new state equals the tip's tree when the old one was itself a |
|
| 574 | 757 | // Git commit's tree (fetched or exported), or there was none. |
|
| 575 | - | let old_known = old_heads.is_empty() || old_state_ref.is_some_and(|s| known.exported.values().any(|e| *e == s)); |
|
| 758 | + | let old_known = old_heads.is_empty() |
|
| 759 | + | || old_state_ref.is_some_and(|s| known.exported.values().any(|e| *e == s)); |
|
| 576 | 760 | if let (Some(tip), true) = (commits.last(), old_known) { |
|
| 577 | 761 | self.record_git_state(git_dir, &state_ref, &tip.sha)?; |
|
| 578 | 762 | } |
|
| … | 17 unchanged lines |
||
| 596 | 780 | /// Add to `st` the changes of every Git commit `rev-list args` lists: |
|
| 597 | 781 | /// an imported commit's change, or the whole state an exported commit |
|
| 598 | 782 | /// was made from (which may hold changes Git never saw). |
|
| 599 | - | fn add_git_ancestors(&self, st: &mut State, git_dir: &Path, args: &[&str], known: &GitKnown) -> Result<()> { |
|
| 783 | + | fn add_git_ancestors( |
|
| 784 | + | &self, |
|
| 785 | + | st: &mut State, |
|
| 786 | + | git_dir: &Path, |
|
| 787 | + | args: &[&str], |
|
| 788 | + | known: &GitKnown, |
|
| 789 | + | ) -> Result<()> { |
|
| 600 | 790 | let mut full = vec!["rev-list"]; |
|
| 601 | 791 | full.extend(args); |
|
| 602 | 792 | let out = git(git_dir, &full)?; |
|
| … | 40 unchanged lines |
||
| 643 | 833 | ||
| 644 | 834 | fn git_author(&self, c: &Change) -> String { |
|
| 645 | 835 | match self.store.get_identity(&c.author) { |
|
| 646 | - | Ok(id) if !id.name.is_empty() => format!("{} <{}>", id.name.replace(['<', '>', '\n'], " "), id.email.replace(['<', '>', '\n', ' '], "")), |
|
| 836 | + | Ok(id) if !id.name.is_empty() => format!( |
|
| 837 | + | "{} <{}>", |
|
| 838 | + | id.name.replace(['<', '>', '\n'], " "), |
|
| 839 | + | id.email.replace(['<', '>', '\n', ' '], "") |
|
| 840 | + | ), |
|
| 647 | 841 | _ => "oxy <oxy@localhost>".to_string(), |
|
| 648 | 842 | } |
|
| 649 | 843 | } |
|
| 650 | 844 | ||
| 651 | 845 | #[allow(clippy::too_many_arguments)] |
|
| 652 | - | fn write_commit(&self, out: &mut Vec<u8>, reference: &str, mark: u64, author: &str, secs: u64, message: &str, parents: &[String], snapshot: &Snapshot) -> Result<()> { |
|
| 846 | + | fn write_commit( |
|
| 847 | + | &self, |
|
| 848 | + | out: &mut Vec<u8>, |
|
| 849 | + | reference: &str, |
|
| 850 | + | mark: u64, |
|
| 851 | + | author: &str, |
|
| 852 | + | secs: u64, |
|
| 853 | + | message: &str, |
|
| 854 | + | parents: &[String], |
|
| 855 | + | snapshot: &Snapshot, |
|
| 856 | + | ) -> Result<()> { |
|
| 653 | 857 | out.extend_from_slice(format!("commit {reference}\nmark :{mark}\nauthor {author} {secs} +0000\ncommitter {author} {secs} +0000\ndata {}\n{message}\n", message.len()).as_bytes()); |
|
| 654 | 858 | for (i, p) in parents.iter().enumerate() { |
|
| 655 | - | out.extend_from_slice(format!("{} {p}\n", if i == 0 { "from" } else { "merge" }).as_bytes()); |
|
| 859 | + | out.extend_from_slice( |
|
| 860 | + | format!("{} {p}\n", if i == 0 { "from" } else { "merge" }).as_bytes(), |
|
| 861 | + | ); |
|
| 656 | 862 | } |
|
| 657 | 863 | out.extend_from_slice(b"deleteall\n"); |
|
| 658 | 864 | let has_gitmodules = snapshot.files.contains_key(".gitmodules"); |
|
| … | 2 unchanged lines |
||
| 661 | 867 | // Git reads `.gitmodules`; derive it unless the tree has one. |
|
| 662 | 868 | if !has_gitmodules { |
|
| 663 | 869 | if let FileContent::Text { bytes, .. } = &f.content { |
|
| 664 | - | let gm = crate::subrepo::Modules::parse(&String::from_utf8_lossy(bytes)).to_gitmodules(); |
|
| 870 | + | let gm = crate::subrepo::Modules::parse(&String::from_utf8_lossy(bytes)) |
|
| 871 | + | .to_gitmodules(); |
|
| 665 | 872 | if !gm.is_empty() { |
|
| 666 | - | out.extend_from_slice(format!("M 100644 inline .gitmodules\ndata {}\n", gm.len()).as_bytes()); |
|
| 873 | + | out.extend_from_slice( |
|
| 874 | + | format!("M 100644 inline .gitmodules\ndata {}\n", gm.len()) |
|
| 875 | + | .as_bytes(), |
|
| 876 | + | ); |
|
| 667 | 877 | out.extend_from_slice(gm.as_bytes()); |
|
| 668 | 878 | out.push(b'\n'); |
|
| 669 | 879 | } |
|
| … | 5 unchanged lines |
||
| 675 | 885 | if let FileContent::Opaque { blobs } = &f.content { |
|
| 676 | 886 | if let Some(b) = blobs.first() { |
|
| 677 | 887 | match crate::subrepo::Pin::decode(&self.read_blob(b)?)? { |
|
| 678 | - | crate::subrepo::Pin::Git { sha, .. } => out.extend_from_slice(format!("M 160000 {sha} {path}\n").as_bytes()), |
|
| 679 | - | crate::subrepo::Pin::Oxy { .. } => self.git_export_skipped.set(self.git_export_skipped.get() + 1), |
|
| 888 | + | crate::subrepo::Pin::Git { sha, .. } => { |
|
| 889 | + | out.extend_from_slice(format!("M 160000 {sha} {path}\n").as_bytes()) |
|
| 890 | + | } |
|
| 891 | + | crate::subrepo::Pin::Oxy { .. } => self |
|
| 892 | + | .git_export_skipped |
|
| 893 | + | .set(self.git_export_skipped.get() + 1), |
|
| 680 | 894 | } |
|
| 681 | 895 | } |
|
| 682 | 896 | } |
|
| … | 1 unchanged lines |
||
| 684 | 898 | } |
|
| 685 | 899 | let (mode, bytes): (&str, Vec<u8>) = match (&f.kind, &f.content) { |
|
| 686 | 900 | (NodeKind::Dir, _) => continue, |
|
| 687 | - | (NodeKind::Symlink, FileContent::Opaque { blobs }) => ("120000", blobs.first().map(|b| self.read_blob(b)).transpose()?.unwrap_or_default()), |
|
| 688 | - | (_, FileContent::Text { bytes, .. }) => (if f.mode.exec { "100755" } else { "100644" }, bytes.clone()), |
|
| 689 | - | (_, FileContent::Opaque { blobs }) => (if f.mode.exec { "100755" } else { "100644" }, blobs.first().map(|b| self.read_blob(b)).transpose()?.unwrap_or_default()), |
|
| 901 | + | (NodeKind::Symlink, FileContent::Opaque { blobs }) => ( |
|
| 902 | + | "120000", |
|
| 903 | + | blobs |
|
| 904 | + | .first() |
|
| 905 | + | .map(|b| self.read_blob(b)) |
|
| 906 | + | .transpose()? |
|
| 907 | + | .unwrap_or_default(), |
|
| 908 | + | ), |
|
| 909 | + | (_, FileContent::Text { bytes, .. }) => { |
|
| 910 | + | (if f.mode.exec { "100755" } else { "100644" }, bytes.clone()) |
|
| 911 | + | } |
|
| 912 | + | (_, FileContent::Opaque { blobs }) => ( |
|
| 913 | + | if f.mode.exec { "100755" } else { "100644" }, |
|
| 914 | + | blobs |
|
| 915 | + | .first() |
|
| 916 | + | .map(|b| self.read_blob(b)) |
|
| 917 | + | .transpose()? |
|
| 918 | + | .unwrap_or_default(), |
|
| 919 | + | ), |
|
| 690 | 920 | (_, FileContent::Dir) => continue, |
|
| 691 | 921 | }; |
|
| 692 | - | out.extend_from_slice(format!("M {mode} inline {path}\ndata {}\n", bytes.len()).as_bytes()); |
|
| 922 | + | out.extend_from_slice( |
|
| 923 | + | format!("M {mode} inline {path}\ndata {}\n", bytes.len()).as_bytes(), |
|
| 924 | + | ); |
|
| 693 | 925 | out.extend_from_slice(&bytes); |
|
| 694 | 926 | out.push(b'\n'); |
|
| 695 | 927 | } |
|
| … | 4 unchanged lines |
modified crates/oxy-core/src/repo/history.rs +443 −90
| … | 28 unchanged lines |
||
| 29 | 29 | /// Replace head `old` with `new` (or with nothing) on the bookmark and |
|
| 30 | 30 | /// restart the working copy on the resulting base. Files on disk are not |
|
| 31 | 31 | /// touched: the next snapshot diffs them against the new base. |
|
| 32 | - | fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> { |
|
| 32 | + | fn replace_head( |
|
| 33 | + | &self, |
|
| 34 | + | old: &ChangeHash, |
|
| 35 | + | new: Option<&Change>, |
|
| 36 | + | command: &str, |
|
| 37 | + | ) -> Result<Option<ChangeHash>> { |
|
| 33 | 38 | let new_hash = match new { |
|
| 34 | 39 | Some(c) => Some(self.store_signed(c)?), |
|
| 35 | 40 | None => None, |
|
| … | 28 unchanged lines |
||
| 64 | 69 | let wc_hash = self.store.put_change(&fresh)?; |
|
| 65 | 70 | ||
| 66 | 71 | let mut view = op.view.clone(); |
|
| 67 | - | view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?); |
|
| 72 | + | view.bookmarks.insert( |
|
| 73 | + | bookmark, |
|
| 74 | + | self.store.put_state(&StateObject::new(new_heads))?, |
|
| 75 | + | ); |
|
| 68 | 76 | view.workspaces.insert(self.workspace, wc_hash); |
|
| 69 | 77 | self.record_op(&[op_ref], command, view)?; |
|
| 70 | 78 | self.drop_manifest(); |
|
| … | 3 unchanged lines |
||
| 74 | 82 | /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state, |
|
| 75 | 83 | /// rewriting every dependent, including the working copy. Returns the |
|
| 76 | 84 | /// new hash of `old`, if any. |
|
| 77 | - | fn rewrite_change(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<Option<ChangeHash>> { |
|
| 78 | - | self.rewrite_change_noted(old, new, corr, command).map(|(h, _)| h) |
|
| 85 | + | fn rewrite_change( |
|
| 86 | + | &self, |
|
| 87 | + | old: &ChangeHash, |
|
| 88 | + | new: Option<&Change>, |
|
| 89 | + | corr: &Correspondence, |
|
| 90 | + | command: &str, |
|
| 91 | + | ) -> Result<Option<ChangeHash>> { |
|
| 92 | + | self.rewrite_change_noted(old, new, corr, command) |
|
| 93 | + | .map(|(h, _)| h) |
|
| 79 | 94 | } |
|
| 80 | 95 | ||
| 81 | 96 | /// Like `rewrite_change`, also returning notes about reified references. |
|
| 82 | - | fn rewrite_change_noted(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<(Option<ChangeHash>, Vec<String>)> { |
|
| 97 | + | fn rewrite_change_noted( |
|
| 98 | + | &self, |
|
| 99 | + | old: &ChangeHash, |
|
| 100 | + | new: Option<&Change>, |
|
| 101 | + | corr: &Correspondence, |
|
| 102 | + | command: &str, |
|
| 103 | + | ) -> Result<(Option<ChangeHash>, Vec<String>)> { |
|
| 83 | 104 | let (op_ref, op) = self.head_op()?; |
|
| 84 | 105 | let bookmark = self.config.bookmark.clone(); |
|
| 85 | 106 | let heads = self.heads_of(&op.view, &bookmark)?; |
|
| … | 1 unchanged lines |
||
| 87 | 108 | let mut all = heads.clone(); |
|
| 88 | 109 | all.push(wc_hash); |
|
| 89 | 110 | let state = self.load_state(&all)?; |
|
| 90 | - | if !state.closure(&wc_hash).contains(old) && !heads.contains(old) && !state.heads().contains(old) && !state.contains(old) { |
|
| 91 | - | return fail(format!("change #{} is not in {} or under the working copy", old.short(12), bookmark)); |
|
| 111 | + | if !state.closure(&wc_hash).contains(old) |
|
| 112 | + | && !heads.contains(old) |
|
| 113 | + | && !state.heads().contains(old) |
|
| 114 | + | && !state.contains(old) |
|
| 115 | + | { |
|
| 116 | + | return fail(format!( |
|
| 117 | + | "change #{} is not in {} or under the working copy", |
|
| 118 | + | old.short(12), |
|
| 119 | + | bookmark |
|
| 120 | + | )); |
|
| 92 | 121 | } |
|
| 93 | 122 | let old_change = self.store.get_change(old)?; |
|
| 94 | 123 | if old_change.kind == ChangeKind::WorkingCopy { |
|
| 95 | 124 | return fail("that is the working copy; commit it instead"); |
|
| 96 | 125 | } |
|
| 97 | 126 | let load = |h: &ChangeHash| self.store.get_change(h).ok(); |
|
| 98 | - | let propagation = propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(e.to_string()))?; |
|
| 127 | + | let propagation = |
|
| 128 | + | propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(e.to_string()))?; |
|
| 99 | 129 | let rewritten = propagation.changes; |
|
| 100 | 130 | ||
| 101 | 131 | let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new(); |
|
| … | 6 unchanged lines |
||
| 108 | 138 | None => None, |
|
| 109 | 139 | }; |
|
| 110 | 140 | for (h, c) in &rewritten { |
|
| 111 | - | let nh = if c.kind == ChangeKind::WorkingCopy { self.store.put_change(c)? } else { self.store_signed(c)? }; |
|
| 141 | + | let nh = if c.kind == ChangeKind::WorkingCopy { |
|
| 142 | + | self.store.put_change(c)? |
|
| 143 | + | } else { |
|
| 144 | + | self.store_signed(c)? |
|
| 145 | + | }; |
|
| 112 | 146 | mapping.insert(*h, nh); |
|
| 113 | 147 | } |
|
| 114 | 148 | let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> { |
|
| … | 13 unchanged lines |
||
| 128 | 162 | let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash); |
|
| 129 | 163 | ||
| 130 | 164 | let mut view = op.view.clone(); |
|
| 131 | - | view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?); |
|
| 165 | + | view.bookmarks.insert( |
|
| 166 | + | bookmark, |
|
| 167 | + | self.store.put_state(&StateObject::new(new_heads))?, |
|
| 168 | + | ); |
|
| 132 | 169 | view.workspaces.insert(self.workspace, new_wc); |
|
| 133 | 170 | self.record_op(&[op_ref], command, view)?; |
|
| 134 | 171 | self.drop_manifest(); |
|
| … | 16 unchanged lines |
||
| 151 | 188 | c.message = message.trim_end().to_string(); |
|
| 152 | 189 | c.hlc = Hlc::now(); |
|
| 153 | 190 | let id = c.change_id; |
|
| 154 | - | Ok(self.rewrite_change(hash, Some(&c), &corr, &format!("oxy describe {} -m {message:?}", id.short(8)))?.unwrap()) |
|
| 191 | + | Ok(self |
|
| 192 | + | .rewrite_change( |
|
| 193 | + | hash, |
|
| 194 | + | Some(&c), |
|
| 195 | + | &corr, |
|
| 196 | + | &format!("oxy describe {} -m {message:?}", id.short(8)), |
|
| 197 | + | )? |
|
| 198 | + | .unwrap()) |
|
| 155 | 199 | } |
|
| 156 | 200 | ||
| 157 | 201 | /// Fold the working copy into the head it sits on: the head is rebuilt |
|
| 158 | 202 | /// as the difference between the state beneath it and the files on disk. |
|
| 159 | 203 | pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> { |
|
| 160 | 204 | let snap = self.snapshot("oxy amend")?; |
|
| 161 | - | if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) { |
|
| 205 | + | if snap |
|
| 206 | + | .statuses |
|
| 207 | + | .iter() |
|
| 208 | + | .any(|s| s.status == ChangeStatus::ConflictEdited) |
|
| 209 | + | { |
|
| 162 | 210 | return fail("a file still contains conflict markers"); |
|
| 163 | 211 | } |
|
| 164 | 212 | let base_heads = snap.base.maximal(&snap.wc.deps); |
|
| 165 | 213 | let [head] = base_heads.as_slice() else { |
|
| 166 | - | return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len())); |
|
| 214 | + | return fail(format!( |
|
| 215 | + | "the working copy sits on {} heads; check out the single change to amend", |
|
| 216 | + | base_heads.len() |
|
| 217 | + | )); |
|
| 167 | 218 | }; |
|
| 168 | 219 | let head = *head; |
|
| 169 | 220 | let old = self.rewritable_head(&head)?; |
|
| 170 | 221 | if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() { |
|
| 171 | - | return fail("nothing to amend: the working copy is clean and no new message was given"); |
|
| 222 | + | return fail( |
|
| 223 | + | "nothing to amend: the working copy is clean and no new message was given", |
|
| 224 | + | ); |
|
| 172 | 225 | } |
|
| 173 | 226 | // Rebuild against the state beneath the head. |
|
| 174 | 227 | let beneath = self.load_state(&old.deps)?; |
|
| … | 10 unchanged lines |
||
| 185 | 238 | deps: vec![], |
|
| 186 | 239 | author: self.author(), |
|
| 187 | 240 | hlc: Hlc::now(), |
|
| 188 | - | message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()), |
|
| 241 | + | message: message |
|
| 242 | + | .map(|m| m.trim_end().to_string()) |
|
| 243 | + | .unwrap_or(old.message.clone()), |
|
| 189 | 244 | tree_ops: built.tree_ops, |
|
| 190 | 245 | edits: built.edits, |
|
| 191 | - | kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal }, |
|
| 246 | + | kind: if built.resolves || old.kind == ChangeKind::Resolution { |
|
| 247 | + | ChangeKind::Resolution |
|
| 248 | + | } else { |
|
| 249 | + | ChangeKind::Normal |
|
| 250 | + | }, |
|
| 192 | 251 | }; |
|
| 193 | 252 | deps.extend(referenced_changes(&new, &beneath)); |
|
| 194 | 253 | new.deps = deps.into_iter().collect(); |
|
| 195 | 254 | new.normalize(); |
|
| 196 | - | Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap()) |
|
| 255 | + | Ok(self |
|
| 256 | + | .replace_head( |
|
| 257 | + | &head, |
|
| 258 | + | Some(&new), |
|
| 259 | + | &format!("oxy amend {}", old.change_id.short(8)), |
|
| 260 | + | )? |
|
| 261 | + | .unwrap()) |
|
| 197 | 262 | } |
|
| 198 | 263 | ||
| 199 | 264 | /// Drop a change from the bookmark. Dependents inherit its dependencies; |
|
| … | 10 unchanged lines |
||
| 210 | 275 | self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?; |
|
| 211 | 276 | Ok(vec![]) |
|
| 212 | 277 | } else { |
|
| 213 | - | let (_, notes) = self.rewrite_change_noted(hash, None, &Correspondence::default(), &format!("oxy abandon {}", c.change_id.short(8)))?; |
|
| 278 | + | let (_, notes) = self.rewrite_change_noted( |
|
| 279 | + | hash, |
|
| 280 | + | None, |
|
| 281 | + | &Correspondence::default(), |
|
| 282 | + | &format!("oxy abandon {}", c.change_id.short(8)), |
|
| 283 | + | )?; |
|
| 214 | 284 | Ok(notes) |
|
| 215 | 285 | } |
|
| 216 | 286 | } |
|
| … | 2 unchanged lines |
||
| 219 | 289 | /// Translate a reference to change `target`'s own atom or op into a self |
|
| 220 | 290 | /// reference, and a working-copy self reference into one offset past the |
|
| 221 | 291 | /// target's own count; other references pass through. |
|
| 222 | - | fn fold_atom(r: AtomRef, target: &ChangeHash, wc_offset: &HashMap<NodeId, u32>, node: &NodeId) -> Option<AtomRef> { |
|
| 292 | + | fn fold_atom( |
|
| 293 | + | r: AtomRef, |
|
| 294 | + | target: &ChangeHash, |
|
| 295 | + | wc_offset: &HashMap<NodeId, u32>, |
|
| 296 | + | node: &NodeId, |
|
| 297 | + | ) -> Option<AtomRef> { |
|
| 223 | 298 | match r { |
|
| 224 | - | AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom { change: ObjectRef::ZERO, index }), |
|
| 225 | - | AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom { change: ObjectRef::ZERO, index: index + wc_offset.get(node).copied().unwrap_or(0) }), |
|
| 299 | + | AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom { |
|
| 300 | + | change: ObjectRef::ZERO, |
|
| 301 | + | index, |
|
| 302 | + | }), |
|
| 303 | + | AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom { |
|
| 304 | + | change: ObjectRef::ZERO, |
|
| 305 | + | index: index + wc_offset.get(node).copied().unwrap_or(0), |
|
| 306 | + | }), |
|
| 226 | 307 | // References into the target's closure were validated by the caller and stay as they are. |
|
| 227 | 308 | other => Some(other), |
|
| 228 | 309 | } |
|
| … | 7 unchanged lines |
||
| 236 | 317 | /// atoms keep their indices. |
|
| 237 | 318 | pub fn amend_into(&self, target: &ChangeHash, message: Option<&str>) -> Result<ChangeHash> { |
|
| 238 | 319 | let snap = self.snapshot("oxy amend")?; |
|
| 239 | - | if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) { |
|
| 320 | + | if snap |
|
| 321 | + | .statuses |
|
| 322 | + | .iter() |
|
| 323 | + | .any(|s| s.status == ChangeStatus::ConflictEdited) |
|
| 324 | + | { |
|
| 240 | 325 | return fail("a file still contains conflict markers"); |
|
| 241 | 326 | } |
|
| 242 | 327 | let wc = &snap.wc; |
|
| 243 | 328 | if wc.tree_ops.is_empty() && wc.edits.is_empty() && message.is_none() { |
|
| 244 | - | return fail("nothing to amend: the working copy is clean and no new message was given"); |
|
| 329 | + | return fail( |
|
| 330 | + | "nothing to amend: the working copy is clean and no new message was given", |
|
| 331 | + | ); |
|
| 245 | 332 | } |
|
| 246 | 333 | let old = self.store.get_change(target)?; |
|
| 247 | 334 | if old.kind == ChangeKind::WorkingCopy { |
|
| … | 3 unchanged lines |
||
| 251 | 338 | let closure = self.load_state(&[*target])?; |
|
| 252 | 339 | let ok_change = |h: &ChangeHash| *h == *target || closure.contains(h); |
|
| 253 | 340 | let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some(); |
|
| 254 | - | let too_new = |what: &str| RepoError::Msg(format!("{what} was introduced after change #{}; amend into a later change instead", target.short(12))); |
|
| 341 | + | let too_new = |what: &str| { |
|
| 342 | + | RepoError::Msg(format!( |
|
| 343 | + | "{what} was introduced after change #{}; amend into a later change instead", |
|
| 344 | + | target.short(12) |
|
| 345 | + | )) |
|
| 346 | + | }; |
|
| 255 | 347 | for op in &wc.tree_ops { |
|
| 256 | 348 | match op { |
|
| 257 | 349 | TreeOp::AddNode { parent, .. } => { |
|
| … | 6 unchanged lines |
||
| 264 | 356 | return Err(too_new("a removed file")); |
|
| 265 | 357 | } |
|
| 266 | 358 | } |
|
| 267 | - | TreeOp::Move { node, old, parent, .. } => { |
|
| 359 | + | TreeOp::Move { |
|
| 360 | + | node, old, parent, .. |
|
| 361 | + | } => { |
|
| 268 | 362 | if !ok_node(node) || !ok_change(&old.change) || !ok_node(parent) { |
|
| 269 | 363 | return Err(too_new("a moved file")); |
|
| 270 | 364 | } |
|
| … | 5 unchanged lines |
||
| 276 | 370 | } |
|
| 277 | 371 | } |
|
| 278 | 372 | } |
|
| 279 | - | let wc_added: BTreeSet<NodeId> = wc.tree_ops.iter().filter_map(|o| if let TreeOp::AddNode { node, .. } = o { Some(*node) } else { None }).collect(); |
|
| 373 | + | let wc_added: BTreeSet<NodeId> = wc |
|
| 374 | + | .tree_ops |
|
| 375 | + | .iter() |
|
| 376 | + | .filter_map(|o| { |
|
| 377 | + | if let TreeOp::AddNode { node, .. } = o { |
|
| 378 | + | Some(*node) |
|
| 379 | + | } else { |
|
| 380 | + | None |
|
| 381 | + | } |
|
| 382 | + | }) |
|
| 383 | + | .collect(); |
|
| 280 | 384 | for e in &wc.edits { |
|
| 281 | 385 | let n = e.node(); |
|
| 282 | 386 | if !wc_added.contains(&n) && !ok_node(&n) { |
|
| 283 | 387 | return Err(too_new("an edited file")); |
|
| 284 | 388 | } |
|
| 285 | 389 | let refs: Vec<ChangeHash> = match e { |
|
| 286 | - | Edit::Insert { after, before, .. } => [after, before].iter().filter_map(|r| if let AtomRef::Atom { change, .. } = r { Some(*change) } else { None }).collect(), |
|
| 390 | + | Edit::Insert { after, before, .. } => [after, before] |
|
| 391 | + | .iter() |
|
| 392 | + | .filter_map(|r| { |
|
| 393 | + | if let AtomRef::Atom { change, .. } = r { |
|
| 394 | + | Some(*change) |
|
| 395 | + | } else { |
|
| 396 | + | None |
|
| 397 | + | } |
|
| 398 | + | }) |
|
| 399 | + | .collect(), |
|
| 287 | 400 | Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(), |
|
| 288 | 401 | Edit::Replace { .. } => vec![], |
|
| 289 | 402 | }; |
|
| … | 14 unchanged lines |
||
| 304 | 417 | let op_offset = old.tree_ops.len() as u32; |
|
| 305 | 418 | let fold_op = |r: OpRef| -> OpRef { |
|
| 306 | 419 | if r.change == *target { |
|
| 307 | - | OpRef { change: ObjectRef::ZERO, index: r.index } |
|
| 420 | + | OpRef { |
|
| 421 | + | change: ObjectRef::ZERO, |
|
| 422 | + | index: r.index, |
|
| 423 | + | } |
|
| 308 | 424 | } else if r.change.is_zero() { |
|
| 309 | - | OpRef { change: ObjectRef::ZERO, index: r.index + op_offset } |
|
| 425 | + | OpRef { |
|
| 426 | + | change: ObjectRef::ZERO, |
|
| 427 | + | index: r.index + op_offset, |
|
| 428 | + | } |
|
| 310 | 429 | } else { |
|
| 311 | 430 | r |
|
| 312 | 431 | } |
|
| … | 1 unchanged lines |
||
| 314 | 433 | let mut tree_ops = old.tree_ops.clone(); |
|
| 315 | 434 | for op in &wc.tree_ops { |
|
| 316 | 435 | tree_ops.push(match op { |
|
| 317 | - | TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: fold_op(*placement) }, |
|
| 318 | - | TreeOp::Move { node, old, parent, name } => TreeOp::Move { node: *node, old: fold_op(*old), parent: *parent, name: name.clone() }, |
|
| 319 | - | TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: fold_op(*old), mode: *mode }, |
|
| 436 | + | TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { |
|
| 437 | + | node: *node, |
|
| 438 | + | placement: fold_op(*placement), |
|
| 439 | + | }, |
|
| 440 | + | TreeOp::Move { |
|
| 441 | + | node, |
|
| 442 | + | old, |
|
| 443 | + | parent, |
|
| 444 | + | name, |
|
| 445 | + | } => TreeOp::Move { |
|
| 446 | + | node: *node, |
|
| 447 | + | old: fold_op(*old), |
|
| 448 | + | parent: *parent, |
|
| 449 | + | name: name.clone(), |
|
| 450 | + | }, |
|
| 451 | + | TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { |
|
| 452 | + | node: *node, |
|
| 453 | + | old: fold_op(*old), |
|
| 454 | + | mode: *mode, |
|
| 455 | + | }, |
|
| 320 | 456 | add => add.clone(), |
|
| 321 | 457 | }); |
|
| 322 | 458 | } |
|
| 323 | 459 | let mut edits = old.edits.clone(); |
|
| 324 | 460 | for e in &wc.edits { |
|
| 325 | 461 | edits.push(match e { |
|
| 326 | - | Edit::Insert { node, after, before, lines } => Edit::Insert { |
|
| 462 | + | Edit::Insert { |
|
| 463 | + | node, |
|
| 464 | + | after, |
|
| 465 | + | before, |
|
| 466 | + | lines, |
|
| 467 | + | } => Edit::Insert { |
|
| 327 | 468 | node: *node, |
|
| 328 | - | after: fold_atom(*after, target, &per_node, node).ok_or_else(|| too_new("an anchor line"))?, |
|
| 329 | - | before: fold_atom(*before, target, &per_node, node).ok_or_else(|| too_new("an anchor line"))?, |
|
| 469 | + | after: fold_atom(*after, target, &per_node, node) |
|
| 470 | + | .ok_or_else(|| too_new("an anchor line"))?, |
|
| 471 | + | before: fold_atom(*before, target, &per_node, node) |
|
| 472 | + | .ok_or_else(|| too_new("an anchor line"))?, |
|
| 330 | 473 | lines: lines.clone(), |
|
| 331 | 474 | }, |
|
| 332 | 475 | Edit::Delete { node, atoms } => Edit::Delete { |
|
| … | 2 unchanged lines |
||
| 335 | 478 | .iter() |
|
| 336 | 479 | .map(|r| { |
|
| 337 | 480 | if r.change == *target { |
|
| 338 | - | AtomRange { change: ObjectRef::ZERO, from: r.from, to: r.to } |
|
| 481 | + | AtomRange { |
|
| 482 | + | change: ObjectRef::ZERO, |
|
| 483 | + | from: r.from, |
|
| 484 | + | to: r.to, |
|
| 485 | + | } |
|
| 339 | 486 | } else if r.change.is_zero() { |
|
| 340 | 487 | let off = per_node.get(node).copied().unwrap_or(0); |
|
| 341 | - | AtomRange { change: ObjectRef::ZERO, from: r.from + off, to: r.to + off } |
|
| 488 | + | AtomRange { |
|
| 489 | + | change: ObjectRef::ZERO, |
|
| 490 | + | from: r.from + off, |
|
| 491 | + | to: r.to + off, |
|
| 492 | + | } |
|
| 342 | 493 | } else { |
|
| 343 | 494 | *r |
|
| 344 | 495 | } |
|
| … | 8 unchanged lines |
||
| 353 | 504 | deps: vec![], |
|
| 354 | 505 | author: self.author(), |
|
| 355 | 506 | hlc: Hlc::now(), |
|
| 356 | - | message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()), |
|
| 507 | + | message: message |
|
| 508 | + | .map(|m| m.trim_end().to_string()) |
|
| 509 | + | .unwrap_or(old.message.clone()), |
|
| 357 | 510 | tree_ops, |
|
| 358 | 511 | edits, |
|
| 359 | 512 | kind: old.kind, |
|
| 360 | 513 | }; |
|
| 361 | 514 | let beneath = self.load_state(&old.deps)?; |
|
| 362 | 515 | let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect(); |
|
| 363 | - | deps.extend(referenced_changes(&new, &beneath).into_iter().filter(|h| *h != *target)); |
|
| 364 | - | deps.extend(wc.deps.iter().filter(|d| closure.contains(d) && **d != *target).copied()); |
|
| 516 | + | deps.extend( |
|
| 517 | + | referenced_changes(&new, &beneath) |
|
| 518 | + | .into_iter() |
|
| 519 | + | .filter(|h| *h != *target), |
|
| 520 | + | ); |
|
| 521 | + | deps.extend( |
|
| 522 | + | wc.deps |
|
| 523 | + | .iter() |
|
| 524 | + | .filter(|d| closure.contains(d) && **d != *target) |
|
| 525 | + | .copied(), |
|
| 526 | + | ); |
|
| 365 | 527 | new.deps = deps.into_iter().collect(); |
|
| 366 | 528 | new.normalize(); |
|
| 367 | 529 | ||
| 368 | 530 | // Rewrite dependents (identity correspondence), then start a fresh, |
|
| 369 | 531 | // empty working copy: its edits now live in the amended change. |
|
| 370 | 532 | let corr = Correspondence::identity(&old); |
|
| 371 | - | let new_hash = self.rewrite_change(target, Some(&new), &corr, &format!("oxy amend --into {}", old.change_id.short(8)))?.unwrap(); |
|
| 533 | + | let new_hash = self |
|
| 534 | + | .rewrite_change( |
|
| 535 | + | target, |
|
| 536 | + | Some(&new), |
|
| 537 | + | &corr, |
|
| 538 | + | &format!("oxy amend --into {}", old.change_id.short(8)), |
|
| 539 | + | )? |
|
| 540 | + | .unwrap(); |
|
| 372 | 541 | let (op_ref, op) = self.head_op()?; |
|
| 373 | 542 | let heads = self.heads_of(&op.view, &self.config.bookmark)?; |
|
| 374 | 543 | let fresh = self.new_working_copy(heads, ""); |
|
| … | 22 unchanged lines |
||
| 397 | 566 | let beneath_snapshot = beneath.materialize(); |
|
| 398 | 567 | let full = self.load_state(&[*hash])?; |
|
| 399 | 568 | let full_snapshot = full.materialize(); |
|
| 400 | - | let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))); |
|
| 569 | + | let selected = |p: &str| { |
|
| 570 | + | paths |
|
| 571 | + | .iter() |
|
| 572 | + | .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))) |
|
| 573 | + | }; |
|
| 401 | 574 | // Entries of the intermediate state: selected paths as the head |
|
| 402 | 575 | // leaves them, everything else as beneath. |
|
| 403 | - | let entries_of = |snap: &Snapshot, keep_from_full: &dyn Fn(&str) -> bool| -> Result<BTreeMap<String, FsEntry>> { |
|
| 576 | + | let entries_of = |snap: &Snapshot, |
|
| 577 | + | keep_from_full: &dyn Fn(&str) -> bool| |
|
| 578 | + | -> Result<BTreeMap<String, FsEntry>> { |
|
| 404 | 579 | let mut out = BTreeMap::new(); |
|
| 405 | 580 | for (p, _) in snap.files.iter().chain(full_snapshot.files.iter()) { |
|
| 406 | 581 | let take_full = keep_from_full(p); |
|
| 407 | - | let src = if take_full { full_snapshot.files.get(p) } else { snap.files.get(p) }; |
|
| 582 | + | let src = if take_full { |
|
| 583 | + | full_snapshot.files.get(p) |
|
| 584 | + | } else { |
|
| 585 | + | snap.files.get(p) |
|
| 586 | + | }; |
|
| 408 | 587 | let Some(src) = src else { continue }; |
|
| 409 | 588 | if out.contains_key(p) { |
|
| 410 | 589 | continue; |
|
| … | 11 unchanged lines |
||
| 422 | 601 | Ok(out) |
|
| 423 | 602 | }; |
|
| 424 | 603 | let mid_entries = entries_of(&beneath_snapshot, &|p| selected(p))?; |
|
| 425 | - | let first = self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?; |
|
| 604 | + | let first = |
|
| 605 | + | self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?; |
|
| 426 | 606 | if first.tree_ops.is_empty() && first.edits.is_empty() { |
|
| 427 | 607 | return fail("the named paths are not touched by that change"); |
|
| 428 | 608 | } |
|
| … | 2 unchanged lines |
||
| 431 | 611 | deps: old.deps.clone(), |
|
| 432 | 612 | author: old.author, |
|
| 433 | 613 | hlc: Hlc::now(), |
|
| 434 | - | message: format!("{} (part 1: {})", old.message.lines().next().unwrap_or(""), paths.join(", ")), |
|
| 614 | + | message: format!( |
|
| 615 | + | "{} (part 1: {})", |
|
| 616 | + | old.message.lines().next().unwrap_or(""), |
|
| 617 | + | paths.join(", ") |
|
| 618 | + | ), |
|
| 435 | 619 | tree_ops: first.tree_ops, |
|
| 436 | 620 | edits: first.edits, |
|
| 437 | 621 | kind: ChangeKind::Normal, |
|
| … | 9 unchanged lines |
||
| 447 | 631 | let rest_entries = entries_of(&full_snapshot, &|_| true)?; |
|
| 448 | 632 | let rest = self.build_from_entries(&mid, &mid_snapshot, HashMap::new(), &rest_entries)?; |
|
| 449 | 633 | if rest.tree_ops.is_empty() && rest.edits.is_empty() { |
|
| 450 | - | return fail("everything in that change is under the named paths; nothing would remain"); |
|
| 634 | + | return fail( |
|
| 635 | + | "everything in that change is under the named paths; nothing would remain", |
|
| 636 | + | ); |
|
| 451 | 637 | } |
|
| 452 | 638 | let mut c2 = Change { |
|
| 453 | 639 | change_id: old.change_id, |
|
| … | 9 unchanged lines |
||
| 463 | 649 | d2.extend(old.deps.iter().copied()); |
|
| 464 | 650 | c2.deps = d2.into_iter().collect(); |
|
| 465 | 651 | c2.normalize(); |
|
| 466 | - | let h2 = self.replace_head(hash, Some(&c2), &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap(); |
|
| 652 | + | let h2 = self |
|
| 653 | + | .replace_head( |
|
| 654 | + | hash, |
|
| 655 | + | Some(&c2), |
|
| 656 | + | &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")), |
|
| 657 | + | )? |
|
| 658 | + | .unwrap(); |
|
| 467 | 659 | self.add_to_bookmark(h1)?; |
|
| 468 | 660 | Ok((h1, h2)) |
|
| 469 | 661 | } |
|
| … | 8 unchanged lines |
||
| 478 | 670 | let st = self.load_state(&heads)?; |
|
| 479 | 671 | let heads = st.maximal(&heads); |
|
| 480 | 672 | let mut view = op.view.clone(); |
|
| 481 | - | view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(heads.clone()))?); |
|
| 673 | + | view.bookmarks.insert( |
|
| 674 | + | bookmark, |
|
| 675 | + | self.store.put_state(&StateObject::new(heads.clone()))?, |
|
| 676 | + | ); |
|
| 482 | 677 | let (_, wc) = self.working_copy()?; |
|
| 483 | 678 | let mut base = wc.deps.clone(); |
|
| 484 | 679 | base.push(h); |
|
| 485 | 680 | let wc_base = self.load_state(&base)?.maximal(&base); |
|
| 486 | 681 | let fresh = self.new_working_copy(wc_base, &wc.message); |
|
| 487 | - | view.workspaces.insert(self.workspace, self.store.put_change(&fresh)?); |
|
| 682 | + | view.workspaces |
|
| 683 | + | .insert(self.workspace, self.store.put_change(&fresh)?); |
|
| 488 | 684 | self.record_op(&[op_ref], "oxy split (heads)", view)?; |
|
| 489 | 685 | self.drop_manifest(); |
|
| 490 | 686 | Ok(()) |
|
| … | 3 unchanged lines |
||
| 494 | 690 | /// under `paths` become a new change, the rest keeps the ChangeId with |
|
| 495 | 691 | /// every remaining atom at its old index, so dependents are rewritten |
|
| 496 | 692 | /// exactly. Refused when a dependent touches a split-off node. |
|
| 497 | - | fn split_by_node(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> { |
|
| 693 | + | fn split_by_node( |
|
| 694 | + | &self, |
|
| 695 | + | hash: &ChangeHash, |
|
| 696 | + | paths: &[String], |
|
| 697 | + | ) -> Result<(ChangeHash, ChangeHash)> { |
|
| 498 | 698 | let old = self.store.get_change(hash)?; |
|
| 499 | 699 | if old.kind == ChangeKind::WorkingCopy { |
|
| 500 | 700 | return fail("that is the working copy; commit it instead"); |
|
| 501 | 701 | } |
|
| 502 | - | let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))); |
|
| 702 | + | let selected = |p: &str| { |
|
| 703 | + | paths |
|
| 704 | + | .iter() |
|
| 705 | + | .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))) |
|
| 706 | + | }; |
|
| 503 | 707 | let beneath = self.load_state(&old.deps)?; |
|
| 504 | 708 | let full = self.load_state(&[*hash])?; |
|
| 505 | 709 | let mut removed: BTreeSet<NodeId> = BTreeSet::new(); |
|
| … | 27 unchanged lines |
||
| 533 | 737 | continue; |
|
| 534 | 738 | } |
|
| 535 | 739 | let d = self.store.get_change(h)?; |
|
| 536 | - | if d.tree_ops.iter().any(|op| removed.contains(&op.node())) || d.edits.iter().any(|e| removed.contains(&e.node())) { |
|
| 537 | - | return fail(format!("change {} depends on the split-off paths of {}; split it first", d.change_id.short(8), old.change_id.short(8))); |
|
| 740 | + | if d.tree_ops.iter().any(|op| removed.contains(&op.node())) |
|
| 741 | + | || d.edits.iter().any(|e| removed.contains(&e.node())) |
|
| 742 | + | { |
|
| 743 | + | return fail(format!( |
|
| 744 | + | "change {} depends on the split-off paths of {}; split it first", |
|
| 745 | + | d.change_id.short(8), |
|
| 746 | + | old.change_id.short(8) |
|
| 747 | + | )); |
|
| 538 | 748 | } |
|
| 539 | 749 | } |
|
| 540 | 750 | let mut c1 = Change { |
|
| … | 1 unchanged lines |
||
| 542 | 752 | deps: old.deps.clone(), |
|
| 543 | 753 | author: old.author, |
|
| 544 | 754 | hlc: Hlc::now(), |
|
| 545 | - | message: format!("{} (part 1: {})", old.message.lines().next().unwrap_or(""), paths.join(", ")), |
|
| 546 | - | tree_ops: old.tree_ops.iter().filter(|op| removed.contains(&op.node())).cloned().collect(), |
|
| 547 | - | edits: old.edits.iter().filter(|e| removed.contains(&e.node())).cloned().collect(), |
|
| 755 | + | message: format!( |
|
| 756 | + | "{} (part 1: {})", |
|
| 757 | + | old.message.lines().next().unwrap_or(""), |
|
| 758 | + | paths.join(", ") |
|
| 759 | + | ), |
|
| 760 | + | tree_ops: old |
|
| 761 | + | .tree_ops |
|
| 762 | + | .iter() |
|
| 763 | + | .filter(|op| removed.contains(&op.node())) |
|
| 764 | + | .cloned() |
|
| 765 | + | .collect(), |
|
| 766 | + | edits: old |
|
| 767 | + | .edits |
|
| 768 | + | .iter() |
|
| 769 | + | .filter(|e| removed.contains(&e.node())) |
|
| 770 | + | .cloned() |
|
| 771 | + | .collect(), |
|
| 548 | 772 | kind: ChangeKind::Normal, |
|
| 549 | 773 | }; |
|
| 550 | 774 | let mut c2 = old.clone(); |
|
| … | 12 unchanged lines |
||
| 563 | 787 | return fail("the named paths are not touched by that change"); |
|
| 564 | 788 | } |
|
| 565 | 789 | if c2.tree_ops.is_empty() && c2.edits.is_empty() { |
|
| 566 | - | return fail("everything in that change is under the named paths; use `oxy abandon` instead"); |
|
| 790 | + | return fail( |
|
| 791 | + | "everything in that change is under the named paths; use `oxy abandon` instead", |
|
| 792 | + | ); |
|
| 567 | 793 | } |
|
| 568 | 794 | let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath); |
|
| 569 | 795 | d1.extend(old.deps.iter().copied()); |
|
| … | 3 unchanged lines |
||
| 573 | 799 | d2.extend(old.deps.iter().copied()); |
|
| 574 | 800 | c2.deps = d2.into_iter().collect(); |
|
| 575 | 801 | c2.normalize(); |
|
| 576 | - | let corr = Correspondence { atoms: Correspondence::identity(&old).atoms, ops: ops_map }; |
|
| 577 | - | let h2 = self.rewrite_change(hash, Some(&c2), &corr, &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap(); |
|
| 802 | + | let corr = Correspondence { |
|
| 803 | + | atoms: Correspondence::identity(&old).atoms, |
|
| 804 | + | ops: ops_map, |
|
| 805 | + | }; |
|
| 806 | + | let h2 = self |
|
| 807 | + | .rewrite_change( |
|
| 808 | + | hash, |
|
| 809 | + | Some(&c2), |
|
| 810 | + | &corr, |
|
| 811 | + | &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")), |
|
| 812 | + | )? |
|
| 813 | + | .unwrap(); |
|
| 578 | 814 | let h1 = self.store_signed(&c1)?; |
|
| 579 | 815 | self.add_to_bookmark(h1)?; |
|
| 580 | 816 | Ok((h1, h2)) |
|
| … | 17 unchanged lines |
||
| 598 | 834 | return fail("cannot squash into the working copy"); |
|
| 599 | 835 | } |
|
| 600 | 836 | let closure = self.load_state(&[*into])?; |
|
| 601 | - | let outside: Vec<String> = src.deps.iter().filter(|d| *d != into && !closure.contains(d)).map(|d| d.short(12)).collect(); |
|
| 837 | + | let outside: Vec<String> = src |
|
| 838 | + | .deps |
|
| 839 | + | .iter() |
|
| 840 | + | .filter(|d| *d != into && !closure.contains(d)) |
|
| 841 | + | .map(|d| d.short(12)) |
|
| 842 | + | .collect(); |
|
| 602 | 843 | if !outside.is_empty() { |
|
| 603 | 844 | return fail(format!("{} depends on {} which #{} does not; squash those first or squash into a later change", src.change_id.short(8), outside.join(", "), into.short(12))); |
|
| 604 | 845 | } |
|
| 605 | 846 | let ok_change = |h: &ChangeHash| *h == *into || closure.contains(h); |
|
| 606 | 847 | let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some(); |
|
| 607 | - | let too_new = |what: &str| RepoError::Msg(format!("{what} was introduced after change #{}; squash into a later change instead", into.short(12))); |
|
| 608 | - | let src_added: BTreeSet<NodeId> = src.tree_ops.iter().filter_map(|o| if let TreeOp::AddNode { node, .. } = o { Some(*node) } else { None }).collect(); |
|
| 848 | + | let too_new = |what: &str| { |
|
| 849 | + | RepoError::Msg(format!( |
|
| 850 | + | "{what} was introduced after change #{}; squash into a later change instead", |
|
| 851 | + | into.short(12) |
|
| 852 | + | )) |
|
| 853 | + | }; |
|
| 854 | + | let src_added: BTreeSet<NodeId> = src |
|
| 855 | + | .tree_ops |
|
| 856 | + | .iter() |
|
| 857 | + | .filter_map(|o| { |
|
| 858 | + | if let TreeOp::AddNode { node, .. } = o { |
|
| 859 | + | Some(*node) |
|
| 860 | + | } else { |
|
| 861 | + | None |
|
| 862 | + | } |
|
| 863 | + | }) |
|
| 864 | + | .collect(); |
|
| 609 | 865 | for op in &src.tree_ops { |
|
| 610 | 866 | match op { |
|
| 611 | 867 | TreeOp::AddNode { parent, .. } => { |
|
| … | 6 unchanged lines |
||
| 618 | 874 | return Err(too_new("a removed file")); |
|
| 619 | 875 | } |
|
| 620 | 876 | } |
|
| 621 | - | TreeOp::Move { node, old, parent, .. } => { |
|
| 622 | - | if !ok_node(node) || !ok_change(&old.change) || (!ok_node(parent) && !src_added.contains(parent)) { |
|
| 877 | + | TreeOp::Move { |
|
| 878 | + | node, old, parent, .. |
|
| 879 | + | } => { |
|
| 880 | + | if !ok_node(node) |
|
| 881 | + | || !ok_change(&old.change) |
|
| 882 | + | || (!ok_node(parent) && !src_added.contains(parent)) |
|
| 883 | + | { |
|
| 623 | 884 | return Err(too_new("a moved file")); |
|
| 624 | 885 | } |
|
| 625 | 886 | } |
|
| … | 10 unchanged lines |
||
| 636 | 897 | return Err(too_new("an edited file")); |
|
| 637 | 898 | } |
|
| 638 | 899 | let refs: Vec<ChangeHash> = match e { |
|
| 639 | - | Edit::Insert { after, before, .. } => [after, before].iter().filter_map(|r| if let AtomRef::Atom { change, .. } = r { Some(*change) } else { None }).collect(), |
|
| 900 | + | Edit::Insert { after, before, .. } => [after, before] |
|
| 901 | + | .iter() |
|
| 902 | + | .filter_map(|r| { |
|
| 903 | + | if let AtomRef::Atom { change, .. } = r { |
|
| 904 | + | Some(*change) |
|
| 905 | + | } else { |
|
| 906 | + | None |
|
| 907 | + | } |
|
| 908 | + | }) |
|
| 909 | + | .collect(), |
|
| 640 | 910 | Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(), |
|
| 641 | 911 | Edit::Replace { .. } => vec![], |
|
| 642 | 912 | }; |
|
| … | 12 unchanged lines |
||
| 655 | 925 | let op_offset = dst.tree_ops.len() as u32; |
|
| 656 | 926 | let fold_op = |r: OpRef| -> OpRef { |
|
| 657 | 927 | if r.change == *into { |
|
| 658 | - | OpRef { change: ObjectRef::ZERO, index: r.index } |
|
| 928 | + | OpRef { |
|
| 929 | + | change: ObjectRef::ZERO, |
|
| 930 | + | index: r.index, |
|
| 931 | + | } |
|
| 659 | 932 | } else if r.change.is_zero() { |
|
| 660 | - | OpRef { change: ObjectRef::ZERO, index: r.index + op_offset } |
|
| 933 | + | OpRef { |
|
| 934 | + | change: ObjectRef::ZERO, |
|
| 935 | + | index: r.index + op_offset, |
|
| 936 | + | } |
|
| 661 | 937 | } else { |
|
| 662 | 938 | r |
|
| 663 | 939 | } |
|
| … | 1 unchanged lines |
||
| 665 | 941 | let mut tree_ops = dst.tree_ops.clone(); |
|
| 666 | 942 | for op in &src.tree_ops { |
|
| 667 | 943 | tree_ops.push(match op { |
|
| 668 | - | TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: fold_op(*placement) }, |
|
| 669 | - | TreeOp::Move { node, old, parent, name } => TreeOp::Move { node: *node, old: fold_op(*old), parent: *parent, name: name.clone() }, |
|
| 670 | - | TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: fold_op(*old), mode: *mode }, |
|
| 944 | + | TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { |
|
| 945 | + | node: *node, |
|
| 946 | + | placement: fold_op(*placement), |
|
| 947 | + | }, |
|
| 948 | + | TreeOp::Move { |
|
| 949 | + | node, |
|
| 950 | + | old, |
|
| 951 | + | parent, |
|
| 952 | + | name, |
|
| 953 | + | } => TreeOp::Move { |
|
| 954 | + | node: *node, |
|
| 955 | + | old: fold_op(*old), |
|
| 956 | + | parent: *parent, |
|
| 957 | + | name: name.clone(), |
|
| 958 | + | }, |
|
| 959 | + | TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { |
|
| 960 | + | node: *node, |
|
| 961 | + | old: fold_op(*old), |
|
| 962 | + | mode: *mode, |
|
| 963 | + | }, |
|
| 671 | 964 | add => add.clone(), |
|
| 672 | 965 | }); |
|
| 673 | 966 | } |
|
| 674 | 967 | let mut edits = dst.edits.clone(); |
|
| 675 | 968 | for e in &src.edits { |
|
| 676 | 969 | edits.push(match e { |
|
| 677 | - | Edit::Insert { node, after, before, lines } => Edit::Insert { |
|
| 970 | + | Edit::Insert { |
|
| 971 | + | node, |
|
| 972 | + | after, |
|
| 973 | + | before, |
|
| 974 | + | lines, |
|
| 975 | + | } => Edit::Insert { |
|
| 678 | 976 | node: *node, |
|
| 679 | - | after: fold_atom(*after, into, &per_node, node).ok_or_else(|| too_new("an anchor line"))?, |
|
| 680 | - | before: fold_atom(*before, into, &per_node, node).ok_or_else(|| too_new("an anchor line"))?, |
|
| 977 | + | after: fold_atom(*after, into, &per_node, node) |
|
| 978 | + | .ok_or_else(|| too_new("an anchor line"))?, |
|
| 979 | + | before: fold_atom(*before, into, &per_node, node) |
|
| 980 | + | .ok_or_else(|| too_new("an anchor line"))?, |
|
| 681 | 981 | lines: lines.clone(), |
|
| 682 | 982 | }, |
|
| 683 | 983 | Edit::Delete { node, atoms } => Edit::Delete { |
|
| … | 2 unchanged lines |
||
| 686 | 986 | .iter() |
|
| 687 | 987 | .map(|r| { |
|
| 688 | 988 | if r.change == *into { |
|
| 689 | - | AtomRange { change: ObjectRef::ZERO, from: r.from, to: r.to } |
|
| 989 | + | AtomRange { |
|
| 990 | + | change: ObjectRef::ZERO, |
|
| 991 | + | from: r.from, |
|
| 992 | + | to: r.to, |
|
| 993 | + | } |
|
| 690 | 994 | } else if r.change.is_zero() { |
|
| 691 | 995 | let off = per_node.get(node).copied().unwrap_or(0); |
|
| 692 | - | AtomRange { change: ObjectRef::ZERO, from: r.from + off, to: r.to + off } |
|
| 996 | + | AtomRange { |
|
| 997 | + | change: ObjectRef::ZERO, |
|
| 998 | + | from: r.from + off, |
|
| 999 | + | to: r.to + off, |
|
| 1000 | + | } |
|
| 693 | 1001 | } else { |
|
| 694 | 1002 | *r |
|
| 695 | 1003 | } |
|
| … | 11 unchanged lines |
||
| 707 | 1015 | message: dst.message.clone(), |
|
| 708 | 1016 | tree_ops, |
|
| 709 | 1017 | edits, |
|
| 710 | - | kind: if dst.kind == ChangeKind::Resolution || src.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal }, |
|
| 1018 | + | kind: if dst.kind == ChangeKind::Resolution || src.kind == ChangeKind::Resolution { |
|
| 1019 | + | ChangeKind::Resolution |
|
| 1020 | + | } else { |
|
| 1021 | + | ChangeKind::Normal |
|
| 1022 | + | }, |
|
| 711 | 1023 | }; |
|
| 712 | 1024 | let beneath = self.load_state(&dst.deps)?; |
|
| 713 | 1025 | let mut deps: BTreeSet<ChangeHash> = dst.deps.iter().copied().collect(); |
|
| 714 | - | deps.extend(referenced_changes(&new, &beneath).into_iter().filter(|h| *h != *into && *h != *c)); |
|
| 1026 | + | deps.extend( |
|
| 1027 | + | referenced_changes(&new, &beneath) |
|
| 1028 | + | .into_iter() |
|
| 1029 | + | .filter(|h| *h != *into && *h != *c), |
|
| 1030 | + | ); |
|
| 715 | 1031 | deps.extend(src.deps.iter().filter(|d| **d != *into).copied()); |
|
| 716 | 1032 | new.deps = deps.into_iter().collect(); |
|
| 717 | 1033 | new.normalize(); |
|
| 718 | 1034 | ||
| 719 | 1035 | // Dependents of dst keep their indices; dependents of src follow its |
|
| 720 | 1036 | // atoms and operations to their shifted places in the new change. |
|
| 721 | - | let cmd = format!("oxy squash {} --into {}", src.change_id.short(8), dst.change_id.short(8)); |
|
| 722 | - | let dst_new = self.rewrite_change(into, Some(&new), &Correspondence::identity(&dst), &cmd)?.unwrap(); |
|
| 1037 | + | let cmd = format!( |
|
| 1038 | + | "oxy squash {} --into {}", |
|
| 1039 | + | src.change_id.short(8), |
|
| 1040 | + | dst.change_id.short(8) |
|
| 1041 | + | ); |
|
| 1042 | + | let dst_new = self |
|
| 1043 | + | .rewrite_change(into, Some(&new), &Correspondence::identity(&dst), &cmd)? |
|
| 1044 | + | .unwrap(); |
|
| 723 | 1045 | // src was rewritten along with the other dependents of dst; find it by ChangeId. |
|
| 724 | 1046 | let (wc_hash, _) = self.working_copy()?; |
|
| 725 | 1047 | let mut all = self.bookmark_heads()?; |
|
| 726 | 1048 | all.push(wc_hash); |
|
| 727 | 1049 | let state = self.load_state(&all)?; |
|
| 728 | - | let src_now = state.changes().find(|(_, m)| m.change_id == src.change_id).map(|(h, _)| *h).unwrap_or(*c); |
|
| 1050 | + | let src_now = state |
|
| 1051 | + | .changes() |
|
| 1052 | + | .find(|(_, m)| m.change_id == src.change_id) |
|
| 1053 | + | .map(|(h, _)| *h) |
|
| 1054 | + | .unwrap_or(*c); |
|
| 729 | 1055 | let src_change = self.store.get_change(&src_now)?; |
|
| 730 | - | let mut corr = Correspondence { atoms: HashMap::new(), ops: HashMap::new() }; |
|
| 1056 | + | let mut corr = Correspondence { |
|
| 1057 | + | atoms: HashMap::new(), |
|
| 1058 | + | ops: HashMap::new(), |
|
| 1059 | + | }; |
|
| 731 | 1060 | let mut counts: HashMap<NodeId, u32> = HashMap::new(); |
|
| 732 | 1061 | for e in &src_change.edits { |
|
| 733 | 1062 | if let Edit::Insert { node, lines, .. } = e { |
|
| … | 23 unchanged lines |
||
| 757 | 1086 | } |
|
| 758 | 1087 | let target = self.load_state(onto)?; |
|
| 759 | 1088 | if target.contains(c) { |
|
| 760 | - | return fail(format!("#{} is already part of the target state", c.short(12))); |
|
| 1089 | + | return fail(format!( |
|
| 1090 | + | "#{} is already part of the target state", |
|
| 1091 | + | c.short(12) |
|
| 1092 | + | )); |
|
| 761 | 1093 | } |
|
| 762 | 1094 | let onto = target.maximal(onto); |
|
| 763 | 1095 | let target_snapshot = target.materialize(); |
|
| … | 2 unchanged lines |
||
| 766 | 1098 | // shows is what the re-derived change must produce. |
|
| 767 | 1099 | let mut with_c = target.clone(); |
|
| 768 | 1100 | if with_c.add(old.clone()).is_err() { |
|
| 769 | - | return fail(format!("the target state lacks dependencies of #{}; rebase onto a state that has them", c.short(12))); |
|
| 1101 | + | return fail(format!( |
|
| 1102 | + | "the target state lacks dependencies of #{}; rebase onto a state that has them", |
|
| 1103 | + | c.short(12) |
|
| 1104 | + | )); |
|
| 770 | 1105 | } |
|
| 771 | 1106 | let after = with_c.materialize(); |
|
| 772 | 1107 | // The paths whose file differs between the target and the target with c. |
|
| 773 | 1108 | let mut touched: BTreeSet<String> = BTreeSet::new(); |
|
| 774 | 1109 | for p in target_snapshot.files.keys().chain(after.files.keys()) { |
|
| 775 | 1110 | match (target_snapshot.files.get(p), after.files.get(p)) { |
|
| 776 | - | (Some(a), Some(b)) if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {} |
|
| 1111 | + | (Some(a), Some(b)) |
|
| 1112 | + | if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {} |
|
| 777 | 1113 | _ => { |
|
| 778 | 1114 | touched.insert(p.clone()); |
|
| 779 | 1115 | } |
|
| 780 | 1116 | } |
|
| 781 | 1117 | } |
|
| 782 | 1118 | for p in &touched { |
|
| 783 | - | if target_snapshot.files.get(p).is_some_and(|f| f.is_conflicted()) || after.files.get(p).is_some_and(|f| f.is_conflicted()) { |
|
| 1119 | + | if target_snapshot |
|
| 1120 | + | .files |
|
| 1121 | + | .get(p) |
|
| 1122 | + | .is_some_and(|f| f.is_conflicted()) |
|
| 1123 | + | || after.files.get(p).is_some_and(|f| f.is_conflicted()) |
|
| 1124 | + | { |
|
| 784 | 1125 | return fail(format!("{p} would be conflicted; resolve the target state or the conflict with #{} first", c.short(12))); |
|
| 785 | 1126 | } |
|
| 786 | 1127 | } |
|
| … | 30 unchanged lines |
||
| 817 | 1158 | } |
|
| 818 | 1159 | let built = self.build_from_entries(&target, &target_snapshot, reuse, &entries)?; |
|
| 819 | 1160 | if built.tree_ops.is_empty() && built.edits.is_empty() { |
|
| 820 | - | return fail("rebasing would leave the change empty; the target already has its effect"); |
|
| 1161 | + | return fail( |
|
| 1162 | + | "rebasing would leave the change empty; the target already has its effect", |
|
| 1163 | + | ); |
|
| 821 | 1164 | } |
|
| 822 | 1165 | let mut new = Change { |
|
| 823 | 1166 | change_id: old.change_id, |
|
| … | 3 unchanged lines |
||
| 827 | 1170 | message: old.message.clone(), |
|
| 828 | 1171 | tree_ops: built.tree_ops, |
|
| 829 | 1172 | edits: built.edits, |
|
| 830 | - | kind: if built.resolves { ChangeKind::Resolution } else { ChangeKind::Normal }, |
|
| 1173 | + | kind: if built.resolves { |
|
| 1174 | + | ChangeKind::Resolution |
|
| 1175 | + | } else { |
|
| 1176 | + | ChangeKind::Normal |
|
| 1177 | + | }, |
|
| 831 | 1178 | }; |
|
| 832 | 1179 | let mut deps: BTreeSet<ChangeHash> = onto.iter().copied().collect(); |
|
| 833 | 1180 | deps.extend(referenced_changes(&new, &target)); |
|
| … | 1 unchanged lines |
||
| 835 | 1182 | new.deps = deps.into_iter().collect(); |
|
| 836 | 1183 | new.normalize(); |
|
| 837 | 1184 | let corr = Correspondence::by_content(&old, &new); |
|
| 838 | - | let cmd = format!("oxy rebase {} --onto {}", old.change_id.short(8), onto.iter().map(|h| h.short(12)).collect::<Vec<_>>().join(" ")); |
|
| 1185 | + | let cmd = format!( |
|
| 1186 | + | "oxy rebase {} --onto {}", |
|
| 1187 | + | old.change_id.short(8), |
|
| 1188 | + | onto.iter() |
|
| 1189 | + | .map(|h| h.short(12)) |
|
| 1190 | + | .collect::<Vec<_>>() |
|
| 1191 | + | .join(" ") |
|
| 1192 | + | ); |
|
| 839 | 1193 | Ok(self.rewrite_change(c, Some(&new), &corr, &cmd)?.unwrap()) |
|
| 840 | 1194 | } |
|
| 841 | 1195 | } |
|
| 842 | - |
modified crates/oxy-core/src/repo/hooks.rs +33 −6
| … | 46 unchanged lines |
||
| 47 | 47 | pub fn run_hook(&self, event: &str, input: &str) -> Result<HookOutcome> { |
|
| 48 | 48 | let path = self.hook_path(event); |
|
| 49 | 49 | if !path.is_file() { |
|
| 50 | - | return Ok(HookOutcome { ran: false, stdout: String::new() }); |
|
| 50 | + | return Ok(HookOutcome { |
|
| 51 | + | ran: false, |
|
| 52 | + | stdout: String::new(), |
|
| 53 | + | }); |
|
| 51 | 54 | } |
|
| 52 | 55 | let mut child = Command::new(&path) |
|
| 53 | 56 | .current_dir(&self.root) |
|
| … | 11 unchanged lines |
||
| 65 | 68 | let stdout = String::from_utf8_lossy(&out.stdout).to_string(); |
|
| 66 | 69 | if !out.status.success() { |
|
| 67 | 70 | let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); |
|
| 68 | - | return fail(format!("hook {event} refused: {}", if stderr.is_empty() { "(no message)".to_string() } else { stderr })); |
|
| 71 | + | return fail(format!( |
|
| 72 | + | "hook {event} refused: {}", |
|
| 73 | + | if stderr.is_empty() { |
|
| 74 | + | "(no message)".to_string() |
|
| 75 | + | } else { |
|
| 76 | + | stderr |
|
| 77 | + | } |
|
| 78 | + | )); |
|
| 69 | 79 | } |
|
| 70 | 80 | Ok(HookOutcome { ran: true, stdout }) |
|
| 71 | 81 | } |
|
| 72 | 82 | ||
| 73 | 83 | /// Extra dependencies from the `semantic-deps` hook (spec §8.1, §20). |
|
| 74 | - | pub(crate) fn semantic_deps(&self, change_id: &ChangeId, touched: &[String], candidates: &[(ChangeId, ChangeHash, Vec<String>)]) -> Result<Vec<ChangeHash>> { |
|
| 84 | + | pub(crate) fn semantic_deps( |
|
| 85 | + | &self, |
|
| 86 | + | change_id: &ChangeId, |
|
| 87 | + | touched: &[String], |
|
| 88 | + | candidates: &[(ChangeId, ChangeHash, Vec<String>)], |
|
| 89 | + | ) -> Result<Vec<ChangeHash>> { |
|
| 75 | 90 | if !self.config.semantic_deps { |
|
| 76 | 91 | return Ok(vec![]); |
|
| 77 | 92 | } |
|
| … | 1 unchanged lines |
||
| 79 | 94 | "{{\"change_id\":{},\"touched_paths\":{},\"candidate_deps\":{}}}", |
|
| 80 | 95 | json_str(&change_id.to_hex()), |
|
| 81 | 96 | json_array(touched.iter().map(|p| json_str(p))), |
|
| 82 | - | json_array(candidates.iter().map(|(id, h, paths)| format!("{{\"change_id\":{},\"hash\":{},\"paths\":{}}}", json_str(&id.to_hex()), json_str(&h.to_hex()), json_array(paths.iter().map(|p| json_str(p)))))) |
|
| 97 | + | json_array(candidates.iter().map(|(id, h, paths)| format!( |
|
| 98 | + | "{{\"change_id\":{},\"hash\":{},\"paths\":{}}}", |
|
| 99 | + | json_str(&id.to_hex()), |
|
| 100 | + | json_str(&h.to_hex()), |
|
| 101 | + | json_array(paths.iter().map(|p| json_str(p))) |
|
| 102 | + | ))) |
|
| 83 | 103 | ); |
|
| 84 | 104 | let out = self.run_hook("semantic-deps", &input)?; |
|
| 85 | 105 | if !out.ran { |
|
| 86 | 106 | return Ok(vec![]); |
|
| 87 | 107 | } |
|
| 88 | 108 | // Accept every 64-hex token after "deps"; any other shape is ignored. |
|
| 89 | - | let body = out.stdout.split_once("\"deps\"").map(|(_, rest)| rest.to_string()).unwrap_or_default(); |
|
| 109 | + | let body = out |
|
| 110 | + | .stdout |
|
| 111 | + | .split_once("\"deps\"") |
|
| 112 | + | .map(|(_, rest)| rest.to_string()) |
|
| 113 | + | .unwrap_or_default(); |
|
| 90 | 114 | let mut deps = Vec::new(); |
|
| 91 | 115 | let bytes = body.as_bytes(); |
|
| 92 | 116 | let mut i = 0; |
|
| 93 | 117 | while i + 64 <= bytes.len() { |
|
| 94 | - | if bytes[i..i + 64].iter().all(|b| b.is_ascii_hexdigit()) && (i == 0 || !bytes[i - 1].is_ascii_hexdigit()) && (i + 64 == bytes.len() || !bytes[i + 64].is_ascii_hexdigit()) { |
|
| 118 | + | if bytes[i..i + 64].iter().all(|b| b.is_ascii_hexdigit()) |
|
| 119 | + | && (i == 0 || !bytes[i - 1].is_ascii_hexdigit()) |
|
| 120 | + | && (i + 64 == bytes.len() || !bytes[i + 64].is_ascii_hexdigit()) |
|
| 121 | + | { |
|
| 95 | 122 | if let Some(h) = ObjectRef::from_hex(&body[i..i + 64]) { |
|
| 96 | 123 | deps.push(h); |
|
| 97 | 124 | } |
|
| … | 8 unchanged lines |
modified crates/oxy-core/src/repo/identity.rs +139 −28
| … | 7 unchanged lines |
||
| 8 | 8 | Unsigned, |
|
| 9 | 9 | /// `revoked` marks a signature that verifies but whose key has since |
|
| 10 | 10 | /// been revoked by policy or by a revocation this repository recorded. |
|
| 11 | - | Valid { key: KeyId, name: String, email: String, sealed: bool, revoked: bool }, |
|
| 11 | + | Valid { |
|
| 12 | + | key: KeyId, |
|
| 13 | + | name: String, |
|
| 14 | + | email: String, |
|
| 15 | + | sealed: bool, |
|
| 16 | + | revoked: bool, |
|
| 17 | + | }, |
|
| 12 | 18 | Invalid(String), |
|
| 13 | 19 | } |
|
| 14 | 20 | ||
| … | 13 unchanged lines |
||
| 28 | 34 | ||
| 29 | 35 | /// Generate a composite key pair, store its identity object, keep the |
|
| 30 | 36 | /// secret in the key directory, and record the author at `scope`. |
|
| 31 | - | pub fn create_identity(&mut self, name: &str, email: &str, scope: IdentityScope) -> Result<(KeyId, Identity)> { |
|
| 37 | + | pub fn create_identity( |
|
| 38 | + | &mut self, |
|
| 39 | + | name: &str, |
|
| 40 | + | email: &str, |
|
| 41 | + | scope: IdentityScope, |
|
| 42 | + | ) -> Result<(KeyId, Identity)> { |
|
| 32 | 43 | self.create_identity_with(name, email, scope, None) |
|
| 33 | 44 | } |
|
| 34 | 45 | ||
| 35 | 46 | /// `create_identity` with an expiry (milliseconds since the epoch). |
|
| 36 | - | pub fn create_identity_with(&mut self, name: &str, email: &str, scope: IdentityScope, expires: Option<u64>) -> Result<(KeyId, Identity)> { |
|
| 47 | + | pub fn create_identity_with( |
|
| 48 | + | &mut self, |
|
| 49 | + | name: &str, |
|
| 50 | + | email: &str, |
|
| 51 | + | scope: IdentityScope, |
|
| 52 | + | expires: Option<u64>, |
|
| 53 | + | ) -> Result<(KeyId, Identity)> { |
|
| 37 | 54 | let sk = SecretKey::generate(); |
|
| 38 | 55 | let mut id = sk.identity(name, email, Hlc::now().wall_ms); |
|
| 39 | 56 | id.expires = expires; |
|
| … | 9 unchanged lines |
||
| 49 | 66 | fs::create_dir_all(&self.keys_dir)?; |
|
| 50 | 67 | let path = self.keys_dir.join(format!("{}.secret", key.to_hex())); |
|
| 51 | 68 | write_atomic(&path, &sk.encode(&key))?; |
|
| 52 | - | write_atomic(&self.keys_dir.join(format!("{}.identity", key.to_hex())), &id.encode())?; |
|
| 69 | + | write_atomic( |
|
| 70 | + | &self.keys_dir.join(format!("{}.identity", key.to_hex())), |
|
| 71 | + | &id.encode(), |
|
| 72 | + | )?; |
|
| 53 | 73 | #[cfg(unix)] |
|
| 54 | 74 | { |
|
| 55 | 75 | use std::os::unix::fs::PermissionsExt; |
|
| … | 22 unchanged lines |
||
| 78 | 98 | ||
| 79 | 99 | /// The scope that holds the configured author key. |
|
| 80 | 100 | fn author_scope(&self) -> IdentityScope { |
|
| 81 | - | if self.config.repo_layer().contains_key("author.key") { IdentityScope::Repo } else { IdentityScope::User } |
|
| 101 | + | if self.config.repo_layer().contains_key("author.key") { |
|
| 102 | + | IdentityScope::Repo |
|
| 103 | + | } else { |
|
| 104 | + | IdentityScope::User |
|
| 105 | + | } |
|
| 82 | 106 | } |
|
| 83 | 107 | ||
| 84 | 108 | /// Replace the configured identity by `new` signed with `sk`: store it, |
|
| 85 | 109 | /// record a `rotate` attestation signed by the old key, move the old |
|
| 86 | 110 | /// key's policy grants to the new key, and switch the author config. |
|
| 87 | - | fn replace_identity(&mut self, new: Identity, sk: SecretKey, command: &str) -> Result<Rotation> { |
|
| 111 | + | fn replace_identity( |
|
| 112 | + | &mut self, |
|
| 113 | + | new: Identity, |
|
| 114 | + | sk: SecretKey, |
|
| 115 | + | command: &str, |
|
| 116 | + | ) -> Result<Rotation> { |
|
| 88 | 117 | let Some((old, old_sk)) = self.secret_key()? else { |
|
| 89 | 118 | return fail("no secret key: run `oxy identity create` first"); |
|
| 90 | 119 | }; |
|
| … | 1 unchanged lines |
||
| 92 | 121 | return fail("the successor identity must name the current key as previous"); |
|
| 93 | 122 | } |
|
| 94 | 123 | let new_key = self.install_key(&new, &sk)?; |
|
| 95 | - | let attest = Attest { repo: self.id()?, hlc: Hlc::now(), body: AttestBody::Rotate { new: new_key } }; |
|
| 124 | + | let attest = Attest { |
|
| 125 | + | repo: self.id()?, |
|
| 126 | + | hlc: Hlc::now(), |
|
| 127 | + | body: AttestBody::Rotate { new: new_key }, |
|
| 128 | + | }; |
|
| 96 | 129 | let r = self.store.put_attest(&attest)?; |
|
| 97 | - | self.store.put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?; |
|
| 130 | + | self.store |
|
| 131 | + | .put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?; |
|
| 98 | 132 | let policy = match self.current_policy()? { |
|
| 99 | 133 | Some((_, sp)) if sp.policy.mentions(&old) => { |
|
| 100 | 134 | let mut p = self.next_policy()?; |
|
| … | 3 unchanged lines |
||
| 104 | 138 | _ => None, |
|
| 105 | 139 | }; |
|
| 106 | 140 | self.set_author(&new, &new_key, self.author_scope())?; |
|
| 107 | - | Ok(Rotation { old, new: new_key, identity: new, policy }) |
|
| 141 | + | Ok(Rotation { |
|
| 142 | + | old, |
|
| 143 | + | new: new_key, |
|
| 144 | + | identity: new, |
|
| 145 | + | policy, |
|
| 146 | + | }) |
|
| 108 | 147 | } |
|
| 109 | 148 | ||
| 110 | 149 | /// Rotate to a fresh key pair; the new identity names the old one as |
|
| … | 38 unchanged lines |
||
| 149 | 188 | let mut delegates = cur.delegates.clone(); |
|
| 150 | 189 | delegates.push(dk); |
|
| 151 | 190 | let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms); |
|
| 152 | - | self.replace_identity(id, sk, &format!("oxy identity delegate add {}", dk.short(12))) |
|
| 191 | + | self.replace_identity( |
|
| 192 | + | id, |
|
| 193 | + | sk, |
|
| 194 | + | &format!("oxy identity delegate add {}", dk.short(12)), |
|
| 195 | + | ) |
|
| 153 | 196 | } |
|
| 154 | 197 | ||
| 155 | 198 | /// Withdraw a delegation; re-issues the identity with the same keys. |
|
| … | 5 unchanged lines |
||
| 161 | 204 | if !cur.delegates.contains(delegate) { |
|
| 162 | 205 | return fail(format!("{} is not a delegate", delegate.short(12))); |
|
| 163 | 206 | } |
|
| 164 | - | let delegates = cur.delegates.iter().copied().filter(|d| d != delegate).collect(); |
|
| 207 | + | let delegates = cur |
|
| 208 | + | .delegates |
|
| 209 | + | .iter() |
|
| 210 | + | .copied() |
|
| 211 | + | .filter(|d| d != delegate) |
|
| 212 | + | .collect(); |
|
| 165 | 213 | let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms); |
|
| 166 | - | self.replace_identity(id, sk, &format!("oxy identity delegate remove {}", delegate.short(12))) |
|
| 214 | + | self.replace_identity( |
|
| 215 | + | id, |
|
| 216 | + | sk, |
|
| 217 | + | &format!("oxy identity delegate remove {}", delegate.short(12)), |
|
| 218 | + | ) |
|
| 167 | 219 | } |
|
| 168 | 220 | ||
| 169 | 221 | /// Revoke `key` (your own, or any key when you hold a policy root): |
|
| … | 8 unchanged lines |
||
| 178 | 230 | if *key != own && !is_root { |
|
| 179 | 231 | return fail("only the key's holder or a policy root can revoke it"); |
|
| 180 | 232 | } |
|
| 181 | - | let attest = Attest { repo: self.id()?, hlc: Hlc::now(), body: AttestBody::Revoke { key: *key, reason: reason.into() } }; |
|
| 233 | + | let attest = Attest { |
|
| 234 | + | repo: self.id()?, |
|
| 235 | + | hlc: Hlc::now(), |
|
| 236 | + | body: AttestBody::Revoke { |
|
| 237 | + | key: *key, |
|
| 238 | + | reason: reason.into(), |
|
| 239 | + | }, |
|
| 240 | + | }; |
|
| 182 | 241 | let r = self.store.put_attest(&attest)?; |
|
| 183 | - | self.store.put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?; |
|
| 242 | + | self.store |
|
| 243 | + | .put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?; |
|
| 184 | 244 | let dir = self.oxy.join("revoked"); |
|
| 185 | 245 | fs::create_dir_all(&dir)?; |
|
| 186 | 246 | write_atomic(&dir.join(key.to_hex()), r.to_hex().as_bytes())?; |
|
| … | 8 unchanged lines |
||
| 195 | 255 | if self.oxy.join("revoked").join(key.to_hex()).exists() { |
|
| 196 | 256 | return Ok(true); |
|
| 197 | 257 | } |
|
| 198 | - | Ok(self.current_policy()?.is_some_and(|(_, sp)| sp.policy.is_revoked(key))) |
|
| 258 | + | Ok(self |
|
| 259 | + | .current_policy()? |
|
| 260 | + | .is_some_and(|(_, sp)| sp.policy.is_revoked(key))) |
|
| 199 | 261 | } |
|
| 200 | 262 | ||
| 201 | 263 | /// The secret key for `author.key`, if configured and present on disk. |
|
| 202 | 264 | /// The identity object is kept beside the secret so a repository that has |
|
| 203 | 265 | /// never seen it (a fresh clone) can store it when it first signs. |
|
| 204 | 266 | pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> { |
|
| 205 | - | let Some(key) = self.config.author_key else { return Ok(None) }; |
|
| 267 | + | let Some(key) = self.config.author_key else { |
|
| 268 | + | return Ok(None); |
|
| 269 | + | }; |
|
| 206 | 270 | let path = self.keys_dir.join(format!("{}.secret", key.to_hex())); |
|
| 207 | 271 | match fs::read(&path) { |
|
| 208 | 272 | Ok(b) => { |
|
| 209 | 273 | let (k, sk) = SecretKey::decode(&b)?; |
|
| 210 | 274 | if k != key { |
|
| 211 | - | return fail(format!("{} holds a different key than its name", path.display())); |
|
| 275 | + | return fail(format!( |
|
| 276 | + | "{} holds a different key than its name", |
|
| 277 | + | path.display() |
|
| 278 | + | )); |
|
| 212 | 279 | } |
|
| 213 | 280 | if !self.store.has(&key) { |
|
| 214 | 281 | let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex())); |
|
| … | 35 unchanged lines |
||
| 250 | 317 | let mut last_err = String::new(); |
|
| 251 | 318 | for sig in &sigs { |
|
| 252 | 319 | match self.check_signer(&change, sig, Domain::Change, hash) { |
|
| 253 | - | Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: false, revoked: self.is_revoked(&sig.key)? }), |
|
| 320 | + | Ok(id) => { |
|
| 321 | + | return Ok(SignatureStatus::Valid { |
|
| 322 | + | key: sig.key, |
|
| 323 | + | name: id.name, |
|
| 324 | + | email: id.email, |
|
| 325 | + | sealed: false, |
|
| 326 | + | revoked: self.is_revoked(&sig.key)?, |
|
| 327 | + | }) |
|
| 328 | + | } |
|
| 254 | 329 | Err(e) => last_err = e, |
|
| 255 | 330 | } |
|
| 256 | 331 | } |
|
| … | 1 unchanged lines |
||
| 258 | 333 | } |
|
| 259 | 334 | ||
| 260 | 335 | /// Verify `sig` over `target` and check the author-or-delegate rule for `change`. |
|
| 261 | - | fn check_signer(&self, change: &Change, sig: &Signature, domain: Domain, target: &ObjectRef) -> std::result::Result<Identity, String> { |
|
| 262 | - | let id = self.store.get_identity(&sig.key).map_err(|_| format!("unknown key {}", sig.key.short(12)))?; |
|
| 263 | - | let authorised = |
|
| 264 | - | sig.key == change.author || self.store.get_identity(&change.author).map(|a| a.delegates.contains(&sig.key)).unwrap_or(false); |
|
| 336 | + | fn check_signer( |
|
| 337 | + | &self, |
|
| 338 | + | change: &Change, |
|
| 339 | + | sig: &Signature, |
|
| 340 | + | domain: Domain, |
|
| 341 | + | target: &ObjectRef, |
|
| 342 | + | ) -> std::result::Result<Identity, String> { |
|
| 343 | + | let id = self |
|
| 344 | + | .store |
|
| 345 | + | .get_identity(&sig.key) |
|
| 346 | + | .map_err(|_| format!("unknown key {}", sig.key.short(12)))?; |
|
| 347 | + | let authorised = sig.key == change.author |
|
| 348 | + | || self |
|
| 349 | + | .store |
|
| 350 | + | .get_identity(&change.author) |
|
| 351 | + | .map(|a| a.delegates.contains(&sig.key)) |
|
| 352 | + | .unwrap_or(false); |
|
| 265 | 353 | if !authorised { |
|
| 266 | - | return Err(format!("key {} is neither the author nor a delegate", sig.key.short(12))); |
|
| 354 | + | return Err(format!( |
|
| 355 | + | "key {} is neither the author nor a delegate", |
|
| 356 | + | sig.key.short(12) |
|
| 357 | + | )); |
|
| 267 | 358 | } |
|
| 268 | 359 | if id.is_expired_at(change.hlc.wall_ms) { |
|
| 269 | - | return Err(format!("key {} had expired when the change was made", sig.key.short(12))); |
|
| 360 | + | return Err(format!( |
|
| 361 | + | "key {} had expired when the change was made", |
|
| 362 | + | sig.key.short(12) |
|
| 363 | + | )); |
|
| 270 | 364 | } |
|
| 271 | 365 | verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?; |
|
| 272 | 366 | Ok(id) |
|
| … | 13 unchanged lines |
||
| 286 | 380 | }; |
|
| 287 | 381 | for seal_ref in self.store.seals_for(hash)? { |
|
| 288 | 382 | let attest = self.store.get_attest(&seal_ref)?; |
|
| 289 | - | let AttestBody::Seal { hashes } = &attest.body else { continue }; |
|
| 383 | + | let AttestBody::Seal { hashes } = &attest.body else { |
|
| 384 | + | continue; |
|
| 385 | + | }; |
|
| 290 | 386 | if !hashes.contains(hash) { |
|
| 291 | 387 | continue; |
|
| 292 | 388 | } |
|
| 293 | 389 | for sig in self.store.signatures(&seal_ref)? { |
|
| 294 | 390 | match self.check_signer(&change, &sig, Domain::Attest, &seal_ref) { |
|
| 295 | - | Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: true, revoked: self.is_revoked(&sig.key)? }), |
|
| 391 | + | Ok(id) => { |
|
| 392 | + | return Ok(SignatureStatus::Valid { |
|
| 393 | + | key: sig.key, |
|
| 394 | + | name: id.name, |
|
| 395 | + | email: id.email, |
|
| 396 | + | sealed: true, |
|
| 397 | + | revoked: self.is_revoked(&sig.key)?, |
|
| 398 | + | }) |
|
| 399 | + | } |
|
| 296 | 400 | Err(e) => last_err = e, |
|
| 297 | 401 | } |
|
| 298 | 402 | } |
|
| 299 | 403 | } |
|
| 300 | - | if last_err.is_empty() { Ok(SignatureStatus::Unsigned) } else { Ok(SignatureStatus::Invalid(last_err)) } |
|
| 404 | + | if last_err.is_empty() { |
|
| 405 | + | Ok(SignatureStatus::Unsigned) |
|
| 406 | + | } else { |
|
| 407 | + | Ok(SignatureStatus::Invalid(last_err)) |
|
| 408 | + | } |
|
| 301 | 409 | } |
|
| 302 | 410 | ||
| 303 | 411 | /// Store and sign an attestation with the configured key. |
|
| … | 9 unchanged lines |
||
| 313 | 421 | self.store.index_seal(h, &r)?; |
|
| 314 | 422 | } |
|
| 315 | 423 | } |
|
| 316 | - | Ok(SignedAttest { attest: attest.clone(), sigs: vec![sig] }) |
|
| 424 | + | Ok(SignedAttest { |
|
| 425 | + | attest: attest.clone(), |
|
| 426 | + | sigs: vec![sig], |
|
| 427 | + | }) |
|
| 317 | 428 | } |
|
| 318 | 429 | } |
modified crates/oxy-core/src/repo/inspect.rs +116 −27
| … | 21 unchanged lines |
||
| 22 | 22 | /// More `good`/`bad` marks are needed before the search can start. |
|
| 23 | 23 | NeedMarks { good: usize, bad: usize }, |
|
| 24 | 24 | /// A state is checked out for testing. |
|
| 25 | - | Testing { heads: Vec<ChangeHash>, remaining: usize, steps_left: u32 }, |
|
| 25 | + | Testing { |
|
| 26 | + | heads: Vec<ChangeHash>, |
|
| 27 | + | remaining: usize, |
|
| 28 | + | steps_left: u32, |
|
| 29 | + | }, |
|
| 26 | 30 | /// The first change whose inclusion makes the state bad. |
|
| 27 | 31 | Found { culprit: ChangeHash }, |
|
| 28 | 32 | /// Every remaining candidate was skipped. |
|
| … | 17 unchanged lines |
||
| 46 | 50 | v.iter().map(|h| h.to_hex()).collect::<Vec<_>>().join(" ") |
|
| 47 | 51 | } |
|
| 48 | 52 | fn hashes_from(s: &str) -> Vec<ChangeHash> { |
|
| 49 | - | s.split_whitespace().filter_map(ObjectRef::from_hex).collect() |
|
| 53 | + | s.split_whitespace() |
|
| 54 | + | .filter_map(ObjectRef::from_hex) |
|
| 55 | + | .collect() |
|
| 50 | 56 | } |
|
| 51 | 57 | ||
| 52 | 58 | impl Repo { |
|
| … | 1 unchanged lines |
||
| 54 | 60 | /// the working copy's base state (spec §27). |
|
| 55 | 61 | pub fn blame(&self, path: &str) -> Result<Vec<BlameLine>> { |
|
| 56 | 62 | let snap = self.snapshot("oxy blame")?; |
|
| 57 | - | self.blame_state(path, &snap.base, &snap.base_snapshot, "the working copy's base state") |
|
| 63 | + | self.blame_state( |
|
| 64 | + | path, |
|
| 65 | + | &snap.base, |
|
| 66 | + | &snap.base_snapshot, |
|
| 67 | + | "the working copy's base state", |
|
| 68 | + | ) |
|
| 58 | 69 | } |
|
| 59 | 70 | ||
| 60 | 71 | /// Blame `path` in the state with these heads (hosted repositories). |
|
| … | 3 unchanged lines |
||
| 64 | 75 | self.blame_state(path, &state, &snapshot, "that state") |
|
| 65 | 76 | } |
|
| 66 | 77 | ||
| 67 | - | fn blame_state(&self, path: &str, state: &crate::state::State, snapshot: &crate::state::Snapshot, what: &str) -> Result<Vec<BlameLine>> { |
|
| 68 | - | let Some(f) = snapshot.files.get(path) else { return fail(format!("{path} is not in {what}")) }; |
|
| 78 | + | fn blame_state( |
|
| 79 | + | &self, |
|
| 80 | + | path: &str, |
|
| 81 | + | state: &crate::state::State, |
|
| 82 | + | snapshot: &crate::state::Snapshot, |
|
| 83 | + | what: &str, |
|
| 84 | + | ) -> Result<Vec<BlameLine>> { |
|
| 85 | + | let Some(f) = snapshot.files.get(path) else { |
|
| 86 | + | return fail(format!("{path} is not in {what}")); |
|
| 87 | + | }; |
|
| 69 | 88 | if !matches!(f.content, FileContent::Text { .. }) { |
|
| 70 | 89 | return fail(format!("{path} is not a text file")); |
|
| 71 | 90 | } |
|
| … | 1 unchanged lines |
||
| 73 | 92 | for (i, ((hash, index), text)) in state.live_lines(&f.node).into_iter().enumerate() { |
|
| 74 | 93 | let meta = state.get(&hash); |
|
| 75 | 94 | let author = match meta { |
|
| 76 | - | Some(m) if !m.author.is_zero() => self.store.get_identity(&m.author).map(|id| id.name).unwrap_or_else(|_| m.author.short(12)), |
|
| 77 | - | Some(m) => m.message.lines().rev().find_map(|l| l.strip_prefix("Git-Author: ")).map(|a| a.split('<').next().unwrap_or(a).trim().to_string()).unwrap_or_else(|| "unknown".into()), |
|
| 95 | + | Some(m) if !m.author.is_zero() => self |
|
| 96 | + | .store |
|
| 97 | + | .get_identity(&m.author) |
|
| 98 | + | .map(|id| id.name) |
|
| 99 | + | .unwrap_or_else(|_| m.author.short(12)), |
|
| 100 | + | Some(m) => m |
|
| 101 | + | .message |
|
| 102 | + | .lines() |
|
| 103 | + | .rev() |
|
| 104 | + | .find_map(|l| l.strip_prefix("Git-Author: ")) |
|
| 105 | + | .map(|a| a.split('<').next().unwrap_or(a).trim().to_string()) |
|
| 106 | + | .unwrap_or_else(|| "unknown".into()), |
|
| 78 | 107 | None => "unknown".into(), |
|
| 79 | 108 | }; |
|
| 80 | 109 | out.push(BlameLine { |
|
| … | 21 unchanged lines |
||
| 102 | 131 | return Ok(None); |
|
| 103 | 132 | } |
|
| 104 | 133 | let read = |name: &str| fs::read_to_string(dir.join(name)).unwrap_or_default(); |
|
| 105 | - | let nums: Vec<usize> = read("range").split_whitespace().filter_map(|n| n.parse().ok()).collect(); |
|
| 134 | + | let nums: Vec<usize> = read("range") |
|
| 135 | + | .split_whitespace() |
|
| 136 | + | .filter_map(|n| n.parse().ok()) |
|
| 137 | + | .collect(); |
|
| 106 | 138 | Ok(Some(BisectState { |
|
| 107 | 139 | start: hashes_from(&read("start")), |
|
| 108 | 140 | good: hashes_from(&read("good")), |
|
| … | 2 unchanged lines |
||
| 111 | 143 | lo: nums.first().copied().unwrap_or(0), |
|
| 112 | 144 | hi: nums.get(1).copied().unwrap_or(0), |
|
| 113 | 145 | testing: nums.get(2).copied().filter(|_| nums.len() > 2), |
|
| 114 | - | skipped: read("skipped").split_whitespace().filter_map(|n| n.parse().ok()).collect(), |
|
| 146 | + | skipped: read("skipped") |
|
| 147 | + | .split_whitespace() |
|
| 148 | + | .filter_map(|n| n.parse().ok()) |
|
| 149 | + | .collect(), |
|
| 115 | 150 | })) |
|
| 116 | 151 | } |
|
| 117 | 152 | ||
| … | 3 unchanged lines |
||
| 121 | 156 | write_atomic(&dir.join("start"), hashes_line(&st.start).as_bytes())?; |
|
| 122 | 157 | write_atomic(&dir.join("good"), hashes_line(&st.good).as_bytes())?; |
|
| 123 | 158 | write_atomic(&dir.join("bad"), hashes_line(&st.bad).as_bytes())?; |
|
| 124 | - | write_atomic(&dir.join("candidates"), hashes_line(&st.candidates).as_bytes())?; |
|
| 159 | + | write_atomic( |
|
| 160 | + | &dir.join("candidates"), |
|
| 161 | + | hashes_line(&st.candidates).as_bytes(), |
|
| 162 | + | )?; |
|
| 125 | 163 | let mut range = format!("{} {}", st.lo, st.hi); |
|
| 126 | 164 | if let Some(t) = st.testing { |
|
| 127 | 165 | range.push_str(&format!(" {t}")); |
|
| 128 | 166 | } |
|
| 129 | 167 | write_atomic(&dir.join("range"), range.as_bytes())?; |
|
| 130 | - | write_atomic(&dir.join("skipped"), st.skipped.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(" ").as_bytes())?; |
|
| 168 | + | write_atomic( |
|
| 169 | + | &dir.join("skipped"), |
|
| 170 | + | st.skipped |
|
| 171 | + | .iter() |
|
| 172 | + | .map(|n| n.to_string()) |
|
| 173 | + | .collect::<Vec<_>>() |
|
| 174 | + | .join(" ") |
|
| 175 | + | .as_bytes(), |
|
| 176 | + | )?; |
|
| 131 | 177 | Ok(()) |
|
| 132 | 178 | } |
|
| 133 | 179 | ||
| … | 4 unchanged lines |
||
| 138 | 184 | } |
|
| 139 | 185 | let (_, wc) = self.working_copy()?; |
|
| 140 | 186 | let start = self.load_state(&wc.deps)?.maximal(&wc.deps); |
|
| 141 | - | self.bisect_save(&BisectState { start, ..Default::default() }) |
|
| 187 | + | self.bisect_save(&BisectState { |
|
| 188 | + | start, |
|
| 189 | + | ..Default::default() |
|
| 190 | + | }) |
|
| 142 | 191 | } |
|
| 143 | 192 | ||
| 144 | 193 | pub fn bisect_reset(&self) -> Result<()> { |
|
| 145 | - | let Some(st) = self.bisect_load()? else { return fail("no bisect in progress") }; |
|
| 194 | + | let Some(st) = self.bisect_load()? else { |
|
| 195 | + | return fail("no bisect in progress"); |
|
| 196 | + | }; |
|
| 146 | 197 | self.checkout(&st.start, false, "oxy bisect reset")?; |
|
| 147 | 198 | fs::remove_dir_all(self.bisect_dir())?; |
|
| 148 | 199 | Ok(()) |
|
| 149 | 200 | } |
|
| 150 | 201 | ||
| 151 | 202 | pub fn bisect_status(&self) -> Result<Option<BisectStep>> { |
|
| 152 | - | let Some(st) = self.bisect_load()? else { return Ok(None) }; |
|
| 203 | + | let Some(st) = self.bisect_load()? else { |
|
| 204 | + | return Ok(None); |
|
| 205 | + | }; |
|
| 153 | 206 | Ok(Some(self.bisect_step(&st))) |
|
| 154 | 207 | } |
|
| 155 | 208 | ||
| 156 | 209 | fn bisect_step(&self, st: &BisectState) -> BisectStep { |
|
| 157 | 210 | if st.candidates.is_empty() { |
|
| 158 | - | return BisectStep::NeedMarks { good: st.good.len(), bad: st.bad.len() }; |
|
| 211 | + | return BisectStep::NeedMarks { |
|
| 212 | + | good: st.good.len(), |
|
| 213 | + | bad: st.bad.len(), |
|
| 214 | + | }; |
|
| 159 | 215 | } |
|
| 160 | 216 | if st.hi <= st.lo + 1 { |
|
| 161 | - | return BisectStep::Found { culprit: st.candidates[st.lo] }; |
|
| 217 | + | return BisectStep::Found { |
|
| 218 | + | culprit: st.candidates[st.lo], |
|
| 219 | + | }; |
|
| 162 | 220 | } |
|
| 163 | 221 | match st.testing { |
|
| 164 | 222 | Some(_) => { |
|
| 165 | 223 | let remaining = st.hi - st.lo; |
|
| 166 | - | BisectStep::Testing { heads: self.bisect_heads(st, st.testing.unwrap()), remaining, steps_left: (remaining as f64).log2().ceil() as u32 } |
|
| 224 | + | BisectStep::Testing { |
|
| 225 | + | heads: self.bisect_heads(st, st.testing.unwrap()), |
|
| 226 | + | remaining, |
|
| 227 | + | steps_left: (remaining as f64).log2().ceil() as u32, |
|
| 228 | + | } |
|
| 167 | 229 | } |
|
| 168 | - | None => BisectStep::Inconclusive { candidates: st.candidates[st.lo..st.hi].to_vec() }, |
|
| 230 | + | None => BisectStep::Inconclusive { |
|
| 231 | + | candidates: st.candidates[st.lo..st.hi].to_vec(), |
|
| 232 | + | }, |
|
| 169 | 233 | } |
|
| 170 | 234 | } |
|
| 171 | 235 | ||
| … | 1 unchanged lines |
||
| 173 | 237 | fn bisect_heads(&self, st: &BisectState, k: usize) -> Vec<ChangeHash> { |
|
| 174 | 238 | let mut all = st.good.clone(); |
|
| 175 | 239 | all.extend(st.candidates[..k].iter().copied()); |
|
| 176 | - | self.load_state(&all).map(|s| s.maximal(&all)).unwrap_or(all) |
|
| 240 | + | self.load_state(&all) |
|
| 241 | + | .map(|s| s.maximal(&all)) |
|
| 242 | + | .unwrap_or(all) |
|
| 177 | 243 | } |
|
| 178 | 244 | ||
| 179 | 245 | /// Check out the midpoint of the open range, skipping indices marked |
|
| … | 2 unchanged lines |
||
| 182 | 248 | if st.hi <= st.lo + 1 { |
|
| 183 | 249 | st.testing = None; |
|
| 184 | 250 | self.bisect_save(st)?; |
|
| 185 | - | return Ok(BisectStep::Found { culprit: st.candidates[st.lo] }); |
|
| 251 | + | return Ok(BisectStep::Found { |
|
| 252 | + | culprit: st.candidates[st.lo], |
|
| 253 | + | }); |
|
| 186 | 254 | } |
|
| 187 | 255 | let mid = st.lo + (st.hi - st.lo) / 2; |
|
| 188 | 256 | // Try mid, then the nearest untried indices on either side. |
|
| … | 9 unchanged lines |
||
| 198 | 266 | let Some(k) = order.into_iter().find(|k| !st.skipped.contains(k)) else { |
|
| 199 | 267 | st.testing = None; |
|
| 200 | 268 | self.bisect_save(st)?; |
|
| 201 | - | return Ok(BisectStep::Inconclusive { candidates: st.candidates[st.lo..st.hi].to_vec() }); |
|
| 269 | + | return Ok(BisectStep::Inconclusive { |
|
| 270 | + | candidates: st.candidates[st.lo..st.hi].to_vec(), |
|
| 271 | + | }); |
|
| 202 | 272 | }; |
|
| 203 | 273 | let heads = self.bisect_heads(st, k); |
|
| 204 | - | self.checkout(&heads, false, &format!("oxy bisect (testing {k} of {})", st.candidates.len()))?; |
|
| 274 | + | self.checkout( |
|
| 275 | + | &heads, |
|
| 276 | + | false, |
|
| 277 | + | &format!("oxy bisect (testing {k} of {})", st.candidates.len()), |
|
| 278 | + | )?; |
|
| 205 | 279 | st.testing = Some(k); |
|
| 206 | 280 | self.bisect_save(st)?; |
|
| 207 | 281 | let remaining = st.hi - st.lo; |
|
| 208 | - | Ok(BisectStep::Testing { heads, remaining, steps_left: (remaining as f64).log2().ceil() as u32 }) |
|
| 282 | + | Ok(BisectStep::Testing { |
|
| 283 | + | heads, |
|
| 284 | + | remaining, |
|
| 285 | + | steps_left: (remaining as f64).log2().ceil() as u32, |
|
| 286 | + | }) |
|
| 209 | 287 | } |
|
| 210 | 288 | ||
| 211 | 289 | /// Mark states good or bad. With `changes`, before the search starts, |
|
| 212 | 290 | /// they name known-good or known-bad states; without, the state under |
|
| 213 | 291 | /// test gets the verdict. `None` for `good` means skip (untestable). |
|
| 214 | 292 | pub fn bisect_mark(&self, good: Option<bool>, changes: &[ChangeHash]) -> Result<BisectStep> { |
|
| 215 | - | let Some(mut st) = self.bisect_load()? else { return fail("no bisect in progress; run `oxy bisect start`") }; |
|
| 293 | + | let Some(mut st) = self.bisect_load()? else { |
|
| 294 | + | return fail("no bisect in progress; run `oxy bisect start`"); |
|
| 295 | + | }; |
|
| 216 | 296 | if !changes.is_empty() { |
|
| 217 | 297 | if !st.candidates.is_empty() { |
|
| 218 | 298 | return fail("the search has started; mark the state under test with plain `oxy bisect good|bad`"); |
|
| … | 5 unchanged lines |
||
| 224 | 304 | } |
|
| 225 | 305 | if st.good.is_empty() || st.bad.is_empty() { |
|
| 226 | 306 | self.bisect_save(&st)?; |
|
| 227 | - | return Ok(BisectStep::NeedMarks { good: st.good.len(), bad: st.bad.len() }); |
|
| 307 | + | return Ok(BisectStep::NeedMarks { |
|
| 308 | + | good: st.good.len(), |
|
| 309 | + | bad: st.bad.len(), |
|
| 310 | + | }); |
|
| 228 | 311 | } |
|
| 229 | 312 | let good_heads = self.load_state(&st.good)?.maximal(&st.good); |
|
| 230 | 313 | let bad_heads = self.load_state(&st.bad)?.maximal(&st.bad); |
|
| … | 21 unchanged lines |
||
| 252 | 335 | /// Automate with a command run in the working copy: exit 0 is good, 125 |
|
| 253 | 336 | /// skips, anything else is bad. |
|
| 254 | 337 | pub fn bisect_run(&self, command: &[String]) -> Result<BisectStep> { |
|
| 255 | - | let Some(st) = self.bisect_load()? else { return fail("no bisect in progress; run `oxy bisect start`") }; |
|
| 338 | + | let Some(st) = self.bisect_load()? else { |
|
| 339 | + | return fail("no bisect in progress; run `oxy bisect start`"); |
|
| 340 | + | }; |
|
| 256 | 341 | if st.testing.is_none() { |
|
| 257 | 342 | return Ok(self.bisect_step(&st)); |
|
| 258 | 343 | } |
|
| 259 | 344 | loop { |
|
| 260 | - | let status = std::process::Command::new(&command[0]).args(&command[1..]).current_dir(&self.root).status().map_err(|e| RepoError::Msg(format!("running {}: {e}", command[0])))?; |
|
| 345 | + | let status = std::process::Command::new(&command[0]) |
|
| 346 | + | .args(&command[1..]) |
|
| 347 | + | .current_dir(&self.root) |
|
| 348 | + | .status() |
|
| 349 | + | .map_err(|e| RepoError::Msg(format!("running {}: {e}", command[0])))?; |
|
| 261 | 350 | let verdict = match status.code() { |
|
| 262 | 351 | Some(0) => Some(true), |
|
| 263 | 352 | Some(125) => None, |
|
| … | 9 unchanged lines |
modified crates/oxy-core/src/repo/mod.rs +347 −71
| … | 8 unchanged lines |
||
| 9 | 9 | ||
| 10 | 10 | use crate::attest::{Attest, AttestBody}; |
|
| 11 | 11 | use crate::diff::{ranges, split_lines, text_edits}; |
|
| 12 | - | use crate::protocol::{open_remote, Bundle, RefResult, RefUpdate, RefsResponse, RemoteOptions, RemoteRef, SignedAttest, SignedPolicy}; |
|
| 13 | - | #[allow(unused_imports)] |
|
| 14 | - | use crate::subrepo::Pin; |
|
| 15 | 12 | use crate::hash::{hash_object, ChangeHash, KeyId, ObjectRef, ObjectType}; |
|
| 16 | 13 | use crate::identity::{verify as verify_signature, Domain, Identity, SecretKey, Signature}; |
|
| 17 | 14 | use crate::ids::{ChangeId, NodeId, WorkspaceId}; |
|
| 18 | - | use crate::objects::{Blob, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, OpRef, StateObject, TreeOp}; |
|
| 15 | + | use crate::objects::{ |
|
| 16 | + | Blob, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, OpRef, StateObject, TreeOp, |
|
| 17 | + | }; |
|
| 19 | 18 | use crate::oplog::{Format1Op, Op, View, SNAPSHOT_SUFFIX}; |
|
| 19 | + | use crate::protocol::{ |
|
| 20 | + | open_remote, Bundle, RefResult, RefUpdate, RefsResponse, RemoteOptions, RemoteRef, |
|
| 21 | + | SignedAttest, SignedPolicy, |
|
| 22 | + | }; |
|
| 20 | 23 | use crate::state::{FileContent, Snapshot, State}; |
|
| 21 | 24 | use crate::store::{Store, StoreError}; |
|
| 25 | + | #[allow(unused_imports)] |
|
| 26 | + | use crate::subrepo::Pin; |
|
| 22 | 27 | use crate::tree::OpKey; |
|
| 23 | 28 | use rand::RngCore; |
|
| 24 | 29 | use std::collections::{BTreeMap, BTreeSet, HashMap}; |
|
| … | 30 unchanged lines |
||
| 55 | 60 | ||
| 56 | 61 | mod bookmarks; |
|
| 57 | 62 | mod checkout; |
|
| 58 | - | mod subrepos; |
|
| 59 | 63 | mod checkpoint; |
|
| 60 | 64 | mod config; |
|
| 61 | 65 | mod gc; |
|
| 62 | 66 | mod git; |
|
| 63 | 67 | mod history; |
|
| 64 | 68 | mod hooks; |
|
| 69 | + | mod identity; |
|
| 65 | 70 | mod inspect; |
|
| 66 | - | mod identity; |
|
| 67 | 71 | mod policy; |
|
| 68 | 72 | mod prompt; |
|
| 69 | 73 | mod snapshot; |
|
| 74 | + | mod subrepos; |
|
| 70 | 75 | mod sync; |
|
| 71 | 76 | mod tags; |
|
| 72 | - | mod workspace; |
|
| 73 | 77 | #[cfg(test)] |
|
| 74 | 78 | mod tests; |
|
| 79 | + | mod workspace; |
|
| 75 | 80 | ||
| 76 | 81 | pub use bookmarks::{BookmarkInfo, MergeReport}; |
|
| 77 | - | pub use subrepos::{SubrepoInfo, SubrepoSync}; |
|
| 78 | 82 | pub use checkout::CheckoutReport; |
|
| 79 | 83 | pub use config::{parse_size, read_config_file, Config}; |
|
| 80 | 84 | pub use gc::GcReport; |
|
| 81 | - | pub use hooks::{json_array, json_str, HookOutcome}; |
|
| 82 | - | pub use inspect::{BisectStep, BlameLine}; |
|
| 83 | - | pub use prompt::{PromptInfo, DEFAULT_PROMPT_FORMAT, GLYPH_PROMPT_FORMAT}; |
|
| 84 | 85 | pub use git::{GitExportReport, GitImportReport}; |
|
| 86 | + | pub use hooks::{json_array, json_str, HookOutcome}; |
|
| 85 | 87 | pub use identity::{Rotation, SignatureStatus}; |
|
| 88 | + | pub use inspect::{BisectStep, BlameLine}; |
|
| 86 | 89 | pub use policy::PolicyUpdate; |
|
| 87 | - | pub use snapshot::{added_node_paths, diff_files_of, has_conflict_markers, is_binary, referenced_changes, render_change, render_hunks, similarity, ChangeStatus, DiffHunk, FileDiff, Hunk, PathStatus, Selection, SnapshotResult}; |
|
| 90 | + | pub use prompt::{PromptInfo, DEFAULT_PROMPT_FORMAT, GLYPH_PROMPT_FORMAT}; |
|
| 91 | + | pub use snapshot::{ |
|
| 92 | + | added_node_paths, diff_files_of, has_conflict_markers, is_binary, referenced_changes, |
|
| 93 | + | render_change, render_hunks, similarity, ChangeStatus, DiffHunk, FileDiff, Hunk, PathStatus, |
|
| 94 | + | Selection, SnapshotResult, |
|
| 95 | + | }; |
|
| 96 | + | pub use subrepos::{SubrepoInfo, SubrepoSync}; |
|
| 88 | 97 | pub use sync::{FetchReport, PullReport}; |
|
| 89 | 98 | pub use tags::TagInfo; |
|
| 90 | 99 | pub use workspace::{SnapshotLock, WorkspaceInfo}; |
|
| … | 34 unchanged lines |
||
| 125 | 134 | ||
| 126 | 135 | impl FsEntry { |
|
| 127 | 136 | pub(crate) fn new(kind: NodeKind, exec: bool, content: Vec<u8>) -> FsEntry { |
|
| 128 | - | FsEntry { kind, exec, content, blob: None, content_hash: None } |
|
| 137 | + | FsEntry { |
|
| 138 | + | kind, |
|
| 139 | + | exec, |
|
| 140 | + | content, |
|
| 141 | + | blob: None, |
|
| 142 | + | content_hash: None, |
|
| 143 | + | } |
|
| 129 | 144 | } |
|
| 130 | 145 | } |
|
| 131 | 146 | ||
| … | 13 unchanged lines |
||
| 145 | 160 | ||
| 146 | 161 | /// Marker file of a bare repository, inside `.oxy/`. |
|
| 147 | 162 | const BARE_MARKER: &str = "bare"; |
|
| 163 | + | /// Marker of a hosted repository that has no id yet: the first push binds |
|
| 164 | + | /// it to the pusher's id (spec §16.1). |
|
| 165 | + | const UNBOUND_MARKER: &str = "unbound"; |
|
| 148 | 166 | ||
| 149 | 167 | /// The user's oxy directory: `$OXY_HOME`, else `$XDG_CONFIG_HOME/oxy`, else |
|
| 150 | 168 | /// `~/.config/oxy`. It holds `config.toml` (spec §19) and `keys/`. |
|
| … | 51 unchanged lines |
||
| 202 | 220 | let workspace = WorkspaceId::random(); |
|
| 203 | 221 | write_atomic(&oxy.join("workspace"), workspace.to_hex().as_bytes())?; |
|
| 204 | 222 | let store = Store::open(oxy.join("objects"))?; |
|
| 205 | - | let repo = Repo { git_export_skipped: Default::default(), root, oxy, store, workspace, bare: false, keys_dir: default_keys_dir(), user_config: default_user_config(), config }; |
|
| 223 | + | let repo = Repo { |
|
| 224 | + | git_export_skipped: Default::default(), |
|
| 225 | + | root, |
|
| 226 | + | oxy, |
|
| 227 | + | store, |
|
| 228 | + | workspace, |
|
| 229 | + | bare: false, |
|
| 230 | + | keys_dir: default_keys_dir(), |
|
| 231 | + | user_config: default_user_config(), |
|
| 232 | + | config, |
|
| 233 | + | }; |
|
| 206 | 234 | repo.register_workspace(&workspace, &repo.root)?; |
|
| 207 | 235 | ||
| 208 | 236 | let wc = repo.new_working_copy(vec![], ""); |
|
| 209 | 237 | let wc_hash = repo.store.put_change(&wc)?; |
|
| 210 | 238 | let mut view = View::default(); |
|
| 211 | - | view.bookmarks.insert(repo.config.bookmark.clone(), repo.store.put_state(&StateObject::new(vec![]))?); |
|
| 239 | + | view.bookmarks.insert( |
|
| 240 | + | repo.config.bookmark.clone(), |
|
| 241 | + | repo.store.put_state(&StateObject::new(vec![]))?, |
|
| 242 | + | ); |
|
| 212 | 243 | view.workspaces.insert(workspace, wc_hash); |
|
| 213 | 244 | repo.record_op(&[], "oxy init", view)?; |
|
| 214 | 245 | Ok(repo) |
|
| … | 22 unchanged lines |
||
| 237 | 268 | let config = Config::load(Some(&default_user_config()), &oxy.join("config"))?; |
|
| 238 | 269 | config.save(&oxy.join("config"))?; |
|
| 239 | 270 | let store = Store::open(oxy.join("objects"))?; |
|
| 240 | - | let repo = Repo { git_export_skipped: Default::default(), root, oxy, store, workspace: WorkspaceId::ZERO, bare: true, keys_dir: default_keys_dir(), user_config: default_user_config(), config }; |
|
| 271 | + | let repo = Repo { |
|
| 272 | + | git_export_skipped: Default::default(), |
|
| 273 | + | root, |
|
| 274 | + | oxy, |
|
| 275 | + | store, |
|
| 276 | + | workspace: WorkspaceId::ZERO, |
|
| 277 | + | bare: true, |
|
| 278 | + | keys_dir: default_keys_dir(), |
|
| 279 | + | user_config: default_user_config(), |
|
| 280 | + | config, |
|
| 281 | + | }; |
|
| 241 | 282 | let mut view = View::default(); |
|
| 242 | - | view.bookmarks.insert(repo.config.bookmark.clone(), repo.store.put_state(&StateObject::new(vec![]))?); |
|
| 283 | + | view.bookmarks.insert( |
|
| 284 | + | repo.config.bookmark.clone(), |
|
| 285 | + | repo.store.put_state(&StateObject::new(vec![]))?, |
|
| 286 | + | ); |
|
| 243 | 287 | repo.record_op(&[], "oxy init --bare", view)?; |
|
| 244 | 288 | Ok(repo) |
|
| 245 | 289 | } |
|
| 246 | 290 | ||
| 291 | + | /// A bare repository with no id yet. Its `id()` is all zero until the |
|
| 292 | + | /// first push binds it (`bind_id`), so an existing repository can be |
|
| 293 | + | /// pushed to a freshly created hosted one and keep its identity. |
|
| 294 | + | pub fn init_bare_unbound(root: &Path) -> Result<Repo> { |
|
| 295 | + | let repo = Self::init_bare_with_id(root, [0; 16])?; |
|
| 296 | + | fs::remove_file(repo.oxy.join("id"))?; |
|
| 297 | + | write_atomic(&repo.oxy.join(UNBOUND_MARKER), b"")?; |
|
| 298 | + | Ok(repo) |
|
| 299 | + | } |
|
| 300 | + | ||
| 301 | + | pub fn is_unbound(&self) -> bool { |
|
| 302 | + | self.oxy.join(UNBOUND_MARKER).is_file() |
|
| 303 | + | } |
|
| 304 | + | ||
| 305 | + | /// Give an unbound repository its id. |
|
| 306 | + | pub fn bind_id(&self, id: [u8; 16]) -> Result<()> { |
|
| 307 | + | if !self.is_unbound() { |
|
| 308 | + | return fail("this repository already has an id"); |
|
| 309 | + | } |
|
| 310 | + | if id == [0; 16] { |
|
| 311 | + | return fail("a repository id cannot be all zero"); |
|
| 312 | + | } |
|
| 313 | + | write_atomic(&self.oxy.join("id"), hex::encode(id).as_bytes())?; |
|
| 314 | + | fs::remove_file(self.oxy.join(UNBOUND_MARKER))?; |
|
| 315 | + | Ok(()) |
|
| 316 | + | } |
|
| 317 | + | ||
| 247 | 318 | /// Whether this repository has no working copy (`init_bare`). |
|
| 248 | 319 | pub fn is_bare(&self) -> bool { |
|
| 249 | 320 | self.bare |
|
| … | 17 unchanged lines |
||
| 267 | 338 | } |
|
| 268 | 339 | let config = Config::load(Some(&default_user_config()), &shared.join("config"))?; |
|
| 269 | 340 | let store = Store::open(shared.join("objects"))?; |
|
| 270 | - | let mut repo = Repo { git_export_skipped: Default::default(), root: d.to_path_buf(), oxy: shared, store, workspace, bare: false, keys_dir: default_keys_dir(), user_config: default_user_config(), config }; |
|
| 341 | + | let mut repo = Repo { |
|
| 342 | + | git_export_skipped: Default::default(), |
|
| 343 | + | root: d.to_path_buf(), |
|
| 344 | + | oxy: shared, |
|
| 345 | + | store, |
|
| 346 | + | workspace, |
|
| 347 | + | bare: false, |
|
| 348 | + | keys_dir: default_keys_dir(), |
|
| 349 | + | user_config: default_user_config(), |
|
| 350 | + | config, |
|
| 351 | + | }; |
|
| 271 | 352 | repo.apply_workspace_bookmark(); |
|
| 272 | 353 | return Ok(repo); |
|
| 273 | 354 | } |
|
| … | 4 unchanged lines |
||
| 278 | 359 | let workspace = if bare { |
|
| 279 | 360 | WorkspaceId::ZERO |
|
| 280 | 361 | } else { |
|
| 281 | - | WorkspaceId::from_hex(fs::read_to_string(oxy.join("workspace"))?.trim()).ok_or_else(|| RepoError::Msg("corrupt .oxy/workspace".into()))? |
|
| 362 | + | WorkspaceId::from_hex(fs::read_to_string(oxy.join("workspace"))?.trim()) |
|
| 363 | + | .ok_or_else(|| RepoError::Msg("corrupt .oxy/workspace".into()))? |
|
| 282 | 364 | }; |
|
| 283 | 365 | let store = Store::open(oxy.join("objects"))?; |
|
| 284 | - | let mut repo = Repo { git_export_skipped: Default::default(), root: d.to_path_buf(), oxy, store, workspace, bare, keys_dir: default_keys_dir(), user_config: default_user_config(), config }; |
|
| 366 | + | let mut repo = Repo { |
|
| 367 | + | git_export_skipped: Default::default(), |
|
| 368 | + | root: d.to_path_buf(), |
|
| 369 | + | oxy, |
|
| 370 | + | store, |
|
| 371 | + | workspace, |
|
| 372 | + | bare, |
|
| 373 | + | keys_dir: default_keys_dir(), |
|
| 374 | + | user_config: default_user_config(), |
|
| 375 | + | config, |
|
| 376 | + | }; |
|
| 285 | 377 | if !bare { |
|
| 286 | 378 | repo.apply_workspace_bookmark(); |
|
| 287 | 379 | } |
|
| … | 6 unchanged lines |
||
| 294 | 386 | } |
|
| 295 | 387 | dir = d.parent(); |
|
| 296 | 388 | } |
|
| 297 | - | fail(format!("no oxy repository found at or above {}", start.display())) |
|
| 389 | + | fail(format!( |
|
| 390 | + | "no oxy repository found at or above {}", |
|
| 391 | + | start.display() |
|
| 392 | + | )) |
|
| 298 | 393 | } |
|
| 299 | 394 | ||
| 300 | 395 | /// Rewrite a format-1 operation log, whose bookmarks were head lists, |
|
| 301 | 396 | /// into format 2, where each bookmark names a `state` object. |
|
| 302 | 397 | fn migrate_format1(&self) -> Result<()> { |
|
| 303 | 398 | let hex = fs::read_to_string(self.oxy.join("ops").join("head"))?; |
|
| 304 | - | let head = ObjectRef::from_hex(hex.trim()).ok_or_else(|| RepoError::Msg("corrupt .oxy/ops/head".into()))?; |
|
| 399 | + | let head = ObjectRef::from_hex(hex.trim()) |
|
| 400 | + | .ok_or_else(|| RepoError::Msg("corrupt .oxy/ops/head".into()))?; |
|
| 305 | 401 | // Collect the chain root-first. |
|
| 306 | 402 | let mut chain: Vec<Format1Op> = Vec::new(); |
|
| 307 | 403 | let mut cur = Some(head); |
|
| … | 7 unchanged lines |
||
| 315 | 411 | for old in chain { |
|
| 316 | 412 | let mut view = View::default(); |
|
| 317 | 413 | for (name, heads) in &old.view.bookmarks { |
|
| 318 | - | view.bookmarks.insert(name.clone(), self.store.put_state(&StateObject::new(heads.clone()))?); |
|
| 414 | + | view.bookmarks.insert( |
|
| 415 | + | name.clone(), |
|
| 416 | + | self.store.put_state(&StateObject::new(heads.clone()))?, |
|
| 417 | + | ); |
|
| 319 | 418 | } |
|
| 320 | 419 | view.workspaces = old.view.workspaces.clone(); |
|
| 321 | - | let op = Op { parents: new_parent.into_iter().collect(), hlc: old.hlc, command: old.command, view }; |
|
| 420 | + | let op = Op { |
|
| 421 | + | parents: new_parent.into_iter().collect(), |
|
| 422 | + | hlc: old.hlc, |
|
| 423 | + | command: old.command, |
|
| 424 | + | view, |
|
| 425 | + | }; |
|
| 322 | 426 | new_parent = Some(self.store.put(ObjectType::Op, &op.encode())?); |
|
| 323 | 427 | } |
|
| 324 | - | write_atomic(&self.oxy.join("ops").join("head"), new_parent.unwrap().to_hex().as_bytes())?; |
|
| 428 | + | write_atomic( |
|
| 429 | + | &self.oxy.join("ops").join("head"), |
|
| 430 | + | new_parent.unwrap().to_hex().as_bytes(), |
|
| 431 | + | )?; |
|
| 325 | 432 | write_atomic(&self.oxy.join("format"), FORMAT_LINE.as_bytes())?; |
|
| 326 | 433 | Ok(()) |
|
| 327 | 434 | } |
|
| … | 9 unchanged lines |
||
| 337 | 444 | ||
| 338 | 445 | /// The repository ID (spec §14.4). Repositories made before IDs existed get one on first use. |
|
| 339 | 446 | pub fn id(&self) -> Result<[u8; 16]> { |
|
| 447 | + | if self.is_unbound() { |
|
| 448 | + | return Ok([0; 16]); |
|
| 449 | + | } |
|
| 340 | 450 | let path = self.oxy.join("id"); |
|
| 341 | 451 | match fs::read_to_string(&path) { |
|
| 342 | - | Ok(h) => hex::decode(h.trim()).ok().and_then(|v| v.try_into().ok()).ok_or_else(|| RepoError::Msg("corrupt .oxy/id".into())), |
|
| 452 | + | Ok(h) => hex::decode(h.trim()) |
|
| 453 | + | .ok() |
|
| 454 | + | .and_then(|v| v.try_into().ok()) |
|
| 455 | + | .ok_or_else(|| RepoError::Msg("corrupt .oxy/id".into())), |
|
| 343 | 456 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => { |
|
| 344 | 457 | let mut id = [0u8; 16]; |
|
| 345 | 458 | rand::rngs::OsRng.fill_bytes(&mut id); |
|
| … | 30 unchanged lines |
||
| 376 | 489 | /// The workspace's current bookmark (spec §10.1) overrides the |
|
| 377 | 490 | /// configured default when `oxy bookmark switch` has recorded one. |
|
| 378 | 491 | fn apply_workspace_bookmark(&mut self) { |
|
| 379 | - | if let Ok(b) = fs::read_to_string(self.oxy.join("workspaces").join(self.workspace.to_hex()).join("bookmark")) { |
|
| 492 | + | if let Ok(b) = fs::read_to_string( |
|
| 493 | + | self.oxy |
|
| 494 | + | .join("workspaces") |
|
| 495 | + | .join(self.workspace.to_hex()) |
|
| 496 | + | .join("bookmark"), |
|
| 497 | + | ) { |
|
| 380 | 498 | let b = b.trim(); |
|
| 381 | 499 | if !b.is_empty() { |
|
| 382 | 500 | self.config.bookmark = b.to_string(); |
|
| … | 41 unchanged lines |
||
| 424 | 542 | ||
| 425 | 543 | pub fn head_op(&self) -> Result<(ObjectRef, Op)> { |
|
| 426 | 544 | let hex = fs::read_to_string(self.oxy.join("ops").join("head"))?; |
|
| 427 | - | let r = ObjectRef::from_hex(hex.trim()).ok_or_else(|| RepoError::Msg("corrupt .oxy/ops/head".into()))?; |
|
| 545 | + | let r = ObjectRef::from_hex(hex.trim()) |
|
| 546 | + | .ok_or_else(|| RepoError::Msg("corrupt .oxy/ops/head".into()))?; |
|
| 428 | 547 | let op = Op::decode(&self.store.get_typed(&r, ObjectType::Op)?)?; |
|
| 429 | 548 | Ok((r, op)) |
|
| 430 | 549 | } |
|
| … | 3 unchanged lines |
||
| 434 | 553 | } |
|
| 435 | 554 | ||
| 436 | 555 | fn record_op(&self, parents: &[ObjectRef], command: &str, view: View) -> Result<ObjectRef> { |
|
| 437 | - | let op = Op { parents: parents.to_vec(), hlc: Hlc::now(), command: command.into(), view }; |
|
| 556 | + | let op = Op { |
|
| 557 | + | parents: parents.to_vec(), |
|
| 558 | + | hlc: Hlc::now(), |
|
| 559 | + | command: command.into(), |
|
| 560 | + | view, |
|
| 561 | + | }; |
|
| 438 | 562 | let r = self.store.put(ObjectType::Op, &op.encode())?; |
|
| 439 | 563 | write_atomic(&self.oxy.join("ops").join("head"), r.to_hex().as_bytes())?; |
|
| 440 | 564 | Ok(r) |
|
| … | 22 unchanged lines |
||
| 463 | 587 | pub fn undo(&self) -> Result<Op> { |
|
| 464 | 588 | let (_, mut op) = self.head_op()?; |
|
| 465 | 589 | while op.is_snapshot() { |
|
| 466 | - | let Some(parent) = op.parents.first() else { break }; |
|
| 590 | + | let Some(parent) = op.parents.first() else { |
|
| 591 | + | break; |
|
| 592 | + | }; |
|
| 467 | 593 | op = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?; |
|
| 468 | 594 | } |
|
| 469 | 595 | let Some(parent) = op.parents.first() else { |
|
| 470 | 596 | return fail("nothing to undo"); |
|
| 471 | 597 | }; |
|
| 472 | 598 | let parent_op = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?; |
|
| 473 | - | write_atomic(&self.oxy.join("ops").join("head"), parent.to_hex().as_bytes())?; |
|
| 599 | + | write_atomic( |
|
| 600 | + | &self.oxy.join("ops").join("head"), |
|
| 601 | + | parent.to_hex().as_bytes(), |
|
| 602 | + | )?; |
|
| 474 | 603 | self.drop_manifest(); |
|
| 475 | 604 | Ok(parent_op) |
|
| 476 | 605 | } |
|
| … | 2 unchanged lines |
||
| 479 | 608 | pub fn last_command(&self) -> Result<Op> { |
|
| 480 | 609 | let (_, mut op) = self.head_op()?; |
|
| 481 | 610 | while op.is_snapshot() { |
|
| 482 | - | let Some(parent) = op.parents.first() else { break }; |
|
| 611 | + | let Some(parent) = op.parents.first() else { |
|
| 612 | + | break; |
|
| 613 | + | }; |
|
| 483 | 614 | op = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?; |
|
| 484 | 615 | } |
|
| 485 | 616 | Ok(op) |
|
| … | 55 unchanged lines |
||
| 541 | 672 | fn blob_for(content: &[u8]) -> Blob { |
|
| 542 | 673 | Blob { |
|
| 543 | 674 | length: content.len() as u64, |
|
| 544 | - | chunks: crate::chunk::chunks(content).into_iter().map(|c| hash_object(ObjectType::Chunk, c)).collect(), |
|
| 675 | + | chunks: crate::chunk::chunks(content) |
|
| 676 | + | .into_iter() |
|
| 677 | + | .map(|c| hash_object(ObjectType::Chunk, c)) |
|
| 678 | + | .collect(), |
|
| 545 | 679 | } |
|
| 546 | 680 | } |
|
| 547 | 681 | ||
| … | 5 unchanged lines |
||
| 553 | 687 | for c in crate::chunk::chunks(content) { |
|
| 554 | 688 | self.store.put_chunk(c)?; |
|
| 555 | 689 | } |
|
| 556 | - | Ok(self.store.put(ObjectType::Blob, &Self::blob_for(content).encode())?) |
|
| 690 | + | Ok(self |
|
| 691 | + | .store |
|
| 692 | + | .put(ObjectType::Blob, &Self::blob_for(content).encode())?) |
|
| 557 | 693 | } |
|
| 558 | 694 | ||
| 559 | 695 | /// Read a blob's content, fetching it from the remote that withheld it |
|
| … | 19 unchanged lines |
||
| 579 | 715 | files.push((".oxyattributes".into(), b)); |
|
| 580 | 716 | } |
|
| 581 | 717 | let mut wb = ignore::WalkBuilder::new(&self.root); |
|
| 582 | - | wb.hidden(false).git_ignore(false).git_global(false).git_exclude(false).require_git(false).follow_links(false).add_custom_ignore_filename(".oxyignore"); |
|
| 718 | + | wb.hidden(false) |
|
| 719 | + | .git_ignore(false) |
|
| 720 | + | .git_global(false) |
|
| 721 | + | .git_exclude(false) |
|
| 722 | + | .require_git(false) |
|
| 723 | + | .follow_links(false) |
|
| 724 | + | .add_custom_ignore_filename(".oxyignore"); |
|
| 583 | 725 | wb.filter_entry(|e| e.file_name() != OXY_DIR); |
|
| 584 | 726 | for entry in wb.build().flatten() { |
|
| 585 | - | if entry.file_name() == ".oxyattributes" && entry.path() != self.root.join(".oxyattributes") { |
|
| 727 | + | if entry.file_name() == ".oxyattributes" |
|
| 728 | + | && entry.path() != self.root.join(".oxyattributes") |
|
| 729 | + | { |
|
| 586 | 730 | if let Ok(b) = fs::read(entry.path()) { |
|
| 587 | - | let rel: String = entry.path().strip_prefix(&self.root).unwrap().components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::<Vec<_>>().join("/"); |
|
| 731 | + | let rel: String = entry |
|
| 732 | + | .path() |
|
| 733 | + | .strip_prefix(&self.root) |
|
| 734 | + | .unwrap() |
|
| 735 | + | .components() |
|
| 736 | + | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 737 | + | .collect::<Vec<_>>() |
|
| 738 | + | .join("/"); |
|
| 588 | 739 | files.push((rel, b)); |
|
| 589 | 740 | } |
|
| 590 | 741 | } |
|
| 591 | 742 | } |
|
| 592 | - | crate::attributes::Attributes::from_files(files.iter().map(|(p, c)| (p.as_str(), c.as_slice()))) |
|
| 743 | + | crate::attributes::Attributes::from_files( |
|
| 744 | + | files.iter().map(|(p, c)| (p.as_str(), c.as_slice())), |
|
| 745 | + | ) |
|
| 593 | 746 | } |
|
| 594 | 747 | ||
| 595 | 748 | /// Attributes from a materialized state's `.oxyattributes` files. |
|
| 596 | 749 | pub fn state_attributes(snapshot: &Snapshot) -> crate::attributes::Attributes { |
|
| 597 | - | crate::attributes::Attributes::from_files(snapshot.files.iter().filter_map(|(p, f)| match &f.content { |
|
| 598 | - | FileContent::Text { bytes, .. } if p == ".oxyattributes" || p.ends_with("/.oxyattributes") => Some((p.as_str(), bytes.as_slice())), |
|
| 599 | - | _ => None, |
|
| 750 | + | crate::attributes::Attributes::from_files(snapshot.files.iter().filter_map(|(p, f)| { |
|
| 751 | + | match &f.content { |
|
| 752 | + | FileContent::Text { bytes, .. } |
|
| 753 | + | if p == ".oxyattributes" || p.ends_with("/.oxyattributes") => |
|
| 754 | + | { |
|
| 755 | + | Some((p.as_str(), bytes.as_slice())) |
|
| 756 | + | } |
|
| 757 | + | _ => None, |
|
| 758 | + | } |
|
| 600 | 759 | })) |
|
| 601 | 760 | } |
|
| 602 | 761 | ||
| … | 5 unchanged lines |
||
| 608 | 767 | for c in crate::chunk::chunks(content) { |
|
| 609 | 768 | self.store.put_chunk_nocompress(c)?; |
|
| 610 | 769 | } |
|
| 611 | - | Ok(self.store.put(ObjectType::Blob, &Self::blob_for(content).encode())?) |
|
| 770 | + | Ok(self |
|
| 771 | + | .store |
|
| 772 | + | .put(ObjectType::Blob, &Self::blob_for(content).encode())?) |
|
| 612 | 773 | } |
|
| 613 | 774 | ||
| 614 | 775 | fn locks_dir(&self) -> PathBuf { |
|
| … | 12 unchanged lines |
||
| 627 | 788 | return fail(format!("{path} is not marked lockable in .oxyattributes")); |
|
| 628 | 789 | } |
|
| 629 | 790 | fs::create_dir_all(self.locks_dir())?; |
|
| 630 | - | write_atomic(&self.locks_dir().join(path.replace('/', "%2F")), std::process::id().to_string().as_bytes())?; |
|
| 791 | + | write_atomic( |
|
| 792 | + | &self.locks_dir().join(path.replace('/', "%2F")), |
|
| 793 | + | std::process::id().to_string().as_bytes(), |
|
| 794 | + | )?; |
|
| 631 | 795 | set_readonly(&self.root.join(path), false)?; |
|
| 632 | 796 | Ok(()) |
|
| 633 | 797 | } |
|
| … | 9 unchanged lines |
||
| 643 | 807 | ||
| 644 | 808 | /// Whether a repository path is inside the sparse filter (spec §17). |
|
| 645 | 809 | pub fn in_scope(&self, path: &str) -> bool { |
|
| 646 | - | self.config.sparse_paths.is_empty() || self.config.sparse_paths.iter().any(|p| path == p || path.starts_with(&format!("{p}/"))) |
|
| 810 | + | self.config.sparse_paths.is_empty() |
|
| 811 | + | || self |
|
| 812 | + | .config |
|
| 813 | + | .sparse_paths |
|
| 814 | + | .iter() |
|
| 815 | + | .any(|p| path == p || path.starts_with(&format!("{p}/"))) |
|
| 647 | 816 | } |
|
| 648 | 817 | ||
| 649 | 818 | fn walk_working_copy(&self) -> Result<BTreeMap<String, FsEntry>> { |
|
| … | 3 unchanged lines |
||
| 653 | 822 | /// A walker over the working copy that never enters a nested repository |
|
| 654 | 823 | /// (a directory below the root holding `.oxy` or `.git`); the directories |
|
| 655 | 824 | /// it skipped are collected into `nested`. |
|
| 656 | - | pub(crate) fn walker(&self, nested: &std::sync::Arc<std::sync::Mutex<Vec<PathBuf>>>) -> ignore::WalkBuilder { |
|
| 825 | + | pub(crate) fn walker( |
|
| 826 | + | &self, |
|
| 827 | + | nested: &std::sync::Arc<std::sync::Mutex<Vec<PathBuf>>>, |
|
| 828 | + | ) -> ignore::WalkBuilder { |
|
| 657 | 829 | let mut wb = ignore::WalkBuilder::new(&self.root); |
|
| 658 | 830 | wb.hidden(false) |
|
| 659 | 831 | .git_ignore(false) |
|
| … | 7 unchanged lines |
||
| 667 | 839 | if e.file_name() == OXY_DIR { |
|
| 668 | 840 | return false; |
|
| 669 | 841 | } |
|
| 670 | - | if e.depth() > 0 && e.file_type().is_some_and(|t| t.is_dir()) && (e.path().join(OXY_DIR).exists() || e.path().join(".git").exists()) { |
|
| 842 | + | if e.depth() > 0 |
|
| 843 | + | && e.file_type().is_some_and(|t| t.is_dir()) |
|
| 844 | + | && (e.path().join(OXY_DIR).exists() || e.path().join(".git").exists()) |
|
| 845 | + | { |
|
| 671 | 846 | nested.lock().unwrap().push(e.path().to_path_buf()); |
|
| 672 | 847 | return false; |
|
| 673 | 848 | } |
|
| … | 10 unchanged lines |
||
| 684 | 859 | /// `.oxyignore` applies to untracked paths only: every path in `tracked` |
|
| 685 | 860 | /// (the base state's files) that exists on disk is included even when |
|
| 686 | 861 | /// an ignore rule matches it, as in Git. |
|
| 687 | - | fn walk_working_copy_with(&self, cache: Option<&prompt::Manifest>, tracked: &BTreeMap<String, (NodeKind, Option<ObjectRef>)>) -> Result<BTreeMap<String, FsEntry>> { |
|
| 862 | + | fn walk_working_copy_with( |
|
| 863 | + | &self, |
|
| 864 | + | cache: Option<&prompt::Manifest>, |
|
| 865 | + | tracked: &BTreeMap<String, (NodeKind, Option<ObjectRef>)>, |
|
| 866 | + | ) -> Result<BTreeMap<String, FsEntry>> { |
|
| 688 | 867 | let nested = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); |
|
| 689 | 868 | let wb = self.walker(&nested); |
|
| 690 | 869 | let mut out = BTreeMap::new(); |
|
| … | 4 unchanged lines |
||
| 695 | 874 | continue; |
|
| 696 | 875 | } |
|
| 697 | 876 | let rel = path.strip_prefix(&self.root).unwrap(); |
|
| 698 | - | let rel: String = rel.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::<Vec<_>>().join("/"); |
|
| 699 | - | let Some(ft) = entry.file_type() else { continue }; |
|
| 877 | + | let rel: String = rel |
|
| 878 | + | .components() |
|
| 879 | + | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 880 | + | .collect::<Vec<_>>() |
|
| 881 | + | .join("/"); |
|
| 882 | + | let Some(ft) = entry.file_type() else { |
|
| 883 | + | continue; |
|
| 884 | + | }; |
|
| 700 | 885 | if ft.is_dir() || !self.in_scope(&rel) { |
|
| 701 | 886 | continue; |
|
| 702 | 887 | } |
|
| 703 | 888 | if ft.is_symlink() { |
|
| 704 | 889 | let target = fs::read_link(path)?; |
|
| 705 | - | out.insert(rel, FsEntry::new(NodeKind::Symlink, false, target.to_string_lossy().as_bytes().to_vec())); |
|
| 890 | + | out.insert( |
|
| 891 | + | rel, |
|
| 892 | + | FsEntry::new( |
|
| 893 | + | NodeKind::Symlink, |
|
| 894 | + | false, |
|
| 895 | + | target.to_string_lossy().as_bytes().to_vec(), |
|
| 896 | + | ), |
|
| 897 | + | ); |
|
| 706 | 898 | } else if ft.is_file() { |
|
| 707 | - | let meta = entry.metadata().map_err(|e| RepoError::Msg(e.to_string()))?; |
|
| 899 | + | let meta = entry |
|
| 900 | + | .metadata() |
|
| 901 | + | .map_err(|e| RepoError::Msg(e.to_string()))?; |
|
| 708 | 902 | let e = self.file_entry(&rel, path, &meta, cache)?; |
|
| 709 | 903 | out.insert(rel, e); |
|
| 710 | 904 | } |
|
| … | 15 unchanged lines |
||
| 726 | 920 | continue; |
|
| 727 | 921 | } |
|
| 728 | 922 | let registered = modules.entries.get(&rel); |
|
| 729 | - | if registered.is_none() && tracked.get(&rel).map(|(k, _)| *k) != Some(NodeKind::Subrepo) { |
|
| 923 | + | if registered.is_none() && tracked.get(&rel).map(|(k, _)| *k) != Some(NodeKind::Subrepo) |
|
| 924 | + | { |
|
| 730 | 925 | continue; |
|
| 731 | 926 | } |
|
| 732 | 927 | let pin = subrepos::read_pin(&dir, registered.and_then(|m| m.bookmark.as_deref()))?; |
|
| … | 9 unchanged lines |
||
| 742 | 937 | if removed.contains(rel) { |
|
| 743 | 938 | continue; |
|
| 744 | 939 | } |
|
| 745 | - | let present = path.is_dir() && (path.join(OXY_DIR).exists() || path.join(".git").exists()); |
|
| 940 | + | let present = |
|
| 941 | + | path.is_dir() && (path.join(OXY_DIR).exists() || path.join(".git").exists()); |
|
| 746 | 942 | if present { |
|
| 747 | - | let pin = subrepos::read_pin(&path, modules.entries.get(rel).and_then(|m| m.bookmark.as_deref()))?; |
|
| 748 | - | out.insert(rel.clone(), FsEntry::new(NodeKind::Subrepo, false, pin.encode())); |
|
| 943 | + | let pin = subrepos::read_pin( |
|
| 944 | + | &path, |
|
| 945 | + | modules.entries.get(rel).and_then(|m| m.bookmark.as_deref()), |
|
| 946 | + | )?; |
|
| 947 | + | out.insert( |
|
| 948 | + | rel.clone(), |
|
| 949 | + | FsEntry::new(NodeKind::Subrepo, false, pin.encode()), |
|
| 950 | + | ); |
|
| 749 | 951 | } else if let Some(b) = blob { |
|
| 750 | 952 | // Not cloned (or emptied): the pin stays as recorded, as a |
|
| 751 | 953 | // Git gitlink does when its directory is absent. |
|
| 752 | - | out.insert(rel.clone(), FsEntry { kind: NodeKind::Subrepo, exec: false, content: vec![], blob: Some(*b), content_hash: None }); |
|
| 954 | + | out.insert( |
|
| 955 | + | rel.clone(), |
|
| 956 | + | FsEntry { |
|
| 957 | + | kind: NodeKind::Subrepo, |
|
| 958 | + | exec: false, |
|
| 959 | + | content: vec![], |
|
| 960 | + | blob: Some(*b), |
|
| 961 | + | content_hash: None, |
|
| 962 | + | }, |
|
| 963 | + | ); |
|
| 753 | 964 | } |
|
| 754 | 965 | continue; |
|
| 755 | 966 | } |
|
| 756 | - | let Ok(meta) = fs::symlink_metadata(&path) else { continue }; |
|
| 967 | + | let Ok(meta) = fs::symlink_metadata(&path) else { |
|
| 968 | + | continue; |
|
| 969 | + | }; |
|
| 757 | 970 | let ft = meta.file_type(); |
|
| 758 | 971 | if ft.is_symlink() { |
|
| 759 | 972 | let target = fs::read_link(&path)?; |
|
| 760 | - | out.insert(rel.clone(), FsEntry::new(NodeKind::Symlink, false, target.to_string_lossy().as_bytes().to_vec())); |
|
| 973 | + | out.insert( |
|
| 974 | + | rel.clone(), |
|
| 975 | + | FsEntry::new( |
|
| 976 | + | NodeKind::Symlink, |
|
| 977 | + | false, |
|
| 978 | + | target.to_string_lossy().as_bytes().to_vec(), |
|
| 979 | + | ), |
|
| 980 | + | ); |
|
| 761 | 981 | } else if ft.is_file() { |
|
| 762 | 982 | let e = self.file_entry(rel, &path, &meta, cache)?; |
|
| 763 | 983 | out.insert(rel.clone(), e); |
|
| … | 3 unchanged lines |
||
| 767 | 987 | } |
|
| 768 | 988 | ||
| 769 | 989 | /// The entry for one regular file, read or served from the stat cache. |
|
| 770 | - | fn file_entry(&self, rel: &str, path: &Path, meta: &fs::Metadata, cache: Option<&prompt::Manifest>) -> Result<FsEntry> { |
|
| 990 | + | fn file_entry( |
|
| 991 | + | &self, |
|
| 992 | + | rel: &str, |
|
| 993 | + | path: &Path, |
|
| 994 | + | meta: &fs::Metadata, |
|
| 995 | + | cache: Option<&prompt::Manifest>, |
|
| 996 | + | ) -> Result<FsEntry> { |
|
| 771 | 997 | let exec = is_executable(meta); |
|
| 772 | 998 | let cached = cache.and_then(|m| { |
|
| 773 | 999 | let e = m.entries.get(rel)?; |
|
| … | 2 unchanged lines |
||
| 776 | 1002 | (e.size == meta.len() && e.mtime_ns == mtime_ns && !racy).then_some(e) |
|
| 777 | 1003 | }); |
|
| 778 | 1004 | Ok(match cached { |
|
| 779 | - | Some(e) if e.blob.is_some_and(|b| self.store.has(&b)) => FsEntry { kind: NodeKind::File, exec, content: vec![], blob: e.blob, content_hash: Some(e.hash.clone()) }, |
|
| 780 | - | Some(e) => FsEntry { kind: NodeKind::File, exec, content: fs::read(path)?, blob: None, content_hash: Some(e.hash.clone()) }, |
|
| 1005 | + | Some(e) if e.blob.is_some_and(|b| self.store.has(&b)) => FsEntry { |
|
| 1006 | + | kind: NodeKind::File, |
|
| 1007 | + | exec, |
|
| 1008 | + | content: vec![], |
|
| 1009 | + | blob: e.blob, |
|
| 1010 | + | content_hash: Some(e.hash.clone()), |
|
| 1011 | + | }, |
|
| 1012 | + | Some(e) => FsEntry { |
|
| 1013 | + | kind: NodeKind::File, |
|
| 1014 | + | exec, |
|
| 1015 | + | content: fs::read(path)?, |
|
| 1016 | + | blob: None, |
|
| 1017 | + | content_hash: Some(e.hash.clone()), |
|
| 1018 | + | }, |
|
| 781 | 1019 | None => FsEntry::new(NodeKind::File, exec, fs::read(path)?), |
|
| 782 | 1020 | }) |
|
| 783 | 1021 | } |
|
| 784 | 1022 | ||
| 785 | 1023 | /// The non-directory paths of a snapshot with their kinds, for the |
|
| 786 | 1024 | /// walker's `tracked` set. |
|
| 787 | - | pub(crate) fn tracked_kinds(snapshot: &Snapshot) -> BTreeMap<String, (NodeKind, Option<ObjectRef>)> { |
|
| 1025 | + | pub(crate) fn tracked_kinds( |
|
| 1026 | + | snapshot: &Snapshot, |
|
| 1027 | + | ) -> BTreeMap<String, (NodeKind, Option<ObjectRef>)> { |
|
| 788 | 1028 | snapshot |
|
| 789 | 1029 | .files |
|
| 790 | 1030 | .iter() |
|
| … | 10 unchanged lines |
||
| 801 | 1041 | ||
| 802 | 1042 | /// A path under the root as a `/`-separated relative string. |
|
| 803 | 1043 | pub(crate) fn rel_path(&self, path: &Path) -> String { |
|
| 804 | - | path.strip_prefix(&self.root).unwrap_or(path).components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::<Vec<_>>().join("/") |
|
| 1044 | + | path.strip_prefix(&self.root) |
|
| 1045 | + | .unwrap_or(path) |
|
| 1046 | + | .components() |
|
| 1047 | + | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 1048 | + | .collect::<Vec<_>>() |
|
| 1049 | + | .join("/") |
|
| 805 | 1050 | } |
|
| 806 | 1051 | } |
|
| 807 | 1052 | ||
| … | 11 unchanged lines |
||
| 819 | 1064 | let head_set: BTreeSet<ChangeHash> = heads.into_iter().collect(); |
|
| 820 | 1065 | let mut entries: Vec<LogEntry> = Vec::new(); |
|
| 821 | 1066 | for (h, meta) in state.changes() { |
|
| 822 | - | entries.push(LogEntry { hash: *h, change: meta.clone(), is_working_copy: *h == wc_hash, is_head: head_set.contains(h) }); |
|
| 1067 | + | entries.push(LogEntry { |
|
| 1068 | + | hash: *h, |
|
| 1069 | + | change: meta.clone(), |
|
| 1070 | + | is_working_copy: *h == wc_hash, |
|
| 1071 | + | is_head: head_set.contains(h), |
|
| 1072 | + | }); |
|
| 823 | 1073 | } |
|
| 824 | - | entries.sort_by(|a, b| b.is_working_copy.cmp(&a.is_working_copy).then(b.change.hlc.cmp(&a.change.hlc)).then(b.hash.cmp(&a.hash))); |
|
| 1074 | + | entries.sort_by(|a, b| { |
|
| 1075 | + | b.is_working_copy |
|
| 1076 | + | .cmp(&a.is_working_copy) |
|
| 1077 | + | .then(b.change.hlc.cmp(&a.change.hlc)) |
|
| 1078 | + | .then(b.hash.cmp(&a.hash)) |
|
| 1079 | + | }); |
|
| 825 | 1080 | Ok(entries) |
|
| 826 | 1081 | } |
|
| 827 | 1082 | ||
| … | 4 unchanged lines |
||
| 832 | 1087 | let head_set: BTreeSet<ChangeHash> = state.heads().into_iter().collect(); |
|
| 833 | 1088 | let mut entries: Vec<LogEntry> = state |
|
| 834 | 1089 | .changes() |
|
| 835 | - | .map(|(h, meta)| LogEntry { hash: *h, change: meta.clone(), is_working_copy: false, is_head: head_set.contains(h) }) |
|
| 1090 | + | .map(|(h, meta)| LogEntry { |
|
| 1091 | + | hash: *h, |
|
| 1092 | + | change: meta.clone(), |
|
| 1093 | + | is_working_copy: false, |
|
| 1094 | + | is_head: head_set.contains(h), |
|
| 1095 | + | }) |
|
| 836 | 1096 | .collect(); |
|
| 837 | 1097 | entries.sort_by(|a, b| b.change.hlc.cmp(&a.change.hlc).then(b.hash.cmp(&a.hash))); |
|
| 838 | 1098 | Ok(entries) |
|
| … | 29 unchanged lines |
||
| 868 | 1128 | } |
|
| 869 | 1129 | let entries = self.log()?; |
|
| 870 | 1130 | let matches: Vec<&LogEntry> = if let Some(hp) = spec.strip_prefix('#') { |
|
| 871 | - | entries.iter().filter(|e| e.hash.starts_with_hex(hp)).collect() |
|
| 1131 | + | entries |
|
| 1132 | + | .iter() |
|
| 1133 | + | .filter(|e| e.hash.starts_with_hex(hp)) |
|
| 1134 | + | .collect() |
|
| 872 | 1135 | } else { |
|
| 873 | - | entries.iter().filter(|e| e.change.change_id.starts_with_hex(spec)).collect() |
|
| 1136 | + | entries |
|
| 1137 | + | .iter() |
|
| 1138 | + | .filter(|e| e.change.change_id.starts_with_hex(spec)) |
|
| 1139 | + | .collect() |
|
| 874 | 1140 | }; |
|
| 875 | 1141 | match matches.len() { |
|
| 876 | 1142 | 1 => Ok(matches[0].hash), |
|
| … | 1 unchanged lines |
||
| 878 | 1144 | _ => { |
|
| 879 | 1145 | let ids: BTreeSet<ChangeId> = matches.iter().map(|e| e.change.change_id).collect(); |
|
| 880 | 1146 | if ids.len() == 1 { |
|
| 881 | - | fail(format!("change {spec} is divergent: {} variants", matches.len())) |
|
| 1147 | + | fail(format!( |
|
| 1148 | + | "change {spec} is divergent: {} variants", |
|
| 1149 | + | matches.len() |
|
| 1150 | + | )) |
|
| 882 | 1151 | } else { |
|
| 883 | 1152 | fail(format!("{spec:?} is ambiguous")) |
|
| 884 | 1153 | } |
|
| … | 12 unchanged lines |
||
| 897 | 1166 | ||
| 898 | 1167 | fn remove_any(path: &Path) -> std::io::Result<()> { |
|
| 899 | 1168 | let m = fs::symlink_metadata(path)?; |
|
| 900 | - | if m.is_dir() { fs::remove_dir_all(path) } else { fs::remove_file(path) } |
|
| 1169 | + | if m.is_dir() { |
|
| 1170 | + | fs::remove_dir_all(path) |
|
| 1171 | + | } else { |
|
| 1172 | + | fs::remove_file(path) |
|
| 1173 | + | } |
|
| 901 | 1174 | } |
|
| 902 | 1175 | ||
| 903 | 1176 | fn set_readonly(path: &Path, readonly: bool) -> std::io::Result<()> { |
|
| … | 30 unchanged lines |
||
| 934 | 1207 | } |
|
| 935 | 1208 | ||
| 936 | 1209 | fn op_ref(k: OpKey) -> OpRef { |
|
| 937 | - | OpRef { change: k.0, index: k.1 } |
|
| 1210 | + | OpRef { |
|
| 1211 | + | change: k.0, |
|
| 1212 | + | index: k.1, |
|
| 1213 | + | } |
|
| 938 | 1214 | } |
|
| 939 | 1215 | ||
| 940 | 1216 | fn split_path(path: &str) -> (&str, &str) { |
|
| … | 5 unchanged lines |
modified crates/oxy-core/src/repo/policy.rs +172 −37
| … | 23 unchanged lines |
||
| 24 | 24 | ||
| 25 | 25 | impl PolicyUpdate { |
|
| 26 | 26 | pub fn signed(&self) -> SignedPolicy { |
|
| 27 | - | SignedPolicy { policy: self.policy.clone(), sigs: self.sigs.clone() } |
|
| 27 | + | SignedPolicy { |
|
| 28 | + | policy: self.policy.clone(), |
|
| 29 | + | sigs: self.sigs.clone(), |
|
| 30 | + | } |
|
| 28 | 31 | } |
|
| 29 | 32 | } |
|
| 30 | 33 | ||
| 31 | 34 | impl Repo { |
|
| 32 | 35 | /// The current policy and its signatures, if the repository has one. |
|
| 33 | 36 | pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> { |
|
| 34 | - | let Some(r) = self.view()?.policy else { return Ok(None) }; |
|
| 37 | + | let Some(r) = self.view()?.policy else { |
|
| 38 | + | return Ok(None); |
|
| 39 | + | }; |
|
| 35 | 40 | let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?; |
|
| 36 | - | Ok(Some((r, SignedPolicy { policy, sigs: self.store.signatures(&r)? }))) |
|
| 41 | + | Ok(Some(( |
|
| 42 | + | r, |
|
| 43 | + | SignedPolicy { |
|
| 44 | + | policy, |
|
| 45 | + | sigs: self.store.signatures(&r)?, |
|
| 46 | + | }, |
|
| 47 | + | ))) |
|
| 37 | 48 | } |
|
| 38 | 49 | ||
| 39 | 50 | /// Store a policy with its signatures and point the `policy` reference at it. |
|
| … | 7 unchanged lines |
||
| 47 | 58 | } |
|
| 48 | 59 | let dir = self.oxy.join("policies"); |
|
| 49 | 60 | fs::create_dir_all(&dir)?; |
|
| 50 | - | write_atomic(&dir.join(format!("{:020}", sp.policy.serial)), r.to_hex().as_bytes())?; |
|
| 61 | + | write_atomic( |
|
| 62 | + | &dir.join(format!("{:020}", sp.policy.serial)), |
|
| 63 | + | r.to_hex().as_bytes(), |
|
| 64 | + | )?; |
|
| 51 | 65 | let (op_ref, op) = self.head_op()?; |
|
| 52 | 66 | if op.view.policy != Some(r) { |
|
| 53 | 67 | let mut view = op.view.clone(); |
|
| … | 10 unchanged lines |
||
| 64 | 78 | let mut out: BTreeMap<u64, SignedPolicy> = BTreeMap::new(); |
|
| 65 | 79 | if let Ok(rd) = fs::read_dir(self.oxy.join("policies")) { |
|
| 66 | 80 | for e in rd.flatten() { |
|
| 67 | - | let Ok(hex) = fs::read_to_string(e.path()) else { continue }; |
|
| 68 | - | let Some(r) = ObjectRef::from_hex(hex.trim()) else { continue }; |
|
| 69 | - | let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else { continue }; |
|
| 81 | + | let Ok(hex) = fs::read_to_string(e.path()) else { |
|
| 82 | + | continue; |
|
| 83 | + | }; |
|
| 84 | + | let Some(r) = ObjectRef::from_hex(hex.trim()) else { |
|
| 85 | + | continue; |
|
| 86 | + | }; |
|
| 87 | + | let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else { |
|
| 88 | + | continue; |
|
| 89 | + | }; |
|
| 70 | 90 | let policy = Policy::decode(&bytes)?; |
|
| 71 | - | out.insert(policy.serial, SignedPolicy { policy, sigs: self.store.signatures(&r)? }); |
|
| 91 | + | out.insert( |
|
| 92 | + | policy.serial, |
|
| 93 | + | SignedPolicy { |
|
| 94 | + | policy, |
|
| 95 | + | sigs: self.store.signatures(&r)?, |
|
| 96 | + | }, |
|
| 97 | + | ); |
|
| 72 | 98 | } |
|
| 73 | 99 | } |
|
| 74 | 100 | if let Some((_, sp)) = self.current_policy()? { |
|
| … | 5 unchanged lines |
||
| 80 | 106 | /// Verify and apply a chain of policies ending in `newest`, each |
|
| 81 | 107 | /// against the one before it (spec §14.5). `chain` holds earlier |
|
| 82 | 108 | /// policies in any order; only those after the current serial are used. |
|
| 83 | - | pub(crate) fn adopt_policy_chain(&self, chain: &[SignedPolicy], newest: &SignedPolicy, command: &str) -> std::result::Result<(), String> { |
|
| 84 | - | let mut current = self.current_policy().map_err(|e| e.to_string())?.map(|(_, sp)| sp.policy); |
|
| 109 | + | pub(crate) fn adopt_policy_chain( |
|
| 110 | + | &self, |
|
| 111 | + | chain: &[SignedPolicy], |
|
| 112 | + | newest: &SignedPolicy, |
|
| 113 | + | command: &str, |
|
| 114 | + | ) -> std::result::Result<(), String> { |
|
| 115 | + | let mut current = self |
|
| 116 | + | .current_policy() |
|
| 117 | + | .map_err(|e| e.to_string())? |
|
| 118 | + | .map(|(_, sp)| sp.policy); |
|
| 85 | 119 | let mut steps: Vec<&SignedPolicy> = chain |
|
| 86 | 120 | .iter() |
|
| 87 | - | .filter(|sp| current.as_ref().is_none_or(|c| sp.policy.serial > c.serial) && sp.policy.serial < newest.policy.serial) |
|
| 121 | + | .filter(|sp| { |
|
| 122 | + | current.as_ref().is_none_or(|c| sp.policy.serial > c.serial) |
|
| 123 | + | && sp.policy.serial < newest.policy.serial |
|
| 124 | + | }) |
|
| 88 | 125 | .collect(); |
|
| 89 | 126 | steps.sort_by_key(|sp| sp.policy.serial); |
|
| 90 | 127 | steps.push(newest); |
|
| 91 | 128 | for sp in steps { |
|
| 92 | 129 | self.check_policy(sp, current.as_ref())?; |
|
| 93 | - | self.set_policy(sp, &format!("{command} (policy serial {})", sp.policy.serial)).map_err(|e| e.to_string())?; |
|
| 130 | + | self.set_policy( |
|
| 131 | + | sp, |
|
| 132 | + | &format!("{command} (policy serial {})", sp.policy.serial), |
|
| 133 | + | ) |
|
| 134 | + | .map_err(|e| e.to_string())?; |
|
| 94 | 135 | current = Some(sp.policy.clone()); |
|
| 95 | 136 | } |
|
| 96 | 137 | Ok(()) |
|
| … | 1 unchanged lines |
||
| 98 | 139 | ||
| 99 | 140 | /// Validate an incoming policy against the one we trust (`None` when |
|
| 100 | 141 | /// bootstrapping: the policy then vouches for itself). |
|
| 101 | - | pub fn check_policy(&self, sp: &SignedPolicy, trusted: Option<&Policy>) -> std::result::Result<(), String> { |
|
| 142 | + | pub fn check_policy( |
|
| 143 | + | &self, |
|
| 144 | + | sp: &SignedPolicy, |
|
| 145 | + | trusted: Option<&Policy>, |
|
| 146 | + | ) -> std::result::Result<(), String> { |
|
| 102 | 147 | if sp.policy.repo != self.id().map_err(|e| e.to_string())? { |
|
| 103 | 148 | return Err("policy is for a different repository".into()); |
|
| 104 | 149 | } |
|
| 105 | 150 | if let Some(t) = trusted { |
|
| 106 | 151 | if sp.policy.serial <= t.serial { |
|
| 107 | - | return Err(format!("policy serial {} is not greater than the current {}", sp.policy.serial, t.serial)); |
|
| 152 | + | return Err(format!( |
|
| 153 | + | "policy serial {} is not greater than the current {}", |
|
| 154 | + | sp.policy.serial, t.serial |
|
| 155 | + | )); |
|
| 108 | 156 | } |
|
| 109 | 157 | } |
|
| 110 | 158 | match trusted { |
|
| … | 15 unchanged lines |
||
| 126 | 174 | /// Sign with every listed key; the first must be a root of the current |
|
| 127 | 175 | /// policy (the others may be roots of the new policy only, so that a |
|
| 128 | 176 | /// fresh clone can bootstrap from it). |
|
| 129 | - | pub(crate) fn sign_policy_with(&self, policy: Policy, signers: &[(KeyId, &SecretKey)], command: &str) -> Result<PolicyUpdate> { |
|
| 130 | - | let trusted = self.current_policy()?.map(|(_, sp)| sp.policy).unwrap_or_else(|| policy.clone()); |
|
| 131 | - | if !signers.first().is_some_and(|(k, _)| trusted.roots.contains(k)) { |
|
| 177 | + | pub(crate) fn sign_policy_with( |
|
| 178 | + | &self, |
|
| 179 | + | policy: Policy, |
|
| 180 | + | signers: &[(KeyId, &SecretKey)], |
|
| 181 | + | command: &str, |
|
| 182 | + | ) -> Result<PolicyUpdate> { |
|
| 183 | + | let trusted = self |
|
| 184 | + | .current_policy()? |
|
| 185 | + | .map(|(_, sp)| sp.policy) |
|
| 186 | + | .unwrap_or_else(|| policy.clone()); |
|
| 187 | + | if !signers |
|
| 188 | + | .first() |
|
| 189 | + | .is_some_and(|(k, _)| trusted.roots.contains(k)) |
|
| 190 | + | { |
|
| 132 | 191 | return fail("your key is not a root key of the current policy"); |
|
| 133 | 192 | } |
|
| 134 | 193 | let target = policy.hash(); |
|
| 135 | - | let sigs = signers.iter().map(|(k, sk)| sk.sign(Domain::Policy, k, &target)).collect(); |
|
| 194 | + | let sigs = signers |
|
| 195 | + | .iter() |
|
| 196 | + | .map(|(k, sk)| sk.sign(Domain::Policy, k, &target)) |
|
| 197 | + | .collect(); |
|
| 136 | 198 | self.gather(SignedPolicy { policy, sigs }, &trusted, command) |
|
| 137 | 199 | } |
|
| 138 | 200 | ||
| … | 3 unchanged lines |
||
| 142 | 204 | if applied { |
|
| 143 | 205 | self.set_policy(&sp, command)?; |
|
| 144 | 206 | } |
|
| 145 | - | Ok(PolicyUpdate { policy: sp.policy, sigs: sp.sigs, applied }) |
|
| 207 | + | Ok(PolicyUpdate { |
|
| 208 | + | policy: sp.policy, |
|
| 209 | + | sigs: sp.sigs, |
|
| 210 | + | applied, |
|
| 211 | + | }) |
|
| 146 | 212 | } |
|
| 147 | 213 | ||
| 148 | 214 | /// Add your signature to a proposal made by another root holder; the |
|
| … | 9 unchanged lines |
||
| 158 | 224 | return fail("the proposal is for a different repository"); |
|
| 159 | 225 | } |
|
| 160 | 226 | if proposal.policy.serial <= current.policy.serial { |
|
| 161 | - | return fail(format!("the proposal's serial {} is not greater than the current {}", proposal.policy.serial, current.policy.serial)); |
|
| 227 | + | return fail(format!( |
|
| 228 | + | "the proposal's serial {} is not greater than the current {}", |
|
| 229 | + | proposal.policy.serial, current.policy.serial |
|
| 230 | + | )); |
|
| 162 | 231 | } |
|
| 163 | 232 | if !current.policy.roots.contains(&key) { |
|
| 164 | 233 | return fail("your key is not a root key of the current policy"); |
|
| 165 | 234 | } |
|
| 166 | 235 | let mut sigs: Vec<Signature> = proposal.sigs.into_iter().filter(|s| s.key != key).collect(); |
|
| 167 | 236 | sigs.push(sk.sign(Domain::Policy, &key, &proposal.policy.hash())); |
|
| 168 | - | self.gather(SignedPolicy { policy: proposal.policy, sigs }, ¤t.policy, "oxy policy sign") |
|
| 237 | + | self.gather( |
|
| 238 | + | SignedPolicy { |
|
| 239 | + | policy: proposal.policy, |
|
| 240 | + | sigs, |
|
| 241 | + | }, |
|
| 242 | + | ¤t.policy, |
|
| 243 | + | "oxy policy sign", |
|
| 244 | + | ) |
|
| 169 | 245 | } |
|
| 170 | 246 | ||
| 171 | 247 | /// Create the initial policy with the configured identity as sole root. |
|
| … | 5 unchanged lines |
||
| 177 | 253 | return fail("no secret key: run `oxy identity create` first"); |
|
| 178 | 254 | }; |
|
| 179 | 255 | let identity = self.identity(&key)?; |
|
| 180 | - | let policy = Policy::single_root(self.id()?, identity, Hlc::now().wall_ms + POLICY_LIFETIME_MS); |
|
| 256 | + | let policy = Policy::single_root( |
|
| 257 | + | self.id()?, |
|
| 258 | + | identity, |
|
| 259 | + | Hlc::now().wall_ms + POLICY_LIFETIME_MS, |
|
| 260 | + | ); |
|
| 181 | 261 | self.sign_policy(policy, "oxy policy init") |
|
| 182 | 262 | } |
|
| 183 | 263 | ||
| … | 21 unchanged lines |
||
| 205 | 285 | } |
|
| 206 | 286 | p.roots.retain(|k| k != key); |
|
| 207 | 287 | if (p.roots.len() as u32) < p.root_threshold { |
|
| 208 | - | return fail(format!("removing it would leave {} root(s) for a threshold of {}", p.roots.len(), p.root_threshold)); |
|
| 288 | + | return fail(format!( |
|
| 289 | + | "removing it would leave {} root(s) for a threshold of {}", |
|
| 290 | + | p.roots.len(), |
|
| 291 | + | p.root_threshold |
|
| 292 | + | )); |
|
| 209 | 293 | } |
|
| 210 | 294 | self.sign_policy(p, &format!("oxy policy root remove {}", key.short(12))) |
|
| 211 | 295 | } |
|
| … | 2 unchanged lines |
||
| 214 | 298 | pub fn policy_set_threshold(&self, n: u32) -> Result<PolicyUpdate> { |
|
| 215 | 299 | let mut p = self.next_policy()?; |
|
| 216 | 300 | if n == 0 || n as usize > p.roots.len() { |
|
| 217 | - | return fail(format!("the threshold must be between 1 and the number of roots ({})", p.roots.len())); |
|
| 301 | + | return fail(format!( |
|
| 302 | + | "the threshold must be between 1 and the number of roots ({})", |
|
| 303 | + | p.roots.len() |
|
| 304 | + | )); |
|
| 218 | 305 | } |
|
| 219 | 306 | p.root_threshold = n; |
|
| 220 | 307 | self.sign_policy(p, &format!("oxy policy root threshold {n}")) |
|
| … | 22 unchanged lines |
||
| 243 | 330 | rule.keys.sort(); |
|
| 244 | 331 | } |
|
| 245 | 332 | } |
|
| 246 | - | None => p.rules.push(Rule { pattern: pattern.into(), keys: vec![key], threshold: 1, require: Require::Any, allow_conflicts: true }), |
|
| 333 | + | None => p.rules.push(Rule { |
|
| 334 | + | pattern: pattern.into(), |
|
| 335 | + | keys: vec![key], |
|
| 336 | + | threshold: 1, |
|
| 337 | + | require: Require::Any, |
|
| 338 | + | allow_conflicts: true, |
|
| 339 | + | }), |
|
| 247 | 340 | } |
|
| 248 | 341 | if p.identity(&key).is_none() { |
|
| 249 | 342 | p.identities.push(identity); |
|
| … | 13 unchanged lines |
||
| 263 | 356 | } |
|
| 264 | 357 | p.roots.retain(|k| k != key); |
|
| 265 | 358 | if (p.roots.len() as u32) < p.root_threshold { |
|
| 266 | - | return fail(format!("revoking it would leave {} root(s) for a threshold of {}", p.roots.len(), p.root_threshold)); |
|
| 359 | + | return fail(format!( |
|
| 360 | + | "revoking it would leave {} root(s) for a threshold of {}", |
|
| 361 | + | p.roots.len(), |
|
| 362 | + | p.root_threshold |
|
| 363 | + | )); |
|
| 267 | 364 | } |
|
| 268 | 365 | } |
|
| 269 | 366 | for r in &mut p.rules { |
|
| … | 12 unchanged lines |
||
| 282 | 379 | /// rule lists, using the authenticator kind the rule requires. A rule key |
|
| 283 | 380 | /// covers its rotation predecessors, and a delegate's signature counts |
|
| 284 | 381 | /// for the author it acts for. |
|
| 285 | - | pub fn change_allowed(&self, hash: &ChangeHash, policy: &Policy, reference: &str) -> std::result::Result<(), String> { |
|
| 286 | - | let rule = policy.rule_for(reference).ok_or_else(|| format!("no policy rule matches {reference:?}"))?; |
|
| 382 | + | pub fn change_allowed( |
|
| 383 | + | &self, |
|
| 384 | + | hash: &ChangeHash, |
|
| 385 | + | policy: &Policy, |
|
| 386 | + | reference: &str, |
|
| 387 | + | ) -> std::result::Result<(), String> { |
|
| 388 | + | let rule = policy |
|
| 389 | + | .rule_for(reference) |
|
| 390 | + | .ok_or_else(|| format!("no policy rule matches {reference:?}"))?; |
|
| 287 | 391 | match self.authenticate_change(hash).map_err(|e| e.to_string())? { |
|
| 288 | 392 | SignatureStatus::Valid { key, sealed, .. } => { |
|
| 289 | 393 | if policy.is_revoked(&key) { |
|
| 290 | - | return Err(format!("change {} is signed by revoked key {}", hash.short(12), key.short(12))); |
|
| 394 | + | return Err(format!( |
|
| 395 | + | "change {} is signed by revoked key {}", |
|
| 396 | + | hash.short(12), |
|
| 397 | + | key.short(12) |
|
| 398 | + | )); |
|
| 291 | 399 | } |
|
| 292 | - | let author = self.store.get_change(hash).map(|c| c.author).map_err(|e| e.to_string())?; |
|
| 400 | + | let author = self |
|
| 401 | + | .store |
|
| 402 | + | .get_change(hash) |
|
| 403 | + | .map(|c| c.author) |
|
| 404 | + | .map_err(|e| e.to_string())?; |
|
| 293 | 405 | let principal = if author.is_zero() { key } else { author }; |
|
| 294 | - | let covered = rule.keys.iter().any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal)); |
|
| 406 | + | let covered = rule |
|
| 407 | + | .keys |
|
| 408 | + | .iter() |
|
| 409 | + | .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal)); |
|
| 295 | 410 | if !covered { |
|
| 296 | - | return Err(format!("change {} is signed by {}, which may not update {reference:?}", hash.short(12), key.short(12))); |
|
| 411 | + | return Err(format!( |
|
| 412 | + | "change {} is signed by {}, which may not update {reference:?}", |
|
| 413 | + | hash.short(12), |
|
| 414 | + | key.short(12) |
|
| 415 | + | )); |
|
| 297 | 416 | } |
|
| 298 | 417 | match (rule.require, sealed) { |
|
| 299 | - | (Require::Signed, true) => Err(format!("{reference:?} requires individually signed changes; {} is only sealed", hash.short(12))), |
|
| 300 | - | (Require::Sealed, false) => Err(format!("{reference:?} requires sealed changes; {} is individually signed", hash.short(12))), |
|
| 418 | + | (Require::Signed, true) => Err(format!( |
|
| 419 | + | "{reference:?} requires individually signed changes; {} is only sealed", |
|
| 420 | + | hash.short(12) |
|
| 421 | + | )), |
|
| 422 | + | (Require::Sealed, false) => Err(format!( |
|
| 423 | + | "{reference:?} requires sealed changes; {} is individually signed", |
|
| 424 | + | hash.short(12) |
|
| 425 | + | )), |
|
| 301 | 426 | _ => Ok(()), |
|
| 302 | 427 | } |
|
| 303 | 428 | } |
|
| 304 | - | SignatureStatus::Unsigned => Err(format!("change {} is not signed or sealed", hash.short(12))), |
|
| 429 | + | SignatureStatus::Unsigned => { |
|
| 430 | + | Err(format!("change {} is not signed or sealed", hash.short(12))) |
|
| 431 | + | } |
|
| 305 | 432 | SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))), |
|
| 306 | 433 | } |
|
| 307 | 434 | } |
|
| 308 | 435 | ||
| 309 | 436 | /// Check every change that `new` adds over `old` for `reference`. |
|
| 310 | - | pub(crate) fn state_allowed(&self, old_heads: &[ChangeHash], new_heads: &[ChangeHash], policy: &Policy, reference: &str) -> std::result::Result<(), String> { |
|
| 311 | - | let added = self.negotiate(old_heads, new_heads).map_err(|e| e.to_string())?; |
|
| 437 | + | pub(crate) fn state_allowed( |
|
| 438 | + | &self, |
|
| 439 | + | old_heads: &[ChangeHash], |
|
| 440 | + | new_heads: &[ChangeHash], |
|
| 441 | + | policy: &Policy, |
|
| 442 | + | reference: &str, |
|
| 443 | + | ) -> std::result::Result<(), String> { |
|
| 444 | + | let added = self |
|
| 445 | + | .negotiate(old_heads, new_heads) |
|
| 446 | + | .map_err(|e| e.to_string())?; |
|
| 312 | 447 | for h in &added { |
|
| 313 | 448 | self.change_allowed(h, policy, reference)?; |
|
| 314 | 449 | } |
|
| … | 11 unchanged lines |
modified crates/oxy-core/src/repo/prompt.rs +114 −23
| … | 49 unchanged lines |
||
| 50 | 50 | /// `format` wrapped in the state colour. `guard` is `("\\[", "\\]")` for |
|
| 51 | 51 | /// bash prompts, `("%{", "%}")` for zsh, or empty for raw output. |
|
| 52 | 52 | pub fn format_colored(&self, fmt: &str, guard: (&str, &str)) -> String { |
|
| 53 | - | format!("{}\x1b[{}m{}{}{}\x1b[0m{}", guard.0, self.state_color(), guard.1, self.format(fmt), guard.0, guard.1) |
|
| 53 | + | format!( |
|
| 54 | + | "{}\x1b[{}m{}{}{}\x1b[0m{}", |
|
| 55 | + | guard.0, |
|
| 56 | + | self.state_color(), |
|
| 57 | + | guard.1, |
|
| 58 | + | self.format(fmt), |
|
| 59 | + | guard.0, |
|
| 60 | + | guard.1 |
|
| 61 | + | ) |
|
| 54 | 62 | } |
|
| 55 | 63 | ||
| 56 | 64 | /// Expand `%b` bookmark, `%c` change id, `%h` heads, `%d` dirty marker |
|
| … | 78 unchanged lines |
||
| 135 | 143 | } |
|
| 136 | 144 | ||
| 137 | 145 | pub(crate) fn mtime_ns(m: &fs::Metadata) -> u128 { |
|
| 138 | - | m.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()).map(|d| d.as_nanos()).unwrap_or(0) |
|
| 146 | + | m.modified() |
|
| 147 | + | .ok() |
|
| 148 | + | .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) |
|
| 149 | + | .map(|d| d.as_nanos()) |
|
| 150 | + | .unwrap_or(0) |
|
| 139 | 151 | } |
|
| 140 | 152 | ||
| 141 | 153 | pub const DEFAULT_PROMPT_FORMAT: &str = "%b %c%d%x%A%D"; |
|
| … | 4 unchanged lines |
||
| 146 | 158 | ||
| 147 | 159 | /// One manifest line per file: size, mtime in nanoseconds, hash, blob ref |
|
| 148 | 160 | /// or `-`, path. |
|
| 149 | - | fn manifest_line(path: &str, size: u64, mtime_ns: u128, hash: &str, blob: Option<ObjectRef>) -> String { |
|
| 150 | - | format!("{size}\t{mtime_ns}\t{hash}\t{}\t{path}\n", blob.map(|b| b.to_hex()).unwrap_or_else(|| "-".into())) |
|
| 161 | + | fn manifest_line( |
|
| 162 | + | path: &str, |
|
| 163 | + | size: u64, |
|
| 164 | + | mtime_ns: u128, |
|
| 165 | + | hash: &str, |
|
| 166 | + | blob: Option<ObjectRef>, |
|
| 167 | + | ) -> String { |
|
| 168 | + | format!( |
|
| 169 | + | "{size}\t{mtime_ns}\t{hash}\t{}\t{path}\n", |
|
| 170 | + | blob.map(|b| b.to_hex()).unwrap_or_else(|| "-".into()) |
|
| 171 | + | ) |
|
| 151 | 172 | } |
|
| 152 | 173 | ||
| 153 | 174 | impl Repo { |
|
| 154 | 175 | fn manifest_path(&self) -> PathBuf { |
|
| 155 | - | self.oxy.join("workspaces").join(self.workspace.to_hex()).join("manifest") |
|
| 176 | + | self.oxy |
|
| 177 | + | .join("workspaces") |
|
| 178 | + | .join(self.workspace.to_hex()) |
|
| 179 | + | .join("manifest") |
|
| 156 | 180 | } |
|
| 157 | 181 | ||
| 158 | 182 | /// Record the working copy as it is now (after a snapshot, commit, or |
|
| … | 6 unchanged lines |
||
| 165 | 189 | ||
| 166 | 190 | /// Write the manifest from entries already walked; `blobs` names the |
|
| 167 | 191 | /// stored blob of each opaque path so the next walk can skip reading it. |
|
| 168 | - | pub(crate) fn write_manifest_entries(&self, conflicted: bool, uncommitted: bool, entries: &BTreeMap<String, FsEntry>, blobs: &HashMap<String, ObjectRef>) -> Result<()> { |
|
| 169 | - | let written = std::time::SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0); |
|
| 170 | - | let mut text = format!("# conflicted={}\n# uncommitted={}\n# written={written}\n", conflicted as u8, uncommitted as u8); |
|
| 192 | + | pub(crate) fn write_manifest_entries( |
|
| 193 | + | &self, |
|
| 194 | + | conflicted: bool, |
|
| 195 | + | uncommitted: bool, |
|
| 196 | + | entries: &BTreeMap<String, FsEntry>, |
|
| 197 | + | blobs: &HashMap<String, ObjectRef>, |
|
| 198 | + | ) -> Result<()> { |
|
| 199 | + | let written = std::time::SystemTime::now() |
|
| 200 | + | .duration_since(UNIX_EPOCH) |
|
| 201 | + | .map(|d| d.as_nanos()) |
|
| 202 | + | .unwrap_or(0); |
|
| 203 | + | let mut text = format!( |
|
| 204 | + | "# conflicted={}\n# uncommitted={}\n# written={written}\n", |
|
| 205 | + | conflicted as u8, uncommitted as u8 |
|
| 206 | + | ); |
|
| 171 | 207 | for (path, e) in entries { |
|
| 172 | 208 | let full = self.root.join(path); |
|
| 173 | 209 | let (size, mtime_ns) = match fs::symlink_metadata(&full) { |
|
| … | 67 unchanged lines |
||
| 241 | 277 | if f.len() == 5 { |
|
| 242 | 278 | out.entries.insert( |
|
| 243 | 279 | f[4].to_string(), |
|
| 244 | - | ManifestEntry { size: f[0].parse().unwrap_or(0), mtime_ns: f[1].parse().unwrap_or(0), hash: f[2].to_string(), blob: ObjectRef::from_hex(f[3]) }, |
|
| 280 | + | ManifestEntry { |
|
| 281 | + | size: f[0].parse().unwrap_or(0), |
|
| 282 | + | mtime_ns: f[1].parse().unwrap_or(0), |
|
| 283 | + | hash: f[2].to_string(), |
|
| 284 | + | blob: ObjectRef::from_hex(f[3]), |
|
| 285 | + | }, |
|
| 245 | 286 | ); |
|
| 246 | 287 | } |
|
| 247 | 288 | } |
|
| … | 3 unchanged lines |
||
| 251 | 292 | /// Whether the working copy differs from the last manifest. Without a |
|
| 252 | 293 | /// manifest the answer is `None`. |
|
| 253 | 294 | pub fn quick_dirty(&self) -> Result<Option<bool>> { |
|
| 254 | - | let Some((_, uncommitted, manifest)) = self.read_manifest()? else { return Ok(None) }; |
|
| 295 | + | let Some((_, uncommitted, manifest)) = self.read_manifest()? else { |
|
| 296 | + | return Ok(None); |
|
| 297 | + | }; |
|
| 255 | 298 | if uncommitted { |
|
| 256 | 299 | return Ok(Some(true)); |
|
| 257 | 300 | } |
|
| … | 2 unchanged lines |
||
| 260 | 303 | let wb = self.walker(&nested); |
|
| 261 | 304 | for entry in wb.build() { |
|
| 262 | 305 | let Ok(entry) = entry else { continue }; |
|
| 263 | - | let Some(ft) = entry.file_type() else { continue }; |
|
| 306 | + | let Some(ft) = entry.file_type() else { |
|
| 307 | + | continue; |
|
| 308 | + | }; |
|
| 264 | 309 | if ft.is_dir() { |
|
| 265 | 310 | continue; |
|
| 266 | 311 | } |
|
| 267 | - | let rel: String = entry.path().strip_prefix(&self.root).unwrap().components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::<Vec<_>>().join("/"); |
|
| 312 | + | let rel: String = entry |
|
| 313 | + | .path() |
|
| 314 | + | .strip_prefix(&self.root) |
|
| 315 | + | .unwrap() |
|
| 316 | + | .components() |
|
| 317 | + | .map(|c| c.as_os_str().to_string_lossy().to_string()) |
|
| 318 | + | .collect::<Vec<_>>() |
|
| 319 | + | .join("/"); |
|
| 268 | 320 | if !self.in_scope(&rel) { |
|
| 269 | 321 | continue; |
|
| 270 | 322 | } |
|
| 271 | - | let Some(me) = manifest.entries.get(&rel) else { return Ok(Some(true)) }; |
|
| 323 | + | let Some(me) = manifest.entries.get(&rel) else { |
|
| 324 | + | return Ok(Some(true)); |
|
| 325 | + | }; |
|
| 272 | 326 | seen += 1; |
|
| 273 | 327 | let m = fs::symlink_metadata(entry.path())?; |
|
| 274 | 328 | let now_mtime = mtime_ns(&m); |
|
| 275 | 329 | if ft.is_symlink() { |
|
| 276 | 330 | let target = fs::read_link(entry.path())?; |
|
| 277 | - | if blake3::hash(target.to_string_lossy().as_bytes()).to_hex().as_str() != me.hash { |
|
| 331 | + | if blake3::hash(target.to_string_lossy().as_bytes()) |
|
| 332 | + | .to_hex() |
|
| 333 | + | .as_str() |
|
| 334 | + | != me.hash |
|
| 335 | + | { |
|
| 278 | 336 | return Ok(Some(true)); |
|
| 279 | 337 | } |
|
| 280 | 338 | continue; |
|
| … | 1 unchanged lines |
||
| 282 | 340 | if m.len() != me.size { |
|
| 283 | 341 | return Ok(Some(true)); |
|
| 284 | 342 | } |
|
| 285 | - | if now_mtime != me.mtime_ns && blake3::hash(&fs::read(entry.path())?).to_hex().as_str() != me.hash { |
|
| 343 | + | if now_mtime != me.mtime_ns |
|
| 344 | + | && blake3::hash(&fs::read(entry.path())?).to_hex().as_str() != me.hash |
|
| 345 | + | { |
|
| 286 | 346 | return Ok(Some(true)); |
|
| 287 | 347 | } |
|
| 288 | 348 | } |
|
| … | 4 unchanged lines |
||
| 293 | 353 | // hide. They count as present when still on disk and unchanged. |
|
| 294 | 354 | for (rel, me) in &manifest.entries { |
|
| 295 | 355 | let path = self.root.join(rel); |
|
| 296 | - | let Ok(m) = fs::symlink_metadata(&path) else { return Ok(Some(true)) }; |
|
| 356 | + | let Ok(m) = fs::symlink_metadata(&path) else { |
|
| 357 | + | return Ok(Some(true)); |
|
| 358 | + | }; |
|
| 297 | 359 | if m.file_type().is_symlink() { |
|
| 298 | 360 | continue; |
|
| 299 | 361 | } |
|
| 300 | - | if m.len() != me.size || (mtime_ns(&m) != me.mtime_ns && blake3::hash(&fs::read(&path)?).to_hex().as_str() != me.hash) { |
|
| 362 | + | if m.len() != me.size |
|
| 363 | + | || (mtime_ns(&m) != me.mtime_ns |
|
| 364 | + | && blake3::hash(&fs::read(&path)?).to_hex().as_str() != me.hash) |
|
| 365 | + | { |
|
| 301 | 366 | return Ok(Some(true)); |
|
| 302 | 367 | } |
|
| 303 | 368 | } |
|
| … | 24 unchanged lines |
||
| 328 | 393 | } |
|
| 329 | 394 | None => (None, None), |
|
| 330 | 395 | }; |
|
| 331 | - | Ok(PromptInfo { bookmark, change_id, heads: heads.len(), dirty, conflicted, ahead, behind }) |
|
| 396 | + | Ok(PromptInfo { |
|
| 397 | + | bookmark, |
|
| 398 | + | change_id, |
|
| 399 | + | heads: heads.len(), |
|
| 400 | + | dirty, |
|
| 401 | + | conflicted, |
|
| 402 | + | ahead, |
|
| 403 | + | behind, |
|
| 404 | + | }) |
|
| 332 | 405 | } |
|
| 333 | 406 | } |
|
| 334 | 407 | ||
| … | 3 unchanged lines |
||
| 338 | 411 | ||
| 339 | 412 | #[test] |
|
| 340 | 413 | fn placeholders_and_glyphs() { |
|
| 341 | - | let clean = PromptInfo { bookmark: "main".into(), change_id: "658990aa".into(), heads: 1, dirty: false, conflicted: false, ahead: Some(0), behind: None }; |
|
| 414 | + | let clean = PromptInfo { |
|
| 415 | + | bookmark: "main".into(), |
|
| 416 | + | change_id: "658990aa".into(), |
|
| 417 | + | heads: 1, |
|
| 418 | + | dirty: false, |
|
| 419 | + | conflicted: false, |
|
| 420 | + | ahead: Some(0), |
|
| 421 | + | behind: None, |
|
| 422 | + | }; |
|
| 342 | 423 | assert_eq!(clean.format(DEFAULT_PROMPT_FORMAT), "main 658990aa"); |
|
| 343 | 424 | assert_eq!(clean.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✔"); |
|
| 344 | - | let busy = PromptInfo { dirty: true, ahead: Some(2), behind: Some(3), ..clean.clone() }; |
|
| 425 | + | let busy = PromptInfo { |
|
| 426 | + | dirty: true, |
|
| 427 | + | ahead: Some(2), |
|
| 428 | + | behind: Some(3), |
|
| 429 | + | ..clean.clone() |
|
| 430 | + | }; |
|
| 345 | 431 | assert_eq!(busy.format(DEFAULT_PROMPT_FORMAT), "main 658990aa*+2-3"); |
|
| 346 | 432 | assert_eq!(busy.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✎↑2↓3"); |
|
| 347 | - | let bad = PromptInfo { conflicted: true, ..busy.clone() }; |
|
| 433 | + | let bad = PromptInfo { |
|
| 434 | + | conflicted: true, |
|
| 435 | + | ..busy.clone() |
|
| 436 | + | }; |
|
| 348 | 437 | assert_eq!(bad.format("%s%x%d %h %a/%B %%"), "‼!* 1 2/3 %"); |
|
| 349 | 438 | assert_eq!(bad.state_color(), 31); |
|
| 350 | - | assert_eq!(busy.format_colored("%b", ("\\[", "\\]")), "\\[\x1b[33m\\]main\\[\x1b[0m\\]"); |
|
| 439 | + | assert_eq!( |
|
| 440 | + | busy.format_colored("%b", ("\\[", "\\]")), |
|
| 441 | + | "\\[\x1b[33m\\]main\\[\x1b[0m\\]" |
|
| 442 | + | ); |
|
| 351 | 443 | assert_eq!(clean.format_colored("%b", ("", "")), "\x1b[36mmain\x1b[0m"); |
|
| 352 | 444 | assert_eq!(clean.format("%q"), "%q"); |
|
| 353 | 445 | } |
|
| 354 | 446 | } |
|
| 355 | - |
modified crates/oxy-core/src/repo/snapshot.rs +706 −133
| … | 19 unchanged lines |
||
| 20 | 20 | /// Lines that mark a conflict region in a materialized file (spec §9.5). |
|
| 21 | 21 | pub fn has_conflict_markers(content: &[u8]) -> bool { |
|
| 22 | 22 | split_lines(content).iter().any(|l| { |
|
| 23 | - | l.starts_with(b"<<<<<<< oxy conflict") || l.starts_with(b"======= oxy conflict") || l.starts_with(b">>>>>>> oxy conflict") |
|
| 23 | + | l.starts_with(b"<<<<<<< oxy conflict") |
|
| 24 | + | || l.starts_with(b"======= oxy conflict") |
|
| 25 | + | || l.starts_with(b">>>>>>> oxy conflict") |
|
| 24 | 26 | }) |
|
| 25 | 27 | } |
|
| 26 | 28 | ||
| … | 38 unchanged lines |
||
| 65 | 67 | /// Diff the files on disk against `base` and build the operations of a |
|
| 66 | 68 | /// change from `base` to the working directory. `reuse` maps paths to the |
|
| 67 | 69 | /// NodeIds an earlier version of the same change assigned to new paths. |
|
| 68 | - | pub(crate) fn build_against(&self, base: &State, base_snapshot: &Snapshot, reuse: HashMap<String, (NodeId, NodeKind)>) -> Result<BuiltChange> { |
|
| 70 | + | pub(crate) fn build_against( |
|
| 71 | + | &self, |
|
| 72 | + | base: &State, |
|
| 73 | + | base_snapshot: &Snapshot, |
|
| 74 | + | reuse: HashMap<String, (NodeId, NodeKind)>, |
|
| 75 | + | ) -> Result<BuiltChange> { |
|
| 69 | 76 | let fs_entries = self.walk_working_copy_with(None, &Self::tracked_kinds(base_snapshot))?; |
|
| 70 | 77 | self.build_from_entries(base, base_snapshot, reuse, &fs_entries) |
|
| 71 | 78 | } |
|
| 72 | 79 | ||
| 73 | 80 | /// Like `build_against`, for files given in memory (imports). |
|
| 74 | - | pub(crate) fn build_from_entries(&self, base: &State, base_snapshot: &Snapshot, reuse: HashMap<String, (NodeId, NodeKind)>, fs_entries: &BTreeMap<String, FsEntry>) -> Result<BuiltChange> { |
|
| 81 | + | pub(crate) fn build_from_entries( |
|
| 82 | + | &self, |
|
| 83 | + | base: &State, |
|
| 84 | + | base_snapshot: &Snapshot, |
|
| 85 | + | reuse: HashMap<String, (NodeId, NodeKind)>, |
|
| 86 | + | fs_entries: &BTreeMap<String, FsEntry>, |
|
| 87 | + | ) -> Result<BuiltChange> { |
|
| 75 | 88 | // Attributes come from the files being committed, so a new |
|
| 76 | 89 | // `.oxyattributes` applies to the same change that adds it. |
|
| 77 | 90 | let attrs = crate::attributes::Attributes::from_files( |
|
| 78 | - | fs_entries.iter().filter(|(p, _)| p.as_str() == ".oxyattributes" || p.ends_with("/.oxyattributes")).map(|(p, e)| (p.as_str(), e.content.as_slice())), |
|
| 91 | + | fs_entries |
|
| 92 | + | .iter() |
|
| 93 | + | .filter(|(p, _)| p.as_str() == ".oxyattributes" || p.ends_with("/.oxyattributes")) |
|
| 94 | + | .map(|(p, e)| (p.as_str(), e.content.as_slice())), |
|
| 79 | 95 | ); |
|
| 80 | 96 | let mut b = ChangeBuilder::new(base, base_snapshot, reuse).with_attributes(attrs); |
|
| 81 | 97 | // Rename detection (spec §7.1, §23): a removed file and an added file |
|
| 82 | 98 | // of the same kind whose contents are at least half alike become one |
|
| 83 | 99 | // move of the same node, so identity survives across the rename. |
|
| 84 | 100 | let renames = detect_renames(base_snapshot, fs_entries, |p| self.in_scope(p)); |
|
| 85 | - | let renamed_from: HashMap<&str, &str> = renames.iter().map(|(old, new)| (new.as_str(), old.as_str())).collect(); |
|
| 86 | - | let renamed_to: HashMap<&str, &str> = renames.iter().map(|(old, new)| (old.as_str(), new.as_str())).collect(); |
|
| 101 | + | let renamed_from: HashMap<&str, &str> = renames |
|
| 102 | + | .iter() |
|
| 103 | + | .map(|(old, new)| (new.as_str(), old.as_str())) |
|
| 104 | + | .collect(); |
|
| 105 | + | let renamed_to: HashMap<&str, &str> = renames |
|
| 106 | + | .iter() |
|
| 107 | + | .map(|(old, new)| (old.as_str(), new.as_str())) |
|
| 108 | + | .collect(); |
|
| 87 | 109 | for (path, fs_entry) in fs_entries { |
|
| 88 | 110 | match base_snapshot.files.get(path) { |
|
| 89 | 111 | None => match renamed_from.get(path.as_str()) { |
|
| 90 | - | Some(old) => b.move_path(self, old, path, fs_entry, &base_snapshot.files[*old])?, |
|
| 112 | + | Some(old) => { |
|
| 113 | + | b.move_path(self, old, path, fs_entry, &base_snapshot.files[*old])? |
|
| 114 | + | } |
|
| 91 | 115 | None => b.add_path(self, path, fs_entry)?, |
|
| 92 | 116 | }, |
|
| 93 | 117 | Some(expected) => b.update_path(self, path, fs_entry, expected)?, |
|
| 94 | 118 | } |
|
| 95 | 119 | } |
|
| 96 | 120 | for (path, expected) in &base_snapshot.files { |
|
| 97 | - | if fs_entries.contains_key(path) || !self.in_scope(path) || renamed_to.contains_key(path.as_str()) { |
|
| 121 | + | if fs_entries.contains_key(path) |
|
| 122 | + | || !self.in_scope(path) |
|
| 123 | + | || renamed_to.contains_key(path.as_str()) |
|
| 124 | + | { |
|
| 98 | 125 | continue; |
|
| 99 | 126 | } |
|
| 100 | 127 | // A directory stays while anything under it exists or is out of scope. |
|
| 101 | 128 | let still_needed = expected.kind == NodeKind::Dir |
|
| 102 | - | && (fs_entries.keys().any(|p| p.starts_with(&format!("{path}/"))) |
|
| 103 | - | || base_snapshot.files.keys().any(|p| p.starts_with(&format!("{path}/")) && !self.in_scope(p))); |
|
| 129 | + | && (fs_entries |
|
| 130 | + | .keys() |
|
| 131 | + | .any(|p| p.starts_with(&format!("{path}/"))) |
|
| 132 | + | || base_snapshot |
|
| 133 | + | .files |
|
| 134 | + | .keys() |
|
| 135 | + | .any(|p| p.starts_with(&format!("{path}/")) && !self.in_scope(p))); |
|
| 104 | 136 | if !still_needed { |
|
| 105 | 137 | b.remove_path(path, expected); |
|
| 106 | 138 | } |
|
| 107 | 139 | } |
|
| 108 | 140 | let mut statuses = b.statuses; |
|
| 109 | 141 | statuses.sort_by(|a, b| a.path.cmp(&b.path)); |
|
| 110 | - | Ok(BuiltChange { tree_ops: b.tree_ops, edits: b.edits, contributors: b.contributors.into_iter().collect(), resolves: b.resolves, statuses, blobs: b.blobs }) |
|
| 142 | + | Ok(BuiltChange { |
|
| 143 | + | tree_ops: b.tree_ops, |
|
| 144 | + | edits: b.edits, |
|
| 145 | + | contributors: b.contributors.into_iter().collect(), |
|
| 146 | + | resolves: b.resolves, |
|
| 147 | + | statuses, |
|
| 148 | + | blobs: b.blobs, |
|
| 149 | + | }) |
|
| 111 | 150 | } |
|
| 112 | 151 | ||
| 113 | 152 | /// Store a change and, when `core.sign = change` and we are its author, |
|
| … | 31 unchanged lines |
||
| 145 | 184 | let reuse = added_node_paths(&old_wc, &base_snapshot); |
|
| 146 | 185 | // The last manifest lets unchanged opaque files go unread. |
|
| 147 | 186 | let manifest = self.read_manifest()?.map(|(_, _, m)| m); |
|
| 148 | - | let fs_entries = self.walk_working_copy_with(manifest.as_ref(), &Self::tracked_kinds(&base_snapshot))?; |
|
| 187 | + | let fs_entries = |
|
| 188 | + | self.walk_working_copy_with(manifest.as_ref(), &Self::tracked_kinds(&base_snapshot))?; |
|
| 149 | 189 | let built = self.build_from_entries(&base, &base_snapshot, reuse, &fs_entries)?; |
|
| 150 | 190 | ||
| 151 | 191 | let mut wc = Change { |
|
| … | 25 unchanged lines |
||
| 177 | 217 | self.record_op(&[op_ref], &format!("{command}{SNAPSHOT_SUFFIX}"), view)?; |
|
| 178 | 218 | } |
|
| 179 | 219 | // The prompt compares against this without recording anything. |
|
| 180 | - | self.write_manifest_entries(base_snapshot.is_conflicted(), !wc.tree_ops.is_empty() || !wc.edits.is_empty(), &fs_entries, &built.blobs)?; |
|
| 181 | - | Ok(SnapshotResult { wc_hash: new_hash, wc, base, base_snapshot, statuses, changed, resolves, contributors }) |
|
| 220 | + | self.write_manifest_entries( |
|
| 221 | + | base_snapshot.is_conflicted(), |
|
| 222 | + | !wc.tree_ops.is_empty() || !wc.edits.is_empty(), |
|
| 223 | + | &fs_entries, |
|
| 224 | + | &built.blobs, |
|
| 225 | + | )?; |
|
| 226 | + | Ok(SnapshotResult { |
|
| 227 | + | wc_hash: new_hash, |
|
| 228 | + | wc, |
|
| 229 | + | base, |
|
| 230 | + | base_snapshot, |
|
| 231 | + | statuses, |
|
| 232 | + | changed, |
|
| 233 | + | resolves, |
|
| 234 | + | contributors, |
|
| 235 | + | }) |
|
| 182 | 236 | } |
|
| 183 | 237 | ||
| 184 | 238 | /// `oxy status --json` (spec §9.5): the changed paths and every conflict |
|
| … | 4 unchanged lines |
||
| 189 | 243 | let base_snapshot = &snap.base_snapshot; |
|
| 190 | 244 | let by_node = base_snapshot.tree.by_node(); |
|
| 191 | 245 | let path_of = |n: &NodeId| by_node.get(n).map(|e| e.path.clone()); |
|
| 192 | - | let id_of = |h: &ChangeHash| snap.base.get(h).map(|m| m.change_id.to_hex()).unwrap_or_else(|| h.to_hex()); |
|
| 193 | - | let paths = json_array(snap.statuses.iter().map(|s| format!("{{\"path\":{},\"status\":{}}}", json_str(&s.path), json_str(&format!("{:?}", s.status))))); |
|
| 246 | + | let id_of = |h: &ChangeHash| { |
|
| 247 | + | snap.base |
|
| 248 | + | .get(h) |
|
| 249 | + | .map(|m| m.change_id.to_hex()) |
|
| 250 | + | .unwrap_or_else(|| h.to_hex()) |
|
| 251 | + | }; |
|
| 252 | + | let paths = json_array(snap.statuses.iter().map(|s| { |
|
| 253 | + | format!( |
|
| 254 | + | "{{\"path\":{},\"status\":{}}}", |
|
| 255 | + | json_str(&s.path), |
|
| 256 | + | json_str(&format!("{:?}", s.status)) |
|
| 257 | + | ) |
|
| 258 | + | })); |
|
| 194 | 259 | let mut conflicts: Vec<String> = Vec::new(); |
|
| 195 | 260 | for (p, f) in &base_snapshot.files { |
|
| 196 | 261 | if !f.is_conflicted() { |
|
| 197 | 262 | continue; |
|
| 198 | 263 | } |
|
| 199 | 264 | let (kind, changes) = match &f.content { |
|
| 200 | - | FileContent::Text { .. } => ("text", snap.base.conflict_regions(&f.node).contributors), |
|
| 201 | - | FileContent::Opaque { blobs } => ("replace", snap.base.blob_contributors(&f.node, blobs)), |
|
| 265 | + | FileContent::Text { .. } => { |
|
| 266 | + | ("text", snap.base.conflict_regions(&f.node).contributors) |
|
| 267 | + | } |
|
| 268 | + | FileContent::Opaque { blobs } => { |
|
| 269 | + | ("replace", snap.base.blob_contributors(&f.node, blobs)) |
|
| 270 | + | } |
|
| 202 | 271 | FileContent::Dir => continue, |
|
| 203 | 272 | }; |
|
| 204 | - | let resolved = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) { |
|
| 273 | + | let resolved = match snap |
|
| 274 | + | .statuses |
|
| 275 | + | .iter() |
|
| 276 | + | .find(|s| s.path == *p) |
|
| 277 | + | .map(|s| s.status) |
|
| 278 | + | { |
|
| 205 | 279 | Some(ChangeStatus::Resolved) => "true", |
|
| 206 | 280 | Some(ChangeStatus::ConflictEdited) => "\"markers\"", |
|
| 207 | 281 | _ => "false", |
|
| … | 9 unchanged lines |
||
| 217 | 291 | for c in &base_snapshot.tree.conflicts { |
|
| 218 | 292 | let (kind, nodes): (&str, Vec<NodeId>) = match c { |
|
| 219 | 293 | crate::tree::TreeConflict::Name { nodes, .. } => ("name", nodes.clone()), |
|
| 220 | - | crate::tree::TreeConflict::MultiPlacement { node, .. } => ("multi-placement", vec![*node]), |
|
| 294 | + | crate::tree::TreeConflict::MultiPlacement { node, .. } => { |
|
| 295 | + | ("multi-placement", vec![*node]) |
|
| 296 | + | } |
|
| 221 | 297 | crate::tree::TreeConflict::Orphan { node, .. } => ("orphan", vec![*node]), |
|
| 222 | 298 | crate::tree::TreeConflict::Cycle { node } => ("cycle", vec![*node]), |
|
| 223 | 299 | crate::tree::TreeConflict::Mode { node, .. } => ("mode", vec![*node]), |
|
| 224 | 300 | }; |
|
| 225 | 301 | let detail = match c { |
|
| 226 | 302 | crate::tree::TreeConflict::Name { name, parent, .. } => { |
|
| 227 | - | let dir = if *parent == NodeId::ROOT { String::new() } else { path_of(parent).unwrap_or_default() }; |
|
| 228 | - | format!(",\"path\":{}", json_str(&if dir.is_empty() { name.clone() } else { format!("{dir}/{name}") })) |
|
| 303 | + | let dir = if *parent == NodeId::ROOT { |
|
| 304 | + | String::new() |
|
| 305 | + | } else { |
|
| 306 | + | path_of(parent).unwrap_or_default() |
|
| 307 | + | }; |
|
| 308 | + | format!( |
|
| 309 | + | ",\"path\":{}", |
|
| 310 | + | json_str(&if dir.is_empty() { |
|
| 311 | + | name.clone() |
|
| 312 | + | } else { |
|
| 313 | + | format!("{dir}/{name}") |
|
| 314 | + | }) |
|
| 315 | + | ) |
|
| 229 | 316 | } |
|
| 230 | - | _ => nodes.first().and_then(path_of).map(|p| format!(",\"path\":{}", json_str(&p))).unwrap_or_default(), |
|
| 317 | + | _ => nodes |
|
| 318 | + | .first() |
|
| 319 | + | .and_then(path_of) |
|
| 320 | + | .map(|p| format!(",\"path\":{}", json_str(&p))) |
|
| 321 | + | .unwrap_or_default(), |
|
| 231 | 322 | }; |
|
| 232 | 323 | conflicts.push(format!( |
|
| 233 | 324 | "{{\"kind\":{},\"nodes\":{}{detail}}}", |
|
| … | 29 unchanged lines |
||
| 263 | 354 | /// stays on disk as uncommitted work: the next snapshot rebuilds it |
|
| 264 | 355 | /// against the new base, so the working copy always describes the |
|
| 265 | 356 | /// directory. |
|
| 266 | - | pub fn commit_selection(&self, message: &str, extra_deps: &[ChangeHash], selection: &Selection) -> Result<ChangeHash> { |
|
| 357 | + | pub fn commit_selection( |
|
| 358 | + | &self, |
|
| 359 | + | message: &str, |
|
| 360 | + | extra_deps: &[ChangeHash], |
|
| 361 | + | selection: &Selection, |
|
| 362 | + | ) -> Result<ChangeHash> { |
|
| 267 | 363 | let snap = self.snapshot("oxy commit")?; |
|
| 268 | 364 | if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() { |
|
| 269 | 365 | return fail("nothing to commit: the working copy matches its base"); |
|
| 270 | 366 | } |
|
| 271 | - | if let Some(s) = snap.statuses.iter().find(|s| s.status == ChangeStatus::ConflictEdited) { |
|
| 367 | + | if let Some(s) = snap |
|
| 368 | + | .statuses |
|
| 369 | + | .iter() |
|
| 370 | + | .find(|s| s.status == ChangeStatus::ConflictEdited) |
|
| 371 | + | { |
|
| 272 | 372 | return fail(format!("{} still contains conflict markers", s.path)); |
|
| 273 | 373 | } |
|
| 274 | 374 | if message.trim().is_empty() { |
|
| … | 24 unchanged lines |
||
| 299 | 399 | &format!( |
|
| 300 | 400 | "{{\"event\":\"pre-commit\",\"message\":{},\"paths\":{}}}", |
|
| 301 | 401 | json_str(message), |
|
| 302 | - | json_array(snap.statuses.iter().map(|s| format!("{{\"path\":{},\"status\":{}}}", json_str(&s.path), json_str(&format!("{:?}", s.status))))) |
|
| 402 | + | json_array(snap.statuses.iter().map(|s| format!( |
|
| 403 | + | "{{\"path\":{},\"status\":{}}}", |
|
| 404 | + | json_str(&s.path), |
|
| 405 | + | json_str(&format!("{:?}", s.status)) |
|
| 406 | + | ))) |
|
| 303 | 407 | ), |
|
| 304 | 408 | )?; |
|
| 305 | 409 | let (op_ref, op) = self.head_op()?; |
|
| … | 10 unchanged lines |
||
| 316 | 420 | } |
|
| 317 | 421 | let c = self.store.get_change(h)?; |
|
| 318 | 422 | let mut paths: BTreeSet<String> = BTreeSet::new(); |
|
| 319 | - | for n in c.tree_ops.iter().map(|o| o.node()).chain(c.edits.iter().map(|e| e.node())) { |
|
| 423 | + | for n in c |
|
| 424 | + | .tree_ops |
|
| 425 | + | .iter() |
|
| 426 | + | .map(|o| o.node()) |
|
| 427 | + | .chain(c.edits.iter().map(|e| e.node())) |
|
| 428 | + | { |
|
| 320 | 429 | if let Some(e) = by_node.get(&n) { |
|
| 321 | 430 | paths.insert(e.path.clone()); |
|
| 322 | 431 | } |
|
| … | 12 unchanged lines |
||
| 335 | 444 | deps.extend(snap.contributors.iter().copied()); |
|
| 336 | 445 | for d in extra_deps { |
|
| 337 | 446 | if !snap.base.contains(d) { |
|
| 338 | - | return fail(format!("--depends-on {d}: not in the working copy's base state")); |
|
| 447 | + | return fail(format!( |
|
| 448 | + | "--depends-on {d}: not in the working copy's base state" |
|
| 449 | + | )); |
|
| 339 | 450 | } |
|
| 340 | 451 | deps.insert(*d); |
|
| 341 | 452 | } |
|
| 342 | 453 | let mut change = Change { |
|
| 343 | - | change_id: if partial { ChangeId::random() } else { snap.wc.change_id }, |
|
| 454 | + | change_id: if partial { |
|
| 455 | + | ChangeId::random() |
|
| 456 | + | } else { |
|
| 457 | + | snap.wc.change_id |
|
| 458 | + | }, |
|
| 344 | 459 | deps: vec![], |
|
| 345 | 460 | author: self.author(), |
|
| 346 | 461 | hlc: Hlc::now(), |
|
| 347 | 462 | message: message.trim_end().to_string(), |
|
| 348 | 463 | tree_ops, |
|
| 349 | 464 | edits, |
|
| 350 | - | kind: if snap.resolves { ChangeKind::Resolution } else { ChangeKind::Normal }, |
|
| 465 | + | kind: if snap.resolves { |
|
| 466 | + | ChangeKind::Resolution |
|
| 467 | + | } else { |
|
| 468 | + | ChangeKind::Normal |
|
| 469 | + | }, |
|
| 351 | 470 | }; |
|
| 352 | 471 | if partial { |
|
| 353 | 472 | // Dependencies come from what the selected part references, not |
|
| … | 22 unchanged lines |
||
| 376 | 495 | with_new.push(hash); |
|
| 377 | 496 | let new_heads = st.maximal(&with_new); |
|
| 378 | 497 | // Automatic checkpoint once enough changes accumulated (spec §11). |
|
| 379 | - | let since = self.latest_checkpoint(&self.config.bookmark)?.map(|(_, cp)| cp.count).unwrap_or(0); |
|
| 380 | - | if self.config.checkpoint_interval > 0 && (st.len() as u64).saturating_sub(since) >= self.config.checkpoint_interval { |
|
| 498 | + | let since = self |
|
| 499 | + | .latest_checkpoint(&self.config.bookmark)? |
|
| 500 | + | .map(|(_, cp)| cp.count) |
|
| 501 | + | .unwrap_or(0); |
|
| 502 | + | if self.config.checkpoint_interval > 0 |
|
| 503 | + | && (st.len() as u64).saturating_sub(since) >= self.config.checkpoint_interval |
|
| 504 | + | { |
|
| 381 | 505 | let cp_state = self.load_state_full(&new_heads)?; |
|
| 382 | 506 | self.write_checkpoint(&self.config.bookmark, &cp_state, &new_heads)?; |
|
| 383 | 507 | } |
|
| … | 4 unchanged lines |
||
| 388 | 512 | let wc = self.new_working_copy(wc_base, ""); |
|
| 389 | 513 | let wc_hash = self.store.put_change(&wc)?; |
|
| 390 | 514 | let mut view = op.view.clone(); |
|
| 391 | - | view.bookmarks.insert(self.config.bookmark.clone(), self.store.put_state(&StateObject::new(new_heads))?); |
|
| 515 | + | view.bookmarks.insert( |
|
| 516 | + | self.config.bookmark.clone(), |
|
| 517 | + | self.store.put_state(&StateObject::new(new_heads))?, |
|
| 518 | + | ); |
|
| 392 | 519 | view.workspaces.insert(self.workspace, wc_hash); |
|
| 393 | 520 | self.record_op(&[op_ref], &format!("oxy commit -m {message:?}"), view)?; |
|
| 394 | 521 | if partial { |
|
| 395 | 522 | // The rest is rebuilt from disk by the next snapshot. |
|
| 396 | 523 | self.drop_manifest(); |
|
| 397 | 524 | } else { |
|
| 398 | - | self.rewrite_manifest_flags(snap.base_snapshot.is_conflicted() && !snap.resolves, false)?; |
|
| 525 | + | self.rewrite_manifest_flags( |
|
| 526 | + | snap.base_snapshot.is_conflicted() && !snap.resolves, |
|
| 527 | + | false, |
|
| 528 | + | )?; |
|
| 399 | 529 | } |
|
| 400 | 530 | self.maybe_autopack()?; |
|
| 401 | - | let _ = self.run_hook("post-commit", &format!("{{\"event\":\"post-commit\",\"change_id\":{},\"hash\":{},\"message\":{}}}", json_str(&snap.wc.change_id.to_hex()), json_str(&hash.to_hex()), json_str(message))); |
|
| 531 | + | let _ = self.run_hook( |
|
| 532 | + | "post-commit", |
|
| 533 | + | &format!( |
|
| 534 | + | "{{\"event\":\"post-commit\",\"change_id\":{},\"hash\":{},\"message\":{}}}", |
|
| 535 | + | json_str(&snap.wc.change_id.to_hex()), |
|
| 536 | + | json_str(&hash.to_hex()), |
|
| 537 | + | json_str(message) |
|
| 538 | + | ), |
|
| 539 | + | ); |
|
| 402 | 540 | Ok(hash) |
|
| 403 | 541 | } |
|
| 404 | 542 | } |
|
| … | 19 unchanged lines |
||
| 424 | 562 | ||
| 425 | 563 | /// The tree operations and edits on nodes under `paths`, plus the |
|
| 426 | 564 | /// additions of any new parent directories they need. |
|
| 427 | - | fn select_paths(wc: &Change, base_snapshot: &Snapshot, paths: &[String]) -> (Vec<TreeOp>, Vec<Edit>) { |
|
| 428 | - | let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))); |
|
| 565 | + | fn select_paths( |
|
| 566 | + | wc: &Change, |
|
| 567 | + | base_snapshot: &Snapshot, |
|
| 568 | + | paths: &[String], |
|
| 569 | + | ) -> (Vec<TreeOp>, Vec<Edit>) { |
|
| 570 | + | let selected = |p: &str| { |
|
| 571 | + | paths |
|
| 572 | + | .iter() |
|
| 573 | + | .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))) |
|
| 574 | + | }; |
|
| 429 | 575 | let by_node = base_snapshot.tree.by_node(); |
|
| 430 | 576 | let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new(); |
|
| 431 | 577 | for op in &wc.tree_ops { |
|
| 432 | - | if let TreeOp::AddNode { node, parent, name, kind, .. } = op { |
|
| 578 | + | if let TreeOp::AddNode { |
|
| 579 | + | node, |
|
| 580 | + | parent, |
|
| 581 | + | name, |
|
| 582 | + | kind, |
|
| 583 | + | .. |
|
| 584 | + | } = op |
|
| 585 | + | { |
|
| 433 | 586 | new_names.insert(*node, (*parent, name.clone(), *kind)); |
|
| 434 | 587 | } |
|
| 435 | 588 | } |
|
| 436 | 589 | let mut nodes: BTreeSet<NodeId> = BTreeSet::new(); |
|
| 437 | - | for n in wc.tree_ops.iter().map(|o| o.node()).chain(wc.edits.iter().map(|e| e.node())) { |
|
| 590 | + | for n in wc |
|
| 591 | + | .tree_ops |
|
| 592 | + | .iter() |
|
| 593 | + | .map(|o| o.node()) |
|
| 594 | + | .chain(wc.edits.iter().map(|e| e.node())) |
|
| 595 | + | { |
|
| 438 | 596 | if path_of_node(&n, &by_node, &new_names).is_some_and(|p| selected(&p)) { |
|
| 439 | 597 | nodes.insert(n); |
|
| 440 | 598 | } |
|
| … | 8 unchanged lines |
||
| 449 | 607 | } |
|
| 450 | 608 | } |
|
| 451 | 609 | } |
|
| 452 | - | let ops = wc.tree_ops.iter().filter(|o| nodes.contains(&o.node())).cloned().collect(); |
|
| 453 | - | let edits = wc.edits.iter().filter(|e| nodes.contains(&e.node())).cloned().collect(); |
|
| 610 | + | let ops = wc |
|
| 611 | + | .tree_ops |
|
| 612 | + | .iter() |
|
| 613 | + | .filter(|o| nodes.contains(&o.node())) |
|
| 614 | + | .cloned() |
|
| 615 | + | .collect(); |
|
| 616 | + | let edits = wc |
|
| 617 | + | .edits |
|
| 618 | + | .iter() |
|
| 619 | + | .filter(|e| nodes.contains(&e.node())) |
|
| 620 | + | .cloned() |
|
| 621 | + | .collect(); |
|
| 454 | 622 | (ops, edits) |
|
| 455 | 623 | } |
|
| 456 | 624 | ||
| … | 25 unchanged lines |
||
| 482 | 650 | _ => None, |
|
| 483 | 651 | }); |
|
| 484 | 652 | } |
|
| 485 | - | let add_index: HashMap<NodeId, usize> = wc.tree_ops.iter().enumerate().filter_map(|(i, o)| if let TreeOp::AddNode { node, .. } = o { Some((*node, i)) } else { None }).collect(); |
|
| 486 | - | let insert_for = |node: &NodeId, index: u32| -> Option<usize> { insert_ranges.iter().position(|r| matches!(r, Some((n, a, b)) if n == node && *a <= index && index < *b)) }; |
|
| 653 | + | let add_index: HashMap<NodeId, usize> = wc |
|
| 654 | + | .tree_ops |
|
| 655 | + | .iter() |
|
| 656 | + | .enumerate() |
|
| 657 | + | .filter_map(|(i, o)| { |
|
| 658 | + | if let TreeOp::AddNode { node, .. } = o { |
|
| 659 | + | Some((*node, i)) |
|
| 660 | + | } else { |
|
| 661 | + | None |
|
| 662 | + | } |
|
| 663 | + | }) |
|
| 664 | + | .collect(); |
|
| 665 | + | let insert_for = |node: &NodeId, index: u32| -> Option<usize> { |
|
| 666 | + | insert_ranges |
|
| 667 | + | .iter() |
|
| 668 | + | .position(|r| matches!(r, Some((n, a, b)) if n == node && *a <= index && index < *b)) |
|
| 669 | + | }; |
|
| 487 | 670 | // Closure. |
|
| 488 | 671 | let mut changed = true; |
|
| 489 | 672 | while changed { |
|
| … | 30 unchanged lines |
||
| 520 | 703 | changed = true; |
|
| 521 | 704 | } |
|
| 522 | 705 | } |
|
| 523 | - | if let Edit::Insert { node, after, before, .. } = e { |
|
| 706 | + | if let Edit::Insert { |
|
| 707 | + | node, |
|
| 708 | + | after, |
|
| 709 | + | before, |
|
| 710 | + | .. |
|
| 711 | + | } = e |
|
| 712 | + | { |
|
| 524 | 713 | for r in [after, before] { |
|
| 525 | 714 | if let AtomRef::Atom { change, index } = r { |
|
| 526 | 715 | if change.is_zero() { |
|
| … | 23 unchanged lines |
||
| 550 | 739 | } |
|
| 551 | 740 | let map_ref = |r: AtomRef, node: &NodeId| -> AtomRef { |
|
| 552 | 741 | match r { |
|
| 553 | - | AtomRef::Atom { change, index } if change.is_zero() => AtomRef::Atom { change, index: remap.get(&(*node, index)).copied().unwrap_or(index) }, |
|
| 742 | + | AtomRef::Atom { change, index } if change.is_zero() => AtomRef::Atom { |
|
| 743 | + | change, |
|
| 744 | + | index: remap.get(&(*node, index)).copied().unwrap_or(index), |
|
| 745 | + | }, |
|
| 554 | 746 | other => other, |
|
| 555 | 747 | } |
|
| 556 | 748 | }; |
|
| 557 | - | let ops = wc.tree_ops.iter().enumerate().filter(|(i, _)| ops_sel[*i]).map(|(_, o)| o.clone()).collect(); |
|
| 749 | + | let ops = wc |
|
| 750 | + | .tree_ops |
|
| 751 | + | .iter() |
|
| 752 | + | .enumerate() |
|
| 753 | + | .filter(|(i, _)| ops_sel[*i]) |
|
| 754 | + | .map(|(_, o)| o.clone()) |
|
| 755 | + | .collect(); |
|
| 558 | 756 | let edits = wc |
|
| 559 | 757 | .edits |
|
| 560 | 758 | .iter() |
|
| 561 | 759 | .enumerate() |
|
| 562 | 760 | .filter(|(j, _)| edits_sel[*j]) |
|
| 563 | 761 | .map(|(_, e)| match e { |
|
| 564 | - | Edit::Insert { node, after, before, lines } => Edit::Insert { node: *node, after: map_ref(*after, node), before: map_ref(*before, node), lines: lines.clone() }, |
|
| 762 | + | Edit::Insert { |
|
| 763 | + | node, |
|
| 764 | + | after, |
|
| 765 | + | before, |
|
| 766 | + | lines, |
|
| 767 | + | } => Edit::Insert { |
|
| 768 | + | node: *node, |
|
| 769 | + | after: map_ref(*after, node), |
|
| 770 | + | before: map_ref(*before, node), |
|
| 771 | + | lines: lines.clone(), |
|
| 772 | + | }, |
|
| 565 | 773 | other => other.clone(), |
|
| 566 | 774 | }) |
|
| 567 | 775 | .collect(); |
|
| … | 5 unchanged lines |
||
| 573 | 781 | let by_node = base_snapshot.tree.by_node(); |
|
| 574 | 782 | let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new(); |
|
| 575 | 783 | for op in &change.tree_ops { |
|
| 576 | - | if let TreeOp::AddNode { node, parent, name, kind, .. } = op { |
|
| 784 | + | if let TreeOp::AddNode { |
|
| 785 | + | node, |
|
| 786 | + | parent, |
|
| 787 | + | name, |
|
| 788 | + | kind, |
|
| 789 | + | .. |
|
| 790 | + | } = op |
|
| 791 | + | { |
|
| 577 | 792 | new_names.insert(*node, (*parent, name.clone(), *kind)); |
|
| 578 | 793 | } |
|
| 579 | 794 | } |
|
| 580 | 795 | let mut out = Vec::new(); |
|
| 581 | 796 | for (i, op) in change.tree_ops.iter().enumerate() { |
|
| 582 | - | let one = Change { tree_ops: vec![op.clone()], edits: vec![], ..change.clone() }; |
|
| 797 | + | let one = Change { |
|
| 798 | + | tree_ops: vec![op.clone()], |
|
| 799 | + | edits: vec![], |
|
| 800 | + | ..change.clone() |
|
| 801 | + | }; |
|
| 583 | 802 | let path = path_of_node(&op.node(), &by_node, &new_names).unwrap_or_default(); |
|
| 584 | - | out.push(Hunk { index: i, path, text: render_change(&one, base, base_snapshot) }); |
|
| 803 | + | out.push(Hunk { |
|
| 804 | + | index: i, |
|
| 805 | + | path, |
|
| 806 | + | text: render_change(&one, base, base_snapshot), |
|
| 807 | + | }); |
|
| 585 | 808 | } |
|
| 586 | 809 | for (j, e) in change.edits.iter().enumerate() { |
|
| 587 | - | let one = Change { tree_ops: vec![], edits: vec![e.clone()], ..change.clone() }; |
|
| 810 | + | let one = Change { |
|
| 811 | + | tree_ops: vec![], |
|
| 812 | + | edits: vec![e.clone()], |
|
| 813 | + | ..change.clone() |
|
| 814 | + | }; |
|
| 588 | 815 | let path = path_of_node(&e.node(), &by_node, &new_names).unwrap_or_default(); |
|
| 589 | - | out.push(Hunk { index: change.tree_ops.len() + j, path, text: render_change(&one, base, base_snapshot) }); |
|
| 816 | + | out.push(Hunk { |
|
| 817 | + | index: change.tree_ops.len() + j, |
|
| 818 | + | path, |
|
| 819 | + | text: render_change(&one, base, base_snapshot), |
|
| 820 | + | }); |
|
| 590 | 821 | } |
|
| 591 | 822 | out |
|
| 592 | 823 | } |
|
| … | 28 unchanged lines |
||
| 621 | 852 | self.load_state(&wc.deps)?.materialize() |
|
| 622 | 853 | } |
|
| 623 | 854 | }; |
|
| 624 | - | let Some(f) = snapshot.files.get(path) else { return fail(format!("{path} is not in that state")) }; |
|
| 855 | + | let Some(f) = snapshot.files.get(path) else { |
|
| 856 | + | return fail(format!("{path} is not in that state")); |
|
| 857 | + | }; |
|
| 625 | 858 | match &f.content { |
|
| 626 | 859 | FileContent::Dir => fail(format!("{path} is a directory")), |
|
| 627 | 860 | FileContent::Text { bytes, .. } => Ok(bytes.clone()), |
|
| … | 9 unchanged lines |
||
| 637 | 870 | /// number of paths touched. |
|
| 638 | 871 | pub fn discard(&self, paths: &[String]) -> Result<usize> { |
|
| 639 | 872 | let snap = self.snapshot("oxy discard")?; |
|
| 640 | - | let under = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))); |
|
| 873 | + | let under = |p: &str| { |
|
| 874 | + | paths |
|
| 875 | + | .iter() |
|
| 876 | + | .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))) |
|
| 877 | + | }; |
|
| 641 | 878 | let attrs = Repo::state_attributes(&snap.base_snapshot); |
|
| 642 | 879 | let mut n = 0; |
|
| 643 | 880 | for s in &snap.statuses { |
|
| … | 15 unchanged lines |
||
| 659 | 896 | match &f.content { |
|
| 660 | 897 | FileContent::Dir => fs::create_dir_all(self.root.join(&s.path))?, |
|
| 661 | 898 | FileContent::Text { bytes, .. } => { |
|
| 662 | - | let on_disk: std::borrow::Cow<[u8]> = if attrs.lookup(&s.path).eol == Some(crate::attributes::Eol::Crlf) { crate::attributes::to_crlf(bytes).into() } else { bytes.as_slice().into() }; |
|
| 899 | + | let on_disk: std::borrow::Cow<[u8]> = if attrs.lookup(&s.path).eol |
|
| 900 | + | == Some(crate::attributes::Eol::Crlf) |
|
| 901 | + | { |
|
| 902 | + | crate::attributes::to_crlf(bytes).into() |
|
| 903 | + | } else { |
|
| 904 | + | bytes.as_slice().into() |
|
| 905 | + | }; |
|
| 663 | 906 | self.write_file(&s.path, &on_disk, f.mode.exec)?; |
|
| 664 | 907 | } |
|
| 665 | 908 | FileContent::Opaque { blobs } => { |
|
| … | 14 unchanged lines |
||
| 680 | 923 | } |
|
| 681 | 924 | // A rename's old name is not in the statuses; put it back too. |
|
| 682 | 925 | for (p, f) in &snap.base_snapshot.files { |
|
| 683 | - | if under(p) && !self.root.join(p).exists() && f.kind != NodeKind::Dir && f.kind != NodeKind::Subrepo && !snap.statuses.iter().any(|s| s.path == *p) { |
|
| 926 | + | if under(p) |
|
| 927 | + | && !self.root.join(p).exists() |
|
| 928 | + | && f.kind != NodeKind::Dir |
|
| 929 | + | && f.kind != NodeKind::Subrepo |
|
| 930 | + | && !snap.statuses.iter().any(|s| s.path == *p) |
|
| 931 | + | { |
|
| 684 | 932 | let bytes = match &f.content { |
|
| 685 | 933 | FileContent::Text { bytes, .. } => bytes.clone(), |
|
| 686 | - | FileContent::Opaque { blobs } => blobs.first().map(|b| self.read_blob(b)).transpose()?.unwrap_or_default(), |
|
| 934 | + | FileContent::Opaque { blobs } => blobs |
|
| 935 | + | .first() |
|
| 936 | + | .map(|b| self.read_blob(b)) |
|
| 937 | + | .transpose()? |
|
| 938 | + | .unwrap_or_default(), |
|
| 687 | 939 | FileContent::Dir => continue, |
|
| 688 | 940 | }; |
|
| 689 | - | if f.kind == NodeKind::Symlink { self.write_symlink(p, &bytes)? } else { self.write_file(p, &bytes, f.mode.exec)? } |
|
| 941 | + | if f.kind == NodeKind::Symlink { |
|
| 942 | + | self.write_symlink(p, &bytes)? |
|
| 943 | + | } else { |
|
| 944 | + | self.write_file(p, &bytes, f.mode.exec)? |
|
| 945 | + | } |
|
| 690 | 946 | n += 1; |
|
| 691 | 947 | } |
|
| 692 | 948 | } |
|
| … | 6 unchanged lines |
||
| 699 | 955 | /// the remaining hunks. |
|
| 700 | 956 | pub fn discard_hunks(&self, indices: &[usize]) -> Result<usize> { |
|
| 701 | 957 | let (snap, hunks) = self.hunks()?; |
|
| 702 | - | let keep: Vec<usize> = hunks.iter().map(|h| h.index).filter(|i| !indices.contains(i)).collect(); |
|
| 958 | + | let keep: Vec<usize> = hunks |
|
| 959 | + | .iter() |
|
| 960 | + | .map(|h| h.index) |
|
| 961 | + | .filter(|i| !indices.contains(i)) |
|
| 962 | + | .collect(); |
|
| 703 | 963 | let (ops, edits) = select_hunks(&snap.wc, &keep); |
|
| 704 | 964 | let dropped_nodes: BTreeSet<NodeId> = hunks |
|
| 705 | 965 | .iter() |
|
| 706 | 966 | .filter(|h| indices.contains(&h.index)) |
|
| 707 | - | .map(|h| if h.index < snap.wc.tree_ops.len() { snap.wc.tree_ops[h.index].node() } else { snap.wc.edits[h.index - snap.wc.tree_ops.len()].node() }) |
|
| 967 | + | .map(|h| { |
|
| 968 | + | if h.index < snap.wc.tree_ops.len() { |
|
| 969 | + | snap.wc.tree_ops[h.index].node() |
|
| 970 | + | } else { |
|
| 971 | + | snap.wc.edits[h.index - snap.wc.tree_ops.len()].node() |
|
| 972 | + | } |
|
| 973 | + | }) |
|
| 708 | 974 | .collect(); |
|
| 709 | 975 | let mut partial = snap.wc.clone(); |
|
| 710 | 976 | partial.tree_ops = ops; |
|
| … | 13 unchanged lines |
||
| 724 | 990 | match &f.content { |
|
| 725 | 991 | FileContent::Dir => {} |
|
| 726 | 992 | FileContent::Text { bytes, .. } => { |
|
| 727 | - | let on_disk: std::borrow::Cow<[u8]> = if attrs.lookup(&e.path).eol == Some(crate::attributes::Eol::Crlf) { crate::attributes::to_crlf(bytes).into() } else { bytes.as_slice().into() }; |
|
| 993 | + | let on_disk: std::borrow::Cow<[u8]> = if attrs.lookup(&e.path).eol |
|
| 994 | + | == Some(crate::attributes::Eol::Crlf) |
|
| 995 | + | { |
|
| 996 | + | crate::attributes::to_crlf(bytes).into() |
|
| 997 | + | } else { |
|
| 998 | + | bytes.as_slice().into() |
|
| 999 | + | }; |
|
| 728 | 1000 | self.write_file(&e.path, &on_disk, f.mode.exec)?; |
|
| 729 | 1001 | } |
|
| 730 | 1002 | FileContent::Opaque { blobs } => { |
|
| 731 | - | let bytes = blobs.first().map(|b| self.read_blob(b)).transpose()?.unwrap_or_default(); |
|
| 732 | - | if f.kind == NodeKind::Symlink { self.write_symlink(&e.path, &bytes)? } else { self.write_file(&e.path, &bytes, f.mode.exec)? } |
|
| 1003 | + | let bytes = blobs |
|
| 1004 | + | .first() |
|
| 1005 | + | .map(|b| self.read_blob(b)) |
|
| 1006 | + | .transpose()? |
|
| 1007 | + | .unwrap_or_default(); |
|
| 1008 | + | if f.kind == NodeKind::Symlink { |
|
| 1009 | + | self.write_symlink(&e.path, &bytes)? |
|
| 1010 | + | } else { |
|
| 1011 | + | self.write_file(&e.path, &bytes, f.mode.exec)? |
|
| 1012 | + | } |
|
| 733 | 1013 | } |
|
| 734 | 1014 | } |
|
| 735 | 1015 | // A discarded rename: the new name goes. |
|
| 736 | - | if let Some(old_path) = snap.statuses.iter().find(|s| s.status == ChangeStatus::Renamed && !target.files.contains_key(&s.path)).map(|s| s.path.clone()) { |
|
| 1016 | + | if let Some(old_path) = snap |
|
| 1017 | + | .statuses |
|
| 1018 | + | .iter() |
|
| 1019 | + | .find(|s| { |
|
| 1020 | + | s.status == ChangeStatus::Renamed && !target.files.contains_key(&s.path) |
|
| 1021 | + | }) |
|
| 1022 | + | .map(|s| s.path.clone()) |
|
| 1023 | + | { |
|
| 737 | 1024 | let _ = fs::remove_file(self.root.join(old_path)); |
|
| 738 | 1025 | } |
|
| 739 | 1026 | n += 1; |
|
| 740 | 1027 | } |
|
| 741 | 1028 | None => { |
|
| 742 | 1029 | // The node exists in neither target nor base with a live path: an addition to remove. |
|
| 743 | - | if let Some(added) = snap.statuses.iter().find(|s| s.status == ChangeStatus::Added && !target.files.contains_key(&s.path) && !by_node_base.contains_key(&node)) { |
|
| 1030 | + | if let Some(added) = snap.statuses.iter().find(|s| { |
|
| 1031 | + | s.status == ChangeStatus::Added |
|
| 1032 | + | && !target.files.contains_key(&s.path) |
|
| 1033 | + | && !by_node_base.contains_key(&node) |
|
| 1034 | + | }) { |
|
| 744 | 1035 | let full = self.root.join(&added.path); |
|
| 745 | - | if full.is_dir() { let _ = fs::remove_dir_all(&full); } else { let _ = fs::remove_file(&full); } |
|
| 1036 | + | if full.is_dir() { |
|
| 1037 | + | let _ = fs::remove_dir_all(&full); |
|
| 1038 | + | } else { |
|
| 1039 | + | let _ = fs::remove_file(&full); |
|
| 1040 | + | } |
|
| 746 | 1041 | n += 1; |
|
| 747 | 1042 | } |
|
| 748 | 1043 | } |
|
| … | 32 unchanged lines |
||
| 781 | 1076 | let (_, wc) = self.working_copy()?; |
|
| 782 | 1077 | let base_snapshot = self.load_state(&wc.deps)?.materialize(); |
|
| 783 | 1078 | let state_snapshot = self.load_state(&heads)?.materialize(); |
|
| 784 | - | let node = base_snapshot.files.get(path).or_else(|| state_snapshot.files.get(path)).map(|f| f.node); |
|
| 785 | - | let Some(node) = node else { return fail(format!("{path} is not tracked")) }; |
|
| 1079 | + | let node = base_snapshot |
|
| 1080 | + | .files |
|
| 1081 | + | .get(path) |
|
| 1082 | + | .or_else(|| state_snapshot.files.get(path)) |
|
| 1083 | + | .map(|f| f.node); |
|
| 1084 | + | let Some(node) = node else { |
|
| 1085 | + | return fail(format!("{path} is not tracked")); |
|
| 1086 | + | }; |
|
| 786 | 1087 | Ok(self.entries_touching(entries, node)) |
|
| 787 | 1088 | } |
|
| 788 | 1089 | ||
| … | 2 unchanged lines |
||
| 791 | 1092 | pub fn log_for_path_in(&self, path: &str, heads: &[ChangeHash]) -> Result<Vec<LogEntry>> { |
|
| 792 | 1093 | let entries = self.log_state(heads)?; |
|
| 793 | 1094 | let snapshot = self.load_state(heads)?.materialize(); |
|
| 794 | - | let Some(node) = snapshot.files.get(path).map(|f| f.node) else { return fail(format!("{path} is not in that state")) }; |
|
| 1095 | + | let Some(node) = snapshot.files.get(path).map(|f| f.node) else { |
|
| 1096 | + | return fail(format!("{path} is not in that state")); |
|
| 1097 | + | }; |
|
| 795 | 1098 | Ok(self.entries_touching(entries, node)) |
|
| 796 | 1099 | } |
|
| 797 | 1100 | ||
| 798 | 1101 | fn entries_touching(&self, entries: Vec<LogEntry>, node: NodeId) -> Vec<LogEntry> { |
|
| 799 | 1102 | let mut out = Vec::new(); |
|
| 800 | 1103 | for e in entries { |
|
| 801 | - | let Ok(c) = self.store.get_change(&e.hash) else { continue }; |
|
| 802 | - | if c.tree_ops.iter().any(|o| o.node() == node) || c.edits.iter().any(|x| x.node() == node) { |
|
| 1104 | + | let Ok(c) = self.store.get_change(&e.hash) else { |
|
| 1105 | + | continue; |
|
| 1106 | + | }; |
|
| 1107 | + | if c.tree_ops.iter().any(|o| o.node() == node) |
|
| 1108 | + | || c.edits.iter().any(|x| x.node() == node) |
|
| 1109 | + | { |
|
| 803 | 1110 | out.push(e); |
|
| 804 | 1111 | } |
|
| 805 | 1112 | } |
|
| … | 6 unchanged lines |
||
| 812 | 1119 | let by_node = base_snapshot.tree.by_node(); |
|
| 813 | 1120 | let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new(); |
|
| 814 | 1121 | for op in &change.tree_ops { |
|
| 815 | - | if let TreeOp::AddNode { node, parent, name, kind, .. } = op { |
|
| 1122 | + | if let TreeOp::AddNode { |
|
| 1123 | + | node, |
|
| 1124 | + | parent, |
|
| 1125 | + | name, |
|
| 1126 | + | kind, |
|
| 1127 | + | .. |
|
| 1128 | + | } = op |
|
| 1129 | + | { |
|
| 816 | 1130 | new_names.insert(*node, (*parent, name.clone(), *kind)); |
|
| 817 | 1131 | } |
|
| 818 | 1132 | } |
|
| 819 | - | let path_of = |n: &NodeId| -> String { path_of_node(n, &by_node, &new_names).unwrap_or_else(|| format!("<node {}>", n.short(8))) }; |
|
| 1133 | + | let path_of = |n: &NodeId| -> String { |
|
| 1134 | + | path_of_node(n, &by_node, &new_names).unwrap_or_else(|| format!("<node {}>", n.short(8))) |
|
| 1135 | + | }; |
|
| 820 | 1136 | let mut files: BTreeMap<NodeId, FileDiff> = BTreeMap::new(); |
|
| 821 | 1137 | let mut order: Vec<NodeId> = Vec::new(); |
|
| 822 | 1138 | let mut entry = |files: &mut BTreeMap<NodeId, FileDiff>, n: NodeId, kind: &str| { |
|
| 823 | 1139 | if let std::collections::btree_map::Entry::Vacant(v) = files.entry(n) { |
|
| 824 | 1140 | order.push(n); |
|
| 825 | - | v.insert(FileDiff { path: path_of(&n), kind: kind.to_string(), hunks: vec![], from: None }); |
|
| 1141 | + | v.insert(FileDiff { |
|
| 1142 | + | path: path_of(&n), |
|
| 1143 | + | kind: kind.to_string(), |
|
| 1144 | + | hunks: vec![], |
|
| 1145 | + | from: None, |
|
| 1146 | + | }); |
|
| 826 | 1147 | } |
|
| 827 | 1148 | }; |
|
| 828 | 1149 | for op in &change.tree_ops { |
|
| … | 9 unchanged lines |
||
| 838 | 1159 | files.get_mut(node).unwrap().path = e.path.clone(); |
|
| 839 | 1160 | } |
|
| 840 | 1161 | } |
|
| 841 | - | TreeOp::Move { node, parent, name, .. } => { |
|
| 1162 | + | TreeOp::Move { |
|
| 1163 | + | node, parent, name, .. |
|
| 1164 | + | } => { |
|
| 842 | 1165 | entry(&mut files, *node, "renamed"); |
|
| 843 | 1166 | let f = files.get_mut(node).unwrap(); |
|
| 844 | 1167 | f.from = by_node.get(node).map(|e| e.path.clone()); |
|
| 845 | - | let dir = if parent.is_root() { String::new() } else { path_of_node(parent, &by_node, &new_names).unwrap_or_default() }; |
|
| 846 | - | f.path = if dir.is_empty() { name.clone() } else { format!("{dir}/{name}") }; |
|
| 1168 | + | let dir = if parent.is_root() { |
|
| 1169 | + | String::new() |
|
| 1170 | + | } else { |
|
| 1171 | + | path_of_node(parent, &by_node, &new_names).unwrap_or_default() |
|
| 1172 | + | }; |
|
| 1173 | + | f.path = if dir.is_empty() { |
|
| 1174 | + | name.clone() |
|
| 1175 | + | } else { |
|
| 1176 | + | format!("{dir}/{name}") |
|
| 1177 | + | }; |
|
| 847 | 1178 | } |
|
| 848 | 1179 | TreeOp::SetMode { node, .. } => entry(&mut files, *node, "mode"), |
|
| 849 | 1180 | } |
|
| … | 1 unchanged lines |
||
| 851 | 1182 | // Base line numbers per node, 1-based. |
|
| 852 | 1183 | let mut line_of: HashMap<NodeId, HashMap<(ChangeHash, u32), u32>> = HashMap::new(); |
|
| 853 | 1184 | let mut lines_for = |n: &NodeId| -> HashMap<(ChangeHash, u32), u32> { |
|
| 854 | - | line_of.entry(*n).or_insert_with(|| base.live_lines(n).into_iter().enumerate().map(|(i, (k, _))| (k, i as u32 + 1)).collect()).clone() |
|
| 1185 | + | line_of |
|
| 1186 | + | .entry(*n) |
|
| 1187 | + | .or_insert_with(|| { |
|
| 1188 | + | base.live_lines(n) |
|
| 1189 | + | .into_iter() |
|
| 1190 | + | .enumerate() |
|
| 1191 | + | .map(|(i, (k, _))| (k, i as u32 + 1)) |
|
| 1192 | + | .collect() |
|
| 1193 | + | }) |
|
| 1194 | + | .clone() |
|
| 855 | 1195 | }; |
|
| 856 | 1196 | for e in &change.edits { |
|
| 857 | 1197 | let n = e.node(); |
|
| … | 6 unchanged lines |
||
| 864 | 1204 | let base_line = match after { |
|
| 865 | 1205 | AtomRef::Start => 0, |
|
| 866 | 1206 | AtomRef::End => map.len() as u32, |
|
| 867 | - | AtomRef::Atom { change, index } if change.is_zero() => f.hunks.last().map(|h| h.base_line).unwrap_or(0), |
|
| 868 | - | AtomRef::Atom { change, index } => map.get(&(*change, *index)).copied().unwrap_or(0), |
|
| 1207 | + | AtomRef::Atom { change, index } if change.is_zero() => { |
|
| 1208 | + | f.hunks.last().map(|h| h.base_line).unwrap_or(0) |
|
| 1209 | + | } |
|
| 1210 | + | AtomRef::Atom { change, index } => { |
|
| 1211 | + | map.get(&(*change, *index)).copied().unwrap_or(0) |
|
| 1212 | + | } |
|
| 869 | 1213 | }; |
|
| 870 | - | f.hunks.push(DiffHunk { base_line, added: lines.clone(), removed: vec![] }); |
|
| 1214 | + | f.hunks.push(DiffHunk { |
|
| 1215 | + | base_line, |
|
| 1216 | + | added: lines.clone(), |
|
| 1217 | + | removed: vec![], |
|
| 1218 | + | }); |
|
| 871 | 1219 | } |
|
| 872 | 1220 | Edit::Delete { atoms, .. } => { |
|
| 873 | 1221 | let map = lines_for(&n); |
|
| … | 1 unchanged lines |
||
| 875 | 1223 | for r in atoms { |
|
| 876 | 1224 | for i in r.from..r.to { |
|
| 877 | 1225 | let line = map.get(&(r.change, i)).copied().unwrap_or(0); |
|
| 878 | - | let text = base.file(&n).and_then(|g| g.content(&(r.change, i))).unwrap_or(b"").to_vec(); |
|
| 1226 | + | let text = base |
|
| 1227 | + | .file(&n) |
|
| 1228 | + | .and_then(|g| g.content(&(r.change, i))) |
|
| 1229 | + | .unwrap_or(b"") |
|
| 1230 | + | .to_vec(); |
|
| 879 | 1231 | removed.push((line, text)); |
|
| 880 | 1232 | } |
|
| 881 | 1233 | } |
|
| 882 | - | let base_line = removed.first().map(|(l, _)| l.saturating_sub(1)).unwrap_or(0); |
|
| 883 | - | f.hunks.push(DiffHunk { base_line, added: vec![], removed }); |
|
| 1234 | + | let base_line = removed |
|
| 1235 | + | .first() |
|
| 1236 | + | .map(|(l, _)| l.saturating_sub(1)) |
|
| 1237 | + | .unwrap_or(0); |
|
| 1238 | + | f.hunks.push(DiffHunk { |
|
| 1239 | + | base_line, |
|
| 1240 | + | added: vec![], |
|
| 1241 | + | removed, |
|
| 1242 | + | }); |
|
| 884 | 1243 | } |
|
| 885 | 1244 | Edit::Replace { .. } => { |
|
| 886 | 1245 | if !is_new { |
|
| … | 22 unchanged lines |
||
| 909 | 1268 | } |
|
| 910 | 1269 | ||
| 911 | 1270 | impl<'a> ChangeBuilder<'a> { |
|
| 912 | - | fn new(base: &'a State, base_snapshot: &'a Snapshot, reuse: HashMap<String, (NodeId, NodeKind)>) -> Self { |
|
| 1271 | + | fn new( |
|
| 1272 | + | base: &'a State, |
|
| 1273 | + | base_snapshot: &'a Snapshot, |
|
| 1274 | + | reuse: HashMap<String, (NodeId, NodeKind)>, |
|
| 1275 | + | ) -> Self { |
|
| 913 | 1276 | let mut dir_nodes: HashMap<String, NodeId> = HashMap::new(); |
|
| 914 | 1277 | dir_nodes.insert(String::new(), NodeId::ROOT); |
|
| 915 | 1278 | for (p, f) in &base_snapshot.files { |
|
| … | 1 unchanged lines |
||
| 917 | 1280 | dir_nodes.insert(p.clone(), f.node); |
|
| 918 | 1281 | } |
|
| 919 | 1282 | } |
|
| 920 | - | ChangeBuilder { base, base_snapshot, tree_ops: vec![], edits: vec![], statuses: vec![], dir_nodes, reuse, contributors: BTreeSet::new(), resolves: false, attrs: crate::attributes::Attributes::default(), blobs: HashMap::new() } |
|
| 1283 | + | ChangeBuilder { |
|
| 1284 | + | base, |
|
| 1285 | + | base_snapshot, |
|
| 1286 | + | tree_ops: vec![], |
|
| 1287 | + | edits: vec![], |
|
| 1288 | + | statuses: vec![], |
|
| 1289 | + | dir_nodes, |
|
| 1290 | + | reuse, |
|
| 1291 | + | contributors: BTreeSet::new(), |
|
| 1292 | + | resolves: false, |
|
| 1293 | + | attrs: crate::attributes::Attributes::default(), |
|
| 1294 | + | blobs: HashMap::new(), |
|
| 1295 | + | } |
|
| 921 | 1296 | } |
|
| 922 | 1297 | ||
| 923 | 1298 | fn with_attributes(mut self, attrs: crate::attributes::Attributes) -> Self { |
|
| … | 4 unchanged lines |
||
| 928 | 1303 | /// Whether a file is opaque: forced by `binary`, forced text by `text` |
|
| 929 | 1304 | /// only when it has no NUL bytes, else sniffed. |
|
| 930 | 1305 | /// The blob for an opaque entry: the cached one, else stored now. |
|
| 931 | - | fn blob_of(&mut self, repo: &Repo, path: &str, e: &FsEntry, nocompress: bool) -> Result<ObjectRef> { |
|
| 1306 | + | fn blob_of( |
|
| 1307 | + | &mut self, |
|
| 1308 | + | repo: &Repo, |
|
| 1309 | + | path: &str, |
|
| 1310 | + | e: &FsEntry, |
|
| 1311 | + | nocompress: bool, |
|
| 1312 | + | ) -> Result<ObjectRef> { |
|
| 932 | 1313 | let b = match e.blob { |
|
| 933 | 1314 | Some(b) => b, |
|
| 934 | 1315 | None => repo.store_blob_for(&e.content, nocompress)?, |
|
| … | 18 unchanged lines |
||
| 953 | 1334 | ||
| 954 | 1335 | /// Text content as stored: `\r\n` normalized for `eol=` files. |
|
| 955 | 1336 | fn stored_text(&self, path: &str, content: &[u8]) -> Vec<u8> { |
|
| 956 | - | if self.attrs.lookup(path).eol.is_some() { crate::attributes::normalize_eol(content) } else { content.to_vec() } |
|
| 1337 | + | if self.attrs.lookup(path).eol.is_some() { |
|
| 1338 | + | crate::attributes::normalize_eol(content) |
|
| 1339 | + | } else { |
|
| 1340 | + | content.to_vec() |
|
| 1341 | + | } |
|
| 957 | 1342 | } |
|
| 958 | 1343 | ||
| 959 | 1344 | fn reused_node(&self, path: &str, kind: NodeKind) -> Option<NodeId> { |
|
| 960 | - | self.reuse.get(path).filter(|(_, k)| *k == kind).map(|(n, _)| *n) |
|
| 1345 | + | self.reuse |
|
| 1346 | + | .get(path) |
|
| 1347 | + | .filter(|(_, k)| *k == kind) |
|
| 1348 | + | .map(|(n, _)| *n) |
|
| 961 | 1349 | } |
|
| 962 | 1350 | ||
| 963 | 1351 | fn ensure_dir(&mut self, path: &str) -> NodeId { |
|
| … | 10 unchanged lines |
||
| 974 | 1362 | parent = match self.dir_nodes.get(&prefix) { |
|
| 975 | 1363 | Some(n) => *n, |
|
| 976 | 1364 | None => { |
|
| 977 | - | let node = self.reused_node(&prefix, NodeKind::Dir).unwrap_or_else(NodeId::random); |
|
| 978 | - | self.tree_ops.push(TreeOp::AddNode { node, kind: NodeKind::Dir, mode: Mode::default(), parent, name: comp.to_string() }); |
|
| 1365 | + | let node = self |
|
| 1366 | + | .reused_node(&prefix, NodeKind::Dir) |
|
| 1367 | + | .unwrap_or_else(NodeId::random); |
|
| 1368 | + | self.tree_ops.push(TreeOp::AddNode { |
|
| 1369 | + | node, |
|
| 1370 | + | kind: NodeKind::Dir, |
|
| 1371 | + | mode: Mode::default(), |
|
| 1372 | + | parent, |
|
| 1373 | + | name: comp.to_string(), |
|
| 1374 | + | }); |
|
| 979 | 1375 | self.dir_nodes.insert(prefix.clone(), node); |
|
| 980 | 1376 | node |
|
| 981 | 1377 | } |
|
| … | 5 unchanged lines |
||
| 987 | 1383 | fn add_path(&mut self, repo: &Repo, path: &str, e: &FsEntry) -> Result<()> { |
|
| 988 | 1384 | let (dir, name) = split_path(path); |
|
| 989 | 1385 | let parent = self.ensure_dir(dir); |
|
| 990 | - | let node = self.reused_node(path, e.kind).unwrap_or_else(NodeId::random); |
|
| 1386 | + | let node = self |
|
| 1387 | + | .reused_node(path, e.kind) |
|
| 1388 | + | .unwrap_or_else(NodeId::random); |
|
| 991 | 1389 | let opaque = self.opaque_for(path, e); |
|
| 992 | 1390 | let nocompress = self.attrs.lookup(path).nocompress; |
|
| 993 | - | self.tree_ops.push(TreeOp::AddNode { node, kind: e.kind, mode: Mode { exec: e.exec, opaque }, parent, name: name.to_string() }); |
|
| 1391 | + | self.tree_ops.push(TreeOp::AddNode { |
|
| 1392 | + | node, |
|
| 1393 | + | kind: e.kind, |
|
| 1394 | + | mode: Mode { |
|
| 1395 | + | exec: e.exec, |
|
| 1396 | + | opaque, |
|
| 1397 | + | }, |
|
| 1398 | + | parent, |
|
| 1399 | + | name: name.to_string(), |
|
| 1400 | + | }); |
|
| 994 | 1401 | if opaque { |
|
| 995 | 1402 | let blob = self.blob_of(repo, path, e, nocompress)?; |
|
| 996 | - | self.edits.push(Edit::Replace { node, old: None, new: Some(blob) }); |
|
| 1403 | + | self.edits.push(Edit::Replace { |
|
| 1404 | + | node, |
|
| 1405 | + | old: None, |
|
| 1406 | + | new: Some(blob), |
|
| 1407 | + | }); |
|
| 997 | 1408 | } else if !e.content.is_empty() { |
|
| 998 | 1409 | let content = self.stored_text(path, &e.content); |
|
| 999 | 1410 | self.edits.push(Edit::Insert { |
|
| … | 3 unchanged lines |
||
| 1003 | 1414 | lines: split_lines(&content).iter().map(|l| l.to_vec()).collect(), |
|
| 1004 | 1415 | }); |
|
| 1005 | 1416 | } |
|
| 1006 | - | self.statuses.push(PathStatus { path: path.to_string(), status: ChangeStatus::Added }); |
|
| 1417 | + | self.statuses.push(PathStatus { |
|
| 1418 | + | path: path.to_string(), |
|
| 1419 | + | status: ChangeStatus::Added, |
|
| 1420 | + | }); |
|
| 1007 | 1421 | Ok(()) |
|
| 1008 | 1422 | } |
|
| 1009 | 1423 | ||
| 1010 | 1424 | /// Move the node at `old` to `new` and apply `e`'s content to it. |
|
| 1011 | - | fn move_path(&mut self, repo: &Repo, old: &str, new: &str, e: &FsEntry, expected: &crate::state::FileState) -> Result<()> { |
|
| 1425 | + | fn move_path( |
|
| 1426 | + | &mut self, |
|
| 1427 | + | repo: &Repo, |
|
| 1428 | + | old: &str, |
|
| 1429 | + | new: &str, |
|
| 1430 | + | e: &FsEntry, |
|
| 1431 | + | expected: &crate::state::FileState, |
|
| 1432 | + | ) -> Result<()> { |
|
| 1012 | 1433 | let entry = &self.base_snapshot.tree.entries[old]; |
|
| 1013 | 1434 | let (dir, name) = split_path(new); |
|
| 1014 | 1435 | let parent = self.ensure_dir(dir); |
|
| 1015 | - | self.tree_ops.push(TreeOp::Move { node: expected.node, old: op_ref(entry.placement), parent, name: name.to_string() }); |
|
| 1436 | + | self.tree_ops.push(TreeOp::Move { |
|
| 1437 | + | node: expected.node, |
|
| 1438 | + | old: op_ref(entry.placement), |
|
| 1439 | + | parent, |
|
| 1440 | + | name: name.to_string(), |
|
| 1441 | + | }); |
|
| 1016 | 1442 | self.update_path(repo, old, e, expected)?; |
|
| 1017 | 1443 | // update_path reports under the old path; the rename is what the user sees. |
|
| 1018 | 1444 | self.statuses.retain(|s| s.path != old); |
|
| 1019 | - | self.statuses.push(PathStatus { path: new.to_string(), status: ChangeStatus::Renamed }); |
|
| 1445 | + | self.statuses.push(PathStatus { |
|
| 1446 | + | path: new.to_string(), |
|
| 1447 | + | status: ChangeStatus::Renamed, |
|
| 1448 | + | }); |
|
| 1020 | 1449 | Ok(()) |
|
| 1021 | 1450 | } |
|
| 1022 | 1451 | ||
| 1023 | 1452 | fn remove_path(&mut self, path: &str, expected: &crate::state::FileState) { |
|
| 1024 | 1453 | let entry = &self.base_snapshot.tree.entries[path]; |
|
| 1025 | - | self.tree_ops.push(TreeOp::RemoveNode { node: expected.node, placement: op_ref(entry.placement) }); |
|
| 1454 | + | self.tree_ops.push(TreeOp::RemoveNode { |
|
| 1455 | + | node: expected.node, |
|
| 1456 | + | placement: op_ref(entry.placement), |
|
| 1457 | + | }); |
|
| 1026 | 1458 | if path.contains(".oxy-conflict-") || expected.is_conflicted() { |
|
| 1027 | 1459 | self.resolves = true; |
|
| 1028 | 1460 | } |
|
| 1029 | - | self.statuses.push(PathStatus { path: path.to_string(), status: ChangeStatus::Removed }); |
|
| 1461 | + | self.statuses.push(PathStatus { |
|
| 1462 | + | path: path.to_string(), |
|
| 1463 | + | status: ChangeStatus::Removed, |
|
| 1464 | + | }); |
|
| 1030 | 1465 | } |
|
| 1031 | 1466 | ||
| 1032 | - | fn update_path(&mut self, repo: &Repo, path: &str, e: &FsEntry, expected: &crate::state::FileState) -> Result<()> { |
|
| 1467 | + | fn update_path( |
|
| 1468 | + | &mut self, |
|
| 1469 | + | repo: &Repo, |
|
| 1470 | + | path: &str, |
|
| 1471 | + | e: &FsEntry, |
|
| 1472 | + | expected: &crate::state::FileState, |
|
| 1473 | + | ) -> Result<()> { |
|
| 1033 | 1474 | if expected.kind != e.kind { |
|
| 1034 | 1475 | self.remove_path(path, expected); |
|
| 1035 | 1476 | self.statuses.pop(); |
|
| 1036 | 1477 | self.add_path(repo, path, e)?; |
|
| 1037 | 1478 | self.statuses.pop(); |
|
| 1038 | - | self.statuses.push(PathStatus { path: path.to_string(), status: ChangeStatus::Modified }); |
|
| 1479 | + | self.statuses.push(PathStatus { |
|
| 1480 | + | path: path.to_string(), |
|
| 1481 | + | status: ChangeStatus::Modified, |
|
| 1482 | + | }); |
|
| 1039 | 1483 | return Ok(()); |
|
| 1040 | 1484 | } |
|
| 1041 | 1485 | let entry = &self.base_snapshot.tree.entries[path]; |
|
| … | 5 unchanged lines |
||
| 1047 | 1491 | let disk_text = self.stored_text(path, &e.content); |
|
| 1048 | 1492 | match &expected.content { |
|
| 1049 | 1493 | FileContent::Dir => {} |
|
| 1050 | - | FileContent::Text { bytes, materialized } => { |
|
| 1494 | + | FileContent::Text { |
|
| 1495 | + | bytes, |
|
| 1496 | + | materialized, |
|
| 1497 | + | } => { |
|
| 1051 | 1498 | if materialized.is_conflicted() { |
|
| 1052 | 1499 | if bytes == &disk_text { |
|
| 1053 | 1500 | return Ok(()); |
|
| 1054 | 1501 | } |
|
| 1055 | 1502 | if has_conflict_markers(&e.content) { |
|
| 1056 | - | self.statuses.push(PathStatus { path: path.to_string(), status: ChangeStatus::ConflictEdited }); |
|
| 1503 | + | self.statuses.push(PathStatus { |
|
| 1504 | + | path: path.to_string(), |
|
| 1505 | + | status: ChangeStatus::ConflictEdited, |
|
| 1506 | + | }); |
|
| 1057 | 1507 | return Ok(()); |
|
| 1058 | 1508 | } |
|
| 1059 | 1509 | // Resolution (spec §9.6): diff against the unconflicted lines |
|
| … | 3 unchanged lines |
||
| 1063 | 1513 | let regions = self.base.conflict_regions(&node); |
|
| 1064 | 1514 | let mut edits = text_edits(node, ®ions.plain, &disk_text); |
|
| 1065 | 1515 | if !regions.region_atoms.is_empty() { |
|
| 1066 | - | edits.insert(0, Edit::Delete { node, atoms: ranges(®ions.region_atoms) }); |
|
| 1516 | + | edits.insert( |
|
| 1517 | + | 0, |
|
| 1518 | + | Edit::Delete { |
|
| 1519 | + | node, |
|
| 1520 | + | atoms: ranges(®ions.region_atoms), |
|
| 1521 | + | }, |
|
| 1522 | + | ); |
|
| 1067 | 1523 | } |
|
| 1068 | 1524 | self.edits.extend(edits); |
|
| 1069 | 1525 | self.contributors.extend(regions.contributors); |
|
| 1070 | 1526 | self.resolves = true; |
|
| 1071 | 1527 | if e.exec != expected.mode.exec { |
|
| 1072 | 1528 | if let Some(old) = mode_atom { |
|
| 1073 | - | self.tree_ops.push(TreeOp::SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } }); |
|
| 1529 | + | self.tree_ops.push(TreeOp::SetMode { |
|
| 1530 | + | node, |
|
| 1531 | + | old, |
|
| 1532 | + | mode: Mode { |
|
| 1533 | + | exec: e.exec, |
|
| 1534 | + | opaque: false, |
|
| 1535 | + | }, |
|
| 1536 | + | }); |
|
| 1074 | 1537 | } |
|
| 1075 | 1538 | } |
|
| 1076 | - | self.statuses.push(PathStatus { path: path.to_string(), status: ChangeStatus::Resolved }); |
|
| 1539 | + | self.statuses.push(PathStatus { |
|
| 1540 | + | path: path.to_string(), |
|
| 1541 | + | status: ChangeStatus::Resolved, |
|
| 1542 | + | }); |
|
| 1077 | 1543 | return Ok(()); |
|
| 1078 | 1544 | } |
|
| 1079 | 1545 | if wants_opaque { |
|
| 1080 | 1546 | // Text → opaque (spec §7.5), by content or by attribute. |
|
| 1081 | 1547 | if let Some(old) = mode_atom { |
|
| 1082 | - | self.tree_ops.push(TreeOp::SetMode { node, old, mode: Mode { exec: e.exec, opaque: true } }); |
|
| 1548 | + | self.tree_ops.push(TreeOp::SetMode { |
|
| 1549 | + | node, |
|
| 1550 | + | old, |
|
| 1551 | + | mode: Mode { |
|
| 1552 | + | exec: e.exec, |
|
| 1553 | + | opaque: true, |
|
| 1554 | + | }, |
|
| 1555 | + | }); |
|
| 1083 | 1556 | } |
|
| 1084 | 1557 | let live: Vec<_> = repo_live_keys(self.base, &node); |
|
| 1085 | 1558 | if !live.is_empty() { |
|
| 1086 | - | self.edits.push(Edit::Delete { node, atoms: ranges(&live) }); |
|
| 1559 | + | self.edits.push(Edit::Delete { |
|
| 1560 | + | node, |
|
| 1561 | + | atoms: ranges(&live), |
|
| 1562 | + | }); |
|
| 1087 | 1563 | } |
|
| 1088 | 1564 | let blob = self.blob_of(repo, path, e, nocompress)?; |
|
| 1089 | - | self.edits.push(Edit::Replace { node, old: None, new: Some(blob) }); |
|
| 1565 | + | self.edits.push(Edit::Replace { |
|
| 1566 | + | node, |
|
| 1567 | + | old: None, |
|
| 1568 | + | new: Some(blob), |
|
| 1569 | + | }); |
|
| 1090 | 1570 | status = Some(ChangeStatus::Modified); |
|
| 1091 | 1571 | } else if bytes != &disk_text { |
|
| 1092 | 1572 | let old = self.base.live_lines(&node); |
|
| … | 6 unchanged lines |
||
| 1099 | 1579 | let switched_to_opaque = wants_opaque; |
|
| 1100 | 1580 | if !switched_to_opaque && e.exec != expected.mode.exec { |
|
| 1101 | 1581 | if let Some(old) = mode_atom { |
|
| 1102 | - | self.tree_ops.push(TreeOp::SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } }); |
|
| 1582 | + | self.tree_ops.push(TreeOp::SetMode { |
|
| 1583 | + | node, |
|
| 1584 | + | old, |
|
| 1585 | + | mode: Mode { |
|
| 1586 | + | exec: e.exec, |
|
| 1587 | + | opaque: false, |
|
| 1588 | + | }, |
|
| 1589 | + | }); |
|
| 1103 | 1590 | status.get_or_insert(ChangeStatus::ModeChanged); |
|
| 1104 | 1591 | } |
|
| 1105 | 1592 | } |
|
| … | 6 unchanged lines |
||
| 1112 | 1599 | } |
|
| 1113 | 1600 | if blobs.len() > 1 { |
|
| 1114 | 1601 | if !blobs.contains(&new_ref) { |
|
| 1115 | - | self.statuses.push(PathStatus { path: path.to_string(), status: ChangeStatus::ConflictEdited }); |
|
| 1602 | + | self.statuses.push(PathStatus { |
|
| 1603 | + | path: path.to_string(), |
|
| 1604 | + | status: ChangeStatus::ConflictEdited, |
|
| 1605 | + | }); |
|
| 1116 | 1606 | } |
|
| 1117 | 1607 | return Ok(()); |
|
| 1118 | 1608 | } |
|
| … | 2 unchanged lines |
||
| 1121 | 1611 | if e.kind == NodeKind::File && !wants_opaque { |
|
| 1122 | 1612 | // Opaque → text (spec §7.5). |
|
| 1123 | 1613 | if let Some(old) = mode_atom { |
|
| 1124 | - | self.tree_ops.push(TreeOp::SetMode { node, old, mode: Mode { exec: e.exec, opaque: false } }); |
|
| 1614 | + | self.tree_ops.push(TreeOp::SetMode { |
|
| 1615 | + | node, |
|
| 1616 | + | old, |
|
| 1617 | + | mode: Mode { |
|
| 1618 | + | exec: e.exec, |
|
| 1619 | + | opaque: false, |
|
| 1620 | + | }, |
|
| 1621 | + | }); |
|
| 1125 | 1622 | } |
|
| 1126 | - | self.edits.push(Edit::Replace { node, old: current, new: None }); |
|
| 1623 | + | self.edits.push(Edit::Replace { |
|
| 1624 | + | node, |
|
| 1625 | + | old: current, |
|
| 1626 | + | new: None, |
|
| 1627 | + | }); |
|
| 1127 | 1628 | if !e.content.is_empty() { |
|
| 1128 | 1629 | self.edits.push(Edit::Insert { |
|
| 1129 | 1630 | node, |
|
| … | 4 unchanged lines |
||
| 1134 | 1635 | } |
|
| 1135 | 1636 | } else { |
|
| 1136 | 1637 | let blob = self.blob_of(repo, path, e, nocompress)?; |
|
| 1137 | - | self.edits.push(Edit::Replace { node, old: current, new: Some(blob) }); |
|
| 1638 | + | self.edits.push(Edit::Replace { |
|
| 1639 | + | node, |
|
| 1640 | + | old: current, |
|
| 1641 | + | new: Some(blob), |
|
| 1642 | + | }); |
|
| 1138 | 1643 | } |
|
| 1139 | 1644 | status = Some(ChangeStatus::Modified); |
|
| 1140 | 1645 | } else if e.kind == NodeKind::File && e.exec != expected.mode.exec { |
|
| 1141 | 1646 | if let Some(old) = mode_atom { |
|
| 1142 | - | self.tree_ops.push(TreeOp::SetMode { node, old, mode: Mode { exec: e.exec, opaque: true } }); |
|
| 1647 | + | self.tree_ops.push(TreeOp::SetMode { |
|
| 1648 | + | node, |
|
| 1649 | + | old, |
|
| 1650 | + | mode: Mode { |
|
| 1651 | + | exec: e.exec, |
|
| 1652 | + | opaque: true, |
|
| 1653 | + | }, |
|
| 1654 | + | }); |
|
| 1143 | 1655 | status = Some(ChangeStatus::ModeChanged); |
|
| 1144 | 1656 | } |
|
| 1145 | 1657 | } |
|
| 1146 | 1658 | } |
|
| 1147 | 1659 | } |
|
| 1148 | 1660 | if let Some(s) = status { |
|
| 1149 | - | self.statuses.push(PathStatus { path: path.to_string(), status: s }); |
|
| 1661 | + | self.statuses.push(PathStatus { |
|
| 1662 | + | path: path.to_string(), |
|
| 1663 | + | status: s, |
|
| 1664 | + | }); |
|
| 1150 | 1665 | } |
|
| 1151 | 1666 | Ok(()) |
|
| 1152 | 1667 | } |
|
| … | 20 unchanged lines |
||
| 1173 | 1688 | nodes.push(*node); |
|
| 1174 | 1689 | opref(placement, &mut out); |
|
| 1175 | 1690 | } |
|
| 1176 | - | TreeOp::Move { node, old, parent, .. } => { |
|
| 1691 | + | TreeOp::Move { |
|
| 1692 | + | node, old, parent, .. |
|
| 1693 | + | } => { |
|
| 1177 | 1694 | nodes.push(*node); |
|
| 1178 | 1695 | nodes.push(*parent); |
|
| 1179 | 1696 | opref(old, &mut out); |
|
| … | 35 unchanged lines |
||
| 1215 | 1732 | } |
|
| 1216 | 1733 | ||
| 1217 | 1734 | /// Paths of the nodes a change adds on top of `base_snapshot`. |
|
| 1218 | - | pub fn added_node_paths(change: &Change, base_snapshot: &Snapshot) -> HashMap<String, (NodeId, NodeKind)> { |
|
| 1735 | + | pub fn added_node_paths( |
|
| 1736 | + | change: &Change, |
|
| 1737 | + | base_snapshot: &Snapshot, |
|
| 1738 | + | ) -> HashMap<String, (NodeId, NodeKind)> { |
|
| 1219 | 1739 | let by_node = base_snapshot.tree.by_node(); |
|
| 1220 | 1740 | let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new(); |
|
| 1221 | 1741 | for op in &change.tree_ops { |
|
| 1222 | - | if let TreeOp::AddNode { node, parent, name, kind, .. } = op { |
|
| 1742 | + | if let TreeOp::AddNode { |
|
| 1743 | + | node, |
|
| 1744 | + | parent, |
|
| 1745 | + | name, |
|
| 1746 | + | kind, |
|
| 1747 | + | .. |
|
| 1748 | + | } = op |
|
| 1749 | + | { |
|
| 1223 | 1750 | new_names.insert(*node, (*parent, name.clone(), *kind)); |
|
| 1224 | 1751 | } |
|
| 1225 | 1752 | } |
|
| … | 39 unchanged lines |
||
| 1265 | 1792 | let by_node = base_snapshot.tree.by_node(); |
|
| 1266 | 1793 | let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new(); |
|
| 1267 | 1794 | for op in &change.tree_ops { |
|
| 1268 | - | if let TreeOp::AddNode { node, parent, name, kind, .. } = op { |
|
| 1795 | + | if let TreeOp::AddNode { |
|
| 1796 | + | node, |
|
| 1797 | + | parent, |
|
| 1798 | + | name, |
|
| 1799 | + | kind, |
|
| 1800 | + | .. |
|
| 1801 | + | } = op |
|
| 1802 | + | { |
|
| 1269 | 1803 | new_names.insert(*node, (*parent, name.clone(), *kind)); |
|
| 1270 | 1804 | } |
|
| 1271 | 1805 | } |
|
| 1272 | - | let path_of = |n: &NodeId| -> String { path_of_node(n, &by_node, &new_names).unwrap_or_else(|| format!("<node {}>", n.short(8))) }; |
|
| 1806 | + | let path_of = |n: &NodeId| -> String { |
|
| 1807 | + | path_of_node(n, &by_node, &new_names).unwrap_or_else(|| format!("<node {}>", n.short(8))) |
|
| 1808 | + | }; |
|
| 1273 | 1809 | ||
| 1274 | 1810 | let mut out = String::new(); |
|
| 1275 | 1811 | for op in &change.tree_ops { |
|
| 1276 | 1812 | match op { |
|
| 1277 | - | TreeOp::AddNode { node, kind, mode, .. } => { |
|
| 1278 | - | out.push_str(&format!("A {} ({:?}{}{})\n", path_of(node), kind, if mode.exec { ", exec" } else { "" }, if mode.opaque { ", opaque" } else { "" })); |
|
| 1813 | + | TreeOp::AddNode { |
|
| 1814 | + | node, kind, mode, .. |
|
| 1815 | + | } => { |
|
| 1816 | + | out.push_str(&format!( |
|
| 1817 | + | "A {} ({:?}{}{})\n", |
|
| 1818 | + | path_of(node), |
|
| 1819 | + | kind, |
|
| 1820 | + | if mode.exec { ", exec" } else { "" }, |
|
| 1821 | + | if mode.opaque { ", opaque" } else { "" } |
|
| 1822 | + | )); |
|
| 1279 | 1823 | } |
|
| 1280 | 1824 | TreeOp::RemoveNode { node, .. } => out.push_str(&format!("D {}\n", path_of(node))), |
|
| 1281 | - | TreeOp::Move { node, name, .. } => out.push_str(&format!("R {} -> {}\n", path_of(node), name)), |
|
| 1825 | + | TreeOp::Move { node, name, .. } => { |
|
| 1826 | + | out.push_str(&format!("R {} -> {}\n", path_of(node), name)) |
|
| 1827 | + | } |
|
| 1282 | 1828 | TreeOp::SetMode { node, mode, .. } => { |
|
| 1283 | - | out.push_str(&format!("M {} mode: exec={} opaque={}\n", path_of(node), mode.exec, mode.opaque)); |
|
| 1829 | + | out.push_str(&format!( |
|
| 1830 | + | "M {} mode: exec={} opaque={}\n", |
|
| 1831 | + | path_of(node), |
|
| 1832 | + | mode.exec, |
|
| 1833 | + | mode.opaque |
|
| 1834 | + | )); |
|
| 1284 | 1835 | } |
|
| 1285 | 1836 | } |
|
| 1286 | 1837 | } |
|
| … | 17 unchanged lines |
||
| 1304 | 1855 | Edit::Delete { atoms, .. } => { |
|
| 1305 | 1856 | for r in atoms { |
|
| 1306 | 1857 | for i in r.from..r.to { |
|
| 1307 | - | let content = base.file(&n).and_then(|g| g.content(&(r.change, i))).unwrap_or(b"<unknown>\n"); |
|
| 1858 | + | let content = base |
|
| 1859 | + | .file(&n) |
|
| 1860 | + | .and_then(|g| g.content(&(r.change, i))) |
|
| 1861 | + | .unwrap_or(b"<unknown>\n"); |
|
| 1308 | 1862 | out.push('-'); |
|
| 1309 | 1863 | out.push_str(&String::from_utf8_lossy(content)); |
|
| 1310 | 1864 | if !content.ends_with(b"\n") { |
|
| … | 20 unchanged lines |
||
| 1331 | 1885 | const RENAME_THRESHOLD: u32 = 50; |
|
| 1332 | 1886 | const RENAME_LIMIT: usize = 10_000; |
|
| 1333 | 1887 | ||
| 1334 | - | fn detect_renames(base_snapshot: &Snapshot, fs_entries: &BTreeMap<String, FsEntry>, in_scope: impl Fn(&str) -> bool) -> Vec<(String, String)> { |
|
| 1888 | + | fn detect_renames( |
|
| 1889 | + | base_snapshot: &Snapshot, |
|
| 1890 | + | fs_entries: &BTreeMap<String, FsEntry>, |
|
| 1891 | + | in_scope: impl Fn(&str) -> bool, |
|
| 1892 | + | ) -> Vec<(String, String)> { |
|
| 1335 | 1893 | let removed: Vec<(&String, &crate::state::FileState)> = base_snapshot |
|
| 1336 | 1894 | .files |
|
| 1337 | 1895 | .iter() |
|
| 1338 | - | .filter(|(p, f)| f.kind != NodeKind::Dir && !fs_entries.contains_key(*p) && in_scope(p) && !f.is_conflicted()) |
|
| 1896 | + | .filter(|(p, f)| { |
|
| 1897 | + | f.kind != NodeKind::Dir |
|
| 1898 | + | && !fs_entries.contains_key(*p) |
|
| 1899 | + | && in_scope(p) |
|
| 1900 | + | && !f.is_conflicted() |
|
| 1901 | + | }) |
|
| 1902 | + | .collect(); |
|
| 1903 | + | let added: Vec<(&String, &FsEntry)> = fs_entries |
|
| 1904 | + | .iter() |
|
| 1905 | + | .filter(|(p, e)| e.kind != NodeKind::Dir && !base_snapshot.files.contains_key(*p)) |
|
| 1339 | 1906 | .collect(); |
|
| 1340 | - | let added: Vec<(&String, &FsEntry)> = fs_entries.iter().filter(|(p, e)| e.kind != NodeKind::Dir && !base_snapshot.files.contains_key(*p)).collect(); |
|
| 1341 | 1907 | if removed.is_empty() || added.is_empty() { |
|
| 1342 | 1908 | return vec![]; |
|
| 1343 | 1909 | } |
|
| … | 18 unchanged lines |
||
| 1362 | 1928 | let score = match (&old, old_blob) { |
|
| 1363 | 1929 | (Some(o), _) => { |
|
| 1364 | 1930 | if exact_only { |
|
| 1365 | - | if *o == e.content { 100 } else { 0 } |
|
| 1931 | + | if *o == e.content { |
|
| 1932 | + | 100 |
|
| 1933 | + | } else { |
|
| 1934 | + | 0 |
|
| 1935 | + | } |
|
| 1366 | 1936 | } else { |
|
| 1367 | 1937 | similarity(o, &e.content) |
|
| 1368 | 1938 | } |
|
| … | 6 unchanged lines |
||
| 1375 | 1945 | } |
|
| 1376 | 1946 | } |
|
| 1377 | 1947 | } |
|
| 1378 | - | scored.sort_by(|a, b| b.0.cmp(&a.0).then(removed[a.1].0.cmp(removed[b.1].0)).then(added[a.2].0.cmp(added[b.2].0))); |
|
| 1948 | + | scored.sort_by(|a, b| { |
|
| 1949 | + | b.0.cmp(&a.0) |
|
| 1950 | + | .then(removed[a.1].0.cmp(removed[b.1].0)) |
|
| 1951 | + | .then(added[a.2].0.cmp(added[b.2].0)) |
|
| 1952 | + | }); |
|
| 1379 | 1953 | let mut used_r = vec![false; removed.len()]; |
|
| 1380 | 1954 | let mut used_a = vec![false; added.len()]; |
|
| 1381 | 1955 | let mut out = Vec::new(); |
|
| … | 32 unchanged lines |
||
| 1414 | 1988 | } |
|
| 1415 | 1989 | (shared * 100 / a.len().max(b.len())) as u32 |
|
| 1416 | 1990 | } |
|
| 1417 | - |
modified crates/oxy-core/src/repo/subrepos.rs +98 −19
| … | 27 unchanged lines |
||
| 28 | 28 | } |
|
| 29 | 29 | ||
| 30 | 30 | fn git_out(dir: &Path, args: &[&str]) -> Result<String> { |
|
| 31 | - | let out = Command::new("git").arg("-C").arg(dir).args(args).output().map_err(|e| RepoError::Msg(format!("running git: {e}")))?; |
|
| 31 | + | let out = Command::new("git") |
|
| 32 | + | .arg("-C") |
|
| 33 | + | .arg(dir) |
|
| 34 | + | .args(args) |
|
| 35 | + | .output() |
|
| 36 | + | .map_err(|e| RepoError::Msg(format!("running git: {e}")))?; |
|
| 32 | 37 | if !out.status.success() { |
|
| 33 | - | return fail(format!("git {} in {}: {}", args.join(" "), dir.display(), String::from_utf8_lossy(&out.stderr).trim())); |
|
| 38 | + | return fail(format!( |
|
| 39 | + | "git {} in {}: {}", |
|
| 40 | + | args.join(" "), |
|
| 41 | + | dir.display(), |
|
| 42 | + | String::from_utf8_lossy(&out.stderr).trim() |
|
| 43 | + | )); |
|
| 34 | 44 | } |
|
| 35 | 45 | Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) |
|
| 36 | 46 | } |
|
| … | 3 unchanged lines |
||
| 40 | 50 | pub(crate) fn read_pin(dir: &Path, bookmark_hint: Option<&str>) -> Result<Pin> { |
|
| 41 | 51 | if dir.join(OXY_DIR).exists() { |
|
| 42 | 52 | let nested = Repo::open(dir)?; |
|
| 43 | - | return Ok(Pin::Oxy { repo: nested.id()?, heads: nested.bookmark_heads()?, bookmark: nested.config.bookmark.clone() }); |
|
| 53 | + | return Ok(Pin::Oxy { |
|
| 54 | + | repo: nested.id()?, |
|
| 55 | + | heads: nested.bookmark_heads()?, |
|
| 56 | + | bookmark: nested.config.bookmark.clone(), |
|
| 57 | + | }); |
|
| 44 | 58 | } |
|
| 45 | 59 | if dir.join(".git").exists() { |
|
| 46 | 60 | let sha = git_out(dir, &["rev-parse", "HEAD"])?; |
|
| 47 | - | return Ok(Pin::Git { sha, branch: bookmark_hint.unwrap_or("HEAD").to_string() }); |
|
| 61 | + | return Ok(Pin::Git { |
|
| 62 | + | sha, |
|
| 63 | + | branch: bookmark_hint.unwrap_or("HEAD").to_string(), |
|
| 64 | + | }); |
|
| 48 | 65 | } |
|
| 49 | 66 | fail(format!("{} is not a repository", dir.display())) |
|
| 50 | 67 | } |
|
| … | 11 unchanged lines |
||
| 62 | 79 | /// Whether `url` names an Oxygen repository (else it is taken to be Git). |
|
| 63 | 80 | fn is_oxy_url(url: &str) -> bool { |
|
| 64 | 81 | if url.starts_with("http://") || url.starts_with("https://") { |
|
| 65 | - | return crate::protocol::open_remote(url, &RemoteOptions::default()).and_then(|r| r.repo_id()).is_ok(); |
|
| 82 | + | return crate::protocol::open_remote(url, &RemoteOptions::default()) |
|
| 83 | + | .and_then(|r| r.repo_id()) |
|
| 84 | + | .is_ok(); |
|
| 66 | 85 | } |
|
| 67 | 86 | let p = Path::new(url.strip_prefix("file://").unwrap_or(url)); |
|
| 68 | 87 | p.join(OXY_DIR).exists() |
|
| … | 1 unchanged lines |
||
| 70 | 89 | ||
| 71 | 90 | impl Repo { |
|
| 72 | 91 | pub(crate) fn modules_on_disk(&self) -> Modules { |
|
| 73 | - | fs::read_to_string(self.root.join(".oxymodules")).map(|t| Modules::parse(&t)).unwrap_or_default() |
|
| 92 | + | fs::read_to_string(self.root.join(".oxymodules")) |
|
| 93 | + | .map(|t| Modules::parse(&t)) |
|
| 94 | + | .unwrap_or_default() |
|
| 74 | 95 | } |
|
| 75 | 96 | ||
| 76 | 97 | fn write_modules(&self, m: &Modules) -> Result<()> { |
|
| … | 15 unchanged lines |
||
| 92 | 113 | if !init { |
|
| 93 | 114 | return Ok(SubrepoSync::Missing); |
|
| 94 | 115 | } |
|
| 95 | - | let Some(m) = &module else { return Ok(SubrepoSync::Failed("no .oxymodules entry to clone from".into())) }; |
|
| 116 | + | let Some(m) = &module else { |
|
| 117 | + | return Ok(SubrepoSync::Failed( |
|
| 118 | + | "no .oxymodules entry to clone from".into(), |
|
| 119 | + | )); |
|
| 120 | + | }; |
|
| 96 | 121 | if let Some(parent) = dir.parent() { |
|
| 97 | 122 | fs::create_dir_all(parent)?; |
|
| 98 | 123 | } |
|
| … | 5 unchanged lines |
||
| 104 | 129 | .arg(&dir) |
|
| 105 | 130 | .status() |
|
| 106 | 131 | .map_err(|e| RepoError::Msg(e.to_string())) |
|
| 107 | - | .and_then(|s| if s.success() { Ok(()) } else { fail(format!("git clone {} failed", m.url)) }), |
|
| 132 | + | .and_then(|s| { |
|
| 133 | + | if s.success() { |
|
| 134 | + | Ok(()) |
|
| 135 | + | } else { |
|
| 136 | + | fail(format!("git clone {} failed", m.url)) |
|
| 137 | + | } |
|
| 138 | + | }), |
|
| 108 | 139 | }; |
|
| 109 | 140 | if let Err(e) = r { |
|
| 110 | 141 | return Ok(SubrepoSync::Failed(format!("cloning {}: {e}", m.url))); |
|
| … | 11 unchanged lines |
||
| 122 | 153 | _ => false, |
|
| 123 | 154 | }; |
|
| 124 | 155 | if same { |
|
| 125 | - | return Ok(if cloned { SubrepoSync::Updated } else { SubrepoSync::UpToDate }); |
|
| 156 | + | return Ok(if cloned { |
|
| 157 | + | SubrepoSync::Updated |
|
| 158 | + | } else { |
|
| 159 | + | SubrepoSync::UpToDate |
|
| 160 | + | }); |
|
| 126 | 161 | } |
|
| 127 | 162 | if nested_dirty(&dir)? { |
|
| 128 | 163 | return Ok(SubrepoSync::Dirty); |
|
| … | 4 unchanged lines |
||
| 133 | 168 | if nested.load_state(heads).is_err() { |
|
| 134 | 169 | // The pinned changes are not here yet: fetch from origin. |
|
| 135 | 170 | if let Err(e) = nested.fetch("origin") { |
|
| 136 | - | return Ok(SubrepoSync::Failed(format!("fetching the pinned changes: {e}"))); |
|
| 171 | + | return Ok(SubrepoSync::Failed(format!( |
|
| 172 | + | "fetching the pinned changes: {e}" |
|
| 173 | + | ))); |
|
| 137 | 174 | } |
|
| 138 | 175 | } |
|
| 139 | 176 | match nested.checkout(heads, false, "oxy submodule update") { |
|
| … | 28 unchanged lines |
||
| 168 | 205 | if f.kind != NodeKind::Subrepo { |
|
| 169 | 206 | continue; |
|
| 170 | 207 | } |
|
| 171 | - | let FileContent::Opaque { blobs } = &f.content else { continue }; |
|
| 208 | + | let FileContent::Opaque { blobs } = &f.content else { |
|
| 209 | + | continue; |
|
| 210 | + | }; |
|
| 172 | 211 | let Some(b) = blobs.first() else { continue }; |
|
| 173 | 212 | let pin = Pin::decode(&self.read_blob(b)?)?; |
|
| 174 | 213 | let dir = self.root.join(path); |
|
| 175 | 214 | let present = dir.join(OXY_DIR).exists() || dir.join(".git").exists(); |
|
| 176 | - | let actual = if present { read_pin(&dir, modules.entries.get(path).and_then(|m| m.bookmark.as_deref())).ok() } else { None }; |
|
| 215 | + | let actual = if present { |
|
| 216 | + | read_pin( |
|
| 217 | + | &dir, |
|
| 218 | + | modules |
|
| 219 | + | .entries |
|
| 220 | + | .get(path) |
|
| 221 | + | .and_then(|m| m.bookmark.as_deref()), |
|
| 222 | + | ) |
|
| 223 | + | .ok() |
|
| 224 | + | } else { |
|
| 225 | + | None |
|
| 226 | + | }; |
|
| 177 | 227 | let dirty = present && nested_dirty(&dir).unwrap_or(false); |
|
| 178 | - | out.push(SubrepoInfo { path: path.clone(), pin, url: modules.entries.get(path).map(|m| m.url.clone()), actual, dirty }); |
|
| 228 | + | out.push(SubrepoInfo { |
|
| 229 | + | path: path.clone(), |
|
| 230 | + | pin, |
|
| 231 | + | url: modules.entries.get(path).map(|m| m.url.clone()), |
|
| 232 | + | actual, |
|
| 233 | + | dirty, |
|
| 234 | + | }); |
|
| 179 | 235 | } |
|
| 180 | 236 | Ok(out) |
|
| 181 | 237 | } |
|
| … | 2 unchanged lines |
||
| 184 | 240 | /// pin the next snapshot will record. |
|
| 185 | 241 | pub fn submodule_add(&self, url: &str, path: &str, bookmark: Option<&str>) -> Result<Pin> { |
|
| 186 | 242 | let path = path.trim_end_matches('/').to_string(); |
|
| 187 | - | if path.is_empty() || path.starts_with('/') || path.split('/').any(|c| c == ".." || c == ".") { |
|
| 243 | + | if path.is_empty() |
|
| 244 | + | || path.starts_with('/') |
|
| 245 | + | || path.split('/').any(|c| c == ".." || c == ".") |
|
| 246 | + | { |
|
| 188 | 247 | return fail(format!("{path:?} is not a valid subrepo path")); |
|
| 189 | 248 | } |
|
| 190 | 249 | let dir = self.root.join(&path); |
|
| … | 10 unchanged lines |
||
| 201 | 260 | if let Some(b) = bookmark { |
|
| 202 | 261 | cmd.args(["--branch", b]); |
|
| 203 | 262 | } |
|
| 204 | - | let status = cmd.arg(url).arg(&dir).status().map_err(|e| RepoError::Msg(format!("running git: {e}")))?; |
|
| 263 | + | let status = cmd |
|
| 264 | + | .arg(url) |
|
| 265 | + | .arg(&dir) |
|
| 266 | + | .status() |
|
| 267 | + | .map_err(|e| RepoError::Msg(format!("running git: {e}")))?; |
|
| 205 | 268 | if !status.success() { |
|
| 206 | 269 | return fail(format!("git clone {url} failed")); |
|
| 207 | 270 | } |
|
| … | 5 unchanged lines |
||
| 213 | 276 | } |
|
| 214 | 277 | } |
|
| 215 | 278 | let mut modules = self.modules_on_disk(); |
|
| 216 | - | modules.entries.insert(path.clone(), Module { url: url.to_string(), bookmark: bookmark.map(str::to_string), git }); |
|
| 279 | + | modules.entries.insert( |
|
| 280 | + | path.clone(), |
|
| 281 | + | Module { |
|
| 282 | + | url: url.to_string(), |
|
| 283 | + | bookmark: bookmark.map(str::to_string), |
|
| 284 | + | git, |
|
| 285 | + | }, |
|
| 286 | + | ); |
|
| 217 | 287 | self.write_modules(&modules)?; |
|
| 218 | 288 | read_pin(&dir, bookmark) |
|
| 219 | 289 | } |
|
| 220 | 290 | ||
| 221 | 291 | /// Bring nested repositories to their pins (`paths` empty = all). |
|
| 222 | - | pub fn submodule_update(&self, init: bool, paths: &[String]) -> Result<Vec<(String, SubrepoSync)>> { |
|
| 292 | + | pub fn submodule_update( |
|
| 293 | + | &self, |
|
| 294 | + | init: bool, |
|
| 295 | + | paths: &[String], |
|
| 296 | + | ) -> Result<Vec<(String, SubrepoSync)>> { |
|
| 223 | 297 | let mut out = Vec::new(); |
|
| 224 | 298 | for s in self.subrepos()? { |
|
| 225 | 299 | if !paths.is_empty() && !paths.iter().any(|p| p.trim_end_matches('/') == s.path) { |
|
| … | 13 unchanged lines |
||
| 239 | 313 | pub fn submodule_remove(&self, path: &str) -> Result<()> { |
|
| 240 | 314 | let path = path.trim_end_matches('/').to_string(); |
|
| 241 | 315 | let mut modules = self.modules_on_disk(); |
|
| 242 | - | let known = modules.entries.remove(&path).is_some() || self.subrepos()?.iter().any(|s| s.path == path); |
|
| 316 | + | let known = modules.entries.remove(&path).is_some() |
|
| 317 | + | || self.subrepos()?.iter().any(|s| s.path == path); |
|
| 243 | 318 | if !known { |
|
| 244 | 319 | return fail(format!("{path} is not a subrepo")); |
|
| 245 | 320 | } |
|
| … | 20 unchanged lines |
||
| 266 | 341 | } |
|
| 267 | 342 | ||
| 268 | 343 | pub(crate) fn clear_removed_subrepo(&self, path: &str) { |
|
| 269 | - | let _ = fs::remove_file(self.oxy.join("subrepos-removed").join(path.replace('/', "%2F"))); |
|
| 344 | + | let _ = fs::remove_file( |
|
| 345 | + | self.oxy |
|
| 346 | + | .join("subrepos-removed") |
|
| 347 | + | .join(path.replace('/', "%2F")), |
|
| 348 | + | ); |
|
| 270 | 349 | } |
|
| 271 | 350 | } |
modified crates/oxy-core/src/repo/sync.rs +500 −113
| … | 31 unchanged lines |
||
| 32 | 32 | pub fn latest_ref_attest(&self, bookmark: &str) -> Result<Option<(ObjectRef, SignedAttest)>> { |
|
| 33 | 33 | match fs::read_to_string(self.ref_attest_path(bookmark)) { |
|
| 34 | 34 | Ok(h) => { |
|
| 35 | - | let r = ObjectRef::from_hex(h.trim()).ok_or_else(|| RepoError::Msg("corrupt attestation pointer".into()))?; |
|
| 36 | - | Ok(Some((r, SignedAttest { attest: self.store.get_attest(&r)?, sigs: self.store.signatures(&r)? }))) |
|
| 35 | + | let r = ObjectRef::from_hex(h.trim()) |
|
| 36 | + | .ok_or_else(|| RepoError::Msg("corrupt attestation pointer".into()))?; |
|
| 37 | + | Ok(Some(( |
|
| 38 | + | r, |
|
| 39 | + | SignedAttest { |
|
| 40 | + | attest: self.store.get_attest(&r)?, |
|
| 41 | + | sigs: self.store.signatures(&r)?, |
|
| 42 | + | }, |
|
| 43 | + | ))) |
|
| 37 | 44 | } |
|
| 38 | 45 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), |
|
| 39 | 46 | Err(e) => Err(e.into()), |
|
| … | 15 unchanged lines |
||
| 55 | 62 | _ => None, |
|
| 56 | 63 | }); |
|
| 57 | 64 | let attestation = latest.filter(|a| matches!(&a.attest.body, AttestBody::Ref { new_state, .. } if new_state == state)); |
|
| 58 | - | out.push(RemoteRef { name: name.clone(), state: *state, heads: self.store.get_state(state)?.heads, attestation, last_serial }); |
|
| 65 | + | out.push(RemoteRef { |
|
| 66 | + | name: name.clone(), |
|
| 67 | + | state: *state, |
|
| 68 | + | heads: self.store.get_state(state)?.heads, |
|
| 69 | + | attestation, |
|
| 70 | + | last_serial, |
|
| 71 | + | }); |
|
| 59 | 72 | } |
|
| 60 | 73 | let policy = self.current_policy()?.map(|(_, sp)| sp); |
|
| 61 | - | let history = self.policy_history()?.into_iter().filter(|sp| policy.as_ref().is_some_and(|p| sp.policy.serial < p.policy.serial)).collect(); |
|
| 62 | - | Ok(RefsResponse { refs: out, policy, tags: self.tag_refs()?, history }) |
|
| 74 | + | let history = self |
|
| 75 | + | .policy_history()? |
|
| 76 | + | .into_iter() |
|
| 77 | + | .filter(|sp| { |
|
| 78 | + | policy |
|
| 79 | + | .as_ref() |
|
| 80 | + | .is_some_and(|p| sp.policy.serial < p.policy.serial) |
|
| 81 | + | }) |
|
| 82 | + | .collect(); |
|
| 83 | + | Ok(RefsResponse { |
|
| 84 | + | refs: out, |
|
| 85 | + | policy, |
|
| 86 | + | tags: self.tag_refs()?, |
|
| 87 | + | history, |
|
| 88 | + | }) |
|
| 63 | 89 | } |
|
| 64 | 90 | ||
| 65 | 91 | /// Every change reachable from `heads` that is in the store. |
|
| … | 41 unchanged lines |
||
| 107 | 133 | ||
| 108 | 134 | /// The shallow boundary: dependencies this repository does not hold (spec §17). |
|
| 109 | 135 | pub fn boundary(&self) -> BTreeSet<ChangeHash> { |
|
| 110 | - | fs::read_to_string(self.shallow_path()).map(|t| t.lines().filter_map(ObjectRef::from_hex).collect()).unwrap_or_default() |
|
| 136 | + | fs::read_to_string(self.shallow_path()) |
|
| 137 | + | .map(|t| t.lines().filter_map(ObjectRef::from_hex).collect()) |
|
| 138 | + | .unwrap_or_default() |
|
| 111 | 139 | } |
|
| 112 | 140 | ||
| 113 | 141 | pub fn is_shallow(&self) -> bool { |
|
| … | 5 unchanged lines |
||
| 119 | 147 | let _ = fs::remove_file(self.shallow_path()); |
|
| 120 | 148 | return Ok(()); |
|
| 121 | 149 | } |
|
| 122 | - | let text: String = boundary.iter().map(|h| format!("{}\n", h.to_hex())).collect(); |
|
| 150 | + | let text: String = boundary |
|
| 151 | + | .iter() |
|
| 152 | + | .map(|h| format!("{}\n", h.to_hex())) |
|
| 153 | + | .collect(); |
|
| 123 | 154 | write_atomic(&self.shallow_path(), text.as_bytes()) |
|
| 124 | 155 | } |
|
| 125 | 156 | ||
| … | 91 unchanged lines |
||
| 217 | 248 | ||
| 218 | 249 | /// Like `bundle_for`, leaving out blobs larger than `max_blob` bytes |
|
| 219 | 250 | /// (and their chunks) so the receiver fetches them lazily (spec §13.5). |
|
| 220 | - | pub fn bundle_for_filtered(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> { |
|
| 251 | + | pub fn bundle_for_filtered( |
|
| 252 | + | &self, |
|
| 253 | + | changes: &[ChangeHash], |
|
| 254 | + | max_blob: Option<u64>, |
|
| 255 | + | ) -> Result<Bundle> { |
|
| 221 | 256 | let mut bundle = Bundle::default(); |
|
| 222 | 257 | let mut objects: BTreeSet<ObjectRef> = BTreeSet::new(); |
|
| 223 | 258 | let mut seals: BTreeSet<ObjectRef> = BTreeSet::new(); |
|
| … | 13 unchanged lines |
||
| 237 | 272 | if let Edit::Replace { old, new, .. } = e { |
|
| 238 | 273 | for b in [old, new].into_iter().flatten() { |
|
| 239 | 274 | if self.store.has(b) { |
|
| 240 | - | if let Ok(blob) = Blob::decode(&self.store.get_typed(b, ObjectType::Blob)?) { |
|
| 275 | + | if let Ok(blob) = |
|
| 276 | + | Blob::decode(&self.store.get_typed(b, ObjectType::Blob)?) |
|
| 277 | + | { |
|
| 241 | 278 | if max_blob.is_some_and(|m| blob.length > m) { |
|
| 242 | 279 | continue; |
|
| 243 | 280 | } |
|
| … | 36 unchanged lines |
||
| 280 | 317 | self.import_bundle_inner(bundle, strict, true) |
|
| 281 | 318 | } |
|
| 282 | 319 | ||
| 283 | - | fn import_bundle_inner(&self, bundle: &Bundle, strict: bool, shallow: bool) -> Result<Vec<ChangeHash>> { |
|
| 320 | + | fn import_bundle_inner( |
|
| 321 | + | &self, |
|
| 322 | + | bundle: &Bundle, |
|
| 323 | + | strict: bool, |
|
| 324 | + | shallow: bool, |
|
| 325 | + | ) -> Result<Vec<ChangeHash>> { |
|
| 284 | 326 | let batch = self.store.begin_batch()?; |
|
| 285 | 327 | let result = self.import_bundle_batched(bundle, strict, shallow); |
|
| 286 | 328 | batch.finish()?; |
|
| 287 | 329 | result |
|
| 288 | 330 | } |
|
| 289 | 331 | ||
| 290 | - | fn import_bundle_batched(&self, bundle: &Bundle, strict: bool, shallow: bool) -> Result<Vec<ChangeHash>> { |
|
| 332 | + | fn import_bundle_batched( |
|
| 333 | + | &self, |
|
| 334 | + | bundle: &Bundle, |
|
| 335 | + | strict: bool, |
|
| 336 | + | shallow: bool, |
|
| 337 | + | ) -> Result<Vec<ChangeHash>> { |
|
| 291 | 338 | for (ty, body) in &bundle.objects { |
|
| 292 | 339 | match ty { |
|
| 293 | - | ObjectType::Chunk | ObjectType::Blob | ObjectType::Identity | ObjectType::State | ObjectType::Checkpoint => { |
|
| 340 | + | ObjectType::Chunk |
|
| 341 | + | | ObjectType::Blob |
|
| 342 | + | | ObjectType::Identity |
|
| 343 | + | | ObjectType::State |
|
| 344 | + | | ObjectType::Checkpoint => { |
|
| 294 | 345 | self.store.put(*ty, body)?; |
|
| 295 | 346 | } |
|
| 296 | 347 | other => return fail(format!("bundle may not carry {} objects", other.name())), |
|
| … | 16 unchanged lines |
||
| 313 | 364 | for (c, sigs) in &bundle.changes { |
|
| 314 | 365 | let h = c.hash(); |
|
| 315 | 366 | for d in &c.deps { |
|
| 316 | - | if !self.store.has(d) && !incoming.contains(d) && !shallow && !boundary.contains(d) { |
|
| 317 | - | return fail(format!("change {} depends on {}, which is neither stored nor in the bundle", h.short(12), d.short(12))); |
|
| 367 | + | if !self.store.has(d) && !incoming.contains(d) && !shallow && !boundary.contains(d) |
|
| 368 | + | { |
|
| 369 | + | return fail(format!( |
|
| 370 | + | "change {} depends on {}, which is neither stored nor in the bundle", |
|
| 371 | + | h.short(12), |
|
| 372 | + | d.short(12) |
|
| 373 | + | )); |
|
| 318 | 374 | } |
|
| 319 | 375 | } |
|
| 320 | 376 | self.store.put_change(c)?; |
|
| … | 5 unchanged lines |
||
| 326 | 382 | if strict { |
|
| 327 | 383 | for h in &imported { |
|
| 328 | 384 | if !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. }) { |
|
| 329 | - | return fail(format!("change {} is not authenticated by its author", h.short(12))); |
|
| 385 | + | return fail(format!( |
|
| 386 | + | "change {} is not authenticated by its author", |
|
| 387 | + | h.short(12) |
|
| 388 | + | )); |
|
| 330 | 389 | } |
|
| 331 | 390 | } |
|
| 332 | 391 | } |
|
| … | 3 unchanged lines |
||
| 336 | 395 | /// Verify a reference attestation against an update it claims to describe. |
|
| 337 | 396 | /// `allow_equal` lets a client re-read the attestation it already has; a |
|
| 338 | 397 | /// server receiving a push requires a strictly greater serial. |
|
| 339 | - | fn check_ref_attest(&self, sa: &SignedAttest, name: &str, old_state: &ObjectRef, new_state: &ObjectRef, last_serial: Option<u64>, allow_equal: bool) -> std::result::Result<u64, String> { |
|
| 340 | - | let AttestBody::Ref { name: n, old_state: o, new_state: nw, serial } = &sa.attest.body else { |
|
| 398 | + | fn check_ref_attest( |
|
| 399 | + | &self, |
|
| 400 | + | sa: &SignedAttest, |
|
| 401 | + | name: &str, |
|
| 402 | + | old_state: &ObjectRef, |
|
| 403 | + | new_state: &ObjectRef, |
|
| 404 | + | last_serial: Option<u64>, |
|
| 405 | + | allow_equal: bool, |
|
| 406 | + | ) -> std::result::Result<u64, String> { |
|
| 407 | + | let AttestBody::Ref { |
|
| 408 | + | name: n, |
|
| 409 | + | old_state: o, |
|
| 410 | + | new_state: nw, |
|
| 411 | + | serial, |
|
| 412 | + | } = &sa.attest.body |
|
| 413 | + | else { |
|
| 341 | 414 | return Err("attestation is not a ref attestation".into()); |
|
| 342 | 415 | }; |
|
| 343 | 416 | if n != name || o != old_state || nw != new_state { |
|
| … | 4 unchanged lines |
||
| 348 | 421 | } |
|
| 349 | 422 | if let Some(last) = last_serial { |
|
| 350 | 423 | if *serial < last || (*serial == last && !allow_equal) { |
|
| 351 | - | return Err(format!("attestation serial {serial} is not greater than the last seen {last}")); |
|
| 424 | + | return Err(format!( |
|
| 425 | + | "attestation serial {serial} is not greater than the last seen {last}" |
|
| 426 | + | )); |
|
| 352 | 427 | } |
|
| 353 | 428 | } |
|
| 354 | 429 | let r = sa.attest.hash(); |
|
| 355 | 430 | let sig = sa.sigs.first().ok_or("attestation carries no signature")?; |
|
| 356 | - | let id = self.store.get_identity(&sig.key).map_err(|_| format!("unknown key {}", sig.key.short(12)))?; |
|
| 431 | + | let id = self |
|
| 432 | + | .store |
|
| 433 | + | .get_identity(&sig.key) |
|
| 434 | + | .map_err(|_| format!("unknown key {}", sig.key.short(12)))?; |
|
| 357 | 435 | verify_signature(&id, sig, Domain::Attest, &r).map_err(|e| e.to_string())?; |
|
| 358 | 436 | Ok(*serial) |
|
| 359 | 437 | } |
|
| … | 3 unchanged lines |
||
| 363 | 441 | /// attestations that are present must still be valid. With a policy, |
|
| 364 | 442 | /// every change and update is checked against it. A newer policy in the |
|
| 365 | 443 | /// push is applied first when it is signed by the current roots. |
|
| 366 | - | pub fn receive_push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>, tags: Vec<crate::protocol::TagRef>) -> Result<Vec<RefResult>> { |
|
| 444 | + | pub fn receive_push( |
|
| 445 | + | &self, |
|
| 446 | + | bundle: Bundle, |
|
| 447 | + | updates: Vec<RefUpdate>, |
|
| 448 | + | policy: Option<SignedPolicy>, |
|
| 449 | + | tags: Vec<crate::protocol::TagRef>, |
|
| 450 | + | ) -> Result<Vec<RefResult>> { |
|
| 367 | 451 | let mut results = Vec::new(); |
|
| 368 | 452 | if let Some(sp) = policy { |
|
| 369 | 453 | match self.adopt_policy_chain(&bundle.policies, &sp, "push received") { |
|
| 370 | 454 | Ok(()) => { |
|
| 371 | - | results.push(RefResult { name: "policy".into(), ok: true, reason: format!("serial {}", sp.policy.serial) }); |
|
| 455 | + | results.push(RefResult { |
|
| 456 | + | name: "policy".into(), |
|
| 457 | + | ok: true, |
|
| 458 | + | reason: format!("serial {}", sp.policy.serial), |
|
| 459 | + | }); |
|
| 372 | 460 | } |
|
| 373 | 461 | Err(e) => { |
|
| 374 | - | results.push(RefResult { name: "policy".into(), ok: false, reason: e }); |
|
| 462 | + | results.push(RefResult { |
|
| 463 | + | name: "policy".into(), |
|
| 464 | + | ok: false, |
|
| 465 | + | reason: e, |
|
| 466 | + | }); |
|
| 375 | 467 | for u in updates { |
|
| 376 | - | results.push(RefResult { name: u.name, ok: false, reason: "policy update was rejected".into() }); |
|
| 468 | + | results.push(RefResult { |
|
| 469 | + | name: u.name, |
|
| 470 | + | ok: false, |
|
| 471 | + | reason: "policy update was rejected".into(), |
|
| 472 | + | }); |
|
| 377 | 473 | } |
|
| 378 | 474 | return Ok(results); |
|
| 379 | 475 | } |
|
| … | 2 unchanged lines |
||
| 382 | 478 | let policy = self.current_policy()?.map(|(_, sp)| sp.policy); |
|
| 383 | 479 | if let Err(e) = self.import_bundle(&bundle, policy.is_some()) { |
|
| 384 | 480 | for u in updates { |
|
| 385 | - | results.push(RefResult { name: u.name, ok: false, reason: e.to_string() }); |
|
| 481 | + | results.push(RefResult { |
|
| 482 | + | name: u.name, |
|
| 483 | + | ok: false, |
|
| 484 | + | reason: e.to_string(), |
|
| 485 | + | }); |
|
| 386 | 486 | } |
|
| 387 | 487 | return Ok(results); |
|
| 388 | 488 | } |
|
| … | 9 unchanged lines |
||
| 398 | 498 | if ok { |
|
| 399 | 499 | names.push(u.name.clone()); |
|
| 400 | 500 | } |
|
| 401 | - | results.push(RefResult { name: u.name, ok, reason }); |
|
| 501 | + | results.push(RefResult { |
|
| 502 | + | name: u.name, |
|
| 503 | + | ok, |
|
| 504 | + | reason, |
|
| 505 | + | }); |
|
| 402 | 506 | } |
|
| 403 | 507 | for t in &tags { |
|
| 404 | 508 | let name = format!("tags/{}", t.name); |
|
| 405 | 509 | match self.adopt_tag(&mut view, t, policy.as_ref()) { |
|
| 406 | 510 | Ok(()) => { |
|
| 407 | 511 | names.push(name.clone()); |
|
| 408 | - | results.push(RefResult { name, ok: true, reason: String::new() }); |
|
| 512 | + | results.push(RefResult { |
|
| 513 | + | name, |
|
| 514 | + | ok: true, |
|
| 515 | + | reason: String::new(), |
|
| 516 | + | }); |
|
| 409 | 517 | } |
|
| 410 | - | Err(e) => results.push(RefResult { name, ok: false, reason: e }), |
|
| 518 | + | Err(e) => results.push(RefResult { |
|
| 519 | + | name, |
|
| 520 | + | ok: false, |
|
| 521 | + | reason: e, |
|
| 522 | + | }), |
|
| 411 | 523 | } |
|
| 412 | 524 | } |
|
| 413 | 525 | if !names.is_empty() { |
|
| 414 | - | self.record_op(&[op_ref], &format!("push received: {}", names.join(" ")), view)?; |
|
| 526 | + | self.record_op( |
|
| 527 | + | &[op_ref], |
|
| 528 | + | &format!("push received: {}", names.join(" ")), |
|
| 529 | + | view, |
|
| 530 | + | )?; |
|
| 415 | 531 | } |
|
| 416 | 532 | Ok(results) |
|
| 417 | 533 | } |
|
| 418 | 534 | ||
| 419 | - | fn apply_update(&self, view: &mut View, u: &RefUpdate, policy: Option<&crate::policy::Policy>) -> std::result::Result<(), String> { |
|
| 535 | + | fn apply_update( |
|
| 536 | + | &self, |
|
| 537 | + | view: &mut View, |
|
| 538 | + | u: &RefUpdate, |
|
| 539 | + | policy: Option<&crate::policy::Policy>, |
|
| 540 | + | ) -> std::result::Result<(), String> { |
|
| 420 | 541 | if u.name == "policy" { |
|
| 421 | - | return Err("the policy reference is updated by sending a policy, not a bookmark update".into()); |
|
| 542 | + | return Err( |
|
| 543 | + | "the policy reference is updated by sending a policy, not a bookmark update".into(), |
|
| 544 | + | ); |
|
| 422 | 545 | } |
|
| 423 | - | let empty = self.store.put_state(&StateObject::new(vec![])).map_err(|e| e.to_string())?; |
|
| 546 | + | let empty = self |
|
| 547 | + | .store |
|
| 548 | + | .put_state(&StateObject::new(vec![])) |
|
| 549 | + | .map_err(|e| e.to_string())?; |
|
| 424 | 550 | let current = view.bookmarks.get(&u.name).copied().unwrap_or(empty); |
|
| 425 | 551 | if current != u.old_state { |
|
| 426 | 552 | return Err("stale: the remote bookmark has moved; pull first".into()); |
|
| 427 | 553 | } |
|
| 428 | - | let new_state = self.store.get_state(&u.new_state).map_err(|_| "new state object is missing".to_string())?; |
|
| 554 | + | let new_state = self |
|
| 555 | + | .store |
|
| 556 | + | .get_state(&u.new_state) |
|
| 557 | + | .map_err(|_| "new state object is missing".to_string())?; |
|
| 429 | 558 | for h in &new_state.heads { |
|
| 430 | 559 | if !self.store.has(h) { |
|
| 431 | 560 | return Err(format!("head {} is missing", h.short(12))); |
|
| 432 | 561 | } |
|
| 433 | 562 | } |
|
| 434 | 563 | if !u.force { |
|
| 435 | - | let old_heads = self.store.get_state(¤t).map_err(|e| e.to_string())?.heads; |
|
| 436 | - | let closure = self.closure_in_store(&new_state.heads).map_err(|e| e.to_string())?; |
|
| 564 | + | let old_heads = self |
|
| 565 | + | .store |
|
| 566 | + | .get_state(¤t) |
|
| 567 | + | .map_err(|e| e.to_string())? |
|
| 568 | + | .heads; |
|
| 569 | + | let closure = self |
|
| 570 | + | .closure_in_store(&new_state.heads) |
|
| 571 | + | .map_err(|e| e.to_string())?; |
|
| 437 | 572 | if let Some(h) = old_heads.iter().find(|h| !closure.contains(h)) { |
|
| 438 | - | return Err(format!("not a fast-forward: current head {} would be dropped (use --force)", h.short(12))); |
|
| 573 | + | return Err(format!( |
|
| 574 | + | "not a fast-forward: current head {} would be dropped (use --force)", |
|
| 575 | + | h.short(12) |
|
| 576 | + | )); |
|
| 439 | 577 | } |
|
| 440 | 578 | } |
|
| 441 | 579 | if let Some(p) = policy { |
|
| 442 | 580 | let Some(sa) = &u.attestation else { |
|
| 443 | - | return Err(format!("{:?} requires a signed reference attestation under this repository's policy", u.name)); |
|
| 581 | + | return Err(format!( |
|
| 582 | + | "{:?} requires a signed reference attestation under this repository's policy", |
|
| 583 | + | u.name |
|
| 584 | + | )); |
|
| 444 | 585 | }; |
|
| 445 | - | let signer = sa.sigs.first().map(|s| s.key).ok_or("attestation carries no signature")?; |
|
| 586 | + | let signer = sa |
|
| 587 | + | .sigs |
|
| 588 | + | .first() |
|
| 589 | + | .map(|s| s.key) |
|
| 590 | + | .ok_or("attestation carries no signature")?; |
|
| 446 | 591 | if !p.authorizes(&signer, &u.name) { |
|
| 447 | - | return Err(format!("key {} may not update {:?} under this repository's policy", signer.short(12), u.name)); |
|
| 592 | + | return Err(format!( |
|
| 593 | + | "key {} may not update {:?} under this repository's policy", |
|
| 594 | + | signer.short(12), |
|
| 595 | + | u.name |
|
| 596 | + | )); |
|
| 448 | 597 | } |
|
| 449 | - | let old_heads = self.store.get_state(¤t).map_err(|e| e.to_string())?.heads; |
|
| 598 | + | let old_heads = self |
|
| 599 | + | .store |
|
| 600 | + | .get_state(¤t) |
|
| 601 | + | .map_err(|e| e.to_string())? |
|
| 602 | + | .heads; |
|
| 450 | 603 | self.state_allowed(&old_heads, &new_state.heads, p, &u.name)?; |
|
| 451 | 604 | } |
|
| 452 | 605 | if let Some(sa) = &u.attestation { |
|
| 453 | - | let last = self.latest_ref_attest(&u.name).map_err(|e| e.to_string())?.and_then(|(_, a)| match a.attest.body { |
|
| 454 | - | AttestBody::Ref { serial, .. } => Some(serial), |
|
| 455 | - | _ => None, |
|
| 456 | - | }); |
|
| 606 | + | let last = self |
|
| 607 | + | .latest_ref_attest(&u.name) |
|
| 608 | + | .map_err(|e| e.to_string())? |
|
| 609 | + | .and_then(|(_, a)| match a.attest.body { |
|
| 610 | + | AttestBody::Ref { serial, .. } => Some(serial), |
|
| 611 | + | _ => None, |
|
| 612 | + | }); |
|
| 457 | 613 | self.check_ref_attest(sa, &u.name, &u.old_state, &u.new_state, last, false)?; |
|
| 458 | - | let r = self.store.put_attest(&sa.attest).map_err(|e| e.to_string())?; |
|
| 614 | + | let r = self |
|
| 615 | + | .store |
|
| 616 | + | .put_attest(&sa.attest) |
|
| 617 | + | .map_err(|e| e.to_string())?; |
|
| 459 | 618 | for sig in &sa.sigs { |
|
| 460 | - | self.store.put_signature(&r, sig).map_err(|e| e.to_string())?; |
|
| 619 | + | self.store |
|
| 620 | + | .put_signature(&r, sig) |
|
| 621 | + | .map_err(|e| e.to_string())?; |
|
| 461 | 622 | } |
|
| 462 | - | self.set_ref_attest(&u.name, &r).map_err(|e| e.to_string())?; |
|
| 623 | + | self.set_ref_attest(&u.name, &r) |
|
| 624 | + | .map_err(|e| e.to_string())?; |
|
| 463 | 625 | } |
|
| 464 | 626 | view.bookmarks.insert(u.name.clone(), u.new_state); |
|
| 465 | 627 | Ok(()) |
|
| … | 2 unchanged lines |
||
| 468 | 630 | // ------------------------------------------------------------ client side |
|
| 469 | 631 | ||
| 470 | 632 | fn remote_url(&self, name: &str) -> Result<String> { |
|
| 471 | - | self.config.remotes.get(name).cloned().ok_or_else(|| RepoError::Msg(format!("no remote named {name:?}; add one with `oxy remote add`"))) |
|
| 633 | + | self.config.remotes.get(name).cloned().ok_or_else(|| { |
|
| 634 | + | RepoError::Msg(format!( |
|
| 635 | + | "no remote named {name:?}; add one with `oxy remote add`" |
|
| 636 | + | )) |
|
| 637 | + | }) |
|
| 472 | 638 | } |
|
| 473 | 639 | ||
| 474 | 640 | /// Credentials and trust for a remote: `remote.<name>.token` (else |
|
| 475 | 641 | /// `$OXY_TOKEN`) and `remote.<name>.ca` (else `$OXY_CA`). |
|
| 642 | + | /// Refuse a remote that is another repository. An **unbound** remote |
|
| 643 | + | /// (id all zero: a hosted repository nobody has pushed to) is accepted |
|
| 644 | + | /// for pushes, which bind it to this repository's id (spec §16.1). |
|
| 645 | + | fn check_remote_id(&self, remote: &dyn crate::protocol::Remote, remote_name: &str, allow_unbound: bool) -> Result<()> { |
|
| 646 | + | let theirs = remote.repo_id()?; |
|
| 647 | + | if theirs == self.id()? { |
|
| 648 | + | return Ok(()); |
|
| 649 | + | } |
|
| 650 | + | if theirs == crate::protocol::UNBOUND_ID { |
|
| 651 | + | if allow_unbound { |
|
| 652 | + | return Ok(()); |
|
| 653 | + | } |
|
| 654 | + | return fail(format!("remote {remote_name:?} is empty and unbound: push to it first")); |
|
| 655 | + | } |
|
| 656 | + | fail(format!("remote {remote_name:?} is a different repository (repository IDs differ)")) |
|
| 657 | + | } |
|
| 658 | + | ||
| 476 | 659 | fn remote_options(&self, name: &str) -> RemoteOptions { |
|
| 477 | 660 | RemoteOptions { |
|
| 478 | - | token: self.config.remote_tokens.get(name).cloned().or_else(|| std::env::var("OXY_TOKEN").ok()), |
|
| 479 | - | ca: self.config.remote_cas.get(name).map(PathBuf::from).or_else(|| std::env::var("OXY_CA").ok().map(PathBuf::from)), |
|
| 661 | + | repo_id: self.id().ok(), |
|
| 662 | + | token: self |
|
| 663 | + | .config |
|
| 664 | + | .remote_tokens |
|
| 665 | + | .get(name) |
|
| 666 | + | .cloned() |
|
| 667 | + | .or_else(|| std::env::var("OXY_TOKEN").ok()), |
|
| 668 | + | ca: self |
|
| 669 | + | .config |
|
| 670 | + | .remote_cas |
|
| 671 | + | .get(name) |
|
| 672 | + | .map(PathBuf::from) |
|
| 673 | + | .or_else(|| std::env::var("OXY_CA").ok().map(PathBuf::from)), |
|
| 480 | 674 | } |
|
| 481 | 675 | } |
|
| 482 | 676 | ||
| … | 2 unchanged lines |
||
| 485 | 679 | } |
|
| 486 | 680 | ||
| 487 | 681 | fn serial_path(&self, remote: &str, bookmark: &str) -> PathBuf { |
|
| 488 | - | self.oxy.join("remotes").join(remote).join(format!("{bookmark}.serial")) |
|
| 682 | + | self.oxy |
|
| 683 | + | .join("remotes") |
|
| 684 | + | .join(remote) |
|
| 685 | + | .join(format!("{bookmark}.serial")) |
|
| 489 | 686 | } |
|
| 490 | 687 | ||
| 491 | 688 | fn last_seen_serial(&self, remote: &str, bookmark: &str) -> Option<u64> { |
|
| 492 | - | fs::read_to_string(self.serial_path(remote, bookmark)).ok().and_then(|s| s.trim().parse().ok()) |
|
| 689 | + | fs::read_to_string(self.serial_path(remote, bookmark)) |
|
| 690 | + | .ok() |
|
| 691 | + | .and_then(|s| s.trim().parse().ok()) |
|
| 493 | 692 | } |
|
| 494 | 693 | ||
| 495 | 694 | fn record_serial(&self, remote: &str, bookmark: &str, serial: u64) -> Result<()> { |
|
| … | 8 unchanged lines |
||
| 504 | 703 | ||
| 505 | 704 | /// The highest policy serial `remote` has ever presented. |
|
| 506 | 705 | fn remote_policy_serial(&self, remote: &str) -> Option<u64> { |
|
| 507 | - | fs::read_to_string(self.policy_serial_path(remote)).ok().and_then(|s| s.trim().parse().ok()) |
|
| 706 | + | fs::read_to_string(self.policy_serial_path(remote)) |
|
| 707 | + | .ok() |
|
| 708 | + | .and_then(|s| s.trim().parse().ok()) |
|
| 508 | 709 | } |
|
| 509 | 710 | ||
| 510 | 711 | fn record_policy_serial(&self, remote: &str, serial: u64) -> Result<()> { |
|
| … | 14 unchanged lines |
||
| 525 | 726 | ||
| 526 | 727 | /// Which remote promised an absent blob, if any. |
|
| 527 | 728 | pub fn lazy_remote(&self, blob: &ObjectRef) -> Option<String> { |
|
| 528 | - | fs::read_to_string(self.lazy_dir().join(blob.to_hex())).ok().map(|s| s.trim().to_string()) |
|
| 729 | + | fs::read_to_string(self.lazy_dir().join(blob.to_hex())) |
|
| 730 | + | .ok() |
|
| 731 | + | .map(|s| s.trim().to_string()) |
|
| 529 | 732 | } |
|
| 530 | 733 | ||
| 531 | 734 | /// Fetch an absent blob (and its chunks) from the remote that promised it. |
|
| … | 9 unchanged lines |
||
| 541 | 744 | } |
|
| 542 | 745 | } |
|
| 543 | 746 | if !self.store.has(blob) { |
|
| 544 | - | return fail(format!("{remote_name} did not send blob {}", blob.short(12))); |
|
| 747 | + | return fail(format!( |
|
| 748 | + | "{remote_name} did not send blob {}", |
|
| 749 | + | blob.short(12) |
|
| 750 | + | )); |
|
| 545 | 751 | } |
|
| 546 | 752 | let _ = fs::remove_file(self.lazy_dir().join(blob.to_hex())); |
|
| 547 | 753 | Ok(()) |
|
| … | 22 unchanged lines |
||
| 570 | 776 | /// Every head this repository knows: bookmarks, remote tracking, working copy. |
|
| 571 | 777 | fn known_heads(&self, view: &View) -> Result<Vec<ChangeHash>> { |
|
| 572 | 778 | let mut have: BTreeSet<ChangeHash> = BTreeSet::new(); |
|
| 573 | - | for st in view.bookmarks.values().chain(view.remotes.values().flat_map(|b| b.values())) { |
|
| 779 | + | for st in view |
|
| 780 | + | .bookmarks |
|
| 781 | + | .values() |
|
| 782 | + | .chain(view.remotes.values().flat_map(|b| b.values())) |
|
| 783 | + | { |
|
| 574 | 784 | if let Ok(s) = self.store.get_state(st) { |
|
| 575 | 785 | have.extend(s.heads); |
|
| 576 | 786 | } |
|
| … | 5 unchanged lines |
||
| 582 | 792 | /// Fetch every bookmark of a remote into `remotes/<name>/bookmarks/*` (spec §10.2). |
|
| 583 | 793 | pub fn fetch(&self, remote_name: &str) -> Result<FetchReport> { |
|
| 584 | 794 | let remote = self.open_named_remote(remote_name)?; |
|
| 585 | - | if remote.repo_id()? != self.id()? { |
|
| 586 | - | return fail(format!("remote {remote_name:?} is a different repository (repository IDs differ)")); |
|
| 587 | - | } |
|
| 795 | + | self.check_remote_id(remote.as_ref(), remote_name, false)?; |
|
| 588 | 796 | let resp = remote.refs()?; |
|
| 589 | 797 | // Trust policy (spec §14.5). Adopt the remote's policy when it is newer |
|
| 590 | 798 | // and signed by roots we trust, or on first contact. A remote that |
|
| … | 12 unchanged lines |
||
| 603 | 811 | Some(l) if sp.policy.serial < l.serial => {} |
|
| 604 | 812 | Some(l) if sp.policy.serial == l.serial => { |
|
| 605 | 813 | if sp.policy.hash() != l.hash() { |
|
| 606 | - | return fail(format!("{remote_name} presents a different policy with the same serial {}", l.serial)); |
|
| 814 | + | return fail(format!( |
|
| 815 | + | "{remote_name} presents a different policy with the same serial {}", |
|
| 816 | + | l.serial |
|
| 817 | + | )); |
|
| 607 | 818 | } |
|
| 608 | 819 | } |
|
| 609 | 820 | _ => { |
|
| 610 | - | self.adopt_policy_chain(&resp.history, sp, &format!("oxy fetch {remote_name}")).map_err(|e| RepoError::Msg(format!("policy from {remote_name}: {e}")))?; |
|
| 821 | + | self.adopt_policy_chain( |
|
| 822 | + | &resp.history, |
|
| 823 | + | sp, |
|
| 824 | + | &format!("oxy fetch {remote_name}"), |
|
| 825 | + | ) |
|
| 826 | + | .map_err(|e| RepoError::Msg(format!("policy from {remote_name}: {e}")))?; |
|
| 611 | 827 | } |
|
| 612 | 828 | } |
|
| 613 | 829 | self.record_policy_serial(remote_name, sp.policy.serial)?; |
|
| … | 5 unchanged lines |
||
| 619 | 835 | unverified = true; |
|
| 620 | 836 | } |
|
| 621 | 837 | } |
|
| 622 | - | let policy = if unverified { None } else { self.current_policy()?.map(|(_, sp)| sp.policy) }; |
|
| 838 | + | let policy = if unverified { |
|
| 839 | + | None |
|
| 840 | + | } else { |
|
| 841 | + | self.current_policy()?.map(|(_, sp)| sp.policy) |
|
| 842 | + | }; |
|
| 623 | 843 | let (op_ref, op) = self.head_op()?; |
|
| 624 | 844 | let mut view = op.view.clone(); |
|
| 625 | 845 | let have = self.known_heads(&view)?; |
|
| … | 16 unchanged lines |
||
| 642 | 862 | } |
|
| 643 | 863 | let state = self.store.put_state(&StateObject::new(r.heads.clone()))?; |
|
| 644 | 864 | if state != r.state { |
|
| 645 | - | return fail(format!("remote state {} does not match its heads", r.state.short(12))); |
|
| 865 | + | return fail(format!( |
|
| 866 | + | "remote state {} does not match its heads", |
|
| 867 | + | r.state.short(12) |
|
| 868 | + | )); |
|
| 646 | 869 | } |
|
| 647 | 870 | if let Some(p) = &policy { |
|
| 648 | 871 | let Some(sa) = &r.attestation else { |
|
| 649 | - | return fail(format!("{remote_name}/{} has no attestation, which its policy requires", r.name)); |
|
| 872 | + | return fail(format!( |
|
| 873 | + | "{remote_name}/{} has no attestation, which its policy requires", |
|
| 874 | + | r.name |
|
| 875 | + | )); |
|
| 650 | 876 | }; |
|
| 651 | - | let signer = sa.sigs.first().map(|s| s.key).ok_or_else(|| RepoError::Msg("attestation carries no signature".into()))?; |
|
| 877 | + | let signer = sa |
|
| 878 | + | .sigs |
|
| 879 | + | .first() |
|
| 880 | + | .map(|s| s.key) |
|
| 881 | + | .ok_or_else(|| RepoError::Msg("attestation carries no signature".into()))?; |
|
| 652 | 882 | if !p.authorizes(&signer, &r.name) { |
|
| 653 | - | return fail(format!("{remote_name}/{}: attested by {}, which the policy does not authorize", r.name, signer.short(12))); |
|
| 883 | + | return fail(format!( |
|
| 884 | + | "{remote_name}/{}: attested by {}, which the policy does not authorize", |
|
| 885 | + | r.name, |
|
| 886 | + | signer.short(12) |
|
| 887 | + | )); |
|
| 654 | 888 | } |
|
| 655 | 889 | } |
|
| 656 | 890 | if let Some(sa) = &r.attestation { |
|
| 657 | - | let AttestBody::Ref { old_state, new_state, .. } = &sa.attest.body else { |
|
| 891 | + | let AttestBody::Ref { |
|
| 892 | + | old_state, |
|
| 893 | + | new_state, |
|
| 894 | + | .. |
|
| 895 | + | } = &sa.attest.body |
|
| 896 | + | else { |
|
| 658 | 897 | return fail("remote sent a non-ref attestation for a bookmark"); |
|
| 659 | 898 | }; |
|
| 660 | 899 | let (old_state, new_state) = (*old_state, *new_state); |
|
| 661 | 900 | let serial = self |
|
| 662 | - | .check_ref_attest(sa, &r.name, &old_state, &new_state, self.last_seen_serial(remote_name, &r.name), true) |
|
| 663 | - | .map_err(|e| RepoError::Msg(format!("attestation for {}/{}: {e}", remote_name, r.name)))?; |
|
| 901 | + | .check_ref_attest( |
|
| 902 | + | sa, |
|
| 903 | + | &r.name, |
|
| 904 | + | &old_state, |
|
| 905 | + | &new_state, |
|
| 906 | + | self.last_seen_serial(remote_name, &r.name), |
|
| 907 | + | true, |
|
| 908 | + | ) |
|
| 909 | + | .map_err(|e| { |
|
| 910 | + | RepoError::Msg(format!("attestation for {}/{}: {e}", remote_name, r.name)) |
|
| 911 | + | })?; |
|
| 664 | 912 | if new_state != r.state { |
|
| 665 | - | return fail(format!("attestation for {}/{} does not cover its current state", remote_name, r.name)); |
|
| 913 | + | return fail(format!( |
|
| 914 | + | "attestation for {}/{} does not cover its current state", |
|
| 915 | + | remote_name, r.name |
|
| 916 | + | )); |
|
| 666 | 917 | } |
|
| 667 | 918 | let ar = self.store.put_attest(&sa.attest)?; |
|
| 668 | 919 | for sig in &sa.sigs { |
|
| … | 1 unchanged lines |
||
| 670 | 921 | } |
|
| 671 | 922 | self.record_serial(remote_name, &r.name, serial)?; |
|
| 672 | 923 | } |
|
| 673 | - | view.remotes.entry(remote_name.to_string()).or_default().insert(r.name.clone(), state); |
|
| 924 | + | view.remotes |
|
| 925 | + | .entry(remote_name.to_string()) |
|
| 926 | + | .or_default() |
|
| 927 | + | .insert(r.name.clone(), state); |
|
| 674 | 928 | report.push((r.name, r.heads, imported.len())); |
|
| 675 | 929 | } |
|
| 676 | 930 | let mut new_tags = 0; |
|
| … | 2 unchanged lines |
||
| 679 | 933 | continue; |
|
| 680 | 934 | } |
|
| 681 | 935 | // The tag's changes must be here: fetch them if the bookmarks did not bring them. |
|
| 682 | - | let missing: Vec<ChangeHash> = t.heads.iter().filter(|h| !self.store.has(h)).copied().collect(); |
|
| 936 | + | let missing: Vec<ChangeHash> = t |
|
| 937 | + | .heads |
|
| 938 | + | .iter() |
|
| 939 | + | .filter(|h| !self.store.has(h)) |
|
| 940 | + | .copied() |
|
| 941 | + | .collect(); |
|
| 683 | 942 | if !missing.is_empty() { |
|
| 684 | 943 | let needed = remote.negotiate(&self.known_heads(&view)?, &missing)?; |
|
| 685 | 944 | let bundle = remote.fetch(&needed, max_blob)?; |
|
| 686 | 945 | self.import_bundle(&bundle, false)?; |
|
| 687 | 946 | } |
|
| 688 | 947 | self.store.put_state(&StateObject::new(t.heads.clone()))?; |
|
| 689 | - | self.adopt_tag(&mut view, t, policy.as_ref()).map_err(|e| RepoError::Msg(format!("{remote_name} {}: {e}", t.name)))?; |
|
| 948 | + | self.adopt_tag(&mut view, t, policy.as_ref()) |
|
| 949 | + | .map_err(|e| RepoError::Msg(format!("{remote_name} {}: {e}", t.name)))?; |
|
| 690 | 950 | new_tags += 1; |
|
| 691 | 951 | } |
|
| 692 | 952 | if view != op.view { |
|
| … | 4 unchanged lines |
||
| 697 | 957 | &format!( |
|
| 698 | 958 | "{{\"event\":\"post-fetch\",\"remote\":{},\"bookmarks\":{}}}", |
|
| 699 | 959 | json_str(remote_name), |
|
| 700 | - | json_array(report.iter().map(|(n, heads, _)| format!("{{\"name\":{},\"heads\":{}}}", json_str(n), json_array(heads.iter().map(|h| json_str(&h.to_hex())))))) |
|
| 960 | + | json_array(report.iter().map(|(n, heads, _)| format!( |
|
| 961 | + | "{{\"name\":{},\"heads\":{}}}", |
|
| 962 | + | json_str(n), |
|
| 963 | + | json_array(heads.iter().map(|h| json_str(&h.to_hex()))) |
|
| 964 | + | ))) |
|
| 701 | 965 | ), |
|
| 702 | 966 | ); |
|
| 703 | - | Ok(FetchReport { refs: report, unverified, new_tags }) |
|
| 967 | + | Ok(FetchReport { |
|
| 968 | + | refs: report, |
|
| 969 | + | unverified, |
|
| 970 | + | new_tags, |
|
| 971 | + | }) |
|
| 704 | 972 | } |
|
| 705 | 973 | ||
| 706 | 974 | /// Shallow fetch of the remote's bookmark: its checkpoint as our base, |
|
| 707 | 975 | /// the changes within `depth` steps of its heads, and the boundary below. |
|
| 708 | 976 | pub fn fetch_shallow(&self, remote_name: &str, depth: u32) -> Result<usize> { |
|
| 709 | 977 | let remote = self.open_named_remote(remote_name)?; |
|
| 710 | - | if remote.repo_id()? != self.id()? { |
|
| 711 | - | return fail(format!("remote {remote_name:?} is a different repository (repository IDs differ)")); |
|
| 712 | - | } |
|
| 978 | + | self.check_remote_id(remote.as_ref(), remote_name, false)?; |
|
| 713 | 979 | let resp = remote.refs()?; |
|
| 714 | 980 | let bookmark = self.config.bookmark.clone(); |
|
| 715 | 981 | if let Some(sp) = &resp.policy { |
|
| 716 | - | self.adopt_policy_chain(&resp.history, sp, "oxy clone --depth").map_err(|e| RepoError::Msg(format!("policy from {remote_name}: {e}")))?; |
|
| 982 | + | self.adopt_policy_chain(&resp.history, sp, "oxy clone --depth") |
|
| 983 | + | .map_err(|e| RepoError::Msg(format!("policy from {remote_name}: {e}")))?; |
|
| 717 | 984 | self.record_policy_serial(remote_name, sp.policy.serial)?; |
|
| 718 | 985 | } |
|
| 719 | 986 | let Some(r) = resp.refs.into_iter().find(|r| r.name == bookmark) else { |
|
| … | 11 unchanged lines |
||
| 731 | 998 | cp_ref = Some(put); |
|
| 732 | 999 | } |
|
| 733 | 1000 | } |
|
| 734 | - | let Some(cp_ref) = cp_ref else { return fail(format!("{remote_name} sent no checkpoint")) }; |
|
| 1001 | + | let Some(cp_ref) = cp_ref else { |
|
| 1002 | + | return fail(format!("{remote_name} sent no checkpoint")); |
|
| 1003 | + | }; |
|
| 735 | 1004 | self.install_checkpoint(&bookmark, &cp_ref)?; |
|
| 736 | 1005 | // History window. |
|
| 737 | 1006 | let wanted = remote.shallow(&r.heads, depth)?; |
|
| … | 15 unchanged lines |
||
| 753 | 1022 | let state = self.store.put_state(&StateObject::new(r.heads.clone()))?; |
|
| 754 | 1023 | let (op_ref, op) = self.head_op()?; |
|
| 755 | 1024 | let mut view = op.view.clone(); |
|
| 756 | - | view.remotes.entry(remote_name.to_string()).or_default().insert(r.name.clone(), state); |
|
| 757 | - | self.record_op(&[op_ref], &format!("oxy fetch {remote_name} --depth {depth}"), view)?; |
|
| 1025 | + | view.remotes |
|
| 1026 | + | .entry(remote_name.to_string()) |
|
| 1027 | + | .or_default() |
|
| 1028 | + | .insert(r.name.clone(), state); |
|
| 1029 | + | self.record_op( |
|
| 1030 | + | &[op_ref], |
|
| 1031 | + | &format!("oxy fetch {remote_name} --depth {depth}"), |
|
| 1032 | + | view, |
|
| 1033 | + | )?; |
|
| 758 | 1034 | Ok(imported.len()) |
|
| 759 | 1035 | } |
|
| 760 | 1036 | ||
| … | 4 unchanged lines |
||
| 765 | 1041 | let (op_ref, op) = self.head_op()?; |
|
| 766 | 1042 | let bookmark = self.config.bookmark.clone(); |
|
| 767 | 1043 | let local = self.heads_of(&op.view, &bookmark)?; |
|
| 768 | - | let Some(remote_state) = op.view.remotes.get(remote_name).and_then(|b| b.get(&bookmark)).copied() else { |
|
| 769 | - | return Ok(PullReport { heads: local, changed: false, checked_out: false }); |
|
| 1044 | + | let Some(remote_state) = op |
|
| 1045 | + | .view |
|
| 1046 | + | .remotes |
|
| 1047 | + | .get(remote_name) |
|
| 1048 | + | .and_then(|b| b.get(&bookmark)) |
|
| 1049 | + | .copied() |
|
| 1050 | + | else { |
|
| 1051 | + | return Ok(PullReport { |
|
| 1052 | + | heads: local, |
|
| 1053 | + | changed: false, |
|
| 1054 | + | checked_out: false, |
|
| 1055 | + | }); |
|
| 770 | 1056 | }; |
|
| 771 | 1057 | let remote_heads = self.store.get_state(&remote_state)?.heads; |
|
| 772 | 1058 | let mut all = local.clone(); |
|
| … | 4 unchanged lines |
||
| 777 | 1063 | let mut sorted_local = local.clone(); |
|
| 778 | 1064 | sorted_local.sort(); |
|
| 779 | 1065 | if heads == sorted_local { |
|
| 780 | - | return Ok(PullReport { heads, changed: false, checked_out: false }); |
|
| 1066 | + | return Ok(PullReport { |
|
| 1067 | + | heads, |
|
| 1068 | + | changed: false, |
|
| 1069 | + | checked_out: false, |
|
| 1070 | + | }); |
|
| 781 | 1071 | } |
|
| 782 | 1072 | let mut view = op.view.clone(); |
|
| 783 | - | view.bookmarks.insert(bookmark.clone(), self.store.put_state(&StateObject::new(heads.clone()))?); |
|
| 1073 | + | view.bookmarks.insert( |
|
| 1074 | + | bookmark.clone(), |
|
| 1075 | + | self.store.put_state(&StateObject::new(heads.clone()))?, |
|
| 1076 | + | ); |
|
| 784 | 1077 | self.record_op(&[op_ref], &format!("oxy pull {remote_name}"), view)?; |
|
| 785 | 1078 | // Follow along if the working copy was clean and on the old heads. |
|
| 786 | 1079 | let snap = self.snapshot("oxy pull")?; |
|
| … | 5 unchanged lines |
||
| 792 | 1085 | self.checkout(&heads, false, "oxy pull (checkout)")?; |
|
| 793 | 1086 | checked_out = true; |
|
| 794 | 1087 | } |
|
| 795 | - | Ok(PullReport { heads, changed: true, checked_out }) |
|
| 1088 | + | Ok(PullReport { |
|
| 1089 | + | heads, |
|
| 1090 | + | changed: true, |
|
| 1091 | + | checked_out, |
|
| 1092 | + | }) |
|
| 796 | 1093 | } |
|
| 797 | 1094 | ||
| 798 | 1095 | /// Push the current bookmark (spec §16.3), sealing this author's unsigned |
|
| … | 4 unchanged lines |
||
| 803 | 1100 | } |
|
| 804 | 1101 | ||
| 805 | 1102 | /// `push` for a named bookmark (`oxy push -b NAME`). |
|
| 806 | - | pub fn push_bookmark(&self, remote_name: &str, bookmark: &str, force: bool) -> Result<Vec<RefResult>> { |
|
| 1103 | + | pub fn push_bookmark( |
|
| 1104 | + | &self, |
|
| 1105 | + | remote_name: &str, |
|
| 1106 | + | bookmark: &str, |
|
| 1107 | + | force: bool, |
|
| 1108 | + | ) -> Result<Vec<RefResult>> { |
|
| 807 | 1109 | let (op_ref, op) = self.head_op()?; |
|
| 808 | 1110 | if !op.view.bookmarks.contains_key(bookmark) { |
|
| 809 | 1111 | return fail(format!("no bookmark {bookmark}")); |
|
| … | 3 unchanged lines |
||
| 813 | 1115 | // pre-push hook: may veto before the remote is contacted. |
|
| 814 | 1116 | self.run_hook( |
|
| 815 | 1117 | "pre-push", |
|
| 816 | - | &format!("{{\"event\":\"pre-push\",\"remote\":{},\"bookmark\":{},\"heads\":{}}}", json_str(remote_name), json_str(&bookmark), json_array(local_heads.iter().map(|h| json_str(&h.to_hex())))), |
|
| 1118 | + | &format!( |
|
| 1119 | + | "{{\"event\":\"pre-push\",\"remote\":{},\"bookmark\":{},\"heads\":{}}}", |
|
| 1120 | + | json_str(remote_name), |
|
| 1121 | + | json_str(&bookmark), |
|
| 1122 | + | json_array(local_heads.iter().map(|h| json_str(&h.to_hex()))) |
|
| 1123 | + | ), |
|
| 817 | 1124 | )?; |
|
| 818 | 1125 | let remote = self.open_named_remote(remote_name)?; |
|
| 819 | - | if remote.repo_id()? != self.id()? { |
|
| 820 | - | return fail(format!("remote {remote_name:?} is a different repository (repository IDs differ)")); |
|
| 821 | - | } |
|
| 1126 | + | self.check_remote_id(remote.as_ref(), remote_name, true)?; |
|
| 822 | 1127 | let resp = remote.refs()?; |
|
| 823 | 1128 | let local_policy = self.current_policy()?.map(|(_, sp)| sp); |
|
| 824 | 1129 | let resp_policy_serial = resp.policy.as_ref().map(|p| p.policy.serial); |
|
| … | 1 unchanged lines |
||
| 826 | 1131 | (Some(l), Some(r)) if l.policy.serial > r.policy.serial => Some(l.clone()), |
|
| 827 | 1132 | (Some(l), None) => Some(l.clone()), |
|
| 828 | 1133 | (Some(l), Some(r)) if l.policy.serial < r.policy.serial => { |
|
| 829 | - | return fail(format!("{remote_name} has a newer policy (serial {}); fetch first", r.policy.serial)); |
|
| 1134 | + | return fail(format!( |
|
| 1135 | + | "{remote_name} has a newer policy (serial {}); fetch first", |
|
| 1136 | + | r.policy.serial |
|
| 1137 | + | )); |
|
| 830 | 1138 | } |
|
| 831 | 1139 | _ => None, |
|
| 832 | 1140 | }; |
|
| … | 2 unchanged lines |
||
| 835 | 1143 | return fail(format!("{remote_name} enforces a trust policy; pushing needs an identity (`oxy identity create`)")); |
|
| 836 | 1144 | } |
|
| 837 | 1145 | let remote_tags: BTreeSet<String> = resp.tags.iter().map(|t| t.name.clone()).collect(); |
|
| 838 | - | let tags_to_send: Vec<crate::protocol::TagRef> = self.tag_refs()?.into_iter().filter(|t| !remote_tags.contains(&t.name)).collect(); |
|
| 1146 | + | let tags_to_send: Vec<crate::protocol::TagRef> = self |
|
| 1147 | + | .tag_refs()? |
|
| 1148 | + | .into_iter() |
|
| 1149 | + | .filter(|t| !remote_tags.contains(&t.name)) |
|
| 1150 | + | .collect(); |
|
| 839 | 1151 | let refs = resp.refs; |
|
| 840 | 1152 | let empty = self.store.put_state(&StateObject::new(vec![]))?; |
|
| 841 | - | let (remote_state, remote_heads, last_serial) = match refs.iter().find(|r| r.name == bookmark) { |
|
| 842 | - | Some(r) => (r.state, r.heads.clone(), r.last_serial), |
|
| 843 | - | None => (empty, vec![], None), |
|
| 844 | - | }; |
|
| 845 | - | let new_state = self.store.put_state(&StateObject::new(local_heads.clone()))?; |
|
| 1153 | + | let (remote_state, remote_heads, last_serial) = |
|
| 1154 | + | match refs.iter().find(|r| r.name == bookmark) { |
|
| 1155 | + | Some(r) => (r.state, r.heads.clone(), r.last_serial), |
|
| 1156 | + | None => (empty, vec![], None), |
|
| 1157 | + | }; |
|
| 1158 | + | let new_state = self |
|
| 1159 | + | .store |
|
| 1160 | + | .put_state(&StateObject::new(local_heads.clone()))?; |
|
| 846 | 1161 | // A push that carries a policy re-attests the bookmark even when its |
|
| 847 | 1162 | // state is unchanged, so the current attestation is always by a key |
|
| 848 | 1163 | // the current policy authorizes (a revoked key's attestation would |
|
| 849 | 1164 | // otherwise be refused by every later fetch). |
|
| 850 | 1165 | if new_state == remote_state && policy_to_send.is_none() && tags_to_send.is_empty() { |
|
| 851 | - | return Ok(vec![RefResult { name: bookmark, ok: true, reason: "already up to date".into() }]); |
|
| 1166 | + | return Ok(vec![RefResult { |
|
| 1167 | + | name: bookmark, |
|
| 1168 | + | ok: true, |
|
| 1169 | + | reason: "already up to date".into(), |
|
| 1170 | + | }]); |
|
| 852 | 1171 | } |
|
| 853 | 1172 | for h in &remote_heads { |
|
| 854 | 1173 | if !self.store.has(h) { |
|
| 855 | - | return fail(format!("the remote has change {} which you do not; pull first", h.short(12))); |
|
| 1174 | + | return fail(format!( |
|
| 1175 | + | "the remote has change {} which you do not; pull first", |
|
| 1176 | + | h.short(12) |
|
| 1177 | + | )); |
|
| 856 | 1178 | } |
|
| 857 | 1179 | } |
|
| 858 | 1180 | if !force { |
|
| … | 10 unchanged lines |
||
| 869 | 1191 | let mut unsigned = Vec::new(); |
|
| 870 | 1192 | for h in &to_send { |
|
| 871 | 1193 | let c = self.store.get_change(h)?; |
|
| 872 | - | if c.author == key && !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. }) { |
|
| 1194 | + | if c.author == key |
|
| 1195 | + | && !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. }) |
|
| 1196 | + | { |
|
| 873 | 1197 | unsigned.push(*h); |
|
| 874 | 1198 | } |
|
| 875 | 1199 | } |
|
| … | 3 unchanged lines |
||
| 879 | 1203 | } |
|
| 880 | 1204 | bundle = self.bundle_for(&to_send)?; |
|
| 881 | 1205 | } |
|
| 882 | - | bundle.objects.push((ObjectType::State, StateObject::new(local_heads.clone()).encode())); |
|
| 1206 | + | bundle.objects.push(( |
|
| 1207 | + | ObjectType::State, |
|
| 1208 | + | StateObject::new(local_heads.clone()).encode(), |
|
| 1209 | + | )); |
|
| 883 | 1210 | let attestation = match key { |
|
| 884 | 1211 | Some(_) => Some(self.sign_attest(&Attest { |
|
| 885 | 1212 | repo: self.id()?, |
|
| 886 | 1213 | hlc: Hlc::now(), |
|
| 887 | - | body: AttestBody::Ref { name: bookmark.clone(), old_state: remote_state, new_state, serial: last_serial.unwrap_or(0) + 1 }, |
|
| 1214 | + | body: AttestBody::Ref { |
|
| 1215 | + | name: bookmark.clone(), |
|
| 1216 | + | old_state: remote_state, |
|
| 1217 | + | new_state, |
|
| 1218 | + | serial: last_serial.unwrap_or(0) + 1, |
|
| 1219 | + | }, |
|
| 888 | 1220 | })?), |
|
| 889 | 1221 | None => None, |
|
| 890 | 1222 | }; |
|
| … | 5 unchanged lines |
||
| 896 | 1228 | // Tag states must be present on the remote: bundle their changes too. |
|
| 897 | 1229 | for t in &tags_to_send { |
|
| 898 | 1230 | let extra = self.negotiate(&remote_heads, &t.heads)?; |
|
| 899 | - | let extra: Vec<ChangeHash> = extra.into_iter().filter(|h| !to_send.contains(h)).collect(); |
|
| 1231 | + | let extra: Vec<ChangeHash> = |
|
| 1232 | + | extra.into_iter().filter(|h| !to_send.contains(h)).collect(); |
|
| 900 | 1233 | if !extra.is_empty() { |
|
| 901 | 1234 | let more = self.bundle_for(&extra)?; |
|
| 902 | 1235 | bundle.changes.extend(more.changes); |
|
| 903 | 1236 | bundle.attests.extend(more.attests); |
|
| 904 | 1237 | bundle.objects.extend(more.objects); |
|
| 905 | 1238 | } |
|
| 906 | - | bundle.objects.push((ObjectType::State, StateObject::new(t.heads.clone()).encode())); |
|
| 1239 | + | bundle.objects.push(( |
|
| 1240 | + | ObjectType::State, |
|
| 1241 | + | StateObject::new(t.heads.clone()).encode(), |
|
| 1242 | + | )); |
|
| 907 | 1243 | } |
|
| 908 | 1244 | if let Some(l) = &policy_to_send { |
|
| 909 | 1245 | let remote_serial = resp_policy_serial; |
|
| 910 | - | bundle.policies = self.policy_history()?.into_iter().filter(|sp| remote_serial.is_none_or(|r| sp.policy.serial > r) && sp.policy.serial < l.policy.serial).collect(); |
|
| 1246 | + | bundle.policies = self |
|
| 1247 | + | .policy_history()? |
|
| 1248 | + | .into_iter() |
|
| 1249 | + | .filter(|sp| { |
|
| 1250 | + | remote_serial.is_none_or(|r| sp.policy.serial > r) |
|
| 1251 | + | && sp.policy.serial < l.policy.serial |
|
| 1252 | + | }) |
|
| 1253 | + | .collect(); |
|
| 911 | 1254 | } |
|
| 912 | - | let results = remote.push(bundle, vec![RefUpdate { name: bookmark.clone(), old_state: remote_state, new_state, force, attestation }], policy_to_send, tags_to_send)?; |
|
| 913 | - | if let (Some(serial), true) = (sent_serial, results.iter().any(|r| r.ok && r.name == "policy")) { |
|
| 1255 | + | let results = remote.push( |
|
| 1256 | + | bundle, |
|
| 1257 | + | vec![RefUpdate { |
|
| 1258 | + | name: bookmark.clone(), |
|
| 1259 | + | old_state: remote_state, |
|
| 1260 | + | new_state, |
|
| 1261 | + | force, |
|
| 1262 | + | attestation, |
|
| 1263 | + | }], |
|
| 1264 | + | policy_to_send, |
|
| 1265 | + | tags_to_send, |
|
| 1266 | + | )?; |
|
| 1267 | + | if let (Some(serial), true) = ( |
|
| 1268 | + | sent_serial, |
|
| 1269 | + | results.iter().any(|r| r.ok && r.name == "policy"), |
|
| 1270 | + | ) { |
|
| 914 | 1271 | self.record_policy_serial(remote_name, serial)?; |
|
| 915 | 1272 | } |
|
| 916 | 1273 | if results.iter().any(|r| r.ok && r.name == bookmark) { |
|
| 917 | 1274 | let mut view = op.view.clone(); |
|
| 918 | - | view.remotes.entry(remote_name.to_string()).or_default().insert(bookmark.clone(), new_state); |
|
| 1275 | + | view.remotes |
|
| 1276 | + | .entry(remote_name.to_string()) |
|
| 1277 | + | .or_default() |
|
| 1278 | + | .insert(bookmark.clone(), new_state); |
|
| 919 | 1279 | self.record_op(&[op_ref], &format!("oxy push {remote_name}"), view)?; |
|
| 920 | 1280 | if let Some(s) = serial { |
|
| 921 | 1281 | self.record_serial(remote_name, &bookmark, s)?; |
|
| … | 11 unchanged lines |
||
| 933 | 1293 | /// Clone, leaving blobs above `max_blob` bytes to be fetched on first |
|
| 934 | 1294 | /// read, and materializing only `sparse` paths (which implies lazy blobs |
|
| 935 | 1295 | /// unless a size is given, so out-of-scope binaries are never fetched). |
|
| 936 | - | pub fn clone_with(url: &str, dest: &Path, max_blob: Option<u64>, sparse: &[String]) -> Result<Repo> { |
|
| 1296 | + | pub fn clone_with( |
|
| 1297 | + | url: &str, |
|
| 1298 | + | dest: &Path, |
|
| 1299 | + | max_blob: Option<u64>, |
|
| 1300 | + | sparse: &[String], |
|
| 1301 | + | ) -> Result<Repo> { |
|
| 937 | 1302 | Self::clone_full(url, dest, max_blob, sparse, None) |
|
| 938 | 1303 | } |
|
| 939 | 1304 | ||
| 940 | 1305 | /// Shallow clone (spec §17): a checkpoint of the bookmark's heads plus the |
|
| 941 | 1306 | /// changes within `depth` steps of them; the rest is the boundary. |
|
| 942 | - | pub fn clone_shallow(url: &str, dest: &Path, depth: u32, max_blob: Option<u64>, sparse: &[String]) -> Result<Repo> { |
|
| 1307 | + | pub fn clone_shallow( |
|
| 1308 | + | url: &str, |
|
| 1309 | + | dest: &Path, |
|
| 1310 | + | depth: u32, |
|
| 1311 | + | max_blob: Option<u64>, |
|
| 1312 | + | sparse: &[String], |
|
| 1313 | + | ) -> Result<Repo> { |
|
| 943 | 1314 | Self::clone_full(url, dest, max_blob, sparse, Some(depth)) |
|
| 944 | 1315 | } |
|
| 945 | 1316 | ||
| 946 | - | fn clone_full(url: &str, dest: &Path, max_blob: Option<u64>, sparse: &[String], depth: Option<u32>) -> Result<Repo> { |
|
| 1317 | + | fn clone_full( |
|
| 1318 | + | url: &str, |
|
| 1319 | + | dest: &Path, |
|
| 1320 | + | max_blob: Option<u64>, |
|
| 1321 | + | sparse: &[String], |
|
| 1322 | + | depth: Option<u32>, |
|
| 1323 | + | ) -> Result<Repo> { |
|
| 947 | 1324 | let options = RemoteOptions { |
|
| 948 | 1325 | token: std::env::var("OXY_TOKEN").ok(), |
|
| 949 | 1326 | ca: std::env::var("OXY_CA").ok().map(PathBuf::from), |
|
| 1327 | + | repo_id: None, |
|
| 950 | 1328 | }; |
|
| 951 | 1329 | let remote = open_remote(url, &options)?; |
|
| 952 | 1330 | let id = remote.repo_id()?; |
|
| 1331 | + | if id == crate::protocol::UNBOUND_ID { |
|
| 1332 | + | return fail(format!("{url} is empty and unbound: push a repository to it first")); |
|
| 1333 | + | } |
|
| 953 | 1334 | let mut repo = Repo::init_with_id(dest, id)?; |
|
| 954 | 1335 | repo.config.set("remote.origin.url", url)?; |
|
| 955 | 1336 | if let Some(m) = max_blob { |
|
| … | 12 unchanged lines |
||
| 968 | 1349 | } |
|
| 969 | 1350 | let (op_ref, op) = repo.head_op()?; |
|
| 970 | 1351 | let bookmark = repo.config.bookmark.clone(); |
|
| 971 | - | if let Some(state) = op.view.remotes.get("origin").and_then(|b| b.get(&bookmark)).copied() { |
|
| 1352 | + | if let Some(state) = op |
|
| 1353 | + | .view |
|
| 1354 | + | .remotes |
|
| 1355 | + | .get("origin") |
|
| 1356 | + | .and_then(|b| b.get(&bookmark)) |
|
| 1357 | + | .copied() |
|
| 1358 | + | { |
|
| 972 | 1359 | let heads = repo.store.get_state(&state)?.heads; |
|
| 973 | 1360 | let mut view = op.view.clone(); |
|
| 974 | 1361 | view.bookmarks.insert(bookmark, state); |
|
| … | 6 unchanged lines |
modified crates/oxy-core/src/repo/tags.rs +111 −22
| … | 23 unchanged lines |
||
| 24 | 24 | pub fn tag_attest(&self, name: &str) -> Result<Option<(ObjectRef, SignedAttest)>> { |
|
| 25 | 25 | match fs::read_to_string(self.tag_attest_path(name)) { |
|
| 26 | 26 | Ok(h) => { |
|
| 27 | - | let r = ObjectRef::from_hex(h.trim()).ok_or_else(|| RepoError::Msg("corrupt tag attestation pointer".into()))?; |
|
| 28 | - | Ok(Some((r, SignedAttest { attest: self.store.get_attest(&r)?, sigs: self.store.signatures(&r)? }))) |
|
| 27 | + | let r = ObjectRef::from_hex(h.trim()) |
|
| 28 | + | .ok_or_else(|| RepoError::Msg("corrupt tag attestation pointer".into()))?; |
|
| 29 | + | Ok(Some(( |
|
| 30 | + | r, |
|
| 31 | + | SignedAttest { |
|
| 32 | + | attest: self.store.get_attest(&r)?, |
|
| 33 | + | sigs: self.store.signatures(&r)?, |
|
| 34 | + | }, |
|
| 35 | + | ))) |
|
| 29 | 36 | } |
|
| 30 | 37 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), |
|
| 31 | 38 | Err(e) => Err(e.into()), |
|
| … | 7 unchanged lines |
||
| 39 | 46 | ||
| 40 | 47 | /// Create a tag named `name` for the state of `heads` (spec §10.1). |
|
| 41 | 48 | pub fn tag_create(&self, name: &str, heads: &[ChangeHash], message: &str) -> Result<TagInfo> { |
|
| 42 | - | crate::objects::validate_name(name).map_err(|e| RepoError::Msg(format!("tag name: {e}")))?; |
|
| 49 | + | crate::objects::validate_name(name) |
|
| 50 | + | .map_err(|e| RepoError::Msg(format!("tag name: {e}")))?; |
|
| 43 | 51 | let (op_ref, op) = self.head_op()?; |
|
| 44 | 52 | if op.view.tags.contains_key(name) { |
|
| 45 | 53 | return fail(format!("tag {name:?} already exists; tags are immutable")); |
|
| … | 5 unchanged lines |
||
| 51 | 59 | } |
|
| 52 | 60 | let state = self.store.put_state(&StateObject::new(heads.clone()))?; |
|
| 53 | 61 | let mut signer = None; |
|
| 54 | - | let attest = Attest { repo: self.id()?, hlc: Hlc::now(), body: AttestBody::Tag { name: name.into(), state, message: message.into() } }; |
|
| 62 | + | let attest = Attest { |
|
| 63 | + | repo: self.id()?, |
|
| 64 | + | hlc: Hlc::now(), |
|
| 65 | + | body: AttestBody::Tag { |
|
| 66 | + | name: name.into(), |
|
| 67 | + | state, |
|
| 68 | + | message: message.into(), |
|
| 69 | + | }, |
|
| 70 | + | }; |
|
| 55 | 71 | if self.secret_key()?.is_some() { |
|
| 56 | 72 | let sa = self.sign_attest(&attest)?; |
|
| 57 | 73 | let r = sa.attest.hash(); |
|
| 58 | 74 | self.set_tag_attest(name, &r)?; |
|
| 59 | - | signer = sa.sigs.first().and_then(|s| self.store.get_identity(&s.key).ok()).map(|i| i.name); |
|
| 75 | + | signer = sa |
|
| 76 | + | .sigs |
|
| 77 | + | .first() |
|
| 78 | + | .and_then(|s| self.store.get_identity(&s.key).ok()) |
|
| 79 | + | .map(|i| i.name); |
|
| 60 | 80 | } else { |
|
| 61 | 81 | // No key: keep the attestation unsigned so the message survives; |
|
| 62 | 82 | // it is never sent to remotes (`tag_refs`). |
|
| … | 3 unchanged lines |
||
| 66 | 86 | let mut view = op.view.clone(); |
|
| 67 | 87 | view.tags.insert(name.into(), state); |
|
| 68 | 88 | self.record_op(&[op_ref], &format!("oxy tag create {name}"), view)?; |
|
| 69 | - | Ok(TagInfo { name: name.into(), state, heads, message: message.into(), signer }) |
|
| 89 | + | Ok(TagInfo { |
|
| 90 | + | name: name.into(), |
|
| 91 | + | state, |
|
| 92 | + | heads, |
|
| 93 | + | message: message.into(), |
|
| 94 | + | signer, |
|
| 95 | + | }) |
|
| 70 | 96 | } |
|
| 71 | 97 | ||
| 72 | 98 | /// Every tag, by name. |
|
| … | 8 unchanged lines |
||
| 81 | 107 | AttestBody::Tag { message, .. } => message.clone(), |
|
| 82 | 108 | _ => String::new(), |
|
| 83 | 109 | }; |
|
| 84 | - | (message, sa.sigs.first().and_then(|s| self.store.get_identity(&s.key).ok()).map(|i| i.name)) |
|
| 110 | + | ( |
|
| 111 | + | message, |
|
| 112 | + | sa.sigs |
|
| 113 | + | .first() |
|
| 114 | + | .and_then(|s| self.store.get_identity(&s.key).ok()) |
|
| 115 | + | .map(|i| i.name), |
|
| 116 | + | ) |
|
| 85 | 117 | } |
|
| 86 | 118 | None => (String::new(), None), |
|
| 87 | 119 | }; |
|
| 88 | - | out.push(TagInfo { name: name.clone(), state: *state, heads, message, signer }); |
|
| 120 | + | out.push(TagInfo { |
|
| 121 | + | name: name.clone(), |
|
| 122 | + | state: *state, |
|
| 123 | + | heads, |
|
| 124 | + | message, |
|
| 125 | + | signer, |
|
| 126 | + | }); |
|
| 89 | 127 | } |
|
| 90 | 128 | Ok(out) |
|
| 91 | 129 | } |
|
| … | 10 unchanged lines |
||
| 102 | 140 | pub(crate) fn tag_refs(&self) -> Result<Vec<crate::protocol::TagRef>> { |
|
| 103 | 141 | let mut out = Vec::new(); |
|
| 104 | 142 | for t in self.tags()? { |
|
| 105 | - | let attestation = self.tag_attest(&t.name)?.map(|(_, a)| a).filter(|a| !a.sigs.is_empty()); |
|
| 106 | - | out.push(crate::protocol::TagRef { name: t.name.clone(), state: t.state, heads: t.heads, attestation }); |
|
| 143 | + | let attestation = self |
|
| 144 | + | .tag_attest(&t.name)? |
|
| 145 | + | .map(|(_, a)| a) |
|
| 146 | + | .filter(|a| !a.sigs.is_empty()); |
|
| 147 | + | out.push(crate::protocol::TagRef { |
|
| 148 | + | name: t.name.clone(), |
|
| 149 | + | state: t.state, |
|
| 150 | + | heads: t.heads, |
|
| 151 | + | attestation, |
|
| 152 | + | }); |
|
| 107 | 153 | } |
|
| 108 | 154 | Ok(out) |
|
| 109 | 155 | } |
|
| … | 1 unchanged lines |
||
| 111 | 157 | /// Adopt a tag received from a remote or a push. Verifies the |
|
| 112 | 158 | /// attestation when present (and requires one, by an authorized key, |
|
| 113 | 159 | /// under a policy). An existing tag with a different state is refused. |
|
| 114 | - | pub(crate) fn adopt_tag(&self, view: &mut View, t: &crate::protocol::TagRef, policy: Option<&crate::policy::Policy>) -> std::result::Result<(), String> { |
|
| 160 | + | pub(crate) fn adopt_tag( |
|
| 161 | + | &self, |
|
| 162 | + | view: &mut View, |
|
| 163 | + | t: &crate::protocol::TagRef, |
|
| 164 | + | policy: Option<&crate::policy::Policy>, |
|
| 165 | + | ) -> std::result::Result<(), String> { |
|
| 115 | 166 | if let Some(existing) = view.tags.get(&t.name) { |
|
| 116 | - | return if *existing == t.state { Ok(()) } else { Err(format!("tag {:?} already exists with a different state; tags are immutable", t.name)) }; |
|
| 167 | + | return if *existing == t.state { |
|
| 168 | + | Ok(()) |
|
| 169 | + | } else { |
|
| 170 | + | Err(format!( |
|
| 171 | + | "tag {:?} already exists with a different state; tags are immutable", |
|
| 172 | + | t.name |
|
| 173 | + | )) |
|
| 174 | + | }; |
|
| 117 | 175 | } |
|
| 118 | - | let state = self.store.get_state(&t.state).map_err(|_| "tag state object is missing".to_string())?; |
|
| 176 | + | let state = self |
|
| 177 | + | .store |
|
| 178 | + | .get_state(&t.state) |
|
| 179 | + | .map_err(|_| "tag state object is missing".to_string())?; |
|
| 119 | 180 | for h in &state.heads { |
|
| 120 | 181 | if !self.store.has(h) && !self.boundary().contains(h) { |
|
| 121 | - | return Err(format!("tag {:?} names change {}, which is missing", t.name, h.short(12))); |
|
| 182 | + | return Err(format!( |
|
| 183 | + | "tag {:?} names change {}, which is missing", |
|
| 184 | + | t.name, |
|
| 185 | + | h.short(12) |
|
| 186 | + | )); |
|
| 122 | 187 | } |
|
| 123 | 188 | } |
|
| 124 | 189 | if let Some(sa) = &t.attestation { |
|
| 125 | - | let AttestBody::Tag { name, state: st, .. } = &sa.attest.body else { return Err("tag attestation has the wrong kind".into()) }; |
|
| 190 | + | let AttestBody::Tag { |
|
| 191 | + | name, state: st, .. |
|
| 192 | + | } = &sa.attest.body |
|
| 193 | + | else { |
|
| 194 | + | return Err("tag attestation has the wrong kind".into()); |
|
| 195 | + | }; |
|
| 126 | 196 | if *name != t.name || *st != t.state { |
|
| 127 | 197 | return Err("tag attestation does not describe this tag".into()); |
|
| 128 | 198 | } |
|
| … | 1 unchanged lines |
||
| 130 | 200 | return Err("tag attestation is for a different repository".into()); |
|
| 131 | 201 | } |
|
| 132 | 202 | let r = sa.attest.hash(); |
|
| 133 | - | let sig = sa.sigs.first().ok_or("tag attestation carries no signature")?; |
|
| 134 | - | let id = self.store.get_identity(&sig.key).map_err(|_| format!("unknown key {}", sig.key.short(12)))?; |
|
| 203 | + | let sig = sa |
|
| 204 | + | .sigs |
|
| 205 | + | .first() |
|
| 206 | + | .ok_or("tag attestation carries no signature")?; |
|
| 207 | + | let id = self |
|
| 208 | + | .store |
|
| 209 | + | .get_identity(&sig.key) |
|
| 210 | + | .map_err(|_| format!("unknown key {}", sig.key.short(12)))?; |
|
| 135 | 211 | verify_signature(&id, sig, Domain::Attest, &r).map_err(|e| e.to_string())?; |
|
| 136 | 212 | if let Some(p) = policy { |
|
| 137 | 213 | if !p.authorizes(&sig.key, &format!("tags/{}", t.name)) { |
|
| 138 | - | return Err(format!("key {} may not create tags/{} under this repository's policy", sig.key.short(12), t.name)); |
|
| 214 | + | return Err(format!( |
|
| 215 | + | "key {} may not create tags/{} under this repository's policy", |
|
| 216 | + | sig.key.short(12), |
|
| 217 | + | t.name |
|
| 218 | + | )); |
|
| 139 | 219 | } |
|
| 140 | 220 | } |
|
| 141 | - | let stored = self.store.put_attest(&sa.attest).map_err(|e| e.to_string())?; |
|
| 221 | + | let stored = self |
|
| 222 | + | .store |
|
| 223 | + | .put_attest(&sa.attest) |
|
| 224 | + | .map_err(|e| e.to_string())?; |
|
| 142 | 225 | for s in &sa.sigs { |
|
| 143 | - | self.store.put_signature(&stored, s).map_err(|e| e.to_string())?; |
|
| 226 | + | self.store |
|
| 227 | + | .put_signature(&stored, s) |
|
| 228 | + | .map_err(|e| e.to_string())?; |
|
| 144 | 229 | } |
|
| 145 | - | self.set_tag_attest(&t.name, &stored).map_err(|e| e.to_string())?; |
|
| 230 | + | self.set_tag_attest(&t.name, &stored) |
|
| 231 | + | .map_err(|e| e.to_string())?; |
|
| 146 | 232 | } else if policy.is_some() { |
|
| 147 | - | return Err(format!("tag {:?} needs a signed attestation under this repository's policy", t.name)); |
|
| 233 | + | return Err(format!( |
|
| 234 | + | "tag {:?} needs a signed attestation under this repository's policy", |
|
| 235 | + | t.name |
|
| 236 | + | )); |
|
| 148 | 237 | } |
|
| 149 | 238 | view.tags.insert(t.name.clone(), t.state); |
|
| 150 | 239 | Ok(()) |
|
| … | 2 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +1676 −364
| 1 | 1 | //! Tests for the repository layer. |
|
| 2 | 2 | ||
| 3 | 3 | use super::*; |
|
| 4 | + | use crate::protocol::SignedPolicy; |
|
| 4 | 5 | use crate::repo::{BisectStep, Selection}; |
|
| 5 | - | use crate::protocol::SignedPolicy; |
|
| 6 | 6 | ||
| 7 | 7 | /// Point `$OXY_HOME` at a throwaway directory once per test process so no |
|
| 8 | 8 | /// test reads or writes the developer's real user configuration or keys. |
|
| … | 14 unchanged lines |
||
| 23 | 23 | } |
|
| 24 | 24 | ||
| 25 | 25 | fn statuses(s: &SnapshotResult) -> Vec<(String, ChangeStatus)> { |
|
| 26 | - | s.statuses.iter().map(|p| (p.path.clone(), p.status)).collect() |
|
| 26 | + | s.statuses |
|
| 27 | + | .iter() |
|
| 28 | + | .map(|p| (p.path.clone(), p.status)) |
|
| 29 | + | .collect() |
|
| 27 | 30 | } |
|
| 28 | 31 | ||
| 29 | 32 | #[test] |
|
| … | 44 unchanged lines |
||
| 74 | 77 | assert!(repo.commit("nothing", &[]).is_err()); |
|
| 75 | 78 | ||
| 76 | 79 | // Modify one file, remove another, add a binary file. |
|
| 77 | - | write(root, "src/main.rs", b"fn main() {\n println!(\"hi\");\n}\n"); |
|
| 80 | + | write( |
|
| 81 | + | root, |
|
| 82 | + | "src/main.rs", |
|
| 83 | + | b"fn main() {\n println!(\"hi\");\n}\n", |
|
| 84 | + | ); |
|
| 78 | 85 | fs::remove_file(root.join("README.md")).unwrap(); |
|
| 79 | 86 | write(root, "logo.png", b"\x89PNG\0\0\x01"); |
|
| 80 | 87 | let s = repo.snapshot("oxy status").unwrap(); |
|
| … | 12 unchanged lines |
||
| 93 | 100 | ||
| 94 | 101 | let h2 = repo.commit("Second", &[]).unwrap(); |
|
| 95 | 102 | let c2 = repo.store().get_change(&h2).unwrap(); |
|
| 96 | - | assert_eq!(c2.deps, vec![h1], "everything it touches was introduced by h1"); |
|
| 103 | + | assert_eq!( |
|
| 104 | + | c2.deps, |
|
| 105 | + | vec![h1], |
|
| 106 | + | "everything it touches was introduced by h1" |
|
| 107 | + | ); |
|
| 97 | 108 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h2]); |
|
| 98 | 109 | ||
| 99 | 110 | // The materialized head state matches the working copy. |
|
| 100 | 111 | let st = repo.load_state(&[h2]).unwrap(); |
|
| 101 | 112 | let snap = st.materialize(); |
|
| 102 | 113 | assert!(!snap.is_conflicted()); |
|
| 103 | - | assert_eq!(snap.files.keys().cloned().collect::<Vec<_>>(), [".oxyignore", "logo.png", "src", "src/main.rs"]); |
|
| 104 | - | assert!(matches!(&snap.files["src/main.rs"].content, FileContent::Text { bytes, .. } if bytes == b"fn main() {\n println!(\"hi\");\n}\n")); |
|
| 114 | + | assert_eq!( |
|
| 115 | + | snap.files.keys().cloned().collect::<Vec<_>>(), |
|
| 116 | + | [".oxyignore", "logo.png", "src", "src/main.rs"] |
|
| 117 | + | ); |
|
| 118 | + | assert!( |
|
| 119 | + | matches!(&snap.files["src/main.rs"].content, FileContent::Text { bytes, .. } if bytes == b"fn main() {\n println!(\"hi\");\n}\n") |
|
| 120 | + | ); |
|
| 105 | 121 | assert!(snap.files["logo.png"].mode.opaque); |
|
| 106 | 122 | if let FileContent::Opaque { blobs } = &snap.files["logo.png"].content { |
|
| 107 | 123 | assert_eq!(repo.read_blob(&blobs[0]).unwrap(), b"\x89PNG\0\0\x01"); |
|
| … | 15 unchanged lines |
||
| 123 | 139 | ||
| 124 | 140 | // Resolution by prefix. |
|
| 125 | 141 | assert_eq!(repo.resolve_change(&c2.change_id.short(6)).unwrap(), h2); |
|
| 126 | - | assert_eq!(repo.resolve_change(&format!("#{}", h1.short(8))).unwrap(), h1); |
|
| 142 | + | assert_eq!( |
|
| 143 | + | repo.resolve_change(&format!("#{}", h1.short(8))).unwrap(), |
|
| 144 | + | h1 |
|
| 145 | + | ); |
|
| 127 | 146 | assert!(repo.resolve_change("zzzz").is_err()); |
|
| 128 | 147 | ||
| 129 | 148 | // Undo the commit: bookmark and working copy go back. |
|
| 130 | 149 | repo.undo().unwrap(); |
|
| 131 | 150 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h1]); |
|
| 132 | 151 | let s = repo.snapshot("oxy status").unwrap(); |
|
| 133 | - | assert!(!s.changed, "files on disk still match the pre-commit working copy"); |
|
| 152 | + | assert!( |
|
| 153 | + | !s.changed, |
|
| 154 | + | "files on disk still match the pre-commit working copy" |
|
| 155 | + | ); |
|
| 134 | 156 | assert_eq!(s.statuses.len(), 3); |
|
| 135 | 157 | } |
|
| 136 | 158 | ||
| … | 23 unchanged lines |
||
| 160 | 182 | assert!(st.materialize().is_conflicted()); |
|
| 161 | 183 | ||
| 162 | 184 | // Checking out main writes markers. |
|
| 163 | - | let r = repo.checkout(&repo.bookmark_heads().unwrap(), false, "checkout main").unwrap(); |
|
| 185 | + | let r = repo |
|
| 186 | + | .checkout(&repo.bookmark_heads().unwrap(), false, "checkout main") |
|
| 187 | + | .unwrap(); |
|
| 164 | 188 | assert!(r.conflicted); |
|
| 165 | 189 | let marked = fs::read_to_string(root.join("f.txt")).unwrap(); |
|
| 166 | - | assert!(marked.starts_with("a\n<<<<<<< oxy conflict 1 of 1"), "{marked}"); |
|
| 167 | - | assert!(marked.contains("B\n") && marked.contains("X\n") && marked.ends_with("end\nc\n"), "{marked}"); |
|
| 190 | + | assert!( |
|
| 191 | + | marked.starts_with("a\n<<<<<<< oxy conflict 1 of 1"), |
|
| 192 | + | "{marked}" |
|
| 193 | + | ); |
|
| 194 | + | assert!( |
|
| 195 | + | marked.contains("B\n") && marked.contains("X\n") && marked.ends_with("end\nc\n"), |
|
| 196 | + | "{marked}" |
|
| 197 | + | ); |
|
| 168 | 198 | assert!(has_conflict_markers(marked.as_bytes())); |
|
| 169 | 199 | // Untouched markers: clean working copy; commit has nothing to do. |
|
| 170 | 200 | let s = repo.snapshot("status").unwrap(); |
|
| … | 3 unchanged lines |
||
| 174 | 204 | // Partially edited file that still has markers cannot be committed. |
|
| 175 | 205 | write(root, "f.txt", marked.replace("B\n", "BB\n").as_bytes()); |
|
| 176 | 206 | let s = repo.snapshot("status").unwrap(); |
|
| 177 | - | assert_eq!(statuses(&s), vec![("f.txt".into(), ChangeStatus::ConflictEdited)]); |
|
| 207 | + | assert_eq!( |
|
| 208 | + | statuses(&s), |
|
| 209 | + | vec![("f.txt".into(), ChangeStatus::ConflictEdited)] |
|
| 210 | + | ); |
|
| 178 | 211 | assert!(repo.commit("no", &[]).is_err()); |
|
| 179 | 212 | ||
| 180 | 213 | // Resolve by keeping one side and adding a line elsewhere. |
|
| … | 6 unchanged lines |
||
| 187 | 220 | assert_eq!(c4.kind, ChangeKind::Resolution); |
|
| 188 | 221 | let mut want = vec![h1, h2, h3]; |
|
| 189 | 222 | want.sort(); |
|
| 190 | - | assert_eq!(c4.deps, want, "a resolution depends on exactly the contributors and what it references"); |
|
| 223 | + | assert_eq!( |
|
| 224 | + | c4.deps, want, |
|
| 225 | + | "a resolution depends on exactly the contributors and what it references" |
|
| 226 | + | ); |
|
| 191 | 227 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h4]); |
|
| 192 | 228 | let snap = repo.load_state(&[h4]).unwrap().materialize(); |
|
| 193 | 229 | assert!(!snap.is_conflicted()); |
|
| 194 | - | assert!(matches!(&snap.files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nX\nc\nd\n")); |
|
| 230 | + | assert!( |
|
| 231 | + | matches!(&snap.files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nX\nc\nd\n") |
|
| 232 | + | ); |
|
| 195 | 233 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 196 | 234 | // A peer holding only h2 and h3 and receiving h4 sees the same result, |
|
| 197 | 235 | // whatever order the changes arrive in. |
|
| … | 2 unchanged lines |
||
| 200 | 238 | st.add_unchecked(repo.store().get_change(&h).unwrap()); |
|
| 201 | 239 | } |
|
| 202 | 240 | st.validate().unwrap(); |
|
| 203 | - | assert!(matches!(&st.materialize().files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nX\nc\nd\n")); |
|
| 241 | + | assert!( |
|
| 242 | + | matches!(&st.materialize().files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nX\nc\nd\n") |
|
| 243 | + | ); |
|
| 204 | 244 | } |
|
| 205 | 245 | ||
| 206 | 246 | #[test] |
|
| … | 1 unchanged lines |
||
| 208 | 248 | hermetic(); |
|
| 209 | 249 | let (dir, repo, _h1, _h2, _h3) = conflicted_repo(); |
|
| 210 | 250 | let root = dir.path(); |
|
| 211 | - | repo.checkout(&repo.bookmark_heads().unwrap(), false, "checkout main").unwrap(); |
|
| 251 | + | repo.checkout(&repo.bookmark_heads().unwrap(), false, "checkout main") |
|
| 252 | + | .unwrap(); |
|
| 212 | 253 | // Keep both sides in a chosen order. |
|
| 213 | 254 | write(root, "f.txt", b"a\nX\nB\nc\n"); |
|
| 214 | 255 | let h = repo.commit("both", &[]).unwrap(); |
|
| 215 | 256 | let snap = repo.load_state(&[h]).unwrap().materialize(); |
|
| 216 | 257 | assert!(!snap.is_conflicted()); |
|
| 217 | - | assert!(matches!(&snap.files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nX\nB\nc\n")); |
|
| 258 | + | assert!( |
|
| 259 | + | matches!(&snap.files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nX\nB\nc\n") |
|
| 260 | + | ); |
|
| 218 | 261 | // Undo and instead drop the whole region. |
|
| 219 | 262 | repo.undo().unwrap(); |
|
| 220 | 263 | write(root, "f.txt", b"a\nc\n"); |
|
| 221 | 264 | let h = repo.commit("neither", &[]).unwrap(); |
|
| 222 | 265 | let snap = repo.load_state(&[h]).unwrap().materialize(); |
|
| 223 | 266 | assert!(!snap.is_conflicted()); |
|
| 224 | - | assert!(matches!(&snap.files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n")); |
|
| 267 | + | assert!( |
|
| 268 | + | matches!(&snap.files["f.txt"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n") |
|
| 269 | + | ); |
|
| 225 | 270 | } |
|
| 226 | 271 | ||
| 227 | 272 | #[test] |
|
| … | 9 unchanged lines |
||
| 237 | 282 | repo.checkout(&[h1], false, "checkout").unwrap(); |
|
| 238 | 283 | write(root, "new.txt", b"two\n"); |
|
| 239 | 284 | repo.commit("two", &[]).unwrap(); |
|
| 240 | - | let r = repo.checkout(&repo.bookmark_heads().unwrap(), false, "checkout main").unwrap(); |
|
| 285 | + | let r = repo |
|
| 286 | + | .checkout(&repo.bookmark_heads().unwrap(), false, "checkout main") |
|
| 287 | + | .unwrap(); |
|
| 241 | 288 | assert!(r.conflicted); |
|
| 242 | - | assert!(root.join("new.txt.oxy-conflict-1").exists() && root.join("new.txt.oxy-conflict-2").exists()); |
|
| 289 | + | assert!( |
|
| 290 | + | root.join("new.txt.oxy-conflict-1").exists() |
|
| 291 | + | && root.join("new.txt.oxy-conflict-2").exists() |
|
| 292 | + | ); |
|
| 243 | 293 | assert!(!root.join("new.txt").exists()); |
|
| 244 | 294 | // Candidate numbering follows change-hash order, so pick by content: |
|
| 245 | 295 | // keep the "two" candidate under the real name and drop the other. |
|
| 246 | - | let (keep, drop) = if fs::read(root.join("new.txt.oxy-conflict-1")).unwrap() == b"two\n" { (1, 2) } else { (2, 1) }; |
|
| 296 | + | let (keep, drop) = if fs::read(root.join("new.txt.oxy-conflict-1")).unwrap() == b"two\n" { |
|
| 297 | + | (1, 2) |
|
| 298 | + | } else { |
|
| 299 | + | (2, 1) |
|
| 300 | + | }; |
|
| 247 | 301 | fs::remove_file(root.join(format!("new.txt.oxy-conflict-{drop}"))).unwrap(); |
|
| 248 | - | fs::rename(root.join(format!("new.txt.oxy-conflict-{keep}")), root.join("new.txt")).unwrap(); |
|
| 302 | + | fs::rename( |
|
| 303 | + | root.join(format!("new.txt.oxy-conflict-{keep}")), |
|
| 304 | + | root.join("new.txt"), |
|
| 305 | + | ) |
|
| 306 | + | .unwrap(); |
|
| 249 | 307 | let h = repo.commit("pick two", &[]).unwrap(); |
|
| 250 | - | assert_eq!(repo.store().get_change(&h).unwrap().kind, ChangeKind::Resolution); |
|
| 308 | + | assert_eq!( |
|
| 309 | + | repo.store().get_change(&h).unwrap().kind, |
|
| 310 | + | ChangeKind::Resolution |
|
| 311 | + | ); |
|
| 251 | 312 | // The resolution does not touch keep.txt, so it does not depend on h1; |
|
| 252 | 313 | // the bookmark keeps both as heads and their union is the state. |
|
| 253 | 314 | let heads: BTreeSet<_> = repo.bookmark_heads().unwrap().into_iter().collect(); |
|
| 254 | 315 | assert_eq!(heads, [h1, h].into_iter().collect()); |
|
| 255 | - | let snap = repo.load_state(&repo.bookmark_heads().unwrap()).unwrap().materialize(); |
|
| 316 | + | let snap = repo |
|
| 317 | + | .load_state(&repo.bookmark_heads().unwrap()) |
|
| 318 | + | .unwrap() |
|
| 319 | + | .materialize(); |
|
| 256 | 320 | assert!(!snap.is_conflicted(), "{:?}", snap.tree.conflicts); |
|
| 257 | - | assert_eq!(snap.files.keys().cloned().collect::<Vec<_>>(), ["keep.txt", "new.txt"]); |
|
| 258 | - | assert!(matches!(&snap.files["new.txt"].content, FileContent::Text { bytes, .. } if bytes == b"two\n")); |
|
| 321 | + | assert_eq!( |
|
| 322 | + | snap.files.keys().cloned().collect::<Vec<_>>(), |
|
| 323 | + | ["keep.txt", "new.txt"] |
|
| 324 | + | ); |
|
| 325 | + | assert!( |
|
| 326 | + | matches!(&snap.files["new.txt"].content, FileContent::Text { bytes, .. } if bytes == b"two\n") |
|
| 327 | + | ); |
|
| 259 | 328 | } |
|
| 260 | 329 | ||
| 261 | 330 | #[test] |
|
| … | 5 unchanged lines |
||
| 267 | 336 | write(root, "a.txt", b"a\n"); |
|
| 268 | 337 | let h0 = repo.commit("unsigned", &[]).unwrap(); |
|
| 269 | 338 | assert_eq!(repo.verify_change(&h0).unwrap(), SignatureStatus::Unsigned); |
|
| 270 | - | assert_eq!(repo.store().get_change(&h0).unwrap().author, ObjectRef::ZERO); |
|
| 339 | + | assert_eq!( |
|
| 340 | + | repo.store().get_change(&h0).unwrap().author, |
|
| 341 | + | ObjectRef::ZERO |
|
| 342 | + | ); |
|
| 271 | 343 | ||
| 272 | - | let (key, id) = repo.create_identity("Ada", "[email protected]", IdentityScope::Repo).unwrap(); |
|
| 344 | + | let (key, id) = repo |
|
| 345 | + | .create_identity("Ada", "[email protected]", IdentityScope::Repo) |
|
| 346 | + | .unwrap(); |
|
| 273 | 347 | assert_eq!(id.hash(), key); |
|
| 274 | 348 | assert_eq!(repo.config.author_key, Some(key)); |
|
| 275 | 349 | // Config survives reopen; the secret key is found again. |
|
| … | 8 unchanged lines |
||
| 284 | 358 | assert_eq!(repo.verify_change(&h1).unwrap(), SignatureStatus::Unsigned); |
|
| 285 | 359 | // Signing later works too. |
|
| 286 | 360 | repo.sign_change(&h1).unwrap(); |
|
| 287 | - | assert!(matches!(repo.verify_change(&h1).unwrap(), SignatureStatus::Valid { key: k, .. } if k == key)); |
|
| 361 | + | assert!( |
|
| 362 | + | matches!(repo.verify_change(&h1).unwrap(), SignatureStatus::Valid { key: k, .. } if k == key) |
|
| 363 | + | ); |
|
| 288 | 364 | ||
| 289 | 365 | repo.config.set("core.sign", "change").unwrap(); |
|
| 290 | 366 | repo.save_config().unwrap(); |
|
| 291 | 367 | write(root, "a.txt", b"a\nb\nc\n"); |
|
| 292 | 368 | let h2 = repo.commit("signed", &[]).unwrap(); |
|
| 293 | - | assert_eq!(repo.verify_change(&h2).unwrap(), SignatureStatus::Valid { key, name: "Ada".into(), email: "[email protected]".into(), sealed: false, revoked: false }); |
|
| 369 | + | assert_eq!( |
|
| 370 | + | repo.verify_change(&h2).unwrap(), |
|
| 371 | + | SignatureStatus::Valid { |
|
| 372 | + | key, |
|
| 373 | + | name: "Ada".into(), |
|
| 374 | + | email: "[email protected]".into(), |
|
| 375 | + | sealed: false, |
|
| 376 | + | revoked: false |
|
| 377 | + | } |
|
| 378 | + | ); |
|
| 294 | 379 | ||
| 295 | 380 | // A signature by a key that is not the author or a delegate is rejected. |
|
| 296 | 381 | let other = SecretKey::generate(); |
|
| … | 1 unchanged lines |
||
| 298 | 383 | let other_key = repo.store().put_identity(&other_id).unwrap(); |
|
| 299 | 384 | let forged = other.sign(Domain::Change, &other_key, &h0); |
|
| 300 | 385 | repo.store().put_signature(&h0, &forged).unwrap(); |
|
| 301 | - | assert!(matches!(repo.verify_change(&h0).unwrap(), SignatureStatus::Invalid(m) if m.contains("neither the author"))); |
|
| 386 | + | assert!( |
|
| 387 | + | matches!(repo.verify_change(&h0).unwrap(), SignatureStatus::Invalid(m) if m.contains("neither the author")) |
|
| 388 | + | ); |
|
| 302 | 389 | ||
| 303 | 390 | // Tampering with the stored signature invalidates it. |
|
| 304 | 391 | let mut sig = repo.store().signatures(&h2).unwrap().remove(0); |
|
| 305 | 392 | sig.ed[0] ^= 1; |
|
| 306 | 393 | repo.store().put_signature(&h2, &sig).unwrap(); |
|
| 307 | - | assert!(matches!(repo.verify_change(&h2).unwrap(), SignatureStatus::Invalid(_))); |
|
| 394 | + | assert!(matches!( |
|
| 395 | + | repo.verify_change(&h2).unwrap(), |
|
| 396 | + | SignatureStatus::Invalid(_) |
|
| 397 | + | )); |
|
| 308 | 398 | ||
| 309 | 399 | // Without the secret key, signing at commit fails cleanly. |
|
| 310 | 400 | let no_keys = Repo::open(root).unwrap().with_home(root.join("nowhere")); |
|
| … | 16 unchanged lines |
||
| 327 | 417 | let h2 = repo.commit("second", &[]).unwrap(); |
|
| 328 | 418 | ||
| 329 | 419 | let r = repo.checkout(&[h1], false, "oxy checkout").unwrap(); |
|
| 330 | - | assert_eq!(r, CheckoutReport { written: 2, removed: 1, conflicted: false, notes: vec![] }); |
|
| 420 | + | assert_eq!( |
|
| 421 | + | r, |
|
| 422 | + | CheckoutReport { |
|
| 423 | + | written: 2, |
|
| 424 | + | removed: 1, |
|
| 425 | + | conflicted: false, |
|
| 426 | + | notes: vec![] |
|
| 427 | + | } |
|
| 428 | + | ); |
|
| 331 | 429 | assert_eq!(fs::read(root.join("a.txt")).unwrap(), b"one\n"); |
|
| 332 | 430 | assert_eq!(fs::read(root.join("sub/deep/b.txt")).unwrap(), b"b\n"); |
|
| 333 | 431 | assert!(!root.join("bin.dat").exists()); |
|
| 334 | 432 | let (_, wc) = repo.working_copy().unwrap(); |
|
| 335 | 433 | assert_eq!(wc.deps, vec![h1]); |
|
| 336 | - | assert!(!repo.snapshot("status").unwrap().changed, "checkout leaves a clean working copy"); |
|
| 434 | + | assert!( |
|
| 435 | + | !repo.snapshot("status").unwrap().changed, |
|
| 436 | + | "checkout leaves a clean working copy" |
|
| 437 | + | ); |
|
| 337 | 438 | // The bookmark still points at h2. |
|
| 338 | 439 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h2]); |
|
| 339 | 440 | ||
| 340 | 441 | // Dirty working copy refuses without --discard. |
|
| 341 | 442 | write(root, "a.txt", b"dirty\n"); |
|
| 342 | 443 | assert!(repo.checkout(&[h2], false, "oxy checkout").is_err()); |
|
| 343 | - | let r = repo.checkout(&[h2], true, "oxy checkout --discard").unwrap(); |
|
| 444 | + | let r = repo |
|
| 445 | + | .checkout(&[h2], true, "oxy checkout --discard") |
|
| 446 | + | .unwrap(); |
|
| 344 | 447 | assert_eq!(fs::read(root.join("a.txt")).unwrap(), b"two\n"); |
|
| 345 | 448 | assert_eq!(fs::read(root.join("bin.dat")).unwrap(), [0, 1, 2, 3]); |
|
| 346 | 449 | assert!(!root.join("sub").exists(), "empty directories are pruned"); |
|
| … | 35 unchanged lines |
||
| 382 | 485 | let origin_url = origin_path.to_string_lossy().to_string(); |
|
| 383 | 486 | ||
| 384 | 487 | // A clones, creates an identity (default core.sign = seal), commits, pushes. |
|
| 385 | - | let mut a = Repo::clone(&origin_url, &root.join("a")).unwrap().with_home(root.join("home-a")); |
|
| 488 | + | let mut a = Repo::clone(&origin_url, &root.join("a")) |
|
| 489 | + | .unwrap() |
|
| 490 | + | .with_home(root.join("home-a")); |
|
| 386 | 491 | assert_eq!(a.id().unwrap(), origin.id().unwrap()); |
|
| 387 | - | let (key_a, _) = a.create_identity("Ada", "[email protected]", IdentityScope::Repo).unwrap(); |
|
| 492 | + | let (key_a, _) = a |
|
| 493 | + | .create_identity("Ada", "[email protected]", IdentityScope::Repo) |
|
| 494 | + | .unwrap(); |
|
| 388 | 495 | write(&root.join("a"), "f.txt", b"a\n"); |
|
| 389 | 496 | let h1 = a.commit("first", &[]).unwrap(); |
|
| 390 | 497 | let results = a.push("origin", false).unwrap(); |
|
| 391 | 498 | assert!(results[0].ok, "{}", results[0].reason); |
|
| 392 | 499 | assert_eq!(origin.bookmark_heads().unwrap(), vec![h1]); |
|
| 393 | 500 | // The change was sealed at push and the reference attested. |
|
| 394 | - | assert!(matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key, sealed: true, .. } if key == key_a)); |
|
| 395 | - | assert_eq!(origin.verify_change(&h1).unwrap(), SignatureStatus::Unsigned); |
|
| 501 | + | assert!( |
|
| 502 | + | matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key, sealed: true, .. } if key == key_a) |
|
| 503 | + | ); |
|
| 504 | + | assert_eq!( |
|
| 505 | + | origin.verify_change(&h1).unwrap(), |
|
| 506 | + | SignatureStatus::Unsigned |
|
| 507 | + | ); |
|
| 396 | 508 | let (_, att) = origin.latest_ref_attest("main").unwrap().unwrap(); |
|
| 397 | 509 | assert!(matches!(att.attest.body, AttestBody::Ref { serial: 1, .. })); |
|
| 398 | 510 | let again = a.push("origin", false).unwrap(); |
|
| 399 | 511 | assert!(again[0].ok && again[0].reason.contains("up to date")); |
|
| 400 | 512 | let (_, att) = origin.latest_ref_attest("main").unwrap().unwrap(); |
|
| 401 | - | assert!(matches!(att.attest.body, AttestBody::Ref { serial: 1, .. }), "a no-op push does not attest again"); |
|
| 513 | + | assert!( |
|
| 514 | + | matches!(att.attest.body, AttestBody::Ref { serial: 1, .. }), |
|
| 515 | + | "a no-op push does not attest again" |
|
| 516 | + | ); |
|
| 402 | 517 | ||
| 403 | 518 | // B clones and sees the file and the seal. |
|
| 404 | 519 | let b = Repo::clone(&origin_url, &root.join("b")).unwrap(); |
|
| 405 | 520 | assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"a\n"); |
|
| 406 | 521 | assert_eq!(b.bookmark_heads().unwrap(), vec![h1]); |
|
| 407 | - | assert!(matches!(b.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { sealed: true, .. })); |
|
| 522 | + | assert!(matches!( |
|
| 523 | + | b.authenticate_change(&h1).unwrap(), |
|
| 524 | + | SignatureStatus::Valid { sealed: true, .. } |
|
| 525 | + | )); |
|
| 408 | 526 | ||
| 409 | 527 | // B (no identity) commits and pushes unsigned; the unverified origin accepts. |
|
| 410 | 528 | write(&root.join("b"), "f.txt", b"a\nb\n"); |
|
| … | 38 unchanged lines |
||
| 449 | 567 | assert!(text.contains("<<<<<<< oxy conflict"), "{text}"); |
|
| 450 | 568 | write(&root.join("b"), "f.txt", b"a\nAB\n"); |
|
| 451 | 569 | let hr = b.commit("resolve", &[]).unwrap(); |
|
| 452 | - | assert_eq!(b.store().get_change(&hr).unwrap().kind, ChangeKind::Resolution); |
|
| 570 | + | assert_eq!( |
|
| 571 | + | b.store().get_change(&hr).unwrap().kind, |
|
| 572 | + | ChangeKind::Resolution |
|
| 573 | + | ); |
|
| 453 | 574 | assert!(b.push("origin", false).unwrap()[0].ok); |
|
| 454 | 575 | a.pull("origin").unwrap(); |
|
| 455 | 576 | assert_eq!(fs::read(root.join("a/f.txt")).unwrap(), b"a\nAB\n"); |
|
| … | 12 unchanged lines |
||
| 468 | 589 | let root = dir.path(); |
|
| 469 | 590 | let origin = Repo::init(&root.join("origin")).unwrap(); |
|
| 470 | 591 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 471 | - | let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home")); |
|
| 472 | - | let (key, _) = a.create_identity("Ada", "a@b", IdentityScope::Repo).unwrap(); |
|
| 592 | + | let mut a = Repo::clone(&url, &root.join("a")) |
|
| 593 | + | .unwrap() |
|
| 594 | + | .with_home(root.join("home")); |
|
| 595 | + | let (key, _) = a |
|
| 596 | + | .create_identity("Ada", "a@b", IdentityScope::Repo) |
|
| 597 | + | .unwrap(); |
|
| 473 | 598 | write(&root.join("a"), "f.txt", b"x\n"); |
|
| 474 | 599 | let h1 = a.commit("one", &[]).unwrap(); |
|
| 475 | 600 | assert!(a.push("origin", false).unwrap()[0].ok); |
|
| 476 | 601 | let empty = origin.store().put_state(&StateObject::new(vec![])).unwrap(); |
|
| 477 | - | let s1 = origin.store().put_state(&StateObject::new(vec![h1])).unwrap(); |
|
| 602 | + | let s1 = origin |
|
| 603 | + | .store() |
|
| 604 | + | .put_state(&StateObject::new(vec![h1])) |
|
| 605 | + | .unwrap(); |
|
| 478 | 606 | ||
| 479 | 607 | // Missing head. |
|
| 480 | - | let ghost = origin.store().put_state(&StateObject::new(vec![ObjectRef([9; 32])])).unwrap(); |
|
| 481 | - | let r = origin.receive_push(Bundle::default(), vec![RefUpdate { name: "main".into(), old_state: s1, new_state: ghost, force: false, attestation: None }], None, vec![]).unwrap(); |
|
| 482 | - | assert!(!r[0].ok && r[0].reason.contains("missing"), "{}", r[0].reason); |
|
| 608 | + | let ghost = origin |
|
| 609 | + | .store() |
|
| 610 | + | .put_state(&StateObject::new(vec![ObjectRef([9; 32])])) |
|
| 611 | + | .unwrap(); |
|
| 612 | + | let r = origin |
|
| 613 | + | .receive_push( |
|
| 614 | + | Bundle::default(), |
|
| 615 | + | vec![RefUpdate { |
|
| 616 | + | name: "main".into(), |
|
| 617 | + | old_state: s1, |
|
| 618 | + | new_state: ghost, |
|
| 619 | + | force: false, |
|
| 620 | + | attestation: None, |
|
| 621 | + | }], |
|
| 622 | + | None, |
|
| 623 | + | vec![], |
|
| 624 | + | ) |
|
| 625 | + | .unwrap(); |
|
| 626 | + | assert!( |
|
| 627 | + | !r[0].ok && r[0].reason.contains("missing"), |
|
| 628 | + | "{}", |
|
| 629 | + | r[0].reason |
|
| 630 | + | ); |
|
| 483 | 631 | ||
| 484 | 632 | // Stale old_state. |
|
| 485 | - | let r = origin.receive_push(Bundle::default(), vec![RefUpdate { name: "main".into(), old_state: empty, new_state: s1, force: false, attestation: None }], None, vec![]).unwrap(); |
|
| 633 | + | let r = origin |
|
| 634 | + | .receive_push( |
|
| 635 | + | Bundle::default(), |
|
| 636 | + | vec![RefUpdate { |
|
| 637 | + | name: "main".into(), |
|
| 638 | + | old_state: empty, |
|
| 639 | + | new_state: s1, |
|
| 640 | + | force: false, |
|
| 641 | + | attestation: None, |
|
| 642 | + | }], |
|
| 643 | + | None, |
|
| 644 | + | vec![], |
|
| 645 | + | ) |
|
| 646 | + | .unwrap(); |
|
| 486 | 647 | assert!(!r[0].ok && r[0].reason.contains("stale"), "{}", r[0].reason); |
|
| 487 | 648 | ||
| 488 | 649 | // Non-fast-forward without force is refused; with force it passes. |
|
| 489 | - | let r = origin.receive_push(Bundle::default(), vec![RefUpdate { name: "main".into(), old_state: s1, new_state: empty, force: false, attestation: None }], None, vec![]).unwrap(); |
|
| 490 | - | assert!(!r[0].ok && r[0].reason.contains("fast-forward"), "{}", r[0].reason); |
|
| 491 | - | let r = origin.receive_push(Bundle::default(), vec![RefUpdate { name: "main".into(), old_state: s1, new_state: empty, force: true, attestation: None }], None, vec![]).unwrap(); |
|
| 650 | + | let r = origin |
|
| 651 | + | .receive_push( |
|
| 652 | + | Bundle::default(), |
|
| 653 | + | vec![RefUpdate { |
|
| 654 | + | name: "main".into(), |
|
| 655 | + | old_state: s1, |
|
| 656 | + | new_state: empty, |
|
| 657 | + | force: false, |
|
| 658 | + | attestation: None, |
|
| 659 | + | }], |
|
| 660 | + | None, |
|
| 661 | + | vec![], |
|
| 662 | + | ) |
|
| 663 | + | .unwrap(); |
|
| 664 | + | assert!( |
|
| 665 | + | !r[0].ok && r[0].reason.contains("fast-forward"), |
|
| 666 | + | "{}", |
|
| 667 | + | r[0].reason |
|
| 668 | + | ); |
|
| 669 | + | let r = origin |
|
| 670 | + | .receive_push( |
|
| 671 | + | Bundle::default(), |
|
| 672 | + | vec![RefUpdate { |
|
| 673 | + | name: "main".into(), |
|
| 674 | + | old_state: s1, |
|
| 675 | + | new_state: empty, |
|
| 676 | + | force: true, |
|
| 677 | + | attestation: None, |
|
| 678 | + | }], |
|
| 679 | + | None, |
|
| 680 | + | vec![], |
|
| 681 | + | ) |
|
| 682 | + | .unwrap(); |
|
| 492 | 683 | assert!(r[0].ok); |
|
| 493 | 684 | assert_eq!(origin.bookmark_heads().unwrap(), vec![]); |
|
| 494 | 685 | ||
| 495 | 686 | // Attestation with a stale serial is a rollback and is refused. |
|
| 496 | - | let sa = a.sign_attest(&Attest { repo: a.id().unwrap(), hlc: Hlc::now(), body: AttestBody::Ref { name: "main".into(), old_state: empty, new_state: s1, serial: 1 } }).unwrap(); |
|
| 497 | - | let r = origin.receive_push(Bundle::default(), vec![RefUpdate { name: "main".into(), old_state: empty, new_state: s1, force: false, attestation: Some(sa.clone()) }], None, vec![]).unwrap(); |
|
| 498 | - | assert!(!r[0].ok && r[0].reason.contains("serial"), "{}", r[0].reason); |
|
| 687 | + | let sa = a |
|
| 688 | + | .sign_attest(&Attest { |
|
| 689 | + | repo: a.id().unwrap(), |
|
| 690 | + | hlc: Hlc::now(), |
|
| 691 | + | body: AttestBody::Ref { |
|
| 692 | + | name: "main".into(), |
|
| 693 | + | old_state: empty, |
|
| 694 | + | new_state: s1, |
|
| 695 | + | serial: 1, |
|
| 696 | + | }, |
|
| 697 | + | }) |
|
| 698 | + | .unwrap(); |
|
| 699 | + | let r = origin |
|
| 700 | + | .receive_push( |
|
| 701 | + | Bundle::default(), |
|
| 702 | + | vec![RefUpdate { |
|
| 703 | + | name: "main".into(), |
|
| 704 | + | old_state: empty, |
|
| 705 | + | new_state: s1, |
|
| 706 | + | force: false, |
|
| 707 | + | attestation: Some(sa.clone()), |
|
| 708 | + | }], |
|
| 709 | + | None, |
|
| 710 | + | vec![], |
|
| 711 | + | ) |
|
| 712 | + | .unwrap(); |
|
| 713 | + | assert!( |
|
| 714 | + | !r[0].ok && r[0].reason.contains("serial"), |
|
| 715 | + | "{}", |
|
| 716 | + | r[0].reason |
|
| 717 | + | ); |
|
| 499 | 718 | // An attestation for a different update is refused too. |
|
| 500 | - | let sa2 = a.sign_attest(&Attest { repo: a.id().unwrap(), hlc: Hlc::now(), body: AttestBody::Ref { name: "main".into(), old_state: s1, new_state: empty, serial: 5 } }).unwrap(); |
|
| 501 | - | let r = origin.receive_push(Bundle::default(), vec![RefUpdate { name: "main".into(), old_state: empty, new_state: s1, force: false, attestation: Some(sa2) }], None, vec![]).unwrap(); |
|
| 502 | - | assert!(!r[0].ok && r[0].reason.contains("describe"), "{}", r[0].reason); |
|
| 719 | + | let sa2 = a |
|
| 720 | + | .sign_attest(&Attest { |
|
| 721 | + | repo: a.id().unwrap(), |
|
| 722 | + | hlc: Hlc::now(), |
|
| 723 | + | body: AttestBody::Ref { |
|
| 724 | + | name: "main".into(), |
|
| 725 | + | old_state: s1, |
|
| 726 | + | new_state: empty, |
|
| 727 | + | serial: 5, |
|
| 728 | + | }, |
|
| 729 | + | }) |
|
| 730 | + | .unwrap(); |
|
| 731 | + | let r = origin |
|
| 732 | + | .receive_push( |
|
| 733 | + | Bundle::default(), |
|
| 734 | + | vec![RefUpdate { |
|
| 735 | + | name: "main".into(), |
|
| 736 | + | old_state: empty, |
|
| 737 | + | new_state: s1, |
|
| 738 | + | force: false, |
|
| 739 | + | attestation: Some(sa2), |
|
| 740 | + | }], |
|
| 741 | + | None, |
|
| 742 | + | vec![], |
|
| 743 | + | ) |
|
| 744 | + | .unwrap(); |
|
| 745 | + | assert!( |
|
| 746 | + | !r[0].ok && r[0].reason.contains("describe"), |
|
| 747 | + | "{}", |
|
| 748 | + | r[0].reason |
|
| 749 | + | ); |
|
| 503 | 750 | let _ = key; |
|
| 504 | 751 | } |
|
| 505 | 752 | ||
| … | 8 unchanged lines |
||
| 514 | 761 | ||
| 515 | 762 | // Identity at user scope: the repository file stays free of author keys. |
|
| 516 | 763 | let mut a = Repo::init(&root.join("a")).unwrap().with_home(&home); |
|
| 517 | - | let (key, _) = a.create_identity("Ada", "[email protected]", IdentityScope::User).unwrap(); |
|
| 764 | + | let (key, _) = a |
|
| 765 | + | .create_identity("Ada", "[email protected]", IdentityScope::User) |
|
| 766 | + | .unwrap(); |
|
| 518 | 767 | assert!(!a.config.repo_layer().contains_key("author.key")); |
|
| 519 | 768 | assert_eq!(a.config.author_key, Some(key)); |
|
| 520 | 769 | assert!(home.join("config.toml").exists()); |
|
| … | 6 unchanged lines |
||
| 527 | 776 | let again = Repo::open(&root.join("a")).unwrap().with_home(&home); |
|
| 528 | 777 | assert!(!again.config.sign_changes); |
|
| 529 | 778 | assert_eq!(again.config.author_key, Some(key)); |
|
| 530 | - | assert_eq!(read_config_file(&root.join("a/.oxy/config")).unwrap().get("core.sign").map(String::as_str), Some("seal")); |
|
| 779 | + | assert_eq!( |
|
| 780 | + | read_config_file(&root.join("a/.oxy/config")) |
|
| 781 | + | .unwrap() |
|
| 782 | + | .get("core.sign") |
|
| 783 | + | .map(String::as_str), |
|
| 784 | + | Some("seal") |
|
| 785 | + | ); |
|
| 531 | 786 | ||
| 532 | 787 | // A fresh clone made by this user signs with the user-level identity, |
|
| 533 | 788 | // even though the clone's store has never seen the identity object. |
|
| … | 1 unchanged lines |
||
| 535 | 790 | assert!(b.config.sign_changes); |
|
| 536 | 791 | write(&root.join("b"), "f.txt", b"f\n"); |
|
| 537 | 792 | let h = b.commit("signed from a clone", &[]).unwrap(); |
|
| 538 | - | assert!(matches!(b.verify_change(&h).unwrap(), SignatureStatus::Valid { key: k, .. } if k == key)); |
|
| 793 | + | assert!( |
|
| 794 | + | matches!(b.verify_change(&h).unwrap(), SignatureStatus::Valid { key: k, .. } if k == key) |
|
| 795 | + | ); |
|
| 539 | 796 | assert!(b.push("origin", false).unwrap()[0].ok); |
|
| 540 | - | assert!(matches!(origin.verify_change(&h).unwrap(), SignatureStatus::Valid { .. })); |
|
| 797 | + | assert!(matches!( |
|
| 798 | + | origin.verify_change(&h).unwrap(), |
|
| 799 | + | SignatureStatus::Valid { .. } |
|
| 800 | + | )); |
|
| 541 | 801 | } |
|
| 542 | 802 | ||
| 543 | 803 | #[test] |
|
| … | 10 unchanged lines |
||
| 554 | 814 | let mut v1 = Format1View::default(); |
|
| 555 | 815 | v1.bookmarks.insert("main".into(), vec![]); |
|
| 556 | 816 | v1.workspaces.insert(repo.workspace(), wc_hash); |
|
| 557 | - | let init = Format1Op { parents: vec![], hlc: Hlc::default(), command: "oxy init".into(), view: v1.clone() }; |
|
| 817 | + | let init = Format1Op { |
|
| 818 | + | parents: vec![], |
|
| 819 | + | hlc: Hlc::default(), |
|
| 820 | + | command: "oxy init".into(), |
|
| 821 | + | view: v1.clone(), |
|
| 822 | + | }; |
|
| 558 | 823 | let init_ref = repo.store().put(ObjectType::Op, &init.encode()).unwrap(); |
|
| 559 | 824 | v1.bookmarks.insert("main".into(), vec![h1]); |
|
| 560 | - | let commit = Format1Op { parents: vec![init_ref], hlc: Hlc::default(), command: "oxy commit".into(), view: v1 }; |
|
| 825 | + | let commit = Format1Op { |
|
| 826 | + | parents: vec![init_ref], |
|
| 827 | + | hlc: Hlc::default(), |
|
| 828 | + | command: "oxy commit".into(), |
|
| 829 | + | view: v1, |
|
| 830 | + | }; |
|
| 561 | 831 | let commit_ref = repo.store().put(ObjectType::Op, &commit.encode()).unwrap(); |
|
| 562 | 832 | write_atomic(&root.join(".oxy/ops/head"), commit_ref.to_hex().as_bytes()).unwrap(); |
|
| 563 | 833 | write_atomic(&root.join(".oxy/format"), FORMAT1_LINE.as_bytes()).unwrap(); |
|
| 564 | 834 | drop(repo); |
|
| 565 | 835 | ||
| 566 | 836 | let repo = Repo::open(root).unwrap(); |
|
| 567 | - | assert_eq!(fs::read_to_string(root.join(".oxy/format")).unwrap(), FORMAT_LINE); |
|
| 837 | + | assert_eq!( |
|
| 838 | + | fs::read_to_string(root.join(".oxy/format")).unwrap(), |
|
| 839 | + | FORMAT_LINE |
|
| 840 | + | ); |
|
| 568 | 841 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h1]); |
|
| 569 | 842 | let ops = repo.op_log().unwrap(); |
|
| 570 | - | assert_eq!(ops.iter().map(|(_, o)| o.command.as_str()).collect::<Vec<_>>(), ["oxy commit", "oxy init"]); |
|
| 843 | + | assert_eq!( |
|
| 844 | + | ops.iter() |
|
| 845 | + | .map(|(_, o)| o.command.as_str()) |
|
| 846 | + | .collect::<Vec<_>>(), |
|
| 847 | + | ["oxy commit", "oxy init"] |
|
| 848 | + | ); |
|
| 571 | 849 | assert_eq!(repo.heads_of(&ops[1].1.view, "main").unwrap(), vec![]); |
|
| 572 | 850 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 573 | 851 | // Reopening again is a no-op. |
|
| … | 19 unchanged lines |
||
| 593 | 871 | assert_eq!(repo.bookmark_heads().unwrap(), vec![]); |
|
| 594 | 872 | let s = repo.snapshot("oxy status").unwrap(); |
|
| 595 | 873 | let paths: Vec<_> = s.statuses.iter().map(|p| p.path.as_str()).collect(); |
|
| 596 | - | assert_eq!(paths, ["a.txt", "b.txt"], "both files are back in the working copy"); |
|
| 874 | + | assert_eq!( |
|
| 875 | + | paths, |
|
| 876 | + | ["a.txt", "b.txt"], |
|
| 877 | + | "both files are back in the working copy" |
|
| 878 | + | ); |
|
| 597 | 879 | assert!(repo.store().get_change(&h1).unwrap().deps.is_empty()); |
|
| 598 | 880 | // Only snapshots remain above `init`, so there is nothing left to undo. |
|
| 599 | 881 | assert_eq!(repo.last_command().unwrap().command, "oxy init"); |
|
| … | 42 unchanged lines |
||
| 642 | 924 | use std::os::unix::fs::PermissionsExt; |
|
| 643 | 925 | fs::set_permissions(root.join("run.sh"), fs::Permissions::from_mode(0o755)).unwrap(); |
|
| 644 | 926 | let s = repo.snapshot("status").unwrap(); |
|
| 645 | - | assert_eq!(statuses(&s), vec![("run.sh".into(), ChangeStatus::ModeChanged)]); |
|
| 927 | + | assert_eq!( |
|
| 928 | + | statuses(&s), |
|
| 929 | + | vec![("run.sh".into(), ChangeStatus::ModeChanged)] |
|
| 930 | + | ); |
|
| 646 | 931 | let h = repo.commit("exec", &[]).unwrap(); |
|
| 647 | 932 | let snap = repo.load_state(&[h]).unwrap().materialize(); |
|
| 648 | 933 | assert!(snap.files["run.sh"].mode.exec); |
|
| … | 7 unchanged lines |
||
| 656 | 941 | let h = repo.commit("text", &[]).unwrap(); |
|
| 657 | 942 | let snap = repo.load_state(&[h]).unwrap().materialize(); |
|
| 658 | 943 | assert!(!snap.files["run.sh"].mode.opaque); |
|
| 659 | - | assert!(matches!(&snap.files["run.sh"].content, FileContent::Text { bytes, .. } if bytes == b"text again\n")); |
|
| 944 | + | assert!( |
|
| 945 | + | matches!(&snap.files["run.sh"].content, FileContent::Text { bytes, .. } if bytes == b"text again\n") |
|
| 946 | + | ); |
|
| 660 | 947 | assert!(!snap.is_conflicted()); |
|
| 661 | 948 | } |
|
| 662 | 949 | ||
| … | 6 unchanged lines |
||
| 669 | 956 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 670 | 957 | ||
| 671 | 958 | // A is the root. Before the policy, an unsigned push from B is fine. |
|
| 672 | - | let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home-a")); |
|
| 673 | - | let (key_a, _) = a.create_identity("Ada", "[email protected]", IdentityScope::User).unwrap(); |
|
| 674 | - | let mut b = Repo::clone(&url, &root.join("b")).unwrap().with_home(root.join("home-b")); |
|
| 959 | + | let mut a = Repo::clone(&url, &root.join("a")) |
|
| 960 | + | .unwrap() |
|
| 961 | + | .with_home(root.join("home-a")); |
|
| 962 | + | let (key_a, _) = a |
|
| 963 | + | .create_identity("Ada", "[email protected]", IdentityScope::User) |
|
| 964 | + | .unwrap(); |
|
| 965 | + | let mut b = Repo::clone(&url, &root.join("b")) |
|
| 966 | + | .unwrap() |
|
| 967 | + | .with_home(root.join("home-b")); |
|
| 675 | 968 | write(&root.join("b"), "b.txt", b"b\n"); |
|
| 676 | 969 | b.commit("unsigned before policy", &[]).unwrap(); |
|
| 677 | 970 | assert!(b.push("origin", false).unwrap()[0].ok); |
|
| … | 10 unchanged lines |
||
| 688 | 981 | ||
| 689 | 982 | // B pulls the policy. Without an identity B cannot push at all. |
|
| 690 | 983 | b.pull("origin").unwrap(); |
|
| 691 | - | assert!(b.current_policy().unwrap().is_some(), "the policy arrived with the fetch"); |
|
| 984 | + | assert!( |
|
| 985 | + | b.current_policy().unwrap().is_some(), |
|
| 986 | + | "the policy arrived with the fetch" |
|
| 987 | + | ); |
|
| 692 | 988 | assert!(b.push("origin", false).is_err()); |
|
| 693 | 989 | // With an identity the policy does not list, the push is rejected by origin. |
|
| 694 | - | let (key_b, id_b) = b.create_identity("Bob", "[email protected]", IdentityScope::User).unwrap(); |
|
| 990 | + | let (key_b, id_b) = b |
|
| 991 | + | .create_identity("Bob", "[email protected]", IdentityScope::User) |
|
| 992 | + | .unwrap(); |
|
| 695 | 993 | write(&root.join("b"), "b.txt", b"b2\n"); |
|
| 696 | 994 | let h_b2 = b.commit("b2 as Bob", &[]).unwrap(); |
|
| 697 | 995 | assert_eq!(b.store().get_change(&h_b2).unwrap().author, key_b); |
|
| 698 | 996 | let r = b.push("origin", false).unwrap(); |
|
| 699 | - | assert!(!r[0].ok && r[0].reason.contains("may not update"), "{:?}", r); |
|
| 997 | + | assert!( |
|
| 998 | + | !r[0].ok && r[0].reason.contains("may not update"), |
|
| 999 | + | "{:?}", |
|
| 1000 | + | r |
|
| 1001 | + | ); |
|
| 700 | 1002 | ||
| 701 | 1003 | // A grants B (identity handed over as a file) and publishes the policy. |
|
| 702 | 1004 | let sp = a.policy_grant(id_b.clone(), None).unwrap(); |
|
| … | 1 unchanged lines |
||
| 704 | 1006 | let r = a.push("origin", false).unwrap(); |
|
| 705 | 1007 | assert!(r.iter().any(|x| x.name == "policy" && x.ok), "{r:?}"); |
|
| 706 | 1008 | // B must fetch the new policy before pushing. |
|
| 707 | - | assert!(b.push("origin", false).unwrap_or_else(|e| vec![RefResult { name: "main".into(), ok: false, reason: e.to_string() }]).iter().all(|x| !x.ok)); |
|
| 1009 | + | assert!(b |
|
| 1010 | + | .push("origin", false) |
|
| 1011 | + | .unwrap_or_else(|e| vec![RefResult { |
|
| 1012 | + | name: "main".into(), |
|
| 1013 | + | ok: false, |
|
| 1014 | + | reason: e.to_string() |
|
| 1015 | + | }]) |
|
| 1016 | + | .iter() |
|
| 1017 | + | .all(|x| !x.ok)); |
|
| 708 | 1018 | b.pull("origin").unwrap(); |
|
| 709 | 1019 | assert_eq!(b.current_policy().unwrap().unwrap().1.policy.serial, 2); |
|
| 710 | 1020 | let r = b.push("origin", false).unwrap(); |
|
| … | 3 unchanged lines |
||
| 714 | 1024 | for h in &heads { |
|
| 715 | 1025 | let c = origin.store().get_change(h).unwrap(); |
|
| 716 | 1026 | if c.author == key_b { |
|
| 717 | - | origin.change_allowed(h, &origin.current_policy().unwrap().unwrap().1.policy, "main").unwrap(); |
|
| 1027 | + | origin |
|
| 1028 | + | .change_allowed( |
|
| 1029 | + | h, |
|
| 1030 | + | &origin.current_policy().unwrap().unwrap().1.policy, |
|
| 1031 | + | "main", |
|
| 1032 | + | ) |
|
| 1033 | + | .unwrap(); |
|
| 718 | 1034 | } |
|
| 719 | 1035 | } |
|
| 720 | 1036 | ||
| … | 3 unchanged lines |
||
| 724 | 1040 | forged.roots = vec![key_b]; |
|
| 725 | 1041 | let fh = forged.hash(); |
|
| 726 | 1042 | let (_, sk_b) = b.secret_key().unwrap().unwrap(); |
|
| 727 | - | let forged = SignedPolicy { policy: forged, sigs: vec![sk_b.sign(Domain::Policy, &key_b, &fh)] }; |
|
| 728 | - | let r = origin.receive_push(Bundle::default(), vec![], Some(forged), vec![]).unwrap(); |
|
| 1043 | + | let forged = SignedPolicy { |
|
| 1044 | + | policy: forged, |
|
| 1045 | + | sigs: vec![sk_b.sign(Domain::Policy, &key_b, &fh)], |
|
| 1046 | + | }; |
|
| 1047 | + | let r = origin |
|
| 1048 | + | .receive_push(Bundle::default(), vec![], Some(forged), vec![]) |
|
| 1049 | + | .unwrap(); |
|
| 729 | 1050 | assert!(!r[0].ok && r[0].reason.contains("root"), "{:?}", r); |
|
| 730 | 1051 | let stale = a.current_policy().unwrap().unwrap().1; |
|
| 731 | 1052 | let mut old = stale.clone(); |
|
| 732 | 1053 | old.policy.serial = 1; |
|
| 733 | - | let r = origin.receive_push(Bundle::default(), vec![], Some(old), vec![]).unwrap(); |
|
| 1054 | + | let r = origin |
|
| 1055 | + | .receive_push(Bundle::default(), vec![], Some(old), vec![]) |
|
| 1056 | + | .unwrap(); |
|
| 734 | 1057 | assert!(!r[0].ok && r[0].reason.contains("serial"), "{:?}", r); |
|
| 735 | 1058 | ||
| 736 | 1059 | // A revokes B; after B pulls the policy, B's pushes are refused again. |
|
| … | 8 unchanged lines |
||
| 745 | 1068 | assert!(!r[0].ok, "{:?}", r); |
|
| 746 | 1069 | ||
| 747 | 1070 | // A clone by a third party receives and verifies everything. |
|
| 748 | - | let c = Repo::clone(&url, &root.join("c")).unwrap().with_home(root.join("home-c")); |
|
| 1071 | + | let c = Repo::clone(&url, &root.join("c")) |
|
| 1072 | + | .unwrap() |
|
| 1073 | + | .with_home(root.join("home-c")); |
|
| 749 | 1074 | assert_eq!(c.current_policy().unwrap().unwrap().1.policy.serial, 3); |
|
| 750 | - | assert!(c.current_policy().unwrap().unwrap().1.policy.roots.contains(&key_a)); |
|
| 1075 | + | assert!(c |
|
| 1076 | + | .current_policy() |
|
| 1077 | + | .unwrap() |
|
| 1078 | + | .unwrap() |
|
| 1079 | + | .1 |
|
| 1080 | + | .policy |
|
| 1081 | + | .roots |
|
| 1082 | + | .contains(&key_a)); |
|
| 751 | 1083 | assert!(!c.snapshot("status").unwrap().changed); |
|
| 752 | 1084 | } |
|
| 753 | 1085 | ||
| … | 3 unchanged lines |
||
| 757 | 1089 | let dir = tempfile::tempdir().unwrap(); |
|
| 758 | 1090 | let root = dir.path(); |
|
| 759 | 1091 | let mut repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 760 | - | repo.create_identity("Ada", "a@b", IdentityScope::User).unwrap(); |
|
| 1092 | + | repo.create_identity("Ada", "a@b", IdentityScope::User) |
|
| 1093 | + | .unwrap(); |
|
| 761 | 1094 | repo.set_user_config("core.sign", "change").unwrap(); |
|
| 762 | 1095 | write(root, "a.txt", b"one\n"); |
|
| 763 | 1096 | let h1 = repo.commit("one", &[]).unwrap(); |
|
| … | 32 unchanged lines |
||
| 796 | 1129 | } |
|
| 797 | 1130 | // Everything still works from the pruned, packed store. |
|
| 798 | 1131 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h2]); |
|
| 799 | - | assert!(matches!(repo.verify_change(&h2).unwrap(), SignatureStatus::Valid { .. })); |
|
| 1132 | + | assert!(matches!( |
|
| 1133 | + | repo.verify_change(&h2).unwrap(), |
|
| 1134 | + | SignatureStatus::Valid { .. } |
|
| 1135 | + | )); |
|
| 800 | 1136 | assert_eq!(repo.log().unwrap().len(), 3); |
|
| 801 | 1137 | repo.checkout(&[h1], false, "checkout").unwrap(); |
|
| 802 | 1138 | assert_eq!(fs::read(root.join("a.txt")).unwrap(), b"one\n"); |
|
| … | 17 unchanged lines |
||
| 820 | 1156 | let dir = tempfile::tempdir().unwrap(); |
|
| 821 | 1157 | let root = dir.path(); |
|
| 822 | 1158 | let mut repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 823 | - | repo.create_identity("Ada", "a@b", IdentityScope::User).unwrap(); |
|
| 1159 | + | repo.create_identity("Ada", "a@b", IdentityScope::User) |
|
| 1160 | + | .unwrap(); |
|
| 824 | 1161 | repo.set_user_config("core.sign", "change").unwrap(); |
|
| 825 | 1162 | write(root, "a.txt", b"one\n"); |
|
| 826 | 1163 | let h1 = repo.commit("first", &[]).unwrap(); |
|
| … | 5 unchanged lines |
||
| 832 | 1169 | let h2b = repo.describe(&h2, "second").unwrap(); |
|
| 833 | 1170 | assert_ne!(h2b, h2); |
|
| 834 | 1171 | let c = repo.store().get_change(&h2b).unwrap(); |
|
| 835 | - | assert_eq!((c.change_id, c.message.as_str(), c.deps.clone()), (id2, "second", vec![h1])); |
|
| 1172 | + | assert_eq!( |
|
| 1173 | + | (c.change_id, c.message.as_str(), c.deps.clone()), |
|
| 1174 | + | (id2, "second", vec![h1]) |
|
| 1175 | + | ); |
|
| 836 | 1176 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h2b]); |
|
| 837 | - | assert!(matches!(repo.verify_change(&h2b).unwrap(), SignatureStatus::Valid { .. })); |
|
| 1177 | + | assert!(matches!( |
|
| 1178 | + | repo.verify_change(&h2b).unwrap(), |
|
| 1179 | + | SignatureStatus::Valid { .. } |
|
| 1180 | + | )); |
|
| 838 | 1181 | let s = repo.snapshot("status").unwrap(); |
|
| 839 | 1182 | assert!(s.statuses.is_empty(), "{:?}", s.statuses); |
|
| 840 | - | assert_eq!(repo.resolve_change(&id2.short(8)).unwrap(), h2b, "the old object is no longer reachable from the log"); |
|
| 1183 | + | assert_eq!( |
|
| 1184 | + | repo.resolve_change(&id2.short(8)).unwrap(), |
|
| 1185 | + | h2b, |
|
| 1186 | + | "the old object is no longer reachable from the log" |
|
| 1187 | + | ); |
|
| 841 | 1188 | // Describing a non-head rewrites its dependents (h2b → h2c') and the working copy. |
|
| 842 | 1189 | let h1b = repo.describe(&h1, "first, described").unwrap(); |
|
| 843 | 1190 | assert_ne!(h1b, h1); |
|
| … | 1 unchanged lines |
||
| 845 | 1192 | assert_eq!(heads.len(), 1); |
|
| 846 | 1193 | let top = repo.store().get_change(&heads[0]).unwrap(); |
|
| 847 | 1194 | assert_eq!((top.change_id, top.deps.clone()), (id2, vec![h1b])); |
|
| 848 | - | assert!(matches!(repo.verify_change(&heads[0]).unwrap(), SignatureStatus::Valid { .. }), "rewritten own change is re-signed"); |
|
| 1195 | + | assert!( |
|
| 1196 | + | matches!( |
|
| 1197 | + | repo.verify_change(&heads[0]).unwrap(), |
|
| 1198 | + | SignatureStatus::Valid { .. } |
|
| 1199 | + | ), |
|
| 1200 | + | "rewritten own change is re-signed" |
|
| 1201 | + | ); |
|
| 849 | 1202 | let (_, wc) = repo.working_copy().unwrap(); |
|
| 850 | - | assert_eq!(wc.deps, vec![heads[0]], "the working copy follows the rewrite"); |
|
| 1203 | + | assert_eq!( |
|
| 1204 | + | wc.deps, |
|
| 1205 | + | vec![heads[0]], |
|
| 1206 | + | "the working copy follows the rewrite" |
|
| 1207 | + | ); |
|
| 851 | 1208 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 852 | 1209 | let snap = repo.load_state(&heads).unwrap().materialize(); |
|
| 853 | - | assert!(matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\n")); |
|
| 1210 | + | assert!( |
|
| 1211 | + | matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\n") |
|
| 1212 | + | ); |
|
| 854 | 1213 | ||
| 855 | 1214 | // amend: fold an edit into the head; the result is one change from h1. |
|
| 856 | 1215 | write(root, "a.txt", b"one\ntwo\nthree\n"); |
|
| … | 3 unchanged lines |
||
| 860 | 1219 | assert_eq!(c.change_id, id2); |
|
| 861 | 1220 | assert_eq!(c.message, "second"); |
|
| 862 | 1221 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h2c]); |
|
| 863 | - | assert_eq!(repo.log().unwrap().len(), 3, "working copy, amended head, first"); |
|
| 1222 | + | assert_eq!( |
|
| 1223 | + | repo.log().unwrap().len(), |
|
| 1224 | + | 3, |
|
| 1225 | + | "working copy, amended head, first" |
|
| 1226 | + | ); |
|
| 864 | 1227 | let snap = repo.load_state(&[h2c]).unwrap().materialize(); |
|
| 865 | - | assert!(matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\nthree\n")); |
|
| 1228 | + | assert!( |
|
| 1229 | + | matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\nthree\n") |
|
| 1230 | + | ); |
|
| 866 | 1231 | assert!(snap.files.contains_key("b.txt")); |
|
| 867 | 1232 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 868 | - | assert!(repo.amend(None).is_err(), "clean working copy and no message"); |
|
| 1233 | + | assert!( |
|
| 1234 | + | repo.amend(None).is_err(), |
|
| 1235 | + | "clean working copy and no message" |
|
| 1236 | + | ); |
|
| 869 | 1237 | let h2d = repo.amend(Some("second, amended")).unwrap(); |
|
| 870 | - | assert_eq!(repo.store().get_change(&h2d).unwrap().message, "second, amended"); |
|
| 1238 | + | assert_eq!( |
|
| 1239 | + | repo.store().get_change(&h2d).unwrap().message, |
|
| 1240 | + | "second, amended" |
|
| 1241 | + | ); |
|
| 871 | 1242 | ||
| 872 | 1243 | // abandon: the head goes, its content stays as uncommitted work. |
|
| 873 | 1244 | repo.abandon(&h2d).unwrap(); |
|
| 874 | - | assert_eq!(repo.bookmark_heads().unwrap(), vec![h1b], "the described first change is the head again"); |
|
| 1245 | + | assert_eq!( |
|
| 1246 | + | repo.bookmark_heads().unwrap(), |
|
| 1247 | + | vec![h1b], |
|
| 1248 | + | "the described first change is the head again" |
|
| 1249 | + | ); |
|
| 875 | 1250 | let s = repo.snapshot("status").unwrap(); |
|
| 876 | - | let paths: Vec<_> = s.statuses.iter().map(|p| (p.path.as_str(), p.status)).collect(); |
|
| 877 | - | assert_eq!(paths, vec![("a.txt", ChangeStatus::Modified), ("b.txt", ChangeStatus::Added)]); |
|
| 1251 | + | let paths: Vec<_> = s |
|
| 1252 | + | .statuses |
|
| 1253 | + | .iter() |
|
| 1254 | + | .map(|p| (p.path.as_str(), p.status)) |
|
| 1255 | + | .collect(); |
|
| 1256 | + | assert_eq!( |
|
| 1257 | + | paths, |
|
| 1258 | + | vec![ |
|
| 1259 | + | ("a.txt", ChangeStatus::Modified), |
|
| 1260 | + | ("b.txt", ChangeStatus::Added) |
|
| 1261 | + | ] |
|
| 1262 | + | ); |
|
| 878 | 1263 | assert_eq!(fs::read(root.join("a.txt")).unwrap(), b"one\ntwo\nthree\n"); |
|
| 879 | 1264 | let h3 = repo.commit("recommitted", &[]).unwrap(); |
|
| 880 | 1265 | assert_eq!(repo.bookmark_heads().unwrap(), vec![h3]); |
|
| … | 9 unchanged lines |
||
| 890 | 1275 | let base_change = repo.store().get_change(&base).unwrap(); |
|
| 891 | 1276 | let h1_now = base_change.deps[0]; |
|
| 892 | 1277 | let notes = repo.abandon(&h1_now).unwrap(); |
|
| 893 | - | assert!(!notes.is_empty(), "the dependent's edit of a.txt was reified"); |
|
| 1278 | + | assert!( |
|
| 1279 | + | !notes.is_empty(), |
|
| 1280 | + | "the dependent's edit of a.txt was reified" |
|
| 1281 | + | ); |
|
| 894 | 1282 | let heads = repo.bookmark_heads().unwrap(); |
|
| 895 | 1283 | let after = repo.load_state(&heads).unwrap().materialize(); |
|
| 896 | - | assert!(!after.files.contains_key("a.txt") && after.files.contains_key("b.txt"), "{:?}", after.files.keys()); |
|
| 1284 | + | assert!( |
|
| 1285 | + | !after.files.contains_key("a.txt") && after.files.contains_key("b.txt"), |
|
| 1286 | + | "{:?}", |
|
| 1287 | + | after.files.keys() |
|
| 1288 | + | ); |
|
| 897 | 1289 | repo.undo().unwrap(); |
|
| 898 | 1290 | assert_eq!(repo.bookmark_heads().unwrap(), vec![base]); |
|
| 899 | 1291 | write(root, "c.txt", b"c\n"); |
|
| … | 4 unchanged lines |
||
| 904 | 1296 | repo.abandon(&hc).unwrap(); |
|
| 905 | 1297 | let heads: BTreeSet<_> = repo.bookmark_heads().unwrap().into_iter().collect(); |
|
| 906 | 1298 | assert!(!heads.contains(&hc)); |
|
| 907 | - | let d_now = heads.iter().find(|h| repo.store().get_change(h).unwrap().message == "d").copied().unwrap(); |
|
| 908 | - | assert!(repo.store().get_change(&d_now).unwrap().deps.is_empty(), "d inherited c's (empty) dependencies"); |
|
| 1299 | + | let d_now = heads |
|
| 1300 | + | .iter() |
|
| 1301 | + | .find(|h| repo.store().get_change(h).unwrap().message == "d") |
|
| 1302 | + | .copied() |
|
| 1303 | + | .unwrap(); |
|
| 1304 | + | assert!( |
|
| 1305 | + | repo.store().get_change(&d_now).unwrap().deps.is_empty(), |
|
| 1306 | + | "d inherited c's (empty) dependencies" |
|
| 1307 | + | ); |
|
| 909 | 1308 | let s = repo.snapshot("status").unwrap(); |
|
| 910 | - | assert_eq!(s.statuses.iter().map(|p| p.path.as_str()).collect::<Vec<_>>(), vec!["c.txt"], "c's content is now uncommitted work"); |
|
| 1309 | + | assert_eq!( |
|
| 1310 | + | s.statuses |
|
| 1311 | + | .iter() |
|
| 1312 | + | .map(|p| p.path.as_str()) |
|
| 1313 | + | .collect::<Vec<_>>(), |
|
| 1314 | + | vec!["c.txt"], |
|
| 1315 | + | "c's content is now uncommitted work" |
|
| 1316 | + | ); |
|
| 911 | 1317 | } |
|
| 912 | 1318 | ||
| 913 | 1319 | #[test] |
|
| 914 | 1320 | fn git_export_mirrors_history() { |
|
| 915 | 1321 | hermetic(); |
|
| 916 | - | if std::process::Command::new("git").arg("--version").output().is_err() { |
|
| 1322 | + | if std::process::Command::new("git") |
|
| 1323 | + | .arg("--version") |
|
| 1324 | + | .output() |
|
| 1325 | + | .is_err() |
|
| 1326 | + | { |
|
| 917 | 1327 | eprintln!("git not installed; skipping"); |
|
| 918 | 1328 | return; |
|
| 919 | 1329 | } |
|
| 920 | 1330 | let dir = tempfile::tempdir().unwrap(); |
|
| 921 | 1331 | let root = dir.path(); |
|
| 922 | - | let mut repo = Repo::init(&root.join("r")).unwrap().with_home(root.join("home")); |
|
| 923 | - | repo.create_identity("Ada Lovelace", "[email protected]", IdentityScope::User).unwrap(); |
|
| 1332 | + | let mut repo = Repo::init(&root.join("r")) |
|
| 1333 | + | .unwrap() |
|
| 1334 | + | .with_home(root.join("home")); |
|
| 1335 | + | repo.create_identity("Ada Lovelace", "[email protected]", IdentityScope::User) |
|
| 1336 | + | .unwrap(); |
|
| 924 | 1337 | let r = root.join("r"); |
|
| 925 | 1338 | write(&r, "README.md", b"hello\n"); |
|
| 926 | 1339 | write(&r, "src/main.rs", b"fn main() {}\n"); |
|
| … | 6 unchanged lines |
||
| 933 | 1346 | let rep = repo.git_export(&git_dir).unwrap(); |
|
| 934 | 1347 | assert_eq!((rep.exported, rep.skipped, rep.heads), (2, 0, 1)); |
|
| 935 | 1348 | let git = |args: &[&str]| -> String { |
|
| 936 | - | let out = std::process::Command::new("git").arg("-C").arg(&git_dir).args(args).output().unwrap(); |
|
| 937 | - | assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); |
|
| 1349 | + | let out = std::process::Command::new("git") |
|
| 1350 | + | .arg("-C") |
|
| 1351 | + | .arg(&git_dir) |
|
| 1352 | + | .args(args) |
|
| 1353 | + | .output() |
|
| 1354 | + | .unwrap(); |
|
| 1355 | + | assert!( |
|
| 1356 | + | out.status.success(), |
|
| 1357 | + | "{}", |
|
| 1358 | + | String::from_utf8_lossy(&out.stderr) |
|
| 1359 | + | ); |
|
| 938 | 1360 | String::from_utf8_lossy(&out.stdout).trim().to_string() |
|
| 939 | 1361 | }; |
|
| 940 | 1362 | assert_eq!(git(&["log", "--format=%s", "main"]), "second\nfirst"); |
|
| 941 | 1363 | assert_eq!(git(&["show", "main:src/main.rs"]), "fn main() { run(); }"); |
|
| 942 | 1364 | assert_eq!(git(&["show", "main:README.md"]), "hello"); |
|
| 943 | 1365 | let body = git(&["log", "-1", "--format=%B", "main"]); |
|
| 944 | - | assert!(body.contains(&format!("Oxy-Hash: {}", h2.to_hex())), "{body}"); |
|
| 1366 | + | assert!( |
|
| 1367 | + | body.contains(&format!("Oxy-Hash: {}", h2.to_hex())), |
|
| 1368 | + | "{body}" |
|
| 1369 | + | ); |
|
| 945 | 1370 | assert!(body.contains("Change-Id: "), "{body}"); |
|
| 946 | - | assert_eq!(git(&["log", "-1", "--format=%an <%ae>", "main"]), "Ada Lovelace <[email protected]>"); |
|
| 947 | - | assert_eq!(git(&["ls-tree", "--name-only", "main"]), "README.md\nlogo.png\nsrc"); |
|
| 1371 | + | assert_eq!( |
|
| 1372 | + | git(&["log", "-1", "--format=%an <%ae>", "main"]), |
|
| 1373 | + | "Ada Lovelace <[email protected]>" |
|
| 1374 | + | ); |
|
| 1375 | + | assert_eq!( |
|
| 1376 | + | git(&["ls-tree", "--name-only", "main"]), |
|
| 1377 | + | "README.md\nlogo.png\nsrc" |
|
| 1378 | + | ); |
|
| 948 | 1379 | ||
| 949 | 1380 | // Re-export is incremental. |
|
| 950 | 1381 | let rep = repo.git_export(&git_dir).unwrap(); |
|
| … | 11 unchanged lines |
||
| 962 | 1393 | assert_eq!(parents.split(' ').count(), 2, "one parent: {parents}"); |
|
| 963 | 1394 | assert_eq!(git(&["show", "main:other.txt"]), "o"); |
|
| 964 | 1395 | assert_eq!(git(&["show", "main:src/main.rs"]), "fn main() { run(); }"); |
|
| 965 | - | assert_eq!(git(&["ls-tree", "--name-only", "main"]), "README.md\nlogo.png\nother.txt\nsrc"); |
|
| 1396 | + | assert_eq!( |
|
| 1397 | + | git(&["ls-tree", "--name-only", "main"]), |
|
| 1398 | + | "README.md\nlogo.png\nother.txt\nsrc" |
|
| 1399 | + | ); |
|
| 966 | 1400 | let body = git(&["log", "-1", "--format=%B", "main"]); |
|
| 967 | - | assert!(body.contains(&format!("Oxy-Hash: {}", h3.to_hex())) && body.contains("Oxy-State: "), "{body}"); |
|
| 1401 | + | assert!( |
|
| 1402 | + | body.contains(&format!("Oxy-Hash: {}", h3.to_hex())) && body.contains("Oxy-State: "), |
|
| 1403 | + | "{body}" |
|
| 1404 | + | ); |
|
| 968 | 1405 | // A rewrite is a new state whose one new change is the rewritten one. |
|
| 969 | 1406 | repo.describe(&h3, "third, described").unwrap(); |
|
| 970 | 1407 | let rep = repo.git_export(&git_dir).unwrap(); |
|
| 971 | 1408 | assert_eq!(rep.exported, 1); |
|
| 972 | - | assert_eq!(git(&["log", "-1", "--format=%s", "main"]), "third, described"); |
|
| 1409 | + | assert_eq!( |
|
| 1410 | + | git(&["log", "-1", "--format=%s", "main"]), |
|
| 1411 | + | "third, described" |
|
| 1412 | + | ); |
|
| 973 | 1413 | assert_eq!(git(&["log", "--format=%s", "main"]).lines().count(), 4); |
|
| 974 | 1414 | let _ = h2; |
|
| 975 | 1415 | } |
|
| … | 1 unchanged lines |
||
| 977 | 1417 | #[test] |
|
| 978 | 1418 | fn git_import_reproduces_trees() { |
|
| 979 | 1419 | hermetic(); |
|
| 980 | - | if std::process::Command::new("git").arg("--version").output().is_err() { |
|
| 1420 | + | if std::process::Command::new("git") |
|
| 1421 | + | .arg("--version") |
|
| 1422 | + | .output() |
|
| 1423 | + | .is_err() |
|
| 1424 | + | { |
|
| 981 | 1425 | eprintln!("git not installed; skipping"); |
|
| 982 | 1426 | return; |
|
| 983 | 1427 | } |
|
| … | 5 unchanged lines |
||
| 989 | 1433 | let out = std::process::Command::new("git") |
|
| 990 | 1434 | .arg("-C") |
|
| 991 | 1435 | .arg(&g) |
|
| 992 | - | .args(["-c", "user.name=Grace", "-c", "[email protected]", "-c", "commit.gpgsign=false"]) |
|
| 1436 | + | .args([ |
|
| 1437 | + | "-c", |
|
| 1438 | + | "user.name=Grace", |
|
| 1439 | + | "-c", |
|
| 1440 | + | "[email protected]", |
|
| 1441 | + | "-c", |
|
| 1442 | + | "commit.gpgsign=false", |
|
| 1443 | + | ]) |
|
| 993 | 1444 | .args(args) |
|
| 994 | 1445 | .output() |
|
| 995 | 1446 | .unwrap(); |
|
| 996 | - | assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); |
|
| 1447 | + | assert!( |
|
| 1448 | + | out.status.success(), |
|
| 1449 | + | "git {args:?}: {}", |
|
| 1450 | + | String::from_utf8_lossy(&out.stderr) |
|
| 1451 | + | ); |
|
| 997 | 1452 | String::from_utf8_lossy(&out.stdout).trim().to_string() |
|
| 998 | 1453 | }; |
|
| 999 | 1454 | git(&["init", "-q", "-b", "main"]); |
|
| … | 10 unchanged lines |
||
| 1010 | 1465 | } |
|
| 1011 | 1466 | write(&g, "logo.png", b"\x89PNG\0\0\x01"); |
|
| 1012 | 1467 | git(&["add", "."]); |
|
| 1013 | - | git(&["commit", "-q", "-m", "second\n\nChange-Id: 0123456789abcdef0123456789abcdef"]); |
|
| 1468 | + | git(&[ |
|
| 1469 | + | "commit", |
|
| 1470 | + | "-q", |
|
| 1471 | + | "-m", |
|
| 1472 | + | "second\n\nChange-Id: 0123456789abcdef0123456789abcdef", |
|
| 1473 | + | ]); |
|
| 1014 | 1474 | // A branch that edits README, merged back with a conflict-free merge. |
|
| 1015 | 1475 | git(&["checkout", "-q", "-b", "topic", "HEAD~1"]); |
|
| 1016 | 1476 | write(&g, "README.md", b"# Hi\n\nMore.\n"); |
|
| … | 7 unchanged lines |
||
| 1024 | 1484 | write(&g, "src/lib.rs", b"pub fn a() {}\npub fn c() {}\n"); |
|
| 1025 | 1485 | git(&["commit", "-q", "-am", "clash"]); |
|
| 1026 | 1486 | git(&["checkout", "-q", "main"]); |
|
| 1027 | - | let _ = std::process::Command::new("git").arg("-C").arg(&g).args(["merge", "-q", "clash"]).output(); |
|
| 1028 | - | write(&g, "src/lib.rs", b"pub fn a() {}\npub fn b() {}\npub fn c() {}\n"); |
|
| 1487 | + | let _ = std::process::Command::new("git") |
|
| 1488 | + | .arg("-C") |
|
| 1489 | + | .arg(&g) |
|
| 1490 | + | .args(["merge", "-q", "clash"]) |
|
| 1491 | + | .output(); |
|
| 1492 | + | write( |
|
| 1493 | + | &g, |
|
| 1494 | + | "src/lib.rs", |
|
| 1495 | + | b"pub fn a() {}\npub fn b() {}\npub fn c() {}\n", |
|
| 1496 | + | ); |
|
| 1029 | 1497 | git(&["add", "."]); |
|
| 1030 | 1498 | git(&["commit", "-q", "-m", "resolve clash"]); |
|
| 1031 | 1499 | git(&["rm", "-q", "logo.png"]); |
|
| 1032 | 1500 | git(&["commit", "-q", "-m", "drop logo"]); |
|
| 1033 | 1501 | // A rename with a small edit is imported as a move of the same node. |
|
| 1034 | 1502 | git(&["mv", "src/lib.rs", "src/lib2.rs"]); |
|
| 1035 | - | write(&g, "src/lib2.rs", b"pub fn a() {}\npub fn b() {}\npub fn c() {}\npub fn d() {}\n"); |
|
| 1503 | + | write( |
|
| 1504 | + | &g, |
|
| 1505 | + | "src/lib2.rs", |
|
| 1506 | + | b"pub fn a() {}\npub fn b() {}\npub fn c() {}\npub fn d() {}\n", |
|
| 1507 | + | ); |
|
| 1036 | 1508 | git(&["add", "."]); |
|
| 1037 | 1509 | git(&["commit", "-q", "-m", "rename lib"]); |
|
| 1038 | 1510 | let expected_count: usize = git(&["rev-list", "--count", "main"]).parse().unwrap(); |
|
| … | 4 unchanged lines |
||
| 1043 | 1515 | assert_eq!(rep.commits, expected_count); |
|
| 1044 | 1516 | assert_eq!(rep.merges, 2); |
|
| 1045 | 1517 | // The working copy equals the Git tree at main. |
|
| 1046 | - | assert_eq!(fs::read(dest.join("src/lib2.rs")).unwrap(), b"pub fn a() {}\npub fn b() {}\npub fn c() {}\npub fn d() {}\n"); |
|
| 1518 | + | assert_eq!( |
|
| 1519 | + | fs::read(dest.join("src/lib2.rs")).unwrap(), |
|
| 1520 | + | b"pub fn a() {}\npub fn b() {}\npub fn c() {}\npub fn d() {}\n" |
|
| 1521 | + | ); |
|
| 1047 | 1522 | assert!(!dest.join("src/lib.rs").exists()); |
|
| 1048 | - | assert_eq!(fs::read(dest.join("README.md")).unwrap(), b"# Hi\n\nMore.\n"); |
|
| 1523 | + | assert_eq!( |
|
| 1524 | + | fs::read(dest.join("README.md")).unwrap(), |
|
| 1525 | + | b"# Hi\n\nMore.\n" |
|
| 1526 | + | ); |
|
| 1049 | 1527 | assert!(!dest.join("logo.png").exists()); |
|
| 1050 | 1528 | #[cfg(unix)] |
|
| 1051 | 1529 | { |
|
| 1052 | 1530 | use std::os::unix::fs::PermissionsExt; |
|
| 1053 | - | assert!(fs::metadata(dest.join("run.sh")).unwrap().permissions().mode() & 0o111 != 0); |
|
| 1531 | + | assert!( |
|
| 1532 | + | fs::metadata(dest.join("run.sh")) |
|
| 1533 | + | .unwrap() |
|
| 1534 | + | .permissions() |
|
| 1535 | + | .mode() |
|
| 1536 | + | & 0o111 |
|
| 1537 | + | != 0 |
|
| 1538 | + | ); |
|
| 1054 | 1539 | } |
|
| 1055 | - | assert!(!repo.snapshot("status").unwrap().changed, "clean after import"); |
|
| 1540 | + | assert!( |
|
| 1541 | + | !repo.snapshot("status").unwrap().changed, |
|
| 1542 | + | "clean after import" |
|
| 1543 | + | ); |
|
| 1056 | 1544 | let heads = repo.bookmark_heads().unwrap(); |
|
| 1057 | 1545 | let st = repo.load_state(&heads).unwrap(); |
|
| 1058 | 1546 | assert!(!st.materialize().is_conflicted()); |
|
| 1059 | 1547 | assert_eq!(st.len(), expected_count); |
|
| 1060 | - | let tip = repo.log().unwrap().into_iter().find(|e| e.change.message.starts_with("rename lib")).unwrap(); |
|
| 1548 | + | let tip = repo |
|
| 1549 | + | .log() |
|
| 1550 | + | .unwrap() |
|
| 1551 | + | .into_iter() |
|
| 1552 | + | .find(|e| e.change.message.starts_with("rename lib")) |
|
| 1553 | + | .unwrap(); |
|
| 1061 | 1554 | let c = repo.store().get_change(&tip.hash).unwrap(); |
|
| 1062 | - | assert!(c.tree_ops.iter().any(|op| matches!(op, TreeOp::Move { name, .. } if name == "lib2.rs")), "{:?}", c.tree_ops); |
|
| 1063 | - | assert!(!c.tree_ops.iter().any(|op| matches!(op, TreeOp::AddNode { .. }))); |
|
| 1555 | + | assert!( |
|
| 1556 | + | c.tree_ops |
|
| 1557 | + | .iter() |
|
| 1558 | + | .any(|op| matches!(op, TreeOp::Move { name, .. } if name == "lib2.rs")), |
|
| 1559 | + | "{:?}", |
|
| 1560 | + | c.tree_ops |
|
| 1561 | + | ); |
|
| 1562 | + | assert!(!c |
|
| 1563 | + | .tree_ops |
|
| 1564 | + | .iter() |
|
| 1565 | + | .any(|op| matches!(op, TreeOp::AddNode { .. }))); |
|
| 1064 | 1566 | // Messages carry Git trailers; the Change-Id trailer was honoured. |
|
| 1065 | 1567 | let log = repo.log().unwrap(); |
|
| 1066 | - | let second = log.iter().find(|e| e.change.message.starts_with("second")).unwrap(); |
|
| 1067 | - | assert_eq!(second.change.change_id, ChangeId::from_hex("0123456789abcdef0123456789abcdef").unwrap()); |
|
| 1068 | - | assert!(second.change.message.contains("Git-Author: Grace <[email protected]>")); |
|
| 1069 | - | let resolve = log.iter().find(|e| e.change.message.starts_with("resolve clash")).unwrap(); |
|
| 1070 | - | assert_eq!(resolve.change.kind, ChangeKind::Resolution, "a conflicting Git merge becomes a resolution"); |
|
| 1568 | + | let second = log |
|
| 1569 | + | .iter() |
|
| 1570 | + | .find(|e| e.change.message.starts_with("second")) |
|
| 1571 | + | .unwrap(); |
|
| 1572 | + | assert_eq!( |
|
| 1573 | + | second.change.change_id, |
|
| 1574 | + | ChangeId::from_hex("0123456789abcdef0123456789abcdef").unwrap() |
|
| 1575 | + | ); |
|
| 1576 | + | assert!(second |
|
| 1577 | + | .change |
|
| 1578 | + | .message |
|
| 1579 | + | .contains("Git-Author: Grace <[email protected]>")); |
|
| 1580 | + | let resolve = log |
|
| 1581 | + | .iter() |
|
| 1582 | + | .find(|e| e.change.message.starts_with("resolve clash")) |
|
| 1583 | + | .unwrap(); |
|
| 1584 | + | assert_eq!( |
|
| 1585 | + | resolve.change.kind, |
|
| 1586 | + | ChangeKind::Resolution, |
|
| 1587 | + | "a conflicting Git merge becomes a resolution" |
|
| 1588 | + | ); |
|
| 1071 | 1589 | // Re-importing into another fresh repository gives the same ChangeIds. |
|
| 1072 | - | let repo2 = Repo::init(&root.join("oxy2")).unwrap().with_home(root.join("home")); |
|
| 1590 | + | let repo2 = Repo::init(&root.join("oxy2")) |
|
| 1591 | + | .unwrap() |
|
| 1592 | + | .with_home(root.join("home")); |
|
| 1073 | 1593 | repo2.import_git(&g, "main").unwrap(); |
|
| 1074 | - | let ids: BTreeSet<ChangeId> = repo.log().unwrap().iter().filter(|e| !e.is_working_copy).map(|e| e.change.change_id).collect(); |
|
| 1075 | - | let ids2: BTreeSet<ChangeId> = repo2.log().unwrap().iter().filter(|e| !e.is_working_copy).map(|e| e.change.change_id).collect(); |
|
| 1594 | + | let ids: BTreeSet<ChangeId> = repo |
|
| 1595 | + | .log() |
|
| 1596 | + | .unwrap() |
|
| 1597 | + | .iter() |
|
| 1598 | + | .filter(|e| !e.is_working_copy) |
|
| 1599 | + | .map(|e| e.change.change_id) |
|
| 1600 | + | .collect(); |
|
| 1601 | + | let ids2: BTreeSet<ChangeId> = repo2 |
|
| 1602 | + | .log() |
|
| 1603 | + | .unwrap() |
|
| 1604 | + | .iter() |
|
| 1605 | + | .filter(|e| !e.is_working_copy) |
|
| 1606 | + | .map(|e| e.change.change_id) |
|
| 1607 | + | .collect(); |
|
| 1076 | 1608 | assert_eq!(ids, ids2); |
|
| 1077 | 1609 | // Round trip back out to Git reproduces the tip tree. |
|
| 1078 | 1610 | let mirror = root.join("mirror.git"); |
|
| 1079 | 1611 | repo.git_export(&mirror).unwrap(); |
|
| 1080 | - | let out = std::process::Command::new("git").arg("-C").arg(&mirror).args(["show", "main:src/lib2.rs"]).output().unwrap(); |
|
| 1081 | - | assert_eq!(String::from_utf8_lossy(&out.stdout), "pub fn a() {}\npub fn b() {}\npub fn c() {}\npub fn d() {}\n"); |
|
| 1082 | - | let out = std::process::Command::new("git").arg("-C").arg(&mirror).args(["show", "main:src/lib.rs"]).output().unwrap(); |
|
| 1083 | - | assert!(!out.status.success(), "the old path is gone from the export"); |
|
| 1612 | + | let out = std::process::Command::new("git") |
|
| 1613 | + | .arg("-C") |
|
| 1614 | + | .arg(&mirror) |
|
| 1615 | + | .args(["show", "main:src/lib2.rs"]) |
|
| 1616 | + | .output() |
|
| 1617 | + | .unwrap(); |
|
| 1618 | + | assert_eq!( |
|
| 1619 | + | String::from_utf8_lossy(&out.stdout), |
|
| 1620 | + | "pub fn a() {}\npub fn b() {}\npub fn c() {}\npub fn d() {}\n" |
|
| 1621 | + | ); |
|
| 1622 | + | let out = std::process::Command::new("git") |
|
| 1623 | + | .arg("-C") |
|
| 1624 | + | .arg(&mirror) |
|
| 1625 | + | .args(["show", "main:src/lib.rs"]) |
|
| 1626 | + | .output() |
|
| 1627 | + | .unwrap(); |
|
| 1628 | + | assert!( |
|
| 1629 | + | !out.status.success(), |
|
| 1630 | + | "the old path is gone from the export" |
|
| 1631 | + | ); |
|
| 1084 | 1632 | } |
|
| 1085 | 1633 | ||
| 1086 | 1634 | #[test] |
|
| … | 7 unchanged lines |
||
| 1094 | 1642 | let repo = Repo::open(root).unwrap().with_home(root.join("home")); |
|
| 1095 | 1643 | let mut hashes = Vec::new(); |
|
| 1096 | 1644 | for i in 0..12 { |
|
| 1097 | - | write(root, "a.txt", format!("{}\n", (0..=i).map(|k| k.to_string()).collect::<Vec<_>>().join("\n")).as_bytes()); |
|
| 1645 | + | write( |
|
| 1646 | + | root, |
|
| 1647 | + | "a.txt", |
|
| 1648 | + | format!( |
|
| 1649 | + | "{}\n", |
|
| 1650 | + | (0..=i) |
|
| 1651 | + | .map(|k| k.to_string()) |
|
| 1652 | + | .collect::<Vec<_>>() |
|
| 1653 | + | .join("\n") |
|
| 1654 | + | ) |
|
| 1655 | + | .as_bytes(), |
|
| 1656 | + | ); |
|
| 1098 | 1657 | write(root, &format!("f{i}.txt"), b"x\n"); |
|
| 1099 | 1658 | hashes.push(repo.commit(&format!("c{i}"), &[]).unwrap()); |
|
| 1100 | 1659 | } |
|
| 1101 | - | let (_, cp) = repo.latest_checkpoint("main").unwrap().expect("automatic checkpoint"); |
|
| 1660 | + | let (_, cp) = repo |
|
| 1661 | + | .latest_checkpoint("main") |
|
| 1662 | + | .unwrap() |
|
| 1663 | + | .expect("automatic checkpoint"); |
|
| 1102 | 1664 | assert!(cp.count >= 5 && cp.count <= 12, "{}", cp.count); |
|
| 1103 | 1665 | // Loading through the checkpoint equals loading everything. |
|
| 1104 | 1666 | let heads = repo.bookmark_heads().unwrap(); |
|
| … | 2 unchanged lines |
||
| 1107 | 1669 | assert_eq!(fast.len(), full.len()); |
|
| 1108 | 1670 | assert_eq!(fast.heads(), full.heads()); |
|
| 1109 | 1671 | let (a, b) = (fast.materialize(), full.materialize()); |
|
| 1110 | - | assert_eq!(a.files.keys().collect::<Vec<_>>(), b.files.keys().collect::<Vec<_>>()); |
|
| 1672 | + | assert_eq!( |
|
| 1673 | + | a.files.keys().collect::<Vec<_>>(), |
|
| 1674 | + | b.files.keys().collect::<Vec<_>>() |
|
| 1675 | + | ); |
|
| 1111 | 1676 | for (p, f) in &a.files { |
|
| 1112 | 1677 | assert_eq!(f.content, b.files[p].content, "{p}"); |
|
| 1113 | 1678 | } |
|
| 1114 | 1679 | assert!(repo.load_state_checkpointed(&heads).unwrap().is_some()); |
|
| 1115 | 1680 | // A head set that does not contain the checkpoint's heads falls back. |
|
| 1116 | - | assert!(repo.load_state_checkpointed(&[hashes[0]]).unwrap().is_none()); |
|
| 1681 | + | assert!(repo |
|
| 1682 | + | .load_state_checkpointed(&[hashes[0]]) |
|
| 1683 | + | .unwrap() |
|
| 1684 | + | .is_none()); |
|
| 1117 | 1685 | assert_eq!(repo.load_state(&[hashes[0]]).unwrap().len(), 1); |
|
| 1118 | 1686 | // Everything else keeps working on top of the checkpoint. |
|
| 1119 | 1687 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| … | 6 unchanged lines |
||
| 1126 | 1694 | let h_after = repo.commit("after", &[]).unwrap(); |
|
| 1127 | 1695 | let st = repo.load_state(&repo.bookmark_heads().unwrap()).unwrap(); |
|
| 1128 | 1696 | assert!(st.contains(&h_after)); |
|
| 1129 | - | assert!(matches!(&st.materialize().files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"after\n")); |
|
| 1697 | + | assert!( |
|
| 1698 | + | matches!(&st.materialize().files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"after\n") |
|
| 1699 | + | ); |
|
| 1130 | 1700 | assert_eq!(repo.log().unwrap().len(), 14); |
|
| 1131 | 1701 | // Checkpoints survive gc. |
|
| 1132 | 1702 | repo.gc(true).unwrap(); |
|
| 1133 | 1703 | assert!(repo.latest_checkpoint("main").unwrap().is_some()); |
|
| 1134 | - | assert!(repo.load_state_checkpointed(&repo.bookmark_heads().unwrap()).unwrap().is_some()); |
|
| 1704 | + | assert!(repo |
|
| 1705 | + | .load_state_checkpointed(&repo.bookmark_heads().unwrap()) |
|
| 1706 | + | .unwrap() |
|
| 1707 | + | .is_some()); |
|
| 1135 | 1708 | } |
|
| 1136 | 1709 | ||
| 1137 | 1710 | #[test] |
|
| … | 3 unchanged lines |
||
| 1141 | 1714 | let root = dir.path(); |
|
| 1142 | 1715 | let origin = Repo::init(&root.join("origin")).unwrap(); |
|
| 1143 | 1716 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 1144 | - | let a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home")); |
|
| 1717 | + | let a = Repo::clone(&url, &root.join("a")) |
|
| 1718 | + | .unwrap() |
|
| 1719 | + | .with_home(root.join("home")); |
|
| 1145 | 1720 | let ops_before = a.op_log().unwrap().len(); |
|
| 1146 | 1721 | let info = a.quick_status().unwrap(); |
|
| 1147 | - | assert_eq!((info.bookmark.as_str(), info.heads, info.dirty, info.conflicted, info.ahead, info.behind), ("main", 0, false, false, Some(0), Some(0))); |
|
| 1722 | + | assert_eq!( |
|
| 1723 | + | ( |
|
| 1724 | + | info.bookmark.as_str(), |
|
| 1725 | + | info.heads, |
|
| 1726 | + | info.dirty, |
|
| 1727 | + | info.conflicted, |
|
| 1728 | + | info.ahead, |
|
| 1729 | + | info.behind |
|
| 1730 | + | ), |
|
| 1731 | + | ("main", 0, false, false, Some(0), Some(0)) |
|
| 1732 | + | ); |
|
| 1148 | 1733 | write(&root.join("a"), "f.txt", b"f\n"); |
|
| 1149 | 1734 | assert!(a.quick_status().unwrap().dirty, "a new file is dirty"); |
|
| 1150 | - | assert_eq!(a.op_log().unwrap().len(), ops_before, "quick_status records nothing"); |
|
| 1735 | + | assert_eq!( |
|
| 1736 | + | a.op_log().unwrap().len(), |
|
| 1737 | + | ops_before, |
|
| 1738 | + | "quick_status records nothing" |
|
| 1739 | + | ); |
|
| 1151 | 1740 | // A snapshotted but uncommitted change is still dirty. |
|
| 1152 | 1741 | a.snapshot("status").unwrap(); |
|
| 1153 | 1742 | assert!(a.quick_status().unwrap().dirty); |
|
| … | 1 unchanged lines |
||
| 1155 | 1744 | let info = a.quick_status().unwrap(); |
|
| 1156 | 1745 | assert!(!info.dirty, "clean right after a commit"); |
|
| 1157 | 1746 | assert_eq!((info.ahead, info.behind), (Some(1), Some(0))); |
|
| 1158 | - | assert_eq!(info.format("%b %c%d%x%A%D"), format!("main {}+1", info.change_id)); |
|
| 1747 | + | assert_eq!( |
|
| 1748 | + | info.format("%b %c%d%x%A%D"), |
|
| 1749 | + | format!("main {}+1", info.change_id) |
|
| 1750 | + | ); |
|
| 1159 | 1751 | assert_eq!(info.format("[%h heads] %a/%B %%"), "[1 heads] 1/0 %"); |
|
| 1160 | 1752 | // Touching a file without changing it is not dirty; changing content of |
|
| 1161 | 1753 | // the same size is. |
|
| … | 7 unchanged lines |
||
| 1169 | 1761 | // Push, then a second clone commits and pushes: we are behind. |
|
| 1170 | 1762 | assert!(a.push("origin", false).unwrap()[0].ok); |
|
| 1171 | 1763 | assert_eq!(a.quick_status().unwrap().ahead, Some(0)); |
|
| 1172 | - | let b = Repo::clone(&url, &root.join("b")).unwrap().with_home(root.join("home-b")); |
|
| 1764 | + | let b = Repo::clone(&url, &root.join("b")) |
|
| 1765 | + | .unwrap() |
|
| 1766 | + | .with_home(root.join("home-b")); |
|
| 1173 | 1767 | write(&root.join("b"), "g.txt", b"g\n"); |
|
| 1174 | 1768 | b.commit("g", &[]).unwrap(); |
|
| 1175 | 1769 | b.push("origin", false).unwrap(); |
|
| … | 10 unchanged lines |
||
| 1186 | 1780 | let dir = tempfile::tempdir().unwrap(); |
|
| 1187 | 1781 | let root = dir.path(); |
|
| 1188 | 1782 | let origin_dir = root.join("origin"); |
|
| 1189 | - | let origin = Repo::init(&origin_dir).unwrap().with_home(root.join("home")); |
|
| 1783 | + | let origin = Repo::init(&origin_dir) |
|
| 1784 | + | .unwrap() |
|
| 1785 | + | .with_home(root.join("home")); |
|
| 1190 | 1786 | let url = origin_dir.to_string_lossy().to_string(); |
|
| 1191 | 1787 | let big: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect(); // has NUL bytes: opaque |
|
| 1192 | 1788 | write(&origin_dir, "big.bin", &big); |
|
| … | 3 unchanged lines |
||
| 1196 | 1792 | let big_ref = Repo::blob_ref(&big); |
|
| 1197 | 1793 | ||
| 1198 | 1794 | // Fetch only (no checkout) with a filter: the big blob stays absent. |
|
| 1199 | - | let a = Repo::init_with_id(&root.join("a"), origin.id().unwrap()).unwrap().with_home(root.join("home")); |
|
| 1795 | + | let a = Repo::init_with_id(&root.join("a"), origin.id().unwrap()) |
|
| 1796 | + | .unwrap() |
|
| 1797 | + | .with_home(root.join("home")); |
|
| 1200 | 1798 | let a = { |
|
| 1201 | 1799 | let mut a = a; |
|
| 1202 | 1800 | a.config.set("remote.origin.url", &url).unwrap(); |
|
| … | 5 unchanged lines |
||
| 1208 | 1806 | a.fetch("origin").unwrap(); |
|
| 1209 | 1807 | assert!(a.store().has(&h)); |
|
| 1210 | 1808 | assert!(!a.store().has(&big_ref), "withheld"); |
|
| 1211 | - | assert!(a.store().has(&Repo::blob_ref(&[0u8, 1, 2])), "small blob came along"); |
|
| 1809 | + | assert!( |
|
| 1810 | + | a.store().has(&Repo::blob_ref(&[0u8, 1, 2])), |
|
| 1811 | + | "small blob came along" |
|
| 1812 | + | ); |
|
| 1212 | 1813 | assert_eq!(a.lazy_remote(&big_ref).as_deref(), Some("origin")); |
|
| 1213 | 1814 | ||
| 1214 | 1815 | // status and commit do not need it: adopt the state, edit a text file, commit. |
|
| 1215 | 1816 | let (op_ref, op) = a.head_op().unwrap(); |
|
| 1216 | 1817 | let mut view = op.view.clone(); |
|
| 1217 | - | view.bookmarks.insert("main".into(), a.store().put_state(&StateObject::new(vec![h])).unwrap()); |
|
| 1818 | + | view.bookmarks.insert( |
|
| 1819 | + | "main".into(), |
|
| 1820 | + | a.store().put_state(&StateObject::new(vec![h])).unwrap(), |
|
| 1821 | + | ); |
|
| 1218 | 1822 | a.record_op(&[op_ref], "adopt", view).unwrap(); |
|
| 1219 | 1823 | // Checkout reads the blob lazily, which fetches it. |
|
| 1220 | 1824 | a.checkout(&[h], true, "checkout").unwrap(); |
|
| … | 17 unchanged lines |
||
| 1238 | 1842 | let dir = tempfile::tempdir().unwrap(); |
|
| 1239 | 1843 | let root = dir.path(); |
|
| 1240 | 1844 | let origin_dir = root.join("origin"); |
|
| 1241 | - | let origin = Repo::init(&origin_dir).unwrap().with_home(root.join("home")); |
|
| 1845 | + | let origin = Repo::init(&origin_dir) |
|
| 1846 | + | .unwrap() |
|
| 1847 | + | .with_home(root.join("home")); |
|
| 1242 | 1848 | write(&origin_dir, "src/main.rs", b"fn main() {}\n"); |
|
| 1243 | 1849 | write(&origin_dir, "src/lib.rs", b"pub fn a() {}\n"); |
|
| 1244 | 1850 | write(&origin_dir, "docs/guide.md", b"# guide\n"); |
|
| … | 3 unchanged lines |
||
| 1248 | 1854 | let h1 = origin.commit("tree", &[]).unwrap(); |
|
| 1249 | 1855 | let url = origin_dir.to_string_lossy().to_string(); |
|
| 1250 | 1856 | ||
| 1251 | - | let a = Repo::clone_with(&url, &root.join("a"), None, &["src".into()]).unwrap().with_home(root.join("home")); |
|
| 1857 | + | let a = Repo::clone_with(&url, &root.join("a"), None, &["src".into()]) |
|
| 1858 | + | .unwrap() |
|
| 1859 | + | .with_home(root.join("home")); |
|
| 1252 | 1860 | assert_eq!(a.config.sparse_paths, vec!["src"]); |
|
| 1253 | 1861 | assert_eq!(a.config.max_blob, Some(0), "sparse implies lazy blobs"); |
|
| 1254 | 1862 | assert!(root.join("a/src/main.rs").exists()); |
|
| 1255 | 1863 | assert!(!root.join("a/docs").exists() && !root.join("a/README.md").exists()); |
|
| 1256 | - | assert!(!a.store().has(&Repo::blob_ref(&logo)), "out-of-scope blob never fetched"); |
|
| 1257 | - | assert!(!a.snapshot("status").unwrap().changed, "out-of-scope files are not removals"); |
|
| 1864 | + | assert!( |
|
| 1865 | + | !a.store().has(&Repo::blob_ref(&logo)), |
|
| 1866 | + | "out-of-scope blob never fetched" |
|
| 1867 | + | ); |
|
| 1868 | + | assert!( |
|
| 1869 | + | !a.snapshot("status").unwrap().changed, |
|
| 1870 | + | "out-of-scope files are not removals" |
|
| 1871 | + | ); |
|
| 1258 | 1872 | assert_eq!(a.quick_status().unwrap().dirty, false); |
|
| 1259 | 1873 | ||
| 1260 | 1874 | // Edits in scope commit normally and keep the rest of the tree intact. |
|
| 1261 | - | write(&root.join("a"), "src/lib.rs", b"pub fn a() {}\npub fn b() {}\n"); |
|
| 1875 | + | write( |
|
| 1876 | + | &root.join("a"), |
|
| 1877 | + | "src/lib.rs", |
|
| 1878 | + | b"pub fn a() {}\npub fn b() {}\n", |
|
| 1879 | + | ); |
|
| 1262 | 1880 | let h2 = a.commit("edit lib", &[]).unwrap(); |
|
| 1263 | 1881 | let st = a.load_state(&[h2]).unwrap().materialize(); |
|
| 1264 | - | assert!(st.files.contains_key("docs/guide.md"), "the state still has everything"); |
|
| 1882 | + | assert!( |
|
| 1883 | + | st.files.contains_key("docs/guide.md"), |
|
| 1884 | + | "the state still has everything" |
|
| 1885 | + | ); |
|
| 1265 | 1886 | assert!(a.push("origin", false).unwrap()[0].ok); |
|
| 1266 | 1887 | // Files created outside the scope are ignored by status. |
|
| 1267 | 1888 | write(&root.join("a"), "docs/new.md", b"ignored\n"); |
|
| … | 3 unchanged lines |
||
| 1271 | 1892 | let mut a = a; |
|
| 1272 | 1893 | a.config.set("filter.paths", "src,docs").unwrap(); |
|
| 1273 | 1894 | a.save_config().unwrap(); |
|
| 1274 | - | let r = a.checkout(&a.bookmark_heads().unwrap(), true, "widen").unwrap(); |
|
| 1895 | + | let r = a |
|
| 1896 | + | .checkout(&a.bookmark_heads().unwrap(), true, "widen") |
|
| 1897 | + | .unwrap(); |
|
| 1275 | 1898 | assert!(r.written >= 2); |
|
| 1276 | - | assert_eq!(fs::read(root.join("a/docs/logo.png")).unwrap(), logo, "now fetched lazily"); |
|
| 1899 | + | assert_eq!( |
|
| 1900 | + | fs::read(root.join("a/docs/logo.png")).unwrap(), |
|
| 1901 | + | logo, |
|
| 1902 | + | "now fetched lazily" |
|
| 1903 | + | ); |
|
| 1277 | 1904 | assert!(!root.join("a/README.md").exists()); |
|
| 1278 | 1905 | // Origin sees the change made under a sparse checkout with the full tree intact. |
|
| 1279 | 1906 | origin.pull("origin").err(); |
|
| 1280 | - | let full = origin.load_state(&origin.bookmark_heads().unwrap()).unwrap().materialize(); |
|
| 1281 | - | assert!(matches!(&full.files["src/lib.rs"].content, FileContent::Text { bytes, .. } if bytes == b"pub fn a() {}\npub fn b() {}\n")); |
|
| 1907 | + | let full = origin |
|
| 1908 | + | .load_state(&origin.bookmark_heads().unwrap()) |
|
| 1909 | + | .unwrap() |
|
| 1910 | + | .materialize(); |
|
| 1911 | + | assert!( |
|
| 1912 | + | matches!(&full.files["src/lib.rs"].content, FileContent::Text { bytes, .. } if bytes == b"pub fn a() {}\npub fn b() {}\n") |
|
| 1913 | + | ); |
|
| 1282 | 1914 | assert!(full.files.contains_key("README.md")); |
|
| 1283 | 1915 | let _ = h1; |
|
| 1284 | 1916 | } |
|
| … | 4 unchanged lines |
||
| 1289 | 1921 | let dir = tempfile::tempdir().unwrap(); |
|
| 1290 | 1922 | let root = dir.path(); |
|
| 1291 | 1923 | let origin_dir = root.join("origin"); |
|
| 1292 | - | let origin = Repo::init(&origin_dir).unwrap().with_home(root.join("home")); |
|
| 1924 | + | let origin = Repo::init(&origin_dir) |
|
| 1925 | + | .unwrap() |
|
| 1926 | + | .with_home(root.join("home")); |
|
| 1293 | 1927 | let mut hashes = Vec::new(); |
|
| 1294 | 1928 | for i in 0..8 { |
|
| 1295 | - | write(&origin_dir, "a.txt", format!("{}\n", (0..=i).map(|k| k.to_string()).collect::<Vec<_>>().join("\n")).as_bytes()); |
|
| 1929 | + | write( |
|
| 1930 | + | &origin_dir, |
|
| 1931 | + | "a.txt", |
|
| 1932 | + | format!( |
|
| 1933 | + | "{}\n", |
|
| 1934 | + | (0..=i) |
|
| 1935 | + | .map(|k| k.to_string()) |
|
| 1936 | + | .collect::<Vec<_>>() |
|
| 1937 | + | .join("\n") |
|
| 1938 | + | ) |
|
| 1939 | + | .as_bytes(), |
|
| 1940 | + | ); |
|
| 1296 | 1941 | hashes.push(origin.commit(&format!("c{i}"), &[]).unwrap()); |
|
| 1297 | 1942 | } |
|
| 1298 | 1943 | let url = origin_dir.to_string_lossy().to_string(); |
|
| 1299 | - | let a = Repo::clone_shallow(&url, &root.join("a"), 3, None, &[]).unwrap().with_home(root.join("home")); |
|
| 1944 | + | let a = Repo::clone_shallow(&url, &root.join("a"), 3, None, &[]) |
|
| 1945 | + | .unwrap() |
|
| 1946 | + | .with_home(root.join("home")); |
|
| 1300 | 1947 | assert!(a.is_shallow()); |
|
| 1301 | - | assert_eq!(fs::read_to_string(root.join("a/a.txt")).unwrap(), "0\n1\n2\n3\n4\n5\n6\n7\n"); |
|
| 1302 | - | assert!(a.store().has(&hashes[7]) && a.store().has(&hashes[5]), "three levels present"); |
|
| 1948 | + | assert_eq!( |
|
| 1949 | + | fs::read_to_string(root.join("a/a.txt")).unwrap(), |
|
| 1950 | + | "0\n1\n2\n3\n4\n5\n6\n7\n" |
|
| 1951 | + | ); |
|
| 1952 | + | assert!( |
|
| 1953 | + | a.store().has(&hashes[7]) && a.store().has(&hashes[5]), |
|
| 1954 | + | "three levels present" |
|
| 1955 | + | ); |
|
| 1303 | 1956 | assert!(!a.store().has(&hashes[4]), "beyond the depth is absent"); |
|
| 1304 | 1957 | assert_eq!(a.boundary(), [hashes[4]].into_iter().collect()); |
|
| 1305 | - | assert_eq!(a.log().unwrap().len(), 9, "the log shows every change from the checkpoint's metadata"); |
|
| 1958 | + | assert_eq!( |
|
| 1959 | + | a.log().unwrap().len(), |
|
| 1960 | + | 9, |
|
| 1961 | + | "the log shows every change from the checkpoint's metadata" |
|
| 1962 | + | ); |
|
| 1306 | 1963 | assert!(!a.snapshot("status").unwrap().changed); |
|
| 1307 | 1964 | assert_eq!(a.quick_status().unwrap().heads, 1); |
|
| 1308 | 1965 | ||
| … | 7 unchanged lines |
||
| 1316 | 1973 | origin.pull("origin").err(); |
|
| 1317 | 1974 | let (_, wc) = origin.working_copy().unwrap(); |
|
| 1318 | 1975 | let _ = wc; |
|
| 1319 | - | origin.checkout(&origin.bookmark_heads().unwrap(), true, "co").unwrap(); |
|
| 1976 | + | origin |
|
| 1977 | + | .checkout(&origin.bookmark_heads().unwrap(), true, "co") |
|
| 1978 | + | .unwrap(); |
|
| 1320 | 1979 | write(&origin_dir, "b.txt", b"b\n"); |
|
| 1321 | 1980 | let h_b = origin.commit("b", &[]).unwrap(); |
|
| 1322 | 1981 | let pr = a.pull("origin").unwrap(); |
|
| … | 10 unchanged lines |
||
| 1333 | 1992 | a.deepen("origin", 100).unwrap(); |
|
| 1334 | 1993 | assert!(!a.is_shallow(), "boundary reached the root"); |
|
| 1335 | 1994 | assert!(a.store().has(&hashes[0])); |
|
| 1336 | - | assert_eq!(a.load_state_full(&a.bookmark_heads().unwrap()).unwrap().len(), 10); |
|
| 1995 | + | assert_eq!( |
|
| 1996 | + | a.load_state_full(&a.bookmark_heads().unwrap()) |
|
| 1997 | + | .unwrap() |
|
| 1998 | + | .len(), |
|
| 1999 | + | 10 |
|
| 2000 | + | ); |
|
| 1337 | 2001 | } |
|
| 1338 | 2002 | ||
| 1339 | 2003 | #[test] |
|
| … | 3 unchanged lines |
||
| 1343 | 2007 | let root = dir.path(); |
|
| 1344 | 2008 | let origin = Repo::init(&root.join("origin")).unwrap(); |
|
| 1345 | 2009 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 1346 | - | let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home-a")); |
|
| 1347 | - | let (key_a, _) = a.create_identity("Ada", "[email protected]", IdentityScope::User).unwrap(); |
|
| 2010 | + | let mut a = Repo::clone(&url, &root.join("a")) |
|
| 2011 | + | .unwrap() |
|
| 2012 | + | .with_home(root.join("home-a")); |
|
| 2013 | + | let (key_a, _) = a |
|
| 2014 | + | .create_identity("Ada", "[email protected]", IdentityScope::User) |
|
| 2015 | + | .unwrap(); |
|
| 1348 | 2016 | write(&root.join("a"), "f.txt", b"1\n"); |
|
| 1349 | 2017 | let h1 = a.commit("one", &[]).unwrap(); |
|
| 1350 | 2018 | write(&root.join("a"), "f.txt", b"1\n2\n"); |
|
| 1351 | 2019 | let h2 = a.commit("two", &[]).unwrap(); |
|
| 1352 | 2020 | ||
| 1353 | 2021 | let t = a.tag_create("v1", &[h1], "first release").unwrap(); |
|
| 1354 | - | assert_eq!((t.heads.clone(), t.signer.as_deref()), (vec![h1], Some("Ada"))); |
|
| 2022 | + | assert_eq!( |
|
| 2023 | + | (t.heads.clone(), t.signer.as_deref()), |
|
| 2024 | + | (vec![h1], Some("Ada")) |
|
| 2025 | + | ); |
|
| 1355 | 2026 | assert!(a.tag_create("v1", &[h2], "").is_err(), "immutable"); |
|
| 1356 | 2027 | assert!(a.tag_create("bad/name", &[h2], "").is_err()); |
|
| 1357 | 2028 | assert_eq!(a.tag_heads("v1").unwrap(), Some(vec![h1])); |
|
| … | 2 unchanged lines |
||
| 1360 | 2031 | let (_, sa) = a.tag_attest("v1").unwrap().unwrap(); |
|
| 1361 | 2032 | assert_eq!(sa.sigs[0].key, key_a); |
|
| 1362 | 2033 | // Checkout by tag. |
|
| 1363 | - | a.checkout(&a.resolve_heads("v1").unwrap(), false, "co").unwrap(); |
|
| 2034 | + | a.checkout(&a.resolve_heads("v1").unwrap(), false, "co") |
|
| 2035 | + | .unwrap(); |
|
| 1364 | 2036 | assert_eq!(fs::read(root.join("a/f.txt")).unwrap(), b"1\n"); |
|
| 1365 | - | a.checkout(&a.bookmark_heads().unwrap(), false, "co").unwrap(); |
|
| 2037 | + | a.checkout(&a.bookmark_heads().unwrap(), false, "co") |
|
| 2038 | + | .unwrap(); |
|
| 1366 | 2039 | ||
| 1367 | 2040 | // Push carries the tag; a second attestation for the name is refused. |
|
| 1368 | 2041 | let r = a.push("origin", false).unwrap(); |
|
| 1369 | 2042 | assert!(r.iter().any(|x| x.name == "tags/v1" && x.ok), "{r:?}"); |
|
| 1370 | 2043 | assert_eq!(origin.tag_heads("v1").unwrap(), Some(vec![h1])); |
|
| 1371 | - | let mut forged = a.tag_refs().unwrap().into_iter().find(|t| t.name == "v1").unwrap(); |
|
| 1372 | - | forged.state = origin.store().put_state(&StateObject::new(vec![h2])).unwrap(); |
|
| 2044 | + | let mut forged = a |
|
| 2045 | + | .tag_refs() |
|
| 2046 | + | .unwrap() |
|
| 2047 | + | .into_iter() |
|
| 2048 | + | .find(|t| t.name == "v1") |
|
| 2049 | + | .unwrap(); |
|
| 2050 | + | forged.state = origin |
|
| 2051 | + | .store() |
|
| 2052 | + | .put_state(&StateObject::new(vec![h2])) |
|
| 2053 | + | .unwrap(); |
|
| 1373 | 2054 | forged.heads = vec![h2]; |
|
| 1374 | 2055 | forged.attestation = None; |
|
| 1375 | - | let r = origin.receive_push(Bundle::default(), vec![], None, vec![forged]).unwrap(); |
|
| 2056 | + | let r = origin |
|
| 2057 | + | .receive_push(Bundle::default(), vec![], None, vec![forged]) |
|
| 2058 | + | .unwrap(); |
|
| 1376 | 2059 | assert!(!r[0].ok && r[0].reason.contains("immutable"), "{r:?}"); |
|
| 1377 | 2060 | ||
| 1378 | 2061 | // A clone gets the tag; a later tag arrives on fetch. |
|
| 1379 | - | let b = Repo::clone(&url, &root.join("b")).unwrap().with_home(root.join("home-b")); |
|
| 2062 | + | let b = Repo::clone(&url, &root.join("b")) |
|
| 2063 | + | .unwrap() |
|
| 2064 | + | .with_home(root.join("home-b")); |
|
| 1380 | 2065 | assert_eq!(b.tag_heads("v1").unwrap(), Some(vec![h1])); |
|
| 1381 | 2066 | assert!(b.tags().unwrap()[0].signer.as_deref() == Some("Ada")); |
|
| 1382 | 2067 | a.tag_create("v2", &[h2], "second").unwrap(); |
|
| … | 5 unchanged lines |
||
| 1388 | 2073 | // Under a policy, an unattested tag is refused and an attested one by the root passes. |
|
| 1389 | 2074 | a.policy_init().unwrap(); |
|
| 1390 | 2075 | a.push("origin", false).unwrap(); |
|
| 1391 | - | let unattested = crate::protocol::TagRef { name: "v3".into(), state: origin.store().put_state(&StateObject::new(vec![h2])).unwrap(), heads: vec![h2], attestation: None }; |
|
| 1392 | - | let r = origin.receive_push(Bundle::default(), vec![], None, vec![unattested]).unwrap(); |
|
| 2076 | + | let unattested = crate::protocol::TagRef { |
|
| 2077 | + | name: "v3".into(), |
|
| 2078 | + | state: origin |
|
| 2079 | + | .store() |
|
| 2080 | + | .put_state(&StateObject::new(vec![h2])) |
|
| 2081 | + | .unwrap(), |
|
| 2082 | + | heads: vec![h2], |
|
| 2083 | + | attestation: None, |
|
| 2084 | + | }; |
|
| 2085 | + | let r = origin |
|
| 2086 | + | .receive_push(Bundle::default(), vec![], None, vec![unattested]) |
|
| 2087 | + | .unwrap(); |
|
| 1393 | 2088 | assert!(!r[0].ok && r[0].reason.contains("attestation"), "{r:?}"); |
|
| 1394 | 2089 | a.tag_create("v3", &[h2], "").unwrap(); |
|
| 1395 | 2090 | let r = a.push("origin", false).unwrap(); |
|
| … | 21 unchanged lines |
||
| 1417 | 2112 | assert_ne!(ws.workspace(), repo.workspace()); |
|
| 1418 | 2113 | let list = repo.workspaces().unwrap(); |
|
| 1419 | 2114 | assert_eq!(list.len(), 2); |
|
| 1420 | - | assert!(list.iter().any(|w| w.id == ws.workspace() && w.path == ws_dir.canonicalize().unwrap() && !w.current)); |
|
| 2115 | + | assert!(list |
|
| 2116 | + | .iter() |
|
| 2117 | + | .any(|w| w.id == ws.workspace() && w.path == ws_dir.canonicalize().unwrap() && !w.current)); |
|
| 1421 | 2118 | assert!(!ws.snapshot("status").unwrap().changed); |
|
| 1422 | 2119 | ||
| 1423 | 2120 | // Work in the second workspace: its own working copy, shared bookmark. |
|
| 1424 | 2121 | write(&ws_dir, "b.txt", b"b\n"); |
|
| 1425 | 2122 | let s = ws.snapshot("status").unwrap(); |
|
| 1426 | 2123 | assert_eq!(s.statuses.len(), 1); |
|
| 1427 | - | assert!(!repo.snapshot("status").unwrap().changed, "the first workspace is untouched"); |
|
| 2124 | + | assert!( |
|
| 2125 | + | !repo.snapshot("status").unwrap().changed, |
|
| 2126 | + | "the first workspace is untouched" |
|
| 2127 | + | ); |
|
| 1428 | 2128 | let h2 = ws.commit("b from ws2", &[]).unwrap(); |
|
| 1429 | 2129 | let mut heads = repo.bookmark_heads().unwrap(); |
|
| 1430 | 2130 | heads.sort(); |
|
| … | 1 unchanged lines |
||
| 1432 | 2132 | want.sort(); |
|
| 1433 | 2133 | assert_eq!(heads, want, "both workspaces see the bookmark move"); |
|
| 1434 | 2134 | assert!(!main_dir.join("b.txt").exists()); |
|
| 1435 | - | repo.checkout(&repo.bookmark_heads().unwrap(), false, "co").unwrap(); |
|
| 2135 | + | repo.checkout(&repo.bookmark_heads().unwrap(), false, "co") |
|
| 2136 | + | .unwrap(); |
|
| 1436 | 2137 | assert!(main_dir.join("b.txt").exists()); |
|
| 1437 | 2138 | ||
| 1438 | 2139 | // Reopening from inside the second workspace finds the shared repository. |
|
| … | 8 unchanged lines |
||
| 1447 | 2148 | repo.workspace_forget(&ws.workspace()).unwrap(); |
|
| 1448 | 2149 | assert_eq!(repo.workspaces().unwrap().len(), 1); |
|
| 1449 | 2150 | assert!(ws_dir.join("b.txt").exists()); |
|
| 1450 | - | assert!(ws.snapshot("status").is_err(), "a forgotten workspace is no longer in the view"); |
|
| 2151 | + | assert!( |
|
| 2152 | + | ws.snapshot("status").is_err(), |
|
| 2153 | + | "a forgotten workspace is no longer in the view" |
|
| 2154 | + | ); |
|
| 1451 | 2155 | ||
| 1452 | 2156 | // The snapshot lock: a fresh lock blocks a second, a stale one is taken over. |
|
| 1453 | 2157 | let lock = repo.lock_snapshot().unwrap(); |
|
| 1454 | 2158 | drop(lock); |
|
| 1455 | - | let lock_path = main_dir.join(".oxy/workspaces").join(repo.workspace().to_hex()).join("lock"); |
|
| 2159 | + | let lock_path = main_dir |
|
| 2160 | + | .join(".oxy/workspaces") |
|
| 2161 | + | .join(repo.workspace().to_hex()) |
|
| 2162 | + | .join("lock"); |
|
| 1456 | 2163 | fs::write(&lock_path, "1").unwrap(); |
|
| 1457 | 2164 | let old = std::time::SystemTime::now() - std::time::Duration::from_secs(120); |
|
| 1458 | - | fs::File::open(&lock_path).unwrap().set_modified(old).unwrap(); |
|
| 2165 | + | fs::File::open(&lock_path) |
|
| 2166 | + | .unwrap() |
|
| 2167 | + | .set_modified(old) |
|
| 2168 | + | .unwrap(); |
|
| 1459 | 2169 | assert!(repo.lock_snapshot().is_ok(), "stale lock taken over"); |
|
| 1460 | 2170 | } |
|
| 1461 | 2171 | ||
| … | 3 unchanged lines |
||
| 1465 | 2175 | let dir = tempfile::tempdir().unwrap(); |
|
| 1466 | 2176 | let root = dir.path(); |
|
| 1467 | 2177 | let repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 1468 | - | write(root, ".oxyattributes", b"*.dat binary nocompress\n*.txt eol=crlf\nassets/** lockable\n"); |
|
| 2178 | + | write( |
|
| 2179 | + | root, |
|
| 2180 | + | ".oxyattributes", |
|
| 2181 | + | b"*.dat binary nocompress\n*.txt eol=crlf\nassets/** lockable\n", |
|
| 2182 | + | ); |
|
| 1469 | 2183 | write(root, "plain.dat", b"no nul bytes here\n"); |
|
| 1470 | 2184 | write(root, "notes.txt", b"one\r\ntwo\r\n"); |
|
| 1471 | 2185 | write(root, "assets/model.bin", b"\0model"); |
|
| 1472 | 2186 | let h = repo.commit("attrs", &[]).unwrap(); |
|
| 1473 | 2187 | let snap = repo.load_state(&[h]).unwrap().materialize(); |
|
| 1474 | - | assert!(snap.files["plain.dat"].mode.opaque, "binary attribute forces opaque without NUL bytes"); |
|
| 1475 | - | assert!(matches!(&snap.files["notes.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\n"), "stored with LF"); |
|
| 1476 | - | assert!(!repo.snapshot("status").unwrap().changed, "CRLF on disk equals LF in the state"); |
|
| 2188 | + | assert!( |
|
| 2189 | + | snap.files["plain.dat"].mode.opaque, |
|
| 2190 | + | "binary attribute forces opaque without NUL bytes" |
|
| 2191 | + | ); |
|
| 2192 | + | assert!( |
|
| 2193 | + | matches!(&snap.files["notes.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\n"), |
|
| 2194 | + | "stored with LF" |
|
| 2195 | + | ); |
|
| 2196 | + | assert!( |
|
| 2197 | + | !repo.snapshot("status").unwrap().changed, |
|
| 2198 | + | "CRLF on disk equals LF in the state" |
|
| 2199 | + | ); |
|
| 1477 | 2200 | // nocompress chunks pack raw. |
|
| 1478 | 2201 | let r = repo.gc(true).unwrap(); |
|
| 1479 | 2202 | assert!(r.packed.objects > 0); |
|
| … | 1 unchanged lines |
||
| 1481 | 2204 | fs::remove_file(root.join("notes.txt")).unwrap(); |
|
| 1482 | 2205 | repo.checkout(&[h], true, "co").unwrap(); |
|
| 1483 | 2206 | assert_eq!(fs::read(root.join("notes.txt")).unwrap(), b"one\r\ntwo\r\n"); |
|
| 1484 | - | assert!(fs::metadata(root.join("assets/model.bin")).unwrap().permissions().readonly()); |
|
| 2207 | + | assert!(fs::metadata(root.join("assets/model.bin")) |
|
| 2208 | + | .unwrap() |
|
| 2209 | + | .permissions() |
|
| 2210 | + | .readonly()); |
|
| 1485 | 2211 | assert!(repo.lock_path("plain.dat").is_err(), "not lockable"); |
|
| 1486 | 2212 | repo.lock_path("assets/model.bin").unwrap(); |
|
| 1487 | - | assert!(!fs::metadata(root.join("assets/model.bin")).unwrap().permissions().readonly()); |
|
| 2213 | + | assert!(!fs::metadata(root.join("assets/model.bin")) |
|
| 2214 | + | .unwrap() |
|
| 2215 | + | .permissions() |
|
| 2216 | + | .readonly()); |
|
| 1488 | 2217 | assert!(repo.is_locked("assets/model.bin")); |
|
| 1489 | 2218 | write(root, "assets/model.bin", b"\0model2"); |
|
| 1490 | 2219 | repo.commit("edit model", &[]).unwrap(); |
|
| 1491 | 2220 | repo.unlock_path("assets/model.bin").unwrap(); |
|
| 1492 | - | assert!(fs::metadata(root.join("assets/model.bin")).unwrap().permissions().readonly()); |
|
| 2221 | + | assert!(fs::metadata(root.join("assets/model.bin")) |
|
| 2222 | + | .unwrap() |
|
| 2223 | + | .permissions() |
|
| 2224 | + | .readonly()); |
|
| 1493 | 2225 | // A later checkout can still overwrite a read-only lockable file. |
|
| 1494 | 2226 | repo.checkout(&[h], true, "co").unwrap(); |
|
| 1495 | 2227 | assert_eq!(fs::read(root.join("assets/model.bin")).unwrap(), b"\0model"); |
|
| … | 24 unchanged lines |
||
| 1520 | 2252 | assert!(err.contains("pre-commit refused: no WIP commits"), "{err}"); |
|
| 1521 | 2253 | let h1 = repo.commit("first", &[]).unwrap(); |
|
| 1522 | 2254 | let pre = fs::read_to_string(root.join(".last-pre-commit")).unwrap(); |
|
| 1523 | - | assert!(pre.contains("\"event\":\"pre-commit\"") && pre.contains("\"path\":\"a.txt\"") && pre.contains("\"message\":\"first\""), "{pre}"); |
|
| 2255 | + | assert!( |
|
| 2256 | + | pre.contains("\"event\":\"pre-commit\"") |
|
| 2257 | + | && pre.contains("\"path\":\"a.txt\"") |
|
| 2258 | + | && pre.contains("\"message\":\"first\""), |
|
| 2259 | + | "{pre}" |
|
| 2260 | + | ); |
|
| 1524 | 2261 | let post = fs::read_to_string(root.join(".last-post-commit")).unwrap(); |
|
| 1525 | 2262 | assert!(post.contains(&h1.to_hex()), "{post}"); |
|
| 1526 | 2263 | repo.checkout(&[h1], false, "co").unwrap(); |
|
| 1527 | - | assert!(fs::read_to_string(root.join(".last-post-checkout")).unwrap().contains("post-checkout")); |
|
| 2264 | + | assert!(fs::read_to_string(root.join(".last-post-checkout")) |
|
| 2265 | + | .unwrap() |
|
| 2266 | + | .contains("post-checkout")); |
|
| 1528 | 2267 | repo.config.set("remote.origin.url", "/nowhere").unwrap(); |
|
| 1529 | 2268 | repo.save_config().unwrap(); |
|
| 1530 | 2269 | let err = repo.push("origin", false).unwrap_err().to_string(); |
|
| … | 1 unchanged lines |
||
| 1532 | 2271 | ||
| 1533 | 2272 | // semantic-deps: only when enabled; adds an explicit dependency. |
|
| 1534 | 2273 | write(root, "b.txt", b"b\n"); |
|
| 1535 | - | hook("semantic-deps", &format!("cat > \"$OXY_ROOT/.last-semantic\"; echo '{{\"deps\": [\"{}\"]}}'", h1.to_hex())); |
|
| 2274 | + | hook( |
|
| 2275 | + | "semantic-deps", |
|
| 2276 | + | &format!( |
|
| 2277 | + | "cat > \"$OXY_ROOT/.last-semantic\"; echo '{{\"deps\": [\"{}\"]}}'", |
|
| 2278 | + | h1.to_hex() |
|
| 2279 | + | ), |
|
| 2280 | + | ); |
|
| 1536 | 2281 | let h2 = repo.commit("b without hook", &[]).unwrap(); |
|
| 1537 | - | assert!(repo.store().get_change(&h2).unwrap().deps.is_empty(), "hook disabled by default"); |
|
| 2282 | + | assert!( |
|
| 2283 | + | repo.store().get_change(&h2).unwrap().deps.is_empty(), |
|
| 2284 | + | "hook disabled by default" |
|
| 2285 | + | ); |
|
| 1538 | 2286 | repo.config.set("hooks.semantic-deps", "true").unwrap(); |
|
| 1539 | 2287 | repo.save_config().unwrap(); |
|
| 1540 | 2288 | write(root, "c.txt", b"c\n"); |
|
| 1541 | 2289 | let h3 = repo.commit("c with hook", &[]).unwrap(); |
|
| 1542 | 2290 | assert_eq!(repo.store().get_change(&h3).unwrap().deps, vec![h1]); |
|
| 1543 | 2291 | let sem = fs::read_to_string(root.join(".last-semantic")).unwrap(); |
|
| 1544 | - | assert!(sem.contains("\"touched_paths\":[\"c.txt\"]") && sem.contains("candidate_deps"), "{sem}"); |
|
| 2292 | + | assert!( |
|
| 2293 | + | sem.contains("\"touched_paths\":[\"c.txt\"]") && sem.contains("candidate_deps"), |
|
| 2294 | + | "{sem}" |
|
| 2295 | + | ); |
|
| 1545 | 2296 | assert_eq!(json_str("a\"b\\c\n"), "\"a\\\"b\\\\c\\n\""); |
|
| 1546 | 2297 | } |
|
| 1547 | 2298 | ||
| … | 3 unchanged lines |
||
| 1551 | 2302 | let dir = tempfile::tempdir().unwrap(); |
|
| 1552 | 2303 | let root = dir.path(); |
|
| 1553 | 2304 | let mut repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 1554 | - | repo.create_identity("Ada", "a@b", IdentityScope::User).unwrap(); |
|
| 2305 | + | repo.create_identity("Ada", "a@b", IdentityScope::User) |
|
| 2306 | + | .unwrap(); |
|
| 1555 | 2307 | repo.set_user_config("core.sign", "change").unwrap(); |
|
| 1556 | 2308 | write(root, "a.txt", b"one\ntwo\n"); |
|
| 1557 | 2309 | let h0 = repo.commit("base", &[]).unwrap(); |
|
| … | 10 unchanged lines |
||
| 1568 | 2320 | let c1b = repo.store().get_change(&h1b).unwrap(); |
|
| 1569 | 2321 | assert_eq!(c1b.change_id, id1); |
|
| 1570 | 2322 | assert_eq!(c1b.deps, vec![h0]); |
|
| 1571 | - | assert!(matches!(repo.verify_change(&h1b).unwrap(), SignatureStatus::Valid { .. })); |
|
| 2323 | + | assert!(matches!( |
|
| 2324 | + | repo.verify_change(&h1b).unwrap(), |
|
| 2325 | + | SignatureStatus::Valid { .. } |
|
| 2326 | + | )); |
|
| 1572 | 2327 | let heads = repo.bookmark_heads().unwrap(); |
|
| 1573 | 2328 | assert_eq!(heads.len(), 1); |
|
| 1574 | 2329 | let top = repo.store().get_change(&heads[0]).unwrap(); |
|
| 1575 | 2330 | assert_eq!(top.message, "add three"); |
|
| 1576 | - | assert_eq!(top.deps.iter().copied().collect::<BTreeSet<_>>(), [h0, h1b].into_iter().collect(), "depends on the file's creator and the rewritten line"); |
|
| 2331 | + | assert_eq!( |
|
| 2332 | + | top.deps.iter().copied().collect::<BTreeSet<_>>(), |
|
| 2333 | + | [h0, h1b].into_iter().collect(), |
|
| 2334 | + | "depends on the file's creator and the rewritten line" |
|
| 2335 | + | ); |
|
| 1577 | 2336 | let snap = repo.load_state(&heads).unwrap().materialize(); |
|
| 1578 | - | assert!(matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\nTWO!\nthree\n")); |
|
| 1579 | - | assert!(!repo.snapshot("status").unwrap().changed, "the working copy is clean after folding"); |
|
| 2337 | + | assert!( |
|
| 2338 | + | matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\nTWO!\nthree\n") |
|
| 2339 | + | ); |
|
| 2340 | + | assert!( |
|
| 2341 | + | !repo.snapshot("status").unwrap().changed, |
|
| 2342 | + | "the working copy is clean after folding" |
|
| 2343 | + | ); |
|
| 1580 | 2344 | // Only h1b's own state shows the fix without "three". |
|
| 1581 | 2345 | let only = repo.load_state(&[h1b]).unwrap().materialize(); |
|
| 1582 | - | assert!(matches!(&only.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\nTWO!\n")); |
|
| 2346 | + | assert!( |
|
| 2347 | + | matches!(&only.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\nTWO!\n") |
|
| 2348 | + | ); |
|
| 1583 | 2349 | assert_eq!(repo.log().unwrap().len(), 4); |
|
| 1584 | 2350 | ||
| 1585 | 2351 | // An edit to a line introduced by h2 cannot fold into h1. |
|
| 1586 | 2352 | write(root, "a.txt", b"one\nTWO!\nthree!\n"); |
|
| 1587 | 2353 | let err = repo.amend_into(&h1b, None).unwrap_err().to_string(); |
|
| 1588 | 2354 | assert!(err.contains("introduced after"), "{err}"); |
|
| 1589 | - | assert!(repo.snapshot("status").unwrap().statuses.len() == 1, "nothing was changed by the refusal"); |
|
| 2355 | + | assert!( |
|
| 2356 | + | repo.snapshot("status").unwrap().statuses.len() == 1, |
|
| 2357 | + | "nothing was changed by the refusal" |
|
| 2358 | + | ); |
|
| 1590 | 2359 | write(root, "a.txt", b"one\nTWO!\nthree\n"); |
|
| 1591 | 2360 | ||
| 1592 | 2361 | // Split the head that touches two files. |
|
| … | 7 unchanged lines |
||
| 1600 | 2369 | assert!(c_1.message.contains("part 1: b.txt")); |
|
| 1601 | 2370 | assert_eq!(c_2.change_id, id3); |
|
| 1602 | 2371 | let heads: BTreeSet<_> = repo.bookmark_heads().unwrap().into_iter().collect(); |
|
| 1603 | - | assert!(heads.contains(&p1) && heads.contains(&p2), "independent parts are both heads"); |
|
| 1604 | - | let snap = repo.load_state(&repo.bookmark_heads().unwrap()).unwrap().materialize(); |
|
| 2372 | + | assert!( |
|
| 2373 | + | heads.contains(&p1) && heads.contains(&p2), |
|
| 2374 | + | "independent parts are both heads" |
|
| 2375 | + | ); |
|
| 2376 | + | let snap = repo |
|
| 2377 | + | .load_state(&repo.bookmark_heads().unwrap()) |
|
| 2378 | + | .unwrap() |
|
| 2379 | + | .materialize(); |
|
| 1605 | 2380 | assert!(snap.files.contains_key("b.txt") && snap.files.contains_key("c.txt")); |
|
| 1606 | 2381 | let only1 = repo.load_state(&[p1]).unwrap().materialize(); |
|
| 1607 | 2382 | assert!(only1.files.contains_key("b.txt") && !only1.files.contains_key("c.txt")); |
|
| … | 9 unchanged lines |
||
| 1617 | 2392 | let root = dir.path(); |
|
| 1618 | 2393 | let origin = Repo::init(&root.join("origin")).unwrap(); |
|
| 1619 | 2394 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 1620 | - | let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home-a")); |
|
| 2395 | + | let mut a = Repo::clone(&url, &root.join("a")) |
|
| 2396 | + | .unwrap() |
|
| 2397 | + | .with_home(root.join("home-a")); |
|
| 1621 | 2398 | a.set_user_config("core.sign", "change").unwrap(); |
|
| 1622 | - | let (key_a, _) = a.create_identity("Ada", "[email protected]", IdentityScope::User).unwrap(); |
|
| 2399 | + | let (key_a, _) = a |
|
| 2400 | + | .create_identity("Ada", "[email protected]", IdentityScope::User) |
|
| 2401 | + | .unwrap(); |
|
| 1623 | 2402 | write(&root.join("a"), "a.txt", b"a\n"); |
|
| 1624 | 2403 | let h1 = a.commit("one", &[]).unwrap(); |
|
| 1625 | 2404 | assert!(a.policy_init().unwrap().applied); |
|
| 1626 | 2405 | assert!(a.push("origin", false).unwrap().iter().all(|r| r.ok)); |
|
| 1627 | 2406 | ||
| 1628 | 2407 | // Expiry: an identity that had expired when the change was made does not authenticate it. |
|
| 1629 | - | let mut e = Repo::init(&root.join("e")).unwrap().with_home(root.join("home-e")); |
|
| 2408 | + | let mut e = Repo::init(&root.join("e")) |
|
| 2409 | + | .unwrap() |
|
| 2410 | + | .with_home(root.join("home-e")); |
|
| 1630 | 2411 | e.set_user_config("core.sign", "change").unwrap(); |
|
| 1631 | - | e.create_identity_with("Eve", "[email protected]", IdentityScope::User, Some(Hlc::now().wall_ms - 1)).unwrap(); |
|
| 2412 | + | e.create_identity_with( |
|
| 2413 | + | "Eve", |
|
| 2414 | + | "[email protected]", |
|
| 2415 | + | IdentityScope::User, |
|
| 2416 | + | Some(Hlc::now().wall_ms - 1), |
|
| 2417 | + | ) |
|
| 2418 | + | .unwrap(); |
|
| 1632 | 2419 | write(&root.join("e"), "e.txt", b"e\n"); |
|
| 1633 | 2420 | let he = e.commit("late", &[]).unwrap(); |
|
| 1634 | - | assert!(matches!(e.verify_change(&he).unwrap(), SignatureStatus::Invalid(m) if m.contains("expired"))); |
|
| 2421 | + | assert!( |
|
| 2422 | + | matches!(e.verify_change(&he).unwrap(), SignatureStatus::Invalid(m) if m.contains("expired")) |
|
| 2423 | + | ); |
|
| 1635 | 2424 | // Renewal re-issues the identity (same keys, new KeyId) with a later expiry. |
|
| 1636 | - | let r = e.renew_identity(Some(Hlc::now().wall_ms + 86_400_000)).unwrap(); |
|
| 2425 | + | let r = e |
|
| 2426 | + | .renew_identity(Some(Hlc::now().wall_ms + 86_400_000)) |
|
| 2427 | + | .unwrap(); |
|
| 1637 | 2428 | assert_eq!(e.identity(&r.new).unwrap().previous, Some(r.old)); |
|
| 1638 | 2429 | write(&root.join("e"), "e.txt", b"e2\n"); |
|
| 1639 | 2430 | let he2 = e.commit("in time", &[]).unwrap(); |
|
| 1640 | - | assert!(matches!(e.verify_change(&he2).unwrap(), SignatureStatus::Valid { key, .. } if key == r.new)); |
|
| 2431 | + | assert!( |
|
| 2432 | + | matches!(e.verify_change(&he2).unwrap(), SignatureStatus::Valid { key, .. } if key == r.new) |
|
| 2433 | + | ); |
|
| 1641 | 2434 | ||
| 1642 | 2435 | // Rotation: a fresh key; the old change still verifies; the policy follows. |
|
| 1643 | 2436 | let r = a.rotate_identity().unwrap(); |
|
| … | 2 unchanged lines |
||
| 1646 | 2439 | assert_eq!(a.identity(&r.new).unwrap().previous, Some(key_a)); |
|
| 1647 | 2440 | assert!(r.policy.as_ref().unwrap().applied); |
|
| 1648 | 2441 | let (_, sp) = a.current_policy().unwrap().unwrap(); |
|
| 1649 | - | assert_eq!((sp.policy.serial, sp.policy.roots.clone()), (2, vec![r.new])); |
|
| 1650 | - | assert!(!sp.policy.revoked.contains(&key_a) && sp.policy.authorizes(&r.new, "main"), "superseded, not revoked"); |
|
| 2442 | + | assert_eq!( |
|
| 2443 | + | (sp.policy.serial, sp.policy.roots.clone()), |
|
| 2444 | + | (2, vec![r.new]) |
|
| 2445 | + | ); |
|
| 2446 | + | assert!( |
|
| 2447 | + | !sp.policy.revoked.contains(&key_a) && sp.policy.authorizes(&r.new, "main"), |
|
| 2448 | + | "superseded, not revoked" |
|
| 2449 | + | ); |
|
| 1651 | 2450 | assert!(sp.policy.succeeds(&r.new, &key_a) && !sp.policy.succeeds(&key_a, &r.new)); |
|
| 1652 | - | assert!(matches!(a.verify_change(&h1).unwrap(), SignatureStatus::Valid { key, revoked: false, .. } if key == key_a)); |
|
| 2451 | + | assert!( |
|
| 2452 | + | matches!(a.verify_change(&h1).unwrap(), SignatureStatus::Valid { key, revoked: false, .. } if key == key_a) |
|
| 2453 | + | ); |
|
| 1653 | 2454 | write(&root.join("a"), "a.txt", b"a2\n"); |
|
| 1654 | 2455 | let h2 = a.commit("two", &[]).unwrap(); |
|
| 1655 | - | assert!(matches!(a.verify_change(&h2).unwrap(), SignatureStatus::Valid { key, revoked: false, .. } if key == r.new)); |
|
| 2456 | + | assert!( |
|
| 2457 | + | matches!(a.verify_change(&h2).unwrap(), SignatureStatus::Valid { key, revoked: false, .. } if key == r.new) |
|
| 2458 | + | ); |
|
| 1656 | 2459 | // Origin accepts the rotated policy (signed by the old root) and the new key's change. |
|
| 1657 | 2460 | let res = a.push("origin", false).unwrap(); |
|
| 1658 | 2461 | assert!(res.iter().all(|x| x.ok), "{res:?}"); |
|
| … | 2 unchanged lines |
||
| 1661 | 2464 | ||
| 1662 | 2465 | // Delegation: Bob may sign on Ada's behalf. Delegates live in the |
|
| 1663 | 2466 | // identity object, so Ada's KeyId changes while her keys stay the same. |
|
| 1664 | - | let mut b = Repo::clone(&url, &root.join("b")).unwrap().with_home(root.join("home-b")); |
|
| 1665 | - | let (key_b, id_b) = b.create_identity("Bob", "[email protected]", IdentityScope::User).unwrap(); |
|
| 2467 | + | let mut b = Repo::clone(&url, &root.join("b")) |
|
| 2468 | + | .unwrap() |
|
| 2469 | + | .with_home(root.join("home-b")); |
|
| 2470 | + | let (key_b, id_b) = b |
|
| 2471 | + | .create_identity("Bob", "[email protected]", IdentityScope::User) |
|
| 2472 | + | .unwrap(); |
|
| 1666 | 2473 | let r = a.delegate_add(id_b.clone()).unwrap(); |
|
| 1667 | 2474 | assert_eq!(r.old, key_a); |
|
| 1668 | 2475 | let key_a2 = r.new; |
|
| 1669 | 2476 | assert_eq!(a.identity(&key_a2).unwrap().delegates, vec![key_b]); |
|
| 1670 | - | assert_eq!(a.identity(&key_a2).unwrap().ed25519_pk, a.identity(&key_a).unwrap().ed25519_pk); |
|
| 2477 | + | assert_eq!( |
|
| 2478 | + | a.identity(&key_a2).unwrap().ed25519_pk, |
|
| 2479 | + | a.identity(&key_a).unwrap().ed25519_pk |
|
| 2480 | + | ); |
|
| 1671 | 2481 | assert!(a.delegate_add(id_b.clone()).is_err(), "already a delegate"); |
|
| 1672 | 2482 | assert!(a.push("origin", false).unwrap().iter().all(|x| x.ok)); |
|
| 1673 | 2483 | write(&root.join("a"), "a.txt", b"a3\n"); |
|
| 1674 | 2484 | let h3 = a.commit("three", &[]).unwrap(); |
|
| 1675 | - | assert!(matches!(a.verify_change(&h3).unwrap(), SignatureStatus::Valid { key, .. } if key == key_a2), "the same secret signs under the new KeyId"); |
|
| 2485 | + | assert!( |
|
| 2486 | + | matches!(a.verify_change(&h3).unwrap(), SignatureStatus::Valid { key, .. } if key == key_a2), |
|
| 2487 | + | "the same secret signs under the new KeyId" |
|
| 2488 | + | ); |
|
| 1676 | 2489 | // A change authored by Ada but signed only by Bob verifies through the delegation. |
|
| 1677 | 2490 | a.set_user_config("core.sign", "seal").unwrap(); |
|
| 1678 | 2491 | write(&root.join("a"), "a.txt", b"a4\n"); |
|
| 1679 | 2492 | let h4 = a.commit("four", &[]).unwrap(); |
|
| 1680 | 2493 | assert_eq!(a.verify_change(&h4).unwrap(), SignatureStatus::Unsigned); |
|
| 1681 | 2494 | let (_, sk_b) = b.secret_key().unwrap().unwrap(); |
|
| 1682 | - | a.store().put_signature(&h4, &sk_b.sign(Domain::Change, &key_b, &h4)).unwrap(); |
|
| 1683 | - | assert!(matches!(a.verify_change(&h4).unwrap(), SignatureStatus::Valid { key, .. } if key == key_b)); |
|
| 2495 | + | a.store() |
|
| 2496 | + | .put_signature(&h4, &sk_b.sign(Domain::Change, &key_b, &h4)) |
|
| 2497 | + | .unwrap(); |
|
| 2498 | + | assert!( |
|
| 2499 | + | matches!(a.verify_change(&h4).unwrap(), SignatureStatus::Valid { key, .. } if key == key_b) |
|
| 2500 | + | ); |
|
| 1684 | 2501 | a.set_user_config("core.sign", "change").unwrap(); |
|
| 1685 | 2502 | // Withdrawing the delegation re-issues the identity again. |
|
| 1686 | 2503 | let r = a.delegate_remove(&key_b).unwrap(); |
|
| … | 11 unchanged lines |
||
| 1698 | 2515 | assert!(a.policy_set_threshold(2).unwrap().applied); |
|
| 1699 | 2516 | assert!(a.push("origin", false).unwrap().iter().all(|x| x.ok)); |
|
| 1700 | 2517 | b.pull("origin").unwrap(); |
|
| 1701 | - | assert_eq!(b.current_policy().unwrap().unwrap().1.policy.root_threshold, 2); |
|
| 1702 | - | let mut c = Repo::init(&root.join("c")).unwrap().with_home(root.join("home-c")); |
|
| 1703 | - | let (key_c, id_c) = c.create_identity("Cy", "[email protected]", IdentityScope::User).unwrap(); |
|
| 2518 | + | assert_eq!( |
|
| 2519 | + | b.current_policy().unwrap().unwrap().1.policy.root_threshold, |
|
| 2520 | + | 2 |
|
| 2521 | + | ); |
|
| 2522 | + | let mut c = Repo::init(&root.join("c")) |
|
| 2523 | + | .unwrap() |
|
| 2524 | + | .with_home(root.join("home-c")); |
|
| 2525 | + | let (key_c, id_c) = c |
|
| 2526 | + | .create_identity("Cy", "[email protected]", IdentityScope::User) |
|
| 2527 | + | .unwrap(); |
|
| 1704 | 2528 | let serial_before = a.current_policy().unwrap().unwrap().1.policy.serial; |
|
| 1705 | 2529 | let proposal = a.policy_grant(id_c, None).unwrap(); |
|
| 1706 | 2530 | assert!(!proposal.applied && proposal.sigs.len() == 1); |
|
| 1707 | 2531 | assert_eq!(proposal.policy.serial, serial_before + 1); |
|
| 1708 | - | assert_eq!(a.current_policy().unwrap().unwrap().1.policy.serial, serial_before, "a proposal changes nothing yet"); |
|
| 2532 | + | assert_eq!( |
|
| 2533 | + | a.current_policy().unwrap().unwrap().1.policy.serial, |
|
| 2534 | + | serial_before, |
|
| 2535 | + | "a proposal changes nothing yet" |
|
| 2536 | + | ); |
|
| 1709 | 2537 | // Bob signs the proposal, handed over as a file, and it applies for him. |
|
| 1710 | 2538 | let bytes = proposal.signed().encode(); |
|
| 1711 | - | let signed = b.policy_sign(SignedPolicy::decode(&bytes).unwrap()).unwrap(); |
|
| 2539 | + | let signed = b |
|
| 2540 | + | .policy_sign(SignedPolicy::decode(&bytes).unwrap()) |
|
| 2541 | + | .unwrap(); |
|
| 1712 | 2542 | assert!(signed.applied && signed.sigs.len() == 2); |
|
| 1713 | - | assert!(b.current_policy().unwrap().unwrap().1.policy.authorizes(&key_c, "main")); |
|
| 1714 | - | assert!(b.policy_sign(signed.signed()).is_err(), "a stale proposal is refused"); |
|
| 2543 | + | assert!(b |
|
| 2544 | + | .current_policy() |
|
| 2545 | + | .unwrap() |
|
| 2546 | + | .unwrap() |
|
| 2547 | + | .1 |
|
| 2548 | + | .policy |
|
| 2549 | + | .authorizes(&key_c, "main")); |
|
| 2550 | + | assert!( |
|
| 2551 | + | b.policy_sign(signed.signed()).is_err(), |
|
| 2552 | + | "a stale proposal is refused" |
|
| 2553 | + | ); |
|
| 1715 | 2554 | // Bob publishes it and Ada picks it up. |
|
| 1716 | - | assert!(b.push("origin", false).unwrap().iter().any(|x| x.name == "policy" && x.ok)); |
|
| 2555 | + | assert!(b |
|
| 2556 | + | .push("origin", false) |
|
| 2557 | + | .unwrap() |
|
| 2558 | + | .iter() |
|
| 2559 | + | .any(|x| x.name == "policy" && x.ok)); |
|
| 1717 | 2560 | a.pull("origin").unwrap(); |
|
| 1718 | - | assert_eq!(a.current_policy().unwrap().unwrap().1.policy.serial, serial_before + 1); |
|
| 2561 | + | assert_eq!( |
|
| 2562 | + | a.current_policy().unwrap().unwrap().1.policy.serial, |
|
| 2563 | + | serial_before + 1 |
|
| 2564 | + | ); |
|
| 1719 | 2565 | // A root cannot revoke itself; revoking Cy needs both roots but is recorded locally at once. |
|
| 1720 | 2566 | assert!(a.policy_revoke(&key_a3).is_err()); |
|
| 1721 | - | let u = a.revoke_identity(&key_c, "left the project").unwrap().unwrap(); |
|
| 2567 | + | let u = a |
|
| 2568 | + | .revoke_identity(&key_c, "left the project") |
|
| 2569 | + | .unwrap() |
|
| 2570 | + | .unwrap(); |
|
| 1722 | 2571 | assert!(!u.applied); |
|
| 1723 | 2572 | assert!(a.is_revoked(&key_c).unwrap()); |
|
| 1724 | 2573 | assert!(!b.is_revoked(&key_c).unwrap()); |
|
| … | 1 unchanged lines |
||
| 1726 | 2575 | assert!(u.applied && u.policy.is_revoked(&key_c)); |
|
| 1727 | 2576 | assert!(b.is_revoked(&key_c).unwrap()); |
|
| 1728 | 2577 | // Bob can revoke Ada's root key, leaving himself as the only root (threshold 2 would be unmeetable). |
|
| 1729 | - | assert!(b.policy_revoke(&key_a3).is_err(), "would leave one root for a threshold of 2"); |
|
| 2578 | + | assert!( |
|
| 2579 | + | b.policy_revoke(&key_a3).is_err(), |
|
| 2580 | + | "would leave one root for a threshold of 2" |
|
| 2581 | + | ); |
|
| 1730 | 2582 | // Revoking one's own key without a policy root works locally: a plain marker plus attestation. |
|
| 1731 | 2583 | let key_c_own = c.config.author_key.unwrap(); |
|
| 1732 | - | assert!(c.revoke_identity(&key_c_own, "lost laptop").unwrap().is_none()); |
|
| 2584 | + | assert!(c |
|
| 2585 | + | .revoke_identity(&key_c_own, "lost laptop") |
|
| 2586 | + | .unwrap() |
|
| 2587 | + | .is_none()); |
|
| 1733 | 2588 | assert!(c.is_revoked(&key_c_own).unwrap()); |
|
| 1734 | 2589 | assert_eq!(key_c_own, key_c); |
|
| 1735 | 2590 | } |
|
| … | 4 unchanged lines |
||
| 1740 | 2595 | let dir = tempfile::tempdir().unwrap(); |
|
| 1741 | 2596 | let root = dir.path(); |
|
| 1742 | 2597 | let repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 1743 | - | write(root, "src/old.rs", b"fn a() {}\nfn b() {}\nfn c() {}\nfn d() {}\n"); |
|
| 2598 | + | write( |
|
| 2599 | + | root, |
|
| 2600 | + | "src/old.rs", |
|
| 2601 | + | b"fn a() {}\nfn b() {}\nfn c() {}\nfn d() {}\n", |
|
| 2602 | + | ); |
|
| 1744 | 2603 | write(root, "img.bin", b"\x89PNG\0\0\x01\x02"); |
|
| 1745 | 2604 | write(root, "tiny.txt", b"x\n"); |
|
| 1746 | 2605 | let h1 = repo.commit("base", &[]).unwrap(); |
|
| … | 3 unchanged lines |
||
| 1750 | 2609 | // Rename with a small edit, a binary moved unchanged, and a rewrite |
|
| 1751 | 2610 | // that shares nothing with the removed file. |
|
| 1752 | 2611 | fs::remove_file(root.join("src/old.rs")).unwrap(); |
|
| 1753 | - | write(root, "lib/new.rs", b"fn a() {}\nfn b() {}\nfn c() {}\nfn e() {}\n"); |
|
| 2612 | + | write( |
|
| 2613 | + | root, |
|
| 2614 | + | "lib/new.rs", |
|
| 2615 | + | b"fn a() {}\nfn b() {}\nfn c() {}\nfn e() {}\n", |
|
| 2616 | + | ); |
|
| 1754 | 2617 | fs::remove_file(root.join("img.bin")).unwrap(); |
|
| 1755 | 2618 | write(root, "assets/img.bin", b"\x89PNG\0\0\x01\x02"); |
|
| 1756 | 2619 | fs::remove_file(root.join("tiny.txt")).unwrap(); |
|
| 1757 | 2620 | write(root, "other.txt", b"completely different\n"); |
|
| 1758 | 2621 | let snap = repo.snapshot("status").unwrap(); |
|
| 1759 | - | let statuses: Vec<(&str, ChangeStatus)> = snap.statuses.iter().map(|s| (s.path.as_str(), s.status)).collect(); |
|
| 2622 | + | let statuses: Vec<(&str, ChangeStatus)> = snap |
|
| 2623 | + | .statuses |
|
| 2624 | + | .iter() |
|
| 2625 | + | .map(|s| (s.path.as_str(), s.status)) |
|
| 2626 | + | .collect(); |
|
| 1760 | 2627 | assert_eq!( |
|
| 1761 | 2628 | statuses, |
|
| 1762 | - | vec![("assets/img.bin", ChangeStatus::Renamed), ("lib/new.rs", ChangeStatus::Renamed), ("other.txt", ChangeStatus::Added), ("src", ChangeStatus::Removed), ("tiny.txt", ChangeStatus::Removed)] |
|
| 2629 | + | vec![ |
|
| 2630 | + | ("assets/img.bin", ChangeStatus::Renamed), |
|
| 2631 | + | ("lib/new.rs", ChangeStatus::Renamed), |
|
| 2632 | + | ("other.txt", ChangeStatus::Added), |
|
| 2633 | + | ("src", ChangeStatus::Removed), |
|
| 2634 | + | ("tiny.txt", ChangeStatus::Removed) |
|
| 2635 | + | ] |
|
| 1763 | 2636 | ); |
|
| 1764 | 2637 | let h2 = repo.commit("move things", &[]).unwrap(); |
|
| 1765 | 2638 | let c = repo.store().get_change(&h2).unwrap(); |
|
| 1766 | - | let moves: Vec<_> = c.tree_ops.iter().filter(|op| matches!(op, TreeOp::Move { .. })).collect(); |
|
| 2639 | + | let moves: Vec<_> = c |
|
| 2640 | + | .tree_ops |
|
| 2641 | + | .iter() |
|
| 2642 | + | .filter(|op| matches!(op, TreeOp::Move { .. })) |
|
| 2643 | + | .collect(); |
|
| 1767 | 2644 | assert_eq!(moves.len(), 2, "{:?}", c.tree_ops); |
|
| 1768 | - | assert!(c.tree_ops.iter().any(|op| matches!(op, TreeOp::Move { node, name, .. } if *node == node_old && name == "new.rs"))); |
|
| 1769 | - | assert!(c.tree_ops.iter().any(|op| matches!(op, TreeOp::Move { node, name, .. } if *node == node_img && name == "img.bin"))); |
|
| 2645 | + | assert!(c.tree_ops.iter().any( |
|
| 2646 | + | |op| matches!(op, TreeOp::Move { node, name, .. } if *node == node_old && name == "new.rs") |
|
| 2647 | + | )); |
|
| 2648 | + | assert!(c.tree_ops.iter().any( |
|
| 2649 | + | |op| matches!(op, TreeOp::Move { node, name, .. } if *node == node_img && name == "img.bin") |
|
| 2650 | + | )); |
|
| 1770 | 2651 | let snap = repo.load_state(&[h2]).unwrap().materialize(); |
|
| 1771 | - | assert_eq!(snap.files["lib/new.rs"].node, node_old, "the node survives the rename"); |
|
| 1772 | - | assert!(matches!(&snap.files["lib/new.rs"].content, FileContent::Text { bytes, .. } if bytes == b"fn a() {}\nfn b() {}\nfn c() {}\nfn e() {}\n")); |
|
| 2652 | + | assert_eq!( |
|
| 2653 | + | snap.files["lib/new.rs"].node, node_old, |
|
| 2654 | + | "the node survives the rename" |
|
| 2655 | + | ); |
|
| 2656 | + | assert!( |
|
| 2657 | + | matches!(&snap.files["lib/new.rs"].content, FileContent::Text { bytes, .. } if bytes == b"fn a() {}\nfn b() {}\nfn c() {}\nfn e() {}\n") |
|
| 2658 | + | ); |
|
| 1773 | 2659 | assert!(!snap.files.contains_key("src/old.rs") && !snap.files.contains_key("src")); |
|
| 1774 | - | assert!(render_change(&c, &repo.load_state(&[h1]).unwrap(), &repo.load_state(&[h1]).unwrap().materialize()).contains("R ")); |
|
| 2660 | + | assert!(render_change( |
|
| 2661 | + | &c, |
|
| 2662 | + | &repo.load_state(&[h1]).unwrap(), |
|
| 2663 | + | &repo.load_state(&[h1]).unwrap().materialize() |
|
| 2664 | + | ) |
|
| 2665 | + | .contains("R ")); |
|
| 1775 | 2666 | // A fresh checkout reproduces the tree. |
|
| 1776 | 2667 | let other = tempfile::tempdir().unwrap(); |
|
| 1777 | 2668 | let clone = Repo::clone(&root.to_string_lossy(), other.path()).unwrap(); |
|
| 1778 | - | assert_eq!(fs::read(other.path().join("lib/new.rs")).unwrap(), b"fn a() {}\nfn b() {}\nfn c() {}\nfn e() {}\n"); |
|
| 2669 | + | assert_eq!( |
|
| 2670 | + | fs::read(other.path().join("lib/new.rs")).unwrap(), |
|
| 2671 | + | b"fn a() {}\nfn b() {}\nfn c() {}\nfn e() {}\n" |
|
| 2672 | + | ); |
|
| 1779 | 2673 | assert!(!other.path().join("src").exists()); |
|
| 1780 | 2674 | assert!(!clone.snapshot("status").unwrap().changed); |
|
| 1781 | 2675 | // Similarity is line based, byte weighted. |
|
| … | 6 unchanged lines |
||
| 1788 | 2682 | #[test] |
|
| 1789 | 2683 | fn git_fetch_and_push_round_trip() { |
|
| 1790 | 2684 | hermetic(); |
|
| 1791 | - | if std::process::Command::new("git").arg("--version").output().is_err() { |
|
| 2685 | + | if std::process::Command::new("git") |
|
| 2686 | + | .arg("--version") |
|
| 2687 | + | .output() |
|
| 2688 | + | .is_err() |
|
| 2689 | + | { |
|
| 1792 | 2690 | eprintln!("git not installed; skipping"); |
|
| 1793 | 2691 | return; |
|
| 1794 | 2692 | } |
|
| … | 3 unchanged lines |
||
| 1798 | 2696 | let out = std::process::Command::new("git") |
|
| 1799 | 2697 | .arg("-C") |
|
| 1800 | 2698 | .arg(d) |
|
| 1801 | - | .args(["-c", "user.name=Grace", "-c", "[email protected]", "-c", "commit.gpgsign=false"]) |
|
| 2699 | + | .args([ |
|
| 2700 | + | "-c", |
|
| 2701 | + | "user.name=Grace", |
|
| 2702 | + | "-c", |
|
| 2703 | + | "[email protected]", |
|
| 2704 | + | "-c", |
|
| 2705 | + | "commit.gpgsign=false", |
|
| 2706 | + | ]) |
|
| 1802 | 2707 | .args(args) |
|
| 1803 | 2708 | .output() |
|
| 1804 | 2709 | .unwrap(); |
|
| 1805 | - | assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); |
|
| 2710 | + | assert!( |
|
| 2711 | + | out.status.success(), |
|
| 2712 | + | "git {args:?}: {}", |
|
| 2713 | + | String::from_utf8_lossy(&out.stderr) |
|
| 2714 | + | ); |
|
| 1806 | 2715 | String::from_utf8_lossy(&out.stdout).trim().to_string() |
|
| 1807 | 2716 | }; |
|
| 1808 | 2717 | // A bare "remote" and a work tree that pushes to it. |
|
| … | 1 unchanged lines |
||
| 1810 | 2719 | fs::create_dir_all(&remote).unwrap(); |
|
| 1811 | 2720 | git_in(&remote, &["init", "-q", "--bare", "-b", "main"]); |
|
| 1812 | 2721 | let work = root.join("work"); |
|
| 1813 | - | git_in(root, &["clone", "-q", remote.to_str().unwrap(), work.to_str().unwrap()]); |
|
| 2722 | + | git_in( |
|
| 2723 | + | root, |
|
| 2724 | + | &[ |
|
| 2725 | + | "clone", |
|
| 2726 | + | "-q", |
|
| 2727 | + | remote.to_str().unwrap(), |
|
| 2728 | + | work.to_str().unwrap(), |
|
| 2729 | + | ], |
|
| 2730 | + | ); |
|
| 1814 | 2731 | git_in(&work, &["checkout", "-q", "-b", "main"]); |
|
| 1815 | 2732 | write(&work, "README.md", b"# Project\n"); |
|
| 1816 | 2733 | write(&work, "src/lib.rs", b"pub fn a() {}\n"); |
|
| … | 7 unchanged lines |
||
| 1824 | 2741 | // Fetch into a fresh oxy repository. |
|
| 1825 | 2742 | let o = root.join("oxy"); |
|
| 1826 | 2743 | let mut repo = Repo::init(&o).unwrap().with_home(root.join("home")); |
|
| 1827 | - | repo.create_identity("Ada", "[email protected]", IdentityScope::User).unwrap(); |
|
| 2744 | + | repo.create_identity("Ada", "[email protected]", IdentityScope::User) |
|
| 2745 | + | .unwrap(); |
|
| 1828 | 2746 | let url = remote.to_string_lossy().to_string(); |
|
| 1829 | 2747 | let r = repo.git_fetch(&url, "main").unwrap(); |
|
| 1830 | 2748 | assert_eq!((r.commits, r.heads), (2, 1)); |
|
| 1831 | - | assert_eq!(fs::read(o.join("src/lib.rs")).unwrap(), b"pub fn a() {}\npub fn b() {}\n"); |
|
| 2749 | + | assert_eq!( |
|
| 2750 | + | fs::read(o.join("src/lib.rs")).unwrap(), |
|
| 2751 | + | b"pub fn a() {}\npub fn b() {}\n" |
|
| 2752 | + | ); |
|
| 1832 | 2753 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 1833 | - | assert_eq!(repo.git_fetch(&url, "main").unwrap().commits, 0, "nothing new"); |
|
| 2754 | + | assert_eq!( |
|
| 2755 | + | repo.git_fetch(&url, "main").unwrap().commits, |
|
| 2756 | + | 0, |
|
| 2757 | + | "nothing new" |
|
| 2758 | + | ); |
|
| 1834 | 2759 | ||
| 1835 | 2760 | // An oxy change, pushed back: one commit on top of the Git tip. |
|
| 1836 | - | write(&o, "src/lib.rs", b"pub fn a() {}\npub fn b() {}\npub fn c() {}\n"); |
|
| 2761 | + | write( |
|
| 2762 | + | &o, |
|
| 2763 | + | "src/lib.rs", |
|
| 2764 | + | b"pub fn a() {}\npub fn b() {}\npub fn c() {}\n", |
|
| 2765 | + | ); |
|
| 1837 | 2766 | write(&o, "NOTES.md", b"from oxy\n"); |
|
| 1838 | 2767 | let h_oxy = repo.commit("add c and notes", &[]).unwrap(); |
|
| 1839 | 2768 | let r = repo.git_push(&url, "main").unwrap(); |
|
| 1840 | 2769 | assert_eq!(r.exported, 1, "{r:?}"); |
|
| 1841 | 2770 | git_in(&work, &["pull", "-q", "--ff-only", "origin", "main"]); |
|
| 1842 | - | assert_eq!(git_in(&work, &["log", "--format=%s", "main"]), "add c and notes\nsecond\ninitial"); |
|
| 2771 | + | assert_eq!( |
|
| 2772 | + | git_in(&work, &["log", "--format=%s", "main"]), |
|
| 2773 | + | "add c and notes\nsecond\ninitial" |
|
| 2774 | + | ); |
|
| 1843 | 2775 | assert_eq!(git_in(&work, &["rev-parse", "HEAD~1"]), tip1); |
|
| 1844 | 2776 | assert_eq!(fs::read(work.join("NOTES.md")).unwrap(), b"from oxy\n"); |
|
| 1845 | 2777 | assert_eq!(git_in(&work, &["log", "-1", "--format=%an", "main"]), "Ada"); |
|
| 1846 | - | assert!(git_in(&work, &["log", "-1", "--format=%B", "main"]).contains(&format!("Oxy-Hash: {}", h_oxy.to_hex()))); |
|
| 1847 | - | assert_eq!(repo.git_push(&url, "main").unwrap().exported, 0, "already pushed"); |
|
| 2778 | + | assert!(git_in(&work, &["log", "-1", "--format=%B", "main"]) |
|
| 2779 | + | .contains(&format!("Oxy-Hash: {}", h_oxy.to_hex()))); |
|
| 2780 | + | assert_eq!( |
|
| 2781 | + | repo.git_push(&url, "main").unwrap().exported, |
|
| 2782 | + | 0, |
|
| 2783 | + | "already pushed" |
|
| 2784 | + | ); |
|
| 1848 | 2785 | ||
| 1849 | 2786 | // Git moves on; pushing without fetching is refused, fetching brings |
|
| 1850 | 2787 | // the commit in and the working copy follows. |
|
| … | 1 unchanged lines |
||
| 1852 | 2789 | git_in(&work, &["commit", "-q", "-am", "readme from git"]); |
|
| 1853 | 2790 | git_in(&work, &["push", "-q", "origin", "main"]); |
|
| 1854 | 2791 | let tip2 = git_in(&work, &["rev-parse", "HEAD"]); |
|
| 1855 | - | assert!(repo.git_push(&url, "main").unwrap_err().to_string().contains("fetch")); |
|
| 2792 | + | assert!(repo |
|
| 2793 | + | .git_push(&url, "main") |
|
| 2794 | + | .unwrap_err() |
|
| 2795 | + | .to_string() |
|
| 2796 | + | .contains("fetch")); |
|
| 1856 | 2797 | let r = repo.git_fetch(&url, "main").unwrap(); |
|
| 1857 | - | assert_eq!((r.commits, r.heads), (1, 2), "the Git change and the oxy change commute"); |
|
| 1858 | - | assert_eq!(fs::read(o.join("README.md")).unwrap(), b"# Project\n\nMore.\n"); |
|
| 2798 | + | assert_eq!( |
|
| 2799 | + | (r.commits, r.heads), |
|
| 2800 | + | (1, 2), |
|
| 2801 | + | "the Git change and the oxy change commute" |
|
| 2802 | + | ); |
|
| 2803 | + | assert_eq!( |
|
| 2804 | + | fs::read(o.join("README.md")).unwrap(), |
|
| 2805 | + | b"# Project\n\nMore.\n" |
|
| 2806 | + | ); |
|
| 1859 | 2807 | assert_eq!(fs::read(o.join("NOTES.md")).unwrap(), b"from oxy\n"); |
|
| 1860 | 2808 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 1861 | 2809 | // Another oxy change pushes on top of Git's newest commit. |
|
| … | 2 unchanged lines |
||
| 1864 | 2812 | assert_eq!(repo.git_push(&url, "main").unwrap().exported, 1); |
|
| 1865 | 2813 | git_in(&work, &["pull", "-q", "--ff-only", "origin", "main"]); |
|
| 1866 | 2814 | assert_eq!(git_in(&work, &["rev-parse", "HEAD~1"]), tip2); |
|
| 1867 | - | assert_eq!(fs::read(work.join("NOTES.md")).unwrap(), b"from oxy\nagain\n"); |
|
| 1868 | - | assert_eq!(git_in(&work, &["log", "--format=%s", "main"]).lines().count(), 5); |
|
| 2815 | + | assert_eq!( |
|
| 2816 | + | fs::read(work.join("NOTES.md")).unwrap(), |
|
| 2817 | + | b"from oxy\nagain\n" |
|
| 2818 | + | ); |
|
| 2819 | + | assert_eq!( |
|
| 2820 | + | git_in(&work, &["log", "--format=%s", "main"]) |
|
| 2821 | + | .lines() |
|
| 2822 | + | .count(), |
|
| 2823 | + | 5 |
|
| 2824 | + | ); |
|
| 1869 | 2825 | } |
|
| 1870 | 2826 | ||
| 1871 | 2827 | /// Benchmark, not a gate: `cargo test --release -p oxy-core --lib bench_one_gibibyte -- --ignored --nocapture`. |
|
| … | 8 unchanged lines |
||
| 1880 | 2836 | None => tempfile::tempdir().unwrap(), |
|
| 1881 | 2837 | }; |
|
| 1882 | 2838 | let root = dir.path(); |
|
| 1883 | - | let repo = Repo::init(&root.join("r")).unwrap().with_home(root.join("home")); |
|
| 2839 | + | let repo = Repo::init(&root.join("r")) |
|
| 2840 | + | .unwrap() |
|
| 2841 | + | .with_home(root.join("home")); |
|
| 1884 | 2842 | let size = 1usize << 30; |
|
| 1885 | 2843 | let mut data = vec![0u8; size]; |
|
| 1886 | 2844 | let mut x = 0x9e3779b97f4a7c15u64; |
|
| … | 28 unchanged lines |
||
| 1915 | 2873 | eprintln!("read back and verify: {:.2?}", t.elapsed()); |
|
| 1916 | 2874 | assert_eq!(clone.bookmark_heads().unwrap(), vec![h]); |
|
| 1917 | 2875 | let store_bytes: u64 = walkdir_size(&root.join("r/.oxy/objects")); |
|
| 1918 | - | eprintln!("store size: {:.1} MiB for 1 GiB of data", store_bytes as f64 / (1024.0 * 1024.0)); |
|
| 2876 | + | eprintln!( |
|
| 2877 | + | "store size: {:.1} MiB for 1 GiB of data", |
|
| 2878 | + | store_bytes as f64 / (1024.0 * 1024.0) |
|
| 2879 | + | ); |
|
| 1919 | 2880 | } |
|
| 1920 | 2881 | ||
| 1921 | 2882 | fn walkdir_size(p: &Path) -> u64 { |
|
| … | 30 unchanged lines |
||
| 1952 | 2913 | write(root, "a.txt", b"text\n"); |
|
| 1953 | 2914 | let h1 = repo.commit("base", &[]).unwrap(); |
|
| 1954 | 2915 | let (_, _, m) = repo.read_manifest().unwrap().unwrap(); |
|
| 1955 | - | assert!(m.entries["big.bin"].blob.is_some(), "opaque files record their blob"); |
|
| 2916 | + | assert!( |
|
| 2917 | + | m.entries["big.bin"].blob.is_some(), |
|
| 2918 | + | "opaque files record their blob" |
|
| 2919 | + | ); |
|
| 1956 | 2920 | assert!(m.entries["a.txt"].blob.is_none()); |
|
| 1957 | 2921 | let blob = m.entries["big.bin"].blob.unwrap(); |
|
| 1958 | 2922 | assert!(repo.store().has(&blob)); |
|
| … | 1 unchanged lines |
||
| 1960 | 2924 | // Files modified within two seconds of the manifest are racy and still |
|
| 1961 | 2925 | // read; age the file so the cache applies, then snapshot to record it. |
|
| 1962 | 2926 | let old = std::time::SystemTime::now() - std::time::Duration::from_secs(30); |
|
| 1963 | - | fs::File::options().write(true).open(root.join("big.bin")).unwrap().set_modified(old).unwrap(); |
|
| 2927 | + | fs::File::options() |
|
| 2928 | + | .write(true) |
|
| 2929 | + | .open(root.join("big.bin")) |
|
| 2930 | + | .unwrap() |
|
| 2931 | + | .set_modified(old) |
|
| 2932 | + | .unwrap(); |
|
| 1964 | 2933 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 1965 | 2934 | // Now the file cannot be read at all; the snapshot succeeds without it. |
|
| 1966 | 2935 | fs::set_permissions(root.join("big.bin"), fs::Permissions::from_mode(0o000)).unwrap(); |
|
| 1967 | 2936 | let snap = repo.snapshot("status").unwrap(); |
|
| 1968 | - | assert!(!snap.changed && snap.statuses.is_empty(), "{:?}", snap.statuses); |
|
| 2937 | + | assert!( |
|
| 2938 | + | !snap.changed && snap.statuses.is_empty(), |
|
| 2939 | + | "{:?}", |
|
| 2940 | + | snap.statuses |
|
| 2941 | + | ); |
|
| 1969 | 2942 | fs::set_permissions(root.join("big.bin"), fs::Permissions::from_mode(0o644)).unwrap(); |
|
| 1970 | 2943 | // A real change (new mtime, new content) is seen and committed correctly. |
|
| 1971 | 2944 | let mut big2 = big.clone(); |
|
| 1972 | 2945 | big2[10] ^= 0xff; |
|
| 1973 | 2946 | write(root, "big.bin", &big2); |
|
| 1974 | 2947 | let snap = repo.snapshot("status").unwrap(); |
|
| 1975 | - | assert_eq!(snap.statuses.iter().map(|s| (s.path.as_str(), s.status)).collect::<Vec<_>>(), vec![("big.bin", ChangeStatus::Modified)]); |
|
| 2948 | + | assert_eq!( |
|
| 2949 | + | snap.statuses |
|
| 2950 | + | .iter() |
|
| 2951 | + | .map(|s| (s.path.as_str(), s.status)) |
|
| 2952 | + | .collect::<Vec<_>>(), |
|
| 2953 | + | vec![("big.bin", ChangeStatus::Modified)] |
|
| 2954 | + | ); |
|
| 1976 | 2955 | let h2 = repo.commit("edit", &[]).unwrap(); |
|
| 1977 | 2956 | let files = repo.load_state(&[h2]).unwrap().materialize().files; |
|
| 1978 | - | assert_eq!(files["big.bin"].content, FileContent::Opaque { blobs: vec![Repo::blob_ref(&big2)] }); |
|
| 2957 | + | assert_eq!( |
|
| 2958 | + | files["big.bin"].content, |
|
| 2959 | + | FileContent::Opaque { |
|
| 2960 | + | blobs: vec![Repo::blob_ref(&big2)] |
|
| 2961 | + | } |
|
| 2962 | + | ); |
|
| 1979 | 2963 | // Checkout writes a manifest with blobs too, so the next status is cached. |
|
| 1980 | 2964 | repo.checkout(&[h1], false, "checkout").unwrap(); |
|
| 1981 | 2965 | let (_, _, m) = repo.read_manifest().unwrap().unwrap(); |
|
| … | 21 unchanged lines |
||
| 2003 | 2987 | let ida = repo.store().get_change(&ha).unwrap().change_id.to_hex(); |
|
| 2004 | 2988 | let idb = repo.store().get_change(&hb).unwrap().change_id.to_hex(); |
|
| 2005 | 2989 | assert!(json.contains("\"conflicted\":true"), "{json}"); |
|
| 2006 | - | assert!(json.contains("\"kind\":\"text\",\"path\":\"a.txt\""), "{json}"); |
|
| 2990 | + | assert!( |
|
| 2991 | + | json.contains("\"kind\":\"text\",\"path\":\"a.txt\""), |
|
| 2992 | + | "{json}" |
|
| 2993 | + | ); |
|
| 2007 | 2994 | assert!(json.contains(&ida) && json.contains(&idb), "{json}"); |
|
| 2008 | 2995 | assert!(json.contains("\"resolved\":false"), "{json}"); |
|
| 2009 | 2996 | assert!(json.starts_with('{') && json.ends_with('}')); |
|
| … | 5 unchanged lines |
||
| 2015 | 3002 | repo.commit("resolve", &[]).unwrap(); |
|
| 2016 | 3003 | let snap = repo.snapshot("status").unwrap(); |
|
| 2017 | 3004 | let json = repo.status_json(&snap).unwrap(); |
|
| 2018 | - | assert!(json.contains("\"clean\":true,\"conflicted\":false,\"paths\":[],\"conflicts\":[]"), "{json}"); |
|
| 3005 | + | assert!( |
|
| 3006 | + | json.contains("\"clean\":true,\"conflicted\":false,\"paths\":[],\"conflicts\":[]"), |
|
| 3007 | + | "{json}" |
|
| 3008 | + | ); |
|
| 2019 | 3009 | } |
|
| 2020 | 3010 | ||
| 2021 | 3011 | #[test] |
|
| … | 9 unchanged lines |
||
| 2031 | 3021 | write(root, ".oxyignore", b"/build\n*.log\n!sub/keep.log\n"); |
|
| 2032 | 3022 | let snap = repo.snapshot("status").unwrap(); |
|
| 2033 | 3023 | let added: Vec<&str> = snap.statuses.iter().map(|s| s.path.as_str()).collect(); |
|
| 2034 | - | assert_eq!(added, vec![".oxyignore", "src.c", "sub/keep.log"], "anchored, glob, and negated rules"); |
|
| 3024 | + | assert_eq!( |
|
| 3025 | + | added, |
|
| 3026 | + | vec![".oxyignore", "src.c", "sub/keep.log"], |
|
| 3027 | + | "anchored, glob, and negated rules" |
|
| 3028 | + | ); |
|
| 2035 | 3029 | let h1 = repo.commit("base", &[]).unwrap(); |
|
| 2036 | 3030 | // Ignoring a tracked file changes nothing about it. |
|
| 2037 | 3031 | write(root, ".oxyignore", b"/build\n*.log\n!sub/keep.log\nsrc.c\n"); |
|
| 2038 | 3032 | let snap = repo.snapshot("status").unwrap(); |
|
| 2039 | - | assert_eq!(snap.statuses.iter().map(|s| s.path.as_str()).collect::<Vec<_>>(), vec![".oxyignore"]); |
|
| 3033 | + | assert_eq!( |
|
| 3034 | + | snap.statuses |
|
| 3035 | + | .iter() |
|
| 3036 | + | .map(|s| s.path.as_str()) |
|
| 3037 | + | .collect::<Vec<_>>(), |
|
| 3038 | + | vec![".oxyignore"] |
|
| 3039 | + | ); |
|
| 2040 | 3040 | let h2 = repo.commit("ignore src.c", &[]).unwrap(); |
|
| 2041 | - | assert!(repo.load_state(&[h2]).unwrap().materialize().files.contains_key("src.c")); |
|
| 3041 | + | assert!(repo |
|
| 3042 | + | .load_state(&[h2]) |
|
| 3043 | + | .unwrap() |
|
| 3044 | + | .materialize() |
|
| 3045 | + | .files |
|
| 3046 | + | .contains_key("src.c")); |
|
| 2042 | 3047 | // Its edits are still seen, by status and by the prompt. |
|
| 2043 | 3048 | assert_eq!(repo.quick_dirty().unwrap(), Some(false)); |
|
| 2044 | 3049 | write(root, "src.c", b"int x, y;\n"); |
|
| 2045 | 3050 | assert_eq!(repo.quick_dirty().unwrap(), Some(true)); |
|
| 2046 | 3051 | let snap = repo.snapshot("status").unwrap(); |
|
| 2047 | - | assert_eq!(snap.statuses.iter().map(|s| (s.path.as_str(), s.status)).collect::<Vec<_>>(), vec![("src.c", ChangeStatus::Modified)]); |
|
| 3052 | + | assert_eq!( |
|
| 3053 | + | snap.statuses |
|
| 3054 | + | .iter() |
|
| 3055 | + | .map(|s| (s.path.as_str(), s.status)) |
|
| 3056 | + | .collect::<Vec<_>>(), |
|
| 3057 | + | vec![("src.c", ChangeStatus::Modified)] |
|
| 3058 | + | ); |
|
| 2048 | 3059 | let h3 = repo.commit("edit src.c", &[]).unwrap(); |
|
| 2049 | - | assert!(matches!(&repo.load_state(&[h3]).unwrap().materialize().files["src.c"].content, FileContent::Text { bytes, .. } if bytes == b"int x, y;\n")); |
|
| 3060 | + | assert!( |
|
| 3061 | + | matches!(&repo.load_state(&[h3]).unwrap().materialize().files["src.c"].content, FileContent::Text { bytes, .. } if bytes == b"int x, y;\n") |
|
| 3062 | + | ); |
|
| 2050 | 3063 | // Deleting it on disk removes it, ignored or not; a checkout brings it back. |
|
| 2051 | 3064 | fs::remove_file(root.join("src.c")).unwrap(); |
|
| 2052 | 3065 | let snap = repo.snapshot("status").unwrap(); |
|
| 2053 | - | assert_eq!(snap.statuses.iter().map(|s| (s.path.as_str(), s.status)).collect::<Vec<_>>(), vec![("src.c", ChangeStatus::Removed)]); |
|
| 3066 | + | assert_eq!( |
|
| 3067 | + | snap.statuses |
|
| 3068 | + | .iter() |
|
| 3069 | + | .map(|s| (s.path.as_str(), s.status)) |
|
| 3070 | + | .collect::<Vec<_>>(), |
|
| 3071 | + | vec![("src.c", ChangeStatus::Removed)] |
|
| 3072 | + | ); |
|
| 2054 | 3073 | repo.checkout(&[h3], true, "checkout").unwrap(); |
|
| 2055 | 3074 | assert_eq!(fs::read(root.join("src.c")).unwrap(), b"int x, y;\n"); |
|
| 2056 | 3075 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| … | 16 unchanged lines |
||
| 2073 | 3092 | // h1 is not a head: split it by node. |
|
| 2074 | 3093 | let (hj, h1b) = repo.split(&h1, &["junk".into()]).unwrap(); |
|
| 2075 | 3094 | assert_ne!(h1b, h1); |
|
| 2076 | - | assert_eq!(repo.store().get_change(&h1b).unwrap().change_id, id1, "the rest keeps the ChangeId"); |
|
| 3095 | + | assert_eq!( |
|
| 3096 | + | repo.store().get_change(&h1b).unwrap().change_id, |
|
| 3097 | + | id1, |
|
| 3098 | + | "the rest keeps the ChangeId" |
|
| 3099 | + | ); |
|
| 2077 | 3100 | let junk = repo.store().get_change(&hj).unwrap(); |
|
| 2078 | - | assert!(junk.tree_ops.iter().all(|op| matches!(op, TreeOp::AddNode { name, .. } if name == "junk" || name == "x.lock"))); |
|
| 3101 | + | assert!(junk.tree_ops.iter().all( |
|
| 3102 | + | |op| matches!(op, TreeOp::AddNode { name, .. } if name == "junk" || name == "x.lock") |
|
| 3103 | + | )); |
|
| 2079 | 3104 | let heads = repo.bookmark_heads().unwrap(); |
|
| 2080 | - | assert!(heads.contains(&hj), "the split-off part is a head of its own"); |
|
| 3105 | + | assert!( |
|
| 3106 | + | heads.contains(&hj), |
|
| 3107 | + | "the split-off part is a head of its own" |
|
| 3108 | + | ); |
|
| 2081 | 3109 | let snap = repo.load_state(&heads).unwrap().materialize(); |
|
| 2082 | - | assert!(matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\n"), "the dependent was rewritten onto the new hash"); |
|
| 3110 | + | assert!( |
|
| 3111 | + | matches!(&snap.files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"one\ntwo\n"), |
|
| 3112 | + | "the dependent was rewritten onto the new hash" |
|
| 3113 | + | ); |
|
| 2083 | 3114 | assert!(snap.files.contains_key("junk/x.lock")); |
|
| 2084 | - | let h2b = heads.iter().find(|h| repo.store().get_change(h).unwrap().message == "more work").copied().unwrap(); |
|
| 3115 | + | let h2b = heads |
|
| 3116 | + | .iter() |
|
| 3117 | + | .find(|h| repo.store().get_change(h).unwrap().message == "more work") |
|
| 3118 | + | .copied() |
|
| 3119 | + | .unwrap(); |
|
| 2085 | 3120 | assert_eq!(repo.store().get_change(&h2b).unwrap().deps, vec![h1b]); |
|
| 2086 | 3121 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 2087 | 3122 | // Abandoning the split-off head removes the junk from history. |
|
| … | 19 unchanged lines |
||
| 2107 | 3142 | let root = dir.path(); |
|
| 2108 | 3143 | let origin = Repo::init(&root.join("origin")).unwrap(); |
|
| 2109 | 3144 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 2110 | - | let mut repo = Repo::clone(&url, &root.join("r")).unwrap().with_home(root.join("home")); |
|
| 3145 | + | let mut repo = Repo::clone(&url, &root.join("r")) |
|
| 3146 | + | .unwrap() |
|
| 3147 | + | .with_home(root.join("home")); |
|
| 2111 | 3148 | let r = root.join("r"); |
|
| 2112 | 3149 | write(&r, "a.txt", b"a\n"); |
|
| 2113 | 3150 | let h_a = repo.commit("a", &[]).unwrap(); |
|
| … | 9 unchanged lines |
||
| 2123 | 3160 | write(&r, "a.txt", b"a\nb\n"); |
|
| 2124 | 3161 | write(&r, "b.txt", b"b\n"); |
|
| 2125 | 3162 | let h_b = repo.commit("b on feature", &[]).unwrap(); |
|
| 2126 | - | assert_eq!(repo.bookmark_heads().unwrap(), vec![h_b], "b edits a.txt, so it depends on a"); |
|
| 2127 | - | assert_eq!(repo.resolve_heads("main").unwrap(), vec![h_a], "main is untouched"); |
|
| 2128 | - | assert_eq!(repo.resolve_change("feature").unwrap(), h_b, "a one-head bookmark names its change"); |
|
| 3163 | + | assert_eq!( |
|
| 3164 | + | repo.bookmark_heads().unwrap(), |
|
| 3165 | + | vec![h_b], |
|
| 3166 | + | "b edits a.txt, so it depends on a" |
|
| 3167 | + | ); |
|
| 3168 | + | assert_eq!( |
|
| 3169 | + | repo.resolve_heads("main").unwrap(), |
|
| 3170 | + | vec![h_a], |
|
| 3171 | + | "main is untouched" |
|
| 3172 | + | ); |
|
| 3173 | + | assert_eq!( |
|
| 3174 | + | repo.resolve_change("feature").unwrap(), |
|
| 3175 | + | h_b, |
|
| 3176 | + | "a one-head bookmark names its change" |
|
| 3177 | + | ); |
|
| 2129 | 3178 | // Reopening keeps the workspace's bookmark. |
|
| 2130 | 3179 | let reopened = Repo::open(&r).unwrap().with_home(root.join("home")); |
|
| 2131 | 3180 | assert_eq!(reopened.config.bookmark, "feature"); |
|
| … | 4 unchanged lines |
||
| 2136 | 3185 | write(&r, "c.txt", b"c\n"); |
|
| 2137 | 3186 | let h_c = repo.commit("c on main", &[]).unwrap(); |
|
| 2138 | 3187 | let list = repo.bookmarks().unwrap(); |
|
| 2139 | - | assert_eq!(list.iter().map(|b| (b.name.as_str(), b.current)).collect::<Vec<_>>(), vec![("feature", false), ("main", true)]); |
|
| 2140 | - | assert_eq!(list.iter().find(|b| b.name == "main").unwrap().ahead, Some(1), "c is not on origin yet"); |
|
| 3188 | + | assert_eq!( |
|
| 3189 | + | list.iter() |
|
| 3190 | + | .map(|b| (b.name.as_str(), b.current)) |
|
| 3191 | + | .collect::<Vec<_>>(), |
|
| 3192 | + | vec![("feature", false), ("main", true)] |
|
| 3193 | + | ); |
|
| 3194 | + | assert_eq!( |
|
| 3195 | + | list.iter().find(|b| b.name == "main").unwrap().ahead, |
|
| 3196 | + | Some(1), |
|
| 3197 | + | "c is not on origin yet" |
|
| 3198 | + | ); |
|
| 2141 | 3199 | // Merge feature into main: the union, both heads, no merge change. |
|
| 2142 | 3200 | let m = repo.merge("feature").unwrap(); |
|
| 2143 | 3201 | assert!(m.changed && m.checked_out); |
|
| … | 7 unchanged lines |
||
| 2151 | 3209 | assert!(repo.push_bookmark("origin", "feature", false).unwrap()[0].ok); |
|
| 2152 | 3210 | assert_eq!(origin.resolve_heads("feature").unwrap(), vec![h_b]); |
|
| 2153 | 3211 | assert!(repo.push("origin", false).unwrap()[0].ok); |
|
| 2154 | - | assert_eq!(repo.bookmarks().unwrap().iter().find(|b| b.name == "main").unwrap().ahead, Some(0)); |
|
| 3212 | + | assert_eq!( |
|
| 3213 | + | repo.bookmarks() |
|
| 3214 | + | .unwrap() |
|
| 3215 | + | .iter() |
|
| 3216 | + | .find(|b| b.name == "main") |
|
| 3217 | + | .unwrap() |
|
| 3218 | + | .ahead, |
|
| 3219 | + | Some(0) |
|
| 3220 | + | ); |
|
| 2155 | 3221 | // A second workspace inherits the current bookmark and can switch independently. |
|
| 2156 | 3222 | let ws = repo.workspace_add(&root.join("ws"), None).unwrap(); |
|
| 2157 | 3223 | assert_eq!(ws.config.bookmark, "main"); |
|
| 2158 | 3224 | let mut ws = ws; |
|
| 2159 | 3225 | ws.bookmark_switch("feature", false).unwrap(); |
|
| 2160 | - | assert_eq!(Repo::open(&r).unwrap().config.bookmark, "main", "the first workspace is unaffected"); |
|
| 2161 | - | assert!(repo.bookmark_delete("feature").is_err(), "current bookmark of another workspace"); |
|
| 3226 | + | assert_eq!( |
|
| 3227 | + | Repo::open(&r).unwrap().config.bookmark, |
|
| 3228 | + | "main", |
|
| 3229 | + | "the first workspace is unaffected" |
|
| 3230 | + | ); |
|
| 3231 | + | assert!( |
|
| 3232 | + | repo.bookmark_delete("feature").is_err(), |
|
| 3233 | + | "current bookmark of another workspace" |
|
| 3234 | + | ); |
|
| 2162 | 3235 | ws.bookmark_switch("main", false).unwrap(); |
|
| 2163 | 3236 | // Set and delete. |
|
| 2164 | 3237 | assert_eq!(repo.bookmark_set("feature", &[h_a]).unwrap(), vec![h_a]); |
|
| … | 1 unchanged lines |
||
| 2166 | 3239 | repo.bookmark_delete("feature").unwrap(); |
|
| 2167 | 3240 | assert!(repo.resolve_heads("feature").is_err()); |
|
| 2168 | 3241 | assert!(repo.bookmark_set("feature", &[h_a]).is_err()); |
|
| 2169 | - | assert!(repo.checkout(&[h_a], false, "checkout").is_ok(), "changes of a deleted bookmark remain"); |
|
| 3242 | + | assert!( |
|
| 3243 | + | repo.checkout(&[h_a], false, "checkout").is_ok(), |
|
| 3244 | + | "changes of a deleted bookmark remain" |
|
| 3245 | + | ); |
|
| 2170 | 3246 | } |
|
| 2171 | 3247 | ||
| 2172 | 3248 | #[test] |
|
| … | 4 unchanged lines |
||
| 2177 | 3253 | // A library repository with two commits, and a bare-ish origin for it. |
|
| 2178 | 3254 | let lib_origin = Repo::init(&root.join("lib-origin")).unwrap(); |
|
| 2179 | 3255 | let lib_url = root.join("lib-origin").to_string_lossy().to_string(); |
|
| 2180 | - | let lib = Repo::clone(&lib_url, &root.join("lib")).unwrap().with_home(root.join("home")); |
|
| 3256 | + | let lib = Repo::clone(&lib_url, &root.join("lib")) |
|
| 3257 | + | .unwrap() |
|
| 3258 | + | .with_home(root.join("home")); |
|
| 2181 | 3259 | write(&root.join("lib"), "lib.txt", b"v1\n"); |
|
| 2182 | 3260 | let l1 = lib.commit("lib v1", &[]).unwrap(); |
|
| 2183 | 3261 | assert!(lib.push("origin", false).unwrap()[0].ok); |
|
| … | 4 unchanged lines |
||
| 2188 | 3266 | let app_url = root.join("app-origin").to_string_lossy().to_string(); |
|
| 2189 | 3267 | let _ = app_origin; |
|
| 2190 | 3268 | let app_dir = root.join("app"); |
|
| 2191 | - | let app = Repo::clone(&app_url, &app_dir).unwrap().with_home(root.join("home")); |
|
| 3269 | + | let app = Repo::clone(&app_url, &app_dir) |
|
| 3270 | + | .unwrap() |
|
| 3271 | + | .with_home(root.join("home")); |
|
| 2192 | 3272 | write(&app_dir, "main.txt", b"app\n"); |
|
| 2193 | 3273 | let pin = app.submodule_add(&lib_url, "libs/lib", None).unwrap(); |
|
| 2194 | 3274 | assert!(matches!(&pin, crate::subrepo::Pin::Oxy { heads, .. } if heads == &vec![l1])); |
|
| 2195 | 3275 | assert!(app_dir.join(".oxymodules").exists() && app_dir.join("libs/lib/.oxy").is_dir()); |
|
| 2196 | 3276 | let snap = app.snapshot("status").unwrap(); |
|
| 2197 | 3277 | let paths: Vec<&str> = snap.statuses.iter().map(|s| s.path.as_str()).collect(); |
|
| 2198 | - | assert_eq!(paths, vec![".oxymodules", "libs/lib", "main.txt"], "the nested files are not walked"); |
|
| 3278 | + | assert_eq!( |
|
| 3279 | + | paths, |
|
| 3280 | + | vec![".oxymodules", "libs/lib", "main.txt"], |
|
| 3281 | + | "the nested files are not walked" |
|
| 3282 | + | ); |
|
| 2199 | 3283 | let a1 = app.commit("app with lib", &[]).unwrap(); |
|
| 2200 | 3284 | let files = app.load_state(&[a1]).unwrap().materialize().files; |
|
| 2201 | 3285 | assert_eq!(files["libs/lib"].kind, NodeKind::Subrepo); |
|
| … | 4 unchanged lines |
||
| 2206 | 3290 | assert_eq!(subs[0].actual.as_ref(), Some(&subs[0].pin)); |
|
| 2207 | 3291 | ||
| 2208 | 3292 | // Advance the nested repository: the parent sees a modified pin. |
|
| 2209 | - | let nested = Repo::open(&app_dir.join("libs/lib")).unwrap().with_home(root.join("home")); |
|
| 3293 | + | let nested = Repo::open(&app_dir.join("libs/lib")) |
|
| 3294 | + | .unwrap() |
|
| 3295 | + | .with_home(root.join("home")); |
|
| 2210 | 3296 | write(&app_dir.join("libs/lib"), "lib.txt", b"v2\n"); |
|
| 2211 | 3297 | let l2 = nested.commit("lib v2", &[]).unwrap(); |
|
| 2212 | 3298 | assert!(nested.push("origin", false).unwrap()[0].ok); |
|
| 2213 | 3299 | let snap = app.snapshot("status").unwrap(); |
|
| 2214 | - | assert_eq!(snap.statuses.iter().map(|s| (s.path.as_str(), s.status)).collect::<Vec<_>>(), vec![("libs/lib", ChangeStatus::Modified)]); |
|
| 3300 | + | assert_eq!( |
|
| 3301 | + | snap.statuses |
|
| 3302 | + | .iter() |
|
| 3303 | + | .map(|s| (s.path.as_str(), s.status)) |
|
| 3304 | + | .collect::<Vec<_>>(), |
|
| 3305 | + | vec![("libs/lib", ChangeStatus::Modified)] |
|
| 3306 | + | ); |
|
| 2215 | 3307 | let a2 = app.commit("bump lib", &[]).unwrap(); |
|
| 2216 | 3308 | // Checking out the older app state moves the nested repository back. |
|
| 2217 | 3309 | let r = app.checkout(&[a1], false, "checkout").unwrap(); |
|
| 2218 | 3310 | assert!(r.notes.is_empty(), "{:?}", r.notes); |
|
| 2219 | 3311 | assert_eq!(fs::read(app_dir.join("libs/lib/lib.txt")).unwrap(), b"v1\n"); |
|
| 2220 | - | assert_eq!(Repo::open(&app_dir.join("libs/lib")).unwrap().bookmark_heads().unwrap(), vec![l1]); |
|
| 3312 | + | assert_eq!( |
|
| 3313 | + | Repo::open(&app_dir.join("libs/lib")) |
|
| 3314 | + | .unwrap() |
|
| 3315 | + | .bookmark_heads() |
|
| 3316 | + | .unwrap(), |
|
| 3317 | + | vec![l1] |
|
| 3318 | + | ); |
|
| 2221 | 3319 | assert!(!app.snapshot("status").unwrap().changed); |
|
| 2222 | 3320 | // A dirty nested repository is left alone and reported. |
|
| 2223 | 3321 | write(&app_dir.join("libs/lib"), "scratch.txt", b"wip\n"); |
|
| 2224 | 3322 | let r = app.checkout(&[a2], false, "checkout").unwrap(); |
|
| 2225 | - | assert!(r.notes.iter().any(|n| n.contains("uncommitted")), "{:?}", r.notes); |
|
| 3323 | + | assert!( |
|
| 3324 | + | r.notes.iter().any(|n| n.contains("uncommitted")), |
|
| 3325 | + | "{:?}", |
|
| 3326 | + | r.notes |
|
| 3327 | + | ); |
|
| 2226 | 3328 | assert_eq!(fs::read(app_dir.join("libs/lib/lib.txt")).unwrap(), b"v1\n"); |
|
| 2227 | 3329 | fs::remove_file(app_dir.join("libs/lib/scratch.txt")).unwrap(); |
|
| 2228 | 3330 | let r = app.submodule_update(false, &[]).unwrap(); |
|
| … | 4 unchanged lines |
||
| 2233 | 3335 | // A fresh clone of the app has the pin but no nested repository until update --init. |
|
| 2234 | 3336 | assert!(app.push("origin", false).unwrap()[0].ok); |
|
| 2235 | 3337 | let clone_dir = root.join("app-clone"); |
|
| 2236 | - | let clone = Repo::clone(&app_url, &clone_dir).unwrap().with_home(root.join("home")); |
|
| 3338 | + | let clone = Repo::clone(&app_url, &clone_dir) |
|
| 3339 | + | .unwrap() |
|
| 3340 | + | .with_home(root.join("home")); |
|
| 2237 | 3341 | assert!(!clone_dir.join("libs/lib/.oxy").exists()); |
|
| 2238 | - | assert_eq!(clone.submodule_update(false, &[]).unwrap(), vec![("libs/lib".to_string(), SubrepoSync::Missing)]); |
|
| 2239 | - | assert_eq!(clone.submodule_update(true, &[]).unwrap(), vec![("libs/lib".to_string(), SubrepoSync::Updated)]); |
|
| 2240 | - | assert_eq!(fs::read(clone_dir.join("libs/lib/lib.txt")).unwrap(), b"v2\n"); |
|
| 3342 | + | assert_eq!( |
|
| 3343 | + | clone.submodule_update(false, &[]).unwrap(), |
|
| 3344 | + | vec![("libs/lib".to_string(), SubrepoSync::Missing)] |
|
| 3345 | + | ); |
|
| 3346 | + | assert_eq!( |
|
| 3347 | + | clone.submodule_update(true, &[]).unwrap(), |
|
| 3348 | + | vec![("libs/lib".to_string(), SubrepoSync::Updated)] |
|
| 3349 | + | ); |
|
| 3350 | + | assert_eq!( |
|
| 3351 | + | fs::read(clone_dir.join("libs/lib/lib.txt")).unwrap(), |
|
| 3352 | + | b"v2\n" |
|
| 3353 | + | ); |
|
| 2241 | 3354 | assert!(!clone.snapshot("status").unwrap().changed); |
|
| 2242 | 3355 | // Removing the subrepo leaves the directory and drops the node. |
|
| 2243 | 3356 | clone.submodule_remove("libs/lib").unwrap(); |
|
| 2244 | 3357 | let snap = clone.snapshot("status").unwrap(); |
|
| 2245 | - | assert!(snap.statuses.iter().any(|s| s.path == "libs/lib" && s.status == ChangeStatus::Removed), "{:?}", snap.statuses); |
|
| 3358 | + | assert!( |
|
| 3359 | + | snap.statuses |
|
| 3360 | + | .iter() |
|
| 3361 | + | .any(|s| s.path == "libs/lib" && s.status == ChangeStatus::Removed), |
|
| 3362 | + | "{:?}", |
|
| 3363 | + | snap.statuses |
|
| 3364 | + | ); |
|
| 2246 | 3365 | let c = clone.commit("drop lib", &[]).unwrap(); |
|
| 2247 | - | assert!(!clone.load_state(&[c]).unwrap().materialize().files.contains_key("libs/lib")); |
|
| 3366 | + | assert!(!clone |
|
| 3367 | + | .load_state(&[c]) |
|
| 3368 | + | .unwrap() |
|
| 3369 | + | .materialize() |
|
| 3370 | + | .files |
|
| 3371 | + | .contains_key("libs/lib")); |
|
| 2248 | 3372 | assert!(clone_dir.join("libs/lib/lib.txt").exists()); |
|
| 2249 | - | assert!(!clone.snapshot("status").unwrap().changed, "an unregistered nested repository is ignored"); |
|
| 3373 | + | assert!( |
|
| 3374 | + | !clone.snapshot("status").unwrap().changed, |
|
| 3375 | + | "an unregistered nested repository is ignored" |
|
| 3376 | + | ); |
|
| 2250 | 3377 | } |
|
| 2251 | 3378 | ||
| 2252 | 3379 | #[test] |
|
| 2253 | 3380 | fn git_submodules_import_and_export_as_gitlinks() { |
|
| 2254 | 3381 | hermetic(); |
|
| 2255 | - | if std::process::Command::new("git").arg("--version").output().is_err() { |
|
| 3382 | + | if std::process::Command::new("git") |
|
| 3383 | + | .arg("--version") |
|
| 3384 | + | .output() |
|
| 3385 | + | .is_err() |
|
| 3386 | + | { |
|
| 2256 | 3387 | eprintln!("git not installed; skipping"); |
|
| 2257 | 3388 | return; |
|
| 2258 | 3389 | } |
|
| … | 3 unchanged lines |
||
| 2262 | 3393 | let out = std::process::Command::new("git") |
|
| 2263 | 3394 | .arg("-C") |
|
| 2264 | 3395 | .arg(d) |
|
| 2265 | - | .args(["-c", "user.name=Grace", "-c", "[email protected]", "-c", "commit.gpgsign=false", "-c", "protocol.file.allow=always"]) |
|
| 3396 | + | .args([ |
|
| 3397 | + | "-c", |
|
| 3398 | + | "user.name=Grace", |
|
| 3399 | + | "-c", |
|
| 3400 | + | "[email protected]", |
|
| 3401 | + | "-c", |
|
| 3402 | + | "commit.gpgsign=false", |
|
| 3403 | + | "-c", |
|
| 3404 | + | "protocol.file.allow=always", |
|
| 3405 | + | ]) |
|
| 2266 | 3406 | .args(args) |
|
| 2267 | 3407 | .output() |
|
| 2268 | 3408 | .unwrap(); |
|
| 2269 | - | assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); |
|
| 3409 | + | assert!( |
|
| 3410 | + | out.status.success(), |
|
| 3411 | + | "git {args:?}: {}", |
|
| 3412 | + | String::from_utf8_lossy(&out.stderr) |
|
| 3413 | + | ); |
|
| 2270 | 3414 | String::from_utf8_lossy(&out.stdout).trim().to_string() |
|
| 2271 | 3415 | }; |
|
| 2272 | 3416 | let sub = root.join("sub"); |
|
| … | 9 unchanged lines |
||
| 2282 | 3426 | write(&g, "README.md", b"# app\n"); |
|
| 2283 | 3427 | git_in(&g, &["add", "."]); |
|
| 2284 | 3428 | git_in(&g, &["commit", "-q", "-m", "initial"]); |
|
| 2285 | - | git_in(&g, &["submodule", "add", "-q", sub.to_str().unwrap(), "vendor/sub"]); |
|
| 3429 | + | git_in( |
|
| 3430 | + | &g, |
|
| 3431 | + | &[ |
|
| 3432 | + | "submodule", |
|
| 3433 | + | "add", |
|
| 3434 | + | "-q", |
|
| 3435 | + | sub.to_str().unwrap(), |
|
| 3436 | + | "vendor/sub", |
|
| 3437 | + | ], |
|
| 3438 | + | ); |
|
| 2286 | 3439 | git_in(&g, &["commit", "-q", "-m", "add submodule"]); |
|
| 2287 | 3440 | ||
| 2288 | 3441 | let dest = root.join("oxy"); |
|
| 2289 | 3442 | let repo = Repo::init(&dest).unwrap().with_home(root.join("home")); |
|
| 2290 | 3443 | let rep = repo.import_git(&g, "main").unwrap(); |
|
| 2291 | 3444 | assert_eq!((rep.commits, rep.submodules), (2, 1)); |
|
| 2292 | - | let files = repo.load_state(&repo.bookmark_heads().unwrap()).unwrap().materialize().files; |
|
| 3445 | + | let files = repo |
|
| 3446 | + | .load_state(&repo.bookmark_heads().unwrap()) |
|
| 3447 | + | .unwrap() |
|
| 3448 | + | .materialize() |
|
| 3449 | + | .files; |
|
| 2293 | 3450 | assert_eq!(files["vendor/sub"].kind, NodeKind::Subrepo); |
|
| 2294 | - | let FileContent::Opaque { blobs } = &files["vendor/sub"].content else { panic!() }; |
|
| 2295 | - | assert_eq!(crate::subrepo::Pin::decode(&repo.read_blob(&blobs[0]).unwrap()).unwrap(), crate::subrepo::Pin::Git { sha: sub_sha.clone(), branch: "HEAD".into() }); |
|
| 2296 | - | let modules = crate::subrepo::Modules::parse(&fs::read_to_string(dest.join(".oxymodules")).unwrap()); |
|
| 3451 | + | let FileContent::Opaque { blobs } = &files["vendor/sub"].content else { |
|
| 3452 | + | panic!() |
|
| 3453 | + | }; |
|
| 3454 | + | assert_eq!( |
|
| 3455 | + | crate::subrepo::Pin::decode(&repo.read_blob(&blobs[0]).unwrap()).unwrap(), |
|
| 3456 | + | crate::subrepo::Pin::Git { |
|
| 3457 | + | sha: sub_sha.clone(), |
|
| 3458 | + | branch: "HEAD".into() |
|
| 3459 | + | } |
|
| 3460 | + | ); |
|
| 3461 | + | let modules = |
|
| 3462 | + | crate::subrepo::Modules::parse(&fs::read_to_string(dest.join(".oxymodules")).unwrap()); |
|
| 2297 | 3463 | assert_eq!(modules.entries["vendor/sub"].url, sub.to_string_lossy()); |
|
| 2298 | 3464 | assert!(modules.entries["vendor/sub"].git); |
|
| 2299 | - | assert!(files.contains_key(".gitmodules"), "the Git file is kept for round trips"); |
|
| 2300 | - | assert!(!dest.join("vendor/sub/.git").exists(), "not cloned until update --init"); |
|
| 3465 | + | assert!( |
|
| 3466 | + | files.contains_key(".gitmodules"), |
|
| 3467 | + | "the Git file is kept for round trips" |
|
| 3468 | + | ); |
|
| 3469 | + | assert!( |
|
| 3470 | + | !dest.join("vendor/sub/.git").exists(), |
|
| 3471 | + | "not cloned until update --init" |
|
| 3472 | + | ); |
|
| 2301 | 3473 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 2302 | 3474 | // update --init clones it at the pinned commit; status is unchanged. |
|
| 2303 | - | assert_eq!(repo.submodule_update(true, &[]).unwrap(), vec![("vendor/sub".to_string(), SubrepoSync::Updated)]); |
|
| 2304 | - | assert_eq!(git_in(&dest.join("vendor/sub"), &["rev-parse", "HEAD"]), sub_sha); |
|
| 3475 | + | assert_eq!( |
|
| 3476 | + | repo.submodule_update(true, &[]).unwrap(), |
|
| 3477 | + | vec![("vendor/sub".to_string(), SubrepoSync::Updated)] |
|
| 3478 | + | ); |
|
| 3479 | + | assert_eq!( |
|
| 3480 | + | git_in(&dest.join("vendor/sub"), &["rev-parse", "HEAD"]), |
|
| 3481 | + | sub_sha |
|
| 3482 | + | ); |
|
| 2305 | 3483 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 2306 | 3484 | // Export writes the gitlink and .gitmodules back. |
|
| 2307 | 3485 | let mirror = root.join("mirror.git"); |
|
| 2308 | 3486 | let rep = repo.git_export(&mirror).unwrap(); |
|
| 2309 | 3487 | assert_eq!(rep.skipped_subrepos, 0); |
|
| 2310 | 3488 | let ls = git_in(&mirror, &["ls-tree", "main", "vendor/sub"]); |
|
| 2311 | - | assert!(ls.starts_with("160000 commit ") && ls.contains(&sub_sha), "{ls}"); |
|
| 2312 | - | assert_eq!(git_in(&mirror, &["ls-tree", "--name-only", "main"]), ".gitmodules\nREADME.md\nvendor"); |
|
| 3489 | + | assert!( |
|
| 3490 | + | ls.starts_with("160000 commit ") && ls.contains(&sub_sha), |
|
| 3491 | + | "{ls}" |
|
| 3492 | + | ); |
|
| 3493 | + | assert_eq!( |
|
| 3494 | + | git_in(&mirror, &["ls-tree", "--name-only", "main"]), |
|
| 3495 | + | ".gitmodules\nREADME.md\nvendor" |
|
| 3496 | + | ); |
|
| 2313 | 3497 | let orig_tree = git_in(&g, &["rev-parse", "main^{tree}"]); |
|
| 2314 | - | assert_eq!(git_in(&mirror, &["rev-parse", "main^{tree}"]), orig_tree, "tree round trip"); |
|
| 3498 | + | assert_eq!( |
|
| 3499 | + | git_in(&mirror, &["rev-parse", "main^{tree}"]), |
|
| 3500 | + | orig_tree, |
|
| 3501 | + | "tree round trip" |
|
| 3502 | + | ); |
|
| 2315 | 3503 | } |
|
| 2316 | 3504 | ||
| 2317 | 3505 | #[test] |
|
| … | 9 unchanged lines |
||
| 2327 | 3515 | write(root, "b.txt", b"b\nbb\n"); |
|
| 2328 | 3516 | write(root, "c.txt", b"c\n"); |
|
| 2329 | 3517 | // By path: only a.txt goes in; the rest stays on disk as uncommitted work. |
|
| 2330 | - | let h1 = repo.commit_selection("a only", &[], &Selection::Paths(vec!["a.txt".into()])).unwrap(); |
|
| 3518 | + | let h1 = repo |
|
| 3519 | + | .commit_selection("a only", &[], &Selection::Paths(vec!["a.txt".into()])) |
|
| 3520 | + | .unwrap(); |
|
| 2331 | 3521 | let c1 = repo.store().get_change(&h1).unwrap(); |
|
| 2332 | - | assert!(c1.tree_ops.is_empty() && c1.edits.iter().all(|e| e.node() == c1.edits[0].node()), "{:?}", c1.tree_ops); |
|
| 3522 | + | assert!( |
|
| 3523 | + | c1.tree_ops.is_empty() && c1.edits.iter().all(|e| e.node() == c1.edits[0].node()), |
|
| 3524 | + | "{:?}", |
|
| 3525 | + | c1.tree_ops |
|
| 3526 | + | ); |
|
| 2333 | 3527 | assert_eq!(c1.deps, vec![h0]); |
|
| 2334 | 3528 | let files = repo.load_state(&[h1]).unwrap().materialize().files; |
|
| 2335 | - | assert!(matches!(&files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"0\n1\n2\n3\n4\n")); |
|
| 3529 | + | assert!( |
|
| 3530 | + | matches!(&files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"0\n1\n2\n3\n4\n") |
|
| 3531 | + | ); |
|
| 2336 | 3532 | assert!(matches!(&files["b.txt"].content, FileContent::Text { bytes, .. } if bytes == b"b\n")); |
|
| 2337 | 3533 | assert!(!files.contains_key("c.txt")); |
|
| 2338 | 3534 | let snap = repo.snapshot("status").unwrap(); |
|
| 2339 | - | assert_eq!(snap.statuses.iter().map(|s| (s.path.as_str(), s.status)).collect::<Vec<_>>(), vec![("b.txt", ChangeStatus::Modified), ("c.txt", ChangeStatus::Added)]); |
|
| 2340 | - | assert_eq!(fs::read(root.join("b.txt")).unwrap(), b"b\nbb\n", "disk untouched"); |
|
| 3535 | + | assert_eq!( |
|
| 3536 | + | snap.statuses |
|
| 3537 | + | .iter() |
|
| 3538 | + | .map(|s| (s.path.as_str(), s.status)) |
|
| 3539 | + | .collect::<Vec<_>>(), |
|
| 3540 | + | vec![ |
|
| 3541 | + | ("b.txt", ChangeStatus::Modified), |
|
| 3542 | + | ("c.txt", ChangeStatus::Added) |
|
| 3543 | + | ] |
|
| 3544 | + | ); |
|
| 3545 | + | assert_eq!( |
|
| 3546 | + | fs::read(root.join("b.txt")).unwrap(), |
|
| 3547 | + | b"b\nbb\n", |
|
| 3548 | + | "disk untouched" |
|
| 3549 | + | ); |
|
| 2341 | 3550 | // By hunk: pick the b.txt insert; c.txt's addition is left. |
|
| 2342 | 3551 | let (_, hunks) = repo.hunks().unwrap(); |
|
| 2343 | - | let b_hunk = hunks.iter().find(|h| h.path == "b.txt" && h.text.contains("+bb")).unwrap().index; |
|
| 2344 | - | let h2 = repo.commit_selection("b only", &[], &Selection::Hunks(vec![b_hunk])).unwrap(); |
|
| 3552 | + | let b_hunk = hunks |
|
| 3553 | + | .iter() |
|
| 3554 | + | .find(|h| h.path == "b.txt" && h.text.contains("+bb")) |
|
| 3555 | + | .unwrap() |
|
| 3556 | + | .index; |
|
| 3557 | + | let h2 = repo |
|
| 3558 | + | .commit_selection("b only", &[], &Selection::Hunks(vec![b_hunk])) |
|
| 3559 | + | .unwrap(); |
|
| 2345 | 3560 | let files = repo.load_state(&[h2]).unwrap().materialize().files; |
|
| 2346 | - | assert!(matches!(&files["b.txt"].content, FileContent::Text { bytes, .. } if bytes == b"b\nbb\n")); |
|
| 3561 | + | assert!( |
|
| 3562 | + | matches!(&files["b.txt"].content, FileContent::Text { bytes, .. } if bytes == b"b\nbb\n") |
|
| 3563 | + | ); |
|
| 2347 | 3564 | assert!(!files.contains_key("c.txt")); |
|
| 2348 | 3565 | let snap = repo.snapshot("status").unwrap(); |
|
| 2349 | - | assert_eq!(snap.statuses.iter().map(|s| s.path.as_str()).collect::<Vec<_>>(), vec!["c.txt"]); |
|
| 3566 | + | assert_eq!( |
|
| 3567 | + | snap.statuses |
|
| 3568 | + | .iter() |
|
| 3569 | + | .map(|s| s.path.as_str()) |
|
| 3570 | + | .collect::<Vec<_>>(), |
|
| 3571 | + | vec!["c.txt"] |
|
| 3572 | + | ); |
|
| 2350 | 3573 | // Choosing only the content hunk of a file in a new directory pulls in |
|
| 2351 | 3574 | // the directory and file additions it needs. |
|
| 2352 | 3575 | write(root, "d/e.txt", b"e\n"); |
|
| 2353 | 3576 | let (_, hunks) = repo.hunks().unwrap(); |
|
| 2354 | - | let e_insert = hunks.iter().find(|h| h.path == "d/e.txt" && h.text.contains("+e")).unwrap().index; |
|
| 2355 | - | let h3 = repo.commit_selection("e only", &[], &Selection::Hunks(vec![e_insert])).unwrap(); |
|
| 3577 | + | let e_insert = hunks |
|
| 3578 | + | .iter() |
|
| 3579 | + | .find(|h| h.path == "d/e.txt" && h.text.contains("+e")) |
|
| 3580 | + | .unwrap() |
|
| 3581 | + | .index; |
|
| 3582 | + | let h3 = repo |
|
| 3583 | + | .commit_selection("e only", &[], &Selection::Hunks(vec![e_insert])) |
|
| 3584 | + | .unwrap(); |
|
| 2356 | 3585 | let c3 = repo.store().get_change(&h3).unwrap(); |
|
| 2357 | 3586 | assert_eq!(c3.tree_ops.len(), 2, "{:?}", c3.tree_ops); |
|
| 2358 | 3587 | let files = repo.load_state(&[h3]).unwrap().materialize().files; |
|
| … | 1 unchanged lines |
||
| 2360 | 3589 | // Choosing a new file's addition hunk brings its content along. |
|
| 2361 | 3590 | write(root, "f/g.txt", b"g\n"); |
|
| 2362 | 3591 | let (_, hunks) = repo.hunks().unwrap(); |
|
| 2363 | - | let g_add = hunks.iter().find(|h| h.path == "f/g.txt" && h.text.starts_with("A ")).unwrap().index; |
|
| 2364 | - | let h4 = repo.commit_selection("g by addition", &[], &Selection::Hunks(vec![g_add])).unwrap(); |
|
| 3592 | + | let g_add = hunks |
|
| 3593 | + | .iter() |
|
| 3594 | + | .find(|h| h.path == "f/g.txt" && h.text.starts_with("A ")) |
|
| 3595 | + | .unwrap() |
|
| 3596 | + | .index; |
|
| 3597 | + | let h4 = repo |
|
| 3598 | + | .commit_selection("g by addition", &[], &Selection::Hunks(vec![g_add])) |
|
| 3599 | + | .unwrap(); |
|
| 2365 | 3600 | let files = repo.load_state(&[h4]).unwrap().materialize().files; |
|
| 2366 | - | assert!(matches!(&files["f/g.txt"].content, FileContent::Text { bytes, .. } if bytes == b"g\n")); |
|
| 2367 | - | assert!(!repo.snapshot("status").unwrap().statuses.iter().any(|s| s.path == "f/g.txt")); |
|
| 3601 | + | assert!( |
|
| 3602 | + | matches!(&files["f/g.txt"].content, FileContent::Text { bytes, .. } if bytes == b"g\n") |
|
| 3603 | + | ); |
|
| 3604 | + | assert!(!repo |
|
| 3605 | + | .snapshot("status") |
|
| 3606 | + | .unwrap() |
|
| 3607 | + | .statuses |
|
| 3608 | + | .iter() |
|
| 3609 | + | .any(|s| s.path == "f/g.txt")); |
|
| 2368 | 3610 | // Everything left commits normally; the working copy is then clean. |
|
| 2369 | 3611 | repo.commit("the rest", &[]).unwrap(); |
|
| 2370 | 3612 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 2371 | - | let files = repo.load_state(&repo.bookmark_heads().unwrap()).unwrap().materialize().files; |
|
| 3613 | + | let files = repo |
|
| 3614 | + | .load_state(&repo.bookmark_heads().unwrap()) |
|
| 3615 | + | .unwrap() |
|
| 3616 | + | .materialize() |
|
| 3617 | + | .files; |
|
| 2372 | 3618 | assert!(files.contains_key("c.txt")); |
|
| 2373 | 3619 | } |
|
| 2374 | 3620 | ||
| … | 3 unchanged lines |
||
| 2378 | 3624 | let dir = tempfile::tempdir().unwrap(); |
|
| 2379 | 3625 | let root = dir.path(); |
|
| 2380 | 3626 | let mut repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 2381 | - | repo.create_identity("Ada", "[email protected]", IdentityScope::User).unwrap(); |
|
| 3627 | + | repo.create_identity("Ada", "[email protected]", IdentityScope::User) |
|
| 3628 | + | .unwrap(); |
|
| 2382 | 3629 | write(root, "a.txt", b"one\n"); |
|
| 2383 | 3630 | let h1 = repo.commit("first", &[]).unwrap(); |
|
| 2384 | 3631 | write(root, "a.txt", b"one\ntwo\n"); |
|
| 2385 | 3632 | let h2 = repo.commit("second", &[]).unwrap(); |
|
| 2386 | 3633 | let b = repo.blame("a.txt").unwrap(); |
|
| 2387 | - | assert_eq!(b.iter().map(|l| (l.line, l.hash, l.text.as_slice())).collect::<Vec<_>>(), vec![(1, h1, b"one\n".as_slice()), (2, h2, b"two\n".as_slice())]); |
|
| 3634 | + | assert_eq!( |
|
| 3635 | + | b.iter() |
|
| 3636 | + | .map(|l| (l.line, l.hash, l.text.as_slice())) |
|
| 3637 | + | .collect::<Vec<_>>(), |
|
| 3638 | + | vec![(1, h1, b"one\n".as_slice()), (2, h2, b"two\n".as_slice())] |
|
| 3639 | + | ); |
|
| 2388 | 3640 | assert!(b.iter().all(|l| l.author == "Ada")); |
|
| 2389 | - | assert_eq!(b[0].change_id, repo.store().get_change(&h1).unwrap().change_id); |
|
| 3641 | + | assert_eq!( |
|
| 3642 | + | b[0].change_id, |
|
| 3643 | + | repo.store().get_change(&h1).unwrap().change_id |
|
| 3644 | + | ); |
|
| 2390 | 3645 | fs::rename(root.join("a.txt"), root.join("b.txt")).unwrap(); |
|
| 2391 | 3646 | repo.commit("rename", &[]).unwrap(); |
|
| 2392 | 3647 | let b = repo.blame("b.txt").unwrap(); |
|
| 2393 | - | assert_eq!(b.iter().map(|l| l.hash).collect::<Vec<_>>(), vec![h1, h2], "the node survives the rename"); |
|
| 3648 | + | assert_eq!( |
|
| 3649 | + | b.iter().map(|l| l.hash).collect::<Vec<_>>(), |
|
| 3650 | + | vec![h1, h2], |
|
| 3651 | + | "the node survives the rename" |
|
| 3652 | + | ); |
|
| 2394 | 3653 | assert!(repo.blame("a.txt").is_err()); |
|
| 2395 | 3654 | } |
|
| 2396 | 3655 | ||
| … | 13 unchanged lines |
||
| 2410 | 3669 | // Each step appends after the previous line, so the chain is linear. |
|
| 2411 | 3670 | assert_eq!(repo.bookmark_heads().unwrap(), vec![hashes[7]]); |
|
| 2412 | 3671 | repo.bisect_start().unwrap(); |
|
| 2413 | - | assert!(matches!(repo.bisect_mark(Some(true), &[hashes[0]]).unwrap(), BisectStep::NeedMarks { good: 1, bad: 0 })); |
|
| 3672 | + | assert!(matches!( |
|
| 3673 | + | repo.bisect_mark(Some(true), &[hashes[0]]).unwrap(), |
|
| 3674 | + | BisectStep::NeedMarks { good: 1, bad: 0 } |
|
| 3675 | + | )); |
|
| 2414 | 3676 | let step = repo.bisect_mark(Some(false), &[hashes[7]]).unwrap(); |
|
| 2415 | - | assert!(matches!(step, BisectStep::Testing { remaining: 7, .. }), "{step:?}"); |
|
| 3677 | + | assert!( |
|
| 3678 | + | matches!(step, BisectStep::Testing { remaining: 7, .. }), |
|
| 3679 | + | "{step:?}" |
|
| 3680 | + | ); |
|
| 2416 | 3681 | // Automated: bad when the bug line is present. |
|
| 2417 | - | let step = repo.bisect_run(&["sh".into(), "-c".into(), "! grep -q bug f.txt".into()]).unwrap(); |
|
| 3682 | + | let step = repo |
|
| 3683 | + | .bisect_run(&["sh".into(), "-c".into(), "! grep -q bug f.txt".into()]) |
|
| 3684 | + | .unwrap(); |
|
| 2418 | 3685 | assert_eq!(step, BisectStep::Found { culprit: hashes[4] }); |
|
| 2419 | - | assert_eq!(repo.bisect_status().unwrap(), Some(BisectStep::Found { culprit: hashes[4] })); |
|
| 3686 | + | assert_eq!( |
|
| 3687 | + | repo.bisect_status().unwrap(), |
|
| 3688 | + | Some(BisectStep::Found { culprit: hashes[4] }) |
|
| 3689 | + | ); |
|
| 2420 | 3690 | repo.bisect_reset().unwrap(); |
|
| 2421 | 3691 | assert_eq!(fs::read_to_string(root.join("f.txt")).unwrap(), content); |
|
| 2422 | 3692 | assert!(repo.bisect_status().unwrap().is_none()); |
|
| … | 28 unchanged lines |
||
| 2451 | 3721 | guard += 1; |
|
| 2452 | 3722 | assert!(guard < 20); |
|
| 2453 | 3723 | let text = fs::read_to_string(root.join("f.txt")).unwrap(); |
|
| 2454 | - | let is_culprit_state = heads.contains(&hashes[4]) && !heads.iter().any(|h| hashes[5..].contains(h)); |
|
| 2455 | - | step = if is_culprit_state { repo.bisect_mark(None, &[]).unwrap() } else { repo.bisect_mark(Some(!text.contains("bug")), &[]).unwrap() }; |
|
| 3724 | + | let is_culprit_state = |
|
| 3725 | + | heads.contains(&hashes[4]) && !heads.iter().any(|h| hashes[5..].contains(h)); |
|
| 3726 | + | step = if is_culprit_state { |
|
| 3727 | + | repo.bisect_mark(None, &[]).unwrap() |
|
| 3728 | + | } else { |
|
| 3729 | + | repo.bisect_mark(Some(!text.contains("bug")), &[]).unwrap() |
|
| 3730 | + | }; |
|
| 2456 | 3731 | } |
|
| 2457 | 3732 | match step { |
|
| 2458 | - | BisectStep::Inconclusive { candidates } => assert!(candidates.contains(&hashes[4]) && candidates.len() <= 2, "{candidates:?}"), |
|
| 3733 | + | BisectStep::Inconclusive { candidates } => assert!( |
|
| 3734 | + | candidates.contains(&hashes[4]) && candidates.len() <= 2, |
|
| 3735 | + | "{candidates:?}" |
|
| 3736 | + | ), |
|
| 2459 | 3737 | other => panic!("{other:?}"), |
|
| 2460 | 3738 | } |
|
| 2461 | 3739 | repo.bisect_reset().unwrap(); |
|
| … | 26 unchanged lines |
||
| 2488 | 3766 | let heads = repo.bookmark_heads().unwrap(); |
|
| 2489 | 3767 | let st = repo.load_state(&heads).unwrap(); |
|
| 2490 | 3768 | assert!(!st.contains(&c) && !st.contains(&a) && !st.contains(&e)); |
|
| 2491 | - | let e2 = st.changes().find(|(_, m)| m.change_id == id_e).map(|(h, _)| *h).unwrap(); |
|
| 2492 | - | assert_eq!(repo.store().get_change(&e2).unwrap().deps, vec![a2], "E now depends on A' alone"); |
|
| 3769 | + | let e2 = st |
|
| 3770 | + | .changes() |
|
| 3771 | + | .find(|(_, m)| m.change_id == id_e) |
|
| 3772 | + | .map(|(h, _)| *h) |
|
| 3773 | + | .unwrap(); |
|
| 3774 | + | assert_eq!( |
|
| 3775 | + | repo.store().get_change(&e2).unwrap().deps, |
|
| 3776 | + | vec![a2], |
|
| 3777 | + | "E now depends on A' alone" |
|
| 3778 | + | ); |
|
| 2493 | 3779 | let files = st.materialize().files; |
|
| 2494 | - | assert!(matches!(&files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"1\n2\n3\n")); |
|
| 3780 | + | assert!( |
|
| 3781 | + | matches!(&files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"1\n2\n3\n") |
|
| 3782 | + | ); |
|
| 2495 | 3783 | assert!(files.contains_key("b.txt")); |
|
| 2496 | 3784 | assert!(st.contains(&b)); |
|
| 2497 | 3785 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| … | 31 unchanged lines |
||
| 2529 | 3817 | let heads = repo.bookmark_heads().unwrap(); |
|
| 2530 | 3818 | let st = repo.load_state(&heads).unwrap(); |
|
| 2531 | 3819 | assert!(!st.contains(&c) && st.contains(&d)); |
|
| 2532 | - | let e2 = st.changes().find(|(_, m)| m.change_id == id_e).map(|(h, _)| *h).unwrap(); |
|
| 3820 | + | let e2 = st |
|
| 3821 | + | .changes() |
|
| 3822 | + | .find(|(_, m)| m.change_id == id_e) |
|
| 3823 | + | .map(|(h, _)| *h) |
|
| 3824 | + | .unwrap(); |
|
| 2533 | 3825 | let e2deps = repo.store().get_change(&e2).unwrap().deps; |
|
| 2534 | - | assert!(e2deps.contains(&c2) && !e2deps.contains(&c) && e2deps.contains(&a), "E follows C by content and still depends on A, which introduced the file: {e2deps:?}"); |
|
| 3826 | + | assert!( |
|
| 3827 | + | e2deps.contains(&c2) && !e2deps.contains(&c) && e2deps.contains(&a), |
|
| 3828 | + | "E follows C by content and still depends on A, which introduced the file: {e2deps:?}" |
|
| 3829 | + | ); |
|
| 2535 | 3830 | let files = st.materialize().files; |
|
| 2536 | - | assert!(matches!(&files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"0\n1\n2\n3\n4\n"), "{:?}", files["a.txt"].content); |
|
| 3831 | + | assert!( |
|
| 3832 | + | matches!(&files["a.txt"].content, FileContent::Text { bytes, .. } if bytes == b"0\n1\n2\n3\n4\n"), |
|
| 3833 | + | "{:?}", |
|
| 3834 | + | files["a.txt"].content |
|
| 3835 | + | ); |
|
| 2537 | 3836 | assert!(!files["a.txt"].is_conflicted()); |
|
| 2538 | 3837 | // Rebasing onto a state that already contains the change is refused. |
|
| 2539 | 3838 | assert!(repo.rebase(&c2, &[c2]).is_err()); |
|
| 2540 | 3839 | let err = repo.rebase(&e2, &[d]).unwrap_err().to_string(); |
|
| 2541 | - | assert!(err.contains("lacks dependencies"), "E needs C, which D's state does not have: {err}"); |
|
| 3840 | + | assert!( |
|
| 3841 | + | err.contains("lacks dependencies"), |
|
| 3842 | + | "E needs C, which D's state does not have: {err}" |
|
| 3843 | + | ); |
|
| 2542 | 3844 | } |
|
| 2543 | 3845 | ||
| 2544 | 3846 | #[test] |
|
| … | 5 unchanged lines |
||
| 2550 | 3852 | write(root, "a.txt", b"a\n"); |
|
| 2551 | 3853 | repo.commit("a", &[]).unwrap(); |
|
| 2552 | 3854 | assert!(repo.secret_key().unwrap().is_none()); |
|
| 2553 | - | repo.tag_create("v1", &repo.bookmark_heads().unwrap(), "first release").unwrap(); |
|
| 3855 | + | repo.tag_create("v1", &repo.bookmark_heads().unwrap(), "first release") |
|
| 3856 | + | .unwrap(); |
|
| 2554 | 3857 | let tags = repo.tags().unwrap(); |
|
| 2555 | - | assert_eq!((tags[0].name.as_str(), tags[0].message.as_str(), tags[0].signer.as_deref()), ("v1", "first release", None)); |
|
| 2556 | - | assert!(repo.tag_refs().unwrap()[0].attestation.is_none(), "an unsigned attestation is not advertised"); |
|
| 3858 | + | assert_eq!( |
|
| 3859 | + | ( |
|
| 3860 | + | tags[0].name.as_str(), |
|
| 3861 | + | tags[0].message.as_str(), |
|
| 3862 | + | tags[0].signer.as_deref() |
|
| 3863 | + | ), |
|
| 3864 | + | ("v1", "first release", None) |
|
| 3865 | + | ); |
|
| 3866 | + | assert!( |
|
| 3867 | + | repo.tag_refs().unwrap()[0].attestation.is_none(), |
|
| 3868 | + | "an unsigned attestation is not advertised" |
|
| 3869 | + | ); |
|
| 2557 | 3870 | } |
|
| 2558 | - |
modified crates/oxy-core/src/repo/workspace.rs +35 −7
| … | 54 unchanged lines |
||
| 55 | 55 | ||
| 56 | 56 | /// The directory of a workspace, if registered. |
|
| 57 | 57 | pub fn workspace_path(&self, id: &WorkspaceId) -> Option<PathBuf> { |
|
| 58 | - | fs::read_to_string(self.workspace_dir(id).join("path")).ok().map(|s| PathBuf::from(s.trim())) |
|
| 58 | + | fs::read_to_string(self.workspace_dir(id).join("path")) |
|
| 59 | + | .ok() |
|
| 60 | + | .map(|s| PathBuf::from(s.trim())) |
|
| 59 | 61 | } |
|
| 60 | 62 | ||
| 61 | 63 | /// Every workspace this repository knows about. |
|
| … | 14 unchanged lines |
||
| 76 | 78 | let path = if id == self.workspace { |
|
| 77 | 79 | self.root.clone() |
|
| 78 | 80 | } else { |
|
| 79 | - | self.workspace_path(&id).unwrap_or_else(|| PathBuf::from("(unknown)")) |
|
| 81 | + | self.workspace_path(&id) |
|
| 82 | + | .unwrap_or_else(|| PathBuf::from("(unknown)")) |
|
| 80 | 83 | }; |
|
| 81 | - | out.push(WorkspaceInfo { id, path, working_copy: view.workspaces.get(&id).copied(), current: id == self.workspace }); |
|
| 84 | + | out.push(WorkspaceInfo { |
|
| 85 | + | id, |
|
| 86 | + | path, |
|
| 87 | + | working_copy: view.workspaces.get(&id).copied(), |
|
| 88 | + | current: id == self.workspace, |
|
| 89 | + | }); |
|
| 82 | 90 | } |
|
| 83 | 91 | Ok(out) |
|
| 84 | 92 | } |
|
| … | 19 unchanged lines |
||
| 104 | 112 | let (op_ref, op) = self.head_op()?; |
|
| 105 | 113 | let mut view = op.view.clone(); |
|
| 106 | 114 | view.workspaces.insert(id, wc_hash); |
|
| 107 | - | self.record_op(&[op_ref], &format!("oxy workspace add {}", dir.display()), view)?; |
|
| 115 | + | self.record_op( |
|
| 116 | + | &[op_ref], |
|
| 117 | + | &format!("oxy workspace add {}", dir.display()), |
|
| 118 | + | view, |
|
| 119 | + | )?; |
|
| 108 | 120 | self.register_workspace(&id, &dir)?; |
|
| 109 | 121 | self.set_workspace_bookmark(&id, &self.config.bookmark)?; |
|
| 110 | - | write_atomic(&dir.join(OXY_DIR), format!("oxydir: {}\nworkspace: {}\n", self.oxy.display(), id.to_hex()).as_bytes())?; |
|
| 122 | + | write_atomic( |
|
| 123 | + | &dir.join(OXY_DIR), |
|
| 124 | + | format!( |
|
| 125 | + | "oxydir: {}\nworkspace: {}\n", |
|
| 126 | + | self.oxy.display(), |
|
| 127 | + | id.to_hex() |
|
| 128 | + | ) |
|
| 129 | + | .as_bytes(), |
|
| 130 | + | )?; |
|
| 111 | 131 | let repo = Repo::open(&dir)?.with_home(self.keys_dir.parent().unwrap_or(&self.keys_dir)); |
|
| 112 | 132 | repo.checkout(&heads, true, "oxy workspace add (checkout)")?; |
|
| 113 | 133 | Ok(repo) |
|
| … | 9 unchanged lines |
||
| 123 | 143 | if view.workspaces.remove(id).is_none() && self.workspace_path(id).is_none() { |
|
| 124 | 144 | return fail(format!("no workspace {}", id.short(8))); |
|
| 125 | 145 | } |
|
| 126 | - | self.record_op(&[op_ref], &format!("oxy workspace forget {}", id.short(8)), view)?; |
|
| 146 | + | self.record_op( |
|
| 147 | + | &[op_ref], |
|
| 148 | + | &format!("oxy workspace forget {}", id.short(8)), |
|
| 149 | + | view, |
|
| 150 | + | )?; |
|
| 127 | 151 | let _ = fs::remove_dir_all(self.workspace_dir(id)); |
|
| 128 | 152 | Ok(()) |
|
| 129 | 153 | } |
|
| … | 4 unchanged lines |
||
| 134 | 158 | fs::create_dir_all(&dir)?; |
|
| 135 | 159 | let path = dir.join("lock"); |
|
| 136 | 160 | for _ in 0..50 { |
|
| 137 | - | match fs::OpenOptions::new().write(true).create_new(true).open(&path) { |
|
| 161 | + | match fs::OpenOptions::new() |
|
| 162 | + | .write(true) |
|
| 163 | + | .create_new(true) |
|
| 164 | + | .open(&path) |
|
| 165 | + | { |
|
| 138 | 166 | Ok(mut f) => { |
|
| 139 | 167 | let _ = write!(f, "{}", std::process::id()); |
|
| 140 | 168 | return Ok(SnapshotLock { path }); |
|
| … | 18 unchanged lines |
modified crates/oxy-core/src/rewrite.rs +377 −64
| … | 72 unchanged lines |
||
| 73 | 73 | } |
|
| 74 | 74 | let mut ops = HashMap::new(); |
|
| 75 | 75 | for (i, op) in old.tree_ops.iter().enumerate() { |
|
| 76 | - | if let Some(j) = new.tree_ops.iter().position(|n| n.node() == op.node() && std::mem::discriminant(n) == std::mem::discriminant(op)) { |
|
| 76 | + | if let Some(j) = new.tree_ops.iter().position(|n| { |
|
| 77 | + | n.node() == op.node() && std::mem::discriminant(n) == std::mem::discriminant(op) |
|
| 78 | + | }) { |
|
| 77 | 79 | ops.insert(i as u32, j as u32); |
|
| 78 | 80 | } |
|
| 79 | 81 | } |
|
| … | 29 unchanged lines |
||
| 109 | 111 | let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new(); |
|
| 110 | 112 | let mut out = Vec::new(); |
|
| 111 | 113 | for e in &c.edits { |
|
| 112 | - | if let Edit::Insert { node, after, before, lines } = e { |
|
| 114 | + | if let Edit::Insert { |
|
| 115 | + | node, |
|
| 116 | + | after, |
|
| 117 | + | before, |
|
| 118 | + | lines, |
|
| 119 | + | } = e |
|
| 120 | + | { |
|
| 113 | 121 | let idx = per_node.entry(*node).or_default(); |
|
| 114 | - | out.push(Chain { node: *node, first: *idx, last: *idx + lines.len() as u32 - 1, after: *after, before: *before }); |
|
| 122 | + | out.push(Chain { |
|
| 123 | + | node: *node, |
|
| 124 | + | first: *idx, |
|
| 125 | + | last: *idx + lines.len() as u32 - 1, |
|
| 126 | + | after: *after, |
|
| 127 | + | before: *before, |
|
| 128 | + | }); |
|
| 115 | 129 | *idx += lines.len() as u32; |
|
| 116 | 130 | } |
|
| 117 | 131 | } |
|
| … | 15 unchanged lines |
||
| 133 | 147 | /// orphaned references. |
|
| 134 | 148 | /// |
|
| 135 | 149 | /// `load` fetches a change's full object (a state keeps only metadata). |
|
| 136 | - | pub fn propagate(state: &State, old: ChangeHash, new: Option<&Change>, corr: &Correspondence, load: &dyn Fn(&ChangeHash) -> Option<Change>) -> Result<Propagation, RewriteError> { |
|
| 150 | + | pub fn propagate( |
|
| 151 | + | state: &State, |
|
| 152 | + | old: ChangeHash, |
|
| 153 | + | new: Option<&Change>, |
|
| 154 | + | corr: &Correspondence, |
|
| 155 | + | load: &dyn Fn(&ChangeHash) -> Option<Change>, |
|
| 156 | + | ) -> Result<Propagation, RewriteError> { |
|
| 137 | 157 | let old_meta = state.get(&old).ok_or(RewriteError::Missing(old))?; |
|
| 138 | 158 | let old_full = load(&old); |
|
| 139 | 159 | let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new(); |
|
| 140 | - | mapping.insert(old, match new { |
|
| 141 | - | Some(n) => Replacement::Change(n.hash()), |
|
| 142 | - | None => Replacement::Removed(old_meta.deps.clone()), |
|
| 143 | - | }); |
|
| 160 | + | mapping.insert( |
|
| 161 | + | old, |
|
| 162 | + | match new { |
|
| 163 | + | Some(n) => Replacement::Change(n.hash()), |
|
| 164 | + | None => Replacement::Removed(old_meta.deps.clone()), |
|
| 165 | + | }, |
|
| 166 | + | ); |
|
| 144 | 167 | let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new(); |
|
| 145 | 168 | corrs.insert(old, corr.clone()); |
|
| 146 | 169 | // Chains of the rewritten change, for re-anchoring; vanished nodes. |
|
| 147 | - | let chains: HashMap<ChangeHash, Vec<Chain>> = old_full.as_ref().map(|c| (old, chains_of(c))).into_iter().collect(); |
|
| 170 | + | let chains: HashMap<ChangeHash, Vec<Chain>> = old_full |
|
| 171 | + | .as_ref() |
|
| 172 | + | .map(|c| (old, chains_of(c))) |
|
| 173 | + | .into_iter() |
|
| 174 | + | .collect(); |
|
| 148 | 175 | let mut vanished_nodes: BTreeSet<crate::ids::NodeId> = BTreeSet::new(); |
|
| 149 | 176 | if let Some(c) = &old_full { |
|
| 150 | 177 | for (i, op) in c.tree_ops.iter().enumerate() { |
|
| … | 9 unchanged lines |
||
| 160 | 187 | ||
| 161 | 188 | // Dependents in topological order: every change whose closure has `old`, |
|
| 162 | 189 | // sorted so that dependencies come first. |
|
| 163 | - | let mut dependents: Vec<ChangeHash> = state.changes().filter(|(h, _)| **h != old && state.closure(h).contains(&old)).map(|(h, _)| *h).collect(); |
|
| 190 | + | let mut dependents: Vec<ChangeHash> = state |
|
| 191 | + | .changes() |
|
| 192 | + | .filter(|(h, _)| **h != old && state.closure(h).contains(&old)) |
|
| 193 | + | .map(|(h, _)| *h) |
|
| 194 | + | .collect(); |
|
| 164 | 195 | dependents.sort_by_key(|h| (state.closure(h).len(), *h)); |
|
| 165 | 196 | ||
| 166 | 197 | let mut out = Vec::new(); |
|
| 167 | 198 | for h in dependents { |
|
| 168 | 199 | let c = load(&h).ok_or(RewriteError::Missing(h))?; |
|
| 169 | - | let rewritten = rewrite_one(&h, &c, &mapping, &corrs, &chains, &vanished_nodes, &mut notes); |
|
| 200 | + | let rewritten = rewrite_one( |
|
| 201 | + | &h, |
|
| 202 | + | &c, |
|
| 203 | + | &mapping, |
|
| 204 | + | &corrs, |
|
| 205 | + | &chains, |
|
| 206 | + | &vanished_nodes, |
|
| 207 | + | &mut notes, |
|
| 208 | + | ); |
|
| 170 | 209 | let rh = rewritten.hash(); |
|
| 171 | 210 | corrs.insert(h, Correspondence::identity(&rewritten)); |
|
| 172 | 211 | mapping.insert(h, Replacement::Change(rh)); |
|
| 173 | 212 | out.push((h, rewritten)); |
|
| 174 | 213 | } |
|
| 175 | - | Ok(Propagation { changes: out, notes }) |
|
| 214 | + | Ok(Propagation { |
|
| 215 | + | changes: out, |
|
| 216 | + | notes, |
|
| 217 | + | }) |
|
| 176 | 218 | } |
|
| 177 | 219 | ||
| 178 | 220 | /// Re-anchor a reference to a vanished atom of `change`: the nearest |
|
| 179 | 221 | /// surviving atom of its chain (before it for `after` anchors, after it for |
|
| 180 | 222 | /// `before` anchors), else the chain's own anchor. |
|
| 181 | - | fn reanchor(change: &ChangeHash, index: u32, want_after: bool, mapping: &HashMap<ChangeHash, Replacement>, corrs: &HashMap<ChangeHash, Correspondence>, chains: &HashMap<ChangeHash, Vec<Chain>>) -> AtomRef { |
|
| 223 | + | fn reanchor( |
|
| 224 | + | change: &ChangeHash, |
|
| 225 | + | index: u32, |
|
| 226 | + | want_after: bool, |
|
| 227 | + | mapping: &HashMap<ChangeHash, Replacement>, |
|
| 228 | + | corrs: &HashMap<ChangeHash, Correspondence>, |
|
| 229 | + | chains: &HashMap<ChangeHash, Vec<Chain>>, |
|
| 230 | + | ) -> AtomRef { |
|
| 182 | 231 | let corr = corrs.get(change); |
|
| 183 | - | let chain = chains.get(change).and_then(|cs| cs.iter().find(|ch| ch.first <= index && index <= ch.last)); |
|
| 184 | - | if let (Some(Replacement::Change(nh)), Some(corr), Some(ch)) = (mapping.get(change), corr, chain) { |
|
| 185 | - | let candidates: Box<dyn Iterator<Item = u32>> = if want_after { Box::new((ch.first..index).rev()) } else { Box::new(index + 1..=ch.last) }; |
|
| 232 | + | let chain = chains |
|
| 233 | + | .get(change) |
|
| 234 | + | .and_then(|cs| cs.iter().find(|ch| ch.first <= index && index <= ch.last)); |
|
| 235 | + | if let (Some(Replacement::Change(nh)), Some(corr), Some(ch)) = |
|
| 236 | + | (mapping.get(change), corr, chain) |
|
| 237 | + | { |
|
| 238 | + | let candidates: Box<dyn Iterator<Item = u32>> = if want_after { |
|
| 239 | + | Box::new((ch.first..index).rev()) |
|
| 240 | + | } else { |
|
| 241 | + | Box::new(index + 1..=ch.last) |
|
| 242 | + | }; |
|
| 186 | 243 | for j in candidates { |
|
| 187 | 244 | if let Some(nj) = corr.atoms.get(&(ch.node, j)) { |
|
| 188 | - | return AtomRef::Atom { change: *nh, index: *nj }; |
|
| 245 | + | return AtomRef::Atom { |
|
| 246 | + | change: *nh, |
|
| 247 | + | index: *nj, |
|
| 248 | + | }; |
|
| 189 | 249 | } |
|
| 190 | 250 | } |
|
| 191 | 251 | } |
|
| 192 | 252 | match chain { |
|
| 193 | - | Some(ch) => if want_after { ch.after } else { ch.before }, |
|
| 194 | - | None => if want_after { AtomRef::Start } else { AtomRef::End }, |
|
| 253 | + | Some(ch) => { |
|
| 254 | + | if want_after { |
|
| 255 | + | ch.after |
|
| 256 | + | } else { |
|
| 257 | + | ch.before |
|
| 258 | + | } |
|
| 259 | + | } |
|
| 260 | + | None => { |
|
| 261 | + | if want_after { |
|
| 262 | + | AtomRef::Start |
|
| 263 | + | } else { |
|
| 264 | + | AtomRef::End |
|
| 265 | + | } |
|
| 266 | + | } |
|
| 195 | 267 | } |
|
| 196 | 268 | } |
|
| 197 | 269 | ||
| … | 8 unchanged lines |
||
| 206 | 278 | ) -> Change { |
|
| 207 | 279 | let short = hash.short(12); |
|
| 208 | 280 | // An anchor: translate, or re-anchor when its counterpart is gone. |
|
| 209 | - | let map_anchor = |r: AtomRef, node: &crate::ids::NodeId, want_after: bool, notes: &mut Vec<String>| -> AtomRef { |
|
| 281 | + | let map_anchor = |r: AtomRef, |
|
| 282 | + | node: &crate::ids::NodeId, |
|
| 283 | + | want_after: bool, |
|
| 284 | + | notes: &mut Vec<String>| |
|
| 285 | + | -> AtomRef { |
|
| 210 | 286 | match r { |
|
| 211 | 287 | AtomRef::Atom { change, index } => match mapping.get(&change) { |
|
| 212 | 288 | Some(Replacement::Change(nh)) => match corrs[&change].atoms.get(&(*node, index)) { |
|
| 213 | - | Some(idx) => AtomRef::Atom { change: *nh, index: *idx }, |
|
| 289 | + | Some(idx) => AtomRef::Atom { |
|
| 290 | + | change: *nh, |
|
| 291 | + | index: *idx, |
|
| 292 | + | }, |
|
| 214 | 293 | None => { |
|
| 215 | 294 | notes.push(format!("change {short}: an anchor line of the rewritten change vanished; re-anchored to its nearest neighbour")); |
|
| 216 | 295 | reanchor(&change, index, want_after, mapping, corrs, chains) |
|
| … | 11 unchanged lines |
||
| 228 | 307 | // A tree-operation reference: translate, or `None` when gone. |
|
| 229 | 308 | let map_op = |r: OpRef| -> Option<OpRef> { |
|
| 230 | 309 | match mapping.get(&r.change) { |
|
| 231 | - | Some(Replacement::Change(nh)) => corrs[&r.change].ops.get(&r.index).map(|idx| OpRef { change: *nh, index: *idx }), |
|
| 310 | + | Some(Replacement::Change(nh)) => corrs[&r.change].ops.get(&r.index).map(|idx| OpRef { |
|
| 311 | + | change: *nh, |
|
| 312 | + | index: *idx, |
|
| 313 | + | }), |
|
| 232 | 314 | Some(Replacement::Removed(_)) => None, |
|
| 233 | 315 | None => Some(r), |
|
| 234 | 316 | } |
|
| … | 15 unchanged lines |
||
| 250 | 332 | let mut ops = Vec::new(); |
|
| 251 | 333 | for op in &out.tree_ops { |
|
| 252 | 334 | let node = op.node(); |
|
| 253 | - | if vanished_nodes.contains(&node) || matches!(op, TreeOp::AddNode { parent, .. } if vanished_nodes.contains(parent)) { |
|
| 335 | + | if vanished_nodes.contains(&node) |
|
| 336 | + | || matches!(op, TreeOp::AddNode { parent, .. } if vanished_nodes.contains(parent)) |
|
| 337 | + | { |
|
| 254 | 338 | notes.push(format!("change {short}: an operation on a file that vanished with the rewritten change was dropped")); |
|
| 255 | 339 | continue; |
|
| 256 | 340 | } |
|
| 257 | 341 | match op { |
|
| 258 | 342 | TreeOp::RemoveNode { node, placement } => match map_op(*placement) { |
|
| 259 | - | Some(p) => ops.push(TreeOp::RemoveNode { node: *node, placement: p }), |
|
| 260 | - | None => notes.push(format!("change {short}: removal of a placement that vanished was dropped")), |
|
| 343 | + | Some(p) => ops.push(TreeOp::RemoveNode { |
|
| 344 | + | node: *node, |
|
| 345 | + | placement: p, |
|
| 346 | + | }), |
|
| 347 | + | None => notes.push(format!( |
|
| 348 | + | "change {short}: removal of a placement that vanished was dropped" |
|
| 349 | + | )), |
|
| 261 | 350 | }, |
|
| 262 | - | TreeOp::Move { node, old, parent, name } => match map_op(*old) { |
|
| 263 | - | Some(o) => ops.push(TreeOp::Move { node: *node, old: o, parent: *parent, name: name.clone() }), |
|
| 264 | - | None => notes.push(format!("change {short}: a move of {name} lost its origin and was dropped")), |
|
| 351 | + | TreeOp::Move { |
|
| 352 | + | node, |
|
| 353 | + | old, |
|
| 354 | + | parent, |
|
| 355 | + | name, |
|
| 356 | + | } => match map_op(*old) { |
|
| 357 | + | Some(o) => ops.push(TreeOp::Move { |
|
| 358 | + | node: *node, |
|
| 359 | + | old: o, |
|
| 360 | + | parent: *parent, |
|
| 361 | + | name: name.clone(), |
|
| 362 | + | }), |
|
| 363 | + | None => notes.push(format!( |
|
| 364 | + | "change {short}: a move of {name} lost its origin and was dropped" |
|
| 365 | + | )), |
|
| 265 | 366 | }, |
|
| 266 | 367 | TreeOp::SetMode { node, old, mode } => match map_op(*old) { |
|
| 267 | - | Some(o) => ops.push(TreeOp::SetMode { node: *node, old: o, mode: *mode }), |
|
| 268 | - | None => notes.push(format!("change {short}: a mode change lost its origin and was dropped")), |
|
| 368 | + | Some(o) => ops.push(TreeOp::SetMode { |
|
| 369 | + | node: *node, |
|
| 370 | + | old: o, |
|
| 371 | + | mode: *mode, |
|
| 372 | + | }), |
|
| 373 | + | None => notes.push(format!( |
|
| 374 | + | "change {short}: a mode change lost its origin and was dropped" |
|
| 375 | + | )), |
|
| 269 | 376 | }, |
|
| 270 | 377 | add @ TreeOp::AddNode { .. } => ops.push(add.clone()), |
|
| 271 | 378 | } |
|
| … | 6 unchanged lines |
||
| 278 | 385 | continue; |
|
| 279 | 386 | } |
|
| 280 | 387 | match e { |
|
| 281 | - | Edit::Insert { node, after, before, lines } => edits.push(Edit::Insert { |
|
| 388 | + | Edit::Insert { |
|
| 389 | + | node, |
|
| 390 | + | after, |
|
| 391 | + | before, |
|
| 392 | + | lines, |
|
| 393 | + | } => edits.push(Edit::Insert { |
|
| 282 | 394 | node: *node, |
|
| 283 | 395 | after: map_anchor(*after, node, true, notes), |
|
| 284 | 396 | before: map_anchor(*before, node, false, notes), |
|
| … | 5 unchanged lines |
||
| 290 | 402 | for r in atoms.iter() { |
|
| 291 | 403 | for i in r.from..r.to { |
|
| 292 | 404 | match mapping.get(&r.change) { |
|
| 293 | - | Some(Replacement::Change(nh)) => match corrs[&r.change].atoms.get(&(*node, i)) { |
|
| 294 | - | Some(idx) => mapped.push((*nh, *idx)), |
|
| 295 | - | None => dropped += 1, |
|
| 296 | - | }, |
|
| 405 | + | Some(Replacement::Change(nh)) => { |
|
| 406 | + | match corrs[&r.change].atoms.get(&(*node, i)) { |
|
| 407 | + | Some(idx) => mapped.push((*nh, *idx)), |
|
| 408 | + | None => dropped += 1, |
|
| 409 | + | } |
|
| 410 | + | } |
|
| 297 | 411 | Some(Replacement::Removed(_)) => dropped += 1, |
|
| 298 | 412 | None => mapped.push((r.change, i)), |
|
| 299 | 413 | } |
|
| 300 | 414 | } |
|
| 301 | 415 | } |
|
| 302 | 416 | if dropped > 0 { |
|
| 303 | - | notes.push(format!("change {short}: {dropped} deletion(s) of lines that vanished were dropped")); |
|
| 417 | + | notes.push(format!( |
|
| 418 | + | "change {short}: {dropped} deletion(s) of lines that vanished were dropped" |
|
| 419 | + | )); |
|
| 304 | 420 | } |
|
| 305 | 421 | let mut ranges: Vec<AtomRange> = Vec::new(); |
|
| 306 | 422 | for (ch, i) in mapped { |
|
| 307 | 423 | match ranges.last_mut() { |
|
| 308 | 424 | Some(r) if r.change == ch && r.to == i => r.to = i + 1, |
|
| 309 | - | _ => ranges.push(AtomRange { change: ch, from: i, to: i + 1 }), |
|
| 425 | + | _ => ranges.push(AtomRange { |
|
| 426 | + | change: ch, |
|
| 427 | + | from: i, |
|
| 428 | + | to: i + 1, |
|
| 429 | + | }), |
|
| 310 | 430 | } |
|
| 311 | 431 | } |
|
| 312 | 432 | if !ranges.is_empty() { |
|
| 313 | - | edits.push(Edit::Delete { node: *node, atoms: ranges }); |
|
| 433 | + | edits.push(Edit::Delete { |
|
| 434 | + | node: *node, |
|
| 435 | + | atoms: ranges, |
|
| 436 | + | }); |
|
| 314 | 437 | } |
|
| 315 | 438 | } |
|
| 316 | 439 | other => edits.push(other.clone()), |
|
| … | 13 unchanged lines |
||
| 330 | 453 | use crate::state::FileContent; |
|
| 331 | 454 | ||
| 332 | 455 | fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change { |
|
| 333 | - | let mut c = Change { change_id: ChangeId::random(), deps, author: ObjectRef::ZERO, hlc: Hlc { wall_ms: 1, logical: 0 }, message: msg.into(), tree_ops, edits, kind: ChangeKind::Normal }; |
|
| 456 | + | let mut c = Change { |
|
| 457 | + | change_id: ChangeId::random(), |
|
| 458 | + | deps, |
|
| 459 | + | author: ObjectRef::ZERO, |
|
| 460 | + | hlc: Hlc { |
|
| 461 | + | wall_ms: 1, |
|
| 462 | + | logical: 0, |
|
| 463 | + | }, |
|
| 464 | + | message: msg.into(), |
|
| 465 | + | tree_ops, |
|
| 466 | + | edits, |
|
| 467 | + | kind: ChangeKind::Normal, |
|
| 468 | + | }; |
|
| 334 | 469 | c.normalize(); |
|
| 335 | 470 | c |
|
| 336 | 471 | } |
|
| … | 2 unchanged lines |
||
| 339 | 474 | fn describe_propagates_through_dependents() { |
|
| 340 | 475 | let mut st = State::new(); |
|
| 341 | 476 | let f = NodeId::random(); |
|
| 342 | - | let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"b\n".to_vec()] }], "one"); |
|
| 477 | + | let c1 = change( |
|
| 478 | + | vec![], |
|
| 479 | + | vec![TreeOp::AddNode { |
|
| 480 | + | node: f, |
|
| 481 | + | kind: NodeKind::File, |
|
| 482 | + | mode: Mode::TEXT, |
|
| 483 | + | parent: NodeId::ROOT, |
|
| 484 | + | name: "f".into(), |
|
| 485 | + | }], |
|
| 486 | + | vec![Edit::Insert { |
|
| 487 | + | node: f, |
|
| 488 | + | after: AtomRef::Start, |
|
| 489 | + | before: AtomRef::End, |
|
| 490 | + | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()], |
|
| 491 | + | }], |
|
| 492 | + | "one", |
|
| 493 | + | ); |
|
| 343 | 494 | let h1 = st.add(c1.clone()).unwrap(); |
|
| 344 | - | let c2 = change(vec![h1], vec![], vec![Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] }, Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"c\n".to_vec()] }], "two"); |
|
| 495 | + | let c2 = change( |
|
| 496 | + | vec![h1], |
|
| 497 | + | vec![], |
|
| 498 | + | vec![ |
|
| 499 | + | Edit::Delete { |
|
| 500 | + | node: f, |
|
| 501 | + | atoms: vec![AtomRange { |
|
| 502 | + | change: h1, |
|
| 503 | + | from: 0, |
|
| 504 | + | to: 1, |
|
| 505 | + | }], |
|
| 506 | + | }, |
|
| 507 | + | Edit::Insert { |
|
| 508 | + | node: f, |
|
| 509 | + | after: AtomRef::Atom { |
|
| 510 | + | change: h1, |
|
| 511 | + | index: 1, |
|
| 512 | + | }, |
|
| 513 | + | before: AtomRef::End, |
|
| 514 | + | lines: vec![b"c\n".to_vec()], |
|
| 515 | + | }, |
|
| 516 | + | ], |
|
| 517 | + | "two", |
|
| 518 | + | ); |
|
| 345 | 519 | let h2 = st.add(c2.clone()).unwrap(); |
|
| 346 | - | let c3 = change(vec![h2], vec![TreeOp::SetMode { node: f, old: OpRef { change: h1, index: 0 }, mode: Mode { exec: true, opaque: false } }], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h2, index: 0 }, before: AtomRef::End, lines: vec![b"d\n".to_vec()] }], "three"); |
|
| 520 | + | let c3 = change( |
|
| 521 | + | vec![h2], |
|
| 522 | + | vec![TreeOp::SetMode { |
|
| 523 | + | node: f, |
|
| 524 | + | old: OpRef { |
|
| 525 | + | change: h1, |
|
| 526 | + | index: 0, |
|
| 527 | + | }, |
|
| 528 | + | mode: Mode { |
|
| 529 | + | exec: true, |
|
| 530 | + | opaque: false, |
|
| 531 | + | }, |
|
| 532 | + | }], |
|
| 533 | + | vec![Edit::Insert { |
|
| 534 | + | node: f, |
|
| 535 | + | after: AtomRef::Atom { |
|
| 536 | + | change: h2, |
|
| 537 | + | index: 0, |
|
| 538 | + | }, |
|
| 539 | + | before: AtomRef::End, |
|
| 540 | + | lines: vec![b"d\n".to_vec()], |
|
| 541 | + | }], |
|
| 542 | + | "three", |
|
| 543 | + | ); |
|
| 347 | 544 | let h3 = st.add(c3.clone()).unwrap(); |
|
| 348 | 545 | let before = st.materialize(); |
|
| 349 | 546 | ||
| 350 | 547 | // Rewrite c1's message; everything downstream follows. |
|
| 351 | 548 | let mut n1 = c1.clone(); |
|
| 352 | 549 | n1.message = "one, described".into(); |
|
| 353 | - | let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())].into_iter().collect(); |
|
| 550 | + | let all: HashMap<ChangeHash, Change> = |
|
| 551 | + | [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())] |
|
| 552 | + | .into_iter() |
|
| 553 | + | .collect(); |
|
| 354 | 554 | let load = |h: &ChangeHash| all.get(h).cloned(); |
|
| 355 | - | let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load).unwrap().changes; |
|
| 356 | - | assert_eq!(rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(), vec![h2, h3]); |
|
| 555 | + | let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load) |
|
| 556 | + | .unwrap() |
|
| 557 | + | .changes; |
|
| 558 | + | assert_eq!( |
|
| 559 | + | rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(), |
|
| 560 | + | vec![h2, h3] |
|
| 561 | + | ); |
|
| 357 | 562 | let mut st2 = State::new(); |
|
| 358 | 563 | st2.add(n1.clone()).unwrap(); |
|
| 359 | 564 | for (_, c) in &rewritten { |
|
| … | 3 unchanged lines |
||
| 363 | 568 | let after = st2.materialize(); |
|
| 364 | 569 | assert_eq!(after.files["f"].mode, before.files["f"].mode); |
|
| 365 | 570 | match (&after.files["f"].content, &before.files["f"].content) { |
|
| 366 | - | (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => assert_eq!(a, b), |
|
| 571 | + | (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => { |
|
| 572 | + | assert_eq!(a, b) |
|
| 573 | + | } |
|
| 367 | 574 | _ => panic!(), |
|
| 368 | 575 | } |
|
| 369 | - | assert!(matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n")); |
|
| 576 | + | assert!( |
|
| 577 | + | matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n") |
|
| 578 | + | ); |
|
| 370 | 579 | // ChangeIds survive; hashes change; deps point at the new hashes. |
|
| 371 | 580 | assert_eq!(rewritten[0].1.change_id, c2.change_id); |
|
| 372 | 581 | assert_eq!(rewritten[0].1.deps, vec![n1.hash()]); |
|
| … | 5 unchanged lines |
||
| 378 | 587 | fn missing_counterpart_is_reanchored() { |
|
| 379 | 588 | let mut st = State::new(); |
|
| 380 | 589 | let f = NodeId::random(); |
|
| 381 | - | let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"b\n".to_vec()] }], "one"); |
|
| 590 | + | let c1 = change( |
|
| 591 | + | vec![], |
|
| 592 | + | vec![TreeOp::AddNode { |
|
| 593 | + | node: f, |
|
| 594 | + | kind: NodeKind::File, |
|
| 595 | + | mode: Mode::TEXT, |
|
| 596 | + | parent: NodeId::ROOT, |
|
| 597 | + | name: "f".into(), |
|
| 598 | + | }], |
|
| 599 | + | vec![Edit::Insert { |
|
| 600 | + | node: f, |
|
| 601 | + | after: AtomRef::Start, |
|
| 602 | + | before: AtomRef::End, |
|
| 603 | + | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()], |
|
| 604 | + | }], |
|
| 605 | + | "one", |
|
| 606 | + | ); |
|
| 382 | 607 | let h1 = st.add(c1.clone()).unwrap(); |
|
| 383 | - | let c2 = change(vec![h1], vec![], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"c\n".to_vec()] }], "two"); |
|
| 608 | + | let c2 = change( |
|
| 609 | + | vec![h1], |
|
| 610 | + | vec![], |
|
| 611 | + | vec![Edit::Insert { |
|
| 612 | + | node: f, |
|
| 613 | + | after: AtomRef::Atom { |
|
| 614 | + | change: h1, |
|
| 615 | + | index: 1, |
|
| 616 | + | }, |
|
| 617 | + | before: AtomRef::End, |
|
| 618 | + | lines: vec![b"c\n".to_vec()], |
|
| 619 | + | }], |
|
| 620 | + | "two", |
|
| 621 | + | ); |
|
| 384 | 622 | let h2 = st.add(c2.clone()).unwrap(); |
|
| 385 | 623 | let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect(); |
|
| 386 | 624 | let load = |h: &ChangeHash| all.get(h).cloned(); |
|
| 387 | 625 | // The rewritten c1 drops line "b": atom 1 has no counterpart, so c2's |
|
| 388 | 626 | // insert after "b" is re-anchored after "a", the nearest survivor. |
|
| 389 | 627 | let mut n1 = c1.clone(); |
|
| 390 | - | n1.edits = vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec()] }]; |
|
| 391 | - | let corr = Correspondence { atoms: [((f, 0), 0)].into_iter().collect(), ops: [(0, 0)].into_iter().collect() }; |
|
| 628 | + | n1.edits = vec![Edit::Insert { |
|
| 629 | + | node: f, |
|
| 630 | + | after: AtomRef::Start, |
|
| 631 | + | before: AtomRef::End, |
|
| 632 | + | lines: vec![b"a\n".to_vec()], |
|
| 633 | + | }]; |
|
| 634 | + | let corr = Correspondence { |
|
| 635 | + | atoms: [((f, 0), 0)].into_iter().collect(), |
|
| 636 | + | ops: [(0, 0)].into_iter().collect(), |
|
| 637 | + | }; |
|
| 392 | 638 | let p = propagate(&st, h1, Some(&n1), &corr, &load).unwrap(); |
|
| 393 | 639 | assert_eq!(p.notes.len(), 1, "{:?}", p.notes); |
|
| 394 | 640 | let n1h = n1.hash(); |
|
| 395 | - | assert!(matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h)); |
|
| 641 | + | assert!( |
|
| 642 | + | matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h) |
|
| 643 | + | ); |
|
| 396 | 644 | let mut st2 = State::new(); |
|
| 397 | 645 | st2.add(n1.clone()).unwrap(); |
|
| 398 | 646 | st2.add(p.changes[0].1.clone()).unwrap(); |
|
| 399 | 647 | let m = st2.materialize(); |
|
| 400 | 648 | assert!(!m.is_conflicted()); |
|
| 401 | - | assert!(matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n")); |
|
| 402 | - | assert_eq!(propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(), RewriteError::Missing(ObjectRef([9; 32]))); |
|
| 649 | + | assert!( |
|
| 650 | + | matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n") |
|
| 651 | + | ); |
|
| 652 | + | assert_eq!( |
|
| 653 | + | propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(), |
|
| 654 | + | RewriteError::Missing(ObjectRef([9; 32])) |
|
| 655 | + | ); |
|
| 403 | 656 | ||
| 404 | 657 | // Removing c1 altogether: f vanishes with it, so c2's edit is dropped. |
|
| 405 | 658 | let p = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap(); |
|
| … | 4 unchanged lines |
||
| 410 | 663 | // to c1's chain anchors (START, END). |
|
| 411 | 664 | let mut n2 = c1.clone(); |
|
| 412 | 665 | n2.edits = vec![]; |
|
| 413 | - | let corr = Correspondence { atoms: HashMap::new(), ops: [(0, 0)].into_iter().collect() }; |
|
| 666 | + | let corr = Correspondence { |
|
| 667 | + | atoms: HashMap::new(), |
|
| 668 | + | ops: [(0, 0)].into_iter().collect(), |
|
| 669 | + | }; |
|
| 414 | 670 | let p = propagate(&st, h1, Some(&n2), &corr, &load).unwrap(); |
|
| 415 | - | assert!(matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Start, before: AtomRef::End, .. })); |
|
| 671 | + | assert!(matches!( |
|
| 672 | + | p.changes[0].1.edits[0], |
|
| 673 | + | Edit::Insert { |
|
| 674 | + | after: AtomRef::Start, |
|
| 675 | + | before: AtomRef::End, |
|
| 676 | + | .. |
|
| 677 | + | } |
|
| 678 | + | )); |
|
| 416 | 679 | let mut st3 = State::new(); |
|
| 417 | 680 | st3.add(n2.clone()).unwrap(); |
|
| 418 | 681 | st3.add(p.changes[0].1.clone()).unwrap(); |
|
| 419 | 682 | let m = st3.materialize(); |
|
| 420 | 683 | assert!(!m.is_conflicted()); |
|
| 421 | - | assert!(matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n")); |
|
| 684 | + | assert!( |
|
| 685 | + | matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n") |
|
| 686 | + | ); |
|
| 422 | 687 | } |
|
| 423 | 688 | ||
| 424 | 689 | #[test] |
|
| … | 1 unchanged lines |
||
| 426 | 691 | let mut st = State::new(); |
|
| 427 | 692 | let f = NodeId::random(); |
|
| 428 | 693 | let g = NodeId::random(); |
|
| 429 | - | let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![], "one"); |
|
| 694 | + | let c1 = change( |
|
| 695 | + | vec![], |
|
| 696 | + | vec![TreeOp::AddNode { |
|
| 697 | + | node: f, |
|
| 698 | + | kind: NodeKind::File, |
|
| 699 | + | mode: Mode::TEXT, |
|
| 700 | + | parent: NodeId::ROOT, |
|
| 701 | + | name: "f".into(), |
|
| 702 | + | }], |
|
| 703 | + | vec![], |
|
| 704 | + | "one", |
|
| 705 | + | ); |
|
| 430 | 706 | let h1 = st.add(c1.clone()).unwrap(); |
|
| 431 | 707 | // c2 merely depends on c1 explicitly (touches another file). |
|
| 432 | - | let c2 = change(vec![h1], vec![TreeOp::AddNode { node: g, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "g".into() }], vec![], "two"); |
|
| 708 | + | let c2 = change( |
|
| 709 | + | vec![h1], |
|
| 710 | + | vec![TreeOp::AddNode { |
|
| 711 | + | node: g, |
|
| 712 | + | kind: NodeKind::File, |
|
| 713 | + | mode: Mode::TEXT, |
|
| 714 | + | parent: NodeId::ROOT, |
|
| 715 | + | name: "g".into(), |
|
| 716 | + | }], |
|
| 717 | + | vec![], |
|
| 718 | + | "two", |
|
| 719 | + | ); |
|
| 433 | 720 | let h2 = st.add(c2.clone()).unwrap(); |
|
| 434 | - | let c3 = change(vec![h2], vec![], vec![Edit::Insert { node: g, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }], "three"); |
|
| 721 | + | let c3 = change( |
|
| 722 | + | vec![h2], |
|
| 723 | + | vec![], |
|
| 724 | + | vec![Edit::Insert { |
|
| 725 | + | node: g, |
|
| 726 | + | after: AtomRef::Start, |
|
| 727 | + | before: AtomRef::End, |
|
| 728 | + | lines: vec![b"x\n".to_vec()], |
|
| 729 | + | }], |
|
| 730 | + | "three", |
|
| 731 | + | ); |
|
| 435 | 732 | let h3 = st.add(c3.clone()).unwrap(); |
|
| 436 | - | let mut all: HashMap<ChangeHash, Change> = [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect(); |
|
| 733 | + | let mut all: HashMap<ChangeHash, Change> = |
|
| 734 | + | [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect(); |
|
| 437 | 735 | let load_all = all.clone(); |
|
| 438 | 736 | let load = move |h: &ChangeHash| load_all.get(h).cloned(); |
|
| 439 | 737 | // Removing c2 (an explicit-only dependency of nothing that references it) works: c3 inherits h1. |
|
| 440 | - | let out = propagate(&st, h2, None, &Correspondence::default(), &load).unwrap().changes; |
|
| 738 | + | let out = propagate(&st, h2, None, &Correspondence::default(), &load) |
|
| 739 | + | .unwrap() |
|
| 740 | + | .changes; |
|
| 441 | 741 | assert_eq!(out.len(), 1); |
|
| 442 | 742 | assert_eq!(out[0].0, h3); |
|
| 443 | 743 | assert_eq!(out[0].1.deps, vec![h1]); |
|
| 444 | 744 | // Removing c1 fails: c2 does not reference c1 (only an explicit dep), but |
|
| 445 | 745 | // nothing else does either, so it succeeds and c2 inherits no deps. |
|
| 446 | - | let out = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap().changes; |
|
| 746 | + | let out = propagate(&st, h1, None, &Correspondence::default(), &load) |
|
| 747 | + | .unwrap() |
|
| 748 | + | .changes; |
|
| 447 | 749 | assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty())); |
|
| 448 | 750 | // Removing c2, which created g: c3's edit of g and c4's removal of g's |
|
| 449 | 751 | // placement are dropped with notes, since the file no longer exists. |
|
| 450 | - | let c4 = change(vec![h2], vec![TreeOp::RemoveNode { node: g, placement: OpRef { change: h2, index: 0 } }], vec![], "four"); |
|
| 752 | + | let c4 = change( |
|
| 753 | + | vec![h2], |
|
| 754 | + | vec![TreeOp::RemoveNode { |
|
| 755 | + | node: g, |
|
| 756 | + | placement: OpRef { |
|
| 757 | + | change: h2, |
|
| 758 | + | index: 0, |
|
| 759 | + | }, |
|
| 760 | + | }], |
|
| 761 | + | vec![], |
|
| 762 | + | "four", |
|
| 763 | + | ); |
|
| 451 | 764 | let h4 = st.add(c4.clone()).unwrap(); |
|
| 452 | 765 | all.insert(h4, c4); |
|
| 453 | 766 | let load2 = |h: &ChangeHash| all.get(h).cloned(); |
|
| … | 7 unchanged lines |
modified crates/oxy-core/src/state.rs +336 −66
| 1 | 1 | //! A state: a dependency-closed set of changes, applied to a tree graph and |
|
| 2 | 2 | //! per-node file graphs (spec §8.3), with materialization (spec §9.5, §11). |
|
| 3 | 3 | ||
| 4 | + | use crate::cbor::{self, Value}; |
|
| 4 | 5 | use crate::graph::{AtomKey, DepOracle, FileGraph, Materialized, Vertex}; |
|
| 6 | + | use crate::hash::KeyId; |
|
| 5 | 7 | use crate::hash::{ChangeHash, ObjectRef}; |
|
| 6 | 8 | use crate::ids::NodeId; |
|
| 7 | - | use crate::cbor::{self, Value}; |
|
| 8 | - | use crate::hash::KeyId; |
|
| 9 | - | use crate::objects::{self as o, AtomRef, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, ObjectError, TreeOp}; |
|
| 9 | + | use crate::objects::{ |
|
| 10 | + | self as o, AtomRef, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, ObjectError, TreeOp, |
|
| 11 | + | }; |
|
| 10 | 12 | use crate::tree::{TreeGraph, TreeView}; |
|
| 11 | 13 | use std::cell::RefCell; |
|
| 12 | 14 | use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; |
|
| … | 22 unchanged lines |
||
| 35 | 37 | ||
| 36 | 38 | impl ChangeMeta { |
|
| 37 | 39 | pub fn of(c: &Change) -> ChangeMeta { |
|
| 38 | - | ChangeMeta { change_id: c.change_id, deps: c.deps.clone(), author: c.author, hlc: c.hlc, message: c.message.clone(), kind: c.kind } |
|
| 40 | + | ChangeMeta { |
|
| 41 | + | change_id: c.change_id, |
|
| 42 | + | deps: c.deps.clone(), |
|
| 43 | + | author: c.author, |
|
| 44 | + | hlc: c.hlc, |
|
| 45 | + | message: c.message.clone(), |
|
| 46 | + | kind: c.kind, |
|
| 47 | + | } |
|
| 39 | 48 | } |
|
| 40 | 49 | } |
|
| 41 | 50 | ||
| … | 8 unchanged lines |
||
| 50 | 59 | ||
| 51 | 60 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 52 | 61 | pub enum FileContent { |
|
| 53 | - | Text { bytes: Vec<u8>, materialized: Materialized }, |
|
| 62 | + | Text { |
|
| 63 | + | bytes: Vec<u8>, |
|
| 64 | + | materialized: Materialized, |
|
| 65 | + | }, |
|
| 54 | 66 | /// Live blobs; more than one is a replace conflict (spec §9.3). |
|
| 55 | - | Opaque { blobs: Vec<ObjectRef> }, |
|
| 67 | + | Opaque { |
|
| 68 | + | blobs: Vec<ObjectRef>, |
|
| 69 | + | }, |
|
| 56 | 70 | Dir, |
|
| 57 | 71 | } |
|
| 58 | 72 | ||
| … | 47 unchanged lines |
||
| 106 | 120 | /// Replace self references in a tree op with the concrete change hash. |
|
| 107 | 121 | pub fn resolve_tree_op(op: &TreeOp, me: ChangeHash) -> TreeOp { |
|
| 108 | 122 | match op { |
|
| 109 | - | TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: placement.resolve(me) }, |
|
| 110 | - | TreeOp::Move { node, old, parent, name } => { |
|
| 111 | - | TreeOp::Move { node: *node, old: old.resolve(me), parent: *parent, name: name.clone() } |
|
| 112 | - | } |
|
| 113 | - | TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: old.resolve(me), mode: *mode }, |
|
| 123 | + | TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { |
|
| 124 | + | node: *node, |
|
| 125 | + | placement: placement.resolve(me), |
|
| 126 | + | }, |
|
| 127 | + | TreeOp::Move { |
|
| 128 | + | node, |
|
| 129 | + | old, |
|
| 130 | + | parent, |
|
| 131 | + | name, |
|
| 132 | + | } => TreeOp::Move { |
|
| 133 | + | node: *node, |
|
| 134 | + | old: old.resolve(me), |
|
| 135 | + | parent: *parent, |
|
| 136 | + | name: name.clone(), |
|
| 137 | + | }, |
|
| 138 | + | TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { |
|
| 139 | + | node: *node, |
|
| 140 | + | old: old.resolve(me), |
|
| 141 | + | mode: *mode, |
|
| 142 | + | }, |
|
| 114 | 143 | add @ TreeOp::AddNode { .. } => add.clone(), |
|
| 115 | 144 | } |
|
| 116 | 145 | } |
|
| … | 77 unchanged lines |
||
| 194 | 223 | ); |
|
| 195 | 224 | let mut nodes: Vec<&NodeId> = self.files.keys().collect(); |
|
| 196 | 225 | nodes.sort(); |
|
| 197 | - | let files = Value::Array(nodes.into_iter().map(|n| Value::Array(vec![Value::Bytes(n.0.to_vec()), self.files[n].to_value()])).collect()); |
|
| 226 | + | let files = Value::Array( |
|
| 227 | + | nodes |
|
| 228 | + | .into_iter() |
|
| 229 | + | .map(|n| Value::Array(vec![Value::Bytes(n.0.to_vec()), self.files[n].to_value()])) |
|
| 230 | + | .collect(), |
|
| 231 | + | ); |
|
| 198 | 232 | let mut rnodes: Vec<&NodeId> = self.replaces.keys().collect(); |
|
| 199 | 233 | rnodes.sort(); |
|
| 200 | - | let opt = |o: &Option<ObjectRef>| o.map(|r| Value::Bytes(r.0.to_vec())).unwrap_or(Value::Null); |
|
| 234 | + | let opt = |
|
| 235 | + | |o: &Option<ObjectRef>| o.map(|r| Value::Bytes(r.0.to_vec())).unwrap_or(Value::Null); |
|
| 201 | 236 | let replaces = Value::Array( |
|
| 202 | 237 | rnodes |
|
| 203 | 238 | .into_iter() |
|
| 204 | 239 | .map(|n| { |
|
| 205 | 240 | Value::Array(vec![ |
|
| 206 | 241 | Value::Bytes(n.0.to_vec()), |
|
| 207 | - | Value::Array(self.replaces[n].iter().map(|(c, o, nw)| Value::Array(vec![Value::Bytes(c.0.to_vec()), opt(o), opt(nw)])).collect()), |
|
| 242 | + | Value::Array( |
|
| 243 | + | self.replaces[n] |
|
| 244 | + | .iter() |
|
| 245 | + | .map(|(c, o, nw)| { |
|
| 246 | + | Value::Array(vec![Value::Bytes(c.0.to_vec()), opt(o), opt(nw)]) |
|
| 247 | + | }) |
|
| 248 | + | .collect(), |
|
| 249 | + | ), |
|
| 208 | 250 | ]) |
|
| 209 | 251 | }) |
|
| 210 | 252 | .collect(), |
|
| 211 | 253 | ); |
|
| 212 | - | cbor::encode(&Value::map([(0, Value::Uint(o::FORMAT_VERSION)), (1, changes), (2, self.tree.to_value()), (3, files), (4, replaces)])) |
|
| 254 | + | cbor::encode(&Value::map([ |
|
| 255 | + | (0, Value::Uint(o::FORMAT_VERSION)), |
|
| 256 | + | (1, changes), |
|
| 257 | + | (2, self.tree.to_value()), |
|
| 258 | + | (3, files), |
|
| 259 | + | (4, replaces), |
|
| 260 | + | ])) |
|
| 213 | 261 | } |
|
| 214 | 262 | ||
| 215 | 263 | pub fn from_checkpoint_payload(bytes: &[u8]) -> Result<State, ObjectError> { |
|
| … | 8 unchanged lines |
||
| 224 | 272 | } |
|
| 225 | 273 | let hash = o::oref(&e[0], "hash")?; |
|
| 226 | 274 | let meta = ChangeMeta { |
|
| 227 | - | change_id: crate::ids::ChangeId::from_slice(o::bytes(&e[1], "change id")?).ok_or_else(|| ObjectError::Schema("change id: expected 16 bytes".into()))?, |
|
| 228 | - | deps: o::array(&e[2], "deps")?.iter().map(|d| o::oref(d, "dep")).collect::<Result<Vec<_>, _>>()?, |
|
| 275 | + | change_id: crate::ids::ChangeId::from_slice(o::bytes(&e[1], "change id")?) |
|
| 276 | + | .ok_or_else(|| ObjectError::Schema("change id: expected 16 bytes".into()))?, |
|
| 277 | + | deps: o::array(&e[2], "deps")? |
|
| 278 | + | .iter() |
|
| 279 | + | .map(|d| o::oref(d, "dep")) |
|
| 280 | + | .collect::<Result<Vec<_>, _>>()?, |
|
| 229 | 281 | message: o::text(&e[3], "message")?.to_owned(), |
|
| 230 | - | hlc: Hlc { wall_ms: o::uint(&e[4], "wall")?, logical: o::u32_(&e[5], "logical")? }, |
|
| 282 | + | hlc: Hlc { |
|
| 283 | + | wall_ms: o::uint(&e[4], "wall")?, |
|
| 284 | + | logical: o::u32_(&e[5], "logical")?, |
|
| 285 | + | }, |
|
| 231 | 286 | kind: ChangeKind::from_tag(o::uint(&e[6], "kind")?)?, |
|
| 232 | 287 | author: o::oref(&e[7], "author")?, |
|
| 233 | 288 | }; |
|
| … | 5 unchanged lines |
||
| 239 | 294 | if e.len() != 2 { |
|
| 240 | 295 | return Err(ObjectError::Schema("file: expected [node, graph]".into())); |
|
| 241 | 296 | } |
|
| 242 | - | let node = NodeId::from_slice(o::bytes(&e[0], "node")?).ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?; |
|
| 297 | + | let node = NodeId::from_slice(o::bytes(&e[0], "node")?) |
|
| 298 | + | .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?; |
|
| 243 | 299 | st.files.insert(node, FileGraph::from_value(&e[1])?); |
|
| 244 | 300 | } |
|
| 245 | - | let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> { if v.is_null() { Ok(None) } else { o::oref(v, "blob").map(Some) } }; |
|
| 301 | + | let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> { |
|
| 302 | + | if v.is_null() { |
|
| 303 | + | Ok(None) |
|
| 304 | + | } else { |
|
| 305 | + | o::oref(v, "blob").map(Some) |
|
| 306 | + | } |
|
| 307 | + | }; |
|
| 246 | 308 | for r in o::array(o::field(m, 4, "replaces")?, "replaces")? { |
|
| 247 | 309 | let e = o::array(r, "replace")?; |
|
| 248 | 310 | if e.len() != 2 { |
|
| 249 | - | return Err(ObjectError::Schema("replace: expected [node, records]".into())); |
|
| 311 | + | return Err(ObjectError::Schema( |
|
| 312 | + | "replace: expected [node, records]".into(), |
|
| 313 | + | )); |
|
| 250 | 314 | } |
|
| 251 | - | let node = NodeId::from_slice(o::bytes(&e[0], "node")?).ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?; |
|
| 315 | + | let node = NodeId::from_slice(o::bytes(&e[0], "node")?) |
|
| 316 | + | .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?; |
|
| 252 | 317 | let mut recs = Vec::new(); |
|
| 253 | 318 | for rec in o::array(&e[1], "records")? { |
|
| 254 | 319 | let x = o::array(rec, "record")?; |
|
| … | 25 unchanged lines |
||
| 280 | 345 | let mut counters: HashMap<NodeId, u32> = HashMap::new(); |
|
| 281 | 346 | for e in &c.edits { |
|
| 282 | 347 | match e { |
|
| 283 | - | Edit::Insert { node, after, before, lines } => { |
|
| 348 | + | Edit::Insert { |
|
| 349 | + | node, |
|
| 350 | + | after, |
|
| 351 | + | before, |
|
| 352 | + | lines, |
|
| 353 | + | } => { |
|
| 284 | 354 | let idx = counters.entry(*node).or_default(); |
|
| 285 | 355 | let first = *idx; |
|
| 286 | 356 | *idx += lines.len() as u32; |
|
| 287 | - | self.files.entry(*node).or_default().insert(h, first, vertex(after.resolve(h)), vertex(before.resolve(h)), lines); |
|
| 357 | + | self.files.entry(*node).or_default().insert( |
|
| 358 | + | h, |
|
| 359 | + | first, |
|
| 360 | + | vertex(after.resolve(h)), |
|
| 361 | + | vertex(before.resolve(h)), |
|
| 362 | + | lines, |
|
| 363 | + | ); |
|
| 288 | 364 | } |
|
| 289 | 365 | Edit::Delete { node, atoms } => { |
|
| 290 | 366 | let g = self.files.entry(*node).or_default(); |
|
| … | 2 unchanged lines |
||
| 293 | 369 | } |
|
| 294 | 370 | } |
|
| 295 | 371 | Edit::Replace { node, old, new } => { |
|
| 296 | - | self.replaces.entry(*node).or_default().push((h, *old, *new)); |
|
| 372 | + | self.replaces |
|
| 373 | + | .entry(*node) |
|
| 374 | + | .or_default() |
|
| 375 | + | .push((h, *old, *new)); |
|
| 297 | 376 | } |
|
| 298 | 377 | } |
|
| 299 | 378 | } |
|
| … | 5 unchanged lines |
||
| 305 | 384 | return c.clone(); |
|
| 306 | 385 | } |
|
| 307 | 386 | let mut out = HashSet::new(); |
|
| 308 | - | let mut stack: Vec<ChangeHash> = self.changes.get(h).map(|c| c.deps.clone()).unwrap_or_default(); |
|
| 387 | + | let mut stack: Vec<ChangeHash> = self |
|
| 388 | + | .changes |
|
| 389 | + | .get(h) |
|
| 390 | + | .map(|c| c.deps.clone()) |
|
| 391 | + | .unwrap_or_default(); |
|
| 309 | 392 | while let Some(d) = stack.pop() { |
|
| 310 | 393 | if out.insert(d) { |
|
| 311 | 394 | if let Some(c) = self.changes.get(&d) { |
|
| … | 8 unchanged lines |
||
| 320 | 403 | ||
| 321 | 404 | /// Changes that no other change depends on directly, sorted. |
|
| 322 | 405 | pub fn heads(&self) -> Vec<ChangeHash> { |
|
| 323 | - | let depended: HashSet<&ChangeHash> = self.changes.values().flat_map(|c| c.deps.iter()).collect(); |
|
| 324 | - | let mut heads: Vec<ChangeHash> = self.changes.keys().filter(|h| !depended.contains(h)).copied().collect(); |
|
| 406 | + | let depended: HashSet<&ChangeHash> = |
|
| 407 | + | self.changes.values().flat_map(|c| c.deps.iter()).collect(); |
|
| 408 | + | let mut heads: Vec<ChangeHash> = self |
|
| 409 | + | .changes |
|
| 410 | + | .keys() |
|
| 411 | + | .filter(|h| !depended.contains(h)) |
|
| 412 | + | .copied() |
|
| 413 | + | .collect(); |
|
| 325 | 414 | heads.sort(); |
|
| 326 | 415 | heads |
|
| 327 | 416 | } |
|
| … | 1 unchanged lines |
||
| 329 | 418 | /// Maximal elements among `hashes` (those not in the closure of another). |
|
| 330 | 419 | pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> { |
|
| 331 | 420 | let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect(); |
|
| 332 | - | set.iter().filter(|h| !set.iter().any(|o| o != *h && self.closure(o).contains(h))).copied().collect() |
|
| 421 | + | set.iter() |
|
| 422 | + | .filter(|h| !set.iter().any(|o| o != *h && self.closure(o).contains(h))) |
|
| 423 | + | .copied() |
|
| 424 | + | .collect() |
|
| 333 | 425 | } |
|
| 334 | 426 | ||
| 335 | 427 | pub fn materialize_file(&self, node: &NodeId) -> Materialized { |
|
| 336 | - | self.files.get(node).map(|g| g.materialize(self)).unwrap_or_default() |
|
| 428 | + | self.files |
|
| 429 | + | .get(node) |
|
| 430 | + | .map(|g| g.materialize(self)) |
|
| 431 | + | .unwrap_or_default() |
|
| 337 | 432 | } |
|
| 338 | 433 | ||
| 339 | 434 | /// Live lines of a text node in materialized order. |
|
| 340 | 435 | pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> { |
|
| 341 | - | let Some(g) = self.files.get(node) else { return vec![] }; |
|
| 436 | + | let Some(g) = self.files.get(node) else { |
|
| 437 | + | return vec![]; |
|
| 438 | + | }; |
|
| 342 | 439 | let m = g.materialize(self); |
|
| 343 | - | m.live_atoms().into_iter().map(|a| (a, g.content(&a).unwrap_or_default().to_vec())).collect() |
|
| 440 | + | m.live_atoms() |
|
| 441 | + | .into_iter() |
|
| 442 | + | .map(|a| (a, g.content(&a).unwrap_or_default().to_vec())) |
|
| 443 | + | .collect() |
|
| 344 | 444 | } |
|
| 345 | 445 | ||
| 346 | 446 | /// Live blobs of an opaque node: every `new` that no later `Replace` retired. |
|
| 347 | 447 | pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> { |
|
| 348 | - | let Some(rs) = self.replaces.get(node) else { return vec![] }; |
|
| 448 | + | let Some(rs) = self.replaces.get(node) else { |
|
| 449 | + | return vec![]; |
|
| 450 | + | }; |
|
| 349 | 451 | let olds: HashSet<ObjectRef> = rs.iter().filter_map(|(_, o, _)| *o).collect(); |
|
| 350 | - | let mut live: Vec<ObjectRef> = rs.iter().filter_map(|(_, _, n)| *n).filter(|n| !olds.contains(n)).collect(); |
|
| 452 | + | let mut live: Vec<ObjectRef> = rs |
|
| 453 | + | .iter() |
|
| 454 | + | .filter_map(|(_, _, n)| *n) |
|
| 455 | + | .filter(|n| !olds.contains(n)) |
|
| 456 | + | .collect(); |
|
| 351 | 457 | live.sort(); |
|
| 352 | 458 | live.dedup(); |
|
| 353 | 459 | live |
|
| … | 2 unchanged lines |
||
| 356 | 462 | /// Short label for conflict markers: `changeid-prefix "message"`. |
|
| 357 | 463 | pub fn label(&self, h: &ChangeHash) -> String { |
|
| 358 | 464 | match self.changes.get(h) { |
|
| 359 | - | Some(c) => format!("{} \"{}\"", c.change_id.short(8), c.message.lines().next().unwrap_or("")), |
|
| 465 | + | Some(c) => format!( |
|
| 466 | + | "{} \"{}\"", |
|
| 467 | + | c.change_id.short(8), |
|
| 468 | + | c.message.lines().next().unwrap_or("") |
|
| 469 | + | ), |
|
| 360 | 470 | None => format!("#{}", h.short(12)), |
|
| 361 | 471 | } |
|
| 362 | 472 | } |
|
| … | 16 unchanged lines |
||
| 379 | 489 | ||
| 380 | 490 | pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions { |
|
| 381 | 491 | let mut out = ConflictRegions::default(); |
|
| 382 | - | let Some(g) = self.files.get(node) else { return out }; |
|
| 492 | + | let Some(g) = self.files.get(node) else { |
|
| 493 | + | return out; |
|
| 494 | + | }; |
|
| 383 | 495 | let m = g.materialize(self); |
|
| 384 | 496 | let mut contributors = BTreeSet::new(); |
|
| 385 | 497 | for seg in &m.segments { |
|
| 386 | 498 | match seg { |
|
| 387 | 499 | crate::graph::Segment::Lines(atoms) => { |
|
| 388 | 500 | for a in atoms { |
|
| 389 | - | out.plain.push((*a, g.content(a).unwrap_or_default().to_vec())); |
|
| 501 | + | out.plain |
|
| 502 | + | .push((*a, g.content(a).unwrap_or_default().to_vec())); |
|
| 390 | 503 | } |
|
| 391 | 504 | } |
|
| 392 | 505 | crate::graph::Segment::Conflict(sides) => { |
|
| … | 21 unchanged lines |
||
| 414 | 527 | } |
|
| 415 | 528 | ||
| 416 | 529 | pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) { |
|
| 417 | - | let Some(g) = self.files.get(node) else { return (Vec::new(), Materialized::default()) }; |
|
| 530 | + | let Some(g) = self.files.get(node) else { |
|
| 531 | + | return (Vec::new(), Materialized::default()); |
|
| 532 | + | }; |
|
| 418 | 533 | let m = g.materialize(self); |
|
| 419 | 534 | let bytes = g.render(&m, &|h| self.label(h)); |
|
| 420 | 535 | (bytes, m) |
|
| … | 5 unchanged lines |
||
| 426 | 541 | for (path, e) in &tree.entries { |
|
| 427 | 542 | let content = match e.kind { |
|
| 428 | 543 | NodeKind::Dir => FileContent::Dir, |
|
| 429 | - | NodeKind::Symlink | NodeKind::Subrepo => FileContent::Opaque { blobs: self.live_blobs(&e.node) }, |
|
| 430 | - | NodeKind::File if e.mode.opaque => FileContent::Opaque { blobs: self.live_blobs(&e.node) }, |
|
| 544 | + | NodeKind::Symlink | NodeKind::Subrepo => FileContent::Opaque { |
|
| 545 | + | blobs: self.live_blobs(&e.node), |
|
| 546 | + | }, |
|
| 547 | + | NodeKind::File if e.mode.opaque => FileContent::Opaque { |
|
| 548 | + | blobs: self.live_blobs(&e.node), |
|
| 549 | + | }, |
|
| 431 | 550 | NodeKind::File => { |
|
| 432 | 551 | let (bytes, materialized) = self.render_file(&e.node); |
|
| 433 | - | FileContent::Text { bytes, materialized } |
|
| 552 | + | FileContent::Text { |
|
| 553 | + | bytes, |
|
| 554 | + | materialized, |
|
| 555 | + | } |
|
| 434 | 556 | } |
|
| 435 | 557 | }; |
|
| 436 | - | files.insert(path.clone(), FileState { node: e.node, kind: e.kind, mode: e.mode, content }); |
|
| 558 | + | files.insert( |
|
| 559 | + | path.clone(), |
|
| 560 | + | FileState { |
|
| 561 | + | node: e.node, |
|
| 562 | + | kind: e.kind, |
|
| 563 | + | mode: e.mode, |
|
| 564 | + | content, |
|
| 565 | + | }, |
|
| 566 | + | ); |
|
| 437 | 567 | } |
|
| 438 | 568 | Snapshot { tree, files } |
|
| 439 | 569 | } |
|
| … | 16 unchanged lines |
||
| 456 | 586 | change_id: ChangeId::random(), |
|
| 457 | 587 | deps, |
|
| 458 | 588 | author: ObjectRef::ZERO, |
|
| 459 | - | hlc: Hlc { wall_ms: 1, logical: 0 }, |
|
| 589 | + | hlc: Hlc { |
|
| 590 | + | wall_ms: 1, |
|
| 591 | + | logical: 0, |
|
| 592 | + | }, |
|
| 460 | 593 | message: "m".into(), |
|
| 461 | 594 | tree_ops, |
|
| 462 | 595 | edits, |
|
| … | 9 unchanged lines |
||
| 472 | 605 | let f = NodeId::random(); |
|
| 473 | 606 | let c1 = change( |
|
| 474 | 607 | vec![], |
|
| 475 | - | vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "a.txt".into() }], |
|
| 476 | - | vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"one\n".to_vec(), b"two\n".to_vec()] }], |
|
| 608 | + | vec![TreeOp::AddNode { |
|
| 609 | + | node: f, |
|
| 610 | + | kind: NodeKind::File, |
|
| 611 | + | mode: Mode::TEXT, |
|
| 612 | + | parent: NodeId::ROOT, |
|
| 613 | + | name: "a.txt".into(), |
|
| 614 | + | }], |
|
| 615 | + | vec![Edit::Insert { |
|
| 616 | + | node: f, |
|
| 617 | + | after: AtomRef::Start, |
|
| 618 | + | before: AtomRef::End, |
|
| 619 | + | lines: vec![b"one\n".to_vec(), b"two\n".to_vec()], |
|
| 620 | + | }], |
|
| 477 | 621 | ); |
|
| 478 | 622 | let h1 = st.add(c1).unwrap(); |
|
| 479 | 623 | // Depends on h1: append a line and delete "one". |
|
| … | 1 unchanged lines |
||
| 481 | 625 | vec![h1], |
|
| 482 | 626 | vec![], |
|
| 483 | 627 | vec![ |
|
| 484 | - | Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] }, |
|
| 485 | - | Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"three\n".to_vec()] }, |
|
| 628 | + | Edit::Delete { |
|
| 629 | + | node: f, |
|
| 630 | + | atoms: vec![AtomRange { |
|
| 631 | + | change: h1, |
|
| 632 | + | from: 0, |
|
| 633 | + | to: 1, |
|
| 634 | + | }], |
|
| 635 | + | }, |
|
| 636 | + | Edit::Insert { |
|
| 637 | + | node: f, |
|
| 638 | + | after: AtomRef::Atom { |
|
| 639 | + | change: h1, |
|
| 640 | + | index: 1, |
|
| 641 | + | }, |
|
| 642 | + | before: AtomRef::End, |
|
| 643 | + | lines: vec![b"three\n".to_vec()], |
|
| 644 | + | }, |
|
| 486 | 645 | ], |
|
| 487 | 646 | ); |
|
| 488 | 647 | let h2 = st.add(c2).unwrap(); |
|
| … | 18 unchanged lines |
||
| 507 | 666 | fn self_references_and_multi_insert_indexing() { |
|
| 508 | 667 | let mut st = State::new(); |
|
| 509 | 668 | let f = NodeId::random(); |
|
| 510 | - | let me = OpRef { change: ObjectRef::ZERO, index: 0 }; |
|
| 669 | + | let me = OpRef { |
|
| 670 | + | change: ObjectRef::ZERO, |
|
| 671 | + | index: 0, |
|
| 672 | + | }; |
|
| 511 | 673 | let c1 = change( |
|
| 512 | 674 | vec![], |
|
| 513 | 675 | vec![ |
|
| 514 | - | TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "x".into() }, |
|
| 515 | - | TreeOp::SetMode { node: f, old: me, mode: Mode { exec: true, opaque: false } }, |
|
| 676 | + | TreeOp::AddNode { |
|
| 677 | + | node: f, |
|
| 678 | + | kind: NodeKind::File, |
|
| 679 | + | mode: Mode::TEXT, |
|
| 680 | + | parent: NodeId::ROOT, |
|
| 681 | + | name: "x".into(), |
|
| 682 | + | }, |
|
| 683 | + | TreeOp::SetMode { |
|
| 684 | + | node: f, |
|
| 685 | + | old: me, |
|
| 686 | + | mode: Mode { |
|
| 687 | + | exec: true, |
|
| 688 | + | opaque: false, |
|
| 689 | + | }, |
|
| 690 | + | }, |
|
| 516 | 691 | ], |
|
| 517 | 692 | vec![ |
|
| 518 | - | Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"c\n".to_vec()] }, |
|
| 693 | + | Edit::Insert { |
|
| 694 | + | node: f, |
|
| 695 | + | after: AtomRef::Start, |
|
| 696 | + | before: AtomRef::End, |
|
| 697 | + | lines: vec![b"a\n".to_vec(), b"c\n".to_vec()], |
|
| 698 | + | }, |
|
| 519 | 699 | // Insert between the two lines just inserted, using self references. |
|
| 520 | 700 | Edit::Insert { |
|
| 521 | 701 | node: f, |
|
| 522 | - | after: AtomRef::Atom { change: ObjectRef::ZERO, index: 0 }, |
|
| 523 | - | before: AtomRef::Atom { change: ObjectRef::ZERO, index: 1 }, |
|
| 702 | + | after: AtomRef::Atom { |
|
| 703 | + | change: ObjectRef::ZERO, |
|
| 704 | + | index: 0, |
|
| 705 | + | }, |
|
| 706 | + | before: AtomRef::Atom { |
|
| 707 | + | change: ObjectRef::ZERO, |
|
| 708 | + | index: 1, |
|
| 709 | + | }, |
|
| 524 | 710 | lines: vec![b"b\n".to_vec()], |
|
| 525 | 711 | }, |
|
| 526 | 712 | ], |
|
| … | 1 unchanged lines |
||
| 528 | 714 | st.add(c1).unwrap(); |
|
| 529 | 715 | let snap = st.materialize(); |
|
| 530 | 716 | let x = &snap.files["x"]; |
|
| 531 | - | assert_eq!(x.mode, Mode { exec: true, opaque: false }); |
|
| 717 | + | assert_eq!( |
|
| 718 | + | x.mode, |
|
| 719 | + | Mode { |
|
| 720 | + | exec: true, |
|
| 721 | + | opaque: false |
|
| 722 | + | } |
|
| 723 | + | ); |
|
| 532 | 724 | assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n")); |
|
| 533 | 725 | } |
|
| 534 | 726 | ||
| … | 4 unchanged lines |
||
| 539 | 731 | let b1 = ObjectRef([1; 32]); |
|
| 540 | 732 | let b2 = ObjectRef([2; 32]); |
|
| 541 | 733 | let b3 = ObjectRef([3; 32]); |
|
| 542 | - | let h1 = st.add(change( |
|
| 734 | + | let h1 = st |
|
| 735 | + | .add(change( |
|
| 736 | + | vec![], |
|
| 737 | + | vec![TreeOp::AddNode { |
|
| 738 | + | node: f, |
|
| 739 | + | kind: NodeKind::File, |
|
| 740 | + | mode: Mode::OPAQUE, |
|
| 741 | + | parent: NodeId::ROOT, |
|
| 742 | + | name: "img".into(), |
|
| 743 | + | }], |
|
| 744 | + | vec![Edit::Replace { |
|
| 745 | + | node: f, |
|
| 746 | + | old: None, |
|
| 747 | + | new: Some(b1), |
|
| 748 | + | }], |
|
| 749 | + | )) |
|
| 750 | + | .unwrap(); |
|
| 751 | + | assert_eq!(st.live_blobs(&f), vec![b1]); |
|
| 752 | + | st.add(change( |
|
| 753 | + | vec![h1], |
|
| 754 | + | vec![], |
|
| 755 | + | vec![Edit::Replace { |
|
| 756 | + | node: f, |
|
| 757 | + | old: Some(b1), |
|
| 758 | + | new: Some(b2), |
|
| 759 | + | }], |
|
| 760 | + | )) |
|
| 761 | + | .unwrap(); |
|
| 762 | + | st.add(change( |
|
| 763 | + | vec![h1], |
|
| 543 | 764 | vec![], |
|
| 544 | - | vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::OPAQUE, parent: NodeId::ROOT, name: "img".into() }], |
|
| 545 | - | vec![Edit::Replace { node: f, old: None, new: Some(b1) }], |
|
| 765 | + | vec![Edit::Replace { |
|
| 766 | + | node: f, |
|
| 767 | + | old: Some(b1), |
|
| 768 | + | new: Some(b3), |
|
| 769 | + | }], |
|
| 546 | 770 | )) |
|
| 547 | 771 | .unwrap(); |
|
| 548 | - | assert_eq!(st.live_blobs(&f), vec![b1]); |
|
| 549 | - | st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b2) }])).unwrap(); |
|
| 550 | - | st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b3) }])).unwrap(); |
|
| 551 | 772 | let snap = st.materialize(); |
|
| 552 | 773 | assert!(snap.is_conflicted()); |
|
| 553 | - | assert!(matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3])); |
|
| 774 | + | assert!( |
|
| 775 | + | matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3]) |
|
| 776 | + | ); |
|
| 554 | 777 | } |
|
| 555 | 778 | ||
| 556 | 779 | #[test] |
|
| 557 | 780 | fn checkpoint_payload_round_trip_and_replay() { |
|
| 558 | 781 | let mut st = State::new(); |
|
| 559 | 782 | let f = NodeId::random(); |
|
| 560 | - | let h1 = st.add(change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "a".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"1\n".to_vec(), b"2\n".to_vec()] }])).unwrap(); |
|
| 561 | - | let h2 = st.add(change(vec![h1], vec![], vec![Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] }])).unwrap(); |
|
| 783 | + | let h1 = st |
|
| 784 | + | .add(change( |
|
| 785 | + | vec![], |
|
| 786 | + | vec![TreeOp::AddNode { |
|
| 787 | + | node: f, |
|
| 788 | + | kind: NodeKind::File, |
|
| 789 | + | mode: Mode::TEXT, |
|
| 790 | + | parent: NodeId::ROOT, |
|
| 791 | + | name: "a".into(), |
|
| 792 | + | }], |
|
| 793 | + | vec![Edit::Insert { |
|
| 794 | + | node: f, |
|
| 795 | + | after: AtomRef::Start, |
|
| 796 | + | before: AtomRef::End, |
|
| 797 | + | lines: vec![b"1\n".to_vec(), b"2\n".to_vec()], |
|
| 798 | + | }], |
|
| 799 | + | )) |
|
| 800 | + | .unwrap(); |
|
| 801 | + | let h2 = st |
|
| 802 | + | .add(change( |
|
| 803 | + | vec![h1], |
|
| 804 | + | vec![], |
|
| 805 | + | vec![Edit::Delete { |
|
| 806 | + | node: f, |
|
| 807 | + | atoms: vec![AtomRange { |
|
| 808 | + | change: h1, |
|
| 809 | + | from: 0, |
|
| 810 | + | to: 1, |
|
| 811 | + | }], |
|
| 812 | + | }], |
|
| 813 | + | )) |
|
| 814 | + | .unwrap(); |
|
| 562 | 815 | let payload = st.checkpoint_payload(); |
|
| 563 | 816 | let back = State::from_checkpoint_payload(&payload).unwrap(); |
|
| 564 | 817 | assert_eq!(back.len(), 2); |
|
| … | 1 unchanged lines |
||
| 566 | 819 | assert_eq!(back.get(&h1).unwrap().message, "m"); |
|
| 567 | 820 | assert_eq!(back.checkpoint_payload(), payload, "deterministic"); |
|
| 568 | 821 | // Replaying a further change onto the restored state equals a full build. |
|
| 569 | - | let c3 = change(vec![h2], vec![], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"3\n".to_vec()] }]); |
|
| 822 | + | let c3 = change( |
|
| 823 | + | vec![h2], |
|
| 824 | + | vec![], |
|
| 825 | + | vec![Edit::Insert { |
|
| 826 | + | node: f, |
|
| 827 | + | after: AtomRef::Atom { |
|
| 828 | + | change: h1, |
|
| 829 | + | index: 1, |
|
| 830 | + | }, |
|
| 831 | + | before: AtomRef::End, |
|
| 832 | + | lines: vec![b"3\n".to_vec()], |
|
| 833 | + | }], |
|
| 834 | + | ); |
|
| 570 | 835 | let mut full = st.clone(); |
|
| 571 | 836 | full.add(c3.clone()).unwrap(); |
|
| 572 | 837 | let mut restored = back; |
|
| 573 | 838 | restored.add(c3).unwrap(); |
|
| 574 | 839 | let (a, b) = (full.materialize(), restored.materialize()); |
|
| 575 | - | assert_eq!(a.files.keys().collect::<Vec<_>>(), b.files.keys().collect::<Vec<_>>()); |
|
| 840 | + | assert_eq!( |
|
| 841 | + | a.files.keys().collect::<Vec<_>>(), |
|
| 842 | + | b.files.keys().collect::<Vec<_>>() |
|
| 843 | + | ); |
|
| 576 | 844 | assert_eq!(a.files["a"].content, b.files["a"].content); |
|
| 577 | - | assert!(matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n")); |
|
| 845 | + | assert!( |
|
| 846 | + | matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n") |
|
| 847 | + | ); |
|
| 578 | 848 | assert!(State::from_checkpoint_payload(b"nonsense").is_err()); |
|
| 579 | 849 | } |
|
| 580 | 850 | ||
| … | 14 unchanged lines |
modified crates/oxy-core/src/store.rs +287 −64
| … | 46 unchanged lines |
||
| 47 | 47 | impl PackIndex { |
|
| 48 | 48 | fn load(idx_path: &Path, pack_path: &Path) -> Result<PackIndex> { |
|
| 49 | 49 | let data = fs::read(idx_path)?; |
|
| 50 | - | let bad = |m: &str| StoreError::Corrupt(ObjectRef::ZERO, format!("{}: {m}", idx_path.display())); |
|
| 50 | + | let bad = |
|
| 51 | + | |m: &str| StoreError::Corrupt(ObjectRef::ZERO, format!("{}: {m}", idx_path.display())); |
|
| 51 | 52 | if data.len() < 12 || &data[..8] != IDX_MAGIC { |
|
| 52 | 53 | return Err(bad("bad index header")); |
|
| 53 | 54 | } |
|
| … | 17 unchanged lines |
||
| 71 | 72 | offset: u64::from_le_bytes(e[32..40].try_into().unwrap()), |
|
| 72 | 73 | stored_len: u32::from_le_bytes(e[40..44].try_into().unwrap()), |
|
| 73 | 74 | raw_len: u32::from_le_bytes(e[44..48].try_into().unwrap()), |
|
| 74 | - | ty: ObjectType::ALL.get(e[48] as usize).copied().ok_or_else(|| bad("unknown object type"))?, |
|
| 75 | + | ty: ObjectType::ALL |
|
| 76 | + | .get(e[48] as usize) |
|
| 77 | + | .copied() |
|
| 78 | + | .ok_or_else(|| bad("unknown object type"))?, |
|
| 75 | 79 | flags: e[49], |
|
| 76 | 80 | }; |
|
| 77 | - | if entry.stored_len > MAX_OBJECT_LEN || entry.raw_len > MAX_OBJECT_LEN || entry.offset.saturating_add(entry.stored_len as u64) > pack_len { |
|
| 81 | + | if entry.stored_len > MAX_OBJECT_LEN |
|
| 82 | + | || entry.raw_len > MAX_OBJECT_LEN |
|
| 83 | + | || entry.offset.saturating_add(entry.stored_len as u64) > pack_len |
|
| 84 | + | { |
|
| 78 | 85 | return Err(bad("index entry points outside the pack")); |
|
| 79 | 86 | } |
|
| 80 | 87 | entries.push(entry); |
|
| 81 | 88 | } |
|
| 82 | - | Ok(PackIndex { path: pack_path.to_path_buf(), refs, entries }) |
|
| 89 | + | Ok(PackIndex { |
|
| 90 | + | path: pack_path.to_path_buf(), |
|
| 91 | + | refs, |
|
| 92 | + | entries, |
|
| 93 | + | }) |
|
| 83 | 94 | } |
|
| 84 | 95 | ||
| 85 | 96 | fn find(&self, r: &ObjectRef) -> Option<PackEntry> { |
|
| … | 6 unchanged lines |
||
| 92 | 103 | let mut stored = vec![0u8; e.stored_len as usize]; |
|
| 93 | 104 | f.read_exact(&mut stored)?; |
|
| 94 | 105 | let body = if e.flags & FLAG_ZSTD != 0 { |
|
| 95 | - | zstd::bulk::decompress(&stored, e.raw_len as usize).map_err(|err| StoreError::Corrupt(*r, format!("zstd: {err}")))? |
|
| 106 | + | zstd::bulk::decompress(&stored, e.raw_len as usize) |
|
| 107 | + | .map_err(|err| StoreError::Corrupt(*r, format!("zstd: {err}")))? |
|
| 96 | 108 | } else { |
|
| 97 | 109 | stored |
|
| 98 | 110 | }; |
|
| 99 | 111 | if body.len() != e.raw_len as usize || hash_object(e.ty, &body) != *r { |
|
| 100 | - | return Err(StoreError::Corrupt(*r, "packed object does not match its reference".into())); |
|
| 112 | + | return Err(StoreError::Corrupt( |
|
| 113 | + | *r, |
|
| 114 | + | "packed object does not match its reference".into(), |
|
| 115 | + | )); |
|
| 101 | 116 | } |
|
| 102 | 117 | Ok((e.ty, body)) |
|
| 103 | 118 | } |
|
| … | 104 unchanged lines |
||
| 208 | 223 | pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> { |
|
| 209 | 224 | let dir = dir.into(); |
|
| 210 | 225 | fs::create_dir_all(dir.join("loose"))?; |
|
| 211 | - | let store = Store { dir, packs: RefCell::new(Vec::new()), pending: RefCell::new(None), batch_depth: std::cell::Cell::new(0) }; |
|
| 212 | - | store.reload_packs().map_err(|e| io::Error::other(e.to_string()))?; |
|
| 226 | + | let store = Store { |
|
| 227 | + | dir, |
|
| 228 | + | packs: RefCell::new(Vec::new()), |
|
| 229 | + | pending: RefCell::new(None), |
|
| 230 | + | batch_depth: std::cell::Cell::new(0), |
|
| 231 | + | }; |
|
| 232 | + | store |
|
| 233 | + | .reload_packs() |
|
| 234 | + | .map_err(|e| io::Error::other(e.to_string()))?; |
|
| 213 | 235 | Ok(store) |
|
| 214 | 236 | } |
|
| 215 | 237 | ||
| … | 1 unchanged lines |
||
| 217 | 239 | let mut packs = Vec::new(); |
|
| 218 | 240 | let mut names: Vec<PathBuf> = fs::read_dir(&self.dir)? |
|
| 219 | 241 | .filter_map(|e| e.ok().map(|e| e.path())) |
|
| 220 | - | .filter(|p| p.extension().is_some_and(|x| x == "idx") && p.file_name().is_some_and(|n| n.to_string_lossy().starts_with("pack-"))) |
|
| 242 | + | .filter(|p| { |
|
| 243 | + | p.extension().is_some_and(|x| x == "idx") |
|
| 244 | + | && p.file_name() |
|
| 245 | + | .is_some_and(|n| n.to_string_lossy().starts_with("pack-")) |
|
| 246 | + | }) |
|
| 221 | 247 | .collect(); |
|
| 222 | 248 | names.sort(); |
|
| 223 | 249 | for idx in names { |
|
| … | 11 unchanged lines |
||
| 235 | 261 | } |
|
| 236 | 262 | ||
| 237 | 263 | fn find_packed(&self, r: &ObjectRef) -> Option<(usize, PackEntry)> { |
|
| 238 | - | self.packs.borrow().iter().enumerate().find_map(|(i, p)| p.find(r).map(|e| (i, e))) |
|
| 264 | + | self.packs |
|
| 265 | + | .borrow() |
|
| 266 | + | .iter() |
|
| 267 | + | .enumerate() |
|
| 268 | + | .find_map(|(i, p)| p.find(r).map(|e| (i, e))) |
|
| 239 | 269 | } |
|
| 240 | 270 | ||
| 241 | 271 | /// Loose objects on disk, with their paths. |
|
| … | 26 unchanged lines |
||
| 268 | 298 | /// deleted only after the pack and its index are durable. |
|
| 269 | 299 | pub fn pack_loose(&self) -> Result<PackReport> { |
|
| 270 | 300 | let loose = self.loose_objects()?; |
|
| 271 | - | self.write_pack(&loose.iter().map(|(r, _)| *r).collect::<Vec<_>>(), &loose.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(), &[]) |
|
| 301 | + | self.write_pack( |
|
| 302 | + | &loose.iter().map(|(r, _)| *r).collect::<Vec<_>>(), |
|
| 303 | + | &loose.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(), |
|
| 304 | + | &[], |
|
| 305 | + | ) |
|
| 272 | 306 | } |
|
| 273 | 307 | ||
| 274 | 308 | /// Fold every pack into one when there are more than `MAX_PACKS` |
|
| … | 4 unchanged lines |
||
| 279 | 313 | if packs.len() <= MAX_PACKS { |
|
| 280 | 314 | return Ok(0); |
|
| 281 | 315 | } |
|
| 282 | - | (packs.iter().flat_map(|p| p.refs.iter().copied()).collect(), packs.iter().map(|p| p.path.clone()).collect()) |
|
| 316 | + | ( |
|
| 317 | + | packs.iter().flat_map(|p| p.refs.iter().copied()).collect(), |
|
| 318 | + | packs.iter().map(|p| p.path.clone()).collect(), |
|
| 319 | + | ) |
|
| 283 | 320 | }; |
|
| 284 | 321 | let n = paths.len(); |
|
| 285 | 322 | self.write_pack(&refs, &[], &paths)?; |
|
| … | 5 unchanged lines |
||
| 291 | 328 | /// have recorded its references yet. Packs holding unreachable objects |
|
| 292 | 329 | /// are rewritten without them. Signature sidecars and seal-index entries |
|
| 293 | 330 | /// of deleted objects go too. |
|
| 294 | - | pub fn prune(&self, keep: &std::collections::HashSet<ObjectRef>, grace: std::time::Duration) -> Result<PruneReport> { |
|
| 331 | + | pub fn prune( |
|
| 332 | + | &self, |
|
| 333 | + | keep: &std::collections::HashSet<ObjectRef>, |
|
| 334 | + | grace: std::time::Duration, |
|
| 335 | + | ) -> Result<PruneReport> { |
|
| 295 | 336 | let now = std::time::SystemTime::now(); |
|
| 296 | - | let old_enough = |m: &fs::Metadata| m.modified().map(|t| now.duration_since(t).unwrap_or_default() >= grace).unwrap_or(true); |
|
| 337 | + | let old_enough = |m: &fs::Metadata| { |
|
| 338 | + | m.modified() |
|
| 339 | + | .map(|t| now.duration_since(t).unwrap_or_default() >= grace) |
|
| 340 | + | .unwrap_or(true) |
|
| 341 | + | }; |
|
| 297 | 342 | let mut report = PruneReport::default(); |
|
| 298 | 343 | // Loose objects. |
|
| 299 | 344 | for (r, path) in self.loose_objects()? { |
|
| … | 39 unchanged lines |
||
| 339 | 384 | if rewrite { |
|
| 340 | 385 | let dropped: Vec<ObjectRef> = { |
|
| 341 | 386 | let packs = self.packs.borrow(); |
|
| 342 | - | packs.iter().filter(|p| old_paths.contains(&p.path)).flat_map(|p| p.refs.iter().copied()).filter(|r| !keep.contains(r)).collect() |
|
| 387 | + | packs |
|
| 388 | + | .iter() |
|
| 389 | + | .filter(|p| old_paths.contains(&p.path)) |
|
| 390 | + | .flat_map(|p| p.refs.iter().copied()) |
|
| 391 | + | .filter(|r| !keep.contains(r)) |
|
| 392 | + | .collect() |
|
| 343 | 393 | }; |
|
| 344 | 394 | self.write_pack(&keep_refs, &[], &old_paths)?; |
|
| 345 | 395 | for r in dropped { |
|
| … | 19 unchanged lines |
||
| 365 | 415 | /// Write `refs` into a new pack, then delete `loose_paths` and `old_packs`. |
|
| 366 | 416 | /// Write the index for a complete, synced temporary pack file and move |
|
| 367 | 417 | /// both into place; `index` must be sorted by ObjectRef. |
|
| 368 | - | fn finish_pack_files(&self, id: &str, tmp_pack: &Path, index: &[(ObjectRef, PackEntry)]) -> Result<()> { |
|
| 418 | + | fn finish_pack_files( |
|
| 419 | + | &self, |
|
| 420 | + | id: &str, |
|
| 421 | + | tmp_pack: &Path, |
|
| 422 | + | index: &[(ObjectRef, PackEntry)], |
|
| 423 | + | ) -> Result<()> { |
|
| 369 | 424 | let pack_path = self.dir.join(format!("pack-{id}.pack")); |
|
| 370 | 425 | let idx_path = self.dir.join(format!("pack-{id}.idx")); |
|
| 371 | 426 | let tmp_idx = self.dir.join(format!("tmp-{id}.idx")); |
|
| … | 16 unchanged lines |
||
| 388 | 443 | self.reload_packs() |
|
| 389 | 444 | } |
|
| 390 | 445 | ||
| 391 | - | fn write_pack(&self, refs: &[ObjectRef], loose_paths: &[PathBuf], old_packs: &[PathBuf]) -> Result<PackReport> { |
|
| 446 | + | fn write_pack( |
|
| 447 | + | &self, |
|
| 448 | + | refs: &[ObjectRef], |
|
| 449 | + | loose_paths: &[PathBuf], |
|
| 450 | + | old_packs: &[PathBuf], |
|
| 451 | + | ) -> Result<PackReport> { |
|
| 392 | 452 | if refs.is_empty() { |
|
| 393 | 453 | for p in old_packs { |
|
| 394 | 454 | fs::remove_file(p)?; |
|
| … | 20 unchanged lines |
||
| 415 | 475 | let (flags, stored) = if self.is_nocompress(r) { |
|
| 416 | 476 | (0, body.clone()) |
|
| 417 | 477 | } else { |
|
| 418 | - | let compressed = zstd::bulk::compress(&body, ZSTD_LEVEL).map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?; |
|
| 419 | - | if compressed.len() < body.len() { (FLAG_ZSTD, compressed) } else { (0, body.clone()) } |
|
| 478 | + | let compressed = zstd::bulk::compress(&body, ZSTD_LEVEL) |
|
| 479 | + | .map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?; |
|
| 480 | + | if compressed.len() < body.len() { |
|
| 481 | + | (FLAG_ZSTD, compressed) |
|
| 482 | + | } else { |
|
| 483 | + | (0, body.clone()) |
|
| 484 | + | } |
|
| 420 | 485 | }; |
|
| 421 | 486 | let header_len = 1 + 1 + 4 + 4 + 32; |
|
| 422 | 487 | f.write_all(&[type_index(ty), flags])?; |
|
| … | 1 unchanged lines |
||
| 424 | 489 | f.write_all(&(body.len() as u32).to_le_bytes())?; |
|
| 425 | 490 | f.write_all(&r.0)?; |
|
| 426 | 491 | f.write_all(&stored)?; |
|
| 427 | - | index.push((*r, PackEntry { offset: offset + header_len, stored_len: stored.len() as u32, raw_len: body.len() as u32, ty, flags })); |
|
| 492 | + | index.push(( |
|
| 493 | + | *r, |
|
| 494 | + | PackEntry { |
|
| 495 | + | offset: offset + header_len, |
|
| 496 | + | stored_len: stored.len() as u32, |
|
| 497 | + | raw_len: body.len() as u32, |
|
| 498 | + | ty, |
|
| 499 | + | flags, |
|
| 500 | + | }, |
|
| 501 | + | )); |
|
| 428 | 502 | offset += header_len + stored.len() as u64; |
|
| 429 | 503 | report.objects += 1; |
|
| 430 | 504 | report.raw_bytes += body.len() as u64; |
|
| … | 27 unchanged lines |
||
| 458 | 532 | } |
|
| 459 | 533 | ||
| 460 | 534 | pub fn has(&self, r: &ObjectRef) -> bool { |
|
| 461 | - | self.path_for(r).is_file() || self.find_packed(r).is_some() || self.pending.borrow().as_ref().is_some_and(|p| p.lookup.contains_key(r)) |
|
| 535 | + | self.path_for(r).is_file() |
|
| 536 | + | || self.find_packed(r).is_some() |
|
| 537 | + | || self |
|
| 538 | + | .pending |
|
| 539 | + | .borrow() |
|
| 540 | + | .as_ref() |
|
| 541 | + | .is_some_and(|p| p.lookup.contains_key(r)) |
|
| 462 | 542 | } |
|
| 463 | 543 | ||
| 464 | 544 | /// Start a write batch: until the matching `finish`, `put` collects |
|
| … | 3 unchanged lines |
||
| 468 | 548 | pub fn begin_batch(&self) -> Result<Batch<'_>> { |
|
| 469 | 549 | let depth = self.batch_depth.get(); |
|
| 470 | 550 | if depth == 0 { |
|
| 471 | - | *self.pending.borrow_mut() = Some(Pending { buffered: vec![], buffered_bytes: 0, lookup: Default::default(), pack: None, report: PackReport::default() }); |
|
| 551 | + | *self.pending.borrow_mut() = Some(Pending { |
|
| 552 | + | buffered: vec![], |
|
| 553 | + | buffered_bytes: 0, |
|
| 554 | + | lookup: Default::default(), |
|
| 555 | + | pack: None, |
|
| 556 | + | report: PackReport::default(), |
|
| 557 | + | }); |
|
| 472 | 558 | } |
|
| 473 | 559 | self.batch_depth.set(depth + 1); |
|
| 474 | - | Ok(Batch { store: self, done: false }) |
|
| 560 | + | Ok(Batch { |
|
| 561 | + | store: self, |
|
| 562 | + | done: false, |
|
| 563 | + | }) |
|
| 475 | 564 | } |
|
| 476 | 565 | ||
| 477 | 566 | fn finish_batch(&self) -> Result<()> { |
|
| … | 5 unchanged lines |
||
| 483 | 572 | if depth > 1 { |
|
| 484 | 573 | return Ok(()); |
|
| 485 | 574 | } |
|
| 486 | - | let Some(pending) = self.pending.borrow_mut().take() else { return Ok(()) }; |
|
| 575 | + | let Some(pending) = self.pending.borrow_mut().take() else { |
|
| 576 | + | return Ok(()); |
|
| 577 | + | }; |
|
| 487 | 578 | match pending.pack { |
|
| 488 | 579 | None => { |
|
| 489 | 580 | for (ty, body) in &pending.buffered { |
|
| … | 23 unchanged lines |
||
| 513 | 604 | Ok((*ty, body.clone())) |
|
| 514 | 605 | } |
|
| 515 | 606 | PendingEntry::Packed(e) => { |
|
| 516 | - | let Some(pack) = pending.pack.as_mut() else { return Some(Err(StoreError::NotFound(*r))) }; |
|
| 607 | + | let Some(pack) = pending.pack.as_mut() else { |
|
| 608 | + | return Some(Err(StoreError::NotFound(*r))); |
|
| 609 | + | }; |
|
| 517 | 610 | match pack.writer.flush() { |
|
| 518 | - | Ok(()) => PackIndex { path: pack.tmp_path.clone(), refs: vec![], entries: vec![] }.read(r, e), |
|
| 611 | + | Ok(()) => PackIndex { |
|
| 612 | + | path: pack.tmp_path.clone(), |
|
| 613 | + | refs: vec![], |
|
| 614 | + | entries: vec![], |
|
| 615 | + | } |
|
| 616 | + | .read(r, e), |
|
| 519 | 617 | Err(err) => Err(err.into()), |
|
| 520 | 618 | } |
|
| 521 | 619 | } |
|
| … | 1 unchanged lines |
||
| 523 | 621 | } |
|
| 524 | 622 | ||
| 525 | 623 | /// Append one object to the pending pack file. |
|
| 526 | - | fn pending_pack_write(&self, pack: &mut PendingPack, r: &ObjectRef, ty: ObjectType, body: &[u8], report: &mut PackReport) -> Result<PackEntry> { |
|
| 624 | + | fn pending_pack_write( |
|
| 625 | + | &self, |
|
| 626 | + | pack: &mut PendingPack, |
|
| 627 | + | r: &ObjectRef, |
|
| 628 | + | ty: ObjectType, |
|
| 629 | + | body: &[u8], |
|
| 630 | + | report: &mut PackReport, |
|
| 631 | + | ) -> Result<PackEntry> { |
|
| 527 | 632 | let (flags, stored) = if self.is_nocompress(r) { |
|
| 528 | 633 | (0, body.to_vec()) |
|
| 529 | 634 | } else { |
|
| 530 | - | let compressed = zstd::bulk::compress(body, ZSTD_LEVEL).map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?; |
|
| 531 | - | if compressed.len() < body.len() { (FLAG_ZSTD, compressed) } else { (0, body.to_vec()) } |
|
| 635 | + | let compressed = zstd::bulk::compress(body, ZSTD_LEVEL) |
|
| 636 | + | .map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?; |
|
| 637 | + | if compressed.len() < body.len() { |
|
| 638 | + | (FLAG_ZSTD, compressed) |
|
| 639 | + | } else { |
|
| 640 | + | (0, body.to_vec()) |
|
| 641 | + | } |
|
| 532 | 642 | }; |
|
| 533 | 643 | let header_len = 1 + 1 + 4 + 4 + 32; |
|
| 534 | 644 | pack.writer.write_all(&[type_index(ty), flags])?; |
|
| 535 | - | pack.writer.write_all(&(stored.len() as u32).to_le_bytes())?; |
|
| 645 | + | pack.writer |
|
| 646 | + | .write_all(&(stored.len() as u32).to_le_bytes())?; |
|
| 536 | 647 | pack.writer.write_all(&(body.len() as u32).to_le_bytes())?; |
|
| 537 | 648 | pack.writer.write_all(&r.0)?; |
|
| 538 | 649 | pack.writer.write_all(&stored)?; |
|
| 539 | - | let entry = PackEntry { offset: pack.offset + header_len, stored_len: stored.len() as u32, raw_len: body.len() as u32, ty, flags }; |
|
| 650 | + | let entry = PackEntry { |
|
| 651 | + | offset: pack.offset + header_len, |
|
| 652 | + | stored_len: stored.len() as u32, |
|
| 653 | + | raw_len: body.len() as u32, |
|
| 654 | + | ty, |
|
| 655 | + | flags, |
|
| 656 | + | }; |
|
| 540 | 657 | pack.offset += header_len + stored.len() as u64; |
|
| 541 | 658 | pack.index.push((*r, entry)); |
|
| 542 | 659 | report.objects += 1; |
|
| … | 8 unchanged lines |
||
| 551 | 668 | if pending.lookup.contains_key(&r) { |
|
| 552 | 669 | return Ok(()); |
|
| 553 | 670 | } |
|
| 554 | - | if pending.pack.is_none() && pending.buffered.len() < SPILL_OBJECTS && pending.buffered_bytes + body.len() <= SPILL_BYTES { |
|
| 671 | + | if pending.pack.is_none() |
|
| 672 | + | && pending.buffered.len() < SPILL_OBJECTS |
|
| 673 | + | && pending.buffered_bytes + body.len() <= SPILL_BYTES |
|
| 674 | + | { |
|
| 555 | 675 | pending.buffered.push((ty, body.to_vec())); |
|
| 556 | 676 | pending.buffered_bytes += body.len(); |
|
| 557 | - | pending.lookup.insert(r, PendingEntry::Buffered(pending.buffered.len() - 1)); |
|
| 677 | + | pending |
|
| 678 | + | .lookup |
|
| 679 | + | .insert(r, PendingEntry::Buffered(pending.buffered.len() - 1)); |
|
| 558 | 680 | return Ok(()); |
|
| 559 | 681 | } |
|
| 560 | 682 | if pending.pack.is_none() { |
|
| … | 4 unchanged lines |
||
| 565 | 687 | let tmp_path = self.dir.join(format!("tmp-{id}.pack")); |
|
| 566 | 688 | let mut writer = io::BufWriter::new(fs::File::create(&tmp_path)?); |
|
| 567 | 689 | writer.write_all(PACK_MAGIC)?; |
|
| 568 | - | let mut pack = PendingPack { id, tmp_path, writer, offset: PACK_MAGIC.len() as u64, index: vec![] }; |
|
| 690 | + | let mut pack = PendingPack { |
|
| 691 | + | id, |
|
| 692 | + | tmp_path, |
|
| 693 | + | writer, |
|
| 694 | + | offset: PACK_MAGIC.len() as u64, |
|
| 695 | + | index: vec![], |
|
| 696 | + | }; |
|
| 569 | 697 | let buffered = std::mem::take(&mut pending.buffered); |
|
| 570 | 698 | pending.buffered_bytes = 0; |
|
| 571 | 699 | let mut report = std::mem::take(&mut pending.report); |
|
| … | 38 unchanged lines |
||
| 610 | 738 | fs::create_dir_all(path.parent().unwrap())?; |
|
| 611 | 739 | let mut nonce = [0u8; 8]; |
|
| 612 | 740 | rand::rngs::OsRng.fill_bytes(&mut nonce); |
|
| 613 | - | let tmp = self.dir.join("loose").join(format!("tmp-{}", hex::encode(nonce))); |
|
| 741 | + | let tmp = self |
|
| 742 | + | .dir |
|
| 743 | + | .join("loose") |
|
| 744 | + | .join(format!("tmp-{}", hex::encode(nonce))); |
|
| 614 | 745 | { |
|
| 615 | 746 | let mut f = fs::File::create(&tmp)?; |
|
| 616 | 747 | f.write_all(&header(ty, body.len()))?; |
|
| … | 17 unchanged lines |
||
| 634 | 765 | } |
|
| 635 | 766 | Err(e) => return Err(e.into()), |
|
| 636 | 767 | }; |
|
| 637 | - | let nul = data.iter().position(|&b| b == 0).ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?; |
|
| 638 | - | let head = std::str::from_utf8(&data[..nul]).map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?; |
|
| 768 | + | let nul = data |
|
| 769 | + | .iter() |
|
| 770 | + | .position(|&b| b == 0) |
|
| 771 | + | .ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?; |
|
| 772 | + | let head = std::str::from_utf8(&data[..nul]) |
|
| 773 | + | .map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?; |
|
| 639 | 774 | let mut parts = head.split(' '); |
|
| 640 | - | let (Some("oxy1"), Some(ty), Some(len), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else { |
|
| 775 | + | let (Some("oxy1"), Some(ty), Some(len), None) = |
|
| 776 | + | (parts.next(), parts.next(), parts.next(), parts.next()) |
|
| 777 | + | else { |
|
| 641 | 778 | return Err(StoreError::Corrupt(*r, format!("bad header {head:?}"))); |
|
| 642 | 779 | }; |
|
| 643 | - | let ty = ObjectType::from_name(ty).ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?; |
|
| 644 | - | let len: usize = len.parse().map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?; |
|
| 780 | + | let ty = ObjectType::from_name(ty) |
|
| 781 | + | .ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?; |
|
| 782 | + | let len: usize = len |
|
| 783 | + | .parse() |
|
| 784 | + | .map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?; |
|
| 645 | 785 | let body = &data[nul + 1..]; |
|
| 646 | 786 | if body.len() != len { |
|
| 647 | - | return Err(StoreError::Corrupt(*r, format!("length {} != header {len}", body.len()))); |
|
| 787 | + | return Err(StoreError::Corrupt( |
|
| 788 | + | *r, |
|
| 789 | + | format!("length {} != header {len}", body.len()), |
|
| 790 | + | )); |
|
| 648 | 791 | } |
|
| 649 | 792 | if hash_object(ty, body) != *r { |
|
| 650 | 793 | return Err(StoreError::Corrupt(*r, "hash mismatch".into())); |
|
| … | 148 unchanged lines |
||
| 799 | 942 | assert_eq!(s.get(&r).unwrap(), (ObjectType::Chunk, b"hello".to_vec())); |
|
| 800 | 943 | assert_eq!(s.put(ObjectType::Chunk, b"hello").unwrap(), r); |
|
| 801 | 944 | assert_eq!(s.list().unwrap(), vec![r]); |
|
| 802 | - | assert!(matches!(s.get_typed(&r, ObjectType::Blob), Err(StoreError::WrongType(..)))); |
|
| 803 | - | assert!(matches!(s.get(&ObjectRef([9; 32])), Err(StoreError::NotFound(_)))); |
|
| 945 | + | assert!(matches!( |
|
| 946 | + | s.get_typed(&r, ObjectType::Blob), |
|
| 947 | + | Err(StoreError::WrongType(..)) |
|
| 948 | + | )); |
|
| 949 | + | assert!(matches!( |
|
| 950 | + | s.get(&ObjectRef([9; 32])), |
|
| 951 | + | Err(StoreError::NotFound(_)) |
|
| 952 | + | )); |
|
| 804 | 953 | ||
| 805 | 954 | // Tamper: flip a byte, read must fail. |
|
| 806 | 955 | let path = s.path_for(&r); |
|
| … | 23 unchanged lines |
||
| 830 | 979 | assert_eq!(s.pack_count(), 1); |
|
| 831 | 980 | for (r, body) in &refs { |
|
| 832 | 981 | assert!(s.has(r)); |
|
| 833 | - | assert_eq!(s.get(r).unwrap(), (ObjectType::Chunk, body.as_bytes().to_vec())); |
|
| 982 | + | assert_eq!( |
|
| 983 | + | s.get(r).unwrap(), |
|
| 984 | + | (ObjectType::Chunk, body.as_bytes().to_vec()) |
|
| 985 | + | ); |
|
| 834 | 986 | } |
|
| 835 | 987 | assert_eq!(s.get_change(&cr).unwrap(), c); |
|
| 836 | 988 | assert_eq!(s.list().unwrap().len(), 301); |
|
| 837 | 989 | // Re-putting a packed object is a no-op; new objects go loose and a |
|
| 838 | 990 | // second pack coexists with the first. |
|
| 839 | - | assert_eq!(s.put(ObjectType::Chunk, refs[0].1.as_bytes()).unwrap(), refs[0].0); |
|
| 991 | + | assert_eq!( |
|
| 992 | + | s.put(ObjectType::Chunk, refs[0].1.as_bytes()).unwrap(), |
|
| 993 | + | refs[0].0 |
|
| 994 | + | ); |
|
| 840 | 995 | assert_eq!(s.loose_count().unwrap(), 0); |
|
| 841 | 996 | let extra = s.put(ObjectType::Chunk, b"after packing").unwrap(); |
|
| 842 | 997 | s.pack_loose().unwrap(); |
|
| … | 3 unchanged lines |
||
| 846 | 1001 | let s2 = Store::open(dir.path()).unwrap(); |
|
| 847 | 1002 | assert_eq!(s2.pack_count(), 2); |
|
| 848 | 1003 | assert_eq!(s2.get(&refs[5].0).unwrap().1, refs[5].1.as_bytes()); |
|
| 849 | - | assert!(matches!(s2.get(&ObjectRef([9; 32])), Err(StoreError::NotFound(_)))); |
|
| 1004 | + | assert!(matches!( |
|
| 1005 | + | s2.get(&ObjectRef([9; 32])), |
|
| 1006 | + | Err(StoreError::NotFound(_)) |
|
| 1007 | + | )); |
|
| 850 | 1008 | // Corrupting an object's body inside the pack is detected on read. |
|
| 851 | 1009 | let target = refs[5].0; |
|
| 852 | 1010 | let (pack_path, offset) = { |
|
| … | 6 unchanged lines |
||
| 859 | 1017 | fs::write(&pack_path, data).unwrap(); |
|
| 860 | 1018 | let s3 = Store::open(dir.path()).unwrap(); |
|
| 861 | 1019 | assert!(matches!(s3.get(&target), Err(StoreError::Corrupt(..)))); |
|
| 862 | - | assert_eq!(s3.get(&refs[6].0).unwrap().1, refs[6].1.as_bytes(), "other objects are unaffected"); |
|
| 1020 | + | assert_eq!( |
|
| 1021 | + | s3.get(&refs[6].0).unwrap().1, |
|
| 1022 | + | refs[6].1.as_bytes(), |
|
| 1023 | + | "other objects are unaffected" |
|
| 1024 | + | ); |
|
| 863 | 1025 | } |
|
| 864 | 1026 | ||
| 865 | 1027 | #[test] |
|
| … | 19 unchanged lines |
||
| 885 | 1047 | let s = Store::open(dir.path()).unwrap(); |
|
| 886 | 1048 | let mut refs = Vec::new(); |
|
| 887 | 1049 | for i in 0..(MAX_PACKS + 2) { |
|
| 888 | - | refs.push(s.put(ObjectType::Chunk, format!("pack {i}").as_bytes()).unwrap()); |
|
| 1050 | + | refs.push( |
|
| 1051 | + | s.put(ObjectType::Chunk, format!("pack {i}").as_bytes()) |
|
| 1052 | + | .unwrap(), |
|
| 1053 | + | ); |
|
| 889 | 1054 | s.pack_loose().unwrap(); |
|
| 890 | 1055 | } |
|
| 891 | 1056 | assert_eq!(s.pack_count(), MAX_PACKS + 2); |
|
| … | 16 unchanged lines |
||
| 908 | 1073 | s.pack_loose().unwrap(); |
|
| 909 | 1074 | let keep_b = s.put(ObjectType::Chunk, b"keep b").unwrap(); |
|
| 910 | 1075 | let drop_b = s.put(ObjectType::Chunk, b"drop b").unwrap(); |
|
| 911 | - | let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] }; |
|
| 1076 | + | let sk = crate::identity::SecretKey { |
|
| 1077 | + | ed_seed: [1; 32], |
|
| 1078 | + | ml_seed: [2; 32], |
|
| 1079 | + | }; |
|
| 912 | 1080 | let id = sk.identity("a", "b", 0); |
|
| 913 | 1081 | let key = s.put_identity(&id).unwrap(); |
|
| 914 | - | s.put_signature(&drop_b, &sk.sign(crate::identity::Domain::Change, &key, &drop_b)).unwrap(); |
|
| 1082 | + | s.put_signature( |
|
| 1083 | + | &drop_b, |
|
| 1084 | + | &sk.sign(crate::identity::Domain::Change, &key, &drop_b), |
|
| 1085 | + | ) |
|
| 1086 | + | .unwrap(); |
|
| 915 | 1087 | s.index_seal(&drop_a, &ObjectRef([3; 32])).unwrap(); |
|
| 916 | 1088 | let keep: HashSet<ObjectRef> = [keep_a, keep_b, key].into_iter().collect(); |
|
| 917 | 1089 | // Everything is too young under a grace period. |
|
| 918 | - | let r = s.prune(&keep, std::time::Duration::from_secs(3600)).unwrap(); |
|
| 1090 | + | let r = s |
|
| 1091 | + | .prune(&keep, std::time::Duration::from_secs(3600)) |
|
| 1092 | + | .unwrap(); |
|
| 919 | 1093 | assert_eq!((r.objects, r.deferred), (0, 2)); |
|
| 920 | 1094 | let r = s.prune(&keep, std::time::Duration::ZERO).unwrap(); |
|
| 921 | 1095 | assert_eq!(r.objects, 2, "{r:?}"); |
|
| … | 13 unchanged lines |
||
| 935 | 1109 | let s = Store::open(dir.path()).unwrap(); |
|
| 936 | 1110 | let r = s.put(ObjectType::Chunk, b"x").unwrap(); |
|
| 937 | 1111 | assert!(s.signatures(&r).unwrap().is_empty()); |
|
| 938 | - | let sk = SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] }; |
|
| 1112 | + | let sk = SecretKey { |
|
| 1113 | + | ed_seed: [1; 32], |
|
| 1114 | + | ml_seed: [2; 32], |
|
| 1115 | + | }; |
|
| 939 | 1116 | let id = sk.identity("a", "b", 0); |
|
| 940 | 1117 | let key = s.put_identity(&id).unwrap(); |
|
| 941 | 1118 | assert_eq!(s.get_identity(&key).unwrap(), id); |
|
| … | 36 unchanged lines |
||
| 978 | 1155 | let len = (xorshift(&mut seed) % 5000) as usize; |
|
| 979 | 1156 | let mut body = vec![0u8; len]; |
|
| 980 | 1157 | for b in body.iter_mut() { |
|
| 981 | - | *b = if i % 2 == 0 { (xorshift(&mut seed) & 0xff) as u8 } else { b'a' + (i % 26) as u8 }; |
|
| 1158 | + | *b = if i % 2 == 0 { |
|
| 1159 | + | (xorshift(&mut seed) & 0xff) as u8 |
|
| 1160 | + | } else { |
|
| 1161 | + | b'a' + (i % 26) as u8 |
|
| 1162 | + | }; |
|
| 982 | 1163 | } |
|
| 983 | 1164 | refs.push(s.put(ObjectType::Chunk, &body).unwrap()); |
|
| 984 | 1165 | } |
|
| 985 | 1166 | s.pack_loose().unwrap(); |
|
| 986 | - | let pack = fs::read_dir(&objects).unwrap().filter_map(|e| e.ok().map(|e| e.path())).find(|p| p.extension().is_some_and(|x| x == "pack")).unwrap(); |
|
| 1167 | + | let pack = fs::read_dir(&objects) |
|
| 1168 | + | .unwrap() |
|
| 1169 | + | .filter_map(|e| e.ok().map(|e| e.path())) |
|
| 1170 | + | .find(|p| p.extension().is_some_and(|x| x == "pack")) |
|
| 1171 | + | .unwrap(); |
|
| 987 | 1172 | let idx = pack.with_extension("idx"); |
|
| 988 | 1173 | let pristine_pack = fs::read(&pack).unwrap(); |
|
| 989 | 1174 | let pristine_idx = fs::read(&idx).unwrap(); |
|
| 990 | 1175 | ||
| 991 | 1176 | for round in 0..300u32 { |
|
| 992 | - | let (path, pristine) = if round % 2 == 0 { (&pack, &pristine_pack) } else { (&idx, &pristine_idx) }; |
|
| 1177 | + | let (path, pristine) = if round % 2 == 0 { |
|
| 1178 | + | (&pack, &pristine_pack) |
|
| 1179 | + | } else { |
|
| 1180 | + | (&idx, &pristine_idx) |
|
| 1181 | + | }; |
|
| 993 | 1182 | let mut data = pristine.clone(); |
|
| 994 | 1183 | match xorshift(&mut seed) % 4 { |
|
| 995 | 1184 | 0 => { |
|
| … | 22 unchanged lines |
||
| 1018 | 1207 | if let Ok(s2) = Store::open(&objects) { |
|
| 1019 | 1208 | for r in &refs { |
|
| 1020 | 1209 | match s2.get(r) { |
|
| 1021 | - | Ok((ty, body)) => assert_eq!(hash_object(ty, &body), *r, "a returned object always matches its reference"), |
|
| 1022 | - | Err(StoreError::Corrupt(..)) | Err(StoreError::NotFound(_)) | Err(StoreError::Io(_)) => {} |
|
| 1210 | + | Ok((ty, body)) => assert_eq!( |
|
| 1211 | + | hash_object(ty, &body), |
|
| 1212 | + | *r, |
|
| 1213 | + | "a returned object always matches its reference" |
|
| 1214 | + | ), |
|
| 1215 | + | Err(StoreError::Corrupt(..)) |
|
| 1216 | + | | Err(StoreError::NotFound(_)) |
|
| 1217 | + | | Err(StoreError::Io(_)) => {} |
|
| 1023 | 1218 | Err(e) => panic!("unexpected error {e}"), |
|
| 1024 | 1219 | } |
|
| 1025 | 1220 | } |
|
| … | 22 unchanged lines |
||
| 1048 | 1243 | assert!(s.has(&b)); |
|
| 1049 | 1244 | assert_eq!(s.get(&b).unwrap().1, b"small b"); |
|
| 1050 | 1245 | inner.finish().unwrap(); |
|
| 1051 | - | assert_eq!(s.loose_count().unwrap(), 0, "the inner batch defers to the outer one"); |
|
| 1246 | + | assert_eq!( |
|
| 1247 | + | s.loose_count().unwrap(), |
|
| 1248 | + | 0, |
|
| 1249 | + | "the inner batch defers to the outer one" |
|
| 1250 | + | ); |
|
| 1052 | 1251 | } |
|
| 1053 | 1252 | assert_eq!(s.get(&a).unwrap().1, b"small a"); |
|
| 1054 | - | assert_eq!(s.put(ObjectType::Chunk, b"small a").unwrap(), a, "idempotent while pending"); |
|
| 1253 | + | assert_eq!( |
|
| 1254 | + | s.put(ObjectType::Chunk, b"small a").unwrap(), |
|
| 1255 | + | a, |
|
| 1256 | + | "idempotent while pending" |
|
| 1257 | + | ); |
|
| 1055 | 1258 | outer.finish().unwrap(); |
|
| 1056 | 1259 | assert_eq!(s.loose_count().unwrap(), 2); |
|
| 1057 | 1260 | assert_eq!(s.pack_count(), 0); |
|
| … | 11 unchanged lines |
||
| 1069 | 1272 | } |
|
| 1070 | 1273 | } |
|
| 1071 | 1274 | body[..4].copy_from_slice(&i.to_le_bytes()); |
|
| 1072 | - | let r = if i % 5 == 0 { s.put_chunk_nocompress(&body).unwrap() } else { s.put(ObjectType::Chunk, &body).unwrap() }; |
|
| 1073 | - | assert_eq!(s.get(&r).unwrap().1, body, "readable while pending (object {i})"); |
|
| 1275 | + | let r = if i % 5 == 0 { |
|
| 1276 | + | s.put_chunk_nocompress(&body).unwrap() |
|
| 1277 | + | } else { |
|
| 1278 | + | s.put(ObjectType::Chunk, &body).unwrap() |
|
| 1279 | + | }; |
|
| 1280 | + | assert_eq!( |
|
| 1281 | + | s.get(&r).unwrap().1, |
|
| 1282 | + | body, |
|
| 1283 | + | "readable while pending (object {i})" |
|
| 1284 | + | ); |
|
| 1074 | 1285 | refs.push((r, body)); |
|
| 1075 | 1286 | } |
|
| 1076 | 1287 | assert!(s.has(&refs[0].0)); |
|
| 1077 | 1288 | big.finish().unwrap(); |
|
| 1078 | 1289 | assert_eq!(s.pack_count(), 1); |
|
| 1079 | - | assert_eq!(s.loose_count().unwrap(), 2, "nothing from the big batch is loose"); |
|
| 1080 | - | assert!(fs::read_dir(dir.path().join("objects")).unwrap().flatten().all(|e| !e.file_name().to_string_lossy().starts_with("tmp-"))); |
|
| 1290 | + | assert_eq!( |
|
| 1291 | + | s.loose_count().unwrap(), |
|
| 1292 | + | 2, |
|
| 1293 | + | "nothing from the big batch is loose" |
|
| 1294 | + | ); |
|
| 1295 | + | assert!(fs::read_dir(dir.path().join("objects")) |
|
| 1296 | + | .unwrap() |
|
| 1297 | + | .flatten() |
|
| 1298 | + | .all(|e| !e.file_name().to_string_lossy().starts_with("tmp-"))); |
|
| 1081 | 1299 | let s2 = Store::open(dir.path().join("objects")).unwrap(); |
|
| 1082 | 1300 | for (r, body) in &refs { |
|
| 1083 | 1301 | assert_eq!(s2.get(r).unwrap().1, *body); |
|
| … | 3 unchanged lines |
||
| 1087 | 1305 | let _b = s2.begin_batch().unwrap(); |
|
| 1088 | 1306 | s2.put(ObjectType::Chunk, b"dropped").unwrap(); |
|
| 1089 | 1307 | } |
|
| 1090 | - | assert_eq!(s2.get(&hash_object(ObjectType::Chunk, b"dropped")).unwrap().1, b"dropped"); |
|
| 1308 | + | assert_eq!( |
|
| 1309 | + | s2.get(&hash_object(ObjectType::Chunk, b"dropped")) |
|
| 1310 | + | .unwrap() |
|
| 1311 | + | .1, |
|
| 1312 | + | b"dropped" |
|
| 1313 | + | ); |
|
| 1091 | 1314 | } |
|
| 1092 | 1315 | } |
modified crates/oxy-core/src/subrepo.rs +116 −26
| … | 6 unchanged lines |
||
| 7 | 7 | ||
| 8 | 8 | use crate::cbor::{self, Value}; |
|
| 9 | 9 | use crate::hash::ChangeHash; |
|
| 10 | - | use crate::objects::{array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION}; |
|
| 10 | + | use crate::objects::{ |
|
| 11 | + | array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION, |
|
| 12 | + | }; |
|
| 11 | 13 | use std::collections::BTreeMap; |
|
| 12 | 14 | ||
| 13 | 15 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 14 | 16 | pub enum Pin { |
|
| 15 | - | Oxy { repo: [u8; 16], heads: Vec<ChangeHash>, bookmark: String }, |
|
| 16 | - | Git { sha: String, branch: String }, |
|
| 17 | + | Oxy { |
|
| 18 | + | repo: [u8; 16], |
|
| 19 | + | heads: Vec<ChangeHash>, |
|
| 20 | + | bookmark: String, |
|
| 21 | + | }, |
|
| 22 | + | Git { |
|
| 23 | + | sha: String, |
|
| 24 | + | branch: String, |
|
| 25 | + | }, |
|
| 17 | 26 | } |
|
| 18 | 27 | ||
| 19 | 28 | impl Pin { |
|
| 20 | 29 | pub fn encode(&self) -> Vec<u8> { |
|
| 21 | 30 | let v = match self { |
|
| 22 | - | Pin::Oxy { repo, heads, bookmark } => { |
|
| 31 | + | Pin::Oxy { |
|
| 32 | + | repo, |
|
| 33 | + | heads, |
|
| 34 | + | bookmark, |
|
| 35 | + | } => { |
|
| 23 | 36 | let mut heads = heads.clone(); |
|
| 24 | 37 | heads.sort(); |
|
| 25 | 38 | heads.dedup(); |
|
| … | 1 unchanged lines |
||
| 27 | 40 | (0, Value::Uint(FORMAT_VERSION)), |
|
| 28 | 41 | (1, Value::Uint(0)), |
|
| 29 | 42 | (2, Value::Bytes(repo.to_vec())), |
|
| 30 | - | (3, Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())), |
|
| 43 | + | ( |
|
| 44 | + | 3, |
|
| 45 | + | Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()), |
|
| 46 | + | ), |
|
| 31 | 47 | (4, Value::Text(bookmark.clone())), |
|
| 32 | 48 | ]) |
|
| 33 | 49 | } |
|
| … | 10 unchanged lines |
||
| 44 | 60 | ||
| 45 | 61 | pub fn decode(b: &[u8]) -> Result<Pin, ObjectError> { |
|
| 46 | 62 | let v = cbor::decode(b)?; |
|
| 47 | - | let m = v.as_map().ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?; |
|
| 63 | + | let m = v |
|
| 64 | + | .as_map() |
|
| 65 | + | .ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?; |
|
| 48 | 66 | check_version(m)?; |
|
| 49 | 67 | let bookmark = text(field(m, 4, "bookmark")?, "bookmark")?.to_owned(); |
|
| 50 | 68 | match uint(field(m, 1, "kind")?, "kind")? { |
|
| 51 | 69 | 0 => { |
|
| 52 | - | let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?.try_into().map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?; |
|
| 53 | - | let heads = array(field(m, 3, "heads")?, "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?; |
|
| 54 | - | Ok(Pin::Oxy { repo, heads, bookmark }) |
|
| 70 | + | let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")? |
|
| 71 | + | .try_into() |
|
| 72 | + | .map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?; |
|
| 73 | + | let heads = array(field(m, 3, "heads")?, "heads")? |
|
| 74 | + | .iter() |
|
| 75 | + | .map(|h| oref(h, "head")) |
|
| 76 | + | .collect::<Result<Vec<_>, _>>()?; |
|
| 77 | + | Ok(Pin::Oxy { |
|
| 78 | + | repo, |
|
| 79 | + | heads, |
|
| 80 | + | bookmark, |
|
| 81 | + | }) |
|
| 55 | 82 | } |
|
| 56 | - | 1 => Ok(Pin::Git { sha: text(field(m, 3, "sha")?, "sha")?.to_owned(), branch: bookmark }), |
|
| 83 | + | 1 => Ok(Pin::Git { |
|
| 84 | + | sha: text(field(m, 3, "sha")?, "sha")?.to_owned(), |
|
| 85 | + | branch: bookmark, |
|
| 86 | + | }), |
|
| 57 | 87 | k => Err(ObjectError::Schema(format!("pin: unknown kind {k}"))), |
|
| 58 | 88 | } |
|
| 59 | 89 | } |
|
| … | 1 unchanged lines |
||
| 61 | 91 | /// A one-line description for listings. |
|
| 62 | 92 | pub fn summary(&self) -> String { |
|
| 63 | 93 | match self { |
|
| 64 | - | Pin::Oxy { heads, bookmark, .. } => format!("oxy {bookmark} {}", heads.iter().map(|h| h.short(12)).collect::<Vec<_>>().join(" ")), |
|
| 94 | + | Pin::Oxy { |
|
| 95 | + | heads, bookmark, .. |
|
| 96 | + | } => format!( |
|
| 97 | + | "oxy {bookmark} {}", |
|
| 98 | + | heads |
|
| 99 | + | .iter() |
|
| 100 | + | .map(|h| h.short(12)) |
|
| 101 | + | .collect::<Vec<_>>() |
|
| 102 | + | .join(" ") |
|
| 103 | + | ), |
|
| 65 | 104 | Pin::Git { sha, branch } => format!("git {branch} {}", &sha[..sha.len().min(12)]), |
|
| 66 | 105 | } |
|
| 67 | 106 | } |
|
| … | 29 unchanged lines |
||
| 97 | 136 | continue; |
|
| 98 | 137 | } |
|
| 99 | 138 | if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { |
|
| 100 | - | let path = inner.trim().trim_matches('"').trim_end_matches('/').to_string(); |
|
| 139 | + | let path = inner |
|
| 140 | + | .trim() |
|
| 141 | + | .trim_matches('"') |
|
| 142 | + | .trim_end_matches('/') |
|
| 143 | + | .to_string(); |
|
| 101 | 144 | out.entries.entry(path.clone()).or_default(); |
|
| 102 | 145 | current = Some(path); |
|
| 103 | 146 | continue; |
|
| 104 | 147 | } |
|
| 105 | - | let (Some(path), Some((k, v))) = (¤t, line.split_once('=')) else { continue }; |
|
| 148 | + | let (Some(path), Some((k, v))) = (¤t, line.split_once('=')) else { |
|
| 149 | + | continue; |
|
| 150 | + | }; |
|
| 106 | 151 | let v = v.trim().trim_matches('"').to_string(); |
|
| 107 | 152 | let m = out.entries.get_mut(path).unwrap(); |
|
| 108 | 153 | match k.trim() { |
|
| … | 21 unchanged lines |
||
| 130 | 175 | s.push_str("kind = git\n"); |
|
| 131 | 176 | } |
|
| 132 | 177 | if let Some(b) = &m.bookmark { |
|
| 133 | - | s.push_str(&format!("{} = {b}\n", if m.git { "branch" } else { "bookmark" })); |
|
| 178 | + | s.push_str(&format!( |
|
| 179 | + | "{} = {b}\n", |
|
| 180 | + | if m.git { "branch" } else { "bookmark" } |
|
| 181 | + | )); |
|
| 134 | 182 | } |
|
| 135 | 183 | s.push('\n'); |
|
| 136 | 184 | } |
|
| … | 6 unchanged lines |
||
| 143 | 191 | let mut path: Option<String> = None; |
|
| 144 | 192 | let mut url = String::new(); |
|
| 145 | 193 | let mut branch: Option<String> = None; |
|
| 146 | - | let flush = |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| { |
|
| 147 | - | if let Some(p) = path { |
|
| 148 | - | out.entries.insert(p.clone(), Module { url: url.to_string(), bookmark: branch.clone(), git: true }); |
|
| 149 | - | } |
|
| 150 | - | }; |
|
| 194 | + | let flush = |
|
| 195 | + | |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| { |
|
| 196 | + | if let Some(p) = path { |
|
| 197 | + | out.entries.insert( |
|
| 198 | + | p.clone(), |
|
| 199 | + | Module { |
|
| 200 | + | url: url.to_string(), |
|
| 201 | + | bookmark: branch.clone(), |
|
| 202 | + | git: true, |
|
| 203 | + | }, |
|
| 204 | + | ); |
|
| 205 | + | } |
|
| 206 | + | }; |
|
| 151 | 207 | for line in text.lines() { |
|
| 152 | 208 | let line = line.trim(); |
|
| 153 | 209 | if line.starts_with("[submodule") { |
|
| … | 24 unchanged lines |
||
| 178 | 234 | if !m.git { |
|
| 179 | 235 | continue; |
|
| 180 | 236 | } |
|
| 181 | - | s.push_str(&format!("[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n", m.url)); |
|
| 237 | + | s.push_str(&format!( |
|
| 238 | + | "[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n", |
|
| 239 | + | m.url |
|
| 240 | + | )); |
|
| 182 | 241 | if let Some(b) = &m.bookmark { |
|
| 183 | 242 | s.push_str(&format!("\tbranch = {b}\n")); |
|
| 184 | 243 | } |
|
| … | 9 unchanged lines |
||
| 194 | 253 | ||
| 195 | 254 | #[test] |
|
| 196 | 255 | fn pins_round_trip() { |
|
| 197 | - | let o = Pin::Oxy { repo: [7; 16], heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])], bookmark: "main".into() }; |
|
| 256 | + | let o = Pin::Oxy { |
|
| 257 | + | repo: [7; 16], |
|
| 258 | + | heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])], |
|
| 259 | + | bookmark: "main".into(), |
|
| 260 | + | }; |
|
| 198 | 261 | let back = Pin::decode(&o.encode()).unwrap(); |
|
| 199 | - | assert_eq!(back, Pin::Oxy { repo: [7; 16], heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])], bookmark: "main".into() }); |
|
| 200 | - | let g = Pin::Git { sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(), branch: "main".into() }; |
|
| 262 | + | assert_eq!( |
|
| 263 | + | back, |
|
| 264 | + | Pin::Oxy { |
|
| 265 | + | repo: [7; 16], |
|
| 266 | + | heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])], |
|
| 267 | + | bookmark: "main".into() |
|
| 268 | + | } |
|
| 269 | + | ); |
|
| 270 | + | let g = Pin::Git { |
|
| 271 | + | sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(), |
|
| 272 | + | branch: "main".into(), |
|
| 273 | + | }; |
|
| 201 | 274 | assert_eq!(Pin::decode(&g.encode()).unwrap(), g); |
|
| 202 | 275 | assert!(g.is_git() && !o.is_git()); |
|
| 203 | 276 | } |
|
| … | 2 unchanged lines |
||
| 206 | 279 | fn modules_files() { |
|
| 207 | 280 | let text = "[libs/sub]\nurl = ../sub\nbookmark = main\n\n[vendor/x]\nurl = https://example.org/x.git\nkind = git\nbranch = trunk\n"; |
|
| 208 | 281 | let m = Modules::parse(text); |
|
| 209 | - | assert_eq!(m.entries["libs/sub"], Module { url: "../sub".into(), bookmark: Some("main".into()), git: false }); |
|
| 210 | - | assert_eq!(m.entries["vendor/x"], Module { url: "https://example.org/x.git".into(), bookmark: Some("trunk".into()), git: true }); |
|
| 282 | + | assert_eq!( |
|
| 283 | + | m.entries["libs/sub"], |
|
| 284 | + | Module { |
|
| 285 | + | url: "../sub".into(), |
|
| 286 | + | bookmark: Some("main".into()), |
|
| 287 | + | git: false |
|
| 288 | + | } |
|
| 289 | + | ); |
|
| 290 | + | assert_eq!( |
|
| 291 | + | m.entries["vendor/x"], |
|
| 292 | + | Module { |
|
| 293 | + | url: "https://example.org/x.git".into(), |
|
| 294 | + | bookmark: Some("trunk".into()), |
|
| 295 | + | git: true |
|
| 296 | + | } |
|
| 297 | + | ); |
|
| 211 | 298 | assert_eq!(Modules::parse(&m.to_text()), m); |
|
| 212 | 299 | let gm = "[submodule \"rstar-api\"]\n\tpath = rstar-api\n\turl = https://github.com/x/rstar-api.git\n[submodule \"c\"]\n\tpath = cstar-api\n\turl = https://github.com/x/cstar-api.git\n\tbranch = dev\n"; |
|
| 213 | 300 | let g = Modules::from_gitmodules(gm); |
|
| 214 | 301 | assert_eq!(g.entries.len(), 2); |
|
| 215 | 302 | assert!(g.entries["rstar-api"].git && g.entries["rstar-api"].bookmark.is_none()); |
|
| 216 | 303 | assert_eq!(g.entries["cstar-api"].bookmark.as_deref(), Some("dev")); |
|
| 217 | - | assert!(g.to_gitmodules().contains("[submodule \"cstar-api\"]") && g.to_gitmodules().contains("branch = dev")); |
|
| 304 | + | assert!( |
|
| 305 | + | g.to_gitmodules().contains("[submodule \"cstar-api\"]") |
|
| 306 | + | && g.to_gitmodules().contains("branch = dev") |
|
| 307 | + | ); |
|
| 218 | 308 | } |
|
| 219 | 309 | } |
modified crates/oxy-core/src/tree.rs +375 −53
| … | 33 unchanged lines |
||
| 34 | 34 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 35 | 35 | pub enum TreeConflict { |
|
| 36 | 36 | /// Two live nodes share `(parent, name)`. |
|
| 37 | - | Name { parent: NodeId, name: String, nodes: Vec<NodeId> }, |
|
| 37 | + | Name { |
|
| 38 | + | parent: NodeId, |
|
| 39 | + | name: String, |
|
| 40 | + | nodes: Vec<NodeId>, |
|
| 41 | + | }, |
|
| 38 | 42 | /// One node has several live placements. |
|
| 39 | - | MultiPlacement { node: NodeId, placements: Vec<OpKey> }, |
|
| 43 | + | MultiPlacement { |
|
| 44 | + | node: NodeId, |
|
| 45 | + | placements: Vec<OpKey>, |
|
| 46 | + | }, |
|
| 40 | 47 | /// A live node's parent is not live. |
|
| 41 | 48 | Orphan { node: NodeId, parent: NodeId }, |
|
| 42 | 49 | /// A directory is placed inside its own descendant. |
|
| … | 41 unchanged lines |
||
| 84 | 91 | pub fn apply(&mut self, change: ChangeHash, index: u32, op: &TreeOp) { |
|
| 85 | 92 | let key = (change, index); |
|
| 86 | 93 | match op { |
|
| 87 | - | TreeOp::AddNode { node, kind, mode, parent, name } => { |
|
| 94 | + | TreeOp::AddNode { |
|
| 95 | + | node, |
|
| 96 | + | kind, |
|
| 97 | + | mode, |
|
| 98 | + | parent, |
|
| 99 | + | name, |
|
| 100 | + | } => { |
|
| 88 | 101 | self.kinds.insert(*node, *kind); |
|
| 89 | 102 | self.introduced.insert(*node, change); |
|
| 90 | - | self.placements.insert(key, Placement { node: *node, parent: *parent, name: name.clone() }); |
|
| 103 | + | self.placements.insert( |
|
| 104 | + | key, |
|
| 105 | + | Placement { |
|
| 106 | + | node: *node, |
|
| 107 | + | parent: *parent, |
|
| 108 | + | name: name.clone(), |
|
| 109 | + | }, |
|
| 110 | + | ); |
|
| 91 | 111 | self.placements_of.entry(*node).or_default().insert(key); |
|
| 92 | 112 | if *kind != NodeKind::Dir { |
|
| 93 | 113 | self.modes.insert(key, (*node, *mode)); |
|
| … | 1 unchanged lines |
||
| 95 | 115 | } |
|
| 96 | 116 | } |
|
| 97 | 117 | TreeOp::RemoveNode { placement, .. } => { |
|
| 98 | - | self.deleted_placements.entry(op_key(*placement)).or_default().insert(change); |
|
| 118 | + | self.deleted_placements |
|
| 119 | + | .entry(op_key(*placement)) |
|
| 120 | + | .or_default() |
|
| 121 | + | .insert(change); |
|
| 99 | 122 | } |
|
| 100 | - | TreeOp::Move { node, old, parent, name } => { |
|
| 101 | - | self.deleted_placements.entry(op_key(*old)).or_default().insert(change); |
|
| 102 | - | self.placements.insert(key, Placement { node: *node, parent: *parent, name: name.clone() }); |
|
| 123 | + | TreeOp::Move { |
|
| 124 | + | node, |
|
| 125 | + | old, |
|
| 126 | + | parent, |
|
| 127 | + | name, |
|
| 128 | + | } => { |
|
| 129 | + | self.deleted_placements |
|
| 130 | + | .entry(op_key(*old)) |
|
| 131 | + | .or_default() |
|
| 132 | + | .insert(change); |
|
| 133 | + | self.placements.insert( |
|
| 134 | + | key, |
|
| 135 | + | Placement { |
|
| 136 | + | node: *node, |
|
| 137 | + | parent: *parent, |
|
| 138 | + | name: name.clone(), |
|
| 139 | + | }, |
|
| 140 | + | ); |
|
| 103 | 141 | self.placements_of.entry(*node).or_default().insert(key); |
|
| 104 | 142 | } |
|
| 105 | 143 | TreeOp::SetMode { node, old, mode } => { |
|
| 106 | - | self.deleted_modes.entry(op_key(*old)).or_default().insert(change); |
|
| 144 | + | self.deleted_modes |
|
| 145 | + | .entry(op_key(*old)) |
|
| 146 | + | .or_default() |
|
| 147 | + | .insert(change); |
|
| 107 | 148 | self.modes.insert(key, (*node, *mode)); |
|
| 108 | 149 | self.modes_of.entry(*node).or_default().insert(key); |
|
| 109 | 150 | } |
|
| … | 8 unchanged lines |
||
| 118 | 159 | } |
|
| 119 | 160 | ||
| 120 | 161 | pub fn kind(&self, node: &NodeId) -> Option<NodeKind> { |
|
| 121 | - | if node.is_root() { Some(NodeKind::Dir) } else { self.kinds.get(node).copied() } |
|
| 162 | + | if node.is_root() { |
|
| 163 | + | Some(NodeKind::Dir) |
|
| 164 | + | } else { |
|
| 165 | + | self.kinds.get(node).copied() |
|
| 166 | + | } |
|
| 122 | 167 | } |
|
| 123 | 168 | ||
| 124 | 169 | /// The change whose `AddNode` introduced `node`. |
|
| … | 27 unchanged lines |
||
| 152 | 197 | ||
| 153 | 198 | /// Every node that has at least one live placement. |
|
| 154 | 199 | pub fn placed_nodes(&self) -> impl Iterator<Item = NodeId> + '_ { |
|
| 155 | - | self.placements_of.keys().copied().filter(|n| !self.live_placements(n).is_empty()) |
|
| 200 | + | self.placements_of |
|
| 201 | + | .keys() |
|
| 202 | + | .copied() |
|
| 203 | + | .filter(|n| !self.live_placements(n).is_empty()) |
|
| 156 | 204 | } |
|
| 157 | 205 | ||
| 158 | 206 | /// Resolve the tree: assign paths, find conflicts. |
|
| … | 17 unchanged lines |
||
| 176 | 224 | let mut multi: HashSet<NodeId> = HashSet::new(); |
|
| 177 | 225 | for n in &nodes { |
|
| 178 | 226 | let lp = self.live_placements(n); |
|
| 179 | - | let distinct_places: BTreeSet<(&NodeId, &str)> = lp.iter().map(|(_, p)| (&p.parent, p.name.as_str())).collect(); |
|
| 227 | + | let distinct_places: BTreeSet<(&NodeId, &str)> = lp |
|
| 228 | + | .iter() |
|
| 229 | + | .map(|(_, p)| (&p.parent, p.name.as_str())) |
|
| 230 | + | .collect(); |
|
| 180 | 231 | if distinct_places.len() > 1 { |
|
| 181 | 232 | multi.insert(*n); |
|
| 182 | - | view.conflicts.push(TreeConflict::MultiPlacement { node: *n, placements: lp.iter().map(|(k, _)| *k).collect() }); |
|
| 233 | + | view.conflicts.push(TreeConflict::MultiPlacement { |
|
| 234 | + | node: *n, |
|
| 235 | + | placements: lp.iter().map(|(k, _)| *k).collect(), |
|
| 236 | + | }); |
|
| 183 | 237 | } |
|
| 184 | 238 | let lm = self.live_modes(n); |
|
| 185 | 239 | let distinct_modes: BTreeSet<Mode> = lm.iter().map(|(_, m)| *m).collect(); |
|
| 186 | 240 | if distinct_modes.len() > 1 { |
|
| 187 | - | view.conflicts.push(TreeConflict::Mode { node: *n, modes: distinct_modes.into_iter().collect() }); |
|
| 241 | + | view.conflicts.push(TreeConflict::Mode { |
|
| 242 | + | node: *n, |
|
| 243 | + | modes: distinct_modes.into_iter().collect(), |
|
| 244 | + | }); |
|
| 188 | 245 | } |
|
| 189 | 246 | } |
|
| 190 | 247 | ||
| … | 3 unchanged lines |
||
| 194 | 251 | let mut queue: Vec<(NodeId, String)> = vec![(NodeId::ROOT, String::new())]; |
|
| 195 | 252 | let mut on_path: HashSet<NodeId> = HashSet::new(); |
|
| 196 | 253 | while let Some((dir, dir_path)) = queue.pop() { |
|
| 197 | - | let Some(kids) = children.get(&dir) else { continue }; |
|
| 254 | + | let Some(kids) = children.get(&dir) else { |
|
| 255 | + | continue; |
|
| 256 | + | }; |
|
| 198 | 257 | // Name conflicts within this directory. |
|
| 199 | 258 | let mut by_name: BTreeMap<&str, Vec<(OpKey, &Placement)>> = BTreeMap::new(); |
|
| 200 | 259 | for (k, p) in kids { |
|
| … | 2 unchanged lines |
||
| 203 | 262 | for (name, group) in by_name { |
|
| 204 | 263 | let distinct: BTreeSet<NodeId> = group.iter().map(|(_, p)| p.node).collect(); |
|
| 205 | 264 | if distinct.len() > 1 { |
|
| 206 | - | view.conflicts.push(TreeConflict::Name { parent: dir, name: name.to_string(), nodes: distinct.iter().copied().collect() }); |
|
| 265 | + | view.conflicts.push(TreeConflict::Name { |
|
| 266 | + | parent: dir, |
|
| 267 | + | name: name.to_string(), |
|
| 268 | + | nodes: distinct.iter().copied().collect(), |
|
| 269 | + | }); |
|
| 207 | 270 | } |
|
| 208 | 271 | let suffixed = distinct.len() > 1; |
|
| 209 | 272 | for (i, (k, p)) in group.iter().enumerate() { |
|
| … | 3 unchanged lines |
||
| 213 | 276 | continue; |
|
| 214 | 277 | } |
|
| 215 | 278 | let needs_suffix = suffixed || multi.contains(&node); |
|
| 216 | - | let leaf = if needs_suffix { format!("{name}.oxy-conflict-{}", i + 1) } else { name.to_string() }; |
|
| 217 | - | let path = if dir_path.is_empty() { leaf } else { format!("{dir_path}/{leaf}") }; |
|
| 279 | + | let leaf = if needs_suffix { |
|
| 280 | + | format!("{name}.oxy-conflict-{}", i + 1) |
|
| 281 | + | } else { |
|
| 282 | + | name.to_string() |
|
| 283 | + | }; |
|
| 284 | + | let path = if dir_path.is_empty() { |
|
| 285 | + | leaf |
|
| 286 | + | } else { |
|
| 287 | + | format!("{dir_path}/{leaf}") |
|
| 288 | + | }; |
|
| 218 | 289 | let kind = self.kind(&node).unwrap_or(NodeKind::File); |
|
| 219 | 290 | let modes = self.live_modes(&node); |
|
| 220 | 291 | let mode_atom = modes.iter().map(|(k, _)| *k).max(); |
|
| 221 | - | let mode = mode_atom.and_then(|k| self.modes.get(&k).map(|(_, m)| *m)).unwrap_or_default(); |
|
| 222 | - | view.entries.insert(path.clone(), TreeEntry { path: path.clone(), node, kind, mode, placement: *k, mode_atom }); |
|
| 292 | + | let mode = mode_atom |
|
| 293 | + | .and_then(|k| self.modes.get(&k).map(|(_, m)| *m)) |
|
| 294 | + | .unwrap_or_default(); |
|
| 295 | + | view.entries.insert( |
|
| 296 | + | path.clone(), |
|
| 297 | + | TreeEntry { |
|
| 298 | + | path: path.clone(), |
|
| 299 | + | node, |
|
| 300 | + | kind, |
|
| 301 | + | mode, |
|
| 302 | + | placement: *k, |
|
| 303 | + | mode_atom, |
|
| 304 | + | }, |
|
| 305 | + | ); |
|
| 223 | 306 | if kind == NodeKind::Dir && reached.insert(node) { |
|
| 224 | 307 | on_path.insert(dir); |
|
| 225 | 308 | queue.push((node, path)); |
|
| … | 21 unchanged lines |
||
| 247 | 330 | } |
|
| 248 | 331 | let lp = self.live_placements(&cur); |
|
| 249 | 332 | let Some((_, p)) = lp.first() else { |
|
| 250 | - | break TreeConflict::Orphan { node: *n, parent: cur }; |
|
| 333 | + | break TreeConflict::Orphan { |
|
| 334 | + | node: *n, |
|
| 335 | + | parent: cur, |
|
| 336 | + | }; |
|
| 251 | 337 | }; |
|
| 252 | 338 | if reached.contains(&p.parent) { |
|
| 253 | 339 | // Parent reached but this node was not: the parent is not a |
|
| 254 | 340 | // directory, so this is a name/kind problem surfaced as orphan. |
|
| 255 | - | break TreeConflict::Orphan { node: *n, parent: p.parent }; |
|
| 341 | + | break TreeConflict::Orphan { |
|
| 342 | + | node: *n, |
|
| 343 | + | parent: p.parent, |
|
| 344 | + | }; |
|
| 256 | 345 | } |
|
| 257 | 346 | cur = p.parent; |
|
| 258 | 347 | }; |
|
| … | 17 unchanged lines |
||
| 276 | 365 | ks.into_iter() |
|
| 277 | 366 | .map(|k| { |
|
| 278 | 367 | let mut v = opkey(k); |
|
| 279 | - | v.push(Value::Array(m[k].iter().map(|d| Value::Bytes(d.0.to_vec())).collect())); |
|
| 368 | + | v.push(Value::Array( |
|
| 369 | + | m[k].iter().map(|d| Value::Bytes(d.0.to_vec())).collect(), |
|
| 370 | + | )); |
|
| 280 | 371 | Value::Array(v) |
|
| 281 | 372 | }) |
|
| 282 | 373 | .collect(), |
|
| … | 15 unchanged lines |
||
| 298 | 389 | Value::Array(vec![ |
|
| 299 | 390 | Value::Bytes(n.0.to_vec()), |
|
| 300 | 391 | Value::Uint(self.kinds[n].tag()), |
|
| 301 | - | self.introduced.get(n).map(|h| Value::Bytes(h.0.to_vec())).unwrap_or(Value::Null), |
|
| 392 | + | self.introduced |
|
| 393 | + | .get(n) |
|
| 394 | + | .map(|h| Value::Bytes(h.0.to_vec())) |
|
| 395 | + | .unwrap_or(Value::Null), |
|
| 302 | 396 | ]) |
|
| 303 | 397 | }) |
|
| 304 | 398 | .collect(), |
|
| … | 6 unchanged lines |
||
| 311 | 405 | .map(|k| { |
|
| 312 | 406 | let p = &self.placements[k]; |
|
| 313 | 407 | let mut v = opkey(k); |
|
| 314 | - | v.extend([Value::Bytes(p.node.0.to_vec()), Value::Bytes(p.parent.0.to_vec()), Value::Text(p.name.clone())]); |
|
| 408 | + | v.extend([ |
|
| 409 | + | Value::Bytes(p.node.0.to_vec()), |
|
| 410 | + | Value::Bytes(p.parent.0.to_vec()), |
|
| 411 | + | Value::Text(p.name.clone()), |
|
| 412 | + | ]); |
|
| 315 | 413 | Value::Array(v) |
|
| 316 | 414 | }) |
|
| 317 | 415 | .collect(), |
|
| … | 20 unchanged lines |
||
| 338 | 436 | pub fn from_value(v: &crate::cbor::Value) -> Result<TreeGraph, crate::objects::ObjectError> { |
|
| 339 | 437 | use crate::objects::{array, bytes, field, map, oref, text, u32_, uint, ObjectError}; |
|
| 340 | 438 | let m = map(v, "tree graph")?; |
|
| 341 | - | let node = |v: &crate::cbor::Value| NodeId::from_slice(bytes(v, "node")?).ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into())); |
|
| 342 | - | let opkey = |e: &[crate::cbor::Value]| -> Result<OpKey, ObjectError> { Ok((oref(&e[0], "op change")?, u32_(&e[1], "op index")?)) }; |
|
| 439 | + | let node = |v: &crate::cbor::Value| { |
|
| 440 | + | NodeId::from_slice(bytes(v, "node")?) |
|
| 441 | + | .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into())) |
|
| 442 | + | }; |
|
| 443 | + | let opkey = |e: &[crate::cbor::Value]| -> Result<OpKey, ObjectError> { |
|
| 444 | + | Ok((oref(&e[0], "op change")?, u32_(&e[1], "op index")?)) |
|
| 445 | + | }; |
|
| 343 | 446 | let mut g = TreeGraph::default(); |
|
| 344 | 447 | for n in array(field(m, 0, "nodes")?, "nodes")? { |
|
| 345 | 448 | let e = array(n, "node entry")?; |
|
| … | 1 unchanged lines |
||
| 347 | 450 | return Err(ObjectError::Schema("node entry: expected 3 fields".into())); |
|
| 348 | 451 | } |
|
| 349 | 452 | let id = node(&e[0])?; |
|
| 350 | - | g.kinds.insert(id, NodeKind::from_tag(uint(&e[1], "kind")?)?); |
|
| 453 | + | g.kinds |
|
| 454 | + | .insert(id, NodeKind::from_tag(uint(&e[1], "kind")?)?); |
|
| 351 | 455 | if !e[2].is_null() { |
|
| 352 | 456 | g.introduced.insert(id, oref(&e[2], "introducer")?); |
|
| 353 | 457 | } |
|
| … | 4 unchanged lines |
||
| 358 | 462 | return Err(ObjectError::Schema("placement: expected 5 fields".into())); |
|
| 359 | 463 | } |
|
| 360 | 464 | let k = opkey(e)?; |
|
| 361 | - | let pl = Placement { node: node(&e[2])?, parent: node(&e[3])?, name: text(&e[4], "name")?.to_owned() }; |
|
| 465 | + | let pl = Placement { |
|
| 466 | + | node: node(&e[2])?, |
|
| 467 | + | parent: node(&e[3])?, |
|
| 468 | + | name: text(&e[4], "name")?.to_owned(), |
|
| 469 | + | }; |
|
| 362 | 470 | g.placements_of.entry(pl.node).or_default().insert(k); |
|
| 363 | 471 | g.placements.insert(k, pl); |
|
| 364 | 472 | } |
|
| 365 | - | let deleters = |v: &crate::cbor::Value, what: &str| -> Result<HashMap<OpKey, BTreeSet<ChangeHash>>, ObjectError> { |
|
| 473 | + | let deleters = |v: &crate::cbor::Value, |
|
| 474 | + | what: &str| |
|
| 475 | + | -> Result<HashMap<OpKey, BTreeSet<ChangeHash>>, ObjectError> { |
|
| 366 | 476 | let mut out = HashMap::new(); |
|
| 367 | 477 | for d in array(v, what)? { |
|
| 368 | 478 | let e = array(d, what)?; |
|
| 369 | 479 | if e.len() != 3 { |
|
| 370 | 480 | return Err(ObjectError::Schema(format!("{what}: expected 3 fields"))); |
|
| 371 | 481 | } |
|
| 372 | - | out.insert(opkey(e)?, array(&e[2], "deleters")?.iter().map(|x| oref(x, "deleter")).collect::<Result<BTreeSet<_>, _>>()?); |
|
| 482 | + | out.insert( |
|
| 483 | + | opkey(e)?, |
|
| 484 | + | array(&e[2], "deleters")? |
|
| 485 | + | .iter() |
|
| 486 | + | .map(|x| oref(x, "deleter")) |
|
| 487 | + | .collect::<Result<BTreeSet<_>, _>>()?, |
|
| 488 | + | ); |
|
| 373 | 489 | } |
|
| 374 | 490 | Ok(out) |
|
| 375 | 491 | }; |
|
| … | 5 unchanged lines |
||
| 381 | 497 | } |
|
| 382 | 498 | let k = opkey(e)?; |
|
| 383 | 499 | let n = node(&e[2])?; |
|
| 384 | - | g.modes.insert(k, (n, Mode::from_bits(uint(&e[3], "mode bits")?)?)); |
|
| 500 | + | g.modes |
|
| 501 | + | .insert(k, (n, Mode::from_bits(uint(&e[3], "mode bits")?)?)); |
|
| 385 | 502 | g.modes_of.entry(n).or_default().insert(k); |
|
| 386 | 503 | } |
|
| 387 | 504 | g.deleted_modes = deleters(field(m, 4, "deleted modes")?, "deleted mode")?; |
|
| … | 17 unchanged lines |
||
| 405 | 522 | NodeId([n; 16]) |
|
| 406 | 523 | } |
|
| 407 | 524 | fn add(g: &mut TreeGraph, c: u8, i: u32, node: u8, kind: NodeKind, parent: NodeId, name: &str) { |
|
| 408 | - | g.apply(h(c), i, &TreeOp::AddNode { node: nid(node), kind, mode: Mode::TEXT, parent, name: name.into() }); |
|
| 525 | + | g.apply( |
|
| 526 | + | h(c), |
|
| 527 | + | i, |
|
| 528 | + | &TreeOp::AddNode { |
|
| 529 | + | node: nid(node), |
|
| 530 | + | kind, |
|
| 531 | + | mode: Mode::TEXT, |
|
| 532 | + | parent, |
|
| 533 | + | name: name.into(), |
|
| 534 | + | }, |
|
| 535 | + | ); |
|
| 409 | 536 | } |
|
| 410 | 537 | fn paths(v: &TreeView) -> Vec<String> { |
|
| 411 | 538 | v.entries.keys().cloned().collect() |
|
| … | 10 unchanged lines |
||
| 422 | 549 | assert!(!v.is_conflicted()); |
|
| 423 | 550 | ||
| 424 | 551 | // Rename src/main.rs → src/lib.rs, move src → lib (subtree follows). |
|
| 425 | - | g.apply(h(2), 0, &TreeOp::Move { node: nid(2), old: OpRef { change: h(1), index: 1 }, parent: nid(1), name: "lib.rs".into() }); |
|
| 426 | - | g.apply(h(2), 1, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: NodeId::ROOT, name: "lib".into() }); |
|
| 552 | + | g.apply( |
|
| 553 | + | h(2), |
|
| 554 | + | 0, |
|
| 555 | + | &TreeOp::Move { |
|
| 556 | + | node: nid(2), |
|
| 557 | + | old: OpRef { |
|
| 558 | + | change: h(1), |
|
| 559 | + | index: 1, |
|
| 560 | + | }, |
|
| 561 | + | parent: nid(1), |
|
| 562 | + | name: "lib.rs".into(), |
|
| 563 | + | }, |
|
| 564 | + | ); |
|
| 565 | + | g.apply( |
|
| 566 | + | h(2), |
|
| 567 | + | 1, |
|
| 568 | + | &TreeOp::Move { |
|
| 569 | + | node: nid(1), |
|
| 570 | + | old: OpRef { |
|
| 571 | + | change: h(1), |
|
| 572 | + | index: 0, |
|
| 573 | + | }, |
|
| 574 | + | parent: NodeId::ROOT, |
|
| 575 | + | name: "lib".into(), |
|
| 576 | + | }, |
|
| 577 | + | ); |
|
| 427 | 578 | let v = g.view(); |
|
| 428 | 579 | assert_eq!(paths(&v), ["README", "lib", "lib/lib.rs"]); |
|
| 429 | 580 | assert_eq!(v.entries["lib/lib.rs"].placement, (h(2), 0)); |
|
| 430 | 581 | ||
| 431 | - | g.apply(h(3), 0, &TreeOp::RemoveNode { node: nid(3), placement: OpRef { change: h(1), index: 2 } }); |
|
| 582 | + | g.apply( |
|
| 583 | + | h(3), |
|
| 584 | + | 0, |
|
| 585 | + | &TreeOp::RemoveNode { |
|
| 586 | + | node: nid(3), |
|
| 587 | + | placement: OpRef { |
|
| 588 | + | change: h(1), |
|
| 589 | + | index: 2, |
|
| 590 | + | }, |
|
| 591 | + | }, |
|
| 592 | + | ); |
|
| 432 | 593 | assert_eq!(paths(&g.view()), ["lib", "lib/lib.rs"]); |
|
| 433 | 594 | } |
|
| 434 | 595 | ||
| … | 4 unchanged lines |
||
| 439 | 600 | add(&mut g, 2, 0, 2, NodeKind::File, NodeId::ROOT, "a.txt"); |
|
| 440 | 601 | let v = g.view(); |
|
| 441 | 602 | assert_eq!(paths(&v), ["a.txt.oxy-conflict-1", "a.txt.oxy-conflict-2"]); |
|
| 442 | - | assert_eq!(v.conflicts, vec![TreeConflict::Name { parent: NodeId::ROOT, name: "a.txt".into(), nodes: vec![nid(1), nid(2)] }]); |
|
| 603 | + | assert_eq!( |
|
| 604 | + | v.conflicts, |
|
| 605 | + | vec![TreeConflict::Name { |
|
| 606 | + | parent: NodeId::ROOT, |
|
| 607 | + | name: "a.txt".into(), |
|
| 608 | + | nodes: vec![nid(1), nid(2)] |
|
| 609 | + | }] |
|
| 610 | + | ); |
|
| 443 | 611 | } |
|
| 444 | 612 | ||
| 445 | 613 | #[test] |
|
| 446 | 614 | fn concurrent_renames_are_multi_placement() { |
|
| 447 | 615 | let mut g = TreeGraph::new(); |
|
| 448 | 616 | add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "a"); |
|
| 449 | - | g.apply(h(2), 0, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: NodeId::ROOT, name: "b".into() }); |
|
| 450 | - | g.apply(h(3), 0, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: NodeId::ROOT, name: "c".into() }); |
|
| 617 | + | g.apply( |
|
| 618 | + | h(2), |
|
| 619 | + | 0, |
|
| 620 | + | &TreeOp::Move { |
|
| 621 | + | node: nid(1), |
|
| 622 | + | old: OpRef { |
|
| 623 | + | change: h(1), |
|
| 624 | + | index: 0, |
|
| 625 | + | }, |
|
| 626 | + | parent: NodeId::ROOT, |
|
| 627 | + | name: "b".into(), |
|
| 628 | + | }, |
|
| 629 | + | ); |
|
| 630 | + | g.apply( |
|
| 631 | + | h(3), |
|
| 632 | + | 0, |
|
| 633 | + | &TreeOp::Move { |
|
| 634 | + | node: nid(1), |
|
| 635 | + | old: OpRef { |
|
| 636 | + | change: h(1), |
|
| 637 | + | index: 0, |
|
| 638 | + | }, |
|
| 639 | + | parent: NodeId::ROOT, |
|
| 640 | + | name: "c".into(), |
|
| 641 | + | }, |
|
| 642 | + | ); |
|
| 451 | 643 | let v = g.view(); |
|
| 452 | 644 | assert_eq!(paths(&v), ["b.oxy-conflict-1", "c.oxy-conflict-1"]); |
|
| 453 | - | assert!(matches!(v.conflicts[0], TreeConflict::MultiPlacement { node, .. } if node == nid(1))); |
|
| 645 | + | assert!( |
|
| 646 | + | matches!(v.conflicts[0], TreeConflict::MultiPlacement { node, .. } if node == nid(1)) |
|
| 647 | + | ); |
|
| 454 | 648 | } |
|
| 455 | 649 | ||
| 456 | 650 | #[test] |
|
| … | 2 unchanged lines |
||
| 459 | 653 | add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "d"); |
|
| 460 | 654 | add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "f"); |
|
| 461 | 655 | // Concurrently: 2 removes d, 3 adds d/g. |
|
| 462 | - | g.apply(h(2), 0, &TreeOp::RemoveNode { node: nid(1), placement: OpRef { change: h(1), index: 0 } }); |
|
| 656 | + | g.apply( |
|
| 657 | + | h(2), |
|
| 658 | + | 0, |
|
| 659 | + | &TreeOp::RemoveNode { |
|
| 660 | + | node: nid(1), |
|
| 661 | + | placement: OpRef { |
|
| 662 | + | change: h(1), |
|
| 663 | + | index: 0, |
|
| 664 | + | }, |
|
| 665 | + | }, |
|
| 666 | + | ); |
|
| 463 | 667 | add(&mut g, 3, 0, 3, NodeKind::File, nid(1), "g"); |
|
| 464 | 668 | let v = g.view(); |
|
| 465 | 669 | assert_eq!(paths(&v), Vec::<String>::new()); |
|
| 466 | - | assert!(v.conflicts.contains(&TreeConflict::Orphan { node: nid(2), parent: nid(1) })); |
|
| 467 | - | assert!(v.conflicts.contains(&TreeConflict::Orphan { node: nid(3), parent: nid(1) })); |
|
| 670 | + | assert!(v.conflicts.contains(&TreeConflict::Orphan { |
|
| 671 | + | node: nid(2), |
|
| 672 | + | parent: nid(1) |
|
| 673 | + | })); |
|
| 674 | + | assert!(v.conflicts.contains(&TreeConflict::Orphan { |
|
| 675 | + | node: nid(3), |
|
| 676 | + | parent: nid(1) |
|
| 677 | + | })); |
|
| 468 | 678 | ||
| 469 | 679 | // Cycle: a/b, then concurrently move a under b and b under a's parent... simplest: move a into b. |
|
| 470 | 680 | let mut g = TreeGraph::new(); |
|
| 471 | 681 | add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "a"); |
|
| 472 | 682 | add(&mut g, 1, 1, 2, NodeKind::Dir, nid(1), "b"); |
|
| 473 | - | g.apply(h(2), 0, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: nid(2), name: "a".into() }); |
|
| 683 | + | g.apply( |
|
| 684 | + | h(2), |
|
| 685 | + | 0, |
|
| 686 | + | &TreeOp::Move { |
|
| 687 | + | node: nid(1), |
|
| 688 | + | old: OpRef { |
|
| 689 | + | change: h(1), |
|
| 690 | + | index: 0, |
|
| 691 | + | }, |
|
| 692 | + | parent: nid(2), |
|
| 693 | + | name: "a".into(), |
|
| 694 | + | }, |
|
| 695 | + | ); |
|
| 474 | 696 | let v = g.view(); |
|
| 475 | - | assert!(v.conflicts.iter().any(|c| matches!(c, TreeConflict::Cycle { .. })), "{:?}", v.conflicts); |
|
| 697 | + | assert!( |
|
| 698 | + | v.conflicts |
|
| 699 | + | .iter() |
|
| 700 | + | .any(|c| matches!(c, TreeConflict::Cycle { .. })), |
|
| 701 | + | "{:?}", |
|
| 702 | + | v.conflicts |
|
| 703 | + | ); |
|
| 476 | 704 | } |
|
| 477 | 705 | ||
| 478 | 706 | #[test] |
|
| … | 1 unchanged lines |
||
| 480 | 708 | let mut g = TreeGraph::new(); |
|
| 481 | 709 | add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "src"); |
|
| 482 | 710 | add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "main.rs"); |
|
| 483 | - | g.apply(h(2), 0, &TreeOp::Move { node: nid(2), old: OpRef { change: h(1), index: 1 }, parent: nid(1), name: "lib.rs".into() }); |
|
| 484 | - | g.apply(h(3), 0, &TreeOp::SetMode { node: nid(2), old: OpRef { change: h(1), index: 1 }, mode: Mode { exec: true, opaque: false } }); |
|
| 711 | + | g.apply( |
|
| 712 | + | h(2), |
|
| 713 | + | 0, |
|
| 714 | + | &TreeOp::Move { |
|
| 715 | + | node: nid(2), |
|
| 716 | + | old: OpRef { |
|
| 717 | + | change: h(1), |
|
| 718 | + | index: 1, |
|
| 719 | + | }, |
|
| 720 | + | parent: nid(1), |
|
| 721 | + | name: "lib.rs".into(), |
|
| 722 | + | }, |
|
| 723 | + | ); |
|
| 724 | + | g.apply( |
|
| 725 | + | h(3), |
|
| 726 | + | 0, |
|
| 727 | + | &TreeOp::SetMode { |
|
| 728 | + | node: nid(2), |
|
| 729 | + | old: OpRef { |
|
| 730 | + | change: h(1), |
|
| 731 | + | index: 1, |
|
| 732 | + | }, |
|
| 733 | + | mode: Mode { |
|
| 734 | + | exec: true, |
|
| 735 | + | opaque: false, |
|
| 736 | + | }, |
|
| 737 | + | }, |
|
| 738 | + | ); |
|
| 485 | 739 | let bytes = crate::cbor::encode(&g.to_value()); |
|
| 486 | 740 | let back = TreeGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap(); |
|
| 487 | 741 | assert_eq!(back.view(), g.view()); |
|
| … | 5 unchanged lines |
||
| 493 | 747 | fn mode_conflict_and_resolution() { |
|
| 494 | 748 | let mut g = TreeGraph::new(); |
|
| 495 | 749 | add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "run.sh"); |
|
| 496 | - | g.apply(h(2), 0, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(1), index: 0 }, mode: Mode { exec: true, opaque: false } }); |
|
| 497 | - | g.apply(h(3), 0, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(1), index: 0 }, mode: Mode { exec: false, opaque: true } }); |
|
| 750 | + | g.apply( |
|
| 751 | + | h(2), |
|
| 752 | + | 0, |
|
| 753 | + | &TreeOp::SetMode { |
|
| 754 | + | node: nid(1), |
|
| 755 | + | old: OpRef { |
|
| 756 | + | change: h(1), |
|
| 757 | + | index: 0, |
|
| 758 | + | }, |
|
| 759 | + | mode: Mode { |
|
| 760 | + | exec: true, |
|
| 761 | + | opaque: false, |
|
| 762 | + | }, |
|
| 763 | + | }, |
|
| 764 | + | ); |
|
| 765 | + | g.apply( |
|
| 766 | + | h(3), |
|
| 767 | + | 0, |
|
| 768 | + | &TreeOp::SetMode { |
|
| 769 | + | node: nid(1), |
|
| 770 | + | old: OpRef { |
|
| 771 | + | change: h(1), |
|
| 772 | + | index: 0, |
|
| 773 | + | }, |
|
| 774 | + | mode: Mode { |
|
| 775 | + | exec: false, |
|
| 776 | + | opaque: true, |
|
| 777 | + | }, |
|
| 778 | + | }, |
|
| 779 | + | ); |
|
| 498 | 780 | let v = g.view(); |
|
| 499 | 781 | assert!(matches!(v.conflicts[0], TreeConflict::Mode { .. })); |
|
| 500 | 782 | // Materialized with the greatest mode atom. |
|
| 501 | - | assert_eq!(v.entries["run.sh"].mode, Mode { exec: false, opaque: true }); |
|
| 783 | + | assert_eq!( |
|
| 784 | + | v.entries["run.sh"].mode, |
|
| 785 | + | Mode { |
|
| 786 | + | exec: false, |
|
| 787 | + | opaque: true |
|
| 788 | + | } |
|
| 789 | + | ); |
|
| 502 | 790 | // The placement introduced by the same AddNode is unaffected. |
|
| 503 | 791 | assert_eq!(v.entries["run.sh"].placement, (h(1), 0)); |
|
| 504 | 792 | // Resolution: retire both live mode atoms; identical live values do not conflict. |
|
| 505 | - | g.apply(h(4), 0, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(2), index: 0 }, mode: Mode { exec: true, opaque: false } }); |
|
| 506 | - | g.apply(h(4), 1, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(3), index: 0 }, mode: Mode { exec: true, opaque: false } }); |
|
| 793 | + | g.apply( |
|
| 794 | + | h(4), |
|
| 795 | + | 0, |
|
| 796 | + | &TreeOp::SetMode { |
|
| 797 | + | node: nid(1), |
|
| 798 | + | old: OpRef { |
|
| 799 | + | change: h(2), |
|
| 800 | + | index: 0, |
|
| 801 | + | }, |
|
| 802 | + | mode: Mode { |
|
| 803 | + | exec: true, |
|
| 804 | + | opaque: false, |
|
| 805 | + | }, |
|
| 806 | + | }, |
|
| 807 | + | ); |
|
| 808 | + | g.apply( |
|
| 809 | + | h(4), |
|
| 810 | + | 1, |
|
| 811 | + | &TreeOp::SetMode { |
|
| 812 | + | node: nid(1), |
|
| 813 | + | old: OpRef { |
|
| 814 | + | change: h(3), |
|
| 815 | + | index: 0, |
|
| 816 | + | }, |
|
| 817 | + | mode: Mode { |
|
| 818 | + | exec: true, |
|
| 819 | + | opaque: false, |
|
| 820 | + | }, |
|
| 821 | + | }, |
|
| 822 | + | ); |
|
| 507 | 823 | let v = g.view(); |
|
| 508 | 824 | assert!(!v.is_conflicted(), "{:?}", v.conflicts); |
|
| 509 | - | assert_eq!(v.entries["run.sh"].mode, Mode { exec: true, opaque: false }); |
|
| 825 | + | assert_eq!( |
|
| 826 | + | v.entries["run.sh"].mode, |
|
| 827 | + | Mode { |
|
| 828 | + | exec: true, |
|
| 829 | + | opaque: false |
|
| 830 | + | } |
|
| 831 | + | ); |
|
| 510 | 832 | } |
|
| 511 | 833 | } |
modified crates/oxy-server/src/lib.rs +96 −18
| … | 150 unchanged lines |
||
| 151 | 151 | pub fn valid_repo_name(name: &str) -> bool { |
|
| 152 | 152 | !name.is_empty() |
|
| 153 | 153 | && !name.starts_with('.') |
|
| 154 | - | && name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) |
|
| 154 | + | && name |
|
| 155 | + | .chars() |
|
| 156 | + | .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) |
|
| 155 | 157 | } |
|
| 156 | 158 | ||
| 157 | 159 | impl Server { |
|
| 158 | 160 | pub fn new(backend: impl Backend) -> Arc<Server> { |
|
| 159 | - | Arc::new(Server { backend: Box::new(backend), locks: Mutex::new(HashMap::new()) }) |
|
| 161 | + | Arc::new(Server { |
|
| 162 | + | backend: Box::new(backend), |
|
| 163 | + | locks: Mutex::new(HashMap::new()), |
|
| 164 | + | }) |
|
| 160 | 165 | } |
|
| 161 | 166 | ||
| 162 | 167 | pub fn backend(&self) -> &dyn Backend { |
|
| … | 1 unchanged lines |
||
| 164 | 169 | } |
|
| 165 | 170 | ||
| 166 | 171 | fn lock_for(&self, name: &str) -> Arc<Mutex<()>> { |
|
| 167 | - | self.locks.lock().unwrap().entry(name.to_string()).or_default().clone() |
|
| 172 | + | self.locks |
|
| 173 | + | .lock() |
|
| 174 | + | .unwrap() |
|
| 175 | + | .entry(name.to_string()) |
|
| 176 | + | .or_default() |
|
| 177 | + | .clone() |
|
| 168 | 178 | } |
|
| 169 | 179 | ||
| 170 | 180 | /// Answer one protocol request for repository `name`. `method` is |
|
| 171 | 181 | /// checked against the endpoint; the body is the raw CBOR request. |
|
| 172 | - | pub async fn handle(self: &Arc<Self>, name: String, endpoint: Endpoint, method: &Method, headers: &HeaderMap, body: Bytes) -> Response { |
|
| 182 | + | pub async fn handle( |
|
| 183 | + | self: &Arc<Self>, |
|
| 184 | + | name: String, |
|
| 185 | + | endpoint: Endpoint, |
|
| 186 | + | method: &Method, |
|
| 187 | + | headers: &HeaderMap, |
|
| 188 | + | body: Bytes, |
|
| 189 | + | ) -> Response { |
|
| 173 | 190 | if *method != endpoint.method() { |
|
| 174 | - | return Failure(StatusCode::METHOD_NOT_ALLOWED, format!("{} is {}", endpoint.name(), endpoint.method())).into_response(); |
|
| 191 | + | return Failure( |
|
| 192 | + | StatusCode::METHOD_NOT_ALLOWED, |
|
| 193 | + | format!("{} is {}", endpoint.name(), endpoint.method()), |
|
| 194 | + | ) |
|
| 195 | + | .into_response(); |
|
| 175 | 196 | } |
|
| 176 | 197 | if let Err(f) = self.backend.authorize(&name, endpoint.access(), headers) { |
|
| 177 | 198 | return f.into_response(); |
|
| 178 | 199 | } |
|
| 200 | + | let claimed = headers |
|
| 201 | + | .get(oxy_core::protocol::REPO_ID_HEADER) |
|
| 202 | + | .and_then(|v| v.to_str().ok()) |
|
| 203 | + | .and_then(hex_id); |
|
| 179 | 204 | let server = Arc::clone(self); |
|
| 180 | - | let result = tokio::task::spawn_blocking(move || server.respond(&name, endpoint, body)).await; |
|
| 205 | + | let result = |
|
| 206 | + | tokio::task::spawn_blocking(move || server.respond(&name, endpoint, body, claimed)).await; |
|
| 181 | 207 | match result { |
|
| 182 | 208 | Ok(Ok(r)) => r, |
|
| 183 | 209 | Ok(Err(f)) => f.into_response(), |
|
| … | 1 unchanged lines |
||
| 185 | 211 | } |
|
| 186 | 212 | } |
|
| 187 | 213 | ||
| 188 | - | fn respond(&self, name: &str, endpoint: Endpoint, body: Bytes) -> Result<Response, Failure> { |
|
| 214 | + | fn respond(&self, name: &str, endpoint: Endpoint, body: Bytes, claimed: Option<[u8; 16]>) -> Result<Response, Failure> { |
|
| 189 | 215 | let r = self.backend.open(name)?; |
|
| 190 | 216 | Ok(match endpoint { |
|
| 191 | 217 | Endpoint::Capabilities => ok(wire::encode_capabilities(r.id()?)), |
|
| … | 4 unchanged lines |
||
| 196 | 222 | } |
|
| 197 | 223 | Endpoint::Fetch => { |
|
| 198 | 224 | let (changes, max_blob) = wire::decode_fetch(&body)?; |
|
| 199 | - | ok(wire::encode_bundle(&r.bundle_for_filtered(&changes, max_blob)?)) |
|
| 225 | + | ok(wire::encode_bundle( |
|
| 226 | + | &r.bundle_for_filtered(&changes, max_blob)?, |
|
| 227 | + | )) |
|
| 200 | 228 | } |
|
| 201 | 229 | Endpoint::Objects => { |
|
| 202 | 230 | let refs = wire::decode_hashes(&body)?; |
|
| 203 | - | let bundle = oxy_core::protocol::Bundle { objects: r.objects_for(&refs)?, ..Default::default() }; |
|
| 231 | + | let bundle = oxy_core::protocol::Bundle { |
|
| 232 | + | objects: r.objects_for(&refs)?, |
|
| 233 | + | ..Default::default() |
|
| 234 | + | }; |
|
| 204 | 235 | ok(wire::encode_bundle(&bundle)) |
|
| 205 | 236 | } |
|
| 206 | 237 | Endpoint::Checkpoint => { |
|
| … | 10 unchanged lines |
||
| 217 | 248 | let (bundle, updates, policy, tags) = wire::decode_push(&body)?; |
|
| 218 | 249 | let lock = self.lock_for(name); |
|
| 219 | 250 | let _guard = lock.lock().unwrap(); |
|
| 251 | + | if r.is_unbound() { |
|
| 252 | + | // The first push binds the repository to the pusher's id (spec §16.1). |
|
| 253 | + | match claimed { |
|
| 254 | + | Some(id) => r.bind_id(id)?, |
|
| 255 | + | None => { |
|
| 256 | + | return Err(Failure( |
|
| 257 | + | StatusCode::BAD_REQUEST, |
|
| 258 | + | format!("this repository is unbound; push with a client that sends {}", oxy_core::protocol::REPO_ID_HEADER), |
|
| 259 | + | )) |
|
| 260 | + | } |
|
| 261 | + | } |
|
| 262 | + | } |
|
| 220 | 263 | let results = r.receive_push(bundle, updates, policy, tags)?; |
|
| 221 | 264 | self.backend.after_push(name, &results); |
|
| 222 | 265 | ok(wire::encode_push_response(&results)) |
|
| … | 27 unchanged lines |
||
| 250 | 293 | } |
|
| 251 | 294 | match bearer_token(headers) { |
|
| 252 | 295 | Some(t) if self.tokens.iter().any(|x| x == t) => Ok(()), |
|
| 253 | - | _ => Err(Failure(StatusCode::UNAUTHORIZED, "a valid bearer token is required to push".into())), |
|
| 296 | + | _ => Err(Failure( |
|
| 297 | + | StatusCode::UNAUTHORIZED, |
|
| 298 | + | "a valid bearer token is required to push".into(), |
|
| 299 | + | )), |
|
| 254 | 300 | } |
|
| 255 | 301 | } |
|
| 256 | 302 | } |
|
| 257 | 303 | ||
| 304 | + | fn hex_id(h: &str) -> Option<[u8; 16]> { |
|
| 305 | + | let h = h.trim(); |
|
| 306 | + | if h.len() != 32 { |
|
| 307 | + | return None; |
|
| 308 | + | } |
|
| 309 | + | let mut out = [0u8; 16]; |
|
| 310 | + | for (i, byte) in out.iter_mut().enumerate() { |
|
| 311 | + | *byte = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok()?; |
|
| 312 | + | } |
|
| 313 | + | Some(out) |
|
| 314 | + | } |
|
| 315 | + | ||
| 258 | 316 | /// The token of an `Authorization: Bearer` header, if any. |
|
| 259 | 317 | pub fn bearer_token(headers: &HeaderMap) -> Option<&str> { |
|
| 260 | - | headers.get(header::AUTHORIZATION).and_then(|v| v.to_str().ok()).and_then(|v| v.strip_prefix("Bearer ")).map(str::trim) |
|
| 318 | + | headers |
|
| 319 | + | .get(header::AUTHORIZATION) |
|
| 320 | + | .and_then(|v| v.to_str().ok()) |
|
| 321 | + | .and_then(|v| v.strip_prefix("Bearer ")) |
|
| 322 | + | .map(str::trim) |
|
| 261 | 323 | } |
|
| 262 | 324 | ||
| 263 | - | async fn dispatch(State(server): State<Arc<Server>>, Path((repo, endpoint)): Path<(String, String)>, method: Method, headers: HeaderMap, body: Bytes) -> Response { |
|
| 325 | + | async fn dispatch( |
|
| 326 | + | State(server): State<Arc<Server>>, |
|
| 327 | + | Path((repo, endpoint)): Path<(String, String)>, |
|
| 328 | + | method: Method, |
|
| 329 | + | headers: HeaderMap, |
|
| 330 | + | body: Bytes, |
|
| 331 | + | ) -> Response { |
|
| 264 | 332 | match Endpoint::parse(&endpoint) { |
|
| 265 | 333 | Some(e) => server.handle(repo, e, &method, &headers, body).await, |
|
| 266 | 334 | None => Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response(), |
|
| … | 2 unchanged lines |
||
| 269 | 337 | ||
| 270 | 338 | /// The router of `oxy serve`: `/{repo}/oxy/v1/{endpoint}` over a [`DirBackend`]. |
|
| 271 | 339 | pub fn router(cfg: ServerConfig) -> Router { |
|
| 272 | - | let server = Server::new(DirBackend { root: cfg.root, tokens: cfg.tokens }); |
|
| 273 | - | Router::new().route("/{repo}/oxy/v1/{endpoint}", any(dispatch)).with_state(server) |
|
| 340 | + | let server = Server::new(DirBackend { |
|
| 341 | + | root: cfg.root, |
|
| 342 | + | tokens: cfg.tokens, |
|
| 343 | + | }); |
|
| 344 | + | Router::new() |
|
| 345 | + | .route("/{repo}/oxy/v1/{endpoint}", any(dispatch)) |
|
| 346 | + | .with_state(server) |
|
| 274 | 347 | } |
|
| 275 | 348 | ||
| 276 | 349 | /// Serve on an already-bound listener until the process ends; HTTPS when |
|
| … | 3 unchanged lines |
||
| 280 | 353 | match cfg.tls.clone() { |
|
| 281 | 354 | Some(tls) => { |
|
| 282 | 355 | let _ = rustls::crypto::ring::default_provider().install_default(); |
|
| 283 | - | let rustls_cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key) |
|
| 356 | + | let rustls_cfg = |
|
| 357 | + | axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key) |
|
| 358 | + | .await |
|
| 359 | + | .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?; |
|
| 360 | + | axum_server::from_tcp_rustls(listener, rustls_cfg)? |
|
| 361 | + | .serve(router(cfg).into_make_service()) |
|
| 284 | 362 | .await |
|
| 285 | - | .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?; |
|
| 286 | - | axum_server::from_tcp_rustls(listener, rustls_cfg)?.serve(router(cfg).into_make_service()).await |
|
| 287 | 363 | } |
|
| 288 | 364 | None => { |
|
| 289 | 365 | let listener = tokio::net::TcpListener::from_std(listener)?; |
|
| … | 4 unchanged lines |
||
| 294 | 370 | ||
| 295 | 371 | /// Build a runtime and serve; for the CLI and for tests on a background thread. |
|
| 296 | 372 | pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> { |
|
| 297 | - | let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; |
|
| 373 | + | let rt = tokio::runtime::Builder::new_multi_thread() |
|
| 374 | + | .enable_all() |
|
| 375 | + | .build()?; |
|
| 298 | 376 | rt.block_on(serve(listener, cfg)) |
|
| 299 | 377 | } |
modified crates/oxy-server/tests/http.rs +181 −32
| … | 30 unchanged lines |
||
| 31 | 31 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); |
|
| 32 | 32 | let port = listener.local_addr().unwrap().port(); |
|
| 33 | 33 | let scheme = if tls.is_some() { "https" } else { "http" }; |
|
| 34 | - | let cfg = oxy_server::ServerConfig { root: root.to_path_buf(), tokens, tls }; |
|
| 34 | + | let cfg = oxy_server::ServerConfig { |
|
| 35 | + | root: root.to_path_buf(), |
|
| 36 | + | tokens, |
|
| 37 | + | tls, |
|
| 38 | + | }; |
|
| 35 | 39 | std::thread::spawn(move || oxy_server::run_blocking(listener, cfg).unwrap()); |
|
| 36 | 40 | // Give the acceptor a moment; the first request otherwise races the bind. |
|
| 37 | 41 | std::thread::sleep(std::time::Duration::from_millis(50)); |
|
| … | 3 unchanged lines |
||
| 41 | 45 | /// A self-signed certificate for 127.0.0.1 written to `dir`: (cert, key). |
|
| 42 | 46 | fn self_signed(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf) { |
|
| 43 | 47 | fs::create_dir_all(dir).unwrap(); |
|
| 44 | - | let ck = rcgen::generate_simple_self_signed(vec!["127.0.0.1".into(), "localhost".into()]).unwrap(); |
|
| 48 | + | let ck = |
|
| 49 | + | rcgen::generate_simple_self_signed(vec!["127.0.0.1".into(), "localhost".into()]).unwrap(); |
|
| 45 | 50 | let cert = dir.join("cert.pem"); |
|
| 46 | 51 | let key = dir.join("key.pem"); |
|
| 47 | 52 | fs::write(&cert, ck.cert.pem()).unwrap(); |
|
| … | 11 unchanged lines |
||
| 59 | 64 | let base = start(&served, vec![]); |
|
| 60 | 65 | let url = format!("{base}/origin"); |
|
| 61 | 66 | ||
| 62 | - | let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home")); |
|
| 63 | - | let (key, _) = a.create_identity("Ada", "[email protected]", oxy_core::repo::IdentityScope::Repo).unwrap(); |
|
| 67 | + | let mut a = Repo::clone(&url, &root.join("a")) |
|
| 68 | + | .unwrap() |
|
| 69 | + | .with_home(root.join("home")); |
|
| 70 | + | let (key, _) = a |
|
| 71 | + | .create_identity( |
|
| 72 | + | "Ada", |
|
| 73 | + | "[email protected]", |
|
| 74 | + | oxy_core::repo::IdentityScope::Repo, |
|
| 75 | + | ) |
|
| 76 | + | .unwrap(); |
|
| 64 | 77 | write(&root.join("a"), "f.txt", b"hello\n"); |
|
| 65 | 78 | let h1 = a.commit("hello", &[]).unwrap(); |
|
| 66 | 79 | let r = a.push("origin", false).unwrap(); |
|
| … | 1 unchanged lines |
||
| 68 | 81 | ||
| 69 | 82 | let origin = Repo::open(&served.join("origin")).unwrap(); |
|
| 70 | 83 | assert_eq!(origin.bookmark_heads().unwrap(), vec![h1]); |
|
| 71 | - | assert!(matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key: k, sealed: true, .. } if k == key)); |
|
| 84 | + | assert!( |
|
| 85 | + | matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key: k, sealed: true, .. } if k == key) |
|
| 86 | + | ); |
|
| 72 | 87 | ||
| 73 | 88 | let b = Repo::clone(&url, &root.join("b")).unwrap(); |
|
| 74 | 89 | assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"hello\n"); |
|
| … | 11 unchanged lines |
||
| 86 | 101 | assert_eq!(heads, want); |
|
| 87 | 102 | ||
| 88 | 103 | // Unknown repositories and names the server refuses are surfaced as errors. |
|
| 89 | - | let err = Repo::clone(&format!("{base}/nope"), &root.join("c")).err().unwrap().to_string(); |
|
| 104 | + | let err = Repo::clone(&format!("{base}/nope"), &root.join("c")) |
|
| 105 | + | .err() |
|
| 106 | + | .unwrap() |
|
| 107 | + | .to_string(); |
|
| 90 | 108 | assert!(err.contains("404"), "{err}"); |
|
| 91 | 109 | fs::create_dir_all(served.join(".hidden/.oxy")).unwrap(); |
|
| 92 | - | assert!(Repo::clone(&format!("{base}/.hidden"), &root.join("d")).err().unwrap().to_string().contains("404")); |
|
| 110 | + | assert!(Repo::clone(&format!("{base}/.hidden"), &root.join("d")) |
|
| 111 | + | .err() |
|
| 112 | + | .unwrap() |
|
| 113 | + | .to_string() |
|
| 114 | + | .contains("404")); |
|
| 93 | 115 | } |
|
| 94 | 116 | ||
| 95 | 117 | #[test] |
|
| … | 10 unchanged lines |
||
| 106 | 128 | let base = start(&served, vec![]); |
|
| 107 | 129 | let url = format!("{base}/origin"); |
|
| 108 | 130 | let a = Repo::clone_with(&url, &root.join("a"), Some(1000), &[]).unwrap(); |
|
| 109 | - | assert_eq!(fs::read(root.join("a/big.bin")).unwrap(), big, "fetched through the objects endpoint during checkout"); |
|
| 131 | + | assert_eq!( |
|
| 132 | + | fs::read(root.join("a/big.bin")).unwrap(), |
|
| 133 | + | big, |
|
| 134 | + | "fetched through the objects endpoint during checkout" |
|
| 135 | + | ); |
|
| 110 | 136 | assert!(!a.snapshot("status").unwrap().changed); |
|
| 111 | 137 | } |
|
| 112 | 138 | ||
| … | 16 unchanged lines |
||
| 129 | 155 | ||
| 130 | 156 | a.config.set("remote.origin.token", "wrong").unwrap(); |
|
| 131 | 157 | a.save_config().unwrap(); |
|
| 132 | - | assert!(a.push("origin", false).err().unwrap().to_string().contains("401")); |
|
| 158 | + | assert!(a |
|
| 159 | + | .push("origin", false) |
|
| 160 | + | .err() |
|
| 161 | + | .unwrap() |
|
| 162 | + | .to_string() |
|
| 163 | + | .contains("401")); |
|
| 133 | 164 | ||
| 134 | 165 | a.config.set("remote.origin.token", "s3cret").unwrap(); |
|
| 135 | 166 | a.save_config().unwrap(); |
|
| … | 10 unchanged lines |
||
| 146 | 177 | let served = root.join("served"); |
|
| 147 | 178 | Repo::init(&served.join("origin")).unwrap(); |
|
| 148 | 179 | let (cert, key) = self_signed(root); |
|
| 149 | - | let base = start_with(&served, vec![], Some(oxy_server::TlsFiles { cert: cert.clone(), key })); |
|
| 180 | + | let base = start_with( |
|
| 181 | + | &served, |
|
| 182 | + | vec![], |
|
| 183 | + | Some(oxy_server::TlsFiles { |
|
| 184 | + | cert: cert.clone(), |
|
| 185 | + | key, |
|
| 186 | + | }), |
|
| 187 | + | ); |
|
| 150 | 188 | let url = format!("{base}/origin"); |
|
| 151 | 189 | ||
| 152 | 190 | // Without trusting the certificate the clone fails; with `OXY_CA` it works. |
|
| 153 | - | let err = Repo::clone(&url, &root.join("untrusted")).err().unwrap().to_string(); |
|
| 154 | - | assert!(err.contains("certificate") || err.contains("tls") || err.contains("TLS") || err.contains("handshake"), "{err}"); |
|
| 191 | + | let err = Repo::clone(&url, &root.join("untrusted")) |
|
| 192 | + | .err() |
|
| 193 | + | .unwrap() |
|
| 194 | + | .to_string(); |
|
| 195 | + | assert!( |
|
| 196 | + | err.contains("certificate") |
|
| 197 | + | || err.contains("tls") |
|
| 198 | + | || err.contains("TLS") |
|
| 199 | + | || err.contains("handshake"), |
|
| 200 | + | "{err}" |
|
| 201 | + | ); |
|
| 155 | 202 | std::env::set_var("OXY_CA", &cert); |
|
| 156 | 203 | let mut a = Repo::clone(&url, &root.join("a")).unwrap(); |
|
| 157 | 204 | std::env::remove_var("OXY_CA"); |
|
| 158 | 205 | // Persist the trust for this remote so later commands need no env var. |
|
| 159 | - | a.config.set("remote.origin.ca", cert.to_str().unwrap()).unwrap(); |
|
| 206 | + | a.config |
|
| 207 | + | .set("remote.origin.ca", cert.to_str().unwrap()) |
|
| 208 | + | .unwrap(); |
|
| 160 | 209 | a.save_config().unwrap(); |
|
| 161 | 210 | write(&root.join("a"), "f.txt", b"tls\n"); |
|
| 162 | 211 | let h = a.commit("over tls", &[]).unwrap(); |
|
| … | 2 unchanged lines |
||
| 165 | 214 | assert_eq!(origin.bookmark_heads().unwrap(), vec![h]); |
|
| 166 | 215 | // A wrong CA is refused. |
|
| 167 | 216 | let (other_cert, _) = self_signed(&root.join("other")); |
|
| 168 | - | a.config.set("remote.origin.ca", other_cert.to_str().unwrap()).unwrap(); |
|
| 217 | + | a.config |
|
| 218 | + | .set("remote.origin.ca", other_cert.to_str().unwrap()) |
|
| 219 | + | .unwrap(); |
|
| 169 | 220 | a.save_config().unwrap(); |
|
| 170 | 221 | assert!(a.fetch("origin").is_err()); |
|
| 171 | 222 | } |
|
| … | 23 unchanged lines |
||
| 195 | 246 | let barrier = barrier.clone(); |
|
| 196 | 247 | handles.push(std::thread::spawn(move || { |
|
| 197 | 248 | let repo = Repo::open(&path).unwrap(); |
|
| 198 | - | write(&path, &format!("file-{i}.txt"), format!("from clone {i}\n").as_bytes()); |
|
| 249 | + | write( |
|
| 250 | + | &path, |
|
| 251 | + | &format!("file-{i}.txt"), |
|
| 252 | + | format!("from clone {i}\n").as_bytes(), |
|
| 253 | + | ); |
|
| 199 | 254 | let h = repo.commit(&format!("change {i}"), &[]).unwrap(); |
|
| 200 | 255 | barrier.wait(); |
|
| 201 | 256 | // A push is refused either up front (the remote moved since the |
|
| … | 3 unchanged lines |
||
| 205 | 260 | match repo.push("origin", false) { |
|
| 206 | 261 | Ok(r) if r.iter().all(|x| x.ok) => return (h, attempt), |
|
| 207 | 262 | Ok(_) => {} |
|
| 208 | - | Err(e) if e.to_string().contains("pull first") || e.to_string().contains("moved") => {} |
|
| 263 | + | Err(e) |
|
| 264 | + | if e.to_string().contains("pull first") |
|
| 265 | + | || e.to_string().contains("moved") => {} |
|
| 209 | 266 | Err(e) => panic!("clone {i}: {e}"), |
|
| 210 | 267 | } |
|
| 211 | - | repo.pull("origin").unwrap_or_else(|e| panic!("clone {i} pull: {e}")); |
|
| 268 | + | repo.pull("origin") |
|
| 269 | + | .unwrap_or_else(|e| panic!("clone {i} pull: {e}")); |
|
| 212 | 270 | } |
|
| 213 | 271 | panic!("clone {i} never managed to push"); |
|
| 214 | 272 | })); |
|
| … | 20 unchanged lines |
||
| 235 | 293 | let ops = origin.op_log().unwrap(); |
|
| 236 | 294 | assert!(ops.len() > N); |
|
| 237 | 295 | } |
|
| 238 | - | ||
| 239 | 296 | ||
| 240 | 297 | #[test] |
|
| 241 | 298 | fn a_bare_repository_is_served_and_browsed_without_a_working_copy() { |
|
| … | 3 unchanged lines |
||
| 245 | 302 | let served = root.join("served"); |
|
| 246 | 303 | let hosted = Repo::init_bare(&served.join("lib")).unwrap(); |
|
| 247 | 304 | assert!(hosted.is_bare()); |
|
| 248 | - | assert!(hosted.working_copy().is_err(), "a bare repository has no working copy"); |
|
| 305 | + | assert!( |
|
| 306 | + | hosted.working_copy().is_err(), |
|
| 307 | + | "a bare repository has no working copy" |
|
| 308 | + | ); |
|
| 249 | 309 | assert!(!served.join("lib").join(".oxy").join("workspace").exists()); |
|
| 250 | 310 | let base = start(&served, vec![]); |
|
| 251 | 311 | let url = format!("{base}/lib"); |
|
| … | 19 unchanged lines |
||
| 271 | 331 | assert_eq!(log.iter().map(|e| e.hash).collect::<Vec<_>>(), vec![c2, c1]); |
|
| 272 | 332 | assert!(log.iter().all(|e| !e.is_working_copy)); |
|
| 273 | 333 | assert_eq!(hosted.log_state(&[c1]).unwrap().len(), 1); |
|
| 274 | - | assert_eq!(hosted.file_at("src/lib.rs", Some(&heads)).unwrap(), b"pub fn one() {}\npub fn two() {}\n"); |
|
| 334 | + | assert_eq!( |
|
| 335 | + | hosted.file_at("src/lib.rs", Some(&heads)).unwrap(), |
|
| 336 | + | b"pub fn one() {}\npub fn two() {}\n" |
|
| 337 | + | ); |
|
| 275 | 338 | let blame = hosted.blame_in("src/lib.rs", &heads).unwrap(); |
|
| 276 | - | assert_eq!(blame.iter().map(|l| l.hash).collect::<Vec<_>>(), vec![c1, c2]); |
|
| 339 | + | assert_eq!( |
|
| 340 | + | blame.iter().map(|l| l.hash).collect::<Vec<_>>(), |
|
| 341 | + | vec![c1, c2] |
|
| 342 | + | ); |
|
| 277 | 343 | assert_eq!(hosted.log_for_path("src/lib.rs").unwrap().len(), 2); |
|
| 278 | 344 | assert_eq!(hosted.diff_files(Some(&c2)).unwrap().len(), 1); |
|
| 279 | - | assert!(matches!(hosted.verify_change(&c2).unwrap(), SignatureStatus::Unsigned | SignatureStatus::Valid { .. })); |
|
| 345 | + | assert!(matches!( |
|
| 346 | + | hosted.verify_change(&c2).unwrap(), |
|
| 347 | + | SignatureStatus::Unsigned | SignatureStatus::Valid { .. } |
|
| 348 | + | )); |
|
| 280 | 349 | ||
| 281 | 350 | // Snapshotting a bare repository is refused rather than inventing a working copy. |
|
| 282 | 351 | assert!(hosted.snapshot("test").is_err()); |
|
| … | 15 unchanged lines |
||
| 298 | 367 | } |
|
| 299 | 368 | impl Backend for Hub { |
|
| 300 | 369 | fn open(&self, name: &str) -> Result<Repo, Failure> { |
|
| 301 | - | let Some((owner, repo)) = name.split_once('/') else { return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into())) }; |
|
| 370 | + | let Some((owner, repo)) = name.split_once('/') else { |
|
| 371 | + | return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into())); |
|
| 372 | + | }; |
|
| 302 | 373 | let dir = self.root.join(owner).join(repo); |
|
| 303 | 374 | if !dir.join(".oxy").is_dir() { |
|
| 304 | 375 | return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into())); |
|
| 305 | 376 | } |
|
| 306 | 377 | Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) |
|
| 307 | 378 | } |
|
| 308 | - | fn authorize(&self, name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> { |
|
| 379 | + | fn authorize( |
|
| 380 | + | &self, |
|
| 381 | + | name: &str, |
|
| 382 | + | access: Access, |
|
| 383 | + | headers: &HeaderMap, |
|
| 384 | + | ) -> Result<(), Failure> { |
|
| 309 | 385 | // `secret/*` is private: unknown to anyone without the token, even for reads. |
|
| 310 | 386 | let token = oxy_server::bearer_token(headers); |
|
| 311 | 387 | if name.starts_with("secret/") && token != Some("s3cret") { |
|
| … | 5 unchanged lines |
||
| 317 | 393 | Ok(()) |
|
| 318 | 394 | } |
|
| 319 | 395 | fn after_push(&self, name: &str, results: &[oxy_core::protocol::RefResult]) { |
|
| 320 | - | self.pushes.lock().unwrap().push(format!("{name}: {}", results.iter().filter(|r| r.ok).count())); |
|
| 396 | + | self.pushes.lock().unwrap().push(format!( |
|
| 397 | + | "{name}: {}", |
|
| 398 | + | results.iter().filter(|r| r.ok).count() |
|
| 399 | + | )); |
|
| 321 | 400 | } |
|
| 322 | 401 | } |
|
| 323 | - | async fn route(State(server): State<Arc<Server>>, AxPath((owner, repo, endpoint)): AxPath<(String, String, String)>, method: Method, headers: HeaderMap, body: axum::body::Bytes) -> Response { |
|
| 402 | + | async fn route( |
|
| 403 | + | State(server): State<Arc<Server>>, |
|
| 404 | + | AxPath((owner, repo, endpoint)): AxPath<(String, String, String)>, |
|
| 405 | + | method: Method, |
|
| 406 | + | headers: HeaderMap, |
|
| 407 | + | body: axum::body::Bytes, |
|
| 408 | + | ) -> Response { |
|
| 324 | 409 | match Endpoint::parse(&endpoint) { |
|
| 325 | - | Some(e) => server.handle(format!("{owner}/{repo}"), e, &method, &headers, body).await, |
|
| 410 | + | Some(e) => { |
|
| 411 | + | server |
|
| 412 | + | .handle(format!("{owner}/{repo}"), e, &method, &headers, body) |
|
| 413 | + | .await |
|
| 414 | + | } |
|
| 326 | 415 | None => Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response(), |
|
| 327 | 416 | } |
|
| 328 | 417 | } |
|
| … | 4 unchanged lines |
||
| 333 | 422 | Repo::init_bare(&root.join("repos/andy/app")).unwrap(); |
|
| 334 | 423 | Repo::init_bare(&root.join("repos/secret/plans")).unwrap(); |
|
| 335 | 424 | let pushes = Arc::new(Mutex::new(Vec::new())); |
|
| 336 | - | let server = Server::new(Hub { root: root.join("repos"), pushes: pushes.clone() }); |
|
| 425 | + | let server = Server::new(Hub { |
|
| 426 | + | root: root.join("repos"), |
|
| 427 | + | pushes: pushes.clone(), |
|
| 428 | + | }); |
|
| 337 | 429 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); |
|
| 338 | 430 | let port = listener.local_addr().unwrap().port(); |
|
| 339 | 431 | std::thread::spawn(move || { |
|
| 340 | - | let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); |
|
| 432 | + | let rt = tokio::runtime::Builder::new_multi_thread() |
|
| 433 | + | .enable_all() |
|
| 434 | + | .build() |
|
| 435 | + | .unwrap(); |
|
| 341 | 436 | rt.block_on(async move { |
|
| 342 | 437 | listener.set_nonblocking(true).unwrap(); |
|
| 343 | - | let app = axum::Router::new().route("/{owner}/{repo}/oxy/v1/{endpoint}", axum::routing::any(route)).with_state(server); |
|
| 344 | - | axum::serve(tokio::net::TcpListener::from_std(listener).unwrap(), app).await.unwrap(); |
|
| 438 | + | let app = axum::Router::new() |
|
| 439 | + | .route( |
|
| 440 | + | "/{owner}/{repo}/oxy/v1/{endpoint}", |
|
| 441 | + | axum::routing::any(route), |
|
| 442 | + | ) |
|
| 443 | + | .with_state(server); |
|
| 444 | + | axum::serve(tokio::net::TcpListener::from_std(listener).unwrap(), app) |
|
| 445 | + | .await |
|
| 446 | + | .unwrap(); |
|
| 345 | 447 | }); |
|
| 346 | 448 | }); |
|
| 347 | 449 | std::thread::sleep(std::time::Duration::from_millis(50)); |
|
| … | 8 unchanged lines |
||
| 356 | 458 | write(&work, "a.txt", b"a\n"); |
|
| 357 | 459 | client.snapshot("test").unwrap(); |
|
| 358 | 460 | client.commit("a", &[]).unwrap(); |
|
| 359 | - | assert!(client.push("origin", false).is_err(), "anonymous push must be refused"); |
|
| 461 | + | assert!( |
|
| 462 | + | client.push("origin", false).is_err(), |
|
| 463 | + | "anonymous push must be refused" |
|
| 464 | + | ); |
|
| 360 | 465 | let mut client = client; |
|
| 361 | 466 | client.config.set("remote.origin.token", "s3cret").unwrap(); |
|
| 362 | 467 | let results = client.push("origin", false).unwrap(); |
|
| … | 3 unchanged lines |
||
| 366 | 471 | let hosted = Repo::open(&root.join("repos/andy/app")).unwrap(); |
|
| 367 | 472 | assert_eq!(hosted.bookmark_heads().unwrap().len(), 1); |
|
| 368 | 473 | } |
|
| 474 | + | ||
| 475 | + | /// A hosted repository created unbound adopts the id of the first |
|
| 476 | + | /// repository pushed to it; before that it cannot be cloned or fetched. |
|
| 477 | + | #[test] |
|
| 478 | + | fn an_unbound_repository_adopts_the_first_pushers_id() { |
|
| 479 | + | hermetic(); |
|
| 480 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 481 | + | let root = dir.path(); |
|
| 482 | + | let served = root.join("served"); |
|
| 483 | + | let hosted = Repo::init_bare_unbound(&served.join("lib")).unwrap(); |
|
| 484 | + | assert!(hosted.is_unbound()); |
|
| 485 | + | assert_eq!(hosted.id().unwrap(), [0; 16]); |
|
| 486 | + | let base = start(&served, vec![]); |
|
| 487 | + | let url = format!("{base}/lib"); |
|
| 488 | + | assert!(Repo::clone(&url, &root.join("early")).err().unwrap().to_string().contains("unbound")); |
|
| 489 | + | ||
| 490 | + | // An existing repository with its own id and history. |
|
| 491 | + | let work = root.join("work"); |
|
| 492 | + | let mut local = Repo::init(&work).unwrap(); |
|
| 493 | + | let my_id = local.id().unwrap(); |
|
| 494 | + | write(&work, "a.txt", b"a\n"); |
|
| 495 | + | local.snapshot("test").unwrap(); |
|
| 496 | + | local.commit("a", &[]).unwrap(); |
|
| 497 | + | local.config.set("remote.origin.url", &url).unwrap(); |
|
| 498 | + | local.save_config().unwrap(); |
|
| 499 | + | assert!(local.fetch("origin").err().unwrap().to_string().contains("unbound"), "fetching an unbound remote is refused"); |
|
| 500 | + | let results = local.push("origin", false).unwrap(); |
|
| 501 | + | assert!(results.iter().all(|r| r.ok), "{results:?}"); |
|
| 502 | + | ||
| 503 | + | let hosted = Repo::open(&served.join("lib")).unwrap(); |
|
| 504 | + | assert!(!hosted.is_unbound()); |
|
| 505 | + | assert_eq!(hosted.id().unwrap(), my_id, "bound to the pusher's id"); |
|
| 506 | + | assert_eq!(hosted.bookmark_heads().unwrap().len(), 1); |
|
| 507 | + | // From now on it is an ordinary remote: clones share the id, other repositories are refused. |
|
| 508 | + | let cloned = Repo::clone(&url, &root.join("late")).unwrap(); |
|
| 509 | + | assert_eq!(cloned.id().unwrap(), my_id); |
|
| 510 | + | let mut other = Repo::init(&root.join("other")).unwrap(); |
|
| 511 | + | other.config.set("remote.origin.url", &url).unwrap(); |
|
| 512 | + | other.save_config().unwrap(); |
|
| 513 | + | write(&root.join("other"), "b.txt", b"b\n"); |
|
| 514 | + | other.snapshot("test").unwrap(); |
|
| 515 | + | other.commit("b", &[]).unwrap(); |
|
| 516 | + | assert!(other.push("origin", false).err().unwrap().to_string().contains("different repository")); |
|
| 517 | + | } |
modified docs/plan.md +1 −0
| … | 82 unchanged lines |
||
| 83 | 83 | ||
| 84 | 84 | - [x] Bare repositories: `Repo::init_bare`, `is_bare`, `oxy init --bare`; `working_copy` refuses; `log`, `log_for_path` work from the bookmark state; `log_state`, `blame_in`, `log_for_path_in` for any state. |
|
| 85 | 85 | - [x] Embeddable server: `oxy_server::{Backend, Server, Endpoint, Access, Failure, DirBackend, bearer_token}`; `router` is `DirBackend` on `/{repo}/oxy/v1/{endpoint}`; `after_push` hook. |
|
| 86 | + | - [x] Unbound hosted repositories (`Repo::init_bare_unbound`, `bind_id`, `Oxy-Repo-Id` header): an existing repository pushed to an empty hosted one keeps its id (protocol.md §2). |
|
| 86 | 87 | - [ ] `oxy identity sign-challenge` and `verify_challenge` (hub 0.2). |
|
| 87 | 88 | - [ ] `oxy seal <hash>..` and `--proposal URL` (hub 0.3). |
|
| 88 | 89 | - [ ] `oxy proposal show|land URL` (hub 0.3). |
|
| … | 3 unchanged lines |
modified docs/protocol.md +9 −0
| … | 56 unchanged lines |
||
| 57 | 57 | plus an optional PEM file (`remote.<name>.ca` or `OXY_CA`); servers with |
|
| 58 | 58 | self-signed certificates are expected to hand that file out. |
|
| 59 | 59 | - Pushes to one repository are serialised by the server; reads are not. |
|
| 60 | + | - A hosted repository may be **unbound**: created empty with no id, so an |
|
| 61 | + | existing repository can be pushed to it and keep its identity. Its |
|
| 62 | + | `capabilities` reports an all-zero id. A client refuses to clone or fetch |
|
| 63 | + | from an unbound remote and accepts it for a push, sending its own id in |
|
| 64 | + | the `Oxy-Repo-Id` request header (32 hex characters); the server binds |
|
| 65 | + | the repository to that id under the push lock before validating the |
|
| 66 | + | push, and refuses a push to an unbound repository that carries no header |
|
| 67 | + | with `400`. Clients send the header on every request when they have an |
|
| 68 | + | id; servers ignore it on bound repositories. |
|
| 60 | 69 | ||
| 61 | 70 | Embedding: `oxy_server::Server` answers any endpoint for a repository name |
|
| 62 | 71 | a host's `Backend` resolves and authorises (`open`, `authorize`, and an |
|
| … | 132 unchanged lines |
modified docs/spec.md +3 −3
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | | | | |
|
| 4 | 4 | |---|---| |
|
| 5 | - | | **Status** | Draft 0.8c | |
|
| 5 | + | | **Status** | Draft 0.8d | |
|
| 6 | 6 | | **Format version** | 1 | |
|
| 7 | 7 | | **Date** | 2026-09-26 | |
|
| 8 | 8 | | **Audience** | Implementers, security reviewers, and contributors | |
|
| … | 684 unchanged lines |
||
| 693 | 693 | ||
| 694 | 694 | | Endpoint | Request | Response | |
|
| 695 | 695 | |---|---|---| |
|
| 696 | - | | `GET capabilities` | — | `{ versions: [1], features }`; feature names are `seal`, and, when offered, `lock`; `max_frame` and `transports` are reserved for framed transports | |
|
| 696 | + | | `GET capabilities` | — | `{ versions: [1], features }`; feature names are `seal`, and, when offered, `lock`; `max_frame` and `transports` are reserved for framed transports. The repository id is all zero for an **unbound** hosted repository, one created empty that adopts the id of the first repository pushed to it; a client MUST NOT clone or fetch from one and MAY push to one, sending its id in the `Oxy-Repo-Id` header, which the server binds before validating the push | |
|
| 697 | 697 | | `GET refs` | `{ patterns }` | `[ { name, state: ObjectRef, heads, attestation: Signed<Attest> } ]` plus the current `Signed<Policy>` | |
|
| 698 | 698 | | `POST negotiate` | `{ have: [ChangeHash], want: [ChangeHash], filter }` | stream of `ChangeHash` the client lacks, in dependency order | |
|
| 699 | 699 | | `POST fetch` | `{ changes: [ChangeHash], include_blobs: bool, filter }` | stream of `Signed<Change>`, then every `Signed<Attest kind=seal>` covering them, then referenced `blob`/`chunk` objects | |
|
| … | 215 unchanged lines |
||
| 915 | 915 | ||
| 916 | 916 | --- |
|
| 917 | 917 | ||
| 918 | - | *Changelog.* Draft 0.8c (2026-09-27): bare repositories (`oxy init --bare`, §12.2: no workspace, no working copy; hosting) and the reference server as an embeddable library (§15.1). Draft 0.8b (2026-09-27): the machine interface for editors (§24): `root`, `cat`, `version`, `hunks`, `discard`, and `--json` on the reporting commands. |
|
| 918 | + | *Changelog.* Draft 0.8d (2026-09-27): unbound hosted repositories adopt the first pusher's id (§16.1). Draft 0.8c (2026-09-27): bare repositories (`oxy init --bare`, §12.2: no workspace, no working copy; hosting) and the reference server as an embeddable library (§15.1). Draft 0.8b (2026-09-27): the machine interface for editors (§24): `root`, `cat`, `version`, `hunks`, `discard`, and `--json` on the reporting commands. |
|
| 919 | 919 | ||
| 920 | 920 | Draft 0.8a (2026-09-27): every Draft 0.8 addition is implemented in the reference implementation (§26.1). Clarifications from doing so: an unregistered nested repository is left alone and a tracked subrepo whose directory is absent keeps its pin (§7.8); Oxygen pins are omitted from Git export (§23). |
|
| 921 | 921 | ||
| … | 59 unchanged lines |
modified src/cli/format.rs +37 −6
| … | 35 unchanged lines |
||
| 36 | 36 | let d = doy - (153 * mp + 2) / 5 + 1; |
|
| 37 | 37 | let m = if mp < 10 { mp + 3 } else { mp - 9 }; |
|
| 38 | 38 | let y = if m <= 2 { y + 1 } else { y }; |
|
| 39 | - | format!("{y:04}-{m:02}-{d:02} {:02}:{:02}:{:02}Z", rem / 3600, (rem % 3600) / 60, rem % 60) |
|
| 39 | + | format!( |
|
| 40 | + | "{y:04}-{m:02}-{d:02} {:02}:{:02}:{:02}Z", |
|
| 41 | + | rem / 3600, |
|
| 42 | + | (rem % 3600) / 60, |
|
| 43 | + | rem % 60 |
|
| 44 | + | ) |
|
| 40 | 45 | } |
|
| 41 | 46 | ||
| 42 | 47 | /// One line describing how a change is authenticated. |
|
| 43 | 48 | pub fn signature_line(repo: &Repo, h: &ChangeHash) -> Result<String> { |
|
| 44 | 49 | Ok(match repo.authenticate_change(h)? { |
|
| 45 | 50 | SignatureStatus::Unsigned => "unsigned".to_string(), |
|
| 46 | - | SignatureStatus::Valid { key, name, email, sealed, revoked } => { |
|
| 47 | - | format!("{} by {name} <{email}> key {}{}", if sealed { "sealed" } else { "signed" }, key.short(12), if revoked { " (KEY REVOKED)" } else { "" }) |
|
| 51 | + | SignatureStatus::Valid { |
|
| 52 | + | key, |
|
| 53 | + | name, |
|
| 54 | + | email, |
|
| 55 | + | sealed, |
|
| 56 | + | revoked, |
|
| 57 | + | } => { |
|
| 58 | + | format!( |
|
| 59 | + | "{} by {name} <{email}> key {}{}", |
|
| 60 | + | if sealed { "sealed" } else { "signed" }, |
|
| 61 | + | key.short(12), |
|
| 62 | + | if revoked { " (KEY REVOKED)" } else { "" } |
|
| 63 | + | ) |
|
| 48 | 64 | } |
|
| 49 | 65 | SignatureStatus::Invalid(why) => format!("INVALID signature: {why}"), |
|
| 50 | 66 | }) |
|
| … | 3 unchanged lines |
||
| 54 | 70 | pub fn signature_mark(repo: &Repo, h: &ChangeHash) -> Result<String> { |
|
| 55 | 71 | Ok(match repo.authenticate_change(h)? { |
|
| 56 | 72 | SignatureStatus::Unsigned => ui::dim(format!("{} unsigned", ui::glyph("·", "-"))), |
|
| 57 | - | SignatureStatus::Valid { sealed, revoked: true, .. } => ui::warn(format!("{} {} (key revoked)", ui::glyph("⚠", "!"), if sealed { "sealed" } else { "signed" })), |
|
| 58 | - | SignatureStatus::Valid { sealed, .. } => ui::ok(format!("{} {}", ui::glyph("✔", "v"), if sealed { "sealed" } else { "signed" })), |
|
| 73 | + | SignatureStatus::Valid { |
|
| 74 | + | sealed, |
|
| 75 | + | revoked: true, |
|
| 76 | + | .. |
|
| 77 | + | } => ui::warn(format!( |
|
| 78 | + | "{} {} (key revoked)", |
|
| 79 | + | ui::glyph("⚠", "!"), |
|
| 80 | + | if sealed { "sealed" } else { "signed" } |
|
| 81 | + | )), |
|
| 82 | + | SignatureStatus::Valid { sealed, .. } => ui::ok(format!( |
|
| 83 | + | "{} {}", |
|
| 84 | + | ui::glyph("✔", "v"), |
|
| 85 | + | if sealed { "sealed" } else { "signed" } |
|
| 86 | + | )), |
|
| 59 | 87 | SignatureStatus::Invalid(why) => ui::err(format!("{} INVALID: {why}", ui::glyph("✖", "x"))), |
|
| 60 | 88 | }) |
|
| 61 | 89 | } |
|
| … | 28 unchanged lines |
||
| 90 | 118 | } |
|
| 91 | 119 | ||
| 92 | 120 | pub fn deps_of(deps: &[oxy_core::hash::ChangeHash]) -> String { |
|
| 93 | - | deps.iter().map(|d| format!("#{}", d.short(12))).collect::<Vec<_>>().join(" ") |
|
| 121 | + | deps.iter() |
|
| 122 | + | .map(|d| format!("#{}", d.short(12))) |
|
| 123 | + | .collect::<Vec<_>>() |
|
| 124 | + | .join(" ") |
|
| 94 | 125 | } |
|
| 95 | 126 | ||
| 96 | 127 | #[cfg(test)] |
|
| … | 11 unchanged lines |
modified src/cli/identity.rs +319 −49
| 1 | 1 | //! Identity, verification, and configuration commands. |
|
| 2 | 2 | ||
| 3 | 3 | use super::format::{short_id, signature_line, when}; |
|
| 4 | + | use super::json; |
|
| 4 | 5 | use super::open; |
|
| 5 | - | use super::json; |
|
| 6 | 6 | use super::ui; |
|
| 7 | 7 | use anyhow::Result; |
|
| 8 | 8 | use oxy_core::hash::ChangeHash; |
|
| 9 | + | use oxy_core::hash::KeyId; |
|
| 9 | 10 | use oxy_core::identity::Identity; |
|
| 10 | - | use oxy_core::hash::KeyId; |
|
| 11 | 11 | use oxy_core::objects::Hlc; |
|
| 12 | 12 | use oxy_core::protocol::SignedPolicy; |
|
| 13 | 13 | use oxy_core::repo::{self, IdentityScope, PolicyUpdate, Repo, Rotation, SignatureStatus}; |
|
| … | 23 unchanged lines |
||
| 37 | 37 | } |
|
| 38 | 38 | candidates.sort(); |
|
| 39 | 39 | candidates.dedup(); |
|
| 40 | - | let matches: Vec<_> = candidates.into_iter().filter(|k| k.starts_with_hex(prefix)).collect(); |
|
| 40 | + | let matches: Vec<_> = candidates |
|
| 41 | + | .into_iter() |
|
| 42 | + | .filter(|k| k.starts_with_hex(prefix)) |
|
| 43 | + | .collect(); |
|
| 41 | 44 | match matches.len() { |
|
| 42 | 45 | 1 => Ok(matches[0]), |
|
| 43 | 46 | 0 => anyhow::bail!("no known key starts with {prefix:?}"), |
|
| … | 5 unchanged lines |
||
| 49 | 52 | /// other root holders sign with `oxy policy sign`. |
|
| 50 | 53 | fn report_update(u: &PolicyUpdate, out: Option<PathBuf>) -> Result<()> { |
|
| 51 | 54 | if u.applied { |
|
| 52 | - | println!("{} policy serial {} {}", ui::done("Applied"), u.policy.serial, ui::dim("(push to publish it)")); |
|
| 55 | + | println!( |
|
| 56 | + | "{} policy serial {} {}", |
|
| 57 | + | ui::done("Applied"), |
|
| 58 | + | u.policy.serial, |
|
| 59 | + | ui::dim("(push to publish it)") |
|
| 60 | + | ); |
|
| 53 | 61 | return Ok(()); |
|
| 54 | 62 | } |
|
| 55 | 63 | let path = out.unwrap_or_else(|| PathBuf::from(format!("policy-{}.proposal", u.policy.serial))); |
|
| … | 12 unchanged lines |
||
| 68 | 76 | } |
|
| 69 | 77 | ||
| 70 | 78 | fn report_rotation(r: &Rotation, verb: &str) -> Result<()> { |
|
| 71 | - | println!("{} identity {} <{}>", ui::done(verb), ui::author(&r.identity.name), r.identity.email); |
|
| 79 | + | println!( |
|
| 80 | + | "{} identity {} <{}>", |
|
| 81 | + | ui::done(verb), |
|
| 82 | + | ui::author(&r.identity.name), |
|
| 83 | + | r.identity.email |
|
| 84 | + | ); |
|
| 72 | 85 | println!("{}", ui::kv("old key", ui::dim(r.old.to_hex()))); |
|
| 73 | 86 | println!("{}", ui::kv("new key", r.new.to_hex())); |
|
| 74 | 87 | if !r.identity.delegates.is_empty() { |
|
| 75 | - | println!("{}", ui::kv("delegates", r.identity.delegates.iter().map(|d| d.short(12)).collect::<Vec<_>>().join(" "))); |
|
| 88 | + | println!( |
|
| 89 | + | "{}", |
|
| 90 | + | ui::kv( |
|
| 91 | + | "delegates", |
|
| 92 | + | r.identity |
|
| 93 | + | .delegates |
|
| 94 | + | .iter() |
|
| 95 | + | .map(|d| d.short(12)) |
|
| 96 | + | .collect::<Vec<_>>() |
|
| 97 | + | .join(" ") |
|
| 98 | + | ) |
|
| 99 | + | ); |
|
| 76 | 100 | } |
|
| 77 | 101 | if let Some(e) = r.identity.expires { |
|
| 78 | 102 | println!("{}", ui::kv("expires", when(e))); |
|
| 79 | 103 | } |
|
| 80 | 104 | match &r.policy { |
|
| 81 | 105 | Some(u) => report_update(u, None)?, |
|
| 82 | - | None => println!(" {} {}", ui::note(), ui::dim("no policy names the old key; export the new identity for policy holders")), |
|
| 106 | + | None => println!( |
|
| 107 | + | " {} {}", |
|
| 108 | + | ui::note(), |
|
| 109 | + | ui::dim("no policy names the old key; export the new identity for policy holders") |
|
| 110 | + | ), |
|
| 83 | 111 | } |
|
| 84 | 112 | Ok(()) |
|
| 85 | 113 | } |
|
| 86 | 114 | ||
| 87 | - | pub fn create(name: Option<String>, email: Option<String>, local: bool, expires: Option<u64>) -> Result<i32> { |
|
| 115 | + | pub fn create( |
|
| 116 | + | name: Option<String>, |
|
| 117 | + | email: Option<String>, |
|
| 118 | + | local: bool, |
|
| 119 | + | expires: Option<u64>, |
|
| 120 | + | ) -> Result<i32> { |
|
| 88 | 121 | let mut repo = open()?; |
|
| 89 | 122 | let name = name.unwrap_or_else(|| repo.config.author_name.clone()); |
|
| 90 | 123 | let email = email.unwrap_or_else(|| repo.config.author_email.clone()); |
|
| 91 | - | let scope = if local { IdentityScope::Repo } else { IdentityScope::User }; |
|
| 92 | - | let (key, _) = repo.create_identity_with(&name, &email, scope, expires.and_then(expiry_from_days))?; |
|
| 93 | - | println!("{} identity {} <{email}>", ui::done("Created"), ui::author(&name)); |
|
| 124 | + | let scope = if local { |
|
| 125 | + | IdentityScope::Repo |
|
| 126 | + | } else { |
|
| 127 | + | IdentityScope::User |
|
| 128 | + | }; |
|
| 129 | + | let (key, _) = |
|
| 130 | + | repo.create_identity_with(&name, &email, scope, expires.and_then(expiry_from_days))?; |
|
| 131 | + | println!( |
|
| 132 | + | "{} identity {} <{email}>", |
|
| 133 | + | ui::done("Created"), |
|
| 134 | + | ui::author(&name) |
|
| 135 | + | ); |
|
| 94 | 136 | println!("{}", ui::kv("key", key)); |
|
| 95 | - | println!("{}", ui::kv("secret", ui::path(repo::default_keys_dir().display()))); |
|
| 137 | + | println!( |
|
| 138 | + | "{}", |
|
| 139 | + | ui::kv("secret", ui::path(repo::default_keys_dir().display())) |
|
| 140 | + | ); |
|
| 96 | 141 | match scope { |
|
| 97 | - | IdentityScope::User => println!("{}", ui::kv("scope", format!("default author in {}", ui::path(repo.user_config_path().display())))), |
|
| 98 | - | IdentityScope::Repo => println!("{}", ui::kv("scope", "this repository's .oxy/config only")), |
|
| 142 | + | IdentityScope::User => println!( |
|
| 143 | + | "{}", |
|
| 144 | + | ui::kv( |
|
| 145 | + | "scope", |
|
| 146 | + | format!( |
|
| 147 | + | "default author in {}", |
|
| 148 | + | ui::path(repo.user_config_path().display()) |
|
| 149 | + | ) |
|
| 150 | + | ) |
|
| 151 | + | ), |
|
| 152 | + | IdentityScope::Repo => { |
|
| 153 | + | println!("{}", ui::kv("scope", "this repository's .oxy/config only")) |
|
| 154 | + | } |
|
| 99 | 155 | } |
|
| 100 | 156 | if !repo.config.sign_changes { |
|
| 101 | 157 | println!(" {} {}", ui::note(), ui::dim("changes will carry this author; run oxy config --user core.sign change to sign each commit")); |
|
| … | 8 unchanged lines |
||
| 110 | 166 | return Ok(0); |
|
| 111 | 167 | }; |
|
| 112 | 168 | let id = repo.identity(&key)?; |
|
| 113 | - | println!("{} {} <{}>", ui::section("identity"), ui::author(&id.name), id.email); |
|
| 169 | + | println!( |
|
| 170 | + | "{} {} <{}>", |
|
| 171 | + | ui::section("identity"), |
|
| 172 | + | ui::author(&id.name), |
|
| 173 | + | id.email |
|
| 174 | + | ); |
|
| 114 | 175 | println!("{}", ui::kv("key", key)); |
|
| 115 | - | println!("{}", ui::kv("created", format!("{} {}", when(id.created), ui::dim(format!("({})", ui::ago(id.created)))))); |
|
| 176 | + | println!( |
|
| 177 | + | "{}", |
|
| 178 | + | ui::kv( |
|
| 179 | + | "created", |
|
| 180 | + | format!( |
|
| 181 | + | "{} {}", |
|
| 182 | + | when(id.created), |
|
| 183 | + | ui::dim(format!("({})", ui::ago(id.created))) |
|
| 184 | + | ) |
|
| 185 | + | ) |
|
| 186 | + | ); |
|
| 116 | 187 | if let Some(e) = id.expires { |
|
| 117 | 188 | let expired = id.is_expired_at(Hlc::now().wall_ms); |
|
| 118 | - | println!("{}", ui::kv("expires", if expired { ui::err(format!("{} EXPIRED", when(e))) } else { when(e) })); |
|
| 189 | + | println!( |
|
| 190 | + | "{}", |
|
| 191 | + | ui::kv( |
|
| 192 | + | "expires", |
|
| 193 | + | if expired { |
|
| 194 | + | ui::err(format!("{} EXPIRED", when(e))) |
|
| 195 | + | } else { |
|
| 196 | + | when(e) |
|
| 197 | + | } |
|
| 198 | + | ) |
|
| 199 | + | ); |
|
| 119 | 200 | } |
|
| 120 | 201 | if let Some(p) = id.previous { |
|
| 121 | 202 | println!("{}", ui::kv("previous", ui::dim(p.short(12)))); |
|
| 122 | 203 | } |
|
| 123 | 204 | if !id.delegates.is_empty() { |
|
| 124 | - | println!("{}", ui::kv("delegates", id.delegates.iter().map(|d| d.short(12)).collect::<Vec<_>>().join(" "))); |
|
| 205 | + | println!( |
|
| 206 | + | "{}", |
|
| 207 | + | ui::kv( |
|
| 208 | + | "delegates", |
|
| 209 | + | id.delegates |
|
| 210 | + | .iter() |
|
| 211 | + | .map(|d| d.short(12)) |
|
| 212 | + | .collect::<Vec<_>>() |
|
| 213 | + | .join(" ") |
|
| 214 | + | ) |
|
| 215 | + | ); |
|
| 125 | 216 | } |
|
| 126 | 217 | if repo.is_revoked(&key)? { |
|
| 127 | - | println!("{}", ui::kv("status", ui::err(format!("{} REVOKED", ui::glyph("✖", "x"))))); |
|
| 218 | + | println!( |
|
| 219 | + | "{}", |
|
| 220 | + | ui::kv( |
|
| 221 | + | "status", |
|
| 222 | + | ui::err(format!("{} REVOKED", ui::glyph("✖", "x"))) |
|
| 223 | + | ) |
|
| 224 | + | ); |
|
| 128 | 225 | } |
|
| 129 | - | println!("{}", ui::kv("secret", if repo.secret_key()?.is_some() { ui::ok(format!("{} available", ui::glyph("✔", "*"))) } else { ui::err(format!("{} not found in the key directory", ui::glyph("✖", "x"))) })); |
|
| 130 | - | println!("{}", ui::kv("signing", if repo.config.sign_changes { "every change (core.sign = change)" } else { "at push via seals (core.sign = seal)" })); |
|
| 226 | + | println!( |
|
| 227 | + | "{}", |
|
| 228 | + | ui::kv( |
|
| 229 | + | "secret", |
|
| 230 | + | if repo.secret_key()?.is_some() { |
|
| 231 | + | ui::ok(format!("{} available", ui::glyph("✔", "*"))) |
|
| 232 | + | } else { |
|
| 233 | + | ui::err(format!( |
|
| 234 | + | "{} not found in the key directory", |
|
| 235 | + | ui::glyph("✖", "x") |
|
| 236 | + | )) |
|
| 237 | + | } |
|
| 238 | + | ) |
|
| 239 | + | ); |
|
| 240 | + | println!( |
|
| 241 | + | "{}", |
|
| 242 | + | ui::kv( |
|
| 243 | + | "signing", |
|
| 244 | + | if repo.config.sign_changes { |
|
| 245 | + | "every change (core.sign = change)" |
|
| 246 | + | } else { |
|
| 247 | + | "at push via seals (core.sign = seal)" |
|
| 248 | + | } |
|
| 249 | + | ) |
|
| 250 | + | ); |
|
| 131 | 251 | Ok(0) |
|
| 132 | 252 | } |
|
| 133 | 253 | ||
| … | 1 unchanged lines |
||
| 135 | 255 | let repo = open()?; |
|
| 136 | 256 | let hashes: Vec<ChangeHash> = match change { |
|
| 137 | 257 | Some(c) => vec![repo.resolve_change(c)?], |
|
| 138 | - | None => repo.log()?.into_iter().filter(|e| !e.is_working_copy).map(|e| e.hash).collect(), |
|
| 258 | + | None => repo |
|
| 259 | + | .log()? |
|
| 260 | + | .into_iter() |
|
| 261 | + | .filter(|e| !e.is_working_copy) |
|
| 262 | + | .map(|e| e.hash) |
|
| 263 | + | .collect(), |
|
| 139 | 264 | }; |
|
| 140 | 265 | let mut bad = 0; |
|
| 141 | 266 | if as_json { |
|
| 142 | 267 | let mut items = Vec::new(); |
|
| 143 | 268 | for h in &hashes { |
|
| 144 | - | let Ok(c) = repo.store().get_change(h) else { continue }; |
|
| 269 | + | let Ok(c) = repo.store().get_change(h) else { |
|
| 270 | + | continue; |
|
| 271 | + | }; |
|
| 145 | 272 | let (status, key, name, revoked, reason) = match repo.authenticate_change(h)? { |
|
| 146 | 273 | SignatureStatus::Unsigned => ("unsigned", None, None, false, None), |
|
| 147 | - | SignatureStatus::Valid { key, name, sealed, revoked, .. } => (if sealed { "sealed" } else { "signed" }, Some(key.to_hex()), Some(name), revoked, None), |
|
| 274 | + | SignatureStatus::Valid { |
|
| 275 | + | key, |
|
| 276 | + | name, |
|
| 277 | + | sealed, |
|
| 278 | + | revoked, |
|
| 279 | + | .. |
|
| 280 | + | } => ( |
|
| 281 | + | if sealed { "sealed" } else { "signed" }, |
|
| 282 | + | Some(key.to_hex()), |
|
| 283 | + | Some(name), |
|
| 284 | + | revoked, |
|
| 285 | + | None, |
|
| 286 | + | ), |
|
| 148 | 287 | SignatureStatus::Invalid(why) => { |
|
| 149 | 288 | bad += 1; |
|
| 150 | 289 | ("invalid", None, None, false, Some(why)) |
|
| … | 14 unchanged lines |
||
| 165 | 304 | } |
|
| 166 | 305 | for h in hashes { |
|
| 167 | 306 | let Ok(c) = repo.store().get_change(&h) else { |
|
| 168 | - | println!("{} {}", ui::hash(format!("#{}", h.short(12))), ui::dim("not present (shallow)")); |
|
| 307 | + | println!( |
|
| 308 | + | "{} {}", |
|
| 309 | + | ui::hash(format!("#{}", h.short(12))), |
|
| 310 | + | ui::dim("not present (shallow)") |
|
| 311 | + | ); |
|
| 169 | 312 | continue; |
|
| 170 | 313 | }; |
|
| 171 | 314 | let line = signature_line(&repo, &h)?; |
|
| … | 5 unchanged lines |
||
| 177 | 320 | } else { |
|
| 178 | 321 | ui::ok(format!("{} {line}", ui::glyph("✔", "v"))) |
|
| 179 | 322 | }; |
|
| 180 | - | println!("{} {} {styled}", ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12)))); |
|
| 323 | + | println!( |
|
| 324 | + | "{} {} {styled}", |
|
| 325 | + | ui::id(short_id(&c)), |
|
| 326 | + | ui::hash(format!("#{}", h.short(12))) |
|
| 327 | + | ); |
|
| 181 | 328 | } |
|
| 182 | 329 | Ok(if bad > 0 { 1 } else { 0 }) |
|
| 183 | 330 | } |
|
| … | 2 unchanged lines |
||
| 186 | 333 | if user { |
|
| 187 | 334 | let path = repo::default_user_config(); |
|
| 188 | 335 | repo::Config::set_user(&path, key, value)?; |
|
| 189 | - | println!("{} {key} = {value} {}", ui::done("Set"), ui::dim(format!("(in {})", path.display()))); |
|
| 336 | + | println!( |
|
| 337 | + | "{} {key} = {value} {}", |
|
| 338 | + | ui::done("Set"), |
|
| 339 | + | ui::dim(format!("(in {})", path.display())) |
|
| 340 | + | ); |
|
| 190 | 341 | } else { |
|
| 191 | 342 | let mut repo = open()?; |
|
| 192 | 343 | repo.config.set(key, value)?; |
|
| 193 | 344 | repo.save_config()?; |
|
| 194 | - | println!("{} {key} = {value} {}", ui::done("Set"), ui::dim("(in this repository)")); |
|
| 345 | + | println!( |
|
| 346 | + | "{} {key} = {value} {}", |
|
| 347 | + | ui::done("Set"), |
|
| 348 | + | ui::dim("(in this repository)") |
|
| 349 | + | ); |
|
| 195 | 350 | } |
|
| 196 | 351 | Ok(0) |
|
| 197 | 352 | } |
|
| … | 6 unchanged lines |
||
| 204 | 359 | let id = repo.identity(&key)?; |
|
| 205 | 360 | let path = path.unwrap_or_else(|| PathBuf::from(format!("{}.identity", key.short(16)))); |
|
| 206 | 361 | std::fs::write(&path, id.encode())?; |
|
| 207 | - | println!("{} identity {} <{}> ({}) to {}", ui::done("Wrote"), ui::author(&id.name), id.email, key.short(12), ui::path(path.display())); |
|
| 362 | + | println!( |
|
| 363 | + | "{} identity {} <{}> ({}) to {}", |
|
| 364 | + | ui::done("Wrote"), |
|
| 365 | + | ui::author(&id.name), |
|
| 366 | + | id.email, |
|
| 367 | + | key.short(12), |
|
| 368 | + | ui::path(path.display()) |
|
| 369 | + | ); |
|
| 208 | 370 | Ok(0) |
|
| 209 | 371 | } |
|
| 210 | 372 | ||
| … | 15 unchanged lines |
||
| 226 | 388 | let repo = open()?; |
|
| 227 | 389 | let key = match key { |
|
| 228 | 390 | Some(k) => resolve_key(&repo, k)?, |
|
| 229 | - | None => repo.config.author_key.ok_or_else(|| anyhow::anyhow!("no identity configured"))?, |
|
| 391 | + | None => repo |
|
| 392 | + | .config |
|
| 393 | + | .author_key |
|
| 394 | + | .ok_or_else(|| anyhow::anyhow!("no identity configured"))?, |
|
| 230 | 395 | }; |
|
| 231 | 396 | let update = repo.revoke_identity(&key, reason)?; |
|
| 232 | - | println!("{} key {}{}", ui::done("Revoked"), ui::hash(key.short(12)), if reason.is_empty() { String::new() } else { format!(": {reason}") }); |
|
| 397 | + | println!( |
|
| 398 | + | "{} key {}{}", |
|
| 399 | + | ui::done("Revoked"), |
|
| 400 | + | ui::hash(key.short(12)), |
|
| 401 | + | if reason.is_empty() { |
|
| 402 | + | String::new() |
|
| 403 | + | } else { |
|
| 404 | + | format!(": {reason}") |
|
| 405 | + | } |
|
| 406 | + | ); |
|
| 233 | 407 | match update { |
|
| 234 | 408 | Some(u) => report_update(&u, None)?, |
|
| 235 | - | None => println!(" {} {}", ui::note(), ui::dim("recorded locally; no policy named the key")), |
|
| 409 | + | None => println!( |
|
| 410 | + | " {} {}", |
|
| 411 | + | ui::note(), |
|
| 412 | + | ui::dim("recorded locally; no policy named the key") |
|
| 413 | + | ), |
|
| 236 | 414 | } |
|
| 237 | 415 | if repo.config.author_key == Some(key) { |
|
| 238 | - | println!(" {} {}", ui::caution(), ui::warn("this was your configured key; run oxy identity create or rotate")); |
|
| 416 | + | println!( |
|
| 417 | + | " {} {}", |
|
| 418 | + | ui::caution(), |
|
| 419 | + | ui::warn("this was your configured key; run oxy identity create or rotate") |
|
| 420 | + | ); |
|
| 239 | 421 | } |
|
| 240 | 422 | Ok(0) |
|
| 241 | 423 | } |
|
| … | 25 unchanged lines |
||
| 267 | 449 | println!("{}", ui::dim("No delegates.")); |
|
| 268 | 450 | } |
|
| 269 | 451 | for d in &id.delegates { |
|
| 270 | - | let who = repo.identity(d).map(|i| format!("{} <{}>", i.name, i.email)).unwrap_or_else(|_| "(unknown identity)".into()); |
|
| 271 | - | println!(" {} {} {}", ui::ok(ui::glyph("🔑", "*")), ui::hash(d.short(12)), ui::author(who)); |
|
| 452 | + | let who = repo |
|
| 453 | + | .identity(d) |
|
| 454 | + | .map(|i| format!("{} <{}>", i.name, i.email)) |
|
| 455 | + | .unwrap_or_else(|_| "(unknown identity)".into()); |
|
| 456 | + | println!( |
|
| 457 | + | " {} {} {}", |
|
| 458 | + | ui::ok(ui::glyph("🔑", "*")), |
|
| 459 | + | ui::hash(d.short(12)), |
|
| 460 | + | ui::author(who) |
|
| 461 | + | ); |
|
| 272 | 462 | } |
|
| 273 | 463 | Ok(0) |
|
| 274 | 464 | } |
|
| … | 3 unchanged lines |
||
| 278 | 468 | let id = Identity::decode(&std::fs::read(identity_file)?)?; |
|
| 279 | 469 | let key = id.hash(); |
|
| 280 | 470 | let u = repo.policy_root_add(id.clone())?; |
|
| 281 | - | println!("{} root {} <{}> ({})", ui::done("Added"), ui::author(&id.name), id.email, key.short(12)); |
|
| 471 | + | println!( |
|
| 472 | + | "{} root {} <{}> ({})", |
|
| 473 | + | ui::done("Added"), |
|
| 474 | + | ui::author(&id.name), |
|
| 475 | + | id.email, |
|
| 476 | + | key.short(12) |
|
| 477 | + | ); |
|
| 282 | 478 | report_update(&u, out)?; |
|
| 283 | 479 | Ok(0) |
|
| 284 | 480 | } |
|
| … | 27 unchanged lines |
||
| 312 | 508 | pub fn policy_init() -> Result<i32> { |
|
| 313 | 509 | let repo = open()?; |
|
| 314 | 510 | let sp = repo.policy_init()?; |
|
| 315 | - | println!("{} policy with root key {}", ui::done("Created"), sp.policy.roots[0].short(12)); |
|
| 511 | + | println!( |
|
| 512 | + | "{} policy with root key {}", |
|
| 513 | + | ui::done("Created"), |
|
| 514 | + | sp.policy.roots[0].short(12) |
|
| 515 | + | ); |
|
| 316 | 516 | report_update(&sp, None)?; |
|
| 317 | 517 | Ok(0) |
|
| 318 | 518 | } |
|
| … | 4 unchanged lines |
||
| 323 | 523 | if as_json { |
|
| 324 | 524 | println!("null"); |
|
| 325 | 525 | } else { |
|
| 326 | - | println!("No policy: this repository is unverified. Run `oxy policy init` to create one."); |
|
| 526 | + | println!( |
|
| 527 | + | "No policy: this repository is unverified. Run `oxy policy init` to create one." |
|
| 528 | + | ); |
|
| 327 | 529 | } |
|
| 328 | 530 | return Ok(0); |
|
| 329 | 531 | }; |
|
| 330 | 532 | let p = &sp.policy; |
|
| 331 | 533 | if as_json { |
|
| 332 | 534 | let ident = |k: &oxy_core::hash::KeyId| { |
|
| 333 | - | let (name, email) = p.identity(k).map(|i| (i.name.clone(), i.email.clone())).unwrap_or_default(); |
|
| 334 | - | json::obj([("key", json::s(&k.to_hex())), ("name", json::s(&name)), ("email", json::s(&email))]) |
|
| 535 | + | let (name, email) = p |
|
| 536 | + | .identity(k) |
|
| 537 | + | .map(|i| (i.name.clone(), i.email.clone())) |
|
| 538 | + | .unwrap_or_default(); |
|
| 539 | + | json::obj([ |
|
| 540 | + | ("key", json::s(&k.to_hex())), |
|
| 541 | + | ("name", json::s(&name)), |
|
| 542 | + | ("email", json::s(&email)), |
|
| 543 | + | ]) |
|
| 335 | 544 | }; |
|
| 336 | 545 | println!( |
|
| 337 | 546 | "{}", |
|
| … | 1 unchanged lines |
||
| 339 | 548 | ("hash", json::s(&r.to_hex())), |
|
| 340 | 549 | ("serial", json::n(p.serial)), |
|
| 341 | 550 | ("expires_ms", json::n(p.expires)), |
|
| 342 | - | ("expired", json::b(p.is_expired(oxy_core::objects::Hlc::now().wall_ms))), |
|
| 551 | + | ( |
|
| 552 | + | "expired", |
|
| 553 | + | json::b(p.is_expired(oxy_core::objects::Hlc::now().wall_ms)) |
|
| 554 | + | ), |
|
| 343 | 555 | ("root_threshold", json::n(p.root_threshold)), |
|
| 344 | 556 | ("roots", json::arr(p.roots.iter().map(ident))), |
|
| 345 | 557 | ( |
|
| … | 13 unchanged lines |
||
| 359 | 571 | ); |
|
| 360 | 572 | return Ok(0); |
|
| 361 | 573 | } |
|
| 362 | - | let name = |k: &oxy_core::hash::KeyId| p.identity(k).map(|i| format!("{} <{}>", i.name, i.email)).unwrap_or_else(|| "(unknown identity)".into()); |
|
| 574 | + | let name = |k: &oxy_core::hash::KeyId| { |
|
| 575 | + | p.identity(k) |
|
| 576 | + | .map(|i| format!("{} <{}>", i.name, i.email)) |
|
| 577 | + | .unwrap_or_else(|| "(unknown identity)".into()) |
|
| 578 | + | }; |
|
| 363 | 579 | let expired = p.is_expired(oxy_core::objects::Hlc::now().wall_ms); |
|
| 364 | - | println!("{} {}{}serial {}{}expires {}{}", ui::section("policy"), ui::hash(format!("#{}", r.short(12))), ui::sep(), p.serial, ui::sep(), when(p.expires), if expired { ui::err(format!(" {} EXPIRED", ui::glyph("✖", "x"))) } else { String::new() }); |
|
| 365 | - | println!("{}", ui::section(format!("roots {}", ui::dim(format!("(threshold {})", p.root_threshold))))); |
|
| 580 | + | println!( |
|
| 581 | + | "{} {}{}serial {}{}expires {}{}", |
|
| 582 | + | ui::section("policy"), |
|
| 583 | + | ui::hash(format!("#{}", r.short(12))), |
|
| 584 | + | ui::sep(), |
|
| 585 | + | p.serial, |
|
| 586 | + | ui::sep(), |
|
| 587 | + | when(p.expires), |
|
| 588 | + | if expired { |
|
| 589 | + | ui::err(format!(" {} EXPIRED", ui::glyph("✖", "x"))) |
|
| 590 | + | } else { |
|
| 591 | + | String::new() |
|
| 592 | + | } |
|
| 593 | + | ); |
|
| 594 | + | println!( |
|
| 595 | + | "{}", |
|
| 596 | + | ui::section(format!( |
|
| 597 | + | "roots {}", |
|
| 598 | + | ui::dim(format!("(threshold {})", p.root_threshold)) |
|
| 599 | + | )) |
|
| 600 | + | ); |
|
| 366 | 601 | for k in &p.roots { |
|
| 367 | - | println!(" {} {} {}", ui::ok(ui::glyph("🔑", "*")), ui::hash(k.short(12)), ui::author(name(k))); |
|
| 602 | + | println!( |
|
| 603 | + | " {} {} {}", |
|
| 604 | + | ui::ok(ui::glyph("🔑", "*")), |
|
| 605 | + | ui::hash(k.short(12)), |
|
| 606 | + | ui::author(name(k)) |
|
| 607 | + | ); |
|
| 368 | 608 | } |
|
| 369 | 609 | println!("{}", ui::section("rules")); |
|
| 370 | 610 | for rule in &p.rules { |
|
| 371 | - | println!(" {} {:12} {}", ui::ok(ui::glyph("▸", ">")), ui::bookmark(&rule.pattern), ui::dim(format!("threshold {}, require {}, conflicts {}", rule.threshold, rule.require.name(), if rule.allow_conflicts { "allowed" } else { "refused" }))); |
|
| 611 | + | println!( |
|
| 612 | + | " {} {:12} {}", |
|
| 613 | + | ui::ok(ui::glyph("▸", ">")), |
|
| 614 | + | ui::bookmark(&rule.pattern), |
|
| 615 | + | ui::dim(format!( |
|
| 616 | + | "threshold {}, require {}, conflicts {}", |
|
| 617 | + | rule.threshold, |
|
| 618 | + | rule.require.name(), |
|
| 619 | + | if rule.allow_conflicts { |
|
| 620 | + | "allowed" |
|
| 621 | + | } else { |
|
| 622 | + | "refused" |
|
| 623 | + | } |
|
| 624 | + | )) |
|
| 625 | + | ); |
|
| 372 | 626 | for k in &rule.keys { |
|
| 373 | 627 | println!(" {} {}", ui::hash(k.short(12)), ui::author(name(k))); |
|
| 374 | 628 | } |
|
| … | 1 unchanged lines |
||
| 376 | 630 | if !p.revoked.is_empty() { |
|
| 377 | 631 | println!("{}", ui::section("revoked")); |
|
| 378 | 632 | for k in &p.revoked { |
|
| 379 | - | println!(" {} {} {}", ui::err(ui::glyph("✖", "x")), ui::hash(k.short(12)), ui::author(name(k))); |
|
| 633 | + | println!( |
|
| 634 | + | " {} {} {}", |
|
| 635 | + | ui::err(ui::glyph("✖", "x")), |
|
| 636 | + | ui::hash(k.short(12)), |
|
| 637 | + | ui::author(name(k)) |
|
| 638 | + | ); |
|
| 380 | 639 | } |
|
| 381 | 640 | } |
|
| 382 | 641 | Ok(0) |
|
| 383 | 642 | } |
|
| 384 | 643 | ||
| 385 | - | pub fn policy_grant(identity_file: &Path, pattern: Option<&str>, out: Option<PathBuf>) -> Result<i32> { |
|
| 644 | + | pub fn policy_grant( |
|
| 645 | + | identity_file: &Path, |
|
| 646 | + | pattern: Option<&str>, |
|
| 647 | + | out: Option<PathBuf>, |
|
| 648 | + | ) -> Result<i32> { |
|
| 386 | 649 | let repo = open()?; |
|
| 387 | 650 | let id = Identity::decode(&std::fs::read(identity_file)?)?; |
|
| 388 | 651 | let key = id.hash(); |
|
| 389 | 652 | let u = repo.policy_grant(id.clone(), pattern)?; |
|
| 390 | - | println!("{} {} <{}> ({}) on {}", ui::done("Granted"), ui::author(&id.name), id.email, key.short(12), ui::bookmark(pattern.unwrap_or("*"))); |
|
| 653 | + | println!( |
|
| 654 | + | "{} {} <{}> ({}) on {}", |
|
| 655 | + | ui::done("Granted"), |
|
| 656 | + | ui::author(&id.name), |
|
| 657 | + | id.email, |
|
| 658 | + | key.short(12), |
|
| 659 | + | ui::bookmark(pattern.unwrap_or("*")) |
|
| 660 | + | ); |
|
| 391 | 661 | report_update(&u, out)?; |
|
| 392 | 662 | Ok(0) |
|
| 393 | 663 | } |
|
| … | 12 unchanged lines |
modified src/cli/json.rs +4 −1
| … | 3 unchanged lines |
||
| 4 | 4 | pub use oxy_core::repo::{json_array as arr, json_str as s}; |
|
| 5 | 5 | ||
| 6 | 6 | pub fn obj<I: IntoIterator<Item = (&'static str, String)>>(pairs: I) -> String { |
|
| 7 | - | let inner: Vec<String> = pairs.into_iter().map(|(k, v)| format!("{}:{v}", s(k))).collect(); |
|
| 7 | + | let inner: Vec<String> = pairs |
|
| 8 | + | .into_iter() |
|
| 9 | + | .map(|(k, v)| format!("{}:{v}", s(k))) |
|
| 10 | + | .collect(); |
|
| 8 | 11 | format!("{{{}}}", inner.join(",")) |
|
| 9 | 12 | } |
|
| 10 | 13 | pub fn b(v: bool) -> String { |
|
| … | 14 unchanged lines |
modified src/cli/mod.rs +96 −24
| 1 | 1 | //! Command-line definitions and dispatch. |
|
| 2 | 2 | ||
| 3 | 3 | mod format; |
|
| 4 | + | mod identity; |
|
| 4 | 5 | mod json; |
|
| 5 | - | mod identity; |
|
| 6 | 6 | mod sync; |
|
| 7 | 7 | pub mod ui; |
|
| 8 | 8 | mod workspace; |
|
| … | 4 unchanged lines |
||
| 13 | 13 | use std::path::PathBuf; |
|
| 14 | 14 | ||
| 15 | 15 | #[derive(Parser)] |
|
| 16 | - | #[command(name = "oxy", version, about = "Oxygen: breathing fresh air into version control")] |
|
| 16 | + | #[command( |
|
| 17 | + | name = "oxy", |
|
| 18 | + | version, |
|
| 19 | + | about = "Oxygen: breathing fresh air into version control" |
|
| 20 | + | )] |
|
| 17 | 21 | struct Cli { |
|
| 18 | 22 | /// Plain output: no colour, no spinner (also implied by NO_COLOR or a non-terminal) |
|
| 19 | 23 | #[arg(long, global = true)] |
|
| … | 157 unchanged lines |
||
| 177 | 181 | into: Option<String>, |
|
| 178 | 182 | }, |
|
| 179 | 183 | /// Split a head: the named paths become a separate first change |
|
| 180 | - | Split { |
|
| 181 | - | change: String, |
|
| 182 | - | paths: Vec<String>, |
|
| 183 | - | }, |
|
| 184 | + | Split { change: String, paths: Vec<String> }, |
|
| 184 | 185 | /// Drop a head from the bookmark; its content stays on disk as uncommitted work |
|
| 185 | 186 | Abandon { change: String }, |
|
| 186 | 187 | /// Show one change (ChangeId prefix, #hash prefix, or @) |
|
| … | 242 unchanged lines |
||
| 429 | 430 | ||
| 430 | 431 | #[derive(Subcommand)] |
|
| 431 | 432 | enum RemoteCmd { |
|
| 432 | - | Add { name: String, url: String }, |
|
| 433 | - | Remove { name: String }, |
|
| 433 | + | Add { |
|
| 434 | + | name: String, |
|
| 435 | + | url: String, |
|
| 436 | + | }, |
|
| 437 | + | Remove { |
|
| 438 | + | name: String, |
|
| 439 | + | }, |
|
| 434 | 440 | List { |
|
| 435 | 441 | #[arg(long)] |
|
| 436 | 442 | json: bool, |
|
| … | 154 unchanged lines |
||
| 591 | 597 | match cli.cmd { |
|
| 592 | 598 | Cmd::Init { path, bare } => workspace::init(path, bare), |
|
| 593 | 599 | Cmd::Status { json } => workspace::status(json), |
|
| 594 | - | Cmd::Prompt { format, glyphs, color } => workspace::prompt(&format, glyphs, color.as_deref()), |
|
| 595 | - | Cmd::Commit { message, depends_on, paths, interactive, hunks } => workspace::commit(&message, &depends_on, &paths, interactive, &hunks), |
|
| 600 | + | Cmd::Prompt { |
|
| 601 | + | format, |
|
| 602 | + | glyphs, |
|
| 603 | + | color, |
|
| 604 | + | } => workspace::prompt(&format, glyphs, color.as_deref()), |
|
| 605 | + | Cmd::Commit { |
|
| 606 | + | message, |
|
| 607 | + | depends_on, |
|
| 608 | + | paths, |
|
| 609 | + | interactive, |
|
| 610 | + | hunks, |
|
| 611 | + | } => workspace::commit(&message, &depends_on, &paths, interactive, &hunks), |
|
| 596 | 612 | Cmd::Cat { path, change } => workspace::cat(&path, change.as_deref()), |
|
| 597 | 613 | Cmd::Root { json } => workspace::root(json), |
|
| 598 | 614 | Cmd::Version { json } => workspace::version(json), |
|
| … | 11 unchanged lines |
||
| 610 | 626 | BisectCmd::Status { json } => workspace::bisect_status(json), |
|
| 611 | 627 | BisectCmd::Reset => workspace::bisect_reset(), |
|
| 612 | 628 | }, |
|
| 613 | - | Cmd::Log { limit, verbose, path, json } => workspace::log(limit, verbose, path.as_deref(), json), |
|
| 629 | + | Cmd::Log { |
|
| 630 | + | limit, |
|
| 631 | + | verbose, |
|
| 632 | + | path, |
|
| 633 | + | json, |
|
| 634 | + | } => workspace::log(limit, verbose, path.as_deref(), json), |
|
| 614 | 635 | Cmd::Diff { change, json } => workspace::diff(change.as_deref(), json), |
|
| 615 | 636 | Cmd::Describe { message, change } => workspace::describe(&message, change.as_deref()), |
|
| 616 | 637 | Cmd::Amend { message, into } => workspace::amend(message.as_deref(), into.as_deref()), |
|
| … | 5 unchanged lines |
||
| 622 | 643 | Cmd::Undo => workspace::undo(), |
|
| 623 | 644 | Cmd::Gc { now } => workspace::gc(now), |
|
| 624 | 645 | Cmd::Checkpoint => workspace::checkpoint(), |
|
| 625 | - | Cmd::Op { cmd: OpCmd::Log { json } } => workspace::op_log(json), |
|
| 626 | - | Cmd::Clone { url, dest, max_blob, sparse, depth } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth), |
|
| 646 | + | Cmd::Op { |
|
| 647 | + | cmd: OpCmd::Log { json }, |
|
| 648 | + | } => workspace::op_log(json), |
|
| 649 | + | Cmd::Clone { |
|
| 650 | + | url, |
|
| 651 | + | dest, |
|
| 652 | + | max_blob, |
|
| 653 | + | sparse, |
|
| 654 | + | depth, |
|
| 655 | + | } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth), |
|
| 627 | 656 | Cmd::Deepen { depth, remote } => sync::deepen(&remote, depth), |
|
| 628 | 657 | Cmd::Sparse { paths } => workspace::sparse(&paths), |
|
| 629 | 658 | Cmd::Lock { path } => workspace::lock(&path), |
|
| … | 12 unchanged lines |
||
| 642 | 671 | }, |
|
| 643 | 672 | Cmd::Merge { bookmark } => workspace::merge(&bookmark), |
|
| 644 | 673 | Cmd::Submodule { cmd } => match cmd { |
|
| 645 | - | SubmoduleCmd::Add { url, path, bookmark } => sync::submodule_add(&url, &path, bookmark.as_deref()), |
|
| 674 | + | SubmoduleCmd::Add { |
|
| 675 | + | url, |
|
| 676 | + | path, |
|
| 677 | + | bookmark, |
|
| 678 | + | } => sync::submodule_add(&url, &path, bookmark.as_deref()), |
|
| 646 | 679 | SubmoduleCmd::Update { init, paths } => sync::submodule_update(init, &paths), |
|
| 647 | 680 | SubmoduleCmd::Status { json } => sync::submodule_status(json), |
|
| 648 | 681 | SubmoduleCmd::Remove { path } => sync::submodule_remove(&path), |
|
| 649 | 682 | }, |
|
| 650 | 683 | Cmd::Tag { cmd } => match cmd { |
|
| 651 | - | TagCmd::Create { name, changes, message } => workspace::tag_create(&name, &changes, &message), |
|
| 684 | + | TagCmd::Create { |
|
| 685 | + | name, |
|
| 686 | + | changes, |
|
| 687 | + | message, |
|
| 688 | + | } => workspace::tag_create(&name, &changes, &message), |
|
| 652 | 689 | TagCmd::List { json } => workspace::tag_list(json), |
|
| 653 | 690 | }, |
|
| 654 | 691 | Cmd::Remote { cmd } => match cmd { |
|
| … | 3 unchanged lines |
||
| 658 | 695 | }, |
|
| 659 | 696 | Cmd::Fetch { remote, json } => sync::fetch(remote.as_deref(), json), |
|
| 660 | 697 | Cmd::Pull { remote, json } => sync::pull(remote.as_deref(), json), |
|
| 661 | - | Cmd::Push { remote, force, bookmark, json } => sync::push(remote.as_deref(), force, bookmark.as_deref(), json), |
|
| 662 | - | Cmd::Serve { root, bind, tokens, tls_cert, tls_key } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)), |
|
| 698 | + | Cmd::Push { |
|
| 699 | + | remote, |
|
| 700 | + | force, |
|
| 701 | + | bookmark, |
|
| 702 | + | json, |
|
| 703 | + | } => sync::push(remote.as_deref(), force, bookmark.as_deref(), json), |
|
| 704 | + | Cmd::Serve { |
|
| 705 | + | root, |
|
| 706 | + | bind, |
|
| 707 | + | tokens, |
|
| 708 | + | tls_cert, |
|
| 709 | + | tls_key, |
|
| 710 | + | } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)), |
|
| 663 | 711 | Cmd::Identity { cmd } => match cmd { |
|
| 664 | - | IdentityCmd::Create { name, email, local, expires } => identity::create(name, email, local, expires), |
|
| 712 | + | IdentityCmd::Create { |
|
| 713 | + | name, |
|
| 714 | + | email, |
|
| 715 | + | local, |
|
| 716 | + | expires, |
|
| 717 | + | } => identity::create(name, email, local, expires), |
|
| 665 | 718 | IdentityCmd::Show => identity::show(), |
|
| 666 | 719 | IdentityCmd::Export { path } => identity::export(path), |
|
| 667 | 720 | IdentityCmd::Rotate => identity::rotate(), |
|
| … | 5 unchanged lines |
||
| 673 | 726 | DelegateCmd::List => identity::delegate_list(), |
|
| 674 | 727 | }, |
|
| 675 | 728 | }, |
|
| 676 | - | Cmd::Git { cmd: GitCmd::Export { git_dir, branch } } => sync::git_export(&git_dir, branch.as_deref()), |
|
| 677 | - | Cmd::Git { cmd: GitCmd::Fetch { url, branch } } => sync::git_fetch(&url, &branch), |
|
| 678 | - | Cmd::Git { cmd: GitCmd::Push { url, branch } } => sync::git_push(&url, &branch), |
|
| 679 | - | Cmd::Git { cmd: GitCmd::Import { git_dir, dest, branch } } => sync::git_import(&git_dir, &dest, &branch), |
|
| 729 | + | Cmd::Git { |
|
| 730 | + | cmd: GitCmd::Export { git_dir, branch }, |
|
| 731 | + | } => sync::git_export(&git_dir, branch.as_deref()), |
|
| 732 | + | Cmd::Git { |
|
| 733 | + | cmd: GitCmd::Fetch { url, branch }, |
|
| 734 | + | } => sync::git_fetch(&url, &branch), |
|
| 735 | + | Cmd::Git { |
|
| 736 | + | cmd: GitCmd::Push { url, branch }, |
|
| 737 | + | } => sync::git_push(&url, &branch), |
|
| 738 | + | Cmd::Git { |
|
| 739 | + | cmd: |
|
| 740 | + | GitCmd::Import { |
|
| 741 | + | git_dir, |
|
| 742 | + | dest, |
|
| 743 | + | branch, |
|
| 744 | + | }, |
|
| 745 | + | } => sync::git_import(&git_dir, &dest, &branch), |
|
| 680 | 746 | Cmd::Policy { cmd } => match cmd { |
|
| 681 | 747 | PolicyCmd::Init => identity::policy_init(), |
|
| 682 | 748 | PolicyCmd::Show { json } => identity::policy_show(json), |
|
| 683 | - | PolicyCmd::Grant { identity_file, pattern, out } => identity::policy_grant(&identity_file, pattern.as_deref(), out), |
|
| 749 | + | PolicyCmd::Grant { |
|
| 750 | + | identity_file, |
|
| 751 | + | pattern, |
|
| 752 | + | out, |
|
| 753 | + | } => identity::policy_grant(&identity_file, pattern.as_deref(), out), |
|
| 684 | 754 | PolicyCmd::Revoke { key, out } => identity::policy_revoke(&key, out), |
|
| 685 | 755 | PolicyCmd::Root { cmd } => match cmd { |
|
| 686 | - | RootCmd::Add { identity_file, out } => identity::policy_root_add(&identity_file, out), |
|
| 756 | + | RootCmd::Add { identity_file, out } => { |
|
| 757 | + | identity::policy_root_add(&identity_file, out) |
|
| 758 | + | } |
|
| 687 | 759 | RootCmd::Remove { key, out } => identity::policy_root_remove(&key, out), |
|
| 688 | 760 | RootCmd::Threshold { n, out } => identity::policy_threshold(n, out), |
|
| 689 | 761 | }, |
|
| … | 6 unchanged lines |
modified src/cli/sync.rs +292 −45
| 1 | 1 | //! Commands that talk to remotes, and the reference server. |
|
| 2 | 2 | ||
| 3 | 3 | use super::format::plural; |
|
| 4 | + | use super::json; |
|
| 4 | 5 | use super::open; |
|
| 5 | - | use super::json; |
|
| 6 | 6 | use super::ui; |
|
| 7 | 7 | use anyhow::{Context, Result}; |
|
| 8 | 8 | use oxy_core::repo::Repo; |
|
| 9 | 9 | use std::path::PathBuf; |
|
| 10 | 10 | ||
| 11 | - | pub fn clone(url: &str, dest: Option<PathBuf>, max_blob: Option<&str>, sparse: &[String], depth: Option<u32>) -> Result<i32> { |
|
| 11 | + | pub fn clone( |
|
| 12 | + | url: &str, |
|
| 13 | + | dest: Option<PathBuf>, |
|
| 14 | + | max_blob: Option<&str>, |
|
| 15 | + | sparse: &[String], |
|
| 16 | + | depth: Option<u32>, |
|
| 17 | + | ) -> Result<i32> { |
|
| 12 | 18 | let dest = dest.unwrap_or_else(|| { |
|
| 13 | - | let base = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo"); |
|
| 19 | + | let base = url |
|
| 20 | + | .trim_end_matches('/') |
|
| 21 | + | .rsplit('/') |
|
| 22 | + | .next() |
|
| 23 | + | .unwrap_or("repo"); |
|
| 14 | 24 | PathBuf::from(base) |
|
| 15 | 25 | }); |
|
| 16 | 26 | let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?; |
|
| … | 3 unchanged lines |
||
| 20 | 30 | None => Repo::clone_with(url, &dest, max_blob, sparse)?, |
|
| 21 | 31 | }; |
|
| 22 | 32 | let heads = repo.bookmark_heads()?; |
|
| 23 | - | sp.done(&format!("{} into {} {}", ui::done("Cloned"), ui::path(repo.root().display()), ui::dim(format!("({url}, {} with {})", ui::bookmark(&repo.config.bookmark), plural(heads.len(), "head"))))); |
|
| 33 | + | sp.done(&format!( |
|
| 34 | + | "{} into {} {}", |
|
| 35 | + | ui::done("Cloned"), |
|
| 36 | + | ui::path(repo.root().display()), |
|
| 37 | + | ui::dim(format!( |
|
| 38 | + | "({url}, {} with {})", |
|
| 39 | + | ui::bookmark(&repo.config.bookmark), |
|
| 40 | + | plural(heads.len(), "head") |
|
| 41 | + | )) |
|
| 42 | + | )); |
|
| 24 | 43 | Ok(0) |
|
| 25 | 44 | } |
|
| 26 | 45 | ||
| … | 1 unchanged lines |
||
| 28 | 47 | let mut repo = open()?; |
|
| 29 | 48 | repo.config.set(&format!("remote.{name}.url"), url)?; |
|
| 30 | 49 | repo.save_config()?; |
|
| 31 | - | println!("{} remote {} {} {url}", ui::done("Added"), ui::bookmark(name), ui::dim(ui::glyph("→", "->"))); |
|
| 50 | + | println!( |
|
| 51 | + | "{} remote {} {} {url}", |
|
| 52 | + | ui::done("Added"), |
|
| 53 | + | ui::bookmark(name), |
|
| 54 | + | ui::dim(ui::glyph("→", "->")) |
|
| 55 | + | ); |
|
| 32 | 56 | Ok(0) |
|
| 33 | 57 | } |
|
| 34 | 58 | ||
| … | 12 unchanged lines |
||
| 47 | 71 | let items = repo.config.remotes.iter().map(|(name, url)| { |
|
| 48 | 72 | let bms = view.remotes.get(name).map(|b| { |
|
| 49 | 73 | json::arr(b.iter().map(|(bm, st)| { |
|
| 50 | - | let heads = repo.store().get_state(st).map(|s| s.heads).unwrap_or_default(); |
|
| 74 | + | let heads = repo |
|
| 75 | + | .store() |
|
| 76 | + | .get_state(st) |
|
| 77 | + | .map(|s| s.heads) |
|
| 78 | + | .unwrap_or_default(); |
|
| 51 | 79 | json::obj([("name", json::s(bm)), ("heads", json::hashes(&heads))]) |
|
| 52 | 80 | })) |
|
| 53 | 81 | }); |
|
| 54 | - | json::obj([("name", json::s(name)), ("url", json::s(url)), ("bookmarks", bms.unwrap_or_else(|| "[]".into()))]) |
|
| 82 | + | json::obj([ |
|
| 83 | + | ("name", json::s(name)), |
|
| 84 | + | ("url", json::s(url)), |
|
| 85 | + | ("bookmarks", bms.unwrap_or_else(|| "[]".into())), |
|
| 86 | + | ]) |
|
| 55 | 87 | }); |
|
| 56 | 88 | println!("{}", json::arr(items)); |
|
| 57 | 89 | return Ok(0); |
|
| 58 | 90 | } |
|
| 59 | 91 | if repo.config.remotes.is_empty() { |
|
| 60 | - | println!("{}", ui::dim("No remotes; add one with oxy remote add <name> <url>.")); |
|
| 92 | + | println!( |
|
| 93 | + | "{}", |
|
| 94 | + | ui::dim("No remotes; add one with oxy remote add <name> <url>.") |
|
| 95 | + | ); |
|
| 61 | 96 | } |
|
| 62 | 97 | for (name, url) in &repo.config.remotes { |
|
| 63 | - | println!("{} {}{}{}", ui::ok(ui::glyph("⇄", "<>")), ui::bookmark(name), ui::sep(), ui::dim(url)); |
|
| 98 | + | println!( |
|
| 99 | + | "{} {}{}{}", |
|
| 100 | + | ui::ok(ui::glyph("⇄", "<>")), |
|
| 101 | + | ui::bookmark(name), |
|
| 102 | + | ui::sep(), |
|
| 103 | + | ui::dim(url) |
|
| 104 | + | ); |
|
| 64 | 105 | if let Some(bms) = view.remotes.get(name) { |
|
| 65 | 106 | for (bm, st) in bms { |
|
| 66 | - | let heads = repo.store().get_state(st).map(|s| s.heads.len()).unwrap_or(0); |
|
| 67 | - | println!(" {} {}{}{}", ui::dim(ui::glyph("↳", "->")), ui::id(format!("{name}/{bm}")), ui::sep(), ui::dim(plural(heads, "head"))); |
|
| 107 | + | let heads = repo |
|
| 108 | + | .store() |
|
| 109 | + | .get_state(st) |
|
| 110 | + | .map(|s| s.heads.len()) |
|
| 111 | + | .unwrap_or(0); |
|
| 112 | + | println!( |
|
| 113 | + | " {} {}{}{}", |
|
| 114 | + | ui::dim(ui::glyph("↳", "->")), |
|
| 115 | + | ui::id(format!("{name}/{bm}")), |
|
| 116 | + | ui::sep(), |
|
| 117 | + | ui::dim(plural(heads, "head")) |
|
| 118 | + | ); |
|
| 68 | 119 | } |
|
| 69 | 120 | } |
|
| 70 | 121 | } |
|
| … | 15 unchanged lines |
||
| 86 | 137 | "{}", |
|
| 87 | 138 | json::obj([ |
|
| 88 | 139 | ("remote", json::s(&remote)), |
|
| 89 | - | ("refs", json::arr(report.refs.iter().map(|(name, heads, n)| json::obj([("name", json::s(name)), ("heads", json::hashes(heads)), ("new", json::n(*n))])))), |
|
| 140 | + | ( |
|
| 141 | + | "refs", |
|
| 142 | + | json::arr(report.refs.iter().map(|(name, heads, n)| json::obj([ |
|
| 143 | + | ("name", json::s(name)), |
|
| 144 | + | ("heads", json::hashes(heads)), |
|
| 145 | + | ("new", json::n(*n)) |
|
| 146 | + | ]))) |
|
| 147 | + | ), |
|
| 90 | 148 | ("new_tags", json::n(report.new_tags)), |
|
| 91 | 149 | ("unverified", json::b(report.unverified)), |
|
| 92 | 150 | ]) |
|
| … | 1 unchanged lines |
||
| 94 | 152 | return Ok(0); |
|
| 95 | 153 | } |
|
| 96 | 154 | if report.refs.is_empty() { |
|
| 97 | - | println!("{} {}", ui::done("Fetched"), ui::dim(format!("nothing new from {remote}"))); |
|
| 155 | + | println!( |
|
| 156 | + | "{} {}", |
|
| 157 | + | ui::done("Fetched"), |
|
| 158 | + | ui::dim(format!("nothing new from {remote}")) |
|
| 159 | + | ); |
|
| 98 | 160 | } |
|
| 99 | 161 | for (name, heads, n) in &report.refs { |
|
| 100 | - | let what = if *n == 0 { ui::dim("up to date") } else { ui::ok(plural(*n, "new change")) }; |
|
| 101 | - | println!("{} {}{}{}{}{what}", ui::done("Fetched"), ui::id(format!("{remote}/{name}")), ui::sep(), ui::dim(plural(heads.len(), "head")), ui::sep()); |
|
| 162 | + | let what = if *n == 0 { |
|
| 163 | + | ui::dim("up to date") |
|
| 164 | + | } else { |
|
| 165 | + | ui::ok(plural(*n, "new change")) |
|
| 166 | + | }; |
|
| 167 | + | println!( |
|
| 168 | + | "{} {}{}{}{}{what}", |
|
| 169 | + | ui::done("Fetched"), |
|
| 170 | + | ui::id(format!("{remote}/{name}")), |
|
| 171 | + | ui::sep(), |
|
| 172 | + | ui::dim(plural(heads.len(), "head")), |
|
| 173 | + | ui::sep() |
|
| 174 | + | ); |
|
| 102 | 175 | } |
|
| 103 | 176 | if report.new_tags > 0 { |
|
| 104 | - | println!(" {} {}", ui::note(), ui::dim(format!("{} adopted", plural(report.new_tags, "tag")))); |
|
| 177 | + | println!( |
|
| 178 | + | " {} {}", |
|
| 179 | + | ui::note(), |
|
| 180 | + | ui::dim(format!("{} adopted", plural(report.new_tags, "tag"))) |
|
| 181 | + | ); |
|
| 105 | 182 | } |
|
| 106 | 183 | if report.unverified { |
|
| 107 | - | eprintln!("{} {remote} has no trust policy; nothing fetched from it was verified", ui::caution()); |
|
| 184 | + | eprintln!( |
|
| 185 | + | "{} {remote} has no trust policy; nothing fetched from it was verified", |
|
| 186 | + | ui::caution() |
|
| 187 | + | ); |
|
| 108 | 188 | } |
|
| 109 | 189 | Ok(0) |
|
| 110 | 190 | } |
|
| … | 6 unchanged lines |
||
| 117 | 197 | if as_json { |
|
| 118 | 198 | sp.clear(); |
|
| 119 | 199 | let conflicted = repo.snapshot("oxy pull")?.base_snapshot.is_conflicted(); |
|
| 120 | - | println!("{}", json::obj([("remote", json::s(&remote)), ("heads", json::hashes(&r.heads)), ("changed", json::b(r.changed)), ("checked_out", json::b(r.checked_out)), ("conflicted", json::b(conflicted))])); |
|
| 200 | + | println!( |
|
| 201 | + | "{}", |
|
| 202 | + | json::obj([ |
|
| 203 | + | ("remote", json::s(&remote)), |
|
| 204 | + | ("heads", json::hashes(&r.heads)), |
|
| 205 | + | ("changed", json::b(r.changed)), |
|
| 206 | + | ("checked_out", json::b(r.checked_out)), |
|
| 207 | + | ("conflicted", json::b(conflicted)) |
|
| 208 | + | ]) |
|
| 209 | + | ); |
|
| 121 | 210 | return Ok(0); |
|
| 122 | 211 | } |
|
| 123 | 212 | if !r.changed { |
|
| 124 | - | sp.done(&format!("{} {}", ui::ok(ui::glyph("✔", "*")), ui::dim("Already up to date."))); |
|
| 213 | + | sp.done(&format!( |
|
| 214 | + | "{} {}", |
|
| 215 | + | ui::ok(ui::glyph("✔", "*")), |
|
| 216 | + | ui::dim("Already up to date.") |
|
| 217 | + | )); |
|
| 125 | 218 | return Ok(0); |
|
| 126 | 219 | } |
|
| 127 | 220 | sp.done(&format!( |
|
| … | 1 unchanged lines |
||
| 129 | 222 | ui::done("Pulled:"), |
|
| 130 | 223 | ui::bookmark(&repo.config.bookmark), |
|
| 131 | 224 | plural(r.heads.len(), "head"), |
|
| 132 | - | ui::dim(if r.checked_out { "; working copy updated" } else { "; run oxy checkout to update the working copy" }) |
|
| 225 | + | ui::dim(if r.checked_out { |
|
| 226 | + | "; working copy updated" |
|
| 227 | + | } else { |
|
| 228 | + | "; run oxy checkout to update the working copy" |
|
| 229 | + | }) |
|
| 133 | 230 | )); |
|
| 134 | 231 | let snap = repo.snapshot("oxy pull")?; |
|
| 135 | 232 | if snap.base_snapshot.is_conflicted() { |
|
| 136 | - | println!(" {} {}", ui::caution(), ui::warn("the result has conflicts; see oxy status")); |
|
| 233 | + | println!( |
|
| 234 | + | " {} {}", |
|
| 235 | + | ui::caution(), |
|
| 236 | + | ui::warn("the result has conflicts; see oxy status") |
|
| 237 | + | ); |
|
| 137 | 238 | } |
|
| 138 | 239 | Ok(0) |
|
| 139 | 240 | } |
|
| 140 | 241 | ||
| 141 | - | pub fn push(remote: Option<&str>, force: bool, bookmark: Option<&str>, as_json: bool) -> Result<i32> { |
|
| 242 | + | pub fn push( |
|
| 243 | + | remote: Option<&str>, |
|
| 244 | + | force: bool, |
|
| 245 | + | bookmark: Option<&str>, |
|
| 246 | + | as_json: bool, |
|
| 247 | + | ) -> Result<i32> { |
|
| 142 | 248 | let repo = open()?; |
|
| 143 | 249 | let remote = remote_name(remote); |
|
| 144 | 250 | let sp = ui::spinner(&format!("Pushing to {remote}…")); |
|
| … | 3 unchanged lines |
||
| 148 | 254 | }; |
|
| 149 | 255 | sp.clear(); |
|
| 150 | 256 | if as_json { |
|
| 151 | - | println!("{}", json::arr(results.iter().map(|r| json::obj([("name", json::s(&r.name)), ("ok", json::b(r.ok)), ("reason", json::s(&r.reason))])))); |
|
| 257 | + | println!( |
|
| 258 | + | "{}", |
|
| 259 | + | json::arr(results.iter().map(|r| json::obj([ |
|
| 260 | + | ("name", json::s(&r.name)), |
|
| 261 | + | ("ok", json::b(r.ok)), |
|
| 262 | + | ("reason", json::s(&r.reason)) |
|
| 263 | + | ]))) |
|
| 264 | + | ); |
|
| 152 | 265 | return Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 }); |
|
| 153 | 266 | } |
|
| 154 | 267 | let mut code = 0; |
|
| 155 | 268 | for r in results { |
|
| 156 | 269 | if r.ok { |
|
| 157 | - | println!("{} {}{}{}", ui::done("Pushed"), ui::id(format!("{remote}/{}", r.name)), ui::sep(), ui::dim(if r.reason.is_empty() { "updated" } else { r.reason.as_str() })); |
|
| 270 | + | println!( |
|
| 271 | + | "{} {}{}{}", |
|
| 272 | + | ui::done("Pushed"), |
|
| 273 | + | ui::id(format!("{remote}/{}", r.name)), |
|
| 274 | + | ui::sep(), |
|
| 275 | + | ui::dim(if r.reason.is_empty() { |
|
| 276 | + | "updated" |
|
| 277 | + | } else { |
|
| 278 | + | r.reason.as_str() |
|
| 279 | + | }) |
|
| 280 | + | ); |
|
| 158 | 281 | } else { |
|
| 159 | - | println!("{} {} {}{}{}", ui::err(ui::glyph("✖", "x")), ui::err("Rejected"), ui::id(format!("{remote}/{}", r.name)), ui::sep(), r.reason); |
|
| 282 | + | println!( |
|
| 283 | + | "{} {} {}{}{}", |
|
| 284 | + | ui::err(ui::glyph("✖", "x")), |
|
| 285 | + | ui::err("Rejected"), |
|
| 286 | + | ui::id(format!("{remote}/{}", r.name)), |
|
| 287 | + | ui::sep(), |
|
| 288 | + | r.reason |
|
| 289 | + | ); |
|
| 160 | 290 | code = 1; |
|
| 161 | 291 | } |
|
| 162 | 292 | } |
|
| 163 | 293 | Ok(code) |
|
| 164 | 294 | } |
|
| 165 | 295 | ||
| 166 | - | pub fn serve(root: PathBuf, bind: &str, tokens: Vec<String>, tls: Option<(PathBuf, PathBuf)>) -> Result<i32> { |
|
| 296 | + | pub fn serve( |
|
| 297 | + | root: PathBuf, |
|
| 298 | + | bind: &str, |
|
| 299 | + | tokens: Vec<String>, |
|
| 300 | + | tls: Option<(PathBuf, PathBuf)>, |
|
| 301 | + | ) -> Result<i32> { |
|
| 167 | 302 | let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?; |
|
| 168 | 303 | let root = root.canonicalize()?; |
|
| 169 | 304 | let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key }); |
|
| … | 1 unchanged lines |
||
| 171 | 306 | "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})", |
|
| 172 | 307 | root.display(), |
|
| 173 | 308 | if tls.is_some() { "https" } else { "http" }, |
|
| 174 | - | if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" } |
|
| 309 | + | if tokens.is_empty() { |
|
| 310 | + | "open: no push tokens configured" |
|
| 311 | + | } else { |
|
| 312 | + | "push requires a token" |
|
| 313 | + | } |
|
| 175 | 314 | ); |
|
| 176 | - | if tls.is_none() && !bind.starts_with("127.") && !bind.starts_with("localhost") && !bind.starts_with("[::1]") { |
|
| 315 | + | if tls.is_none() |
|
| 316 | + | && !bind.starts_with("127.") |
|
| 317 | + | && !bind.starts_with("localhost") |
|
| 318 | + | && !bind.starts_with("[::1]") |
|
| 319 | + | { |
|
| 177 | 320 | eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key"); |
|
| 178 | 321 | } |
|
| 179 | 322 | oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?; |
|
| … | 14 unchanged lines |
||
| 194 | 337 | plural(r.exported, "new state"), |
|
| 195 | 338 | ui::path(git_dir.display()), |
|
| 196 | 339 | ui::bookmark(&r.branch), |
|
| 197 | - | ui::dim(format!("(currently {}; {} already exported)", plural(r.heads, "head"), r.skipped)) |
|
| 340 | + | ui::dim(format!( |
|
| 341 | + | "(currently {}; {} already exported)", |
|
| 342 | + | plural(r.heads, "head"), |
|
| 343 | + | r.skipped |
|
| 344 | + | )) |
|
| 198 | 345 | ); |
|
| 199 | 346 | Ok(0) |
|
| 200 | 347 | } |
|
| … | 4 unchanged lines |
||
| 205 | 352 | let r = repo.git_fetch(url, branch)?; |
|
| 206 | 353 | sp.clear(); |
|
| 207 | 354 | if r.commits == 0 { |
|
| 208 | - | println!("{} {}", ui::done("Fetched"), ui::dim(format!("nothing new from {url} {branch}"))); |
|
| 355 | + | println!( |
|
| 356 | + | "{} {}", |
|
| 357 | + | ui::done("Fetched"), |
|
| 358 | + | ui::dim(format!("nothing new from {url} {branch}")) |
|
| 359 | + | ); |
|
| 209 | 360 | } else { |
|
| 210 | 361 | println!( |
|
| 211 | 362 | "{} {} ({} merges) from {} {}{}{} now has {}", |
|
| … | 9 unchanged lines |
||
| 221 | 372 | if !repo.snapshot("oxy git fetch")?.changed { |
|
| 222 | 373 | println!(" {} {}", ui::note(), ui::dim("working copy updated")); |
|
| 223 | 374 | } else { |
|
| 224 | - | println!(" {} {}", ui::note(), ui::dim("run oxy checkout to update the working copy")); |
|
| 375 | + | println!( |
|
| 376 | + | " {} {}", |
|
| 377 | + | ui::note(), |
|
| 378 | + | ui::dim("run oxy checkout to update the working copy") |
|
| 379 | + | ); |
|
| 225 | 380 | } |
|
| 226 | 381 | } |
|
| 227 | 382 | if r.submodules > 0 { |
|
| 228 | - | println!(" {} {}", ui::note(), ui::dim(format!("{} submodule {} imported as subrepos with Git pins", r.submodules, if r.submodules == 1 { "entry" } else { "entries" }))); |
|
| 383 | + | println!( |
|
| 384 | + | " {} {}", |
|
| 385 | + | ui::note(), |
|
| 386 | + | ui::dim(format!( |
|
| 387 | + | "{} submodule {} imported as subrepos with Git pins", |
|
| 388 | + | r.submodules, |
|
| 389 | + | if r.submodules == 1 { |
|
| 390 | + | "entry" |
|
| 391 | + | } else { |
|
| 392 | + | "entries" |
|
| 393 | + | } |
|
| 394 | + | )) |
|
| 395 | + | ); |
|
| 229 | 396 | } |
|
| 230 | 397 | Ok(0) |
|
| 231 | 398 | } |
|
| … | 28 unchanged lines |
||
| 260 | 427 | ui::path(git_dir.display()), |
|
| 261 | 428 | ui::bookmark(branch), |
|
| 262 | 429 | ui::path(repo.root().display()), |
|
| 263 | - | ui::dim(format!("in {:.1?}; {} now has {}", started.elapsed(), repo.config.bookmark, plural(r.heads, "head"))) |
|
| 430 | + | ui::dim(format!( |
|
| 431 | + | "in {:.1?}; {} now has {}", |
|
| 432 | + | started.elapsed(), |
|
| 433 | + | repo.config.bookmark, |
|
| 434 | + | plural(r.heads, "head") |
|
| 435 | + | )) |
|
| 264 | 436 | ); |
|
| 265 | 437 | if r.submodules > 0 { |
|
| 266 | - | println!(" {} {}", ui::note(), ui::dim(format!("{} submodule {} imported as subrepos with Git pins", r.submodules, if r.submodules == 1 { "entry" } else { "entries" }))); |
|
| 438 | + | println!( |
|
| 439 | + | " {} {}", |
|
| 440 | + | ui::note(), |
|
| 441 | + | ui::dim(format!( |
|
| 442 | + | "{} submodule {} imported as subrepos with Git pins", |
|
| 443 | + | r.submodules, |
|
| 444 | + | if r.submodules == 1 { |
|
| 445 | + | "entry" |
|
| 446 | + | } else { |
|
| 447 | + | "entries" |
|
| 448 | + | } |
|
| 449 | + | )) |
|
| 450 | + | ); |
|
| 267 | 451 | } |
|
| 268 | 452 | Ok(0) |
|
| 269 | 453 | } |
|
| … | 2 unchanged lines |
||
| 272 | 456 | let repo = open()?; |
|
| 273 | 457 | let sp = ui::spinner(&format!("Cloning {url} into {path}…")); |
|
| 274 | 458 | let pin = repo.submodule_add(url, path, bookmark)?; |
|
| 275 | - | sp.done(&format!("{} subrepo {} {} {}", ui::done("Added"), ui::path(path), ui::dim(ui::glyph("→", "->")), ui::dim(pin.summary()))); |
|
| 276 | - | println!(" {} {}", ui::note(), ui::dim("recorded in .oxymodules; commit to pin it")); |
|
| 459 | + | sp.done(&format!( |
|
| 460 | + | "{} subrepo {} {} {}", |
|
| 461 | + | ui::done("Added"), |
|
| 462 | + | ui::path(path), |
|
| 463 | + | ui::dim(ui::glyph("→", "->")), |
|
| 464 | + | ui::dim(pin.summary()) |
|
| 465 | + | )); |
|
| 466 | + | println!( |
|
| 467 | + | " {} {}", |
|
| 468 | + | ui::note(), |
|
| 469 | + | ui::dim("recorded in .oxymodules; commit to pin it") |
|
| 470 | + | ); |
|
| 277 | 471 | Ok(0) |
|
| 278 | 472 | } |
|
| 279 | 473 | ||
| … | 13 unchanged lines |
||
| 293 | 487 | UpToDate => ui::ok(format!("{} up to date", ui::glyph("✔", "*"))), |
|
| 294 | 488 | Missing => { |
|
| 295 | 489 | code = 1; |
|
| 296 | - | ui::warn(format!("{} missing (pass --init to clone)", ui::glyph("⚠", "!"))) |
|
| 490 | + | ui::warn(format!( |
|
| 491 | + | "{} missing (pass --init to clone)", |
|
| 492 | + | ui::glyph("⚠", "!") |
|
| 493 | + | )) |
|
| 297 | 494 | } |
|
| 298 | 495 | Dirty => { |
|
| 299 | 496 | code = 1; |
|
| 300 | - | ui::warn(format!("{} has uncommitted changes; left as is", ui::glyph("⚠", "!"))) |
|
| 497 | + | ui::warn(format!( |
|
| 498 | + | "{} has uncommitted changes; left as is", |
|
| 499 | + | ui::glyph("⚠", "!") |
|
| 500 | + | )) |
|
| 301 | 501 | } |
|
| 302 | 502 | Failed(e) => { |
|
| 303 | 503 | code = 1; |
|
| … | 7 unchanged lines |
||
| 311 | 511 | ||
| 312 | 512 | fn pin_json(p: &oxy_core::subrepo::Pin) -> String { |
|
| 313 | 513 | match p { |
|
| 314 | - | oxy_core::subrepo::Pin::Oxy { repo, heads, bookmark } => json::obj([("kind", json::s("oxy")), ("repo", json::s(&repo.iter().map(|b| format!("{b:02x}")).collect::<String>())), ("heads", json::hashes(heads)), ("bookmark", json::s(bookmark))]), |
|
| 315 | - | oxy_core::subrepo::Pin::Git { sha, branch } => json::obj([("kind", json::s("git")), ("sha", json::s(sha)), ("branch", json::s(branch))]), |
|
| 514 | + | oxy_core::subrepo::Pin::Oxy { |
|
| 515 | + | repo, |
|
| 516 | + | heads, |
|
| 517 | + | bookmark, |
|
| 518 | + | } => json::obj([ |
|
| 519 | + | ("kind", json::s("oxy")), |
|
| 520 | + | ( |
|
| 521 | + | "repo", |
|
| 522 | + | json::s(&repo.iter().map(|b| format!("{b:02x}")).collect::<String>()), |
|
| 523 | + | ), |
|
| 524 | + | ("heads", json::hashes(heads)), |
|
| 525 | + | ("bookmark", json::s(bookmark)), |
|
| 526 | + | ]), |
|
| 527 | + | oxy_core::subrepo::Pin::Git { sha, branch } => json::obj([ |
|
| 528 | + | ("kind", json::s("git")), |
|
| 529 | + | ("sha", json::s(sha)), |
|
| 530 | + | ("branch", json::s(branch)), |
|
| 531 | + | ]), |
|
| 316 | 532 | } |
|
| 317 | 533 | } |
|
| 318 | 534 | ||
| … | 23 unchanged lines |
||
| 342 | 558 | let state = match (&s.actual, s.dirty) { |
|
| 343 | 559 | (None, _) => ui::warn(format!("{} missing", ui::glyph("⚠", "!"))), |
|
| 344 | 560 | (Some(a), dirty) if *a == s.pin => { |
|
| 345 | - | if dirty { ui::warn(format!("{} at pin, uncommitted changes", ui::glyph("✎", "~"))) } else { ui::ok(format!("{} at pin", ui::glyph("✔", "*"))) } |
|
| 561 | + | if dirty { |
|
| 562 | + | ui::warn(format!( |
|
| 563 | + | "{} at pin, uncommitted changes", |
|
| 564 | + | ui::glyph("✎", "~") |
|
| 565 | + | )) |
|
| 566 | + | } else { |
|
| 567 | + | ui::ok(format!("{} at pin", ui::glyph("✔", "*"))) |
|
| 568 | + | } |
|
| 346 | 569 | } |
|
| 347 | 570 | (Some(a), _) => ui::warn(format!("{} at {}", ui::glyph("↕", "<>"), a.summary())), |
|
| 348 | 571 | }; |
|
| 349 | - | println!("{} {}{}{}{}{state}", ui::ok(ui::glyph("⧉", "#")), ui::path(&s.path), ui::sep(), ui::dim(s.pin.summary()), ui::sep()); |
|
| 572 | + | println!( |
|
| 573 | + | "{} {}{}{}{}{state}", |
|
| 574 | + | ui::ok(ui::glyph("⧉", "#")), |
|
| 575 | + | ui::path(&s.path), |
|
| 576 | + | ui::sep(), |
|
| 577 | + | ui::dim(s.pin.summary()), |
|
| 578 | + | ui::sep() |
|
| 579 | + | ); |
|
| 350 | 580 | if let Some(u) = s.url { |
|
| 351 | 581 | println!(" {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u)); |
|
| 352 | 582 | } |
|
| … | 4 unchanged lines |
||
| 357 | 587 | pub fn submodule_remove(path: &str) -> Result<i32> { |
|
| 358 | 588 | let repo = open()?; |
|
| 359 | 589 | repo.submodule_remove(path)?; |
|
| 360 | - | println!("{} subrepo {} {}", ui::done("Removed"), ui::path(path), ui::dim("(directory left in place; commit to record the removal)")); |
|
| 590 | + | println!( |
|
| 591 | + | "{} subrepo {} {}", |
|
| 592 | + | ui::done("Removed"), |
|
| 593 | + | ui::path(path), |
|
| 594 | + | ui::dim("(directory left in place; commit to record the removal)") |
|
| 595 | + | ); |
|
| 361 | 596 | Ok(0) |
|
| 362 | 597 | } |
|
| 363 | 598 | ||
| 364 | 599 | pub fn deepen(remote: &str, depth: u32) -> Result<i32> { |
|
| 365 | 600 | let repo = open()?; |
|
| 366 | 601 | if !repo.is_shallow() { |
|
| 367 | - | println!("{}", ui::dim("This repository is not shallow; nothing to deepen.")); |
|
| 602 | + | println!( |
|
| 603 | + | "{}", |
|
| 604 | + | ui::dim("This repository is not shallow; nothing to deepen.") |
|
| 605 | + | ); |
|
| 368 | 606 | return Ok(0); |
|
| 369 | 607 | } |
|
| 370 | 608 | let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…")); |
|
| 371 | 609 | let n = repo.deepen(remote, depth)?; |
|
| 372 | - | sp.done(&format!("{} {}; {}", ui::ok("Deepened by"), plural(n, "change"), if repo.is_shallow() { "still shallow".to_string() } else { ui::ok("history is now complete") })); |
|
| 610 | + | sp.done(&format!( |
|
| 611 | + | "{} {}; {}", |
|
| 612 | + | ui::ok("Deepened by"), |
|
| 613 | + | plural(n, "change"), |
|
| 614 | + | if repo.is_shallow() { |
|
| 615 | + | "still shallow".to_string() |
|
| 616 | + | } else { |
|
| 617 | + | ui::ok("history is now complete") |
|
| 618 | + | } |
|
| 619 | + | )); |
|
| 373 | 620 | Ok(0) |
|
| 374 | 621 | } |
modified src/cli/ui.rs +26 −6
| … | 22 unchanged lines |
||
| 23 | 23 | .iter() |
|
| 24 | 24 | .filter_map(|k| std::env::var(k).ok()) |
|
| 25 | 25 | .find(|v| !v.is_empty()) |
|
| 26 | - | .map(|v| v.to_ascii_uppercase().contains("UTF-8") || v.to_ascii_uppercase().contains("UTF8")) |
|
| 26 | + | .map(|v| { |
|
| 27 | + | v.to_ascii_uppercase().contains("UTF-8") || v.to_ascii_uppercase().contains("UTF8") |
|
| 28 | + | }) |
|
| 27 | 29 | .unwrap_or(false); |
|
| 28 | 30 | UNICODE.store(!plain && utf8, Ordering::Relaxed); |
|
| 29 | 31 | }); |
|
| … | 10 unchanged lines |
||
| 40 | 42 | ||
| 41 | 43 | /// A glyph with an ASCII fallback for plain output and non-UTF-8 locales. |
|
| 42 | 44 | pub fn glyph<'a>(fancy: &'a str, ascii: &'a str) -> &'a str { |
|
| 43 | - | if unicode() { fancy } else { ascii } |
|
| 45 | + | if unicode() { |
|
| 46 | + | fancy |
|
| 47 | + | } else { |
|
| 48 | + | ascii |
|
| 49 | + | } |
|
| 44 | 50 | } |
|
| 45 | 51 | ||
| 46 | 52 | /// A finished action: a tick and the verb in green. |
|
| … | 44 unchanged lines |
||
| 91 | 97 | /// Milliseconds since the epoch as a relative time ("3 minutes ago"), or the |
|
| 92 | 98 | /// date when older than a month. |
|
| 93 | 99 | pub fn ago(ms: u64) -> String { |
|
| 94 | - | let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0); |
|
| 100 | + | let now = std::time::SystemTime::now() |
|
| 101 | + | .duration_since(std::time::UNIX_EPOCH) |
|
| 102 | + | .map(|d| d.as_millis() as u64) |
|
| 103 | + | .unwrap_or(0); |
|
| 95 | 104 | let secs = now.saturating_sub(ms) / 1000; |
|
| 96 | 105 | let unit = |n: u64, w: &str| format!("{n} {w}{} ago", if n == 1 { "" } else { "s" }); |
|
| 97 | 106 | match secs { |
|
| … | 4 unchanged lines |
||
| 102 | 111 | 5_400..=79_199 => unit(secs.div_ceil(3_600), "hour"), |
|
| 103 | 112 | 79_200..=129_599 => "yesterday".into(), |
|
| 104 | 113 | 129_600..=2_591_999 => unit(secs.div_ceil(86_400), "day"), |
|
| 105 | - | _ => super::format::when(ms).split(' ').next().unwrap_or("").to_string(), |
|
| 114 | + | _ => super::format::when(ms) |
|
| 115 | + | .split(' ') |
|
| 116 | + | .next() |
|
| 117 | + | .unwrap_or("") |
|
| 118 | + | .to_string(), |
|
| 106 | 119 | } |
|
| 107 | 120 | } |
|
| 108 | 121 | ||
| … | 82 unchanged lines |
||
| 191 | 204 | return Spinner { bar: None }; |
|
| 192 | 205 | } |
|
| 193 | 206 | let bar = ProgressBar::new_spinner(); |
|
| 194 | - | bar.set_style(ProgressStyle::with_template("{spinner:.cyan} {msg}").unwrap().tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", "✔"])); |
|
| 207 | + | bar.set_style( |
|
| 208 | + | ProgressStyle::with_template("{spinner:.cyan} {msg}") |
|
| 209 | + | .unwrap() |
|
| 210 | + | .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", "✔"]), |
|
| 211 | + | ); |
|
| 195 | 212 | bar.set_message(msg.to_string()); |
|
| 196 | 213 | bar.enable_steady_tick(Duration::from_millis(80)); |
|
| 197 | 214 | Spinner { bar: Some(bar) } |
|
| … | 21 unchanged lines |
||
| 219 | 236 | ||
| 220 | 237 | #[test] |
|
| 221 | 238 | fn relative_times() { |
|
| 222 | - | let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; |
|
| 239 | + | let now = std::time::SystemTime::now() |
|
| 240 | + | .duration_since(std::time::UNIX_EPOCH) |
|
| 241 | + | .unwrap() |
|
| 242 | + | .as_millis() as u64; |
|
| 223 | 243 | assert_eq!(ago(now), "just now"); |
|
| 224 | 244 | assert_eq!(ago(now - 60_000), "a minute ago"); |
|
| 225 | 245 | assert_eq!(ago(now - 5 * 60_000), "5 minutes ago"); |
|
| … | 7 unchanged lines |
modified src/cli/workspace.rs +658 −109
| 1 | 1 | //! Commands that act on the local repository and working copy. |
|
| 2 | 2 | ||
| 3 | - | use super::format::{author_name, deps_line, deps_of, plural, short_id, short_meta_id, signature_line, signature_mark, subject_of, when}; |
|
| 3 | + | use super::format::{ |
|
| 4 | + | author_name, deps_line, deps_of, plural, short_id, short_meta_id, signature_line, |
|
| 5 | + | signature_mark, subject_of, when, |
|
| 6 | + | }; |
|
| 4 | 7 | use super::json; |
|
| 5 | - | use oxy_core::repo::SignatureStatus; |
|
| 6 | 8 | use super::open; |
|
| 7 | 9 | use super::ui; |
|
| 8 | 10 | use anyhow::Result; |
|
| 9 | 11 | use oxy_core::objects::ChangeKind; |
|
| 12 | + | use oxy_core::repo::SignatureStatus; |
|
| 10 | 13 | use oxy_core::repo::{render_change, ChangeStatus, Repo}; |
|
| 11 | 14 | use std::path::PathBuf; |
|
| 12 | 15 | ||
| 13 | 16 | pub fn init(path: Option<PathBuf>, bare: bool) -> Result<i32> { |
|
| 14 | 17 | let path = path.unwrap_or(std::env::current_dir()?); |
|
| 15 | - | let repo = if bare { Repo::init_bare(&path)? } else { Repo::init(&path)? }; |
|
| 16 | - | println!("{} {}oxy repository in {}", ui::done("Initialized"), if bare { "bare " } else { "" }, ui::path(repo.root().display())); |
|
| 17 | - | println!("{}", ui::kv("bookmark", ui::bookmark(&repo.config.bookmark))); |
|
| 18 | - | println!("{}", ui::kv("author", format!("{} <{}>", ui::author(&repo.config.author_name), repo.config.author_email))); |
|
| 18 | + | let repo = if bare { |
|
| 19 | + | Repo::init_bare(&path)? |
|
| 20 | + | } else { |
|
| 21 | + | Repo::init(&path)? |
|
| 22 | + | }; |
|
| 23 | + | println!( |
|
| 24 | + | "{} {}oxy repository in {}", |
|
| 25 | + | ui::done("Initialized"), |
|
| 26 | + | if bare { "bare " } else { "" }, |
|
| 27 | + | ui::path(repo.root().display()) |
|
| 28 | + | ); |
|
| 29 | + | println!( |
|
| 30 | + | "{}", |
|
| 31 | + | ui::kv("bookmark", ui::bookmark(&repo.config.bookmark)) |
|
| 32 | + | ); |
|
| 33 | + | println!( |
|
| 34 | + | "{}", |
|
| 35 | + | ui::kv( |
|
| 36 | + | "author", |
|
| 37 | + | format!( |
|
| 38 | + | "{} <{}>", |
|
| 39 | + | ui::author(&repo.config.author_name), |
|
| 40 | + | repo.config.author_email |
|
| 41 | + | ) |
|
| 42 | + | ) |
|
| 43 | + | ); |
|
| 19 | 44 | Ok(0) |
|
| 20 | 45 | } |
|
| 21 | 46 | ||
| … | 17 unchanged lines |
||
| 39 | 64 | sp.clear(); |
|
| 40 | 65 | if json { |
|
| 41 | 66 | println!("{}", repo.status_json(&snap)?); |
|
| 42 | - | return Ok(if snap.base_snapshot.is_conflicted() { 2 } else { 0 }); |
|
| 67 | + | return Ok(if snap.base_snapshot.is_conflicted() { |
|
| 68 | + | 2 |
|
| 69 | + | } else { |
|
| 70 | + | 0 |
|
| 71 | + | }); |
|
| 43 | 72 | } |
|
| 44 | 73 | println!( |
|
| 45 | 74 | "{} {}{}working copy {}{}{}", |
|
| … | 5 unchanged lines |
||
| 51 | 80 | ui::dim(plural(heads.len(), "head")) |
|
| 52 | 81 | ); |
|
| 53 | 82 | if snap.statuses.is_empty() { |
|
| 54 | - | println!(" {} {}", ui::ok(ui::glyph("✔", "*")), ui::dim("nothing to commit, working copy clean")); |
|
| 83 | + | println!( |
|
| 84 | + | " {} {}", |
|
| 85 | + | ui::ok(ui::glyph("✔", "*")), |
|
| 86 | + | ui::dim("nothing to commit, working copy clean") |
|
| 87 | + | ); |
|
| 55 | 88 | } else { |
|
| 56 | 89 | println!(); |
|
| 57 | 90 | for s in &snap.statuses { |
|
| 58 | - | println!(" {} {}", ui::status_mark(status_tag(s.status)), ui::path(&s.path)); |
|
| 91 | + | println!( |
|
| 92 | + | " {} {}", |
|
| 93 | + | ui::status_mark(status_tag(s.status)), |
|
| 94 | + | ui::path(&s.path) |
|
| 95 | + | ); |
|
| 59 | 96 | } |
|
| 60 | 97 | } |
|
| 61 | 98 | if !snap.base_snapshot.is_conflicted() { |
|
| 62 | 99 | return Ok(0); |
|
| 63 | 100 | } |
|
| 64 | 101 | println!(); |
|
| 65 | - | println!("{} {}", ui::section(ui::err("Conflicts")), ui::dim("edit the marked files, or the .oxy-conflict-N candidates, then commit")); |
|
| 102 | + | println!( |
|
| 103 | + | "{} {}", |
|
| 104 | + | ui::section(ui::err("Conflicts")), |
|
| 105 | + | ui::dim("edit the marked files, or the .oxy-conflict-N candidates, then commit") |
|
| 106 | + | ); |
|
| 66 | 107 | for c in &snap.base_snapshot.tree.conflicts { |
|
| 67 | 108 | println!(" {} tree: {c:?}", ui::err(ui::glyph("✱", "!"))); |
|
| 68 | 109 | } |
|
| 69 | 110 | for (p, f) in &snap.base_snapshot.files { |
|
| 70 | 111 | if f.is_conflicted() { |
|
| 71 | - | let state = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) { |
|
| 72 | - | Some(ChangeStatus::Resolved) => ui::ok(format!("{} resolved in working copy", ui::glyph("✔", "*"))), |
|
| 73 | - | Some(ChangeStatus::ConflictEdited) => ui::warn(format!("{} markers remain", ui::glyph("‼", "!"))), |
|
| 112 | + | let state = match snap |
|
| 113 | + | .statuses |
|
| 114 | + | .iter() |
|
| 115 | + | .find(|s| s.path == *p) |
|
| 116 | + | .map(|s| s.status) |
|
| 117 | + | { |
|
| 118 | + | Some(ChangeStatus::Resolved) => { |
|
| 119 | + | ui::ok(format!("{} resolved in working copy", ui::glyph("✔", "*"))) |
|
| 120 | + | } |
|
| 121 | + | Some(ChangeStatus::ConflictEdited) => { |
|
| 122 | + | ui::warn(format!("{} markers remain", ui::glyph("‼", "!"))) |
|
| 123 | + | } |
|
| 74 | 124 | _ => ui::err(format!("{} unresolved", ui::glyph("✱", "!"))), |
|
| 75 | 125 | }; |
|
| 76 | 126 | println!(" {} {state}", ui::path(p)); |
|
| … | 2 unchanged lines |
||
| 79 | 129 | Ok(2) |
|
| 80 | 130 | } |
|
| 81 | 131 | ||
| 82 | - | pub fn commit(message: &str, depends_on: &[String], paths: &[String], interactive: bool, hunks: &[usize]) -> Result<i32> { |
|
| 132 | + | pub fn commit( |
|
| 133 | + | message: &str, |
|
| 134 | + | depends_on: &[String], |
|
| 135 | + | paths: &[String], |
|
| 136 | + | interactive: bool, |
|
| 137 | + | hunks: &[usize], |
|
| 138 | + | ) -> Result<i32> { |
|
| 83 | 139 | let repo = open()?; |
|
| 84 | 140 | let mut deps = Vec::new(); |
|
| 85 | 141 | for d in depends_on { |
|
| … | 6 unchanged lines |
||
| 92 | 148 | if hunks.is_empty() { |
|
| 93 | 149 | anyhow::bail!("nothing to commit: the working copy matches its base"); |
|
| 94 | 150 | } |
|
| 95 | - | let hunks: Vec<_> = hunks.into_iter().filter(|h| paths.is_empty() || paths.iter().any(|p| h.path == *p || h.path.starts_with(&format!("{}/", p.trim_end_matches('/'))))).collect(); |
|
| 151 | + | let hunks: Vec<_> = hunks |
|
| 152 | + | .into_iter() |
|
| 153 | + | .filter(|h| { |
|
| 154 | + | paths.is_empty() |
|
| 155 | + | || paths.iter().any(|p| { |
|
| 156 | + | h.path == *p || h.path.starts_with(&format!("{}/", p.trim_end_matches('/'))) |
|
| 157 | + | }) |
|
| 158 | + | }) |
|
| 159 | + | .collect(); |
|
| 96 | 160 | let mut chosen = Vec::new(); |
|
| 97 | 161 | let stdin = std::io::stdin(); |
|
| 98 | 162 | let mut take_rest = false; |
|
| … | 2 unchanged lines |
||
| 101 | 165 | chosen.push(h.index); |
|
| 102 | 166 | continue; |
|
| 103 | 167 | } |
|
| 104 | - | println!("{} {} {}", ui::section(format!("hunk {} of {}", n + 1, hunks.len())), ui::path(&h.path), ui::dim(format!("(#{})", h.index))); |
|
| 168 | + | println!( |
|
| 169 | + | "{} {} {}", |
|
| 170 | + | ui::section(format!("hunk {} of {}", n + 1, hunks.len())), |
|
| 171 | + | ui::path(&h.path), |
|
| 172 | + | ui::dim(format!("(#{})", h.index)) |
|
| 173 | + | ); |
|
| 105 | 174 | for line in h.text.lines() { |
|
| 106 | - | let styled = if line.starts_with('+') { ui::ok(line) } else if line.starts_with('-') { ui::err(line) } else { ui::dim(line) }; |
|
| 175 | + | let styled = if line.starts_with('+') { |
|
| 176 | + | ui::ok(line) |
|
| 177 | + | } else if line.starts_with('-') { |
|
| 178 | + | ui::err(line) |
|
| 179 | + | } else { |
|
| 180 | + | ui::dim(line) |
|
| 181 | + | }; |
|
| 107 | 182 | println!(" {styled}"); |
|
| 108 | 183 | } |
|
| 109 | 184 | loop { |
|
| 110 | - | print!("{} ", ui::warn("Commit this hunk? [y]es [n]o [a]ll remaining [q]uit:")); |
|
| 185 | + | print!( |
|
| 186 | + | "{} ", |
|
| 187 | + | ui::warn("Commit this hunk? [y]es [n]o [a]ll remaining [q]uit:") |
|
| 188 | + | ); |
|
| 111 | 189 | use std::io::Write as _; |
|
| 112 | 190 | std::io::stdout().flush()?; |
|
| 113 | 191 | let mut answer = String::new(); |
|
| … | 28 unchanged lines |
||
| 142 | 220 | let sp = ui::spinner("Committing…"); |
|
| 143 | 221 | let h = repo.commit_selection(message, &deps, &selection)?; |
|
| 144 | 222 | let c = repo.store().get_change(&h)?; |
|
| 145 | - | sp.done(&format!("{} {} {} {}", ui::done("Committed"), ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12))), ui::subject(subject_of(message)))); |
|
| 223 | + | sp.done(&format!( |
|
| 224 | + | "{} {} {} {}", |
|
| 225 | + | ui::done("Committed"), |
|
| 226 | + | ui::id(short_id(&c)), |
|
| 227 | + | ui::hash(format!("#{}", h.short(12))), |
|
| 228 | + | ui::subject(subject_of(message)) |
|
| 229 | + | )); |
|
| 146 | 230 | if !matches!(selection, oxy_core::repo::Selection::All) { |
|
| 147 | 231 | let rest = repo.snapshot("oxy commit")?; |
|
| 148 | 232 | if !rest.statuses.is_empty() { |
|
| 149 | - | println!(" {} {}", ui::note(), ui::dim(format!("{} left uncommitted", plural(rest.statuses.len(), "path")))); |
|
| 233 | + | println!( |
|
| 234 | + | " {} {}", |
|
| 235 | + | ui::note(), |
|
| 236 | + | ui::dim(format!( |
|
| 237 | + | "{} left uncommitted", |
|
| 238 | + | plural(rest.statuses.len(), "path") |
|
| 239 | + | )) |
|
| 240 | + | ); |
|
| 150 | 241 | } |
|
| 151 | 242 | } |
|
| 152 | 243 | Ok(0) |
|
| … | 17 unchanged lines |
||
| 170 | 261 | println!("{}", oxy_core::repo::json_array(items)); |
|
| 171 | 262 | return Ok(0); |
|
| 172 | 263 | } |
|
| 173 | - | let width = lines.iter().map(|l| l.author.chars().count()).max().unwrap_or(0).min(20); |
|
| 264 | + | let width = lines |
|
| 265 | + | .iter() |
|
| 266 | + | .map(|l| l.author.chars().count()) |
|
| 267 | + | .max() |
|
| 268 | + | .unwrap_or(0) |
|
| 269 | + | .min(20); |
|
| 174 | 270 | let digits = lines.len().to_string().len(); |
|
| 175 | 271 | for l in &lines { |
|
| 176 | 272 | let author: String = l.author.chars().take(20).collect(); |
|
| … | 15 unchanged lines |
||
| 192 | 288 | use oxy_core::repo::BisectStep::*; |
|
| 193 | 289 | match step { |
|
| 194 | 290 | NeedMarks { good, bad } => { |
|
| 195 | - | println!("{} {} {}", ui::note(), ui::dim(format!("{good} good, {bad} bad;")), ui::dim(if *good == 0 { "mark a known-good change or bookmark: oxy bisect good CHANGE" } else { "mark a known-bad change or bookmark: oxy bisect bad CHANGE" })); |
|
| 291 | + | println!( |
|
| 292 | + | "{} {} {}", |
|
| 293 | + | ui::note(), |
|
| 294 | + | ui::dim(format!("{good} good, {bad} bad;")), |
|
| 295 | + | ui::dim(if *good == 0 { |
|
| 296 | + | "mark a known-good change or bookmark: oxy bisect good CHANGE" |
|
| 297 | + | } else { |
|
| 298 | + | "mark a known-bad change or bookmark: oxy bisect bad CHANGE" |
|
| 299 | + | }) |
|
| 300 | + | ); |
|
| 196 | 301 | Ok(0) |
|
| 197 | 302 | } |
|
| 198 | - | Testing { heads, remaining, steps_left } => { |
|
| 199 | - | println!("{} {} {}", ui::done("Testing"), ui::dim(format!("{} in the state under test;", plural(heads.len(), "head"))), ui::dim(format!("{remaining} candidates left, about {steps_left} more steps"))); |
|
| 200 | - | println!(" {} {}", ui::note(), ui::dim("run your test, then oxy bisect good | bad | skip")); |
|
| 303 | + | Testing { |
|
| 304 | + | heads, |
|
| 305 | + | remaining, |
|
| 306 | + | steps_left, |
|
| 307 | + | } => { |
|
| 308 | + | println!( |
|
| 309 | + | "{} {} {}", |
|
| 310 | + | ui::done("Testing"), |
|
| 311 | + | ui::dim(format!( |
|
| 312 | + | "{} in the state under test;", |
|
| 313 | + | plural(heads.len(), "head") |
|
| 314 | + | )), |
|
| 315 | + | ui::dim(format!( |
|
| 316 | + | "{remaining} candidates left, about {steps_left} more steps" |
|
| 317 | + | )) |
|
| 318 | + | ); |
|
| 319 | + | println!( |
|
| 320 | + | " {} {}", |
|
| 321 | + | ui::note(), |
|
| 322 | + | ui::dim("run your test, then oxy bisect good | bad | skip") |
|
| 323 | + | ); |
|
| 201 | 324 | Ok(0) |
|
| 202 | 325 | } |
|
| 203 | 326 | Found { culprit } => { |
|
| 204 | 327 | let c = repo.store().get_change(culprit)?; |
|
| 205 | - | println!("{} {} {} {}", ui::done("Found"), ui::id(short_id(&c)), ui::hash(format!("#{}", culprit.short(12))), ui::subject(subject_of(&c.message))); |
|
| 328 | + | println!( |
|
| 329 | + | "{} {} {} {}", |
|
| 330 | + | ui::done("Found"), |
|
| 331 | + | ui::id(short_id(&c)), |
|
| 332 | + | ui::hash(format!("#{}", culprit.short(12))), |
|
| 333 | + | ui::subject(subject_of(&c.message)) |
|
| 334 | + | ); |
|
| 206 | 335 | println!(" {} {}", ui::note(), ui::dim("the first change whose inclusion makes the state bad; oxy bisect reset to return")); |
|
| 207 | 336 | Ok(0) |
|
| 208 | 337 | } |
|
| 209 | 338 | Inconclusive { candidates } => { |
|
| 210 | - | println!("{} {}", ui::caution(), ui::warn(format!("every remaining candidate was skipped ({}); oxy bisect reset to return", plural(candidates.len(), "change")))); |
|
| 339 | + | println!( |
|
| 340 | + | "{} {}", |
|
| 341 | + | ui::caution(), |
|
| 342 | + | ui::warn(format!( |
|
| 343 | + | "every remaining candidate was skipped ({}); oxy bisect reset to return", |
|
| 344 | + | plural(candidates.len(), "change") |
|
| 345 | + | )) |
|
| 346 | + | ); |
|
| 211 | 347 | Ok(1) |
|
| 212 | 348 | } |
|
| 213 | 349 | } |
|
| … | 2 unchanged lines |
||
| 216 | 352 | pub fn bisect_start() -> Result<i32> { |
|
| 217 | 353 | let repo = open()?; |
|
| 218 | 354 | repo.bisect_start()?; |
|
| 219 | - | println!("{} bisect; {}", ui::done("Started"), ui::dim("now: oxy bisect good CHANGE and oxy bisect bad CHANGE")); |
|
| 355 | + | println!( |
|
| 356 | + | "{} bisect; {}", |
|
| 357 | + | ui::done("Started"), |
|
| 358 | + | ui::dim("now: oxy bisect good CHANGE and oxy bisect bad CHANGE") |
|
| 359 | + | ); |
|
| 220 | 360 | Ok(0) |
|
| 221 | 361 | } |
|
| 222 | 362 | ||
| … | 19 unchanged lines |
||
| 242 | 382 | use oxy_core::repo::BisectStep::*; |
|
| 243 | 383 | let v = match repo.bisect_status()? { |
|
| 244 | 384 | None => "null".to_string(), |
|
| 245 | - | Some(NeedMarks { good, bad }) => json::obj([("state", json::s("need-marks")), ("good", json::n(good)), ("bad", json::n(bad))]), |
|
| 246 | - | Some(Testing { heads, remaining, steps_left }) => json::obj([("state", json::s("testing")), ("heads", json::hashes(&heads)), ("remaining", json::n(remaining)), ("steps_left", json::n(steps_left))]), |
|
| 247 | - | Some(Found { culprit }) => json::obj([("state", json::s("found")), ("culprit", json::s(&culprit.to_hex()))]), |
|
| 248 | - | Some(Inconclusive { candidates }) => json::obj([("state", json::s("inconclusive")), ("candidates", json::hashes(&candidates))]), |
|
| 385 | + | Some(NeedMarks { good, bad }) => json::obj([ |
|
| 386 | + | ("state", json::s("need-marks")), |
|
| 387 | + | ("good", json::n(good)), |
|
| 388 | + | ("bad", json::n(bad)), |
|
| 389 | + | ]), |
|
| 390 | + | Some(Testing { |
|
| 391 | + | heads, |
|
| 392 | + | remaining, |
|
| 393 | + | steps_left, |
|
| 394 | + | }) => json::obj([ |
|
| 395 | + | ("state", json::s("testing")), |
|
| 396 | + | ("heads", json::hashes(&heads)), |
|
| 397 | + | ("remaining", json::n(remaining)), |
|
| 398 | + | ("steps_left", json::n(steps_left)), |
|
| 399 | + | ]), |
|
| 400 | + | Some(Found { culprit }) => json::obj([ |
|
| 401 | + | ("state", json::s("found")), |
|
| 402 | + | ("culprit", json::s(&culprit.to_hex())), |
|
| 403 | + | ]), |
|
| 404 | + | Some(Inconclusive { candidates }) => json::obj([ |
|
| 405 | + | ("state", json::s("inconclusive")), |
|
| 406 | + | ("candidates", json::hashes(&candidates)), |
|
| 407 | + | ]), |
|
| 249 | 408 | }; |
|
| 250 | 409 | println!("{v}"); |
|
| 251 | 410 | return Ok(0); |
|
| … | 20 unchanged lines |
||
| 272 | 431 | fn signature_json(repo: &Repo, h: &oxy_core::hash::ChangeHash) -> Result<String> { |
|
| 273 | 432 | Ok(match repo.authenticate_change(h)? { |
|
| 274 | 433 | SignatureStatus::Unsigned => json::obj([("status", json::s("unsigned"))]), |
|
| 275 | - | SignatureStatus::Valid { key, name, email, sealed, revoked } => json::obj([ |
|
| 434 | + | SignatureStatus::Valid { |
|
| 435 | + | key, |
|
| 436 | + | name, |
|
| 437 | + | email, |
|
| 438 | + | sealed, |
|
| 439 | + | revoked, |
|
| 440 | + | } => json::obj([ |
|
| 276 | 441 | ("status", json::s(if sealed { "sealed" } else { "signed" })), |
|
| 277 | 442 | ("key", json::s(&key.to_hex())), |
|
| 278 | 443 | ("name", json::s(&name)), |
|
| 279 | 444 | ("email", json::s(&email)), |
|
| 280 | 445 | ("revoked", json::b(revoked)), |
|
| 281 | 446 | ]), |
|
| 282 | - | SignatureStatus::Invalid(why) => json::obj([("status", json::s("invalid")), ("reason", json::s(&why))]), |
|
| 447 | + | SignatureStatus::Invalid(why) => { |
|
| 448 | + | json::obj([("status", json::s("invalid")), ("reason", json::s(&why))]) |
|
| 449 | + | } |
|
| 283 | 450 | }) |
|
| 284 | 451 | } |
|
| 285 | 452 | ||
| … | 10 unchanged lines |
||
| 296 | 463 | ("deps", json::hashes(&c.deps)), |
|
| 297 | 464 | ("head", json::b(e.is_head)), |
|
| 298 | 465 | ("working_copy", json::b(e.is_working_copy)), |
|
| 299 | - | ("signature", if e.is_working_copy { "null".into() } else { signature_json(repo, &e.hash)? }), |
|
| 466 | + | ( |
|
| 467 | + | "signature", |
|
| 468 | + | if e.is_working_copy { |
|
| 469 | + | "null".into() |
|
| 470 | + | } else { |
|
| 471 | + | signature_json(repo, &e.hash)? |
|
| 472 | + | }, |
|
| 473 | + | ), |
|
| 300 | 474 | ])) |
|
| 301 | 475 | } |
|
| 302 | 476 | ||
| … | 11 unchanged lines |
||
| 314 | 488 | for e in entries.iter().take(shown) { |
|
| 315 | 489 | items.push(log_entry_json(&repo, e)?); |
|
| 316 | 490 | } |
|
| 317 | - | println!("{}", json::obj([("bookmark", json::s(&repo.config.bookmark)), ("total", json::n(entries.len())), ("changes", json::arr(items))])); |
|
| 491 | + | println!( |
|
| 492 | + | "{}", |
|
| 493 | + | json::obj([ |
|
| 494 | + | ("bookmark", json::s(&repo.config.bookmark)), |
|
| 495 | + | ("total", json::n(entries.len())), |
|
| 496 | + | ("changes", json::arr(items)) |
|
| 497 | + | ]) |
|
| 498 | + | ); |
|
| 318 | 499 | return Ok(0); |
|
| 319 | 500 | } |
|
| 320 | 501 | let total = entries.len(); |
|
| … | 1 unchanged lines |
||
| 322 | 503 | let rail = ui::rail(); |
|
| 323 | 504 | for (i, e) in entries.iter().take(shown).enumerate() { |
|
| 324 | 505 | let c = &e.change; |
|
| 325 | - | let bullet = ui::log_bullet(e.is_working_copy, e.is_head, e.is_working_copy && conflicted); |
|
| 506 | + | let bullet = ui::log_bullet( |
|
| 507 | + | e.is_working_copy, |
|
| 508 | + | e.is_head, |
|
| 509 | + | e.is_working_copy && conflicted, |
|
| 510 | + | ); |
|
| 326 | 511 | let mut labels = Vec::new(); |
|
| 327 | 512 | if e.is_working_copy { |
|
| 328 | 513 | labels.push(ui::warn("working copy")); |
|
| … | 1 unchanged lines |
||
| 330 | 515 | if c.kind == ChangeKind::Resolution { |
|
| 331 | 516 | labels.push(ui::warn("resolution")); |
|
| 332 | 517 | } |
|
| 333 | - | let labels = if labels.is_empty() { String::new() } else { format!(" {}{}{}", ui::dim("("), labels.join(ui::dim(", ").as_str()), ui::dim(")")) }; |
|
| 518 | + | let labels = if labels.is_empty() { |
|
| 519 | + | String::new() |
|
| 520 | + | } else { |
|
| 521 | + | format!( |
|
| 522 | + | " {}{}{}", |
|
| 523 | + | ui::dim("("), |
|
| 524 | + | labels.join(ui::dim(", ").as_str()), |
|
| 525 | + | ui::dim(")") |
|
| 526 | + | ) |
|
| 527 | + | }; |
|
| 334 | 528 | let subject = if e.is_working_copy && c.message.is_empty() { |
|
| 335 | - | ui::dim(if snap.statuses.is_empty() { "(clean)" } else { "(uncommitted changes)" }) |
|
| 529 | + | ui::dim(if snap.statuses.is_empty() { |
|
| 530 | + | "(clean)" |
|
| 531 | + | } else { |
|
| 532 | + | "(uncommitted changes)" |
|
| 533 | + | }) |
|
| 336 | 534 | } else { |
|
| 337 | 535 | ui::subject(subject_of(&c.message)) |
|
| 338 | 536 | }; |
|
| … | 6 unchanged lines |
||
| 345 | 543 | ui::sep(), |
|
| 346 | 544 | ui::dim(ui::ago(c.hlc.wall_ms)), |
|
| 347 | 545 | ui::sep(), |
|
| 348 | - | if e.is_working_copy { ui::dim("not yet committed") } else { signature_mark(&repo, &e.hash)? } |
|
| 546 | + | if e.is_working_copy { |
|
| 547 | + | ui::dim("not yet committed") |
|
| 548 | + | } else { |
|
| 549 | + | signature_mark(&repo, &e.hash)? |
|
| 550 | + | } |
|
| 349 | 551 | ); |
|
| 350 | 552 | println!("{rail} {meta}"); |
|
| 351 | 553 | if verbose { |
|
| 352 | 554 | if !e.is_working_copy { |
|
| 353 | - | println!("{rail} {}", ui::dim(format!("{} {}", when(c.hlc.wall_ms), signature_line(&repo, &e.hash)?))); |
|
| 555 | + | println!( |
|
| 556 | + | "{rail} {}", |
|
| 557 | + | ui::dim(format!( |
|
| 558 | + | "{} {}", |
|
| 559 | + | when(c.hlc.wall_ms), |
|
| 560 | + | signature_line(&repo, &e.hash)? |
|
| 561 | + | )) |
|
| 562 | + | ); |
|
| 354 | 563 | } |
|
| 355 | 564 | if !c.deps.is_empty() { |
|
| 356 | - | println!("{rail} {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(deps_of(&c.deps))); |
|
| 565 | + | println!( |
|
| 566 | + | "{rail} {} {}", |
|
| 567 | + | ui::dim(ui::glyph("↳", "->")), |
|
| 568 | + | ui::dim(deps_of(&c.deps)) |
|
| 569 | + | ); |
|
| 357 | 570 | } |
|
| 358 | - | let body: Vec<&str> = c.message.lines().skip(1).skip_while(|l| l.trim().is_empty()).collect(); |
|
| 571 | + | let body: Vec<&str> = c |
|
| 572 | + | .message |
|
| 573 | + | .lines() |
|
| 574 | + | .skip(1) |
|
| 575 | + | .skip_while(|l| l.trim().is_empty()) |
|
| 576 | + | .collect(); |
|
| 359 | 577 | if !body.is_empty() { |
|
| 360 | 578 | println!("{rail}"); |
|
| 361 | 579 | for line in body { |
|
| … | 6 unchanged lines |
||
| 368 | 586 | } |
|
| 369 | 587 | } |
|
| 370 | 588 | if shown < total { |
|
| 371 | - | println!("{}", ui::dim(format!("{} {} more; pass -n to see them", ui::glyph("…", "..."), total - shown))); |
|
| 589 | + | println!( |
|
| 590 | + | "{}", |
|
| 591 | + | ui::dim(format!( |
|
| 592 | + | "{} {} more; pass -n to see them", |
|
| 593 | + | ui::glyph("…", "..."), |
|
| 594 | + | total - shown |
|
| 595 | + | )) |
|
| 596 | + | ); |
|
| 372 | 597 | } |
|
| 373 | 598 | Ok(0) |
|
| 374 | 599 | } |
|
| … | 13 unchanged lines |
||
| 388 | 613 | json::arr(f.hunks.iter().map(|h| { |
|
| 389 | 614 | json::obj([ |
|
| 390 | 615 | ("base_line", json::n(h.base_line)), |
|
| 391 | - | ("added", json::arr(h.added.iter().map(|l| json::s(&String::from_utf8_lossy(l))))), |
|
| 392 | - | ("removed", json::arr(h.removed.iter().map(|(n, l)| json::obj([("line", json::n(*n)), ("text", json::s(&String::from_utf8_lossy(l)))])))), |
|
| 616 | + | ( |
|
| 617 | + | "added", |
|
| 618 | + | json::arr( |
|
| 619 | + | h.added.iter().map(|l| json::s(&String::from_utf8_lossy(l))), |
|
| 620 | + | ), |
|
| 621 | + | ), |
|
| 622 | + | ( |
|
| 623 | + | "removed", |
|
| 624 | + | json::arr(h.removed.iter().map(|(n, l)| { |
|
| 625 | + | json::obj([ |
|
| 626 | + | ("line", json::n(*n)), |
|
| 627 | + | ("text", json::s(&String::from_utf8_lossy(l))), |
|
| 628 | + | ]) |
|
| 629 | + | })), |
|
| 630 | + | ), |
|
| 393 | 631 | ]) |
|
| 394 | 632 | })), |
|
| 395 | 633 | ), |
|
| 396 | 634 | ]) |
|
| 397 | 635 | }); |
|
| 398 | - | println!("{}", json::obj([("change", json::opt(hash, |h| json::s(&h.to_hex()))), ("files", json::arr(items))])); |
|
| 636 | + | println!( |
|
| 637 | + | "{}", |
|
| 638 | + | json::obj([ |
|
| 639 | + | ("change", json::opt(hash, |h| json::s(&h.to_hex()))), |
|
| 640 | + | ("files", json::arr(items)) |
|
| 641 | + | ]) |
|
| 642 | + | ); |
|
| 399 | 643 | return Ok(0); |
|
| 400 | 644 | } |
|
| 401 | 645 | match hash { |
|
| … | 5 unchanged lines |
||
| 407 | 651 | } |
|
| 408 | 652 | None => { |
|
| 409 | 653 | let snap = repo.snapshot("oxy diff")?; |
|
| 410 | - | print!("{}", render_change(&snap.wc, &snap.base, &snap.base_snapshot)); |
|
| 654 | + | print!( |
|
| 655 | + | "{}", |
|
| 656 | + | render_change(&snap.wc, &snap.base, &snap.base_snapshot) |
|
| 657 | + | ); |
|
| 411 | 658 | } |
|
| 412 | 659 | } |
|
| 413 | 660 | Ok(0) |
|
| … | 16 unchanged lines |
||
| 430 | 677 | return Ok(1); |
|
| 431 | 678 | }; |
|
| 432 | 679 | if as_json { |
|
| 433 | - | println!("{}", json::obj([("root", json::s(&repo.root().to_string_lossy())), ("workspace", json::s(&repo.workspace().to_hex())), ("bookmark", json::s(&repo.config.bookmark))])); |
|
| 680 | + | println!( |
|
| 681 | + | "{}", |
|
| 682 | + | json::obj([ |
|
| 683 | + | ("root", json::s(&repo.root().to_string_lossy())), |
|
| 684 | + | ("workspace", json::s(&repo.workspace().to_hex())), |
|
| 685 | + | ("bookmark", json::s(&repo.config.bookmark)) |
|
| 686 | + | ]) |
|
| 687 | + | ); |
|
| 434 | 688 | } else { |
|
| 435 | 689 | println!("{}", repo.root().display()); |
|
| 436 | 690 | } |
|
| … | 2 unchanged lines |
||
| 439 | 693 | ||
| 440 | 694 | pub fn version(as_json: bool) -> Result<i32> { |
|
| 441 | 695 | if as_json { |
|
| 442 | - | println!("{}", json::obj([("version", json::s(env!("CARGO_PKG_VERSION"))), ("protocol", json::n(oxy_core::protocol::PROTOCOL_VERSION)), ("format", json::n(2))])); |
|
| 696 | + | println!( |
|
| 697 | + | "{}", |
|
| 698 | + | json::obj([ |
|
| 699 | + | ("version", json::s(env!("CARGO_PKG_VERSION"))), |
|
| 700 | + | ("protocol", json::n(oxy_core::protocol::PROTOCOL_VERSION)), |
|
| 701 | + | ("format", json::n(2)) |
|
| 702 | + | ]) |
|
| 703 | + | ); |
|
| 443 | 704 | } else { |
|
| 444 | - | println!("oxy {} (repository format 2, protocol {})", env!("CARGO_PKG_VERSION"), oxy_core::protocol::PROTOCOL_VERSION); |
|
| 705 | + | println!( |
|
| 706 | + | "oxy {} (repository format 2, protocol {})", |
|
| 707 | + | env!("CARGO_PKG_VERSION"), |
|
| 708 | + | oxy_core::protocol::PROTOCOL_VERSION |
|
| 709 | + | ); |
|
| 445 | 710 | } |
|
| 446 | 711 | Ok(0) |
|
| 447 | 712 | } |
|
| … | 10 unchanged lines |
||
| 458 | 723 | if !paths.is_empty() { |
|
| 459 | 724 | n += repo.discard(paths)?; |
|
| 460 | 725 | } |
|
| 461 | - | println!("{} {} restored to the working copy's base", ui::done("Discarded"), plural(n, "path")); |
|
| 726 | + | println!( |
|
| 727 | + | "{} {} restored to the working copy's base", |
|
| 728 | + | ui::done("Discarded"), |
|
| 729 | + | plural(n, "path") |
|
| 730 | + | ); |
|
| 462 | 731 | Ok(0) |
|
| 463 | 732 | } |
|
| 464 | 733 | ||
| … | 1 unchanged lines |
||
| 466 | 735 | let repo = open()?; |
|
| 467 | 736 | let (_, hunks) = repo.hunks()?; |
|
| 468 | 737 | if as_json { |
|
| 469 | - | println!("{}", json::arr(hunks.iter().map(|h| json::obj([("index", json::n(h.index)), ("path", json::s(&h.path)), ("text", json::s(&h.text))])))); |
|
| 738 | + | println!( |
|
| 739 | + | "{}", |
|
| 740 | + | json::arr(hunks.iter().map(|h| json::obj([ |
|
| 741 | + | ("index", json::n(h.index)), |
|
| 742 | + | ("path", json::s(&h.path)), |
|
| 743 | + | ("text", json::s(&h.text)) |
|
| 744 | + | ]))) |
|
| 745 | + | ); |
|
| 470 | 746 | return Ok(0); |
|
| 471 | 747 | } |
|
| 472 | 748 | if hunks.is_empty() { |
|
| 473 | - | println!("{}", ui::dim("No hunks: the working copy matches its base.")); |
|
| 749 | + | println!( |
|
| 750 | + | "{}", |
|
| 751 | + | ui::dim("No hunks: the working copy matches its base.") |
|
| 752 | + | ); |
|
| 474 | 753 | } |
|
| 475 | 754 | for h in &hunks { |
|
| 476 | - | println!("{} {}", ui::section(format!("hunk {}", h.index)), ui::path(&h.path)); |
|
| 755 | + | println!( |
|
| 756 | + | "{} {}", |
|
| 757 | + | ui::section(format!("hunk {}", h.index)), |
|
| 758 | + | ui::path(&h.path) |
|
| 759 | + | ); |
|
| 477 | 760 | for line in h.text.lines() { |
|
| 478 | - | let styled = if line.starts_with('+') { ui::ok(line) } else if line.starts_with('-') { ui::err(line) } else { ui::dim(line) }; |
|
| 761 | + | let styled = if line.starts_with('+') { |
|
| 762 | + | ui::ok(line) |
|
| 763 | + | } else if line.starts_with('-') { |
|
| 764 | + | ui::err(line) |
|
| 765 | + | } else { |
|
| 766 | + | ui::dim(line) |
|
| 767 | + | }; |
|
| 479 | 768 | println!(" {styled}"); |
|
| 480 | 769 | } |
|
| 481 | 770 | } |
|
| … | 8 unchanged lines |
||
| 490 | 779 | if as_json { |
|
| 491 | 780 | let files = repo.diff_files(Some(&h))?; |
|
| 492 | 781 | let heads = repo.bookmark_heads()?; |
|
| 493 | - | let entry = oxy_core::repo::LogEntry { hash: h, change: oxy_core::state::ChangeMeta::of(&c), is_working_copy: c.kind == oxy_core::objects::ChangeKind::WorkingCopy, is_head: heads.contains(&h) }; |
|
| 782 | + | let entry = oxy_core::repo::LogEntry { |
|
| 783 | + | hash: h, |
|
| 784 | + | change: oxy_core::state::ChangeMeta::of(&c), |
|
| 785 | + | is_working_copy: c.kind == oxy_core::objects::ChangeKind::WorkingCopy, |
|
| 786 | + | is_head: heads.contains(&h), |
|
| 787 | + | }; |
|
| 494 | 788 | let mut body = log_entry_json(&repo, &entry)?; |
|
| 495 | 789 | body.pop(); |
|
| 496 | - | body.push_str(&format!(",{}:{}}}", json::s("files"), json::arr(files.iter().map(|f| json::obj([("path", json::s(&f.path)), ("kind", json::s(&f.kind)), ("from", json::opt(f.from.as_deref(), json::s))]))))); |
|
| 790 | + | body.push_str(&format!( |
|
| 791 | + | ",{}:{}}}", |
|
| 792 | + | json::s("files"), |
|
| 793 | + | json::arr(files.iter().map(|f| json::obj([ |
|
| 794 | + | ("path", json::s(&f.path)), |
|
| 795 | + | ("kind", json::s(&f.kind)), |
|
| 796 | + | ("from", json::opt(f.from.as_deref(), json::s)) |
|
| 797 | + | ]))) |
|
| 798 | + | )); |
|
| 497 | 799 | println!("{body}"); |
|
| 498 | 800 | return Ok(0); |
|
| 499 | 801 | } |
|
| 500 | - | println!("{} {} {}", ui::section("change"), ui::id(short_id(&c)), ui::hash(format!("#{h}"))); |
|
| 802 | + | println!( |
|
| 803 | + | "{} {} {}", |
|
| 804 | + | ui::section("change"), |
|
| 805 | + | ui::id(short_id(&c)), |
|
| 806 | + | ui::hash(format!("#{h}")) |
|
| 807 | + | ); |
|
| 501 | 808 | println!("{}", ui::kv("kind", format!("{:?}", c.kind).to_lowercase())); |
|
| 502 | - | println!("{}", ui::kv("author", ui::author(author_name(&repo, &oxy_core::state::ChangeMeta::of(&c))))); |
|
| 503 | - | println!("{}", ui::kv("date", format!("{} {}", when(c.hlc.wall_ms), ui::dim(format!("({})", ui::ago(c.hlc.wall_ms)))))); |
|
| 809 | + | println!( |
|
| 810 | + | "{}", |
|
| 811 | + | ui::kv( |
|
| 812 | + | "author", |
|
| 813 | + | ui::author(author_name(&repo, &oxy_core::state::ChangeMeta::of(&c))) |
|
| 814 | + | ) |
|
| 815 | + | ); |
|
| 816 | + | println!( |
|
| 817 | + | "{}", |
|
| 818 | + | ui::kv( |
|
| 819 | + | "date", |
|
| 820 | + | format!( |
|
| 821 | + | "{} {}", |
|
| 822 | + | when(c.hlc.wall_ms), |
|
| 823 | + | ui::dim(format!("({})", ui::ago(c.hlc.wall_ms))) |
|
| 824 | + | ) |
|
| 825 | + | ) |
|
| 826 | + | ); |
|
| 504 | 827 | println!("{}", ui::kv("deps", ui::dim(deps_line(&c)))); |
|
| 505 | 828 | println!("{}", ui::kv("sig", signature_line(&repo, &h)?)); |
|
| 506 | 829 | println!(); |
|
| 507 | - | for line in (if c.message.is_empty() { "(no description)" } else { &c.message }).lines() { |
|
| 830 | + | for line in (if c.message.is_empty() { |
|
| 831 | + | "(no description)" |
|
| 832 | + | } else { |
|
| 833 | + | &c.message |
|
| 834 | + | }) |
|
| 835 | + | .lines() |
|
| 836 | + | { |
|
| 508 | 837 | println!(" {line}"); |
|
| 509 | 838 | } |
|
| 510 | 839 | println!(); |
|
| … | 12 unchanged lines |
||
| 523 | 852 | } |
|
| 524 | 853 | heads |
|
| 525 | 854 | }; |
|
| 526 | - | let spec = if changes.is_empty() { repo.config.bookmark.clone() } else { changes.join(" ") }; |
|
| 855 | + | let spec = if changes.is_empty() { |
|
| 856 | + | repo.config.bookmark.clone() |
|
| 857 | + | } else { |
|
| 858 | + | changes.join(" ") |
|
| 859 | + | }; |
|
| 527 | 860 | let sp = ui::spinner(&format!("Checking out {spec}…")); |
|
| 528 | 861 | let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?; |
|
| 529 | - | sp.done(&format!("{} on {}: {} written, {} removed", ui::done("Working copy"), ui::bookmark(&spec), plural(r.written, "file"), r.removed)); |
|
| 862 | + | sp.done(&format!( |
|
| 863 | + | "{} on {}: {} written, {} removed", |
|
| 864 | + | ui::done("Working copy"), |
|
| 865 | + | ui::bookmark(&spec), |
|
| 866 | + | plural(r.written, "file"), |
|
| 867 | + | r.removed |
|
| 868 | + | )); |
|
| 530 | 869 | for n in &r.notes { |
|
| 531 | 870 | println!(" {} {n}", ui::note()); |
|
| 532 | 871 | } |
|
| 533 | 872 | if r.conflicted { |
|
| 534 | - | println!(" {} {}", ui::caution(), ui::warn("this state has conflicts; see oxy status")); |
|
| 873 | + | println!( |
|
| 874 | + | " {} {}", |
|
| 875 | + | ui::caution(), |
|
| 876 | + | ui::warn("this state has conflicts; see oxy status") |
|
| 877 | + | ); |
|
| 535 | 878 | } |
|
| 536 | 879 | Ok(0) |
|
| 537 | 880 | } |
|
| … | 3 unchanged lines |
||
| 541 | 884 | let snap = repo.snapshot("oxy resolve")?; |
|
| 542 | 885 | if as_json { |
|
| 543 | 886 | println!("{}", repo.status_json(&snap)?); |
|
| 544 | - | return Ok(if snap.base_snapshot.is_conflicted() { 2 } else { 0 }); |
|
| 887 | + | return Ok(if snap.base_snapshot.is_conflicted() { |
|
| 888 | + | 2 |
|
| 889 | + | } else { |
|
| 890 | + | 0 |
|
| 891 | + | }); |
|
| 545 | 892 | } |
|
| 546 | 893 | if !snap.base_snapshot.is_conflicted() { |
|
| 547 | 894 | println!("The working copy's base has no conflicts."); |
|
| … | 4 unchanged lines |
||
| 552 | 899 | if !f.is_conflicted() { |
|
| 553 | 900 | continue; |
|
| 554 | 901 | } |
|
| 555 | - | match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) { |
|
| 902 | + | match snap |
|
| 903 | + | .statuses |
|
| 904 | + | .iter() |
|
| 905 | + | .find(|s| s.path == *p) |
|
| 906 | + | .map(|s| s.status) |
|
| 907 | + | { |
|
| 556 | 908 | Some(ChangeStatus::Resolved) => println!("resolved {p}"), |
|
| 557 | 909 | Some(ChangeStatus::ConflictEdited) => { |
|
| 558 | 910 | remaining += 1; |
|
| … | 43 unchanged lines |
||
| 602 | 954 | return Ok(0); |
|
| 603 | 955 | } |
|
| 604 | 956 | for (i, (r, op)) in repo.op_log()?.into_iter().enumerate() { |
|
| 605 | - | let bullet = if i == 0 { ui::ok(ui::glyph("●", "@")) } else { ui::dim(ui::glyph("○", "o")) }; |
|
| 957 | + | let bullet = if i == 0 { |
|
| 958 | + | ui::ok(ui::glyph("●", "@")) |
|
| 959 | + | } else { |
|
| 960 | + | ui::dim(ui::glyph("○", "o")) |
|
| 961 | + | }; |
|
| 606 | 962 | let cmd = subject_of(&op.command).split("\\n").next().unwrap_or(""); |
|
| 607 | - | println!("{bullet} {}{}{} {}", ui::hash(r.short(12)), ui::sep(), ui::dim(format!("{:<16}", ui::ago(op.hlc.wall_ms))), if i == 0 { ui::subject(cmd) } else { cmd.to_string() }); |
|
| 963 | + | println!( |
|
| 964 | + | "{bullet} {}{}{} {}", |
|
| 965 | + | ui::hash(r.short(12)), |
|
| 966 | + | ui::sep(), |
|
| 967 | + | ui::dim(format!("{:<16}", ui::ago(op.hlc.wall_ms))), |
|
| 968 | + | if i == 0 { |
|
| 969 | + | ui::subject(cmd) |
|
| 970 | + | } else { |
|
| 971 | + | cmd.to_string() |
|
| 972 | + | } |
|
| 973 | + | ); |
|
| 608 | 974 | } |
|
| 609 | 975 | Ok(0) |
|
| 610 | 976 | } |
|
| … | 12 unchanged lines |
||
| 623 | 989 | 100.0 * r.packed.packed_bytes as f64 / r.packed.raw_bytes.max(1) as f64 |
|
| 624 | 990 | ); |
|
| 625 | 991 | } |
|
| 626 | - | println!("{} reachable; pruned {} ({} bytes){}{}; {} present.", |
|
| 992 | + | println!( |
|
| 993 | + | "{} reachable; pruned {} ({} bytes){}{}; {} present.", |
|
| 627 | 994 | plural(r.reachable, "object"), |
|
| 628 | 995 | plural(r.pruned.objects, "unreachable object"), |
|
| 629 | 996 | r.pruned.bytes, |
|
| 630 | - | if r.pruned.deferred > 0 { format!("; {} deferred by the grace period (use --now)", plural(r.pruned.deferred, "object")) } else { String::new() }, |
|
| 631 | - | if r.merged_packs > 0 { format!("; merged {} into one", plural(r.merged_packs, "pack")) } else { String::new() }, |
|
| 997 | + | if r.pruned.deferred > 0 { |
|
| 998 | + | format!( |
|
| 999 | + | "; {} deferred by the grace period (use --now)", |
|
| 1000 | + | plural(r.pruned.deferred, "object") |
|
| 1001 | + | ) |
|
| 1002 | + | } else { |
|
| 1003 | + | String::new() |
|
| 1004 | + | }, |
|
| 1005 | + | if r.merged_packs > 0 { |
|
| 1006 | + | format!("; merged {} into one", plural(r.merged_packs, "pack")) |
|
| 1007 | + | } else { |
|
| 1008 | + | String::new() |
|
| 1009 | + | }, |
|
| 632 | 1010 | plural(repo.store().pack_count(), "pack") |
|
| 633 | 1011 | ); |
|
| 634 | 1012 | Ok(0) |
|
| … | 15 unchanged lines |
||
| 650 | 1028 | .copied() |
|
| 651 | 1029 | .unwrap(); |
|
| 652 | 1030 | let c = repo.store().get_change(&latest)?; |
|
| 653 | - | println!("Defaulting to {} #{} (most recent of the {} heads under the working copy)", short_id(&c), latest.short(12), many.len()); |
|
| 1031 | + | println!( |
|
| 1032 | + | "Defaulting to {} #{} (most recent of the {} heads under the working copy)", |
|
| 1033 | + | short_id(&c), |
|
| 1034 | + | latest.short(12), |
|
| 1035 | + | many.len() |
|
| 1036 | + | ); |
|
| 654 | 1037 | Ok(latest) |
|
| 655 | 1038 | } |
|
| 656 | 1039 | } |
|
| … | 7 unchanged lines |
||
| 664 | 1047 | }; |
|
| 665 | 1048 | let new = repo.describe(&h, message)?; |
|
| 666 | 1049 | let c = repo.store().get_change(&new)?; |
|
| 667 | - | println!("{} {} {} {}", ui::done("Described"), ui::id(short_id(&c)), ui::hash(format!("#{}", new.short(12))), ui::subject(subject_of(message))); |
|
| 1050 | + | println!( |
|
| 1051 | + | "{} {} {} {}", |
|
| 1052 | + | ui::done("Described"), |
|
| 1053 | + | ui::id(short_id(&c)), |
|
| 1054 | + | ui::hash(format!("#{}", new.short(12))), |
|
| 1055 | + | ui::subject(subject_of(message)) |
|
| 1056 | + | ); |
|
| 668 | 1057 | Ok(0) |
|
| 669 | 1058 | } |
|
| 670 | 1059 | ||
| … | 7 unchanged lines |
||
| 678 | 1067 | None => repo.amend(message)?, |
|
| 679 | 1068 | }; |
|
| 680 | 1069 | let c = repo.store().get_change(&new)?; |
|
| 681 | - | println!("{} {} {} {}", ui::done("Amended"), ui::id(short_id(&c)), ui::hash(format!("#{}", new.short(12))), ui::subject(subject_of(&c.message))); |
|
| 1070 | + | println!( |
|
| 1071 | + | "{} {} {} {}", |
|
| 1072 | + | ui::done("Amended"), |
|
| 1073 | + | ui::id(short_id(&c)), |
|
| 1074 | + | ui::hash(format!("#{}", new.short(12))), |
|
| 1075 | + | ui::subject(subject_of(&c.message)) |
|
| 1076 | + | ); |
|
| 682 | 1077 | Ok(0) |
|
| 683 | 1078 | } |
|
| 684 | 1079 | ||
| … | 2 unchanged lines |
||
| 687 | 1082 | let h = repo.resolve_change(change)?; |
|
| 688 | 1083 | let c = repo.store().get_change(&h)?; |
|
| 689 | 1084 | let notes = repo.abandon(&h)?; |
|
| 690 | - | println!("{} {} {} {}", ui::done("Abandoned"), ui::id(short_id(&c)), ui::hash(format!("#{}", h.short(12))), ui::dim("its content remains on disk as uncommitted work if the working copy sat on it")); |
|
| 1085 | + | println!( |
|
| 1086 | + | "{} {} {} {}", |
|
| 1087 | + | ui::done("Abandoned"), |
|
| 1088 | + | ui::id(short_id(&c)), |
|
| 1089 | + | ui::hash(format!("#{}", h.short(12))), |
|
| 1090 | + | ui::dim("its content remains on disk as uncommitted work if the working copy sat on it") |
|
| 1091 | + | ); |
|
| 691 | 1092 | for n in notes { |
|
| 692 | 1093 | println!(" {} {n}", ui::note()); |
|
| 693 | 1094 | } |
|
| … | 4 unchanged lines |
||
| 698 | 1099 | let repo = open()?; |
|
| 699 | 1100 | let sp = ui::spinner("Writing checkpoint…"); |
|
| 700 | 1101 | let (r, changes, bytes) = repo.checkpoint()?; |
|
| 701 | - | sp.done(&format!("{} {} of {} ({} bytes) for {}", ui::done("Checkpoint"), ui::hash(format!("#{}", r.short(12))), plural(changes, "change"), bytes, ui::id(&repo.config.bookmark))); |
|
| 1102 | + | sp.done(&format!( |
|
| 1103 | + | "{} {} of {} ({} bytes) for {}", |
|
| 1104 | + | ui::done("Checkpoint"), |
|
| 1105 | + | ui::hash(format!("#{}", r.short(12))), |
|
| 1106 | + | plural(changes, "change"), |
|
| 1107 | + | bytes, |
|
| 1108 | + | ui::id(&repo.config.bookmark) |
|
| 1109 | + | )); |
|
| 702 | 1110 | Ok(0) |
|
| 703 | 1111 | } |
|
| 704 | 1112 | ||
| 705 | 1113 | /// For shell prompts: silent (exit 1) outside a repository, otherwise one line. |
|
| 706 | 1114 | pub fn prompt(format: &str, glyphs: bool, color: Option<&str>) -> Result<i32> { |
|
| 707 | - | let Ok(repo) = Repo::open(&std::env::current_dir()?) else { return Ok(1) }; |
|
| 1115 | + | let Ok(repo) = Repo::open(&std::env::current_dir()?) else { |
|
| 1116 | + | return Ok(1); |
|
| 1117 | + | }; |
|
| 708 | 1118 | let info = repo.quick_status()?; |
|
| 709 | - | let fmt = if glyphs && format == oxy_core::repo::DEFAULT_PROMPT_FORMAT { oxy_core::repo::GLYPH_PROMPT_FORMAT } else { format }; |
|
| 1119 | + | let fmt = if glyphs && format == oxy_core::repo::DEFAULT_PROMPT_FORMAT { |
|
| 1120 | + | oxy_core::repo::GLYPH_PROMPT_FORMAT |
|
| 1121 | + | } else { |
|
| 1122 | + | format |
|
| 1123 | + | }; |
|
| 710 | 1124 | let line = match color { |
|
| 711 | 1125 | Some("bash") => info.format_colored(fmt, ("\\[", "\\]")), |
|
| 712 | 1126 | Some("zsh") => info.format_colored(fmt, ("%{", "%}")), |
|
| … | 19 unchanged lines |
||
| 732 | 1146 | } |
|
| 733 | 1147 | let snap = repo.snapshot("oxy sparse")?; |
|
| 734 | 1148 | if !snap.wc.tree_ops.is_empty() || !snap.wc.edits.is_empty() { |
|
| 735 | - | anyhow::bail!("the working copy has changes; commit them before changing the sparse filter"); |
|
| 1149 | + | anyhow::bail!( |
|
| 1150 | + | "the working copy has changes; commit them before changing the sparse filter" |
|
| 1151 | + | ); |
|
| 736 | 1152 | } |
|
| 737 | 1153 | repo.config.set("filter.paths", &paths.join(","))?; |
|
| 738 | 1154 | repo.save_config()?; |
|
| 739 | 1155 | let heads = repo.bookmark_heads()?; |
|
| 740 | 1156 | let sp = ui::spinner("Re-checking out with the new filter…"); |
|
| 741 | 1157 | let r = repo.checkout(&heads, true, "oxy sparse (checkout)")?; |
|
| 742 | - | sp.done(&format!("{} {}: {} written, {} removed", ui::ok("Sparse filter set to"), ui::path(paths.join(",")), plural(r.written, "file"), r.removed)); |
|
| 1158 | + | sp.done(&format!( |
|
| 1159 | + | "{} {}: {} written, {} removed", |
|
| 1160 | + | ui::ok("Sparse filter set to"), |
|
| 1161 | + | ui::path(paths.join(",")), |
|
| 1162 | + | plural(r.written, "file"), |
|
| 1163 | + | r.removed |
|
| 1164 | + | )); |
|
| 743 | 1165 | Ok(0) |
|
| 744 | 1166 | } |
|
| 745 | 1167 | ||
| … | 17 unchanged lines |
||
| 763 | 1185 | ui::dim(ui::glyph("→", "->")), |
|
| 764 | 1186 | ui::hash(format!("#{}", t.state.short(12))), |
|
| 765 | 1187 | plural(t.heads.len(), "head"), |
|
| 766 | - | t.signer.map(|s| format!(", signed by {s}")).unwrap_or_else(|| ui::dim(", unsigned")) |
|
| 1188 | + | t.signer |
|
| 1189 | + | .map(|s| format!(", signed by {s}")) |
|
| 1190 | + | .unwrap_or_else(|| ui::dim(", unsigned")) |
|
| 767 | 1191 | ); |
|
| 768 | 1192 | Ok(0) |
|
| 769 | 1193 | } |
|
| … | 10 unchanged lines |
||
| 780 | 1204 | ("state", json::s(&t.state.to_hex())), |
|
| 781 | 1205 | ("heads", json::hashes(&t.heads)), |
|
| 782 | 1206 | ("message", json::s(&t.message)), |
|
| 783 | - | ("signer", json::opt(t.signer.as_ref(), |s| json::s(&s.to_string()))), |
|
| 1207 | + | ( |
|
| 1208 | + | "signer", |
|
| 1209 | + | json::opt(t.signer.as_ref(), |s| json::s(&s.to_string())), |
|
| 1210 | + | ), |
|
| 784 | 1211 | ]) |
|
| 785 | 1212 | })) |
|
| 786 | 1213 | ); |
|
| … | 9 unchanged lines |
||
| 796 | 1223 | ui::bookmark(&t.name), |
|
| 797 | 1224 | ui::hash(format!("#{}", t.state.short(12))), |
|
| 798 | 1225 | ui::sep() + &ui::dim(plural(t.heads.len(), "head")), |
|
| 799 | - | if t.message.is_empty() { String::new() } else { format!(" {}", ui::subject(subject_of(&t.message))) }, |
|
| 800 | - | t.signer.map(|s| ui::sep() + &ui::ok(format!("{} {s}", ui::glyph("✔", "v")))).unwrap_or_default() |
|
| 1226 | + | if t.message.is_empty() { |
|
| 1227 | + | String::new() |
|
| 1228 | + | } else { |
|
| 1229 | + | format!(" {}", ui::subject(subject_of(&t.message))) |
|
| 1230 | + | }, |
|
| 1231 | + | t.signer |
|
| 1232 | + | .map(|s| ui::sep() + &ui::ok(format!("{} {s}", ui::glyph("✔", "v")))) |
|
| 1233 | + | .unwrap_or_default() |
|
| 801 | 1234 | ); |
|
| 802 | 1235 | } |
|
| 803 | 1236 | Ok(0) |
|
| … | 12 unchanged lines |
||
| 816 | 1249 | }; |
|
| 817 | 1250 | let sp = ui::spinner(&format!("Creating workspace at {}…", path.display())); |
|
| 818 | 1251 | let ws = repo.workspace_add(path, heads.as_deref())?; |
|
| 819 | - | sp.done(&format!("{} {} (workspace {})", ui::done("Workspace created at"), ui::path(ws.root().display()), ui::id(ws.workspace().short(8)))); |
|
| 1252 | + | sp.done(&format!( |
|
| 1253 | + | "{} {} (workspace {})", |
|
| 1254 | + | ui::done("Workspace created at"), |
|
| 1255 | + | ui::path(ws.root().display()), |
|
| 1256 | + | ui::id(ws.workspace().short(8)) |
|
| 1257 | + | )); |
|
| 820 | 1258 | Ok(0) |
|
| 821 | 1259 | } |
|
| 822 | 1260 | ||
| … | 7 unchanged lines |
||
| 830 | 1268 | ("id", json::s(&w.id.to_hex())), |
|
| 831 | 1269 | ("path", json::s(&w.path.to_string_lossy())), |
|
| 832 | 1270 | ("current", json::b(w.current)), |
|
| 833 | - | ("working_copy", json::opt(w.working_copy, |h| json::s(&h.to_hex()))), |
|
| 1271 | + | ( |
|
| 1272 | + | "working_copy", |
|
| 1273 | + | json::opt(w.working_copy, |h| json::s(&h.to_hex())), |
|
| 1274 | + | ), |
|
| 834 | 1275 | ]) |
|
| 835 | 1276 | })) |
|
| 836 | 1277 | ); |
|
| … | 2 unchanged lines |
||
| 839 | 1280 | for w in repo.workspaces()? { |
|
| 840 | 1281 | println!( |
|
| 841 | 1282 | "{} {} {}{}", |
|
| 842 | - | if w.current { ui::ok(ui::glyph("●", "*")) } else { ui::dim(ui::glyph("○", "o")) }, |
|
| 1283 | + | if w.current { |
|
| 1284 | + | ui::ok(ui::glyph("●", "*")) |
|
| 1285 | + | } else { |
|
| 1286 | + | ui::dim(ui::glyph("○", "o")) |
|
| 1287 | + | }, |
|
| 843 | 1288 | ui::id(w.id.short(8)), |
|
| 844 | 1289 | ui::path(w.path.display()), |
|
| 845 | - | w.working_copy.map(|h| ui::sep() + &ui::dim(format!("working copy #{}", h.short(12)))).unwrap_or_else(|| ui::sep() + &ui::warn("not in view")) |
|
| 1290 | + | w.working_copy |
|
| 1291 | + | .map(|h| ui::sep() + &ui::dim(format!("working copy #{}", h.short(12)))) |
|
| 1292 | + | .unwrap_or_else(|| ui::sep() + &ui::warn("not in view")) |
|
| 846 | 1293 | ); |
|
| 847 | 1294 | } |
|
| 848 | 1295 | Ok(0) |
|
| … | 3 unchanged lines |
||
| 852 | 1299 | let repo = open()?; |
|
| 853 | 1300 | let all = repo.workspaces()?; |
|
| 854 | 1301 | let target = std::fs::canonicalize(spec).ok(); |
|
| 855 | - | let matches: Vec<_> = all.iter().filter(|w| w.id.starts_with_hex(spec) || target.as_ref().is_some_and(|t| *t == w.path)).collect(); |
|
| 1302 | + | let matches: Vec<_> = all |
|
| 1303 | + | .iter() |
|
| 1304 | + | .filter(|w| w.id.starts_with_hex(spec) || target.as_ref().is_some_and(|t| *t == w.path)) |
|
| 1305 | + | .collect(); |
|
| 856 | 1306 | let w = match matches.as_slice() { |
|
| 857 | 1307 | [w] => *w, |
|
| 858 | 1308 | [] => anyhow::bail!("no workspace matches {spec:?}"), |
|
| 859 | 1309 | _ => anyhow::bail!("{spec:?} is ambiguous"), |
|
| 860 | 1310 | }; |
|
| 861 | 1311 | repo.workspace_forget(&w.id)?; |
|
| 862 | - | println!("{} workspace {} at {}; its files were left in place.", ui::done("Forgot"), ui::id(w.id.short(8)), ui::path(w.path.display())); |
|
| 1312 | + | println!( |
|
| 1313 | + | "{} workspace {} at {}; its files were left in place.", |
|
| 1314 | + | ui::done("Forgot"), |
|
| 1315 | + | ui::id(w.id.short(8)), |
|
| 1316 | + | ui::path(w.path.display()) |
|
| 1317 | + | ); |
|
| 863 | 1318 | Ok(0) |
|
| 864 | 1319 | } |
|
| 865 | 1320 | ||
| 866 | 1321 | pub fn lock(path: &str) -> Result<i32> { |
|
| 867 | 1322 | let repo = open()?; |
|
| 868 | 1323 | repo.lock_path(path)?; |
|
| 869 | - | println!("{} {} {}", ui::done("Locked"), ui::path(path), ui::dim("(advisory, local)")); |
|
| 1324 | + | println!( |
|
| 1325 | + | "{} {} {}", |
|
| 1326 | + | ui::done("Locked"), |
|
| 1327 | + | ui::path(path), |
|
| 1328 | + | ui::dim("(advisory, local)") |
|
| 1329 | + | ); |
|
| 870 | 1330 | Ok(0) |
|
| 871 | 1331 | } |
|
| 872 | 1332 | ||
| … | 10 unchanged lines |
||
| 883 | 1343 | let (h1, h2) = repo.split(&h, paths)?; |
|
| 884 | 1344 | let c1 = repo.store().get_change(&h1)?; |
|
| 885 | 1345 | let c2 = repo.store().get_change(&h2)?; |
|
| 886 | - | println!("{} into {} {} and {} {}", ui::done("Split"), ui::id(short_id(&c1)), ui::hash(format!("#{}", h1.short(12))), ui::id(short_id(&c2)), ui::hash(format!("#{}", h2.short(12)))); |
|
| 1346 | + | println!( |
|
| 1347 | + | "{} into {} {} and {} {}", |
|
| 1348 | + | ui::done("Split"), |
|
| 1349 | + | ui::id(short_id(&c1)), |
|
| 1350 | + | ui::hash(format!("#{}", h1.short(12))), |
|
| 1351 | + | ui::id(short_id(&c2)), |
|
| 1352 | + | ui::hash(format!("#{}", h2.short(12))) |
|
| 1353 | + | ); |
|
| 887 | 1354 | Ok(0) |
|
| 888 | 1355 | } |
|
| 889 | 1356 | ||
| … | 8 unchanged lines |
||
| 898 | 1365 | pub fn bookmark_create(name: &str, changes: &[String]) -> Result<i32> { |
|
| 899 | 1366 | let repo = open()?; |
|
| 900 | 1367 | repo.snapshot("oxy bookmark create")?; |
|
| 901 | - | let heads = if changes.is_empty() { None } else { Some(heads_from(&repo, changes)?) }; |
|
| 1368 | + | let heads = if changes.is_empty() { |
|
| 1369 | + | None |
|
| 1370 | + | } else { |
|
| 1371 | + | Some(heads_from(&repo, changes)?) |
|
| 1372 | + | }; |
|
| 902 | 1373 | let heads = repo.bookmark_create(name, heads.as_deref())?; |
|
| 903 | - | println!("{} bookmark {} {} {}", ui::done("Created"), ui::bookmark(name), ui::dim(ui::glyph("→", "->")), ui::dim(plural(heads.len(), "head"))); |
|
| 904 | - | println!(" {} {}", ui::note(), ui::dim(format!("switch to it with oxy bookmark switch {name}"))); |
|
| 1374 | + | println!( |
|
| 1375 | + | "{} bookmark {} {} {}", |
|
| 1376 | + | ui::done("Created"), |
|
| 1377 | + | ui::bookmark(name), |
|
| 1378 | + | ui::dim(ui::glyph("→", "->")), |
|
| 1379 | + | ui::dim(plural(heads.len(), "head")) |
|
| 1380 | + | ); |
|
| 1381 | + | println!( |
|
| 1382 | + | " {} {}", |
|
| 1383 | + | ui::note(), |
|
| 1384 | + | ui::dim(format!("switch to it with oxy bookmark switch {name}")) |
|
| 1385 | + | ); |
|
| 905 | 1386 | Ok(0) |
|
| 906 | 1387 | } |
|
| 907 | 1388 | ||
| … | 2 unchanged lines |
||
| 910 | 1391 | repo.snapshot("oxy bookmark set")?; |
|
| 911 | 1392 | let heads = heads_from(&repo, changes)?; |
|
| 912 | 1393 | let heads = repo.bookmark_set(name, &heads)?; |
|
| 913 | - | println!("{} bookmark {} {} {}", ui::done("Moved"), ui::bookmark(name), ui::dim(ui::glyph("→", "->")), ui::dim(plural(heads.len(), "head"))); |
|
| 1394 | + | println!( |
|
| 1395 | + | "{} bookmark {} {} {}", |
|
| 1396 | + | ui::done("Moved"), |
|
| 1397 | + | ui::bookmark(name), |
|
| 1398 | + | ui::dim(ui::glyph("→", "->")), |
|
| 1399 | + | ui::dim(plural(heads.len(), "head")) |
|
| 1400 | + | ); |
|
| 914 | 1401 | Ok(0) |
|
| 915 | 1402 | } |
|
| 916 | 1403 | ||
| 917 | 1404 | pub fn bookmark_delete(name: &str) -> Result<i32> { |
|
| 918 | 1405 | let repo = open()?; |
|
| 919 | 1406 | repo.bookmark_delete(name)?; |
|
| 920 | - | println!("{} bookmark {} {}", ui::done("Deleted"), ui::bookmark(name), ui::dim("(its changes stay until unreachable)")); |
|
| 1407 | + | println!( |
|
| 1408 | + | "{} bookmark {} {}", |
|
| 1409 | + | ui::done("Deleted"), |
|
| 1410 | + | ui::bookmark(name), |
|
| 1411 | + | ui::dim("(its changes stay until unreachable)") |
|
| 1412 | + | ); |
|
| 921 | 1413 | Ok(0) |
|
| 922 | 1414 | } |
|
| 923 | 1415 | ||
| … | 13 unchanged lines |
||
| 937 | 1429 | return Ok(0); |
|
| 938 | 1430 | } |
|
| 939 | 1431 | for b in repo.bookmarks()? { |
|
| 940 | - | let mark = if b.current { ui::ok(ui::glyph("●", "*")) } else { ui::dim(ui::glyph("○", "o")) }; |
|
| 1432 | + | let mark = if b.current { |
|
| 1433 | + | ui::ok(ui::glyph("●", "*")) |
|
| 1434 | + | } else { |
|
| 1435 | + | ui::dim(ui::glyph("○", "o")) |
|
| 1436 | + | }; |
|
| 941 | 1437 | let mut tail = String::new(); |
|
| 942 | 1438 | if let Some(n) = b.ahead.filter(|n| *n > 0) { |
|
| 943 | 1439 | tail += &ui::ok(format!(" {}{n}", ui::glyph("↑", "+"))); |
|
| … | 1 unchanged lines |
||
| 945 | 1441 | if let Some(n) = b.behind.filter(|n| *n > 0) { |
|
| 946 | 1442 | tail += &ui::warn(format!(" {}{n}", ui::glyph("↓", "-"))); |
|
| 947 | 1443 | } |
|
| 948 | - | let head = b.heads.first().map(|h| repo.store().get_change(h).map(|c| ui::subject(subject_of(&c.message))).unwrap_or_default()).unwrap_or_else(|| ui::dim("(empty)")); |
|
| 949 | - | println!("{mark} {}{}{}{tail} {head}", ui::bookmark(&b.name), ui::sep(), ui::dim(plural(b.heads.len(), "head"))); |
|
| 1444 | + | let head = b |
|
| 1445 | + | .heads |
|
| 1446 | + | .first() |
|
| 1447 | + | .map(|h| { |
|
| 1448 | + | repo.store() |
|
| 1449 | + | .get_change(h) |
|
| 1450 | + | .map(|c| ui::subject(subject_of(&c.message))) |
|
| 1451 | + | .unwrap_or_default() |
|
| 1452 | + | }) |
|
| 1453 | + | .unwrap_or_else(|| ui::dim("(empty)")); |
|
| 1454 | + | println!( |
|
| 1455 | + | "{mark} {}{}{}{tail} {head}", |
|
| 1456 | + | ui::bookmark(&b.name), |
|
| 1457 | + | ui::sep(), |
|
| 1458 | + | ui::dim(plural(b.heads.len(), "head")) |
|
| 1459 | + | ); |
|
| 950 | 1460 | } |
|
| 951 | 1461 | Ok(0) |
|
| 952 | 1462 | } |
|
| … | 2 unchanged lines |
||
| 955 | 1465 | let mut repo = open()?; |
|
| 956 | 1466 | let sp = ui::spinner(&format!("Switching to {name}…")); |
|
| 957 | 1467 | let r = repo.bookmark_switch(name, discard)?; |
|
| 958 | - | sp.done(&format!("{} to {}: {} written, {} removed", ui::done("Switched"), ui::bookmark(name), plural(r.written, "file"), r.removed)); |
|
| 1468 | + | sp.done(&format!( |
|
| 1469 | + | "{} to {}: {} written, {} removed", |
|
| 1470 | + | ui::done("Switched"), |
|
| 1471 | + | ui::bookmark(name), |
|
| 1472 | + | plural(r.written, "file"), |
|
| 1473 | + | r.removed |
|
| 1474 | + | )); |
|
| 959 | 1475 | if r.conflicted { |
|
| 960 | - | println!(" {} {}", ui::caution(), ui::warn("this state has conflicts; see oxy status")); |
|
| 1476 | + | println!( |
|
| 1477 | + | " {} {}", |
|
| 1478 | + | ui::caution(), |
|
| 1479 | + | ui::warn("this state has conflicts; see oxy status") |
|
| 1480 | + | ); |
|
| 961 | 1481 | } |
|
| 962 | 1482 | Ok(0) |
|
| 963 | 1483 | } |
|
| … | 3 unchanged lines |
||
| 967 | 1487 | let sp = ui::spinner(&format!("Merging {bookmark}…")); |
|
| 968 | 1488 | let r = repo.merge(bookmark)?; |
|
| 969 | 1489 | if !r.changed { |
|
| 970 | - | sp.done(&format!("{} {}", ui::ok(ui::glyph("✔", "*")), ui::dim(format!("{} already contains {bookmark}", repo.config.bookmark)))); |
|
| 1490 | + | sp.done(&format!( |
|
| 1491 | + | "{} {}", |
|
| 1492 | + | ui::ok(ui::glyph("✔", "*")), |
|
| 1493 | + | ui::dim(format!( |
|
| 1494 | + | "{} already contains {bookmark}", |
|
| 1495 | + | repo.config.bookmark |
|
| 1496 | + | )) |
|
| 1497 | + | )); |
|
| 971 | 1498 | return Ok(0); |
|
| 972 | 1499 | } |
|
| 973 | 1500 | sp.done(&format!( |
|
| … | 2 unchanged lines |
||
| 976 | 1503 | ui::bookmark(bookmark), |
|
| 977 | 1504 | ui::bookmark(&repo.config.bookmark), |
|
| 978 | 1505 | plural(r.heads.len(), "head"), |
|
| 979 | - | ui::dim(if r.checked_out { "; working copy updated" } else { "; run oxy checkout to update the working copy" }) |
|
| 1506 | + | ui::dim(if r.checked_out { |
|
| 1507 | + | "; working copy updated" |
|
| 1508 | + | } else { |
|
| 1509 | + | "; run oxy checkout to update the working copy" |
|
| 1510 | + | }) |
|
| 980 | 1511 | )); |
|
| 981 | 1512 | let snap = repo.snapshot("oxy merge")?; |
|
| 982 | 1513 | if snap.base_snapshot.is_conflicted() { |
|
| 983 | - | println!(" {} {}", ui::caution(), ui::warn("the result has conflicts; see oxy status")); |
|
| 1514 | + | println!( |
|
| 1515 | + | " {} {}", |
|
| 1516 | + | ui::caution(), |
|
| 1517 | + | ui::warn("the result has conflicts; see oxy status") |
|
| 1518 | + | ); |
|
| 984 | 1519 | } |
|
| 985 | 1520 | Ok(0) |
|
| 986 | 1521 | } |
|
| … | 5 unchanged lines |
||
| 992 | 1527 | let sp = ui::spinner("Squashing…"); |
|
| 993 | 1528 | let h = repo.squash(&c, &d)?; |
|
| 994 | 1529 | let new = repo.store().get_change(&h)?; |
|
| 995 | - | sp.done(&format!("{} into {} {} {}", ui::done("Squashed"), ui::id(short_id(&new)), ui::hash(format!("#{}", h.short(12))), ui::subject(subject_of(&new.message)))); |
|
| 1530 | + | sp.done(&format!( |
|
| 1531 | + | "{} into {} {} {}", |
|
| 1532 | + | ui::done("Squashed"), |
|
| 1533 | + | ui::id(short_id(&new)), |
|
| 1534 | + | ui::hash(format!("#{}", h.short(12))), |
|
| 1535 | + | ui::subject(subject_of(&new.message)) |
|
| 1536 | + | )); |
|
| 996 | 1537 | Ok(0) |
|
| 997 | 1538 | } |
|
| 998 | 1539 | ||
| … | 7 unchanged lines |
||
| 1006 | 1547 | let sp = ui::spinner("Rebasing…"); |
|
| 1007 | 1548 | let h = repo.rebase(&c, &heads)?; |
|
| 1008 | 1549 | let new = repo.store().get_change(&h)?; |
|
| 1009 | - | sp.done(&format!("{} {} {} {}", ui::done("Rebased"), ui::id(short_id(&new)), ui::hash(format!("#{}", h.short(12))), ui::dim(format!("now depends on {}", plural(new.deps.len(), "change"))))); |
|
| 1550 | + | sp.done(&format!( |
|
| 1551 | + | "{} {} {} {}", |
|
| 1552 | + | ui::done("Rebased"), |
|
| 1553 | + | ui::id(short_id(&new)), |
|
| 1554 | + | ui::hash(format!("#{}", h.short(12))), |
|
| 1555 | + | ui::dim(format!( |
|
| 1556 | + | "now depends on {}", |
|
| 1557 | + | plural(new.deps.len(), "change") |
|
| 1558 | + | )) |
|
| 1559 | + | )); |
|
| 1010 | 1560 | Ok(0) |
|
| 1011 | 1561 | } |
|
| 1012 | - |
modified tests/cli_json.rs +113 −19
| … | 5 unchanged lines |
||
| 6 | 6 | use std::process::Command; |
|
| 7 | 7 | ||
| 8 | 8 | fn oxy(dir: &Path, home: &Path, args: &[&str]) -> (i32, Vec<u8>, String) { |
|
| 9 | - | let out = Command::new(env!("CARGO_BIN_EXE_oxy")).arg("--plain").args(args).current_dir(dir).env("OXY_HOME", home).env("NO_COLOR", "1").output().unwrap(); |
|
| 10 | - | (out.status.code().unwrap_or(-1), out.stdout, String::from_utf8_lossy(&out.stderr).to_string()) |
|
| 9 | + | let out = Command::new(env!("CARGO_BIN_EXE_oxy")) |
|
| 10 | + | .arg("--plain") |
|
| 11 | + | .args(args) |
|
| 12 | + | .current_dir(dir) |
|
| 13 | + | .env("OXY_HOME", home) |
|
| 14 | + | .env("NO_COLOR", "1") |
|
| 15 | + | .output() |
|
| 16 | + | .unwrap(); |
|
| 17 | + | ( |
|
| 18 | + | out.status.code().unwrap_or(-1), |
|
| 19 | + | out.stdout, |
|
| 20 | + | String::from_utf8_lossy(&out.stderr).to_string(), |
|
| 21 | + | ) |
|
| 11 | 22 | } |
|
| 12 | 23 | ||
| 13 | 24 | fn json(dir: &Path, home: &Path, args: &[&str]) -> Value { |
|
| 14 | 25 | let (code, out, err) = oxy(dir, home, args); |
|
| 15 | - | assert!(code == 0 || code == 2 || code == 1, "oxy {args:?} exited {code}: {err}"); |
|
| 16 | - | serde_json::from_slice(&out).unwrap_or_else(|e| panic!("oxy {args:?} is not JSON: {e}\n{}", String::from_utf8_lossy(&out))) |
|
| 26 | + | assert!( |
|
| 27 | + | code == 0 || code == 2 || code == 1, |
|
| 28 | + | "oxy {args:?} exited {code}: {err}" |
|
| 29 | + | ); |
|
| 30 | + | serde_json::from_slice(&out).unwrap_or_else(|e| { |
|
| 31 | + | panic!( |
|
| 32 | + | "oxy {args:?} is not JSON: {e}\n{}", |
|
| 33 | + | String::from_utf8_lossy(&out) |
|
| 34 | + | ) |
|
| 35 | + | }) |
|
| 17 | 36 | } |
|
| 18 | 37 | ||
| 19 | 38 | fn write(dir: &Path, rel: &str, content: &str) { |
|
| … | 8 unchanged lines |
||
| 28 | 47 | let home = tmp.path().join("home"); |
|
| 29 | 48 | std::fs::create_dir_all(&home).unwrap(); |
|
| 30 | 49 | let origin = tmp.path().join("origin"); |
|
| 31 | - | assert_eq!(oxy(tmp.path(), &home, &["init", origin.to_str().unwrap()]).0, 0); |
|
| 50 | + | assert_eq!( |
|
| 51 | + | oxy(tmp.path(), &home, &["init", origin.to_str().unwrap()]).0, |
|
| 52 | + | 0 |
|
| 53 | + | ); |
|
| 32 | 54 | let repo = tmp.path().join("repo"); |
|
| 33 | - | assert_eq!(oxy(tmp.path(), &home, &["clone", origin.to_str().unwrap(), repo.to_str().unwrap()]).0, 0); |
|
| 55 | + | assert_eq!( |
|
| 56 | + | oxy( |
|
| 57 | + | tmp.path(), |
|
| 58 | + | &home, |
|
| 59 | + | &["clone", origin.to_str().unwrap(), repo.to_str().unwrap()] |
|
| 60 | + | ) |
|
| 61 | + | .0, |
|
| 62 | + | 0 |
|
| 63 | + | ); |
|
| 34 | 64 | let r = repo.as_path(); |
|
| 35 | - | assert_eq!(oxy(r, &home, &["identity", "create", "--name", "Ada", "--email", "[email protected]"]).0, 0); |
|
| 36 | - | assert_eq!(oxy(r, &home, &["config", "--user", "core.sign", "change"]).0, 0); |
|
| 65 | + | assert_eq!( |
|
| 66 | + | oxy( |
|
| 67 | + | r, |
|
| 68 | + | &home, |
|
| 69 | + | &[ |
|
| 70 | + | "identity", |
|
| 71 | + | "create", |
|
| 72 | + | "--name", |
|
| 73 | + | "Ada", |
|
| 74 | + | "--email", |
|
| 75 | + | "[email protected]" |
|
| 76 | + | ] |
|
| 77 | + | ) |
|
| 78 | + | .0, |
|
| 79 | + | 0 |
|
| 80 | + | ); |
|
| 81 | + | assert_eq!( |
|
| 82 | + | oxy(r, &home, &["config", "--user", "core.sign", "change"]).0, |
|
| 83 | + | 0 |
|
| 84 | + | ); |
|
| 37 | 85 | ||
| 38 | 86 | let v = json(r, &home, &["version", "--json"]); |
|
| 39 | 87 | assert_eq!(v["protocol"], 1); |
|
| … | 23 unchanged lines |
||
| 63 | 111 | let hunks = json(r, &home, &["hunks", "--json"]); |
|
| 64 | 112 | let hunks = hunks.as_array().unwrap(); |
|
| 65 | 113 | assert!(hunks.len() >= 3, "{hunks:?}"); |
|
| 66 | - | let b_hunk = hunks.iter().find(|h| h["path"] == "b.txt").unwrap()["index"].as_u64().unwrap(); |
|
| 114 | + | let b_hunk = hunks.iter().find(|h| h["path"] == "b.txt").unwrap()["index"] |
|
| 115 | + | .as_u64() |
|
| 116 | + | .unwrap(); |
|
| 67 | 117 | // Commit only b's hunk; discard c by path; then discard a's remaining hunk. |
|
| 68 | - | assert_eq!(oxy(r, &home, &["commit", "-m", "b only", "--hunks", &b_hunk.to_string()]).0, 0); |
|
| 118 | + | assert_eq!( |
|
| 119 | + | oxy( |
|
| 120 | + | r, |
|
| 121 | + | &home, |
|
| 122 | + | &["commit", "-m", "b only", "--hunks", &b_hunk.to_string()] |
|
| 123 | + | ) |
|
| 124 | + | .0, |
|
| 125 | + | 0 |
|
| 126 | + | ); |
|
| 69 | 127 | assert_eq!(oxy(r, &home, &["discard", "c.txt"]).0, 0); |
|
| 70 | 128 | assert!(!r.join("c.txt").exists()); |
|
| 71 | 129 | let hunks = json(r, &home, &["hunks", "--json"]); |
|
| 72 | - | let a_hunk = hunks.as_array().unwrap().iter().find(|h| h["path"] == "a.txt").unwrap()["index"].as_u64().unwrap(); |
|
| 73 | - | assert_eq!(oxy(r, &home, &["discard", "--hunks", &a_hunk.to_string()]).0, 0); |
|
| 74 | - | assert_eq!(std::fs::read_to_string(r.join("a.txt")).unwrap(), "1\n2\n3\n"); |
|
| 130 | + | let a_hunk = hunks |
|
| 131 | + | .as_array() |
|
| 132 | + | .unwrap() |
|
| 133 | + | .iter() |
|
| 134 | + | .find(|h| h["path"] == "a.txt") |
|
| 135 | + | .unwrap()["index"] |
|
| 136 | + | .as_u64() |
|
| 137 | + | .unwrap(); |
|
| 138 | + | assert_eq!( |
|
| 139 | + | oxy(r, &home, &["discard", "--hunks", &a_hunk.to_string()]).0, |
|
| 140 | + | 0 |
|
| 141 | + | ); |
|
| 142 | + | assert_eq!( |
|
| 143 | + | std::fs::read_to_string(r.join("a.txt")).unwrap(), |
|
| 144 | + | "1\n2\n3\n" |
|
| 145 | + | ); |
|
| 75 | 146 | let st = json(r, &home, &["status", "--json"]); |
|
| 76 | 147 | assert_eq!(st["clean"], true, "{st}"); |
|
| 77 | 148 | ||
| … | 16 unchanged lines |
||
| 94 | 165 | let show = json(r, &home, &["show", "--json", &format!("#{}", &hash[..12])]); |
|
| 95 | 166 | assert_eq!(show["files"][0]["path"], "b.txt"); |
|
| 96 | 167 | let verify = json(r, &home, &["verify", "--json"]); |
|
| 97 | - | assert!(verify.as_array().unwrap().iter().all(|v| v["status"] == "signed")); |
|
| 168 | + | assert!(verify |
|
| 169 | + | .as_array() |
|
| 170 | + | .unwrap() |
|
| 171 | + | .iter() |
|
| 172 | + | .all(|v| v["status"] == "signed")); |
|
| 98 | 173 | let ops = json(r, &home, &["op", "log", "--json"]); |
|
| 99 | 174 | assert!(ops[0]["current"] == true && ops.as_array().unwrap().len() > 3); |
|
| 100 | 175 | assert_eq!(oxy(r, &home, &["bookmark", "create", "feature"]).0, 0); |
|
| 101 | 176 | let bms = json(r, &home, &["bookmark", "list", "--json"]); |
|
| 102 | - | assert_eq!(bms.as_array().unwrap().iter().map(|b| b["name"].as_str().unwrap()).collect::<Vec<_>>(), vec!["feature", "main"]); |
|
| 177 | + | assert_eq!( |
|
| 178 | + | bms.as_array() |
|
| 179 | + | .unwrap() |
|
| 180 | + | .iter() |
|
| 181 | + | .map(|b| b["name"].as_str().unwrap()) |
|
| 182 | + | .collect::<Vec<_>>(), |
|
| 183 | + | vec!["feature", "main"] |
|
| 184 | + | ); |
|
| 103 | 185 | let remotes = json(r, &home, &["remote", "list", "--json"]); |
|
| 104 | 186 | assert_eq!(remotes[0]["name"], "origin"); |
|
| 105 | 187 | let push = json(r, &home, &["push", "--json"]); |
|
| … | 3 unchanged lines |
||
| 109 | 191 | let pull = json(r, &home, &["pull", "--json"]); |
|
| 110 | 192 | assert_eq!(pull["changed"], false); |
|
| 111 | 193 | assert_eq!(json(r, &home, &["resolve", "--json"])["conflicted"], false); |
|
| 112 | - | assert!(json(r, &home, &["submodule", "status", "--json"]).as_array().unwrap().is_empty()); |
|
| 194 | + | assert!(json(r, &home, &["submodule", "status", "--json"]) |
|
| 195 | + | .as_array() |
|
| 196 | + | .unwrap() |
|
| 197 | + | .is_empty()); |
|
| 113 | 198 | assert!(json(r, &home, &["bisect", "status", "--json"]).is_null()); |
|
| 114 | 199 | assert!(json(r, &home, &["policy", "show", "--json"]).is_null()); |
|
| 115 | 200 | assert_eq!(oxy(r, &home, &["policy", "init"]).0, 0); |
|
| 116 | 201 | let policy = json(r, &home, &["policy", "show", "--json"]); |
|
| 117 | 202 | assert_eq!(policy["roots"][0]["name"], "Ada"); |
|
| 118 | - | assert_eq!(oxy(r, &home, &["tag", "create", "v1", "-m", "first tag"]).0, 0); |
|
| 203 | + | assert_eq!( |
|
| 204 | + | oxy(r, &home, &["tag", "create", "v1", "-m", "first tag"]).0, |
|
| 205 | + | 0 |
|
| 206 | + | ); |
|
| 119 | 207 | let tags = json(r, &home, &["tag", "list", "--json"]); |
|
| 120 | - | assert_eq!((tags[0]["name"].as_str(), tags[0]["message"].as_str()), (Some("v1"), Some("first tag"))); |
|
| 208 | + | assert_eq!( |
|
| 209 | + | (tags[0]["name"].as_str(), tags[0]["message"].as_str()), |
|
| 210 | + | (Some("v1"), Some("first tag")) |
|
| 211 | + | ); |
|
| 121 | 212 | let ws = json(r, &home, &["workspace", "list", "--json"]); |
|
| 122 | - | assert_eq!((ws.as_array().unwrap().len(), ws[0]["current"].as_bool()), (1, Some(true))); |
|
| 213 | + | assert_eq!( |
|
| 214 | + | (ws.as_array().unwrap().len(), ws[0]["current"].as_bool()), |
|
| 215 | + | (1, Some(true)) |
|
| 216 | + | ); |
|
| 123 | 217 | } |