edsger 0.21.4 → 0.21.6

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.
@@ -42,6 +42,7 @@ export function devBranchToFeatBranch(devBranchName) {
42
42
  }
43
43
  /**
44
44
  * Push a branch to remote
45
+ * Falls back to force-with-lease if normal push fails (e.g., after rebase)
45
46
  */
46
47
  async function pushBranch(branchName, verbose) {
47
48
  try {
@@ -66,8 +67,27 @@ async function pushBranch(branchName, verbose) {
66
67
  return { success: true };
67
68
  }
68
69
  catch (retryError) {
69
- const errorMessage = retryError instanceof Error ? retryError.message : String(retryError);
70
- return { success: false, error: errorMessage };
70
+ // If push still fails (likely non-fast-forward after rebase),
71
+ // use force-with-lease for safer force push
72
+ if (verbose) {
73
+ logInfo(`⚠️ Push rejected, attempting force push with lease...`);
74
+ }
75
+ try {
76
+ execSync(`git push --force-with-lease origin ${branchName}`, {
77
+ encoding: 'utf-8',
78
+ stdio: verbose ? 'inherit' : 'pipe',
79
+ });
80
+ if (verbose) {
81
+ logInfo(`✅ Successfully force pushed ${branchName}`);
82
+ }
83
+ return { success: true };
84
+ }
85
+ catch (forceError) {
86
+ const errorMessage = forceError instanceof Error
87
+ ? forceError.message
88
+ : String(forceError);
89
+ return { success: false, error: errorMessage };
90
+ }
71
91
  }
72
92
  }
73
93
  }
@@ -6,7 +6,7 @@ import { fetchCodeImplementationContext, formatContextForPrompt, } from './conte
6
6
  import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
7
7
  import { buildImplementationResult, buildVerificationFailureResult, buildNoResultsError, } from './outcome.js';
8
8
  import { performVerificationCycle } from '../code-implementation-verification/index.js';
