1mod builtin;
16mod diff_working_copies;
17mod external;
18
19use std::sync::Arc;
20
21use itertools::Itertools as _;
22use jj_lib::backend::BackendError;
23use jj_lib::backend::CopyId;
24use jj_lib::backend::TreeValue;
25use jj_lib::config::ConfigGetError;
26use jj_lib::config::ConfigGetResultExt as _;
27use jj_lib::config::ConfigNamePathBuf;
28use jj_lib::conflicts::ConflictMarkerStyle;
29use jj_lib::conflicts::MaterializedFileConflictValue;
30use jj_lib::conflicts::try_materialize_file_conflict_value;
31use jj_lib::gitignore::GitIgnoreFile;
32use jj_lib::matchers::Matcher;
33use jj_lib::merge::Diff;
34use jj_lib::merge::Merge;
35use jj_lib::merge::MergedTreeValue;
36use jj_lib::merged_tree::MergedTree;
37use jj_lib::merged_tree::MergedTreeBuilder;
38use jj_lib::repo_path::InvalidRepoPathError;
39use jj_lib::repo_path::RepoPath;
40use jj_lib::repo_path::RepoPathBuf;
41use jj_lib::repo_path::RepoPathUiConverter;
42use jj_lib::settings::UserSettings;
43use jj_lib::working_copy::SnapshotError;
44use pollster::FutureExt as _;
45use thiserror::Error;
46
47use self::builtin::BuiltinToolError;
48use self::builtin::edit_diff_builtin;
49use self::builtin::edit_merge_builtin;
50use self::diff_working_copies::DiffCheckoutError;
51pub(crate) use self::diff_working_copies::new_utf8_temp_dir;
52pub use self::external::DiffToolMode;
53pub use self::external::ExternalMergeTool;
54use self::external::ExternalToolError;
55use self::external::edit_diff_external;
56pub use self::external::generate_diff;
57pub use self::external::invoke_external_diff;
58use crate::config::CommandNameAndArgs;
59use crate::ui::Ui;
60
61const BUILTIN_EDITOR_NAME: &str = ":builtin";
62const OURS_TOOL_NAME: &str = ":ours";
63const THEIRS_TOOL_NAME: &str = ":theirs";
64
65#[derive(Debug, Error)]
66pub enum DiffEditError {
67 #[error(transparent)]
68 InternalTool(#[from] Box<BuiltinToolError>),
69 #[error(transparent)]
70 ExternalTool(#[from] ExternalToolError),
71 #[error(transparent)]
72 DiffCheckoutError(#[from] DiffCheckoutError),
73 #[error("Failed to snapshot changes")]
74 Snapshot(#[from] SnapshotError),
75 #[error(transparent)]
76 Config(#[from] ConfigGetError),
77}
78
79#[derive(Debug, Error)]
80pub enum DiffGenerateError {
81 #[error(transparent)]
82 ExternalTool(#[from] ExternalToolError),
83 #[error(transparent)]
84 DiffCheckoutError(#[from] DiffCheckoutError),
85}
86
87#[derive(Debug, Error)]
88pub enum ConflictResolveError {
89 #[error(transparent)]
90 InternalTool(#[from] Box<BuiltinToolError>),
91 #[error(transparent)]
92 ExternalTool(#[from] ExternalToolError),
93 #[error(transparent)]
94 InvalidRepoPath(#[from] InvalidRepoPathError),
95 #[error("Couldn't find the path {0:?} in this revision")]
96 PathNotFound(RepoPathBuf),
97 #[error("Couldn't find any conflicts at {0:?} in this revision")]
98 NotAConflict(RepoPathBuf),
99 #[error(
100 "Only conflicts that involve normal files (not symlinks, etc.) are supported. Conflict \
101 summary for {path:?}:\n{summary}",
102 summary = summary.trim_end()
103 )]
104 NotNormalFiles { path: RepoPathBuf, summary: String },
105 #[error("The conflict at {path:?} has {sides} sides. At most 2 sides are supported.")]
106 ConflictTooComplicated { path: RepoPathBuf, sides: usize },
107 #[error("{path:?} has conflicts in executable bit\n{summary}", summary = summary.trim_end())]
108 ExecutableConflict { path: RepoPathBuf, summary: String },
109 #[error(
110 "The output file is either unchanged or empty after the editor quit (run with --debug to \
111 see the exact invocation)."
112 )]
113 EmptyOrUnchanged,
114 #[error(transparent)]
115 Backend(#[from] jj_lib::backend::BackendError),
116 #[error(transparent)]
117 Io(#[from] std::io::Error),
118}
119
120#[derive(Debug, Error)]
121#[error("Stopped due to error after resolving {resolved_count} conflicts")]
122pub struct MergeToolPartialResolutionError {
123 pub source: ConflictResolveError,
124 pub resolved_count: usize,
125}
126
127#[derive(Debug, Error)]
128pub enum MergeToolConfigError {
129 #[error(transparent)]
130 Config(#[from] ConfigGetError),
131 #[error("The tool `{tool_name}` cannot be used as a merge tool with `jj resolve`")]
132 MergeArgsNotConfigured { tool_name: String },
133 #[error("The tool `{tool_name}` cannot be used as a diff editor")]
134 EditArgsNotConfigured { tool_name: String },
135}
136
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub enum MergeTool {
139 Builtin,
140 Ours,
141 Theirs,
142 External(Box<ExternalMergeTool>),
144}
145
146impl MergeTool {
147 fn external(tool: ExternalMergeTool) -> Self {
148 Self::External(Box::new(tool))
149 }
150
151 fn get_tool_config(
154 settings: &UserSettings,
155 name: &str,
156 ) -> Result<Option<Self>, MergeToolConfigError> {
157 match name {
158 BUILTIN_EDITOR_NAME => Ok(Some(Self::Builtin)),
159 OURS_TOOL_NAME => Ok(Some(Self::Ours)),
160 THEIRS_TOOL_NAME => Ok(Some(Self::Theirs)),
161 _ => Ok(get_external_tool_config(settings, name)?.map(Self::external)),
162 }
163 }
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum DiffEditTool {
168 Builtin,
169 External(Box<ExternalMergeTool>),
171}
172
173impl DiffEditTool {
174 fn external(tool: ExternalMergeTool) -> Self {
175 Self::External(Box::new(tool))
176 }
177
178 fn get_tool_config(
181 settings: &UserSettings,
182 name: &str,
183 ) -> Result<Option<Self>, MergeToolConfigError> {
184 match name {
185 BUILTIN_EDITOR_NAME => Ok(Some(Self::Builtin)),
186 _ => Ok(get_external_tool_config(settings, name)?.map(Self::external)),
187 }
188 }
189}
190
191fn editor_args_from_settings(
193 ui: &Ui,
194 settings: &UserSettings,
195 key: &'static str,
196) -> Result<CommandNameAndArgs, ConfigGetError> {
197 if let Some(args) = settings.get(key).optional()? {
200 Ok(args)
201 } else {
202 let default_editor = BUILTIN_EDITOR_NAME;
203 writeln!(
204 ui.hint_default(),
205 "Using default editor '{default_editor}'; run `jj config set --user {key} :builtin` \
206 to disable this message."
207 )
208 .ok();
209 Ok(default_editor.into())
210 }
211}
212
213pub fn configured_merge_tools(settings: &UserSettings) -> impl Iterator<Item = &str> {
215 settings.table_keys("merge-tools")
216}
217
218pub fn get_external_tool_config(
220 settings: &UserSettings,
221 name: &str,
222) -> Result<Option<ExternalMergeTool>, ConfigGetError> {
223 let full_name = ConfigNamePathBuf::from_iter(["merge-tools", name]);
224 let Some(mut tool) = settings.get::<ExternalMergeTool>(&full_name).optional()? else {
225 return Ok(None);
226 };
227 if tool.program.is_empty() {
228 tool.program = name.to_owned();
229 };
230 Ok(Some(tool))
231}
232
233#[derive(Clone, Debug)]
235pub struct DiffEditor {
236 tool: DiffEditTool,
237 base_ignores: Arc<GitIgnoreFile>,
238 use_instructions: bool,
239 conflict_marker_style: ConflictMarkerStyle,
240}
241
242impl DiffEditor {
243 pub fn with_name(
246 name: &str,
247 settings: &UserSettings,
248 base_ignores: Arc<GitIgnoreFile>,
249 conflict_marker_style: ConflictMarkerStyle,
250 ) -> Result<Self, MergeToolConfigError> {
251 let tool = DiffEditTool::get_tool_config(settings, name)?
252 .unwrap_or_else(|| DiffEditTool::external(ExternalMergeTool::with_program(name)));
253 Self::new_inner(name, tool, settings, base_ignores, conflict_marker_style)
254 }
255
256 pub fn from_settings(
258 ui: &Ui,
259 settings: &UserSettings,
260 base_ignores: Arc<GitIgnoreFile>,
261 conflict_marker_style: ConflictMarkerStyle,
262 ) -> Result<Self, MergeToolConfigError> {
263 let args = editor_args_from_settings(ui, settings, "ui.diff-editor")?;
264 let tool = if let Some(name) = args.as_str() {
265 DiffEditTool::get_tool_config(settings, name)?
266 } else {
267 None
268 }
269 .unwrap_or_else(|| DiffEditTool::external(ExternalMergeTool::with_edit_args(&args)));
270 Self::new_inner(&args, tool, settings, base_ignores, conflict_marker_style)
271 }
272
273 fn new_inner(
274 name: impl ToString,
275 tool: DiffEditTool,
276 settings: &UserSettings,
277 base_ignores: Arc<GitIgnoreFile>,
278 conflict_marker_style: ConflictMarkerStyle,
279 ) -> Result<Self, MergeToolConfigError> {
280 if let DiffEditTool::External(mergetool) = &tool
281 && mergetool.edit_args.is_empty()
282 {
283 return Err(MergeToolConfigError::EditArgsNotConfigured {
284 tool_name: name.to_string(),
285 });
286 }
287 Ok(Self {
288 tool,
289 base_ignores,
290 use_instructions: settings.get_bool("ui.diff-instructions")?,
291 conflict_marker_style,
292 })
293 }
294
295 pub fn edit(
297 &self,
298 trees: Diff<&MergedTree>,
299 matcher: &dyn Matcher,
300 format_instructions: impl FnOnce() -> String,
301 ) -> Result<MergedTree, DiffEditError> {
302 match &self.tool {
303 DiffEditTool::Builtin => {
304 Ok(
305 edit_diff_builtin(trees, matcher, self.conflict_marker_style)
306 .map_err(Box::new)?,
307 )
308 }
309 DiffEditTool::External(editor) => {
310 let instructions = self.use_instructions.then(format_instructions);
311 edit_diff_external(
312 editor,
313 trees,
314 matcher,
315 instructions.as_deref(),
316 self.base_ignores.clone(),
317 self.conflict_marker_style,
318 )
319 }
320 }
321 }
322}
323
324struct MergeToolFile {
326 repo_path: RepoPathBuf,
327 conflict: MergedTreeValue,
328 file: MaterializedFileConflictValue,
329}
330
331impl MergeToolFile {
332 fn from_tree_and_path(
333 tree: &MergedTree,
334 repo_path: &RepoPath,
335 ) -> Result<Self, ConflictResolveError> {
336 let conflict = match tree.path_value(repo_path)?.into_resolved() {
337 Err(conflict) => conflict,
338 Ok(Some(_)) => return Err(ConflictResolveError::NotAConflict(repo_path.to_owned())),
339 Ok(None) => return Err(ConflictResolveError::PathNotFound(repo_path.to_owned())),
340 };
341 let file =
342 try_materialize_file_conflict_value(tree.store(), repo_path, &conflict, tree.labels())
343 .block_on()?
344 .ok_or_else(|| ConflictResolveError::NotNormalFiles {
345 path: repo_path.to_owned(),
346 summary: conflict.describe(tree.labels()),
347 })?;
348 if file.ids.num_sides() > 2 {
350 return Err(ConflictResolveError::ConflictTooComplicated {
351 path: repo_path.to_owned(),
352 sides: file.ids.num_sides(),
353 });
354 };
355 if file.executable.is_none() {
356 return Err(ConflictResolveError::ExecutableConflict {
357 path: repo_path.to_owned(),
358 summary: conflict.describe(tree.labels()),
359 });
360 }
361 Ok(Self {
362 repo_path: repo_path.to_owned(),
363 conflict,
364 file,
365 })
366 }
367}
368
369#[derive(Clone, Debug)]
371pub struct MergeEditor {
372 tool: MergeTool,
373 path_converter: RepoPathUiConverter,
374 conflict_marker_style: ConflictMarkerStyle,
375}
376
377impl MergeEditor {
378 pub fn with_name(
381 name: &str,
382 settings: &UserSettings,
383 path_converter: RepoPathUiConverter,
384 conflict_marker_style: ConflictMarkerStyle,
385 ) -> Result<Self, MergeToolConfigError> {
386 let tool = MergeTool::get_tool_config(settings, name)?
387 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_program(name)));
388 Self::new_inner(name, tool, path_converter, conflict_marker_style)
389 }
390
391 pub fn from_settings(
393 ui: &Ui,
394 settings: &UserSettings,
395 path_converter: RepoPathUiConverter,
396 conflict_marker_style: ConflictMarkerStyle,
397 ) -> Result<Self, MergeToolConfigError> {
398 let args = editor_args_from_settings(ui, settings, "ui.merge-editor")?;
399 let tool = if let Some(name) = args.as_str() {
400 MergeTool::get_tool_config(settings, name)?
401 } else {
402 None
403 }
404 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_merge_args(&args)));
405 Self::new_inner(&args, tool, path_converter, conflict_marker_style)
406 }
407
408 fn new_inner(
409 name: impl ToString,
410 tool: MergeTool,
411 path_converter: RepoPathUiConverter,
412 conflict_marker_style: ConflictMarkerStyle,
413 ) -> Result<Self, MergeToolConfigError> {
414 if let MergeTool::External(mergetool) = &tool
415 && mergetool.merge_args.is_empty()
416 {
417 return Err(MergeToolConfigError::MergeArgsNotConfigured {
418 tool_name: name.to_string(),
419 });
420 }
421 Ok(Self {
422 tool,
423 path_converter,
424 conflict_marker_style,
425 })
426 }
427
428 pub fn edit_files(
430 &self,
431 ui: &Ui,
432 tree: &MergedTree,
433 repo_paths: &[&RepoPath],
434 ) -> Result<(MergedTree, Option<MergeToolPartialResolutionError>), ConflictResolveError> {
435 let merge_tool_files: Vec<MergeToolFile> = repo_paths
436 .iter()
437 .map(|&repo_path| MergeToolFile::from_tree_and_path(tree, repo_path))
438 .try_collect()?;
439
440 match &self.tool {
441 MergeTool::Builtin => {
442 let tree = edit_merge_builtin(tree, &merge_tool_files).map_err(Box::new)?;
443 Ok((tree, None))
444 }
445 MergeTool::Ours => {
446 let tree = pick_conflict_side(tree, &merge_tool_files, 0)?;
447 Ok((tree, None))
448 }
449 MergeTool::Theirs => {
450 let tree = pick_conflict_side(tree, &merge_tool_files, 1)?;
451 Ok((tree, None))
452 }
453 MergeTool::External(editor) => external::run_mergetool_external(
454 ui,
455 &self.path_converter,
456 editor,
457 tree,
458 &merge_tool_files,
459 self.conflict_marker_style,
460 ),
461 }
462 }
463}
464
465fn pick_conflict_side(
466 tree: &MergedTree,
467 merge_tool_files: &[MergeToolFile],
468 add_index: usize,
469) -> Result<MergedTree, BackendError> {
470 let mut tree_builder = MergedTreeBuilder::new(tree.clone());
471 for merge_tool_file in merge_tool_files {
472 let file = &merge_tool_file.file;
475 let file_id = file.ids.get_add(add_index).unwrap();
476 let executable = file.executable.expect("should have been resolved");
477 let new_tree_value = Merge::resolved(file_id.clone().map(|id| TreeValue::File {
478 id,
479 executable,
480 copy_id: CopyId::placeholder(),
481 }));
482 tree_builder.set_or_remove(merge_tool_file.repo_path.clone(), new_tree_value);
483 }
484 tree_builder.write_tree()
485}
486
487#[cfg(test)]
488mod tests {
489 use jj_lib::config::ConfigLayer;
490 use jj_lib::config::ConfigSource;
491 use jj_lib::config::StackedConfig;
492
493 use super::*;
494
495 fn config_from_string(text: &str) -> StackedConfig {
496 let mut config = StackedConfig::with_defaults();
497 config.extend_layers(crate::config::default_config_layers());
499 config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
500 config
501 }
502
503 #[test]
504 fn test_get_diff_editor_with_name() {
505 let get = |name, config_text| {
506 let config = config_from_string(config_text);
507 let settings = UserSettings::from_config(config).unwrap();
508 DiffEditor::with_name(
509 name,
510 &settings,
511 GitIgnoreFile::empty(),
512 ConflictMarkerStyle::Diff,
513 )
514 .map(|editor| editor.tool)
515 };
516
517 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
518
519 insta::assert_debug_snapshot!(get("my diff", "").unwrap(), @r#"
521 External(
522 ExternalMergeTool {
523 program: "my diff",
524 diff_args: [
525 "$left",
526 "$right",
527 ],
528 diff_expected_exit_codes: [
529 0,
530 ],
531 diff_invocation_mode: Dir,
532 diff_do_chdir: true,
533 edit_args: [
534 "$left",
535 "$right",
536 ],
537 merge_args: [],
538 merge_conflict_exit_codes: [],
539 merge_tool_edits_conflict_markers: false,
540 conflict_marker_style: None,
541 },
542 )
543 "#);
544
545 insta::assert_debug_snapshot!(get(
547 "foo bar", r#"
548 [merge-tools."foo bar"]
549 edit-args = ["--edit", "args", "$left", "$right"]
550 "#,
551 ).unwrap(), @r#"
552 External(
553 ExternalMergeTool {
554 program: "foo bar",
555 diff_args: [
556 "$left",
557 "$right",
558 ],
559 diff_expected_exit_codes: [
560 0,
561 ],
562 diff_invocation_mode: Dir,
563 diff_do_chdir: true,
564 edit_args: [
565 "--edit",
566 "args",
567 "$left",
568 "$right",
569 ],
570 merge_args: [],
571 merge_conflict_exit_codes: [],
572 merge_tool_edits_conflict_markers: false,
573 conflict_marker_style: None,
574 },
575 )
576 "#);
577 }
578
579 #[test]
580 fn test_get_diff_editor_from_settings() {
581 let get = |text| {
582 let config = config_from_string(text);
583 let ui = Ui::with_config(&config).unwrap();
584 let settings = UserSettings::from_config(config).unwrap();
585 DiffEditor::from_settings(
586 &ui,
587 &settings,
588 GitIgnoreFile::empty(),
589 ConflictMarkerStyle::Diff,
590 )
591 .map(|editor| editor.tool)
592 };
593
594 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
596
597 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = "my-diff""#).unwrap(), @r#"
599 External(
600 ExternalMergeTool {
601 program: "my-diff",
602 diff_args: [
603 "$left",
604 "$right",
605 ],
606 diff_expected_exit_codes: [
607 0,
608 ],
609 diff_invocation_mode: Dir,
610 diff_do_chdir: true,
611 edit_args: [
612 "$left",
613 "$right",
614 ],
615 merge_args: [],
616 merge_conflict_exit_codes: [],
617 merge_tool_edits_conflict_markers: false,
618 conflict_marker_style: None,
619 },
620 )
621 "#);
622
623 insta::assert_debug_snapshot!(
625 get(r#"ui.diff-editor = "my-diff -l $left -r $right""#).unwrap(), @r#"
626 External(
627 ExternalMergeTool {
628 program: "my-diff",
629 diff_args: [
630 "$left",
631 "$right",
632 ],
633 diff_expected_exit_codes: [
634 0,
635 ],
636 diff_invocation_mode: Dir,
637 diff_do_chdir: true,
638 edit_args: [
639 "-l",
640 "$left",
641 "-r",
642 "$right",
643 ],
644 merge_args: [],
645 merge_conflict_exit_codes: [],
646 merge_tool_edits_conflict_markers: false,
647 conflict_marker_style: None,
648 },
649 )
650 "#);
651
652 insta::assert_debug_snapshot!(
654 get(r#"ui.diff-editor = ["my-diff", "--diff", "$left", "$right"]"#).unwrap(), @r#"
655 External(
656 ExternalMergeTool {
657 program: "my-diff",
658 diff_args: [
659 "$left",
660 "$right",
661 ],
662 diff_expected_exit_codes: [
663 0,
664 ],
665 diff_invocation_mode: Dir,
666 diff_do_chdir: true,
667 edit_args: [
668 "--diff",
669 "$left",
670 "$right",
671 ],
672 merge_args: [],
673 merge_conflict_exit_codes: [],
674 merge_tool_edits_conflict_markers: false,
675 conflict_marker_style: None,
676 },
677 )
678 "#);
679
680 insta::assert_debug_snapshot!(get(
682 r#"
683 ui.diff-editor = "foo bar"
684 [merge-tools."foo bar"]
685 edit-args = ["--edit", "args", "$left", "$right"]
686 diff-args = [] # Should not cause an error, since we're getting the diff *editor*
687 "#,
688 ).unwrap(), @r#"
689 External(
690 ExternalMergeTool {
691 program: "foo bar",
692 diff_args: [],
693 diff_expected_exit_codes: [
694 0,
695 ],
696 diff_invocation_mode: Dir,
697 diff_do_chdir: true,
698 edit_args: [
699 "--edit",
700 "args",
701 "$left",
702 "$right",
703 ],
704 merge_args: [],
705 merge_conflict_exit_codes: [],
706 merge_tool_edits_conflict_markers: false,
707 conflict_marker_style: None,
708 },
709 )
710 "#);
711
712 insta::assert_debug_snapshot!(get(
714 r#"
715 ui.diff-editor = "my-diff"
716 [merge-tools.my-diff]
717 program = "MyDiff"
718 "#,
719 ).unwrap(), @r#"
720 External(
721 ExternalMergeTool {
722 program: "MyDiff",
723 diff_args: [
724 "$left",
725 "$right",
726 ],
727 diff_expected_exit_codes: [
728 0,
729 ],
730 diff_invocation_mode: Dir,
731 diff_do_chdir: true,
732 edit_args: [
733 "$left",
734 "$right",
735 ],
736 merge_args: [],
737 merge_conflict_exit_codes: [],
738 merge_tool_edits_conflict_markers: false,
739 conflict_marker_style: None,
740 },
741 )
742 "#);
743
744 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = ["meld"]"#).unwrap(), @r#"
746 External(
747 ExternalMergeTool {
748 program: "meld",
749 diff_args: [
750 "$left",
751 "$right",
752 ],
753 diff_expected_exit_codes: [
754 0,
755 ],
756 diff_invocation_mode: Dir,
757 diff_do_chdir: true,
758 edit_args: [
759 "$left",
760 "$right",
761 ],
762 merge_args: [],
763 merge_conflict_exit_codes: [],
764 merge_tool_edits_conflict_markers: false,
765 conflict_marker_style: None,
766 },
767 )
768 "#);
769
770 assert!(get(r#"ui.diff-editor.k = 0"#).is_err());
772
773 insta::assert_debug_snapshot!(get(
775 r#"
776 ui.diff-editor = "my-diff"
777 [merge-tools.my-diff]
778 program = "MyDiff"
779 edit-args = []
780 "#,
781 ), @r#"
782 Err(
783 EditArgsNotConfigured {
784 tool_name: "my-diff",
785 },
786 )
787 "#);
788 }
789
790 #[test]
791 fn test_get_merge_editor_with_name() {
792 let get = |name, config_text| {
793 let config = config_from_string(config_text);
794 let settings = UserSettings::from_config(config).unwrap();
795 let path_converter = RepoPathUiConverter::Fs {
796 cwd: "".into(),
797 base: "".into(),
798 };
799 MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff)
800 .map(|editor| editor.tool)
801 };
802
803 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
804
805 insta::assert_debug_snapshot!(get("my diff", "").unwrap_err(), @r#"
807 MergeArgsNotConfigured {
808 tool_name: "my diff",
809 }
810 "#);
811
812 insta::assert_debug_snapshot!(get(
814 "foo bar", r#"
815 [merge-tools."foo bar"]
816 merge-args = ["$base", "$left", "$right", "$output"]
817 "#,
818 ).unwrap(), @r#"
819 External(
820 ExternalMergeTool {
821 program: "foo bar",
822 diff_args: [
823 "$left",
824 "$right",
825 ],
826 diff_expected_exit_codes: [
827 0,
828 ],
829 diff_invocation_mode: Dir,
830 diff_do_chdir: true,
831 edit_args: [
832 "$left",
833 "$right",
834 ],
835 merge_args: [
836 "$base",
837 "$left",
838 "$right",
839 "$output",
840 ],
841 merge_conflict_exit_codes: [],
842 merge_tool_edits_conflict_markers: false,
843 conflict_marker_style: None,
844 },
845 )
846 "#);
847 }
848
849 #[test]
850 fn test_get_merge_editor_from_settings() {
851 let get = |text| {
852 let config = config_from_string(text);
853 let ui = Ui::with_config(&config).unwrap();
854 let settings = UserSettings::from_config(config).unwrap();
855 let path_converter = RepoPathUiConverter::Fs {
856 cwd: "".into(),
857 base: "".into(),
858 };
859 MergeEditor::from_settings(&ui, &settings, path_converter, ConflictMarkerStyle::Diff)
860 .map(|editor| editor.tool)
861 };
862
863 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
865
866 insta::assert_debug_snapshot!(get(r#"ui.merge-editor = "my-merge""#).unwrap_err(), @r#"
868 MergeArgsNotConfigured {
869 tool_name: "my-merge",
870 }
871 "#);
872
873 insta::assert_debug_snapshot!(
875 get(r#"ui.merge-editor = "my-merge $left $base $right $output""#).unwrap(), @r#"
876 External(
877 ExternalMergeTool {
878 program: "my-merge",
879 diff_args: [
880 "$left",
881 "$right",
882 ],
883 diff_expected_exit_codes: [
884 0,
885 ],
886 diff_invocation_mode: Dir,
887 diff_do_chdir: true,
888 edit_args: [
889 "$left",
890 "$right",
891 ],
892 merge_args: [
893 "$left",
894 "$base",
895 "$right",
896 "$output",
897 ],
898 merge_conflict_exit_codes: [],
899 merge_tool_edits_conflict_markers: false,
900 conflict_marker_style: None,
901 },
902 )
903 "#);
904
905 insta::assert_debug_snapshot!(
907 get(
908 r#"ui.merge-editor = ["my-merge", "$left", "$base", "$right", "$output"]"#,
909 ).unwrap(), @r#"
910 External(
911 ExternalMergeTool {
912 program: "my-merge",
913 diff_args: [
914 "$left",
915 "$right",
916 ],
917 diff_expected_exit_codes: [
918 0,
919 ],
920 diff_invocation_mode: Dir,
921 diff_do_chdir: true,
922 edit_args: [
923 "$left",
924 "$right",
925 ],
926 merge_args: [
927 "$left",
928 "$base",
929 "$right",
930 "$output",
931 ],
932 merge_conflict_exit_codes: [],
933 merge_tool_edits_conflict_markers: false,
934 conflict_marker_style: None,
935 },
936 )
937 "#);
938
939 insta::assert_debug_snapshot!(get(
941 r#"
942 ui.merge-editor = "foo bar"
943 [merge-tools."foo bar"]
944 merge-args = ["$base", "$left", "$right", "$output"]
945 "#,
946 ).unwrap(), @r#"
947 External(
948 ExternalMergeTool {
949 program: "foo bar",
950 diff_args: [
951 "$left",
952 "$right",
953 ],
954 diff_expected_exit_codes: [
955 0,
956 ],
957 diff_invocation_mode: Dir,
958 diff_do_chdir: true,
959 edit_args: [
960 "$left",
961 "$right",
962 ],
963 merge_args: [
964 "$base",
965 "$left",
966 "$right",
967 "$output",
968 ],
969 merge_conflict_exit_codes: [],
970 merge_tool_edits_conflict_markers: false,
971 conflict_marker_style: None,
972 },
973 )
974 "#);
975
976 insta::assert_debug_snapshot!(
978 get(r#"ui.merge-editor = ["meld"]"#).unwrap_err(), @r#"
979 MergeArgsNotConfigured {
980 tool_name: "meld",
981 }
982 "#);
983
984 assert!(get(r#"ui.merge-editor.k = 0"#).is_err());
986 }
987}