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 edit_invocation_mode: Dir,
546 merge_args: [],
547 merge_conflict_exit_codes: [],
548 merge_tool_edits_conflict_markers: false,
549 conflict_marker_style: None,
550 },
551 )
552 "#);
553
554 insta::assert_debug_snapshot!(get(
556 "foo bar", r#"
557 [merge-tools."foo bar"]
558 edit-args = ["--edit", "args", "$left", "$right"]
559 "#,
560 ).unwrap(), @r#"
561 External(
562 ExternalMergeTool {
563 program: "foo bar",
564 diff_args: [
565 "$left",
566 "$right",
567 ],
568 diff_expected_exit_codes: [
569 0,
570 ],
571 diff_invocation_mode: Dir,
572 diff_do_chdir: true,
573 edit_args: [
574 "--edit",
575 "args",
576 "$left",
577 "$right",
578 ],
579 edit_invocation_mode: Dir,
580 merge_args: [],
581 merge_conflict_exit_codes: [],
582 merge_tool_edits_conflict_markers: false,
583 conflict_marker_style: None,
584 },
585 )
586 "#);
587 }
588
589 #[test]
590 fn test_get_diff_editor_from_settings() {
591 let get = |text| {
592 let config = config_from_string(text);
593 let ui = Ui::with_config(&config).unwrap();
594 let settings = UserSettings::from_config(config).unwrap();
595 DiffEditor::from_settings(
596 &ui,
597 &settings,
598 GitIgnoreFile::empty(),
599 ConflictMarkerStyle::Diff,
600 )
601 .map(|editor| editor.tool)
602 };
603
604 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
606
607 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = "my-diff""#).unwrap(), @r#"
609 External(
610 ExternalMergeTool {
611 program: "my-diff",
612 diff_args: [
613 "$left",
614 "$right",
615 ],
616 diff_expected_exit_codes: [
617 0,
618 ],
619 diff_invocation_mode: Dir,
620 diff_do_chdir: true,
621 edit_args: [
622 "$left",
623 "$right",
624 ],
625 edit_invocation_mode: Dir,
626 merge_args: [],
627 merge_conflict_exit_codes: [],
628 merge_tool_edits_conflict_markers: false,
629 conflict_marker_style: None,
630 },
631 )
632 "#);
633
634 insta::assert_debug_snapshot!(
636 get(r#"ui.diff-editor = "my-diff -l $left -r $right""#).unwrap(), @r#"
637 External(
638 ExternalMergeTool {
639 program: "my-diff",
640 diff_args: [
641 "$left",
642 "$right",
643 ],
644 diff_expected_exit_codes: [
645 0,
646 ],
647 diff_invocation_mode: Dir,
648 diff_do_chdir: true,
649 edit_args: [
650 "-l",
651 "$left",
652 "-r",
653 "$right",
654 ],
655 edit_invocation_mode: Dir,
656 merge_args: [],
657 merge_conflict_exit_codes: [],
658 merge_tool_edits_conflict_markers: false,
659 conflict_marker_style: None,
660 },
661 )
662 "#);
663
664 insta::assert_debug_snapshot!(
666 get(r#"ui.diff-editor = ["my-diff", "--diff", "$left", "$right"]"#).unwrap(), @r#"
667 External(
668 ExternalMergeTool {
669 program: "my-diff",
670 diff_args: [
671 "$left",
672 "$right",
673 ],
674 diff_expected_exit_codes: [
675 0,
676 ],
677 diff_invocation_mode: Dir,
678 diff_do_chdir: true,
679 edit_args: [
680 "--diff",
681 "$left",
682 "$right",
683 ],
684 edit_invocation_mode: Dir,
685 merge_args: [],
686 merge_conflict_exit_codes: [],
687 merge_tool_edits_conflict_markers: false,
688 conflict_marker_style: None,
689 },
690 )
691 "#);
692
693 insta::assert_debug_snapshot!(get(
695 r#"
696 ui.diff-editor = "foo bar"
697 [merge-tools."foo bar"]
698 edit-args = ["--edit", "args", "$left", "$right"]
699 diff-args = [] # Should not cause an error, since we're getting the diff *editor*
700 "#,
701 ).unwrap(), @r#"
702 External(
703 ExternalMergeTool {
704 program: "foo bar",
705 diff_args: [],
706 diff_expected_exit_codes: [
707 0,
708 ],
709 diff_invocation_mode: Dir,
710 diff_do_chdir: true,
711 edit_args: [
712 "--edit",
713 "args",
714 "$left",
715 "$right",
716 ],
717 edit_invocation_mode: Dir,
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(
728 r#"
729 ui.diff-editor = "my-diff"
730 [merge-tools.my-diff]
731 program = "MyDiff"
732 "#,
733 ).unwrap(), @r#"
734 External(
735 ExternalMergeTool {
736 program: "MyDiff",
737 diff_args: [
738 "$left",
739 "$right",
740 ],
741 diff_expected_exit_codes: [
742 0,
743 ],
744 diff_invocation_mode: Dir,
745 diff_do_chdir: true,
746 edit_args: [
747 "$left",
748 "$right",
749 ],
750 edit_invocation_mode: Dir,
751 merge_args: [],
752 merge_conflict_exit_codes: [],
753 merge_tool_edits_conflict_markers: false,
754 conflict_marker_style: None,
755 },
756 )
757 "#);
758
759 insta::assert_debug_snapshot!(get(r#"ui.diff-editor = ["meld"]"#).unwrap(), @r#"
761 External(
762 ExternalMergeTool {
763 program: "meld",
764 diff_args: [
765 "$left",
766 "$right",
767 ],
768 diff_expected_exit_codes: [
769 0,
770 ],
771 diff_invocation_mode: Dir,
772 diff_do_chdir: true,
773 edit_args: [
774 "$left",
775 "$right",
776 ],
777 edit_invocation_mode: Dir,
778 merge_args: [],
779 merge_conflict_exit_codes: [],
780 merge_tool_edits_conflict_markers: false,
781 conflict_marker_style: None,
782 },
783 )
784 "#);
785
786 assert!(get(r#"ui.diff-editor.k = 0"#).is_err());
788
789 insta::assert_debug_snapshot!(get(
791 r#"
792 ui.diff-editor = "my-diff"
793 [merge-tools.my-diff]
794 program = "MyDiff"
795 edit-args = []
796 "#,
797 ), @r#"
798 Err(
799 EditArgsNotConfigured {
800 tool_name: "my-diff",
801 },
802 )
803 "#);
804 }
805
806 #[test]
807 fn test_get_merge_editor_with_name() {
808 let get = |name, config_text| {
809 let config = config_from_string(config_text);
810 let settings = UserSettings::from_config(config).unwrap();
811 let path_converter = RepoPathUiConverter::Fs {
812 cwd: "".into(),
813 base: "".into(),
814 };
815 MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff)
816 .map(|editor| editor.tool)
817 };
818
819 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
820
821 insta::assert_debug_snapshot!(get("my diff", "").unwrap_err(), @r#"
823 MergeArgsNotConfigured {
824 tool_name: "my diff",
825 }
826 "#);
827
828 insta::assert_debug_snapshot!(get(
830 "foo bar", r#"
831 [merge-tools."foo bar"]
832 merge-args = ["$base", "$left", "$right", "$output"]
833 "#,
834 ).unwrap(), @r#"
835 External(
836 ExternalMergeTool {
837 program: "foo bar",
838 diff_args: [
839 "$left",
840 "$right",
841 ],
842 diff_expected_exit_codes: [
843 0,
844 ],
845 diff_invocation_mode: Dir,
846 diff_do_chdir: true,
847 edit_args: [
848 "$left",
849 "$right",
850 ],
851 edit_invocation_mode: Dir,
852 merge_args: [
853 "$base",
854 "$left",
855 "$right",
856 "$output",
857 ],
858 merge_conflict_exit_codes: [],
859 merge_tool_edits_conflict_markers: false,
860 conflict_marker_style: None,
861 },
862 )
863 "#);
864 }
865
866 #[test]
867 fn test_get_merge_editor_from_settings() {
868 let get = |text| {
869 let config = config_from_string(text);
870 let ui = Ui::with_config(&config).unwrap();
871 let settings = UserSettings::from_config(config).unwrap();
872 let path_converter = RepoPathUiConverter::Fs {
873 cwd: "".into(),
874 base: "".into(),
875 };
876 MergeEditor::from_settings(&ui, &settings, path_converter, ConflictMarkerStyle::Diff)
877 .map(|editor| editor.tool)
878 };
879
880 insta::assert_debug_snapshot!(get("").unwrap(), @"Builtin");
882
883 insta::assert_debug_snapshot!(get(r#"ui.merge-editor = "my-merge""#).unwrap_err(), @r#"
885 MergeArgsNotConfigured {
886 tool_name: "my-merge",
887 }
888 "#);
889
890 insta::assert_debug_snapshot!(
892 get(r#"ui.merge-editor = "my-merge $left $base $right $output""#).unwrap(), @r#"
893 External(
894 ExternalMergeTool {
895 program: "my-merge",
896 diff_args: [
897 "$left",
898 "$right",
899 ],
900 diff_expected_exit_codes: [
901 0,
902 ],
903 diff_invocation_mode: Dir,
904 diff_do_chdir: true,
905 edit_args: [
906 "$left",
907 "$right",
908 ],
909 edit_invocation_mode: Dir,
910 merge_args: [
911 "$left",
912 "$base",
913 "$right",
914 "$output",
915 ],
916 merge_conflict_exit_codes: [],
917 merge_tool_edits_conflict_markers: false,
918 conflict_marker_style: None,
919 },
920 )
921 "#);
922
923 insta::assert_debug_snapshot!(
925 get(
926 r#"ui.merge-editor = ["my-merge", "$left", "$base", "$right", "$output"]"#,
927 ).unwrap(), @r#"
928 External(
929 ExternalMergeTool {
930 program: "my-merge",
931 diff_args: [
932 "$left",
933 "$right",
934 ],
935 diff_expected_exit_codes: [
936 0,
937 ],
938 diff_invocation_mode: Dir,
939 diff_do_chdir: true,
940 edit_args: [
941 "$left",
942 "$right",
943 ],
944 edit_invocation_mode: Dir,
945 merge_args: [
946 "$left",
947 "$base",
948 "$right",
949 "$output",
950 ],
951 merge_conflict_exit_codes: [],
952 merge_tool_edits_conflict_markers: false,
953 conflict_marker_style: None,
954 },
955 )
956 "#);
957
958 insta::assert_debug_snapshot!(get(
960 r#"
961 ui.merge-editor = "foo bar"
962 [merge-tools."foo bar"]
963 merge-args = ["$base", "$left", "$right", "$output"]
964 "#,
965 ).unwrap(), @r#"
966 External(
967 ExternalMergeTool {
968 program: "foo bar",
969 diff_args: [
970 "$left",
971 "$right",
972 ],
973 diff_expected_exit_codes: [
974 0,
975 ],
976 diff_invocation_mode: Dir,
977 diff_do_chdir: true,
978 edit_args: [
979 "$left",
980 "$right",
981 ],
982 edit_invocation_mode: Dir,
983 merge_args: [
984 "$base",
985 "$left",
986 "$right",
987 "$output",
988 ],
989 merge_conflict_exit_codes: [],
990 merge_tool_edits_conflict_markers: false,
991 conflict_marker_style: None,
992 },
993 )
994 "#);
995
996 insta::assert_debug_snapshot!(
998 get(r#"ui.merge-editor = ["meld"]"#).unwrap_err(), @r#"
999 MergeArgsNotConfigured {
1000 tool_name: "meld",
1001 }
1002 "#);
1003
1004 assert!(get(r#"ui.merge-editor.k = 0"#).is_err());
1006 }
1007}