gitbutler/gitbutler-app/tests/virtual_branches.rs
Kiril Videlov 90e71e6458 use zero context hunks internally
Use hunks with zero context lines for virtual branch ownership. Reconstruct the context lines before returning hunks to the frontend
working on tests WIP
handle new files
cleanup comments
skip binary files
🐛 fix: add trailing newline to diff string in hunk_with_context function
handle empty diffs
fix diff header expectations
2024-01-14 23:42:34 +02:00

5795 lines
193 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 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 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 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);
}
}