@fern-api/replay 0.7.0 → 0.8.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 +173 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +174 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -8
- package/dist/index.d.ts +19 -8
- package/dist/index.js +172 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -100,6 +100,7 @@ declare class GitClient {
|
|
|
100
100
|
}>>;
|
|
101
101
|
isAncestor(commit: string, descendant: string): Promise<boolean>;
|
|
102
102
|
commitExists(sha: string): Promise<boolean>;
|
|
103
|
+
getCommitBody(commitSha: string): Promise<string>;
|
|
103
104
|
getRepoPath(): string;
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -108,6 +109,12 @@ declare const FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com
|
|
|
108
109
|
declare const FERN_BOT_LOGIN = "fern-api[bot]";
|
|
109
110
|
declare function isGenerationCommit(commit: CommitInfo): boolean;
|
|
110
111
|
declare function isReplayCommit(commit: CommitInfo): boolean;
|
|
112
|
+
/** Check if a commit message matches git's standard revert format: Revert "..." */
|
|
113
|
+
declare function isRevertCommit(message: string): boolean;
|
|
114
|
+
/** Extract the reverted commit SHA from a full commit body containing "This reverts commit SHA." */
|
|
115
|
+
declare function parseRevertedSha(fullBody: string): string | undefined;
|
|
116
|
+
/** Extract the original commit message from a revert subject like 'Revert "original message"' */
|
|
117
|
+
declare function parseRevertedMessage(subject: string): string | undefined;
|
|
111
118
|
|
|
112
119
|
declare class LockfileManager {
|
|
113
120
|
private outputDir;
|
|
@@ -143,20 +150,28 @@ declare class LockfileManager {
|
|
|
143
150
|
private ensureLoaded;
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
interface DetectionResult {
|
|
154
|
+
patches: StoredPatch[];
|
|
155
|
+
revertedPatchIds: string[];
|
|
156
|
+
}
|
|
146
157
|
declare class ReplayDetector {
|
|
147
158
|
private git;
|
|
148
159
|
private lockManager;
|
|
149
160
|
private sdkOutputDir;
|
|
150
161
|
readonly warnings: string[];
|
|
151
162
|
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
|
|
152
|
-
detectNewPatches(): Promise<
|
|
163
|
+
detectNewPatches(): Promise<DetectionResult>;
|
|
153
164
|
/**
|
|
154
165
|
* Compute content hash for deduplication.
|
|
155
166
|
* Removes commit SHA line and index lines before hashing,
|
|
156
167
|
* so rebased commits with same content produce the same hash.
|
|
157
168
|
*/
|
|
158
169
|
computeContentHash(patchContent: string): string;
|
|
159
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
172
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
173
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
174
|
+
*/
|
|
160
175
|
private detectPatchesViaTreeDiff;
|
|
161
176
|
private parseGitLog;
|
|
162
177
|
private getLastGeneration;
|
|
@@ -242,6 +257,7 @@ interface ReplayReport {
|
|
|
242
257
|
patchesKeptAsUserOwned?: number;
|
|
243
258
|
patchesPartiallyApplied?: number;
|
|
244
259
|
patchesConflictResolved?: number;
|
|
260
|
+
patchesReverted?: number;
|
|
245
261
|
patchesRefreshed?: number;
|
|
246
262
|
conflicts: FileResult[];
|
|
247
263
|
conflictDetails?: ConflictDetail[];
|
|
@@ -319,11 +335,6 @@ declare class ReplayService {
|
|
|
319
335
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
320
336
|
*/
|
|
321
337
|
private preGenerationRebase;
|
|
322
|
-
/**
|
|
323
|
-
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
324
|
-
* Preserves clean patches' non-conflicting changes on shared files.
|
|
325
|
-
*/
|
|
326
|
-
private stripConflictMarkers;
|
|
327
338
|
/**
|
|
328
339
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
329
340
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -492,4 +503,4 @@ interface StatusGeneration {
|
|
|
492
503
|
}
|
|
493
504
|
declare function status(outputDir: string): StatusResult;
|
|
494
505
|
|
|
495
|
-
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
|
|
506
|
+
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
|
package/dist/index.d.ts
CHANGED
|
@@ -100,6 +100,7 @@ declare class GitClient {
|
|
|
100
100
|
}>>;
|
|
101
101
|
isAncestor(commit: string, descendant: string): Promise<boolean>;
|
|
102
102
|
commitExists(sha: string): Promise<boolean>;
|
|
103
|
+
getCommitBody(commitSha: string): Promise<string>;
|
|
103
104
|
getRepoPath(): string;
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -108,6 +109,12 @@ declare const FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com
|
|
|
108
109
|
declare const FERN_BOT_LOGIN = "fern-api[bot]";
|
|
109
110
|
declare function isGenerationCommit(commit: CommitInfo): boolean;
|
|
110
111
|
declare function isReplayCommit(commit: CommitInfo): boolean;
|
|
112
|
+
/** Check if a commit message matches git's standard revert format: Revert "..." */
|
|
113
|
+
declare function isRevertCommit(message: string): boolean;
|
|
114
|
+
/** Extract the reverted commit SHA from a full commit body containing "This reverts commit SHA." */
|
|
115
|
+
declare function parseRevertedSha(fullBody: string): string | undefined;
|
|
116
|
+
/** Extract the original commit message from a revert subject like 'Revert "original message"' */
|
|
117
|
+
declare function parseRevertedMessage(subject: string): string | undefined;
|
|
111
118
|
|
|
112
119
|
declare class LockfileManager {
|
|
113
120
|
private outputDir;
|
|
@@ -143,20 +150,28 @@ declare class LockfileManager {
|
|
|
143
150
|
private ensureLoaded;
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
interface DetectionResult {
|
|
154
|
+
patches: StoredPatch[];
|
|
155
|
+
revertedPatchIds: string[];
|
|
156
|
+
}
|
|
146
157
|
declare class ReplayDetector {
|
|
147
158
|
private git;
|
|
148
159
|
private lockManager;
|
|
149
160
|
private sdkOutputDir;
|
|
150
161
|
readonly warnings: string[];
|
|
151
162
|
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
|
|
152
|
-
detectNewPatches(): Promise<
|
|
163
|
+
detectNewPatches(): Promise<DetectionResult>;
|
|
153
164
|
/**
|
|
154
165
|
* Compute content hash for deduplication.
|
|
155
166
|
* Removes commit SHA line and index lines before hashing,
|
|
156
167
|
* so rebased commits with same content produce the same hash.
|
|
157
168
|
*/
|
|
158
169
|
computeContentHash(patchContent: string): string;
|
|
159
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
172
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
173
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
174
|
+
*/
|
|
160
175
|
private detectPatchesViaTreeDiff;
|
|
161
176
|
private parseGitLog;
|
|
162
177
|
private getLastGeneration;
|
|
@@ -242,6 +257,7 @@ interface ReplayReport {
|
|
|
242
257
|
patchesKeptAsUserOwned?: number;
|
|
243
258
|
patchesPartiallyApplied?: number;
|
|
244
259
|
patchesConflictResolved?: number;
|
|
260
|
+
patchesReverted?: number;
|
|
245
261
|
patchesRefreshed?: number;
|
|
246
262
|
conflicts: FileResult[];
|
|
247
263
|
conflictDetails?: ConflictDetail[];
|
|
@@ -319,11 +335,6 @@ declare class ReplayService {
|
|
|
319
335
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
320
336
|
*/
|
|
321
337
|
private preGenerationRebase;
|
|
322
|
-
/**
|
|
323
|
-
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
324
|
-
* Preserves clean patches' non-conflicting changes on shared files.
|
|
325
|
-
*/
|
|
326
|
-
private stripConflictMarkers;
|
|
327
338
|
/**
|
|
328
339
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
329
340
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -492,4 +503,4 @@ interface StatusGeneration {
|
|
|
492
503
|
}
|
|
493
504
|
declare function status(outputDir: string): StatusResult;
|
|
494
505
|
|
|
495
|
-
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
|
|
506
|
+
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,9 @@ var init_GitClient = __esm({
|
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
async getCommitBody(commitSha) {
|
|
115
|
+
return this.exec(["log", "-1", "--format=%B", commitSha]);
|
|
116
|
+
}
|
|
114
117
|
getRepoPath() {
|
|
115
118
|
return this.repoPath;
|
|
116
119
|
}
|
|
@@ -138,6 +141,17 @@ function isGenerationCommit(commit) {
|
|
|
138
141
|
function isReplayCommit(commit) {
|
|
139
142
|
return commit.message.startsWith("[fern-replay]");
|
|
140
143
|
}
|
|
144
|
+
function isRevertCommit(message) {
|
|
145
|
+
return /^Revert ".+"$/.test(message);
|
|
146
|
+
}
|
|
147
|
+
function parseRevertedSha(fullBody) {
|
|
148
|
+
const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
|
|
149
|
+
return match?.[1];
|
|
150
|
+
}
|
|
151
|
+
function parseRevertedMessage(subject) {
|
|
152
|
+
const match = subject.match(/^Revert "(.+)"$/);
|
|
153
|
+
return match?.[1];
|
|
154
|
+
}
|
|
141
155
|
|
|
142
156
|
// src/LockfileManager.ts
|
|
143
157
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
|
|
@@ -300,14 +314,14 @@ var ReplayDetector = class {
|
|
|
300
314
|
const lock = this.lockManager.read();
|
|
301
315
|
const lastGen = this.getLastGeneration(lock);
|
|
302
316
|
if (!lastGen) {
|
|
303
|
-
return [];
|
|
317
|
+
return { patches: [], revertedPatchIds: [] };
|
|
304
318
|
}
|
|
305
319
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
306
320
|
if (!exists) {
|
|
307
321
|
this.warnings.push(
|
|
308
322
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Skipping new patch detection. Existing lockfile patches will still be applied.`
|
|
309
323
|
);
|
|
310
|
-
return [];
|
|
324
|
+
return { patches: [], revertedPatchIds: [] };
|
|
311
325
|
}
|
|
312
326
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
313
327
|
if (!isAncestor) {
|
|
@@ -321,7 +335,7 @@ var ReplayDetector = class {
|
|
|
321
335
|
this.sdkOutputDir
|
|
322
336
|
]);
|
|
323
337
|
if (!log.trim()) {
|
|
324
|
-
return [];
|
|
338
|
+
return { patches: [], revertedPatchIds: [] };
|
|
325
339
|
}
|
|
326
340
|
const commits = this.parseGitLog(log);
|
|
327
341
|
const newPatches = [];
|
|
@@ -357,7 +371,63 @@ var ReplayDetector = class {
|
|
|
357
371
|
patch_content: patchContent
|
|
358
372
|
});
|
|
359
373
|
}
|
|
360
|
-
|
|
374
|
+
newPatches.reverse();
|
|
375
|
+
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
376
|
+
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
377
|
+
for (let i = 0; i < newPatches.length; i++) {
|
|
378
|
+
const patch = newPatches[i];
|
|
379
|
+
if (!isRevertCommit(patch.original_message)) continue;
|
|
380
|
+
let body = "";
|
|
381
|
+
try {
|
|
382
|
+
body = await this.git.getCommitBody(patch.original_commit);
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
const revertedSha = parseRevertedSha(body);
|
|
386
|
+
const revertedMessage = parseRevertedMessage(patch.original_message);
|
|
387
|
+
let matchedExisting = false;
|
|
388
|
+
if (revertedSha) {
|
|
389
|
+
const existing = lock.patches.find((p) => p.original_commit === revertedSha);
|
|
390
|
+
if (existing) {
|
|
391
|
+
revertedPatchIdSet.add(existing.id);
|
|
392
|
+
revertIndicesToRemove.add(i);
|
|
393
|
+
matchedExisting = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (!matchedExisting && revertedMessage) {
|
|
397
|
+
const existing = lock.patches.find((p) => p.original_message === revertedMessage);
|
|
398
|
+
if (existing) {
|
|
399
|
+
revertedPatchIdSet.add(existing.id);
|
|
400
|
+
revertIndicesToRemove.add(i);
|
|
401
|
+
matchedExisting = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (matchedExisting) continue;
|
|
405
|
+
let matchedNew = false;
|
|
406
|
+
if (revertedSha) {
|
|
407
|
+
const idx = newPatches.findIndex(
|
|
408
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
|
|
409
|
+
);
|
|
410
|
+
if (idx !== -1) {
|
|
411
|
+
revertIndicesToRemove.add(i);
|
|
412
|
+
revertIndicesToRemove.add(idx);
|
|
413
|
+
matchedNew = true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!matchedNew && revertedMessage) {
|
|
417
|
+
const idx = newPatches.findIndex(
|
|
418
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
|
|
419
|
+
);
|
|
420
|
+
if (idx !== -1) {
|
|
421
|
+
revertIndicesToRemove.add(i);
|
|
422
|
+
revertIndicesToRemove.add(idx);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!matchedExisting && !matchedNew) {
|
|
426
|
+
revertIndicesToRemove.add(i);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
430
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
361
431
|
}
|
|
362
432
|
/**
|
|
363
433
|
* Compute content hash for deduplication.
|
|
@@ -368,31 +438,34 @@ var ReplayDetector = class {
|
|
|
368
438
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
369
439
|
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
370
440
|
}
|
|
371
|
-
/**
|
|
441
|
+
/**
|
|
442
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
443
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
444
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
445
|
+
*/
|
|
372
446
|
async detectPatchesViaTreeDiff(lastGen) {
|
|
373
447
|
const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
|
|
374
448
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
375
|
-
if (files.length === 0) return [];
|
|
449
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
376
450
|
const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
|
|
377
|
-
if (!diff.trim()) return [];
|
|
451
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
378
452
|
const contentHash = this.computeContentHash(diff);
|
|
379
453
|
const lock = this.lockManager.read();
|
|
380
454
|
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
381
|
-
return [];
|
|
455
|
+
return { patches: [], revertedPatchIds: [] };
|
|
382
456
|
}
|
|
383
457
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
384
|
-
|
|
385
|
-
{
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
];
|
|
458
|
+
const compositePatch = {
|
|
459
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
460
|
+
content_hash: contentHash,
|
|
461
|
+
original_commit: headSha,
|
|
462
|
+
original_message: "Customer customizations (composite)",
|
|
463
|
+
original_author: "composite",
|
|
464
|
+
base_generation: lastGen.commit_sha,
|
|
465
|
+
files,
|
|
466
|
+
patch_content: diff
|
|
467
|
+
};
|
|
468
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
396
469
|
}
|
|
397
470
|
parseGitLog(log) {
|
|
398
471
|
return log.trim().split("\n").map((line) => {
|
|
@@ -990,10 +1063,40 @@ CLI Version: ${options.cliVersion}`;
|
|
|
990
1063
|
};
|
|
991
1064
|
|
|
992
1065
|
// src/ReplayService.ts
|
|
993
|
-
init_GitClient();
|
|
994
1066
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
995
1067
|
import { join as join3 } from "path";
|
|
996
1068
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1069
|
+
init_GitClient();
|
|
1070
|
+
|
|
1071
|
+
// src/conflict-utils.ts
|
|
1072
|
+
function stripConflictMarkers(content) {
|
|
1073
|
+
const lines = content.split("\n");
|
|
1074
|
+
const result = [];
|
|
1075
|
+
let inConflict = false;
|
|
1076
|
+
let inOurs = false;
|
|
1077
|
+
for (const line of lines) {
|
|
1078
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1079
|
+
inConflict = true;
|
|
1080
|
+
inOurs = true;
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (inConflict && line === "=======") {
|
|
1084
|
+
inOurs = false;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1088
|
+
inConflict = false;
|
|
1089
|
+
inOurs = false;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (!inConflict || inOurs) {
|
|
1093
|
+
result.push(line);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return result.join("\n");
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/ReplayService.ts
|
|
997
1100
|
var ReplayService = class {
|
|
998
1101
|
git;
|
|
999
1102
|
detector;
|
|
@@ -1061,7 +1164,7 @@ var ReplayService = class {
|
|
|
1061
1164
|
}
|
|
1062
1165
|
this.lockManager.save();
|
|
1063
1166
|
try {
|
|
1064
|
-
const redetectedPatches = await this.detector.detectNewPatches();
|
|
1167
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
1065
1168
|
if (redetectedPatches.length > 0) {
|
|
1066
1169
|
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
1067
1170
|
const currentPatches = this.lockManager.getPatches();
|
|
@@ -1152,7 +1255,7 @@ var ReplayService = class {
|
|
|
1152
1255
|
};
|
|
1153
1256
|
}
|
|
1154
1257
|
async handleNoPatchesRegeneration(options) {
|
|
1155
|
-
const newPatches = await this.detector.detectNewPatches();
|
|
1258
|
+
const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1156
1259
|
const warnings = [...this.detector.warnings];
|
|
1157
1260
|
if (options?.dryRun) {
|
|
1158
1261
|
return {
|
|
@@ -1161,11 +1264,18 @@ var ReplayService = class {
|
|
|
1161
1264
|
patchesApplied: 0,
|
|
1162
1265
|
patchesWithConflicts: 0,
|
|
1163
1266
|
patchesSkipped: 0,
|
|
1267
|
+
patchesReverted: revertedPatchIds.length,
|
|
1164
1268
|
conflicts: [],
|
|
1165
1269
|
wouldApply: newPatches,
|
|
1166
1270
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
1167
1271
|
};
|
|
1168
1272
|
}
|
|
1273
|
+
for (const id of revertedPatchIds) {
|
|
1274
|
+
try {
|
|
1275
|
+
this.lockManager.removePatch(id);
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1169
1279
|
const commitOpts = options ? {
|
|
1170
1280
|
cliVersion: options.cliVersion ?? "unknown",
|
|
1171
1281
|
generatorVersions: options.generatorVersions ?? {},
|
|
@@ -1199,12 +1309,12 @@ var ReplayService = class {
|
|
|
1199
1309
|
await this.committer.stageAll();
|
|
1200
1310
|
}
|
|
1201
1311
|
}
|
|
1202
|
-
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
|
|
1312
|
+
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
|
|
1203
1313
|
}
|
|
1204
1314
|
async handleNormalRegeneration(options) {
|
|
1205
1315
|
if (options?.dryRun) {
|
|
1206
1316
|
const existingPatches2 = this.lockManager.getPatches();
|
|
1207
|
-
const newPatches2 = await this.detector.detectNewPatches();
|
|
1317
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
|
|
1208
1318
|
const warnings2 = [...this.detector.warnings];
|
|
1209
1319
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
1210
1320
|
return {
|
|
@@ -1213,13 +1323,19 @@ var ReplayService = class {
|
|
|
1213
1323
|
patchesApplied: 0,
|
|
1214
1324
|
patchesWithConflicts: 0,
|
|
1215
1325
|
patchesSkipped: 0,
|
|
1326
|
+
patchesReverted: dryRunReverted.length,
|
|
1216
1327
|
conflicts: [],
|
|
1217
1328
|
wouldApply: allPatches2,
|
|
1218
1329
|
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
1219
1330
|
};
|
|
1220
1331
|
}
|
|
1221
1332
|
let existingPatches = this.lockManager.getPatches();
|
|
1333
|
+
const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
|
|
1222
1334
|
const preRebaseCounts = await this.preGenerationRebase(existingPatches);
|
|
1335
|
+
const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
|
|
1336
|
+
const removedByPreRebase = existingPatches.filter(
|
|
1337
|
+
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
1338
|
+
);
|
|
1223
1339
|
existingPatches = this.lockManager.getPatches();
|
|
1224
1340
|
const seenHashes = /* @__PURE__ */ new Set();
|
|
1225
1341
|
for (const p of existingPatches) {
|
|
@@ -1230,8 +1346,28 @@ var ReplayService = class {
|
|
|
1230
1346
|
}
|
|
1231
1347
|
}
|
|
1232
1348
|
existingPatches = this.lockManager.getPatches();
|
|
1233
|
-
|
|
1349
|
+
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1234
1350
|
const warnings = [...this.detector.warnings];
|
|
1351
|
+
if (removedByPreRebase.length > 0) {
|
|
1352
|
+
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
1353
|
+
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
1354
|
+
newPatches = newPatches.filter((p) => {
|
|
1355
|
+
if (removedOriginalCommits.has(p.original_commit)) return false;
|
|
1356
|
+
if (isRevertCommit(p.original_message)) {
|
|
1357
|
+
const revertedMsg = parseRevertedMessage(p.original_message);
|
|
1358
|
+
if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
|
|
1359
|
+
}
|
|
1360
|
+
return true;
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
for (const id of revertedPatchIds) {
|
|
1364
|
+
try {
|
|
1365
|
+
this.lockManager.removePatch(id);
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const revertedSet = new Set(revertedPatchIds);
|
|
1370
|
+
existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
|
|
1235
1371
|
const allPatches = [...existingPatches, ...newPatches];
|
|
1236
1372
|
const commitOpts = options ? {
|
|
1237
1373
|
cliVersion: options.cliVersion ?? "unknown",
|
|
@@ -1276,7 +1412,8 @@ var ReplayService = class {
|
|
|
1276
1412
|
options,
|
|
1277
1413
|
warnings,
|
|
1278
1414
|
rebaseCounts,
|
|
1279
|
-
preRebaseCounts
|
|
1415
|
+
preRebaseCounts,
|
|
1416
|
+
revertedPatchIds.length
|
|
1280
1417
|
);
|
|
1281
1418
|
}
|
|
1282
1419
|
/**
|
|
@@ -1469,36 +1606,6 @@ var ReplayService = class {
|
|
|
1469
1606
|
}
|
|
1470
1607
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
1471
1608
|
}
|
|
1472
|
-
/**
|
|
1473
|
-
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
1474
|
-
* Preserves clean patches' non-conflicting changes on shared files.
|
|
1475
|
-
*/
|
|
1476
|
-
stripConflictMarkers(content) {
|
|
1477
|
-
const lines = content.split("\n");
|
|
1478
|
-
const result = [];
|
|
1479
|
-
let inConflict = false;
|
|
1480
|
-
let inOurs = false;
|
|
1481
|
-
for (const line of lines) {
|
|
1482
|
-
if (line.startsWith("<<<<<<< ")) {
|
|
1483
|
-
inConflict = true;
|
|
1484
|
-
inOurs = true;
|
|
1485
|
-
continue;
|
|
1486
|
-
}
|
|
1487
|
-
if (inConflict && line === "=======") {
|
|
1488
|
-
inOurs = false;
|
|
1489
|
-
continue;
|
|
1490
|
-
}
|
|
1491
|
-
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1492
|
-
inConflict = false;
|
|
1493
|
-
inOurs = false;
|
|
1494
|
-
continue;
|
|
1495
|
-
}
|
|
1496
|
-
if (!inConflict || inOurs) {
|
|
1497
|
-
result.push(line);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
return result.join("\n");
|
|
1501
|
-
}
|
|
1502
1609
|
/**
|
|
1503
1610
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
1504
1611
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -1511,7 +1618,7 @@ var ReplayService = class {
|
|
|
1511
1618
|
const filePath = join3(this.outputDir, fileResult.file);
|
|
1512
1619
|
try {
|
|
1513
1620
|
const content = readFileSync2(filePath, "utf-8");
|
|
1514
|
-
const stripped =
|
|
1621
|
+
const stripped = stripConflictMarkers(content);
|
|
1515
1622
|
writeFileSync2(filePath, stripped);
|
|
1516
1623
|
} catch {
|
|
1517
1624
|
}
|
|
@@ -1523,7 +1630,7 @@ var ReplayService = class {
|
|
|
1523
1630
|
if (!existsSync2(fernignorePath)) return [];
|
|
1524
1631
|
return readFileSync2(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1525
1632
|
}
|
|
1526
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
|
|
1633
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
1527
1634
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
1528
1635
|
const conflictDetails = conflictResults.map((r) => {
|
|
1529
1636
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -1548,6 +1655,7 @@ var ReplayService = class {
|
|
|
1548
1655
|
patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
|
|
1549
1656
|
patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
|
|
1550
1657
|
patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
|
|
1658
|
+
patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
|
|
1551
1659
|
patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
|
|
1552
1660
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
1553
1661
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
@@ -1594,7 +1702,7 @@ var FernignoreMigrator = class {
|
|
|
1594
1702
|
async analyzeMigration() {
|
|
1595
1703
|
const patterns = this.readFernignorePatterns();
|
|
1596
1704
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1597
|
-
const patches = await detector.detectNewPatches();
|
|
1705
|
+
const { patches } = await detector.detectNewPatches();
|
|
1598
1706
|
const trackedByBoth = [];
|
|
1599
1707
|
const fernignoreOnly = [];
|
|
1600
1708
|
const commitsOnly = [];
|
|
@@ -1719,7 +1827,7 @@ var FernignoreMigrator = class {
|
|
|
1719
1827
|
async migrate() {
|
|
1720
1828
|
const analysis = await this.analyzeMigration();
|
|
1721
1829
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1722
|
-
const patches = await detector.detectNewPatches();
|
|
1830
|
+
const { patches } = await detector.detectNewPatches();
|
|
1723
1831
|
const warnings = [];
|
|
1724
1832
|
let patchesCreated = 0;
|
|
1725
1833
|
for (const patch of patches) {
|
|
@@ -2135,6 +2243,9 @@ export {
|
|
|
2135
2243
|
forget,
|
|
2136
2244
|
isGenerationCommit,
|
|
2137
2245
|
isReplayCommit,
|
|
2246
|
+
isRevertCommit,
|
|
2247
|
+
parseRevertedMessage,
|
|
2248
|
+
parseRevertedSha,
|
|
2138
2249
|
reset,
|
|
2139
2250
|
resolve,
|
|
2140
2251
|
status,
|