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:
Scott Chacon 2024-04-29 15:03:01 +02:00
parent d6a882b1ba
commit 2b1d808314
12 changed files with 1667 additions and 338 deletions

View File

@ -150,6 +150,9 @@ pub enum OperationType {
UpdateCommitMessage, UpdateCommitMessage,
MoveCommit, MoveCommit,
RestoreFromSnapshot, RestoreFromSnapshot,
ReorderCommit,
InsertBlankCommit,
MoveCommitFile,
#[default] #[default]
Unknown, Unknown,
} }

View File

@ -231,11 +231,70 @@ impl Controller {
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,
branch_id: &BranchId, branch_id: &BranchId,
commit_oid: git::Oid,
ownership: &BranchOwnershipClaims, ownership: &BranchOwnershipClaims,
) -> Result<git::Oid, Error> { ) -> Result<git::Oid, Error> {
self.inner(project_id) self.inner(project_id)
.await .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 .await
} }
@ -714,12 +773,14 @@ impl ControllerInner {
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,
branch_id: &BranchId, branch_id: &BranchId,
commit_oid: git::Oid,
ownership: &BranchOwnershipClaims, ownership: &BranchOwnershipClaims,
) -> Result<git::Oid, Error> { ) -> Result<git::Oid, Error> {
let _permit = self.semaphore.acquire().await; let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, _| { 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( snapshot::create(
project_repository.project(), project_repository.project(),
SnapshotDetails::new(OperationType::AmendCommit), 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( pub async fn reset_virtual_branch(
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,

View File

@ -6,6 +6,59 @@ use crate::{
projects::ProjectId, 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)] #[derive(Debug, thiserror::Error)]
pub enum VerifyError { pub enum VerifyError {
#[error("head is detached")] #[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)] #[derive(Debug, thiserror::Error)]
pub enum CherryPickError { pub enum CherryPickError {
#[error("target commit {0} not found ")] #[error("target commit {0} not found ")]

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,5 @@
use super::*; 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] #[tokio::test]
async fn forcepush_allowed() { async fn forcepush_allowed() {
let Test { let Test {
@ -70,14 +38,12 @@ async fn forcepush_allowed() {
.await .await
.unwrap(); .unwrap();
{
// create commit // create commit
fs::write(repository.path().join("file.txt"), "content").unwrap(); fs::write(repository.path().join("file.txt"), "content").unwrap();
controller let commit_id = controller
.create_commit(project_id, &branch_id, "commit one", None, false) .create_commit(project_id, &branch_id, "commit one", None, false)
.await .await
.unwrap(); .unwrap();
};
controller controller
.push_virtual_branch(project_id, &branch_id, false, None) .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(); fs::write(repository.path().join("file2.txt"), "content2").unwrap();
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap(); let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
controller controller
.amend(project_id, &branch_id, &to_amend) .amend(project_id, &branch_id, commit_id, &to_amend)
.await .await
.unwrap(); .unwrap();
@ -137,14 +103,12 @@ async fn forcepush_forbidden() {
.await .await
.unwrap(); .unwrap();
{
// create commit // create commit
fs::write(repository.path().join("file.txt"), "content").unwrap(); fs::write(repository.path().join("file.txt"), "content").unwrap();
controller let commit_oid = controller
.create_commit(project_id, &branch_id, "commit one", None, false) .create_commit(project_id, &branch_id, "commit one", None, false)
.await .await
.unwrap(); .unwrap();
};
controller controller
.push_virtual_branch(project_id, &branch_id, false, None) .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(); let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
assert!(matches!( assert!(matches!(
controller controller
.amend(project_id, &branch_id, &to_amend) .amend(project_id, &branch_id, commit_oid, &to_amend)
.await .await
.unwrap_err() .unwrap_err()
.downcast_ref(), .downcast_ref(),
Some(errors::AmendError::ForcePushNotAllowed(_)) Some(errors::VirtualBranchError::ForcePushNotAllowed(_))
)); ));
} }
} }
@ -184,10 +148,9 @@ async fn non_locked_hunk() {
.await .await
.unwrap(); .unwrap();
{
// create commit // create commit
fs::write(repository.path().join("file.txt"), "content").unwrap(); fs::write(repository.path().join("file.txt"), "content").unwrap();
controller let commit_oid = controller
.create_commit(project_id, &branch_id, "commit one", None, false) .create_commit(project_id, &branch_id, "commit one", None, false)
.await .await
.unwrap(); .unwrap();
@ -203,14 +166,13 @@ async fn non_locked_hunk() {
assert_eq!(branch.commits.len(), 1); assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0); assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 1); assert_eq!(branch.commits[0].files.len(), 1);
};
{ {
// amend another hunk // amend another hunk
fs::write(repository.path().join("file2.txt"), "content2").unwrap(); fs::write(repository.path().join("file2.txt"), "content2").unwrap();
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap(); let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
controller controller
.amend(project_id, &branch_id, &to_amend) .amend(project_id, &branch_id, commit_oid, &to_amend)
.await .await
.unwrap(); .unwrap();
@ -247,10 +209,9 @@ async fn locked_hunk() {
.await .await
.unwrap(); .unwrap();
{
// create commit // create commit
fs::write(repository.path().join("file.txt"), "content").unwrap(); fs::write(repository.path().join("file.txt"), "content").unwrap();
controller let commit_oid = controller
.create_commit(project_id, &branch_id, "commit one", None, false) .create_commit(project_id, &branch_id, "commit one", None, false)
.await .await
.unwrap(); .unwrap();
@ -270,14 +231,13 @@ async fn locked_hunk() {
branch.commits[0].files[0].hunks[0].diff, branch.commits[0].files[0].hunks[0].diff,
"@@ -0,0 +1 @@\n+content\n\\ No newline at end of file\n" "@@ -0,0 +1 @@\n+content\n\\ No newline at end of file\n"
); );
};
{ {
// amend another hunk // amend another hunk
fs::write(repository.path().join("file.txt"), "more content").unwrap(); fs::write(repository.path().join("file.txt"), "more content").unwrap();
let to_amend: branch::BranchOwnershipClaims = "file.txt:1-2".parse().unwrap(); let to_amend: branch::BranchOwnershipClaims = "file.txt:1-2".parse().unwrap();
controller controller
.amend(project_id, &branch_id, &to_amend) .amend(project_id, &branch_id, commit_oid, &to_amend)
.await .await
.unwrap(); .unwrap();
@ -319,10 +279,9 @@ async fn non_existing_ownership() {
.await .await
.unwrap(); .unwrap();
{
// create commit // create commit
fs::write(repository.path().join("file.txt"), "content").unwrap(); fs::write(repository.path().join("file.txt"), "content").unwrap();
controller let commit_oid = controller
.create_commit(project_id, &branch_id, "commit one", None, false) .create_commit(project_id, &branch_id, "commit one", None, false)
.await .await
.unwrap(); .unwrap();
@ -338,18 +297,17 @@ async fn non_existing_ownership() {
assert_eq!(branch.commits.len(), 1); assert_eq!(branch.commits.len(), 1);
assert_eq!(branch.files.len(), 0); assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits[0].files.len(), 1); assert_eq!(branch.commits[0].files.len(), 1);
};
{ {
// amend non existing hunk // amend non existing hunk
let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap(); let to_amend: branch::BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
assert!(matches!( assert!(matches!(
controller controller
.amend(project_id, &branch_id, &to_amend) .amend(project_id, &branch_id, commit_oid, &to_amend)
.await .await
.unwrap_err() .unwrap_err()
.downcast_ref(), .downcast_ref(),
Some(errors::AmendError::TargetOwnerhshipNotFound(_)) Some(errors::VirtualBranchError::TargetOwnerhshipNotFound(_))
)); ));
} }
} }

View File

@ -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"]
);
}

View File

@ -57,14 +57,18 @@ mod create_virtual_branch_from_branch;
mod delete_virtual_branch; mod delete_virtual_branch;
mod fetch_from_target; mod fetch_from_target;
mod init; mod init;
mod insert_blank_commit;
mod move_commit_file;
mod move_commit_to_vbranch; mod move_commit_to_vbranch;
mod references; mod references;
mod reorder_commit;
mod reset_virtual_branch; mod reset_virtual_branch;
mod selected_for_changes; mod selected_for_changes;
mod set_base_branch; mod set_base_branch;
mod squash; mod squash;
mod unapply; mod unapply;
mod unapply_ownership; mod unapply_ownership;
mod undo_commit;
mod update_base_branch; mod update_base_branch;
mod update_commit_message; mod update_commit_message;
mod upstream; mod upstream;

View File

@ -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);
//
}
*/

View File

@ -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"]);
}

View File

@ -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"]);
}

View File

@ -255,6 +255,11 @@ fn main() {
virtual_branches::commands::reset_virtual_branch, virtual_branches::commands::reset_virtual_branch,
virtual_branches::commands::cherry_pick_onto_virtual_branch, virtual_branches::commands::cherry_pick_onto_virtual_branch,
virtual_branches::commands::amend_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::list_remote_branches,
virtual_branches::commands::get_remote_branch_data, virtual_branches::commands::get_remote_branch_data,
virtual_branches::commands::squash_branch_commit, virtual_branches::commands::squash_branch_commit,

View File

@ -350,11 +350,86 @@ pub mod commands {
handle: AppHandle, handle: AppHandle,
project_id: ProjectId, project_id: ProjectId,
branch_id: BranchId, branch_id: BranchId,
commit_oid: git::Oid,
ownership: BranchOwnershipClaims, ownership: BranchOwnershipClaims,
) -> Result<git::Oid, Error> { ) -> Result<git::Oid, Error> {
let oid = handle let oid = handle
.state::<Controller>() .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?; .await?;
emit_vbranches(&handle, &project_id).await; emit_vbranches(&handle, &project_id).await;
Ok(oid) Ok(oid)
@ -445,6 +520,8 @@ pub mod commands {
Ok(()) Ok(())
} }
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn update_commit_message( pub async fn update_commit_message(
handle: tauri::AppHandle, handle: tauri::AppHandle,
project_id: ProjectId, project_id: ProjectId,