move virtual, integration and base modules to gitbutler-branch crate

This commit is contained in:
Kiril Videlov 2024-07-07 17:30:18 +02:00
parent f224207029
commit 9dc82e8fe9
No known key found for this signature in database
GPG Key ID: A4C733025427C471
14 changed files with 2353 additions and 6796 deletions

1
Cargo.lock generated
View File

@ -2270,6 +2270,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"git2",
"gitbutler-branch",
"gitbutler-core",
"keyring",
"once_cell",

View File

@ -4,12 +4,12 @@ use anyhow::{anyhow, Context, Result};
use git2::Index;
use serde::Serialize;
use gitbutler_core::virtual_branches::integration::{
get_workspace_head, update_gitbutler_integration,
};
use super::r#virtual as vb;
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::{
branch, convert_to_real_branch, target, BranchId, RemoteCommit, VirtualBranchHunk,
VirtualBranchesHandle, GITBUTLER_INTEGRATION_REFERENCE,
branch, target, BranchId, RemoteCommit, VirtualBranchesHandle, GITBUTLER_INTEGRATION_REFERENCE,
};
use gitbutler_core::{error::Marker, git::RepositoryExt, rebase::cherry_rebase};
use gitbutler_core::{
@ -246,7 +246,7 @@ pub fn set_base_branch(
created_timestamp_ms: now_ms,
updated_timestamp_ms: now_ms,
head: current_head_commit.id(),
tree: gitbutler_core::virtual_branches::write_tree_onto_commit(
tree: vb::write_tree_onto_commit(
project_repository,
current_head_commit.id(),
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)?;
// try to update every branch
let updated_vbranches = gitbutler_core::virtual_branches::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 =
let updated_vbranches =
vb::get_status_by_branch(project_repository, Some(&integration_commit))?
.0
.into_iter()
.map(|(branch, _)| branch)
.map(
|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.
let branch_tree = repo.find_tree(branch.tree)?;
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.
// disconnect it from the upstream
branch.upstream = None;
branch.upstream_head = None;
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.
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))
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);
}
};
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))?;
// 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(),
)?;
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);
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 =
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_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));
}
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
))?;
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);
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);
}
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 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 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>> {
// 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")?;
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))
};
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);
}
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,
);
// 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);
}
// 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));
}
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<_>>();
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
@ -560,10 +568,7 @@ pub fn update_base_branch(
})?;
// Rewriting the integration commit is necessary after changing target sha.
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(unapplied_branch_names)
}

View File

@ -155,8 +155,7 @@ impl Controller {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::MergeUpstream));
virtual_branches::integrate_upstream_commits(&project_repository, branch_id)
.map_err(Into::into)
branch::integrate_upstream_commits(&project_repository, branch_id).map_err(Into::into)
}
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> {
let project_repository = Repository::open(project)?;
virtual_branches::integration::verify_branch(&project_repository)?;
crate::integration::verify_branch(&project_repository)?;
Ok(project_repository)
}

View File

