|
|
… |
18 unchanged lines |
| 19 |
19 |
|
/// Files the edits do not name keep their content, conflict markers |
| 20 |
20 |
|
/// included, so an edit that removes a file's markers resolves that |
| 21 |
21 |
|
/// file's conflict (a resolution change, spec §9.6). |
| 22 |
|
- |
pub fn build_change(&self, heads: &[ChangeHash], edits: &[FileEdit], message: &str, author: KeyId) -> Result<Change> { |
|
22 |
+ |
pub fn build_change( |
|
23 |
+ |
&self, |
|
24 |
+ |
heads: &[ChangeHash], |
|
25 |
+ |
edits: &[FileEdit], |
|
26 |
+ |
message: &str, |
|
27 |
+ |
author: KeyId, |
|
28 |
+ |
) -> Result<Change> { |
| 23 |
29 |
|
if message.trim().is_empty() { |
| 24 |
30 |
|
return fail("a change message is required"); |
| 25 |
31 |
|
} |
|
|
… |
6 unchanged lines |
| 32 |
38 |
|
for (path, f) in &base_snapshot.files { |
| 33 |
39 |
|
let entry = match (&f.kind, &f.content) { |
| 34 |
40 |
|
(NodeKind::Dir, _) | (_, FileContent::Dir) => continue, |
| 35 |
|
- |
(kind, FileContent::Text { bytes, .. }) => FsEntry::new(*kind, f.mode.exec, bytes.clone()), |
| 36 |
|
- |
(kind, FileContent::Opaque { blobs }) => FsEntry { kind: *kind, exec: f.mode.exec, content: vec![], blob: blobs.first().copied(), content_hash: None }, |
|
41 |
+ |
(kind, FileContent::Text { bytes, .. }) => { |
|
42 |
+ |
FsEntry::new(*kind, f.mode.exec, bytes.clone()) |
|
43 |
+ |
} |
|
44 |
+ |
(kind, FileContent::Opaque { blobs }) => FsEntry { |
|
45 |
+ |
kind: *kind, |
|
46 |
+ |
exec: f.mode.exec, |
|
47 |
+ |
content: vec![], |
|
48 |
+ |
blob: blobs.first().copied(), |
|
49 |
+ |
content_hash: None, |
|
50 |
+ |
}, |
| 37 |
51 |
|
}; |
| 38 |
52 |
|
entries.insert(path.clone(), entry); |
| 39 |
53 |
|
} |
| 40 |
54 |
|
for e in edits { |
| 41 |
55 |
|
let path = e.path.trim_matches('/'); |
| 42 |
|
- |
if path.is_empty() || path.split('/').any(|c| c.is_empty() || c == "." || c == ".." || c == OXY_DIR) { |
|
56 |
+ |
if path.is_empty() |
|
57 |
+ |
|| path |
|
58 |
+ |
.split('/') |
|
59 |
+ |
.any(|c| c.is_empty() || c == "." || c == ".." || c == OXY_DIR) |
|
60 |
+ |
{ |
| 43 |
61 |
|
return fail(format!("{:?} is not a valid path", e.path)); |
| 44 |
62 |
|
} |
| 45 |
63 |
|
match &e.content { |
| 46 |
64 |
|
Some(bytes) => { |
| 47 |
65 |
|
let exec = entries.get(path).map(|x| x.exec).unwrap_or(false); |
| 48 |
66 |
|
// A file cannot sit where a directory is, or inside a file. |
| 49 |
|
- |
if base_snapshot.files.get(path).is_some_and(|f| f.kind == NodeKind::Dir) { |
|
67 |
+ |
if base_snapshot |
|
68 |
+ |
.files |
|
69 |
+ |
.get(path) |
|
70 |
+ |
.is_some_and(|f| f.kind == NodeKind::Dir) |
|
71 |
+ |
{ |
| 50 |
72 |
|
return fail(format!("{path} is a directory")); |
| 51 |
73 |
|
} |
| 52 |
74 |
|
let mut parent = path; |
| 53 |
75 |
|
while let Some((p, _)) = parent.rsplit_once('/') { |
| 54 |
|
- |
if base_snapshot.files.get(p).is_some_and(|f| f.kind != NodeKind::Dir) { |
|
76 |
+ |
if base_snapshot |
|
77 |
+ |
.files |
|
78 |
+ |
.get(p) |
|
79 |
+ |
.is_some_and(|f| f.kind != NodeKind::Dir) |
|
80 |
+ |
{ |
| 55 |
81 |
|
return fail(format!("{p} is a file, not a directory")); |
| 56 |
82 |
|
} |
| 57 |
83 |
|
parent = p; |
| 58 |
84 |
|
} |
| 59 |
|
- |
entries.insert(path.to_string(), FsEntry::new(NodeKind::File, exec, bytes.clone())); |
|
85 |
+ |
entries.insert( |
|
86 |
+ |
path.to_string(), |
|
87 |
+ |
FsEntry::new(NodeKind::File, exec, bytes.clone()), |
|
88 |
+ |
); |
| 60 |
89 |
|
} |
| 61 |
90 |
|
None => { |
| 62 |
91 |
|
if entries.remove(path).is_none() { |
|
|
… |
9 unchanged lines |
| 72 |
101 |
|
} |
| 73 |
102 |
|
} |
| 74 |
103 |
|
let built = self.build_from_entries(&base, &base_snapshot, HashMap::new(), &entries)?; |
| 75 |
|
- |
if let Some(s) = built.statuses.iter().find(|s| s.status == ChangeStatus::ConflictEdited) { |
|
104 |
+ |
if let Some(s) = built |
|
105 |
+ |
.statuses |
|
106 |
+ |
.iter() |
|
107 |
+ |
.find(|s| s.status == ChangeStatus::ConflictEdited) |
|
108 |
+ |
{ |
| 76 |
109 |
|
return fail(format!("{} still contains conflict markers", s.path)); |
| 77 |
110 |
|
} |
| 78 |
111 |
|
if built.tree_ops.is_empty() && built.edits.is_empty() { |
|
|
… |
7 unchanged lines |
| 86 |
119 |
|
message: message.trim_end().to_string(), |
| 87 |
120 |
|
tree_ops: built.tree_ops, |
| 88 |
121 |
|
edits: built.edits, |
| 89 |
|
- |
kind: if built.resolves { ChangeKind::Resolution } else { ChangeKind::Normal }, |
|
122 |
+ |
kind: if built.resolves { |
|
123 |
+ |
ChangeKind::Resolution |
|
124 |
+ |
} else { |
|
125 |
+ |
ChangeKind::Normal |
|
126 |
+ |
}, |
| 90 |
127 |
|
}; |
| 91 |
128 |
|
let mut deps: BTreeSet<ChangeHash> = referenced_changes(&change, &base); |
| 92 |
129 |
|
deps.extend(built.contributors.iter().copied()); |
|
|
… |
4 unchanged lines |
| 97 |
134 |
|
|
| 98 |
135 |
|
/// A reference update of `bookmark` to `heads`, attested by `signer` |
| 99 |
136 |
|
/// when given (the attestation a push would carry, spec §14.4). |
| 100 |
|
- |
pub fn host_ref_update(&self, bookmark: &str, heads: &[ChangeHash], signer: Option<(&KeyId, &SecretKey)>) -> Result<RefUpdate> { |
|
137 |
+ |
pub fn host_ref_update( |
|
138 |
+ |
&self, |
|
139 |
+ |
bookmark: &str, |
|
140 |
+ |
heads: &[ChangeHash], |
|
141 |
+ |
signer: Option<(&KeyId, &SecretKey)>, |
|
142 |
+ |
) -> Result<RefUpdate> { |
| 101 |
143 |
|
let view = self.view()?; |
| 102 |
144 |
|
let empty = self.store.put_state(&StateObject::new(vec![]))?; |
| 103 |
145 |
|
let old_state = view.bookmarks.get(bookmark).copied().unwrap_or(empty); |
|
|
… |
11 unchanged lines |
| 115 |
157 |
|
let attest = Attest { |
| 116 |
158 |
|
repo: self.id()?, |
| 117 |
159 |
|
hlc: Hlc::now(), |
| 118 |
|
- |
body: AttestBody::Ref { name: bookmark.to_string(), old_state, new_state, serial }, |
|
160 |
+ |
body: AttestBody::Ref { |
|
161 |
+ |
name: bookmark.to_string(), |
|
162 |
+ |
old_state, |
|
163 |
+ |
new_state, |
|
164 |
+ |
serial, |
|
165 |
+ |
}, |
| 119 |
166 |
|
}; |
| 120 |
167 |
|
let r = attest.hash(); |
| 121 |
168 |
|
let sig = sk.sign(Domain::Attest, key, &r); |
| 122 |
|
- |
Some(SignedAttest { attest, sigs: vec![sig] }) |
|
169 |
+ |
Some(SignedAttest { |
|
170 |
+ |
attest, |
|
171 |
+ |
sigs: vec![sig], |
|
172 |
+ |
}) |
| 123 |
173 |
|
} |
| 124 |
174 |
|
None => None, |
| 125 |
175 |
|
}; |
| 126 |
|
- |
Ok(RefUpdate { name: bookmark.to_string(), old_state, new_state, force: false, attestation }) |
|
176 |
+ |
Ok(RefUpdate { |
|
177 |
+ |
name: bookmark.to_string(), |
|
178 |
+ |
old_state, |
|
179 |
+ |
new_state, |
|
180 |
+ |
force: false, |
|
181 |
+ |
attestation, |
|
182 |
+ |
}) |
| 127 |
183 |
|
} |
| 128 |
184 |
|
|
| 129 |
185 |
|
/// Apply a host-made update as if it had been pushed: the new change |
| 130 |
186 |
|
/// (with its signatures) if any, the identities it needs, and the |
| 131 |
187 |
|
/// reference update, all through `receive_push_report`, so the same |
| 132 |
188 |
|
/// authentication, compare-and-swap and policy checks apply. |
| 133 |
|
- |
pub fn apply_hosted(&self, change: Option<(Change, Vec<Signature>)>, identities: &[Identity], update: RefUpdate) -> Result<PushReport> { |
|
189 |
+ |
pub fn apply_hosted( |
|
190 |
+ |
&self, |
|
191 |
+ |
change: Option<(Change, Vec<Signature>)>, |
|
192 |
+ |
identities: &[Identity], |
|
193 |
+ |
update: RefUpdate, |
|
194 |
+ |
) -> Result<PushReport> { |
| 134 |
195 |
|
let mut bundle = Bundle::default(); |
| 135 |
196 |
|
for id in identities { |
| 136 |
197 |
|
bundle.objects.push((ObjectType::Identity, id.encode())); |
|
|
… |
31 unchanged lines |
| 168 |
229 |
|
|
| 169 |
230 |
|
/// A user repository pushed to a bare host, and a hub key the user |
| 170 |
231 |
|
/// has delegated to. Returns (host, user, hub key, hub secret). |
| 171 |
|
- |
fn setup(root: &Path, with_policy: bool, delegate: bool) -> (Repo, Repo, KeyId, SecretKey, Identity) { |
|
232 |
+ |
fn setup( |
|
233 |
+ |
root: &Path, |
|
234 |
+ |
with_policy: bool, |
|
235 |
+ |
delegate: bool, |
|
236 |
+ |
) -> (Repo, Repo, KeyId, SecretKey, Identity) { |
| 172 |
237 |
|
hermetic(); |
| 173 |
238 |
|
let host_dir = root.join("host"); |
| 174 |
239 |
|
Repo::init_bare_unbound(&host_dir).unwrap(); |
| 175 |
240 |
|
let work = root.join("work"); |
| 176 |
241 |
|
let mut user = Repo::init(&work).unwrap(); |
| 177 |
|
- |
user.create_identity("Andy", "[email protected]", IdentityScope::Repo).unwrap(); |
|
242 |
+ |
user.create_identity("Andy", "[email protected]", IdentityScope::Repo) |
|
243 |
+ |
.unwrap(); |
| 178 |
244 |
|
user.config.set("core.sign", "change").unwrap(); |
| 179 |
|
- |
user.config.set("remote.origin.url", host_dir.to_str().unwrap()).unwrap(); |
|
245 |
+ |
user.config |
|
246 |
+ |
.set("remote.origin.url", host_dir.to_str().unwrap()) |
|
247 |
+ |
.unwrap(); |
| 180 |
248 |
|
user.save_config().unwrap(); |
| 181 |
249 |
|
let hub_sk = SecretKey::generate(); |
| 182 |
250 |
|
let hub_id = hub_sk.identity("Hub", "hub@example", 1); |
|
|
… |
9 unchanged lines |
| 192 |
260 |
|
user.commit("first", &[]).unwrap(); |
| 193 |
261 |
|
let r = user.push("origin", false).unwrap(); |
| 194 |
262 |
|
assert!(r.iter().all(|x| x.ok), "{r:?}"); |
| 195 |
|
- |
(Repo::open(&host_dir).unwrap(), user, hub_key, hub_sk, hub_id) |
|
263 |
+ |
( |
|
264 |
+ |
Repo::open(&host_dir).unwrap(), |
|
265 |
+ |
user, |
|
266 |
+ |
hub_key, |
|
267 |
+ |
hub_sk, |
|
268 |
+ |
hub_id, |
|
269 |
+ |
) |
| 196 |
270 |
|
} |
| 197 |
271 |
|
|
| 198 |
272 |
|
#[test] |
|
|
… |
2 unchanged lines |
| 201 |
275 |
|
let (host, user, hub_key, hub_sk, hub_id) = setup(dir.path(), false, true); |
| 202 |
276 |
|
let author = user.config.author_key.unwrap(); |
| 203 |
277 |
|
let heads = host.bookmark_heads().unwrap(); |
| 204 |
|
- |
let change = host.build_change(&heads, &[FileEdit { path: "a.txt".into(), content: Some(b"one\ntwo\nthree\n".to_vec()) }, FileEdit { path: "docs/new.md".into(), content: Some(b"# New\n".to_vec()) }], "Edit on the web", author).unwrap(); |
|
278 |
+ |
let change = host |
|
279 |
+ |
.build_change( |
|
280 |
+ |
&heads, |
|
281 |
+ |
&[ |
|
282 |
+ |
FileEdit { |
|
283 |
+ |
path: "a.txt".into(), |
|
284 |
+ |
content: Some(b"one\ntwo\nthree\n".to_vec()), |
|
285 |
+ |
}, |
|
286 |
+ |
FileEdit { |
|
287 |
+ |
path: "docs/new.md".into(), |
|
288 |
+ |
content: Some(b"# New\n".to_vec()), |
|
289 |
+ |
}, |
|
290 |
+ |
], |
|
291 |
+ |
"Edit on the web", |
|
292 |
+ |
author, |
|
293 |
+ |
) |
|
294 |
+ |
.unwrap(); |
| 205 |
295 |
|
assert_eq!(change.author, author); |
| 206 |
296 |
|
assert_eq!(change.kind, ChangeKind::Normal); |
| 207 |
297 |
|
let hash = change.hash(); |
| 208 |
298 |
|
let sig = hub_sk.sign(Domain::Change, &hub_key, &hash); |
| 209 |
299 |
|
let mut new_heads = heads.clone(); |
| 210 |
300 |
|
new_heads.push(hash); |
| 211 |
|
- |
let update = host.host_ref_update("main", &[hash], Some((&hub_key, &hub_sk))).unwrap(); |
|
301 |
+ |
let update = host |
|
302 |
+ |
.host_ref_update("main", &[hash], Some((&hub_key, &hub_sk))) |
|
303 |
+ |
.unwrap(); |
| 212 |
304 |
|
let ids = vec![hub_id, user.identity(&author).unwrap()]; |
| 213 |
|
- |
let report = host.apply_hosted(Some((change, vec![sig])), &ids, update).unwrap(); |
|
305 |
+ |
let report = host |
|
306 |
+ |
.apply_hosted(Some((change, vec![sig])), &ids, update) |
|
307 |
+ |
.unwrap(); |
| 214 |
308 |
|
assert!(report.results.iter().all(|r| r.ok), "{:?}", report.results); |
| 215 |
309 |
|
assert_eq!(host.bookmark_heads().unwrap(), vec![hash]); |
| 216 |
|
- |
assert_eq!(host.file_at("a.txt", Some(&[hash])).unwrap(), b"one\ntwo\nthree\n"); |
| 217 |
|
- |
assert_eq!(host.file_at("docs/new.md", Some(&[hash])).unwrap(), b"# New\n"); |
|
310 |
+ |
assert_eq!( |
|
311 |
+ |
host.file_at("a.txt", Some(&[hash])).unwrap(), |
|
312 |
+ |
b"one\ntwo\nthree\n" |
|
313 |
+ |
); |
|
314 |
+ |
assert_eq!( |
|
315 |
+ |
host.file_at("docs/new.md", Some(&[hash])).unwrap(), |
|
316 |
+ |
b"# New\n" |
|
317 |
+ |
); |
| 218 |
318 |
|
// Authored by the user, authenticated by the hub key as delegate. |
| 219 |
|
- |
assert!(matches!(host.authenticate_change(&hash).unwrap(), SignatureStatus::Valid { key, name, .. } if key == hub_key && name == "Hub")); |
|
319 |
+ |
assert!( |
|
320 |
+ |
matches!(host.authenticate_change(&hash).unwrap(), SignatureStatus::Valid { key, name, .. } if key == hub_key && name == "Hub") |
|
321 |
+ |
); |
| 220 |
322 |
|
let _ = new_heads; |
| 221 |
323 |
|
// Bad paths and empty edits are refused. |
| 222 |
|
- |
assert!(host.build_change(&[hash], &[FileEdit { path: "../x".into(), content: Some(vec![]) }], "m", author).is_err()); |
| 223 |
|
- |
assert!(host.build_change(&[hash], &[FileEdit { path: "a.txt".into(), content: Some(b"one\ntwo\nthree\n".to_vec()) }], "m", author).is_err(), "no-op edits are refused"); |
| 224 |
|
- |
assert!(host.build_change(&[hash], &[FileEdit { path: "a.txt/b".into(), content: Some(vec![]) }], "m", author).is_err(), "a file inside a file"); |
| 225 |
|
- |
let rm = host.build_change(&[hash], &[FileEdit { path: "docs".into(), content: None }], "Remove docs", author).unwrap(); |
|
324 |
+ |
assert!(host |
|
325 |
+ |
.build_change( |
|
326 |
+ |
&[hash], |
|
327 |
+ |
&[FileEdit { |
|
328 |
+ |
path: "../x".into(), |
|
329 |
+ |
content: Some(vec![]) |
|
330 |
+ |
}], |
|
331 |
+ |
"m", |
|
332 |
+ |
author |
|
333 |
+ |
) |
|
334 |
+ |
.is_err()); |
|
335 |
+ |
assert!( |
|
336 |
+ |
host.build_change( |
|
337 |
+ |
&[hash], |
|
338 |
+ |
&[FileEdit { |
|
339 |
+ |
path: "a.txt".into(), |
|
340 |
+ |
content: Some(b"one\ntwo\nthree\n".to_vec()) |
|
341 |
+ |
}], |
|
342 |
+ |
"m", |
|
343 |
+ |
author |
|
344 |
+ |
) |
|
345 |
+ |
.is_err(), |
|
346 |
+ |
"no-op edits are refused" |
|
347 |
+ |
); |
|
348 |
+ |
assert!( |
|
349 |
+ |
host.build_change( |
|
350 |
+ |
&[hash], |
|
351 |
+ |
&[FileEdit { |
|
352 |
+ |
path: "a.txt/b".into(), |
|
353 |
+ |
content: Some(vec![]) |
|
354 |
+ |
}], |
|
355 |
+ |
"m", |
|
356 |
+ |
author |
|
357 |
+ |
) |
|
358 |
+ |
.is_err(), |
|
359 |
+ |
"a file inside a file" |
|
360 |
+ |
); |
|
361 |
+ |
let rm = host |
|
362 |
+ |
.build_change( |
|
363 |
+ |
&[hash], |
|
364 |
+ |
&[FileEdit { |
|
365 |
+ |
path: "docs".into(), |
|
366 |
+ |
content: None, |
|
367 |
+ |
}], |
|
368 |
+ |
"Remove docs", |
|
369 |
+ |
author, |
|
370 |
+ |
) |
|
371 |
+ |
.unwrap(); |
| 226 |
372 |
|
assert!(!rm.tree_ops.is_empty()); |
| 227 |
373 |
|
} |
| 228 |
374 |
|
|
|
|
… |
3 unchanged lines |
| 232 |
378 |
|
let (host, user, hub_key, hub_sk, hub_id) = setup(dir.path(), false, false); |
| 233 |
379 |
|
let author = user.config.author_key.unwrap(); |
| 234 |
380 |
|
let heads = host.bookmark_heads().unwrap(); |
| 235 |
|
- |
let change = host.build_change(&heads, &[FileEdit { path: "a.txt".into(), content: Some(b"x\n".to_vec()) }], "Edit", author).unwrap(); |
|
381 |
+ |
let change = host |
|
382 |
+ |
.build_change( |
|
383 |
+ |
&heads, |
|
384 |
+ |
&[FileEdit { |
|
385 |
+ |
path: "a.txt".into(), |
|
386 |
+ |
content: Some(b"x\n".to_vec()), |
|
387 |
+ |
}], |
|
388 |
+ |
"Edit", |
|
389 |
+ |
author, |
|
390 |
+ |
) |
|
391 |
+ |
.unwrap(); |
| 236 |
392 |
|
let hash = change.hash(); |
| 237 |
393 |
|
let sig = hub_sk.sign(Domain::Change, &hub_key, &hash); |
| 238 |
|
- |
let update = host.host_ref_update("main", &[hash], Some((&hub_key, &hub_sk))).unwrap(); |
| 239 |
|
- |
let report = host.apply_hosted(Some((change, vec![sig])), &[hub_id, user.identity(&author).unwrap()], update).unwrap(); |
|
394 |
+ |
let update = host |
|
395 |
+ |
.host_ref_update("main", &[hash], Some((&hub_key, &hub_sk))) |
|
396 |
+ |
.unwrap(); |
|
397 |
+ |
let report = host |
|
398 |
+ |
.apply_hosted( |
|
399 |
+ |
Some((change, vec![sig])), |
|
400 |
+ |
&[hub_id, user.identity(&author).unwrap()], |
|
401 |
+ |
update, |
|
402 |
+ |
) |
|
403 |
+ |
.unwrap(); |
| 240 |
404 |
|
// Without a policy the host accepts unauthenticated changes, but the |
| 241 |
405 |
|
// signature does not verify as the author's: it stays unauthenticated. |
| 242 |
406 |
|
assert!(report.results.iter().all(|r| r.ok)); |
| 243 |
|
- |
assert!(matches!(host.authenticate_change(&hash).unwrap(), SignatureStatus::Invalid(why) if why.contains("neither the author"))); |
|
407 |
+ |
assert!( |
|
408 |
+ |
matches!(host.authenticate_change(&hash).unwrap(), SignatureStatus::Invalid(why) if why.contains("neither the author")) |
|
409 |
+ |
); |
| 244 |
410 |
|
} |
| 245 |
411 |
|
|
| 246 |
412 |
|
#[test] |
|
|
… |
3 unchanged lines |
| 250 |
416 |
|
assert!(host.current_policy().unwrap().is_some()); |
| 251 |
417 |
|
let author = user.config.author_key.unwrap(); |
| 252 |
418 |
|
let heads = host.bookmark_heads().unwrap(); |
| 253 |
|
- |
let change = host.build_change(&heads, &[FileEdit { path: "a.txt".into(), content: Some(b"one\n".to_vec()) }], "Trim", author).unwrap(); |
|
419 |
+ |
let change = host |
|
420 |
+ |
.build_change( |
|
421 |
+ |
&heads, |
|
422 |
+ |
&[FileEdit { |
|
423 |
+ |
path: "a.txt".into(), |
|
424 |
+ |
content: Some(b"one\n".to_vec()), |
|
425 |
+ |
}], |
|
426 |
+ |
"Trim", |
|
427 |
+ |
author, |
|
428 |
+ |
) |
|
429 |
+ |
.unwrap(); |
| 254 |
430 |
|
let hash = change.hash(); |
| 255 |
431 |
|
let sig = hub_sk.sign(Domain::Change, &hub_key, &hash); |
| 256 |
432 |
|
let ids = vec![hub_id.clone(), user.identity(&author).unwrap()]; |
| 257 |
|
- |
let update = host.host_ref_update("main", &[hash], Some((&hub_key, &hub_sk))).unwrap(); |
| 258 |
|
- |
let report = host.apply_hosted(Some((change, vec![sig])), &ids, update).unwrap(); |
|
433 |
+ |
let update = host |
|
434 |
+ |
.host_ref_update("main", &[hash], Some((&hub_key, &hub_sk))) |
|
435 |
+ |
.unwrap(); |
|
436 |
+ |
let report = host |
|
437 |
+ |
.apply_hosted(Some((change, vec![sig])), &ids, update) |
|
438 |
+ |
.unwrap(); |
| 259 |
439 |
|
assert!(report.results.iter().all(|r| r.ok), "{:?}", report.results); |
| 260 |
440 |
|
assert_eq!(host.bookmark_heads().unwrap(), vec![hash]); |
| 261 |
441 |
|
|
|
|
… |
1 unchanged lines |
| 263 |
443 |
|
let stranger = SecretKey::generate(); |
| 264 |
444 |
|
let sid = stranger.identity("Mallory", "m@example", 1); |
| 265 |
445 |
|
let skey = sid.hash(); |
| 266 |
|
- |
let change2 = host.build_change(&[hash], &[FileEdit { path: "a.txt".into(), content: Some(b"evil\n".to_vec()) }], "Evil", author).unwrap(); |
|
446 |
+ |
let change2 = host |
|
447 |
+ |
.build_change( |
|
448 |
+ |
&[hash], |
|
449 |
+ |
&[FileEdit { |
|
450 |
+ |
path: "a.txt".into(), |
|
451 |
+ |
content: Some(b"evil\n".to_vec()), |
|
452 |
+ |
}], |
|
453 |
+ |
"Evil", |
|
454 |
+ |
author, |
|
455 |
+ |
) |
|
456 |
+ |
.unwrap(); |
| 267 |
457 |
|
let h2 = change2.hash(); |
| 268 |
458 |
|
let sig2 = stranger.sign(Domain::Change, &skey, &h2); |
| 269 |
|
- |
let update = host.host_ref_update("main", &[h2], Some((&skey, &stranger))).unwrap(); |
| 270 |
|
- |
let report = host.apply_hosted(Some((change2, vec![sig2])), &[sid], update).unwrap(); |
|
459 |
+ |
let update = host |
|
460 |
+ |
.host_ref_update("main", &[h2], Some((&skey, &stranger))) |
|
461 |
+ |
.unwrap(); |
|
462 |
+ |
let report = host |
|
463 |
+ |
.apply_hosted(Some((change2, vec![sig2])), &[sid], update) |
|
464 |
+ |
.unwrap(); |
| 271 |
465 |
|
assert!(report.results.iter().all(|r| !r.ok), "{:?}", report.results); |
| 272 |
|
- |
assert_eq!(host.bookmark_heads().unwrap(), vec![hash], "the reference did not move"); |
|
466 |
+ |
assert_eq!( |
|
467 |
+ |
host.bookmark_heads().unwrap(), |
|
468 |
+ |
vec![hash], |
|
469 |
+ |
"the reference did not move" |
|
470 |
+ |
); |
| 273 |
471 |
|
// An unsigned update is refused under a policy too. |
| 274 |
472 |
|
let update = host.host_ref_update("main", &[hash], None).unwrap(); |
| 275 |
473 |
|
let report = host.apply_hosted(None, &[], update).unwrap(); |
|
|
… |
7 unchanged lines |
| 283 |
481 |
|
let author = user.config.author_key.unwrap(); |
| 284 |
482 |
|
let base = host.bookmark_heads().unwrap(); |
| 285 |
483 |
|
// Two independent edits of the same line: a conflict in the union. |
| 286 |
|
- |
let a = host.build_change(&base, &[FileEdit { path: "a.txt".into(), content: Some(b"ONE\ntwo\n".to_vec()) }], "A", author).unwrap(); |
| 287 |
|
- |
let b = host.build_change(&base, &[FileEdit { path: "a.txt".into(), content: Some(b"uno\ntwo\n".to_vec()) }], "B", author).unwrap(); |
|
484 |
+ |
let a = host |
|
485 |
+ |
.build_change( |
|
486 |
+ |
&base, |
|
487 |
+ |
&[FileEdit { |
|
488 |
+ |
path: "a.txt".into(), |
|
489 |
+ |
content: Some(b"ONE\ntwo\n".to_vec()), |
|
490 |
+ |
}], |
|
491 |
+ |
"A", |
|
492 |
+ |
author, |
|
493 |
+ |
) |
|
494 |
+ |
.unwrap(); |
|
495 |
+ |
let b = host |
|
496 |
+ |
.build_change( |
|
497 |
+ |
&base, |
|
498 |
+ |
&[FileEdit { |
|
499 |
+ |
path: "a.txt".into(), |
|
500 |
+ |
content: Some(b"uno\ntwo\n".to_vec()), |
|
501 |
+ |
}], |
|
502 |
+ |
"B", |
|
503 |
+ |
author, |
|
504 |
+ |
) |
|
505 |
+ |
.unwrap(); |
| 288 |
506 |
|
let (ha, hb) = (a.hash(), b.hash()); |
| 289 |
507 |
|
let ids = vec![hub_id.clone(), user.identity(&author).unwrap()]; |
| 290 |
508 |
|
for (c, h) in [(a, ha), (b, hb)] { |
|
|
… |
1 unchanged lines |
| 292 |
510 |
|
let mut heads = host.bookmark_heads().unwrap(); |
| 293 |
511 |
|
heads.retain(|x| !base.contains(x)); |
| 294 |
512 |
|
heads.push(h); |
| 295 |
|
- |
let update = host.host_ref_update("main", &heads, Some((&hub_key, &hub_sk))).unwrap(); |
| 296 |
|
- |
let r = host.apply_hosted(Some((c, vec![sig])), &ids, update).unwrap(); |
|
513 |
+ |
let update = host |
|
514 |
+ |
.host_ref_update("main", &heads, Some((&hub_key, &hub_sk))) |
|
515 |
+ |
.unwrap(); |
|
516 |
+ |
let r = host |
|
517 |
+ |
.apply_hosted(Some((c, vec![sig])), &ids, update) |
|
518 |
+ |
.unwrap(); |
| 297 |
519 |
|
assert!(r.results.iter().all(|x| x.ok), "{:?}", r.results); |
| 298 |
520 |
|
} |
| 299 |
521 |
|
let heads = host.bookmark_heads().unwrap(); |
|
|
… |
1 unchanged lines |
| 301 |
523 |
|
let marked = String::from_utf8(host.file_at("a.txt", Some(&heads)).unwrap()).unwrap(); |
| 302 |
524 |
|
assert!(marked.contains("<<<<<<< oxy conflict"), "{marked}"); |
| 303 |
525 |
|
// Saving with markers left in is refused; saving without them resolves. |
| 304 |
|
- |
assert!(host.build_change(&heads, &[FileEdit { path: "a.txt".into(), content: Some(format!("{marked}x\n").into_bytes()) }], "half", author).is_err()); |
| 305 |
|
- |
let res = host.build_change(&heads, &[FileEdit { path: "a.txt".into(), content: Some(b"One\ntwo\n".to_vec()) }], "Resolve", author).unwrap(); |
|
526 |
+ |
assert!(host |
|
527 |
+ |
.build_change( |
|
528 |
+ |
&heads, |
|
529 |
+ |
&[FileEdit { |
|
530 |
+ |
path: "a.txt".into(), |
|
531 |
+ |
content: Some(format!("{marked}x\n").into_bytes()) |
|
532 |
+ |
}], |
|
533 |
+ |
"half", |
|
534 |
+ |
author |
|
535 |
+ |
) |
|
536 |
+ |
.is_err()); |
|
537 |
+ |
let res = host |
|
538 |
+ |
.build_change( |
|
539 |
+ |
&heads, |
|
540 |
+ |
&[FileEdit { |
|
541 |
+ |
path: "a.txt".into(), |
|
542 |
+ |
content: Some(b"One\ntwo\n".to_vec()), |
|
543 |
+ |
}], |
|
544 |
+ |
"Resolve", |
|
545 |
+ |
author, |
|
546 |
+ |
) |
|
547 |
+ |
.unwrap(); |
| 306 |
548 |
|
assert_eq!(res.kind, ChangeKind::Resolution); |
| 307 |
|
- |
assert!(res.deps.contains(&ha) && res.deps.contains(&hb), "a resolution depends on both sides"); |
|
549 |
+ |
assert!( |
|
550 |
+ |
res.deps.contains(&ha) && res.deps.contains(&hb), |
|
551 |
+ |
"a resolution depends on both sides" |
|
552 |
+ |
); |
| 308 |
553 |
|
let hr = res.hash(); |
| 309 |
554 |
|
let sig = hub_sk.sign(Domain::Change, &hub_key, &hr); |
| 310 |
|
- |
let update = host.host_ref_update("main", &[hr], Some((&hub_key, &hub_sk))).unwrap(); |
| 311 |
|
- |
let r = host.apply_hosted(Some((res, vec![sig])), &ids, update).unwrap(); |
|
555 |
+ |
let update = host |
|
556 |
+ |
.host_ref_update("main", &[hr], Some((&hub_key, &hub_sk))) |
|
557 |
+ |
.unwrap(); |
|
558 |
+ |
let r = host |
|
559 |
+ |
.apply_hosted(Some((res, vec![sig])), &ids, update) |
|
560 |
+ |
.unwrap(); |
| 312 |
561 |
|
assert!(r.results.iter().all(|x| x.ok), "{:?}", r.results); |
| 313 |
562 |
|
assert_eq!(host.file_at("a.txt", Some(&[hr])).unwrap(), b"One\ntwo\n"); |
| 314 |
|
- |
assert!(!host.load_state(&[hr]).unwrap().materialize().is_conflicted()); |
|
563 |
+ |
assert!(!host |
|
564 |
+ |
.load_state(&[hr]) |
|
565 |
+ |
.unwrap() |
|
566 |
+ |
.materialize() |
|
567 |
+ |
.is_conflicted()); |
| 315 |
568 |
|
let _ = &mut user; |
| 316 |
569 |
|
} |
| 317 |
570 |
|
} |