edsger 0.19.14 ā 0.19.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/phases/code-refine/index.js +25 -4
- package/dist/phases/code-review/index.js +25 -4
- package/dist/services/branches.d.ts +11 -2
- package/dist/services/branches.js +32 -2
- package/dist/utils/conflict-resolver.d.ts +43 -0
- package/dist/utils/conflict-resolver.js +185 -0
- package/dist/utils/git-branch-manager.d.ts +117 -0
- package/dist/utils/git-branch-manager.js +379 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
|
|
|
9
9
|
import { fetchCodeRefineContext, } from './context.js';
|
|
10
10
|
import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
|
|
11
11
|
import { createSystemPrompt, createCodeRefinePrompt } from './prompts.js';
|
|
12
|
-
import {
|
|
12
|
+
import { preparePhaseGitEnvironmentAsync, prepareCustomBranchGitEnvironmentAsync, hasUncommittedChanges, getUncommittedFiles, } from '../../utils/git-branch-manager.js';
|
|
13
13
|
import { getFeature } from '../../api/features/get-feature.js';
|
|
14
14
|
import { getBranches, getBaseBranchInfo, updateBranch, } from '../../services/branches.js';
|
|
15
15
|
import { verifyAndResolveComments, } from '../code-refine-verification/index.js';
|
|
@@ -76,6 +76,7 @@ export const refineCodeFromPRFeedback = async (options, config) => {
|
|
|
76
76
|
let currentBranch = null;
|
|
77
77
|
let allBranches = [];
|
|
78
78
|
let baseBranchForRebase = 'main'; // Default base branch for rebase
|
|
79
|
+
let originalBaseBranchForRebase; // For --onto when base was squash-merged
|
|
79
80
|
try {
|
|
80
81
|
// Get all branches to determine base branch info
|
|
81
82
|
allBranches = await getBranches({ featureId, verbose });
|
|
@@ -94,12 +95,17 @@ export const refineCodeFromPRFeedback = async (options, config) => {
|
|
|
94
95
|
// - Base branch merged: returns feat branch (dev/xxx -> feat/xxx)
|
|
95
96
|
// - Base branch not merged: returns dev branch
|
|
96
97
|
baseBranchForRebase = baseBranchInfo.baseBranch;
|
|
98
|
+
// When base branch is merged (squash merge), we need originalBaseBranch for --onto
|
|
99
|
+
originalBaseBranchForRebase = baseBranchInfo.originalBaseBranch;
|
|
97
100
|
if (verbose) {
|
|
98
101
|
if (!currentBranch.base_branch_id) {
|
|
99
102
|
// No base branch - first branch in chain
|
|
100
103
|
}
|
|
101
104
|
else if (baseBranchInfo.baseBranchMerged) {
|
|
102
105
|
logInfo(` Base branch is merged, will rebase from feat branch: ${baseBranchForRebase}`);
|
|
106
|
+
if (originalBaseBranchForRebase) {
|
|
107
|
+
logInfo(` Using --onto from original base: ${originalBaseBranchForRebase}`);
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
else {
|
|
105
111
|
logInfo(` Will rebase from base branch: ${baseBranchForRebase}`);
|
|
@@ -136,9 +142,24 @@ export const refineCodeFromPRFeedback = async (options, config) => {
|
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
// Prepare git environment: switch to the appropriate branch and rebase from correct base
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
// Use async version with automatic conflict resolution
|
|
146
|
+
const gitEnvResult = currentBranch
|
|
147
|
+
? await prepareCustomBranchGitEnvironmentAsync({
|
|
148
|
+
featureBranch: branchName,
|
|
149
|
+
baseBranch: baseBranchForRebase,
|
|
150
|
+
originalBaseBranch: originalBaseBranchForRebase,
|
|
151
|
+
verbose,
|
|
152
|
+
resolveConflicts: true,
|
|
153
|
+
conflictResolverConfig: {
|
|
154
|
+
model: config.claude.model || 'sonnet',
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
: await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, { model: config.claude.model || 'sonnet' });
|
|
158
|
+
const cleanupGit = gitEnvResult.cleanup;
|
|
159
|
+
// Log if conflicts were automatically resolved
|
|
160
|
+
if (gitEnvResult.conflictsResolved && verbose) {
|
|
161
|
+
logInfo(`š§ Automatically resolved ${gitEnvResult.resolvedFiles?.length || 0} conflict(s) during rebase`);
|
|
162
|
+
}
|
|
142
163
|
try {
|
|
143
164
|
// Fetch initial code refine context (PR reviews and comments)
|
|
144
165
|
if (verbose) {
|
|
@@ -7,7 +7,7 @@ import { logInfo, logError } from '../../utils/logger.js';
|
|
|
7
7
|
import { Octokit } from '@octokit/rest';
|
|
8
8
|
import { fetchCodeReviewContext, formatContextForPrompt, } from './context.js';
|
|
9
9
|
import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
|
|
10
|
-
import {
|
|
10
|
+
import { preparePhaseGitEnvironmentAsync, prepareCustomBranchGitEnvironmentAsync, } from '../../utils/git-branch-manager.js';
|
|
11
11
|
import { getBranches, getBaseBranchInfo, updateBranch, } from '../../services/branches.js';
|
|
12
12
|
import { getFeature } from '../../api/features/get-feature.js';
|
|
13
13
|
function userMessage(content) {
|
|
@@ -128,6 +128,7 @@ export const reviewPullRequest = async (options, config) => {
|
|
|
128
128
|
let currentBranch = null;
|
|
129
129
|
let allBranches = [];
|
|
130
130
|
let baseBranchForRebase = 'main'; // Default base branch for rebase
|
|
131
|
+
let originalBaseBranchForRebase; // For --onto when base was squash-merged
|
|
131
132
|
try {
|
|
132
133
|
// Get all branches to determine base branch info
|
|
133
134
|
allBranches = await getBranches({ featureId, verbose });
|
|
@@ -147,12 +148,17 @@ export const reviewPullRequest = async (options, config) => {
|
|
|
147
148
|
// - Base branch merged: returns feat branch (dev/xxx -> feat/xxx)
|
|
148
149
|
// - Base branch not merged: returns dev branch
|
|
149
150
|
baseBranchForRebase = baseBranchInfo.baseBranch;
|
|
151
|
+
// When base branch is merged (squash merge), we need originalBaseBranch for --onto
|
|
152
|
+
originalBaseBranchForRebase = baseBranchInfo.originalBaseBranch;
|
|
150
153
|
if (verbose) {
|
|
151
154
|
if (!currentBranch.base_branch_id) {
|
|
152
155
|
// No base branch - first branch in chain
|
|
153
156
|
}
|
|
154
157
|
else if (baseBranchInfo.baseBranchMerged) {
|
|
155
158
|
logInfo(` Base branch is merged, will rebase from feat branch: ${baseBranchForRebase}`);
|
|
159
|
+
if (originalBaseBranchForRebase) {
|
|
160
|
+
logInfo(` Using --onto from original base: ${originalBaseBranchForRebase}`);
|
|
161
|
+
}
|
|
156
162
|
}
|
|
157
163
|
else {
|
|
158
164
|
logInfo(` Will rebase from base branch: ${baseBranchForRebase}`);
|
|
@@ -189,9 +195,24 @@ export const reviewPullRequest = async (options, config) => {
|
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
// Prepare git environment: switch to the appropriate branch and rebase from correct base
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
// Use async version with automatic conflict resolution
|
|
199
|
+
const gitEnvResult = currentBranch
|
|
200
|
+
? await prepareCustomBranchGitEnvironmentAsync({
|
|
201
|
+
featureBranch: branchName,
|
|
202
|
+
baseBranch: baseBranchForRebase,
|
|
203
|
+
originalBaseBranch: originalBaseBranchForRebase,
|
|
204
|
+
verbose,
|
|
205
|
+
resolveConflicts: true,
|
|
206
|
+
conflictResolverConfig: {
|
|
207
|
+
model: config.claude.model || 'sonnet',
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
: await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, { model: config.claude.model || 'sonnet' });
|
|
211
|
+
const cleanupGit = gitEnvResult.cleanup;
|
|
212
|
+
// Log if conflicts were automatically resolved
|
|
213
|
+
if (gitEnvResult.conflictsResolved && verbose) {
|
|
214
|
+
logInfo(`š§ Automatically resolved ${gitEnvResult.resolvedFiles?.length || 0} conflict(s) during rebase`);
|
|
215
|
+
}
|
|
195
216
|
try {
|
|
196
217
|
// Fetch code review context (PR data, files, commits)
|
|
197
218
|
if (verbose) {
|
|
@@ -12,7 +12,7 @@ export interface Branch {
|
|
|
12
12
|
base_branch_id: string | null;
|
|
13
13
|
pull_request_url: string | null;
|
|
14
14
|
pull_request_number: number | null;
|
|
15
|
-
status: 'pending' | 'in_progress' | 'ready_for_review' | 'reviewed' | 'refined' | 'merged' | 'closed';
|
|
15
|
+
status: 'pending' | 'in_progress' | 'ready_for_review' | 'reviewed' | 'refined' | 'merged' | 'completed' | 'closed';
|
|
16
16
|
created_at: string;
|
|
17
17
|
updated_at: string;
|
|
18
18
|
}
|
|
@@ -56,19 +56,28 @@ export declare function hasMultipleBranches(options: PipelinePhaseOptions): Prom
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function getNextPendingBranch(options: PipelinePhaseOptions): Promise<Branch | null>;
|
|
58
58
|
/**
|
|
59
|
-
* Check if all branches are completed
|
|
59
|
+
* Check if all branches are completed (feat merged to main)
|
|
60
60
|
*/
|
|
61
61
|
export declare function allBranchesCompleted(options: PipelinePhaseOptions): Promise<boolean>;
|
|
62
62
|
/**
|
|
63
63
|
* Get the base branch information for a branch.
|
|
64
64
|
* Returns:
|
|
65
65
|
* - If base_branch_id is null: use main (first branch in chain)
|
|
66
|
+
* - If base_branch_id is set and that branch is completed: use main
|
|
67
|
+
* (completed means feat merged to main)
|
|
66
68
|
* - If base_branch_id is set and that branch is merged: use base branch's feat branch
|
|
67
69
|
* (merged means dev merged to feat, not to main)
|
|
68
70
|
* - If base_branch_id is set and that branch is not merged: use base branch's dev branch
|
|
71
|
+
*
|
|
72
|
+
* When base branch is merged/completed (squash merge), we need to use `git rebase --onto` to
|
|
73
|
+
* correctly rebase only the new commits. This function returns both the new base
|
|
74
|
+
* and the original base for this purpose:
|
|
75
|
+
* - completed: baseBranch=main, originalBaseBranch=feat/xxx
|
|
76
|
+
* - merged: baseBranch=feat/xxx, originalBaseBranch=dev/xxx
|
|
69
77
|
*/
|
|
70
78
|
export declare function getBaseBranchInfo(branch: Branch, allBranches: Branch[], mainBranch?: string): Promise<{
|
|
71
79
|
baseBranch: string;
|
|
80
|
+
originalBaseBranch?: string;
|
|
72
81
|
needsRebase: boolean;
|
|
73
82
|
baseBranchMerged: boolean;
|
|
74
83
|
}>;
|
|
@@ -97,6 +97,8 @@ export function formatBranchesForContext(branches) {
|
|
|
97
97
|
}
|
|
98
98
|
const getStatusEmoji = (status) => {
|
|
99
99
|
switch (status) {
|
|
100
|
+
case 'completed':
|
|
101
|
+
return 'š';
|
|
100
102
|
case 'merged':
|
|
101
103
|
return 'ā
';
|
|
102
104
|
case 'refined':
|
|
@@ -148,21 +150,29 @@ export async function getNextPendingBranch(options) {
|
|
|
148
150
|
return branches.find((b) => b.status === 'pending') || null;
|
|
149
151
|
}
|
|
150
152
|
/**
|
|
151
|
-
* Check if all branches are completed
|
|
153
|
+
* Check if all branches are completed (feat merged to main)
|
|
152
154
|
*/
|
|
153
155
|
export async function allBranchesCompleted(options) {
|
|
154
156
|
const branches = await getBranches(options);
|
|
155
157
|
if (branches.length === 0)
|
|
156
158
|
return true;
|
|
157
|
-
return branches.every((b) => b.status === 'merged' || b.status === 'closed');
|
|
159
|
+
return branches.every((b) => b.status === 'completed' || b.status === 'merged' || b.status === 'closed');
|
|
158
160
|
}
|
|
159
161
|
/**
|
|
160
162
|
* Get the base branch information for a branch.
|
|
161
163
|
* Returns:
|
|
162
164
|
* - If base_branch_id is null: use main (first branch in chain)
|
|
165
|
+
* - If base_branch_id is set and that branch is completed: use main
|
|
166
|
+
* (completed means feat merged to main)
|
|
163
167
|
* - If base_branch_id is set and that branch is merged: use base branch's feat branch
|
|
164
168
|
* (merged means dev merged to feat, not to main)
|
|
165
169
|
* - If base_branch_id is set and that branch is not merged: use base branch's dev branch
|
|
170
|
+
*
|
|
171
|
+
* When base branch is merged/completed (squash merge), we need to use `git rebase --onto` to
|
|
172
|
+
* correctly rebase only the new commits. This function returns both the new base
|
|
173
|
+
* and the original base for this purpose:
|
|
174
|
+
* - completed: baseBranch=main, originalBaseBranch=feat/xxx
|
|
175
|
+
* - merged: baseBranch=feat/xxx, originalBaseBranch=dev/xxx
|
|
166
176
|
*/
|
|
167
177
|
export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main') {
|
|
168
178
|
// No base branch - start from main (first branch in chain)
|
|
@@ -183,6 +193,25 @@ export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main'
|
|
|
183
193
|
baseBranchMerged: true,
|
|
184
194
|
};
|
|
185
195
|
}
|
|
196
|
+
// Check if base branch is completed (feat merged to main)
|
|
197
|
+
if (baseBranch.status === 'completed') {
|
|
198
|
+
// Base branch's feat is merged to main - rebase directly from main
|
|
199
|
+
// Use feat branch as originalBaseBranch for --onto
|
|
200
|
+
if (!baseBranch.branch_name) {
|
|
201
|
+
return {
|
|
202
|
+
baseBranch: mainBranch,
|
|
203
|
+
needsRebase: false,
|
|
204
|
+
baseBranchMerged: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const featBranchName = baseBranch.branch_name.replace(/^dev\//, 'feat/');
|
|
208
|
+
return {
|
|
209
|
+
baseBranch: mainBranch,
|
|
210
|
+
originalBaseBranch: featBranchName, // Use feat branch for --onto
|
|
211
|
+
needsRebase: true,
|
|
212
|
+
baseBranchMerged: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
186
215
|
// Check if base branch is merged (dev merged to feat)
|
|
187
216
|
if (baseBranch.status === 'merged') {
|
|
188
217
|
// Base branch's dev is merged to feat - rebase from base branch's feat branch
|
|
@@ -197,6 +226,7 @@ export async function getBaseBranchInfo(branch, allBranches, mainBranch = 'main'
|
|
|
197
226
|
const featBranchName = baseBranch.branch_name.replace(/^dev\//, 'feat/');
|
|
198
227
|
return {
|
|
199
228
|
baseBranch: featBranchName,
|
|
229
|
+
originalBaseBranch: baseBranch.branch_name, // Return original dev branch for --onto
|
|
200
230
|
needsRebase: true,
|
|
201
231
|
baseBranchMerged: true,
|
|
202
232
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Resolver
|
|
3
|
+
* Uses Claude agent to complete the entire rebase process including conflict resolution
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for conflict resolution
|
|
7
|
+
*/
|
|
8
|
+
export interface ConflictResolverConfig {
|
|
9
|
+
model?: string;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
maxTurns?: number;
|
|
12
|
+
featureBranch?: string;
|
|
13
|
+
baseBranch?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Original base branch before squash merge.
|
|
16
|
+
* When provided, this indicates the rebase was started with --onto
|
|
17
|
+
* to only replay commits after this branch.
|
|
18
|
+
*/
|
|
19
|
+
originalBaseBranch?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Result of conflict resolution
|
|
23
|
+
*/
|
|
24
|
+
export interface ConflictResolutionResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
resolvedFiles: string[];
|
|
27
|
+
failedFiles: string[];
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve all conflicts by letting Claude complete the entire rebase process
|
|
32
|
+
*
|
|
33
|
+
* This function gives Claude full control to:
|
|
34
|
+
* 1. Examine the conflict state
|
|
35
|
+
* 2. Read and understand conflicting files
|
|
36
|
+
* 3. Edit files to resolve conflicts
|
|
37
|
+
* 4. Stage resolved files and continue rebase
|
|
38
|
+
* 5. Repeat until rebase is complete
|
|
39
|
+
*
|
|
40
|
+
* @param config - Configuration for conflict resolution
|
|
41
|
+
* @returns Result of the conflict resolution
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveConflictsWithAgent(config?: ConflictResolverConfig): Promise<ConflictResolutionResult>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Resolver
|
|
3
|
+
* Uses Claude agent to complete the entire rebase process including conflict resolution
|
|
4
|
+
*/
|
|
5
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
import { logInfo, logError } from './logger.js';
|
|
7
|
+
import { getConflictFiles, isRebaseInProgress, abortRebase, getCurrentBranch, } from './git-branch-manager.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a prompt generator for the Claude agent
|
|
10
|
+
*/
|
|
11
|
+
function* createPromptGenerator(prompt) {
|
|
12
|
+
yield {
|
|
13
|
+
type: 'user',
|
|
14
|
+
message: { role: 'user', content: prompt },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a system prompt for conflict resolution
|
|
19
|
+
*/
|
|
20
|
+
function createConflictResolutionSystemPrompt() {
|
|
21
|
+
return `You are a skilled software engineer completing a git rebase operation that has conflicts.
|
|
22
|
+
|
|
23
|
+
Your task is to resolve ALL merge conflicts and complete the rebase successfully.
|
|
24
|
+
|
|
25
|
+
WORKFLOW:
|
|
26
|
+
1. First, run "git status" to see the current state and identify all conflicting files
|
|
27
|
+
2. For each conflicting file:
|
|
28
|
+
a. Read the file to see the conflict markers
|
|
29
|
+
b. Understand the intent of both sides (ours vs theirs)
|
|
30
|
+
c. Use the Edit tool to resolve the conflict - merge changes intelligently
|
|
31
|
+
d. Run "git add <file>" to stage the resolved file
|
|
32
|
+
3. After ALL conflicts are resolved, run "git rebase --continue"
|
|
33
|
+
4. If more conflicts appear from subsequent commits, repeat steps 1-3
|
|
34
|
+
5. Continue until the rebase is complete (no more rebase in progress)
|
|
35
|
+
|
|
36
|
+
CONFLICT RESOLUTION RULES:
|
|
37
|
+
- Remove ALL conflict markers (<<<<<<, =======, >>>>>>>)
|
|
38
|
+
- Merge changes intelligently - don't just pick one side
|
|
39
|
+
- Preserve functionality from both sides when possible
|
|
40
|
+
- If changes are truly incompatible, prefer theirs (incoming) as it's usually newer
|
|
41
|
+
- Ensure the resulting code is syntactically correct
|
|
42
|
+
- DO NOT add comments explaining the merge
|
|
43
|
+
|
|
44
|
+
IMPORTANT:
|
|
45
|
+
- Use "git status" to check progress
|
|
46
|
+
- Use the Read tool to examine conflicting files
|
|
47
|
+
- Use the Edit tool to fix conflicts (remove markers, merge content)
|
|
48
|
+
- Use Bash for git commands (git add, git rebase --continue)
|
|
49
|
+
- Keep working until "git status" shows no rebase in progress
|
|
50
|
+
- If you encounter an error you cannot resolve, explain what went wrong`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create the initial prompt describing the rebase task
|
|
54
|
+
*/
|
|
55
|
+
function createRebaseTaskPrompt(featureBranch, baseBranch, conflictFiles, originalBaseBranch) {
|
|
56
|
+
let context = `A git rebase operation is in progress and has encountered conflicts.
|
|
57
|
+
|
|
58
|
+
## Current Situation
|
|
59
|
+
- Feature branch: ${featureBranch}
|
|
60
|
+
- Rebasing onto: ${baseBranch}`;
|
|
61
|
+
if (originalBaseBranch) {
|
|
62
|
+
context += `
|
|
63
|
+
- Original base branch: ${originalBaseBranch}
|
|
64
|
+
- Note: This is a rebase after squash-merge. The base branch (${originalBaseBranch}) was squash-merged
|
|
65
|
+
into ${baseBranch}. We used "git rebase --onto" to only replay commits that are new relative to
|
|
66
|
+
${originalBaseBranch}. The "theirs" side in conflicts comes from ${baseBranch} (the squashed code).`;
|
|
67
|
+
}
|
|
68
|
+
context += `
|
|
69
|
+
- Conflicting files: ${conflictFiles.length > 0 ? conflictFiles.join(', ') : '(run git status to see)'}
|
|
70
|
+
|
|
71
|
+
## Your Task
|
|
72
|
+
Complete the rebase by resolving all conflicts. Use git status, read the conflicting files, edit them to resolve conflicts, stage with git add, and continue the rebase.
|
|
73
|
+
|
|
74
|
+
Start by running "git status" to see the current state.`;
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve all conflicts by letting Claude complete the entire rebase process
|
|
79
|
+
*
|
|
80
|
+
* This function gives Claude full control to:
|
|
81
|
+
* 1. Examine the conflict state
|
|
82
|
+
* 2. Read and understand conflicting files
|
|
83
|
+
* 3. Edit files to resolve conflicts
|
|
84
|
+
* 4. Stage resolved files and continue rebase
|
|
85
|
+
* 5. Repeat until rebase is complete
|
|
86
|
+
*
|
|
87
|
+
* @param config - Configuration for conflict resolution
|
|
88
|
+
* @returns Result of the conflict resolution
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveConflictsWithAgent(config = {}) {
|
|
91
|
+
const { model = 'sonnet', verbose, maxTurns = 100, featureBranch, baseBranch, originalBaseBranch, } = config;
|
|
92
|
+
const resolvedFiles = [];
|
|
93
|
+
if (verbose) {
|
|
94
|
+
logInfo(`\nš§ Starting automatic conflict resolution with Claude agent...`);
|
|
95
|
+
}
|
|
96
|
+
// Get initial conflict information
|
|
97
|
+
const currentBranch = featureBranch || getCurrentBranch();
|
|
98
|
+
const targetBranch = baseBranch || 'main';
|
|
99
|
+
const initialConflictFiles = getConflictFiles();
|
|
100
|
+
if (verbose) {
|
|
101
|
+
logInfo(` Feature branch: ${currentBranch}`);
|
|
102
|
+
logInfo(` Rebasing onto: ${targetBranch}`);
|
|
103
|
+
if (originalBaseBranch) {
|
|
104
|
+
logInfo(` Original base (--onto): ${originalBaseBranch}`);
|
|
105
|
+
}
|
|
106
|
+
logInfo(` Initial conflicts: ${initialConflictFiles.length} file(s)`);
|
|
107
|
+
}
|
|
108
|
+
const systemPrompt = createConflictResolutionSystemPrompt();
|
|
109
|
+
const userPrompt = createRebaseTaskPrompt(currentBranch, targetBranch, initialConflictFiles, originalBaseBranch);
|
|
110
|
+
try {
|
|
111
|
+
let lastMessage = '';
|
|
112
|
+
for await (const message of query({
|
|
113
|
+
prompt: createPromptGenerator(userPrompt),
|
|
114
|
+
options: {
|
|
115
|
+
systemPrompt: {
|
|
116
|
+
type: 'preset',
|
|
117
|
+
preset: 'claude_code',
|
|
118
|
+
append: systemPrompt,
|
|
119
|
+
},
|
|
120
|
+
model: model,
|
|
121
|
+
maxTurns: maxTurns,
|
|
122
|
+
permissionMode: 'bypassPermissions',
|
|
123
|
+
},
|
|
124
|
+
})) {
|
|
125
|
+
if (verbose && message.type === 'assistant' && message.message?.content) {
|
|
126
|
+
// Log progress
|
|
127
|
+
const content = message.message.content;
|
|
128
|
+
if (typeof content === 'string') {
|
|
129
|
+
lastMessage = content;
|
|
130
|
+
}
|
|
131
|
+
else if (Array.isArray(content)) {
|
|
132
|
+
for (const block of content) {
|
|
133
|
+
if (block.type === 'text') {
|
|
134
|
+
lastMessage = block.text;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Check if rebase completed successfully
|
|
141
|
+
if (!isRebaseInProgress()) {
|
|
142
|
+
if (verbose) {
|
|
143
|
+
logInfo(`ā
Rebase completed successfully`);
|
|
144
|
+
}
|
|
145
|
+
// Get the list of files that were resolved (approximate from initial conflicts)
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
resolvedFiles: initialConflictFiles,
|
|
149
|
+
failedFiles: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Rebase still in progress - something went wrong
|
|
154
|
+
const remainingConflicts = getConflictFiles();
|
|
155
|
+
if (verbose) {
|
|
156
|
+
logError(`ā Rebase still in progress with ${remainingConflicts.length} conflicts`);
|
|
157
|
+
if (lastMessage) {
|
|
158
|
+
logError(` Last message: ${lastMessage.substring(0, 200)}...`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
abortRebase(verbose);
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
resolvedFiles: [],
|
|
165
|
+
failedFiles: remainingConflicts,
|
|
166
|
+
error: `Rebase not completed. Remaining conflicts: ${remainingConflicts.join(', ')}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (verbose) {
|
|
172
|
+
logError(`ā Error during conflict resolution: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
// Try to abort rebase to leave repo in clean state
|
|
175
|
+
if (isRebaseInProgress()) {
|
|
176
|
+
abortRebase(verbose);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
resolvedFiles: [],
|
|
181
|
+
failedFiles: getConflictFiles(),
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
* Git Branch Manager
|
|
3
3
|
* Shared utilities for consistent git branch management across all phases
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Conflict resolver function type
|
|
7
|
+
* Takes conflict file info and returns resolved content
|
|
8
|
+
*/
|
|
9
|
+
export type ConflictResolver = (conflictFiles: ConflictFileInfo[]) => Promise<Map<string, string>>;
|
|
10
|
+
/**
|
|
11
|
+
* Information about a file with merge conflicts
|
|
12
|
+
*/
|
|
13
|
+
export interface ConflictFileInfo {
|
|
14
|
+
filePath: string;
|
|
15
|
+
content: string;
|
|
16
|
+
oursContent: string;
|
|
17
|
+
theirsContent: string;
|
|
18
|
+
}
|
|
5
19
|
/**
|
|
6
20
|
* Get current Git branch name
|
|
7
21
|
*/
|
|
@@ -18,6 +32,34 @@ export declare function getUncommittedFiles(): string[];
|
|
|
18
32
|
* Reset uncommitted changes in the working directory
|
|
19
33
|
*/
|
|
20
34
|
export declare function resetUncommittedChanges(verbose?: boolean): void;
|
|
35
|
+
/**
|
|
36
|
+
* Check if rebase is in progress with conflicts
|
|
37
|
+
*/
|
|
38
|
+
export declare function isRebaseInProgress(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Get list of files with merge conflicts
|
|
41
|
+
*/
|
|
42
|
+
export declare function getConflictFiles(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Check if there are any merge conflicts
|
|
45
|
+
*/
|
|
46
|
+
export declare function hasConflicts(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Get detailed information about conflict files including their content
|
|
49
|
+
*/
|
|
50
|
+
export declare function getConflictFileInfos(): ConflictFileInfo[];
|
|
51
|
+
/**
|
|
52
|
+
* Write resolved content for a conflict file and stage it
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveConflictFile(filePath: string, resolvedContent: string, verbose?: boolean): void;
|
|
55
|
+
/**
|
|
56
|
+
* Continue rebase after conflicts are resolved
|
|
57
|
+
*/
|
|
58
|
+
export declare function continueRebase(verbose?: boolean): void;
|
|
59
|
+
/**
|
|
60
|
+
* Abort the current rebase
|
|
61
|
+
*/
|
|
62
|
+
export declare function abortRebase(verbose?: boolean): void;
|
|
21
63
|
/**
|
|
22
64
|
* Check if a Git branch exists locally
|
|
23
65
|
*/
|
|
@@ -102,3 +144,78 @@ export declare function prepareCustomBranchGitEnvironment(featureBranch: string,
|
|
|
102
144
|
* @param verbose - Whether to log verbose output
|
|
103
145
|
*/
|
|
104
146
|
export declare function syncFeatBranchWithMain(featureId: string, githubToken: string, owner: string, repo: string, baseBranch?: string, verbose?: boolean): Promise<boolean>;
|
|
147
|
+
/**
|
|
148
|
+
* Options for async rebase with conflict resolution
|
|
149
|
+
*/
|
|
150
|
+
export interface RebaseWithConflictResolutionOptions {
|
|
151
|
+
featureBranch: string;
|
|
152
|
+
baseBranch?: string;
|
|
153
|
+
/**
|
|
154
|
+
* Original base branch before squash merge.
|
|
155
|
+
* When set, uses `git rebase --onto baseBranch originalBaseBranch` to only
|
|
156
|
+
* rebase commits that are new relative to originalBaseBranch.
|
|
157
|
+
* This is required when the base branch was squash-merged, otherwise
|
|
158
|
+
* git will try to reapply all the original commits causing many conflicts.
|
|
159
|
+
*/
|
|
160
|
+
originalBaseBranch?: string;
|
|
161
|
+
verbose?: boolean;
|
|
162
|
+
resolveConflicts?: boolean;
|
|
163
|
+
conflictResolverConfig?: {
|
|
164
|
+
model?: string;
|
|
165
|
+
maxTurns?: number;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Switch to feature branch and rebase with base branch, with optional automatic conflict resolution
|
|
170
|
+
* This is the async version that supports conflict resolution using Claude agent
|
|
171
|
+
*
|
|
172
|
+
* @param options - Options for the rebase operation
|
|
173
|
+
* @returns Object containing the previous branch name and conflict resolution result
|
|
174
|
+
*/
|
|
175
|
+
export declare function switchToFeatureBranchAndRebaseAsync(options: RebaseWithConflictResolutionOptions): Promise<{
|
|
176
|
+
previousBranch: string;
|
|
177
|
+
conflictsResolved?: boolean;
|
|
178
|
+
resolvedFiles?: string[];
|
|
179
|
+
}>;
|
|
180
|
+
/**
|
|
181
|
+
* Async version of prepareCustomBranchGitEnvironment with conflict resolution support.
|
|
182
|
+
*
|
|
183
|
+
* Usage:
|
|
184
|
+
* const cleanup = await prepareCustomBranchGitEnvironmentAsync({
|
|
185
|
+
* featureBranch: 'feat/123/2-api',
|
|
186
|
+
* baseBranch: 'feat/123/1-database',
|
|
187
|
+
* verbose: true,
|
|
188
|
+
* resolveConflicts: true,
|
|
189
|
+
* })
|
|
190
|
+
* try {
|
|
191
|
+
* // ... phase logic ...
|
|
192
|
+
* } finally {
|
|
193
|
+
* cleanup()
|
|
194
|
+
* }
|
|
195
|
+
*
|
|
196
|
+
* @param options - Options for the git environment preparation
|
|
197
|
+
* @returns Cleanup function that will return to main branch
|
|
198
|
+
*/
|
|
199
|
+
export declare function prepareCustomBranchGitEnvironmentAsync(options: RebaseWithConflictResolutionOptions): Promise<{
|
|
200
|
+
cleanup: () => void;
|
|
201
|
+
conflictsResolved?: boolean;
|
|
202
|
+
resolvedFiles?: string[];
|
|
203
|
+
}>;
|
|
204
|
+
/**
|
|
205
|
+
* Async version of preparePhaseGitEnvironment with conflict resolution support.
|
|
206
|
+
*
|
|
207
|
+
* @param featureId - The feature ID (will be used to construct branch name "dev/{featureId}")
|
|
208
|
+
* @param baseBranch - The base branch to rebase from (default: "main")
|
|
209
|
+
* @param verbose - Whether to log verbose output
|
|
210
|
+
* @param resolveConflicts - Whether to automatically resolve conflicts using Claude
|
|
211
|
+
* @param conflictResolverConfig - Configuration for the conflict resolver
|
|
212
|
+
* @returns Cleanup function and conflict resolution info
|
|
213
|
+
*/
|
|
214
|
+
export declare function preparePhaseGitEnvironmentAsync(featureId: string, baseBranch?: string, verbose?: boolean, resolveConflicts?: boolean, conflictResolverConfig?: {
|
|
215
|
+
model?: string;
|
|
216
|
+
maxTurns?: number;
|
|
217
|
+
}): Promise<{
|
|
218
|
+
cleanup: () => void;
|
|
219
|
+
conflictsResolved?: boolean;
|
|
220
|
+
resolvedFiles?: string[];
|
|
221
|
+
}>;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Shared utilities for consistent git branch management across all phases
|
|
4
4
|
*/
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
6
8
|
import { Octokit } from '@octokit/rest';
|
|
7
9
|
import { logInfo, logError } from './logger.js';
|
|
8
10
|
/**
|
|
@@ -62,6 +64,148 @@ export function resetUncommittedChanges(verbose) {
|
|
|
62
64
|
throw new Error(`Failed to reset uncommitted changes: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if rebase is in progress with conflicts
|
|
69
|
+
*/
|
|
70
|
+
export function isRebaseInProgress() {
|
|
71
|
+
try {
|
|
72
|
+
// Check for .git/rebase-merge or .git/rebase-apply directory
|
|
73
|
+
const gitDir = execSync('git rev-parse --git-dir', {
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
}).trim();
|
|
76
|
+
const rebaseMerge = path.join(gitDir, 'rebase-merge');
|
|
77
|
+
const rebaseApply = path.join(gitDir, 'rebase-apply');
|
|
78
|
+
return fs.existsSync(rebaseMerge) || fs.existsSync(rebaseApply);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get list of files with merge conflicts
|
|
86
|
+
*/
|
|
87
|
+
export function getConflictFiles() {
|
|
88
|
+
try {
|
|
89
|
+
const status = execSync('git diff --name-only --diff-filter=U', {
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
});
|
|
92
|
+
return status
|
|
93
|
+
.trim()
|
|
94
|
+
.split('\n')
|
|
95
|
+
.filter((line) => line.length > 0);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if there are any merge conflicts
|
|
103
|
+
*/
|
|
104
|
+
export function hasConflicts() {
|
|
105
|
+
return getConflictFiles().length > 0;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get detailed information about conflict files including their content
|
|
109
|
+
*/
|
|
110
|
+
export function getConflictFileInfos() {
|
|
111
|
+
const conflictFiles = getConflictFiles();
|
|
112
|
+
const infos = [];
|
|
113
|
+
for (const filePath of conflictFiles) {
|
|
114
|
+
try {
|
|
115
|
+
// Read the file with conflict markers
|
|
116
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
117
|
+
// Get our version (current branch)
|
|
118
|
+
let oursContent = '';
|
|
119
|
+
try {
|
|
120
|
+
oursContent = execSync(`git show :2:${filePath}`, {
|
|
121
|
+
encoding: 'utf-8',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
// File might be new on our side
|
|
126
|
+
oursContent = '';
|
|
127
|
+
}
|
|
128
|
+
// Get their version (incoming branch)
|
|
129
|
+
let theirsContent = '';
|
|
130
|
+
try {
|
|
131
|
+
theirsContent = execSync(`git show :3:${filePath}`, {
|
|
132
|
+
encoding: 'utf-8',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
// File might be new on their side
|
|
137
|
+
theirsContent = '';
|
|
138
|
+
}
|
|
139
|
+
infos.push({
|
|
140
|
+
filePath,
|
|
141
|
+
content,
|
|
142
|
+
oursContent,
|
|
143
|
+
theirsContent,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// Skip files we can't read
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return infos;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Write resolved content for a conflict file and stage it
|
|
155
|
+
*/
|
|
156
|
+
export function resolveConflictFile(filePath, resolvedContent, verbose) {
|
|
157
|
+
try {
|
|
158
|
+
if (verbose) {
|
|
159
|
+
logInfo(`š Writing resolved content for ${filePath}...`);
|
|
160
|
+
}
|
|
161
|
+
fs.writeFileSync(filePath, resolvedContent, 'utf-8');
|
|
162
|
+
execSync(`git add "${filePath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
163
|
+
if (verbose) {
|
|
164
|
+
logInfo(`ā
Resolved and staged ${filePath}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw new Error(`Failed to resolve conflict in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Continue rebase after conflicts are resolved
|
|
173
|
+
*/
|
|
174
|
+
export function continueRebase(verbose) {
|
|
175
|
+
try {
|
|
176
|
+
if (verbose) {
|
|
177
|
+
logInfo(`š Continuing rebase...`);
|
|
178
|
+
}
|
|
179
|
+
// Use GIT_EDITOR=true to skip the commit message editor
|
|
180
|
+
execSync('GIT_EDITOR=true git rebase --continue', {
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
stdio: verbose ? 'inherit' : 'pipe',
|
|
183
|
+
});
|
|
184
|
+
if (verbose) {
|
|
185
|
+
logInfo(`ā
Rebase continued successfully`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw new Error(`Failed to continue rebase: ${error instanceof Error ? error.message : String(error)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Abort the current rebase
|
|
194
|
+
*/
|
|
195
|
+
export function abortRebase(verbose) {
|
|
196
|
+
try {
|
|
197
|
+
if (verbose) {
|
|
198
|
+
logInfo(`š Aborting rebase...`);
|
|
199
|
+
}
|
|
200
|
+
execSync('git rebase --abort', { encoding: 'utf-8', stdio: 'pipe' });
|
|
201
|
+
if (verbose) {
|
|
202
|
+
logInfo(`ā
Rebase aborted`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// Ignore errors - rebase might not be in progress
|
|
207
|
+
}
|
|
208
|
+
}
|
|
65
209
|
/**
|
|
66
210
|
* Check if a Git branch exists locally
|
|
67
211
|
*/
|
|
@@ -476,3 +620,238 @@ export async function syncFeatBranchWithMain(featureId, githubToken, owner, repo
|
|
|
476
620
|
return false;
|
|
477
621
|
}
|
|
478
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Switch to feature branch and rebase with base branch, with optional automatic conflict resolution
|
|
625
|
+
* This is the async version that supports conflict resolution using Claude agent
|
|
626
|
+
*
|
|
627
|
+
* @param options - Options for the rebase operation
|
|
628
|
+
* @returns Object containing the previous branch name and conflict resolution result
|
|
629
|
+
*/
|
|
630
|
+
export async function switchToFeatureBranchAndRebaseAsync(options) {
|
|
631
|
+
const { featureBranch, baseBranch = 'main', originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, } = options;
|
|
632
|
+
const previousBranch = getCurrentBranch();
|
|
633
|
+
if (verbose) {
|
|
634
|
+
logInfo(`\nš Preparing feature branch: ${featureBranch}`);
|
|
635
|
+
logInfo(` Current branch: ${previousBranch}`);
|
|
636
|
+
}
|
|
637
|
+
// If feature branch doesn't exist, we need to create it from base branch
|
|
638
|
+
if (!branchExists(featureBranch)) {
|
|
639
|
+
if (verbose) {
|
|
640
|
+
logInfo(` Feature branch ${featureBranch} doesn't exist, will create from ${baseBranch}`);
|
|
641
|
+
}
|
|
642
|
+
// First, ensure we're on the base branch to create feature branch from it
|
|
643
|
+
if (previousBranch !== baseBranch) {
|
|
644
|
+
if (verbose) {
|
|
645
|
+
logInfo(` Switching to ${baseBranch} first...`);
|
|
646
|
+
}
|
|
647
|
+
switchToBranch(baseBranch, verbose);
|
|
648
|
+
}
|
|
649
|
+
// Pull latest base branch before creating feature branch
|
|
650
|
+
try {
|
|
651
|
+
if (verbose) {
|
|
652
|
+
logInfo(` Pulling latest ${baseBranch}...`);
|
|
653
|
+
}
|
|
654
|
+
pullLatestFromBranch(baseBranch, verbose);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (verbose) {
|
|
658
|
+
logInfo(` ā ļø Could not pull ${baseBranch}, will create branch from local ${baseBranch}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Switch to feature branch (will create if doesn't exist)
|
|
663
|
+
if (getCurrentBranch() !== featureBranch) {
|
|
664
|
+
switchToBranch(featureBranch, verbose);
|
|
665
|
+
}
|
|
666
|
+
// Sync with remote feature branch if it exists
|
|
667
|
+
try {
|
|
668
|
+
if (hasUncommittedChanges()) {
|
|
669
|
+
if (verbose) {
|
|
670
|
+
logInfo(`ā ļø Found uncommitted changes. Resetting to clean state before sync...`);
|
|
671
|
+
}
|
|
672
|
+
resetUncommittedChanges(verbose);
|
|
673
|
+
}
|
|
674
|
+
execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
|
|
675
|
+
try {
|
|
676
|
+
execSync(`git rev-parse --verify origin/${featureBranch}`, {
|
|
677
|
+
encoding: 'utf-8',
|
|
678
|
+
stdio: 'pipe',
|
|
679
|
+
});
|
|
680
|
+
if (verbose) {
|
|
681
|
+
logInfo(`š„ Syncing with remote feature branch origin/${featureBranch}...`);
|
|
682
|
+
}
|
|
683
|
+
execSync(`git reset --hard origin/${featureBranch}`, {
|
|
684
|
+
encoding: 'utf-8',
|
|
685
|
+
stdio: 'pipe',
|
|
686
|
+
});
|
|
687
|
+
if (verbose) {
|
|
688
|
+
logInfo(`ā
Synced local branch with origin/${featureBranch}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (e) {
|
|
692
|
+
if (verbose) {
|
|
693
|
+
logInfo(` Remote branch origin/${featureBranch} doesn't exist yet, will create on push`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
if (verbose) {
|
|
699
|
+
logInfo(`ā ļø Could not sync with remote feature branch, continuing with local branch`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// Rebase feature branch with latest base branch
|
|
703
|
+
if (verbose) {
|
|
704
|
+
if (originalBaseBranch) {
|
|
705
|
+
logInfo(`š„ Rebasing ${featureBranch} onto origin/${baseBranch} (from ${originalBaseBranch})...`);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
logInfo(`š„ Rebasing ${featureBranch} with origin/${baseBranch}...`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
if (hasUncommittedChanges()) {
|
|
713
|
+
if (verbose) {
|
|
714
|
+
logInfo(`ā ļø Found uncommitted changes. Resetting to clean state before rebase...`);
|
|
715
|
+
}
|
|
716
|
+
resetUncommittedChanges(verbose);
|
|
717
|
+
}
|
|
718
|
+
// Fetch the base branch first
|
|
719
|
+
execSync(`git fetch origin ${baseBranch}`, {
|
|
720
|
+
encoding: 'utf-8',
|
|
721
|
+
stdio: 'pipe',
|
|
722
|
+
});
|
|
723
|
+
if (originalBaseBranch) {
|
|
724
|
+
// When base branch was squash-merged, use --onto to only rebase new commits
|
|
725
|
+
// This prevents re-applying commits that were already included in the squash
|
|
726
|
+
// Command: git rebase --onto <new-base> <old-base>
|
|
727
|
+
// This rebases commits from <old-base>..HEAD onto <new-base>
|
|
728
|
+
if (verbose) {
|
|
729
|
+
logInfo(` Using --onto to rebase only new commits (after ${originalBaseBranch})`);
|
|
730
|
+
}
|
|
731
|
+
execSync(`git rebase --onto origin/${baseBranch} origin/${originalBaseBranch}`, {
|
|
732
|
+
encoding: 'utf-8',
|
|
733
|
+
stdio: verbose ? 'inherit' : 'pipe',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// Normal rebase
|
|
738
|
+
execSync(`git rebase origin/${baseBranch}`, {
|
|
739
|
+
encoding: 'utf-8',
|
|
740
|
+
stdio: verbose ? 'inherit' : 'pipe',
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (verbose) {
|
|
744
|
+
logInfo(`ā
Successfully rebased ${featureBranch} with origin/${baseBranch}\n`);
|
|
745
|
+
}
|
|
746
|
+
return { previousBranch };
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
// Check if there are conflicts that we can try to resolve
|
|
750
|
+
if (resolveConflicts && isRebaseInProgress() && hasConflicts()) {
|
|
751
|
+
if (verbose) {
|
|
752
|
+
logInfo(`\nā ļø Rebase conflicts detected. Attempting automatic resolution...`);
|
|
753
|
+
}
|
|
754
|
+
// Dynamically import the conflict resolver to avoid circular dependencies
|
|
755
|
+
const { resolveConflictsWithAgent } = await import('./conflict-resolver.js');
|
|
756
|
+
const result = await resolveConflictsWithAgent({
|
|
757
|
+
model: conflictResolverConfig?.model,
|
|
758
|
+
maxTurns: conflictResolverConfig?.maxTurns,
|
|
759
|
+
verbose,
|
|
760
|
+
featureBranch,
|
|
761
|
+
baseBranch,
|
|
762
|
+
originalBaseBranch,
|
|
763
|
+
});
|
|
764
|
+
if (result.success) {
|
|
765
|
+
if (verbose) {
|
|
766
|
+
logInfo(`ā
Successfully resolved conflicts and rebased ${featureBranch} with origin/${baseBranch}\n`);
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
previousBranch,
|
|
770
|
+
conflictsResolved: true,
|
|
771
|
+
resolvedFiles: result.resolvedFiles,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
// Conflict resolution failed, abort rebase
|
|
776
|
+
abortRebase(verbose);
|
|
777
|
+
throw new Error(`Failed to automatically resolve conflicts during rebase ${featureBranch} with ${baseBranch}.\n` +
|
|
778
|
+
`${result.error || 'Unknown error'}\n` +
|
|
779
|
+
`Please resolve conflicts manually and try again.`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// No conflict resolution attempted or not a conflict error
|
|
783
|
+
// Abort rebase and throw error
|
|
784
|
+
abortRebase(verbose);
|
|
785
|
+
if (verbose) {
|
|
786
|
+
logError(`ā Rebase failed and was aborted. Repository is in clean state.`);
|
|
787
|
+
}
|
|
788
|
+
throw new Error(`Failed to rebase ${featureBranch} with ${baseBranch}: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
789
|
+
`This usually means there are conflicts between your feature branch and base branch.\n` +
|
|
790
|
+
`Please resolve conflicts manually and try again.`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Async version of prepareCustomBranchGitEnvironment with conflict resolution support.
|
|
795
|
+
*
|
|
796
|
+
* Usage:
|
|
797
|
+
* const cleanup = await prepareCustomBranchGitEnvironmentAsync({
|
|
798
|
+
* featureBranch: 'feat/123/2-api',
|
|
799
|
+
* baseBranch: 'feat/123/1-database',
|
|
800
|
+
* verbose: true,
|
|
801
|
+
* resolveConflicts: true,
|
|
802
|
+
* })
|
|
803
|
+
* try {
|
|
804
|
+
* // ... phase logic ...
|
|
805
|
+
* } finally {
|
|
806
|
+
* cleanup()
|
|
807
|
+
* }
|
|
808
|
+
*
|
|
809
|
+
* @param options - Options for the git environment preparation
|
|
810
|
+
* @returns Cleanup function that will return to main branch
|
|
811
|
+
*/
|
|
812
|
+
export async function prepareCustomBranchGitEnvironmentAsync(options) {
|
|
813
|
+
const { verbose } = options;
|
|
814
|
+
// Create cleanup function BEFORE any operations that might fail
|
|
815
|
+
const cleanup = () => {
|
|
816
|
+
returnToMainBranch('main', verbose);
|
|
817
|
+
};
|
|
818
|
+
try {
|
|
819
|
+
const result = await switchToFeatureBranchAndRebaseAsync(options);
|
|
820
|
+
return {
|
|
821
|
+
cleanup,
|
|
822
|
+
conflictsResolved: result.conflictsResolved,
|
|
823
|
+
resolvedFiles: result.resolvedFiles,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
try {
|
|
828
|
+
cleanup();
|
|
829
|
+
}
|
|
830
|
+
catch (cleanupError) {
|
|
831
|
+
if (verbose) {
|
|
832
|
+
logError(`ā ļø Cleanup failed after setup error: ${cleanupError}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Async version of preparePhaseGitEnvironment with conflict resolution support.
|
|
840
|
+
*
|
|
841
|
+
* @param featureId - The feature ID (will be used to construct branch name "dev/{featureId}")
|
|
842
|
+
* @param baseBranch - The base branch to rebase from (default: "main")
|
|
843
|
+
* @param verbose - Whether to log verbose output
|
|
844
|
+
* @param resolveConflicts - Whether to automatically resolve conflicts using Claude
|
|
845
|
+
* @param conflictResolverConfig - Configuration for the conflict resolver
|
|
846
|
+
* @returns Cleanup function and conflict resolution info
|
|
847
|
+
*/
|
|
848
|
+
export async function preparePhaseGitEnvironmentAsync(featureId, baseBranch = 'main', verbose, resolveConflicts, conflictResolverConfig) {
|
|
849
|
+
const featureBranch = `dev/${featureId}`;
|
|
850
|
+
return prepareCustomBranchGitEnvironmentAsync({
|
|
851
|
+
featureBranch,
|
|
852
|
+
baseBranch,
|
|
853
|
+
verbose,
|
|
854
|
+
resolveConflicts,
|
|
855
|
+
conflictResolverConfig,
|
|
856
|
+
});
|
|
857
|
+
}
|