use crate::services::GitService;
use crate::app::rebase::{RebaseSession, RebaseEntry, RebaseAction, RebasePhase};
use crate::app::rebase::validation::{RebaseValidator, RebaseValidationError};
use crate::app::rebase::io::RebaseIO;
use std::path::Path;
#[derive(Debug)]
pub enum RebaseResult {
Success,
Conflicts { conflicted_files: Vec<String> },
Completed,
Aborted,
}
pub struct RebaseOperations;
pub struct RebaseRecovery;
impl RebaseRecovery {
pub fn detect_interrupted_rebase(repo_path: &str) -> Result<Option<RebaseRecoveryInfo>, RebaseValidationError> {
let git_dir = std::path::Path::new(repo_path).join(".git");
let rebase_merge_dir = git_dir.join("rebase-merge");
let rebase_apply_dir = git_dir.join("rebase-apply");
if rebase_merge_dir.exists() {
let todo_path = rebase_merge_dir.join("git-rebase-todo");
let done_path = rebase_merge_dir.join("done");
let current_commit = std::fs::read_to_string(rebase_merge_dir.join("head-name"))
.unwrap_or_else(|_| "unknown".to_string())
.trim()
.to_string();
let todo_lines = if todo_path.exists() {
std::fs::read_to_string(&todo_path)
.unwrap_or_default()
.lines()
.map(|s| s.to_string())
.collect()
} else {
Vec::new()
};
let done_lines = if done_path.exists() {
std::fs::read_to_string(&done_path)
.unwrap_or_default()
.lines()
.map(|s| s.to_string())
.collect()
} else {
Vec::new()
};
Ok(Some(RebaseRecoveryInfo {
recovery_type: RebaseRecoveryType::Interactive,
current_commit,
remaining_steps: todo_lines.len(),
completed_steps: done_lines.len(),
has_conflicts: Self::check_for_conflicts(repo_path)?,
}))
} else if rebase_apply_dir.exists() {
Ok(Some(RebaseRecoveryInfo {
recovery_type: RebaseRecoveryType::Apply,
current_commit: "unknown".to_string(),
remaining_steps: 0,
completed_steps: 0,
has_conflicts: Self::check_for_conflicts(repo_path)?,
}))
} else {
Ok(None)
}
}
fn check_for_conflicts(repo_path: &str) -> Result<bool, RebaseValidationError> {
match std::process::Command::new("git")
.args(&["status", "--porcelain"])
.current_dir(repo_path)
.output()
{
Ok(output) => {
let status = String::from_utf8_lossy(&output.stdout);
let has_conflicts = status.lines()
.any(|line| line.starts_with("U ") || line.starts_with("AA ") || line.starts_with("DD "));
Ok(has_conflicts)
}
Err(_) => Ok(false), }
}
pub fn continue_interrupted_rebase(
git_service: &GitService,
repo_path: &str,
session: &mut RebaseSession,
) -> Result<RebaseResult, RebaseValidationError> {
if let Some(recovery_info) = Self::detect_interrupted_rebase(repo_path)? {
if recovery_info.has_conflicts {
session.phase = RebasePhase::Conflict;
return Ok(RebaseResult::Conflicts {
conflicted_files: Vec::new() });
}
match recovery_info.recovery_type {
RebaseRecoveryType::Interactive => {
git_service
.rebase_continue(repo_path, None, None, None)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))?;
session.phase = RebasePhase::Active;
Ok(RebaseResult::Success)
}
RebaseRecoveryType::Apply => {
git_service
.rebase_continue(repo_path, None, None, None)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))?;
session.phase = RebasePhase::Active;
Ok(RebaseResult::Success)
}
}
} else {
Err(RebaseValidationError::InvalidRepositoryState("No interrupted rebase found".to_string()))
}
}
pub fn abort_interrupted_rebase(
git_service: &GitService,
repo_path: &str,
) -> Result<(), RebaseValidationError> {
git_service
.rebase_abort(repo_path)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))
}
}
#[derive(Debug, Clone)]
pub struct RebaseRecoveryInfo {
pub recovery_type: RebaseRecoveryType,
pub current_commit: String,
pub remaining_steps: usize,
pub completed_steps: usize,
pub has_conflicts: bool,
}
#[derive(Debug, Clone)]
pub enum RebaseRecoveryType {
Interactive,
Apply,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup_test_repo() -> (TempDir, String) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let repo_path = temp_dir.path().to_string();
let output = std::process::Command::new("git")
.args(&["init"])
.current_dir(&repo_path)
.output()
.expect("Failed to init git repo");
assert!(output.status.success(), "Git init failed");
std::process::Command::new("git")
.args(&["config", "user.name", "Test User"])
.current_dir(&repo_path)
.output()
.expect("Failed to configure git user");
std::process::Command::new("git")
.args(&["config", "user.email", "test@example.com"])
.current_dir(&repo_path)
.output()
.expect("Failed to configure git email");
fs::write(temp_dir.path().join("file.txt"), "initial content").unwrap();
std::process::Command::new("git")
.args(&["add", "file.txt"])
.current_dir(&repo_path)
.output()
.unwrap();
std::process::Command::new("git")
.args(&["commit", "-m", "Initial commit"])
.current_dir(&repo_path)
.output()
.unwrap();
(temp_dir, repo_path)
}
#[test]
fn test_start_planning_session() {
let commit_hashes = vec![
"abc123def456".to_string(),
"def456ghi789".to_string(),
"ghi789jkl012".to_string(),
];
let result = RebaseOperations::start_planning_session(
commit_hashes,
Some("HEAD~3".to_string()),
false,
);
assert!(result.is_ok());
let session = result.unwrap();
assert_eq!(session.phase, crate::RebasePhase::Planning);
assert_eq!(session.entries.len(), 3);
assert_eq!(session.base_commit, Some("HEAD~3".to_string()));
assert!(!session.use_root);
}
#[test]
fn test_recovery_detection_no_rebase() {
let (_temp_dir, repo_path) = setup_test_repo();
let result = RebaseRecovery::detect_interrupted_rebase(&repo_path);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_recovery_info_creation() {
let recovery_info = RebaseRecoveryInfo {
recovery_type: RebaseRecoveryType::Interactive,
current_commit: "feature-branch".to_string(),
remaining_steps: 5,
completed_steps: 2,
has_conflicts: true,
};
assert_eq!(recovery_info.recovery_type, RebaseRecoveryType::Interactive);
assert_eq!(recovery_info.current_commit, "feature-branch");
assert_eq!(recovery_info.remaining_steps, 5);
assert_eq!(recovery_info.completed_steps, 2);
assert!(recovery_info.has_conflicts);
}
#[test]
fn test_rebase_session_state_transitions() {
let mut session = RebaseSession::new();
assert_eq!(session.phase, RebasePhase::Planning);
session.phase = RebasePhase::Active;
assert_eq!(session.phase, RebasePhase::Active);
session.phase = RebasePhase::Conflict;
assert_eq!(session.phase, RebasePhase::Conflict);
session.phase = RebasePhase::Completed;
assert_eq!(session.phase, RebasePhase::Completed);
}
#[test]
fn test_rebase_entry_creation() {
let entry = crate::RebaseEntry::new(
"abc123def456".to_string(),
"Test commit message".to_string(),
crate::RebaseAction::Pick,
);
assert_eq!(entry.hash, "abc123def456");
assert_eq!(entry.message, "Test commit message");
assert_eq!(entry.action, crate::RebaseAction::Pick);
assert_eq!(entry.short_hash, "abc123"); }
#[test]
fn test_rebase_action_string_conversion() {
assert_eq!(crate::RebaseAction::Pick.to_str(), "pick");
assert_eq!(crate::RebaseAction::Reword.to_str(), "reword");
assert_eq!(crate::RebaseAction::Edit.to_str(), "edit");
assert_eq!(crate::RebaseAction::Squash.to_str(), "squash");
assert_eq!(crate::RebaseAction::Fixup.to_str(), "fixup");
assert_eq!(crate::RebaseAction::Drop.to_str(), "drop");
}
#[test]
fn test_rebase_session_default() {
let session = RebaseSession::default();
assert_eq!(session.phase, RebasePhase::Planning);
assert!(session.entries.is_empty());
assert_eq!(session.cursor, 0);
assert!(session.todo_path.is_none());
assert!(session.base_commit.is_none());
assert!(!session.use_root);
assert!(!session.dirty);
}
#[test]
fn test_rebase_validation_errors() {
use crate::validation::RebaseValidationError;
let error = RebaseValidationError::InvalidRepositoryState("test error".to_string());
match error {
RebaseValidationError::InvalidRepositoryState(msg) => assert_eq!(msg, "test error"),
_ => panic!("Wrong error type"),
}
let error = RebaseValidationError::SessionNotFound("test session".to_string());
match error {
RebaseValidationError::SessionNotFound(name) => assert_eq!(name, "test session"),
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_rebase_workflow_state_transitions() {
let mut session = RebaseSession::default();
assert_eq!(session.phase, RebasePhase::Planning);
session.phase = RebasePhase::Active;
assert_eq!(session.phase, RebasePhase::Active);
session.phase = RebasePhase::Conflict;
assert_eq!(session.phase, RebasePhase::Conflict);
session.phase = RebasePhase::Active;
assert_eq!(session.phase, RebasePhase::Active);
session.phase = RebasePhase::Completed;
assert_eq!(session.phase, RebasePhase::Completed);
session.phase = RebasePhase::Aborted;
assert_eq!(session.phase, RebasePhase::Aborted);
}
#[test]
fn test_rebase_session_with_entries() {
let mut session = RebaseSession::default();
session.entries.push(crate::RebaseEntry::new(
"abc123".to_string(),
"First commit".to_string(),
crate::RebaseAction::Pick,
));
session.entries.push(crate::RebaseEntry::new(
"def456".to_string(),
"Second commit".to_string(),
crate::RebaseAction::Reword,
));
assert_eq!(session.entries.len(), 2);
assert_eq!(session.entries[0].action, crate::RebaseAction::Pick);
assert_eq!(session.entries[1].action, crate::RebaseAction::Reword);
assert_eq!(session.cursor, 0);
session.cursor = 1;
assert_eq!(session.cursor, 1);
assert!(!session.dirty);
session.dirty = true;
assert!(session.dirty);
}
}
impl RebaseOperations {
pub fn start_planning_session(
commit_hashes: Vec<String>,
base_commit: Option<String>,
use_root: bool,
) -> Result<RebaseSession, RebaseValidationError> {
let mut session = RebaseSession::new();
session.phase = RebasePhase::Planning;
session.base_commit = base_commit;
session.use_root = use_root;
session.entries = commit_hashes
.into_iter()
.enumerate()
.map(|(i, hash)| {
RebaseEntry::new(
hash,
format!("Commit {}", i + 1), RebaseAction::Pick, )
})
.collect();
RebaseValidator::validate_session_entries(&session)?;
Ok(session)
}
pub fn execute_rebase(
git_service: &GitService,
repo_path: &str,
session: &mut RebaseSession,
) -> Result<RebaseResult, RebaseValidationError> {
RebaseValidator::validate_before_execution(git_service, repo_path, session)?;
let todo_content = RebaseIO::format_todo_content(&session.entries);
let result = git_service
.start_rebase_with_todo(
repo_path,
session.base_commit.as_deref().unwrap_or("HEAD~1"),
&todo_content,
session.use_root,
)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))?;
session.phase = RebasePhase::Active;
session.todo_path = Some(Path::new(&result).to_path_buf());
session.dirty = false;
match git_service.status_porcelain(repo_path) {
Ok(status) => {
let conflicted_files: Vec<String> = status
.lines()
.filter(|line| line.starts_with("U ") || line.starts_with("AA ") || line.starts_with("DD "))
.map(|line| line[3..].trim().to_string()) .collect();
if !conflicted_files.is_empty() {
session.phase = RebasePhase::Conflict;
Ok(RebaseResult::Conflicts { conflicted_files })
} else {
Ok(RebaseResult::Success)
}
}
Err(_) => {
Ok(RebaseResult::Success)
}
}
}
pub fn continue_rebase(
git_service: &GitService,
repo_path: &str,
session: &mut RebaseSession,
) -> Result<RebaseResult, RebaseValidationError> {
RebaseValidator::validate_session_state(session, "continue_rebase")?;
git_service
.rebase_continue(repo_path, None, None, None)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))?;
match git_service.status_porcelain(repo_path) {
Ok(status) => {
let conflicted_files: Vec<String> = status
.lines()
.filter(|line| line.starts_with("U ") || line.starts_with("AA ") || line.starts_with("DD "))
.map(|line| line[3..].trim().to_string())
.collect();
if !conflicted_files.is_empty() {
session.phase = RebasePhase::Conflict;
Ok(RebaseResult::Conflicts { conflicted_files })
} else {
Ok(RebaseResult::Success)
}
}
Err(_) => {
Ok(RebaseResult::Success)
}
}
}
pub fn skip_rebase_step(
git_service: &GitService,
repo_path: &str,
session: &mut RebaseSession,
) -> Result<RebaseResult, RebaseValidationError> {
RebaseValidator::validate_session_state(session, "continue_rebase")?;
git_service
.rebase_skip(repo_path)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))?;
match git_service.status_porcelain(repo_path) {
Ok(status) => {
let conflicted_files: Vec<String> = status
.lines()
.filter(|line| line.starts_with("U ") || line.starts_with("AA ") || line.starts_with("DD "))
.map(|line| line[3..].trim().to_string())
.collect();
if !conflicted_files.is_empty() {
session.phase = RebasePhase::Conflict;
Ok(RebaseResult::Conflicts { conflicted_files })
} else {
Ok(RebaseResult::Success)
}
}
Err(_) => {
Ok(RebaseResult::Success)
}
}
}
pub fn abort_rebase(
git_service: &GitService,
repo_path: &str,
session: &mut RebaseSession,
) -> Result<RebaseResult, RebaseValidationError> {
RebaseValidator::validate_session_state(session, "abort_rebase")?;
git_service
.rebase_abort(repo_path)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(e.to_string()))?;
session.phase = RebasePhase::Planning;
session.entries.clear();
session.todo_path = None;
session.dirty = false;
Ok(RebaseResult::Aborted)
}
pub fn save_todo_changes(
session: &mut RebaseSession,
) -> Result<(), RebaseValidationError> {
if let Some(todo_path) = &session.todo_path {
let todo_content = RebaseIO::format_todo_content(&session.entries);
RebaseIO::write_todo_atomically(todo_path, &todo_content)
.map_err(|e| RebaseValidationError::InvalidRepositoryState(
format!("Failed to save todo file: {}", e)
))?;
session.dirty = false;
}
Ok(())
}
}