Skip to content

Commit 72ada25

Browse files
committed
Optimizes Git remote, tree, and file parsers
- Adds `exists` to revision sub-provider for better usage alignment
1 parent 8f07681 commit 72ada25

File tree

7 files changed

+190
-157
lines changed

7 files changed

+190
-157
lines changed

src/env/node/git/git.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,28 +1089,6 @@ export class Git {
10891089
}
10901090
}
10911091

1092-
async ls_files(
1093-
repoPath: string,
1094-
fileName: string,
1095-
options?: { rev?: string; untracked?: boolean },
1096-
): Promise<string> {
1097-
const params = ['ls-files'];
1098-
if (options?.rev) {
1099-
if (!isUncommitted(options.rev)) {
1100-
params.push(`--with-tree=${options.rev}`);
1101-
} else if (isUncommittedStaged(options.rev)) {
1102-
params.push('--stage');
1103-
}
1104-
}
1105-
1106-
if (!options?.rev && options?.untracked) {
1107-
params.push('-o');
1108-
}
1109-
1110-
const result = await this.exec({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params, '--', fileName);
1111-
return result.stdout.trim();
1112-
}
1113-
11141092
async reset(
11151093
repoPath: string,
11161094
pathspecs: string[],

src/env/node/git/localGitProvider.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import type { LogScope } from '../../../system/logger.scope';
7070
import { getLogScope, setLogScopeExit } from '../../../system/logger.scope';
7171
import { commonBaseIndex, dirname, isAbsolute, maybeUri, normalizePath, pathEquals } from '../../../system/path';
7272
import { any, asSettled, getSettledValue } from '../../../system/promise';
73-
import { equalsIgnoreCase, getDurationMilliseconds, splitSingle } from '../../../system/string';
73+
import { equalsIgnoreCase, getDurationMilliseconds } from '../../../system/string';
7474
import { compare, fromString } from '../../../system/version';
7575
import type { CachedBlame, CachedDiff, TrackedGitDocument } from '../../../trackers/trackedDocument';
7676
import { GitDocumentState } from '../../../trackers/trackedDocument';
@@ -786,12 +786,12 @@ export class LocalGitProvider implements GitProvider, Disposable {
786786
// TODO@eamodio Align this with isTrackedCore?
787787
if (!rev || (isUncommitted(rev) && !isUncommittedStaged(rev))) {
788788
// Make sure the file exists in the repo
789-
let data = await this.git.ls_files(repoPath, path);
790-
if (data) return this.getAbsoluteUri(path, repoPath);
789+
let exists = await this.revision.exists(repoPath, path);
790+
if (exists) return this.getAbsoluteUri(path, repoPath);
791791

792792
// Check if the file exists untracked
793-
data = await this.git.ls_files(repoPath, path, { untracked: true });
794-
if (data) return this.getAbsoluteUri(path, repoPath);
793+
exists = await this.revision.exists(repoPath, path, { untracked: true });
794+
if (exists) return this.getAbsoluteUri(path, repoPath);
795795

796796
return undefined;
797797
}
@@ -890,15 +890,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
890890
async getWorkingUri(repoPath: string, uri: Uri): Promise<Uri | undefined> {
891891
let relativePath = this.getRelativePath(uri, repoPath);
892892

893-
let data;
894893
let result;
895894
let rev;
896895
do {
897-
data = await this.git.ls_files(repoPath, relativePath);
898-
if (data) {
899-
relativePath = splitSingle(data, '\n')[0];
900-
break;
901-
}
896+
if (await this.revision.exists(repoPath, relativePath)) break;
902897

903898
// TODO: Add caching
904899

@@ -1996,14 +1991,14 @@ export class LocalGitProvider implements GitProvider, Disposable {
19961991
}
19971992

19981993
// Even if we have a ref, check first to see if the file exists (that way the cache will be better reused)
1999-
let tracked = Boolean(await this.git.ls_files(repoPath, relativePath));
1994+
let tracked = await this.revision.exists(repoPath, relativePath);
20001995
if (tracked) return [relativePath, repoPath];
20011996

20021997
if (repoPath) {
20031998
const [newRelativePath, newRepoPath] = splitPath(path, '', true);
20041999
if (newRelativePath !== relativePath) {
20052000
// If we didn't find it, check it as close to the file as possible (will find nested repos)
2006-
tracked = Boolean(await this.git.ls_files(newRepoPath, newRelativePath));
2001+
tracked = await this.revision.exists(newRepoPath, newRelativePath);
20072002
if (tracked) {
20082003
repository = await this.container.git.getOrOpenRepository(Uri.file(path), {
20092004
detectNested: true,
@@ -2018,10 +2013,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
20182013
}
20192014

20202015
if (!tracked && ref && !isUncommitted(ref)) {
2021-
tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { rev: ref }));
2016+
tracked = await this.revision.exists(repoPath, relativePath, ref);
20222017
// If we still haven't found this file, make sure it wasn't deleted in that ref (i.e. check the previous)
20232018
if (!tracked) {
2024-
tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { rev: `${ref}^` }));
2019+
tracked = await this.revision.exists(repoPath, relativePath, `${ref}^`);
20252020
}
20262021
}
20272022

src/env/node/git/sub-providers/revision.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { GitFileStatus } from '../../../../git/models/fileStatus';
77
import { deletedOrMissing } from '../../../../git/models/revision';
88
import type { GitTreeEntry } from '../../../../git/models/tree';
99
import { getShaAndFileSummaryLogParser } from '../../../../git/parsers/logParser';
10-
import { parseGitLsFiles, parseGitTree } from '../../../../git/parsers/treeParser';
10+
import { parseGitLsFilesStaged, parseGitTree } from '../../../../git/parsers/treeParser';
1111
import {
1212
isRevisionWithSuffix,
1313
isSha,
@@ -22,6 +22,8 @@ import { first } from '../../../../system/iterable';
2222
import type { Git } from '../git';
2323
import type { LocalGitProvider } from '../localGitProvider';
2424

25+
const emptyArray: readonly any[] = Object.freeze([]);
26+
2527
export class RevisionGitSubProvider implements GitRevisionSubProvider {
2628
constructor(
2729
private readonly container: Container,
@@ -30,6 +32,32 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
3032
private readonly provider: LocalGitProvider,
3133
) {}
3234

35+
exists(repoPath: string, path: string, rev?: string): Promise<boolean>;
36+
exists(repoPath: string, path: string, options?: { untracked?: boolean }): Promise<boolean>;
37+
async exists(repoPath: string, path: string, revOrOptions?: string | { untracked?: boolean }): Promise<boolean> {
38+
let rev;
39+
let untracked;
40+
if (typeof revOrOptions === 'string') {
41+
rev = revOrOptions;
42+
} else if (revOrOptions != null) {
43+
untracked = revOrOptions.untracked;
44+
}
45+
46+
const args = ['ls-files'];
47+
if (rev) {
48+
if (!isUncommitted(rev)) {
49+
args.push(`--with-tree=${rev}`);
50+
} else if (isUncommittedStaged(rev)) {
51+
args.push('--stage');
52+
}
53+
} else if (untracked) {
54+
args.push('-o');
55+
}
56+
57+
const result = await this.git.exec({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...args, '--', path);
58+
return Boolean(result.stdout.trim());
59+
}
60+
3361
@gate()
3462
@log()
3563
getRevisionContent(repoPath: string, rev: string, path: string): Promise<Uint8Array | undefined> {
@@ -43,47 +71,46 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
4371
@gate()
4472
@log()
4573
async getTreeEntryForRevision(repoPath: string, rev: string, path: string): Promise<GitTreeEntry | undefined> {
46-
if (repoPath == null || !path) return undefined;
74+
if (!repoPath || !path) return undefined;
4775

4876
const [relativePath, root] = splitPath(path, repoPath);
4977

5078
if (isUncommittedStaged(rev)) {
51-
let data = await this.git.ls_files(root, relativePath, { rev: rev });
52-
const [entry] = parseGitLsFiles(data);
79+
let result = await this.git.exec(
80+
{ cwd: root, errors: GitErrorHandling.Ignore },
81+
'ls-files',
82+
'--stage',
83+
'--',
84+
relativePath,
85+
);
86+
87+
const [entry] = parseGitLsFilesStaged(result.stdout, true);
5388
if (entry == null) return undefined;
5489

55-
const result = await this.git.exec({ cwd: repoPath }, 'cat-file', '-s', entry.oid);
56-
data = result.stdout.trim();
57-
const size = data ? parseInt(data, 10) : 0;
90+
result = await this.git.exec({ cwd: root }, 'cat-file', '-s', entry.oid);
91+
const size = result ? parseInt(result.stdout.trim(), 10) : 0;
5892

59-
return {
60-
ref: rev,
61-
oid: entry.oid,
62-
path: relativePath,
63-
size: size,
64-
type: 'blob',
65-
};
93+
return { ref: rev, oid: entry.oid, path: relativePath, size: size, type: 'blob' };
6694
}
6795

68-
const entries = await this.getTreeForRevisionCore(repoPath, rev, path);
69-
return entries[0];
96+
const [entry] = await this.getTreeForRevisionCore(repoPath, rev, path);
97+
return entry;
7098
}
7199

72100
@log()
73101
async getTreeForRevision(repoPath: string, rev: string): Promise<GitTreeEntry[]> {
74-
if (repoPath == null) return [];
75-
76-
return this.getTreeForRevisionCore(repoPath, rev);
102+
return repoPath ? this.getTreeForRevisionCore(repoPath, rev) : [];
77103
}
78104

79105
@gate()
80106
private async getTreeForRevisionCore(repoPath: string, rev: string, path?: string): Promise<GitTreeEntry[]> {
81-
const args = path ? ['-l', rev, '--', path] : ['-lrt', rev, '--'];
82-
const result = await this.git.exec({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'ls-tree', ...args);
107+
const hasPath = Boolean(path);
108+
const args = hasPath ? ['ls-tree', '-l', rev, '--', path] : ['ls-tree', '-lrt', rev, '--'];
109+
const result = await this.git.exec({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...args);
83110
const data = result.stdout.trim();
84-
if (!data) return [];
111+
if (!data) return emptyArray as GitTreeEntry[];
85112

86-
return parseGitTree(data, rev);
113+
return parseGitTree(data, rev, hasPath);
87114
}
88115

89116
@log()

src/git/fsProvider.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class GitFileSystemProvider implements FileSystemProvider, Disposable {
5858
const { path, ref, repoPath } = fromGitLensFSUri(uri);
5959

6060
const tree = await this.getTree(path, ref, repoPath);
61-
if (tree === undefined) throw FileSystemError.FileNotFound(uri);
61+
if (tree == null) throw FileSystemError.FileNotFound(uri);
6262

6363
const items = [
6464
...map<GitTreeEntry, [string, FileType]>(tree, t => [
@@ -88,12 +88,7 @@ export class GitFileSystemProvider implements FileSystemProvider, Disposable {
8888
const { path, ref, repoPath } = fromGitLensFSUri(uri);
8989

9090
if (ref === deletedOrMissing) {
91-
return {
92-
type: FileType.File,
93-
size: 0,
94-
ctime: 0,
95-
mtime: 0,
96-
};
91+
return { type: FileType.File, size: 0, ctime: 0, mtime: 0 };
9792
}
9893

9994
let treeItem;
@@ -103,29 +98,19 @@ export class GitFileSystemProvider implements FileSystemProvider, Disposable {
10398
// Add the fake root folder to the path
10499
treeItem = (await searchTree).get(`/~/${path}`);
105100
} else {
106-
if (path == null || path.length === 0) {
101+
if (!path) {
107102
const tree = await this.getTree(path, ref, repoPath);
108103
if (tree == null) throw FileSystemError.FileNotFound(uri);
109104

110-
return {
111-
type: FileType.Directory,
112-
size: 0,
113-
ctime: 0,
114-
mtime: 0,
115-
};
105+
return { type: FileType.Directory, size: 0, ctime: 0, mtime: 0 };
116106
}
117107

118108
treeItem = await this.container.git.revision(repoPath).getTreeEntryForRevision(ref, path);
119109
}
120110

121111
if (treeItem == null) throw FileSystemError.FileNotFound(uri);
122112

123-
return {
124-
type: typeToFileType(treeItem.type),
125-
size: treeItem.size,
126-
ctime: 0,
127-
mtime: 0,
128-
};
113+
return { type: typeToFileType(treeItem.type), size: treeItem.size, ctime: 0, mtime: 0 };
129114
}
130115

131116
watch(): Disposable {
@@ -151,7 +136,7 @@ export class GitFileSystemProvider implements FileSystemProvider, Disposable {
151136

152137
private getOrCreateSearchTree(ref: string, repoPath: string) {
153138
let searchTree = this._searchTreeMap.get(ref);
154-
if (searchTree === undefined) {
139+
if (searchTree == null) {
155140
searchTree = this.createSearchTree(ref, repoPath);
156141
this._searchTreeMap.set(ref, searchTree);
157142
}

src/git/parsers/remoteParser.ts

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { Container } from '../../container';
22
import { maybeStopWatch } from '../../system/stopwatch';
3+
import { iterateByDelimiter } from '../../system/string';
34
import type { GitRemoteType } from '../models/remote';
45
import { GitRemote } from '../models/remote';
56
import type { getRemoteProviderMatcher } from '../remotes/remoteProviders';
67

7-
const remoteRegex = /^(.*)\t(.*)\s\((.*)\)$/gm;
8-
98
export function parseGitRemotes(
109
container: Container,
1110
data: string,
@@ -18,31 +17,47 @@ export function parseGitRemotes(
1817
return [];
1918
}
2019

20+
// Format: <name>\t<url> (<type>)
21+
2122
const remotes = new Map<string, GitRemote>();
2223

23-
let name;
24-
let url;
25-
let type;
24+
let name: string;
25+
let url: string;
26+
let type: GitRemoteType;
2627

27-
let scheme;
28-
let domain;
29-
let path;
28+
let scheme: string;
29+
let domain: string;
30+
let path: string;
3031

3132
let remote: GitRemote | undefined;
3233

33-
let match;
34-
do {
35-
match = remoteRegex.exec(data);
36-
if (match == null) break;
34+
let startIndex = 0;
35+
let endIndex = 0;
36+
37+
for (let line of iterateByDelimiter(data, '\n')) {
38+
line = line.trim();
39+
if (!line) continue;
40+
41+
// Parse name
42+
startIndex = 0;
43+
endIndex = line.indexOf('\t');
44+
if (endIndex === -1) continue;
3745

38-
[, name, url, type] = match;
46+
name = line.substring(startIndex, endIndex);
3947

40-
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
41-
name = ` ${name}`.substring(1);
42-
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
43-
url = ` ${url}`.substring(1);
44-
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
45-
type = ` ${type}`.substring(1);
48+
// Parse url
49+
startIndex = endIndex + 1;
50+
endIndex = line.lastIndexOf(' (');
51+
if (endIndex === -1) continue;
52+
53+
url = line.substring(startIndex, endIndex);
54+
55+
// Parse type
56+
startIndex = endIndex + 2;
57+
endIndex = line.lastIndexOf(')');
58+
if (endIndex === -1) continue;
59+
60+
type = line.substring(startIndex, endIndex) as GitRemoteType;
4661

4762
[scheme, domain, path] = parseGitRemoteUrl(url);
4863

@@ -56,11 +71,11 @@ export function parseGitRemotes(
5671
domain,
5772
path,
5873
remoteProviderMatcher(url, domain, path, scheme),
59-
[{ url: url, type: type as GitRemoteType }],
74+
[{ url: url, type: type }],
6075
);
6176
remotes.set(name, remote);
6277
} else {
63-
remote.urls.push({ url: url, type: type as GitRemoteType });
78+
remote.urls.push({ url: url, type: type });
6479
if (remote.provider != null && type !== 'push') continue;
6580

6681
const provider = remoteProviderMatcher(url, domain, path, scheme);
@@ -69,7 +84,7 @@ export function parseGitRemotes(
6984
remote = new GitRemote(container, repoPath, name, scheme, domain, path, provider, remote.urls);
7085
remotes.set(name, remote);
7186
}
72-
} while (true);
87+
}
7388

7489
sw?.stop({ suffix: ` parsed ${remotes.size} remotes` });
7590

0 commit comments

Comments
 (0)