Skip to content

Commit 9b7cc20

Browse files
Copilotalexr00
andauthored
Add support for multiple PR templates selection (#8269)
* Initial plan * Initial plan for multiple PR template support Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add createFromTemplate setting with none, first, and prompt options Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Replace createFromTemplate setting with templatePrompt option in pullRequestDescription Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add change template icon button next to Description when using template setting Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * 💄 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 02e2df7 commit 9b7cc20

10 files changed

Lines changed: 102 additions & 6 deletions

File tree

common/views.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface CreateParamsNew {
130130

131131
creating: boolean;
132132
reviewing: boolean;
133+
usingTemplate: boolean;
133134
}
134135

135136
export interface ChooseRemoteAndBranchArgs {
Lines changed: 1 addition & 0 deletions
Loading

src/github/createPRViewProvider.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { PullRequestModel } from './pullRequestModel';
1717
import { getDefaultMergeMethod } from './pullRequestOverview';
1818
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
1919
import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils';
20-
import { DisplayLabel, PreReviewState } from './views';
20+
import { ChangeTemplateReply, DisplayLabel, PreReviewState } from './views';
2121
import { RemoteInfo } from '../../common/types';
2222
import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, TitleAndDescriptionArgs } from '../../common/views';
2323
import type { Branch, Ref } from '../api/api';
@@ -245,7 +245,9 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
245245
}
246246
commands.setContext(contexts.CREATE_PR_PERMISSIONS, viewerPermission);
247247

