mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
Merge pull request #4850 from gitbutlerapp/ndom91/use-template-dropdown
fix: refactor pr template path input to pre-filled `Select` instead of `TextBox`
This commit is contained in:
commit
1a47f5ec4d
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2566,6 +2566,7 @@ dependencies = [
|
||||
"gitbutler-edit-mode",
|
||||
"gitbutler-error",
|
||||
"gitbutler-feedback",
|
||||
"gitbutler-fs",
|
||||
"gitbutler-id",
|
||||
"gitbutler-operating-modes",
|
||||
"gitbutler-oplog",
|
||||
|
@ -27,6 +27,10 @@ export class Project {
|
||||
use_diff_context: boolean | undefined;
|
||||
snapshot_lines_threshold!: number | undefined;
|
||||
use_new_locking!: boolean;
|
||||
git_host!: {
|
||||
hostType: 'github' | 'gitlab' | 'bitbucket' | 'azure';
|
||||
pullRequestTemplatePath: string;
|
||||
};
|
||||
|
||||
private succeeding_rebases!: boolean;
|
||||
get succeedingRebases() {
|
||||
|
@ -4,6 +4,7 @@
|
||||
import BranchLaneContextMenu from './BranchLaneContextMenu.svelte';
|
||||
import DefaultTargetButton from './DefaultTargetButton.svelte';
|
||||
import PullRequestButton from '../pr/PullRequestButton.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
@ -41,6 +42,7 @@
|
||||
const branchStore = getContextStore(VirtualBranch);
|
||||
const prMonitor = getGitHostPrMonitor();
|
||||
const gitHost = getGitHost();
|
||||
const project = getContext(Project);
|
||||
|
||||
const baseBranchName = $derived($baseBranch.shortName);
|
||||
const branch = $derived($branchStore);
|
||||
@ -87,15 +89,30 @@
|
||||
let title: string;
|
||||
let body: string;
|
||||
|
||||
// In case of a single commit, use the commit summary and description for the title and
|
||||
// description of the PR.
|
||||
if (branch.commits.length === 1) {
|
||||
const commit = branch.commits[0];
|
||||
title = commit?.descriptionTitle ?? '';
|
||||
body = commit?.descriptionBody ?? '';
|
||||
} else {
|
||||
let pullRequestTemplateBody: string | undefined;
|
||||
const prTemplatePath = project.git_host.pullRequestTemplatePath;
|
||||
|
||||
if (prTemplatePath) {
|
||||
pullRequestTemplateBody = await $prService?.pullRequestTemplateContent(
|
||||
prTemplatePath,
|
||||
project.id
|
||||
);
|
||||
}
|
||||
|
||||
if (pullRequestTemplateBody) {
|
||||
title = branch.name;
|
||||
body = '';
|
||||
body = pullRequestTemplateBody;
|
||||
} else {
|
||||
// In case of a single commit, use the commit summary and description for the title and
|
||||
// description of the PR.
|
||||
if (branch.commits.length === 1) {
|
||||
const commit = branch.commits[0];
|
||||
title = commit?.descriptionTitle ?? '';
|
||||
body = commit?.descriptionBody ?? '';
|
||||
} else {
|
||||
title = branch.name;
|
||||
body = '';
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
@ -49,11 +49,3 @@ export function projectLaneCollapsed(projectId: string, laneId: string): Persist
|
||||
export function persistedCommitMessage(projectId: string, branchId: string): Persisted<string> {
|
||||
return persisted('', 'projectCurrentCommitMessage_' + projectId + '_' + branchId);
|
||||
}
|
||||
|
||||
export function gitHostUsePullRequestTemplate(): Persisted<boolean> {
|
||||
return persisted(false, 'gitHostUsePullRequestTemplate');
|
||||
}
|
||||
|
||||
export function gitHostPullRequestTemplatePath(): Persisted<string> {
|
||||
return persisted('', 'gitHostPullRequestTemplatePath');
|
||||
}
|
||||
|
@ -47,4 +47,13 @@ export class AzureDevOps implements GitHost {
|
||||
checksMonitor(_sourceBranch: string) {
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -51,4 +51,13 @@ export class BitBucket implements GitHost {
|
||||
checksMonitor(_sourceBranch: string) {
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { BitBucket, BITBUCKET_DOMAIN } from './bitbucket/bitbucket';
|
||||
import { GitHub, GITHUB_DOMAIN } from './github/github';
|
||||
import { GitLab, GITLAB_DOMAIN, GITLAB_SUB_DOMAIN } from './gitlab/gitlab';
|
||||
import { ProjectMetrics } from '$lib/metrics/projectMetrics';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from './interface/gitHost';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
@ -17,13 +16,7 @@ export interface GitHostFactory {
|
||||
export class DefaultGitHostFactory implements GitHostFactory {
|
||||
constructor(private octokit: Octokit | undefined) {}
|
||||
|
||||
build(
|
||||
repo: RepoInfo,
|
||||
baseBranch: string,
|
||||
fork?: RepoInfo,
|
||||
usePullRequestTemplate?: Persisted<boolean>,
|
||||
pullRequestTemplatePath?: Persisted<string>
|
||||
) {
|
||||
build(repo: RepoInfo, baseBranch: string, fork?: RepoInfo) {
|
||||
const domain = repo.domain;
|
||||
const forkStr = fork ? `${fork.owner}:${fork.name}` : undefined;
|
||||
|
||||
@ -33,9 +26,7 @@ export class DefaultGitHostFactory implements GitHostFactory {
|
||||
baseBranch,
|
||||
forkStr,
|
||||
octokit: this.octokit,
|
||||
projectMetrics: new ProjectMetrics(),
|
||||
usePullRequestTemplate,
|
||||
pullRequestTemplatePath
|
||||
projectMetrics: new ProjectMetrics()
|
||||
});
|
||||
}
|
||||
if (domain === GITLAB_DOMAIN || domain.startsWith(GITLAB_SUB_DOMAIN + '.')) {
|
||||
|
@ -5,7 +5,6 @@ import { GitHubPrService } from './githubPrService';
|
||||
import { GitHubIssueService } from '$lib/gitHost/github/issueService';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import type { ProjectMetrics } from '$lib/metrics/projectMetrics';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from '../interface/gitHost';
|
||||
import type { GitHostArguments } from '../interface/types';
|
||||
@ -19,22 +18,16 @@ export class GitHub implements GitHost {
|
||||
private forkStr?: string;
|
||||
private octokit?: Octokit;
|
||||
private projectMetrics?: ProjectMetrics;
|
||||
private usePullRequestTemplate?: Persisted<boolean>;
|
||||
private pullRequestTemplatePath?: Persisted<string>;
|
||||
|
||||
constructor({
|
||||
repo,
|
||||
baseBranch,
|
||||
forkStr,
|
||||
octokit,
|
||||
projectMetrics,
|
||||
usePullRequestTemplate,
|
||||
pullRequestTemplatePath
|
||||
projectMetrics
|
||||
}: GitHostArguments & {
|
||||
octokit?: Octokit;
|
||||
projectMetrics?: ProjectMetrics;
|
||||
usePullRequestTemplate?: Persisted<boolean>;
|
||||
pullRequestTemplatePath?: Persisted<string>;
|
||||
}) {
|
||||
this.baseUrl = `https://${GITHUB_DOMAIN}/${repo.owner}/${repo.name}`;
|
||||
this.repo = repo;
|
||||
@ -42,8 +35,6 @@ export class GitHub implements GitHost {
|
||||
this.forkStr = forkStr;
|
||||
this.octokit = octokit;
|
||||
this.projectMetrics = projectMetrics;
|
||||
this.usePullRequestTemplate = usePullRequestTemplate;
|
||||
this.pullRequestTemplatePath = pullRequestTemplatePath;
|
||||
}
|
||||
|
||||
listService() {
|
||||
@ -57,12 +48,7 @@ export class GitHub implements GitHost {
|
||||
if (!this.octokit) {
|
||||
return;
|
||||
}
|
||||
return new GitHubPrService(
|
||||
this.octokit,
|
||||
this.repo,
|
||||
this.usePullRequestTemplate,
|
||||
this.pullRequestTemplatePath
|
||||
);
|
||||
return new GitHubPrService(this.octokit, this.repo);
|
||||
}
|
||||
|
||||
issueService() {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { GitHubPrMonitor } from './githubPrMonitor';
|
||||
import { DEFAULT_HEADERS } from './headers';
|
||||
import { ghResponseToInstance, parseGitHubDetailedPullRequest } from './types';
|
||||
import { invoke } from '$lib/backend/ipc';
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
import posthog from 'posthog-js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { GitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHostPrService } from '../interface/gitHostPrService';
|
||||
import type {
|
||||
CreatePullRequestArgs,
|
||||
DetailedPullRequest,
|
||||
@ -16,16 +16,12 @@ import type {
|
||||
} from '../interface/types';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
|
||||
const DEFAULT_PULL_REQUEST_TEMPLATE_PATH = '.github/PULL_REQUEST_TEMPLATE.md';
|
||||
|
||||
export class GitHubPrService implements GitHostPrService {
|
||||
loading = writable(false);
|
||||
|
||||
constructor(
|
||||
private octokit: Octokit,
|
||||
private repo: RepoInfo,
|
||||
private usePullRequestTemplate?: Persisted<boolean>,
|
||||
private pullRequestTemplatePath?: Persisted<string>
|
||||
private repo: RepoInfo
|
||||
) {}
|
||||
|
||||
async createPr({
|
||||
@ -36,33 +32,28 @@ export class GitHubPrService implements GitHostPrService {
|
||||
upstreamName
|
||||
}: CreatePullRequestArgs): Promise<PullRequest> {
|
||||
this.loading.set(true);
|
||||
const request = async (pullRequestTemplate: string | undefined = '') => {
|
||||
const request = async () => {
|
||||
const resp = await this.octokit.rest.pulls.create({
|
||||
owner: this.repo.owner,
|
||||
repo: this.repo.name,
|
||||
head: upstreamName,
|
||||
base: baseBranchName,
|
||||
title,
|
||||
body: body ? body : pullRequestTemplate,
|
||||
body,
|
||||
draft
|
||||
});
|
||||
|
||||
return ghResponseToInstance(resp.data);
|
||||
};
|
||||
|
||||
let attempts = 0;
|
||||
let lastError: any;
|
||||
let pr: PullRequest | undefined;
|
||||
let pullRequestTemplate: string | undefined;
|
||||
const usePrTemplate = this.usePullRequestTemplate ? get(this.usePullRequestTemplate) : null;
|
||||
|
||||
if (!body && usePrTemplate) {
|
||||
pullRequestTemplate = await this.fetchPrTemplate();
|
||||
}
|
||||
|
||||
// Use retries since request can fail right after branch push.
|
||||
while (attempts < 4) {
|
||||
try {
|
||||
pr = await request(pullRequestTemplate);
|
||||
pr = await request();
|
||||
posthog.capture('PR Successful');
|
||||
return pr;
|
||||
} catch (err: any) {
|
||||
@ -76,32 +67,6 @@ export class GitHubPrService implements GitHostPrService {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async fetchPrTemplate() {
|
||||
const path = this.pullRequestTemplatePath
|
||||
? get(this.pullRequestTemplatePath)
|
||||
: DEFAULT_PULL_REQUEST_TEMPLATE_PATH;
|
||||
|
||||
try {
|
||||
const response = await this.octokit.rest.repos.getContent({
|
||||
owner: this.repo.owner,
|
||||
repo: this.repo.name,
|
||||
path
|
||||
});
|
||||
const b64Content = (response.data as any)?.content;
|
||||
if (b64Content) {
|
||||
return decodeURIComponent(escape(atob(b64Content)));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching pull request template at path: ${path}`, err);
|
||||
|
||||
showToast({
|
||||
title: 'Failed to fetch pull request template',
|
||||
message: `Template not found at path: \`${path}\`.`,
|
||||
style: 'neutral'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async get(prNumber: number): Promise<DetailedPullRequest> {
|
||||
const resp = await this.octokit.pulls.get({
|
||||
headers: DEFAULT_HEADERS,
|
||||
@ -124,4 +89,36 @@ export class GitHubPrService implements GitHostPrService {
|
||||
prMonitor(prNumber: number): GitHubPrMonitor {
|
||||
return new GitHubPrMonitor(this, prNumber);
|
||||
}
|
||||
|
||||
async pullRequestTemplateContent(path: string, projectId: string) {
|
||||
try {
|
||||
const fileContents: string | undefined = await invoke('get_pr_template_contents', {
|
||||
relativePath: path,
|
||||
projectId
|
||||
});
|
||||
return fileContents;
|
||||
} catch (err) {
|
||||
console.error(`Error reading pull request template at path: ${path}`, err);
|
||||
|
||||
showToast({
|
||||
title: 'Failed to read pull request template',
|
||||
message: `Could not read: \`${path}\`.`,
|
||||
style: 'neutral'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -52,4 +52,13 @@ export class GitLab implements GitHost {
|
||||
checksMonitor(_sourceBranch: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async availablePullRequestTemplates(_path?: string) {
|
||||
// See: https://docs.gitlab.com/ee/user/project/description_templates.html
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async pullRequestTemplateContent(_path?: string) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,8 @@ export interface GitHostPrService {
|
||||
baseBranchName,
|
||||
upstreamName
|
||||
}: CreatePullRequestArgs): Promise<PullRequest>;
|
||||
fetchPrTemplate(path?: string): Promise<string | undefined>;
|
||||
availablePullRequestTemplates(path: string): Promise<string[] | undefined>;
|
||||
pullRequestTemplateContent(path: string, projectId: string): Promise<string | undefined>;
|
||||
merge(method: MergeMethod, prNumber: number): Promise<void>;
|
||||
prMonitor(prNumber: number): GitHostPrMonitor;
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import {
|
||||
gitHostPullRequestTemplatePath,
|
||||
gitHostUsePullRequestTemplate
|
||||
} from '$lib/config/config';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
|
||||
const usePullRequestTemplate = gitHostUsePullRequestTemplate();
|
||||
const pullRequestTemplatePath = gitHostPullRequestTemplatePath();
|
||||
</script>
|
||||
|
||||
<Section>
|
||||
<svelte:fragment slot="title">Pull Request Template</svelte:fragment>
|
||||
<svelte:fragment slot="description">
|
||||
Use Pull Request template when creating a Pull Requests.
|
||||
</svelte:fragment>
|
||||
|
||||
<div>
|
||||
<SectionCard
|
||||
roundedBottom={false}
|
||||
orientation="row"
|
||||
labelFor="use-pull-request-template-boolean"
|
||||
>
|
||||
<svelte:fragment slot="title">Enable Pull Request Templates</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="use-pull-request-template-boolean"
|
||||
value="false"
|
||||
bind:checked={$usePullRequestTemplate}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
If enabled, we will use the path below to set the initial body of any pull requested created
|
||||
on this project through GitButler.
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
<SectionCard roundedTop={false} orientation="row" labelFor="use-pull-request-template-path">
|
||||
<svelte:fragment slot="caption">
|
||||
<form>
|
||||
<fieldset class="fields-wrapper">
|
||||
<TextBox
|
||||
label="Pull request template path"
|
||||
id="use-pull-request-template-path"
|
||||
bind:value={$pullRequestTemplatePath}
|
||||
placeholder=".github/pull_request_template.md"
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</Section>
|
||||
<Spacer />
|
102
apps/desktop/src/lib/settings/PullRequestTemplateForm.svelte
Normal file
102
apps/desktop/src/lib/settings/PullRequestTemplateForm.svelte
Normal file
@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { Project, ProjectService } from '$lib/backend/projects';
|
||||
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 SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
|
||||
const projectService = getContext(ProjectService);
|
||||
const project = getContext(Project);
|
||||
const gitHost = getGitHost();
|
||||
const prService = createGitHostPrServiceStore(undefined);
|
||||
$effect(() => prService.set($gitHost?.prService()));
|
||||
|
||||
let useTemplate = $state(!!project.git_host?.pullRequestTemplatePath);
|
||||
let selectedTemplate = $state(project.git_host?.pullRequestTemplatePath ?? '');
|
||||
let allAvailableTemplates = $state<{ label: string; value: string }[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
if (!project.path) return;
|
||||
$prService?.availablePullRequestTemplates(project.path).then((availableTemplates) => {
|
||||
if (availableTemplates) {
|
||||
allAvailableTemplates = availableTemplates.map((availableTemplate) => {
|
||||
const relativePath = availableTemplate.replace(`${project.path}/`, '');
|
||||
return {
|
||||
label: relativePath,
|
||||
value: relativePath
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function setUsePullRequestTemplate(value: boolean) {
|
||||
if (!value) {
|
||||
project.git_host.pullRequestTemplatePath = '';
|
||||
}
|
||||
await projectService.updateProject(project);
|
||||
}
|
||||
|
||||
async function setPullRequestTemplatePath(value: string) {
|
||||
selectedTemplate = value;
|
||||
project.git_host.pullRequestTemplatePath = value;
|
||||
await projectService.updateProject(project);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section>
|
||||
<svelte:fragment slot="title">Pull Request Template</svelte:fragment>
|
||||
<svelte:fragment slot="description">
|
||||
Use your pull request template of choice when creating pull requests from GitButler.
|
||||
</svelte:fragment>
|
||||
|
||||
<div>
|
||||
<SectionCard
|
||||
roundedBottom={false}
|
||||
orientation="row"
|
||||
labelFor="use-pull-request-template-boolean"
|
||||
>
|
||||
<svelte:fragment slot="title">Enable Pull Request Templates</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="use-pull-request-template-boolean"
|
||||
bind:checked={useTemplate}
|
||||
on:click={(e) => {
|
||||
setUsePullRequestTemplate((e.target as MouseEvent['target'] & { checked: boolean }).checked);
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
If enabled, we will use the path below to set the initial body of any pull requested created
|
||||
on this project through GitButler.
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
<SectionCard roundedTop={false} orientation="row" labelFor="use-pull-request-template-path">
|
||||
<svelte:fragment slot="caption">
|
||||
<Select
|
||||
value={selectedTemplate}
|
||||
options={allAvailableTemplates.map(({ label, value }) => ({ label, value }))}
|
||||
label="Available Templates"
|
||||
wide={true}
|
||||
searchable
|
||||
disabled={allAvailableTemplates.length === 0}
|
||||
onselect={(value) => {
|
||||
setPullRequestTemplatePath(value);
|
||||
}}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedTemplate} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</Section>
|
||||
<Spacer />
|
@ -12,7 +12,6 @@
|
||||
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
|
||||
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
|
||||
import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.svelte';
|
||||
import { gitHostUsePullRequestTemplate } from '$lib/config/config';
|
||||
import { featureTopics } from '$lib/config/uiFeatureFlags';
|
||||
import { ReorderDropzoneManagerFactory } from '$lib/dragging/reorderDropzoneManager';
|
||||
import { DefaultGitHostFactory } from '$lib/gitHost/gitHostFactory';
|
||||
@ -86,7 +85,6 @@
|
||||
let intervalId: any;
|
||||
|
||||
const showHistoryView = persisted(false, 'showHistoryView');
|
||||
const usePullRequestTemplate = gitHostUsePullRequestTemplate();
|
||||
const octokit = $derived(accessToken ? octokitFromAccessToken(accessToken) : undefined);
|
||||
const gitHostFactory = $derived(new DefaultGitHostFactory(octokit));
|
||||
const repoInfo = $derived(remoteUrl ? parseRemoteUrl(remoteUrl) : undefined);
|
||||
@ -118,7 +116,6 @@
|
||||
// Refresh base branch if git fetch event is detected.
|
||||
const mode = $derived(modeService.mode);
|
||||
const head = $derived(modeService.head);
|
||||
|
||||
// We end up with a `state_unsafe_mutation` when switching projects if we
|
||||
// don't use $effect.pre here.
|
||||
// TODO: can we eliminate the need to debounce?
|
||||
@ -137,7 +134,7 @@
|
||||
$effect.pre(() => {
|
||||
const gitHost =
|
||||
repoInfo && baseBranchName
|
||||
? gitHostFactory.build(repoInfo, baseBranchName, forkInfo, usePullRequestTemplate)
|
||||
? gitHostFactory.build(repoInfo, baseBranchName, forkInfo)
|
||||
: undefined;
|
||||
|
||||
const ghListService = gitHost?.listService();
|
||||
|
@ -4,16 +4,16 @@
|
||||
import RemoveProjectButton from '$lib/components/RemoveProjectButton.svelte';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import { featureBaseBranchSwitching } from '$lib/config/uiFeatureFlags';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import SettingsPage from '$lib/layout/SettingsPage.svelte';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
import { platformName } from '$lib/platform/platform';
|
||||
import CloudForm from '$lib/settings/CloudForm.svelte';
|
||||
import DetailsForm from '$lib/settings/DetailsForm.svelte';
|
||||
import GitHostForm from '$lib/settings/GitHostForm.svelte';
|
||||
import KeysForm from '$lib/settings/KeysForm.svelte';
|
||||
import PreferencesForm from '$lib/settings/PreferencesForm.svelte';
|
||||
import PullRequestTemplateForm from '$lib/settings/PullRequestTemplateForm.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { goto } from '$app/navigation';
|
||||
@ -21,11 +21,10 @@
|
||||
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||
const projectService = getContext(ProjectService);
|
||||
const project = getContext(Project);
|
||||
const userService = getContext(UserService);
|
||||
const user = userService.user;
|
||||
const gitHost = getGitHost();
|
||||
|
||||
let deleteConfirmationModal: RemoveProjectButton;
|
||||
let isDeleting = false;
|
||||
let isDeleting = $state(false);
|
||||
|
||||
async function onDeleteClicked() {
|
||||
isDeleting = true;
|
||||
@ -49,8 +48,8 @@
|
||||
{/if}
|
||||
<CloudForm />
|
||||
<DetailsForm />
|
||||
{#if $user?.github_access_token}
|
||||
<GitHostForm />
|
||||
{#if $gitHost}
|
||||
<PullRequestTemplateForm />
|
||||
{/if}
|
||||
{#if $platformName !== 'win32'}
|
||||
<KeysForm showProjectName={false} />
|
||||
|
@ -112,3 +112,20 @@ pub fn read_toml_file_or_default<T: DeserializeOwned + Default>(path: &Path) ->
|
||||
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Reads file from disk at workspace
|
||||
pub fn read_file_from_workspace(path: &Path) -> Result<String> {
|
||||
let mut file = match File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error {}\n\nUnable to read file: {}",
|
||||
err,
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ mod project;
|
||||
mod storage;
|
||||
|
||||
pub use controller::Controller;
|
||||
pub use project::{ApiProject, AuthKey, CodePushState, FetchResult, Project, ProjectId};
|
||||
pub use project::{
|
||||
ApiProject, AuthKey, CodePushState, FetchResult, GitHostSettings, Project, ProjectId,
|
||||
};
|
||||
pub use storage::UpdateRequest;
|
||||
|
||||
/// A utility to be used from applications to optimize `git2` configuration.
|
||||
|
@ -96,6 +96,17 @@ pub struct Project {
|
||||
pub snapshot_lines_threshold: Option<usize>,
|
||||
#[serde(default = "default_false")]
|
||||
pub succeeding_rebases: bool,
|
||||
#[serde(default)]
|
||||
pub git_host: GitHostSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GitHostSettings {
|
||||
#[serde(default)]
|
||||
pub host_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub pull_request_template_path: Option<String>,
|
||||
}
|
||||
|
||||
fn default_false() -> bool {
|
||||
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{ApiProject, AuthKey, CodePushState, FetchResult, Project, ProjectId};
|
||||
use crate::{ApiProject, AuthKey, CodePushState, FetchResult, GitHostSettings, Project, ProjectId};
|
||||
|
||||
const PROJECTS_FILE: &str = "projects.json";
|
||||
|
||||
@ -28,6 +28,7 @@ pub struct UpdateRequest {
|
||||
pub use_diff_context: Option<bool>,
|
||||
pub snapshot_lines_threshold: Option<usize>,
|
||||
pub succeeding_rebases: Option<bool>,
|
||||
pub git_host: Option<GitHostSettings>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
@ -128,6 +129,10 @@ impl Storage {
|
||||
project.succeeding_rebases = succeeding_rebases;
|
||||
}
|
||||
|
||||
if let Some(git_host) = &update_request.git_host {
|
||||
project.git_host = git_host.clone();
|
||||
}
|
||||
|
||||
self.inner
|
||||
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;
|
||||
|
||||
|
@ -54,6 +54,7 @@ gitbutler-oplog.workspace = true
|
||||
gitbutler-repo.workspace = true
|
||||
gitbutler-command-context.workspace = true
|
||||
gitbutler-feedback.workspace = true
|
||||
gitbutler-fs.workspace = true
|
||||
gitbutler-config.workspace = true
|
||||
gitbutler-project.workspace = true
|
||||
gitbutler-user.workspace = true
|
||||
|
@ -1,7 +1,8 @@
|
||||
pub mod commands {
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_fs::list_files;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
@ -79,4 +80,28 @@ pub mod commands {
|
||||
.context("Failed to parse response body")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument]
|
||||
pub fn available_pull_request_templates(root_path: &path::Path) -> Result<Vec<String>, Error> {
|
||||
let walked_paths = list_files(root_path, &[root_path])?;
|
||||
|
||||
let mut available_paths = Vec::new();
|
||||
for entry in walked_paths {
|
||||
let path_entry = entry.as_path();
|
||||
let path_str = path_entry.to_string_lossy();
|
||||
// TODO: Refactor these paths out in the future to something like a common
|
||||
// gitHosts.pullRequestTemplatePaths map, an entry for each gitHost type and
|
||||
// their valid files / directories. So that this 'get_available_templates'
|
||||
// can be more generic and we can add / modify paths more easily for all supported githost types
|
||||
if path_str == "PULL_REQUEST_TEMPLATE.md"
|
||||
|| path_str == "pull_request_template.md"
|
||||
|| path_str.contains("PULL_REQUEST_TEMPLATE/")
|
||||
{
|
||||
available_paths.push(root_path.join(path_entry).to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(available_paths)
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ fn main() {
|
||||
projects::commands::list_projects,
|
||||
projects::commands::set_project_active,
|
||||
projects::commands::open_project_in_window,
|
||||
projects::commands::get_pr_template_contents,
|
||||
repo::commands::git_get_local_config,
|
||||
repo::commands::git_set_local_config,
|
||||
repo::commands::check_signing_settings,
|
||||
@ -200,6 +201,7 @@ fn main() {
|
||||
menu::get_editor_link_scheme,
|
||||
github::commands::init_device_oauth,
|
||||
github::commands::check_auth_status,
|
||||
github::commands::available_pull_request_templates,
|
||||
askpass::commands::submit_prompt_response,
|
||||
remotes::list_remotes,
|
||||
remotes::add_remote,
|
||||
|
@ -4,6 +4,7 @@ pub mod commands {
|
||||
use std::path;
|
||||
|
||||
use anyhow::Context;
|
||||
use gitbutler_fs::read_file_from_workspace;
|
||||
use gitbutler_project::{self as projects, Controller, ProjectId};
|
||||
use tauri::{State, Window};
|
||||
use tracing::instrument;
|
||||
@ -96,6 +97,19 @@ pub mod commands {
|
||||
pub fn delete_project(projects: State<'_, Controller>, id: ProjectId) -> Result<(), Error> {
|
||||
projects.delete(id).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects))]
|
||||
pub fn get_pr_template_contents(
|
||||
projects: State<'_, Controller>,
|
||||
relative_path: &path::Path,
|
||||
project_id: ProjectId,
|
||||
) -> Result<String, Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
let template_path = project.path.join(relative_path);
|
||||
|
||||
Ok(read_file_from_workspace(template_path.as_path())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
|
Loading…
Reference in New Issue
Block a user