From ccec1868f83d325d82f2fc047d6e33496ee4c1e7 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Tue, 22 Oct 2024 15:26:53 +0200 Subject: [PATCH] Refactor - join stack and stack api crates Separation was previously needed due to cycle dependency with the repo crate. The stack_ext is to be refactored as an impl on stack --- Cargo.lock | 29 +- Cargo.toml | 2 - crates/gitbutler-branch-actions/Cargo.toml | 1 - crates/gitbutler-branch-actions/src/base.rs | 2 +- .../src/branch_manager/branch_creation.rs | 2 +- .../src/branch_upstream_integration.rs | 2 +- .../src/integration.rs | 2 +- .../src/move_commits.rs | 2 +- .../src/reorder_commits.rs | 2 +- crates/gitbutler-branch-actions/src/stack.rs | 4 +- crates/gitbutler-branch-actions/src/status.rs | 2 +- .../src/undo_commit.rs | 2 +- .../src/upstream_integration.rs | 2 +- .../gitbutler-branch-actions/src/virtual.rs | 2 +- .../tests/extra/mod.rs | 2 +- crates/gitbutler-edit-mode/Cargo.toml | 1 - crates/gitbutler-edit-mode/src/lib.rs | 2 +- crates/gitbutler-stack-api/Cargo.toml | 34 - crates/gitbutler-stack-api/src/lib.rs | 7 - crates/gitbutler-stack-api/tests/mod.rs | 1132 ----------------- crates/gitbutler-stack/Cargo.toml | 13 +- .../src/heads.rs | 0 crates/gitbutler-stack/src/lib.rs | 8 + .../src/series.rs | 0 .../src/stack_ext.rs | 4 +- .../tests/fixtures/stacking.sh | 0 crates/gitbutler-stack/tests/mod.rs | 1132 +++++++++++++++++ 27 files changed, 1172 insertions(+), 1219 deletions(-) delete mode 100644 crates/gitbutler-stack-api/Cargo.toml delete mode 100644 crates/gitbutler-stack-api/src/lib.rs delete mode 100644 crates/gitbutler-stack-api/tests/mod.rs rename crates/{gitbutler-stack-api => gitbutler-stack}/src/heads.rs (100%) rename crates/{gitbutler-stack-api => gitbutler-stack}/src/series.rs (100%) rename crates/{gitbutler-stack-api => gitbutler-stack}/src/stack_ext.rs (99%) rename crates/{gitbutler-stack-api => gitbutler-stack}/tests/fixtures/stacking.sh (100%) diff --git a/Cargo.lock b/Cargo.lock index 4586863fa..000f3da38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2192,7 +2192,6 @@ dependencies = [ "gitbutler-repo-actions", "gitbutler-serde", "gitbutler-stack", - "gitbutler-stack-api", "gitbutler-testsupport", "gitbutler-time", "gitbutler-url", @@ -2316,7 +2315,6 @@ dependencies = [ "gitbutler-reference", "gitbutler-repo", "gitbutler-stack", - "gitbutler-stack-api", "gitbutler-time", "serde", ] @@ -2584,43 +2582,26 @@ version = "0.0.0" dependencies = [ "anyhow", "git2", + "gitbutler-command-context", + "gitbutler-commit", "gitbutler-diff", "gitbutler-error", "gitbutler-fs", - "gitbutler-id", - "gitbutler-patch-reference", - "gitbutler-reference", - "gitbutler-serde", - "gitbutler-time", - "gix", - "itertools 0.13.0", - "serde", - "toml 0.8.19", -] - -[[package]] -name = "gitbutler-stack-api" -version = "0.0.0" -dependencies = [ - "anyhow", - "git2", - "gitbutler-branch", - "gitbutler-command-context", - "gitbutler-commit", "gitbutler-git", + "gitbutler-id", "gitbutler-patch-reference", "gitbutler-reference", "gitbutler-repo", "gitbutler-repo-actions", - "gitbutler-stack", + "gitbutler-serde", "gitbutler-testsupport", "gitbutler-time", "gix", "gix-utils 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.13.0", - "rand 0.8.5", "serde", "tempfile", + "toml 0.8.19", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index abc0147aa..7f457ca44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ members = [ "crates/gitbutler-edit-mode", "crates/gitbutler-cherry-pick", "crates/gitbutler-oxidize", - "crates/gitbutler-stack-api", "crates/gitbutler-stack", "crates/gitbutler-patch-reference", ] @@ -91,7 +90,6 @@ gitbutler-operating-modes = { path = "crates/gitbutler-operating-modes" } gitbutler-edit-mode = { path = "crates/gitbutler-edit-mode" } gitbutler-cherry-pick = { path = "crates/gitbutler-cherry-pick" } gitbutler-oxidize = { path = "crates/gitbutler-oxidize" } -gitbutler-stack-api = { path = "crates/gitbutler-stack-api" } gitbutler-stack = { path = "crates/gitbutler-stack" } gitbutler-patch-reference = { path = "crates/gitbutler-patch-reference" } gitbutler-forge = { path = "crates/gitbutler-forge" } diff --git a/crates/gitbutler-branch-actions/Cargo.toml b/crates/gitbutler-branch-actions/Cargo.toml index b7e94f822..5a59563ba 100644 --- a/crates/gitbutler-branch-actions/Cargo.toml +++ b/crates/gitbutler-branch-actions/Cargo.toml @@ -29,7 +29,6 @@ gitbutler-operating-modes.workspace = true gitbutler-cherry-pick.workspace = true gitbutler-oxidize.workspace = true gitbutler-stack.workspace = true -gitbutler-stack-api.workspace = true gitbutler-patch-reference.workspace = true serde = { workspace = true, features = ["std"] } bstr.workspace = true diff --git a/crates/gitbutler-branch-actions/src/base.rs b/crates/gitbutler-branch-actions/src/base.rs index ee9c9cfe5..b04cc5034 100644 --- a/crates/gitbutler-branch-actions/src/base.rs +++ b/crates/gitbutler-branch-actions/src/base.rs @@ -8,8 +8,8 @@ use gitbutler_project::FetchResult; use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::{LogUntil, RepositoryExt}; use gitbutler_repo_actions::RepoActionsExt; +use gitbutler_stack::StackExt; use gitbutler_stack::{BranchOwnershipClaims, Stack, Target, VirtualBranchesHandle}; -use gitbutler_stack_api::StackExt; use serde::Serialize; use crate::{ diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs index 28cd61400..8581cb78c 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -13,8 +13,8 @@ use gitbutler_repo::{ LogUntil, RepositoryExt, }; use gitbutler_repo_actions::RepoActionsExt; +use gitbutler_stack::StackExt; use gitbutler_stack::{BranchOwnershipClaims, Stack, StackId}; -use gitbutler_stack_api::StackExt; use gitbutler_time::time::now_since_unix_epoch_ms; use tracing::instrument; diff --git a/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs b/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs index 6cc1f7853..6e55f0ce6 100644 --- a/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/branch_upstream_integration.rs @@ -6,7 +6,7 @@ use gitbutler_repo::{ LogUntil, RepositoryExt as _, }; use gitbutler_stack::StackId; -use gitbutler_stack_api::{commit_by_oid_or_change_id, StackExt}; +use gitbutler_stack::{commit_by_oid_or_change_id, StackExt}; use crate::{ branch_trees::{ diff --git a/crates/gitbutler-branch-actions/src/integration.rs b/crates/gitbutler-branch-actions/src/integration.rs index 554af412d..adaea62d8 100644 --- a/crates/gitbutler-branch-actions/src/integration.rs +++ b/crates/gitbutler-branch-actions/src/integration.rs @@ -12,8 +12,8 @@ use gitbutler_operating_modes::OPEN_WORKSPACE_REFS; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::SignaturePurpose; use gitbutler_repo::{LogUntil, RepositoryExt}; +use gitbutler_stack::StackExt; use gitbutler_stack::{Stack, VirtualBranchesHandle}; -use gitbutler_stack_api::StackExt; use tracing::instrument; use crate::{branch_manager::BranchManagerExt, conflicts, VirtualBranchesExt}; diff --git a/crates/gitbutler-branch-actions/src/move_commits.rs b/crates/gitbutler-branch-actions/src/move_commits.rs index 332e907db..3905c8883 100644 --- a/crates/gitbutler-branch-actions/src/move_commits.rs +++ b/crates/gitbutler-branch-actions/src/move_commits.rs @@ -7,8 +7,8 @@ use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::{rebase::cherry_rebase_group, LogUntil, RepositoryExt}; +use gitbutler_stack::StackExt; use gitbutler_stack::{OwnershipClaim, StackId}; -use gitbutler_stack_api::StackExt; use std::collections::HashMap; /// moves commit from the branch it's in to the top of the target branch diff --git a/crates/gitbutler-branch-actions/src/reorder_commits.rs b/crates/gitbutler-branch-actions/src/reorder_commits.rs index 603533e0c..4f2d9f7af 100644 --- a/crates/gitbutler-branch-actions/src/reorder_commits.rs +++ b/crates/gitbutler-branch-actions/src/reorder_commits.rs @@ -2,8 +2,8 @@ use anyhow::{bail, Context as _, Result}; use gitbutler_command_context::CommandContext; use gitbutler_project::access::WorktreeWritePermission; use gitbutler_repo::{rebase::cherry_rebase_group, LogUntil, RepositoryExt as _}; +use gitbutler_stack::StackExt; use gitbutler_stack::StackId; -use gitbutler_stack_api::StackExt; use crate::{ branch_trees::{ diff --git a/crates/gitbutler-branch-actions/src/stack.rs b/crates/gitbutler-branch-actions/src/stack.rs index 525187f27..0bb73fb5f 100644 --- a/crates/gitbutler-branch-actions/src/stack.rs +++ b/crates/gitbutler-branch-actions/src/stack.rs @@ -7,10 +7,8 @@ use gitbutler_commit::commit_ext::CommitExt; use gitbutler_patch_reference::{CommitOrChangeId, PatchReference}; use gitbutler_project::Project; use gitbutler_repo_actions::RepoActionsExt; +use gitbutler_stack::{commit_by_oid_or_change_id, CommitsForId, PatchReferenceUpdate, StackExt}; use gitbutler_stack::{Stack, StackId, Target}; -use gitbutler_stack_api::{ - commit_by_oid_or_change_id, CommitsForId, PatchReferenceUpdate, StackExt, -}; use serde::{Deserialize, Serialize}; use crate::{ diff --git a/crates/gitbutler-branch-actions/src/status.rs b/crates/gitbutler-branch-actions/src/status.rs index 7a5a4665a..fdd85a76b 100644 --- a/crates/gitbutler-branch-actions/src/status.rs +++ b/crates/gitbutler-branch-actions/src/status.rs @@ -15,8 +15,8 @@ use gitbutler_command_context::CommandContext; use gitbutler_diff::{diff_files_into_hunks, GitHunk, Hunk, HunkHash}; use gitbutler_operating_modes::assure_open_workspace_mode; use gitbutler_project::access::WorktreeWritePermission; +use gitbutler_stack::StackExt; use gitbutler_stack::{BranchOwnershipClaims, OwnershipClaim, Stack, StackId}; -use gitbutler_stack_api::StackExt; use tracing::instrument; /// Represents the uncommitted status of the applied virtual branches in the workspace. diff --git a/crates/gitbutler-branch-actions/src/undo_commit.rs b/crates/gitbutler-branch-actions/src/undo_commit.rs index 383a20a27..93c20e732 100644 --- a/crates/gitbutler-branch-actions/src/undo_commit.rs +++ b/crates/gitbutler-branch-actions/src/undo_commit.rs @@ -2,8 +2,8 @@ use anyhow::{bail, Context as _, Result}; use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt as _; use gitbutler_repo::{rebase::cherry_rebase_group, LogUntil, RepositoryExt as _}; +use gitbutler_stack::StackExt; use gitbutler_stack::{Stack, StackId}; -use gitbutler_stack_api::StackExt; use crate::VirtualBranchesExt as _; diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs index a0bf875f9..f277a38ae 100644 --- a/crates/gitbutler-branch-actions/src/upstream_integration.rs +++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs @@ -7,8 +7,8 @@ use gitbutler_repo::{ LogUntil, RepositoryExt as _, }; use gitbutler_repo_actions::RepoActionsExt as _; +use gitbutler_stack::StackExt; use gitbutler_stack::{Stack, StackId, Target, VirtualBranchesHandle}; -use gitbutler_stack_api::StackExt; use serde::{Deserialize, Serialize}; use crate::{ diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 0b68a799c..833467977 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -28,10 +28,10 @@ use gitbutler_repo::{ LogUntil, RepositoryExt, }; use gitbutler_repo_actions::RepoActionsExt; +use gitbutler_stack::StackExt; use gitbutler_stack::{ reconcile_claims, BranchOwnershipClaims, Stack, StackId, Target, VirtualBranchesHandle, }; -use gitbutler_stack_api::StackExt; use gitbutler_time::time::now_since_unix_epoch_ms; use serde::Serialize; use std::collections::HashSet; diff --git a/crates/gitbutler-branch-actions/tests/extra/mod.rs b/crates/gitbutler-branch-actions/tests/extra/mod.rs index f56be83eb..e7c2358e9 100644 --- a/crates/gitbutler-branch-actions/tests/extra/mod.rs +++ b/crates/gitbutler-branch-actions/tests/extra/mod.rs @@ -20,8 +20,8 @@ use gitbutler_branch_actions::{ use gitbutler_commit::{commit_ext::CommitExt, commit_headers::CommitHeadersV2}; use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::RepositoryExt; +use gitbutler_stack::StackExt; use gitbutler_stack::{BranchOwnershipClaims, Target, VirtualBranchesHandle}; -use gitbutler_stack_api::StackExt; use gitbutler_testsupport::{commit_all, virtual_branches::set_test_target, Case, Suite}; use pretty_assertions::assert_eq; diff --git a/crates/gitbutler-edit-mode/Cargo.toml b/crates/gitbutler-edit-mode/Cargo.toml index 22c5b6686..cfe9bf8af 100644 --- a/crates/gitbutler-edit-mode/Cargo.toml +++ b/crates/gitbutler-edit-mode/Cargo.toml @@ -22,5 +22,4 @@ gitbutler-oplog.workspace = true gitbutler-diff.workspace = true gitbutler-stack.workspace = true gitbutler-cherry-pick.workspace = true -gitbutler-stack-api.workspace = true serde.workspace = true diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index 4f8c76dd2..09e485c3f 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -25,8 +25,8 @@ use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission} use gitbutler_reference::{ReferenceName, Refname}; use gitbutler_repo::{rebase::cherry_rebase, RepositoryExt}; use gitbutler_repo::{signature, SignaturePurpose}; +use gitbutler_stack::StackExt; use gitbutler_stack::{Stack, VirtualBranchesHandle}; -use gitbutler_stack_api::StackExt; use serde::Serialize; pub mod commands; diff --git a/crates/gitbutler-stack-api/Cargo.toml b/crates/gitbutler-stack-api/Cargo.toml deleted file mode 100644 index 98f47c695..000000000 --- a/crates/gitbutler-stack-api/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "gitbutler-stack-api" -version = "0.0.0" -edition = "2021" -authors = ["GitButler "] -publish = false - -[dependencies] -anyhow = "1.0.86" -itertools = "0.13" -rand = "0.8.5" -gix-utils = "0.1.12" -serde = { workspace = true, features = ["std"] } -git2.workspace = true -gix.workspace = true -tracing.workspace = true -gitbutler-command-context.workspace = true -gitbutler-branch.workspace = true -gitbutler-patch-reference.workspace = true -gitbutler-reference.workspace = true -gitbutler-repo.workspace = true -gitbutler-commit.workspace = true -gitbutler-time.workspace = true -gitbutler-stack.workspace = true - -[[test]] -name = "stack-api" -path = "tests/mod.rs" - -[dev-dependencies] -gitbutler-git = { workspace = true, features = ["test-askpass-path"] } -gitbutler-testsupport.workspace = true -gitbutler-repo-actions.workspace = true -tempfile = "3.13" diff --git a/crates/gitbutler-stack-api/src/lib.rs b/crates/gitbutler-stack-api/src/lib.rs deleted file mode 100644 index 921824ca1..000000000 --- a/crates/gitbutler-stack-api/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod heads; -mod series; -mod stack_ext; -pub use series::Series; -pub use stack_ext::{ - commit_by_oid_or_change_id, CommitsForId, PatchReferenceUpdate, StackExt, TargetUpdate, -}; diff --git a/crates/gitbutler-stack-api/tests/mod.rs b/crates/gitbutler-stack-api/tests/mod.rs deleted file mode 100644 index 8578cce10..000000000 --- a/crates/gitbutler-stack-api/tests/mod.rs +++ /dev/null @@ -1,1132 +0,0 @@ -use anyhow::Result; -use gitbutler_command_context::CommandContext; -use gitbutler_commit::commit_ext::CommitExt; -use gitbutler_patch_reference::{CommitOrChangeId, PatchReference}; -use gitbutler_reference::RemoteRefname; -use gitbutler_repo::{LogUntil, RepositoryExt as _}; -use gitbutler_repo_actions::RepoActionsExt; -use gitbutler_stack::VirtualBranchesHandle; -use gitbutler_stack_api::{PatchReferenceUpdate, StackExt, TargetUpdate}; -use itertools::Itertools; -use tempfile::TempDir; - -#[test] -fn init_success() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let test_ctx = test_ctx(&ctx)?; - let mut branch = test_ctx.branch; - let result = branch.initialize(&ctx); // this is noop really - assert!(result.is_ok()); - assert!(branch.initialized()); - assert_eq!(branch.heads.len(), 1); - assert_eq!(branch.heads[0].name, "a-branch-2"); // matches the stack name - assert_eq!( - branch.heads[0].target, - CommitOrChangeId::ChangeId( - ctx.repository() - .find_commit(branch.head())? - .change_id() - .unwrap() - ) - ); - // Assert persisted - assert_eq!(branch, test_ctx.handle.get_branch(branch.id)?); - Ok(()) -} - -#[test] -fn init_already_initialized_noop() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let test_ctx = test_ctx(&ctx)?; - let mut branch = test_ctx.branch; - let result = branch.initialize(&ctx); - assert!(result.is_ok()); - let result = branch.initialize(&ctx); - assert!(result.is_ok()); // noop - Ok(()) -} - -#[test] -fn add_series_success() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "asdf".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: Some("my description".into()), - }; - let result = test_ctx.branch.add_series(&ctx, reference, None); - assert!(result.is_ok()); - assert_eq!(test_ctx.branch.heads.len(), 2); - assert_eq!(test_ctx.branch.heads[0].name, "asdf"); - assert_eq!( - test_ctx.branch.heads[0].description, - Some("my description".into()) - ); - // Assert persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn add_series_top_of_stack() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let result = - test_ctx - .branch - .add_series_top_of_stack(&ctx, "asdf".into(), Some("my description".into())); - assert!(result.is_ok()); - assert_eq!(test_ctx.branch.heads.len(), 2); - assert_eq!(test_ctx.branch.heads[1].name, "asdf"); - assert_eq!( - test_ctx.branch.heads[1].description, - Some("my description".into()) - ); - // Assert persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn add_series_top_base() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let merge_base = ctx.repository().find_commit( - ctx.repository() - .merge_base(test_ctx.branch.head(), test_ctx.default_target.sha)?, - )?; - let reference = PatchReference { - name: "asdf".into(), - target: CommitOrChangeId::CommitId(merge_base.id().to_string()), - description: Some("my description".into()), - }; - let result = test_ctx.branch.add_series(&ctx, reference, None); - println!("{:?}", result); - // Assert persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn add_multiple_series() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - - assert_eq!(test_ctx.branch.heads.len(), 1); - assert_eq!(head_names(&test_ctx), vec!["a-branch-2"]); // defaults to stack name - let default_head = test_ctx.branch.heads[0].clone(); - - let head_4 = PatchReference { - name: "head_4".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), - description: None, - }; - let result = test_ctx - .branch - .add_series(&ctx, head_4, Some(default_head.name.clone())); - assert!(result.is_ok()); - assert_eq!(head_names(&test_ctx), vec!["a-branch-2", "head_4"]); - - let head_2 = PatchReference { - name: "head_2".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, head_2, None); - assert!(result.is_ok()); - assert_eq!( - head_names(&test_ctx), - vec!["head_2", "a-branch-2", "head_4"] - ); - - let head_1 = PatchReference { - name: "head_1".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()), - description: None, - }; - - let result = test_ctx.branch.add_series(&ctx, head_1, None); - assert!(result.is_ok()); - assert_eq!( - head_names(&test_ctx), - vec!["head_1", "head_2", "a-branch-2", "head_4"] - ); - Ok(()) -} - -#[test] -fn add_series_commit_id_when_change_id_available() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "asdf".into(), - target: CommitOrChangeId::CommitId(test_ctx.commits[1].id().to_string()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference, None); - assert_eq!( - result.err().unwrap().to_string(), - format!( - "The commit {} has a change id associated with it. Use the change id instead", - test_ctx.commits[1].id() - ) - ); - Ok(()) -} - -#[test] -fn add_series_invalid_name_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "name with spaces".into(), - target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference, None); - assert_eq!(result.err().unwrap().to_string(), "Invalid branch name"); - Ok(()) -} - -#[test] -fn add_series_duplicate_name_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "asdf".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); - assert!(result.is_ok()); - let result = test_ctx.branch.add_series(&ctx, reference, None); - assert_eq!( - result.err().unwrap().to_string(), - "A patch reference with the name asdf exists" - ); - Ok(()) -} - -#[test] -fn add_series_matching_git_ref_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "existing-branch".into(), - target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); - assert_eq!( - result.err().unwrap().to_string(), - "A git reference with the name existing-branch exists" - ); - Ok(()) -} - -#[test] -fn add_series_including_refs_head_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "refs/heads/my-branch".into(), - target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); - assert_eq!( - result.err().unwrap().to_string(), - "Stack head name cannot start with 'refs/heads'" - ); - Ok(()) -} - -#[test] -fn add_series_target_commit_doesnt_exist() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "my-branch".into(), - target: CommitOrChangeId::CommitId("30696678319e0fa3a20e54f22d47fc8cf1ceaade".into()), // does not exist - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); - assert!(result - .err() - .unwrap() - .to_string() - .contains("object not found"),); - Ok(()) -} - -#[test] -fn add_series_target_change_id_doesnt_exist() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let reference = PatchReference { - name: "my-branch".into(), - target: CommitOrChangeId::ChangeId("does-not-exist".into()), // does not exist - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); - assert_eq!( - result.err().unwrap().to_string(), - "No commit with change id does-not-exist found", - ); - Ok(()) -} - -#[test] -fn add_series_target_commit_not_in_stack() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let other_commit_id = test_ctx.other_commits.last().unwrap().id().to_string(); - let reference = PatchReference { - name: "my-branch".into(), - target: CommitOrChangeId::CommitId(other_commit_id.clone()), // does not exist - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); - assert_eq!( - result.err().unwrap().to_string(), - format!( - "The commit {} is not between the stack head and the stack base", - other_commit_id - ) - ); - Ok(()) -} - -#[test] -fn remove_series_last_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let result = test_ctx - .branch - .remove_series(&ctx, test_ctx.branch.heads[0].name.clone()); - assert_eq!( - result.err().unwrap().to_string(), - "Cannot remove the last branch from the stack" - ); - Ok(()) -} - -#[test] -fn remove_series_nonexistent_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let result = test_ctx - .branch - .remove_series(&ctx, "does-not-exist".to_string()); - assert_eq!( - result.err().unwrap().to_string(), - "Series with name does-not-exist not found" - ); - Ok(()) -} - -#[test] -fn remove_series_with_multiple_last_heads() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - - assert_eq!(test_ctx.branch.heads.len(), 1); - assert_eq!(head_names(&test_ctx), vec!["a-branch-2"]); // defaults to stack name - let default_head = test_ctx.branch.heads[0].clone(); - - let to_stay = PatchReference { - name: "to_stay".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None); - assert!(result.is_ok()); - assert_eq!(head_names(&test_ctx), vec!["to_stay", "a-branch-2"]); - - let result = test_ctx - .branch - .remove_series(&ctx, default_head.name.clone()); - assert!(result.is_ok()); - assert_eq!(head_names(&test_ctx), vec!["to_stay"]); - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()) - ); // it references the newest commit - Ok(()) -} - -#[test] -fn remove_series_no_orphan_commits() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - - assert_eq!(test_ctx.branch.heads.len(), 1); - assert_eq!(head_names(&test_ctx), vec!["a-branch-2"]); // defaults to stack name - let default_head = test_ctx.branch.heads[0].clone(); // references the newest commit - - let to_stay = PatchReference { - name: "to_stay".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()), - description: None, - }; // references the oldest commit - let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None); - assert!(result.is_ok()); - assert_eq!(head_names(&test_ctx), vec!["to_stay", "a-branch-2"]); - - let result = test_ctx - .branch - .remove_series(&ctx, default_head.name.clone()); - assert!(result.is_ok()); - assert_eq!(head_names(&test_ctx), vec!["to_stay"]); - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()) - ); // it was updated to reference the newest commit - Ok(()) -} - -#[test] -fn update_series_noop_does_nothing() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let heads_before = test_ctx.branch.heads.clone(); - let noop_update = PatchReferenceUpdate::default(); - let result = test_ctx - .branch - .update_series(&ctx, "a-branch-2".into(), &noop_update); - assert!(result.is_ok()); - assert_eq!(test_ctx.branch.heads, heads_before); - Ok(()) -} - -#[test] -fn update_series_name_fails_validation() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let update = PatchReferenceUpdate { - name: Some("invalid name".into()), - target_update: None, - description: None, - }; - let result = test_ctx - .branch - .update_series(&ctx, "a-branch-2".into(), &update); - assert_eq!(result.err().unwrap().to_string(), "Invalid branch name"); - Ok(()) -} - -#[test] -fn update_series_name_success() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let update = PatchReferenceUpdate { - name: Some("new-name".into()), - target_update: None, - description: None, - }; - let result = test_ctx - .branch - .update_series(&ctx, "a-branch-2".into(), &update); - assert!(result.is_ok()); - assert_eq!(test_ctx.branch.heads[0].name, "new-name"); - // Assert persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn update_series_set_description() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let update = PatchReferenceUpdate { - name: None, - target_update: None, - description: Some(Some("my description".into())), - }; - let result = test_ctx - .branch - .update_series(&ctx, "a-branch-2".into(), &update); - assert!(result.is_ok()); - assert_eq!( - test_ctx.branch.heads[0].description, - Some("my description".into()) - ); - // Assert persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn update_series_target_fails_commit_not_in_stack() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let other_commit_id = test_ctx.other_commits.last().unwrap().id().to_string(); - let update = PatchReferenceUpdate { - name: None, - target_update: Some(TargetUpdate { - target: CommitOrChangeId::CommitId(other_commit_id.clone()), - preceding_head: None, - }), - description: None, - }; - let result = test_ctx - .branch - .update_series(&ctx, "a-branch-2".into(), &update); - assert_eq!( - result.err().unwrap().to_string(), - format!( - "The commit {} is not between the stack head and the stack base", - other_commit_id - ) - ); - Ok(()) -} - -#[test] -fn update_series_target_orphan_commit_fails() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let initial_state = test_ctx.branch.heads.clone(); - let first_commit_change_id = test_ctx.commits.first().unwrap().change_id().unwrap(); - let update = PatchReferenceUpdate { - name: Some("new-lol".into()), - target_update: Some(TargetUpdate { - target: CommitOrChangeId::ChangeId(first_commit_change_id.clone()), - preceding_head: None, - }), - description: None, - }; - let result = test_ctx - .branch - .update_series(&ctx, "a-branch-2".into(), &update); - - assert_eq!( - result.err().unwrap().to_string(), - "This update would cause orphaned patches, which is disallowed" - ); - assert_eq!(initial_state, test_ctx.branch.heads); // no change due to failure - Ok(()) -} - -#[test] -fn update_series_target_success() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let commit_0_change_id = CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()); - let series_1 = PatchReference { - name: "series_1".into(), - target: commit_0_change_id.clone(), - description: None, - }; - let result = test_ctx.branch.add_series(&ctx, series_1, None); - assert!(result.is_ok()); - assert_eq!(test_ctx.branch.heads[0].target, commit_0_change_id); - let commit_1_change_id = CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()); - let update = PatchReferenceUpdate { - name: None, - target_update: Some(TargetUpdate { - target: commit_1_change_id.clone(), - preceding_head: None, - }), - description: None, - }; - let result = test_ctx - .branch - .update_series(&ctx, "series_1".into(), &update); - assert!(result.is_ok()); - assert_eq!(test_ctx.branch.heads[0].target, commit_1_change_id); - // Assert persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn push_series_success() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - - let state = VirtualBranchesHandle::new(ctx.project().gb_dir()); - let mut target = state.get_default_target()?; - target.push_remote_name = Some("origin".into()); - state.set_default_target(target)?; - - let result = test_ctx.branch.push_details(&ctx, "a-branch-2".into()); - assert!(result.is_ok()); - Ok(()) -} - -#[test] -fn update_name_after_push() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - - let state = VirtualBranchesHandle::new(ctx.project().gb_dir()); - let mut target = state.get_default_target()?; - target.push_remote_name = Some("origin".into()); - state.set_default_target(target)?; - - let push_details = test_ctx.branch.push_details(&ctx, "a-branch-2".into())?; - let result = ctx.push( - push_details.head, - &push_details.remote_refname, - false, - None, - Some(Some(test_ctx.branch.id)), - ); - assert!(result.is_ok()); - let result = test_ctx.branch.update_series( - &ctx, - "a-branch-2".into(), - &PatchReferenceUpdate { - name: Some("new-name".into()), - ..Default::default() - }, - ); - - assert!(result.is_ok()); - Ok(()) -} - -#[test] -fn list_series_default_head() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let result = test_ctx.branch.list_series(&ctx); - assert!(result.is_ok()); - let result = result?; - // the number of series matches the number of heads - assert_eq!(result.len(), test_ctx.branch.heads.len()); - assert_eq!(result[0].head.name, "a-branch-2"); - let expected_patches = test_ctx - .commits - .iter() - .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap())) - .collect_vec(); - assert_eq!(result[0].local_commits, expected_patches); - Ok(()) -} - -#[test] -fn list_series_two_heads_same_commit() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let head_before = PatchReference { - name: "head_before".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), - description: None, - }; - // add `head_before` before the initial head - let result = test_ctx.branch.add_series(&ctx, head_before, None); - assert!(result.is_ok()); - - let result = test_ctx.branch.list_series(&ctx); - assert!(result.is_ok()); - let result = result?; - - // the number of series matches the number of heads - assert_eq!(result.len(), test_ctx.branch.heads.len()); - - let expected_patches = test_ctx - .commits - .iter() - .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap())) - .collect_vec(); - // Expect the commits to be part of the `head_before` - assert_eq!(result[0].local_commits, expected_patches); - assert_eq!(result[0].head.name, "head_before"); - assert_eq!(result[1].local_commits, vec![]); - assert_eq!(result[1].head.name, "a-branch-2"); - Ok(()) -} - -#[test] -fn list_series_two_heads_different_commit() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - test_ctx.branch.initialize(&ctx)?; - let head_before = PatchReference { - name: "head_before".into(), - // point to the first commit - target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()), - description: None, - }; - // add `head_before` before the initial head - let result = test_ctx.branch.add_series(&ctx, head_before, None); - assert!(result.is_ok()); - let result = test_ctx.branch.list_series(&ctx); - assert!(result.is_ok()); - let result = result?; - // the number of series matches the number of heads - assert_eq!(result.len(), test_ctx.branch.heads.len()); - let mut expected_patches = test_ctx - .commits - .iter() - .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap())) - .collect_vec(); - assert_eq!(result[0].local_commits, vec![expected_patches.remove(0)]); // the first patch is in the first series - assert_eq!(result[0].head.name, "head_before"); - assert_eq!(expected_patches.len(), 2); - assert_eq!(result[1].local_commits, expected_patches); // the other two patches are in the second series - assert_eq!(result[1].head.name, "a-branch-2"); - - Ok(()) -} - -#[test] -fn set_stack_head_commit_invalid() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let result = test_ctx - .branch - .set_stack_head(&ctx, git2::Oid::zero(), None); - assert!(result.is_err()); - Ok(()) -} - -#[test] -fn set_stack_head() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let commit = test_ctx.other_commits.last().unwrap(); - let result = test_ctx.branch.set_stack_head(&ctx, commit.id(), None); - assert!(result.is_ok()); - let result = test_ctx.branch.list_series(&ctx)?; - assert_eq!( - result.first().unwrap().head.target, - CommitOrChangeId::ChangeId(commit.change_id().unwrap()) - ); - assert_eq!( - test_ctx.branch.head(), - test_ctx.other_commits.last().unwrap().id() - ); - Ok(()) -} - -#[test] -fn replace_head_single() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone(); - let from_head = PatchReference { - name: "from_head".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - test_ctx.branch.add_series(&ctx, from_head, None)?; - // replace with previous head - let result = test_ctx - .branch - .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]); - assert!(result.is_ok()); - // the head is updated to point to the new commit - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()) - ); - // the top of the stack is not changed - assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack); - // the state was persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_head_single_with_merge_base() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone(); - let from_head = PatchReference { - name: "from_head".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - test_ctx.branch.add_series(&ctx, from_head, None)?; - // replace with merge base - let merge_base = ctx.repository().find_commit( - ctx.repository() - .merge_base(test_ctx.branch.head(), test_ctx.default_target.sha)?, - )?; - let result = test_ctx - .branch - .replace_head(&ctx, &test_ctx.commits[1], &merge_base); - assert!(result.is_ok()); - // the head is updated to point to the new commit - // this time it's a commit id - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::CommitId(merge_base.id().to_string()) - ); - // the top of the stack is not changed - assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack); - // the state was persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_head_with_invalid_commit_error() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let from_head = PatchReference { - name: "from_head".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - test_ctx.branch.add_series(&ctx, from_head, None)?; - let stack = test_ctx.branch.clone(); - let result = - test_ctx - .branch - .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.other_commits[0]); //in another stack - assert!(result.is_err()); - // is unmodified - assert_eq!(stack, test_ctx.branch); - // same in persistence - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_head_with_same_noop() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let from_head = PatchReference { - name: "from_head".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - test_ctx.branch.add_series(&ctx, from_head, None)?; - let stack = test_ctx.branch.clone(); - let result = test_ctx - .branch - .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[1]); - assert!(result.is_ok()); - // is unmodified - assert_eq!(stack, test_ctx.branch); - // same in persistence - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_no_head_noop() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let stack = test_ctx.branch.clone(); - let result = test_ctx - .branch - .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]); - assert!(result.is_ok()); - // is unmodified - assert_eq!(stack, test_ctx.branch); - // same in persistence - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_non_member_commit_noop_no_error() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let stack = test_ctx.branch.clone(); - let result = - test_ctx - .branch - .replace_head(&ctx, &test_ctx.other_commits[0], &test_ctx.commits[0]); - assert!(result.is_ok()); - // is unmodified - assert_eq!(stack, test_ctx.branch); - Ok(()) -} - -#[test] -fn replace_top_of_stack_single() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let initial_head = ctx.repository().find_commit(test_ctx.branch.head())?; - - let result = test_ctx - .branch - .replace_head(&ctx, &initial_head, &test_ctx.commits[1]); - assert!(result.is_ok()); - // the head is updated to point to the new commit - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()) - ); - assert_eq!(test_ctx.branch.head(), test_ctx.commits[1].id()); - assert_eq!(test_ctx.branch.heads.len(), 1); - // the state was persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_head_multiple() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone(); - let from_head_1 = PatchReference { - name: "from_head_1".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - let from_head_2 = PatchReference { - name: "from_head_2".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - // both references point to the same commit - test_ctx.branch.add_series(&ctx, from_head_1, None)?; - test_ctx - .branch - .add_series(&ctx, from_head_2, Some("from_head_1".into()))?; - // replace the commit - let result = test_ctx - .branch - .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]); - assert!(result.is_ok()); - // both heads are updated to point to the new commit - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()) - ); - assert_eq!( - test_ctx.branch.heads[1].target, - CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()) - ); - // the top of the stack is not changed - assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack); - // the state was persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn replace_head_top_of_stack_multiple() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let initial_head = ctx.repository().find_commit(test_ctx.branch.head())?; - let extra_head = PatchReference { - name: "extra_head".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - // an extra head just beneath the top of the stack - test_ctx.branch.add_series(&ctx, extra_head, None)?; - // replace top of stack the commit - let result = test_ctx - .branch - .replace_head(&ctx, &initial_head, &test_ctx.commits[1]); - assert!(result.is_ok()); - // both heads are updated to point to the new commit - assert_eq!( - test_ctx.branch.heads[0].target, - CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()) - ); - assert_eq!( - test_ctx.branch.heads[1].target, - CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()) - ); - assert_eq!(test_ctx.branch.head(), test_ctx.commits[1].id()); - // order is the same - assert_eq!(test_ctx.branch.heads[0].name, "extra_head"); - // the state was persisted - assert_eq!( - test_ctx.branch, - test_ctx.handle.get_branch(test_ctx.branch.id)? - ); - Ok(()) -} - -#[test] -fn set_legacy_refname() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse()?; - test_ctx.branch.upstream = Some(remote_branch.clone()); - test_ctx - .branch - .set_legacy_compatible_stack_reference(&ctx)?; - // reference name was updated - assert_eq!(test_ctx.branch.heads[0].name, "my-branch"); - Ok(()) -} - -#[test] -fn set_legacy_refname_no_upstream_set() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let initial_state = test_ctx.branch.clone(); - test_ctx - .branch - .set_legacy_compatible_stack_reference(&ctx)?; - // no changes - assert_eq!(initial_state, test_ctx.branch); - Ok(()) -} - -#[test] -fn set_legacy_refname_multiple_heads() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse()?; - test_ctx.branch.upstream = Some(remote_branch.clone()); - let extra_head = PatchReference { - name: "extra_head".into(), - target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), - description: None, - }; - // an extra head just beneath the top of the stack - test_ctx.branch.add_series(&ctx, extra_head, None)?; - let initial_state = test_ctx.branch.clone(); - test_ctx - .branch - .set_legacy_compatible_stack_reference(&ctx)?; - // no changes - assert_eq!(initial_state, test_ctx.branch); - Ok(()) -} - -#[test] -fn set_legacy_refname_pushed() -> Result<()> { - let (ctx, _temp_dir) = command_ctx("multiple-commits")?; - let mut test_ctx = test_ctx(&ctx)?; - let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse()?; - test_ctx.branch.upstream = Some(remote_branch.clone()); - - let state = VirtualBranchesHandle::new(ctx.project().gb_dir()); - let mut target = state.get_default_target()?; - target.push_remote_name = Some("origin".into()); - state.set_default_target(target)?; - let push_details = test_ctx.branch.push_details(&ctx, "a-branch-2".into())?; - ctx.push( - push_details.head, - &push_details.remote_refname, - false, - None, - Some(Some(test_ctx.branch.id)), - )?; - let initial_state = test_ctx.branch.clone(); - - test_ctx - .branch - .set_legacy_compatible_stack_reference(&ctx)?; - // no changes - assert_eq!(initial_state, test_ctx.branch); - Ok(()) -} - -fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> { - gitbutler_testsupport::writable::fixture("stacking.sh", name) -} - -fn head_names(test_ctx: &TestContext) -> Vec { - test_ctx - .branch - .heads - .iter() - .map(|h| h.name.clone()) - .collect_vec() -} - -fn test_ctx(ctx: &CommandContext) -> Result { - let handle = VirtualBranchesHandle::new(ctx.project().gb_dir()); - let branches = handle.list_all_branches()?; - let branch = branches.iter().find(|b| b.name == "virtual").unwrap(); - let other_branch = branches.iter().find(|b| b.name != "virtual").unwrap(); - let target = handle.get_default_target()?; - let mut branch_commits = - ctx.repository() - .log(branch.head(), LogUntil::Commit(target.sha), false)?; - branch_commits.reverse(); - let mut other_commits = - ctx.repository() - .log(other_branch.head(), LogUntil::Commit(target.sha), false)?; - other_commits.reverse(); - Ok(TestContext { - branch: branch.clone(), - commits: branch_commits, - // other_branch: other_branch.clone(), - other_commits, - handle, - default_target: target, - }) -} -struct TestContext<'a> { - branch: gitbutler_stack::Stack, - /// Oldest commit first - commits: Vec>, - /// Oldest commit first - #[allow(dead_code)] - other_commits: Vec>, - handle: VirtualBranchesHandle, - default_target: gitbutler_stack::Target, -} diff --git a/crates/gitbutler-stack/Cargo.toml b/crates/gitbutler-stack/Cargo.toml index 6fb5e3c32..eae0f8c49 100644 --- a/crates/gitbutler-stack/Cargo.toml +++ b/crates/gitbutler-stack/Cargo.toml @@ -8,10 +8,12 @@ publish = false [dependencies] git2.workspace = true gix = { workspace = true, features = [] } +gix-utils = "0.1.12" itertools = "0.13" anyhow = "1.0.86" serde = { workspace = true, features = ["std"] } toml.workspace = true +tracing.workspace = true gitbutler-patch-reference.workspace = true gitbutler-reference.workspace = true gitbutler-id.workspace = true @@ -20,7 +22,16 @@ gitbutler-time.workspace = true gitbutler-diff.workspace = true gitbutler-error.workspace = true gitbutler-fs.workspace = true +gitbutler-command-context.workspace = true +gitbutler-repo.workspace = true +gitbutler-commit.workspace = true [[test]] -name = "branch" +name = "stack" path = "tests/mod.rs" + +[dev-dependencies] +gitbutler-git = { workspace = true, features = ["test-askpass-path"] } # Runtime test dependency +gitbutler-testsupport.workspace = true +gitbutler-repo-actions.workspace = true +tempfile = "3.13" diff --git a/crates/gitbutler-stack-api/src/heads.rs b/crates/gitbutler-stack/src/heads.rs similarity index 100% rename from crates/gitbutler-stack-api/src/heads.rs rename to crates/gitbutler-stack/src/heads.rs diff --git a/crates/gitbutler-stack/src/lib.rs b/crates/gitbutler-stack/src/lib.rs index 16576ba6c..17f904378 100644 --- a/crates/gitbutler-stack/src/lib.rs +++ b/crates/gitbutler-stack/src/lib.rs @@ -9,3 +9,11 @@ pub use ownership::{reconcile_claims, BranchOwnershipClaims, ClaimOutcome}; pub use stack::{Stack, StackId}; pub use state::{VirtualBranches as VirtualBranchesState, VirtualBranchesHandle}; pub use target::Target; + +mod heads; +mod series; +mod stack_ext; +pub use series::Series; +pub use stack_ext::{ + commit_by_oid_or_change_id, CommitsForId, PatchReferenceUpdate, StackExt, TargetUpdate, +}; diff --git a/crates/gitbutler-stack-api/src/series.rs b/crates/gitbutler-stack/src/series.rs similarity index 100% rename from crates/gitbutler-stack-api/src/series.rs rename to crates/gitbutler-stack/src/series.rs diff --git a/crates/gitbutler-stack-api/src/stack_ext.rs b/crates/gitbutler-stack/src/stack_ext.rs similarity index 99% rename from crates/gitbutler-stack-api/src/stack_ext.rs rename to crates/gitbutler-stack/src/stack_ext.rs index 057d2901c..c3698bc38 100644 --- a/crates/gitbutler-stack-api/src/stack_ext.rs +++ b/crates/gitbutler-stack/src/stack_ext.rs @@ -1,5 +1,7 @@ use std::str::FromStr; +use crate::Stack; +use crate::VirtualBranchesHandle; use anyhow::anyhow; use anyhow::bail; use anyhow::Context; @@ -13,8 +15,6 @@ use gitbutler_reference::Refname; use gitbutler_reference::RemoteRefname; use gitbutler_repo::LogUntil; use gitbutler_repo::RepositoryExt; -use gitbutler_stack::Stack; -use gitbutler_stack::VirtualBranchesHandle; use gix::validate::reference::name_partial; use gix_utils::str::decompose; use itertools::Itertools; diff --git a/crates/gitbutler-stack-api/tests/fixtures/stacking.sh b/crates/gitbutler-stack/tests/fixtures/stacking.sh similarity index 100% rename from crates/gitbutler-stack-api/tests/fixtures/stacking.sh rename to crates/gitbutler-stack/tests/fixtures/stacking.sh diff --git a/crates/gitbutler-stack/tests/mod.rs b/crates/gitbutler-stack/tests/mod.rs index 415608397..bbce397f1 100644 --- a/crates/gitbutler-stack/tests/mod.rs +++ b/crates/gitbutler-stack/tests/mod.rs @@ -1,2 +1,1134 @@ pub mod file_ownership; pub mod ownership; + +use anyhow::Result; +use gitbutler_command_context::CommandContext; +use gitbutler_commit::commit_ext::CommitExt; +use gitbutler_patch_reference::{CommitOrChangeId, PatchReference}; +use gitbutler_reference::RemoteRefname; +use gitbutler_repo::{LogUntil, RepositoryExt as _}; +use gitbutler_repo_actions::RepoActionsExt; +use gitbutler_stack::VirtualBranchesHandle; +use gitbutler_stack::{PatchReferenceUpdate, StackExt, TargetUpdate}; +use itertools::Itertools; +use tempfile::TempDir; + +#[test] +fn init_success() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let test_ctx = test_ctx(&ctx)?; + let mut branch = test_ctx.branch; + let result = branch.initialize(&ctx); // this is noop really + assert!(result.is_ok()); + assert!(branch.initialized()); + assert_eq!(branch.heads.len(), 1); + assert_eq!(branch.heads[0].name, "a-branch-2"); // matches the stack name + assert_eq!( + branch.heads[0].target, + CommitOrChangeId::ChangeId( + ctx.repository() + .find_commit(branch.head())? + .change_id() + .unwrap() + ) + ); + // Assert persisted + assert_eq!(branch, test_ctx.handle.get_branch(branch.id)?); + Ok(()) +} + +#[test] +fn init_already_initialized_noop() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let test_ctx = test_ctx(&ctx)?; + let mut branch = test_ctx.branch; + let result = branch.initialize(&ctx); + assert!(result.is_ok()); + let result = branch.initialize(&ctx); + assert!(result.is_ok()); // noop + Ok(()) +} + +#[test] +fn add_series_success() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "asdf".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: Some("my description".into()), + }; + let result = test_ctx.branch.add_series(&ctx, reference, None); + assert!(result.is_ok()); + assert_eq!(test_ctx.branch.heads.len(), 2); + assert_eq!(test_ctx.branch.heads[0].name, "asdf"); + assert_eq!( + test_ctx.branch.heads[0].description, + Some("my description".into()) + ); + // Assert persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn add_series_top_of_stack() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let result = + test_ctx + .branch + .add_series_top_of_stack(&ctx, "asdf".into(), Some("my description".into())); + assert!(result.is_ok()); + assert_eq!(test_ctx.branch.heads.len(), 2); + assert_eq!(test_ctx.branch.heads[1].name, "asdf"); + assert_eq!( + test_ctx.branch.heads[1].description, + Some("my description".into()) + ); + // Assert persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn add_series_top_base() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let merge_base = ctx.repository().find_commit( + ctx.repository() + .merge_base(test_ctx.branch.head(), test_ctx.default_target.sha)?, + )?; + let reference = PatchReference { + name: "asdf".into(), + target: CommitOrChangeId::CommitId(merge_base.id().to_string()), + description: Some("my description".into()), + }; + let result = test_ctx.branch.add_series(&ctx, reference, None); + println!("{:?}", result); + // Assert persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn add_multiple_series() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + + assert_eq!(test_ctx.branch.heads.len(), 1); + assert_eq!(head_names(&test_ctx), vec!["a-branch-2"]); // defaults to stack name + let default_head = test_ctx.branch.heads[0].clone(); + + let head_4 = PatchReference { + name: "head_4".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), + description: None, + }; + let result = test_ctx + .branch + .add_series(&ctx, head_4, Some(default_head.name.clone())); + assert!(result.is_ok()); + assert_eq!(head_names(&test_ctx), vec!["a-branch-2", "head_4"]); + + let head_2 = PatchReference { + name: "head_2".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, head_2, None); + assert!(result.is_ok()); + assert_eq!( + head_names(&test_ctx), + vec!["head_2", "a-branch-2", "head_4"] + ); + + let head_1 = PatchReference { + name: "head_1".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()), + description: None, + }; + + let result = test_ctx.branch.add_series(&ctx, head_1, None); + assert!(result.is_ok()); + assert_eq!( + head_names(&test_ctx), + vec!["head_1", "head_2", "a-branch-2", "head_4"] + ); + Ok(()) +} + +#[test] +fn add_series_commit_id_when_change_id_available() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "asdf".into(), + target: CommitOrChangeId::CommitId(test_ctx.commits[1].id().to_string()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference, None); + assert_eq!( + result.err().unwrap().to_string(), + format!( + "The commit {} has a change id associated with it. Use the change id instead", + test_ctx.commits[1].id() + ) + ); + Ok(()) +} + +#[test] +fn add_series_invalid_name_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "name with spaces".into(), + target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference, None); + assert_eq!(result.err().unwrap().to_string(), "Invalid branch name"); + Ok(()) +} + +#[test] +fn add_series_duplicate_name_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "asdf".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); + assert!(result.is_ok()); + let result = test_ctx.branch.add_series(&ctx, reference, None); + assert_eq!( + result.err().unwrap().to_string(), + "A patch reference with the name asdf exists" + ); + Ok(()) +} + +#[test] +fn add_series_matching_git_ref_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "existing-branch".into(), + target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); + assert_eq!( + result.err().unwrap().to_string(), + "A git reference with the name existing-branch exists" + ); + Ok(()) +} + +#[test] +fn add_series_including_refs_head_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "refs/heads/my-branch".into(), + target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); + assert_eq!( + result.err().unwrap().to_string(), + "Stack head name cannot start with 'refs/heads'" + ); + Ok(()) +} + +#[test] +fn add_series_target_commit_doesnt_exist() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "my-branch".into(), + target: CommitOrChangeId::CommitId("30696678319e0fa3a20e54f22d47fc8cf1ceaade".into()), // does not exist + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); + assert!(result + .err() + .unwrap() + .to_string() + .contains("object not found"),); + Ok(()) +} + +#[test] +fn add_series_target_change_id_doesnt_exist() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let reference = PatchReference { + name: "my-branch".into(), + target: CommitOrChangeId::ChangeId("does-not-exist".into()), // does not exist + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); + assert_eq!( + result.err().unwrap().to_string(), + "No commit with change id does-not-exist found", + ); + Ok(()) +} + +#[test] +fn add_series_target_commit_not_in_stack() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let other_commit_id = test_ctx.other_commits.last().unwrap().id().to_string(); + let reference = PatchReference { + name: "my-branch".into(), + target: CommitOrChangeId::CommitId(other_commit_id.clone()), // does not exist + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, reference.clone(), None); + assert_eq!( + result.err().unwrap().to_string(), + format!( + "The commit {} is not between the stack head and the stack base", + other_commit_id + ) + ); + Ok(()) +} + +#[test] +fn remove_series_last_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let result = test_ctx + .branch + .remove_series(&ctx, test_ctx.branch.heads[0].name.clone()); + assert_eq!( + result.err().unwrap().to_string(), + "Cannot remove the last branch from the stack" + ); + Ok(()) +} + +#[test] +fn remove_series_nonexistent_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let result = test_ctx + .branch + .remove_series(&ctx, "does-not-exist".to_string()); + assert_eq!( + result.err().unwrap().to_string(), + "Series with name does-not-exist not found" + ); + Ok(()) +} + +#[test] +fn remove_series_with_multiple_last_heads() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + + assert_eq!(test_ctx.branch.heads.len(), 1); + assert_eq!(head_names(&test_ctx), vec!["a-branch-2"]); // defaults to stack name + let default_head = test_ctx.branch.heads[0].clone(); + + let to_stay = PatchReference { + name: "to_stay".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None); + assert!(result.is_ok()); + assert_eq!(head_names(&test_ctx), vec!["to_stay", "a-branch-2"]); + + let result = test_ctx + .branch + .remove_series(&ctx, default_head.name.clone()); + assert!(result.is_ok()); + assert_eq!(head_names(&test_ctx), vec!["to_stay"]); + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()) + ); // it references the newest commit + Ok(()) +} + +#[test] +fn remove_series_no_orphan_commits() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + + assert_eq!(test_ctx.branch.heads.len(), 1); + assert_eq!(head_names(&test_ctx), vec!["a-branch-2"]); // defaults to stack name + let default_head = test_ctx.branch.heads[0].clone(); // references the newest commit + + let to_stay = PatchReference { + name: "to_stay".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()), + description: None, + }; // references the oldest commit + let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None); + assert!(result.is_ok()); + assert_eq!(head_names(&test_ctx), vec!["to_stay", "a-branch-2"]); + + let result = test_ctx + .branch + .remove_series(&ctx, default_head.name.clone()); + assert!(result.is_ok()); + assert_eq!(head_names(&test_ctx), vec!["to_stay"]); + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()) + ); // it was updated to reference the newest commit + Ok(()) +} + +#[test] +fn update_series_noop_does_nothing() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let heads_before = test_ctx.branch.heads.clone(); + let noop_update = PatchReferenceUpdate::default(); + let result = test_ctx + .branch + .update_series(&ctx, "a-branch-2".into(), &noop_update); + assert!(result.is_ok()); + assert_eq!(test_ctx.branch.heads, heads_before); + Ok(()) +} + +#[test] +fn update_series_name_fails_validation() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let update = PatchReferenceUpdate { + name: Some("invalid name".into()), + target_update: None, + description: None, + }; + let result = test_ctx + .branch + .update_series(&ctx, "a-branch-2".into(), &update); + assert_eq!(result.err().unwrap().to_string(), "Invalid branch name"); + Ok(()) +} + +#[test] +fn update_series_name_success() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let update = PatchReferenceUpdate { + name: Some("new-name".into()), + target_update: None, + description: None, + }; + let result = test_ctx + .branch + .update_series(&ctx, "a-branch-2".into(), &update); + assert!(result.is_ok()); + assert_eq!(test_ctx.branch.heads[0].name, "new-name"); + // Assert persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn update_series_set_description() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let update = PatchReferenceUpdate { + name: None, + target_update: None, + description: Some(Some("my description".into())), + }; + let result = test_ctx + .branch + .update_series(&ctx, "a-branch-2".into(), &update); + assert!(result.is_ok()); + assert_eq!( + test_ctx.branch.heads[0].description, + Some("my description".into()) + ); + // Assert persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn update_series_target_fails_commit_not_in_stack() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let other_commit_id = test_ctx.other_commits.last().unwrap().id().to_string(); + let update = PatchReferenceUpdate { + name: None, + target_update: Some(TargetUpdate { + target: CommitOrChangeId::CommitId(other_commit_id.clone()), + preceding_head: None, + }), + description: None, + }; + let result = test_ctx + .branch + .update_series(&ctx, "a-branch-2".into(), &update); + assert_eq!( + result.err().unwrap().to_string(), + format!( + "The commit {} is not between the stack head and the stack base", + other_commit_id + ) + ); + Ok(()) +} + +#[test] +fn update_series_target_orphan_commit_fails() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let initial_state = test_ctx.branch.heads.clone(); + let first_commit_change_id = test_ctx.commits.first().unwrap().change_id().unwrap(); + let update = PatchReferenceUpdate { + name: Some("new-lol".into()), + target_update: Some(TargetUpdate { + target: CommitOrChangeId::ChangeId(first_commit_change_id.clone()), + preceding_head: None, + }), + description: None, + }; + let result = test_ctx + .branch + .update_series(&ctx, "a-branch-2".into(), &update); + + assert_eq!( + result.err().unwrap().to_string(), + "This update would cause orphaned patches, which is disallowed" + ); + assert_eq!(initial_state, test_ctx.branch.heads); // no change due to failure + Ok(()) +} + +#[test] +fn update_series_target_success() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let commit_0_change_id = CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()); + let series_1 = PatchReference { + name: "series_1".into(), + target: commit_0_change_id.clone(), + description: None, + }; + let result = test_ctx.branch.add_series(&ctx, series_1, None); + assert!(result.is_ok()); + assert_eq!(test_ctx.branch.heads[0].target, commit_0_change_id); + let commit_1_change_id = CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()); + let update = PatchReferenceUpdate { + name: None, + target_update: Some(TargetUpdate { + target: commit_1_change_id.clone(), + preceding_head: None, + }), + description: None, + }; + let result = test_ctx + .branch + .update_series(&ctx, "series_1".into(), &update); + assert!(result.is_ok()); + assert_eq!(test_ctx.branch.heads[0].target, commit_1_change_id); + // Assert persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn push_series_success() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + + let state = VirtualBranchesHandle::new(ctx.project().gb_dir()); + let mut target = state.get_default_target()?; + target.push_remote_name = Some("origin".into()); + state.set_default_target(target)?; + + let result = test_ctx.branch.push_details(&ctx, "a-branch-2".into()); + assert!(result.is_ok()); + Ok(()) +} + +#[test] +fn update_name_after_push() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + + let state = VirtualBranchesHandle::new(ctx.project().gb_dir()); + let mut target = state.get_default_target()?; + target.push_remote_name = Some("origin".into()); + state.set_default_target(target)?; + + let push_details = test_ctx.branch.push_details(&ctx, "a-branch-2".into())?; + let result = ctx.push( + push_details.head, + &push_details.remote_refname, + false, + None, + Some(Some(test_ctx.branch.id)), + ); + assert!(result.is_ok()); + let result = test_ctx.branch.update_series( + &ctx, + "a-branch-2".into(), + &PatchReferenceUpdate { + name: Some("new-name".into()), + ..Default::default() + }, + ); + assert!(result.is_ok()); + Ok(()) +} + +#[test] +fn list_series_default_head() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let result = test_ctx.branch.list_series(&ctx); + assert!(result.is_ok()); + let result = result?; + // the number of series matches the number of heads + assert_eq!(result.len(), test_ctx.branch.heads.len()); + assert_eq!(result[0].head.name, "a-branch-2"); + let expected_patches = test_ctx + .commits + .iter() + .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap())) + .collect_vec(); + assert_eq!(result[0].local_commits, expected_patches); + Ok(()) +} + +#[test] +fn list_series_two_heads_same_commit() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let head_before = PatchReference { + name: "head_before".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()), + description: None, + }; + // add `head_before` before the initial head + let result = test_ctx.branch.add_series(&ctx, head_before, None); + assert!(result.is_ok()); + + let result = test_ctx.branch.list_series(&ctx); + assert!(result.is_ok()); + let result = result?; + + // the number of series matches the number of heads + assert_eq!(result.len(), test_ctx.branch.heads.len()); + + let expected_patches = test_ctx + .commits + .iter() + .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap())) + .collect_vec(); + // Expect the commits to be part of the `head_before` + assert_eq!(result[0].local_commits, expected_patches); + assert_eq!(result[0].head.name, "head_before"); + assert_eq!(result[1].local_commits, vec![]); + assert_eq!(result[1].head.name, "a-branch-2"); + Ok(()) +} + +#[test] +fn list_series_two_heads_different_commit() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + test_ctx.branch.initialize(&ctx)?; + let head_before = PatchReference { + name: "head_before".into(), + // point to the first commit + target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()), + description: None, + }; + // add `head_before` before the initial head + let result = test_ctx.branch.add_series(&ctx, head_before, None); + assert!(result.is_ok()); + let result = test_ctx.branch.list_series(&ctx); + assert!(result.is_ok()); + let result = result?; + // the number of series matches the number of heads + assert_eq!(result.len(), test_ctx.branch.heads.len()); + let mut expected_patches = test_ctx + .commits + .iter() + .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap())) + .collect_vec(); + assert_eq!(result[0].local_commits, vec![expected_patches.remove(0)]); // the first patch is in the first series + assert_eq!(result[0].head.name, "head_before"); + assert_eq!(expected_patches.len(), 2); + assert_eq!(result[1].local_commits, expected_patches); // the other two patches are in the second series + assert_eq!(result[1].head.name, "a-branch-2"); + + Ok(()) +} + +#[test] +fn set_stack_head_commit_invalid() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let result = test_ctx + .branch + .set_stack_head(&ctx, git2::Oid::zero(), None); + assert!(result.is_err()); + Ok(()) +} + +#[test] +fn set_stack_head() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let commit = test_ctx.other_commits.last().unwrap(); + let result = test_ctx.branch.set_stack_head(&ctx, commit.id(), None); + assert!(result.is_ok()); + let result = test_ctx.branch.list_series(&ctx)?; + assert_eq!( + result.first().unwrap().head.target, + CommitOrChangeId::ChangeId(commit.change_id().unwrap()) + ); + assert_eq!( + test_ctx.branch.head(), + test_ctx.other_commits.last().unwrap().id() + ); + Ok(()) +} + +#[test] +fn replace_head_single() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone(); + let from_head = PatchReference { + name: "from_head".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + test_ctx.branch.add_series(&ctx, from_head, None)?; + // replace with previous head + let result = test_ctx + .branch + .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]); + assert!(result.is_ok()); + // the head is updated to point to the new commit + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()) + ); + // the top of the stack is not changed + assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack); + // the state was persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_head_single_with_merge_base() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone(); + let from_head = PatchReference { + name: "from_head".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + test_ctx.branch.add_series(&ctx, from_head, None)?; + // replace with merge base + let merge_base = ctx.repository().find_commit( + ctx.repository() + .merge_base(test_ctx.branch.head(), test_ctx.default_target.sha)?, + )?; + let result = test_ctx + .branch + .replace_head(&ctx, &test_ctx.commits[1], &merge_base); + assert!(result.is_ok()); + // the head is updated to point to the new commit + // this time it's a commit id + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::CommitId(merge_base.id().to_string()) + ); + // the top of the stack is not changed + assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack); + // the state was persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_head_with_invalid_commit_error() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let from_head = PatchReference { + name: "from_head".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + test_ctx.branch.add_series(&ctx, from_head, None)?; + let stack = test_ctx.branch.clone(); + let result = + test_ctx + .branch + .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.other_commits[0]); //in another stack + assert!(result.is_err()); + // is unmodified + assert_eq!(stack, test_ctx.branch); + // same in persistence + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_head_with_same_noop() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let from_head = PatchReference { + name: "from_head".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + test_ctx.branch.add_series(&ctx, from_head, None)?; + let stack = test_ctx.branch.clone(); + let result = test_ctx + .branch + .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[1]); + assert!(result.is_ok()); + // is unmodified + assert_eq!(stack, test_ctx.branch); + // same in persistence + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_no_head_noop() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let stack = test_ctx.branch.clone(); + let result = test_ctx + .branch + .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]); + assert!(result.is_ok()); + // is unmodified + assert_eq!(stack, test_ctx.branch); + // same in persistence + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_non_member_commit_noop_no_error() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let stack = test_ctx.branch.clone(); + let result = + test_ctx + .branch + .replace_head(&ctx, &test_ctx.other_commits[0], &test_ctx.commits[0]); + assert!(result.is_ok()); + // is unmodified + assert_eq!(stack, test_ctx.branch); + Ok(()) +} + +#[test] +fn replace_top_of_stack_single() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let initial_head = ctx.repository().find_commit(test_ctx.branch.head())?; + + let result = test_ctx + .branch + .replace_head(&ctx, &initial_head, &test_ctx.commits[1]); + assert!(result.is_ok()); + // the head is updated to point to the new commit + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()) + ); + assert_eq!(test_ctx.branch.head(), test_ctx.commits[1].id()); + assert_eq!(test_ctx.branch.heads.len(), 1); + // the state was persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_head_multiple() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let top_of_stack = test_ctx.branch.heads.last().unwrap().target.clone(); + let from_head_1 = PatchReference { + name: "from_head_1".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + let from_head_2 = PatchReference { + name: "from_head_2".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + // both references point to the same commit + test_ctx.branch.add_series(&ctx, from_head_1, None)?; + test_ctx + .branch + .add_series(&ctx, from_head_2, Some("from_head_1".into()))?; + // replace the commit + let result = test_ctx + .branch + .replace_head(&ctx, &test_ctx.commits[1], &test_ctx.commits[0]); + assert!(result.is_ok()); + // both heads are updated to point to the new commit + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()) + ); + assert_eq!( + test_ctx.branch.heads[1].target, + CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap()) + ); + // the top of the stack is not changed + assert_eq!(test_ctx.branch.heads.last().unwrap().target, top_of_stack); + // the state was persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn replace_head_top_of_stack_multiple() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let initial_head = ctx.repository().find_commit(test_ctx.branch.head())?; + let extra_head = PatchReference { + name: "extra_head".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + // an extra head just beneath the top of the stack + test_ctx.branch.add_series(&ctx, extra_head, None)?; + // replace top of stack the commit + let result = test_ctx + .branch + .replace_head(&ctx, &initial_head, &test_ctx.commits[1]); + assert!(result.is_ok()); + // both heads are updated to point to the new commit + assert_eq!( + test_ctx.branch.heads[0].target, + CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()) + ); + assert_eq!( + test_ctx.branch.heads[1].target, + CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()) + ); + assert_eq!(test_ctx.branch.head(), test_ctx.commits[1].id()); + // order is the same + assert_eq!(test_ctx.branch.heads[0].name, "extra_head"); + // the state was persisted + assert_eq!( + test_ctx.branch, + test_ctx.handle.get_branch(test_ctx.branch.id)? + ); + Ok(()) +} + +#[test] +fn set_legacy_refname() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse()?; + test_ctx.branch.upstream = Some(remote_branch.clone()); + test_ctx + .branch + .set_legacy_compatible_stack_reference(&ctx)?; + // reference name was updated + assert_eq!(test_ctx.branch.heads[0].name, "my-branch"); + Ok(()) +} + +#[test] +fn set_legacy_refname_no_upstream_set() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let initial_state = test_ctx.branch.clone(); + test_ctx + .branch + .set_legacy_compatible_stack_reference(&ctx)?; + // no changes + assert_eq!(initial_state, test_ctx.branch); + Ok(()) +} + +#[test] +fn set_legacy_refname_multiple_heads() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse()?; + test_ctx.branch.upstream = Some(remote_branch.clone()); + let extra_head = PatchReference { + name: "extra_head".into(), + target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()), + description: None, + }; + // an extra head just beneath the top of the stack + test_ctx.branch.add_series(&ctx, extra_head, None)?; + let initial_state = test_ctx.branch.clone(); + test_ctx + .branch + .set_legacy_compatible_stack_reference(&ctx)?; + // no changes + assert_eq!(initial_state, test_ctx.branch); + Ok(()) +} + +#[test] +fn set_legacy_refname_pushed() -> Result<()> { + let (ctx, _temp_dir) = command_ctx("multiple-commits")?; + let mut test_ctx = test_ctx(&ctx)?; + let remote_branch: RemoteRefname = "refs/remotes/origin/my-branch".parse()?; + test_ctx.branch.upstream = Some(remote_branch.clone()); + + let state = VirtualBranchesHandle::new(ctx.project().gb_dir()); + let mut target = state.get_default_target()?; + target.push_remote_name = Some("origin".into()); + state.set_default_target(target)?; + let push_details = test_ctx.branch.push_details(&ctx, "a-branch-2".into())?; + ctx.push( + push_details.head, + &push_details.remote_refname, + false, + None, + Some(Some(test_ctx.branch.id)), + )?; + let initial_state = test_ctx.branch.clone(); + + test_ctx + .branch + .set_legacy_compatible_stack_reference(&ctx)?; + // no changes + assert_eq!(initial_state, test_ctx.branch); + Ok(()) +} + +fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> { + gitbutler_testsupport::writable::fixture("stacking.sh", name) +} + +fn head_names(test_ctx: &TestContext) -> Vec { + test_ctx + .branch + .heads + .iter() + .map(|h| h.name.clone()) + .collect_vec() +} + +fn test_ctx(ctx: &CommandContext) -> Result { + let handle = VirtualBranchesHandle::new(ctx.project().gb_dir()); + let branches = handle.list_all_branches()?; + let branch = branches.iter().find(|b| b.name == "virtual").unwrap(); + let other_branch = branches.iter().find(|b| b.name != "virtual").unwrap(); + let target = handle.get_default_target()?; + let mut branch_commits = + ctx.repository() + .log(branch.head(), LogUntil::Commit(target.sha), false)?; + branch_commits.reverse(); + let mut other_commits = + ctx.repository() + .log(other_branch.head(), LogUntil::Commit(target.sha), false)?; + other_commits.reverse(); + Ok(TestContext { + branch: branch.clone(), + commits: branch_commits, + // other_branch: other_branch.clone(), + other_commits, + handle, + default_target: target, + }) +} +struct TestContext<'a> { + branch: gitbutler_stack::Stack, + /// Oldest commit first + commits: Vec>, + /// Oldest commit first + #[allow(dead_code)] + other_commits: Vec>, + handle: VirtualBranchesHandle, + default_target: gitbutler_stack::Target, +}