ForgeType

Set the type of repository forge type (e.g. GitHub, GitLab) once the base branch is determined.
Get the available templates with the updated method
This commit is contained in:
estib 2024-10-17 18:14:35 +02:00
parent 4ee01031f6
commit d41e271e99
13 changed files with 78 additions and 52 deletions

View File

@ -0,0 +1,15 @@
import { invoke } from './ipc';
export type ForgeType = 'github' | 'gitlab' | 'bitbucket' | 'azure';
export class ForgeService {
constructor(private projectId: string) {}
async getAvailableReviewTemplates(): Promise<string[]> {
const templates = await invoke<string[]>('get_available_review_templates', {
projectId: this.projectId
});
return templates;
}
}

View File

@ -5,6 +5,7 @@ import { persisted } from '@gitbutler/shared/persisted';
import { open } from '@tauri-apps/api/dialog'; import { open } from '@tauri-apps/api/dialog';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { derived, get, writable, type Readable } from 'svelte/store'; import { derived, get, writable, type Readable } from 'svelte/store';
import type { ForgeType } from './forge';
import type { HttpClient } from '@gitbutler/shared/httpClient'; import type { HttpClient } from '@gitbutler/shared/httpClient';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -15,6 +16,8 @@ export type LocalKey = {
export type Key = Exclude<KeyType, 'local'> | LocalKey; export type Key = Exclude<KeyType, 'local'> | LocalKey;
export type HostType = { type: ForgeType };
export class Project { export class Project {
id!: string; id!: string;
title!: string; title!: string;
@ -28,8 +31,8 @@ export class Project {
snapshot_lines_threshold!: number | undefined; snapshot_lines_threshold!: number | undefined;
use_new_locking!: boolean; use_new_locking!: boolean;
git_host!: { git_host!: {
hostType: 'github' | 'gitlab' | 'bitbucket' | 'azure'; hostType: HostType | undefined;
pullRequestTemplatePath: string; reviewTemplatePath: string | undefined;
}; };
// Produced just for the frontend to determine if the project is open in any window. // Produced just for the frontend to determine if the project is open in any window.
@ -217,6 +220,14 @@ export class ProjectsService {
async getCloudProject(repositoryId: string): Promise<CloudProject> { async getCloudProject(repositoryId: string): Promise<CloudProject> {
return await this.httpClient.get(`projects/${repositoryId}.json`); return await this.httpClient.get(`projects/${repositoryId}.json`);
} }
async setGitHostType(project: Project, type: ForgeType) {
if (project.git_host.hostType?.type === type) return;
const hostType: HostType = { type };
const gitHost = { hostType };
await invoke('update_project_git_host', { projectId: project.id, gitHost });
this.reload();
}
} }
/** /**

View File

@ -1,4 +1,5 @@
import { AzureBranch } from './azureBranch'; import { AzureBranch } from './azureBranch';
import type { ForgeType } from '$lib/backend/forge';
import type { RepoInfo } from '$lib/url/gitUrl'; import type { RepoInfo } from '$lib/url/gitUrl';
import type { GitHost } from '../interface/gitHost'; import type { GitHost } from '../interface/gitHost';
import type { GitHostArguments } from '../interface/types'; import type { GitHostArguments } from '../interface/types';
@ -12,6 +13,7 @@ export const AZURE_DOMAIN = 'dev.azure.com';
* https://github.com/gitbutlerapp/gitbutler/issues/2651 * https://github.com/gitbutlerapp/gitbutler/issues/2651
*/ */
export class AzureDevOps implements GitHost { export class AzureDevOps implements GitHost {
readonly type: ForgeType = 'azure';
private baseUrl: string; private baseUrl: string;
private repo: RepoInfo; private repo: RepoInfo;
private baseBranch: string; private baseBranch: string;
@ -48,11 +50,6 @@ export class AzureDevOps implements GitHost {
return undefined; return undefined;
} }
async availablePullRequestTemplates(_path?: string) {
// See: https://learn.microsoft.com/en-us/azure/devops/repos/git/pull-request-templates?view=azure-devops#default-pull-request-templates
return undefined;
}
async pullRequestTemplateContent(_path?: string) { async pullRequestTemplateContent(_path?: string) {
return undefined; return undefined;
} }

View File

@ -1,4 +1,5 @@
import { BitBucketBranch } from './bitbucketBranch'; import { BitBucketBranch } from './bitbucketBranch';
import type { ForgeType } from '$lib/backend/forge';
import type { RepoInfo } from '$lib/url/gitUrl'; import type { RepoInfo } from '$lib/url/gitUrl';
import type { GitHost } from '../interface/gitHost'; import type { GitHost } from '../interface/gitHost';
import type { DetailedPullRequest, GitHostArguments } from '../interface/types'; import type { DetailedPullRequest, GitHostArguments } from '../interface/types';
@ -16,6 +17,7 @@ export const BITBUCKET_DOMAIN = 'bitbucket.org';
* https://github.com/gitbutlerapp/gitbutler/issues/3252 * https://github.com/gitbutlerapp/gitbutler/issues/3252
*/ */
export class BitBucket implements GitHost { export class BitBucket implements GitHost {
readonly type: ForgeType = 'bitbucket';
private baseUrl: string; private baseUrl: string;
private repo: RepoInfo; private repo: RepoInfo;
private baseBranch: string; private baseBranch: string;
@ -52,11 +54,6 @@ export class BitBucket implements GitHost {
return undefined; return undefined;
} }
async availablePullRequestTemplates(_path?: string) {
// See: https://confluence.atlassian.com/bitbucketserver/create-a-pull-request-808488431.html#Createapullrequest-templatePullrequestdescriptiontemplates
return undefined;
}
async pullRequestTemplateContent(_path?: string) { async pullRequestTemplateContent(_path?: string) {
return undefined; return undefined;
} }

View File

@ -4,6 +4,7 @@ import { GitHubListingService } from './githubListingService';
import { GitHubPrService } from './githubPrService'; import { GitHubPrService } from './githubPrService';
import { GitHubIssueService } from '$lib/gitHost/github/issueService'; import { GitHubIssueService } from '$lib/gitHost/github/issueService';
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
import type { ForgeType } from '$lib/backend/forge';
import type { ProjectMetrics } from '$lib/metrics/projectMetrics'; import type { ProjectMetrics } from '$lib/metrics/projectMetrics';
import type { RepoInfo } from '$lib/url/gitUrl'; import type { RepoInfo } from '$lib/url/gitUrl';
import type { GitHost } from '../interface/gitHost'; import type { GitHost } from '../interface/gitHost';
@ -12,6 +13,7 @@ import type { GitHostArguments } from '../interface/types';
export const GITHUB_DOMAIN = 'github.com'; export const GITHUB_DOMAIN = 'github.com';
export class GitHub implements GitHost { export class GitHub implements GitHost {
readonly type: ForgeType = 'github';
private baseUrl: string; private baseUrl: string;
private repo: RepoInfo; private repo: RepoInfo;
private baseBranch: string; private baseBranch: string;

View File

@ -107,18 +107,4 @@ export class GitHubPrService implements GitHostPrService {
}); });
} }
} }
async availablePullRequestTemplates(path: string): Promise<string[] | undefined> {
// TODO: Find a workaround to avoid this dynamic import
// https://github.com/sveltejs/kit/issues/905
const { join } = await import('@tauri-apps/api/path');
const targetPath = await join(path, '.github');
const availableTemplates: string[] | undefined = await invoke(
'available_pull_request_templates',
{ rootPath: targetPath }
);
return availableTemplates;
}
} }

