mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 19:29:14 +03:00
branch merge from upstream (#384)
* better structure of all branch functions * support and unittest fast forward merge
This commit is contained in:
parent
c1565eb000
commit
c96feb0fe6
162
asyncgit/src/sync/branch/merge.rs
Normal file
162
asyncgit/src/sync/branch/merge.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
67
asyncgit/src/sync/branch/rename.rs
Normal file
67
asyncgit/src/sync/branch/rename.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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())
|
||||
|
33
asyncgit/src/sync/state.rs
Normal file
33
asyncgit/src/sync/state.rs
Normal 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())
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user