Skip to content

Commit 0cb2ce5

Browse files
authored
fix: tracking really large repos in chunks, lower limit for windows
* fix: chunk really large git.add, better error output * test: nut for large project scale * refactor: smaller file limit for windows * refactor: come on windows, you can do it * refactor: use ternary correctly
1 parent b50685e commit 0cb2ce5

File tree

5 files changed

+116
-23
lines changed

5 files changed

+116
-23
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@salesforce/kit": "^1.5.17",
4848
"@salesforce/source-deploy-retrieve": "^5.9.4",
4949
"graceful-fs": "^4.2.9",
50-
"isomorphic-git": "1.16.0",
50+
"isomorphic-git": "1.17.0",
5151
"ts-retry-promise": "^0.6.0"
5252
},
5353
"devDependencies": {

src/shared/functions.ts

+4
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ export const pathIsInFolder = (filePath: string, folder: string): boolean => {
3838
const nonEmptyStringFilter = (value: string): boolean => {
3939
return isString(value) && value.length > 0;
4040
};
41+
42+
// adapted for TS from https://github.com/30-seconds/30-seconds-of-code/blob/master/snippets/chunk.md
43+
export const chunkArray = <T>(arr: T[], size: number): T[][] =>
44+
Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => arr.slice(i * size, i * size + size));

src/shared/localShadowRepo.ts

+32-18
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@
88
import * as path from 'path';
99
import * as os from 'os';
1010
import * as fs from 'graceful-fs';
11-
import { NamedPackageDir, Logger } from '@salesforce/core';
11+
import { NamedPackageDir, Logger, SfdxError } from '@salesforce/core';
1212
import * as git from 'isomorphic-git';
13-
import { pathIsInFolder } from './functions';
13+
import { chunkArray, pathIsInFolder } from './functions';
1414

15-
const gitIgnoreFileName = '.gitignore';
16-
/**
17-
* returns the full path to where we store the shadow repo
18-
*/
15+
/** returns the full path to where we store the shadow repo */
1916
const getGitDir = (orgId: string, projectPath: string): string => {
2017
return path.join(projectPath, '.sfdx', 'orgs', orgId, 'localSourceTracking');
2118
};
@@ -53,11 +50,17 @@ export class ShadowRepo {
5350
private packageDirs!: NamedPackageDir[];
5451
private status!: StatusRow[];
5552
private logger!: Logger;
53+
private isWindows: boolean;
54+
55+
/** do not try to add more than this many files at a time through isogit. You'll hit EMFILE: too many open files even with graceful-fs */
56+
private maxFileAdd: number;
5657

5758
private constructor(options: ShadowRepoOptions) {
5859
this.gitDir = getGitDir(options.orgId, options.projectPath);
5960
this.projectPath = options.projectPath;
6061
this.packageDirs = options.packageDirs;
62+
this.isWindows = os.type() === 'Windows_NT';
63+
this.maxFileAdd = this.isWindows ? 8000 : 15000;
6164
}
6265

6366
// think of singleton behavior but unique to the projectPath
@@ -113,14 +116,12 @@ export class ShadowRepo {
113116
*/
114117
public async getStatus(noCache = false): Promise<StatusRow[]> {
115118
if (!this.status || noCache) {
116-
// only ask about OS once but use twice
117-
const isWindows = os.type() === 'Windows_NT';
118119
// iso-git uses relative, posix paths
119120
// but packageDirs has already resolved / normalized them
120121
// so we need to make them project-relative again and convert if windows
121122
const filepaths = this.packageDirs
122123
.map((dir) => path.relative(this.projectPath, dir.fullPath))
123-
.map((p) => (isWindows ? p.split(path.sep).join(path.posix.sep) : p));
124+
.map((p) => (this.isWindows ? p.split(path.sep).join(path.posix.sep) : p));
124125

125126
// status hasn't been initalized yet
126127
this.status = await git.statusMatrix({
@@ -135,12 +136,12 @@ export class ShadowRepo {
135136
// no lwc tests
136137
!f.includes('__tests__') &&
137138
// no gitignore files
138-
!f.endsWith(gitIgnoreFileName) &&
139+
!f.endsWith('.gitignore') &&
139140
// isogit uses `startsWith` for filepaths so it's possible to get a false positive
140141
filepaths.some((pkgDir) => pathIsInFolder(f, pkgDir)),
141142
});
142143
// isomorphic-git stores things in unix-style tree. Convert to windows-style if necessary
143-
if (isWindows) {
144+
if (this.isWindows) {
144145
this.status = this.status.map((row) => [path.normalize(row[FILE]), row[HEAD], row[WORKDIR], row[3]]);
145146
}
146147
}
@@ -229,13 +230,26 @@ export class ShadowRepo {
229230
}
230231

231232
if (deployedFiles.length) {
232-
await git.add({
233-
fs,
234-
dir: this.projectPath,
235-
gitdir: this.gitDir,
236-
filepath: [...new Set(deployedFiles)],
237-
force: true,
238-
});
233+
const chunks = chunkArray([...new Set(deployedFiles)], this.maxFileAdd);
234+
for (const chunk of chunks) {
235+
try {
236+
await git.add({
237+
fs,
238+
dir: this.projectPath,
239+
gitdir: this.gitDir,
240+
filepath: chunk,
241+
force: true,
242+
});
243+
} catch (e) {
244+
if (e instanceof git.Errors.MultipleGitError) {
245+
this.logger.error('multiple errors on git.add', e.errors.slice(0, 5));
246+
const error = new SfdxError(e.message, e.name, [], 1);
247+
error.setData(e.errors);
248+
throw error;
249+
}
250+
throw e;
251+
}
252+
}
239253
}
240254

241255
for (const filepath of [...new Set(deletedFiles)]) {

test/nuts/local/tracking-scale.nut.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import * as path from 'path';
8+
import { TestSession } from '@salesforce/cli-plugins-testkit';
9+
import { expect } from 'chai';
10+
import { fs } from '@salesforce/core';
11+
import { ShadowRepo } from '../../../src/shared/localShadowRepo';
12+
13+
const dirCount = 200;
14+
const classesPerDir = 500;
15+
const classCount = dirCount * classesPerDir;
16+
17+
describe(`verify tracking handles an add of ${classCount.toLocaleString()} classes (${(
18+
classCount * 2
19+
).toLocaleString()} files across ${dirCount.toLocaleString()} folders)`, () => {
20+
let session: TestSession;
21+
let repo: ShadowRepo;
22+
let filesToSync: string[];
23+
24+
before(async () => {
25+
session = await TestSession.create({
26+
project: {
27+
name: 'large-repo',
28+
},
29+
authStrategy: 'NONE',
30+
});
31+
// create some number of files
32+
const classdir = path.join(session.project.dir, 'force-app', 'main', 'default', 'classes');
33+
for (let d = 0; d < dirCount; d++) {
34+
const dirName = path.join(classdir, `dir${d}`);
35+
await fs.promises.mkdir(dirName);
36+
for (let c = 0; c < classesPerDir; c++) {
37+
const className = `x${d}x${c}`;
38+
await Promise.all([
39+
fs.promises.writeFile(
40+
path.join(dirName, `${className}.cls`),
41+
`public with sharing class ${className} {public ${className}() {}}`
42+
),
43+
fs.promises.writeFile(
44+
path.join(dirName, `${className}.cls-meta.xml`),
45+
'<?xml version="1.0" encoding="UTF-8"?><ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"><apiVersion>54.0</apiVersion><status>Active</status></ApexClass>'
46+
),
47+
]);
48+
}
49+
}
50+
});
51+
52+
after(async () => {
53+
await session?.clean();
54+
});
55+
56+
it('initialize the local tracking', async () => {
57+
repo = await ShadowRepo.getInstance({
58+
orgId: 'fakeOrgId',
59+
projectPath: session.project.dir,
60+
packageDirs: [{ path: 'force-app', name: 'force-app', fullPath: path.join(session.project.dir, 'force-app') }],
61+
});
62+
});
63+
64+
it(`should see ${(classCount * 2).toLocaleString()} files (git status)`, async () => {
65+
filesToSync = await repo.getChangedFilenames();
66+
expect(filesToSync)
67+
.to.be.an('array')
68+
// windows ends up with 2 extra files!?
69+
.with.length.greaterThanOrEqual(classCount * 2);
70+
});
71+
72+
it('should sync (commit) them locally without error', async () => {
73+
await repo.commitChanges({ deployedFiles: filesToSync, needsUpdatedStatus: false });
74+
});
75+
});

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -3387,10 +3387,10 @@ isexe@^2.0.0:
33873387
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
33883388
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
33893389

3390-
isomorphic-git@1.16.0:
3391-
version "1.16.0"
3392-
resolved "https://registry.yarnpkg.com/isomorphic-git/-/isomorphic-git-1.16.0.tgz#f4de2fd0e1c78bbbe03230fbefa88a84463e736a"
3393-
integrity sha512-pyYcMRp0125hmxsagSaAN63WgHd4x+4sR3eJ71+xdIN0aOij0q5ASPIN7jiJissUHx9/FE4dY73pzDehk+Xomw==
3390+
isomorphic-git@1.17.0:
3391+
version "1.17.0"
3392+
resolved "https://registry.yarnpkg.com/isomorphic-git/-/isomorphic-git-1.17.0.tgz#8bd423ddb8ebfb924799be0ac75fb5bede5cfad7"
3393+
integrity sha512-8ToEVqYLeTE1Ys3UQ21yAxQf0rW7GYRvsENhvXNDONAHgNks1fsgUJH3mVzgbsGf4LpW3kuJI6e/e3VIeaTW3w==
33943394
dependencies:
33953395
async-lock "^1.1.0"
33963396
clean-git-ref "^2.0.1"

0 commit comments

Comments
 (0)