View File

@ -1,4 +1,5 @@
import { GitLabBranch } from './gitlabBranch'; import { GitLabBranch } from './gitlabBranch';
import type { ForgeType } from '$lib/backend/forge';
import type { RepoInfo } from '$lib/url/gitUrl'; import type { RepoInfo } from '$lib/url/gitUrl';
import type { GitHost } from '../interface/gitHost'; import type { GitHost } from '../interface/gitHost';
import type { DetailedPullRequest, GitHostArguments } from '../interface/types'; import type { DetailedPullRequest, GitHostArguments } from '../interface/types';
@ -17,6 +18,7 @@ export const GITLAB_SUB_DOMAIN = 'gitlab'; // For self hosted instance of Gitlab
* https://github.com/gitbutlerapp/gitbutler/issues/2511 * https://github.com/gitbutlerapp/gitbutler/issues/2511
*/ */
export class GitLab implements GitHost { export class GitLab implements GitHost {
readonly type: ForgeType = 'gitlab';
private baseUrl: string; private baseUrl: string;
private repo: RepoInfo; private repo: RepoInfo;
private baseBranch: string; private baseBranch: string;
@ -53,11 +55,6 @@ export class GitLab implements GitHost {
return undefined; return undefined;
} }
async availablePullRequestTemplates(_path?: string) {
// See: https://docs.gitlab.com/ee/user/project/description_templates.html
return undefined;
}
async pullRequestTemplateContent(_path?: string) { async pullRequestTemplateContent(_path?: string) {
return undefined; return undefined;
} }

View File

@ -1,4 +1,5 @@
import { buildContextStore } from '@gitbutler/shared/context'; import { buildContextStore } from '@gitbutler/shared/context';
import type { ForgeType } from '$lib/backend/forge';
import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService'; import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService';
import type { GitHostBranch } from './gitHostBranch'; import type { GitHostBranch } from './gitHostBranch';
import type { GitHostChecksMonitor } from './gitHostChecksMonitor'; import type { GitHostChecksMonitor } from './gitHostChecksMonitor';
@ -6,6 +7,7 @@ import type { GitHostListingService } from './gitHostListingService';
import type { GitHostPrService } from './gitHostPrService'; import type { GitHostPrService } from './gitHostPrService';
export interface GitHost { export interface GitHost {
readonly type: ForgeType;
// Lists PRs for the repo. // Lists PRs for the repo.
listService(): GitHostListingService | undefined; listService(): GitHostListingService | undefined;

View File

@ -17,7 +17,6 @@ export interface GitHostPrService {
baseBranchName, baseBranchName,
upstreamName upstreamName
}: CreatePullRequestArgs): Promise<PullRequest>; }: CreatePullRequestArgs): Promise<PullRequest>;
availablePullRequestTemplates(path: string): Promise<string[] | undefined>;
pullRequestTemplateContent(path: string, projectId: string): Promise<string | undefined>; pullRequestTemplateContent(path: string, projectId: string): Promise<string | undefined>;
merge(method: MergeMethod, prNumber: number): Promise<void>; merge(method: MergeMethod, prNumber: number): Promise<void>;
prMonitor(prNumber: number): GitHostPrMonitor; prMonitor(prNumber: number): GitHostPrMonitor;

