Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/divergence.rs

16.6 KiB, 33 runs

created by r2848102244:27, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Two writers, two branches, one line: what the tool says about it.
2//!
3//! A second replica is awkward to conjure in version zero, there being no sync
4//! verb yet to bring one replica's operations to another. The importer supplies
5//! one for nothing: each git author becomes a replica of their own, so a
6//! repository two people committed to imports as a history two replicas wrote.
7//! That is what these tests use.
8
9mod support;
10
11use support::{
12 git,
13 git_as,
14 git_init,
15 marks,
16 ore,
17 write,
18 Scratch,
19};
20
21use oxedyne_fe2o3_core::prelude::*;
22
23use std::collections::BTreeSet;
24use std::path::Path;
25
26
27/// Builds a repository whose two branches were written by two people and never
28/// merged, both of them editing the same line of one file.
29fn diverge(src: &Path)
30 -> Outcome<()>
31{
32 res!(git_init(src));
33 res!(write(src, "f.txt", b"alpha\nbeta\ngamma\n"));
34 res!(git(src, &["add", "."]));
35 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "base"]));
36
37 res!(write(src, "f.txt", b"alpha\nBETA by Ada\ngamma\n"));
38 res!(git(src, &["add", "."]));
39 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "Ada edits the middle"]));
40
41 res!(git(src, &["checkout", "--quiet", "-b", "side", "HEAD~1"]));
42 res!(write(src, "f.txt", b"alpha\nBETA by Grace\ngamma\n"));
43 res!(git_as(src, "Grace Hopper", "grace@example.org",
44 &["add", "."]));
45 res!(git_as(src, "Grace Hopper", "grace@example.org",
46 &["commit", "--quiet", "--no-verify", "-m", "Grace edits the middle"]));
47 res!(git(src, &["checkout", "--quiet", "main"]));
48 Ok(())
49}
50
51/// Returns the replicas the runs of a `who` listing name.
52fn replicas_in(text: &str) -> BTreeSet<String> {
53 let mut out = BTreeSet::new();
54 for line in text.lines() {
55 let trimmed = line.trim_start();
56 // A run line begins with its byte range, so the replica is the field that
57 // follows it.
58 if !trimmed.starts_with(|c: char| c.is_ascii_digit()) {
59 continue;
60 }
61 for field in trimmed.split_whitespace() {
62 if field.starts_with('r') && field.contains(':') {
63 if let Some(name) = field.split(':').next() {
64 out.insert(fmt!("{}", name));
65 }
66 }
67 }
68 }
69 out
70}
71
72/// Two branches that touched the same line are concurrent, and `flags` says so.
73#[test]
74fn divergent_branches_raise_an_overlap() -> Outcome<()> {
75 let scratch = res!(Scratch::new("diverge"));
76 let src = res!(scratch.sub("src"));
77 let into = res!(scratch.sub("ore"));
78 res!(diverge(&src));
79 res!(res!(ore(&into, &["init"])).good("init"));
80 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
81 let text = fmt!("{}", res!(out.good("import")));
82 assert!(text.contains("2 authors"), "two people wrote it: {}", text);
83
84 let out = res!(ore(&into, &["flags"]));
85 let text = fmt!("{}", res!(out.good("flags")));
86 assert!(text.contains("overlap"),
87 "the two edits were concurrent and neither author could see the other: {}",
88 text);
89 assert!(text.contains("f.txt"), "and it says which file: {}", text);
90 assert!(!text.contains("clean"), "which is not a clean history: {}", text);
91 Ok(())
92}
93
94/// `who` attributes the bytes of a file to the replicas whose work is in it, and
95/// the arbitration decides whose that is.
96///
97/// Both writers replaced the same line, so they are one overlap group: the member
98/// highest in op order prevails and the other yields, its insertion buried whole.
99/// The file therefore shows one writer's bytes over the contended line, not two,
100/// and `who` says so rather than attributing half a line to each. The writer who
101/// yielded is not silently gone -- `flags` names them -- and that is the division
102/// of labour the rule was adopted for: the file is one author's, and the other's
103/// is one flag away.
104#[test]
105fn who_attributes_only_the_bytes_that_prevailed() -> Outcome<()> {
106 let scratch = res!(Scratch::new("who_two"));
107 let src = res!(scratch.sub("src"));
108 let into = res!(scratch.sub("ore"));
109 res!(diverge(&src));
110 res!(res!(ore(&into, &["init"])).good("init"));
111 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
112
113 let out = res!(ore(&into, &["who", "f.txt"]));
114 let text = fmt!("{}", res!(out.good("who f.txt")));
115 let replicas = replicas_in(&text);
116 assert_eq!(replicas.len(), 1,
117 "the contended line reads as one writer, whole, not {:?}: {}", replicas, text);
118 assert!(text.contains("yielded"),
119 "and the writer who did not prevail is named rather than dropped: {}", text);
120
121 // An import authors operations here, on those writers' behalf, and seals
122 // them with this repository's key: the operations carry the git authors'
123 // replicas and the signature says who actually vouches for the bytes, which
124 // is whoever ran the import.
125 for run in support::runs_in(&text) {
126 assert_eq!(run.mark, '+',
127 "an imported operation is signed by the importer: {}", text);
128 }
129 Ok(())
130}
131
132/// The whole history is written into the working copy, not one branch of it, and
133/// a branch is reached by the mark that stands for its commit.
134///
135/// Ore has no branches to point at: the state is the operation set, and the
136/// render of the whole set is what the history says the working copy is. Writing
137/// out one branch instead would leave the very next capture asserting that branch
138/// as the truth, which is one splice's worth of noise when two branches share a
139/// file and a file deletion when they do not.
140#[test]
141fn the_working_copy_is_the_whole_history() -> Outcome<()> {
142 let scratch = res!(Scratch::new("head_only"));
143 let src = res!(scratch.sub("src"));
144 let into = res!(scratch.sub("ore"));
145 res!(diverge(&src));
146 res!(res!(ore(&into, &["init"])).good("init"));
147 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
148
149 // The file is the arbitrated merge of the two edits. Two writers who
150 // concurrently replaced one line are one overlap group; the member highest in
151 // op order prevails and the other yields, its removals not burying and its
152 // insertion buried whole, so the line reads as one of them wrote it. What is
153 // being asserted here is not which of them: it is that the whole history was
154 // written out -- the lines neither of them touched are present, and the state
155 // on disk is a state one of them actually authored rather than a mixture no
156 // compiler would take.
157 let bytes = res!(std::fs::read(into.join("f.txt")));
158 let text = fmt!("{}", String::from_utf8_lossy(&bytes));
159 assert!(text == "alpha\nBETA by Ada\ngamma\n" || text == "alpha\nBETA by Grace\ngamma\n",
160 "the contended line is one writer's, whole: {:?}", text);
161 assert!(text.starts_with("alpha\n") && text.ends_with("gamma\n"),
162 "and the lines neither of them touched: {:?}", text);
163
164 // The version that is not on disk is in the log and flagged, not discarded,
165 // and the spill puts it beside the working copy for a reader with no forge.
166 let out = res!(ore(&into, &["flags"]));
167 let said = fmt!("{}", res!(out.good("flags")));
168 assert!(said.contains("yielded"),
169 "the buried version is named rather than dropped: {}", said);
170 let spilled = res!(std::fs::read_dir(into.join(".ore").join("collisions")));
171 let mut buried: Vec<Vec<u8>> = Vec::new();
172 for entry in spilled {
173 buried.push(res!(std::fs::read(res!(entry).path())));
174 }
175 assert_eq!(buried.len(), 1, "one version yielded, so one is spilled");
176 let held = fmt!("{}", String::from_utf8_lossy(&buried[0]));
177 assert!(held.contains("BETA by "),
178 "and the spill holds the line its author wrote: {:?}", held);
179 assert!(!text.contains(held.trim_end_matches('\n')),
180 "which is the version the file does not hold: {:?} against {:?}", held, text);
181
182 // Both branches are in the history: three commits, three marks. The marks the
183 // import named are counted apart from the one the tool wrote when the command
184 // finished, because the claim is about commits and that one stands for no
185 // commit. `--auto` because the plain listing shows only the named ones, which
186 // is the point of it.
187 let out = res!(ore(&into, &["log", "--auto"]));
188 let text = fmt!("{}", res!(out.good("log")));
189 let (named, automatic) = marks(&text);
190 assert_eq!(named, 3, "every commit of every branch is a mark: {}", text);
191 assert_eq!(automatic, 1, "and the import ends at one point of its own: {}", text);
192 assert!(text.contains("captured nothing"),
193 "the import wrote what the history says, so there is nothing to capture: {}",
194 text);
195
196 // One branch's state is reached by the mark that stands for its commit.
197 res!(res!(ore(&into, &["back", "Ada edits the middle"])).good("back"));
198 let bytes = res!(std::fs::read(into.join("f.txt")));
199 assert_eq!(bytes, b"alpha\nBETA by Ada\ngamma\n".to_vec(),
200 "a mark names the state its commit left behind");
201 Ok(())
202}
203
204/// Builds a repository whose two branches each *created* one path, and were
205/// never merged: two files at one name, which is the state a path cannot
206/// describe and an identity can.
207fn clash(src: &Path)
208 -> Outcome<()>
209{
210 res!(git_init(src));
211 res!(write(src, "a.txt", b"the file they agree about\n"));
212 res!(git(src, &["add", "."]));
213 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "base"]));
214 let base = res!(git(src, &["rev-parse", "HEAD"]));
215 let base = fmt!("{}", String::from_utf8_lossy(&base).trim());
216
217 res!(write(src, "notes.md", MAIN_NOTES));
218 res!(git(src, &["add", "."]));
219 res!(git(src, &["commit", "--quiet", "--no-verify", "-m", "Ada starts the notes"]));
220
221 res!(git(src, &["checkout", "--quiet", "-b", "side", &base]));
222 res!(write(src, "notes.md", SIDE_NOTES));
223 res!(git_as(src, "Grace Hopper", "grace@example.org", &["add", "."]));
224 res!(git_as(src, "Grace Hopper", "grace@example.org",
225 &["commit", "--quiet", "--no-verify", "-m", "Grace starts the notes"]));
226 res!(git(src, &["checkout", "--quiet", "main"]));
227 Ok(())
228}
229
230/// The notes one branch wrote.
231const MAIN_NOTES: &[u8] = b"notes from the trunk\n";
232/// The notes the other branch wrote, at the same path and knowing nothing of it.
233const SIDE_NOTES: &[u8] = b"notes from the side\n";
234
235/// Two branches that each created one path are two files, and both of them reach
236/// the working copy: one under the name, one under a derived name.
237///
238/// A path is metadata and a file is an identity, so this is not a conflict to be
239/// resolved; it is a state the repository is genuinely in. What the tool owes the
240/// reader is a deterministic name for the second file and a plain sentence saying
241/// what it did.
242#[test]
243fn two_files_at_one_path_both_reach_the_working_copy() -> Outcome<()> {
244 let scratch = res!(Scratch::new("clash"));
245 let src = res!(scratch.sub("src"));
246 let into = res!(scratch.sub("ore"));
247 res!(clash(&src));
248 res!(res!(ore(&into, &["init"])).good("init"));
249 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
250 let text = fmt!("{}", res!(out.good("import")));
251 assert!(text.contains("clash at notes.md"),
252 "the import says what it did about the clash: {}", text);
253
254 // Both files are on disk: one at the path, one at the derived name.
255 let now = res!(std::fs::read_dir(&into));
256 let mut derived: Vec<String> = Vec::new();
257 for entry in now {
258 let name = res!(entry).file_name().to_string_lossy().into_owned();
259 if name.starts_with("notes.md.clash-") {
260 derived.push(name);
261 }
262 }
263 assert_eq!(derived.len(), 1, "one file lost the name: {:?}", derived);
264 let kept = res!(std::fs::read(into.join("notes.md")));
265 let moved = res!(std::fs::read(into.join(&derived[0])));
266 let mut both = vec![kept.clone(), moved.clone()];
267 both.sort();
268 let mut want = vec![MAIN_NOTES.to_vec(), SIDE_NOTES.to_vec()];
269 want.sort();
270 assert_eq!(both, want, "both branches' notes are there, and nothing else is");
271
272 // The derived name names its file's identity, so it is the same name every
273 // time and on every replica.
274 let tail = match derived[0].strip_prefix("notes.md.clash-r") {
275 Some(t) => t,
276 None => return Err(err!(
277 "The derived name {:?} does not name a replica.", derived[0]; Test)),
278 };
279 let mut parts = tail.split('-');
280 let replica = parts.next().unwrap_or("");
281 let counter = parts.next().unwrap_or("");
282 assert!(!replica.is_empty() && replica.chars().all(|c| c.is_ascii_digit()),
283 "the derived name carries a replica: {:?}", derived[0]);
284 assert!(!counter.is_empty() && counter.chars().all(|c| c.is_ascii_digit()),
285 "and the counter of the operation that created the file: {:?}", derived[0]);
286
287 // The clash is reported, and it is not something the renderer flagged: two
288 // files at one path is a fact about the repository, not a fault in it.
289 let out = res!(ore(&into, &["flags"]));
290 let text = fmt!("{}", res!(out.good("flags")));
291 assert!(text.contains("captured nothing"),
292 "the working copy already holds both files: {}", text);
293 assert!(text.contains("clash at notes.md: 2 files hold that path"),
294 "`ore flags` says what happened: {}", text);
295 assert!(text.contains("keeps the name, being highest in op order"),
296 "and how it was decided: {}", text);
297 assert!(text.contains("is written as notes.md.clash-r"),
298 "and where the other went: {}", text);
299 assert!(text.contains("1 path claimed by more than one"),
300 "a clash is not a flag, and the summary does not call the tree clean: {}", text);
301 assert!(!text.contains("clean:"), "which is what `clean` would have said: {}", text);
302
303 // The file under the derived name is accounted for under that name, since
304 // that is the name a person has in front of them.
305 let out = res!(ore(&into, &["who", &derived[0]]));
306 let text = fmt!("{}", res!(out.good("who")));
307 assert!(text.contains("recorded at notes.md"),
308 "`ore who` says which path the file really holds: {}", text);
309
310 // And the history is untouched by any of it: three commits, three marks,
311 // beside the one the import's own command wrote at the end.
312 let out = res!(ore(&into, &["log", "--auto"]));
313 let text = fmt!("{}", res!(out.good("log")));
314 let (named, automatic) = marks(&text);
315 assert_eq!(named, 3, "every commit is a mark: {}", text);
316 assert_eq!(automatic, 1, "and the import ends at one point of its own: {}", text);
317 Ok(())
318}
319
320
321/// One person writing under several identity lines imports as one replica, when
322/// the configuration says they are one person.
323///
324/// A replica identifier is derived from the git identity line, so a name that
325/// changed or an address that lapsed imports as two authors who never met. That
326/// cannot be corrected afterwards: the identifier is part of every operation
327/// identifier the author writes, and the header carrying it is covered by the
328/// signature. The mapping in `.ore/config` is the only chance to say so, and the
329/// import consults it before deriving anything.
330#[test]
331fn several_identity_lines_import_as_one_author_when_the_map_says_so() -> Outcome<()> {
332 let scratch = res!(Scratch::new("diverge_authors"));
333 let src = res!(scratch.sub("src"));
334 let into = res!(scratch.sub("ore"));
335
336 // One person, two identity lines, committing in turn.
337 res!(git_init(&src));
338 res!(write(&src, "f.txt", b"one\n"));
339 res!(git(&src, &["add", "."]));
340 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "first"]));
341 res!(write(&src, "f.txt", b"one\ntwo\n"));
342 res!(git_as(&src, "Ada L", "ada@old.example", &["add", "."]));
343 res!(git_as(&src, "Ada L", "ada@old.example",
344 &["commit", "--quiet", "--no-verify", "-m", "second"]));
345
346 res!(res!(ore(&into, &["init"])).good("init"));
347
348 // Say, before importing, that the two lines are one person. The identity line
349 // is exactly what git writes: `name <email>`.
350 let cfg = into.join(".ore").join("config");
351 let text = fmt!("{}", String::from_utf8_lossy(&res!(std::fs::read(&cfg))));
352 let ran = res!(ore(&into, &["log"]));
353 let named = fmt!("{}", res!(ran.good("log")));
354 let mut mine: Option<String> = None;
355 for field in named.split_whitespace() {
356 if let Some(rest) = field.strip_prefix('r') {
357 if rest.chars().all(|c| c.is_ascii_digit()) && !rest.is_empty() {
358 mine = Some(fmt!("{}", rest));
359 break;
360 }
361 }
362 }
363 let mine = res!(mine.ok_or_else(|| err!(
364 "`ore log` named no replica for the repository just made: {}", named;
365 Test, Missing)));
366 // Every identity line the history carries, including the one the harness's
367 // own git configuration writes. An unmapped line is derived, which is the
368 // behaviour being distinguished from.
369 let map = fmt!(
370 "\"authors\": {{\n \"Ada L <ada@old.example>\": (u64|{}),\n \
371 \"Ada Lovelace <ada@example.org>\": (u64|{}),\n \
372 \"Test User <test@example.org>\": (u64|{}),\n }},", mine, mine, mine);
373 let text = text.replacen("\"authors\": {},", &map, 1);
374 res!(std::fs::write(&cfg, text.as_bytes()));
375
376 let out = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
377 let said = fmt!("{}", res!(out.good("import")));
378
379 // Both commits are one author's, so the import says one and `who` names one.
380 assert!(said.contains("1 author"),
381 "two identity lines mapped to one replica import as one author: {}", said);
382 let out = res!(ore(&into, &["who", "f.txt"]));
383 let text = fmt!("{}", res!(out.good("who")));
384 let named = replicas_in(&text);
385 assert_eq!(named.len(), 1,
386 "the file's bytes are one person's, and `who` names {:?}: {}", named, text);
387 Ok(())
388}