Skip to content

Add UI-based merge conflict resolution#166

Draft
pedrogaudencio wants to merge 7 commits intomasterfrom
change-request-conflict-resolution
Draft

Add UI-based merge conflict resolution#166
pedrogaudencio wants to merge 7 commits intomasterfrom
change-request-conflict-resolution

Conversation

@pedrogaudencio
Copy link
Copy Markdown
Collaborator

  • add conflicts tab to change request menu/view
  • add active state styling to conflicts tab in change request menu
  • create conflicts template for change request view
  • use base branch commit instead of merge base for change request conflict resolution
  • add resolve box/selection for each conflict
  • add submit flow to commit changes
  • UI tweaks
  • add tests

Closes #153

@pieer
Copy link
Copy Markdown
Collaborator

pieer commented Apr 6, 2026

/crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}}


AI review started.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

Advanced AI Review

  • Type: Agentic (crush)
  • Model: google/gemini-3.1-pro-preview
Click to expand review

1. 🔴 Conflict wrappers are appended to the bottom of the table, breaking file order

  • Addressed
  • Dismissed

In web_src/js/features/repo-conflict-review.ts, you correctly capture the container's parent node to insert the wrapper into the table. However, since the script moves all group rows inside innerTbody.append(row) before inserting the wrapperRow, the firstRow becomes detached from parentNode. Consequently, parentNode.insertBefore(wrapperRow, null) acts purely like appendChild and pushes all conflicts to the very bottom of the document tree, scrambling the file's visual diff sequence.

To fix this, insert the wrapperRow into the DOM before the inner loop detaches the conflict rows.

web_src/js/features/repo-conflict-review.ts:69-79

    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:
web_src/js/features/repo-conflict-review.ts:181-183

-    // 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

  • Addressed
  • Dismissed

In custom/templates/repo/diff/conflicts_section_split.tmpl, the condition {{if or (ne .GetType 2) (not $hasmatch)}} strictly scopes conflicts to DEL rows that have an exact, matching ADD counterpart. If an author writes a completely new paragraph (.GetType == 2) or removes a chunk without replacing it (.GetType == 3 without a match), the code delegates those changes to the {{else}} block where they are hardcoded as data-line-type="context" and CSS class same-code.

Because your JS gathers conflicts based on data-line-type="conflict", these changes will bypass the conflict resolver interface entirely and be visually folded/hidden by the "Fold" button, enabling users to silently weave unresolved additions and deletions into the codebase.

Separate actual identical context lines (.GetType == 1) from single-side changes (.GetType == 2 or 3).

custom/templates/repo/diff/conflicts_section_split.tmpl:56-80

			{{else if eq .GetType 1}}
				{{/* Context line (same on both sides) */}}
				{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
				<tr class="context-line same-code nl-{{$k}} ol-{{$k}}" data-line-type="context">
					<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span>{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}</span></td>
					<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
					<td class="lines-code lines-code-old">
						{{- if $line.LeftIdx -}}
							{{- template "repo/diff/section_code" dict "diff" $inlineDiff -}}
						{{- else -}}
							<code class="code-inner"></code>
						{{- end -}}
					</td>
					<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span>{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span></td>
					<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
					<td class="lines-code lines-code-new">
						{{- if $line.RightIdx -}}
							{{- template "repo/diff/section_code" dict "diff" $inlineDiff -}}
						{{- else -}}
							<code class="code-inner"></code>
						{{- end -}}
					</td>
				</tr>
			{{else}}
				{{/* Single-side change (unmatched DEL or unmatched ADD) */}}
				{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
				<tr class="conflict-line {{if eq .GetType 2}}add-code{{else}}del-code{{end}} nl-{{$k}} ol-{{$k}}" data-line-type="conflict">
					<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span>{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}</span></td>
					<td class="lines-type-marker lines-type-marker-old {{if eq .GetType 3}}del-code{{end}}">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
					<td class="lines-code lines-code-old {{if eq .GetType 3}}del-code{{end}}">
						{{- if $line.LeftIdx -}}
							{{- template "repo/diff/section_code" dict "diff" $inlineDiff -}}
						{{- else -}}
							<code class="code-inner"></code>
						{{- end -}}
					</td>
					<td class="lines-num lines-num-new {{if eq .GetType 2}}add-code{{end}}" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span>{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}</span></td>
					<td class="lines-type-marker lines-type-marker-new {{if eq .GetType 2}}add-code{{end}}">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
					<td class="lines-code lines-code-new {{if eq .GetType 2}}add-code{{end}}">
						{{- if $line.RightIdx -}}
							{{- template "repo/diff/section_code" dict "diff" $inlineDiff -}}
						{{- else -}}
							<code class="code-inner"></code>
						{{- end -}}
					</td>
				</tr>
			{{end}}

3. 🟡 Active state styling is missing on the Conflicts tab

  • Addressed
  • Dismissed

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 {{if .PageIsPullConflicts}}active{{end}} check that the other tabs use.

custom/templates/repo/pulls/tab_menu.tmpl:7-11

        {{if .IsPullFilesConflicted}}
        <a class="item {{if .PageIsPullConflicts}}active{{end}}" href="{{.Issue.Link}}/conflicts">
            {{svg "octicon-git-merge"}}
            {{template "shared/misc/tabtitle" (ctx.Locale.Tr "repo.pulls.tab_conflicts")}}
        </a>
        {{end}}

4. 🟡 Incorrect and mismatching backgrounds for right-side code

  • Addressed
  • Dismissed

In custom/public/assets/css/repo.custom.css, there are hardcoded CSS overrides. Currently, the color for .lines-code-new (intended as the right-side addition block) uses #ffe7d1 (a light orange tint) rather than the intended #e5e9ff blue color used accurately in the gutter directly above it. Using hardcoded colors also actively breaks readability when standard Gitea dark mode is invoked.

It is better to rely on var(--color-diff-added-row-bg) and var(--color-diff-removed-row-bg), but if you need strictly overriding the hardcoded values:

custom/public/assets/css/repo.custom.css:262-264

 /* Right code content: match gutter #FFE7D1 */
 .conflict-inner-table .add-code .lines-code-new {
-  background: #ffe7d1 !important;
+  background: #e5e9ff !important;
 }

5. ⚪️ <template> elements should be strictly cloned via .content instead of .innerHTML

  • Addressed
  • Dismissed

In web_src/js/features/repo-conflict-review.ts, parsing the inner elements of a standard <template> tag using .innerHTML can be technically unpredictable in specific browser edge cases. Best practices call for structurally cloning the nodes context to preserve strict fragment functionality (essential for isolated component scripts).

web_src/js/features/repo-conflict-review.ts:153-156

    if (editorTemplate) {
      // Clone the editor template content
      const editorContent = document.createElement('div');
      editorContent.className = 'conflict-comment-form';
      editorContent.appendChild(editorTemplate.content.cloneNode(true));

6. ⚪️ Consider allowing users to un-resolve or change their selection

  • Addressed
  • Dismissed

Currently, clicking the "Resolve" button permanently forces keepBtn.disabled = true; and useBtn.disabled = true; leaving users stranded with no way to revert or toggle resolving states without doing a hard page refresh. Implementing a basic "undo" flow or a simple toggle structure for the resolve mechanism would improve UX usability.


Review generated using google/gemini-3.1-pro-preview via OpenRouter. Comment /crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}} to re-run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add UI-based merge conflict resolution

2 participants