sfdx-git-delta 7.0.1 → 7.1.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 +2 -2
- package/lib/adapter/GitAdapter.d.ts +15 -62
- package/lib/adapter/GitAdapter.js +376 -485
- 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 +4 -3
- package/lib/adapter/ioExecutor.js +22 -14
- package/lib/adapter/ioExecutor.js.map +1 -1
- 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/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.js +1 -1
- package/lib/main.js.map +1 -1
- package/lib/post-processor/flowTranslationProcessor.js +2 -3
- package/lib/post-processor/flowTranslationProcessor.js.map +1 -1
- package/lib/service/inFileHandler.js +4 -3
- package/lib/service/inFileHandler.js.map +1 -1
- 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/oclif.manifest.json +1 -1
- package/package.json +22 -15
- package/lib/adapter/gitBatchCatFile.d.ts +0 -42
- package/lib/adapter/gitBatchCatFile.js +0 -203
- package/lib/adapter/gitBatchCatFile.js.map +0 -1
|
@@ -4,32 +4,64 @@ 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
|
+
* git config core.* -> no-op (nothing to configure)
|
|
21
|
+
*
|
|
22
|
+
* Fidelity note: gitGrep matches with JS RegExp semantics, not POSIX basic
|
|
23
|
+
* regex. Callers are expected to pass metacharacter-free literals so the two
|
|
24
|
+
* semantics agree; the known pattern set is pinned by
|
|
25
|
+
* gitGrepPatternInventory.test.ts (new callers must extend that inventory).
|
|
26
|
+
*/
|
|
27
|
+
import { once } from 'node:events';
|
|
8
28
|
import { createReadStream } from 'node:fs';
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
29
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
10
30
|
import { join } from 'node:path/posix';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
31
|
+
import { PassThrough, Readable } from 'node:stream';
|
|
32
|
+
import { openRepository, toSimilarityPercent, } from '@scolladon/tsgit';
|
|
13
33
|
import { TAB } from '../constant/cliConstants.js';
|
|
14
34
|
import { UTF8_ENCODING } from '../constant/fsConstants.js';
|
|
15
|
-
import { ADDITION, DELETION, HEAD,
|
|
35
|
+
import { ADDITION, DELETION, HEAD, MODIFICATION, RENAMED, } from '../constant/gitConstants.js';
|
|
16
36
|
import { pushAll } from '../utils/arrayUtils.js';
|
|
17
37
|
import { getErrorMessage } from '../utils/errorUtils.js';
|
|
18
38
|
import { treatPathSep } from '../utils/fsUtils.js';
|
|
19
39
|
import { getLFSObjectContentPath, isLFS } from '../utils/gitLfsHelper.js';
|
|
20
40
|
import { log } from '../utils/LoggingDecorator.js';
|
|
21
41
|
import { Logger, lazy } from '../utils/LoggingService.js';
|
|
22
|
-
import {
|
|
42
|
+
import { EscalateToStreamingSignal, SIZE_THRESHOLD, } from './gitBlobReader.js';
|
|
23
43
|
import { TreeIndex } from './treeIndex.js';
|
|
24
|
-
|
|
25
|
-
const EOL = /\r?\n/;
|
|
44
|
+
import { mapTsgitError } from './tsgitErrorMap.js';
|
|
26
45
|
const ROOT_PATHS = new Set(['', '.', './']);
|
|
46
|
+
// walkTree yields directories and gitlinks too; only blob-bearing modes
|
|
47
|
+
// belong in the path -> blob id index (ls-tree -r parity).
|
|
48
|
+
const BLOB_MODES = new Set(['100644', '100755', '120000']);
|
|
49
|
+
const GITLINK_MODE = '160000';
|
|
50
|
+
// Maps sgd's whitespace-ignoring diff request onto tsgit's data-mode
|
|
51
|
+
// whitespace knobs: whitespace-only modifications drop out of the TreeDiff.
|
|
52
|
+
const IGNORE_WHITESPACE_OPTIONS = {
|
|
53
|
+
ignoreWhitespace: 'all',
|
|
54
|
+
ignoreBlankLines: true,
|
|
55
|
+
};
|
|
56
|
+
// Streaming LFS-pointer detection for streamContent: the literal magic
|
|
57
|
+
// prefix released LFS pointer files start with, and a generous upper
|
|
58
|
+
// bound on pointer body size (real pointers are ~130 bytes; this guards
|
|
59
|
+
// against a corrupt/oversized "pointer" masquerading as one).
|
|
60
|
+
const LFS_MAGIC = Buffer.from('version https://git-lfs.github.com/spec/v1\n');
|
|
61
|
+
const LFS_POINTER_CAP = 1024;
|
|
27
62
|
export default class GitAdapter {
|
|
28
63
|
config;
|
|
29
64
|
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
65
|
static keyFor(config) {
|
|
34
66
|
return `${config.repo}\0${config.to}`;
|
|
35
67
|
}
|
|
@@ -40,198 +72,98 @@ export default class GitAdapter {
|
|
|
40
72
|
}
|
|
41
73
|
return GitAdapter.instances.get(key);
|
|
42
74
|
}
|
|
75
|
+
static async closeAll() {
|
|
76
|
+
for (const instance of GitAdapter.instances.values()) {
|
|
77
|
+
await instance.close();
|
|
78
|
+
}
|
|
79
|
+
GitAdapter.instances.clear();
|
|
80
|
+
}
|
|
43
81
|
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;
|
|
82
|
+
// Per revision: repo-relative path -> blob ObjectId. The tsgit counterpart
|
|
83
|
+
// of `git cat-file --batch` oid:path resolution.
|
|
84
|
+
blobIdIndex;
|
|
85
|
+
repoHandle = null;
|
|
59
86
|
constructor(config) {
|
|
60
87
|
this.config = config;
|
|
61
88
|
this.treeIndex = new Map();
|
|
89
|
+
this.blobIdIndex = new Map();
|
|
62
90
|
}
|
|
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
|
-
});
|
|
91
|
+
getRepo() {
|
|
92
|
+
if (!this.repoHandle) {
|
|
93
|
+
this.repoHandle = openRepository({ cwd: this.config.repo });
|
|
75
94
|
}
|
|
76
|
-
return this.
|
|
95
|
+
return this.repoHandle;
|
|
77
96
|
}
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
child.kill();
|
|
84
|
-
}
|
|
97
|
+
async close() {
|
|
98
|
+
if (this.repoHandle) {
|
|
99
|
+
const repo = await this.repoHandle;
|
|
100
|
+
await repo.dispose();
|
|
101
|
+
this.repoHandle = null;
|
|
85
102
|
}
|
|
86
|
-
this.streamingChildren.length = 0;
|
|
87
|
-
}
|
|
88
|
-
static closeAll() {
|
|
89
|
-
for (const instance of GitAdapter.instances.values()) {
|
|
90
|
-
instance.closeBatchProcess();
|
|
91
|
-
}
|
|
92
|
-
GitAdapter.instances.clear();
|
|
93
103
|
}
|
|
94
104
|
async configureRepository() {
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
// core.longpaths / core.quotepath exist to fix `git` CLI output and
|
|
106
|
+
// Windows path handling in subprocesses. tsgit reads the object store
|
|
107
|
+
// directly: nothing to configure.
|
|
97
108
|
}
|
|
98
109
|
async parseRev(ref) {
|
|
99
|
-
|
|
110
|
+
try {
|
|
111
|
+
const repo = await this.getRepo();
|
|
112
|
+
return await repo.revParse(ref);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw mapTsgitError(error, ref);
|
|
116
|
+
}
|
|
100
117
|
}
|
|
101
118
|
async preBuildTreeIndex(revision, scopePaths) {
|
|
102
119
|
if (this.treeIndex.has(revision)) {
|
|
103
120
|
return;
|
|
104
121
|
}
|
|
105
122
|
try {
|
|
106
|
-
const
|
|
107
|
-
if (scopePaths.length > 0) {
|
|
108
|
-
args.push('--', ...scopePaths);
|
|
109
|
-
}
|
|
123
|
+
const blobIds = await this.indexRevision(revision);
|
|
110
124
|
const index = new TreeIndex();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
const scopes = scopePaths.filter(scope => !ROOT_PATHS.has(scope));
|
|
126
|
+
for (const path of blobIds.keys()) {
|
|
127
|
+
if (scopes.length === 0 || inScope(path, scopes)) {
|
|
128
|
+
index.add(path);
|
|
129
|
+
}
|
|
114
130
|
}
|
|
115
131
|
this.treeIndex.set(revision, index);
|
|
116
132
|
}
|
|
117
133
|
catch (error) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
|
134
|
+
Logger.debug(lazy `preBuildTreeIndex: tree walk for '${revision}' failed: ${() => getErrorMessage(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Flattens the full tree at `revision` once and caches path -> blob oid.
|
|
138
|
+
// Shared by the tree index, blob reads, archive streaming and grep.
|
|
139
|
+
// flattenTree is the bulk traversal path (one call, no per-entry yields);
|
|
140
|
+
// it takes a tree oid, so the commit is peeled first.
|
|
141
|
+
async indexRevision(revision) {
|
|
142
|
+
const cached = this.blobIdIndex.get(revision);
|
|
143
|
+
if (cached) {
|
|
144
|
+
return cached;
|
|
145
|
+
}
|
|
146
|
+
const repo = await this.getRepo();
|
|
147
|
+
const revisionId = await repo.revParse(revision);
|
|
148
|
+
// revParse returns the tag OBJECT oid for annotated tags (no auto-peel),
|
|
149
|
+
// so follow the tag chain down to the tagged commit before reading its
|
|
150
|
+
// tree — matching `git ls-tree -r <tag>` semantics.
|
|
151
|
+
let target = await repo.primitives.readObject(revisionId);
|
|
152
|
+
while (target.type === 'tag') {
|
|
153
|
+
target = await repo.primitives.readObject(target.data.object);
|
|
154
|
+
}
|
|
155
|
+
if (target.type !== 'commit') {
|
|
156
|
+
throw new Error(`'${revision}' does not resolve to a commit`);
|
|
157
|
+
}
|
|
158
|
+
const { entries } = await repo.primitives.flattenTree(target.data.tree);
|
|
159
|
+
const blobIds = new Map();
|
|
160
|
+
for (const [path, entry] of entries) {
|
|
161
|
+
if (BLOB_MODES.has(entry.mode)) {
|
|
162
|
+
blobIds.set(treatPathSep(path), entry.id);
|
|
168
163
|
}
|
|
169
164
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Stryker disable next-line ConditionalExpression,LogicalOperator,EqualityOperator -- equivalent: this is a defensive cleanup gate; in unit tests the spawned child mock has already exited cleanly by the time the finally runs, so the guard's truth value is irrelevant — child.kill() on an already-exited child is a no-op
|
|
173
|
-
if (!child.killed && child.exitCode === null)
|
|
174
|
-
child.kill();
|
|
175
|
-
}
|
|
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);
|
|
212
|
-
}
|
|
213
|
-
return Buffer.concat(stdoutChunks).toString('utf8').trim();
|
|
214
|
-
}
|
|
215
|
-
// Shared by _spawnLines and _gitBuffered: build the rejection thrown when
|
|
216
|
-
// git exits with a non-zero, non-null code. Captures the stderr tail
|
|
217
|
-
// (capped at STDERR_BUFFER_CAP) into the message for diagnostics.
|
|
218
|
-
_buildExitError(args, code, stderrChunks) {
|
|
219
|
-
// Stryker disable next-line MethodExpression -- equivalent: trim() strips trailing newlines from git stderr; tests synthesise stderr without trailing whitespace
|
|
220
|
-
const stderr = Buffer.concat(stderrChunks)
|
|
221
|
-
.subarray(0, GitAdapter.STDERR_BUFFER_CAP)
|
|
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
|
-
});
|
|
165
|
+
this.blobIdIndex.set(revision, blobIds);
|
|
166
|
+
return blobIds;
|
|
235
167
|
}
|
|
236
168
|
pathExistsImpl(path, revision) {
|
|
237
169
|
const index = this.treeIndex.get(revision);
|
|
@@ -245,197 +177,129 @@ export default class GitAdapter {
|
|
|
245
177
|
return this.pathExistsImpl(path, revision);
|
|
246
178
|
}
|
|
247
179
|
async getFirstCommitRef() {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
180
|
+
try {
|
|
181
|
+
const repo = await this.getRepo();
|
|
182
|
+
const head = await repo.revParse(HEAD);
|
|
183
|
+
let firstCommit = head;
|
|
184
|
+
for await (const commit of repo.primitives.walkCommits({
|
|
185
|
+
from: [head],
|
|
186
|
+
})) {
|
|
187
|
+
if (commit.data.parents.length === 0) {
|
|
188
|
+
firstCommit = commit.id;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return firstCommit;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw mapTsgitError(error, HEAD);
|
|
255
196
|
}
|
|
256
|
-
return content;
|
|
257
197
|
}
|
|
258
|
-
async
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
198
|
+
async resolveObjectId(forRef) {
|
|
199
|
+
const blobIds = await this.indexRevision(forRef.oid);
|
|
200
|
+
const blobId = blobIds.get(treatPathSep(forRef.path));
|
|
201
|
+
if (!blobId) {
|
|
202
|
+
throw new Error(`Path '${forRef.path}' not found at '${forRef.oid}'`);
|
|
263
203
|
}
|
|
264
|
-
return
|
|
204
|
+
return blobId;
|
|
265
205
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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}`);
|
|
279
|
-
}
|
|
280
|
-
const { extract } = await import('tar-stream');
|
|
281
|
-
const extractor = extract();
|
|
282
|
-
const child = this.spawnFn('git', ['archive', '--format=tar', revision, '--', path], { cwd: this.config.repo, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
283
|
-
this._trackChild(child);
|
|
284
|
-
// Stryker disable next-line ArrowFunction -- equivalent: the arrow forwards the spawn error onto the tar-stream extractor; the unit tests assert the extractor's downstream destroy effect, not that the arrow is the literal forwarder
|
|
285
|
-
child.on('error', err => extractor.destroy(err));
|
|
286
|
-
// Stryker disable BlockStatement,StringLiteral,ArrowFunction -- equivalent: stderr listener is observability-only; tests assert the iterator yields/terminates, not on the lazy log line
|
|
287
|
-
child.stderr.on('data', (chunk) => {
|
|
288
|
-
Logger.debug(lazy `streamArchive stderr for ${path}@${revision}: ${() => chunk.toString()}`);
|
|
289
|
-
});
|
|
290
|
-
// Stryker restore BlockStatement,StringLiteral,ArrowFunction
|
|
291
|
-
child.stdout.pipe(extractor);
|
|
206
|
+
async readBlobBuffer(forRef) {
|
|
207
|
+
const repo = await this.getRepo();
|
|
208
|
+
const blobId = await this.resolveObjectId(forRef);
|
|
209
|
+
const blob = await repo.primitives.readBlob(blobId);
|
|
210
|
+
return Buffer.from(blob.content);
|
|
211
|
+
}
|
|
212
|
+
async getBufferContent(forRef) {
|
|
292
213
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
214
|
+
let content = await this.readBlobBuffer(forRef);
|
|
215
|
+
if (isLFS(content)) {
|
|
216
|
+
const lfsPath = getLFSObjectContentPath(content);
|
|
217
|
+
content = await readFile(join(this.config.repo, lfsPath));
|
|
218
|
+
}
|
|
219
|
+
return content;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw mapTsgitError(error, forRef.oid);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// tsgit's Blob/BlobStream carry no size field, so the only way to know a
|
|
226
|
+
// blob is oversized is to accumulate streamBlob chunks and watch the
|
|
227
|
+
// running total: crossing SIZE_THRESHOLD escalates the caller onto the
|
|
228
|
+
// dedicated streamContent path instead of materializing further.
|
|
229
|
+
async getBufferContentOrEscalate(forRef) {
|
|
230
|
+
const repo = await this.getRepo();
|
|
231
|
+
const blobId = await this.resolveObjectId(forRef);
|
|
232
|
+
const blobStream = await repo.primitives.streamBlob(blobId);
|
|
233
|
+
const parts = [];
|
|
234
|
+
let length = 0;
|
|
235
|
+
for await (const chunk of blobStream) {
|
|
236
|
+
parts.push(chunk);
|
|
237
|
+
length += chunk.length;
|
|
238
|
+
if (length > SIZE_THRESHOLD) {
|
|
239
|
+
throw new EscalateToStreamingSignal(length, forRef);
|
|
299
240
|
}
|
|
300
241
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
242
|
+
let content = Buffer.concat(parts, length);
|
|
243
|
+
if (isLFS(content)) {
|
|
244
|
+
// The pointer itself is tiny, so the accumulated-length guard above
|
|
245
|
+
// never fires for LFS-backed files — size the resolved object instead
|
|
246
|
+
// and escalate oversized ones onto the streaming path.
|
|
247
|
+
const lfsFile = join(this.config.repo, getLFSObjectContentPath(content));
|
|
248
|
+
const { size } = await stat(lfsFile);
|
|
249
|
+
if (size > SIZE_THRESHOLD) {
|
|
250
|
+
throw new EscalateToStreamingSignal(size, forRef);
|
|
251
|
+
}
|
|
252
|
+
content = await readFile(lfsFile);
|
|
306
253
|
}
|
|
254
|
+
return content;
|
|
307
255
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
*/
|
|
256
|
+
// Peeks the first LFS_MAGIC.length bytes off the pull-based streamBlob
|
|
257
|
+
// iterator: a match hands off to the resolved LFS object file, otherwise
|
|
258
|
+
// the peeked head and every remaining chunk are forwarded as-is. Backed
|
|
259
|
+
// by the async iterator's own pull semantics, so no pause/resume is
|
|
260
|
+
// needed to respect backpressure — only out.write()'s own signal.
|
|
314
261
|
streamContent(forRef) {
|
|
315
262
|
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);
|
|
263
|
+
this.pipeBlobContent(forRef, out).catch((error) => {
|
|
264
|
+
out.destroy(error instanceof Error ? error : new Error(getErrorMessage(error)));
|
|
265
|
+
});
|
|
328
266
|
return out;
|
|
329
267
|
}
|
|
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
|
-
});
|
|
268
|
+
async pipeBlobContent(forRef, out) {
|
|
269
|
+
const repo = await this.getRepo();
|
|
270
|
+
const blobId = await this.resolveObjectId(forRef);
|
|
271
|
+
const blobStream = await repo.primitives.streamBlob(blobId);
|
|
272
|
+
const chunks = normalizeChunks(blobStream);
|
|
273
|
+
const { head, exhausted } = await peekHead(chunks);
|
|
274
|
+
if (isLfsPointer(head)) {
|
|
275
|
+
await this.pipeLfsObject(chunks, head, out);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
await forwardChunks(chunks, out, head, exhausted);
|
|
394
279
|
}
|
|
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();
|
|
280
|
+
async pipeLfsObject(chunks, head, out) {
|
|
281
|
+
const pointer = await accumulatePointer(chunks, head);
|
|
282
|
+
const lfsPath = getLFSObjectContentPath(pointer);
|
|
283
|
+
createReadStream(join(this.config.repo, lfsPath))
|
|
284
|
+
.on('error', (error) => out.destroy(error))
|
|
285
|
+
.pipe(out);
|
|
286
|
+
}
|
|
287
|
+
async *streamArchive(path, revision) {
|
|
288
|
+
const repo = await this.getRepo();
|
|
289
|
+
const blobIds = await this.indexRevision(revision);
|
|
290
|
+
for (const [filePath, blobId] of blobIds) {
|
|
291
|
+
if (!inScope(filePath, [path]))
|
|
292
|
+
continue;
|
|
293
|
+
const blob = await repo.primitives.streamBlob(blobId);
|
|
294
|
+
yield {
|
|
295
|
+
path: filePath,
|
|
296
|
+
stream: Readable.from(blob, { objectMode: false }),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
437
299
|
}
|
|
438
300
|
async getStringContent(forRef) {
|
|
301
|
+
// getBufferContent already maps raw tsgit errors — mapping exactly once
|
|
302
|
+
// keeps the released error shapes intact (no double-wrapped messages).
|
|
439
303
|
const content = await this.getBufferContent(forRef);
|
|
440
304
|
return content.toString(UTF8_ENCODING);
|
|
441
305
|
}
|
|
@@ -445,7 +309,6 @@ export default class GitAdapter {
|
|
|
445
309
|
return [];
|
|
446
310
|
if (ROOT_PATHS.has(path))
|
|
447
311
|
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
312
|
if (index.has(path))
|
|
450
313
|
return [path];
|
|
451
314
|
return index.getFilesUnder(path);
|
|
@@ -468,147 +331,53 @@ export default class GitAdapter {
|
|
|
468
331
|
}
|
|
469
332
|
async gitGrep(pattern, path, revision = this.config.to) {
|
|
470
333
|
try {
|
|
334
|
+
const repo = await this.getRepo();
|
|
471
335
|
const paths = Array.isArray(path) ? path : [path];
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
336
|
+
const blobIds = await this.indexRevision(revision);
|
|
337
|
+
const matcher = new RegExp(pattern);
|
|
338
|
+
const matchesPathspec = buildPathspecMatcher(paths);
|
|
339
|
+
const matches = [];
|
|
340
|
+
for (const [filePath, blobId] of blobIds) {
|
|
341
|
+
if (!matchesPathspec(filePath))
|
|
342
|
+
continue;
|
|
343
|
+
const blob = await repo.primitives.readBlob(blobId);
|
|
344
|
+
if (matcher.test(Buffer.from(blob.content).toString(UTF8_ENCODING))) {
|
|
345
|
+
matches.push(filePath);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return matches;
|
|
484
349
|
}
|
|
485
350
|
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)}`);
|
|
351
|
+
Logger.debug(lazy `gitGrep: grep for '${pattern}' in '${path}' at '${revision}' failed: ${() => getErrorMessage(error)}`);
|
|
490
352
|
return [];
|
|
491
353
|
}
|
|
492
354
|
}
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// `config.changesManifest`
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
// A/D while recording the rename pair for ChangeSet.
|
|
499
|
-
//
|
|
500
|
-
// Whitespace path: three (or four, when rename detection is on) parallel
|
|
501
|
-
// `git diff --numstat` calls, one per --diff-filter. `--name-status` does
|
|
502
|
-
// NOT honor `--ignore-all-space` (git decides A/M/D from blob SHAs for
|
|
503
|
-
// that mode, so a whitespace-only change still appears as `M`). Only
|
|
504
|
-
// `--numstat` computes a real content diff under the whitespace flags,
|
|
505
|
-
// so files with 0/0 line changes drop out naturally. When rename
|
|
506
|
-
// detection is enabled the R call uses `-z` so it can sidestep numstat's
|
|
507
|
-
// brace/arrow rename-path encoding.
|
|
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.
|
|
355
|
+
// One facade diff call carries the whole subprocess contract: `recursive`
|
|
356
|
+
// is `git diff -r`, `detectRenames` is `-M` (similarity-based, gated
|
|
357
|
+
// behind `config.changesManifest` like the subprocess `-M`), and the
|
|
358
|
+
// whitespace options drop whitespace-only modifications the way the
|
|
359
|
+
// subprocess numstat path does.
|
|
514
360
|
async *streamDiffLines() {
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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)) {
|
|
546
|
-
yield line;
|
|
547
|
-
}
|
|
361
|
+
const { changes } = await this.requestDiff();
|
|
362
|
+
const scopes = this.config.source.filter(scope => !ROOT_PATHS.has(scope));
|
|
363
|
+
for (const change of changes) {
|
|
364
|
+
yield* toDiffLines(change, scopes);
|
|
548
365
|
}
|
|
549
366
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (changeType === RENAMED) {
|
|
564
|
-
return this._getRenameLines();
|
|
565
|
-
}
|
|
566
|
-
const args = [
|
|
567
|
-
'diff',
|
|
568
|
-
'--numstat',
|
|
569
|
-
...(detectRenames ? ['-M'] : ['--no-renames']),
|
|
570
|
-
...IGNORE_WHITESPACE_PARAMS,
|
|
571
|
-
`--diff-filter=${changeType}`,
|
|
572
|
-
this.config.from,
|
|
573
|
-
this.config.to,
|
|
574
|
-
// Stryker disable next-line StringLiteral -- equivalent: '--' is the git path separator marker; tests stub _spawnLines so the args array is opaque past the call, and the contract enforced is that {from, to, ...source} land in the right slots
|
|
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`)));
|
|
585
|
-
}
|
|
586
|
-
return lines;
|
|
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}`));
|
|
367
|
+
async requestDiff() {
|
|
368
|
+
try {
|
|
369
|
+
const repo = await this.getRepo();
|
|
370
|
+
return await repo.diff({
|
|
371
|
+
from: this.config.from,
|
|
372
|
+
to: this.config.to,
|
|
373
|
+
recursive: true,
|
|
374
|
+
detectRenames: Boolean(this.config.changesManifest),
|
|
375
|
+
...(this.config.ignoreWhitespace ? IGNORE_WHITESPACE_OPTIONS : {}),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
throw mapTsgitError(error, `${this.config.from}..${this.config.to}`);
|
|
610
380
|
}
|
|
611
|
-
return lines;
|
|
612
381
|
}
|
|
613
382
|
}
|
|
614
383
|
__decorate([
|
|
@@ -641,4 +410,126 @@ __decorate([
|
|
|
641
410
|
__decorate([
|
|
642
411
|
log
|
|
643
412
|
], GitAdapter.prototype, "streamDiffLines", null);
|
|
413
|
+
// Normalizes streamBlob's result (a real BlobStream in production, plain
|
|
414
|
+
// arrays of chunks in unit fakes) into a single stateful AsyncGenerator so
|
|
415
|
+
// streamContent can peek a few chunks via next() and later hand the same
|
|
416
|
+
// cursor to a for-await loop without losing its place.
|
|
417
|
+
async function* normalizeChunks(source) {
|
|
418
|
+
yield* source;
|
|
419
|
+
}
|
|
420
|
+
const isLfsPointer = (head) => head.length >= LFS_MAGIC.length &&
|
|
421
|
+
head.subarray(0, LFS_MAGIC.length).equals(LFS_MAGIC);
|
|
422
|
+
const peekHead = async (chunks) => {
|
|
423
|
+
const parts = [];
|
|
424
|
+
let length = 0;
|
|
425
|
+
while (length < LFS_MAGIC.length) {
|
|
426
|
+
const result = await chunks.next();
|
|
427
|
+
if (result.done) {
|
|
428
|
+
return { head: Buffer.concat(parts, length), exhausted: true };
|
|
429
|
+
}
|
|
430
|
+
parts.push(result.value);
|
|
431
|
+
length += result.value.length;
|
|
432
|
+
}
|
|
433
|
+
return { head: Buffer.concat(parts, length), exhausted: false };
|
|
434
|
+
};
|
|
435
|
+
const writeChunk = async (out, chunk) => {
|
|
436
|
+
if (!out.write(chunk)) {
|
|
437
|
+
await once(out, 'drain');
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const forwardChunks = async (chunks, out, head, exhausted) => {
|
|
441
|
+
if (head.length > 0) {
|
|
442
|
+
await writeChunk(out, head);
|
|
443
|
+
}
|
|
444
|
+
if (!exhausted) {
|
|
445
|
+
for await (const chunk of chunks) {
|
|
446
|
+
await writeChunk(out, chunk);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
out.end();
|
|
450
|
+
};
|
|
451
|
+
const assertWithinPointerCap = (length) => {
|
|
452
|
+
if (length > LFS_POINTER_CAP) {
|
|
453
|
+
throw new Error('LFS pointer exceeds expected size');
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const accumulatePointer = async (chunks, head) => {
|
|
457
|
+
const parts = [head];
|
|
458
|
+
let length = head.length;
|
|
459
|
+
assertWithinPointerCap(length);
|
|
460
|
+
for await (const chunk of chunks) {
|
|
461
|
+
parts.push(chunk);
|
|
462
|
+
length += chunk.length;
|
|
463
|
+
assertWithinPointerCap(length);
|
|
464
|
+
}
|
|
465
|
+
return Buffer.concat(parts, length);
|
|
466
|
+
};
|
|
467
|
+
const inScope = (path, scopes) => scopes.some(scope => ROOT_PATHS.has(scope) || path === scope || path.startsWith(`${scope}/`));
|
|
468
|
+
const keepSide = (mode, path, scopes) => mode !== GITLINK_MODE && (scopes.length === 0 || inScope(path, scopes));
|
|
469
|
+
// git prints rename similarity as a zero-padded three-digit percent (R087).
|
|
470
|
+
const similarityPercent = (similarity) => String(toSimilarityPercent(similarity.score)).padStart(3, '0');
|
|
471
|
+
// The facade diff takes no pathspec, so `-- <source>` scoping is replicated
|
|
472
|
+
// per side. Gitlink changes are skipped (submodule pointer moves are not
|
|
473
|
+
// deployable metadata) and `type-change`/`copy` entries are dropped for
|
|
474
|
+
// parity with the subprocess `--diff-filter=AMD(R)`. A rename with only one
|
|
475
|
+
// side in scope degrades to that side's A/D line, matching what the
|
|
476
|
+
// subprocess pathspec does to a broken rename pair.
|
|
477
|
+
function* toDiffLines(change, scopes) {
|
|
478
|
+
switch (change.type) {
|
|
479
|
+
case 'add':
|
|
480
|
+
if (keepSide(change.newMode, change.newPath, scopes)) {
|
|
481
|
+
yield `${ADDITION}${TAB}${treatPathSep(change.newPath)}`;
|
|
482
|
+
}
|
|
483
|
+
break;
|
|
484
|
+
case 'delete':
|
|
485
|
+
if (keepSide(change.oldMode, change.oldPath, scopes)) {
|
|
486
|
+
yield `${DELETION}${TAB}${treatPathSep(change.oldPath)}`;
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
case 'modify':
|
|
490
|
+
if (change.oldMode !== GITLINK_MODE &&
|
|
491
|
+
keepSide(change.newMode, change.path, scopes)) {
|
|
492
|
+
yield `${MODIFICATION}${TAB}${treatPathSep(change.path)}`;
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
case 'rename': {
|
|
496
|
+
const oldKept = keepSide(change.oldMode, change.oldPath, scopes);
|
|
497
|
+
const newKept = keepSide(change.newMode, change.newPath, scopes);
|
|
498
|
+
if (oldKept && newKept) {
|
|
499
|
+
yield `${RENAMED}${similarityPercent(change.similarity)}${TAB}${treatPathSep(change.oldPath)}${TAB}${treatPathSep(change.newPath)}`;
|
|
500
|
+
}
|
|
501
|
+
else if (newKept) {
|
|
502
|
+
yield `${ADDITION}${TAB}${treatPathSep(change.newPath)}`;
|
|
503
|
+
}
|
|
504
|
+
else if (oldKept) {
|
|
505
|
+
yield `${DELETION}${TAB}${treatPathSep(change.oldPath)}`;
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const GLOB_CHARS = /[*?[]/;
|
|
512
|
+
const REGEXP_SPECIALS = /[.+^${}()|\\\]]/g;
|
|
513
|
+
// Git pathspec semantics: a literal pathspec matches by directory prefix; a
|
|
514
|
+
// pathspec containing wildcards uses wildmatch where `*` also crosses `/`
|
|
515
|
+
// (no `:(glob)` magic). Callers mix both shapes (e.g. flow translations use
|
|
516
|
+
// `<source>/*.translation-meta.xml`).
|
|
517
|
+
const buildPathspecMatcher = (specs) => {
|
|
518
|
+
// Git normalizes leading `./` (and the `.//*` shape produced by the
|
|
519
|
+
// default `./` source dir) away before matching; repo paths never carry
|
|
520
|
+
// either prefix.
|
|
521
|
+
const normalized = specs.map(spec => spec.replace(/^(\.\/)+/, '').replace(/^\/+/, ''));
|
|
522
|
+
const literals = normalized.filter(spec => !GLOB_CHARS.test(spec));
|
|
523
|
+
const globs = normalized
|
|
524
|
+
.filter(spec => GLOB_CHARS.test(spec))
|
|
525
|
+
.map(spec => {
|
|
526
|
+
const escaped = spec
|
|
527
|
+
.replace(REGEXP_SPECIALS, '\\$&')
|
|
528
|
+
.replace(/\*/g, '.*')
|
|
529
|
+
.replace(/\?/g, '.');
|
|
530
|
+
return new RegExp(`^${escaped}$`);
|
|
531
|
+
});
|
|
532
|
+
return (path) => (literals.length > 0 && inScope(path, literals)) ||
|
|
533
|
+
globs.some(glob => glob.test(path));
|
|
534
|
+
};
|
|
644
535
|
//# sourceMappingURL=GitAdapter.js.map
|