diff --git a/package-lock.json b/package-lock.json index 30fc93a9b..d82bcf4f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@redhat-developer/vscode-redhat-telemetry": "^0.5.2", "clsx": "^1.1.1", "fs-extra": "^10.0.0", - "git-url-parse": "^13.1.0", "globby": "^10.0.1", "got": "^11.8.6", "hasha": "^5.2.2", @@ -88,6 +87,7 @@ "eslint-plugin-prettier": "^4.0.0", "express": "^4.17.2", "file-loader": "^6.2.0", + "git-up": "^7.0.0", "glob": "^7.2.0", "istanbul": "^0.4.5", "leasot": "^12.0.0", @@ -11132,19 +11132,12 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", + "dev": true, "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^8.1.0" } }, - "node_modules/git-url-parse": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", - "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", - "dependencies": { - "git-up": "^7.0.0" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -12360,6 +12353,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dev": true, "dependencies": { "protocols": "^2.0.1" } @@ -16355,6 +16349,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dev": true, "dependencies": { "protocols": "^2.0.0" } @@ -16381,6 +16376,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dev": true, "dependencies": { "parse-path": "^7.0.0" } @@ -16960,7 +16956,8 @@ "node_modules/protocols": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -28570,19 +28567,12 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", + "dev": true, "requires": { "is-ssh": "^1.4.0", "parse-url": "^8.1.0" } }, - "git-url-parse": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", - "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", - "requires": { - "git-up": "^7.0.0" - } - }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -29455,6 +29445,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dev": true, "requires": { "protocols": "^2.0.1" } @@ -32526,6 +32517,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dev": true, "requires": { "protocols": "^2.0.0" } @@ -32551,6 +32543,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dev": true, "requires": { "parse-path": "^7.0.0" } @@ -32985,7 +32978,8 @@ "protocols": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true }, "proxy-addr": { "version": "2.0.7", diff --git a/package.json b/package.json index 24b474761..57bcc9f42 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,6 @@ "@redhat-developer/vscode-redhat-telemetry": "^0.5.2", "clsx": "^1.1.1", "fs-extra": "^10.0.0", - "git-url-parse": "^13.1.0", "globby": "^10.0.1", "got": "^11.8.6", "hasha": "^5.2.2", @@ -152,6 +151,7 @@ "eslint-plugin-prettier": "^4.0.0", "express": "^4.17.2", "file-loader": "^6.2.0", + "git-up": "^7.0.0", "glob": "^7.2.0", "istanbul": "^0.4.5", "leasot": "^12.0.0", diff --git a/src/webview/git-import/app/gitImport.tsx b/src/webview/git-import/app/gitImport.tsx index 2440eb61c..56f2634e2 100644 --- a/src/webview/git-import/app/gitImport.tsx +++ b/src/webview/git-import/app/gitImport.tsx @@ -16,6 +16,7 @@ import { VSCodeMessage } from './vsCodeMessage'; import { CardItem } from './cardItem'; import { ComponentTypeDescription } from '../../../odo/componentType'; import { LoadScreen } from './loading'; +import { gitUrlParse } from '../gitParse'; import './gitImport.scss'; import { Uri } from 'vscode'; @@ -135,7 +136,7 @@ export class GitImport extends React.Component { + const parsedURL = gitUrlParse(value); + return gitUrlParse.stringify(parsedURL, ''); + } + analyze = (): void => { this.setState({ applicationName: undefined, @@ -251,7 +261,7 @@ export class GitImport extends React.Component { const selctedFolder: vscode.Uri = event.folder; const cloneProcess: CloneProcess = await clone(event.gitURL, selctedFolder.fsPath); if (!cloneProcess.status && cloneProcess.error) { - showError(event, event.folder.fsPath, cloneProcess.error); + showError(event.folder.fsPath, cloneProcess.error); return null; + } else { + panel?.webview.postMessage({ + action: 'cloneCompleted' + }); } - panel?.webview.postMessage({ - action: 'cloneCompleted' - }); } default: break; @@ -249,7 +250,7 @@ async function parseGitURL(event: any) { name: event.projectName + '-comp', error: compDescriptions.length > 0 ? false : true, isDevFile: isDevFile, - helpText: compDescriptions.length > 0 ? 'The git repo is valid.' : 'Issue on Parsing Git URL/devfile', + helpText: compDescriptions.length > 0 ? 'The git repo URL is valid.' : 'Issue on Parsing Git URL/devfile', compDescription: compDescriptions, parser: event.parser }); @@ -273,7 +274,7 @@ function validateGitURL(event: any) { }); } else { try { - const parse = GitUrlParse(event.param); + const parse = gitUrlParse(event.param); const isGitRepo = isGitURL(parse.host); if (!isGitRepo) { throw 'Invalid Git URL'; @@ -282,7 +283,7 @@ function validateGitURL(event: any) { panel?.webview.postMessage({ action: event.action, error: false, - helpText: 'The git repo is valid.', + helpText: 'The git repo URL is valid.', parser: parse, gitURL: event.param }); @@ -319,8 +320,11 @@ function clone(url: string, location: string): Promise { const gitExtension = vscode.extensions.getExtension('vscode.git').exports; const git = gitExtension.getAPI(1).git.path; // run 'git clone url location' as external process and return location - return new Promise((resolve, reject) => (childProcess = cp.exec(`${git} clone ${url} ${location}`, (error: cp.ExecException) => error ? - reject({ status: false, error: error.message }) : resolve({ status: true, error: undefined })))); + return new Promise((resolve, reject) => (childProcess = cp.exec(`${git} clone ${url} ${location}`, + (error: cp.ExecException) => { + error ? resolve({ status: false, error: error.message }) : resolve({ status: true, error: undefined }); + } + ))); } function validateComponentName(event: any) { @@ -352,17 +356,15 @@ function validateDevFilePath(event: any) { }); } -function showError(event: any, location: string, message: string): void { +function showError(location: string, message: string): void { + const permissonDeniedIndex = message.toLowerCase().indexOf('permission denied'); + const errorMsg = permissonDeniedIndex !== -1 ? message.substring(permissonDeniedIndex) : 'Error occurred while cloning the repository. Please try again.'; panel?.webview.postMessage({ - action: event.action, - status: false + action: 'cloneError', + error: errorMsg }); if (!forceCancel) { - if (message.indexOf('already exists') !== -1) { - vscode.window.showErrorMessage(`Folder already exists on the selected ${location.substring(0, location.lastIndexOf('\\'))}`); - } else { - vscode.window.showErrorMessage('Error occurred while cloning the repository. Please try again.'); - } + vscode.window.showErrorMessage(errorMsg); } } diff --git a/src/webview/git-import/gitParse.ts b/src/webview/git-import/gitParse.ts new file mode 100644 index 000000000..b700acc52 --- /dev/null +++ b/src/webview/git-import/gitParse.ts @@ -0,0 +1,316 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +'use strict'; + +const gitUp = require('git-up'); + +/** + * gitUrlParse + * Parses a Git url. + * + * @name gitUrlParse + * @function + * @param {String} url The Git url to parse. + * @return {GitUrl} The `GitUrl` object containing: + * + * - `protocols` (Array): An array with the url protocols (usually it has one element). + * - `port` (null|Number): The domain port. + * - `resource` (String): The url domain (including subdomains). + * - `user` (String): The authentication user (usually for ssh urls). + * - `pathname` (String): The url pathname. + * - `hash` (String): The url hash. + * - `search` (String): The url querystring value. + * - `href` (String): The input url. + * - `protocol` (String): The git url protocol. + * - `token` (String): The oauth token (could appear in the https urls). + * - `source` (String): The Git provider (e.g. `'github.com'`). + * - `owner` (String): The repository owner. + * - `name` (String): The repository name. + * - `ref` (String): The repository ref (e.g., 'master' or 'dev'). + * - `filepath` (String): A filepath relative to the repository root. + * - `filepathtype` (String): The type of filepath in the url ('blob' or 'tree'). + * - `full_name` (String): The owner and name values in the `owner/name` format. + * - `toString` (Function): A function to stringify the parsed url into another url type. + * - `organization` (String): The organization the owner belongs to. This is CloudForge specific. + * - `git_suffix` (Boolean): Whether to add the `.git` suffix or not. + * + */ +export function gitUrlParse(url) { + + if (typeof url !== 'string') { + throw new Error('The url must be a string.'); + } + + const shorthandRe = /^([a-z\d-]{1,39})\/([-\.\w]{1,100})$/i + + if (shorthandRe.test(url)) { + url = `https://github.com/${url}` + } + + let urlInfo = gitUp(url) + , sourceParts = urlInfo.resource.split('.') + , splits = null + ; + + urlInfo.toString = function (type) { + return gitUrlParse.stringify(this, type); + }; + + urlInfo.source = sourceParts.length > 2 + ? sourceParts.slice(1 - sourceParts.length).join('.') + : urlInfo.source = urlInfo.resource + ; + + // Note: Some hosting services (e.g. Visual Studio Team Services) allow whitespace characters + // in the repository and owner names so we decode the URL pieces to get the correct result + urlInfo.git_suffix = /\.git$/.test(urlInfo.pathname); + urlInfo.name = decodeURIComponent((urlInfo.pathname || urlInfo.href).replace(/(^\/)|(\/$)/g, '').replace(/\.git$/, '')); + urlInfo.owner = decodeURIComponent(urlInfo.user); + + switch (urlInfo.source) { + case 'git.cloudforge.com': + urlInfo.owner = urlInfo.user; + urlInfo.organization = sourceParts[0]; + urlInfo.source = 'cloudforge.com'; + break; + case 'visualstudio.com': + // Handle VSTS SSH URLs + if (urlInfo.resource === 'vs-ssh.visualstudio.com') { + splits = urlInfo.name.split('/'); + if (splits.length === 4) { + urlInfo.organization = splits[1]; + urlInfo.owner = splits[2]; + urlInfo.name = splits[3]; + urlInfo.full_name = splits[2] + '/' + splits[3]; + } + break; + } else { + splits = urlInfo.name.split('/'); + if (splits.length === 2) { + urlInfo.owner = splits[1]; + urlInfo.name = splits[1]; + urlInfo.full_name = '_git/' + urlInfo.name; + } else if (splits.length === 3) { + urlInfo.name = splits[2]; + if (splits[0] === 'DefaultCollection') { + urlInfo.owner = splits[2]; + urlInfo.organization = splits[0]; + urlInfo.full_name = urlInfo.organization + '/_git/' + urlInfo.name; + } else { + urlInfo.owner = splits[0]; + urlInfo.full_name = urlInfo.owner + '/_git/' + urlInfo.name; + } + } else if (splits.length === 4) { + urlInfo.organization = splits[0]; + urlInfo.owner = splits[1]; + urlInfo.name = splits[3]; + urlInfo.full_name = urlInfo.organization + '/' + urlInfo.owner + '/_git/' + urlInfo.name; + } + break; + } + + // Azure DevOps (formerly Visual Studio Team Services) + case 'dev.azure.com': + case 'azure.com': + if (urlInfo.resource === 'ssh.dev.azure.com') { + splits = urlInfo.name.split('/'); + if (splits.length === 4) { + urlInfo.organization = splits[1]; + urlInfo.owner = splits[2]; + urlInfo.name = splits[3]; + } + break; + } else { + splits = urlInfo.name.split('/'); + if (splits.length === 5) { + urlInfo.organization = splits[0]; + urlInfo.owner = splits[1]; + urlInfo.name = splits[4]; + urlInfo.full_name = '_git/' + urlInfo.name; + } else if (splits.length === 3) { + urlInfo.name = splits[2]; + if (splits[0] === 'DefaultCollection') { + urlInfo.owner = splits[2]; + urlInfo.organization = splits[0]; + urlInfo.full_name = urlInfo.organization + '/_git/' + urlInfo.name; + } else { + urlInfo.owner = splits[0]; + urlInfo.full_name = urlInfo.owner + '/_git/' + urlInfo.name; + } + } else if (splits.length === 4) { + urlInfo.organization = splits[0]; + urlInfo.owner = splits[1]; + urlInfo.name = splits[3]; + urlInfo.full_name = urlInfo.organization + '/' + urlInfo.owner + '/_git/' + urlInfo.name; + } + if(urlInfo.query && urlInfo.query['path']) { + urlInfo.filepath = urlInfo.query['path'].replace(/^\/+/g, ''); // Strip leading slash (/) + } + if(urlInfo.query && urlInfo.query['version']) { // version=GB + urlInfo.ref = urlInfo.query['version'].replace(/^GB/, ''); // remove GB + } + break; + } + default: + splits = urlInfo.name.split('/'); + let nameIndex = splits.length - 1; + if (splits.length >= 2) { + const dashIndex = splits.indexOf('-', 2) + const blobIndex = splits.indexOf('blob', 2); + const treeIndex = splits.indexOf('tree', 2); + const commitIndex = splits.indexOf('commit', 2); + const issuesIndex = splits.indexOf("issues", 2); + const pullIndex = splits.indexOf("pull", 2); + const srcIndex = splits.indexOf('src', 2); + const rawIndex = splits.indexOf('raw', 2); + const editIndex = splits.indexOf('edit', 2); + nameIndex = dashIndex > 0 ? dashIndex - 1 + : blobIndex > 0 ? blobIndex - 1 + : treeIndex > 0 ? treeIndex - 1 + : commitIndex > 0 ? commitIndex - 1 + : issuesIndex > 0 ? issuesIndex - 1 + : pullIndex > 0 ? pullIndex - 1 + : srcIndex > 0 ? srcIndex - 1 + : rawIndex > 0 ? rawIndex - 1 + : editIndex > 0 ? editIndex - 1 + : nameIndex; + + urlInfo.owner = splits.slice(0, nameIndex).join('/'); + urlInfo.name = splits[nameIndex]; + if (commitIndex !== -1) { + urlInfo.commit = splits[nameIndex + 2] + } + } + + urlInfo.ref = ''; + urlInfo.filepathtype = ''; + urlInfo.filepath = ''; + const offsetNameIndex = splits.length > nameIndex && splits[nameIndex+1] === '-' ? nameIndex + 1 : nameIndex; + + if ((splits.length > offsetNameIndex + 2) && (['raw', 'src', 'blob', 'tree', 'edit'].indexOf(splits[offsetNameIndex + 1]) >= 0)) { + urlInfo.filepathtype = splits[offsetNameIndex + 1]; + urlInfo.ref = splits[offsetNameIndex + 2]; + if (splits.length > offsetNameIndex + 3) { + urlInfo.filepath = splits.slice(offsetNameIndex + 3).join('/'); + } + } + urlInfo.organization = urlInfo.owner; + break; + } + + if (!urlInfo.full_name) { + urlInfo.full_name = urlInfo.owner; + if (urlInfo.name) { + urlInfo.full_name && (urlInfo.full_name += '/'); + urlInfo.full_name += urlInfo.name; + } + } + // Bitbucket Server + if(urlInfo.owner.startsWith('scm/')) { + urlInfo.source = 'bitbucket-server'; + urlInfo.owner = urlInfo.owner.replace('scm/',''); + urlInfo.organization = urlInfo.owner; + urlInfo.full_name = `${urlInfo.owner}/${urlInfo.name}` + } + + const bitbucket = /(projects|users)\/(.*?)\/repos\/(.*?)((\/.*$)|$)/ + const matches = bitbucket.exec(urlInfo.pathname) + if(matches != null) { + urlInfo.source = 'bitbucket-server'; + if (matches[1] === 'users') { + urlInfo.owner = '~' + matches[2]; + } else { + urlInfo.owner = matches[2]; + } + + urlInfo.organization = urlInfo.owner; + urlInfo.name = matches[3]; + + splits = matches[4].split('/'); + if(splits.length > 1) { + if(['raw','browse'].indexOf(splits[1]) >= 0) { + urlInfo.filepathtype = splits[1]; + if (splits.length > 2) { + urlInfo.filepath = splits.slice(2).join('/'); + } + } else if(splits[1] === 'commits' && splits.length > 2) { + urlInfo.commit = splits[2]; + } + } + urlInfo.full_name = `${urlInfo.owner}/${urlInfo.name}` + + if(urlInfo.query.at) { + urlInfo.ref = urlInfo.query.at; + } else { + urlInfo.ref = ''; + } + } + return urlInfo; +} + +/** + * stringify + * Stringifies a `GitUrl` object. + * + * @name stringify + * @function + * @param {GitUrl} obj The parsed Git url object. + * @param {String} type The type of the stringified url (default `obj.protocol`). + * @return {String} The stringified url. + */ +gitUrlParse.stringify = function (obj, type) { + type = type || ((obj.protocols && obj.protocols.length) ? obj.protocols.join('+') : obj.protocol); + const port = obj.port ? `:${obj.port}` : ''; + const user = obj.user || 'git'; + const maybeGitSuffix = obj.git_suffix ? '.git' : '' + switch (type) { + case 'ssh': + if (port) + return `ssh://${user}@${obj.resource}${port}/${obj.full_name}${maybeGitSuffix}`; + else + return `${user}@${obj.resource}:${obj.full_name}${maybeGitSuffix}`; + case 'git+ssh': + case 'ssh+git': + case 'ftp': + case 'ftps': + return `${type}://${user}@${obj.resource}${port}/${obj.full_name}${maybeGitSuffix}`; + case 'http': + case 'https': + const auth = obj.token + ? buildToken(obj) : obj.user && (obj.protocols.includes('http') || obj.protocols.includes('https')) + ? `${obj.user}@` : ''; + return `${type}://${auth}${obj.resource}${port}/${buildPath(obj)}${maybeGitSuffix}`; + default: + return obj.git_suffix ? obj.href : `${obj.protocol}://${obj.source}/${obj.full_name}`; + } +}; + +/*! + * buildToken + * Builds OAuth token prefix (helper function) + * + * @name buildToken + * @function + * @param {GitUrl} obj The parsed Git url object. + * @return {String} token prefix + */ +function buildToken(obj) { + switch (obj.source) { + case 'bitbucket.org': + return `x-token-auth:${obj.token}@`; + default: + return `${obj.token}@` + } +} + +function buildPath(obj) { + switch(obj.source) { + case 'bitbucket-server': + return `scm/${obj.full_name}`; + default: + return `${obj.full_name}`; + + } +}