Split branch creation and deletion into their own home

This commit is contained in:
Caleb Owens 2024-07-09 16:43:58 +02:00
parent f9043920c0
commit 9b59764db3
No known key found for this signature in database
20 changed files with 982 additions and 877 deletions

4
Cargo.lock generated
View File

@ -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",

View File

@ -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" }

View File

@ -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"

View File

@ -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<ReferenceName>;

View File

@ -1,5 +1,6 @@
pub mod branch;
pub mod branch_ext;
pub mod dedup;
pub mod diff;
pub mod file_ownership;
pub mod hunk;

View File

@ -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]

View File

@ -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)?;

View File

@ -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>;

View File

@ -54,3 +54,7 @@ impl<T> fmt::Debug for TaggedString<T> {
self.0.fmt(f)
}
}
pub struct _ReferenceName;
/// The name of a reference ie. `refs/heads/master`
pub type ReferenceName = TaggedString<_ReferenceName>;

View File

@ -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"]

View File

@ -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;

View File

@ -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]

View File

@ -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<Vec<git2::Branch<'_>>> {
) -> anyhow::Result<Vec<ReferenceName>> {
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<git2::Branch> = Vec::new();
let mut unapplied_branch_names: Vec<ReferenceName> = 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);

View File

@ -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<branch::Branch> {
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::<Vec<_>>(),
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<BranchId> {
// 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::<Vec<branch::Branch>>();
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<String> {
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<ReferenceName> {
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<git2::Branch<'_>> {
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<git2::Oid> {
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(())
}
}

View File

@ -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<BranchId> {
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<BaseBranch> {
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<BranchId> {
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();
}
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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<String>)
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<git2::Branch<'_>> {
fn build_real_branch<'l>(
project_repository: &'l ProjectRepository,
vbranch: &mut branch::Branch,
name_conflict_resolution: NameConflitResolution,
) -> Result<git2::Branch<'l>> {
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<git2::Oid> {
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<branch::Branch> {
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::<Vec<_>>(),
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<BranchId, BranchStatus> = virtual_branches
@ -3152,392 +2854,6 @@ pub fn move_commit(
Ok(())
}
pub fn create_virtual_branch_from_branch(
project_repository: &ProjectRepository,
upstream: &Refname,
) -> Result<BranchId> {
fn apply_branch(project_repository: &ProjectRepository, branch_id: BranchId) -> Result<String> {
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::<Vec<branch::Branch>>();
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<S: AsRef<[u8]>>(base_image: S, patch: &Patch<'_, [u8]>) -> Result<BString> {
fn md5_hash_hex(b: impl AsRef<[u8]>) -> String {

View File

@ -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;