oxedyne/ore/cli/tests/ignore.rs
19.7 KiB, 6 runs
created by r2848102244:29, 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 | //! What the ignore file keeps out of the history, and what it cannot touch. |
| 2 | //! |
| 3 | //! The glob matching itself is proven upstream in `oxedyne_fe2o3_file`; what is |
| 4 | //! unproven until the binary is driven is that capture consults the rules at |
| 5 | //! all, that a tracked file outlives a rule that later names it, and that the |
| 6 | //! rules are read fresh at every verb rather than remembered from the first. |
| 7 | |
| 8 | mod support; |
| 9 | |
| 10 | use support::{ |
| 11 | git, |
| 12 | git_init, |
| 13 | ore, |
| 14 | write, |
| 15 | write_raw, |
| 16 | Scratch, |
| 17 | }; |
| 18 | |
| 19 | use oxedyne_fe2o3_core::prelude::*; |
| 20 | |
| 21 | use std::fs; |
| 22 | use std::os::unix::ffi::OsStrExt; |
| 23 | |
| 24 | |
| 25 | /// Git's glob shapes decide what a first capture takes: an anchored directory, |
| 26 | /// a suffix, a negation that wins by coming last, and a `**` across levels. |
| 27 | #[test] |
| 28 | fn glob_shapes_choose_what_is_captured() -> Outcome<()> { |
| 29 | let scratch = res!(Scratch::new("glob_shapes")); |
| 30 | let root = res!(scratch.sub("work")); |
| 31 | res!(res!(ore(&root, &["init"])).good("init")); |
| 32 | res!(write(&root, ".oreignore", |
| 33 | b"/target/\n*.tmp\n!keep.tmp\ndoc/**/*.bak\n")); |
| 34 | res!(write(&root, "target/debug/junk.bin", b"artefact\n")); |
| 35 | res!(write(&root, "src/target/x.txt", b"not at the root\n")); |
| 36 | res!(write(&root, "a.tmp", b"scratch\n")); |
| 37 | res!(write(&root, "keep.tmp", b"re-included\n")); |
| 38 | res!(write(&root, "doc/a/b/n.bak", b"deep backup\n")); |
| 39 | res!(write(&root, "root.bak", b"not under doc\n")); |
| 40 | let out = res!(ore(&root, &["mark", "one"])); |
| 41 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 42 | assert!(text.contains("4 created"), |
| 43 | "the three the rules let through, and the rules themselves: {}", text); |
| 44 | assert!(text.contains(".oreignore"), "the ignore file is tracked: {}", text); |
| 45 | assert!(text.contains("keep.tmp"), "the negation is later, so it wins: {}", text); |
| 46 | assert!(text.contains("src/target/x.txt"), "the leading slash anchors: {}", text); |
| 47 | assert!(text.contains("root.bak"), "doc/**/*.bak says nothing of the root: {}", text); |
| 48 | assert!(!text.contains("junk.bin"), "the anchored directory is out: {}", text); |
| 49 | assert!(!text.contains("a.tmp"), "the suffix is out: {}", text); |
| 50 | assert!(!text.contains("n.bak"), "the double star reached it: {}", text); |
| 51 | // The ignored files are still on disk, merely unrecorded. |
| 52 | assert!(root.join("target/debug/junk.bin").exists()); |
| 53 | assert!(root.join("a.tmp").exists()); |
| 54 | Ok(()) |
| 55 | } |
| 56 | |
| 57 | /// A file the history already tracks outlives a rule that later names it: its |
| 58 | /// edits are captured, and its removal is a deletion, exactly as git treats a |
| 59 | /// tracked file `.gitignore` mentions. |
| 60 | #[test] |
| 61 | fn a_tracked_file_outlives_a_new_ignore_rule() -> Outcome<()> { |
| 62 | let scratch = res!(Scratch::new("tracked_ignored")); |
| 63 | let root = res!(scratch.sub("work")); |
| 64 | res!(res!(ore(&root, &["init"])).good("init")); |
| 65 | res!(write(&root, "app.log", b"first line\n")); |
| 66 | res!(res!(ore(&root, &["mark", "one"])).good("mark one")); |
| 67 | |
| 68 | // The rule arrives after the file is tracked, along with a new file the |
| 69 | // rule does shut out. |
| 70 | res!(write(&root, ".oreignore", b"*.log\n")); |
| 71 | res!(write(&root, "app.log", b"first line\nsecond line\n")); |
| 72 | res!(write(&root, "new.log", b"never captured\n")); |
| 73 | let out = res!(ore(&root, &["mark", "two"])); |
| 74 | let text = fmt!("{}", res!(out.good("mark two"))); |
| 75 | assert!(text.contains("1 edited"), "the tracked file is still read: {}", text); |
| 76 | assert!(text.contains("app.log"), "and it is the tracked one: {}", text); |
| 77 | assert!(text.contains("1 created"), "the arriving ignore file is captured: {}", text); |
| 78 | assert!(!text.contains("new.log"), "the new file stays out: {}", text); |
| 79 | |
| 80 | // Removing the tracked file is a deletion, not an ignored non-event. |
| 81 | res!(fs::remove_file(root.join("app.log"))); |
| 82 | let out = res!(ore(&root, &["mark", "three"])); |
| 83 | let text = fmt!("{}", res!(out.good("mark three"))); |
| 84 | assert!(text.contains("1 deleted"), "gone means deleted, rule or no rule: {}", text); |
| 85 | Ok(()) |
| 86 | } |
| 87 | |
| 88 | /// The ignore file is read at every verb, so a rule removed speaks no longer. |
| 89 | #[test] |
| 90 | fn the_ignore_file_speaks_fresh_at_every_verb() -> Outcome<()> { |
| 91 | let scratch = res!(Scratch::new("fresh_rules")); |
| 92 | let root = res!(scratch.sub("work")); |
| 93 | res!(res!(ore(&root, &["init"])).good("init")); |
| 94 | res!(write(&root, ".oreignore", b"secret.txt\n")); |
| 95 | res!(write(&root, "secret.txt", b"kept out\n")); |
| 96 | let out = res!(ore(&root, &["mark", "one"])); |
| 97 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 98 | assert!(text.contains("1 created"), "only the rules themselves are captured: {}", text); |
| 99 | assert!(text.contains(".oreignore"), "which is the ignore file: {}", text); |
| 100 | assert!(!text.contains("secret.txt"), "the rule holds: {}", text); |
| 101 | |
| 102 | res!(write(&root, ".oreignore", b"# nothing any more\n")); |
| 103 | let out = res!(ore(&root, &["mark", "two"])); |
| 104 | let text = fmt!("{}", res!(out.good("mark two"))); |
| 105 | assert!(text.contains("1 created"), "the rule is gone, so the file is in: {}", text); |
| 106 | assert!(text.contains("secret.txt"), "and it is that file: {}", text); |
| 107 | assert!(text.contains("1 edited"), "and the rewritten rules are an edit: {}", text); |
| 108 | Ok(()) |
| 109 | } |
| 110 | |
| 111 | /// Nothing inside an ignored directory can be re-included, because the scan |
| 112 | /// never descends into it -- which is git's rule too. |
| 113 | #[test] |
| 114 | fn nothing_returns_from_inside_an_ignored_directory() -> Outcome<()> { |
| 115 | let scratch = res!(Scratch::new("swallowed")); |
| 116 | let root = res!(scratch.sub("work")); |
| 117 | res!(res!(ore(&root, &["init"])).good("init")); |
| 118 | res!(write(&root, ".oreignore", b"target/\n!target/keep.txt\n")); |
| 119 | res!(write(&root, "target/keep.txt", b"unreachable\n")); |
| 120 | let out = res!(ore(&root, &["mark", "one"])); |
| 121 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 122 | assert!(text.contains("1 created"), "only the rules themselves: {}", text); |
| 123 | assert!(!text.contains("keep.txt"), |
| 124 | "the negation cannot reach inside the ignored directory: {}", text); |
| 125 | Ok(()) |
| 126 | } |
| 127 | |
| 128 | /// A rule and a path that are not UTF-8 meet byte for byte. |
| 129 | #[test] |
| 130 | fn ignore_rules_apply_to_byte_paths() -> Outcome<()> { |
| 131 | let scratch = res!(Scratch::new("byte_paths")); |
| 132 | let root = res!(scratch.sub("work")); |
| 133 | res!(res!(ore(&root, &["init"])).good("init")); |
| 134 | // The pattern is the two bytes 0xff and `*`: not text, still a rule. |
| 135 | res!(write(&root, ".oreignore", b"\xff*\n")); |
| 136 | res!(write_raw(&root, b"\xff\xfe.bin", b"named in no encoding\n")); |
| 137 | res!(write_raw(&root, b"\xfe.bin", b"a different first byte\n")); |
| 138 | let out = res!(ore(&root, &["mark", "one"])); |
| 139 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 140 | assert!(text.contains("2 created"), |
| 141 | "the unmatched byte name and the rules are in: {}", text); |
| 142 | assert!(root.join(std::ffi::OsStr::from_bytes(b"\xff\xfe.bin")).exists()); |
| 143 | Ok(()) |
| 144 | } |
| 145 | |
| 146 | /// The ignore file is history like any other file, so a sync carries it to a |
| 147 | /// replica that never wrote it, rules and all. |
| 148 | #[test] |
| 149 | fn the_ignore_file_travels_with_a_sync() -> Outcome<()> { |
| 150 | let scratch = res!(Scratch::new("travelling_rules")); |
| 151 | let a = res!(scratch.sub("a")); |
| 152 | let b = res!(scratch.sub("b")); |
| 153 | res!(res!(ore(&a, &["init"])).good("init a")); |
| 154 | res!(write(&a, ".oreignore", b"*.tmp\n")); |
| 155 | res!(write(&a, "real.txt", b"content\n")); |
| 156 | res!(write(&a, "junk.tmp", b"never recorded\n")); |
| 157 | res!(res!(ore(&a, &["mark", "base"])).good("mark base")); |
| 158 | |
| 159 | res!(res!(ore(&b, &["init"])).good("init b")); |
| 160 | res!(res!(ore(&b, &["sync", &fmt!("{}", a.display())])).good("sync")); |
| 161 | let rules = res!(fs::read(b.join(".oreignore"))); |
| 162 | assert_eq!(rules.as_slice(), b"*.tmp\n", "the rules arrived byte for byte"); |
| 163 | assert!(b.join("real.txt").exists(), "alongside the ordinary file"); |
| 164 | assert!(!b.join("junk.tmp").exists(), "and what they ignored never travelled"); |
| 165 | Ok(()) |
| 166 | } |
| 167 | |
| 168 | /// A dotfile is content: it is captured, it survives a round trip through the |
| 169 | /// working copy, and it comes back when `ore back` puts a state on disk. |
| 170 | /// |
| 171 | /// The rule this proves was the other way round until 12026-07-30, when the |
| 172 | /// scan skipped everything whose name began with a full stop. That lost |
| 173 | /// `.gitignore`, `.github/workflows`, `.cargo/config.toml` and every other |
| 174 | /// configuration a repository is expected to carry -- silently, and then |
| 175 | /// destructively, because `back` removes what the state does not describe. |
| 176 | #[test] |
| 177 | fn a_dotfile_is_captured_like_any_other_file() -> Outcome<()> { |
| 178 | let scratch = res!(Scratch::new("dotfiles")); |
| 179 | let root = res!(scratch.sub("work")); |
| 180 | res!(res!(ore(&root, &["init"])).good("init")); |
| 181 | res!(write(&root, ".gitignore", b"target/\n")); |
| 182 | res!(write(&root, ".github/workflows/ci.yml", b"on: push\n")); |
| 183 | res!(write(&root, ".cargo/config.toml", b"[build]\n")); |
| 184 | res!(write(&root, "plain.txt", b"ordinary\n")); |
| 185 | let out = res!(ore(&root, &["mark", "one"])); |
| 186 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 187 | assert!(text.contains("4 created"), "every one of them: {}", text); |
| 188 | for path in [".gitignore", ".github/workflows/ci.yml", ".cargo/config.toml"] { |
| 189 | assert!(text.contains(path), "{} is captured: {}", path, text); |
| 190 | } |
| 191 | |
| 192 | // Editing one is an edit, not a file arriving anew. |
| 193 | res!(write(&root, ".gitignore", b"target/\n*.tmp\n")); |
| 194 | let out = res!(ore(&root, &["mark", "two"])); |
| 195 | let text = fmt!("{}", res!(out.good("mark two"))); |
| 196 | assert!(text.contains("1 edited"), "the dotfile is tracked: {}", text); |
| 197 | assert!(text.contains(".gitignore"), "and it is that one: {}", text); |
| 198 | |
| 199 | // And going back restores it rather than removing it, which is the failure |
| 200 | // the old rule would have produced. |
| 201 | res!(res!(ore(&root, &["back", "one"])).good("back one")); |
| 202 | let got = res!(fs::read(root.join(".gitignore"))); |
| 203 | assert_eq!(got.as_slice(), b"target/\n", "the earlier state came back"); |
| 204 | assert!(root.join(".github/workflows/ci.yml").exists(), |
| 205 | "a dotfile deep in a dot-named directory is still there"); |
| 206 | Ok(()) |
| 207 | } |
| 208 | |
| 209 | /// The two stores stay out, at the root and nested, and nothing a rule says can |
| 210 | /// bring either in. |
| 211 | /// |
| 212 | /// These are the only two names the scan knows by name. `.ore` is this tool's |
| 213 | /// own log, and capturing it would put the history inside itself; `.git` is |
| 214 | /// another version control system's, and a repository vendored inside the |
| 215 | /// working copy is no more this repository's content than the one at the top. |
| 216 | #[test] |
| 217 | fn the_two_stores_are_never_captured() -> Outcome<()> { |
| 218 | let scratch = res!(Scratch::new("stores")); |
| 219 | let root = res!(scratch.sub("work")); |
| 220 | res!(res!(ore(&root, &["init"])).good("init")); |
| 221 | // A rule that re-includes everything, so nothing below can be blamed on the |
| 222 | // ignore file. |
| 223 | res!(write(&root, ".oreignore", b"!*\n")); |
| 224 | res!(write(&root, ".git/config", b"[core]\n")); |
| 225 | res!(write(&root, ".git/objects/ab/cdef", b"\x00binary\n")); |
| 226 | res!(write(&root, "vendor/lib/.git/HEAD", b"ref: refs/heads/main\n")); |
| 227 | res!(write(&root, "vendor/lib/.ore/config", b"not ours either\n")); |
| 228 | res!(write(&root, "vendor/lib/src.rs", b"fn main() {}\n")); |
| 229 | res!(write(&root, "real.txt", b"content\n")); |
| 230 | let out = res!(ore(&root, &["mark", "one"])); |
| 231 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 232 | assert!(text.contains("3 created"), |
| 233 | "the rules, the vendored source and the ordinary file: {}", text); |
| 234 | for path in [".git/config", "abcdef", "HEAD", "vendor/lib/.ore/config"] { |
| 235 | assert!(!text.contains(path), "{} stayed out: {}", path, text); |
| 236 | } |
| 237 | assert!(text.contains("vendor/lib/src.rs"), |
| 238 | "a file beside a nested store is still content: {}", text); |
| 239 | // This repository's own log is where it was, and is not in its own history. |
| 240 | assert!(root.join(".ore").join("config").is_file()); |
| 241 | let out = res!(ore(&root, &["log"])); |
| 242 | let text = fmt!("{}", res!(out.good("log"))); |
| 243 | assert!(text.contains("captured nothing"), |
| 244 | "a second look finds neither store: {}", text); |
| 245 | Ok(()) |
| 246 | } |
| 247 | |
| 248 | /// An ignore rule governs a dotfile exactly as it governs anything else, by |
| 249 | /// name, by suffix, and by directory. |
| 250 | #[test] |
| 251 | fn an_ignore_rule_governs_a_dotfile() -> Outcome<()> { |
| 252 | let scratch = res!(Scratch::new("dot_rules")); |
| 253 | let root = res!(scratch.sub("work")); |
| 254 | res!(res!(ore(&root, &["init"])).good("init")); |
| 255 | res!(write(&root, ".oreignore", b".env\n.cache/\n*.local\n!.env.example\n")); |
| 256 | res!(write(&root, ".env", b"SECRET=nobody\n")); |
| 257 | res!(write(&root, ".env.example", b"SECRET=\n")); |
| 258 | res!(write(&root, ".envrc", b"use flake\n")); |
| 259 | res!(write(&root, ".cache/blob.bin", b"artefact\n")); |
| 260 | res!(write(&root, ".config.local", b"machine only\n")); |
| 261 | let out = res!(ore(&root, &["mark", "one"])); |
| 262 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 263 | assert!(text.contains("3 created"), |
| 264 | "the rules, the example and the unmatched dotfile: {}", text); |
| 265 | assert!(text.contains(".env.example"), "an exact rule does not catch a prefix \ |
| 266 | of a longer name, and the negation says so anyway: {}", text); |
| 267 | assert!(text.contains(".envrc"), "nor does it catch a different name: {}", text); |
| 268 | assert!(!text.contains("blob.bin"), "an ignored dot-named directory is not \ |
| 269 | descended into: {}", text); |
| 270 | assert!(!text.contains(".config.local"), "a suffix rule reaches a dotfile: {}", text); |
| 271 | // And the secret is still on disk, merely unrecorded. |
| 272 | assert!(root.join(".env").exists()); |
| 273 | Ok(()) |
| 274 | } |
| 275 | |
| 276 | /// A tracked dotfile outlives a rule that later names it, exactly as any other |
| 277 | /// tracked file does. |
| 278 | /// |
| 279 | /// This is the half of the policy that keeps a repository honest. A rule added |
| 280 | /// after the fact must not quietly freeze a file the history is already |
| 281 | /// carrying, or the working copy walks away from the history with nothing said. |
| 282 | #[test] |
| 283 | fn a_tracked_dotfile_outlives_a_later_rule() -> Outcome<()> { |
| 284 | let scratch = res!(Scratch::new("dot_tracked")); |
| 285 | let root = res!(scratch.sub("work")); |
| 286 | res!(res!(ore(&root, &["init"])).good("init")); |
| 287 | res!(write(&root, ".gitignore", b"target/\n")); |
| 288 | res!(res!(ore(&root, &["mark", "one"])).good("mark one")); |
| 289 | |
| 290 | // The rule arrives afterwards, and names the tracked file and a new one. |
| 291 | res!(write(&root, ".oreignore", b".gitignore\n.npmrc\n")); |
| 292 | res!(write(&root, ".gitignore", b"target/\nbuild/\n")); |
| 293 | res!(write(&root, ".npmrc", b"never captured\n")); |
| 294 | let out = res!(ore(&root, &["mark", "two"])); |
| 295 | let text = fmt!("{}", res!(out.good("mark two"))); |
| 296 | assert!(text.contains("1 edited"), "the tracked dotfile is still read: {}", text); |
| 297 | assert!(text.contains(".gitignore"), "and it is that one: {}", text); |
| 298 | assert!(!text.contains(".npmrc"), "the new dotfile stays out: {}", text); |
| 299 | |
| 300 | // Removing it is a deletion, not an ignored non-event. |
| 301 | res!(fs::remove_file(root.join(".gitignore"))); |
| 302 | let out = res!(ore(&root, &["mark", "three"])); |
| 303 | let text = fmt!("{}", res!(out.good("mark three"))); |
| 304 | assert!(text.contains("1 deleted"), "gone means deleted, rule or no rule: {}", text); |
| 305 | Ok(()) |
| 306 | } |
| 307 | |
| 308 | /// The buried side of a collision is written into `.ore`, and never comes back |
| 309 | /// as an authored edit. |
| 310 | /// |
| 311 | /// The spill is derived data: a render that arbitrates an overlap writes each |
| 312 | /// yielded operation's buried version under `.ore/collisions/`, and it is |
| 313 | /// rewritten from scratch on every render. That is only safe because `.ore` is |
| 314 | /// one of the two names the scan skips outright, so this checks the property the |
| 315 | /// design rests on rather than assuming it -- a spill captured as content would |
| 316 | /// put a losing version into the history as a third author's work, and would do |
| 317 | /// it again on every verb. |
| 318 | #[test] |
| 319 | fn a_spilled_collision_never_enters_the_history() -> Outcome<()> { |
| 320 | let scratch = res!(Scratch::new("spill_skip")); |
| 321 | let root = res!(scratch.sub("work")); |
| 322 | res!(res!(ore(&root, &["init"])).good("init")); |
| 323 | // A rule that re-includes everything, so nothing below can be blamed on the |
| 324 | // ignore file. |
| 325 | res!(write(&root, ".oreignore", b"!*\n")); |
| 326 | // A collision is awkward to conjure in one repository, so the spill is placed |
| 327 | // by hand: what is being tested is the scan, not the renderer. |
| 328 | res!(write(&root, ".ore/collisions/r1.4", b"the version that yielded\n")); |
| 329 | res!(write(&root, "real.txt", b"content\n")); |
| 330 | |
| 331 | let out = res!(ore(&root, &["mark", "one"])); |
| 332 | let text = fmt!("{}", res!(out.good("mark one"))); |
| 333 | assert!(text.contains("real.txt"), "the ordinary file is captured: {}", text); |
| 334 | assert!(!text.contains("collisions"), "and the spill is not: {}", text); |
| 335 | assert!(!text.contains("r1.4"), "under any name: {}", text); |
| 336 | |
| 337 | // It is still on disk, and a second verb still does not see it. |
| 338 | assert_eq!(res!(std::fs::read(root.join(".ore").join("collisions").join("r1.4"))), |
| 339 | b"the version that yielded\n".to_vec()); |
| 340 | let out = res!(ore(&root, &["log"])); |
| 341 | let text = fmt!("{}", res!(out.good("log"))); |
| 342 | assert!(text.contains("captured nothing"), |
| 343 | "a second look finds no spill: {}", text); |
| 344 | Ok(()) |
| 345 | } |
| 346 | |
| 347 | |
| 348 | /// An import leaves the rules, and everything else it never recorded, on the disk. |
| 349 | /// |
| 350 | /// The import ends by materialising the whole history over the working copy, and it is the |
| 351 | /// one verb that captures nothing first -- so a file the history does not describe was never |
| 352 | /// written down, and taking it off the disk loses it outright. The ignore file is the case |
| 353 | /// that bites: written before the import, as every runbook says to, it is not in the git |
| 354 | /// history, and a removal would hand the next verb the whole of `target/` to record for ever. |
| 355 | #[test] |
| 356 | fn an_import_leaves_what_it_never_recorded() -> Outcome<()> { |
| 357 | let scratch = res!(Scratch::new("import_keeps")); |
| 358 | let src = res!(scratch.sub("src")); |
| 359 | let into = res!(scratch.sub("ore")); |
| 360 | res!(git_init(&src)); |
| 361 | res!(write(&src, "src/main.rs", b"fn main() {}\n")); |
| 362 | res!(git(&src, &["add", "."])); |
| 363 | res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "the first commit"])); |
| 364 | |
| 365 | res!(res!(ore(&into, &["init"])).good("init")); |
| 366 | // The rules, and a file they exist to keep out, and one plain local file. None of the |
| 367 | // three is in the git history the import is about to read. |
| 368 | res!(write(&into, ".oreignore", b"*.log\n")); |
| 369 | res!(write(&into, "noise.log", b"an artefact\n")); |
| 370 | res!(write(&into, "notes.txt", b"mine, and not the history's\n")); |
| 371 | let ran = res!(ore(&into, &["import", &fmt!("{}", src.display())])); |
| 372 | assert!(ran.ok, "the import went through: {}{}", ran.out, ran.err); |
| 373 | assert!(ran.out.contains("0 removed"), "and removed nothing: {}", ran.out); |
| 374 | |
| 375 | assert!(into.join(".oreignore").is_file(), "the rules survived the import"); |
| 376 | assert!(into.join("noise.log").is_file(), "so did what they keep out"); |
| 377 | assert!(into.join("notes.txt").is_file(), "so did a file nobody claimed"); |
| 378 | assert!(into.join("src/main.rs").is_file(), "and the history did arrive"); |
| 379 | |
| 380 | // The point of keeping the rules: the next verb honours them. |
| 381 | let out = res!(ore(&into, &["mark", "after"])); |
| 382 | let text = fmt!("{}", res!(out.good("mark after"))); |
| 383 | assert!(!text.contains("noise.log"), |
| 384 | "the rules bound the capture that followed: {}", text); |
| 385 | assert!(text.contains(".oreignore"), |
| 386 | "and the rules themselves were taken, since nothing excludes them: {}", text); |
| 387 | Ok(()) |
| 388 | } |
| 389 | |
| 390 | /// Going back to a state older than the rules does not withdraw them. |
| 391 | /// |
| 392 | /// Every other file the tree does not describe goes, because the capture the verb began with |
| 393 | /// recorded it and it can be had again. The ignore file is not that kind of file: it is the |
| 394 | /// boundary of what the *next* capture reads, a history only grows, and one verb run without |
| 395 | /// it is enough to put an entire build directory beyond recall. |
| 396 | #[test] |
| 397 | fn a_state_older_than_the_rules_keeps_them() -> Outcome<()> { |
| 398 | let scratch = res!(Scratch::new("back_keeps_rules")); |
| 399 | let root = res!(scratch.sub("work")); |
| 400 | res!(res!(ore(&root, &["init"])).good("init")); |
| 401 | res!(write(&root, "a.txt", b"before the rules\n")); |
| 402 | res!(res!(ore(&root, &["mark", "bare"])).good("mark bare")); |
| 403 | res!(write(&root, ".oreignore", b"*.log\n")); |
| 404 | res!(write(&root, "noise.log", b"an artefact\n")); |
| 405 | res!(res!(ore(&root, &["mark", "ruled"])).good("mark ruled")); |
| 406 | |
| 407 | let out = res!(ore(&root, &["back", "bare"])); |
| 408 | let text = fmt!("{}", res!(out.good("back bare"))); |
| 409 | assert!(text.contains("bare"), "the older state was rendered: {}", text); |
| 410 | assert!(!text.contains("removed"), |
| 411 | "and the rules were not counted among what moved: {}", text); |
| 412 | assert!(root.join(".oreignore").is_file(), |
| 413 | "the rules are still on the disk, though that state predates them"); |
| 414 | assert!(root.join("noise.log").is_file(), |
| 415 | "and so is what they keep out, untouched either way"); |
| 416 | |
| 417 | // Which is the whole of why: the verb after the journey is still bounded. |
| 418 | let out = res!(ore(&root, &["mark", "after"])); |
| 419 | let text = fmt!("{}", res!(out.good("mark after"))); |
| 420 | assert!(!text.contains("noise.log"), |
| 421 | "the rules still bound the capture: {}", text); |
| 422 | Ok(()) |
| 423 | } |