Merge commiting

This commit is contained in:
Caleb Owens 2024-09-11 14:47:36 +02:00
parent 955c99e5d8
commit 6e7aefd5c3
No known key found for this signature in database
6 changed files with 231 additions and 82 deletions

View File

@ -1,5 +1,4 @@
use super::r#virtual as vbranch;
use super::r#virtual as branch;
use crate::upstream_integration::{self, BranchStatuses, Resolution, UpstreamIntegrationContext};
use crate::{
base,

View File

@ -7,12 +7,12 @@ use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::{
rebase::cherry_rebase_group, LogUntil, RepoActionsExt as _, RepositoryExt as _,
rebase::{cherry_rebase_group, gitbutler_merge_commits},
LogUntil, RepoActionsExt as _, RepositoryExt as _,
};
use gix::discover::repository;
use serde::{Deserialize, Serialize};
use crate::{convert_to_real_branch, integration, BranchManagerExt, VirtualBranchesExt as _};
use crate::{BranchManagerExt, VirtualBranchesExt as _};
#[derive(Serialize, PartialEq, Debug)]
#[serde(tag = "type", content = "subject", rename_all = "camelCase")]
@ -77,6 +77,7 @@ pub struct UpstreamIntegrationContext<'a> {
virtual_branches_in_workspace: Vec<Branch>,
new_target: git2::Commit<'a>,
old_target: git2::Commit<'a>,
target_branch_name: String,
}
impl<'a> UpstreamIntegrationContext<'a> {
@ -100,6 +101,7 @@ impl<'a> UpstreamIntegrationContext<'a> {
new_target,
old_target,
virtual_branches_in_workspace,
target_branch_name: target.branch.branch().to_string(),
})
}
}
@ -332,8 +334,8 @@ fn compute_resolutions(
let UpstreamIntegrationContext {
repository,
new_target,
old_target,
virtual_branches_in_workspace,
target_branch_name,
..
} = context;
@ -358,15 +360,57 @@ fn compute_resolutions(
// Make a merge commit on top of the branch commits,
// then rebase the tree ontop of that. If the tree ends
// up conflicted, commit the tree.
todo!();
let target_commit = repository.find_commit(virtual_branch.head)?;
Ok((
virtual_branch.id,
IntegrationResult::UpdatedObjects {
head: todo!(),
tree: todo!(),
},
))
let new_head = gitbutler_merge_commits(
repository,
target_commit,
new_target.clone(),
&virtual_branch.name,
target_branch_name,
)?;
let head = repository.find_commit(virtual_branch.head)?;
let tree = repository.find_tree(virtual_branch.tree)?;
// Rebase tree
let author_signature = signature(SignaturePurpose::Author)
.context("Failed to get gitbutler signature")?;
let committer_signature = signature(SignaturePurpose::Committer)
.context("Failed to get gitbutler signature")?;
let committed_tree = repository.commit(
None,
&author_signature,
&committer_signature,
"Uncommited changes",
&tree,
&[&head],
)?;
// Rebase commited tree
let new_commited_tree =
cherry_rebase_group(repository, new_head.id(), &[committed_tree], true)?;
let new_commited_tree = repository.find_commit(new_commited_tree)?;
if new_commited_tree.is_conflicted() {
Ok((
virtual_branch.id,
IntegrationResult::UpdatedObjects {
head: new_commited_tree.id(),
tree: repository
.find_real_tree(&new_commited_tree, Default::default())?
.id(),
},
))
} else {
Ok((
virtual_branch.id,
IntegrationResult::UpdatedObjects {
head: new_head.id(),
tree: new_commited_tree.tree_id(),
},
))
}
}
ResolutionApproach::Rebase => {
// Rebase the commits, then try rebasing the tree. If
@ -514,6 +558,7 @@ mod test {
new_target: head_commit,
repository: &repository,
virtual_branches_in_workspace: vec![],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -536,6 +581,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -560,6 +606,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -597,6 +644,7 @@ mod test {
new_target: new_target.clone(),
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -659,6 +707,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -690,6 +739,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -719,6 +769,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -757,6 +808,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(
@ -804,6 +856,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
target_branch_name: "main".to_string(),
};
assert_eq!(

View File

@ -236,7 +236,6 @@ pub(crate) fn save_and_return_to_workspace(
let commit = repository
.find_commit(edit_mode_metadata.commit_oid)
.context("Failed to find commit")?;
let commit_parent = commit.parent(0).context("Failed to get commit's parent")?;
let stashed_workspace_changes_reference = repository
.find_reference(EDIT_UNCOMMITED_FILES_REF)
.context("Failed to find stashed workspace changes")?;
@ -250,6 +249,8 @@ pub(crate) fn save_and_return_to_workspace(
bail!("Failed to find virtual branch for this reference. Entering and leaving edit mode for non-virtual branches is unsupported")
};
let parents = commit.parents().collect::<Vec<_>>();
// Recommit commit
let tree = repository.create_wd_tree()?;
let commit_headers = commit
@ -266,7 +267,7 @@ pub(crate) fn save_and_return_to_workspace(
&commit.committer(),
&commit.message_bstr().to_str_lossy(),
&tree,
&[&commit_parent],
&parents.iter().collect::<Vec<_>>(),
commit_headers,
)
.context("Failed to commit new commit")?;

View File

@ -7,7 +7,6 @@ use gitbutler_commit::{
commit_headers::{CommitHeadersV2, HasCommitHeaders},
};
use gitbutler_error::error::Marker;
use itertools::Itertools;
use crate::{LogUntil, RepositoryExt as _};
@ -80,9 +79,21 @@ pub fn cherry_rebase_group(
if !succeeding_rebases {
return Err(anyhow!("failed to rebase")).context(Marker::BranchConflict);
}
commit_conflicted_cherry_result(repository, head, to_rebase, cherrypick_index)
commit_conflicted_cherry_result(
repository,
head,
to_rebase,
cherrypick_index,
None,
)
} else {
commit_unconflicted_cherry_result(repository, head, to_rebase, cherrypick_index)
commit_unconflicted_cherry_result(
repository,
head,
to_rebase,
cherrypick_index,
None,
)
}
},
)?
@ -91,11 +102,19 @@ pub fn cherry_rebase_group(
Ok(new_head_id)
}
pub struct OverrideCommitDetails<'a, 'repository> {
message: &'a str,
parents: &'a [&'a git2::Commit<'repository>],
author: &'a git2::Signature<'repository>,
commiter: &'a git2::Signature<'repository>,
}
fn commit_unconflicted_cherry_result<'repository>(
repository: &'repository git2::Repository,
head: git2::Commit<'repository>,
to_rebase: git2::Commit,
mut cherrypick_index: git2::Index,
override_commit_details: Option<OverrideCommitDetails>,
) -> Result<git2::Commit<'repository>> {
let commit_headers = to_rebase.gitbutler_headers();
@ -119,17 +138,31 @@ fn commit_unconflicted_cherry_result<'repository>(
..commit_headers
});
let commit_oid = crate::RepositoryExt::commit_with_signature(
repository,
None,
&to_rebase.author(),
&to_rebase.committer(),
&to_rebase.message_bstr().to_str_lossy(),
&merge_tree,
&[&head],
commit_headers,
)
.context("failed to create commit")?;
let commit_oid = if let Some(override_commit_details) = override_commit_details {
crate::RepositoryExt::commit_with_signature(
repository,
None,
override_commit_details.author,
override_commit_details.commiter,
override_commit_details.message,
&merge_tree,
override_commit_details.parents,
commit_headers,
)
.context("failed to create commit")?
} else {
crate::RepositoryExt::commit_with_signature(
repository,
None,
&to_rebase.author(),
&to_rebase.committer(),
&to_rebase.message_bstr().to_str_lossy(),
&merge_tree,
&[&head],
commit_headers,
)
.context("failed to create commit")?
};
repository
.find_commit(commit_oid)
@ -141,6 +174,7 @@ fn commit_conflicted_cherry_result<'repository>(
head: git2::Commit,
to_rebase: git2::Commit,
cherrypick_index: git2::Index,
override_commit_details: Option<OverrideCommitDetails>,
) -> Result<git2::Commit<'repository>> {
let commit_headers = to_rebase.gitbutler_headers();
@ -206,33 +240,91 @@ fn commit_conflicted_cherry_result<'repository>(
let tree_oid = tree_writer.write().context("failed to write tree")?;
let commit_headers = commit_headers.map(|commit_headers| {
let conflicted_file_count = dbg!(conflicted_files)
.len()
.try_into()
.expect("If you have more than 2^64 conflicting files, we've got bigger problems");
CommitHeadersV2 {
conflicted: Some(conflicted_file_count),
..commit_headers
}
});
let commit_headers =
commit_headers
.or_else(|| Some(Default::default()))
.map(|commit_headers| {
let conflicted_file_count = conflicted_files.len().try_into().expect(
"If you have more than 2^64 conflicting files, we've got bigger problems",
);
CommitHeadersV2 {
conflicted: Some(conflicted_file_count),
..commit_headers
}
});
// write a commit
let commit_oid = crate::RepositoryExt::commit_with_signature(
repository,
None,
&to_rebase.author(),
&to_rebase.committer(),
&to_rebase.message_bstr().to_str_lossy(),
&repository
.find_tree(tree_oid)
.context("failed to find tree")?,
&[&head],
commit_headers,
)
.context("failed to create commit")?;
let commit_oid = if let Some(override_commit_details) = override_commit_details {
crate::RepositoryExt::commit_with_signature(
repository,
None,
override_commit_details.author,
override_commit_details.commiter,
override_commit_details.message,
&repository
.find_tree(tree_oid)
.context("failed to find tree")?,
override_commit_details.parents,
commit_headers,
)
.context("failed to create commit")?
} else {
crate::RepositoryExt::commit_with_signature(
repository,
None,
&to_rebase.author(),
&to_rebase.committer(),
&to_rebase.message_bstr().to_str_lossy(),
&repository
.find_tree(tree_oid)
.context("failed to find tree")?,
&[&head],
commit_headers,
)
.context("failed to create commit")?
};
repository
.find_commit(commit_oid)
.context("failed to find commit")
}
pub fn gitbutler_merge_commits<'repository>(
repository: &'repository git2::Repository,
target_commit: git2::Commit<'repository>,
incoming_commit: git2::Commit<'repository>,
target_branch_name: &str,
incoming_branch_name: &str,
) -> Result<git2::Commit<'repository>> {
let cherrypick_index =
repository.cherry_pick_gitbutler(&target_commit, &incoming_commit, None)?;
let (author, committer) = repository.signatures()?;
let override_commit_details = OverrideCommitDetails {
message: &format!(
"Merge branch `{}` into `{}`",
incoming_branch_name, target_branch_name
),
parents: &[&target_commit.clone(), &incoming_commit.clone()],
author: &author,
commiter: &committer,
};
if cherrypick_index.has_conflicts() {
commit_conflicted_cherry_result(
repository,
target_commit,
incoming_commit,
cherrypick_index,
Some(override_commit_details),
)
} else {
commit_unconflicted_cherry_result(
repository,
target_commit,
incoming_commit,
cherrypick_index,
Some(override_commit_details),
)
}
}

