From 6178fa34fd5319532836c24794056fd8febd941b Mon Sep 17 00:00:00 2001 From: Ramin Tadayon <67011668+axosoft-ramint@users.noreply.github.com> Date: Thu, 8 Jun 2023 10:14:34 +0900 Subject: [PATCH] Workspaces Polish (#2719) * Validates description on creation * Sets the workspace name in current window description * Unifies codepaths for resolving/matching repos to descriptors * Filters out repos already in workspace when adding * Allows multi-pick repos when adding to workspace * Allows choice of folder when adding repositories * Removes descriptions on quickpick * Prefilters "current window" option when no valid repos --- package.json | 10 +- src/plus/workspaces/models.ts | 20 +- src/plus/workspaces/workspacesService.ts | 523 ++++++++++++++++++------------- src/views/nodes/repositoriesNode.ts | 7 +- src/views/nodes/workspaceNode.ts | 26 +- src/views/workspacesView.ts | 4 +- 6 files changed, 343 insertions(+), 247 deletions(-) diff --git a/package.json b/package.json index e954eed..e7179f2 100644 --- a/package.json +++ b/package.json @@ -6905,8 +6905,8 @@ "enablement": "!operationInProgress" }, { - "command": "gitlens.views.workspaces.addRepo", - "title": "Add Repository to Workspace...", + "command": "gitlens.views.workspaces.addRepos", + "title": "Add Repositories to Workspace...", "category": "GitLens", "icon": "$(add)" }, @@ -9433,7 +9433,7 @@ "when": "false" }, { - "command": "gitlens.views.workspaces.addRepo", + "command": "gitlens.views.workspaces.addRepos", "when": "false" }, { @@ -11058,7 +11058,7 @@ "group": "inline@1" }, { - "command": "gitlens.views.workspaces.addRepo", + "command": "gitlens.views.workspaces.addRepos", "when": "viewItem =~ /gitlens:workspace\\b(?=.*?\\b\\+cloud\\b)/", "group": "inline@2" }, @@ -11104,7 +11104,7 @@ "group": "1_gitlens_actions@2" }, { - "command": "gitlens.views.workspaces.addRepo", + "command": "gitlens.views.workspaces.addRepos", "when": "viewItem =~ /gitlens:workspace\\b(?=.*?\\b\\+cloud\\b)/", "group": "1_gitlens_actions@3" }, diff --git a/src/plus/workspaces/models.ts b/src/plus/workspaces/models.ts index d1d7e55..d1f20fd 100644 --- a/src/plus/workspaces/models.ts +++ b/src/plus/workspaces/models.ts @@ -10,7 +10,18 @@ export type CodeWorkspaceFileContents = { settings: { [key: string]: any }; }; -export type WorkspaceRepositoriesByName = Map; +export type WorkspaceRepositoriesByName = Map; + +export interface RepositoryMatch { + repository: Repository; + descriptor: CloudWorkspaceRepositoryDescriptor | LocalWorkspaceRepositoryDescriptor; +} + +export interface RemoteDescriptor { + provider: string; + owner: string; + repoName: string; +} export interface GetWorkspacesResponse { cloudWorkspaces: GKCloudWorkspace[]; @@ -124,6 +135,7 @@ export interface CloudWorkspaceRepositoryDescriptor { provider_organization_id: string; provider_organization_name: string; url: string; + workspaceId: string; } export enum CloudWorkspaceProviderInputType { @@ -162,6 +174,11 @@ export const cloudWorkspaceProviderInputTypeToRemoteProviderId = { [CloudWorkspaceProviderInputType.GitLabSelfHosted]: 'gitlab', }; +export enum WorkspaceAddRepositoriesChoice { + CurrentWindow = 'Current Window', + ParentFolder = 'Parent Folder', +} + export const defaultWorkspaceCount = 100; export const defaultWorkspaceRepoCount = 100; @@ -538,6 +555,7 @@ export interface LocalWorkspaceRepositoryPath { export interface LocalWorkspaceRepositoryDescriptor extends LocalWorkspaceRepositoryPath { id?: undefined; name: string; + workspaceId: string; } export interface CloudWorkspaceFileData { diff --git a/src/plus/workspaces/workspacesService.ts b/src/plus/workspaces/workspacesService.ts index ebe4850..76ef81e 100644 --- a/src/plus/workspaces/workspacesService.ts +++ b/src/plus/workspaces/workspacesService.ts @@ -2,10 +2,12 @@ import type { CancellationToken, Event } from 'vscode'; import { Disposable, EventEmitter, Uri, window } from 'vscode'; import { getSupportedWorkspacesPathMappingProvider } from '@env/providers'; import type { Container } from '../../container'; +import type { GitRemote } from '../../git/models/remote'; import { RemoteResourceType } from '../../git/models/remoteResource'; -import type { Repository } from '../../git/models/repository'; -import { showRepositoryPicker } from '../../quickpicks/repositoryPicker'; +import { Repository } from '../../git/models/repository'; +import { showRepositoriesPicker } from '../../quickpicks/repositoryPicker'; import { SubscriptionState } from '../../subscription'; +import { normalizePath } from '../../system/path'; import { openWorkspace, OpenWorkspaceLocation } from '../../system/utils'; import type { ServerConnection } from '../subscription/serverConnection'; import type { SubscriptionChangeEvent } from '../subscription/subscriptionService'; @@ -20,15 +22,17 @@ import type { LoadLocalWorkspacesResponse, LocalWorkspaceData, LocalWorkspaceRepositoryDescriptor, + RemoteDescriptor, + RepositoryMatch, WorkspaceRepositoriesByName, WorkspacesResponse, } from './models'; import { CloudWorkspaceProviderInputType, - cloudWorkspaceProviderInputTypeToRemoteProviderId, cloudWorkspaceProviderTypeToRemoteProviderId, GKCloudWorkspace, GKLocalWorkspace, + WorkspaceAddRepositoriesChoice, WorkspaceType, } from './models'; import { WorkspacesApi } from './workspacesApi'; @@ -50,8 +54,12 @@ export class WorkspacesService implements Disposable { async (workspaceId: string) => { try { const workspaceRepos = await this._workspacesApi.getWorkspaceRepositories(workspaceId); + const repoDescriptors = workspaceRepos?.data?.project?.provider_data?.repositories?.nodes; return { - repositories: workspaceRepos?.data?.project?.provider_data?.repositories?.nodes ?? [], + repositories: + repoDescriptors != null + ? repoDescriptors.map(descriptor => ({ ...descriptor, workspaceId: workspaceId })) + : [], repositoriesInfo: undefined, }; } catch { @@ -119,8 +127,11 @@ export class WorkspacesService implements Disposable { continue; } - let repositories: CloudWorkspaceRepositoryDescriptor[] | undefined = - workspace.provider_data?.repositories?.nodes; + const repoDescriptors = workspace.provider_data?.repositories?.nodes; + let repositories = + repoDescriptors != null + ? repoDescriptors.map(descriptor => ({ ...descriptor, workspaceId: workspace.id })) + : repoDescriptors; if (repositories == null && !excludeRepositories) { repositories = []; } @@ -160,6 +171,7 @@ export class WorkspacesService implements Disposable { workspace.repositories.map(repositoryPath => ({ localPath: repositoryPath.localPath, name: repositoryPath.localPath.split(/[\\/]/).pop() ?? 'unknown', + workspaceId: workspace.localId, })), ), ); @@ -222,69 +234,57 @@ export class WorkspacesService implements Disposable { await this._workspacesPathProvider.writeCloudWorkspaceDiskPathToMap(workspaceId, repoId, localPath); } - async locateAllCloudWorkspaceRepos(workspaceId: string, cancellation?: CancellationToken): Promise { - const workspace = this.getCloudWorkspace(workspaceId); - if (workspace == null) return; - - const repoDescriptors = workspace.repositories; - if (repoDescriptors == null || repoDescriptors.length === 0) return; - + private async getRepositoriesInParentFolder(cancellation?: CancellationToken): Promise { const parentUri = ( await window.showOpenDialog({ - title: `Choose a folder containing the repositories in this workspace`, + title: `Choose a folder containing repositories for this workspace`, canSelectFiles: false, canSelectFolders: true, canSelectMany: false, }) )?.[0]; - if (parentUri == null || cancellation?.isCancellationRequested) return; + if (parentUri == null || cancellation?.isCancellationRequested) return undefined; - let foundRepos; try { - foundRepos = await this.container.git.findRepositories(parentUri, { + return this.container.git.findRepositories(parentUri, { cancellation: cancellation, depth: 1, silent: true, }); } catch (ex) { - foundRepos = []; - return; + return undefined; } + } - if (foundRepos.length === 0 || cancellation?.isCancellationRequested) return; + 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; - // Map repos by provider/owner/name - const foundReposMap = new Map(); - const foundReposNameMap = new Map(); - for (const repo of foundRepos) { - foundReposNameMap.set(repo.name.toLowerCase(), repo); + const parentUri = ( + await window.showOpenDialog({ + title: `Choose a folder containing the repositories in this workspace`, + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + }) + )?.[0]; - if (cancellation?.isCancellationRequested) break; + if (parentUri == null || cancellation?.isCancellationRequested) return; - const remotes = await repo.getRemotes(); - for (const remote of remotes) { - if (remote.provider?.owner == null) continue; - foundReposMap.set( - `${remote.provider.id.toLowerCase()}/${remote.provider.owner.toLowerCase()}/${remote.provider.path - .split('/') - .pop() - ?.toLowerCase()}`, - repo, - ); - } - } + const foundRepos = await this.getRepositoriesInParentFolder(cancellation); + if (foundRepos == null || foundRepos.length === 0 || cancellation?.isCancellationRequested) return; - for (const repoDescriptor of repoDescriptors) { - const foundRepo = - foundReposMap.get( - `${repoDescriptor.provider.toLowerCase()}/${repoDescriptor.provider_organization_id.toLowerCase()}/${repoDescriptor.name.toLowerCase()}`, - ) ?? foundReposNameMap.get(repoDescriptor.name.toLowerCase()); - if (foundRepo != null) { - await this.locateWorkspaceRepo(workspaceId, repoDescriptor, foundRepo); + for (const repoMatch of ( + await this.resolveWorkspaceRepositoriesByName(workspaceId, { + cancellation: cancellation, + repositories: foundRepos, + }) + ).values()) { + await this.locateWorkspaceRepo(workspaceId, repoMatch.descriptor, repoMatch.repository); - if (cancellation?.isCancellationRequested) return; - } + if (cancellation?.isCancellationRequested) return; } } @@ -385,13 +385,12 @@ export class WorkspacesService implements Disposable { const disposables: Disposable[] = []; let workspaceName: string | undefined; - let workspaceDescription = ''; + let workspaceDescription: string | undefined; let hostUrl: string | undefined; let azureOrganizationName: string | undefined; let azureProjectName: string | undefined; let workspaceProvider: CloudWorkspaceProviderInputType | undefined; - let matchingProviderRepos: Repository[] = []; if (options?.repos != null && options.repos.length > 0) { // Currently only GitHub is supported. for (const repo of options.repos) { @@ -406,10 +405,8 @@ export class WorkspacesService implements Disposable { } workspaceProvider = CloudWorkspaceProviderInputType.GitHub; - matchingProviderRepos = options.repos; } - let includeReposResponse; try { workspaceName = await new Promise(resolve => { disposables.push( @@ -432,12 +429,17 @@ export class WorkspacesService implements Disposable { if (!workspaceName) return; - workspaceDescription = await new Promise(resolve => { + workspaceDescription = await new Promise(resolve => { disposables.push( - input.onDidHide(() => resolve('')), + input.onDidHide(() => resolve(undefined)), input.onDidAccept(() => { const value = input.value.trim(); - resolve(value || ''); + if (!value) { + input.validationMessage = 'Please enter a non-empty description for the workspace'; + return; + } + + resolve(value); }), ); @@ -448,6 +450,8 @@ export class WorkspacesService implements Disposable { input.show(); }); + if (!workspaceDescription) return; + if (workspaceProvider == null) { workspaceProvider = await new Promise(resolve => { disposables.push( @@ -541,27 +545,6 @@ export class WorkspacesService implements Disposable { if (!azureProjectName) return; } - - if (workspaceProvider != null && matchingProviderRepos.length === 0) { - for (const repo of this.container.git.openRepositories) { - const matchingRemotes = await repo.getRemotes({ - filter: r => - r.provider?.id === cloudWorkspaceProviderInputTypeToRemoteProviderId[workspaceProvider!], - }); - if (matchingRemotes.length) { - matchingProviderRepos.push(repo); - } - } - - if (matchingProviderRepos.length) { - includeReposResponse = await window.showInformationMessage( - 'Would you like to include your open repositories in the workspace?', - { modal: true }, - { title: 'Yes' }, - { title: 'No', isCloseAffordance: true }, - ); - } - } } finally { input.dispose(); quickpick.dispose(); @@ -603,43 +586,11 @@ export class WorkspacesService implements Disposable { ); const newWorkspace = this.getCloudWorkspace(createdProjectData.id); - if (newWorkspace != null && (includeReposResponse?.title === 'Yes' || options?.repos)) { - const repoInputs: { repo: Repository; inputDescriptor: AddWorkspaceRepoDescriptor }[] = []; - for (const repo of matchingProviderRepos) { - const remote = (await repo.getRemote('origin')) || (await repo.getRemotes())?.[0]; - const remoteOwnerAndName = remote?.provider?.path?.split('/') || remote?.path?.split('/'); - if (remoteOwnerAndName == null || remoteOwnerAndName.length !== 2) continue; - repoInputs.push({ - repo: repo, - inputDescriptor: { owner: remoteOwnerAndName[0], repoName: remoteOwnerAndName[1] }, - }); - } - - if (repoInputs.length) { - let newRepoDescriptors: CloudWorkspaceRepositoryDescriptor[] = []; - try { - const response = await this._workspacesApi.addReposToWorkspace( - newWorkspace.id, - repoInputs.map(r => r.inputDescriptor), - ); - if (response?.data.add_repositories_to_project == null) return; - newRepoDescriptors = Object.values( - response.data.add_repositories_to_project.provider_data, - ) as CloudWorkspaceRepositoryDescriptor[]; - } catch { - return; - } - - if (newRepoDescriptors.length === 0) return; - newWorkspace.addRepositories(newRepoDescriptors); - for (const repoInput of repoInputs) { - const repoDescriptor = newRepoDescriptors.find( - r => r.name === repoInput.inputDescriptor.repoName, - ); - if (!repoDescriptor) continue; - await this.locateWorkspaceRepo(newWorkspace.id, repoDescriptor, repoInput.repo); - } - } + if (newWorkspace != null) { + await this.addCloudWorkspaceRepos(newWorkspace.id, { + repos: options?.repos, + suppressNotifications: true, + }); } } } @@ -661,58 +612,151 @@ export class WorkspacesService implements Disposable { } catch {} } - async addCloudWorkspaceRepo(workspaceId: string) { - const workspace = this.getCloudWorkspace(workspaceId); - if (workspace == null) return; - - const matchingProviderRepos = []; - for (const repo of this.container.git.openRepositories) { + private async filterReposForProvider( + repos: Repository[], + provider: CloudWorkspaceProviderType, + ): Promise { + const validRepos: Repository[] = []; + for (const repo of repos) { const matchingRemotes = await repo.getRemotes({ - filter: r => r.provider?.id === cloudWorkspaceProviderTypeToRemoteProviderId[workspace.provider], + filter: r => r.provider?.id === cloudWorkspaceProviderTypeToRemoteProviderId[provider], }); if (matchingRemotes.length) { - matchingProviderRepos.push(repo); + validRepos.push(repo); } } - if (!matchingProviderRepos.length) { - void window.showInformationMessage(`No open repositories found for provider ${workspace.provider}`); - return; - } + return validRepos; + } - const pick = await showRepositoryPicker( - 'Add Repository to Workspace', - 'Choose which repository to add to the workspace', - matchingProviderRepos, - ); - if (pick?.item == null) return; + private async filterReposForCloudWorkspace(repos: Repository[], workspaceId: string): Promise { + const workspaceRepos = [ + ...( + await this.resolveWorkspaceRepositoriesByName(workspaceId, { + resolveFromPath: true, + usePathMapping: true, + }) + ).values(), + ].map(match => match.repository); + return repos.filter(repo => !workspaceRepos.find(r => r.id === repo.id)); + } - const repoPath = pick.repoPath; - const repo = this.container.git.getRepository(repoPath); - if (repo == null) return; + async addCloudWorkspaceRepos( + workspaceId: string, + options?: { repos?: Repository[]; suppressNotifications?: boolean }, + ) { + const workspace = this.getCloudWorkspace(workspaceId); + if (workspace == null) return; - const remote = (await repo.getRemote('origin')) || (await repo.getRemotes())?.[0]; - const remoteOwnerAndName = remote?.provider?.path?.split('/') || remote?.path?.split('/'); - if (remoteOwnerAndName == null || remoteOwnerAndName.length !== 2) return; + const repoInputs: (AddWorkspaceRepoDescriptor & { repo: Repository })[] = []; + let reposOrRepoPaths: Repository[] | string[] | undefined = options?.repos; + if (!options?.repos) { + let validRepos = await this.filterReposForProvider(this.container.git.openRepositories, workspace.provider); + validRepos = await this.filterReposForCloudWorkspace(validRepos, workspaceId); + const choices: { + label: string; + description?: string; + choice: WorkspaceAddRepositoriesChoice; + picked?: boolean; + }[] = [ + { + label: 'Choose repositories from a folder', + description: undefined, + choice: WorkspaceAddRepositoriesChoice.ParentFolder, + }, + ]; + + if (validRepos.length > 0) { + choices.unshift({ + label: 'Choose repositories from the current window', + description: undefined, + choice: WorkspaceAddRepositoriesChoice.CurrentWindow, + }); + } + + choices[0].picked = true; + + const repoChoice = await window.showQuickPick(choices, { + placeHolder: 'Choose repositories from the current window or a folder', + ignoreFocusOut: true, + }); + + if (repoChoice == null) return; + + if (repoChoice.choice === WorkspaceAddRepositoriesChoice.ParentFolder) { + const foundRepos = await this.getRepositoriesInParentFolder(); + if (foundRepos == null) return; + validRepos = await this.filterReposForProvider(foundRepos, workspace.provider); + if (validRepos.length === 0) { + if (!options?.suppressNotifications) { + void window.showInformationMessage( + `No matching repositories found for provider ${workspace.provider}.`, + { + modal: true, + }, + ); + } + return; + } + + validRepos = await this.filterReposForCloudWorkspace(validRepos, workspaceId); + if (validRepos.length === 0) { + if (!options?.suppressNotifications) { + void window.showInformationMessage(`All possible repositories are already in this workspace.`, { + modal: true, + }); + } + return; + } + } + + const pick = await showRepositoriesPicker( + 'Add Repositories to Workspace', + 'Choose which repositories to add to the workspace', + validRepos, + ); + if (pick.length === 0) return; + reposOrRepoPaths = pick.map(p => p.repoPath); + } + + if (reposOrRepoPaths == null) return; + for (const repoOrPath of reposOrRepoPaths) { + const repo = + repoOrPath instanceof Repository + ? repoOrPath + : await this.container.git.getOrOpenRepository(Uri.file(repoOrPath), { closeOnOpen: true }); + if (repo == null) continue; + const remote = (await repo.getRemote('origin')) || (await repo.getRemotes())?.[0]; + const remoteDescriptor = getRemoteDescriptor(remote); + if (remoteDescriptor == null) continue; + repoInputs.push({ owner: remoteDescriptor.owner, repoName: remoteDescriptor.repoName, repo: repo }); + } + + if (repoInputs.length === 0) return; let newRepoDescriptors: CloudWorkspaceRepositoryDescriptor[] = []; try { - const response = await this._workspacesApi.addReposToWorkspace(workspaceId, [ - { owner: remoteOwnerAndName[0], repoName: remoteOwnerAndName[1] }, - ]); + const response = await this._workspacesApi.addReposToWorkspace( + workspaceId, + repoInputs.map(r => ({ owner: r.owner, repoName: r.repoName })), + ); if (response?.data.add_repositories_to_project == null) return; - newRepoDescriptors = Object.values( - response.data.add_repositories_to_project.provider_data, + newRepoDescriptors = Object.values(response.data.add_repositories_to_project.provider_data).map( + descriptor => ({ ...descriptor, workspaceId: workspaceId }), ) as CloudWorkspaceRepositoryDescriptor[]; } catch { return; } if (newRepoDescriptors.length === 0) return; - workspace.addRepositories(newRepoDescriptors); - await this.locateWorkspaceRepo(workspaceId, newRepoDescriptors[0], repo); + + for (const { repo, repoName } of repoInputs) { + const successfullyAddedDescriptor = newRepoDescriptors.find(r => r.name === repoName); + if (successfullyAddedDescriptor == null) continue; + await this.locateWorkspaceRepo(workspaceId, successfullyAddedDescriptor, repo); + } } async removeCloudWorkspaceRepo(workspaceId: string, descriptor: CloudWorkspaceRepositoryDescriptor) { @@ -739,94 +783,88 @@ export class WorkspacesService implements Disposable { async resolveWorkspaceRepositoriesByName( workspaceId: string, - workspaceType: WorkspaceType, + options?: { + cancellation?: CancellationToken; + repositories?: Repository[]; + resolveFromPath?: boolean; + usePathMapping?: boolean; + }, ): Promise { - const workspaceRepositoriesByName: WorkspaceRepositoriesByName = new Map(); + const workspaceRepositoriesByName: WorkspaceRepositoriesByName = new Map(); const workspace: GKCloudWorkspace | GKLocalWorkspace | undefined = - workspaceType === WorkspaceType.Cloud - ? this.getCloudWorkspace(workspaceId) - : this.getLocalWorkspace(workspaceId); - - if (workspace?.repositories == null) return workspaceRepositoriesByName; - for (const repository of workspace.repositories) { - const currentRepositories = this.container.git.repositories; - let repo: Repository | undefined = undefined; - let repoId: string | undefined = undefined; - let repoLocalPath: string | undefined = undefined; - let repoRemoteUrl: string | undefined = undefined; - let repoName: string | undefined = undefined; - let repoProvider: string | undefined = undefined; - let repoOwner: string | undefined = undefined; - if (workspaceType === WorkspaceType.Local) { - repoLocalPath = (repository as LocalWorkspaceRepositoryDescriptor).localPath; - // repo name in this case is the last part of the path after splitting from the path separator - repoName = (repository as LocalWorkspaceRepositoryDescriptor).name; - for (const currentRepository of currentRepositories) { - if (currentRepository.path.replaceAll('\\', '/') === repoLocalPath.replaceAll('\\', '/')) { - repo = currentRepository; - } - } - } else if (workspaceType === WorkspaceType.Cloud) { - repoId = (repository as CloudWorkspaceRepositoryDescriptor).id; - repoLocalPath = await this.getCloudWorkspaceRepoPath(workspaceId, repoId); - repoRemoteUrl = (repository as CloudWorkspaceRepositoryDescriptor).url; - repoName = (repository as CloudWorkspaceRepositoryDescriptor).name; - repoProvider = (repository as CloudWorkspaceRepositoryDescriptor).provider; - repoOwner = (repository as CloudWorkspaceRepositoryDescriptor).provider_organization_id; - - if (repoLocalPath == null) { - const repoLocalPaths = await this.container.repositoryPathMapping.getLocalRepoPaths({ - remoteUrl: repoRemoteUrl, - repoInfo: { - repoName: repoName, - provider: repoProvider, - owner: repoOwner, - }, - }); - - // TODO@ramint: The user should be able to choose which path to use if multiple available - if (repoLocalPaths.length > 0) { - repoLocalPath = repoLocalPaths[0]; - } + this.getCloudWorkspace(workspaceId) ?? this.getLocalWorkspace(workspaceId); + + if (workspace?.repositories == null || workspace.repositories.length === 0) return workspaceRepositoriesByName; + const currentRepositories = options?.repositories ?? this.container.git.repositories; + + const reposProviderMap = new Map(); + const reposPathMap = new Map(); + for (const repo of currentRepositories) { + if (options?.cancellation?.isCancellationRequested) break; + reposPathMap.set(normalizePath(repo.uri.fsPath.toLowerCase()), repo); + + if (workspace instanceof GKCloudWorkspace) { + const remotes = await repo.getRemotes(); + for (const remote of remotes) { + const remoteDescriptor = getRemoteDescriptor(remote); + if (remoteDescriptor == null) continue; + reposProviderMap.set( + `${remoteDescriptor.provider}/${remoteDescriptor.owner}/${remoteDescriptor.repoName}`, + repo, + ); } + } + } - for (const currentRepository of currentRepositories) { - if ( - repoLocalPath != null && - currentRepository.path.replaceAll('\\', '/') === repoLocalPath.replaceAll('\\', '/') - ) { - repo = currentRepository; - } - } + for (const descriptor of workspace.repositories) { + let repoLocalPath = null; + let foundRepo = null; + + // Local workspace repo descriptors should match on local path + if (descriptor.id == null) { + repoLocalPath = descriptor.localPath; + // Cloud workspace repo descriptors should match on either provider/owner/name or url on any remote + } else if (options?.usePathMapping === true) { + repoLocalPath = await this.getMappedPathForCloudWorkspaceRepoDescriptor(descriptor); } - // TODO: Add this logic back in once we think through virtual repository support a bit more. - // We want to support virtual repositories not just as an automatic backup, but as a user choice. - /*if (!repo) { - let uri: Uri | undefined = undefined; - if (repoLocalPath) { - uri = Uri.file(repoLocalPath); - } else if (repoRemoteUrl) { - uri = Uri.parse(repoRemoteUrl); - uri = uri.with({ - scheme: Schemes.Virtual, - authority: encodeAuthority('github'), - path: uri.path, - }); - } - if (uri) { - repo = await this.container.git.getOrOpenRepository(uri, { closeOnOpen: true }); - } - }*/ - if (repoLocalPath != null && !repo) { - repo = await this.container.git.getOrOpenRepository(Uri.file(repoLocalPath), { closeOnOpen: true }); + if (repoLocalPath != null) { + foundRepo = reposPathMap.get(normalizePath(repoLocalPath.toLowerCase())); + } + + if (foundRepo == null && descriptor.id != null && descriptor.provider != null) { + foundRepo = reposProviderMap.get( + `${descriptor.provider.toLowerCase()}/${descriptor.provider_organization_id.toLowerCase()}/${descriptor.name.toLowerCase()}`, + ); } - if (!repoName || !repo) { - continue; + if (repoLocalPath != null && foundRepo == null && options?.resolveFromPath === true) { + foundRepo = await this.container.git.getOrOpenRepository(Uri.file(repoLocalPath), { + closeOnOpen: true, + }); + // TODO: Add this logic back in once we think through virtual repository support a bit more. + // We want to support virtual repositories not just as an automatic backup, but as a user choice. + /*if (!foundRepo) { + let uri: Uri | undefined = undefined; + if (repoLocalPath) { + uri = Uri.file(repoLocalPath); + } else if (descriptor.url) { + uri = Uri.parse(descriptor.url); + uri = uri.with({ + scheme: Schemes.Virtual, + authority: encodeAuthority('github'), + path: uri.path, + }); + } + if (uri) { + foundRepo = await this.container.git.getOrOpenRepository(uri, { closeOnOpen: true }); + } + }*/ } - workspaceRepositoriesByName.set(repoName, repo); + if (foundRepo != null) { + workspaceRepositoriesByName.set(descriptor.name, { descriptor: descriptor, repository: foundRepo }); + } } return workspaceRepositoriesByName; @@ -844,7 +882,10 @@ export class WorkspacesService implements Disposable { if (workspace?.repositories == null) return; - const workspaceRepositoriesByName = await this.resolveWorkspaceRepositoriesByName(workspaceId, workspaceType); + const workspaceRepositoriesByName = await this.resolveWorkspaceRepositoriesByName(workspaceId, { + resolveFromPath: true, + usePathMapping: true, + }); if (workspaceRepositoriesByName.size === 0) { void window.showErrorMessage('No repositories could be found in this workspace.', { modal: true }); @@ -852,9 +893,10 @@ export class WorkspacesService implements Disposable { } const workspaceFolderPaths: string[] = []; - for (const repo of workspaceRepositoriesByName.values()) { - if (!repo.virtual && repo.path != null) { - workspaceFolderPaths.push(repo.path); + for (const repoMatch of workspaceRepositoriesByName.values()) { + const repo = repoMatch.repository; + if (!repo.virtual) { + workspaceFolderPaths.push(repo.uri.fsPath); } } @@ -894,6 +936,37 @@ export class WorkspacesService implements Disposable { openWorkspace(newWorkspaceUri, { location: OpenWorkspaceLocation.NewWindow }); } } + + private async getMappedPathForCloudWorkspaceRepoDescriptor( + descriptor: CloudWorkspaceRepositoryDescriptor, + ): Promise { + let repoLocalPath = await this.getCloudWorkspaceRepoPath(descriptor.workspaceId, descriptor.id); + if (repoLocalPath == null) { + repoLocalPath = ( + await this.container.repositoryPathMapping.getLocalRepoPaths({ + remoteUrl: descriptor.url, + repoInfo: { + repoName: descriptor.name, + provider: descriptor.provider, + owner: descriptor.provider_organization_id, + }, + }) + )?.[0]; + } + + return repoLocalPath; + } +} + +function getRemoteDescriptor(remote: GitRemote): RemoteDescriptor | undefined { + if (remote.provider?.owner == null) return undefined; + const remoteRepoName = remote.provider.path.split('/').pop(); + if (remoteRepoName == null) return undefined; + return { + provider: remote.provider.id.toLowerCase(), + owner: remote.provider.owner.toLowerCase(), + repoName: remoteRepoName.toLowerCase(), + }; } // TODO: Add back in once we think through virtual repository support a bit more. diff --git a/src/views/nodes/repositoriesNode.ts b/src/views/nodes/repositoriesNode.ts index a8aa659..9746bfc 100644 --- a/src/views/nodes/repositoriesNode.ts +++ b/src/views/nodes/repositoriesNode.ts @@ -1,5 +1,5 @@ import type { TextEditor } from 'vscode'; -import { Disposable, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; +import { Disposable, TreeItem, TreeItemCollapsibleState, window, workspace } from 'vscode'; import type { RepositoriesChangeEvent } from '../../git/gitProviderService'; import { GitUri, unknownGitUri } from '../../git/gitUri'; import { gate } from '../../system/decorators/gate'; @@ -55,6 +55,11 @@ export class RepositoriesNode extends SubscribeableViewNode { async getChildren(): Promise { if (this._children == null) { this._children = []; - let repositories: CloudWorkspaceRepositoryDescriptor[] | LocalWorkspaceRepositoryDescriptor[] | undefined; + let descriptors: CloudWorkspaceRepositoryDescriptor[] | LocalWorkspaceRepositoryDescriptor[] | undefined; let repositoryInfo: string | undefined; if (this.workspace instanceof GKLocalWorkspace) { - repositories = (await this.getRepositories()) ?? []; + descriptors = (await this.getRepositories()) ?? []; } else { const { repositories: repos, repositoriesInfo: repoInfo } = await this.workspace.getOrLoadRepositories(); - repositories = repos; + descriptors = repos; repositoryInfo = repoInfo; } - if (repositories?.length === 0) { + if (descriptors?.length === 0) { this._children.push(new MessageNode(this.view, this, 'No repositories in this workspace.')); return this._children; - } else if (repositories?.length) { + } else if (descriptors?.length) { const reposByName: WorkspaceRepositoriesByName = - await this.view.container.workspaces.resolveWorkspaceRepositoriesByName( - this.workspaceId, - this.type, - ); + await this.view.container.workspaces.resolveWorkspaceRepositoriesByName(this.workspaceId, { + resolveFromPath: true, + usePathMapping: true, + }); - for (const repository of repositories) { - const repo = reposByName.get(repository.name); + for (const descriptor of descriptors) { + const repo = reposByName.get(descriptor.name)?.repository; if (!repo) { this._children.push( - new WorkspaceMissingRepositoryNode(this.view, this, this.workspaceId, repository), + new WorkspaceMissingRepositoryNode(this.view, this, this.workspaceId, descriptor), ); continue; } @@ -92,7 +92,7 @@ export class WorkspaceNode extends ViewNode { this._children.push( new RepositoryNode(GitUri.fromRepoPath(repo.path), this.view, this, repo, { workspace: this._workspace, - workspaceRepoDescriptor: repository, + workspaceRepoDescriptor: descriptor, }), ); } diff --git a/src/views/workspacesView.ts b/src/views/workspacesView.ts index a1894a8..30f0251 100644 --- a/src/views/workspacesView.ts +++ b/src/views/workspacesView.ts @@ -189,8 +189,8 @@ export class WorkspacesView extends ViewBase { - await this.container.workspaces.addCloudWorkspaceRepo(node.workspaceId); + registerViewCommand(this.getQualifiedCommand('addRepos'), async (node: WorkspaceNode) => { + await this.container.workspaces.addCloudWorkspaceRepos(node.workspaceId); void node.getParent()?.triggerChange(true); }), registerViewCommand(