From 7df0f16fe032e675e8942e368bb3dbcab31f88d7 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Tue, 17 Dec 2024 09:33:19 -0600 Subject: [PATCH] resolve: try to resolve all conflicted files in fileset If many files are conflicted, it would be nice to be able to resolve all conflicts at once without having to run `jj resolve` multiple times. This is especially nice for merge tools which try to automatically resolve conflicts without user input, but it is also good for regular merge editors like VS Code. This change makes the behavior of `jj resolve` more consistent with other commands which accept filesets since it will use the entire fileset instead of picking an arbitrary file from the fileset. Since we don't support passing directories to merge tools yet, the current implementation just calls the merge tool repeatedly in a loop until every file is resolved, or until an error occurs. If an error occurs after successfully resolving at least one file, the transaction is committed with all of the successful changes before returning the error. This means the user can just close the editor at any point to cancel resolution on all remaining files. --- CHANGELOG.md | 4 + cli/src/cli_util.rs | 14 +- cli/src/command_error.rs | 13 +- cli/src/commands/resolve.rs | 32 +++-- cli/src/merge_tools/builtin.rs | 12 +- cli/src/merge_tools/external.rs | 62 +++++++-- cli/src/merge_tools/mod.rs | 56 ++++++-- cli/tests/cli-reference@.md.snap | 10 +- cli/tests/test_resolve_command.rs | 220 +++++++++++++++++++++++++----- lib/src/repo_path.rs | 2 +- 10 files changed, 343 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa5522af5..72b55ccba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Inner items of inline tables are no longer merged across configuration files. +* `jj resolve` will now attempt to resolve all conflicted files instead of + resolving the first conflicted file. To resolve a single file, pass a file + path to `jj resolve`. + ### Deprecations ### New features diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index acb8f16c54..25d38c29fb 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1484,9 +1484,19 @@ to the current parents may contain changes from multiple commits. ) -> Result { let conflict_marker_style = self.env.conflict_marker_style(); if let Some(name) = tool_name { - MergeEditor::with_name(name, self.settings(), conflict_marker_style) + MergeEditor::with_name( + name, + self.settings(), + self.path_converter().clone(), + conflict_marker_style, + ) } else { - MergeEditor::from_settings(ui, self.settings(), conflict_marker_style) + MergeEditor::from_settings( + ui, + self.settings(), + self.path_converter().clone(), + conflict_marker_style, + ) } } diff --git a/cli/src/command_error.rs b/cli/src/command_error.rs index 652df549ed..cf0edae390 100644 --- a/cli/src/command_error.rs +++ b/cli/src/command_error.rs @@ -66,6 +66,7 @@ use crate::formatter::Formatter; use crate::merge_tools::ConflictResolveError; use crate::merge_tools::DiffEditError; use crate::merge_tools::MergeToolConfigError; +use crate::merge_tools::MergeToolPartialResolutionError; use crate::revset_util::UserRevsetEvaluationError; use crate::template_parser::TemplateParseError; use crate::template_parser::TemplateParseErrorKind; @@ -418,7 +419,17 @@ impl From for CommandError { impl From for CommandError { fn from(err: ConflictResolveError) -> Self { - user_error_with_message("Failed to resolve conflicts", err) + match err { + ConflictResolveError::Backend(err) => err.into(), + ConflictResolveError::Io(err) => err.into(), + _ => user_error_with_message("Failed to resolve conflicts", err), + } + } +} + +impl From for CommandError { + fn from(err: MergeToolPartialResolutionError) -> Self { + user_error(err) } } diff --git a/cli/src/commands/resolve.rs b/cli/src/commands/resolve.rs index 79b5b04b83..cbde1ccc52 100644 --- a/cli/src/commands/resolve.rs +++ b/cli/src/commands/resolve.rs @@ -28,10 +28,13 @@ use crate::command_error::CommandError; use crate::complete; use crate::ui::Ui; -/// Resolve a conflicted file with an external merge tool +/// Resolve conflicted files with an external merge tool /// /// Only conflicts that can be resolved with a 3-way merge are supported. See -/// docs for merge tool configuration instructions. +/// docs for merge tool configuration instructions. External merge tools will be +/// invoked for each conflicted file one-by-one until all conflicts are +/// resolved. To stop resolving conflicts, exit the merge tool without making +/// any changes. /// /// Note that conflicts can also be resolved without using this command. You may /// edit the conflict markers in the conflicted file directly with a text @@ -52,7 +55,7 @@ pub(crate) struct ResolveArgs { add = ArgValueCandidates::new(complete::mutable_revisions), )] revision: RevisionArg, - /// Instead of resolving one conflict, list all the conflicts + /// Instead of resolving conflicts, list all the conflicts // TODO: Also have a `--summary` option. `--list` currently acts like // `diff --summary`, but should be more verbose. #[arg(long, short)] @@ -60,10 +63,8 @@ pub(crate) struct ResolveArgs { /// Specify 3-way merge tool to be used #[arg(long, conflicts_with = "list", value_name = "NAME")] tool: Option, - /// Restrict to these paths when searching for a conflict to resolve. We - /// will attempt to resolve the first conflict we can find. You can use - /// the `--list` argument to find paths to use here. - // TODO: Find the conflict we can resolve even if it's not the first one. + /// Only resolve conflicts in these paths. You can use the `--list` argument + /// to find paths to use here. #[arg( value_name = "FILESETS", value_hint = clap::ValueHint::AnyPath, @@ -103,16 +104,15 @@ pub(crate) fn cmd_resolve( ); }; - let (repo_path, _) = conflicts.first().unwrap(); + let repo_paths = conflicts + .iter() + .map(|(path, _)| path.as_ref()) + .collect_vec(); workspace_command.check_rewritable([commit.id()])?; let merge_editor = workspace_command.merge_editor(ui, args.tool.as_deref())?; - writeln!( - ui.status(), - "Resolving conflicts in: {}", - workspace_command.format_file_path(repo_path) - )?; let mut tx = workspace_command.start_transaction(); - let new_tree_id = merge_editor.edit_file(&tree, repo_path)?; + let (new_tree_id, partial_resolution_error) = + merge_editor.edit_files(ui, &tree, &repo_paths)?; let new_commit = tx .repo_mut() .rewrite_commit(&commit) @@ -139,5 +139,9 @@ pub(crate) fn cmd_resolve( } } } + + if let Some(err) = partial_resolution_error { + return Err(err.into()); + } Ok(()) } diff --git a/cli/src/merge_tools/builtin.rs b/cli/src/merge_tools/builtin.rs index 9f62b0af43..b545965d6a 100644 --- a/cli/src/merge_tools/builtin.rs +++ b/cli/src/merge_tools/builtin.rs @@ -658,26 +658,28 @@ fn make_merge_file( pub fn edit_merge_builtin( tree: &MergedTree, - merge_tool_file: &MergeToolFile, + merge_tool_files: &[MergeToolFile], ) -> Result { let mut input = scm_record::helpers::CrosstermInput; let recorder = scm_record::Recorder::new( scm_record::RecordState { is_read_only: false, - files: vec![make_merge_file(merge_tool_file)?], + files: merge_tool_files.iter().map(make_merge_file).try_collect()?, commits: Default::default(), }, &mut input, ); let state = recorder.run()?; - let file = state.files.into_iter().exactly_one().unwrap(); apply_diff_builtin( tree.store(), tree, tree, - vec![merge_tool_file.repo_path.clone()], - &[file], + merge_tool_files + .iter() + .map(|file| file.repo_path.clone()) + .collect_vec(), + &state.files, ) .map_err(BuiltinToolError::BackendError) } diff --git a/cli/src/merge_tools/external.rs b/cli/src/merge_tools/external.rs index 61b2177f92..4c2fd8d303 100644 --- a/cli/src/merge_tools/external.rs +++ b/cli/src/merge_tools/external.rs @@ -20,6 +20,8 @@ use jj_lib::matchers::Matcher; use jj_lib::merge::Merge; use jj_lib::merged_tree::MergedTree; use jj_lib::merged_tree::MergedTreeBuilder; +use jj_lib::repo_path::RepoPathUiConverter; +use jj_lib::store::Store; use jj_lib::working_copy::CheckoutOptions; use pollster::FutureExt; use thiserror::Error; @@ -33,6 +35,7 @@ use super::ConflictResolveError; use super::DiffEditError; use super::DiffGenerateError; use super::MergeToolFile; +use super::MergeToolPartialResolutionError; use crate::config::find_all_variables; use crate::config::interpolate_variables; use crate::config::CommandNameAndArgs; @@ -171,12 +174,13 @@ pub enum ExternalToolError { Io(#[source] std::io::Error), } -pub fn run_mergetool_external( +fn run_mergetool_external_single_file( editor: &ExternalMergeTool, - tree: &MergedTree, + store: &Store, merge_tool_file: &MergeToolFile, default_conflict_marker_style: ConflictMarkerStyle, -) -> Result { + tree_builder: &mut MergedTreeBuilder, +) -> Result<(), ConflictResolveError> { let MergeToolFile { repo_path, conflict, @@ -276,7 +280,7 @@ pub fn run_mergetool_external( let new_file_ids = if editor.merge_tool_edits_conflict_markers || exit_status_implies_conflict { conflicts::update_from_content( file_merge, - tree.store(), + store, repo_path, output_file_contents.as_slice(), conflict_marker_style, @@ -284,8 +288,7 @@ pub fn run_mergetool_external( ) .block_on()? } else { - let new_file_id = tree - .store() + let new_file_id = store .write_file(repo_path, &mut output_file_contents.as_slice()) .block_on()?; Merge::normal(new_file_id) @@ -313,10 +316,53 @@ pub fn run_mergetool_external( }), Err(new_file_ids) => conflict.with_new_file_ids(&new_file_ids), }; - let mut tree_builder = MergedTreeBuilder::new(tree.id()); tree_builder.set_or_remove(repo_path.to_owned(), new_tree_value); + Ok(()) +} + +pub fn run_mergetool_external( + ui: &Ui, + path_converter: &RepoPathUiConverter, + editor: &ExternalMergeTool, + tree: &MergedTree, + merge_tool_files: &[MergeToolFile], + default_conflict_marker_style: ConflictMarkerStyle, +) -> Result<(MergedTreeId, Option), ConflictResolveError> { + // TODO: add support for "dir" invocation mode, similar to the + // "diff-invocation-mode" config option for diffs + let mut tree_builder = MergedTreeBuilder::new(tree.id()); + let mut partial_resolution_error = None; + for (i, merge_tool_file) in merge_tool_files.iter().enumerate() { + writeln!( + ui.status(), + "Resolving conflicts in: {}", + path_converter.format_file_path(&merge_tool_file.repo_path) + )?; + match run_mergetool_external_single_file( + editor, + tree.store(), + merge_tool_file, + default_conflict_marker_style, + &mut tree_builder, + ) { + Ok(()) => {} + Err(err) if i == 0 => { + // If the first resolution fails, just return the error normally + return Err(err); + } + Err(err) => { + // Some conflicts were already resolved, so we should return an error with the + // partially-resolved tree so that the caller can save the resolved files. + partial_resolution_error = Some(MergeToolPartialResolutionError { + source: err, + resolved_count: i, + }); + break; + } + } + } let new_tree = tree_builder.write_tree(tree.store())?; - Ok(new_tree) + Ok((new_tree, partial_resolution_error)) } pub fn edit_diff_external( diff --git a/cli/src/merge_tools/mod.rs b/cli/src/merge_tools/mod.rs index c61b67c0f3..d017bf90bb 100644 --- a/cli/src/merge_tools/mod.rs +++ b/cli/src/merge_tools/mod.rs @@ -19,6 +19,7 @@ mod external; use std::sync::Arc; use bstr::BString; +use itertools::Itertools; use jj_lib::backend::FileId; use jj_lib::backend::MergedTreeId; use jj_lib::config::ConfigGetError; @@ -34,6 +35,7 @@ use jj_lib::merged_tree::MergedTree; use jj_lib::repo_path::InvalidRepoPathError; use jj_lib::repo_path::RepoPath; use jj_lib::repo_path::RepoPathBuf; +use jj_lib::repo_path::RepoPathUiConverter; use jj_lib::settings::UserSettings; use jj_lib::working_copy::SnapshotError; use pollster::FutureExt; @@ -101,8 +103,17 @@ pub enum ConflictResolveError { see the exact invocation)." )] EmptyOrUnchanged, - #[error("Backend error")] + #[error(transparent)] Backend(#[from] jj_lib::backend::BackendError), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +#[error("Stopped due to error after resolving {resolved_count} conflicts")] +pub struct MergeToolPartialResolutionError { + pub source: ConflictResolveError, + pub resolved_count: usize, } #[derive(Debug, Error)] @@ -313,6 +324,7 @@ impl MergeToolFile { #[derive(Clone, Debug)] pub struct MergeEditor { tool: MergeTool, + path_converter: RepoPathUiConverter, conflict_marker_style: ConflictMarkerStyle, } @@ -322,17 +334,19 @@ impl MergeEditor { pub fn with_name( name: &str, settings: &UserSettings, + path_converter: RepoPathUiConverter, conflict_marker_style: ConflictMarkerStyle, ) -> Result { let tool = get_tool_config(settings, name)? .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_program(name))); - Self::new_inner(name, tool, conflict_marker_style) + Self::new_inner(name, tool, path_converter, conflict_marker_style) } /// Loads the default 3-way merge editor from the settings. pub fn from_settings( ui: &Ui, settings: &UserSettings, + path_converter: RepoPathUiConverter, conflict_marker_style: ConflictMarkerStyle, ) -> Result { let args = editor_args_from_settings(ui, settings, "ui.merge-editor")?; @@ -342,12 +356,13 @@ impl MergeEditor { None } .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_merge_args(&args))); - Self::new_inner(&args, tool, conflict_marker_style) + Self::new_inner(&args, tool, path_converter, conflict_marker_style) } fn new_inner( name: impl ToString, tool: MergeTool, + path_converter: RepoPathUiConverter, conflict_marker_style: ConflictMarkerStyle, ) -> Result { if matches!(&tool, MergeTool::External(mergetool) if mergetool.merge_args.is_empty()) { @@ -357,27 +372,34 @@ impl MergeEditor { } Ok(MergeEditor { tool, + path_converter, conflict_marker_style, }) } - /// Starts a merge editor for the specified file. - pub fn edit_file( + /// Starts a merge editor for the specified files. + pub fn edit_files( &self, + ui: &Ui, tree: &MergedTree, - repo_path: &RepoPath, - ) -> Result { - let merge_tool_file = MergeToolFile::from_tree_and_path(tree, repo_path)?; + repo_paths: &[&RepoPath], + ) -> Result<(MergedTreeId, Option), ConflictResolveError> { + let merge_tool_files: Vec = repo_paths + .iter() + .map(|&repo_path| MergeToolFile::from_tree_and_path(tree, repo_path)) + .try_collect()?; match &self.tool { MergeTool::Builtin => { - let tree_id = edit_merge_builtin(tree, &merge_tool_file).map_err(Box::new)?; - Ok(tree_id) + let tree_id = edit_merge_builtin(tree, &merge_tool_files).map_err(Box::new)?; + Ok((tree_id, None)) } MergeTool::External(editor) => external::run_mergetool_external( + ui, + &self.path_converter, editor, tree, - &merge_tool_file, + &merge_tool_files, self.conflict_marker_style, ), } @@ -670,7 +692,11 @@ mod tests { let get = |name, config_text| { let config = config_from_string(config_text); let settings = UserSettings::from_config(config).unwrap(); - MergeEditor::with_name(name, &settings, ConflictMarkerStyle::Diff) + let path_converter = RepoPathUiConverter::Fs { + cwd: "".into(), + base: "".into(), + }; + MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff) .map(|editor| editor.tool) }; @@ -725,7 +751,11 @@ mod tests { let config = config_from_string(text); let ui = Ui::with_config(&config).unwrap(); let settings = UserSettings::from_config(config).unwrap(); - MergeEditor::from_settings(&ui, &settings, ConflictMarkerStyle::Diff) + let path_converter = RepoPathUiConverter::Fs { + cwd: "".into(), + base: "".into(), + }; + MergeEditor::from_settings(&ui, &settings, path_converter, ConflictMarkerStyle::Diff) .map(|editor| editor.tool) }; diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 47a060f66c..0621078b4b 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -145,7 +145,7 @@ To get started, see the tutorial at https://jj-vcs.github.io/jj/latest/tutorial/ * `parallelize` — Parallelize revisions by making them siblings * `prev` — Change the working copy revision relative to the parent revision * `rebase` — Move revisions to different parent(s) -* `resolve` — Resolve a conflicted file with an external merge tool +* `resolve` — Resolve conflicted files with an external merge tool * `restore` — Restore paths from another revision * `root` — Show the current workspace root directory * `show` — Show commit description and changes in a revision @@ -1885,9 +1885,9 @@ commit. This is true in general; it is not specific to this command. ## `jj resolve` -Resolve a conflicted file with an external merge tool +Resolve conflicted files with an external merge tool -Only conflicts that can be resolved with a 3-way merge are supported. See docs for merge tool configuration instructions. +Only conflicts that can be resolved with a 3-way merge are supported. See docs for merge tool configuration instructions. External merge tools will be invoked for each conflicted file one-by-one until all conflicts are resolved. To stop resolving conflicts, exit the merge tool without making any changes. Note that conflicts can also be resolved without using this command. You may edit the conflict markers in the conflicted file directly with a text editor. @@ -1895,14 +1895,14 @@ Note that conflicts can also be resolved without using this command. You may edi ###### **Arguments:** -* `` — Restrict to these paths when searching for a conflict to resolve. We will attempt to resolve the first conflict we can find. You can use the `--list` argument to find paths to use here +* `` — Only resolve conflicts in these paths. You can use the `--list` argument to find paths to use here ###### **Options:** * `-r`, `--revision ` Default value: `@` -* `-l`, `--list` — Instead of resolving one conflict, list all the conflicts +* `-l`, `--list` — Instead of resolving conflicts, list all the conflicts * `--tool ` — Specify 3-way merge tool to be used diff --git a/cli/tests/test_resolve_command.rs b/cli/tests/test_resolve_command.rs index 7bae69d5bf..7c5d2fc513 100644 --- a/cli/tests/test_resolve_command.rs +++ b/cli/tests/test_resolve_command.rs @@ -672,7 +672,6 @@ fn test_too_many_parents() { let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]); insta::assert_snapshot!(error, @r###" Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message. - Resolving conflicts in: file Error: Failed to resolve conflicts Caused by: The conflict at "file" has 3 sides. At most 2 sides are supported. "###); @@ -880,7 +879,6 @@ fn test_file_vs_dir() { let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]); insta::assert_snapshot!(error, @r###" Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message. - Resolving conflicts in: file Error: Failed to resolve conflicts Caused by: Only conflicts that involve normal files (not symlinks, not executable, etc.) are supported. Conflict summary for "file": Conflict: @@ -937,7 +935,6 @@ fn test_description_with_dir_and_deletion() { let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]); insta::assert_snapshot!(error, @r###" Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message. - Resolving conflicts in: file Error: Failed to resolve conflicts Caused by: Only conflicts that involve normal files (not symlinks, not executable, etc.) are supported. Conflict summary for "file": Conflict: @@ -1495,43 +1492,22 @@ fn test_multiple_conflicts() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @""); - // For the rest of the test, we call `jj resolve` several times in a row to - // resolve each conflict in the order it chooses. + // Without a path, `jj resolve` should call the merge tool multiple times test_env.jj_cmd_ok(&repo_path, &["undo"]); insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", "--git"]), @""); std::fs::write( &editor_script, - "expect\n\0write\nfirst resolution for auto-chosen file\n", - ) - .unwrap(); - test_env.jj_cmd_ok(&repo_path, &["resolve"]); - insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", "--git"]), - @r###" - diff --git a/another_file b/another_file - index 0000000000..7903e1c1c7 100644 - --- a/another_file - +++ b/another_file - @@ -1,7 +1,1 @@ - -<<<<<<< Conflict 1 of 1 - -%%%%%%% Changes from base to side #1 - --second base - -+second a - -+++++++ Contents of side #2 - -second b - ->>>>>>> Conflict 1 of 1 ends - +first resolution for auto-chosen file - "###); - insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]), - @r###" - this_file_has_a_very_long_name_to_test_padding 2-sided conflict - "###); - std::fs::write( - &editor_script, - "expect\n\0write\nsecond resolution for auto-chosen file\n", + [ + "expect\n", + "write\nfirst resolution for auto-chosen file\n", + "next invocation\n", + "expect\n", + "write\nsecond resolution for auto-chosen file\n", + ] + .join("\0"), ) .unwrap(); - test_env.jj_cmd_ok(&repo_path, &["resolve"]); insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", "--git"]), @r###" @@ -1572,3 +1548,181 @@ fn test_multiple_conflicts() { Error: No conflicts found at this revision "###); } + +#[test] +fn test_multiple_conflicts_with_error() { + let mut test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // Create two conflicted files, and one non-conflicted file + create_commit( + &test_env, + &repo_path, + "base", + &[], + &[ + ("file1", "base1\n"), + ("file2", "base2\n"), + ("file3", "base3\n"), + ], + ); + create_commit( + &test_env, + &repo_path, + "a", + &["base"], + &[("file1", "a1\n"), ("file2", "a2\n")], + ); + create_commit( + &test_env, + &repo_path, + "b", + &["base"], + &[("file1", "b1\n"), ("file2", "b2\n")], + ); + create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]); + insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]), + @r#" + file1 2-sided conflict + file2 2-sided conflict + "#); + insta::assert_snapshot!( + std::fs::read_to_string(repo_path.join("file1")).unwrap(), + @r##" + <<<<<<< Conflict 1 of 1 + %%%%%%% Changes from base to side #1 + -base1 + +a1 + +++++++ Contents of side #2 + b1 + >>>>>>> Conflict 1 of 1 ends + "## + ); + insta::assert_snapshot!( + std::fs::read_to_string(repo_path.join("file2")).unwrap(), + @r##" + <<<<<<< Conflict 1 of 1 + %%%%%%% Changes from base to side #1 + -base2 + +a2 + +++++++ Contents of side #2 + b2 + >>>>>>> Conflict 1 of 1 ends + "## + ); + let editor_script = test_env.set_up_fake_editor(); + + // Test resolving one conflict, then exiting without resolving the second one + std::fs::write( + &editor_script, + ["write\nresolution1\n", "next invocation\n"].join("\0"), + ) + .unwrap(); + let stderr = test_env.jj_cmd_failure(&repo_path, &["resolve"]); + insta::assert_snapshot!(stderr.replace("exit code", "exit status"), @r#" + Resolving conflicts in: file1 + Resolving conflicts in: file2 + Working copy now at: vruxwmqv d2f3f858 conflict | (conflict) conflict + Parent commit : zsuskuln 9db7fdfb a | a + Parent commit : royxmykx d67e26e4 b | b + Added 0 files, modified 1 files, removed 0 files + There are unresolved conflicts at these paths: + file2 2-sided conflict + New conflicts appeared in these commits: + vruxwmqv d2f3f858 conflict | (conflict) conflict + To resolve the conflicts, start by updating to it: + jj new vruxwmqv + Then use `jj resolve`, or edit the conflict markers in the file directly. + Once the conflicts are resolved, you may want to inspect the result with `jj diff`. + Then run `jj squash` to move the resolution into the conflicted commit. + Error: Stopped due to error after resolving 1 conflicts + Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation). + "#); + insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", "--git"]), + @r##" + diff --git a/file1 b/file1 + index 0000000000..95cc18629d 100644 + --- a/file1 + +++ b/file1 + @@ -1,7 +1,1 @@ + -<<<<<<< Conflict 1 of 1 + -%%%%%%% Changes from base to side #1 + --base1 + -+a1 + -+++++++ Contents of side #2 + -b1 + ->>>>>>> Conflict 1 of 1 ends + +resolution1 + "##); + insta::assert_snapshot!( + test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]), + @"file2 2-sided conflict" + ); + + // Test resolving one conflict, then failing during the second resolution + test_env.jj_cmd_ok(&repo_path, &["undo"]); + std::fs::write( + &editor_script, + ["write\nresolution1\n", "next invocation\n", "fail"].join("\0"), + ) + .unwrap(); + let stderr = test_env.jj_cmd_failure(&repo_path, &["resolve"]); + insta::assert_snapshot!(stderr.replace("exit code", "exit status"), @r#" + Resolving conflicts in: file1 + Resolving conflicts in: file2 + Working copy now at: vruxwmqv 0a54e8ed conflict | (conflict) conflict + Parent commit : zsuskuln 9db7fdfb a | a + Parent commit : royxmykx d67e26e4 b | b + Added 0 files, modified 1 files, removed 0 files + There are unresolved conflicts at these paths: + file2 2-sided conflict + New conflicts appeared in these commits: + vruxwmqv 0a54e8ed conflict | (conflict) conflict + To resolve the conflicts, start by updating to it: + jj new vruxwmqv + Then use `jj resolve`, or edit the conflict markers in the file directly. + Once the conflicts are resolved, you may want to inspect the result with `jj diff`. + Then run `jj squash` to move the resolution into the conflicted commit. + Error: Stopped due to error after resolving 1 conflicts + Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation) + "#); + insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", "--git"]), + @r##" + diff --git a/file1 b/file1 + index 0000000000..95cc18629d 100644 + --- a/file1 + +++ b/file1 + @@ -1,7 +1,1 @@ + -<<<<<<< Conflict 1 of 1 + -%%%%%%% Changes from base to side #1 + --base1 + -+a1 + -+++++++ Contents of side #2 + -b1 + ->>>>>>> Conflict 1 of 1 ends + +resolution1 + "##); + insta::assert_snapshot!( + test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]), + @"file2 2-sided conflict" + ); + + // Test immediately failing to resolve any conflict + test_env.jj_cmd_ok(&repo_path, &["undo"]); + std::fs::write(&editor_script, "fail").unwrap(); + let stderr = test_env.jj_cmd_failure(&repo_path, &["resolve"]); + insta::assert_snapshot!(stderr.replace("exit code", "exit status"), @r#" + Resolving conflicts in: file1 + Error: Failed to resolve conflicts + Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation) + "#); + insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff", "--git"]), @""); + insta::assert_snapshot!( + test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]), + @r#" + file1 2-sided conflict + file2 2-sided conflict + "# + ); +} diff --git a/lib/src/repo_path.rs b/lib/src/repo_path.rs index 0a3e577703..23f1cdaa9d 100644 --- a/lib/src/repo_path.rs +++ b/lib/src/repo_path.rs @@ -547,7 +547,7 @@ pub enum UiPathParseError { /// Converts `RepoPath`s to and from plain strings as displayed to the user /// (e.g. relative to CWD). -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RepoPathUiConverter { /// Variant for a local file system. Paths are interpreted relative to `cwd` /// with the repo rooted in `base`.