Kaynağa Gözat

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
main
Ramin Tadayon 1 yıl önce
işlemeyi yapan: GitHub
ebeveyn
işleme
6178fa34fd
Veri tabanında bu imza için bilinen anahtar bulunamadı GPG Anahtar Kimliği: 4AEE18F83AFDEB23
6 değiştirilmiş dosya ile 343 ekleme ve 247 silme
  1. +5
    -5
      package.json
  2. +19
    -1
      src/plus/workspaces/models.ts
  3. +298
    -225
      src/plus/workspaces/workspacesService.ts
  4. +6
    -1
      src/views/nodes/repositoriesNode.ts
  5. +13
    -13
      src/views/nodes/workspaceNode.ts
  6. +2
    -2
      src/views/workspacesView.ts

+ 5
- 5
package.json Dosyayı Görüntüle

@ -6905,8 +6905,8 @@
"enablement": "!operationInProgress" "enablement": "!operationInProgress"
}, },
{ {
"command": "gitlens.views.workspaces.addRepo",
"title": "Add Repository to Workspace...",
"command": "gitlens.views.workspaces.addRepos",
"title": "Add Repositories to Workspace...",
"category": "GitLens", "category": "GitLens",
"icon": "$(add)" "icon": "$(add)"
}, },
@ -9433,7 +9433,7 @@
"when": "false" "when": "false"
}, },
{ {
"command": "gitlens.views.workspaces.addRepo",
"command": "gitlens.views.workspaces.addRepos",
"when": "false" "when": "false"
}, },
{ {
@ -11058,7 +11058,7 @@
"group": "inline@1" "group": "inline@1"
}, },
{ {
"command": "gitlens.views.workspaces.addRepo",
"command": "gitlens.views.workspaces.addRepos",
"when": "viewItem =~ /gitlens:workspace\\b(?=.*?\\b\\+cloud\\b)/", "when": "viewItem =~ /gitlens:workspace\\b(?=.*?\\b\\+cloud\\b)/",
"group": "inline@2" "group": "inline@2"
}, },
@ -11104,7 +11104,7 @@
"group": "1_gitlens_actions@2" "group": "1_gitlens_actions@2"
}, },
{ {
"command": "gitlens.views.workspaces.addRepo",
"command": "gitlens.views.workspaces.addRepos",
"when": "viewItem =~ /gitlens:workspace\\b(?=.*?\\b\\+cloud\\b)/", "when": "viewItem =~ /gitlens:workspace\\b(?=.*?\\b\\+cloud\\b)/",
"group": "1_gitlens_actions@3" "group": "1_gitlens_actions@3"
}, },

+ 19
- 1
src/plus/workspaces/models.ts Dosyayı Görüntüle

