Skip to content

Commit

Permalink
Merge pull request gitbutlerapp#1228 from gitbutlerapp/add-merge-virt…
Browse files Browse the repository at this point in the history
…ual-branch-function

Detect and merge upstream branch changes
  • Loading branch information
schacon committed Sep 21, 2023
2 parents e282f22 + 661d111 commit 49dfcac
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 12 deletions.
1 change: 1 addition & 0 deletions packages/tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ async fn main() {
virtual_branches::commands::get_base_branch_data,
virtual_branches::commands::set_base_branch,
virtual_branches::commands::update_base_branch,
virtual_branches::commands::merge_virtual_branch_upstream,
virtual_branches::commands::update_virtual_branch,
virtual_branches::commands::delete_virtual_branch,
virtual_branches::commands::apply_branch,
Expand Down
14 changes: 14 additions & 0 deletions packages/tauri/src/virtual_branches/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ pub async fn create_virtual_branch_from_branch(
.map_err(Into::into)
}

#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn merge_virtual_branch_upstream(
handle: AppHandle,
project_id: &str,
branch: &str,
) -> Result<(), Error> {
handle
.state::<Controller>()
.merge_virtual_branch_upstream(project_id, branch)
.await
.map_err(Into::into)
}

#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_base_branch_data(
Expand Down
32 changes: 32 additions & 0 deletions packages/tauri/src/virtual_branches/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,38 @@ impl Controller {
Ok(target)
}

pub async fn merge_virtual_branch_upstream(
&self,
project_id: &str,
branch: &str,
) -> Result<(), Error> {
self.with_lock(project_id, || {
self.with_verify_branch(project_id, |gb_repository, project_repository| {
let signing_key = if project_repository
.config()
.sign_commits()
.context("failed to get sign commits option")?
{
Some(
self.keys_storage
.get_or_create()
.context("failed to get private key")?,
)
} else {
None
};
super::merge_virtual_branch_upstream(
gb_repository,
project_repository,
branch,
signing_key.as_ref(),
)
.map_err(Error::Other)
})
})
.await
}

