#[cfg(any(test, feature = "test-utils"))]
pub fn validate_rebase_preconditions(
executor: &dyn crate::executor::ProcessExecutor,
) -> io::Result<()> {
let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
validate_git_state()?;
if let Some(concurrent_op) = detect_concurrent_git_operations()? {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Cannot start rebase: {} already in progress. \
Please complete or abort the current operation first.",
concurrent_op.description()
),
));
}
let config = repo.config().map_err(|e| git2_to_io_error(&e))?;
let user_name = config.get_string("user.name");
let user_email = config.get_string("user.email");
if user_name.is_err() && user_email.is_err() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Git identity is not configured. Please set user.name and user.email:\n \
git config --global user.name \"Your Name\"\n \
git config --global user.email \"you@example.com\"",
));
}
let status_output = executor.execute("git", &["status", "--porcelain"], &[], None)?;
if status_output.status.success() {
let stdout = status_output.stdout.trim();
if !stdout.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Working tree is not clean. Please commit or stash changes before rebasing.",
));
}
} else {
let statuses = repo.statuses(None).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to check working tree status: {e}"),
)
})?;
if !statuses.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Working tree is not clean. Please commit or stash changes before rebasing.",
));
}
}
check_shallow_clone()?;
check_worktree_conflicts()?;
check_submodule_state()?;
check_sparse_checkout_state()?;
Ok(())
}
#[cfg(any(test, feature = "test-utils"))]
fn check_shallow_clone() -> io::Result<()> {
use std::fs;
let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
let git_dir = repo.path();
let shallow_file = git_dir.join("shallow");
if shallow_file.exists() {
let content = fs::read_to_string(&shallow_file).unwrap_or_default();
let line_count = content.lines().count();
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Repository is a shallow clone with {line_count} commits. \
Rebasing may fail due to missing history. \
Consider running: git fetch --unshallow"
),
));
}
Ok(())
}
#[cfg(any(test, feature = "test-utils"))]
fn check_worktree_conflicts() -> io::Result<()> {
use std::fs;
let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
let head = repo.head().map_err(|e| git2_to_io_error(&e))?;
let branch_name = match head.shorthand() {
Some(name) if head.is_branch() => name,
_ => return Ok(()), };
let git_dir = repo.path();
let worktrees_dir = git_dir.join("worktrees");
if !worktrees_dir.exists() {
return Ok(());
}
let entries = fs::read_dir(&worktrees_dir).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to read worktrees directory: {e}"),
)
})?;
let result: io::Result<Option<String>> = entries.flatten().try_fold(None, |acc, entry| {
if acc.is_some() {
return Ok(acc);
}
let worktree_path = entry.path();
let worktree_head = worktree_path.join("HEAD");
if worktree_head.exists() {
if let Ok(content) = fs::read_to_string(&worktree_head) {
if content.contains(&format!("refs/heads/{branch_name}")) {
let worktree_name = worktree_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
return Ok(Some(worktree_name.to_string()));
}
}
}
Ok(acc)
});
if let Ok(Some(worktree_name)) = result {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Branch '{branch_name}' is already checked out in worktree '{worktree_name}'. \
Use 'git worktree add' to create a new worktree for this branch."
),
));
}
Ok(())
}
#[cfg(any(test, feature = "test-utils"))]
fn check_submodule_state() -> io::Result<()> {
use std::fs;
let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
let git_dir = repo.path();
let workdir = repo.workdir().unwrap_or(git_dir);
let gitmodules_path = workdir.join(".gitmodules");
if !gitmodules_path.exists() {
return Ok(()); }
let modules_dir = git_dir.join("modules");
if !modules_dir.exists() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Submodules are not initialized. Run: git submodule update --init --recursive",
));
}
let gitmodules_content = fs::read_to_string(&gitmodules_path).unwrap_or_default();
let submodule_count = gitmodules_content.matches("path = ").count();
if submodule_count > 0 {
gitmodules_content.lines().try_for_each(|line| {
if line.contains("path = ") {
if let Some(path) = line.split("path = ").nth(1) {
let submodule_path = workdir.join(path.trim());
if !submodule_path.exists() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Submodule '{}' is not initialized. Run: git submodule update --init --recursive",
path.trim()
),
));
}
}
}
Ok(())
})?;
}
Ok(())
}
#[cfg(any(test, feature = "test-utils"))]
fn check_sparse_checkout_state() -> io::Result<()> {
use std::fs;
let repo = git2::Repository::discover(".").map_err(|e| git2_to_io_error(&e))?;
let git_dir = repo.path();
let config = repo.config().map_err(|e| git2_to_io_error(&e))?;
let sparse_checkout = config.get_bool("core.sparseCheckout");
let sparse_checkout_cone = config.get_bool("extensions.sparseCheckoutCone");
match (sparse_checkout, sparse_checkout_cone) {
(Ok(true), _) | (_, Ok(true)) => {
let info_sparse_dir = git_dir.join("info").join("sparse-checkout");
if !info_sparse_dir.exists() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Sparse checkout is enabled but not configured. \
Run: git sparse-checkout init",
));
}
if let Ok(content) = fs::read_to_string(&info_sparse_dir) {
if content.trim().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Sparse checkout configuration is empty. \
Run: git sparse-checkout set <patterns>",
));
}
}
}
_ => {
}
}
Ok(())
}