branch merge from upstream (#384)

* better structure of all branch functions
* support and unittest fast forward merge
This commit is contained in:
Stephan Dilly 2021-02-28 01:55:35 +01:00 committed by GitHub
parent c1565eb000
commit c96feb0fe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 361 additions and 92 deletions

View File

@ -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);
}
}

View File

@ -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"
);
}
}

View File

@ -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"
);
}
}

View File

@ -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<CommitId> {
let mut commit_ids = Vec::<CommitId>::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")

View File

@ -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::<CommitId>::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::<CommitId>::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::<CommitId>::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())

View File

@ -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<RepositoryState> 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<RepoState> {
scope_time!("repo_state");
let repo = utils::repo(repo_path)?;
Ok(repo.state().into())
}

View File

@ -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<B: tui::backend::Backend>(
f: &mut tui::Frame<B>,
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(),
),
);
}
}
}
}