mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-09-11 12:25:42 +03:00
move virtual, integration and base modules to gitbutler-branch crate
This commit is contained in:
parent
f224207029
commit
9dc82e8fe9
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2270,6 +2270,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"git2",
|
"git2",
|
||||||
|
"gitbutler-branch",
|
||||||
"gitbutler-core",
|
"gitbutler-core",
|
||||||
"keyring",
|
"keyring",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -4,12 +4,12 @@ use anyhow::{anyhow, Context, Result};
|
|||||||
use git2::Index;
|
use git2::Index;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use gitbutler_core::virtual_branches::integration::{
|
use super::r#virtual as vb;
|
||||||
get_workspace_head, update_gitbutler_integration,
|
use super::r#virtual::convert_to_real_branch;
|
||||||
};
|
use crate::integration::{get_workspace_head, update_gitbutler_integration};
|
||||||
|
use crate::VirtualBranchHunk;
|
||||||
use gitbutler_core::virtual_branches::{
|
use gitbutler_core::virtual_branches::{
|
||||||
branch, convert_to_real_branch, target, BranchId, RemoteCommit, VirtualBranchHunk,
|
branch, target, BranchId, RemoteCommit, VirtualBranchesHandle, GITBUTLER_INTEGRATION_REFERENCE,
|
||||||
VirtualBranchesHandle, GITBUTLER_INTEGRATION_REFERENCE,
|
|
||||||
};
|
};
|
||||||
use gitbutler_core::{error::Marker, git::RepositoryExt, rebase::cherry_rebase};
|
use gitbutler_core::{error::Marker, git::RepositoryExt, rebase::cherry_rebase};
|
||||||
use gitbutler_core::{
|
use gitbutler_core::{
|
||||||
@ -246,7 +246,7 @@ pub fn set_base_branch(
|
|||||||
created_timestamp_ms: now_ms,
|
created_timestamp_ms: now_ms,
|
||||||
updated_timestamp_ms: now_ms,
|
updated_timestamp_ms: now_ms,
|
||||||
head: current_head_commit.id(),
|
head: current_head_commit.id(),
|
||||||
tree: gitbutler_core::virtual_branches::write_tree_onto_commit(
|
tree: vb::write_tree_onto_commit(
|
||||||
project_repository,
|
project_repository,
|
||||||
current_head_commit.id(),
|
current_head_commit.id(),
|
||||||
diff::diff_files_into_hunks(wd_diff),
|
diff::diff_files_into_hunks(wd_diff),
|
||||||
@ -363,173 +363,181 @@ pub fn update_base_branch(
|
|||||||
let integration_commit = get_workspace_head(&vb_state, project_repository)?;
|
let integration_commit = get_workspace_head(&vb_state, project_repository)?;
|
||||||
|
|
||||||
// try to update every branch
|
// try to update every branch
|
||||||
let updated_vbranches = gitbutler_core::virtual_branches::get_status_by_branch(
|
let updated_vbranches =
|
||||||
project_repository,
|
vb::get_status_by_branch(project_repository, Some(&integration_commit))?
|
||||||
Some(&integration_commit),
|
.0
|
||||||
)?
|
.into_iter()
|
||||||
.0
|
.map(|(branch, _)| branch)
|
||||||
.into_iter()
|
.map(
|
||||||
.map(|(branch, _)| branch)
|
|
||||||
.map(
|
|
||||||
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
|
||||||
let branch_tree = repo.find_tree(branch.tree)?;
|
|
||||||
|
|
||||||
let branch_head_commit = repo.find_commit(branch.head).context(format!(
|
|
||||||
"failed to find commit {} for branch {}",
|
|
||||||
branch.head, branch.id
|
|
||||||
))?;
|
|
||||||
let branch_head_tree = branch_head_commit.tree().context(format!(
|
|
||||||
"failed to find tree for commit {} for branch {}",
|
|
||||||
branch.head, branch.id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let result_integrated_detected =
|
|
||||||
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
||||||
// branch head tree is the same as the new target tree.
|
let branch_tree = repo.find_tree(branch.tree)?;
|
||||||
// meaning we can safely use the new target commit as the branch head.
|
|
||||||
|
|
||||||
branch.head = new_target_commit.id();
|
let branch_head_commit = repo.find_commit(branch.head).context(format!(
|
||||||
|
"failed to find commit {} for branch {}",
|
||||||
|
branch.head, branch.id
|
||||||
|
))?;
|
||||||
|
let branch_head_tree = branch_head_commit.tree().context(format!(
|
||||||
|
"failed to find tree for commit {} for branch {}",
|
||||||
|
branch.head, branch.id
|
||||||
|
))?;
|
||||||
|
|
||||||
// it also means that the branch is fully integrated into the target.
|
let result_integrated_detected =
|
||||||
// disconnect it from the upstream
|
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
||||||
branch.upstream = None;
|
// branch head tree is the same as the new target tree.
|
||||||
branch.upstream_head = None;
|
// meaning we can safely use the new target commit as the branch head.
|
||||||
|
|
||||||
let non_commited_files =
|
branch.head = new_target_commit.id();
|
||||||
diff::trees(project_repository.repo(), &branch_head_tree, &branch_tree)?;
|
|
||||||
if non_commited_files.is_empty() {
|
// it also means that the branch is fully integrated into the target.
|
||||||
// if there are no commited files, then the branch is fully merged
|
// disconnect it from the upstream
|
||||||
// and we can delete it.
|
branch.upstream = None;
|
||||||
vb_state.remove_branch(branch.id)?;
|
branch.upstream_head = None;
|
||||||
project_repository.delete_branch_reference(&branch)?;
|
|
||||||
Ok(None)
|
let non_commited_files = diff::trees(
|
||||||
} else {
|
project_repository.repo(),
|
||||||
vb_state.set_branch(branch.clone())?;
|
&branch_head_tree,
|
||||||
Ok(Some(branch))
|
&branch_tree,
|
||||||
|
)?;
|
||||||
|
if non_commited_files.is_empty() {
|
||||||
|
// if there are no commited files, then the branch is fully merged
|
||||||
|
// and we can delete it.
|
||||||
|
vb_state.remove_branch(branch.id)?;
|
||||||
|
project_repository.delete_branch_reference(&branch)?;
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
Ok(Some(branch))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if branch_head_tree.id() == new_target_tree.id() {
|
||||||
|
return result_integrated_detected(branch);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if branch_head_tree.id() == new_target_tree.id() {
|
// try to merge branch head with new target
|
||||||
return result_integrated_detected(branch);
|
let mut branch_tree_merge_index = repo
|
||||||
}
|
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree, None)
|
||||||
|
.context(format!("failed to merge trees for branch {}", branch.id))?;
|
||||||
|
|
||||||
// try to merge branch head with new target
|
if branch_tree_merge_index.has_conflicts() {
|
||||||
let mut branch_tree_merge_index = repo
|
// branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back.
|
||||||
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree, None)
|
let unapplied_real_branch = convert_to_real_branch(
|
||||||
.context(format!("failed to merge trees for branch {}", branch.id))?;
|
project_repository,
|
||||||
|
branch.id,
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
if branch_tree_merge_index.has_conflicts() {
|
unapplied_branch_names.push(unapplied_real_branch);
|
||||||
// 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())?;
|
|
||||||
|
|
||||||
unapplied_branch_names.push(unapplied_real_branch);
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(None);
|
let branch_merge_index_tree_oid =
|
||||||
}
|
branch_tree_merge_index.write_tree_to(project_repository.repo())?;
|
||||||
|
|
||||||
let branch_merge_index_tree_oid =
|
if branch_merge_index_tree_oid == new_target_tree.id() {
|
||||||
branch_tree_merge_index.write_tree_to(project_repository.repo())?;
|
return result_integrated_detected(branch);
|
||||||
|
}
|
||||||
|
|
||||||
if branch_merge_index_tree_oid == new_target_tree.id() {
|
if branch.head == target.sha {
|
||||||
return result_integrated_detected(branch);
|
// there are no commits on the branch, so we can just update the head to the new target and calculate the new tree
|
||||||
}
|
branch.head = new_target_commit.id();
|
||||||
|
branch.tree = branch_merge_index_tree_oid;
|
||||||
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
return Ok(Some(branch));
|
||||||
|
}
|
||||||
|
|
||||||
if branch.head == target.sha {
|
let mut branch_head_merge_index = repo
|
||||||
// there are no commits on the branch, so we can just update the head to the new target and calculate the new tree
|
.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree, None)
|
||||||
branch.head = new_target_commit.id();
|
.context(format!(
|
||||||
branch.tree = branch_merge_index_tree_oid;
|
"failed to merge head tree for branch {}",
|
||||||
vb_state.set_branch(branch.clone())?;
|
branch.id
|
||||||
return Ok(Some(branch));
|
))?;
|
||||||
}
|
|
||||||
|
|
||||||
let mut branch_head_merge_index = repo
|
if branch_head_merge_index.has_conflicts() {
|
||||||
.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree, None)
|
// branch commits conflict with new target, make sure the branch is
|
||||||
.context(format!(
|
// unapplied. conflicts witll be dealt with when applying it back.
|
||||||
"failed to merge head tree for branch {}",
|
let unapplied_real_branch = convert_to_real_branch(
|
||||||
branch.id
|
project_repository,
|
||||||
))?;
|
branch.id,
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
unapplied_branch_names.push(unapplied_real_branch);
|
||||||
|
|
||||||
if branch_head_merge_index.has_conflicts() {
|
return Ok(None);
|
||||||
// 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())?;
|
|
||||||
unapplied_branch_names.push(unapplied_real_branch);
|
|
||||||
|
|
||||||
return Ok(None);
|
// branch commits do not conflict with new target, so lets merge them
|
||||||
}
|
let branch_head_merge_tree_oid = branch_head_merge_index
|
||||||
|
.write_tree_to(project_repository.repo())
|
||||||
|
.context(format!(
|
||||||
|
"failed to write head merge index for {}",
|
||||||
|
branch.id
|
||||||
|
))?;
|
||||||
|
|
||||||
// branch commits do not conflict with new target, so lets merge them
|
let ok_with_force_push = branch.allow_rebasing;
|
||||||
let branch_head_merge_tree_oid = branch_head_merge_index
|
|
||||||
.write_tree_to(project_repository.repo())
|
|
||||||
.context(format!(
|
|
||||||
"failed to write head merge index for {}",
|
|
||||||
branch.id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let ok_with_force_push = branch.allow_rebasing;
|
let result_merge =
|
||||||
|
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
||||||
|
// 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 branch_head_merge_tree = repo
|
||||||
|
.find_tree(branch_head_merge_tree_oid)
|
||||||
|
.context("failed to find tree")?;
|
||||||
|
|
||||||
let result_merge = |mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
let new_target_head = project_repository
|
||||||
// branch was pushed to upstream, and user doesn't like force pushing.
|
.commit(
|
||||||
// create a merge commit to avoid the need of force pushing then.
|
format!(
|
||||||
let branch_head_merge_tree = repo
|
"Merged {}/{} into {}",
|
||||||
.find_tree(branch_head_merge_tree_oid)
|
target.branch.remote(),
|
||||||
.context("failed to find tree")?;
|
target.branch.branch(),
|
||||||
|
branch.name,
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
&branch_head_merge_tree,
|
||||||
|
&[&branch_head_commit, &new_target_commit],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("failed to commit merge")?;
|
||||||
|
|
||||||
let new_target_head = project_repository
|
branch.head = new_target_head;
|
||||||
.commit(
|
branch.tree = branch_merge_index_tree_oid;
|
||||||
format!(
|
vb_state.set_branch(branch.clone())?;
|
||||||
"Merged {}/{} into {}",
|
Ok(Some(branch))
|
||||||
target.branch.remote(),
|
};
|
||||||
target.branch.branch(),
|
|
||||||
branch.name,
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
&branch_head_merge_tree,
|
|
||||||
&[&branch_head_commit, &new_target_commit],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.context("failed to commit merge")?;
|
|
||||||
|
|
||||||
branch.head = new_target_head;
|
if branch.upstream.is_some() && !ok_with_force_push {
|
||||||
branch.tree = branch_merge_index_tree_oid;
|
return result_merge(branch);
|
||||||
vb_state.set_branch(branch.clone())?;
|
}
|
||||||
Ok(Some(branch))
|
|
||||||
};
|
|
||||||
|
|
||||||
if branch.upstream.is_some() && !ok_with_force_push {
|
// branch was not pushed to upstream yet. attempt a rebase,
|
||||||
return result_merge(branch);
|
let rebased_head_oid = cherry_rebase(
|
||||||
}
|
project_repository,
|
||||||
|
new_target_commit.id(),
|
||||||
|
new_target_commit.id(),
|
||||||
|
branch.head,
|
||||||
|
);
|
||||||
|
|
||||||
// branch was not pushed to upstream yet. attempt a rebase,
|
// rebase failed, just do the merge
|
||||||
let rebased_head_oid = cherry_rebase(
|
if rebased_head_oid.is_err() {
|
||||||
project_repository,
|
return result_merge(branch);
|
||||||
new_target_commit.id(),
|
}
|
||||||
new_target_commit.id(),
|
|
||||||
branch.head,
|
|
||||||
);
|
|
||||||
|
|
||||||
// rebase failed, just do the merge
|
if let Some(rebased_head_oid) = rebased_head_oid? {
|
||||||
if rebased_head_oid.is_err() {
|
// rebase worked out, rewrite the branch head
|
||||||
return result_merge(branch);
|
branch.head = rebased_head_oid;
|
||||||
}
|
branch.tree = branch_merge_index_tree_oid;
|
||||||
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
return Ok(Some(branch));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(rebased_head_oid) = rebased_head_oid? {
|
result_merge(branch)
|
||||||
// rebase worked out, rewrite the branch head
|
},
|
||||||
branch.head = rebased_head_oid;
|
)
|
||||||
branch.tree = branch_merge_index_tree_oid;
|
.collect::<Result<Vec<_>>>()?
|
||||||
vb_state.set_branch(branch.clone())?;
|
.into_iter()
|
||||||
return Ok(Some(branch));
|
.flatten()
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
result_merge(branch)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect::<Result<Vec<_>>>()?
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// ok, now all the problematic branches have been unapplied
|
// ok, now all the problematic branches have been unapplied
|
||||||
// now we calculate and checkout new tree for the working directory
|
// now we calculate and checkout new tree for the working directory
|
||||||
@ -560,10 +568,7 @@ pub fn update_base_branch(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Rewriting the integration commit is necessary after changing target sha.
|
// Rewriting the integration commit is necessary after changing target sha.
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
Ok(unapplied_branch_names)
|
Ok(unapplied_branch_names)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,8 +155,7 @@ impl Controller {
|
|||||||
let _ = project_repository
|
let _ = project_repository
|
||||||
.project()
|
.project()
|
||||||
.create_snapshot(SnapshotDetails::new(OperationKind::MergeUpstream));
|
.create_snapshot(SnapshotDetails::new(OperationKind::MergeUpstream));
|
||||||
virtual_branches::integrate_upstream_commits(&project_repository, branch_id)
|
branch::integrate_upstream_commits(&project_repository, branch_id).map_err(Into::into)
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_base_branch(&self, project: &Project) -> Result<Vec<ReferenceName>> {
|
pub async fn update_base_branch(&self, project: &Project) -> Result<Vec<ReferenceName>> {
|
||||||
@ -499,7 +498,7 @@ impl Controller {
|
|||||||
|
|
||||||
fn open_with_verify(project: &Project) -> Result<Repository> {
|
fn open_with_verify(project: &Project) -> Result<Repository> {
|
||||||
let project_repository = Repository::open(project)?;
|
let project_repository = Repository::open(project)?;
|
||||||
virtual_branches::integration::verify_branch(&project_repository)?;
|
crate::integration::verify_branch(&project_repository)?;
|
||||||
Ok(project_repository)
|
Ok(project_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,13 +3,13 @@ use std::{path::PathBuf, vec};
|
|||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use bstr::ByteSlice;
|
use bstr::ByteSlice;
|
||||||
|
|
||||||
use super::{
|
use gitbutler_core::error::Marker;
|
||||||
|
use gitbutler_core::git::RepositoryExt;
|
||||||
|
use gitbutler_core::virtual_branches::{
|
||||||
VirtualBranchesHandle, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
|
VirtualBranchesHandle, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
|
||||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME, GITBUTLER_INTEGRATION_REFERENCE,
|
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME, GITBUTLER_INTEGRATION_REFERENCE,
|
||||||
};
|
};
|
||||||
use crate::error::Marker;
|
use gitbutler_core::{
|
||||||
use crate::git::RepositoryExt;
|
|
||||||
use crate::{
|
|
||||||
git::CommitExt,
|
git::CommitExt,
|
||||||
project_repository::{self, conflicts, LogUntil},
|
project_repository::{self, conflicts, LogUntil},
|
||||||
virtual_branches::branch::BranchCreateRequest,
|
virtual_branches::branch::BranchCreateRequest,
|
||||||
@ -297,7 +297,13 @@ pub fn verify_branch(project_repository: &project_repository::Repository) -> Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl project_repository::Repository {
|
pub trait Verify {
|
||||||
|
fn verify_head_is_set(&self) -> Result<&Self>;
|
||||||
|
fn verify_current_branch_name(&self) -> Result<&Self>;
|
||||||
|
fn verify_head_is_clean(&self) -> Result<&Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Verify for project_repository::Repository {
|
||||||
fn verify_head_is_set(&self) -> Result<&Self> {
|
fn verify_head_is_set(&self) -> Result<&Self> {
|
||||||
match self.get_head().context("failed to get head")?.name() {
|
match self.get_head().context("failed to get head")?.name() {
|
||||||
Some(refname) if *refname == GITBUTLER_INTEGRATION_REFERENCE.to_string() => Ok(self),
|
Some(refname) if *refname == GITBUTLER_INTEGRATION_REFERENCE.to_string() => Ok(self),
|
@ -8,3 +8,5 @@ pub use r#virtual::*;
|
|||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
||||||
pub mod base;
|
pub mod base;
|
||||||
|
|
||||||
|
pub mod integration;
|
||||||
|
@ -18,6 +18,7 @@ use gitbutler_core::virtual_branches::Author;
|
|||||||
use hex::ToHex;
|
use hex::ToHex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::integration::{get_integration_commiter, get_workspace_head};
|
||||||
use gitbutler_core::error::Code;
|
use gitbutler_core::error::Code;
|
||||||
use gitbutler_core::error::Marker;
|
use gitbutler_core::error::Marker;
|
||||||
use gitbutler_core::git::diff::GitHunk;
|
use gitbutler_core::git::diff::GitHunk;
|
||||||
@ -28,7 +29,6 @@ use gitbutler_core::git::{
|
|||||||
use gitbutler_core::rebase::{cherry_rebase, cherry_rebase_group};
|
use gitbutler_core::rebase::{cherry_rebase, cherry_rebase_group};
|
||||||
use gitbutler_core::time::now_since_unix_epoch_ms;
|
use gitbutler_core::time::now_since_unix_epoch_ms;
|
||||||
use gitbutler_core::virtual_branches::branch::HunkHash;
|
use gitbutler_core::virtual_branches::branch::HunkHash;
|
||||||
use gitbutler_core::virtual_branches::integration::{get_integration_commiter, get_workspace_head};
|
|
||||||
use gitbutler_core::virtual_branches::{
|
use gitbutler_core::virtual_branches::{
|
||||||
branch::{
|
branch::{
|
||||||
self, Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims, Hunk, OwnershipClaim,
|
self, Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims, Hunk, OwnershipClaim,
|
||||||
@ -299,10 +299,7 @@ pub fn unapply_ownership(
|
|||||||
.checkout()
|
.checkout()
|
||||||
.context("failed to checkout tree")?;
|
.context("failed to checkout tree")?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -452,10 +449,7 @@ pub fn convert_to_real_branch(
|
|||||||
// Ensure we still have a default target
|
// Ensure we still have a default target
|
||||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(real_branch)
|
Ok(real_branch)
|
||||||
}
|
}
|
||||||
@ -489,10 +483,8 @@ pub fn list_virtual_branches(
|
|||||||
.get_default_target()
|
.get_default_target()
|
||||||
.context("failed to get default target")?;
|
.context("failed to get default target")?;
|
||||||
|
|
||||||
let integration_commit_id = gitbutler_core::virtual_branches::integration::get_workspace_head(
|
let integration_commit_id =
|
||||||
&vb_state,
|
crate::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
let integration_commit = project_repository
|
let integration_commit = project_repository
|
||||||
.repo()
|
.repo()
|
||||||
.find_commit(integration_commit_id)
|
.find_commit(integration_commit_id)
|
||||||
@ -1000,10 +992,7 @@ pub fn integrate_upstream_commits(
|
|||||||
.checkout()?;
|
.checkout()?;
|
||||||
};
|
};
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1859,11 +1848,8 @@ pub fn reset_branch(
|
|||||||
.set_branch(branch)
|
.set_branch(branch)
|
||||||
.context("failed to write branch")?;
|
.context("failed to write branch")?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -2164,11 +2150,8 @@ pub fn commit(
|
|||||||
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
||||||
vb_state.set_branch(branch.clone())?;
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
|
|
||||||
Ok(commit_oid)
|
Ok(commit_oid)
|
||||||
}
|
}
|
||||||
@ -2569,10 +2552,7 @@ pub fn move_commit_file(
|
|||||||
if upstream_commits.is_empty() {
|
if upstream_commits.is_empty() {
|
||||||
target_branch.head = commit_oid;
|
target_branch.head = commit_oid;
|
||||||
vb_state.set_branch(target_branch.clone())?;
|
vb_state.set_branch(target_branch.clone())?;
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
return Ok(commit_oid);
|
return Ok(commit_oid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2589,10 +2569,7 @@ pub fn move_commit_file(
|
|||||||
if let Some(new_head) = new_head {
|
if let Some(new_head) = new_head {
|
||||||
target_branch.head = new_head;
|
target_branch.head = new_head;
|
||||||
vb_state.set_branch(target_branch.clone())?;
|
vb_state.set_branch(target_branch.clone())?;
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
Ok(commit_oid)
|
Ok(commit_oid)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("rebase failed"))
|
Err(anyhow!("rebase failed"))
|
||||||
@ -2630,10 +2607,8 @@ pub fn amend(
|
|||||||
|
|
||||||
let default_target = vb_state.get_default_target()?;
|
let default_target = vb_state.get_default_target()?;
|
||||||
|
|
||||||
let integration_commit_id = gitbutler_core::virtual_branches::integration::get_workspace_head(
|
let integration_commit_id =
|
||||||
&vb_state,
|
crate::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (mut applied_statuses, _) = get_applied_status(
|
let (mut applied_statuses, _) = get_applied_status(
|
||||||
project_repository,
|
project_repository,
|
||||||
@ -2729,10 +2704,7 @@ pub fn amend(
|
|||||||
if upstream_commits.is_empty() {
|
if upstream_commits.is_empty() {
|
||||||
target_branch.head = commit_oid;
|
target_branch.head = commit_oid;
|
||||||
vb_state.set_branch(target_branch.clone())?;
|
vb_state.set_branch(target_branch.clone())?;
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
return Ok(commit_oid);
|
return Ok(commit_oid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2748,10 +2720,7 @@ pub fn amend(
|
|||||||
if let Some(new_head) = new_head {
|
if let Some(new_head) = new_head {
|
||||||
target_branch.head = new_head;
|
target_branch.head = new_head;
|
||||||
vb_state.set_branch(target_branch.clone())?;
|
vb_state.set_branch(target_branch.clone())?;
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
Ok(commit_oid)
|
Ok(commit_oid)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("rebase failed"))
|
Err(anyhow!("rebase failed"))
|
||||||
@ -2806,11 +2775,8 @@ pub fn reorder_commit(
|
|||||||
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
||||||
vb_state.set_branch(branch.clone())?;
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
} else {
|
} else {
|
||||||
// move commit down
|
// move commit down
|
||||||
if default_target.sha == parent_oid {
|
if default_target.sha == parent_oid {
|
||||||
@ -2846,11 +2812,8 @@ pub fn reorder_commit(
|
|||||||
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
||||||
vb_state.set_branch(branch.clone())?;
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -2884,11 +2847,8 @@ pub fn insert_blank_commit(
|
|||||||
if commit.id() == branch.head && offset < 0 {
|
if commit.id() == branch.head && offset < 0 {
|
||||||
// inserting before the first commit
|
// inserting before the first commit
|
||||||
branch.head = blank_commit_oid;
|
branch.head = blank_commit_oid;
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
} else {
|
} else {
|
||||||
// rebase all commits above it onto the new commit
|
// rebase all commits above it onto the new commit
|
||||||
match cherry_rebase(
|
match cherry_rebase(
|
||||||
@ -2899,11 +2859,8 @@ pub fn insert_blank_commit(
|
|||||||
) {
|
) {
|
||||||
Ok(Some(new_head)) => {
|
Ok(Some(new_head)) => {
|
||||||
branch.head = new_head;
|
branch.head = new_head;
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
}
|
}
|
||||||
Ok(None) => bail!("no rebase happened"),
|
Ok(None) => bail!("no rebase happened"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -2962,11 +2919,8 @@ pub fn undo_commit(
|
|||||||
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
||||||
vb_state.set_branch(branch.clone())?;
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -3060,11 +3014,8 @@ pub fn squash(
|
|||||||
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
||||||
vb_state.set_branch(branch.clone())?;
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.context("rebase error").context(Code::Unknown)),
|
Err(err) => Err(err.context("rebase error").context(Code::Unknown)),
|
||||||
@ -3147,11 +3098,8 @@ pub fn update_commit_message(
|
|||||||
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
|
||||||
vb_state.set_branch(branch.clone())?;
|
vb_state.set_branch(branch.clone())?;
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3177,10 +3125,8 @@ pub fn move_commit(
|
|||||||
|
|
||||||
let default_target = vb_state.get_default_target()?;
|
let default_target = vb_state.get_default_target()?;
|
||||||
|
|
||||||
let integration_commit_id = gitbutler_core::virtual_branches::integration::get_workspace_head(
|
let integration_commit_id =
|
||||||
&vb_state,
|
crate::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (mut applied_statuses, _) = get_applied_status(
|
let (mut applied_statuses, _) = get_applied_status(
|
||||||
project_repository,
|
project_repository,
|
||||||
@ -3292,11 +3238,8 @@ pub fn move_commit(
|
|||||||
vb_state.set_branch(destination_branch.clone())?;
|
vb_state.set_branch(destination_branch.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
&vb_state,
|
.context("failed to update gitbutler integration")?;
|
||||||
project_repository,
|
|
||||||
)
|
|
||||||
.context("failed to update gitbutler integration")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -3531,10 +3474,7 @@ pub fn create_virtual_branch_from_branch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
|
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||||
&vb_state,
|
|
||||||
project_repository,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(branch.name)
|
Ok(branch.name)
|
||||||
}
|
}
|
||||||
|
@ -1 +1,3 @@
|
|||||||
mod virtual_branches;
|
mod virtual_branches;
|
||||||
|
|
||||||
|
mod extra;
|
||||||
|
2141
crates/gitbutler-branch/tests/extra/mod.rs
Normal file
2141
crates/gitbutler-branch/tests/extra/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,638 +0,0 @@
|
|||||||
use std::{path::Path, time};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use git2::Index;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
branch, convert_to_real_branch,
|
|
||||||
integration::{
|
|
||||||
get_workspace_head, update_gitbutler_integration, GITBUTLER_INTEGRATION_REFERENCE,
|
|
||||||
},
|
|
||||||
target, BranchId, RemoteCommit, VirtualBranchHunk, VirtualBranchesHandle,
|
|
||||||
};
|
|
||||||
use crate::{error::Marker, git::RepositoryExt, rebase::cherry_rebase};
|
|
||||||
use crate::{
|
|
||||||
git::{self, diff},
|
|
||||||
project_repository::{self, LogUntil},
|
|
||||||
projects::FetchResult,
|
|
||||||
virtual_branches::branch::BranchOwnershipClaims,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, PartialEq, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct BaseBranch {
|
|
||||||
pub branch_name: String,
|
|
||||||
pub remote_name: String,
|
|
||||||
pub remote_url: String,
|
|
||||||
pub push_remote_name: Option<String>,
|
|
||||||
pub push_remote_url: String,
|
|
||||||
#[serde(with = "crate::serde::oid")]
|
|
||||||
pub base_sha: git2::Oid,
|
|
||||||
#[serde(with = "crate::serde::oid")]
|
|
||||||
pub current_sha: git2::Oid,
|
|
||||||
pub behind: usize,
|
|
||||||
pub upstream_commits: Vec<RemoteCommit>,
|
|
||||||
pub recent_commits: Vec<RemoteCommit>,
|
|
||||||
pub last_fetched_ms: Option<u128>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_base_branch_data(
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
) -> Result<BaseBranch> {
|
|
||||||
let target = default_target(&project_repository.project().gb_dir())?;
|
|
||||||
let base = target_to_base_branch(project_repository, &target)?;
|
|
||||||
Ok(base)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn go_back_to_integration(
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
default_target: &target::Target,
|
|
||||||
) -> Result<BaseBranch> {
|
|
||||||
let statuses = project_repository
|
|
||||||
.repo()
|
|
||||||
.statuses(Some(
|
|
||||||
git2::StatusOptions::new()
|
|
||||||
.show(git2::StatusShow::IndexAndWorkdir)
|
|
||||||
.include_untracked(true),
|
|
||||||
))
|
|
||||||
.context("failed to get status")?;
|
|
||||||
if !statuses.is_empty() {
|
|
||||||
return Err(anyhow!("current HEAD is dirty")).context(Marker::ProjectConflict);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vb_state = project_repository.project().virtual_branches();
|
|
||||||
let all_virtual_branches = vb_state
|
|
||||||
.list_branches()
|
|
||||||
.context("failed to read virtual branches")?;
|
|
||||||
|
|
||||||
let applied_virtual_branches = all_virtual_branches
|
|
||||||
.iter()
|
|
||||||
.filter(|branch| branch.applied)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let target_commit = project_repository
|
|
||||||
.repo()
|
|
||||||
.find_commit(default_target.sha)
|
|
||||||
.context("failed to find target commit")?;
|
|
||||||
|
|
||||||
let base_tree = target_commit
|
|
||||||
.tree()
|
|
||||||
.context("failed to get base tree from commit")?;
|
|
||||||
let mut final_tree = target_commit
|
|
||||||
.tree()
|
|
||||||
.context("failed to get base tree from commit")?;
|
|
||||||
for branch in &applied_virtual_branches {
|
|
||||||
// merge this branches tree with our tree
|
|
||||||
let branch_head = project_repository
|
|
||||||
.repo()
|
|
||||||
.find_commit(branch.head)
|
|
||||||
.context("failed to find branch head")?;
|
|
||||||
let branch_tree = branch_head
|
|
||||||
.tree()
|
|
||||||
.context("failed to get branch head tree")?;
|
|
||||||
let mut result = project_repository
|
|
||||||
.repo()
|
|
||||||
.merge_trees(&base_tree, &final_tree, &branch_tree, None)
|
|
||||||
.context("failed to merge")?;
|
|
||||||
let final_tree_oid = result
|
|
||||||
.write_tree_to(project_repository.repo())
|
|
||||||
.context("failed to write tree")?;
|
|
||||||
final_tree = project_repository
|
|
||||||
.repo()
|
|
||||||
.find_tree(final_tree_oid)
|
|
||||||
.context("failed to find written tree")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
project_repository
|
|
||||||
.repo()
|
|
||||||
.checkout_tree_builder(&final_tree)
|
|
||||||
.force()
|
|
||||||
.checkout()
|
|
||||||
.context("failed to checkout tree")?;
|
|
||||||
|
|
||||||
let base = target_to_base_branch(project_repository, default_target)?;
|
|
||||||
update_gitbutler_integration(&vb_state, project_repository)?;
|
|
||||||
Ok(base)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_base_branch(
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
target_branch_ref: &git::RemoteRefname,
|
|
||||||
) -> Result<BaseBranch> {
|
|
||||||
let repo = project_repository.repo();
|
|
||||||
|
|
||||||
// if target exists, and it is the same as the requested branch, we should go back
|
|
||||||
if let Ok(target) = default_target(&project_repository.project().gb_dir()) {
|
|
||||||
if target.branch.eq(target_branch_ref) {
|
|
||||||
return go_back_to_integration(project_repository, &target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookup a branch by name
|
|
||||||
let target_branch = match repo.find_branch_by_refname(&target_branch_ref.clone().into()) {
|
|
||||||
Ok(branch) => branch,
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
}
|
|
||||||
.ok_or(anyhow!("remote branch '{}' not found", target_branch_ref))?;
|
|
||||||
|
|
||||||
let remote = repo
|
|
||||||
.find_remote(target_branch_ref.remote())
|
|
||||||
.context(format!(
|
|
||||||
"failed to find remote for branch {}",
|
|
||||||
target_branch.get().name().unwrap()
|
|
||||||
))?;
|
|
||||||
let remote_url = remote.url().context(format!(
|
|
||||||
"failed to get remote url for {}",
|
|
||||||
target_branch_ref.remote()
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let target_branch_head = target_branch.get().peel_to_commit().context(format!(
|
|
||||||
"failed to peel branch {} to commit",
|
|
||||||
target_branch.get().name().unwrap()
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let current_head = repo.head().context("Failed to get HEAD reference")?;
|
|
||||||
let current_head_commit = current_head
|
|
||||||
.peel_to_commit()
|
|
||||||
.context("Failed to peel HEAD reference to commit")?;
|
|
||||||
|
|
||||||
// calculate the commit as the merge-base between HEAD in project_repository and this target commit
|
|
||||||
let target_commit_oid = repo
|
|
||||||
.merge_base(current_head_commit.id(), target_branch_head.id())
|
|
||||||
.context(format!(
|
|
||||||
"Failed to calculate merge base between {} and {}",
|
|
||||||
current_head_commit.id(),
|
|
||||||
target_branch_head.id()
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let target = target::Target {
|
|
||||||
branch: target_branch_ref.clone(),
|
|
||||||
remote_url: remote_url.to_string(),
|
|
||||||
sha: target_commit_oid,
|
|
||||||
push_remote_name: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let vb_state = project_repository.project().virtual_branches();
|
|
||||||
vb_state.set_default_target(target.clone())?;
|
|
||||||
|
|
||||||
// TODO: make sure this is a real branch
|
|
||||||
let head_name: git::Refname = current_head
|
|
||||||
.name()
|
|
||||||
.map(|name| name.parse().expect("libgit2 provides valid refnames"))
|
|
||||||
.context("Failed to get HEAD reference name")?;
|
|
||||||
if !head_name
|
|
||||||
.to_string()
|
|
||||||
.eq(&GITBUTLER_INTEGRATION_REFERENCE.to_string())
|
|
||||||
{
|
|
||||||
// if there are any commits on the head branch or uncommitted changes in the working directory, we need to
|
|
||||||
// put them into a virtual branch
|
|
||||||
|
|
||||||
let wd_diff = diff::workdir(repo, ¤t_head_commit.id())?;
|
|
||||||
if !wd_diff.is_empty() || current_head_commit.id() != target.sha {
|
|
||||||
// assign ownership to the branch
|
|
||||||
let ownership = wd_diff.iter().fold(
|
|
||||||
BranchOwnershipClaims::default(),
|
|
||||||
|mut ownership, (file_path, diff)| {
|
|
||||||
for hunk in &diff.hunks {
|
|
||||||
ownership.put(
|
|
||||||
format!(
|
|
||||||
"{}:{}",
|
|
||||||
file_path.display(),
|
|
||||||
VirtualBranchHunk::gen_id(hunk.new_start, hunk.new_lines)
|
|
||||||
)
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ownership
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let now_ms = crate::time::now_ms();
|
|
||||||
|
|
||||||
let (upstream, upstream_head) = if let git::Refname::Local(head_name) = &head_name {
|
|
||||||
let upstream_name = target_branch_ref.with_branch(head_name.branch());
|
|
||||||
if upstream_name.eq(target_branch_ref) {
|
|
||||||
(None, None)
|
|
||||||
} else {
|
|
||||||
match repo.find_reference(&git::Refname::from(&upstream_name).to_string()) {
|
|
||||||
Ok(upstream) => {
|
|
||||||
let head = upstream
|
|
||||||
.peel_to_commit()
|
|
||||||
.map(|commit| commit.id())
|
|
||||||
.context(format!(
|
|
||||||
"failed to peel upstream {} to commit",
|
|
||||||
upstream.name().unwrap()
|
|
||||||
))?;
|
|
||||||
Ok((Some(upstream_name), Some(head)))
|
|
||||||
}
|
|
||||||
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok((None, None)),
|
|
||||||
Err(error) => Err(error),
|
|
||||||
}
|
|
||||||
.context(format!("failed to find upstream for {}", head_name))?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let branch = branch::Branch {
|
|
||||||
id: BranchId::generate(),
|
|
||||||
name: head_name.to_string().replace("refs/heads/", ""),
|
|
||||||
notes: String::new(),
|
|
||||||
applied: true,
|
|
||||||
upstream,
|
|
||||||
upstream_head,
|
|
||||||
created_timestamp_ms: now_ms,
|
|
||||||
updated_timestamp_ms: now_ms,
|
|
||||||
head: current_head_commit.id(),
|
|
||||||
tree: super::write_tree_onto_commit(
|
|
||||||
project_repository,
|
|
||||||
current_head_commit.id(),
|
|
||||||
diff::diff_files_into_hunks(wd_diff),
|
|
||||||
)?,
|
|
||||||
ownership,
|
|
||||||
order: 0,
|
|
||||||
selected_for_changes: None,
|
|
||||||
allow_rebasing: project_repository.project().ok_with_force_push.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
vb_state.set_branch(branch)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set_exclude_decoration(project_repository)?;
|
|
||||||
|
|
||||||
update_gitbutler_integration(&vb_state, project_repository)?;
|
|
||||||
|
|
||||||
let base = target_to_base_branch(project_repository, &target)?;
|
|
||||||
Ok(base)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_target_push_remote(
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
push_remote_name: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let remote = project_repository
|
|
||||||
.repo()
|
|
||||||
.find_remote(push_remote_name)
|
|
||||||
.context(format!("failed to find remote {}", push_remote_name))?;
|
|
||||||
|
|
||||||
// if target exists, and it is the same as the requested branch, we should go back
|
|
||||||
let mut target = default_target(&project_repository.project().gb_dir())?;
|
|
||||||
target.push_remote_name = remote
|
|
||||||
.name()
|
|
||||||
.context("failed to get remote name")?
|
|
||||||
.to_string()
|
|
||||||
.into();
|
|
||||||
let vb_state = project_repository.project().virtual_branches();
|
|
||||||
vb_state.set_default_target(target)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_exclude_decoration(project_repository: &project_repository::Repository) -> Result<()> {
|
|
||||||
let repo = project_repository.repo();
|
|
||||||
let mut config = repo.config()?;
|
|
||||||
config
|
|
||||||
.set_multivar("log.excludeDecoration", "refs/gitbutler", "refs/gitbutler")
|
|
||||||
.context("failed to set log.excludeDecoration")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
|
|
||||||
println!("tree id: {}", tree.id());
|
|
||||||
for entry in tree {
|
|
||||||
println!(
|
|
||||||
" entry: {} {}",
|
|
||||||
entry.name().unwrap_or_default(),
|
|
||||||
entry.id()
|
|
||||||
);
|
|
||||||
// get entry contents
|
|
||||||
let object = entry.to_object(repo).context("failed to get object")?;
|
|
||||||
let blob = object.as_blob().context("failed to get blob")?;
|
|
||||||
// convert content to string
|
|
||||||
if let Ok(content) = std::str::from_utf8(blob.content()) {
|
|
||||||
println!(" blob: {}", content);
|
|
||||||
} else {
|
|
||||||
println!(" blob: BINARY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to update the target branch
|
|
||||||
// this means that we need to:
|
|
||||||
// determine if what the target branch is now pointing to is mergeable with our current working directory
|
|
||||||
// merge the target branch into our current working directory
|
|
||||||
// update the target sha
|
|
||||||
pub fn update_base_branch(
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
) -> anyhow::Result<Vec<git2::Branch<'_>>> {
|
|
||||||
project_repository.assure_resolved()?;
|
|
||||||
|
|
||||||
// look up the target and see if there is a new oid
|
|
||||||
let target = default_target(&project_repository.project().gb_dir())?;
|
|
||||||
let repo = project_repository.repo();
|
|
||||||
let target_branch = repo
|
|
||||||
.find_branch_by_refname(&target.branch.clone().into())
|
|
||||||
.context(format!("failed to find branch {}", target.branch))?;
|
|
||||||
|
|
||||||
let new_target_commit = target_branch
|
|
||||||
.ok_or(anyhow!("failed to get branch"))?
|
|
||||||
.get()
|
|
||||||
.peel_to_commit()
|
|
||||||
.context(format!("failed to peel branch {} to commit", target.branch))?;
|
|
||||||
|
|
||||||
let mut unapplied_branch_names: Vec<git2::Branch> = Vec::new();
|
|
||||||
|
|
||||||
if new_target_commit.id() == target.sha {
|
|
||||||
return Ok(unapplied_branch_names);
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_target_tree = new_target_commit
|
|
||||||
.tree()
|
|
||||||
.context("failed to get new target commit tree")?;
|
|
||||||
|
|
||||||
let old_target_tree = repo.find_commit(target.sha)?.tree().context(format!(
|
|
||||||
"failed to get old target commit tree {}",
|
|
||||||
target.sha
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let vb_state = project_repository.project().virtual_branches();
|
|
||||||
let integration_commit = get_workspace_head(&vb_state, project_repository)?;
|
|
||||||
|
|
||||||
// try to update every branch
|
|
||||||
let updated_vbranches =
|
|
||||||
super::get_status_by_branch(project_repository, Some(&integration_commit))?
|
|
||||||
.0
|
|
||||||
.into_iter()
|
|
||||||
.map(|(branch, _)| branch)
|
|
||||||
.map(
|
|
||||||
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
|
||||||
let branch_tree = repo.find_tree(branch.tree)?;
|
|
||||||
|
|
||||||
let branch_head_commit = repo.find_commit(branch.head).context(format!(
|
|
||||||
"failed to find commit {} for branch {}",
|
|
||||||
branch.head, branch.id
|
|
||||||
))?;
|
|
||||||
let branch_head_tree = branch_head_commit.tree().context(format!(
|
|
||||||
"failed to find tree for commit {} for branch {}",
|
|
||||||
branch.head, branch.id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let result_integrated_detected =
|
|
||||||
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
|
||||||
// branch head tree is the same as the new target tree.
|
|
||||||
// meaning we can safely use the new target commit as the branch head.
|
|
||||||
|
|
||||||
branch.head = new_target_commit.id();
|
|
||||||
|
|
||||||
// it also means that the branch is fully integrated into the target.
|
|
||||||
// disconnect it from the upstream
|
|
||||||
branch.upstream = None;
|
|
||||||
branch.upstream_head = None;
|
|
||||||
|
|
||||||
let non_commited_files = diff::trees(
|
|
||||||
project_repository.repo(),
|
|
||||||
&branch_head_tree,
|
|
||||||
&branch_tree,
|
|
||||||
)?;
|
|
||||||
if non_commited_files.is_empty() {
|
|
||||||
// if there are no commited files, then the branch is fully merged
|
|
||||||
// and we can delete it.
|
|
||||||
vb_state.remove_branch(branch.id)?;
|
|
||||||
project_repository.delete_branch_reference(&branch)?;
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
vb_state.set_branch(branch.clone())?;
|
|
||||||
Ok(Some(branch))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if branch_head_tree.id() == new_target_tree.id() {
|
|
||||||
return result_integrated_detected(branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to merge branch head with new target
|
|
||||||
let mut branch_tree_merge_index = repo
|
|
||||||
.merge_trees(&old_target_tree, &branch_tree, &new_target_tree, None)
|
|
||||||
.context(format!("failed to merge trees for branch {}", branch.id))?;
|
|
||||||
|
|
||||||
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(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
unapplied_branch_names.push(unapplied_real_branch);
|
|
||||||
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let branch_merge_index_tree_oid =
|
|
||||||
branch_tree_merge_index.write_tree_to(project_repository.repo())?;
|
|
||||||
|
|
||||||
if branch_merge_index_tree_oid == new_target_tree.id() {
|
|
||||||
return result_integrated_detected(branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch.head == target.sha {
|
|
||||||
// there are no commits on the branch, so we can just update the head to the new target and calculate the new tree
|
|
||||||
branch.head = new_target_commit.id();
|
|
||||||
branch.tree = branch_merge_index_tree_oid;
|
|
||||||
vb_state.set_branch(branch.clone())?;
|
|
||||||
return Ok(Some(branch));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut branch_head_merge_index = repo
|
|
||||||
.merge_trees(&old_target_tree, &branch_head_tree, &new_target_tree, None)
|
|
||||||
.context(format!(
|
|
||||||
"failed to merge head tree for branch {}",
|
|
||||||
branch.id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
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(),
|
|
||||||
)?;
|
|
||||||
unapplied_branch_names.push(unapplied_real_branch);
|
|
||||||
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// branch commits do not conflict with new target, so lets merge them
|
|
||||||
let branch_head_merge_tree_oid = branch_head_merge_index
|
|
||||||
.write_tree_to(project_repository.repo())
|
|
||||||
.context(format!(
|
|
||||||
"failed to write head merge index for {}",
|
|
||||||
branch.id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let ok_with_force_push = branch.allow_rebasing;
|
|
||||||
|
|
||||||
let result_merge =
|
|
||||||
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
|
||||||
// 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 branch_head_merge_tree = repo
|
|
||||||
.find_tree(branch_head_merge_tree_oid)
|
|
||||||
.context("failed to find tree")?;
|
|
||||||
|
|
||||||
let new_target_head = project_repository
|
|
||||||
.commit(
|
|
||||||
format!(
|
|
||||||
"Merged {}/{} into {}",
|
|
||||||
target.branch.remote(),
|
|
||||||
target.branch.branch(),
|
|
||||||
branch.name,
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
&branch_head_merge_tree,
|
|
||||||
&[&branch_head_commit, &new_target_commit],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.context("failed to commit merge")?;
|
|
||||||
|
|
||||||
branch.head = new_target_head;
|
|
||||||
branch.tree = branch_merge_index_tree_oid;
|
|
||||||
vb_state.set_branch(branch.clone())?;
|
|
||||||
Ok(Some(branch))
|
|
||||||
};
|
|
||||||
|
|
||||||
if branch.upstream.is_some() && !ok_with_force_push {
|
|
||||||
return result_merge(branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// branch was not pushed to upstream yet. attempt a rebase,
|
|
||||||
let rebased_head_oid = cherry_rebase(
|
|
||||||
project_repository,
|
|
||||||
new_target_commit.id(),
|
|
||||||
new_target_commit.id(),
|
|
||||||
branch.head,
|
|
||||||
);
|
|
||||||
|
|
||||||
// rebase failed, just do the merge
|
|
||||||
if rebased_head_oid.is_err() {
|
|
||||||
return result_merge(branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(rebased_head_oid) = rebased_head_oid? {
|
|
||||||
// rebase worked out, rewrite the branch head
|
|
||||||
branch.head = rebased_head_oid;
|
|
||||||
branch.tree = branch_merge_index_tree_oid;
|
|
||||||
vb_state.set_branch(branch.clone())?;
|
|
||||||
return Ok(Some(branch));
|
|
||||||
}
|
|
||||||
|
|
||||||
result_merge(branch)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect::<Result<Vec<_>>>()?
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// ok, now all the problematic branches have been unapplied
|
|
||||||
// now we calculate and checkout new tree for the working directory
|
|
||||||
|
|
||||||
let final_tree = updated_vbranches
|
|
||||||
.iter()
|
|
||||||
.filter(|branch| branch.applied)
|
|
||||||
.fold(new_target_commit.tree(), |final_tree, branch| {
|
|
||||||
let repo: &git2::Repository = repo;
|
|
||||||
let final_tree = final_tree?;
|
|
||||||
let branch_tree = repo.find_tree(branch.tree)?;
|
|
||||||
let mut merge_result: Index =
|
|
||||||
repo.merge_trees(&new_target_tree, &final_tree, &branch_tree, None)?;
|
|
||||||
let final_tree_oid = merge_result.write_tree_to(repo)?;
|
|
||||||
repo.find_tree(final_tree_oid)
|
|
||||||
})
|
|
||||||
.context("failed to calculate final tree")?;
|
|
||||||
|
|
||||||
repo.checkout_tree_builder(&final_tree)
|
|
||||||
.force()
|
|
||||||
.checkout()
|
|
||||||
.context("failed to checkout index, this should not have happened, we should have already detected this")?;
|
|
||||||
|
|
||||||
// write new target oid
|
|
||||||
vb_state.set_default_target(target::Target {
|
|
||||||
sha: new_target_commit.id(),
|
|
||||||
..target
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Rewriting the integration commit is necessary after changing target sha.
|
|
||||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
|
||||||
Ok(unapplied_branch_names)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn target_to_base_branch(
|
|
||||||
project_repository: &project_repository::Repository,
|
|
||||||
target: &target::Target,
|
|
||||||
) -> Result<super::BaseBranch> {
|
|
||||||
let repo = project_repository.repo();
|
|
||||||
let branch = repo
|
|
||||||
.find_branch_by_refname(&target.branch.clone().into())?
|
|
||||||
.ok_or(anyhow!("failed to get branch"))?;
|
|
||||||
let commit = branch.get().peel_to_commit()?;
|
|
||||||
let oid = commit.id();
|
|
||||||
|
|
||||||
// gather a list of commits between oid and target.sha
|
|
||||||
let upstream_commits = project_repository
|
|
||||||
.log(oid, project_repository::LogUntil::Commit(target.sha))
|
|
||||||
.context("failed to get upstream commits")?
|
|
||||||
.iter()
|
|
||||||
.map(super::commit_to_remote_commit)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// get some recent commits
|
|
||||||
let recent_commits = project_repository
|
|
||||||
.log(target.sha, LogUntil::Take(20))
|
|
||||||
.context("failed to get recent commits")?
|
|
||||||
.iter()
|
|
||||||
.map(super::commit_to_remote_commit)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// there has got to be a better way to do this.
|
|
||||||
let push_remote_url = match target.push_remote_name {
|
|
||||||
Some(ref name) => match repo.find_remote(name) {
|
|
||||||
Ok(remote) => match remote.url() {
|
|
||||||
Some(url) => url.to_string(),
|
|
||||||
None => target.remote_url.clone(),
|
|
||||||
},
|
|
||||||
Err(_err) => target.remote_url.clone(),
|
|
||||||
},
|
|
||||||
None => target.remote_url.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let base = super::BaseBranch {
|
|
||||||
branch_name: format!("{}/{}", target.branch.remote(), target.branch.branch()),
|
|
||||||
remote_name: target.branch.remote().to_string(),
|
|
||||||
remote_url: target.remote_url.clone(),
|
|
||||||
push_remote_name: target.push_remote_name.clone(),
|
|
||||||
push_remote_url,
|
|
||||||
base_sha: target.sha,
|
|
||||||
current_sha: oid,
|
|
||||||
behind: upstream_commits.len(),
|
|
||||||
upstream_commits,
|
|
||||||
recent_commits,
|
|
||||||
last_fetched_ms: project_repository
|
|
||||||
.project()
|
|
||||||
.project_data_last_fetch
|
|
||||||
.as_ref()
|
|
||||||
.map(FetchResult::timestamp)
|
|
||||||
.copied()
|
|
||||||
.map(|t| t.duration_since(time::UNIX_EPOCH).unwrap().as_millis()),
|
|
||||||
};
|
|
||||||
Ok(base)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_target(base_path: &Path) -> Result<target::Target> {
|
|
||||||
VirtualBranchesHandle::new(base_path).get_default_target()
|
|
||||||
}
|
|
@ -5,11 +5,6 @@ pub mod target;
|
|||||||
mod files;
|
mod files;
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
|
|
||||||
pub mod integration;
|
|
||||||
|
|
||||||
mod r#virtual;
|
|
||||||
pub use r#virtual::*;
|
|
||||||
|
|
||||||
mod remote;
|
mod remote;
|
||||||
pub use remote::*;
|
pub use remote::*;
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -18,3 +18,4 @@ tempfile = "3.10.1"
|
|||||||
keyring.workspace = true
|
keyring.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
gitbutler-core = { path = "../gitbutler-core" }
|
gitbutler-core = { path = "../gitbutler-core" }
|
||||||
|
gitbutler-branch = { path = "../gitbutler-branch" }
|
||||||
|
@ -42,7 +42,7 @@ pub mod virtual_branches {
|
|||||||
})
|
})
|
||||||
.expect("failed to write target");
|
.expect("failed to write target");
|
||||||
|
|
||||||
virtual_branches::integration::update_gitbutler_integration(&vb_state, project_repository)
|
gitbutler_branch::integration::update_gitbutler_integration(&vb_state, project_repository)
|
||||||
.expect("failed to update integration");
|
.expect("failed to update integration");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
Loading…
Reference in New Issue
Block a user