1mod builtin;
16mod diff_working_copies;
17mod external;
18
19use std::sync::Arc;
20
21use futures::future::try_join_all;
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_builder::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 thiserror::Error;
45
46use self::builtin::BuiltinToolError;
47use self::builtin::edit_diff_builtin;
48use self::builtin::edit_merge_builtin;
49use self::diff_working_copies::DiffCheckoutError;
50pub(crate) use self::diff_working_copies::new_utf8_temp_dir;
51pub use self::external::DiffToolMode;
52pub use self::external::ExternalMergeTool;
53use self::external::ExternalToolError;
54use self::external::edit_diff_external;
55pub use self::external::generate_diff;
56pub use self::external::invoke_external_diff;
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 #[error("The tool `{tool_name}` cannot be used as a diff editor")]
133 EditArgsNotConfigured { tool_name: String },
134}
135
136#[derive(Clone, Debug, Eq, PartialEq)]
137pub enum MergeTool {
138 Builtin,
139 Ours,
140 Theirs,
141 External(Box<ExternalMergeTool>),
143}
144
145impl MergeTool {
146 fn external(tool: ExternalMergeTool) -> Self {
147 Self::External(Box::new(tool))
148 }
149
150 fn get_tool_config(
153 settings: &UserSettings,
154 name: &str,
155 ) -> Result<Option<Self>, MergeToolConfigError> {
156 match name {
157 BUILTIN_EDITOR_NAME => Ok(Some(Self::Builtin)),
158 OURS_TOOL_NAME => Ok(Some(Self::Ours)),
159 THEIRS_TOOL_NAME => Ok(Some(Self::Theirs)),
160 _ => Ok(get_external_tool_config(settings, name)?.map(Self::external)),
161 }
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub enum DiffEditTool {
167 Builtin,
168 External(Box<ExternalMergeTool>),
170}
171
172impl DiffEditTool {
173 fn external(tool: ExternalMergeTool) -> Self {
174 Self::External(Box::new(tool))
175 }
176
177 fn get_tool_config(
180 settings: &UserSettings,
181 name: &str,
182 ) -> Result<Option<Self>, MergeToolConfigError> {
183 match name {
184 BUILTIN_EDITOR_NAME => Ok(Some(Self::Builtin)),
185 _ => Ok(get_external_tool_config(settings, name)?.map(Self::external)),
186 }
187 }
188}
189
190fn editor_args_from_settings(
192 ui: &Ui,
193 settings: &UserSettings,
194 key: &'static str,
195) -> Result<CommandNameAndArgs, ConfigGetError> {
196 if let Some(args) = settings.get(key).optional()? {
199 Ok(args)
200 } else {
201 let default_editor = BUILTIN_EDITOR_NAME;
202 writeln!(
203 ui.hint_default(),
204 "Using default editor '{default_editor}'; run `jj config set --user {key} :builtin` \
205 to disable this message."
206 )
207 .ok();
208 Ok(default_editor.into())
209 }
210}
211
212pub fn configured_merge_tools(settings: &UserSettings) -> impl Iterator<Item = &str> {
214 settings.table_keys("merge-tools")
215}
216
217pub fn get_external_tool_config(
219 settings: &UserSettings,
220 name: &str,
221) -> Result<Option<ExternalMergeTool>, ConfigGetError> {
222 let full_name = ConfigNamePathBuf::from_iter(["merge-tools", name]);
223 let Some(mut tool) = settings.get::<ExternalMergeTool>(&full_name).optional()? else {
224 return Ok(None);
225 };
226 if tool.program.is_empty() {
227 tool.program = name.to_owned();
228 }
229 Ok(Some(tool))
230}
231
232#[derive(Clone, Debug)]
234pub struct DiffEditor {
235 tool: DiffEditTool,
236 base_ignores: Arc<GitIgnoreFile>,
237 use_instructions: bool,
238 conflict_marker_style: ConflictMarkerStyle,
239}
240
241impl DiffEditor {
242 pub fn with_name(
245 name: &str,
246 settings: &UserSettings,
247 base_ignores: Arc<GitIgnoreFile>,
248 conflict_marker_style: ConflictMarkerStyle,
249 ) -> Result<Self, MergeToolConfigError> {
250 let tool = DiffEditTool::get_tool_config(settings, name)?
251 .unwrap_or_else(|| DiffEditTool::external(ExternalMergeTool::with_program(name)));
252 Self::new_inner(name, tool, settings, base_ignores, conflict_marker_style)
253 }
254
255 pub fn from_settings(
257 ui: &Ui,
258 settings: &UserSettings,
259 base_ignores: Arc<GitIgnoreFile>,
260 conflict_marker_style: ConflictMarkerStyle,
261 ) -> Result<Self, MergeToolConfigError> {
262 let args = editor_args_from_settings(ui, settings, "ui.diff-editor")?;
263 let tool = if let Some(name) = args.as_str() {
264 DiffEditTool::get_tool_config(settings, name)?
265 } else {
266 None
267 }
268 .unwrap_or_else(|| DiffEditTool::external(ExternalMergeTool::with_edit_args(&args)));
269 Self::new_inner(&args, tool, settings, base_ignores, conflict_marker_style)
270 }
271
272 fn new_inner(
273 name: impl ToString,
274 tool: DiffEditTool,
275 settings: &UserSettings,
276 base_ignores: Arc<GitIgnoreFile>,
277 conflict_marker_style: ConflictMarkerStyle,
278 ) -> Result<Self, MergeToolConfigError> {
279 if let DiffEditTool::External(mergetool) = &tool
280 && mergetool.edit_args.is_empty()
281 {
282 return Err(MergeToolConfigError::EditArgsNotConfigured {
283 tool_name: name.to_string(),
284 });
285 }
286 Ok(Self {
287 tool,
288 base_ignores,
289 use_instructions: settings.get_bool("ui.diff-instructions")?,
290 conflict_marker_style,
291 })
292 }
293
294 pub async fn edit(
296 &self,
297 trees: Diff<&MergedTree>,
298 matcher: &dyn Matcher,
299 format_instructions: impl FnOnce() -> String,
300 ) -> Result<MergedTree, DiffEditError> {
301 match &self.tool {
302 DiffEditTool::Builtin => {
303 Ok(
304 edit_diff_builtin(trees, matcher, self.conflict_marker_style)
305 .await
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 .await
320 }
321 }
322 }
323}
324
325struct MergeToolFile {
327 repo_path: RepoPathBuf,
328 conflict: MergedTreeValue,
329 file: MaterializedFileConflictValue,
330}
331
332impl MergeToolFile {
333 async fn from_tree_and_path(
334 tree: &MergedTree,
335 repo_path: &RepoPath,
336 ) -> Result<Self, ConflictResolveError> {
337 let conflict = match tree.path_value(repo_path).await?.into_resolved() {
338 Err(conflict) => conflict,
339 Ok(Some(_)) => return Err(ConflictResolveError::NotAConflict(repo_path.to_owned())),
340 Ok(None) => return Err(ConflictResolveError::PathNotFound(repo_path.to_owned())),
341 };
342 let file =
343 try_materialize_file_conflict_value(tree.store(), repo_path, &conflict, tree.labels())
344 .await?
345 .ok_or_else(|| ConflictResolveError::NotNormalFiles {
346 path: repo_path.to_owned(),
347 summary: conflict.describe(tree.labels()),
348 })?;
349 if file.ids.num_sides() > 2 {
351 return Err(ConflictResolveError::ConflictTooComplicated {
352 path: repo_path.to_owned(),
353 sides: file.ids.num_sides(),
354 });
355 }
356 if file.executable.is_none() {
357 return Err(ConflictResolveError::ExecutableConflict {
358 path: repo_path.to_owned(),
359 summary: conflict.describe(tree.labels()),
360 });
361 }
362 Ok(Self {
363 repo_path: repo_path.to_owned(),
364 conflict,
365 file,
366 })
367 }
368}
369
370#[derive(Clone, Debug)]
372pub struct MergeEditor {
373 tool: MergeTool,
374 path_converter: RepoPathUiConverter,
375 conflict_marker_style: ConflictMarkerStyle,
376}
377
378impl MergeEditor {
379 pub fn with_name(
382 name: &str,
383 settings: &UserSettings,
384 path_converter: RepoPathUiConverter,
385 conflict_marker_style: ConflictMarkerStyle,
386 ) -> Result<Self, MergeToolConfigError> {
387 let tool = MergeTool::get_tool_config(settings, name)?
388 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_program(name)));
389 Self::new_inner(name, tool, path_converter, conflict_marker_style)
390 }
391
392 pub fn from_settings(
394 ui: &Ui,
395 settings: &UserSettings,
396 path_converter: RepoPathUiConverter,
397 conflict_marker_style: ConflictMarkerStyle,
398 ) -> Result<Self, MergeToolConfigError> {
399 let args = editor_args_from_settings(ui, settings, "ui.merge-editor")?;
400 let tool = if let Some(name) = args.as_str() {
401 MergeTool::get_tool_config(settings, name)?
402 } else {
403 None
404 }
405 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_merge_args(&args)));
406 Self::new_inner(&args, tool, path_converter, conflict_marker_style)
407 }
408
409 fn new_inner(
410 name: impl ToString,
411 tool: MergeTool,
412 path_converter: RepoPathUiConverter,
413 conflict_marker_style: ConflictMarkerStyle,
414 ) -> Result<Self, MergeToolConfigError> {
415 if let MergeTool::External(mergetool) = &tool
416 && mergetool.merge_args.is_empty()
417 {
418 return Err(MergeToolConfigError::MergeArgsNotConfigured {
419 tool_name: name.to_string(),
420 });
421 }
422 Ok(Self {
423 tool,
424 path_converter,
425 conflict_marker_style,
426 })
427 }
428
429 pub async fn edit_files(
431 &self,
432 ui: &Ui,
433 tree: &MergedTree,
434 repo_paths: &[&RepoPath],
435 ) -> Result<(MergedTree, Option<MergeToolPartialResolutionError>), ConflictResolveError> {
436 let merge_tool_files: Vec<MergeToolFile> = try_join_all(
437 repo_paths
438 .iter()
439 .map(|&repo_path| MergeToolFile::from_tree_and_path(tree, repo_path)),
440 )
441 .await?;
442
443 match &self.tool {
444 MergeTool::Builtin => {
445 let tree = edit_merge_builtin(tree, &merge_tool_files)
446 .await
447 .map_err(Box::new)?;
448 Ok((tree, None))
449 }
450 MergeTool::Ours => {
451 let tree = pick_conflict_side(tree, &merge_tool_files, 0).await?;
452 Ok((tree, None))
453 }
454 MergeTool::Theirs => {
455 let tree = pick_conflict_side(tree, &merge_tool_files, 1).await?;
456 Ok((tree, None))
457 }
458 MergeTool::External(editor) => {
459 external::run_mergetool_external(
460 ui,
461 &self.path_converter,
462 editor,
463 tree,
464 &merge_tool_files,
465 self.conflict_marker_style,
466 )
467 .await
468 }
469 }
470 }
471}
472
473async fn pick_conflict_side(
474 tree: &MergedTree,
475 merge_tool_files: &[MergeToolFile],
476 add_index: usize,
477) -> Result<MergedTree, BackendError> {
478 let mut tree_builder = MergedTreeBuilder::new(tree.clone());
479 for merge_tool_file in merge_tool_files {
480 let file = &merge_tool_file.file;
483 let file_id = file.ids.get_add(add_index).unwrap();
484 let executable = file.executable.expect("should have been resolved");
485 let new_tree_value = Merge::resolved(file_id.clone().map(|id| TreeValue::File {
486 id,
487 executable,
488 copy_id: CopyId::placeholder(),
489 }));
490 tree_builder.set_or_remove(merge_tool_file.repo_path.clone(), new_tree_value);
491 }
492 tree_builder.write_tree().await
493}
494
495#[cfg(test)]
496mod tests {
497 use jj_lib::config::ConfigLayer;
498 use jj_lib::config::ConfigSource;
499 use jj_lib::config::StackedConfig;
500
501 use super::*;
502
503 fn config_from_string(text: &str) -> StackedConfig {
504 let mut config = StackedConfig::with_defaults();
505 config.extend_layers(crate::config::default_config_layers());
507 config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
508 config
509 }
510
511 #[test]
512 fn test_get_diff_editor_with_name() {
513 let get = |name, config_text| {
514 let config = config_from_string(config_text);
515 let settings = UserSettings::from_config(config).unwrap();
516 DiffEditor::with_name(
517 name,
518 &settings,
519 GitIgnoreFile::empty(),
520 ConflictMarkerStyle::Diff,
521 )
522 .map(|editor| editor.tool)
523 };
524
525 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
526
527 insta::assert_debug_snapshot!(get("my diff", "").unwrap(), @r#"
529 External(
530 ExternalMergeTool {
531 program: "my diff",
532 diff_args: [
533 "$left",
534 "$right",
535 ],
536 diff_expected_exit_codes: [
537 0,
538 ],
539 diff_invocation_mode: Dir,
540 diff_do_chdir: true,
541 edit_args: [
542 "$left",
543 "$right",
544 ],
545 merge_args: [],
546 merge_conflict_exit_codes: [],
547 merge_tool_edits_conflict_markers: false,
548 conflict_marker_style: None,
549 },
550 )
551 "#);
552
553 insta::assert_debug_snapshot!(get(
555 "foo bar", r#"
556 [merge-tools."foo bar"]
557 edit-args = ["--edit", "args", "$left", "$right"]
558 "#,
559 ).unwrap(), @r#"
560 External(
561 ExternalMergeTool {
562 program: "foo bar",
563 diff_args: [
564 "$left",
565 "$right",
566 ],
567 diff_expected_exit_codes: [
568 0,
569 ],
570 diff_invocation_mode: Dir,
571 diff_do_chdir: true,
572 edit_args: [
573 "--edit",
574 "args",
575 "$left",
576 "$right",
577 ],
578 merge_args: [],
579 merge_conflict_exit_codes: [],
580 merge_tool_edits_conflict_markers: false,
581 conflict_marker_style: None,
582 },
583 )
584 "#);
585 }
586
587 #[test]
588 fn test_get_diff_editor_from_settings() {
589 let get = |text| {
590 let config = config_from_string(text);
591 let ui = Ui::with_config(&config).unwrap();
592 let settings = UserSettings::from_config(config).unwrap();
593 DiffEditor::from_settings(
594 &ui,
595 &settings,
596 GitIgnoreFile::empty(),
597 ConflictMarkerStyle::Diff,
598 )
599 .map(|editor| editor.tool)
600 };
601
602 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
604
605 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = "my-diff""#).unwrap(), @r#"
607 External(
608 ExternalMergeTool {
609 program: "my-diff",
610 diff_args: [
611 "$left",
612 "$right",
613 ],
614 diff_expected_exit_codes: [
615 0,
616 ],
617 diff_invocation_mode: Dir,
618 diff_do_chdir: true,
619 edit_args: [
620 "$left",
621 "$right",
622 ],
623 merge_args: [],
624 merge_conflict_exit_codes: [],
625 merge_tool_edits_conflict_markers: false,
626 conflict_marker_style: None,
627 },
628 )
629 "#);
630
631 insta::assert_debug_snapshot!(
633 get(r#"ui.diff-editor = "my-diff -l $left -r $right""#).unwrap(), @r#"
634 External(
635 ExternalMergeTool {
636 program: "my-diff",
637 diff_args: [
638 "$left",
639 "$right",
640 ],
641 diff_expected_exit_codes: [
642 0,
643 ],
644 diff_invocation_mode: Dir,
645 diff_do_chdir: true,
646 edit_args: [
647 "-l",
648 "$left",
649 "-r",
650 "$right",
651 ],
652 merge_args: [],
653 merge_conflict_exit_codes: [],
654 merge_tool_edits_conflict_markers: false,
655 conflict_marker_style: None,
656 },
657 )
658 "#);
659
660 insta::assert_debug_snapshot!(
662 get(r#"ui.diff-editor = ["my-diff", "--diff", "$left", "$right"]"#).unwrap(), @r#"
663 External(
664 ExternalMergeTool {
665 program: "my-diff",
666 diff_args: [
667 "$left",
668 "$right",
669 ],
670 diff_expected_exit_codes: [
671 0,
672 ],
673 diff_invocation_mode: Dir,
674 diff_do_chdir: true,
675 edit_args: [
676 "--diff",
677 "$left",
678 "$right",
679 ],
680 merge_args: [],
681 merge_conflict_exit_codes: [],
682 merge_tool_edits_conflict_markers: false,
683 conflict_marker_style: None,
684 },
685 )
686 "#);
687
688 insta::assert_debug_snapshot!(get(
690 r#"
691 ui.diff-editor = "foo bar"
692 [merge-tools."foo bar"]
693 edit-args = ["--edit", "args", "$left", "$right"]
694 diff-args = [] # Should not cause an error, since we're getting the diff *editor*
695 "#,
696 ).unwrap(), @r#"
697 External(
698 ExternalMergeTool {
699 program: "foo bar",
700 diff_args: [],
701 diff_expected_exit_codes: [
702 0,
703 ],
704 diff_invocation_mode: Dir,
705 diff_do_chdir: true,
706 edit_args: [
707 "--edit",
708 "args",
709 "$left",
710 "$right",
711 ],
712 merge_args: [],
713 merge_conflict_exit_codes: [],
714 merge_tool_edits_conflict_markers: false,
715 conflict_marker_style: None,
716 },
717 )
718 "#);
719
720 insta::assert_debug_snapshot!(get(
722 r#"
723 ui.diff-editor = "my-diff"
724 [merge-tools.my-diff]
725 program = "MyDiff"
726 "#,
727 ).unwrap(), @r#"
728 External(
729 ExternalMergeTool {
730 program: "MyDiff",
731 diff_args: [
732 "$left",
733 "$right",
734 ],
735 diff_expected_exit_codes: [
736 0,
737 ],
738 diff_invocation_mode: Dir,
739 diff_do_chdir: true,
740 edit_args: [
741 "$left",
742 "$right",
743 ],
744 merge_args: [],
745 merge_conflict_exit_codes: [],
746 merge_tool_edits_conflict_markers: false,
747 conflict_marker_style: None,
748 },
749 )
750 "#);
751
752 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = ["meld"]"#).unwrap(), @r#"
754 External(
755 ExternalMergeTool {
756 program: "meld",
757 diff_args: [
758 "$left",
759 "$right",
760 ],
761 diff_expected_exit_codes: [
762 0,
763 ],
764 diff_invocation_mode: Dir,
765 diff_do_chdir: true,
766 edit_args: [
767 "$left",
768 "$right",
769 ],
770 merge_args: [],
771 merge_conflict_exit_codes: [],
772 merge_tool_edits_conflict_markers: false,
773 conflict_marker_style: None,
774 },
775 )
776 "#);
777
778 assert!(get(r#"ui.diff-editor.k = 0"#).is_err());
780
781 insta::assert_debug_snapshot!(get(
783 r#"
784 ui.diff-editor = "my-diff"
785 [merge-tools.my-diff]
786 program = "MyDiff"
787 edit-args = []
788 "#,
789 ), @r#"
790 Err(
791 EditArgsNotConfigured {
792 tool_name: "my-diff",
793 },
794 )
795 "#);
796 }
797
798 #[test]
799 fn test_get_merge_editor_with_name() {
800 let get = |name, config_text| {
801 let config = config_from_string(config_text);
802 let settings = UserSettings::from_config(config).unwrap();
803 let path_converter = RepoPathUiConverter::Fs {
804 cwd: "".into(),
805 base: "".into(),
806 };
807 MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff)
808 .map(|editor| editor.tool)
809 };
810
811 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
812
813 insta::assert_debug_snapshot!(get("my diff", "").unwrap_err(), @r#"
815 MergeArgsNotConfigured {
816 tool_name: "my diff",
817 }
818 "#);
819
820 insta::assert_debug_snapshot!(get(
822 "foo bar", r#"
823 [merge-tools."foo bar"]
824 merge-args = ["$base", "$left", "$right", "$output"]
825 "#,
826 ).unwrap(), @r#"
827 External(
828 ExternalMergeTool {
829 program: "foo bar",
830 diff_args: [
831 "$left",
832 "$right",
833 ],
834 diff_expected_exit_codes: [
835 0,
836 ],
837 diff_invocation_mode: Dir,
838 diff_do_chdir: true,
839 edit_args: [
840 "$left",
841 "$right",
842 ],
843 merge_args: [
844 "$base",
845 "$left",
846 "$right",
847 "$output",
848 ],
849 merge_conflict_exit_codes: [],
850 merge_tool_edits_conflict_markers: false,
851 conflict_marker_style: None,
852 },
853 )
854 "#);
855 }
856
857 #[test]
858 fn test_get_merge_editor_from_settings() {
859 let get = |text| {
860 let config = config_from_string(text);
861 let ui = Ui::with_config(&config).unwrap();
862 let settings = UserSettings::from_config(config).unwrap();
863 let path_converter = RepoPathUiConverter::Fs {
864 cwd: "".into(),
865 base: "".into(),
866 };
867 MergeEditor::from_settings(&ui, &settings, path_converter, ConflictMarkerStyle::Diff)
868 .map(|editor| editor.tool)
869 };
870
871 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
873
874 insta::assert_debug_snapshot!(get(r#"ui.merge-editor = "my-merge""#).unwrap_err(), @r#"
876 MergeArgsNotConfigured {
877 tool_name: "my-merge",
878 }
879 "#);
880
881 insta::assert_debug_snapshot!(
883 get(r#"ui.merge-editor = "my-merge $left $base $right $output""#).unwrap(), @r#"
884 External(
885 ExternalMergeTool {
886 program: "my-merge",
887 diff_args: [
888 "$left",
889 "$right",
890 ],
891 diff_expected_exit_codes: [
892 0,
893 ],
894 diff_invocation_mode: Dir,
895 diff_do_chdir: true,
896 edit_args: [
897 "$left",
898 "$right",
899 ],
900 merge_args: [
901 "$left",
902 "$base",
903 "$right",
904 "$output",
905 ],
906 merge_conflict_exit_codes: [],
907 merge_tool_edits_conflict_markers: false,
908 conflict_marker_style: None,
909 },
910 )
911 "#);
912
913 insta::assert_debug_snapshot!(
915 get(
916 r#"ui.merge-editor = ["my-merge", "$left", "$base", "$right", "$output"]"#,
917 ).unwrap(), @r#"
918 External(
919 ExternalMergeTool {
920 program: "my-merge",
921 diff_args: [
922 "$left",
923 "$right",
924 ],
925 diff_expected_exit_codes: [
926 0,
927 ],
928 diff_invocation_mode: Dir,
929 diff_do_chdir: true,
930 edit_args: [
931 "$left",
932 "$right",
933 ],
934 merge_args: [
935 "$left",
936 "$base",
937 "$right",
938 "$output",
939 ],
940 merge_conflict_exit_codes: [],
941 merge_tool_edits_conflict_markers: false,
942 conflict_marker_style: None,
943 },
944 )
945 "#);
946
947 insta::assert_debug_snapshot!(get(
949 r#"
950 ui.merge-editor = "foo bar"
951 [merge-tools."foo bar"]
952 merge-args = ["$base", "$left", "$right", "$output"]
953 "#,
954 ).unwrap(), @r#"
955 External(
956 ExternalMergeTool {
957 program: "foo bar",
958 diff_args: [
959 "$left",
960 "$right",
961 ],
962 diff_expected_exit_codes: [
963 0,
964 ],
965 diff_invocation_mode: Dir,
966 diff_do_chdir: true,
967 edit_args: [
968 "$left",
969 "$right",
970 ],
971 merge_args: [
972 "$base",
973 "$left",
974 "$right",
975 "$output",
976 ],
977 merge_conflict_exit_codes: [],
978 merge_tool_edits_conflict_markers: false,
979 conflict_marker_style: None,
980 },
981 )
982 "#);
983
984 insta::assert_debug_snapshot!(
986 get(r#"ui.merge-editor = ["meld"]"#).unwrap_err(), @r#"
987 MergeArgsNotConfigured {
988 tool_name: "meld",
989 }
990 "#);
991
992 assert!(get(r#"ui.merge-editor.k = 0"#).is_err());
994 }
995}