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

feat: insert project snippets into the text editor

上级 241bdef6
...@@ -30,6 +30,7 @@ Advanced pipeline actions allow you to view pipeline on GitLab, create a new pip ...@@ -30,6 +30,7 @@ Advanced pipeline actions allow you to view pipeline on GitLab, create a new pip
- `GitLab: Search project merge requests (Supports filters)`. [Read more](#search-with-filters) - `GitLab: Search project merge requests (Supports filters)`. [Read more](#search-with-filters)
- `GitLab: Project Advanced Search (Issues, MR's, commits, comments...)`. [Read more](#search-with-advanced-search) - `GitLab: Project Advanced Search (Issues, MR's, commits, comments...)`. [Read more](#search-with-advanced-search)
- `GitLab: Create snippet` - Create public, internal or private snippet from entire file or selection. [Read more](#create-snippet). - `GitLab: Create snippet` - Create public, internal or private snippet from entire file or selection. [Read more](#create-snippet).
- `GitLab: Insert snippet` - Insert a project snippet, supports multi-file snippets.
- `GitLab: Compare current branch with master` - Compare your branch with master and view changes on GitLab. [Read more](#compare-with-master). - `GitLab: Compare current branch with master` - Compare your branch with master and view changes on GitLab. [Read more](#compare-with-master).
- `GitLab: Open active file on GitLab` - View active file on GitLab with highlighting active line number and selected text block. [Read more](#open-active-file). - `GitLab: Open active file on GitLab` - View active file on GitLab with highlighting active line number and selected text block. [Read more](#open-active-file).
- `GitLab: Validate GitLab CI config` - Validate GitLab CI configuration file `.gitlab-ci.yml`. [Read more](#validate-gitlab-ci-configuration). - `GitLab: Validate GitLab CI config` - Validate GitLab CI configuration file `.gitlab-ci.yml`. [Read more](#validate-gitlab-ci-configuration).
......
...@@ -2808,6 +2808,14 @@ ...@@ -2808,6 +2808,14 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
}, },
"cross-fetch": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz",
"integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==",
"requires": {
"node-fetch": "2.6.1"
}
},
"cross-spawn": { "cross-spawn": {
"version": "6.0.5", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
...@@ -2909,7 +2917,6 @@ ...@@ -2909,7 +2917,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": { "requires": {
"ms": "^2.1.1" "ms": "^2.1.1"
} }
...@@ -3806,6 +3813,11 @@ ...@@ -3806,6 +3813,11 @@
} }
} }
}, },
"extract-files": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
"integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ=="
},
"extsprintf": { "extsprintf": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
...@@ -4441,8 +4453,29 @@ ...@@ -4441,8 +4453,29 @@
"graphql": { "graphql": {
"version": "15.3.0", "version": "15.3.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==", "integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
"dev": true },
"graphql-request": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.2.0.tgz",
"integrity": "sha512-s4zIfQsDkGUv5ECCkGq55Png7hJjFBV7PMIadB403VDaXv0T1RThPSRgZM1hiKgB420rOItkR5BDQ3vPvaAWqw==",
"requires": {
"cross-fetch": "^3.0.6",
"extract-files": "^9.0.0",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
}, },
"growl": { "growl": {
"version": "1.10.5", "version": "1.10.5",
...@@ -4707,22 +4740,20 @@ ...@@ -4707,22 +4740,20 @@
} }
}, },
"https-proxy-agent": { "https-proxy-agent": {
"version": "2.2.4", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": { "requires": {
"agent-base": "^4.3.0", "agent-base": "6",
"debug": "^3.1.0" "debug": "4"
}, },
"dependencies": { "dependencies": {
"debug": { "agent-base": {
"version": "3.2.6", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==",
"dev": true,
"requires": { "requires": {
"ms": "^2.1.1" "debug": "4"
} }
} }
} }
...@@ -8181,8 +8212,7 @@ ...@@ -8181,8 +8212,7 @@
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true
}, },
"msw": { "msw": {
"version": "0.21.2", "version": "0.21.2",
...@@ -8393,8 +8423,7 @@ ...@@ -8393,8 +8423,7 @@
"node-fetch": { "node-fetch": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
"dev": true
}, },
"node-int64": { "node-int64": {
"version": "0.4.0", "version": "0.4.0",
...@@ -11120,6 +11149,27 @@ ...@@ -11120,6 +11149,27 @@
"http-proxy-agent": "^2.1.0", "http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.4", "https-proxy-agent": "^2.2.4",
"rimraf": "^2.6.3" "rimraf": "^2.6.3"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"https-proxy-agent": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
"integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
"dev": true,
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
}
}
} }
}, },
"w3c-hr-time": { "w3c-hr-time": {
......
...@@ -105,6 +105,10 @@ ...@@ -105,6 +105,10 @@
"command": "gl.createSnippet", "command": "gl.createSnippet",
"title": "GitLab: Create snippet" "title": "GitLab: Create snippet"
}, },
{
"command": "gl.insertSnippet",
"title": "GitLab: Insert snippet"
},
{ {
"command": "gl.validateCIConfig", "command": "gl.validateCIConfig",
"title": "GitLab: Validate GitLab CI config" "title": "GitLab: Validate GitLab CI config"
...@@ -542,7 +546,11 @@ ...@@ -542,7 +546,11 @@
"vscode-test": "^1.4.0" "vscode-test": "^1.4.0"
}, },
"dependencies": { "dependencies": {
"cross-fetch": "^3.0.6",
"execa": "^4.0.3", "execa": "^4.0.3",
"graphql": "^15.3.0",
"graphql-request": "^3.2.0",
"https-proxy-agent": "^5.0.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise": "^4.2.5", "request-promise": "^4.2.5",
......
const vscode = require('vscode'); const vscode = require('vscode');
const openers = require('./openers'); const openers = require('../openers');
const gitLabService = require('./gitlab_service'); const gitLabService = require('../gitlab_service');
const gitlabProjectInput = require('./gitlab_project_input'); const gitlabProjectInput = require('../gitlab_project_input');
const { getCurrentWorkspaceFolder } = require('./services/workspace_service'); const { getCurrentWorkspaceFolder } = require('../services/workspace_service');
const visibilityOptions = [ const visibilityOptions = [
{ {
...@@ -30,7 +30,7 @@ const contextOptions = [ ...@@ -30,7 +30,7 @@ const contextOptions = [
}, },
]; ];
async function createSnippet(project, editor, visibility, context) { async function uploadSnippet(project, editor, visibility, context) {
let content = ''; let content = '';
const fileName = editor.document.fileName.split('/').reverse()[0]; const fileName = editor.document.fileName.split('/').reverse()[0];
...@@ -62,7 +62,7 @@ async function createSnippet(project, editor, visibility, context) { ...@@ -62,7 +62,7 @@ async function createSnippet(project, editor, visibility, context) {
openers.openUrl(snippet.web_url); openers.openUrl(snippet.web_url);
} }
async function showPicker() { async function createSnippet() {
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor;
let workspaceFolder = null; let workspaceFolder = null;
let project = null; let project = null;
...@@ -90,7 +90,7 @@ async function showPicker() { ...@@ -90,7 +90,7 @@ async function showPicker() {
const context = await vscode.window.showQuickPick(contextOptions); const context = await vscode.window.showQuickPick(contextOptions);
if (context) { if (context) {
createSnippet(project, editor, visibility.type, context.type); uploadSnippet(project, editor, visibility.type, context.type);
} }
} }
} else { } else {
...@@ -98,4 +98,6 @@ async function showPicker() { ...@@ -98,4 +98,6 @@ async function showPicker() {
} }
} }
exports.show = showPicker; module.exports = {
createSnippet,
};
import * as vscode from 'vscode';
import { GitLabNewService, GraphQLSnippet, GraphQLBlob } from '../gitlab/gitlab_new_service';
import { createGitService } from '../git_service_factory';
import { getCurrentWorkspaceFolderOrSelectOne } from '../services/workspace_service';
const pickSnippet = async (snippets: GraphQLSnippet[]) => {
const quickPickItems = snippets.map(s => ({
label: s.title,
description: s.description,
detail: s.blobs.nodes.map(blob => blob.name).join(','),
original: s,
}));
return vscode.window.showQuickPick(quickPickItems);
};
const pickBlob = async (blobs: GraphQLBlob[]) => {
const quickPickItems = blobs.map(b => ({
label: b.name,
original: b,
}));
const result = await vscode.window.showQuickPick(quickPickItems);
return result?.original;
};
export const insertSnippet = async (): Promise<void> => {
if (!vscode.window.activeTextEditor) {
vscode.window.showInformationMessage('There is no open file.');
return;
}
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) {
return;
}
const gitService = createGitService(workspaceFolder);
const instanceUrl = await gitService.fetchCurrentInstanceUrl();
const gitLabService = new GitLabNewService(instanceUrl);
const remote = await gitService.fetchGitRemote();
if (!remote) {
throw new Error('Could not get parsed remote for your workspace');
}
const snippets = await gitLabService.getSnippets(`${remote.namespace}/${remote.project}`);
if (snippets.length === 0) {
vscode.window.showInformationMessage('There are no project snippets.');
return;
}
const result = await pickSnippet(snippets);
if (!result) {
return;
}
const blobs = result.original.blobs.nodes;
const blob = blobs.length > 1 ? await pickBlob(blobs) : blobs[0];
if (!blob) {
return;
}
const snippet = await gitLabService.getSnippetContent(result.original, blob);
const editor = vscode.window.activeTextEditor;
await editor.edit(editBuilder => {
editBuilder.insert(editor.selection.start, snippet);
});
};
...@@ -4,6 +4,7 @@ const gitLabService = require('../gitlab_service'); ...@@ -4,6 +4,7 @@ const gitLabService = require('../gitlab_service');
const { SidebarTreeItem } = require('./sidebar_tree_item'); const { SidebarTreeItem } = require('./sidebar_tree_item');
const ErrorItem = require('./error_item'); const ErrorItem = require('./error_item');
const { getCurrentWorkspaceFolder } = require('../services/workspace_service'); const { getCurrentWorkspaceFolder } = require('../services/workspace_service');
const { handleError } = require('../log');
class DataProvider { class DataProvider {
constructor() { constructor() {
...@@ -101,7 +102,7 @@ class DataProvider { ...@@ -101,7 +102,7 @@ class DataProvider {
await this.fetchMR(workspaceFolder); await this.fetchMR(workspaceFolder);
await this.fetchClosingIssue(workspaceFolder); await this.fetchClosingIssue(workspaceFolder);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(e); handleError(e);
this.children.push(new ErrorItem()); this.children.push(new ErrorItem());
} }
......
...@@ -2,6 +2,7 @@ const vscode = require('vscode'); ...@@ -2,6 +2,7 @@ const vscode = require('vscode');
const gitLabService = require('../gitlab_service'); const gitLabService = require('../gitlab_service');
const { SidebarTreeItem } = require('./sidebar_tree_item'); const { SidebarTreeItem } = require('./sidebar_tree_item');
const ErrorItem = require('./error_item'); const ErrorItem = require('./error_item');
const { handleError } = require('../log');
class DataProvider { class DataProvider {
constructor() { constructor() {
...@@ -48,7 +49,7 @@ class DataProvider { ...@@ -48,7 +49,7 @@ class DataProvider {
try { try {
return await this.collectIssuables(el); return await this.collectIssuables(el);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(e); handleError(e);
return [new ErrorItem()]; return [new ErrorItem()];
} }
} }
......
/* eslint-disable max-classes-per-file */
const { StatusCodeError } = require('request-promise/errors'); const { StatusCodeError } = require('request-promise/errors');
const { prettyJson, stackToArray } = require('./common');
const prettyJson = obj => JSON.stringify(obj, null, 2);
const stackToArray = stack => stack && stack.split('\n');
class ApiError extends Error { class ApiError extends Error {
constructor(error, action) { constructor(error, action) {
...@@ -33,24 +29,4 @@ class ApiError extends Error { ...@@ -33,24 +29,4 @@ class ApiError extends Error {
} }
} }
class UserFriendlyError extends Error { module.exports = { ApiError };
constructor(message, originalError, additionalInfo) {
super(message);
this.originalError = originalError;
this.additionalInfo = additionalInfo;
}
get details() {
return prettyJson({
userMessage: this.message,
errorMessage: this.originalError.message,
stack: stackToArray(this.originalError.stack),
additionalInfo: this.additionalInfo,
});
}
}
module.exports = {
ApiError,
UserFriendlyError,
};
export const prettyJson = (obj: Record<string, unknown>) => JSON.stringify(obj, null, 2);
export const stackToArray = (stack: string | undefined) => stack && stack.split('\n');
export interface IDetailedError extends Error {
readonly details: string;
}
import { prettyJson, stackToArray, IDetailedError } from './common';
export class FetchError extends Error implements IDetailedError {
response: Response;
constructor(message: string, response: Response) {
super(message);
this.response = response;
}
private get requestDetails() {
return {
response: {
status: this.response.status,
headers: this.response.headers,
},
};
}
get details(): string {
const { message, stack } = this;
return prettyJson({
message,
stack: stackToArray(stack),
...this.requestDetails,
});
}
}
import { prettyJson, stackToArray, IDetailedError } from './common';
export class UserFriendlyError extends Error implements IDetailedError {
originalError: Error;
additionalInfo: string;
constructor(message: string, originalError: Error, additionalInfo: string) {
super(message);
this.originalError = originalError;
this.additionalInfo = additionalInfo;
}
get details(): string {
return prettyJson({
userMessage: this.message,
errorMessage: this.originalError.message,
stack: stackToArray(this.originalError.stack),
additionalInfo: this.additionalInfo,
});
}
}
...@@ -5,31 +5,24 @@ const { tokenService } = require('./services/token_service'); ...@@ -5,31 +5,24 @@ const { tokenService } = require('./services/token_service');
const tokenServiceWrapper = require('./token_service_wrapper'); const tokenServiceWrapper = require('./token_service_wrapper');
const pipelineActionsPicker = require('./pipeline_actions_picker'); const pipelineActionsPicker = require('./pipeline_actions_picker');
const searchInput = require('./search_input'); const searchInput = require('./search_input');
const snippetInput = require('./snippet_input'); const { createSnippet } = require('./commands/create_snippet');
const { insertSnippet } = require('./commands/insert_snippet');
const sidebar = require('./sidebar'); const sidebar = require('./sidebar');
const ciConfigValidator = require('./ci_config_validator'); const ciConfigValidator = require('./ci_config_validator');
const webviewController = require('./webview_controller'); const webviewController = require('./webview_controller');
const IssuableDataProvider = require('./data_providers/issuable').DataProvider; const IssuableDataProvider = require('./data_providers/issuable').DataProvider;
const CurrentBranchDataProvider = require('./data_providers/current_branch').DataProvider; const CurrentBranchDataProvider = require('./data_providers/current_branch').DataProvider;
const { initializeLogging, handleError } = require('./log');
vscode.gitLabWorkflow = { vscode.gitLabWorkflow = {
sidebarDataProviders: [], sidebarDataProviders: [],
log: () => {},
logError: e => vscode.gitLabWorkflow.log(e.details || `${e.message}\n${e.stack}`),
handleError: async e => {
vscode.gitLabWorkflow.logError(e);
const choice = await vscode.window.showErrorMessage(e.message, null, 'Show logs');
if (choice === 'Show logs') {
await vscode.commands.executeCommand('gl.showOutput');
}
},
}; };
const wrapWithCatch = command => async (...args) => { const wrapWithCatch = command => async (...args) => {
try { try {
await command(...args); await command(...args);
} catch (e) { } catch (e) {
await vscode.gitLabWorkflow.handleError(e); await handleError(e);
} }
}; };
...@@ -65,7 +58,8 @@ const registerCommands = (context, outputChannel) => { ...@@ -65,7 +58,8 @@ const registerCommands = (context, outputChannel) => {
'gl.mergeRequestSearch': searchInput.showMergeRequestSearchInput, 'gl.mergeRequestSearch': searchInput.showMergeRequestSearchInput,
'gl.projectAdvancedSearch': searchInput.showProjectAdvancedSearchInput, 'gl.projectAdvancedSearch': searchInput.showProjectAdvancedSearchInput,
'gl.compareCurrentBranch': openers.compareCurrentBranch, 'gl.compareCurrentBranch': openers.compareCurrentBranch,
'gl.createSnippet': snippetInput.show, 'gl.createSnippet': createSnippet,
'gl.insertSnippet': insertSnippet,
'gl.validateCIConfig': ciConfigValidator.validate, 'gl.validateCIConfig': ciConfigValidator.validate,
'gl.refreshSidebar': sidebar.refresh, 'gl.refreshSidebar': sidebar.refresh,
'gl.showRichContent': webviewController.create, 'gl.showRichContent': webviewController.create,
...@@ -81,7 +75,7 @@ const registerCommands = (context, outputChannel) => { ...@@ -81,7 +75,7 @@ const registerCommands = (context, outputChannel) => {
const activate = context => { const activate = context => {
const outputChannel = vscode.window.createOutputChannel('GitLab Workflow'); const outputChannel = vscode.window.createOutputChannel('GitLab Workflow');
vscode.gitLabWorkflow.log = line => outputChannel.appendLine(line); initializeLogging(line => outputChannel.appendLine(line));
registerCommands(context, outputChannel); registerCommands(context, outputChannel);
webviewController.addDeps(context); webviewController.addDeps(context);
......
import * as vscode from 'vscode';
import { GitService } from './git_service';
import { tokenService } from './services/token_service';
import { log } from './log';
export function createGitService(workspaceFolder: string): GitService {
const { instanceUrl, remoteName, pipelineGitRemoteName } = vscode.workspace.getConfiguration(
'gitlab',
);
// the getConfiguration() returns null for missing attributes, we need to convert them to
// undefined so that we can use optional properties and default function parameters
return new GitService({
workspaceFolder,
instanceUrl: instanceUrl || undefined,
remoteName: remoteName || undefined,
pipelineGitRemoteName: pipelineGitRemoteName || undefined,
tokenService,
log,
});
}
import * as vscode from 'vscode';
import { GraphQLClient, gql } from 'graphql-request';
import crossFetch from 'cross-fetch';
import { URL } from 'url';
import * as createHttpProxyAgent from 'https-proxy-agent';
import { tokenService } from '../services/token_service';
import { FetchError } from '../errors/fetch_error';
interface Node<T> {
nodes: T[];
}
interface GraphQLProjectResult {
project?: {
id: string;
snippets: Node<GraphQLSnippet>;
};
}
export interface GraphQLSnippet {
id: string;
projectId: string;
title: string;
description: string;
blobs: Node<GraphQLBlob>;
}
export interface GraphQLBlob {
name: string;
path: string;
}
const queryGetSnippets = gql`
query GetSnippets($projectPath: ID!) {
project(fullPath: $projectPath) {
id
snippets {
nodes {
id
title
description
blobs {
nodes {
name
path
}
}
}
}
}
}
`;
const getUserAgent = () => {
const extension = vscode.extensions.getExtension('GitLab.gitlab-workflow');
const packageJson: Record<string, string | undefined> = extension?.packageJSON;
return `vs-code-gitlab-workflow/${packageJson.version}`;
};
export class GitLabNewService {
client: GraphQLClient;
instanceUrl: string;
constructor(instanceUrl: string) {
this.instanceUrl = instanceUrl;
const endpoint = new URL('/api/graphql', this.instanceUrl).href;
this.client = new GraphQLClient(endpoint, this.fetchOptions);
}
private get fetchOptions() {
const token = tokenService.getToken(this.instanceUrl);
const { proxy } = vscode.workspace.getConfiguration('http');
const agent = proxy ? createHttpProxyAgent(proxy) : undefined;
return {
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': getUserAgent(),
},
agent,
};
}
async getSnippets(projectPath: string): Promise<GraphQLSnippet[]> {
const result = await this.client.request<GraphQLProjectResult>(queryGetSnippets, {
projectPath,
});
const { project } = result;
// this can mean three things: project doesn't exist, user doesn't have access, or user credentials are wrong
// https://gitlab.com/gitlab-org/gitlab/-/issues/270055
if (!project) {
throw new Error(
`Project ${projectPath} was not found. You might not have permissions to see it.`,
);
}
const snippets = project.snippets.nodes;
// each snippet has to contain projectId so we can make REST API call for the content
return snippets.map(sn => ({
...sn,
projectId: project.id,
}));
}
// TODO change this method to use GraphQL when https://gitlab.com/gitlab-org/gitlab/-/issues/260316 is done
async getSnippetContent(snippet: GraphQLSnippet, blob: GraphQLBlob): Promise<string> {
const projectId = snippet.projectId.replace('gid://gitlab/Project/', '');
const snippetId = snippet.id.replace('gid://gitlab/ProjectSnippet/', '');
const url = `${this.instanceUrl}/api/v4/projects/${projectId}/snippets/${snippetId}/files/master/${blob.path}/raw`;
const result = await crossFetch(url, this.fetchOptions);
if (!result.ok) {
throw new FetchError(`Fetching snippet from ${url} failed`, result);
}
return result.text();
}
}
const vscode = require('vscode'); const vscode = require('vscode');
const request = require('request-promise'); const request = require('request-promise');
const fs = require('fs'); const fs = require('fs');
const { GitService } = require('./git_service');
const { tokenService } = require('./services/token_service'); const { tokenService } = require('./services/token_service');
const statusBar = require('./status_bar'); const statusBar = require('./status_bar');
const { ApiError, UserFriendlyError } = require('./errors'); const { UserFriendlyError } = require('./errors/user_friendly_error');
const { ApiError } = require('./errors/api_error');
const { getCurrentWorkspaceFolder } = require('./services/workspace_service'); const { getCurrentWorkspaceFolder } = require('./services/workspace_service');
const { createGitService } = require('./git_service_factory');
const { handleError, logError } = require('./log');
const projectCache = []; const projectCache = [];
let versionCache = null; let versionCache = null;
const createGitService = workspaceFolder => {
const { instanceUrl, remoteName, pipelineGitRemoteName } = vscode.workspace.getConfiguration(
'gitlab',
);
return new GitService({
workspaceFolder,
instanceUrl: instanceUrl || undefined,
remoteName: remoteName || undefined,
pipelineGitRemoteName: pipelineGitRemoteName || undefined,
tokenService,
log: vscode.gitLabWorkflow.log,
});
};
async function fetch(path, method = 'GET', data = null) { async function fetch(path, method = 'GET', data = null) {
const { ignoreCertificateErrors, ca, cert, certKey } = vscode.workspace.getConfiguration( const { ignoreCertificateErrors, ca, cert, certKey } = vscode.workspace.getConfiguration(
'gitlab', 'gitlab',
...@@ -69,7 +57,7 @@ async function fetch(path, method = 'GET', data = null) { ...@@ -69,7 +57,7 @@ async function fetch(path, method = 'GET', data = null) {
try { try {
config.ca = fs.readFileSync(ca); config.ca = fs.readFileSync(ca);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(new UserFriendlyError(`Cannot read CA '${ca}'`, e)); handleError(new UserFriendlyError(`Cannot read CA '${ca}'`, e));
} }
} }
...@@ -77,7 +65,7 @@ async function fetch(path, method = 'GET', data = null) { ...@@ -77,7 +65,7 @@ async function fetch(path, method = 'GET', data = null) {
try { try {
config.cert = fs.readFileSync(cert); config.cert = fs.readFileSync(cert);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(new UserFriendlyError(`Cannot read CA '${cert}'`, e)); handleError(new UserFriendlyError(`Cannot read CA '${cert}'`, e));
} }
} }
...@@ -85,7 +73,7 @@ async function fetch(path, method = 'GET', data = null) { ...@@ -85,7 +73,7 @@ async function fetch(path, method = 'GET', data = null) {
try { try {
config.key = fs.readFileSync(certKey); config.key = fs.readFileSync(certKey);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(new UserFriendlyError(`Cannot read CA '${certKey}'`, e)); handleError(new UserFriendlyError(`Cannot read CA '${certKey}'`, e));
} }
} }
...@@ -100,7 +88,7 @@ async function fetch(path, method = 'GET', data = null) { ...@@ -100,7 +88,7 @@ async function fetch(path, method = 'GET', data = null) {
headers: response.headers, headers: response.headers,
}; };
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError( handleError(
new UserFriendlyError('Failed to parse GitLab API response', e, `Response body: ${body}`), new UserFriendlyError('Failed to parse GitLab API response', e, `Response body: ${body}`),
); );
return { error: e }; return { error: e };
...@@ -138,7 +126,7 @@ async function fetchCurrentProjectSwallowError(workspaceFolder) { ...@@ -138,7 +126,7 @@ async function fetchCurrentProjectSwallowError(workspaceFolder) {
try { try {
return await fetchCurrentProject(workspaceFolder); return await fetchCurrentProject(workspaceFolder);
} catch (error) { } catch (error) {
vscode.gitLabWorkflow.logError(error); logError(error);
return null; return null;
} }
} }
...@@ -149,7 +137,7 @@ async function fetchCurrentPipelineProject(workspaceFolder) { ...@@ -149,7 +137,7 @@ async function fetchCurrentPipelineProject(workspaceFolder) {
return await fetchProjectData(remote); return await fetchProjectData(remote);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
return null; return null;
} }
} }
...@@ -168,7 +156,7 @@ async function fetchFirstUserByUsername(userName) { ...@@ -168,7 +156,7 @@ async function fetchFirstUserByUsername(userName) {
const { response: users } = await fetch(`/users?username=${userName}`); const { response: users } = await fetch(`/users?username=${userName}`);
return users[0]; return users[0];
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(new UserFriendlyError('Error when fetching GitLab user.', e)); handleError(new UserFriendlyError('Error when fetching GitLab user.', e));
return undefined; return undefined;
} }
} }
...@@ -180,7 +168,7 @@ async function fetchVersion() { ...@@ -180,7 +168,7 @@ async function fetchVersion() {
versionCache = response.version; versionCache = response.version;
} }
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
} }
return versionCache; return versionCache;
...@@ -473,7 +461,7 @@ async function handlePipelineAction(action, workspaceFolder) { ...@@ -473,7 +461,7 @@ async function handlePipelineAction(action, workspaceFolder) {
const { response } = await fetch(endpoint, 'POST'); const { response } = await fetch(endpoint, 'POST');
newPipeline = response; newPipeline = response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(new UserFriendlyError(`Failed to ${action} pipeline.`, e)); handleError(new UserFriendlyError(`Failed to ${action} pipeline.`, e));
} }
if (newPipeline) { if (newPipeline) {
...@@ -495,7 +483,7 @@ async function fetchMRIssues(mrId, workspaceFolder) { ...@@ -495,7 +483,7 @@ async function fetchMRIssues(mrId, workspaceFolder) {
); );
issues = response; issues = response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
} }
} }
...@@ -514,7 +502,7 @@ async function createSnippet(data) { ...@@ -514,7 +502,7 @@ async function createSnippet(data) {
const { response } = await fetch(path, 'POST', data); const { response } = await fetch(path, 'POST', data);
snippet = response; snippet = response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(new UserFriendlyError('Failed to create your snippet.', e)); handleError(new UserFriendlyError('Failed to create your snippet.', e));
} }
return snippet; return snippet;
...@@ -527,9 +515,7 @@ async function validateCIConfig(content) { ...@@ -527,9 +515,7 @@ async function validateCIConfig(content) {
const { response } = await fetch('/ci/lint', 'POST', { content }); const { response } = await fetch('/ci/lint', 'POST', { content });
validCIConfig = response; validCIConfig = response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError( handleError(new UserFriendlyError('Failed to validate CI configuration.', e));
new UserFriendlyError('Failed to validate CI configuration.', e),
);
} }
return validCIConfig; return validCIConfig;
...@@ -545,9 +531,7 @@ async function fetchLabelEvents(issuable) { ...@@ -545,9 +531,7 @@ async function fetchLabelEvents(issuable) {
); );
labelEvents = response; labelEvents = response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError( handleError(new UserFriendlyError('Failed to fetch label events for this issuable.', e));
new UserFriendlyError('Failed to fetch label events for this issuable.', e),
);
} }
labelEvents.forEach(el => { labelEvents.forEach(el => {
...@@ -583,9 +567,7 @@ async function fetchDiscussions(issuable, page = 1) { ...@@ -583,9 +567,7 @@ async function fetchDiscussions(issuable, page = 1) {
delete discussions.headers; delete discussions.headers;
} }
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError( handleError(new UserFriendlyError('Failed to fetch discussions for this issuable.', e));
new UserFriendlyError('Failed to fetch discussions for this issuable.', e),
);
} }
return discussions; return discussions;
...@@ -612,7 +594,7 @@ async function renderMarkdown(markdown, workspaceFolder) { ...@@ -612,7 +594,7 @@ async function renderMarkdown(markdown, workspaceFolder) {
}); });
rendered = response; rendered = response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
return markdown; return markdown;
} }
...@@ -629,7 +611,7 @@ async function saveNote({ issuable, note, noteType }) { ...@@ -629,7 +611,7 @@ async function saveNote({ issuable, note, noteType }) {
}); });
return response; return response;
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
} }
return { success: false }; return { success: false };
...@@ -653,4 +635,3 @@ exports.renderMarkdown = renderMarkdown; ...@@ -653,4 +635,3 @@ exports.renderMarkdown = renderMarkdown;
exports.saveNote = saveNote; exports.saveNote = saveNote;
exports.getAllGitlabProjects = getAllGitlabProjects; exports.getAllGitlabProjects = getAllGitlabProjects;
exports.fetchLabelEvents = fetchLabelEvents; exports.fetchLabelEvents = fetchLabelEvents;
exports.createGitService = createGitService;
import * as vscode from 'vscode';
import { IDetailedError } from './errors/common';
function isDetailedError(object: any): object is IDetailedError {
return Boolean(object.details);
}
type logFunction = (line: string) => void;
let globalLog: logFunction;
export const initializeLogging = (logLine: logFunction): void => {
globalLog = logLine;
};
export const log = (line: string): void => globalLog(line);
export const logError = (e: Error | IDetailedError): void =>
isDetailedError(e) ? globalLog(e.details) : globalLog(`${e.message}\n${e.stack}`);
export const handleError = async (e: Error | IDetailedError): Promise<void> => {
logError(e);
const choice = await vscode.window.showErrorMessage(e.message, 'Show logs');
if (choice === 'Show logs') {
await vscode.commands.executeCommand('gl.showOutput');
}
};
const vscode = require('vscode'); const vscode = require('vscode');
const { GitService } = require('./git_service');
const gitLabService = require('./gitlab_service'); const gitLabService = require('./gitlab_service');
const { tokenService } = require('./services/token_service');
const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service'); const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service');
const { createGitService } = require('./git_service_factory');
const { handleError } = require('./log');
const openUrl = url => vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); const openUrl = url => vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url));
const createGitService = workspaceFolder => {
const { instanceUrl, remoteName, pipelineGitRemoteName } = vscode.workspace.getConfiguration(
'gitlab',
);
return new GitService({
workspaceFolder,
instanceUrl: instanceUrl || undefined,
remoteName: remoteName || undefined,
pipelineGitRemoteName: pipelineGitRemoteName || undefined,
tokenService,
log: vscode.gitLabWorkflow.log,
});
};
/** /**
* Fetches user and project before opening a link. * Fetches user and project before opening a link.
* Link can contain some placeholders which will be replaced by this method * Link can contain some placeholders which will be replaced by this method
...@@ -66,7 +52,7 @@ async function getActiveFile() { ...@@ -66,7 +52,7 @@ async function getActiveFile() {
try { try {
currentProject = await gitLabService.fetchCurrentProject(workspaceFolder); currentProject = await gitLabService.fetchCurrentProject(workspaceFolder);
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError(e); handleError(e);
return undefined; return undefined;
} }
const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName(); const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName();
......
...@@ -2,6 +2,7 @@ const vscode = require('vscode'); ...@@ -2,6 +2,7 @@ const vscode = require('vscode');
const gitLabService = require('./gitlab_service'); const gitLabService = require('./gitlab_service');
const openers = require('./openers'); const openers = require('./openers');
const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service'); const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service');
const { createGitService } = require('./git_service_factory');
const parseQuery = (query, noteableType) => { const parseQuery = (query, noteableType) => {
const params = {}; const params = {};
...@@ -120,9 +121,7 @@ async function showProjectAdvancedSearchInput() { ...@@ -120,9 +121,7 @@ async function showProjectAdvancedSearchInput() {
'Project Advanced Search. (Check extension page for Advanced Search)', 'Project Advanced Search. (Check extension page for Advanced Search)',
); );
const queryString = await encodeURIComponent(query); const queryString = await encodeURIComponent(query);
const instanceUrl = await gitLabService const instanceUrl = await createGitService(workspaceFolder).fetchCurrentInstanceUrl();
.createGitService(workspaceFolder)
.fetchCurrentInstanceUrl();
// Select issues tab by default for Advanced Search // Select issues tab by default for Advanced Search
await openers.openUrl( await openers.openUrl(
......
...@@ -2,7 +2,8 @@ const vscode = require('vscode'); ...@@ -2,7 +2,8 @@ const vscode = require('vscode');
const openers = require('./openers'); const openers = require('./openers');
const gitLabService = require('./gitlab_service'); const gitLabService = require('./gitlab_service');
const { getCurrentWorkspaceFolder } = require('./services/workspace_service'); const { getCurrentWorkspaceFolder } = require('./services/workspace_service');
const { UserFriendlyError } = require('./errors'); const { UserFriendlyError } = require('./errors/user_friendly_error');
const { handleError, logError } = require('./log');
let context = null; let context = null;
let pipelineStatusBarItem = null; let pipelineStatusBarItem = null;
...@@ -60,7 +61,7 @@ async function refreshPipeline() { ...@@ -60,7 +61,7 @@ async function refreshPipeline() {
pipelineStatusBarItem.hide(); pipelineStatusBarItem.hide();
} }
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
if (!project) { if (!project) {
pipelineStatusBarItem.hide(); pipelineStatusBarItem.hide();
return; return;
...@@ -86,9 +87,7 @@ async function refreshPipeline() { ...@@ -86,9 +87,7 @@ async function refreshPipeline() {
} }
} }
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.handleError( handleError(new UserFriendlyError('Failed to fetch jobs for pipeline.', e));
new UserFriendlyError('Failed to fetch jobs for pipeline.', e),
);
} }
} }
...@@ -154,7 +153,7 @@ async function fetchBranchMR() { ...@@ -154,7 +153,7 @@ async function fetchBranchMR() {
mrStatusBarItem.hide(); mrStatusBarItem.hide();
} }
} catch (e) { } catch (e) {
vscode.gitLabWorkflow.logError(e); logError(e);
mrStatusBarItem.hide(); mrStatusBarItem.hide();
} }
......
...@@ -2,9 +2,11 @@ import * as temp from 'temp'; ...@@ -2,9 +2,11 @@ import * as temp from 'temp';
import simpleGit from 'simple-git'; import simpleGit from 'simple-git';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { REMOTE } from './integration/test_infrastructure/constants';
const vsCodeSettings = { const vsCodeSettings = {
'gitlab.instanceUrl': 'https://test.gitlab.com', 'gitlab.instanceUrl': 'https://test.gitlab.com',
'files.enableTrash': false,
}; };
async function createTempFolder(): Promise<string> { async function createTempFolder(): Promise<string> {
...@@ -31,7 +33,7 @@ export default async function createTmpWorkspace(autoCleanUp = true): Promise<st ...@@ -31,7 +33,7 @@ export default async function createTmpWorkspace(autoCleanUp = true): Promise<st
const dirPath = await createTempFolder(); const dirPath = await createTempFolder();
const git = simpleGit(dirPath, { binary: 'git' }); const git = simpleGit(dirPath, { binary: 'git' });
await git.init(); await git.init();
await git.addRemote('origin', 'git@test.gitlab.com:gitlab-org/gitlab.git'); await git.addRemote(REMOTE.NAME, REMOTE.URL);
await addFile(dirPath, '/.vscode/settings.json', JSON.stringify(vsCodeSettings)); await addFile(dirPath, '/.vscode/settings.json', JSON.stringify(vsCodeSettings));
return dirPath; return dirPath;
} }
{
"project": {
"id": "gid://gitlab/Project/278964",
"snippets": {
"nodes": [
{
"id": "gid://gitlab/ProjectSnippet/111",
"title": "Test snippet",
"description": "test description",
"blobs": {
"nodes": [
{
"name": "test.js",
"path": "test.js"
}
]
}
},
{
"id": "gid://gitlab/ProjectSnippet/222",
"title": "Test snippet",
"description": "test description",
"blobs": {
"nodes": [
{
"name": "test1.js",
"path": "test1.js"
},
{
"name": "test2.js",
"path": "test2.js"
}
]
}
}
]
}
}
}
const assert = require('assert');
const sinon = require('sinon');
const vscode = require('vscode');
const simpleGit = require('simple-git');
const { insertSnippet } = require('../../src/commands/insert_snippet');
const { tokenService } = require('../../src/services/token_service');
const getServer = require('./test_infrastructure/mock_server');
const { GITLAB_HOST, REMOTE } = require('./test_infrastructure/constants');
const {
createAndOpenFile,
closeAndDeleteFile,
simulateQuickPickChoice,
} = require('./test_infrastructure/helpers');
describe('Insert snippet', async () => {
let server;
let testFileUri;
const sandbox = sinon.createSandbox();
before(() => {
server = getServer();
tokenService.setToken(`https://${GITLAB_HOST}`, 'abcd-secret');
});
beforeEach(async () => {
server.resetHandlers();
testFileUri = vscode.Uri.parse(`${vscode.workspace.workspaceFolders[0].uri.fsPath}/newfile.js`);
await createAndOpenFile(testFileUri);
});
afterEach(async () => {
const git = simpleGit(vscode.workspace.workspaceFolders[0].uri.fsPath);
await git.removeRemote(REMOTE.NAME);
await git.addRemote(REMOTE.NAME, REMOTE.URL);
sandbox.restore();
await closeAndDeleteFile(testFileUri);
});
after(() => {
server.close();
tokenService.setToken(`https://${GITLAB_HOST}`, undefined);
});
it('inserts snippet when there is only one blob', async () => {
simulateQuickPickChoice(sandbox, 0);
await insertSnippet();
assert.strictEqual(vscode.window.activeTextEditor.document.getText(), 'snippet content');
});
it('inserts snippet when there are multiple blobs', async () => {
simulateQuickPickChoice(sandbox, 1);
await insertSnippet();
assert.strictEqual(vscode.window.activeTextEditor.document.getText(), 'second blob content');
});
it('throws an error when it cannot find GitLab project', async () => {
const git = simpleGit(vscode.workspace.workspaceFolders[0].uri.fsPath);
await git.removeRemote(REMOTE.NAME);
await git.addRemote(REMOTE.NAME, 'git@test.gitlab.com:gitlab-org/nonexistent.git');
await assert.rejects(insertSnippet(), /Project gitlab-org\/nonexistent was not found./);
});
});
...@@ -2,6 +2,11 @@ const assert = require('assert'); ...@@ -2,6 +2,11 @@ const assert = require('assert');
const sinon = require('sinon'); const sinon = require('sinon');
const vscode = require('vscode'); const vscode = require('vscode');
const workspaceService = require('../../../src/services/workspace_service'); const workspaceService = require('../../../src/services/workspace_service');
const {
createAndOpenFile,
closeAndDeleteFile,
simulateQuickPickChoice,
} = require('../test_infrastructure/helpers');
describe('workspace_service', () => { describe('workspace_service', () => {
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
...@@ -48,9 +53,7 @@ describe('workspace_service', () => { ...@@ -48,9 +53,7 @@ describe('workspace_service', () => {
it('getCurrentWorkspaceFolderOrSelectOne lets user select a workspace', async () => { it('getCurrentWorkspaceFolderOrSelectOne lets user select a workspace', async () => {
// simulating user selecting second option // simulating user selecting second option
sandbox.stub(vscode.window, 'showQuickPick').callsFake(async options => { simulateQuickPickChoice(sandbox, 1);
return options[1];
});
const result = await workspaceService.getCurrentWorkspaceFolderOrSelectOne(); const result = await workspaceService.getCurrentWorkspaceFolderOrSelectOne();
assert.strictEqual(result, '/ws2'); assert.strictEqual(result, '/ws2');
}); });
...@@ -59,16 +62,11 @@ describe('workspace_service', () => { ...@@ -59,16 +62,11 @@ describe('workspace_service', () => {
let testFileUri; let testFileUri;
beforeEach(async () => { beforeEach(async () => {
testFileUri = vscode.Uri.parse(`${originalWorkspace.uri.fsPath}/newfile.js`); testFileUri = vscode.Uri.parse(`${originalWorkspace.uri.fsPath}/newfile.js`);
const createFileEdit = new vscode.WorkspaceEdit(); await createAndOpenFile(testFileUri);
createFileEdit.createFile(testFileUri);
await vscode.workspace.applyEdit(createFileEdit);
await vscode.window.showTextDocument(testFileUri);
}); });
afterEach(async () => { afterEach(async () => {
const edit = new vscode.WorkspaceEdit(); await closeAndDeleteFile(testFileUri);
edit.deleteFile(testFileUri);
await vscode.workspace.applyEdit(edit);
}); });
it('getCurrentWorkspaceFolder returns workspace folder', async () => { it('getCurrentWorkspaceFolder returns workspace folder', async () => {
......
module.exports = {
GITLAB_HOST: 'test.gitlab.com',
};
export const GITLAB_HOST = 'test.gitlab.com';
export const REMOTE = {
NAME: 'origin',
URL: 'git@test.gitlab.com:gitlab-org/gitlab.git',
};
const vscode = require('vscode');
const createAndOpenFile = async testFileUri => {
const createFileEdit = new vscode.WorkspaceEdit();
createFileEdit.createFile(testFileUri);
await vscode.workspace.applyEdit(createFileEdit);
await vscode.window.showTextDocument(testFileUri);
};
const closeAndDeleteFile = async testFileUri => {
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
const edit = new vscode.WorkspaceEdit();
edit.deleteFile(testFileUri);
await vscode.workspace.applyEdit(edit);
};
const simulateQuickPickChoice = (sandbox, nthItem) => {
sandbox.stub(vscode.window, 'showQuickPick').callsFake(async options => {
return options[nthItem];
});
};
module.exports = { createAndOpenFile, closeAndDeleteFile, simulateQuickPickChoice };
const { setupServer } = require('msw/node'); const { setupServer } = require('msw/node');
const { rest } = require('msw'); const { rest, graphql } = require('msw');
const { GITLAB_HOST } = require('./constants'); const { GITLAB_HOST } = require('./constants');
const projectResponse = require('../fixtures/project.json'); const projectResponse = require('../fixtures/rest/project.json');
const versionResponse = require('../fixtures/version.json'); const versionResponse = require('../fixtures/rest/version.json');
const openIssueResponse = require('../fixtures/open_issue.json'); const openIssueResponse = require('../fixtures/rest/open_issue.json');
const openMergeRequestResponse = require('../fixtures/open_mr.json'); const openMergeRequestResponse = require('../fixtures/rest/open_mr.json');
const pipelinesResponse = require('../fixtures/pipelines.json'); const pipelinesResponse = require('../fixtures/rest/pipelines.json');
const pipelineResponse = require('../fixtures/pipeline.json'); const pipelineResponse = require('../fixtures/rest/pipeline.json');
const snippetsResponse = require('../fixtures/graphql/snippets.json');
const instancePrefix = `https://${GITLAB_HOST}/api/v4`; const instancePrefix = `https://${GITLAB_HOST}/api/v4`;
const createEndpoint = (path, response) => const createJsonEndpoint = (path, response) =>
rest.get(`${instancePrefix}${path}`, (req, res, ctx) => { rest.get(`${instancePrefix}${path}`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(response)); return res(ctx.status(200), ctx.json(response));
}); });
const createTextEndpoint = (path, response) =>
rest.get(`${instancePrefix}${path}`, (req, res, ctx) => {
return res(ctx.status(200), ctx.text(response));
});
const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => res(ctx.status(404))); const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => res(ctx.status(404)));
module.exports = () => { module.exports = () => {
const server = setupServer( const server = setupServer(
createEndpoint('/projects/gitlab-org%2Fgitlab', projectResponse), createJsonEndpoint('/projects/gitlab-org%2Fgitlab', projectResponse),
createEndpoint('/version', versionResponse), createJsonEndpoint('/version', versionResponse),
createEndpoint('/projects/278964/merge_requests?scope=assigned_to_me&state=opened', [ createJsonEndpoint('/projects/278964/merge_requests?scope=assigned_to_me&state=opened', [
openMergeRequestResponse, openMergeRequestResponse,
]), ]),
createEndpoint('/projects/278964/issues?scope=assigned_to_me&state=opened', [ createJsonEndpoint('/projects/278964/issues?scope=assigned_to_me&state=opened', [
openIssueResponse, openIssueResponse,
]), ]),
createEndpoint('/projects/278964/pipelines?ref=master', pipelinesResponse), createJsonEndpoint('/projects/278964/pipelines?ref=master', pipelinesResponse),
createEndpoint('/projects/278964/pipelines/47', pipelineResponse), createJsonEndpoint('/projects/278964/pipelines/47', pipelineResponse),
createTextEndpoint('/projects/278964/snippets/111/files/master/test.js/raw', 'snippet content'),
createTextEndpoint(
'/projects/278964/snippets/222/files/master/test2.js/raw',
'second blob content',
),
graphql.query('GetSnippets', (req, res, ctx) => {
if (req.variables.projectPath === 'gitlab-org/gitlab') return res(ctx.data(snippetsResponse));
return res(ctx.data({ project: null }));
}),
notFoundByDefault, notFoundByDefault,
); );
server.listen(); server.listen();
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"lib": ["es6", "dom"], "lib": ["es6", "dom"],
"allowJs": true, "allowJs": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
}, },
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册