@magnusekdahl/parallix 1.2.1 → 1.3.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/index.js +190 -185
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1331 -1468
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +4 -4
- package/px.js +226 -208
package/lib/commands/rebase.js
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
const git_js_1 = require("../core/git.js");
|
|
40
|
+
const integrate_js_1 = __importDefault(require("./integrate.js"));
|
|
41
|
+
const mission_utils_js_1 = require("../core/mission-utils.js");
|
|
42
|
+
const agents_js_1 = require("../agents/agents.js");
|
|
43
|
+
const forgejo_js_1 = require("../tools/forgejo.js");
|
|
44
|
+
const backlog_js_1 = require("../tools/backlog.js");
|
|
45
|
+
const review_state_js_1 = require("../review/review-state.js");
|
|
46
|
+
const product_config_js_1 = require("../core/product-config.js");
|
|
47
|
+
const fmt = __importStar(require("../core/fmt.js"));
|
|
48
|
+
const verification_js_1 = require("../core/verification.js");
|
|
13
49
|
/**
|
|
14
50
|
* Rebase a mission branch onto local master, auto-resolving mission-specific
|
|
15
51
|
* conflicts and launching an agent for shared-file conflicts.
|
|
@@ -17,454 +53,416 @@ const { formatVerificationCommand } = require('../core/verification');
|
|
|
17
53
|
* Usage: px rebase [<slug>] [--push]
|
|
18
54
|
*/
|
|
19
55
|
/** @param {string[]} args @param {{inferSlugFn?: Function, findMissionDirFn?: Function, findMissionAreaFn?: Function, getCurrentBranchFn?: Function, resolveConflictsFn?: Function, startAgentFn?: Function, createPrFn?: Function, readTokenFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, detectRebaseStateFn?: Function, resolveMissionBaseBranchFn?: Function, gitFn?: Function, exitFn?: Function, isForgejoReviewEnabledFn?: Function, fetchReviewBranchFn?: Function}} opts */
|
|
20
|
-
async function rebase(args, {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
resolveTaskFileFn = resolveTaskFile,
|
|
31
|
-
getTaskImplementerFn = getTaskImplementer,
|
|
32
|
-
detectRebaseStateFn = detectRebaseState,
|
|
33
|
-
resolveMissionBaseBranchFn = resolveMissionBaseBranch,
|
|
34
|
-
gitFn = git,
|
|
35
|
-
exitFn = (/** @type{number} */ code) => process.exit(code),
|
|
36
|
-
isForgejoReviewEnabledFn = isForgejoReviewEnabled,
|
|
37
|
-
fetchReviewBranchFn = fetchReviewBranch,
|
|
38
|
-
} = {}) {
|
|
39
|
-
const flags = args.filter(a => a.startsWith('--'));
|
|
40
|
-
const params = args.filter(a => !a.startsWith('--'));
|
|
41
|
-
const isPush = flags.includes('--push');
|
|
42
|
-
const explicitSlug = params[0];
|
|
43
|
-
const slug = inferSlugFn(explicitSlug);
|
|
44
|
-
if (!slug) {
|
|
45
|
-
fmt.log.fail('Usage: px rebase [<slug>] [--push]');
|
|
46
|
-
exitFn(1);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const missionDir = findMissionDirFn(slug);
|
|
51
|
-
const area = missionDir ? findMissionAreaFn(missionDir) : 'docs';
|
|
52
|
-
const branch = missionBranchName(slug);
|
|
53
|
-
|
|
54
|
-
const existingRebase = detectRebaseStateFn(process.cwd());
|
|
55
|
-
if (existingRebase.inProgress) {
|
|
56
|
-
fmt.log.fail(`Rebase already in progress for ${fmt.branch(branch)}.`);
|
|
57
|
-
if (existingRebase.rebaseHead) {
|
|
58
|
-
fmt.log.info(`Current rebase head: ${existingRebase.rebaseHead}`);
|
|
59
|
-
}
|
|
60
|
-
if (existingRebase.unmergedFiles.length > 0) {
|
|
61
|
-
fmt.log.info('Unmerged files:');
|
|
62
|
-
existingRebase.unmergedFiles.forEach((/** @type {string} */ file) => fmt.log.info(` - ${file}`));
|
|
63
|
-
}
|
|
64
|
-
fmt.log.info('Recovery commands:');
|
|
65
|
-
fmt.log.info(` ${fmt.command('git rebase --continue')}`);
|
|
66
|
-
fmt.log.info(` ${fmt.command('git rebase --abort')}`);
|
|
67
|
-
fmt.log.info(` ${fmt.command('git rebase --skip')}`);
|
|
68
|
-
exitFn(1);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const performPush = async () => {
|
|
73
|
-
if (!isPush) {return;}
|
|
74
|
-
if (!isForgejoReviewEnabledFn(process.cwd())) {
|
|
75
|
-
fmt.log.info(`Skipping Forgejo push (review provider is not forgejo).`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
fmt.log.info(`--push detected. Updating Forgejo PR for ${fmt.branch(branch)}...`);
|
|
79
|
-
|
|
80
|
-
const reviewIdentity = resolveReviewIdentity(slug, resolveWorktree(slug) || process.cwd());
|
|
81
|
-
let forgejoUser = reviewIdentity.forgejoUser;
|
|
82
|
-
if (!forgejoUser) {
|
|
83
|
-
const taskResolution = resolveTaskFileFn(slug, getPrimaryWorktree());
|
|
84
|
-
if (taskResolution.ok) {
|
|
85
|
-
forgejoUser = getTaskImplementerFn(taskResolution.taskFile);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
forgejoUser = resolveForgejoUserFn(forgejoUser);
|
|
89
|
-
|
|
90
|
-
const token = readTokenFn(forgejoUser || 'default');
|
|
91
|
-
if (!token) {
|
|
92
|
-
fmt.log.fail(`No Forgejo token found for user "${fmt.agent(forgejoUser || 'default')}". Push failed.`);
|
|
93
|
-
exitFn(1);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const result = createPrFn(branch, forgejoUser || 'default', token, { rootDir: getPrimaryWorktree(), forceWithLease: true });
|
|
97
|
-
if (!result.ok) {
|
|
98
|
-
fmt.log.fail(`Push to Forgejo failed: ${result.error}`);
|
|
99
|
-
exitFn(1);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
fmt.log.pass(`Branch pushed and PR updated for ${fmt.branch(branch)}.`);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Verify we are on the correct branch
|
|
106
|
-
const currentBranch = getCurrentBranchFn();
|
|
107
|
-
if (currentBranch !== branch) {
|
|
108
|
-
fmt.log.fail(`Expected branch ${fmt.branch(branch)}, found ${fmt.branch(currentBranch)}`);
|
|
109
|
-
fmt.log.info(`Switch to the mission branch first: ${fmt.command(`git checkout ${branch}`)}`);
|
|
110
|
-
exitFn(1);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Honor the mission's recorded base branch (a feature-branch mission rebases
|
|
115
|
-
// onto its base, e.g. skunkworks — not the primary branch). Falls back to the
|
|
116
|
-
// primary branch for every mission without a recorded Base-Branch.
|
|
117
|
-
const baseBranch = resolveMissionBaseBranchFn(slug, process.cwd(), { gitFn });
|
|
118
|
-
fmt.log.info(`Rebasing ${fmt.branch(branch)} onto local ${fmt.branch(baseBranch)}...`);
|
|
119
|
-
const rebaseResult = gitFn(['-c', 'core.editor=true', '-c', 'merge.autoedit=no', 'rebase', baseBranch]);
|
|
120
|
-
|
|
121
|
-
// Rebase succeeded (status 0) or was already up to date
|
|
122
|
-
// Also handle "Already up to date" variants
|
|
123
|
-
if (rebaseResult.status === 0 || /up to date|Already up to date/i.test(rebaseResult.stdout + rebaseResult.stderr)) {
|
|
124
|
-
const rebaseStatus = gitFn(['rebase', '--show-current']);
|
|
125
|
-
// If no rebase is in progress, we're done
|
|
126
|
-
const rebaseInProg = rebaseStatus.stdout.trim().length > 0;
|
|
127
|
-
if (rebaseInProg) {
|
|
128
|
-
// rebase --show-current returned something but status was 0 — treat as incomplete
|
|
129
|
-
fmt.log.pass('Rebase round completed.');
|
|
130
|
-
fmt.log.warn('Rebase is still in progress (non-empty --show-current). Skipping automatic push.');
|
|
131
|
-
fmt.log.info(`Next: ${fmt.command(formatVerificationCommand(area, process.cwd()))}`);
|
|
132
|
-
fmt.log.info(`Next: ${fmt.command('git rebase --continue')}`);
|
|
133
|
-
exitFn(0);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fmt.log.pass('Rebase completed cleanly.');
|
|
138
|
-
await performPush();
|
|
139
|
-
fmt.log.info(`Next: ${fmt.command(formatVerificationCommand(area, process.cwd()))}`);
|
|
140
|
-
fmt.log.info(`Next: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
|
|
141
|
-
exitFn(0);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Rebase paused on conflicts — classify and resolve
|
|
146
|
-
const rebaseOutput = [rebaseResult.stdout, rebaseResult.stderr].filter(Boolean).join('\n').trim();
|
|
147
|
-
|
|
148
|
-
// Check if this is actually a conflict (contains CONFLICT lines) or another error
|
|
149
|
-
// Also handle localized output (e.g. Swedish "KONFLIKT")
|
|
150
|
-
const isConflict = /CONFLICT|KONFLIKT/i.test(rebaseOutput);
|
|
151
|
-
if (!isConflict) {
|
|
152
|
-
fmt.log.fail('Rebase failed with a non-conflict error.');
|
|
153
|
-
if (rebaseOutput) {
|
|
154
|
-
fmt.log.fail('--- Git Output ---');
|
|
155
|
-
fmt.log.fail(rebaseOutput);
|
|
156
|
-
fmt.log.fail('------------------');
|
|
56
|
+
async function rebase(args, { inferSlugFn = mission_utils_js_1.inferSlug, findMissionDirFn = mission_utils_js_1.findMissionDir, findMissionAreaFn = mission_utils_js_1.findMissionArea, getCurrentBranchFn = git_js_1.getCurrentBranch, resolveConflictsFn = /** @type {import('./integrate.js').IntegrateFn} */ (integrate_js_1.default).resolveConflictsForMission, startAgentFn = agents_js_1.startAgent, createPrFn = forgejo_js_1.createPr, readTokenFn = forgejo_js_1.readToken, resolveForgejoUserFn = forgejo_js_1.resolveForgejoUser, resolveTaskFileFn = backlog_js_1.resolveTaskFile, getTaskImplementerFn = backlog_js_1.getTaskImplementer, detectRebaseStateFn = git_js_1.detectRebaseState, resolveMissionBaseBranchFn = mission_utils_js_1.resolveMissionBaseBranch, gitFn = git_js_1.git, exitFn = ((code) => process.exit(code)), isForgejoReviewEnabledFn = product_config_js_1.isForgejoReviewEnabled, fetchReviewBranchFn = forgejo_js_1.fetchReviewBranch, } = {}) {
|
|
57
|
+
const flags = args.filter(a => a.startsWith('--'));
|
|
58
|
+
const params = args.filter(a => !a.startsWith('--'));
|
|
59
|
+
const isPush = flags.includes('--push');
|
|
60
|
+
const explicitSlug = params[0];
|
|
61
|
+
const slug = inferSlugFn(explicitSlug);
|
|
62
|
+
if (!slug) {
|
|
63
|
+
fmt.log.fail('Usage: px rebase [<slug>] [--push]');
|
|
64
|
+
exitFn(1);
|
|
65
|
+
return;
|
|
157
66
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
67
|
+
const missionDir = findMissionDirFn(slug);
|
|
68
|
+
const area = missionDir ? findMissionAreaFn(missionDir) : 'docs';
|
|
69
|
+
const branch = (0, mission_utils_js_1.missionBranchName)(slug);
|
|
70
|
+
const existingRebase = detectRebaseStateFn(process.cwd());
|
|
71
|
+
if (existingRebase.inProgress) {
|
|
72
|
+
fmt.log.fail(`Rebase already in progress for ${fmt.branch(branch)}.`);
|
|
73
|
+
if (existingRebase.rebaseHead) {
|
|
74
|
+
fmt.log.info(`Current rebase head: ${existingRebase.rebaseHead}`);
|
|
75
|
+
}
|
|
76
|
+
if (existingRebase.unmergedFiles.length > 0) {
|
|
77
|
+
fmt.log.info('Unmerged files:');
|
|
78
|
+
existingRebase.unmergedFiles.forEach((file) => fmt.log.info(` - ${file}`));
|
|
79
|
+
}
|
|
80
|
+
fmt.log.info('Recovery commands:');
|
|
81
|
+
fmt.log.info(` ${fmt.command('git rebase --continue')}`);
|
|
82
|
+
fmt.log.info(` ${fmt.command('git rebase --abort')}`);
|
|
83
|
+
fmt.log.info(` ${fmt.command('git rebase --skip')}`);
|
|
84
|
+
exitFn(1);
|
|
85
|
+
return;
|
|
162
86
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
conflictFiles,
|
|
203
|
-
missionSpecificFiles,
|
|
204
|
-
sharedFiles,
|
|
205
|
-
});
|
|
206
|
-
} else if (!conflictResult.ok) {
|
|
207
|
-
/** @type{{error?: string}} */
|
|
208
|
-
const cr = conflictResult;
|
|
209
|
-
if (cr.error === 'worktree-missing') {
|
|
210
|
-
fmt.log.fail(`Mission worktree not found: ${fmt.path(worktreePath)}`);
|
|
211
|
-
fmt.log.info(`Ensure the worktree is registered: ${fmt.command(`git worktree add ${conventionalWorktreePath(slug, getPrimaryWorktree())} ${branch}`)}`);
|
|
212
|
-
exitFn(1);
|
|
213
|
-
return;
|
|
87
|
+
const performPush = async () => {
|
|
88
|
+
if (!isPush) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!isForgejoReviewEnabledFn(process.cwd())) {
|
|
92
|
+
fmt.log.info(`Skipping Forgejo push (review provider is not forgejo).`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
fmt.log.info(`--push detected. Updating Forgejo PR for ${fmt.branch(branch)}...`);
|
|
96
|
+
const reviewIdentity = (0, review_state_js_1.resolveReviewIdentity)(slug, (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd());
|
|
97
|
+
let forgejoUser = reviewIdentity.forgejoUser;
|
|
98
|
+
if (!forgejoUser) {
|
|
99
|
+
const taskResolution = resolveTaskFileFn(slug, (0, mission_utils_js_1.getPrimaryWorktree)());
|
|
100
|
+
if (taskResolution.ok) {
|
|
101
|
+
forgejoUser = getTaskImplementerFn(taskResolution.taskFile);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
forgejoUser = resolveForgejoUserFn(forgejoUser);
|
|
105
|
+
const token = readTokenFn(forgejoUser || 'default');
|
|
106
|
+
if (!token) {
|
|
107
|
+
fmt.log.fail(`No Forgejo token found for user "${fmt.agent(forgejoUser || 'default')}". Push failed.`);
|
|
108
|
+
exitFn(1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const result = createPrFn(branch, forgejoUser || 'default', token, { rootDir: (0, mission_utils_js_1.getPrimaryWorktree)(), forceWithLease: true });
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
fmt.log.fail(`Push to Forgejo failed: ${result.error}`);
|
|
114
|
+
exitFn(1);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
fmt.log.pass(`Branch pushed and PR updated for ${fmt.branch(branch)}.`);
|
|
118
|
+
};
|
|
119
|
+
// Verify we are on the correct branch
|
|
120
|
+
const currentBranch = getCurrentBranchFn();
|
|
121
|
+
if (currentBranch !== branch) {
|
|
122
|
+
fmt.log.fail(`Expected branch ${fmt.branch(branch)}, found ${fmt.branch(currentBranch)}`);
|
|
123
|
+
fmt.log.info(`Switch to the mission branch first: ${fmt.command(`git checkout ${branch}`)}`);
|
|
124
|
+
exitFn(1);
|
|
125
|
+
return;
|
|
214
126
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
127
|
+
// Honor the mission's recorded base branch (a feature-branch mission rebases
|
|
128
|
+
// onto its base, e.g. skunkworks — not the primary branch). Falls back to the
|
|
129
|
+
// primary branch for every mission without a recorded Base-Branch.
|
|
130
|
+
const baseBranch = resolveMissionBaseBranchFn(slug, process.cwd(), { gitFn });
|
|
131
|
+
fmt.log.info(`Rebasing ${fmt.branch(branch)} onto local ${fmt.branch(baseBranch)}...`);
|
|
132
|
+
const rebaseResult = gitFn(['-c', 'core.editor=true', '-c', 'merge.autoedit=no', 'rebase', baseBranch]);
|
|
133
|
+
// Rebase succeeded (status 0) or was already up to date
|
|
134
|
+
// Also handle "Already up to date" variants
|
|
135
|
+
if (rebaseResult.status === 0 || /up to date|Already up to date/i.test(rebaseResult.stdout + rebaseResult.stderr)) {
|
|
136
|
+
const rebaseStatus = gitFn(['rebase', '--show-current']);
|
|
137
|
+
// If no rebase is in progress, we're done
|
|
138
|
+
const rebaseInProg = rebaseStatus.stdout.trim().length > 0;
|
|
139
|
+
if (rebaseInProg) {
|
|
140
|
+
// rebase --show-current returned something but status was 0 — treat as incomplete
|
|
141
|
+
fmt.log.pass('Rebase round completed.');
|
|
142
|
+
fmt.log.warn('Rebase is still in progress (non-empty --show-current). Skipping automatic push.');
|
|
143
|
+
fmt.log.info(`Next: ${fmt.command((0, verification_js_1.formatVerificationCommand)(area, process.cwd()))}`);
|
|
144
|
+
fmt.log.info(`Next: ${fmt.command('git rebase --continue')}`);
|
|
145
|
+
exitFn(0);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
fmt.log.pass('Rebase completed cleanly.');
|
|
149
|
+
await performPush();
|
|
150
|
+
fmt.log.info(`Next: ${fmt.command((0, verification_js_1.formatVerificationCommand)(area, process.cwd()))}`);
|
|
151
|
+
fmt.log.info(`Next: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
|
|
152
|
+
exitFn(0);
|
|
153
|
+
return;
|
|
233
154
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
fmt.log.fail(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (output.toLowerCase().includes('pre-commit') || output.toLowerCase().includes('hook')) {
|
|
256
|
-
fmt.log.fail('Hint: A git hook failed. Fix the issues reported above.');
|
|
257
|
-
}
|
|
258
|
-
fmt.log.fail(`Check rebase state: ${fmt.command('git status')}`);
|
|
259
|
-
fmt.log.fail(`Recovery: ${fmt.command('git rebase --abort')}`);
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
/** @param {string} branchName */
|
|
263
|
-
const failContinueBudget = (branchName) => {
|
|
264
|
-
fmt.log.fail(`Rebase still in progress on branch ${fmt.branch(branchName || slug)} after ${continueAttempts} failed --continue attempt(s).`);
|
|
265
|
-
fmt.log.fail(`Check rebase state: ${fmt.command('git status')}`);
|
|
266
|
-
fmt.log.fail(`Next: ${fmt.command('git rebase --continue')}`);
|
|
267
|
-
fmt.log.fail(`If the current commit is empty: ${fmt.command('git rebase --skip')}`);
|
|
268
|
-
fmt.log.fail(`If recovery is needed: ${fmt.command('git rebase --abort')}`);
|
|
269
|
-
exitFn(1);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
for (const file of conflictResult.missionSpecificFiles) {
|
|
273
|
-
fmt.log.info(`Resolving: ${fmt.path(file)}`);
|
|
274
|
-
const checkoutResult = gitFn(['checkout', '--theirs', file]);
|
|
275
|
-
if (checkoutResult.status !== 0) {
|
|
276
|
-
// Try add -u as fallback (file may have been added/deleted)
|
|
277
|
-
gitFn(['add', file]);
|
|
278
|
-
}
|
|
279
|
-
const addResult = gitFn(['add', file]);
|
|
280
|
-
if (addResult.status !== 0) {
|
|
281
|
-
fmt.log.warn(`Could not add ${fmt.path(file)} for rebase continue.`);
|
|
282
|
-
}
|
|
155
|
+
// Rebase paused on conflicts — classify and resolve
|
|
156
|
+
const rebaseOutput = [rebaseResult.stdout, rebaseResult.stderr].filter(Boolean).join('\n').trim();
|
|
157
|
+
// Check if this is actually a conflict (contains CONFLICT lines) or another error
|
|
158
|
+
// Also handle localized output (e.g. Swedish "KONFLIKT")
|
|
159
|
+
const isConflict = /CONFLICT|KONFLIKT/i.test(rebaseOutput);
|
|
160
|
+
if (!isConflict) {
|
|
161
|
+
fmt.log.fail('Rebase failed with a non-conflict error.');
|
|
162
|
+
if (rebaseOutput) {
|
|
163
|
+
fmt.log.fail('--- Git Output ---');
|
|
164
|
+
fmt.log.fail(rebaseOutput);
|
|
165
|
+
fmt.log.fail('------------------');
|
|
166
|
+
}
|
|
167
|
+
if (rebaseResult.status === 128) {
|
|
168
|
+
fmt.log.fail('Hint: This might be a repository lock or an invalid upstream branch.');
|
|
169
|
+
}
|
|
170
|
+
else if (rebaseOutput.toLowerCase().includes('pre-commit') || rebaseOutput.toLowerCase().includes('hook')) {
|
|
171
|
+
fmt.log.fail('Hint: A git hook failed. Fix the issues reported above and try again.');
|
|
172
|
+
}
|
|
173
|
+
fmt.log.fail(`Recovery: ${fmt.command('git rebase --abort')}`);
|
|
174
|
+
exitFn(1);
|
|
175
|
+
return;
|
|
283
176
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
177
|
+
fmt.log.warn('Rebase paused on conflicts. Classifying...');
|
|
178
|
+
// Resolve the worktree for conflict classification
|
|
179
|
+
const rootDir = (0, mission_utils_js_1.getPrimaryWorktree)();
|
|
180
|
+
const worktreePath = (0, mission_utils_js_1.resolveWorktree)(slug) || (0, mission_utils_js_1.conventionalWorktreePath)(slug, rootDir);
|
|
181
|
+
/** @type{{worktreePathOverride?: string}} */
|
|
182
|
+
const conflictOpts = { worktreePathOverride: worktreePath };
|
|
183
|
+
const conflictResult = resolveConflictsFn(slug, area, conflictOpts);
|
|
184
|
+
// When rebase is in progress in the worktree, dry merge may fail.
|
|
185
|
+
// Fall back to parsing the rebase output directly.
|
|
186
|
+
if (conflictResult.ok === false && conflictResult.error === 'merge-failed') {
|
|
187
|
+
fmt.log.info('Dry merge failed (rebase in progress). Parsing rebase output directly...');
|
|
188
|
+
// Prefer git status --porcelain (authoritative index entries) over localized
|
|
189
|
+
// rebase text, which can contain advice prefixes that parse as false paths.
|
|
190
|
+
const statusFiles = parseConflictFilesFromGitStatus(worktreePath, gitFn);
|
|
191
|
+
const conflictFiles = statusFiles.length > 0
|
|
192
|
+
? statusFiles
|
|
193
|
+
: parseConflictFilesFromRebaseOutput(rebaseOutput);
|
|
194
|
+
// Classify using mission-specific patterns
|
|
195
|
+
const missionAbsDir = (0, mission_utils_js_1.findMissionDir)(slug, worktreePath);
|
|
196
|
+
const missionDocPrefix = missionAbsDir
|
|
197
|
+
? (node_path_1.default.relative(worktreePath, missionAbsDir) + '/')
|
|
198
|
+
: node_path_1.default.relative(worktreePath, (0, mission_utils_js_1.missionDirForSlug)(worktreePath, slug)).split(node_path_1.default.sep).join('/') + '/';
|
|
199
|
+
const taskPattern = new RegExp(`backlog/(?:tasks|completed)/[^/]*${slug}`);
|
|
200
|
+
const missionSpecificFiles = conflictFiles.filter((f) => f.startsWith(missionDocPrefix) || taskPattern.test(f));
|
|
201
|
+
const sharedFiles = conflictFiles.filter((f) => !missionSpecificFiles.includes(f));
|
|
202
|
+
Object.assign(conflictResult, {
|
|
203
|
+
ok: true,
|
|
204
|
+
conflictFiles,
|
|
205
|
+
missionSpecificFiles,
|
|
206
|
+
sharedFiles,
|
|
309
207
|
});
|
|
208
|
+
}
|
|
209
|
+
else if (!conflictResult.ok) {
|
|
210
|
+
/** @type{{error?: string}} */
|
|
211
|
+
const cr = conflictResult;
|
|
212
|
+
if (cr.error === 'worktree-missing') {
|
|
213
|
+
fmt.log.fail(`Mission worktree not found: ${fmt.path(worktreePath)}`);
|
|
214
|
+
fmt.log.info(`Ensure the worktree is registered: ${fmt.command(`git worktree add ${(0, mission_utils_js_1.conventionalWorktreePath)(slug, (0, mission_utils_js_1.getPrimaryWorktree)())} ${branch}`)}`);
|
|
215
|
+
exitFn(1);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
fmt.log.fail('Conflict detection failed.');
|
|
219
|
+
fmt.log.info(`Check rebase state: ${fmt.command('git status')}`);
|
|
220
|
+
exitFn(1);
|
|
310
221
|
return;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
222
|
+
}
|
|
223
|
+
// No conflicts detected by classification but rebase reported CONFLICT —
|
|
224
|
+
// fall through to shared-file handling with whatever git reports
|
|
225
|
+
const conflictFiles = conflictResult.conflictFiles.length > 0
|
|
226
|
+
? conflictResult.conflictFiles
|
|
227
|
+
: parseConflictFilesFromRebaseOutput(rebaseOutput);
|
|
228
|
+
// Only treat unclassified conflict files as shared-file conflicts
|
|
229
|
+
if (conflictResult.sharedFiles.length === 0 && conflictFiles.length > 0) {
|
|
230
|
+
const classified = new Set([...conflictResult.missionSpecificFiles]);
|
|
231
|
+
const unclassified = conflictFiles.filter((f) => !classified.has(f));
|
|
232
|
+
if (unclassified.length > 0) {
|
|
233
|
+
conflictResult.sharedFiles = unclassified;
|
|
321
234
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
235
|
+
}
|
|
236
|
+
// All conflicts are mission-specific — auto-resolve with --theirs
|
|
237
|
+
if (conflictResult.sharedFiles.length === 0) {
|
|
238
|
+
fmt.log.info(`All ${conflictResult.missionSpecificFiles.length} conflict(s) are mission-specific. Auto-resolving...`);
|
|
239
|
+
const maxContinueAttempts = 3;
|
|
240
|
+
let continueAttempts = 0;
|
|
241
|
+
/** @param {{stdio?: string}} opts */
|
|
242
|
+
const continueRebase = (opts = {}) => {
|
|
243
|
+
continueAttempts += 1;
|
|
244
|
+
return gitFn(['-c', 'core.editor=true', '-c', 'merge.autoedit=no', 'rebase', '--continue'], opts);
|
|
245
|
+
};
|
|
246
|
+
/** @param {{stdout: string, stderr: string, status: number}} result */
|
|
247
|
+
const reportContinueFailure = (result) => {
|
|
248
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
249
|
+
fmt.log.fail(`git rebase --continue failed (status ${result.status}).`);
|
|
250
|
+
if (output) {
|
|
251
|
+
fmt.log.fail('--- Git Output ---');
|
|
252
|
+
fmt.log.fail(output);
|
|
253
|
+
fmt.log.fail('------------------');
|
|
332
254
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const retryResult = continueRebase();
|
|
355
|
-
if (retryResult.status !== 0) {
|
|
356
|
-
const retryOutput = [retryResult.stdout, retryResult.stderr].filter(Boolean).join('\n').trim();
|
|
357
|
-
if (/CONFLICT|KONFLIKT/i.test(retryOutput)) {
|
|
358
|
-
fmt.log.info('More conflicts found after retry. Re-running classification...');
|
|
359
|
-
await rebase(args, {
|
|
360
|
-
inferSlugFn,
|
|
361
|
-
findMissionDirFn,
|
|
362
|
-
findMissionAreaFn,
|
|
363
|
-
getCurrentBranchFn,
|
|
364
|
-
resolveConflictsFn,
|
|
365
|
-
startAgentFn,
|
|
366
|
-
createPrFn,
|
|
367
|
-
readTokenFn,
|
|
368
|
-
resolveForgejoUserFn,
|
|
369
|
-
fetchReviewBranchFn,
|
|
370
|
-
resolveTaskFileFn,
|
|
371
|
-
getTaskImplementerFn,
|
|
372
|
-
gitFn,
|
|
373
|
-
exitFn,
|
|
374
|
-
isForgejoReviewEnabledFn,
|
|
375
|
-
});
|
|
376
|
-
return;
|
|
255
|
+
if (output.toLowerCase().includes('pre-commit') || output.toLowerCase().includes('hook')) {
|
|
256
|
+
fmt.log.fail('Hint: A git hook failed. Fix the issues reported above.');
|
|
257
|
+
}
|
|
258
|
+
fmt.log.fail(`Check rebase state: ${fmt.command('git status')}`);
|
|
259
|
+
fmt.log.fail(`Recovery: ${fmt.command('git rebase --abort')}`);
|
|
260
|
+
};
|
|
261
|
+
/** @param {string} branchName */
|
|
262
|
+
const failContinueBudget = (branchName) => {
|
|
263
|
+
fmt.log.fail(`Rebase still in progress on branch ${fmt.branch(branchName || slug)} after ${continueAttempts} failed --continue attempt(s).`);
|
|
264
|
+
fmt.log.fail(`Check rebase state: ${fmt.command('git status')}`);
|
|
265
|
+
fmt.log.fail(`Next: ${fmt.command('git rebase --continue')}`);
|
|
266
|
+
fmt.log.fail(`If the current commit is empty: ${fmt.command('git rebase --skip')}`);
|
|
267
|
+
fmt.log.fail(`If recovery is needed: ${fmt.command('git rebase --abort')}`);
|
|
268
|
+
exitFn(1);
|
|
269
|
+
};
|
|
270
|
+
for (const file of conflictResult.missionSpecificFiles) {
|
|
271
|
+
fmt.log.info(`Resolving: ${fmt.path(file)}`);
|
|
272
|
+
const checkoutResult = gitFn(['checkout', '--theirs', file]);
|
|
273
|
+
if (checkoutResult.status !== 0) {
|
|
274
|
+
// Try add -u as fallback (file may have been added/deleted)
|
|
275
|
+
gitFn(['add', file]);
|
|
377
276
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
277
|
+
const addResult = gitFn(['add', file]);
|
|
278
|
+
if (addResult.status !== 0) {
|
|
279
|
+
fmt.log.warn(`Could not add ${fmt.path(file)} for rebase continue.`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
fmt.log.info('Continuing rebase...');
|
|
283
|
+
let rebaseCompleted = false;
|
|
284
|
+
const continueResult = continueRebase();
|
|
285
|
+
if (continueResult.status !== 0) {
|
|
286
|
+
// Might have pre-commit hook or editor — check for more conflicts
|
|
287
|
+
const moreOutput = [continueResult.stdout, continueResult.stderr].filter(Boolean).join('\n').trim();
|
|
288
|
+
if (/CONFLICT|KONFLIKT/i.test(moreOutput)) {
|
|
289
|
+
fmt.log.info('More conflicts found. Re-running classification...');
|
|
290
|
+
// Recurse once more for chained conflicts, preserving flags and options
|
|
291
|
+
await rebase(args, {
|
|
292
|
+
inferSlugFn,
|
|
293
|
+
findMissionDirFn,
|
|
294
|
+
findMissionAreaFn,
|
|
295
|
+
getCurrentBranchFn,
|
|
296
|
+
resolveConflictsFn,
|
|
297
|
+
startAgentFn,
|
|
298
|
+
createPrFn,
|
|
299
|
+
readTokenFn,
|
|
300
|
+
resolveForgejoUserFn,
|
|
301
|
+
resolveTaskFileFn,
|
|
302
|
+
getTaskImplementerFn,
|
|
303
|
+
gitFn,
|
|
304
|
+
exitFn,
|
|
305
|
+
isForgejoReviewEnabledFn,
|
|
306
|
+
});
|
|
384
307
|
return;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
308
|
+
}
|
|
309
|
+
// Could be editor opening — check if rebase is still in progress
|
|
310
|
+
const statusResult = gitFn(['status', '--porcelain']);
|
|
311
|
+
if (statusResult.stdout.trim()) {
|
|
312
|
+
fmt.log.info('More changes detected. Continuing rebase...');
|
|
313
|
+
gitFn(['add', '-A']);
|
|
314
|
+
if (continueAttempts >= maxContinueAttempts) {
|
|
315
|
+
const rebaseCheck = gitFn(['rebase', '--show-current']);
|
|
316
|
+
failContinueBudget(rebaseCheck.stdout.trim());
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const cont2 = continueRebase();
|
|
320
|
+
if (cont2.status !== 0) {
|
|
321
|
+
// Verify whether a rebase is still in progress
|
|
322
|
+
const rebaseCheck = gitFn(['rebase', '--show-current']);
|
|
323
|
+
if (rebaseCheck.stdout.trim().length > 0) {
|
|
324
|
+
if (continueAttempts >= maxContinueAttempts) {
|
|
325
|
+
failContinueBudget(rebaseCheck.stdout.trim());
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
reportContinueFailure(cont2);
|
|
329
|
+
exitFn(1);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
reportContinueFailure(cont2);
|
|
398
334
|
exitFn(1);
|
|
399
|
-
|
|
400
|
-
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
rebaseCompleted = true;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// No staged changes and --continue failed but no conflict — rebase may have completed
|
|
341
|
+
const rebaseCheck = gitFn(['rebase', '--show-current']);
|
|
342
|
+
if (rebaseCheck.stdout.trim().length === 0) {
|
|
343
|
+
rebaseCompleted = true;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// Rebase still in progress with no unresolved conflicts: likely a hook
|
|
347
|
+
// failure or empty pick. Retry --continue with GIT_EDITOR=true to avoid
|
|
348
|
+
// spawning an interactive editor.
|
|
349
|
+
fmt.log.info('No unresolved conflicts; rebase still in progress. Retrying --continue (hook/empty-pick)...');
|
|
350
|
+
if (continueAttempts >= maxContinueAttempts) {
|
|
351
|
+
failContinueBudget(slug);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const retryResult = continueRebase();
|
|
355
|
+
if (retryResult.status !== 0) {
|
|
356
|
+
const retryOutput = [retryResult.stdout, retryResult.stderr].filter(Boolean).join('\n').trim();
|
|
357
|
+
if (/CONFLICT|KONFLIKT/i.test(retryOutput)) {
|
|
358
|
+
fmt.log.info('More conflicts found after retry. Re-running classification...');
|
|
359
|
+
await rebase(args, {
|
|
360
|
+
inferSlugFn,
|
|
361
|
+
findMissionDirFn,
|
|
362
|
+
findMissionAreaFn,
|
|
363
|
+
getCurrentBranchFn,
|
|
364
|
+
resolveConflictsFn,
|
|
365
|
+
startAgentFn,
|
|
366
|
+
createPrFn,
|
|
367
|
+
readTokenFn,
|
|
368
|
+
resolveForgejoUserFn,
|
|
369
|
+
fetchReviewBranchFn,
|
|
370
|
+
resolveTaskFileFn,
|
|
371
|
+
getTaskImplementerFn,
|
|
372
|
+
gitFn,
|
|
373
|
+
exitFn,
|
|
374
|
+
isForgejoReviewEnabledFn,
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Another empty-pick or hook — verify again
|
|
379
|
+
const recheck = gitFn(['rebase', '--show-current']);
|
|
380
|
+
if (recheck.stdout.trim().length > 0) {
|
|
381
|
+
fmt.log.info('Empty pick detected; continuing to next commit...');
|
|
382
|
+
if (continueAttempts >= maxContinueAttempts) {
|
|
383
|
+
failContinueBudget(recheck.stdout.trim());
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const cont3 = continueRebase();
|
|
387
|
+
if (cont3.status === 0) {
|
|
388
|
+
rebaseCompleted = true;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const recheck2 = gitFn(['rebase', '--show-current']);
|
|
392
|
+
if (recheck2.stdout.trim().length === 0) {
|
|
393
|
+
rebaseCompleted = true;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
if (continueAttempts >= maxContinueAttempts) {
|
|
397
|
+
failContinueBudget(recheck2.stdout.trim());
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
reportContinueFailure(cont3);
|
|
401
|
+
exitFn(1);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
rebaseCompleted = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
rebaseCompleted = true;
|
|
413
|
+
}
|
|
401
414
|
}
|
|
402
|
-
}
|
|
403
|
-
} else {
|
|
404
|
-
rebaseCompleted = true;
|
|
405
415
|
}
|
|
406
|
-
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
407
418
|
rebaseCompleted = true;
|
|
408
|
-
}
|
|
409
419
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
if (rebaseCompleted) {
|
|
421
|
+
fmt.log.pass('Mission-specific conflicts resolved. Rebase completed.');
|
|
422
|
+
await performPush();
|
|
423
|
+
fmt.log.info(`Next: ${fmt.command((0, verification_js_1.formatVerificationCommand)(area, process.cwd()))}`);
|
|
424
|
+
fmt.log.info(`Next: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
|
|
425
|
+
exitFn(0);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
413
428
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
429
|
+
// Shared-file conflicts exist — launch agent to resolve them
|
|
430
|
+
fmt.log.info(`${conflictResult.sharedFiles.length} shared file(s) require agent-assisted resolution:`);
|
|
431
|
+
conflictResult.sharedFiles.forEach((f) => fmt.log.info(` - ${fmt.path(f)}`));
|
|
432
|
+
const prompt = buildRebasePrompt({
|
|
433
|
+
slug,
|
|
434
|
+
area,
|
|
435
|
+
worktreePath,
|
|
436
|
+
missionSpecificFiles: conflictResult.missionSpecificFiles,
|
|
437
|
+
sharedFiles: conflictResult.sharedFiles,
|
|
438
|
+
gitFn: gitFn,
|
|
439
|
+
});
|
|
440
|
+
fmt.log.info('Launching agent for conflict resolution...');
|
|
441
|
+
const { agent, result: agentResult } = await startAgentFn('conflict-resolution', {
|
|
442
|
+
prompt,
|
|
443
|
+
worktree: worktreePath,
|
|
444
|
+
});
|
|
445
|
+
if (agentResult.status !== 0) {
|
|
446
|
+
fmt.log.fail(`Agent (${fmt.agent(agent)}) exited with status ${agentResult.status}.`);
|
|
447
|
+
fmt.log.info(`You may need to abort the rebase: ${fmt.command('git rebase --abort')}`);
|
|
448
|
+
exitFn(agentResult.status || 1);
|
|
449
|
+
return;
|
|
422
450
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
fmt.log.info('Launching agent for conflict resolution...');
|
|
439
|
-
const { agent, result: agentResult } = await startAgentFn('conflict-resolution', {
|
|
440
|
-
prompt,
|
|
441
|
-
worktree: worktreePath,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
if (agentResult.status !== 0) {
|
|
445
|
-
fmt.log.fail(`Agent (${fmt.agent(agent)}) exited with status ${agentResult.status}.`);
|
|
446
|
-
fmt.log.info(`You may need to abort the rebase: ${fmt.command('git rebase --abort')}`);
|
|
447
|
-
exitFn(agentResult.status || 1);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Verify rebase is actually complete before pushing
|
|
452
|
-
const finalRebaseCheck = gitFn(['rebase', '--show-current']);
|
|
453
|
-
if (finalRebaseCheck.stdout.trim().length > 0) {
|
|
454
|
-
fmt.log.pass(`Agent (${fmt.agent(agent)}) completed their round.`);
|
|
455
|
-
fmt.log.warn('Rebase is still in progress. Skipping automatic push.');
|
|
456
|
-
fmt.log.info('Next: Resolve remaining conflicts or continue rebase.');
|
|
451
|
+
// Verify rebase is actually complete before pushing
|
|
452
|
+
const finalRebaseCheck = gitFn(['rebase', '--show-current']);
|
|
453
|
+
if (finalRebaseCheck.stdout.trim().length > 0) {
|
|
454
|
+
fmt.log.pass(`Agent (${fmt.agent(agent)}) completed their round.`);
|
|
455
|
+
fmt.log.warn('Rebase is still in progress. Skipping automatic push.');
|
|
456
|
+
fmt.log.info('Next: Resolve remaining conflicts or continue rebase.');
|
|
457
|
+
exitFn(0);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
fmt.log.pass(`Agent (${fmt.agent(agent)}) completed conflict resolution.`);
|
|
461
|
+
await performPush();
|
|
462
|
+
fmt.log.info(`Next: ${fmt.command((0, verification_js_1.formatVerificationCommand)(area, process.cwd()))}`);
|
|
463
|
+
fmt.log.info(`Next: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
|
|
457
464
|
exitFn(0);
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
fmt.log.pass(`Agent (${fmt.agent(agent)}) completed conflict resolution.`);
|
|
462
|
-
await performPush();
|
|
463
|
-
fmt.log.info(`Next: ${fmt.command(formatVerificationCommand(area, process.cwd()))}`);
|
|
464
|
-
fmt.log.info(`Next: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
|
|
465
|
-
exitFn(0);
|
|
466
465
|
}
|
|
467
|
-
|
|
468
466
|
/**
|
|
469
467
|
* Parse conflict file paths from git status --porcelain output
|
|
470
468
|
* when a rebase is in progress. Handles all unmerged states:
|
|
@@ -472,164 +470,190 @@ async function rebase(args, {
|
|
|
472
470
|
*/
|
|
473
471
|
/** @param {string} worktreePath @param {Function} gitFn */
|
|
474
472
|
function parseConflictFilesFromGitStatus(worktreePath, gitFn) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
473
|
+
const statusResult = gitFn(['-C', worktreePath, 'status', '--porcelain']);
|
|
474
|
+
const files = [];
|
|
475
|
+
const unmergedStates = new Set(['UU', 'DU', 'UD', 'AU', 'UA', 'AA']);
|
|
476
|
+
for (const line of (statusResult.stdout || '').split('\n')) {
|
|
477
|
+
const trimmed = line.trim();
|
|
478
|
+
if (!trimmed) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const match = trimmed.match(/^([A-Z]{2})\s+(.+)$/);
|
|
482
|
+
if (match) {
|
|
483
|
+
const code = match[1];
|
|
484
|
+
if (!unmergedStates.has(code)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
let file = match[2].trim();
|
|
488
|
+
// Remove surrounding quotes if present
|
|
489
|
+
if ((file.startsWith('"') && file.endsWith('"')) || (file.startsWith("'") && file.endsWith("'"))) {
|
|
490
|
+
file = file.slice(1, -1);
|
|
491
|
+
}
|
|
492
|
+
if (file) {
|
|
493
|
+
files.push(file);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
491
496
|
}
|
|
492
|
-
|
|
493
|
-
return [...new Set(files)];
|
|
497
|
+
return [...new Set(files)];
|
|
494
498
|
}
|
|
495
|
-
|
|
496
499
|
/**
|
|
497
500
|
* Parse conflict file paths from git rebase output.
|
|
498
501
|
* Similar to parseConflictFilesFromMergeOutput but handles rebase-specific output.
|
|
499
502
|
*/
|
|
500
503
|
/** @param {string} output */
|
|
501
504
|
function parseConflictFilesFromRebaseOutput(output) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
505
|
+
const seen = new Set();
|
|
506
|
+
const files = [];
|
|
507
|
+
for (const line of output.split('\n')) {
|
|
508
|
+
if (!/CONFLICT|KONFLIKT/i.test(line)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
// English: "Merge conflict in <file>"
|
|
512
|
+
const inMatch = line.match(/Merge conflict in (.+)$/);
|
|
513
|
+
if (inMatch) {
|
|
514
|
+
let f = inMatch[1].trim();
|
|
515
|
+
// Strip modify/delete description (e.g. ": deleted by master, modified by HEAD")
|
|
516
|
+
f = f.replace(/\s*:\s*(deleted|modified|added|removed|renamed|both|ours|yours|theirs|by\s*\w+,\s*(modified|deleted)\b).*$/i, '').trim();
|
|
517
|
+
if (f && !seen.has(f)) {
|
|
518
|
+
seen.add(f);
|
|
519
|
+
files.push(f);
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Swedish: "Sammanslagningskonflikt i <file>"
|
|
524
|
+
const svMatch = line.match(/Sammanslagningskonflikt\s+i\s+(.+)$/);
|
|
525
|
+
if (svMatch) {
|
|
526
|
+
let f = svMatch[1].trim();
|
|
527
|
+
// Strip modify/delete description (e.g. ": deleted/raderad av master, modified/ändrad av HEAD")
|
|
528
|
+
f = f.replace(/\s*:\s*(deleted|modified|added|removed|renamed|both|ours|yours|theirs|raderad|ändrad|lagd till|borttagen|by|av|av\s*\w+,\s*(modified|ändrad|deleted|raderad)\b).*$/i, '').trim();
|
|
529
|
+
if (f && !seen.has(f)) {
|
|
530
|
+
seen.add(f);
|
|
531
|
+
files.push(f);
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
// Swedish modify/delete: "KONFLIKT (ändra/radera): <file> raderad i <commit> ... och ändrad i HEAD"
|
|
536
|
+
const svModDelMatch = line.match(/^KONFLIKT\s+\(ändra\/radera\)\s*:\s*(.+)$/i);
|
|
537
|
+
if (svModDelMatch) {
|
|
538
|
+
let f = svModDelMatch[1].trim();
|
|
539
|
+
// Strip trailing "Versionen HEAD av <file> lämnad i trädet." sentence (real git output)
|
|
540
|
+
// Must come before raderad/ändrad stripping since the trailing sentence contains "ändrad"
|
|
541
|
+
f = f.replace(/\s+Versionen\s+HEAD[\s\S]*$/i, '').trim();
|
|
542
|
+
// Strip "raderad i <commit> ... och ändrad i HEAD" (Swedish modify/delete description)
|
|
543
|
+
f = f.replace(/\s+raderad\s+i\s+\S+(?:\s*\([^)]*\))?\s+(?:och|and)\s+ändrad\s+i\s+\S+\.?\s*$/i, '').trim();
|
|
544
|
+
// Also handle "raderad i <commit> och ändrad i HEAD" without trailing period
|
|
545
|
+
f = f.replace(/\s+raderad\s+i\s+\S+(?:\s*\([^)]*\))?\s+och\s+ändrad\s+i\s+\S+\s*$/i, '').trim();
|
|
546
|
+
if (f && !seen.has(f)) {
|
|
547
|
+
seen.add(f);
|
|
548
|
+
files.push(f);
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
// Generic: try to extract a file path from the line using colon-delimited segments.
|
|
553
|
+
// Common patterns:
|
|
554
|
+
// "CONFLICT (content): Merge conflict in <file>" -> handled above
|
|
555
|
+
// "<file>: <description>" -> generic fallback
|
|
556
|
+
// "CONFLICT (content): <file>: <description>" -> nested colons
|
|
557
|
+
// "CONFLICT (modify/delete): <file>: deleted by ..., modified by ..." -> modify/delete
|
|
558
|
+
// "CONFLICT (modify/delete): <file>: ..." -> modify/delete header
|
|
559
|
+
const colonIdx = line.indexOf(':');
|
|
560
|
+
if (colonIdx !== -1) {
|
|
561
|
+
const beforeColon = line.slice(0, colonIdx).trim();
|
|
562
|
+
const afterColon = line.slice(colonIdx + 1).trim();
|
|
563
|
+
const isNonPathPrefix = /^(CONFLICT|KONFLIKT|CONFLICTS|Merge conflict|Sammanslagningskonflikt|Automatic merge|Auto-merging|resolved|merged|deleted|added|changed|modified|rejected|skipped|dropped|superseded|discarded|kept|stashed|applied|already|would|both|ours|yours|their|his|her|its|your|my|us|we|they|he|she|it|a|an|the|but|and|or|for|nor|not|so|yet)\b/i.test(beforeColon);
|
|
564
|
+
if (!isNonPathPrefix) {
|
|
565
|
+
// Skip known advice/hint labels — the rest of the line is not a path.
|
|
566
|
+
if (/^(tips|hint|note)\b/i.test(beforeColon)) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const f = beforeColon;
|
|
570
|
+
if (f && !seen.has(f)) {
|
|
571
|
+
seen.add(f);
|
|
572
|
+
files.push(f);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else if (afterColon) {
|
|
576
|
+
// Skip the prefix and look for a path after the first colon.
|
|
577
|
+
// If there's a second colon, take the segment before it as the path.
|
|
578
|
+
const secondColonIdx = afterColon.indexOf(':');
|
|
579
|
+
if (secondColonIdx !== -1) {
|
|
580
|
+
let f = afterColon.slice(0, secondColonIdx).trim();
|
|
581
|
+
// Strip modify/delete description (e.g. ": deleted by master, modified by HEAD")
|
|
582
|
+
f = f.replace(/\s*:\s*(deleted|modified|added|removed|renamed|both|ours|yours|theirs|raderad|ändrad|lagd till|borttagen|by|av|av\s*\w+,\s*(modified|ändrad|deleted|raderad)\b).*$/i, '');
|
|
583
|
+
f = f.trim();
|
|
584
|
+
if (f && !seen.has(f)) {
|
|
585
|
+
seen.add(f);
|
|
586
|
+
files.push(f);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
// No second colon — take everything after the first colon as the path.
|
|
591
|
+
const f = afterColon;
|
|
592
|
+
if (f && !seen.has(f)) {
|
|
593
|
+
seen.add(f);
|
|
594
|
+
files.push(f);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
570
598
|
}
|
|
571
|
-
}
|
|
572
599
|
}
|
|
573
|
-
|
|
574
|
-
return files;
|
|
600
|
+
return files;
|
|
575
601
|
}
|
|
576
|
-
|
|
577
602
|
/**
|
|
578
603
|
* Build the prompt for the agent during shared-file conflict resolution.
|
|
579
604
|
*/
|
|
580
605
|
/** @param {{slug: string, area: string, worktreePath: string, missionSpecificFiles: string[], sharedFiles: string[], gitFn?: Function}} opts */
|
|
581
|
-
function buildRebasePrompt({ slug, area, worktreePath, missionSpecificFiles, sharedFiles, gitFn =
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
let primaryBranch = 'main';
|
|
588
|
-
try {
|
|
589
|
-
primaryBranch = resolveMissionBaseBranch(slug, worktreePath, { gitFn });
|
|
590
|
-
} catch (_) {
|
|
606
|
+
function buildRebasePrompt({ slug, area, worktreePath, missionSpecificFiles, sharedFiles, gitFn = undefined }) {
|
|
607
|
+
const missionFileCommands = missionSpecificFiles
|
|
608
|
+
.map(f => ` git checkout --theirs "${f}" && git add "${f}"`)
|
|
609
|
+
.join('\n');
|
|
610
|
+
const sharedFileList = sharedFiles.map(f => ` - ${f}`).join('\n');
|
|
611
|
+
let primaryBranch = 'main';
|
|
591
612
|
try {
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
613
|
+
primaryBranch = (0, mission_utils_js_1.resolveMissionBaseBranch)(slug, worktreePath, { gitFn });
|
|
614
|
+
}
|
|
615
|
+
catch (_) {
|
|
616
|
+
try {
|
|
617
|
+
primaryBranch = (0, mission_utils_js_1.getPrimaryBranch)(worktreePath, gitFn);
|
|
618
|
+
}
|
|
619
|
+
catch (_e) {
|
|
620
|
+
primaryBranch = 'main';
|
|
621
|
+
}
|
|
595
622
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
'- If any command fails, stop and report the failure.',
|
|
629
|
-
].join('\n');
|
|
623
|
+
return [
|
|
624
|
+
'Mode: rebase conflict-resolution.',
|
|
625
|
+
'',
|
|
626
|
+
`Mission: ${slug}`,
|
|
627
|
+
`Mission worktree: ${worktreePath}`,
|
|
628
|
+
'',
|
|
629
|
+
`Rebase onto ${primaryBranch} paused on conflicts.`,
|
|
630
|
+
'',
|
|
631
|
+
'Step 1 — Resolve mission-specific conflicts (--theirs):',
|
|
632
|
+
missionFileCommands.length > 0
|
|
633
|
+
? missionFileCommands
|
|
634
|
+
: ' (none — all conflicts are shared files)',
|
|
635
|
+
'',
|
|
636
|
+
'Step 2 — Resolve shared-file conflicts:',
|
|
637
|
+
sharedFileList,
|
|
638
|
+
'',
|
|
639
|
+
'Step 3 — After resolving each shared file:',
|
|
640
|
+
' git add <resolved-files>',
|
|
641
|
+
' git rebase --continue',
|
|
642
|
+
'',
|
|
643
|
+
'Step 4 — Repeat Steps 1-3 until rebase completes.',
|
|
644
|
+
'',
|
|
645
|
+
'Step 5 — Verify:',
|
|
646
|
+
` ${(0, verification_js_1.formatVerificationCommand)(area, worktreePath)}`,
|
|
647
|
+
` px integrate ${slug} --dry-run`,
|
|
648
|
+
'',
|
|
649
|
+
'Rules:',
|
|
650
|
+
'- Take --theirs for every mission-specific file listed above.',
|
|
651
|
+
'- For shared files, inspect the conflict markers and resolve sensibly.',
|
|
652
|
+
'- If rebase pauses again, repeat the process.',
|
|
653
|
+
'- If any command fails, stop and report the failure.',
|
|
654
|
+
].join('\n');
|
|
630
655
|
}
|
|
631
|
-
|
|
656
|
+
rebase.buildRebasePrompt = buildRebasePrompt;
|
|
657
|
+
rebase.parseConflictFilesFromRebaseOutput = parseConflictFilesFromRebaseOutput;
|
|
658
|
+
rebase.parseConflictFilesFromGitStatus = parseConflictFilesFromGitStatus;
|
|
632
659
|
module.exports = rebase;
|
|
633
|
-
module.exports.buildRebasePrompt = buildRebasePrompt;
|
|
634
|
-
module.exports.parseConflictFilesFromRebaseOutput = parseConflictFilesFromRebaseOutput;
|
|
635
|
-
module.exports.parseConflictFilesFromGitStatus = parseConflictFilesFromGitStatus;
|