Skip to content

Commit

Permalink
Enable mentioning a milestone when creating an issue (#5350)
Browse files Browse the repository at this point in the history
Part of #3062
  • Loading branch information
alexr00 authored Oct 16, 2023
1 parent af8e1c6 commit 43c619e
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 68 deletions.
7 changes: 7 additions & 0 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,7 @@ export class FolderRepositoryManager implements vscode.Disposable {
createdAt: new Date(0).toDateString(),
id: '',
title: NO_MILESTONE,
number: -1
},
issues: await Promise.all(additionalIssues.items.map(async (issue) => {
const githubRepository = await this.getRepoForIssue(issue);
Expand All @@ -1083,6 +1084,7 @@ export class FolderRepositoryManager implements vscode.Disposable {
dueOn: data.due_on,
createdAt: data.created_at,
id: data.node_id,
number: data.number
};
}
catch (e) {
Expand Down Expand Up @@ -1202,6 +1204,11 @@ export class FolderRepositoryManager implements vscode.Disposable {
};
}

async getPullRequestDefaultRepo(): Promise<GitHubRepository> {
const defaults = await this.getPullRequestDefaults();
return this.findRepo(repo => repo.remote.owner === defaults.owner && repo.remote.repositoryName === defaults.repo) || this._githubRepositories[0];
}

async getMetadata(remote: string): Promise<any> {
const repo = this.findRepo(byRemoteName(remote));
return repo && repo.getMetadata();
Expand Down
2 changes: 2 additions & 0 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ export interface PullRequest {
dueOn?: string;
id: string;
createdAt: string;
number: number;
};
repository?: {
name: string;
Expand Down Expand Up @@ -626,6 +627,7 @@ export interface MilestoneIssuesResponse {
createdAt: string;
title: string;
id: string;
number: number
issues: {
edges: {
node: PullRequest;
Expand Down
1 change: 1 addition & 0 deletions src/github/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface IMilestone {
dueOn?: string | null;
createdAt: string;
id: string;
number: number;
}

export interface MergePullRequest {
Expand Down
1 change: 1 addition & 0 deletions src/github/queries.gql
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ fragment PullRequestFragment on PullRequest {
dueOn
createdAt
id
number
}
assignees(first: 10) {
nodes {
Expand Down
1 change: 1 addition & 0 deletions src/github/queriesLimited.gql
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ fragment PullRequestFragment on PullRequest {
dueOn
createdAt
id
number
}
assignees(first: 10) {
nodes {
Expand Down
1 change: 1 addition & 0 deletions src/github/queriesShared.gql
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,7 @@ query GetMilestones($owner: String!, $name: String!, $states: [MilestoneState!]!
title
createdAt
id
number
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/github/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ export function parseProjectItems(projects: { id: string; project: { id: string;
}

export function parseMilestone(
milestone: { title: string; dueOn?: string; createdAt: string; id: string } | undefined,
milestone: { title: string; dueOn?: string; createdAt: string; id: string, number: number } | undefined,
): IMilestone | undefined {
if (!milestone) {
return undefined;
Expand All @@ -602,6 +602,7 @@ export function parseMilestone(
dueOn: milestone.dueOn,
createdAt: milestone.createdAt,
id: milestone.id,
number: milestone.number
};
}

Expand Down
83 changes: 18 additions & 65 deletions src/issues/issueFeatureRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ import { CurrentIssue } from './currentIssue';
import { IssueCompletionProvider } from './issueCompletionProvider';
import {
ASSIGNEES,
extractIssueOriginFromQuery,
extractMetadataFromFile,
IssueFileSystemProvider,
LabelCompletionProvider,
LABELS,
MILESTONE,
NEW_ISSUE_FILE,
NEW_ISSUE_SCHEME,
NewIssueCache,
NewIssueFileCompletionProvider,
} from './issueFile';
import { IssueHoverProvider } from './issueHoverProvider';
import { openCodeLink } from './issueLinkLookup';
Expand Down Expand Up @@ -87,7 +88,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
this.context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
{ scheme: NEW_ISSUE_SCHEME },
new LabelCompletionProvider(this.manager),
new NewIssueFileCompletionProvider(this.manager),
' ',
',',
),
Expand Down Expand Up @@ -623,73 +624,21 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
}

async createIssueFromFile() {
let text: string;
if (
!vscode.window.activeTextEditor ||
vscode.window.activeTextEditor.document.uri.scheme !== NEW_ISSUE_SCHEME
) {
return;
}
text = vscode.window.activeTextEditor.document.getText();
const indexOfEmptyLineWindows = text.indexOf('\r\n\r\n');
const indexOfEmptyLineOther = text.indexOf('\n\n');
let indexOfEmptyLine: number;
if (indexOfEmptyLineWindows < 0 && indexOfEmptyLineOther < 0) {
return;
} else {
if (indexOfEmptyLineWindows < 0) {
indexOfEmptyLine = indexOfEmptyLineOther;
} else if (indexOfEmptyLineOther < 0) {
indexOfEmptyLine = indexOfEmptyLineWindows;
} else {
indexOfEmptyLine = Math.min(indexOfEmptyLineWindows, indexOfEmptyLineOther);
}
}
const title = text.substring(0, indexOfEmptyLine);
let assignees: string[] | undefined;
text = text.substring(indexOfEmptyLine + 2).trim();
if (text.startsWith(ASSIGNEES)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
assignees = lines[0]
.substring(ASSIGNEES.length)
.split(',')
.map(value => {
value = value.trim();
if (value.startsWith('@')) {
value = value.substring(1);
}
return value;
});
text = text.substring(lines[0].length).trim();
}
}
let labels: string[] | undefined;
if (text.startsWith(LABELS)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
labels = lines[0]
.substring(LABELS.length)
.split(',')
.map(value => value.trim())
.filter(label => label);
text = text.substring(lines[0].length).trim();
}
}
const body = text ?? '';
if (!title) {
const metadata = await extractMetadataFromFile(this.manager);
if (!metadata || !vscode.window.activeTextEditor) {
return;
}
const createSucceeded = await this.doCreateIssue(
this.createIssueInfo?.document,
this.createIssueInfo?.newIssue,
title,
body,
assignees,
labels,
metadata.title,
metadata.body,
metadata.assignees,
metadata.labels,
metadata.milestone,
this.createIssueInfo?.lineNumber,
this.createIssueInfo?.insertIndex,
extractIssueOriginFromQuery(vscode.window.activeTextEditor.document.uri),
metadata.originUri
);
this.createIssueInfo = undefined;
if (createSucceeded) {
Expand Down Expand Up @@ -966,7 +915,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
title = quickInput.value;
if (title) {
quickInput.busy = true;
await this.doCreateIssue(document, newIssue, title, body, assignee, undefined, lineNumber, insertIndex);
await this.doCreateIssue(document, newIssue, title, body, assignee, undefined, undefined, lineNumber, insertIndex);
quickInput.busy = false;
}
quickInput.hide();
Expand Down Expand Up @@ -1004,10 +953,12 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
const assigneeLine = `${ASSIGNEES} ${assignees && assignees.length > 0 ? assignees.map(value => '@' + value).join(', ') + ' ' : ''
}`;
const labelLine = `${LABELS} `;
const milestoneLine = `${MILESTONE} `;
const cached = this._newIssueCache.get();
const text = (cached && cached !== '') ? cached : `${title ?? vscode.l10n.t('Issue Title')}\n
${assigneeLine}
${labelLine}\n
${labelLine}
${milestoneLine}\n
${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));
Expand Down Expand Up @@ -1135,6 +1086,7 @@ ${body ?? ''}\n
issueBody: string | undefined,
assignees: string[] | undefined,
labels: string[] | undefined,
milestone: number | undefined,
lineNumber: number | undefined,
insertIndex: number | undefined,
originUri?: vscode.Uri,
Expand Down Expand Up @@ -1171,6 +1123,7 @@ ${body ?? ''}\n
body,
assignees,
labels,
milestone
};
if (!(await this.verifyLabels(folderManager, createParams))) {
return false;
Expand Down
109 changes: 107 additions & 2 deletions src/issues/issueFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager';
import { RepositoriesManager } from '../github/repositoriesManager';

export const NEW_ISSUE_SCHEME = 'newIssue';
export const NEW_ISSUE_FILE = 'NewIssue.md';
export const ASSIGNEES = vscode.l10n.t('Assignees:');
export const LABELS = vscode.l10n.t('Labels:');
export const MILESTONE = vscode.l10n.t('Milestone:');

const NEW_ISSUE_CACHE = 'newIssue.cache';

Expand Down Expand Up @@ -79,7 +81,7 @@ export class IssueFileSystemProvider implements vscode.FileSystemProvider {
rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { overwrite: boolean }): void | Thenable<void> { }
}

export class LabelCompletionProvider implements vscode.CompletionItemProvider {
export class NewIssueFileCompletionProvider implements vscode.CompletionItemProvider {
constructor(private manager: RepositoriesManager) { }

async provideCompletionItems(
Expand All @@ -88,7 +90,8 @@ export class LabelCompletionProvider implements vscode.CompletionItemProvider {
_token: vscode.CancellationToken,
_context: vscode.CompletionContext,
): Promise<vscode.CompletionItem[]> {
if (!document.lineAt(position.line).text.startsWith(LABELS)) {
const line = document.lineAt(position.line).text;
if (!line.startsWith(LABELS) && !line.startsWith(MILESTONE)) {
return [];
}
const originFile = extractIssueOriginFromQuery(document.uri);
Expand All @@ -100,6 +103,17 @@ export class LabelCompletionProvider implements vscode.CompletionItemProvider {
return [];
}
const defaults = await folderManager.getPullRequestDefaults();

if (line.startsWith(LABELS)) {
return this.provideLabelCompletionItems(folderManager, defaults);
} else if (line.startsWith(MILESTONE)) {
return this.provideMilestoneCompletionItems(folderManager);
} else {
return [];
}
}

private async provideLabelCompletionItems(folderManager: FolderRepositoryManager, defaults: PullRequestDefaults): Promise<vscode.CompletionItem[]> {
const labels = await folderManager.getLabels(undefined, defaults);
return labels.map(label => {
const item = new vscode.CompletionItem(label.name, vscode.CompletionItemKind.Color);
Expand All @@ -108,6 +122,15 @@ export class LabelCompletionProvider implements vscode.CompletionItemProvider {
return item;
});
}

private async provideMilestoneCompletionItems(folderManager: FolderRepositoryManager): Promise<vscode.CompletionItem[]> {
const milestones = await (await folderManager.getPullRequestDefaultRepo())?.getMilestones() ?? [];
return milestones.map(milestone => {
const item = new vscode.CompletionItem(milestone.title, vscode.CompletionItemKind.Event);
item.commitCharacters = [' ', ','];
return item;
});
}
}

export class NewIssueCache {
Expand All @@ -130,3 +153,85 @@ export class NewIssueCache {
}
}
}

export async function extractMetadataFromFile(repositoriesManager: RepositoriesManager): Promise<{ labels: string[] | undefined, milestone: number | undefined, assignees: string[] | undefined, title: string, body: string | undefined, originUri: vscode.Uri } | undefined> {
let text: string;
if (
!vscode.window.activeTextEditor ||
vscode.window.activeTextEditor.document.uri.scheme !== NEW_ISSUE_SCHEME
) {
return;
}
const originUri = extractIssueOriginFromQuery(vscode.window.activeTextEditor.document.uri);
if (!originUri) {
return;
}
const folderManager = repositoriesManager.getManagerForFile(originUri);
if (!folderManager) {
return;
}
const repo = await folderManager.getPullRequestDefaultRepo();
text = vscode.window.activeTextEditor.document.getText();
const indexOfEmptyLineWindows = text.indexOf('\r\n\r\n');
const indexOfEmptyLineOther = text.indexOf('\n\n');
let indexOfEmptyLine: number;
if (indexOfEmptyLineWindows < 0 && indexOfEmptyLineOther < 0) {
return;
} else {
if (indexOfEmptyLineWindows < 0) {
indexOfEmptyLine = indexOfEmptyLineOther;
} else if (indexOfEmptyLineOther < 0) {
indexOfEmptyLine = indexOfEmptyLineWindows;
} else {
indexOfEmptyLine = Math.min(indexOfEmptyLineWindows, indexOfEmptyLineOther);
}
}
const title = text.substring(0, indexOfEmptyLine);
if (!title) {
return;
}
let assignees: string[] | undefined;
text = text.substring(indexOfEmptyLine + 2).trim();
if (text.startsWith(ASSIGNEES)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
assignees = lines[0]
.substring(ASSIGNEES.length)
.split(',')
.map(value => {
value = value.trim();
if (value.startsWith('@')) {
value = value.substring(1);
}
return value;
});
text = text.substring(lines[0].length).trim();
}
}
let labels: string[] | undefined;
if (text.startsWith(LABELS)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
labels = lines[0]
.substring(LABELS.length)
.split(',')
.map(value => value.trim())
.filter(label => label);
text = text.substring(lines[0].length).trim();
}
}
let milestone: number | undefined;
if (text.startsWith(MILESTONE)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
const milestoneTitle = lines[0].substring(MILESTONE.length).trim();
if (milestoneTitle) {
const repoMilestones = await repo.getMilestones();
milestone = repoMilestones?.find(milestone => milestone.title === milestoneTitle)?.number;
}
text = text.substring(lines[0].length).trim();
}
}
const body = text ?? '';
return { labels, milestone, assignees, title, body, originUri };
}

0 comments on commit 43c619e

Please sign in to comment.