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:
Esteban Vega 2024-09-13 17:25:58 +02:00 committed by GitHub
commit 1a47f5ec4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 292 additions and 158 deletions

1
Cargo.lock generated
View File

@ -2566,6 +2566,7 @@ dependencies = [
"gitbutler-edit-mode",
"gitbutler-error",
"gitbutler-feedback",
"gitbutler-fs",
"gitbutler-id",
"gitbutler-operating-modes",
"gitbutler-oplog",

View File

@ -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() {

View File

@ -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;

View File

@ -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');
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 + '.')) {

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 />

View 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 />

View File

@ -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();

View File

@ -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} />

View File

@ -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)
}

View File

@ -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.

View File

@ -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 {

View File

@ -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)?)?;

View File

@ -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

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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)]