mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-06 01:27:24 +03:00
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:
parent
3da2290744
commit
b2833e2549
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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| {
|
||||
|
@ -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, ¤t_session)
|
||||
.context("failed to open current session")?;
|
||||
|
||||
// get the branch
|
||||
let branch_reader = branch::Reader::new(¤t_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(¤t_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,
|
||||
|
@ -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', {
|
||||
|
@ -39,6 +39,8 @@ export class Branch {
|
||||
description!: string;
|
||||
order!: number;
|
||||
upstream?: string;
|
||||
@Type(() => Commit)
|
||||
upstreamCommits!: Commit[];
|
||||
conflicted!: boolean;
|
||||
baseCurrent!: boolean;
|
||||
ownership!: string;
|
||||
|
@ -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}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user