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