sfdx-git-delta 7.0.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/lib/adapter/GitAdapter.d.ts +25 -64
- package/lib/adapter/GitAdapter.js +352 -483
- package/lib/adapter/GitAdapter.js.map +1 -1
- package/lib/adapter/gitBlobReader.d.ts +8 -10
- package/lib/adapter/gitBlobReader.js.map +1 -1
- package/lib/adapter/ioExecutor.d.ts +5 -4
- package/lib/adapter/ioExecutor.js +22 -14
- package/lib/adapter/ioExecutor.js.map +1 -1
- package/lib/adapter/pathMatching.d.ts +9 -0
- package/lib/adapter/pathMatching.js +83 -0
- package/lib/adapter/pathMatching.js.map +1 -0
- package/lib/adapter/tsgitErrorMap.d.ts +7 -0
- package/lib/adapter/tsgitErrorMap.js +32 -0
- package/lib/adapter/tsgitErrorMap.js.map +1 -0
- package/lib/commands/sgd/source/delta.js.map +1 -1
- package/lib/constant/gitConstants.d.ts +0 -2
- package/lib/constant/gitConstants.js +0 -8
- package/lib/constant/gitConstants.js.map +1 -1
- package/lib/constant/metadataConstants.d.ts +1 -0
- package/lib/constant/metadataConstants.js +1 -0
- package/lib/constant/metadataConstants.js.map +1 -1
- package/lib/main.d.ts +2 -2
- package/lib/main.js +64 -35
- package/lib/main.js.map +1 -1
- package/lib/post-processor/baseProcessor.d.ts +9 -6
- package/lib/post-processor/baseProcessor.js +5 -6
- package/lib/post-processor/baseProcessor.js.map +1 -1
- package/lib/post-processor/changesManifestProcessor.d.ts +3 -2
- package/lib/post-processor/changesManifestProcessor.js +5 -4
- package/lib/post-processor/changesManifestProcessor.js.map +1 -1
- package/lib/post-processor/flowTranslationProcessor.d.ts +8 -7
- package/lib/post-processor/flowTranslationProcessor.js +20 -19
- package/lib/post-processor/flowTranslationProcessor.js.map +1 -1
- package/lib/post-processor/includeProcessor.d.ts +6 -5
- package/lib/post-processor/includeProcessor.js +7 -6
- package/lib/post-processor/includeProcessor.js.map +1 -1
- package/lib/post-processor/packageGenerator.d.ts +3 -2
- package/lib/post-processor/packageGenerator.js +5 -4
- package/lib/post-processor/packageGenerator.js.map +1 -1
- package/lib/post-processor/postProcessorManager.d.ts +5 -7
- package/lib/post-processor/postProcessorManager.js +23 -31
- package/lib/post-processor/postProcessorManager.js.map +1 -1
- package/lib/service/botHandler.d.ts +1 -2
- package/lib/service/botHandler.js +17 -10
- package/lib/service/botHandler.js.map +1 -1
- package/lib/service/containedDecomposedHandler.d.ts +4 -5
- package/lib/service/containedDecomposedHandler.js +13 -12
- package/lib/service/containedDecomposedHandler.js.map +1 -1
- package/lib/service/customFieldHandler.d.ts +1 -2
- package/lib/service/customFieldHandler.js +7 -6
- package/lib/service/customFieldHandler.js.map +1 -1
- package/lib/service/customLabelHandler.d.ts +1 -2
- package/lib/service/customLabelHandler.js +3 -3
- package/lib/service/customLabelHandler.js.map +1 -1
- package/lib/service/customObjectHandler.d.ts +3 -4
- package/lib/service/customObjectHandler.js +10 -9
- package/lib/service/customObjectHandler.js.map +1 -1
- package/lib/service/decomposedHandler.d.ts +3 -4
- package/lib/service/decomposedHandler.js +5 -4
- package/lib/service/decomposedHandler.js.map +1 -1
- package/lib/service/diffLineInterpreter.d.ts +3 -3
- package/lib/service/diffLineInterpreter.js +18 -14
- package/lib/service/diffLineInterpreter.js.map +1 -1
- package/lib/service/flowHandler.d.ts +1 -2
- package/lib/service/flowHandler.js +9 -4
- package/lib/service/flowHandler.js.map +1 -1
- package/lib/service/inFileHandler.d.ts +8 -9
- package/lib/service/inFileHandler.js +28 -27
- package/lib/service/inFileHandler.js.map +1 -1
- package/lib/service/inFolderHandler.d.ts +4 -5
- package/lib/service/inFolderHandler.js +6 -5
- package/lib/service/inFolderHandler.js.map +1 -1
- package/lib/service/inResourceHandler.d.ts +4 -5
- package/lib/service/inResourceHandler.js +8 -7
- package/lib/service/inResourceHandler.js.map +1 -1
- package/lib/service/objectTranslationHandler.d.ts +1 -2
- package/lib/service/objectTranslationHandler.js +6 -5
- package/lib/service/objectTranslationHandler.js.map +1 -1
- package/lib/service/reportingFolderHandler.d.ts +6 -7
- package/lib/service/reportingFolderHandler.js +6 -6
- package/lib/service/reportingFolderHandler.js.map +1 -1
- package/lib/service/sharedFolderHandler.d.ts +4 -5
- package/lib/service/sharedFolderHandler.js +6 -6
- package/lib/service/sharedFolderHandler.js.map +1 -1
- package/lib/service/standardHandler.d.ts +5 -9
- package/lib/service/standardHandler.js +28 -35
- package/lib/service/standardHandler.js.map +1 -1
- package/lib/service/typeHandlerFactory.d.ts +3 -3
- package/lib/service/typeHandlerFactory.js +6 -6
- package/lib/service/typeHandlerFactory.js.map +1 -1
- package/lib/types/config.d.ts +5 -1
- package/lib/types/handlerResult.d.ts +7 -7
- package/lib/types/handlerResult.js +10 -1
- package/lib/types/handlerResult.js.map +1 -1
- package/lib/types/work.d.ts +1 -1
- package/lib/utils/arrayUtils.d.ts +1 -1
- package/lib/utils/arrayUtils.js.map +1 -1
- package/lib/utils/bundleRollup.d.ts +6 -0
- package/lib/utils/bundleRollup.js +62 -0
- package/lib/utils/bundleRollup.js.map +1 -0
- package/lib/utils/changeSet.d.ts +12 -15
- package/lib/utils/changeSet.js +23 -120
- package/lib/utils/changeSet.js.map +1 -1
- package/lib/utils/changesAssembly.d.ts +8 -0
- package/lib/utils/changesAssembly.js +21 -0
- package/lib/utils/changesAssembly.js.map +1 -0
- package/lib/utils/configValidator.d.ts +7 -6
- package/lib/utils/configValidator.js +32 -11
- package/lib/utils/configValidator.js.map +1 -1
- package/lib/utils/fsHelper.d.ts +2 -1
- package/lib/utils/fsHelper.js +7 -3
- package/lib/utils/fsHelper.js.map +1 -1
- package/lib/utils/messageSanitizer.d.ts +1 -0
- package/lib/utils/messageSanitizer.js +48 -0
- package/lib/utils/messageSanitizer.js.map +1 -0
- package/lib/utils/metadataDiff/index.d.ts +0 -2
- package/lib/utils/metadataDiff/index.js.map +1 -1
- package/lib/utils/metadataDiff/streamingDiff.d.ts +1 -3
- package/lib/utils/metadataDiff/streamingDiff.js +16 -54
- package/lib/utils/metadataDiff/streamingDiff.js.map +1 -1
- package/lib/utils/pathspec.d.ts +15 -0
- package/lib/utils/pathspec.js +71 -0
- package/lib/utils/pathspec.js.map +1 -0
- package/lib/utils/renameResolver.d.ts +6 -6
- package/lib/utils/renameResolver.js +10 -9
- package/lib/utils/renameResolver.js.map +1 -1
- package/lib/utils/repoGitDiff.d.ts +2 -0
- package/lib/utils/repoGitDiff.js +14 -1
- package/lib/utils/repoGitDiff.js.map +1 -1
- package/messages/delta.md +26 -0
- package/oclif.manifest.json +2 -2
- package/package.json +28 -20
- package/lib/adapter/gitBatchCatFile.d.ts +0 -42
- package/lib/adapter/gitBatchCatFile.js +0 -203
- package/lib/adapter/gitBatchCatFile.js.map +0 -1
- package/lib/post-processor/bundleRollupProcessor.d.ts +0 -7
- package/lib/post-processor/bundleRollupProcessor.js +0 -81
- package/lib/post-processor/bundleRollupProcessor.js.map +0 -1
|
@@ -4,32 +4,70 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
/*
|
|
8
|
+
* In-process git backend for sfdx-git-delta, backed by `@scolladon/tsgit`
|
|
9
|
+
* (zero-dependency, no `git` binary, no subprocess). Object-store reads,
|
|
10
|
+
* diffs, tree walks and blob content all go through the tsgit facade:
|
|
11
|
+
*
|
|
12
|
+
* git rev-parse --verify <ref> -> repo.revParse(ref)
|
|
13
|
+
* git ls-tree --name-only -r <rev> -> repo.primitives.flattenTree
|
|
14
|
+
* git rev-list --max-parents=0 HEAD -> repo.primitives.walkCommits
|
|
15
|
+
* git cat-file --batch / blob -> repo.primitives.readBlob / streamBlob
|
|
16
|
+
* git diff --name-status -M -w -> repo.diff({ recursive,
|
|
17
|
+
* detectRenames, ignoreWhitespace })
|
|
18
|
+
* git archive --format=tar -> tree walk + streamBlob
|
|
19
|
+
* git grep -l -> tree walk + readBlob + RegExp
|
|
20
|
+
* (grepBlobs, shared by
|
|
21
|
+
* grepUnderPaths and
|
|
22
|
+
* grepMatchingPathspecs)
|
|
23
|
+
* git config core.* -> no-op (nothing to configure)
|
|
24
|
+
*
|
|
25
|
+
* Fidelity note: grepBlobs matches content with JS RegExp semantics, not
|
|
26
|
+
* POSIX basic regex. Callers are expected to pass metacharacter-free
|
|
27
|
+
* literals so the two semantics agree; the known pattern set is pinned by
|
|
28
|
+
* gitGrepPatternInventory.test.ts (new callers must extend that inventory).
|
|
29
|
+
* A second fidelity axis follows from the path-matching split: path
|
|
30
|
+
* matching is literal-prefix on grepUnderPaths' concrete-path surface and
|
|
31
|
+
* wildmatch on grepMatchingPathspecs' pattern surface — only sgd-constructed
|
|
32
|
+
* values, already validated to carry no wildcard metacharacters, reach the
|
|
33
|
+
* latter.
|
|
34
|
+
*/
|
|
35
|
+
import { once } from 'node:events';
|
|
8
36
|
import { createReadStream } from 'node:fs';
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
37
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
10
38
|
import { join } from 'node:path/posix';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { TAB } from '../constant/cliConstants.js';
|
|
39
|
+
import { PassThrough, Readable } from 'node:stream';
|
|
40
|
+
import { openRepository, } from '@scolladon/tsgit';
|
|
14
41
|
import { UTF8_ENCODING } from '../constant/fsConstants.js';
|
|
15
|
-
import {
|
|
42
|
+
import { HEAD } from '../constant/gitConstants.js';
|
|
16
43
|
import { pushAll } from '../utils/arrayUtils.js';
|
|
17
44
|
import { getErrorMessage } from '../utils/errorUtils.js';
|
|
18
45
|
import { treatPathSep } from '../utils/fsUtils.js';
|
|
19
46
|
import { getLFSObjectContentPath, isLFS } from '../utils/gitLfsHelper.js';
|
|
20
47
|
import { log } from '../utils/LoggingDecorator.js';
|
|
21
48
|
import { Logger, lazy } from '../utils/LoggingService.js';
|
|
22
|
-
import {
|
|
49
|
+
import { EscalateToStreamingSignal, SIZE_THRESHOLD, } from './gitBlobReader.js';
|
|
50
|
+
import { buildLiteralMatcher, buildPathspecMatcher, hasRootScope, inScope, nonRootScopes, ROOT_PATHS, toDiffLines, } from './pathMatching.js';
|
|
23
51
|
import { TreeIndex } from './treeIndex.js';
|
|
52
|
+
import { mapTsgitError } from './tsgitErrorMap.js';
|
|
53
|
+
// walkTree yields directories and gitlinks too; only blob-bearing modes
|
|
54
|
+
// belong in the path -> blob id index (ls-tree -r parity).
|
|
55
|
+
const BLOB_MODES = new Set(['100644', '100755', '120000']);
|
|
56
|
+
// Maps sgd's whitespace-ignoring diff request onto tsgit's data-mode
|
|
57
|
+
// whitespace knobs: whitespace-only modifications drop out of the TreeDiff.
|
|
58
|
+
const IGNORE_WHITESPACE_OPTIONS = {
|
|
59
|
+
ignoreWhitespace: 'all',
|
|
60
|
+
ignoreBlankLines: true,
|
|
61
|
+
};
|
|
62
|
+
// Streaming LFS-pointer detection for streamContent: the literal magic
|
|
63
|
+
// prefix released LFS pointer files start with, and a generous upper
|
|
64
|
+
// bound on pointer body size (real pointers are ~130 bytes; this guards
|
|
65
|
+
// against a corrupt/oversized "pointer" masquerading as one).
|
|
24
66
|
const LFS_MAGIC = Buffer.from('version https://git-lfs.github.com/spec/v1\n');
|
|
25
|
-
const
|
|
26
|
-
const ROOT_PATHS = new Set(['', '.', './']);
|
|
67
|
+
const LFS_POINTER_CAP = 1024;
|
|
27
68
|
export default class GitAdapter {
|
|
28
69
|
config;
|
|
29
70
|
static instances = new Map();
|
|
30
|
-
// Keyed by repo+to so spread copies of the same config (e.g. ioExecutor's
|
|
31
|
-
// per-revision {...config, to: rev}) share one adapter instead of spawning
|
|
32
|
-
// a fresh git cat-file subprocess per call.
|
|
33
71
|
static keyFor(config) {
|
|
34
72
|
return `${config.repo}\0${config.to}`;
|
|
35
73
|
}
|
|
@@ -40,198 +78,98 @@ export default class GitAdapter {
|
|
|
40
78
|
}
|
|
41
79
|
return GitAdapter.instances.get(key);
|
|
42
80
|
}
|
|
81
|
+
static async closeAll() {
|
|
82
|
+
for (const instance of GitAdapter.instances.values()) {
|
|
83
|
+
await instance.close();
|
|
84
|
+
}
|
|
85
|
+
GitAdapter.instances.clear();
|
|
86
|
+
}
|
|
43
87
|
treeIndex;
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// kill()ed if still alive.
|
|
49
|
-
streamingChildren = [];
|
|
50
|
-
// Cap on stderr buffered per-subprocess: long-running git processes that
|
|
51
|
-
// emit progress to stderr would otherwise grow this without bound. The
|
|
52
|
-
// final error message truncates at this size.
|
|
53
|
-
static STDERR_BUFFER_CAP = 8 * 1024;
|
|
54
|
-
// Cap on LFS pointer buffering: real pointers are < 200 bytes; a crafted
|
|
55
|
-
// blob with the LFS magic prefix followed by gigabytes of content should
|
|
56
|
-
// not OOM the process before validation fails.
|
|
57
|
-
static LFS_POINTER_CAP = 1024;
|
|
58
|
-
spawnFn = spawn;
|
|
88
|
+
// Per revision: repo-relative path -> blob ObjectId. The tsgit counterpart
|
|
89
|
+
// of `git cat-file --batch` oid:path resolution.
|
|
90
|
+
blobIdIndex;
|
|
91
|
+
repoHandle = null;
|
|
59
92
|
constructor(config) {
|
|
60
93
|
this.config = config;
|
|
61
94
|
this.treeIndex = new Map();
|
|
95
|
+
this.blobIdIndex = new Map();
|
|
62
96
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*/
|
|
67
|
-
setSpawnFn(spawnFn) {
|
|
68
|
-
this.spawnFn = spawnFn;
|
|
69
|
-
}
|
|
70
|
-
getBatchCatFile() {
|
|
71
|
-
if (!this.batchCatFile) {
|
|
72
|
-
this.batchCatFile = new GitBatchCatFile(this.config.repo, {
|
|
73
|
-
spawnFn: this.spawnFn,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
return this.batchCatFile;
|
|
77
|
-
}
|
|
78
|
-
closeBatchProcess() {
|
|
79
|
-
this.batchCatFile?.close();
|
|
80
|
-
this.batchCatFile = null;
|
|
81
|
-
for (const child of this.streamingChildren) {
|
|
82
|
-
if (child.exitCode === null && !child.killed) {
|
|
83
|
-
child.kill();
|
|
84
|
-
}
|
|
97
|
+
getRepo() {
|
|
98
|
+
if (!this.repoHandle) {
|
|
99
|
+
this.repoHandle = openRepository({ cwd: this.config.repo });
|
|
85
100
|
}
|
|
86
|
-
this.
|
|
101
|
+
return this.repoHandle;
|
|
87
102
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
async close() {
|
|
104
|
+
if (this.repoHandle) {
|
|
105
|
+
const repo = await this.repoHandle;
|
|
106
|
+
await repo.dispose();
|
|
107
|
+
this.repoHandle = null;
|
|
91
108
|
}
|
|
92
|
-
GitAdapter.instances.clear();
|
|
93
109
|
}
|
|
94
110
|
async configureRepository() {
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
// core.longpaths / core.quotepath exist to fix `git` CLI output and
|
|
112
|
+
// Windows path handling in subprocesses. tsgit reads the object store
|
|
113
|
+
// directly: nothing to configure.
|
|
97
114
|
}
|
|
98
115
|
async parseRev(ref) {
|
|
99
|
-
|
|
116
|
+
try {
|
|
117
|
+
const repo = await this.getRepo();
|
|
118
|
+
return await repo.revParse(ref);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw mapTsgitError(error, ref);
|
|
122
|
+
}
|
|
100
123
|
}
|
|
101
124
|
async preBuildTreeIndex(revision, scopePaths) {
|
|
102
125
|
if (this.treeIndex.has(revision)) {
|
|
103
126
|
return;
|
|
104
127
|
}
|
|
105
128
|
try {
|
|
106
|
-
const
|
|
107
|
-
if (scopePaths.length > 0) {
|
|
108
|
-
args.push('--', ...scopePaths);
|
|
109
|
-
}
|
|
129
|
+
const blobIds = await this.indexRevision(revision);
|
|
110
130
|
const index = new TreeIndex();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
const scopes = scopePaths.filter(scope => !ROOT_PATHS.has(scope));
|
|
132
|
+
for (const path of blobIds.keys()) {
|
|
133
|
+
if (scopes.length === 0 || inScope(path, scopes)) {
|
|
134
|
+
index.add(path);
|
|
135
|
+
}
|
|
114
136
|
}
|
|
115
137
|
this.treeIndex.set(revision, index);
|
|
116
138
|
}
|
|
117
139
|
catch (error) {
|
|
118
|
-
|
|
119
|
-
Logger.debug(
|
|
120
|
-
// Stryker disable next-line StringLiteral,ArrowFunction -- equivalent: lazy log content is observability only; tests don't assert on the lazy log line
|
|
121
|
-
lazy `preBuildTreeIndex: scoped ls-tree for '${revision}' failed: ${() => getErrorMessage(error)}`);
|
|
140
|
+
Logger.debug(lazy `preBuildTreeIndex: tree walk for '${revision}' failed: ${() => getErrorMessage(error)}`);
|
|
122
141
|
}
|
|
123
142
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* debug log; a non-zero exit code rejects the iterator on next read.
|
|
133
|
-
*/
|
|
134
|
-
async *_spawnLines(args) {
|
|
135
|
-
// Stryker disable ObjectLiteral,ArrayDeclaration,StringLiteral -- equivalent: spawn options are Node.js child_process internals; tests stub spawnFn and assert on yielded lines, not on the option object passed through to the spawn
|
|
136
|
-
const child = this.spawnFn('git', args, {
|
|
137
|
-
cwd: this.config.repo,
|
|
138
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
139
|
-
});
|
|
140
|
-
// Stryker restore ObjectLiteral,ArrayDeclaration,StringLiteral
|
|
141
|
-
this._trackChild(child);
|
|
142
|
-
const stderrChunks = [];
|
|
143
|
-
let stderrLen = 0;
|
|
144
|
-
child.stderr.on('data', (chunk) => {
|
|
145
|
-
// Stryker disable next-line ConditionalExpression,EqualityOperator -- equivalent: stderr buffer cap guard; the test surface only checks the truncated error message at exit so >= vs > and the threshold flip are unobservable
|
|
146
|
-
if (stderrLen >= GitAdapter.STDERR_BUFFER_CAP)
|
|
147
|
-
return;
|
|
148
|
-
stderrChunks.push(chunk);
|
|
149
|
-
// Stryker disable next-line AssignmentOperator -- equivalent: same rationale — stderr cap accumulator; -= would underflow but no test feeds >cap stderr to observe
|
|
150
|
-
stderrLen += chunk.length;
|
|
151
|
-
});
|
|
152
|
-
const rl = createInterface({
|
|
153
|
-
input: child.stdout,
|
|
154
|
-
crlfDelay: Number.POSITIVE_INFINITY,
|
|
155
|
-
});
|
|
156
|
-
const exitPromise = new Promise((resolve, reject) => {
|
|
157
|
-
// Stryker disable next-line StringLiteral -- equivalent: 'error' and 'close' are EventEmitter event names; tests stub child.once with a fake EE that observes only the contract via the resolve/reject side-effect, not the literal name
|
|
158
|
-
child.once('error', reject);
|
|
159
|
-
child.once('close', resolve);
|
|
160
|
-
});
|
|
161
|
-
try {
|
|
162
|
-
for await (const line of rl) {
|
|
163
|
-
yield line;
|
|
164
|
-
}
|
|
165
|
-
const code = await exitPromise;
|
|
166
|
-
if (code !== 0 && code !== null) {
|
|
167
|
-
throw this._buildExitError(args, code, stderrChunks);
|
|
168
|
-
}
|
|
143
|
+
// Flattens the full tree at `revision` once and caches path -> blob oid.
|
|
144
|
+
// Shared by the tree index, blob reads, archive streaming and grep.
|
|
145
|
+
// flattenTree is the bulk traversal path (one call, no per-entry yields);
|
|
146
|
+
// it takes a tree oid, so the commit is peeled first.
|
|
147
|
+
async indexRevision(revision) {
|
|
148
|
+
const cached = this.blobIdIndex.get(revision);
|
|
149
|
+
if (cached) {
|
|
150
|
+
return cached;
|
|
169
151
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
152
|
+
const repo = await this.getRepo();
|
|
153
|
+
const revisionId = await repo.revParse(revision);
|
|
154
|
+
// revParse returns the tag OBJECT oid for annotated tags (no auto-peel),
|
|
155
|
+
// so follow the tag chain down to the tagged commit before reading its
|
|
156
|
+
// tree — matching `git ls-tree -r <tag>` semantics.
|
|
157
|
+
let target = await repo.primitives.readObject(revisionId);
|
|
158
|
+
while (target.type === 'tag') {
|
|
159
|
+
target = await repo.primitives.readObject(target.data.object);
|
|
175
160
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
* One-shot buffered git invocation. Spawns `git <args>`, drains stdout
|
|
179
|
-
* fully, and returns the trimmed UTF-8 string. Used by callers that
|
|
180
|
-
* need the complete output as a single string (revparse, raw-style
|
|
181
|
-
* commands, config writes) rather than a line-by-line stream.
|
|
182
|
-
*
|
|
183
|
-
* Mirrors _spawnLines' exit-code handling: non-zero exits throw with
|
|
184
|
-
* the stderr tail (capped at STDERR_BUFFER_CAP) in the message. Output
|
|
185
|
-
* is trimmed so callers receive the same shape they used to get from
|
|
186
|
-
* the legacy `simpleGit({ trimmed: true })` configuration.
|
|
187
|
-
*/
|
|
188
|
-
async _gitBuffered(args) {
|
|
189
|
-
const child = this.spawnFn('git', args, {
|
|
190
|
-
cwd: this.config.repo,
|
|
191
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
192
|
-
});
|
|
193
|
-
this._trackChild(child);
|
|
194
|
-
const stdoutChunks = [];
|
|
195
|
-
const stderrChunks = [];
|
|
196
|
-
let stderrLen = 0;
|
|
197
|
-
child.stdout.on('data', (chunk) => {
|
|
198
|
-
stdoutChunks.push(chunk);
|
|
199
|
-
});
|
|
200
|
-
child.stderr.on('data', (chunk) => {
|
|
201
|
-
if (stderrLen >= GitAdapter.STDERR_BUFFER_CAP)
|
|
202
|
-
return;
|
|
203
|
-
stderrChunks.push(chunk);
|
|
204
|
-
stderrLen += chunk.length;
|
|
205
|
-
});
|
|
206
|
-
const code = await new Promise((resolve, reject) => {
|
|
207
|
-
child.once('error', reject);
|
|
208
|
-
child.once('close', resolve);
|
|
209
|
-
});
|
|
210
|
-
if (code !== 0 && code !== null) {
|
|
211
|
-
throw this._buildExitError(args, code, stderrChunks);
|
|
161
|
+
if (target.type !== 'commit') {
|
|
162
|
+
throw new Error(`'${revision}' does not resolve to a commit`);
|
|
212
163
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
.toString('utf8')
|
|
223
|
-
.trim();
|
|
224
|
-
return new Error(`git ${args[0]} exited ${code}${stderr ? `: ${stderr}` : ''}`);
|
|
225
|
-
}
|
|
226
|
-
_trackChild(child) {
|
|
227
|
-
this.streamingChildren.push(child);
|
|
228
|
-
child.once('close', () => {
|
|
229
|
-
const idx = this.streamingChildren.indexOf(child);
|
|
230
|
-
// Stryker disable next-line ConditionalExpression,UnaryOperator -- equivalent: see v8 ignore — idx is always !== -1 because the just-pushed child is still in the array; the guard is a defensive safety net
|
|
231
|
-
/* v8 ignore next -- defensive: the close listener is once-only and bound to the tracked child; idx is always >= 0 here */
|
|
232
|
-
if (idx !== -1)
|
|
233
|
-
this.streamingChildren.splice(idx, 1);
|
|
234
|
-
});
|
|
164
|
+
const { entries } = await repo.primitives.flattenTree(target.data.tree);
|
|
165
|
+
const blobIds = new Map();
|
|
166
|
+
for (const [path, entry] of entries) {
|
|
167
|
+
if (BLOB_MODES.has(entry.mode)) {
|
|
168
|
+
blobIds.set(treatPathSep(path), entry.id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
this.blobIdIndex.set(revision, blobIds);
|
|
172
|
+
return blobIds;
|
|
235
173
|
}
|
|
236
174
|
pathExistsImpl(path, revision) {
|
|
237
175
|
const index = this.treeIndex.get(revision);
|
|
@@ -245,197 +183,129 @@ export default class GitAdapter {
|
|
|
245
183
|
return this.pathExistsImpl(path, revision);
|
|
246
184
|
}
|
|
247
185
|
async getFirstCommitRef() {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
186
|
+
try {
|
|
187
|
+
const repo = await this.getRepo();
|
|
188
|
+
const head = await repo.revParse(HEAD);
|
|
189
|
+
let firstCommit = head;
|
|
190
|
+
for await (const commit of repo.primitives.walkCommits({
|
|
191
|
+
from: [head],
|
|
192
|
+
})) {
|
|
193
|
+
if (commit.data.parents.length === 0) {
|
|
194
|
+
firstCommit = commit.id;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return firstCommit;
|
|
255
199
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
async getBufferContentOrEscalate(forRef) {
|
|
259
|
-
let content = await this.getBatchCatFile().getContentOrEscalate(forRef.oid, forRef.path);
|
|
260
|
-
if (isLFS(content)) {
|
|
261
|
-
const lfsPath = getLFSObjectContentPath(content);
|
|
262
|
-
content = await readFile(join(this.config.repo, lfsPath));
|
|
200
|
+
catch (error) {
|
|
201
|
+
throw mapTsgitError(error, HEAD);
|
|
263
202
|
}
|
|
264
|
-
return content;
|
|
265
203
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
*
|
|
272
|
-
* Callers MUST consume each entry's stream — even when skipping the
|
|
273
|
-
* entry — otherwise tar-stream back-pressures and halts parsing. For
|
|
274
|
-
* skip cases use `stream.resume()` to drain-and-discard.
|
|
275
|
-
*/
|
|
276
|
-
async *streamArchive(path, revision) {
|
|
277
|
-
if (path.startsWith('-') || revision.startsWith('-')) {
|
|
278
|
-
throw new Error(`Refusing to spawn git archive for ${path}`);
|
|
204
|
+
async resolveObjectId(forRef) {
|
|
205
|
+
const blobIds = await this.indexRevision(forRef.oid);
|
|
206
|
+
const blobId = blobIds.get(treatPathSep(forRef.path));
|
|
207
|
+
if (!blobId) {
|
|
208
|
+
throw new Error(`Path '${forRef.path}' not found at '${forRef.oid}'`);
|
|
279
209
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
});
|
|
290
|
-
// Stryker restore BlockStatement,StringLiteral,ArrowFunction
|
|
291
|
-
child.stdout.pipe(extractor);
|
|
210
|
+
return blobId;
|
|
211
|
+
}
|
|
212
|
+
async readBlobBuffer(forRef) {
|
|
213
|
+
const repo = await this.getRepo();
|
|
214
|
+
const blobId = await this.resolveObjectId(forRef);
|
|
215
|
+
const blob = await repo.primitives.readBlob(blobId);
|
|
216
|
+
return Buffer.from(blob.content);
|
|
217
|
+
}
|
|
218
|
+
async getBufferContent(forRef) {
|
|
292
219
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
220
|
+
let content = await this.readBlobBuffer(forRef);
|
|
221
|
+
if (isLFS(content)) {
|
|
222
|
+
const lfsPath = getLFSObjectContentPath(content);
|
|
223
|
+
content = await readFile(join(this.config.repo, lfsPath));
|
|
224
|
+
}
|
|
225
|
+
return content;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
throw mapTsgitError(error, forRef.oid);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// tsgit's Blob/BlobStream carry no size field, so the only way to know a
|
|
232
|
+
// blob is oversized is to accumulate streamBlob chunks and watch the
|
|
233
|
+
// running total: crossing SIZE_THRESHOLD escalates the caller onto the
|
|
234
|
+
// dedicated streamContent path instead of materializing further.
|
|
235
|
+
async getBufferContentOrEscalate(forRef) {
|
|
236
|
+
const repo = await this.getRepo();
|
|
237
|
+
const blobId = await this.resolveObjectId(forRef);
|
|
238
|
+
const blobStream = await repo.primitives.streamBlob(blobId);
|
|
239
|
+
const parts = [];
|
|
240
|
+
let length = 0;
|
|
241
|
+
for await (const chunk of blobStream) {
|
|
242
|
+
parts.push(chunk);
|
|
243
|
+
length += chunk.length;
|
|
244
|
+
if (length > SIZE_THRESHOLD) {
|
|
245
|
+
throw new EscalateToStreamingSignal(length, forRef);
|
|
299
246
|
}
|
|
300
247
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
248
|
+
let content = Buffer.concat(parts, length);
|
|
249
|
+
if (isLFS(content)) {
|
|
250
|
+
// The pointer itself is tiny, so the accumulated-length guard above
|
|
251
|
+
// never fires for LFS-backed files — size the resolved object instead
|
|
252
|
+
// and escalate oversized ones onto the streaming path.
|
|
253
|
+
const lfsFile = join(this.config.repo, getLFSObjectContentPath(content));
|
|
254
|
+
const { size } = await stat(lfsFile);
|
|
255
|
+
if (size > SIZE_THRESHOLD) {
|
|
256
|
+
throw new EscalateToStreamingSignal(size, forRef);
|
|
257
|
+
}
|
|
258
|
+
content = await readFile(lfsFile);
|
|
306
259
|
}
|
|
260
|
+
return content;
|
|
307
261
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
*/
|
|
262
|
+
// Peeks the first LFS_MAGIC.length bytes off the pull-based streamBlob
|
|
263
|
+
// iterator: a match hands off to the resolved LFS object file, otherwise
|
|
264
|
+
// the peeked head and every remaining chunk are forwarded as-is. Backed
|
|
265
|
+
// by the async iterator's own pull semantics, so no pause/resume is
|
|
266
|
+
// needed to respect backpressure — only out.write()'s own signal.
|
|
314
267
|
streamContent(forRef) {
|
|
315
268
|
const out = new PassThrough();
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// path with a leading dash would give git an option it respects.
|
|
320
|
-
// Fail fast with a clear error rather than trust the subprocess.
|
|
321
|
-
if (forRef.path.startsWith('-') || forRef.oid.startsWith('-')) {
|
|
322
|
-
process.nextTick(() => out.destroy(new Error(`Refusing to spawn git cat-file for ${forRef.path}`)));
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
|
-
const child = this.spawnFn('git', ['cat-file', 'blob', `${forRef.oid}:${forRef.path}`], { cwd: this.config.repo, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
326
|
-
this._trackChild(child);
|
|
327
|
-
this._wireStreamContent(child, out, forRef);
|
|
269
|
+
this.pipeBlobContent(forRef, out).catch((error) => {
|
|
270
|
+
out.destroy(error instanceof Error ? error : new Error(getErrorMessage(error)));
|
|
271
|
+
});
|
|
328
272
|
return out;
|
|
329
273
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const onChunk = (chunk) => {
|
|
342
|
-
if (decided) {
|
|
343
|
-
// Stryker disable next-line BooleanLiteral -- equivalent: see v8 ignore — backpressure pause is rarely triggered in unit tests, and the drain listener resumes either way
|
|
344
|
-
/* v8 ignore next -- defensive: PassThrough rarely returns false here; backpressure handled at write time, drain listener resumes */
|
|
345
|
-
if (!out.write(chunk))
|
|
346
|
-
child.stdout.pause();
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
peeked.push(chunk);
|
|
350
|
-
peekedLen += chunk.length;
|
|
351
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: the LFS_MAGIC.length threshold gates the peek-vs-decide flow; the test surface verifies LFS handoff via a chunk >= LFS_MAGIC bytes, so the < flip path is exercised but the false-branch only changes whether the next chunk runs through here vs adds another peek; downstream contract is unchanged
|
|
352
|
-
if (peekedLen < LFS_MAGIC.length)
|
|
353
|
-
return;
|
|
354
|
-
// Stryker disable next-line BooleanLiteral -- equivalent: `decided` is local state; the next event loop tick reads it as true regardless of the literal we set, because the function returns immediately after the magic check
|
|
355
|
-
decided = true;
|
|
356
|
-
const head = forwardPeeked();
|
|
357
|
-
if (head.subarray(0, LFS_MAGIC.length).equals(LFS_MAGIC)) {
|
|
358
|
-
this._handoffToLfs(child, out, head);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
// Stryker disable next-line BooleanLiteral -- equivalent: see v8 ignore — pause is rarely triggered in tests
|
|
362
|
-
/* v8 ignore next -- defensive: PassThrough rarely returns false here; backpressure handled at write time, drain listener resumes */
|
|
363
|
-
if (!out.write(head))
|
|
364
|
-
child.stdout.pause();
|
|
365
|
-
};
|
|
366
|
-
child.stdout.on('data', onChunk);
|
|
367
|
-
// Stryker disable next-line StringLiteral,ArrowFunction -- equivalent: 'drain' event name and the resume-on-drain callback are EventEmitter wiring; the test contract verifies that forwarded bytes land on `out`, not the literal event name
|
|
368
|
-
out.on('drain', () => child.stdout.resume());
|
|
369
|
-
child.stdout.on('end', () => {
|
|
370
|
-
// Stryker disable next-line LogicalOperator,ConditionalExpression,EqualityOperator -- equivalent: this is the flush gate for peeked bytes that never reached LFS_MAGIC.length; tests exercise the gate by feeding a sub-magic stream, so the && arm is covered, but the OR-mutated short-circuit produces the same downstream out.write() call
|
|
371
|
-
if (!decided && peekedLen > 0) {
|
|
372
|
-
out.write(forwardPeeked());
|
|
373
|
-
}
|
|
374
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: see v8 ignore — second arm always true after flush
|
|
375
|
-
/* v8 ignore next -- defensive: forwardPeeked above zeroes peekedLen, so the second arm always evaluates true after a flush */
|
|
376
|
-
if (decided || peekedLen === 0)
|
|
377
|
-
out.end();
|
|
378
|
-
});
|
|
379
|
-
// Stryker disable BlockStatement,StringLiteral,ArrowFunction -- equivalent: stderr listener is observability-only; tests assert that the consumer sees the streamed content, not on the lazy log line
|
|
380
|
-
child.stderr.on('data', (chunk) => {
|
|
381
|
-
Logger.debug(lazy `streamContent stderr for ${forRef.path}: ${() => chunk.toString()}`);
|
|
382
|
-
});
|
|
383
|
-
// Stryker restore BlockStatement,StringLiteral,ArrowFunction
|
|
384
|
-
child.on('error', err => out.destroy(err));
|
|
385
|
-
child.on('close', code => {
|
|
386
|
-
// Intentional kills during LFS handoff close with a null/non-zero
|
|
387
|
-
// code; destroying `out` here would truncate the piped LFS stream
|
|
388
|
-
// the handoff just started.
|
|
389
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: this 4-arm guard rejects only the (non-zero, non-null, non-killed, non-destroyed) corner; unit tests stub close with code=0 so the guard short-circuits regardless of mutation, leaving out destinations unaffected
|
|
390
|
-
if (code !== 0 && code !== null && !child.killed && !out.destroyed) {
|
|
391
|
-
out.destroy(new Error(`git cat-file blob exited ${code}`));
|
|
392
|
-
}
|
|
393
|
-
});
|
|
274
|
+
async pipeBlobContent(forRef, out) {
|
|
275
|
+
const repo = await this.getRepo();
|
|
276
|
+
const blobId = await this.resolveObjectId(forRef);
|
|
277
|
+
const blobStream = await repo.primitives.streamBlob(blobId);
|
|
278
|
+
const chunks = normalizeChunks(blobStream);
|
|
279
|
+
const { head, exhausted } = await peekHead(chunks);
|
|
280
|
+
if (isLfsPointer(head)) {
|
|
281
|
+
await this.pipeLfsObject(chunks, head, out);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await forwardChunks(chunks, out, head, exhausted);
|
|
394
285
|
}
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
out.destroy(new Error('LFS pointer exceeds expected size'));
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
child.stdout.on('end', () => {
|
|
418
|
-
if (aborted)
|
|
419
|
-
return;
|
|
420
|
-
try {
|
|
421
|
-
const pointer = Buffer.concat(pointerParts, pointerLen);
|
|
422
|
-
const lfsPath = getLFSObjectContentPath(pointer);
|
|
423
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: this is a Readable.on('error') wiring that propagates ENOENT/permission errors during the LFS file read; unit tests fixture an existing LFS file so the error listener never fires
|
|
424
|
-
createReadStream(join(this.config.repo, lfsPath))
|
|
425
|
-
.on('error', err => out.destroy(err))
|
|
426
|
-
.pipe(out);
|
|
427
|
-
}
|
|
428
|
-
catch (err) {
|
|
429
|
-
/* v8 ignore next -- defensive: getLFSObjectContentPath / createReadStream throw Error instances in practice; the String() fallback exists for non-Error throws */
|
|
430
|
-
out.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: see v8 ignore — child is always alive here
|
|
434
|
-
/* v8 ignore next -- defensive: _handoffToLfs is reached after the first peek; the child is alive at this point */
|
|
435
|
-
if (!child.killed)
|
|
436
|
-
child.kill();
|
|
286
|
+
async pipeLfsObject(chunks, head, out) {
|
|
287
|
+
const pointer = await accumulatePointer(chunks, head);
|
|
288
|
+
const lfsPath = getLFSObjectContentPath(pointer);
|
|
289
|
+
createReadStream(join(this.config.repo, lfsPath))
|
|
290
|
+
.on('error', (error) => out.destroy(error))
|
|
291
|
+
.pipe(out);
|
|
292
|
+
}
|
|
293
|
+
async *streamArchive(path, revision) {
|
|
294
|
+
const repo = await this.getRepo();
|
|
295
|
+
const blobIds = await this.indexRevision(revision);
|
|
296
|
+
for (const [filePath, blobId] of blobIds) {
|
|
297
|
+
if (!inScope(filePath, [path]))
|
|
298
|
+
continue;
|
|
299
|
+
const blob = await repo.primitives.streamBlob(blobId);
|
|
300
|
+
yield {
|
|
301
|
+
path: filePath,
|
|
302
|
+
stream: Readable.from(blob, { objectMode: false }),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
437
305
|
}
|
|
438
306
|
async getStringContent(forRef) {
|
|
307
|
+
// getBufferContent already maps raw tsgit errors — mapping exactly once
|
|
308
|
+
// keeps the released error shapes intact (no double-wrapped messages).
|
|
439
309
|
const content = await this.getBufferContent(forRef);
|
|
440
310
|
return content.toString(UTF8_ENCODING);
|
|
441
311
|
}
|
|
@@ -445,7 +315,6 @@ export default class GitAdapter {
|
|
|
445
315
|
return [];
|
|
446
316
|
if (ROOT_PATHS.has(path))
|
|
447
317
|
return index.allPaths();
|
|
448
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: file-vs-dir fast path; flipping to false routes file paths through getFilesUnder which returns the same single-element array because the trie navigation lands on the file node
|
|
449
318
|
if (index.has(path))
|
|
450
319
|
return [path];
|
|
451
320
|
return index.getFilesUnder(path);
|
|
@@ -466,149 +335,92 @@ export default class GitAdapter {
|
|
|
466
335
|
return [];
|
|
467
336
|
return index.listChildren(dir);
|
|
468
337
|
}
|
|
469
|
-
|
|
338
|
+
// Concrete-path surface: `path` comes straight off the repository (a git
|
|
339
|
+
// diff path run through treatPathSep, or MetadataElement.basePath) and is
|
|
340
|
+
// matched by literal directory prefix — never as a wildmatch pathspec, so
|
|
341
|
+
// a metacharacter incidentally present in a real path (e.g. an object
|
|
342
|
+
// folder named `Custom[1]__c`) can never be misread as a glob.
|
|
343
|
+
async grepUnderPaths(pattern, path, revision = this.config.to) {
|
|
344
|
+
return this.grepBlobs(pattern, path, revision, buildLiteralMatcher);
|
|
345
|
+
}
|
|
346
|
+
// Pattern surface: `path` is an sgd-constructed pathspec (e.g.
|
|
347
|
+
// `<source>/*.translation-meta.xml`) and is matched with git pathspec
|
|
348
|
+
// wildmatch semantics (literal + glob).
|
|
349
|
+
async grepMatchingPathspecs(pattern, path, revision = this.config.to) {
|
|
350
|
+
return this.grepBlobs(pattern, path, revision, buildPathspecMatcher);
|
|
351
|
+
}
|
|
352
|
+
async grepBlobs(pattern, path, revision, buildMatcher) {
|
|
470
353
|
try {
|
|
354
|
+
const repo = await this.getRepo();
|
|
471
355
|
const paths = Array.isArray(path) ? path : [path];
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
356
|
+
const blobIds = await this.indexRevision(revision);
|
|
357
|
+
const matcher = new RegExp(pattern);
|
|
358
|
+
const matchesPath = buildMatcher(paths);
|
|
359
|
+
const matches = [];
|
|
360
|
+
for (const [filePath, blobId] of blobIds) {
|
|
361
|
+
if (!matchesPath(filePath))
|
|
362
|
+
continue;
|
|
363
|
+
const blob = await repo.primitives.readBlob(blobId);
|
|
364
|
+
if (matcher.test(Buffer.from(blob.content).toString(UTF8_ENCODING))) {
|
|
365
|
+
matches.push(filePath);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return matches;
|
|
484
369
|
}
|
|
485
370
|
catch (error) {
|
|
486
|
-
|
|
487
|
-
Logger.debug(
|
|
488
|
-
// Stryker disable next-line StringLiteral,ArrowFunction -- equivalent: lazy log content is observability only
|
|
489
|
-
lazy `gitGrep: grep for '${pattern}' in '${path}' at '${revision}' failed: ${() => getErrorMessage(error)}`);
|
|
371
|
+
Logger.debug(lazy `grepBlobs: grep for '${pattern}' in '${path}' at '${revision}' failed: ${() => getErrorMessage(error)}`);
|
|
490
372
|
return [];
|
|
491
373
|
}
|
|
492
374
|
}
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// `config.changesManifest`
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
//
|
|
509
|
-
// The Promise.all fans out to 4 items with rename detection on. That's a
|
|
510
|
-
// localised exception to the CLAUDE.local.md ≤3 bounded-Promise.all
|
|
511
|
-
// guideline: the four `--diff-filter`s are the canonical per-kind split,
|
|
512
|
-
// so hiding R as an out-of-band sequential await just obscures the real
|
|
513
|
-
// symmetric fan-out without changing the resource footprint.
|
|
514
|
-
async *streamDiffLines() {
|
|
515
|
-
const detectRenames = Boolean(this.config.changesManifest);
|
|
516
|
-
if (!this.config.ignoreWhitespace) {
|
|
517
|
-
const args = [
|
|
518
|
-
'diff',
|
|
519
|
-
'--name-status',
|
|
520
|
-
...(detectRenames ? ['-M'] : ['--no-renames']),
|
|
521
|
-
`--diff-filter=${detectRenames ? 'AMDR' : 'AMD'}`,
|
|
522
|
-
this.config.from,
|
|
523
|
-
this.config.to,
|
|
524
|
-
'--',
|
|
525
|
-
...this.config.source,
|
|
526
|
-
];
|
|
527
|
-
for await (const line of this._spawnLines(args)) {
|
|
528
|
-
if (line)
|
|
529
|
-
yield treatPathSep(line);
|
|
530
|
-
}
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
// When rename detection is on, the A/M/D filters also run with -M so
|
|
534
|
-
// renamed files drop out (reclassified to R by git), and the dedicated
|
|
535
|
-
// R call is the single source of rename lines — no dedup needed.
|
|
536
|
-
// Without rename detection, A/M/D keep their pre-feature line shape via
|
|
537
|
-
// --no-renames.
|
|
538
|
-
const filters = detectRenames
|
|
539
|
-
? [ADDITION, MODIFICATION, DELETION, RENAMED]
|
|
540
|
-
: [ADDITION, MODIFICATION, DELETION];
|
|
541
|
-
// Numstat path runs one git invocation per change-type; we still emit
|
|
542
|
-
// each batch as it lands so downstream filters can begin work before
|
|
543
|
-
// every filter has finished.
|
|
544
|
-
for (const changeType of filters) {
|
|
545
|
-
for (const line of await this._getNumstatLines(changeType, detectRenames)) {
|
|
375
|
+
// One facade diff call carries the whole subprocess contract: `recursive`
|
|
376
|
+
// is `git diff -r`, `detectRenames` is `-M` (similarity-based, gated
|
|
377
|
+
// behind `config.changesManifest` like the subprocess `-M`), and the
|
|
378
|
+
// whitespace options drop whitespace-only modifications the way the
|
|
379
|
+
// subprocess numstat path does.
|
|
380
|
+
async *streamDiffLines(verdict, scopes) {
|
|
381
|
+
const { changes } = await this.requestDiff();
|
|
382
|
+
// git unions pathspecs (`-- . src` matches everything), so a root scope
|
|
383
|
+
// must not be filtered out here even when non-root scopes are also
|
|
384
|
+
// configured — the full, unfiltered source list is what `inScope`
|
|
385
|
+
// (via toDiffLines) expects to reproduce that union.
|
|
386
|
+
for (const change of changes) {
|
|
387
|
+
verdict.changesSeen++;
|
|
388
|
+
for (const line of toDiffLines(change, scopes)) {
|
|
389
|
+
verdict.linesYielded++;
|
|
546
390
|
yield line;
|
|
547
391
|
}
|
|
548
392
|
}
|
|
549
393
|
}
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
...this.config.source,
|
|
577
|
-
];
|
|
578
|
-
const lines = [];
|
|
579
|
-
for await (const line of this._spawnLines(args)) {
|
|
580
|
-
// Stryker disable next-line ConditionalExpression -- equivalent: see v8 ignore — _spawnLines is the line splitter; empty lines are unreachable in practice
|
|
581
|
-
/* v8 ignore next -- defensive: _spawnLines splits on EOL; trailing/empty lines from git numstat are filtered here */
|
|
582
|
-
if (!line)
|
|
583
|
-
continue;
|
|
584
|
-
lines.push(treatPathSep(line.replace(NUM_STAT_CHANGE_INFORMATION, `${changeType}\t`)));
|
|
394
|
+
// Every non-root scope matched nothing in the drained diff. A path can
|
|
395
|
+
// match several scopes, so this is all-or-nothing rather than per-scope
|
|
396
|
+
// — per-scope attribution needs bookkeeping that buys nothing. A root
|
|
397
|
+
// scope alongside non-root ones still means "everything is in scope"
|
|
398
|
+
// (union), so naming the non-root scopes as unmatched would be
|
|
399
|
+
// misleading — suppress the verdict entirely when one is present.
|
|
400
|
+
getUnmatchedSourceScopes(verdict, scopes) {
|
|
401
|
+
if (hasRootScope(scopes))
|
|
402
|
+
return [];
|
|
403
|
+
const unmatchable = nonRootScopes(scopes);
|
|
404
|
+
return unmatchable.length > 0 &&
|
|
405
|
+
verdict.changesSeen > 0 &&
|
|
406
|
+
verdict.linesYielded === 0
|
|
407
|
+
? unmatchable
|
|
408
|
+
: [];
|
|
409
|
+
}
|
|
410
|
+
async requestDiff() {
|
|
411
|
+
try {
|
|
412
|
+
const repo = await this.getRepo();
|
|
413
|
+
return await repo.diff({
|
|
414
|
+
from: this.config.from,
|
|
415
|
+
to: this.config.to,
|
|
416
|
+
recursive: true,
|
|
417
|
+
detectRenames: Boolean(this.config.changesManifest),
|
|
418
|
+
...(this.config.ignoreWhitespace ? IGNORE_WHITESPACE_OPTIONS : {}),
|
|
419
|
+
});
|
|
585
420
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
async _getRenameLines() {
|
|
589
|
-
const output = await this._gitBuffered([
|
|
590
|
-
'diff',
|
|
591
|
-
'--numstat',
|
|
592
|
-
'-M',
|
|
593
|
-
'-z',
|
|
594
|
-
...IGNORE_WHITESPACE_PARAMS,
|
|
595
|
-
`--diff-filter=${RENAMED}`,
|
|
596
|
-
this.config.from,
|
|
597
|
-
this.config.to,
|
|
598
|
-
'--',
|
|
599
|
-
...this.config.source,
|
|
600
|
-
]);
|
|
601
|
-
const tokens = output.split('\0');
|
|
602
|
-
const lines = [];
|
|
603
|
-
// Stryker disable next-line EqualityOperator,ArithmeticOperator,AssignmentOperator -- equivalent: this is the stride-3 loop bound; <= vs < and i-2 vs i+2 only widen/narrow the iteration count by one trailing position whose tokens are undefined and rejected by the `!src || !dst` guard below; i-=3 produces an unbounded negative-index walk that reads tokens[-N] (undefined) and is also rejected by the same guard
|
|
604
|
-
for (let i = 0; i + 2 < tokens.length; i += 3) {
|
|
605
|
-
const src = tokens[i + 1];
|
|
606
|
-
const dst = tokens[i + 2];
|
|
607
|
-
if (!src || !dst)
|
|
608
|
-
continue;
|
|
609
|
-
lines.push(treatPathSep(`${RENAMED}${TAB}${src}${TAB}${dst}`));
|
|
421
|
+
catch (error) {
|
|
422
|
+
throw mapTsgitError(error, `${this.config.from}..${this.config.to}`);
|
|
610
423
|
}
|
|
611
|
-
return lines;
|
|
612
424
|
}
|
|
613
425
|
}
|
|
614
426
|
__decorate([
|
|
@@ -637,8 +449,65 @@ __decorate([
|
|
|
637
449
|
], GitAdapter.prototype, "listDirAtRevision", null);
|
|
638
450
|
__decorate([
|
|
639
451
|
log
|
|
640
|
-
], GitAdapter.prototype, "
|
|
452
|
+
], GitAdapter.prototype, "grepUnderPaths", null);
|
|
453
|
+
__decorate([
|
|
454
|
+
log
|
|
455
|
+
], GitAdapter.prototype, "grepMatchingPathspecs", null);
|
|
641
456
|
__decorate([
|
|
642
457
|
log
|
|
643
458
|
], GitAdapter.prototype, "streamDiffLines", null);
|
|
459
|
+
// Normalizes streamBlob's result (a real BlobStream in production, plain
|
|
460
|
+
// arrays of chunks in unit fakes) into a single stateful AsyncGenerator so
|
|
461
|
+
// streamContent can peek a few chunks via next() and later hand the same
|
|
462
|
+
// cursor to a for-await loop without losing its place.
|
|
463
|
+
async function* normalizeChunks(source) {
|
|
464
|
+
yield* source;
|
|
465
|
+
}
|
|
466
|
+
const isLfsPointer = (head) => head.length >= LFS_MAGIC.length &&
|
|
467
|
+
head.subarray(0, LFS_MAGIC.length).equals(LFS_MAGIC);
|
|
468
|
+
const peekHead = async (chunks) => {
|
|
469
|
+
const parts = [];
|
|
470
|
+
let length = 0;
|
|
471
|
+
while (length < LFS_MAGIC.length) {
|
|
472
|
+
const result = await chunks.next();
|
|
473
|
+
if (result.done) {
|
|
474
|
+
return { head: Buffer.concat(parts, length), exhausted: true };
|
|
475
|
+
}
|
|
476
|
+
parts.push(result.value);
|
|
477
|
+
length += result.value.length;
|
|
478
|
+
}
|
|
479
|
+
return { head: Buffer.concat(parts, length), exhausted: false };
|
|
480
|
+
};
|
|
481
|
+
const writeChunk = async (out, chunk) => {
|
|
482
|
+
if (!out.write(chunk)) {
|
|
483
|
+
await once(out, 'drain');
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
const forwardChunks = async (chunks, out, head, exhausted) => {
|
|
487
|
+
if (head.length > 0) {
|
|
488
|
+
await writeChunk(out, head);
|
|
489
|
+
}
|
|
490
|
+
if (!exhausted) {
|
|
491
|
+
for await (const chunk of chunks) {
|
|
492
|
+
await writeChunk(out, chunk);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
out.end();
|
|
496
|
+
};
|
|
497
|
+
const assertWithinPointerCap = (length) => {
|
|
498
|
+
if (length > LFS_POINTER_CAP) {
|
|
499
|
+
throw new Error('LFS pointer exceeds expected size');
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const accumulatePointer = async (chunks, head) => {
|
|
503
|
+
const parts = [head];
|
|
504
|
+
let length = head.length;
|
|
505
|
+
assertWithinPointerCap(length);
|
|
506
|
+
for await (const chunk of chunks) {
|
|
507
|
+
parts.push(chunk);
|
|
508
|
+
length += chunk.length;
|
|
509
|
+
assertWithinPointerCap(length);
|
|
510
|
+
}
|
|
511
|
+
return Buffer.concat(parts, length);
|
|
512
|
+
};
|
|
644
513
|
//# sourceMappingURL=GitAdapter.js.map
|