@fern-api/replay 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +170 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +171 -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 +169 -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 indicates a git 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 indicates a git 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 message.startsWith('Revert "');
|
|
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,60 @@ 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
|
+
}
|
|
426
|
+
const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
427
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
361
428
|
}
|
|
362
429
|
/**
|
|
363
430
|
* Compute content hash for deduplication.
|
|
@@ -368,31 +435,34 @@ var ReplayDetector = class {
|
|
|
368
435
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
369
436
|
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
370
437
|
}
|
|
371
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
440
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
441
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
442
|
+
*/
|
|
372
443
|
async detectPatchesViaTreeDiff(lastGen) {
|
|
373
444
|
const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
|
|
374
445
|
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 [];
|
|
446
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
376
447
|
const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
|
|
377
|
-
if (!diff.trim()) return [];
|
|
448
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
378
449
|
const contentHash = this.computeContentHash(diff);
|
|
379
450
|
const lock = this.lockManager.read();
|
|
380
451
|
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
381
|
-
return [];
|
|
452
|
+
return { patches: [], revertedPatchIds: [] };
|
|
382
453
|
}
|
|
383
454
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
384
|
-
|
|
385
|
-
{
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
];
|
|
455
|
+
const compositePatch = {
|
|
456
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
457
|
+
content_hash: contentHash,
|
|
458
|
+
original_commit: headSha,
|
|
459
|
+
original_message: "Customer customizations (composite)",
|
|
460
|
+
original_author: "composite",
|
|
461
|
+
base_generation: lastGen.commit_sha,
|
|
462
|
+
files,
|
|
463
|
+
patch_content: diff
|
|
464
|
+
};
|
|
465
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
396
466
|
}
|
|
397
467
|
parseGitLog(log) {
|
|
398
468
|
return log.trim().split("\n").map((line) => {
|
|
@@ -990,10 +1060,40 @@ CLI Version: ${options.cliVersion}`;
|
|
|
990
1060
|
};
|
|
991
1061
|
|
|
992
1062
|
// src/ReplayService.ts
|
|
993
|
-
init_GitClient();
|
|
994
1063
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
995
1064
|
import { join as join3 } from "path";
|
|
996
1065
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1066
|
+
init_GitClient();
|
|
1067
|
+
|
|
1068
|
+
// src/conflict-utils.ts
|
|
1069
|
+
function stripConflictMarkers(content) {
|
|
1070
|
+
const lines = content.split("\n");
|
|
1071
|
+
const result = [];
|
|
1072
|
+
let inConflict = false;
|
|
1073
|
+
let inOurs = false;
|
|
1074
|
+
for (const line of lines) {
|
|
1075
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1076
|
+
inConflict = true;
|
|
1077
|
+
inOurs = true;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (inConflict && line === "=======") {
|
|
1081
|
+
inOurs = false;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1085
|
+
inConflict = false;
|
|
1086
|
+
inOurs = false;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (!inConflict || inOurs) {
|
|
1090
|
+
result.push(line);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return result.join("\n");
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/ReplayService.ts
|
|
997
1097
|
var ReplayService = class {
|
|
998
1098
|
git;
|
|
999
1099
|
detector;
|
|
@@ -1061,7 +1161,7 @@ var ReplayService = class {
|
|
|
1061
1161
|
}
|
|
1062
1162
|
this.lockManager.save();
|
|
1063
1163
|
try {
|
|
1064
|
-
const redetectedPatches = await this.detector.detectNewPatches();
|
|
1164
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
1065
1165
|
if (redetectedPatches.length > 0) {
|
|
1066
1166
|
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
1067
1167
|
const currentPatches = this.lockManager.getPatches();
|
|
@@ -1152,7 +1252,7 @@ var ReplayService = class {
|
|
|
1152
1252
|
};
|
|
1153
1253
|
}
|
|
1154
1254
|
async handleNoPatchesRegeneration(options) {
|
|
1155
|
-
const newPatches = await this.detector.detectNewPatches();
|
|
1255
|
+
const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1156
1256
|
const warnings = [...this.detector.warnings];
|
|
1157
1257
|
if (options?.dryRun) {
|
|
1158
1258
|
return {
|
|
@@ -1161,11 +1261,18 @@ var ReplayService = class {
|
|
|
1161
1261
|
patchesApplied: 0,
|
|
1162
1262
|
patchesWithConflicts: 0,
|
|
1163
1263
|
patchesSkipped: 0,
|
|
1264
|
+
patchesReverted: revertedPatchIds.length,
|
|
1164
1265
|
conflicts: [],
|
|
1165
1266
|
wouldApply: newPatches,
|
|
1166
1267
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
1167
1268
|
};
|
|
1168
1269
|
}
|
|
1270
|
+
for (const id of revertedPatchIds) {
|
|
1271
|
+
try {
|
|
1272
|
+
this.lockManager.removePatch(id);
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1169
1276
|
const commitOpts = options ? {
|
|
1170
1277
|
cliVersion: options.cliVersion ?? "unknown",
|
|
1171
1278
|
generatorVersions: options.generatorVersions ?? {},
|
|
@@ -1199,12 +1306,12 @@ var ReplayService = class {
|
|
|
1199
1306
|
await this.committer.stageAll();
|
|
1200
1307
|
}
|
|
1201
1308
|
}
|
|
1202
|
-
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
|
|
1309
|
+
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
|
|
1203
1310
|
}
|
|
1204
1311
|
async handleNormalRegeneration(options) {
|
|
1205
1312
|
if (options?.dryRun) {
|
|
1206
1313
|
const existingPatches2 = this.lockManager.getPatches();
|
|
1207
|
-
const newPatches2 = await this.detector.detectNewPatches();
|
|
1314
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
|
|
1208
1315
|
const warnings2 = [...this.detector.warnings];
|
|
1209
1316
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
1210
1317
|
return {
|
|
@@ -1213,13 +1320,19 @@ var ReplayService = class {
|
|
|
1213
1320
|
patchesApplied: 0,
|
|
1214
1321
|
patchesWithConflicts: 0,
|
|
1215
1322
|
patchesSkipped: 0,
|
|
1323
|
+
patchesReverted: dryRunReverted.length,
|
|
1216
1324
|
conflicts: [],
|
|
1217
1325
|
wouldApply: allPatches2,
|
|
1218
1326
|
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
1219
1327
|
};
|
|
1220
1328
|
}
|
|
1221
1329
|
let existingPatches = this.lockManager.getPatches();
|
|
1330
|
+
const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
|
|
1222
1331
|
const preRebaseCounts = await this.preGenerationRebase(existingPatches);
|
|
1332
|
+
const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
|
|
1333
|
+
const removedByPreRebase = existingPatches.filter(
|
|
1334
|
+
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
1335
|
+
);
|
|
1223
1336
|
existingPatches = this.lockManager.getPatches();
|
|
1224
1337
|
const seenHashes = /* @__PURE__ */ new Set();
|
|
1225
1338
|
for (const p of existingPatches) {
|
|
@@ -1230,8 +1343,28 @@ var ReplayService = class {
|
|
|
1230
1343
|
}
|
|
1231
1344
|
}
|
|
1232
1345
|
existingPatches = this.lockManager.getPatches();
|
|
1233
|
-
|
|
1346
|
+
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1234
1347
|
const warnings = [...this.detector.warnings];
|
|
1348
|
+
if (removedByPreRebase.length > 0) {
|
|
1349
|
+
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
1350
|
+
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
1351
|
+
newPatches = newPatches.filter((p) => {
|
|
1352
|
+
if (removedOriginalCommits.has(p.original_commit)) return false;
|
|
1353
|
+
if (isRevertCommit(p.original_message)) {
|
|
1354
|
+
const revertedMsg = parseRevertedMessage(p.original_message);
|
|
1355
|
+
if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
|
|
1356
|
+
}
|
|
1357
|
+
return true;
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
for (const id of revertedPatchIds) {
|
|
1361
|
+
try {
|
|
1362
|
+
this.lockManager.removePatch(id);
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const revertedSet = new Set(revertedPatchIds);
|
|
1367
|
+
existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
|
|
1235
1368
|
const allPatches = [...existingPatches, ...newPatches];
|
|
1236
1369
|
const commitOpts = options ? {
|
|
1237
1370
|
cliVersion: options.cliVersion ?? "unknown",
|
|
@@ -1276,7 +1409,8 @@ var ReplayService = class {
|
|
|
1276
1409
|
options,
|
|
1277
1410
|
warnings,
|
|
1278
1411
|
rebaseCounts,
|
|
1279
|
-
preRebaseCounts
|
|
1412
|
+
preRebaseCounts,
|
|
1413
|
+
revertedPatchIds.length
|
|
1280
1414
|
);
|
|
1281
1415
|
}
|
|
1282
1416
|
/**
|
|
@@ -1469,36 +1603,6 @@ var ReplayService = class {
|
|
|
1469
1603
|
}
|
|
1470
1604
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
1471
1605
|
}
|
|
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
1606
|
/**
|
|
1503
1607
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
1504
1608
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -1511,7 +1615,7 @@ var ReplayService = class {
|
|
|
1511
1615
|
const filePath = join3(this.outputDir, fileResult.file);
|
|
1512
1616
|
try {
|
|
1513
1617
|
const content = readFileSync2(filePath, "utf-8");
|
|
1514
|
-
const stripped =
|
|
1618
|
+
const stripped = stripConflictMarkers(content);
|
|
1515
1619
|
writeFileSync2(filePath, stripped);
|
|
1516
1620
|
} catch {
|
|
1517
1621
|
}
|
|
@@ -1523,7 +1627,7 @@ var ReplayService = class {
|
|
|
1523
1627
|
if (!existsSync2(fernignorePath)) return [];
|
|
1524
1628
|
return readFileSync2(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1525
1629
|
}
|
|
1526
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
|
|
1630
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
1527
1631
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
1528
1632
|
const conflictDetails = conflictResults.map((r) => {
|
|
1529
1633
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -1548,6 +1652,7 @@ var ReplayService = class {
|
|
|
1548
1652
|
patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
|
|
1549
1653
|
patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
|
|
1550
1654
|
patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
|
|
1655
|
+
patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
|
|
1551
1656
|
patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
|
|
1552
1657
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
1553
1658
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
@@ -1594,7 +1699,7 @@ var FernignoreMigrator = class {
|
|
|
1594
1699
|
async analyzeMigration() {
|
|
1595
1700
|
const patterns = this.readFernignorePatterns();
|
|
1596
1701
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1597
|
-
const patches = await detector.detectNewPatches();
|
|
1702
|
+
const { patches } = await detector.detectNewPatches();
|
|
1598
1703
|
const trackedByBoth = [];
|
|
1599
1704
|
const fernignoreOnly = [];
|
|
1600
1705
|
const commitsOnly = [];
|
|
@@ -1719,7 +1824,7 @@ var FernignoreMigrator = class {
|
|
|
1719
1824
|
async migrate() {
|
|
1720
1825
|
const analysis = await this.analyzeMigration();
|
|
1721
1826
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1722
|
-
const patches = await detector.detectNewPatches();
|
|
1827
|
+
const { patches } = await detector.detectNewPatches();
|
|
1723
1828
|
const warnings = [];
|
|
1724
1829
|
let patchesCreated = 0;
|
|
1725
1830
|
for (const patch of patches) {
|
|
@@ -2135,6 +2240,9 @@ export {
|
|
|
2135
2240
|
forget,
|
|
2136
2241
|
isGenerationCommit,
|
|
2137
2242
|
isReplayCommit,
|
|
2243
|
+
isRevertCommit,
|
|
2244
|
+
parseRevertedMessage,
|
|
2245
|
+
parseRevertedSha,
|
|
2138
2246
|
reset,
|
|
2139
2247
|
resolve,
|
|
2140
2248
|
status,
|