@ -10,7 +10,18 @@ export type CodeWorkspaceFileContents = {
settings: { [key: string]: any }; settings: { [key: string]: any };
}; };
export type WorkspaceRepositoriesByName = Map<string, Repository>;
export type WorkspaceRepositoriesByName = Map<string, RepositoryMatch>;
export interface RepositoryMatch {
repository: Repository;
descriptor: CloudWorkspaceRepositoryDescriptor | LocalWorkspaceRepositoryDescriptor;
}
export interface RemoteDescriptor {
provider: string;
owner: string;
repoName: string;
}
export interface GetWorkspacesResponse { export interface GetWorkspacesResponse {
cloudWorkspaces: GKCloudWorkspace[]; cloudWorkspaces: GKCloudWorkspace[];
@ -124,6 +135,7 @@ export interface CloudWorkspaceRepositoryDescriptor {
provider_organization_id: string; provider_organization_id: string;
provider_organization_name: string; provider_organization_name: string;
url: string; url: string;
workspaceId: string;
} }
export enum CloudWorkspaceProviderInputType { export enum CloudWorkspaceProviderInputType {
@ -162,6 +174,11 @@ export const cloudWorkspaceProviderInputTypeToRemoteProviderId = {
[CloudWorkspaceProviderInputType.GitLabSelfHosted]: 'gitlab', [CloudWorkspaceProviderInputType.GitLabSelfHosted]: 'gitlab',
}; };
export enum WorkspaceAddRepositoriesChoice {
CurrentWindow = 'Current Window',
ParentFolder = 'Parent Folder',
}
export const defaultWorkspaceCount = 100; export const defaultWorkspaceCount = 100;
export const defaultWorkspaceRepoCount = 100; export const defaultWorkspaceRepoCount = 100;
@ -538,6 +555,7 @@ export interface LocalWorkspaceRepositoryPath {
export interface LocalWorkspaceRepositoryDescriptor extends LocalWorkspaceRepositoryPath { export interface LocalWorkspaceRepositoryDescriptor extends LocalWorkspaceRepositoryPath {
id?: undefined; id?: undefined;
name: string; name: string;
workspaceId: string;
} }
export interface CloudWorkspaceFileData { export interface CloudWorkspaceFileData {

+ 298
- 225
src/plus/workspaces/workspacesService.ts Dosyayı Görüntüle

@ -2,10 +2,12 @@ import type { CancellationToken, Event } from 'vscode';
import { Disposable, EventEmitter, Uri, window } from 'vscode'; import { Disposable, EventEmitter, Uri, window } from 'vscode';
import { getSupportedWorkspacesPathMappingProvider } from '@env/providers'; import { getSupportedWorkspacesPathMappingProvider } from '@env/providers';
import type { Container } from '../../container'; import type { Container } from '../../container';
import type { GitRemote } from '../../git/models/remote';
import { RemoteResourceType } from '../../git/models/remoteResource'; 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 { SubscriptionState } from '../../subscription';
import { normalizePath } from '../../system/path';
import { openWorkspace, OpenWorkspaceLocation } from '../../system/utils'; import { openWorkspace, OpenWorkspaceLocation } from '../../system/utils';
import type { ServerConnection } from '../subscription/serverConnection'; import type { ServerConnection } from '../subscription/serverConnection';
import type { SubscriptionChangeEvent } from '../subscription/subscriptionService'; import type { SubscriptionChangeEvent } from '../subscription/subscriptionService';
@ -20,15 +22,17 @@ import type {
LoadLocalWorkspacesResponse, LoadLocalWorkspacesResponse,
LocalWorkspaceData, LocalWorkspaceData,
LocalWorkspaceRepositoryDescriptor, LocalWorkspaceRepositoryDescriptor,
RemoteDescriptor,
RepositoryMatch,
WorkspaceRepositoriesByName, WorkspaceRepositoriesByName,
WorkspacesResponse, WorkspacesResponse,
} from './models'; } from './models';
import { import {
CloudWorkspaceProviderInputType, CloudWorkspaceProviderInputType,
cloudWorkspaceProviderInputTypeToRemoteProviderId,
cloudWorkspaceProviderTypeToRemoteProviderId, cloudWorkspaceProviderTypeToRemoteProviderId,
GKCloudWorkspace, GKCloudWorkspace,
GKLocalWorkspace, GKLocalWorkspace,
WorkspaceAddRepositoriesChoice,
WorkspaceType, WorkspaceType,
} from './models'; } from './models';
import { WorkspacesApi } from './workspacesApi'; import { WorkspacesApi } from './workspacesApi';
@ -50,8 +54,12 @@ export class WorkspacesService implements Disposable {
async (workspaceId: string) => { async (workspaceId: string) => {
try { try {
const workspaceRepos = await this._workspacesApi.getWorkspaceRepositories(workspaceId); const workspaceRepos = await this._workspacesApi.getWorkspaceRepositories(workspaceId);
const repoDescriptors = workspaceRepos?.data?.project?.provider_data?.repositories?.nodes;
return { return {
repositories: workspaceRepos?.data?.project?.provider_data?.repositories?.nodes ?? [],
repositories:
repoDescriptors != null
? repoDescriptors.map(descriptor => ({ ...descriptor, workspaceId: workspaceId }))
: [],
repositoriesInfo: undefined, repositoriesInfo: undefined,
}; };
} catch { } catch {
@ -119,8 +127,11 @@ export class WorkspacesService implements Disposable {
continue; 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) { if (repositories == null && !excludeRepositories) {
repositories = []; repositories = [];
} }
@ -160,6 +171,7 @@ export class WorkspacesService implements Disposable {
workspace.repositories.map(repositoryPath => ({ workspace.repositories.map(repositoryPath => ({
localPath: repositoryPath.localPath, localPath: repositoryPath.localPath,
name: repositoryPath.localPath.split(/[\\/]/).pop() ?? 'unknown', 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); await this._workspacesPathProvider.writeCloudWorkspaceDiskPathToMap(workspaceId, repoId, localPath);
} }
async locateAllCloudWorkspaceRepos(workspaceId: string, cancellation?: CancellationToken): Promise<void> {
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<Repository[] | undefined> {
const parentUri = ( const parentUri = (
await window.showOpenDialog({ await window.showOpenDialog({
title: `Choose a folder containing the repositories in this workspace`,
title: `Choose a folder containing repositories for this workspace`,
canSelectFiles: false, canSelectFiles: false,
canSelectFolders: true, canSelectFolders: true,
canSelectMany: false, canSelectMany: false,
}) })
)?.[0]; )?.[0];
if (parentUri == null || cancellation?.isCancellationRequested) return;
if (parentUri == null || cancellation?.isCancellationRequested) return undefined;
let foundRepos;
try { try {
foundRepos = await this.container.git.findRepositories(parentUri, {
return this.container.git.findRepositories(parentUri, {
cancellation: cancellation, cancellation: cancellation,
depth: 1, depth: 1,
silent: true, silent: true,
}); });
} catch (ex) { } catch (ex) {
foundRepos = [];
return;
return undefined;
} }
}
if (foundRepos.length === 0 || cancellation?.isCancellationRequested) return;
async locateAllCloudWorkspaceRepos(workspaceId: string, cancellation?: CancellationToken): Promise<void> {
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<string, Repository>();
const foundReposNameMap = new Map<string, Repository>();
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[] = []; const disposables: Disposable[] = [];
let workspaceName: string | undefined; let workspaceName: string | undefined;
let workspaceDescription = '';
let workspaceDescription: string | undefined;
let hostUrl: string | undefined; let hostUrl: string | undefined;
let azureOrganizationName: string | undefined; let azureOrganizationName: string | undefined;
let azureProjectName: string | undefined; let azureProjectName: string | undefined;
let workspaceProvider: CloudWorkspaceProviderInputType | undefined; let workspaceProvider: CloudWorkspaceProviderInputType | undefined;
let matchingProviderRepos: Repository[] = [];
if (options?.repos != null && options.repos.length > 0) { if (options?.repos != null && options.repos.length > 0) {
// Currently only GitHub is supported. // Currently only GitHub is supported.
for (const repo of options.repos) { for (const repo of options.repos) {
@ -406,10 +405,8 @@ export class WorkspacesService implements Disposable {
} }
workspaceProvider = CloudWorkspaceProviderInputType.GitHub; workspaceProvider = CloudWorkspaceProviderInputType.GitHub;
matchingProviderRepos = options.repos;
} }
let includeReposResponse;
try { try {
workspaceName = await new Promise<string | undefined>(resolve => { workspaceName = await new Promise<string | undefined>(resolve => {
disposables.push( disposables.push(
@ -432,12 +429,17 @@ export class WorkspacesService implements Disposable {
if (!workspaceName) return; if (!workspaceName) return;
workspaceDescription = await new Promise<string>(resolve => {
workspaceDescription = await new Promise<string | undefined>(resolve => {
disposables.push( disposables.push(
input.onDidHide(() => resolve('')),
input.onDidHide(() => resolve(undefined)),
input.onDidAccept(() => { input.onDidAccept(() => {
const value = input.value.trim(); 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(); input.show();
}); });
if (!workspaceDescription) return;
if (workspaceProvider == null) { if (workspaceProvider == null) {
workspaceProvider = await new Promise<CloudWorkspaceProviderInputType | undefined>(resolve => { workspaceProvider = await new Promise<CloudWorkspaceProviderInputType | undefined>(resolve => {
disposables.push( disposables.push(
@ -541,27 +545,6 @@ export class WorkspacesService implements Disposable {
if (!azureProjectName) return; 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 { } finally {
input.dispose(); input.dispose();
quickpick.dispose(); quickpick.dispose();
@ -603,43 +586,11 @@ export class WorkspacesService implements Disposable {
); );
const newWorkspace = this.getCloudWorkspace(createdProjectData.id); 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 {} } 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<Repository[]> {
const validRepos: Repository[] = [];
for (const repo of repos) {
const matchingRemotes = await repo.getRemotes({ const matchingRemotes = await repo.getRemotes({
filter: r => r.provider?.id === cloudWorkspaceProviderTypeToRemoteProviderId[workspace.provider],
filter: r => r.provider?.id === cloudWorkspaceProviderTypeToRemoteProviderId[provider],
}); });
if (matchingRemotes.length) { 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<Repository[]> {
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[] = []; let newRepoDescriptors: CloudWorkspaceRepositoryDescriptor[] = [];
try { 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; 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[]; ) as CloudWorkspaceRepositoryDescriptor[];
} catch { } catch {
return; return;
} }
if (newRepoDescriptors.length === 0) return; if (newRepoDescriptors.length === 0) return;
workspace.addRepositories(newRepoDescriptors); 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) { async removeCloudWorkspaceRepo(workspaceId: string, descriptor: CloudWorkspaceRepositoryDescriptor) {
@ -739,94 +783,88 @@ export class WorkspacesService implements Disposable {
async resolveWorkspaceRepositoriesByName( async resolveWorkspaceRepositoriesByName(
workspaceId: string, workspaceId: string,
workspaceType: WorkspaceType,
options?: {
cancellation?: CancellationToken;
repositories?: Repository[];
resolveFromPath?: boolean;
usePathMapping?: boolean;
},
): Promise<WorkspaceRepositoriesByName> { ): Promise<WorkspaceRepositoriesByName> {
const workspaceRepositoriesByName: WorkspaceRepositoriesByName = new Map<string, Repository>();
const workspaceRepositoriesByName: WorkspaceRepositoriesByName = new Map<string, RepositoryMatch>();
const workspace: GKCloudWorkspace | GKLocalWorkspace | undefined = 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<string, Repository>();
const reposPathMap = new Map<string, Repository>();
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<GitHubAuthorityMetadata>('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<GitHubAuthorityMetadata>('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; return workspaceRepositoriesByName;
@ -844,7 +882,10 @@ export class WorkspacesService implements Disposable {
if (workspace?.repositories == null) return; 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) { if (workspaceRepositoriesByName.size === 0) {
void window.showErrorMessage('No repositories could be found in this workspace.', { modal: true }); 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[] = []; 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 }); openWorkspace(newWorkspaceUri, { location: OpenWorkspaceLocation.NewWindow });
} }
} }
private async getMappedPathForCloudWorkspaceRepoDescriptor(
descriptor: CloudWorkspaceRepositoryDescriptor,
): Promise<string | undefined> {
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. // TODO: Add back in once we think through virtual repository support a bit more.

+ 6
- 1
src/views/nodes/repositoriesNode.ts Dosyayı Görüntüle

@ -1,5 +1,5 @@
import type { TextEditor } from 'vscode'; 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 type { RepositoriesChangeEvent } from '../../git/gitProviderService';
import { GitUri, unknownGitUri } from '../../git/gitUri'; import { GitUri, unknownGitUri } from '../../git/gitUri';
import { gate } from '../../system/decorators/gate'; import { gate } from '../../system/decorators/gate';
@ -55,6 +55,11 @@ export class RepositoriesNode extends SubscribeableViewNode
isInWorkspacesView ? 'Current Window' : 'Repositories', isInWorkspacesView ? 'Current Window' : 'Repositories',
isInWorkspacesView ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.Expanded, isInWorkspacesView ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.Expanded,
); );
if (isInWorkspacesView) {
item.description = workspace.name ?? workspace.workspaceFolders?.[0]?.name ?? '';
}
let contextValue: string = ContextValues.Repositories; let contextValue: string = ContextValues.Repositories;
if (isInWorkspacesView) { if (isInWorkspacesView) {
contextValue += '+workspaces'; contextValue += '+workspaces';

+ 13
- 13
src/views/nodes/workspaceNode.ts Dosyayı Görüntüle

@ -59,32 +59,32 @@ export class WorkspaceNode extends ViewNode {
async getChildren(): Promise<ViewNode[]> { async getChildren(): Promise<ViewNode[]> {
if (this._children == null) { if (this._children == null) {
this._children = []; this._children = [];
let repositories: CloudWorkspaceRepositoryDescriptor[] | LocalWorkspaceRepositoryDescriptor[] | undefined;
let descriptors: CloudWorkspaceRepositoryDescriptor[] | LocalWorkspaceRepositoryDescriptor[] | undefined;
let repositoryInfo: string | undefined; let repositoryInfo: string | undefined;
if (this.workspace instanceof GKLocalWorkspace) { if (this.workspace instanceof GKLocalWorkspace) {
repositories = (await this.getRepositories()) ?? [];
descriptors = (await this.getRepositories()) ?? [];
} else { } else {
const { repositories: repos, repositoriesInfo: repoInfo } = const { repositories: repos, repositoriesInfo: repoInfo } =
await this.workspace.getOrLoadRepositories(); await this.workspace.getOrLoadRepositories();
repositories = repos;
descriptors = repos;
repositoryInfo = repoInfo; repositoryInfo = repoInfo;
} }
if (repositories?.length === 0) {
if (descriptors?.length === 0) {
this._children.push(new MessageNode(this.view, this, 'No repositories in this workspace.')); this._children.push(new MessageNode(this.view, this, 'No repositories in this workspace.'));
return this._children; return this._children;
} else if (repositories?.length) {
} else if (descriptors?.length) {
const reposByName: WorkspaceRepositoriesByName = 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) { if (!repo) {
this._children.push( this._children.push(
new WorkspaceMissingRepositoryNode(this.view, this, this.workspaceId, repository),
new WorkspaceMissingRepositoryNode(this.view, this, this.workspaceId, descriptor),
); );
continue; continue;
} }
@ -92,7 +92,7 @@ export class WorkspaceNode extends ViewNode {
this._children.push( this._children.push(
new RepositoryNode(GitUri.fromRepoPath(repo.path), this.view, this, repo, { new RepositoryNode(GitUri.fromRepoPath(repo.path), this.view, this, repo, {
workspace: this._workspace, workspace: this._workspace,
workspaceRepoDescriptor: repository,
workspaceRepoDescriptor: descriptor,
}), }),
); );
} }

+ 2
- 2
src/views/workspacesView.ts Dosyayı Görüntüle

@ -189,8 +189,8 @@ export class WorkspacesView extends ViewBase
}, },
this, this,
), ),
registerViewCommand(this.getQualifiedCommand('addRepo'), async (node: WorkspaceNode) => {
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); void node.getParent()?.triggerChange(true);
}), }),
registerViewCommand( registerViewCommand(

Yükleniyor…
İptal
Kaydet