pub async fn update_base_branch(&self, project_id: &str) -> Result<(), Error> {
self.with_lock(project_id, || {
self.with_verify_branch(project_id, |gb_repository, project_repository| {
Expand Down
251 changes: 251 additions & 0 deletions packages/tauri/src/virtual_branches/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1433,6 +1433,257 @@ fn test_update_base_branch_no_commits_no_conflict() -> Result<()> {
Ok(())
}

#[test]
fn test_merge_vbranch_upstream_clean() -> Result<()> {
let TestDeps {
repository,
project,
gb_repo_path,
user_store,
project_store,
keys_controller,
..
} = new_test_deps()?;

let gb_repo =
gb_repository::Repository::open(gb_repo_path, &project.id, project_store, user_store)?;
let project_repository = project_repository::Repository::open(&project)?;

// create a commit and set the target
let file_path = std::path::Path::new("test.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
test_utils::commit_all(&repository);
let target_oid = repository.head().unwrap().target().unwrap();

std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
// add a commit to the target branch it's pointing to so there is something "upstream"
test_utils::commit_all(&repository);
let last_push = repository.head().unwrap().target().unwrap();

// coworker adds some work
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\ncoworker work\n",
)?;

test_utils::commit_all(&repository);
let coworker_work = repository.head().unwrap().target().unwrap();

//update repo ref refs/remotes/origin/master to up_target oid
repository.reference(
"refs/remotes/origin/master",
coworker_work,
true,
"update target",
)?;

// revert to our file
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;

set_test_target(&gb_repo, &project_repository, &repository)?;
target::Writer::new(&gb_repo).write_default(&target::Target {
branch: "refs/remotes/origin/master".parse().unwrap(),
remote_url: "origin".to_string(),
sha: target_oid,
})?;

// add some uncommitted work
let file_path2 = std::path::Path::new("test2.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path2),
"file2\n",
)?;

let remote_branch: git::RemoteBranchName = "refs/remotes/origin/master".parse().unwrap();
let branch_writer = branch::Writer::new(&gb_repo);
let mut branch = create_virtual_branch(&gb_repo, &BranchCreateRequest::default())
.expect("failed to create virtual branch");
branch.upstream = Some(remote_branch.clone());
branch.head = last_push;
branch_writer
.write(&branch)
.context("failed to write target branch after push")?;

// create the branch
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches[0];
assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 1);
assert_eq!(branch1.upstream_commits.len(), 1);

merge_virtual_branch_upstream(
&gb_repo,
&project_repository,
&branch1.id,
Some(keys_controller.get_or_create()?).as_ref(),
)?;

let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches[0];

let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
assert_eq!(
"line1\nline2\nline3\nline4\nupstream\ncoworker work\n",
String::from_utf8(contents)?
);
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
assert_eq!("file2\n", String::from_utf8(contents)?);
assert_eq!(branch1.files.len(), 0);
assert_eq!(branch1.commits.len(), 3);
assert_eq!(branch1.upstream_commits.len(), 0);

// make sure the last commit was signed
let last_id = &branch1.commits[0].id;
let last_commit = repository.find_commit(last_id.parse::<git::Oid>()?)?;
assert!(last_commit.raw_header().unwrap().contains("SSH SIGNATURE"));

Ok(())
}

#[test]
fn test_merge_vbranch_upstream_conflict() -> Result<()> {
let TestDeps {
repository,
project,
gb_repo_path,
user_store,
project_store,
..
} = new_test_deps()?;

let gb_repo =
gb_repository::Repository::open(gb_repo_path, &project.id, project_store, user_store)?;
let project_repository = project_repository::Repository::open(&project)?;

// create a commit and set the target
let file_path = std::path::Path::new("test.txt");
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\n",
)?;
test_utils::commit_all(&repository);
let target_oid = repository.head().unwrap().target().unwrap();

std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;
// add a commit to the target branch it's pointing to so there is something "upstream"
test_utils::commit_all(&repository);
let last_push = repository.head().unwrap().target().unwrap();

// coworker adds some work
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\ncoworker work\n",
)?;

test_utils::commit_all(&repository);
let coworker_work = repository.head().unwrap().target().unwrap();

//update repo ref refs/remotes/origin/master to up_target oid
repository.reference(
"refs/remotes/origin/master",
coworker_work,
true,
"update target",
)?;

// revert to our file
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\n",
)?;

set_test_target(&gb_repo, &project_repository, &repository)?;
target::Writer::new(&gb_repo).write_default(&target::Target {
branch: "refs/remotes/origin/master".parse().unwrap(),
remote_url: "origin".to_string(),
sha: target_oid,
})?;

// add some uncommitted work
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\nother side\n",
)?;

let remote_branch: git::RemoteBranchName = "refs/remotes/origin/master".parse().unwrap();
let branch_writer = branch::Writer::new(&gb_repo);
let mut branch = create_virtual_branch(&gb_repo, &BranchCreateRequest::default())
.expect("failed to create virtual branch");
branch.upstream = Some(remote_branch.clone());
branch.head = last_push;
branch_writer
.write(&branch)
.context("failed to write target branch after push")?;

// create the branch
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches[0];

assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 1);
assert_eq!(branch1.upstream_commits.len(), 1);

merge_virtual_branch_upstream(&gb_repo, &project_repository, &branch1.id, None)?;

let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches[0];
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;

assert_eq!(
"line1\nline2\nline3\nline4\nupstream\n<<<<<<< ours\nother side\n=======\ncoworker work\n>>>>>>> theirs\n",
String::from_utf8(contents)?
);

assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 1);
assert!(branch1.conflicted);

// fix the conflict
std::fs::write(
std::path::Path::new(&project.path).join(file_path),
"line1\nline2\nline3\nline4\nupstream\nother side\ncoworker work\n",
)?;

// make gb see the conflict resolution
let branches = list_virtual_branches(&gb_repo, &project_repository)?;
assert!(branches[0].conflicted);

// commit the merge resolution
commit(
&gb_repo,
&project_repository,
&branch1.id,
"fix merge conflict",
None,
None,
)?;

let branches = list_virtual_branches(&gb_repo, &project_repository)?;
let branch1 = &branches[0];
assert!(!branch1.conflicted);
assert_eq!(branch1.files.len(), 0);
assert_eq!(branch1.commits.len(), 3);

// make sure the last commit was a merge commit (2 parents)
let last_id = &branch1.commits[0].id;
let last_commit = repository.find_commit(last_id.parse::<git::Oid>()?)?;
assert_eq!(last_commit.parent_count(), 2);

Ok(())
}

#[test]
fn test_update_target_with_conflicts_in_vbranches() -> Result<()> {
let TestDeps {
Expand Down
Loading

0 comments on commit 49dfcac

Please sign in to comment.