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