Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface CreateParamsNew {

creating: boolean;
reviewing: boolean;
usingTemplate: boolean;
}

export interface ChooseRemoteAndBranchArgs {
Expand Down
1 change: 1 addition & 0 deletions resources/icons/codicons/notebook-template.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 49 additions & 3 deletions src/github/createPRViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { PullRequestModel } from './pullRequestModel';
import { getDefaultMergeMethod } from './pullRequestOverview';
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils';
import { DisplayLabel, PreReviewState } from './views';
import { ChangeTemplateReply, DisplayLabel, PreReviewState } from './views';
import { RemoteInfo } from '../../common/types';
import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, TitleAndDescriptionArgs } from '../../common/views';
import type { Branch, Ref } from '../api/api';
Expand Down Expand Up @@ -245,7 +245,9 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
}
commands.setContext(contexts.CREATE_PR_PERMISSIONS, viewerPermission);

const useCopilot: boolean = !!this.getTitleAndDescriptionProvider('Copilot') && (vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'commit' | 'template' | 'branchName' | 'none' | 'Copilot'>(PULL_REQUEST_DESCRIPTION) === 'Copilot');
const descriptionSource = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'commit' | 'template' | 'none' | 'Copilot'>(PULL_REQUEST_DESCRIPTION);
const useCopilot: boolean = !!this.getTitleAndDescriptionProvider('Copilot') && (descriptionSource === 'Copilot');
const usingTemplate: boolean = descriptionSource === 'template';
const defaultTitleAndDescriptionProvider = this.getTitleAndDescriptionProvider()?.title;
if (defaultTitleAndDescriptionProvider) {
/* __GDPR__
Expand Down Expand Up @@ -282,7 +284,8 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
initializeWithGeneratedTitleAndDescription: useCopilot,
preReviewState: PreReviewState.None,
preReviewer: preReviewer?.title,
reviewing: false
reviewing: false,
usingTemplate
};

return params;
Expand Down Expand Up @@ -799,6 +802,46 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
return this._folderRepositoryManager.getPullRequestTemplateBody(this.model.baseOwner);
}

private async changeTemplate(message: IRequestMessage<any>): Promise<void> {
const templates = await this._folderRepositoryManager.getAllPullRequestTemplates(this.model.baseOwner);

if (!templates || templates.length === 0) {
vscode.window.showInformationMessage(vscode.l10n.t('No pull request templates found'));
return this._replyMessage(message, undefined);
}

if (templates.length === 1) {
vscode.window.showInformationMessage(vscode.l10n.t('Only one template is available'));
return this._replyMessage(message, undefined);
}

// Multiple templates exist - show quick pick
const selectedTemplate = await vscode.window.showQuickPick(
templates.map((template, index) => {
// Try to extract a meaningful name from the template (first line or first few chars)
const firstLine = template.split('\n')[0].trim();
const label = firstLine || `Template ${index + 1}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the 'Template ...' be a localized string?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should, and I could have sworn that I made that change...

return {
label: label.substring(0, 50) + (label.length > 50 ? '...' : ''),
description: `${template.length} characters`,
template: template
};
}),
{
placeHolder: vscode.l10n.t('Select a pull request template'),
ignoreFocusOut: true
}
);

if (selectedTemplate) {
const reply: ChangeTemplateReply = {
description: selectedTemplate.template
};
return this._replyMessage(message, reply);
}
return this._replyMessage(message, undefined);
}

protected async detectBaseMetadata(defaultCompareBranch: Branch): Promise<BaseBranchMetadata | undefined> {
const owner = this.model.compareOwner;
const repositoryName = this.model.repositoryName;
Expand Down Expand Up @@ -1413,6 +1456,9 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
case 'pr.cancelGenerateTitleAndDescription':
return this.cancelGenerateTitleAndDescription();

case 'pr.changeTemplate':
return this.changeTemplate(message);

case 'pr.preReview':
return this.preReview(message);

Expand Down
23 changes: 23 additions & 0 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,29 @@ export class FolderRepositoryManager extends Disposable {
}
}

async getAllPullRequestTemplates(owner: string): Promise<string[] | undefined> {
try {
const repository = this.gitHubRepositories.find(repo => repo.remote.owner === owner);
if (!repository) {
return undefined;
}
const templates = await repository.getPullRequestTemplates();
if (templates && templates.length > 0) {
return templates;
}

// If there's no local template, look for owner-wide templates
const githubRepository = await this.createGitHubRepositoryFromOwnerName(owner, '.github');
if (!githubRepository) {
return undefined;
}
return githubRepository.getPullRequestTemplates();
} catch (e) {
Logger.error(`Error fetching pull request templates for ${owner}: ${e instanceof Error ? e.message : e}`, this.id);
return undefined;
}
}

private async getPullRequestTemplateWithCache(owner: string): Promise<string | undefined> {
const cacheLocation = `${CACHED_TEMPLATE_BODY}+${this.repository.rootUri.toString()}`;

Expand Down
4 changes: 4 additions & 0 deletions src/github/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ export enum PreReviewState {
ReviewedWithoutComments
}

export interface ChangeTemplateReply {
description: string;
}

export interface CancelCodingAgentReply {
events: TimelineEvent[];
}
Expand Down
1 change: 1 addition & 0 deletions webviews/common/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ body img.avatar {
.section .icon-button:hover,
.section .icon-button:focus {
background-color: var(--vscode-toolbar-hoverBackground);
cursor: pointer;
}

.icon-button:focus,
Expand Down
3 changes: 2 additions & 1 deletion webviews/common/createContextNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const defaultCreateParams: CreateParamsNew = {
baseHasMergeQueue: false,
preReviewState: PreReviewState.None,
preReviewer: undefined,
reviewing: false
reviewing: false,
usingTemplate: false
};

export class CreatePRContextNew {
Expand Down
1 change: 1 addition & 0 deletions webviews/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const gitPullRequestIcon = <Icon src={require('../../resources/icons/codi
export const issuescon = <Icon src={require('../../resources/icons/codicons/issues.svg')} />;
export const loadingIcon = <Icon className='loading' src={require('../../resources/icons/codicons/loading.svg')} />;
export const milestoneIcon = <Icon src={require('../../resources/icons/codicons/milestone.svg')} />;
export const notebookTemplate = <Icon src={require('../../resources/icons/codicons/notebook-template.svg')} />;
export const passIcon = <Icon src={require('../../resources/icons/codicons/pass.svg')} />;
export const projectIcon = <Icon src={require('../../resources/icons/codicons/github-project.svg')} />;
export const quoteIcon = <Icon src={require('../../resources/icons/codicons/quote.svg')} />;
Expand Down
16 changes: 14 additions & 2 deletions webviews/createPullRequestViewNew/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { render } from 'react-dom';
import { RemoteInfo } from '../../common/types';
import { CreateParamsNew } from '../../common/views';
import { isITeam, MergeMethod } from '../../src/github/interface';
import { ChangeTemplateReply } from '../../src/github/views';
import PullRequestContextNew from '../common/createContextNew';
import { ErrorBoundary } from '../common/errorBoundary';
import { LabelCreate } from '../common/label';
import { ContextDropdown } from '../components/contextDropdown';
import { accountIcon, feedbackIcon, gitCompareIcon, milestoneIcon, prMergeIcon, projectIcon, sparkleIcon, stopCircleIcon, tagIcon } from '../components/icon';
import { accountIcon, feedbackIcon, gitCompareIcon, milestoneIcon, notebookTemplate, prMergeIcon, projectIcon, sparkleIcon, stopCircleIcon, tagIcon } from '../components/icon';
import { Avatar } from '../components/user';

type CreateMethod = 'create-draft' | 'create' | 'create-automerge-squash' | 'create-automerge-rebase' | 'create-automerge-merge';
Expand Down Expand Up @@ -178,6 +179,13 @@ export function main() {
setGeneratingTitle(false);
}

async function changeTemplate() {
const result: ChangeTemplateReply = await ctx.postMessage({ command: 'pr.changeTemplate' });
if (result && result.description) {
ctx.updateState({ pendingDescription: result.description });
}
}


if (!ctx.initialized) {
ctx.initialize();
Expand Down Expand Up @@ -325,7 +333,11 @@ export function main() {
: null}
</div>

<label htmlFor='description' className='input-title'>Description</label>
<div className='description-title'>
<label htmlFor='description' className='input-title'>Description</label>
{ctx.createParams.usingTemplate ?
<a title='Change template' className={`title-action icon-button${isBusy || !ctx.initialized ? ' disabled' : ''}`} onClick={() => changeTemplate()} tabIndex={0}>{notebookTemplate}</a> : null}
</div>
<div className='group-description'>
<textarea
id='description'
Expand Down
6 changes: 6 additions & 0 deletions webviews/createPullRequestViewNew/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ button.input-box {
text-overflow: ellipsis;
}

.description-title {
display: flex;
justify-content: space-between;
align-items: center;
}

.group-title {
position: relative;
display: flex;
Expand Down