@webpieces/pr-gate 0.3.354 → 0.3.356
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/package.json +7 -2
- package/src/dashboard/dashboard.d.ts +12 -3
- package/src/dashboard/dashboard.js +136 -129
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.js +4 -4
- package/src/index.js.map +1 -1
- package/src/scripts/commands/finish-update-command.d.ts +10 -0
- package/src/scripts/commands/finish-update-command.js +51 -0
- package/src/scripts/commands/finish-update-command.js.map +1 -0
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +22 -0
- package/src/scripts/commands/finish-upsert-pr-command.js +131 -0
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -0
- package/src/scripts/commands/start-update-command.d.ts +9 -0
- package/src/scripts/commands/start-update-command.js +67 -0
- package/src/scripts/commands/start-update-command.js.map +1 -0
- package/src/scripts/commands/start-upsert-pr-command.d.ts +15 -0
- package/src/scripts/commands/start-upsert-pr-command.js +73 -0
- package/src/scripts/commands/start-upsert-pr-command.js.map +1 -0
- package/src/scripts/git-finishUpsertPr.d.ts +1 -1
- package/src/scripts/git-finishUpsertPr.js +11 -109
- package/src/scripts/git-finishUpsertPr.js.map +1 -1
- package/src/scripts/git-gatherInfo.d.ts +15 -1
- package/src/scripts/git-gatherInfo.js +79 -61
- package/src/scripts/git-gatherInfo.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +25 -0
- package/src/scripts/pr-gate-app.js +56 -0
- package/src/scripts/pr-gate-app.js.map +1 -0
- package/src/scripts/workflow/branch-naming.d.ts +11 -8
- package/src/scripts/workflow/branch-naming.js +30 -21
- package/src/scripts/workflow/branch-naming.js.map +1 -1
- package/src/scripts/workflow/build-affected.d.ts +21 -30
- package/src/scripts/workflow/build-affected.js +57 -63
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/workflow/cleanTmp.d.ts +6 -1
- package/src/scripts/workflow/cleanTmp.js +83 -78
- package/src/scripts/workflow/cleanTmp.js.map +1 -1
- package/src/scripts/workflow/git-exec.d.ts +32 -28
- package/src/scripts/workflow/git-exec.js +92 -85
- package/src/scripts/workflow/git-exec.js.map +1 -1
- package/src/scripts/workflow/git-findForkPoint.d.ts +12 -3
- package/src/scripts/workflow/git-findForkPoint.js +116 -107
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/git-readAiBranchName.d.ts +7 -2
- package/src/scripts/workflow/git-readAiBranchName.js +24 -15
- package/src/scripts/workflow/git-readAiBranchName.js.map +1 -1
- package/src/scripts/workflow/merge-end.d.ts +21 -6
- package/src/scripts/workflow/merge-end.js +147 -133
- package/src/scripts/workflow/merge-end.js.map +1 -1
- package/src/scripts/workflow/merge-start.d.ts +22 -1
- package/src/scripts/workflow/merge-start.js +180 -186
- package/src/scripts/workflow/merge-start.js.map +1 -1
- package/src/scripts/workflow/merge-state.d.ts +26 -23
- package/src/scripts/workflow/merge-state.js +131 -137
- package/src/scripts/workflow/merge-state.js.map +1 -1
- package/src/scripts/workflow/open-pr-check.d.ts +7 -1
- package/src/scripts/workflow/open-pr-check.js +34 -23
- package/src/scripts/workflow/open-pr-check.js.map +1 -1
- package/src/scripts/workflow/run-update.d.ts +13 -1
- package/src/scripts/workflow/run-update.js +59 -28
- package/src/scripts/workflow/run-update.js.map +1 -1
- package/src/scripts/wp-finish-update.d.ts +1 -1
- package/src/scripts/wp-finish-update.js +11 -30
- package/src/scripts/wp-finish-update.js.map +1 -1
- package/src/scripts/wp-start-update.d.ts +1 -2
- package/src/scripts/wp-start-update.js +11 -51
- package/src/scripts/wp-start-update.js.map +1 -1
- package/src/scripts/wp-start-upsert-pr.d.ts +1 -1
- package/src/scripts/wp-start-upsert-pr.js +11 -47
- package/src/scripts/wp-start-upsert-pr.js.map +1 -1
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
3
|
+
exports.AiBranchName = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
|
-
const
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
7
8
|
const branch_naming_1 = require("./branch-naming");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
/** The stable feature identity used to key the merge-context + PR-body dirs. */
|
|
10
|
+
let AiBranchName = class AiBranchName {
|
|
11
|
+
branchNaming;
|
|
12
|
+
constructor(branchNaming) {
|
|
13
|
+
this.branchNaming = branchNaming;
|
|
14
|
+
}
|
|
15
|
+
// Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant
|
|
16
|
+
// across a sync's transient `<feature>Squash` temp branch (and any leftover `…wpN` from the old
|
|
17
|
+
// scheme), so derive it from baseBranchName (strips `Squash` + a legacy `wpN`) before slugifying.
|
|
18
|
+
getFeatureName() {
|
|
19
|
+
const branch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
20
|
+
return this.branchNaming.baseBranchName(branch).replace(/\//g, '-');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
exports.AiBranchName = AiBranchName;
|
|
24
|
+
exports.AiBranchName = AiBranchName = tslib_1.__decorate([
|
|
25
|
+
(0, core_context_1.provideSingleton)(),
|
|
26
|
+
(0, inversify_1.injectable)(),
|
|
27
|
+
tslib_1.__metadata("design:paramtypes", [branch_naming_1.BranchNaming])
|
|
28
|
+
], AiBranchName);
|
|
20
29
|
//# sourceMappingURL=git-readAiBranchName.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-readAiBranchName.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-readAiBranchName.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"git-readAiBranchName.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-readAiBranchName.ts"],"names":[],"mappings":";;;;AAAA,iDAAyC;AACzC,0DAA2D;AAC3D,yCAAuC;AACvC,mDAA+C;AAE/C,gFAAgF;AAGzE,IAAM,YAAY,GAAlB,MAAM,YAAY;IACQ;IAA7B,YAA6B,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAE3D,mGAAmG;IACnG,gGAAgG;IAChG,kGAAkG;IAClG,cAAc;QACV,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAClF,OAAO,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxE,CAAC;CACJ,CAAA;AAVY,oCAAY;uBAAZ,YAAY;IAFxB,IAAA,+BAAgB,GAAE;IAClB,IAAA,sBAAU,GAAE;6CAEkC,4BAAY;GAD9C,YAAY,CAUxB","sourcesContent":["import { execSync } from 'child_process';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { injectable } from 'inversify';\nimport { BranchNaming } from './branch-naming';\n\n/** The stable feature identity used to key the merge-context + PR-body dirs. */\n@provideSingleton()\n@injectable()\nexport class AiBranchName {\n constructor(private readonly branchNaming: BranchNaming) {}\n\n // Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant\n // across a sync's transient `<feature>Squash` temp branch (and any leftover `…wpN` from the old\n // scheme), so derive it from baseBranchName (strips `Squash` + a legacy `wpN`) before slugifying.\n getFeatureName(): string {\n const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n return this.branchNaming.baseBranchName(branch).replace(/\\//g, '-');\n }\n}\n"]}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { MutationVerb } from '@webpieces/rules-config';
|
|
2
|
+
import { BranchNaming } from './branch-naming';
|
|
3
|
+
import { CleanTmp } from './cleanTmp';
|
|
4
|
+
import { GitExec } from './git-exec';
|
|
2
5
|
import { MergeContext } from './merge-start';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { MergeState } from './merge-state';
|
|
7
|
+
export declare class MergeEnd {
|
|
8
|
+
private readonly branchNaming;
|
|
9
|
+
private readonly cleanTmpService;
|
|
10
|
+
private readonly gitExec;
|
|
11
|
+
private readonly mergeState;
|
|
12
|
+
constructor(branchNaming: BranchNaming, cleanTmpService: CleanTmp, gitExec: GitExec, mergeState: MergeState);
|
|
13
|
+
/**
|
|
14
|
+
* Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the
|
|
15
|
+
* AI and must be validated + committed before finalizing; null means a clean merge that merge-START
|
|
16
|
+
* already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
|
|
17
|
+
*/
|
|
18
|
+
mergeEnd(repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null): Promise<void>;
|
|
19
|
+
private validateResolution;
|
|
20
|
+
private localBranchExists;
|
|
21
|
+
private finalizeBranch;
|
|
22
|
+
private printSyncRecap;
|
|
23
|
+
}
|
|
@@ -1,153 +1,167 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.MergeEnd = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
7
7
|
const path = tslib_1.__importStar(require("path"));
|
|
8
8
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
9
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
10
|
+
const inversify_1 = require("inversify");
|
|
9
11
|
const branch_naming_1 = require("./branch-naming");
|
|
10
12
|
const cleanTmp_1 = require("./cleanTmp");
|
|
11
13
|
const git_exec_1 = require("./git-exec");
|
|
12
14
|
const merge_state_1 = require("./merge-state");
|
|
15
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
13
16
|
// merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given
|
|
14
17
|
// the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS
|
|
15
|
-
// finalizes
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
scan.filesWithMarkers.map((file) => ` - ${file}`).join('\n') +
|
|
30
|
-
'\n\nResolve them, then re-run: pnpm wp-finish-upsert-pr');
|
|
31
|
-
}
|
|
32
|
-
// 2. Ensure git itself has no remaining unmerged entries.
|
|
33
|
-
const unmerged = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
|
|
34
|
-
if (unmerged !== '') {
|
|
35
|
-
throw new rules_config_1.CliExitError(1, '❌ Git still reports unmerged files:\n' + unmerged +
|
|
36
|
-
'\n\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr');
|
|
37
|
-
}
|
|
38
|
-
process.stdout.write('✅ No conflict markers in resolved files.\n');
|
|
39
|
-
// 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in
|
|
40
|
-
// its per-file context dir, proving the AI deliberately 3-point merged it (and recording how)
|
|
41
|
-
// rather than blindly taking one side. A sidecar file works for any type, incl. JSON/deletes.
|
|
42
|
-
const explanations = (0, merge_state_1.scanMergeExplanations)(mergeDir, conflictedFiles);
|
|
43
|
-
if (!explanations.clean) {
|
|
44
|
-
const missing = explanations.filesWithMarkers
|
|
45
|
-
.map((file) => ` - ${file}\n → ${path.join((0, merge_state_1.perFileContextDir)(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE)}`)
|
|
46
|
-
.join('\n');
|
|
47
|
-
throw new rules_config_1.CliExitError(1, `❌ Missing/empty merge explanation (${rules_config_1.MERGE_EXPLANATION_FILE}) for:\n` +
|
|
48
|
-
missing +
|
|
49
|
-
'\n\nWrite a few sentences on how you resolved each (which side, what you combined, why),\n' +
|
|
50
|
-
'then re-run: pnpm wp-finish-upsert-pr');
|
|
18
|
+
// finalizes — force-pushes `<branch>Squash` to the stable feature branch and renames it BACK to that
|
|
19
|
+
// same feature name (local == remote == PR head), stamps a clean main-sync status, clears the marker,
|
|
20
|
+
// and sweeps stale tmp. RunUpdate (clean path / validated resume), wp-finish-update, and
|
|
21
|
+
// wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one place.
|
|
22
|
+
let MergeEnd = class MergeEnd {
|
|
23
|
+
branchNaming;
|
|
24
|
+
cleanTmpService;
|
|
25
|
+
gitExec;
|
|
26
|
+
mergeState;
|
|
27
|
+
constructor(branchNaming, cleanTmpService, gitExec, mergeState) {
|
|
28
|
+
this.branchNaming = branchNaming;
|
|
29
|
+
this.cleanTmpService = cleanTmpService;
|
|
30
|
+
this.gitExec = gitExec;
|
|
31
|
+
this.mergeState = mergeState;
|
|
51
32
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the
|
|
35
|
+
* AI and must be validated + committed before finalizing; null means a clean merge that merge-START
|
|
36
|
+
* already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
|
|
37
|
+
*/
|
|
38
|
+
async mergeEnd(repoRoot, verb, mergeDir, ctx, conflictedFiles) {
|
|
39
|
+
if (conflictedFiles !== null) {
|
|
40
|
+
process.stdout.write('\n' + SEP + '🔎 Validating Merge Resolution\n' + SEP + '\n');
|
|
41
|
+
this.validateResolution(repoRoot, mergeDir, conflictedFiles);
|
|
42
|
+
// Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit.
|
|
43
|
+
// Fail on untracked so the AI commits or deletes them explicitly; then `git add -u` stages
|
|
44
|
+
// tracked resolutions only.
|
|
45
|
+
this.gitExec.assertNoUntracked(repoRoot);
|
|
46
|
+
this.gitExec.runGitChecked(['add', '-u'], 'Failed to stage resolved files');
|
|
47
|
+
const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
|
|
48
|
+
if (!nothingStaged) {
|
|
49
|
+
this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`], 'Failed to commit resolved merge');
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');
|
|
52
|
+
process.stdout.write('\n✅ Merge validated and committed.\n');
|
|
53
|
+
}
|
|
54
|
+
// A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back).
|
|
55
|
+
// Read it BEFORE clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.
|
|
56
|
+
const hadConflict = this.mergeState.readMergeMarker(mergeDir) !== null;
|
|
57
|
+
this.finalizeBranch(repoRoot, verb, ctx, hadConflict);
|
|
58
|
+
this.mergeState.clearMergeMarker(mergeDir);
|
|
59
|
+
await this.cleanTmpService.cleanTmp();
|
|
73
60
|
}
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
61
|
+
// Validate the AI's resolution of the conflicted files. Throws CliExitError with a fix instruction
|
|
62
|
+
// on any failure; returns only when all three checks pass.
|
|
63
|
+
validateResolution(repoRoot, mergeDir, conflictedFiles) {
|
|
64
|
+
// 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).
|
|
65
|
+
const scan = this.mergeState.scanConflictMarkers(repoRoot, conflictedFiles);
|
|
66
|
+
if (!scan.clean) {
|
|
67
|
+
throw new rules_config_1.CliExitError(1, '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\n' +
|
|
68
|
+
scan.filesWithMarkers.map((file) => ` - ${file}`).join('\n') +
|
|
69
|
+
'\n\nResolve them, then re-run: pnpm wp-finish-upsert-pr');
|
|
70
|
+
}
|
|
71
|
+
// 2. Ensure git itself has no remaining unmerged entries.
|
|
72
|
+
const unmerged = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
|
|
73
|
+
if (unmerged !== '') {
|
|
74
|
+
throw new rules_config_1.CliExitError(1, '❌ Git still reports unmerged files:\n' + unmerged +
|
|
75
|
+
'\n\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr');
|
|
76
|
+
}
|
|
77
|
+
process.stdout.write('✅ No conflict markers in resolved files.\n');
|
|
78
|
+
// 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in its
|
|
79
|
+
// per-file context dir, proving the AI deliberately 3-point merged it (and recording how).
|
|
80
|
+
const explanations = this.mergeState.scanMergeExplanations(mergeDir, conflictedFiles);
|
|
81
|
+
if (!explanations.clean) {
|
|
82
|
+
const missing = explanations.filesWithMarkers
|
|
83
|
+
.map((file) => ` - ${file}\n → ${path.join(this.mergeState.perFileContextDir(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE)}`)
|
|
84
|
+
.join('\n');
|
|
85
|
+
throw new rules_config_1.CliExitError(1, `❌ Missing/empty merge explanation (${rules_config_1.MERGE_EXPLANATION_FILE}) for:\n` +
|
|
86
|
+
missing +
|
|
87
|
+
'\n\nWrite a few sentences on how you resolved each (which side, what you combined, why),\n' +
|
|
88
|
+
'then re-run: pnpm wp-finish-upsert-pr');
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write('✅ Merge explanations present for all resolved files.\n');
|
|
79
91
|
}
|
|
80
|
-
(
|
|
81
|
-
|
|
82
|
-
renameEvent.fromBranch = ctx.currentBranch;
|
|
83
|
-
renameEvent.toBranch = base;
|
|
84
|
-
(0, rules_config_1.logBranchMutation)(repoRoot, renameEvent);
|
|
85
|
-
// Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard
|
|
86
|
-
// unblocks edits immediately (no wait for the async refresher).
|
|
87
|
-
(0, rules_config_1.stampCleanMainSyncStatus)((0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());
|
|
88
|
-
// Clean sync → the pre-merge snapshot was never needed; delete it (LAST, so any earlier finalize
|
|
89
|
-
// failure above keeps it). Conflict sync → keep it as the undo point + trail.
|
|
90
|
-
const backupKept = hadConflict;
|
|
91
|
-
if (!backupKept && localBranchExists(ctx.backupBranch)) {
|
|
92
|
-
(0, git_exec_1.runGitChecked)(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');
|
|
92
|
+
localBranchExists(name) {
|
|
93
|
+
return (0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
: `landed back on ${feature} (local only — no remote branch yet)`;
|
|
109
|
-
const step1 = backupKept
|
|
110
|
-
? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`
|
|
111
|
-
: `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;
|
|
112
|
-
const trailer = backupKept
|
|
113
|
-
? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\n` +
|
|
114
|
-
` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\n` +
|
|
115
|
-
` Prune this run's snapshot when safe: git branch -D ${backupBranch}\n\n`
|
|
116
|
-
: '\n';
|
|
117
|
-
process.stdout.write('\n' + SEP + '✅ Sync complete — here is exactly what I did\n' + SEP + '\n' +
|
|
118
|
-
` 1. ${step1}\n` +
|
|
119
|
-
` 2. pulled origin/main\n` +
|
|
120
|
-
` 3. squash-merged your work onto main\n` +
|
|
121
|
-
` 4. ${remoteLine}\n\n` +
|
|
122
|
-
trailer);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI
|
|
126
|
-
* and must be validated + committed before finalizing; null means a clean merge that merge-START
|
|
127
|
-
* already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
|
|
128
|
-
*/
|
|
129
|
-
async function mergeEnd(repoRoot, verb, mergeDir, ctx, conflictedFiles) {
|
|
130
|
-
if (conflictedFiles !== null) {
|
|
131
|
-
process.stdout.write('\n' + SEP + '🔎 Validating Merge Resolution\n' + SEP + '\n');
|
|
132
|
-
validateResolution(repoRoot, mergeDir, conflictedFiles);
|
|
133
|
-
// Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit
|
|
134
|
-
// (a blanket `git add -A` once swept a stale untracked dir in). Fail on untracked so the AI
|
|
135
|
-
// commits or deletes them explicitly; then `git add -u` stages tracked resolutions only.
|
|
136
|
-
(0, git_exec_1.assertNoUntracked)(repoRoot);
|
|
137
|
-
(0, git_exec_1.runGitChecked)(['add', '-u'], 'Failed to stage resolved files');
|
|
138
|
-
const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
|
|
139
|
-
if (!nothingStaged) {
|
|
140
|
-
(0, git_exec_1.runGitChecked)(['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`], 'Failed to commit resolved merge');
|
|
95
|
+
// Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME
|
|
96
|
+
// the local squash branch back to that SAME feature name. On a CLEAN sync the pre-merge snapshot is
|
|
97
|
+
// disposable, so it's deleted at the very end; a CONFLICT sync keeps it.
|
|
98
|
+
finalizeBranch(repoRoot, verb, ctx, hadConflict) {
|
|
99
|
+
process.stdout.write('\n' + SEP + '🗑️ Finalizing\n' + SEP + '\n');
|
|
100
|
+
const base = this.branchNaming.baseBranchName(ctx.currentBranch);
|
|
101
|
+
this.gitExec.runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');
|
|
102
|
+
const remoteExists = (0, child_process_1.spawnSync)('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;
|
|
103
|
+
if (remoteExists) {
|
|
104
|
+
process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\n` : 'Updating remote branch (force-with-lease)...\n');
|
|
105
|
+
this.gitExec.runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
process.stdout.write('No remote branch — local only.\n');
|
|
141
109
|
}
|
|
142
|
-
|
|
143
|
-
|
|
110
|
+
this.gitExec.runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');
|
|
111
|
+
// Free the rename target: `base` is normally the branch we just deleted, but a backward-compat
|
|
112
|
+
// sync from a leftover `…wpN` can leave a separate stale `base` lingering — drop it too.
|
|
113
|
+
if (base !== ctx.currentBranch && this.localBranchExists(base)) {
|
|
114
|
+
this.gitExec.runGitChecked(['branch', '-D', base], 'Failed to delete stale base branch');
|
|
115
|
+
}
|
|
116
|
+
this.gitExec.runGitChecked(['branch', '-m', base], 'Failed to rename squash branch to the feature name');
|
|
117
|
+
const renameEvent = new rules_config_1.BranchMutationEvent(verb, 'RENAME');
|
|
118
|
+
renameEvent.fromBranch = ctx.currentBranch;
|
|
119
|
+
renameEvent.toBranch = base;
|
|
120
|
+
(0, rules_config_1.logBranchMutation)(repoRoot, renameEvent);
|
|
121
|
+
// Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard
|
|
122
|
+
// unblocks edits immediately (no wait for the async refresher).
|
|
123
|
+
(0, rules_config_1.stampCleanMainSyncStatus)((0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());
|
|
124
|
+
// Clean sync → the pre-merge snapshot was never needed; delete it LAST. Conflict sync → keep it.
|
|
125
|
+
const backupKept = hadConflict;
|
|
126
|
+
if (!backupKept && this.localBranchExists(ctx.backupBranch)) {
|
|
127
|
+
this.gitExec.runGitChecked(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');
|
|
128
|
+
}
|
|
129
|
+
const finalizeEvent = new rules_config_1.BranchMutationEvent(verb, 'FINALIZE');
|
|
130
|
+
finalizeEvent.fromBranch = ctx.currentBranch;
|
|
131
|
+
finalizeEvent.toBranch = base;
|
|
132
|
+
finalizeEvent.outcome = 'finalized';
|
|
133
|
+
finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];
|
|
134
|
+
(0, rules_config_1.logBranchMutation)(repoRoot, finalizeEvent);
|
|
135
|
+
this.printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);
|
|
136
|
+
}
|
|
137
|
+
// The explicit, numbered "here is exactly what I did" recap the AI (and human) reads after a sync.
|
|
138
|
+
printSyncRecap(feature, backupBranch, prNumber, pushed, backupKept) {
|
|
139
|
+
const remoteLine = pushed
|
|
140
|
+
? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`
|
|
141
|
+
: `landed back on ${feature} (local only — no remote branch yet)`;
|
|
142
|
+
const step1 = backupKept
|
|
143
|
+
? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`
|
|
144
|
+
: `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;
|
|
145
|
+
const trailer = backupKept
|
|
146
|
+
? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\n` +
|
|
147
|
+
` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\n` +
|
|
148
|
+
` Prune this run's snapshot when safe: git branch -D ${backupBranch}\n\n`
|
|
149
|
+
: '\n';
|
|
150
|
+
process.stdout.write('\n' + SEP + '✅ Sync complete — here is exactly what I did\n' + SEP + '\n' +
|
|
151
|
+
` 1. ${step1}\n` +
|
|
152
|
+
` 2. pulled origin/main\n` +
|
|
153
|
+
` 3. squash-merged your work onto main\n` +
|
|
154
|
+
` 4. ${remoteLine}\n\n` +
|
|
155
|
+
trailer);
|
|
144
156
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
(
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
};
|
|
158
|
+
exports.MergeEnd = MergeEnd;
|
|
159
|
+
exports.MergeEnd = MergeEnd = tslib_1.__decorate([
|
|
160
|
+
(0, core_context_1.provideSingleton)(),
|
|
161
|
+
(0, inversify_1.injectable)(),
|
|
162
|
+
tslib_1.__metadata("design:paramtypes", [branch_naming_1.BranchNaming,
|
|
163
|
+
cleanTmp_1.CleanTmp,
|
|
164
|
+
git_exec_1.GitExec,
|
|
165
|
+
merge_state_1.MergeState])
|
|
166
|
+
], MergeEnd);
|
|
153
167
|
//# sourceMappingURL=merge-end.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-end.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-end.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n MERGE_EXPLANATION_FILE, stampCleanMainSyncStatus, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation,\n} from '@webpieces/rules-config';\nimport { baseBranchName } from './branch-naming';\nimport { cleanTmp } from './cleanTmp';\nimport { assertNoUntracked, runGitChecked } from './git-exec';\nimport { MergeContext } from './merge-start';\nimport { clearMergeMarker, readMergeMarker, perFileContextDir, scanConflictMarkers, scanMergeExplanations } from './merge-state';\n\n// merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given\n// the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS\n// finalizes the merge — force-pushes `<branch>Squash` to the stable feature branch and renames it\n// BACK to that same feature name (local == remote == PR head), stamps a clean main-sync status,\n// clears the marker and sweeps stale tmp. The shared runUpdateFromMain (clean path / validated resume), wp-finish-update, and\n// wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one\n// place and the conflict path can no longer post a PR from the un-swapped squash branch.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Validate the AI's resolution of the conflicted files — the part of the process the AI owns\n// (branch creation/finalization is the script's job, so it is not re-checked here). Throws\n// CliExitError with a fix instruction on any failure; returns only when all three checks pass.\nfunction validateResolution(repoRoot: string, mergeDir: string, conflictedFiles: string[]): void {\n // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).\n const scan = scanConflictMarkers(repoRoot, conflictedFiles);\n if (!scan.clean) {\n throw new CliExitError(1,\n '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\\n' +\n scan.filesWithMarkers.map((file: string): string => ` - ${file}`).join('\\n') +\n '\\n\\nResolve them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n\n // 2. Ensure git itself has no remaining unmerged entries.\n const unmerged = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n if (unmerged !== '') {\n throw new CliExitError(1,\n '❌ Git still reports unmerged files:\\n' + unmerged +\n '\\n\\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ No conflict markers in resolved files.\\n');\n\n // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in\n // its per-file context dir, proving the AI deliberately 3-point merged it (and recording how)\n // rather than blindly taking one side. A sidecar file works for any type, incl. JSON/deletes.\n const explanations = scanMergeExplanations(mergeDir, conflictedFiles);\n if (!explanations.clean) {\n const missing = explanations.filesWithMarkers\n .map((file: string): string => ` - ${file}\\n → ${path.join(perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE)}`)\n .join('\\n');\n throw new CliExitError(1,\n `❌ Missing/empty merge explanation (${MERGE_EXPLANATION_FILE}) for:\\n` +\n missing +\n '\\n\\nWrite a few sentences on how you resolved each (which side, what you combined, why),\\n' +\n 'then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ Merge explanations present for all resolved files.\\n');\n}\n\nfunction localBranchExists(name: string): boolean {\n return spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n}\n\n// Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME\n// the local squash branch back to that SAME feature name — so the local branch, the remote branch,\n// and the PR head are all one name (no `wpN` divergence). On a CLEAN sync (`!hadConflict`) the\n// pre-merge snapshot is disposable, so it's deleted at the very end; a CONFLICT sync keeps it (paired\n// with its `merge-<n>/` context). Ends with an explicit \"here is exactly what I did\" recap.\nfunction finalizeBranch(repoRoot: string, verb: MutationVerb, ctx: MergeContext, hadConflict: boolean): void {\n process.stdout.write('\\n' + SEP + '🗑️ Finalizing\\n' + SEP + '\\n');\n const base = baseBranchName(ctx.currentBranch);\n runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');\n\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;\n if (remoteExists) {\n process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\\n` : 'Updating remote branch (force-with-lease)...\\n');\n runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');\n } else {\n process.stdout.write('No remote branch — local only.\\n');\n }\n runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');\n // Free the rename target: `base` is normally the branch we just deleted (ctx.currentBranch), but on\n // a backward-compat sync from a leftover `…wpN` a separate stale `base` can linger — drop it too.\n if (base !== ctx.currentBranch && localBranchExists(base)) {\n runGitChecked(['branch', '-D', base], 'Failed to delete stale base branch');\n }\n runGitChecked(['branch', '-m', base], 'Failed to rename squash branch to the feature name');\n const renameEvent = new BranchMutationEvent(verb, 'RENAME');\n renameEvent.fromBranch = ctx.currentBranch;\n renameEvent.toBranch = base;\n logBranchMutation(repoRoot, renameEvent);\n\n // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard\n // unblocks edits immediately (no wait for the async refresher).\n stampCleanMainSyncStatus(execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());\n\n // Clean sync → the pre-merge snapshot was never needed; delete it (LAST, so any earlier finalize\n // failure above keeps it). Conflict sync → keep it as the undo point + trail.\n const backupKept = hadConflict;\n if (!backupKept && localBranchExists(ctx.backupBranch)) {\n runGitChecked(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');\n }\n\n const finalizeEvent = new BranchMutationEvent(verb, 'FINALIZE');\n finalizeEvent.fromBranch = ctx.currentBranch;\n finalizeEvent.toBranch = base;\n finalizeEvent.outcome = 'finalized';\n finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];\n logBranchMutation(repoRoot, finalizeEvent);\n\n printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);\n}\n\n// The explicit, numbered \"here is exactly what I did\" recap the AI (and human) reads after a sync.\n// The whole point of the branch-name change is that step 4 can now say \"same name as remote/PR\" —\n// there is no confusing local-vs-remote divergence left to explain.\nfunction printSyncRecap(feature: string, backupBranch: string, prNumber: string, pushed: boolean, backupKept: boolean): void {\n const remoteLine = pushed\n ? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`\n : `landed back on ${feature} (local only — no remote branch yet)`;\n const step1 = backupKept\n ? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`\n : `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;\n const trailer = backupKept\n ? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\\n` +\n ` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\\n` +\n ` Prune this run's snapshot when safe: git branch -D ${backupBranch}\\n\\n`\n : '\\n';\n process.stdout.write(\n '\\n' + SEP + '✅ Sync complete — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. ${step1}\\n` +\n ` 2. pulled origin/main\\n` +\n ` 3. squash-merged your work onto main\\n` +\n ` 4. ${remoteLine}\\n\\n` +\n trailer,\n );\n}\n\n/**\n * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI\n * and must be validated + committed before finalizing; null means a clean merge that merge-START\n * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.\n */\nexport async function mergeEnd(\n repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null,\n): Promise<void> {\n if (conflictedFiles !== null) {\n process.stdout.write('\\n' + SEP + '🔎 Validating Merge Resolution\\n' + SEP + '\\n');\n validateResolution(repoRoot, mergeDir, conflictedFiles);\n // Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit\n // (a blanket `git add -A` once swept a stale untracked dir in). Fail on untracked so the AI\n // commits or deletes them explicitly; then `git add -u` stages tracked resolutions only.\n assertNoUntracked(repoRoot);\n runGitChecked(['add', '-u'], 'Failed to stage resolved files');\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (!nothingStaged) {\n runGitChecked(\n ['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`],\n 'Failed to commit resolved merge',\n );\n }\n fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');\n process.stdout.write('\\n✅ Merge validated and committed.\\n');\n }\n\n // A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back) —\n // correct even for the validated-resume path (conflictedFiles=null yet a conflict). Read it BEFORE\n // clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.\n const hadConflict = readMergeMarker(mergeDir) !== null;\n finalizeBranch(repoRoot, verb, ctx, hadConflict);\n clearMergeMarker(mergeDir);\n await cleanTmp();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"merge-end.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-end.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n MERGE_EXPLANATION_FILE, stampCleanMainSyncStatus, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation,\n} from '@webpieces/rules-config';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { injectable } from 'inversify';\nimport { BranchNaming } from './branch-naming';\nimport { CleanTmp } from './cleanTmp';\nimport { GitExec } from './git-exec';\nimport { MergeContext } from './merge-start';\nimport { MergeState } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given\n// the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS\n// finalizes — force-pushes `<branch>Squash` to the stable feature branch and renames it BACK to that\n// same feature name (local == remote == PR head), stamps a clean main-sync status, clears the marker,\n// and sweeps stale tmp. RunUpdate (clean path / validated resume), wp-finish-update, and\n// wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one place.\n@provideSingleton()\n@injectable()\nexport class MergeEnd {\n constructor(\n private readonly branchNaming: BranchNaming,\n private readonly cleanTmpService: CleanTmp,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n /**\n * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the\n * AI and must be validated + committed before finalizing; null means a clean merge that merge-START\n * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.\n */\n async mergeEnd(\n repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null,\n ): Promise<void> {\n if (conflictedFiles !== null) {\n process.stdout.write('\\n' + SEP + '🔎 Validating Merge Resolution\\n' + SEP + '\\n');\n this.validateResolution(repoRoot, mergeDir, conflictedFiles);\n // Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit.\n // Fail on untracked so the AI commits or deletes them explicitly; then `git add -u` stages\n // tracked resolutions only.\n this.gitExec.assertNoUntracked(repoRoot);\n this.gitExec.runGitChecked(['add', '-u'], 'Failed to stage resolved files');\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (!nothingStaged) {\n this.gitExec.runGitChecked(\n ['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`],\n 'Failed to commit resolved merge',\n );\n }\n fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');\n process.stdout.write('\\n✅ Merge validated and committed.\\n');\n }\n\n // A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back).\n // Read it BEFORE clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.\n const hadConflict = this.mergeState.readMergeMarker(mergeDir) !== null;\n this.finalizeBranch(repoRoot, verb, ctx, hadConflict);\n this.mergeState.clearMergeMarker(mergeDir);\n await this.cleanTmpService.cleanTmp();\n }\n\n // Validate the AI's resolution of the conflicted files. Throws CliExitError with a fix instruction\n // on any failure; returns only when all three checks pass.\n private validateResolution(repoRoot: string, mergeDir: string, conflictedFiles: string[]): void {\n // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).\n const scan = this.mergeState.scanConflictMarkers(repoRoot, conflictedFiles);\n if (!scan.clean) {\n throw new CliExitError(1,\n '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\\n' +\n scan.filesWithMarkers.map((file: string): string => ` - ${file}`).join('\\n') +\n '\\n\\nResolve them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n\n // 2. Ensure git itself has no remaining unmerged entries.\n const unmerged = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n if (unmerged !== '') {\n throw new CliExitError(1,\n '❌ Git still reports unmerged files:\\n' + unmerged +\n '\\n\\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ No conflict markers in resolved files.\\n');\n\n // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in its\n // per-file context dir, proving the AI deliberately 3-point merged it (and recording how).\n const explanations = this.mergeState.scanMergeExplanations(mergeDir, conflictedFiles);\n if (!explanations.clean) {\n const missing = explanations.filesWithMarkers\n .map((file: string): string => ` - ${file}\\n → ${path.join(this.mergeState.perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE)}`)\n .join('\\n');\n throw new CliExitError(1,\n `❌ Missing/empty merge explanation (${MERGE_EXPLANATION_FILE}) for:\\n` +\n missing +\n '\\n\\nWrite a few sentences on how you resolved each (which side, what you combined, why),\\n' +\n 'then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ Merge explanations present for all resolved files.\\n');\n }\n\n private localBranchExists(name: string): boolean {\n return spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n }\n\n // Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME\n // the local squash branch back to that SAME feature name. On a CLEAN sync the pre-merge snapshot is\n // disposable, so it's deleted at the very end; a CONFLICT sync keeps it.\n private finalizeBranch(repoRoot: string, verb: MutationVerb, ctx: MergeContext, hadConflict: boolean): void {\n process.stdout.write('\\n' + SEP + '🗑️ Finalizing\\n' + SEP + '\\n');\n const base = this.branchNaming.baseBranchName(ctx.currentBranch);\n this.gitExec.runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');\n\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;\n if (remoteExists) {\n process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\\n` : 'Updating remote branch (force-with-lease)...\\n');\n this.gitExec.runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');\n } else {\n process.stdout.write('No remote branch — local only.\\n');\n }\n this.gitExec.runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');\n // Free the rename target: `base` is normally the branch we just deleted, but a backward-compat\n // sync from a leftover `…wpN` can leave a separate stale `base` lingering — drop it too.\n if (base !== ctx.currentBranch && this.localBranchExists(base)) {\n this.gitExec.runGitChecked(['branch', '-D', base], 'Failed to delete stale base branch');\n }\n this.gitExec.runGitChecked(['branch', '-m', base], 'Failed to rename squash branch to the feature name');\n const renameEvent = new BranchMutationEvent(verb, 'RENAME');\n renameEvent.fromBranch = ctx.currentBranch;\n renameEvent.toBranch = base;\n logBranchMutation(repoRoot, renameEvent);\n\n // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard\n // unblocks edits immediately (no wait for the async refresher).\n stampCleanMainSyncStatus(execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());\n\n // Clean sync → the pre-merge snapshot was never needed; delete it LAST. Conflict sync → keep it.\n const backupKept = hadConflict;\n if (!backupKept && this.localBranchExists(ctx.backupBranch)) {\n this.gitExec.runGitChecked(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');\n }\n\n const finalizeEvent = new BranchMutationEvent(verb, 'FINALIZE');\n finalizeEvent.fromBranch = ctx.currentBranch;\n finalizeEvent.toBranch = base;\n finalizeEvent.outcome = 'finalized';\n finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];\n logBranchMutation(repoRoot, finalizeEvent);\n\n this.printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);\n }\n\n // The explicit, numbered \"here is exactly what I did\" recap the AI (and human) reads after a sync.\n private printSyncRecap(feature: string, backupBranch: string, prNumber: string, pushed: boolean, backupKept: boolean): void {\n const remoteLine = pushed\n ? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`\n : `landed back on ${feature} (local only — no remote branch yet)`;\n const step1 = backupKept\n ? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`\n : `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;\n const trailer = backupKept\n ? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\\n` +\n ` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\\n` +\n ` Prune this run's snapshot when safe: git branch -D ${backupBranch}\\n\\n`\n : '\\n';\n process.stdout.write(\n '\\n' + SEP + '✅ Sync complete — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. ${step1}\\n` +\n ` 2. pulled origin/main\\n` +\n ` 3. squash-merged your work onto main\\n` +\n ` 4. ${remoteLine}\\n\\n` +\n trailer,\n );\n }\n}\n"]}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { MutationVerb } from '@webpieces/rules-config';
|
|
2
|
+
import { GatherInfo } from '../git-gatherInfo';
|
|
3
|
+
import { BranchNaming } from './branch-naming';
|
|
4
|
+
import { GitExec } from './git-exec';
|
|
5
|
+
import { MergeState } from './merge-state';
|
|
2
6
|
export declare class MergeContext {
|
|
3
7
|
currentBranch: string;
|
|
4
8
|
squashBranch: string;
|
|
@@ -12,4 +16,21 @@ export declare class MergeStartResult {
|
|
|
12
16
|
runDir: string;
|
|
13
17
|
constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string);
|
|
14
18
|
}
|
|
15
|
-
export declare
|
|
19
|
+
export declare class MergeStart {
|
|
20
|
+
private readonly gatherInfo;
|
|
21
|
+
private readonly branchNaming;
|
|
22
|
+
private readonly gitExec;
|
|
23
|
+
private readonly mergeState;
|
|
24
|
+
constructor(gatherInfo: GatherInfo, branchNaming: BranchNaming, gitExec: GitExec, mergeState: MergeState);
|
|
25
|
+
mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult>;
|
|
26
|
+
private detectPr;
|
|
27
|
+
private chooseSyncSlot;
|
|
28
|
+
private createBackup;
|
|
29
|
+
private saveConflictContext;
|
|
30
|
+
private mergeProcessDoc;
|
|
31
|
+
private writeMergeProcessDoc;
|
|
32
|
+
private printConflictHandback;
|
|
33
|
+
private handleConflictsHandback;
|
|
34
|
+
private shortSha;
|
|
35
|
+
private logConflict;
|
|
36
|
+
}
|