Ask user to select PR templates when forking a repository (#143733)
* Add getPullRequestTemplates method to discover templates
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Add method to quick pick for PR templates
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Handle possible PR templates
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Remove unnecessary return value assignment
Co-authored-by: João Moreno <mail@joaomoreno.com>
* Change comparison operands' order
Co-authored-by: João Moreno <mail@joaomoreno.com>
* Remove sorting template URIs in pickPullRequestTemplate
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Sort template URIs before showing quick-pick list
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Rename getPullRequestTemplates method to findPullRequestTemplates
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Find Github PR templates in-parallel using readdir/stat
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Export method for visibitliy in tests
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Add tests for Github PR template detection
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Add launcher configration to run Github tests
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* 💄
* Replace stat with readDirectory for OS native case sensitivity
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Delete some files to avoid duplicate names on case insensitive envs
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
* Exclude deleted files from test case expected result
Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
Co-authored-by: João Moreno <mail@joaomoreno.com>
Co-authored-by: João Moreno <joao.moreno@microsoft.com>
pull/146588/head
parent
62ace5901d
commit
7fc55261aa
|
@ -114,6 +114,24 @@
|
|||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Github Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/github/testWorkspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/github",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/github/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/github/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { commands, env, ProgressLocation, Uri, window } from 'vscode';
|
||||
import { TextDecoder } from 'util';
|
||||
import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { getOctokit } from './auth';
|
||||
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
|
||||
import path = require('path');
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
@ -103,10 +105,24 @@ async function handlePushError(repository: Repository, remote: Remote, refspec:
|
|||
title = commit.message.replace(/\n.*$/m, '');
|
||||
}
|
||||
|
||||
let body: string | undefined;
|
||||
|
||||
const templates = await findPullRequestTemplates(repository.rootUri);
|
||||
if (templates.length > 0) {
|
||||
templates.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
const template = await pickPullRequestTemplate(templates);
|
||||
|
||||
if (template) {
|
||||
body = new TextDecoder('utf-8').decode(await workspace.fs.readFile(template));
|
||||
}
|
||||
}
|
||||
|
||||
const res = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
head: `${ghRepository.owner.login}:${remoteName}`,
|
||||
base: remoteName
|
||||
});
|
||||
|
@ -128,6 +144,67 @@ async function handlePushError(repository: Repository, remote: Remote, refspec:
|
|||
})();
|
||||
}
|
||||
|
||||
const PR_TEMPLATE_FILES = [
|
||||
{ dir: '.', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
|
||||
{ dir: 'docs', files: ['pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'] },
|
||||
{ dir: '.github', files: ['PULL_REQUEST_TEMPLATE.md', 'PULL_REQUEST_TEMPLATE.md'] }
|
||||
];
|
||||
|
||||
const PR_TEMPLATE_DIRECTORY_NAMES = [
|
||||
'PULL_REQUEST_TEMPLATE',
|
||||
'docs/PULL_REQUEST_TEMPLATE',
|
||||
'.github/PULL_REQUEST_TEMPLATE'
|
||||
];
|
||||
|
||||
async function assertMarkdownFiles(dir: Uri, files: string[]): Promise<Uri[]> {
|
||||
const dirFiles = await workspace.fs.readDirectory(dir);
|
||||
return dirFiles
|
||||
.filter(([name, type]) => Boolean(type & FileType.File) && files.indexOf(name) !== -1)
|
||||
.map(([name]) => Uri.joinPath(dir, name));
|
||||
}
|
||||
|
||||
async function findMarkdownFilesInDir(uri: Uri): Promise<Uri[]> {
|
||||
const files = await workspace.fs.readDirectory(uri);
|
||||
return files
|
||||
.filter(([name, type]) => Boolean(type & FileType.File) && path.extname(name) === '.md')
|
||||
.map(([name]) => Uri.joinPath(uri, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* PR templates can be:
|
||||
* - In the root, `docs`, or `.github` folders, called `pull_request_template.md` or `PULL_REQUEST_TEMPLATE.md`
|
||||
* - Or, in a `PULL_REQUEST_TEMPLATE` directory directly below the root, `docs`, or `.github` folders, called `*.md`
|
||||
*
|
||||
* NOTE This method is a modified copy of a method with same name at microsoft/vscode-pull-request-github repository:
|
||||
* https://github.com/microsoft/vscode-pull-request-github/blob/0a0c3c6c21c0b9c2f4d5ffbc3f8c6a825472e9e6/src/github/folderRepositoryManager.ts#L1061
|
||||
*
|
||||
*/
|
||||
export async function findPullRequestTemplates(repositoryRootUri: Uri): Promise<Uri[]> {
|
||||
const results = await Promise.allSettled([
|
||||
...PR_TEMPLATE_FILES.map(x => assertMarkdownFiles(Uri.joinPath(repositoryRootUri, x.dir), x.files)),
|
||||
...PR_TEMPLATE_DIRECTORY_NAMES.map(x => findMarkdownFilesInDir(Uri.joinPath(repositoryRootUri, x)))
|
||||
]);
|
||||
|
||||
return results.flatMap(x => x.status === 'fulfilled' && x.value || []);
|
||||
}
|
||||
|
||||
export async function pickPullRequestTemplate(templates: Uri[]): Promise<Uri | undefined> {
|
||||
const quickPickItemFromUri = (x: Uri) => ({ label: x.path, template: x });
|
||||
const quickPickItems = [
|
||||
{
|
||||
label: localize('no pr template', "No template"),
|
||||
picked: true,
|
||||
template: undefined,
|
||||
},
|
||||
...templates.map(quickPickItemFromUri)
|
||||
];
|
||||
const quickPickOptions: QuickPickOptions = {
|
||||
placeHolder: localize('select pr template', "Select the Pull Request template")
|
||||
};
|
||||
const pickedTemplate = await window.showQuickPick(quickPickItems, quickPickOptions);
|
||||
return pickedTemplate?.template;
|
||||
}
|
||||
|
||||
export class GithubPushErrorHandler implements PushErrorHandler {
|
||||
|
||||
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean> {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { workspace, extensions, Uri, commands } from 'vscode';
|
||||
import { findPullRequestTemplates, pickPullRequestTemplate } from '../pushErrorHandler';
|
||||
|
||||
suite('github smoke test', function () {
|
||||
const cwd = workspace.workspaceFolders![0].uri;
|
||||
|
||||
suiteSetup(async function () {
|
||||
const ext = extensions.getExtension('vscode.github');
|
||||
await ext?.activate();
|
||||
});
|
||||
|
||||
test('should find all templates', async function () {
|
||||
const expectedValuesSorted = [
|
||||
'/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'/docs/PULL_REQUEST_TEMPLATE.md',
|
||||
'/docs/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'/docs/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'/.github/PULL_REQUEST_TEMPLATE.md',
|
||||
'/.github/PULL_REQUEST_TEMPLATE/a.md',
|
||||
'/.github/PULL_REQUEST_TEMPLATE/b.md',
|
||||
'/PULL_REQUEST_TEMPLATE.md'
|
||||
];
|
||||
expectedValuesSorted.sort();
|
||||
|
||||
const uris = await findPullRequestTemplates(cwd);
|
||||
|
||||
const urisSorted = uris.map(x => x.path.slice(cwd.path.length));
|
||||
urisSorted.sort();
|
||||
|
||||
assert.deepStrictEqual(urisSorted, expectedValuesSorted);
|
||||
});
|
||||
|
||||
test('selecting non-default quick-pick item should correspond to a template', async () => {
|
||||
const template0 = Uri.file("some-imaginary-template-0");
|
||||
const template1 = Uri.file("some-imaginary-template-1");
|
||||
const templates = [template0, template1];
|
||||
|
||||
const pick = pickPullRequestTemplate(templates);
|
||||
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
|
||||
|
||||
assert.ok(await pick === template0);
|
||||
});
|
||||
|
||||
test('selecting first quick-pick item should return undefined', async () => {
|
||||
const templates = [Uri.file("some-imaginary-file")];
|
||||
|
||||
const pick = pickPullRequestTemplate(templates);
|
||||
|
||||
await commands.executeCommand('workbench.action.quickOpenSelectNext');
|
||||
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
|
||||
|
||||
assert.ok(await pick === undefined);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const path = require('path');
|
||||
const testRunner = require('../../../../test/integration/electron/testrunner');
|
||||
|
||||
const suite = 'Github Tests';
|
||||
|
||||
const options: any = {
|
||||
ui: 'tdd',
|
||||
color: true,
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testRunner.configure(options);
|
||||
|
||||
export = testRunner;
|
Loading…
Reference in New Issue