248-
const useCopilot: boolean = !!this.getTitleAndDescriptionProvider('Copilot') && (vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'commit' | 'template' | 'branchName' | 'none' | 'Copilot'>(PULL_REQUEST_DESCRIPTION) === 'Copilot');
248+
const descriptionSource = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'commit' | 'template' | 'none' | 'Copilot'>(PULL_REQUEST_DESCRIPTION);
249+
const useCopilot: boolean = !!this.getTitleAndDescriptionProvider('Copilot') && (descriptionSource === 'Copilot');
250+
const usingTemplate: boolean = descriptionSource === 'template';
249251
const defaultTitleAndDescriptionProvider = this.getTitleAndDescriptionProvider()?.title;
250252
if (defaultTitleAndDescriptionProvider) {
251253
/* __GDPR__
@@ -282,7 +284,8 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
282284
initializeWithGeneratedTitleAndDescription: useCopilot,
283285
preReviewState: PreReviewState.None,
284286
preReviewer: preReviewer?.title,
285-
reviewing: false
287+
reviewing: false,
288+
usingTemplate
286289
};
287290

288291
return params;
@@ -799,6 +802,46 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
799802
return this._folderRepositoryManager.getPullRequestTemplateBody(this.model.baseOwner);
800803
}
801804

805+
private async changeTemplate(message: IRequestMessage<any>): Promise<void> {
806+
const templates = await this._folderRepositoryManager.getAllPullRequestTemplates(this.model.baseOwner);
807+
808+
if (!templates || templates.length === 0) {
809+
vscode.window.showInformationMessage(vscode.l10n.t('No pull request templates found'));
810+
return this._replyMessage(message, undefined);
811+
}
812+
813+
if (templates.length === 1) {
814+
vscode.window.showInformationMessage(vscode.l10n.t('Only one template is available'));
815+
return this._replyMessage(message, undefined);
816+
}
817+
818+
// Multiple templates exist - show quick pick
819+
const selectedTemplate = await vscode.window.showQuickPick(
820+
templates.map((template, index) => {
821+
// Try to extract a meaningful name from the template (first line or first few chars)
822+
const firstLine = template.split('\n')[0].trim();
823+
const label = firstLine || `Template ${index + 1}`;
824+
return {
825+
label: label.substring(0, 50) + (label.length > 50 ? '...' : ''),
826+
description: `${template.length} characters`,
827+
template: template
828+
};
829+
}),
830+
{
831+
placeHolder: vscode.l10n.t('Select a pull request template'),
832+
ignoreFocusOut: true
833+
}
834+
);
835+
836+
if (selectedTemplate) {
837+
const reply: ChangeTemplateReply = {
838+
description: selectedTemplate.template
839+
};
840+
return this._replyMessage(message, reply);
841+
}
842+
return this._replyMessage(message, undefined);
843+
}
844+
802845
protected async detectBaseMetadata(defaultCompareBranch: Branch): Promise<BaseBranchMetadata | undefined> {
803846
const owner = this.model.compareOwner;
804847
const repositoryName = this.model.repositoryName;
@@ -1413,6 +1456,9 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv
14131456
case 'pr.cancelGenerateTitleAndDescription':
14141457
return this.cancelGenerateTitleAndDescription();
14151458

1459+
case 'pr.changeTemplate':
1460+
return this.changeTemplate(message);
1461+
14161462
case 'pr.preReview':
14171463
return this.preReview(message);
14181464

src/github/folderRepositoryManager.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,29 @@ export class FolderRepositoryManager extends Disposable {
14861486
}
14871487
}
14881488

1489+
async getAllPullRequestTemplates(owner: string): Promise<string[] | undefined> {
1490+
try {
1491+
const repository = this.gitHubRepositories.find(repo => repo.remote.owner === owner);
1492+
if (!repository) {
1493+
return undefined;
1494+
}
1495+
const templates = await repository.getPullRequestTemplates();
1496+
if (templates && templates.length > 0) {
1497+
return templates;
1498+
}
1499+
1500+
// If there's no local template, look for owner-wide templates
1501+
const githubRepository = await this.createGitHubRepositoryFromOwnerName(owner, '.github');
1502+
if (!githubRepository) {
1503+
return undefined;
1504+
}
1505+
return githubRepository.getPullRequestTemplates();
1506+
} catch (e) {
1507+
Logger.error(`Error fetching pull request templates for ${owner}: ${e instanceof Error ? e.message : e}`, this.id);
1508+
return undefined;
1509+
}
1510+
}
1511+
14891512
private async getPullRequestTemplateWithCache(owner: string): Promise<string | undefined> {
14901513
const cacheLocation = `${CACHED_TEMPLATE_BODY}+${this.repository.rootUri.toString()}`;
14911514

src/github/views.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ export enum PreReviewState {
163163
ReviewedWithoutComments
164164
}
165165

166+
export interface ChangeTemplateReply {
167+
description: string;
168+
}
169+
166170
export interface CancelCodingAgentReply {
167171
events: TimelineEvent[];
168172
}

webviews/common/common.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ body img.avatar {
176176
.section .icon-button:hover,
177177
.section .icon-button:focus {
178178
background-color: var(--vscode-toolbar-hoverBackground);
179+
cursor: pointer;
179180
}
180181

181182
.icon-button:focus,

webviews/common/createContextNew.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const defaultCreateParams: CreateParamsNew = {
3434
baseHasMergeQueue: false,
3535
preReviewState: PreReviewState.None,
3636
preReviewer: undefined,
37-
reviewing: false
37+
reviewing: false,
38+
usingTemplate: false
3839
};
3940

4041
export class CreatePRContextNew {

webviews/components/icon.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const gitPullRequestIcon = <Icon src={require('../../resources/icons/codi
3434
export const issuescon = <Icon src={require('../../resources/icons/codicons/issues.svg')} />;
3535
export const loadingIcon = <Icon className='loading' src={require('../../resources/icons/codicons/loading.svg')} />;
3636
export const milestoneIcon = <Icon src={require('../../resources/icons/codicons/milestone.svg')} />;
37+
export const notebookTemplate = <Icon src={require('../../resources/icons/codicons/notebook-template.svg')} />;
3738
export const passIcon = <Icon src={require('../../resources/icons/codicons/pass.svg')} />;
3839
export const projectIcon = <Icon src={require('../../resources/icons/codicons/github-project.svg')} />;
3940
export const quoteIcon = <Icon src={require('../../resources/icons/codicons/quote.svg')} />;

webviews/createPullRequestViewNew/app.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import { render } from 'react-dom';
88
import { RemoteInfo } from '../../common/types';
99
import { CreateParamsNew } from '../../common/views';
1010
import { isITeam, MergeMethod } from '../../src/github/interface';
11+
import { ChangeTemplateReply } from '../../src/github/views';
1112
import PullRequestContextNew from '../common/createContextNew';
1213
import { ErrorBoundary } from '../common/errorBoundary';
1314
import { LabelCreate } from '../common/label';
1415
import { ContextDropdown } from '../components/contextDropdown';
15-
import { accountIcon, feedbackIcon, gitCompareIcon, milestoneIcon, prMergeIcon, projectIcon, sparkleIcon, stopCircleIcon, tagIcon } from '../components/icon';
16+
import { accountIcon, feedbackIcon, gitCompareIcon, milestoneIcon, notebookTemplate, prMergeIcon, projectIcon, sparkleIcon, stopCircleIcon, tagIcon } from '../components/icon';
1617
import { Avatar } from '../components/user';
1718

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

182+
async function changeTemplate() {
183+
const result: ChangeTemplateReply = await ctx.postMessage({ command: 'pr.changeTemplate' });
184+
if (result && result.description) {
185+
ctx.updateState({ pendingDescription: result.description });
186+
}
187+
}
188+
181189

182190
if (!ctx.initialized) {
183191
ctx.initialize();
@@ -325,7 +333,11 @@ export function main() {
325333
: null}
326334
</div>
327335

328-
<label htmlFor='description' className='input-title'>Description</label>
336+
<div className='description-title'>
337+
<label htmlFor='description' className='input-title'>Description</label>
338+
{ctx.createParams.usingTemplate ?
339+
<a title='Change template' className={`title-action icon-button${isBusy || !ctx.initialized ? ' disabled' : ''}`} onClick={() => changeTemplate()} tabIndex={0}>{notebookTemplate}</a> : null}
340+
</div>
329341
<div className='group-description'>
330342
<textarea
331343
id='description'

webviews/createPullRequestViewNew/index.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ button.input-box {
9898
text-overflow: ellipsis;
9999
}
100100

101+
.description-title {
102+
display: flex;
103+
justify-content: space-between;
104+
align-items: center;
105+
}
106+
101107
.group-title {
102108
position: relative;
103109
display: flex;

0 commit comments

Comments
 (0)