use git2::{BranchType, Repository}; use scopetime::scope_time; use crate::{ error::{Error, Result}, sync::repository::repo, }; use super::{CommitId, RepoPath}; /// rebase current HEAD on `branch` pub fn rebase_branch( repo_path: &RepoPath, branch: &str, branch_type: BranchType, ) -> Result { scope_time!("rebase_branch"); let repo = repo(repo_path)?; rebase_branch_repo(&repo, branch, branch_type) } fn rebase_branch_repo( repo: &Repository, branch_name: &str, branch_type: BranchType, ) -> Result { let branch = repo.find_branch(branch_name, branch_type)?; let annotated = repo.reference_to_annotated_commit(&branch.into_reference())?; rebase(repo, &annotated) } /// rebase attempt which aborts and undo's rebase if any conflict appears pub fn conflict_free_rebase( repo: &git2::Repository, commit: &git2::AnnotatedCommit, ) -> Result { let mut rebase = repo.rebase(None, Some(commit), None, None)?; let signature = crate::sync::commit::signature_allow_undefined_name(repo)?; let mut last_commit = None; while let Some(op) = rebase.next() { let _op = op?; if repo.index()?.has_conflicts() { rebase.abort()?; return Err(Error::RebaseConflict); } let c = rebase.commit(None, &signature, None)?; last_commit = Some(CommitId::from(c)); } if repo.index()?.has_conflicts() { rebase.abort()?; return Err(Error::RebaseConflict); } rebase.finish(Some(&signature))?; last_commit.ok_or_else(|| { Error::Generic(String::from("no commit rebased")) }) } /// #[derive(PartialEq, Eq, Debug)] pub enum RebaseState { /// Finished, /// Conflicted, } /// rebase pub fn rebase( repo: &git2::Repository, commit: &git2::AnnotatedCommit, ) -> Result { let mut rebase = repo.rebase(None, Some(commit), None, None)?; let signature = crate::sync::commit::signature_allow_undefined_name(repo)?; while let Some(op) = rebase.next() { let _op = op?; // dbg!(op.id()); if repo.index()?.has_conflicts() { return Ok(RebaseState::Conflicted); } rebase.commit(None, &signature, None)?; } if repo.index()?.has_conflicts() { return Ok(RebaseState::Conflicted); } rebase.finish(Some(&signature))?; Ok(RebaseState::Finished) } /// continue pending rebase pub fn continue_rebase( repo: &git2::Repository, ) -> Result { let mut rebase = repo.open_rebase(None)?; let signature = crate::sync::commit::signature_allow_undefined_name(repo)?; if repo.index()?.has_conflicts() { return Ok(RebaseState::Conflicted); } // try commit current rebase step if !repo.index()?.is_empty() { rebase.commit(None, &signature, None)?; } while let Some(op) = rebase.next() { let _op = op?; // dbg!(op.id()); if repo.index()?.has_conflicts() { return Ok(RebaseState::Conflicted); } rebase.commit(None, &signature, None)?; } if repo.index()?.has_conflicts() { return Ok(RebaseState::Conflicted); } rebase.finish(Some(&signature))?; Ok(RebaseState::Finished) } /// #[derive(PartialEq, Eq, Debug)] pub struct RebaseProgress { /// pub steps: usize, /// pub current: usize, /// pub current_commit: Option, } /// pub fn get_rebase_progress( repo: &git2::Repository, ) -> Result { let mut rebase = repo.open_rebase(None)?; let current_commit: Option = rebase .operation_current() .and_then(|idx| rebase.nth(idx)) .map(|op| op.id().into()); let progress = RebaseProgress { steps: rebase.len(), current: rebase.operation_current().unwrap_or_default(), current_commit, }; Ok(progress) } /// pub fn abort_rebase(repo: &git2::Repository) -> Result<()> { let mut rebase = repo.open_rebase(None)?; rebase.abort()?; Ok(()) } #[cfg(test)] mod test_conflict_free_rebase { use crate::sync::{ checkout_branch, create_branch, rebase::{rebase_branch, RebaseState}, repo_state, repository::repo, tests::{repo_init, write_commit_file}, CommitId, RepoPath, RepoState, }; use git2::{BranchType, Repository}; use super::conflict_free_rebase; fn parent_ids(repo: &Repository, c: CommitId) -> Vec { let foo = repo .find_commit(c.into()) .unwrap() .parent_ids() .map(CommitId::from) .collect(); foo } /// fn test_rebase_branch_repo( repo_path: &RepoPath, branch_name: &str, ) -> CommitId { let repo = repo(repo_path).unwrap(); let branch = repo.find_branch(branch_name, BranchType::Local).unwrap(); let annotated = repo .reference_to_annotated_commit(&branch.into_reference()) .unwrap(); conflict_free_rebase(&repo, &annotated).unwrap() } #[test] fn test_smoke() { let (_td, repo) = repo_init().unwrap(); let root = repo.path().parent().unwrap(); let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into(); let c1 = write_commit_file(&repo, "test1.txt", "test", "commit1"); create_branch(repo_path, "foo").unwrap(); let c2 = write_commit_file(&repo, "test2.txt", "test", "commit2"); assert_eq!(parent_ids(&repo, c2), vec![c1]); checkout_branch(repo_path, "master").unwrap(); let c3 = write_commit_file(&repo, "test3.txt", "test", "commit3"); checkout_branch(repo_path, "foo").unwrap(); let r = test_rebase_branch_repo(repo_path, "master"); assert_eq!(parent_ids(&repo, r), vec![c3]); } #[test] fn test_conflict() { let (_td, repo) = repo_init().unwrap(); let root = repo.path().parent().unwrap(); let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into(); write_commit_file(&repo, "test.txt", "test1", "commit1"); create_branch(repo_path, "foo").unwrap(); write_commit_file(&repo, "test.txt", "test2", "commit2"); checkout_branch(repo_path, "master").unwrap(); write_commit_file(&repo, "test.txt", "test3", "commit3"); checkout_branch(repo_path, "foo").unwrap(); let res = rebase_branch(repo_path, "master", BranchType::Local); assert!(matches!(res.unwrap(), RebaseState::Conflicted)); assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase); } } #[cfg(test)] mod test_rebase { use crate::sync::{ checkout_branch, create_branch, rebase::{ abort_rebase, get_rebase_progress, RebaseProgress, RebaseState, }, rebase_branch, repo_state, tests::{repo_init, write_commit_file}, RepoPath, RepoState, }; use git2::BranchType; #[test] fn test_conflicted_abort() { let (_td, repo) = repo_init().unwrap(); let root = repo.path().parent().unwrap(); let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into(); write_commit_file(&repo, "test.txt", "test1", "commit1"); create_branch(repo_path, "foo").unwrap(); let c = write_commit_file(&repo, "test.txt", "test2", "commit2"); checkout_branch(repo_path, "master").unwrap(); write_commit_file(&repo, "test.txt", "test3", "commit3"); checkout_branch(repo_path, "foo").unwrap(); assert!(get_rebase_progress(&repo).is_err()); // rebase let r = rebase_branch(repo_path, "master", BranchType::Local) .unwrap(); assert_eq!(r, RebaseState::Conflicted); assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase); assert_eq!( get_rebase_progress(&repo).unwrap(), RebaseProgress { current: 0, steps: 1, current_commit: Some(c) } ); // abort abort_rebase(&repo).unwrap(); assert_eq!(repo_state(repo_path).unwrap(), RepoState::Clean); } }