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:
Mattias Granlund 2024-04-23 16:11:27 +02:00
parent 2f7b396ab8
commit 811f0ee35b
5 changed files with 394 additions and 278 deletions

View File

@ -362,8 +362,7 @@ 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()))?
let updated_vbranches = super::get_status_by_branch(project_repository, None)?
.0
.into_iter()
.map(|(branch, _)| branch)
@ -425,8 +424,7 @@ pub fn update_base_branch(
return Ok(Some(branch));
}
let branch_merge_index_tree_oid =
branch_tree_merge_index.write_tree_to(repo)?;
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);
@ -465,8 +463,7 @@ pub fn update_base_branch(
let ok_with_force_push = project_repository.project().ok_with_force_push;
let result_merge =
|mut branch: branch::Branch| -> Result<Option<branch::Branch>> {
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
@ -525,8 +522,7 @@ pub fn update_base_branch(
break;
}
if let Ok(commit_id) = rebase.commit(None, &committer.clone().into(), None)
{
if let Ok(commit_id) = rebase.commit(None, &committer.clone().into(), None) {
last_rebase_head = commit_id.into();
} else {
rebase_success = false;

View File

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

View File

@ -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');
}
}
if let Some(prev_branch) = prev_branch {
message.push_str("\nYour previous branch was: ");
message.push_str(&prev_head);
message.push_str(&prev_branch.head);
message.push_str("\n\n");
message.push_str("The sha for that commit was: ");
message.push_str(&prev_sha);
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

View File

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

View File

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