Merged origin/master into lane-style-updates

This commit is contained in:
Pavel Laptev 2024-01-24 11:08:01 +01:00 committed by GitButler
commit 6999c94586
11 changed files with 114 additions and 183 deletions

View File

@ -6,16 +6,18 @@ import { map, startWith, switchMap } from 'rxjs/operators';
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
import type { GitHubService } from '$lib/github/service';
import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache';
import type { Transaction } from '@sentry/sveltekit';
import { capture } from '$lib/analytics/posthog';
export class BranchService {
public branches$: Observable<CombinedBranch[]>;
constructor(
vbranchService: VirtualBranchService,
private vbranchService: VirtualBranchService,
remoteBranchService: RemoteBranchService,
githubService: GitHubService
private githubService: GitHubService
) {
const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([]));
const vbranchesWithEmpty$ = vbranchService.activeBranches$.pipe(startWith([]));
const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([]));
const prWithEmpty$ = githubService.prs$.pipe(startWith([]));
@ -34,6 +36,50 @@ export class BranchService {
map((branches) => branches.filter((b) => !b.vbranch || b.vbranch.active))
);
}
async createPr(
branch: Branch,
baseBranch: string,
sentryTxn: Transaction
): Promise<PullRequest | undefined> {
// Using this mutable variable while investigating why branch variable
// does not seem to update reliably.
// TODO: This needs to be fixed and removed.
let newBranch: Branch | undefined;
// Push if local commits
if (branch.commits.some((c) => !c.isRemote)) {
const pushBranchSpan = sentryTxn.startChild({ op: 'branch_push' });
newBranch = await this.vbranchService.pushBranch(branch.id, branch.requiresForce);
pushBranchSpan.finish();
} else {
newBranch = branch;
}
if (!newBranch) {
const err = 'branch push failed';
capture(err, { upstream: branch.upstreamName });
throw err;
}
if (!newBranch.upstreamName) {
throw 'Cannot create PR without remote branch name';
}
const createPrSpan = sentryTxn.startChild({ op: 'pr_api_create' });
try {
return await this.githubService.createPullRequest(
baseBranch,
newBranch.name,
newBranch.notes,
newBranch.id,
newBranch.upstreamName
);
} finally {
createPrSpan.finish();
}
}
}
function mergeBranchesAndPrs(

View File

@ -30,7 +30,10 @@ export class VirtualBranchService {
private reload$ = new BehaviorSubject<void>(undefined);
private fresh$ = new Subject<void>();
constructor(projectId: string, gbBranchActive$: Observable<boolean>) {
constructor(
private projectId: string,
gbBranchActive$: Observable<boolean>
) {
this.branches$ = this.reload$.pipe(
switchMap(() => gbBranchActive$),
switchMap((gbBranchActive) =>
@ -98,6 +101,24 @@ export class VirtualBranchService {
)
);
}
async pushBranch(branchId: string, withForce: boolean): Promise<Branch | undefined> {
try {
await invoke<void>('push_virtual_branch', {
projectId: this.projectId,
branchId,
withForce
});
await this.reload();
return await this.getById(branchId);
} catch (err: any) {
if (err.code === 'errors.git.authentication') {
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
} else {
toasts.error(`Failed to push branch: ${err.message}`);
}
}
}
}
function subscribeToVirtualBranches(projectId: string, callback: (branches: Branch[]) => void) {

View File

@ -5,6 +5,6 @@
$: projectId = $page.params.projectId;
$: if (projectId) {
goto(`/${projectId}/base`, { replaceState: true });
goto(`/${projectId}/board`, { replaceState: true });
}
</script>

View File

@ -15,6 +15,7 @@
$: base$ = baseBranchService.base$;
$: user$ = data.user$;
$: githubService = data.githubService;
$: branchService = data.branchService;
$: project$ = data.project$;
$: branches = vbranchService.branches$;
@ -58,6 +59,7 @@
<div class="scroll-contents" bind:this={contents}>
<Board
{branchController}
{branchService}
project={$project$}
{cloud}
base={$base$}

View File

@ -1,15 +1,18 @@
<script lang="ts" async="true">
import BranchLane from '../components/BranchLane.svelte';
import NewBranchDropZone from './NewBranchDropZone.svelte';
import type { User, getCloudApiClient } from '$lib/backend/cloud';
import type { BaseBranch, Branch } from '$lib/vbranches/types';
import type { BranchController } from '$lib/vbranches/branchController';
import type { User, getCloudApiClient } from '$lib/backend/cloud';
import { open } from '@tauri-apps/api/shell';
import type { BranchService } from '$lib/branches/service';
import type { GitHubService } from '$lib/github/service';
import { cloneWithRotation } from '$lib/utils/draggable';
import type { Project } from '$lib/backend/projects';
import Icon from '$lib/icons/Icon.svelte';
import { cloneWithRotation } from '$lib/utils/draggable';
import { open } from '@tauri-apps/api/shell';
import NewBranchDropZone from './NewBranchDropZone.svelte';
import BranchLane from '../components/BranchLane.svelte';
import ImgThemed from '$lib/components/ImgThemed.svelte';
import Icon from '$lib/icons/Icon.svelte';
export let project: Project;
export let projectPath: string;
@ -21,6 +24,7 @@
export let cloud: ReturnType<typeof getCloudApiClient>;
export let branchController: BranchController;
export let branchService: BranchService;
export let githubService: GitHubService;
export let user: User | undefined;
@ -111,6 +115,7 @@
{base}
{cloud}
{branchController}
{branchService}
branchCount={branches.filter((c) => c.active).length}
{projectPath}
{user}

View File

@ -30,6 +30,7 @@
import DropzoneOverlay from './DropzoneOverlay.svelte';
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
import type { BranchService } from '$lib/branches/service';
export let branch: Branch;
export let readonly = false;
@ -37,6 +38,7 @@
export let base: BaseBranch | undefined | null;
export let cloud: ReturnType<typeof getCloudApiClient>;
export let branchController: BranchController;
export let branchService: BranchService;
export let branchCount = 1;
export let user: User | undefined;
export let selectedFiles: Writable<File[]>;
@ -256,6 +258,7 @@
{project}
{githubService}
{branchController}
{branchService}
{branchCount}
{readonly}
/>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import type { Project } from '$lib/backend/projects';
import type { BranchService } from '$lib/branches/service';
import type { GitHubService } from '$lib/github/service';
import type { BranchController } from '$lib/vbranches/branchController';
import type { BaseBranch, Branch } from '$lib/vbranches/types';
@ -10,6 +11,7 @@
export let base: BaseBranch | undefined | null;
export let githubService: GitHubService;
export let branchController: BranchController;
export let branchService: BranchService;
export let readonly: boolean;
export let branchCount: number;
</script>
@ -20,6 +22,7 @@
{base}
{project}
{branchController}
{branchService}
{branchCount}
{githubService}
{readonly}
@ -30,6 +33,7 @@
{base}
{project}
{branchController}
{branchService}
{githubService}
{readonly}
type="local"
@ -39,6 +43,7 @@
{base}
{project}
{branchController}
{branchService}
{githubService}
{readonly}
type="remote"
@ -48,6 +53,7 @@
{base}
{project}
{branchController}
{branchService}
{githubService}
{readonly}
type="integrated"

View File

@ -8,6 +8,7 @@
import { Ownership } from '$lib/vbranches/ownership';
import type { GitHubService } from '$lib/github/service';
import type { Project } from '$lib/backend/projects';
import type { BranchService } from '$lib/branches/service';
export let branch: Branch;
export let readonly = false;
@ -15,6 +16,7 @@
export let base: BaseBranch | undefined | null;
export let cloud: ReturnType<typeof getCloudApiClient>;
export let branchController: BranchController;
export let branchService: BranchService;
export let branchCount = 1;
export let user: User | undefined;
export let projectPath: string;
@ -43,6 +45,7 @@
{base}
{cloud}
{branchController}
{branchService}
{selectedOwnership}
bind:commitBoxOpen
{branchCount}

View File

@ -6,6 +6,7 @@
import CommitListHeader from './CommitListHeader.svelte';
import CommitListFooter from './CommitListFooter.svelte';
import type { Project } from '$lib/backend/projects';
import type { BranchService } from '$lib/branches/service';
export let branch: Branch;
export let base: BaseBranch | undefined | null;
@ -13,6 +14,7 @@
export let branchController: BranchController;
export let type: CommitStatus;
export let githubService: GitHubService;
export let branchService: BranchService;
export let readonly: boolean;
export let branchCount: number = 0;
@ -58,6 +60,7 @@
</div>{/if}
<CommitListFooter
{branchController}
{branchService}
{branch}
{githubService}
{type}

View File

@ -7,12 +7,13 @@
import type { GitHubService } from '$lib/github/service';
import toast from 'svelte-french-toast';
import { startTransaction } from '@sentry/sveltekit';
import { capture } from '$lib/analytics/posthog';
import type { BranchService } from '$lib/branches/service';
export let branch: Branch;
export let type: CommitStatus;
export let readonly: boolean;
export let branchController: BranchController;
export let branchService: BranchService;
export let githubService: GitHubService;
export let base: BaseBranch | undefined | null;
export let projectId: string;
@ -31,9 +32,6 @@
}
async function createPr(): Promise<PullRequest | undefined> {
isPushing = true;
const txn = startTransaction({ name: 'pull_request_create' });
if (!githubService.isEnabled()) {
toast.error('Cannot create PR without GitHub credentials');
return;
@ -44,43 +42,19 @@
return;
}
let newBranch: Branch | undefined;
// Push if local commits
if (branch.commits.some((c) => !c.isRemote)) {
const pushBranchSpan = txn.startChild({ op: 'branch_push' });
newBranch = await branchController.pushBranch(branch.id, branch.requiresForce);
pushBranchSpan.finish();
} else {
newBranch = branch;
// Sentry transaction for measuring pr creation latency
const sentryTxn = startTransaction({ name: 'pull_request_create' });
isPushing = true;
try {
return await branchService.createPr(branch, base.shortName, sentryTxn);
} catch (err) {
toast.error(err as string);
console.error(err);
} finally {
sentryTxn.finish();
isPushing = false;
}
if (newBranch?.upstreamName != branch.upstreamName) {
capture('branch push mismatch', { new: newBranch?.upstreamName, old: branch.upstreamName });
}
if (!newBranch) {
console.error('Branch push failed');
return;
}
if (!newBranch.upstreamName) {
toast.error('Cannot create PR without remote branch name');
return;
}
const createPrSpan = txn.startChild({ op: 'pr_api_create' });
const resp = await githubService.createPullRequest(
base.shortName,
newBranch.name,
newBranch.notes,
newBranch.id,
newBranch.upstreamName
);
createPrSpan.finish();
txn.finish();
isPushing = false;
return resp;
}
</script>

View File

@ -1,132 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import { page } from '$app/stores';
import BranchLane from '../../components/BranchLane.svelte';
import Button from '$lib/components/Button.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import type { Branch } from '$lib/vbranches/types';
import Modal from '$lib/components/Modal.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { goto } from '$app/navigation';
export let data: PageData;
let applyConflictedModal: Modal;
let deleteBranchModal: Modal;
$: projectId = data.projectId;
$: user$ = data.user$;
$: cloud = data.cloud;
$: project$ = data.project$;
$: branchController = data.branchController;
$: vbranchService = data.vbranchService;
$: baseBranchService = data.baseBranchService;
$: baseBranch$ = baseBranchService.base$;
$: branches$ = vbranchService.branches$;
$: error$ = vbranchService.branchesError$;
$: branch = $branches$?.find((b) => b.id == $page.params.branchId);
$: githubService = data.githubService;
function applyBranch(branch: Branch) {
if (!branch.isMergeable) {
applyConflictedModal.show(branch);
} else {
branchController.applyBranch(branch.id);
}
}
</script>
<div class="flex h-full flex-col p-3">
{#if $error$}
<p>Error...</p>
{:else if !$branches$}
<p>Loading...</p>
{:else if branch}
<div class="flex items-center justify-between pb-2">
<Button
kind="outlined"
color="primary"
on:click={() => {
branch && applyBranch(branch);
goto(`/${projectId}/board`);
}}
>
<span class="purple"> Apply </span>
</Button>
<div class="flex items-center">
{#await branch.isMergeable then isMergeable}
{#if !isMergeable}
<Tooltip
timeoutMilliseconds={100}
label="Applying this branch will add merge conflict markers that you will have to resolve"
>
<div
class="flex cursor-default select-none rounded bg-yellow-300 px-2 py-0.5 dark:bg-yellow-800"
>
Conflicts with Applied Branches
</div>
</Tooltip>
{/if}
{/await}
<IconButton
icon="cross"
title="delete branch"
on:click={() => deleteBranchModal.show(branch)}
/>
</div>
</div>
<div class="h-full">
<BranchLane
{branch}
{branchController}
base={$baseBranch$}
{cloud}
project={$project$}
readonly={true}
user={$user$}
projectPath={$project$.path}
{githubService}
/>
</div>
{:else}
<p>Branch no longer exists</p>
{/if}
</div>
<Modal width="small" title="Merge conflicts" bind:this={applyConflictedModal}>
<p>Applying this branch will introduce merge conflicts.</p>
<svelte:fragment slot="controls" let:item let:close>
<Button kind="outlined" color="neutral" on:click={close}>Cancel</Button>
<Button
color="primary"
on:click={() => {
branchController.applyBranch(item.id);
close();
goto(`/${projectId}/board`);
}}
>
Update
</Button>
</svelte:fragment>
</Modal>
<Modal width="small" title="Delete branch" bind:this={deleteBranchModal} let:item>
<div>
Deleting <code>{item.name}</code> cannot be undone.
</div>
<svelte:fragment slot="controls" let:close let:item>
<Button kind="outlined" color="neutral" on:click={close}>Cancel</Button>
<Button
color="error"
on:click={() => {
branchController.deleteBranch(item.id);
close();
goto(`/${projectId}/board`);
}}
>
Delete
</Button>
</svelte:fragment>
</Modal>