Skip to content

Commit 6cc1d95

Browse files
committed
Implement merge workflow and MergeManager tests Fixes #51
1 parent 8617ccd commit 6cc1d95

File tree

7 files changed

+1188
-38
lines changed

7 files changed

+1188
-38
lines changed

src/commands/finish.test.ts

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GitHubService } from '../lib/GitHubService.js'
44
import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
55
import { ValidationRunner } from '../lib/ValidationRunner.js'
66
import { CommitManager } from '../lib/CommitManager.js'
7+
import { MergeManager } from '../lib/MergeManager.js'
78
import type { Issue, PullRequest } from '../types/index.js'
89
import type { GitWorktree } from '../types/worktree.js'
910

@@ -12,6 +13,7 @@ vi.mock('../lib/GitHubService.js')
1213
vi.mock('../lib/GitWorktreeManager.js')
1314
vi.mock('../lib/ValidationRunner.js')
1415
vi.mock('../lib/CommitManager.js')
16+
vi.mock('../lib/MergeManager.js')
1517

1618
// Mock the logger to prevent console output during tests
1719
vi.mock('../utils/logger.js', () => ({
@@ -30,12 +32,14 @@ describe('FinishCommand', () => {
3032
let mockGitWorktreeManager: GitWorktreeManager
3133
let mockValidationRunner: ValidationRunner
3234
let mockCommitManager: CommitManager
35+
let mockMergeManager: MergeManager
3336

3437
beforeEach(() => {
3538
mockGitHubService = new GitHubService()
3639
mockGitWorktreeManager = new GitWorktreeManager()
3740
mockValidationRunner = new ValidationRunner()
3841
mockCommitManager = new CommitManager()
42+
mockMergeManager = new MergeManager()
3943

4044
// Mock ValidationRunner.runValidations to always succeed by default
4145
vi.mocked(mockValidationRunner.runValidations).mockResolvedValue({
@@ -57,11 +61,16 @@ describe('FinishCommand', () => {
5761
// Mock CommitManager.commitChanges to succeed by default
5862
vi.mocked(mockCommitManager.commitChanges).mockResolvedValue(undefined)
5963

64+
// Mock MergeManager methods to succeed by default
65+
vi.mocked(mockMergeManager.rebaseOnMain).mockResolvedValue(undefined)
66+
vi.mocked(mockMergeManager.performFastForwardMerge).mockResolvedValue(undefined)
67+
6068
command = new FinishCommand(
6169
mockGitHubService,
6270
mockGitWorktreeManager,
6371
mockValidationRunner,
64-
mockCommitManager
72+
mockCommitManager,
73+
mockMergeManager
6574
)
6675
})
6776

@@ -1296,6 +1305,82 @@ describe('FinishCommand', () => {
12961305
})
12971306

12981307
describe('workflow execution order', () => {
1308+
it('should execute complete workflow including merge steps', async () => {
1309+
// Test the complete workflow order: validate → detect → commit → rebase → merge
1310+
const executionOrder: string[] = []
1311+
1312+
// Mock ValidationRunner to track execution order
1313+
vi.mocked(mockValidationRunner.runValidations).mockImplementation(async () => {
1314+
executionOrder.push('validation')
1315+
return {
1316+
success: true,
1317+
steps: [],
1318+
totalDuration: 0,
1319+
}
1320+
})
1321+
1322+
// Mock CommitManager to track execution order
1323+
vi.mocked(mockCommitManager.detectUncommittedChanges).mockImplementation(async () => {
1324+
executionOrder.push('commit-detect')
1325+
return {
1326+
hasUncommittedChanges: true,
1327+
unstagedFiles: ['src/test.ts'],
1328+
stagedFiles: [],
1329+
currentBranch: 'feat/issue-123',
1330+
isAheadOfRemote: false,
1331+
isBehindRemote: false,
1332+
}
1333+
})
1334+
1335+
vi.mocked(mockCommitManager.commitChanges).mockImplementation(async () => {
1336+
executionOrder.push('commit-execute')
1337+
})
1338+
1339+
// Mock MergeManager to track execution order
1340+
vi.mocked(mockMergeManager.rebaseOnMain).mockImplementation(async () => {
1341+
executionOrder.push('rebase')
1342+
})
1343+
1344+
vi.mocked(mockMergeManager.performFastForwardMerge).mockImplementation(async () => {
1345+
executionOrder.push('merge')
1346+
})
1347+
1348+
vi.mocked(mockGitHubService.detectInputType).mockResolvedValue({
1349+
type: 'issue',
1350+
number: 123,
1351+
rawInput: '123',
1352+
})
1353+
vi.mocked(mockGitHubService.fetchIssue).mockResolvedValue({
1354+
number: 123,
1355+
title: 'Test Issue',
1356+
state: 'open',
1357+
body: '',
1358+
labels: [],
1359+
assignees: [],
1360+
url: 'https://github.com/test/repo/issues/123',
1361+
} as Issue)
1362+
vi.mocked(mockGitWorktreeManager.findWorktreesByIdentifier).mockResolvedValue([
1363+
{ path: '/test/issue-123', branch: 'feat/issue-123', commit: 'abc123', bare: false },
1364+
] as GitWorktree[])
1365+
1366+
// This should succeed with the correct order
1367+
await expect(
1368+
command.execute({
1369+
identifier: '123',
1370+
options: {},
1371+
})
1372+
).resolves.not.toThrow()
1373+
1374+
// ✅ CORRECT: The implementation should follow this order
1375+
expect(executionOrder).toEqual([
1376+
'validation', // ✅ First: Ensure code quality
1377+
'commit-detect', // ✅ Second: Check if there are changes to commit
1378+
'commit-execute', // ✅ Third: Only commit if validation passed
1379+
'rebase', // ✅ Fourth: Rebase branch on main
1380+
'merge' // ✅ Fifth: Perform fast-forward merge
1381+
])
1382+
})
1383+
12991384
it('should run validation BEFORE detecting and committing changes', async () => {
13001385
// Test the correct workflow order: validate → detect → commit
13011386
const executionOrder: string[] = []
@@ -1423,6 +1508,77 @@ describe('FinishCommand', () => {
14231508
expect(mockCommitManager.detectUncommittedChanges).not.toHaveBeenCalled()
14241509
expect(mockCommitManager.commitChanges).not.toHaveBeenCalled()
14251510
})
1511+
1512+
it('should pass correct options to MergeManager', async () => {
1513+
vi.mocked(mockGitHubService.detectInputType).mockResolvedValue({
1514+
type: 'issue',
1515+
number: 123,
1516+
rawInput: '123',
1517+
})
1518+
vi.mocked(mockGitHubService.fetchIssue).mockResolvedValue({
1519+
number: 123,
1520+
title: 'Test Issue',
1521+
state: 'open',
1522+
body: '',
1523+
labels: [],
1524+
assignees: [],
1525+
url: 'https://github.com/test/repo/issues/123',
1526+
} as Issue)
1527+
vi.mocked(mockGitWorktreeManager.findWorktreesByIdentifier).mockResolvedValue([
1528+
{ path: '/test/issue-123', branch: 'feat/issue-123', commit: 'abc123', bare: false },
1529+
] as GitWorktree[])
1530+
1531+
await command.execute({
1532+
identifier: '123',
1533+
options: { dryRun: true, force: true },
1534+
})
1535+
1536+
// Verify MergeManager was called with correct options
1537+
expect(mockMergeManager.rebaseOnMain).toHaveBeenCalledWith('/test/issue-123', {
1538+
dryRun: true,
1539+
force: true,
1540+
})
1541+
expect(mockMergeManager.performFastForwardMerge).toHaveBeenCalledWith('feat/issue-123', '/test/issue-123', {
1542+
dryRun: true,
1543+
force: true,
1544+
})
1545+
})
1546+
1547+
it('should handle rebase conflicts and stop workflow', async () => {
1548+
vi.mocked(mockGitHubService.detectInputType).mockResolvedValue({
1549+
type: 'issue',
1550+
number: 123,
1551+
rawInput: '123',
1552+
})
1553+
vi.mocked(mockGitHubService.fetchIssue).mockResolvedValue({
1554+
number: 123,
1555+
title: 'Test Issue',
1556+
state: 'open',
1557+
body: '',
1558+
labels: [],
1559+
assignees: [],
1560+
url: 'https://github.com/test/repo/issues/123',
1561+
} as Issue)
1562+
vi.mocked(mockGitWorktreeManager.findWorktreesByIdentifier).mockResolvedValue([
1563+
{ path: '/test/issue-123', branch: 'feat/issue-123', commit: 'abc123', bare: false },
1564+
] as GitWorktree[])
1565+
1566+
// Mock rebase to fail with conflicts
1567+
vi.mocked(mockMergeManager.rebaseOnMain).mockRejectedValue(
1568+
new Error('Rebase failed: conflicts detected in src/test.ts')
1569+
)
1570+
1571+
await expect(
1572+
command.execute({
1573+
identifier: '123',
1574+
options: {},
1575+
})
1576+
).rejects.toThrow('Rebase failed: conflicts detected in src/test.ts')
1577+
1578+
// Verify merge was never called after rebase failure
1579+
expect(mockMergeManager.performFastForwardMerge).not.toHaveBeenCalled()
1580+
})
1581+
14261582
})
14271583

14281584
describe('dependency injection', () => {
@@ -1450,6 +1606,12 @@ describe('FinishCommand', () => {
14501606
expect(cmd).toBeDefined()
14511607
})
14521608

1609+
it('should accept MergeManager via constructor', () => {
1610+
const customMergeManager = new MergeManager()
1611+
const cmd = new FinishCommand(undefined, undefined, undefined, undefined, customMergeManager)
1612+
expect(cmd).toBeDefined()
1613+
})
1614+
14531615
it('should create default instances when not provided', () => {
14541616
const cmd = new FinishCommand()
14551617
expect(cmd).toBeDefined()

src/commands/finish.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { GitHubService } from '../lib/GitHubService.js'
33
import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
44
import { ValidationRunner } from '../lib/ValidationRunner.js'
55
import { CommitManager } from '../lib/CommitManager.js'
6-
import type { FinishOptions, GitWorktree, CommitOptions } from '../types/index.js'
6+
import { MergeManager } from '../lib/MergeManager.js'
7+
import type { FinishOptions, GitWorktree, CommitOptions, MergeOptions } from '../types/index.js'
78
import path from 'path'
89

910
export interface FinishCommandInput {
@@ -24,18 +25,21 @@ export class FinishCommand {
2425
private gitWorktreeManager: GitWorktreeManager
2526
private validationRunner: ValidationRunner
2627
private commitManager: CommitManager
28+
private mergeManager: MergeManager
2729

2830
constructor(
2931
gitHubService?: GitHubService,
3032
gitWorktreeManager?: GitWorktreeManager,
3133
validationRunner?: ValidationRunner,
32-
commitManager?: CommitManager
34+
commitManager?: CommitManager,
35+
mergeManager?: MergeManager
3336
) {
3437
// Dependency injection for testing
3538
this.gitHubService = gitHubService ?? new GitHubService()
3639
this.gitWorktreeManager = gitWorktreeManager ?? new GitWorktreeManager()
3740
this.validationRunner = validationRunner ?? new ValidationRunner()
3841
this.commitManager = commitManager ?? new CommitManager()
42+
this.mergeManager = mergeManager ?? new MergeManager()
3943
}
4044

4145
/**
@@ -97,13 +101,21 @@ export class FinishCommand {
97101
logger.debug('No uncommitted changes found')
98102
}
99103

100-
// Step 6: Additional workflow steps (PLACEHOLDER - implemented in later sub-issues)
101-
// Migration handling, rebasing, merging, etc.
102-
if (input.options.dryRun) {
103-
logger.info('[DRY RUN] Would proceed with finish workflow')
104-
} else {
105-
logger.info('Ready to proceed with finish workflow (not yet implemented)')
104+
// Step 7: Rebase branch on main
105+
logger.info('Rebasing branch on main...')
106+
107+
const mergeOptions: MergeOptions = {
108+
dryRun: input.options.dryRun ?? false,
109+
force: input.options.force ?? false,
106110
}
111+
112+
await this.mergeManager.rebaseOnMain(worktree.path, mergeOptions)
113+
logger.success('Branch rebased successfully')
114+
115+
// Step 8: Perform fast-forward merge
116+
logger.info('Performing fast-forward merge...')
117+
await this.mergeManager.performFastForwardMerge(worktree.branch, worktree.path, mergeOptions)
118+
logger.success('Fast-forward merge completed successfully')
107119
} catch (error) {
108120
if (error instanceof Error) {
109121
logger.error(`${error.message}`)

0 commit comments

Comments
 (0)