on master help sign in
Create an editable default config when none exists
e8d9fc2iko
src/main.rs+27−18
@@ -38,19 +38,25 @@ fn config_path() -> String {
38 format!("{}/.config/j/config.j", home)
38 format!("{}/.config/j/config.j", home)
39}
39}
40
40
41+/// The default config, embedded so `j` always has it regardless of how it was
42+/// installed (the share/j/config.j file may not exist next to the binary).
43+const DEFAULT_CONFIG: &str = include_str!("../config.j");
44+
41fn load_config_file() -> Result<(Config, String), ExitCode> {
45fn load_config_file() -> Result<(Config, String), ExitCode> {
42 let path = config_path();
46 let path = config_path();
43 let src = match std::fs::read_to_string(&path) {
47 let src = match std::fs::read_to_string(&path) {
44 Ok(s) => s,
48 Ok(s) => s,
45 Err(_) => {
49 Err(_) => {
46− return Err(err(
50+ // No user config yet: materialise the default there, editable by
47− 3,
51+ // the current user, so they have a starting point to customise.
48− format!(
52+ if let Err(e) = write_default_config(&path) {
49− "no config at {}; the reference config.j is installed at {}",
53+ return Err(err(3, format!("cannot create config at {}: {}", path, e)));
50− path,
54+ }
51− reference_config_path()
55+ eprintln!("j: created an editable default config at {}", path);
52− ),
56+ match std::fs::read_to_string(&path) {
53− ));
57+ Ok(s) => s,
58+ Err(e) => return Err(err(3, format!("cannot read config at {}: {}", path, e))),
59+ }
54 }
60 }
55 };
61 };
56 match config::load_config(&src) {
62 match config::load_config(&src) {
@@ -59,17 +65,20 @@ fn load_config_file() -> Result<(Config, String), ExitCode> {
59 }
65 }
60}
66}
61
67
62−fn reference_config_path() -> String {
68+/// Write the embedded default config to `path`, creating parent directories
63− // installed next to the binary under <prefix>/share/j/config.j
69+/// and making the file readable and writable by the current user.
64− if let Ok(exe) = std::env::current_exe() {
70+fn write_default_config(path: &str) -> std::io::Result<()> {
65− if let Some(p) = exe.parent().and_then(|p| p.parent()) {
71+ if let Some(parent) = std::path::Path::new(path).parent() {
66− let cand = p.join("share/j/config.j");
72+ std::fs::create_dir_all(parent)?;
67− if cand.exists() {
73+ }
68− return cand.display().to_string();
74+ std::fs::write(path, DEFAULT_CONFIG)?;
69− }
75+ // user-read+write (0o600 on unix): the file is meant to be edited by them
70− }
76+ #[cfg(unix)]
77+ {
78+ use std::os::unix::fs::PermissionsExt;
79+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
71 }
80 }
72− "/usr/local/share/j/config.j".into()
81+ Ok(())
73}
82}
74
83
75const RESERVED: &[&str] = &["init", "clone", "remote", "fetch", "push", "undo", "redo", "ops"];
84const RESERVED: &[&str] = &["init", "clone", "remote", "fetch", "push", "undo", "redo", "ops"];
tests/cli.rs+15−4
@@ -450,7 +450,7 @@ fn init_refuses_inside_existing_repo() {
450}
450}
451
451
452#[test]
452#[test]
453−fn missing_config_is_exit_3() {
453+fn missing_config_is_created_editable() {
454 let dir = std::env::temp_dir().join(format!(
454 let dir = std::env::temp_dir().join(format!(
455 "j-cli-nocfg-{}-{}",
455 "j-cli-nocfg-{}-{}",
456 std::process::id(),
456 std::process::id(),
@@ -461,14 +461,25 @@ fn missing_config_is_exit_3() {
461 ));
461 ));
462 let _ = std::fs::remove_dir_all(&dir);
462 let _ = std::fs::remove_dir_all(&dir);
463 std::fs::create_dir_all(&dir).unwrap();
463 std::fs::create_dir_all(&dir).unwrap();
464+ let cfg_home = dir.join("nonexistent");
464 let out = Command::new(j_bin())
465 let out = Command::new(j_bin())
465 .arg("status")
466 .arg("status")
466 .current_dir(&dir)
467 .current_dir(&dir)
467− .env("XDG_CONFIG_HOME", dir.join("nonexistent"))
468+ .env("XDG_CONFIG_HOME", &cfg_home)
468 .output()
469 .output()
469 .unwrap();
470 .unwrap();
470− assert_eq!(out.status.code(), Some(3));
471+ // no config error: the default config is materialised instead
471 let stderr = String::from_utf8_lossy(&out.stderr);
472 let stderr = String::from_utf8_lossy(&out.stderr);
472− assert!(stderr.contains("no config"), "{}", stderr);
473+ assert!(!stderr.contains("no config"), "{}", stderr);
474+ assert!(stderr.contains("created an editable default config"), "{}", stderr);
475+ // the config now exists at the user path and is editable by the owner
476+ let cfg = cfg_home.join("j/config.j");
477+ let written = std::fs::read_to_string(&cfg).expect("config was created");
478+ assert!(written.contains("treeWith"), "default config content");
479+ #[cfg(unix)]
480+ {
481+ use std::os::unix::fs::PermissionsExt;
482+ assert_eq!(std::fs::metadata(&cfg).unwrap().permissions().mode() & 0o777, 0o600);
483+ }
473 let _ = std::fs::remove_dir_all(&dir);
484 let _ = std::fs::remove_dir_all(&dir);
474}
485}