Skip to content
This repository was archived by the owner on Oct 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions diffr.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions diffr.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
24 changes: 6 additions & 18 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ struct Cli {
command: Option<Command>,
#[arg(long, value_name = "repo", default_value = ".")]
repo: String,
/// Replace the global configuration file
#[arg(long = "config", value_name = "PATH")]
config_file: Option<PathBuf>,
/// Concurrent file diffs for --format ndjson; results are emitted as each finishes
#[arg(short, long, value_name = "jobs", default_value_t = 16)]
jobs: usize,
Expand Down Expand Up @@ -152,7 +149,7 @@ pub(crate) fn run() -> Result<i32> {
.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();
Expand Down Expand Up @@ -198,7 +195,7 @@ pub(crate) fn run() -> Result<i32> {
}
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:#}"))?;
Expand Down Expand Up @@ -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:#}"))?;
Expand Down Expand Up @@ -521,21 +518,16 @@ fn apply_unified(args: &Cli, config: &mut Config) {
}
}

/// The global file, or the `--config` file in its place.
fn load_config(args: &Cli) -> Result<Config> {
Ok(Config::load(args.config_file.as_deref())?)
}

/// `diffr config`: settings, schema, resolved values and edits.
fn run_config(args: &Cli, config: &ConfigArgs) -> Result<i32> {
fn run_config(config: &ConfigArgs) -> Result<i32> {
let mut stdout = io::stdout().lock();
match &config.command {
Some(ConfigCommand::Schema) => {
serde_json::to_writer_pretty(&mut stdout, &Config::schema())?;
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")?;
Expand All @@ -546,11 +538,7 @@ fn run_config(args: &Cli, config: &ConfigArgs) -> Result<i32> {
}
}
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")];
Expand Down
33 changes: 17 additions & 16 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Self, ConfigError> {
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, ConfigError> {
Self::load_from(&global_path()?)
}

/// The file at `path`; a missing file is the defaults.
fn load_from(path: &Path) -> Result<Self, ConfigError> {
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())))
}

Expand Down Expand Up @@ -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!(
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion src/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 15 additions & 16 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand All @@ -58,15 +59,14 @@ 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",
"--no-index",
&format!("sample_files/{fixture}_1.{extension}"),
&format!("sample_files/{fixture}_2.{extension}"),
])
.env("XDG_CONFIG_HOME", dir.path())
.assert()
.success()
.get_output()
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 6 additions & 7 deletions tests/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading