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.
This commit is contained in:
Scott Taylor 2024-12-17 09:33:19 -06:00 committed by Scott Taylor
parent 5f77edf503
commit 7df0f16fe0
10 changed files with 343 additions and 82 deletions

View file

@ -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

View file

@ -1484,9 +1484,19 @@ to the current parents may contain changes from multiple commits.
) -> Result<MergeEditor, MergeToolConfigError> {
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,
)
}
}

View file

@ -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<DiffRenderError> for CommandError {
impl From<ConflictResolveError> 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<MergeToolPartialResolutionError> for CommandError {
fn from(err: MergeToolPartialResolutionError) -> Self {
user_error(err)
}
}

View file

@ -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<String>,
/// 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(())
}

View file

@ -658,26 +658,28 @@ fn make_merge_file(
pub fn edit_merge_builtin(
tree: &MergedTree,
merge_tool_file: &MergeToolFile,
merge_tool_files: &[MergeToolFile],
) -> Result<MergedTreeId, BuiltinToolError> {
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)
}

View file

@ -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<MergedTreeId, ConflictResolveError> {
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<MergeToolPartialResolutionError>), 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(

View file

@ -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<Self, MergeToolConfigError> {
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<Self, MergeToolConfigError> {
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<Self, MergeToolConfigError> {
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<MergedTreeId, ConflictResolveError> {
let merge_tool_file = MergeToolFile::from_tree_and_path(tree, repo_path)?;
repo_paths: &[&RepoPath],
) -> Result<(MergedTreeId, Option<MergeToolPartialResolutionError>), ConflictResolveError> {
let merge_tool_files: Vec<MergeToolFile> = 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)
};

View file

@ -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:**
* `<FILESETS>` — 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
* `<FILESETS>` — Only resolve conflicts in these paths. You can use the `--list` argument to find paths to use here
###### **Options:**
* `-r`, `--revision <REVSET>`
Default value: `@`
* `-l`, `--list` — Instead of resolving one conflict, list all the conflicts
* `-l`, `--list` — Instead of resolving conflicts, list all the conflicts
* `--tool <NAME>` — Specify 3-way merge tool to be used

View file

@ -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",
[
"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###"
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",
)
.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
"#
);
}

View file

@ -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`.