@ -3,13 +3,13 @@ use std::{path::PathBuf, vec};
use anyhow::{anyhow, bail, Context, Result};
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,
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME, GITBUTLER_INTEGRATION_REFERENCE,
};
use crate::error::Marker;
use crate::git::RepositoryExt;
use crate::{
use gitbutler_core::{
git::CommitExt,
project_repository::{self, conflicts, LogUntil},
virtual_branches::branch::BranchCreateRequest,
@ -297,7 +297,13 @@ pub fn verify_branch(project_repository: &project_repository::Repository) -> Res
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> {
match self.get_head().context("failed to get head")?.name() {
Some(refname) if *refname == GITBUTLER_INTEGRATION_REFERENCE.to_string() => Ok(self),

View File

@ -8,3 +8,5 @@ pub use r#virtual::*;
pub mod assets;
pub mod base;
pub mod integration;

View File

@ -18,6 +18,7 @@ use gitbutler_core::virtual_branches::Author;
use hex::ToHex;
use serde::{Deserialize, Serialize};
use crate::integration::{get_integration_commiter, get_workspace_head};
use gitbutler_core::error::Code;
use gitbutler_core::error::Marker;
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::time::now_since_unix_epoch_ms;
use gitbutler_core::virtual_branches::branch::HunkHash;
use gitbutler_core::virtual_branches::integration::{get_integration_commiter, get_workspace_head};
use gitbutler_core::virtual_branches::{
branch::{
self, Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims, Hunk, OwnershipClaim,
@ -299,10 +299,7 @@ pub fn unapply_ownership(
.checkout()
.context("failed to checkout tree")?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(())
}
@ -452,10 +449,7 @@ pub fn convert_to_real_branch(
// Ensure we still have a default target
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(real_branch)
}
@ -489,10 +483,8 @@ pub fn list_virtual_branches(
.get_default_target()
.context("failed to get default target")?;
let integration_commit_id = gitbutler_core::virtual_branches::integration::get_workspace_head(
&vb_state,
project_repository,
)?;
let integration_commit_id =
crate::integration::get_workspace_head(&vb_state, project_repository)?;
let integration_commit = project_repository
.repo()
.find_commit(integration_commit_id)
@ -1000,10 +992,7 @@ pub fn integrate_upstream_commits(
.checkout()?;
};
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(())
}
@ -1859,11 +1848,8 @@ pub fn reset_branch(
.set_branch(branch)
.context("failed to write branch")?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
Ok(())
}
@ -2164,11 +2150,8 @@ pub fn commit(
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
vb_state.set_branch(branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
Ok(commit_oid)
}
@ -2569,10 +2552,7 @@ pub fn move_commit_file(
if upstream_commits.is_empty() {
target_branch.head = commit_oid;
vb_state.set_branch(target_branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
return Ok(commit_oid);
}
@ -2589,10 +2569,7 @@ pub fn move_commit_file(
if let Some(new_head) = new_head {
target_branch.head = new_head;
vb_state.set_branch(target_branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(commit_oid)
} else {
Err(anyhow!("rebase failed"))
@ -2630,10 +2607,8 @@ pub fn amend(
let default_target = vb_state.get_default_target()?;
let integration_commit_id = gitbutler_core::virtual_branches::integration::get_workspace_head(
&vb_state,
project_repository,
)?;
let integration_commit_id =
crate::integration::get_workspace_head(&vb_state, project_repository)?;
let (mut applied_statuses, _) = get_applied_status(
project_repository,
@ -2729,10 +2704,7 @@ pub fn amend(
if upstream_commits.is_empty() {
target_branch.head = commit_oid;
vb_state.set_branch(target_branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
return Ok(commit_oid);
}
@ -2748,10 +2720,7 @@ pub fn amend(
if let Some(new_head) = new_head {
target_branch.head = new_head;
vb_state.set_branch(target_branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(commit_oid)
} else {
Err(anyhow!("rebase failed"))
@ -2806,11 +2775,8 @@ pub fn reorder_commit(
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
vb_state.set_branch(branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
} else {
// move commit down
if default_target.sha == parent_oid {
@ -2846,11 +2812,8 @@ pub fn reorder_commit(
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
vb_state.set_branch(branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
}
Ok(())
@ -2884,11 +2847,8 @@ pub fn insert_blank_commit(
if commit.id() == branch.head && offset < 0 {
// inserting before the first commit
branch.head = blank_commit_oid;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
} else {
// rebase all commits above it onto the new commit
match cherry_rebase(
@ -2899,11 +2859,8 @@ pub fn insert_blank_commit(
) {
Ok(Some(new_head)) => {
branch.head = new_head;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
}
Ok(None) => bail!("no rebase happened"),
Err(err) => {
@ -2962,11 +2919,8 @@ pub fn undo_commit(
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
vb_state.set_branch(branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
}
Ok(())
@ -3060,11 +3014,8 @@ pub fn squash(
branch.updated_timestamp_ms = gitbutler_core::time::now_ms();
vb_state.set_branch(branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
Ok(())
}
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();
vb_state.set_branch(branch.clone())?;
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
Ok(())
}
@ -3177,10 +3125,8 @@ pub fn move_commit(
let default_target = vb_state.get_default_target()?;
let integration_commit_id = gitbutler_core::virtual_branches::integration::get_workspace_head(
&vb_state,
project_repository,
)?;
let integration_commit_id =
crate::integration::get_workspace_head(&vb_state, project_repository)?;
let (mut applied_statuses, _) = get_applied_status(
project_repository,
@ -3292,11 +3238,8 @@ pub fn move_commit(
vb_state.set_branch(destination_branch.clone())?;
}
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)
.context("failed to update gitbutler integration")?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)
.context("failed to update gitbutler integration")?;
Ok(())
}
@ -3531,10 +3474,7 @@ pub fn create_virtual_branch_from_branch(
}
}
gitbutler_core::virtual_branches::integration::update_gitbutler_integration(
&vb_state,
project_repository,
)?;
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(branch.name)
}

View File

@ -1 +1,3 @@
mod virtual_branches;
mod extra;

File diff suppressed because it is too large Load Diff

View File

@ -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, &current_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()
}

View File

@ -5,11 +5,6 @@ pub mod target;
mod files;
pub use files::*;
pub mod integration;
mod r#virtual;
pub use r#virtual::*;
mod remote;
pub use remote::*;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -18,3 +18,4 @@ tempfile = "3.10.1"
keyring.workspace = true
serde_json = "1.0"
gitbutler-core = { path = "../gitbutler-core" }
gitbutler-branch = { path = "../gitbutler-branch" }

View File

@ -42,7 +42,7 @@ pub mod virtual_branches {
})
.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");
Ok(())