mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-18 23:02:31 +03:00
Merge pull request #4899 from gitbutlerapp/base-branch-improvements
This commit is contained in:
commit
4de7206b33
@ -17,6 +17,10 @@ export class BaseBranch {
|
||||
@Type(() => Commit)
|
||||
recentCommits!: Commit[];
|
||||
lastFetchedMs?: number;
|
||||
conflicted!: boolean;
|
||||
diverged!: boolean;
|
||||
divergedAhead!: string[];
|
||||
divergedBehind!: string[];
|
||||
|
||||
actualPushRemoteName(): string {
|
||||
return this.pushRemoteName || this.remoteName;
|
||||
|
@ -67,6 +67,27 @@ export class BaseBranchService {
|
||||
});
|
||||
await this.fetchFromRemotes();
|
||||
}
|
||||
|
||||
async push(withForce?: boolean) {
|
||||
this.loading.set(true);
|
||||
try {
|
||||
await invoke<void>('push_base_branch', {
|
||||
projectId: this.projectId,
|
||||
withForce
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err.code === Code.DefaultTargetNotFound) {
|
||||
// Swallow this error since user should be taken to project setup page
|
||||
return;
|
||||
} else if (err.code === Code.ProjectsGitAuth) {
|
||||
showError('Failed to authenticate', err);
|
||||
} else {
|
||||
showError('Failed to push', err);
|
||||
}
|
||||
console.error(err);
|
||||
}
|
||||
await this.fetchFromRemotes();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRemoteBranches(
|
||||
|
@ -4,11 +4,12 @@
|
||||
|
||||
interface Props {
|
||||
bottomBorder?: boolean;
|
||||
backgroundColor?: boolean;
|
||||
lines: Snippet;
|
||||
action: Snippet;
|
||||
}
|
||||
|
||||
const { bottomBorder = true, lines, action }: Props = $props();
|
||||
const { bottomBorder = true, backgroundColor = true, lines, action }: Props = $props();
|
||||
|
||||
let isNotInViewport = $state(false);
|
||||
</script>
|
||||
@ -18,6 +19,7 @@
|
||||
class:not-in-viewport={!isNotInViewport}
|
||||
class:sticky-z-index={!isNotInViewport}
|
||||
class:bottom-border={bottomBorder}
|
||||
class:background-color={backgroundColor}
|
||||
use:intersectionObserver={{
|
||||
callback: (entry) => {
|
||||
if (entry?.isIntersecting) {
|
||||
@ -46,12 +48,15 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
background-color: var(--clr-bg-2);
|
||||
overflow: hidden;
|
||||
|
||||
transition: border-top var(--transition-fast);
|
||||
}
|
||||
|
||||
.background-color {
|
||||
background-color: var(--clr-bg-2);
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -142,7 +142,7 @@
|
||||
modeService!.enterEditMode(commit.id, branch!.refname);
|
||||
}
|
||||
|
||||
$: conflicted = commit instanceof DetailedCommit && commit.conflicted;
|
||||
$: conflicted = commit.conflicted;
|
||||
</script>
|
||||
|
||||
<Modal bind:this={commitMessageModal} width="small" onSubmit={submitCommitMessageModal}>
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { Commit, DetailedCommit, VirtualBranch } from '$lib/vbranches/types';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import LineGroup from '@gitbutler/ui/commitLines/LineGroup.svelte';
|
||||
import { LineManagerFactory } from '@gitbutler/ui/commitLines/lineManager';
|
||||
import { LineManagerFactory, LineSpacer } from '@gitbutler/ui/commitLines/lineManager';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
@ -52,14 +52,6 @@
|
||||
const reorderDropzoneManagerFactory = getContext(ReorderDropzoneManagerFactory);
|
||||
const gitHost = getGitHost();
|
||||
|
||||
// TODO: Why does eslint-svelte-plugin complain about enum?
|
||||
// eslint-disable-next-line svelte/valid-compile
|
||||
enum LineSpacer {
|
||||
Remote = 'remote-spacer',
|
||||
Local = 'local-spacer',
|
||||
LocalAndRemote = 'local-and-remote-spacer'
|
||||
}
|
||||
|
||||
const mappedRemoteCommits = $derived(
|
||||
remoteCommits.length > 0
|
||||
? [...remoteCommits.map(transformAnyCommit), { id: LineSpacer.Remote }]
|
||||
|
@ -1,78 +1,236 @@
|
||||
<script lang="ts">
|
||||
import IntegrateUpstreamModal from './IntegrateUpstreamModal.svelte';
|
||||
import Spacer from '../shared/Spacer.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import CommitAction from '$lib/commit/CommitAction.svelte';
|
||||
import CommitCard from '$lib/commit/CommitCard.svelte';
|
||||
import { transformAnyCommit } from '$lib/commitLines/transformers';
|
||||
import { projectMergeUpstreamWarningDismissed } from '$lib/config/config';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { ModeService } from '$lib/modes/service';
|
||||
import { showInfo } from '$lib/notifications/toasts';
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
import { groupByCondition } from '$lib/utils/array';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Checkbox from '@gitbutler/ui/Checkbox.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
|
||||
import LineGroup from '@gitbutler/ui/commitLines/LineGroup.svelte';
|
||||
import { LineManagerFactory, LineSpacer } from '@gitbutler/ui/commitLines/lineManager';
|
||||
import { tick } from 'svelte';
|
||||
import type { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
|
||||
export let base: BaseBranch;
|
||||
interface Props {
|
||||
base: BaseBranch;
|
||||
}
|
||||
|
||||
const { base }: Props = $props();
|
||||
|
||||
const resetBaseTo = {
|
||||
local: {
|
||||
title: 'Push local changes',
|
||||
header:
|
||||
'You are about to reset the upstream target branch to the local branch. This will lose all the remote changes not in the local branch.',
|
||||
content: 'You will force-push your local changes to the remote branch.',
|
||||
tooltip:
|
||||
'Resets the upstream branch to the local target branch. Will lose all the remote changes not in the local branch.',
|
||||
color: 'pop',
|
||||
handler: handlePushLocalChanges,
|
||||
action: pushBaseBranch
|
||||
},
|
||||
remote: {
|
||||
title: 'Discard local changes',
|
||||
header:
|
||||
'You are about to reset the target branch to the remote branch. This will lose all the changes ahead of the remote branch.',
|
||||
content: 'You are about to hard reset your local base branch to the remote branch',
|
||||
tooltip: `Choose how to integrate the commits from ${base.branchName} into the base of all applied virtual branches`,
|
||||
color: 'warning',
|
||||
handler: handleMergeUpstream,
|
||||
action: updateBaseBranch
|
||||
}
|
||||
} as const;
|
||||
|
||||
type ResetBaseStrategy = keyof typeof resetBaseTo;
|
||||
|
||||
const baseBranchService = getContext(BaseBranchService);
|
||||
const branchController = getContext(BranchController);
|
||||
const modeService = getContext(ModeService);
|
||||
const gitHost = getGitHost();
|
||||
const project = getContext(Project);
|
||||
const lineManagerFactory = getContext(LineManagerFactory);
|
||||
|
||||
const mode = modeService.mode;
|
||||
|
||||
const mergeUpstreamWarningDismissed = projectMergeUpstreamWarningDismissed(
|
||||
branchController.projectId
|
||||
const mode = $derived(modeService.mode);
|
||||
const mergeUpstreamWarningDismissed = $derived(
|
||||
projectMergeUpstreamWarningDismissed(branchController.projectId)
|
||||
);
|
||||
|
||||
let updateTargetModal: Modal;
|
||||
let integrateUpstreamModal: IntegrateUpstreamModal | undefined;
|
||||
let mergeUpstreamWarningDismissedCheckbox = false;
|
||||
let baseBranchIsUpdating = $state<boolean>(false);
|
||||
const baseBranchConflicted = $derived(base.conflicted);
|
||||
let updateTargetModal = $state<Modal>();
|
||||
let resetBaseStrategy = $state<ResetBaseStrategy | undefined>(undefined);
|
||||
let confirmResetModal = $state<Modal>();
|
||||
const confirmResetModalOpen = $derived(!!confirmResetModal?.imports.open);
|
||||
let integrateUpstreamModal = $state<IntegrateUpstreamModal>();
|
||||
const integrateUpstreamModalOpen = $derived(!!integrateUpstreamModal?.imports.open);
|
||||
let mergeUpstreamWarningDismissedCheckbox = $state<boolean>(false);
|
||||
|
||||
$: multiple = base ? base.upstreamCommits.length > 1 || base.upstreamCommits.length === 0 : false;
|
||||
const pushButtonTooltip = $derived.by(() => {
|
||||
if (onlyLocalAhead) return 'Push your local changes to upstream';
|
||||
if (base.conflicted) return 'Cannot push while there are conflicts';
|
||||
return resetBaseTo.local.tooltip;
|
||||
});
|
||||
|
||||
const multiple = $derived(
|
||||
base ? base.upstreamCommits.length > 1 || base.upstreamCommits.length === 0 : false
|
||||
);
|
||||
|
||||
const onlyLocalAhead = $derived(
|
||||
base.diverged && base.divergedBehind.length === 0 && base.divergedAhead.length > 0
|
||||
);
|
||||
|
||||
const { satisfied: commitsAhead, rest: localAndRemoteCommits } = $derived(
|
||||
groupByCondition(base.recentCommits, (c) => base.divergedAhead.includes(c.id))
|
||||
);
|
||||
|
||||
const mappedRemoteCommits = $derived(
|
||||
base.upstreamCommits.length > 0
|
||||
? [...base.upstreamCommits.map(transformAnyCommit), { id: LineSpacer.Remote }]
|
||||
: []
|
||||
);
|
||||
|
||||
const mappedLocalCommits = $derived.by(() => {
|
||||
if (!base.diverged) return [];
|
||||
return commitsAhead.length > 0
|
||||
? [...commitsAhead.map(transformAnyCommit), { id: LineSpacer.Local }]
|
||||
: [];
|
||||
});
|
||||
|
||||
const mappedLocalAndRemoteCommits = $derived.by(() => {
|
||||
return localAndRemoteCommits.length > 0
|
||||
? [...localAndRemoteCommits.map(transformAnyCommit), { id: LineSpacer.LocalAndRemote }]
|
||||
: [];
|
||||
});
|
||||
|
||||
const lineManager = $derived(
|
||||
lineManagerFactory.build(
|
||||
{
|
||||
remoteCommits: mappedRemoteCommits,
|
||||
localCommits: mappedLocalCommits,
|
||||
localAndRemoteCommits: mappedLocalAndRemoteCommits,
|
||||
integratedCommits: []
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
async function updateBaseBranch() {
|
||||
let infoText = await branchController.updateBaseBranch();
|
||||
baseBranchIsUpdating = true;
|
||||
const infoText = await branchController.updateBaseBranch();
|
||||
if (infoText) {
|
||||
showInfo('Stashed conflicting branches', infoText);
|
||||
}
|
||||
await tick();
|
||||
baseBranchIsUpdating = false;
|
||||
}
|
||||
|
||||
function mergeUpstream() {
|
||||
if (project.succeedingRebases) {
|
||||
async function handleMergeUpstream() {
|
||||
if (project.succeedingRebases && !onlyLocalAhead) {
|
||||
integrateUpstreamModal?.show();
|
||||
} else {
|
||||
if (mergeUpstreamWarningDismissedCheckbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (base.diverged) {
|
||||
await confirmResetBranch('remote');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mergeUpstreamWarningDismissed) {
|
||||
updateBaseBranch();
|
||||
} else {
|
||||
updateTargetModal.show();
|
||||
return;
|
||||
}
|
||||
updateTargetModal?.show();
|
||||
}
|
||||
|
||||
async function pushBaseBranch() {
|
||||
baseBranchIsUpdating = true;
|
||||
await baseBranchService.push(!onlyLocalAhead);
|
||||
await tick();
|
||||
await baseBranchService.refresh();
|
||||
baseBranchIsUpdating = false;
|
||||
}
|
||||
|
||||
async function confirmResetBranch(strategy: ResetBaseStrategy) {
|
||||
resetBaseStrategy = strategy;
|
||||
await tick();
|
||||
confirmResetModal?.show();
|
||||
}
|
||||
|
||||
async function handlePushLocalChanges() {
|
||||
if (onlyLocalAhead) {
|
||||
await pushBaseBranch();
|
||||
return;
|
||||
}
|
||||
await confirmResetBranch('local');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#if base.diverged}
|
||||
<div class="message-wrapper">
|
||||
{#if !onlyLocalAhead}
|
||||
<InfoMessage style="warning" filled outlined={false}>
|
||||
<svelte:fragment slot="content">
|
||||
Your local target branch has diverged from upstream.
|
||||
<br />
|
||||
Target branch is
|
||||
<b>
|
||||
{`ahead by ${base.divergedAhead.length}`}
|
||||
</b>
|
||||
commits and
|
||||
<b>
|
||||
{`behind by ${base.divergedBehind.length}`}
|
||||
</b>
|
||||
commits
|
||||
</svelte:fragment>
|
||||
</InfoMessage>
|
||||
{:else}
|
||||
<InfoMessage style="neutral" filled outlined={false}>
|
||||
<svelte:fragment slot="content">
|
||||
Your local target branch is
|
||||
<b>
|
||||
{`ahead by ${base.divergedAhead.length}`}
|
||||
</b>
|
||||
commits
|
||||
</svelte:fragment>
|
||||
</InfoMessage>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !base.diverged && base.upstreamCommits.length > 0}
|
||||
<div class="header-wrapper">
|
||||
<div class="info-text text-13">
|
||||
There {multiple ? 'are' : 'is'}
|
||||
{base.upstreamCommits.length} unmerged upstream
|
||||
{multiple ? 'commits' : 'commit'}
|
||||
</div>
|
||||
|
||||
{#if base.upstreamCommits?.length > 0}
|
||||
<IntegrateUpstreamModal bind:this={integrateUpstreamModal} />
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
tooltip={`Merges the commits from ${base.branchName} into the base of all applied virtual branches`}
|
||||
disabled={$mode?.type !== 'OpenWorkspace' || integrateUpstreamModal?.imports.open}
|
||||
loading={integrateUpstreamModal?.imports.open}
|
||||
onclick={mergeUpstream}
|
||||
disabled={$mode?.type !== 'OpenWorkspace' || integrateUpstreamModalOpen}
|
||||
loading={integrateUpstreamModalOpen}
|
||||
onclick={handleMergeUpstream}
|
||||
>
|
||||
Merge into common base
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="wrapper">
|
||||
<!-- UPSTREAM COMMITS -->
|
||||
{#if base.upstreamCommits?.length > 0}
|
||||
<div>
|
||||
{#each base.upstreamCommits as commit, index}
|
||||
<CommitCard
|
||||
@ -82,25 +240,114 @@
|
||||
isUnapplied={true}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="remote"
|
||||
/>
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
</div>
|
||||
<Spacer margin={2} />
|
||||
|
||||
{#if base.diverged}
|
||||
<CommitAction backgroundColor={false}>
|
||||
{#snippet lines()}
|
||||
<LineGroup lineGroup={lineManager.get(LineSpacer.Remote)} topHeightPx={0} />
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button
|
||||
wide
|
||||
icon="warning"
|
||||
kind="solid"
|
||||
style={resetBaseTo.remote.color}
|
||||
tooltip={resetBaseTo.remote.tooltip}
|
||||
loading={baseBranchIsUpdating || integrateUpstreamModalOpen}
|
||||
disabled={$mode?.type !== 'OpenWorkspace' ||
|
||||
baseBranchIsUpdating ||
|
||||
integrateUpstreamModalOpen}
|
||||
onclick={resetBaseTo.remote.handler}
|
||||
>
|
||||
Integrate upstream changes
|
||||
</Button>
|
||||
{/snippet}
|
||||
</CommitAction>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- DIVERGED (LOCAL) COMMITS -->
|
||||
{#if commitsAhead.length > 0}
|
||||
<div>
|
||||
<Tooltip text="Current base for virtual branches.">
|
||||
<h1 class="text-13 info-text text-bold">Local</h1>
|
||||
</Tooltip>
|
||||
{#each base.recentCommits as commit, index}
|
||||
{#each commitsAhead as commit, index}
|
||||
<CommitCard
|
||||
{commit}
|
||||
first={index === 0}
|
||||
last={index === base.recentCommits.length - 1}
|
||||
last={index === commitsAhead.length - 1}
|
||||
isUnapplied={true}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="local"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<CommitAction backgroundColor={false}>
|
||||
{#snippet lines()}
|
||||
<LineGroup lineGroup={lineManager.get(LineSpacer.Local)} topHeightPx={0} />
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div class="local-actions-wrapper">
|
||||
<Button
|
||||
wide
|
||||
style={resetBaseTo.local.color}
|
||||
icon={onlyLocalAhead ? undefined : 'warning'}
|
||||
kind="solid"
|
||||
tooltip={pushButtonTooltip}
|
||||
loading={baseBranchIsUpdating || confirmResetModalOpen}
|
||||
disabled={$mode?.type !== 'OpenWorkspace' ||
|
||||
baseBranchIsUpdating ||
|
||||
confirmResetModalOpen ||
|
||||
baseBranchConflicted}
|
||||
onclick={resetBaseTo.local.handler}
|
||||
>
|
||||
{onlyLocalAhead ? 'Push' : resetBaseTo.local.title}
|
||||
</Button>
|
||||
|
||||
{#if onlyLocalAhead}
|
||||
<Button
|
||||
wide
|
||||
style="error"
|
||||
icon="warning"
|
||||
kind="solid"
|
||||
tooltip="Discard your local changes"
|
||||
disabled={$mode?.type !== 'OpenWorkspace' || integrateUpstreamModalOpen}
|
||||
loading={integrateUpstreamModalOpen}
|
||||
onclick={handleMergeUpstream}
|
||||
>
|
||||
Discard local changes
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CommitAction>
|
||||
{/if}
|
||||
|
||||
<!-- LOCAL AND REMOTE COMMITS -->
|
||||
<div>
|
||||
{#each localAndRemoteCommits as commit, index}
|
||||
<CommitCard
|
||||
{commit}
|
||||
first={index === 0}
|
||||
last={index === localAndRemoteCommits.length - 1}
|
||||
isUnapplied={true}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="localAndRemote"
|
||||
/>
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@ -146,17 +393,71 @@
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
{#if resetBaseStrategy}
|
||||
<Modal
|
||||
width="small"
|
||||
title={resetBaseTo[resetBaseStrategy].title}
|
||||
bind:this={confirmResetModal}
|
||||
onSubmit={async (close) => {
|
||||
if (resetBaseStrategy) await resetBaseTo[resetBaseStrategy].action();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div class="modal-content">
|
||||
<p class="text-12">
|
||||
{resetBaseTo[resetBaseStrategy].content}
|
||||
<br />
|
||||
<br />
|
||||
{#if resetBaseStrategy === 'local'}
|
||||
{base.divergedBehind.length > 1
|
||||
? `The ${base.divergedBehind.length} commits in the remote branch will be lost.`
|
||||
: 'The commit in the remote branch will be lost.'}
|
||||
{:else if resetBaseStrategy === 'remote'}
|
||||
{base.divergedAhead.length > 1
|
||||
? `The ${base.divergedAhead.length} commits in the local branch will be lost.`
|
||||
: 'The commit in the local branch will be lost.'}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#snippet controls(close)}
|
||||
<Button style="ghost" outline onclick={close}>Cancel</Button>
|
||||
<Button style="error" kind="solid" type="submit" icon="warning"
|
||||
>{resetBaseTo[resetBaseStrategy!].title}</Button
|
||||
>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<IntegrateUpstreamModal bind:this={integrateUpstreamModal} />
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.message-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.local-actions-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -7,9 +7,11 @@
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import {
|
||||
getBaseBrancheResolution,
|
||||
getResolutionApproach,
|
||||
sortStatusInfo,
|
||||
UpstreamIntegrationService,
|
||||
type BaseBranchResolutionApproach,
|
||||
type BranchStatusesWithBranches,
|
||||
type BranchStatusInfo,
|
||||
type Resolution
|
||||
@ -39,6 +41,8 @@
|
||||
let results = $state(new SvelteMap<string, Resolution>());
|
||||
let statuses = $state<BranchStatusInfo[]>([]);
|
||||
let expanded = $state<boolean>(false);
|
||||
let baseResolutionApproach = $state<BaseBranchResolutionApproach>('hardReset');
|
||||
let targetCommitOid = $state<string | undefined>(undefined);
|
||||
|
||||
$effect(() => {
|
||||
if ($branchStatuses?.type !== 'updatesRequired') {
|
||||
@ -68,10 +72,35 @@
|
||||
statuses = statusesTmp;
|
||||
});
|
||||
|
||||
// Re-fetch upstream statuses if the target commit oid changes
|
||||
$effect(() => {
|
||||
if (targetCommitOid) {
|
||||
branchStatuses = upstreamIntegrationService.upstreamStatuses(targetCommitOid);
|
||||
}
|
||||
});
|
||||
|
||||
// Resolve the target commit oid if the base branch diverged and the the resolution
|
||||
// approach is changed
|
||||
$effect(() => {
|
||||
if ($base?.diverged) {
|
||||
upstreamIntegrationService.resolveUpstreamIntegration(baseResolutionApproach).then((Oid) => {
|
||||
targetCommitOid = Oid;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function handleBaseResolutionSelection(resolution: BaseBranchResolutionApproach) {
|
||||
baseResolutionApproach = resolution;
|
||||
}
|
||||
|
||||
async function integrate() {
|
||||
integratingUpstream = 'loading';
|
||||
await tick();
|
||||
await upstreamIntegrationService.integrateUpstream(Array.from(results.values()));
|
||||
const baseResolution = getBaseBrancheResolution(targetCommitOid, baseResolutionApproach);
|
||||
await upstreamIntegrationService.integrateUpstream(
|
||||
Array.from(results.values()),
|
||||
baseResolution
|
||||
);
|
||||
await baseBranchService.refresh();
|
||||
integratingUpstream = 'completed';
|
||||
|
||||
@ -144,6 +173,34 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $base?.diverged}
|
||||
<div class="branch-status">
|
||||
<div class="description">
|
||||
<h5 class="text-16">{$base.branchName ?? 'Unknown'}</h5>
|
||||
<p>Diverged</p>
|
||||
</div>
|
||||
|
||||
<div class="action">
|
||||
<Select
|
||||
value={baseResolutionApproach}
|
||||
onselect={handleBaseResolutionSelection}
|
||||
options={[
|
||||
{ label: 'Rebase', value: 'rebase' },
|
||||
// { label: 'Merge', value: 'merge' }, hide merging for now
|
||||
{ label: 'Hard reset', value: 'hardReset' }
|
||||
]}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={highlighted} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if statuses.length > 0}
|
||||
<div class="statuses">
|
||||
{#each statuses as { branch, status }}
|
||||
@ -168,10 +225,7 @@
|
||||
onselect={(value) => {
|
||||
const result = results.get(branch.id)!;
|
||||
|
||||
results.set(branch.id, {
|
||||
...result,
|
||||
approach: { type: value as 'rebase' | 'merge' | 'unapply' }
|
||||
});
|
||||
results.set(branch.id, { ...result, approach: { type: value } });
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Rebase', value: 'rebase' },
|
||||
|
@ -15,7 +15,8 @@
|
||||
|
||||
const displayButton = $derived.by(() => {
|
||||
const hasUpstreamCommits = ($base?.upstreamCommits?.length ?? 0) > 0;
|
||||
return hasUpstreamCommits;
|
||||
const diverged = $base?.diverged ?? true;
|
||||
return hasUpstreamCommits && !diverged;
|
||||
});
|
||||
|
||||
let modal = $state<IntegrateUpstreamModal>();
|
||||
|
@ -17,6 +17,11 @@
|
||||
|
||||
const base = baseBranchService.base;
|
||||
$: selected = $page.url.href.endsWith('/base');
|
||||
$: baseBranchDiverged = !!$base?.diverged;
|
||||
$: baseBranchAheadOnly = baseBranchDiverged && !!$base?.divergedBehind?.length === false;
|
||||
$: divergenceTooltip = baseBranchAheadOnly
|
||||
? 'Your local target branch is ahead of its upstream'
|
||||
: 'Your local target branch has diverged from its upstream';
|
||||
</script>
|
||||
|
||||
<DomainButton
|
||||
@ -41,11 +46,21 @@
|
||||
<Tooltip text="The branch your Workspace branches are based on and merge into.">
|
||||
<span class="text-14 text-semibold trunk-label">Target</span>
|
||||
</Tooltip>
|
||||
{#if ($base?.behind || 0) > 0}
|
||||
{#if ($base?.behind || 0) > 0 && !baseBranchDiverged}
|
||||
<Tooltip text="Unmerged upstream commits">
|
||||
<Badge label={$base?.behind || 0} />
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if baseBranchDiverged}
|
||||
<Tooltip text={divergenceTooltip}>
|
||||
<div>
|
||||
<Icon
|
||||
name={baseBranchAheadOnly ? 'info' : 'warning'}
|
||||
color={baseBranchAheadOnly ? undefined : 'warning'}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<SyncButton />
|
||||
</div>
|
||||
<div class="base-branch-label">
|
||||
|
@ -1,6 +1,8 @@
|
||||
type Predicate<T> = (item: T) => boolean;
|
||||
|
||||
type ItemsSatisfyResult = 'all' | 'some' | 'none';
|
||||
|
||||
export function itemsSatisfy<T>(arr: T[], predicate: (item: T) => boolean): ItemsSatisfyResult {
|
||||
export function itemsSatisfy<T>(arr: T[], predicate: Predicate<T>): ItemsSatisfyResult {
|
||||
let satisfyCount = 0;
|
||||
let offenseCount = 0;
|
||||
for (const item of arr) {
|
||||
@ -29,6 +31,27 @@ export function chunk<T>(arr: T[], size: number) {
|
||||
);
|
||||
}
|
||||
|
||||
interface GroupByResult<T> {
|
||||
satisfied: T[];
|
||||
rest: T[];
|
||||
}
|
||||
|
||||
export function groupByCondition<T>(arr: T[], predicate: Predicate<T>): GroupByResult<T> {
|
||||
const satisfied: T[] = [];
|
||||
const rest: T[] = [];
|
||||
|
||||
for (const item of arr) {
|
||||
if (predicate(item)) {
|
||||
satisfied.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
rest.push(item);
|
||||
}
|
||||
|
||||
return { satisfied, rest };
|
||||
}
|
||||
|
||||
export function unique<T>(arr: T[]): T[] {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
|
@ -228,6 +228,7 @@ export class Commit {
|
||||
changeId!: string;
|
||||
isSigned!: boolean;
|
||||
parentIds!: string[];
|
||||
conflicted!: boolean;
|
||||
|
||||
prev?: Commit;
|
||||
next?: Commit;
|
||||
|
@ -37,6 +37,25 @@ export type Resolution = {
|
||||
approach: ResolutionApproach;
|
||||
};
|
||||
|
||||
export type BaseBranchResolutionApproach = 'rebase' | 'merge' | 'hardReset';
|
||||
|
||||
export type BaseBranchResolution = {
|
||||
targetCommitOid: string;
|
||||
approach: { type: BaseBranchResolutionApproach };
|
||||
};
|
||||
|
||||
export function getBaseBrancheResolution(
|
||||
targetCommitOid: string | undefined,
|
||||
approach: BaseBranchResolutionApproach
|
||||
): BaseBranchResolution | undefined {
|
||||
if (!targetCommitOid) return;
|
||||
|
||||
return {
|
||||
targetCommitOid,
|
||||
approach: { type: approach }
|
||||
};
|
||||
}
|
||||
|
||||
export function getResolutionApproach(statusInfo: BranchStatusInfo): ResolutionApproach {
|
||||
if (statusInfo.status.type === 'fullyIntegrated') {
|
||||
return { type: 'delete' };
|
||||
@ -79,10 +98,11 @@ export class UpstreamIntegrationService {
|
||||
private virtualBranchService: VirtualBranchService
|
||||
) {}
|
||||
|
||||
upstreamStatuses(): Readable<BranchStatusesWithBranches | undefined> {
|
||||
upstreamStatuses(targetCommitOid?: string): Readable<BranchStatusesWithBranches | undefined> {
|
||||
const branchStatuses = readable<BranchStatusesResponse | undefined>(undefined, (set) => {
|
||||
invoke<BranchStatusesResponse>('upstream_integration_statuses', {
|
||||
projectId: this.project.id
|
||||
projectId: this.project.id,
|
||||
targetCommitOid
|
||||
}).then(set);
|
||||
});
|
||||
|
||||
@ -113,7 +133,18 @@ export class UpstreamIntegrationService {
|
||||
return branchStatusesWithBranches;
|
||||
}
|
||||
|
||||
async integrateUpstream(resolutions: Resolution[]) {
|
||||
return await invoke('integrate_upstream', { projectId: this.project.id, resolutions });
|
||||
async integrateUpstream(resolutions: Resolution[], baseBranchResolution?: BaseBranchResolution) {
|
||||
return await invoke('integrate_upstream', {
|
||||
projectId: this.project.id,
|
||||
resolutions,
|
||||
baseBranchResolution
|
||||
});
|
||||
}
|
||||
|
||||
async resolveUpstreamIntegration(type: BaseBranchResolutionApproach) {
|
||||
return await invoke<string>('resolve_upstream_integration', {
|
||||
projectId: this.project.id,
|
||||
resolutionApproach: { type }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
use super::r#virtual as vbranch;
|
||||
use crate::upstream_integration::{self, BranchStatuses, Resolution, UpstreamIntegrationContext};
|
||||
use crate::move_commits;
|
||||
use crate::reorder_commits;
|
||||
use crate::upstream_integration::{
|
||||
self, BaseBranchResolution, BaseBranchResolutionApproach, BranchStatuses, Resolution,
|
||||
UpstreamIntegrationContext,
|
||||
};
|
||||
use crate::{
|
||||
base,
|
||||
base::BaseBranch,
|
||||
@ -9,7 +14,6 @@ use crate::{
|
||||
remote::{RemoteBranch, RemoteBranchData, RemoteCommit},
|
||||
VirtualBranchesExt,
|
||||
};
|
||||
use crate::{move_commits, reorder_commits};
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchId, BranchOwnershipClaims, BranchUpdateRequest};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
@ -158,6 +162,11 @@ pub fn set_target_push_remote(project: &Project, push_remote: &str) -> Result<()
|
||||
base::set_target_push_remote(&ctx, push_remote)
|
||||
}
|
||||
|
||||
pub fn push_base_branch(project: &Project, with_force: bool) -> Result<()> {
|
||||
let ctx = CommandContext::open(project)?;
|
||||
base::push(&ctx, with_force)
|
||||
}
|
||||
|
||||
pub fn integrate_upstream_commits(project: &Project, branch_id: BranchId) -> Result<()> {
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
@ -514,16 +523,27 @@ pub fn get_uncommited_files_reusable(project: &Project) -> Result<DiffByPathMap>
|
||||
crate::branch::get_uncommited_files_raw(&context, guard.read_permission())
|
||||
}
|
||||
|
||||
pub fn upstream_integration_statuses(project: &Project) -> Result<BranchStatuses> {
|
||||
pub fn upstream_integration_statuses(
|
||||
project: &Project,
|
||||
target_commit_oid: Option<git2::Oid>,
|
||||
) -> Result<BranchStatuses> {
|
||||
let command_context = CommandContext::open(project)?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
|
||||
let context = UpstreamIntegrationContext::open(&command_context, guard.write_permission())?;
|
||||
let context = UpstreamIntegrationContext::open(
|
||||
&command_context,
|
||||
target_commit_oid,
|
||||
guard.write_permission(),
|
||||
)?;
|
||||
|
||||
upstream_integration::upstream_integration_statuses(&context)
|
||||
}
|
||||
|
||||
pub fn integrate_upstream(project: &Project, resolutions: &[Resolution]) -> Result<()> {
|
||||
pub fn integrate_upstream(
|
||||
project: &Project,
|
||||
resolutions: &[Resolution],
|
||||
base_branch_resolution: Option<BaseBranchResolution>,
|
||||
) -> Result<()> {
|
||||
let command_context = CommandContext::open(project)?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
|
||||
@ -535,6 +555,21 @@ pub fn integrate_upstream(project: &Project, resolutions: &[Resolution]) -> Resu
|
||||
upstream_integration::integrate_upstream(
|
||||
&command_context,
|
||||
resolutions,
|
||||
base_branch_resolution,
|
||||
guard.write_permission(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resolve_upstream_integration(
|
||||
project: &Project,
|
||||
resolution_approach: BaseBranchResolutionApproach,
|
||||
) -> Result<git2::Oid> {
|
||||
let command_context = CommandContext::open(project)?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
|
||||
upstream_integration::resolve_upstream_integration(
|
||||
&command_context,
|
||||
resolution_approach,
|
||||
guard.write_permission(),
|
||||
)
|
||||
}
|
||||
|
@ -39,6 +39,12 @@ pub struct BaseBranch {
|
||||
pub upstream_commits: Vec<RemoteCommit>,
|
||||
pub recent_commits: Vec<RemoteCommit>,
|
||||
pub last_fetched_ms: Option<u128>,
|
||||
pub conflicted: bool,
|
||||
pub diverged: bool,
|
||||
#[serde(with = "gitbutler_serde::oid_vec")]
|
||||
pub diverged_ahead: Vec<git2::Oid>,
|
||||
#[serde(with = "gitbutler_serde::oid_vec")]
|
||||
pub diverged_behind: Vec<git2::Oid>,
|
||||
}
|
||||
|
||||
pub(crate) fn get_base_branch_data(ctx: &CommandContext) -> Result<BaseBranch> {
|
||||
@ -558,6 +564,26 @@ pub(crate) fn target_to_base_branch(ctx: &CommandContext, target: &Target) -> Re
|
||||
let commit = branch.get().peel_to_commit()?;
|
||||
let oid = commit.id();
|
||||
|
||||
// determine if the base branch is behind it's upstream
|
||||
let (number_commits_ahead, number_commits_behind) = repo.graph_ahead_behind(target.sha, oid)?;
|
||||
|
||||
let diverged_ahead = repo
|
||||
.log(target.sha, LogUntil::Take(number_commits_ahead))
|
||||
.context("failed to get fork point")?
|
||||
.iter()
|
||||
.map(|commit| commit.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let diverged_behind = repo
|
||||
.log(oid, LogUntil::Take(number_commits_behind))
|
||||
.context("failed to get fork point")?
|
||||
.iter()
|
||||
.map(|commit| commit.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// if there are commits ahead of the base branch consider it diverged
|
||||
let diverged = !diverged_ahead.is_empty();
|
||||
|
||||
// gather a list of commits between oid and target.sha
|
||||
let upstream_commits = repo
|
||||
.log(oid, LogUntil::Commit(target.sha))
|
||||
@ -574,6 +600,9 @@ pub(crate) fn target_to_base_branch(ctx: &CommandContext, target: &Target) -> Re
|
||||
.map(commit_to_remote_commit)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// we assume that only local commits can be conflicted
|
||||
let conflicted = recent_commits.iter().any(|commit| commit.conflicted);
|
||||
|
||||
// there has got to be a better way to do this.
|
||||
let push_remote_url = match target.push_remote_name {
|
||||
Some(ref name) => match repo.find_remote(name) {
|
||||
@ -604,6 +633,10 @@ pub(crate) fn target_to_base_branch(ctx: &CommandContext, target: &Target) -> Re
|
||||
.map(FetchResult::timestamp)
|
||||
.copied()
|
||||
.map(|t| t.duration_since(time::UNIX_EPOCH).unwrap().as_millis()),
|
||||
conflicted,
|
||||
diverged,
|
||||
diverged_ahead,
|
||||
diverged_behind,
|
||||
};
|
||||
Ok(base)
|
||||
}
|
||||
@ -611,3 +644,10 @@ pub(crate) fn target_to_base_branch(ctx: &CommandContext, target: &Target) -> Re
|
||||
fn default_target(base_path: &Path) -> Result<Target> {
|
||||
VirtualBranchesHandle::new(base_path).get_default_target()
|
||||
}
|
||||
|
||||
pub(crate) fn push(ctx: &CommandContext, with_force: bool) -> Result<()> {
|
||||
ctx.assure_resolved()?;
|
||||
let target = default_target(&ctx.project().gb_dir())?;
|
||||
let _ = ctx.push(target.sha, &target.branch, with_force, None, None);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -8,11 +8,11 @@ pub use actions::{
|
||||
get_uncommited_files_reusable, insert_blank_commit, integrate_upstream,
|
||||
integrate_upstream_commits, list_local_branches, list_remote_commit_files,
|
||||
list_virtual_branches, list_virtual_branches_cached, move_commit, move_commit_file,
|
||||
push_virtual_branch, reorder_commit, reset_files, reset_virtual_branch,
|
||||
save_and_unapply_virutal_branch, set_base_branch, set_target_push_remote, squash,
|
||||
unapply_ownership, unapply_without_saving_virtual_branch, undo_commit, update_base_branch,
|
||||
update_branch_order, update_commit_message, update_virtual_branch,
|
||||
upstream_integration_statuses,
|
||||
push_base_branch, push_virtual_branch, reorder_commit, reset_files, reset_virtual_branch,
|
||||
resolve_upstream_integration, save_and_unapply_virutal_branch, set_base_branch,
|
||||
set_target_push_remote, squash, unapply_ownership, unapply_without_saving_virtual_branch,
|
||||
undo_commit, update_base_branch, update_branch_order, update_commit_message,
|
||||
update_virtual_branch, upstream_integration_statuses,
|
||||
};
|
||||
|
||||
mod r#virtual;
|
||||
|
@ -55,6 +55,7 @@ pub struct RemoteCommit {
|
||||
pub change_id: Option<String>,
|
||||
#[serde(with = "gitbutler_serde::oid_vec")]
|
||||
pub parent_ids: Vec<git2::Oid>,
|
||||
pub conflicted: bool,
|
||||
}
|
||||
|
||||
/// Return information on all local branches, while skipping gitbutler-specific branches in `refs/heads`.
|
||||
@ -206,6 +207,7 @@ pub(crate) fn commit_to_remote_commit(commit: &git2::Commit) -> RemoteCommit {
|
||||
author: commit.author().into(),
|
||||
change_id: commit.change_id(),
|
||||
parent_ids,
|
||||
conflicted: commit.is_conflicted(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,14 @@ pub enum BranchStatuses {
|
||||
UpdatesRequired(Vec<(BranchId, BranchStatus)>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(tag = "type", content = "subject", rename_all = "camelCase")]
|
||||
pub enum BaseBranchResolutionApproach {
|
||||
Rebase,
|
||||
Merge,
|
||||
HardReset,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(tag = "type", content = "subject", rename_all = "camelCase")]
|
||||
enum ResolutionApproach {
|
||||
@ -41,6 +49,14 @@ enum ResolutionApproach {
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BaseBranchResolution {
|
||||
#[serde(with = "gitbutler_serde::oid")]
|
||||
target_commit_oid: git2::Oid,
|
||||
approach: BaseBranchResolutionApproach,
|
||||
}
|
||||
|
||||
impl BranchStatus {
|
||||
fn resolution_acceptable(&self, approach: &ResolutionApproach) -> bool {
|
||||
match self {
|
||||
@ -83,6 +99,7 @@ pub struct UpstreamIntegrationContext<'a> {
|
||||
impl<'a> UpstreamIntegrationContext<'a> {
|
||||
pub(crate) fn open(
|
||||
command_context: &'a CommandContext,
|
||||
target_commit_oid: Option<git2::Oid>,
|
||||
permission: &'a mut WorktreeWritePermission,
|
||||
) -> Result<Self> {
|
||||
let virtual_branches_handle = command_context.project().virtual_branches();
|
||||
@ -91,7 +108,12 @@ impl<'a> UpstreamIntegrationContext<'a> {
|
||||
let target_branch = repository
|
||||
.find_branch_by_refname(&target.branch.clone().into())?
|
||||
.ok_or(anyhow!("Branch not found"))?;
|
||||
let new_target = target_branch.get().peel_to_commit()?;
|
||||
|
||||
let new_target = target_commit_oid.map_or_else(
|
||||
|| target_branch.get().peel_to_commit(),
|
||||
|oid| repository.find_commit(oid),
|
||||
)?;
|
||||
|
||||
let old_target = repository.find_commit(target.sha)?;
|
||||
let virtual_branches_in_workspace = virtual_branches_handle.list_branches_in_workspace()?;
|
||||
|
||||
@ -203,9 +225,14 @@ pub fn upstream_integration_statuses(
|
||||
pub(crate) fn integrate_upstream(
|
||||
command_context: &CommandContext,
|
||||
resolutions: &[Resolution],
|
||||
base_branch_resolution: Option<BaseBranchResolution>,
|
||||
permission: &mut WorktreeWritePermission,
|
||||
) -> Result<()> {
|
||||
let context = UpstreamIntegrationContext::open(command_context, permission)?;
|
||||
let (target_commit_oid, base_branch_resolution_approach) = base_branch_resolution
|
||||
.map(|r| (Some(r.target_commit_oid), Some(r.approach)))
|
||||
.unwrap_or((None, None));
|
||||
|
||||
let context = UpstreamIntegrationContext::open(command_context, target_commit_oid, permission)?;
|
||||
let virtual_branches_state = VirtualBranchesHandle::new(command_context.project().gb_dir());
|
||||
let default_target = virtual_branches_state.get_default_target()?;
|
||||
|
||||
@ -250,7 +277,8 @@ pub(crate) fn integrate_upstream(
|
||||
}
|
||||
}
|
||||
|
||||
let integration_results = compute_resolutions(&context, resolutions)?;
|
||||
let integration_results =
|
||||
compute_resolutions(&context, resolutions, base_branch_resolution_approach)?;
|
||||
|
||||
{
|
||||
// We preform the updates in stages. If deleting or unapplying fails, we
|
||||
@ -313,13 +341,48 @@ pub(crate) fn integrate_upstream(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_upstream_integration(
|
||||
command_context: &CommandContext,
|
||||
resolution_approach: BaseBranchResolutionApproach,
|
||||
permission: &mut WorktreeWritePermission,
|
||||
) -> Result<git2::Oid> {
|
||||
let context = UpstreamIntegrationContext::open(command_context, None, permission)?;
|
||||
let repo = command_context.repository();
|
||||
let new_target_id = context.new_target.id();
|
||||
let old_target_id = context.old_target.id();
|
||||
let fork_point = repo.merge_base(old_target_id, new_target_id)?;
|
||||
|
||||
match resolution_approach {
|
||||
BaseBranchResolutionApproach::HardReset => Ok(new_target_id),
|
||||
BaseBranchResolutionApproach::Merge => {
|
||||
let new_head = gitbutler_merge_commits(
|
||||
repo,
|
||||
context.old_target,
|
||||
context.new_target,
|
||||
&context.target_branch_name,
|
||||
&context.target_branch_name,
|
||||
)?;
|
||||
|
||||
Ok(new_head.id())
|
||||
}
|
||||
BaseBranchResolutionApproach::Rebase => {
|
||||
let commits = repo.l(old_target_id, LogUntil::Commit(fork_point))?;
|
||||
let new_head = cherry_rebase_group(repo, new_target_id, &commits, true)?;
|
||||
|
||||
Ok(new_head)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_resolutions(
|
||||
context: &UpstreamIntegrationContext,
|
||||
resolutions: &[Resolution],
|
||||
base_branch_resolution_approach: Option<BaseBranchResolutionApproach>,
|
||||
) -> Result<Vec<(BranchId, IntegrationResult)>> {
|
||||
let UpstreamIntegrationContext {
|
||||
repository,
|
||||
new_target,
|
||||
old_target,
|
||||
virtual_branches_in_workspace,
|
||||
target_branch_name,
|
||||
..
|
||||
@ -402,9 +465,17 @@ fn compute_resolutions(
|
||||
// Rebase the commits, then try rebasing the tree. If
|
||||
// the tree ends up conflicted, commit the tree.
|
||||
|
||||
// If the base branch needs to resolve its divergence
|
||||
// pick only the commits that are ahead of the old target head
|
||||
let lower_bound = if base_branch_resolution_approach.is_some() {
|
||||
old_target.id()
|
||||
} else {
|
||||
new_target.id()
|
||||
};
|
||||
|
||||
// Rebase virtual branches' commits
|
||||
let virtual_branch_commits =
|
||||
repository.l(virtual_branch.head, LogUntil::Commit(new_target.id()))?;
|
||||
repository.l(virtual_branch.head, LogUntil::Commit(lower_bound))?;
|
||||
|
||||
let new_head = cherry_rebase_group(
|
||||
repository,
|
||||
@ -595,6 +666,7 @@ mod test {
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -614,6 +686,383 @@ mod test {
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflicted_head_branch_resolve_divergence_hard_reset() {
|
||||
let test_repository = TestingRepository::open();
|
||||
let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]);
|
||||
let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]);
|
||||
let branch_head = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "fux")]);
|
||||
// new target diverged from old target
|
||||
let new_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "qux")]);
|
||||
|
||||
let branch = make_branch(branch_head.id(), branch_head.tree_id());
|
||||
|
||||
let context = UpstreamIntegrationContext {
|
||||
_permission: None,
|
||||
old_target,
|
||||
new_target: new_target.clone(),
|
||||
repository: &test_repository.repository,
|
||||
virtual_branches_in_workspace: vec![branch.clone()],
|
||||
target_branch_name: "main".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
upstream_integration_statuses(&context).unwrap(),
|
||||
BranchStatuses::UpdatesRequired(vec![(
|
||||
branch.id,
|
||||
BranchStatus::Conflicted {
|
||||
potentially_conflicted_uncommited_changes: false
|
||||
}
|
||||
)]),
|
||||
);
|
||||
|
||||
let updates = compute_resolutions(
|
||||
&context,
|
||||
&[Resolution {
|
||||
branch_id: branch.id,
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
Some(BaseBranchResolutionApproach::HardReset),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updates.len(), 1);
|
||||
let IntegrationResult::UpdatedObjects { head, tree } = updates[0].1 else {
|
||||
panic!("Should be variant UpdatedObjects")
|
||||
};
|
||||
|
||||
let head_commit = test_repository.repository.find_commit(head).unwrap();
|
||||
assert_eq!(head_commit.parent(0).unwrap().id(), new_target.id());
|
||||
assert!(head_commit.is_conflicted());
|
||||
|
||||
let head_tree = test_repository
|
||||
.repository
|
||||
.find_real_tree(&head_commit, Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unconflicted_head_branch_resolve_divergence_hard_reset() {
|
||||
let test_repository = TestingRepository::open();
|
||||
let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]);
|
||||
let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]);
|
||||
let branch_head =
|
||||
test_repository.commit_tree(Some(&old_target), &[("bar.txt", "no problem")]);
|
||||
// new target diverged from old target
|
||||
let new_target =
|
||||
test_repository.commit_tree(Some(&initial_commit), &[("other.txt", "qux")]);
|
||||
|
||||
let branch = make_branch(branch_head.id(), branch_head.tree_id());
|
||||
|
||||
let context = UpstreamIntegrationContext {
|
||||
_permission: None,
|
||||
old_target,
|
||||
new_target: new_target.clone(),
|
||||
repository: &test_repository.repository,
|
||||
virtual_branches_in_workspace: vec![branch.clone()],
|
||||
target_branch_name: "main".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
upstream_integration_statuses(&context).unwrap(),
|
||||
BranchStatuses::UpdatesRequired(vec![(branch.id, BranchStatus::SaflyUpdatable)]),
|
||||
);
|
||||
|
||||
let updates = compute_resolutions(
|
||||
&context,
|
||||
&[Resolution {
|
||||
branch_id: branch.id,
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
Some(BaseBranchResolutionApproach::HardReset),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updates.len(), 1);
|
||||
let IntegrationResult::UpdatedObjects { head, tree } = updates[0].1 else {
|
||||
panic!("Should be variant UpdatedObjects")
|
||||
};
|
||||
|
||||
let head_commit = test_repository.repository.find_commit(head).unwrap();
|
||||
assert_eq!(head_commit.parent(0).unwrap().id(), new_target.id());
|
||||
assert!(!head_commit.is_conflicted());
|
||||
|
||||
let head_tree = test_repository
|
||||
.repository
|
||||
.find_real_tree(&head_commit, Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflicted_head_branch_resolve_divergence_rebase() {
|
||||
let test_repository = TestingRepository::open();
|
||||
let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]);
|
||||
let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]);
|
||||
let branch_head = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "fux")]);
|
||||
// new target diverged from old target
|
||||
let new_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "qux")]);
|
||||
|
||||
let branch = make_branch(branch_head.id(), branch_head.tree_id());
|
||||
|
||||
let commits_to_rebase = test_repository
|
||||
.repository
|
||||
.l(old_target.id(), LogUntil::Commit(initial_commit.id()))
|
||||
.unwrap();
|
||||
let head_after_rebase = cherry_rebase_group(
|
||||
&test_repository.repository,
|
||||
new_target.id(),
|
||||
&commits_to_rebase,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let context = UpstreamIntegrationContext {
|
||||
_permission: None,
|
||||
old_target,
|
||||
new_target: test_repository
|
||||
.repository
|
||||
.find_commit(head_after_rebase)
|
||||
.unwrap(),
|
||||
repository: &test_repository.repository,
|
||||
virtual_branches_in_workspace: vec![branch.clone()],
|
||||
target_branch_name: "main".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
upstream_integration_statuses(&context).unwrap(),
|
||||
BranchStatuses::UpdatesRequired(vec![(
|
||||
branch.id,
|
||||
BranchStatus::Conflicted {
|
||||
potentially_conflicted_uncommited_changes: false
|
||||
}
|
||||
)]),
|
||||
);
|
||||
|
||||
let updates = compute_resolutions(
|
||||
&context,
|
||||
&[Resolution {
|
||||
branch_id: branch.id,
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
Some(BaseBranchResolutionApproach::Rebase),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updates.len(), 1);
|
||||
let IntegrationResult::UpdatedObjects { head, tree } = updates[0].1 else {
|
||||
panic!("Should be variant UpdatedObjects")
|
||||
};
|
||||
|
||||
let head_commit = test_repository.repository.find_commit(head).unwrap();
|
||||
assert_eq!(head_commit.parent(0).unwrap().id(), head_after_rebase);
|
||||
assert!(head_commit.is_conflicted());
|
||||
|
||||
let head_tree = test_repository
|
||||
.repository
|
||||
.find_real_tree(&head_commit, Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unconflicted_head_branch_resolve_divergence_rebase() {
|
||||
let test_repository = TestingRepository::open();
|
||||
let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]);
|
||||
let old_target = test_repository.commit_tree(Some(&initial_commit), &[("bar.txt", "baz")]);
|
||||
let branch_head = test_repository.commit_tree(Some(&old_target), &[("bar.txt", "fux")]);
|
||||
// new target diverged from old target
|
||||
let new_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "qux")]);
|
||||
|
||||
let branch = make_branch(branch_head.id(), branch_head.tree_id());
|
||||
|
||||
let commits_to_rebase = test_repository
|
||||
.repository
|
||||
.l(old_target.id(), LogUntil::Commit(initial_commit.id()))
|
||||
.unwrap();
|
||||
let head_after_rebase = cherry_rebase_group(
|
||||
&test_repository.repository,
|
||||
new_target.id(),
|
||||
&commits_to_rebase,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let context = UpstreamIntegrationContext {
|
||||
_permission: None,
|
||||
old_target,
|
||||
new_target: test_repository
|
||||
.repository
|
||||
.find_commit(head_after_rebase)
|
||||
.unwrap(),
|
||||
repository: &test_repository.repository,
|
||||
virtual_branches_in_workspace: vec![branch.clone()],
|
||||
target_branch_name: "main".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
upstream_integration_statuses(&context).unwrap(),
|
||||
BranchStatuses::UpdatesRequired(vec![(branch.id, BranchStatus::SaflyUpdatable)]),
|
||||
);
|
||||
|
||||
let updates = compute_resolutions(
|
||||
&context,
|
||||
&[Resolution {
|
||||
branch_id: branch.id,
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
Some(BaseBranchResolutionApproach::Rebase),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updates.len(), 1);
|
||||
let IntegrationResult::UpdatedObjects { head, tree } = updates[0].1 else {
|
||||
panic!("Should be variant UpdatedObjects")
|
||||
};
|
||||
|
||||
let head_commit = test_repository.repository.find_commit(head).unwrap();
|
||||
assert_eq!(head_commit.parent(0).unwrap().id(), head_after_rebase);
|
||||
assert!(!head_commit.is_conflicted());
|
||||
|
||||
let head_tree = test_repository
|
||||
.repository
|
||||
.find_real_tree(&head_commit, Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflicted_head_branch_resolve_divergence_merge() {
|
||||
let test_repository = TestingRepository::open();
|
||||
let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]);
|
||||
let old_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "baz")]);
|
||||
let branch_head = test_repository.commit_tree(Some(&old_target), &[("foo.txt", "fux")]);
|
||||
// new target diverged from old target
|
||||
let new_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "qux")]);
|
||||
|
||||
let branch = make_branch(branch_head.id(), branch_head.tree_id());
|
||||
|
||||
let merge_commit = gitbutler_merge_commits(
|
||||
&test_repository.repository,
|
||||
old_target.clone(),
|
||||
new_target,
|
||||
"main",
|
||||
"main",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let context = UpstreamIntegrationContext {
|
||||
_permission: None,
|
||||
old_target,
|
||||
new_target: merge_commit.clone(),
|
||||
repository: &test_repository.repository,
|
||||
virtual_branches_in_workspace: vec![branch.clone()],
|
||||
target_branch_name: "main".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
upstream_integration_statuses(&context).unwrap(),
|
||||
BranchStatuses::UpdatesRequired(vec![(
|
||||
branch.id,
|
||||
BranchStatus::Conflicted {
|
||||
potentially_conflicted_uncommited_changes: false
|
||||
}
|
||||
)]),
|
||||
);
|
||||
|
||||
let updates = compute_resolutions(
|
||||
&context,
|
||||
&[Resolution {
|
||||
branch_id: branch.id,
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
Some(BaseBranchResolutionApproach::Merge),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updates.len(), 1);
|
||||
let IntegrationResult::UpdatedObjects { head, tree } = updates[0].1 else {
|
||||
panic!("Should be variant UpdatedObjects")
|
||||
};
|
||||
|
||||
let head_commit = test_repository.repository.find_commit(head).unwrap();
|
||||
assert_eq!(head_commit.parent(0).unwrap().id(), merge_commit.id());
|
||||
assert!(head_commit.is_conflicted());
|
||||
|
||||
let head_tree = test_repository
|
||||
.repository
|
||||
.find_real_tree(&head_commit, Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unconflicted_head_branch_resolve_divergence_merge() {
|
||||
let test_repository = TestingRepository::open();
|
||||
let initial_commit = test_repository.commit_tree(None, &[("foo.txt", "bar")]);
|
||||
let old_target = test_repository.commit_tree(Some(&initial_commit), &[("bar.txt", "baz")]);
|
||||
let branch_head = test_repository.commit_tree(Some(&old_target), &[("bar.txt", "fux")]);
|
||||
// new target diverged from old target
|
||||
let new_target = test_repository.commit_tree(Some(&initial_commit), &[("foo.txt", "qux")]);
|
||||
|
||||
let branch = make_branch(branch_head.id(), branch_head.tree_id());
|
||||
|
||||
let merge_commit = gitbutler_merge_commits(
|
||||
&test_repository.repository,
|
||||
old_target.clone(),
|
||||
new_target,
|
||||
"main",
|
||||
"main",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let context = UpstreamIntegrationContext {
|
||||
_permission: None,
|
||||
old_target,
|
||||
new_target: merge_commit.clone(),
|
||||
repository: &test_repository.repository,
|
||||
virtual_branches_in_workspace: vec![branch.clone()],
|
||||
target_branch_name: "main".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
upstream_integration_statuses(&context).unwrap(),
|
||||
BranchStatuses::UpdatesRequired(vec![(branch.id, BranchStatus::SaflyUpdatable)]),
|
||||
);
|
||||
|
||||
let updates = compute_resolutions(
|
||||
&context,
|
||||
&[Resolution {
|
||||
branch_id: branch.id,
|
||||
branch_tree: branch.tree,
|
||||
approach: ResolutionApproach::Rebase,
|
||||
}],
|
||||
Some(BaseBranchResolutionApproach::Merge),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updates.len(), 1);
|
||||
let IntegrationResult::UpdatedObjects { head, tree } = updates[0].1 else {
|
||||
panic!("Should be variant UpdatedObjects")
|
||||
};
|
||||
|
||||
let head_commit = test_repository.repository.find_commit(head).unwrap();
|
||||
assert_eq!(head_commit.parent(0).unwrap().id(), merge_commit.id());
|
||||
assert!(!head_commit.is_conflicted());
|
||||
|
||||
let head_tree = test_repository
|
||||
.repository
|
||||
.find_real_tree(&head_commit, Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(head_tree.id(), tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflicted_tree_branch() {
|
||||
let test_repository = TestingRepository::open();
|
||||
|
@ -436,7 +436,7 @@ mod conflict_cases {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
gitbutler_branch_actions::integrate_upstream(&project, &[]).unwrap();
|
||||
gitbutler_branch_actions::integrate_upstream(&project, &[], None).unwrap();
|
||||
|
||||
// Apply B
|
||||
|
||||
@ -535,7 +535,7 @@ mod conflict_cases {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
gitbutler_branch_actions::integrate_upstream(&project, &[]).unwrap();
|
||||
gitbutler_branch_actions::integrate_upstream(&project, &[], None).unwrap();
|
||||
|
||||
// Apply B
|
||||
|
||||
|
@ -159,6 +159,7 @@ fn main() {
|
||||
virtual_branches::commands::get_base_branch_data,
|
||||
virtual_branches::commands::set_base_branch,
|
||||
virtual_branches::commands::update_base_branch,
|
||||
virtual_branches::commands::push_base_branch,
|
||||
virtual_branches::commands::integrate_upstream_commits,
|
||||
virtual_branches::commands::update_virtual_branch,
|
||||
virtual_branches::commands::update_branch_order,
|
||||
@ -187,6 +188,7 @@ fn main() {
|
||||
virtual_branches::commands::normalize_branch_name,
|
||||
virtual_branches::commands::upstream_integration_statuses,
|
||||
virtual_branches::commands::integrate_upstream,
|
||||
virtual_branches::commands::resolve_upstream_integration,
|
||||
virtual_branches::commands::find_commit,
|
||||
stack::create_series,
|
||||
stack::remove_series,
|
||||
|
@ -4,7 +4,9 @@ pub mod commands {
|
||||
BranchCreateRequest, BranchId, BranchOwnershipClaims, BranchUpdateRequest,
|
||||
};
|
||||
use gitbutler_branch_actions::internal::PushResult;
|
||||
use gitbutler_branch_actions::upstream_integration::{BranchStatuses, Resolution};
|
||||
use gitbutler_branch_actions::upstream_integration::{
|
||||
BaseBranchResolution, BaseBranchResolutionApproach, BranchStatuses, Resolution,
|
||||
};
|
||||
use gitbutler_branch_actions::{
|
||||
BaseBranch, BranchListing, BranchListingDetails, BranchListingFilter, RemoteBranch,
|
||||
RemoteBranchData, RemoteBranchFile, RemoteCommit, VirtualBranches,
|
||||
@ -174,6 +176,20 @@ pub mod commands {
|
||||
Ok(unapplied_branches)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects, windows), err(Debug))]
|
||||
pub fn push_base_branch(
|
||||
windows: State<'_, WindowState>,
|
||||
projects: State<'_, projects::Controller>,
|
||||
project_id: ProjectId,
|
||||
with_force: bool,
|
||||
) -> Result<(), Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
gitbutler_branch_actions::push_base_branch(&project, with_force)?;
|
||||
emit_vbranches(&windows, project_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects, windows), err(Debug))]
|
||||
pub fn update_virtual_branch(
|
||||
@ -559,10 +575,14 @@ pub mod commands {
|
||||
pub fn upstream_integration_statuses(
|
||||
projects: State<'_, projects::Controller>,
|
||||
project_id: ProjectId,
|
||||
target_commit_oid: Option<String>,
|
||||
) -> Result<BranchStatuses, Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
let commit_oid = target_commit_oid
|
||||
.map(|commit_id| git2::Oid::from_str(&commit_id).map_err(|e| anyhow!(e)))
|
||||
.transpose()?;
|
||||
Ok(gitbutler_branch_actions::upstream_integration_statuses(
|
||||
&project,
|
||||
&project, commit_oid,
|
||||
)?)
|
||||
}
|
||||
|
||||
@ -573,16 +593,35 @@ pub mod commands {
|
||||
projects: State<'_, projects::Controller>,
|
||||
project_id: ProjectId,
|
||||
resolutions: Vec<Resolution>,
|
||||
base_branch_resolution: Option<BaseBranchResolution>,
|
||||
) -> Result<(), Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
|
||||
gitbutler_branch_actions::integrate_upstream(&project, &resolutions)?;
|
||||
gitbutler_branch_actions::integrate_upstream(
|
||||
&project,
|
||||
&resolutions,
|
||||
base_branch_resolution,
|
||||
)?;
|
||||
|
||||
emit_vbranches(&windows, project_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects), err(Debug))]
|
||||
pub fn resolve_upstream_integration(
|
||||
projects: State<'_, projects::Controller>,
|
||||
project_id: ProjectId,
|
||||
resolution_approach: BaseBranchResolutionApproach,
|
||||
) -> Result<String, Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
|
||||
let new_target_id =
|
||||
gitbutler_branch_actions::resolve_upstream_integration(&project, resolution_approach)?;
|
||||
let commit_id = git2::Oid::to_string(&new_target_id);
|
||||
Ok(commit_id)
|
||||
}
|
||||
|
||||
pub(crate) fn emit_vbranches(windows: &WindowState, project_id: projects::ProjectId) {
|
||||
if let Err(error) = windows.post(gitbutler_watcher::Action::CalculateVirtualBranches(
|
||||
project_id,
|
||||
|
@ -1,5 +1,11 @@
|
||||
import type { CommitData, LineGroupData, LineData, Color } from '$lib/commitLines/types';
|
||||
|
||||
export enum LineSpacer {
|
||||
Remote = 'remote-spacer',
|
||||
Local = 'local-spacer',
|
||||
LocalAndRemote = 'local-and-remote-spacer'
|
||||
}
|
||||
|
||||
interface Commits {
|
||||
remoteCommits: CommitData[];
|
||||
localCommits: CommitData[];
|
||||
|
Loading…
Reference in New Issue
Block a user