9
- import { prepareCustomBranchGitEnvironment, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
9
+ import { prepareCustomBranchGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
10
10
  import { getCurrentBranch, updateBranch, getBranches, createBranches, getBaseBranchInfo, } from '../../services/branches.js';
11
11
  import { createBranchPullRequest, } from './branch-pr-creator.js';
12
12
  import { getGitHubConfig } from '../../api/github.js';
@@ -114,7 +114,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
114
114
  // Determine the actual base branch for branch chaining
115
115
  // If the current feature branch depends on another branch, use that as the base
116
116
  let actualBaseBranch = baseBranch;
117
- let needsRebase = false;
117
+ let originalBaseBranch;
118
118
  if (currentBranch && currentBranch.base_branch_id) {
119
119
  try {
120
120
  const allBranches = await getBranches({
@@ -123,20 +123,20 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
123
123
  });
124
124
  const baseBranchInfo = await getBaseBranchInfo(currentBranch, allBranches, baseBranch);
125
125
  actualBaseBranch = baseBranchInfo.baseBranch;
126
- needsRebase = baseBranchInfo.needsRebase;
126
+ originalBaseBranch = baseBranchInfo.originalBaseBranch;
127
127
  if (verbose) {
128
128
  logInfo(`🔗 Branch chaining detected:`);
129
129
  logInfo(` Current branch: ${currentBranch.name}`);
130
130
  logInfo(` Base branch: ${actualBaseBranch}`);
131
+ if (originalBaseBranch) {
132
+ logInfo(` Original base branch: ${originalBaseBranch} (for --onto rebase)`);
133
+ }
131
134
  if (baseBranchInfo.baseBranchMerged) {
132
- logInfo(` Parent branch merged: yes (using main)`);
135
+ logInfo(` Parent branch merged: yes`);
133
136
  }
134
137
  else {
135
138
  logInfo(` Parent branch merged: no (using parent branch)`);
136
139
  }
137
- if (needsRebase) {
138
- logInfo(` Rebase needed: yes`);
139
- }
140
140
  }
141
141
  }
142
142
  catch (error) {
@@ -178,29 +178,15 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
178
178
  }
179
179
  }
180
180
  }
181
- const cleanupGit = prepareCustomBranchGitEnvironment(devBranchName, actualBaseBranch, verbose);
182
- // Force push after rebase to trigger GitHub to recalculate PR diff
183
- if (featSyncedToMain) {
184
- try {
185
- const { execSync } = await import('child_process');
186
- if (verbose) {
187
- logInfo(`📤 Force pushing ${devBranchName} to trigger PR diff recalculation...`);
188
- }
189
- execSync(`git push --force-with-lease origin ${devBranchName}`, {
190
- encoding: 'utf-8',
191
- stdio: verbose ? 'inherit' : 'pipe',
192
- });
193
- if (verbose) {
194
- logInfo(`✅ Successfully force pushed ${devBranchName}`);
195
- }
196
- }
197
- catch (pushError) {
198
- if (verbose) {
199
- logInfo(`⚠️ Could not force push ${devBranchName}: ${pushError instanceof Error ? pushError.message : String(pushError)}`);
200
- }
201
- // Don't fail the whole process if push fails
202
- }
203
- }
181
+ // Use async version to support --onto rebase for branch chaining
182
+ // This correctly handles cases where base branch was squash-merged
183
+ const { cleanup: cleanupGit } = await prepareCustomBranchGitEnvironmentAsync({
184
+ featureBranch: devBranchName,
185
+ baseBranch: actualBaseBranch,
186
+ originalBaseBranch, // For --onto rebase when base branch was squash-merged
187
+ verbose,
188
+ forcePushAfterRebase: featSyncedToMain, // Trigger GitHub to recalculate PR diff
189
+ });
204
190
  try {
205
191
  // Fetch all required context information via MCP endpoints
206
192
  if (verbose) {
@@ -856,6 +842,7 @@ const parseImplementationResponse = (response, featureId) => {
856
842
  };
857
843
  /**
858
844
  * Push branch to remote repository
845
+ * Falls back to force-with-lease if normal push fails (e.g., after rebase)
859
846
  */
860
847
  async function pushToRemote(branchName, verbose) {
861
848
  try {
@@ -883,11 +870,31 @@ async function pushToRemote(branchName, verbose) {
883
870
  return { success: true };
884
871
  }
885
872
  catch (retryError) {
886
- const errorMessage = retryError instanceof Error ? retryError.message : String(retryError);
887
- return {
888
- success: false,
889
- error: errorMessage,
890
- };
873
+ // If push still fails (likely non-fast-forward after rebase),
874
+ // use force-with-lease for safer force push
875
+ // force-with-lease ensures we don't overwrite others' work by checking remote state
876
+ if (verbose) {
877
+ logInfo(`⚠️ Push rejected, attempting force push with lease...`);
878
+ }
879
+ try {
880
+ execSync(`git push --force-with-lease origin ${branchName}`, {
881
+ encoding: 'utf-8',
882
+ stdio: verbose ? 'inherit' : 'pipe',
883
+ });
884
+ if (verbose) {
885
+ logInfo(`✅ Successfully force pushed ${branchName}`);
886
+ }
887
+ return { success: true };
888
+ }
889
+ catch (forceError) {
890
+ const errorMessage = forceError instanceof Error
891
+ ? forceError.message
892
+ : String(forceError);
893
+ return {
894
+ success: false,
895
+ error: errorMessage,
896
+ };
897
+ }
891
898
  }
892
899
  }
893
900
  }
@@ -62,12 +62,44 @@ const discardUncommittedChanges = (verbose) => {
62
62
  };
63
63
  /**
64
64
  * Switch to an existing branch
65
+ * By default, checks remote if branch doesn't exist locally (handles multi-clone scenarios)
66
+ * @param branch - The branch to switch to
67
+ * @param verbose - Whether to log verbose output
65
68
  */
66
69
  const switchToBranch = (branch, verbose) => {
67
70
  try {
68
- // First check if branch exists
71
+ // First check if branch exists locally
69
72
  if (!branchExists(branch)) {
70
- throw new Error(`Branch '${branch}' does not exist. The branch should have been created during code implementation phase.`);
73
+ // Branch doesn't exist locally, try to fetch and create tracking branch from remote
74
+ if (verbose) {
75
+ console.log(`🔍 Branch '${branch}' not found locally, checking remote...`);
76
+ }
77
+ try {
78
+ // Fetch to get latest remote refs
79
+ execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
80
+ // Check if remote branch exists
81
+ execSync(`git rev-parse --verify origin/${branch}`, {
82
+ encoding: 'utf-8',
83
+ stdio: 'pipe',
84
+ });
85
+ // Remote branch exists, create local tracking branch
86
+ if (verbose) {
87
+ console.log(`📥 Found branch '${branch}' on remote, creating local tracking branch...`);
88
+ }
89
+ // Discard any uncommitted changes before creating branch
90
+ discardUncommittedChanges(verbose);
91
+ execSync(`git checkout -b ${branch} --track origin/${branch}`, {
92
+ encoding: 'utf-8',
93
+ });
94
+ if (verbose) {
95
+ console.log(`✅ Created and switched to tracking branch: ${branch}`);
96
+ }
97
+ return;
98
+ }
99
+ catch (e) {
100
+ // Remote branch doesn't exist either
101
+ throw new Error(`Branch '${branch}' does not exist locally or on remote. The branch should have been created during code implementation phase.`);
102
+ }
71
103
  }
72
104
  if (verbose) {
73
105
  console.log(`🔀 Switching to branch: ${branch}`);
@@ -85,6 +117,7 @@ const switchToBranch = (branch, verbose) => {
85
117
  };
86
118
  /**
87
119
  * Push current branch to remote
120
+ * Falls back to force-with-lease if normal push fails (e.g., after rebase)
88
121
  */
89
122
  const pushBranch = (branch, verbose) => {
90
123
  try {
@@ -97,7 +130,22 @@ const pushBranch = (branch, verbose) => {
97
130
  }
98
131
  }
99
132
  catch (error) {
100
- throw new Error(`Failed to push branch: ${error}`);
133
+ // If push fails (likely non-fast-forward after rebase),
134
+ // use force-with-lease for safer force push
135
+ if (verbose) {
136
+ console.log(`⚠️ Push rejected, attempting force push with lease...`);
137
+ }
138
+ try {
139
+ execSync(`git push --force-with-lease origin ${branch}`, {
140
+ encoding: 'utf-8',
141
+ });
142
+ if (verbose) {
143
+ console.log(`✅ Successfully force pushed ${branch}`);
144
+ }
145
+ }
146
+ catch (forceError) {
147
+ throw new Error(`Failed to push branch: ${forceError}`);
148
+ }
101
149
  }
102
150
  };
103
151
  /**
@@ -208,6 +256,7 @@ export async function createPullRequest(config, feature) {
208
256
  console.log(`🔍 Current branch: ${currentBranch}`);
209
257
  }
210
258
  // If we're on the base branch, switch to dev branch (should already exist)
259
+ // Default behavior now checks remote if not found locally (multi-clone scenario)
211
260
  if (currentBranch === baseBranch) {
212
261
  const devBranch = `dev/${feature.id}`;
213
262
  if (verbose) {
@@ -68,10 +68,34 @@ export declare function branchExists(branch: string): boolean;
68
68
  * Check if a remote branch exists
69
69
  */
70
70
  export declare function remoteBranchExists(branch: string): boolean;
71
+ /**
72
+ * Options for switchToBranch
73
+ */
74
+ export interface SwitchToBranchOptions {
75
+ /**
76
+ * Skip checking remote for branch existence.
77
+ * By default, switchToBranch will check remote if branch doesn't exist locally,
78
+ * and create a tracking branch if found on remote.
79
+ * Set this to true to skip the remote check (useful for well-known local branches like 'main').
80
+ */
81
+ skipRemoteCheck?: boolean;
82
+ }
71
83
  /**
72
84
  * Switch to a specific Git branch, creating it if it doesn't exist
85
+ *
86
+ * By default, if the branch doesn't exist locally, it will:
87
+ * 1. Fetch from origin to check if branch exists on remote
88
+ * 2. If found on remote, create a local tracking branch from it
89
+ * 3. If not found on remote, create a new branch from current HEAD
90
+ *
91
+ * This handles multi-clone scenarios where a branch may have been created
92
+ * and pushed from another local clone.
93
+ *
94
+ * @param branch - The branch name to switch to
95
+ * @param verbose - Whether to log verbose output
96
+ * @param options - Additional options for branch switching
73
97
  */
74
- export declare function switchToBranch(branch: string, verbose?: boolean): void;
98
+ export declare function switchToBranch(branch: string, verbose?: boolean, options?: SwitchToBranchOptions): void;
75
99
  /**
76
100
  * Pull latest changes from remote for a specific branch
77
101
  */
@@ -240,20 +240,32 @@ export function remoteBranchExists(branch) {
240
240
  }
241
241
  /**
242
242
  * Switch to a specific Git branch, creating it if it doesn't exist
243
+ *
244
+ * By default, if the branch doesn't exist locally, it will:
245
+ * 1. Fetch from origin to check if branch exists on remote
246
+ * 2. If found on remote, create a local tracking branch from it
247
+ * 3. If not found on remote, create a new branch from current HEAD
248
+ *
249
+ * This handles multi-clone scenarios where a branch may have been created
250
+ * and pushed from another local clone.
251
+ *
252
+ * @param branch - The branch name to switch to
253
+ * @param verbose - Whether to log verbose output
254
+ * @param options - Additional options for branch switching
243
255
  */
244
- export function switchToBranch(branch, verbose) {
256
+ export function switchToBranch(branch, verbose, options) {
245
257
  try {
246
258
  if (verbose) {
247
259
  logInfo(`🔄 Switching to branch ${branch}...`);
248
260
  }
249
261
  // Check if branch exists locally
250
- const exists = branchExists(branch);
251
- if (exists) {
252
- // Branch exists, just switch to it
262
+ const localExists = branchExists(branch);
263
+ if (localExists) {
264
+ // Branch exists locally, just switch to it
253
265
  execSync(`git checkout ${branch}`, { encoding: 'utf-8', stdio: 'pipe' });
254
266
  }
255
- else {
256
- // Branch doesn't exist, create it from current HEAD
267
+ else if (options?.skipRemoteCheck) {
268
+ // Skip remote check, just create from current HEAD
257
269
  if (verbose) {
258
270
  logInfo(` Branch ${branch} doesn't exist, creating it...`);
259
271
  }
@@ -262,6 +274,37 @@ export function switchToBranch(branch, verbose) {
262
274
  stdio: 'pipe',
263
275
  });
264
276
  }
277
+ else {
278
+ // Default: Check if branch exists on remote before creating locally
279
+ // This handles multi-clone scenarios where branch was pushed from another clone
280
+ try {
281
+ // Fetch to get latest remote refs
282
+ execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
283
+ // Check if remote branch exists
284
+ execSync(`git rev-parse --verify origin/${branch}`, {
285
+ encoding: 'utf-8',
286
+ stdio: 'pipe',
287
+ });
288
+ // Remote branch exists, create local tracking branch from it
289
+ if (verbose) {
290
+ logInfo(` Branch ${branch} found on remote, creating local tracking branch...`);
291
+ }
292
+ execSync(`git checkout -b ${branch} --track origin/${branch}`, {
293
+ encoding: 'utf-8',
294
+ stdio: 'pipe',
295
+ });
296
+ }
297
+ catch (e) {
298
+ // Remote branch doesn't exist, create new branch from current HEAD
299
+ if (verbose) {
300
+ logInfo(` Branch ${branch} doesn't exist locally or remotely, creating new branch...`);
301
+ }
302
+ execSync(`git checkout -b ${branch}`, {
303
+ encoding: 'utf-8',
304
+ stdio: 'pipe',
305
+ });
306
+ }
307
+ }
265
308
  if (verbose) {
266
309
  logInfo(`✅ Switched to ${branch} branch`);
267
310
  }
@@ -338,11 +381,12 @@ export function switchToFeatureBranchAndRebase(featureBranch, baseBranch = 'main
338
381
  }
339
382
  }
340
383
  // Switch to feature branch (will create if doesn't exist)
384
+ // Default behavior now checks remote first (handles multi-clone scenarios)
341
385
  if (getCurrentBranch() !== featureBranch) {
342
386
  switchToBranch(featureBranch, verbose);
343
387
  }
344
- // Sync with remote feature branch if it exists
345
- // This handles the case where the branch was pushed from another machine
388
+ // Sync with remote feature branch if it exists and local branch needs updating
389
+ // This handles the case where local branch exists but is behind remote
346
390
  try {
347
391
  // Check for uncommitted changes and reset if found before any git operations
348
392
  if (hasUncommittedChanges()) {
@@ -351,7 +395,7 @@ export function switchToFeatureBranchAndRebase(featureBranch, baseBranch = 'main
351
395
  }
352
396
  resetUncommittedChanges(verbose);
353
397
  }
354
- // Fetch to get latest remote state
398
+ // Fetch to get latest remote state (may have been fetched in switchToBranch, but ensures fresh data)
355
399
  execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
356
400
  // Check if remote feature branch exists
357
401
  try {
@@ -359,18 +403,35 @@ export function switchToFeatureBranchAndRebase(featureBranch, baseBranch = 'main
359
403
  encoding: 'utf-8',
360
404
  stdio: 'pipe',
361
405
  });
362
- // Remote branch exists, sync with it
363
- if (verbose) {
364
- logInfo(`📥 Syncing with remote feature branch origin/${featureBranch}...`);
365
- }
366
- // Reset local branch to match remote branch
367
- // This ensures we have all commits from the remote (e.g., from another machine)
368
- execSync(`git reset --hard origin/${featureBranch}`, {
406
+ // Remote branch exists, check if we need to sync
407
+ // Get local and remote commit SHAs
408
+ const localSha = execSync(`git rev-parse ${featureBranch}`, {
369
409
  encoding: 'utf-8',
370
410
  stdio: 'pipe',
371
- });
372
- if (verbose) {
373
- logInfo(`✅ Synced local branch with origin/${featureBranch}`);
411
+ }).trim();
412
+ const remoteSha = execSync(`git rev-parse origin/${featureBranch}`, {
413
+ encoding: 'utf-8',
414
+ stdio: 'pipe',
415
+ }).trim();
416
+ if (localSha !== remoteSha) {
417
+ // Local and remote differ, reset to remote
418
+ if (verbose) {
419
+ logInfo(`📥 Syncing with remote feature branch origin/${featureBranch}...`);
420
+ logInfo(` Local: ${localSha.substring(0, 7)}`);
421
+ logInfo(` Remote: ${remoteSha.substring(0, 7)}`);
422
+ }
423
+ // Reset local branch to match remote branch
424
+ // This ensures we have all commits from the remote (e.g., from another machine)
425
+ execSync(`git reset --hard origin/${featureBranch}`, {
426
+ encoding: 'utf-8',
427
+ stdio: 'pipe',
428
+ });
429
+ if (verbose) {
430
+ logInfo(`✅ Synced local branch with origin/${featureBranch}`);
431
+ }
432
+ }
433
+ else if (verbose) {
434
+ logInfo(`✅ Local branch is up to date with origin/${featureBranch}`);
374
435
  }
375
436
  }
376
437
  catch (e) {
@@ -660,10 +721,12 @@ export async function switchToFeatureBranchAndRebaseAsync(options) {
660
721
  }
661
722
  }
662
723
  // Switch to feature branch (will create if doesn't exist)
724
+ // Default behavior now checks remote first (handles multi-clone scenarios)
663
725
  if (getCurrentBranch() !== featureBranch) {
664
726
  switchToBranch(featureBranch, verbose);
665
727
  }
666
- // Sync with remote feature branch if it exists
728
+ // Sync with remote feature branch if it exists and local branch needs updating
729
+ // This handles the case where local branch exists but is behind remote
667
730
  try {
668
731
  if (hasUncommittedChanges()) {
669
732
  if (verbose) {
@@ -671,21 +734,40 @@ export async function switchToFeatureBranchAndRebaseAsync(options) {
671
734
  }
672
735
  resetUncommittedChanges(verbose);
673
736
  }
737
+ // Fetch to get latest remote state (may have been fetched in switchToBranch, but ensures fresh data)
674
738
  execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
675
739
  try {
676
740
  execSync(`git rev-parse --verify origin/${featureBranch}`, {
677
741
  encoding: 'utf-8',
678
742
  stdio: 'pipe',
679
743
  });
680
- if (verbose) {
681
- logInfo(`📥 Syncing with remote feature branch origin/${featureBranch}...`);
682
- }
683
- execSync(`git reset --hard origin/${featureBranch}`, {
744
+ // Remote branch exists, check if we need to sync
745
+ // Get local and remote commit SHAs
746
+ const localSha = execSync(`git rev-parse ${featureBranch}`, {
684
747
  encoding: 'utf-8',
685
748
  stdio: 'pipe',
686
- });
687
- if (verbose) {
688
- logInfo(`✅ Synced local branch with origin/${featureBranch}`);
749
+ }).trim();
750
+ const remoteSha = execSync(`git rev-parse origin/${featureBranch}`, {
751
+ encoding: 'utf-8',
752
+ stdio: 'pipe',
753
+ }).trim();
754
+ if (localSha !== remoteSha) {
755
+ // Local and remote differ, reset to remote
756
+ if (verbose) {
757
+ logInfo(`📥 Syncing with remote feature branch origin/${featureBranch}...`);
758
+ logInfo(` Local: ${localSha.substring(0, 7)}`);
759
+ logInfo(` Remote: ${remoteSha.substring(0, 7)}`);
760
+ }
761
+ execSync(`git reset --hard origin/${featureBranch}`, {
762
+ encoding: 'utf-8',
763
+ stdio: 'pipe',
764
+ });
765
+ if (verbose) {
766
+ logInfo(`✅ Synced local branch with origin/${featureBranch}`);
767
+ }
768
+ }
769
+ else if (verbose) {
770
+ logInfo(`✅ Local branch is up to date with origin/${featureBranch}`);
689
771
  }
690
772
  }
691
773
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.21.4",
3
+ "version": "0.21.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"