mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-18 06:22:28 +03:00
Blame across workspace instead of by branch
- refactors `update_gitbutler_integration` - creates new `get_workspace_head` function - ignore locking when resolving merge conflicts
This commit is contained in:
parent
2f7b396ab8
commit
811f0ee35b
@ -362,197 +362,193 @@ pub fn update_base_branch(
|
||||
let vb_state = VirtualBranchesHandle::new(&project_repository.project().gb_dir());
|
||||
|
||||
// try to update every branch
|
||||
let updated_vbranches =
|
||||
super::get_status_by_branch(project_repository, Some(&new_target_commit.id()))?
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(branch, _)| branch)
|
||||
.map(
|
||||
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
|
||||
let branch_tree = repo.find_tree(branch.tree)?;
|
||||
let updated_vbranches = super::get_status_by_branch(project_repository, None)?
|
||||
.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 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.
|
||||
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.git_repository,
|
||||
&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)
|
||||
.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.
|
||||
branch.applied = false;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
let branch_merge_index_tree_oid =
|
||||
branch_tree_merge_index.write_tree_to(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)
|
||||
.context(format!(
|
||||
"failed to merge head tree for branch {}",
|
||||
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;
|
||||
|
||||
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.
|
||||
branch.applied = false;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
// 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(repo)
|
||||
.context(format!(
|
||||
"failed to write head merge index for {}",
|
||||
branch.id
|
||||
))?;
|
||||
|
||||
let ok_with_force_push = project_repository.project().ok_with_force_push;
|
||||
|
||||
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(
|
||||
user,
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
target.branch.remote(),
|
||||
target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&branch_head_merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
branch.head = new_target_head;
|
||||
branch.tree = branch_merge_index_tree_oid;
|
||||
let non_commited_files = diff::trees(
|
||||
&project_repository.git_repository,
|
||||
&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.upstream.is_some() && !ok_with_force_push {
|
||||
return result_merge(branch);
|
||||
}
|
||||
if branch_head_tree.id() == new_target_tree.id() {
|
||||
return result_integrated_detected(branch);
|
||||
}
|
||||
|
||||
// branch was not pushed to upstream yet. attempt a rebase,
|
||||
let (_, committer) = project_repository.git_signatures(user)?;
|
||||
let mut rebase_options = git2::RebaseOptions::new();
|
||||
rebase_options.quiet(true);
|
||||
rebase_options.inmemory(true);
|
||||
let mut rebase = repo
|
||||
.rebase(
|
||||
Some(branch.head),
|
||||
Some(new_target_commit.id()),
|
||||
None,
|
||||
Some(&mut rebase_options),
|
||||
// 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)
|
||||
.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.
|
||||
branch.applied = false;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
let branch_merge_index_tree_oid = branch_tree_merge_index.write_tree_to(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)
|
||||
.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.
|
||||
branch.applied = false;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
|
||||
// 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(repo)
|
||||
.context(format!(
|
||||
"failed to write head merge index for {}",
|
||||
branch.id
|
||||
))?;
|
||||
|
||||
let ok_with_force_push = project_repository.project().ok_with_force_push;
|
||||
|
||||
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(
|
||||
user,
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
target.branch.remote(),
|
||||
target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&branch_head_merge_tree,
|
||||
&[&branch_head_commit, &new_target_commit],
|
||||
signing_key,
|
||||
)
|
||||
.context("failed to rebase")?;
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
let mut rebase_success = true;
|
||||
// check to see if these commits have already been pushed
|
||||
let mut last_rebase_head = branch.head;
|
||||
while rebase.next().is_some() {
|
||||
let index = rebase
|
||||
.inmemory_index()
|
||||
.context("failed to get inmemory index")?;
|
||||
if index.has_conflicts() {
|
||||
rebase_success = false;
|
||||
break;
|
||||
}
|
||||
branch.head = new_target_head;
|
||||
branch.tree = branch_merge_index_tree_oid;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
Ok(Some(branch))
|
||||
};
|
||||
|
||||
if let Ok(commit_id) = rebase.commit(None, &committer.clone().into(), None)
|
||||
{
|
||||
last_rebase_head = commit_id.into();
|
||||
} else {
|
||||
rebase_success = false;
|
||||
break;
|
||||
}
|
||||
if branch.upstream.is_some() && !ok_with_force_push {
|
||||
return result_merge(branch);
|
||||
}
|
||||
|
||||
// branch was not pushed to upstream yet. attempt a rebase,
|
||||
let (_, committer) = project_repository.git_signatures(user)?;
|
||||
let mut rebase_options = git2::RebaseOptions::new();
|
||||
rebase_options.quiet(true);
|
||||
rebase_options.inmemory(true);
|
||||
let mut rebase = repo
|
||||
.rebase(
|
||||
Some(branch.head),
|
||||
Some(new_target_commit.id()),
|
||||
None,
|
||||
Some(&mut rebase_options),
|
||||
)
|
||||
.context("failed to rebase")?;
|
||||
|
||||
let mut rebase_success = true;
|
||||
// check to see if these commits have already been pushed
|
||||
let mut last_rebase_head = branch.head;
|
||||
while rebase.next().is_some() {
|
||||
let index = rebase
|
||||
.inmemory_index()
|
||||
.context("failed to get inmemory index")?;
|
||||
if index.has_conflicts() {
|
||||
rebase_success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if rebase_success {
|
||||
// rebase worked out, rewrite the branch head
|
||||
rebase.finish(None).context("failed to finish rebase")?;
|
||||
branch.head = last_rebase_head;
|
||||
branch.tree = branch_merge_index_tree_oid;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
return Ok(Some(branch));
|
||||
if let Ok(commit_id) = rebase.commit(None, &committer.clone().into(), None) {
|
||||
last_rebase_head = commit_id.into();
|
||||
} else {
|
||||
rebase_success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// rebase failed, do a merge commit
|
||||
rebase.abort().context("failed to abort rebase")?;
|
||||
if rebase_success {
|
||||
// rebase worked out, rewrite the branch head
|
||||
rebase.finish(None).context("failed to finish rebase")?;
|
||||
branch.head = last_rebase_head;
|
||||
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<_>>();
|
||||
// rebase failed, do a merge commit
|
||||
rebase.abort().context("failed to abort rebase")?;
|
||||
|
||||
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
|
||||
|
@ -3,9 +3,7 @@ use std::{fmt::Display, ops::RangeInclusive, str::FromStr};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::{BStr, ByteSlice};
|
||||
|
||||
use crate::{git::diff, id::Id};
|
||||
|
||||
use super::Branch;
|
||||
use crate::git::{self, diff};
|
||||
|
||||
pub type HunkHash = md5::Digest;
|
||||
|
||||
@ -15,7 +13,7 @@ pub struct Hunk {
|
||||
pub timestamp_ms: Option<u128>,
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
pub locked_to: Vec<Id<Branch>>,
|
||||
pub locked_to: Vec<git::Oid>,
|
||||
}
|
||||
|
||||
impl From<&diff::GitHunk> for Hunk {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
@ -16,9 +16,112 @@ lazy_static! {
|
||||
git::LocalRefname::new("gitbutler/integration", None);
|
||||
}
|
||||
|
||||
const WORKSPACE_HEAD: &str = "Workspace Head";
|
||||
const GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME: &str = "GitButler";
|
||||
const GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL: &str = "gitbutler@gitbutler.com";
|
||||
|
||||
fn get_committer<'a>() -> Result<git::Signature<'a>> {
|
||||
Ok(git::Signature::now(
|
||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
|
||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
|
||||
)?)
|
||||
}
|
||||
|
||||
// Creates and returns a merge commit of all active branch heads.
|
||||
//
|
||||
// This is the base against which we diff the working directory to understand
|
||||
// what files have been modified.
|
||||
pub fn get_workspace_head(
|
||||
vb_state: &VirtualBranchesHandle,
|
||||
project_repository: &project_repository::Repository,
|
||||
) -> Result<git::Oid> {
|
||||
let target = vb_state
|
||||
.get_default_target()
|
||||
.context("failed to get target")?;
|
||||
let repo = &project_repository.git_repository;
|
||||
let vb_state = VirtualBranchesHandle::new(&project_repository.project().gb_dir());
|
||||
|
||||
let all_virtual_branches = vb_state.list_branches()?;
|
||||
let applied_virtual_branches = all_virtual_branches
|
||||
.iter()
|
||||
.filter(|branch| branch.applied)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let target_commit = repo.find_commit(target.sha)?;
|
||||
let target_tree = target_commit.tree()?;
|
||||
let mut workspace_tree = target_commit.tree()?;
|
||||
|
||||
// Merge applied branches into one `workspace_tree``.
|
||||
for branch in &applied_virtual_branches {
|
||||
let branch_head = repo.find_commit(branch.head)?;
|
||||
let branch_tree = branch_head.tree()?;
|
||||
|
||||
if let Ok(mut result) = repo.merge_trees(&target_tree, &workspace_tree, &branch_tree) {
|
||||
if !result.has_conflicts() {
|
||||
let final_tree_oid = result.write_tree_to(repo)?;
|
||||
workspace_tree = repo.find_tree(final_tree_oid)?;
|
||||
} else {
|
||||
// TODO: Create error type and provide context.
|
||||
return Err(anyhow!("Unexpected merge conflict"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let branch_heads = applied_virtual_branches
|
||||
.iter()
|
||||
.map(|b| repo.find_commit(b.head))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let branch_head_refs = branch_heads.iter().collect::<Vec<_>>();
|
||||
|
||||
// If no branches are applied then the workspace head is the target.
|
||||
if branch_head_refs.is_empty() {
|
||||
return Ok(target_commit.id());
|
||||
}
|
||||
|
||||
// TODO(mg): Can we make this a constant?
|
||||
let committer = get_committer()?;
|
||||
|
||||
// Create merge commit of branch heads.
|
||||
let workspace_head_id = repo.commit(
|
||||
None,
|
||||
&committer,
|
||||
&committer,
|
||||
WORKSPACE_HEAD,
|
||||
&workspace_tree,
|
||||
branch_head_refs.as_slice(),
|
||||
)?;
|
||||
Ok(workspace_head_id)
|
||||
}
|
||||
|
||||
// Before switching the user to our gitbutler integration branch we save
|
||||
// the current branch into a text file. It is used in generating the commit
|
||||
// message for integration branch, as a helpful hint about how to get back
|
||||
// to where you were.
|
||||
struct PreviousHead {
|
||||
head: String,
|
||||
sha: String,
|
||||
}
|
||||
|
||||
fn read_integration_file(path: &PathBuf) -> Result<Option<PreviousHead>> {
|
||||
if let Ok(prev_data) = std::fs::read_to_string(path) {
|
||||
let parts: Vec<&str> = prev_data.split(':').collect();
|
||||
let prev_head = parts[0].to_string();
|
||||
let prev_sha = parts[1].to_string();
|
||||
Ok(Some(PreviousHead {
|
||||
head: prev_head,
|
||||
sha: prev_sha,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_integration_file(head: &git::Reference, path: PathBuf) -> Result<()> {
|
||||
let sha = head.target().unwrap().to_string();
|
||||
std::fs::write(path, format!(":{}", sha))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_gitbutler_integration(
|
||||
vb_state: &VirtualBranchesHandle,
|
||||
project_repository: &project_repository::Repository,
|
||||
@ -41,27 +144,19 @@ pub fn update_gitbutler_integration(
|
||||
let target_commit = repo.find_commit(target.sha)?;
|
||||
|
||||
// get current repo head for reference
|
||||
let head = repo.head()?;
|
||||
let mut prev_head = head.name().unwrap().to_string();
|
||||
let mut prev_sha = head.target().unwrap().to_string();
|
||||
let integration_file = repo.path().join("integration");
|
||||
if prev_head == GITBUTLER_INTEGRATION_REFERENCE.to_string() {
|
||||
// read the .git/integration file
|
||||
if let Ok(mut integration_file) = std::fs::File::open(integration_file) {
|
||||
let mut prev_data = String::new();
|
||||
integration_file.read_to_string(&mut prev_data)?;
|
||||
let parts: Vec<&str> = prev_data.split(':').collect();
|
||||
|
||||
prev_head = parts[0].to_string();
|
||||
prev_sha = parts[1].to_string();
|
||||
let head_ref = repo.head()?;
|
||||
let integration_filepath = repo.path().join("integration");
|
||||
let mut prev_branch = read_integration_file(&integration_filepath)?;
|
||||
if let Some(branch) = &prev_branch {
|
||||
if branch.head != GITBUTLER_INTEGRATION_REFERENCE.to_string() {
|
||||
// we are moving from a regular branch to our gitbutler integration branch, write a file to
|
||||
// .git/integration with the previous head and name
|
||||
write_integration_file(&head_ref, integration_filepath)?;
|
||||
prev_branch = Some(PreviousHead {
|
||||
head: head_ref.target().unwrap().to_string(),
|
||||
sha: head_ref.target().unwrap().to_string(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// we are moving from a regular branch to our gitbutler integration branch, save the original
|
||||
// write a file to .git/integration with the previous head and name
|
||||
let mut file = std::fs::File::create(integration_file)?;
|
||||
prev_head.push(':');
|
||||
prev_head.push_str(&prev_sha);
|
||||
file.write_all(prev_head.as_bytes())?;
|
||||
}
|
||||
|
||||
// commit index to temp head for the merge
|
||||
@ -80,19 +175,9 @@ pub fn update_gitbutler_integration(
|
||||
.filter(|branch| branch.applied)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let base_tree = target_commit.tree()?;
|
||||
let mut final_tree = target_commit.tree()?;
|
||||
for branch in &applied_virtual_branches {
|
||||
// merge this branches tree with our tree
|
||||
let branch_head = repo.find_commit(branch.head)?;
|
||||
let branch_tree = branch_head.tree()?;
|
||||
if let Ok(mut result) = repo.merge_trees(&base_tree, &final_tree, &branch_tree) {
|
||||
if !result.has_conflicts() {
|
||||
let final_tree_oid = result.write_tree_to(repo)?;
|
||||
final_tree = repo.find_tree(final_tree_oid)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
|
||||
let integration_commit = repo.find_commit(integration_commit_id).unwrap();
|
||||
let integration_tree = integration_commit.tree()?;
|
||||
|
||||
// message that says how to get back to where they were
|
||||
let mut message = "GitButler Integration Commit".to_string();
|
||||
@ -125,32 +210,31 @@ pub fn update_gitbutler_integration(
|
||||
message.push('\n');
|
||||
}
|
||||
}
|
||||
message.push_str("\nYour previous branch was: ");
|
||||
message.push_str(&prev_head);
|
||||
message.push_str("\n\n");
|
||||
message.push_str("The sha for that commit was: ");
|
||||
message.push_str(&prev_sha);
|
||||
message.push_str("\n\n");
|
||||
if let Some(prev_branch) = prev_branch {
|
||||
message.push_str("\nYour previous branch was: ");
|
||||
message.push_str(&prev_branch.head);
|
||||
message.push_str("\n\n");
|
||||
message.push_str("The sha for that commit was: ");
|
||||
message.push_str(&prev_branch.sha);
|
||||
message.push_str("\n\n");
|
||||
}
|
||||
message.push_str("For more information about what we're doing here, check out our docs:\n");
|
||||
message.push_str("https://docs.gitbutler.com/features/virtual-branches/integration-branch\n");
|
||||
|
||||
let committer = git::Signature::now(
|
||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
|
||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
|
||||
)?;
|
||||
let committer = get_committer()?;
|
||||
|
||||
let final_commit = repo.commit(
|
||||
Some(&"refs/heads/gitbutler/integration".parse().unwrap()),
|
||||
&committer,
|
||||
&committer,
|
||||
&message,
|
||||
&final_tree,
|
||||
&integration_commit.tree()?,
|
||||
&[&target_commit],
|
||||
)?;
|
||||
|
||||
// write final_tree as the current index
|
||||
let mut index = repo.index()?;
|
||||
index.read_tree(&final_tree)?;
|
||||
index.read_tree(&integration_tree)?;
|
||||
index.write()?;
|
||||
|
||||
// finally, update the refs/gitbutler/ heads to the states of the current virtual branches
|
||||
|
@ -497,12 +497,12 @@ pub fn unapply_ownership(
|
||||
.filter(|b| b.applied)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let integration_commit =
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let integration_commit_id =
|
||||
super::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||
|
||||
let (applied_statuses, _) = get_applied_status(
|
||||
project_repository,
|
||||
&integration_commit,
|
||||
&integration_commit_id,
|
||||
&default_target.sha,
|
||||
applied_branches,
|
||||
)
|
||||
@ -551,7 +551,7 @@ pub fn unapply_ownership(
|
||||
let repo = &project_repository.git_repository;
|
||||
|
||||
let target_commit = repo
|
||||
.find_commit(integration_commit)
|
||||
.find_commit(integration_commit_id)
|
||||
.context("failed to find target commit")?;
|
||||
|
||||
let base_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
@ -559,7 +559,7 @@ pub fn unapply_ownership(
|
||||
target_commit.tree().context("failed to get target tree"),
|
||||
|final_tree, status| {
|
||||
let final_tree = final_tree?;
|
||||
let tree_oid = write_tree(project_repository, &integration_commit, &status.1)?;
|
||||
let tree_oid = write_tree(project_repository, &integration_commit_id, &status.1)?;
|
||||
let branch_tree = repo.find_tree(tree_oid)?;
|
||||
let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree)?;
|
||||
let final_tree_oid = result.write_tree_to(repo)?;
|
||||
@ -781,11 +781,17 @@ pub fn list_virtual_branches(
|
||||
.get_default_target()
|
||||
.context("failed to get default target")?;
|
||||
|
||||
let integration_commit =
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let integration_commit_id =
|
||||
super::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||
let integration_commit = project_repository
|
||||
.git_repository
|
||||
.find_commit(integration_commit_id)
|
||||
.unwrap();
|
||||
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
|
||||
let (statuses, skipped_files) =
|
||||
get_status_by_branch(project_repository, Some(&integration_commit))?;
|
||||
get_status_by_branch(project_repository, Some(&integration_commit.id()))?;
|
||||
let max_selected_for_changes = statuses
|
||||
.iter()
|
||||
.filter_map(|(branch, _)| branch.selected_for_changes)
|
||||
@ -1661,7 +1667,7 @@ pub type BranchStatus = HashMap<PathBuf, Vec<diff::GitHunk>>;
|
||||
pub fn get_status_by_branch(
|
||||
project_repository: &project_repository::Repository,
|
||||
integration_commit: Option<&git::Oid>,
|
||||
) -> Result<(Vec<(branch::Branch, BranchStatus)>, Vec<diff::FileDiff>)> {
|
||||
) -> Result<(AppliedStatuses, Vec<diff::FileDiff>)> {
|
||||
let vb_state = VirtualBranchesHandle::new(&project_repository.project().gb_dir());
|
||||
|
||||
let default_target =
|
||||
@ -1740,10 +1746,8 @@ fn get_non_applied_status(
|
||||
.collect::<Result<Vec<_>>>()
|
||||
}
|
||||
|
||||
// given a list of applied virtual branches, return the status of each file, comparing the default target with
|
||||
// the working directory
|
||||
//
|
||||
// ownerships are updated if nessessary
|
||||
// Returns branches and their associated file changes, in addition to a list
|
||||
// of skipped files.
|
||||
fn get_applied_status(
|
||||
project_repository: &project_repository::Repository,
|
||||
integration_commit: &git::Oid,
|
||||
@ -1765,7 +1769,6 @@ fn get_applied_status(
|
||||
virtual_branches.sort_by(|a, b| a.order.cmp(&b.order));
|
||||
|
||||
if virtual_branches.is_empty() && !base_diffs.is_empty() {
|
||||
// no virtual branches, but hunks: create default branch
|
||||
virtual_branches =
|
||||
vec![
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
@ -1773,22 +1776,43 @@ fn get_applied_status(
|
||||
];
|
||||
}
|
||||
|
||||
// align branch ownership to the real hunks:
|
||||
// - update shifted hunks
|
||||
// - remove non existent hunks
|
||||
|
||||
let mut diffs_by_branch: HashMap<BranchId, BranchStatus> = virtual_branches
|
||||
.iter()
|
||||
.map(|branch| (branch.id, HashMap::new()))
|
||||
.collect();
|
||||
|
||||
let mut mtimes = MTimeCache::default();
|
||||
let mut git_hunk_map = HashMap::new();
|
||||
let mut locked_hunk_map = HashMap::<HunkHash, Vec<git::Oid>>::new();
|
||||
|
||||
for branch in &virtual_branches {
|
||||
if !branch.applied {
|
||||
bail!("branch {} is not applied", branch.name);
|
||||
}
|
||||
let merge_base = project_repository
|
||||
.git_repository
|
||||
.merge_base(*target_sha, *integration_commit)?;
|
||||
|
||||
// The merge base between the integration commit and target _is_ the
|
||||
// target, unless you are resolving merge conflicts. If that's the case
|
||||
// we ignore locks until we are back to normal mode.
|
||||
//
|
||||
// If we keep the test repo and panic when `berge_base != target_sha`
|
||||
// when running the test below, then we have the following commit graph.
|
||||
//
|
||||
// Test: virtual_branches::update_base_branch::applied_branch::integrated_with_locked_conflicting_hunks
|
||||
// Command: `git log --oneline --all --graph``
|
||||
// * 244b526 GitButler WIP Commit
|
||||
// | * ea2956e (HEAD -> gitbutler/integration) GitButler Integration Commit
|
||||
// | * 3f2ccce (origin/master) Merge pull request from refs/heads/Virtual-branch
|
||||
// | |\
|
||||
// | |/
|
||||
// |/|
|
||||
// | * a6a0ed8 second
|
||||
// | | * f833dbe (int) Workspace Head
|
||||
// | |/
|
||||
// |/|
|
||||
// * | dee400c (origin/Virtual-branch, merge_base) third
|
||||
// |/
|
||||
// * 56c139c (master) first
|
||||
// * 6276165 Initial commit
|
||||
// (END)
|
||||
if merge_base == *target_sha {
|
||||
for (path, hunks) in base_diffs.clone().into_iter() {
|
||||
for hunk in hunks {
|
||||
let blame = project_repository.git_repository.blame(
|
||||
@ -1796,16 +1820,20 @@ fn get_applied_status(
|
||||
hunk.old_start,
|
||||
(hunk.old_start + hunk.old_lines).saturating_sub(1),
|
||||
target_sha,
|
||||
&branch.head,
|
||||
integration_commit,
|
||||
);
|
||||
|
||||
if let Ok(blame) = blame {
|
||||
for blame_hunk in blame.iter() {
|
||||
let commit = blame_hunk.orig_commit_id();
|
||||
if git::Oid::from(commit) != *target_sha {
|
||||
let hash = Hunk::hash(hunk.diff_lines.as_ref());
|
||||
git_hunk_map.insert(hash, branch.id);
|
||||
break;
|
||||
let commit = git::Oid::from(blame_hunk.orig_commit_id());
|
||||
if commit != *target_sha && commit != *integration_commit {
|
||||
let hash = Hunk::hash_diff(hunk.diff_lines.as_ref());
|
||||
locked_hunk_map
|
||||
.entry(hash)
|
||||
.and_modify(|commits| {
|
||||
commits.push(commit);
|
||||
})
|
||||
.or_insert(vec![commit]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1813,6 +1841,13 @@ fn get_applied_status(
|
||||
}
|
||||
}
|
||||
|
||||
let mut commit_to_branch = HashMap::new();
|
||||
for branch in &mut virtual_branches {
|
||||
for commit in project_repository.log(branch.head, LogUntil::Commit(*target_sha))? {
|
||||
commit_to_branch.insert(commit.id(), branch.id);
|
||||
}
|
||||
}
|
||||
|
||||
for branch in &mut virtual_branches {
|
||||
if !branch.applied {
|
||||
bail!("branch {} is not applied", branch.name);
|
||||
@ -1836,13 +1871,10 @@ fn get_applied_status(
|
||||
// if any of the current hunks intersects with the owned hunk, we want to keep it
|
||||
for (i, git_diff_hunk) in git_diff_hunks.iter().enumerate() {
|
||||
let hash = Hunk::hash_diff(git_diff_hunk.diff_lines.as_ref());
|
||||
if let Some(locked_to) = git_hunk_map.get(&hash) {
|
||||
if locked_to != &branch.id {
|
||||
return None;
|
||||
}
|
||||
if locked_hunk_map.contains_key(&hash) {
|
||||
return None; // Defer allocation to unclaimed hunks processing
|
||||
}
|
||||
if claimed_hunk.eq(&Hunk::from(git_diff_hunk)) {
|
||||
// try to re-use old timestamp
|
||||
let timestamp = claimed_hunk.timestam_ms().unwrap_or(mtime);
|
||||
diffs_by_branch
|
||||
.entry(branch.id)
|
||||
@ -1905,11 +1937,16 @@ fn get_applied_status(
|
||||
.position(|b| b.selected_for_changes == Some(max_selected_for_changes))
|
||||
.unwrap_or(0);
|
||||
|
||||
// Everything claimed has been removed from `base_diffs`, here we just
|
||||
// process the remaining ones.
|
||||
for (filepath, hunks) in base_diffs {
|
||||
for hunk in hunks {
|
||||
let hash = Hunk::hash_diff(hunk.diff_lines.as_ref());
|
||||
let vbranch_pos = if let Some(locked_to) = git_hunk_map.get(&hash) {
|
||||
let p = virtual_branches.iter().position(|vb| vb.id == *locked_to);
|
||||
let locked_to = locked_hunk_map.get(&hash);
|
||||
|
||||
let vbranch_pos = if let Some(locked_to) = locked_to {
|
||||
let branch_id = commit_to_branch.get(&locked_to[0]).unwrap();
|
||||
let p = virtual_branches.iter().position(|vb| vb.id == *branch_id);
|
||||
match p {
|
||||
Some(p) => p,
|
||||
_ => default_vbranch_pos,
|
||||
@ -1918,14 +1955,15 @@ fn get_applied_status(
|
||||
default_vbranch_pos
|
||||
};
|
||||
|
||||
let hash = Hunk::hash(hunk.diff_lines.as_ref());
|
||||
let hash = Hunk::hash_diff(hunk.diff_lines.as_ref());
|
||||
let mut new_hunk = Hunk::from(&hunk)
|
||||
.with_timestamp(mtimes.mtime_by_path(filepath.as_path()))
|
||||
.with_hash(hash);
|
||||
new_hunk.locked_to = git_hunk_map
|
||||
.get(&hash)
|
||||
.map(|locked_to| vec![*locked_to])
|
||||
.unwrap_or_default();
|
||||
new_hunk.locked_to = match locked_to {
|
||||
Some(locked_to) => locked_to.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
virtual_branches[vbranch_pos]
|
||||
.ownership
|
||||
.put(&OwnershipClaim {
|
||||
@ -2287,10 +2325,10 @@ pub fn commit(
|
||||
|
||||
let message = &message_buffer;
|
||||
|
||||
let integration_commit =
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let integration_commit_id =
|
||||
super::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||
// get the files to commit
|
||||
let (mut statuses, _) = get_status_by_branch(project_repository, Some(&integration_commit))
|
||||
let (mut statuses, _) = get_status_by_branch(project_repository, Some(&integration_commit_id))
|
||||
.context("failed to get status by branch")?;
|
||||
|
||||
let (ref mut branch, files) = statuses
|
||||
@ -2709,12 +2747,12 @@ pub fn amend(
|
||||
})
|
||||
})?;
|
||||
|
||||
let integration_commit =
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let integration_commit_id =
|
||||
super::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||
|
||||
let (mut applied_statuses, _) = get_applied_status(
|
||||
project_repository,
|
||||
&integration_commit,
|
||||
&integration_commit_id,
|
||||
&default_target.sha,
|
||||
applied_branches,
|
||||
)?;
|
||||
@ -2864,12 +2902,12 @@ pub fn cherry_pick(
|
||||
.filter(|b| b.applied)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let integration_commit =
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let integration_commit_id =
|
||||
super::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||
|
||||
let (applied_statuses, _) = get_applied_status(
|
||||
project_repository,
|
||||
&integration_commit,
|
||||
&integration_commit_id,
|
||||
&default_target.sha,
|
||||
applied_branches,
|
||||
)?;
|
||||
@ -3390,12 +3428,12 @@ pub fn move_commit(
|
||||
})
|
||||
})?;
|
||||
|
||||
let integration_commit =
|
||||
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let integration_commit_id =
|
||||
super::integration::get_workspace_head(&vb_state, project_repository)?;
|
||||
|
||||
let (mut applied_statuses, _) = get_applied_status(
|
||||
project_repository,
|
||||
&integration_commit,
|
||||
&integration_commit_id,
|
||||
&default_target.sha,
|
||||
applied_branches,
|
||||
)?;
|
||||
|
@ -1576,7 +1576,7 @@ mod applied_branch {
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.create_commit(project_id, &branch_id, "first", None, false)
|
||||
.create_commit(project_id, &branch_id, "third", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user