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::MergedTreeId;
25use jj_lib::backend::TreeValue;
26use jj_lib::config::ConfigGetError;
27use jj_lib::config::ConfigGetResultExt as _;
28use jj_lib::config::ConfigNamePathBuf;
29use jj_lib::conflicts::ConflictMarkerStyle;
30use jj_lib::conflicts::MaterializedFileConflictValue;
31use jj_lib::conflicts::try_materialize_file_conflict_value;
32use jj_lib::gitignore::GitIgnoreFile;
33use jj_lib::matchers::Matcher;
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: [&MergedTree; 2],
299 matcher: &dyn Matcher,
300 format_instructions: impl FnOnce() -> String,
301 ) -> Result<MergedTreeId, 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 = try_materialize_file_conflict_value(tree.store(), repo_path, &conflict)
342 .block_on()?
343 .ok_or_else(|| ConflictResolveError::NotNormalFiles {
344 path: repo_path.to_owned(),
345 summary: conflict.describe(),
346 })?;
347 if file.ids.num_sides() > 2 {
349 return Err(ConflictResolveError::ConflictTooComplicated {
350 path: repo_path.to_owned(),
351 sides: file.ids.num_sides(),
352 });
353 };
354 if file.executable.is_none() {
355 return Err(ConflictResolveError::ExecutableConflict {
356 path: repo_path.to_owned(),
357 summary: conflict.describe(),
358 });
359 }
360 Ok(Self {
361 repo_path: repo_path.to_owned(),
362 conflict,
363 file,
364 })
365 }
366}
367
368#[derive(Clone, Debug)]
370pub struct MergeEditor {
371 tool: MergeTool,
372 path_converter: RepoPathUiConverter,
373 conflict_marker_style: ConflictMarkerStyle,
374}
375
376impl MergeEditor {
377 pub fn with_name(
380 name: &str,
381 settings: &UserSettings,
382 path_converter: RepoPathUiConverter,
383 conflict_marker_style: ConflictMarkerStyle,
384 ) -> Result<Self, MergeToolConfigError> {
385 let tool = MergeTool::get_tool_config(settings, name)?
386 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_program(name)));
387 Self::new_inner(name, tool, path_converter, conflict_marker_style)
388 }
389
390 pub fn from_settings(
392 ui: &Ui,
393 settings: &UserSettings,
394 path_converter: RepoPathUiConverter,
395 conflict_marker_style: ConflictMarkerStyle,
396 ) -> Result<Self, MergeToolConfigError> {
397 let args = editor_args_from_settings(ui, settings, "ui.merge-editor")?;
398 let tool = if let Some(name) = args.as_str() {
399 MergeTool::get_tool_config(settings, name)?
400 } else {
401 None
402 }
403 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_merge_args(&args)));
404 Self::new_inner(&args, tool, path_converter, conflict_marker_style)
405 }
406
407 fn new_inner(
408 name: impl ToString,
409 tool: MergeTool,
410 path_converter: RepoPathUiConverter,
411 conflict_marker_style: ConflictMarkerStyle,
412 ) -> Result<Self, MergeToolConfigError> {
413 if let MergeTool::External(mergetool) = &tool
414 && mergetool.merge_args.is_empty()
415 {
416 return Err(MergeToolConfigError::MergeArgsNotConfigured {
417 tool_name: name.to_string(),
418 });
419 }
420 Ok(Self {
421 tool,
422 path_converter,
423 conflict_marker_style,
424 })
425 }
426
427 pub fn edit_files(
429 &self,
430 ui: &Ui,
431 tree: &MergedTree,
432 repo_paths: &[&RepoPath],
433 ) -> Result<(MergedTreeId, Option<MergeToolPartialResolutionError>), ConflictResolveError> {
434 let merge_tool_files: Vec<MergeToolFile> = repo_paths
435 .iter()
436 .map(|&repo_path| MergeToolFile::from_tree_and_path(tree, repo_path))
437 .try_collect()?;
438
439 match &self.tool {
440 MergeTool::Builtin => {
441 let tree_id = edit_merge_builtin(tree, &merge_tool_files).map_err(Box::new)?;
442 Ok((tree_id, None))
443 }
444 MergeTool::Ours => {
445 let tree_id = pick_conflict_side(tree, &merge_tool_files, 0)?;
446 Ok((tree_id, None))
447 }
448 MergeTool::Theirs => {
449 let tree_id = pick_conflict_side(tree, &merge_tool_files, 1)?;
450 Ok((tree_id, None))
451 }
452 MergeTool::External(editor) => external::run_mergetool_external(
453 ui,
454 &self.path_converter,
455 editor,
456 tree,
457 &merge_tool_files,
458 self.conflict_marker_style,
459 ),
460 }
461 }
462}
463
464fn pick_conflict_side(
465 tree: &MergedTree,
466 merge_tool_files: &[MergeToolFile],
467 add_index: usize,
468) -> Result<MergedTreeId, BackendError> {
469 let mut tree_builder = MergedTreeBuilder::new(tree.id());
470 for merge_tool_file in merge_tool_files {
471 let file = &merge_tool_file.file;
474 let file_id = file.ids.get_add(add_index).unwrap();
475 let executable = file.executable.expect("should have been resolved");
476 let new_tree_value = Merge::resolved(file_id.clone().map(|id| TreeValue::File {
477 id,
478 executable,
479 copy_id: CopyId::placeholder(),
480 }));
481 tree_builder.set_or_remove(merge_tool_file.repo_path.clone(), new_tree_value);
482 }
483 tree_builder.write_tree(tree.store())
484}
485
486#[cfg(test)]
487mod tests {
488 use jj_lib::config::ConfigLayer;
489 use jj_lib::config::ConfigSource;
490 use jj_lib::config::StackedConfig;
491
492 use super::*;
493
494 fn config_from_string(text: &str) -> StackedConfig {
495 let mut config = StackedConfig::with_defaults();
496 config.extend_layers(crate::config::default_config_layers());
498 config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
499 config
500 }
501
502 #[test]
503 fn test_get_diff_editor_with_name() {
504 let get = |name, config_text| {
505 let config = config_from_string(config_text);
506 let settings = UserSettings::from_config(config).unwrap();
507 DiffEditor::with_name(
508 name,
509 &settings,
510 GitIgnoreFile::empty(),
511 ConflictMarkerStyle::Diff,
512 )
513 .map(|editor| editor.tool)
514 };
515
516 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
517
518 insta::assert_debug_snapshot!(get("my diff", "").unwrap(), @r#"
520 External(
521 ExternalMergeTool {
522 program: "my diff",
523 diff_args: [
524 "$left",
525 "$right",
526 ],
527 diff_expected_exit_codes: [
528 0,
529 ],
530 diff_invocation_mode: Dir,
531 diff_do_chdir: true,
532 edit_args: [
533 "$left",
534 "$right",
535 ],
536 merge_args: [],
537 merge_conflict_exit_codes: [],
538 merge_tool_edits_conflict_markers: false,
539 conflict_marker_style: None,
540 },
541 )
542 "#);
543
544 insta::assert_debug_snapshot!(get(
546 "foo bar", r#"
547 [merge-tools."foo bar"]
548 edit-args = ["--edit", "args", "$left", "$right"]
549 "#,
550 ).unwrap(), @r#"
551 External(
552 ExternalMergeTool {
553 program: "foo bar",
554 diff_args: [
555 "$left",
556 "$right",
557 ],
558 diff_expected_exit_codes: [
559 0,
560 ],
561 diff_invocation_mode: Dir,
562 diff_do_chdir: true,
563 edit_args: [
564 "--edit",
565 "args",
566 "$left",
567 "$right",
568 ],
569 merge_args: [],
570 merge_conflict_exit_codes: [],
571 merge_tool_edits_conflict_markers: false,
572 conflict_marker_style: None,
573 },
574 )
575 "#);
576 }
577
578 #[test]
579 fn test_get_diff_editor_from_settings() {
580 let get = |text| {
581 let config = config_from_string(text);
582 let ui = Ui::with_config(&config).unwrap();
583 let settings = UserSettings::from_config(config).unwrap();
584 DiffEditor::from_settings(
585 &ui,
586 &settings,
587 GitIgnoreFile::empty(),
588 ConflictMarkerStyle::Diff,
589 )
590 .map(|editor| editor.tool)
591 };
592
593 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
595
596 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = "my-diff""#).unwrap(), @r#"
598 External(
599 ExternalMergeTool {
600 program: "my-diff",
601 diff_args: [
602 "$left",
603 "$right",
604 ],
605 diff_expected_exit_codes: [
606 0,
607 ],
608 diff_invocation_mode: Dir,
609 diff_do_chdir: true,
610 edit_args: [
611 "$left",
612 "$right",
613 ],
614 merge_args: [],
615 merge_conflict_exit_codes: [],
616 merge_tool_edits_conflict_markers: false,
617 conflict_marker_style: None,
618 },
619 )
620 "#);
621
622 insta::assert_debug_snapshot!(
624 get(r#"ui.diff-editor = "my-diff -l $left -r $right""#).unwrap(), @r#"
625 External(
626 ExternalMergeTool {
627 program: "my-diff",
628 diff_args: [
629 "$left",
630 "$right",
631 ],
632 diff_expected_exit_codes: [
633 0,
634 ],
635 diff_invocation_mode: Dir,
636 diff_do_chdir: true,
637 edit_args: [
638 "-l",
639 "$left",
640 "-r",
641 "$right",
642 ],
643 merge_args: [],
644 merge_conflict_exit_codes: [],
645 merge_tool_edits_conflict_markers: false,
646 conflict_marker_style: None,
647 },
648 )
649 "#);
650
651 insta::assert_debug_snapshot!(
653 get(r#"ui.diff-editor = ["my-diff", "--diff", "$left", "$right"]"#).unwrap(), @r#"
654 External(
655 ExternalMergeTool {
656 program: "my-diff",
657 diff_args: [
658 "$left",
659 "$right",
660 ],
661 diff_expected_exit_codes: [
662 0,
663 ],
664 diff_invocation_mode: Dir,
665 diff_do_chdir: true,
666 edit_args: [
667 "--diff",
668 "$left",
669 "$right",
670 ],
671 merge_args: [],
672 merge_conflict_exit_codes: [],
673 merge_tool_edits_conflict_markers: false,
674 conflict_marker_style: None,
675 },
676 )
677 "#);
678
679 insta::assert_debug_snapshot!(get(
681 r#"
682 ui.diff-editor = "foo bar"
683 [merge-tools."foo bar"]
684 edit-args = ["--edit", "args", "$left", "$right"]
685 diff-args = [] # Should not cause an error, since we're getting the diff *editor*
686 "#,
687 ).unwrap(), @r#"
688 External(
689 ExternalMergeTool {
690 program: "foo bar",
691 diff_args: [],
692 diff_expected_exit_codes: [
693 0,
694 ],
695 diff_invocation_mode: Dir,
696 diff_do_chdir: true,
697 edit_args: [
698 "--edit",
699 "args",
700 "$left",
701 "$right",
702 ],
703 merge_args: [],
704 merge_conflict_exit_codes: [],
705 merge_tool_edits_conflict_markers: false,
706 conflict_marker_style: None,
707 },
708 )
709 "#);
710
711 insta::assert_debug_snapshot!(get(
713 r#"
714 ui.diff-editor = "my-diff"
715 [merge-tools.my-diff]
716 program = "MyDiff"
717 "#,
718 ).unwrap(), @r#"
719 External(
720 ExternalMergeTool {
721 program: "MyDiff",
722 diff_args: [
723 "$left",
724 "$right",
725 ],
726 diff_expected_exit_codes: [
727 0,
728 ],
729 diff_invocation_mode: Dir,
730 diff_do_chdir: true,
731 edit_args: [
732 "$left",
733 "$right",
734 ],
735 merge_args: [],
736 merge_conflict_exit_codes: [],
737 merge_tool_edits_conflict_markers: false,
738 conflict_marker_style: None,
739 },
740 )
741 "#);
742
743 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = ["meld"]"#).unwrap(), @r#"
745 External(
746 ExternalMergeTool {
747 program: "meld",
748 diff_args: [
749 "$left",
750 "$right",
751 ],
752 diff_expected_exit_codes: [
753 0,
754 ],
755 diff_invocation_mode: Dir,
756 diff_do_chdir: true,
757 edit_args: [
758 "$left",
759 "$right",
760 ],
761 merge_args: [],
762 merge_conflict_exit_codes: [],
763 merge_tool_edits_conflict_markers: false,
764 conflict_marker_style: None,
765 },
766 )
767 "#);
768
769 assert!(get(r#"ui.diff-editor.k = 0"#).is_err());
771
772 insta::assert_debug_snapshot!(get(
774 r#"
775 ui.diff-editor = "my-diff"
776 [merge-tools.my-diff]
777 program = "MyDiff"
778 edit-args = []
779 "#,
780 ), @r#"
781 Err(
782 EditArgsNotConfigured {
783 tool_name: "my-diff",
784 },
785 )
786 "#);
787 }
788
789 #[test]
790 fn test_get_merge_editor_with_name() {
791 let get = |name, config_text| {
792 let config = config_from_string(config_text);
793 let settings = UserSettings::from_config(config).unwrap();
794 let path_converter = RepoPathUiConverter::Fs {
795 cwd: "".into(),
796 base: "".into(),
797 };
798 MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff)
799 .map(|editor| editor.tool)
800 };
801
802 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
803
804 insta::assert_debug_snapshot!(get("my diff", "").unwrap_err(), @r#"
806 MergeArgsNotConfigured {
807 tool_name: "my diff",
808 }
809 "#);
810
811 insta::assert_debug_snapshot!(get(
813 "foo bar", r#"
814 [merge-tools."foo bar"]
815 merge-args = ["$base", "$left", "$right", "$output"]
816 "#,
817 ).unwrap(), @r#"
818 External(
819 ExternalMergeTool {
820 program: "foo bar",
821 diff_args: [
822 "$left",
823 "$right",
824 ],
825 diff_expected_exit_codes: [
826 0,
827 ],
828 diff_invocation_mode: Dir,
829 diff_do_chdir: true,
830 edit_args: [
831 "$left",
832 "$right",
833 ],
834 merge_args: [
835 "$base",
836 "$left",
837 "$right",
838 "$output",
839 ],
840 merge_conflict_exit_codes: [],
841 merge_tool_edits_conflict_markers: false,
842 conflict_marker_style: None,
843 },
844 )
845 "#);
846 }
847
848 #[test]
849 fn test_get_merge_editor_from_settings() {
850 let get = |text| {
851 let config = config_from_string(text);
852 let ui = Ui::with_config(&config).unwrap();
853 let settings = UserSettings::from_config(config).unwrap();
854 let path_converter = RepoPathUiConverter::Fs {
855 cwd: "".into(),
856 base: "".into(),
857 };
858 MergeEditor::from_settings(&ui, &settings, path_converter, ConflictMarkerStyle::Diff)
859 .map(|editor| editor.tool)
860 };
861
862 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
864
865 insta::assert_debug_snapshot!(get(r#"ui.merge-editor = "my-merge""#).unwrap_err(), @r#"
867 MergeArgsNotConfigured {
868 tool_name: "my-merge",
869 }
870 "#);
871
872 insta::assert_debug_snapshot!(
874 get(r#"ui.merge-editor = "my-merge $left $base $right $output""#).unwrap(), @r#"
875 External(
876 ExternalMergeTool {
877 program: "my-merge",
878 diff_args: [
879 "$left",
880 "$right",
881 ],
882 diff_expected_exit_codes: [
883 0,
884 ],
885 diff_invocation_mode: Dir,
886 diff_do_chdir: true,
887 edit_args: [
888 "$left",
889 "$right",
890 ],
891 merge_args: [
892 "$left",
893 "$base",
894 "$right",
895 "$output",
896 ],
897 merge_conflict_exit_codes: [],
898 merge_tool_edits_conflict_markers: false,
899 conflict_marker_style: None,
900 },
901 )
902 "#);
903
904 insta::assert_debug_snapshot!(
906 get(
907 r#"ui.merge-editor = ["my-merge", "$left", "$base", "$right", "$output"]"#,
908 ).unwrap(), @r#"
909 External(
910 ExternalMergeTool {
911 program: "my-merge",
912 diff_args: [
913 "$left",
914 "$right",
915 ],
916 diff_expected_exit_codes: [
917 0,
918 ],
919 diff_invocation_mode: Dir,
920 diff_do_chdir: true,
921 edit_args: [
922 "$left",
923 "$right",
924 ],
925 merge_args: [
926 "$left",
927 "$base",
928 "$right",
929 "$output",
930 ],
931 merge_conflict_exit_codes: [],
932 merge_tool_edits_conflict_markers: false,
933 conflict_marker_style: None,
934 },
935 )
936 "#);
937
938 insta::assert_debug_snapshot!(get(
940 r#"
941 ui.merge-editor = "foo bar"
942 [merge-tools."foo bar"]
943 merge-args = ["$base", "$left", "$right", "$output"]
944 "#,
945 ).unwrap(), @r#"
946 External(
947 ExternalMergeTool {
948 program: "foo bar",
949 diff_args: [
950 "$left",
951 "$right",
952 ],
953 diff_expected_exit_codes: [
954 0,
955 ],
956 diff_invocation_mode: Dir,
957 diff_do_chdir: true,
958 edit_args: [
959 "$left",
960 "$right",
961 ],
962 merge_args: [
963 "$base",
964 "$left",
965 "$right",
966 "$output",
967 ],
968 merge_conflict_exit_codes: [],
969 merge_tool_edits_conflict_markers: false,
970 conflict_marker_style: None,
971 },
972 )
973 "#);
974
975 insta::assert_debug_snapshot!(
977 get(r#"ui.merge-editor = ["meld"]"#).unwrap_err(), @r#"
978 MergeArgsNotConfigured {
979 tool_name: "meld",
980 }
981 "#);
982
983 assert!(get(r#"ui.merge-editor.k = 0"#).is_err());
985 }
986}