View File

@ -1,14 +1,14 @@
use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
use gitbutler_branch::{gix_to_git2_signature, Branch, BranchId, SignaturePurpose};
use gitbutler_branch::{Branch, BranchId};
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_headers::CommitHeadersV2;
use gitbutler_error::error::Code;
use gitbutler_project::AuthKey;
use gitbutler_reference::{Refname, RemoteRefname};
use crate::{askpass, credentials, Config, RepositoryExt};
use crate::{askpass, credentials, RepositoryExt};
pub trait RepoActionsExt {
fn fetch(&self, remote_name: &str, askpass: Option<String>) -> Result<()>;
fn push(
@ -35,7 +35,6 @@ pub trait RepoActionsExt {
branch_name: &str,
askpass: Option<Option<BranchId>>,
) -> Result<()>;
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>;
}
impl RepoActionsExt for CommandContext {
@ -136,7 +135,10 @@ impl RepoActionsExt for CommandContext {
parents: &[&git2::Commit],
commit_headers: Option<CommitHeadersV2>,
) -> Result<git2::Oid> {
let (author, committer) = self.signatures().context("failed to get signatures")?;
let (author, committer) = self
.repository()
.signatures()
.context("failed to get signatures")?;
self.repository()
.commit_with_signature(
None,
@ -313,30 +315,6 @@ impl RepoActionsExt for CommandContext {
Err(anyhow!("authentication failed")).context(Code::ProjectGitAuth)
}
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)> {
let repo = gix::open(self.repository().path())?;
let author = repo
.author()
.transpose()?
.map(gitbutler_branch::gix_to_git2_signature)
.transpose()?
.context("No author is configured in Git")
.context(Code::AuthorMissing)?;
let config: Config = self.repository().into();
let committer = if config.user_real_comitter()? {
repo.committer()
.transpose()?
.map(gix_to_git2_signature)
.unwrap_or_else(|| gitbutler_branch::signature(SignaturePurpose::Committer))
} else {
gitbutler_branch::signature(SignaturePurpose::Committer)
}?;
Ok((author, committer))
}
}
type OidFilter = dyn Fn(&git2::Commit) -> Result<bool>;

View File

@ -7,18 +7,20 @@ use std::{io::Write, path::Path, process::Stdio, str};
use anyhow::{anyhow, bail, Context, Result};
use bstr::BString;
use git2::{BlameOptions, Tree};
use gitbutler_branch::{gix_to_git2_signature, SignaturePurpose};
use gitbutler_commit::{commit_buffer::CommitBuffer, commit_headers::CommitHeadersV2};
use gitbutler_config::git::{GbConfig, GitConfig};
use gitbutler_error::error::Code;
use gitbutler_reference::{Refname, RemoteRefname};
use tracing::instrument;
use crate::LogUntil;
use crate::{Config, LogUntil};
/// Extension trait for `git2::Repository`.
///
/// For now, it collects useful methods from `gitbutler-core::git::Repository`
pub trait RepositoryExt {
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>;
fn l(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Oid>>;
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>>;
fn log(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Commit>>;
@ -468,6 +470,30 @@ impl RepositoryExt for git2::Repository {
.collect::<Result<Vec<_>, _>>()
.context("failed to collect commits")
}
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)> {
let repo = gix::open(self.path())?;
let author = repo
.author()
.transpose()?
.map(gitbutler_branch::gix_to_git2_signature)
.transpose()?
.context("No author is configured in Git")
.context(Code::AuthorMissing)?;
let config: Config = self.into();
let committer = if config.user_real_comitter()? {
repo.committer()
.transpose()?
.map(gix_to_git2_signature)
.unwrap_or_else(|| gitbutler_branch::signature(SignaturePurpose::Committer))
} else {
gitbutler_branch::signature(SignaturePurpose::Committer)
}?;
Ok((author, committer))
}
}
/// Signs the buffer with the configured gpg key, returning the signature.