mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-01 12:26:02 +03:00
Merge pull request #3880 from gitbutlerapp/remote-branch-display
Add a "apply from remote" flow
This commit is contained in:
commit
b71a30258c
@ -116,7 +116,7 @@ function mergeBranchesAndPrs(
|
||||
if (remoteBranches) {
|
||||
contributions.push(
|
||||
...remoteBranches
|
||||
.filter((rb) => !contributions.some((cb) => rb.sha == cb.sha))
|
||||
.filter((rb) => !contributions.some((cb) => rb.sha == cb.upstreamSha))
|
||||
.map((rb) => {
|
||||
const pr = pullRequests?.find((pr) => pr.sha == rb.sha);
|
||||
return new CombinedBranch({ remoteBranch: rb, pr });
|
||||
@ -128,7 +128,7 @@ function mergeBranchesAndPrs(
|
||||
if (pullRequests) {
|
||||
contributions.push(
|
||||
...pullRequests
|
||||
.filter((pr) => !contributions.some((cb) => pr.sha == cb.sha))
|
||||
.filter((pr) => !contributions.some((cb) => pr.sha == cb.upstreamSha))
|
||||
.map((pr) => {
|
||||
return new CombinedBranch({ pr });
|
||||
})
|
||||
|
@ -20,8 +20,14 @@ export class CombinedBranch {
|
||||
this.pr = pr;
|
||||
}
|
||||
|
||||
get sha(): string {
|
||||
return this.pr?.sha || this.remoteBranch?.sha || this.vbranch?.head || 'unknown';
|
||||
get upstreamSha(): string {
|
||||
return (
|
||||
this.pr?.sha ||
|
||||
this.remoteBranch?.sha ||
|
||||
this.vbranch?.upstream?.sha ||
|
||||
this.vbranch?.head ||
|
||||
'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
|
@ -31,7 +31,7 @@
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
|
||||
$: branch = $branchStore;
|
||||
$: pr$ = githubService.getPr$(branch.upstreamName);
|
||||
$: pr$ = githubService.getPr$(branch.upstream?.sha || branch.head);
|
||||
$: hasPullRequest = branch.upstreamName && $pr$;
|
||||
|
||||
let meatballButton: HTMLDivElement;
|
||||
|
@ -15,11 +15,11 @@
|
||||
import { Branch } from '$lib/vbranches/types';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
import type { ChecksStatus, DetailedPullRequest } from '$lib/github/types';
|
||||
import type { ComponentColor } from '$lib/vbranches/types';
|
||||
import type { MessageStyle } from './InfoMessage.svelte';
|
||||
import type iconsJson from '../icons/icons.json';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
type StatusInfo = {
|
||||
text: string;
|
||||
@ -44,13 +44,7 @@
|
||||
let checksStatus: ChecksStatus | null | undefined = undefined;
|
||||
let lastDetailsFetch: Readable<string> | undefined;
|
||||
|
||||
// We only want to call `.getPr$()` when the upstream name changes, rather
|
||||
// than each time the branch object updates.
|
||||
let distinctUpstreamName = derived<Readable<Branch>, string | undefined>(branch, (b, set) => {
|
||||
set(b.upstreamName);
|
||||
});
|
||||
|
||||
$: pr$ = githubService.getPr$($distinctUpstreamName).pipe(
|
||||
$: pr$ = githubService.getPr$($branch.upstream?.sha || $branch.head).pipe(
|
||||
// Only emit a new objcect if the modified timestamp has changed.
|
||||
distinctUntilChanged((prev, curr) => {
|
||||
return prev?.modifiedAt.getTime() === curr?.modifiedAt.getTime();
|
||||
@ -63,15 +57,16 @@
|
||||
$: prStatusInfo = getPrStatusInfo(detailedPr);
|
||||
|
||||
async function updateDetailsAndChecks() {
|
||||
if (!isFetchingDetails) await updateDetailedPullRequest($pr$?.targetBranch, true);
|
||||
if (!$pr$) return;
|
||||
if (!isFetchingDetails) await updateDetailedPullRequest($pr$.sha, true);
|
||||
if (!isFetchingChecks) await fetchChecks();
|
||||
}
|
||||
|
||||
async function updateDetailedPullRequest(targetBranch: string | undefined, skipCache: boolean) {
|
||||
async function updateDetailedPullRequest(targetBranchSha: string, skipCache: boolean) {
|
||||
detailsError = undefined;
|
||||
isFetchingDetails = true;
|
||||
try {
|
||||
detailedPr = await githubService.getDetailedPr(targetBranch, skipCache);
|
||||
detailedPr = await githubService.getDetailedPr(targetBranchSha, skipCache);
|
||||
mergeableState = detailedPr?.mergeableState;
|
||||
lastDetailsFetch = createTimeAgoStore(new Date(), true);
|
||||
} catch (err: any) {
|
||||
@ -347,7 +342,7 @@
|
||||
toasts.error('Failed to merge pull request');
|
||||
} finally {
|
||||
isMerging = false;
|
||||
baseBranchService.fetchFromTarget();
|
||||
baseBranchService.fetchFromRemotes();
|
||||
branchService.reloadVirtualBranches();
|
||||
updateDetailsAndChecks();
|
||||
}
|
||||
|
@ -1,71 +1,143 @@
|
||||
<script lang="ts">
|
||||
// This is always displayed in the context of not having a cooresponding vbranch or remote
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Link from '$lib/components/Link.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import Tag from '$lib/components/Tag.svelte';
|
||||
import TextBox from '$lib/components/TextBox.svelte';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { BaseBranchService } from '$lib/vbranches/baseBranch';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||
import { marked } from 'marked';
|
||||
import type { PullRequest } from '$lib/github/types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let pullrequest: PullRequest | undefined;
|
||||
export let pullrequest: PullRequest;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const project = getContext(Project);
|
||||
const remotesService = getContext(RemotesService);
|
||||
const baseBranchService = getContext(BaseBranchService);
|
||||
const virtualBranchService = getContext(VirtualBranchService);
|
||||
|
||||
let remoteName = structuredClone(pullrequest.repoName) || '';
|
||||
let createRemoteModal: Modal | undefined;
|
||||
|
||||
let loading = false;
|
||||
|
||||
function closeModal() {
|
||||
remoteName = structuredClone(pullrequest.repoName) || '';
|
||||
createRemoteModal?.close();
|
||||
}
|
||||
|
||||
async function createRemoteAndBranch() {
|
||||
if (!pullrequest.sshUrl) return;
|
||||
|
||||
const remotes = await remotesService.remotes(project.id);
|
||||
if (remotes.includes(remoteName)) {
|
||||
toasts.error('Remote already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await remotesService.addRemote(project.id, remoteName, pullrequest.sshUrl);
|
||||
await baseBranchService.fetchFromRemotes();
|
||||
await branchController.createvBranchFromBranch(
|
||||
`refs/remotes/${remoteName}/${pullrequest.targetBranch}`
|
||||
);
|
||||
await virtualBranchService.reload();
|
||||
const vbranch = await virtualBranchService.getByUpstreamSha(pullrequest.sha);
|
||||
|
||||
// This is a little absurd, but it makes it soundly typed
|
||||
if (!vbranch) {
|
||||
goto(`/${project.id}/board`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Active seems to be a more reliable metric to determine whether to go to the branch page
|
||||
if (vbranch.active) {
|
||||
goto(`/${project.id}/board`);
|
||||
} else {
|
||||
goto(`/${project.id}/stashed/${vbranch.id}`);
|
||||
}
|
||||
|
||||
createRemoteModal?.close();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if pullrequest != undefined}
|
||||
<div class="wrapper">
|
||||
<div class="card">
|
||||
<div class="card__header text-base-body-14 text-semibold">
|
||||
<h2 class="text-base-14 text-semibold">
|
||||
{pullrequest.title}
|
||||
<span class="card__title-pr">
|
||||
<Link target="_blank" rel="noreferrer" href={pullrequest.htmlUrl}>
|
||||
#{pullrequest.number}
|
||||
</Link>
|
||||
</span>
|
||||
</h2>
|
||||
{#if pullrequest.draft}
|
||||
<Tag style="neutral" icon="draft-pr-small">Draft</Tag>
|
||||
{:else}
|
||||
<Tag style="success" kind="solid" icon="pr-small">Open</Tag>
|
||||
{/if}
|
||||
</div>
|
||||
<Modal width="small" bind:this={createRemoteModal}>
|
||||
<p class="text-base-15 fork-notice">
|
||||
In order to apply a branch from a fork, GitButler must first add a remote.
|
||||
</p>
|
||||
<TextBox label="Choose a remote name" bind:value={remoteName}></TextBox>
|
||||
<svelte:fragment slot="controls">
|
||||
<Button style="ghost" kind="solid" on:click={closeModal}>Cancel</Button>
|
||||
<Button style="pop" kind="solid" grow on:click={createRemoteAndBranch} {loading}>Confirm</Button
|
||||
>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<div class="card__content">
|
||||
<div class="text-base-13">
|
||||
<span class="text-bold">
|
||||
{pullrequest.author?.name}
|
||||
</span>
|
||||
wants to merge into
|
||||
<span class="code-string">
|
||||
{pullrequest.sourceBranch}
|
||||
</span>
|
||||
from
|
||||
<span class="code-string">
|
||||
{pullrequest.targetBranch}
|
||||
</span>
|
||||
</div>
|
||||
{#if pullrequest.body}
|
||||
<div class="markdown">
|
||||
{@html marked.parse(pullrequest.body)}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="wrapper">
|
||||
<div class="card">
|
||||
<div class="card__header text-base-body-14 text-semibold">
|
||||
<h2 class="text-base-14 text-semibold">
|
||||
{pullrequest.title}
|
||||
<span class="card__title-pr">
|
||||
<Link target="_blank" rel="noreferrer" href={pullrequest.htmlUrl}>
|
||||
#{pullrequest.number}
|
||||
</Link>
|
||||
</span>
|
||||
</h2>
|
||||
{#if pullrequest.draft}
|
||||
<Tag style="neutral" icon="draft-pr-small">Draft</Tag>
|
||||
{:else}
|
||||
<Tag style="success" kind="solid" icon="pr-small">Open</Tag>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card__content">
|
||||
<div class="text-base-13">
|
||||
<span class="text-bold">
|
||||
{pullrequest.author?.name}
|
||||
</span>
|
||||
wants to merge into
|
||||
<span class="code-string">
|
||||
{pullrequest.sourceBranch}
|
||||
</span>
|
||||
from
|
||||
<span class="code-string">
|
||||
{pullrequest.targetBranch}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card__footer">
|
||||
{#if pullrequest.body}
|
||||
<div class="markdown">
|
||||
{@html marked.parse(pullrequest.body)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card__footer">
|
||||
{#if !pullrequest.repoName && !pullrequest.sshUrl}
|
||||
<p>Cannot apply pull request due to insufficient information</p>
|
||||
{:else}
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
help="Does not create a commit. Can be toggled."
|
||||
on:click={async () =>
|
||||
await (pullrequest &&
|
||||
branchController.createvBranchFromBranch(
|
||||
'refs/remotes/origin/' + pullrequest.targetBranch
|
||||
))}>Apply</Button
|
||||
on:click={async () => createRemoteModal?.show()}>Apply from fork</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.wrapper {
|
||||
@ -81,4 +153,8 @@
|
||||
opacity: 0.4;
|
||||
margin-left: var(--size-4);
|
||||
}
|
||||
|
||||
.fork-notice {
|
||||
margin-bottom: var(--size-8);
|
||||
}
|
||||
</style>
|
||||
|
@ -23,7 +23,7 @@
|
||||
on:mousedown={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await baseBranchService.fetchFromTarget('modal');
|
||||
await baseBranchService.fetchFromRemotes('modal');
|
||||
if (githubService.isEnabled) {
|
||||
await githubService.reload();
|
||||
}
|
||||
|
@ -173,21 +173,19 @@ export class GitHubService {
|
||||
}
|
||||
|
||||
async getDetailedPr(
|
||||
branch: string | undefined,
|
||||
branchSha: string,
|
||||
skipCache: boolean
|
||||
): Promise<DetailedPullRequest | undefined> {
|
||||
if (!branch) return;
|
||||
|
||||
const cachedPr = !skipCache && this.prCache.get(branch);
|
||||
const cachedPr = !skipCache && this.prCache.get(branchSha);
|
||||
if (cachedPr) {
|
||||
const cacheTimeMs = 2 * 1000;
|
||||
const age = new Date().getTime() - cachedPr.fetchedAt.getTime();
|
||||
if (age < cacheTimeMs) return cachedPr.value;
|
||||
}
|
||||
|
||||
const prNumber = this.getListedPr(branch)?.number;
|
||||
const prNumber = this.getListedPr(branchSha)?.number;
|
||||
if (!prNumber) {
|
||||
toasts.error('No pull request number for branch ' + branch);
|
||||
toasts.error('No pull request number for branch ' + branchSha);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -209,7 +207,7 @@ export class GitHubService {
|
||||
attempt++;
|
||||
try {
|
||||
pr = await request();
|
||||
if (pr) this.prCache.set(branch, { value: pr, fetchedAt: new Date() });
|
||||
if (pr) this.prCache.set(branchSha, { value: pr, fetchedAt: new Date() });
|
||||
return pr;
|
||||
} catch (err: any) {
|
||||
if (err.status != 422) throw err;
|
||||
@ -229,18 +227,12 @@ export class GitHubService {
|
||||
if (checkSuites.some((suite) => suite.status != 'completed')) return true;
|
||||
}
|
||||
|
||||
getListedPr(branch: string | undefined): PullRequest | undefined {
|
||||
if (!branch) return;
|
||||
return this.prs?.find((pr) => pr.targetBranch == branch);
|
||||
getListedPr(branchSha: string): PullRequest | undefined {
|
||||
return this.prs?.find((pr) => pr.sha == branchSha);
|
||||
}
|
||||
|
||||
getPr$(branch: string | undefined): Observable<PullRequest | undefined> {
|
||||
if (!branch) return of(undefined);
|
||||
return this.prs$.pipe(map((prs) => prs.find((pr) => pr.targetBranch == branch)));
|
||||
}
|
||||
|
||||
hasPr(branch: string): boolean {
|
||||
return !!this.prs$.value.find((pr) => pr.targetBranch == branch);
|
||||
getPr$(branchSha: string): Observable<PullRequest | undefined> {
|
||||
return this.prs$.pipe(map((prs) => prs.find((pr) => pr.sha == branchSha)));
|
||||
}
|
||||
|
||||
/* TODO: Figure out a way to cleanup old behavior subjects */
|
||||
|
@ -22,6 +22,8 @@ export interface PullRequest {
|
||||
modifiedAt: Date;
|
||||
mergedAt?: Date;
|
||||
closedAt?: Date;
|
||||
repoName?: string;
|
||||
sshUrl?: string;
|
||||
}
|
||||
|
||||
export type DetailedGitHubPullRequest = RestEndpointMethodTypes['pulls']['get']['response']['data'];
|
||||
@ -102,7 +104,9 @@ export function ghResponseToInstance(
|
||||
sourceBranch: pr.base.ref,
|
||||
sha: pr.head.sha,
|
||||
mergedAt: pr.merged_at ? new Date(pr.merged_at) : undefined,
|
||||
closedAt: pr.closed_at ? new Date(pr.closed_at) : undefined
|
||||
closedAt: pr.closed_at ? new Date(pr.closed_at) : undefined,
|
||||
repoName: pr.head.repo?.full_name,
|
||||
sshUrl: pr.head.repo?.ssh_url
|
||||
};
|
||||
}
|
||||
|
||||
|
16
app/src/lib/remotes/service.ts
Normal file
16
app/src/lib/remotes/service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { invoke } from '$lib/backend/ipc';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
|
||||
export class RemotesService {
|
||||
async remotes(projectId: string) {
|
||||
return await invoke<string[]>('list_remotes', { projectId });
|
||||
}
|
||||
|
||||
async addRemote(projectId: string, name: string, url: string) {
|
||||
try {
|
||||
await invoke('add_remote', { projectId, name, url });
|
||||
} catch (e) {
|
||||
showError('Failed to add remote', e);
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ export function mockTauri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cmd === 'fetch_from_target') {
|
||||
if (cmd === 'fetch_from_remotes') {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -63,12 +63,12 @@ export class BaseBranchService {
|
||||
[this.base, this.error] = observableToStore(this.base$, this.reload$);
|
||||
}
|
||||
|
||||
async fetchFromTarget(action: string | undefined = undefined) {
|
||||
async fetchFromRemotes(action: string | undefined = undefined) {
|
||||
this.busy$.next(true);
|
||||
try {
|
||||
// Note that we expect the back end to emit new fetches event, and therefore
|
||||
// trigger a base branch reload. It feels a bit awkward and should be improved.
|
||||
await invoke<void>('fetch_from_target', {
|
||||
await invoke<void>('fetch_from_remotes', {
|
||||
projectId: this.projectId,
|
||||
action: action || 'auto'
|
||||
});
|
||||
@ -94,7 +94,7 @@ export class BaseBranchService {
|
||||
branch,
|
||||
pushRemote
|
||||
});
|
||||
await this.fetchFromTarget();
|
||||
await this.fetchFromRemotes();
|
||||
}
|
||||
|
||||
reload() {
|
||||
|
@ -139,6 +139,15 @@ export class VirtualBranchService {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async getByUpstreamSha(upstreamSha: string) {
|
||||
return await firstValueFrom(
|
||||
this.branches$.pipe(
|
||||
timeout(10000),
|
||||
map((branches) => branches?.find((b) => b.upstream?.sha == upstreamSha))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToVirtualBranches(projectId: string, callback: (branches: Branch[]) => void) {
|
||||
|
@ -13,6 +13,7 @@
|
||||
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import ToastController from '$lib/notifications/ToastController.svelte';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import { SETTINGS, loadUserSettings } from '$lib/settings/userSettings';
|
||||
import { User, UserService } from '$lib/stores/user';
|
||||
import * as events from '$lib/utils/events';
|
||||
@ -42,6 +43,7 @@
|
||||
setContext(AuthService, data.authService);
|
||||
setContext(HttpClient, data.cloud);
|
||||
setContext(User, data.userService.user);
|
||||
setContext(RemotesService, data.remotesService);
|
||||
|
||||
let shareIssueModal: ShareIssueModal;
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { ProjectService } from '$lib/backend/projects';
|
||||
import { PromptService } from '$lib/backend/prompt';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { mockTauri } from '$lib/testing/index';
|
||||
import lscache from 'lscache';
|
||||
@ -52,6 +53,7 @@ export async function load() {
|
||||
|
||||
const gitConfig = new GitConfigService();
|
||||
const aiService = new AIService(gitConfig, httpClient);
|
||||
const remotesService = new RemotesService();
|
||||
|
||||
return {
|
||||
authService,
|
||||
@ -64,6 +66,7 @@ export async function load() {
|
||||
// These observables are provided for convenience
|
||||
remoteUrl$,
|
||||
gitConfig,
|
||||
aiService
|
||||
aiService,
|
||||
remotesService
|
||||
};
|
||||
}
|
||||
|
@ -54,10 +54,10 @@
|
||||
$: if (projectId) setupFetchInterval();
|
||||
|
||||
function setupFetchInterval() {
|
||||
baseBranchService.fetchFromTarget();
|
||||
baseBranchService.fetchFromRemotes();
|
||||
clearFetchInterval();
|
||||
const intervalMs = 15 * 60 * 1000; // 15 minutes
|
||||
intervalId = setInterval(async () => await baseBranchService.fetchFromTarget(), intervalMs);
|
||||
intervalId = setInterval(async () => await baseBranchService.fetchFromRemotes(), intervalMs);
|
||||
}
|
||||
|
||||
function clearFetchInterval() {
|
||||
|
@ -1,4 +1,9 @@
|
||||
<script lang="ts">
|
||||
// This page is displayed when:
|
||||
// - A pr is found
|
||||
// - And it does NOT have a cooresponding vbranch
|
||||
// - And it does NOT have a cooresponding remote
|
||||
// It may also display details about a cooresponding pr if they exist
|
||||
import FullviewLoading from '$lib/components/FullviewLoading.svelte';
|
||||
import PullRequestPreview from '$lib/components/PullRequestPreview.svelte';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
|
@ -1,4 +1,8 @@
|
||||
<script lang="ts">
|
||||
// This page is displayed when:
|
||||
// - A remote branch is found
|
||||
// - And it does NOT have a cooresponding vbranch
|
||||
// It may also display details about a cooresponding pr if they exist
|
||||
import FullviewLoading from '$lib/components/FullviewLoading.svelte';
|
||||
import RemoteBranchPreview from '$lib/components/RemoteBranchPreview.svelte';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
@ -13,7 +17,7 @@
|
||||
$: ({ error, branches } = data.remoteBranchService);
|
||||
|
||||
$: branch = $branches?.find((b) => b.sha == $page.params.sha);
|
||||
$: pr = githubService.getListedPr(branch?.displayName);
|
||||
$: pr = branch && githubService.getListedPr(branch.sha);
|
||||
</script>
|
||||
|
||||
{#if $error}
|
||||
|
@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
// This page is displayed when:
|
||||
// - A vbranch is found
|
||||
// It may also display details about a cooresponding remote and/or pr if they exist
|
||||
import BranchLane from '$lib/components//BranchLane.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import FullviewLoading from '$lib/components/FullviewLoading.svelte';
|
||||
|
@ -655,6 +655,11 @@ impl Repository {
|
||||
})
|
||||
.map_err(super::Error::Remotes)
|
||||
}
|
||||
|
||||
pub fn add_remote(&self, name: &str, url: &str) -> Result<()> {
|
||||
self.0.remote(name, url)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckoutTreeBuidler<'a> {
|
||||
|
@ -27,6 +27,7 @@ pub mod path;
|
||||
pub mod project_repository;
|
||||
pub mod projects;
|
||||
pub mod reader;
|
||||
pub mod remotes;
|
||||
pub mod ssh;
|
||||
pub mod storage;
|
||||
pub mod synchronize;
|
||||
|
@ -611,6 +611,12 @@ impl Repository {
|
||||
pub fn remotes(&self) -> Result<Vec<String>> {
|
||||
self.git_repository.remotes().map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub fn add_remote(&self, name: &str, url: &str) -> Result<()> {
|
||||
self.git_repository
|
||||
.add_remote(name, url)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
35
crates/gitbutler-core/src/remotes/controller.rs
Normal file
35
crates/gitbutler-core/src/remotes/controller.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::{
|
||||
error::Error,
|
||||
project_repository,
|
||||
projects::{self, ProjectId},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Controller {
|
||||
projects: projects::Controller,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(projects: projects::Controller) -> Self {
|
||||
Self { projects }
|
||||
}
|
||||
|
||||
pub async fn remotes(&self, project_id: &ProjectId) -> Result<Vec<String>, Error> {
|
||||
let project = self.projects.get(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)?;
|
||||
|
||||
project_repository.remotes().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn add_remote(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
name: &str,
|
||||
url: &str,
|
||||
) -> Result<(), Error> {
|
||||
let project = self.projects.get(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)?;
|
||||
|
||||
project_repository.add_remote(name, url).map_err(Into::into)
|
||||
}
|
||||
}
|
2
crates/gitbutler-core/src/remotes/mod.rs
Normal file
2
crates/gitbutler-core/src/remotes/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod controller;
|
||||
pub use controller::Controller;
|
@ -388,14 +388,14 @@ impl Controller {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_from_target(
|
||||
pub async fn fetch_from_remotes(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
askpass: Option<String>,
|
||||
) -> Result<BaseBranch, Error> {
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.fetch_from_target(project_id, askpass)
|
||||
.fetch_from_remotes(project_id, askpass)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -880,7 +880,7 @@ impl ControllerInner {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_from_target(
|
||||
pub async fn fetch_from_remotes(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
askpass: Option<String>,
|
||||
|
@ -103,7 +103,7 @@ async fn integration() {
|
||||
{
|
||||
// should mark commits as integrated
|
||||
controller
|
||||
.fetch_from_target(project_id, None)
|
||||
.fetch_from_remotes(project_id, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -17,7 +17,7 @@ async fn should_update_last_fetched() {
|
||||
assert!(before_fetch.last_fetched_ms.is_none());
|
||||
|
||||
let fetch = controller
|
||||
.fetch_from_target(project_id, None)
|
||||
.fetch_from_remotes(project_id, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(fetch.last_fetched_ms.is_some());
|
||||
@ -27,7 +27,7 @@ async fn should_update_last_fetched() {
|
||||
assert_eq!(fetch.last_fetched_ms, after_fetch.last_fetched_ms);
|
||||
|
||||
let second_fetch = controller
|
||||
.fetch_from_target(project_id, None)
|
||||
.fetch_from_remotes(project_id, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(second_fetch.last_fetched_ms.is_some());
|
@ -56,7 +56,7 @@ mod cherry_pick;
|
||||
mod create_commit;
|
||||
mod create_virtual_branch_from_branch;
|
||||
mod delete_virtual_branch;
|
||||
mod fetch_from_target;
|
||||
mod fetch_from_remotes;
|
||||
mod init;
|
||||
mod insert_blank_commit;
|
||||
mod move_commit_file;
|
||||
|
@ -25,6 +25,7 @@ pub mod error;
|
||||
pub mod github;
|
||||
pub mod keys;
|
||||
pub mod projects;
|
||||
pub mod remotes;
|
||||
pub mod undo;
|
||||
pub mod users;
|
||||
pub mod virtual_branches;
|
||||
|
@ -15,8 +15,8 @@
|
||||
|
||||
use gitbutler_core::{assets, git, storage};
|
||||
use gitbutler_tauri::{
|
||||
app, askpass, commands, github, keys, logs, menu, projects, undo, users, virtual_branches,
|
||||
watcher, zip,
|
||||
app, askpass, commands, github, keys, logs, menu, projects, remotes, undo, users,
|
||||
virtual_branches, watcher, zip,
|
||||
};
|
||||
use tauri::{generate_context, Manager};
|
||||
use tauri_plugin_log::LogTarget;
|
||||
@ -134,6 +134,12 @@ fn main() {
|
||||
git_credentials_controller.clone(),
|
||||
));
|
||||
|
||||
let remotes_controller = gitbutler_core::remotes::controller::Controller::new(
|
||||
projects_controller.clone(),
|
||||
);
|
||||
|
||||
app_handle.manage(remotes_controller.clone());
|
||||
|
||||
let app = app::App::new(
|
||||
projects_controller,
|
||||
);
|
||||
@ -200,7 +206,7 @@ fn main() {
|
||||
virtual_branches::commands::list_remote_branches,
|
||||
virtual_branches::commands::get_remote_branch_data,
|
||||
virtual_branches::commands::squash_branch_commit,
|
||||
virtual_branches::commands::fetch_from_target,
|
||||
virtual_branches::commands::fetch_from_remotes,
|
||||
virtual_branches::commands::move_commit,
|
||||
undo::list_snapshots,
|
||||
undo::restore_snapshot,
|
||||
@ -210,6 +216,8 @@ fn main() {
|
||||
github::commands::init_device_oauth,
|
||||
github::commands::check_auth_status,
|
||||
askpass::commands::submit_prompt_response,
|
||||
remotes::list_remotes,
|
||||
remotes::add_remote
|
||||
])
|
||||
.menu(menu::build(tauri_context.package_info()))
|
||||
.on_menu_event(|event|menu::handle_event(&event))
|
||||
|
32
crates/gitbutler-tauri/src/remotes.rs
Normal file
32
crates/gitbutler-tauri/src/remotes.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use crate::error::Error;
|
||||
use gitbutler_core::{projects::ProjectId, remotes::Controller};
|
||||
use tauri::Manager;
|
||||
use tracing::instrument;
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn list_remotes(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: ProjectId,
|
||||
) -> Result<Vec<String>, Error> {
|
||||
handle
|
||||
.state::<Controller>()
|
||||
.remotes(&project_id)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn add_remote(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: ProjectId,
|
||||
name: &str,
|
||||
url: &str,
|
||||
) -> Result<(), Error> {
|
||||
handle
|
||||
.state::<Controller>()
|
||||
.add_remote(&project_id, name, url)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
@ -485,14 +485,14 @@ pub mod commands {
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle), err(Debug))]
|
||||
pub async fn fetch_from_target(
|
||||
pub async fn fetch_from_remotes(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: ProjectId,
|
||||
action: Option<String>,
|
||||
) -> Result<BaseBranch, Error> {
|
||||
let base_branch = handle
|
||||
.state::<Controller>()
|
||||
.fetch_from_target(
|
||||
.fetch_from_remotes(
|
||||
&project_id,
|
||||
Some(action.unwrap_or_else(|| "unknown".to_string())),
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user