use std::io;
use camino::Utf8PathBuf;
use cap_std::{ambient_authority, fs_utf8::Dir};
use rstest::rstest;
use super::{
ConflictGuard,
conflict::{
ConflictMarkerState,
RepositoryOperationState,
has_conflict_markers,
marker_state,
refuses,
},
repository_state::{marker_present, opened_directory, operation_in_progress},
};
fn git_dir_fixture() -> io::Result<(tempfile::TempDir, Utf8PathBuf, Dir)> {
let temporary = tempfile::tempdir()?;
let git_dir = Utf8PathBuf::from_path_buf(temporary.path().to_path_buf()).map_err(|path| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("non-UTF-8 temporary path: {}", path.display()),
)
})?;
let directory = Dir::open_ambient_dir(&git_dir, ambient_authority())?;
Ok((temporary, git_dir, directory))
}
const CONFLICTED: &str = "<<<<<<< HEAD\n| A | B |\n=======\n| A | C |\n>>>>>>> side\n";
#[rstest]
#[case(CONFLICTED, true)]
#[case("<<<<<<< HEAD\n=======\n>>>>>>> side\n", true)]
#[case("<<<<<<< HEAD\n| A |\n=======\n| B |\n", false)]
#[case("<<<<<<< HEAD\n| A |\n>>>>>>> side\n", false)]
#[case("| A |\n=======\n| B |\n>>>>>>> side\n", false)]
#[case("<<<<<<<< HEAD\n=======\n>>>>>>> side\n", true)]
#[case("<<<<<<<<<<<< HEAD\n============\n>>>>>>>>>>>> side\n", true)]
#[case(" <<<<<<< HEAD\n=======\n>>>>>>> side\n", false)]
#[case("<<<<<<< HEAD\n =======\n>>>>>>> side\n", false)]
#[case("```\n<<<<<<< HEAD\n```\n", false)]
fn all_three_markers_are_required_at_the_start_of_a_line(
#[case] content: &str,
#[case] expected: bool,
) {
assert_eq!(has_conflict_markers(content), expected, "{content:?}");
}
#[test]
fn a_document_about_conflict_markers_is_not_a_conflicted_one() {
let prose = concat!(
"Resolving conflicts\n=======\n\nGit writes `<<<<<<< HEAD` where the first side ",
"begins and `>>>>>>>` at the end.\n"
);
assert!(!has_conflict_markers(prose), "{prose:?}");
}
#[test]
fn a_fenced_example_carrying_all_three_markers_still_counts() {
let fenced = "```text\n<<<<<<< HEAD\n=======\n>>>>>>> side\n```\n";
assert!(has_conflict_markers(fenced));
}
#[derive(Debug, Clone, Copy)]
enum Guarding {
Nothing,
Idle,
MidOperation,
}
#[rstest]
#[case(Guarding::Nothing, true, false)]
#[case(Guarding::Nothing, false, false)]
#[case(Guarding::Idle, true, false)]
#[case(Guarding::Idle, false, false)]
#[case(Guarding::MidOperation, true, true)]
#[case(Guarding::MidOperation, false, false)]
fn a_conflicted_file_is_refused_only_mid_operation_and_without_the_override(
#[case] guarding: Guarding,
#[case] conflicted: bool,
#[case] expected: bool,
) {
let (_temporary, git_dir, directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
if matches!(guarding, Guarding::MidOperation) {
directory
.write("MERGE_HEAD", "")
.expect("create the marker");
}
let guard = match guarding {
Guarding::Nothing => ConflictGuard::unguarded(),
Guarding::Idle | Guarding::MidOperation => ConflictGuard::guarded(&git_dir),
};
let content = if conflicted {
CONFLICTED
} else {
"| A | B |\n"
};
let markers = marker_state(content);
let state = if markers == ConflictMarkerState::Present {
guard.operation_state().expect("read the repository")
} else {
RepositoryOperationState::Idle
};
assert_eq!(
refuses(markers, state),
expected,
"guarding={guarding:?} conflicted={conflicted}"
);
}
#[test]
fn the_unguarded_run_refuses_nothing() {
let refused = refuses(
marker_state(CONFLICTED),
ConflictGuard::unguarded()
.operation_state()
.expect("the unguarded guard asks nothing"),
);
assert!(!refused);
}
#[test]
fn a_document_without_markers_never_asks_the_repository() {
let (_temporary, root, directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
directory
.write("not-a-directory", "")
.expect("create a file where a Git directory was expected");
let git_dir = root.join("not-a-directory");
let guard = ConflictGuard::guarded(&git_dir);
let markers = marker_state("| A | B |\n");
let state = if markers == ConflictMarkerState::Present {
guard
.operation_state()
.expect("a document without markers must not ask the repository")
} else {
RepositoryOperationState::Idle
};
let refused = refuses(markers, state);
assert!(!refused);
}
#[derive(Debug, Clone, Copy)]
enum Marker {
File(&'static str),
Directory(&'static str),
}
#[rstest]
#[case(Marker::File("MERGE_HEAD"))]
#[case(Marker::File("REVERT_HEAD"))]
#[case(Marker::File("CHERRY_PICK_HEAD"))]
#[case(Marker::Directory("rebase-merge"))]
#[case(Marker::Directory("rebase-apply"))]
fn an_in_progress_operation_is_detected(#[case] marker: Marker) {
let (_temporary, git_dir, directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
assert!(
operation_in_progress(&git_dir).expect("read the idle fixture")
== RepositoryOperationState::Idle,
"the fixture must start idle"
);
match marker {
Marker::File(name) => directory.write(name, ""),
Marker::Directory(name) => directory.create_dir_all(name),
}
.expect("create the marker");
assert!(
operation_in_progress(&git_dir).expect("read the marked fixture")
== RepositoryOperationState::InProgress,
"{marker:?} must signal an operation in progress"
);
}
#[test]
fn an_idle_git_directory_is_not_mid_operation() {
let (_temporary, git_dir, directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
directory
.create_dir_all("objects")
.expect("create an object store");
directory
.create_dir_all("refs")
.expect("create a ref store");
for name in ["HEAD", "ORIG_HEAD", "SQUASH_MSG", "COMMIT_EDITMSG"] {
directory
.write(name, "")
.expect("create a file an idle repository holds");
}
assert!(
operation_in_progress(&git_dir).expect("read the idle repository")
== RepositoryOperationState::Idle,
"a directory holding the entries an idle repository holds is not mid-operation"
);
}
#[test]
fn an_unreadable_repository_is_an_error_rather_than_an_answer() {
let (_temporary, root, directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
directory
.write("not-a-directory", "")
.expect("create a file where a Git directory was expected");
let git_dir = root.join("not-a-directory");
let error = operation_in_progress(&git_dir).expect_err("a file is not a Git directory");
assert_eq!(error.git_dir, git_dir);
assert_eq!(
error.source.kind(),
std::io::ErrorKind::NotADirectory,
"an unanswered question must not be reported as absence"
);
}
#[test]
fn a_git_directory_that_is_not_a_directory_is_reported() {
let (_temporary, root, directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
directory
.write("not-a-directory", "")
.expect("create a file where a Git directory was expected");
let git_dir = root.join("not-a-directory");
let metadata = directory
.metadata("not-a-directory")
.expect("read the fixture's metadata");
let error = opened_directory(&git_dir, &metadata).expect_err("a file is not a directory");
assert_eq!(
error.git_dir, git_dir,
"the failure names the directory it was asked about"
);
assert_eq!(
error.source.kind(),
std::io::ErrorKind::NotADirectory,
"the kind Unix reports for it, reported on every platform"
);
}
#[rstest]
#[case(Ok(()), Some(true))]
#[case(Err(io::Error::from(io::ErrorKind::NotFound)), Some(false))]
#[case(Err(io::Error::from(io::ErrorKind::PermissionDenied)), None)]
fn an_unreadable_marker_is_an_unanswered_question_rather_than_an_absent_one(
#[case] test: io::Result<()>,
#[case] expected: Option<bool>,
) {
match (marker_present(test), expected) {
(Ok(present), Some(expected)) => assert_eq!(present, expected),
(Err(error), None) => assert_eq!(error.kind(), io::ErrorKind::PermissionDenied),
(outcome, _) => panic!("the marker test was read as {outcome:?}"),
}
}
#[test]
fn a_git_directory_that_has_gone_is_reported_rather_than_answered() {
let (_temporary, root, _directory) =
git_dir_fixture().expect("create a UTF-8 Git directory fixture");
let gone = root.join("gone");
let error = operation_in_progress(&gone).expect_err("a Git directory that is not there");
assert_eq!(
error.git_dir, gone,
"the failure names the directory it could not open"
);
assert_eq!(
error.source.kind(),
std::io::ErrorKind::NotFound,
"the cause is the absence of the directory itself"
);
}