mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-18 06:22:28 +03:00
add several history manipulation backend functions
this adds backend functions in Rust to do the following: * move file hunks between commits (basic) * undo any commit in a stack * insert a blank commit * move a commit within the stack * update a commit message in place
This commit is contained in:
parent
d6a882b1ba
commit
2b1d808314
@ -150,6 +150,9 @@ pub enum OperationType {
|
||||
UpdateCommitMessage,
|
||||
MoveCommit,
|
||||
RestoreFromSnapshot,
|
||||
ReorderCommit,
|
||||
InsertBlankCommit,
|
||||
MoveCommitFile,
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
@ -231,11 +231,70 @@ impl Controller {
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<git::Oid, Error> {
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.amend(project_id, branch_id, ownership)
|
||||
.amend(project_id, branch_id, commit_oid, ownership)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn move_commit_file(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
from_commit_oid: git::Oid,
|
||||
to_commit_oid: git::Oid,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<git::Oid, Error> {
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.move_commit_file(
|
||||
project_id,
|
||||
branch_id,
|
||||
from_commit_oid,
|
||||
to_commit_oid,
|
||||
ownership,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn undo_commit(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
) -> Result<(), Error> {
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.undo_commit(project_id, branch_id, commit_oid)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert_blank_commit(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
offset: i32,
|
||||
) -> Result<(), Error> {
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.insert_blank_commit(project_id, branch_id, commit_oid, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn reorder_commit(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
offset: i32,
|
||||
) -> Result<(), Error> {
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.reorder_commit(project_id, branch_id, commit_oid, offset)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -714,12 +773,14 @@ impl ControllerInner {
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<git::Oid, Error> {
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::amend(project_repository, branch_id, ownership).map_err(Into::into);
|
||||
let result = super::amend(project_repository, branch_id, commit_oid, ownership)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::AmendCommit),
|
||||
@ -728,6 +789,93 @@ impl ControllerInner {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn move_commit_file(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
from_commit_oid: git::Oid,
|
||||
to_commit_oid: git::Oid,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<git::Oid, Error> {
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::move_commit_file(
|
||||
project_repository,
|
||||
branch_id,
|
||||
from_commit_oid,
|
||||
to_commit_oid,
|
||||
ownership,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::MoveCommitFile),
|
||||
)?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn undo_commit(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
) -> Result<(), Error> {
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result =
|
||||
super::undo_commit(project_repository, branch_id, commit_oid).map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::UndoCommit),
|
||||
)?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn insert_blank_commit(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
offset: i32,
|
||||
) -> Result<(), Error> {
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, user| {
|
||||
let result =
|
||||
super::insert_blank_commit(project_repository, branch_id, commit_oid, user, offset)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::InsertBlankCommit),
|
||||
)?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn reorder_commit(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_id: &BranchId,
|
||||
commit_oid: git::Oid,
|
||||
offset: i32,
|
||||
) -> Result<(), Error> {
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
|
||||
self.with_verify_branch(project_id, |project_repository, _| {
|
||||
let result = super::reorder_commit(project_repository, branch_id, commit_oid, offset)
|
||||
.map_err(Into::into);
|
||||
snapshot::create(
|
||||
project_repository.project(),
|
||||
SnapshotDetails::new(OperationType::ReorderCommit),
|
||||
)?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn reset_virtual_branch(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
|
@ -6,6 +6,59 @@ use crate::{
|
||||
projects::ProjectId,
|
||||
};
|
||||
|
||||
// Generic error enum for use in the virtual branches module.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum VirtualBranchError {
|
||||
#[error("project")]
|
||||
Conflict(ProjectConflict),
|
||||
#[error("branch not found")]
|
||||
BranchNotFound(BranchNotFound),
|
||||
#[error("default target not set")]
|
||||
DefaultTargetNotSet(DefaultTargetNotSet),
|
||||
#[error("target ownership not found")]
|
||||
TargetOwnerhshipNotFound(BranchOwnershipClaims),
|
||||
#[error("git object {0} not found")]
|
||||
GitObjectNotFound(git::Oid),
|
||||
#[error("commit failed")]
|
||||
CommitFailed,
|
||||
#[error("rebase failed")]
|
||||
RebaseFailed,
|
||||
#[error("force push not allowed")]
|
||||
ForcePushNotAllowed(ForcePushNotAllowed),
|
||||
#[error("branch has no commits")]
|
||||
BranchHasNoCommits,
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl ErrorWithContext for VirtualBranchError {
|
||||
fn context(&self) -> Option<Context> {
|
||||
Some(match self {
|
||||
VirtualBranchError::Conflict(ctx) => ctx.to_context(),
|
||||
VirtualBranchError::BranchNotFound(ctx) => ctx.to_context(),
|
||||
VirtualBranchError::DefaultTargetNotSet(ctx) => ctx.to_context(),
|
||||
VirtualBranchError::TargetOwnerhshipNotFound(_) => {
|
||||
error::Context::new_static(Code::Branches, "target ownership not found")
|
||||
}
|
||||
VirtualBranchError::GitObjectNotFound(oid) => {
|
||||
error::Context::new(Code::Branches, format!("git object {oid} not found"))
|
||||
}
|
||||
VirtualBranchError::CommitFailed => {
|
||||
error::Context::new_static(Code::Branches, "commit failed")
|
||||
}
|
||||
VirtualBranchError::RebaseFailed => {
|
||||
error::Context::new_static(Code::Branches, "rebase failed")
|
||||
}
|
||||
VirtualBranchError::BranchHasNoCommits => error::Context::new_static(
|
||||
Code::Branches,
|
||||
"Branch has no commits - there is nothing to amend to",
|
||||
),
|
||||
VirtualBranchError::ForcePushNotAllowed(ctx) => ctx.to_context(),
|
||||
VirtualBranchError::Other(error) => return error.custom_context_or_root_cause().into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum VerifyError {
|
||||
#[error("head is detached")]
|
||||
@ -316,43 +369,6 @@ impl ForcePushNotAllowed {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AmendError {
|
||||
#[error("force push not allowed")]
|
||||
ForcePushNotAllowed(ForcePushNotAllowed),
|
||||
#[error("target ownership not found")]
|
||||
TargetOwnerhshipNotFound(BranchOwnershipClaims),
|
||||
#[error("branch has no commits")]
|
||||
BranchHasNoCommits,
|
||||
#[error("default target not set")]
|
||||
DefaultTargetNotSet(DefaultTargetNotSet),
|
||||
#[error("branch not found")]
|
||||
BranchNotFound(BranchNotFound),
|
||||
#[error("project is in conflict state")]
|
||||
Conflict(ProjectConflict),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl ErrorWithContext for AmendError {
|
||||
fn context(&self) -> Option<Context> {
|
||||
Some(match self {
|
||||
AmendError::ForcePushNotAllowed(ctx) => ctx.to_context(),
|
||||
AmendError::Conflict(ctx) => ctx.to_context(),
|
||||
AmendError::BranchNotFound(ctx) => ctx.to_context(),
|
||||
AmendError::BranchHasNoCommits => error::Context::new_static(
|
||||
Code::Branches,
|
||||
"Branch has no commits - there is nothing to amend to",
|
||||
),
|
||||
AmendError::DefaultTargetNotSet(ctx) => ctx.to_context(),
|
||||
AmendError::TargetOwnerhshipNotFound(_) => {
|
||||
error::Context::new_static(Code::Branches, "target ownership not found")
|
||||
}
|
||||
AmendError::Other(error) => return error.custom_context_or_root_cause().into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CherryPickError {
|
||||
#[error("target commit {0} not found ")]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,37 +1,5 @@
|
||||
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::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
assert!(matches!(
|
||||
controller
|
||||
.amend(project_id, &branch_id, &to_amend)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.downcast_ref(),
|
||||
Some(&errors::AmendError::BranchHasNoCommits)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forcepush_allowed() {
|
||||
let Test {
|
||||
@ -70,14 +38,12 @@ async fn forcepush_allowed() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
controller
|
||||
let commit_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
controller
|
||||
.push_virtual_branch(project_id, &branch_id, false, None)
|
||||
@ -89,7 +55,7 @@ async fn forcepush_allowed() {
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.amend(project_id, &branch_id, &to_amend)
|
||||
.amend(project_id, &branch_id, commit_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -137,14 +103,12 @@ async fn forcepush_forbidden() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
controller
|
||||
let commit_oid = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
controller
|
||||
.push_virtual_branch(project_id, &branch_id, false, None)
|
||||
@ -156,11 +120,11 @@ async fn forcepush_forbidden() {
|
||||
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
assert!(matches!(
|
||||
controller
|
||||
.amend(project_id, &branch_id, &to_amend)
|
||||
.amend(project_id, &branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.downcast_ref(),
|
||||
Some(errors::AmendError::ForcePushNotAllowed(_))
|
||||
Some(errors::VirtualBranchError::ForcePushNotAllowed(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -184,10 +148,9 @@ async fn non_locked_hunk() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
controller
|
||||
let commit_oid = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -203,14 +166,13 @@ async fn non_locked_hunk() {
|
||||
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::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.amend(project_id, &branch_id, &to_amend)
|
||||
.amend(project_id, &branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -247,10 +209,9 @@ async fn locked_hunk() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
controller
|
||||
let commit_oid = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -270,14 +231,13 @@ async fn locked_hunk() {
|
||||
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::BranchOwnershipClaims = "file.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.amend(project_id, &branch_id, &to_amend)
|
||||
.amend(project_id, &branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -319,10 +279,9 @@ async fn non_existing_ownership() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
controller
|
||||
let commit_oid = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -338,18 +297,17 @@ async fn non_existing_ownership() {
|
||||
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::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
assert!(matches!(
|
||||
controller
|
||||
.amend(project_id, &branch_id, &to_amend)
|
||||
.amend(project_id, &branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.downcast_ref(),
|
||||
Some(errors::AmendError::TargetOwnerhshipNotFound(_))
|
||||
Some(errors::VirtualBranchError::TargetOwnerhshipNotFound(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,145 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_blank_commit_down() {
|
||||
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();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let _commit3_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit three", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.insert_blank_commit(project_id, &branch_id, commit2_id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(branch.commits.len(), 4);
|
||||
assert_eq!(branch.commits[0].files.len(), 1);
|
||||
assert_eq!(branch.commits[1].files.len(), 2);
|
||||
assert_eq!(branch.commits[2].files.len(), 0); // blank commit
|
||||
|
||||
let descriptions = branch
|
||||
.commits
|
||||
.iter()
|
||||
.map(|c| c.description.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
descriptions,
|
||||
vec!["commit three", "commit two", "", "commit one"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_blank_commit_up() {
|
||||
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();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let _commit3_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit three", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.insert_blank_commit(project_id, &branch_id, commit2_id, -1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(branch.commits.len(), 4);
|
||||
assert_eq!(branch.commits[0].files.len(), 1);
|
||||
assert_eq!(branch.commits[1].files.len(), 0); // blank commit
|
||||
assert_eq!(branch.commits[2].files.len(), 2);
|
||||
|
||||
let descriptions = branch
|
||||
.commits
|
||||
.iter()
|
||||
.map(|c| c.description.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
descriptions,
|
||||
vec!["commit three", "", "commit two", "commit one"]
|
||||
);
|
||||
}
|
@ -57,14 +57,18 @@ mod create_virtual_branch_from_branch;
|
||||
mod delete_virtual_branch;
|
||||
mod fetch_from_target;
|
||||
mod init;
|
||||
mod insert_blank_commit;
|
||||
mod move_commit_file;
|
||||
mod move_commit_to_vbranch;
|
||||
mod references;
|
||||
mod reorder_commit;
|
||||
mod reset_virtual_branch;
|
||||
mod selected_for_changes;
|
||||
mod set_base_branch;
|
||||
mod squash;
|
||||
mod unapply;
|
||||
mod unapply_ownership;
|
||||
mod undo_commit;
|
||||
mod update_base_branch;
|
||||
mod update_commit_message;
|
||||
mod upstream;
|
||||
|
@ -0,0 +1,190 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn move_file_down() {
|
||||
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();
|
||||
let commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// amend another hunk
|
||||
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.move_commit_file(project_id, &branch_id, commit2_id, commit1_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(branch.commits.len(), 2);
|
||||
assert_eq!(branch.commits[0].files.len(), 1);
|
||||
assert_eq!(branch.commits[1].files.len(), 2); // this now has both file changes
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn move_file_up() {
|
||||
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();
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
let commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// amend another hunk
|
||||
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.move_commit_file(project_id, &branch_id, commit1_id, commit2_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(branch.commits.len(), 2);
|
||||
assert_eq!(branch.commits[0].files.len(), 2); // this now has both file changes
|
||||
assert_eq!(branch.commits[1].files.len(), 1);
|
||||
}
|
||||
|
||||
// This test is failing because the file is not being moved up to the correct commit
|
||||
// This is out of scope for the first release, but should be fixed in the future
|
||||
// where you can take overlapping hunks between commits and resolve a move between them
|
||||
/*
|
||||
#[tokio::test]
|
||||
async fn move_file_up_overlapping_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 = controller
|
||||
.create_virtual_branch(project_id, &branch::BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create bottom commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create middle commit one
|
||||
fs::write(repository.path().join("file2.txt"), "content2\ncontent2a\n").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create middle commit two
|
||||
fs::write(
|
||||
repository.path().join("file2.txt"),
|
||||
"content2\ncontent2a\ncontent2b\ncontent2c\ncontent2d",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let commit3_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit three", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create top commit
|
||||
fs::write(repository.path().join("file5.txt"), "content5").unwrap();
|
||||
let _commit4_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit four", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// move one line from middle commit two up to middle commit one
|
||||
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-6".parse().unwrap();
|
||||
controller
|
||||
.move_commit_file(project_id, &branch_id, commit2_id, commit3_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
dbg!(&branch.commits);
|
||||
assert_eq!(branch.commits.len(), 4);
|
||||
//
|
||||
}
|
||||
*/
|
@ -0,0 +1,123 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reorder_commit_down() {
|
||||
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();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.reorder_commit(project_id, &branch_id, commit2_id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(branch.commits.len(), 2);
|
||||
assert_eq!(branch.commits[0].files.len(), 1); // this now has the 2 file changes
|
||||
assert_eq!(branch.commits[1].files.len(), 2); // and this has the single file change
|
||||
|
||||
let descriptions = branch
|
||||
.commits
|
||||
.iter()
|
||||
.map(|c| c.description.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(descriptions, vec!["commit one", "commit two"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reorder_commit_up() {
|
||||
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();
|
||||
let commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let _commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.reorder_commit(project_id, &branch_id, commit1_id, -1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(branch.commits.len(), 2);
|
||||
assert_eq!(branch.commits[0].files.len(), 1); // this now has the 2 file changes
|
||||
assert_eq!(branch.commits[1].files.len(), 2); // and this has the single file change
|
||||
|
||||
let descriptions = branch
|
||||
.commits
|
||||
.iter()
|
||||
.map(|c| c.description.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(descriptions, vec!["commit one", "commit two"]);
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_commit_simple() {
|
||||
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();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let _commit3_id = controller
|
||||
.create_commit(project_id, &branch_id, "commit three", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.undo_commit(project_id, &branch_id, commit2_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
// should be two uncommitted files now (file2.txt and file3.txt)
|
||||
assert_eq!(branch.files.len(), 2);
|
||||
assert_eq!(branch.commits.len(), 2);
|
||||
assert_eq!(branch.commits[0].files.len(), 1);
|
||||
assert_eq!(branch.commits[1].files.len(), 1);
|
||||
|
||||
let descriptions = branch
|
||||
.commits
|
||||
.iter()
|
||||
.map(|c| c.description.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(descriptions, vec!["commit three", "commit one"]);
|
||||
}
|
@ -255,6 +255,11 @@ fn main() {
|
||||
virtual_branches::commands::reset_virtual_branch,
|
||||
virtual_branches::commands::cherry_pick_onto_virtual_branch,
|
||||
virtual_branches::commands::amend_virtual_branch,
|
||||
virtual_branches::commands::move_commit_file,
|
||||
virtual_branches::commands::undo_commit,
|
||||
virtual_branches::commands::insert_blank_commit,
|
||||
virtual_branches::commands::reorder_commit,
|
||||
virtual_branches::commands::update_commit_message,
|
||||
virtual_branches::commands::list_remote_branches,
|
||||
virtual_branches::commands::get_remote_branch_data,
|
||||
virtual_branches::commands::squash_branch_commit,
|
||||
|
@ -350,11 +350,86 @@ pub mod commands {
|
||||
handle: AppHandle,
|
||||
project_id: ProjectId,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git::Oid,
|
||||
ownership: BranchOwnershipClaims,
|
||||
) -> Result<git::Oid, Error> {
|
||||
let oid = handle
|
||||
.state::<Controller>()
|
||||
.amend(&project_id, &branch_id, &ownership)
|
||||
.amend(&project_id, &branch_id, commit_oid, &ownership)
|
||||
.await?;
|
||||
emit_vbranches(&handle, &project_id).await;
|
||||
Ok(oid)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn move_commit_file(
|
||||
handle: AppHandle,
|
||||
project_id: ProjectId,
|
||||
branch_id: BranchId,
|
||||
from_commit_oid: git::Oid,
|
||||
to_commit_oid: git::Oid,
|
||||
ownership: BranchOwnershipClaims,
|
||||
) -> Result<git::Oid, Error> {
|
||||
let oid = handle
|
||||
.state::<Controller>()
|
||||
.move_commit_file(
|
||||
&project_id,
|
||||
&branch_id,
|
||||
from_commit_oid,
|
||||
to_commit_oid,
|
||||
&ownership,
|
||||
)
|
||||
.await?;
|
||||
emit_vbranches(&handle, &project_id).await;
|
||||
Ok(oid)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn undo_commit(
|
||||
handle: AppHandle,
|
||||
project_id: ProjectId,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git::Oid,
|
||||
) -> Result<(), Error> {
|
||||
let oid = handle
|
||||
.state::<Controller>()
|
||||
.undo_commit(&project_id, &branch_id, commit_oid)
|
||||
.await?;
|
||||
emit_vbranches(&handle, &project_id).await;
|
||||
Ok(oid)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn insert_blank_commit(
|
||||
handle: AppHandle,
|
||||
project_id: ProjectId,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git::Oid,
|
||||
offset: i32,
|
||||
) -> Result<(), Error> {
|
||||
let oid = handle
|
||||
.state::<Controller>()
|
||||
.insert_blank_commit(&project_id, &branch_id, commit_oid, offset)
|
||||
.await?;
|
||||
emit_vbranches(&handle, &project_id).await;
|
||||
Ok(oid)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn reorder_commit(
|
||||
handle: AppHandle,
|
||||
project_id: ProjectId,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git::Oid,
|
||||
offset: i32,
|
||||
) -> Result<(), Error> {
|
||||
let oid = handle
|
||||
.state::<Controller>()
|
||||
.reorder_commit(&project_id, &branch_id, commit_oid, offset)
|
||||
.await?;
|
||||
emit_vbranches(&handle, &project_id).await;
|
||||
Ok(oid)
|
||||
@ -445,6 +520,8 @@ pub mod commands {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn update_commit_message(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: ProjectId,
|
||||
|
Loading…
Reference in New Issue
Block a user