Skip to content

Commit bb0eccc

Browse files
committed
Makes GitLens XDG-compatible
1 parent 7c5b385 commit bb0eccc

7 files changed

+153
-72
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1111
- Adds go to home view button to the commit graph title section — closes [#3873](https://github.com/gitkraken/vscode-gitlens/issues/3873)
1212
- Adds a _Contributors_ section to comparison results in the views
1313

14+
### Changed
15+
16+
- Makes GitLens XDG-compatible— closes [#3660](https://github.com/gitkraken/vscode-gitlens/issues/3660)
17+
1418
### Fixed
1519

1620
- Fixes [#3915](https://github.com/gitkraken/vscode-gitlens/issues/3915) - Closing a split editor with annotations causes the Clear Annotations button to get stuck

ThirdPartyNotices.txt

+17-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ This project incorporates components from the projects listed below.
3333
28. signal-utils version 0.21.1 (https://github.com/proposal-signals/signal-utils)
3434
29. slug version 10.0.0 (https://github.com/Trott/slug)
3535
30. sortablejs version 1.15.0 (https://github.com/SortableJS/Sortable)
36+
31. xdg-basedir version 5.1.0 (https://github.com/sindresorhus/xdg-basedir)
3637

3738
%% @gk-nzaytsev/fast-string-truncated-width NOTICES AND INFORMATION BEGIN HERE
3839
=========================================
@@ -2244,4 +2245,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22442245
SOFTWARE.
22452246

22462247
=========================================
2247-
END OF sortablejs NOTICES AND INFORMATION
2248+
END OF sortablejs NOTICES AND INFORMATION
2249+
2250+
%% xdg-basedir NOTICES AND INFORMATION BEGIN HERE
2251+
=========================================
2252+
MIT License
2253+
2254+
Copyright (c) Sindre Sorhus <[email protected]> (https://sindresorhus.com)
2255+
2256+
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:
2257+
2258+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2259+
2260+
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.
2261+
2262+
=========================================
2263+
END OF xdg-basedir NOTICES AND INFORMATION

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -20035,7 +20035,8 @@
2003520035
"react-dom": "16.8.4",
2003620036
"signal-utils": "0.21.1",
2003720037
"slug": "10.0.0",
20038-
"sortablejs": "1.15.0"
20038+
"sortablejs": "1.15.0",
20039+
"xdg-basedir": "5.1.0"
2003920040
},
2004020041
"devDependencies": {
2004120042
"@eamodio/eslint-lite-webpack-plugin": "0.2.0",

pnpm-lock.yaml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/env/node/pathMapping/repositoryLocalPathMappingProvider.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import type { Container } from '../../../container';
44
import type { LocalRepoDataMap } from '../../../pathMapping/models';
55
import type { RepositoryPathMappingProvider } from '../../../pathMapping/repositoryPathMappingProvider';
66
import { Logger } from '../../../system/logger';
7-
import {
8-
acquireSharedFolderWriteLock,
9-
getSharedRepositoryMappingFileUri,
10-
releaseSharedFolderWriteLock,
11-
} from './sharedGKDataFolder';
7+
import { SharedGKDataFolderMapper } from './sharedGKDataFolder';
128

139
export class RepositoryLocalPathMappingProvider implements RepositoryPathMappingProvider, Disposable {
1410
constructor(private readonly container: Container) {}
@@ -58,7 +54,7 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping
5854
}
5955

6056
private async loadLocalRepoDataMap() {
61-
const localFileUri = getSharedRepositoryMappingFileUri();
57+
const localFileUri = await SharedGKDataFolderMapper.getSharedRepositoryMappingFileUri();
6258
try {
6359
const data = await workspace.fs.readFile(localFileUri);
6460
this._localRepoDataMap = (JSON.parse(data.toString()) ?? {}) as LocalRepoDataMap;
@@ -86,7 +82,7 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping
8682
}
8783

8884
private async _writeLocalRepoPath(key: string, localPath: string): Promise<void> {
89-
if (!key || !localPath || !(await acquireSharedFolderWriteLock())) {
85+
if (!key || !localPath || !(await SharedGKDataFolderMapper.acquireSharedFolderWriteLock())) {
9086
return;
9187
}
9288

@@ -103,13 +99,13 @@ export class RepositoryLocalPathMappingProvider implements RepositoryPathMapping
10399
this._localRepoDataMap[key].paths.push(localPath);
104100
}
105101

106-
const localFileUri = getSharedRepositoryMappingFileUri();
102+
const localFileUri = await SharedGKDataFolderMapper.getSharedRepositoryMappingFileUri();
107103
const outputData = new Uint8Array(Buffer.from(JSON.stringify(this._localRepoDataMap)));
108104
try {
109105
await workspace.fs.writeFile(localFileUri, outputData);
110106
} catch (error) {
111107
Logger.error(error, 'writeLocalRepoPath');
112108
}
113-
await releaseSharedFolderWriteLock();
109+
await SharedGKDataFolderMapper.releaseSharedFolderWriteLock();
114110
}
115111
}

src/env/node/pathMapping/sharedGKDataFolder.ts

+104-43
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,134 @@
11
import os from 'os';
22
import path from 'path';
3+
import { env } from 'process';
34
import { Uri, workspace } from 'vscode';
5+
import { xdgData } from 'xdg-basedir';
46
import { Logger } from '../../../system/logger';
57
import { wait } from '../../../system/promise';
68
import { getPlatform } from '../platform';
79

8-
export const sharedGKDataFolder = '.gk';
10+
/** @deprecated prefer using XDG paths */
11+
const legacySharedGKDataFolder = path.join(os.homedir(), '.gk');
912

10-
export async function acquireSharedFolderWriteLock(): Promise<boolean> {
11-
const lockFileUri = getSharedLockFileUri();
13+
class SharedGKDataFolderMapper {
14+
private _initPromise: Promise<void> | undefined;
15+
constructor(
16+
// do soft migration, use new folders only for new users (without existing folders)
17+
// eslint-disable-next-line @typescript-eslint/no-deprecated
18+
private sharedGKDataFolder = legacySharedGKDataFolder,
19+
private _isInitialized: boolean = false,
20+
) {}
1221

13-
let stat;
14-
while (true) {
22+
private async _initialize() {
23+
if (this._initPromise) {
24+
throw new Error('cannot be initialized multiple times');
25+
}
1526
try {
16-
stat = await workspace.fs.stat(lockFileUri);
27+
await workspace.fs.stat(Uri.file(this.sharedGKDataFolder));
1728
} catch {
18-
// File does not exist, so we can safely create it
19-
break;
29+
// Path does not exist, so we can safely use xdg paths
30+
const platform = getPlatform();
31+
const folderName = 'gk';
32+
switch (platform) {
33+
case 'windows':
34+
if (env.LOCALAPPDATA) {
35+
this.sharedGKDataFolder = path.join(env.LOCALAPPDATA, folderName, 'Data');
36+
} else {
37+
this.sharedGKDataFolder = path.join(os.homedir(), 'AppData', 'Local', folderName, 'Data');
38+
}
39+
break;
40+
case 'macOS':
41+
this.sharedGKDataFolder = path.join(os.homedir(), 'Library', 'Application Support', folderName);
42+
break;
43+
default:
44+
if (xdgData) {
45+
this.sharedGKDataFolder = path.join(xdgData, folderName);
46+
} else {
47+
this.sharedGKDataFolder = path.join(os.homedir(), '.local', 'share', folderName);
48+
}
49+
}
50+
} finally {
51+
this._isInitialized = true;
2052
}
53+
}
2154

22-
const currentTime = new Date().getTime();
23-
if (currentTime - stat.ctime > 30000) {
24-
// File exists, but the timestamp is older than 30 seconds, so we can safely remove it
25-
break;
55+
private async waitForInitialized() {
56+
if (this._isInitialized) {
57+
return;
2658
}
27-
28-
// File exists, and the timestamp is less than 30 seconds old, so we need to wait for it to be removed
29-
await wait(100);
59+
if (!this._initPromise) {
60+
this._initPromise = this._initialize();
61+
}
62+
await this._initPromise;
3063
}
3164

32-
try {
33-
// write the lockfile to the shared data folder
34-
await workspace.fs.writeFile(lockFileUri, new Uint8Array(0));
35-
} catch (error) {
36-
Logger.error(error, 'acquireSharedFolderWriteLock');
37-
return false;
65+
private async getUri(relativeFilePath: string) {
66+
await this.waitForInitialized();
67+
return Uri.file(path.join(this.sharedGKDataFolder, relativeFilePath));
3868
}
3969

40-
return true;
41-
}
70+
async acquireSharedFolderWriteLock(): Promise<boolean> {
71+
const lockFileUri = await this.getUri('lockfile');
72+
73+
let stat;
74+
while (true) {
75+
try {
76+
stat = await workspace.fs.stat(lockFileUri);
77+
} catch {
78+
// File does not exist, so we can safely create it
79+
break;
80+
}
81+
82+
const currentTime = new Date().getTime();
83+
if (currentTime - stat.ctime > 30000) {
84+
// File exists, but the timestamp is older than 30 seconds, so we can safely remove it
85+
break;
86+
}
87+
88+
// File exists, and the timestamp is less than 30 seconds old, so we need to wait for it to be removed
89+
await wait(100);
90+
}
91+
92+
try {
93+
// write the lockfile to the shared data folder
94+
await workspace.fs.writeFile(lockFileUri, new Uint8Array(0));
95+
} catch (error) {
96+
Logger.error(error, 'acquireSharedFolderWriteLock');
97+
return false;
98+
}
4299

43-
export async function releaseSharedFolderWriteLock(): Promise<boolean> {
44-
try {
45-
const lockFileUri = getSharedLockFileUri();
46-
await workspace.fs.delete(lockFileUri);
47-
} catch (error) {
48-
Logger.error(error, 'releaseSharedFolderWriteLock');
49-
return false;
100+
return true;
50101
}
51102

52-
return true;
53-
}
103+
async releaseSharedFolderWriteLock(): Promise<boolean> {
104+
try {
105+
const lockFileUri = await this.getUri('lockfile');
106+
await workspace.fs.delete(lockFileUri);
107+
} catch (error) {
108+
Logger.error(error, 'releaseSharedFolderWriteLock');
109+
return false;
110+
}
54111

55-
function getSharedLockFileUri() {
56-
return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'lockfile'));
57-
}
112+
return true;
113+
}
58114

59-
export function getSharedRepositoryMappingFileUri() {
60-
return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'repoMapping.json'));
61-
}
115+
async getSharedRepositoryMappingFileUri() {
116+
return this.getUri('repoMapping.json');
117+
}
62118

63-
export function getSharedCloudWorkspaceMappingFileUri() {
64-
return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'cloudWorkspaces.json'));
65-
}
119+
async getSharedCloudWorkspaceMappingFileUri() {
120+
return this.getUri('cloudWorkspaces.json');
121+
}
66122

67-
export function getSharedLocalWorkspaceMappingFileUri() {
68-
return Uri.file(path.join(os.homedir(), sharedGKDataFolder, 'localWorkspaces.json'));
123+
async getSharedLocalWorkspaceMappingFileUri() {
124+
return this.getUri('localWorkspaces.json');
125+
}
69126
}
70127

128+
// export as a singleton
129+
const instance = new SharedGKDataFolderMapper();
130+
export { instance as SharedGKDataFolderMapper };
131+
71132
export function getSharedLegacyLocalWorkspaceMappingFileUri() {
72133
return Uri.file(
73134
path.join(

0 commit comments

Comments
 (0)