1//! First-run configuration must be complete, safe to repeat, and independent
2//! of repository state.
5use std::process::{Command, Output, Stdio};
8 dir: tempfile::TempDir,
9 config: tempfile::TempDir,
14 Self { dir: tempfile::tempdir().unwrap(), config: tempfile::tempdir().unwrap() }
17 fn path(&self) -> PathBuf { self.config.path().join("j/config.j") }
19 fn command(&self, args: &[&str]) -> Command {
20 let mut command = Command::new(env!("CARGO_BIN_EXE_j"));
21 command.args(args).current_dir(self.dir.path()).env("XDG_CONFIG_HOME", self.config.path())
22 .env("NO_COLOR", "1").stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
26 fn run(&self, args: &[&str]) -> Output { self.command(args).output().unwrap() }
29fn success(output: Output) -> String {
30 assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
31 assert!(output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr));
32 String::from_utf8(output.stdout).unwrap()
36fn setup_with_identity_creates_a_complete_usable_config_without_a_repo() {
37 let cli = SetupCli::new();
38 let output = success(cli.run(&["setup", "Ada Lovelace", "ada@example.org"]));
39 assert!(output.contains("Configuration ready"));
40 assert!(output.contains("j init"));
41 assert!(!cli.dir.path().join(".jj").exists());
42 let source = std::fs::read_to_string(cli.path()).unwrap();
43 let cfg = j::config::load_config(&source).unwrap();
44 assert!(cfg.defs.iter().any(|(name, _)| name == "new"));
45 assert!(cfg.defs.iter().any(|(name, _)| name == "save"));
46 assert_eq!(j::config::eval_user_only(&mut j::eval::Interp::dummy(), &cfg).unwrap(), ("Ada Lovelace".into(), "ada@example.org".into()));
47 success(cli.run(&["init"]));
48 success(cli.run(&["status"]));
51 use std::os::unix::fs::PermissionsExt;
52 assert_eq!(std::fs::metadata(cli.path()).unwrap().permissions().mode() & 0o777, 0o600);
57fn bare_setup_creates_the_entire_default_and_explains_the_identity_edit() {
58 let cli = SetupCli::new();
59 let output = success(cli.run(&["setup"]));
60 assert!(output.contains("user = { name ="));
61 assert!(output.contains("Keep the rest of the file"));
62 assert_eq!(std::fs::read_to_string(cli.path()).unwrap(), include_str!("../config.j"));
63 assert_eq!(std::fs::read_dir(cli.dir.path()).unwrap().count(), 0);
64 let output = cli.run(&["init"]);
65 assert_eq!(output.status.code(), Some(3));
66 assert!(String::from_utf8_lossy(&output.stderr).contains("identity is not configured"));
70fn identity_strings_round_trip_quotes_backslashes_unicode_and_controls() {
71 let cli = SetupCli::new();
72 let name = "Ада \"Lovelace\" \\ Test\nSecond line\t";
73 let email = "ada+\"quote\"\\suffix@example.org";
74 success(cli.run(&["setup", name, email]));
75 let cfg = j::config::load_config(&std::fs::read_to_string(cli.path()).unwrap()).unwrap();
76 assert_eq!(j::config::eval_user_only(&mut j::eval::Interp::dummy(), &cfg).unwrap(), (name.into(), email.into()));
80fn existing_custom_or_broken_configs_are_never_replaced() {
81 let cli = SetupCli::new();
82 success(cli.run(&["setup", "Existing Person", "existing@example.org"]));
83 let source = format!("{}\nmyCommand = \\repo -> repo\n", std::fs::read_to_string(cli.path()).unwrap());
84 std::fs::write(cli.path(), &source).unwrap();
85 let output = success(cli.run(&["setup"]));
86 assert!(output.contains("already exists") && output.contains("ready"));
87 assert!(output.contains("Existing Person"));
88 let output = cli.run(&["setup", "Replacement", "replacement@example.org"]);
89 assert_eq!(output.status.code(), Some(2));
90 assert!(String::from_utf8_lossy(&output.stderr).contains("was not changed"));
91 assert_eq!(std::fs::read_to_string(cli.path()).unwrap(), source);
93 std::fs::write(cli.path(), "broken =\n").unwrap();
94 let output = success(cli.run(&["setup"]));
95 assert!(output.contains("j help config"));
96 assert!(output.contains("line 1:") && output.contains("unexpected"), "{}", output);
97 assert_eq!(std::fs::read_to_string(cli.path()).unwrap(), "broken =\n");
101fn usage_and_invalid_identities_do_not_create_files() {
103 vec!["setup", "one"],
104 vec!["setup", "one", "two", "three"],
105 vec!["setup", "", "ada@example.org"],
106 vec!["setup", "Ada", " "],
107 vec!["setup", "Your Name", "you@example.com"],
109 let cli = SetupCli::new();
110 let output = cli.run(&args);
111 assert!(!output.status.success(), "{:?}", args);
112 assert!(!cli.path().exists());
113 assert_eq!(std::fs::read_dir(cli.config.path()).unwrap().count(), 0);
118fn concurrent_setup_publishes_one_complete_config_without_overwriting() {
119 let cli = SetupCli::new();
120 let first = cli.command(&["setup", "First Person", "first@example.org"]).spawn().unwrap();
121 let second = cli.command(&["setup", "Second Person", "second@example.org"]).spawn().unwrap();
122 let first = first.wait_with_output().unwrap();
123 let second = second.wait_with_output().unwrap();
124 assert_ne!(first.status.success(), second.status.success());
125 let cfg = j::config::load_config(&std::fs::read_to_string(cli.path()).unwrap()).unwrap();
126 let identity = j::config::eval_user_only(&mut j::eval::Interp::dummy(), &cfg).unwrap();
127 assert!(identity == ("First Person".into(), "first@example.org".into())
128 || identity == ("Second Person".into(), "second@example.org".into()));
129 assert_eq!(std::fs::read_dir(cli.path().parent().unwrap()).unwrap().count(), 1);
134fn setup_does_not_follow_existing_symlinks_to_overwrite_files() {
135 let cli = SetupCli::new();
136 let target = cli.config.path().join("personal-config");
137 std::fs::write(&target, "my private config").unwrap();
138 std::fs::create_dir_all(cli.path().parent().unwrap()).unwrap();
139 std::os::unix::fs::symlink(&target, cli.path()).unwrap();
140 let output = cli.run(&["setup", "Ada", "ada@example.org"]);
141 assert_eq!(output.status.code(), Some(2));
142 assert_eq!(std::fs::read_to_string(&target).unwrap(), "my private config");
143 assert!(std::fs::symlink_metadata(cli.path()).unwrap().file_type().is_symlink());
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list