Repository navigation
Merge conflict resolution writes raw |||||||| diff3 base-section markers into the file when git conflictStyle is diff3/zdiff3 #6014
Description
Current Behavior
If git is configured with merge.conflictStyle = diff3 (or zdiff3), resolving a merge conflict on a non-flow file (e.g. package.json, README.md) via the "resolve conflicts" diff view leaves the raw ||||||| <sha> base-section marker plus the base-version lines in the resolved file. The file is then staged and committed in that corrupted state. For package.json this breaks the project (invalid JSON).
With the default (merge) conflict style the same steps produce a clean file, so this only shows up when diff3/zdiff3 is configured (either globally or per-repo).
Steps to Reproduce
git config --global merge.conflictStyle diff3(works the same withzdiff3)- Create a Node-RED project whose
package.json(or any tracked non-flow file) is modified differently on two branches; pull/merge so a content conflict occurs. - In the version-control sidebar, open the unmerged file → "resolve conflicts" diff view.
- Click use local changes on the conflict, then save conflict resolution.
- Inspect the file on disk / the staged version.
Expected Behavior
The resolved file should contain only the chosen side of the conflict, with no conflict markers.
Actual Behavior
The resolved file contains the raw base marker and the base section, e.g. for a conflict between MAIN and SIDE on a base line line2, choosing local produces:
line1
MAIN
||||||| 44d5626
line2
line3
||||||| 44d5626 and line2 (the base side) have been written into the file. Choosing use remote changes happens to work, because the base section sits before the ======= separator and is skipped by the ignore block.
Analysis
The diff view only recognizes three conflict marker lines. packages/node_modules/@node-red/editor-client/src/js/ui/diff.js:1971:
var isMergeHeader = isConflict && /^\+\+(<<<<<<<|=======$|>>>>>>>)/.test(lineText);There is no alternative for |||||||, so with diff3/zdiff3 the ||||||| <sha> line and the base-section lines are treated as ordinary "ours-side" content lines in the conflict view (they are rendered as regular additions and are included in the rows kept when clicking "use local changes", which builds the resolution entry at diff.js:2066).
Server-side, Project.prototype.resolveMerge (packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js:701-730) then applies that resolution line-by-line: for selection === "A" it keeps every line from changeStart until separator (=======), which in diff3 includes the ||||||| marker line and the base section, and writes them into the file before staging it.
Repro trace
I extracted the relevant logic verbatim (parseUnifiedDiff + the conflict-resolution construction from diff.js, and the resolveMerge filter loop from Project.js) and ran it against the real output of git diff -w -- f.txt for a conflict created with merge.conflictStyle=diff3:
- working file (as written by git):
line1 <<<<<<< HEAD MAIN ||||||| 44d5626 line2 ======= SIDE >>>>>>> side line3 - resolution computed by the editor for "use local changes":
{changeStart: 2, separator: 6, changeEnd: 8, selection: "A"} - file written by resolveMerge:
line1 / MAIN / ||||||| 44d5626 / line2 / line3← corrupted - same conflict re-created with the default
mergestyle resolves cleanly toline1 / MAIN / line3.
So the marker detection and the resolveMerge loop both need to handle the optional base section, e.g. treating ||||||| as an additional merge-header boundary (and ending the "ours" region there for selection === "A").
Your Environment
- Version: current
dev/master (7f8d1ac) - Node.js version: 26
- npm version: n/a
- Platform/OS: Linux/arm64
- Browser: any
Activity
knolleary commentedon Oct 17, 2026
Hi @rahul05ranjan
You have raised a lot of quite obscure issues, many of which users are very unlikely to hit.
Can you explain what you're doing to hit so many issues like this?
Given how verbose, obscure and lengthy all of the Issues are, my guess is you are just fishing for issues with AI. Are any of these issues ones you have actually hit through normal usage?
Is it your intention to open PRs with proposed fixes for each of them?
rahul05ranjan commentedon Oct 17, 2026
Hi @knolleary — fair question, and I'll answer it directly.
No, these weren't stumbled upon during normal day-to-day usage. I've been doing a deliberate, systematic test pass over Node-RED's projects/version-control features — reading the source, constructing minimal repros for suspicious paths, and only filing an issue when the repro fails from a clean setup. So the volume comes from intentional testing, but every report I've opened has an actually-executed repro behind it; none are speculative "might be a bug" filings.
If you consider any of them invalid, out of scope, or not worth the maintainers' time, closing them is completely fine with me.
On PRs: I don't intend to open drive-by PRs for each one. If you can tell me which of these you'd consider valid and worth fixing, I can start with those — I'd rank #6014 first, since users with
diff3/zdiff3configured get a silently corruptedpackage.jsoncommitted from the conflict-resolution UI.knolleary commentedon Oct 17, 2026
I'm going to be honest. It feels like I'm talking to an AI agent.
You have raised a lot of issues, with suggested fixes, for scenarios that are edge cases, unlikely to be hit, not actually seen - regardless of how valid they are. You have suddenly put a lot of burden on the project maintainers to read and evaluate each issue. To have proposed fixes, but not raised a pr to match puts even more burden on the project maintainers. Please consider your approach here.
rahul05ranjan commentedon Oct 18, 2026
Hi @knolleary — you're right, and I appreciate you saying it directly rather than just closing things silently.
To be fully honest: yes, the testing pass and the report write-ups are AI-assisted. The repros behind them are real and were actually executed, but the volume, the verbosity and the edge-case focus create exactly the maintainer burden you're describing — regardless of how valid each individual report is. That's on me, and I'm sorry for the load it has put on you.
Here's what I'll do:
Thanks for engaging with each report properly despite the noise — that effort was real, and I should have made your life easier, not harder.
hlovdal commentedon Oct 23, 2026
That is horrible fragile and almost doomed to fail. The real way to detect if a file has "both modified" status or similar is not to parse the output from
git statusbut to rungit ls-files --unmerged. That will give the real (duplicated) list of unmerged files.This is what I use in my
git resolve-conflict-using-kdiff3tool (which I tend to use multiple times a day).rahul05ranjan commentedon Oct 24, 2026
@hlovdal Fair point — you're right. Parsing the text of
git statusoutput for those markers is fragile (filenames can contain those sequences, and the porcelain format has quoting/edge cases the regex can't see).git ls-files --unmergedis the authoritative way to get the unmerged paths, since it reads them straight from the index rather than a rendering of it. Thank you for the correction — and for the pointer togit-resolve-conflict-using-kdiff3, it's a nice reference implementation of the pattern.