mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-19 07:32:22 +03:00
Simplify GitHub service a little
- this branch will be force pushed again
This commit is contained in:
parent
229b9b9bc9
commit
5d927abfcc
@ -1,7 +1,7 @@
|
||||
import { capture } from '$lib/analytics/posthog';
|
||||
import { CombinedBranch } from '$lib/branches/types';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { startWith, switchMap } from 'rxjs/operators';
|
||||
import { shareReplay, startWith, switchMap } from 'rxjs/operators';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { PullRequest } from '$lib/github/types';
|
||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
@ -20,20 +20,17 @@ export class BranchService {
|
||||
) {
|
||||
const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([]));
|
||||
const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([]));
|
||||
const prWithEmpty$ = githubService.prs$.pipe(startWith([]));
|
||||
const prWithEmpty$ = githubService.prs$;
|
||||
|
||||
this.branches$ = combineLatest([vbranchesWithEmpty$, branchesWithEmpty$, prWithEmpty$]).pipe(
|
||||
switchMap(
|
||||
([vbranches, remoteBranches, pullRequests]) =>
|
||||
new Observable<CombinedBranch[]>((observer) => {
|
||||
const contributions = mergeBranchesAndPrs(
|
||||
vbranches,
|
||||
pullRequests,
|
||||
remoteBranches || []
|
||||
);
|
||||
observer.next(contributions);
|
||||
})
|
||||
)
|
||||
switchMap(([vbranches, remoteBranches, pullRequests]) => {
|
||||
return new Observable<CombinedBranch[]>((observer) => {
|
||||
const contributions = mergeBranchesAndPrs(vbranches, pullRequests, remoteBranches || []);
|
||||
observer.next(contributions);
|
||||
observer.complete();
|
||||
});
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
import type { BaseBranch, Branch } from '$lib/vbranches/types';
|
||||
|
||||
export let project: Project;
|
||||
@ -25,6 +26,7 @@
|
||||
export let branchController: BranchController;
|
||||
export let branchService: BranchService;
|
||||
export let githubService: GitHubService;
|
||||
export let baseBranchService: BaseBranchService;
|
||||
|
||||
export let user: User | undefined;
|
||||
|
||||
@ -117,6 +119,7 @@
|
||||
{base}
|
||||
{cloud}
|
||||
{branchController}
|
||||
{baseBranchService}
|
||||
{branchService}
|
||||
branchCount={branches.filter((c) => c.active).length}
|
||||
{projectPath}
|
||||
|
@ -37,6 +37,7 @@
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
import type { BaseBranch, Branch, LocalFile, RemoteBranchData } from '$lib/vbranches/types';
|
||||
|
||||
export let branch: Branch;
|
||||
@ -52,15 +53,12 @@
|
||||
export let githubService: GitHubService;
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
export let commitBoxOpen: Writable<boolean>;
|
||||
|
||||
export let isLaneCollapsed: Persisted<boolean>;
|
||||
export let baseBranchService: BaseBranchService;
|
||||
|
||||
const aiGenEnabled = projectAiGenEnabled(project.id);
|
||||
const aiGenAutoBranchNamingEnabled = projectAiGenAutoBranchNamingEnabled(project.id);
|
||||
|
||||
let scrollViewport: HTMLElement;
|
||||
let rsViewport: HTMLElement;
|
||||
|
||||
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
||||
const defaultBranchWidthRem = persisted<number>(24, 'defaulBranchWidth' + project.id);
|
||||
const laneWidthKey = 'laneWidth_';
|
||||
@ -68,6 +66,8 @@
|
||||
|
||||
let laneWidth: number;
|
||||
let remoteBranchData: RemoteBranchData | undefined;
|
||||
let scrollViewport: HTMLElement;
|
||||
let rsViewport: HTMLElement;
|
||||
|
||||
$: upstream = branch.upstream;
|
||||
$: if (upstream) reloadRemoteBranch();
|
||||
@ -215,6 +215,7 @@
|
||||
{branch}
|
||||
{branchService}
|
||||
{githubService}
|
||||
{baseBranchService}
|
||||
{isUnapplied}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
|
@ -23,6 +23,7 @@
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
|
||||
export let branch: Branch;
|
||||
export let isUnapplied = false;
|
||||
@ -35,6 +36,7 @@
|
||||
export let user: User | undefined;
|
||||
export let projectPath: string;
|
||||
export let githubService: GitHubService;
|
||||
export let baseBranchService: BaseBranchService;
|
||||
|
||||
$: selectedOwnership = writable(Ownership.fromBranch(branch));
|
||||
$: selected = setSelected($selectedFiles, branch);
|
||||
@ -82,6 +84,7 @@
|
||||
{cloud}
|
||||
{branchController}
|
||||
{branchService}
|
||||
{baseBranchService}
|
||||
{selectedOwnership}
|
||||
bind:commitBoxOpen
|
||||
bind:isLaneCollapsed
|
||||
|
@ -19,8 +19,6 @@
|
||||
export let projectId: string;
|
||||
export let pr: PullRequest | undefined;
|
||||
|
||||
// $: prStatus$ = githubService.getStatus($pr$?.targetBranch);
|
||||
|
||||
let meatballButton: HTMLDivElement;
|
||||
let container: HTMLDivElement;
|
||||
let isApplying = false;
|
||||
|
@ -24,7 +24,6 @@
|
||||
|
||||
export const textFilter$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
||||
const githubEnabled$ = githubService.isEnabled$;
|
||||
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
||||
const height = persisted<number | undefined>(undefined, 'branchesHeight');
|
||||
|
||||
@ -149,7 +148,7 @@
|
||||
{includeStashed}
|
||||
{hideBots}
|
||||
{hideInactive}
|
||||
showPrCheckbox={$githubEnabled$}
|
||||
showPrCheckbox={githubService.isEnabled}
|
||||
on:action
|
||||
/>
|
||||
</BranchesHeader>
|
||||
|
@ -19,8 +19,7 @@
|
||||
export let hasCommits: boolean;
|
||||
|
||||
$: githubServiceState$ = githubService.getState(branch.id);
|
||||
$: githubEnabled$ = githubService.isEnabled$;
|
||||
$: pr$ = githubService.get(branch.upstreamName);
|
||||
$: pr = githubService.getPr(branch.upstreamName);
|
||||
|
||||
let isPushing: boolean;
|
||||
let isMerging: boolean;
|
||||
@ -41,7 +40,7 @@
|
||||
|
||||
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
|
||||
const opts = { ...defaultPrOpts, ...createPrOpts };
|
||||
if (!githubService.isEnabled()) {
|
||||
if (!githubService.isEnabled) {
|
||||
toast.error('Cannot create PR without GitHub credentials');
|
||||
return;
|
||||
}
|
||||
@ -62,15 +61,15 @@
|
||||
|
||||
{#if !isUnapplied && type != 'integrated'}
|
||||
<div class="actions" class:hasCommits>
|
||||
{#if $githubEnabled$ && (type == 'local' || type == 'remote')}
|
||||
{#if githubService.isEnabled && (type == 'local' || type == 'remote')}
|
||||
<PushButton
|
||||
wide
|
||||
isLoading={isPushing || $githubServiceState$?.busy}
|
||||
isPr={!!$pr$}
|
||||
isPr={!!pr}
|
||||
{type}
|
||||
{branch}
|
||||
{projectId}
|
||||
githubEnabled={$githubEnabled$}
|
||||
githubEnabled={true}
|
||||
on:trigger={async (e) => {
|
||||
try {
|
||||
if (e.detail.action == BranchAction.Pr) {
|
||||
|
@ -5,21 +5,22 @@
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import { getAuthenticated } from '$lib/github/user';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { UserService } from '$lib/stores/user';
|
||||
|
||||
export let userService: UserService;
|
||||
export let githubService: GitHubService;
|
||||
export let minimal = false;
|
||||
export let disabled = false;
|
||||
|
||||
$: user$ = userService.user$;
|
||||
|
||||
let isCheckingStatus = false;
|
||||
|
||||
let loading = false;
|
||||
let userCode = '';
|
||||
let deviceCode = '';
|
||||
let gitHubOauthModal: Modal;
|
||||
|
||||
function gitHubStartOauth() {
|
||||
initDeviceOauth().then((verification) => {
|
||||
@ -29,37 +30,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
let gitHubOauthModal: Modal;
|
||||
function gitHubOauthCheckStatus(deviceCode: string) {
|
||||
isCheckingStatus = true;
|
||||
let u = $user$;
|
||||
|
||||
checkAuthStatus({ deviceCode })
|
||||
.then(async (access_token) => {
|
||||
if (u) {
|
||||
u.github_access_token = access_token;
|
||||
u.github_username = await getAuthenticated(access_token);
|
||||
userService.setUser(u);
|
||||
|
||||
toasts.success('GitHub authenticated');
|
||||
|
||||
isCheckingStatus = false;
|
||||
gitHubOauthModal.close();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
isCheckingStatus = false;
|
||||
gitHubOauthModal.close();
|
||||
});
|
||||
async function gitHubOauthCheckStatus(deviceCode: string) {
|
||||
loading = true;
|
||||
let user = $user$;
|
||||
if (!user) return;
|
||||
try {
|
||||
const accessToken = await checkAuthStatus({ deviceCode });
|
||||
user.github_access_token = accessToken;
|
||||
// TODO: Refactor so we don't have to call this twice
|
||||
userService.setUser(user);
|
||||
user.github_username = await githubService.fetchGitHubLogin();
|
||||
userService.setUser(user);
|
||||
toasts.success('GitHub authenticated');
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toasts.success('GitHub authentication failed');
|
||||
} finally {
|
||||
loading = false;
|
||||
gitHubOauthModal.close();
|
||||
}
|
||||
}
|
||||
|
||||
function forgetGitHub(): void {
|
||||
let u = $user$;
|
||||
if (u) {
|
||||
u.github_access_token = '';
|
||||
u.github_username = '';
|
||||
userService.setUser(u);
|
||||
let user = $user$;
|
||||
if (user) {
|
||||
user.github_access_token = '';
|
||||
user.github_username = '';
|
||||
userService.setUser(user);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -149,9 +146,9 @@
|
||||
<Button
|
||||
kind="filled"
|
||||
color="primary"
|
||||
loading={isCheckingStatus}
|
||||
{loading}
|
||||
on:click={async () => {
|
||||
gitHubOauthCheckStatus(deviceCode);
|
||||
await gitHubOauthCheckStatus(deviceCode);
|
||||
}}
|
||||
>
|
||||
Check the status and close
|
||||
|
@ -5,6 +5,7 @@
|
||||
import DecorativeSplitView from '$lib/components/DecorativeSplitView.svelte';
|
||||
import type { AuthService } from '$lib/backend/auth';
|
||||
import type { Project, ProjectService } from '$lib/backend/projects';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { UserService } from '$lib/stores/user';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
@ -17,6 +18,7 @@
|
||||
export let project: Project;
|
||||
export let userService: UserService;
|
||||
export let remoteBranches: { name: string }[];
|
||||
export let githubService: GitHubService;
|
||||
|
||||
$: user$ = userService.user$;
|
||||
|
||||
@ -63,6 +65,7 @@
|
||||
projectId={project.id}
|
||||
{userService}
|
||||
{remoteBranches}
|
||||
{githubService}
|
||||
on:branchSelected={(e) => {
|
||||
selectedBranch = e.detail;
|
||||
}}
|
||||
|
@ -10,11 +10,13 @@
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { UserService } from '$lib/stores/user';
|
||||
|
||||
export let userService: UserService;
|
||||
export let projectId: string;
|
||||
export let remoteBranches: { name: string }[];
|
||||
export let githubService: GitHubService;
|
||||
|
||||
$: user$ = userService.user$;
|
||||
|
||||
@ -165,7 +167,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !$user$?.github_access_token}
|
||||
<GithubIntegration {userService} disabled={!$user$} />
|
||||
<GithubIntegration {userService} {githubService} disabled={!$user$} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SetupFeature>
|
||||
|
@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import IconButton from './IconButton.svelte';
|
||||
import MergeButton from './MergeButton.svelte';
|
||||
import Tag, { type TagColor } from './Tag.svelte';
|
||||
import ViewPrContextMenu from '$lib/components/ViewPrContextMenu.svelte';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import { onDestroy } from 'svelte';
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { ChecksStatus } from '$lib/github/types';
|
||||
import type { ChecksStatus, DetailedPullRequest } from '$lib/github/types';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
import type { Branch } from '$lib/vbranches/types';
|
||||
import type iconsJson from '../icons/icons.json';
|
||||
|
||||
@ -17,12 +20,33 @@
|
||||
export let githubService: GitHubService;
|
||||
export let projectId: string;
|
||||
export let isUnapplied = false;
|
||||
export let baseBranchService: BaseBranchService;
|
||||
|
||||
let isMerging = false;
|
||||
let isFetching = false;
|
||||
let checksStatus: ChecksStatus | null | undefined;
|
||||
let isFetchingDetails = false;
|
||||
let checksStatus: ChecksStatus | null | undefined = undefined;
|
||||
let detailedPr: DetailedPullRequest | undefined;
|
||||
|
||||
$: pr$ = githubService.get(branch.upstreamName);
|
||||
$: pr$ = githubService.getPr$(branch.upstreamName);
|
||||
$: if (branch && pr$) {
|
||||
isFetchingDetails = true;
|
||||
sleep(1000).then(() => {
|
||||
updateDetailedPullRequest($pr$?.targetBranch, false);
|
||||
fetchChecks();
|
||||
});
|
||||
}
|
||||
|
||||
async function updateDetailedPullRequest(targetBranch: string | undefined, skipCache: boolean) {
|
||||
isFetchingDetails = true;
|
||||
try {
|
||||
detailedPr = await githubService.getDetailedPullRequest(targetBranch, skipCache);
|
||||
} catch {
|
||||
toasts.error('Failed to fetch PR details');
|
||||
} finally {
|
||||
isFetchingDetails = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateContextMenu(copyablePrUrl: string) {
|
||||
if (popupMenu) popupMenu.$destroy();
|
||||
@ -32,7 +56,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchPrStatus() {
|
||||
async function fetchChecks() {
|
||||
isFetching = true;
|
||||
try {
|
||||
checksStatus = await githubService.checks($pr$?.targetBranch);
|
||||
@ -69,17 +93,16 @@
|
||||
// Stop polling for status.
|
||||
return;
|
||||
}
|
||||
setTimeout(() => fetchPrStatus(), timeUntilUdate * 1000);
|
||||
setTimeout(() => fetchChecks(), timeUntilUdate * 1000);
|
||||
}
|
||||
|
||||
$: checksIcon = getChecksIcon(checksStatus);
|
||||
$: checksIcon = getChecksIcon(checksStatus, isFetching);
|
||||
$: checksColor = getChecksColor(checksStatus);
|
||||
$: checksText = getChecksText(checksStatus);
|
||||
$: statusIcon = getStatusIcon();
|
||||
$: statusColor = getStatusColor();
|
||||
$: statusLabel = getPrStatusLabel();
|
||||
|
||||
$: if ($pr$) fetchPrStatus();
|
||||
|
||||
// TODO: Refactor away the code duplication in the following functions
|
||||
function getChecksColor(status: ChecksStatus): TagColor | undefined {
|
||||
if (!status) return;
|
||||
@ -91,9 +114,12 @@
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
function getChecksIcon(status: ChecksStatus): keyof typeof iconsJson | undefined {
|
||||
if (isFetching) return 'spinner';
|
||||
if (!status) return;
|
||||
function getChecksIcon(
|
||||
status: ChecksStatus,
|
||||
fetching: boolean
|
||||
): keyof typeof iconsJson | undefined {
|
||||
if (status === null) return;
|
||||
if (fetching || !status) return 'spinner';
|
||||
if (!status.hasChecks) return;
|
||||
if (status.error) return 'error-small';
|
||||
if (status.completed) {
|
||||
@ -103,8 +129,8 @@
|
||||
return 'spinner';
|
||||
}
|
||||
|
||||
function statusToTagText(status: ChecksStatus | undefined): string | undefined {
|
||||
if (!status) return;
|
||||
function getChecksText(status: ChecksStatus | undefined | null): string | undefined {
|
||||
if (!status) return 'Checks';
|
||||
if (!status.hasChecks) return 'No checks';
|
||||
if (status.error) return 'error';
|
||||
if (status.completed) {
|
||||
@ -144,10 +170,21 @@
|
||||
</script>
|
||||
|
||||
{#if $pr$?.htmlUrl}
|
||||
{@const pr = $pr$}
|
||||
<div class="card pr-card">
|
||||
<div class="floating-button">
|
||||
<IconButton
|
||||
icon="update-small"
|
||||
loading={isFetchingDetails}
|
||||
on:click={async () => {
|
||||
updateDetailedPullRequest(pr?.targetBranch, true);
|
||||
fetchChecks();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="pr-title text-base-13 font-semibold">
|
||||
<span style="color: var(--clr-theme-scale-ntrl-50)">PR #{$pr$.number}:</span>
|
||||
{$pr$.title}
|
||||
<span style="color: var(--clr-theme-scale-ntrl-50)">PR #{pr.number}:</span>
|
||||
{pr.title}
|
||||
</div>
|
||||
<div class="pr-tags">
|
||||
<Tag
|
||||
@ -165,10 +202,10 @@
|
||||
filled={checksIcon == 'success-small'}
|
||||
clickable
|
||||
verticalOrientation={isLaneCollapsed}
|
||||
on:mousedown={fetchPrStatus}
|
||||
on:mousedown={fetchChecks}
|
||||
help="Checks status"
|
||||
>
|
||||
{statusToTagText(checksStatus)}
|
||||
{checksText}
|
||||
</Tag>
|
||||
{:else}
|
||||
<Tag
|
||||
@ -187,7 +224,7 @@
|
||||
shrinkable
|
||||
verticalOrientation={isLaneCollapsed}
|
||||
on:mousedown={(e) => {
|
||||
const url = $pr$?.htmlUrl;
|
||||
const url = pr?.htmlUrl;
|
||||
if (url) openExternalUrl(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -209,28 +246,28 @@
|
||||
determining "no checks will run for this PR" such that we can show the merge button
|
||||
immediately.
|
||||
-->
|
||||
{#if $pr$}
|
||||
{#if pr}
|
||||
<div class="pr-actions">
|
||||
<MergeButton
|
||||
{projectId}
|
||||
wide
|
||||
disabled={isFetching || isUnapplied || !$pr$ || (!!checksStatus && !checksStatus.success)}
|
||||
disabled={isFetching || isUnapplied || !pr || detailedPr?.mergeableState == 'blocked'}
|
||||
loading={isMerging}
|
||||
help="Merge pull request and refresh"
|
||||
on:click={async (e) => {
|
||||
if (!pr) return;
|
||||
isMerging = true;
|
||||
const method = e.detail.method;
|
||||
try {
|
||||
if ($pr$) {
|
||||
await githubService.merge($pr$.number, method);
|
||||
}
|
||||
await githubService.merge(pr.number, method);
|
||||
} catch {
|
||||
// TODO: Should we show the error from GitHub?
|
||||
toasts.error('Failed to merge pull request');
|
||||
} finally {
|
||||
isMerging = false;
|
||||
await fetchPrStatus();
|
||||
await fetchChecks();
|
||||
await branchService.reloadVirtualBranches();
|
||||
baseBranchService.reload();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -241,6 +278,7 @@
|
||||
|
||||
<style lang="postcss">
|
||||
.pr-card {
|
||||
position: relative;
|
||||
padding: var(--space-14);
|
||||
}
|
||||
|
||||
@ -260,4 +298,10 @@
|
||||
margin-top: var(--space-14);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
position: absolute;
|
||||
right: var(--space-6);
|
||||
top: var(--space-6);
|
||||
}
|
||||
</style>
|
||||
|
@ -23,7 +23,7 @@
|
||||
e.stopPropagation();
|
||||
if (cloudEnabled) syncToCloud(projectId); // don't wait for this
|
||||
await baseBranchService.fetchFromTarget();
|
||||
if (githubService.isEnabled()) {
|
||||
if (githubService.isEnabled) {
|
||||
await githubService.reload();
|
||||
}
|
||||
}}
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
export function newClient(authToken: string) {
|
||||
return new Octokit({
|
||||
auth: authToken,
|
||||
userAgent: 'GitButler Client',
|
||||
baseUrl: 'https://api.github.com'
|
||||
});
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
import { newClient } from '$lib/github/client';
|
||||
import {
|
||||
type PullRequest,
|
||||
type GitHubIntegrationContext,
|
||||
ghResponseToInstance,
|
||||
type ChecksStatus,
|
||||
MergeMethod
|
||||
MergeMethod,
|
||||
type DetailedPullRequest,
|
||||
parseGitHubDetailedPullRequest as parsePullRequestResponse
|
||||
} from '$lib/github/types';
|
||||
import { showToast, type Toast } from '$lib/notifications/toasts';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import lscache from 'lscache';
|
||||
import posthog from 'posthog-js';
|
||||
import {
|
||||
@ -25,95 +26,100 @@ import {
|
||||
} from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
distinct,
|
||||
delay,
|
||||
finalize,
|
||||
map,
|
||||
retry,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
timeout
|
||||
} from 'rxjs/operators';
|
||||
import type { UserService } from '$lib/stores/user';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
export type PrAction = 'creating_pr';
|
||||
export type PrState = { busy: boolean; branchId: string; action?: PrAction };
|
||||
export type PrCacheKey = { value: Promise<DetailedPullRequest | undefined>; fetchedAt: Date };
|
||||
|
||||
export class GitHubService {
|
||||
prs$: Observable<PullRequest[]>;
|
||||
error$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
readonly prs$ = new BehaviorSubject<PullRequest[]>([]);
|
||||
|
||||
private prCache = new Map<string, PrCacheKey>();
|
||||
|
||||
private error$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
private stateMap = new Map<string, BehaviorSubject<PrState>>();
|
||||
private reload$ = new BehaviorSubject<{ skipCache: boolean } | undefined>(undefined);
|
||||
private fresh$ = new Subject<void>();
|
||||
|
||||
private ctx$: Observable<GitHubIntegrationContext | undefined>;
|
||||
private octokit$: Observable<Octokit | undefined>;
|
||||
|
||||
// For use with user initiated actions like merging
|
||||
private ctx: GitHubIntegrationContext | undefined;
|
||||
private octokit: Octokit | undefined;
|
||||
|
||||
private enabled = false;
|
||||
private _octokit: Octokit | undefined;
|
||||
private _repo: string | undefined;
|
||||
private _owner: string | undefined;
|
||||
|
||||
constructor(
|
||||
userService: UserService,
|
||||
private baseBranchService: BaseBranchService
|
||||
accessToken$: Observable<string | undefined>,
|
||||
remoteUrl$: Observable<string | undefined>
|
||||
) {
|
||||
// A few things will cause the baseBranch to update, so we filter for distinct
|
||||
// changes to the remoteUrl.
|
||||
const distinctUrl$ = baseBranchService.base$.pipe(distinct((ctx) => ctx?.remoteUrl));
|
||||
combineLatest([accessToken$, remoteUrl$])
|
||||
.pipe(
|
||||
tap(([accessToken, remoteUrl]) => {
|
||||
if (!remoteUrl?.includes('github') || !accessToken) {
|
||||
return of();
|
||||
}
|
||||
this._octokit = new Octokit({
|
||||
auth: accessToken,
|
||||
userAgent: 'GitButler Client',
|
||||
baseUrl: 'https://api.github.com'
|
||||
});
|
||||
const [owner, repo] = remoteUrl.split('.git')[0].split(/\/|:/).slice(-2);
|
||||
this._repo = repo;
|
||||
this._owner = owner;
|
||||
}),
|
||||
shareReplay(1)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.ctx$ = combineLatest([userService.user$, distinctUrl$]).pipe(
|
||||
switchMap(([user, baseBranch]) => {
|
||||
const remoteUrl = baseBranch?.remoteUrl;
|
||||
const authToken = user?.github_access_token;
|
||||
const username = user?.github_username || '';
|
||||
if (!remoteUrl || !remoteUrl.includes('github') || !authToken) {
|
||||
this.enabled = false;
|
||||
return of();
|
||||
}
|
||||
this.enabled = true;
|
||||
const [owner, repo] = remoteUrl.split('.git')[0].split(/\/|:/).slice(-2);
|
||||
return of({ authToken, owner, repo, username });
|
||||
}),
|
||||
distinct((val) => JSON.stringify(val)),
|
||||
tap((ctx) => (this.ctx = ctx)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
// Create a github client
|
||||
this.octokit$ = this.ctx$.pipe(
|
||||
map((ctx) => (ctx ? newClient(ctx.authToken) : undefined)),
|
||||
tap((octokit) => (this.octokit = octokit)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
// Load pull requests
|
||||
this.prs$ = combineLatest([this.ctx$, this.octokit$, this.reload$]).pipe(
|
||||
tap(() => this.error$.next(undefined)),
|
||||
switchMap(([ctx, octokit, reload]) => {
|
||||
if (!ctx || !octokit) return EMPTY;
|
||||
const prs = loadPrs(ctx, octokit, !!reload?.skipCache);
|
||||
this.fresh$.next();
|
||||
return prs;
|
||||
}),
|
||||
shareReplay(1),
|
||||
catchError((err) => {
|
||||
console.log(err);
|
||||
this.error$.next(err);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
combineLatest([this.reload$, accessToken$, remoteUrl$])
|
||||
.pipe(
|
||||
tap(() => this.error$.next(undefined)),
|
||||
switchMap(([reload]) => {
|
||||
if (!this.isEnabled) return EMPTY;
|
||||
const prs = this.fetchPrs(!!reload?.skipCache);
|
||||
this.fresh$.next();
|
||||
return prs;
|
||||
}),
|
||||
shareReplay(1),
|
||||
catchError((err) => {
|
||||
console.error(err);
|
||||
toasts.error('Failed to load pull requests');
|
||||
this.error$.next(err);
|
||||
return of([]);
|
||||
}),
|
||||
tap((prs) => this.prs$.next(prs))
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
get isEnabled(): boolean {
|
||||
return !!this._octokit;
|
||||
}
|
||||
|
||||
get isEnabled$(): Observable<boolean> {
|
||||
return this.octokit$.pipe(map((octokit) => !!octokit));
|
||||
get octokit(): Octokit {
|
||||
if (!this._octokit) throw new Error('No GitHub client available');
|
||||
return this._octokit;
|
||||
}
|
||||
|
||||
get repo(): string {
|
||||
if (!this._repo) throw new Error('No repo name specified');
|
||||
return this._repo;
|
||||
}
|
||||
|
||||
get owner(): string {
|
||||
if (!this._owner) throw new Error('No owner name specified');
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
get prs() {
|
||||
return this.prs$.value;
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
@ -132,8 +138,74 @@ export class GitHubService {
|
||||
return await fresh;
|
||||
}
|
||||
|
||||
get(branch: string | undefined): Observable<PullRequest | undefined> | undefined {
|
||||
fetchPrs(skipCache: boolean): Observable<PullRequest[]> {
|
||||
return new Observable<PullRequest[]>((subscriber) => {
|
||||
const key = this.owner + '/' + this.repo;
|
||||
|
||||
if (!skipCache) {
|
||||
const cachedRsp = lscache.get(key);
|
||||
if (cachedRsp) subscriber.next(cachedRsp.data.map(ghResponseToInstance));
|
||||
}
|
||||
|
||||
try {
|
||||
this.octokit.rest.pulls
|
||||
.list({
|
||||
owner: this.owner,
|
||||
repo: this.repo
|
||||
})
|
||||
.then((rsp) => {
|
||||
lscache.set(key, rsp, 1440); // 1 day ttl
|
||||
subscriber.next(rsp.data.map(ghResponseToInstance));
|
||||
})
|
||||
.catch((e) => subscriber.error(e));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getDetailedPullRequest(
|
||||
branch: string | undefined,
|
||||
skipCache: boolean
|
||||
): Promise<DetailedPullRequest | undefined> {
|
||||
if (!branch) return;
|
||||
|
||||
// We should remove this cache when `list_virtual_branches` no longer triggers
|
||||
// immedate updates on the subscription.
|
||||
const cacheHit = this.prCache.get(branch);
|
||||
if (cacheHit && !skipCache) {
|
||||
if (new Date().getTime() - cacheHit.fetchedAt.getTime() < 1000 * 5) {
|
||||
return await cacheHit.value;
|
||||
}
|
||||
}
|
||||
|
||||
const pr = this.getPr(branch);
|
||||
if (!pr) {
|
||||
toasts.error('Failed to get pull request data'); // TODO: Notify user
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await this.octokit.pulls.get({
|
||||
owner: this.owner,
|
||||
repo: this.repo,
|
||||
pull_number: pr.number,
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
});
|
||||
const detailedPr = Promise.resolve(parsePullRequestResponse(resp.data));
|
||||
|
||||
if (detailedPr) this.prCache.set(branch, { value: detailedPr, fetchedAt: new Date() });
|
||||
return await detailedPr;
|
||||
}
|
||||
|
||||
getPr(branch: string | undefined): PullRequest | undefined {
|
||||
if (!branch) return;
|
||||
return this.prs?.find((pr) => pr.targetBranch == branch);
|
||||
}
|
||||
|
||||
getPr$(branch: string | undefined): Observable<PullRequest | undefined> {
|
||||
if (!branch) return of(undefined);
|
||||
return this.prs$.pipe(map((prs) => prs.find((pr) => pr.targetBranch == branch)));
|
||||
}
|
||||
|
||||
@ -165,48 +237,37 @@ export class GitHubService {
|
||||
upstreamName: string,
|
||||
draft: boolean
|
||||
): Promise<{ pr: PullRequest } | { err: string | { message: string; help: string } }> {
|
||||
if (!this.enabled) {
|
||||
throw "Can't create PR when service not enabled";
|
||||
}
|
||||
this.setBusy('creating_pr', branchId);
|
||||
return firstValueFrom(
|
||||
// We have to wrap with defer becasue using `async` functions with operators
|
||||
// create a promise that will stay rejected when rejected.
|
||||
defer(() =>
|
||||
combineLatest([this.octokit$, this.ctx$]).pipe(
|
||||
switchMap(async ([octokit, ctx]) => {
|
||||
if (!octokit || !ctx) {
|
||||
throw "Can't create PR without credentials";
|
||||
}
|
||||
try {
|
||||
const rsp = await octokit.rest.pulls.create({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
head: upstreamName,
|
||||
base,
|
||||
title,
|
||||
body,
|
||||
draft
|
||||
});
|
||||
await this.reload();
|
||||
posthog.capture('PR Successful');
|
||||
return { pr: ghResponseToInstance(rsp.data) };
|
||||
} catch (err: any) {
|
||||
const toast = mapErrorToToast(err);
|
||||
if (toast) {
|
||||
// TODO: This needs disambiguation, not the same as `toasts.error`
|
||||
// Show toast with rich content
|
||||
showToast(toast);
|
||||
// Handled errors should not be retried
|
||||
return { err };
|
||||
} else {
|
||||
// Rethrow so that error is retried
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
).pipe(
|
||||
defer(async () => {
|
||||
try {
|
||||
const rsp = await this.octokit.rest.pulls.create({
|
||||
owner: this.owner,
|
||||
repo: this.repo,
|
||||
head: upstreamName,
|
||||
base,
|
||||
title,
|
||||
body,
|
||||
draft
|
||||
});
|
||||
posthog.capture('PR Successful');
|
||||
return { pr: ghResponseToInstance(rsp.data) };
|
||||
} catch (err: any) {
|
||||
const toast = mapErrorToToast(err);
|
||||
if (toast) {
|
||||
// TODO: This needs disambiguation, not the same as `toasts.error`
|
||||
// Show toast with rich content
|
||||
showToast(toast);
|
||||
// Handled errors should not be retried
|
||||
return { err };
|
||||
} else {
|
||||
// Rethrow so that error is retried
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}).pipe(
|
||||
retry({
|
||||
count: 2,
|
||||
delay: 500
|
||||
@ -248,7 +309,13 @@ export class GitHubService {
|
||||
}
|
||||
return throwError(() => err.message);
|
||||
}),
|
||||
tap(() => this.setIdle(branchId))
|
||||
tap(() => this.setIdle(branchId)),
|
||||
// Makes finalize happen after first and only result
|
||||
take(1),
|
||||
// Wait for GitHub to become eventually consistent. If we refresh too quickly then
|
||||
// then it'll show as mergeable and no checks even if checks are present.
|
||||
delay(1000),
|
||||
finalize(async () => await this.reload())
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -260,7 +327,8 @@ export class GitHubService {
|
||||
// the pull request has been created.
|
||||
let resp: Awaited<ReturnType<typeof this.fetchChecksWithRetries>>;
|
||||
try {
|
||||
resp = await this.fetchChecksWithRetries(ref, 5, 2000);
|
||||
// resp = await this.fetchChecksWithRetries(ref, 5, 2000);
|
||||
resp = await this.fetchChecks(ref);
|
||||
} catch (err: any) {
|
||||
return { error: err };
|
||||
}
|
||||
@ -291,6 +359,17 @@ export class GitHubService {
|
||||
};
|
||||
}
|
||||
|
||||
async fetchChecks(ref: string) {
|
||||
return await this.octokit.checks.listForRef({
|
||||
owner: this.owner,
|
||||
repo: this.repo,
|
||||
ref: ref,
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchChecksWithRetries(ref: string, retries: number, delayMs: number) {
|
||||
let resp = await this.fetchChecks(ref);
|
||||
let retried = 0;
|
||||
@ -303,61 +382,28 @@ export class GitHubService {
|
||||
return resp;
|
||||
}
|
||||
|
||||
async fetchChecks(ref: string) {
|
||||
if (!this.octokit || !this.ctx) throw 'GitHub client not available';
|
||||
return await this.octokit.checks.listForRef({
|
||||
owner: this.ctx.owner,
|
||||
repo: this.ctx.repo,
|
||||
ref: ref,
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async merge(pullNumber: number, method: MergeMethod) {
|
||||
if (!this.octokit || !this.ctx) return;
|
||||
try {
|
||||
await this.octokit.pulls.merge({
|
||||
owner: this.ctx.owner,
|
||||
repo: this.ctx.repo,
|
||||
return await this.octokit.pulls.merge({
|
||||
owner: this.owner,
|
||||
repo: this.repo,
|
||||
pull_number: pullNumber,
|
||||
merge_method: method
|
||||
});
|
||||
await this.baseBranchService.fetchFromTarget();
|
||||
} finally {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadPrs(
|
||||
ctx: GitHubIntegrationContext,
|
||||
octokit: Octokit,
|
||||
skipCache: boolean
|
||||
): Observable<PullRequest[]> {
|
||||
return new Observable<PullRequest[]>((subscriber) => {
|
||||
const key = ctx.owner + '/' + ctx.repo;
|
||||
|
||||
if (!skipCache) {
|
||||
const cachedRsp = lscache.get(key);
|
||||
if (cachedRsp) subscriber.next(cachedRsp.data.map(ghResponseToInstance));
|
||||
}
|
||||
|
||||
async fetchGitHubLogin(): Promise<string> {
|
||||
try {
|
||||
octokit.rest.pulls
|
||||
.list({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo
|
||||
})
|
||||
.then((rsp) => {
|
||||
lscache.set(key, rsp, 1440); // 1 day ttl
|
||||
subscriber.next(rsp.data.map(ghResponseToInstance));
|
||||
});
|
||||
const rsp = await this.octokit.users.getAuthenticated();
|
||||
return rsp.data.login;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,13 +1,6 @@
|
||||
import type { Author } from '$lib/vbranches/types';
|
||||
import type { RestEndpointMethodTypes } from '@octokit/rest';
|
||||
|
||||
export interface GitHubIntegrationContext {
|
||||
authToken: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface Label {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
@ -30,6 +23,36 @@ export interface PullRequest {
|
||||
closedAt?: Date;
|
||||
}
|
||||
|
||||
export type DetailedGitHubPullRequest = RestEndpointMethodTypes['pulls']['get']['response']['data'];
|
||||
|
||||
export interface DetailedPullRequest {
|
||||
targetBranch: string;
|
||||
createdAt: Date;
|
||||
mergedAt?: Date;
|
||||
closedAt?: Date;
|
||||
htmlUrl: string;
|
||||
mergeable: boolean;
|
||||
mergeableState: string;
|
||||
rebaseable: boolean;
|
||||
squashable: boolean;
|
||||
}
|
||||
|
||||
export function parseGitHubDetailedPullRequest(
|
||||
data: DetailedGitHubPullRequest
|
||||
): DetailedPullRequest {
|
||||
return {
|
||||
targetBranch: data.base.ref,
|
||||
htmlUrl: data.html_url,
|
||||
createdAt: new Date(data.created_at),
|
||||
mergedAt: data.merged_at ? new Date(data.merged_at) : undefined,
|
||||
closedAt: data.closed_at ? new Date(data.closed_at) : undefined,
|
||||
mergeable: !!data.mergeable,
|
||||
mergeableState: data.mergeable_state,
|
||||
rebaseable: !!data.rebaseable,
|
||||
squashable: !!data.mergeable // Enabled whenever merge is enabled
|
||||
};
|
||||
}
|
||||
|
||||
export type ChecksStatus =
|
||||
| {
|
||||
startedAt?: Date;
|
||||
@ -46,13 +69,11 @@ export function ghResponseToInstance(
|
||||
| RestEndpointMethodTypes['pulls']['create']['response']['data']
|
||||
| RestEndpointMethodTypes['pulls']['list']['response']['data'][number]
|
||||
): PullRequest {
|
||||
const labels: Label[] = pr.labels.map((label) => {
|
||||
return {
|
||||
name: label.name,
|
||||
description: label.description || undefined,
|
||||
color: label.color
|
||||
};
|
||||
});
|
||||
const labels: Label[] = pr.labels.map((label) => ({
|
||||
name: label.name,
|
||||
description: label.description || undefined,
|
||||
color: label.color
|
||||
}));
|
||||
|
||||
return {
|
||||
htmlUrl: pr.html_url,
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { newClient } from '$lib/github/client';
|
||||
|
||||
export async function getAuthenticated(authTokenstring: string): Promise<string> {
|
||||
const octokit = newClient(authTokenstring);
|
||||
try {
|
||||
const rsp = await octokit.users.getAuthenticated();
|
||||
return rsp.data.login;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
@ -4,15 +4,15 @@ import { getCloudApiClient, type User } from '$lib/backend/cloud';
|
||||
import { invoke } from '$lib/backend/ipc';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import { BehaviorSubject, Observable, Subject, merge, shareReplay } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subject, distinct, map, merge, shareReplay } from 'rxjs';
|
||||
|
||||
export class UserService {
|
||||
private cloud = getCloudApiClient();
|
||||
private readonly cloud = getCloudApiClient();
|
||||
|
||||
reset$ = new Subject<User | undefined>();
|
||||
loading$ = new BehaviorSubject(false);
|
||||
readonly reset$ = new Subject<User | undefined>();
|
||||
readonly loading$ = new BehaviorSubject(false);
|
||||
|
||||
user$ = merge(
|
||||
readonly user$ = merge(
|
||||
new Observable<User | undefined>((subscriber) => {
|
||||
invoke<User | undefined>('get_user').then((user) => {
|
||||
if (user) {
|
||||
@ -25,6 +25,11 @@ export class UserService {
|
||||
this.reset$
|
||||
).pipe(shareReplay(1));
|
||||
|
||||
readonly accessToken$ = this.user$.pipe(
|
||||
map((user) => user?.github_access_token),
|
||||
distinct()
|
||||
);
|
||||
|
||||
constructor() {}
|
||||
|
||||
async setUser(user: User | undefined) {
|
||||
|
@ -109,21 +109,24 @@ function subscribeToVirtualBranches(projectId: string, callback: (branches: Bran
|
||||
}
|
||||
|
||||
export class BaseBranchService {
|
||||
base$: Observable<BaseBranch | null | undefined>;
|
||||
busy$ = new BehaviorSubject(false);
|
||||
error$ = new BehaviorSubject<any>(undefined);
|
||||
private reload$ = new BehaviorSubject<void>(undefined);
|
||||
readonly base$: Observable<BaseBranch | null | undefined>;
|
||||
readonly busy$ = new BehaviorSubject(false);
|
||||
readonly error$ = new BehaviorSubject<any>(undefined);
|
||||
private readonly reload$ = new BehaviorSubject<void>(undefined);
|
||||
|
||||
constructor(
|
||||
private projectId: string,
|
||||
private readonly projectId: string,
|
||||
readonly remoteUrl$: BehaviorSubject<string | undefined>,
|
||||
fetches$: Observable<unknown>,
|
||||
head$: Observable<string>
|
||||
readonly head$: Observable<string>
|
||||
) {
|
||||
this.base$ = combineLatest([fetches$, head$, this.reload$]).pipe(
|
||||
debounceTime(100),
|
||||
switchMap(async () => {
|
||||
this.busy$.next(true);
|
||||
return await getBaseBranch({ projectId });
|
||||
const baseBranch = await getBaseBranch({ projectId });
|
||||
if (baseBranch?.remoteUrl) this.remoteUrl$.next(baseBranch.remoteUrl);
|
||||
return baseBranch;
|
||||
}),
|
||||
tap(() => this.busy$.next(false)),
|
||||
// Start with undefined to prevent delay in updating $baseBranch$ value in
|
||||
|
@ -5,9 +5,10 @@ import { getCloudApiClient } from '$lib/backend/cloud';
|
||||
import { ProjectService } from '$lib/backend/projects';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import lscache from 'lscache';
|
||||
import { config } from 'rxjs';
|
||||
import { BehaviorSubject, config } from 'rxjs';
|
||||
|
||||
// call on startup so we don't accumulate old items
|
||||
lscache.flushExpired();
|
||||
@ -19,8 +20,6 @@ export const ssr = false;
|
||||
export const prerender = false;
|
||||
export const csr = true;
|
||||
|
||||
let homeDir: () => Promise<string>;
|
||||
|
||||
export async function load({ fetch: realFetch }: { fetch: typeof fetch }) {
|
||||
appErrorReportingEnabled()
|
||||
.onDisk()
|
||||
@ -32,19 +31,37 @@ export async function load({ fetch: realFetch }: { fetch: typeof fetch }) {
|
||||
.then((enabled) => {
|
||||
if (enabled) initPostHog();
|
||||
});
|
||||
const userService = new UserService();
|
||||
|
||||
// TODO: Find a workaround to avoid this dynamic import
|
||||
// https://github.com/sveltejs/kit/issues/905
|
||||
homeDir = (await import('@tauri-apps/api/path')).homeDir;
|
||||
const defaultPath = await homeDir();
|
||||
const defaultPath = await (await import('@tauri-apps/api/path')).homeDir();
|
||||
|
||||
const authService = new AuthService();
|
||||
const cloud = getCloudApiClient({ fetch: realFetch });
|
||||
const projectService = new ProjectService(defaultPath);
|
||||
const updaterService = new UpdaterService();
|
||||
const userService = new UserService();
|
||||
const user$ = userService.user$;
|
||||
|
||||
// We're declaring a remoteUrl$ observable here that is written to by `BaseBranchService`. This
|
||||
// is a bit awkard, but `GitHubService` needs to be available at the root scoped layout.ts, such
|
||||
// that we can perform actions related to GitHub that do not depend on repo information.
|
||||
// We should evaluate whether or not to split this service into two separate services. That
|
||||
// way we would not need `remoteUrl$` for the non-repo service, and therefore the other one
|
||||
// could easily get an observable of the remote url from `BaseBranchService`.
|
||||
const remoteUrl$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
const githubService = new GitHubService(userService.accessToken$, remoteUrl$);
|
||||
|
||||
return {
|
||||
authService: new AuthService(),
|
||||
projectService: new ProjectService(defaultPath),
|
||||
cloud: getCloudApiClient({ fetch: realFetch }),
|
||||
updaterService: new UpdaterService(),
|
||||
authService,
|
||||
cloud,
|
||||
githubService,
|
||||
projectService,
|
||||
updaterService,
|
||||
userService,
|
||||
user$: userService.user$
|
||||
|
||||
// These observables are provided for convenience
|
||||
remoteUrl$,
|
||||
user$
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { syncToCloud } from '$lib/backend/cloud';
|
||||
import { handleMenuActions } from '$lib/backend/menuActions';
|
||||
import { ProjectService } from '$lib/backend/projects';
|
||||
import FullscreenLoading from '$lib/components/FullscreenLoading.svelte';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
|
||||
@ -8,12 +9,13 @@
|
||||
import { subscribe as menuSubscribe } from '$lib/menu';
|
||||
import * as hotkeys from '$lib/utils/hotkeys';
|
||||
import { unsubscribe } from '$lib/utils/random';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount, setContext } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
setContext(ProjectService, data.projectService);
|
||||
$: projectService = data.projectService;
|
||||
$: branchController = data.branchController;
|
||||
$: githubService = data.githubService;
|
||||
@ -51,16 +53,20 @@
|
||||
goto(`/${projectId}/setup`, { replaceState: true });
|
||||
}
|
||||
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
onMount(() => {
|
||||
return unsubscribe(
|
||||
menuSubscribe(data.projectId),
|
||||
hotkeys.on('Meta+Shift+S', () => syncToCloud(projectId))
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearFetchInterval();
|
||||
});
|
||||
|
||||
$: if (data) {
|
||||
setContext('hello', data.projectId);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !$project$}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BranchService } from '$lib/branches/service';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { getFetchNotifications } from '$lib/stores/fetches';
|
||||
import { getHeads } from '$lib/stores/head';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
@ -10,13 +9,21 @@ import { map } from 'rxjs';
|
||||
export const prerender = false;
|
||||
|
||||
export async function load({ params, parent }) {
|
||||
const { authService, projectService, userService } = await parent();
|
||||
// prettier-ignore
|
||||
const {
|
||||
authService,
|
||||
githubService,
|
||||
projectService,
|
||||
remoteUrl$,
|
||||
} = await parent();
|
||||
|
||||
const projectId = params.projectId;
|
||||
const project$ = projectService.getProject(projectId);
|
||||
const fetches$ = getFetchNotifications(projectId);
|
||||
const heads$ = getHeads(projectId);
|
||||
const gbBranchActive$ = heads$.pipe(map((head) => head == 'gitbutler/integration'));
|
||||
const baseBranchService = new BaseBranchService(projectId, fetches$, heads$);
|
||||
|
||||
const baseBranchService = new BaseBranchService(projectId, remoteUrl$, fetches$, heads$);
|
||||
const vbranchService = new VirtualBranchService(projectId, gbBranchActive$);
|
||||
|
||||
const remoteBranchService = new RemoteBranchService(
|
||||
@ -31,8 +38,6 @@ export async function load({ params, parent }) {
|
||||
remoteBranchService,
|
||||
baseBranchService
|
||||
);
|
||||
|
||||
const githubService = new GitHubService(userService, baseBranchService);
|
||||
const branchService = new BranchService(
|
||||
vbranchService,
|
||||
remoteBranchService,
|
||||
@ -40,19 +45,18 @@ export async function load({ params, parent }) {
|
||||
branchController
|
||||
);
|
||||
|
||||
const user$ = userService.user$;
|
||||
|
||||
return {
|
||||
projectId,
|
||||
authService,
|
||||
branchController,
|
||||
baseBranchService,
|
||||
githubService,
|
||||
vbranchService,
|
||||
remoteBranchService,
|
||||
user$,
|
||||
project$,
|
||||
branchController,
|
||||
branchService,
|
||||
githubService,
|
||||
projectId,
|
||||
remoteBranchService,
|
||||
vbranchService,
|
||||
|
||||
// These observables are provided for convenience
|
||||
project$,
|
||||
gbBranchActive$
|
||||
};
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
$: project$ = data.project$;
|
||||
$: activeBranches$ = vbranchService.activeBranches$;
|
||||
$: error$ = vbranchService.branchesError$;
|
||||
$: githubEnabled$ = githubService.isEnabled$;
|
||||
|
||||
let viewport: HTMLDivElement;
|
||||
let contents: HTMLDivElement;
|
||||
@ -30,7 +29,7 @@
|
||||
function shouldShowHttpsWarning() {
|
||||
if (httpsWarningBannerDismissed) return false;
|
||||
if (!$base$?.remoteUrl.startsWith('https')) return false;
|
||||
if ($base$?.remoteUrl.includes('github.com') && $githubEnabled$) return false;
|
||||
if ($base$?.remoteUrl.includes('github.com') && githubService.isEnabled) return false;
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
@ -54,6 +53,7 @@
|
||||
<Board
|
||||
{branchController}
|
||||
{branchService}
|
||||
{baseBranchService}
|
||||
project={$project$}
|
||||
{cloud}
|
||||
base={$base$}
|
||||
|
@ -14,7 +14,7 @@
|
||||
$: base$ = data.baseBranchService.base$;
|
||||
|
||||
$: branch = $branches$?.find((b) => b.sha == $page.params.sha);
|
||||
$: pr$ = githubService.get(branch?.displayName);
|
||||
$: pr = githubService.getPr(branch?.displayName);
|
||||
</script>
|
||||
|
||||
{#if $error$}
|
||||
@ -27,7 +27,7 @@
|
||||
projectPath={$project$.path}
|
||||
project={$project$}
|
||||
base={$base$}
|
||||
pr={$pr$}
|
||||
{pr}
|
||||
{branchController}
|
||||
{branch}
|
||||
/>
|
||||
|
@ -14,6 +14,7 @@
|
||||
$: authService = data.authService;
|
||||
$: projectId = data.projectId;
|
||||
$: project$ = data.project$;
|
||||
$: githubService = data.githubService;
|
||||
</script>
|
||||
|
||||
{#await getRemoteBranches(projectId)}
|
||||
@ -37,6 +38,7 @@
|
||||
{branchController}
|
||||
{userService}
|
||||
{remoteBranches}
|
||||
{githubService}
|
||||
/>
|
||||
{/if}
|
||||
{:catch}
|
||||
|
@ -38,6 +38,7 @@
|
||||
{branch}
|
||||
{branchController}
|
||||
{branchService}
|
||||
{baseBranchService}
|
||||
base={$baseBranch$}
|
||||
{cloud}
|
||||
project={$project$}
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const { cloud, user$, userService, authService } = data;
|
||||
$: ({ cloud, user$, userService, authService, githubService } = data);
|
||||
const fileTypes = ['image/jpeg', 'image/png'];
|
||||
|
||||
// TODO: Maybe break these into components?
|
||||
@ -324,7 +324,7 @@
|
||||
{:else if currentSection === 'integrations'}
|
||||
<ContentWrapper title="Integrations">
|
||||
{#if $user$}
|
||||
<GithubIntegration {userService} />
|
||||
<GithubIntegration {userService} {githubService} />
|
||||
{/if}
|
||||
</ContentWrapper>
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user