@fern-api/replay 0.9.0 → 0.9.1
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/dist/cli.cjs +321 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +321 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +321 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -101,6 +101,7 @@ declare class GitClient {
|
|
|
101
101
|
}>>;
|
|
102
102
|
isAncestor(commit: string, descendant: string): Promise<boolean>;
|
|
103
103
|
commitExists(sha: string): Promise<boolean>;
|
|
104
|
+
treeExists(treeHash: string): Promise<boolean>;
|
|
104
105
|
getCommitBody(commitSha: string): Promise<string>;
|
|
105
106
|
getRepoPath(): string;
|
|
106
107
|
}
|
|
@@ -194,6 +195,7 @@ declare class ReplayApplicator {
|
|
|
194
195
|
private lockManager;
|
|
195
196
|
private outputDir;
|
|
196
197
|
private renameCache;
|
|
198
|
+
private treeExistsCache;
|
|
197
199
|
private fileTheirsAccumulator;
|
|
198
200
|
constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
|
|
199
201
|
/** Reset inter-patch accumulator for a new cycle. */
|
|
@@ -208,6 +210,7 @@ declare class ReplayApplicator {
|
|
|
208
210
|
private applyPatchWithFallback;
|
|
209
211
|
private applyWithThreeWayMerge;
|
|
210
212
|
private mergeFile;
|
|
213
|
+
private isTreeReachable;
|
|
211
214
|
private isExcluded;
|
|
212
215
|
private resolveFilePath;
|
|
213
216
|
private applyPatchToContent;
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,7 @@ declare class GitClient {
|
|
|
101
101
|
}>>;
|
|
102
102
|
isAncestor(commit: string, descendant: string): Promise<boolean>;
|
|
103
103
|
commitExists(sha: string): Promise<boolean>;
|
|
104
|
+
treeExists(treeHash: string): Promise<boolean>;
|
|
104
105
|
getCommitBody(commitSha: string): Promise<string>;
|
|
105
106
|
getRepoPath(): string;
|
|
106
107
|
}
|
|
@@ -194,6 +195,7 @@ declare class ReplayApplicator {
|
|
|
194
195
|
private lockManager;
|
|
195
196
|
private outputDir;
|
|
196
197
|
private renameCache;
|
|
198
|
+
private treeExistsCache;
|
|
197
199
|
private fileTheirsAccumulator;
|
|
198
200
|
constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
|
|
199
201
|
/** Reset inter-patch accumulator for a new cycle. */
|
|
@@ -208,6 +210,7 @@ declare class ReplayApplicator {
|
|
|
208
210
|
private applyPatchWithFallback;
|
|
209
211
|
private applyWithThreeWayMerge;
|
|
210
212
|
private mergeFile;
|
|
213
|
+
private isTreeReachable;
|
|
211
214
|
private isExcluded;
|
|
212
215
|
private resolveFilePath;
|
|
213
216
|
private applyPatchToContent;
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,14 @@ var init_GitClient = __esm({
|
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
async treeExists(treeHash) {
|
|
115
|
+
try {
|
|
116
|
+
const type = await this.exec(["cat-file", "-t", treeHash]);
|
|
117
|
+
return type.trim() === "tree";
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
114
122
|
async getCommitBody(commitSha) {
|
|
115
123
|
return this.exec(["log", "-1", "--format=%B", commitSha]);
|
|
116
124
|
}
|
|
@@ -121,6 +129,255 @@ var init_GitClient = __esm({
|
|
|
121
129
|
}
|
|
122
130
|
});
|
|
123
131
|
|
|
132
|
+
// src/HybridReconstruction.ts
|
|
133
|
+
var HybridReconstruction_exports = {};
|
|
134
|
+
__export(HybridReconstruction_exports, {
|
|
135
|
+
assembleHybrid: () => assembleHybrid,
|
|
136
|
+
locateHunksInOurs: () => locateHunksInOurs,
|
|
137
|
+
parseHunks: () => parseHunks,
|
|
138
|
+
reconstructFromGhostPatch: () => reconstructFromGhostPatch
|
|
139
|
+
});
|
|
140
|
+
function parseHunks(fileDiff) {
|
|
141
|
+
const lines = fileDiff.split("\n");
|
|
142
|
+
const hunks = [];
|
|
143
|
+
let currentHunk = null;
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
const headerMatch = line.match(
|
|
146
|
+
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
147
|
+
);
|
|
148
|
+
if (headerMatch) {
|
|
149
|
+
if (currentHunk) {
|
|
150
|
+
hunks.push(currentHunk);
|
|
151
|
+
}
|
|
152
|
+
currentHunk = {
|
|
153
|
+
oldStart: parseInt(headerMatch[1], 10),
|
|
154
|
+
oldCount: headerMatch[2] != null ? parseInt(headerMatch[2], 10) : 1,
|
|
155
|
+
newStart: parseInt(headerMatch[3], 10),
|
|
156
|
+
newCount: headerMatch[4] != null ? parseInt(headerMatch[4], 10) : 1,
|
|
157
|
+
lines: []
|
|
158
|
+
};
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!currentHunk) continue;
|
|
162
|
+
if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("similarity index") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("new file mode") || line.startsWith("deleted file mode")) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (line === "\") {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (line.startsWith("-")) {
|
|
169
|
+
currentHunk.lines.push({ type: "remove", content: line.slice(1) });
|
|
170
|
+
} else if (line.startsWith("+")) {
|
|
171
|
+
currentHunk.lines.push({ type: "add", content: line.slice(1) });
|
|
172
|
+
} else if (line.startsWith(" ") || line === "") {
|
|
173
|
+
currentHunk.lines.push({
|
|
174
|
+
type: "context",
|
|
175
|
+
content: line.startsWith(" ") ? line.slice(1) : line
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (currentHunk) {
|
|
180
|
+
hunks.push(currentHunk);
|
|
181
|
+
}
|
|
182
|
+
return hunks;
|
|
183
|
+
}
|
|
184
|
+
function extractLeadingContext(hunk) {
|
|
185
|
+
const result = [];
|
|
186
|
+
for (const line of hunk.lines) {
|
|
187
|
+
if (line.type !== "context") break;
|
|
188
|
+
result.push(line.content);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function extractTrailingContext(hunk) {
|
|
193
|
+
const result = [];
|
|
194
|
+
for (let i = hunk.lines.length - 1; i >= 0; i--) {
|
|
195
|
+
if (hunk.lines[i].type !== "context") break;
|
|
196
|
+
result.unshift(hunk.lines[i].content);
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
function countOursLinesBeforeTrailing(hunk) {
|
|
201
|
+
let count = 0;
|
|
202
|
+
const trailingStart = findTrailingContextStart(hunk);
|
|
203
|
+
for (let i = 0; i < trailingStart; i++) {
|
|
204
|
+
if (hunk.lines[i].type === "context") count++;
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
function findTrailingContextStart(hunk) {
|
|
209
|
+
let i = hunk.lines.length - 1;
|
|
210
|
+
while (i >= 0 && hunk.lines[i].type === "context") {
|
|
211
|
+
i--;
|
|
212
|
+
}
|
|
213
|
+
return i + 1;
|
|
214
|
+
}
|
|
215
|
+
function matchesAt(needle, haystack, offset) {
|
|
216
|
+
for (let i = 0; i < needle.length; i++) {
|
|
217
|
+
if (haystack[offset + i] !== needle[i]) return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
function findContextInOurs(contextLines, oursLines, minIndex, hint) {
|
|
222
|
+
const SEARCH_WINDOW = 200;
|
|
223
|
+
const maxStart = oursLines.length - contextLines.length;
|
|
224
|
+
const clampedHint = Math.max(minIndex, Math.min(hint, maxStart));
|
|
225
|
+
if (clampedHint >= minIndex && clampedHint <= maxStart) {
|
|
226
|
+
if (matchesAt(contextLines, oursLines, clampedHint)) {
|
|
227
|
+
return clampedHint;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (let delta = 1; delta <= SEARCH_WINDOW; delta++) {
|
|
231
|
+
for (const sign of [1, -1]) {
|
|
232
|
+
const idx = clampedHint + delta * sign;
|
|
233
|
+
if (idx < minIndex || idx > maxStart) continue;
|
|
234
|
+
if (matchesAt(contextLines, oursLines, idx)) {
|
|
235
|
+
return idx;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
function computeOursSpan(hunk, oursLines, oursOffset) {
|
|
242
|
+
const leading = extractLeadingContext(hunk);
|
|
243
|
+
const trailing = extractTrailingContext(hunk);
|
|
244
|
+
if (trailing.length === 0) {
|
|
245
|
+
const contextCount2 = hunk.lines.filter(
|
|
246
|
+
(l) => l.type === "context"
|
|
247
|
+
).length;
|
|
248
|
+
return Math.min(contextCount2, oursLines.length - oursOffset);
|
|
249
|
+
}
|
|
250
|
+
const searchStart = oursOffset + leading.length;
|
|
251
|
+
for (let i = searchStart; i <= oursLines.length - trailing.length; i++) {
|
|
252
|
+
if (matchesAt(trailing, oursLines, i)) {
|
|
253
|
+
return i + trailing.length - oursOffset;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const contextCount = hunk.lines.filter(
|
|
257
|
+
(l) => l.type === "context"
|
|
258
|
+
).length;
|
|
259
|
+
return Math.min(contextCount, oursLines.length - oursOffset);
|
|
260
|
+
}
|
|
261
|
+
function locateHunksInOurs(hunks, oursLines) {
|
|
262
|
+
const located = [];
|
|
263
|
+
let minOursIndex = 0;
|
|
264
|
+
for (const hunk of hunks) {
|
|
265
|
+
const contextLines = extractLeadingContext(hunk);
|
|
266
|
+
let oursOffset;
|
|
267
|
+
if (contextLines.length > 0) {
|
|
268
|
+
const found = findContextInOurs(
|
|
269
|
+
contextLines,
|
|
270
|
+
oursLines,
|
|
271
|
+
minOursIndex,
|
|
272
|
+
hunk.newStart - 1
|
|
273
|
+
);
|
|
274
|
+
if (found === -1) {
|
|
275
|
+
const trailingContext = extractTrailingContext(hunk);
|
|
276
|
+
if (trailingContext.length > 0) {
|
|
277
|
+
const trailingFound = findContextInOurs(
|
|
278
|
+
trailingContext,
|
|
279
|
+
oursLines,
|
|
280
|
+
minOursIndex,
|
|
281
|
+
hunk.newStart - 1
|
|
282
|
+
);
|
|
283
|
+
if (trailingFound === -1) return null;
|
|
284
|
+
const nonTrailingCount = countOursLinesBeforeTrailing(hunk);
|
|
285
|
+
oursOffset = trailingFound - nonTrailingCount;
|
|
286
|
+
if (oursOffset < minOursIndex) return null;
|
|
287
|
+
} else {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
oursOffset = found;
|
|
292
|
+
}
|
|
293
|
+
} else if (hunk.oldStart === 1 && hunk.oldCount === 0) {
|
|
294
|
+
oursOffset = 0;
|
|
295
|
+
} else {
|
|
296
|
+
oursOffset = Math.max(hunk.newStart - 1, minOursIndex);
|
|
297
|
+
}
|
|
298
|
+
const oursSpan = computeOursSpan(hunk, oursLines, oursOffset);
|
|
299
|
+
located.push({ hunk, oursOffset, oursSpan });
|
|
300
|
+
minOursIndex = oursOffset + oursSpan;
|
|
301
|
+
}
|
|
302
|
+
return located;
|
|
303
|
+
}
|
|
304
|
+
function assembleHybrid(locatedHunks, oursLines) {
|
|
305
|
+
const baseLines = [];
|
|
306
|
+
const theirsLines = [];
|
|
307
|
+
let oursCursor = 0;
|
|
308
|
+
for (const { hunk, oursOffset, oursSpan } of locatedHunks) {
|
|
309
|
+
if (oursOffset > oursCursor) {
|
|
310
|
+
const gapLines = oursLines.slice(oursCursor, oursOffset);
|
|
311
|
+
baseLines.push(...gapLines);
|
|
312
|
+
theirsLines.push(...gapLines);
|
|
313
|
+
}
|
|
314
|
+
for (const line of hunk.lines) {
|
|
315
|
+
switch (line.type) {
|
|
316
|
+
case "context":
|
|
317
|
+
baseLines.push(line.content);
|
|
318
|
+
theirsLines.push(line.content);
|
|
319
|
+
break;
|
|
320
|
+
case "remove":
|
|
321
|
+
baseLines.push(line.content);
|
|
322
|
+
break;
|
|
323
|
+
case "add":
|
|
324
|
+
theirsLines.push(line.content);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
oursCursor = oursOffset + oursSpan;
|
|
329
|
+
}
|
|
330
|
+
if (oursCursor < oursLines.length) {
|
|
331
|
+
const gapLines = oursLines.slice(oursCursor);
|
|
332
|
+
baseLines.push(...gapLines);
|
|
333
|
+
theirsLines.push(...gapLines);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
base: baseLines.join("\n"),
|
|
337
|
+
theirs: theirsLines.join("\n")
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function reconstructFromGhostPatch(fileDiff, ours) {
|
|
341
|
+
const hunks = parseHunks(fileDiff);
|
|
342
|
+
if (hunks.length === 0) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const isPureAddition = hunks.every(
|
|
346
|
+
(h) => h.oldCount === 0 && h.lines.every((l) => l.type !== "remove")
|
|
347
|
+
);
|
|
348
|
+
if (isPureAddition) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const isPureDeletion = hunks.every(
|
|
352
|
+
(h) => h.newCount === 0 && h.lines.every((l) => l.type !== "add")
|
|
353
|
+
);
|
|
354
|
+
if (isPureDeletion) {
|
|
355
|
+
const baseLines = [];
|
|
356
|
+
for (const hunk of hunks) {
|
|
357
|
+
for (const line of hunk.lines) {
|
|
358
|
+
if (line.type === "context" || line.type === "remove") {
|
|
359
|
+
baseLines.push(line.content);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
base: baseLines.join("\n"),
|
|
365
|
+
theirs: ""
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const oursLines = ours.split("\n");
|
|
369
|
+
const located = locateHunksInOurs(hunks, oursLines);
|
|
370
|
+
if (!located) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return assembleHybrid(located, oursLines);
|
|
374
|
+
}
|
|
375
|
+
var init_HybridReconstruction = __esm({
|
|
376
|
+
"src/HybridReconstruction.ts"() {
|
|
377
|
+
"use strict";
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
124
381
|
// src/index.ts
|
|
125
382
|
init_GitClient();
|
|
126
383
|
|
|
@@ -615,6 +872,7 @@ var ReplayApplicator = class {
|
|
|
615
872
|
lockManager;
|
|
616
873
|
outputDir;
|
|
617
874
|
renameCache = /* @__PURE__ */ new Map();
|
|
875
|
+
treeExistsCache = /* @__PURE__ */ new Map();
|
|
618
876
|
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
619
877
|
constructor(git, lockManager, outputDir) {
|
|
620
878
|
this.git = git;
|
|
@@ -811,14 +1069,33 @@ var ReplayApplicator = class {
|
|
|
811
1069
|
}
|
|
812
1070
|
const oursPath = join2(this.outputDir, resolvedPath);
|
|
813
1071
|
const ours = await readFile(oursPath, "utf-8").catch(() => null);
|
|
814
|
-
let
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1072
|
+
let ghostReconstructed = false;
|
|
1073
|
+
let theirs = null;
|
|
1074
|
+
if (!base && ours && !renameSourcePath) {
|
|
1075
|
+
const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
|
|
1076
|
+
if (!treeReachable) {
|
|
1077
|
+
const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
|
|
1078
|
+
if (fileDiff) {
|
|
1079
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
1080
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
1081
|
+
if (result) {
|
|
1082
|
+
base = result.base;
|
|
1083
|
+
theirs = result.theirs;
|
|
1084
|
+
ghostReconstructed = true;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!ghostReconstructed) {
|
|
1090
|
+
theirs = await this.applyPatchToContent(
|
|
1091
|
+
base,
|
|
1092
|
+
patch.patch_content,
|
|
1093
|
+
filePath,
|
|
1094
|
+
tempGit,
|
|
1095
|
+
tempDir,
|
|
1096
|
+
renameSourcePath
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
822
1099
|
let useAccumulatorAsMergeBase = false;
|
|
823
1100
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
824
1101
|
if (!theirs && accumulatorEntry) {
|
|
@@ -851,13 +1128,13 @@ var ReplayApplicator = class {
|
|
|
851
1128
|
baseMismatchSkipped = true;
|
|
852
1129
|
}
|
|
853
1130
|
}
|
|
854
|
-
if (
|
|
1131
|
+
if (base == null && !ours && effective_theirs) {
|
|
855
1132
|
const outDir2 = dirname2(oursPath);
|
|
856
1133
|
await mkdir(outDir2, { recursive: true });
|
|
857
1134
|
await writeFile(oursPath, effective_theirs);
|
|
858
1135
|
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
859
1136
|
}
|
|
860
|
-
if (
|
|
1137
|
+
if (base == null && ours && effective_theirs) {
|
|
861
1138
|
const merged2 = threeWayMerge("", ours, effective_theirs);
|
|
862
1139
|
const outDir2 = dirname2(oursPath);
|
|
863
1140
|
await mkdir(outDir2, { recursive: true });
|
|
@@ -880,7 +1157,7 @@ var ReplayApplicator = class {
|
|
|
880
1157
|
reason: "missing-content"
|
|
881
1158
|
};
|
|
882
1159
|
}
|
|
883
|
-
if (
|
|
1160
|
+
if (base == null && !useAccumulatorAsMergeBase || !ours) {
|
|
884
1161
|
return {
|
|
885
1162
|
file: resolvedPath,
|
|
886
1163
|
status: "skipped",
|
|
@@ -923,6 +1200,14 @@ var ReplayApplicator = class {
|
|
|
923
1200
|
};
|
|
924
1201
|
}
|
|
925
1202
|
}
|
|
1203
|
+
async isTreeReachable(treeHash) {
|
|
1204
|
+
let result = this.treeExistsCache.get(treeHash);
|
|
1205
|
+
if (result === void 0) {
|
|
1206
|
+
result = await this.git.treeExists(treeHash);
|
|
1207
|
+
this.treeExistsCache.set(treeHash, result);
|
|
1208
|
+
}
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
926
1211
|
isExcluded(patch) {
|
|
927
1212
|
const config = this.lockManager.getCustomizationsConfig();
|
|
928
1213
|
if (!config.exclude) return false;
|
|
@@ -1997,6 +2282,7 @@ async function bootstrap(outputDir, options) {
|
|
|
1997
2282
|
}
|
|
1998
2283
|
lockManager.save();
|
|
1999
2284
|
const fernignoreUpdated = ensureFernignoreEntries(outputDir);
|
|
2285
|
+
ensureGitattributesEntries(outputDir);
|
|
2000
2286
|
if (migrator.fernignoreExists() && fernignorePatterns.length > 0) {
|
|
2001
2287
|
const action = options?.fernignoreAction ?? "skip";
|
|
2002
2288
|
if (action === "migrate") {
|
|
@@ -2078,7 +2364,7 @@ function parseGitLog(log) {
|
|
|
2078
2364
|
return { sha, authorName, authorEmail, message };
|
|
2079
2365
|
});
|
|
2080
2366
|
}
|
|
2081
|
-
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml"];
|
|
2367
|
+
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
2082
2368
|
function ensureFernignoreEntries(outputDir) {
|
|
2083
2369
|
const fernignorePath = join5(outputDir, ".fernignore");
|
|
2084
2370
|
let content = "";
|
|
@@ -2102,6 +2388,29 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
2102
2388
|
writeFileSync4(fernignorePath, content, "utf-8");
|
|
2103
2389
|
return true;
|
|
2104
2390
|
}
|
|
2391
|
+
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
2392
|
+
function ensureGitattributesEntries(outputDir) {
|
|
2393
|
+
const gitattributesPath = join5(outputDir, ".gitattributes");
|
|
2394
|
+
let content = "";
|
|
2395
|
+
if (existsSync4(gitattributesPath)) {
|
|
2396
|
+
content = readFileSync4(gitattributesPath, "utf-8");
|
|
2397
|
+
}
|
|
2398
|
+
const lines = content.split("\n");
|
|
2399
|
+
const toAdd = [];
|
|
2400
|
+
for (const entry of GITATTRIBUTES_ENTRIES) {
|
|
2401
|
+
if (!lines.some((line) => line.trim() === entry)) {
|
|
2402
|
+
toAdd.push(entry);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
if (toAdd.length === 0) {
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (content && !content.endsWith("\n")) {
|
|
2409
|
+
content += "\n";
|
|
2410
|
+
}
|
|
2411
|
+
content += toAdd.join("\n") + "\n";
|
|
2412
|
+
writeFileSync4(gitattributesPath, content, "utf-8");
|
|
2413
|
+
}
|
|
2105
2414
|
function computeContentHash(patchContent) {
|
|
2106
2415
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
2107
2416
|
return `sha256:${createHash3("sha256").update(normalized).digest("hex")}`;
|