Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mergeAll btn #5634

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src/lib/commit/CommitList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@
{#snippet title()}
<span class="text-12 text-body"
>Some branches in this stack have been integrated. Please force push to sync your branch
with the updated base.↘</span
with the updated base ↘</span
>
{/snippet}
{#each remoteIntegratedPatches as commit, idx (commit.id)}
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/lib/forge/github/githubPrService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ export class GitHubPrService implements ForgePrService {
return parseGitHubDetailedPullRequest(resp.data);
}

async updateBase(prNumber: number, targetBase: string) {
await this.octokit.pulls.update({
owner: this.repo.owner,
repo: this.repo.name,
pull_number: prNumber,
base: targetBase
});
}

async merge(method: MergeMethod, prNumber: number) {
await this.octokit.pulls.merge({
owner: this.repo.owner,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/lib/forge/interface/forgePrService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ForgePrService {
upstreamName
}: CreatePullRequestArgs): Promise<PullRequest>;
merge(method: MergeMethod, prNumber: number): Promise<void>;
updateBase(prNumber: number, targetBase: string): Promise<void>;
reopen(prNumber: number): Promise<void>;
prMonitor(prNumber: number): ForgePrMonitor;
update(
Expand Down
42 changes: 29 additions & 13 deletions apps/desktop/src/lib/pr/MergeButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,37 @@
import { MergeMethod } from '$lib/forge/interface/types';
import DropDownButton from '$lib/shared/DropDownButton.svelte';
import { persisted, type Persisted } from '@gitbutler/shared/persisted';
import { createEventDispatcher } from 'svelte';
import type { Props as ButtonProps } from '@gitbutler/ui/Button.svelte';

export let projectId: string;
export let loading = false;
export let disabled = false;
export let wide = false;
export let tooltip = '';
interface Props {
projectId: string;
onclick: (method: MergeMethod) => void;
loading?: boolean;
disabled?: boolean;
wide?: boolean;
tooltip?: string;
style?: ButtonProps['style'];
kind?: ButtonProps['kind'];
outline?: boolean;
}

const {
projectId,
onclick,
loading = false,
disabled = false,
wide = false,
tooltip = '',
style = 'ghost',
kind = 'soft',
outline = true
}: Props = $props();

function persistedAction(projectId: string): Persisted<MergeMethod> {
const key = 'projectMergeMethod';
return persisted<MergeMethod>(MergeMethod.Merge, key + projectId);
}

const dispatch = createEventDispatcher<{ click: { method: MergeMethod } }>();
const action = persistedAction(projectId);

let dropDown: ReturnType<typeof DropDownButton> | undefined;
Expand All @@ -30,16 +47,15 @@
</script>

<DropDownButton
style="ghost"
outline
{loading}
bind:this={dropDown}
onclick={() => onclick($action)}
{outline}
{style}
{kind}
{loading}
{wide}
{tooltip}
{disabled}
onclick={() => {
dispatch('click', { method: $action });
}}
>
{labels[$action]}
{#snippet contextMenuSlot()}
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/src/lib/pr/PullRequestCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,9 @@
disabled={mergeStatus.disabled}
tooltip={mergeStatus.tooltip}
loading={isMerging}
on:click={async (e) => {
onclick={async (method) => {
if (!pr) return;
isMerging = true;
const method = e.detail.method;
try {
await $prService?.merge(method, pr.number);
await baseBranchService.fetchFromRemotes();
Expand Down
104 changes: 100 additions & 4 deletions apps/desktop/src/lib/stack/Stack.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@
import laneNewSvg from '$lib/assets/empty-state/lane-new.svg?raw';
import noChangesSvg from '$lib/assets/empty-state/lane-no-changes.svg?raw';
import { Project } from '$lib/backend/projects';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import Dropzones from '$lib/branch/Dropzones.svelte';
import { getForgeListingService } from '$lib/forge/interface/forgeListingService';
import { getForgePrService } from '$lib/forge/interface/forgePrService';
import { type MergeMethod } from '$lib/forge/interface/types';
import { showError } from '$lib/notifications/toasts';
import MergeButton from '$lib/pr/MergeButton.svelte';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import Resizer from '$lib/shared/Resizer.svelte';
import CollapsedLane from '$lib/stack/CollapsedLane.svelte';
import { intersectionObserver } from '$lib/utils/intersectionObserver';
import * as toasts from '$lib/utils/toasts';
import { BranchController } from '$lib/vbranches/branchController';
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { DetailedCommit, VirtualBranch } from '$lib/vbranches/types';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import { getContext, getContextStore, getContextStoreBySymbol } from '@gitbutler/shared/context';
import { persisted } from '@gitbutler/shared/persisted';
import Button from '@gitbutler/ui/Button.svelte';
Expand All @@ -29,20 +36,24 @@
commitBoxOpen
}: { isLaneCollapsed: Writable<boolean>; commitBoxOpen: Writable<boolean> } = $props();

const vbranchService = getContext(VirtualBranchService);
const branchController = getContext(BranchController);
const fileIdSelection = getContext(FileIdSelection);
const branchStore = getContextStore(VirtualBranch);
const baseBranchService = getContext(BaseBranchService);
const project = getContext(Project);
const prService = getForgePrService();
const listingService = getForgeListingService();
const branch = $derived($branchStore);

const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
const defaultBranchWidthRem = persisted<number>(24, 'defaulBranchWidth' + project.id);
const laneWidthKey = 'laneWidth_';
let lastPush = $state<Date | undefined>();

const laneWidthKey = 'laneWidth_';
let laneWidth: number | undefined = $state();

let rsViewport = $state<HTMLElement>();

const branchHasFiles = $derived(branch.files !== undefined && branch.files.length > 0);
const branchHasNoCommits = $derived(branch.commits !== undefined && branch.commits.length === 0);

Expand All @@ -58,6 +69,7 @@

let scrollEndVisible = $state(true);
let isPushingCommits = $state(false);
let isMergingSeries = $state(false);

const { upstreamPatches, branchPatches, hasConflicts } = $derived.by(() => {
let hasConflicts = false;
Expand All @@ -84,8 +96,6 @@
return false;
});

const listingService = getForgeListingService();

async function push() {
isPushingCommits = true;
try {
Expand All @@ -96,6 +106,53 @@
isPushingCommits = false;
}
}

async function checkMergeable() {
const seriesMergeResponse = await Promise.allSettled(
branch.validSeries.map((series) => {
if (!series.prNumber) return Promise.reject();

const detailedPr = $prService?.get(series.prNumber);
return detailedPr;
})
);

return seriesMergeResponse.every((s) => {
if (s.status === 'fulfilled' && s.value) {
return s.value.mergeable === true;
}
return false;
});
}

let canMergeAll = $derived(checkMergeable());

async function mergeAll(method: MergeMethod) {
isMergingSeries = true;
try {
const topBranch = branch.validSeries[0];

if (topBranch?.prNumber && $prService) {
// TODO: Figure out default base branch of repo
await $prService.updateBase(topBranch.prNumber, 'main');
await $prService.merge(method, topBranch.prNumber);
await baseBranchService.fetchFromRemotes();
toasts.success('Stack Merged Successfully');

await Promise.all([
$prService?.prMonitor(topBranch.prNumber).refresh(),
$listingService?.refresh(),
vbranchService.refresh(),
baseBranchService.refresh()
]);
}
} catch (e) {
console.error(e);
showError('Failed to merge PR', e);
} finally {
isMergingSeries = false;
}
}
</script>

{#if $isLaneCollapsed}
Expand Down Expand Up @@ -161,6 +218,7 @@
<div
class="lane-branches__action"
class:scroll-end-visible={scrollEndVisible}
class:can-merge-all={canMergeAll}
use:intersectionObserver={{
callback: (entry) => {
if (entry?.isIntersecting) {
Expand Down Expand Up @@ -195,6 +253,40 @@
</Button>
</div>
{/if}

{#await canMergeAll then isMergeable}
{#if isMergeable}
<div
class="lane-branches__action"
class:scroll-end-visible={scrollEndVisible}
class:can-merge-all={canMergeAll}
use:intersectionObserver={{
callback: (entry) => {
if (entry?.isIntersecting) {
scrollEndVisible = false;
} else {
scrollEndVisible = true;
}
},
options: {
root: null,
rootMargin: `-100% 0px 0px 0px`,
threshold: 0
}
}}
>
<MergeButton
style="neutral"
kind="solid"
wide
projectId={project.id}
tooltip="Merge all possible branches"
loading={isMergingSeries}
onclick={mergeAll}
/>
</div>
{/if}
{/await}
</ScrollableContainer>
<div class="divider-line">
{#if rsViewport}
Expand Down Expand Up @@ -245,6 +337,10 @@
bottom: 0;
transition: background-color var(--transition-fast);

&:global(.can-merge-all > button:not(:last-child)) {
margin-bottom: 8px;
}

&:after {
content: '';
display: block;
Expand Down
Loading