Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ask user to select PR templates when forking a repository #143733

Merged
merged 17 commits into from Apr 1, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions .vscode/launch.json
Expand Up @@ -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",
Expand Down
79 changes: 78 additions & 1 deletion extensions/github/src/pushErrorHandler.ts
Expand Up @@ -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';
joaomoreno marked this conversation as resolved.
Show resolved Hide resolved
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();

Expand Down Expand Up @@ -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
});
Expand All @@ -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> {
Expand Down
65 changes: 65 additions & 0 deletions 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);
});
});
30 changes: 30 additions & 0 deletions 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;
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.