diff --git a/docs/config.md b/docs/config.md index 0675cdab9..813407ac1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -143,9 +143,18 @@ ui.diff.format = "git" ### Generating diffs by external command -If `diff --tool ` argument is given, the external diff command will be -called instead of the internal diff function. The command arguments can be -specified as follows. +If `ui.diff.tool` is set, the specified diff command will be called instead of +the internal diff function. + +```toml +# Use Difftastic by default +ui.diff.tool = ["difft", "--color=always", "$left", "$right"] +# Use tool named "" (see below) +ui.diff.tool = "" +``` + +The external diff tool can also be enabled by `diff --tool ` argument. +For the tool named ``, command arguments can be configured as follows. ```toml [merge-tools.] diff --git a/src/config-schema.json b/src/config-schema.json index a326b8a6c..e4e336301 100644 --- a/src/config-schema.json +++ b/src/config-schema.json @@ -82,6 +82,10 @@ "summary" ], "default": "color-words" + }, + "tool": { + "type": "string", + "description": "External tool for generating diffs" } } }, diff --git a/src/diff_util.rs b/src/diff_util.rs index 25a0424de..4aad8390b 100644 --- a/src/diff_util.rs +++ b/src/diff_util.rs @@ -122,6 +122,12 @@ fn diff_formats_from_args( fn default_diff_format(settings: &UserSettings) -> Result { let config = settings.config(); + if let Some(args) = config.get("ui.diff.tool").optional()? { + // External "tool" overrides the internal "format" option. + let tool = merge_tools::get_tool_config_from_args(settings, &args)? + .unwrap_or_else(|| MergeTool::with_diff_args(&args)); + return Ok(DiffFormat::Tool(Box::new(tool))); + } let name = if let Some(name) = config.get_string("ui.diff.format").optional()? { name } else if let Some(name) = config.get_string("diff.format").optional()? { @@ -134,7 +140,6 @@ fn default_diff_format(settings: &UserSettings) -> Result Ok(DiffFormat::Types), "git" => Ok(DiffFormat::Git), "color-words" => Ok(DiffFormat::ColorWords), - // TODO: add configurable default for DiffFormat::Tool? _ => Err(config::ConfigError::Message(format!( "invalid diff format: {name}" ))), diff --git a/src/merge_tools.rs b/src/merge_tools.rs index 05440a8bf..3dd9da999 100644 --- a/src/merge_tools.rs +++ b/src/merge_tools.rs @@ -509,6 +509,10 @@ impl MergeTool { } } + pub fn with_diff_args(command_args: &CommandNameAndArgs) -> Self { + Self::with_args_inner(command_args, |tool| &mut tool.diff_args) + } + pub fn with_edit_args(command_args: &CommandNameAndArgs) -> Self { Self::with_args_inner(command_args, |tool| &mut tool.edit_args) } @@ -558,7 +562,7 @@ pub fn get_tool_config( /// Loads merge tool options from `[merge-tools.]` if `args` is of /// unstructured string type. -fn get_tool_config_from_args( +pub fn get_tool_config_from_args( settings: &UserSettings, args: &CommandNameAndArgs, ) -> Result, ConfigError> { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 03e94e8a5..cb13e7852 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -250,11 +250,7 @@ impl TestEnvironment { /// Sets up the fake diff-editor to read an edit script from the returned /// path pub fn set_up_fake_diff_editor(&mut self) -> PathBuf { - let diff_editor_path = assert_cmd::cargo::cargo_bin("fake-diff-editor"); - assert!(diff_editor_path.is_file()); - // Simplified TOML escaping, hoping that there are no '"' or control characters - // in it - let escaped_diff_editor_path = diff_editor_path.to_str().unwrap().replace('\\', r"\\"); + let escaped_diff_editor_path = escaped_fake_diff_editor_path(); self.add_config(&format!( r###" ui.diff-editor = "fake-diff-editor" @@ -301,3 +297,11 @@ pub fn get_stdout_string(assert: &assert_cmd::assert::Assert) -> String { pub fn get_stderr_string(assert: &assert_cmd::assert::Assert) -> String { String::from_utf8(assert.get_output().stderr.clone()).unwrap() } + +pub fn escaped_fake_diff_editor_path() -> String { + let diff_editor_path = assert_cmd::cargo::cargo_bin("fake-diff-editor"); + assert!(diff_editor_path.is_file()); + // Simplified TOML escaping, hoping that there are no '"' or control characters + // in it + diff_editor_path.to_str().unwrap().replace('\\', r"\\") +} diff --git a/tests/test_diff_command.rs b/tests/test_diff_command.rs index f0ae184fa..923d1cd4c 100644 --- a/tests/test_diff_command.rs +++ b/tests/test_diff_command.rs @@ -647,6 +647,27 @@ fn test_diff_external_tool() { file3 "###); + // Enabled by default, looks up the merge-tools table + let config = "--config-toml=ui.diff.tool='fake-diff-editor'"; + insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", config]), @r###" + file1 + file2 + -- + file2 + file3 + "###); + + // Inlined command arguments + let command = common::escaped_fake_diff_editor_path(); + let config = format!(r#"--config-toml=ui.diff.tool=["{command}", "$right", "$left"]"#); + insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", &config]), @r###" + file2 + file3 + -- + file1 + file2 + "###); + // Output of external diff tool shouldn't be escaped std::fs::write(&edit_script, "print \x1b[1;31mred").unwrap(); insta::assert_snapshot!(