diff --git a/diffr.1 b/diffr.1 index 37cab73c1..df3dc10a7 100644 --- a/diffr.1 +++ b/diffr.1 @@ -38,9 +38,6 @@ terminal UI, which needs a terminal. .B -U N Ordinary context padding (default 3). .TP -.B --config FILE -Read this TOML file in place of the global configuration file; it must exist. -.TP .B --help List supported options. Unsupported Git flags are rejected. .SH STREAMING diff --git a/diffr.1.md b/diffr.1.md index 4970d39c1..ac9a7e9af 100644 --- a/diffr.1.md +++ b/diffr.1.md @@ -43,9 +43,6 @@ compare their trees. A...B compares merge-base(A,B) to B; A..B compares A to B. **-U** N : Select ordinary context padding (default 3). -**--config** FILE -: Read this TOML file in place of the global configuration file (`~/.config/diffr/config.toml`); it must exist. - **--help** : List the supported options. Unsupported Git flags are rejected. diff --git a/src/cli.rs b/src/cli.rs index 47b74631e..cb7f110ec 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,9 +25,6 @@ struct Cli { command: Option, #[arg(long, value_name = "repo", default_value = ".")] repo: String, - /// Replace the global configuration file - #[arg(long = "config", value_name = "PATH")] - config_file: Option, /// Concurrent file diffs for --format ndjson; results are emitted as each finishes #[arg(short, long, value_name = "jobs", default_value_t = 16)] jobs: usize, @@ -152,7 +149,7 @@ pub(crate) fn run() -> Result { .unwrap_or_default(); let args = Cli::parse_from(argv); match &args.command { - Some(Command::Config(config)) => return run_config(&args, config), + Some(Command::Config(config)) => return run_config(config), None => {} } let streaming = args.format.is_some(); @@ -198,7 +195,7 @@ pub(crate) fn run() -> Result { } return Ok(i32::from(changed && (args.exit_code || args.quiet))); } - let mut config = load_config(&args)?; + let mut config = Config::load()?; apply_unified(&args, &mut config); let pipeline = Pipeline::from_config(&config.plugins, workspace).map_err(|error| format!("{error:#}"))?; @@ -456,7 +453,7 @@ fn no_index( if args.quiet { return Ok(i32::from(changed)); } - let mut config = load_config(args)?; + let mut config = Config::load()?; apply_unified(args, &mut config); let pipeline = Pipeline::from_config(&config.plugins, &std::env::current_dir()?) .map_err(|error| format!("{error:#}"))?; @@ -521,13 +518,8 @@ fn apply_unified(args: &Cli, config: &mut Config) { } } -/// The global file, or the `--config` file in its place. -fn load_config(args: &Cli) -> Result { - Ok(Config::load(args.config_file.as_deref())?) -} - /// `diffr config`: settings, schema, resolved values and edits. -fn run_config(args: &Cli, config: &ConfigArgs) -> Result { +fn run_config(config: &ConfigArgs) -> Result { let mut stdout = io::stdout().lock(); match &config.command { Some(ConfigCommand::Schema) => { @@ -535,7 +527,7 @@ fn run_config(args: &Cli, config: &ConfigArgs) -> Result { stdout.write_all(b"\n")?; } Some(ConfigCommand::Show { json, reveal }) => { - let config = load_config(args)?; + let config = Config::load()?; if *json { serde_json::to_writer_pretty(&mut stdout, &config::store::show(&config, *reveal))?; stdout.write_all(b"\n")?; @@ -546,11 +538,7 @@ fn run_config(args: &Cli, config: &ConfigArgs) -> Result { } } Some(ConfigCommand::Set { key, value }) => { - let path = match &args.config_file { - Some(path) => path.clone(), - None => config::global_path()?, - }; - config::store::set(&path, key, value)?; + config::store::set(&config::global_path()?, key, value)?; } None => { let mut frontend = vec![OsString::from("--settings")]; diff --git a/src/config.rs b/src/config.rs index c1ad8bf4d..a3460e6d6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ //! diffr's configuration comes from three places: the global file -//! (`$XDG_CONFIG_HOME/diffr/config.toml`, or `--config PATH` in its place), +//! (`$XDG_CONFIG_HOME/diffr/config.toml`), //! command-line flags, and git attributes. This module owns the file: every //! key it omits keeps its serde default, and an unknown key or mistyped //! value is an error naming the key's dotted path. Flags and attributes are @@ -182,21 +182,21 @@ pub(crate) fn directory_of(file: &Path) -> &Path { } impl Config { - /// Read the global file, or `explicit` in its place. A missing global - /// file is the defaults; an explicit file must exist. - pub(crate) fn load(explicit: Option<&Path>) -> Result { - let path = match explicit { - Some(path) => path.to_path_buf(), - None => global_path()?, - }; - let source = match std::fs::read_to_string(&path) { + /// Read the global file; a missing one is the defaults. + pub(crate) fn load() -> Result { + Self::load_from(&global_path()?) + } + + /// The file at `path`; a missing file is the defaults. + fn load_from(path: &Path) -> Result { + let source = match std::fs::read_to_string(path) { Ok(source) => source, - Err(error) if explicit.is_none() && error.kind() == std::io::ErrorKind::NotFound => { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { return Ok(Self::default()); } Err(error) => return Err(ConfigError(format!("{}: {error}", path.display()))), }; - Self::from_toml_in(&source, directory_of(&path)) + Self::from_toml_in(&source, directory_of(path)) .map_err(|error| ConfigError(format!("{}: {error}", path.display()))) } @@ -728,7 +728,7 @@ mod load_tests { "[diff]\ngraph_limit = 5\nbyte_limit = 6\n[theme]\nname = 'mine'\n", ) .unwrap(); - let config = Config::load(Some(&path)).unwrap(); + let config = Config::load_from(&path).unwrap(); assert_eq!(config.diff.graph_limit, 5); assert_eq!(config.diff.byte_limit, 6); assert_eq!( @@ -744,20 +744,21 @@ mod load_tests { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.toml"); std::fs::write(&path, "[diff]\ngraph_limit = 5\ntypo = 1\n").unwrap(); - let error = Config::load(Some(&path)).unwrap_err().to_string(); + let error = Config::load_from(&path).unwrap_err().to_string(); assert!( error.starts_with(&format!("{}: diff.typo: ", path.display())), "{error}" ); std::fs::write(&path, "[diff]\ngraph_limit = 'many'\n").unwrap(); - let error = Config::load(Some(&path)).unwrap_err().to_string(); + let error = Config::load_from(&path).unwrap_err().to_string(); assert!(error.contains("diff.graph_limit: "), "{error}"); } #[test] - fn a_missing_explicit_file_is_an_error() { + fn a_missing_file_is_the_defaults() { let dir = tempfile::tempdir().unwrap(); - assert!(Config::load(Some(&dir.path().join("absent.toml"))).is_err()); + let config = Config::load_from(&dir.path().join("absent.toml")).unwrap(); + assert_eq!(config.theme.name, Config::default().theme.name); } #[test] diff --git a/src/config/README.md b/src/config/README.md index cf817dda8..afce36a1a 100644 --- a/src/config/README.md +++ b/src/config/README.md @@ -3,7 +3,7 @@ The caller parses TOML with `Config::from_toml`, builds the plugin pipeline, then calls `compile_with(&pipeline)` once. The resulting `Params` owns one compiled query per language, holding the enabled plugins' fold patterns, and is borrowed by each diff. -`Config::load` reads the global file (`$XDG_CONFIG_HOME/diffr/config.toml`) or an explicit replacement file. +`Config::load` reads the global file (`$XDG_CONFIG_HOME/diffr/config.toml`). File selection and ordering belong to the caller, not this configuration. ```rust diff --git a/tests/cli.rs b/tests/cli.rs index b1670166a..4946a56de 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -36,7 +36,8 @@ fn optional_languages_follow_build_features() { .clone(); let listed = String::from_utf8(listed).unwrap(); let dir = tempfile::tempdir().unwrap(); - let config = dir.path().join("config.toml"); + let config = dir.path().join("diffr/config.toml"); + std::fs::create_dir_all(config.parent().unwrap()).unwrap(); std::fs::write(&config, "[plugins]\norder = []\n").unwrap(); for (name, fixture, extension, enabled) in [ @@ -58,8 +59,6 @@ fn optional_languages_follow_build_features() { ] { assert_eq!(listed.contains(name), enabled, "{name}"); let output = get_base_command() - .arg("--config") - .arg(&config) .args([ "--format", "ndjson", @@ -67,6 +66,7 @@ fn optional_languages_follow_build_features() { &format!("sample_files/{fixture}_1.{extension}"), &format!("sample_files/{fixture}_2.{extension}"), ]) + .env("XDG_CONFIG_HOME", dir.path()) .assert() .success() .get_output() @@ -139,22 +139,21 @@ fn text_output_is_gone() { #[test] fn a_plugin_that_cannot_be_made_stops_diffr_before_any_record() { let dir = tempfile::tempdir().unwrap(); - let config = dir.path().join("config.toml"); + let config = dir.path().join("diffr/config.toml"); + std::fs::create_dir_all(config.parent().unwrap()).unwrap(); std::fs::write(&config, "[plugins.bundled.summarize]\nenabled = true\n").unwrap(); let mut cmd = get_base_command(); - cmd.arg("--config") - .arg(&config) - .args([ - "--no-index", - "sample_files/simple_1.js", - "sample_files/simple_2.js", - "--format", - "ndjson", - ]) - .env("XDG_CONFIG_HOME", dir.path()) - .env_remove("GEMINI_API_KEY") - .env_remove("GOOGLE_API_KEY"); + cmd.args([ + "--no-index", + "sample_files/simple_1.js", + "sample_files/simple_2.js", + "--format", + "ndjson", + ]) + .env("XDG_CONFIG_HOME", dir.path()) + .env_remove("GEMINI_API_KEY") + .env_remove("GOOGLE_API_KEY"); cmd.assert() .failure() .code(2) diff --git a/tests/wasm.rs b/tests/wasm.rs index 6001793fe..d7a5b5eca 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -100,24 +100,23 @@ impl Fixture { .to_string() } - /// A config file holding `text`, in a directory of its own. + /// A global config file holding `text`, under a config home of its own. fn config(&self, name: &str, text: &str) -> PathBuf { - let path = self.dir.path().join(name).join("config.toml"); + let path = self.dir.path().join(name).join("diffr/config.toml"); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(&path, text).unwrap(); path } + /// Diff `base` to `head` with `config` as the global config file. fn run(&self, config: &Path, base: &str, head: &str) -> Output { - let home = self.dir.path().join("home"); + let config_home = config.parent().unwrap().parent().unwrap(); get_base_command() .arg("--repo") .arg(self.dir.path().join("repo")) - .arg("--config") - .arg(config) .args([base, head, "--format", "ndjson"]) - .env("HOME", &home) - .env("XDG_CONFIG_HOME", home.join(".config")) + .env("HOME", self.dir.path().join("home")) + .env("XDG_CONFIG_HOME", config_home) .env_remove("GIT_DIR") .env_remove("GEMINI_API_KEY") .env_remove("GOOGLE_API_KEY")