Skip to content

Commit

Permalink
Makes GitLens XDG-compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
nzaytsev committed Dec 24, 2024
1 parent 21cbf8a commit 75caad1
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 72 deletions.
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();
}
}
131 changes: 88 additions & 43 deletions src/env/node/pathMapping/sharedGKDataFolder.ts
Original file line number Diff line number Diff line change
@@ -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<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
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<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
// eslint-disable-next-line import-x/no-default-export
export default new SharedGKDataFolderMapper();

export function getSharedLegacyLocalWorkspaceMappingFileUri() {
return Uri.file(
path.join(
Expand Down
Loading

0 comments on commit 75caad1

Please sign in to comment.