Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makes GitLens XDG-compatible #3904

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [#3888](https://github.com/gitkraken/vscode-gitlens/issues/#3888) - Graph hover should disappear when right-clicking a row
Expand Down
18 changes: 17 additions & 1 deletion ThirdPartyNotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
=========================================
Expand Down Expand Up @@ -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
END OF sortablejs NOTICES AND INFORMATION

%% xdg-basedir NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License

Copyright (c) Sindre Sorhus <[email protected]> (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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 5 additions & 9 deletions src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -86,7 +82,7 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping
}

private async _writeLocalRepoPath(key: string, localPath: string): Promise<void> {
if (!key || !localPath || !(await acquireSharedFolderWriteLock())) {
if (!key || !localPath || !(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) {
return;
}

Expand All @@ -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();
}
}
147 changes: 104 additions & 43 deletions src/env/node/pathMapping/sharedGKDataFolder.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
const lockFileUri = getSharedLockFileUri();
class SharedGKDataFolderMapper {
private _initPromise: Promise<void> | 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
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<boolean> {
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<boolean> {
try {
const lockFileUri = getSharedLockFileUri();
await workspace.fs.delete(lockFileUri);
} catch (error) {
Logger.error(error, 'releaseSharedFolderWriteLock');
return false;
return true;
}

return true;
}
async releaseSharedFolderWriteLock(): Promise<boolean> {
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(
Expand Down
Loading
Loading