diff --git a/plugins/workspace-plugin/__mocks__/@eclipse-che/plugin.ts b/plugins/workspace-plugin/__mocks__/@eclipse-che/plugin.ts new file mode 100644 index 000000000..ff552384e --- /dev/null +++ b/plugins/workspace-plugin/__mocks__/@eclipse-che/plugin.ts @@ -0,0 +1,13 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const chePlugin: any = {}; +module.exports = chePlugin; diff --git a/plugins/workspace-plugin/__mocks__/@theia/plugin.ts b/plugins/workspace-plugin/__mocks__/@theia/plugin.ts index 03d70d6da..2133ef59e 100644 --- a/plugins/workspace-plugin/__mocks__/@theia/plugin.ts +++ b/plugins/workspace-plugin/__mocks__/@theia/plugin.ts @@ -1,14 +1,14 @@ -/* - * Copyright (c) 2020 Red Hat, Inc. - * All rights reserved. This program and the accompanying materials are made +/********************************************************************** + * Copyright (c) 2020-2021 Red Hat, Inc. + * + * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ -/** - * Mock of @theia/plugin module - * @author Valerii Svydenko - */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const theiaPlugin: any = {}; +theiaPlugin.window = {}; +module.exports = theiaPlugin; diff --git a/plugins/workspace-plugin/src/ephemeral-workspace-checker.ts b/plugins/workspace-plugin/src/ephemeral-workspace-checker.ts index 580586a87..1ce8ee96b 100644 --- a/plugins/workspace-plugin/src/ephemeral-workspace-checker.ts +++ b/plugins/workspace-plugin/src/ephemeral-workspace-checker.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2018-2020 Red Hat, Inc. + * Copyright (c) 2018-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -11,21 +11,18 @@ import * as che from '@eclipse-che/plugin'; import * as theia from '@theia/plugin'; -import { che as cheApi } from '@eclipse-che/api'; - /** * Make checks on workspace ephemeral configuration and shows dedicated information to user. */ export class EphemeralWorkspaceChecker { constructor() {} - public check(): void { - che.workspace.getCurrentWorkspace().then((workspace: cheApi.workspace.Workspace) => { - const isEphemeralWorkspace = this.isEphemeralWorkspace(workspace); - if (isEphemeralWorkspace) { - this.displayEphemeralWarning(); - } - }); + public async check(): Promise { + const devfile = await che.devfile.get(); + const isEphemeralWorkspace = devfile.metadata?.attributes && devfile.metadata.attributes.persistVolumes === 'false'; + if (isEphemeralWorkspace) { + this.displayEphemeralWarning(); + } } /** @@ -39,19 +36,4 @@ export class EphemeralWorkspaceChecker { item.color = '#fcc13d'; item.show(); } - - /** - * Returns, whether provided workspace is ephmeral or not. - */ - private isEphemeralWorkspace(workspace: cheApi.workspace.Workspace): boolean { - if (workspace.devfile) { - const workspaceAttributes = workspace.devfile.attributes; - if (workspaceAttributes) { - return workspaceAttributes.persistVolumes === 'false'; - } - return false; - } else { - return workspace.config!.attributes!.persistVolumes === 'false' || false; - } - } } diff --git a/plugins/workspace-plugin/src/git.ts b/plugins/workspace-plugin/src/git.ts index cc33c9e7f..36f56c246 100644 --- a/plugins/workspace-plugin/src/git.ts +++ b/plugins/workspace-plugin/src/git.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2019-2020 Red Hat, Inc. + * Copyright (c) 2019-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -45,7 +45,7 @@ export async function getRemoteURL(remote: string, projectPath: string): Promise export async function sparseCheckout( projectPath: string, repositoryUri: string, - sparseCheckoutDirectory: string, + sparseCheckoutDirectories: string[], commitReference: string ): Promise { await initRepository(projectPath); @@ -53,8 +53,8 @@ export async function sparseCheckout( await setConfig(projectPath, 'core.sparsecheckout', 'true'); // Write sparse checkout directory const gitInfoFolderPath = path.join(projectPath, '.git/info/'); - fs.ensureDirSync(gitInfoFolderPath); - fs.writeFileSync(path.join(gitInfoFolderPath, 'sparse-checkout'), sparseCheckoutDirectory); + await fs.ensureDir(gitInfoFolderPath); + await fs.writeFile(path.join(gitInfoFolderPath, 'sparse-checkout'), sparseCheckoutDirectories.join('\n'), 'utf-8'); // Add remote, pull changes and create the selected directory content await execGit(projectPath, 'remote', 'add', '-f', 'origin', repositoryUri); await execGit(projectPath, 'pull', 'origin', commitReference); diff --git a/plugins/workspace-plugin/src/projects.ts b/plugins/workspace-plugin/src/projects.ts index 4a6e82cbc..77b762264 100644 --- a/plugins/workspace-plugin/src/projects.ts +++ b/plugins/workspace-plugin/src/projects.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2019-2020 Red Hat, Inc. + * Copyright (c) 2019-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ -import { che as cheApi } from '@eclipse-che/api'; +import * as che from '@eclipse-che/plugin'; // devfile projects handling @@ -22,11 +22,11 @@ import { che as cheApi } from '@eclipse-che/api'; * @param projectGitRemoteBranch git branch of the project */ export function updateOrCreateGitProjectInDevfile( - projects: cheApi.workspace.devfile.Project[], + projects: che.devfile.DevfileProject[] | undefined, projectPath: string | undefined, projectGitLocation: string, projectGitRemoteBranch: string -): cheApi.workspace.devfile.Project[] { +): che.devfile.DevfileProject[] { if (!projects) { projects = []; } @@ -42,10 +42,13 @@ export function updateOrCreateGitProjectInDevfile( // create a new one projects.push({ name: projectName ? projectName : 'new-project', - source: { - location: projectGitLocation, - type: 'git', - branch: projectGitRemoteBranch, + git: { + remotes: { + origin: projectGitLocation, + }, + checkoutFrom: { + revision: projectGitRemoteBranch, + }, }, clonePath: projectPath, }); @@ -53,18 +56,25 @@ export function updateOrCreateGitProjectInDevfile( } filteredProject.forEach(project => { - if (!project.source) { - project.source = { - location: projectGitLocation, - type: 'git', - branch: projectGitRemoteBranch, + if (!project.git) { + project.git = { + remotes: { + origin: projectGitLocation, + }, + checkoutFrom: { + revision: projectGitRemoteBranch, + }, + }; + } + const defaultRemote = project.git.checkoutFrom?.remote || Object.keys(project.git.remotes)[0]; + project.git.remotes[defaultRemote] = projectGitLocation; + if (!project.git.checkoutFrom) { + project.git.checkoutFrom = { + revision: projectGitRemoteBranch, }; + } else { + project.git.checkoutFrom.revision = projectGitRemoteBranch; } - project.source.location = projectGitLocation; - project.source.branch = projectGitRemoteBranch; - delete project.source.startPoint; - delete project.source.tag; - delete project.source.commitId; }); return projects; @@ -78,9 +88,9 @@ export function updateOrCreateGitProjectInDevfile( * @param projectPath relative path of the project to delete according to projets root directory */ export function deleteProjectFromDevfile( - projects: cheApi.workspace.devfile.Project[], + projects: che.devfile.DevfileProject[] | undefined, projectPath: string -): cheApi.workspace.devfile.Project[] { +): che.devfile.DevfileProject[] { if (!projects) { projects = []; } @@ -96,72 +106,3 @@ export function deleteProjectFromDevfile( return projects; } - -// workspace config projects handling - -export function updateOrCreateGitProjectInWorkspaceConfig( - projects: cheApi.workspace.ProjectConfig[], - projectPath: string, - projectGitLocation: string, - projectGitRemoteBranch: string -): cheApi.workspace.ProjectConfig[] { - const filteredProject = projects.filter(project => project.path === projectPath); - if (filteredProject.length === 0) { - const projectName = projectPath.split('/').pop(); - - // create a new one - projects.push({ - name: projectName ? projectName : 'new-project', - attributes: {}, - source: { - location: projectGitLocation, - type: 'git', - parameters: { - branch: projectGitRemoteBranch, - }, - }, - path: projectPath, - description: '', - mixins: [], - }); - return projects; - } - - filteredProject.forEach(project => { - if (!project.source) { - project.source = { - type: 'git', - location: projectGitLocation, - parameters: { - branch: projectGitRemoteBranch, - }, - }; - } - project.source.location = projectGitLocation; - if (!project.source.parameters) { - project.source.parameters = {}; - } - project.source.parameters['branch'] = projectGitRemoteBranch; - delete project.source.parameters['startPoint']; - delete project.source.parameters['tag']; - delete project.source.parameters['commitId']; - }); - - return projects; -} - -export function deleteProjectFromWorkspaceConfig( - projects: cheApi.workspace.ProjectConfig[], - projectPath: string -): cheApi.workspace.ProjectConfig[] { - for (let i = 0; i < projects.length; i++) { - const project = projects[i]; - const currentProjectPath = project.path ? project.path : project.name; - if (currentProjectPath === projectPath) { - projects.splice(i, 1); - break; - } - } - - return projects; -} diff --git a/plugins/workspace-plugin/src/theia-commands.ts b/plugins/workspace-plugin/src/theia-commands.ts index 4256430d0..65fda9722 100644 --- a/plugins/workspace-plugin/src/theia-commands.ts +++ b/plugins/workspace-plugin/src/theia-commands.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2018-2020 Red Hat, Inc. + * Copyright (c) 2018-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,6 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ +import * as che from '@eclipse-che/plugin'; import * as fileuri from './file-uri'; import * as fs from 'fs-extra'; import * as git from './git'; @@ -17,7 +18,6 @@ import * as ssh from './ssh'; import * as theia from '@theia/plugin'; import { TaskScope } from '@eclipse-che/plugin'; -import { che as cheApi } from '@eclipse-che/api'; import { execute } from './exec'; import { getCertificate } from './ca-cert'; @@ -31,103 +31,74 @@ export enum ActionId { RUN_COMMAND = 'runCommand', } -function isDevfileProjectConfig( - project: cheApi.workspace.ProjectConfig | cheApi.workspace.devfile.Project -): project is cheApi.workspace.devfile.Project { - return ( - !!project.name && - !!project.source && - !!project.source.type && - !!project.source.location && - !(project.source as { [index: string]: string })['parameters'] - ); -} - export interface TheiaImportCommand { /** @returns the path to the imported project */ execute(): Promise; } export function buildProjectImportCommand( - project: cheApi.workspace.ProjectConfig | cheApi.workspace.devfile.Project, + project: che.devfile.DevfileProject, projectsRoot: string ): TheiaImportCommand | undefined { - if (!project.source) { + if (project.git || project.github) { + return new TheiaGitCloneCommand(project, projectsRoot); + } else if (project.zip) { + return new TheiaImportZipCommand(project, projectsRoot); + } else { + const message = `Project ${JSON.stringify(project, undefined, 2)} is not supported.`; + theia.window.showWarningMessage(message); + console.warn(message); return; } - - switch (project.source.type) { - case 'git': - case 'github': - return new TheiaGitCloneCommand(project, projectsRoot); - case 'zip': - return new TheiaImportZipCommand(project, projectsRoot); - default: - const message = `Project type "${project.source.type}" is not supported.`; - theia.window.showWarningMessage(message); - console.warn(message); - return; - } } let output: theia.OutputChannel; export class TheiaGitCloneCommand implements TheiaImportCommand { private projectName: string | undefined; - private locationURI: string; private projectPath: string; - private checkoutBranch?: string | undefined; - private checkoutTag?: string | undefined; - private checkoutStartPoint?: string | undefined; - private checkoutCommitId?: string | undefined; - private sparseCheckoutDir: string | undefined; + private revision: string | undefined; + private sparseCheckoutDirs: string[]; private projectsRoot: string; - - constructor(project: cheApi.workspace.ProjectConfig | cheApi.workspace.devfile.Project, projectsRoot: string) { - if (isDevfileProjectConfig(project)) { - const source = project.source; - if (!source || !source.location) { - throw new Error('Source location is not defined for "' + this.projectName + '" project.'); - } - - this.projectName = project.name; - this.locationURI = source.location; - this.projectPath = project.clonePath - ? path.join(projectsRoot, project.clonePath) - : path.join(projectsRoot, project.name!); - this.checkoutBranch = source.branch; - this.checkoutStartPoint = source.startPoint; - this.checkoutTag = source.tag; - this.checkoutCommitId = source.commitId; - this.sparseCheckoutDir = source.sparseCheckoutDir; + private remotes: { [remoteName: string]: string }; + private defaultRemoteLocation: string; + private defaultRemoteName: string; + + init(devfileProjectInfo: che.devfile.DevfileProjectInfo): void { + this.remotes = devfileProjectInfo.remotes; + if (devfileProjectInfo.checkoutFrom) { + this.revision = devfileProjectInfo.checkoutFrom.revision; + } + if (devfileProjectInfo?.checkoutFrom?.remote) { + this.defaultRemoteName = devfileProjectInfo.checkoutFrom.remote; } else { - // legacy project config - if (!project.source || !project.source.parameters) { - throw new Error('Project with name "' + this.projectName + '" is not defined correctly.'); - } - const parameters = project.source.parameters; - - this.projectName = project.name; - this.locationURI = project.source.location!; - this.projectPath = projectsRoot + project.path; - this.checkoutBranch = parameters['branch']; - this.checkoutStartPoint = parameters['startPoint']; - this.checkoutTag = project.source.parameters['tag']; - this.checkoutCommitId = project.source.parameters['commitId']; - this.sparseCheckoutDir = project.source.parameters['keepDir']; + this.defaultRemoteName = Object.keys(this.remotes)[0]; } + this.defaultRemoteLocation = this.remotes[this.defaultRemoteName]; + } + constructor(project: che.devfile.DevfileProject, projectsRoot: string) { + if (project.git) { + this.init(project.git); + } else if (project.github) { + this.init(project.github); + } this.projectsRoot = projectsRoot; + this.projectPath = project.clonePath + ? path.join(projectsRoot, project.clonePath) + : path.join(projectsRoot, project.name); + + this.sparseCheckoutDirs = project.sparseCheckoutDirs || []; } clone(): PromiseLike { return theia.window.withProgress( { location: theia.ProgressLocation.Notification, - title: `Cloning ${this.locationURI} ...`, + title: `Cloning ${this.defaultRemoteLocation} ...`, }, (progress, token) => { - if (this.sparseCheckoutDir) { + if (this.sparseCheckoutDirs.length > 0) { return this.gitSparseCheckout(progress, token); } else { return this.gitClone(progress, token); @@ -137,7 +108,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { } async execute(): Promise { - if (!git.isSecureGitURI(this.locationURI)) { + if (!git.isSecureGitURI(this.defaultRemoteLocation)) { // clone using regular URI return this.clone(); } @@ -147,7 +118,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { while (true) { // test secure login try { - await git.testSecureLogin(this.locationURI); + await git.testSecureLogin(this.defaultRemoteLocation); // exit the loop when successfull login break; } catch (error) { @@ -171,12 +142,12 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { const ADD_KEY_TO_GITHUB = 'Add Key To GitHub'; const CONFIGURE_SSH = 'Configure SSH'; - let message = `Failure to clone git project ${this.locationURI}`; + let message = `Failure to clone git project ${this.defaultRemoteLocation}`; if (latestError) { message += ` ${latestError}`; } - const isSecureGitHubURI = git.isSecureGitHubURI(this.locationURI); + const isSecureGitHubURI = git.isSecureGitHubURI(this.defaultRemoteLocation); const buttons = isSecureGitHubURI ? [RETRY, ADD_KEY_TO_GITHUB, CONFIGURE_SSH] : [RETRY, CONFIGURE_SSH]; const action = await theia.window.showWarningMessage(message, ...buttons); if (action === RETRY) { @@ -195,7 +166,7 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { const SKIP = 'Skip'; const TRY_AGAIN = 'Try Again'; const tryAgain = await theia.window.showWarningMessage( - `Cloning of ${this.locationURI} will be skipped`, + `Cloning of ${this.defaultRemoteLocation} will be skipped`, SKIP, TRY_AGAIN ); @@ -218,35 +189,39 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { progress: theia.Progress<{ message?: string; increment?: number }>, token: theia.CancellationToken ): Promise { - const args: string[] = ['clone', this.locationURI, this.projectPath]; - if (this.checkoutBranch) { - args.push('--branch'); - args.push(this.checkoutBranch); - } + const args: string[] = ['clone', this.defaultRemoteLocation, this.projectPath]; try { await git.execGit(this.projectsRoot, ...args); + + // Add extra remotes if defined + if (Object.keys(this.remotes).length > 1) { + await Promise.all( + Object.entries(this.remotes).map(async ([remoteName, remoteValue]) => { + if (this.defaultRemoteName !== remoteName) { + const remoteArgs = ['remote', 'add', remoteName, remoteValue]; + await git.execGit(this.projectsRoot, ...remoteArgs); + } + }) + ); + } + // Figure out what to reset to. // The priority order is startPoint > tag > commitId - const treeish = this.checkoutStartPoint - ? this.checkoutStartPoint - : this.checkoutTag - ? this.checkoutTag - : this.checkoutCommitId; + const messageStart = `Project ${this.defaultRemoteLocation} cloned to ${this.projectPath} using default branch`; - const branch = this.checkoutBranch ? this.checkoutBranch : 'default branch'; - const messageStart = `Project ${this.locationURI} cloned to ${this.projectPath} and checked out ${branch}`; - - if (treeish) { - git.execGit(this.projectPath, 'reset', '--hard', treeish).then( + if (this.revision) { + git.execGit(this.projectPath, 'checkout', this.revision).then( _ => { - theia.window.showInformationMessage(`${messageStart} which has been reset to ${treeish}.`); + theia.window.showInformationMessage(`${messageStart} which has been reset to ${this.revision}.`); }, e => { - theia.window.showErrorMessage(`${messageStart} but resetting to ${treeish} failed with ${e.message}.`); + theia.window.showErrorMessage( + `${messageStart} but resetting to ${this.revision} failed with ${e.message}.` + ); console.log( - `Couldn't reset to ${treeish} of ${this.projectPath} cloned from ${this.locationURI} and checked out to ${branch}.`, + `Couldn't reset to ${this.revision} of ${this.projectPath} cloned from ${this.defaultRemoteLocation} and checked from default branch.`, e ); } @@ -256,8 +231,8 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { } return this.projectPath; } catch (e) { - theia.window.showErrorMessage(`Couldn't clone ${this.locationURI}: ${e.message}`); - console.log(`Couldn't clone ${this.locationURI}`, e); + theia.window.showErrorMessage(`Couldn't clone ${this.defaultRemoteLocation}: ${e.message}`); + console.log(`Couldn't clone ${this.defaultRemoteLocation}`, e); throw new Error(e); } } @@ -267,25 +242,22 @@ export class TheiaGitCloneCommand implements TheiaImportCommand { progress: theia.Progress<{ message?: string; increment?: number }>, token: theia.CancellationToken ): Promise { - if (!this.sparseCheckoutDir) { + if (this.sparseCheckoutDirs.length === 0) { throw new Error('Parameter "sparseCheckoutDir" is not set for "' + this.projectName + '" project.'); } - const commitReference = this.checkoutStartPoint - ? this.checkoutStartPoint - : this.checkoutTag - ? this.checkoutTag - : this.checkoutCommitId - ? this.checkoutCommitId - : this.checkoutBranch - ? this.checkoutBranch - : 'master'; await fs.ensureDir(this.projectPath); - await git.sparseCheckout(this.projectPath, this.locationURI, this.sparseCheckoutDir, commitReference); + // if no revision is specified, use the HEAD + await git.sparseCheckout( + this.projectPath, + this.defaultRemoteLocation, + this.sparseCheckoutDirs, + this.revision || 'HEAD' + ); theia.window.showInformationMessage( - `Sources by template ${this.sparseCheckoutDir} of ${this.locationURI} was cloned to ${this.projectPath}.` + `Sources by template ${this.sparseCheckoutDirs} of ${this.defaultRemoteLocation} was cloned to ${this.projectPath}.` ); return this.projectPath; } @@ -298,19 +270,14 @@ export class TheiaImportZipCommand implements TheiaImportCommand { private zipfile: string; private zipfilePath: string; - constructor(project: cheApi.workspace.ProjectConfig | cheApi.workspace.devfile.Project, projectsRoot: string) { - if (isDevfileProjectConfig(project)) { - const source = project.source; - - this.locationURI = source!.location; - this.projectDir = path.join(projectsRoot, project.name!); - this.tmpDir = fs.mkdtempSync(path.join(`${os.tmpdir()}${path.sep}`, 'workspace-plugin-')); - this.zipfile = `${project.name}.zip`; - this.zipfilePath = path.join(this.tmpDir, this.zipfile); - } else { - // legacy project config - theia.window.showErrorMessage('Legacy workspace config is not supported. Please use devfile instead.'); + constructor(project: che.devfile.DevfileProject, projectsRoot: string) { + if (project.zip) { + this.locationURI = project.zip.location; } + this.projectDir = path.join(projectsRoot, project.name); + this.tmpDir = fs.mkdtempSync(path.join(`${os.tmpdir()}${path.sep}`, 'workspace-plugin-')); + this.zipfile = `${project.name}.zip`; + this.zipfilePath = path.join(this.tmpDir, this.zipfile); } async execute(): Promise { diff --git a/plugins/workspace-plugin/src/workspace-projects-manager.ts b/plugins/workspace-plugin/src/workspace-projects-manager.ts index 3c345ee07..58509ec55 100644 --- a/plugins/workspace-plugin/src/workspace-projects-manager.ts +++ b/plugins/workspace-plugin/src/workspace-projects-manager.ts @@ -19,67 +19,63 @@ import * as theia from '@theia/plugin'; import { TheiaImportCommand, buildProjectImportCommand } from './theia-commands'; import { WorkspaceFolderUpdater } from './workspace-folder-updater'; -import { che as cheApi } from '@eclipse-che/api'; const onDidCloneSourcesEmitter = new theia.EventEmitter(); export const onDidCloneSources = onDidCloneSourcesEmitter.event; export function handleWorkspaceProjects(pluginContext: theia.PluginContext, projectsRoot: string): void { - che.workspace.getCurrentWorkspace().then((workspace: cheApi.workspace.Workspace) => { - new WorkspaceProjectsManager(pluginContext, projectsRoot).run(); - }); + new WorkspaceProjectsManager(pluginContext, projectsRoot).run(); } export class WorkspaceProjectsManager { protected watchers: theia.FileSystemWatcher[] = []; protected workspaceFolderUpdater = new WorkspaceFolderUpdater(); + private outputChannel: theia.OutputChannel; - constructor(protected pluginContext: theia.PluginContext, protected projectsRoot: string) {} + constructor(protected pluginContext: theia.PluginContext, protected projectsRoot: string) { + this.outputChannel = theia.window.createOutputChannel('workspace-plugin'); + } - getProjectPath(project: cheApi.workspace.devfile.Project): string { + getProjectPath(project: che.devfile.DevfileProject): string { return project.clonePath ? path.join(this.projectsRoot, project.clonePath) - : path.join(this.projectsRoot, project.name!); - } - - getProjects(workspace: cheApi.workspace.Workspace): cheApi.workspace.devfile.Project[] { - const projects = workspace.devfile!.projects; - return projects ? projects : []; + : path.join(this.projectsRoot, project.name); } async run(): Promise { - const workspace = await che.workspace.getCurrentWorkspace(); - const cloneCommandList = await this.buildCloneCommands(workspace); - - const cloningPromise = this.executeCloneCommands(cloneCommandList, workspace); + const devfile = await che.devfile.get(); + this.outputChannel.appendLine(`Found devfile ${JSON.stringify(devfile, undefined, 2)}`); + const cloneCommandList = await this.buildCloneCommands(devfile.projects || []); + this.outputChannel.appendLine(`Clone commands are ${JSON.stringify(cloneCommandList, undefined, 2)}`); + const isMultiRoot = devfile.metadata?.attributes?.multiRoot === 'on'; + this.outputChannel.appendLine(`multi root is ${isMultiRoot}`); + const cloningPromise = this.executeCloneCommands(cloneCommandList, isMultiRoot); theia.window.withProgress({ location: { viewId: 'explorer' } }, () => cloningPromise); await cloningPromise; await this.startSyncWorkspaceProjects(); } - async buildCloneCommands(workspace: cheApi.workspace.Workspace): Promise { - const instance = this; + notUndefined(x: T | undefined): x is T { + return x !== undefined; + } - const projects = this.getProjects(workspace); + async buildCloneCommands(projects: che.devfile.DevfileProject[]): Promise { + const instance = this; return projects .filter(project => !fs.existsSync(this.getProjectPath(project))) - .map(project => buildProjectImportCommand(project, instance.projectsRoot)!); + .map(project => buildProjectImportCommand(project, instance.projectsRoot)) + .filter(command => this.notUndefined(command)) as TheiaImportCommand[]; } - private async executeCloneCommands( - cloneCommandList: TheiaImportCommand[], - workspace: cheApi.workspace.Workspace - ): Promise { + private async executeCloneCommands(cloneCommandList: TheiaImportCommand[], isMultiRoot: boolean): Promise { if (cloneCommandList.length === 0) { return; } theia.window.showInformationMessage('Che Workspace: Starting importing projects.'); - const isMultiRoot = isMultiRootWorkspace(workspace); - const cloningPromises: PromiseLike[] = []; for (const cloneCommand of cloneCommandList) { try { @@ -90,6 +86,7 @@ export class WorkspaceProjectsManager { cloningPromise.then(projectPath => this.workspaceFolderUpdater.addWorkspaceFolder(projectPath)); } } catch (e) { + this.outputChannel.appendLine(`Error while cloning: ${e}`); // we continue to clone other projects even if a clone process failed for a project } } @@ -120,18 +117,30 @@ export class WorkspaceProjectsManager { return; } - const currentWorkspace = await che.workspace.getCurrentWorkspace(); - if (!currentWorkspace || !currentWorkspace.id) { - console.error('Unexpected error: current workspace id is not defined'); - return; - } + const devfile = await che.devfile.get(); + await this.updateOrCreateProject(devfile, projectFolderURI); + await che.devfile.update(devfile); + } - await this.updateOrCreateProject(currentWorkspace, projectFolderURI); + async selectProjectToCloneCommands(devfile: che.devfile.Devfile): Promise { + const instance = this; + + const projects = devfile.projects; + if (!projects) { + return []; + } - await che.workspace.update(currentWorkspace.id, currentWorkspace); + return projects + .filter(project => { + const projectPath = project.clonePath + ? path.join(instance.projectsRoot, project.clonePath) + : path.join(instance.projectsRoot, project.name); + return !fs.existsSync(projectPath); + }) + .map(project => buildProjectImportCommand(project, instance.projectsRoot)!); } - async updateOrCreateProject(workspace: cheApi.workspace.Workspace, projectFolderURI: string): Promise { + async updateOrCreateProject(devfile: che.devfile.Devfile, projectFolderURI: string): Promise { const projectUpstreamBranch: git.GitUpstreamBranch | undefined = await git.getUpstreamBranch(projectFolderURI); if (!projectUpstreamBranch || !projectUpstreamBranch.remoteURL) { console.error(`Could not detect git project branch for ${projectFolderURI}`); @@ -139,7 +148,7 @@ export class WorkspaceProjectsManager { } projectsHelper.updateOrCreateGitProjectInDevfile( - workspace.devfile!.projects!, + devfile.projects, fileUri.convertToCheProjectPath(projectFolderURI, this.projectsRoot), projectUpstreamBranch.remoteURL, projectUpstreamBranch.branch @@ -150,26 +159,15 @@ export class WorkspaceProjectsManager { if (!projectFolderURI) { return; } - const currentWorkspace = await che.workspace.getCurrentWorkspace(); - if (!currentWorkspace.id) { - console.error('Unexpected error: current workspace id is not defined'); - return; - } - - this.deleteProject(currentWorkspace, projectFolderURI); - - await che.workspace.update(currentWorkspace.id, currentWorkspace); + const devfile = await che.devfile.get(); + this.deleteProject(devfile, projectFolderURI); + await che.devfile.update(devfile); } - deleteProject(workspace: cheApi.workspace.Workspace, projectFolderURI: string): void { + deleteProject(devfile: che.devfile.Devfile, projectFolderURI: string): void { projectsHelper.deleteProjectFromDevfile( - workspace.devfile!.projects!, + devfile.projects, fileUri.convertToCheProjectPath(projectFolderURI, this.projectsRoot) ); } } - -function isMultiRootWorkspace(workspace: cheApi.workspace.Workspace): boolean { - const devfile = workspace.devfile; - return !!devfile && !!devfile.attributes && !!devfile.attributes.multiRoot && devfile.attributes.multiRoot === 'on'; -} diff --git a/plugins/workspace-plugin/tests/projects.spec.ts b/plugins/workspace-plugin/tests/projects.spec.ts index 270ee5481..0b7dc0edc 100644 --- a/plugins/workspace-plugin/tests/projects.spec.ts +++ b/plugins/workspace-plugin/tests/projects.spec.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2019-2020 Red Hat, Inc. + * Copyright (c) 2019-2021 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,14 +8,13 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ +import * as che from '@eclipse-che/plugin'; import * as projecthelper from '../src/projects'; -import { che as cheApi } from '@eclipse-che/api'; - describe('Devfile: Projects:', () => { describe('Testing basic functionality:', () => { test('Should be able to create project if no projects defined', () => { - const projects: cheApi.workspace.devfile.Project[] = []; + const projects: che.devfile.DevfileProject[] = []; projecthelper.updateOrCreateGitProjectInDevfile( projects, @@ -27,18 +26,21 @@ describe('Devfile: Projects:', () => { expect(projects.length).toBe(1); expect(projects[0].name).toBe('che'); expect(projects[0].clonePath).toBe(undefined); - expect(projects[0].source!.location).toBe('https://github.com/eclipse/che.git'); - expect(projects[0].source!.branch).toBe('che-13112'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse/che.git'); + expect(projects[0].git?.checkoutFrom?.revision).toBe('che-13112'); }); test('Should be able to add project into existing projects list', () => { - const projects: cheApi.workspace.devfile.Project[] = [ + const projects: che.devfile.DevfileProject[] = [ { name: 'che', - source: { - type: 'git', - location: 'https://github.com/eclipse/che.git', - branch: 'che-13112', + git: { + remotes: { + origin: 'https://github.com/eclipse/che.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, }, }, ]; @@ -53,30 +55,36 @@ describe('Devfile: Projects:', () => { expect(projects.length).toBe(2); expect(projects[0].name).toBe('che'); expect(projects[0].clonePath).toBe(undefined); - expect(projects[0].source!.location).toBe('https://github.com/eclipse/che.git'); - expect(projects[0].source!.branch).toBe('che-13112'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse/che.git'); + expect(projects[0].git?.checkoutFrom?.revision).toBe('che-13112'); expect(projects[1].name).toBe('che-theia'); expect(projects[1].clonePath).toBe(undefined); - expect(projects[1].source!.location).toBe('https://github.com/eclipse/che-theia.git'); - expect(projects[1].source!.branch).toBe('issue-12321'); + expect(projects[1].git?.remotes.origin).toBe('https://github.com/eclipse/che-theia.git'); + expect(projects[1].git?.checkoutFrom?.revision).toBe('issue-12321'); }); test('Should be able to delete existing project', () => { - const projects: cheApi.workspace.devfile.Project[] = [ + const projects: che.devfile.DevfileProject[] = [ { name: 'che', - source: { - type: 'git', - location: 'https://github.com/eclipse/che.git', - branch: 'che-13112', + git: { + remotes: { + origin: 'https://github.com/eclipse/che.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, }, }, { name: 'che-theia', - source: { - type: 'git', - location: 'https://github.com/eclipse/che-theia.git', - branch: 'issue-12321', + git: { + remotes: { + origin: 'https://github.com/eclipse/che-theia.git', + }, + checkoutFrom: { + revision: 'issue-12321', + }, }, }, ]; @@ -85,18 +93,21 @@ describe('Devfile: Projects:', () => { expect(projects.length).toBe(1); expect(projects[0].name).toBe('che'); - expect(projects[0].source!.location).toBe('https://github.com/eclipse/che.git'); - expect(projects[0].source!.branch).toBe('che-13112'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse/che.git'); + expect(projects[0].git?.checkoutFrom?.revision).toBe('che-13112'); }); test('Should be able to add project with custom location', () => { - const projects: cheApi.workspace.devfile.Project[] = [ + const projects: che.devfile.DevfileProject[] = [ { name: 'che', - source: { - type: 'git', - location: 'https://github.com/eclipse/che.git', - branch: 'che-13112', + git: { + remotes: { + origin: 'https://github.com/eclipse/che.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, }, }, ]; @@ -111,31 +122,37 @@ describe('Devfile: Projects:', () => { expect(projects.length).toBe(2); expect(projects[0].name).toBe('che'); expect(projects[0].clonePath).toBe(undefined); - expect(projects[0].source!.location).toBe('https://github.com/eclipse/che.git'); - expect(projects[0].source!.branch).toBe('che-13112'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse/che.git'); + expect(projects[0].git?.checkoutFrom?.revision).toBe('che-13112'); expect(projects[1].name).toBe('che-theia'); expect(projects[1].clonePath).toBe('theia/packages/che-theia'); - expect(projects[1].source!.location).toBe('https://github.com/eclipse/che-theia.git'); - expect(projects[1].source!.branch).toBe('issue-12321'); + expect(projects[1].git?.remotes.origin).toBe('https://github.com/eclipse/che-theia.git'); + expect(projects[1].git?.checkoutFrom?.revision).toBe('issue-12321'); }); test('Should be able to delete project with custom location', () => { - const projects: cheApi.workspace.devfile.Project[] = [ + const projects: che.devfile.DevfileProject[] = [ { name: 'che', - source: { - type: 'git', - location: 'https://github.com/eclipse/che.git', - branch: 'che-13112', + git: { + remotes: { + origin: 'https://github.com/eclipse/che.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, }, }, { name: 'che-theia', clonePath: 'theia/packages/che-theia', - source: { - type: 'git', - location: 'https://github.com/eclipse/che-theia.git', - branch: 'issue-12321', + git: { + remotes: { + origin: 'https://github.com/eclipse/che-theia.git', + }, + checkoutFrom: { + revision: 'issue-12321', + }, }, }, ]; @@ -144,33 +161,37 @@ describe('Devfile: Projects:', () => { expect(projects.length).toBe(1); expect(projects[0].name).toBe('che'); - expect(projects[0].source!.location).toBe('https://github.com/eclipse/che.git'); - expect(projects[0].source!.branch).toBe('che-13112'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse/che.git'); + expect(projects[0].git?.checkoutFrom?.revision).toBe('che-13112'); }); }); describe('Testing projects updater when file is triggered:', () => { test('update and create project', async () => { - const projects: cheApi.workspace.devfile.Project[] = [ + const projects: che.devfile.DevfileProject[] = [ { name: 'theia', - source: { - type: 'git', - location: 'https://github.com/eclipse-theia/theia.git', + git: { + remotes: { + origin: 'https://github.com/eclipse-theia/theia.git', + }, }, }, { name: 'che-theia-factory-extension', - source: { - type: 'git', - location: 'https://github.com/eclipse/che-theia-factory-extension.git', - branch: 'master', - tag: 'v42.0', + git: { + remotes: { + origin: 'https://github.com/eclipse/che-theia-factory-extension.git', + }, + checkoutFrom: { + revision: 'v42.0', + }, }, }, ]; - expect(projects[0].source!.location).toBe('https://github.com/eclipse-theia/theia.git'); - expect(projects[1].source!.location).toBe('https://github.com/eclipse/che-theia-factory-extension.git'); + + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse-theia/theia.git'); + expect(projects[1].git?.remotes.origin).toBe('https://github.com/eclipse/che-theia-factory-extension.git'); projecthelper.updateOrCreateGitProjectInDevfile( projects, @@ -178,9 +199,8 @@ describe('Devfile: Projects:', () => { 'https://github.com/sunix/che-theia-factory-extension.git', 'wip-sunix' ); - expect(projects[1].source!.location).toBe('https://github.com/sunix/che-theia-factory-extension.git'); - expect(projects[1].source!['branch']).toBe('wip-sunix'); - expect(projects[1].source!['tag']).toBe(undefined); + expect(projects[1].git?.remotes.origin).toBe('https://github.com/sunix/che-theia-factory-extension.git'); + expect(projects[1].git?.checkoutFrom?.revision).toBe('wip-sunix'); projecthelper.updateOrCreateGitProjectInDevfile( projects, @@ -188,36 +208,42 @@ describe('Devfile: Projects:', () => { 'https://github.com/sunix/che-theia-factory-extension.git', 'wip-theia' ); - expect(projects[2].source!.location).toBe('https://github.com/sunix/che-theia-factory-extension.git'); - expect(projects[2].source!['branch']).toBe('wip-theia'); + expect(projects[2].git?.remotes.origin).toBe('https://github.com/sunix/che-theia-factory-extension.git'); + expect(projects[2].git?.checkoutFrom?.revision).toBe('wip-theia'); expect(projects[2].name).toBe('che-theia-factory-extension'); }); test('delete project', async () => { - const projects: cheApi.workspace.devfile.Project[] = [ + const projects: che.devfile.DevfileProject[] = [ { name: 'theia', - source: { - type: 'git', - location: 'https://github.com/eclipse-theia/theia.git', + git: { + remotes: { + origin: 'https://github.com/eclipse-theia/theia.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, }, }, { name: 'che-theia-factory-extension', - source: { - type: 'git', - location: 'https://github.com/eclipse/che-theia-factory-extension.git', - branch: 'master', - tag: 'v42.0', + git: { + remotes: { + origin: 'https://github.com/eclipse/che-theia-factory-extension.git', + }, + checkoutFrom: { + revision: 'v42.0', + }, }, }, ]; - expect(projects[0].source!.location).toBe('https://github.com/eclipse-theia/theia.git'); - expect(projects[1].source!.location).toBe('https://github.com/eclipse/che-theia-factory-extension.git'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse-theia/theia.git'); + expect(projects[1].git?.remotes.origin).toBe('https://github.com/eclipse/che-theia-factory-extension.git'); projecthelper.deleteProjectFromDevfile(projects, 'che-theia-factory-extension'); expect(projects.length).toBe(1); - expect(projects[0].source!.location).toBe('https://github.com/eclipse-theia/theia.git'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/eclipse-theia/theia.git'); projecthelper.deleteProjectFromDevfile(projects, 'theia'); expect(projects.length).toBe(0); @@ -229,10 +255,9 @@ describe('Devfile: Projects:', () => { 'wip-theia' ); expect(projects.length).toBe(1); - expect(projects[0].source!.location).toBe('https://github.com/sunix/che-theia-factory-extension.git'); - expect(projects[0].source!.branch).toBe('wip-theia'); + expect(projects[0].git?.remotes.origin).toBe('https://github.com/sunix/che-theia-factory-extension.git'); + expect(projects[0].git?.checkoutFrom?.revision).toBe('wip-theia'); expect(projects[0].name).toBe('che-theia-factory-extension'); - projecthelper.deleteProjectFromDevfile(projects, 'che/che-theia-factory-extension'); expect(projects.length).toBe(0); }); diff --git a/plugins/workspace-plugin/tests/theia-commands.spec.ts b/plugins/workspace-plugin/tests/theia-commands.spec.ts new file mode 100644 index 000000000..4bd88cbae --- /dev/null +++ b/plugins/workspace-plugin/tests/theia-commands.spec.ts @@ -0,0 +1,127 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as che from '@eclipse-che/plugin'; +import * as git from '../src/git'; +import * as theia from '@theia/plugin'; + +import { TheiaGitCloneCommand, TheiaImportZipCommand, buildProjectImportCommand } from '../src/theia-commands'; + +jest.mock('../src/git', () => ({ + isSecureGitURI: jest.fn(), + execGit: jest.fn(), +})); + +describe('Test theia-commands', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + test('buildProjectImportCommand git', async () => { + const project: che.devfile.DevfileProject = { + name: 'che-theia', + git: { + remotes: { + origin: 'https://github.com/eclipse/che-theia.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, + }, + }; + const command = buildProjectImportCommand(project, '/foo'); + expect(command).toBeDefined(); + // should be a git clone command + expect(command instanceof TheiaGitCloneCommand).toBeTruthy(); + }); + + test('buildProjectImportCommand github', async () => { + const project: che.devfile.DevfileProject = { + name: 'che-theia', + github: { + remotes: { + origin: 'https://github.com/eclipse/che-theia.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, + }, + }; + const command = buildProjectImportCommand(project, '/foo'); + expect(command).toBeDefined(); + // should be a git clone command + expect(command instanceof TheiaGitCloneCommand).toBeTruthy(); + }); + + test('buildProjectImportCommand zip', async () => { + const project: che.devfile.DevfileProject = { + name: 'che-theia', + zip: { + location: 'http://foo/bar.zip', + }, + }; + const command = buildProjectImportCommand(project, '/foo'); + expect(command).toBeDefined(); + // should be a zip command + expect(command instanceof TheiaImportZipCommand).toBeTruthy(); + }); + + test('buildProjectImportCommand missing type', async () => { + theia.window.showWarningMessage = jest.fn(); + const warningSpy = jest.spyOn(theia.window, 'showWarningMessage'); + const projects: che.devfile.DevfileProject = { + name: 'che-theia', + }; + const command = buildProjectImportCommand(projects, '/foo'); + expect(command).toBeUndefined(); + expect(warningSpy).toBeCalled(); + }); + + test('clone git', async () => { + (theia as any).ProgressLocation = { + Notification: '', + }; + const progressFunction = jest.fn(); + theia.window.withProgress = progressFunction; + theia.window.showErrorMessage = jest.fn(); + theia.window.showInformationMessage = jest.fn(); + + const project: che.devfile.DevfileProject = { + name: 'che-theia', + git: { + remotes: { + origin: 'https://github.com/eclipse/che-theia.git', + }, + checkoutFrom: { + revision: 'che-13112', + }, + }, + }; + const cloneCommand = new TheiaGitCloneCommand(project, '/foo'); + await cloneCommand.execute(); + expect(progressFunction).toBeCalled(); + const execGitSpy = jest.spyOn(git, 'execGit'); + + execGitSpy.mockResolvedValueOnce('foo1'); + execGitSpy.mockResolvedValueOnce('foo2'); + execGitSpy.mockResolvedValueOnce('foo3'); + execGitSpy.mockResolvedValueOnce('foo4'); + // call callback + await progressFunction.mock.calls[0][1].call(cloneCommand); + + expect(execGitSpy).toBeCalledTimes(2); + const infoMessage = (theia.window.showInformationMessage as jest.Mock).mock.calls[0]; + expect(infoMessage[0]).toContain( + 'Project https://github.com/eclipse/che-theia.git cloned to /foo/che-theia using default branch which has been reset to che-13112.' + ); + }); +});