mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-18 18:27:38 +00:00
New jj resolve
command to resolve conflicts
This command uses an external merge tool to resolve conflicts simple enough that they can be resolved with a 3-way merge. This commit provides a very basic version of `jj resolve` that is hardcoded to use vimdiff. This also slightly changes the errors of the Diff Editor, so that both the diff editor and `jj resolve can share an error type.
This commit is contained in:
parent
55762e3681
commit
1158600c80
5 changed files with 257 additions and 35 deletions
|
@ -52,7 +52,7 @@ use jujutsu_lib::workspace::{Workspace, WorkspaceInitError, WorkspaceLoadError};
|
||||||
use jujutsu_lib::{dag_walk, file_util, git, revset};
|
use jujutsu_lib::{dag_walk, file_util, git, revset};
|
||||||
|
|
||||||
use crate::config::read_config;
|
use crate::config::read_config;
|
||||||
use crate::diff_edit::DiffEditError;
|
use crate::diff_edit::{ConflictResolveError, DiffEditError};
|
||||||
use crate::formatter::Formatter;
|
use crate::formatter::Formatter;
|
||||||
use crate::templater::TemplateFormatter;
|
use crate::templater::TemplateFormatter;
|
||||||
use crate::ui::{ColorChoice, Ui};
|
use crate::ui::{ColorChoice, Ui};
|
||||||
|
@ -154,6 +154,12 @@ impl From<DiffEditError> for CommandError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ConflictResolveError> for CommandError {
|
||||||
|
fn from(err: ConflictResolveError) -> Self {
|
||||||
|
user_error(format!("Failed to use external tool to resolve: {err}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<git2::Error> for CommandError {
|
impl From<git2::Error> for CommandError {
|
||||||
fn from(err: git2::Error) -> Self {
|
fn from(err: git2::Error) -> Self {
|
||||||
user_error(format!("Git operation failed: {err}"))
|
user_error(format!("Git operation failed: {err}"))
|
||||||
|
@ -756,14 +762,33 @@ impl WorkspaceCommandHelper {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_mergetool(
|
||||||
|
&self,
|
||||||
|
ui: &mut Ui,
|
||||||
|
tree: &Tree,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<TreeId, CommandError> {
|
||||||
|
Ok(crate::diff_edit::run_mergetool(
|
||||||
|
ui,
|
||||||
|
tree,
|
||||||
|
&self.parse_file_path(path)?,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn edit_diff(
|
pub fn edit_diff(
|
||||||
&self,
|
&self,
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
left_tree: &Tree,
|
left_tree: &Tree,
|
||||||
right_tree: &Tree,
|
right_tree: &Tree,
|
||||||
instructions: &str,
|
instructions: &str,
|
||||||
) -> Result<TreeId, DiffEditError> {
|
) -> Result<TreeId, CommandError> {
|
||||||
crate::diff_edit::edit_diff(ui, left_tree, right_tree, instructions, self.base_ignores())
|
Ok(crate::diff_edit::edit_diff(
|
||||||
|
ui,
|
||||||
|
left_tree,
|
||||||
|
right_tree,
|
||||||
|
instructions,
|
||||||
|
self.base_ignores(),
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_diff(
|
pub fn select_diff(
|
||||||
|
|
|
@ -91,6 +91,7 @@ enum Commands {
|
||||||
Unsquash(UnsquashArgs),
|
Unsquash(UnsquashArgs),
|
||||||
Restore(RestoreArgs),
|
Restore(RestoreArgs),
|
||||||
Touchup(TouchupArgs),
|
Touchup(TouchupArgs),
|
||||||
|
Resolve(ResolveArgs),
|
||||||
Split(SplitArgs),
|
Split(SplitArgs),
|
||||||
/// Merge work from multiple branches
|
/// Merge work from multiple branches
|
||||||
///
|
///
|
||||||
|
@ -495,6 +496,35 @@ struct UnsquashArgs {
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a conflicted file 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.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
// TODOs:
|
||||||
|
// - `jj resolve --list` to list conflicts
|
||||||
|
// - `jj resolve --editor` to resolve a conflict in the default text editor. Should work for
|
||||||
|
// conflicts with 3+ adds. Useful to resolve conflicts in a commit other than the current one.
|
||||||
|
// - Make the `path` argument optional and/or repeatable. If a specific file is not specified,
|
||||||
|
// either pick an arbitrary file with a conflict (e.g. first one `jj resolve --list` shows),
|
||||||
|
// offer a UI, and/or loop over all the conflicted files.
|
||||||
|
// - A way to help split commits with conflicts that are too complicated (more than two sides)
|
||||||
|
// into commits with simpler conflicts. In case of a tree with many merges, we could for example
|
||||||
|
// point to existing commits with simpler conflicts where resolving those conflicts would help
|
||||||
|
// simplify the present one.
|
||||||
|
#[derive(clap::Args, Clone, Debug)]
|
||||||
|
struct ResolveArgs {
|
||||||
|
#[arg(long, short, default_value = "@")]
|
||||||
|
revision: String,
|
||||||
|
/// The path to a file with conflicts. You can use `jj status` to find such
|
||||||
|
/// files
|
||||||
|
#[arg(value_hint = clap::ValueHint::AnyPath)]
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Restore paths from another revision
|
/// Restore paths from another revision
|
||||||
///
|
///
|
||||||
/// That means that the paths get the same content in the destination (`--to`)
|
/// That means that the paths get the same content in the destination (`--to`)
|
||||||
|
@ -2828,6 +2858,25 @@ aborted.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_resolve(
|
||||||
|
ui: &mut Ui,
|
||||||
|
command: &CommandHelper,
|
||||||
|
args: &ResolveArgs,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let mut workspace_command = command.workspace_helper(ui)?;
|
||||||
|
let commit = workspace_command.resolve_single_rev(&args.revision)?;
|
||||||
|
workspace_command.check_rewriteable(&commit)?;
|
||||||
|
let mut tx = workspace_command.start_transaction(&format!(
|
||||||
|
"Resolve conflicts in commit {}",
|
||||||
|
commit.id().hex()
|
||||||
|
));
|
||||||
|
let new_tree_id = workspace_command.run_mergetool(ui, &commit.tree(), &args.path)?;
|
||||||
|
CommitBuilder::for_rewrite_from(ui.settings(), &commit)
|
||||||
|
.set_tree(new_tree_id)
|
||||||
|
.write_to_repo(tx.mut_repo());
|
||||||
|
workspace_command.finish_transaction(ui, tx)
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_restore(
|
fn cmd_restore(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
command: &CommandHelper,
|
command: &CommandHelper,
|
||||||
|
@ -4651,6 +4700,7 @@ pub fn run_command(
|
||||||
Commands::Merge(sub_args) => cmd_merge(ui, command_helper, sub_args),
|
Commands::Merge(sub_args) => cmd_merge(ui, command_helper, sub_args),
|
||||||
Commands::Rebase(sub_args) => cmd_rebase(ui, command_helper, sub_args),
|
Commands::Rebase(sub_args) => cmd_rebase(ui, command_helper, sub_args),
|
||||||
Commands::Backout(sub_args) => cmd_backout(ui, command_helper, sub_args),
|
Commands::Backout(sub_args) => cmd_backout(ui, command_helper, sub_args),
|
||||||
|
Commands::Resolve(sub_args) => cmd_resolve(ui, command_helper, sub_args),
|
||||||
Commands::Branch(sub_args) => cmd_branch(ui, command_helper, sub_args),
|
Commands::Branch(sub_args) => cmd_branch(ui, command_helper, sub_args),
|
||||||
Commands::Undo(sub_args) => cmd_op_undo(ui, command_helper, sub_args),
|
Commands::Undo(sub_args) => cmd_op_undo(ui, command_helper, sub_args),
|
||||||
Commands::Operation(sub_args) => cmd_operation(ui, command_helper, sub_args),
|
Commands::Operation(sub_args) => cmd_operation(ui, command_helper, sub_args),
|
||||||
|
|
205
src/diff_edit.rs
205
src/diff_edit.rs
|
@ -12,6 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -20,7 +21,10 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use config::ConfigError;
|
use config::ConfigError;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use jujutsu_lib::backend::TreeId;
|
use jujutsu_lib::backend::{TreeId, TreeValue};
|
||||||
|
use jujutsu_lib::conflicts::{
|
||||||
|
describe_conflict, extract_file_conflict_as_single_hunk, materialize_merge_result,
|
||||||
|
};
|
||||||
use jujutsu_lib::gitignore::GitIgnoreFile;
|
use jujutsu_lib::gitignore::GitIgnoreFile;
|
||||||
use jujutsu_lib::matchers::EverythingMatcher;
|
use jujutsu_lib::matchers::EverythingMatcher;
|
||||||
use jujutsu_lib::repo_path::RepoPath;
|
use jujutsu_lib::repo_path::RepoPath;
|
||||||
|
@ -33,36 +37,69 @@ use thiserror::Error;
|
||||||
use crate::ui::Ui;
|
use crate::ui::Ui;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum DiffEditError {
|
pub enum ExternalToolError {
|
||||||
#[error("Invalid config: {0}")]
|
#[error("Invalid config: {0}")]
|
||||||
ConfigError(#[from] ConfigError),
|
ConfigError(#[from] ConfigError),
|
||||||
#[error("The diff tool exited with a non-zero code")]
|
|
||||||
DifftoolAborted,
|
|
||||||
#[error("Failed to write directories to diff: {0:?}")]
|
|
||||||
CheckoutError(CheckoutError),
|
|
||||||
#[error("Error setting up temporary directory: {0:?}")]
|
#[error("Error setting up temporary directory: {0:?}")]
|
||||||
SetUpDirError(#[source] std::io::Error),
|
SetUpDirError(#[source] std::io::Error),
|
||||||
#[error("Error executing editor '{editor_binary}': {source}")]
|
#[error("Error executing '{tool_binary}': {source}")]
|
||||||
ExecuteEditorError {
|
FailedToExecute {
|
||||||
editor_binary: String,
|
tool_binary: String,
|
||||||
#[source]
|
#[source]
|
||||||
source: std::io::Error,
|
source: std::io::Error,
|
||||||
},
|
},
|
||||||
|
#[error("Tool exited with a non-zero code.")]
|
||||||
|
ToolAborted,
|
||||||
#[error("I/O error: {0:?}")]
|
#[error("I/O error: {0:?}")]
|
||||||
IoError(#[source] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DiffEditError {
|
||||||
|
#[error("{0}")]
|
||||||
|
ExternalToolError(#[from] ExternalToolError),
|
||||||
|
#[error("Failed to write directories to diff: {0:?}")]
|
||||||
|
CheckoutError(#[from] CheckoutError),
|
||||||
#[error("Failed to snapshot changes: {0:?}")]
|
#[error("Failed to snapshot changes: {0:?}")]
|
||||||
SnapshotError(SnapshotError),
|
SnapshotError(#[from] SnapshotError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CheckoutError> for DiffEditError {
|
#[derive(Debug, Error)]
|
||||||
fn from(err: CheckoutError) -> Self {
|
pub enum ConflictResolveError {
|
||||||
DiffEditError::CheckoutError(err)
|
#[error("{0}")]
|
||||||
}
|
ExternalToolError(#[from] ExternalToolError),
|
||||||
|
#[error("Couldn't find the path {0:?} in this revision")]
|
||||||
|
PathNotFoundError(RepoPath),
|
||||||
|
#[error("Couldn't find any conflicts at {0:?} in this revision")]
|
||||||
|
NotAConflictError(RepoPath),
|
||||||
|
#[error(
|
||||||
|
"Only conflicts that involve normal files (not symlinks, not executable, etc.) are \
|
||||||
|
supported. Conflict summary:\n {1}"
|
||||||
|
)]
|
||||||
|
NotNormalFilesError(RepoPath, String),
|
||||||
|
#[error(
|
||||||
|
"The conflict at {path:?} has {removes} removes and {adds} adds.\nAt most 1 remove and 2 \
|
||||||
|
adds are supported."
|
||||||
|
)]
|
||||||
|
ConflictTooComplicatedError {
|
||||||
|
path: RepoPath,
|
||||||
|
removes: usize,
|
||||||
|
adds: usize,
|
||||||
|
},
|
||||||
|
#[error("The output file is either unchanged or empty after the editor quit.")]
|
||||||
|
EmptyOrUnchanged,
|
||||||
|
#[error("Backend error: {0:?}")]
|
||||||
|
BackendError(#[from] jujutsu_lib::backend::BackendError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SnapshotError> for DiffEditError {
|
impl From<std::io::Error> for DiffEditError {
|
||||||
fn from(err: SnapshotError) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
DiffEditError::SnapshotError(err)
|
DiffEditError::ExternalToolError(ExternalToolError::from(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<std::io::Error> for ConflictResolveError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
ConflictResolveError::ExternalToolError(ExternalToolError::from(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,8 +110,8 @@ fn check_out(
|
||||||
tree: &Tree,
|
tree: &Tree,
|
||||||
sparse_patterns: Vec<RepoPath>,
|
sparse_patterns: Vec<RepoPath>,
|
||||||
) -> Result<TreeState, DiffEditError> {
|
) -> Result<TreeState, DiffEditError> {
|
||||||
std::fs::create_dir(&wc_dir).map_err(DiffEditError::SetUpDirError)?;
|
std::fs::create_dir(&wc_dir).map_err(ExternalToolError::SetUpDirError)?;
|
||||||
std::fs::create_dir(&state_dir).map_err(DiffEditError::SetUpDirError)?;
|
std::fs::create_dir(&state_dir).map_err(ExternalToolError::SetUpDirError)?;
|
||||||
let mut tree_state = TreeState::init(store, wc_dir, state_dir);
|
let mut tree_state = TreeState::init(store, wc_dir, state_dir);
|
||||||
tree_state.set_sparse_patterns(sparse_patterns)?;
|
tree_state.set_sparse_patterns(sparse_patterns)?;
|
||||||
tree_state.check_out(tree)?;
|
tree_state.check_out(tree)?;
|
||||||
|
@ -96,6 +133,113 @@ fn set_readonly_recursively(path: &Path) -> Result<(), std::io::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_mergetool(
|
||||||
|
_ui: &mut Ui,
|
||||||
|
tree: &Tree,
|
||||||
|
repo_path: &RepoPath,
|
||||||
|
) -> Result<TreeId, ConflictResolveError> {
|
||||||
|
let conflict_id = match tree.path_value(repo_path) {
|
||||||
|
Some(TreeValue::Conflict(id)) => id,
|
||||||
|
Some(_) => return Err(ConflictResolveError::NotAConflictError(repo_path.clone())),
|
||||||
|
None => return Err(ConflictResolveError::PathNotFoundError(repo_path.clone())),
|
||||||
|
};
|
||||||
|
let conflict = tree.store().read_conflict(repo_path, &conflict_id)?;
|
||||||
|
let mut content = match extract_file_conflict_as_single_hunk(tree.store(), repo_path, &conflict)
|
||||||
|
{
|
||||||
|
Some(c) => c,
|
||||||
|
_ => {
|
||||||
|
let mut summary_bytes: Vec<u8> = vec![];
|
||||||
|
describe_conflict(&conflict, &mut summary_bytes)
|
||||||
|
.expect("Writing to an in-memory buffer should never fail");
|
||||||
|
return Err(ConflictResolveError::NotNormalFilesError(
|
||||||
|
repo_path.clone(),
|
||||||
|
String::from_utf8_lossy(summary_bytes.as_slice()).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// The usual case is 1 `removes` and 2 `adds`. 0 `removes` means the file did
|
||||||
|
// not exist in the conflict base. Only 1 `adds` may exist for an
|
||||||
|
// edit-delete conflict.
|
||||||
|
if content.removes.len() > 1 || content.adds.len() > 2 {
|
||||||
|
return Err(ConflictResolveError::ConflictTooComplicatedError {
|
||||||
|
path: repo_path.clone(),
|
||||||
|
removes: content.removes.len(),
|
||||||
|
adds: content.adds.len(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut materialized_conflict: Vec<u8> = vec![];
|
||||||
|
materialize_merge_result(&content, &mut materialized_conflict)
|
||||||
|
.expect("Writing to an in-memory buffer should never fail");
|
||||||
|
let materialized_conflict = materialized_conflict;
|
||||||
|
|
||||||
|
let files: HashMap<&str, _> = maplit::hashmap! {
|
||||||
|
"base" => content.removes.pop().unwrap_or_default(),
|
||||||
|
"right" => content.adds.pop().unwrap_or_default(),
|
||||||
|
"left" => content.adds.pop().unwrap_or_default(),
|
||||||
|
"output" => materialized_conflict.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let temp_dir = tempfile::Builder::new()
|
||||||
|
.prefix("jj-resolve-")
|
||||||
|
.tempdir()
|
||||||
|
.map_err(ExternalToolError::SetUpDirError)?;
|
||||||
|
let suffix = repo_path
|
||||||
|
.components()
|
||||||
|
.last()
|
||||||
|
.map(|filename| format!("_{}", filename.as_str()))
|
||||||
|
// The default case below should never actually trigger, but we support it just in case
|
||||||
|
// resolving the root path ever makes sense.
|
||||||
|
.unwrap_or_default();
|
||||||
|
let paths: Result<HashMap<&str, _>, ConflictResolveError> = files
|
||||||
|
.iter()
|
||||||
|
.map(|(role, contents)| {
|
||||||
|
let path = temp_dir.path().join(format!("{role}{suffix}"));
|
||||||
|
std::fs::write(&path, contents).map_err(ExternalToolError::SetUpDirError)?;
|
||||||
|
if *role != "output" {
|
||||||
|
// TODO: Should actually ignore the error here, or have a warning.
|
||||||
|
set_readonly_recursively(&path).map_err(ExternalToolError::SetUpDirError)?;
|
||||||
|
}
|
||||||
|
Ok((*role, path))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let paths = paths?;
|
||||||
|
|
||||||
|
let progname = "vimdiff";
|
||||||
|
let exit_status = Command::new(progname)
|
||||||
|
.args(["-f", "-d"])
|
||||||
|
.arg(paths.get("output").unwrap())
|
||||||
|
.arg("-M")
|
||||||
|
.args(["left", "base", "right"].map(|n| paths.get(n).unwrap()))
|
||||||
|
.args(["-c", "wincmd J", "-c", "setl modifiable write"])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| ExternalToolError::FailedToExecute {
|
||||||
|
tool_binary: progname.to_string(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
if !exit_status.success() {
|
||||||
|
return Err(ConflictResolveError::from(ExternalToolError::ToolAborted));
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_file_contents: Vec<u8> = std::fs::read(paths.get("output").unwrap())?;
|
||||||
|
if output_file_contents.is_empty() || output_file_contents == materialized_conflict {
|
||||||
|
return Err(ConflictResolveError::EmptyOrUnchanged);
|
||||||
|
}
|
||||||
|
// TODO: parse any remaining conflicts (done in followup commit)
|
||||||
|
let new_file_id = tree
|
||||||
|
.store()
|
||||||
|
.write_file(repo_path, &mut File::open(paths.get("output").unwrap())?)?;
|
||||||
|
let mut tree_builder = tree.store().tree_builder(tree.id().clone());
|
||||||
|
tree_builder.set(
|
||||||
|
repo_path.clone(),
|
||||||
|
TreeValue::File {
|
||||||
|
id: new_file_id,
|
||||||
|
executable: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(tree_builder.write_tree())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn edit_diff(
|
pub fn edit_diff(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
left_tree: &Tree,
|
left_tree: &Tree,
|
||||||
|
@ -114,7 +258,7 @@ pub fn edit_diff(
|
||||||
let temp_dir = tempfile::Builder::new()
|
let temp_dir = tempfile::Builder::new()
|
||||||
.prefix("jj-diff-edit-")
|
.prefix("jj-diff-edit-")
|
||||||
.tempdir()
|
.tempdir()
|
||||||
.map_err(DiffEditError::SetUpDirError)?;
|
.map_err(ExternalToolError::SetUpDirError)?;
|
||||||
let left_wc_dir = temp_dir.path().join("left");
|
let left_wc_dir = temp_dir.path().join("left");
|
||||||
let left_state_dir = temp_dir.path().join("left_state");
|
let left_state_dir = temp_dir.path().join("left_state");
|
||||||
let right_wc_dir = temp_dir.path().join("right");
|
let right_wc_dir = temp_dir.path().join("right");
|
||||||
|
@ -126,7 +270,7 @@ pub fn edit_diff(
|
||||||
left_tree,
|
left_tree,
|
||||||
changed_files.clone(),
|
changed_files.clone(),
|
||||||
)?;
|
)?;
|
||||||
set_readonly_recursively(&left_wc_dir).map_err(DiffEditError::SetUpDirError)?;
|
set_readonly_recursively(&left_wc_dir).map_err(ExternalToolError::SetUpDirError)?;
|
||||||
let mut right_tree_state = check_out(
|
let mut right_tree_state = check_out(
|
||||||
store.clone(),
|
store.clone(),
|
||||||
right_wc_dir.clone(),
|
right_wc_dir.clone(),
|
||||||
|
@ -139,9 +283,12 @@ pub fn edit_diff(
|
||||||
// not get any instructions.
|
// not get any instructions.
|
||||||
let add_instructions = !instructions.is_empty() && !instructions_path.exists();
|
let add_instructions = !instructions.is_empty() && !instructions_path.exists();
|
||||||
if add_instructions {
|
if add_instructions {
|
||||||
let mut file = File::create(&instructions_path).map_err(DiffEditError::SetUpDirError)?;
|
// TODO: This can be replaced with std::fs::write. Is this used in other places
|
||||||
|
// as well?
|
||||||
|
let mut file =
|
||||||
|
File::create(&instructions_path).map_err(ExternalToolError::SetUpDirError)?;
|
||||||
file.write_all(instructions.as_bytes())
|
file.write_all(instructions.as_bytes())
|
||||||
.map_err(DiffEditError::SetUpDirError)?;
|
.map_err(ExternalToolError::SetUpDirError)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this configuration have a table of possible editors and detect the
|
// TODO: Make this configuration have a table of possible editors and detect the
|
||||||
|
@ -154,23 +301,23 @@ pub fn edit_diff(
|
||||||
"Using default editor '{}'; you can change this by setting ui.diff-editor\n",
|
"Using default editor '{}'; you can change this by setting ui.diff-editor\n",
|
||||||
default_editor
|
default_editor
|
||||||
))
|
))
|
||||||
.map_err(DiffEditError::IoError)?;
|
.map_err(ExternalToolError::IoError)?;
|
||||||
default_editor
|
default_editor
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let editor = get_tool(ui.settings(), &editor_name)?;
|
let editor = get_tool(ui.settings(), &editor_name).map_err(ExternalToolError::ConfigError)?;
|
||||||
// Start a diff editor on the two directories.
|
// Start a diff editor on the two directories.
|
||||||
let exit_status = Command::new(&editor.program)
|
let exit_status = Command::new(&editor.program)
|
||||||
.args(&editor.edit_args)
|
.args(&editor.edit_args)
|
||||||
.arg(&left_wc_dir)
|
.arg(&left_wc_dir)
|
||||||
.arg(&right_wc_dir)
|
.arg(&right_wc_dir)
|
||||||
.status()
|
.status()
|
||||||
.map_err(|e| DiffEditError::ExecuteEditorError {
|
.map_err(|e| ExternalToolError::FailedToExecute {
|
||||||
editor_binary: editor.program,
|
tool_binary: editor.program.clone(),
|
||||||
source: e,
|
source: e,
|
||||||
})?;
|
})?;
|
||||||
if !exit_status.success() {
|
if !exit_status.success() {
|
||||||
return Err(DiffEditError::DifftoolAborted);
|
return Err(DiffEditError::from(ExternalToolError::ToolAborted));
|
||||||
}
|
}
|
||||||
if add_instructions {
|
if add_instructions {
|
||||||
std::fs::remove_file(instructions_path).ok();
|
std::fs::remove_file(instructions_path).ok();
|
||||||
|
|
|
@ -133,7 +133,7 @@ fn test_restore_interactive() {
|
||||||
std::fs::write(&edit_script, "rm file2\0fail").unwrap();
|
std::fs::write(&edit_script, "rm file2\0fail").unwrap();
|
||||||
let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "-i"]);
|
let stderr = test_env.jj_cmd_failure(&repo_path, &["restore", "-i"]);
|
||||||
insta::assert_snapshot!(stderr, @r###"
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
Error: Failed to edit diff: The diff tool exited with a non-zero code
|
Error: Failed to edit diff: Tool exited with a non-zero code.
|
||||||
"###);
|
"###);
|
||||||
let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]);
|
let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]);
|
||||||
insta::assert_snapshot!(stdout, @r###"
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
|
|
|
@ -51,7 +51,7 @@ fn test_touchup() {
|
||||||
std::fs::write(&edit_script, "rm file2\0fail").unwrap();
|
std::fs::write(&edit_script, "rm file2\0fail").unwrap();
|
||||||
let stderr = test_env.jj_cmd_failure(&repo_path, &["touchup"]);
|
let stderr = test_env.jj_cmd_failure(&repo_path, &["touchup"]);
|
||||||
insta::assert_snapshot!(stderr, @r###"
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
Error: Failed to edit diff: The diff tool exited with a non-zero code
|
Error: Failed to edit diff: Tool exited with a non-zero code.
|
||||||
"###);
|
"###);
|
||||||
let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]);
|
let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]);
|
||||||
insta::assert_snapshot!(stdout, @r###"
|
insta::assert_snapshot!(stdout, @r###"
|
||||||
|
|
Loading…
Reference in a new issue