Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
/**
|
|
* SimpleGit BranchManager Adapter
|
|
*
|
|
* Implementation of BranchManager port using simple-git.
|
|
* Performs branch-level operations (create, merge, diff, delete)
|
|
* on project clones without requiring a worktree.
|
|
*/
|
|
|
|
import { join } from 'node:path';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { simpleGit } from 'simple-git';
|
|
import type { BranchManager } from './branch-manager.js';
|
|
import type { MergeResult } from './types.js';
|
|
import { createModuleLogger } from '../logger/index.js';
|
|
|
|
const log = createModuleLogger('branch-manager');
|
|
|
|
export class SimpleGitBranchManager implements BranchManager {
|
|
async ensureBranch(repoPath: string, branch: string, baseBranch: string): Promise<void> {
|
|
const git = simpleGit(repoPath);
|
|
|
|
const exists = await this.branchExists(repoPath, branch);
|
|
if (exists) {
|
|
log.debug({ repoPath, branch }, 'branch already exists');
|
|
return;
|
|
}
|
|
|
|
await git.branch([branch, baseBranch]);
|
|
log.info({ repoPath, branch, baseBranch }, 'branch created');
|
|
}
|
|
|
|
async mergeBranch(repoPath: string, sourceBranch: string, targetBranch: string): Promise<MergeResult> {
|
|
// Use an ephemeral worktree for merge safety
|
|
const tmpPath = mkdtempSync(join(tmpdir(), 'cw-merge-'));
|
|
const repoGit = simpleGit(repoPath);
|
|
|
|
try {
|
|
// Create ephemeral worktree on target branch
|
|
await repoGit.raw(['worktree', 'add', tmpPath, targetBranch]);
|
|
|
|
const wtGit = simpleGit(tmpPath);
|
|
|
|
try {
|
|
await wtGit.merge([sourceBranch, '--no-edit']);
|
|
|
|
log.info({ repoPath, sourceBranch, targetBranch }, 'merge completed cleanly');
|
|
return { success: true, message: `Merged ${sourceBranch} into ${targetBranch}` };
|
|
} catch (mergeErr) {
|
|
// Check for merge conflicts
|
|
const status = await wtGit.status();
|
|
const conflicts = status.conflicted;
|
|
|
|
if (conflicts.length > 0) {
|
|
log.warn({ repoPath, sourceBranch, targetBranch, conflicts }, 'merge conflicts detected');
|
|
await wtGit.merge(['--abort']);
|
|
return {
|
|
success: false,
|
|
conflicts,
|
|
message: `Merge conflicts in ${conflicts.length} file(s)`,
|
|
};
|
|
}
|
|
|
|
// Non-conflict merge failure
|
|
throw mergeErr;
|
|
}
|
|
} finally {
|
|
// Clean up ephemeral worktree
|
|
try {
|
|
await repoGit.raw(['worktree', 'remove', tmpPath, '--force']);
|
|
} catch {
|
|
// Best-effort cleanup — force-remove the directory if worktree remove fails
|
|
try { rmSync(tmpPath, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
try { await repoGit.raw(['worktree', 'prune']); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
}
|
|
|
|
async diffBranches(repoPath: string, baseBranch: string, headBranch: string): Promise<string> {
|
|
const git = simpleGit(repoPath);
|
|
const diff = await git.diff([`${baseBranch}...${headBranch}`]);
|
|
return diff;
|
|
}
|
|
|
|
async deleteBranch(repoPath: string, branch: string): Promise<void> {
|
|
const git = simpleGit(repoPath);
|
|
const exists = await this.branchExists(repoPath, branch);
|
|
if (!exists) return;
|
|
|
|
try {
|
|
await git.deleteLocalBranch(branch, true);
|
|
log.info({ repoPath, branch }, 'branch deleted');
|
|
} catch (err) {
|
|
log.warn({ repoPath, branch, err: err instanceof Error ? err.message : String(err) }, 'failed to delete branch');
|
|
}
|
|
}
|
|
|
|
async branchExists(repoPath: string, branch: string): Promise<boolean> {
|
|
const git = simpleGit(repoPath);
|
|
try {
|
|
const branches = await git.branchLocal();
|
|
return branches.all.includes(branch);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async remoteBranchExists(repoPath: string, branch: string): Promise<boolean> {
|
|
try {
|
|
const git = simpleGit(repoPath);
|
|
const result = await git.branch(['-r']);
|
|
return result.all.some(
|
|
(ref) => ref === `origin/${branch}` || ref === `remotes/origin/${branch}`,
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|