From 5f1a57e66db27a186f1b7e7470ff951e7aaa20c3 Mon Sep 17 00:00:00 2001 From: nzaytsev Date: Mon, 23 Dec 2024 17:40:18 +0700 Subject: [PATCH] Makes GitLens XDG-compatible --- CHANGELOG.md | 4 + ThirdPartyNotices.txt | 18 ++- package.json | 3 +- pnpm-lock.yaml | 9 ++ .../repositoryLocalPathMappingProvider.ts | 14 +- .../node/pathMapping/sharedGKDataFolder.ts | 131 ++++++++++++------ .../workspacesLocalPathMappingProvider.ts | 30 ++-- 7 files changed, 137 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55170f518fd09..aa4c71bf2d978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - 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) + ## [16.1.1] - 2024-12-20 ### Added diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 860199840847e..92388c75e6fb9 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.20.0 (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 299cd48e1f275..1494731d7182d 100644 --- a/package.json +++ b/package.json @@ -20030,7 +20030,8 @@ "react-dom": "16.8.4", "signal-utils": "0.20.0", "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 a605c267b14f6..bf98e7474223a 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 @@ -5254,6 +5257,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'} @@ -10779,6 +10786,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..396c12934654a 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..722c0107b6821 100644 --- a/src/env/node/pathMapping/sharedGKDataFolder.ts +++ b/src/env/node/pathMapping/sharedGKDataFolder.ts @@ -1,73 +1,118 @@ import os from 'os'; import path from 'path'; 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 it + if (xdgData) { + this.sharedGKDataFolder = path.join(xdgData, 'gk'); + } else { + this.sharedGKDataFolder = path.join(os.homedir()); + } + } 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 +// eslint-disable-next-line import-x/no-default-export +export default new 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..e6a4715a0938e 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 SharedGKDataFolderMapper, { getSharedLegacyLocalWorkspaceMappingFileUri } 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) {