diff --git a/Cargo.lock b/Cargo.lock index 01ccdf255..e98a56516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1970,6 +1970,7 @@ dependencies = [ "gitbutler-id", "gitbutler-reference", "gitbutler-serde", + "gitbutler-tagged-string", "hex", "itertools 0.13.0", "lazy_static", @@ -2120,6 +2121,7 @@ dependencies = [ "gitbutler-project", "gitbutler-repo", "gitbutler-serde", + "gitbutler-tagged-string", "gix", "itertools 0.13.0", "pretty_assertions", @@ -2271,6 +2273,7 @@ dependencies = [ "gitbutler-repo", "gitbutler-secret", "gitbutler-storage", + "gitbutler-tagged-string", "gitbutler-testsupport", "gitbutler-user", "gitbutler-virtual", @@ -2368,6 +2371,7 @@ dependencies = [ "gitbutler-reference", "gitbutler-repo", "gitbutler-serde", + "gitbutler-tagged-string", "gitbutler-testsupport", "gitbutler-time", "gitbutler-url", diff --git a/Cargo.toml b/Cargo.toml index 24386a0a5..93d395c0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,46 +5,52 @@ members = [ "crates/gitbutler-watcher", "crates/gitbutler-watcher/vendor/debouncer", "crates/gitbutler-testsupport", - "crates/gitbutler-cli", - "crates/gitbutler-virtual", + "crates/gitbutler-cli", + "crates/gitbutler-virtual", "crates/gitbutler-sync", - "crates/gitbutler-oplog", - "crates/gitbutler-branchstate", + "crates/gitbutler-oplog", + "crates/gitbutler-branchstate", "crates/gitbutler-repo", - "crates/gitbutler-command-context", - "crates/gitbutler-feedback", + "crates/gitbutler-command-context", + "crates/gitbutler-feedback", "crates/gitbutler-config", "crates/gitbutler-project", - "crates/gitbutler-user", + "crates/gitbutler-user", "crates/gitbutler-branch", "crates/gitbutler-reference", "crates/gitbutler-error", - "crates/gitbutler-serde", - "crates/gitbutler-secret", - "crates/gitbutler-id", - "crates/gitbutler-storage", + "crates/gitbutler-serde", + "crates/gitbutler-secret", + "crates/gitbutler-id", + "crates/gitbutler-storage", "crates/gitbutler-fs", - "crates/gitbutler-time", - "crates/gitbutler-commit", - "crates/gitbutler-tagged-string", + "crates/gitbutler-time", + "crates/gitbutler-commit", + "crates/gitbutler-tagged-string", "crates/gitbutler-url", ] resolver = "2" [workspace.dependencies] # Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes. -gix = { git = "https://github.com/Byron/gitoxide", rev = "55cbc1b9d6f210298a86502a7f20f9736c7e963e", default-features = false, features = [] } -git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] } +gix = { git = "https://github.com/Byron/gitoxide", rev = "55cbc1b9d6f210298a86502a7f20f9736c7e963e", default-features = false, features = [ +] } +git2 = { version = "0.18.3", features = [ + "vendored-openssl", + "vendored-libgit2", +] } uuid = { version = "1.8.0", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } thiserror = "1.0.61" tokio = { version = "1.38.0", default-features = false } keyring = "2.3.3" +anyhow = "1.0.86" +gitbutler-id = { path = "crates/gitbutler-id" } gitbutler-git = { path = "crates/gitbutler-git" } gitbutler-watcher = { path = "crates/gitbutler-watcher" } gitbutler-testsupport = { path = "crates/gitbutler-testsupport" } -gitbutler-cli ={ path = "crates/gitbutler-cli" } +gitbutler-cli = { path = "crates/gitbutler-cli" } gitbutler-virtual = { path = "crates/gitbutler-virtual" } gitbutler-sync = { path = "crates/gitbutler-sync" } gitbutler-oplog = { path = "crates/gitbutler-oplog" } @@ -60,7 +66,6 @@ gitbutler-reference = { path = "crates/gitbutler-reference" } gitbutler-error = { path = "crates/gitbutler-error" } gitbutler-serde = { path = "crates/gitbutler-serde" } gitbutler-secret = { path = "crates/gitbutler-secret" } -gitbutler-id = { path = "crates/gitbutler-id" } gitbutler-storage = { path = "crates/gitbutler-storage" } gitbutler-fs = { path = "crates/gitbutler-fs" } gitbutler-time = { path = "crates/gitbutler-time" } diff --git a/crates/gitbutler-branch/Cargo.toml b/crates/gitbutler-branch/Cargo.toml index 241ce489a..364b00bec 100644 --- a/crates/gitbutler-branch/Cargo.toml +++ b/crates/gitbutler-branch/Cargo.toml @@ -9,10 +9,11 @@ publish = false anyhow = "1.0.86" git2.workspace = true gitbutler-reference.workspace = true +gitbutler-tagged-string.workspace = true gitbutler-serde.workspace = true gitbutler-id.workspace = true itertools = "0.13" -serde = { workspace = true, features = ["std"]} +serde = { workspace = true, features = ["std"] } bstr = "1.9.1" md5 = "0.7.0" hex = "0.4.3" @@ -20,5 +21,5 @@ tracing = "0.1.40" lazy_static = "1.4.0" [[test]] -name="branch" +name = "branch" path = "tests/mod.rs" diff --git a/crates/gitbutler-branch/src/branch_ext.rs b/crates/gitbutler-branch/src/branch_ext.rs index 6a0a7dac5..8fdef7669 100644 --- a/crates/gitbutler-branch/src/branch_ext.rs +++ b/crates/gitbutler-branch/src/branch_ext.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use gitbutler_reference::ReferenceName; +use gitbutler_tagged_string::ReferenceName; pub trait BranchExt { fn reference_name(&self) -> Result; diff --git a/crates/gitbutler-virtual/src/dedup.rs b/crates/gitbutler-branch/src/dedup.rs similarity index 100% rename from crates/gitbutler-virtual/src/dedup.rs rename to crates/gitbutler-branch/src/dedup.rs diff --git a/crates/gitbutler-branch/src/lib.rs b/crates/gitbutler-branch/src/lib.rs index 25c3a0646..1fb950f58 100644 --- a/crates/gitbutler-branch/src/lib.rs +++ b/crates/gitbutler-branch/src/lib.rs @@ -1,5 +1,6 @@ pub mod branch; pub mod branch_ext; +pub mod dedup; pub mod diff; pub mod file_ownership; pub mod hunk; diff --git a/crates/gitbutler-oplog/Cargo.toml b/crates/gitbutler-oplog/Cargo.toml index 76d2e5b70..f6f9f323e 100644 --- a/crates/gitbutler-oplog/Cargo.toml +++ b/crates/gitbutler-oplog/Cargo.toml @@ -10,7 +10,7 @@ anyhow = "1.0.86" git2.workspace = true gitbutler-branchstate.workspace = true gitbutler-repo.workspace = true -serde = { workspace = true, features = ["std"]} +serde = { workspace = true, features = ["std"] } itertools = "0.13" strum = { version = "0.26", features = ["derive"] } tracing = "0.1.40" @@ -20,9 +20,10 @@ gitbutler-project.workspace = true gitbutler-branch.workspace = true gitbutler-serde.workspace = true gitbutler-fs.workspace = true +gitbutler-tagged-string.workspace = true [[test]] -name="oplog" +name = "oplog" path = "tests/mod.rs" [dev-dependencies] diff --git a/crates/gitbutler-oplog/src/snapshot.rs b/crates/gitbutler-oplog/src/snapshot.rs index 1c4c4da73..4f61d3d89 100644 --- a/crates/gitbutler-oplog/src/snapshot.rs +++ b/crates/gitbutler-oplog/src/snapshot.rs @@ -1,6 +1,7 @@ use anyhow::Result; use gitbutler_branch::branch::{Branch, BranchUpdateRequest}; use gitbutler_project::Project; +use gitbutler_tagged_string::ReferenceName; use std::vec; use crate::{ @@ -14,7 +15,7 @@ pub trait Snapshot { fn snapshot_branch_unapplied( &self, snapshot_tree: git2::Oid, - result: Result<&git2::Branch, &anyhow::Error>, + result: Result<&ReferenceName, &anyhow::Error>, ) -> anyhow::Result<()>; fn snapshot_commit_undo( @@ -48,9 +49,9 @@ impl Snapshot for Project { fn snapshot_branch_unapplied( &self, snapshot_tree: git2::Oid, - result: Result<&git2::Branch, &anyhow::Error>, + result: Result<&ReferenceName, &anyhow::Error>, ) -> anyhow::Result<()> { - let result = result.map(|o| o.name().ok().flatten().map(|s| s.to_string())); + let result = result.map(|s| Some(s.to_string())); let details = SnapshotDetails::new(OperationKind::UnapplyBranch) .with_trailers(result_trailer(result, "name".to_string())); self.commit_snapshot(snapshot_tree, details)?; diff --git a/crates/gitbutler-reference/src/lib.rs b/crates/gitbutler-reference/src/lib.rs index 7f925d17a..d0a72bef5 100644 --- a/crates/gitbutler-reference/src/lib.rs +++ b/crates/gitbutler-reference/src/lib.rs @@ -1,5 +1,4 @@ mod refname; -use gitbutler_tagged_string::TaggedString; pub use refname::{LocalRefname, Refname, RemoteRefname, VirtualRefname}; use regex::Regex; @@ -7,7 +6,3 @@ pub fn normalize_branch_name(name: &str) -> String { let pattern = Regex::new("[^A-Za-z0-9_/.#]+").unwrap(); pattern.replace_all(name, "-").to_string() } - -pub struct _ReferenceName; -/// The name of a reference ie. `refs/heads/master` -pub type ReferenceName = TaggedString<_ReferenceName>; diff --git a/crates/gitbutler-tagged-string/src/lib.rs b/crates/gitbutler-tagged-string/src/lib.rs index 2e95136ef..a4f265a51 100644 --- a/crates/gitbutler-tagged-string/src/lib.rs +++ b/crates/gitbutler-tagged-string/src/lib.rs @@ -54,3 +54,7 @@ impl fmt::Debug for TaggedString { self.0.fmt(f) } } + +pub struct _ReferenceName; +/// The name of a reference ie. `refs/heads/master` +pub type ReferenceName = TaggedString<_ReferenceName>; diff --git a/crates/gitbutler-tauri/Cargo.toml b/crates/gitbutler-tauri/Cargo.toml index b12a81ab9..ce0b08744 100644 --- a/crates/gitbutler-tauri/Cargo.toml +++ b/crates/gitbutler-tauri/Cargo.toml @@ -32,7 +32,7 @@ git2.workspace = true once_cell = "1.19" reqwest = { version = "0.12.4", features = ["json"] } serde.workspace = true -serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] } +serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] } tauri-plugin-context-menu = { git = "https://github.com/c2r0b/tauri-plugin-context-menu", branch = "main" } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } @@ -41,7 +41,7 @@ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", br log = "^0.4" thiserror.workspace = true # The features here optimize for performance. -tokio = { workspace = true, features = [ "rt-multi-thread", "parking_lot" ] } +tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot"] } tracing = "0.1.40" tracing-appender = "0.2.3" tracing-subscriber = "0.3.17" @@ -60,15 +60,23 @@ gitbutler-error.workspace = true gitbutler-secret.workspace = true gitbutler-id.workspace = true gitbutler-storage.workspace = true +gitbutler-tagged-string.workspace = true open = "5" [dependencies.tauri] version = "1.7.0" features = [ - "http-all", "os-all", "dialog-open", "fs-read-file", - "path-all", "process-relaunch", "protocol-asset", - "shell-open", "window-maximize", "window-start-dragging", - "window-unmaximize" + "http-all", + "os-all", + "dialog-open", + "fs-read-file", + "path-all", + "process-relaunch", + "protocol-asset", + "shell-open", + "window-maximize", + "window-start-dragging", + "window-unmaximize", ] [lints.clippy] @@ -86,4 +94,4 @@ devtools = ["tauri/devtools"] # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] -error-context = ["dep:backtrace" ] +error-context = ["dep:backtrace"] diff --git a/crates/gitbutler-tauri/src/virtual_branches.rs b/crates/gitbutler-tauri/src/virtual_branches.rs index 4f46302e3..0e840a8ff 100644 --- a/crates/gitbutler-tauri/src/virtual_branches.rs +++ b/crates/gitbutler-tauri/src/virtual_branches.rs @@ -6,8 +6,8 @@ pub mod commands { use gitbutler_error::error::Code; use gitbutler_project as projects; use gitbutler_project::ProjectId; - use gitbutler_reference::ReferenceName; use gitbutler_reference::{Refname, RemoteRefname}; + use gitbutler_tagged_string::ReferenceName; use gitbutler_virtual::assets; use gitbutler_virtual::base::BaseBranch; use gitbutler_virtual::files::RemoteBranchFile; diff --git a/crates/gitbutler-virtual/Cargo.toml b/crates/gitbutler-virtual/Cargo.toml index d697bcbb7..e53725515 100644 --- a/crates/gitbutler-virtual/Cargo.toml +++ b/crates/gitbutler-virtual/Cargo.toml @@ -22,7 +22,8 @@ gitbutler-id.workspace = true gitbutler-time.workspace = true gitbutler-commit.workspace = true gitbutler-url.workspace = true -serde = { workspace = true, features = ["std"]} +gitbutler-tagged-string.workspace = true +serde = { workspace = true, features = ["std"] } bstr = "1.9.1" diffy = "0.3.0" hex = "0.4.3" @@ -38,7 +39,7 @@ urlencoding = "2.1.3" reqwest = { version = "0.12.4", features = ["json"] } [[test]] -name="virtual" +name = "virtual" path = "tests/virtual_branches/mod.rs" [dev-dependencies] diff --git a/crates/gitbutler-virtual/src/base.rs b/crates/gitbutler-virtual/src/base.rs index 520ac0a82..ad8e73de7 100644 --- a/crates/gitbutler-virtual/src/base.rs +++ b/crates/gitbutler-virtual/src/base.rs @@ -11,10 +11,11 @@ use gitbutler_command_context::ProjectRepository; use gitbutler_project::FetchResult; use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt}; +use gitbutler_tagged_string::ReferenceName; use serde::Serialize; use super::r#virtual as vb; -use super::r#virtual::convert_to_real_branch; +use crate::branch_manager::BranchManagerAccess; use crate::conflicts::RepoConflicts; use crate::integration::{get_workspace_head, update_gitbutler_integration}; use crate::remote::{commit_to_remote_commit, RemoteCommit}; @@ -328,7 +329,7 @@ fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> { // update the target sha pub fn update_base_branch( project_repository: &ProjectRepository, -) -> anyhow::Result>> { +) -> anyhow::Result> { project_repository.assure_resolved()?; // look up the target and see if there is a new oid @@ -344,7 +345,7 @@ pub fn update_base_branch( .peel_to_commit() .context(format!("failed to peel branch {} to commit", target.branch))?; - let mut unapplied_branch_names: Vec = Vec::new(); + let mut unapplied_branch_names: Vec = Vec::new(); if new_target_commit.id() == target.sha { return Ok(unapplied_branch_names); @@ -421,11 +422,9 @@ pub fn update_base_branch( if branch_tree_merge_index.has_conflicts() { // branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back. - let unapplied_real_branch = convert_to_real_branch( - project_repository, - branch.id, - Default::default(), - )?; + let branch_manager = project_repository.branch_manager(); + let unapplied_real_branch = + branch_manager.convert_to_real_branch(branch.id, Default::default())?; unapplied_branch_names.push(unapplied_real_branch); @@ -457,11 +456,9 @@ pub fn update_base_branch( if branch_head_merge_index.has_conflicts() { // branch commits conflict with new target, make sure the branch is // unapplied. conflicts witll be dealt with when applying it back. - let unapplied_real_branch = convert_to_real_branch( - project_repository, - branch.id, - Default::default(), - )?; + let branch_manager = project_repository.branch_manager(); + let unapplied_real_branch = + branch_manager.convert_to_real_branch(branch.id, Default::default())?; unapplied_branch_names.push(unapplied_real_branch); return Ok(None); diff --git a/crates/gitbutler-virtual/src/branch_manager.rs b/crates/gitbutler-virtual/src/branch_manager.rs new file mode 100644 index 000000000..4d7d4c11d --- /dev/null +++ b/crates/gitbutler-virtual/src/branch_manager.rs @@ -0,0 +1,724 @@ +use crate::{ + conflicts::{self, RepoConflicts}, + ensure_selected_for_changes, get_applied_status, + integration::{get_integration_commiter, update_gitbutler_integration}, + set_ownership, undo_commit, write_tree, NameConflitResolution, VirtualBranchHunk, +}; +use anyhow::{anyhow, bail, Context, Result}; +use git2::build::TreeUpdateBuilder; +use gitbutler_branch::{ + branch::{self, BranchCreateRequest, BranchId}, + branch_ext::BranchExt, + dedup::dedup, + diff, + ownership::BranchOwnershipClaims, +}; +use gitbutler_branchstate::VirtualBranchesAccess; +use gitbutler_command_context::ProjectRepository; +use gitbutler_commit::commit_headers::{CommitHeadersV2, HasCommitHeaders}; +use gitbutler_error::error::Marker; +use gitbutler_oplog::snapshot::Snapshot; +use gitbutler_reference::{normalize_branch_name, Refname}; +use gitbutler_repo::{rebase::cherry_rebase, RepoActions, RepositoryExt}; +use gitbutler_tagged_string::ReferenceName; +use gitbutler_time::time::now_since_unix_epoch_ms; + +pub struct BranchManager<'l> { + project_repository: &'l ProjectRepository, +} + +pub trait BranchManagerAccess { + fn branch_manager(&self) -> BranchManager; +} + +impl BranchManagerAccess for ProjectRepository { + fn branch_manager(&self) -> BranchManager { + BranchManager { + project_repository: self, + } + } +} + +impl<'l> BranchManager<'l> { + pub fn create_virtual_branch(&self, create: &BranchCreateRequest) -> Result { + let vb_state = self.project_repository.project().virtual_branches(); + + let default_target = vb_state.get_default_target()?; + + let commit = self + .project_repository + .repo() + .find_commit(default_target.sha) + .context("failed to find default target commit")?; + + let tree = commit + .tree() + .context("failed to find defaut target commit tree")?; + + let mut all_virtual_branches = vb_state + .list_branches_in_workspace() + .context("failed to read virtual branches")?; + + let name = dedup( + &all_virtual_branches + .iter() + .map(|b| b.name.as_str()) + .collect::>(), + create + .name + .as_ref() + .unwrap_or(&"Virtual branch".to_string()), + ); + + _ = self + .project_repository + .project() + .snapshot_branch_creation(name.clone()); + + all_virtual_branches.sort_by_key(|branch| branch.order); + + let order = create.order.unwrap_or(vb_state.next_order_index()?); + + let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes { + if selected_for_changes { + for mut other_branch in vb_state + .list_branches_in_workspace() + .context("failed to read virtual branches")? + { + other_branch.selected_for_changes = None; + vb_state.set_branch(other_branch.clone())?; + } + Some(now_since_unix_epoch_ms()) + } else { + None + } + } else { + (!all_virtual_branches + .iter() + .any(|b| b.selected_for_changes.is_some())) + .then_some(now_since_unix_epoch_ms()) + }; + + // make space for the new branch + for (i, branch) in all_virtual_branches.iter().enumerate() { + let mut branch = branch.clone(); + let new_order = if i < order { i } else { i + 1 }; + if branch.order != new_order { + branch.order = new_order; + vb_state.set_branch(branch.clone())?; + } + } + + let now = gitbutler_time::time::now_ms(); + + let mut branch = branch::Branch { + id: BranchId::generate(), + name: name.clone(), + notes: String::new(), + upstream: None, + upstream_head: None, + tree: tree.id(), + head: default_target.sha, + created_timestamp_ms: now, + updated_timestamp_ms: now, + ownership: BranchOwnershipClaims::default(), + order, + selected_for_changes, + allow_rebasing: self.project_repository.project().ok_with_force_push.into(), + old_applied: true, + in_workspace: true, + not_in_workspace_wip_change_id: None, + source_refname: None, + }; + + if let Some(ownership) = &create.ownership { + set_ownership(&vb_state, &mut branch, ownership).context("failed to set ownership")?; + } + + vb_state.set_branch(branch.clone())?; + self.project_repository.add_branch_reference(&branch)?; + + Ok(branch) + } + + pub fn create_virtual_branch_from_branch(&self, upstream: &Refname) -> Result { + // only set upstream if it's not the default target + let upstream_branch = match upstream { + Refname::Other(_) | Refname::Virtual(_) => { + // we only support local or remote branches + bail!("branch {upstream} must be a local or remote branch"); + } + Refname::Remote(remote) => Some(remote.clone()), + Refname::Local(local) => local.remote().cloned(), + }; + + let branch_name = upstream + .branch() + .expect("always a branch reference") + .to_string(); + + let _ = self + .project_repository + .project() + .snapshot_branch_creation(branch_name.clone()); + + let vb_state = self.project_repository.project().virtual_branches(); + + let default_target = vb_state.get_default_target()?; + + if let Refname::Remote(remote_upstream) = upstream { + if default_target.branch == *remote_upstream { + bail!("cannot create a branch from default target") + } + } + + let repo = self.project_repository.repo(); + let head_reference = + repo.find_reference(&upstream.to_string()) + .map_err(|err| match err { + err if err.code() == git2::ErrorCode::NotFound => { + anyhow!("branch {upstream} was not found") + } + err => err.into(), + })?; + let head_commit = head_reference + .peel_to_commit() + .context("failed to peel to commit")?; + let head_commit_tree = head_commit.tree().context("failed to find tree")?; + + let virtual_branches = vb_state + .list_branches_in_workspace() + .context("failed to read virtual branches")? + .into_iter() + .collect::>(); + + let order = vb_state.next_order_index()?; + + let selected_for_changes = (!virtual_branches + .iter() + .any(|b| b.selected_for_changes.is_some())) + .then_some(now_since_unix_epoch_ms()); + + let now = gitbutler_time::time::now_ms(); + + // add file ownership based off the diff + let target_commit = repo.find_commit(default_target.sha)?; + let merge_base_oid = repo.merge_base(target_commit.id(), head_commit.id())?; + let merge_base_tree = repo.find_commit(merge_base_oid)?.tree()?; + + // do a diff between the head of this branch and the target base + let diff = diff::trees( + self.project_repository.repo(), + &merge_base_tree, + &head_commit_tree, + )?; + + // assign ownership to the branch + let ownership = diff.iter().fold( + BranchOwnershipClaims::default(), + |mut ownership, (file_path, file)| { + for hunk in &file.hunks { + ownership.put( + format!( + "{}:{}", + file_path.display(), + VirtualBranchHunk::gen_id(hunk.new_start, hunk.new_lines) + ) + .parse() + .unwrap(), + ); + } + ownership + }, + ); + + let branch = if let Ok(Some(mut branch)) = + vb_state.find_by_source_refname_where_not_in_workspace(upstream) + { + branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id()); + branch.upstream = upstream_branch; + branch.tree = head_commit_tree.id(); + branch.head = head_commit.id(); + branch.ownership = ownership; + branch.order = order; + branch.selected_for_changes = selected_for_changes; + branch.allow_rebasing = self.project_repository.project().ok_with_force_push.into(); + branch.old_applied = true; + branch.in_workspace = true; + + branch + } else { + branch::Branch { + id: BranchId::generate(), + name: branch_name.clone(), + notes: String::new(), + source_refname: Some(upstream.clone()), + upstream_head: upstream_branch.is_some().then_some(head_commit.id()), + upstream: upstream_branch, + tree: head_commit_tree.id(), + head: head_commit.id(), + created_timestamp_ms: now, + updated_timestamp_ms: now, + ownership, + order, + selected_for_changes, + allow_rebasing: self.project_repository.project().ok_with_force_push.into(), + old_applied: true, + in_workspace: true, + not_in_workspace_wip_change_id: None, + } + }; + + vb_state.set_branch(branch.clone())?; + self.project_repository.add_branch_reference(&branch)?; + + match self.apply_branch(branch.id) { + Ok(_) => Ok(branch.id), + Err(err) + if err + .downcast_ref() + .map_or(false, |marker: &Marker| *marker == Marker::ProjectConflict) => + { + // if branch conflicts with the workspace, it's ok. keep it unapplied + Ok(branch.id) + } + Err(err) => Err(err).context("failed to apply"), + } + } + + fn apply_branch(&self, branch_id: BranchId) -> Result { + self.project_repository.assure_resolved()?; + self.project_repository.assure_unconflicted()?; + let repo = self.project_repository.repo(); + + let vb_state = self.project_repository.project().virtual_branches(); + let default_target = vb_state.get_default_target()?; + + let mut branch = vb_state.get_branch_in_workspace(branch_id)?; + + let target_commit = repo + .find_commit(default_target.sha) + .context("failed to find target commit")?; + let target_tree = target_commit.tree().context("failed to get target tree")?; + + // calculate the merge base and make sure it's the same as the target commit + // if not, we need to merge or rebase the branch to get it up to date + + let merge_base = repo + .merge_base(default_target.sha, branch.head) + .context(format!( + "failed to find merge base between {} and {}", + default_target.sha, branch.head + ))?; + if merge_base != default_target.sha { + // Branch is out of date, merge or rebase it + let merge_base_tree = repo + .find_commit(merge_base) + .context(format!("failed to find merge base commit {}", merge_base))? + .tree() + .context("failed to find merge base tree")?; + + let branch_tree = repo + .find_tree(branch.tree) + .context("failed to find branch tree")?; + + let mut merge_index = repo + .merge_trees(&merge_base_tree, &branch_tree, &target_tree, None) + .context("failed to merge trees")?; + + if merge_index.has_conflicts() { + // currently we can only deal with the merge problem branch + for branch in vb_state + .list_branches_in_workspace()? + .iter() + .filter(|branch| branch.id != branch_id) + { + self.convert_to_real_branch(branch.id, Default::default())?; + } + + // apply the branch + vb_state.set_branch(branch.clone())?; + + // checkout the conflicts + repo.checkout_index_builder(&mut merge_index) + .allow_conflicts() + .conflict_style_merge() + .force() + .checkout() + .context("failed to checkout index")?; + + // mark conflicts + let conflicts = merge_index + .conflicts() + .context("failed to get merge index conflicts")?; + let mut merge_conflicts = Vec::new(); + for path in conflicts.flatten() { + if let Some(ours) = path.our { + let path = std::str::from_utf8(&ours.path) + .context("failed to convert path to utf8")? + .to_string(); + merge_conflicts.push(path); + } + } + conflicts::mark( + self.project_repository, + &merge_conflicts, + Some(default_target.sha), + )?; + + return Ok(branch.name); + } + + let head_commit = repo + .find_commit(branch.head) + .context("failed to find head commit")?; + + let merged_branch_tree_oid = merge_index + .write_tree_to(self.project_repository.repo()) + .context("failed to write tree")?; + + let merged_branch_tree = repo + .find_tree(merged_branch_tree_oid) + .context("failed to find tree")?; + + let ok_with_force_push = branch.allow_rebasing; + if branch.upstream.is_some() && !ok_with_force_push { + // branch was pushed to upstream, and user doesn't like force pushing. + // create a merge commit to avoid the need of force pushing then. + + let new_branch_head = self.project_repository.commit( + format!( + "Merged {}/{} into {}", + default_target.branch.remote(), + default_target.branch.branch(), + branch.name + ) + .as_str(), + &merged_branch_tree, + &[&head_commit, &target_commit], + None, + )?; + + // ok, update the virtual branch + branch.head = new_branch_head; + } else { + let rebase = cherry_rebase( + self.project_repository, + target_commit.id(), + target_commit.id(), + branch.head, + ); + let mut rebase_success = true; + let mut last_rebase_head = branch.head; + match rebase { + Ok(rebase_oid) => { + if let Some(oid) = rebase_oid { + last_rebase_head = oid; + } + } + Err(_) => { + rebase_success = false; + } + } + + if rebase_success { + // rebase worked out, rewrite the branch head + branch.head = last_rebase_head; + } else { + // rebase failed, do a merge commit + + // get tree from merge_tree_oid + let merge_tree = repo + .find_tree(merged_branch_tree_oid) + .context("failed to find tree")?; + + // commit the merge tree oid + let new_branch_head = self + .project_repository + .commit( + format!( + "Merged {}/{} into {}", + default_target.branch.remote(), + default_target.branch.branch(), + branch.name + ) + .as_str(), + &merge_tree, + &[&head_commit, &target_commit], + None, + ) + .context("failed to commit merge")?; + + branch.head = new_branch_head; + } + } + + branch.tree = repo + .find_commit(branch.head)? + .tree() + .map_err(anyhow::Error::from)? + .id(); + vb_state.set_branch(branch.clone())?; + } + + let wd_tree = self.project_repository.repo().get_wd_tree()?; + + let branch_tree = repo + .find_tree(branch.tree) + .context("failed to find branch tree")?; + + // check index for conflicts + let mut merge_index = repo + .merge_trees(&target_tree, &wd_tree, &branch_tree, None) + .context("failed to merge trees")?; + + if merge_index.has_conflicts() { + // mark conflicts + let conflicts = merge_index + .conflicts() + .context("failed to get merge index conflicts")?; + let mut merge_conflicts = Vec::new(); + for path in conflicts.flatten() { + if let Some(ours) = path.our { + let path = std::str::from_utf8(&ours.path) + .context("failed to convert path to utf8")? + .to_string(); + merge_conflicts.push(path); + } + } + conflicts::mark( + self.project_repository, + &merge_conflicts, + Some(default_target.sha), + )?; + } + + // apply the branch + vb_state.set_branch(branch.clone())?; + + ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; + // checkout the merge index + repo.checkout_index_builder(&mut merge_index) + .force() + .checkout() + .context("failed to checkout index")?; + + // Look for and handle the vbranch indicator commit + // TODO: This is not unapplying the WIP commit for some unholy reason. + // If you can figgure it out I'll buy you a beer. + { + if let Some(wip_commit_to_unapply) = branch.not_in_workspace_wip_change_id { + let potential_wip_commit = repo.find_commit(branch.head)?; + + if let Some(headers) = potential_wip_commit.gitbutler_headers() { + if headers.change_id == wip_commit_to_unapply { + undo_commit(self.project_repository, branch.id, branch.head)?; + } + } + + branch.not_in_workspace_wip_change_id = None; + vb_state.set_branch(branch.clone())?; + } + } + + update_gitbutler_integration(&vb_state, self.project_repository)?; + + Ok(branch.name) + } + + // to unapply a branch, we need to write the current tree out, then remove those file changes from the wd + pub fn convert_to_real_branch( + &self, + branch_id: BranchId, + name_conflict_resolution: NameConflitResolution, + ) -> Result { + let vb_state = self.project_repository.project().virtual_branches(); + + let mut target_branch = vb_state.get_branch_in_workspace(branch_id)?; + + // Convert the vbranch to a real branch + let real_branch = self.build_real_branch(&mut target_branch, name_conflict_resolution)?; + + self.delete_branch(branch_id)?; + + // If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts + if conflicts::is_conflicting(self.project_repository, None)? { + conflicts::clear(self.project_repository)?; + } + + vb_state.update_ordering()?; + + // Ensure we still have a default target + ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; + + crate::integration::update_gitbutler_integration(&vb_state, self.project_repository)?; + + real_branch.reference_name() + } + + fn build_real_branch( + &self, + vbranch: &mut branch::Branch, + name_conflict_resolution: NameConflitResolution, + ) -> Result> { + let repo = self.project_repository.repo(); + let target_commit = repo.find_commit(vbranch.head)?; + let branch_name = vbranch.name.clone(); + let branch_name = normalize_branch_name(&branch_name); + + // Is there a name conflict? + let branch_name = if repo + .find_branch(branch_name.as_str(), git2::BranchType::Local) + .is_ok() + { + match name_conflict_resolution { + NameConflitResolution::Suffix => { + let mut suffix = 1; + loop { + let new_branch_name = format!("{}-{}", branch_name, suffix); + if repo + .find_branch(new_branch_name.as_str(), git2::BranchType::Local) + .is_err() + { + break new_branch_name; + } + suffix += 1; + } + } + NameConflitResolution::Rename(new_name) => { + if repo + .find_branch(new_name.as_str(), git2::BranchType::Local) + .is_ok() + { + Err(anyhow!("Branch with name {} already exists", new_name))? + } else { + new_name + } + } + NameConflitResolution::Overwrite => branch_name, + } + } else { + branch_name + }; + + let vb_state = self.project_repository.project().virtual_branches(); + let branch = repo.branch(&branch_name, &target_commit, true)?; + vbranch.source_refname = Some(Refname::try_from(&branch)?); + vb_state.set_branch(vbranch.clone())?; + + self.build_metadata_commit(vbranch, &branch)?; + + Ok(branch) + } + + fn build_metadata_commit( + &self, + vbranch: &mut branch::Branch, + branch: &git2::Branch<'_>, + ) -> Result { + let repo = self.project_repository.repo(); + + // Build wip tree as either any uncommitted changes or an empty tree + let vbranch_wip_tree = repo.find_tree(vbranch.tree)?; + let vbranch_head_tree = repo.find_commit(vbranch.head)?.tree()?; + + let tree = if vbranch_head_tree.id() != vbranch_wip_tree.id() { + vbranch_wip_tree + } else { + repo.find_tree(TreeUpdateBuilder::new().create_updated(repo, &vbranch_head_tree)?)? + }; + + // Build commit message + let mut message = "GitButler WIP Commit".to_string(); + message.push_str("\n\n"); + + // Commit wip commit + let committer = get_integration_commiter()?; + let parent = branch.get().peel_to_commit()?; + + let commit_headers = CommitHeadersV2::new(); + + let commit_oid = repo.commit_with_signature( + Some(&branch.try_into()?), + &committer, + &committer, + &message, + &tree, + &[&parent], + Some(commit_headers.clone()), + )?; + + let vb_state = self.project_repository.project().virtual_branches(); + // vbranch.head = commit_oid; + vbranch.not_in_workspace_wip_change_id = Some(commit_headers.change_id); + vb_state.set_branch(vbranch.clone())?; + + Ok(commit_oid) + } + + pub fn delete_branch(&self, branch_id: BranchId) -> Result<()> { + let vb_state = self.project_repository.project().virtual_branches(); + let Some(branch) = vb_state.try_branch_in_workspace(branch_id)? else { + return Ok(()); + }; + _ = self + .project_repository + .project() + .snapshot_branch_deletion(branch.name.clone()); + + let repo = self.project_repository.repo(); + + let integration_commit = repo.integration_commit()?; + let target_commit = repo.target_commit()?; + let base_tree = target_commit.tree().context("failed to get target tree")?; + + let virtual_branches = vb_state + .list_branches_in_workspace() + .context("failed to read virtual branches")?; + + let (applied_statuses, _) = get_applied_status( + self.project_repository, + &integration_commit.id(), + &target_commit.id(), + virtual_branches, + ) + .context("failed to get status by branch")?; + + // go through the other applied branches and merge them into the final tree + // then check that out into the working directory + let final_tree = applied_statuses + .into_iter() + .filter(|(branch, _)| branch.id != branch_id) + .fold( + target_commit.tree().context("failed to get target tree"), + |final_tree, status| { + let final_tree = final_tree?; + let branch = status.0; + let tree_oid = write_tree(self.project_repository, &branch.head, status.1)?; + let branch_tree = repo.find_tree(tree_oid)?; + let mut result = + repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; + let final_tree_oid = result.write_tree_to(repo)?; + repo.find_tree(final_tree_oid) + .context("failed to find tree") + }, + )?; + + // checkout final_tree into the working directory + repo.checkout_tree_builder(&final_tree) + .force() + .remove_untracked() + .checkout() + .context("failed to checkout tree")?; + + vb_state + .mark_as_not_in_workspace(branch.id) + .context("Failed to remove branch")?; + + self.project_repository.delete_branch_reference(&branch)?; + + ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; + + Ok(()) + } +} diff --git a/crates/gitbutler-virtual/src/controller.rs b/crates/gitbutler-virtual/src/controller.rs index 4ad1e4e55..e75b2ebe8 100644 --- a/crates/gitbutler-virtual/src/controller.rs +++ b/crates/gitbutler-virtual/src/controller.rs @@ -1,7 +1,6 @@ use anyhow::Result; use gitbutler_branch::{ branch::{BranchCreateRequest, BranchId, BranchUpdateRequest}, - branch_ext::BranchExt, diff, ownership::BranchOwnershipClaims, }; @@ -13,8 +12,9 @@ use gitbutler_oplog::{ snapshot::Snapshot, }; use gitbutler_project::{FetchResult, Project}; -use gitbutler_reference::{ReferenceName, Refname, RemoteRefname}; +use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::{credentials::Helper, RepoActions, RepositoryExt}; +use gitbutler_tagged_string::ReferenceName; use std::{path::Path, sync::Arc}; use tokio::sync::Semaphore; @@ -24,6 +24,7 @@ use crate::{ get_base_branch_data, set_base_branch, set_target_push_remote, update_base_branch, BaseBranch, }, + branch_manager::BranchManagerAccess, remote::{get_branch_data, list_remote_branches, RemoteBranch, RemoteBranchData}, }; @@ -103,21 +104,11 @@ impl Controller { self.permit(project.ignore_project_semaphore).await; let project_repository = open_with_verify(project)?; - let branch_id = branch::create_virtual_branch(&project_repository, create)?.id; + let branch_manager = project_repository.branch_manager(); + let branch_id = branch_manager.create_virtual_branch(create)?.id; Ok(branch_id) } - pub async fn create_virtual_branch_from_branch( - &self, - project: &Project, - branch: &Refname, - ) -> Result { - self.permit(project.ignore_project_semaphore).await; - - let project_repository = open_with_verify(project)?; - branch::create_virtual_branch_from_branch(&project_repository, branch).map_err(Into::into) - } - pub async fn get_base_branch_data(&self, project: &Project) -> Result { let project_repository = ProjectRepository::open(project)?; get_base_branch_data(&project_repository) @@ -171,14 +162,7 @@ impl Controller { let _ = project_repository .project() .create_snapshot(SnapshotDetails::new(OperationKind::UpdateWorkspaceBase)); - update_base_branch(&project_repository) - .map(|unapplied_branches| { - unapplied_branches - .iter() - .filter_map(|unapplied_branch| unapplied_branch.reference_name().ok()) - .collect() - }) - .map_err(Into::into) + update_base_branch(&project_repository).map_err(Into::into) } pub async fn update_virtual_branch( @@ -214,7 +198,8 @@ impl Controller { self.permit(project.ignore_project_semaphore).await; let project_repository = open_with_verify(project)?; - branch::delete_branch(&project_repository, branch_id) + let branch_manager = project_repository.branch_manager(); + branch_manager.delete_branch(branch_id) } pub async fn unapply_ownership( @@ -362,18 +347,16 @@ impl Controller { let project_repository = open_with_verify(project)?; let snapshot_tree = project_repository.project().prepare_snapshot(); - let result = branch::convert_to_real_branch( - &project_repository, - branch_id, - name_conflict_resolution, - ) - .map_err(Into::into); + let branch_manager = project_repository.branch_manager(); + let result = branch_manager.convert_to_real_branch(branch_id, name_conflict_resolution); + let _ = snapshot_tree.and_then(|snapshot_tree| { project_repository .project() .snapshot_branch_unapplied(snapshot_tree, result.as_ref()) }); - result.and_then(|b| b.reference_name()) + + result } pub async fn push_virtual_branch( @@ -492,9 +475,23 @@ impl Controller { branch::move_commit(&project_repository, target_branch_id, commit_oid).map_err(Into::into) } + pub async fn create_virtual_branch_from_branch( + &self, + project: &Project, + branch: &Refname, + ) -> Result { + self.permit(project.ignore_project_semaphore).await; + + let project_repository = open_with_verify(project)?; + let branch_manager = project_repository.branch_manager(); + branch_manager + .create_virtual_branch_from_branch(branch) + .map_err(Into::into) + } + async fn permit(&self, ignore: bool) { if !ignore { - let _permit = self.semaphore.acquire().await; + let _ = self.semaphore.acquire().await.unwrap(); } } } diff --git a/crates/gitbutler-virtual/src/integration.rs b/crates/gitbutler-virtual/src/integration.rs index c08662a2c..35a0d9bea 100644 --- a/crates/gitbutler-virtual/src/integration.rs +++ b/crates/gitbutler-virtual/src/integration.rs @@ -14,6 +14,7 @@ use gitbutler_commit::commit_ext::CommitExt; use gitbutler_error::error::Marker; use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt}; +use crate::branch_manager::BranchManagerAccess; use crate::conflicts; const WORKSPACE_HEAD: &str = "Workspace Head"; @@ -358,16 +359,15 @@ impl Verify for ProjectRepository { ) .context("failed to reset to integration commit")?; - let mut new_branch = super::create_virtual_branch( - self, - &BranchCreateRequest { + let branch_manager = self.branch_manager(); + let mut new_branch = branch_manager + .create_virtual_branch(&BranchCreateRequest { name: extra_commits .last() .map(|commit| commit.message_bstr().to_string()), ..Default::default() - }, - ) - .context("failed to create virtual branch")?; + }) + .context("failed to create virtual branch")?; // rebasing the extra commits onto the new branch let vb_state = self.project().virtual_branches(); diff --git a/crates/gitbutler-virtual/src/lib.rs b/crates/gitbutler-virtual/src/lib.rs index ecb3e4300..0528e54b5 100644 --- a/crates/gitbutler-virtual/src/lib.rs +++ b/crates/gitbutler-virtual/src/lib.rs @@ -5,6 +5,8 @@ pub use controller::Controller; pub mod r#virtual; pub use r#virtual::*; +pub mod branch_manager; + pub mod assets; pub mod base; @@ -18,4 +20,3 @@ pub mod remote; pub mod conflicts; mod author; -mod dedup; diff --git a/crates/gitbutler-virtual/src/virtual.rs b/crates/gitbutler-virtual/src/virtual.rs index d22ad04ea..3546cc0a3 100644 --- a/crates/gitbutler-virtual/src/virtual.rs +++ b/crates/gitbutler-virtual/src/virtual.rs @@ -1,4 +1,5 @@ use gitbutler_branch::branch::{self, Branch, BranchCreateRequest, BranchId}; +use gitbutler_branch::dedup::{dedup, dedup_fmt}; use gitbutler_branch::diff::{self, diff_files_into_hunks, trees, FileDiff, GitHunk}; use gitbutler_branch::file_ownership::OwnershipClaim; use gitbutler_branch::hunk::{Hunk, HunkHash}; @@ -6,8 +7,7 @@ use gitbutler_branch::ownership::{reconcile_claims, BranchOwnershipClaims}; use gitbutler_branchstate::{VirtualBranchesAccess, VirtualBranchesHandle}; use gitbutler_command_context::ProjectRepository; use gitbutler_commit::commit_ext::CommitExt; -use gitbutler_commit::commit_headers::{CommitHeadersV2, HasCommitHeaders}; -use gitbutler_oplog::snapshot::Snapshot; +use gitbutler_commit::commit_headers::HasCommitHeaders; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::credentials::Helper; use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt}; @@ -24,16 +24,15 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use bstr::{BString, ByteSlice, ByteVec}; use diffy::{apply_bytes as diffy_apply, Line, Patch}; -use git2::build::TreeUpdateBuilder; use git2::ErrorCode; use git2_hooks::HookResult; use hex::ToHex; use serde::{Deserialize, Serialize}; use crate::author::Author; +use crate::branch_manager::BranchManagerAccess; use crate::conflicts::{self, RepoConflicts}; -use crate::dedup::{dedup, dedup_fmt}; -use crate::integration::{get_integration_commiter, get_workspace_head}; +use crate::integration::get_workspace_head; use crate::remote::{branch_to_remote_branch, RemoteBranch}; use gitbutler_branch::target; use gitbutler_error::error::Code; @@ -164,7 +163,7 @@ pub struct VirtualBranchHunk { /// Lifecycle impl VirtualBranchHunk { - pub(crate) fn gen_id(new_start: u32, new_lines: u32) -> String { + pub fn gen_id(new_start: u32, new_lines: u32) -> String { format!("{}-{}", new_start, new_start + new_lines) } fn from_git_hunk( @@ -322,139 +321,6 @@ pub fn reset_files(project_repository: &ProjectRepository, files: &Vec) Ok(()) } - -// to unapply a branch, we need to write the current tree out, then remove those file changes from the wd -pub fn convert_to_real_branch( - project_repository: &ProjectRepository, - branch_id: BranchId, - name_conflict_resolution: NameConflitResolution, -) -> Result> { - fn build_real_branch<'l>( - project_repository: &'l ProjectRepository, - vbranch: &mut branch::Branch, - name_conflict_resolution: NameConflitResolution, - ) -> Result> { - let repo = project_repository.repo(); - let target_commit = repo.find_commit(vbranch.head)?; - let branch_name = vbranch.name.clone(); - let branch_name = normalize_branch_name(&branch_name); - - // Is there a name conflict? - let branch_name = if repo - .find_branch(branch_name.as_str(), git2::BranchType::Local) - .is_ok() - { - match name_conflict_resolution { - NameConflitResolution::Suffix => { - let mut suffix = 1; - loop { - let new_branch_name = format!("{}-{}", branch_name, suffix); - if repo - .find_branch(new_branch_name.as_str(), git2::BranchType::Local) - .is_err() - { - break new_branch_name; - } - suffix += 1; - } - } - NameConflitResolution::Rename(new_name) => { - if repo - .find_branch(new_name.as_str(), git2::BranchType::Local) - .is_ok() - { - Err(anyhow!("Branch with name {} already exists", new_name))? - } else { - new_name - } - } - NameConflitResolution::Overwrite => branch_name, - } - } else { - branch_name - }; - - let vb_state = project_repository.project().virtual_branches(); - let branch = repo.branch(&branch_name, &target_commit, true)?; - vbranch.source_refname = Some(Refname::try_from(&branch)?); - vb_state.set_branch(vbranch.clone())?; - - build_metadata_commit(project_repository, vbranch, &branch)?; - - Ok(branch) - } - fn build_metadata_commit<'l>( - project_repository: &'l ProjectRepository, - vbranch: &mut branch::Branch, - branch: &git2::Branch<'l>, - ) -> Result { - let repo = project_repository.repo(); - - // Build wip tree as either any uncommitted changes or an empty tree - let vbranch_wip_tree = repo.find_tree(vbranch.tree)?; - let vbranch_head_tree = repo.find_commit(vbranch.head)?.tree()?; - - let tree = if vbranch_head_tree.id() != vbranch_wip_tree.id() { - vbranch_wip_tree - } else { - repo.find_tree(TreeUpdateBuilder::new().create_updated(repo, &vbranch_head_tree)?)? - }; - - // Build commit message - let mut message = "GitButler WIP Commit".to_string(); - message.push_str("\n\n"); - - // Commit wip commit - let committer = get_integration_commiter()?; - let parent = branch.get().peel_to_commit()?; - - let commit_headers = CommitHeadersV2::new(); - - let commit_oid = repo.commit_with_signature( - Some(&branch.try_into()?), - &committer, - &committer, - &message, - &tree, - &[&parent], - Some(commit_headers.clone()), - )?; - - let vb_state = project_repository.project().virtual_branches(); - // vbranch.head = commit_oid; - vbranch.not_in_workspace_wip_change_id = Some(commit_headers.change_id); - vb_state.set_branch(vbranch.clone())?; - - Ok(commit_oid) - } - let vb_state = project_repository.project().virtual_branches(); - - let mut target_branch = vb_state.get_branch_in_workspace(branch_id)?; - - // Convert the vbranch to a real branch - let real_branch = build_real_branch( - project_repository, - &mut target_branch, - name_conflict_resolution, - )?; - - delete_branch(project_repository, branch_id)?; - - // If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts - if conflicts::is_conflicting(project_repository, None)? { - conflicts::clear(project_repository)?; - } - - vb_state.update_ordering()?; - - // Ensure we still have a default target - ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; - - crate::integration::update_gitbutler_integration(&vb_state, project_repository)?; - - Ok(real_branch) -} - fn find_base_tree<'a>( repo: &'a git2::Repository, branch_commit: &'a git2::Commit<'a>, @@ -487,9 +353,11 @@ fn resolve_old_applied_state( ) -> Result<()> { let branches = vb_state.list_all_branches()?; + let branch_manager = project_repository.branch_manager(); + for mut branch in branches { if !branch.old_applied && branch.in_workspace { - convert_to_real_branch(project_repository, branch.id, Default::default())?; + branch_manager.convert_to_real_branch(branch.id, Default::default())?; } else { branch.old_applied = branch.in_workspace; vb_state.set_branch(branch)?; @@ -764,108 +632,6 @@ fn commit_to_vbranch_commit( Ok(commit) } -pub fn create_virtual_branch( - project_repository: &ProjectRepository, - create: &BranchCreateRequest, -) -> Result { - let vb_state = project_repository.project().virtual_branches(); - - let default_target = vb_state.get_default_target()?; - - let commit = project_repository - .repo() - .find_commit(default_target.sha) - .context("failed to find default target commit")?; - - let tree = commit - .tree() - .context("failed to find defaut target commit tree")?; - - let mut all_virtual_branches = vb_state - .list_branches_in_workspace() - .context("failed to read virtual branches")?; - - let name = dedup( - &all_virtual_branches - .iter() - .map(|b| b.name.as_str()) - .collect::>(), - create - .name - .as_ref() - .unwrap_or(&"Virtual branch".to_string()), - ); - - _ = project_repository - .project() - .snapshot_branch_creation(name.clone()); - - all_virtual_branches.sort_by_key(|branch| branch.order); - - let order = create.order.unwrap_or(vb_state.next_order_index()?); - - let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes { - if selected_for_changes { - for mut other_branch in vb_state - .list_branches_in_workspace() - .context("failed to read virtual branches")? - { - other_branch.selected_for_changes = None; - vb_state.set_branch(other_branch.clone())?; - } - Some(now_since_unix_epoch_ms()) - } else { - None - } - } else { - (!all_virtual_branches - .iter() - .any(|b| b.selected_for_changes.is_some())) - .then_some(now_since_unix_epoch_ms()) - }; - - // make space for the new branch - for (i, branch) in all_virtual_branches.iter().enumerate() { - let mut branch = branch.clone(); - let new_order = if i < order { i } else { i + 1 }; - if branch.order != new_order { - branch.order = new_order; - vb_state.set_branch(branch.clone())?; - } - } - - let now = gitbutler_time::time::now_ms(); - - let mut branch = Branch { - id: BranchId::generate(), - name: name.clone(), - notes: String::new(), - upstream: None, - upstream_head: None, - tree: tree.id(), - head: default_target.sha, - created_timestamp_ms: now, - updated_timestamp_ms: now, - ownership: BranchOwnershipClaims::default(), - order, - selected_for_changes, - allow_rebasing: project_repository.project().ok_with_force_push.into(), - old_applied: true, - in_workspace: true, - not_in_workspace_wip_change_id: None, - source_refname: None, - }; - - if let Some(ownership) = &create.ownership { - set_ownership(&vb_state, &mut branch, ownership).context("failed to set ownership")?; - } - - vb_state.set_branch(branch.clone())?; - project_repository.add_branch_reference(&branch)?; - - Ok(branch) -} - /// Integrates upstream work from a remote branch. /// /// First we determine strategy based on preferences and branch state. If you @@ -1166,71 +932,7 @@ pub fn update_branch( Ok(branch) } -pub fn delete_branch(project_repository: &ProjectRepository, branch_id: BranchId) -> Result<()> { - let vb_state = project_repository.project().virtual_branches(); - let Some(branch) = vb_state.try_branch_in_workspace(branch_id)? else { - return Ok(()); - }; - _ = project_repository - .project() - .snapshot_branch_deletion(branch.name.clone()); - - let repo = project_repository.repo(); - - let integration_commit = repo.integration_commit()?; - let target_commit = repo.target_commit()?; - let base_tree = target_commit.tree().context("failed to get target tree")?; - - let virtual_branches = vb_state - .list_branches_in_workspace() - .context("failed to read virtual branches")?; - - let (applied_statuses, _) = get_applied_status( - project_repository, - &integration_commit.id(), - &target_commit.id(), - virtual_branches, - ) - .context("failed to get status by branch")?; - - // go through the other applied branches and merge them into the final tree - // then check that out into the working directory - let final_tree = applied_statuses - .into_iter() - .filter(|(branch, _)| branch.id != branch_id) - .fold( - target_commit.tree().context("failed to get target tree"), - |final_tree, status| { - let final_tree = final_tree?; - let branch = status.0; - let tree_oid = write_tree(project_repository, &branch.head, status.1)?; - let branch_tree = repo.find_tree(tree_oid)?; - let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; - let final_tree_oid = result.write_tree_to(repo)?; - repo.find_tree(final_tree_oid) - .context("failed to find tree") - }, - )?; - - // checkout final_tree into the working directory - repo.checkout_tree_builder(&final_tree) - .force() - .remove_untracked() - .checkout() - .context("failed to checkout tree")?; - - vb_state - .mark_as_not_in_workspace(branch.id) - .context("Failed to remove branch")?; - - project_repository.delete_branch_reference(&branch)?; - - ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; - - Ok(()) -} - -fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> { +pub fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> { let mut virtual_branches = vb_state .list_branches_in_workspace() .context("failed to list branches")?; @@ -1255,7 +957,7 @@ fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> { Ok(()) } -fn set_ownership( +pub fn set_ownership( vb_state: &VirtualBranchesHandle, target_branch: &mut branch::Branch, ownership: &gitbutler_branch::ownership::BranchOwnershipClaims, @@ -1527,7 +1229,7 @@ fn compute_locks( // Returns branches and their associated file changes, in addition to a list // of skipped files. -fn get_applied_status( +pub(crate) fn get_applied_status( project_repository: &ProjectRepository, integration_commit: &git2::Oid, target_sha: &git2::Oid, @@ -1547,12 +1249,12 @@ fn get_applied_status( // sort by order, so that the default branch is first (left in the ui) virtual_branches.sort_by(|a, b| a.order.cmp(&b.order)); + let branch_manager = project_repository.branch_manager(); + if virtual_branches.is_empty() && !base_diffs.is_empty() { - virtual_branches = - vec![ - create_virtual_branch(project_repository, &BranchCreateRequest::default()) - .context("failed to create default branch")?, - ]; + virtual_branches = vec![branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) + .context("failed to create default branch")?]; } let mut diffs_by_branch: HashMap = virtual_branches @@ -3152,392 +2854,6 @@ pub fn move_commit( Ok(()) } -pub fn create_virtual_branch_from_branch( - project_repository: &ProjectRepository, - upstream: &Refname, -) -> Result { - fn apply_branch(project_repository: &ProjectRepository, branch_id: BranchId) -> Result { - project_repository.assure_resolved()?; - project_repository.assure_unconflicted()?; - let repo = project_repository.repo(); - - let vb_state = project_repository.project().virtual_branches(); - let default_target = vb_state.get_default_target()?; - - let mut branch = vb_state.get_branch_in_workspace(branch_id)?; - - let target_commit = repo - .find_commit(default_target.sha) - .context("failed to find target commit")?; - let target_tree = target_commit.tree().context("failed to get target tree")?; - - // calculate the merge base and make sure it's the same as the target commit - // if not, we need to merge or rebase the branch to get it up to date - - let merge_base = repo - .merge_base(default_target.sha, branch.head) - .context(format!( - "failed to find merge base between {} and {}", - default_target.sha, branch.head - ))?; - if merge_base != default_target.sha { - // Branch is out of date, merge or rebase it - let merge_base_tree = repo - .find_commit(merge_base) - .context(format!("failed to find merge base commit {}", merge_base))? - .tree() - .context("failed to find merge base tree")?; - - let branch_tree = repo - .find_tree(branch.tree) - .context("failed to find branch tree")?; - - let mut merge_index = repo - .merge_trees(&merge_base_tree, &branch_tree, &target_tree, None) - .context("failed to merge trees")?; - - if merge_index.has_conflicts() { - // currently we can only deal with the merge problem branch - for branch in vb_state - .list_branches_in_workspace()? - .iter() - .filter(|branch| branch.id != branch_id) - { - convert_to_real_branch(project_repository, branch.id, Default::default())?; - } - - // apply the branch - vb_state.set_branch(branch.clone())?; - - // checkout the conflicts - repo.checkout_index_builder(&mut merge_index) - .allow_conflicts() - .conflict_style_merge() - .force() - .checkout() - .context("failed to checkout index")?; - - // mark conflicts - let conflicts = merge_index - .conflicts() - .context("failed to get merge index conflicts")?; - let mut merge_conflicts = Vec::new(); - for path in conflicts.flatten() { - if let Some(ours) = path.our { - let path = std::str::from_utf8(&ours.path) - .context("failed to convert path to utf8")? - .to_string(); - merge_conflicts.push(path); - } - } - conflicts::mark( - project_repository, - &merge_conflicts, - Some(default_target.sha), - )?; - - return Ok(branch.name); - } - - let head_commit = repo - .find_commit(branch.head) - .context("failed to find head commit")?; - - let merged_branch_tree_oid = merge_index - .write_tree_to(project_repository.repo()) - .context("failed to write tree")?; - - let merged_branch_tree = repo - .find_tree(merged_branch_tree_oid) - .context("failed to find tree")?; - - let ok_with_force_push = branch.allow_rebasing; - if branch.upstream.is_some() && !ok_with_force_push { - // branch was pushed to upstream, and user doesn't like force pushing. - // create a merge commit to avoid the need of force pushing then. - - let new_branch_head = project_repository.commit( - format!( - "Merged {}/{} into {}", - default_target.branch.remote(), - default_target.branch.branch(), - branch.name - ) - .as_str(), - &merged_branch_tree, - &[&head_commit, &target_commit], - None, - )?; - - // ok, update the virtual branch - branch.head = new_branch_head; - } else { - let rebase = cherry_rebase( - project_repository, - target_commit.id(), - target_commit.id(), - branch.head, - ); - let mut rebase_success = true; - let mut last_rebase_head = branch.head; - match rebase { - Ok(rebase_oid) => { - if let Some(oid) = rebase_oid { - last_rebase_head = oid; - } - } - Err(_) => { - rebase_success = false; - } - } - - if rebase_success { - // rebase worked out, rewrite the branch head - branch.head = last_rebase_head; - } else { - // rebase failed, do a merge commit - - // get tree from merge_tree_oid - let merge_tree = repo - .find_tree(merged_branch_tree_oid) - .context("failed to find tree")?; - - // commit the merge tree oid - let new_branch_head = project_repository - .commit( - format!( - "Merged {}/{} into {}", - default_target.branch.remote(), - default_target.branch.branch(), - branch.name - ) - .as_str(), - &merge_tree, - &[&head_commit, &target_commit], - None, - ) - .context("failed to commit merge")?; - - branch.head = new_branch_head; - } - } - - branch.tree = repo - .find_commit(branch.head)? - .tree() - .map_err(anyhow::Error::from)? - .id(); - vb_state.set_branch(branch.clone())?; - } - - let wd_tree = project_repository.repo().get_wd_tree()?; - - let branch_tree = repo - .find_tree(branch.tree) - .context("failed to find branch tree")?; - - // check index for conflicts - let mut merge_index = repo - .merge_trees(&target_tree, &wd_tree, &branch_tree, None) - .context("failed to merge trees")?; - - if merge_index.has_conflicts() { - // mark conflicts - let conflicts = merge_index - .conflicts() - .context("failed to get merge index conflicts")?; - let mut merge_conflicts = Vec::new(); - for path in conflicts.flatten() { - if let Some(ours) = path.our { - let path = std::str::from_utf8(&ours.path) - .context("failed to convert path to utf8")? - .to_string(); - merge_conflicts.push(path); - } - } - conflicts::mark( - project_repository, - &merge_conflicts, - Some(default_target.sha), - )?; - } - - // apply the branch - vb_state.set_branch(branch.clone())?; - - ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; - // checkout the merge index - repo.checkout_index_builder(&mut merge_index) - .force() - .checkout() - .context("failed to checkout index")?; - - // Look for and handle the vbranch indicator commit - // TODO: This is not unapplying the WIP commit for some unholy reason. - // If you can figgure it out I'll buy you a beer. - { - if let Some(wip_commit_to_unapply) = branch.not_in_workspace_wip_change_id { - let potential_wip_commit = repo.find_commit(branch.head)?; - - if let Some(headers) = potential_wip_commit.gitbutler_headers() { - if headers.change_id == wip_commit_to_unapply { - undo_commit(project_repository, branch.id, branch.head)?; - } - } - - branch.not_in_workspace_wip_change_id = None; - vb_state.set_branch(branch.clone())?; - } - } - - crate::integration::update_gitbutler_integration(&vb_state, project_repository)?; - - Ok(branch.name) - } - - // only set upstream if it's not the default target - let upstream_branch = match upstream { - Refname::Other(_) | Refname::Virtual(_) => { - // we only support local or remote branches - bail!("branch {upstream} must be a local or remote branch"); - } - Refname::Remote(remote) => Some(remote.clone()), - Refname::Local(local) => local.remote().cloned(), - }; - - let branch_name = upstream - .branch() - .expect("always a branch reference") - .to_string(); - - let _ = project_repository - .project() - .snapshot_branch_creation(branch_name.clone()); - - let vb_state = project_repository.project().virtual_branches(); - - let default_target = vb_state.get_default_target()?; - - if let Refname::Remote(remote_upstream) = upstream { - if default_target.branch == *remote_upstream { - bail!("cannot create a branch from default target") - } - } - - let repo = project_repository.repo(); - let head_reference = repo - .find_reference(&upstream.to_string()) - .map_err(|err| match err { - err if err.code() == git2::ErrorCode::NotFound => { - anyhow!("branch {upstream} was not found") - } - err => err.into(), - })?; - let head_commit = head_reference - .peel_to_commit() - .context("failed to peel to commit")?; - let head_commit_tree = head_commit.tree().context("failed to find tree")?; - - let virtual_branches = vb_state - .list_branches_in_workspace() - .context("failed to read virtual branches")? - .into_iter() - .collect::>(); - - let order = vb_state.next_order_index()?; - - let selected_for_changes = (!virtual_branches - .iter() - .any(|b| b.selected_for_changes.is_some())) - .then_some(now_since_unix_epoch_ms()); - - let now = gitbutler_time::time::now_ms(); - - // add file ownership based off the diff - let target_commit = repo.find_commit(default_target.sha)?; - let merge_base_oid = repo.merge_base(target_commit.id(), head_commit.id())?; - let merge_base_tree = repo.find_commit(merge_base_oid)?.tree()?; - - // do a diff between the head of this branch and the target base - let diff = diff::trees( - project_repository.repo(), - &merge_base_tree, - &head_commit_tree, - )?; - - // assign ownership to the branch - let ownership = diff.iter().fold( - BranchOwnershipClaims::default(), - |mut ownership, (file_path, file)| { - for hunk in &file.hunks { - ownership.put( - format!( - "{}:{}", - file_path.display(), - VirtualBranchHunk::gen_id(hunk.new_start, hunk.new_lines) - ) - .parse() - .unwrap(), - ); - } - ownership - }, - ); - - let branch = if let Ok(Some(mut branch)) = - vb_state.find_by_source_refname_where_not_in_workspace(upstream) - { - branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id()); - branch.upstream = upstream_branch; - branch.tree = head_commit_tree.id(); - branch.head = head_commit.id(); - branch.ownership = ownership; - branch.order = order; - branch.selected_for_changes = selected_for_changes; - branch.allow_rebasing = project_repository.project().ok_with_force_push.into(); - branch.old_applied = true; - branch.in_workspace = true; - - branch - } else { - branch::Branch { - id: BranchId::generate(), - name: branch_name.clone(), - notes: String::new(), - source_refname: Some(upstream.clone()), - upstream_head: upstream_branch.is_some().then_some(head_commit.id()), - upstream: upstream_branch, - tree: head_commit_tree.id(), - head: head_commit.id(), - created_timestamp_ms: now, - updated_timestamp_ms: now, - ownership, - order, - selected_for_changes, - allow_rebasing: project_repository.project().ok_with_force_push.into(), - old_applied: true, - in_workspace: true, - not_in_workspace_wip_change_id: None, - } - }; - - vb_state.set_branch(branch.clone())?; - project_repository.add_branch_reference(&branch)?; - - match apply_branch(project_repository, branch.id) { - Ok(_) => Ok(branch.id), - Err(err) - if err - .downcast_ref() - .map_or(false, |marker: &Marker| *marker == Marker::ProjectConflict) => - { - // if branch conflicts with the workspace, it's ok. keep it unapplied - Ok(branch.id) - } - Err(err) => Err(err).context("failed to apply"), - } -} - /// Just like [`diffy::apply()`], but on error it will attach hashes of the input `base_image` and `patch`. pub fn apply>(base_image: S, patch: &Patch<'_, [u8]>) -> Result { fn md5_hash_hex(b: impl AsRef<[u8]>) -> String { diff --git a/crates/gitbutler-virtual/tests/extra/mod.rs b/crates/gitbutler-virtual/tests/extra/mod.rs index 5826bb5aa..8180d68ea 100644 --- a/crates/gitbutler-virtual/tests/extra/mod.rs +++ b/crates/gitbutler-virtual/tests/extra/mod.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, io::Write, path::{Path, PathBuf}, + str::FromStr, }; #[cfg(target_family = "unix")] use std::{ @@ -20,12 +21,12 @@ use gitbutler_branchstate::VirtualBranchesAccess; use gitbutler_commit::{commit_ext::CommitExt, commit_headers::CommitHeadersV2}; use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::RepositoryExt; -use gitbutler_virtual::integration; use gitbutler_virtual::r#virtual as virtual_branches; use gitbutler_virtual::r#virtual::{ - commit, create_virtual_branch, create_virtual_branch_from_branch, integrate_upstream_commits, - is_remote_branch_mergeable, list_virtual_branches, unapply_ownership, update_branch, + commit, integrate_upstream_commits, is_remote_branch_mergeable, list_virtual_branches, + unapply_ownership, update_branch, }; +use gitbutler_virtual::{branch_manager::BranchManagerAccess, integration}; use pretty_assertions::assert_eq; use gitbutler_testsupport::{commit_all, virtual_branches::set_test_target, Case, Suite}; @@ -44,7 +45,9 @@ fn commit_on_branch_then_change_file_then_get_status() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch1_id = project_repository + .branch_manager() + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -113,7 +116,9 @@ fn track_binary_files() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch1_id = project_repository + .branch_manager() + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -207,7 +212,9 @@ fn create_branch_with_ownership() -> Result<()> { let file_path = Path::new("test.txt"); std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n").unwrap(); - let branch0 = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch0 = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); virtual_branches::get_status_by_branch(project_repository, None).expect("failed to get status"); @@ -215,14 +222,12 @@ fn create_branch_with_ownership() -> Result<()> { let vb_state = project_repository.project().virtual_branches(); let branch0 = vb_state.get_branch_in_workspace(branch0.id).unwrap(); - let branch1 = create_virtual_branch( - project_repository, - &BranchCreateRequest { + let branch1 = branch_manager + .create_virtual_branch(&BranchCreateRequest { ownership: Some(branch0.ownership), ..Default::default() - }, - ) - .expect("failed to create virtual branch"); + }) + .expect("failed to create virtual branch"); let statuses = virtual_branches::get_status_by_branch(project_repository, None) .expect("failed to get status") @@ -249,18 +254,19 @@ fn create_branch_in_the_middle() -> Result<()> { set_test_target(project_repository)?; - create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); - create_virtual_branch(project_repository, &BranchCreateRequest::default()) + branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); - create_virtual_branch( - project_repository, - &BranchCreateRequest { + branch_manager + .create_virtual_branch(&BranchCreateRequest { order: Some(1), ..Default::default() - }, - ) - .expect("failed to create virtual branch"); + }) + .expect("failed to create virtual branch"); let vb_state = project_repository.project().virtual_branches(); let mut branches = vb_state @@ -284,7 +290,9 @@ fn create_branch_no_arguments() -> Result<()> { set_test_target(project_repository)?; - create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); let vb_state = project_repository.project().virtual_branches(); @@ -313,10 +321,13 @@ fn hunk_expantion() -> Result<()> { let file_path = Path::new("test.txt"); std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n")?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -404,10 +415,13 @@ fn get_status_files_by_branch() -> Result<()> { let file_path = Path::new("test.txt"); std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n")?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -440,13 +454,17 @@ fn move_hunks_multiple_sources() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch3_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch3_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -539,11 +557,14 @@ fn move_hunks_partial_explicitly() -> Result<()> { "line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\n", )?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -621,7 +642,9 @@ fn add_new_hunk_to_the_end() -> Result<()> { "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15\n", )?; - create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); let statuses = virtual_branches::get_status_by_branch(project_repository, None) @@ -791,7 +814,9 @@ fn merge_vbranch_upstream_clean_rebase() -> Result<()> { integration::update_gitbutler_integration(&vb_state, project_repository)?; let remote_branch: RemoteRefname = "refs/remotes/origin/master".parse().unwrap(); - let mut branch = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let mut branch = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); branch.upstream = Some(remote_branch.clone()); branch.head = last_push; @@ -888,7 +913,9 @@ async fn merge_vbranch_upstream_conflict() -> Result<()> { )?; let remote_branch: RemoteRefname = "refs/remotes/origin/master".parse().unwrap(); - let mut branch = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let mut branch = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); branch.upstream = Some(remote_branch.clone()); branch.head = last_push; @@ -979,7 +1006,9 @@ fn unapply_ownership_partial() -> Result<()> { "line1\nline2\nline3\nline4\nbranch1\n", )?; - create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?; @@ -1033,10 +1062,13 @@ fn unapply_branch() -> Result<()> { let file_path2 = Path::new("test2.txt"); std::fs::write(Path::new(&project.path).join(file_path2), "line5\nline6\n")?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1062,11 +1094,8 @@ fn unapply_branch() -> Result<()> { assert_eq!(branch.files.len(), 1); assert!(branch.active); - let real_branch = virtual_branches::convert_to_real_branch( - project_repository, - branch1_id, - Default::default(), - )?; + let branch_manager = project_repository.branch_manager(); + let real_branch = branch_manager.convert_to_real_branch(branch1_id, Default::default())?; let contents = std::fs::read(Path::new(&project.path).join(file_path))?; assert_eq!("line1\nline2\nline3\nline4\n", String::from_utf8(contents)?); @@ -1076,10 +1105,9 @@ fn unapply_branch() -> Result<()> { let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?; assert!(!branches.iter().any(|b| b.id == branch1_id)); - let branch1_id = virtual_branches::create_virtual_branch_from_branch( - project_repository, - &Refname::try_from(&real_branch)?, - )?; + let branch_manager = project_repository.branch_manager(); + let branch1_id = + branch_manager.create_virtual_branch_from_branch(&Refname::from_str(&real_branch)?)?; let contents = std::fs::read(Path::new(&project.path).join(file_path))?; assert_eq!( "line1\nline2\nline3\nline4\nbranch1\n", @@ -1120,10 +1148,13 @@ fn apply_unapply_added_deleted_files() -> Result<()> { let file_path3 = Path::new("test3.txt"); std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch3_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch3_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1146,38 +1177,27 @@ fn apply_unapply_added_deleted_files() -> Result<()> { list_virtual_branches(project_repository).unwrap(); - let real_branch_2 = virtual_branches::convert_to_real_branch( - project_repository, - branch2_id, - Default::default(), - )?; + let branch_manager = project_repository.branch_manager(); + let real_branch_2 = branch_manager.convert_to_real_branch(branch2_id, Default::default())?; // check that file2 is back let contents = std::fs::read(Path::new(&project.path).join(file_path2))?; assert_eq!("file2\n", String::from_utf8(contents)?); - let real_branch_3 = virtual_branches::convert_to_real_branch( - project_repository, - branch3_id, - Default::default(), - )?; + let real_branch_3 = branch_manager.convert_to_real_branch(branch3_id, Default::default())?; // check that file3 is gone assert!(!Path::new(&project.path).join(file_path3).exists()); - create_virtual_branch_from_branch( - project_repository, - &Refname::try_from(&real_branch_2).unwrap(), - ) - .unwrap(); + branch_manager + .create_virtual_branch_from_branch(&Refname::from_str(&real_branch_2).unwrap()) + .unwrap(); // check that file2 is gone assert!(!Path::new(&project.path).join(file_path2).exists()); - create_virtual_branch_from_branch( - project_repository, - &Refname::try_from(&real_branch_3).unwrap(), - ) - .unwrap(); + branch_manager + .create_virtual_branch_from_branch(&Refname::from_str(&real_branch_3).unwrap()) + .unwrap(); // check that file3 is back let contents = std::fs::read(Path::new(&project.path).join(file_path3))?; @@ -1213,10 +1233,13 @@ fn detect_mergeable_branch() -> Result<()> { let file_path4 = Path::new("test4.txt"); std::fs::write(Path::new(&project.path).join(file_path4), "line5\nline6\n")?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1231,8 +1254,9 @@ fn detect_mergeable_branch() -> Result<()> { .expect("failed to update branch"); // unapply both branches and create some conflicting ones - virtual_branches::convert_to_real_branch(project_repository, branch1_id, Default::default())?; - virtual_branches::convert_to_real_branch(project_repository, branch2_id, Default::default())?; + let branch_manager = project_repository.branch_manager(); + branch_manager.convert_to_real_branch(branch1_id, Default::default())?; + branch_manager.convert_to_real_branch(branch2_id, Default::default())?; project_repository.repo().set_head("refs/heads/master")?; project_repository @@ -1279,9 +1303,12 @@ fn detect_mergeable_branch() -> Result<()> { .checkout_head(Some(&mut git2::build::CheckoutBuilder::default().force()))?; // create branches that conflict with our earlier branches - create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch"); - let branch4_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch4_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1379,13 +1406,17 @@ fn upstream_integrated_vbranch() -> Result<()> { integration::update_gitbutler_integration(&vb_state, project_repository)?; // create vbranches, one integrated, one not - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch2_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; - let branch3_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch3_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1479,7 +1510,9 @@ fn commit_same_hunk_twice() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1570,7 +1603,9 @@ fn commit_same_file_twice() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1661,7 +1696,9 @@ fn commit_partial_by_hunk() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1739,7 +1776,9 @@ fn commit_partial_by_file() -> Result<()> { let file_path3 = Path::new("test3.txt"); std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1797,7 +1836,9 @@ fn commit_add_and_delete_files() -> Result<()> { let file_path3 = Path::new("test3.txt"); std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -1861,7 +1902,9 @@ fn commit_executable_and_symlinks() -> Result<()> { let new_permissions = Permissions::from_mode(permissions.mode() | 0o111); // Add execute permission std::fs::set_permissions(&exec, new_permissions)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -2020,7 +2063,9 @@ fn pre_commit_hook_rejection() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -2058,7 +2103,9 @@ fn post_commit_hook() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id; @@ -2107,7 +2154,9 @@ fn commit_msg_hook_rejection() -> Result<()> { set_test_target(project_repository)?; - let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default()) + let branch_manager = project_repository.branch_manager(); + let branch1_id = branch_manager + .create_virtual_branch(&BranchCreateRequest::default()) .expect("failed to create virtual branch") .id;