Skip to content

Commit c6231f6

Browse files
committed
fix: sync against local base branch by default instead of upstream
1 parent fde05e8 commit c6231f6

4 files changed

Lines changed: 82 additions & 29 deletions

File tree

src/lib/providers/worktree-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,5 @@ export interface WorktreeProvider {
5757
* Sync a worktree with the base branch using the specified strategy.
5858
* Returns success status and any conflicts.
5959
*/
60-
sync(path: string, strategy: 'rebase' | 'merge', baseBranch?: string): Promise<SyncResult>;
60+
sync(path: string, strategy: 'rebase' | 'merge', baseBranch?: string, source?: 'local' | 'origin'): Promise<SyncResult>;
6161
}

src/lib/worktree.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { spawn } from 'bun';
22
import { join, resolve } from 'path';
33
import { getProjectId, getXdgDataDir } from './paths';
4-
import { gitCommand } from './git';
4+
import { gitCommand, getDefaultBranch } from './git';
55
import type {
66
WorktreeInfo,
77
SyncResult,
@@ -222,30 +222,23 @@ export async function syncWorktree(
222222
path: string,
223223
strategy: 'rebase' | 'merge',
224224
baseBranch?: string,
225+
source?: 'local' | 'origin',
225226
): Promise<SyncResult> {
226227
let targetBranch: string;
227228
if (baseBranch) {
228-
targetBranch = `origin/${baseBranch}`;
229+
const useOrigin = source === 'origin' || (!source);
230+
targetBranch = useOrigin ? `origin/${baseBranch}` : baseBranch;
229231
} else {
230-
const upstreamResult = await gitCommand(
231-
['-C', path, 'rev-parse', '--abbrev-ref', 'HEAD@{upstream}'],
232-
);
233-
234-
if (upstreamResult.exitCode !== 0) {
235-
const defaultBranchResult = await gitCommand([
236-
'symbolic-ref',
237-
'--short',
238-
'refs/remotes/origin/HEAD',
239-
]);
240-
targetBranch = defaultBranchResult.stdout || 'main';
241-
} else {
242-
targetBranch = upstreamResult.stdout;
243-
}
232+
const defaultBranch = await getDefaultBranch();
233+
const useOrigin = source === 'origin';
234+
targetBranch = useOrigin ? `origin/${defaultBranch}` : defaultBranch;
244235
}
245236

246-
const fetchResult = await gitCommand(['-C', path, 'fetch', 'origin']);
247-
if (fetchResult.exitCode !== 0) {
248-
return { success: false, conflicts: ['Failed to fetch from origin'] };
237+
if (source === 'origin' || (baseBranch && !source)) {
238+
const fetchResult = await gitCommand(['-C', path, 'fetch', 'origin']);
239+
if (fetchResult.exitCode !== 0) {
240+
return { success: false, conflicts: ['Failed to fetch from origin'] };
241+
}
249242
}
250243

251244
const syncResult = await gitCommand(['-C', path, strategy, targetBranch]);
@@ -285,7 +278,7 @@ export class GitWorktreeProvider implements WorktreeProvider {
285278
return listWorktrees();
286279
}
287280

288-
async sync(path: string, strategy: 'rebase' | 'merge', baseBranch?: string): Promise<SyncResult> {
289-
return syncWorktree(path, strategy, baseBranch);
281+
async sync(path: string, strategy: 'rebase' | 'merge', baseBranch?: string, source?: 'local' | 'origin'): Promise<SyncResult> {
282+
return syncWorktree(path, strategy, baseBranch, source);
290283
}
291284
}

src/tools/sync.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const mc_sync: ToolDefinition = tool({
1010
.enum(['rebase', 'merge'])
1111
.optional()
1212
.describe('Sync strategy (default: rebase)'),
13+
source: tool.schema
14+
.enum(['local', 'origin'])
15+
.optional()
16+
.describe('Sync source: local base branch (default) or origin'),
1317
},
1418
async execute(args) {
1519
// 1. Find job by name
@@ -22,9 +26,12 @@ export const mc_sync: ToolDefinition = tool({
2226
const syncStrategy = args.strategy || 'rebase';
2327

2428
// 3. Sync the worktree
25-
const result = job.baseBranch
26-
? await syncWorktree(job.worktreePath, syncStrategy, job.baseBranch)
27-
: await syncWorktree(job.worktreePath, syncStrategy);
29+
const result = await syncWorktree(
30+
job.worktreePath,
31+
syncStrategy,
32+
job.baseBranch,
33+
args.source,
34+
);
2835

2936
// 4. Format output
3037
if (result.success) {

tests/tools/sync.test.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,23 +85,29 @@ describe('mc_sync', () => {
8585

8686
await mc_sync.execute({ name: 'test-job' }, mockContext);
8787

88-
expect(mockSyncWorktree).toHaveBeenCalledWith('/tmp/mc-worktrees/test-job', 'rebase');
88+
expect(mockSyncWorktree).toHaveBeenCalledWith(
89+
'/tmp/mc-worktrees/test-job', 'rebase', undefined, undefined,
90+
);
8991
});
9092

9193
it('should use specified rebase strategy', async () => {
9294
mockSyncWorktree.mockResolvedValue({ success: true });
9395

9496
await mc_sync.execute({ name: 'test-job', strategy: 'rebase' }, mockContext);
9597

96-
expect(mockSyncWorktree).toHaveBeenCalledWith('/tmp/mc-worktrees/test-job', 'rebase');
98+
expect(mockSyncWorktree).toHaveBeenCalledWith(
99+
'/tmp/mc-worktrees/test-job', 'rebase', undefined, undefined,
100+
);
97101
});
98102

99103
it('should use specified merge strategy', async () => {
100104
mockSyncWorktree.mockResolvedValue({ success: true });
101105

102106
await mc_sync.execute({ name: 'test-job', strategy: 'merge' }, mockContext);
103107

104-
expect(mockSyncWorktree).toHaveBeenCalledWith('/tmp/mc-worktrees/test-job', 'merge');
108+
expect(mockSyncWorktree).toHaveBeenCalledWith(
109+
'/tmp/mc-worktrees/test-job', 'merge', undefined, undefined,
110+
);
105111
});
106112
});
107113

@@ -187,6 +193,53 @@ describe('mc_sync', () => {
187193
});
188194
});
189195

196+
describe('source parameter', () => {
197+
beforeEach(() => {
198+
mockGetJobByName.mockResolvedValue(createMockJob());
199+
mockSyncWorktree.mockResolvedValue({ success: true });
200+
});
201+
202+
it('should have optional arg: source', () => {
203+
expect(mc_sync.args.source).toBeDefined();
204+
});
205+
206+
it('should pass source=local to syncWorktree', async () => {
207+
await mc_sync.execute({ name: 'test-job', source: 'local' }, mockContext);
208+
209+
expect(mockSyncWorktree).toHaveBeenCalledWith(
210+
'/tmp/mc-worktrees/test-job', 'rebase', undefined, 'local',
211+
);
212+
});
213+
214+
it('should pass source=origin to syncWorktree', async () => {
215+
await mc_sync.execute({ name: 'test-job', source: 'origin' }, mockContext);
216+
217+
expect(mockSyncWorktree).toHaveBeenCalledWith(
218+
'/tmp/mc-worktrees/test-job', 'rebase', undefined, 'origin',
219+
);
220+
});
221+
222+
it('should pass baseBranch from job along with source', async () => {
223+
mockGetJobByName.mockResolvedValue(createMockJob({ baseBranch: 'develop' }));
224+
225+
await mc_sync.execute({ name: 'test-job', source: 'local' }, mockContext);
226+
227+
expect(mockSyncWorktree).toHaveBeenCalledWith(
228+
'/tmp/mc-worktrees/test-job', 'rebase', 'develop', 'local',
229+
);
230+
});
231+
232+
it('should pass baseBranch without source', async () => {
233+
mockGetJobByName.mockResolvedValue(createMockJob({ baseBranch: 'develop' }));
234+
235+
await mc_sync.execute({ name: 'test-job' }, mockContext);
236+
237+
expect(mockSyncWorktree).toHaveBeenCalledWith(
238+
'/tmp/mc-worktrees/test-job', 'rebase', 'develop', undefined,
239+
);
240+
});
241+
});
242+
190243
describe('different job names', () => {
191244
it('should work with different job names', async () => {
192245
mockGetJobByName.mockResolvedValue(createMockJob({ name: 'feature-branch' }));
@@ -217,7 +270,7 @@ describe('mc_sync', () => {
217270

218271
await mc_sync.execute({ name: 'test-job' }, mockContext);
219272

220-
expect(mockSyncWorktree).toHaveBeenCalledWith(worktreePath, 'rebase');
273+
expect(mockSyncWorktree).toHaveBeenCalledWith(worktreePath, 'rebase', undefined, undefined);
221274
});
222275
});
223276
});

0 commit comments

Comments
 (0)