diff --git a/src/plus/workspaces/models.ts b/src/plus/workspaces/models.ts index d1f20fd..95da7f1 100644 --- a/src/plus/workspaces/models.ts +++ b/src/plus/workspaces/models.ts @@ -1,3 +1,4 @@ +import type { Container } from '../../container'; import type { Repository } from '../../git/models/repository'; export enum WorkspaceType { @@ -24,19 +25,19 @@ export interface RemoteDescriptor { } export interface GetWorkspacesResponse { - cloudWorkspaces: GKCloudWorkspace[]; - localWorkspaces: GKLocalWorkspace[]; + cloudWorkspaces: CloudWorkspace[]; + localWorkspaces: LocalWorkspace[]; cloudWorkspaceInfo: string | undefined; localWorkspaceInfo: string | undefined; } export interface LoadCloudWorkspacesResponse { - cloudWorkspaces: GKCloudWorkspace[] | undefined; + cloudWorkspaces: CloudWorkspace[] | undefined; cloudWorkspaceInfo: string | undefined; } export interface LoadLocalWorkspacesResponse { - localWorkspaces: GKLocalWorkspace[] | undefined; + localWorkspaces: LocalWorkspace[] | undefined; localWorkspaceInfo: string | undefined; } @@ -46,60 +47,39 @@ export interface GetCloudWorkspaceRepositoriesResponse { } // Cloud Workspace types -export class GKCloudWorkspace { - private readonly _type: WorkspaceType = WorkspaceType.Cloud; - private readonly _id: string; - private readonly _organizationId: string | undefined; - private readonly _name: string; - private readonly _provider: CloudWorkspaceProviderType; +export class CloudWorkspace { + readonly type = WorkspaceType.Cloud; + private _repositories: CloudWorkspaceRepositoryDescriptor[] | undefined; + constructor( - id: string, - name: string, - organizationId: string | undefined, - provider: CloudWorkspaceProviderType, - private readonly getReposFn: (workspaceId: string) => Promise, + private readonly container: Container, + public readonly id: string, + public readonly name: string, + public readonly organizationId: string | undefined, + public readonly provider: CloudWorkspaceProviderType, repositories?: CloudWorkspaceRepositoryDescriptor[], ) { - this._id = id; - this._name = name; - this._organizationId = organizationId; - this._provider = provider; this._repositories = repositories; } - get type(): WorkspaceType { - return this._type; - } - - get id(): string { - return this._id; - } - - get name(): string { - return this._name; - } - - get organization_id(): string | undefined { - return this._organizationId; + get shared(): boolean { + return this.organizationId != null; } - get provider(): CloudWorkspaceProviderType { - return this._provider; - } + async getRepositoryDescriptors(): Promise { + if (this._repositories == null) { + this._repositories = await this.container.workspaces.getCloudWorkspaceRepositories(this.id); + } - get repositories(): CloudWorkspaceRepositoryDescriptor[] | undefined { return this._repositories; } - isShared(): boolean { - return this._organizationId != null; - } - - getRepository(name: string): CloudWorkspaceRepositoryDescriptor | undefined { - return this._repositories?.find(r => r.name === name); + async getRepositoryDescriptor(name: string): Promise { + return (await this.getRepositoryDescriptors()).find(r => r.name === name); } + // TODO@axosoft-ramint this should be the entry point, not a backdoor to update the cache addRepositories(repositories: CloudWorkspaceRepositoryDescriptor[]): void { if (this._repositories == null) { this._repositories = repositories; @@ -108,22 +88,12 @@ export class GKCloudWorkspace { } } + // TODO@axosoft-ramint this should be the entry point, not a backdoor to update the cache removeRepositories(repoNames: string[]): void { if (this._repositories == null) return; this._repositories = this._repositories.filter(r => !repoNames.includes(r.name)); } - - async getOrLoadRepositories(): Promise { - if (this._repositories != null) return { repositories: this._repositories, repositoriesInfo: undefined }; - - const getResponse = await this.getReposFn(this._id); - if (getResponse.repositories != null) { - this._repositories = getResponse.repositories; - } - - return getResponse; - } } export interface CloudWorkspaceRepositoryDescriptor { @@ -495,39 +465,25 @@ export interface RemoveWorkspaceRepoDescriptor { } // Local Workspace Types -export class GKLocalWorkspace { - private readonly _type: WorkspaceType = WorkspaceType.Local; - private readonly _id: string; - private readonly _name: string; - private readonly _repositories: LocalWorkspaceRepositoryDescriptor[] | undefined; - constructor(id: string, name: string, repositories?: LocalWorkspaceRepositoryDescriptor[]) { - this._id = id; - this._name = name; - this._repositories = repositories; - } - - get type(): WorkspaceType { - return this._type; - } +export class LocalWorkspace { + readonly type = WorkspaceType.Local; - get id(): string { - return this._id; - } - - get name(): string { - return this._name; - } + constructor( + public readonly id: string, + public readonly name: string, + private readonly repositories: LocalWorkspaceRepositoryDescriptor[], + ) {} - get repositories(): LocalWorkspaceRepositoryDescriptor[] | undefined { - return this._repositories; + get shared(): boolean { + return false; } - isShared(): boolean { - return false; + getRepositoryDescriptors(): Promise { + return Promise.resolve(this.repositories); } - getRepository(name: string): LocalWorkspaceRepositoryDescriptor | undefined { - return this._repositories?.find(r => r.name === name); + getRepositoryDescriptor(name: string): Promise { + return Promise.resolve(this.repositories.find(r => r.name === name)); } } diff --git a/src/plus/workspaces/workspacesService.ts b/src/plus/workspaces/workspacesService.ts index 47311a8..8ba7e46 100644 --- a/src/plus/workspaces/workspacesService.ts +++ b/src/plus/workspaces/workspacesService.ts @@ -16,7 +16,6 @@ import type { CloudWorkspaceData, CloudWorkspaceProviderType, CloudWorkspaceRepositoryDescriptor, - GetCloudWorkspaceRepositoriesResponse, GetWorkspacesResponse, LoadCloudWorkspacesResponse, LoadLocalWorkspacesResponse, @@ -28,10 +27,10 @@ import type { WorkspacesResponse, } from './models'; import { + CloudWorkspace, CloudWorkspaceProviderInputType, cloudWorkspaceProviderTypeToRemoteProviderId, - GKCloudWorkspace, - GKLocalWorkspace, + LocalWorkspace, WorkspaceAddRepositoriesChoice, WorkspaceType, } from './models'; @@ -39,36 +38,16 @@ import { WorkspacesApi } from './workspacesApi'; import type { WorkspacesPathMappingProvider } from './workspacesPathMappingProvider'; export class WorkspacesService implements Disposable { - private _cloudWorkspaces: GKCloudWorkspace[] | undefined = undefined; - private _localWorkspaces: GKLocalWorkspace[] | undefined = undefined; - private _workspacesApi: WorkspacesApi; - private _workspacesPathProvider: WorkspacesPathMappingProvider; private _onDidChangeWorkspaces: EventEmitter = new EventEmitter(); get onDidChangeWorkspaces(): Event { return this._onDidChangeWorkspaces.event; } - private _disposable: Disposable; - // TODO@ramint Add error handling/logging when this is used. - private readonly _getCloudWorkspaceRepos: (workspaceId: string) => Promise = - async (workspaceId: string) => { - try { - const workspaceRepos = await this._workspacesApi.getWorkspaceRepositories(workspaceId); - const repoDescriptors = workspaceRepos?.data?.project?.provider_data?.repositories?.nodes; - return { - repositories: - repoDescriptors != null - ? repoDescriptors.map(descriptor => ({ ...descriptor, workspaceId: workspaceId })) - : [], - repositoriesInfo: undefined, - }; - } catch { - return { - repositories: undefined, - repositoriesInfo: 'Failed to load repositories for this workspace.', - }; - } - }; + private _cloudWorkspaces: CloudWorkspace[] | undefined; + private _disposable: Disposable; + private _localWorkspaces: LocalWorkspace[] | undefined; + private _workspacesApi: WorkspacesApi; + private _workspacesPathProvider: WorkspacesPathMappingProvider; constructor(private readonly container: Container, private readonly server: ServerConnection) { this._workspacesApi = new WorkspacesApi(this.container, this.server); @@ -100,7 +79,7 @@ export class WorkspacesService implements Disposable { }; } - const cloudWorkspaces: GKCloudWorkspace[] = []; + const cloudWorkspaces: CloudWorkspace[] = []; let workspaces: CloudWorkspaceData[] | undefined; try { const workspaceResponse: WorkspacesResponse | undefined = excludeRepositories @@ -137,12 +116,12 @@ export class WorkspacesService implements Disposable { } cloudWorkspaces.push( - new GKCloudWorkspace( + new CloudWorkspace( + this.container, workspace.id, workspace.name, workspace.organization?.id, workspace.provider as CloudWorkspaceProviderType, - this._getCloudWorkspaceRepos, repositories, ), ); @@ -160,12 +139,12 @@ export class WorkspacesService implements Disposable { // TODO@ramint: When we interact more with local workspaces, this should return more info about failures. private async loadLocalWorkspaces(): Promise { - const localWorkspaces: GKLocalWorkspace[] = []; + const localWorkspaces: LocalWorkspace[] = []; const workspaceFileData: LocalWorkspaceData = (await this._workspacesPathProvider.getLocalWorkspaceData())?.workspaces || {}; for (const workspace of Object.values(workspaceFileData)) { localWorkspaces.push( - new GKLocalWorkspace( + new LocalWorkspace( workspace.localId, workspace.name, workspace.repositories.map(repositoryPath => ({ @@ -183,11 +162,11 @@ export class WorkspacesService implements Disposable { }; } - private getCloudWorkspace(workspaceId: string): GKCloudWorkspace | undefined { + private getCloudWorkspace(workspaceId: string): CloudWorkspace | undefined { return this._cloudWorkspaces?.find(workspace => workspace.id === workspaceId); } - private getLocalWorkspace(workspaceId: string): GKLocalWorkspace | undefined { + private getLocalWorkspace(workspaceId: string): LocalWorkspace | undefined { return this._localWorkspaces?.find(workspace => workspace.id === workspaceId); } @@ -217,6 +196,13 @@ export class WorkspacesService implements Disposable { return getWorkspacesResponse; } + async getCloudWorkspaceRepositories(workspaceId: string): Promise { + // TODO@ramint Add error handling/logging when this is used. + const workspaceRepos = await this._workspacesApi.getWorkspaceRepositories(workspaceId); + const descriptors = workspaceRepos?.data?.project?.provider_data?.repositories?.nodes; + return descriptors?.map(d => ({ ...d, workspaceId: workspaceId })) ?? []; + } + resetWorkspaces(options?: { cloud?: boolean; local?: boolean }) { if (options?.cloud ?? true) { this._cloudWorkspaces = undefined; @@ -260,7 +246,9 @@ export class WorkspacesService implements Disposable { async locateAllCloudWorkspaceRepos(workspaceId: string, cancellation?: CancellationToken): Promise { const workspace = this.getCloudWorkspace(workspaceId); if (workspace == null) return; - if (workspace.repositories == null || workspace.repositories.length === 0) return; + + const repoDescriptors = await workspace.getRepositoryDescriptors(); + if (repoDescriptors == null || repoDescriptors.length === 0) return; const foundRepos = await this.getRepositoriesInParentFolder(cancellation); if (foundRepos == null || foundRepos.length === 0 || cancellation?.isCancellationRequested) return; @@ -564,12 +552,12 @@ export class WorkspacesService implements Disposable { } this._cloudWorkspaces?.push( - new GKCloudWorkspace( + new CloudWorkspace( + this.container, createdProjectData.id, createdProjectData.name, createdProjectData.organization?.id, createdProjectData.provider as CloudWorkspaceProviderType, - this._getCloudWorkspaceRepos, [], ), ); @@ -780,10 +768,13 @@ export class WorkspacesService implements Disposable { }, ): Promise { const workspaceRepositoriesByName: WorkspaceRepositoriesByName = new Map(); - const workspace: GKCloudWorkspace | GKLocalWorkspace | undefined = - this.getCloudWorkspace(workspaceId) ?? this.getLocalWorkspace(workspaceId); - if (workspace?.repositories == null || workspace.repositories.length === 0) return workspaceRepositoriesByName; + const workspace = this.getLocalWorkspace(workspaceId) ?? this.getCloudWorkspace(workspaceId); + if (workspace == null) return workspaceRepositoriesByName; + + const repoDescriptors = await workspace.getRepositoryDescriptors(); + if (repoDescriptors == null || repoDescriptors.length === 0) return workspaceRepositoriesByName; + const currentRepositories = options?.repositories ?? this.container.git.repositories; const reposProviderMap = new Map(); @@ -792,7 +783,7 @@ export class WorkspacesService implements Disposable { if (options?.cancellation?.isCancellationRequested) break; reposPathMap.set(normalizePath(repo.uri.fsPath.toLowerCase()), repo); - if (workspace instanceof GKCloudWorkspace) { + if (workspace instanceof CloudWorkspace) { const remotes = await repo.getRemotes(); for (const remote of remotes) { const remoteDescriptor = getRemoteDescriptor(remote); @@ -805,7 +796,7 @@ export class WorkspacesService implements Disposable { } } - for (const descriptor of workspace.repositories) { + for (const descriptor of repoDescriptors) { let repoLocalPath = null; let foundRepo = null; @@ -864,12 +855,14 @@ export class WorkspacesService implements Disposable { workspaceType: WorkspaceType, options?: { open?: boolean }, ): Promise { - const workspace: GKCloudWorkspace | GKLocalWorkspace | undefined = + const workspace = workspaceType === WorkspaceType.Cloud ? this.getCloudWorkspace(workspaceId) : this.getLocalWorkspace(workspaceId); + if (workspace == null) return; - if (workspace?.repositories == null) return; + const repoDescriptors = await workspace.getRepositoryDescriptors(); + if (repoDescriptors == null) return; const workspaceRepositoriesByName = await this.resolveWorkspaceRepositoriesByName(workspaceId, { resolveFromPath: true, @@ -889,7 +882,7 @@ export class WorkspacesService implements Disposable { } } - if (workspaceFolderPaths.length < workspace.repositories.length) { + if (workspaceFolderPaths.length < repoDescriptors.length) { const confirmation = await window.showWarningMessage( `Some repositories in this workspace could not be located locally. Do you want to continue?`, { modal: true }, diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index c38ef9a..92732bf 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -8,10 +8,12 @@ import type { RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../ import { Repository, RepositoryChange, RepositoryChangeComparisonMode } from '../../git/models/repository'; import type { GitStatus } from '../../git/models/status'; import type { + CloudWorkspace, CloudWorkspaceRepositoryDescriptor, + LocalWorkspace, LocalWorkspaceRepositoryDescriptor, } from '../../plus/workspaces/models'; -import { GKCloudWorkspace, GKLocalWorkspace } from '../../plus/workspaces/models'; +import { WorkspaceType } from '../../plus/workspaces/models'; import { findLastIndex } from '../../system/array'; import { gate } from '../../system/decorators/gate'; import { debug, log } from '../../system/decorators/log'; @@ -50,7 +52,7 @@ export class RepositoryNode extends SubscribeableViewNode parent: ViewNode, public readonly repo: Repository, private readonly options?: { - workspace?: GKCloudWorkspace | GKLocalWorkspace; + workspace?: CloudWorkspace | LocalWorkspace; workspaceRepoDescriptor: CloudWorkspaceRepositoryDescriptor | LocalWorkspaceRepositoryDescriptor; }, ) { @@ -264,9 +266,9 @@ export class RepositoryNode extends SubscribeableViewNode } if (this.options?.workspace) { contextValue += '+workspace'; - if (this.options.workspace instanceof GKCloudWorkspace) { + if (this.options.workspace.type === WorkspaceType.Cloud) { contextValue += '+cloud'; - } else if (this.options.workspace instanceof GKLocalWorkspace) { + } else if (this.options.workspace.type === WorkspaceType.Local) { contextValue += '+local'; } } @@ -347,7 +349,7 @@ export class RepositoryNode extends SubscribeableViewNode light: this.view.container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`), }; - if (this.options?.workspace && !this.repo.closed) { + if (this.options?.workspace != null && !this.repo.closed) { item.resourceUri = Uri.parse(`gitlens-view://workspaces/repository/open`); } diff --git a/src/views/nodes/workspaceNode.ts b/src/views/nodes/workspaceNode.ts index 6ca43fc..c735e82 100644 --- a/src/views/nodes/workspaceNode.ts +++ b/src/views/nodes/workspaceNode.ts @@ -1,13 +1,10 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GitUri } from '../../git/gitUri'; -import type { - CloudWorkspaceRepositoryDescriptor, - LocalWorkspaceRepositoryDescriptor, - WorkspaceRepositoriesByName, -} from '../../plus/workspaces/models'; -import { GKCloudWorkspace, GKLocalWorkspace, WorkspaceType } from '../../plus/workspaces/models'; +import type { CloudWorkspace, LocalWorkspace, WorkspaceRepositoriesByName } from '../../plus/workspaces/models'; +import { WorkspaceType } from '../../plus/workspaces/models'; +import { createCommand } from '../../system/command'; import type { WorkspacesView } from '../workspacesView'; -import { MessageNode } from './common'; +import { CommandMessageNode, MessageNode } from './common'; import { RepositoryNode } from './repositoryNode'; import { ContextValues, ViewNode } from './viewNode'; import { WorkspaceMissingRepositoryNode } from './workspaceMissingRepositoryNode'; @@ -18,40 +15,17 @@ export class WorkspaceNode extends ViewNode { return `gitlens${this.key}(${workspaceId})`; } - private _workspace: GKCloudWorkspace | GKLocalWorkspace; - private _type: WorkspaceType; - constructor( uri: GitUri, view: WorkspacesView, parent: ViewNode, - public readonly workspace: GKCloudWorkspace | GKLocalWorkspace, + public readonly workspace: CloudWorkspace | LocalWorkspace, ) { super(uri, view, parent); - this._workspace = workspace; - this._type = workspace.type; } override get id(): string { - return WorkspaceNode.getId(this._workspace.id ?? ''); - } - - get name(): string { - return this._workspace?.name ?? ''; - } - - get workspaceId(): string { - return this._workspace.id ?? ''; - } - - get type(): WorkspaceType { - return this._type; - } - - private async getRepositories(): Promise< - CloudWorkspaceRepositoryDescriptor[] | LocalWorkspaceRepositoryDescriptor[] | undefined - > { - return Promise.resolve(this._workspace?.repositories); + return WorkspaceNode.getId(this.workspace.id); } private _children: ViewNode[] | undefined; @@ -59,23 +33,29 @@ export class WorkspaceNode extends ViewNode { async getChildren(): Promise { if (this._children == null) { this._children = []; - let descriptors: CloudWorkspaceRepositoryDescriptor[] | LocalWorkspaceRepositoryDescriptor[] | undefined; - let repositoryInfo: string | undefined; - if (this.workspace instanceof GKLocalWorkspace) { - descriptors = (await this.getRepositories()) ?? []; - } else { - const { repositories: repos, repositoriesInfo: repoInfo } = - await this.workspace.getOrLoadRepositories(); - descriptors = repos; - repositoryInfo = repoInfo; - } - if (descriptors?.length === 0) { - this._children.push(new MessageNode(this.view, this, 'No repositories in this workspace.')); - return this._children; - } else if (descriptors?.length) { + try { + const descriptors = await this.workspace.getRepositoryDescriptors(); + + if (descriptors == null || descriptors.length === 0) { + this._children.push( + new CommandMessageNode( + this.view, + this, + createCommand<[WorkspaceNode]>( + 'gitlens.views.workspaces.addRepos', + 'Add Repositories...', + this, + ), + 'No repositories', + ), + ); + return this._children; + } + + // TODO@eamodio this should not be done here -- it should be done in the workspaces model (when loading the repos) const reposByName: WorkspaceRepositoriesByName = - await this.view.container.workspaces.resolveWorkspaceRepositoriesByName(this.workspaceId, { + await this.view.container.workspaces.resolveWorkspaceRepositoriesByName(this.workspace.id, { resolveFromPath: true, usePathMapping: true, }); @@ -84,22 +64,20 @@ export class WorkspaceNode extends ViewNode { const repo = reposByName.get(descriptor.name)?.repository; if (!repo) { this._children.push( - new WorkspaceMissingRepositoryNode(this.view, this, this.workspaceId, descriptor), + new WorkspaceMissingRepositoryNode(this.view, this, this.workspace.id, descriptor), ); continue; } this._children.push( new RepositoryNode(GitUri.fromRepoPath(repo.path), this.view, this, repo, { - workspace: this._workspace, + workspace: this.workspace, workspaceRepoDescriptor: descriptor, }), ); } - } - - if (repositoryInfo != null) { - this._children.push(new MessageNode(this.view, this, repositoryInfo)); + } catch (ex) { + return [new MessageNode(this.view, this, 'Failed to load repositories')]; } } @@ -107,29 +85,24 @@ export class WorkspaceNode extends ViewNode { } getTreeItem(): TreeItem { - const description = ''; - // TODO@ramint Icon needs to change based on workspace type, and need a tooltip. - const icon: ThemeIcon = new ThemeIcon(this._type == WorkspaceType.Cloud ? 'cloud' : 'folder'); + const item = new TreeItem(this.workspace.name, TreeItemCollapsibleState.Collapsed); - const item = new TreeItem(this.name, TreeItemCollapsibleState.Collapsed); let contextValue = `${ContextValues.Workspace}`; - - if (this._type === WorkspaceType.Cloud) { + if (this.workspace.type === WorkspaceType.Cloud) { contextValue += '+cloud'; } else { contextValue += '+local'; } item.id = this.id; - item.description = description; item.contextValue = contextValue; - item.iconPath = icon; - item.tooltip = `${this.name}\n${ - this._type === WorkspaceType.Cloud - ? `Cloud Workspace ${this._workspace.isShared() ? '(Shared)' : ''}` + item.iconPath = new ThemeIcon(this.workspace.type == WorkspaceType.Cloud ? 'cloud' : 'folder'); + item.tooltip = `${this.workspace.name}\n${ + this.workspace.type === WorkspaceType.Cloud + ? `Cloud Workspace ${this.workspace.shared ? '(Shared)' : ''}` : 'Local Workspace' }${ - this._workspace instanceof GKCloudWorkspace && this._workspace.provider != null - ? `\nProvider: ${this._workspace.provider}` + this.workspace.type === WorkspaceType.Cloud && this.workspace.provider != null + ? `\nProvider: ${this.workspace.provider}` : '' }`; item.resourceUri = undefined;