Merge Conflict

Both sides of a conflict with a resolution per hunk. "Keep both" is offered because it is frequently right — two people adding a different import conflict textually and agree semantically.

src/components/ui/merge-conflict.tsx

About

From the component's own source, where it was written.

A merge conflict, with a resolution per hunk.

Both sides are shown in full, stacked, with the base labelled. Side-by-side looks tidier and is worse: conflict hunks are rarely the same height, so the columns desynchronise and you end up comparing line 4 against line 9.

"Both" is offered as a resolution because it is frequently the right answer — two people adding a different import to the same block conflict textually and agree semantically. A two-button ours/theirs forces a manual re-edit for the commonest case there is.

Nothing resolves itself. Unresolved hunks are counted in the header and the count does not go down until a choice is made, because a conflict UI that quietly defaults to one side is how code silently disappears in a merge.

Install

npx astralyx-ui add merge-conflict

Usage

import { MergeConflict } from '@/components/ui/merge-conflict'

<MergeConflict path={path} hunks={hunks} onResolve={resolve} />

Composer

Change the props and the source below follows.

src/components/ui/status-badge.tsx2 unresolved
  • import { cn } from '@/lib/utils'

    Ours

    import { Badge } from '@/components/ui/badge'

    Base

    Theirs

    import { Button } from '@/components/ui/button'
  • Ours

      const label = status === 'active' ? 'Live' : 'Paused'

    Base

      const label = status

    Theirs

      const label = STATUS[status].label
Props
tsx
<MergeConflict path={path} hunks={hunks} onResolve={resolve} />

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

MergeConflict

Extends Omit<ComponentProps<'div'>, 'children'>.

PropTypeDefaultDescription
pathReactNode——
hunksConflictHunk[]required{ id, ours, theirs, base?, context? }.
resolutionsRecord<string, ConflictSide>—Controlled or uncontrolled. Nothing resolves itself — a conflict UI that quietly defaults to one side is how code disappears in a merge.
onResolve(id: string, side: ConflictSide) => void——
oursLabelReactNode'Ours'—
theirsLabelReactNode'Theirs'—
baseLabelReactNode'Base'—
bothLabelReactNode'Keep both'—
unresolvedLabel(count: number) => ReactNodeDEFAULT_UNRESOLVED_LABEL—
resolvedLabelReactNode'All conflicts resolved'—

Types

ConflictHunk

FieldTypeDescription
idstring—
oursstring—
theirsstring—
base?stringCommon ancestor, from a diff3-style conflict.
context?stringUnchanged lines before the hunk, for orientation.
TypeDefinitionDescription
ConflictSide
'ours' | 'theirs' | 'both'
A merge conflict, with a resolution per hunk.

Behaviour

TopicDetailDescription
layoutstackedNot side by side. Conflict hunks are rarely the same height, so columns desynchronise and you compare line 4 against line 9.
unresolved countin the headerIt does not go down until a choice is made.