mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-28 20:15:20 +03:00
Merge commiting
This commit is contained in:
parent
955c99e5d8
commit
6e7aefd5c3
@ -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,
|
||||
|
@ -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!(
|
||||
|
@ -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")?;
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user