feat: add merge_virtual_branch_upstream function to merge upstream changes into branch

The merge_virtual_branch_upstream function is added to merge the changes from the upstream branch into the current branch. This function checks if the project is in a conflicted state and if not, it proceeds with the merge. It finds the merge base between the upstream commit and the current branch's head commit, and then tries to merge the working directory tree with the remote tree. If the merge is successful, it commits the merge tree and updates the branch data.
This commit is contained in:
Scott Chacon 2023-09-20 10:23:32 +02:00
parent 3da2290744
commit b2833e2549
7 changed files with 231 additions and 10 deletions

View File

@ -676,6 +676,7 @@ async fn main() {
virtual_branches::commands::get_base_branch_data,
virtual_branches::commands::set_base_branch,
virtual_branches::commands::update_base_branch,
virtual_branches::commands::merge_virtual_branch_upstream,
virtual_branches::commands::update_virtual_branch,
virtual_branches::commands::delete_virtual_branch,
virtual_branches::commands::apply_branch,

View File

@ -63,6 +63,20 @@ pub async fn create_virtual_branch_from_branch(
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn merge_virtual_branch_upstream(
handle: AppHandle,
project_id: &str,
branch: &str,
) -> Result<(), Error> {
handle
.state::<Controller>()
.merge_virtual_branch_upstream(project_id, branch)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_base_branch_data(

View File

@ -269,6 +269,20 @@ impl Controller {
Ok(target)
}
pub async fn merge_virtual_branch_upstream(
&self,
project_id: &str,
branch: &str,
) -> Result<(), Error> {
self.with_lock(project_id, || {
self.with_verify_branch(project_id, |gb_repository, project_repository| {
super::merge_virtual_branch_upstream(gb_repository, project_repository, branch)
.map_err(Error::Other)
})
})
.await
}
pub async fn update_base_branch(&self, project_id: &str) -> Result<(), Error> {
self.with_lock(project_id, || {
self.with_verify_branch(project_id, |gb_repository, project_repository| {

View File

@ -45,6 +45,7 @@ pub struct VirtualBranch {
pub upstream: Option<git::RemoteBranchName>, // the name of the upstream branch this branch this pushes to
pub base_current: bool, // is this vbranch based on the current base branch? if false, this needs to be manually merged with conflicts
pub ownership: Ownership,
pub upstream_commits: Vec<VirtualBranchCommit>,
}
// this is the struct that maps to the view `Commit` type in Typescript
@ -464,7 +465,8 @@ pub fn list_virtual_branches(
}
// find upstream commits if we found an upstream reference
let mut upstream_commits = HashMap::new();
let mut upstream_commits = vec![];
let mut pushed_commits = HashMap::new();
if let Some(ref upstream) = upstream_commit {
let merge_base =
repo.merge_base(upstream.id(), default_target.sha)
@ -474,7 +476,14 @@ pub fn list_virtual_branches(
default_target.sha
))?;
for oid in project_repository.l(upstream.id(), LogUntil::Commit(merge_base))? {
upstream_commits.insert(oid, true);
pushed_commits.insert(oid, true);
}
// find any commits on the upstream that aren't in our branch (someone else pushed to our branch)
for commit in project_repository.log(upstream.id(), LogUntil::Commit(branch.head))? {
let commit =
commit_to_vbranch_commit(project_repository, &default_target, &commit, None)?;
upstream_commits.push(commit);
}
}
@ -488,7 +497,7 @@ pub fn list_virtual_branches(
project_repository,
&default_target,
&commit,
Some(&upstream_commits),
Some(&pushed_commits),
)?;
commits.push(commit);
}
@ -515,6 +524,7 @@ pub fn list_virtual_branches(
conflicted: conflicts::is_resolving(project_repository),
base_current,
ownership: branch.ownership.clone(),
upstream_commits,
};
branches.push(branch);
}
@ -798,6 +808,122 @@ pub fn create_virtual_branch(
Ok(branch)
}
pub fn merge_virtual_branch_upstream(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
branch_id: &str,
) -> Result<()> {
if conflicts::is_conflicting(project_repository, None)? {
bail!("cannot merge upstream, project is in a conflicted state");
}
let current_session = gb_repository
.get_or_create_current_session()
.context("failed to get current session")?;
let current_session_reader = sessions::Reader::open(gb_repository, &current_session)
.context("failed to open current session")?;
// get the branch
let branch_reader = branch::Reader::new(&current_session_reader);
let mut branch = branch_reader
.read(branch_id)
.context("failed to read branch")?;
// check if the branch upstream can be merged into the wd cleanly
let repo = &project_repository.git_repository;
// get upstream from the branch and find the remote branch
let mut upstream_commit = None;
let upstream_branch = branch
.upstream
.as_ref()
.context("no upstream branch found")?;
if let Ok(upstream_oid) = repo.refname_to_id(&upstream_branch.to_string()) {
if let Ok(upstream_commit_obj) = repo.find_commit(upstream_oid) {
upstream_commit = Some(upstream_commit_obj);
}
}
// if there is no upstream commit, then there is nothing to do
if upstream_commit.is_none() {
// no upstream commit, no merge to be done
return Ok(());
}
// there is an upstream commit, so lets check it out
let upstream_commit = upstream_commit.unwrap();
let remote_tree = upstream_commit.tree()?;
if upstream_commit.id() == branch.head {
// upstream is already merged, nothing to do
return Ok(());
}
// look up the target to figure out a merge base
let target = get_default_target(&current_session_reader)
.context("failed to get target")?
.context("no target found")?;
// get merge base from remote branch commit and target commit
let merge_base = repo
.merge_base(upstream_commit.id(), branch.head)
.context("failed to find merge base")?;
let merge_tree = repo.find_commit(merge_base)?.tree()?;
// get wd tree
let wd_tree = project_repository.get_wd_tree()?;
// try to merge our wd tree with the upstream tree
let mut merge_index = repo
.merge_trees(&merge_tree, &wd_tree, &remote_tree)
.context("failed to merge trees")?;
// three scenarios:
// - clean merge, clean wd, upstream is a fast forward, just fast forward it
// - clean merge, upstream is not a fast forward, merge it
// - upstream is not a fast forward, and cannot be merged cleanly
// - unapply all other branches, create the merge conflicts in the wd
if merge_index.has_conflicts() {
bail!("cannot merge upstream, conflicts found");
} else {
// get the merge tree oid from writing the index out
let merge_tree_oid = merge_index
.write_tree_to(repo)
.context("failed to write tree")?;
let (author, committer) = gb_repository.git_signatures()?;
let head_commit = repo.find_commit(branch.head)?;
let merge_tree = repo.find_tree(merge_tree_oid)?;
// commit the merge tree oid
let new_branch_head = repo.commit(
None,
&author,
&committer,
"merged from upstream",
&merge_tree,
&[&head_commit, &upstream_commit],
)?;
// checkout the merge tree
let mut checkout_options = git2::build::CheckoutBuilder::new();
checkout_options.force();
repo.checkout_tree(&merge_tree, Some(&mut checkout_options))?;
// write the branch data
// TODO: update ownership?
let branch_writer = branch::Writer::new(gb_repository);
branch.head = new_branch_head;
branch.tree = merge_tree_oid;
branch_writer.write(&branch)?;
super::integration::update_gitbutler_integration(gb_repository, project_repository)?;
}
Ok(())
}
pub fn update_branch(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,

View File

@ -37,6 +37,15 @@ export class BranchController {
}
}
async mergeUpstream(branch: string) {
try {
await invoke<void>('merge_virtual_branch_upstream', { projectId: this.projectId, branch });
await this.virtualBranchStore.reload();
} catch (err) {
toasts.error('Failed to merge upstream branch');
}
}
async updateBranchName(branchId: string, name: string) {
try {
await invoke<void>('update_virtual_branch', {

View File

@ -39,6 +39,8 @@ export class Branch {
description!: string;
order!: number;
upstream?: string;
@Type(() => Commit)
upstreamCommits!: Commit[];
conflicted!: boolean;
baseCurrent!: boolean;
ownership!: string;

View File

@ -86,6 +86,11 @@
}
}
function merge() {
console.log(`merge ${branch.id}`);
branchController.mergeUpstream(branch.id);
}
$: {
// On refresh we need to check expansion status from localStorage
branch.files && expandFromCache();
@ -135,6 +140,12 @@
commitDialogShown = false;
}
let upstreamCommitsShown = false;
$: if (upstreamCommitsShown && branch.upstreamCommits.length === 0) {
upstreamCommitsShown = false;
}
function generateBranchName() {
const diff = branch.files
.map((f) => f.hunks)
@ -192,6 +203,9 @@
});
});
console.log(branch);
console.log(remoteCommits);
const selectedOwnership = writable(Ownership.fromBranch(branch));
$: if (commitDialogShown) selectedOwnership.set(Ownership.fromBranch(branch));
</script>
@ -329,6 +343,47 @@
user={$user}
/>
{/if}
{#if branch.upstreamCommits.length > 0}
<div class="bg-zinc-300 p-2 dark:bg-zinc-800">
<div class="flex flex-row justify-between">
<div class="p-1 text-purple-700">
{branch.upstreamCommits.length}
upstream {branch.upstreamCommits.length > 1 ? 'commits' : 'commit'}
</div>
<Button
class="w-20"
height="small"
kind="outlined"
color="purple"
on:click={() => (upstreamCommitsShown = !upstreamCommitsShown)}
>
<span class="purple">
{#if !upstreamCommitsShown}
View
{:else}
Cancel
{/if}
</span>
</Button>
</div>
</div>
{#if upstreamCommitsShown}
<div
class="border-light-400 bg-light-300 dark:border-dark-400 dark:bg-dark-800 flex w-full flex-col border-t p-2"
id="upstreamCommits"
>
<div class="bg-light-100">
{#each branch.upstreamCommits as commit}
<CommitCard {commit} {projectId} />
{/each}
</div>
<div class="flex justify-end p-2">
<Button class="w-20" height="small" color="purple" on:click={merge}>Merge</Button>
</div>
</div>
{/if}
{/if}
</div>
</div>
{#if branch.files.length !== 0}
@ -357,7 +412,7 @@
<div class="relative flex flex-grow overflow-y-hidden">
<!-- TODO: Figure out why z-10 is necessary for expand up/down to not come out on top -->
<div
class="lane-dz-marker absolute z-10 hidden h-full w-full items-center justify-center rounded bg-blue-100/70 outline-dashed outline-2 -outline-offset-8 outline-light-600 dark:bg-blue-900/60 dark:outline-dark-300"
class="lane-dz-marker outline-light-600 dark:outline-dark-300 absolute z-10 hidden h-full w-full items-center justify-center rounded bg-blue-100/70 outline-dashed outline-2 -outline-offset-8 dark:bg-blue-900/60"
>
<div class="hover-text invisible font-semibold">Move here</div>
</div>
@ -433,17 +488,17 @@
transition:slide={{ duration: 150 }}
>
<div
class="dark:form-dark-600 absolute top-4 ml-[0.75rem] w-px bg-gradient-to-b from-light-400 via-light-500 via-90% dark:from-dark-600 dark:via-dark-600"
class="dark:form-dark-600 from-light-400 via-light-500 via-90% dark:from-dark-600 dark:via-dark-600 absolute top-4 ml-[0.75rem] w-px bg-gradient-to-b"
style={localCommits.length == 0 ? 'height: calc();' : 'height: 100%;'}
/>
<div class="relative flex flex-col gap-2">
<div
class="dark:form-dark-600 absolute top-4 ml-[0.75rem] h-px w-6 bg-gradient-to-r from-light-400 via-light-400 via-10% dark:from-dark-600 dark:via-dark-600"
class="dark:form-dark-600 from-light-400 via-light-400 via-10% dark:from-dark-600 dark:via-dark-600 absolute top-4 ml-[0.75rem] h-px w-6 bg-gradient-to-r"
/>
<div class="ml-10 mr-2 flex items-center py-2">
<div
class="ml-2 flex-grow font-mono text-sm font-bold text-dark-300 dark:text-light-300"
class="text-dark-300 dark:text-light-300 ml-2 flex-grow font-mono text-sm font-bold"
>
local
</div>
@ -479,13 +534,13 @@
{#if remoteCommits.length > 0}
<div class="relative flex-grow">
<div
class="dark:form-dark-600 absolute top-4 ml-[0.75rem] w-px bg-gradient-to-b from-light-600 via-light-600 via-90% dark:from-dark-400 dark:via-dark-400"
class="dark:form-dark-600 from-light-600 via-light-600 via-90% dark:from-dark-400 dark:via-dark-400 absolute top-4 ml-[0.75rem] w-px bg-gradient-to-b"
style="height: calc(100% - 1rem);"
/>
<div class="relative flex flex-grow flex-col gap-2">
<div
class="dark:form-dark-600 absolute top-4 ml-[0.75rem] h-px w-6 bg-gradient-to-r from-light-600 via-light-600 via-10% dark:from-dark-400 dark:via-dark-400"
class="dark:form-dark-600 from-light-600 via-light-600 via-10% dark:from-dark-400 dark:via-dark-400 absolute top-4 ml-[0.75rem] h-px w-6 bg-gradient-to-r"
/>
<div
@ -512,7 +567,7 @@
>
<div class="ml-[0.4rem] mr-1.5">
<div
class="h-3 w-3 rounded-full border-2 border-light-600 bg-light-600 dark:border-dark-400 dark:bg-dark-400"
class="border-light-600 bg-light-600 dark:border-dark-400 dark:bg-dark-400 h-3 w-3 rounded-full border-2"
class:bg-light-500={commit.isRemote}
class:dark:bg-dark-500={commit.isRemote}
/>