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

Add opt-in to always prompt for repo for issue creation and add comment to issue file specifying the repo #6115

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,11 @@
"type": "boolean",
"default": false,
"description": "%githubPullRequests.showPullRequestNumberInTree.description%"
},
"githubIssues.alwaysPromptForNewIssueRepo": {
"type": "boolean",
"default": false,
"description": "%githubIssues.alwaysPromptForNewIssueRepo.description%"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"githubPullRequests.showPullRequestNumberInTree.description": "Shows the pull request number in the tree view.",
"githubPullRequests.labelCreated.description": "Group of labels that you want to add to the pull request automatically. Labels that don't exist in the repository won't be added.",
"githubPullRequests.labelCreated.label.description": "Each string element is value of label that you want to add",
"githubIssues.alwaysPromptForNewIssueRepo.description": "Enabling will always prompt which repository to create an issue in instead of basing off the current open file.",
"view.github.pull.requests.name": "GitHub Pull Requests",
"view.github.pull.request.name": "GitHub Pull Request",
"view.github.login.name": "Login",
Expand Down
1 change: 1 addition & 0 deletions src/common/settingKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const CREATE_ISSUE_TRIGGERS = 'createIssueTriggers';
export const DEFAULT = 'default';
export const IGNORE_MILESTONES = 'ignoreMilestones';
export const ALLOW_FETCH = 'allowFetch';
export const ALWAYS_PROMPT_FOR_NEW_ISSUE_REPO = 'alwaysPromptForNewIssueRepo';

// git
export const GIT = 'git';
Expand Down
128 changes: 101 additions & 27 deletions src/issues/issueFeatureRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import { GitApiImpl } from '../api/api1';
import Logger from '../common/logger';
import {
ALWAYS_PROMPT_FOR_NEW_ISSUE_REPO,
CREATE_INSERT_FORMAT,
ENABLED,
ISSUE_COMPLETIONS,
Expand All @@ -27,7 +28,10 @@ import { CurrentIssue } from './currentIssue';
import { IssueCompletionProvider } from './issueCompletionProvider';
import {
ASSIGNEES,
extractFolderManagerForRepoUri,
extractMetadataFromFile,
FOLDER_MANAGER_ROOT_URI_QUERY,
IssueFileQuery,
IssueFileSystemProvider,
LABELS,
MILESTONE,
Expand All @@ -36,6 +40,7 @@ import {
NewIssueCache,
NewIssueFileCompletionProvider,
PROJECTS,
REPO_SCHEME,
} from './issueFile';
import { IssueHoverProvider } from './issueHoverProvider';
import { openCodeLink } from './issueLinkLookup';
Expand All @@ -57,6 +62,8 @@ import {
pushAndCreatePR,
USER_EXPRESSION,
} from './util';
import { Remote } from '../api/api';
import { Protocol } from '../common/protocol';

const CREATING_ISSUE_FROM_FILE_CONTEXT = 'issues.creatingFromFile';

Expand Down Expand Up @@ -619,7 +626,9 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
async createIssue() {
let uri = vscode.window.activeTextEditor?.document.uri;
let folderManager: FolderRepositoryManager | undefined = uri ? this.manager.getManagerForFile(uri) : undefined;
if (!folderManager) {

const alwaysPrompt = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE).get<boolean>(ALWAYS_PROMPT_FOR_NEW_ISSUE_REPO);
if (!folderManager || alwaysPrompt) {
folderManager = await this.chooseRepo(vscode.l10n.t('Select the repo to create the issue in.'));
uri = folderManager?.repository.rootUri;
}
Expand All @@ -629,10 +638,30 @@ export class IssueFeatureRegistrar implements vscode.Disposable {

const template = await this.chooseTemplate(folderManager);
this._newIssueCache.clear();

const remoteName = folderManager.repository.state.HEAD?.upstream?.remote;
let remote = remoteName ? folderManager.repository.state.remotes.find(r => r.name === remoteName) : undefined;

const potentialRemotes = folderManager.repository.state.remotes.filter(r => r.fetchUrl || r.pushUrl);
interface RemoteChoice extends vscode.QuickPickItem {
remote: Remote;
}
const choices: RemoteChoice[] = potentialRemotes.map(remote => ({
label: `${remote.name}: ${remote.fetchUrl || remote.pushUrl}`,
remote,
}));
if (!remote) {
const choice = await vscode.window.showQuickPick(choices, { placeHolder: vscode.l10n.t('Select a remote to file this issue to') });
if (!choice) {
return;
}
remote = choice.remote;
}

if (template) {
this.makeNewIssueFile(uri, template.title, template.body);
this.makeNewIssueFile(uri, { title: template.title, body: template.body, remote });
} else {
this.makeNewIssueFile(uri);
this.makeNewIssueFile(uri, { remote });
}
}

Expand Down Expand Up @@ -891,7 +920,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
let titlePlaceholder: string | undefined;
let insertIndex: number | undefined;
let lineNumber: number | undefined;
let assignee: string[] | undefined;
let assignees: string[] | undefined;
let issueGenerationText: string | undefined;
if (!newIssue && vscode.window.activeTextEditor) {
document = vscode.window.activeTextEditor.document;
Expand All @@ -909,7 +938,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
}
const matches = issueGenerationText.match(USER_EXPRESSION);
if (matches && matches.length === 2 && (await this._stateManager.getUserMap(document.uri)).has(matches[1])) {
assignee = [matches[1]];
assignees = [matches[1]];
}
let title: string | undefined;
const body: string | undefined = await this.createTodoIssueBody(newIssue, issueBody);
Expand All @@ -929,7 +958,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
title = quickInput.value;
if (title) {
quickInput.busy = true;
await this.doCreateIssue(document, newIssue, title, body, assignee, undefined, undefined, undefined, lineNumber, insertIndex);
await this.doCreateIssue(document, newIssue, title, body, assignees, undefined, undefined, undefined, lineNumber, insertIndex);
quickInput.busy = false;
}
quickInput.hide();
Expand All @@ -939,7 +968,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
quickInput.busy = true;
this.createIssueInfo = { document, newIssue, lineNumber, insertIndex };

this.makeNewIssueFile(document.uri, title, body, assignee);
this.makeNewIssueFile(document.uri, { title, body, assignees });
quickInput.busy = false;
quickInput.hide();
});
Expand All @@ -950,11 +979,27 @@ export class IssueFeatureRegistrar implements vscode.Disposable {

private async makeNewIssueFile(
originUri: vscode.Uri,
title?: string,
body?: string,
assignees?: string[] | undefined,
options?: {
title?: string,
body?: string,
assignees?: string[] | undefined,
remote?: Remote,
}
) {
const query = `?{"origin":"${originUri.toString()}"}`;
const queryBody: IssueFileQuery = {
origin: originUri.toString(),
};
let repoRef: string | undefined;
if (options?.remote && (options.remote.fetchUrl || options.remote.pushUrl)) {
const githubRepo = new Protocol((options.remote.fetchUrl || options.remote.pushUrl)!);
repoRef = githubRepo.nameWithOwner;
const folderManager = this.manager.getManagerForFile(originUri);
if (!folderManager) {
return;
}
queryBody.origin = `${REPO_SCHEME}:${repoRef}?${FOLDER_MANAGER_ROOT_URI_QUERY}=${folderManager.repository.rootUri.toString()}`;
}
const query = `?${JSON.stringify(queryBody)}`;
const bodyPath = vscode.Uri.parse(`${NEW_ISSUE_SCHEME}:/${NEW_ISSUE_FILE}${query}`);
if (
vscode.window.visibleTextEditors.filter(
Expand All @@ -964,18 +1009,19 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
return;
}
await vscode.workspace.fs.delete(bodyPath);
const assigneeLine = `${ASSIGNEES} ${assignees && assignees.length > 0 ? assignees.map(value => '@' + value).join(', ') + ' ' : ''
const assigneeLine = `${ASSIGNEES} ${options?.assignees && options.assignees.length > 0 ? options.assignees.map(value => '@' + value).join(', ') + ' ' : ''
}`;
const labelLine = `${LABELS} `;
const milestoneLine = `${MILESTONE} `;
const projectsLine = `${PROJECTS} `;
const cached = this._newIssueCache.get();
const text = (cached && cached !== '') ? cached : `${title ?? vscode.l10n.t('Issue Title')}\n
const text = (cached && cached !== '') ? cached : `${options?.title ?? vscode.l10n.t('Issue Title')}\n
${repoRef ? `<!-- ${vscode.l10n.t(`This issue will be created in repo ${repoRef} (${queryBody.origin}). Changing this line has no effect.`)} -->\n` : ''}
${assigneeLine}
${labelLine}
${milestoneLine}
${projectsLine}\n
${body ?? ''}\n
${options?.body ?? ''}\n
<!-- ${vscode.l10n.t('Edit the body of your new issue then click the ✓ \"Create Issue\" button in the top right of the editor. The first line will be the issue title. Assignees and Labels follow after a blank line. Leave an empty line before beginning the body of the issue.')} -->`;
await vscode.workspace.fs.writeFile(bodyPath, this.stringToUint8Array(text));
const assigneesDecoration = vscode.window.createTextEditorDecorationType({
Expand All @@ -1001,25 +1047,29 @@ ${body ?? ''}\n
});
const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(textEditor => {
if (textEditor?.document.uri.scheme === NEW_ISSUE_SCHEME) {
const assigneeFullLine = textEditor.document.lineAt(2);
const metadataFirstLine = repoRef ? 4 : 2;
const assigneeLineNum = metadataFirstLine + 0;
const labelLineNum = metadataFirstLine + 1;
const projectsLineNum = metadataFirstLine + 3;
const assigneeFullLine = textEditor.document.lineAt(assigneeLineNum);
if (assigneeFullLine.text.startsWith(ASSIGNEES)) {
textEditor.setDecorations(assigneesDecoration, [
new vscode.Range(
new vscode.Position(2, 0),
new vscode.Position(2, assigneeFullLine.text.length),
new vscode.Position(assigneeLineNum, 0),
new vscode.Position(assigneeLineNum, assigneeFullLine.text.length),
),
]);
}
const labelFullLine = textEditor.document.lineAt(3);
const labelFullLine = textEditor.document.lineAt(labelLineNum);
if (labelFullLine.text.startsWith(LABELS)) {
textEditor.setDecorations(labelsDecoration, [
new vscode.Range(new vscode.Position(3, 0), new vscode.Position(3, labelFullLine.text.length)),
new vscode.Range(new vscode.Position(labelLineNum, 0), new vscode.Position(labelLineNum, labelFullLine.text.length)),
]);
}
const projectsFullLine = textEditor.document.lineAt(5);
const projectsFullLine = textEditor.document.lineAt(projectsLineNum);
if (projectsFullLine.text.startsWith(PROJECTS)) {
textEditor.setDecorations(projectsDecoration, [
new vscode.Range(new vscode.Position(5, 0), new vscode.Position(5, projectsFullLine.text.length)),
new vscode.Range(new vscode.Position(projectsLineNum, 0), new vscode.Position(projectsLineNum, projectsFullLine.text.length)),
]);
}
}
Expand Down Expand Up @@ -1170,26 +1220,50 @@ ${body ?? ''}\n
projects: IProject[] | undefined,
lineNumber: number | undefined,
insertIndex: number | undefined,
originUri?: vscode.Uri,
originUri?: vscode.Uri
): Promise<boolean> {
let origin: PullRequestDefaults | undefined;
let folderManager: FolderRepositoryManager | undefined;
if (document) {
folderManager = this.manager.getManagerForFile(document.uri);
} else if (originUri) {
folderManager = this.manager.getManagerForFile(originUri);
if (originUri && originUri.scheme === REPO_SCHEME) {
const [owner, repo] = originUri.path.split('/');
origin = {
owner,
repo,
base: '',
};

folderManager = extractFolderManagerForRepoUri(this.manager, originUri);
if (!folderManager) {
vscode.window.showErrorMessage(vscode.l10n.t(`Could not find the folder manager for the issue; see logs for more details.`));
Logger.error(`Could not find the folder manager for the issue originUri: ${originUri.toString()}`);
return false;
}
}

if (!folderManager) {
// We don't check for githubIssues.alwaysPromptForNewIssueRepo here because we're
// likely in this scenario due to making an issue from a file selection/etc.
if (document) {
folderManager = this.manager.getManagerForFile(document.uri);
} else if (originUri) {
folderManager = this.manager.getManagerForFile(originUri);
}
}
if (!folderManager) {
folderManager = await this.chooseRepo(vscode.l10n.t('Choose where to create the issue.'));
}

this.manager.folderManagers.find(folderManager => folderManager.repository.rootUri === originUri);

return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Creating issue') }, async (progress) => {
if (!folderManager) {
return false;
}
progress.report({ message: vscode.l10n.t('Verifying that issue data is valid...') });
try {
origin = await folderManager.getPullRequestDefaults();
if (!origin) {
origin = await folderManager.getPullRequestDefaults();
}
} catch (e) {
// There is no remote
vscode.window.showErrorMessage(vscode.l10n.t('There is no remote. Can\'t create an issue.'));
Expand Down
47 changes: 44 additions & 3 deletions src/issues/issueFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,35 @@ export const ASSIGNEES = vscode.l10n.t('Assignees:');
export const LABELS = vscode.l10n.t('Labels:');
export const MILESTONE = vscode.l10n.t('Milestone:');
export const PROJECTS = vscode.l10n.t('Projects:');
export const REPO_SCHEME = 'repo';
export const FOLDER_MANAGER_ROOT_URI_QUERY = 'folderManagerRootUri';

const NEW_ISSUE_CACHE = 'newIssue.cache';

export interface IssueFileQuery {
origin: string;
}

export function extractIssueOriginFromQuery(uri: vscode.Uri): vscode.Uri | undefined {
const query = JSON.parse(uri.query);
const query: IssueFileQuery = JSON.parse(uri.query);
if (query.origin) {
return vscode.Uri.parse(query.origin);
}
}

export function extractFolderManagerForRepoUri(manager: RepositoriesManager, uri: vscode.Uri): FolderRepositoryManager | undefined {
if (uri.scheme !== REPO_SCHEME) {
return undefined;
}
const queryParams = uri.query.split('&');
const folderManagerRootUri = queryParams.find(queryParam => queryParam.startsWith(FOLDER_MANAGER_ROOT_URI_QUERY))?.split('=')[1];
if (!folderManagerRootUri) {
return undefined;
}
const queryUri = vscode.Uri.parse(folderManagerRootUri);
return manager.folderManagers.find(f => f.repository.rootUri.toString() === queryUri.toString());
}

export class IssueFileSystemProvider implements vscode.FileSystemProvider {
private content: Uint8Array | undefined;
private createTime: number = 0;
Expand Down Expand Up @@ -168,7 +187,7 @@ export class NewIssueCache {
}
}

export async function extractMetadataFromFile(repositoriesManager: RepositoriesManager): Promise<{ labels: string[] | undefined, milestone: number | undefined, projects: IProject[] | undefined, assignees: string[] | undefined, title: string, body: string | undefined, originUri: vscode.Uri } | undefined> {
export async function extractMetadataFromFile(repositoriesManager: RepositoriesManager): Promise<{ labels: string[] | undefined, milestone: number | undefined, projects: IProject[] | undefined, assignees: string[] | undefined, title: string, body: string | undefined, originUri: vscode.Uri, repoUri?: vscode.Uri } | undefined> {
let text: string;
if (
!vscode.window.activeTextEditor ||
Expand All @@ -180,7 +199,12 @@ export async function extractMetadataFromFile(repositoriesManager: RepositoriesM
if (!originUri) {
return;
}
const folderManager = repositoriesManager.getManagerForFile(originUri);
let folderManager: FolderRepositoryManager | undefined;
if (originUri.scheme === REPO_SCHEME) {
folderManager = extractFolderManagerForRepoUri(repositoriesManager, originUri);
} else {
folderManager = repositoriesManager.getManagerForFile(originUri);
}
if (!folderManager) {
return;
}
Expand All @@ -206,6 +230,23 @@ export async function extractMetadataFromFile(repositoriesManager: RepositoriesM
}
let assignees: string[] | undefined;
text = text.substring(indexOfEmptyLine + 2).trim();
if (text.startsWith('<!--')) {
const nextIndexOfEmptyLineWindows = text.indexOf('\r\n\r\n');
const nextIndexOfEmptyLineOther = text.indexOf('\n\n');
let nextIndexOfEmptyLine: number;
if (nextIndexOfEmptyLineWindows < 0 && nextIndexOfEmptyLineOther < 0) {
return;
} else {
if (nextIndexOfEmptyLineWindows < 0) {
nextIndexOfEmptyLine = nextIndexOfEmptyLineOther;
} else if (nextIndexOfEmptyLineOther < 0) {
nextIndexOfEmptyLine = nextIndexOfEmptyLineWindows;
} else {
nextIndexOfEmptyLine = Math.min(nextIndexOfEmptyLineWindows, nextIndexOfEmptyLineOther);
}
}
text = text.substring(nextIndexOfEmptyLine + 2).trim();
}
if (text.startsWith(ASSIGNEES)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
Expand Down