Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/export.rs

36.7 KiB, 11 runs

created by r2848102244:130, 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//! The export oracle: git is the judge of what the mirror says.
2//!
3//! The import oracle beside this one proves that an imported repository is the
4//! repository that was imported. This is its counterpart, and it closes the
5//! loop: a git repository is imported, mirrored back out, and the mirror's own
6//! checkout is compared byte for byte and mode for mode with two independent
7//! answers -- what `ore` wrote into its working copy, and what `git checkout` of
8//! the original produces. A history that never touched git is mirrored too,
9//! since the bridge has to work for a repository that was born in Ore.
10//!
11//! It is not ignored. Git is on this machine, and a test of the bridge that does
12//! not run is a test of nothing.
13
14mod support;
15
16use support::{
17 exec_bits_of,
18 exec_bits_raw,
19 git,
20 git_as,
21 git_fixture,
22 git_init,
23 git_when,
24 ore,
25 tree_raw,
26 write,
27 write_raw,
28 Scratch,
29 BINARY,
30};
31
32use oxedyne_fe2o3_core::prelude::*;
33
34use std::collections::{
35 BTreeMap,
36 BTreeSet,
37};
38use std::path::{
39 Path,
40 PathBuf,
41};
42
43
44/// Runs git and returns its output as trimmed text.
45fn said(dir: &Path, args: &[&str])
46 -> Outcome<String>
47{
48 let out = res!(git(dir, args));
49 Ok(fmt!("{}", String::from_utf8_lossy(&out).trim()))
50}
51
52/// Clones a bare mirror and checks a reference out, returning the working tree.
53fn checkout(scratch: &Scratch, mirror: &Path, refname: &str, what: &str)
54 -> Outcome<std::path::PathBuf>
55{
56 let work = res!(scratch.sub(what));
57 res!(git(&work, &["clone", "--quiet", "--no-checkout",
58 &fmt!("{}", mirror.display()), "."]));
59 res!(git(&work, &["checkout", "--quiet", "--force", refname]));
60 Ok(work)
61}
62
63/// Compares two trees by path, bytes and executable bit, saying which file
64/// differs and how.
65fn identical(mine: &Path, theirs: &Path, what: &str)
66 -> Outcome<()>
67{
68 let ours = res!(tree_raw(mine));
69 let yours = res!(tree_raw(theirs));
70 let a: Vec<String> = ours.keys()
71 .map(|p| String::from_utf8_lossy(p).into_owned()).collect();
72 let b: Vec<String> = yours.keys()
73 .map(|p| String::from_utf8_lossy(p).into_owned()).collect();
74 if a != b {
75 return Err(err!(
76 "{}: {:?} holds {:?} where {:?} holds {:?}.",
77 what, mine, a, theirs, b;
78 Test, Mismatch));
79 }
80 for (path, want) in &yours {
81 let shown = String::from_utf8_lossy(path).into_owned();
82 let got = match ours.get(path) {
83 Some(g) => g,
84 None => return Err(err!("{}: {:?} has no {:?}.", what, mine, shown;
85 Test, Missing)),
86 };
87 if got != want {
88 return Err(err!(
89 "{}: {:?} is {} bytes in one and {} in the other.",
90 what, shown, got.len(), want.len();
91 Test, Mismatch));
92 }
93 }
94 let ours = res!(exec_bits_raw(mine));
95 let yours = res!(exec_bits_raw(theirs));
96 if ours != yours {
97 let shown = |m: &BTreeMap<Vec<u8>, bool>| -> Vec<String> {
98 m.iter()
99 .map(|(p, x)| fmt!("{}={}", String::from_utf8_lossy(p), x))
100 .collect()
101 };
102 return Err(err!(
103 "{}: the executable bits differ, {:?} against {:?}.",
104 what, shown(&ours), shown(&yours);
105 Test, Mismatch));
106 }
107 Ok(())
108}
109
110
111/// A git repository imported and mirrored back out is the repository that was
112/// imported.
113///
114/// Three answers are compared and all three have to agree: git's own checkout of
115/// the original, `ore`'s working copy, and a checkout of the mirror. Every
116/// commit is checked and not only the head, and every tree is checked by object
117/// name as well as by content -- git's tree names are hashes of content alone,
118/// so a mirror's tree object is literally the original's where the content is
119/// the same.
120#[test]
121fn a_mirror_is_the_repository_that_was_imported() -> Outcome<()> {
122 let scratch = res!(Scratch::new("export"));
123 let src = res!(scratch.sub("src"));
124 let into = res!(scratch.sub("ore"));
125 let mirror = scratch.path.join("mirror.git");
126
127 let commits = res!(git_fixture(&src));
128 res!(res!(ore(&into, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
129 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
130
131 // git checks the original out for itself, and that is the answer to match.
132 let check = res!(scratch.sub("check"));
133 res!(git(&check, &["clone", "--quiet", &fmt!("{}", src.display()), "."]));
134 res!(git(&check, &["checkout", "--quiet", "main"]));
135
136 let from_mirror = res!(checkout(&scratch, &mirror, "main", "mirrored"));
137 res!(identical(&from_mirror, &into, "the mirror against the working copy"));
138 res!(identical(&from_mirror, &check, "the mirror against git's own checkout"));
139
140 // A script is a script, named outright so the comparisons above cannot pass
141 // by both sides being wrong in the same way.
142 let bits = res!(exec_bits_of(&from_mirror));
143 assert_eq!(bits.get("tools/build.sh"), Some(&true),
144 "the mirror checks the script out executable");
145 assert_eq!(bits.get("later.sh"), Some(&false),
146 "a bit given and then taken away is taken away");
147
148 // One commit per mark, and one mark per commit of the original.
149 let mirrored: usize = match res!(said(&mirror, &["rev-list", "--all", "--count"]))
150 .parse()
151 {
152 Ok(n) => n,
153 Err(e) => return Err(err!(e, "git counted the mirror's commits oddly."; Test)),
154 };
155 assert_eq!(mirrored, commits,
156 "the mirror holds one commit per commit of the original");
157
158 // Every commit's tree, not only the head's. A tag names each commit by the
159 // first line of the message it came in as, which is what the mark is called.
160 for name in ["base", "trunk edit and rename", "side edit", "merge side",
161 "drop the deep file"]
162 {
163 let tag = name.replace(' ', "-");
164 let ours = res!(said(&mirror, &["rev-parse", &fmt!("refs/tags/{}^{{tree}}", tag)]));
165 let theirs = res!(said(&src, &["rev-parse",
166 &fmt!("{}^{{tree}}", res!(commit_titled(&src, name)))]));
167 assert_eq!(ours, theirs,
168 "the tree at {:?} is the original's tree object, not merely its content",
169 name);
170 }
171
172 // The merge is a merge, and its parents are the two lines of work.
173 let parents = res!(said(&mirror, &["rev-list", "--parents", "-n", "1",
174 "refs/tags/merge-side"]));
175 assert_eq!(parents.split(' ').count(), 3,
176 "a mark that joined two lines of work is a git merge commit: {}", parents);
177 Ok(())
178}
179
180/// Returns the object name of the commit whose message begins with `title`.
181fn commit_titled(dir: &Path, title: &str)
182 -> Outcome<String>
183{
184 let out = res!(said(dir, &["log", "--all", "--format=%H %s"]));
185 for line in out.lines() {
186 if let Some((id, subject)) = line.split_once(' ') {
187 if subject == title {
188 return Ok(fmt!("{}", id));
189 }
190 }
191 }
192 Err(err!("No commit of {:?} is titled {:?}.", dir, title; Test, NotFound))
193}
194
195/// A history that never touched git is mirrored just the same.
196#[test]
197fn a_native_history_is_mirrored() -> Outcome<()> {
198 let scratch = res!(Scratch::new("export_native"));
199 let repo = res!(scratch.sub("repo"));
200 let mirror = scratch.path.join("mirror.git");
201 res!(res!(ore(&repo, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
202
203 res!(write(&repo, "notes.md", b"first thought\n"));
204 res!(write(&repo, "deep/down/here.txt", b"buried\n"));
205 res!(write(&repo, "bin.dat", BINARY));
206 // A path no text encoding will read, and one holding a space.
207 res!(write_raw(&repo, &[0xff, 0xfe, b'.', b'b'], b"not utf8\n"));
208 res!(write(&repo, "a space.txt", b"spaced\n"));
209 res!(res!(ore(&repo, &["mark", "opening"])).good("mark opening"));
210
211 res!(write(&repo, "notes.md", b"first thought\nsecond thought\n"));
212 res!(std::fs::remove_file(repo.join("bin.dat")));
213 res!(res!(ore(&repo, &["mark", "second"])).good("mark second"));
214
215 let from_mirror = res!(checkout(&scratch, &mirror, "main", "mirrored"));
216 res!(identical(&from_mirror, &repo, "the mirror against the working copy"));
217
218 // A mark is a tag, and a tag is a commit and not an object of its own.
219 let tags = res!(said(&mirror, &["tag", "-l"]));
220 let mut listed: Vec<&str> = tags.lines().collect();
221 listed.sort();
222 assert_eq!(listed, vec!["opening", "second"], "one tag per mark: {}", tags);
223 assert_eq!(res!(said(&mirror, &["cat-file", "-t", "refs/tags/opening"])), "commit",
224 "a mark is a lightweight tag, which mints no object");
225
226 // The earlier mark is the earlier state, and it still holds the file the
227 // later one dropped.
228 let earlier = res!(checkout(&scratch, &mirror, "opening", "earlier"));
229 let held = res!(tree_raw(&earlier));
230 assert!(held.contains_key(&b"bin.dat".to_vec()),
231 "the state at the first mark still holds what the second dropped");
232 assert_eq!(held.get(&b"notes.md".to_vec()).map(|b| b.as_slice()),
233 Some(&b"first thought\n"[..]),
234 "and the file as it stood then");
235
236 // Every commit says which operation it stands for, since git's author fields
237 // prove nothing and the log is where provenance lives.
238 let bodies = res!(said(&mirror, &["log", "--format=%B", "main"]));
239 assert!(bodies.contains("Ore-Mark: "),
240 "a commit names the mark it stands for: {}", bodies);
241 Ok(())
242}
243
244/// The mirror keeps up: an edit followed by any verb moves it, an arrival from
245/// a sync moves it, and the branch only ever goes forwards.
246#[test]
247fn the_mirror_keeps_up() -> Outcome<()> {
248 let scratch = res!(Scratch::new("export_current"));
249 let here = res!(scratch.sub("here"));
250 let there = res!(scratch.sub("there"));
251 let mirror = scratch.path.join("mirror.git");
252 res!(res!(ore(&here, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
253 res!(res!(ore(&there, &["init"])).good("init there"));
254
255 res!(write(&here, "shared.txt", b"one\n"));
256 res!(res!(ore(&here, &["mark", "start"])).good("mark start"));
257 res!(res!(ore(&there, &["sync", &fmt!("{}", here.display())])).good("sync there"));
258
259 let mut seen: Vec<String> = vec![res!(said(&mirror, &["rev-parse", "main"]))];
260
261 // An edit and a verb that is not `mark`: the mirror shows work no mark names.
262 res!(write(&here, "shared.txt", b"one\ntwo\n"));
263 res!(res!(ore(&here, &["log"])).good("log"));
264 seen.push(res!(said(&mirror, &["rev-parse", "main"])));
265 let after_edit = res!(checkout(&scratch, &mirror, "main", "after_edit"));
266 res!(identical(&after_edit, &here, "the mirror after an edit and a verb"));
267
268 // Work done elsewhere, arriving by sync.
269 res!(write(&there, "elsewhere.txt", b"written over there\n"));
270 res!(res!(ore(&there, &["mark", "away"])).good("mark away"));
271 res!(res!(ore(&here, &["sync", &fmt!("{}", there.display())])).good("sync here"));
272 seen.push(res!(said(&mirror, &["rev-parse", "main"])));
273 let after_sync = res!(checkout(&scratch, &mirror, "main", "after_sync"));
274 res!(identical(&after_sync, &here, "the mirror after a sync"));
275 assert!(res!(tree_raw(&after_sync)).contains_key(&b"elsewhere.txt".to_vec()),
276 "what arrived by sync is in the mirror");
277 assert_eq!(res!(said(&mirror, &["cat-file", "-t", "refs/tags/away"])), "commit",
278 "and the mark it arrived under is a tag");
279
280 // Every state the branch has held is an ancestor of the one after it, so a
281 // reader who cloned it never meets a forced update.
282 for pair in seen.windows(2) {
283 assert_ne!(pair[0], pair[1], "the branch moved: {:?}", seen);
284 let ok = git(&mirror, &["merge-base", "--is-ancestor", &pair[0], &pair[1]]);
285 assert!(ok.is_ok(),
286 "the branch went forwards from {} to {}", pair[0], pair[1]);
287 }
288
289 // And no commit the mirror wrote was ever rewritten: the first state the
290 // branch held is still there, with the tree it had.
291 let first_tree = res!(said(&mirror, &["rev-parse", &fmt!("{}^{{tree}}", seen[0])]));
292 assert!(!first_tree.is_empty(), "the first commit is still readable");
293
294 // Running a verb that changes nothing leaves the mirror alone.
295 res!(res!(ore(&here, &["flags"])).good("flags"));
296 assert_eq!(res!(said(&mirror, &["rev-parse", "main"])), seen[seen.len() - 1],
297 "a verb with nothing to record moves nothing");
298 Ok(())
299}
300
301/// A state git cannot hold is refused by name, and the mirror is left standing
302/// where it was rather than holding something else.
303///
304/// Git names a path either a file or a directory and never both, and it keeps
305/// nothing under a path called `.git` whatever its letter case. The second is
306/// reachable from a working copy -- `.GIT` is an ordinary directory to every
307/// filesystem `ore` runs on, and only `.git` and `.ore` are skipped by capture
308/// -- so it is what this uses. The first is refused by the engine and proven
309/// against git there.
310#[test]
311fn a_state_git_cannot_hold_is_refused_by_name() -> Outcome<()> {
312 let scratch = res!(Scratch::new("export_refuse"));
313 let repo = res!(scratch.sub("repo"));
314 let mirror = scratch.path.join("mirror.git");
315 res!(res!(ore(&repo, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
316
317 res!(write(&repo, "fine.txt", b"nothing wrong here\n"));
318 res!(res!(ore(&repo, &["mark", "good"])).good("mark good"));
319 let stood = res!(said(&mirror, &["rev-parse", "main"]));
320
321 res!(write(&repo, ".GIT/config", b"a path git will not hold\n"));
322 let ran = res!(ore(&repo, &["mark", "trouble"]));
323
324 // The verb succeeds and the history is recorded: a mirror that cannot be
325 // written must never stop somebody recording an edit.
326 assert!(ran.ok, "the verb still records the history: {}{}", ran.out, ran.err);
327 let told = fmt!("{}{}", ran.out, ran.err);
328 assert!(told.contains(".GIT"), "the refusal names the path: {}", told);
329 assert!(told.contains("mirror"), "and says it is the mirror that stopped: {}", told);
330
331 // And the mirror holds what it held, not a tree with a hole in it.
332 assert_eq!(res!(said(&mirror, &["rev-parse", "main"])), stood,
333 "the mirror stands where it stood");
334 let tags = res!(said(&mirror, &["tag", "-l"]));
335 assert!(!tags.contains("trouble"), "and the refused mark got no tag: {}", tags);
336
337 // The history itself is unharmed, and `ore log` still reads it.
338 let out = res!(ore(&repo, &["log"]));
339 let text = fmt!("{}", res!(out.good("log")));
340 assert!(text.contains("mark \"trouble\""),
341 "the mark is in the history whatever the mirror could do with it: {}", text);
342 Ok(())
343}
344
345/// A repository that names no mirror keeps none, and says nothing about it.
346#[test]
347fn a_repository_without_a_mirror_says_nothing() -> Outcome<()> {
348 let scratch = res!(Scratch::new("export_none"));
349 let repo = res!(scratch.sub("repo"));
350 res!(res!(ore(&repo, &["init"])).good("init"));
351 res!(write(&repo, "f.txt", b"x\n"));
352 let out = res!(ore(&repo, &["mark", "only"]));
353 let text = fmt!("{}", res!(out.good("mark only")));
354 assert!(!text.contains("mirror"), "no mirror is mentioned: {}", text);
355 Ok(())
356}
357
358/// A mirror named after the fact catches up with everything already recorded.
359///
360/// This is the whole of a one-off export: name a mirror, run any verb. There is
361/// no export verb because there is nothing for one to do.
362#[test]
363fn a_mirror_named_later_catches_up() -> Outcome<()> {
364 let scratch = res!(Scratch::new("export_later"));
365 let repo = res!(scratch.sub("repo"));
366 let mirror = scratch.path.join("mirror.git");
367 res!(res!(ore(&repo, &["init"])).good("init"));
368
369 res!(write(&repo, "one.txt", b"first\n"));
370 res!(res!(ore(&repo, &["mark", "one"])).good("mark one"));
371 res!(write(&repo, "two.txt", b"second\n"));
372 res!(res!(ore(&repo, &["mark", "two"])).good("mark two"));
373 assert!(!mirror.exists(), "nothing was mirrored while nothing was named");
374
375 // One line in the configuration, which is what a person who did not say
376 // --mirror at init has to write.
377 let cfg = repo.join(".ore").join("config");
378 let text = res!(std::fs::read_to_string(&cfg));
379 let with = text.replace("{\n", &fmt!("{{\n \"mirror\": \"{}\",\n", mirror.display()));
380 assert_ne!(with, text, "the configuration gained a mirror line");
381 res!(std::fs::write(&cfg, with));
382
383 res!(res!(ore(&repo, &["log"])).good("log"));
384 let from_mirror = res!(checkout(&scratch, &mirror, "main", "mirrored"));
385 res!(identical(&from_mirror, &repo, "a mirror named after the fact"));
386 let tags = res!(said(&mirror, &["tag", "-l"]));
387 assert!(tags.contains("one") && tags.contains("two"),
388 "every mark already recorded became a tag: {}", tags);
389 Ok(())
390}
391
392/// Two branches that each created a path leave two files, and the mirror lays
393/// them out exactly as the working copy does rather than losing one.
394#[test]
395fn a_path_two_files_claim_is_laid_out_as_the_working_copy_has_it() -> Outcome<()> {
396 let scratch = res!(Scratch::new("export_clash"));
397 let here = res!(scratch.sub("here"));
398 let there = res!(scratch.sub("there"));
399 let mirror = scratch.path.join("mirror.git");
400 res!(res!(ore(&here, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
401 res!(res!(ore(&there, &["init"])).good("init there"));
402
403 // Each side creates the same path, knowing nothing of the other.
404 res!(write(&here, "notes.md", b"mine\n"));
405 res!(res!(ore(&here, &["mark", "mine"])).good("mark mine"));
406 res!(write(&there, "notes.md", b"theirs\n"));
407 res!(res!(ore(&there, &["mark", "theirs"])).good("mark theirs"));
408 res!(res!(ore(&here, &["sync", &fmt!("{}", there.display())])).good("sync"));
409
410 let from_mirror = res!(checkout(&scratch, &mirror, "main", "mirrored"));
411 res!(identical(&from_mirror, &here, "the mirror against a working copy with a clash"));
412 let held = res!(tree_raw(&from_mirror));
413 assert_eq!(held.len(), 2,
414 "both files are in the mirror, one under a derived name: {:?}",
415 held.keys().map(|p| String::from_utf8_lossy(p).into_owned()).collect::<Vec<_>>());
416 Ok(())
417}
418
419/// A repository imported without git in the picture at all still mirrors, and
420/// the mirror a second run produces is the mirror the first one left.
421#[test]
422fn a_second_run_with_nothing_new_changes_nothing() -> Outcome<()> {
423 let scratch = res!(Scratch::new("export_idempotent"));
424 let src = res!(scratch.sub("src"));
425 let into = res!(scratch.sub("ore"));
426 let mirror = scratch.path.join("mirror.git");
427 res!(git_init(&src));
428 res!(write(&src, "f.txt", b"only\n"));
429 res!(git(&src, &["add", "."]));
430 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "only commit"]));
431
432 res!(res!(ore(&into, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
433 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
434 let first = res!(said(&mirror, &["rev-parse", "main"]));
435 for _ in 0..3 {
436 res!(res!(ore(&into, &["flags"])).good("flags"));
437 }
438 assert_eq!(res!(said(&mirror, &["rev-parse", "main"])), first,
439 "a mirror already right is left alone");
440 assert_eq!(res!(said(&mirror, &["rev-list", "--all", "--count"])), "1",
441 "and no commit was added for saying nothing");
442 Ok(())
443}
444
445
446// ---------------------------------------------------------------------------
447// What a commit is besides its tree: its date, its message and its author.
448// ---------------------------------------------------------------------------
449
450// The three identity lines one person wrote the history below under. All three
451// become one replica, which is the truth about who signed the work and is
452// exactly the shape an author map read backwards cannot resolve on its own.
453const NAMES: [(&str, &str); 3] = [
454 ("Ada Lovelace", "ada@example.org"),
455 ("ada", "ada@old.example"),
456 ("A. Lovelace", "ada@example.org"),
457];
458
459/// One commit of the history the tests below import.
460struct Told {
461 who: usize, // which of NAMES wrote it
462 when: &'static str, // as an identity line spells a moment
463 message: &'static str, // exactly, and git is told not to tidy it
464 holds: &'static str, // what the file says once it is in
465}
466
467// Dates years apart and years from now, each written in a different zone -- east
468// of UTC, west of it, one at a half hour, and only the last at UTC, so that a
469// mirror hard-coding `+0000` agrees with exactly one of the four. Two commits
470// whose first physical line is the same and whose subjects are not; a body under
471// one of them, ending in a trailer of the kind that carries most of what a real
472// history says.
473const STORY: [Told; 4] = [
474 Told {
475 who: 0,
476 when: "1550000000 +1000",
477 message: "base\n",
478 holds: "one\n",
479 },
480 Told {
481 who: 1,
482 when: "1600000000 -0500",
483 message: "notes:\nthe first note\n\nWhat the first note was for.\n",
484 holds: "one\ntwo\n",
485 },
486 Told {
487 who: 2,
488 when: "1650000000 +0530",
489 message: "notes:\nthe second note\n\nWhat the second was for.\n\n\
490 Co-Authored-By: Grace Hopper <grace@example.org>\n",
491 holds: "one\ntwo\nthree\n",
492 },
493 Told {
494 who: 0,
495 when: "1690000000 +0000",
496 message: "tidy up\n",
497 holds: "one\ntwo\nthree\nfour\n",
498 },
499];
500
501/// What git says about one commit.
502struct Said {
503 at: String, // author date, in seconds since the epoch
504 wall: String, // the same moment as the author's own clock read it
505 who: String, // the author identity line
506 body: String, // the message under the subject, as `%b` reports it
507 message: String, // the whole message, verbatim
508}
509
510/// Writes the history of [`STORY`] into a fresh git repository.
511///
512/// Each commit is made under its own identity at its own moment, and the message
513/// is handed over in a file with `--cleanup=verbatim` so that git writes down
514/// what the test said and not a tidied version of it.
515fn story(scratch: &Scratch, src: &Path)
516 -> Outcome<()>
517{
518 res!(git_init(src));
519 for (n, told) in STORY.iter().enumerate() {
520 let (name, email) = NAMES[told.who];
521 res!(write(src, "notes.txt", told.holds.as_bytes()));
522 let said = scratch.path.join(fmt!("message-{}", n));
523 res!(std::fs::write(&said, told.message));
524 res!(git_when(src, name, email, told.when, &["add", "."]));
525 res!(git_when(src, name, email, told.when, &[
526 "commit", "--quiet", "--no-verify", "--cleanup=verbatim",
527 "-F", &fmt!("{}", said.display()),
528 ]));
529 }
530 Ok(())
531}
532
533/// Puts every identity line of [`NAMES`] on the replica this working copy
534/// already is.
535///
536/// It is what a person importing their own history does by hand between `ore
537/// init` and `ore import`, and it has to be that replica rather than the one an
538/// identity derives, or the importing checkout is absent from its own map.
539fn one_author(into: &Path)
540 -> Outcome<u64>
541{
542 let path = into.join(".ore").join("config");
543 let text = fmt!("{}", String::from_utf8_lossy(&res!(std::fs::read(&path))));
544 let opens = "\"replica\": (u64|";
545 let replica: u64 = match text.split_once(opens).and_then(|(_, rest)| rest.split_once(')')) {
546 Some((digits, _)) => match digits.parse() {
547 Ok(n) => n,
548 Err(e) => return Err(err!(e,
549 "The configuration at {:?} spells its replica {:?}.", path, digits; Test)),
550 },
551 None => return Err(err!(
552 "The configuration at {:?} names no replica: {}", path, text; Test, Missing)),
553 };
554 let mut lines = String::new();
555 for (name, email) in NAMES {
556 lines.push_str(&fmt!(" \"{} <{}>\": (u64|{}),\n", name, email, replica));
557 }
558 let filled = text.replace(
559 "\"authors\": {},", &fmt!("\"authors\": {{\n{} }},", lines));
560 if filled == text {
561 return Err(err!(
562 "The configuration at {:?} holds no empty author map to fill: {}",
563 path, text;
564 Test, Missing));
565 }
566 res!(std::fs::write(&path, filled.as_bytes()));
567 Ok(replica)
568}
569
570/// What git says about every commit on a reference, keyed by subject.
571fn said_by(dir: &Path, refname: &str)
572 -> Outcome<BTreeMap<String, Said>>
573{
574 // A NUL between the fields and a byte 1 between the records, neither of which
575 // a commit message this test writes holds, so a message with blank lines in
576 // it comes back whole.
577 let out = res!(git(dir, &[
578 "log", "--format=%s%x00%at%x00%ai%x00%an <%ae>%x00%b%x00%B%x01", refname]));
579 let mut by: BTreeMap<String, Said> = BTreeMap::new();
580 for record in out.split(|b| *b == 0x01) {
581 let whole = String::from_utf8_lossy(record);
582 let whole = whole.trim_start_matches('\n');
583 if whole.is_empty() {
584 continue;
585 }
586 let parts: Vec<&str> = whole.splitn(6, '\0').collect();
587 if parts.len() != 6 {
588 return Err(err!(
589 "git described a commit in {:?} as {:?}, which is not six fields.",
590 dir, whole;
591 Test, Mismatch));
592 }
593 by.insert(fmt!("{}", parts[0]), Said {
594 at: fmt!("{}", parts[1]),
595 wall: fmt!("{}", parts[2]),
596 who: fmt!("{}", parts[3]),
597 body: fmt!("{}", parts[4]),
598 message: fmt!("{}", parts[5]),
599 });
600 }
601 Ok(by)
602}
603
604/// Returns a mirror commit's body with the trailer the mirror adds taken back
605/// off, which is what the mark it was written from said.
606fn without_trailer(body: &str) -> String {
607 if let Some(at) = body.rfind("\nOre-Mark: ") {
608 return fmt!("{}", &body[..at]);
609 }
610 // A commit with nothing under its subject: `%b` skips the blank line, so the
611 // trailer is the whole of the body and there is no newline in front of it.
612 if body.starts_with("Ore-Mark: ") {
613 return String::new();
614 }
615 fmt!("{}", body)
616}
617
618/// A history imported and mirrored, and what git says about both ends of it.
619struct Trip {
620 into: PathBuf, // the Ore working copy
621 src: BTreeMap<String, Said>, // the source's commits, by subject
622 out: BTreeMap<String, Said>, // the mirror's, by subject
623}
624
625/// Imports the history of [`STORY`] and mirrors it back out.
626fn trip(scratch: &Scratch)
627 -> Outcome<Trip>
628{
629 let src = res!(scratch.sub("src"));
630 let into = res!(scratch.sub("ore"));
631 let mirror = scratch.path.join("mirror.git");
632 res!(story(scratch, &src));
633 res!(res!(ore(&into, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
634 res!(one_author(&into));
635 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
636 let told = res!(said_by(&src, "main"));
637 let out = res!(said_by(&mirror, "main"));
638 Ok(Trip { into, src: told, out })
639}
640
641
642/// A mirrored commit is dated when it was written, not when it was imported, and
643/// in the zone it was written in.
644///
645/// The dates are years apart and years ago, because an import that discards them
646/// and hands the mirror the instant it ran agrees with itself on every commit: a
647/// fixture whose commits share a date cannot tell the two apart. The zones are
648/// four different ones for the same reason -- the instant and the wall clock are
649/// two facts and they fail separately, so both are asked. A mirror that kept the
650/// instant and wrote `+0000` would pass every check on `%at` there is.
651#[test]
652fn a_commit_keeps_the_date_it_was_written_on() -> Outcome<()> {
653 let scratch = res!(Scratch::new("export_dates"));
654 let trip = res!(trip(&scratch));
655 assert_eq!(trip.out.len(), STORY.len(), "one mirror commit per commit");
656 for (subject, told) in &trip.src {
657 let got = match trip.out.get(subject) {
658 Some(g) => g,
659 None => return Err(err!(
660 "The mirror holds no commit called {:?}; it holds {:?}.",
661 subject, trip.out.keys().collect::<Vec<_>>();
662 Test, Missing)),
663 };
664 assert_eq!(got.at, told.at,
665 "the commit {:?} is dated at the instant it was written", subject);
666 assert_eq!(got.wall, told.wall,
667 "the commit {:?} is dated by the clock its author was reading", subject);
668 }
669 let spread: BTreeSet<&String> = trip.out.values().map(|s| &s.at).collect();
670 assert_eq!(spread.len(), STORY.len(), "four commits, four dates: {:?}", spread);
671 assert!(spread.contains(&fmt!("1550000000")),
672 "the oldest commit is still in 2019: {:?}", spread);
673 // Named outright, since the offsets are the half of this that a comparison of
674 // two wrong answers could agree on.
675 let zones: BTreeSet<&str> = trip.out.values()
676 .filter_map(|s| s.wall.rsplit_once(' ').map(|(_, tz)| tz))
677 .collect();
678 assert_eq!(zones, ["+0000", "+0530", "+1000", "-0500"].into_iter().collect(),
679 "four commits, four zones, the half hour among them: {:?}", zones);
680 Ok(())
681}
682
683/// A mirrored commit's message is the message that was imported.
684///
685/// Two comparisons, because they are the two things that can be asked. Every
686/// commit is compared as a git reader reads one -- subject, then body -- and a
687/// commit whose subject was one physical line is compared byte for byte as well.
688/// A wrapped subject cannot be both: git's own reading of it is one line, and
689/// writing that back is writing something the source does not say verbatim.
690///
691/// Named trailers besides, because two bodies that were both discarded compare
692/// equal and a history that kept only its subjects would pass the rest.
693#[test]
694fn a_commit_message_is_the_message_it_was_imported_from() -> Outcome<()> {
695 let scratch = res!(Scratch::new("export_bodies"));
696 let trip = res!(trip(&scratch));
697 let mut verbatim = 0;
698 for (subject, told) in &trip.src {
699 let got = match trip.out.get(subject) {
700 Some(g) => g,
701 None => return Err(err!(
702 "The mirror holds no commit called {:?}; it holds {:?}.",
703 subject, trip.out.keys().collect::<Vec<_>>();
704 Test, Missing)),
705 };
706 assert_eq!(without_trailer(&got.body), told.body,
707 "the body of {:?} comes back as it went in", subject);
708 let wrapped = told.message.lines().next().map(str::trim_end) != Some(subject);
709 if !wrapped {
710 assert_eq!(without_trailer(&got.message), told.message,
711 "the message of {:?} comes back byte for byte", subject);
712 verbatim += 1;
713 }
714 }
715 // Two of the four wrapped, so two did not, and those two are the ones the
716 // byte comparison above ran on. A history that lost its bodies would pass
717 // nothing here, and a count of zero would mean it never ran.
718 assert_eq!(verbatim, 2,
719 "two of the four subjects are one line, and are compared as bytes");
720 let note = match trip.out.get("notes: the second note") {
721 Some(n) => n,
722 None => return Err(err!(
723 "The mirror holds no commit called {:?}; it holds {:?}.",
724 "notes: the second note", trip.out.keys().collect::<Vec<_>>();
725 Test, Missing)),
726 };
727 assert!(note.message.contains("What the second was for."),
728 "what the commit said is in the mirror: {:?}", note.message);
729 assert!(note.message.contains("Co-Authored-By: Grace Hopper <grace@example.org>"),
730 "a trailer under the subject reaches the mirror: {:?}", note.message);
731 // The mark's body is what carries it, so it is in the history and not merely
732 // in something the mirror made up.
733 let ran = res!(ore(&trip.into, &["log"]));
734 let shown = res!(ran.good("log"));
735 assert!(shown.contains("notes: the second note"),
736 "the log names the mark: {}", shown);
737 Ok(())
738}
739
740/// A wrapped subject is one name, and not the line it happened to wrap at.
741///
742/// Two of the fixture's commits begin `notes:` on a line of its own. Named by
743/// the first physical line they are one name between them, which is a mark
744/// nobody can reach and a commit nobody can tag.
745#[test]
746fn a_wrapped_subject_is_one_name_and_not_the_line_it_wrapped_at() -> Outcome<()> {
747 let scratch = res!(Scratch::new("export_subjects"));
748 let trip = res!(trip(&scratch));
749 let told: Vec<&String> = trip.src.keys().collect();
750 let out: Vec<&String> = trip.out.keys().collect();
751 assert_eq!(out, told, "every commit is named what git calls it");
752 assert!(trip.out.contains_key("notes: the first note"),
753 "a wrapped subject folds onto one line: {:?}", out);
754 assert!(trip.out.contains_key("notes: the second note"),
755 "and the second one is not the first: {:?}", out);
756 // The payoff: a mark a person can name. `ore back` takes the name the mark
757 // was given, and under the old rule both of these were `notes:`.
758 res!(res!(ore(&trip.into, &["back", "notes: the first note"])).good("back"));
759 let holds = res!(std::fs::read(trip.into.join("notes.txt")));
760 assert_eq!(String::from_utf8_lossy(&holds), STORY[1].holds,
761 "going back to the first note is going back to the first note");
762 Ok(())
763}
764
765/// One person under three identity lines is one replica here and three authors
766/// in the mirror.
767///
768/// The map runs identity to replica, so reversing it to write a commit's author
769/// finds three keys on one value and can only pick one. Which one is settled by
770/// byte order, which is no way to decide whose work it was.
771#[test]
772fn one_person_under_three_names_is_one_replica_and_three_authors() -> Outcome<()> {
773 let scratch = res!(Scratch::new("export_authors"));
774 let trip = res!(trip(&scratch));
775 for (subject, told) in &trip.src {
776 let got = match trip.out.get(subject) {
777 Some(g) => g,
778 None => return Err(err!(
779 "The mirror holds no commit called {:?}; it holds {:?}.",
780 subject, trip.out.keys().collect::<Vec<_>>();
781 Test, Missing)),
782 };
783 assert_eq!(got.who, told.who,
784 "the commit {:?} is authored by whoever wrote it", subject);
785 }
786 let names: BTreeSet<&String> = trip.out.values().map(|s| &s.who).collect();
787 assert_eq!(names.len(), NAMES.len(),
788 "all three identity lines survive the round trip: {:?}", names);
789 // And one replica inside Ore, on a file all three of them wrote a line of.
790 let ran = res!(ore(&trip.into, &["who", "notes.txt"]));
791 let shown = res!(ran.good("who"));
792 let mut replicas: BTreeSet<String> = BTreeSet::new();
793 let mut counters: BTreeSet<String> = BTreeSet::new();
794 for word in shown.split_whitespace() {
795 if let Some((replica, counter)) = word.split_once(':') {
796 let digits = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit());
797 if replica.starts_with('r') && digits(&replica[1..]) && digits(counter) {
798 replicas.insert(fmt!("{}", replica));
799 counters.insert(fmt!("{}", counter));
800 }
801 }
802 }
803 assert!(counters.len() >= 2,
804 "the file holds bytes from more than one commit, or this proves nothing: {}",
805 shown);
806 assert_eq!(replicas.len(), 1,
807 "three identity lines are one replica: {:?} in {}", replicas, shown);
808 Ok(())
809}
810
811/// A mark named after a long subject still becomes a tag.
812///
813/// Git's subject rule folds a wrapped subject onto one line, and fe2o3's history
814/// has three that come to more than 255 characters that way. A reference is a
815/// file named after it, 255 bytes is what a file name may be, and `git
816/// fast-import` refuses the *whole stream* over one reference it cannot create.
817/// The verb it was run from still succeeds, and the only sign is a line on
818/// stderr, so this asserts on the tags and on that line rather than on an exit
819/// status that will not move.
820#[test]
821fn a_mark_named_after_a_long_subject_still_becomes_a_tag() -> Outcome<()> {
822 let scratch = res!(Scratch::new("export_long"));
823 let src = res!(scratch.sub("src"));
824 let into = res!(scratch.sub("ore"));
825 let mirror = scratch.path.join("mirror.git");
826 res!(git_init(&src));
827 // Wrapped, so it is the fold that makes it long, which is how the three in
828 // fe2o3 got that way.
829 let mut message = fmt!("the subject wraps here:\n");
830 for n in 0..20 {
831 message.push_str(&fmt!("and carries on to word number {} of it\n", n));
832 }
833 res!(write(&src, "notes.txt", b"one\n"));
834 let file = scratch.path.join("long-message");
835 res!(std::fs::write(&file, message.as_bytes()));
836 res!(git(&src, &["add", "."]));
837 res!(git(&src, &["commit", "--quiet", "--no-verify", "--cleanup=verbatim",
838 "-F", &fmt!("{}", file.display())]));
839
840 res!(res!(ore(&into, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
841 let ran = res!(ore(&into, &["import", &fmt!("{}", src.display())]));
842 res!(ran.good("import"));
843 assert!(!ran.err.contains("fast-import"),
844 "the mirror's stream was written whole: {}", ran.err);
845
846 let subject = res!(said_by(&src, "main"));
847 assert_eq!(subject.len(), 1, "one commit went in");
848 let named = match subject.keys().next() {
849 Some(n) => n,
850 None => return Err(err!("git named no commit."; Test, Missing)),
851 };
852 assert!(named.len() > 255,
853 "the subject is longer than a file name may be: {} bytes", named.len());
854 let tags = res!(said(&mirror, &["tag", "--list"]));
855 let list: Vec<&str> = tags.lines().filter(|l| !l.is_empty()).collect();
856 assert_eq!(list.len(), 1, "the mark took a tag: {:?}", list);
857 for tag in &list {
858 assert!(tag.len() <= 255, "the tag is {} bytes: {:?}", tag.len(), tag);
859 }
860 Ok(())
861}
862
863/// A commit whose own message ends in an `Ore-Author` line keeps it, and is
864/// still authored by whoever wrote it.
865///
866/// A git commit message can say anything, this one included. The import writes
867/// its trailer after whatever was already there and a reader takes off exactly
868/// one line, so the last one is always the import's and the one under it is
869/// always the person's. A reader that stripped every such line instead would
870/// take the person's away and, on a message ending in two of them, would author
871/// the commit as whatever the person typed.
872#[test]
873fn a_message_that_ends_in_an_author_line_keeps_it() -> Outcome<()> {
874 let scratch = res!(Scratch::new("export_trailer"));
875 let src = res!(scratch.sub("src"));
876 let into = res!(scratch.sub("ore"));
877 let mirror = scratch.path.join("mirror.git");
878 res!(git_init(&src));
879 let message = "a message about trailers\n\n\
880 What it says here is the commit's own.\n\
881 Ore-Author: Nobody At All <nobody@example.invalid>\n";
882 res!(write(&src, "notes.txt", b"one\n"));
883 let file = scratch.path.join("trailer-message");
884 res!(std::fs::write(&file, message));
885 res!(git_as(&src, "Ada Lovelace", "ada@example.org", &["add", "."]));
886 res!(git_as(&src, "Ada Lovelace", "ada@example.org", &[
887 "commit", "--quiet", "--no-verify", "--cleanup=verbatim",
888 "-F", &fmt!("{}", file.display())]));
889
890 res!(res!(ore(&into, &["init", "--mirror", &fmt!("{}", mirror.display())])).good("init"));
891 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
892 let told = res!(said_by(&src, "main"));
893 let out = res!(said_by(&mirror, "main"));
894 let subject = "a message about trailers";
895 let (mine, theirs) = match (out.get(subject), told.get(subject)) {
896 (Some(m), Some(t)) => (m, t),
897 _ => return Err(err!(
898 "The mirror or the source holds no commit called {:?}.", subject;
899 Test, Missing)),
900 };
901 assert_eq!(mine.who, theirs.who,
902 "the commit is authored by whoever wrote it, not by what its message says");
903 assert_eq!(mine.who, "Ada Lovelace <ada@example.org>",
904 "and that is Ada, not Nobody At All");
905 assert_eq!(without_trailer(&mine.message), theirs.message,
906 "the message comes back byte for byte, the person's own line included");
907 assert_eq!(mine.message.matches("Ore-Author: ").count(), 1,
908 "one author line reaches the mirror and it is the person's: {:?}", mine.message);
909 Ok(())
910}