From 7fc55261aa6672a336bc7438abd76dda5789707a Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 1 Apr 2022 14:07:33 +0000 Subject: [PATCH] Ask user to select PR templates when forking a repository (#143733) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add getPullRequestTemplates method to discover templates Signed-off-by: Babak K. Shandiz * Add method to quick pick for PR templates Signed-off-by: Babak K. Shandiz * Handle possible PR templates Signed-off-by: Babak K. Shandiz * Remove unnecessary return value assignment Co-authored-by: João Moreno * Change comparison operands' order Co-authored-by: João Moreno * Remove sorting template URIs in pickPullRequestTemplate Signed-off-by: Babak K. Shandiz * Sort template URIs before showing quick-pick list Signed-off-by: Babak K. Shandiz * Rename getPullRequestTemplates method to findPullRequestTemplates Signed-off-by: Babak K. Shandiz * Find Github PR templates in-parallel using readdir/stat Signed-off-by: Babak K. Shandiz * Export method for visibitliy in tests Signed-off-by: Babak K. Shandiz * Add tests for Github PR template detection Signed-off-by: Babak K. Shandiz * Add launcher configration to run Github tests Signed-off-by: Babak K. Shandiz * :lipstick: * Replace stat with readDirectory for OS native case sensitivity Signed-off-by: Babak K. Shandiz * Delete some files to avoid duplicate names on case insensitive envs Signed-off-by: Babak K. Shandiz * Exclude deleted files from test case expected result Signed-off-by: Babak K. Shandiz Co-authored-by: João Moreno Co-authored-by: João Moreno --- .vscode/launch.json | 18 +++++ extensions/github/src/pushErrorHandler.ts | 79 ++++++++++++++++++- extensions/github/src/test/github.test.ts | 65 +++++++++++++++ extensions/github/src/test/index.ts | 30 +++++++ .../.github/PULL_REQUEST_TEMPLATE.md | 0 .../.github/PULL_REQUEST_TEMPLATE/a.md | 0 .../.github/PULL_REQUEST_TEMPLATE/b.md | 0 .../.github/PULL_REQUEST_TEMPLATE/x.txt | 0 .../testWorkspace/PULL_REQUEST_TEMPLATE.md | 0 .../testWorkspace/PULL_REQUEST_TEMPLATE/a.md | 0 .../testWorkspace/PULL_REQUEST_TEMPLATE/b.md | 0 .../testWorkspace/PULL_REQUEST_TEMPLATE/x.txt | 0 .../docs/PULL_REQUEST_TEMPLATE.md | 0 .../docs/PULL_REQUEST_TEMPLATE/a.md | 0 .../docs/PULL_REQUEST_TEMPLATE/b.md | 0 .../docs/PULL_REQUEST_TEMPLATE/x.txt | 0 .../github/testWorkspace/some-markdown.md | 0 extensions/github/testWorkspace/x.txt | 0 18 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 extensions/github/src/test/github.test.ts create mode 100644 extensions/github/src/test/index.ts create mode 100644 extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/a.md create mode 100644 extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/b.md create mode 100644 extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/x.txt create mode 100644 extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE.md create mode 100644 extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/a.md create mode 100644 extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/b.md create mode 100644 extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/x.txt create mode 100644 extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE.md create mode 100644 extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/a.md create mode 100644 extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/b.md create mode 100644 extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/x.txt create mode 100644 extensions/github/testWorkspace/some-markdown.md create mode 100644 extensions/github/testWorkspace/x.txt diff --git a/.vscode/launch.json b/.vscode/launch.json index 84affe5f920..dd295c02db4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index 73abed1ccd6..5948c526ea1 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { diff --git a/extensions/github/src/test/github.test.ts b/extensions/github/src/test/github.test.ts new file mode 100644 index 00000000000..871a617ab46 --- /dev/null +++ b/extensions/github/src/test/github.test.ts @@ -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); + }); +}); diff --git a/extensions/github/src/test/index.ts b/extensions/github/src/test/index.ts new file mode 100644 index 00000000000..23d36033480 --- /dev/null +++ b/extensions/github/src/test/index.ts @@ -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; diff --git a/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE.md b/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/a.md b/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/a.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/b.md b/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/b.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/x.txt b/extensions/github/testWorkspace/.github/PULL_REQUEST_TEMPLATE/x.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE.md b/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/a.md b/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/a.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/b.md b/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/b.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/x.txt b/extensions/github/testWorkspace/PULL_REQUEST_TEMPLATE/x.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE.md b/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/a.md b/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/a.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/b.md b/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/b.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/x.txt b/extensions/github/testWorkspace/docs/PULL_REQUEST_TEMPLATE/x.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/some-markdown.md b/extensions/github/testWorkspace/some-markdown.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/github/testWorkspace/x.txt b/extensions/github/testWorkspace/x.txt new file mode 100644 index 00000000000..e69de29bb2d