提交 a2b3f881 编写于 作者: T Tomas Vik

feat: show changed files for the MR

上级 629e4d1c
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// This script creates a temporary workspace that can be used for debugging integration tests // This script creates a temporary workspace that can be used for debugging integration tests
const { readFileSync, writeFileSync } = require('fs'); const { readFileSync, writeFileSync } = require('fs');
const path = require('path'); const path = require('path');
const { default: createTmpWorkspace } = require('../out/create_tmp_workspace'); const { default: createTmpWorkspace } = require('../out/test/create_tmp_workspace');
const PLACEHOLDER = `<run \`npm run create-test-workspace\` to generate a test folder>`; const PLACEHOLDER = `<run \`npm run create-test-workspace\` to generate a test folder>`;
......
const vscode = require('vscode'); const vscode = require('vscode');
const moment = require('moment'); const moment = require('moment');
const gitLabService = require('../gitlab_service'); const gitLabService = require('../gitlab_service');
const { SidebarTreeItem } = require('./sidebar_tree_item'); const { ErrorItem } = require('./items/error_item');
const ErrorItem = require('./error_item');
const { getCurrentWorkspaceFolder } = require('../services/workspace_service'); const { getCurrentWorkspaceFolder } = require('../services/workspace_service');
const { handleError } = require('../log'); const { handleError } = require('../log');
const { MrItem } = require('./items/mr_item');
const { IssueItem } = require('./items/issue_item');
const { ExternalUrlItem } = require('./items/external_url_item');
class DataProvider { class DataProvider {
constructor() { constructor() {
...@@ -15,98 +17,68 @@ class DataProvider { ...@@ -15,98 +17,68 @@ class DataProvider {
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
this.onDidChangeTreeData = this._onDidChangeTreeData.event; this.onDidChangeTreeData = this._onDidChangeTreeData.event;
this.children = [];
this.project = null; this.project = null;
this.mr = null; this.mr = null;
} }
async fetchPipeline(workspaceFolder) { async fetchPipeline(workspaceFolder) {
let message = 'No pipeline found.'; const pipeline = await gitLabService.fetchLastPipelineForCurrentBranch(workspaceFolder);
let url = null;
// TODO project is always present (we throw if we fail to fetch it)
if (this.project) {
const pipeline = await gitLabService.fetchLastPipelineForCurrentBranch(workspaceFolder);
if (pipeline) { if (!pipeline) {
const statusText = pipeline.status === 'success' ? 'passed' : pipeline.status; return new vscode.TreeItem('No pipeline found.');
const actions = {
running: 'Started',
pending: 'Created',
success: 'Finished',
failed: 'Failed',
canceled: 'Canceled',
skipped: 'Skipped',
};
const timeAgo = moment(pipeline.updated_at).fromNow();
const actionText = actions[pipeline.status] || '';
message = `Pipeline #${pipeline.id} ${statusText} · ${actionText} ${timeAgo}`;
url = `${this.project.web_url}/pipelines/${pipeline.id}`;
}
} }
this.children.push(new SidebarTreeItem(message, url, 'pipelines', null, workspaceFolder)); const statusText = pipeline.status === 'success' ? 'passed' : pipeline.status;
const actions = {
running: 'Started',
pending: 'Created',
success: 'Finished',
failed: 'Failed',
canceled: 'Canceled',
skipped: 'Skipped',
};
const timeAgo = moment(pipeline.updated_at).fromNow();
const actionText = actions[pipeline.status] || '';
const message = `Pipeline #${pipeline.id} ${statusText} · ${actionText} ${timeAgo}`;
const url = `${this.project.web_url}/pipelines/${pipeline.id}`;
return new ExternalUrlItem(message, url);
} }
async fetchMR(workspaceFolder) { async fetchMR(workspaceFolder) {
this.mr = null; const mr = await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder);
let message = 'No merge request found.';
// TODO project is always present (we throw if we fail to fetch it) if (mr) {
if (this.project) { this.mr = mr;
const mr = await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder); return new MrItem(this.mr, this.project);
if (mr) {
this.mr = mr;
message = `MR: !${mr.iid} · ${mr.title}`;
}
} }
this.children.push( return new vscode.TreeItem('No merge request found.');
new SidebarTreeItem(message, this.mr, 'merge_requests', null, workspaceFolder),
);
} }
async fetchClosingIssue(workspaceFolder) { async fetchClosingIssue(workspaceFolder) {
// TODO project is always present (we throw if we fail to fetch it) if (this.mr) {
if (this.project) { const issues = await gitLabService.fetchMRIssues(this.mr.iid, workspaceFolder);
if (this.mr) {
const issues = await gitLabService.fetchMRIssues(this.mr.iid, workspaceFolder);
if (issues.length) { if (issues.length) {
issues.forEach(issue => { return issues.map(issue => new IssueItem(issue, this.project));
this.children.push(
new SidebarTreeItem(
`Issue: #${issue.iid} · ${issue.title}`,
issue,
'issues',
null,
workspaceFolder,
),
);
});
} else {
this.children.push(new SidebarTreeItem('No closing issue found.'));
}
} else {
this.children.push(new SidebarTreeItem('No closing issue found.'));
} }
} else {
this.children.push(new SidebarTreeItem('No closing issue found.'));
} }
return [new vscode.TreeItem('No closing issue found.')];
} }
async getChildren() { async getChildren(item) {
if (item) return item.getChildren();
try { try {
const workspaceFolder = await getCurrentWorkspaceFolder(); const workspaceFolder = await getCurrentWorkspaceFolder();
this.project = await gitLabService.fetchCurrentProject(workspaceFolder); this.project = await gitLabService.fetchCurrentProject(workspaceFolder);
await this.fetchPipeline(workspaceFolder); const pipelineItem = await this.fetchPipeline(workspaceFolder);
await this.fetchMR(workspaceFolder); const mrItem = await this.fetchMR(workspaceFolder);
await this.fetchClosingIssue(workspaceFolder); const closingIssuesItems = await this.fetchClosingIssue(workspaceFolder);
return [pipelineItem, mrItem, ...closingIssuesItems];
} catch (e) { } catch (e) {
handleError(e); handleError(e);
this.children.push(new ErrorItem()); return [new ErrorItem()];
} }
return this.children;
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
...@@ -120,7 +92,6 @@ class DataProvider { ...@@ -120,7 +92,6 @@ class DataProvider {
} }
refresh() { refresh() {
this.children = [];
// Temporarily disable eslint to be able to start enforcing stricter rules // Temporarily disable eslint to be able to start enforcing stricter rules
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
this._onDidChangeTreeData.fire(); this._onDidChangeTreeData.fire();
......
const vscode = require('vscode');
const { SidebarTreeItem } = require('./sidebar_tree_item');
const gitLabService = require('../gitlab_service');
const { handleError } = require('../log');
const ErrorItem = require('./error_item');
const typeToSignMap = {
issues: '#',
epics: '&',
snippets: '$',
vulnerabilities: '-',
};
class CustomQueryItem extends vscode.TreeItem {
constructor(customQuery, project, showProject = false) {
super(
showProject ? project.label : customQuery.name,
vscode.TreeItemCollapsibleState.Collapsed,
);
this.project = project;
this.customQuery = customQuery;
this.iconPath = showProject ? new vscode.ThemeIcon('project') : new vscode.ThemeIcon('filter');
}
async getProjectIssues() {
const items = [];
const issues = await gitLabService.fetchIssuables(this.customQuery, this.project.uri);
const issuableSign = typeToSignMap[this.customQuery.type] || '!';
if (issues.length) {
issues.forEach(issue => {
let title = `${issuableSign}${issue.iid} · ${issue.title}`;
if (issuableSign === '$') {
title = `${issuableSign}${issue.id} · ${issue.title}`;
} else if (issuableSign === '-') {
title = `[${issue.severity}] - ${issue.name}`;
}
items.push(
new SidebarTreeItem(title, issue, this.customQuery.type, null, this.project.uri),
);
});
} else {
const noItemText = this.customQuery.noItemText || 'No items found.';
items.push(new SidebarTreeItem(noItemText));
}
return items;
}
async getChildren() {
try {
return this.getProjectIssues();
} catch (e) {
handleError(e);
return [new ErrorItem()];
}
}
}
exports.CustomQueryItem = CustomQueryItem;
const vscode = require('vscode'); const vscode = require('vscode');
const { CustomQueryItem } = require('./custom_query_item'); const { CustomQueryItem } = require('./items/custom_query_item');
const { MultirootCustomQueryItem } = require('./multiroot_custom_query_item'); const { MultirootCustomQueryItem } = require('./items/multiroot_custom_query_item');
const { SidebarTreeItem } = require('./sidebar_tree_item');
const gitLabService = require('../gitlab_service'); const gitLabService = require('../gitlab_service');
class DataProvider { class DataProvider {
...@@ -19,7 +18,7 @@ class DataProvider { ...@@ -19,7 +18,7 @@ class DataProvider {
if (el) return el.getChildren(el); if (el) return el.getChildren(el);
const projects = await gitLabService.getAllGitlabProjects(); const projects = await gitLabService.getAllGitlabProjects();
const { customQueries } = vscode.workspace.getConfiguration('gitlab'); const { customQueries } = vscode.workspace.getConfiguration('gitlab');
if (projects.length === 0) return new SidebarTreeItem('No projects found'); if (projects.length === 0) return new vscode.TreeItem('No projects found');
if (projects.length === 1) if (projects.length === 1)
return customQueries.map(customQuery => new CustomQueryItem(customQuery, projects[0])); return customQueries.map(customQuery => new CustomQueryItem(customQuery, projects[0]));
return customQueries.map(customQuery => new MultirootCustomQueryItem(customQuery, projects)); return customQueries.map(customQuery => new MultirootCustomQueryItem(customQuery, projects));
......
const vscode = require('vscode'); import * as vscode from 'vscode';
import { CustomQueryType } from '../../gitlab/custom_query_type';
const { CustomQueryItem } = require('./custom_query_item'); import { CustomQueryItem } from './custom_query_item';
describe('CustomQueryItem', () => { describe('CustomQueryItem', () => {
const customQuery = { name: 'Query name' }; const customQuery = {
const project = { label: 'Project label' }; name: 'Query name',
// eslint-disable-next-line no-unused-vars type: CustomQueryType.ISSUE,
maxResults: 10,
scope: 'all',
state: 'closed',
wip: 'no',
confidential: false,
excludeSearchIn: 'all',
orderBy: 'created_at',
sort: 'desc',
searchIn: 'all',
noItemText: 'No item',
};
const project = { label: 'Project label', uri: '/' };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let item; let item;
describe('item labeled as a query', () => { describe('item labeled as a query', () => {
......
import * as vscode from 'vscode';
import * as gitLabService from '../../gitlab_service';
import { handleError } from '../../log';
import { ErrorItem } from './error_item';
import { MrItem } from './mr_item';
import { ExternalUrlItem } from './external_url_item';
import { IssueItem } from './issue_item';
import { VulnerabilityItem } from './vulnerability_item';
import { CustomQuery } from '../../gitlab/custom_query';
import { CustomQueryType } from '../../gitlab/custom_query_type';
export class CustomQueryItem extends vscode.TreeItem {
private project: VsProject;
private customQuery: CustomQuery;
constructor(customQuery: CustomQuery, project: VsProject, showProject = false) {
super(
showProject ? project.label : customQuery.name,
vscode.TreeItemCollapsibleState.Collapsed,
);
this.project = project;
this.customQuery = customQuery;
this.iconPath = showProject ? new vscode.ThemeIcon('project') : new vscode.ThemeIcon('filter');
}
private async getProjectIssues(): Promise<vscode.TreeItem[]> {
const issues = await gitLabService.fetchIssuables(this.customQuery, this.project.uri);
if (issues.length === 0) {
const noItemText = this.customQuery.noItemText || 'No items found.';
return [new vscode.TreeItem(noItemText)];
}
const { MR, ISSUE, SNIPPET, EPIC, VULNERABILITY } = CustomQueryType;
switch (this.customQuery.type) {
case MR:
return issues.map((mr: RestIssuable) => new MrItem(mr, this.project));
case ISSUE:
return issues.map((issue: RestIssuable) => new IssueItem(issue, this.project));
case SNIPPET:
return issues.map(
(snippet: RestIssuable) =>
new ExternalUrlItem(`$${snippet.id} · ${snippet.title}`, snippet.web_url),
);
case EPIC:
return issues.map(
(epic: RestIssuable) => new ExternalUrlItem(`&${epic.iid} · ${epic.title}`, epic.web_url),
);
case VULNERABILITY:
return issues.map((v: RestVulnerability) => new VulnerabilityItem(v));
default:
throw new Error(`unknown custom query type ${this.customQuery.type}`);
}
}
async getChildren(): Promise<vscode.TreeItem[]> {
try {
return this.getProjectIssues();
} catch (e) {
handleError(e);
return [new ErrorItem()];
}
}
}
const { TreeItem, ThemeIcon } = require('vscode'); import { TreeItem, ThemeIcon } from 'vscode';
class ErrorItem extends TreeItem { export class ErrorItem extends TreeItem {
constructor(message = 'Error occurred, please try to refresh.') { constructor(message = 'Error occurred, please try to refresh.') {
super(message); super(message);
this.iconPath = new ThemeIcon('error'); this.iconPath = new ThemeIcon('error');
} }
} }
module.exports = ErrorItem;
import { TreeItem, Uri } from 'vscode';
export class ExternalUrlItem extends TreeItem {
constructor(label: string, url: string) {
super(label);
this.command = {
title: 'Open URL',
command: 'vscode.open',
arguments: [Uri.parse(url)],
};
}
}
import { TreeItem } from 'vscode';
export class IssueItem extends TreeItem {
issue: RestIssuable;
project: VsProject;
constructor(issue: RestIssuable, project: VsProject) {
super(`#${issue.iid} · ${issue.title}`);
this.issue = issue;
this.project = project;
this.command = {
command: 'gl.showRichContent',
arguments: [this.issue, this.project.uri],
title: 'Show Issue',
};
}
}
import { TreeItem, TreeItemCollapsibleState, ThemeIcon, Uri } from 'vscode';
import * as path from 'path';
import { GitLabNewService } from '../../gitlab/gitlab_new_service';
import { createGitService } from '../../git_service_factory';
const getChangeTypeIndicator = (diff: RestDiffFile): string => {
if (diff.new_file) return '[added] ';
if (diff.deleted_file) return '[deleted] ';
if (diff.renamed_file) return '[renamed] ';
return '';
};
export class MrItem extends TreeItem {
mr: RestIssuable;
project: VsProject;
constructor(mr: RestIssuable, project: VsProject) {
super(`!${mr.iid} · ${mr.title}`, TreeItemCollapsibleState.Collapsed);
this.mr = mr;
this.project = project;
}
async getChildren(): Promise<TreeItem[]> {
const description = new TreeItem('Description');
description.iconPath = new ThemeIcon('note');
description.command = {
command: 'gl.showRichContent',
arguments: [this.mr, this.project.uri],
title: 'Show MR',
};
const changedFiles = await this.getChangedFiles();
return [description, ...changedFiles];
}
private async getChangedFiles(): Promise<TreeItem[]> {
const gitService = createGitService(this.project.uri);
const instanceUrl = await gitService.fetchCurrentInstanceUrl();
const gitlabService = new GitLabNewService(instanceUrl);
const diff = await gitlabService.getMrDiff(this.mr);
return diff.map(d => {
const item = new TreeItem(Uri.file(d.new_path));
// TODO add FileDecorationProvider once it is available in the 1.53 https://github.com/microsoft/vscode/issues/54938
item.description = `${getChangeTypeIndicator(d)}${path.dirname(d.new_path)}`;
return item;
});
}
}
import { TreeItem, Uri } from 'vscode';
import * as vscode from 'vscode';
export class VulnerabilityItem extends TreeItem {
constructor(v: RestVulnerability) {
super(`[${v.severity}] - ${v.name}`);
const arg = v.location
? Uri.file(`${vscode.workspace.rootPath}/${v.location.file}`)
: Uri.parse(v.web_url);
this.command = {
title: 'Open Vulnerability',
command: 'vscode.open',
arguments: [arg],
};
}
}
const vscode = require('vscode');
const path = require('path');
class SidebarTreeItem extends vscode.TreeItem {
constructor(title, data = null, type = 'merge_requests', collapsibleState = null, uri) {
super(title, collapsibleState);
const { enableExperimentalFeatures } = vscode.workspace.getConfiguration('gitlab');
let iconPathLight = `/assets/images/light/stop.svg`;
let iconPathDark = `/assets/images/dark/stop.svg`;
if (data) {
let command = 'gl.showRichContent';
let arg = [data, uri];
iconPathLight = `/assets/images/light/${type}.svg`;
iconPathDark = `/assets/images/dark/${type}.svg`;
if (data == null) {
command = '';
arg = null;
} else if (type === 'pipelines') {
command = 'vscode.open';
arg = [vscode.Uri.parse(data)];
} else if (type === 'vulnerabilities' && data.location) {
command = 'vscode.open';
const file = `${vscode.workspace.rootPath}/${data.location.file}`;
arg = [vscode.Uri.file(file)];
} else if ((type !== 'issues' && type !== 'merge_requests') || !enableExperimentalFeatures) {
command = 'vscode.open';
arg = [vscode.Uri.parse(data.web_url)];
}
this.command = {
command,
arguments: arg,
};
}
this.iconPath = {
light: path.join(__dirname, iconPathLight),
dark: path.join(__dirname, iconPathDark),
};
}
}
exports.SidebarTreeItem = SidebarTreeItem;
import { CustomQueryType } from './custom_query_type';
export interface CustomQuery {
name: string;
type: CustomQueryType;
maxResults: number;
scope: string;
state: string;
labels?: string[];
milestone?: string;
author?: string;
assignee?: string;
search?: string;
createdBefore?: string;
createdAfter?: string;
updatedBefore?: string;
updatedAfter?: string;
wip: string;
confidential: boolean;
excludeLabels?: string[];
excludeMilestone?: string;
excludeAuthor?: string;
excludeAssignee?: string;
excludeSearch?: string;
excludeSearchIn: string;
orderBy: string;
sort: string;
reportTypes?: string[];
severityLevels?: string[];
confidenceLevels?: string[];
searchIn: string;
pipelineId?: string;
noItemText: string;
}
export enum CustomQueryType {
MR = 'merge_requests',
ISSUE = 'issues',
SNIPPET = 'snippets',
EPIC = 'epics',
VULNERABILITY = 'vulnerabilities',
}
...@@ -108,4 +108,22 @@ export class GitLabNewService { ...@@ -108,4 +108,22 @@ export class GitLabNewService {
} }
return result.text(); return result.text();
} }
// This method has to use REST API till https://gitlab.com/gitlab-org/gitlab/-/issues/280803 gets done
async getMrDiff(mr: RestIssuable): Promise<RestDiffFile[]> {
const versionsUrl = `${this.instanceUrl}/api/v4/projects/${mr.project_id}/merge_requests/${mr.iid}/versions`;
const versionsResult = await crossFetch(versionsUrl, this.fetchOptions);
if (!versionsResult.ok) {
throw new FetchError(`Fetching versions from ${versionsUrl} failed`, versionsResult);
}
const versions = await versionsResult.json();
const lastVersion = versions[0];
const lastVersionUrl = `${this.instanceUrl}/api/v4/projects/${mr.project_id}/merge_requests/${mr.iid}/versions/${lastVersion.id}`;
const diffResult = await crossFetch(lastVersionUrl, this.fetchOptions);
if (!diffResult.ok) {
throw new FetchError(`Fetching MR diff from ${lastVersionUrl} failed`, diffResult);
}
const result = await diffResult.json();
return result.diffs;
}
} }
...@@ -9,6 +9,8 @@ import { createGitService } from './git_service_factory'; ...@@ -9,6 +9,8 @@ import { createGitService } from './git_service_factory';
import { GitRemote } from './git/git_remote_parser'; import { GitRemote } from './git/git_remote_parser';
import { handleError, logError } from './log'; import { handleError, logError } from './log';
import { getUserAgentHeader } from './utils/get_user_agent_header'; import { getUserAgentHeader } from './utils/get_user_agent_header';
import { CustomQueryType } from './gitlab/custom_query_type';
import { CustomQuery } from './gitlab/custom_query';
interface GitLabProject { interface GitLabProject {
id: number; id: number;
...@@ -21,13 +23,6 @@ interface GitLabProject { ...@@ -21,13 +23,6 @@ interface GitLabProject {
path_with_namespace: string; path_with_namespace: string;
} }
interface GitLabIssuable {
sha: string;
// eslint-disable-next-line camelcase
project_id: number;
iid: number;
}
interface GitLabPipeline { interface GitLabPipeline {
id: number; id: number;
} }
...@@ -237,40 +232,9 @@ export async function fetchLastPipelineForCurrentBranch(workspaceFolder: string) ...@@ -237,40 +232,9 @@ export async function fetchLastPipelineForCurrentBranch(workspaceFolder: string)
return pipeline; return pipeline;
} }
interface IssuableSearchParams {
type: string;
maxResults: number;
scope: string;
state: string;
labels?: string[];
milestone?: string;
author?: string;
assignee?: string;
search?: string;
createdBefore?: string;
createdAfter?: string;
updatedBefore?: string;
updatedAfter?: string;
wip: string;
confidential: boolean;
excludeLabels?: string[];
excludeMilestone?: string;
excludeAuthor?: string;
excludeAssignee?: string;
excludeSearch?: string;
excludeSearchIn: string;
orderBy: string;
sort: string;
reportTypes?: string[];
severityLevels?: string[];
confidenceLevels?: string[];
searchIn: string;
pipelineId?: string;
}
type QueryValue = string | boolean | string[] | number | undefined; type QueryValue = string | boolean | string[] | number | undefined;
export async function fetchIssuables(params: IssuableSearchParams, workspaceFolder: string) { export async function fetchIssuables(params: CustomQuery, workspaceFolder: string) {
const { type, scope, state, author, assignee, wip } = params; const { type, scope, state, author, assignee, wip } = params;
let { searchIn, pipelineId } = params; let { searchIn, pipelineId } = params;
const config = { const config = {
...@@ -296,9 +260,6 @@ export async function fetchIssuables(params: IssuableSearchParams, workspaceFold ...@@ -296,9 +260,6 @@ export async function fetchIssuables(params: IssuableSearchParams, workspaceFold
) { ) {
config.scope = 'all'; config.scope = 'all';
} }
if (config.type === 'vulnerabilities') {
config.type = 'vulnerability_findings';
}
// Normalize scope parameter for version < 11 instances. // Normalize scope parameter for version < 11 instances.
const [major] = version.split('.'); const [major] = version.split('.');
...@@ -315,7 +276,9 @@ export async function fetchIssuables(params: IssuableSearchParams, workspaceFold ...@@ -315,7 +276,9 @@ export async function fetchIssuables(params: IssuableSearchParams, workspaceFold
return []; return [];
} }
} else { } else {
path = `/projects/${project.id}/${config.type}?scope=${config.scope}&state=${config.state}`; const searchKind =
config.type === CustomQueryType.VULNERABILITY ? 'vulnerability_findings' : config.type;
path = `/projects/${project.id}/${searchKind}?scope=${config.scope}&state=${config.state}`;
} }
if (config.type === 'issues') { if (config.type === 'issues') {
if (author) { if (author) {
...@@ -519,7 +482,7 @@ export async function validateCIConfig(content: string) { ...@@ -519,7 +482,7 @@ export async function validateCIConfig(content: string) {
return validCIConfig; return validCIConfig;
} }
export async function fetchLabelEvents(issuable: GitLabIssuable) { export async function fetchLabelEvents(issuable: RestIssuable) {
let labelEvents: { body: string }[] = []; let labelEvents: { body: string }[] = [];
try { try {
...@@ -540,7 +503,7 @@ export async function fetchLabelEvents(issuable: GitLabIssuable) { ...@@ -540,7 +503,7 @@ export async function fetchLabelEvents(issuable: GitLabIssuable) {
return labelEvents; return labelEvents;
} }
export async function fetchDiscussions(issuable: GitLabIssuable, page = 1) { export async function fetchDiscussions(issuable: RestIssuable, page = 1) {
let discussions: unknown[] = []; let discussions: unknown[] = [];
try { try {
...@@ -598,7 +561,7 @@ export async function renderMarkdown(markdown: string, workspaceFolder: string) ...@@ -598,7 +561,7 @@ export async function renderMarkdown(markdown: string, workspaceFolder: string)
} }
export async function saveNote(params: { export async function saveNote(params: {
issuable: GitLabIssuable; issuable: RestIssuable;
note: string; note: string;
noteType: { path: string }; noteType: { path: string };
}) { }) {
......
/* eslint-disable camelcase */
interface RestIssuable {
id: number;
iid: number;
title: string;
project_id: number;
web_url: string;
sha?: string; // only present in MR, legacy logic uses the presence to decide issuable type
}
interface RestDiffFile {
head_commit_sha: string;
base_commit_sha: string;
new_path: string;
old_path: string;
deleted_file: boolean;
new_file: boolean;
renamed_file: boolean;
}
interface VsProject {
label: string;
uri: string;
}
interface RestVulnerability {
location?: {
file: string;
};
web_url: string;
severity: string;
name: string;
}
...@@ -29,6 +29,11 @@ export default async function createTmpWorkspace(autoCleanUp = true): Promise<st ...@@ -29,6 +29,11 @@ export default async function createTmpWorkspace(autoCleanUp = true): Promise<st
const git = simpleGit(dirPath, { binary: 'git' }); const git = simpleGit(dirPath, { binary: 'git' });
await git.init(); await git.init();
await git.addRemote(REMOTE.NAME, REMOTE.URL); await git.addRemote(REMOTE.NAME, REMOTE.URL);
await git.addConfig('user.email', 'test@example.com');
await git.addConfig('user.name', 'Test Name');
await git.commit('Test commit', [], {
'--allow-empty': null,
});
await addFile(dirPath, '/.vscode/settings.json', JSON.stringify(DEFAULT_VS_CODE_SETTINGS)); await addFile(dirPath, '/.vscode/settings.json', JSON.stringify(DEFAULT_VS_CODE_SETTINGS));
return dirPath; return dirPath;
} }
{
"id": 127919672,
"head_commit_sha": "b6d6f6fd17b52b8cf4e961218c572805e9aa7463",
"base_commit_sha": "1f0fa02de1f6b913d674a8be10899fb8540237a9",
"start_commit_sha": "1f0fa02de1f6b913d674a8be10899fb8540237a9",
"created_at": "2020-12-01T13:59:47.796Z",
"merge_request_id": 77101970,
"state": "collected",
"real_size": "4",
"commits": [
{
"id": "b6d6f6fd17b52b8cf4e961218c572805e9aa7463",
"short_id": "b6d6f6fd",
"created_at": "2020-12-01T13:59:42.000Z",
"parent_ids": [],
"title": "added and removed",
"message": "added and removed\n",
"author_name": "Tomas Vik",
"author_email": "tvik@gitlab.com",
"authored_date": "2020-12-01T13:59:42.000Z",
"committer_name": "Tomas Vik",
"committer_email": "tvik@gitlab.com",
"committed_date": "2020-12-01T13:59:42.000Z",
"web_url": "https://gitlab.com/viktomas/test-project/-/commit/b6d6f6fd17b52b8cf4e961218c572805e9aa7463"
},
{
"id": "f36498b3f5ee3e31001774ee639accc7e0b8242c",
"short_id": "f36498b3",
"created_at": "2020-11-09T14:00:35.000Z",
"parent_ids": [],
"title": "more changes",
"message": "more changes",
"author_name": "Tomas Vik",
"author_email": "tvik@gitlab.com",
"authored_date": "2020-11-09T14:00:35.000Z",
"committer_name": "Tomas Vik",
"committer_email": "tvik@gitlab.com",
"committed_date": "2020-11-09T14:00:35.000Z",
"web_url": "https://gitlab.com/viktomas/test-project/-/commit/f36498b3f5ee3e31001774ee639accc7e0b8242c"
},
{
"id": "15777945e57262106ea300a3bd4dd098e5f8d0ff",
"short_id": "15777945",
"created_at": "2020-11-06T13:50:52.000Z",
"parent_ids": [],
"title": "Update README1.md",
"message": "Update README1.md",
"author_name": "Tomas Vik",
"author_email": "tvik@gitlab.com",
"authored_date": "2020-11-06T13:50:52.000Z",
"committer_name": "Tomas Vik",
"committer_email": "tvik@gitlab.com",
"committed_date": "2020-11-06T13:50:52.000Z",
"web_url": "https://gitlab.com/viktomas/test-project/-/commit/15777945e57262106ea300a3bd4dd098e5f8d0ff"
},
{
"id": "9970d675b33e04edc0a144f973d5171699af026c",
"short_id": "9970d675",
"created_at": "2020-11-06T09:23:12.000Z",
"parent_ids": [],
"title": "Update test.js",
"message": "Update test.js",
"author_name": "Tomas Vik",
"author_email": "tvik@gitlab.com",
"authored_date": "2020-11-06T09:23:12.000Z",
"committer_name": "Tomas Vik",
"committer_email": "tvik@gitlab.com",
"committed_date": "2020-11-06T09:23:12.000Z",
"web_url": "https://gitlab.com/viktomas/test-project/-/commit/9970d675b33e04edc0a144f973d5171699af026c"
}
],
"diffs": [
{
"old_path": ".gitlab-ci.yml",
"new_path": ".gitlab-ci.yml",
"a_mode": "100644",
"b_mode": "0",
"new_file": false,
"renamed_file": false,
"deleted_file": true,
"diff": "@@ -1,12 +0,0 @@\n-image: node:12-slim\n-\n-test:\n- script:\n- - sleep 20\n- - echo hello\n-\n-deploy:\n- stage: deploy\n- script:\n- - sleep 20\n- - echo hello\n\\ No newline at end of file\n"
},
{
"old_path": "README.md",
"new_path": "README1.md",
"a_mode": "100644",
"b_mode": "100644",
"new_file": false,
"renamed_file": true,
"deleted_file": false,
"diff": ""
},
{
"old_path": "new_file.ts",
"new_path": "new_file.ts",
"a_mode": "0",
"b_mode": "100644",
"new_file": true,
"renamed_file": false,
"deleted_file": false,
"diff": "@@ -0,0 +1,3 @@\n+export class NewFile{\n+ private property: string;\n+}\n"
},
{
"old_path": "test.js",
"new_path": "test.js",
"a_mode": "100644",
"b_mode": "100644",
"new_file": false,
"renamed_file": false,
"deleted_file": false,
"diff": "@@ -13,14 +13,11 @@\n function containingFunction(){\n function subFunction(){\n console.log(\"Some Output\");\n+ console.log(\"Some Output1\");\n }\n // Issue is not present when the line after the function name and opening { is empty\n function subFunction(){\n \n- console.log(\"OPutput\");\n+ console.log(\"Output\");\n }\n }\n-\n-function anotherFunction(){\n- console.log(\"Other Output\");\n-}\n"
}
]
}
[
{
"id": 127919672,
"head_commit_sha": "dbcb9939b43a785deeee327e9fa4297550e6b7e3",
"base_commit_sha": "1f0fa02de1f6b913d674a8be10899fb8540237a9",
"start_commit_sha": "1f0fa02de1f6b913d674a8be10899fb8540237a9",
"created_at": "2020-12-01T13:40:48.112Z",
"merge_request_id": 77101970,
"state": "collected",
"real_size": "5"
},
{
"id": 123390038,
"head_commit_sha": "f36498b3f5ee3e31001774ee639accc7e0b8242c",
"base_commit_sha": "1f0fa02de1f6b913d674a8be10899fb8540237a9",
"start_commit_sha": "1f0fa02de1f6b913d674a8be10899fb8540237a9",
"created_at": "2020-11-09T14:00:36.352Z",
"merge_request_id": 77101970,
"state": "collected",
"real_size": "2"
}
]
const assert = require('assert');
const CurrentBranchDataProvider = require('../../src/data_providers/current_branch').DataProvider;
const { tokenService } = require('../../src/services/token_service');
const openIssueResponse = require('./fixtures/rest/open_issue.json');
const pipelinesResopnse = require('./fixtures/rest/pipelines.json');
const pipelineResponse = require('./fixtures/rest/pipeline.json');
const openMergeRequestResponse = require('./fixtures/rest/open_mr.json');
const {
getServer,
createQueryJsonEndpoint,
createJsonEndpoint,
} = require('./test_infrastructure/mock_server');
const { GITLAB_URL } = require('./test_infrastructure/constants');
describe('GitLab tree view for current branch', () => {
let server;
let dataProvider;
before(async () => {
server = getServer([
createQueryJsonEndpoint('/projects/278964/pipelines', {
'?ref=master': pipelinesResopnse,
}),
createJsonEndpoint('/projects/278964/pipelines/47', pipelineResponse),
createQueryJsonEndpoint('/projects/278964/merge_requests', {
'?state=opened&source_branch=master': [openMergeRequestResponse],
}),
createJsonEndpoint('/projects/278964/merge_requests/33824/closes_issues', [
openIssueResponse,
]),
]);
await tokenService.setToken(GITLAB_URL, 'abcd-secret');
});
beforeEach(() => {
server.resetHandlers();
dataProvider = new CurrentBranchDataProvider();
});
after(async () => {
server.close();
await tokenService.setToken(GITLAB_URL, undefined);
});
it('shows pipeline, mr and closing issue for the current branch', async () => {
const forCurrentPipeline = await dataProvider.getChildren();
assert.deepStrictEqual(
forCurrentPipeline.map(i => i.label),
[
'Pipeline #47 passed · Finished 4 years ago',
'!33824 · Web IDE - remove unused actions (mappings)',
'#219925 · Change primary button for editing on files',
],
);
});
});
...@@ -29,7 +29,10 @@ const createPostEndpoint = (path, response) => ...@@ -29,7 +29,10 @@ const createPostEndpoint = (path, response) =>
return res(ctx.status(201), ctx.json(response)); return res(ctx.status(201), ctx.json(response));
}); });
const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => res(ctx.status(404))); const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => {
console.warn(`API call ${req.url.toString()} doesn't have a query handler.`);
res(ctx.status(404));
});
const getServer = (handlers = []) => { const getServer = (handlers = []) => {
const server = setupServer( const server = setupServer(
......
...@@ -5,7 +5,13 @@ const { tokenService } = require('../../src/services/token_service'); ...@@ -5,7 +5,13 @@ const { tokenService } = require('../../src/services/token_service');
const openIssueResponse = require('./fixtures/rest/open_issue.json'); const openIssueResponse = require('./fixtures/rest/open_issue.json');
const openMergeRequestResponse = require('./fixtures/rest/open_mr.json'); const openMergeRequestResponse = require('./fixtures/rest/open_mr.json');
const userResponse = require('./fixtures/rest/user.json'); const userResponse = require('./fixtures/rest/user.json');
const { getServer, createQueryJsonEndpoint } = require('./test_infrastructure/mock_server'); const versionsResponse = require('./fixtures/rest/versions.json');
const versionResponse = require('./fixtures/rest/mr_version.json');
const {
getServer,
createQueryJsonEndpoint,
createJsonEndpoint,
} = require('./test_infrastructure/mock_server');
const { GITLAB_URL } = require('./test_infrastructure/constants'); const { GITLAB_URL } = require('./test_infrastructure/constants');
describe('GitLab tree view', () => { describe('GitLab tree view', () => {
...@@ -74,6 +80,11 @@ describe('GitLab tree view', () => { ...@@ -74,6 +80,11 @@ describe('GitLab tree view', () => {
{ ...openMergeRequestResponse, title: 'Custom Query MR' }, { ...openMergeRequestResponse, title: 'Custom Query MR' },
], ],
}), }),
createJsonEndpoint('/projects/278964/merge_requests/33824/versions', versionsResponse),
createJsonEndpoint(
'/projects/278964/merge_requests/33824/versions/127919672',
versionResponse,
),
createQueryJsonEndpoint('/projects/278964/issues', { createQueryJsonEndpoint('/projects/278964/issues', {
'?scope=assigned_to_me&state=opened': [openIssueResponse], '?scope=assigned_to_me&state=opened': [openIssueResponse],
'?scope=assigned_to_me&state=opened&confidential=true&not[labels]=backstage&not[milestone]=13.5&not[author_username]=johndoe&not[assignee_username]=johndoe&not[search]=bug&not[in]=description': [ '?scope=assigned_to_me&state=opened&confidential=true&not[labels]=backstage&not[milestone]=13.5&not[author_username]=johndoe&not[assignee_username]=johndoe&not[search]=bug&not[in]=description': [
...@@ -119,13 +130,24 @@ describe('GitLab tree view', () => { ...@@ -119,13 +130,24 @@ describe('GitLab tree view', () => {
); );
}); });
it('shows project merge requests assigned to me', async () => { it('shows project merge requests assigned to me with changed files', async () => {
const mergeRequestsAssignedToMe = await openCategory('Merge requests assigned to me'); const mergeRequestsAssignedToMe = await openCategory('Merge requests assigned to me');
assert.strictEqual(mergeRequestsAssignedToMe.length, 1); assert.strictEqual(mergeRequestsAssignedToMe.length, 1);
assert.strictEqual( const mrItem = mergeRequestsAssignedToMe[0];
mergeRequestsAssignedToMe[0].label, assert.strictEqual(mrItem.label, '!33824 · Web IDE - remove unused actions (mappings)');
'!33824 · Web IDE - remove unused actions (mappings)',
const mrContent = await dataProvider.getChildren(mrItem);
assert.strictEqual(mrContent[0].label, 'Description');
const mrFiles = mrContent.slice(1);
assert.deepStrictEqual(
mrFiles.map(f => f.resourceUri.path),
['/.gitlab-ci.yml', '/README1.md', '/new_file.ts', '/test.js'],
);
assert.deepStrictEqual(
mrFiles.map(f => f.description),
['[deleted] .', '[renamed] .', '[added] .', '.'],
); );
}); });
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册