use std::fs::File;
use std::io::Write as _;
use bstr::ByteSlice as _;
use jj_lib::config::ConfigLayer;
use jj_lib::config::ConfigSource;
use jj_lib::repo::Repo as _;
use jj_lib::repo::StoreFactories;
use jj_lib::rewrite::merge_commit_trees;
use jj_lib::settings::UserSettings;
use jj_lib::workspace::Workspace;
use jj_lib::workspace::default_working_copy_factories;
use pollster::FutureExt as _;
use test_case::test_case;
use testutils::CommitBuilderExt as _;
use testutils::TestRepoBackend;
use testutils::TestResult;
use testutils::TestWorkspace;
use testutils::assert_tree_eq;
use testutils::base_user_config;
use testutils::commit_with_tree;
use testutils::repo_path;
static LF_FILE_CONTENT: &[u8] = b"aaa\nbbbb\nccccc\n";
static CRLF_FILE_CONTENT: &[u8] = b"aaa\r\nbbbb\r\nccccc\r\n";
static MIXED_EOL_FILE_CONTENT: &[u8] = b"aaa\nbbbb\r\nccccc\n";
static BINARY_FILE_CONTENT: &[u8] = b"\0";
struct Config {
extra_setting: &'static str,
file_content: &'static [u8],
}
fn base_user_settings_with_extra_configs(extra_settings: &str) -> UserSettings {
let mut config = base_user_config();
config.add_layer(
ConfigLayer::parse(ConfigSource::User, extra_settings)
.expect("Failed to parse the settings"),
);
UserSettings::from_config(config).expect("Failed to create the UserSettings from the config")
}
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: LF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input-output LF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: CRLF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input-output CRLF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: MIXED_EOL_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input-output mixed EOL file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: BINARY_FILE_CONTENT,
} => BINARY_FILE_CONTENT; "eol-conversion input-output binary file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: LF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input LF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: CRLF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input CRLF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: MIXED_EOL_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input mixed EOL file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: BINARY_FILE_CONTENT,
} => BINARY_FILE_CONTENT; "eol-conversion input binary file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: LF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion none LF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: CRLF_FILE_CONTENT,
} => CRLF_FILE_CONTENT; "eol-conversion none CRLF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: MIXED_EOL_FILE_CONTENT,
} => MIXED_EOL_FILE_CONTENT; "eol-conversion none mixed EOL file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: BINARY_FILE_CONTENT,
} => BINARY_FILE_CONTENT; "eol-conversion none binary file")]
fn test_eol_conversion_snapshot(
Config {
extra_setting,
file_content,
}: Config,
) -> Vec<u8> {
let extra_setting = format!("{extra_setting}\n");
let user_settings = base_user_settings_with_extra_configs(&extra_setting);
let mut test_workspace =
TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &user_settings);
let file_repo_path = repo_path("test-eol-file");
let file_disk_path = file_repo_path
.to_fs_path(test_workspace.workspace.workspace_root())
.unwrap();
testutils::write_working_copy_file(
test_workspace.workspace.workspace_root(),
file_repo_path,
file_content,
);
let tree = test_workspace.snapshot().unwrap();
let new_tree = test_workspace.snapshot().unwrap();
assert_tree_eq!(new_tree, tree, "The working copy should be clean.");
let file_added_commit = commit_with_tree(test_workspace.repo.store(), tree);
std::fs::remove_file(&file_disk_path).unwrap();
let tree = test_workspace.snapshot().unwrap();
let file_removed_commit = commit_with_tree(test_workspace.repo.store(), tree);
let workspace = &mut test_workspace.workspace;
workspace
.check_out(
test_workspace.repo.op_id().clone(),
None,
&file_removed_commit,
)
.block_on()
.unwrap();
assert!(!file_disk_path.exists());
let user_settings =
base_user_settings_with_extra_configs("working-copy.eol-conversion = \"none\"\n");
let mut workspace = Workspace::load(
&user_settings,
test_workspace.workspace.workspace_root(),
&StoreFactories::default(),
&default_working_copy_factories(),
)
.expect("Failed to reload the workspace");
let file_added_commit = workspace
.repo_loader()
.store()
.get_commit(file_added_commit.id())
.expect("Failed to find the commit with the test file");
workspace
.check_out(
test_workspace.repo.op_id().clone(),
None,
&file_added_commit,
)
.block_on()
.unwrap();
assert!(file_disk_path.exists());
let new_tree = test_workspace.snapshot().unwrap();
assert_tree_eq!(
new_tree,
file_added_commit.tree(),
"The working copy should be clean."
);
std::fs::read(&file_disk_path).expect("Failed to read the checked out test file")
}
fn create_conflict_snapshot_and_read(extra_setting: &str) -> Vec<u8> {
let no_eol_conversion_settings =
base_user_settings_with_extra_configs("working-copy.eol-conversion = \"none\"\n");
let mut test_workspace = TestWorkspace::init_with_backend_and_settings(
TestRepoBackend::Git,
&no_eol_conversion_settings,
);
let file_repo_path = repo_path("test-eol-file");
let file_disk_path = file_repo_path
.to_fs_path(test_workspace.workspace.workspace_root())
.unwrap();
let root_commit = test_workspace.repo.store().root_commit();
testutils::write_working_copy_file(
test_workspace.workspace.workspace_root(),
file_repo_path,
"a\r\n",
);
let tree = test_workspace.snapshot().unwrap();
let mut tx = test_workspace.repo.start_transaction();
let parent1_commit = tx
.repo_mut()
.new_commit(vec![root_commit.id().clone()], tree)
.write_unwrap();
tx.commit("commit parent1").block_on().unwrap();
test_workspace
.workspace
.check_out(test_workspace.repo.op_id().clone(), None, &root_commit)
.block_on()
.unwrap();
testutils::write_working_copy_file(
test_workspace.workspace.workspace_root(),
file_repo_path,
"b\r\n",
);
let tree = test_workspace.snapshot().unwrap();
let mut tx = test_workspace.repo.start_transaction();
let parent2_commit = tx
.repo_mut()
.new_commit(vec![root_commit.id().clone()], tree)
.write_unwrap();
tx.commit("commit parent2").block_on().unwrap();
test_workspace.repo = test_workspace.repo.reload_at_head().block_on().unwrap();
let tree = merge_commit_trees(&*test_workspace.repo, &[parent1_commit, parent2_commit])
.block_on()
.unwrap();
let merge_commit = commit_with_tree(test_workspace.repo.store(), tree);
test_workspace
.workspace
.check_out(test_workspace.repo.op_id().clone(), None, &merge_commit)
.block_on()
.unwrap();
let mut file = File::options().append(true).open(&file_disk_path).unwrap();
file.write_all(b"c\r\n").unwrap();
drop(file);
let extra_setting = format!("{extra_setting}\n");
let user_settings = base_user_settings_with_extra_configs(&extra_setting);
test_workspace.workspace = Workspace::load(
&user_settings,
test_workspace.workspace.workspace_root(),
&StoreFactories::default(),
&default_working_copy_factories(),
)
.expect("Failed to reload the workspace");
let tree = test_workspace.snapshot().unwrap();
let new_tree = test_workspace.snapshot().unwrap();
assert_tree_eq!(new_tree, tree, "The working copy should be clean.");
let merge_commit = commit_with_tree(test_workspace.repo.store(), tree);
test_workspace.workspace = Workspace::load(
&no_eol_conversion_settings,
test_workspace.workspace.workspace_root(),
&StoreFactories::default(),
&default_working_copy_factories(),
)
.expect("Failed to reload the workspace");
test_workspace
.workspace
.check_out(
test_workspace.repo.op_id().clone(),
None,
&test_workspace.workspace.repo_loader().store().root_commit(),
)
.block_on()
.unwrap();
let merge_commit = test_workspace
.workspace
.repo_loader()
.store()
.get_commit(merge_commit.id())
.expect("Failed to find the commit with the test file");
test_workspace
.workspace
.check_out(test_workspace.repo.op_id().clone(), None, &merge_commit)
.block_on()
.unwrap();
assert!(std::fs::exists(&file_disk_path).unwrap());
std::fs::read(&file_disk_path).unwrap()
}
#[test]
fn test_eol_conversion_input_output_snapshot_conflicts() -> TestResult {
let contents =
create_conflict_snapshot_and_read(r#"working-copy.eol-conversion = "input-output""#);
for line in contents.lines_with_terminator() {
assert!(
!line.ends_with(b"\r\n"),
"{:?} should not end with CRLF",
line.to_str_lossy().as_ref()
);
}
Ok(())
}
#[test]
fn test_eol_conversion_input_snapshot_conflicts() -> TestResult {
let contents = create_conflict_snapshot_and_read(r#"working-copy.eol-conversion = "input""#);
for line in contents.lines_with_terminator() {
assert!(
!line.ends_with(b"\r\n"),
"{:?} should not end with CRLF",
line.to_str_lossy().as_ref()
);
}
Ok(())
}
#[test]
fn test_eol_conversion_none_snapshot_conflicts() -> TestResult {
let contents = create_conflict_snapshot_and_read(r#"working-copy.eol-conversion = "none""#);
let line = contents.lines_with_terminator().next_back().unwrap();
assert!(
line.ends_with(b"\r\n"),
"{:?} should end with CRLF",
line.to_str_lossy().as_ref()
);
Ok(())
}
struct UpdateConflictsTestConfig {
parent1_contents: &'static str,
parent2_contents: &'static str,
extra_setting: &'static str,
expected_eol: &'static str,
expected_conflict_side1: &'static str,
expected_conflict_side2: &'static str,
}
#[test_case(UpdateConflictsTestConfig {
parent1_contents: "a\n",
parent2_contents: "b\n",
extra_setting: r#"working-copy.eol-conversion = "none""#,
expected_eol: "\n",
expected_conflict_side1: "a\n",
expected_conflict_side2: "b\n",
}; "LF parents with none settings")]
#[test_case(UpdateConflictsTestConfig {
parent1_contents: "a\n",
parent2_contents: "b\n",
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
expected_eol: "\r\n",
expected_conflict_side1: "a\r\n",
expected_conflict_side2: "b\r\n",
}; "LF parents with input-output settings")]
#[test_case(UpdateConflictsTestConfig {
parent1_contents: "a\r\n",
parent2_contents: "b\r\n",
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
expected_eol: "\r\n",
expected_conflict_side1: "a\r\n",
expected_conflict_side2: "b\r\n",
}; "CRLF parents with input-output settings")]
fn test_eol_conversion_update_conflicts(
UpdateConflictsTestConfig {
parent1_contents,
parent2_contents,
extra_setting,
expected_eol,
expected_conflict_side1,
expected_conflict_side2,
}: UpdateConflictsTestConfig,
) -> TestResult {
let extra_setting = format!("{extra_setting}\n");
let user_settings = base_user_settings_with_extra_configs(&extra_setting);
let mut test_workspace =
TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &user_settings);
let file_repo_path = repo_path("test-eol-file");
let file_disk_path = file_repo_path.to_fs_path(test_workspace.workspace.workspace_root())?;
let root_commit = test_workspace.repo.store().root_commit();
let mut tx = test_workspace.repo.start_transaction();
let tree = testutils::create_tree(&test_workspace.repo, &[(file_repo_path, parent1_contents)]);
let parent1_commit = tx
.repo_mut()
.new_commit(vec![root_commit.id().clone()], tree)
.write_unwrap();
let tree = testutils::create_tree(&test_workspace.repo, &[(file_repo_path, parent2_contents)]);
let parent2_commit = tx
.repo_mut()
.new_commit(vec![root_commit.id().clone()], tree)
.write_unwrap();
tx.commit("commit parent 2").block_on()?;
test_workspace.repo = test_workspace.repo.reload_at_head().block_on()?;
let tree =
merge_commit_trees(&*test_workspace.repo, &[parent1_commit, parent2_commit]).block_on()?;
let merge_commit = commit_with_tree(test_workspace.repo.store(), tree);
test_workspace
.workspace
.check_out(test_workspace.repo.op_id().clone(), None, &merge_commit)
.block_on()?;
let contents = std::fs::read(&file_disk_path)?;
for line in contents.lines_with_terminator() {
assert!(
line.ends_with_str(expected_eol),
"{:?} should end with {:?}",
&*line.to_str_lossy(),
expected_eol
);
}
let hunks =
jj_lib::conflicts::parse_conflict(&contents, 2, jj_lib::conflicts::MIN_CONFLICT_MARKER_LEN)
.unwrap();
let hunk = &hunks[0];
assert!(!hunk.is_resolved());
let sides = hunk.iter().collect::<Vec<_>>();
assert_eq!(sides[0], expected_conflict_side1);
assert_eq!(sides[2], expected_conflict_side2);
Ok(())
}
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: LF_FILE_CONTENT,
} => CRLF_FILE_CONTENT; "eol-conversion input-output LF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: CRLF_FILE_CONTENT,
} => CRLF_FILE_CONTENT; "eol-conversion input-output CRLF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: MIXED_EOL_FILE_CONTENT,
} => CRLF_FILE_CONTENT; "eol-conversion input-output mixed EOL file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input-output""#,
file_content: BINARY_FILE_CONTENT,
} => BINARY_FILE_CONTENT; "eol-conversion input-output binary file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: LF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion input LF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: CRLF_FILE_CONTENT,
} => CRLF_FILE_CONTENT; "eol-conversion input CRLF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: MIXED_EOL_FILE_CONTENT,
} => MIXED_EOL_FILE_CONTENT; "eol-conversion input mixed EOL file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "input""#,
file_content: BINARY_FILE_CONTENT,
} => BINARY_FILE_CONTENT; "eol-conversion input binary file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: LF_FILE_CONTENT,
} => LF_FILE_CONTENT; "eol-conversion none LF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: CRLF_FILE_CONTENT,
} => CRLF_FILE_CONTENT; "eol-conversion none CRLF only file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: MIXED_EOL_FILE_CONTENT,
} => MIXED_EOL_FILE_CONTENT; "eol-conversion none mixed EOL file")]
#[test_case(Config {
extra_setting: r#"working-copy.eol-conversion = "none""#,
file_content: BINARY_FILE_CONTENT,
} => BINARY_FILE_CONTENT; "eol-conversion none binary file")]
fn test_eol_conversion_checkout(
Config {
extra_setting,
file_content,
}: Config,
) -> Vec<u8> {
let no_eol_conversion_settings =
base_user_settings_with_extra_configs("working-copy.eol-conversion = \"none\"\n");
let mut test_workspace = TestWorkspace::init_with_backend_and_settings(
TestRepoBackend::Git,
&no_eol_conversion_settings,
);
let file_repo_path = repo_path("test-eol-file");
let file_disk_path = file_repo_path
.to_fs_path(test_workspace.workspace.workspace_root())
.unwrap();
testutils::write_working_copy_file(
test_workspace.workspace.workspace_root(),
file_repo_path,
file_content,
);
let tree = test_workspace.snapshot().unwrap();
let commit = commit_with_tree(test_workspace.repo.store(), tree);
test_workspace
.workspace
.check_out(
test_workspace.repo.op_id().clone(),
None,
&test_workspace.workspace.repo_loader().store().root_commit(),
)
.block_on()
.unwrap();
assert!(!std::fs::exists(&file_disk_path).unwrap());
let extra_setting = format!("{extra_setting}\n");
let user_settings = base_user_settings_with_extra_configs(&extra_setting);
test_workspace.workspace = Workspace::load(
&user_settings,
test_workspace.workspace.workspace_root(),
&StoreFactories::default(),
&default_working_copy_factories(),
)
.expect("Failed to reload the workspace");
let commit = test_workspace
.workspace
.repo_loader()
.store()
.get_commit(commit.id())
.expect("Failed to find the commit with the test file");
test_workspace
.workspace
.check_out(test_workspace.repo.op_id().clone(), None, &commit)
.block_on()
.unwrap();
assert!(std::fs::exists(&file_disk_path).unwrap());
std::fs::read(&file_disk_path).unwrap()
}