View File

@ -82,7 +82,7 @@
props.type === 'preview-series' ? props.upstreamName : branch.upstreamName props.type === 'preview-series' ? props.upstreamName : branch.upstreamName
); );
const baseBranchName = $derived($baseBranch.shortName); const baseBranchName = $derived($baseBranch.shortName);
const prTemplatePath = $derived(project.git_host.pullRequestTemplatePath); const prTemplatePath = $derived(project.git_host.reviewTemplatePath);
let isDraft = $state<boolean>($preferredPRAction === PRAction.CreateDraft); let isDraft = $state<boolean>($preferredPRAction === PRAction.CreateDraft);
let modal = $state<ReturnType<typeof Modal>>(); let modal = $state<ReturnType<typeof Modal>>();

View File

@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import notFoundSvg from '$lib/assets/empty-state/not-found.svg?raw'; import notFoundSvg from '$lib/assets/empty-state/not-found.svg?raw';
import { Project, ProjectsService } from '$lib/backend/projects'; import { ForgeService } from '$lib/backend/forge';
import { ProjectService, ProjectsService } from '$lib/backend/projects';
import SectionCard from '$lib/components/SectionCard.svelte'; import SectionCard from '$lib/components/SectionCard.svelte';
import { getGitHost } from '$lib/gitHost/interface/gitHost';
import { createGitHostPrServiceStore } from '$lib/gitHost/interface/gitHostPrService';
import Select from '$lib/select/Select.svelte'; import Select from '$lib/select/Select.svelte';
import SelectItem from '$lib/select/SelectItem.svelte'; import SelectItem from '$lib/select/SelectItem.svelte';
import Section from '$lib/settings/Section.svelte'; import Section from '$lib/settings/Section.svelte';
@ -14,25 +13,27 @@
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte'; import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
const projectsService = getContext(ProjectsService); const projectsService = getContext(ProjectsService);
const project = getContext(Project); const projectService = getContext(ProjectService);
const gitHost = getGitHost(); const forgeService = getContext(ForgeService);
const prService = createGitHostPrServiceStore(undefined);
$effect(() => prService.set($gitHost?.prService()));
let useTemplate = $state(!!project.git_host?.pullRequestTemplatePath); const projectStore = projectService.project;
let selectedTemplate = $state(project.git_host?.pullRequestTemplatePath ?? ''); const project = $derived($projectStore);
const useTemplate = $derived(!!project?.git_host.reviewTemplatePath);
const selectedTemplate = $derived(project?.git_host.reviewTemplatePath);
let allAvailableTemplates = $state<{ label: string; value: string }[]>([]); let allAvailableTemplates = $state<{ label: string; value: string }[]>([]);
let isTemplatesAvailable = $state(false); let isTemplatesAvailable = $state(false);
$effect(() => { $effect(() => {
if (!project.path) return; if (!project) {
$prService?.availablePullRequestTemplates(project.path).then((availableTemplates) => { return;
}
forgeService.getAvailableReviewTemplates().then((availableTemplates) => {
if (availableTemplates) { if (availableTemplates) {
allAvailableTemplates = availableTemplates.map((availableTemplate) => { allAvailableTemplates = availableTemplates.map((availableTemplate) => {
const relativePath = availableTemplate.replace(`${project.path}/`, '');
return { return {
label: relativePath, label: availableTemplate,
value: relativePath value: availableTemplate
}; };
}); });
@ -42,15 +43,26 @@
}); });
async function setUsePullRequestTemplate(value: boolean) { async function setUsePullRequestTemplate(value: boolean) {
if (!value) { if (!project) return;
project.git_host.pullRequestTemplatePath = '';
setTemplate: {
if (!value) {
project.git_host.reviewTemplatePath = undefined;
break setTemplate;
}
if (allAvailableTemplates[0]) {
project.git_host.reviewTemplatePath = allAvailableTemplates[0].value;
break setTemplate;
}
} }
await projectsService.updateProject(project); await projectsService.updateProject(project);
} }
async function setPullRequestTemplatePath(value: string) { async function setPullRequestTemplatePath(value: string) {
selectedTemplate = value; if (!project) return;
project.git_host.pullRequestTemplatePath = value; project.git_host.reviewTemplatePath = value;
await projectsService.updateProject(project); await projectsService.updateProject(project);
} }
</script> </script>
@ -66,7 +78,8 @@
<svelte:fragment slot="actions"> <svelte:fragment slot="actions">
<Toggle <Toggle
id="use-pull-request-template-boolean" id="use-pull-request-template-boolean"
bind:checked={useTemplate} checked={useTemplate}
disabled={!isTemplatesAvailable}
on:click={(e) => { on:click={(e) => {
setUsePullRequestTemplate( setUsePullRequestTemplate(
(e.target as MouseEvent['target'] & { checked: boolean }).checked (e.target as MouseEvent['target'] & { checked: boolean }).checked

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ForgeService } from '$lib/backend/forge';
import { Project, ProjectService } from '$lib/backend/projects'; import { Project, ProjectService } from '$lib/backend/projects';
import FileMenuAction from '$lib/barmenuActions/FileMenuAction.svelte'; import FileMenuAction from '$lib/barmenuActions/FileMenuAction.svelte';
import ProjectSettingsMenuAction from '$lib/barmenuActions/ProjectSettingsMenuAction.svelte'; import ProjectSettingsMenuAction from '$lib/barmenuActions/ProjectSettingsMenuAction.svelte';
@ -71,6 +72,7 @@
setContext(BranchController, data.branchController); setContext(BranchController, data.branchController);
setContext(BaseBranchService, data.baseBranchService); setContext(BaseBranchService, data.baseBranchService);
setContext(CommitService, data.commitService); setContext(CommitService, data.commitService);
setContext(ForgeService, data.forgeService);
setContext(BaseBranch, baseBranch); setContext(BaseBranch, baseBranch);
setContext(Project, project); setContext(Project, project);
setContext(BranchDragActionsFactory, data.branchDragActionsFactory); setContext(BranchDragActionsFactory, data.branchDragActionsFactory);
@ -146,6 +148,8 @@
const ghListService = gitHost?.listService(); const ghListService = gitHost?.listService();
if (gitHost) projectsService.setGitHostType(project, gitHost.type);
listServiceStore.set(ghListService); listServiceStore.set(ghListService);
gitHostStore.set(gitHost); gitHostStore.set(gitHost);
}); });

View File

@ -1,3 +1,4 @@
import { ForgeService } from '$lib/backend/forge';
import { getUserErrorCode, invoke } from '$lib/backend/ipc'; import { getUserErrorCode, invoke } from '$lib/backend/ipc';
import { ProjectService, type Project } from '$lib/backend/projects'; import { ProjectService, type Project } from '$lib/backend/projects';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService'; import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
@ -61,6 +62,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
const historyService = new HistoryService(projectId); const historyService = new HistoryService(projectId);
const baseBranchService = new BaseBranchService(projectId); const baseBranchService = new BaseBranchService(projectId);
const commitService = new CommitService(projectId); const commitService = new CommitService(projectId);
const forgeService = new ForgeService(projectId);
const branchListingService = new BranchListingService(projectId); const branchListingService = new BranchListingService(projectId);
const remoteBranchService = new RemoteBranchService( const remoteBranchService = new RemoteBranchService(
@ -108,6 +110,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
authService, authService,
baseBranchService, baseBranchService,
commitService, commitService,
forgeService,
branchController, branchController,
historyService, historyService,
projectId, projectId,