feat: 项目 logo + 侧栏拖动排序 + 合并修复(主检出干净时 in-place)
- 项目 logo:仓库内文件(logo/icon/favicon 等多路径)→git remote 头像→自定义(URL/相对路径)→ 首字母徽章兜底;GET /api/projects/:id/logo(文件流/302),配置面板加 Logo 输入 - 侧栏项目拖动排序:projects.sort_order + POST /api/projects/reorder,乐观更新 - 合并修复:默认分支正被主检出占用时,若工作区干净则直接在主检出 in-place 合并 (用户手动合并的等价操作,安全);脏工作区拒绝并提示提交/暂存或仅通过 - schema: projects.logo / sort_order(ensureColumn 平滑迁移) 测试 75/75(merge 用例改为干净→成功/脏→拒绝) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+48
-7
@@ -14,17 +14,52 @@ export function mergeWorktreeDirFor(taskId: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把任务分支合并进 defaultBranch(PR 闭环的"merge"步)。
|
||||
* 绝不触碰用户工作区:在 <dataDir>/worktrees/_merge/<taskId>/ 临时 git worktree 检出 defaultBranch,
|
||||
* 在其中 `git merge --no-ff <branch>`,成功后删除临时 worktree。不 push。
|
||||
* 全部 execFile('git')(见 worktree.ts 的 git()),不走 shell。
|
||||
* 把任务分支合并进 defaultBranch(PR 闭环的"merge"步)。不 push。全部 execFile('git'),不走 shell。
|
||||
*
|
||||
* 两条路径:
|
||||
* - defaultBranch 正被主检出(repoPath)占用 → 直接在主检出里 `git merge --no-ff`(工作区必须干净,
|
||||
* 否则拒绝并提示提交/暂存或「仅通过」)。这是用户手动合并会做的事,安全。
|
||||
* - 否则 → 在 <dataDir>/worktrees/_merge/<taskId>/ 临时 worktree 检出 defaultBranch 后合并,不碰任何检出。
|
||||
*
|
||||
* 边界:
|
||||
* - 分支不存在(已删/已清理)→ {ok:false, error}
|
||||
* - defaultBranch 正被用户工作区检出 → git worktree add 失败 → {ok:false, error}(宁可失败也不动用户检出)
|
||||
* - 冲突 → `git merge --abort` 后清理临时 worktree,error 含冲突文件列表;defaultBranch 与任务分支均无损
|
||||
* - 重复合并(分支已在 defaultBranch 里)→ git 返回 Already up to date,ok:true、不新建提交
|
||||
* - 主检出有未提交改动 → 拒绝(提示提交/暂存或仅通过)
|
||||
* - 冲突 → `git merge --abort` 后复原,error 含冲突文件列表;两分支均无损
|
||||
* - 重复合并(分支已在 defaultBranch 里)→ Already up to date,ok:true、不新建提交
|
||||
*/
|
||||
/**
|
||||
* 目标分支正被主检出(repoPath)占用时的合并:直接在主检出里 `git merge --no-ff`。
|
||||
* 这是用户手动合并会做的事,安全;但要求工作区干净,否则可能与未提交改动纠缠 → 拒绝。
|
||||
*/
|
||||
async function mergeInPrimaryCheckout(
|
||||
repoPath: string,
|
||||
branch: string,
|
||||
defaultBranch: string,
|
||||
taskId: string,
|
||||
): Promise<MergeResult> {
|
||||
const dirty = (await git(repoPath, ['status', '--porcelain']).catch(() => '')).trim();
|
||||
if (dirty) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `默认分支 ${defaultBranch} 检出于工作区且有未提交改动,无法安全合并;请先提交/暂存这些改动,或点「仅通过」(任务保留在审核闸)`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
await git(repoPath, [
|
||||
'-c', 'user.name=maestro', '-c', 'user.email=maestro@local',
|
||||
'merge', '--no-ff', branch, '-m', `merge: ${branch} [${taskId}]`,
|
||||
]);
|
||||
const mergeCommit = (await git(repoPath, ['rev-parse', 'HEAD'])).trim();
|
||||
return { ok: true, mergeCommit };
|
||||
} catch (e) {
|
||||
let conflicted = '';
|
||||
try { conflicted = (await git(repoPath, ['diff', '--name-only', '--diff-filter=U'])).trim(); } catch { /* ignore */ }
|
||||
await git(repoPath, ['merge', '--abort']).catch(() => undefined);
|
||||
const files = conflicted ? `;冲突文件:${conflicted.split('\n').join('、')}` : '';
|
||||
return { ok: false, error: `${(e as Error).message}${files}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function mergeBranch(
|
||||
repoPath: string,
|
||||
branch: string,
|
||||
@@ -38,6 +73,12 @@ export async function mergeBranch(
|
||||
return { ok: false, error: `任务分支不存在:${branch}(可能已被删除或清理)` };
|
||||
}
|
||||
|
||||
// 0.5) 目标分支正被主检出占用 → 直接在主检出里合并(工作区干净时),避免临时 worktree 二次检出失败
|
||||
const headBranch = (await git(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => '')).trim();
|
||||
if (headBranch === defaultBranch) {
|
||||
return mergeInPrimaryCheckout(repoPath, branch, defaultBranch, taskId);
|
||||
}
|
||||
|
||||
// 1) 清理残留的临时合并 worktree(上次中断/失败遗留),再新建
|
||||
const dir = mergeWorktreeDirFor(taskId);
|
||||
await git(repoPath, ['worktree', 'prune']).catch(() => undefined);
|
||||
|
||||
Reference in New Issue
Block a user