@fern-api/replay 0.8.1 → 0.9.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 +492 -126
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +246 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -16
- package/dist/index.d.ts +61 -16
- package/dist/index.js +246 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -3,6 +3,7 @@ interface GenerationLock {
|
|
|
3
3
|
generations: GenerationRecord[];
|
|
4
4
|
current_generation: string;
|
|
5
5
|
patches: StoredPatch[];
|
|
6
|
+
forgotten_hashes?: string[];
|
|
6
7
|
replay_skipped_at?: string;
|
|
7
8
|
}
|
|
8
9
|
interface GenerationRecord {
|
|
@@ -137,6 +138,7 @@ declare class LockfileManager {
|
|
|
137
138
|
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
|
|
138
139
|
removePatch(patchId: string): void;
|
|
139
140
|
clearPatches(): void;
|
|
141
|
+
addForgottenHash(hash: string): void;
|
|
140
142
|
getUnresolvedPatches(): StoredPatch[];
|
|
141
143
|
getResolvingPatches(): StoredPatch[];
|
|
142
144
|
markPatchUnresolved(patchId: string): void;
|
|
@@ -426,18 +428,43 @@ declare function bootstrap(outputDir: string, options?: BootstrapOptions): Promi
|
|
|
426
428
|
interface ForgetOptions {
|
|
427
429
|
/** Don't actually remove, just show what would be removed */
|
|
428
430
|
dryRun?: boolean;
|
|
431
|
+
/** Remove all tracked patches (keep lockfile and generation history) */
|
|
432
|
+
all?: boolean;
|
|
433
|
+
/** Specific patch IDs to remove */
|
|
434
|
+
patchIds?: string[];
|
|
435
|
+
/** Search pattern: file path, glob, or commit message substring */
|
|
436
|
+
pattern?: string;
|
|
437
|
+
}
|
|
438
|
+
interface DiffStat {
|
|
439
|
+
additions: number;
|
|
440
|
+
deletions: number;
|
|
441
|
+
}
|
|
442
|
+
interface MatchedPatch {
|
|
443
|
+
id: string;
|
|
444
|
+
message: string;
|
|
445
|
+
files: string[];
|
|
446
|
+
diffstat: DiffStat;
|
|
447
|
+
status?: "unresolved" | "resolving";
|
|
429
448
|
}
|
|
430
449
|
interface ForgetResult {
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
/** True if
|
|
450
|
+
/** Whether replay is initialized (lockfile exists) */
|
|
451
|
+
initialized: boolean;
|
|
452
|
+
/** Patches that were (or would be in dry-run) removed */
|
|
453
|
+
removed: MatchedPatch[];
|
|
454
|
+
/** Number of patches remaining after removal */
|
|
455
|
+
remaining: number;
|
|
456
|
+
/** True if a search/pattern was given but nothing matched */
|
|
438
457
|
notFound: boolean;
|
|
458
|
+
/** Patch IDs that were specified but don't exist (idempotent mode) */
|
|
459
|
+
alreadyForgotten: string[];
|
|
460
|
+
/** Total patches before removal */
|
|
461
|
+
totalPatches: number;
|
|
462
|
+
/** Warnings (e.g., forgetting patches with conflict markers on disk) */
|
|
463
|
+
warnings: string[];
|
|
464
|
+
/** Matching patches for interactive selection (search/no-arg mode only) */
|
|
465
|
+
matched?: MatchedPatch[];
|
|
439
466
|
}
|
|
440
|
-
declare function forget(outputDir: string,
|
|
467
|
+
declare function forget(outputDir: string, options?: ForgetOptions): ForgetResult;
|
|
441
468
|
|
|
442
469
|
interface ResetOptions {
|
|
443
470
|
/** Don't actually delete, just show what would happen */
|
|
@@ -480,27 +507,45 @@ declare function resolve(outputDir: string, options?: ResolveOptions): Promise<R
|
|
|
480
507
|
interface StatusResult {
|
|
481
508
|
/** Whether replay is initialized (lockfile exists) */
|
|
482
509
|
initialized: boolean;
|
|
483
|
-
/**
|
|
484
|
-
|
|
510
|
+
/** Total number of generations tracked */
|
|
511
|
+
generationCount: number;
|
|
485
512
|
/** Last generation info, if available */
|
|
486
513
|
lastGeneration: StatusGeneration | undefined;
|
|
514
|
+
/** Tracked customization patches */
|
|
515
|
+
patches: StatusPatch[];
|
|
516
|
+
/** Count of patches with "unresolved" or "resolving" status */
|
|
517
|
+
unresolvedCount: number;
|
|
518
|
+
/** Exclude patterns from replay.yml */
|
|
519
|
+
excludePatterns: string[];
|
|
487
520
|
}
|
|
488
521
|
interface StatusPatch {
|
|
489
|
-
/**
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
|
|
522
|
+
/** Patch ID (e.g. "patch-def45678") */
|
|
523
|
+
id: string;
|
|
524
|
+
/** "added" if patch_content contains "new file mode", otherwise "modified" */
|
|
525
|
+
type: "added" | "modified";
|
|
493
526
|
/** Original commit message */
|
|
494
527
|
message: string;
|
|
528
|
+
/** Author name (without email) */
|
|
529
|
+
author: string;
|
|
530
|
+
/** Short SHA of the original commit (7 chars) */
|
|
531
|
+
sha: string;
|
|
495
532
|
/** Files touched by this patch */
|
|
496
533
|
files: string[];
|
|
534
|
+
/** Number of files */
|
|
535
|
+
fileCount: number;
|
|
536
|
+
/** Patch resolution status, if any */
|
|
537
|
+
status?: "unresolved" | "resolving";
|
|
497
538
|
}
|
|
498
539
|
interface StatusGeneration {
|
|
499
|
-
/**
|
|
540
|
+
/** Short commit SHA (7 chars) */
|
|
500
541
|
sha: string;
|
|
501
542
|
/** Generation timestamp */
|
|
502
543
|
timestamp: string;
|
|
544
|
+
/** CLI version used for generation */
|
|
545
|
+
cliVersion: string;
|
|
546
|
+
/** Generator versions (e.g. { "fern-java-sdk": "3.35.0" }) */
|
|
547
|
+
generatorVersions: Record<string, string>;
|
|
503
548
|
}
|
|
504
549
|
declare function status(outputDir: string): StatusResult;
|
|
505
550
|
|
|
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 };
|
|
551
|
+
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, type DiffStat, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MatchedPatch, 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
|
@@ -3,6 +3,7 @@ interface GenerationLock {
|
|
|
3
3
|
generations: GenerationRecord[];
|
|
4
4
|
current_generation: string;
|
|
5
5
|
patches: StoredPatch[];
|
|
6
|
+
forgotten_hashes?: string[];
|
|
6
7
|
replay_skipped_at?: string;
|
|
7
8
|
}
|
|
8
9
|
interface GenerationRecord {
|
|
@@ -137,6 +138,7 @@ declare class LockfileManager {
|
|
|
137
138
|
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
|
|
138
139
|
removePatch(patchId: string): void;
|
|
139
140
|
clearPatches(): void;
|
|
141
|
+
addForgottenHash(hash: string): void;
|
|
140
142
|
getUnresolvedPatches(): StoredPatch[];
|
|
141
143
|
getResolvingPatches(): StoredPatch[];
|
|
142
144
|
markPatchUnresolved(patchId: string): void;
|
|
@@ -426,18 +428,43 @@ declare function bootstrap(outputDir: string, options?: BootstrapOptions): Promi
|
|
|
426
428
|
interface ForgetOptions {
|
|
427
429
|
/** Don't actually remove, just show what would be removed */
|
|
428
430
|
dryRun?: boolean;
|
|
431
|
+
/** Remove all tracked patches (keep lockfile and generation history) */
|
|
432
|
+
all?: boolean;
|
|
433
|
+
/** Specific patch IDs to remove */
|
|
434
|
+
patchIds?: string[];
|
|
435
|
+
/** Search pattern: file path, glob, or commit message substring */
|
|
436
|
+
pattern?: string;
|
|
437
|
+
}
|
|
438
|
+
interface DiffStat {
|
|
439
|
+
additions: number;
|
|
440
|
+
deletions: number;
|
|
441
|
+
}
|
|
442
|
+
interface MatchedPatch {
|
|
443
|
+
id: string;
|
|
444
|
+
message: string;
|
|
445
|
+
files: string[];
|
|
446
|
+
diffstat: DiffStat;
|
|
447
|
+
status?: "unresolved" | "resolving";
|
|
429
448
|
}
|
|
430
449
|
interface ForgetResult {
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
/** True if
|
|
450
|
+
/** Whether replay is initialized (lockfile exists) */
|
|
451
|
+
initialized: boolean;
|
|
452
|
+
/** Patches that were (or would be in dry-run) removed */
|
|
453
|
+
removed: MatchedPatch[];
|
|
454
|
+
/** Number of patches remaining after removal */
|
|
455
|
+
remaining: number;
|
|
456
|
+
/** True if a search/pattern was given but nothing matched */
|
|
438
457
|
notFound: boolean;
|
|
458
|
+
/** Patch IDs that were specified but don't exist (idempotent mode) */
|
|
459
|
+
alreadyForgotten: string[];
|
|
460
|
+
/** Total patches before removal */
|
|
461
|
+
totalPatches: number;
|
|
462
|
+
/** Warnings (e.g., forgetting patches with conflict markers on disk) */
|
|
463
|
+
warnings: string[];
|
|
464
|
+
/** Matching patches for interactive selection (search/no-arg mode only) */
|
|
465
|
+
matched?: MatchedPatch[];
|
|
439
466
|
}
|
|
440
|
-
declare function forget(outputDir: string,
|
|
467
|
+
declare function forget(outputDir: string, options?: ForgetOptions): ForgetResult;
|
|
441
468
|
|
|
442
469
|
interface ResetOptions {
|
|
443
470
|
/** Don't actually delete, just show what would happen */
|
|
@@ -480,27 +507,45 @@ declare function resolve(outputDir: string, options?: ResolveOptions): Promise<R
|
|
|
480
507
|
interface StatusResult {
|
|
481
508
|
/** Whether replay is initialized (lockfile exists) */
|
|
482
509
|
initialized: boolean;
|
|
483
|
-
/**
|
|
484
|
-
|
|
510
|
+
/** Total number of generations tracked */
|
|
511
|
+
generationCount: number;
|
|
485
512
|
/** Last generation info, if available */
|
|
486
513
|
lastGeneration: StatusGeneration | undefined;
|
|
514
|
+
/** Tracked customization patches */
|
|
515
|
+
patches: StatusPatch[];
|
|
516
|
+
/** Count of patches with "unresolved" or "resolving" status */
|
|
517
|
+
unresolvedCount: number;
|
|
518
|
+
/** Exclude patterns from replay.yml */
|
|
519
|
+
excludePatterns: string[];
|
|
487
520
|
}
|
|
488
521
|
interface StatusPatch {
|
|
489
|
-
/**
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
|
|
522
|
+
/** Patch ID (e.g. "patch-def45678") */
|
|
523
|
+
id: string;
|
|
524
|
+
/** "added" if patch_content contains "new file mode", otherwise "modified" */
|
|
525
|
+
type: "added" | "modified";
|
|
493
526
|
/** Original commit message */
|
|
494
527
|
message: string;
|
|
528
|
+
/** Author name (without email) */
|
|
529
|
+
author: string;
|
|
530
|
+
/** Short SHA of the original commit (7 chars) */
|
|
531
|
+
sha: string;
|
|
495
532
|
/** Files touched by this patch */
|
|
496
533
|
files: string[];
|
|
534
|
+
/** Number of files */
|
|
535
|
+
fileCount: number;
|
|
536
|
+
/** Patch resolution status, if any */
|
|
537
|
+
status?: "unresolved" | "resolving";
|
|
497
538
|
}
|
|
498
539
|
interface StatusGeneration {
|
|
499
|
-
/**
|
|
540
|
+
/** Short commit SHA (7 chars) */
|
|
500
541
|
sha: string;
|
|
501
542
|
/** Generation timestamp */
|
|
502
543
|
timestamp: string;
|
|
544
|
+
/** CLI version used for generation */
|
|
545
|
+
cliVersion: string;
|
|
546
|
+
/** Generator versions (e.g. { "fern-java-sdk": "3.35.0" }) */
|
|
547
|
+
generatorVersions: Record<string, string>;
|
|
503
548
|
}
|
|
504
549
|
declare function status(outputDir: string): StatusResult;
|
|
505
550
|
|
|
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 };
|
|
551
|
+
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, type DiffStat, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MatchedPatch, 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
|
@@ -243,6 +243,15 @@ var LockfileManager = class {
|
|
|
243
243
|
this.ensureLoaded();
|
|
244
244
|
this.lock.patches = [];
|
|
245
245
|
}
|
|
246
|
+
addForgottenHash(hash) {
|
|
247
|
+
this.ensureLoaded();
|
|
248
|
+
if (!this.lock.forgotten_hashes) {
|
|
249
|
+
this.lock.forgotten_hashes = [];
|
|
250
|
+
}
|
|
251
|
+
if (!this.lock.forgotten_hashes.includes(hash)) {
|
|
252
|
+
this.lock.forgotten_hashes.push(hash);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
246
255
|
getUnresolvedPatches() {
|
|
247
256
|
this.ensureLoaded();
|
|
248
257
|
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
@@ -339,6 +348,7 @@ var ReplayDetector = class {
|
|
|
339
348
|
}
|
|
340
349
|
const commits = this.parseGitLog(log);
|
|
341
350
|
const newPatches = [];
|
|
351
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
342
352
|
for (const commit of commits) {
|
|
343
353
|
if (isGenerationCommit(commit)) {
|
|
344
354
|
continue;
|
|
@@ -352,7 +362,7 @@ var ReplayDetector = class {
|
|
|
352
362
|
}
|
|
353
363
|
const patchContent = await this.git.formatPatch(commit.sha);
|
|
354
364
|
const contentHash = this.computeContentHash(patchContent);
|
|
355
|
-
if (lock.patches.find((p) => p.content_hash === contentHash)) {
|
|
365
|
+
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
356
366
|
continue;
|
|
357
367
|
}
|
|
358
368
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
@@ -451,7 +461,7 @@ var ReplayDetector = class {
|
|
|
451
461
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
452
462
|
const contentHash = this.computeContentHash(diff);
|
|
453
463
|
const lock = this.lockManager.read();
|
|
454
|
-
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
464
|
+
if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
455
465
|
return { patches: [], revertedPatchIds: [] };
|
|
456
466
|
}
|
|
457
467
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
@@ -521,6 +531,36 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
|
521
531
|
import { tmpdir } from "os";
|
|
522
532
|
import { dirname as dirname2, extname, join as join2 } from "path";
|
|
523
533
|
import { minimatch } from "minimatch";
|
|
534
|
+
|
|
535
|
+
// src/conflict-utils.ts
|
|
536
|
+
function stripConflictMarkers(content) {
|
|
537
|
+
const lines = content.split("\n");
|
|
538
|
+
const result = [];
|
|
539
|
+
let inConflict = false;
|
|
540
|
+
let inOurs = false;
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
543
|
+
inConflict = true;
|
|
544
|
+
inOurs = true;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (inConflict && line === "=======") {
|
|
548
|
+
inOurs = false;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
552
|
+
inConflict = false;
|
|
553
|
+
inOurs = false;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (!inConflict || inOurs) {
|
|
557
|
+
result.push(line);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return result.join("\n");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/ReplayApplicator.ts
|
|
524
564
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
525
565
|
".png",
|
|
526
566
|
".jpg",
|
|
@@ -592,7 +632,8 @@ var ReplayApplicator = class {
|
|
|
592
632
|
async applyPatches(patches) {
|
|
593
633
|
this.resetAccumulator();
|
|
594
634
|
const results = [];
|
|
595
|
-
for (
|
|
635
|
+
for (let i = 0; i < patches.length; i++) {
|
|
636
|
+
const patch = patches[i];
|
|
596
637
|
if (this.isExcluded(patch)) {
|
|
597
638
|
results.push({
|
|
598
639
|
patch,
|
|
@@ -603,6 +644,33 @@ var ReplayApplicator = class {
|
|
|
603
644
|
}
|
|
604
645
|
const result = await this.applyPatchWithFallback(patch);
|
|
605
646
|
results.push(result);
|
|
647
|
+
if (result.status === "conflict" && result.fileResults) {
|
|
648
|
+
const laterFiles = /* @__PURE__ */ new Set();
|
|
649
|
+
for (let j = i + 1; j < patches.length; j++) {
|
|
650
|
+
for (const f of patches[j].files) {
|
|
651
|
+
laterFiles.add(f);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
655
|
+
if (result.resolvedFiles) {
|
|
656
|
+
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
657
|
+
resolvedToOriginal.set(resolved, orig);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const fileResult of result.fileResults) {
|
|
661
|
+
if (fileResult.status !== "conflict") continue;
|
|
662
|
+
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
663
|
+
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
664
|
+
const filePath = join2(this.outputDir, fileResult.file);
|
|
665
|
+
try {
|
|
666
|
+
const content = await readFile(filePath, "utf-8");
|
|
667
|
+
const stripped = stripConflictMarkers(content);
|
|
668
|
+
await writeFile(filePath, stripped);
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
606
674
|
}
|
|
607
675
|
return results;
|
|
608
676
|
}
|
|
@@ -621,7 +689,7 @@ var ReplayApplicator = class {
|
|
|
621
689
|
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
622
690
|
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
623
691
|
const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
624
|
-
if (theirs
|
|
692
|
+
if (theirs) {
|
|
625
693
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
626
694
|
content: theirs,
|
|
627
695
|
baseGeneration: patch.base_generation
|
|
@@ -753,7 +821,7 @@ var ReplayApplicator = class {
|
|
|
753
821
|
);
|
|
754
822
|
let useAccumulatorAsMergeBase = false;
|
|
755
823
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
756
|
-
if (!theirs &&
|
|
824
|
+
if (!theirs && accumulatorEntry) {
|
|
757
825
|
theirs = await this.applyPatchToContent(
|
|
758
826
|
accumulatorEntry.content,
|
|
759
827
|
patch.patch_content,
|
|
@@ -812,7 +880,7 @@ var ReplayApplicator = class {
|
|
|
812
880
|
reason: "missing-content"
|
|
813
881
|
};
|
|
814
882
|
}
|
|
815
|
-
if (!base || !ours) {
|
|
883
|
+
if (!base && !useAccumulatorAsMergeBase || !ours) {
|
|
816
884
|
return {
|
|
817
885
|
file: resolvedPath,
|
|
818
886
|
status: "skipped",
|
|
@@ -820,11 +888,18 @@ var ReplayApplicator = class {
|
|
|
820
888
|
};
|
|
821
889
|
}
|
|
822
890
|
const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
|
|
891
|
+
if (mergeBase == null) {
|
|
892
|
+
return {
|
|
893
|
+
file: resolvedPath,
|
|
894
|
+
status: "skipped",
|
|
895
|
+
reason: "missing-content"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
823
898
|
const merged = threeWayMerge(mergeBase, ours, effective_theirs);
|
|
824
899
|
const outDir = dirname2(oursPath);
|
|
825
900
|
await mkdir(outDir, { recursive: true });
|
|
826
901
|
await writeFile(oursPath, merged.content);
|
|
827
|
-
if (effective_theirs
|
|
902
|
+
if (effective_theirs) {
|
|
828
903
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
829
904
|
content: effective_theirs,
|
|
830
905
|
baseGeneration: patch.base_generation
|
|
@@ -1067,36 +1142,6 @@ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync
|
|
|
1067
1142
|
import { join as join3 } from "path";
|
|
1068
1143
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1069
1144
|
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
|
|
1100
1145
|
var ReplayService = class {
|
|
1101
1146
|
git;
|
|
1102
1147
|
detector;
|
|
@@ -2064,30 +2109,145 @@ function computeContentHash(patchContent) {
|
|
|
2064
2109
|
|
|
2065
2110
|
// src/commands/forget.ts
|
|
2066
2111
|
import { minimatch as minimatch4 } from "minimatch";
|
|
2067
|
-
function
|
|
2112
|
+
function parseDiffStat(patchContent) {
|
|
2113
|
+
let additions = 0;
|
|
2114
|
+
let deletions = 0;
|
|
2115
|
+
let inDiffHunk = false;
|
|
2116
|
+
for (const line of patchContent.split("\n")) {
|
|
2117
|
+
if (line.startsWith("diff --git ")) {
|
|
2118
|
+
inDiffHunk = true;
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
if (!inDiffHunk) continue;
|
|
2122
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2123
|
+
additions++;
|
|
2124
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2125
|
+
deletions++;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
return { additions, deletions };
|
|
2129
|
+
}
|
|
2130
|
+
function toMatchedPatch(patch) {
|
|
2131
|
+
return {
|
|
2132
|
+
id: patch.id,
|
|
2133
|
+
message: patch.original_message,
|
|
2134
|
+
files: patch.files,
|
|
2135
|
+
diffstat: parseDiffStat(patch.patch_content),
|
|
2136
|
+
...patch.status ? { status: patch.status } : {}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
function matchesPatch(patch, pattern) {
|
|
2140
|
+
const fileMatch = patch.files.some(
|
|
2141
|
+
(file) => file === pattern || minimatch4(file, pattern)
|
|
2142
|
+
);
|
|
2143
|
+
if (fileMatch) return true;
|
|
2144
|
+
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|
|
2145
|
+
}
|
|
2146
|
+
function buildWarnings(patches) {
|
|
2147
|
+
const warnings = [];
|
|
2148
|
+
for (const patch of patches) {
|
|
2149
|
+
if (patch.status === "resolving") {
|
|
2150
|
+
warnings.push(
|
|
2151
|
+
`patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
|
|
2152
|
+
);
|
|
2153
|
+
} else if (patch.status === "unresolved") {
|
|
2154
|
+
warnings.push(
|
|
2155
|
+
`patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return warnings;
|
|
2160
|
+
}
|
|
2161
|
+
var EMPTY_RESULT = {
|
|
2162
|
+
initialized: false,
|
|
2163
|
+
removed: [],
|
|
2164
|
+
remaining: 0,
|
|
2165
|
+
notFound: false,
|
|
2166
|
+
alreadyForgotten: [],
|
|
2167
|
+
totalPatches: 0,
|
|
2168
|
+
warnings: []
|
|
2169
|
+
};
|
|
2170
|
+
function forget(outputDir, options) {
|
|
2068
2171
|
const lockManager = new LockfileManager(outputDir);
|
|
2069
2172
|
if (!lockManager.exists()) {
|
|
2070
|
-
return {
|
|
2173
|
+
return { ...EMPTY_RESULT };
|
|
2071
2174
|
}
|
|
2072
2175
|
const lock = lockManager.read();
|
|
2073
|
-
const
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2176
|
+
const totalPatches = lock.patches.length;
|
|
2177
|
+
if (options?.all) {
|
|
2178
|
+
const removed = lock.patches.map(toMatchedPatch);
|
|
2179
|
+
const warnings = buildWarnings(lock.patches);
|
|
2180
|
+
if (!options.dryRun) {
|
|
2181
|
+
for (const patch of lock.patches) {
|
|
2182
|
+
lockManager.addForgottenHash(patch.content_hash);
|
|
2183
|
+
}
|
|
2184
|
+
lockManager.clearPatches();
|
|
2185
|
+
lockManager.save();
|
|
2186
|
+
}
|
|
2187
|
+
return {
|
|
2188
|
+
initialized: true,
|
|
2189
|
+
removed,
|
|
2190
|
+
remaining: 0,
|
|
2191
|
+
notFound: false,
|
|
2192
|
+
alreadyForgotten: [],
|
|
2193
|
+
totalPatches,
|
|
2194
|
+
warnings
|
|
2195
|
+
};
|
|
2078
2196
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2197
|
+
if (options?.patchIds && options.patchIds.length > 0) {
|
|
2198
|
+
const removed = [];
|
|
2199
|
+
const alreadyForgotten = [];
|
|
2200
|
+
const patchesToRemove = [];
|
|
2201
|
+
for (const id of options.patchIds) {
|
|
2202
|
+
const patch = lock.patches.find((p) => p.id === id);
|
|
2203
|
+
if (patch) {
|
|
2204
|
+
removed.push(toMatchedPatch(patch));
|
|
2205
|
+
patchesToRemove.push(patch);
|
|
2206
|
+
} else {
|
|
2207
|
+
alreadyForgotten.push(id);
|
|
2208
|
+
}
|
|
2087
2209
|
}
|
|
2088
|
-
|
|
2210
|
+
const warnings = buildWarnings(patchesToRemove);
|
|
2211
|
+
if (!options.dryRun) {
|
|
2212
|
+
for (const patch of patchesToRemove) {
|
|
2213
|
+
lockManager.addForgottenHash(patch.content_hash);
|
|
2214
|
+
lockManager.removePatch(patch.id);
|
|
2215
|
+
}
|
|
2216
|
+
lockManager.save();
|
|
2217
|
+
}
|
|
2218
|
+
return {
|
|
2219
|
+
initialized: true,
|
|
2220
|
+
removed,
|
|
2221
|
+
remaining: totalPatches - removed.length,
|
|
2222
|
+
notFound: removed.length === 0 && alreadyForgotten.length > 0,
|
|
2223
|
+
alreadyForgotten,
|
|
2224
|
+
totalPatches,
|
|
2225
|
+
warnings
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
if (options?.pattern) {
|
|
2229
|
+
const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
|
|
2230
|
+
return {
|
|
2231
|
+
initialized: true,
|
|
2232
|
+
removed: [],
|
|
2233
|
+
remaining: totalPatches,
|
|
2234
|
+
notFound: matched.length === 0,
|
|
2235
|
+
alreadyForgotten: [],
|
|
2236
|
+
totalPatches,
|
|
2237
|
+
warnings: [],
|
|
2238
|
+
matched
|
|
2239
|
+
};
|
|
2089
2240
|
}
|
|
2090
|
-
return {
|
|
2241
|
+
return {
|
|
2242
|
+
initialized: true,
|
|
2243
|
+
removed: [],
|
|
2244
|
+
remaining: totalPatches,
|
|
2245
|
+
notFound: totalPatches === 0,
|
|
2246
|
+
alreadyForgotten: [],
|
|
2247
|
+
totalPatches,
|
|
2248
|
+
warnings: [],
|
|
2249
|
+
matched: lock.patches.map(toMatchedPatch)
|
|
2250
|
+
};
|
|
2091
2251
|
}
|
|
2092
2252
|
|
|
2093
2253
|
// src/commands/reset.ts
|
|
@@ -2209,24 +2369,49 @@ async function getChangedFiles(git, currentGen, files) {
|
|
|
2209
2369
|
function status(outputDir) {
|
|
2210
2370
|
const lockManager = new LockfileManager(outputDir);
|
|
2211
2371
|
if (!lockManager.exists()) {
|
|
2212
|
-
return {
|
|
2372
|
+
return {
|
|
2373
|
+
initialized: false,
|
|
2374
|
+
generationCount: 0,
|
|
2375
|
+
lastGeneration: void 0,
|
|
2376
|
+
patches: [],
|
|
2377
|
+
unresolvedCount: 0,
|
|
2378
|
+
excludePatterns: []
|
|
2379
|
+
};
|
|
2213
2380
|
}
|
|
2214
2381
|
const lock = lockManager.read();
|
|
2215
2382
|
const patches = lock.patches.map((patch) => ({
|
|
2216
|
-
|
|
2217
|
-
|
|
2383
|
+
id: patch.id,
|
|
2384
|
+
type: patch.patch_content.includes("new file mode") ? "added" : "modified",
|
|
2218
2385
|
message: patch.original_message,
|
|
2219
|
-
|
|
2386
|
+
author: patch.original_author.split("<")[0]?.trim() || "unknown",
|
|
2387
|
+
sha: patch.original_commit.slice(0, 7),
|
|
2388
|
+
files: patch.files,
|
|
2389
|
+
fileCount: patch.files.length,
|
|
2390
|
+
...patch.status ? { status: patch.status } : {}
|
|
2220
2391
|
}));
|
|
2392
|
+
const unresolvedCount = lock.patches.filter(
|
|
2393
|
+
(p) => p.status === "unresolved" || p.status === "resolving"
|
|
2394
|
+
).length;
|
|
2221
2395
|
let lastGeneration;
|
|
2222
2396
|
const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
2223
2397
|
if (lastGen) {
|
|
2224
2398
|
lastGeneration = {
|
|
2225
|
-
sha: lastGen.commit_sha,
|
|
2226
|
-
timestamp: lastGen.timestamp
|
|
2399
|
+
sha: lastGen.commit_sha.slice(0, 7),
|
|
2400
|
+
timestamp: lastGen.timestamp,
|
|
2401
|
+
cliVersion: lastGen.cli_version,
|
|
2402
|
+
generatorVersions: lastGen.generator_versions
|
|
2227
2403
|
};
|
|
2228
2404
|
}
|
|
2229
|
-
|
|
2405
|
+
const config = lockManager.getCustomizationsConfig();
|
|
2406
|
+
const excludePatterns = config.exclude ?? [];
|
|
2407
|
+
return {
|
|
2408
|
+
initialized: true,
|
|
2409
|
+
generationCount: lock.generations.length,
|
|
2410
|
+
lastGeneration,
|
|
2411
|
+
patches,
|
|
2412
|
+
unresolvedCount,
|
|
2413
|
+
excludePatterns
|
|
2414
|
+
};
|
|
2230
2415
|
}
|
|
2231
2416
|
export {
|
|
2232
2417
|
FERN_BOT_EMAIL,
|