diff --git a/asyncgit/src/sync/branch/merge.rs b/asyncgit/src/sync/branch/merge.rs new file mode 100644 index 00000000..3f3db70a --- /dev/null +++ b/asyncgit/src/sync/branch/merge.rs @@ -0,0 +1,162 @@ +//! merging from upstream + +use super::BranchType; +use crate::{ + error::{Error, Result}, + sync::utils, +}; +use scopetime::scope_time; + +/// +pub fn branch_merge_upstream_fastforward( + repo_path: &str, + branch: &str, +) -> Result<()> { + scope_time!("branch_merge_upstream"); + + let repo = utils::repo(repo_path)?; + + let branch = repo.find_branch(branch, BranchType::Local)?; + let upstream = branch.upstream()?; + + let upstream_commit = + upstream.into_reference().peel_to_commit()?; + + let annotated = + repo.find_annotated_commit(upstream_commit.id())?; + + let (analysis, _) = repo.merge_analysis(&[&annotated])?; + + if !analysis.is_fast_forward() { + return Err(Error::Generic( + "fast forward merge not possible".into(), + )); + } + + if analysis.is_unborn() { + return Err(Error::Generic("head is unborn".into())); + } + + repo.checkout_tree(upstream_commit.as_object(), None)?; + + repo.head()?.set_target(annotated.id(), "")?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::sync::{ + commit, fetch_origin, + remotes::push::push, + stage_add_file, + tests::{ + debug_cmd_print, get_commit_ids, repo_clone, + repo_init_bare, + }, + CommitId, + }; + use git2::Repository; + use std::{fs::File, io::Write, path::Path}; + + // write, stage and commit a file + fn write_commit_file( + repo: &Repository, + file: &str, + content: &str, + commit_name: &str, + ) -> CommitId { + File::create( + repo.workdir().unwrap().join(file).to_str().unwrap(), + ) + .unwrap() + .write_all(content.as_bytes()) + .unwrap(); + + stage_add_file( + repo.workdir().unwrap().to_str().unwrap(), + Path::new(file), + ) + .unwrap(); + + commit(repo.workdir().unwrap().to_str().unwrap(), commit_name) + .unwrap() + } + + #[test] + fn test_merge() { + let (r1_dir, _repo) = repo_init_bare().unwrap(); + + let (clone1_dir, clone1) = + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); + + let (clone2_dir, clone2) = + repo_clone(r1_dir.path().to_str().unwrap()).unwrap(); + + // clone1 + + let commit1 = + write_commit_file(&clone1, "test.txt", "test", "commit1"); + + push( + clone1_dir.path().to_str().unwrap(), + "origin", + "master", + false, + None, + None, + ) + .unwrap(); + + // clone2 + debug_cmd_print( + clone2_dir.path().to_str().unwrap(), + "git pull --ff", + ); + + let commit2 = write_commit_file( + &clone2, + "test2.txt", + "test", + "commit2", + ); + + push( + clone2_dir.path().to_str().unwrap(), + "origin", + "master", + false, + None, + None, + ) + .unwrap(); + + // clone1 again + + let bytes = fetch_origin( + clone1_dir.path().to_str().unwrap(), + "master", + ) + .unwrap(); + assert!(bytes > 0); + + let bytes = fetch_origin( + clone1_dir.path().to_str().unwrap(), + "master", + ) + .unwrap(); + assert_eq!(bytes, 0); + + branch_merge_upstream_fastforward( + clone1_dir.path().to_str().unwrap(), + "master", + ) + .unwrap(); + + let commits = get_commit_ids(&clone1, 10); + assert_eq!(commits.len(), 2); + assert_eq!(commits[1], commit1); + assert_eq!(commits[0], commit2); + } +} diff --git a/asyncgit/src/sync/branch.rs b/asyncgit/src/sync/branch/mod.rs similarity index 87% rename from asyncgit/src/sync/branch.rs rename to asyncgit/src/sync/branch/mod.rs index 6e8615e2..5bf405a0 100644 --- a/asyncgit/src/sync/branch.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -1,4 +1,7 @@ -//! +//! branch functions + +pub mod merge; +pub mod rename; use super::{ remotes::get_default_remote_in_repo, utils::bytes2string, @@ -182,22 +185,6 @@ pub fn delete_branch( Ok(()) } -/// Rename the branch reference -pub fn rename_branch( - repo_path: &str, - branch_ref: &str, - new_name: &str, -) -> Result<()> { - scope_time!("delete_branch"); - - let repo = utils::repo(repo_path)?; - let branch_as_ref = repo.find_reference(branch_ref)?; - let mut branch = git2::Branch::wrap(branch_as_ref); - branch.rename(new_name, true)?; - - Ok(()) -} - /// creates a new branch pointing to current HEAD commit and updating HEAD to new branch pub fn create_branch(repo_path: &str, name: &str) -> Result<()> { scope_time!("create_branch"); @@ -404,49 +391,3 @@ mod test_delete_branch { ); } } - -#[cfg(test)] -mod test_rename_branch { - use super::*; - use crate::sync::tests::repo_init; - - #[test] - fn test_rename_branch() { - let (_td, repo) = repo_init().unwrap(); - let root = repo.path().parent().unwrap(); - let repo_path = root.as_os_str().to_str().unwrap(); - - create_branch(repo_path, "branch1").unwrap(); - - checkout_branch(repo_path, "refs/heads/branch1").unwrap(); - - assert_eq!( - repo.branches(None) - .unwrap() - .nth(0) - .unwrap() - .unwrap() - .0 - .name() - .unwrap() - .unwrap(), - "branch1" - ); - - rename_branch(repo_path, "refs/heads/branch1", "AnotherName") - .unwrap(); - - assert_eq!( - repo.branches(None) - .unwrap() - .nth(0) - .unwrap() - .unwrap() - .0 - .name() - .unwrap() - .unwrap(), - "AnotherName" - ); - } -} diff --git a/asyncgit/src/sync/branch/rename.rs b/asyncgit/src/sync/branch/rename.rs new file mode 100644 index 00000000..4bb369c5 --- /dev/null +++ b/asyncgit/src/sync/branch/rename.rs @@ -0,0 +1,67 @@ +//! renaming of branches + +use crate::{error::Result, sync::utils}; +use scopetime::scope_time; + +/// Rename the branch reference +pub fn rename_branch( + repo_path: &str, + branch_ref: &str, + new_name: &str, +) -> Result<()> { + scope_time!("delete_branch"); + + let repo = utils::repo(repo_path)?; + let branch_as_ref = repo.find_reference(branch_ref)?; + let mut branch = git2::Branch::wrap(branch_as_ref); + branch.rename(new_name, true)?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::super::*; + use super::rename_branch; + use crate::sync::tests::repo_init; + + #[test] + fn test_rename_branch() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + create_branch(repo_path, "branch1").unwrap(); + + checkout_branch(repo_path, "refs/heads/branch1").unwrap(); + + assert_eq!( + repo.branches(None) + .unwrap() + .nth(0) + .unwrap() + .unwrap() + .0 + .name() + .unwrap() + .unwrap(), + "branch1" + ); + + rename_branch(repo_path, "refs/heads/branch1", "AnotherName") + .unwrap(); + + assert_eq!( + repo.branches(None) + .unwrap() + .nth(0) + .unwrap() + .unwrap() + .0 + .name() + .unwrap() + .unwrap(), + "AnotherName" + ); + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index cdbc8d06..c188fbff 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -17,14 +17,16 @@ mod logwalker; pub mod remotes; mod reset; mod stash; +mod state; pub mod status; mod tags; pub mod utils; pub use branch::{ branch_compare_upstream, checkout_branch, create_branch, - delete_branch, get_branches_info, rename_branch, BranchCompare, - BranchInfo, + delete_branch, get_branches_info, + merge::branch_merge_upstream_fastforward, rename::rename_branch, + BranchCompare, BranchInfo, }; pub use commit::{amend, commit, tag}; pub use commit_details::{ @@ -42,6 +44,7 @@ pub use logwalker::LogWalker; pub use remotes::{fetch_origin, get_default_remote, get_remotes}; pub use reset::{reset_stage, reset_workdir}; pub use stash::{get_stashes, stash_apply, stash_drop, stash_save}; +pub use state::{repo_state, RepoState}; pub use tags::{get_tags, CommitTags, Tags}; pub use utils::{ get_head, get_head_tuple, is_bare_repo, is_repo, stage_add_all, @@ -50,7 +53,10 @@ pub use utils::{ #[cfg(test)] mod tests { - use super::status::{get_status, StatusType}; + use super::{ + status::{get_status, StatusType}, + CommitId, LogWalker, + }; use crate::error::Result; use git2::Repository; use std::process::Command; @@ -120,6 +126,23 @@ mod tests { Ok((td, repo)) } + /// + pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> { + sandbox_config_files(); + + let td = TempDir::new()?; + + let td_path = td.path().as_os_str().to_str().unwrap(); + + let repo = Repository::clone(p, td_path).unwrap(); + + let mut config = repo.config()?; + config.set_str("user.name", "name")?; + config.set_str("user.email", "email")?; + + Ok((td, repo)) + } + /// Same as repo_init, but the repo is a bare repo (--bare) pub fn repo_init_bare() -> Result<(TempDir, Repository)> { let tmp_repo_dir = TempDir::new()?; @@ -145,6 +168,17 @@ mod tests { eprintln!("\n----\n{}", cmd); } + /// helper to fetch commmit details using log walker + pub fn get_commit_ids( + r: &Repository, + max_count: usize, + ) -> Vec { + let mut commit_ids = Vec::::new(); + LogWalker::new(r).read(&mut commit_ids, max_count).unwrap(); + + commit_ids + } + fn debug_cmd(path: &str, cmd: &str) -> String { let output = if cfg!(target_os = "windows") { Command::new("cmd") diff --git a/asyncgit/src/sync/remotes/push.rs b/asyncgit/src/sync/remotes/push.rs index 2b046ca8..efefe81b 100644 --- a/asyncgit/src/sync/remotes/push.rs +++ b/asyncgit/src/sync/remotes/push.rs @@ -212,8 +212,7 @@ mod tests { use super::*; use crate::sync::{ self, - tests::{repo_init, repo_init_bare}, - LogWalker, + tests::{get_commit_ids, repo_init, repo_init_bare}, }; use std::{fs::File, io::Write, path::Path}; @@ -357,9 +356,8 @@ mod tests { String::from("temp_file.txt") ); - let mut repo_commit_ids = Vec::::new(); - LogWalker::new(&repo).read(&mut repo_commit_ids, 1).unwrap(); - assert_eq!(repo_commit_ids.contains(&repo_1_commit), true); + let commits = get_commit_ids(&repo, 1); + assert!(commits.contains(&repo_1_commit)); push( tmp_repo_dir.path().to_str().unwrap(), @@ -397,14 +395,8 @@ mod tests { .unwrap() .id(); - let mut other_repo_commit_ids = Vec::::new(); - LogWalker::new(&other_repo) - .read(&mut other_repo_commit_ids, 1) - .unwrap(); - assert_eq!( - other_repo_commit_ids.contains(&repo_2_commit), - true - ); + let commits = get_commit_ids(&other_repo, 1); + assert!(commits.contains(&repo_2_commit)); // Attempt a normal push, // should fail as branches diverged @@ -423,9 +415,8 @@ mod tests { // Check that the other commit is not in upstream, // a normal push would not rewrite history - let mut commit_ids = Vec::::new(); - LogWalker::new(&upstream).read(&mut commit_ids, 1).unwrap(); - assert_eq!(commit_ids.contains(&repo_1_commit), true); + let commits = get_commit_ids(&upstream, 1); + assert!(!commits.contains(&repo_2_commit)); // Attempt force push, // should work as it forces the push through @@ -440,10 +431,8 @@ mod tests { ) .unwrap(); - commit_ids.clear(); - LogWalker::new(&upstream).read(&mut commit_ids, 1).unwrap(); - // Check that only the other repo commit is now in upstream - assert_eq!(commit_ids.contains(&repo_2_commit), true); + let commits = get_commit_ids(&upstream, 1); + assert!(commits.contains(&repo_2_commit)); let new_upstream_parent = Repository::init_bare(tmp_upstream_dir.path()) diff --git a/asyncgit/src/sync/state.rs b/asyncgit/src/sync/state.rs new file mode 100644 index 00000000..046f5961 --- /dev/null +++ b/asyncgit/src/sync/state.rs @@ -0,0 +1,33 @@ +use crate::{error::Result, sync::utils}; +use git2::RepositoryState; +use scopetime::scope_time; + +/// +#[derive(Debug)] +pub enum RepoState { + /// + Clean, + /// + Merge, + /// + Other, +} + +impl From for RepoState { + fn from(state: RepositoryState) -> Self { + match state { + RepositoryState::Clean => RepoState::Clean, + RepositoryState::Merge => RepoState::Merge, + _ => RepoState::Other, + } + } +} + +/// +pub fn repo_state(repo_path: &str) -> Result { + scope_time!("repo_state"); + + let repo = utils::repo(repo_path)?; + + Ok(repo.state().into()) +} diff --git a/src/tabs/status.rs b/src/tabs/status.rs index df29b41e..32c6f63c 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -103,6 +103,7 @@ impl DrawableComponent for Status { self.index.draw(f, left_chunks[1])?; self.diff.draw(f, chunks[1])?; self.draw_branch_state(f, &left_chunks); + Self::draw_repo_state(f, left_chunks[0]); Ok(()) } @@ -194,6 +195,26 @@ impl Status { } } + fn draw_repo_state( + f: &mut tui::Frame, + r: tui::layout::Rect, + ) { + let w = Paragraph::new(format!( + "{:?}", + asyncgit::sync::repo_state(CWD).expect("") + )) + .alignment(Alignment::Left); + + let mut rect = r; + rect.x += 1; + rect.width = rect.width.saturating_sub(2); + rect.y += rect.height.saturating_sub(1); + rect.height = + rect.height.saturating_sub(rect.height.saturating_sub(1)); + + f.render_widget(w, rect); + } + fn can_focus_diff(&self) -> bool { match self.focus { Focus::WorkDir => self.index_wd.is_file_seleted(), @@ -405,12 +426,34 @@ impl Status { ); } Ok(bytes) => { - self.queue.borrow_mut().push_back( - InternalEvent::ShowErrorMsg(format!( - "fetched:\n{} B", - bytes - )), - ); + if bytes > 0 + || self + .git_branch_state + .as_ref() + .map(|state| state.behind > 0) + .unwrap_or_default() + { + let merge_res = + sync::branch_merge_upstream_fastforward( + CWD, &branch, + ); + let msg = match merge_res { + Err(err) => { + format!("merge failed:\n{}", err) + } + Ok(_) => "merged".to_string(), + }; + + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(msg), + ); + } else { + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg( + "nothing fetched".to_string(), + ), + ); + } } } }