From c39ec450497b203923e1b987b0a7f04d9b82cca1 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 26 Jan 2025 16:07:53 -0500 Subject: [PATCH] Refactors shared location storage - Uses XDG env vars on all OSs - Avoids singleton & adds to container - Avoids empty implementations for web - Renames files and methods for better clarity - Removes dependency on xdg-basedir package Adds new Lazy type & lazy method Adds new UnifiedDisposable & UnifiedAsyncDisposable --- ThirdPartyNotices.txt | 18 +-- package.json | 3 +- pnpm-lock.yaml | 9 -- src/container.ts | 59 ++++++-- .../repositoryWebPathMappingProvider.ts | 21 --- .../workspacesWebPathMappingProvider.ts | 50 ------- src/env/browser/providers.ts | 23 ++- .../gk/localRepositoryLocationProvider.ts | 121 +++++++++++++++ .../localSharedGkStorageLocationProvider.ts | 140 +++++++++++++++++ .../localWorkspacesSharedStorageProvider.ts} | 87 ++++++----- .../repositoryLocalPathMappingProvider.ts | 111 -------------- .../node/pathMapping/sharedGKDataFolder.ts | 141 ------------------ src/env/node/providers.ts | 26 +++- src/git/gitProviderService.ts | 8 +- .../location/repositorylocationProvider.ts | 14 ++ .../repositoryPathMappingProvider.ts | 13 -- src/plus/repos/repositoryIdentityService.ts | 52 +++---- .../repos/sharedGkStorageLocationProvider.ts | 11 ++ src/plus/workspaces/workspacesService.ts | 85 +++++------ ....ts => workspacesSharedStorageProvider.ts} | 14 +- src/system/lazy.ts | 26 ++++ src/system/unifiedDisposable.ts | 22 +++ src/uris/deepLinks/deepLinkService.ts | 12 +- 23 files changed, 541 insertions(+), 525 deletions(-) delete mode 100644 src/env/browser/pathMapping/repositoryWebPathMappingProvider.ts delete mode 100644 src/env/browser/pathMapping/workspacesWebPathMappingProvider.ts create mode 100644 src/env/node/gk/localRepositoryLocationProvider.ts create mode 100644 src/env/node/gk/localSharedGkStorageLocationProvider.ts rename src/env/node/{pathMapping/workspacesLocalPathMappingProvider.ts => gk/localWorkspacesSharedStorageProvider.ts} (72%) delete mode 100644 src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts delete mode 100644 src/env/node/pathMapping/sharedGKDataFolder.ts create mode 100644 src/git/location/repositorylocationProvider.ts delete mode 100644 src/git/pathMapping/repositoryPathMappingProvider.ts create mode 100644 src/plus/repos/sharedGkStorageLocationProvider.ts rename src/plus/workspaces/{workspacesPathMappingProvider.ts => workspacesSharedStorageProvider.ts} (63%) create mode 100644 src/system/lazy.ts create mode 100644 src/system/unifiedDisposable.ts diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 3936644ab86b5..3fec1f460dece 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -33,7 +33,6 @@ This project incorporates components from the projects listed below. 28. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils) 29. slug version 10.0.0 (https://github.com/Trott/slug) 30. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable) -31. xdg-basedir version 5.1.0 (https://github.com/sindresorhus/xdg-basedir) %% @gk-nzaytsev/fast-string-truncated-width NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -2245,19 +2244,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF sortablejs NOTICES AND INFORMATION - -%% xdg-basedir NOTICES AND INFORMATION BEGIN HERE -========================================= -MIT License - -Copyright (c) Sindre Sorhus (https://sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF xdg-basedir NOTICES AND INFORMATION \ No newline at end of file +END OF sortablejs NOTICES AND INFORMATION \ No newline at end of file diff --git a/package.json b/package.json index 14cb122e32377..22cd31b130e0a 100644 --- a/package.json +++ b/package.json @@ -20147,8 +20147,7 @@ "react-dom": "16.8.4", "signal-utils": "0.21.1", "slug": "10.0.0", - "sortablejs": "1.15.0", - "xdg-basedir": "5.1.0" + "sortablejs": "1.15.0" }, "devDependencies": { "@eamodio/eslint-lite-webpack-plugin": "0.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c90077d67545..1eef4a5316b20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,9 +110,6 @@ importers: sortablejs: specifier: 1.15.0 version: 1.15.0 - xdg-basedir: - specifier: 5.1.0 - version: 5.1.0 devDependencies: '@eamodio/eslint-lite-webpack-plugin': specifier: 0.2.0 @@ -5481,10 +5478,6 @@ packages: utf-8-validate: optional: true - xdg-basedir@5.1.0: - resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} - engines: {node: '>=12'} - xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -11279,8 +11272,6 @@ snapshots: ws@7.5.10: {} - xdg-basedir@5.1.0: {} - xml2js@0.5.0: dependencies: sax: 1.4.1 diff --git a/src/container.ts b/src/container.ts index c11d895bef8aa..ed456ee26b82d 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,6 +1,11 @@ import type { ConfigurationChangeEvent, Disposable, Event, ExtensionContext } from 'vscode'; import { EventEmitter, ExtensionMode, Uri } from 'vscode'; -import { getSupportedGitProviders, getSupportedRepositoryPathMappingProvider } from '@env/providers'; +import { + getSharedGKStorageLocationProvider, + getSupportedGitProviders, + getSupportedRepositoryLocationProvider, + getSupportedWorkspacesStorageProvider, +} from '@env/providers'; import type { AIProviderService } from './ai/aiProviderService'; import { FileAnnotationController } from './annotations/fileAnnotationController'; import { LineAnnotationController } from './annotations/lineAnnotationController'; @@ -18,7 +23,7 @@ import { GlCommand } from './constants.commands'; import { EventBus } from './eventBus'; import { GitFileSystemProvider } from './git/fsProvider'; import { GitProviderService } from './git/gitProviderService'; -import type { RepositoryPathMappingProvider } from './git/pathMapping/repositoryPathMappingProvider'; +import type { RepositoryLocationProvider } from './git/location/repositorylocationProvider'; import { LineHoverController } from './hovers/lineHoverController'; import { DraftService } from './plus/drafts/draftsService'; import { AccountAuthenticationProvider } from './plus/gk/authenticationProvider'; @@ -35,6 +40,8 @@ import { EnrichmentService } from './plus/launchpad/enrichmentService'; import { LaunchpadIndicator } from './plus/launchpad/launchpadIndicator'; import { LaunchpadProvider } from './plus/launchpad/launchpadProvider'; import { RepositoryIdentityService } from './plus/repos/repositoryIdentityService'; +import type { SharedGkStorageLocationProvider } from './plus/repos/sharedGkStorageLocationProvider'; +import { WorkspacesApi } from './plus/workspaces/workspacesApi'; import { scheduleAddMissingCurrentWorkspaceRepos, WorkspacesService } from './plus/workspaces/workspacesService'; import { StatusBarController } from './statusbar/statusBarController'; import { executeCommand } from './system/-webview/command'; @@ -404,14 +411,6 @@ export class Container { return this._drafts; } - private _repositoryIdentity: RepositoryIdentityService | undefined; - get repositoryIdentity(): RepositoryIdentityService { - if (this._repositoryIdentity == null) { - this._disposables.push((this._repositoryIdentity = new RepositoryIdentityService(this, this._connection))); - } - return this._repositoryIdentity; - } - private readonly _codeLensController: GitCodeLensController; get codeLens(): GitCodeLensController { return this._codeLensController; @@ -587,12 +586,33 @@ export class Container { return this._rebaseEditor; } - private _repositoryPathMapping: RepositoryPathMappingProvider | undefined; - get repositoryPathMapping(): RepositoryPathMappingProvider { - if (this._repositoryPathMapping == null) { - this._disposables.push((this._repositoryPathMapping = getSupportedRepositoryPathMappingProvider(this))); + private _repositoryIdentity: RepositoryIdentityService | undefined; + get repositoryIdentity(): RepositoryIdentityService { + if (this._repositoryIdentity == null) { + this._disposables.push( + (this._repositoryIdentity = new RepositoryIdentityService(this, this.repositoryLocator)), + ); + } + return this._repositoryIdentity; + } + + private _repositoryLocator: RepositoryLocationProvider | null | undefined; + get repositoryLocator(): RepositoryLocationProvider | undefined { + if (this._repositoryLocator === undefined) { + this._repositoryLocator = getSupportedRepositoryLocationProvider(this, this.sharedGkStorage!) ?? null; + if (this._repositoryLocator != null) { + this._disposables.push(this._repositoryLocator); + } + } + return this._repositoryLocator ?? undefined; + } + + private _sharedGkStorage: SharedGkStorageLocationProvider | null | undefined; + private get sharedGkStorage(): SharedGkStorageLocationProvider | undefined { + if (this._sharedGkStorage === undefined) { + this._sharedGkStorage = getSharedGKStorageLocationProvider(this) ?? null; } - return this._repositoryPathMapping; + return this._sharedGkStorage ?? undefined; } private readonly _statusBarController: StatusBarController; @@ -648,7 +668,14 @@ export class Container { private _workspaces: WorkspacesService | undefined; get workspaces(): WorkspacesService { if (this._workspaces == null) { - this._disposables.push((this._workspaces = new WorkspacesService(this, this._connection))); + this._disposables.push( + (this._workspaces = new WorkspacesService( + this, + new WorkspacesApi(this, this._connection), + getSupportedWorkspacesStorageProvider(this, this.sharedGkStorage!), + this.repositoryLocator, + )), + ); } return this._workspaces; } diff --git a/src/env/browser/pathMapping/repositoryWebPathMappingProvider.ts b/src/env/browser/pathMapping/repositoryWebPathMappingProvider.ts deleted file mode 100644 index c6ff80d658469..0000000000000 --- a/src/env/browser/pathMapping/repositoryWebPathMappingProvider.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Disposable } from 'vscode'; -import type { Container } from '../../../container'; -import type { RepositoryPathMappingProvider } from '../../../git/pathMapping/repositoryPathMappingProvider'; - -export class RepositoryWebPathMappingProvider implements RepositoryPathMappingProvider, Disposable { - constructor(private readonly _container: Container) {} - - dispose() {} - - getLocalRepoPaths(_options: { - remoteUrl?: string; - repoInfo?: { provider?: string; owner?: string; repoName?: string }; - }): Promise { - return Promise.resolve([]); - } - - async writeLocalRepoPath( - _options: { remoteUrl?: string; repoInfo?: { provider?: string; owner?: string; repoName?: string } }, - _localPath: string, - ): Promise {} -} diff --git a/src/env/browser/pathMapping/workspacesWebPathMappingProvider.ts b/src/env/browser/pathMapping/workspacesWebPathMappingProvider.ts deleted file mode 100644 index 576475fb2a958..0000000000000 --- a/src/env/browser/pathMapping/workspacesWebPathMappingProvider.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Uri } from 'vscode'; -import type { LocalWorkspaceFileData } from '../../../plus/workspaces/models/localWorkspace'; -import type { WorkspaceAutoAddSetting } from '../../../plus/workspaces/models/workspaces'; -import type { WorkspacesPathMappingProvider } from '../../../plus/workspaces/workspacesPathMappingProvider'; - -export class WorkspacesWebPathMappingProvider implements WorkspacesPathMappingProvider { - getCloudWorkspaceRepoPath(_cloudWorkspaceId: string, _repoId: string): Promise { - return Promise.resolve(undefined); - } - - getCloudWorkspaceCodeWorkspacePath(_cloudWorkspaceId: string): Promise { - return Promise.resolve(undefined); - } - - async removeCloudWorkspaceCodeWorkspaceFilePath(_cloudWorkspaceId: string): Promise {} - - async writeCloudWorkspaceCodeWorkspaceFilePathToMap( - _cloudWorkspaceId: string, - _codeWorkspaceFilePath: string, - ): Promise {} - - confirmCloudWorkspaceCodeWorkspaceFilePath(_cloudWorkspaceId: string): Promise { - return Promise.resolve(false); - } - - async writeCloudWorkspaceRepoDiskPathToMap( - _cloudWorkspaceId: string, - _repoId: string, - _repoLocalPath: string, - ): Promise {} - - getLocalWorkspaceData(): Promise { - return Promise.resolve({ workspaces: {} }); - } - - writeCodeWorkspaceFile( - _uri: Uri, - _workspaceRepoFilePaths: string[], - _options?: { workspaceId?: string; workspaceAutoAddSetting?: WorkspaceAutoAddSetting }, - ): Promise { - return Promise.resolve(false); - } - - updateCodeWorkspaceFileSettings( - _uri: Uri, - _options: { workspaceAutoAddSetting?: WorkspaceAutoAddSetting }, - ): Promise { - return Promise.resolve(false); - } -} diff --git a/src/env/browser/providers.ts b/src/env/browser/providers.ts index 8970d7be4bc8f..fc1c08137fdd9 100644 --- a/src/env/browser/providers.ts +++ b/src/env/browser/providers.ts @@ -3,9 +3,10 @@ import type { GitCommandOptions } from '../../git/commandOptions'; // Force import of GitHub since dynamic imports are not supported in the WebWorker ExtensionHost // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { GitProvider } from '../../git/gitProvider'; +import type { RepositoryLocationProvider } from '../../git/location/repositorylocationProvider'; import { GitHubGitProvider } from '../../plus/integrations/providers/github/githubGitProvider'; -import { RepositoryWebPathMappingProvider } from './pathMapping/repositoryWebPathMappingProvider'; -import { WorkspacesWebPathMappingProvider } from './pathMapping/workspacesWebPathMappingProvider'; +import type { SharedGkStorageLocationProvider } from '../../plus/repos/sharedGkStorageLocationProvider'; +import type { GkWorkspacesSharedStorageProvider } from '../../plus/workspaces/workspacesSharedStorageProvider'; export function git(_options: GitCommandOptions, ..._args: any[]): Promise { return Promise.resolve(''); @@ -25,10 +26,20 @@ export function getSupportedGitProviders(container: Container): Promise { + await this.ensureLocalRepoDataMap(); + return this._localRepoDataMap ?? {}; + } + + @log() + async getLocation( + remoteUrl: string, + repoInfo?: { provider?: string; owner?: string; repoName?: string }, + ): Promise { + const paths: string[] = []; + if (remoteUrl != null) { + const remoteUrlPaths = await this._getLocalRepoPaths(remoteUrl); + if (remoteUrlPaths != null) { + paths.push(...remoteUrlPaths); + } + } + if (repoInfo != null) { + const { provider, owner, repoName } = repoInfo; + if (provider != null && owner != null && repoName != null) { + const repoInfoPaths = await this._getLocalRepoPaths(`${provider}/${owner}/${repoName}`); + if (repoInfoPaths != null) { + paths.push(...repoInfoPaths); + } + } + } + + return paths; + } + + private async _getLocalRepoPaths(key: string): Promise { + const localRepoDataMap = await this.getLocalRepoDataMap(); + return localRepoDataMap[key]?.paths; + } + + @debug() + private async loadLocalRepoDataMap() { + const scope = getLogScope(); + + const localFileUri = await this.sharedStorage.getSharedRepositoryLocationFileUri(); + try { + const data = await workspace.fs.readFile(localFileUri); + this._localRepoDataMap = (JSON.parse(data.toString()) ?? {}) as LocalRepoDataMap; + } catch (ex) { + Logger.error(ex, scope); + } + } + + @log() + async storeLocation( + path: string, + remoteUrl: string | undefined, + repoInfo?: { provider?: string; owner?: string; repoName?: string }, + ): Promise { + if (remoteUrl != null) { + await this.storeLocationCore(remoteUrl, path); + } + if (repoInfo?.provider != null && repoInfo?.owner != null && repoInfo?.repoName != null) { + const { provider, owner, repoName } = repoInfo; + const key = `${provider}/${owner}/${repoName}`; + await this.storeLocationCore(key, path); + } + } + + @debug() + private async storeLocationCore(key: string, path: string): Promise { + if (!key || !path) return; + + const scope = getLogScope(); + + await using lock = await this.sharedStorage.acquireSharedStorageWriteLock(); + if (lock == null) return; + + await this.loadLocalRepoDataMap(); + if (this._localRepoDataMap == null) { + this._localRepoDataMap = {}; + } + + if (this._localRepoDataMap[key] == null) { + this._localRepoDataMap[key] = { paths: [path] }; + } else if (this._localRepoDataMap[key].paths == null) { + this._localRepoDataMap[key].paths = [path]; + } else if (!this._localRepoDataMap[key].paths.includes(path)) { + this._localRepoDataMap[key].paths.push(path); + } + + const localFileUri = await this.sharedStorage.getSharedRepositoryLocationFileUri(); + const outputData = new Uint8Array(Buffer.from(JSON.stringify(this._localRepoDataMap))); + try { + await workspace.fs.writeFile(localFileUri, outputData); + } catch (ex) { + Logger.error(ex, scope); + } + } +} diff --git a/src/env/node/gk/localSharedGkStorageLocationProvider.ts b/src/env/node/gk/localSharedGkStorageLocationProvider.ts new file mode 100644 index 0000000000000..a7608b8edfa5a --- /dev/null +++ b/src/env/node/gk/localSharedGkStorageLocationProvider.ts @@ -0,0 +1,140 @@ +import { homedir } from 'os'; +import { join } from 'path'; +import { env } from 'process'; +import { Uri, workspace } from 'vscode'; +import type { Container } from '../../../container'; +import type { SharedGkStorageLocationProvider } from '../../../plus/repos/sharedGkStorageLocationProvider'; +import { log } from '../../../system/decorators/log'; +import type { Lazy } from '../../../system/lazy'; +import { lazy } from '../../../system/lazy'; +import { getLoggableName, Logger } from '../../../system/logger'; +import { getLogScope, startLogScope } from '../../../system/logger.scope'; +import { wait } from '../../../system/promise'; +import type { UnifiedAsyncDisposable } from '../../../system/unifiedDisposable'; +import { createAsyncDisposable } from '../../../system/unifiedDisposable'; +import { getPlatform } from '../platform'; + +export class LocalSharedGkStorageLocationProvider implements SharedGkStorageLocationProvider { + private readonly _lazySharedGKUri: Lazy>; + + constructor(private readonly container: Container) { + this._lazySharedGKUri = lazy(async () => { + using scope = startLogScope(`${getLoggableName(this)}.load`, false); + + /** Deprecated prefer using XDG paths */ + const legacySharedGKPath = join(homedir(), '.gk'); + const legacySharedGKUri = Uri.file(legacySharedGKPath); + + // Look for the original shared GK path first, and if not found, use the new XDG path + let path; + try { + await workspace.fs.stat(legacySharedGKUri); + } catch { + path = env.XDG_DATA_HOME; + if (!path) { + const platform = getPlatform(); + switch (platform) { + case 'windows': + path = env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'); + break; + case 'macOS': + path = join(homedir(), 'Library', 'Application Support'); + break; + case 'linux': + path = join(homedir(), '.local', 'share'); + break; + } + } + + if (path) { + path = join(path, 'gk'); + Logger.log(scope, `Using shared GK path: ${path}`); + } + } + + if (path) return Uri.file(path); + + Logger.log(scope, `Using legacy shared GK path: ${legacySharedGKPath}`); + return legacySharedGKUri; + }); + } + + private async getUri(relativeFilePath: string) { + return Uri.joinPath(await this._lazySharedGKUri.value, relativeFilePath); + } + + @log() + async acquireSharedStorageWriteLock(): Promise { + const scope = getLogScope(); + + const lockFileUri = await this.getUri('lockfile'); + + let stat; + while (true) { + try { + stat = await workspace.fs.stat(lockFileUri); + } catch { + // File does not exist, so we can safely create it + break; + } + + const currentTime = new Date().getTime(); + if (currentTime - stat.ctime > 30000) { + // File exists, but the timestamp is older than 30 seconds, so we can safely remove it + break; + } + + // File exists, and the timestamp is less than 30 seconds old, so we need to wait for it to be removed + await wait(100); + } + + try { + // write the lockfile to the shared data folder + await workspace.fs.writeFile(lockFileUri, new Uint8Array(0)); + } catch (ex) { + Logger.error(ex, scope, `Failed to acquire lock: ${lockFileUri.toString(true)}`); + return undefined; + } + + return createAsyncDisposable(() => this.releaseSharedStorageWriteLock()); + } + + @log() + async releaseSharedStorageWriteLock(): Promise { + const scope = getLogScope(); + + const lockFileUri = await this.getUri('lockfile'); + + try { + await workspace.fs.delete(lockFileUri); + } catch (ex) { + Logger.error(ex, scope, `Failed to release lock: ${lockFileUri.toString(true)}`); + return false; + } + + return true; + } + + async getSharedRepositoryLocationFileUri() { + return this.getUri('repoMapping.json'); + } + + async getSharedCloudWorkspaceMappingFileUri() { + return this.getUri('cloudWorkspaces.json'); + } + + async getSharedLocalWorkspaceMappingFileUri() { + return this.getUri('localWorkspaces.json'); + } +} + +export function getGKDLocalWorkspaceMappingFileUri() { + return Uri.file( + join( + homedir(), + `${getPlatform() === 'windows' ? '/AppData/Roaming/' : ''}.gitkraken`, + 'workspaces', + 'workspaces.json', + ), + ); +} diff --git a/src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts b/src/env/node/gk/localWorkspacesSharedStorageProvider.ts similarity index 72% rename from src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts rename to src/env/node/gk/localWorkspacesSharedStorageProvider.ts index 4f2806429778e..822683597fd4f 100644 --- a/src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts +++ b/src/env/node/gk/localWorkspacesSharedStorageProvider.ts @@ -1,14 +1,23 @@ import { Uri, workspace } from 'vscode'; +import type { Container } from '../../../container'; +import type { SharedGkStorageLocationProvider } from '../../../plus/repos/sharedGkStorageLocationProvider'; import type { CloudWorkspacesPathMap } from '../../../plus/workspaces/models/cloudWorkspace'; import type { LocalWorkspaceFileData } from '../../../plus/workspaces/models/localWorkspace'; import type { CodeWorkspaceFileContents, WorkspaceAutoAddSetting } from '../../../plus/workspaces/models/workspaces'; -import type { WorkspacesPathMappingProvider } from '../../../plus/workspaces/workspacesPathMappingProvider'; +import type { GkWorkspacesSharedStorageProvider } from '../../../plus/workspaces/workspacesSharedStorageProvider'; +import { log } from '../../../system/decorators/log'; import { Logger } from '../../../system/logger'; -import { getSharedLegacyLocalWorkspaceMappingFileUri, SharedGKDataFolderMapper } from './sharedGKDataFolder'; +import { getLogScope } from '../../../system/logger.scope'; +import { getGKDLocalWorkspaceMappingFileUri } from './localSharedGkStorageLocationProvider'; -export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMappingProvider { +export class LocalGkWorkspacesSharedStorageProvider implements GkWorkspacesSharedStorageProvider { private _cloudWorkspacePathMap: CloudWorkspacesPathMap | undefined = undefined; + constructor( + private readonly container: Container, + private readonly sharedStorage: SharedGkStorageLocationProvider, + ) {} + private async ensureCloudWorkspacePathMap(): Promise { if (this._cloudWorkspacePathMap == null) { await this.loadCloudWorkspacePathMap(); @@ -21,7 +30,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping } private async loadCloudWorkspacePathMap(): Promise { - const localFileUri = await SharedGKDataFolderMapper.getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await this.sharedStorage.getSharedCloudWorkspaceMappingFileUri(); try { const data = await workspace.fs.readFile(localFileUri); this._cloudWorkspacePathMap = (JSON.parse(data.toString())?.workspaces ?? {}) as CloudWorkspacesPathMap; @@ -30,20 +39,24 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping } } - async getCloudWorkspaceRepoPath(cloudWorkspaceId: string, repoId: string): Promise { + @log() + async getCloudWorkspaceRepositoryLocation(cloudWorkspaceId: string, repoId: string): Promise { const cloudWorkspacePathMap = await this.getCloudWorkspacePathMap(); return cloudWorkspacePathMap[cloudWorkspaceId]?.repoPaths?.[repoId]; } - async getCloudWorkspaceCodeWorkspacePath(cloudWorkspaceId: string): Promise { + @log() + async getCloudWorkspaceCodeWorkspaceFileLocation(cloudWorkspaceId: string): Promise { const cloudWorkspacePathMap = await this.getCloudWorkspacePathMap(); return cloudWorkspacePathMap[cloudWorkspaceId]?.externalLinks?.['.code-workspace']; } - async removeCloudWorkspaceCodeWorkspaceFilePath(cloudWorkspaceId: string): Promise { - if (!(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { - return; - } + @log() + async removeCloudWorkspaceCodeWorkspaceFile(cloudWorkspaceId: string): Promise { + const scope = getLogScope(); + + await using lock = await this.sharedStorage.acquireSharedStorageWriteLock(); + if (lock == null) return; await this.loadCloudWorkspacePathMap(); @@ -51,20 +64,20 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping delete this._cloudWorkspacePathMap[cloudWorkspaceId].externalLinks['.code-workspace']; - const localFileUri = await SharedGKDataFolderMapper.getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await this.sharedStorage.getSharedCloudWorkspaceMappingFileUri(); const outputData = new Uint8Array(Buffer.from(JSON.stringify({ workspaces: this._cloudWorkspacePathMap }))); try { await workspace.fs.writeFile(localFileUri, outputData); - } catch (error) { - Logger.error(error, 'writeCloudWorkspaceCodeWorkspaceFilePathToMap'); + } catch (ex) { + Logger.error(ex, scope); } - await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } + @log() async confirmCloudWorkspaceCodeWorkspaceFilePath(cloudWorkspaceId: string): Promise { - const cloudWorkspacePathMap = await this.getCloudWorkspacePathMap(); - const codeWorkspaceFilePath = cloudWorkspacePathMap[cloudWorkspaceId]?.externalLinks?.['.code-workspace']; + const codeWorkspaceFilePath = await this.getCloudWorkspaceCodeWorkspaceFileLocation(cloudWorkspaceId); if (codeWorkspaceFilePath == null) return false; + try { await workspace.fs.stat(Uri.file(codeWorkspaceFilePath)); return true; @@ -73,14 +86,16 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping } } - async writeCloudWorkspaceRepoDiskPathToMap( + @log() + async storeCloudWorkspaceRepositoryLocation( cloudWorkspaceId: string, repoId: string, repoLocalPath: string, ): Promise { - if (!(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { - return; - } + const scope = getLogScope(); + + await using lock = await this.sharedStorage.acquireSharedStorageWriteLock(); + if (lock == null) return; await this.loadCloudWorkspacePathMap(); @@ -98,23 +113,24 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping this._cloudWorkspacePathMap[cloudWorkspaceId].repoPaths[repoId] = repoLocalPath; - const localFileUri = await SharedGKDataFolderMapper.getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await this.sharedStorage.getSharedCloudWorkspaceMappingFileUri(); const outputData = new Uint8Array(Buffer.from(JSON.stringify({ workspaces: this._cloudWorkspacePathMap }))); try { await workspace.fs.writeFile(localFileUri, outputData); - } catch (error) { - Logger.error(error, 'writeCloudWorkspaceRepoDiskPathToMap'); + } catch (ex) { + Logger.error(ex, scope); } - await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } - async writeCloudWorkspaceCodeWorkspaceFilePathToMap( + @log() + async storeCloudWorkspaceCodeWorkspaceFileLocation( cloudWorkspaceId: string, codeWorkspaceFilePath: string, ): Promise { - if (!(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { - return; - } + const scope = getLogScope(); + + await using lock = await this.sharedStorage.acquireSharedStorageWriteLock(); + if (lock == null) return; await this.loadCloudWorkspacePathMap(); @@ -132,14 +148,13 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping this._cloudWorkspacePathMap[cloudWorkspaceId].externalLinks['.code-workspace'] = codeWorkspaceFilePath; - const localFileUri = await SharedGKDataFolderMapper.getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await this.sharedStorage.getSharedCloudWorkspaceMappingFileUri(); const outputData = new Uint8Array(Buffer.from(JSON.stringify({ workspaces: this._cloudWorkspacePathMap }))); try { await workspace.fs.writeFile(localFileUri, outputData); - } catch (error) { - Logger.error(error, 'writeCloudWorkspaceCodeWorkspaceFilePathToMap'); + } catch (ex) { + Logger.error(ex, scope); } - await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } // TODO@ramint: May want a file watcher on this file down the line @@ -149,13 +164,13 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping let localFileUri; let data; try { - localFileUri = await SharedGKDataFolderMapper.getSharedLocalWorkspaceMappingFileUri(); + localFileUri = await this.sharedStorage.getSharedLocalWorkspaceMappingFileUri(); data = await workspace.fs.readFile(localFileUri); if (data?.length) return JSON.parse(data.toString()) as LocalWorkspaceFileData; } catch (_ex) { // Fall back to using legacy location for file try { - localFileUri = getSharedLegacyLocalWorkspaceMappingFileUri(); + localFileUri = getGKDLocalWorkspaceMappingFileUri(); data = await workspace.fs.readFile(localFileUri); if (data?.length) return JSON.parse(data.toString()) as LocalWorkspaceFileData; } catch (ex) { @@ -166,7 +181,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping return { workspaces: {} }; } - async writeCodeWorkspaceFile( + async createOrUpdateCodeWorkspaceFile( uri: Uri, workspaceRepoFilePaths: string[], options?: { workspaceId?: string; workspaceAutoAddSetting?: WorkspaceAutoAddSetting }, @@ -193,7 +208,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping try { await workspace.fs.writeFile(uri, outputData); if (options?.workspaceId != null) { - await this.writeCloudWorkspaceCodeWorkspaceFilePathToMap(options.workspaceId, uri.fsPath); + await this.storeCloudWorkspaceCodeWorkspaceFileLocation(options.workspaceId, uri.fsPath); } } catch (error) { Logger.error(error, 'writeCodeWorkspaceFile'); diff --git a/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts b/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts deleted file mode 100644 index 921d4570ea0e9..0000000000000 --- a/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Disposable } from 'vscode'; -import { workspace } from 'vscode'; -import type { Container } from '../../../container'; -import type { LocalRepoDataMap } from '../../../git/models/pathMapping'; -import type { RepositoryPathMappingProvider } from '../../../git/pathMapping/repositoryPathMappingProvider'; -import { Logger } from '../../../system/logger'; -import { SharedGKDataFolderMapper } from './sharedGKDataFolder'; - -export class RepositoryLocalPathMappingProvider implements RepositoryPathMappingProvider, Disposable { - constructor(private readonly container: Container) {} - - dispose() {} - - private _localRepoDataMap: LocalRepoDataMap | undefined = undefined; - - private async ensureLocalRepoDataMap() { - if (this._localRepoDataMap == null) { - await this.loadLocalRepoDataMap(); - } - } - - private async getLocalRepoDataMap(): Promise { - await this.ensureLocalRepoDataMap(); - return this._localRepoDataMap ?? {}; - } - - async getLocalRepoPaths(options: { - remoteUrl?: string; - repoInfo?: { provider?: string; owner?: string; repoName?: string }; - }): Promise { - const paths: string[] = []; - if (options.remoteUrl != null) { - const remoteUrlPaths = await this._getLocalRepoPaths(options.remoteUrl); - if (remoteUrlPaths != null) { - paths.push(...remoteUrlPaths); - } - } - if (options.repoInfo != null) { - const { provider, owner, repoName } = options.repoInfo; - if (provider != null && owner != null && repoName != null) { - const repoInfoPaths = await this._getLocalRepoPaths(`${provider}/${owner}/${repoName}`); - if (repoInfoPaths != null) { - paths.push(...repoInfoPaths); - } - } - } - - return paths; - } - - private async _getLocalRepoPaths(key: string): Promise { - const localRepoDataMap = await this.getLocalRepoDataMap(); - return localRepoDataMap[key]?.paths; - } - - private async loadLocalRepoDataMap() { - const localFileUri = await SharedGKDataFolderMapper.getSharedRepositoryMappingFileUri(); - try { - const data = await workspace.fs.readFile(localFileUri); - this._localRepoDataMap = (JSON.parse(data.toString()) ?? {}) as LocalRepoDataMap; - } catch (error) { - Logger.error(error, 'loadLocalRepoDataMap'); - } - } - - async writeLocalRepoPath( - options: { remoteUrl?: string; repoInfo?: { provider?: string; owner?: string; repoName?: string } }, - localPath: string, - ): Promise { - if (options.remoteUrl != null) { - await this._writeLocalRepoPath(options.remoteUrl, localPath); - } - if ( - options.repoInfo?.provider != null && - options.repoInfo?.owner != null && - options.repoInfo?.repoName != null - ) { - const { provider, owner, repoName } = options.repoInfo; - const key = `${provider}/${owner}/${repoName}`; - await this._writeLocalRepoPath(key, localPath); - } - } - - private async _writeLocalRepoPath(key: string, localPath: string): Promise { - if (!key || !localPath || !(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { - return; - } - - await this.loadLocalRepoDataMap(); - if (this._localRepoDataMap == null) { - this._localRepoDataMap = {}; - } - - if (this._localRepoDataMap[key] == null) { - this._localRepoDataMap[key] = { paths: [localPath] }; - } else if (this._localRepoDataMap[key].paths == null) { - this._localRepoDataMap[key].paths = [localPath]; - } else if (!this._localRepoDataMap[key].paths.includes(localPath)) { - this._localRepoDataMap[key].paths.push(localPath); - } - - const localFileUri = await SharedGKDataFolderMapper.getSharedRepositoryMappingFileUri(); - const outputData = new Uint8Array(Buffer.from(JSON.stringify(this._localRepoDataMap))); - try { - await workspace.fs.writeFile(localFileUri, outputData); - } catch (error) { - Logger.error(error, 'writeLocalRepoPath'); - } - await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); - } -} diff --git a/src/env/node/pathMapping/sharedGKDataFolder.ts b/src/env/node/pathMapping/sharedGKDataFolder.ts deleted file mode 100644 index ad736e05e7689..0000000000000 --- a/src/env/node/pathMapping/sharedGKDataFolder.ts +++ /dev/null @@ -1,141 +0,0 @@ -import os from 'os'; -import path from 'path'; -import { env } from 'process'; -import { Uri, workspace } from 'vscode'; -import { xdgData } from 'xdg-basedir'; -import { Logger } from '../../../system/logger'; -import { wait } from '../../../system/promise'; -import { getPlatform } from '../platform'; - -/** @deprecated prefer using XDG paths */ -const legacySharedGKDataFolder = path.join(os.homedir(), '.gk'); - -class SharedGKDataFolderMapper { - private _initPromise: Promise | undefined; - constructor( - // do soft migration, use new folders only for new users (without existing folders) - // eslint-disable-next-line @typescript-eslint/no-deprecated - private sharedGKDataFolder = legacySharedGKDataFolder, - private _isInitialized: boolean = false, - ) {} - - private async _initialize() { - if (this._initPromise) { - throw new Error('cannot be initialized multiple times'); - } - try { - await workspace.fs.stat(Uri.file(this.sharedGKDataFolder)); - } catch { - // Path does not exist, so we can safely use xdg paths - const platform = getPlatform(); - const folderName = 'gk'; - switch (platform) { - case 'windows': - if (env.LOCALAPPDATA) { - this.sharedGKDataFolder = path.join(env.LOCALAPPDATA, folderName, 'Data'); - } else { - this.sharedGKDataFolder = path.join(os.homedir(), 'AppData', 'Local', folderName, 'Data'); - } - break; - case 'macOS': - this.sharedGKDataFolder = path.join(os.homedir(), 'Library', 'Application Support', folderName); - break; - default: - if (xdgData) { - this.sharedGKDataFolder = path.join(xdgData, folderName); - } else { - this.sharedGKDataFolder = path.join(os.homedir(), '.local', 'share', folderName); - } - } - } finally { - this._isInitialized = true; - } - } - - private async waitForInitialized() { - if (this._isInitialized) { - return; - } - if (!this._initPromise) { - this._initPromise = this._initialize(); - } - await this._initPromise; - } - - private async getUri(relativeFilePath: string) { - await this.waitForInitialized(); - return Uri.file(path.join(this.sharedGKDataFolder, relativeFilePath)); - } - - async acquireSharedFolderWriteLock(): Promise { - const lockFileUri = await this.getUri('lockfile'); - - let stat; - while (true) { - try { - stat = await workspace.fs.stat(lockFileUri); - } catch { - // File does not exist, so we can safely create it - break; - } - - const currentTime = new Date().getTime(); - if (currentTime - stat.ctime > 30000) { - // File exists, but the timestamp is older than 30 seconds, so we can safely remove it - break; - } - - // File exists, and the timestamp is less than 30 seconds old, so we need to wait for it to be removed - await wait(100); - } - - try { - // write the lockfile to the shared data folder - await workspace.fs.writeFile(lockFileUri, new Uint8Array(0)); - } catch (error) { - Logger.error(error, 'acquireSharedFolderWriteLock'); - return false; - } - - return true; - } - - async releaseSharedFolderWriteLock(): Promise { - try { - const lockFileUri = await this.getUri('lockfile'); - await workspace.fs.delete(lockFileUri); - } catch (error) { - Logger.error(error, 'releaseSharedFolderWriteLock'); - return false; - } - - return true; - } - - async getSharedRepositoryMappingFileUri() { - return this.getUri('repoMapping.json'); - } - - async getSharedCloudWorkspaceMappingFileUri() { - return this.getUri('cloudWorkspaces.json'); - } - - async getSharedLocalWorkspaceMappingFileUri() { - return this.getUri('localWorkspaces.json'); - } -} - -// export as a singleton -const instance = new SharedGKDataFolderMapper(); -export { instance as SharedGKDataFolderMapper }; - -export function getSharedLegacyLocalWorkspaceMappingFileUri() { - return Uri.file( - path.join( - os.homedir(), - `${getPlatform() === 'windows' ? '/AppData/Roaming/' : ''}.gitkraken`, - 'workspaces', - 'workspaces.json', - ), - ); -} diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts index 828eb071a2223..3efe859b2e0e3 100644 --- a/src/env/node/providers.ts +++ b/src/env/node/providers.ts @@ -1,13 +1,17 @@ import type { Container } from '../../container'; import type { GitCommandOptions } from '../../git/commandOptions'; import type { GitProvider } from '../../git/gitProvider'; +import type { RepositoryLocationProvider } from '../../git/location/repositorylocationProvider'; +import type { SharedGkStorageLocationProvider } from '../../plus/repos/sharedGkStorageLocationProvider'; +import type { GkWorkspacesSharedStorageProvider } from '../../plus/workspaces/workspacesSharedStorageProvider'; import { configuration } from '../../system/-webview/configuration'; // import { GitHubGitProvider } from '../../plus/github/githubGitProvider'; import { Git } from './git/git'; import { LocalGitProvider } from './git/localGitProvider'; import { VslsGit, VslsGitProvider } from './git/vslsGitProvider'; -import { RepositoryLocalPathMappingProvider } from './pathMapping/repositoryLocalPathMappingProvider'; -import { WorkspacesLocalPathMappingProvider } from './pathMapping/workspacesLocalPathMappingProvider'; +import { LocalRepositoryLocationProvider } from './gk/localRepositoryLocationProvider'; +import { LocalSharedGkStorageLocationProvider } from './gk/localSharedGkStorageLocationProvider'; +import { LocalGkWorkspacesSharedStorageProvider } from './gk/localWorkspacesSharedStorageProvider'; let gitInstance: Git | undefined; function ensureGit() { @@ -52,10 +56,20 @@ export async function getSupportedGitProviders(container: Container): Promise { - void this.addRepositoriesToPathMap(added); + void this.storeRepositoriesLocation(added); // Defer the event trigger enough to let everything unwind this.fireRepositoriesChanged(added); }); @@ -2014,7 +2014,7 @@ export class GitProviderService implements Disposable { if (added.length) { this._etag = Date.now(); queueMicrotask(() => { - void this.addRepositoriesToPathMap(added); + void this.storeRepositoriesLocation(added); // Send a notification that the repositories changed this.fireRepositoriesChanged(added); }); @@ -2033,11 +2033,11 @@ export class GitProviderService implements Disposable { @gate() @log() - async addRepositoriesToPathMap(repos: Repository[]): Promise { + async storeRepositoriesLocation(repos: Repository[]): Promise { const scope = getLogScope(); for (const repo of repos) { try { - await this.container.repositoryIdentity.addRepositoryToPathMap(repo); + await this.container.repositoryIdentity.storeRepositoryLocation(repo); } catch (ex) { Logger.error(ex, scope); } diff --git a/src/git/location/repositorylocationProvider.ts b/src/git/location/repositorylocationProvider.ts new file mode 100644 index 0000000000000..eeedf2c7b7326 --- /dev/null +++ b/src/git/location/repositorylocationProvider.ts @@ -0,0 +1,14 @@ +import type { Disposable } from 'vscode'; + +export interface RepositoryLocationProvider extends Disposable { + getLocation( + remoteUrl: string | undefined, + repoInfo?: { provider?: string; owner?: string; repoName?: string }, + ): Promise; + + storeLocation( + path: string, + remoteUrl: string | undefined, + repoInfo?: { provider?: string; owner?: string; repoName?: string }, + ): Promise; +} diff --git a/src/git/pathMapping/repositoryPathMappingProvider.ts b/src/git/pathMapping/repositoryPathMappingProvider.ts deleted file mode 100644 index 05f5e37dac61a..0000000000000 --- a/src/git/pathMapping/repositoryPathMappingProvider.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Disposable } from 'vscode'; - -export interface RepositoryPathMappingProvider extends Disposable { - getLocalRepoPaths(options: { - remoteUrl?: string; - repoInfo?: { provider?: string; owner?: string; repoName?: string }; - }): Promise; - - writeLocalRepoPath( - options: { remoteUrl?: string; repoInfo?: { provider?: string; owner?: string; repoName?: string } }, - localPath: string, - ): Promise; -} diff --git a/src/plus/repos/repositoryIdentityService.ts b/src/plus/repos/repositoryIdentityService.ts index ea556f2e4d238..cdc7eeda2778d 100644 --- a/src/plus/repos/repositoryIdentityService.ts +++ b/src/plus/repos/repositoryIdentityService.ts @@ -1,6 +1,7 @@ import type { Disposable } from 'vscode'; import { Uri, window } from 'vscode'; import type { Container } from '../../container'; +import type { RepositoryLocationProvider } from '../../git/location/repositorylocationProvider'; import { RemoteResourceType } from '../../git/models/remoteResource'; import type { Repository } from '../../git/models/repository'; import type { @@ -12,12 +13,11 @@ import { missingRepositoryId } from '../../git/models/repositoryIdentities'; import { parseGitRemoteUrl } from '../../git/parsers/remoteParser'; import { log } from '../../system/decorators/log'; import { getSettledValue } from '../../system/promise'; -import type { ServerConnection } from '../gk/serverConnection'; export class RepositoryIdentityService implements Disposable { constructor( private readonly container: Container, - private readonly connection: ServerConnection, + private readonly locator: RepositoryLocationProvider | undefined, ) {} dispose(): void {} @@ -30,6 +30,7 @@ export class RepositoryIdentityService implements Disposable { return this.locateRepository(identity, options); } + @log() async getRepositoryIdentity( repository: Repository, ): Promise> { @@ -64,21 +65,20 @@ export class RepositoryIdentityService implements Disposable { const matches = hasRemoteUrl || hasProviderInfo - ? await this.container.repositoryPathMapping.getLocalRepoPaths({ - remoteUrl: identity.remote?.url, - repoInfo: - identity.provider != null - ? { - provider: identity.provider.id, - owner: identity.provider.repoDomain, - repoName: identity.provider.repoName, - } - : undefined, - }) + ? await this.locator?.getLocation( + identity.remote?.url, + identity.provider != null + ? { + provider: identity.provider.id, + owner: identity.provider.repoDomain, + repoName: identity.provider.repoName, + } + : undefined, + ) : []; let foundRepo: Repository | undefined; - if (matches.length) { + if (matches?.length) { for (const match of matches) { const repo = this.container.git.getRepository(Uri.file(match)); if (repo != null) { @@ -154,18 +154,19 @@ export class RepositoryIdentityService implements Disposable { (await this.container.git.validateReference(locatedRepo.uri, identity.initialCommitSha)) ) { foundRepo = locatedRepo; - await this.addRepositoryToPathMap(foundRepo, identity); + await this.storeRepositoryLocation(foundRepo, identity); } } return foundRepo; } - async addRepositoryToPathMap( + @log({ args: { 1: false } }) + async storeRepositoryLocation( repo: Repository, identity?: RepositoryIdentityDescriptor, ) { - if (repo.virtual) return; + if (repo.virtual || this.locator == null) return; const [identityResult, remotesResult] = await Promise.allSettled([ identity == null ? this.getRepositoryIdentity(repo) : undefined, @@ -180,7 +181,7 @@ export class RepositoryIdentityService implements Disposable { for (const remote of remotes) { const remoteUrl = remote.provider?.url({ type: RemoteResourceType.Repo }); if (remoteUrl != null) { - await this.container.repositoryPathMapping.writeLocalRepoPath({ remoteUrl: remoteUrl }, repoPath); + await this.locator.storeLocation(repoPath, remoteUrl); } } @@ -189,16 +190,11 @@ export class RepositoryIdentityService implements Disposable { identity?.provider?.repoDomain != null && identity?.provider?.repoName != null ) { - await this.container.repositoryPathMapping.writeLocalRepoPath( - { - repoInfo: { - provider: identity.provider.id, - owner: identity.provider.repoDomain, - repoName: identity.provider.repoName, - }, - }, - repoPath, - ); + await this.locator.storeLocation(repoPath, undefined, { + provider: identity.provider.id, + owner: identity.provider.repoDomain, + repoName: identity.provider.repoName, + }); } } } diff --git a/src/plus/repos/sharedGkStorageLocationProvider.ts b/src/plus/repos/sharedGkStorageLocationProvider.ts new file mode 100644 index 0000000000000..731fedbd29e12 --- /dev/null +++ b/src/plus/repos/sharedGkStorageLocationProvider.ts @@ -0,0 +1,11 @@ +import type { Uri } from 'vscode'; +import type { UnifiedAsyncDisposable } from '../../system/unifiedDisposable'; + +export interface SharedGkStorageLocationProvider { + getSharedRepositoryLocationFileUri(): Promise; + getSharedCloudWorkspaceMappingFileUri(): Promise; + getSharedLocalWorkspaceMappingFileUri(): Promise; + + acquireSharedStorageWriteLock(): Promise; + releaseSharedStorageWriteLock(): Promise; +} diff --git a/src/plus/workspaces/workspacesService.ts b/src/plus/workspaces/workspacesService.ts index afe75283384ce..e71761fbaa5be 100644 --- a/src/plus/workspaces/workspacesService.ts +++ b/src/plus/workspaces/workspacesService.ts @@ -1,7 +1,7 @@ import type { CancellationToken, Event, MessageItem, QuickPickItem } from 'vscode'; import { Disposable, EventEmitter, ProgressLocation, Uri, window, workspace } from 'vscode'; -import { getSupportedWorkspacesPathMappingProvider } from '@env/providers'; import type { Container } from '../../container'; +import type { RepositoryLocationProvider } from '../../git/location/repositorylocationProvider'; import type { GitRemote } from '../../git/models/remote'; import { RemoteResourceType } from '../../git/models/remoteResource'; import { Repository } from '../../git/models/repository'; @@ -10,7 +10,6 @@ import type { OpenWorkspaceLocation } from '../../system/-webview/vscode'; import { openWorkspace } from '../../system/-webview/vscode'; import { log } from '../../system/decorators/log'; import { normalizePath } from '../../system/path'; -import type { ServerConnection } from '../gk/serverConnection'; import type { SubscriptionChangeEvent } from '../gk/subscriptionService'; import { isSubscriptionStatePaidOrTrial } from '../gk/utils/subscription.utils'; import type { CloudWorkspaceData, CloudWorkspaceRepositoryDescriptor } from './models/cloudWorkspace'; @@ -35,8 +34,8 @@ import type { WorkspacesResponse, } from './models/workspaces'; import { WorkspaceAddRepositoriesChoice } from './models/workspaces'; -import { WorkspacesApi } from './workspacesApi'; -import type { WorkspacesPathMappingProvider } from './workspacesPathMappingProvider'; +import type { WorkspacesApi } from './workspacesApi'; +import type { GkWorkspacesSharedStorageProvider } from './workspacesSharedStorageProvider'; export class WorkspacesService implements Disposable { private _onDidResetWorkspaces: EventEmitter = new EventEmitter(); @@ -47,18 +46,16 @@ export class WorkspacesService implements Disposable { private _cloudWorkspaces: CloudWorkspace[] | undefined; private _disposable: Disposable; private _localWorkspaces: LocalWorkspace[] | undefined; - private _workspacesApi: WorkspacesApi; - private _workspacesPathProvider: WorkspacesPathMappingProvider; private _currentWorkspaceId: string | undefined; private _currentWorkspaceAutoAddSetting: WorkspaceAutoAddSetting = 'disabled'; private _currentWorkspace: CloudWorkspace | LocalWorkspace | undefined; constructor( private readonly container: Container, - private readonly connection: ServerConnection, + private readonly _api: WorkspacesApi, + private readonly _sharedStorage: GkWorkspacesSharedStorageProvider | undefined, + private readonly _repositoryLocator: RepositoryLocationProvider | undefined, ) { - this._workspacesApi = new WorkspacesApi(this.container, this.connection); - this._workspacesPathProvider = getSupportedWorkspacesPathMappingProvider(); this._currentWorkspaceId = getCurrentWorkspaceId(); this._currentWorkspaceAutoAddSetting = workspace.getConfiguration('gitkraken')?.get('workspaceAutoAddSetting') ?? @@ -100,7 +97,7 @@ export class WorkspacesService implements Disposable { const cloudWorkspaces: CloudWorkspace[] = []; let workspaces: CloudWorkspaceData[] | undefined; try { - const workspaceResponse: WorkspacesResponse | undefined = await this._workspacesApi.getWorkspaces({ + const workspaceResponse: WorkspacesResponse | undefined = await this._api.getWorkspaces({ includeRepositories: !excludeRepositories, includeOrganizations: true, }); @@ -116,7 +113,7 @@ export class WorkspacesService implements Disposable { const isPlusEnabled = isSubscriptionStatePaidOrTrial(subscription.state); if (workspaces?.length) { for (const workspace of workspaces) { - const localPath = await this._workspacesPathProvider.getCloudWorkspaceCodeWorkspacePath(workspace.id); + const localPath = await this._sharedStorage?.getCloudWorkspaceCodeWorkspaceFileLocation(workspace.id); if (!isPlusEnabled && workspace.organization?.id) { filteredSharedWorkspaceCount += 1; continue; @@ -166,7 +163,7 @@ export class WorkspacesService implements Disposable { private async loadLocalWorkspaces(): Promise { const localWorkspaces: LocalWorkspace[] = []; const workspaceFileData: LocalWorkspaceData = - (await this._workspacesPathProvider.getLocalWorkspaceData())?.workspaces || {}; + (await this._sharedStorage?.getLocalWorkspaceData())?.workspaces || {}; for (const workspace of Object.values(workspaceFileData)) { if (workspace.localId == null || workspace.name == null) continue; localWorkspaces.push( @@ -236,7 +233,7 @@ export class WorkspacesService implements Disposable { async getCloudWorkspaceRepositories(workspaceId: string): Promise { // TODO@ramint Add error handling/logging when this is used. - const workspaceRepos = await this._workspacesApi.getWorkspaceRepositories(workspaceId); + const workspaceRepos = await this._api.getWorkspaceRepositories(workspaceId); const descriptors = workspaceRepos?.data?.project?.provider_data?.repositories?.nodes; return descriptors?.map(d => ({ ...d, workspaceId: workspaceId })) ?? []; } @@ -250,7 +247,7 @@ export class WorkspacesService implements Disposable { if (currentWorkspace == null) { try { - const workspaceData = await this._workspacesApi.getWorkspace(this._currentWorkspaceId, { + const workspaceData = await this._api.getWorkspace(this._currentWorkspaceId, { includeRepositories: true, }); if (workspaceData?.data?.project == null) return; @@ -372,11 +369,11 @@ export class WorkspacesService implements Disposable { } async getCloudWorkspaceRepoPath(cloudWorkspaceId: string, repoId: string): Promise { - return this._workspacesPathProvider.getCloudWorkspaceRepoPath(cloudWorkspaceId, repoId); + return this._sharedStorage?.getCloudWorkspaceRepositoryLocation(cloudWorkspaceId, repoId); } async updateCloudWorkspaceRepoLocalPath(workspaceId: string, repoId: string, localPath: string): Promise { - await this._workspacesPathProvider.writeCloudWorkspaceRepoDiskPathToMap(workspaceId, repoId, localPath); + await this._sharedStorage?.storeCloudWorkspaceRepositoryLocation(workspaceId, repoId, localPath); } private async getRepositoriesInParentFolder(cancellation?: CancellationToken): Promise { @@ -483,7 +480,7 @@ export class WorkspacesService implements Disposable { } for (const remoteUrl of remoteUrls) { - await this.container.repositoryPathMapping.writeLocalRepoPath({ remoteUrl: remoteUrl }, repoPath); + await this._repositoryLocator?.storeLocation(repoPath, remoteUrl); } const workspace = this.getCloudWorkspace(workspaceId) ?? this.getLocalWorkspace(workspaceId); @@ -497,17 +494,11 @@ export class WorkspacesService implements Disposable { (descriptor.url != null || (descriptor.provider_organization_id != null && descriptor.name != null && provider != null)) ) { - await this.container.repositoryPathMapping.writeLocalRepoPath( - { - remoteUrl: descriptor.url ?? undefined, - repoInfo: { - provider: provider, - owner: descriptor.provider_organization_id, - repoName: descriptor.name, - }, - }, - repoPath, - ); + await this._repositoryLocator?.storeLocation(repoPath, descriptor.url ?? undefined, { + provider: provider, + owner: descriptor.provider_organization_id, + repoName: descriptor.name, + }); } if (descriptor.id != null) { @@ -713,7 +704,7 @@ export class WorkspacesService implements Disposable { let createdProjectData: CloudWorkspaceData | null | undefined; try { - const response = await this._workspacesApi.createWorkspace(createOptions); + const response = await this._api.createWorkspace(createOptions); createdProjectData = response?.data?.create_project; } catch { return; @@ -725,7 +716,7 @@ export class WorkspacesService implements Disposable { this._cloudWorkspaces = []; } - const localPath = await this._workspacesPathProvider.getCloudWorkspaceCodeWorkspacePath( + const localPath = await this._sharedStorage?.getCloudWorkspaceCodeWorkspaceFileLocation( createdProjectData.id, ); @@ -769,7 +760,7 @@ export class WorkspacesService implements Disposable { ); if (confirmation == null || confirmation.title === 'Cancel') return; try { - const response = await this._workspacesApi.deleteWorkspace(workspaceId); + const response = await this._api.deleteWorkspace(workspaceId); if (response?.data?.delete_project?.id === workspaceId) { // Remove the workspace from the local workspace list. this._cloudWorkspaces = this._cloudWorkspaces?.filter(w => w.id !== workspaceId); @@ -936,7 +927,7 @@ export class WorkspacesService implements Disposable { }, async () => { try { - const response = await this._workspacesApi.addReposToWorkspace( + const response = await this._api.addReposToWorkspace( workspaceId, repoInputs.map(r => ({ owner: r.owner, repoName: r.repoName })), ); @@ -987,7 +978,7 @@ export class WorkspacesService implements Disposable { ); if (confirmation == null || confirmation.title === 'Cancel') return; try { - const response = await this._workspacesApi.removeReposFromWorkspace(workspaceId, [ + const response = await this._api.removeReposFromWorkspace(workspaceId, [ { owner: descriptor.provider_organization_id, repoName: descriptor.name }, ]); @@ -1163,7 +1154,7 @@ export class WorkspacesService implements Disposable { const newWorkspaceAutoAddSetting = await this.chooseCodeWorkspaceAutoAddSetting(); - const created = await this._workspacesPathProvider.writeCodeWorkspaceFile( + const created = await this._sharedStorage?.createOrUpdateCodeWorkspaceFile( newWorkspaceUri, workspaceFolderPaths, { @@ -1240,12 +1231,9 @@ export class WorkspacesService implements Disposable { const newWorkspaceAutoAddSetting = newWorkspaceAutoAddOption.option; if (options?.current && workspace.workspaceFile != null) { - const updated = await this._workspacesPathProvider.updateCodeWorkspaceFileSettings( - workspace.workspaceFile, - { - workspaceAutoAddSetting: newWorkspaceAutoAddSetting, - }, - ); + const updated = await this._sharedStorage?.updateCodeWorkspaceFileSettings(workspace.workspaceFile, { + workspaceAutoAddSetting: newWorkspaceAutoAddSetting, + }); if (!updated) return this._currentWorkspaceAutoAddSetting; this._currentWorkspaceAutoAddSetting = newWorkspaceAutoAddSetting; } @@ -1283,8 +1271,8 @@ export class WorkspacesService implements Disposable { openLocation = openLocationChoice.location ?? 'newWindow'; } - if (!(await this._workspacesPathProvider.confirmCloudWorkspaceCodeWorkspaceFilePath(workspace.id))) { - await this._workspacesPathProvider.removeCloudWorkspaceCodeWorkspaceFilePath(workspace.id); + if (!(await this._sharedStorage?.confirmCloudWorkspaceCodeWorkspaceFilePath(workspace.id))) { + await this._sharedStorage?.removeCloudWorkspaceCodeWorkspaceFile(workspace.id); workspace.setLocalPath(undefined); const locateChoice = await window.showInformationMessage( `The workspace file for ${workspace.name} could not be found. Would you like to locate it now?`, @@ -1309,7 +1297,7 @@ export class WorkspacesService implements Disposable { if (newPath == null) return; - await this._workspacesPathProvider.writeCloudWorkspaceCodeWorkspaceFilePathToMap(workspace.id, newPath); + await this._sharedStorage?.storeCloudWorkspaceCodeWorkspaceFileLocation(workspace.id, newPath); workspace.setLocalPath(newPath); } @@ -1322,13 +1310,10 @@ export class WorkspacesService implements Disposable { let repoLocalPath = await this.getCloudWorkspaceRepoPath(descriptor.workspaceId, descriptor.id); if (repoLocalPath == null) { repoLocalPath = ( - await this.container.repositoryPathMapping.getLocalRepoPaths({ - remoteUrl: descriptor.url ?? undefined, - repoInfo: { - repoName: descriptor.name, - provider: descriptor.provider ?? undefined, - owner: descriptor.provider_organization_id, - }, + await this._repositoryLocator?.getLocation(descriptor.url ?? undefined, { + repoName: descriptor.name, + provider: descriptor.provider ?? undefined, + owner: descriptor.provider_organization_id, }) )?.[0]; } diff --git a/src/plus/workspaces/workspacesPathMappingProvider.ts b/src/plus/workspaces/workspacesSharedStorageProvider.ts similarity index 63% rename from src/plus/workspaces/workspacesPathMappingProvider.ts rename to src/plus/workspaces/workspacesSharedStorageProvider.ts index 00a1a965ff762..850e1f1a92fa5 100644 --- a/src/plus/workspaces/workspacesPathMappingProvider.ts +++ b/src/plus/workspaces/workspacesSharedStorageProvider.ts @@ -2,21 +2,21 @@ import type { Uri } from 'vscode'; import type { LocalWorkspaceFileData } from './models/localWorkspace'; import type { WorkspaceAutoAddSetting } from './models/workspaces'; -export interface WorkspacesPathMappingProvider { - getCloudWorkspaceRepoPath(cloudWorkspaceId: string, repoId: string): Promise; +export interface GkWorkspacesSharedStorageProvider { + getCloudWorkspaceRepositoryLocation(cloudWorkspaceId: string, repoId: string): Promise; - getCloudWorkspaceCodeWorkspacePath(cloudWorkspaceId: string): Promise; + getCloudWorkspaceCodeWorkspaceFileLocation(cloudWorkspaceId: string): Promise; - removeCloudWorkspaceCodeWorkspaceFilePath(cloudWorkspaceId: string): Promise; + removeCloudWorkspaceCodeWorkspaceFile(cloudWorkspaceId: string): Promise; - writeCloudWorkspaceCodeWorkspaceFilePathToMap( + storeCloudWorkspaceCodeWorkspaceFileLocation( cloudWorkspaceId: string, codeWorkspaceFilePath: string, ): Promise; confirmCloudWorkspaceCodeWorkspaceFilePath(cloudWorkspaceId: string): Promise; - writeCloudWorkspaceRepoDiskPathToMap( + storeCloudWorkspaceRepositoryLocation( cloudWorkspaceId: string, repoId: string, repoLocalPath: string, @@ -24,7 +24,7 @@ export interface WorkspacesPathMappingProvider { getLocalWorkspaceData(): Promise; - writeCodeWorkspaceFile( + createOrUpdateCodeWorkspaceFile( uri: Uri, workspaceRepoFilePaths: string[], options?: { workspaceId?: string; workspaceAutoAddSetting?: WorkspaceAutoAddSetting }, diff --git a/src/system/lazy.ts b/src/system/lazy.ts new file mode 100644 index 0000000000000..0721f130450cd --- /dev/null +++ b/src/system/lazy.ts @@ -0,0 +1,26 @@ +/** Provides lazy initialization support */ +export class Lazy { + private _value?: T; + private _initialized: boolean = false; + + /** + * Creates a new instance of Lazy that uses the specified initialization function. + * @param valueProvider The initialization function that is used to produce the value when it is needed. + */ + constructor(private readonly valueProvider: () => T) {} + + /** Gets the lazily initialized value of the current Lazy instance */ + get value(): T { + if (!this._initialized) { + this._value = this.valueProvider(); + this._initialized = true; + } + + return this._value!; + } +} + +/** Creates a new lazy value with the specified initialization function */ +export function lazy(valueProvider: () => T): Lazy { + return new Lazy(valueProvider); +} diff --git a/src/system/unifiedDisposable.ts b/src/system/unifiedDisposable.ts new file mode 100644 index 0000000000000..76b8f484b91e2 --- /dev/null +++ b/src/system/unifiedDisposable.ts @@ -0,0 +1,22 @@ +import type { Disposable as CoreDisposable } from 'vscode'; + +export type UnifiedDisposable = Disposable & CoreDisposable; +export type UnifiedAsyncDisposable = { dispose: () => Promise } & AsyncDisposable; + +export function createDisposable(dispose: () => void): UnifiedDisposable { + return { + dispose: dispose, + [Symbol.dispose]: dispose, + }; +} + +export function createAsyncDisposable(dispose: () => Promise): UnifiedAsyncDisposable { + return { + dispose: dispose, + [Symbol.asyncDispose]: dispose, + }; +} + +export function mixinDisposable(obj: T, dispose: () => void): T & UnifiedDisposable { + return { ...obj, ...createDisposable(dispose) }; +} diff --git a/src/uris/deepLinks/deepLinkService.ts b/src/uris/deepLinks/deepLinkService.ts index 740049e9cea0b..132a59ec4a9ec 100644 --- a/src/uris/deepLinks/deepLinkService.ts +++ b/src/uris/deepLinks/deepLinkService.ts @@ -709,10 +709,9 @@ export class DeepLinkService implements Disposable { } if (!this._context.repo && state === DeepLinkServiceState.RepoMatch) { - matchingLocalRepoPaths = await this.container.repositoryPathMapping.getLocalRepoPaths({ - remoteUrl: remoteUrlToSearch, - }); - if (matchingLocalRepoPaths.length > 0) { + matchingLocalRepoPaths = + (await this.container.repositoryLocator?.getLocation(remoteUrlToSearch)) ?? []; + if (matchingLocalRepoPaths.length) { for (const repo of this.container.git.repositories) { if ( matchingLocalRepoPaths.some( @@ -851,10 +850,7 @@ export class DeepLinkService implements Disposable { repoOpenType !== 'workspace' && !matchingLocalRepoPaths.includes(this._context.repoOpenUri.fsPath) ) { - await this.container.repositoryPathMapping.writeLocalRepoPath( - { remoteUrl: remoteUrl }, - chosenRepo.uri.fsPath, - ); + await this.container.repositoryLocator?.storeLocation(chosenRepo.uri.fsPath, remoteUrl); } }