diff --git a/CHANGELOG.md b/CHANGELOG.md index 728bf5801026c..03e5cb3b67f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Adds go to home view button to the commit graph title section — closes [#3873](https://github.com/gitkraken/vscode-gitlens/issues/3873) - Adds a _Contributors_ section to comparison results in the views +### Changed + +- Makes GitLens XDG-compatible— closes [#3660](https://github.com/gitkraken/vscode-gitlens/issues/3660) + ### Fixed - Fixes [#3915](https://github.com/gitkraken/vscode-gitlens/issues/3915) - Closing a split editor with annotations causes the Clear Annotations button to get stuck diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 3fec1f460dece..3936644ab86b5 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -33,6 +33,7 @@ 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 ========================================= @@ -2244,4 +2245,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF sortablejs NOTICES AND INFORMATION \ No newline at end of file +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 diff --git a/package.json b/package.json index 8b4cc88953045..07cdc3d753876 100644 --- a/package.json +++ b/package.json @@ -20035,7 +20035,8 @@ "react-dom": "16.8.4", "signal-utils": "0.21.1", "slug": "10.0.0", - "sortablejs": "1.15.0" + "sortablejs": "1.15.0", + "xdg-basedir": "5.1.0" }, "devDependencies": { "@eamodio/eslint-lite-webpack-plugin": "0.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a84c8878e457..ef3733ea55477 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ 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 @@ -5415,6 +5418,10 @@ 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'} @@ -11153,6 +11160,8 @@ snapshots: ws@7.5.10: {} + xdg-basedir@5.1.0: {} + xml2js@0.5.0: dependencies: sax: 1.4.1 diff --git a/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts b/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts index 9c77d7027f421..09a8bec667fad 100644 --- a/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts +++ b/src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts @@ -4,11 +4,7 @@ import type { Container } from '../../../container'; import type { LocalRepoDataMap } from '../../../pathMapping/models'; import type { RepositoryPathMappingProvider } from '../../../pathMapping/repositoryPathMappingProvider'; import { Logger } from '../../../system/logger'; -import { - acquireSharedFolderWriteLock, - getSharedRepositoryMappingFileUri, - releaseSharedFolderWriteLock, -} from './sharedGKDataFolder'; +import { SharedGKDataFolderMapper } from './sharedGKDataFolder'; export class RepositoryLocalPathMappingProvider implements RepositoryPathMappingProvider, Disposable { constructor(private readonly container: Container) {} @@ -58,7 +54,7 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping } private async loadLocalRepoDataMap() { - const localFileUri = getSharedRepositoryMappingFileUri(); + const localFileUri = await SharedGKDataFolderMapper.getSharedRepositoryMappingFileUri(); try { const data = await workspace.fs.readFile(localFileUri); this._localRepoDataMap = (JSON.parse(data.toString()) ?? {}) as LocalRepoDataMap; @@ -86,7 +82,7 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping } private async _writeLocalRepoPath(key: string, localPath: string): Promise { - if (!key || !localPath || !(await acquireSharedFolderWriteLock())) { + if (!key || !localPath || !(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { return; } @@ -103,13 +99,13 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping this._localRepoDataMap[key].paths.push(localPath); } - const localFileUri = getSharedRepositoryMappingFileUri(); + 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 releaseSharedFolderWriteLock(); + await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } } diff --git a/src/env/node/pathMapping/sharedGKDataFolder.ts b/src/env/node/pathMapping/sharedGKDataFolder.ts index 2a89f174f219f..ad736e05e7689 100644 --- a/src/env/node/pathMapping/sharedGKDataFolder.ts +++ b/src/env/node/pathMapping/sharedGKDataFolder.ts @@ -1,73 +1,134 @@ 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'; -export const sharedGKDataFolder = '.gk'; +/** @deprecated prefer using XDG paths */ +const legacySharedGKDataFolder = path.join(os.homedir(), '.gk'); -export async function acquireSharedFolderWriteLock(): Promise { - const lockFileUri = getSharedLockFileUri(); +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, + ) {} - let stat; - while (true) { + private async _initialize() { + if (this._initPromise) { + throw new Error('cannot be initialized multiple times'); + } try { - stat = await workspace.fs.stat(lockFileUri); + await workspace.fs.stat(Uri.file(this.sharedGKDataFolder)); } catch { - // File does not exist, so we can safely create it - break; + // 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; } + } - 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; + private async waitForInitialized() { + if (this._isInitialized) { + return; } - - // File exists, and the timestamp is less than 30 seconds old, so we need to wait for it to be removed - await wait(100); + if (!this._initPromise) { + this._initPromise = this._initialize(); + } + await this._initPromise; } - 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; + private async getUri(relativeFilePath: string) { + await this.waitForInitialized(); + return Uri.file(path.join(this.sharedGKDataFolder, relativeFilePath)); } - return true; -} + 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; + } -export async function releaseSharedFolderWriteLock(): Promise { - try { - const lockFileUri = getSharedLockFileUri(); - await workspace.fs.delete(lockFileUri); - } catch (error) { - Logger.error(error, 'releaseSharedFolderWriteLock'); - return false; + return true; } - 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; + } -function getSharedLockFileUri() { - return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'lockfile')); -} + return true; + } -export function getSharedRepositoryMappingFileUri() { - return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'repoMapping.json')); -} + async getSharedRepositoryMappingFileUri() { + return this.getUri('repoMapping.json'); + } -export function getSharedCloudWorkspaceMappingFileUri() { - return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'cloudWorkspaces.json')); -} + async getSharedCloudWorkspaceMappingFileUri() { + return this.getUri('cloudWorkspaces.json'); + } -export function getSharedLocalWorkspaceMappingFileUri() { - return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'localWorkspaces.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( diff --git a/src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts b/src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts index 2f4d66df16ac1..1eef26a162288 100644 --- a/src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts +++ b/src/env/node/pathMapping/workspacesLocalPathMappingProvider.ts @@ -7,13 +7,7 @@ import type { } from '../../../plus/workspaces/models'; import type { WorkspacesPathMappingProvider } from '../../../plus/workspaces/workspacesPathMappingProvider'; import { Logger } from '../../../system/logger'; -import { - acquireSharedFolderWriteLock, - getSharedCloudWorkspaceMappingFileUri, - getSharedLegacyLocalWorkspaceMappingFileUri, - getSharedLocalWorkspaceMappingFileUri, - releaseSharedFolderWriteLock, -} from './sharedGKDataFolder'; +import { getSharedLegacyLocalWorkspaceMappingFileUri, SharedGKDataFolderMapper } from './sharedGKDataFolder'; export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMappingProvider { private _cloudWorkspacePathMap: CloudWorkspacesPathMap | undefined = undefined; @@ -30,7 +24,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping } private async loadCloudWorkspacePathMap(): Promise { - const localFileUri = getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await SharedGKDataFolderMapper.getSharedCloudWorkspaceMappingFileUri(); try { const data = await workspace.fs.readFile(localFileUri); this._cloudWorkspacePathMap = (JSON.parse(data.toString())?.workspaces ?? {}) as CloudWorkspacesPathMap; @@ -50,7 +44,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping } async removeCloudWorkspaceCodeWorkspaceFilePath(cloudWorkspaceId: string): Promise { - if (!(await acquireSharedFolderWriteLock())) { + if (!(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { return; } @@ -60,14 +54,14 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping delete this._cloudWorkspacePathMap[cloudWorkspaceId].externalLinks['.code-workspace']; - const localFileUri = getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await SharedGKDataFolderMapper.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'); } - await releaseSharedFolderWriteLock(); + await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } async confirmCloudWorkspaceCodeWorkspaceFilePath(cloudWorkspaceId: string): Promise { @@ -87,7 +81,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping repoId: string, repoLocalPath: string, ): Promise { - if (!(await acquireSharedFolderWriteLock())) { + if (!(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { return; } @@ -107,21 +101,21 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping this._cloudWorkspacePathMap[cloudWorkspaceId].repoPaths[repoId] = repoLocalPath; - const localFileUri = getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await SharedGKDataFolderMapper.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'); } - await releaseSharedFolderWriteLock(); + await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } async writeCloudWorkspaceCodeWorkspaceFilePathToMap( cloudWorkspaceId: string, codeWorkspaceFilePath: string, ): Promise { - if (!(await acquireSharedFolderWriteLock())) { + if (!(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) { return; } @@ -141,14 +135,14 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping this._cloudWorkspacePathMap[cloudWorkspaceId].externalLinks['.code-workspace'] = codeWorkspaceFilePath; - const localFileUri = getSharedCloudWorkspaceMappingFileUri(); + const localFileUri = await SharedGKDataFolderMapper.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'); } - await releaseSharedFolderWriteLock(); + await SharedGKDataFolderMapper.releaseSharedFolderWriteLock(); } // TODO@ramint: May want a file watcher on this file down the line @@ -158,7 +152,7 @@ export class WorkspacesLocalPathMappingProvider implements WorkspacesPathMapping let localFileUri; let data; try { - localFileUri = getSharedLocalWorkspaceMappingFileUri(); + localFileUri = await SharedGKDataFolderMapper.getSharedLocalWorkspaceMappingFileUri(); data = await workspace.fs.readFile(localFileUri); if (data?.length) return JSON.parse(data.toString()) as LocalWorkspaceFileData; } catch (_ex) { diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile index fc877bb5cd580..fbc83fa540aa2 100644 --- a/tests/docker/Dockerfile +++ b/tests/docker/Dockerfile @@ -20,7 +20,7 @@ COPY supervisor.conf /etc/supervisor/conf.d/supervisor.conf # --- --- --- --- --- --- --- # enable node source repo -RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - +RUN curl -sL https://deb.nodesource.com/setup_22.x | bash - # enable yarn repo # RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - # RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list @@ -31,3 +31,4 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* RUN npm install -g pnpm@10 + diff --git a/tests/docker/run-e2e-test-local.sh b/tests/docker/run-e2e-test-local.sh index a8b51ae72ebe6..40925fc6fa370 100755 --- a/tests/docker/run-e2e-test-local.sh +++ b/tests/docker/run-e2e-test-local.sh @@ -4,7 +4,7 @@ docker build . -t e2e-test cd ../../ -npm install -g pnpm +npm install -g pnpm@10 pnpm install mkdir -p out