on master help sign in
j/tests/cli.rs
4864 lines 220 kB history raw
1//! End-to-end CLI tests: the built binary against real jj repositories (§1,
2//! §7). Each test builds its own repo under a temp dir with its own config.
4use std::path::PathBuf;
5use std::process::{Command, Stdio};
7fn j_bin() -> PathBuf {
8 // cargo sets CARGO_BIN_EXE_<name> for integration tests
9 option_env!("CARGO_BIN_EXE_j")
10 .map(PathBuf::from)
11 .unwrap_or_else(|| {
12 let mut p = std::env::current_exe().unwrap();
13 p.pop(); // deps
14 p.pop(); // debug
15 p.push("j");
16 p
17 })
20struct Repo {
21 dir: PathBuf,
22 cfg: PathBuf,
25fn setup() -> Repo {
26 use std::sync::atomic::{AtomicUsize, Ordering};
27 static N: AtomicUsize = AtomicUsize::new(0);
28 let dir = std::env::temp_dir().join(format!(
29 "j-cli-test-{}-{}-{}",
30 std::process::id(),
31 N.fetch_add(1, Ordering::Relaxed),
32 std::time::SystemTime::now()
33 .duration_since(std::time::UNIX_EPOCH)
34 .unwrap()
35 .as_nanos()
36 ));
37 let _ = std::fs::remove_dir_all(&dir);
38 std::fs::create_dir_all(&dir).unwrap();
39 // the config lives outside the repo: a checkout may remove untracked
40 // files inside the working directory
41 let cfg = dir.join("..").join(format!("{}-cfg", dir.file_name().unwrap().to_string_lossy()));
42 std::fs::create_dir_all(&cfg.join("j")).unwrap();
43 let cfg = cfg.canonicalize().unwrap();
44 let mut config = include_str!("../config.j").to_string();
45 config = config.replace("Your Name", "Test User");
46 config = config.replace("you@example.com", "test@example.com");
47 std::fs::write(cfg.join("j/config.j"), config).unwrap();
48 let r = Repo { dir, cfg };
49 let out = r.j(&["init"]);
50 if out.code != 0 {
51 eprintln!("INIT FAILED in {}: {}", r.dir.display(), out.stderr);
52 eprintln!("config exists: {}", r.cfg.join("j/config.j").exists());
53 for e in std::fs::read_dir(&r.dir).unwrap() {
54 eprintln!(" entry: {:?}", e.unwrap().file_name());
55 }
56 }
57 out.ok();
58 r
61impl Repo {
62 fn operation_heads(&self) -> Vec<std::ffi::OsString> {
63 let mut heads: Vec<_> = std::fs::read_dir(self.dir.join(".jj/repo/op_heads/heads"))
64 .unwrap()
65 .map(|entry| entry.unwrap().file_name())
66 .collect();
67 heads.sort();
68 heads
69 }
71 fn j(&self, args: &[&str]) -> Out {
72 let out = Command::new(j_bin())
73 .args(args)
74 .current_dir(&self.dir)
75 .env("XDG_CONFIG_HOME", &self.cfg)
76 .env("NO_COLOR", "1")
77 .output()
78 .unwrap();
79 Out {
80 code: out.status.code().unwrap_or(-1),
81 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
82 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
83 }
84 }
86 fn j_stdin(&self, input: &str, args: &[&str]) -> Out {
87 let mut child = Command::new(j_bin())
88 .args(args)
89 .current_dir(&self.dir)
90 .env("XDG_CONFIG_HOME", &self.cfg)
91 .env("NO_COLOR", "1")
92 .stdin(Stdio::piped())
93 .stdout(Stdio::piped())
94 .stderr(Stdio::piped())
95 .spawn()
96 .unwrap();
97 use std::io::Write;
98 child
99 .stdin
100 .as_mut()
101 .unwrap()
102 .write_all(input.as_bytes())
103 .unwrap();
104 let out = child.wait_with_output().unwrap();
105 Out {
106 code: out.status.code().unwrap_or(-1),
107 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
108 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
109 }
110 }
112 /// Run `j` with stdout sent to `stdout` instead of captured.
113 fn j_to(&self, stdout: impl Into<Stdio>, args: &[&str]) -> Out {
114 let out = Command::new(j_bin())
115 .args(args)
116 .current_dir(&self.dir)
117 .env("XDG_CONFIG_HOME", &self.cfg)
118 .env("NO_COLOR", "1")
119 .stdout(stdout)
120 .output()
121 .unwrap();
122 Out {
123 code: out.status.code().unwrap_or(-1),
124 stdout: String::new(),
125 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
126 }
127 }
129 /// Run `j` with stdout on a pipe whose reader has already exited, as in
130 /// `j tree | head` once `head` has read all it wants.
131 fn j_closed_stdout(&self, args: &[&str]) -> Out {
132 let (reader, writer) = std::io::pipe().unwrap();
133 drop(reader);
134 self.j_to(writer, args)
135 }
137 /// Run `j` with stderr on a pipe whose reader has already exited, as in
138 /// `j … 2>&1 | head` once `head` has read all it wants; only the status
139 /// is left to look at.
140 fn j_closed_stderr(&self, args: &[&str]) -> i32 {
141 let (reader, writer) = std::io::pipe().unwrap();
142 drop(reader);
143 Command::new(j_bin())
144 .args(args)
145 .current_dir(&self.dir)
146 .env("XDG_CONFIG_HOME", &self.cfg)
147 .env("NO_COLOR", "1")
148 .stdin(Stdio::null())
149 .stdout(Stdio::null())
150 .stderr(writer)
151 .status()
152 .unwrap()
153 .code()
154 .unwrap_or(-1)
155 }
157 fn write(&self, path: &str, content: &str) {
158 std::fs::write(self.dir.join(path), content).unwrap();
159 }
161 fn read(&self, path: &str) -> String {
162 std::fs::read_to_string(self.dir.join(path)).unwrap()
163 }
166struct Out {
167 code: i32,
168 stdout: String,
169 stderr: String,
172impl Out {
173 fn ok(self) -> Self {
174 assert_eq!(self.code, 0, "stderr: {}", self.stderr);
175 self
176 }
179impl Drop for Repo {
180 fn drop(&mut self) {
181 let _ = std::fs::remove_dir_all(&self.dir);
182 let _ = std::fs::remove_dir_all(&self.cfg);
183 }
186#[test]
187fn no_args_is_usage_error() {
188 let r = setup();
189 let out = r.j(&[]);
190 assert_eq!(out.code, 2);
191 assert!(out.stderr.contains("usage"), "{}", out.stderr);
194#[test]
195fn both_stdin_and_args_rejected() {
196 let r = setup();
197 let out = r.j_stdin("1 + 1", &["extra"]);
198 assert_eq!(out.code, 2);
199 assert!(out.stderr.contains("both"), "{}", out.stderr);
202#[test]
203fn arguments_do_not_wait_for_an_open_stdin_pipe_to_close() {
204 use std::io::Write;
205 use std::time::{Duration, Instant};
206 let r = setup();
207 for input in [Some(b'1'), Some(0xff), None] {
208 let (reader, mut writer) = std::io::pipe().unwrap();
209 // Supply input before spawning so the readiness check cannot race
210 // the writer. Keep the writer open until the command has exited.
211 if let Some(byte) = input {
212 writer.write_all(&[byte]).unwrap();
213 }
214 let mut child = Command::new(j_bin())
215 .arg(if input.is_some() { "describe \"must not run\"" } else { "1" })
216 .current_dir(&r.dir)
217 .env("XDG_CONFIG_HOME", &r.cfg)
218 .env("NO_COLOR", "1")
219 .stdin(reader)
220 .stdout(Stdio::piped())
221 .stderr(Stdio::piped())
222 .spawn()
223 .unwrap();
224 let deadline = Instant::now() + Duration::from_secs(5);
225 while child.try_wait().unwrap().is_none() {
226 if Instant::now() >= deadline {
227 child.kill().unwrap();
228 let out = child.wait_with_output().unwrap();
229 panic!("waited for stdin EOF with input {:?}: {}", input,
230 String::from_utf8_lossy(&out.stderr));
231 }
232 std::thread::sleep(Duration::from_millis(10));
233 }
234 let out = child.wait_with_output().unwrap();
235 drop(writer);
236 let stderr = String::from_utf8_lossy(&out.stderr);
237 if input.is_some() {
238 assert_eq!(out.status.code(), Some(2), "{}", stderr);
239 assert_eq!(stderr, "j: expression given both as arguments and on stdin\n");
240 assert!(out.stdout.is_empty());
241 assert_eq!(r.j(&["\\r -> show (focus r).message"]).ok().stdout, "\"\"\n");
242 } else {
243 assert_eq!(out.status.code(), Some(0), "{}", stderr);
244 assert_eq!(out.stdout, b"1\n");
245 }
246 }
249#[test]
250fn an_expression_that_is_not_utf8_is_a_usage_error() {
251 // §1: an argument that was not UTF-8 (a path with a Latin-1 byte)
252 // panicked in `std::env::args` and exited 101, and such bytes on stdin
253 // read as no input at all, so the run printed the usage line
254 use std::ffi::OsStr;
255 use std::io::Write;
256 use std::os::unix::ffi::OsStrExt;
257 let r = setup();
258 let run = |args: &[&OsStr], input: &[u8]| {
259 let mut child = Command::new(j_bin())
260 .args(args)
261 .current_dir(&r.dir)
262 .env("XDG_CONFIG_HOME", &r.cfg)
263 .env("NO_COLOR", "1")
264 .stdin(Stdio::piped())
265 .stdout(Stdio::piped())
266 .stderr(Stdio::piped())
267 .spawn()
268 .unwrap();
269 child.stdin.take().unwrap().write_all(input).unwrap();
270 let out = child.wait_with_output().unwrap();
271 Out {
272 code: out.status.code().unwrap_or(-1),
273 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
274 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
275 }
276 };
277 let message = OsStr::from_bytes(b"describe \"\xff\"");
278 for args in [vec![message], vec![OsStr::new("remote"), OsStr::from_bytes(b"../nu\xff/r.git")]] {
279 let out = run(&args, b"");
280 assert_eq!(out.code, 2, "{}", out.stderr);
281 assert!(out.stderr.starts_with("j: argument is not valid UTF-8: "), "{}", out.stderr);
282 assert_eq!(out.stderr.lines().count(), 1, "{}", out.stderr);
283 }
284 let out = run(&[], message.as_bytes());
285 assert_eq!(out.code, 2, "{}", out.stderr);
286 assert_eq!(out.stderr, "j: the expression on stdin is not valid UTF-8\n");
287 // input on stdin is input, whatever its bytes (§1 rule 3)
288 let out = run(&[OsStr::new("1")], message.as_bytes());
289 assert_eq!(out.code, 2, "{}", out.stderr);
290 assert!(out.stderr.contains("both"), "{}", out.stderr);
291 // nothing was described
292 assert_eq!(r.j(&["\\r -> show (focus r).message"]).ok().stdout, "\"\"\n");
295#[test]
296fn expression_from_stdin() {
297 let r = setup();
298 let out = r.j_stdin("1 + 2 * 3", &[]).ok();
299 assert_eq!(out.stdout.trim(), "7");
302#[test]
303fn arguments_joined_verbatim() {
304 let r = setup();
305 let out = r.j(&["1", "+", "2"]).ok();
306 assert_eq!(out.stdout.trim(), "3");
309#[test]
310fn parse_error_is_exit_3() {
311 let r = setup();
312 let out = r.j(&["1 +"]);
313 assert_eq!(out.code, 3);
314 assert!(out.stderr.starts_with("j: "), "{}", out.stderr);
317#[test]
318fn deep_nesting_is_a_parse_error() {
319 // §1.4: however deep the input, the process exits 3 with a diagnostic
320 // and bounded source excerpt rather than overflowing or dumping the input.
321 let r = setup();
322 let n = 100_000;
323 // a left-associative chain too: the parser builds it in a loop, but its
324 // tree is as tall as the chain is long, and resolving its id literals
325 // after the parse recursed that deep
326 for src in [
327 format!("{}1{}", "(".repeat(n), ")".repeat(n)),
328 "[".repeat(n),
329 format!("1{}", " + 1".repeat(1_000_000)),
330 ] {
331 let out = r.j_stdin(&src, &[]);
332 assert_eq!(out.code, 3, "{}", out.stderr);
333 assert!(out.stderr.starts_with("j: ") && out.stderr.lines().count() <= 2, "{}", out.stderr);
334 assert!(out.stderr.len() < 1024, "diagnostic is not bounded: {} bytes", out.stderr.len());
335 }
338#[test]
339fn lists_nested_as_deep_as_they_parse_display() {
340 // §1.4, §3.4: input within the nesting bound is never a stack overflow,
341 // and the displayed result is no exception. Display recursed once per
342 // level of nested lists, at about 14 KB a level in this debug build, so
343 // a list literal 38,000 deep parsed and then aborted with exit 134 and
344 // no `j:` line, as did a list built that deep at run time
345 let r = setup();
346 let n = 40_000;
347 let out = r.j_stdin(&format!("{}{}", "[".repeat(n), "]".repeat(n)), &[]);
348 assert_eq!(out.code, 0, "{}", out.stderr);
349 // the innermost `[[]]` is a list of one path, the empty one (§5.1)
350 assert_eq!(out.stdout, "\n");
351 // `[[[[] 0] 1] …]`: any other list is one block per item (§5.1), and a
352 // list built at run time is not bounded by the parser
353 let n = 100_000;
354 let out = r.j(&[&format!("foldl (\\acc i -> [acc i]) [] (range 0 {})", n)]);
355 assert_eq!(out.code, 0, "{}", out.stderr);
356 let items: String = (0..n).map(|i| format!("\n{}\n", i)).collect();
357 assert!(out.stdout == format!("none\n{}", items), "{}…", &out.stdout[..40.min(out.stdout.len())]);
360#[test]
361fn crash_is_exit_1_with_trace() {
362 let r = setup();
363 let out = r.j(&["crash \"boom\""]);
364 assert_eq!(out.code, 1);
365 assert!(out.stderr.contains("j: crash: boom"), "{}", out.stderr);
366 assert!(out.stderr.contains("from crash \"boom\""), "{}", out.stderr);
369#[test]
370fn crash_trace_names_the_innermost_definition() {
371 // §1.4, §4.13, §5.1: the second line names the innermost definition that
372 // was executing, as the spec's own examples show. Nothing recorded one
373 // at run time, so every crash printed only `from EXPR`. A builtin is no
374 // definition, so a crash outside every definition names none.
375 let r = setup();
376 for (expr, trace) in [
377 ("goto %nope", " in goto, from goto %nope"),
378 ("describe 3", " in describe, from describe 3"),
379 ("new . goto %nope", " in goto, from new . goto %nope"),
380 ("squash 3", " in squash, from squash 3"),
381 // `prev = goto parents` runs goto's body, which raised the crash
382 ("prev . top", " in goto, from prev . top"),
383 ("crash \"boom\"", " from crash \"boom\""),
384 ] {
385 let out = r.j(&[expr]);
386 assert_eq!(out.code, 1, "{}: {}", expr, out.stderr);
387 assert_eq!(out.stderr.lines().nth(1), Some(trace), "{}: {}", expr, out.stderr);
388 }
391#[test]
392fn full_edit_flow() {
393 let r = setup();
394 r.write("a.txt", "one\n");
395 r.j(&["describe \"first\""]).ok();
396 r.j(&["new"]).ok();
397 r.write("b.txt", "two\n");
398 r.j(&["describe \"second\""]).ok();
399 let out = r.j(&["status"]).ok();
400 assert!(out.stdout.contains("second"), "{}", out.stdout);
401 assert!(out.stdout.contains("b.txt"), "{}", out.stdout);
402 r.j(&["prev"]).ok();
403 let out = r.j(&["status"]).ok();
404 assert!(out.stdout.contains("first"), "{}", out.stdout);
405 r.j(&["next"]).ok();
406 let out = r.j(&["here"]).ok();
407 assert!(!out.stdout.trim().is_empty());
410#[test]
411fn gitignore_is_honored() {
412 // §1.2: the snapshot tracks every file not matched by .gitignore
413 let r = setup();
414 r.write(".gitignore", "*.log\n");
415 r.j(&["new"]).ok();
416 r.write("secret.log", "ignored\n");
417 r.write("normal.txt", "tracked\n");
418 r.j(&["id"]).ok();
419 let out = r.j(&["status"]).ok();
420 assert!(out.stdout.contains("normal.txt"), "{}", out.stdout);
421 assert!(!out.stdout.contains("secret.log"), "{}", out.stdout);
424#[test]
425fn a_directory_replaced_by_a_file_inside_an_ignored_directory_is_recorded() {
426 // `build/a` holds tracked files when `.gitignore` comes to ignore
427 // `build/`, and is then replaced by a file. jj's scan reads an ignored
428 // directory only at the paths it tracks there, and took a path below
429 // the file for one it could not look at rather than one deleted, so
430 // every run that scanned exited 2, `undo` included, until the file was
431 // moved away (§7.4)
432 let r = setup();
433 std::fs::create_dir_all(r.dir.join("build/a/c")).unwrap();
434 r.write("build/a/b", "x\n");
435 r.write("build/a/c/d", "y\n");
436 r.write("build/keep", "k\n");
437 r.j(&["id"]).ok();
438 r.write(".gitignore", "build/\n");
439 r.j(&["id"]).ok();
440 std::fs::remove_dir_all(r.dir.join("build/a")).unwrap();
441 r.write("build/a", "f\n");
442 // the files below it are deleted; it is ignored, as a new file there is
443 let paths = "\\r -> show (map (.path) (files r))";
444 let left = "[[\".gitignore\"] [\"build\" \"keep\"]]";
445 assert_eq!(r.j(&[paths]).ok().stdout.trim(), left);
446 let out = r.j(&["undo"]);
447 assert_eq!(out.code, 1, "{}", out.stderr);
448 assert!(out.stderr.contains("working copy has changes not in @"), "{}", out.stderr);
449 r.j(&["id"]).ok();
450 assert_eq!(r.j(&[paths]).ok().stdout.trim(), left);
451 r.j(&["new"]).ok();
452 assert_eq!(r.j(&[paths]).ok().stdout.trim(), left);
453 assert_eq!(r.read("build/a"), "f\n");
454 assert_eq!(r.read("build/keep"), "k\n");
457#[test]
458fn dry_run_persists_nothing() {
459 let r = setup();
460 r.write("a.txt", "x\n");
461 r.j(&["describe \"work\""]).ok();
462 r.j(&["new"]).ok();
463 r.write("b.txt", "y\n");
464 let before = r.j(&["log"]).ok().stdout;
465 let out = r.j(&["tree . squash"]).ok();
466 assert!(!out.stdout.is_empty());
467 let after = r.j(&["log"]).ok().stdout;
468 assert_eq!(before, after, "dry run changed the repository");
471#[test]
472fn validate_catches_what_persistence_refuses() {
473 let r = setup();
474 r.write("a.txt", "x\n");
475 r.j(&["describe \"work\""]).ok();
476 let before = r.j(&["ops"]).ok().stdout.lines().count();
477 let preview = r.j(&["tree . validate . squash"]);
478 let persist = r.j(&["squash"]);
479 assert_eq!(preview.code, 1, "{}", preview.stdout);
480 assert_eq!(persist.code, 1, "{}", persist.stderr);
481 assert_eq!(preview.stderr.lines().next(), persist.stderr.lines().next());
482 assert!(preview.stderr.contains("the root commit cannot be changed"));
483 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), before);
484 assert_eq!(r.read("a.txt"), "x\n");
486 // Once the parent is an ordinary commit, both preview and persistence
487 // succeed, and the preview itself still records nothing.
488 r.j(&["new"]).ok();
489 r.write("b.txt", "y\n");
490 r.j(&["describe \"second\""]).ok();
491 let before = r.j(&["ops"]).ok().stdout.lines().count();
492 let out = r.j(&["tree . validate . squash"]).ok();
493 assert!(!out.stdout.is_empty());
494 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), before);
495 r.j(&["squash"]).ok();
498#[test]
499fn validate_and_persistence_reject_malformed_snapshot_contents() {
500 let r = setup();
501 r.write("kept", "original\n");
502 r.j(&["describe \"before\""]).ok();
503 let before = r.operation_heads();
504 for entry in ["{ path = ./x, content = 1 }", "{ path = ./x }"] {
505 let edit = format!("mapRoot (\\c -> c {{ files = [{}] }})", entry);
506 let dry = r.j(&[&format!("show . validate . {}", edit)]);
507 let persist = r.j(&[&edit]);
508 assert_eq!(dry.code, 1, "validate accepted {}: {}", entry, dry.stdout);
509 assert_eq!(persist.code, 1, "persistence accepted {}", entry);
510 assert_eq!(dry.stderr.lines().next(), persist.stderr.lines().next());
511 assert!(dry.stderr.contains("content"), "{}", dry.stderr);
512 assert_eq!(r.operation_heads(), before);
513 assert_eq!(r.read("kept"), "original\n");
514 assert!(!r.dir.join("x").exists());
515 }
518#[test]
519fn validate_and_persistence_reject_nontext_commit_messages() {
520 let r = setup();
521 r.write("kept", "original\n");
522 r.j(&["describe \"parent\""]).ok();
523 r.j(&["new"]).ok();
524 r.j(&["describe \"focus\""]).ok();
525 let before = r.operation_heads();
526 for target in ["here", "parents"] {
527 for message in ["1", "[]", "true"] {
528 let edit = format!("at {} (mapRoot (\\c -> c {{ message = {} }}))", target, message);
529 let dry = r.j(&[&format!("show . validate . {}", edit)]);
530 let persist = r.j(&[&edit]);
531 assert_eq!(dry.code, 1, "validate accepted {}: {}", edit, dry.stdout);
532 assert_eq!(persist.code, 1, "persistence accepted {}", edit);
533 assert_eq!(dry.stderr.lines().next(), persist.stderr.lines().next());
534 assert!(dry.stderr.contains("expected Text"), "{}", dry.stderr);
535 assert_eq!(r.operation_heads(), before);
536 assert_eq!(r.read("kept"), "original\n");
537 }
538 }
541#[test]
542fn persisting_reads_no_tree_of_a_commit_it_keeps() {
543 // validation checked the paths of every commit's snapshot, and the walk
544 // compared every commit with its stored tree entry by entry, so each
545 // persisting run read the files of the whole history: `describe` took
546 // two minutes (a debug build) on 2,500 commits of 3,000 files. A commit
547 // whose files are still its stored tree's is now compared by tree (§7.5).
548 // Here A's tree is missing from the store, which a run that keeps A
549 // does not notice
550 let r = setup();
551 r.write("a.txt", "only in A\n");
552 r.j(&["describe \"A\""]).ok();
553 r.j(&["new"]).ok();
554 r.write("b.txt", "B\n");
555 r.j(&["describe \"B\""]).ok();
556 r.j(&["new"]).ok();
557 let hash = |kind, bytes: &[u8]| gix::objs::compute_hash(gix::hash::Kind::Sha1, kind, bytes).unwrap();
558 let mut tree = b"100644 a.txt\0".to_vec();
559 tree.extend_from_slice(hash(gix::objs::Kind::Blob, b"only in A\n").as_bytes());
560 let id = hash(gix::objs::Kind::Tree, &tree).to_string();
561 std::fs::remove_file(r.dir.join(".git/objects").join(&id[..2]).join(&id[2..])).unwrap();
562 r.j(&["describe \"C\""]).ok();
563 // a snapshot loads the history afresh
564 r.write("c.txt", "C\n");
565 r.j(&["describe \"C2\""]).ok();
566 r.j(&["new"]).ok();
567 // reading A's files still fails
568 let out = r.j(&[PATHS_BY_COMMIT]);
569 assert_eq!(out.code, 1, "{}", out.stderr);
572#[test]
573fn undo_and_redo() {
574 let r = setup();
575 r.write("a.txt", "x\n");
576 r.j(&["describe \"one\""]).ok();
577 let before = r.j(&["status"]).ok().stdout;
578 assert!(before.contains("one"));
579 r.j(&["undo"]).ok();
580 let after = r.j(&["status"]).ok().stdout;
581 assert!(!after.contains("one"), "{}", after);
582 r.j(&["redo"]).ok();
583 let restored = r.j(&["status"]).ok().stdout;
584 assert!(restored.contains("one"), "{}", restored);
587#[test]
588fn undo_refuses_dirty_working_copy() {
589 let r = setup();
590 r.write("a.txt", "x\n");
591 r.j(&["id"]).ok();
592 r.write("a.txt", "dirty\n");
593 let out = r.j(&["undo"]);
594 assert_eq!(out.code, 1);
595 assert!(out.stderr.contains("working copy has changes"), "{}", out.stderr);
598#[test]
599fn nothing_to_undo_or_redo() {
600 let r = setup();
601 let out = r.j(&["redo"]);
602 assert_eq!(out.code, 1);
603 assert!(out.stderr.contains("nothing to redo"), "{}", out.stderr);
606#[test]
607fn undo_after_init_refuses_cleanly() {
608 // undoing the operation that established the working copy would restore a
609 // view with no checked-out commit, leaving the repository unusable; it is
610 // reported as "nothing to undo", not a confusing error
611 let r = setup();
612 let out = r.j(&["undo"]);
613 assert_eq!(out.code, 1);
614 assert!(out.stderr.contains("nothing to undo"), "{}", out.stderr);
615 // and the repository is still usable afterwards
616 r.write("a.txt", "x\n");
617 r.j(&["describe \"still works\""]).ok();
618 assert!(r.j(&["status"]).ok().stdout.contains("still works"));
621#[test]
622fn ops_lists_operations() {
623 let r = setup();
624 r.write("a.txt", "x\n");
625 r.j(&["describe \"marker\""]).ok();
626 let out = r.j(&["ops"]).ok();
627 assert!(out.stdout.contains("describe \"marker\""), "{}", out.stdout);
628 // the current operation is marked
629 assert!(out.stdout.contains('*'), "{}", out.stdout);
630 // one line per operation, newest first
631 assert!(out.stdout.lines().count() >= 3, "{}", out.stdout);
634#[test]
635fn squash_and_abandon() {
636 let r = setup();
637 r.write("a.txt", "one\n");
638 r.j(&["describe \"base\""]).ok();
639 r.j(&["new"]).ok();
640 r.write("a.txt", "two\n");
641 r.j(&["describe \"fold me\""]).ok();
642 r.j(&["squash"]).ok();
643 let out = r.j(&["log"]).ok().stdout;
644 assert!(!out.contains("fold me"), "{}", out);
645 assert_eq!(r.read("a.txt"), "two\n");
648#[test]
649fn rebase_onto_sibling() {
650 let r = setup();
651 r.write("f.txt", "base\n");
652 r.j(&["describe \"base\""]).ok();
653 // branch from the parent of the focus
654 r.j(&["new"]).ok();
655 r.write("f.txt", "side\n");
656 r.j(&["describe \"side\""]).ok();
657 let out = r.j(&["tree"]).ok();
658 assert!(out.stdout.contains("side"), "{}", out.stdout);
661#[test]
662fn lock_blocks_second_j() {
663 // hold the flock ourselves, then a second j is refused
664 use std::os::unix::io::AsRawFd;
665 let r = setup();
666 let lock = std::fs::OpenOptions::new()
667 .create(true)
668 .write(true)
669 .open(r.dir.join(".jj/j.lock"))
670 .unwrap();
671 let rc = unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
672 assert_eq!(rc, 0);
673 let out = r.j(&["tree"]);
674 assert_eq!(out.code, 2);
675 assert!(out.stderr.contains("another j is running"), "{}", out.stderr);
676 unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_UN) };
677 r.j(&["tree"]).ok();
680#[test]
681fn reserved_words_as_first_word() {
682 let r = setup();
683 // `(push)` is an ordinary expression (unbound name)
684 let out = r.j(&["(push)"]);
685 assert_eq!(out.code, 1);
686 assert!(out.stderr.contains("unbound"), "{}", out.stderr);
687 // `x fetch` too
688 let out = r.j(&["x fetch"]);
689 assert_eq!(out.code, 1);
692#[test]
693fn id_resolution_errors() {
694 let r = setup();
695 let out = r.j(&["by @nope"]);
696 assert_eq!(out.code, 3); // lexical error: @ followed by non-k..z
697 let out = r.j(&["by @kqzz"]);
698 assert_eq!(out.code, 1); // no such commit: crash
699 assert!(out.stderr.contains("matches no commit"), "{}", out.stderr);
702#[test]
703fn goto_trunk_requires_new() {
704 let r = setup();
705 r.write("a", "1\n");
706 r.j(&["describe \"x\""]).ok();
707 // focusing the root commit is refused
708 let out = r.j(&["by @zzzzzzzz"]);
709 assert_eq!(out.code, 1);
710 assert!(out.stderr.contains("mutable"), "{}", out.stderr);
713#[test]
714fn text_result_prints_raw() {
715 let r = setup();
716 let out = r.j(&["\"literal text\""]).ok();
717 assert_eq!(out.stdout, "literal text\n");
718 let out = r.j(&["show [1 \"two\"]"]).ok();
719 assert!(out.stdout.contains("[1 \"two\"]"), "{}", out.stdout);
722#[test]
723fn review_says_no_changes_only_when_nothing_changed() {
724 // A missing renderer must still display changes; an installed renderer's
725 // failures must remain visible instead of claiming "no changes".
726 let r = setup();
727 // a PATH holding nothing but what the test puts there (the config
728 // directory is removed with the repository)
729 let bin = r.cfg.join("bin");
730 std::fs::create_dir_all(&bin).unwrap();
731 let review = |path: &std::path::Path| {
732 let out = Command::new(j_bin())
733 .arg("review")
734 .current_dir(&r.dir)
735 .env("XDG_CONFIG_HOME", &r.cfg)
736 .env("NO_COLOR", "1")
737 .env("PATH", path)
738 .output()
739 .unwrap();
740 Out {
741 code: out.status.code().unwrap_or(-1),
742 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
743 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
744 }
745 };
746 // nothing to render: difft is never run
747 assert_eq!(review(&bin).ok().stdout, "no changes\n");
748 r.write("f", "hello\n");
749 let out = review(&bin);
750 assert_eq!(out.code, 0, "{}", out.stderr);
751 assert_eq!(out.stdout, "--- a/f\n+++ b/f\n@@ -0,0 +1 @@\n+hello\n");
752 assert!(out.stderr.is_empty(), "{}", out.stderr);
753 use std::os::unix::fs::PermissionsExt;
754 // a difft that runs and fails renders what it printed, nothing here,
755 // and its complaint reaches the terminal instead of being dropped (§7.10)
756 let failing = r.cfg.join("failing");
757 std::fs::create_dir_all(&failing).unwrap();
758 let stub = failing.join("difft");
759 std::fs::write(&stub, "#!/bin/sh\necho 'error: invalid value for --color' >&2\nexit 2\n").unwrap();
760 std::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)).unwrap();
761 let out = review(&failing);
762 assert_eq!(out.code, 0, "{}", out.stderr);
763 assert_eq!(out.stderr, "error: invalid value for --color\n");
764 assert_eq!(out.stdout.trim(), "");
765 // with a difft, each changed path's rendering, in order
766 let stub = bin.join("difft");
767 std::fs::write(&stub, "#!/bin/sh\nprintf '%s -> %s\\n' \"${1##*/}\" \"${2##*/}\"\n").unwrap();
768 std::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)).unwrap();
769 r.write("g", "world\n");
770 assert_eq!(review(&bin).ok().stdout, "f -> f\ng -> g\n");
773#[test]
774fn closed_stdout_ends_the_display_quietly() {
775 // §1.4: a reader that stops early (`j tree | head`) is not a failure;
776 // writing into its closed pipe used to panic and exit 101
777 let r = setup();
778 r.write("a.txt", "x\n");
779 r.j(&["describe \"shown\""]).ok();
780 for expr in ["1 + 1", "tree", "range 0 200000"] {
781 let out = r.j_closed_stdout(&[expr]);
782 assert_eq!(out.code, 0, "{}: {}", expr, out.stderr);
783 assert_eq!(out.stderr, "", "{}", expr);
784 }
785 // the reserved command that prints writes the same way
786 let out = r.j_closed_stdout(&["ops"]);
787 assert_eq!(out.code, 0, "ops: {}", out.stderr);
788 assert_eq!(out.stderr, "");
789 // and the lock was released
790 assert!(r.j(&["ops"]).ok().stdout.contains("describe \"shown\""));
793#[test]
794fn closed_stderr_keeps_the_exit_status() {
795 // §1.4: a message stderr cannot take is dropped, and the run keeps its
796 // status; every error writer used to panic on the closed pipe instead,
797 // and the run exited 101
798 use std::os::unix::io::AsRawFd;
799 let r = setup();
800 for (args, code) in [
801 (&["crash \"boom\""][..], 1), // crash and its `from` line
802 (&["by @kqzz"], 1), // an unresolvable id
803 (&["1 +"], 3), // parse error
804 (&["push"], 2), // a reserved command's usage
805 (&[], 2), // no expression at all
806 ] {
807 assert_eq!(r.j_closed_stderr(args), code, "{:?}", args);
808 }
809 // the lock refusal is written by the backend
810 let lock = std::fs::OpenOptions::new()
811 .create(true)
812 .write(true)
813 .open(r.dir.join(".jj/j.lock"))
814 .unwrap();
815 assert_eq!(unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }, 0);
816 assert_eq!(r.j_closed_stderr(&["tree"]), 2);
817 drop(lock);
818 // the notice for a default config written on first use is not an error
819 std::fs::remove_file(r.cfg.join("j/config.j")).unwrap();
820 assert_eq!(r.j_closed_stderr(&["1 + 1"]), 0);
821 assert!(r.cfg.join("j/config.j").exists());
822 // and a config error is still one
823 std::fs::write(r.cfg.join("j/config.j"), "x =\n").unwrap();
824 assert_eq!(r.j_closed_stderr(&["1 + 1"]), 3);
827#[test]
828fn unwritable_stdout_is_a_one_line_error() {
829 // §1.4: any other write failure is an error line, not a panic
830 let full = std::path::Path::new("/dev/full");
831 if !full.exists() {
832 return;
833 }
834 let r = setup();
835 for args in [["1 + 1"], ["ops"]] {
836 let dev = std::fs::OpenOptions::new().write(true).open(full).unwrap();
837 let out = r.j_to(dev, &args);
838 assert_eq!(out.code, 1, "{:?}: {}", args, out.stderr);
839 assert!(out.stderr.starts_with("j: "), "{:?}: {}", args, out.stderr);
840 assert_eq!(out.stderr.lines().count(), 1, "{:?}: {}", args, out.stderr);
841 }
844#[test]
845fn crash_leaves_repo_untouched() {
846 let r = setup();
847 r.write("a.txt", "x\n");
848 r.j(&["describe \"stable\""]).ok();
849 let before = r.j(&["log"]).ok().stdout;
850 r.write("b.txt", "y\n");
851 let out = r.j(&["describe \"x\" . crash \"no\" . describe \"y\""]);
852 assert_eq!(out.code, 1);
853 let after = r.j(&["log"]).ok().stdout;
854 assert_eq!(before, after);
855 // the working directory is untouched too
856 assert_eq!(r.read("b.txt"), "y\n");
859#[test]
860fn unwritable_path_refused_before_anything_is_recorded() {
861 // these paths were written and the operation published; only the
862 // checkout then refused them, so every later persisting run recorded
863 // another operation and crashed the same way, and undo refused
864 let r = setup();
865 r.write("a.txt", "x\n");
866 r.j(&["describe \"stable\""]).ok();
867 let log = r.j(&["log"]).ok().stdout;
868 let ops = r.j(&["ops"]).ok().stdout.lines().count();
869 // longer than NAME_MAX (255 bytes): the checkout failed to stat it
870 let long = format!("[\"{}\"]", "x".repeat(256));
871 for bad in [
872 r#"[".." "evil"]"#,
873 r#"["x" ".."]"#,
874 r#"[".git" "hooks" "post-checkout"]"#,
875 r#"[".jj" "x"]"#,
876 long.as_str(),
877 ] {
878 let edit = format!(
879 "mapRoot (\\c -> c {{ files = c.files ++ [{{ path = {}, content = blob \"pwn\" }}] }})",
880 bad
881 );
882 // the dry run reports what persisting would
883 let out = r.j(&[&format!("tree . validate . {}", edit)]);
884 assert_eq!(out.code, 1, "validate accepted {}: {}", bad, out.stdout);
885 assert!(out.stderr.contains("path component"), "{}", out.stderr);
886 let out = r.j(&[&edit]);
887 assert_eq!(out.code, 1, "{}: {}", bad, out.stderr);
888 assert!(out.stderr.contains("path component"), "{}", out.stderr);
889 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops, "{} was recorded", bad);
890 assert_eq!(r.j(&["log"]).ok().stdout, log);
891 }
892 // nothing was created in the working directory, and the repo still works
893 assert!(!r.dir.join("x").exists());
894 r.j(&["describe \"after\""]).ok();
897#[test]
898fn names_the_filesystem_holds_are_accepted_at_any_length() {
899 // §7.5 step 1: 255 bytes is ext4's name limit, but exFAT, NTFS and APFS
900 // count 255 UTF-16 units or characters and hold a name of 100 `日`
901 // (300 bytes). With one in the working directory, every persisting run
902 // was refused for it, though the checkout never creates a name the
903 // directory holds, and a name the checkout would create was refused
904 // though the filesystem holds it. Where it does not, the name is still
905 // refused before anything is recorded.
906 let r = setup();
907 r.write("a.txt", "x\n");
908 r.j(&["describe \"stable\""]).ok();
909 let wide = "日".repeat(100);
910 let edit = |name: &str| {
911 format!(
912 "mapRoot (\\c -> c {{ files = c.files ++ [{{ path = [\"d\" \"{}\"], content = blob \"new\\n\" }}] }})",
913 name
914 )
915 };
916 if std::fs::write(r.dir.join(&wide), "on disk\n").is_ok() {
917 r.j(&["describe \"wide\""]).ok();
918 r.j(&["new"]).ok();
919 let wider = "日".repeat(101);
920 r.j(&[&format!("tree . validate . {}", edit(&wider))]).ok();
921 r.j(&[&edit(&wider)]).ok();
922 assert_eq!(r.read(&format!("d/{}", wider)), "new\n");
923 assert_eq!(r.read(&wide), "on disk\n");
924 } else {
925 let ops = r.j(&["ops"]).ok().stdout.lines().count();
926 let out = r.j(&[&edit(&wide)]);
927 assert_eq!(out.code, 1, "{}", out.stderr);
928 assert!(out.stderr.contains("path component"), "{}", out.stderr);
929 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
930 assert!(!r.dir.join("d").exists());
931 }
932 r.j(&["describe \"after\""]).ok();
935/// Each commit's message and content of `a`, top-down
936const CONTENT_OF_A: &str =
937 "\\r -> show (map (\\c -> [c.message (contentAt [\"a\"] c.files)]) (commits (top r)))";
939/// An expression showing, for the commit with each of `messages`, the text
940/// at each of `paths` (in the language's syntax), in that order: siblings
941/// made or rewritten by one operation are listed in no order of their own
942fn texts_by_message(paths: &[&str], messages: &[&str]) -> String {
943 let texts: Vec<String> = paths.iter().map(|p| format!("(text (contentAt {} f))", p)).collect();
944 let messages: Vec<String> = messages.iter().map(|m| format!("\"{}\"", m)).collect();
945 format!(
946 "\\r -> show (map (\\m -> let f = (focus (goto (matching (\\c -> c.message == m) all) r)).files in [{}]) [{}])",
947 texts.join(" "),
948 messages.join(" ")
949 )
952#[test]
953fn a_checkout_that_fails_is_recorded_and_undone() {
954 // §7.5 step 7, §7.7: the operation was published only once the checkout
955 // had completed, so a checkout the filesystem refused (a path longer
956 // than PATH_MAX here; a name longer than a smaller NAME_MAX, a full
957 // disk) was put back file by file, and a put-back can fail as well. The
958 // operation is now published first, as jj does, and a checkout that
959 // fails leaves it recorded: the crash says so, the next run takes what
960 // the checkout wrote for the focus's own, and `j undo` goes back.
961 let r = setup();
962 r.write("a", "p\n");
963 r.j(&["describe \"P\""]).ok();
964 r.j(&["new"]).ok();
965 r.write("a", "x1\n");
966 r.j(&["describe \"X\""]).ok();
967 r.write("a", "x2\n");
968 let log = r.j(&["log"]).ok().stdout;
969 let ops = r.j(&["ops"]).ok().stdout.lines().count();
970 // every component is a name a checkout can create (§7.5 step 1)
971 let deep = vec![format!("\"{}\"", "d".repeat(250)); 20].join(" ");
972 let edit = format!(
973 "\\r -> mapRoot (\\c -> c {{ files = c.files ++ [{{ path = [{}], content = blob \"deep\" }}] }}) (prev r)",
974 deep
975 );
976 let out = r.j(&[&edit]);
977 assert_eq!(out.code, 1, "{}", out.stderr);
978 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
979 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
980 assert!(out.stderr.contains("only partly updated"), "{}", out.stderr);
981 assert!(out.stderr.contains("`j undo` goes back"), "{}", out.stderr);
982 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
983 // the checkout wrote P's `a` before it failed
984 assert_eq!(r.read("a"), "p\n");
985 // which is not recorded into P, nor into X, which holds the edit the
986 // run recorded
987 r.j(&["id"]).ok();
988 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
989 let files = r.j(&[CONTENT_OF_A]).ok().stdout;
990 assert_eq!(files.trim(), r#"[["" (blob "")] ["P" (blob "p\n")] ["X" (blob "x2\n")]]"#);
991 r.j(&["undo"]).ok();
992 assert_eq!(r.j(&["log"]).ok().stdout, log);
995#[test]
996#[cfg(target_os = "linux")]
997fn an_undo_whose_checkout_fails_is_recorded_and_redone() {
998 // §7.7: `undo` and `redo` publish their operation before checking the
999 // restored focus out, as every run does (§7.5 step 7); a checkout that
1000 // fails leaves it recorded, and `j redo` goes back
1001 let r = setup();
1002 r.write("a", "one\n");
1003 // a file as deep as this workspace can hold (PATH_MAX, 4096 bytes with
1004 // the NUL), and no deeper
1005 let root = r.dir.canonicalize().unwrap();
1006 let n = (4095 - root.as_os_str().len() - "/f".len()) / 251;
1007 let deep: PathBuf = (0..n).map(|_| "d".repeat(250)).collect();
1008 std::fs::create_dir_all(r.dir.join(&deep)).unwrap();
1009 std::fs::write(r.dir.join(&deep).join("f"), "deep\n").unwrap();
1010 r.j(&["describe \"deep\""]).ok();
1011 r.j(&["new"]).ok();
1012 std::fs::remove_dir_all(r.dir.join("d".repeat(250))).unwrap();
1013 r.write("a", "two\n");
1014 r.j(&["id"]).ok();
1015 // 300 bytes further down, the workspace can no longer hold it
1016 let aside = r.dir.with_extension("aside");
1017 std::fs::rename(&r.dir, &aside).unwrap();
1018 let moved = r.dir.join("m".repeat(150)).join("m".repeat(150));
1019 std::fs::create_dir_all(moved.parent().unwrap()).unwrap();
1020 std::fs::rename(&aside, &moved).unwrap();
1021 let r = Repo { dir: moved, cfg: r.cfg.clone() };
1022 // `ops` prints each operation's age, so count them rather than compare
1023 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1024 let out = r.j(&["undo"]);
1025 assert_eq!(out.code, 1, "{}", out.stderr);
1026 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1027 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1028 assert!(out.stderr.contains("`j redo` goes back"), "{}", out.stderr);
1029 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1030 // the checkout wrote `a` before it failed, which the next run takes for
1031 // the restored focus's own
1032 assert_eq!(r.read("a"), "one\n");
1033 r.j(&["id"]).ok();
1034 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1035 r.j(&["redo"]).ok();
1036 assert_eq!(r.read("a"), "two\n");
1037 r.j(&["id"]).ok();
1038 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1041/// Path components, in the language's syntax, of a path under `first` far
1042/// longer than PATH_MAX, each a name a checkout can create (§7.5 step 1)
1043fn too_deep(first: &str) -> String {
1044 format!("\"{}\" {}", first, vec![format!("\"{}\"", "d".repeat(250)); 20].join(" "))
1047#[test]
1048fn a_failed_checkout_leaves_ignored_files_alone() {
1049 // §7.5 step 7: a failed checkout was put back, which scanned the whole
1050 // directory with the `.gitignore` the checkout had just written, and so
1051 // could read into the store, or delete, an ignored file; a checkout
1052 // that fails is now left as it is, for the next run to carry on from.
1053 // An ignored file put where the focus adds one the checkout did not
1054 // reach is left alone by the runs after it, which do not track it, and
1055 // by `j undo`, which takes back only what was written. (One there
1056 // before the run refuses it, before anything is recorded:
1057 // `a_checkout_writes_nothing_over_what_the_directory_does_not_track`.)
1058 let r = setup();
1059 r.write(".gitignore", "*.env\n");
1060 r.write("keep.txt", "k\n");
1061 r.j(&["describe \"base\""]).ok();
1062 let secret = "API_KEY=hunter2\n";
1063 r.write("secret.env", secret);
1064 let log = r.j(&["log"]).ok().stdout;
1065 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1066 // written in path order: `.gitignore`, `build/out`, then the deep path
1067 // under `c` fails, before `local.env`. Each record is parenthesised:
1068 // side by side, the second would update the first (§3).
1069 let edit = format!(
1070 "mapRoot (\\c -> c {{ files = filter (\\e -> e.path /= [\".gitignore\"]) c.files ++ [\
1071 ({{ path = [\".gitignore\"], content = blob \"*.env\\n*.log\\n\" }}) \
1072 ({{ path = [\"build\" \"out\"], content = blob \"built\\n\" }}) \
1073 ({{ path = [\"local.env\"], content = blob \"committed\\n\" }}) \
1074 ({{ path = [{}], content = blob \"deep\" }})] }})",
1075 too_deep("c")
1076 );
1077 let paths = r.j(&[&format!("\\r -> show (map (\\e -> e.path) (files (({}) r)))", edit)]).ok().stdout;
1078 let paths = paths.split_whitespace().collect::<Vec<_>>().join(" ");
1079 assert!(paths.starts_with(r#"[["keep.txt"] [".gitignore"] ["build" "out"] ["local.env"] ["c" "#), "{}", paths);
1080 let out = r.j(&[&edit]);
1081 assert_eq!(out.code, 1, "{}", out.stderr);
1082 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1083 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1084 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1085 assert_eq!(r.read(".gitignore"), "*.env\n*.log\n");
1086 assert_eq!(r.read("build/out"), "built\n");
1087 assert_eq!(r.read("secret.env"), secret);
1088 assert!(!r.dir.join("local.env").exists());
1089 r.write("local.env", "mine\n");
1090 for run in ["log", "id"] {
1091 r.j(&[run]).ok();
1092 assert!(!has_blob(&r, secret));
1093 assert!(!has_blob(&r, "mine\n"));
1095 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1096 r.j(&["undo"]).ok();
1097 assert_eq!(r.read(".gitignore"), "*.env\n");
1098 assert!(!r.dir.join("build").exists());
1099 assert_eq!(r.read("secret.env"), secret);
1100 assert_eq!(r.read("local.env"), "mine\n");
1101 assert!(!has_blob(&r, secret));
1102 assert!(!has_blob(&r, "mine\n"));
1103 r.j(&["id"]).ok();
1104 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1105 assert_eq!(r.j(&["log"]).ok().stdout, log);
1108#[test]
1109fn undoing_a_failed_checkout_keeps_the_ignored_files_where_it_wrote() {
1110 // §7.5 step 7, §7.7: `j undo` after a failed checkout removes the files
1111 // it added and, as any checkout does, the directories that leaves
1112 // empty; one holding an ignored file is not, and stays, file and all
1113 let r = setup();
1114 r.write(".gitignore", "*.log\n");
1115 r.write("keep.txt", "k\n");
1116 r.j(&["describe \"base\""]).ok();
1117 std::fs::create_dir(r.dir.join("logs")).unwrap();
1118 r.write("logs/old.log", "old\n");
1119 let log = r.j(&["log"]).ok().stdout;
1120 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1121 // `logs/x` is written, then the deep path fails
1122 let edit = format!(
1123 "mapRoot (\\c -> c {{ files = c.files ++ [\
1124 ({{ path = [\"logs\" \"x\"], content = blob \"x\\n\" }}) \
1125 ({{ path = [{}], content = blob \"deep\" }})] }})",
1126 too_deep("zz")
1127 );
1128 let out = r.j(&[&edit]);
1129 assert_eq!(out.code, 1, "{}", out.stderr);
1130 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1131 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1132 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1133 assert_eq!(r.read("logs/x"), "x\n");
1134 r.j(&["undo"]).ok();
1135 assert!(!r.dir.join("logs/x").exists());
1136 assert_eq!(r.read("logs/old.log"), "old\n");
1137 r.j(&["id"]).ok();
1138 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1139 assert_eq!(r.j(&["log"]).ok().stdout, log);
1142/// The entries of `.jj` a checkout moved files aside into (`clear_the_way`)
1143fn aside_in_jj(r: &Repo) -> Vec<String> {
1144 std::fs::read_dir(r.dir.join(".jj"))
1145 .unwrap()
1146 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
1147 .filter(|name| name.starts_with("aside"))
1148 .collect()
1151#[test]
1152fn a_failed_checkout_puts_back_the_ignored_file_it_was_to_write_again() {
1153 // §7.4, §7.5 step 7: an untracked file holding just what the checkout
1154 // writes at a path the focus adds was removed before the checkout, to
1155 // be written again; a checkout that failed before it got there left it
1156 // removed, and `j undo` did not bring it back. It is moved aside now,
1157 // and put back where the checkout did not write.
1158 let r = setup();
1159 r.write(".gitignore", "*.env\n");
1160 r.write("keep.txt", "k\n");
1161 r.j(&["describe \"base\""]).ok();
1162 r.write("local.env", "A=1\n");
1163 let log = r.j(&["log"]).ok().stdout;
1164 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1165 // the deep path under `a` fails, before `local.env`
1166 let edit = format!(
1167 "mapRoot (\\c -> c {{ files = c.files ++ [\
1168 ({{ path = [{}], content = blob \"deep\" }}) \
1169 ({{ path = [\"local.env\"], content = blob \"A=1\\n\" }})] }})",
1170 too_deep("a")
1171 );
1172 let out = r.j(&[&edit]);
1173 assert_eq!(out.code, 1, "{}", out.stderr);
1174 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1175 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1176 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1177 assert_eq!(r.read("local.env"), "A=1\n");
1178 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1179 r.j(&["id"]).ok();
1180 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1181 r.j(&["undo"]).ok();
1182 assert_eq!(r.read("local.env"), "A=1\n");
1183 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1184 assert_eq!(r.j(&["log"]).ok().stdout, log);
1185 // a checkout that completes writes it
1186 let edit = "mapRoot (\\c -> c { files = c.files ++ [{ path = [\"local.env\"], content = blob \"A=1\\n\" }] })";
1187 r.j(&[edit]).ok();
1188 assert_eq!(r.read("local.env"), "A=1\n");
1189 assert_eq!(r.j(&["\\r -> text (contentAt [\"local.env\"] (files r))"]).ok().stdout, "A=1\n");
1190 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1193#[test]
1194#[cfg(unix)]
1195fn a_checkout_names_the_file_it_cannot_move_out_of_its_way() {
1196 // §7.4: an untracked file holding just what the checkout writes at a
1197 // path the focus adds, which could not be removed for it, made jj skip
1198 // the path, and the checkout failed saying only that files were not
1199 // written, naming none, on every run that persisted. It names the file
1200 // now, and why it could not be moved. (A directory the user cannot
1201 // write to holds it here; root can.)
1202 if unsafe { libc::geteuid() } == 0 {
1203 return;
1205 use std::os::unix::fs::PermissionsExt;
1206 let r = setup();
1207 r.write(".gitignore", "*.env\n");
1208 r.write("keep.txt", "k\n");
1209 r.j(&["describe \"A\""]).ok();
1210 let adds = "mapRoot (\\c -> c { files = c.files ++ [{ path = [\"etc\" \"local.env\"], content = blob \"A=1\\n\" }] })";
1211 r.j(&[&format!("describe \"B\" . {} . new", adds)]).ok();
1212 r.j(&[&new_above("A")]).ok();
1213 std::fs::create_dir(r.dir.join("etc")).unwrap();
1214 r.write("etc/local.env", "A=1\n");
1215 let mode = |m| std::fs::set_permissions(r.dir.join("etc"), std::fs::Permissions::from_mode(m)).unwrap();
1216 mode(0o555);
1217 let out = r.j(&[&new_above("B")]);
1218 let again = r.j(&["describe \"again\""]);
1219 mode(0o755);
1220 for out in [out, again] {
1221 assert_eq!(out.code, 1, "{}", out.stderr);
1222 assert!(out.stderr.contains("`etc/local.env` was not written"), "{}", out.stderr);
1223 assert!(out.stderr.contains("could not be moved"), "{}", out.stderr);
1224 assert!(out.stderr.contains("Permission denied"), "{}", out.stderr);
1226 assert_eq!(r.read("etc/local.env"), "A=1\n");
1227 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1228 // once the directory lets it be moved, the run completes the checkout
1229 r.j(&["describe \"done\""]).ok();
1230 assert_eq!(r.read("etc/local.env"), "A=1\n");
1231 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1232 r.j(&["id"]).ok();
1233 let text = "\\r -> text (contentAt [\"etc\" \"local.env\"] (files r))";
1234 assert_eq!(r.j(&[text]).ok().stdout, "A=1\n");
1237/// An expression that goes to a new commit above the one whose message is
1238/// `message`
1239fn new_above(message: &str) -> String {
1240 format!("new . goto (matching (\\c -> c.message == \"{}\") all)", message)
1243/// The focus's paths
1244const FOCUS_PATHS: &str = "\\r -> show (map (\\e -> e.path) (files r))";
1246#[test]
1247#[cfg(unix)]
1248fn a_checkout_writes_nothing_over_what_the_directory_does_not_track() {
1249 // §7.4, §7.5 step 7: jj's checkout writes no file where something
1250 // stands already, and skips the path without failing, recording it as
1251 // written, so the next run read what stood there into the focus: an
1252 // ignored file (a secret, say) in place of the focus's content. The
1253 // run now refuses before anything is recorded or written, naming what
1254 // is in the way where the focus adds a file: an ignored symlink above
1255 // it, an ignored file, a directory in its place holding an ignored
1256 // file, each of them in one refusal.
1257 let r = setup();
1258 r.write(".gitignore", "*.env\nlnk\n");
1259 r.write("keep.txt", "k\n");
1260 r.j(&["describe \"A\""]).ok();
1261 let adds = "mapRoot (\\c -> c { files = c.files ++ [({ path = [\"lnk\" \"f\"], content = blob \"f\\n\" }) \
1262 ({ path = [\"local.env\"], content = blob \"COMMITTED=1\\n\" }) \
1263 ({ path = [\"out\"], content = blob \"out\\n\" })] })";
1264 r.j(&[&format!("describe \"B\" . {} . new", adds)]).ok();
1265 r.j(&[&new_above("A")]).ok();
1266 assert!(!r.dir.join("local.env").exists());
1267 // outside the working directory, and removed with the repository
1268 let outside = r.cfg.join("outside");
1269 std::fs::create_dir(&outside).unwrap();
1270 std::os::unix::fs::symlink(&outside, r.dir.join("lnk")).unwrap();
1271 let secret = "SECRET=hunter2\n";
1272 r.write("local.env", secret);
1273 std::fs::create_dir_all(r.dir.join("out/sub")).unwrap();
1274 r.write("out/junk.env", "junk\n");
1275 let log = r.j(&["log"]).ok().stdout;
1276 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1277 let refused = |says: &[&str]| {
1278 let out = r.j(&[&new_above("B")]);
1279 assert_eq!(out.code, 1, "{}", out.stderr);
1280 for said in says {
1281 assert!(out.stderr.contains(said), "{}", out.stderr);
1283 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
1284 assert_eq!(r.j(&["log"]).ok().stdout, log);
1285 assert!(!has_blob(&r, secret));
1286 };
1287 let out_dir = "`out` (a directory holding untracked files, such as `out/junk.env`)";
1288 refused(&["in the way of files it has: `lnk`, `local.env` and ", out_dir, "; move them aside"]);
1289 assert_eq!(std::fs::read_dir(&outside).unwrap().count(), 0);
1290 std::fs::remove_file(r.dir.join("lnk")).unwrap();
1291 refused(&["in the way of files it has: `local.env` and ", out_dir, "; move them aside"]);
1292 assert_eq!(r.read("local.env"), secret);
1293 r.j(&["id"]).ok();
1294 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
1295 assert!(!has_blob(&r, secret));
1296 let aside = r.cfg.join("local.env");
1297 std::fs::rename(r.dir.join("local.env"), &aside).unwrap();
1298 refused(&[
1299 "it has `out`, and `out` in the working directory is a directory holding untracked files, such as `out/junk.env`; move it aside",
1300 ]);
1301 assert_eq!(r.read("out/junk.env"), "junk\n");
1302 // with those moved aside, B's files are written, over the empty
1303 // directories left in `out`
1304 std::fs::remove_file(r.dir.join("out/junk.env")).unwrap();
1305 r.j(&[&new_above("B")]).ok();
1306 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1307 assert_eq!(r.read("lnk/f"), "f\n");
1308 assert_eq!(r.read("local.env"), "COMMITTED=1\n");
1309 assert_eq!(r.read("out"), "out\n");
1310 r.j(&["id"]).ok();
1311 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1312 let paths = r.j(&[FOCUS_PATHS]).ok().stdout;
1313 assert_eq!(
1314 paths.split_whitespace().collect::<Vec<_>>().join(" "),
1315 r#"[[".gitignore"] ["keep.txt"] ["lnk" "f"] ["local.env"] ["out"]]"#
1316 );
1317 assert_eq!(std::fs::read_to_string(&aside).unwrap(), secret);
1318 assert!(!has_blob(&r, secret));
1319 // `undo` checks out as persistence does (§7.7)
1320 r.j(&[&new_above("A")]).ok();
1321 std::fs::rename(&aside, r.dir.join("local.env")).unwrap();
1322 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1323 let out = r.j(&["undo"]);
1324 assert_eq!(out.code, 1, "{}", out.stderr);
1325 assert!(out.stderr.contains("an untracked `local.env` in the working directory is in its way; move it aside"), "{}", out.stderr);
1326 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
1327 assert_eq!(r.read("local.env"), secret);
1328 std::fs::rename(r.dir.join("local.env"), &aside).unwrap();
1329 r.j(&["undo"]).ok();
1330 assert_eq!(r.read("local.env"), "COMMITTED=1\n");
1331 assert!(!has_blob(&r, secret));
1334#[test]
1335fn a_refused_checkout_names_all_that_is_in_its_way() {
1336 // §7.4: a checkout refused over what the working directory does not
1337 // track named only the first path in its way, so a directory of build
1338 // outputs took a run per file to clear; and for a directory standing
1339 // where the focus has a file, it named a file in it, the first a read
1340 // of the directory found, and said to move that aside. The refusal
1341 // lists what is in the way now, ten paths and how many more, and a
1342 // directory by its own path.
1343 let r = setup();
1344 let dist = |what: &str| {
1345 std::fs::create_dir(r.dir.join("dist")).unwrap();
1346 for i in 1..=12 {
1347 r.write(&format!("dist/m{:02}.js", i), &format!("{} {}\n", what, i));
1349 };
1350 dist("release");
1351 r.write("build", "#!/bin/sh\n");
1352 r.write("keep", "k\n");
1353 r.j(&["describe \"v1\""]).ok();
1354 r.j(&["new"]).ok();
1355 std::fs::remove_dir_all(r.dir.join("dist")).unwrap();
1356 std::fs::remove_file(r.dir.join("build")).unwrap();
1357 std::fs::create_dir(r.dir.join("build")).unwrap();
1358 r.write("build/main.c", "c\n");
1359 r.write(".gitignore", "dist/\n*.o\n");
1360 r.j(&["describe \"v2\""]).ok();
1361 r.j(&["new"]).ok();
1362 // a local build
1363 dist("dev");
1364 r.write("build/1.o", "o\n");
1365 r.write("build/2.o", "o\n");
1366 let log = r.j(&["log"]).ok().stdout;
1367 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1368 let out = r.j(&[&new_above("v1")]);
1369 assert_eq!(out.code, 1, "{}", out.stderr);
1370 let listed = "in the way of files it has: `build` (a directory holding untracked files, such as `build/";
1371 assert!(out.stderr.contains(listed), "{}", out.stderr);
1372 for i in 1..=9 {
1373 assert!(out.stderr.contains(&format!(", `dist/m{:02}.js`", i)), "{}", out.stderr);
1375 assert!(!out.stderr.contains("m10"), "{}", out.stderr);
1376 assert!(out.stderr.contains("`dist/m09.js` and 3 more; move them aside"), "{}", out.stderr);
1377 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
1378 assert_eq!(r.j(&["log"]).ok().stdout, log);
1379 assert_eq!(r.read("dist/m12.js"), "dev 12\n");
1380 assert_eq!(r.read("build/2.o"), "o\n");
1381 // with them moved aside, v1's files are written
1382 std::fs::remove_dir_all(r.dir.join("dist")).unwrap();
1383 std::fs::remove_file(r.dir.join("build/1.o")).unwrap();
1384 std::fs::remove_file(r.dir.join("build/2.o")).unwrap();
1385 r.j(&[&new_above("v1")]).ok();
1386 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1387 assert_eq!(r.read("build"), "#!/bin/sh\n");
1388 assert_eq!(r.read("dist/m12.js"), "release 12\n");
1391#[test]
1392fn a_checkout_clears_the_empty_directories_where_it_writes_a_file() {
1393 // §7.4: a checkout writes no file over a directory, and skips it
1394 // without failing: where a tool had left empty directories, the next
1395 // run took the focus's file for deleted. Only a checkout over a stale
1396 // working copy cleared such directories; every checkout does now,
1397 // those in a directory the focus replaces by a file included.
1398 let r = setup();
1399 r.write("build", "b\n");
1400 r.write("keep", "k\n");
1401 r.j(&["describe \"P\""]).ok();
1402 r.j(&["new"]).ok();
1403 std::fs::remove_file(r.dir.join("build")).unwrap();
1404 r.j(&["describe \"Q\""]).ok();
1405 std::fs::create_dir_all(r.dir.join("build/sub/deeper")).unwrap();
1406 r.j(&["new . goto parents"]).ok();
1407 assert_eq!(r.read("build"), "b\n");
1408 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1409 r.j(&["id"]).ok();
1410 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
1411 assert_eq!(r.j(&[FOCUS_PATHS]).ok().stdout.trim(), r#"[["build"] ["keep"]]"#);
1412 // `lib` holds the tracked `lib/x` and empty directories, and the focus
1413 // has the file `lib`
1414 r.write("lib", "lib\n");
1415 r.j(&["describe \"L\""]).ok();
1416 r.j(&["new"]).ok();
1417 std::fs::remove_file(r.dir.join("lib")).unwrap();
1418 std::fs::create_dir(r.dir.join("lib")).unwrap();
1419 r.write("lib/x", "x\n");
1420 r.j(&["describe \"D\""]).ok();
1421 std::fs::create_dir_all(r.dir.join("lib/e/f")).unwrap();
1422 r.j(&[&new_above("L")]).ok();
1423 assert_eq!(r.read("lib"), "lib\n");
1424 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1425 r.j(&["id"]).ok();
1426 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
1427 assert_eq!(r.j(&[FOCUS_PATHS]).ok().stdout.trim(), r#"[["build"] ["keep"] ["lib"]]"#);
1430#[test]
1431#[cfg(unix)]
1432fn a_checkout_clears_nothing_through_a_symlink() {
1433 // §7.4: clearing the empty directories where a checkout over a stale
1434 // working copy writes a file looked at each path through the symlinks
1435 // above it: `x`, a symlink to a directory outside the working directory
1436 // that the focus has as a directory, had it remove an empty tree the
1437 // symlink's target held
1438 let r = setup();
1439 // outside the working directory, and removed with the repository
1440 let outside = r.cfg.join("outside");
1441 std::fs::create_dir_all(outside.join("y/z/deeper")).unwrap();
1442 r.write("keep", "k\n");
1443 std::os::unix::fs::symlink(&outside, r.dir.join("x")).unwrap();
1444 r.j(&["describe \"A\""]).ok();
1445 r.j(&["new"]).ok();
1446 std::fs::remove_file(r.dir.join("x")).unwrap();
1447 std::fs::create_dir_all(r.dir.join("x/y")).unwrap();
1448 r.write("x/y/z", "z\n");
1449 r.j(&["describe \"B\""]).ok();
1450 r.j(&[&new_above("A")]).ok();
1451 assert!(r.dir.join("x").symlink_metadata().unwrap().is_symlink());
1452 let saved: Vec<(PathBuf, Vec<u8>)> = std::fs::read_dir(r.dir.join(".jj/working_copy"))
1453 .unwrap()
1454 .map(|e| e.unwrap().path())
1455 .map(|p| (p.clone(), std::fs::read(p).unwrap()))
1456 .collect();
1457 r.j(&[&new_above("B")]).ok();
1458 assert!(outside.join("y/z/deeper").is_dir());
1459 assert_eq!(r.read("x/y/z"), "z\n");
1460 // the same over a stale working copy, as jj leaves it when it moves the
1461 // working-copy commit without updating the working directory
1462 for (path, bytes) in saved {
1463 std::fs::write(path, bytes).unwrap();
1465 std::fs::remove_dir_all(r.dir.join("x")).unwrap();
1466 std::os::unix::fs::symlink(&outside, r.dir.join("x")).unwrap();
1467 r.j(&["describe \"B2\""]).ok();
1468 assert!(outside.join("y/z/deeper").is_dir());
1469 assert_eq!(r.read("x/y/z"), "z\n");
1470 assert!(r.dir.join("x").symlink_metadata().unwrap().is_dir());
1473/// Run `edit`, whose checkout writes `m/0` … `m/<n-1>` and more after them,
1474/// and call `meanwhile` with the process stopped while it writes `m`, once
1475/// its operation is published
1476#[cfg(target_os = "linux")]
1477fn stopped_mid_checkout(r: &Repo, edit: &str, n: usize, meanwhile: impl FnOnce()) -> Out {
1478 stopped_mid_checkout_as(r, edit, n, |_| meanwhile())
1481/// `stopped_mid_checkout`, calling `meanwhile` with the stopped process's
1482/// id, which may signal it. The process ignores SIGXFSZ, so that a write
1483/// past a file size limit set on it fails (EFBIG) rather than killing it,
1484/// and dumps no core. A process a signal ended has as its code minus that
1485/// signal's number.
1486#[cfg(target_os = "linux")]
1487fn stopped_mid_checkout_as(r: &Repo, edit: &str, n: usize, meanwhile: impl FnOnce(libc::pid_t)) -> Out {
1488 use std::os::unix::process::{CommandExt, ExitStatusExt};
1489 let mut command = Command::new(j_bin());
1490 command
1491 .arg(edit)
1492 .current_dir(&r.dir)
1493 .env("XDG_CONFIG_HOME", &r.cfg)
1494 .env("NO_COLOR", "1")
1495 .stdout(Stdio::piped())
1496 .stderr(Stdio::piped());
1497 // an ignored signal stays ignored across exec
1498 unsafe {
1499 command.pre_exec(|| {
1500 libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
1501 libc::setrlimit(libc::RLIMIT_CORE, &libc::rlimit { rlim_cur: 0, rlim_max: 0 });
1502 Ok(())
1503 });
1505 let mut child = command.spawn().unwrap();
1506 let pid = child.id() as libc::pid_t;
1507 // `m` is written in path order, `0` then `1`, each file created empty
1508 // and removed once before it is written
1509 let (first, second) = (r.dir.join("m").join("0"), r.dir.join("m").join("1"));
1510 while !second.exists() {
1511 assert!(child.try_wait().unwrap().is_none(), "exited before writing m/1");
1512 std::thread::yield_now();
1514 let mut status = 0;
1515 unsafe {
1516 libc::kill(pid, libc::SIGSTOP);
1517 assert_eq!(libc::waitpid(pid, &mut status, libc::WUNTRACED), pid);
1519 assert!(libc::WIFSTOPPED(status), "exited before it was stopped");
1520 // all of `m` is written before anything after it
1521 let written = std::fs::read_dir(r.dir.join("m")).unwrap().count();
1522 let in_time = first.exists() && written < n;
1523 let done = if in_time {
1524 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| meanwhile(pid)))
1525 } else {
1526 Ok(())
1527 };
1528 unsafe { libc::kill(pid, if in_time { libc::SIGCONT } else { libc::SIGKILL }) };
1529 let out = child.wait_with_output().unwrap();
1530 assert!(in_time, "stopped too late: {} of {} written", written, n);
1531 if let Err(e) = done {
1532 std::panic::resume_unwind(e);
1534 Out {
1535 code: out.status.code().unwrap_or_else(|| -out.status.signal().unwrap()),
1536 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
1537 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
1541/// An edit giving the focus `files` (the language's syntax, over the focus
1542/// `c`), `m/0` … `m/<n-1>`, and a path the checkout fails on after those
1543#[cfg(target_os = "linux")]
1544fn slow_failing_edit(files: &str, n: usize) -> String {
1545 format!(
1546 "mapRoot (\\c -> c {{ files = {} ++ map (\\i -> {{ path = [\"m\" (show i)], content = blob \"m\\n\" }}) (range 0 {}) ++ [{{ path = [{}], content = blob \"deep\" }}] }})",
1547 files,
1548 n,
1549 too_deep("zz")
1553/// An edit giving the focus `files` (the language's syntax, over the focus
1554/// `c`), the empty files `m/0` … `m/<n-1>`, and `zz`, which is not empty:
1555/// a file created is written in full, and only `zz` is cut short by a
1556/// full disk (`fill_disk`)
1557#[cfg(target_os = "linux")]
1558fn slow_edit(files: &str, n: usize) -> String {
1559 format!(
1560 "mapRoot (\\c -> c {{ files = {} ++ map (\\i -> {{ path = [\"m\" (show i)], content = blob \"\" }}) (range 0 {}) ++ [{{ path = [\"zz\"], content = blob \"z\\n\" }}] }})",
1561 files, n
1565/// Leave the stopped process `pid` no room to write: from here on each
1566/// write that would make a file longer than nothing fails, as on a full
1567/// disk, while it can still create, empty and remove files
1568#[cfg(target_os = "linux")]
1569fn fill_disk(pid: libc::pid_t) {
1570 let none = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
1571 assert_eq!(unsafe { libc::prlimit(pid, libc::RLIMIT_FSIZE, &none, std::ptr::null_mut()) }, 0);
1574/// The number of files in `m`
1575#[cfg(target_os = "linux")]
1576fn in_m(r: &Repo) -> usize {
1577 std::fs::read_dir(r.dir.join("m")).map_or(0, |d| d.count())
1580#[test]
1581#[cfg(target_os = "linux")]
1582fn an_interrupted_checkout_is_carried_on_by_the_next_run() {
1583 // §1.3, §7.5 step 7: the checkout ran before the operation was
1584 // published, so a run killed part way through it (SIGKILL here; a crash,
1585 // a power cut) left the working copy at the commit the user was on,
1586 // and the next run recorded into that commit the files the checkout had
1587 // written. The operation is now published first: the next run takes
1588 // those files for the focus's own, records only what changed meanwhile,
1589 // and a run that changes the repository finishes the checkout.
1590 let r = setup();
1591 r.write("keep.txt", "k\n");
1592 r.j(&["describe \"base\""]).ok();
1593 r.j(&["new"]).ok();
1594 r.write("work.txt", "my work\n");
1595 r.j(&["describe \"W\""]).ok();
1596 r.write("work.txt", "my work, edited\n");
1597 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1598 let n = 2000;
1599 // S, a new child of base: `work.txt`, after `m`, is still there when
1600 // the run is killed
1601 let edit = format!("describe \"S\" . ({}) . new . prev", slow_edit("c.files", n));
1602 let out = stopped_mid_checkout_as(&r, &edit, n, |pid| {
1603 r.write("mine.txt", "mine\n");
1604 unsafe { libc::kill(pid, libc::SIGKILL) };
1605 });
1606 assert_eq!(out.code, -libc::SIGKILL, "{}", out.stderr);
1607 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1608 assert!(in_m(&r) < n);
1609 assert_eq!(r.read("work.txt"), "my work, edited\n");
1610 // W holds the edit, and S only what was made meanwhile
1611 let paths = "\\r -> show (map (\\m -> map (\\e -> e.path) (restrict (neg (under ./m)) \
1612 (focus (goto (matching (\\c -> c.message == m) all) r)).files)) [\"base\" \"W\" \"S\"])";
1613 let paths = r.j(&[paths]).ok().stdout;
1614 assert_eq!(
1615 paths.split_whitespace().collect::<Vec<_>>().join(" "),
1616 r#"[[["keep.txt"]] [["keep.txt"] ["work.txt"]] [["keep.txt"] ["mine.txt"] ["zz"]]]"#
1617 );
1618 let text = "\\r -> text (contentAt [\"work.txt\"] (files (goto (matching (\\c -> c.message == \"W\") all) r)))";
1619 assert_eq!(r.j(&[text]).ok().stdout, "my work, edited\n");
1620 r.j(&["describe \"S2\""]).ok();
1621 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1622 assert!(!r.dir.join("work.txt").exists());
1623 assert_eq!(in_m(&r), n);
1624 assert_eq!(r.read("zz"), "z\n");
1625 assert_eq!(r.read("mine.txt"), "mine\n");
1626 r.j(&["id"]).ok();
1627 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1630#[test]
1631#[cfg(target_os = "linux")]
1632fn a_checkout_asked_to_stop_completes_before_the_run_stops() {
1633 // §1.3: with the operation recorded before its checkout, a run stopped
1634 // part way through the checkout by Ctrl-C (SIGINT), SIGTERM, SIGHUP or
1635 // SIGQUIT left the working directory holding neither the old focus nor
1636 // the new one. Such a signal now waits until the checkout and the
1637 // working copy's state are saved, and then stops the run as it would
1638 // have: the directory holds the focus, and nothing is left to carry on.
1639 for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT] {
1640 let r = setup();
1641 r.write("keep.txt", "k\n");
1642 r.j(&["describe \"base\""]).ok();
1643 r.j(&["new"]).ok();
1644 r.write("work.txt", "my work\n");
1645 r.j(&["describe \"W\""]).ok();
1646 r.write("work.txt", "my work, edited\n");
1647 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1648 let n = 2000;
1649 // S, a new child of base: the checkout removes `work.txt` after it
1650 // has written `m`
1651 let edit = format!("describe \"S\" . ({}) . new . prev", slow_edit("c.files", n));
1652 let out = stopped_mid_checkout_as(&r, &edit, n, |pid| {
1653 assert_eq!(unsafe { libc::kill(pid, sig) }, 0);
1654 });
1655 assert_eq!(out.code, -sig, "signal {}: {}", sig, out.stderr);
1656 assert_eq!(in_m(&r), n, "signal {}", sig);
1657 assert_eq!(r.read("zz"), "z\n");
1658 assert!(!r.dir.join("work.txt").exists());
1659 assert_eq!(r.read("keep.txt"), "k\n");
1660 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1661 r.j(&["id"]).ok();
1662 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1, "signal {}", sig);
1663 let texts = r.j(&[&texts_by_message(&["[\"work.txt\"]"], &["W"])]).ok().stdout;
1664 assert_eq!(
1665 texts.split_whitespace().collect::<Vec<_>>().join(" "),
1666 r#"[["my work, edited\n"]]"#
1667 );
1671#[test]
1672#[cfg(target_os = "linux")]
1673fn a_failed_checkout_keeps_what_changed_meanwhile() {
1674 // §7.5 step 7: the put-back rescanned the whole directory, so a file
1675 // edited or created while the checkout ran could be taken back with
1676 // what the checkout wrote. Nothing is put back now, and the next run
1677 // records what changed meanwhile into the focus, beside what the
1678 // checkout wrote and the file the full disk cut short.
1679 let r = setup();
1680 r.write("keep.txt", "k\n");
1681 r.j(&["describe \"base\""]).ok();
1682 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1683 let n = 2000;
1684 let out = stopped_mid_checkout_as(&r, &slow_edit("c.files", n), n, |pid| {
1685 fill_disk(pid);
1686 r.write("keep.txt", "edited meanwhile\n");
1687 r.write("mine.txt", "mine\n");
1688 std::fs::create_dir(r.dir.join("notes")).unwrap();
1689 r.write("notes/todo.md", "my notes\n");
1690 });
1691 assert_eq!(out.code, 1, "{}", out.stderr);
1692 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1693 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1694 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1695 assert_eq!(r.read("keep.txt"), "edited meanwhile\n");
1696 assert_eq!(r.read("mine.txt"), "mine\n");
1697 assert_eq!(r.read("notes/todo.md"), "my notes\n");
1698 r.j(&["id"]).ok();
1699 let texts = "\\r -> show (map (\\e -> [e.path (text e.content)]) (restrict (neg (under ./m)) (files r)))";
1700 let texts = r.j(&[texts]).ok().stdout;
1701 let texts = texts.split_whitespace().collect::<Vec<_>>().join(" ");
1702 assert!(
1703 texts.starts_with(r#"[[["keep.txt"] "edited meanwhile\n"] [["mine.txt"] "mine\n"] [["notes" "todo.md"] "my notes\n"] [["zz"] "#),
1704 "{}",
1705 texts
1706 );
1707 assert_eq!(in_m(&r), n);
1708 r.j(&["id"]).ok();
1709 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1712#[test]
1713#[cfg(target_os = "linux")]
1714fn a_failed_checkout_keeps_what_was_saved_meanwhile_where_it_wrote() {
1715 // §7.5 step 7: the put-back gave each path the focus changes back what
1716 // the directory held there, so a file saved meanwhile at such a path
1717 // after the checkout wrote it, or at a path the focus adds before the
1718 // checkout got there, was lost unless the put-back recognised what the
1719 // checkout had written. Nothing is put back now: the next run takes the
1720 // directory for a change to what it held when the checkout began, and
1721 // each such file is a conflict with the focus's content, neither lost.
1722 // So is the file the full disk cut short. (A file the focus changes,
1723 // saved before the checkout reaches it, is written over, as in jj.)
1724 let r = setup();
1725 r.write("a.txt", "a\n");
1726 r.write("b.txt", "b\n");
1727 r.j(&["describe \"base\""]).ok();
1728 r.write("b.txt", "b edited\n");
1729 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1730 let n = 2000;
1731 // `a.txt` and `b.txt` are written before `m` and saved over after;
1732 // `n.txt`, which the focus adds after `m`, is saved before the
1733 // checkout gets there; then `zz` fails on the full disk
1734 let files = "[({ path = [\"a.txt\"], content = blob \"focus a\\n\" }) \
1735 ({ path = [\"b.txt\"], content = blob \"focus b\\n\" }) \
1736 ({ path = [\"n.txt\"], content = blob \"focus n\\n\" })]";
1737 let out = stopped_mid_checkout_as(&r, &slow_edit(files, n), n, |pid| {
1738 fill_disk(pid);
1739 r.write("a.txt", "mine a\n");
1740 r.write("b.txt", "mine b\n");
1741 r.write("n.txt", "mine n\n");
1742 });
1743 assert_eq!(out.code, 1, "{}", out.stderr);
1744 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1745 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1746 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1747 assert_eq!(r.read("a.txt"), "mine a\n");
1748 assert_eq!(r.read("b.txt"), "mine b\n");
1749 assert_eq!(r.read("n.txt"), "mine n\n");
1750 assert_eq!(r.read("zz"), "");
1751 r.j(&["id"]).ok();
1752 let conflicted = r.j(&["\\r -> show (conflicted (files r))"]).ok().stdout;
1753 assert_eq!(conflicted.trim(), r#"[["a.txt"] ["b.txt"] ["n.txt"] ["zz"]]"#);
1754 for (path, mine, focus) in [("a.txt", "mine a", "focus a"), ("b.txt", "mine b", "focus b"), ("n.txt", "mine n", "focus n")] {
1755 let text = r.read(path);
1756 assert!(text.contains(mine) && text.contains(focus), "{}: {}", path, text);
1758 assert!(r.read("zz").contains("z\n"), "{}", r.read("zz"));
1759 assert_eq!(in_m(&r), n);
1760 r.j(&["id"]).ok();
1761 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1764/// Whether the repository's git store holds `content` as a loose object,
1765/// as each object j writes is
1766fn has_blob(r: &Repo, content: &str) -> bool {
1767 let id = gix::objs::compute_hash(gix::hash::Kind::Sha1, gix::objs::Kind::Blob, content.as_bytes())
1768 .unwrap()
1769 .to_string();
1770 r.dir.join(".git/objects").join(&id[..2]).join(&id[2..]).exists()
1773#[test]
1774#[cfg(target_os = "linux")]
1775fn an_ignored_file_saved_meanwhile_where_the_focus_adds_one_is_left_alone() {
1776 // §7.5 step 7: an ignored file saved meanwhile at a path the focus adds
1777 // is left alone. The put-back knew the file by its inode, so one an
1778 // editor saved meanwhile by renaming a new file over the one the
1779 // checkout wrote was taken for the checkout's own, read into the store
1780 // and deleted. Neither the failed checkout, which writes no file over
1781 // one saved before it reached the path, nor the runs after it read or
1782 // remove such a file; those the checkout wrote stay, ignored. (One
1783 // there before the run refuses it, before anything is recorded:
1784 // `a_checkout_writes_nothing_over_what_the_directory_does_not_track`.)
1785 let r = setup();
1786 r.write(".gitignore", "*.env\n");
1787 r.write("keep.txt", "k\n");
1788 r.j(&["describe \"base\""]).ok();
1789 let log = r.j(&["log"]).ok().stdout;
1790 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1791 let n = 2000;
1792 // the checkout writes `k.env` and `local.env` before `m`, and `o.env`
1793 // and `p.env` after it
1794 let files = "c.files ++ [({ path = [\"k.env\"], content = blob \"committed k\\n\" }) \
1795 ({ path = [\"local.env\"], content = blob \"committed\\n\" }) \
1796 ({ path = [\"o.env\"], content = blob \"committed o\\n\" }) \
1797 ({ path = [\"p.env\"], content = blob \"committed p\\n\" })]";
1798 let saved = "SECRET=v2, saved meanwhile\n";
1799 let out = stopped_mid_checkout(&r, &slow_failing_edit(files, n), n, || {
1800 r.write("local.env.tmp", saved);
1801 std::fs::rename(r.dir.join("local.env.tmp"), r.dir.join("local.env")).unwrap();
1802 r.write("o.env.tmp", "committed");
1803 std::fs::rename(r.dir.join("o.env.tmp"), r.dir.join("o.env")).unwrap();
1804 });
1805 assert_eq!(out.code, 1, "{}", out.stderr);
1806 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
1807 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1808 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1809 let unchanged = |r: &Repo| {
1810 assert_eq!(r.read("k.env"), "committed k\n");
1811 assert_eq!(r.read("local.env"), saved);
1812 assert_eq!(r.read("o.env"), "committed");
1813 assert_eq!(r.read("p.env"), "committed p\n");
1814 assert!(!has_blob(r, saved));
1815 assert!(!has_blob(r, "committed"));
1816 };
1817 unchanged(&r);
1818 // the run wrote the focus's objects before its checkout
1819 assert!(has_blob(&r, "committed p\n"));
1820 r.j(&["log"]).ok();
1821 unchanged(&r);
1822 r.j(&["undo"]).ok();
1823 unchanged(&r);
1824 assert!(!r.dir.join("m").exists());
1825 r.j(&["id"]).ok();
1826 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1827 assert_eq!(r.j(&["log"]).ok().stdout, log);
1830#[test]
1831#[cfg(target_os = "linux")]
1832fn an_ignored_file_put_where_a_checkout_cut_short_adds_one_is_not_read() {
1833 // §7.4, §7.5 step 7: the next run that persisted after a checkout cut
1834 // short completed it, and jj skipped the path where an ignored file had
1835 // been put meanwhile, recording it as written: the run after that read
1836 // the file into the focus. That run refuses now, naming the file, and
1837 // no run reads it. What the checkout wrote at an ignored path before it
1838 // was cut short, the focus's own, is written again.
1839 let r = setup();
1840 r.write(".gitignore", "*.env\n");
1841 r.write("keep.txt", "k\n");
1842 r.j(&["describe \"base\""]).ok();
1843 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1844 let n = 2000;
1845 // `k.env` is written before `m`, `o.env` after it
1846 let files = "c.files ++ [({ path = [\"k.env\"], content = blob \"committed k\\n\" }) \
1847 ({ path = [\"o.env\"], content = blob \"committed\\n\" })]";
1848 let secret = "SECRET=hunter2\n";
1849 let out = stopped_mid_checkout_as(&r, &slow_edit(files, n), n, |pid| {
1850 r.write("o.env", secret);
1851 unsafe { libc::kill(pid, libc::SIGKILL) };
1852 });
1853 assert_eq!(out.code, -libc::SIGKILL, "{}", out.stderr);
1854 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1855 assert!(in_m(&r) < n);
1856 assert_eq!(r.read("k.env"), "committed k\n");
1857 for run in ["log", "id"] {
1858 r.j(&[run]).ok();
1859 assert!(!has_blob(&r, secret));
1861 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1862 let out = r.j(&["describe \"S\""]);
1863 assert_eq!(out.code, 1, "{}", out.stderr);
1864 assert!(out.stderr.contains("untracked `o.env`"), "{}", out.stderr);
1865 assert!(out.stderr.contains("move it aside"), "{}", out.stderr);
1866 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1867 assert_eq!(r.read("o.env"), secret);
1868 r.j(&["id"]).ok();
1869 assert!(!has_blob(&r, secret));
1870 // moved aside, the run completes the checkout
1871 let aside = r.cfg.join("o.env");
1872 std::fs::rename(r.dir.join("o.env"), &aside).unwrap();
1873 r.j(&["describe \"S\""]).ok();
1874 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1875 assert_eq!(r.read("k.env"), "committed k\n");
1876 assert_eq!(r.read("o.env"), "committed\n");
1877 assert_eq!(in_m(&r), n);
1878 assert_eq!(r.read("zz"), "z\n");
1879 r.j(&["id"]).ok();
1880 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
1881 assert!(!has_blob(&r, secret));
1884#[test]
1885#[cfg(target_os = "linux")]
1886fn a_file_a_checkout_cut_short_moved_aside_is_put_back_by_the_next_run() {
1887 // §7.4, §7.5 step 7: an untracked file holding just what the checkout
1888 // writes at a path the focus adds is moved aside into `.jj` for the
1889 // checkout, and put back where it does not get to write; a run killed
1890 // meanwhile leaves it there, which the next run puts back, and `j undo`
1891 // leaves it where it was
1892 let r = setup();
1893 r.write(".gitignore", "*.env\n");
1894 r.write("keep.txt", "k\n");
1895 r.j(&["describe \"base\""]).ok();
1896 r.write("o.env", "committed\n");
1897 let log = r.j(&["log"]).ok().stdout;
1898 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1899 let n = 2000;
1900 // `o.env` is written after `m`
1901 let files = "c.files ++ [{ path = [\"o.env\"], content = blob \"committed\\n\" }]";
1902 let out = stopped_mid_checkout_as(&r, &slow_edit(files, n), n, |pid| {
1903 unsafe { libc::kill(pid, libc::SIGKILL) };
1904 });
1905 assert_eq!(out.code, -libc::SIGKILL, "{}", out.stderr);
1906 assert!(in_m(&r) < n);
1907 assert!(!r.dir.join("o.env").exists());
1908 assert_eq!(aside_in_jj(&r).len(), 1);
1909 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1910 assert_eq!(r.read("o.env"), "committed\n");
1911 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1912 r.j(&["undo"]).ok();
1913 assert_eq!(r.read("o.env"), "committed\n");
1914 assert!(!r.dir.join("m").exists());
1915 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1916 assert_eq!(r.j(&["log"]).ok().stdout, log);
1919#[test]
1920#[cfg(target_os = "linux")]
1921fn a_file_moved_aside_that_a_full_disk_cut_short_is_put_back_whole() {
1922 // §7.4, §7.5 step 7: an untracked file holding just what the checkout
1923 // writes at a path the focus adds was moved aside for the checkout,
1924 // and removed once it returned wherever something stood at its path.
1925 // A write there that a full disk cut short left the ignored file,
1926 // which no run reads, empty: `j undo` left it so, and the checkout,
1927 // done again, was refused, the file being in its way. The file moved
1928 // aside is put back over the start of it the checkout wrote now.
1929 let r = setup();
1930 r.write(".gitignore", "zz\n");
1931 r.write("keep.txt", "k\n");
1932 r.j(&["describe \"base\""]).ok();
1933 r.write("zz", "z\n");
1934 let log = r.j(&["log"]).ok().stdout;
1935 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1936 let n = 2000;
1937 // `zz`, written after `m`, is left empty
1938 let out = stopped_mid_checkout_as(&r, &slow_edit("c.files", n), n, fill_disk);
1939 assert_eq!(out.code, 1, "{}", out.stderr);
1940 assert!(out.stderr.contains("Failed to write the content to the file"), "{}", out.stderr);
1941 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
1942 assert_eq!(in_m(&r), n);
1943 assert_eq!(r.read("zz"), "z\n");
1944 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1945 r.j(&["id"]).ok();
1946 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
1947 assert_eq!(r.read("zz"), "z\n");
1948 r.j(&["undo"]).ok();
1949 assert_eq!(r.read("zz"), "z\n");
1950 assert!(!r.dir.join("m").exists());
1951 assert_eq!(r.j(&["log"]).ok().stdout, log);
1952 // done again, the checkout completes
1953 r.j(&["redo"]).ok();
1954 assert_eq!(in_m(&r), n);
1955 assert_eq!(r.read("zz"), "z\n");
1956 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
1959#[test]
1960#[cfg(target_os = "linux")]
1961fn a_file_moved_aside_that_a_killed_run_left_a_start_of_is_put_back_whole() {
1962 // §7.4, §7.5 step 7: a run killed while its checkout wrote a file it
1963 // had moved aside leaves a start of it at its path (jj's checkout
1964 // makes the file, empty, before it writes it, and a symlink's path is
1965 // tried with an empty file first), and the next run, finding something
1966 // there, removed the whole file it had moved aside, leaving the ignored
1967 // start of it, which no run reads. It puts the file back over a start
1968 // of itself now, and anything else standing there stays as it is.
1969 let r = setup();
1970 r.write("keep.txt", "k\n");
1971 r.j(&["describe \"base\""]).ok();
1972 // B tracks `t.env`, a symlink, which `.gitignore` ignores
1973 r.j(&["new"]).ok();
1974 std::os::unix::fs::symlink("target", r.dir.join("t.env")).unwrap();
1975 r.j(&["describe \"S\""]).ok();
1976 r.j(&["new"]).ok();
1977 r.write(".gitignore", "*.env\n");
1978 r.j(&["describe \"B\""]).ok();
1979 r.j(&[&new_above("base")]).ok();
1980 r.write(".gitignore", "*.env\n");
1981 r.j(&["describe \"A\""]).ok();
1982 let names = ["o.env", "p.env", "q.env", "r.env"];
1983 for name in names {
1984 r.write(name, "committed\n");
1986 std::os::unix::fs::symlink("target", r.dir.join("t.env")).unwrap();
1987 let log = r.j(&["log"]).ok().stdout;
1988 let ops = r.j(&["ops"]).ok().stdout.lines().count();
1989 let n = 2000;
1990 // each `.env` is written after `m`
1991 let files = "c.files ++ map (\\p -> { path = [p], content = blob \"committed\\n\" }) [\"o.env\" \"p.env\" \"q.env\" \"r.env\"]";
1992 let edit = format!("{} . {}", slow_edit(files, n), new_above("B"));
1993 let out = stopped_mid_checkout_as(&r, &edit, n, |pid| {
1994 unsafe { libc::kill(pid, libc::SIGKILL) };
1995 });
1996 assert_eq!(out.code, -libc::SIGKILL, "{}", out.stderr);
1997 assert!(in_m(&r) < n);
1998 assert_eq!(aside_in_jj(&r).len(), 1);
1999 for name in names.iter().chain(&["t.env"]) {
2000 assert!(r.dir.join(name).symlink_metadata().is_err(), "{}", name);
2002 // what a kill while each was written leaves, and what the user saved
2003 // there since, which differs, or goes on after it
2004 r.write("o.env", "comm");
2005 r.write("p.env", "");
2006 r.write("t.env", "");
2007 r.write("q.env", "mine\n");
2008 r.write("r.env", "committed\nand more\n");
2009 let kept = |r: &Repo| {
2010 assert_eq!(r.read("o.env"), "committed\n");
2011 assert_eq!(r.read("p.env"), "committed\n");
2012 assert_eq!(std::fs::read_link(r.dir.join("t.env")).unwrap(), PathBuf::from("target"));
2013 assert_eq!(r.read("q.env"), "mine\n");
2014 assert_eq!(r.read("r.env"), "committed\nand more\n");
2015 assert_eq!(aside_in_jj(r), Vec::<String>::new());
2016 };
2017 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2018 kept(&r);
2019 r.j(&["undo"]).ok();
2020 kept(&r);
2021 assert!(!r.dir.join("m").exists());
2022 assert_eq!(r.j(&["log"]).ok().stdout, log);
2025#[test]
2026#[cfg(target_os = "linux")]
2027fn a_start_of_a_file_moved_aside_saved_meanwhile_stays_where_the_checkout_returns() {
2028 // §7.4, §7.5 step 7: a checkout that returns, having skipped a path,
2029 // wrote no file only in part, so a start of the file moved aside for
2030 // it, saved at its path while the checkout ran, is the user's, and
2031 // stays, holding what it held, as does anything else saved there
2032 let r = setup();
2033 r.write(".gitignore", "*.env\n");
2034 r.write("keep.txt", "k\n");
2035 r.j(&["describe \"base\""]).ok();
2036 r.write("o.env", "committed\n");
2037 let log = r.j(&["log"]).ok().stdout;
2038 let n = 2000;
2039 // `o.env` is written after `m`
2040 let files = "c.files ++ [{ path = [\"o.env\"], content = blob \"committed\\n\" }]";
2041 let out = stopped_mid_checkout(&r, &slow_edit(files, n), n, || {
2042 assert!(!r.dir.join("o.env").exists());
2043 r.write("o.env", "comm");
2044 });
2045 assert_eq!(out.code, 1, "{}", out.stderr);
2046 assert!(out.stderr.contains("`o.env` was not written"), "{}", out.stderr);
2047 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2048 assert_eq!(in_m(&r), n);
2049 assert_eq!(r.read("o.env"), "comm");
2050 assert_eq!(aside_in_jj(&r), Vec::<String>::new());
2051 r.j(&["undo"]).ok();
2052 assert_eq!(r.read("o.env"), "comm");
2053 assert_eq!(r.j(&["log"]).ok().stdout, log);
2056#[test]
2057#[cfg(target_os = "linux")]
2058fn an_ignored_file_saved_meanwhile_where_a_checkout_adds_one_fails_it() {
2059 // §7.4, §7.5 step 7: a file saved while the checkout runs, at a path
2060 // it adds after that, is in its way, and jj skipped the path, recording
2061 // it as written: the next run read an ignored one into the focus. The
2062 // checkout fails now, naming the path, and leaves the working copy
2063 // stale, so no run reads the file.
2064 let r = setup();
2065 r.write(".gitignore", "*.env\n");
2066 r.write("keep.txt", "k\n");
2067 r.j(&["describe \"base\""]).ok();
2068 let log = r.j(&["log"]).ok().stdout;
2069 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2070 let n = 2000;
2071 // `o.env` is written after `m`
2072 let files = "c.files ++ [{ path = [\"o.env\"], content = blob \"committed\\n\" }]";
2073 let secret = "SECRET=hunter2\n";
2074 let out = stopped_mid_checkout(&r, &slow_edit(files, n), n, || r.write("o.env", secret));
2075 assert_eq!(out.code, 1, "{}", out.stderr);
2076 assert!(out.stderr.contains("`o.env`"), "{}", out.stderr);
2077 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2078 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2079 // the checkout carried on past it
2080 assert_eq!(in_m(&r), n);
2081 assert_eq!(r.read("zz"), "z\n");
2082 assert_eq!(r.read("o.env"), secret);
2083 for run in ["log", "id"] {
2084 r.j(&[run]).ok();
2085 assert!(!has_blob(&r, secret));
2087 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2088 let text = "\\r -> text (contentAt [\"o.env\"] (files r))";
2089 assert_eq!(r.j(&[text]).ok().stdout, "committed\n");
2090 let out = r.j(&["describe \"S\""]);
2091 assert_eq!(out.code, 1, "{}", out.stderr);
2092 assert!(out.stderr.contains("untracked `o.env`"), "{}", out.stderr);
2093 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2094 r.j(&["undo"]).ok();
2095 assert_eq!(r.j(&["log"]).ok().stdout, log);
2096 assert!(!r.dir.join("m").exists());
2097 assert_eq!(r.read("o.env"), secret);
2098 r.j(&["id"]).ok();
2099 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2100 assert!(!has_blob(&r, secret));
2103/// Run `edit`, which must take a while to evaluate, over a directory with a
2104/// change to record, and call `meanwhile` with the process stopped once the
2105/// snapshot's operation is written, before it publishes one
2106#[cfg(target_os = "linux")]
2107fn stopped_before_publish(r: &Repo, edit: &str, meanwhile: impl FnOnce()) -> Out {
2108 let ops_dir = r.dir.join(".jj/repo/op_store/operations");
2109 let heads_dir = r.dir.join(".jj/repo/op_heads/heads");
2110 let count = || std::fs::read_dir(&ops_dir).unwrap().count();
2111 let heads = || {
2112 let mut names: Vec<_> = std::fs::read_dir(&heads_dir).unwrap().map(|e| e.unwrap().file_name()).collect();
2113 names.sort();
2114 names
2115 };
2116 let (before, published) = (count(), heads());
2117 let mut child = Command::new(j_bin())
2118 .arg(edit)
2119 .current_dir(&r.dir)
2120 .env("XDG_CONFIG_HOME", &r.cfg)
2121 .env("NO_COLOR", "1")
2122 .stdout(Stdio::piped())
2123 .stderr(Stdio::piped())
2124 .spawn()
2125 .unwrap();
2126 let pid = child.id() as libc::pid_t;
2127 while count() == before {
2128 assert!(child.try_wait().unwrap().is_none(), "exited before its snapshot");
2129 std::thread::yield_now();
2131 let mut status = 0;
2132 unsafe {
2133 libc::kill(pid, libc::SIGSTOP);
2134 assert_eq!(libc::waitpid(pid, &mut status, libc::WUNTRACED), pid);
2136 assert!(libc::WIFSTOPPED(status), "exited before it was stopped");
2137 let in_time = heads() == published;
2138 let done = if in_time {
2139 std::panic::catch_unwind(std::panic::AssertUnwindSafe(meanwhile))
2140 } else {
2141 Ok(())
2142 };
2143 unsafe { libc::kill(pid, if in_time { libc::SIGCONT } else { libc::SIGKILL }) };
2144 let out = child.wait_with_output().unwrap();
2145 assert!(in_time, "stopped too late: an operation was published");
2146 if let Err(e) = done {
2147 std::panic::resume_unwind(e);
2149 Out {
2150 code: out.status.code().unwrap_or(-1),
2151 stdout: String::from_utf8_lossy(&out.stdout).to_string(),
2152 stderr: String::from_utf8_lossy(&out.stderr).to_string(),
2156#[test]
2157#[cfg(target_os = "linux")]
2158fn a_run_that_fails_to_publish_writes_nothing() {
2159 // §7.5 step 7: the operation is published before the checkout, so one
2160 // that fails to publish records nothing and writes nothing: the
2161 // directory, its ignored files and its uncommitted edit are as they
2162 // were, and none of the ignored files is read into the store
2163 let r = setup();
2164 r.write(".gitignore", "*.env\n");
2165 r.write("keep.txt", "k\n");
2166 r.j(&["describe \"base\""]).ok();
2167 let secret = "SECRET=hunter2\n";
2168 r.write("local.env", secret);
2169 r.write("keep.txt", "my uncommitted edit\n");
2170 let log = r.j(&["log"]).ok().stdout;
2171 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2172 // an ignored path the focus adds, where nothing stands: one where
2173 // `local.env` stands would refuse the run before it tries to publish
2174 let edit = "mapRoot (\\c -> c { message = show (foldl (+) 0 (range 0 300000)), \
2175 files = c.files ++ [({ path = [\"b\"], content = blob \"b\\n\" }) \
2176 ({ path = [\"other.env\"], content = blob \"committed\\n\" })] })";
2177 // a file where jj keeps its operation heads: publishing fails
2178 let heads = r.dir.join(".jj/repo/op_heads/heads");
2179 let aside = heads.with_extension("aside");
2180 let out = stopped_before_publish(&r, edit, || {
2181 std::fs::rename(&heads, &aside).unwrap();
2182 std::fs::write(&heads, "").unwrap();
2183 });
2184 std::fs::remove_file(&heads).unwrap();
2185 std::fs::rename(&aside, &heads).unwrap();
2186 assert_eq!(out.code, 1, "{}", out.stderr);
2187 assert!(out.stderr.contains("cannot publish"), "{}", out.stderr);
2188 assert!(!out.stderr.contains("recorded"), "{}", out.stderr);
2189 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
2190 assert_eq!(r.j(&["log"]).ok().stdout, log);
2191 assert_eq!(r.read("local.env"), secret);
2192 assert_eq!(r.read("keep.txt"), "my uncommitted edit\n");
2193 assert!(!r.dir.join("b").exists());
2194 assert!(!r.dir.join("other.env").exists());
2195 assert!(!has_blob(&r, secret));
2196 // the run wrote the focus's objects before it tried to publish
2197 assert!(has_blob(&r, "committed\n"));
2198 r.j(&["id"]).ok();
2199 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2200 let text = r.j(&["\\r -> text (contentAt [\"keep.txt\"] (files r))"]).ok().stdout;
2201 assert_eq!(text, "my uncommitted edit\n");
2204#[test]
2205#[cfg(target_os = "linux")]
2206fn a_file_saved_while_the_program_runs_is_not_written_over() {
2207 // §7.5 step 7: the checkout's scan found a file saved after the run's
2208 // snapshot, while the program ran, and took the directory to hold the
2209 // snapshot there all the same; where the focus has that file otherwise,
2210 // the checkout wrote the focus's over the edit, which no snapshot or
2211 // operation held. Such a run now crashes before its operation is
2212 // recorded, naming the path, and writes nothing, and the next run
2213 // records the edit. One saved where the focus has what the snapshot
2214 // read is left, as before, for the next run to record.
2215 let r = setup();
2216 r.write("a", "base\n");
2217 r.write("same.txt", "same\n");
2218 r.j(&["describe \"P\""]).ok();
2219 r.j(&["new"]).ok();
2220 r.write("a", "child\n");
2221 r.j(&["describe \"C\""]).ok();
2222 // a change for the run's snapshot to record, whose operation
2223 // `stopped_before_publish` waits for
2224 r.write("keep.txt", "uncommitted\n");
2225 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2226 // back to P, which has `a` otherwise, after a slow evaluation
2227 let edit = "prev . mapRoot (\\c -> c { message = show (foldl (+) 0 (range 0 300000)) })";
2228 let out = stopped_before_publish(&r, edit, || r.write("a", "saved meanwhile\n"));
2229 assert_eq!(out.code, 1, "{}", out.stderr);
2230 assert!(out.stderr.contains("`a` changed in the working directory"), "{}", out.stderr);
2231 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
2232 assert_eq!(r.read("a"), "saved meanwhile\n");
2233 assert_eq!(r.read("keep.txt"), "uncommitted\n");
2234 r.j(&["id"]).ok();
2235 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2236 let texts = r.j(&[&texts_by_message(&["[\"a\"]", "[\"keep.txt\"]"], &["C"])]).ok().stdout;
2237 assert_eq!(
2238 texts.split_whitespace().collect::<Vec<_>>().join(" "),
2239 r#"[["saved meanwhile\n" "uncommitted\n"]]"#
2240 );
2241 // `same.txt`, which P has as the snapshot read it
2242 r.write("keep.txt", "uncommitted, again\n");
2243 let out = stopped_before_publish(&r, edit, || r.write("same.txt", "saved meanwhile\n"));
2244 out.ok();
2245 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2246 assert_eq!(r.read("same.txt"), "saved meanwhile\n");
2247 assert_eq!(r.read("a"), "base\n");
2248 assert!(!r.dir.join("keep.txt").exists());
2249 r.j(&["id"]).ok();
2250 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 3);
2251 let texts = r.j(&[&texts_by_message(&["[\"a\"]", "[\"same.txt\"]"], &["P"])]).ok().stdout;
2252 assert_eq!(
2253 texts.split_whitespace().collect::<Vec<_>>().join(" "),
2254 r#"[["base\n" "saved meanwhile\n"]]"#
2255 );
2258#[test]
2259#[cfg(target_os = "linux")]
2260fn a_checkout_that_fills_the_disk_is_recorded_and_carried_on_from() {
2261 // §7.5 step 7: a checkout that used up the disk was put back, and the
2262 // put-back had to write too; where it could not, the edit the checkout
2263 // had written over was recorded alone, or lost. The operation, which
2264 // holds the edit, is recorded before the checkout begins, and the next
2265 // run takes what the checkout wrote for the focus's own, and the file
2266 // it cut short for a conflict with what the focus holds there.
2267 let r = setup();
2268 r.write("keep.txt", "k\n");
2269 r.j(&["describe \"base\""]).ok();
2270 r.write("keep.txt", "my uncommitted edit\n");
2271 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2272 let n = 2000;
2273 let out = stopped_mid_checkout_as(&r, &slow_edit("c.files", n), n, fill_disk);
2274 assert_eq!(out.code, 1, "{}", out.stderr);
2275 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
2276 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2277 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2278 assert_eq!(r.read("keep.txt"), "my uncommitted edit\n");
2279 assert_eq!(in_m(&r), n);
2280 assert_eq!(r.read("zz"), "");
2281 r.j(&["id"]).ok();
2282 let conflicted = r.j(&["\\r -> show (conflicted (files r))"]).ok().stdout;
2283 assert_eq!(conflicted.trim(), r#"[["zz"]]"#);
2284 let text = r.j(&["\\r -> text (contentAt [\"keep.txt\"] (files r))"]).ok().stdout;
2285 assert_eq!(text, "my uncommitted edit\n");
2286 r.j(&["id"]).ok();
2287 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2290#[test]
2291#[cfg(target_os = "linux")]
2292fn a_fetch_after_a_failed_checkout_leaves_every_command_working() {
2293 // §7.4, §7.6: when a checkout that filled the disk could not be put
2294 // back, the snapshot it had written over was recorded alone, leaving
2295 // the working-copy state behind the commit; one `j fetch` or `j push`
2296 // after that, and every run, `j log` included, exited 2 as over a stale
2297 // working copy, and `j undo` was refused. A failed checkout now leaves
2298 // its operation recorded and the state stale, which refuses nothing.
2299 let r = setup();
2300 r.write("a", "a\n");
2301 r.j(&["describe \"B\""]).ok();
2302 r.j(&["new"]).ok();
2303 r.j(&["describe \"W\""]).ok();
2304 r.write("a", "my uncommitted edit\n");
2305 let n = 2000;
2306 // S, a new child of B, writes `a` before `m`
2307 let files = "[{ path = [\"a\"], content = blob \"s\\n\" }]";
2308 let edit = format!("describe \"S\" . ({}) . new . prev", slow_edit(files, n));
2309 let out = stopped_mid_checkout_as(&r, &edit, n, fill_disk);
2310 assert_eq!(out.code, 1, "{}", out.stderr);
2311 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2312 let origin = r.cfg.join("origin.git");
2313 let git = Command::new("git").args(["init", "-q", "--bare"]).arg(&origin).status().unwrap();
2314 assert!(git.success());
2315 r.j(&["remote", origin.to_str().unwrap()]).ok();
2316 r.j(&["fetch"]).ok();
2317 r.j(&["log"]).ok();
2318 let texts = r.j(&[&texts_by_message(&["[\"a\"]"], &["B", "W", "S"])]).ok().stdout;
2319 assert_eq!(
2320 texts.split_whitespace().collect::<Vec<_>>().join(" "),
2321 r#"[["a\n"] ["my uncommitted edit\n"] ["s\n"]]"#
2322 );
2323 r.j(&["id"]).ok();
2324 r.j(&["describe \"S2\""]).ok();
2325 r.j(&["goto (matching (\\c -> c.message == \"W\") all)"]).ok();
2326 assert_eq!(r.read("a"), "my uncommitted edit\n");
2327 r.j(&["undo"]).ok();
2328 assert_eq!(r.read("a"), "s\n");
2331#[test]
2332#[cfg(target_os = "linux")]
2333fn a_failed_checkout_keeps_the_edit_of_an_immutable_focus_in_a_child() {
2334 // §7.2, §7.5 step 7: with an immutable working-copy commit, a checkout
2335 // that filled the disk could not be put back, and the snapshot it had
2336 // written over, recorded instead in a new child of the commit, needed
2337 // room on the disk as well: the uncommitted edit was lost. The
2338 // operation that holds the child is now recorded before the checkout.
2339 let r = setup();
2340 let cfg = r.cfg.join("j/config.j");
2341 let config = std::fs::read_to_string(&cfg).unwrap();
2342 let immutable = "immutable = ancestorsOf trunk\n";
2343 assert!(config.contains(immutable));
2344 let config = config.replace(immutable, "immutable = matching (\\c -> c.message == \"W\") all\n");
2345 std::fs::write(&cfg, config).unwrap();
2346 r.write("a", "a\n");
2347 r.j(&["describe \"B\""]).ok();
2348 r.j(&["new"]).ok();
2349 r.j(&["describe \"W\""]).ok();
2350 r.write("a", "my uncommitted edit\n");
2351 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2352 let n = 2000;
2353 // from the child of W that holds the edit, S, a new child of B
2354 let files = "[{ path = [\"a\"], content = blob \"s\\n\" }]";
2355 let edit = format!("describe \"S\" . ({}) . new . prev . prev", slow_edit(files, n));
2356 let out = stopped_mid_checkout_as(&r, &edit, n, fill_disk);
2357 assert_eq!(out.code, 1, "{}", out.stderr);
2358 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2359 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2360 let files = r.j(&[CONTENT_OF_A]).ok().stdout;
2361 assert_eq!(
2362 files.split_whitespace().collect::<Vec<_>>().join(" "),
2363 r#"[["" (blob "")] ["B" (blob "a\n")] ["W" (blob "a\n")] ["" (blob "my uncommitted edit\n")] ["S" (blob "s\n")]]"#
2364 );
2365 r.j(&["goto (matching (\\c -> contentAt [\"a\"] c.files == blob \"my uncommitted edit\\n\") all)"]).ok();
2366 assert_eq!(r.read("a"), "my uncommitted edit\n");
2369#[test]
2370fn checkouts_that_fail_in_a_row_leave_each_edit_in_its_commit() {
2371 // §7.4, §7.5 step 7: a failed checkout leaves the working-copy state
2372 // stale, recording what the directory held when the checkout began, so
2373 // the runs after it take only what changed since for their changes:
2374 // here an uncommitted edit before each of two failed checkouts, each
2375 // recorded into the commit it was made on
2376 let r = setup();
2377 r.write("a", "a\n");
2378 r.write("d", "d\n");
2379 r.j(&["describe \"base\""]).ok();
2380 r.j(&["new"]).ok();
2381 r.j(&["describe \"W\""]).ok();
2382 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2383 let deep = format!("{{ path = [{}], content = blob \"deep\" }}", too_deep("zz"));
2384 // N1, a new child of W, then N2, a new child of W beside it
2385 for (path, edit, name, to) in [("a", "edit 1\n", "N1", "new"), ("d", "edit 2\n", "N2", "new . prev")] {
2386 r.write(path, edit);
2387 let e = format!("describe \"{}\" . mapRoot (\\c -> c {{ files = c.files ++ [{}] }}) . {}", name, deep, to);
2388 let out = r.j(&[&e]);
2389 assert_eq!(out.code, 1, "{}", out.stderr);
2390 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2392 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2393 // N2's `d` was written before its path under `zz` failed
2394 assert_eq!(r.read("a"), "edit 1\n");
2395 assert_eq!(r.read("d"), "d\n");
2396 r.j(&["id"]).ok();
2397 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2398 let texts = r.j(&[&texts_by_message(&["[\"a\"]", "[\"d\"]"], &["base", "W", "N1", "N2"])]).ok().stdout;
2399 assert_eq!(
2400 texts.split_whitespace().collect::<Vec<_>>().join(" "),
2401 r#"[["a\n" "d\n"] ["edit 1\n" "d\n"] ["edit 1\n" "edit 2\n"] ["edit 1\n" "d\n"]]"#
2402 );
2405#[test]
2406#[cfg(target_os = "linux")]
2407fn a_failed_checkout_leaves_a_nested_repository_alone() {
2408 // §7.5 step 7: no run reads a directory holding `.git` or `.jj`, so
2409 // what a checkout writes there is never recorded; the put-back removed
2410 // what a failed one had written there and named what it left. Nothing
2411 // there is removed now: not what was there before, not what was saved
2412 // meanwhile, and, by `j undo`, not what the checkout wrote either.
2413 let r = setup();
2414 r.write("keep.txt", "k\n");
2415 r.j(&["describe \"base\""]).ok();
2416 std::fs::create_dir(r.dir.join("lib")).unwrap();
2417 std::fs::create_dir(r.dir.join("lib/.git")).unwrap();
2418 r.write("lib/own.c", "own\n");
2419 let log = r.j(&["log"]).ok().stdout;
2420 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2421 let n = 2000;
2422 // `lib/x.c` and `lib/y.c` are written before `m`; `lib/y.c` is saved
2423 // over meanwhile
2424 let files = "c.files ++ [({ path = [\"lib\" \"x.c\"], content = blob \"x\\n\" }) \
2425 ({ path = [\"lib\" \"y.c\"], content = blob \"y\\n\" })]";
2426 let out = stopped_mid_checkout(&r, &slow_failing_edit(files, n), n, || {
2427 r.write("lib/y.c", "mine\n");
2428 });
2429 assert_eq!(out.code, 1, "{}", out.stderr);
2430 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
2431 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2432 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2433 r.j(&["undo"]).ok();
2434 assert_eq!(r.j(&["log"]).ok().stdout, log);
2435 assert!(!r.dir.join("m").exists());
2436 assert_eq!(r.read("lib/own.c"), "own\n");
2437 assert_eq!(r.read("lib/y.c"), "mine\n");
2438 assert_eq!(r.read("lib/x.c"), "x\n");
2441#[test]
2442#[cfg(target_os = "linux")]
2443fn a_failed_checkout_removes_nothing_through_a_symlink() {
2444 // §7.5 step 7, §7.7: the put-back removed what the checkout had added
2445 // one file at a time; a directory the checkout made, replaced meanwhile
2446 // by a symlink to one elsewhere holding the same file, was in the way.
2447 // Nothing is removed after a failed checkout now, and the symlink is
2448 // the user's change, which `j undo` refuses to write over.
2449 let r = setup();
2450 // where the filesystem holds no symlink, none is followed
2451 if std::os::unix::fs::symlink("x", r.dir.join("probe")).is_err() {
2452 return;
2454 std::fs::remove_file(r.dir.join("probe")).unwrap();
2455 r.write("keep.txt", "k\n");
2456 r.j(&["describe \"base\""]).ok();
2457 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2458 let n = 2000;
2459 // `d/f` is written before `m`
2460 let files = "c.files ++ [{ path = [\"d\" \"f\"], content = blob \"f\\n\" }]";
2461 // outside the working directory, and removed with the repository
2462 let elsewhere = r.cfg.join("elsewhere");
2463 let out = stopped_mid_checkout(&r, &slow_failing_edit(files, n), n, || {
2464 std::fs::rename(r.dir.join("d"), &elsewhere).unwrap();
2465 std::os::unix::fs::symlink(&elsewhere, r.dir.join("d")).unwrap();
2466 });
2467 assert_eq!(out.code, 1, "{}", out.stderr);
2468 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2469 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2470 let out = r.j(&["undo"]);
2471 assert_eq!(out.code, 1, "{}", out.stderr);
2472 assert!(out.stderr.contains("working copy has changes not in @"), "{}", out.stderr);
2473 assert_eq!(std::fs::read_to_string(elsewhere.join("f")).unwrap(), "f\n");
2474 assert!(r.dir.join("d").symlink_metadata().unwrap().is_symlink());
2477#[test]
2478fn a_file_a_failed_checkout_replaced_by_directories_comes_back() {
2479 // §7.5 step 7: the checkout removed the file `zz` and made directories
2480 // for a path under it until the path grew too long. No scan sees empty
2481 // directories, but a checkout writes no file over one: `j undo` skipped
2482 // writing `zz` back without an error, and the next run recorded it as
2483 // deleted. A checkout removes such a tree of empty directories where it
2484 // writes a file.
2485 let r = setup();
2486 r.write("zz", "precious\n");
2487 r.write("a", "k\n");
2488 r.j(&["describe \"W\""]).ok();
2489 let log = r.j(&["log"]).ok().stdout;
2490 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2491 let edit = format!(
2492 "mapRoot (\\c -> c {{ files = filter (\\e -> e.path /= [\"zz\"]) c.files ++ [{{ path = [{}], content = blob \"deep\" }}] }})",
2493 too_deep("zz")
2494 );
2495 let out = r.j(&[&edit]);
2496 assert_eq!(out.code, 1, "{}", out.stderr);
2497 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
2498 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2499 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2500 assert!(r.dir.join("zz").is_dir());
2501 r.j(&["undo"]).ok();
2502 assert_eq!(r.j(&["log"]).ok().stdout, log);
2503 assert_eq!(r.read("zz"), "precious\n");
2504 let paths = r.j(&["\\r -> show (map (\\e -> e.path) (files r))"]).ok().stdout;
2505 assert_eq!(paths.trim(), r#"[["a"] ["zz"]]"#);
2506 r.j(&["id"]).ok();
2507 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2510#[test]
2511fn a_conflict_a_failed_checkout_wrote_is_read_back_as_that_conflict() {
2512 // §7.4, §7.5 step 7: a checkout records a conflict in the working-copy
2513 // state as it writes its markers, so a checkout that fails records none
2514 // of those it wrote, and the next run read each file as the text of its
2515 // markers: a conflict nested in the focus's, which `j undo` refused to
2516 // write over. A file holding exactly what the checkout writes for the
2517 // focus's conflict is read back as that conflict, markers made longer
2518 // than any line of its sides that looks like one included.
2519 let r = setup();
2520 r.write("a.txt", "base\n");
2521 r.write("keep.txt", "k\n");
2522 r.j(&["describe \"base\""]).ok();
2523 r.j(&["new"]).ok();
2524 r.write("a.txt", "<<<<<<< left\n");
2525 r.j(&["describe \"left\""]).ok();
2526 r.j(&["new . goto parents"]).ok();
2527 r.write("a.txt", "right\n");
2528 r.j(&["describe \"right\""]).ok();
2529 r.j(&["rebase siblings"]).ok();
2530 let conflicted = "\\r -> show (conflicted (files r))";
2531 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a.txt\"]]");
2532 let log = r.j(&["log"]).ok().stdout;
2533 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2534 // the conflict is written to `c.txt` and over `keep.txt` too, then the
2535 // deep path fails
2536 let conflict = "(filter (\\e -> e.path == [\"a.txt\"]) c.files)";
2537 let edit = format!(
2538 "mapRoot (\\c -> c {{ files = filter (\\e -> e.path /= [\"keep.txt\"]) c.files \
2539 ++ map (\\e -> e {{ path = [\"c.txt\"] }}) {0} ++ map (\\e -> e {{ path = [\"keep.txt\"] }}) {0} \
2540 ++ [{{ path = [{1}], content = blob \"deep\" }}] }})",
2541 conflict,
2542 too_deep("zz")
2543 );
2544 let out = r.j(&[&edit]);
2545 assert_eq!(out.code, 1, "{}", out.stderr);
2546 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
2547 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2548 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2549 assert!(r.read("c.txt").contains("<<<<<<<<"), "{}", r.read("c.txt"));
2550 assert_eq!(r.read("c.txt"), r.read("keep.txt"));
2551 r.j(&["id"]).ok();
2552 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2553 let all = r.j(&[conflicted]).ok().stdout;
2554 assert_eq!(all.split_whitespace().collect::<Vec<_>>().join(" "), r#"[["a.txt"] ["c.txt"] ["keep.txt"]]"#);
2555 r.j(&["undo"]).ok();
2556 assert_eq!(r.j(&["log"]).ok().stdout, log);
2557 assert!(!r.dir.join("c.txt").exists());
2558 assert_eq!(r.read("keep.txt"), "k\n");
2559 r.j(&["id"]).ok();
2560 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2563#[test]
2564#[cfg(target_os = "linux")]
2565fn a_checkout_whose_state_fails_to_save_is_carried_on_from() {
2566 // §7.4, §7.5 step 7: a checkout that completed, but whose working-copy
2567 // state could not then be saved, left the state behind the commit, and
2568 // once any file was edited every run was refused as over a stale
2569 // working copy. The next run takes the directory for the focus's files
2570 // now, and records only the edit.
2571 let r = setup();
2572 r.write("keep.txt", "k\n");
2573 r.j(&["describe \"base\""]).ok();
2574 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2575 let n = 2000;
2576 // a directory where the state is saved: saving it fails
2577 let state = r.dir.join(".jj/working_copy/tree_state");
2578 let aside = state.with_extension("aside");
2579 let out = stopped_mid_checkout(&r, &slow_edit("c.files", n), n, || {
2580 std::fs::rename(&state, &aside).unwrap();
2581 std::fs::create_dir(&state).unwrap();
2582 std::fs::write(state.join("x"), "").unwrap();
2583 });
2584 std::fs::remove_dir_all(&state).unwrap();
2585 std::fs::rename(&aside, &state).unwrap();
2586 assert_eq!(out.code, 1, "{}", out.stderr);
2587 assert!(out.stderr.contains("cannot save the working copy's state"), "{}", out.stderr);
2588 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2589 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2590 assert_eq!(in_m(&r), n);
2591 assert_eq!(r.read("zz"), "z\n");
2592 r.write("keep.txt", "edited\n");
2593 r.j(&["log"]).ok();
2594 r.j(&["id"]).ok();
2595 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2596 let texts = r.j(&["\\r -> show (map (\\e -> text e.content) (restrict (neg (under ./m)) (files r)))"]).ok().stdout;
2597 assert_eq!(texts.trim(), r#"["edited\n" "z\n"]"#);
2598 r.j(&["id"]).ok();
2599 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
2602/// Whether `dir`'s filesystem folds case, judged as the backend judges it:
2603/// `.JJ` names the workspace's `.jj`, which the directory does not list
2604/// under that spelling
2605fn folds_case(dir: &std::path::Path) -> bool {
2606 dir.join(".JJ").symlink_metadata().is_ok()
2607 && !std::fs::read_dir(dir).unwrap().any(|e| e.unwrap().file_name() == ".JJ")
2610#[test]
2611fn a_failed_case_only_rename_keeps_the_edits() {
2612 // §7.5 step 7: the put-back checked the tree back out in one go, in
2613 // path order. Where the filesystem folds case, a focus renaming
2614 // `README` to `readme` had removed `README` and written `readme` when
2615 // its checkout failed; the put-back wrote `README` first, which
2616 // `readme` was in the way of, then removed `readme`, so neither
2617 // spelling was left and the uncommitted edit was lost, as was a file
2618 // in a directory renamed only in case. Nothing is put back now: the
2619 // edits are in the focus, and on disk under one spelling or the other.
2620 // Run with TMPDIR on such a mount (exFAT through FUSE) to take that
2621 // case; elsewhere the two names are two files.
2622 let rename = |from: &str, to: &str| {
2623 format!("map (\\e -> if e.path == {} then e {{ path = {} }} else e)", from, to)
2624 };
2625 let edit = |first: &str| {
2626 format!(
2627 "mapRoot (\\c -> c {{ files = {} ({} c.files) ++ [{{ path = [{}], content = blob \"deep\" }}] }})",
2628 rename("[\"README\"]", "[\"readme\"]"),
2629 rename("[\"Docs\" \"a\"]", "[\"docs\" \"a\"]"),
2630 too_deep(first)
2632 };
2633 let listed = |dir: &std::path::Path| {
2634 let mut names: Vec<String> = std::fs::read_dir(dir)
2635 .unwrap()
2636 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
2637 .filter(|n| !n.starts_with('.'))
2638 .collect();
2639 names.sort();
2640 names
2641 };
2642 // the checkout fails after the renames (at `zz`), or before them (at
2643 // `A`), when `readme` and `docs` name the untouched `README` and `Docs`
2644 // where case folds
2645 for (first, readme, docs) in [("zz", "readme", "docs"), ("A", "README", "Docs")] {
2646 let r = setup();
2647 r.write("README", "hello\n");
2648 std::fs::create_dir(r.dir.join("Docs")).unwrap();
2649 r.write("Docs/a", "a\n");
2650 r.write("notes", "n\n");
2651 r.j(&["describe \"base\""]).ok();
2652 r.write("README", "hello, my uncommitted edit\n");
2653 r.write("Docs/a", "a, my uncommitted edit\n");
2654 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2655 let out = r.j(&[&edit(first)]);
2656 assert_eq!(out.code, 1, "{}", out.stderr);
2657 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
2658 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
2659 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2660 let mut names = vec![docs.to_string(), readme.to_string(), "notes".to_string(), first.to_string()];
2661 names.sort();
2662 assert_eq!(listed(&r.dir), names, "{}", first);
2663 assert_eq!(listed(&r.dir.join(docs)), ["a"], "{}", first);
2664 assert_eq!(r.read(readme), "hello, my uncommitted edit\n");
2665 assert_eq!(r.read(&format!("{}/a", docs)), "a, my uncommitted edit\n");
2666 // the next run records nothing: the edits are in the focus
2667 r.j(&["id"]).ok();
2668 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
2669 let texts = r
2670 .j(&["\\r -> show [(contentAt [\"docs\" \"a\"] (files r)) (contentAt [\"readme\"] (files r)) (contentAt [\"notes\"] (files r))]"])
2671 .ok()
2672 .stdout;
2673 assert_eq!(
2674 texts.split_whitespace().collect::<Vec<_>>().join(" "),
2675 r#"[(blob "a, my uncommitted edit\n") (blob "hello, my uncommitted edit\n") (blob "n\n")]"#
2676 );
2680#[test]
2681fn reserved_names_in_another_case_refused_where_the_filesystem_folds_case() {
2682 // on a case-folding filesystem (macOS, Windows, exFAT) `.GIT` is `.git`,
2683 // which jj's checkout refuses after the operation is recorded or, where
2684 // each spelling has its own inode number (exfat-fuse), writes into;
2685 // elsewhere it is an ordinary name the snapshot tracks. Run with TMPDIR
2686 // on such a mount to take the first branch.
2687 let r = setup();
2688 r.write("a.txt", "x\n");
2689 r.j(&["describe \"stable\""]).ok();
2690 let folds = folds_case(&r.dir);
2691 let ops = r.j(&["ops"]).ok().stdout.lines().count();
2692 let edit = r#"mapRoot (\c -> c { files = c.files ++ [{ path = ["sub" ".GIT" "x"], content = blob "x" }] })"#;
2693 let dry = r.j(&[&format!("tree . validate . {}", edit)]);
2694 let out = r.j(&[edit]);
2695 if folds {
2696 assert_eq!(dry.code, 1, "validate accepted it: {}", dry.stdout);
2697 assert_eq!(out.code, 1, "{}", out.stderr);
2698 assert!(out.stderr.contains("path component"), "{}", out.stderr);
2699 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
2700 } else {
2701 assert_eq!(dry.code, 0, "{}", dry.stderr);
2702 assert_eq!(out.code, 0, "{}", out.stderr);
2703 assert_eq!(r.read("sub/.GIT/x"), "x");
2705 r.j(&["describe \"after\""]).ok();
2708#[test]
2709fn root_path_entry_is_a_crash_not_a_panic() {
2710 // jj's tree builder asserts a path is not the root: exit 101 and a Rust
2711 // panic, which `or` could not catch
2712 let r = setup();
2713 r.write("a.txt", "x\n");
2714 r.j(&["describe \"stable\""]).ok();
2715 let log = r.j(&["log"]).ok().stdout;
2716 let out = r.j(&["replay [{ path = ./, content = blob \"x\" }] ({ from = [], to = [] })"]);
2717 assert_eq!(out.code, 1, "{}", out.stderr);
2718 assert!(out.stderr.starts_with("j: crash:"), "{}", out.stderr);
2719 let out = r
2720 .j(&["(show (replay [] ({ from = [], to = [{ path = ./, content = blob \"x\" }] }))) or \"caught\""])
2721 .ok();
2722 assert_eq!(out.stdout.trim(), "caught");
2723 let edit = "mapRoot (\\c -> c { files = c.files ++ [{ path = ./, content = blob \"x\" }] })";
2724 let out = r.j(&[&format!("tree . validate . {}", edit)]);
2725 assert_eq!(out.code, 1, "validate accepted it: {}", out.stdout);
2726 let out = r.j(&[edit]);
2727 assert_eq!(out.code, 1, "{}", out.stderr);
2728 assert!(out.stderr.contains("root path"), "{}", out.stderr);
2729 assert_eq!(r.j(&["log"]).ok().stdout, log);
2732#[test]
2733fn file_and_directory_at_one_path_refused() {
2734 // a jj tree holds `a` as a file or as a directory, not both: persisting
2735 // kept `a/b` and silently dropped the file `a`, and replay did the same,
2736 // disagreeing with the in-memory backend (§7.3, §10)
2737 let r = setup();
2738 r.write("a.txt", "x\n");
2739 r.j(&["describe \"stable\""]).ok();
2740 let log = r.j(&["log"]).ok().stdout;
2741 let edit = "mapRoot (\\c -> c { files = [({ path = [\"a\"], content = blob \"x\\n\" }) ({ path = [\"a\" \"b\"], content = blob \"y\\n\" })] })";
2742 let out = r.j(&[&format!("tree . validate . {}", edit)]);
2743 assert_eq!(out.code, 1, "validate accepted it: {}", out.stdout);
2744 let out = r.j(&[edit]);
2745 assert_eq!(out.code, 1, "{}", out.stderr);
2746 assert!(out.stderr.contains("both a file and a directory"), "{}", out.stderr);
2747 assert_eq!(r.j(&["log"]).ok().stdout, log);
2748 assert_eq!(r.read("a.txt"), "x\n");
2749 assert!(!r.dir.join("a").exists());
2750 let out = r.j(&[
2751 "let x = [({ path = ./a, content = blob \"file\" }) ({ path = ./a/b, content = blob \"nested\" })] in replay x ({ from = [], to = [] })",
2752 ]);
2753 assert_eq!(out.code, 1, "replay dropped a file: {}", out.stdout);
2754 assert!(out.stderr.contains("both a file and a directory"), "{}", out.stderr);
2757#[test]
2758fn replay_making_a_file_and_a_directory_of_one_path_refused() {
2759 // rebasing a commit that adds `a/b` onto one with a file `a`: jj's merge
2760 // makes one conflict at `a` with the directory as a side, so the rebase
2761 // and its `validate` dry run exited 0 with `a/b` gone from `files` (and,
2762 // while such a side was stored as an empty file, from every tree). The
2763 // in-memory backend left both `a` and `a/b` instead, which its next
2764 // replay refused (§7.3).
2765 let clash = "would be a file on one side and a directory on another";
2766 let r = setup();
2767 r.write("a", "a\n");
2768 r.j(&["describe \"B\""]).ok();
2769 r.j(&["new . top"]).ok();
2770 std::fs::create_dir(r.dir.join("a")).unwrap();
2771 r.write("a/b", "nested\n");
2772 r.j(&["describe \"S\""]).ok();
2773 let log = r.j(&["log"]).ok().stdout;
2774 let out = r.j(&["tree . validate . rebase siblings"]);
2775 assert_eq!(out.code, 1, "validate accepted it: {}", out.stdout);
2776 assert!(out.stderr.contains(&format!("`a` {}", clash)), "{}", out.stderr);
2777 let out = r.j(&["rebase siblings"]);
2778 assert_eq!(out.code, 1, "{}", out.stderr);
2779 assert!(out.stderr.contains(&format!("`a` {}", clash)), "{}", out.stderr);
2780 assert_eq!(r.j(&["log"]).ok().stdout, log);
2781 let paths = r.j(&["\\r -> show (map (.path) (files r))"]).ok().stdout;
2782 assert_eq!(paths.trim(), "[[\"a\" \"b\"]]");
2783 assert_eq!(r.read("a/b"), "nested\n");
2784 // the builtin, either way round, and catchable
2785 let file = "[{ path = ./a, content = blob \"x\" }]";
2786 let dir = "[{ path = ./a/b, content = blob \"y\" }]";
2787 for (onto, to) in [(file, dir), (dir, file)] {
2788 let e = format!("replay {} ({{ from = [], to = {} }})", onto, to);
2789 let out = r.j(&[&format!("show ({})", e)]);
2790 assert_eq!(out.code, 1, "{}", out.stdout);
2791 assert!(out.stderr.contains(clash), "{}", out.stderr);
2792 let out = r.j(&[&format!("(show ({})) or \"caught\"", e)]).ok();
2793 assert_eq!(out.stdout.trim(), "caught");
2795 // jj merges `a` as one value, so the deletion of `a/z` onto a snapshot
2796 // that made the directory `a` a file is refused too, where the in-memory
2797 // merge goes path by path and gives the file (§7.3, §10)
2798 let e = "replay [{ path = ./a, content = blob \"f\" }] \
2799 ({ from = [({ path = ./a/y, content = blob \"y\" }) ({ path = ./a/z, content = blob \"z\" })], \
2800 to = [{ path = ./a/y, content = blob \"y\" }] })";
2801 let out = r.j(&[&format!("show ({})", e)]);
2802 assert_eq!(out.code, 1, "{}", out.stdout);
2803 assert!(out.stderr.contains(&format!("`a` {}", clash)), "{}", out.stderr);
2806#[test]
2807fn conflict_directory_sides_kept_whole() {
2808 // jj keeps a conflict between a file and a directory as one path whose
2809 // side is the whole directory. Such a side was read as an empty file and
2810 // written back as one: the directory was gone from the stored tree, and
2811 // rebasing the commit back where its sides cancel left an empty file `a`
2812 // in place of the user's `a/b` (§7.3).
2813 let conflicted = "\\r -> show (conflicted (files r))";
2814 let paths = "\\r -> show (map (.path) (files r))";
2815 let onto = |m: &str| format!("rebase (matching (\\c -> c.message == \"{}\") all)", m);
2817 // R replaces the file `a` with a directory, L deletes it: R onto L is a
2818 // conflict with R's directory as a side, and no side a file
2819 let r = setup();
2820 r.write("a", "base\n");
2821 r.j(&["describe \"P\""]).ok();
2822 r.j(&["new"]).ok();
2823 std::fs::remove_file(r.dir.join("a")).unwrap();
2824 r.j(&["describe \"L\""]).ok();
2825 r.j(&["new . goto parents"]).ok();
2826 r.write("a", "other\n");
2827 r.j(&["describe \"Q\""]).ok();
2828 r.j(&["new . goto parents"]).ok();
2829 std::fs::remove_file(r.dir.join("a")).unwrap();
2830 std::fs::create_dir(r.dir.join("a")).unwrap();
2831 r.write("a/b", "deep\n");
2832 r.j(&["describe \"R\""]).ok();
2833 r.j(&[&onto("L")]).ok();
2834 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
2835 // a rewrite writes the conflict back as it was read
2836 r.j(&["describe \"R again\""]).ok();
2837 // onto Q's file `a`: the directory stays a side of R's conflict, now
2838 // beside Q's file, and is not refused, as it was a side already (§7.3)
2839 r.j(&[&onto("Q")]).ok();
2840 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
2841 // R back onto P: the sides cancel, leaving R's own directory
2842 r.j(&[&onto("P")]).ok();
2843 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
2844 assert_eq!(r.j(&[paths]).ok().stdout.trim(), "[[\"a\" \"b\"]]");
2845 assert_eq!(r.read("a/b"), "deep\n");
2847 // both sides replace the directory `a` with a file: the base is the
2848 // directory, and it cancels against P's when R goes back
2849 let r = setup();
2850 r.write("base.txt", "base\n");
2851 std::fs::create_dir(r.dir.join("a")).unwrap();
2852 r.write("a/b", "deep\n");
2853 r.j(&["describe \"P\""]).ok();
2854 r.j(&["new"]).ok();
2855 std::fs::remove_dir_all(r.dir.join("a")).unwrap();
2856 r.write("a", "left\n");
2857 r.j(&["describe \"L\""]).ok();
2858 r.j(&["new . goto parents"]).ok();
2859 std::fs::remove_dir_all(r.dir.join("a")).unwrap();
2860 r.write("a", "right\n");
2861 r.j(&["describe \"R\""]).ok();
2862 r.j(&["rebase siblings"]).ok();
2863 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
2864 let back = "\\r -> show (conflicted (files (rebase (parents . prev) r)))";
2865 assert_eq!(r.j(&[back]).ok().stdout.trim(), "[]");
2866 r.j(&["rebase (parents . prev)"]).ok();
2867 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
2868 assert_eq!(r.j(&[paths]).ok().stdout.trim(), "[[\"a\"] [\"base.txt\"]]");
2869 assert_eq!(r.read("a"), "right\n");
2872#[test]
2873fn conflict_between_a_file_and_a_directory_resolved_in_a_child_squashes() {
2874 // resolving a conflict in a child and squashing it: contract replays the
2875 // child with `from` the conflicted parent, whose file side the merge
2876 // carries beside the child's directory. That mix came in with the
2877 // conflict, but replay refused it as one it made, so `squash`, its
2878 // dry run, `abandon . goto parents`, and the law
2879 // (abandon . contract m . split m) r = r all crashed (§7.3, §8)
2880 let conflicted = "\\r -> show (conflicted (files r))";
2881 let paths = "\\r -> show (map (.path) (files r))";
2882 let resolve_into_directory = |r: &Repo| {
2883 r.j(&["new"]).ok();
2884 let a = r.dir.join("a");
2885 std::fs::remove_file(&a).or_else(|_| std::fs::remove_dir_all(&a)).unwrap();
2886 std::fs::create_dir(&a).unwrap();
2887 r.write("a/b", "deep2\n");
2888 r.j(&["describe \"C\""]).ok();
2889 };
2890 let squashes = |r: &Repo| {
2891 for m in ["everything", "(under ./a)"] {
2892 let law = format!("\\r -> remove ((abandon . contract {m} . split {m}) r) == r");
2893 assert_eq!(r.j(&[&law]).ok().stdout.trim(), "true", "{}", m);
2895 r.j(&["tree . validate . squash"]).ok();
2896 r.j(&["tree . validate . abandon . goto parents"]).ok();
2897 r.j(&["squash"]).ok();
2898 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
2899 assert_eq!(r.j(&[paths]).ok().stdout.trim(), "[[\"a\" \"b\"]]");
2900 let parent = "\\r -> show (conflicted (files (up r)))";
2901 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[]");
2902 assert_eq!(r.read("a/b"), "deep2\n");
2903 };
2904 let onto_l = "rebase (matching (\\c -> c.message == \"L\") all)";
2906 // jj's conflict between R's directory and L's deletion of P's file
2907 let r = setup();
2908 r.write("a", "base\n");
2909 r.j(&["describe \"P\""]).ok();
2910 r.j(&["new"]).ok();
2911 std::fs::remove_file(r.dir.join("a")).unwrap();
2912 r.j(&["describe \"L\""]).ok();
2913 r.j(&["new . goto parents"]).ok();
2914 std::fs::remove_file(r.dir.join("a")).unwrap();
2915 std::fs::create_dir(r.dir.join("a")).unwrap();
2916 r.write("a/b", "deep\n");
2917 r.j(&["describe \"R\""]).ok();
2918 r.j(&[onto_l]).ok();
2919 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
2920 resolve_into_directory(&r);
2921 squashes(&r);
2923 // a conflict between two files, resolved into a directory; S, beside L,
2924 // makes `a` a directory
2925 let r = setup();
2926 r.write("a", "base\n");
2927 r.j(&["describe \"P\""]).ok();
2928 r.j(&["new"]).ok();
2929 r.write("a", "left\n");
2930 r.j(&["describe \"L\""]).ok();
2931 r.j(&["new . goto parents"]).ok();
2932 std::fs::remove_file(r.dir.join("a")).unwrap();
2933 std::fs::create_dir(r.dir.join("a")).unwrap();
2934 r.write("a/x", "d\n");
2935 r.j(&["describe \"S\""]).ok();
2936 r.j(&["new . goto parents"]).ok();
2937 r.write("a", "right\n");
2938 r.j(&["describe \"R\""]).ok();
2939 r.j(&[onto_l]).ok();
2940 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
2941 // `a/b` added beside the conflict at `a` is carried as one of its sides,
2942 // where the in-memory merge refuses it (§7.3, §10)
2943 let e = "\\r -> show (conflicted (replay (files r) \
2944 ({ from = [], to = [{ path = ./a/b, content = blob \"b\" }] })))";
2945 assert_eq!(r.j(&[e]).ok().stdout.trim(), "[[\"a\"]]");
2946 // R onto S: S's directory is a side of R's conflict beside its file
2947 // `right`, and only `to` holds a conflict at `a`, so it is carried too
2948 let onto_s = "rebase (matching (\\c -> c.message == \"S\") all)";
2949 let e = format!("\\r -> show (conflicted (files ({} r)))", onto_s);
2950 assert_eq!(r.j(&[&e]).ok().stdout.trim(), "[[\"a\"]]");
2951 r.j(&[&format!("tree . validate . {}", onto_s)]).ok();
2952 resolve_into_directory(&r);
2953 squashes(&r);
2955 // a conflict below the path is none at it: the merge still makes the
2956 // clash between `onto`'s file and `to`'s directory, and is refused
2957 let r = setup();
2958 std::fs::create_dir(r.dir.join("a")).unwrap();
2959 r.write("a/b", "base\n");
2960 r.j(&["describe \"P\""]).ok();
2961 r.j(&["new"]).ok();
2962 r.write("a/b", "left\n");
2963 r.j(&["describe \"L\""]).ok();
2964 r.j(&["new . goto parents"]).ok();
2965 r.write("a/b", "right\n");
2966 r.j(&["describe \"R\""]).ok();
2967 r.j(&[onto_l]).ok();
2968 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\" \"b\"]]");
2969 let e = "\\r -> show (replay [{ path = ./a, content = blob \"f\" }] \
2970 ({ from = files r, to = [{ path = ./a/b, content = blob \"r\" }] }))";
2971 let out = r.j(&[e]);
2972 assert_eq!(out.code, 1, "{}", out.stdout);
2973 assert!(
2974 out.stderr.contains("`a` would be a file on one side and a directory on another"),
2975 "{}",
2976 out.stderr
2977 );
2980#[test]
2981fn a_file_replacing_a_directory_with_a_conflict_inside_is_recorded() {
2982 // the mirror of the test above: a conflict inside the directory `a`,
2983 // resolved in a child by replacing `a` with a file. jj's scan keeps the
2984 // directory's sides at `a` where a file is found there, and drops only
2985 // the entries below it, so the file reached no tree: a debug build
2986 // failed an assertion on every run that scanned (exit 101, `undo`
2987 // included), and a release build recorded the child without `a` (§7.4)
2988 let conflicted = "\\r -> show (conflicted (files r))";
2989 let paths = "\\r -> show (map (.path) (files r))";
2990 let onto_l = "rebase (matching (\\c -> c.message == \"L\") all)";
2991 // R, rebased onto its sibling L, conflicted at `path`
2992 let conflict_at = |path: &str, beside: &[&str]| {
2993 let r = setup();
2994 let write = |content: &str| {
2995 std::fs::create_dir_all(r.dir.join(path).parent().unwrap()).unwrap();
2996 r.write(path, content);
2997 };
2998 write("base\n");
2999 for p in beside {
3000 r.write(p, "beside\n");
3002 r.j(&["describe \"P\""]).ok();
3003 r.j(&["new"]).ok();
3004 write("left\n");
3005 r.j(&["describe \"L\""]).ok();
3006 r.j(&["new . goto parents"]).ok();
3007 write("right\n");
3008 r.j(&["describe \"R\""]).ok();
3009 r.j(&[onto_l]).ok();
3010 r.j(&["new"]).ok();
3012 };
3014 let r = conflict_at("a/b", &[]);
3015 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\" \"b\"]]");
3016 // a checkout that writes the file `a` over the conflicted directory,
3017 // and then fails, leaves the file there: the next run scans it over
3018 // that directory, which the working-copy state still records
3019 let ops = r.j(&["ops"]).ok().stdout.lines().count();
3020 let edit = format!(
3021 "\\r -> mapRoot (\\c -> c {{ files = [({{ path = ./a, content = blob \"f\" }}) \
3022 ({{ path = [{}], content = blob \"deep\" }})] }}) r",
3023 too_deep("d")
3024 );
3025 let out = r.j(&[&edit]);
3026 assert_eq!(out.code, 1, "{}", out.stderr);
3027 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
3028 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
3029 assert_eq!(r.read("a"), "f");
3030 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
3031 r.j(&["id"]).ok();
3032 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
3033 r.j(&["undo"]).ok();
3034 assert!(r.read("a/b").contains("<<<<<<<"), "{}", r.read("a/b"));
3035 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\" \"b\"]]");
3036 std::fs::remove_dir_all(r.dir.join("a")).unwrap();
3037 r.write("a", "file\n");
3038 // `undo` scans to refuse a working directory not recorded
3039 let out = r.j(&["undo"]);
3040 assert_eq!(out.code, 1, "{}", out.stderr);
3041 assert!(out.stderr.contains("working copy has changes not in @"), "{}", out.stderr);
3042 assert_eq!(r.j(&[paths]).ok().stdout.trim(), "[[\"a\"]]");
3043 r.j(&["describe \"C\""]).ok();
3044 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3045 let content = "\\r -> show (contentAt [\"a\"] (files r))";
3046 assert_eq!(r.j(&[content]).ok().stdout.trim(), "blob \"file\\n\"");
3047 // R keeps its conflict; the child resolves it
3048 let parent = "\\r -> show (conflicted (files (up r)))";
3049 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[[\"a\" \"b\"]]");
3050 for m in ["everything", "(under ./a)"] {
3051 let law = format!("\\r -> remove ((abandon . contract {m} . split {m}) r) == r");
3052 assert_eq!(r.j(&[&law]).ok().stdout.trim(), "true", "{}", m);
3054 r.j(&["tree . validate . squash"]).ok();
3055 r.j(&["squash"]).ok();
3056 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3057 assert_eq!(r.j(&[paths]).ok().stdout.trim(), "[[\"a\"]]");
3058 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[]");
3059 assert_eq!(r.read("a"), "file\n");
3061 // deeper down, beside a file the directory keeps
3062 let r = conflict_at("a/x/b", &["a/c"]);
3063 std::fs::remove_dir_all(r.dir.join("a/x")).unwrap();
3064 r.write("a/x", "file\n");
3065 r.j(&["id"]).ok();
3066 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3067 assert_eq!(r.j(&[paths]).ok().stdout.trim(), "[[\"a\" \"c\"] [\"a\" \"x\"]]");
3068 assert_eq!(r.read("a/x"), "file\n");
3071#[cfg(unix)]
3072#[test]
3073fn a_file_written_over_a_conflict_with_a_side_that_is_no_file_is_recorded() {
3074 // a conflict at `a` with a side that is a directory or a symlink is
3075 // checked out as jj's description of its sides. A file written there
3076 // in its place reached no tree: jj's scan found no file merge to read
3077 // it into and kept the conflict, so the run showed the conflict,
3078 // `describe` recorded it, and a checkout wrote the description back
3079 // over the file, which no blob held (§7.4)
3080 let conflicted = "\\r -> show (conflicted (files r))";
3081 let content = "\\r -> show (contentAt [\"a\"] (files r))";
3082 let goto = |m: &str| format!("goto (matching (\\c -> c.message == \"{}\") all)", m);
3083 // R, rebased onto its sibling L, where each changes P's file `a`
3084 let conflict = |left: &dyn Fn(&Repo), right: &dyn Fn(&Repo)| {
3085 let r = setup();
3086 r.write("a", "base\n");
3087 r.j(&["describe \"P\""]).ok();
3088 r.j(&["new"]).ok();
3089 std::fs::remove_file(r.dir.join("a")).unwrap();
3090 left(&r);
3091 r.j(&["describe \"L\""]).ok();
3092 r.j(&["new . goto parents"]).ok();
3093 std::fs::remove_file(r.dir.join("a")).unwrap();
3094 right(&r);
3095 r.j(&["describe \"R\""]).ok();
3096 r.j(&["rebase (matching (\\c -> c.message == \"L\") all)"]).ok();
3097 r.j(&["new"]).ok();
3098 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
3099 assert!(r.read("a").starts_with("Conflict:\n"), "{}", r.read("a"));
3101 };
3103 // R's directory against L's deletion
3104 let r = conflict(&|_| {}, &|r| {
3105 std::fs::create_dir(r.dir.join("a")).unwrap();
3106 r.write("a/b", "deep\n");
3107 });
3108 // the description, left as it is or written again, is the conflict
3109 let description = r.read("a");
3110 r.j(&["id"]).ok();
3111 r.write("a", &description);
3112 r.j(&["id"]).ok();
3113 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"]]");
3114 r.write("a", "resolved\n");
3115 // `undo` scans to refuse a working directory not recorded
3116 let out = r.j(&["undo"]);
3117 assert_eq!(out.code, 1, "{}", out.stderr);
3118 assert!(out.stderr.contains("working copy has changes not in @"), "{}", out.stderr);
3119 assert_eq!(r.j(&[content]).ok().stdout.trim(), "blob \"resolved\\n\"");
3120 r.j(&["describe \"C\""]).ok();
3121 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3122 r.j(&[&goto("P")]).ok();
3123 assert_eq!(r.read("a"), "base\n");
3124 r.j(&[&goto("C")]).ok();
3125 assert_eq!(r.read("a"), "resolved\n");
3126 assert_eq!(r.j(&[content]).ok().stdout.trim(), "blob \"resolved\\n\"");
3127 // R keeps its conflict; the child resolves it
3128 let parent = "\\r -> show (conflicted (files (up r)))";
3129 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[[\"a\"]]");
3130 r.j(&["squash"]).ok();
3131 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3132 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[]");
3133 assert_eq!(r.read("a"), "resolved\n");
3135 // L's symlink against R's file; `a` stays tracked where `.gitignore`
3136 // has come to match it, as a tracked file does
3137 let r = conflict(&|r| std::os::unix::fs::symlink("elsewhere", r.dir.join("a")).unwrap(), &|r| {
3138 r.write("a", "right\n")
3139 });
3140 r.write(".gitignore", "a\n");
3141 r.write("a", "resolved\n");
3142 r.j(&["id"]).ok();
3143 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3144 assert_eq!(r.j(&[content]).ok().stdout.trim(), "blob \"resolved\\n\"");
3145 assert_eq!(r.read("a"), "resolved\n");
3148/// The repository `r` as jj-lib loads it at its head operation, for what a
3149/// test cannot make or see through `j`: a tree as jj itself writes one
3150fn jj_repo(r: &Repo) -> std::sync::Arc<jj_lib::repo::ReadonlyRepo> {
3151 use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
3152 use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
3153 let mut config = StackedConfig::with_defaults();
3154 let user = "[user]\nname = \"Test User\"\nemail = \"test@example.com\"\n";
3155 config.add_layer(ConfigLayer::parse(ConfigSource::User, user).unwrap());
3156 let settings = jj_lib::settings::UserSettings::from_config(config).unwrap();
3157 let ws = jj_lib::workspace::Workspace::load(
3158 &settings,
3159 &r.dir,
3160 &default_backend_factories(),
3161 &default_working_copy_factories(),
3163 .unwrap();
3164 pollster::block_on(ws.repo_loader().load_at_head()).unwrap()
3167/// The stored commit that `id`, an expression of the repo `r` giving one
3168/// commit's `Id`, names
3169fn jj_commit(r: &Repo, repo: &std::sync::Arc<jj_lib::repo::ReadonlyRepo>, id: &str) -> jj_lib::commit::Commit {
3170 use jj_lib::repo::Repo as _;
3171 let hash = r.j(&[&format!("\\r -> (meta ({})).hash", id)]).ok().stdout;
3172 let id = jj_lib::backend::CommitId::try_from_hex(hash.trim()).unwrap();
3173 repo.store().get_commit(&id).unwrap()
3176/// Rebase the commit `id` names onto the one `onto` names, as jj does: it
3177/// merges their trees as trees, so where the commit is conflicted, a path
3178/// can keep each term of its merge, a pair of sides that cancel included
3179fn jj_rebase(r: &Repo, id: &str, onto: &str) {
3180 let repo = jj_repo(r);
3181 let commit = jj_commit(r, &repo, id);
3182 let onto = jj_commit(r, &repo, onto);
3183 let mut tx = repo.start_transaction();
3184 pollster::block_on(jj_lib::rewrite::rebase_commit(tx.repo_mut(), commit, vec![onto.id().clone()])).unwrap();
3185 pollster::block_on(tx.repo_mut().rebase_descendants()).unwrap();
3186 pollster::block_on(tx.commit("rebase")).unwrap();
3189#[cfg(unix)]
3190#[test]
3191fn a_conflict_with_a_side_that_is_no_file_left_as_written_stays_when_its_sides_cancel() {
3192 // a tree of five sides, as jj's rebase of a conflicted commit makes one,
3193 // with a file conflict at `a` and a symlink/file conflict at `b`, whose
3194 // description names L's symlink as removed once and added twice (`j`
3195 // reads the conflict with the pair dropped, but keeps the tree as jj
3196 // stored it). Resolving `a` lets the scan write a tree of three
3197 // sides, where `b`'s conflict has the pair cancelled and describes
3198 // itself in three lines, while the file, which nobody touched, still
3199 // holds the five a checkout wrote. The scan took that file for one
3200 // written over the conflict and recorded the description as `b`'s text:
3201 // the run showed `b` resolved, `undo` refused a directory nobody had
3202 // changed, and `describe` recorded the text in C (§7.4)
3203 let conflicted = "\\r -> show (conflicted (files r))";
3204 let parent = "\\r -> show (conflicted (files (up r)))";
3205 let content = "\\r -> show (contentAt [\"b\"] (files r))";
3206 let goto = |m: &str| format!("goto (matching (\\c -> c.message == \"{}\") all)", m);
3207 let r = setup();
3208 r.write("a", "base\n");
3209 r.write("b", "b0\n");
3210 r.j(&["describe \"P\""]).ok();
3211 r.j(&["new"]).ok();
3212 std::fs::remove_file(r.dir.join("b")).unwrap();
3213 std::os::unix::fs::symlink("elsewhere", r.dir.join("b")).unwrap();
3214 r.j(&["describe \"L\""]).ok();
3215 r.j(&["new . goto parents"]).ok();
3216 r.write("a", "right\n");
3217 r.write("b", "b2\n");
3218 r.j(&["describe \"R\""]).ok();
3219 r.j(&["rebase (matching (\\c -> c.message == \"L\") all)"]).ok();
3220 r.j(&[&goto("L")]).ok();
3221 r.j(&["new"]).ok();
3222 r.write("a", "other\n");
3223 r.j(&["describe \"M\""]).ok();
3224 // R onto M as jj rebases it, keeping the pair at `b` (replay drops it)
3225 jj_rebase(&r, "head (matching (\\c -> c.message == \"R\") all r)", "r.root.id");
3226 r.j(&[&goto("R")]).ok();
3227 r.j(&["new"]).ok();
3228 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"] [\"b\"]]");
3229 let repo = jj_repo(&r);
3230 let tree = |id: &str| jj_commit(&r, &repo, id).tree_ids().clone();
3231 assert_eq!(tree("r.root.id").iter().count(), 5);
3232 assert_eq!(tree("r.root.id"), tree("(up r).root.id"));
3233 let description = r.read("b");
3234 assert_eq!(description.lines().count(), 6, "{}", description);
3235 let modified = || std::fs::symlink_metadata(r.dir.join("b")).unwrap().modified().unwrap();
3236 let written = modified();
3238 r.write("a", "resolved\n");
3239 r.j(&["describe \"C\""]).ok();
3240 r.j(&["new"]).ok();
3241 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[[\"b\"]]");
3242 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"b\"]]");
3243 assert_eq!(r.read("b"), description);
3244 assert_eq!(modified(), written);
3245 let out = r.j(&["undo"]);
3246 assert_eq!(out.code, 0, "{}", out.stderr);
3247 r.j(&["redo"]).ok();
3248 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"b\"]]");
3249 assert_eq!(modified(), written);
3250 // the scan reads no file the working copy's state records as it was
3251 // written, as jj's reads none: one describing the conflict otherwise,
3252 // as another version of jj may have, stays the conflict
3253 let other = description.replace("Conflict:", "CONFLICT:");
3254 r.write("b", &other);
3255 let file = std::fs::File::options().write(true).open(r.dir.join("b")).unwrap();
3256 file.set_modified(written).unwrap();
3257 drop(file);
3258 r.j(&["id"]).ok();
3259 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"b\"]]");
3260 assert_eq!(r.read("b"), other);
3261 // the description written again, as an editor saving it unchanged
3262 // does, is still the conflict
3263 r.write("b", &description);
3264 r.j(&["id"]).ok();
3265 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"b\"]]");
3266 // and so is it where the working-copy state is lost, and made again
3267 // recording no file: nothing is recorded, and `undo` does not refuse
3268 let ops = r.j(&["ops"]).ok().stdout.lines().count();
3269 std::fs::remove_file(r.dir.join(".jj/working_copy/tree_state")).unwrap();
3270 r.j(&["id"]).ok();
3271 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
3272 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"b\"]]");
3273 let out = r.j(&["undo"]);
3274 assert_eq!(out.code, 0, "{}", out.stderr);
3275 r.j(&["redo"]).ok();
3276 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"b\"]]");
3277 // anything else written over it is recorded
3278 r.write("b", "mine\n");
3279 r.j(&["describe \"D\""]).ok();
3280 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
3281 assert_eq!(r.j(&[content]).ok().stdout.trim(), "blob \"mine\\n\"");
3282 assert_eq!(r.j(&[parent]).ok().stdout.trim(), "[[\"b\"]]");
3285#[test]
3286fn edit_applied_to_an_edit_is_a_contract_crash() {
3287 // `new new` gives `new` a function where its signature wants a Repo: a
3288 // contract crash (§4.13), so nothing is persisted (§1.2). It used to
3289 // compose instead, and persisted two new commits.
3290 let r = setup();
3291 let before = r.j(&["length . commits . top"]).ok().stdout;
3292 let out = r.j(&["new new"]);
3293 assert_eq!(out.code, 1, "stdout: {}", out.stdout);
3294 assert!(
3295 out.stderr.contains("contract: new expected Repo (record) as argument 1, got function"),
3296 "{}",
3297 out.stderr
3298 );
3299 let after = r.j(&["length . commits . top"]).ok().stdout;
3300 assert_eq!(before, after);
3303#[test]
3304fn split_and_contract_paths() {
3305 let r = setup();
3306 r.write("code.rs", "fn main() {}\n");
3307 r.write("doc.md", "# doc\n");
3308 r.j(&["describe \"both\""]).ok();
3309 r.j(&["split (ext \"rs\")"]).ok();
3310 let out = r.j(&["tree"]).ok();
3311 assert!(out.stdout.contains("both"), "{}", out.stdout);
3314#[test]
3315fn label_literal_empty_without_remote() {
3316 let r = setup();
3317 let out = r.j(&["labelled \"main\""]).ok();
3318 assert!(out.stdout.contains("none") || out.stdout.contains("0"), "{}", out.stdout);
3319 let out = r.j(&["trunk"]).ok();
3320 assert!(out.stdout.contains("none") || out.stdout.contains("0"), "{}", out.stdout);
3323#[test]
3324fn interrupted_expression_persists_nothing() {
3325 let r = setup();
3326 r.write("a.txt", "x\n");
3327 r.j(&["describe \"stable\""]).ok();
3328 // a non-terminating expression; kill it quickly
3329 let mut child = Command::new(j_bin())
3330 .arg("let go = \\n -> go (n + 1) in go 0")
3331 .current_dir(&r.dir)
3332 .env("XDG_CONFIG_HOME", &r.cfg)
3333 .env("NO_COLOR", "1")
3334 .stdout(Stdio::null())
3335 .stderr(Stdio::null())
3336 .spawn()
3337 .unwrap();
3338 std::thread::sleep(std::time::Duration::from_millis(500));
3339 child.kill().unwrap();
3340 let _ = child.wait();
3341 // lock released, repo intact
3342 let out = r.j(&["status"]).ok();
3343 assert!(out.stdout.contains("stable"));
3346#[test]
3347fn init_refuses_inside_existing_repo() {
3348 let r = setup();
3349 let out = r.j(&["init"]);
3350 assert_eq!(out.code, 2);
3351 assert!(out.stderr.contains("already"), "{}", out.stderr);
3354#[test]
3355fn show_files_roundtrips_binary_and_typed_blobs() {
3356 let r = setup();
3357 let bytes: Vec<u8> = (0..=255).collect();
3358 std::fs::write(r.dir.join("binary"), &bytes).unwrap();
3359 #[cfg(unix)]
3361 use std::os::unix::fs::PermissionsExt;
3362 r.write("executable", "#!/bin/sh\necho preserved\n");
3363 std::fs::set_permissions(r.dir.join("executable"), std::fs::Permissions::from_mode(0o755)).unwrap();
3364 std::os::unix::fs::symlink("binary", r.dir.join("link")).unwrap();
3367 let before = r.operation_heads();
3368 let shown = r.j(&["show . files"]).ok().stdout;
3369 let equal = format!("\\r -> files r == ({})", shown);
3370 assert_eq!(r.j(&[&equal]).ok().stdout.trim(), "true");
3371 assert_eq!(r.operation_heads(), before);
3373 // Check out an empty child, then reconstruct its files from the shown
3374 // expression so the backend must write the decoded bytes and kinds.
3375 r.j(&["mapRoot (\\c -> c { files = [] }) . new . describe \"source\""]).ok();
3376 assert!(!r.dir.join("binary").exists());
3377 #[cfg(unix)]
3379 assert!(!r.dir.join("executable").exists());
3380 assert!(r.dir.join("link").symlink_metadata().is_err());
3382 let restore = format!("mapRoot (\\c -> c {{ files = {}, message = \"restored\" }})", shown);
3383 r.j(&[&restore]).ok();
3384 assert_eq!(std::fs::read(r.dir.join("binary")).unwrap(), bytes);
3385 #[cfg(unix)]
3387 use std::os::unix::fs::PermissionsExt;
3388 assert_eq!(r.read("executable"), "#!/bin/sh\necho preserved\n");
3389 assert_ne!(std::fs::metadata(r.dir.join("executable")).unwrap().permissions().mode() & 0o111, 0);
3390 assert!(r.dir.join("link").symlink_metadata().unwrap().is_symlink());
3391 assert_eq!(std::fs::read_link(r.dir.join("link")).unwrap(), PathBuf::from("binary"));
3393 assert_eq!(r.j(&[&equal]).ok().stdout.trim(), "true");
3396#[test]
3397fn invalid_utf8_config_is_preserved() {
3398 let r = setup();
3399 let cfg = r.cfg.join("j/config.j");
3400 let original = b"-- Custom config with a Latin-1 comment: caf\xe9\n";
3401 std::fs::write(&cfg, original).unwrap();
3402 let before = r.operation_heads();
3404 let out = r.j(&["describe \"must not run\""]);
3405 assert_eq!(out.code, 3, "{}", out.stderr);
3406 assert!(out.stderr.contains("cannot read config"), "{}", out.stderr);
3407 assert!(!out.stderr.contains("created an editable default config"), "{}", out.stderr);
3408 assert_eq!(std::fs::read(&cfg).unwrap(), original);
3409 assert_eq!(r.operation_heads(), before);
3412#[test]
3413fn user_identity_control_characters_do_not_panic() {
3414 let temp = tempfile::tempdir().unwrap();
3415 let r = Repo { dir: temp.path().join("repo"), cfg: temp.path().join("config") };
3416 std::fs::create_dir(&r.dir).unwrap();
3417 std::fs::create_dir_all(r.cfg.join("j")).unwrap();
3418 // Git signatures exclude CR/LF; exercise every other ASCII control
3419 // character, including those the old TOML builder failed to escape.
3420 let controls: String = (0u8..=31).chain([127])
3421 .filter(|c| !matches!(c, b'\r' | b'\n')).map(char::from).collect();
3422 let name = format!("A{}\\\"éZ", controls);
3423 let email = format!("e{}\\\"é@example.com", controls);
3424 let name_literal = j::show::text_literal(&name);
3425 let email_literal = j::show::text_literal(&email);
3426 let config = include_str!("../config.j")
3427 .replace("\"Your Name\"", &name_literal)
3428 .replace("\"you@example.com\"", &email_literal);
3429 std::fs::write(r.cfg.join("j/config.j"), config).unwrap();
3431 // init uses strict identity loading; subsequent commands open an
3432 // existing workspace. Both used to parse hand-built, invalid TOML.
3433 r.j(&["init"]).ok();
3434 assert_eq!(r.j(&["1"]).ok().stdout, "1\n");
3435 r.j(&["describe \"identity preserved\""]).ok();
3436 assert_eq!(r.j(&["show user.name"]).ok().stdout, format!("{}\n", name_literal));
3437 assert_eq!(r.j(&["show user.email"]).ok().stdout, format!("{}\n", email_literal));
3440#[test]
3441fn missing_config_is_created_editable() {
3442 let dir = std::env::temp_dir().join(format!(
3443 "j-cli-nocfg-{}-{}",
3444 std::process::id(),
3445 std::time::SystemTime::now()
3446 .duration_since(std::time::UNIX_EPOCH)
3447 .unwrap()
3448 .as_nanos()
3449 ));
3450 let _ = std::fs::remove_dir_all(&dir);
3451 std::fs::create_dir_all(&dir).unwrap();
3452 let cfg_home = dir.join("nonexistent");
3453 let out = Command::new(j_bin())
3454 .arg("status")
3455 .current_dir(&dir)
3456 .env("XDG_CONFIG_HOME", &cfg_home)
3457 .output()
3458 .unwrap();
3459 // no config error: the default config is materialised instead
3460 let stderr = String::from_utf8_lossy(&out.stderr);
3461 assert!(!stderr.contains("no config"), "{}", stderr);
3462 assert!(stderr.contains("created an editable default config"), "{}", stderr);
3463 // the config now exists at the user path and is editable by the owner
3464 let cfg = cfg_home.join("j/config.j");
3465 let written = std::fs::read_to_string(&cfg).expect("config was created");
3466 assert_eq!(written, include_str!("../config.j"));
3467 #[cfg(unix)]
3469 use std::os::unix::fs::PermissionsExt;
3470 assert_eq!(std::fs::metadata(&cfg).unwrap().permissions().mode() & 0o777, 0o600);
3472 let _ = std::fs::remove_dir_all(&dir);
3475#[test]
3476fn unreadable_config_is_not_replaced_with_defaults() {
3477 let r = setup();
3478 let path = r.cfg.join("j/config.j");
3479 let mut original = std::fs::read(&path).unwrap();
3480 original.extend_from_slice(b"\n-- non-UTF-8 comment: \xff\n");
3481 std::fs::write(&path, &original).unwrap();
3482 let out = r.j(&["1 + 1"]);
3483 assert_eq!(out.code, 3, "{}", out.stderr);
3484 assert!(out.stderr.starts_with("j: cannot read config at "), "{}", out.stderr);
3485 assert!(out.stdout.is_empty());
3486 assert_eq!(std::fs::read(&path).unwrap(), original);
3489#[test]
3490fn missing_config_symlink_target_is_not_created() {
3491 let r = setup();
3492 let path = r.cfg.join("j/config.j");
3493 let target = r.cfg.join("missing-config.j");
3494 std::fs::remove_file(&path).unwrap();
3495 std::os::unix::fs::symlink(&target, &path).unwrap();
3496 let out = r.j(&["1 + 1"]);
3497 assert_eq!(out.code, 3, "{}", out.stderr);
3498 assert!(std::fs::symlink_metadata(&path).unwrap().file_type().is_symlink());
3499 assert!(!target.exists());
3502// ----------------------------------------------------------------------
3503// regressions
3504// ----------------------------------------------------------------------
3506#[test]
3507fn read_only_run_does_not_hide_working_copy_changes() {
3508 // A printing run snapshots the working directory but records no
3509 // operation. It must not save the scanned tree state either: doing so
3510 // told the next run's incremental snapshot that the directory was already
3511 // recorded, so it rebuilt the Repo from the stale tree in the working-copy
3512 // commit — and a later persisting run then checked that stale tree back
3513 // out, discarding the edits from disk (§7.4/§7.7).
3514 let r = setup();
3515 r.write("a.txt", "one\n");
3516 r.j(&["id"]).ok();
3517 r.write("a.txt", "edited content\n");
3518 r.write("b.txt", "brand new\n");
3519 for _ in 0..3 {
3520 let out = r.j(&["files"]).ok().stdout;
3521 assert!(out.contains("a.txt"), "{}", out);
3522 assert!(out.contains("b.txt"), "b.txt missing from: {}", out);
3524 // and the edits survive the next persisting run, on disk and in the commit
3525 r.j(&["describe \"after reads\""]).ok();
3526 assert_eq!(r.read("a.txt"), "edited content\n");
3527 assert_eq!(r.read("b.txt"), "brand new\n");
3528 let files = r.j(&["files"]).ok().stdout;
3529 assert!(files.contains("b.txt"), "{}", files);
3532#[test]
3533fn refused_reserved_command_does_not_hide_working_copy_changes() {
3534 // `undo` refuses a dirty working copy; the snapshot it took to decide
3535 // that must not be saved either, for the same reason
3536 let r = setup();
3537 r.write("a.txt", "one\n");
3538 r.j(&["id"]).ok();
3539 r.write("a.txt", "edited content\n");
3540 assert_eq!(r.j(&["undo"]).code, 1);
3541 let out = r.j(&["files"]).ok().stdout;
3542 assert!(out.contains("a.txt"), "{}", out);
3543 // the size column reflects the edited file, not the recorded one
3544 assert!(out.contains("15 B"), "expected the edited size in: {}", out);
3547#[test]
3548fn tree_does_not_mark_commits_empty() {
3549 // `empty` was read off a file count that is 0 whenever the parent's file
3550 // list was never materialized, so every commit whose parent skipped
3551 // loading files rendered with the empty glyph
3552 let r = setup();
3553 for n in ["a", "b", "c", "d"] {
3554 r.write(&format!("{}.txt", n), &format!("content {}\n", n));
3555 r.j(&[&format!("describe \"commit {}\"", n)]).ok();
3556 r.j(&["new"]).ok();
3558 let out = r.j(&["tree"]).ok().stdout;
3559 // each commit that added a file must not carry the empty glyph (the
3560 // legend at the bottom names it too, so check the commit rows only)
3561 for n in ["a", "b", "c", "d"] {
3562 let row = out
3563 .lines()
3564 .find(|l| l.contains(&format!("commit {}", n)))
3565 .unwrap_or_else(|| panic!("no row for commit {} in:\n{}", n, out));
3566 assert!(!row.contains('◌'), "commit {} marked empty: {:?}", n, row);
3570/// The tree row of the commit whose message is `msg`.
3571fn tree_row<'a>(out: &'a str, msg: &str) -> &'a str {
3572 out.lines()
3573 .find(|l| l.ends_with(&format!(" {}", msg)))
3574 .unwrap_or_else(|| panic!("no row for {:?} in:\n{}", msg, out))
3577#[test]
3578fn dry_run_tree_glyphs_come_from_the_edited_history() {
3579 // `⊗` and `◌` were read off the stored commits by change id, so a dry run
3580 // drew what is stored rather than what the edit would produce (§1.2): a
3581 // rebase that conflicts showed no `⊗`, an edit that resolves a stored
3582 // conflict kept it, and emptying a commit left its glyph and its
3583 // untouched child's as they were
3584 let r = setup();
3585 r.write("f.txt", "base\n");
3586 r.j(&["describe \"base\""]).ok();
3587 r.j(&["new"]).ok();
3588 r.write("f.txt", "left\n");
3589 r.j(&["describe \"left\""]).ok();
3590 r.j(&["describe \"tip\" . new"]).ok();
3591 // `right`: a sibling of `left` on `base`
3592 r.j(&["new . prev . prev"]).ok();
3593 r.write("f.txt", "right\n");
3594 r.j(&["describe \"right\""]).ok();
3596 let out = r.j(&["tree . rebase siblings"]).ok().stdout;
3597 assert!(tree_row(&out, "right").contains('⊗'), "conflict not drawn:\n{}", out);
3599 let emptied = "tree . at siblings (\\r -> r { root = r.root { files = (up r).root.files } })";
3600 let out = r.j(&[emptied]).ok().stdout;
3601 assert!(tree_row(&out, "left").contains('◌'), "emptied commit not empty:\n{}", out);
3602 assert!(!tree_row(&out, "tip").contains('◌'), "changed child still empty:\n{}", out);
3604 // stored with the conflict, then resolved by an edit that is not persisted
3605 r.j(&["rebase siblings"]).ok();
3606 let out = r.j(&["tree"]).ok().stdout;
3607 assert!(tree_row(&out, "right").contains('⊗'), "{}", out);
3608 let resolved = "tree . (\\r -> r { root = r.root { files = (up r).root.files } })";
3609 let out = r.j(&[resolved]).ok().stdout;
3610 let row = tree_row(&out, "right");
3611 assert!(!row.contains('⊗') && row.contains('◌'), "resolved commit drawn as {:?}:\n{}", row, out);
3614#[test]
3615fn tree_draws_a_minted_commit_after_its_stored_sibling() {
3616 // a dry run's new commit has no stored time; it sorted as the oldest, so
3617 // it took its parent's lane and ended it, and the stored sibling forked
3618 // from under it, drawn as its child. Minted commits come last
3619 // (specs/tree.md Step 1), and the stored sibling forks from the parent.
3620 let r = setup();
3621 r.j(&["describe \"base\""]).ok();
3622 r.j(&["describe \"stored\" . new"]).ok();
3623 let out = r.j(&["tree . new . prev"]).ok().stdout;
3624 let rows: Vec<&str> = out.lines().collect();
3625 let stored = rows.iter().position(|l| l.ends_with(" stored")).unwrap();
3626 let minted = rows.iter().position(|l| l.starts_with('▶')).unwrap();
3627 assert!(stored < minted, "minted commit drawn first:\n{}", out);
3628 assert!(rows[stored].contains('├'), "{}", out);
3629 assert!(!rows[minted].contains('├'), "{}", out);
3632#[test]
3633fn tree_draws_the_size_bar_of_the_focus_parent() {
3634 // the focus's parent diffs against its own parent, whose file list was
3635 // only loaded when that grandparent was the root: every other parent of
3636 // the focus lost its bar (specs/tree.md, Step 4 column 5)
3637 let r = setup();
3638 for (n, body) in [("A", "a\n"), ("B", "b\nb\nb\n"), ("C", "c\n")] {
3639 r.write(&format!("{}.txt", n), body);
3640 r.j(&[&format!("describe \"{}\"", n)]).ok();
3641 if n != "C" {
3642 r.j(&["new"]).ok();
3645 let out = r.j(&["tree"]).ok().stdout;
3646 let bars = ['▁', '▂', '▃', '▅', '▇'];
3647 for n in ["B", "C"] {
3648 assert!(tree_row(&out, n).contains(bars), "no size bar on {}:\n{}", n, out);
3650 // the grandparent is neither the focus nor next to it: no bar at detail 1
3651 assert!(!tree_row(&out, "A").contains(bars), "{}", out);
3654#[test]
3655fn tree_data_sizes_commits_by_their_changes() {
3656 // the size bar counted the whole length of both versions of each changed
3657 // file, so a one-line edit to a long file drew `▇`, and the files column
3658 // counted the whole snapshot, so an empty commit showed its parent's file
3659 // count (§7.11 column 5; specs/tree.md Step 4 column 9)
3660 let r = setup();
3661 let big: String = (1..=1500).map(|i| format!("{}\n", i)).collect();
3662 r.write("big", &big);
3663 r.write("small", "hello\n");
3664 r.j(&["describe \"big file\""]).ok();
3665 r.j(&["new"]).ok();
3666 r.write("big", &big.replace("\n750\n", "\nseven fifty\n"));
3667 r.j(&["describe \"one line edit\""]).ok();
3668 r.j(&["new"]).ok();
3669 r.j(&["describe \"empty commit\""]).ok();
3670 let row = |out: &str, msg: &str| -> (String, String) {
3671 let row = out
3672 .lines()
3673 .find(|l| l.contains(&format!(" {} ", msg)))
3674 .unwrap_or_else(|| panic!("no row for {:?} in:\n{}", msg, out))
3675 .to_string();
3676 let bar: String = row.chars().filter(|c| "▁▂▃▅▇".contains(*c)).collect();
3677 let toks: Vec<&str> = row.split_whitespace().collect();
3678 let at = toks.iter().position(|t| *t == "files").expect("files column");
3679 (bar, toks[at - 1].to_string())
3680 };
3681 let out = r.j(&["treeData"]).ok().stdout;
3682 assert_eq!(row(&out, "big file"), ("▇".into(), "2".into()), "{}", out);
3683 assert_eq!(row(&out, "one line edit"), ("▁".into(), "1".into()), "{}", out);
3684 assert_eq!(row(&out, "empty commit"), ("".into(), "0".into()), "{}", out);
3685 // at detail 1 the first commit is not next to the focus and draws no
3686 // bar; its files column is counted all the same
3687 let detail1 = "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = true })";
3688 let out = r.j(&[detail1]).ok().stdout;
3689 assert_eq!(row(&out, "big file"), ("".into(), "2".into()), "{}", out);
3690 assert_eq!(row(&out, "one line edit"), ("▁".into(), "1".into()), "{}", out);
3691 assert_eq!(row(&out, "empty commit"), ("".into(), "0".into()), "{}", out);
3694#[test]
3695fn focus_lists_the_paths_the_commit_changes() {
3696 // `j focus` listed the whole snapshot, counted and unmarked, so a deleted
3697 // file never showed, and any commit shown as a block was drawn as the
3698 // focus (§5.1)
3699 let r = setup();
3700 r.write("a.txt", "a\n");
3701 r.write("b.txt", "b\n");
3702 r.j(&["describe \"A\""]).ok();
3703 r.j(&["new"]).ok();
3704 r.write("a.txt", "a2\n");
3705 std::fs::remove_file(r.dir.join("b.txt")).unwrap();
3706 r.write("c.txt", "c\n");
3707 r.j(&["describe \"B\""]).ok();
3708 let out = r.j(&["focus"]).ok().stdout;
3709 let lines: Vec<&str> = out.lines().collect();
3710 assert!(lines[0].starts_with("◉ ") && lines[0].ends_with(" B"), "{}", out);
3711 assert!(lines[1].ends_with(" · 3 files"), "{}", out);
3712 assert_eq!(lines[2..], ["", " ~ a.txt", " − b.txt", " + c.txt"], "{}", out);
3713 // the parent is drawn as `tree` draws it
3714 let out = r.j(&["\\r -> (up r).root"]).ok().stdout;
3715 let tree = r.j(&["tree"]).ok().stdout;
3716 let lines: Vec<&str> = out.lines().collect();
3717 assert!(tree_row(&tree, "A").contains('●'), "{}", tree);
3718 assert!(lines[0].starts_with("● ") && lines[0].ends_with(" A"), "{}", out);
3719 assert!(lines[1].ends_with(" · 2 files"), "{}", out);
3720 assert_eq!(lines[2..], ["", " + a.txt", " + b.txt"], "{}", out);
3721 // a new commit changes nothing
3722 r.j(&["new"]).ok();
3723 let out = r.j(&["focus"]).ok().stdout;
3724 let lines: Vec<&str> = out.lines().collect();
3725 assert!(lines[0].starts_with("◌ "), "{}", out);
3726 assert!(lines[1].ends_with(" · 0 files"), "{}", out);
3727 assert_eq!(lines.len(), 2, "{}", out);
3730#[test]
3731fn a_commit_is_shown_where_it_stands_after_the_snapshot() {
3732 // a commit's block read its parent from the repository as loaded, before
3733 // the snapshot, while the commit came from the repository the expression
3734 // was given, where the snapshot has rebased every descendant of the
3735 // focus: a dirty focus's edits showed as its child's own changes, and an
3736 // empty grandchild was drawn `○` with a path it does not change (§5.1)
3737 let r = setup();
3738 r.write("a.txt", "one\n");
3739 r.j(&["describe \"A\""]).ok();
3740 r.j(&["new"]).ok();
3741 r.write("c.txt", "c\n");
3742 r.j(&["describe \"C\""]).ok();
3743 r.j(&["new"]).ok();
3744 r.j(&["describe \"E\""]).ok();
3745 r.j(&["prev"]).ok();
3746 r.j(&["prev"]).ok();
3747 r.write("a.txt", "one\ntwo\n");
3748 let out = r.j(&["focus . next"]).ok().stdout;
3749 let lines: Vec<&str> = out.lines().collect();
3750 assert!(lines[0].starts_with("○ ") && lines[0].ends_with(" C"), "{}", out);
3751 assert!(lines[1].ends_with(" · 1 files"), "{}", out);
3752 assert_eq!(lines[2..], ["", " + c.txt"], "{}", out);
3753 let tree = r.j(&["tree"]).ok().stdout;
3754 assert!(tree_row(&tree, "E").contains('◌'), "{}", tree);
3755 let out = r.j(&["focus . next . next"]).ok().stdout;
3756 let lines: Vec<&str> = out.lines().collect();
3757 assert!(lines[0].starts_with("◌ ") && lines[0].ends_with(" E"), "{}", out);
3758 assert!(lines[1].ends_with(" · 0 files"), "{}", out);
3759 assert_eq!(lines.len(), 2, "{}", out);
3760 // a commit table draws each row as the commit's line, glyph included
3761 r.j(&["new"]).ok();
3762 r.write("d.txt", "d\n");
3763 r.j(&["describe \"N\""]).ok();
3764 let tree = r.j(&["tree"]).ok().stdout;
3765 let out = r.j(&["ancestors"]).ok().stdout;
3766 let lines: Vec<&str> = out.lines().collect();
3767 assert_eq!(lines.len(), 3, "{}", out);
3768 assert!(tree_row(&tree, "N").contains('◉'), "{}", tree);
3769 assert!(lines[0].starts_with("◉ ") && lines[0].ends_with(" N"), "{}", out);
3770 assert!(tree_row(&tree, "A").contains('●'), "{}", tree);
3771 assert!(lines[1].starts_with("● ") && lines[1].ends_with(" A"), "{}", out);
3772 assert!(lines[2].starts_with("⌂ "), "{}", out);
3775#[test]
3776fn a_commit_table_reads_its_commits_after_the_snapshot() {
3777 // a list of ids looked its commits up in the repository as loaded, before
3778 // the snapshot rebased the focus's descendants: a child that the working
3779 // directory's edit puts in conflict was drawn `○` where `tree` and the
3780 // child's own line draw `⊗` (§5.1)
3781 let r = setup();
3782 r.write("a.txt", "one\n");
3783 r.j(&["describe \"A\""]).ok();
3784 r.j(&["new"]).ok();
3785 r.write("a.txt", "C\n");
3786 r.j(&["describe \"C\""]).ok();
3787 r.j(&["prev"]).ok();
3788 r.write("a.txt", "zero\n");
3789 let tree = r.j(&["tree"]).ok().stdout;
3790 assert!(tree_row(&tree, "C").contains('⊗'), "{}", tree);
3791 let out = r.j(&["\\r -> [(next r).root.id]"]).ok().stdout;
3792 assert!(out.starts_with("⊗ ") && out.ends_with(" C\n"), "{}", out);
3793 let out = r.j(&["\\r -> { c = (next r).root }"]).ok().stdout;
3794 assert!(out.starts_with("c ⊗ ") && out.ends_with(" C\n"), "{}", out);
3797#[test]
3798fn redo_twice_in_a_row() {
3799 // the second redo followed the redo marker to the undo it reversed and
3800 // restored *that* view, instead of looking outward for an undo that had
3801 // not been reversed yet (§7.7)
3802 let r = setup();
3803 r.write("a.txt", "x\n");
3804 // two operations on the same commit, so undoing both clears the message
3805 r.j(&["describe \"one\""]).ok();
3806 r.j(&["describe \"two\""]).ok();
3807 r.j(&["undo"]).ok();
3808 assert!(r.j(&["status"]).ok().stdout.contains("one"));
3809 r.j(&["undo"]).ok();
3810 let cleared = r.j(&["status"]).ok().stdout;
3811 assert!(!cleared.contains("one"), "{}", cleared);
3812 r.j(&["redo"]).ok();
3813 let first = r.j(&["status"]).ok().stdout;
3814 assert!(first.contains("one"), "first redo: {}", first);
3815 r.j(&["redo"]).ok();
3816 let second = r.j(&["status"]).ok().stdout;
3817 assert!(second.contains("two"), "second redo did not restore it:\n{}", second);
3820#[test]
3821fn undo_redo_cycles_are_stable() {
3822 // alternating undo/redo must keep returning to the same two states
3823 let r = setup();
3824 r.write("a.txt", "x\n");
3825 r.j(&["describe \"one\""]).ok();
3826 for i in 0..3 {
3827 r.j(&["undo"]).ok();
3828 let after = r.j(&["status"]).ok().stdout;
3829 assert!(!after.contains("one"), "cycle {}: {}", i, after);
3830 r.j(&["redo"]).ok();
3831 let back = r.j(&["status"]).ok().stdout;
3832 assert!(back.contains("one"), "cycle {}: {}", i, back);
3836#[test]
3837fn extract_sees_through_lazy_file_lists() {
3838 // a commit's `files` is a thunk until something asks for it; `extract`
3839 // walked past it, so it found no entries at all on a real repository
3840 // while finding them all on the in-memory backend
3841 let r = setup();
3842 r.write("a.txt", "x\n");
3843 r.write("b.txt", "y\n");
3844 r.j(&["id"]).ok();
3845 let entries = r.j(&["length . extract Entry"]).ok().stdout;
3846 assert_eq!(entries.trim(), "2", "extract Entry: {}", entries);
3847 let blobs = r.j(&["length . extract Blob"]).ok().stdout;
3848 assert_eq!(blobs.trim(), "2", "extract Blob: {}", blobs);
3851#[test]
3852fn moving_the_focus_rewrites_uncommitted_edits() {
3853 // A persisting run that starts with uncommitted edits folds their
3854 // snapshot into its operation and then checks out the new focus. The
3855 // checkout diffed from the tree the working copy had last *recorded*,
3856 // not from the snapshot, so a path whose recorded content equals the new
3857 // focus's was never rewritten: `prev` left the child's edit on disk, and
3858 // the next run recorded it into the parent (§7.4, §7.5 step 7).
3859 let r = setup();
3860 r.write("a.txt", "one\n");
3861 r.j(&["describe \"first\""]).ok();
3862 r.j(&["new"]).ok();
3863 r.write("a.txt", "two\n");
3864 r.j(&["prev"]).ok();
3865 assert_eq!(r.read("a.txt"), "one\n");
3866 r.j(&["describe \"first, again\""]).ok();
3867 let files = r.j(&["\\r -> show r.root.files"]).ok().stdout;
3868 assert!(files.contains("blob \"one\\n\""), "the parent took the edit: {}", files);
3869 // the edit itself lives on in the child it was snapshotted into
3870 r.j(&["next"]).ok();
3871 assert_eq!(r.read("a.txt"), "two\n");
3874#[test]
3875fn abandon_discards_uncommitted_edits_from_disk() {
3876 // the same stale diff: the new focus, an empty child of the abandoned
3877 // commit's parent, has exactly the recorded tree, so the checkout did
3878 // nothing at all and the discarded
3879 // edits, deletions and new files stayed on disk to be recorded into the
3880 // new focus by the next run
3881 let r = setup();
3882 r.write("a.txt", "orig\n");
3883 r.write("b.txt", "bee\n");
3884 r.j(&["describe \"base\""]).ok();
3885 r.j(&["new"]).ok();
3886 r.write("a.txt", "scratch\n");
3887 std::fs::remove_file(r.dir.join("b.txt")).unwrap();
3888 r.write("c.txt", "junk\n");
3889 r.j(&["abandon"]).ok();
3890 assert_eq!(r.read("a.txt"), "orig\n");
3891 assert_eq!(r.read("b.txt"), "bee\n");
3892 assert!(!r.dir.join("c.txt").exists(), "c.txt survived the abandon");
3893 // the directory now equals the focus, so recording it is a no-op
3894 r.j(&["id"]).ok();
3895 let ops = r.j(&["ops"]).ok().stdout;
3896 assert!(ops.lines().next().unwrap_or("").contains("abandon"), "{}", ops);
3899#[test]
3900fn reordering_a_snapshot_is_not_a_change() {
3901 // `select` lists the entries a fileset matches first, so a `contract m`
3902 // that moves nothing still hands persistence a parent whose entries are
3903 // in another order. Comparing snapshots as lists called that a change:
3904 // every such run rewrote the parent and each commit above it (new hashes
3905 // and committer times, the same trees) and recorded an operation. A
3906 // snapshot stands for a tree, which has no order (§7.3, §7.5 step 4).
3907 let r = setup();
3908 r.write("a.txt", "a\n");
3909 std::fs::create_dir(r.dir.join("h")).unwrap();
3910 r.write("h/x", "x\n");
3911 r.j(&["describe \"base\""]).ok();
3912 r.j(&["new"]).ok();
3913 r.write("h/x", "x2\n");
3914 r.j(&["describe \"child\""]).ok();
3915 // the first contract moves h/x into base; after it, one moves nothing
3916 r.j(&["contract (under ./h)"]).ok();
3917 let hashes = || {
3918 r.j(&["\\r -> map (\\i -> (meta i).hash) (ancestors r)"])
3919 .ok()
3920 .stdout
3921 };
3922 let before = hashes();
3923 let ops = r.j(&["ops"]).ok().stdout.lines().count();
3924 let rotate = "mapRoot (\\c -> c { files = tail c.files ++ [(head c.files)] })";
3925 for edit in [
3926 "contract (under ./h)".to_string(),
3927 "tree . validate . contract (under ./h)".to_string(),
3928 // the same kind of reorder written by hand, on the focus and its parent
3929 rotate.to_string(),
3930 format!("at parents ({})", rotate),
3931 ] {
3932 r.j(&[&edit]).ok();
3933 let now = r.j(&["ops"]).ok().stdout.lines().count();
3934 assert_eq!(now, ops, "{} was recorded", edit);
3935 assert_eq!(hashes(), before, "{} rewrote a commit", edit);
3937 // an edit in the working directory is recorded, and the parent, which
3938 // comes back only reordered, is kept as stored
3939 let parent_hash = || r.j(&["\\r -> (meta (up r).root.id).hash"]).ok().stdout;
3940 let parent = parent_hash();
3941 r.write("a.txt", "edited\n");
3942 r.j(&["contract (under ./h)"]).ok();
3943 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
3944 assert_eq!(parent_hash(), parent, "the parent was rewritten");
3945 // a real change to the parent is recorded
3946 r.write("h/x", "x3\n");
3947 r.j(&["contract (under ./h)"]).ok();
3948 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
3949 assert_ne!(parent_hash(), parent);
3950 let base = "\\r -> text (contentAt ./h/x (up r).root.files)";
3951 assert_eq!(r.j(&[base]).ok().stdout, "x3\n");
3954#[test]
3955fn replacing_a_file_with_a_directory_survives_persistence() {
3956 // checking out from the stale recorded tree turned a file <-> directory
3957 // swap into working-copy state that contradicts the tree: debug builds
3958 // panicked in jj-lib on this and every later run, release builds dropped
3959 // the directory from disk and from the commit on the next snapshot
3960 let r = setup();
3961 r.write("a", "hi\n");
3962 r.j(&["describe \"file\""]).ok();
3963 r.j(&["new"]).ok();
3964 std::fs::remove_file(r.dir.join("a")).unwrap();
3965 std::fs::create_dir(r.dir.join("a")).unwrap();
3966 r.write("a/b.txt", "t\n");
3967 r.j(&["describe \"dir\""]).ok();
3968 r.j(&["tree"]).ok();
3969 r.j(&["id"]).ok();
3970 assert_eq!(r.read("a/b.txt"), "t\n");
3971 let files = r.j(&["files"]).ok().stdout;
3972 assert!(files.contains("b.txt"), "{}", files);
3973 // and back: a directory replaced by a file
3974 r.j(&["new"]).ok();
3975 std::fs::remove_dir_all(r.dir.join("a")).unwrap();
3976 r.write("a", "file again\n");
3977 r.j(&["describe \"file again\""]).ok();
3978 r.j(&["tree"]).ok();
3979 r.j(&["id"]).ok();
3980 assert_eq!(r.read("a"), "file again\n");
3981 // the checkout itself swaps them back when the focus moves
3982 r.j(&["prev"]).ok();
3983 assert_eq!(r.read("a/b.txt"), "t\n");
3984 r.j(&["prev"]).ok();
3985 assert_eq!(r.read("a"), "hi\n");
3988#[test]
3989fn editing_a_conflict_keeps_its_long_markers() {
3990 // a conflict whose sides hold a 7-dash line (a markdown underline) is
3991 // written with longer markers, and the working copy remembers their
3992 // length. Checking out after a snapshot reset the state of every path
3993 // the snapshot changed, forgetting it, and when the checkout did not
3994 // rewrite the file the next run read it back with 7-character markers:
3995 // the side's own `-------` broke the parse, so a conflict edited with its
3996 // markers kept was recorded as resolved, markers and all (§7.3, §7.4)
3997 let r = setup();
3998 r.write("README.md", "Intro\n");
3999 r.j(&["describe \"base\""]).ok();
4000 r.j(&["new"]).ok();
4001 r.write("README.md", "Intro\n\nUsage\n-------\nA text\n");
4002 r.j(&["describe \"A\""]).ok();
4003 r.j(&["prev"]).ok();
4004 r.j(&["new"]).ok();
4005 r.write("README.md", "Intro\n\nInstall\n-------\nB text\n");
4006 r.j(&["describe \"B\""]).ok();
4007 r.j(&["rebase (matching (\\c -> c.message == \"A\") all)"]).ok();
4008 let text = r.read("README.md");
4009 assert!(text.contains("\n<<<<<<<<<<< "), "not written with long markers:\n{}", text);
4010 let conflicted = "\\r -> show (conflicted r.root.files)";
4011 // a persisting run that leaves the edited file as the snapshot read it
4012 r.write("README.md", &text.replace("B text\n", "B text, edited\n"));
4013 r.j(&["describe \"B edited\""]).ok();
4014 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"README.md\"]]");
4015 r.j(&["id"]).ok();
4016 let content = "\\r -> show (contentAt [\"README.md\"] r.root.files)";
4017 let stored = r.j(&[content]).ok().stdout;
4018 assert!(stored.contains("unresolved") && stored.contains("B text, edited"), "{}", stored);
4019 // one that moves the focus to a child with the same files
4020 r.write("README.md", &text.replace("B text\n", "B text, edited twice\n"));
4021 r.j(&["new"]).ok();
4022 let status = r.j(&["\\r -> show [(status r).changed (status r).conflicts]"]).ok().stdout;
4023 assert_eq!(status.trim(), "[[] [[\"README.md\"]]]");
4024 assert!(r.read("README.md").contains("B text, edited twice\n"));
4027#[test]
4028fn a_directory_that_cannot_be_scanned_mid_run_does_not_fail_the_checkout() {
4029 // §7.4, §7.7: persist's checkout scans the working directory again. An
4030 // entry that appeared during evaluation and cannot be read (a directory
4031 // deeper than PATH_MAX here; an unreadable mount, a temporary file that
4032 // vanishes mid-scan) failed that scan after the operation had been
4033 // published: the run crashed with it recorded and the working copy never
4034 // advanced, and once the directory was gone the next run snapshotted the
4035 // child's files into the new focus, its parent. The checkout now
4036 // describes the snapshot instead, as it did before it rescanned.
4037 let r = setup();
4038 r.write("a", "p\n");
4039 r.j(&["describe \"P\""]).ok();
4040 r.j(&["new"]).ok();
4041 r.write("a", "x1\n");
4042 r.j(&["describe \"X\""]).ok();
4043 r.write("a", "x2\n");
4044 // built outside the working directory: 20 levels of 250 bytes
4045 let half: PathBuf = (0..10).map(|_| "d".repeat(250)).collect();
4046 let deep = r.cfg.join("deep");
4047 std::fs::create_dir_all(deep.join(&half)).unwrap();
4048 std::fs::create_dir_all(r.cfg.join("more").join(&half)).unwrap();
4049 std::fs::rename(r.cfg.join("more"), deep.join(&half).join("more")).unwrap();
4050 let ops_dir = r.dir.join(".jj/repo/op_store/operations");
4051 let count = || std::fs::read_dir(&ops_dir).unwrap().count();
4052 let before = count();
4053 let mut child = Command::new(j_bin())
4054 .arg("\\r -> prev (describe (show (foldl (+) 0 (range 0 1000000))) r)")
4055 .current_dir(&r.dir)
4056 .env("XDG_CONFIG_HOME", &r.cfg)
4057 .env("NO_COLOR", "1")
4058 .stdout(Stdio::null())
4059 .stderr(Stdio::piped())
4060 .spawn()
4061 .unwrap();
4062 // the snapshot's operation is written before evaluation: move the
4063 // directory in while the expression runs
4064 while count() == before && child.try_wait().unwrap().is_none() {
4065 std::thread::sleep(std::time::Duration::from_millis(1));
4067 std::fs::rename(&deep, r.dir.join("deep")).unwrap();
4068 let out = child.wait_with_output().unwrap();
4069 assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
4070 assert_eq!(r.read("a"), "p\n");
4071 std::fs::remove_dir_all(r.dir.join("deep")).unwrap();
4072 let files = r.j(&[CONTENT_OF_A]).ok().stdout;
4073 assert_eq!(
4074 files.trim(),
4075 r#"[["" (blob "")] ["P" (blob "p\n")] ["499999500000" (blob "x2\n")]]"#
4076 );
4079/// A repository whose working copy is stale, as jj leaves it when it moves
4080/// the working-copy commit without updating the working directory (a
4081/// command run with `--ignore-working-copy`, or in another workspace): the
4082/// focus is B, while the directory and the working-copy state hold its
4083/// parent A. `a` is 1 in A and 2 in B, `b` is only in A, `z` only in B.
4084fn stale_working_copy() -> Repo {
4085 let r = setup();
4086 r.write("a", "1\n");
4087 r.write("b", "b\n");
4088 r.j(&["describe \"A\""]).ok();
4089 let saved: Vec<(PathBuf, Vec<u8>)> = std::fs::read_dir(r.dir.join(".jj/working_copy"))
4090 .unwrap()
4091 .map(|e| e.unwrap().path())
4092 .map(|p| (p.clone(), std::fs::read(p).unwrap()))
4093 .collect();
4094 r.j(&["new"]).ok();
4095 r.write("a", "2\n");
4096 std::fs::remove_file(r.dir.join("b")).unwrap();
4097 r.write("z", "z\n");
4098 r.j(&["describe \"B\""]).ok();
4099 for (path, bytes) in saved {
4100 std::fs::write(path, bytes).unwrap();
4102 r.write("a", "1\n");
4103 r.write("b", "b\n");
4104 std::fs::remove_file(r.dir.join("z")).unwrap();
4108/// Each commit's message and paths, top-down
4109const PATHS_BY_COMMIT: &str =
4110 "\\r -> show (map (\\c -> [c.message (map (\\e -> e.path) c.files)]) (commits (top r)))";
4112#[test]
4113fn a_run_over_a_stale_working_copy_writes_the_focus() {
4114 // §7.4: with nothing snapshotted, persist's checkout took the directory
4115 // to hold the focus as loaded. Its rescan found otherwise, so it reset
4116 // the state to B, whose tree the focus shares: the checkout wrote
4117 // nothing, and the state was saved claiming B's files over A's. The
4118 // next run recorded A's files into the focus, a silent revert of B.
4119 let r = stale_working_copy();
4120 r.j(&["describe \"B2\""]).ok();
4121 assert_eq!(r.read("a"), "2\n");
4122 assert!(!r.dir.join("b").exists());
4123 assert_eq!(r.read("z"), "z\n");
4124 // the directory now holds the focus, so there is nothing to record
4125 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4126 r.j(&["id"]).ok();
4127 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
4128 let files = r.j(&[CONTENT_OF_A]).ok().stdout;
4129 assert_eq!(files.trim(), r#"[["" (blob "")] ["A" (blob "1\n")] ["B2" (blob "2\n")]]"#);
4130 let paths = r.j(&[PATHS_BY_COMMIT]).ok().stdout;
4131 assert_eq!(paths.trim(), r#"[["" []] ["A" [["a"] ["b"]]] ["B2" [["a"] ["z"]]]]"#);
4134#[test]
4135fn a_failed_checkout_over_a_stale_working_copy_is_carried_on_from() {
4136 // §7.4, §7.5 step 7: a checkout that fails over a stale working copy
4137 // leaves it stale, recording what the directory held when the
4138 // checkout began, so the next run takes what the checkout wrote for
4139 // the focus's own; `j undo` goes back, and checks out B, which the
4140 // directory did not hold before
4141 let r = stale_working_copy();
4142 let log = r.j(&["log"]).ok().stdout;
4143 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4144 // written in path order: `a`, `b` (removed), then the path under `m`
4145 // fails before `z` is reached
4146 let edit = format!(
4147 "mapRoot (\\c -> c {{ files = c.files ++ [{{ path = [{}], content = blob \"deep\" }}] }})",
4148 too_deep("m")
4149 );
4150 let out = r.j(&[&edit]);
4151 assert_eq!(out.code, 1, "{}", out.stderr);
4152 assert!(out.stderr.contains("cannot check out"), "{}", out.stderr);
4153 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
4154 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
4155 assert_eq!(r.read("a"), "2\n");
4156 assert!(!r.dir.join("b").exists());
4157 assert!(!r.dir.join("z").exists());
4158 r.j(&["id"]).ok();
4159 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
4160 let files = r.j(&[CONTENT_OF_A]).ok().stdout;
4161 assert_eq!(files.trim(), r#"[["" (blob "")] ["A" (blob "1\n")] ["B" (blob "2\n")]]"#);
4162 let z = r.j(&["\\r -> show (contentAt [\"z\"] (files r))"]).ok().stdout;
4163 assert_eq!(z.trim(), r#"blob "z\n""#);
4164 r.j(&["undo"]).ok();
4165 assert_eq!(r.j(&["log"]).ok().stdout, log);
4166 assert_eq!(r.read("a"), "2\n");
4167 assert!(!r.dir.join("b").exists());
4168 assert_eq!(r.read("z"), "z\n");
4169 r.j(&["id"]).ok();
4170 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
4173#[test]
4174fn a_stale_working_copy_with_changes_records_them_onto_the_focus() {
4175 // §7.4: the snapshot read the directory against the stale state and
4176 // recorded all of it into the focus, so one new file beside A's files
4177 // undid B; then every run, printing or not, was refused until the
4178 // directory was put back by hand. What the directory holds is a change
4179 // to what the state records, and only that change is recorded, onto
4180 // the focus.
4181 let r = stale_working_copy();
4182 r.write("notes.txt", "note\n");
4183 let paths = r.j(&["\\r -> show (map (\\e -> e.path) (files r))"]).ok().stdout;
4184 assert_eq!(paths.trim(), r#"[["a"] ["notes.txt"] ["z"]]"#);
4185 r.j(&["id"]).ok();
4186 let paths = r.j(&[PATHS_BY_COMMIT]).ok().stdout;
4187 assert_eq!(paths.trim(), r#"[["" []] ["A" [["a"] ["b"]]] ["B" [["a"] ["notes.txt"] ["z"]]]]"#);
4188 let files = r.j(&[CONTENT_OF_A]).ok().stdout;
4189 assert_eq!(files.trim(), r#"[["" (blob "")] ["A" (blob "1\n")] ["B" (blob "2\n")]]"#);
4190 // and the focus is checked out
4191 assert_eq!(r.read("a"), "2\n");
4192 assert!(!r.dir.join("b").exists());
4193 assert_eq!(r.read("z"), "z\n");
4194 assert_eq!(r.read("notes.txt"), "note\n");
4195 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4196 r.j(&["id"]).ok();
4197 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
4200#[test]
4201fn an_edit_over_a_stale_working_copy_to_a_file_the_focus_changed_is_a_conflict() {
4202 // §7.4: B changed `a` since the state was checked out, and so did the
4203 // user, differently: neither change is lost
4204 let r = stale_working_copy();
4205 r.write("a", "mine\n");
4206 r.j(&["id"]).ok();
4207 let conflicted = r.j(&["\\r -> show (conflicted (files r))"]).ok().stdout;
4208 assert_eq!(conflicted.trim(), r#"[["a"]]"#);
4209 let text = r.read("a");
4210 assert!(text.contains("mine\n") && text.contains("2\n"), "{}", text);
4213#[test]
4214fn a_directory_swapped_with_a_file_after_a_failed_checkout_is_kept() {
4215 // §7.4: over a stale working copy, the snapshot merged what the
4216 // directory changed with what the commit changed. Where the user had
4217 // put a directory in place of a file the commit changes, or a file in
4218 // place of a directory holding one, that merge gave one conflict at the
4219 // path between files and a directory, whose directory side j reads as
4220 // empty, and the next checkout wrote jj's description of it there,
4221 // deleting the files the directory held. The snapshot now takes what
4222 // the directory holds at such a path, as over a working copy that is
4223 // not stale; the commit's change there is left in the operation before.
4224 for (before, after) in [("z", "z/new"), ("z/x", "z")] {
4225 let r = setup();
4226 std::fs::create_dir_all(r.dir.join(before).parent().unwrap()).unwrap();
4227 r.write(before, "1\n");
4228 r.j(&["describe \"A\""]).ok();
4229 // the checkout fails at the path under `m`, before it reaches `z`
4230 let path: Vec<String> = before.split('/').map(|c| format!("\"{}\"", c)).collect();
4231 let edit = format!(
4232 "mapRoot (\\c -> c {{ files = [({{ path = [{}], content = blob \"2\\n\" }}) ({{ path = [{}], content = blob \"deep\" }})] }})",
4233 path.join(" "),
4234 too_deep("m")
4235 );
4236 let out = r.j(&[&edit]);
4237 assert_eq!(out.code, 1, "{}: {}", before, out.stderr);
4238 assert!(out.stderr.contains("the operation is recorded"), "{}: {}", before, out.stderr);
4239 assert_eq!(r.read(before), "1\n");
4240 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4241 if before == "z" {
4242 std::fs::remove_file(r.dir.join("z")).unwrap();
4243 } else {
4244 std::fs::remove_dir_all(r.dir.join("z")).unwrap();
4246 std::fs::create_dir_all(r.dir.join(after).parent().unwrap()).unwrap();
4247 r.write(after, "mine\n");
4248 // the path the checkout failed on is left out of the focus
4249 r.j(&["mapRoot (\\c -> c { files = restrict (neg (under ./m)) c.files })"]).ok();
4250 assert_eq!(r.read(after), "mine\n", "{}", before);
4251 let path: Vec<String> = after.split('/').map(|c| format!("\"{}\"", c)).collect();
4252 let paths = r.j(&[FOCUS_PATHS]).ok().stdout;
4253 assert_eq!(paths.trim(), format!("[[{}]]", path.join(" ")), "{}", before);
4254 let conflicted = r.j(&["\\r -> show (conflicted (files r))"]).ok().stdout;
4255 assert_eq!(conflicted.trim(), "[]", "{}", before);
4256 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
4257 r.j(&["id"]).ok();
4258 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1, "{}", before);
4259 assert_eq!(r.read(after), "mine\n", "{}", before);
4263#[test]
4264fn a_conflict_with_a_directory_side_is_kept_where_a_failed_checkout_left_the_path() {
4265 // §7.4, §7.7: R's `z` is a conflict between L's deletion of a file and
4266 // R's directory in its place. A checkout from O, a child with its own
4267 // file `z`, back to R wrote `a` and then failed before it reached `z`.
4268 // The snapshot took what the directory held at every conflict of its
4269 // merge that set a directory against a file, R's own included, although
4270 // the directory held what the last checkout left at `z`: `j undo` was
4271 // refused as holding changes not in @, and the next persisting run
4272 // recorded O's file into R, its conflict and R's `z/x` gone. Only a
4273 // path the directory changed takes what it holds there.
4274 let conflicted = "\\r -> show (conflicted (files r))";
4275 let goto = |m: &str| format!("goto (matching (\\c -> c.message == \"{}\") all)", m);
4276 for undo in [true, false] {
4277 let r = setup();
4278 r.write("z", "base\n");
4279 r.j(&["describe \"P\""]).ok();
4280 r.j(&["new"]).ok();
4281 std::fs::remove_file(r.dir.join("z")).unwrap();
4282 r.j(&["describe \"L\""]).ok();
4283 r.j(&["new . goto parents"]).ok();
4284 std::fs::remove_file(r.dir.join("z")).unwrap();
4285 std::fs::create_dir(r.dir.join("z")).unwrap();
4286 r.write("z/x", "x\n");
4287 r.j(&["describe \"R\""]).ok();
4288 r.j(&["rebase (matching (\\c -> c.message == \"L\") all)"]).ok();
4289 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"z\"]]");
4290 assert!(r.read("z").starts_with("Conflict:\n"), "{}", r.read("z"));
4291 r.j(&["new"]).ok();
4292 r.write("z", "other\n");
4293 r.j(&["describe \"O\""]).ok();
4294 let log = r.j(&["log"]).ok().stdout;
4295 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4296 // written in path order: `a`, then the path under `m` fails, before
4297 // `z` is reached
4298 let edit = format!(
4299 "mapRoot (\\c -> c {{ files = c.files ++ [({{ path = [\"a\"], content = blob \"a\\n\" }}) ({{ path = [{}], content = blob \"deep\" }})] }}) . {}",
4300 too_deep("m"),
4301 goto("R")
4302 );
4303 let out = r.j(&[&edit]);
4304 assert_eq!(out.code, 1, "{}", out.stderr);
4305 assert!(out.stderr.contains("the operation is recorded"), "{}", out.stderr);
4306 assert_eq!(r.read("a"), "a\n");
4307 assert_eq!(r.read("z"), "other\n");
4308 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"z\"]]");
4309 if undo {
4310 r.j(&["undo"]).ok();
4311 assert_eq!(r.j(&["log"]).ok().stdout, log);
4312 assert!(!r.dir.join("a").exists());
4313 assert_eq!(r.read("z"), "other\n");
4314 r.j(&["id"]).ok();
4315 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
4316 continue;
4318 // the path the checkout failed on is left out of the focus
4319 r.j(&["mapRoot (\\c -> c { files = restrict (neg (under ./m)) c.files })"]).ok();
4320 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"z\"]]");
4321 assert_eq!(r.j(&[FOCUS_PATHS]).ok().stdout.trim(), "[[\"a\"] [\"z\"]]");
4322 let description = r.read("z");
4323 assert!(description.starts_with("Conflict:\n") && description.contains("tree"), "{}", description);
4324 r.j(&["id"]).ok();
4325 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
4326 // the child still holds its own file
4327 r.j(&[&goto("O")]).ok();
4328 assert_eq!(r.read("z"), "other\n");
4332#[test]
4333fn a_stale_working_copy_without_changes_is_undone_and_redone() {
4334 // §7.4, §7.7: `undo` and `redo` refuse only a directory holding changes
4335 // no commit has, and a directory holding what the stale state records
4336 // holds none
4337 let r = stale_working_copy();
4338 let log = r.j(&["log"]).ok().stdout;
4339 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4340 r.j(&["undo"]).ok();
4341 r.j(&["redo"]).ok();
4342 assert_eq!(r.j(&["log"]).ok().stdout, log);
4343 assert_eq!(r.read("a"), "2\n");
4344 assert!(!r.dir.join("b").exists());
4345 assert_eq!(r.read("z"), "z\n");
4346 r.j(&["id"]).ok();
4347 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 2);
4350#[test]
4351fn a_stale_working_copy_holding_the_focus_records_nothing() {
4352 // §7.4: a directory that holds the focus's files over a stale state
4353 // has nothing to record and is not refused; the state is brought up to
4354 // date, so a change made from there is recorded as usual
4355 let r = stale_working_copy();
4356 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4357 r.write("a", "2\n");
4358 std::fs::remove_file(r.dir.join("b")).unwrap();
4359 r.write("z", "z\n");
4360 r.j(&["id"]).ok();
4361 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
4362 r.write("notes.txt", "note\n");
4363 r.j(&["id"]).ok();
4364 let paths = r.j(&[PATHS_BY_COMMIT]).ok().stdout;
4365 assert_eq!(paths.trim(), r#"[["" []] ["A" [["a"] ["b"]]] ["B" [["a"] ["notes.txt"] ["z"]]]]"#);
4368#[test]
4369fn a_lost_working_copy_state_records_the_directory_as_it_is() {
4370 // §7.4: jj makes a new working-copy state, recording no file, where it
4371 // finds none, at the operation the working copy names: the head's. It
4372 // was taken for a stale one, as it records other files than the
4373 // commit, and the snapshot merged the directory onto the commit with
4374 // no file as the base: each file edited since the last checkout became
4375 // a conflict of the edit with the commit's content, and each one
4376 // deleted came back. A state saved at the head operation is current,
4377 // and the directory is recorded as it is, as jj records it.
4378 let r = setup();
4379 r.write("f", "1\n");
4380 r.write("h", "gone\n");
4381 r.j(&["describe \"A\""]).ok();
4382 r.j(&["new"]).ok();
4383 r.write("f", "2\n");
4384 std::fs::remove_file(r.dir.join("h")).unwrap();
4385 std::fs::remove_file(r.dir.join(".jj/working_copy/tree_state")).unwrap();
4386 let conflicted = "\\r -> show (conflicted (files r))";
4387 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
4388 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4389 r.j(&["id"]).ok();
4390 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
4391 assert_eq!(r.read("f"), "2\n");
4392 assert!(!r.dir.join("h").exists());
4393 let paths = r.j(&[PATHS_BY_COMMIT]).ok().stdout;
4394 assert_eq!(paths.trim(), r#"[["" []] ["A" [["f"] ["h"]]] ["" [["f"]]]]"#);
4395 let f = r.j(&["\\r -> show (contentAt [\"f\"] (files r))"]).ok().stdout;
4396 assert_eq!(f.trim(), r#"blob "2\n""#);
4397 // and the state is saved again, with nothing left to record
4398 r.j(&["id"]).ok();
4399 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops + 1);
4402#[test]
4403fn a_conflict_left_as_written_stays_when_the_working_copy_state_is_lost() {
4404 // §7.4: a working-copy state made again where it was lost records no
4405 // conflict, so the scan reads each file the checkout wrote for one as
4406 // the text of its markers. A file holding exactly what a checkout
4407 // writes for one of the commit's conflicts is read back as that
4408 // conflict: a directory nobody touched has nothing to record, and
4409 // `undo` and `redo` do not refuse it.
4410 let r = setup();
4411 r.write("a.txt", "base\n");
4412 r.write("keep.txt", "k\n");
4413 r.j(&["describe \"base\""]).ok();
4414 r.j(&["new"]).ok();
4415 r.write("a.txt", "<<<<<<< left\n");
4416 r.j(&["describe \"left\""]).ok();
4417 r.j(&["new . goto parents"]).ok();
4418 r.write("a.txt", "right\n");
4419 r.j(&["describe \"right\""]).ok();
4420 r.j(&["rebase siblings"]).ok();
4421 let conflicted = "\\r -> show (conflicted (files r))";
4422 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a.txt\"]]");
4423 let written = r.read("a.txt");
4424 let log = r.j(&["log"]).ok().stdout;
4425 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4426 std::fs::remove_file(r.dir.join(".jj/working_copy/tree_state")).unwrap();
4427 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a.txt\"]]");
4428 r.j(&["id"]).ok();
4429 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
4430 r.j(&["undo"]).ok();
4431 r.j(&["redo"]).ok();
4432 assert_eq!(r.j(&["log"]).ok().stdout, log);
4433 assert_eq!(r.read("a.txt"), written);
4434 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a.txt\"]]");
4437#[test]
4438fn replay_carries_conflicts_through() {
4439 // replay unwrapped each input tree's ids as resolved, so every rebase,
4440 // squash or abandon that replayed a snapshot holding a conflict panicked
4441 // (exit 101, uncatchable by `or`) instead of keeping the conflict (§7.3,
4442 // §8); the in-memory backend handled it, so only a real repo showed it
4443 let r = setup();
4444 r.write("a.txt", "base\n");
4445 r.j(&["describe \"base\""]).ok();
4446 r.j(&["new"]).ok();
4447 r.write("a.txt", "left\n");
4448 r.j(&["describe \"left\""]).ok();
4449 r.j(&["new . goto parents"]).ok();
4450 r.write("a.txt", "right\n");
4451 r.j(&["describe \"right\""]).ok();
4452 // all three inputs are still resolved here; the result is not
4453 r.j(&["rebase siblings"]).ok();
4454 let conflicted = "\\r -> show (conflicted (files r))";
4455 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a.txt\"]]");
4456 // the §8 laws, with a conflicted snapshot in each position
4457 for law in [
4458 "\\r -> replay (files r) ({ from = files r, to = files r }) == files r",
4459 "\\r -> replay (files r) ({ from = [], to = [] }) == files r",
4460 "\\r -> replay [] ({ from = [], to = files r }) == files r",
4461 "\\r -> replay (files r) ({ from = files r, to = files (up r) }) == files (up r)",
4462 ] {
4463 assert_eq!(r.j(&[law]).ok().stdout.trim(), "true", "{}", law);
4465 // rebase parents r = r: nothing to record. Count the operations: the
4466 // listing shows each one's age, which can tick over between the runs
4467 let ops = r.j(&["ops"]).ok().stdout.lines().count();
4468 r.j(&["rebase parents"]).ok();
4469 assert_eq!(r.j(&["ops"]).ok().stdout.lines().count(), ops);
4470 // squashing into the conflicted commit keeps its conflict
4471 r.j(&["new"]).ok();
4472 r.write("b.txt", "bee\n");
4473 r.j(&["squash"]).ok();
4474 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a.txt\"]]");
4475 assert_eq!(r.read("b.txt"), "bee\n");
4476 assert!(r.read("a.txt").contains("<<<<<<<"), "{}", r.read("a.txt"));
4477 // abandoning the side the conflict came from replays the child onto
4478 // base, where jj's merge resolves it to the child's own side
4479 let named = |m: &str| format!("(matching (\\c -> c.message == \"{}\") all)", m);
4480 r.j(&[&format!("abandon . goto {}", named("left"))]).ok();
4481 assert_eq!(r.j(&["\\r -> show (conflicts r)"]).ok().stdout.trim(), "[]");
4482 let files = format!("\\r -> show (map (\\i -> (commitAt i r).files) ({} r))", named("right"));
4483 let files = r.j(&[&files]).ok().stdout;
4484 assert!(files.contains("blob \"right\\n\"") && files.contains("blob \"bee\\n\""), "{}", files);
4487#[test]
4488fn a_replayed_conflict_keeps_no_sides_that_cancel() {
4489 // replay kept every term of the merge of a conflicted input, sides that
4490 // cancel included: rebasing a stack in which each commit conflicts with
4491 // the new base gave the second its parent's conflict with a pair of
4492 // sides added that cancel, so it listed its parent's path as changed,
4493 // its inherited conflict was not its parent's, and each commit up the
4494 // stack stored two sides more than the one below it (§7.3)
4495 let named = |m: &str| format!("(matching (\\c -> c.message == \"{}\") all)", m);
4496 let r = setup();
4497 r.write("a", "a1\na2\na3\n");
4498 r.write("b", "b1\nb2\nb3\n");
4499 r.j(&["describe \"trunk\""]).ok();
4500 r.j(&["new"]).ok();
4501 r.write("a", "a1\nA2 one\na3\n");
4502 r.j(&["describe \"one\""]).ok();
4503 r.j(&["new"]).ok();
4504 r.write("b", "b1\nB2 two\nb3\n");
4505 r.j(&["describe \"two\""]).ok();
4506 r.j(&[&format!("new . goto {}", named("trunk"))]).ok();
4507 r.write("a", "a1\nA2 trunk\na3\n");
4508 r.write("b", "b1\nB2 trunk\nb3\n");
4509 r.j(&["describe \"trunk 2\""]).ok();
4510 r.j(&[&format!("goto {}", named("one"))]).ok();
4511 r.j(&[&format!("rebase {}", named("trunk 2"))]).ok();
4512 r.j(&[&format!("goto {}", named("two"))]).ok();
4513 let conflicted = "\\r -> show (conflicted (files r))";
4514 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"] [\"b\"]]");
4515 // two changed `b` only; its `a` is one's conflict, of three terms
4516 assert_eq!(r.j(&["\\r -> show (changed r)"]).ok().stdout.trim(), "[[\"b\"]]");
4517 let inherited = "\\r -> contentAt [\"a\"] (files r) == contentAt [\"a\"] (files (up r))";
4518 assert_eq!(r.j(&[inherited]).ok().stdout.trim(), "true");
4519 let text = r.j(&["\\r -> text (contentAt [\"a\"] (files r))"]).ok().stdout;
4520 assert_eq!(text.matches("%%%%%%%").count(), 1, "{}", text);
4521 // and each is stored as a merge of three trees
4522 let repo = jj_repo(&r);
4523 for m in ["one", "two"] {
4524 let commit = jj_commit(&r, &repo, &format!("head ({} r)", named(m)));
4525 assert_eq!(commit.tree_ids().iter().count(), 3, "{}", m);
4529#[test]
4530fn a_conflict_jj_merged_reads_without_the_sides_that_cancel_and_keeps_its_tree() {
4531 // jj's rebase keeps each term of a merge it cannot resolve, so in a
4532 // stack jj rebased, where each commit conflicts with the new base, the
4533 // second commit stores its parent's conflict with a pair of sides that
4534 // cancel added. `j` read it so, as a change the commit did not make.
4535 // Read without the pair, the conflict is the parent's; and the tree is
4536 // written as jj stored it wherever the files are still its, so `new`
4537 // leaves an empty child, as jj's `new` does, not one of another tree
4538 // for the same files (§7.3, §7.5)
4539 let named = |m: &str| format!("(matching (\\c -> c.message == \"{}\") all)", m);
4540 let r = setup();
4541 r.write("a", "a1\na2\na3\n");
4542 r.write("b", "b1\nb2\nb3\n");
4543 r.j(&["describe \"trunk\""]).ok();
4544 r.j(&["new"]).ok();
4545 r.write("a", "a1\nA2 one\na3\n");
4546 r.j(&["describe \"one\""]).ok();
4547 r.j(&["new"]).ok();
4548 r.write("b", "b1\nB2 two\nb3\n");
4549 r.j(&["describe \"two\""]).ok();
4550 r.j(&[&format!("new . goto {}", named("trunk"))]).ok();
4551 r.write("a", "a1\nA2 trunk\na3\n");
4552 r.write("b", "b1\nB2 trunk\nb3\n");
4553 r.j(&["describe \"trunk 2\""]).ok();
4554 // one onto trunk 2 as jj rebases it, and two with it
4555 jj_rebase(&r, &format!("head ({} r)", named("one")), "r.root.id");
4556 let repo = jj_repo(&r);
4557 let tree = |m: &str| jj_commit(&r, &repo, &format!("head ({} r)", named(m))).tree_ids().clone();
4558 let stored = tree("two");
4559 assert_eq!(stored.iter().count(), 5);
4560 r.j(&[&format!("goto {}", named("two"))]).ok();
4561 let conflicted = "\\r -> show (conflicted (files r))";
4562 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"a\"] [\"b\"]]");
4563 assert_eq!(r.j(&["\\r -> show (changed r)"]).ok().stdout.trim(), "[[\"b\"]]");
4564 let inherited = "\\r -> contentAt [\"a\"] (files r) == contentAt [\"a\"] (files (up r))";
4565 assert_eq!(r.j(&[inherited]).ok().stdout.trim(), "true");
4566 // describing it, and its parent, keeps the tree jj stored
4567 r.j(&["describe \"two, described\""]).ok();
4568 r.j(&["at parents (describe \"one, described\")"]).ok();
4569 let repo = jj_repo(&r);
4570 let tree = |id: &str| jj_commit(&r, &repo, id).tree_ids().clone();
4571 assert_eq!(tree("r.root.id"), stored);
4572 // and a new child holds it too
4573 r.j(&["new"]).ok();
4574 let repo = jj_repo(&r);
4575 let tree = |id: &str| jj_commit(&r, &repo, id).tree_ids().clone();
4576 assert_eq!(tree("r.root.id"), stored);
4577 assert_eq!(tree("(up r).root.id"), stored);
4578 assert_eq!(r.j(&["\\r -> show (changed r)"]).ok().stdout.trim(), "[]");
4581#[test]
4582fn a_commit_left_with_its_parents_files_is_written_with_the_parents_tree() {
4583 // `squash`, `rebase` and `abandon` build a commit's files anew, and a
4584 // tree built from them holds each conflict less the sides that cancel.
4585 // Where the parent is a tree jj merged, which keeps them, the child
4586 // left with its parent's files got another tree for them: jj took it
4587 // for a change at every conflicted path, `tree` did not mark it empty,
4588 // and the checkout rewrote the conflict files without jj's labels
4589 // (§7.5). Here the parent is made only with `j`: the snapshot rebases
4590 // the stack onto its edited base as jj does
4591 let named = |m: &str| format!("(matching (\\c -> c.message == \"{}\") all)", m);
4592 let r = setup();
4593 r.write("a", "a1\na2\na3\n");
4594 r.write("b", "b1\nb2\nb3\n");
4595 r.j(&["describe \"B\""]).ok();
4596 r.j(&["new"]).ok();
4597 r.write("a", "a1\nA2 one\na3\n");
4598 r.j(&["describe \"one\""]).ok();
4599 r.j(&["new"]).ok();
4600 r.write("b", "b1\nB2 two\nb3\n");
4601 r.j(&["describe \"two\""]).ok();
4602 r.j(&[&format!("goto {}", named("B"))]).ok();
4603 r.write("a", "a1\nA2 B\na3\n");
4604 r.write("b", "b1\nB2 B\nb3\n");
4605 r.j(&["id"]).ok();
4606 let two = format!("head ({} r)", named("two"));
4607 let stored = jj_commit(&r, &jj_repo(&r), &two).tree_ids().clone();
4608 assert_eq!(stored.iter().count(), 5);
4609 // the commit `id` names holds two's tree, and jj takes it for empty
4610 let empty_on_two = |id: &str, what: &str| {
4611 let repo = jj_repo(&r);
4612 let c = jj_commit(&r, &repo, id);
4613 assert_eq!(jj_commit(&r, &repo, &two).tree_ids(), &stored, "{}", what);
4614 assert_eq!(c.tree_ids(), &stored, "{}", what);
4615 assert!(pollster::block_on(c.is_empty(repo.as_ref())).unwrap(), "{}", what);
4616 assert_eq!(r.j(&[&format!("\\r -> show (changed (by ({}) r))", id)]).ok().stdout.trim(), "[]");
4617 };
4618 // squash: two is rewritten with its own files, and the new focus on it
4619 // holds them
4620 r.j(&[&format!("goto {}", named("two"))]).ok();
4621 r.j(&["new"]).ok();
4622 let labelled = r.read("a");
4623 assert!(labelled.contains("\"two\" (rebased revision)"), "{}", labelled);
4624 r.j(&["squash"]).ok();
4625 empty_on_two("r.root.id", "squash");
4626 assert_eq!(r.read("a"), labelled);
4627 // squash into one, whose tree of three terms the list builds again with
4628 // the same ones but no labels: the new focus keeps jj's labels too
4629 r.j(&[&format!("new . goto {}", named("one"))]).ok();
4630 let labelled = r.read("a");
4631 assert!(labelled.contains("\"one\" (rebased revision)"), "{}", labelled);
4632 r.j(&["squash"]).ok();
4633 let repo = jj_repo(&r);
4634 let one = jj_commit(&r, &repo, "(up r).root.id").tree();
4635 assert_eq!(one.tree_ids().iter().count(), 3);
4636 assert_eq!(jj_commit(&r, &repo, "r.root.id").tree().tree_ids_and_labels(), one.tree_ids_and_labels());
4637 assert_eq!(r.read("a"), labelled);
4638 // rebase: an empty commit replayed onto two
4639 r.j(&[&format!("new . goto {}", named("B"))]).ok();
4640 r.j(&["describe \"empty\""]).ok();
4641 r.j(&[&format!("rebase {}", named("two"))]).ok();
4642 empty_on_two("r.root.id", "rebase");
4643 // abandon: Y, empty, replayed from X onto two
4644 r.j(&[&format!("new . goto {}", named("two"))]).ok();
4645 r.write("c", "c\n");
4646 r.j(&["describe \"X\""]).ok();
4647 r.j(&["new"]).ok();
4648 r.j(&["describe \"Y\""]).ok();
4649 r.j(&[&format!("goto {}", named("X"))]).ok();
4650 r.j(&["abandon"]).ok();
4651 empty_on_two(&format!("head ({} r)", named("Y")), "abandon");
4652 // a squash that changes the parent's files gives it a tree of its own,
4653 // which the new focus holds
4654 r.j(&[&format!("new . goto {}", named("two"))]).ok();
4655 r.write("d", "d\n");
4656 r.j(&["squash"]).ok();
4657 let repo = jj_repo(&r);
4658 let c = jj_commit(&r, &repo, "r.root.id");
4659 let parent = jj_commit(&r, &repo, "(up r).root.id").tree_ids().clone();
4660 assert_ne!(parent, stored);
4661 assert_eq!(c.tree_ids(), &parent);
4662 assert!(pollster::block_on(c.is_empty(repo.as_ref())).unwrap());
4663 let parent_files = "\\r -> show (map (.path) (files (up r)))";
4664 assert_eq!(r.j(&[parent_files]).ok().stdout.trim(), "[[\"a\"] [\"b\"] [\"d\"]]");
4667#[test]
4668fn a_commit_moved_onto_its_own_files_is_written_with_the_parents_tree() {
4669 // A commit can come to hold its parent's files while its own read as
4670 // they did: an empty commit rebased onto another tree for the same
4671 // files, an empty child of a commit squashed into its parent, a change
4672 // the working copy reverts. Rewritten with its own stored tree, or
4673 // snapshotted into the tree the snapshot built, it held another tree
4674 // for its parent's files: jj took it for a change at each conflicted
4675 // path, and `tree` did not mark it empty, though `changed` listed
4676 // nothing and the dry run of the same edit marked it (§7.4, §7.5).
4677 // The stack is the one above, with two empty children of two
4678 let named = |m: &str| format!("(matching (\\c -> c.message == \"{}\") all)", m);
4679 let head = |m: &str| format!("head ({} r)", named(m));
4680 let r = setup();
4681 r.write("a", "a1\na2\na3\n");
4682 r.write("b", "b1\nb2\nb3\n");
4683 r.j(&["describe \"B\""]).ok();
4684 r.j(&["new"]).ok();
4685 r.write("a", "a1\nA2 one\na3\n");
4686 r.j(&["describe \"one\""]).ok();
4687 r.j(&["new"]).ok();
4688 r.write("b", "b1\nB2 two\nb3\n");
4689 r.j(&["describe \"two\""]).ok();
4690 r.j(&["describe \"E\" . new"]).ok();
4691 r.j(&[&format!("describe \"G\" . new . goto {}", named("two"))]).ok();
4692 r.j(&[&format!("goto {}", named("B"))]).ok();
4693 r.write("a", "a1\nA2 B\na3\n");
4694 r.write("b", "b1\nB2 B\nb3\n");
4695 r.j(&["id"]).ok();
4696 let repo = jj_repo(&r);
4697 let stored = jj_commit(&r, &repo, &head("two")).tree_ids().clone();
4698 assert_eq!(stored.iter().count(), 5);
4699 for m in ["E", "G"] {
4700 assert_eq!(jj_commit(&r, &repo, &head(m)).tree_ids(), &stored, "{}", m);
4702 // the commit `m` names holds its parent's tree, and jj takes it for empty
4703 let empty = |m: &str, what: &str| {
4704 use jj_lib::repo::Repo as _;
4705 let repo = jj_repo(&r);
4706 let c = jj_commit(&r, &repo, &head(m));
4707 let parent = repo.store().get_commit(&c.parent_ids()[0]).unwrap();
4708 assert_eq!(c.tree_ids(), parent.tree_ids(), "{}: {}", what, m);
4709 assert!(pollster::block_on(c.is_empty(repo.as_ref())).unwrap(), "{}: {}", what, m);
4710 let changed = format!("\\r -> show (changed (by ({}) r))", head(m));
4711 assert_eq!(r.j(&[&changed]).ok().stdout.trim(), "[]", "{}: {}", what, m);
4712 };
4713 // whether the coloured tree `expr` renders draws the message `m` as an
4714 // empty commit's, dim and italic
4715 let marked_empty = |expr: &str, m: &str| {
4716 let out = Command::new(j_bin())
4717 .arg(expr)
4718 .current_dir(&r.dir)
4719 .env("XDG_CONFIG_HOME", &r.cfg)
4720 .env_remove("NO_COLOR")
4721 .output()
4722 .unwrap();
4723 assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
4724 String::from_utf8_lossy(&out.stdout).contains(&format!("\x1b[2;3m{}\x1b[0m", m))
4725 };
4726 let coloured = "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"always\", lanes = 4, author = false, date = false, files = false })";
4727 // rebase: Y, two's change picked onto one, holds two's files in a tree
4728 // `j` builds, of three terms; G, empty on two, is moved onto it
4729 let one = named("one");
4730 r.j(&[&format!("describe \"Y\" . child (\\c -> c.message == \"\") . goto {} . pick {} . goto {}", one, one, named("two"))])
4731 .ok();
4732 let y = jj_commit(&r, &jj_repo(&r), &head("Y")).tree_ids().clone();
4733 assert_eq!(y.iter().count(), 3);
4734 r.j(&[&format!("rebase {} . goto {}", named("Y"), named("G"))]).ok();
4735 empty("G", "rebase");
4736 // a change the working copy reverts: R and S, each holding `z`, are
4737 // rebased onto two, which gives each a tree `j` builds; with `z`
4738 // removed, the snapshot alone gives R two's tree, and the snapshot a
4739 // rewrite of S's message folds in gives S two's
4740 for (m, record) in [("R", "id"), ("S", "describe \"S, reverted\"")] {
4741 r.j(&[&format!("new . goto {}", one)]).ok();
4742 r.write("z", "z\n");
4743 r.j(&[&format!("describe \"{}\"", m)]).ok();
4744 r.j(&[&format!("rebase {}", named("two"))]).ok();
4745 std::fs::remove_file(r.dir.join("z")).unwrap();
4746 r.j(&[record]).ok();
4748 empty("R", "snapshot");
4749 empty("S, reverted", "describe");
4750 // squash: two is folded into one, which it rewrites with a tree `j`
4751 // builds; E, R and S, empty on two, are moved onto it. The tree marks
4752 // E empty after the squash as its dry run does
4753 let squash = format!("describe \"F\" . squash . goto {}", named("two"));
4754 assert!(marked_empty(&format!("{} . validate . {}", coloured, squash), "E"));
4755 r.j(&[&squash]).ok();
4756 for m in ["E", "R", "S, reverted", "F"] {
4757 empty(m, "squash");
4759 assert!(marked_empty(coloured, "E"));
4760 let repo = jj_repo(&r);
4761 assert_eq!(jj_commit(&r, &repo, &head("one")).tree_ids().iter().count(), 3);
4764#[test]
4765fn a_conflict_whose_sides_left_are_directories_keeps_its_files() {
4766 // a conflict at `d` whose file sides cancel, leaving directories only.
4767 // jj keeps it as one conflict at the path; without its file sides it
4768 // would be one between the directories, which jj merges entry by
4769 // entry. So `j` reads it with every side, and a new child keeps it
4770 // (§7.3)
4771 let r = setup();
4772 let dir = |x: &str| {
4773 let _ = std::fs::remove_file(r.dir.join("d"));
4774 std::fs::create_dir_all(r.dir.join("d")).unwrap();
4775 r.write("d/x", x);
4776 };
4777 let file = || {
4778 let _ = std::fs::remove_dir_all(r.dir.join("d"));
4779 r.write("d", "f\n");
4780 };
4781 let sides: [(&str, &dyn Fn()); 5] =
4782 [("A", &|| dir("1\n")), ("B", &file), ("C", &file), ("D", &|| dir("0\n")), ("E", &|| dir("2\n"))];
4783 for (m, write) in sides {
4784 write();
4785 r.write("e", m);
4786 r.j(&[&format!("describe \"{}\"", m)]).ok();
4787 r.j(&["new"]).ok();
4789 // A - B + C - D + E, as a commit X on E
4790 use jj_lib::repo::Repo as _;
4791 let named = |m: &str| format!("head ((matching (\\c -> c.message == \"{}\") all) r)", m);
4792 let repo = jj_repo(&r);
4793 let tree = |m: &str| jj_commit(&r, &repo, &named(m)).tree_ids().as_resolved().unwrap().clone();
4794 let trees = jj_lib::merge::Merge::from_vec(["A", "B", "C", "D", "E"].map(tree).to_vec());
4795 let tree = jj_lib::merged_tree::MergedTree::new(
4796 repo.store().clone(),
4797 trees,
4798 jj_lib::conflict_labels::ConflictLabels::unlabeled(),
4799 );
4800 let mut tx = repo.start_transaction();
4801 let parent = jj_commit(&r, &repo, &named("E")).id().clone();
4802 pollster::block_on(tx.repo_mut().new_commit(vec![parent], tree.clone()).set_description("X").write()).unwrap();
4803 pollster::block_on(tx.commit("X")).unwrap();
4804 r.j(&["goto (matching (\\c -> c.message == \"X\") all)"]).ok();
4805 let conflicted = "\\r -> show (conflicted (files r))";
4806 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"d\"] [\"e\"]]");
4807 assert_eq!(r.j(&["\\r -> show (map (.path) (files r))"]).ok().stdout.trim(), "[[\"d\"] [\"e\"]]");
4808 let text = r.j(&["\\r -> text (contentAt [\"d\"] (files r))"]).ok().stdout;
4809 assert_eq!(text.matches("%%%%%%%").count(), 2, "{}", text);
4810 r.j(&["new"]).ok();
4811 let repo = jj_repo(&r);
4812 assert_eq!(jj_commit(&r, &repo, "r.root.id").tree_ids(), tree.tree_ids());
4815#[cfg(unix)]
4816#[test]
4817fn conflict_sides_keep_absence_and_file_type() {
4818 // a conflict blob kept each side as bare bytes: a side the path was
4819 // deleted on came back as an empty file, and every side lost its
4820 // executable bit or symlink type (§7.3). The conflicted checkout of an
4821 // executable file was not executable, and rebasing a conflicted commit
4822 // back where it came from, whose sides must cancel, left an empty file
4823 // for its deletion, dropped +x, and kept a symlink conflict.
4824 use std::os::unix::fs::{symlink, PermissionsExt};
4825 let r = setup();
4826 let exec = |p: &str| {
4827 let mode = std::fs::metadata(r.dir.join(p)).unwrap().permissions().mode();
4828 mode & 0o111 != 0
4829 };
4830 let relink = |target: &str| {
4831 let _ = std::fs::remove_file(r.dir.join("l"));
4832 symlink(target, r.dir.join("l")).unwrap();
4833 };
4834 r.write("f.txt", "base\n");
4835 r.write("g.sh", "x\n");
4836 std::fs::set_permissions(r.dir.join("g.sh"), std::fs::Permissions::from_mode(0o755)).unwrap();
4837 relink("a");
4838 r.j(&["describe \"A\""]).ok();
4839 r.j(&["new"]).ok();
4840 r.write("f.txt", "mod\n");
4841 r.write("g.sh", "y\n");
4842 relink("b");
4843 r.j(&["describe \"B\""]).ok();
4844 r.j(&["new"]).ok();
4845 std::fs::remove_file(r.dir.join("f.txt")).unwrap();
4846 r.write("g.sh", "z\n");
4847 relink("c");
4848 r.j(&["describe \"C\""]).ok();
4849 assert!(exec("g.sh"));
4850 // C onto A: a modify/delete conflict, and executable and symlink sides
4851 r.j(&["rebase (parents . prev)"]).ok();
4852 let conflicted = "\\r -> show (conflicted (files r))";
4853 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[[\"f.txt\"] [\"g.sh\"] [\"l\"]]");
4854 // every side of g.sh is executable, so its materialised conflict is
4855 assert!(exec("g.sh"), "the conflicted checkout of g.sh lost +x");
4856 // C back onto B: B's sides cancel, leaving C's own change
4857 r.j(&["rebase siblings"]).ok();
4858 assert_eq!(r.j(&[conflicted]).ok().stdout.trim(), "[]");
4859 let paths = r.j(&["\\r -> show (map (.path) (files r))"]).ok().stdout;
4860 assert_eq!(paths.trim(), "[[\"g.sh\"] [\"l\"]]");
4861 assert_eq!(r.read("g.sh"), "z\n");
4862 assert!(exec("g.sh"), "g.sh lost +x");
4863 assert_eq!(std::fs::read_link(r.dir.join("l")).unwrap(), PathBuf::from("c"));