2022-11-26 23:57:50 +00:00
|
|
|
// Copyright 2020 The Jujutsu Authors
|
2020-12-26 19:47:13 +00:00
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
use std::collections::HashMap;
|
2021-05-16 05:16:07 +00:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::Write;
|
2021-03-14 17:37:28 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::process::Command;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2022-05-01 01:45:22 +00:00
|
|
|
use config::ConfigError;
|
2022-04-24 05:47:36 +00:00
|
|
|
use itertools::Itertools;
|
2022-10-28 03:30:44 +00:00
|
|
|
use jujutsu_lib::backend::{TreeId, TreeValue};
|
|
|
|
use jujutsu_lib::conflicts::{
|
|
|
|
describe_conflict, extract_file_conflict_as_single_hunk, materialize_merge_result,
|
|
|
|
};
|
2022-03-10 06:41:09 +00:00
|
|
|
use jujutsu_lib::gitignore::GitIgnoreFile;
|
2021-06-05 21:29:40 +00:00
|
|
|
use jujutsu_lib::matchers::EverythingMatcher;
|
2021-05-19 16:41:25 +00:00
|
|
|
use jujutsu_lib::repo_path::RepoPath;
|
2022-03-11 23:33:03 +00:00
|
|
|
use jujutsu_lib::settings::UserSettings;
|
2021-09-12 06:52:38 +00:00
|
|
|
use jujutsu_lib::store::Store;
|
2022-04-24 05:47:36 +00:00
|
|
|
use jujutsu_lib::tree::Tree;
|
2022-05-01 05:36:46 +00:00
|
|
|
use jujutsu_lib::working_copy::{CheckoutError, SnapshotError, TreeState};
|
2020-12-26 03:13:01 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2022-05-02 15:06:44 +00:00
|
|
|
use crate::ui::Ui;
|
|
|
|
|
2022-04-30 19:43:02 +00:00
|
|
|
#[derive(Debug, Error)]
|
2022-10-28 03:30:44 +00:00
|
|
|
pub enum ExternalToolError {
|
2022-05-01 01:45:22 +00:00
|
|
|
#[error("Invalid config: {0}")]
|
|
|
|
ConfigError(#[from] ConfigError),
|
2022-04-30 19:43:02 +00:00
|
|
|
#[error("Error setting up temporary directory: {0:?}")]
|
|
|
|
SetUpDirError(#[source] std::io::Error),
|
2022-10-28 03:30:44 +00:00
|
|
|
#[error("Error executing '{tool_binary}' with args {args:?}: {source}")]
|
2022-10-28 03:30:44 +00:00
|
|
|
FailedToExecute {
|
|
|
|
tool_binary: String,
|
2022-10-28 03:30:44 +00:00
|
|
|
args: Vec<String>,
|
2022-04-30 19:43:02 +00:00
|
|
|
#[source]
|
|
|
|
source: std::io::Error,
|
|
|
|
},
|
2022-10-28 03:30:44 +00:00
|
|
|
#[error(
|
|
|
|
"Tool exited with a non-zero code.\n Details: tool '{tool_binary}' was \
|
|
|
|
executed with args {args:?}. Exit code: {}.",
|
|
|
|
exit_status.code().map(|c| c.to_string()).unwrap_or_else(|| "<unknown>".to_string())
|
|
|
|
)]
|
|
|
|
ToolAborted {
|
|
|
|
tool_binary: String,
|
|
|
|
args: Vec<String>,
|
|
|
|
exit_status: std::process::ExitStatus,
|
|
|
|
},
|
2022-04-30 19:43:02 +00:00
|
|
|
#[error("I/O error: {0:?}")]
|
2022-10-28 03:30:44 +00:00
|
|
|
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),
|
2022-05-01 05:36:46 +00:00
|
|
|
#[error("Failed to snapshot changes: {0:?}")]
|
2022-10-28 03:30:44 +00:00
|
|
|
SnapshotError(#[from] SnapshotError),
|
2020-12-26 03:13:01 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum ConflictResolveError {
|
|
|
|
#[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),
|
2020-12-26 03:13:01 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
impl From<std::io::Error> for DiffEditError {
|
|
|
|
fn from(err: std::io::Error) -> Self {
|
|
|
|
DiffEditError::ExternalToolError(ExternalToolError::from(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl From<std::io::Error> for ConflictResolveError {
|
|
|
|
fn from(err: std::io::Error) -> Self {
|
|
|
|
ConflictResolveError::ExternalToolError(ExternalToolError::from(err))
|
2022-05-01 05:36:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-26 03:13:01 +00:00
|
|
|
fn check_out(
|
2021-09-12 06:52:38 +00:00
|
|
|
store: Arc<Store>,
|
2020-12-26 03:13:01 +00:00
|
|
|
wc_dir: PathBuf,
|
|
|
|
state_dir: PathBuf,
|
2022-02-13 19:54:48 +00:00
|
|
|
tree: &Tree,
|
2022-04-24 05:47:36 +00:00
|
|
|
sparse_patterns: Vec<RepoPath>,
|
2020-12-26 03:13:01 +00:00
|
|
|
) -> Result<TreeState, DiffEditError> {
|
2022-10-28 03:30:44 +00:00
|
|
|
std::fs::create_dir(&wc_dir).map_err(ExternalToolError::SetUpDirError)?;
|
|
|
|
std::fs::create_dir(&state_dir).map_err(ExternalToolError::SetUpDirError)?;
|
2020-12-26 03:13:01 +00:00
|
|
|
let mut tree_state = TreeState::init(store, wc_dir, state_dir);
|
2022-04-24 05:47:36 +00:00
|
|
|
tree_state.set_sparse_patterns(sparse_patterns)?;
|
2022-02-13 19:54:48 +00:00
|
|
|
tree_state.check_out(tree)?;
|
2020-12-26 03:13:01 +00:00
|
|
|
Ok(tree_state)
|
|
|
|
}
|
|
|
|
|
2022-04-30 19:43:02 +00:00
|
|
|
fn set_readonly_recursively(path: &Path) -> Result<(), std::io::Error> {
|
2022-09-07 04:05:28 +00:00
|
|
|
// Directory permission is unchanged since files under readonly directory cannot
|
|
|
|
// be removed.
|
2020-12-26 18:34:58 +00:00
|
|
|
if path.is_dir() {
|
2022-04-30 19:43:02 +00:00
|
|
|
for entry in path.read_dir()? {
|
|
|
|
set_readonly_recursively(&entry?.path())?;
|
2020-12-26 18:34:58 +00:00
|
|
|
}
|
2022-09-07 04:05:28 +00:00
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
let mut perms = std::fs::metadata(path)?.permissions();
|
|
|
|
perms.set_readonly(true);
|
|
|
|
std::fs::set_permissions(path, perms)
|
2020-12-26 18:34:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
pub fn run_mergetool(
|
2022-10-28 03:30:44 +00:00
|
|
|
ui: &mut Ui,
|
2022-10-28 03:30:44 +00:00
|
|
|
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?;
|
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
let editor = get_merge_tool_from_settings(ui)?;
|
|
|
|
// TODO: Error if `editor.merge_args` is empty
|
|
|
|
let args = interpolate_mergetool_filename_patterns(&editor.merge_args, &paths);
|
|
|
|
let args_str = args
|
|
|
|
.iter()
|
|
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
|
|
.collect_vec();
|
|
|
|
let exit_status = Command::new(&editor.program)
|
|
|
|
.args(args)
|
2022-10-28 03:30:44 +00:00
|
|
|
.status()
|
|
|
|
.map_err(|e| ExternalToolError::FailedToExecute {
|
2022-10-28 03:30:44 +00:00
|
|
|
tool_binary: editor.program.clone(),
|
|
|
|
args: args_str.clone(),
|
2022-10-28 03:30:44 +00:00
|
|
|
source: e,
|
|
|
|
})?;
|
|
|
|
if !exit_status.success() {
|
2022-10-28 03:30:44 +00:00
|
|
|
return Err(ConflictResolveError::from(ExternalToolError::ToolAborted {
|
|
|
|
tool_binary: editor.program,
|
|
|
|
args: args_str,
|
|
|
|
exit_status,
|
|
|
|
}));
|
2022-10-28 03:30:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
fn interpolate_mergetool_filename_patterns<T: std::str::FromStr + From<PathBuf>>(
|
|
|
|
merge_args: &[String],
|
|
|
|
paths: &HashMap<&str, PathBuf>,
|
|
|
|
) -> Vec<T>
|
|
|
|
where
|
|
|
|
Vec<T>: FromIterator<PathBuf>,
|
|
|
|
{
|
|
|
|
merge_args
|
|
|
|
.iter()
|
|
|
|
.map(|arg| {
|
|
|
|
// TODO: Match all instances of `\$\w+` pattern and replace them
|
|
|
|
// so that portions of args can be replaced, and so that file paths
|
|
|
|
// that include the '$' character are processed correctly.
|
|
|
|
arg.strip_prefix('$')
|
|
|
|
.and_then(|p| paths.get(p))
|
|
|
|
.and_then(|p| From::from(p.clone()))
|
|
|
|
.unwrap_or_else(|| From::from(arg.clone()))
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2021-05-16 05:16:07 +00:00
|
|
|
pub fn edit_diff(
|
2022-05-02 15:06:44 +00:00
|
|
|
ui: &mut Ui,
|
2021-05-16 05:16:07 +00:00
|
|
|
left_tree: &Tree,
|
|
|
|
right_tree: &Tree,
|
|
|
|
instructions: &str,
|
2022-03-10 06:41:09 +00:00
|
|
|
base_ignores: Arc<GitIgnoreFile>,
|
2021-05-16 05:16:07 +00:00
|
|
|
) -> Result<TreeId, DiffEditError> {
|
2020-12-26 03:13:01 +00:00
|
|
|
let store = left_tree.store();
|
2022-04-24 05:47:36 +00:00
|
|
|
let changed_files = left_tree
|
|
|
|
.diff(right_tree, &EverythingMatcher)
|
|
|
|
.map(|(path, _value)| path)
|
|
|
|
.collect_vec();
|
2020-12-26 03:13:01 +00:00
|
|
|
|
2022-04-24 05:47:36 +00:00
|
|
|
// Check out the two trees in temporary directories. Only include changed files
|
|
|
|
// in the sparse checkout patterns.
|
2022-09-07 04:02:05 +00:00
|
|
|
let temp_dir = tempfile::Builder::new()
|
|
|
|
.prefix("jj-diff-edit-")
|
|
|
|
.tempdir()
|
2022-10-28 03:30:44 +00:00
|
|
|
.map_err(ExternalToolError::SetUpDirError)?;
|
2020-12-26 03:13:01 +00:00
|
|
|
let left_wc_dir = temp_dir.path().join("left");
|
|
|
|
let left_state_dir = temp_dir.path().join("left_state");
|
|
|
|
let right_wc_dir = temp_dir.path().join("right");
|
|
|
|
let right_state_dir = temp_dir.path().join("right_state");
|
|
|
|
check_out(
|
|
|
|
store.clone(),
|
|
|
|
left_wc_dir.clone(),
|
|
|
|
left_state_dir,
|
2022-04-24 05:47:36 +00:00
|
|
|
left_tree,
|
|
|
|
changed_files.clone(),
|
2020-12-26 03:13:01 +00:00
|
|
|
)?;
|
2022-10-28 03:30:44 +00:00
|
|
|
set_readonly_recursively(&left_wc_dir).map_err(ExternalToolError::SetUpDirError)?;
|
2020-12-26 03:13:01 +00:00
|
|
|
let mut right_tree_state = check_out(
|
|
|
|
store.clone(),
|
|
|
|
right_wc_dir.clone(),
|
|
|
|
right_state_dir,
|
2022-04-24 05:47:36 +00:00
|
|
|
right_tree,
|
|
|
|
changed_files,
|
2020-12-26 03:13:01 +00:00
|
|
|
)?;
|
2021-05-16 05:16:07 +00:00
|
|
|
let instructions_path = right_wc_dir.join("JJ-INSTRUCTIONS");
|
|
|
|
// In the unlikely event that the file already exists, then the user will simply
|
|
|
|
// not get any instructions.
|
|
|
|
let add_instructions = !instructions.is_empty() && !instructions_path.exists();
|
|
|
|
if add_instructions {
|
2022-10-28 03:30:44 +00:00
|
|
|
// 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)?;
|
2022-04-30 19:43:02 +00:00
|
|
|
file.write_all(instructions.as_bytes())
|
2022-10-28 03:30:44 +00:00
|
|
|
.map_err(ExternalToolError::SetUpDirError)?;
|
2021-05-16 05:16:07 +00:00
|
|
|
}
|
2020-12-26 03:13:01 +00:00
|
|
|
|
2022-10-28 03:30:44 +00:00
|
|
|
let editor = get_diff_editor_from_settings(ui)?;
|
|
|
|
let mut args_str = editor.edit_args.clone();
|
|
|
|
args_str.push(left_wc_dir.display().to_string());
|
|
|
|
args_str.push(right_wc_dir.display().to_string());
|
2020-12-26 03:13:01 +00:00
|
|
|
// Start a diff editor on the two directories.
|
2022-05-01 01:45:22 +00:00
|
|
|
let exit_status = Command::new(&editor.program)
|
|
|
|
.args(&editor.edit_args)
|
2020-12-26 03:13:01 +00:00
|
|
|
.arg(&left_wc_dir)
|
|
|
|
.arg(&right_wc_dir)
|
|
|
|
.status()
|
2022-10-28 03:30:44 +00:00
|
|
|
.map_err(|e| ExternalToolError::FailedToExecute {
|
|
|
|
tool_binary: editor.program.clone(),
|
2022-10-28 03:30:44 +00:00
|
|
|
args: args_str.clone(),
|
2022-04-30 19:43:02 +00:00
|
|
|
source: e,
|
|
|
|
})?;
|
2020-12-26 03:13:01 +00:00
|
|
|
if !exit_status.success() {
|
2022-10-28 03:30:44 +00:00
|
|
|
return Err(DiffEditError::from(ExternalToolError::ToolAborted {
|
|
|
|
tool_binary: editor.program,
|
|
|
|
args: args_str,
|
|
|
|
exit_status,
|
|
|
|
}));
|
2020-12-26 03:13:01 +00:00
|
|
|
}
|
2021-05-16 05:16:07 +00:00
|
|
|
if add_instructions {
|
|
|
|
std::fs::remove_file(instructions_path).ok();
|
|
|
|
}
|
2020-12-26 03:13:01 +00:00
|
|
|
|
2022-10-02 01:43:57 +00:00
|
|
|
right_tree_state.snapshot(base_ignores)?;
|
|
|
|
Ok(right_tree_state.current_tree_id().clone())
|
2020-12-26 03:13:01 +00:00
|
|
|
}
|
2022-05-01 01:45:22 +00:00
|
|
|
|
|
|
|
/// Merge/diff tool loaded from the settings.
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
struct MergeTool {
|
2022-10-28 03:30:44 +00:00
|
|
|
/// Program to execute. Must be defined; defaults to the tool name
|
|
|
|
/// if not specified in the config.
|
|
|
|
#[serde(default)]
|
2022-05-01 01:45:22 +00:00
|
|
|
pub program: String,
|
|
|
|
/// Arguments to pass to the program when editing diffs.
|
|
|
|
#[serde(default)]
|
|
|
|
pub edit_args: Vec<String>,
|
2022-10-28 03:30:44 +00:00
|
|
|
/// Arguments to pass to the program when resolving 3-way conflicts.
|
|
|
|
/// `$left`, `$right`, `$base`, and `$output` are replaced with
|
|
|
|
/// paths to the corresponding files.
|
|
|
|
/// TODO: Currently, the entire argument has to match one of these 4
|
|
|
|
/// strings to be substituted.
|
|
|
|
#[serde(default)]
|
|
|
|
pub merge_args: Vec<String>,
|
2022-05-01 01:45:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MergeTool {
|
|
|
|
pub fn with_program(program: &str) -> Self {
|
|
|
|
MergeTool {
|
|
|
|
program: program.to_owned(),
|
|
|
|
edit_args: vec![],
|
2022-10-28 03:30:44 +00:00
|
|
|
merge_args: vec![],
|
2022-05-01 01:45:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Loads merge tool options from `[merge-tools.<name>]`. The given name is used
|
|
|
|
/// as an executable name if no configuration found for that name.
|
2022-10-28 03:30:44 +00:00
|
|
|
fn get_tool_config(settings: &UserSettings, name: &str) -> Result<MergeTool, ConfigError> {
|
2022-05-03 04:34:26 +00:00
|
|
|
const TABLE_KEY: &str = "merge-tools";
|
2022-05-01 01:45:22 +00:00
|
|
|
let tools_table = match settings.config().get_table(TABLE_KEY) {
|
|
|
|
Ok(table) => table,
|
|
|
|
Err(ConfigError::NotFound(_)) => return Ok(MergeTool::with_program(name)),
|
|
|
|
Err(err) => return Err(err),
|
|
|
|
};
|
|
|
|
if let Some(v) = tools_table.get(name) {
|
2022-10-28 03:30:44 +00:00
|
|
|
let mut result: MergeTool = v
|
|
|
|
.clone()
|
2022-05-01 01:45:22 +00:00
|
|
|
.try_deserialize()
|
|
|
|
// add config key, deserialize error is otherwise unclear
|
2022-10-28 03:30:44 +00:00
|
|
|
.map_err(|e| ConfigError::Message(format!("{TABLE_KEY}.{name}: {e}")))?;
|
|
|
|
|
|
|
|
if result.program.is_empty() {
|
|
|
|
result.program.clone_from(&name.to_string());
|
|
|
|
};
|
|
|
|
Ok(result)
|
2022-05-01 01:45:22 +00:00
|
|
|
} else {
|
|
|
|
Ok(MergeTool::with_program(name))
|
|
|
|
}
|
|
|
|
}
|
2022-10-28 03:30:44 +00:00
|
|
|
|
|
|
|
fn get_diff_editor_from_settings(ui: &mut Ui) -> Result<MergeTool, ExternalToolError> {
|
|
|
|
_get_editor_from_settings_helper(ui, "diff")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_merge_tool_from_settings(ui: &mut Ui) -> Result<MergeTool, ExternalToolError> {
|
|
|
|
_get_editor_from_settings_helper(ui, "merge")
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds the appropriate tool for diff editing or merges
|
|
|
|
fn _get_editor_from_settings_helper(
|
|
|
|
ui: &mut Ui,
|
|
|
|
diff_or_merge: &str,
|
|
|
|
) -> Result<MergeTool, ExternalToolError> {
|
|
|
|
// TODO: Make this configuration have a table of possible editors and detect the
|
|
|
|
// best one here.
|
|
|
|
let editor_name = match ui
|
|
|
|
.settings()
|
|
|
|
.config()
|
|
|
|
.get_string(&format!("ui.{diff_or_merge}-editor"))
|
|
|
|
{
|
|
|
|
Ok(editor_binary) => editor_binary,
|
|
|
|
Err(_) => {
|
|
|
|
let default_editor = "meld".to_string();
|
|
|
|
ui.write_hint(format!(
|
|
|
|
"Using default editor '{default_editor}'; you can change this by setting \
|
|
|
|
ui.{diff_or_merge}-editor\n"
|
|
|
|
))?;
|
|
|
|
default_editor
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Ok(get_tool_config(ui.settings(), &editor_name)?)
|
|
|
|
}
|