fix(merge): refuse a merge that would overwrite an untracked file in the target

Before merging (or staging a conflict resolution's git add -A), compare the
branch's newly-added paths against what's currently untracked in the target
working directory. A collision aborts with a new untracked_collision status
naming the path and size, surfaced through merge_task/review_task,
preview_merge/preview_merge_set (which merge-tree alone can't see), and the
UI merge paths via FlashFooterError/ShowErrorAsync instead of a silent
catch{}. git's own preflight already refuses this while the path stays
untracked at merge time; this closes the gap once a path becomes trackable
in between (e.g. an unrelated conflict resolution's git add -A) or across
the continue_merge TOCTOU window.
This commit is contained in:
mika kuns
2026-08-10 12:15:41 +02:00
parent 6a2a19cc9e
commit afe1b68f46
9 changed files with 269 additions and 13 deletions
+10 -7
View File
@@ -929,13 +929,16 @@ public sealed class ExternalMcpService
[McpServerTool, Description(
"Check whether a task would merge cleanly before touching anything — `git merge-tree --write-tree`, so the " +
"working tree, index and HEAD are untouched. status is 'clean' or 'conflict' (conflictFiles lists where git " +
"would stop); behind counts commits on targetBranch not yet on this branch, which flags a stale branch even " +
"when the preview is clean. IMPORTANT: a clean preview says nothing about whether the result compiles or " +
"passes tests — git can merge two changes cleanly (one file deleting a symbol another still references) and " +
"still break the build. isEmpty=true means the task's review range contributed nothing; check that flag " +
"rather than reading a small changedFileCount as empty. Throws if the task has neither an active worktree " +
"nor a handler commit range, or the list's working directory is missing from disk.")]
"working tree, index and HEAD are untouched. status is 'clean', 'conflict' (conflictFiles lists where git " +
"would stop), or 'untracked_collision' (conflictFiles lists a path the branch adds that already exists, " +
"untracked, in the target working directory — merge-tree can't see the working tree at all, so this is the " +
"only way to catch it before a real merge either refuses or, if that path became tracked in the meantime, " +
"silently overwrites it); behind counts commits on targetBranch not yet on this branch, which flags a stale " +
"branch even when the preview is clean. IMPORTANT: a clean preview says nothing about whether the result " +
"compiles or passes tests — git can merge two changes cleanly (one file deleting a symbol another still " +
"references) and still break the build. isEmpty=true means the task's review range contributed nothing; " +
"check that flag rather than reading a small changedFileCount as empty. Throws if the task has neither an " +
"active worktree nor a handler commit range, or the list's working directory is missing from disk.")]
public async Task<MergePreviewToolDto> PreviewMerge(
string taskId,
[Description("Branch to preview against; defaults to the repo's current branch.")]