Add UI-based merge conflict resolution#166
Conversation
…t conflict resolution
|
/crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}} AI review started. |
Advanced AI Review
Click to expand review1. 🔴 Conflict wrappers are appended to the bottom of the table, breaking file order
In To fix this, insert the
const firstRow = group[0];
const parentNode = firstRow.parentNode;
// Create wrapper container row
const wrapperRow = document.createElement('tr');
wrapperRow.className = 'conflict-wrapper-row';
// Insert wrapper into the table tree BEFORE we detach the conflict lines
if (parentNode) {
parentNode.insertBefore(wrapperRow, firstRow);
}
const wrapperCell = document.createElement('td');
wrapperCell.colSpan = 6;Be sure to remove the faulty insert statement at the end of the block: - // Insert wrapper before the first conflict line's original position
- parentNode.insertBefore(wrapperRow, null);2. 🔴 Unmatched additions and deletions are hidden as context lines instead of conflicts
In Because your JS gathers conflicts based on Separate actual identical context lines (
3. 🟡 Active state styling is missing on the Conflicts tab
The PR description states "add active state styling to conflicts tab in change request menu". However, the anchor tag for the new Conflicts tab lacks the conditional
4. 🟡 Incorrect and mismatching backgrounds for right-side code
In It is better to rely on
/* Right code content: match gutter #FFE7D1 */
.conflict-inner-table .add-code .lines-code-new {
- background: #ffe7d1 !important;
+ background: #e5e9ff !important;
}5. ⚪️
|
…licts via POST endpoint
…T helper and replace encoding/json with internal module
… in pull request logic
…pport for multiple parent commits and object alternates
…on via editor integration
…ution Two related issues: 1. The index entry was always written as "100644", so executable files (100755) lost their execute bit after conflict resolution. 2. A modify/delete conflict (head side deleted the file) caused a crash at GetBlobByPath. Now the missing head file is treated as empty content so the user's resolution (e.g. choosing the base version) can still be applied. Non-regular modes (symlinks, submodules) are rejected with a 400. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, binary or suppressed conflicted files rendered no conflict wrappers, leaving the submit button permanently disabled with no explanation. Now the Go handler detects IsBin/IsIncomplete early and sets a template variable. The template shows a clear warning message and hides the fold/submit controls so the page is not misleadingly interactive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* MergeNoCommitAllowConflicts: runs git merge --no-ff --no-commit, treats MERGE_HEAD + unmerged entries as expected conflict state * listUnmergedIndexEntries: parses ls-files -u -z into structured entries * resolveIndexPath: clears all unmerged stages via mode=0 --index-info, installs resolved blob at stage 0, verifies post-condition * submitConflictResolution: clones head, merges base, resolves against actual unmerged index, commits with both parents * adds TestSubmitConflictResolutionPreservesBaseOnlyChanges regression test * release conflict lock before sync recheck
* use temp-repo merge in ViewPullConflicts to derive true unmerged files * treat stored ConflictedFiles as advisory in submit prevalidation * cover stale false-positive conflict lists in the integration test * guard temporary repo cleanup when clone/open fails
* reject symlink, submodule, and other non-regular unmerged stages * require locked base/head commit ID validation for conflict submissions * add unit and integration coverage
* returns the no-conflicts error after auth and PR state gates. * preserves authorization behavior for anonymous, forbidden, poster, and admin cases.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
web_src/js/features/repo-conflict-review.ts:605
- The submit flow uses hard-coded UI/toast strings (e.g. “Submitting…”, “Submit failed: …”, “network error”). These should be localized (via
window.config.i18nor template-provided strings) to keep the conflicts flow consistent with the rest of the app’s i18n behavior.
setButtonsState(true, 'Submitting…');
try {
const resp = await POST(window.location.pathname, {data: {baseCommitID, headCommitID, files}});
if (resp.ok) {
window.location.href = issueLink;
} else {
let msg: string;
try {
msg = await resp.text();
} catch {
msg = `HTTP ${resp.status}`;
}
setButtonsState(false);
showErrorToast(`Submit failed: ${msg}`);
}
} catch {
setButtonsState(false);
showErrorToast('Submit failed: network error');
}
* add explicit deleteFile conflict-resolution intent * remove deleted-side paths from the merge index * cover modify/delete conflict deletion with integration test
* parse real merge-marker hunks instead of two-way diffs * preserve auto-merged content during conflict submission * add same-file auto-merge regression coverage
* limit conflict diff rendering to 3 context lines around true merge-marker hunks * merge overlapping/adjacent conflict windows into continuous sections * extend regression coverage to ensure clipped UI still preserves auto-merged content
* add Whitespace string field to conflictResolutionRequest with inline doc explaining why it is marker-independent but validated anyway * validate the echoed whitespace mode in SubmitConflictResolution; unknown values are normalised to "show-all" with a warning log * include data-whitespace in the POST payload Note: whitespace is the diff-whitespace mode that was active when the conflicts page was rendered. The frontend reads it from the page and echoes it back so the POST handler can verify it uses the same mode. Because conflict groups are derived from Git's merge-marker lines - which are whitespace-independent - the field does not affect the current parser, but it is validated and logged here so that any future whitespace-sensitive code path starts from a consistent value.
Advanced AI Review
Click to expand review1. 🔴 Modify/delete conflicts are resolved as empty files, not deletions
The conflict-resolution payload only sends resolved text for each group ( That means a modify/delete conflict cannot be resolved by deleting the file. Choosing the deleted side produces a zero-byte file in the merge commit, which is data corruption and is especially surprising because Git’s native resolution would remove the path from the index. Suggested direction: carry deletion intent through the UI/API and remove the path from the index instead of writing an empty blob when the selected resolution is deletion. For example: type resolvedFile struct {
path string
mode string
content []byte
delete bool
}
for _, rf := range resolvedFiles {
if rf.delete {
if err := t.RemoveFilesFromIndex(ctx, rf.path); err != nil {
ctx.ServerError("RemoveFilesFromIndex", err)
releaser()
return
}
continue
}
// existing hash + ResolveIndexPath path
}This also needs a test that creates a real modify/delete conflict and verifies that choosing the deleted side removes the file from the resulting merge commit rather than creating an empty blob. 2. 🟡 Non-conflicting hunks in conflicted files are treated as conflicts and can be dropped
The backend uses a real merge only to determine which files are unmerged ( For a file that has one true merge conflict plus another base-only change that Git would auto-merge cleanly, the base-only hunk is shown as another “conflict” and is included in the text-based replacement process. If the user clicks “Use this” for that hunk, the final blob is built from the original head content and the auto-mergeable base-side change is discarded ( Suggested direction: render and submit actual merge conflict hunks, not every base-vs-head diff hunk in an unmerged file. A safer approach is to use the post-merge working-tree file with conflict markers as the source of truth, expose only marker blocks as resolvable groups, and replace those marker blocks while preserving the rest of Git’s auto-merged content. Add a regression test where the same file contains one true conflict and one separate base-only edit; resolving the conflict should preserve the base-only edit without requiring the user to manually choose it. 3. 🟡 The final push bypasses pre-receive/post-receive hooks and branch protection
The permission check also grants an unconditional bypass to the PR poster or a site admin ( Suggested direction: push this branch update through the normal hook path, similar to existing pull-update merges, and use a push trigger where needed. If an internal push is absolutely required, duplicate the relevant pre-receive checks before pushing and explicitly enqueue the same post-receive side effects that are skipped. if err := t.PushWithOptions(ctx, ctx.Doer, mergeCommitID, pull.HeadBranch, false); err != nil {
// preserve existing out-of-date/rejected handling
}4. ⚪️ POST recomputes conflict indices with a different whitespace mode than GET
The GET route for For users whose conflict page was rendered with Suggested direction: either force the conflict page to always use |
|
Awesome! @pedrogaudencio could you please run one more test to see what happens in the following scenario:
What happens now? 2 conflicting PRs have been merged. Does UserB's PR handle this gracefully? Or is it broken? |
|
@taoeffect @pedrogaudencio : Looked into what happens when multiple conflicting PRs get merged before a pending PR is resolved. Here's the situation and some options for how we could improve it. The Scenario
How It's Handled Today The backend is solid. When the conflict page loads, it always runs a fresh git merge against the current base branch — so it naturally picks up all accumulated changes from both UserC and UserD at once. There's no "double conflict" bug; it all resolves into a single coherent conflict review. The problem is a race condition on submission:
Improvement Options Option A — Better messaging only (already done) Option B — sessionStorage draft Option C — Server-side draft Option D — Optimistic merge with conflict re-detection Happy to hear preferences — even just A+B would cover most real-world cases with minimal effort. |
Closes #153