gitbutler/gitbutler-app/tests/virtual_branches.rs
2024-01-22 15:37:42 +01:00

6088 lines
202 KiB
Rust

//TODO:
#![allow(
clippy::redundant_closure_for_method_calls,
clippy::rest_pat_in_fully_bound_structs,
clippy::dbg_macro
)]
mod common;
use std::{fs, path, str::FromStr};
use gblib::{
error::Error,
git, keys,
projects::{self, ProjectId},
users,
virtual_branches::{branch, controller::ControllerError, errors, Controller},
};
use self::common::{paths, TestProject};
struct Test {
repository: TestProject,
project_id: ProjectId,
projects: projects::Controller,
controller: Controller,
}
impl Default for Test {
fn default() -> Self {
let data_dir = paths::data_dir();
let keys = keys::Controller::from(&data_dir);
let projects = projects::Controller::from(&data_dir);
let users = users::Controller::from(&data_dir);
let helper = git::credentials::Helper::from(&data_dir);
let test_project = TestProject::default();
let project = projects
.add(test_project.path())
.expect("failed to add project");
Self {
repository: test_project,
project_id: project.id,
controller: Controller::new(&data_dir, &projects, &users, &keys, &helper),
projects,
}
}
}
mod create_commit {
use super::*;
#[tokio::test]
async fn should_lock_updated_hunks() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// by default, hunks are not locked
fs::write(repository.path().join("file.txt"), "content").unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
}
controller
.create_commit(&project_id, &branch_id, "test", None, false)
.await
.unwrap();
{
// change in the committed hunks leads to hunk locking
fs::write(repository.path().join("file.txt"), "updated content").unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(branch.files[0].hunks[0].locked);
}
}
#[tokio::test]
async fn should_not_lock_disjointed_hunks() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
let mut lines: Vec<_> = (0_i32..24_i32).map(|i| format!("line {}", i)).collect();
fs::write(repository.path().join("file.txt"), lines.clone().join("\n")).unwrap();
repository.commit_all("my commit");
repository.push();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// new hunk in the middle of the file
lines[12] = "commited stuff".to_string();
fs::write(repository.path().join("file.txt"), lines.clone().join("\n")).unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
}
controller
.create_commit(&project_id, &branch_id, "test commit", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
{
// hunk before the commited part is not locked
let mut changed_lines = lines.clone();
changed_lines[0] = "updated line\nwith extra line".to_string();
fs::write(repository.path().join("file.txt"), changed_lines.join("\n")).unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
// cleanup
fs::write(repository.path().join("file.txt"), lines.clone().join("\n")).unwrap();
}
{
// hunk after the commited part is not locked
let mut changed_lines = lines.clone();
changed_lines[23] = "updated line".to_string();
fs::write(repository.path().join("file.txt"), changed_lines.join("\n")).unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
// cleanup
fs::write(repository.path().join("file.txt"), lines.clone().join("\n")).unwrap();
}
{
// hunk before the commited part but with overlapping context
let mut changed_lines = lines.clone();
changed_lines[10] = "updated line".to_string();
fs::write(repository.path().join("file.txt"), changed_lines.join("\n")).unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
// cleanup
fs::write(repository.path().join("file.txt"), lines.clone().join("\n")).unwrap();
}
{
// hunk after the commited part but with overlapping context
let mut changed_lines = lines.clone();
changed_lines[14] = "updated line".to_string();
fs::write(repository.path().join("file.txt"), changed_lines.join("\n")).unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.files.len(), 1);
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
assert_eq!(branch.files[0].hunks.len(), 1);
assert!(!branch.files[0].hunks[0].locked);
// cleanup
fs::write(repository.path().join("file.txt"), lines.clone().join("\n")).unwrap();
}
}
}
mod references {
use super::*;
mod create_virtual_branch {
use super::*;
#[tokio::test]
async fn simple() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert_eq!(branches[0].name, "Virtual branch");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&"refs/gitbutler/virtual-branch".to_string()));
}
#[tokio::test]
async fn duplicate_name() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(
&project_id,
&gblib::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branch2_id = controller
.create_virtual_branch(
&project_id,
&gblib::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "name");
assert_eq!(branches[1].id, branch2_id);
assert_eq!(branches[1].name, "name 1");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&"refs/gitbutler/name".to_string()));
assert!(refnames.contains(&"refs/gitbutler/name-1".to_string()));
}
}
mod update_virtual_branch {
use super::*;
#[tokio::test]
async fn simple() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
controller
.update_virtual_branch(
&project_id,
branch::BranchUpdateRequest {
id: branch_id,
name: Some("new name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert_eq!(branches[0].name, "new name");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(!refnames.contains(&"refs/gitbutler/name".to_string()));
assert!(refnames.contains(&"refs/gitbutler/new-name".to_string()));
}
#[tokio::test]
async fn duplicate_name() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branch2_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
..Default::default()
},
)
.await
.unwrap();
controller
.update_virtual_branch(
&project_id,
branch::BranchUpdateRequest {
id: branch2_id,
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "name");
assert_eq!(branches[1].id, branch2_id);
assert_eq!(branches[1].name, "name 1");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&"refs/gitbutler/name".to_string()));
assert!(refnames.contains(&"refs/gitbutler/name-1".to_string()));
}
}
mod push_virtual_branch {
use super::*;
#[tokio::test]
async fn simple() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch1_id, "test", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch1_id, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "name");
assert_eq!(
branches[0].upstream.as_ref().unwrap().name.to_string(),
"refs/remotes/origin/name"
);
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&branches[0].upstream.clone().unwrap().name.to_string()));
}
#[tokio::test]
async fn duplicate_names() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = {
// create and push branch with some work
let branch1_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch1_id, "test", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch1_id, false)
.await
.unwrap();
branch1_id
};
// rename first branch
controller
.update_virtual_branch(
&project_id,
branch::BranchUpdateRequest {
id: branch1_id,
name: Some("updated name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branch2_id = {
// create another branch with first branch's old name and push it
let branch2_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "updated content").unwrap();
controller
.create_commit(&project_id, &branch2_id, "test", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch2_id, false)
.await
.unwrap();
branch2_id
};
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 2);
// first branch is pushing to old ref remotely
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "updated name");
assert_eq!(
branches[0].upstream.as_ref().unwrap().name,
"refs/remotes/origin/name".parse().unwrap()
);
// new branch is pushing to new ref remotely
assert_eq!(branches[1].id, branch2_id);
assert_eq!(branches[1].name, "name");
assert_eq!(
branches[1].upstream.as_ref().unwrap().name,
"refs/remotes/origin/name-1".parse().unwrap()
);
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&branches[0].upstream.clone().unwrap().name.to_string()));
assert!(refnames.contains(&branches[1].upstream.clone().unwrap().name.to_string()));
}
}
}
mod delete_virtual_branch {
use super::*;
#[tokio::test]
async fn should_unapply_diff() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// write some
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
controller
.delete_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
assert!(!repository.path().join("file.txt").exists());
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(!refnames.contains(&"refs/gitbutler/name".to_string()));
}
#[tokio::test]
async fn should_remove_reference() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
controller
.delete_virtual_branch(&project_id, &id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(!refnames.contains(&"refs/gitbutler/name".to_string()));
}
}
mod set_base_branch {
use super::*;
#[tokio::test]
async fn success() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
}
mod errors {
use super::*;
#[tokio::test]
async fn missing() {
let Test {
project_id,
controller,
..
} = Test::default();
assert!(matches!(
controller
.set_base_branch(
&project_id,
&git::RemoteRefname::from_str("refs/remotes/origin/missing").unwrap(),
)
.await,
Err(Error::UserError { .. })
));
}
}
}
mod unapply {
use super::*;
#[tokio::test]
async fn unapply_with_data() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
controller
.unapply_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
assert!(!repository.path().join("file.txt").exists());
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(!branches[0].active);
}
#[tokio::test]
async fn conflicting() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a conflicting branch, and stash it
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].base_current);
assert!(branches[0].active);
assert_eq!(branches[0].files[0].hunks[0].diff, "@@ -1,1 +1,1 @@\n-first\n\\ No newline at end of file\n+conflict\n\\ No newline at end of file\n");
controller
.unapply_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
branches[0].id
};
{
// update base branch, causing conflict
controller.update_base_branch(&project_id).await.unwrap();
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == branch_id)
.unwrap();
assert!(!branch.base_current);
assert!(!branch.active);
}
{
// apply branch, it should conflict
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(branch.base_current);
assert!(branch.conflicted);
assert_eq!(branch.files[0].hunks[0].diff, "@@ -1,1 +1,5 @@\n-first\n\\ No newline at end of file\n+<<<<<<< ours\n+conflict\n+=======\n+second\n+>>>>>>> theirs\n");
}
{
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(!branch.active);
assert!(!branch.base_current);
assert!(!branch.conflicted);
assert_eq!(branch.files[0].hunks[0].diff, "@@ -1,1 +1,1 @@\n-first\n\\ No newline at end of file\n+conflict\n\\ No newline at end of file\n");
}
}
#[tokio::test]
async fn delete_if_empty() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
controller
.unapply_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
}
}
mod apply_virtual_branch {
use super::*;
#[tokio::test]
async fn deltect_conflict() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = {
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "branch one").unwrap();
branch1_id
};
// unapply first vbranch
controller
.unapply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap();
{
// create another vbranch that conflicts with the first one
controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "branch two").unwrap();
}
{
// it should not be possible to apply the first branch
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap());
assert!(matches!(
controller
.apply_virtual_branch(&project_id, &branch1_id)
.await,
Err(ControllerError::Action(
errors::ApplyBranchError::BranchConflicts(_)
))
));
}
}
#[tokio::test]
async fn rebase_commit() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "one").unwrap();
fs::write(repository.path().join("another_file.txt"), "").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "two").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = {
// create a branch with some commited work
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("another_file.txt"), "virtual").unwrap();
controller
.create_commit(&project_id, &branch1_id, "virtual commit", None, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
branch1_id
};
{
// unapply first vbranch
controller
.unapply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap();
assert_eq!(
fs::read_to_string(repository.path().join("another_file.txt")).unwrap(),
""
);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"one"
);
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!branches[0].active);
}
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// branch is stil unapplied
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!branches[0].active);
assert!(!branches[0].conflicted);
assert_eq!(
fs::read_to_string(repository.path().join("another_file.txt")).unwrap(),
""
);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"two"
);
}
{
// apply first vbranch again
controller
.apply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap();
// it should be rebased
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert_eq!(
fs::read_to_string(repository.path().join("another_file.txt")).unwrap(),
"virtual"
);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"two"
);
}
}
#[tokio::test]
async fn rebase_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = {
// make a branch with some work
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("another_file.txt"), "").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
branch1_id
};
{
// unapply first vbranch
controller
.unapply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(!branches[0].active);
assert!(!repository.path().join("another_file.txt").exists());
assert!(!repository.path().join("file.txt").exists());
}
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// first branch is stil unapplied
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(!branches[0].active);
assert!(!branches[0].conflicted);
assert!(!repository.path().join("another_file.txt").exists());
assert!(repository.path().join("file.txt").exists());
}
{
// apply first vbranch again
controller
.apply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap();
// workdir should be rebased, and work should be restored
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(repository.path().join("another_file.txt").exists());
assert!(repository.path().join("file.txt").exists());
}
}
}
#[tokio::test]
async fn resolve_conflict_flow() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
branch1_id
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// there is a conflict now, so the branch should be inactive
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(!branches[0].active);
}
{
// when we apply conflicted branch, it has conflict
controller
.apply_virtual_branch(&project_id, &branch1_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
// and the conflict markers are in the file
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
{
// can't commit conflicts
assert!(matches!(
controller
.create_commit(&project_id, &branch1_id, "commit conflicts", None, false)
.await,
Err(ControllerError::Action(errors::CommitError::Conflicted(_)))
));
}
{
// fixing the conflict removes conflicted mark
fs::write(repository.path().join("file.txt"), "resolved").unwrap();
let commit_oid = controller
.create_commit(&project_id, &branch1_id, "resolution", None, false)
.await
.unwrap();
let commit = repository.find_commit(commit_oid).unwrap();
assert_eq!(commit.parent_count(), 2);
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
}
}
mod fetch_from_target {
use super::*;
#[tokio::test]
async fn should_update_last_fetched() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let before_fetch = controller.get_base_branch_data(&project_id).await.unwrap();
assert!(before_fetch.unwrap().last_fetched_ms.is_none());
let fetch = controller.fetch_from_target(&project_id).await.unwrap();
assert!(fetch.last_fetched_ms.is_some());
let after_fetch = controller.get_base_branch_data(&project_id).await.unwrap();
assert!(after_fetch.as_ref().unwrap().last_fetched_ms.is_some());
assert_eq!(fetch.last_fetched_ms, after_fetch.unwrap().last_fetched_ms);
let second_fetch = controller.fetch_from_target(&project_id).await.unwrap();
assert!(second_fetch.last_fetched_ms.is_some());
assert_ne!(fetch.last_fetched_ms, second_fetch.last_fetched_ms);
let after_second_fetch = controller.get_base_branch_data(&project_id).await.unwrap();
assert!(after_second_fetch
.as_ref()
.unwrap()
.last_fetched_ms
.is_some());
assert_eq!(
second_fetch.last_fetched_ms,
after_second_fetch.unwrap().last_fetched_ms
);
}
}
mod update_base_branch {
use super::*;
mod unapplied_branch {
use super::*;
#[tokio::test]
async fn conflicts_with_uncommitted_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch that is unapplied and contains not commited conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// branch should not be changed.
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_not_pushed() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should not change the branch.
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_pushed() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should not change the branch.
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_not_pushed_fixed_with_more_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "fix conflict").unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should rebase upstream, and leave uncommited file as is
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current); // TODO: should be true
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap()); // TODO: should be true
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nfix conflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_pushed_fixed_with_more_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "fix conflict").unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should not touch the branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].files.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nfix conflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn no_conflicts() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "no conflict").unwrap();
controller
.create_commit(
&project_id,
&branch_id,
"non conflicting commit",
None,
false,
)
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "still no conflicts").unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should update branch base
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].upstream.is_none());
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
}
}
#[tokio::test]
async fn integrated_commit_plus_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
repository.commit_all("first");
repository.push();
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "second").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
// more local work in the same branch
fs::write(repository.path().join("file2.txt"), "other").unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
{
// merge branch upstream
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
repository.merge(&branch.upstream.as_ref().unwrap().name);
repository.fetch();
}
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
branch_id
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// should remove integrated commit, but leave work
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(branches[0].upstream.is_none());
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
assert_eq!(
std::fs::read_to_string(repository.path().join("file2.txt")).unwrap(),
"other"
);
}
}
#[tokio::test]
async fn all_integrated() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
{
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "second").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// should remove identical branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
}
}
#[tokio::test]
async fn integrate_work_while_being_behind() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// open pr
fs::write(repository.path().join("file2.txt"), "new file").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
}
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
{
// merge pr
let branch =
controller.list_virtual_branches(&project_id).await.unwrap()[0].clone();
repository.merge(&branch.upstream.as_ref().unwrap().name);
repository.fetch();
}
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// just removes integrated branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
}
}
}
mod applied_branch {
use super::*;
#[tokio::test]
async fn conflicts_with_uncommitted_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
branch_id
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// should stash conflicing branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_not_pushed() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should stash the branch.
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_pushed() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should stash the branch.
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_not_pushed_fixed_with_more_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "fix conflict").unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should rebase upstream, and leave uncommited file as is
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current); // TODO: should be true
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap()); // TODO: should be true
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nfix conflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
#[tokio::test]
async fn commited_conflict_pushed_fixed_with_more_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch with a commit that conflicts with upstream, and work that fixes
// that conflict
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "conflicting commit", None, false)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "fix conflict").unwrap();
branch_id
};
{
// when fetching remote
controller.update_base_branch(&project_id).await.unwrap();
// should merge upstream, and leave uncommited file as is.
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(!branches[0].base_current); // TODO: should be true
assert_eq!(branches[0].commits.len(), 1); // TODO: should be 2
assert_eq!(branches[0].files.len(), 1);
assert!(!controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap()); // TODO: should be true
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nfix conflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
}
mod no_conflicts_pushed {
use super::*;
#[tokio::test]
async fn force_push_ok() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(true),
..Default::default()
})
.await
.unwrap();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "no conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "no conflicts", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "still no conflict").unwrap();
branch_id
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// rebases branch, since the branch is pushed and force pushing is
// allowed
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].requires_force);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert!(!branches[0].commits[0].is_remote);
assert!(!branches[0].commits[0].is_integrated);
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
}
#[tokio::test]
async fn force_push_not_ok() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "no conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "no conflicts", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "still no conflict").unwrap();
branch_id
};
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(false),
..Default::default()
})
.await
.unwrap();
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// creates a merge commit, since the branch is pushed
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(!branches[0].requires_force);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 2);
assert!(!branches[0].commits[0].is_remote);
assert!(!branches[0].commits[0].is_integrated);
assert!(branches[0].commits[1].is_remote);
assert!(!branches[0].commits[1].is_integrated);
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
}
}
#[tokio::test]
async fn no_conflicts() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "no conflict").unwrap();
controller
.create_commit(&project_id, &branch_id, "no conflicts", None, false)
.await
.unwrap();
fs::write(repository.path().join("file2.txt"), "still no conflict").unwrap();
branch_id
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// just rebases branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
assert_eq!(
std::fs::read_to_string(repository.path().join("file2.txt")).unwrap(),
"still no conflict"
);
}
}
#[tokio::test]
async fn integrated_commit_plus_work() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
repository.commit_all("first");
repository.push();
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "second").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
{
// merge branch upstream
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
repository.merge(&branch.upstream.as_ref().unwrap().name);
repository.fetch();
}
// more local work in the same branch
fs::write(repository.path().join("file2.txt"), "other").unwrap();
branch_id
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// should remove integrated commit, but leave non integrated work as is
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(controller
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap());
}
{
// applying the branch should produce conflict markers
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert_eq!(
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
assert_eq!(
std::fs::read_to_string(repository.path().join("file2.txt")).unwrap(),
"other"
);
}
}
#[tokio::test]
async fn integrated_with_locked_conflicting_hunks() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(
repository.path().join("file.txt"),
"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n",
)
.unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(
repository.path().join("file.txt"),
"1\n2\n3\n4\n5\n6\n17\n8\n9\n10\n11\n12\n",
)
.unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// branch has no conflict
let branch_id = {
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(
repository.path().join("file.txt"),
"1\n2\n3\n4\n5\n6\n7\n8\n19\n10\n11\n12\n",
)
.unwrap();
controller
.create_commit(&project_id, &branch_id, "first", None, false)
.await
.unwrap();
branch_id
};
// push the branch
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
// another locked conflicing hunk
fs::write(
repository.path().join("file.txt"),
"1\n2\n3\n4\n5\n6\n77\n8\n19\n10\n11\n12\n",
)
.unwrap();
{
// merge branch remotely
let branch =
controller.list_virtual_branches(&project_id).await.unwrap()[0].clone();
repository.merge(&branch.upstream.as_ref().unwrap().name);
}
repository.fetch();
{
controller.update_base_branch(&project_id).await.unwrap();
// removes integrated commit, leaves non commited work as is
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert!(branches[0].commits.is_empty());
assert!(!branches[0].files.is_empty());
}
{
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].files[0].hunks.len(), 1);
assert_eq!(branches[0].files[0].hunks[0].diff, "@@ -4,7 +4,11 @@\n 4\n 5\n 6\n-7\n+<<<<<<< ours\n+77\n+=======\n+17\n+>>>>>>> theirs\n 8\n 19\n 10\n");
assert_eq!(branches[0].commits.len(), 0);
}
}
#[tokio::test]
async fn integrated_with_locked_hunks() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(false),
..Default::default()
})
.await
.unwrap();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "first").unwrap();
controller
.create_commit(&project_id, &branch_id, "first", None, false)
.await
.unwrap();
branch_id
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
// another non-locked hunk
fs::write(repository.path().join("file.txt"), "first\nsecond").unwrap();
{
// push and merge branch remotely
let branch =
controller.list_virtual_branches(&project_id).await.unwrap()[0].clone();
repository.merge(&branch.upstream.as_ref().unwrap().name);
}
repository.fetch();
{
controller.update_base_branch(&project_id).await.unwrap();
// removes integrated commit, leaves non commited work as is
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].commits.is_empty());
assert!(branches[0].upstream.is_none());
assert_eq!(branches[0].files.len(), 1);
}
{
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0); // no merge commit
}
}
#[tokio::test]
async fn integrated_with_non_locked_hunks() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = {
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "first").unwrap();
controller
.create_commit(&project_id, &branch_id, "first", None, false)
.await
.unwrap();
branch_id
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
// another non-locked hunk
fs::write(repository.path().join("another_file.txt"), "first").unwrap();
{
// push and merge branch remotely
let branch =
controller.list_virtual_branches(&project_id).await.unwrap()[0].clone();
repository.merge(&branch.upstream.as_ref().unwrap().name);
}
repository.fetch();
{
controller.update_base_branch(&project_id).await.unwrap();
// removes integrated commit, leaves non commited work as is
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].commits.is_empty());
assert!(branches[0].upstream.is_none());
assert!(!branches[0].files.is_empty());
}
{
controller
.apply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
assert!(branches[0].base_current);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
}
}
#[tokio::test]
async fn all_integrated() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
{
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "second").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
};
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// just removes integrated branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
}
}
#[tokio::test]
async fn integrate_work_while_being_behind() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
// make sure we have an undiscovered commit in the remote branch
{
fs::write(repository.path().join("file.txt"), "first").unwrap();
let first_commit_oid = repository.commit_all("first");
fs::write(repository.path().join("file.txt"), "second").unwrap();
repository.commit_all("second");
repository.push();
repository.reset_hard(Some(first_commit_oid));
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// open pr
fs::write(repository.path().join("file2.txt"), "new file").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
}
{
// merge pr
let branch =
controller.list_virtual_branches(&project_id).await.unwrap()[0].clone();
repository.merge(&branch.upstream.as_ref().unwrap().name);
repository.fetch();
}
{
// fetch remote
controller.update_base_branch(&project_id).await.unwrap();
// just removes integrated branch
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
}
}
}
}
mod reset_virtual_branch {
use gblib::virtual_branches::{controller::ControllerError, errors::ResetBranchError};
use super::*;
#[tokio::test]
async fn to_head() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let oid = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
// commit changes
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
oid
};
{
// reset changes to head
controller
.reset_virtual_branch(&project_id, &branch1_id, oid)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
}
}
#[tokio::test]
async fn to_target() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
let base_branch = controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file.txt"), "content").unwrap();
// commit changes
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
}
{
// reset changes to head
controller
.reset_virtual_branch(&project_id, &branch1_id, base_branch.base_sha)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 0);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
}
}
#[tokio::test]
async fn to_commit() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let first_commit_oid = {
// commit some changes
fs::write(repository.path().join("file.txt"), "content").unwrap();
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
oid
};
{
// commit some more
fs::write(repository.path().join("file.txt"), "more content").unwrap();
let second_commit_oid = controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 2);
assert_eq!(branches[0].commits[0].id, second_commit_oid);
assert_eq!(branches[0].commits[1].id, first_commit_oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"more content"
);
}
{
// reset changes to the first commit
controller
.reset_virtual_branch(&project_id, &branch1_id, first_commit_oid)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, first_commit_oid);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"more content"
);
}
}
#[tokio::test]
async fn to_non_existing() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file.txt"), "content").unwrap();
// commit changes
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
oid
};
assert!(matches!(
controller
.reset_virtual_branch(
&project_id,
&branch1_id,
"fe14df8c66b73c6276f7bb26102ad91da680afcb".parse().unwrap()
)
.await,
Err(ControllerError::Action(
ResetBranchError::CommitNotFoundInBranch(_)
))
));
}
}
mod upstream {
use super::*;
#[tokio::test]
async fn detect_upstream_commits() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let oid1 = {
// create first commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap()
};
let oid2 = {
// create second commit
fs::write(repository.path().join("file.txt"), "content2").unwrap();
controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap()
};
// push
controller
.push_virtual_branch(&project_id, &branch1_id, false)
.await
.unwrap();
let oid3 = {
// create third commit
fs::write(repository.path().join("file.txt"), "content3").unwrap();
controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap()
};
{
// should correctly detect pushed commits
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 3);
assert_eq!(branches[0].commits[0].id, oid3);
assert!(!branches[0].commits[0].is_remote);
assert_eq!(branches[0].commits[1].id, oid2);
assert!(branches[0].commits[1].is_remote);
assert_eq!(branches[0].commits[2].id, oid1);
assert!(branches[0].commits[2].is_remote);
}
}
#[tokio::test]
async fn detect_integrated_commits() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let oid1 = {
// create first commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap()
};
let oid2 = {
// create second commit
fs::write(repository.path().join("file.txt"), "content2").unwrap();
controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap()
};
// push
controller
.push_virtual_branch(&project_id, &branch1_id, false)
.await
.unwrap();
{
// merge branch upstream
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch1_id)
.unwrap();
repository.merge(&branch.upstream.as_ref().unwrap().name);
repository.fetch();
}
let oid3 = {
// create third commit
fs::write(repository.path().join("file.txt"), "content3").unwrap();
controller
.create_commit(&project_id, &branch1_id, "commit", None, false)
.await
.unwrap()
};
{
// should correctly detect pushed commits
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 3);
assert_eq!(branches[0].commits[0].id, oid3);
assert!(!branches[0].commits[0].is_integrated);
assert_eq!(branches[0].commits[1].id, oid2);
assert!(branches[0].commits[1].is_integrated);
assert_eq!(branches[0].commits[2].id, oid1);
assert!(branches[0].commits[2].is_integrated);
}
}
}
mod cherry_pick {
use super::*;
mod cleanly {
use super::*;
#[tokio::test]
async fn applied() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
let commit_two = {
fs::write(repository.path().join("file.txt"), "content two").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
controller
.reset_virtual_branch(&project_id, &branch_id, commit_one)
.await
.unwrap();
repository.reset_hard(None);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
let cherry_picked_commit_oid = controller
.cherry_pick(&project_id, &branch_id, commit_two)
.await
.unwrap();
assert!(cherry_picked_commit_oid.is_some());
assert!(repository.path().join("file.txt").exists());
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content two"
);
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert_eq!(branches[0].commits.len(), 2);
assert_eq!(branches[0].commits[0].id, cherry_picked_commit_oid.unwrap());
assert_eq!(branches[0].commits[1].id, commit_one);
}
#[tokio::test]
async fn to_different_branch() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
let commit_two = {
fs::write(repository.path().join("file_two.txt"), "content two").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
controller
.reset_virtual_branch(&project_id, &branch_id, commit_one)
.await
.unwrap();
repository.reset_hard(None);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
assert!(!repository.path().join("file_two.txt").exists());
let branch_two_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let cherry_picked_commit_oid = controller
.cherry_pick(&project_id, &branch_two_id, commit_two)
.await
.unwrap();
assert!(cherry_picked_commit_oid.is_some());
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert!(repository.path().join("file_two.txt").exists());
assert_eq!(
fs::read_to_string(repository.path().join("file_two.txt")).unwrap(),
"content two"
);
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, commit_one);
assert_eq!(branches[1].id, branch_two_id);
assert!(branches[1].active);
assert_eq!(branches[1].commits.len(), 1);
assert_eq!(branches[1].commits[0].id, cherry_picked_commit_oid.unwrap());
}
#[tokio::test]
async fn non_applied() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file_two.txt"), "content two").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
let commit_three_oid = {
fs::write(repository.path().join("file_three.txt"), "content three").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit", None, false)
.await
.unwrap()
};
controller
.reset_virtual_branch(&project_id, &branch_id, commit_one_oid)
.await
.unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
assert!(matches!(
controller
.cherry_pick(&project_id, &branch_id, commit_three_oid)
.await,
Err(ControllerError::Action(errors::CherryPickError::NotApplied))
));
}
}
mod with_conflicts {
use super::*;
#[tokio::test]
async fn applied() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file_two.txt"), "content two").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
let commit_three = {
fs::write(repository.path().join("file_three.txt"), "content three").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
controller
.reset_virtual_branch(&project_id, &branch_id, commit_one)
.await
.unwrap();
repository.reset_hard(None);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
assert!(!repository.path().join("file_two.txt").exists());
assert!(!repository.path().join("file_three.txt").exists());
// introduce conflict with the remote commit
fs::write(repository.path().join("file_three.txt"), "conflict").unwrap();
{
// cherry picking leads to conflict
let cherry_picked_commit_oid = controller
.cherry_pick(&project_id, &branch_id, commit_three)
.await
.unwrap();
assert!(cherry_picked_commit_oid.is_none());
assert_eq!(
fs::read_to_string(repository.path().join("file_three.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\ncontent three\n>>>>>>> theirs\n"
);
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert_eq!(branches[0].files.len(), 1);
assert!(branches[0].files[0].conflicted);
assert_eq!(branches[0].commits.len(), 1);
}
{
// conflict can be resolved
fs::write(repository.path().join("file_three.txt"), "resolved").unwrap();
let commited_oid = controller
.create_commit(&project_id, &branch_id, "resolution", None, false)
.await
.unwrap();
let commit = repository.find_commit(commited_oid).unwrap();
assert_eq!(commit.parent_count(), 2);
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].requires_force);
assert!(!branches[0].conflicted);
assert_eq!(branches[0].commits.len(), 2);
// resolution commit is there
assert_eq!(branches[0].commits[0].id, commited_oid);
assert_eq!(branches[0].commits[1].id, commit_one);
}
}
#[tokio::test]
async fn non_applied() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
let commit_oid = {
let first = repository.commit_all("commit");
fs::write(repository.path().join("file.txt"), "content").unwrap();
let second = repository.commit_all("commit");
repository.push();
repository.reset_hard(Some(first));
second
};
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
// introduce conflict with the remote commit
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.unapply_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
assert!(matches!(
controller
.cherry_pick(&project_id, &branch_id, commit_oid)
.await,
Err(ControllerError::Action(errors::CherryPickError::NotApplied))
));
}
}
}
mod amend {
use super::*;
#[tokio::test]
async fn to_default_target() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
// amend without head commit
fs::write(repository.path().join("file2.txt"), "content").unwrap();
let to_amend: branch::Ownership = "file2.txt:1-2".parse().unwrap();
assert!(matches!(
controller
.amend(&project_id, &branch_id, &to_amend)
.await
.unwrap_err(),
ControllerError::Action(errors::AmendError::BranchHasNoCommits)
));
}
#[tokio::test]
async fn forcepush_allowed() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(false),
..Default::default()
})
.await
.unwrap();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(true),
..Default::default()
})
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// create commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap();
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
{
// amend another hunk
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
let to_amend: branch::Ownership = "file2.txt:1-2".parse().unwrap();
controller
.amend(&project_id, &branch_id, &to_amend)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(branch.requires_force);
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 2);
}
}
#[tokio::test]
async fn forcepush_forbidden() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(false),
..Default::default()
})
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// create commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap();
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
{
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
let to_amend: branch::Ownership = "file2.txt:1-2".parse().unwrap();
assert!(matches!(
controller
.amend(&project_id, &branch_id, &to_amend)
.await
.unwrap_err(),
ControllerError::Action(errors::AmendError::ForcePushNotAllowed(_))
));
}
}
#[tokio::test]
async fn non_locked_hunk() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// create commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 1);
};
{
// amend another hunk
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
let to_amend: branch::Ownership = "file2.txt:1-2".parse().unwrap();
controller
.amend(&project_id, &branch_id, &to_amend)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 2);
}
}
#[tokio::test]
async fn locked_hunk() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// create commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(
branch.commits[0].files[0].hunks[0].diff,
"@@ -0,0 +1 @@\n+content\n\\ No newline at end of file\n"
);
};
{
// amend another hunk
fs::write(repository.path().join("file.txt"), "more content").unwrap();
let to_amend: branch::Ownership = "file.txt:1-2".parse().unwrap();
controller
.amend(&project_id, &branch_id, &to_amend)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 1);
assert_eq!(
branch.commits[0].files[0].hunks[0].diff,
"@@ -0,0 +1 @@\n+more content\n\\ No newline at end of file\n"
);
}
}
#[tokio::test]
async fn non_existing_ownership() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
// create commit
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 1);
};
{
// amend non existing hunk
let to_amend: branch::Ownership = "file2.txt:1-2".parse().unwrap();
assert!(matches!(
controller
.amend(&project_id, &branch_id, &to_amend)
.await
.unwrap_err(),
ControllerError::Action(errors::AmendError::TargetOwnerhshipNotFound(_))
));
}
}
}
mod init {
use super::*;
#[tokio::test]
async fn twice() {
let data_dir = paths::data_dir();
let keys = keys::Controller::from(&data_dir);
let projects = projects::Controller::from(&data_dir);
let users = users::Controller::from(&data_dir);
let helper = git::credentials::Helper::from(&data_dir);
let test_project = TestProject::default();
let controller = Controller::new(&data_dir, &projects, &users, &keys, &helper);
{
let project = projects
.add(test_project.path())
.expect("failed to add project");
controller
.set_base_branch(&project.id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
assert!(controller
.list_virtual_branches(&project.id)
.await
.unwrap()
.is_empty());
projects.delete(&project.id).await.unwrap();
controller
.list_virtual_branches(&project.id)
.await
.unwrap_err();
}
{
let project = projects.add(test_project.path()).unwrap();
controller
.set_base_branch(&project.id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// even though project is on gitbutler/integration, we should not import it
assert!(controller
.list_virtual_branches(&project.id)
.await
.unwrap()
.is_empty());
}
}
#[tokio::test]
async fn dirty_non_target() {
// a situation when you initialize project while being on the local verison of the master
// that has uncommited changes.
let Test {
repository,
project_id,
controller,
..
} = Test::default();
repository.checkout(&"refs/heads/some-feature".parse().unwrap());
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].files[0].hunks.len(), 1);
assert!(branches[0].upstream.is_none());
assert_eq!(branches[0].name, "some-feature");
}
#[tokio::test]
async fn dirty_target() {
// a situation when you initialize project while being on the local verison of the master
// that has uncommited changes.
let Test {
repository,
project_id,
controller,
..
} = Test::default();
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].files[0].hunks.len(), 1);
assert!(branches[0].upstream.is_none());
assert_eq!(branches[0].name, "master");
}
#[tokio::test]
async fn commit_on_non_target_local() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
repository.checkout(&"refs/heads/some-feature".parse().unwrap());
fs::write(repository.path().join("file.txt"), "content").unwrap();
repository.commit_all("commit on target");
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].files.is_empty());
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].upstream.is_none());
assert_eq!(branches[0].name, "some-feature");
}
#[tokio::test]
async fn commit_on_non_target_remote() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
repository.checkout(&"refs/heads/some-feature".parse().unwrap());
fs::write(repository.path().join("file.txt"), "content").unwrap();
repository.commit_all("commit on target");
repository.push_branch(&"refs/heads/some-feature".parse().unwrap());
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].files.is_empty());
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].upstream.is_some());
assert_eq!(branches[0].name, "some-feature");
}
#[tokio::test]
async fn commit_on_target() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
fs::write(repository.path().join("file.txt"), "content").unwrap();
repository.commit_all("commit on target");
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].files.is_empty());
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].upstream.is_none());
assert_eq!(branches[0].name, "master");
}
#[tokio::test]
async fn submodule() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
let submodule_url: git::Url = TestProject::default()
.path()
.display()
.to_string()
.parse()
.unwrap();
repository.add_submodule(&submodule_url, path::Path::new("submodule"));
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].files[0].hunks.len(), 1);
}
}
mod squash {
use super::*;
#[tokio::test]
async fn head() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
let commit_four_oid = {
fs::write(repository.path().join("file four.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit four", None, false)
.await
.unwrap()
};
controller
.squash(&project_id, &branch_id, commit_four_oid)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(
descriptions,
vec!["commit three\ncommit four", "commit two", "commit one"]
);
}
#[tokio::test]
async fn middle() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
let commit_two_oid = {
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file four.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit four", None, false)
.await
.unwrap()
};
controller
.squash(&project_id, &branch_id, commit_two_oid)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(
descriptions,
vec!["commit four", "commit three", "commit one\ncommit two"]
);
}
#[tokio::test]
async fn forcepush_allowed() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(true),
..Default::default()
})
.await
.unwrap();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
let commit_two_oid = {
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file four.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit four", None, false)
.await
.unwrap()
};
controller
.squash(&project_id, &branch_id, commit_two_oid)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(
descriptions,
vec!["commit four", "commit three", "commit one\ncommit two"]
);
assert!(branch.requires_force);
}
#[tokio::test]
async fn forcepush_forbidden() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(false),
..Default::default()
})
.await
.unwrap();
let commit_two_oid = {
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file four.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit four", None, false)
.await
.unwrap()
};
assert!(matches!(
controller
.squash(&project_id, &branch_id, commit_two_oid)
.await
.unwrap_err(),
ControllerError::Action(errors::SquashError::ForcePushNotAllowed(_))
));
}
#[tokio::test]
async fn root() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
assert!(matches!(
controller
.squash(&project_id, &branch_id, commit_one_oid)
.await
.unwrap_err(),
ControllerError::Action(errors::SquashError::CantSquashRootCommit)
));
}
}
mod update_commit_message {
use super::*;
#[tokio::test]
async fn head() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
let commit_three_oid = {
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
controller
.update_commit_message(
&project_id,
&branch_id,
commit_three_oid,
"commit three updated",
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(
descriptions,
vec!["commit three updated", "commit two", "commit one"]
);
}
#[tokio::test]
async fn middle() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
{
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
let commit_two_oid = {
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
controller
.update_commit_message(
&project_id,
&branch_id,
commit_two_oid,
"commit two updated",
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(
descriptions,
vec!["commit three", "commit two updated", "commit one"]
);
}
#[tokio::test]
async fn forcepush_allowed() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(true),
..Default::default()
})
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
controller
.update_commit_message(
&project_id,
&branch_id,
commit_one_oid,
"commit one updated",
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(descriptions, vec!["commit one updated"]);
assert!(branch.requires_force);
}
#[tokio::test]
async fn forcepush_forbidden() {
let Test {
repository,
project_id,
controller,
projects,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
projects
.update(&projects::UpdateRequest {
id: project_id,
ok_with_force_push: Some(false),
..Default::default()
})
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
assert!(matches!(
controller
.update_commit_message(
&project_id,
&branch_id,
commit_one_oid,
"commit one updated",
)
.await
.unwrap_err(),
ControllerError::Action(errors::UpdateCommitMessageError::ForcePushNotAllowed(_))
));
}
#[tokio::test]
async fn root() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file two.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit two", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file three.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit three", None, false)
.await
.unwrap()
};
controller
.update_commit_message(
&project_id,
&branch_id,
commit_one_oid,
"commit one updated",
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
let descriptions = branch
.commits
.iter()
.map(|c| c.description.clone())
.collect::<Vec<_>>();
assert_eq!(
descriptions,
vec!["commit three", "commit two", "commit one updated"]
);
}
#[tokio::test]
async fn empty() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file one.txt"), "").unwrap();
controller
.create_commit(&project_id, &branch_id, "commit one", None, false)
.await
.unwrap()
};
assert!(matches!(
controller
.update_commit_message(&project_id, &branch_id, commit_one_oid, "",)
.await,
Err(ControllerError::Action(
errors::UpdateCommitMessageError::EmptyMessage
))
));
}
}
mod create_virtual_branch_from_branch {
use super::*;
#[tokio::test]
async fn integration() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_name = {
// make a remote branch
let branch_id = controller
.create_virtual_branch(&project_id, &super::branch::BranchCreateRequest::default())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "first\n").unwrap();
controller
.create_commit(&project_id, &branch_id, "first", None, false)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == branch_id)
.unwrap();
let name = branch.upstream.unwrap().name;
controller
.delete_virtual_branch(&project_id, &branch_id)
.await
.unwrap();
name
};
// checkout a existing remote branch
let branch_id = controller
.create_virtual_branch_from_branch(&project_id, &branch_name)
.await
.unwrap();
{
// add a commit
std::fs::write(repository.path().join("file.txt"), "first\nsecond").unwrap();
controller
.create_commit(&project_id, &branch_id, "second", None, false)
.await
.unwrap();
}
{
// meanwhile, there is a new commit on master
repository.checkout(&"refs/heads/master".parse().unwrap());
std::fs::write(repository.path().join("another.txt"), "").unwrap();
repository.commit_all("another");
repository.push_branch(&"refs/heads/master".parse().unwrap());
repository.checkout(&"refs/heads/gitbutler/integration".parse().unwrap());
}
{
// merge branch into master
controller
.push_virtual_branch(&project_id, &branch_id, false)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == branch_id)
.unwrap();
assert!(branch.commits[0].is_remote);
assert!(!branch.commits[0].is_integrated);
assert!(branch.commits[1].is_remote);
assert!(!branch.commits[1].is_integrated);
repository.rebase_and_merge(&branch_name);
}
{
// should mark commits as integrated
controller.fetch_from_target(&project_id).await.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == branch_id)
.unwrap();
assert!(branch.commits[0].is_remote);
assert!(branch.commits[0].is_integrated);
assert!(branch.commits[1].is_remote);
assert!(branch.commits[1].is_integrated);
}
}
#[tokio::test]
async fn no_conflicts() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
{
// create a remote branch
let branch_name: git::LocalRefname = "refs/heads/branch".parse().unwrap();
repository.checkout(&branch_name);
fs::write(repository.path().join("file.txt"), "first").unwrap();
repository.commit_all("first");
repository.push_branch(&branch_name);
repository.checkout(&"refs/heads/master".parse().unwrap());
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert!(branches.is_empty());
let branch_id = controller
.create_virtual_branch_from_branch(
&project_id,
&"refs/remotes/origin/branch".parse().unwrap(),
)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].description, "first");
}
#[tokio::test]
async fn conflicts_with_uncommited() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
{
// create a remote branch
let branch_name: git::LocalRefname = "refs/heads/branch".parse().unwrap();
repository.checkout(&branch_name);
fs::write(repository.path().join("file.txt"), "first").unwrap();
repository.commit_all("first");
repository.push_branch(&branch_name);
repository.checkout(&"refs/heads/master".parse().unwrap());
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// create a local branch that conflicts with remote
{
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
};
// branch should be created unapplied, because of the conflict
let new_branch_id = controller
.create_virtual_branch_from_branch(
&project_id,
&"refs/remotes/origin/branch".parse().unwrap(),
)
.await
.unwrap();
let new_branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == new_branch_id)
.unwrap();
assert!(!new_branch.active);
assert_eq!(new_branch.commits.len(), 1);
assert!(new_branch.upstream.is_some());
}
#[tokio::test]
async fn conflicts_with_commited() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
{
// create a remote branch
let branch_name: git::LocalRefname = "refs/heads/branch".parse().unwrap();
repository.checkout(&branch_name);
fs::write(repository.path().join("file.txt"), "first").unwrap();
repository.commit_all("first");
repository.push_branch(&branch_name);
repository.checkout(&"refs/heads/master".parse().unwrap());
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// create a local branch that conflicts with remote
{
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
controller
.create_commit(&project_id, &branches[0].id, "hej", None, false)
.await
.unwrap();
};
// branch should be created unapplied, because of the conflict
let new_branch_id = controller
.create_virtual_branch_from_branch(
&project_id,
&"refs/remotes/origin/branch".parse().unwrap(),
)
.await
.unwrap();
let new_branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|branch| branch.id == new_branch_id)
.unwrap();
assert!(!new_branch.active);
assert_eq!(new_branch.commits.len(), 1);
assert!(new_branch.upstream.is_some());
}
#[tokio::test]
async fn from_default_target() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// branch should be created unapplied, because of the conflict
assert!(matches!(
controller
.create_virtual_branch_from_branch(
&project_id,
&"refs/remotes/origin/master".parse().unwrap(),
)
.await
.unwrap_err(),
ControllerError::Action(
errors::CreateVirtualBranchFromBranchError::CantMakeBranchFromDefaultTarget
)
));
}
#[tokio::test]
async fn from_non_existent_branch() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// branch should be created unapplied, because of the conflict
assert!(matches!(
controller
.create_virtual_branch_from_branch(
&project_id,
&"refs/remotes/origin/branch".parse().unwrap(),
)
.await
.unwrap_err(),
ControllerError::Action(errors::CreateVirtualBranchFromBranchError::BranchNotFound(
_
))
));
}
#[tokio::test]
async fn from_state_remote_branch() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
{
// create a remote branch
let branch_name: git::LocalRefname = "refs/heads/branch".parse().unwrap();
repository.checkout(&branch_name);
fs::write(repository.path().join("file.txt"), "branch commit").unwrap();
repository.commit_all("branch commit");
repository.push_branch(&branch_name);
repository.checkout(&"refs/heads/master".parse().unwrap());
// make remote branch stale
std::fs::write(repository.path().join("antoher_file.txt"), "master commit").unwrap();
repository.commit_all("master commit");
repository.push();
}
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch_from_branch(
&project_id,
&"refs/remotes/origin/branch".parse().unwrap(),
)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].files.is_empty());
assert_eq!(branches[0].commits[0].description, "branch commit");
}
}
mod selected_for_changes {
use super::*;
#[tokio::test]
async fn unapplying_selected_branch_selects_anther() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
std::fs::write(repository.path().join("file one.txt"), "").unwrap();
// first branch should be created as default
let b_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
// if default branch exists, new branch should not be created as default
let b2_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
let b = branches.iter().find(|b| b.id == b_id).unwrap();
let b2 = branches.iter().find(|b| b.id == b2_id).unwrap();
assert!(b.selected_for_changes);
assert!(!b2.selected_for_changes);
controller
.unapply_virtual_branch(&project_id, &b_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].id, b.id);
assert!(!branches[0].selected_for_changes);
assert!(!branches[0].active);
assert_eq!(branches[1].id, b2.id);
assert!(branches[1].selected_for_changes);
assert!(branches[1].active);
}
#[tokio::test]
async fn deleting_selected_branch_selects_anther() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// first branch should be created as default
let b_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
// if default branch exists, new branch should not be created as default
let b2_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
let b = branches.iter().find(|b| b.id == b_id).unwrap();
let b2 = branches.iter().find(|b| b.id == b2_id).unwrap();
assert!(b.selected_for_changes);
assert!(!b2.selected_for_changes);
controller
.delete_virtual_branch(&project_id, &b_id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, b2.id);
assert!(branches[0].selected_for_changes);
}
#[tokio::test]
async fn create_virtual_branch_should_set_selected_for_changes() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
// first branch should be created as default
let b_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(branch.selected_for_changes);
// if default branch exists, new branch should not be created as default
let b_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(!branch.selected_for_changes);
// explicitly don't make this one default
let b_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
selected_for_changes: Some(false),
..Default::default()
},
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(!branch.selected_for_changes);
// explicitly make this one default
let b_id = controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
)
.await
.unwrap();
let branch = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b_id)
.unwrap();
assert!(branch.selected_for_changes);
}
#[tokio::test]
async fn update_virtual_branch_should_reset_selected_for_changes() {
let Test {
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let b1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(b1.selected_for_changes);
let b2_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let b2 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b2_id)
.unwrap();
assert!(!b2.selected_for_changes);
controller
.update_virtual_branch(
&project_id,
branch::BranchUpdateRequest {
id: b2_id,
selected_for_changes: Some(true),
..Default::default()
},
)
.await
.unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(!b1.selected_for_changes);
let b2 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b2_id)
.unwrap();
assert!(b2.selected_for_changes);
}
#[tokio::test]
async fn unapply_virtual_branch_should_reset_selected_for_changes() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let b1_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(b1.selected_for_changes);
controller
.unapply_virtual_branch(&project_id, &b1_id)
.await
.unwrap();
let b1 = controller
.list_virtual_branches(&project_id)
.await
.unwrap()
.into_iter()
.find(|b| b.id == b1_id)
.unwrap();
assert!(!b1.selected_for_changes);
}
#[tokio::test]
async fn hunks_distribution() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches[0].files.len(), 1);
controller
.create_virtual_branch(
&project_id,
&branch::BranchCreateRequest {
selected_for_changes: Some(true),
..Default::default()
},
)
.await
.unwrap();
std::fs::write(repository.path().join("another_file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[1].files.len(), 1);
}
#[tokio::test]
async fn applying_first_branch() {
let Test {
repository,
project_id,
controller,
..
} = Test::default();
controller
.set_base_branch(&project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
controller
.unapply_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
controller
.apply_virtual_branch(&project_id, &branches[0].id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(branches[0].active);
assert!(branches[0].selected_for_changes);
}
}