edsger 0.13.1 → 0.13.3
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/context.d.ts +1 -0
- package/dist/phases/code-refine/context.js +14 -2
- package/dist/phases/code-refine/index.js +20 -1
- package/dist/phases/code-refine-verification/github.d.ts +7 -0
- package/dist/phases/code-refine-verification/github.js +46 -0
- package/dist/phases/code-refine-verification/index.js +47 -22
- package/dist/phases/code-refine-verification/types.d.ts +1 -0
- package/dist/utils/git-branch-manager.d.ts +13 -0
- package/dist/utils/git-branch-manager.js +98 -0
- package/package.json +2 -2
|
@@ -30,8 +30,19 @@ export async function fetchPRReviews(octokit, owner, repo, prNumber, verbose) {
|
|
|
30
30
|
repo,
|
|
31
31
|
pull_number: prNumber,
|
|
32
32
|
});
|
|
33
|
-
// Filter for reviews that request changes
|
|
34
|
-
const requestChangesReviews = reviews
|
|
33
|
+
// Filter for reviews that request changes and map to our interface
|
|
34
|
+
const requestChangesReviews = reviews
|
|
35
|
+
.filter((review) => review.state === 'CHANGES_REQUESTED')
|
|
36
|
+
.map((review) => ({
|
|
37
|
+
id: review.id,
|
|
38
|
+
node_id: review.node_id, // REST API also returns node_id
|
|
39
|
+
user: {
|
|
40
|
+
login: review.user?.login || 'unknown',
|
|
41
|
+
},
|
|
42
|
+
body: review.body ?? null,
|
|
43
|
+
state: review.state,
|
|
44
|
+
submitted_at: review.submitted_at ?? null,
|
|
45
|
+
}));
|
|
35
46
|
if (verbose) {
|
|
36
47
|
console.log(`✅ Found ${requestChangesReviews.length} reviews requesting changes`);
|
|
37
48
|
}
|
|
@@ -117,6 +128,7 @@ export async function fetchPRDataGraphQL(octokit, owner, repo, prNumber, verbose
|
|
|
117
128
|
// Transform GraphQL reviews to our interface
|
|
118
129
|
const reviews = (pullRequest.reviews.nodes || []).map((review) => ({
|
|
119
130
|
id: review.databaseId,
|
|
131
|
+
node_id: review.id, // GraphQL Node ID for mutations
|
|
120
132
|
user: {
|
|
121
133
|
login: review.author?.login || 'unknown',
|
|
122
134
|
},
|
|
@@ -8,7 +8,9 @@ import { execSync } from 'child_process';
|
|
|
8
8
|
import { fetchCodeRefineContext, } from './context.js';
|
|
9
9
|
import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
|
|
10
10
|
import { createSystemPrompt, createCodeRefinePrompt } from './prompts.js';
|
|
11
|
-
import { preparePhaseGitEnvironment, hasUncommittedChanges, getUncommittedFiles, } from '../../utils/git-branch-manager.js';
|
|
11
|
+
import { preparePhaseGitEnvironment, hasUncommittedChanges, getUncommittedFiles, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
|
|
12
|
+
import { getFeature } from '../../api/features/get-feature.js';
|
|
13
|
+
import { parsePullRequestUrl } from './context.js';
|
|
12
14
|
function userMessage(content) {
|
|
13
15
|
return {
|
|
14
16
|
type: 'user',
|
|
@@ -62,6 +64,23 @@ export const refineCodeFromPRFeedback = async (options, config) => {
|
|
|
62
64
|
if (verbose) {
|
|
63
65
|
logInfo(`Starting code refine for feature ID: ${featureId}`);
|
|
64
66
|
}
|
|
67
|
+
// Sync feat branch with main before preparing git environment
|
|
68
|
+
// This prevents extra commits from appearing in PR when dev branch is rebased
|
|
69
|
+
try {
|
|
70
|
+
const feature = await getFeature(featureId, verbose);
|
|
71
|
+
if (feature.pull_request_url) {
|
|
72
|
+
const prInfo = parsePullRequestUrl(feature.pull_request_url);
|
|
73
|
+
if (prInfo) {
|
|
74
|
+
await syncFeatBranchWithMain(featureId, githubToken, prInfo.owner, prInfo.repo, 'main', verbose);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (verbose) {
|
|
80
|
+
logInfo(`⚠️ Could not sync feat branch: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
// Continue even if sync fails - it's not critical
|
|
83
|
+
}
|
|
65
84
|
// Prepare git environment: switch to feature branch and rebase with main
|
|
66
85
|
const cleanupGit = preparePhaseGitEnvironment(featureId, 'main', verbose);
|
|
67
86
|
try {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
6
|
import { ReviewThread, PRFileChange } from './types.js';
|
|
7
|
+
import { PRReview } from '../code-refine/context.js';
|
|
7
8
|
/**
|
|
8
9
|
* Fetch complete file content from a specific ref (branch/commit)
|
|
9
10
|
*/
|
|
@@ -21,3 +22,9 @@ export declare function fetchUnresolvedReviewThreads(octokit: Octokit, owner: st
|
|
|
21
22
|
* Mark review threads as resolved using GraphQL API
|
|
22
23
|
*/
|
|
23
24
|
export declare function resolveReviewThreads(octokit: Octokit, threads: ReviewThread[], verbose?: boolean): Promise<number>;
|
|
25
|
+
/**
|
|
26
|
+
* Dismiss pull request reviews using GraphQL API
|
|
27
|
+
* This is used to automatically dismiss "CHANGES_REQUESTED" reviews
|
|
28
|
+
* after all feedback has been addressed by code refine
|
|
29
|
+
*/
|
|
30
|
+
export declare function dismissReviews(octokit: Octokit, reviews: PRReview[], resolvedCommentsCount: number, verbose?: boolean): Promise<number>;
|
|
@@ -188,3 +188,49 @@ export async function resolveReviewThreads(octokit, threads, verbose) {
|
|
|
188
188
|
}
|
|
189
189
|
return markedCount;
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Dismiss pull request reviews using GraphQL API
|
|
193
|
+
* This is used to automatically dismiss "CHANGES_REQUESTED" reviews
|
|
194
|
+
* after all feedback has been addressed by code refine
|
|
195
|
+
*/
|
|
196
|
+
export async function dismissReviews(octokit, reviews, resolvedCommentsCount, verbose) {
|
|
197
|
+
let dismissedCount = 0;
|
|
198
|
+
for (const review of reviews) {
|
|
199
|
+
try {
|
|
200
|
+
if (verbose) {
|
|
201
|
+
logInfo(`🔄 Dismissing review ${review.id} by @${review.user.login}...`);
|
|
202
|
+
}
|
|
203
|
+
// Build dismiss message explaining what was resolved
|
|
204
|
+
const message = `All feedback has been addressed by automated code refine.\n\n` +
|
|
205
|
+
`✅ Resolved ${resolvedCommentsCount} review comment(s).\n\n` +
|
|
206
|
+
`Please re-review if you disagree with any changes.`;
|
|
207
|
+
const mutation = `
|
|
208
|
+
mutation($pullRequestReviewId: ID!, $message: String!) {
|
|
209
|
+
dismissPullRequestReview(input: {
|
|
210
|
+
pullRequestReviewId: $pullRequestReviewId
|
|
211
|
+
message: $message
|
|
212
|
+
}) {
|
|
213
|
+
pullRequestReview {
|
|
214
|
+
id
|
|
215
|
+
state
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
await octokit.graphql(mutation, {
|
|
221
|
+
pullRequestReviewId: review.node_id,
|
|
222
|
+
message,
|
|
223
|
+
});
|
|
224
|
+
dismissedCount++;
|
|
225
|
+
if (verbose) {
|
|
226
|
+
logInfo(`✅ Dismissed review ${review.id} by @${review.user.login}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (verbose) {
|
|
231
|
+
logError(`Failed to dismiss review ${review.id} by @${review.user.login}: ${error}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return dismissedCount;
|
|
236
|
+
}
|
|
@@ -7,7 +7,7 @@ import { Octokit } from '@octokit/rest';
|
|
|
7
7
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
8
8
|
import { parsePullRequestUrl, fetchPRReviews } from '../code-refine/context.js';
|
|
9
9
|
import { getFeature } from '../../api/features/get-feature.js';
|
|
10
|
-
import { fetchPRFileChanges, fetchUnresolvedReviewThreads, resolveReviewThreads, } from './github.js';
|
|
10
|
+
import { fetchPRFileChanges, fetchUnresolvedReviewThreads, resolveReviewThreads, dismissReviews, } from './github.js';
|
|
11
11
|
import { analyzeAllThreads } from './llm-analyzer.js';
|
|
12
12
|
// Re-export types for backward compatibility
|
|
13
13
|
export * from './types.js';
|
|
@@ -65,31 +65,52 @@ export async function verifyAndResolveComments(options) {
|
|
|
65
65
|
const addressedThreads = threadAnalysisResults.filter((result) => result.analysis.isAddressed);
|
|
66
66
|
const trulyUnresolvedThreads = threadAnalysisResults.filter((result) => !result.analysis.isAddressed);
|
|
67
67
|
// Auto-resolve threads that LLM determined are addressed
|
|
68
|
+
let totalResolvedComments = 0;
|
|
68
69
|
if (addressedThreads.length > 0) {
|
|
69
70
|
if (verbose) {
|
|
70
71
|
logInfo(`✅ Auto-resolving ${addressedThreads.length} threads that have been addressed...`);
|
|
71
72
|
}
|
|
72
73
|
const resolvedCount = await resolveReviewThreads(octokit, addressedThreads.map((r) => r.thread), verbose);
|
|
74
|
+
totalResolvedComments = resolvedCount;
|
|
73
75
|
if (verbose) {
|
|
74
76
|
logInfo(`✅ Successfully resolved ${resolvedCount} threads`);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
|
-
// Check reviews - they need to be dismissed
|
|
78
|
-
const
|
|
79
|
+
// Check reviews - they need to be dismissed if all comments are addressed
|
|
80
|
+
const changesRequestedReviews = reviews.filter((review) => review.state === 'CHANGES_REQUESTED');
|
|
79
81
|
if (verbose) {
|
|
80
82
|
logInfo(`📊 Review Threads: ${trulyUnresolvedThreads.length} still unresolved (after LLM analysis)`);
|
|
81
83
|
if (reviews.length > 0) {
|
|
82
|
-
logInfo(`📊 Reviews: ${
|
|
84
|
+
logInfo(`📊 Reviews: ${changesRequestedReviews.length} requesting changes`);
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
|
-
// If all threads are truly resolved
|
|
86
|
-
|
|
87
|
+
// If all threads are truly resolved, dismiss the "CHANGES_REQUESTED" reviews
|
|
88
|
+
let dismissedReviewsCount = 0;
|
|
89
|
+
if (trulyUnresolvedThreads.length === 0 &&
|
|
90
|
+
changesRequestedReviews.length > 0) {
|
|
91
|
+
if (verbose) {
|
|
92
|
+
logInfo(`🔄 All comments addressed. Dismissing ${changesRequestedReviews.length} review(s) requesting changes...`);
|
|
93
|
+
}
|
|
94
|
+
dismissedReviewsCount = await dismissReviews(octokit, changesRequestedReviews, totalResolvedComments, verbose);
|
|
95
|
+
if (verbose) {
|
|
96
|
+
logInfo(`✅ Successfully dismissed ${dismissedReviewsCount} review(s)`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// If all threads are truly resolved (after LLM analysis) AND reviews are dismissed, success
|
|
100
|
+
const allReviewsDismissed = changesRequestedReviews.length === 0 ||
|
|
101
|
+
dismissedReviewsCount === changesRequestedReviews.length;
|
|
102
|
+
if (trulyUnresolvedThreads.length === 0 && allReviewsDismissed) {
|
|
87
103
|
if (verbose) {
|
|
88
104
|
logInfo('✅ All comments have been addressed! All review threads are resolved.');
|
|
89
105
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
let successMessage = 'All review comments have been addressed and resolved';
|
|
107
|
+
if (dismissedReviewsCount > 0) {
|
|
108
|
+
successMessage = `All reviews and review comments have been addressed. ${dismissedReviewsCount} review(s) dismissed.`;
|
|
109
|
+
}
|
|
110
|
+
else if (reviews.length > 0) {
|
|
111
|
+
successMessage =
|
|
112
|
+
'All reviews and review comments have been addressed and resolved';
|
|
113
|
+
}
|
|
93
114
|
return {
|
|
94
115
|
status: 'success',
|
|
95
116
|
message: successMessage,
|
|
@@ -101,15 +122,18 @@ export async function verifyAndResolveComments(options) {
|
|
|
101
122
|
resolvedComments: addressedThreads.length,
|
|
102
123
|
unresolvedComments: 0,
|
|
103
124
|
commentsMarkedResolved: addressedThreads.length,
|
|
125
|
+
dismissedReviews: dismissedReviewsCount,
|
|
104
126
|
},
|
|
105
127
|
};
|
|
106
128
|
}
|
|
107
129
|
else {
|
|
108
130
|
// Verification failed - build detailed info with specific failure reasons from LLM analysis
|
|
131
|
+
// Calculate remaining reviews that weren't dismissed
|
|
132
|
+
const remainingReviewsCount = changesRequestedReviews.length - dismissedReviewsCount;
|
|
109
133
|
if (verbose) {
|
|
110
|
-
if (
|
|
111
|
-
logInfo(`⚠️ ${
|
|
112
|
-
|
|
134
|
+
if (remainingReviewsCount > 0) {
|
|
135
|
+
logInfo(`⚠️ ${remainingReviewsCount} reviews still requesting changes (failed to dismiss)`);
|
|
136
|
+
changesRequestedReviews.forEach((review) => {
|
|
113
137
|
logInfo(` - Review ${review.id} by @${review.user.login}`);
|
|
114
138
|
if (review.body) {
|
|
115
139
|
logInfo(` ${review.body.substring(0, 100)}...`);
|
|
@@ -138,10 +162,10 @@ export async function verifyAndResolveComments(options) {
|
|
|
138
162
|
suggestions.push(`${index + 1}. [${firstComment.path}:${firstComment.line || '?'}] by @${firstComment.author.login}: ${result.analysis.reason}`);
|
|
139
163
|
}
|
|
140
164
|
});
|
|
141
|
-
// Add review-specific suggestions if any
|
|
142
|
-
if (
|
|
143
|
-
suggestions.push(`\n${
|
|
144
|
-
|
|
165
|
+
// Add review-specific suggestions if any reviews failed to dismiss
|
|
166
|
+
if (remainingReviewsCount > 0) {
|
|
167
|
+
suggestions.push(`\n${remainingReviewsCount} review(s) requesting changes could not be automatically dismissed:`);
|
|
168
|
+
changesRequestedReviews.forEach((review) => {
|
|
145
169
|
suggestions.push(` - @${review.user.login}: ${review.body ? review.body.substring(0, 150) : 'No details provided'}${review.body && review.body.length > 150 ? '...' : ''}`);
|
|
146
170
|
});
|
|
147
171
|
}
|
|
@@ -158,7 +182,7 @@ export async function verifyAndResolveComments(options) {
|
|
|
158
182
|
url: firstComment.url,
|
|
159
183
|
};
|
|
160
184
|
});
|
|
161
|
-
const unresolvedReviewDetails =
|
|
185
|
+
const unresolvedReviewDetails = changesRequestedReviews.map((review) => ({
|
|
162
186
|
reviewId: review.id,
|
|
163
187
|
author: review.user.login,
|
|
164
188
|
state: review.state,
|
|
@@ -166,11 +190,11 @@ export async function verifyAndResolveComments(options) {
|
|
|
166
190
|
submittedAt: review.submitted_at,
|
|
167
191
|
}));
|
|
168
192
|
let errorMessage = '';
|
|
169
|
-
if (
|
|
170
|
-
errorMessage = `${
|
|
193
|
+
if (remainingReviewsCount > 0 && trulyUnresolvedThreads.length > 0) {
|
|
194
|
+
errorMessage = `${remainingReviewsCount} reviews and ${trulyUnresolvedThreads.length} review threads still need to be addressed (based on LLM analysis)`;
|
|
171
195
|
}
|
|
172
|
-
else if (
|
|
173
|
-
errorMessage = `${
|
|
196
|
+
else if (remainingReviewsCount > 0) {
|
|
197
|
+
errorMessage = `${remainingReviewsCount} reviews could not be dismissed`;
|
|
174
198
|
}
|
|
175
199
|
else {
|
|
176
200
|
errorMessage = `${trulyUnresolvedThreads.length} review comments still need to be addressed (based on LLM analysis)`;
|
|
@@ -181,11 +205,12 @@ export async function verifyAndResolveComments(options) {
|
|
|
181
205
|
data: {
|
|
182
206
|
featureId,
|
|
183
207
|
totalReviews: reviews.length,
|
|
184
|
-
unresolvedReviews:
|
|
208
|
+
unresolvedReviews: remainingReviewsCount,
|
|
185
209
|
totalComments: unresolvedThreads.length, // Original count before LLM analysis
|
|
186
210
|
resolvedComments: addressedThreads.length, // LLM determined these are addressed
|
|
187
211
|
unresolvedComments: trulyUnresolvedThreads.length, // LLM determined these still need work
|
|
188
212
|
commentsMarkedResolved: addressedThreads.length,
|
|
213
|
+
dismissedReviews: dismissedReviewsCount,
|
|
189
214
|
suggestions,
|
|
190
215
|
unresolvedReviewDetails,
|
|
191
216
|
unresolvedCommentDetails,
|
|
@@ -71,3 +71,16 @@ export declare function returnToMainBranch(baseBranch?: string, verbose?: boolea
|
|
|
71
71
|
* @returns Cleanup function that will return to main branch
|
|
72
72
|
*/
|
|
73
73
|
export declare function preparePhaseGitEnvironment(featureId: string, baseBranch?: string, verbose?: boolean): () => void;
|
|
74
|
+
/**
|
|
75
|
+
* Sync feat branch with main using GitHub API
|
|
76
|
+
* This ensures the feat branch (PR base) is up to date with main,
|
|
77
|
+
* preventing extra commits from appearing in PRs when dev branch is rebased.
|
|
78
|
+
*
|
|
79
|
+
* @param featureId - The feature ID (will be used to construct branch name "feat/{featureId}")
|
|
80
|
+
* @param githubToken - GitHub personal access token or app token
|
|
81
|
+
* @param owner - Repository owner
|
|
82
|
+
* @param repo - Repository name
|
|
83
|
+
* @param baseBranch - The base branch to sync from (default: "main")
|
|
84
|
+
* @param verbose - Whether to log verbose output
|
|
85
|
+
*/
|
|
86
|
+
export declare function syncFeatBranchWithMain(featureId: string, githubToken: string, owner: string, repo: string, baseBranch?: string, verbose?: boolean): Promise<boolean>;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shared utilities for consistent git branch management across all phases
|
|
4
4
|
*/
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import { Octokit } from '@octokit/rest';
|
|
6
7
|
import { logInfo, logError } from './logger.js';
|
|
7
8
|
/**
|
|
8
9
|
* Get current Git branch name
|
|
@@ -358,3 +359,100 @@ export function preparePhaseGitEnvironment(featureId, baseBranch = 'main', verbo
|
|
|
358
359
|
// Return cleanup function
|
|
359
360
|
return cleanup;
|
|
360
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Sync feat branch with main using GitHub API
|
|
364
|
+
* This ensures the feat branch (PR base) is up to date with main,
|
|
365
|
+
* preventing extra commits from appearing in PRs when dev branch is rebased.
|
|
366
|
+
*
|
|
367
|
+
* @param featureId - The feature ID (will be used to construct branch name "feat/{featureId}")
|
|
368
|
+
* @param githubToken - GitHub personal access token or app token
|
|
369
|
+
* @param owner - Repository owner
|
|
370
|
+
* @param repo - Repository name
|
|
371
|
+
* @param baseBranch - The base branch to sync from (default: "main")
|
|
372
|
+
* @param verbose - Whether to log verbose output
|
|
373
|
+
*/
|
|
374
|
+
export async function syncFeatBranchWithMain(featureId, githubToken, owner, repo, baseBranch = 'main', verbose) {
|
|
375
|
+
const featBranch = `feat/${featureId}`;
|
|
376
|
+
try {
|
|
377
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
378
|
+
// Check if feat branch exists
|
|
379
|
+
if (verbose) {
|
|
380
|
+
logInfo(`🔍 Checking if ${featBranch} branch exists...`);
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
await octokit.repos.getBranch({
|
|
384
|
+
owner,
|
|
385
|
+
repo,
|
|
386
|
+
branch: featBranch,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (error.status === 404) {
|
|
391
|
+
if (verbose) {
|
|
392
|
+
logInfo(`ℹ️ ${featBranch} branch does not exist, skipping sync`);
|
|
393
|
+
}
|
|
394
|
+
return true; // Not an error, just no feat branch yet
|
|
395
|
+
}
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
// Get the latest SHA of the base branch
|
|
399
|
+
const { data: baseBranchData } = await octokit.repos.getBranch({
|
|
400
|
+
owner,
|
|
401
|
+
repo,
|
|
402
|
+
branch: baseBranch,
|
|
403
|
+
});
|
|
404
|
+
const mainSha = baseBranchData.commit.sha;
|
|
405
|
+
// Get the current SHA of the feat branch
|
|
406
|
+
const { data: featBranchData } = await octokit.repos.getBranch({
|
|
407
|
+
owner,
|
|
408
|
+
repo,
|
|
409
|
+
branch: featBranch,
|
|
410
|
+
});
|
|
411
|
+
const featSha = featBranchData.commit.sha;
|
|
412
|
+
// Check if feat branch is already up to date (same as main or ahead)
|
|
413
|
+
// We need to merge main into feat to keep it updated
|
|
414
|
+
if (verbose) {
|
|
415
|
+
logInfo(`📥 Syncing ${featBranch} with ${baseBranch}...`);
|
|
416
|
+
logInfo(` ${baseBranch} SHA: ${mainSha.substring(0, 7)}`);
|
|
417
|
+
logInfo(` ${featBranch} SHA: ${featSha.substring(0, 7)}`);
|
|
418
|
+
}
|
|
419
|
+
// Use GitHub merge API to merge main into feat branch
|
|
420
|
+
try {
|
|
421
|
+
await octokit.repos.merge({
|
|
422
|
+
owner,
|
|
423
|
+
repo,
|
|
424
|
+
base: featBranch,
|
|
425
|
+
head: baseBranch,
|
|
426
|
+
commit_message: `chore: sync ${featBranch} with ${baseBranch}`,
|
|
427
|
+
});
|
|
428
|
+
if (verbose) {
|
|
429
|
+
logInfo(`✅ Successfully synced ${featBranch} with ${baseBranch}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (mergeError) {
|
|
433
|
+
// 409 means nothing to merge (already up to date)
|
|
434
|
+
if (mergeError.status === 409) {
|
|
435
|
+
if (verbose) {
|
|
436
|
+
logInfo(`ℹ️ ${featBranch} is already up to date with ${baseBranch}`);
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
// 404 means branch doesn't exist (shouldn't happen since we checked above)
|
|
441
|
+
if (mergeError.status === 404) {
|
|
442
|
+
if (verbose) {
|
|
443
|
+
logInfo(`ℹ️ ${featBranch} branch not found, skipping sync`);
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
throw mergeError;
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (verbose) {
|
|
453
|
+
logError(`⚠️ Failed to sync ${featBranch} with ${baseBranch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
454
|
+
}
|
|
455
|
+
// Don't fail the whole process if sync fails, just log warning
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|