@fern-api/fern-api-dev 5.4.0 → 5.4.1-1-g17393ac251c
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/cli.cjs +1127 -703
- package/package.json +1 -1
package/cli.cjs
CHANGED
|
@@ -340394,9 +340394,9 @@ var require_diff3 = __commonJS({
|
|
|
340394
340394
|
}
|
|
340395
340395
|
});
|
|
340396
340396
|
|
|
340397
|
-
// ../../../node_modules/.pnpm/@fern-api+replay@0.14.
|
|
340397
|
+
// ../../../node_modules/.pnpm/@fern-api+replay@0.14.1/node_modules/@fern-api/replay/dist/index.cjs
|
|
340398
340398
|
var require_dist14 = __commonJS({
|
|
340399
|
-
"../../../node_modules/.pnpm/@fern-api+replay@0.14.
|
|
340399
|
+
"../../../node_modules/.pnpm/@fern-api+replay@0.14.1/node_modules/@fern-api/replay/dist/index.cjs"(exports2, module4) {
|
|
340400
340400
|
"use strict";
|
|
340401
340401
|
var __defProp4 = Object.defineProperty;
|
|
340402
340402
|
var __getOwnPropDesc4 = Object.getOwnPropertyDescriptor;
|
|
@@ -340802,6 +340802,64 @@ var require_dist14 = __commonJS({
|
|
|
340802
340802
|
"use strict";
|
|
340803
340803
|
}
|
|
340804
340804
|
});
|
|
340805
|
+
var PatchApplyTheirs_exports = {};
|
|
340806
|
+
__export4(PatchApplyTheirs_exports, {
|
|
340807
|
+
applyPatchToContent: () => applyPatchToContent
|
|
340808
|
+
});
|
|
340809
|
+
async function applyPatchToContent(base4, patchContent, filePath) {
|
|
340810
|
+
const fileDiff = extractFileDiff(patchContent, filePath);
|
|
340811
|
+
if (!fileDiff) return null;
|
|
340812
|
+
const tempDir2 = await (0, import_promises310.mkdtemp)((0, import_node_path42.join)((0, import_node_os32.tmpdir)(), "replay-migrate-"));
|
|
340813
|
+
const tempGit = new GitClient(tempDir2);
|
|
340814
|
+
try {
|
|
340815
|
+
await tempGit.exec(["init"]);
|
|
340816
|
+
await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
|
|
340817
|
+
await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
|
|
340818
|
+
await tempGit.exec(["config", "commit.gpgSign", "false"]);
|
|
340819
|
+
const tempFilePath = (0, import_node_path42.join)(tempDir2, filePath);
|
|
340820
|
+
await (0, import_promises310.mkdir)((0, import_node_path42.dirname)(tempFilePath), { recursive: true });
|
|
340821
|
+
await (0, import_promises310.writeFile)(tempFilePath, base4);
|
|
340822
|
+
await tempGit.exec(["add", filePath]);
|
|
340823
|
+
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
340824
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
340825
|
+
return await (0, import_promises310.readFile)(tempFilePath, "utf-8");
|
|
340826
|
+
} catch {
|
|
340827
|
+
return null;
|
|
340828
|
+
} finally {
|
|
340829
|
+
await (0, import_promises310.rm)(tempDir2, { recursive: true, force: true }).catch(() => {
|
|
340830
|
+
});
|
|
340831
|
+
}
|
|
340832
|
+
}
|
|
340833
|
+
function extractFileDiff(patchContent, filePath) {
|
|
340834
|
+
const lines = patchContent.split("\n");
|
|
340835
|
+
const out = [];
|
|
340836
|
+
let inTarget = false;
|
|
340837
|
+
for (const line of lines) {
|
|
340838
|
+
if (line.startsWith("diff --git")) {
|
|
340839
|
+
if (inTarget) break;
|
|
340840
|
+
const match3 = line.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
340841
|
+
if (match3 && match3[1] === filePath) {
|
|
340842
|
+
inTarget = true;
|
|
340843
|
+
out.push(line);
|
|
340844
|
+
}
|
|
340845
|
+
continue;
|
|
340846
|
+
}
|
|
340847
|
+
if (inTarget) out.push(line);
|
|
340848
|
+
}
|
|
340849
|
+
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
340850
|
+
}
|
|
340851
|
+
var import_promises310;
|
|
340852
|
+
var import_node_os32;
|
|
340853
|
+
var import_node_path42;
|
|
340854
|
+
var init_PatchApplyTheirs = __esm3({
|
|
340855
|
+
"src/PatchApplyTheirs.ts"() {
|
|
340856
|
+
"use strict";
|
|
340857
|
+
import_promises310 = require("fs/promises");
|
|
340858
|
+
import_node_os32 = require("os");
|
|
340859
|
+
import_node_path42 = require("path");
|
|
340860
|
+
init_GitClient();
|
|
340861
|
+
}
|
|
340862
|
+
});
|
|
340805
340863
|
var index_exports = {};
|
|
340806
340864
|
__export4(index_exports, {
|
|
340807
340865
|
CREDENTIAL_PATTERNS: () => CREDENTIAL_PATTERNS,
|
|
@@ -340892,7 +340950,7 @@ var require_dist14 = __commonJS({
|
|
|
340892
340950
|
return match3?.[1];
|
|
340893
340951
|
}
|
|
340894
340952
|
var import_node_fs32 = require("fs");
|
|
340895
|
-
var
|
|
340953
|
+
var import_node_path43 = require("path");
|
|
340896
340954
|
var import_yaml18 = require_dist9();
|
|
340897
340955
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
340898
340956
|
var LockfileNotFoundError = class extends Error {
|
|
@@ -340909,10 +340967,10 @@ var require_dist14 = __commonJS({
|
|
|
340909
340967
|
this.outputDir = outputDir;
|
|
340910
340968
|
}
|
|
340911
340969
|
get lockfilePath() {
|
|
340912
|
-
return (0,
|
|
340970
|
+
return (0, import_node_path43.join)(this.outputDir, ".fern", "replay.lock");
|
|
340913
340971
|
}
|
|
340914
340972
|
get customizationsPath() {
|
|
340915
|
-
return (0,
|
|
340973
|
+
return (0, import_node_path43.join)(this.outputDir, ".fern", "replay.yml");
|
|
340916
340974
|
}
|
|
340917
340975
|
exists() {
|
|
340918
340976
|
return (0, import_node_fs32.existsSync)(this.lockfilePath);
|
|
@@ -340956,7 +341014,17 @@ var require_dist14 = __commonJS({
|
|
|
340956
341014
|
this.warnings.length = 0;
|
|
340957
341015
|
const cleanPatches = [];
|
|
340958
341016
|
for (const patch5 of this.lock.patches) {
|
|
340959
|
-
const
|
|
341017
|
+
const diffMatches = scanForCredentials(patch5.patch_content);
|
|
341018
|
+
const snapshotMatches = [];
|
|
341019
|
+
if (patch5.theirs_snapshot) {
|
|
341020
|
+
for (const content22 of Object.values(patch5.theirs_snapshot)) {
|
|
341021
|
+
const m7 = scanForCredentials(content22);
|
|
341022
|
+
for (const x5 of m7) {
|
|
341023
|
+
if (!snapshotMatches.includes(x5)) snapshotMatches.push(x5);
|
|
341024
|
+
}
|
|
341025
|
+
}
|
|
341026
|
+
}
|
|
341027
|
+
const credentialMatches = [...diffMatches, ...snapshotMatches.filter((m7) => !diffMatches.includes(m7))];
|
|
340960
341028
|
const sensitiveFiles = patch5.files.filter((f5) => isSensitiveFile(f5));
|
|
340961
341029
|
if (credentialMatches.length > 0 || sensitiveFiles.length > 0) {
|
|
340962
341030
|
const reasons = [];
|
|
@@ -340974,7 +341042,7 @@ var require_dist14 = __commonJS({
|
|
|
340974
341042
|
}
|
|
340975
341043
|
}
|
|
340976
341044
|
this.lock.patches = cleanPatches;
|
|
340977
|
-
const dir = (0,
|
|
341045
|
+
const dir = (0, import_node_path43.dirname)(this.lockfilePath);
|
|
340978
341046
|
if (!(0, import_node_fs32.existsSync)(dir)) {
|
|
340979
341047
|
(0, import_node_fs32.mkdirSync)(dir, { recursive: true });
|
|
340980
341048
|
}
|
|
@@ -341075,572 +341143,81 @@ var require_dist14 = __commonJS({
|
|
|
341075
341143
|
}
|
|
341076
341144
|
};
|
|
341077
341145
|
var import_node_crypto7 = require("crypto");
|
|
341078
|
-
var
|
|
341079
|
-
|
|
341080
|
-
|
|
341081
|
-
|
|
341082
|
-
|
|
341083
|
-
|
|
341084
|
-
|
|
341085
|
-
|
|
341086
|
-
|
|
341087
|
-
}
|
|
341088
|
-
};
|
|
341089
|
-
for (const line of patchContent.split("\n")) {
|
|
341090
|
-
if (line.startsWith("diff --git ")) {
|
|
341091
|
-
flush3();
|
|
341092
|
-
currentPath = null;
|
|
341093
|
-
currentIsCreate = false;
|
|
341094
|
-
currentIsDelete = false;
|
|
341095
|
-
const match3 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
341096
|
-
if (match3) {
|
|
341097
|
-
currentPath = match3[2] ?? null;
|
|
341098
|
-
}
|
|
341099
|
-
} else if (currentPath !== null) {
|
|
341100
|
-
if (line.startsWith("new file mode ")) {
|
|
341101
|
-
currentIsCreate = true;
|
|
341102
|
-
} else if (line.startsWith("deleted file mode ")) {
|
|
341103
|
-
currentIsDelete = true;
|
|
341104
|
-
}
|
|
341105
|
-
}
|
|
341106
|
-
}
|
|
341107
|
-
flush3();
|
|
341108
|
-
return results;
|
|
341146
|
+
var import_promises193 = require("fs/promises");
|
|
341147
|
+
var import_node_os15 = require("os");
|
|
341148
|
+
var import_node_path210 = require("path");
|
|
341149
|
+
var import_minimatch8 = require_commonjs3();
|
|
341150
|
+
var CONFLICT_OPENER = "<<<<<<< Generated";
|
|
341151
|
+
var CONFLICT_SEPARATOR = "=======";
|
|
341152
|
+
var CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
341153
|
+
function trimCR(line) {
|
|
341154
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
341109
341155
|
}
|
|
341110
|
-
|
|
341111
|
-
|
|
341112
|
-
|
|
341113
|
-
|
|
341114
|
-
|
|
341115
|
-
|
|
341116
|
-
|
|
341117
|
-
|
|
341118
|
-
|
|
341119
|
-
|
|
341120
|
-
|
|
341121
|
-
|
|
341122
|
-
const lastGen = this.getLastGeneration(lock);
|
|
341123
|
-
if (!lastGen) {
|
|
341124
|
-
return { patches: [], revertedPatchIds: [] };
|
|
341125
|
-
}
|
|
341126
|
-
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
341127
|
-
if (!exists2) {
|
|
341128
|
-
this.warnings.push(
|
|
341129
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
341130
|
-
);
|
|
341131
|
-
return this.detectPatchesViaTreeDiff(
|
|
341132
|
-
lastGen,
|
|
341133
|
-
/* commitKnownMissing */
|
|
341134
|
-
true
|
|
341135
|
-
);
|
|
341136
|
-
}
|
|
341137
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
341138
|
-
if (!isAncestor) {
|
|
341139
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
341140
|
-
}
|
|
341141
|
-
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
341142
|
-
}
|
|
341143
|
-
/**
|
|
341144
|
-
* FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
341145
|
-
*
|
|
341146
|
-
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
341147
|
-
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
341148
|
-
* merge-base is the commit on main from which the bot's branch was created —
|
|
341149
|
-
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
341150
|
-
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
341151
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
341152
|
-
* generation) get bundled as customer customizations.
|
|
341153
|
-
*
|
|
341154
|
-
* Falls through to the legacy composite path when no common ancestor exists
|
|
341155
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
341156
|
-
*/
|
|
341157
|
-
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
341158
|
-
let mergeBase = "";
|
|
341159
|
-
try {
|
|
341160
|
-
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
341161
|
-
} catch {
|
|
341162
|
-
}
|
|
341163
|
-
if (!mergeBase) {
|
|
341164
|
-
this.warnings.push(
|
|
341165
|
-
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
341166
|
-
);
|
|
341167
|
-
return this.detectPatchesViaTreeDiff(
|
|
341168
|
-
lastGen,
|
|
341169
|
-
/* commitKnownMissing */
|
|
341170
|
-
false
|
|
341171
|
-
);
|
|
341172
|
-
}
|
|
341173
|
-
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
341174
|
-
}
|
|
341175
|
-
/**
|
|
341176
|
-
* FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
341177
|
-
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
341178
|
-
* (rangeStart = lastGen.commit_sha) and the merge-base path.
|
|
341179
|
-
*
|
|
341180
|
-
* Patch `base_generation` is always `lastGen.commit_sha` — the
|
|
341181
|
-
* lockfile-tracked reference the applicator uses to fetch base file
|
|
341182
|
-
* content, regardless of where the walk actually started.
|
|
341183
|
-
*/
|
|
341184
|
-
async detectPatchesInRange(rangeStart, lastGen, lock) {
|
|
341185
|
-
const log4 = await this.git.exec([
|
|
341186
|
-
"log",
|
|
341187
|
-
"--first-parent",
|
|
341188
|
-
"--format=%H%x00%an%x00%ae%x00%s",
|
|
341189
|
-
`${rangeStart}..HEAD`,
|
|
341190
|
-
"--",
|
|
341191
|
-
this.sdkOutputDir
|
|
341192
|
-
]);
|
|
341193
|
-
if (!log4.trim()) {
|
|
341194
|
-
return { patches: [], revertedPatchIds: [] };
|
|
341195
|
-
}
|
|
341196
|
-
const commits = this.parseGitLog(log4);
|
|
341197
|
-
const newPatches = [];
|
|
341198
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
341199
|
-
for (const commit of commits) {
|
|
341200
|
-
if (isGenerationCommit(commit)) {
|
|
341201
|
-
continue;
|
|
341202
|
-
}
|
|
341203
|
-
if (lock.patches.find((p14) => p14.original_commit === commit.sha)) {
|
|
341204
|
-
continue;
|
|
341205
|
-
}
|
|
341206
|
-
const parents = await this.git.getCommitParents(commit.sha);
|
|
341207
|
-
if (parents.length > 1) {
|
|
341208
|
-
const compositePatch = await this.createMergeCompositePatch({
|
|
341209
|
-
mergeCommit: commit,
|
|
341210
|
-
firstParent: parents[0],
|
|
341211
|
-
lastGen,
|
|
341212
|
-
lock
|
|
341213
|
-
});
|
|
341214
|
-
if (compositePatch) {
|
|
341215
|
-
newPatches.push(compositePatch);
|
|
341156
|
+
function findConflictRanges(lines) {
|
|
341157
|
+
const ranges = [];
|
|
341158
|
+
let i9 = 0;
|
|
341159
|
+
while (i9 < lines.length) {
|
|
341160
|
+
if (trimCR(lines[i9]) === CONFLICT_OPENER) {
|
|
341161
|
+
let separatorIdx = -1;
|
|
341162
|
+
let j7 = i9 + 1;
|
|
341163
|
+
let found = false;
|
|
341164
|
+
while (j7 < lines.length) {
|
|
341165
|
+
const trimmed2 = trimCR(lines[j7]);
|
|
341166
|
+
if (trimmed2 === CONFLICT_OPENER) {
|
|
341167
|
+
break;
|
|
341216
341168
|
}
|
|
341217
|
-
|
|
341218
|
-
|
|
341219
|
-
|
|
341220
|
-
|
|
341221
|
-
|
|
341222
|
-
|
|
341223
|
-
|
|
341224
|
-
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
341225
|
-
);
|
|
341226
|
-
continue;
|
|
341227
|
-
}
|
|
341228
|
-
let contentHash = this.computeContentHash(patchContent);
|
|
341229
|
-
if (lock.patches.find((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
341230
|
-
continue;
|
|
341231
|
-
}
|
|
341232
|
-
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
341233
|
-
const allFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5)).filter((f5) => !f5.startsWith(".fern/"));
|
|
341234
|
-
const sensitiveFiles = allFiles.filter((f5) => isSensitiveFile(f5));
|
|
341235
|
-
const files = allFiles.filter((f5) => !isSensitiveFile(f5));
|
|
341236
|
-
if (sensitiveFiles.length > 0) {
|
|
341237
|
-
this.warnings.push(
|
|
341238
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
341239
|
-
);
|
|
341240
|
-
if (files.length > 0) {
|
|
341241
|
-
const parentSha = parents[0];
|
|
341242
|
-
if (parentSha) {
|
|
341243
|
-
try {
|
|
341244
|
-
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
341245
|
-
} catch {
|
|
341246
|
-
continue;
|
|
341247
|
-
}
|
|
341248
|
-
if (!patchContent.trim()) continue;
|
|
341249
|
-
contentHash = this.computeContentHash(patchContent);
|
|
341250
|
-
}
|
|
341169
|
+
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
341170
|
+
separatorIdx = j7;
|
|
341171
|
+
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
341172
|
+
ranges.push({ start: i9, separator: separatorIdx, end: j7 });
|
|
341173
|
+
i9 = j7;
|
|
341174
|
+
found = true;
|
|
341175
|
+
break;
|
|
341251
341176
|
}
|
|
341177
|
+
j7++;
|
|
341252
341178
|
}
|
|
341253
|
-
if (
|
|
341179
|
+
if (!found) {
|
|
341180
|
+
i9++;
|
|
341254
341181
|
continue;
|
|
341255
341182
|
}
|
|
341256
|
-
newPatches.push({
|
|
341257
|
-
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
341258
|
-
content_hash: contentHash,
|
|
341259
|
-
original_commit: commit.sha,
|
|
341260
|
-
original_message: commit.message,
|
|
341261
|
-
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
341262
|
-
base_generation: lastGen.commit_sha,
|
|
341263
|
-
files,
|
|
341264
|
-
patch_content: patchContent,
|
|
341265
|
-
...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
|
|
341266
|
-
});
|
|
341267
|
-
}
|
|
341268
|
-
const survivingPatches = await this.collapseNetZeroFiles(newPatches);
|
|
341269
|
-
survivingPatches.reverse();
|
|
341270
|
-
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
341271
|
-
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
341272
|
-
for (let i9 = 0; i9 < survivingPatches.length; i9++) {
|
|
341273
|
-
const patch5 = survivingPatches[i9];
|
|
341274
|
-
if (!isRevertCommit(patch5.original_message)) continue;
|
|
341275
|
-
let body = "";
|
|
341276
|
-
try {
|
|
341277
|
-
body = await this.git.getCommitBody(patch5.original_commit);
|
|
341278
|
-
} catch {
|
|
341279
|
-
}
|
|
341280
|
-
const revertedSha = parseRevertedSha(body);
|
|
341281
|
-
const revertedMessage = parseRevertedMessage(patch5.original_message);
|
|
341282
|
-
let matchedExisting = false;
|
|
341283
|
-
if (revertedSha) {
|
|
341284
|
-
const existing = lock.patches.find((p14) => p14.original_commit === revertedSha);
|
|
341285
|
-
if (existing) {
|
|
341286
|
-
revertedPatchIdSet.add(existing.id);
|
|
341287
|
-
revertIndicesToRemove.add(i9);
|
|
341288
|
-
matchedExisting = true;
|
|
341289
|
-
}
|
|
341290
|
-
}
|
|
341291
|
-
if (!matchedExisting && revertedMessage) {
|
|
341292
|
-
const existing = lock.patches.find((p14) => p14.original_message === revertedMessage);
|
|
341293
|
-
if (existing) {
|
|
341294
|
-
revertedPatchIdSet.add(existing.id);
|
|
341295
|
-
revertIndicesToRemove.add(i9);
|
|
341296
|
-
matchedExisting = true;
|
|
341297
|
-
}
|
|
341298
|
-
}
|
|
341299
|
-
if (matchedExisting) continue;
|
|
341300
|
-
let matchedNew = false;
|
|
341301
|
-
if (revertedSha) {
|
|
341302
|
-
const idx = survivingPatches.findIndex(
|
|
341303
|
-
(p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_commit === revertedSha
|
|
341304
|
-
);
|
|
341305
|
-
if (idx !== -1) {
|
|
341306
|
-
revertIndicesToRemove.add(i9);
|
|
341307
|
-
revertIndicesToRemove.add(idx);
|
|
341308
|
-
matchedNew = true;
|
|
341309
|
-
}
|
|
341310
|
-
}
|
|
341311
|
-
if (!matchedNew && revertedMessage) {
|
|
341312
|
-
const idx = survivingPatches.findIndex(
|
|
341313
|
-
(p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_message === revertedMessage
|
|
341314
|
-
);
|
|
341315
|
-
if (idx !== -1) {
|
|
341316
|
-
revertIndicesToRemove.add(i9);
|
|
341317
|
-
revertIndicesToRemove.add(idx);
|
|
341318
|
-
}
|
|
341319
|
-
}
|
|
341320
|
-
if (!matchedExisting && !matchedNew) {
|
|
341321
|
-
revertIndicesToRemove.add(i9);
|
|
341322
|
-
}
|
|
341323
341183
|
}
|
|
341324
|
-
|
|
341325
|
-
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
341326
|
-
}
|
|
341327
|
-
/**
|
|
341328
|
-
* Compute content hash for deduplication.
|
|
341329
|
-
*
|
|
341330
|
-
* Produces a format-agnostic hash so both `git format-patch` output
|
|
341331
|
-
* (email-wrapped) and plain `git diff` output hash to the same value
|
|
341332
|
-
* when the underlying diff hunks are identical.
|
|
341333
|
-
*
|
|
341334
|
-
* Normalization:
|
|
341335
|
-
* 1. Strip email wrapper: everything before the first `diff --git` line
|
|
341336
|
-
* (From:, Subject:, Date:, diffstat, blank separators).
|
|
341337
|
-
* 2. Strip `index` lines (blob SHA pairs change across rebases).
|
|
341338
|
-
* 3. Strip trailing git version marker (`-- \n<version>`).
|
|
341339
|
-
*
|
|
341340
|
-
* This ensures content hashes match across the no-patches and normal-
|
|
341341
|
-
* regeneration flows, which store formatPatch and git-diff formats
|
|
341342
|
-
* respectively (FER-9850, D5-D9).
|
|
341343
|
-
*/
|
|
341344
|
-
computeContentHash(patchContent) {
|
|
341345
|
-
const lines = patchContent.split("\n");
|
|
341346
|
-
const diffStart = lines.findIndex((l9) => l9.startsWith("diff --git "));
|
|
341347
|
-
const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
|
|
341348
|
-
const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
|
|
341349
|
-
return `sha256:${(0, import_node_crypto7.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
341350
|
-
}
|
|
341351
|
-
/**
|
|
341352
|
-
* FER-9805 — Drop patches whose files were transiently created and then
|
|
341353
|
-
* deleted within the current linear detection window, and are absent from
|
|
341354
|
-
* HEAD at the end of the window.
|
|
341355
|
-
*
|
|
341356
|
-
* Rationale: on a merge commit, createMergeCompositePatch already diffs
|
|
341357
|
-
* firstParent..mergeCommit so net-zero files never enter the composite. On
|
|
341358
|
-
* linear history each commit becomes its own patch, so a file that was
|
|
341359
|
-
* briefly added and later removed survives as a create+delete pair whose
|
|
341360
|
-
* cumulative diff is empty. We drop such patches before they reach the
|
|
341361
|
-
* lockfile.
|
|
341362
|
-
*
|
|
341363
|
-
* A file is "transient" when:
|
|
341364
|
-
* 1. some patch in the window sets `new file mode` for it (a creation), AND
|
|
341365
|
-
* 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
|
|
341366
|
-
* 3. it is absent from HEAD's tree.
|
|
341367
|
-
*
|
|
341368
|
-
* The HEAD guard (#3) prevents false positives when a file is deleted
|
|
341369
|
-
* and then recreated with different content — the cumulative diff of
|
|
341370
|
-
* such a pair is non-empty and must be preserved.
|
|
341371
|
-
*
|
|
341372
|
-
* This only drops patches whose `files` are a subset of the transient
|
|
341373
|
-
* set. A partial-transient patch (one that touches a transient file
|
|
341374
|
-
* alongside a persistent one) is left unmodified; surgical stripping of
|
|
341375
|
-
* single files from a multi-file patch would require a follow-up that
|
|
341376
|
-
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
341377
|
-
* current failing test exercises this, and leaving the patch intact
|
|
341378
|
-
* preserves today's behaviour. TODO(FER-9805): revisit if a future
|
|
341379
|
-
* adversarial test demands per-file stripping.
|
|
341380
|
-
*/
|
|
341381
|
-
async collapseNetZeroFiles(patches) {
|
|
341382
|
-
if (patches.length === 0) return patches;
|
|
341383
|
-
const stateByFile = /* @__PURE__ */ new Map();
|
|
341384
|
-
for (const patch5 of patches) {
|
|
341385
|
-
for (const header of parsePatchFileHeaders(patch5.patch_content)) {
|
|
341386
|
-
if (!header.isCreate && !header.isDelete) continue;
|
|
341387
|
-
const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
|
|
341388
|
-
if (header.isCreate) prior.created = true;
|
|
341389
|
-
if (header.isDelete) prior.deleted = true;
|
|
341390
|
-
stateByFile.set(header.path, prior);
|
|
341391
|
-
}
|
|
341392
|
-
}
|
|
341393
|
-
let anyCandidate = false;
|
|
341394
|
-
for (const state of stateByFile.values()) {
|
|
341395
|
-
if (state.created && state.deleted) {
|
|
341396
|
-
anyCandidate = true;
|
|
341397
|
-
break;
|
|
341398
|
-
}
|
|
341399
|
-
}
|
|
341400
|
-
if (!anyCandidate) return patches;
|
|
341401
|
-
const headFiles = await this.listHeadFiles();
|
|
341402
|
-
const transient = /* @__PURE__ */ new Set();
|
|
341403
|
-
for (const [file4, state] of stateByFile) {
|
|
341404
|
-
if (state.created && state.deleted && !headFiles.has(file4)) {
|
|
341405
|
-
transient.add(file4);
|
|
341406
|
-
}
|
|
341407
|
-
}
|
|
341408
|
-
if (transient.size === 0) return patches;
|
|
341409
|
-
return patches.filter((patch5) => !patch5.files.every((f5) => transient.has(f5)));
|
|
341410
|
-
}
|
|
341411
|
-
/**
|
|
341412
|
-
* List every tracked path at HEAD (one `git ls-tree -r` invocation).
|
|
341413
|
-
* Returns an empty Set if HEAD is unborn or the invocation fails — the
|
|
341414
|
-
* caller then treats every candidate as "not in HEAD" and may collapse
|
|
341415
|
-
* more aggressively. On typical SDK repos this is a single sub-10ms call.
|
|
341416
|
-
*/
|
|
341417
|
-
async listHeadFiles() {
|
|
341418
|
-
try {
|
|
341419
|
-
const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
|
|
341420
|
-
return new Set(out.split("\n").map((l9) => l9.trim()).filter(Boolean));
|
|
341421
|
-
} catch {
|
|
341422
|
-
return /* @__PURE__ */ new Set();
|
|
341423
|
-
}
|
|
341424
|
-
}
|
|
341425
|
-
/**
|
|
341426
|
-
* Create a single composite patch for a merge commit by diffing the merge
|
|
341427
|
-
* commit against its first parent. This captures the net change of the
|
|
341428
|
-
* entire merged branch as one patch, eliminating accumulator chaining and
|
|
341429
|
-
* zero-net-change ghost conflicts.
|
|
341430
|
-
*/
|
|
341431
|
-
async createMergeCompositePatch(input3) {
|
|
341432
|
-
const { mergeCommit, firstParent, lastGen, lock } = input3;
|
|
341433
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
341434
|
-
const filesOutput = await this.git.exec([
|
|
341435
|
-
"diff",
|
|
341436
|
-
"--name-only",
|
|
341437
|
-
firstParent,
|
|
341438
|
-
mergeCommit.sha
|
|
341439
|
-
]);
|
|
341440
|
-
const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5)).filter((f5) => !f5.startsWith(".fern/"));
|
|
341441
|
-
const sensitiveMergeFiles = allMergeFiles.filter((f5) => isSensitiveFile(f5));
|
|
341442
|
-
const files = allMergeFiles.filter((f5) => !isSensitiveFile(f5));
|
|
341443
|
-
if (sensitiveMergeFiles.length > 0) {
|
|
341444
|
-
this.warnings.push(
|
|
341445
|
-
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
341446
|
-
);
|
|
341447
|
-
}
|
|
341448
|
-
if (files.length === 0) return null;
|
|
341449
|
-
const diff2 = await this.git.exec([
|
|
341450
|
-
"diff",
|
|
341451
|
-
firstParent,
|
|
341452
|
-
mergeCommit.sha,
|
|
341453
|
-
"--",
|
|
341454
|
-
...files
|
|
341455
|
-
]);
|
|
341456
|
-
if (!diff2.trim()) return null;
|
|
341457
|
-
const contentHash = this.computeContentHash(diff2);
|
|
341458
|
-
if (lock.patches.some((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
341459
|
-
return null;
|
|
341460
|
-
}
|
|
341461
|
-
return {
|
|
341462
|
-
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
341463
|
-
content_hash: contentHash,
|
|
341464
|
-
original_commit: mergeCommit.sha,
|
|
341465
|
-
original_message: mergeCommit.message,
|
|
341466
|
-
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
341467
|
-
base_generation: lastGen.commit_sha,
|
|
341468
|
-
files,
|
|
341469
|
-
patch_content: diff2,
|
|
341470
|
-
...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
|
|
341471
|
-
};
|
|
341184
|
+
i9++;
|
|
341472
341185
|
}
|
|
341473
|
-
|
|
341474
|
-
|
|
341475
|
-
|
|
341476
|
-
|
|
341477
|
-
|
|
341478
|
-
|
|
341479
|
-
|
|
341480
|
-
if (!diffBase) {
|
|
341481
|
-
return this.detectPatchesViaCommitScan();
|
|
341482
|
-
}
|
|
341483
|
-
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
341484
|
-
const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5)).filter((f5) => !f5.startsWith(".fern/"));
|
|
341485
|
-
const sensitiveTreeFiles = allTreeFiles.filter((f5) => isSensitiveFile(f5));
|
|
341486
|
-
const files = allTreeFiles.filter((f5) => !isSensitiveFile(f5));
|
|
341487
|
-
if (sensitiveTreeFiles.length > 0) {
|
|
341488
|
-
this.warnings.push(
|
|
341489
|
-
`Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
341490
|
-
);
|
|
341491
|
-
}
|
|
341492
|
-
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
341493
|
-
const diff2 = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
341494
|
-
if (!diff2.trim()) return { patches: [], revertedPatchIds: [] };
|
|
341495
|
-
const contentHash = this.computeContentHash(diff2);
|
|
341496
|
-
const lock = this.lockManager.read();
|
|
341497
|
-
if (lock.patches.some((p14) => p14.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
341498
|
-
return { patches: [], revertedPatchIds: [] };
|
|
341499
|
-
}
|
|
341500
|
-
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
341501
|
-
const compositePatch = {
|
|
341502
|
-
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
341503
|
-
content_hash: contentHash,
|
|
341504
|
-
original_commit: headSha,
|
|
341505
|
-
original_message: "Customer customizations (composite)",
|
|
341506
|
-
original_author: "composite",
|
|
341507
|
-
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
341508
|
-
// reference to find base file content. diffBase may be the tree_hash.
|
|
341509
|
-
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
341510
|
-
files,
|
|
341511
|
-
patch_content: diff2,
|
|
341512
|
-
...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
|
|
341513
|
-
};
|
|
341514
|
-
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
341186
|
+
return ranges;
|
|
341187
|
+
}
|
|
341188
|
+
function stripConflictMarkers(content5) {
|
|
341189
|
+
const lines = content5.split(/\r?\n/);
|
|
341190
|
+
const ranges = findConflictRanges(lines);
|
|
341191
|
+
if (ranges.length === 0) {
|
|
341192
|
+
return content5;
|
|
341515
341193
|
}
|
|
341516
|
-
|
|
341517
|
-
|
|
341518
|
-
|
|
341519
|
-
|
|
341520
|
-
|
|
341521
|
-
|
|
341522
|
-
* can find base file content from the parent's tree (which IS reachable).
|
|
341523
|
-
*/
|
|
341524
|
-
async detectPatchesViaCommitScan() {
|
|
341525
|
-
const lock = this.lockManager.read();
|
|
341526
|
-
const log4 = await this.git.exec([
|
|
341527
|
-
"log",
|
|
341528
|
-
"--max-count=200",
|
|
341529
|
-
"--format=%H%x00%an%x00%ae%x00%s",
|
|
341530
|
-
"HEAD",
|
|
341531
|
-
"--",
|
|
341532
|
-
this.sdkOutputDir
|
|
341533
|
-
]);
|
|
341534
|
-
if (!log4.trim()) {
|
|
341535
|
-
return { patches: [], revertedPatchIds: [] };
|
|
341536
|
-
}
|
|
341537
|
-
const commits = this.parseGitLog(log4);
|
|
341538
|
-
const newPatches = [];
|
|
341539
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
341540
|
-
const existingHashes = new Set(lock.patches.map((p14) => p14.content_hash));
|
|
341541
|
-
const existingCommits = new Set(lock.patches.map((p14) => p14.original_commit));
|
|
341542
|
-
for (const commit of commits) {
|
|
341543
|
-
if (isGenerationCommit(commit)) {
|
|
341544
|
-
continue;
|
|
341545
|
-
}
|
|
341546
|
-
const parents = await this.git.getCommitParents(commit.sha);
|
|
341547
|
-
if (parents.length > 1) {
|
|
341548
|
-
continue;
|
|
341549
|
-
}
|
|
341550
|
-
if (existingCommits.has(commit.sha)) {
|
|
341551
|
-
continue;
|
|
341552
|
-
}
|
|
341553
|
-
let patchContent;
|
|
341554
|
-
try {
|
|
341555
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
341556
|
-
} catch {
|
|
341194
|
+
const result = [];
|
|
341195
|
+
let rangeIdx = 0;
|
|
341196
|
+
for (let i9 = 0; i9 < lines.length; i9++) {
|
|
341197
|
+
if (rangeIdx < ranges.length) {
|
|
341198
|
+
const range2 = ranges[rangeIdx];
|
|
341199
|
+
if (i9 === range2.start) {
|
|
341557
341200
|
continue;
|
|
341558
341201
|
}
|
|
341559
|
-
|
|
341560
|
-
|
|
341202
|
+
if (i9 > range2.start && i9 < range2.separator) {
|
|
341203
|
+
result.push(lines[i9]);
|
|
341561
341204
|
continue;
|
|
341562
341205
|
}
|
|
341563
|
-
if (
|
|
341206
|
+
if (i9 === range2.separator) {
|
|
341564
341207
|
continue;
|
|
341565
341208
|
}
|
|
341566
|
-
|
|
341567
|
-
const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5));
|
|
341568
|
-
const sensitiveScanFiles = allScanFiles.filter((f5) => isSensitiveFile(f5));
|
|
341569
|
-
const files = allScanFiles.filter((f5) => !isSensitiveFile(f5));
|
|
341570
|
-
if (sensitiveScanFiles.length > 0) {
|
|
341571
|
-
this.warnings.push(
|
|
341572
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
|
|
341573
|
-
);
|
|
341574
|
-
if (files.length > 0) {
|
|
341575
|
-
if (parents.length === 0) {
|
|
341576
|
-
continue;
|
|
341577
|
-
}
|
|
341578
|
-
try {
|
|
341579
|
-
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
341580
|
-
} catch {
|
|
341581
|
-
continue;
|
|
341582
|
-
}
|
|
341583
|
-
if (!patchContent.trim()) continue;
|
|
341584
|
-
contentHash = this.computeContentHash(patchContent);
|
|
341585
|
-
}
|
|
341586
|
-
}
|
|
341587
|
-
if (files.length === 0) {
|
|
341209
|
+
if (i9 > range2.separator && i9 < range2.end) {
|
|
341588
341210
|
continue;
|
|
341589
341211
|
}
|
|
341590
|
-
if (
|
|
341212
|
+
if (i9 === range2.end) {
|
|
341213
|
+
rangeIdx++;
|
|
341591
341214
|
continue;
|
|
341592
341215
|
}
|
|
341593
|
-
const parentSha = parents[0];
|
|
341594
|
-
newPatches.push({
|
|
341595
|
-
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
341596
|
-
content_hash: contentHash,
|
|
341597
|
-
original_commit: commit.sha,
|
|
341598
|
-
original_message: commit.message,
|
|
341599
|
-
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
341600
|
-
base_generation: parentSha,
|
|
341601
|
-
files,
|
|
341602
|
-
patch_content: patchContent
|
|
341603
|
-
});
|
|
341604
341216
|
}
|
|
341605
|
-
|
|
341606
|
-
return { patches: newPatches, revertedPatchIds: [] };
|
|
341607
|
-
}
|
|
341608
|
-
/**
|
|
341609
|
-
* Check if a format-patch consists entirely of new-file creations.
|
|
341610
|
-
* Used to identify squashed commits after force push, which create all files
|
|
341611
|
-
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
341612
|
-
*/
|
|
341613
|
-
isCreationOnlyPatch(patchContent) {
|
|
341614
|
-
const diffOldHeaders = patchContent.split("\n").filter((l9) => l9.startsWith("--- "));
|
|
341615
|
-
if (diffOldHeaders.length === 0) {
|
|
341616
|
-
return false;
|
|
341617
|
-
}
|
|
341618
|
-
return diffOldHeaders.every((l9) => l9 === "--- /dev/null");
|
|
341619
|
-
}
|
|
341620
|
-
/**
|
|
341621
|
-
* Resolve the best available diff base for a generation record.
|
|
341622
|
-
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
341623
|
-
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
341624
|
-
*/
|
|
341625
|
-
async resolveDiffBase(gen, commitKnownMissing) {
|
|
341626
|
-
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
341627
|
-
return gen.commit_sha;
|
|
341628
|
-
}
|
|
341629
|
-
if (await this.git.treeExists(gen.tree_hash)) {
|
|
341630
|
-
return gen.tree_hash;
|
|
341631
|
-
}
|
|
341632
|
-
return null;
|
|
341633
|
-
}
|
|
341634
|
-
parseGitLog(log4) {
|
|
341635
|
-
return log4.trim().split("\n").map((line) => {
|
|
341636
|
-
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
341637
|
-
return { sha, authorName, authorEmail, message };
|
|
341638
|
-
});
|
|
341639
|
-
}
|
|
341640
|
-
getLastGeneration(lock) {
|
|
341641
|
-
return lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
|
|
341217
|
+
result.push(lines[i9]);
|
|
341642
341218
|
}
|
|
341643
|
-
|
|
341219
|
+
return result.join("\n");
|
|
341220
|
+
}
|
|
341644
341221
|
var import_node_diff3 = require_diff3();
|
|
341645
341222
|
function threeWayMerge(base4, ours, theirs) {
|
|
341646
341223
|
const baseLines = base4.split("\n");
|
|
@@ -341778,81 +341355,6 @@ var require_dist14 = __commonJS({
|
|
|
341778
341355
|
}
|
|
341779
341356
|
return result;
|
|
341780
341357
|
}
|
|
341781
|
-
var import_promises193 = require("fs/promises");
|
|
341782
|
-
var import_node_os15 = require("os");
|
|
341783
|
-
var import_node_path210 = require("path");
|
|
341784
|
-
var import_minimatch8 = require_commonjs3();
|
|
341785
|
-
var CONFLICT_OPENER = "<<<<<<< Generated";
|
|
341786
|
-
var CONFLICT_SEPARATOR = "=======";
|
|
341787
|
-
var CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
341788
|
-
function trimCR(line) {
|
|
341789
|
-
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
341790
|
-
}
|
|
341791
|
-
function findConflictRanges(lines) {
|
|
341792
|
-
const ranges = [];
|
|
341793
|
-
let i9 = 0;
|
|
341794
|
-
while (i9 < lines.length) {
|
|
341795
|
-
if (trimCR(lines[i9]) === CONFLICT_OPENER) {
|
|
341796
|
-
let separatorIdx = -1;
|
|
341797
|
-
let j7 = i9 + 1;
|
|
341798
|
-
let found = false;
|
|
341799
|
-
while (j7 < lines.length) {
|
|
341800
|
-
const trimmed2 = trimCR(lines[j7]);
|
|
341801
|
-
if (trimmed2 === CONFLICT_OPENER) {
|
|
341802
|
-
break;
|
|
341803
|
-
}
|
|
341804
|
-
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
341805
|
-
separatorIdx = j7;
|
|
341806
|
-
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
341807
|
-
ranges.push({ start: i9, separator: separatorIdx, end: j7 });
|
|
341808
|
-
i9 = j7;
|
|
341809
|
-
found = true;
|
|
341810
|
-
break;
|
|
341811
|
-
}
|
|
341812
|
-
j7++;
|
|
341813
|
-
}
|
|
341814
|
-
if (!found) {
|
|
341815
|
-
i9++;
|
|
341816
|
-
continue;
|
|
341817
|
-
}
|
|
341818
|
-
}
|
|
341819
|
-
i9++;
|
|
341820
|
-
}
|
|
341821
|
-
return ranges;
|
|
341822
|
-
}
|
|
341823
|
-
function stripConflictMarkers(content5) {
|
|
341824
|
-
const lines = content5.split(/\r?\n/);
|
|
341825
|
-
const ranges = findConflictRanges(lines);
|
|
341826
|
-
if (ranges.length === 0) {
|
|
341827
|
-
return content5;
|
|
341828
|
-
}
|
|
341829
|
-
const result = [];
|
|
341830
|
-
let rangeIdx = 0;
|
|
341831
|
-
for (let i9 = 0; i9 < lines.length; i9++) {
|
|
341832
|
-
if (rangeIdx < ranges.length) {
|
|
341833
|
-
const range2 = ranges[rangeIdx];
|
|
341834
|
-
if (i9 === range2.start) {
|
|
341835
|
-
continue;
|
|
341836
|
-
}
|
|
341837
|
-
if (i9 > range2.start && i9 < range2.separator) {
|
|
341838
|
-
result.push(lines[i9]);
|
|
341839
|
-
continue;
|
|
341840
|
-
}
|
|
341841
|
-
if (i9 === range2.separator) {
|
|
341842
|
-
continue;
|
|
341843
|
-
}
|
|
341844
|
-
if (i9 > range2.separator && i9 < range2.end) {
|
|
341845
|
-
continue;
|
|
341846
|
-
}
|
|
341847
|
-
if (i9 === range2.end) {
|
|
341848
|
-
rangeIdx++;
|
|
341849
|
-
continue;
|
|
341850
|
-
}
|
|
341851
|
-
}
|
|
341852
|
-
result.push(lines[i9]);
|
|
341853
|
-
}
|
|
341854
|
-
return result.join("\n");
|
|
341855
|
-
}
|
|
341856
341358
|
var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
341857
341359
|
".png",
|
|
341858
341360
|
".jpg",
|
|
@@ -341922,6 +341424,26 @@ var require_dist14 = __commonJS({
|
|
|
341922
341424
|
* cross-patch context mismatch
|
|
341923
341425
|
*/
|
|
341924
341426
|
currentApplyMode = "replay";
|
|
341427
|
+
/**
|
|
341428
|
+
* Set of files that appear in 2+ patches in the current `applyPatches`
|
|
341429
|
+
* invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
|
|
341430
|
+
* and `populateAccumulatorForPatch` to decide whether to use the patch's
|
|
341431
|
+
* `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
|
|
341432
|
+
* back to line-anchored reconstruction (FER-9525 cross-patch isolation).
|
|
341433
|
+
*
|
|
341434
|
+
* Snapshot-as-primary is correct when the patch owns its file (no other
|
|
341435
|
+
* patch in this run touches it): the snapshot is what the customer
|
|
341436
|
+
* intends for that file post-regen, regardless of generator structural
|
|
341437
|
+
* changes that would invalidate the diff's line anchors.
|
|
341438
|
+
*
|
|
341439
|
+
* When a file IS shared, snapshots are CUMULATIVE across the patches
|
|
341440
|
+
* touching it (each one's snapshot includes prior patches' contributions).
|
|
341441
|
+
* Using a later patch's cumulative snapshot as its individual THEIRS
|
|
341442
|
+
* would propagate prior patches' edits into the merge — surfacing nested
|
|
341443
|
+
* conflicts at regions the patch alone never touched. Reconstruction
|
|
341444
|
+
* via `applyPatchToContent` is required there.
|
|
341445
|
+
*/
|
|
341446
|
+
sharedFiles = /* @__PURE__ */ new Set();
|
|
341925
341447
|
constructor(git, lockManager, outputDir) {
|
|
341926
341448
|
this.git = git;
|
|
341927
341449
|
this.lockManager = lockManager;
|
|
@@ -341987,6 +341509,15 @@ var require_dist14 = __commonJS({
|
|
|
341987
341509
|
const applyMode = opts?.applyMode ?? "replay";
|
|
341988
341510
|
this.currentApplyMode = applyMode;
|
|
341989
341511
|
this.resetAccumulator();
|
|
341512
|
+
const fileFreq = /* @__PURE__ */ new Map();
|
|
341513
|
+
for (const p14 of patches) {
|
|
341514
|
+
for (const f5 of p14.files) {
|
|
341515
|
+
fileFreq.set(f5, (fileFreq.get(f5) ?? 0) + 1);
|
|
341516
|
+
}
|
|
341517
|
+
}
|
|
341518
|
+
this.sharedFiles = new Set(
|
|
341519
|
+
Array.from(fileFreq.entries()).filter(([, n19]) => n19 > 1).map(([f5]) => f5)
|
|
341520
|
+
);
|
|
341990
341521
|
const results = [];
|
|
341991
341522
|
for (let i9 = 0; i9 < patches.length; i9++) {
|
|
341992
341523
|
const patch5 = patches[i9];
|
|
@@ -342052,7 +341583,9 @@ var require_dist14 = __commonJS({
|
|
|
342052
341583
|
if (isBinaryFile(filePath)) continue;
|
|
342053
341584
|
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
342054
341585
|
const base4 = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
342055
|
-
const
|
|
341586
|
+
const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
|
|
341587
|
+
const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
|
|
341588
|
+
const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base4, patch5.patch_content, filePath, tempGit, tempDir2);
|
|
342056
341589
|
const finalOnDisk = await (0, import_promises193.readFile)((0, import_node_path210.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
342057
341590
|
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
342058
341591
|
if (effectiveTheirs != null) {
|
|
@@ -342214,15 +341747,27 @@ var require_dist14 = __commonJS({
|
|
|
342214
341747
|
}
|
|
342215
341748
|
}
|
|
342216
341749
|
}
|
|
341750
|
+
const snapshotForFile = patch5.theirs_snapshot?.[resolvedPath] ?? patch5.theirs_snapshot?.[filePath];
|
|
341751
|
+
const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
|
|
341752
|
+
const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
|
|
342217
341753
|
if (!ghostReconstructed) {
|
|
342218
|
-
|
|
342219
|
-
|
|
342220
|
-
|
|
342221
|
-
|
|
342222
|
-
|
|
342223
|
-
|
|
342224
|
-
|
|
342225
|
-
|
|
341754
|
+
if (useSnapshotAsPrimary) {
|
|
341755
|
+
theirs = snapshotForFile;
|
|
341756
|
+
} else {
|
|
341757
|
+
theirs = await this.applyPatchToContent(
|
|
341758
|
+
base4,
|
|
341759
|
+
patch5.patch_content,
|
|
341760
|
+
filePath,
|
|
341761
|
+
tempGit,
|
|
341762
|
+
tempDir2,
|
|
341763
|
+
renameSourcePath
|
|
341764
|
+
);
|
|
341765
|
+
const reconstructionHasMarkers = theirs != null && (theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization"));
|
|
341766
|
+
const baseHadMarkers = base4 != null && (base4.includes("<<<<<<< Generated") || base4.includes(">>>>>>> Your customization"));
|
|
341767
|
+
if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
|
|
341768
|
+
theirs = snapshotForFile;
|
|
341769
|
+
}
|
|
341770
|
+
}
|
|
342226
341771
|
}
|
|
342227
341772
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
342228
341773
|
if (theirs) {
|
|
@@ -342581,6 +342126,600 @@ var require_dist14 = __commonJS({
|
|
|
342581
342126
|
const match3 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
342582
342127
|
return match3 !== null && match3[1] === filePath;
|
|
342583
342128
|
}
|
|
342129
|
+
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
342130
|
+
function parsePatchFileHeaders(patchContent) {
|
|
342131
|
+
const results = [];
|
|
342132
|
+
let currentPath = null;
|
|
342133
|
+
let currentIsCreate = false;
|
|
342134
|
+
let currentIsDelete = false;
|
|
342135
|
+
const flush3 = () => {
|
|
342136
|
+
if (currentPath !== null) {
|
|
342137
|
+
results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
|
|
342138
|
+
}
|
|
342139
|
+
};
|
|
342140
|
+
for (const line of patchContent.split("\n")) {
|
|
342141
|
+
if (line.startsWith("diff --git ")) {
|
|
342142
|
+
flush3();
|
|
342143
|
+
currentPath = null;
|
|
342144
|
+
currentIsCreate = false;
|
|
342145
|
+
currentIsDelete = false;
|
|
342146
|
+
const match3 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
342147
|
+
if (match3) {
|
|
342148
|
+
currentPath = match3[2] ?? null;
|
|
342149
|
+
}
|
|
342150
|
+
} else if (currentPath !== null) {
|
|
342151
|
+
if (line.startsWith("new file mode ")) {
|
|
342152
|
+
currentIsCreate = true;
|
|
342153
|
+
} else if (line.startsWith("deleted file mode ")) {
|
|
342154
|
+
currentIsDelete = true;
|
|
342155
|
+
}
|
|
342156
|
+
}
|
|
342157
|
+
}
|
|
342158
|
+
flush3();
|
|
342159
|
+
return results;
|
|
342160
|
+
}
|
|
342161
|
+
var ReplayDetector = class {
|
|
342162
|
+
git;
|
|
342163
|
+
lockManager;
|
|
342164
|
+
sdkOutputDir;
|
|
342165
|
+
warnings = [];
|
|
342166
|
+
constructor(git, lockManager, sdkOutputDir) {
|
|
342167
|
+
this.git = git;
|
|
342168
|
+
this.lockManager = lockManager;
|
|
342169
|
+
this.sdkOutputDir = sdkOutputDir;
|
|
342170
|
+
}
|
|
342171
|
+
async detectNewPatches() {
|
|
342172
|
+
const lock = this.lockManager.read();
|
|
342173
|
+
const lastGen = this.getLastGeneration(lock);
|
|
342174
|
+
if (!lastGen) {
|
|
342175
|
+
return { patches: [], revertedPatchIds: [] };
|
|
342176
|
+
}
|
|
342177
|
+
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
342178
|
+
if (!exists2) {
|
|
342179
|
+
this.warnings.push(
|
|
342180
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
342181
|
+
);
|
|
342182
|
+
return this.detectPatchesViaTreeDiff(
|
|
342183
|
+
lastGen,
|
|
342184
|
+
/* commitKnownMissing */
|
|
342185
|
+
true
|
|
342186
|
+
);
|
|
342187
|
+
}
|
|
342188
|
+
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
342189
|
+
if (!isAncestor) {
|
|
342190
|
+
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
342191
|
+
}
|
|
342192
|
+
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
342193
|
+
}
|
|
342194
|
+
/**
|
|
342195
|
+
* FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
342196
|
+
*
|
|
342197
|
+
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
342198
|
+
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
342199
|
+
* merge-base is the commit on main from which the bot's branch was created —
|
|
342200
|
+
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
342201
|
+
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
342202
|
+
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
342203
|
+
* generation) get bundled as customer customizations.
|
|
342204
|
+
*
|
|
342205
|
+
* Falls through to the legacy composite path when no common ancestor exists
|
|
342206
|
+
* (truly disjoint history — orphan branches with no shared root).
|
|
342207
|
+
*/
|
|
342208
|
+
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
342209
|
+
let mergeBase = "";
|
|
342210
|
+
try {
|
|
342211
|
+
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
342212
|
+
} catch {
|
|
342213
|
+
}
|
|
342214
|
+
if (!mergeBase) {
|
|
342215
|
+
this.warnings.push(
|
|
342216
|
+
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
342217
|
+
);
|
|
342218
|
+
return this.detectPatchesViaTreeDiff(
|
|
342219
|
+
lastGen,
|
|
342220
|
+
/* commitKnownMissing */
|
|
342221
|
+
false
|
|
342222
|
+
);
|
|
342223
|
+
}
|
|
342224
|
+
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
342225
|
+
}
|
|
342226
|
+
/**
|
|
342227
|
+
* FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
342228
|
+
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
342229
|
+
* (rangeStart = lastGen.commit_sha) and the merge-base path.
|
|
342230
|
+
*
|
|
342231
|
+
* Patch `base_generation` is always `lastGen.commit_sha` — the
|
|
342232
|
+
* lockfile-tracked reference the applicator uses to fetch base file
|
|
342233
|
+
* content, regardless of where the walk actually started.
|
|
342234
|
+
*/
|
|
342235
|
+
async detectPatchesInRange(rangeStart, lastGen, lock) {
|
|
342236
|
+
const log4 = await this.git.exec([
|
|
342237
|
+
"log",
|
|
342238
|
+
"--first-parent",
|
|
342239
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
342240
|
+
`${rangeStart}..HEAD`,
|
|
342241
|
+
"--",
|
|
342242
|
+
this.sdkOutputDir
|
|
342243
|
+
]);
|
|
342244
|
+
if (!log4.trim()) {
|
|
342245
|
+
return { patches: [], revertedPatchIds: [] };
|
|
342246
|
+
}
|
|
342247
|
+
const commits = this.parseGitLog(log4);
|
|
342248
|
+
const newPatches = [];
|
|
342249
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
342250
|
+
for (const commit of commits) {
|
|
342251
|
+
if (isGenerationCommit(commit)) {
|
|
342252
|
+
continue;
|
|
342253
|
+
}
|
|
342254
|
+
if (lock.patches.find((p14) => p14.original_commit === commit.sha)) {
|
|
342255
|
+
continue;
|
|
342256
|
+
}
|
|
342257
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
342258
|
+
if (parents.length > 1) {
|
|
342259
|
+
const compositePatch = await this.createMergeCompositePatch({
|
|
342260
|
+
mergeCommit: commit,
|
|
342261
|
+
firstParent: parents[0],
|
|
342262
|
+
lastGen,
|
|
342263
|
+
lock
|
|
342264
|
+
});
|
|
342265
|
+
if (compositePatch) {
|
|
342266
|
+
newPatches.push(compositePatch);
|
|
342267
|
+
}
|
|
342268
|
+
continue;
|
|
342269
|
+
}
|
|
342270
|
+
let patchContent;
|
|
342271
|
+
try {
|
|
342272
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
342273
|
+
} catch {
|
|
342274
|
+
this.warnings.push(
|
|
342275
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
342276
|
+
);
|
|
342277
|
+
continue;
|
|
342278
|
+
}
|
|
342279
|
+
let contentHash = this.computeContentHash(patchContent);
|
|
342280
|
+
if (lock.patches.find((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
342281
|
+
continue;
|
|
342282
|
+
}
|
|
342283
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
342284
|
+
const allFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5)).filter((f5) => !f5.startsWith(".fern/"));
|
|
342285
|
+
const sensitiveFiles = allFiles.filter((f5) => isSensitiveFile(f5));
|
|
342286
|
+
const files = allFiles.filter((f5) => !isSensitiveFile(f5));
|
|
342287
|
+
if (sensitiveFiles.length > 0) {
|
|
342288
|
+
this.warnings.push(
|
|
342289
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
342290
|
+
);
|
|
342291
|
+
if (files.length > 0) {
|
|
342292
|
+
const parentSha = parents[0];
|
|
342293
|
+
if (parentSha) {
|
|
342294
|
+
try {
|
|
342295
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
342296
|
+
} catch {
|
|
342297
|
+
continue;
|
|
342298
|
+
}
|
|
342299
|
+
if (!patchContent.trim()) continue;
|
|
342300
|
+
contentHash = this.computeContentHash(patchContent);
|
|
342301
|
+
}
|
|
342302
|
+
}
|
|
342303
|
+
}
|
|
342304
|
+
if (files.length === 0) {
|
|
342305
|
+
continue;
|
|
342306
|
+
}
|
|
342307
|
+
const theirsSnapshot = {};
|
|
342308
|
+
for (const file4 of files) {
|
|
342309
|
+
if (isBinaryFile(file4)) continue;
|
|
342310
|
+
const content5 = await this.git.showFile(commit.sha, file4).catch(() => null);
|
|
342311
|
+
if (content5 != null) theirsSnapshot[file4] = content5;
|
|
342312
|
+
}
|
|
342313
|
+
newPatches.push({
|
|
342314
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
342315
|
+
content_hash: contentHash,
|
|
342316
|
+
original_commit: commit.sha,
|
|
342317
|
+
original_message: commit.message,
|
|
342318
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
342319
|
+
base_generation: lastGen.commit_sha,
|
|
342320
|
+
files,
|
|
342321
|
+
patch_content: patchContent,
|
|
342322
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
342323
|
+
...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
|
|
342324
|
+
});
|
|
342325
|
+
}
|
|
342326
|
+
const survivingPatches = await this.collapseNetZeroFiles(newPatches);
|
|
342327
|
+
survivingPatches.reverse();
|
|
342328
|
+
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
342329
|
+
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
342330
|
+
for (let i9 = 0; i9 < survivingPatches.length; i9++) {
|
|
342331
|
+
const patch5 = survivingPatches[i9];
|
|
342332
|
+
if (!isRevertCommit(patch5.original_message)) continue;
|
|
342333
|
+
let body = "";
|
|
342334
|
+
try {
|
|
342335
|
+
body = await this.git.getCommitBody(patch5.original_commit);
|
|
342336
|
+
} catch {
|
|
342337
|
+
}
|
|
342338
|
+
const revertedSha = parseRevertedSha(body);
|
|
342339
|
+
const revertedMessage = parseRevertedMessage(patch5.original_message);
|
|
342340
|
+
let matchedExisting = false;
|
|
342341
|
+
if (revertedSha) {
|
|
342342
|
+
const existing = lock.patches.find((p14) => p14.original_commit === revertedSha);
|
|
342343
|
+
if (existing) {
|
|
342344
|
+
revertedPatchIdSet.add(existing.id);
|
|
342345
|
+
revertIndicesToRemove.add(i9);
|
|
342346
|
+
matchedExisting = true;
|
|
342347
|
+
}
|
|
342348
|
+
}
|
|
342349
|
+
if (!matchedExisting && revertedMessage) {
|
|
342350
|
+
const existing = lock.patches.find((p14) => p14.original_message === revertedMessage);
|
|
342351
|
+
if (existing) {
|
|
342352
|
+
revertedPatchIdSet.add(existing.id);
|
|
342353
|
+
revertIndicesToRemove.add(i9);
|
|
342354
|
+
matchedExisting = true;
|
|
342355
|
+
}
|
|
342356
|
+
}
|
|
342357
|
+
if (matchedExisting) continue;
|
|
342358
|
+
let matchedNew = false;
|
|
342359
|
+
if (revertedSha) {
|
|
342360
|
+
const idx = survivingPatches.findIndex(
|
|
342361
|
+
(p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_commit === revertedSha
|
|
342362
|
+
);
|
|
342363
|
+
if (idx !== -1) {
|
|
342364
|
+
revertIndicesToRemove.add(i9);
|
|
342365
|
+
revertIndicesToRemove.add(idx);
|
|
342366
|
+
matchedNew = true;
|
|
342367
|
+
}
|
|
342368
|
+
}
|
|
342369
|
+
if (!matchedNew && revertedMessage) {
|
|
342370
|
+
const idx = survivingPatches.findIndex(
|
|
342371
|
+
(p14, j7) => j7 !== i9 && !revertIndicesToRemove.has(j7) && p14.original_message === revertedMessage
|
|
342372
|
+
);
|
|
342373
|
+
if (idx !== -1) {
|
|
342374
|
+
revertIndicesToRemove.add(i9);
|
|
342375
|
+
revertIndicesToRemove.add(idx);
|
|
342376
|
+
}
|
|
342377
|
+
}
|
|
342378
|
+
if (!matchedExisting && !matchedNew) {
|
|
342379
|
+
revertIndicesToRemove.add(i9);
|
|
342380
|
+
}
|
|
342381
|
+
}
|
|
342382
|
+
const filteredPatches = survivingPatches.filter((_7, i9) => !revertIndicesToRemove.has(i9));
|
|
342383
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
342384
|
+
}
|
|
342385
|
+
/**
|
|
342386
|
+
* Compute content hash for deduplication.
|
|
342387
|
+
*
|
|
342388
|
+
* Produces a format-agnostic hash so both `git format-patch` output
|
|
342389
|
+
* (email-wrapped) and plain `git diff` output hash to the same value
|
|
342390
|
+
* when the underlying diff hunks are identical.
|
|
342391
|
+
*
|
|
342392
|
+
* Normalization:
|
|
342393
|
+
* 1. Strip email wrapper: everything before the first `diff --git` line
|
|
342394
|
+
* (From:, Subject:, Date:, diffstat, blank separators).
|
|
342395
|
+
* 2. Strip `index` lines (blob SHA pairs change across rebases).
|
|
342396
|
+
* 3. Strip trailing git version marker (`-- \n<version>`).
|
|
342397
|
+
*
|
|
342398
|
+
* This ensures content hashes match across the no-patches and normal-
|
|
342399
|
+
* regeneration flows, which store formatPatch and git-diff formats
|
|
342400
|
+
* respectively (FER-9850, D5-D9).
|
|
342401
|
+
*/
|
|
342402
|
+
computeContentHash(patchContent) {
|
|
342403
|
+
const lines = patchContent.split("\n");
|
|
342404
|
+
const diffStart = lines.findIndex((l9) => l9.startsWith("diff --git "));
|
|
342405
|
+
const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
|
|
342406
|
+
const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
|
|
342407
|
+
return `sha256:${(0, import_node_crypto7.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
342408
|
+
}
|
|
342409
|
+
/**
|
|
342410
|
+
* FER-9805 — Drop patches whose files were transiently created and then
|
|
342411
|
+
* deleted within the current linear detection window, and are absent from
|
|
342412
|
+
* HEAD at the end of the window.
|
|
342413
|
+
*
|
|
342414
|
+
* Rationale: on a merge commit, createMergeCompositePatch already diffs
|
|
342415
|
+
* firstParent..mergeCommit so net-zero files never enter the composite. On
|
|
342416
|
+
* linear history each commit becomes its own patch, so a file that was
|
|
342417
|
+
* briefly added and later removed survives as a create+delete pair whose
|
|
342418
|
+
* cumulative diff is empty. We drop such patches before they reach the
|
|
342419
|
+
* lockfile.
|
|
342420
|
+
*
|
|
342421
|
+
* A file is "transient" when:
|
|
342422
|
+
* 1. some patch in the window sets `new file mode` for it (a creation), AND
|
|
342423
|
+
* 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
|
|
342424
|
+
* 3. it is absent from HEAD's tree.
|
|
342425
|
+
*
|
|
342426
|
+
* The HEAD guard (#3) prevents false positives when a file is deleted
|
|
342427
|
+
* and then recreated with different content — the cumulative diff of
|
|
342428
|
+
* such a pair is non-empty and must be preserved.
|
|
342429
|
+
*
|
|
342430
|
+
* This only drops patches whose `files` are a subset of the transient
|
|
342431
|
+
* set. A partial-transient patch (one that touches a transient file
|
|
342432
|
+
* alongside a persistent one) is left unmodified; surgical stripping of
|
|
342433
|
+
* single files from a multi-file patch would require a follow-up that
|
|
342434
|
+
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
342435
|
+
* current failing test exercises this, and leaving the patch intact
|
|
342436
|
+
* preserves today's behaviour. TODO(FER-9805): revisit if a future
|
|
342437
|
+
* adversarial test demands per-file stripping.
|
|
342438
|
+
*/
|
|
342439
|
+
async collapseNetZeroFiles(patches) {
|
|
342440
|
+
if (patches.length === 0) return patches;
|
|
342441
|
+
const stateByFile = /* @__PURE__ */ new Map();
|
|
342442
|
+
for (const patch5 of patches) {
|
|
342443
|
+
for (const header of parsePatchFileHeaders(patch5.patch_content)) {
|
|
342444
|
+
if (!header.isCreate && !header.isDelete) continue;
|
|
342445
|
+
const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
|
|
342446
|
+
if (header.isCreate) prior.created = true;
|
|
342447
|
+
if (header.isDelete) prior.deleted = true;
|
|
342448
|
+
stateByFile.set(header.path, prior);
|
|
342449
|
+
}
|
|
342450
|
+
}
|
|
342451
|
+
let anyCandidate = false;
|
|
342452
|
+
for (const state of stateByFile.values()) {
|
|
342453
|
+
if (state.created && state.deleted) {
|
|
342454
|
+
anyCandidate = true;
|
|
342455
|
+
break;
|
|
342456
|
+
}
|
|
342457
|
+
}
|
|
342458
|
+
if (!anyCandidate) return patches;
|
|
342459
|
+
const headFiles = await this.listHeadFiles();
|
|
342460
|
+
const transient = /* @__PURE__ */ new Set();
|
|
342461
|
+
for (const [file4, state] of stateByFile) {
|
|
342462
|
+
if (state.created && state.deleted && !headFiles.has(file4)) {
|
|
342463
|
+
transient.add(file4);
|
|
342464
|
+
}
|
|
342465
|
+
}
|
|
342466
|
+
if (transient.size === 0) return patches;
|
|
342467
|
+
return patches.filter((patch5) => !patch5.files.every((f5) => transient.has(f5)));
|
|
342468
|
+
}
|
|
342469
|
+
/**
|
|
342470
|
+
* List every tracked path at HEAD (one `git ls-tree -r` invocation).
|
|
342471
|
+
* Returns an empty Set if HEAD is unborn or the invocation fails — the
|
|
342472
|
+
* caller then treats every candidate as "not in HEAD" and may collapse
|
|
342473
|
+
* more aggressively. On typical SDK repos this is a single sub-10ms call.
|
|
342474
|
+
*/
|
|
342475
|
+
async listHeadFiles() {
|
|
342476
|
+
try {
|
|
342477
|
+
const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
|
|
342478
|
+
return new Set(out.split("\n").map((l9) => l9.trim()).filter(Boolean));
|
|
342479
|
+
} catch {
|
|
342480
|
+
return /* @__PURE__ */ new Set();
|
|
342481
|
+
}
|
|
342482
|
+
}
|
|
342483
|
+
/**
|
|
342484
|
+
* Create a single composite patch for a merge commit by diffing the merge
|
|
342485
|
+
* commit against its first parent. This captures the net change of the
|
|
342486
|
+
* entire merged branch as one patch, eliminating accumulator chaining and
|
|
342487
|
+
* zero-net-change ghost conflicts.
|
|
342488
|
+
*/
|
|
342489
|
+
async createMergeCompositePatch(input3) {
|
|
342490
|
+
const { mergeCommit, firstParent, lastGen, lock } = input3;
|
|
342491
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
342492
|
+
const filesOutput = await this.git.exec([
|
|
342493
|
+
"diff",
|
|
342494
|
+
"--name-only",
|
|
342495
|
+
firstParent,
|
|
342496
|
+
mergeCommit.sha
|
|
342497
|
+
]);
|
|
342498
|
+
const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5)).filter((f5) => !f5.startsWith(".fern/"));
|
|
342499
|
+
const sensitiveMergeFiles = allMergeFiles.filter((f5) => isSensitiveFile(f5));
|
|
342500
|
+
const files = allMergeFiles.filter((f5) => !isSensitiveFile(f5));
|
|
342501
|
+
if (sensitiveMergeFiles.length > 0) {
|
|
342502
|
+
this.warnings.push(
|
|
342503
|
+
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
342504
|
+
);
|
|
342505
|
+
}
|
|
342506
|
+
if (files.length === 0) return null;
|
|
342507
|
+
const diff2 = await this.git.exec([
|
|
342508
|
+
"diff",
|
|
342509
|
+
firstParent,
|
|
342510
|
+
mergeCommit.sha,
|
|
342511
|
+
"--",
|
|
342512
|
+
...files
|
|
342513
|
+
]);
|
|
342514
|
+
if (!diff2.trim()) return null;
|
|
342515
|
+
const contentHash = this.computeContentHash(diff2);
|
|
342516
|
+
if (lock.patches.some((p14) => p14.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
342517
|
+
return null;
|
|
342518
|
+
}
|
|
342519
|
+
const theirsSnapshot = {};
|
|
342520
|
+
for (const file4 of files) {
|
|
342521
|
+
if (isBinaryFile(file4)) continue;
|
|
342522
|
+
const content5 = await this.git.showFile(mergeCommit.sha, file4).catch(() => null);
|
|
342523
|
+
if (content5 != null) theirsSnapshot[file4] = content5;
|
|
342524
|
+
}
|
|
342525
|
+
return {
|
|
342526
|
+
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
342527
|
+
content_hash: contentHash,
|
|
342528
|
+
original_commit: mergeCommit.sha,
|
|
342529
|
+
original_message: mergeCommit.message,
|
|
342530
|
+
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
342531
|
+
base_generation: lastGen.commit_sha,
|
|
342532
|
+
files,
|
|
342533
|
+
patch_content: diff2,
|
|
342534
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
342535
|
+
...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
|
|
342536
|
+
};
|
|
342537
|
+
}
|
|
342538
|
+
/**
|
|
342539
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
342540
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
342541
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
342542
|
+
*/
|
|
342543
|
+
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
342544
|
+
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
342545
|
+
if (!diffBase) {
|
|
342546
|
+
return this.detectPatchesViaCommitScan();
|
|
342547
|
+
}
|
|
342548
|
+
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
342549
|
+
const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5)).filter((f5) => !f5.startsWith(".fern/"));
|
|
342550
|
+
const sensitiveTreeFiles = allTreeFiles.filter((f5) => isSensitiveFile(f5));
|
|
342551
|
+
const files = allTreeFiles.filter((f5) => !isSensitiveFile(f5));
|
|
342552
|
+
if (sensitiveTreeFiles.length > 0) {
|
|
342553
|
+
this.warnings.push(
|
|
342554
|
+
`Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
342555
|
+
);
|
|
342556
|
+
}
|
|
342557
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
342558
|
+
const diff2 = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
342559
|
+
if (!diff2.trim()) return { patches: [], revertedPatchIds: [] };
|
|
342560
|
+
const contentHash = this.computeContentHash(diff2);
|
|
342561
|
+
const lock = this.lockManager.read();
|
|
342562
|
+
if (lock.patches.some((p14) => p14.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
342563
|
+
return { patches: [], revertedPatchIds: [] };
|
|
342564
|
+
}
|
|
342565
|
+
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
342566
|
+
const theirsSnapshot = {};
|
|
342567
|
+
for (const file4 of files) {
|
|
342568
|
+
if (isBinaryFile(file4)) continue;
|
|
342569
|
+
const content5 = await this.git.showFile("HEAD", file4).catch(() => null);
|
|
342570
|
+
if (content5 != null) theirsSnapshot[file4] = content5;
|
|
342571
|
+
}
|
|
342572
|
+
const compositePatch = {
|
|
342573
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
342574
|
+
content_hash: contentHash,
|
|
342575
|
+
original_commit: headSha,
|
|
342576
|
+
original_message: "Customer customizations (composite)",
|
|
342577
|
+
original_author: "composite",
|
|
342578
|
+
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
342579
|
+
// reference to find base file content. diffBase may be the tree_hash.
|
|
342580
|
+
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
342581
|
+
files,
|
|
342582
|
+
patch_content: diff2,
|
|
342583
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
342584
|
+
...this.isCreationOnlyPatch(diff2) ? { user_owned: true } : {}
|
|
342585
|
+
};
|
|
342586
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
342587
|
+
}
|
|
342588
|
+
/**
|
|
342589
|
+
* Last-resort detection when both generation commit and tree are unreachable.
|
|
342590
|
+
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
342591
|
+
* skips creation-only commits (squashed history after force push).
|
|
342592
|
+
*
|
|
342593
|
+
* Detected patches use the commit's parent as base_generation so the applicator
|
|
342594
|
+
* can find base file content from the parent's tree (which IS reachable).
|
|
342595
|
+
*/
|
|
342596
|
+
async detectPatchesViaCommitScan() {
|
|
342597
|
+
const lock = this.lockManager.read();
|
|
342598
|
+
const log4 = await this.git.exec([
|
|
342599
|
+
"log",
|
|
342600
|
+
"--max-count=200",
|
|
342601
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
342602
|
+
"HEAD",
|
|
342603
|
+
"--",
|
|
342604
|
+
this.sdkOutputDir
|
|
342605
|
+
]);
|
|
342606
|
+
if (!log4.trim()) {
|
|
342607
|
+
return { patches: [], revertedPatchIds: [] };
|
|
342608
|
+
}
|
|
342609
|
+
const commits = this.parseGitLog(log4);
|
|
342610
|
+
const newPatches = [];
|
|
342611
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
342612
|
+
const existingHashes = new Set(lock.patches.map((p14) => p14.content_hash));
|
|
342613
|
+
const existingCommits = new Set(lock.patches.map((p14) => p14.original_commit));
|
|
342614
|
+
for (const commit of commits) {
|
|
342615
|
+
if (isGenerationCommit(commit)) {
|
|
342616
|
+
continue;
|
|
342617
|
+
}
|
|
342618
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
342619
|
+
if (parents.length > 1) {
|
|
342620
|
+
continue;
|
|
342621
|
+
}
|
|
342622
|
+
if (existingCommits.has(commit.sha)) {
|
|
342623
|
+
continue;
|
|
342624
|
+
}
|
|
342625
|
+
let patchContent;
|
|
342626
|
+
try {
|
|
342627
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
342628
|
+
} catch {
|
|
342629
|
+
continue;
|
|
342630
|
+
}
|
|
342631
|
+
let contentHash = this.computeContentHash(patchContent);
|
|
342632
|
+
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
342633
|
+
continue;
|
|
342634
|
+
}
|
|
342635
|
+
if (this.isCreationOnlyPatch(patchContent)) {
|
|
342636
|
+
continue;
|
|
342637
|
+
}
|
|
342638
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
342639
|
+
const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f5) => !INFRASTRUCTURE_FILES.has(f5));
|
|
342640
|
+
const sensitiveScanFiles = allScanFiles.filter((f5) => isSensitiveFile(f5));
|
|
342641
|
+
const files = allScanFiles.filter((f5) => !isSensitiveFile(f5));
|
|
342642
|
+
if (sensitiveScanFiles.length > 0) {
|
|
342643
|
+
this.warnings.push(
|
|
342644
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
|
|
342645
|
+
);
|
|
342646
|
+
if (files.length > 0) {
|
|
342647
|
+
if (parents.length === 0) {
|
|
342648
|
+
continue;
|
|
342649
|
+
}
|
|
342650
|
+
try {
|
|
342651
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
342652
|
+
} catch {
|
|
342653
|
+
continue;
|
|
342654
|
+
}
|
|
342655
|
+
if (!patchContent.trim()) continue;
|
|
342656
|
+
contentHash = this.computeContentHash(patchContent);
|
|
342657
|
+
}
|
|
342658
|
+
}
|
|
342659
|
+
if (files.length === 0) {
|
|
342660
|
+
continue;
|
|
342661
|
+
}
|
|
342662
|
+
if (parents.length === 0) {
|
|
342663
|
+
continue;
|
|
342664
|
+
}
|
|
342665
|
+
const parentSha = parents[0];
|
|
342666
|
+
const theirsSnapshot = {};
|
|
342667
|
+
for (const file4 of files) {
|
|
342668
|
+
if (isBinaryFile(file4)) continue;
|
|
342669
|
+
const content5 = await this.git.showFile(commit.sha, file4).catch(() => null);
|
|
342670
|
+
if (content5 != null) theirsSnapshot[file4] = content5;
|
|
342671
|
+
}
|
|
342672
|
+
newPatches.push({
|
|
342673
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
342674
|
+
content_hash: contentHash,
|
|
342675
|
+
original_commit: commit.sha,
|
|
342676
|
+
original_message: commit.message,
|
|
342677
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
342678
|
+
base_generation: parentSha,
|
|
342679
|
+
files,
|
|
342680
|
+
patch_content: patchContent,
|
|
342681
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
342682
|
+
});
|
|
342683
|
+
}
|
|
342684
|
+
newPatches.reverse();
|
|
342685
|
+
return { patches: newPatches, revertedPatchIds: [] };
|
|
342686
|
+
}
|
|
342687
|
+
/**
|
|
342688
|
+
* Check if a format-patch consists entirely of new-file creations.
|
|
342689
|
+
* Used to identify squashed commits after force push, which create all files
|
|
342690
|
+
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
342691
|
+
*/
|
|
342692
|
+
isCreationOnlyPatch(patchContent) {
|
|
342693
|
+
const diffOldHeaders = patchContent.split("\n").filter((l9) => l9.startsWith("--- "));
|
|
342694
|
+
if (diffOldHeaders.length === 0) {
|
|
342695
|
+
return false;
|
|
342696
|
+
}
|
|
342697
|
+
return diffOldHeaders.every((l9) => l9 === "--- /dev/null");
|
|
342698
|
+
}
|
|
342699
|
+
/**
|
|
342700
|
+
* Resolve the best available diff base for a generation record.
|
|
342701
|
+
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
342702
|
+
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
342703
|
+
*/
|
|
342704
|
+
async resolveDiffBase(gen, commitKnownMissing) {
|
|
342705
|
+
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
342706
|
+
return gen.commit_sha;
|
|
342707
|
+
}
|
|
342708
|
+
if (await this.git.treeExists(gen.tree_hash)) {
|
|
342709
|
+
return gen.tree_hash;
|
|
342710
|
+
}
|
|
342711
|
+
return null;
|
|
342712
|
+
}
|
|
342713
|
+
parseGitLog(log4) {
|
|
342714
|
+
return log4.trim().split("\n").map((line) => {
|
|
342715
|
+
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
342716
|
+
return { sha, authorName, authorEmail, message };
|
|
342717
|
+
});
|
|
342718
|
+
}
|
|
342719
|
+
getLastGeneration(lock) {
|
|
342720
|
+
return lock.generations.find((g8) => g8.commit_sha === lock.current_generation);
|
|
342721
|
+
}
|
|
342722
|
+
};
|
|
342584
342723
|
var ReplayCommitter = class {
|
|
342585
342724
|
git;
|
|
342586
342725
|
outputDir;
|
|
@@ -342683,7 +342822,8 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
342683
342822
|
}
|
|
342684
342823
|
};
|
|
342685
342824
|
var import_node_fs210 = require("fs");
|
|
342686
|
-
var
|
|
342825
|
+
var import_promises410 = require("fs/promises");
|
|
342826
|
+
var import_node_path52 = require("path");
|
|
342687
342827
|
var import_minimatch22 = require_commonjs3();
|
|
342688
342828
|
init_GitClient();
|
|
342689
342829
|
var import_promises210 = require("fs/promises");
|
|
@@ -342740,13 +342880,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
342740
342880
|
const hunksRaw = parseHunks(fileDiff).map(normalizeHunkBody);
|
|
342741
342881
|
const hunks = hunksRaw.filter((h4) => {
|
|
342742
342882
|
if (h4.lines.length === 0) return false;
|
|
342743
|
-
let ctx = 0,
|
|
342883
|
+
let ctx = 0, rm42 = 0, add = 0;
|
|
342744
342884
|
for (const ln10 of h4.lines) {
|
|
342745
342885
|
if (ln10.type === "context") ctx++;
|
|
342746
|
-
else if (ln10.type === "remove")
|
|
342886
|
+
else if (ln10.type === "remove") rm42++;
|
|
342747
342887
|
else if (ln10.type === "add") add++;
|
|
342748
342888
|
}
|
|
342749
|
-
return ctx +
|
|
342889
|
+
return ctx + rm42 === h4.oldCount && ctx + add === h4.newCount;
|
|
342750
342890
|
});
|
|
342751
342891
|
if (hunks.length === 0) {
|
|
342752
342892
|
if (hunksRaw.length > 0) unlocatableFiles.push(file4);
|
|
@@ -343454,19 +343594,14 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343454
343594
|
existingPatches = this.lockManager.getPatches();
|
|
343455
343595
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
343456
343596
|
for (const p14 of existingPatches) {
|
|
343457
|
-
const
|
|
343458
|
-
if (
|
|
343459
|
-
|
|
343460
|
-
|
|
343461
|
-
|
|
343462
|
-
|
|
343463
|
-
} else {
|
|
343464
|
-
this.warnings.push(
|
|
343465
|
-
`Preserving patch ${p14.id} (commit ${(p14.original_commit || "<missing>").slice(0, 7)}): shares content_hash with a prior patch ${provenanceMissing ? "(provenance missing on at least one side)" : "from a different source commit"}. Both retained to avoid silent customization loss (FER-9983).`
|
|
343466
|
-
);
|
|
343467
|
-
}
|
|
343597
|
+
const priorPatchId = seenHashes.get(p14.content_hash);
|
|
343598
|
+
if (priorPatchId !== void 0) {
|
|
343599
|
+
this.lockManager.removePatch(p14.id);
|
|
343600
|
+
this.warnings.push(
|
|
343601
|
+
`Consolidated patch ${p14.id} (commit ${(p14.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
343602
|
+
);
|
|
343468
343603
|
} else {
|
|
343469
|
-
seenHashes.set(p14.content_hash, p14.
|
|
343604
|
+
seenHashes.set(p14.content_hash, p14.id);
|
|
343470
343605
|
}
|
|
343471
343606
|
}
|
|
343472
343607
|
existingPatches = this.lockManager.getPatches();
|
|
@@ -343647,17 +343782,27 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343647
343782
|
if (allUserOwned && patch5.files.length > 0) {
|
|
343648
343783
|
const baseChanged = patch5.base_generation !== currentGenSha;
|
|
343649
343784
|
const userDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch5.files]).catch(() => null);
|
|
343785
|
+
let userDiffSnapshot = null;
|
|
343650
343786
|
if (userDiff != null && userDiff.trim()) {
|
|
343651
343787
|
const newHash = this.detector.computeContentHash(userDiff);
|
|
343652
343788
|
patch5.patch_content = userDiff;
|
|
343653
343789
|
patch5.content_hash = newHash;
|
|
343790
|
+
const snap = await this.readSnapshotFromDisk(patch5.files);
|
|
343791
|
+
if (Object.keys(snap).length > 0) {
|
|
343792
|
+
userDiffSnapshot = snap;
|
|
343793
|
+
patch5.theirs_snapshot = snap;
|
|
343794
|
+
}
|
|
343654
343795
|
}
|
|
343655
343796
|
patch5.base_generation = currentGenSha;
|
|
343656
343797
|
try {
|
|
343657
343798
|
this.lockManager.updatePatch(patch5.id, {
|
|
343658
343799
|
base_generation: currentGenSha,
|
|
343659
343800
|
...!patch5.user_owned ? { user_owned: true } : {},
|
|
343660
|
-
...userDiff != null && userDiff.trim() ? {
|
|
343801
|
+
...userDiff != null && userDiff.trim() ? {
|
|
343802
|
+
patch_content: patch5.patch_content,
|
|
343803
|
+
content_hash: patch5.content_hash,
|
|
343804
|
+
...userDiffSnapshot != null && Object.keys(userDiffSnapshot).length > 0 ? { theirs_snapshot: userDiffSnapshot } : {}
|
|
343805
|
+
} : {}
|
|
343661
343806
|
});
|
|
343662
343807
|
} catch {
|
|
343663
343808
|
}
|
|
@@ -343705,30 +343850,34 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343705
343850
|
continue;
|
|
343706
343851
|
}
|
|
343707
343852
|
const newContentHash = this.detector.computeContentHash(diff2);
|
|
343708
|
-
const
|
|
343709
|
-
if (
|
|
343710
|
-
|
|
343711
|
-
|
|
343712
|
-
|
|
343713
|
-
|
|
343714
|
-
|
|
343715
|
-
|
|
343716
|
-
|
|
343853
|
+
const priorPatchId = seenContentHashes.get(newContentHash);
|
|
343854
|
+
if (priorPatchId !== void 0) {
|
|
343855
|
+
this.lockManager.removePatch(patch5.id);
|
|
343856
|
+
absorbedPatchIds.add(patch5.id);
|
|
343857
|
+
absorbed++;
|
|
343858
|
+
if (priorPatchId !== patch5.id) {
|
|
343859
|
+
this.warnings.push(
|
|
343860
|
+
`Consolidated patch ${patch5.id} (commit ${(patch5.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content after rebase. Per-commit attribution preserved in git log.`
|
|
343861
|
+
);
|
|
343717
343862
|
}
|
|
343718
|
-
|
|
343719
|
-
`Preserving patch ${patch5.id} (commit ${(patch5.original_commit || "<missing>").slice(0, 7)}): shares content_hash with a prior patch ${provenanceMissing ? "(provenance missing on at least one side)" : "from a different source commit"}. Both retained to avoid silent customization loss (FER-9983).`
|
|
343720
|
-
);
|
|
343863
|
+
continue;
|
|
343721
343864
|
} else {
|
|
343722
|
-
seenContentHashes.set(newContentHash, patch5.
|
|
343865
|
+
seenContentHashes.set(newContentHash, patch5.id);
|
|
343723
343866
|
}
|
|
343724
343867
|
patch5.base_generation = currentGenSha;
|
|
343725
343868
|
patch5.patch_content = diff2;
|
|
343726
343869
|
patch5.content_hash = newContentHash;
|
|
343870
|
+
const snapshot = await this.readSnapshotFromDisk(patch5.files);
|
|
343871
|
+
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
343872
|
+
if (snapshotIsNonEmpty) {
|
|
343873
|
+
patch5.theirs_snapshot = snapshot;
|
|
343874
|
+
}
|
|
343727
343875
|
try {
|
|
343728
343876
|
this.lockManager.updatePatch(patch5.id, {
|
|
343729
343877
|
base_generation: currentGenSha,
|
|
343730
343878
|
patch_content: diff2,
|
|
343731
|
-
content_hash: newContentHash
|
|
343879
|
+
content_hash: newContentHash,
|
|
343880
|
+
...snapshotIsNonEmpty ? { theirs_snapshot: snapshot } : {}
|
|
343732
343881
|
});
|
|
343733
343882
|
} catch {
|
|
343734
343883
|
}
|
|
@@ -343738,6 +343887,112 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343738
343887
|
}
|
|
343739
343888
|
return { absorbed, repointed, contentRebased, keptAsUserOwned, absorbedPatchIds };
|
|
343740
343889
|
}
|
|
343890
|
+
/**
|
|
343891
|
+
* Read THEIRS snapshot (post-customer-edit content) for a list of files
|
|
343892
|
+
* from the working tree on disk. Used by `rebasePatches` after a clean
|
|
343893
|
+
* apply — disk = correctly-merged customer state at the new generation.
|
|
343894
|
+
* Skips binary files (matches `mergeFile`/`populateAccumulatorForPatch`
|
|
343895
|
+
* policy — binary content as `utf-8` is lossy and snapshots of it would
|
|
343896
|
+
* never be used during apply anyway).
|
|
343897
|
+
*/
|
|
343898
|
+
async readSnapshotFromDisk(files) {
|
|
343899
|
+
const snapshot = {};
|
|
343900
|
+
for (const file4 of files) {
|
|
343901
|
+
if (isBinaryFile(file4)) continue;
|
|
343902
|
+
try {
|
|
343903
|
+
const content5 = await (0, import_promises410.readFile)((0, import_node_path52.join)(this.outputDir, file4), "utf-8");
|
|
343904
|
+
snapshot[file4] = content5;
|
|
343905
|
+
} catch {
|
|
343906
|
+
}
|
|
343907
|
+
}
|
|
343908
|
+
return snapshot;
|
|
343909
|
+
}
|
|
343910
|
+
/**
|
|
343911
|
+
* Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
|
|
343912
|
+
* Used when the diff that produced `patch_content` was relative to that
|
|
343913
|
+
* tree (e.g., `git diff currentGen HEAD --` → snapshot from HEAD).
|
|
343914
|
+
* Skips binary files.
|
|
343915
|
+
*/
|
|
343916
|
+
async readSnapshotFromTree(treeRef, files) {
|
|
343917
|
+
const snapshot = {};
|
|
343918
|
+
for (const file4 of files) {
|
|
343919
|
+
if (isBinaryFile(file4)) continue;
|
|
343920
|
+
const content5 = await this.git.showFile(treeRef, file4).catch(() => null);
|
|
343921
|
+
if (content5 != null) snapshot[file4] = content5;
|
|
343922
|
+
}
|
|
343923
|
+
return snapshot;
|
|
343924
|
+
}
|
|
343925
|
+
/**
|
|
343926
|
+
* One-shot auto-migration: backfill `theirs_snapshot` on existing
|
|
343927
|
+
* patches that predate the snapshot encoding. Computes the snapshot by
|
|
343928
|
+
* applying the patch's `patch_content` to its `base_generation` tree —
|
|
343929
|
+
* yielding the patch's INDIVIDUAL contribution.
|
|
343930
|
+
*
|
|
343931
|
+
* **Skips patches sharing files with other patches.** HEAD's content
|
|
343932
|
+
* for a file shared by N patches is cumulative across all of them, so
|
|
343933
|
+
* a HEAD-fallback would falsely encode other patches' contributions
|
|
343934
|
+
* into one patch's snapshot — breaking FER-9525 cross-patch isolation
|
|
343935
|
+
* if the snapshot fallback later fires. Per-patch snapshots only
|
|
343936
|
+
* make sense when the patch owns its file. Multi-patch-same-file
|
|
343937
|
+
* legacy lockfiles keep using line-anchored reconstruction (the
|
|
343938
|
+
* existing path); they're no worse than pre-upgrade.
|
|
343939
|
+
*
|
|
343940
|
+
* **Does not fall back to HEAD when BASE-reconstruction fails.** Same
|
|
343941
|
+
* reason — HEAD content can be cumulative. Patches whose
|
|
343942
|
+
* reconstruction fails just don't get a snapshot; legacy paths
|
|
343943
|
+
* (PR #73's by-association gate, accumulator fallback) handle them.
|
|
343944
|
+
*
|
|
343945
|
+
* Skips patches that already have a snapshot.
|
|
343946
|
+
*/
|
|
343947
|
+
async migratePatchesToSnapshot(patches) {
|
|
343948
|
+
const { applyPatchToContent: applyPatchToContent2 } = await Promise.resolve().then(() => (init_PatchApplyTheirs(), PatchApplyTheirs_exports));
|
|
343949
|
+
const fileFreq = /* @__PURE__ */ new Map();
|
|
343950
|
+
for (const p14 of patches) {
|
|
343951
|
+
for (const f5 of p14.files) {
|
|
343952
|
+
fileFreq.set(f5, (fileFreq.get(f5) ?? 0) + 1);
|
|
343953
|
+
}
|
|
343954
|
+
}
|
|
343955
|
+
let migrated = 0;
|
|
343956
|
+
let skipped = 0;
|
|
343957
|
+
for (const patch5 of patches) {
|
|
343958
|
+
if (patch5.theirs_snapshot != null) continue;
|
|
343959
|
+
const sharesAnyFile = patch5.files.some((f5) => (fileFreq.get(f5) ?? 0) > 1);
|
|
343960
|
+
if (sharesAnyFile) {
|
|
343961
|
+
skipped++;
|
|
343962
|
+
continue;
|
|
343963
|
+
}
|
|
343964
|
+
const snapshot = {};
|
|
343965
|
+
for (const file4 of patch5.files) {
|
|
343966
|
+
if (isBinaryFile(file4)) continue;
|
|
343967
|
+
try {
|
|
343968
|
+
const base4 = await this.git.showFile(patch5.base_generation, file4).catch(() => null);
|
|
343969
|
+
if (base4 != null) {
|
|
343970
|
+
const content5 = await applyPatchToContent2(base4, patch5.patch_content, file4);
|
|
343971
|
+
if (content5 != null) snapshot[file4] = content5;
|
|
343972
|
+
}
|
|
343973
|
+
} catch {
|
|
343974
|
+
}
|
|
343975
|
+
}
|
|
343976
|
+
if (Object.keys(snapshot).length > 0) {
|
|
343977
|
+
patch5.theirs_snapshot = snapshot;
|
|
343978
|
+
try {
|
|
343979
|
+
this.lockManager.updatePatch(patch5.id, { theirs_snapshot: snapshot });
|
|
343980
|
+
migrated++;
|
|
343981
|
+
} catch {
|
|
343982
|
+
}
|
|
343983
|
+
}
|
|
343984
|
+
}
|
|
343985
|
+
if (migrated > 0) {
|
|
343986
|
+
this.warnings.push(
|
|
343987
|
+
`Migrated ${migrated} patch(es) to snapshot encoding. Future regens use 3-way merge with stored THEIRS, robust to generator structural changes.`
|
|
343988
|
+
);
|
|
343989
|
+
}
|
|
343990
|
+
if (skipped > 0) {
|
|
343991
|
+
this.warnings.push(
|
|
343992
|
+
`Skipped snapshot migration for ${skipped} patch(es) sharing files with other patches. These continue to use line-anchored reconstruction (the existing path).`
|
|
343993
|
+
);
|
|
343994
|
+
}
|
|
343995
|
+
}
|
|
343741
343996
|
/**
|
|
343742
343997
|
* Determine whether `file` is user-owned (never produced by the generator).
|
|
343743
343998
|
*
|
|
@@ -343858,6 +344113,20 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343858
344113
|
this.lockManager.clearReplaySkippedAt();
|
|
343859
344114
|
return { conflictResolved: 0, conflictAbsorbed: 0, contentRefreshed: 0 };
|
|
343860
344115
|
}
|
|
344116
|
+
await this.migratePatchesToSnapshot(patches);
|
|
344117
|
+
const headMessage = await this.git.exec(["log", "-1", "--format=%s%n%aN%n%ae", "HEAD"]).catch(() => "");
|
|
344118
|
+
const [headMsgLine, headAuthorName, headAuthorEmail] = headMessage.split("\n");
|
|
344119
|
+
const headIsGeneration = headMsgLine ? isGenerationCommit({
|
|
344120
|
+
sha: "HEAD",
|
|
344121
|
+
authorName: headAuthorName ?? "",
|
|
344122
|
+
authorEmail: headAuthorEmail ?? "",
|
|
344123
|
+
message: headMsgLine
|
|
344124
|
+
}) : false;
|
|
344125
|
+
const headParentChangedFiles = async (patchFiles) => {
|
|
344126
|
+
if (patchFiles.length === 0) return /* @__PURE__ */ new Set();
|
|
344127
|
+
const diff2 = await this.git.exec(["diff", "--name-only", "HEAD~1", "HEAD", "--", ...patchFiles]).catch(() => "");
|
|
344128
|
+
return new Set(diff2.trim().split("\n").filter(Boolean));
|
|
344129
|
+
};
|
|
343861
344130
|
let conflictResolved = 0;
|
|
343862
344131
|
let conflictAbsorbed = 0;
|
|
343863
344132
|
let contentRefreshed = 0;
|
|
@@ -343894,6 +344163,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343894
344163
|
delete patch5.status;
|
|
343895
344164
|
continue;
|
|
343896
344165
|
}
|
|
344166
|
+
if (headIsGeneration) {
|
|
344167
|
+
const parentChangedFiles = await headParentChangedFiles(patch5.files);
|
|
344168
|
+
const headTouchedSource = patch5.files.some((f5) => parentChangedFiles.has(f5));
|
|
344169
|
+
if (headTouchedSource) {
|
|
344170
|
+
continue;
|
|
344171
|
+
}
|
|
344172
|
+
}
|
|
343897
344173
|
if (patch5.base_generation === currentGen) {
|
|
343898
344174
|
try {
|
|
343899
344175
|
const ppResult = await computePerPatchDiffWithFallback(
|
|
@@ -343930,10 +344206,12 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343930
344206
|
if (newFiles.length === 0) {
|
|
343931
344207
|
this.lockManager.removePatch(patch5.id);
|
|
343932
344208
|
} else {
|
|
344209
|
+
const snapshot = await this.readSnapshotFromTree("HEAD", newFiles);
|
|
343933
344210
|
this.lockManager.updatePatch(patch5.id, {
|
|
343934
344211
|
patch_content: diff2,
|
|
343935
344212
|
content_hash: newContentHash,
|
|
343936
|
-
files: newFiles
|
|
344213
|
+
files: newFiles,
|
|
344214
|
+
...Object.keys(snapshot).length > 0 ? { theirs_snapshot: snapshot } : {}
|
|
343937
344215
|
});
|
|
343938
344216
|
}
|
|
343939
344217
|
contentRefreshed++;
|
|
@@ -343966,10 +344244,12 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
343966
344244
|
continue;
|
|
343967
344245
|
}
|
|
343968
344246
|
const newContentHash = this.detector.computeContentHash(diff2);
|
|
344247
|
+
const snapshot = await this.readSnapshotFromTree("HEAD", patch5.files);
|
|
343969
344248
|
this.lockManager.updatePatch(patch5.id, {
|
|
343970
344249
|
base_generation: currentGen,
|
|
343971
344250
|
patch_content: diff2,
|
|
343972
|
-
content_hash: newContentHash
|
|
344251
|
+
content_hash: newContentHash,
|
|
344252
|
+
...Object.keys(snapshot).length > 0 ? { theirs_snapshot: snapshot } : {}
|
|
343973
344253
|
});
|
|
343974
344254
|
conflictResolved++;
|
|
343975
344255
|
} catch {
|
|
@@ -344012,7 +344292,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344012
344292
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
344013
344293
|
for (const fileResult of result.fileResults) {
|
|
344014
344294
|
if (fileResult.status !== "conflict") continue;
|
|
344015
|
-
const filePath = (0,
|
|
344295
|
+
const filePath = (0, import_node_path52.join)(this.outputDir, fileResult.file);
|
|
344016
344296
|
try {
|
|
344017
344297
|
const content5 = (0, import_node_fs210.readFileSync)(filePath, "utf-8");
|
|
344018
344298
|
const stripped = stripConflictMarkers(content5);
|
|
@@ -344042,7 +344322,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344042
344322
|
}
|
|
344043
344323
|
}
|
|
344044
344324
|
readFernignorePatterns() {
|
|
344045
|
-
const fernignorePath = (0,
|
|
344325
|
+
const fernignorePath = (0, import_node_path52.join)(this.outputDir, ".fernignore");
|
|
344046
344326
|
if (!(0, import_node_fs210.existsSync)(fernignorePath)) return [];
|
|
344047
344327
|
return (0, import_node_fs210.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
344048
344328
|
}
|
|
@@ -344119,7 +344399,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344119
344399
|
}
|
|
344120
344400
|
var import_node_crypto22 = require("crypto");
|
|
344121
344401
|
var import_node_fs33 = require("fs");
|
|
344122
|
-
var
|
|
344402
|
+
var import_node_path62 = require("path");
|
|
344123
344403
|
var import_minimatch32 = require_commonjs3();
|
|
344124
344404
|
var import_yaml22 = require_dist9();
|
|
344125
344405
|
var FernignoreMigrator = class {
|
|
@@ -344132,10 +344412,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344132
344412
|
this.outputDir = outputDir;
|
|
344133
344413
|
}
|
|
344134
344414
|
fernignoreExists() {
|
|
344135
|
-
return (0, import_node_fs33.existsSync)((0,
|
|
344415
|
+
return (0, import_node_fs33.existsSync)((0, import_node_path62.join)(this.outputDir, ".fernignore"));
|
|
344136
344416
|
}
|
|
344137
344417
|
readFernignorePatterns() {
|
|
344138
|
-
const fernignorePath = (0,
|
|
344418
|
+
const fernignorePath = (0, import_node_path62.join)(this.outputDir, ".fernignore");
|
|
344139
344419
|
if (!(0, import_node_fs33.existsSync)(fernignorePath)) {
|
|
344140
344420
|
return [];
|
|
344141
344421
|
}
|
|
@@ -344187,6 +344467,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344187
344467
|
if (patchFiles.length > 0) {
|
|
344188
344468
|
const patchContent = diffParts.join("\n");
|
|
344189
344469
|
const contentHash = `sha256:${(0, import_node_crypto22.createHash)("sha256").update(patchContent).digest("hex")}`;
|
|
344470
|
+
const theirsSnapshot = {};
|
|
344471
|
+
for (const filePath of patchFiles) {
|
|
344472
|
+
if (isBinaryFile(filePath)) continue;
|
|
344473
|
+
const c5 = this.readFileContent(filePath);
|
|
344474
|
+
if (c5 != null) theirsSnapshot[filePath] = c5;
|
|
344475
|
+
}
|
|
344190
344476
|
syntheticPatches.push({
|
|
344191
344477
|
id: `patch-fernignore-${(0, import_node_crypto22.createHash)("sha256").update(pattern).digest("hex").slice(0, 8)}`,
|
|
344192
344478
|
content_hash: contentHash,
|
|
@@ -344196,6 +344482,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344196
344482
|
base_generation: currentGen.commit_sha,
|
|
344197
344483
|
files: patchFiles,
|
|
344198
344484
|
patch_content: patchContent,
|
|
344485
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
344199
344486
|
// Synthetic patches captured from .fernignore-protected files
|
|
344200
344487
|
// are user-owned by definition — the customer's content
|
|
344201
344488
|
// diverges from the generator's pristine output, and the
|
|
@@ -344237,7 +344524,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344237
344524
|
return results;
|
|
344238
344525
|
}
|
|
344239
344526
|
readFileContent(filePath) {
|
|
344240
|
-
const fullPath = (0,
|
|
344527
|
+
const fullPath = (0, import_node_path62.join)(this.outputDir, filePath);
|
|
344241
344528
|
if (!(0, import_node_fs33.existsSync)(fullPath)) {
|
|
344242
344529
|
return null;
|
|
344243
344530
|
}
|
|
@@ -344302,7 +344589,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344302
344589
|
};
|
|
344303
344590
|
}
|
|
344304
344591
|
movePatternsToReplayYml(patterns) {
|
|
344305
|
-
const replayYmlPath = (0,
|
|
344592
|
+
const replayYmlPath = (0, import_node_path62.join)(this.outputDir, ".fern", "replay.yml");
|
|
344306
344593
|
let config5 = {};
|
|
344307
344594
|
if ((0, import_node_fs33.existsSync)(replayYmlPath)) {
|
|
344308
344595
|
const content5 = (0, import_node_fs33.readFileSync)(replayYmlPath, "utf-8");
|
|
@@ -344311,7 +344598,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344311
344598
|
const existing = config5.exclude ?? [];
|
|
344312
344599
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
344313
344600
|
config5.exclude = merged;
|
|
344314
|
-
const dir = (0,
|
|
344601
|
+
const dir = (0, import_node_path62.dirname)(replayYmlPath);
|
|
344315
344602
|
if (!(0, import_node_fs33.existsSync)(dir)) {
|
|
344316
344603
|
(0, import_node_fs33.mkdirSync)(dir, { recursive: true });
|
|
344317
344604
|
}
|
|
@@ -344320,7 +344607,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344320
344607
|
};
|
|
344321
344608
|
var import_node_crypto32 = require("crypto");
|
|
344322
344609
|
var import_node_fs42 = require("fs");
|
|
344323
|
-
var
|
|
344610
|
+
var import_node_path72 = require("path");
|
|
344324
344611
|
init_GitClient();
|
|
344325
344612
|
async function bootstrap(outputDir, options2) {
|
|
344326
344613
|
const git = new GitClient(outputDir);
|
|
@@ -344487,7 +344774,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344487
344774
|
}
|
|
344488
344775
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
344489
344776
|
function ensureFernignoreEntries(outputDir) {
|
|
344490
|
-
const fernignorePath = (0,
|
|
344777
|
+
const fernignorePath = (0, import_node_path72.join)(outputDir, ".fernignore");
|
|
344491
344778
|
let content5 = "";
|
|
344492
344779
|
if ((0, import_node_fs42.existsSync)(fernignorePath)) {
|
|
344493
344780
|
content5 = (0, import_node_fs42.readFileSync)(fernignorePath, "utf-8");
|
|
@@ -344511,7 +344798,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344511
344798
|
}
|
|
344512
344799
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
344513
344800
|
function ensureGitattributesEntries(outputDir) {
|
|
344514
|
-
const gitattributesPath = (0,
|
|
344801
|
+
const gitattributesPath = (0, import_node_path72.join)(outputDir, ".gitattributes");
|
|
344515
344802
|
let content5 = "";
|
|
344516
344803
|
if ((0, import_node_fs42.existsSync)(gitattributesPath)) {
|
|
344517
344804
|
content5 = (0, import_node_fs42.readFileSync)(gitattributesPath, "utf-8");
|
|
@@ -344703,8 +344990,8 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344703
344990
|
nothingToReset: false
|
|
344704
344991
|
};
|
|
344705
344992
|
}
|
|
344706
|
-
var
|
|
344707
|
-
var
|
|
344993
|
+
var import_promises510 = require("fs/promises");
|
|
344994
|
+
var import_node_path82 = require("path");
|
|
344708
344995
|
init_GitClient();
|
|
344709
344996
|
async function resolve15(outputDir, options2) {
|
|
344710
344997
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -344748,36 +345035,78 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
344748
345035
|
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
344749
345036
|
const warnings = [];
|
|
344750
345037
|
let patchesResolved = 0;
|
|
345038
|
+
const pass1 = [];
|
|
344751
345039
|
for (const patch5 of patchesToCommit) {
|
|
344752
345040
|
const result = await computePerPatchDiffWithFallback(
|
|
344753
345041
|
patch5,
|
|
344754
345042
|
(f5) => git.showFile(currentGen, f5),
|
|
344755
|
-
(f5) => (0,
|
|
345043
|
+
(f5) => (0, import_promises510.readFile)((0, import_node_path82.join)(outputDir, f5), "utf-8").catch(() => null),
|
|
344756
345044
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
344757
345045
|
);
|
|
344758
345046
|
if (result === null) {
|
|
344759
345047
|
warnings.push(
|
|
344760
345048
|
`Patch ${patch5.id}: cumulative-diff fallback failed for unlocatable files; preserving patch unchanged`
|
|
344761
345049
|
);
|
|
345050
|
+
pass1.push({ kind: "skip" });
|
|
344762
345051
|
continue;
|
|
344763
345052
|
}
|
|
344764
345053
|
const diff2 = result.diff;
|
|
344765
345054
|
if (!diff2.trim()) {
|
|
345055
|
+
pass1.push({ kind: "revert" });
|
|
345056
|
+
continue;
|
|
345057
|
+
}
|
|
345058
|
+
const hash5 = detector.computeContentHash(diff2);
|
|
345059
|
+
pass1.push({
|
|
345060
|
+
kind: "apply",
|
|
345061
|
+
diff: diff2,
|
|
345062
|
+
hash: hash5,
|
|
345063
|
+
hadFallback: result.hadFallback,
|
|
345064
|
+
fallbackFiles: result.fallbackFiles
|
|
345065
|
+
});
|
|
345066
|
+
}
|
|
345067
|
+
const lastIndexByHash = /* @__PURE__ */ new Map();
|
|
345068
|
+
for (let i9 = 0; i9 < pass1.length; i9++) {
|
|
345069
|
+
const r5 = pass1[i9];
|
|
345070
|
+
if (r5.kind === "apply") lastIndexByHash.set(r5.hash, i9);
|
|
345071
|
+
}
|
|
345072
|
+
for (let i9 = 0; i9 < patchesToCommit.length; i9++) {
|
|
345073
|
+
const patch5 = patchesToCommit[i9];
|
|
345074
|
+
const r5 = pass1[i9];
|
|
345075
|
+
if (r5.kind === "skip") continue;
|
|
345076
|
+
if (r5.kind === "revert") {
|
|
344766
345077
|
lockManager.removePatch(patch5.id);
|
|
344767
345078
|
continue;
|
|
344768
345079
|
}
|
|
344769
|
-
if (
|
|
345080
|
+
if (lastIndexByHash.get(r5.hash) !== i9) {
|
|
345081
|
+
const canonicalIdx = lastIndexByHash.get(r5.hash);
|
|
345082
|
+
const canonical = patchesToCommit[canonicalIdx];
|
|
345083
|
+
lockManager.removePatch(patch5.id);
|
|
345084
|
+
warnings.push(
|
|
345085
|
+
`Consolidated patch ${patch5.id} (commit ${(patch5.original_commit || "<missing>").slice(0, 7)}) into patch ${canonical.id} (commit ${(canonical.original_commit || "<missing>").slice(0, 7)}): byte-identical patch_content after resolution. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
345086
|
+
);
|
|
345087
|
+
continue;
|
|
345088
|
+
}
|
|
345089
|
+
if (r5.hadFallback) {
|
|
344770
345090
|
warnings.push(
|
|
344771
|
-
`Patch ${patch5.id}: ${
|
|
345091
|
+
`Patch ${patch5.id}: ${r5.fallbackFiles.join(", ")} could not be anchored; cumulative-diff fallback applied for those files`
|
|
344772
345092
|
);
|
|
344773
345093
|
}
|
|
344774
|
-
const
|
|
344775
|
-
const
|
|
345094
|
+
const changedFiles = r5.hadFallback ? await getChangedFiles(git, currentGen, patch5.files) : extractFilesFromDiff(r5.diff);
|
|
345095
|
+
const filesForSnapshot = changedFiles.length > 0 ? changedFiles : patch5.files;
|
|
345096
|
+
const theirsSnapshot = {};
|
|
345097
|
+
for (const file4 of filesForSnapshot) {
|
|
345098
|
+
if (isBinaryFile(file4)) continue;
|
|
345099
|
+
const content5 = await (0, import_promises510.readFile)((0, import_node_path82.join)(outputDir, file4), "utf-8").catch(() => null);
|
|
345100
|
+
if (content5 != null) {
|
|
345101
|
+
theirsSnapshot[file4] = content5;
|
|
345102
|
+
}
|
|
345103
|
+
}
|
|
344776
345104
|
lockManager.markPatchResolved(patch5.id, {
|
|
344777
|
-
patch_content:
|
|
344778
|
-
content_hash:
|
|
345105
|
+
patch_content: r5.diff,
|
|
345106
|
+
content_hash: r5.hash,
|
|
344779
345107
|
base_generation: currentGen,
|
|
344780
|
-
files:
|
|
345108
|
+
files: filesForSnapshot,
|
|
345109
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
344781
345110
|
});
|
|
344782
345111
|
patchesResolved++;
|
|
344783
345112
|
}
|
|
@@ -531075,6 +531404,9 @@ function isAdditionalPropertiesAny(additionalProperties7, options2) {
|
|
|
531075
531404
|
if (typeof additionalProperties7 === "boolean") {
|
|
531076
531405
|
return additionalProperties7;
|
|
531077
531406
|
}
|
|
531407
|
+
if (typeof additionalProperties7 !== "object" || additionalProperties7 === null) {
|
|
531408
|
+
return true;
|
|
531409
|
+
}
|
|
531078
531410
|
if (isReferenceObject(additionalProperties7)) {
|
|
531079
531411
|
return false;
|
|
531080
531412
|
}
|
|
@@ -603250,6 +603582,9 @@ var ExampleConverter = class _ExampleConverter extends AbstractConverter {
|
|
|
603250
603582
|
* These schemas are used in allOf compositions to add constraints to the merged schema.
|
|
603251
603583
|
*/
|
|
603252
603584
|
isConstraintOnlySchema(schema2) {
|
|
603585
|
+
if (typeof schema2 !== "object" || schema2 === null) {
|
|
603586
|
+
return false;
|
|
603587
|
+
}
|
|
603253
603588
|
if (schema2.type !== void 0 || schema2.properties !== void 0 || schema2.allOf !== void 0 || schema2.oneOf !== void 0 || schema2.anyOf !== void 0 || "items" in schema2 || schema2.additionalProperties !== void 0 || schema2.enum !== void 0 || schema2.const !== void 0) {
|
|
603254
603589
|
return false;
|
|
603255
603590
|
}
|
|
@@ -604478,7 +604813,7 @@ var OneOfSchemaConverter = class extends AbstractConverter {
|
|
|
604478
604813
|
if (schemaArray == null) {
|
|
604479
604814
|
return void 0;
|
|
604480
604815
|
}
|
|
604481
|
-
const withoutNull = schemaArray.filter((subSchema) => !("type" in subSchema && subSchema.type === "null"));
|
|
604816
|
+
const withoutNull = schemaArray.filter((subSchema) => !(typeof subSchema === "object" && subSchema !== null && "type" in subSchema && subSchema.type === "null"));
|
|
604482
604817
|
if (withoutNull.length === 0) {
|
|
604483
604818
|
this.context.errorCollector.collect({
|
|
604484
604819
|
message: `Received ${schemaType} schema with no valid non-null types`,
|
|
@@ -605406,7 +605741,7 @@ var SchemaOrReferenceConverter = class extends AbstractConverter {
|
|
|
605406
605741
|
return void 0;
|
|
605407
605742
|
}
|
|
605408
605743
|
const refElements = this.schemaOrReference.allOf.filter((s9) => this.context.isReferenceObject(s9));
|
|
605409
|
-
const inlineElements = this.schemaOrReference.allOf.filter((s9) => !this.context.isReferenceObject(s9));
|
|
605744
|
+
const inlineElements = this.schemaOrReference.allOf.filter((s9) => typeof s9 === "object" && s9 !== null && !this.context.isReferenceObject(s9));
|
|
605410
605745
|
const singleRef = refElements.length === 1 ? refElements[0] : void 0;
|
|
605411
605746
|
if (singleRef != null && inlineElements.every((s9) => !s9.properties && !s9.enum && !s9.oneOf && !s9.anyOf && !s9.allOf && !s9.format && !("items" in s9)) && !inlineElements.some((s9) => s9.type === "null" || Array.isArray(s9.type) && s9.type.includes("null"))) {
|
|
605412
605747
|
const resolved = this.context.resolveMaybeReference({
|
|
@@ -622052,7 +622387,7 @@ var AccessTokenPosthogManager = class {
|
|
|
622052
622387
|
properties: {
|
|
622053
622388
|
...event,
|
|
622054
622389
|
...event.properties,
|
|
622055
|
-
version: "5.4.
|
|
622390
|
+
version: "5.4.1-1-g17393ac251c",
|
|
622056
622391
|
usingAccessToken: true
|
|
622057
622392
|
}
|
|
622058
622393
|
});
|
|
@@ -622106,7 +622441,7 @@ var UserPosthogManager = class {
|
|
|
622106
622441
|
distinctId: this.userId ?? await this.getPersistedDistinctId(),
|
|
622107
622442
|
event: "CLI",
|
|
622108
622443
|
properties: {
|
|
622109
|
-
version: "5.4.
|
|
622444
|
+
version: "5.4.1-1-g17393ac251c",
|
|
622110
622445
|
...event,
|
|
622111
622446
|
...event.properties,
|
|
622112
622447
|
usingAccessToken: false,
|
|
@@ -828808,6 +829143,31 @@ function withBasepath2(source2, basePath) {
|
|
|
828808
829143
|
return basePath == null ? source2 : source2.startsWith(basePath) ? source2 : (0, import_url_join22.default)(basePath, source2);
|
|
828809
829144
|
}
|
|
828810
829145
|
|
|
829146
|
+
// ../yaml/docs-validator/lib/rules/valid-markdown-link/with-base-path-prepended.js
|
|
829147
|
+
function withBasePathPrepended(pathname, basePath) {
|
|
829148
|
+
if (basePath == null) {
|
|
829149
|
+
return null;
|
|
829150
|
+
}
|
|
829151
|
+
const basePathSlug = stripSlashes(basePath);
|
|
829152
|
+
if (basePathSlug === "") {
|
|
829153
|
+
return null;
|
|
829154
|
+
}
|
|
829155
|
+
const bare = stripLeadingSlash2(pathname);
|
|
829156
|
+
if (bare === basePathSlug || bare.startsWith(basePathSlug + "/")) {
|
|
829157
|
+
return null;
|
|
829158
|
+
}
|
|
829159
|
+
return `${basePathSlug}/${bare}`;
|
|
829160
|
+
}
|
|
829161
|
+
function stripLeadingSlash2(value2) {
|
|
829162
|
+
return value2.startsWith("/") ? value2.slice(1) : value2;
|
|
829163
|
+
}
|
|
829164
|
+
function stripTrailingSlash(value2) {
|
|
829165
|
+
return value2.endsWith("/") ? value2.slice(0, -1) : value2;
|
|
829166
|
+
}
|
|
829167
|
+
function stripSlashes(value2) {
|
|
829168
|
+
return stripLeadingSlash2(stripTrailingSlash(value2));
|
|
829169
|
+
}
|
|
829170
|
+
|
|
828811
829171
|
// ../yaml/docs-validator/lib/rules/valid-markdown-link/check-if-pathname-exists.js
|
|
828812
829172
|
async function checkIfPathnameExists({ pathname, markdown, absoluteFilepath, workspaceAbsoluteFilePath, pageSlugs, absoluteFilePathsToSlugs, redirects = [], baseUrl, versionSlugs = [], productSlugs = [] }) {
|
|
828813
829173
|
pathname = removeTrailingSlash3(pathname);
|
|
@@ -828831,6 +829191,12 @@ async function checkIfPathnameExists({ pathname, markdown, absoluteFilepath, wor
|
|
|
828831
829191
|
if (markdown && pageSlugs.has(removeLeadingSlash(redirectedPath))) {
|
|
828832
829192
|
return true;
|
|
828833
829193
|
}
|
|
829194
|
+
if (markdown) {
|
|
829195
|
+
const basePathPrefixed = withBasePathPrepended(redirectedPath, baseUrl.basePath);
|
|
829196
|
+
if (basePathPrefixed != null && pageSlugs.has(basePathPrefixed)) {
|
|
829197
|
+
return true;
|
|
829198
|
+
}
|
|
829199
|
+
}
|
|
828834
829200
|
const contextSlugs = [...versionSlugs, ...productSlugs];
|
|
828835
829201
|
if (markdown && contextSlugs.length > 0) {
|
|
828836
829202
|
for (const slug of slugs) {
|
|
@@ -829125,6 +829491,39 @@ function collectPathnamesToCheck(content5, { absoluteFilepath, instanceUrls }) {
|
|
|
829125
829491
|
|
|
829126
829492
|
// ../yaml/docs-validator/lib/rules/valid-markdown-link/valid-markdown-link.js
|
|
829127
829493
|
var NOOP_CONTEXT2 = createMockTaskContext({ logger: createLogger(noop2) });
|
|
829494
|
+
function createDocsResolverValidationContext() {
|
|
829495
|
+
const base4 = createMockTaskContext({ logger: createLogger(noop2) });
|
|
829496
|
+
return {
|
|
829497
|
+
...base4,
|
|
829498
|
+
failAndThrow: (message, error50, _options) => {
|
|
829499
|
+
const messageParts = [];
|
|
829500
|
+
if (message != null) {
|
|
829501
|
+
messageParts.push(message);
|
|
829502
|
+
}
|
|
829503
|
+
if (error50 instanceof Error) {
|
|
829504
|
+
messageParts.push(error50.message);
|
|
829505
|
+
} else if (error50 != null) {
|
|
829506
|
+
messageParts.push(JSON.stringify(error50));
|
|
829507
|
+
}
|
|
829508
|
+
const combined = messageParts.length > 0 ? messageParts.join(": ") : "Docs validation failed";
|
|
829509
|
+
throw new Error(combined);
|
|
829510
|
+
},
|
|
829511
|
+
failWithoutThrowing: (message, error50, _options) => {
|
|
829512
|
+
const messageParts = [];
|
|
829513
|
+
if (message != null) {
|
|
829514
|
+
messageParts.push(message);
|
|
829515
|
+
}
|
|
829516
|
+
if (error50 instanceof Error) {
|
|
829517
|
+
messageParts.push(error50.message);
|
|
829518
|
+
} else if (error50 != null) {
|
|
829519
|
+
messageParts.push(JSON.stringify(error50));
|
|
829520
|
+
}
|
|
829521
|
+
if (messageParts.length > 0) {
|
|
829522
|
+
base4.logger.error(messageParts.join(": "));
|
|
829523
|
+
}
|
|
829524
|
+
}
|
|
829525
|
+
};
|
|
829526
|
+
}
|
|
829128
829527
|
function isV1RootNode2(value2) {
|
|
829129
829528
|
return "type" in value2 && value2.type === "root";
|
|
829130
829529
|
}
|
|
@@ -829139,7 +829538,7 @@ var ValidMarkdownLinks = {
|
|
|
829139
829538
|
docsWorkspace: workspace,
|
|
829140
829539
|
ossWorkspaces,
|
|
829141
829540
|
apiWorkspaces,
|
|
829142
|
-
taskContext:
|
|
829541
|
+
taskContext: createDocsResolverValidationContext(),
|
|
829143
829542
|
editThisPage: void 0,
|
|
829144
829543
|
uploadFiles: void 0,
|
|
829145
829544
|
registerApi: void 0,
|
|
@@ -830363,12 +830762,37 @@ async function runRulesOnDocsWorkspace({ workspace, rules: selectedRules, contex
|
|
|
830363
830762
|
context3.logger.debug(`Initial memory usage: RSS=${(startMemory.rss / 1024 / 1024).toFixed(2)}MB, Heap=${(startMemory.heapUsed / 1024 / 1024).toFixed(2)}MB`);
|
|
830364
830763
|
const violations = [];
|
|
830365
830764
|
const ruleCreationStart = performance.now();
|
|
830366
|
-
const
|
|
830367
|
-
|
|
830368
|
-
|
|
830369
|
-
|
|
830765
|
+
const ruleCreationResults = await Promise.all(rules.map(async (rule) => {
|
|
830766
|
+
try {
|
|
830767
|
+
const visitor = await rule.create({
|
|
830768
|
+
workspace,
|
|
830769
|
+
apiWorkspaces,
|
|
830770
|
+
ossWorkspaces,
|
|
830771
|
+
logger: context3.logger
|
|
830772
|
+
});
|
|
830773
|
+
return { ruleName: rule.name, visitor };
|
|
830774
|
+
} catch (error50) {
|
|
830775
|
+
return { ruleName: rule.name, error: error50 };
|
|
830776
|
+
}
|
|
830777
|
+
}));
|
|
830778
|
+
const allRulesWithVisitors = [];
|
|
830779
|
+
for (const result of ruleCreationResults) {
|
|
830780
|
+
if ("error" in result) {
|
|
830781
|
+
const message = result.error instanceof Error ? result.error.message : String(result.error);
|
|
830782
|
+
violations.push({
|
|
830783
|
+
name: result.ruleName,
|
|
830784
|
+
severity: "fatal",
|
|
830785
|
+
relativeFilepath: RelativeFilePath2.of(DOCS_CONFIGURATION_FILENAME),
|
|
830786
|
+
nodePath: [],
|
|
830787
|
+
message: `Rule "${result.ruleName}" failed to initialize: ${message}`
|
|
830788
|
+
});
|
|
830789
|
+
context3.logger.debug(`Rule "${result.ruleName}" failed to initialize: ${result.error instanceof Error ? result.error.stack ?? result.error.message : String(result.error)}`);
|
|
830790
|
+
} else {
|
|
830791
|
+
allRulesWithVisitors.push(result);
|
|
830792
|
+
}
|
|
830793
|
+
}
|
|
830370
830794
|
const ruleCreationTime = performance.now() - ruleCreationStart;
|
|
830371
|
-
context3.logger.debug(`Created ${
|
|
830795
|
+
context3.logger.debug(`Created ${allRulesWithVisitors.length} rule visitors in ${ruleCreationTime.toFixed(0)}ms (${rules.length - allRulesWithVisitors.length} failed to initialize)`);
|
|
830372
830796
|
const astVisitor = createDocsConfigFileAstVisitorForRules({
|
|
830373
830797
|
relativeFilepath: RelativeFilePath2.of(DOCS_CONFIGURATION_FILENAME),
|
|
830374
830798
|
allRulesWithVisitors,
|
|
@@ -834386,7 +834810,7 @@ var LOCAL_STORAGE_FOLDER4 = ".fern-dev";
|
|
|
834386
834810
|
var LOGS_FOLDER_NAME = "logs";
|
|
834387
834811
|
var MAX_LOGS_DIR_SIZE_BYTES = 100 * 1024 * 1024;
|
|
834388
834812
|
function getCliSource() {
|
|
834389
|
-
const version7 = "5.4.
|
|
834813
|
+
const version7 = "5.4.1-1-g17393ac251c";
|
|
834390
834814
|
return `cli@${version7}`;
|
|
834391
834815
|
}
|
|
834392
834816
|
var DebugLogger = class {
|
|
@@ -846848,7 +847272,7 @@ var LegacyDocsPublisher = class {
|
|
|
846848
847272
|
previewId,
|
|
846849
847273
|
disableTemplates: void 0,
|
|
846850
847274
|
skipUpload,
|
|
846851
|
-
cliVersion: "5.4.
|
|
847275
|
+
cliVersion: "5.4.1-1-g17393ac251c",
|
|
846852
847276
|
loginCommand: "fern auth login"
|
|
846853
847277
|
});
|
|
846854
847278
|
if (taskContext.getResult() === TaskResult.Failure) {
|
|
@@ -921180,7 +921604,7 @@ var CliContext = class _CliContext {
|
|
|
921180
921604
|
if (false) {
|
|
921181
921605
|
this.logger.error("CLI_VERSION is not defined");
|
|
921182
921606
|
}
|
|
921183
|
-
return "5.4.
|
|
921607
|
+
return "5.4.1-1-g17393ac251c";
|
|
921184
921608
|
}
|
|
921185
921609
|
getCliName() {
|
|
921186
921610
|
if (false) {
|