Stateless conflict resolution#14617
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a “stateless” conflict-resolution workflow that lets callers inspect a conflicted commit’s conflict hunks from its stored conflict trees and apply per-hunk resolutions (ours/theirs/custom content/AI) without entering edit mode, rewriting the commit and rebasing descendants under an oplog snapshot.
Changes:
- Introduces
commit_conflicts()andresolve_commit_conflict_hunks()inbut-api::resolve(plus schema/SDK types) and wires the new endpoints through Tauri + desktop. - Extends the legacy
but resolveCLI withconflictsandapplyfor per-hunk / per-file resolution without checkout/edit mode. - Updates desktop diff UX to show inline conflict cards and to surface synthetic diffs for conflict-only files hidden by auto-resolution.
Reviewed changes
Copilot reviewed 25 out of 27 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/but-sdk/src/generated/linear/index.d.ts | Adds generated SDK transport types for commit conflicts + per-hunk resolution results/specs. |
| packages/but-sdk/src/generated/graph/index.d.ts | Same generated SDK updates for graph SDK output. |
| crates/gitbutler-tauri/src/main.rs | Registers new Tauri commands for commit_conflicts and resolve_commit_conflict_hunks. |
| crates/gitbutler-oplog/src/entry.rs | Adds ResolveConflicts operation kind + display/serialization mappings. |
| crates/but/src/command/legacy/resolve.rs | Adds resolve conflicts listing and resolve apply per-hunk/per-file resolution flow. |
| crates/but/src/command/legacy/oplog.rs | Colors/prints the new ResolveConflicts operation in oplog output. |
| crates/but/src/args/resolve.rs | Adds Conflicts and Apply subcommands + CLI flag semantics. |
| crates/but/skill/SKILL.md | Updates resolution guidance to prefer stateless per-hunk resolution workflow. |
| crates/but-api/tests/fixtures/scenario/resolve-ai-conflicted-commit.sh | Expands fixture to produce two separated conflict hunks + new-base parent setup. |
| crates/but-api/tests/api/resolve_hunks.rs | New tests covering commit_conflicts + partial/full per-hunk apply + oplog snapshots. |
| crates/but-api/tests/api/resolve_ai.rs | Updates AI resolution tests to handle multiple hunks and new prompt structs. |
| crates/but-api/tests/api/main.rs | Registers the new resolve-hunks test module. |
| crates/but-api/src/resolve/prompt.rs | Renames AI output hunk struct (HunkResolution → HunkContent) for clarity. |
| crates/but-api/src/resolve/mod.rs | Implements commit_conflicts + per-hunk resolution API + shared AI validation/retry. |
| crates/but-api/src/resolve/context.rs | Extends extracted hunks with an auto-resolution line anchor + exports schema. |
| crates/but-api/src/resolve/apply.rs | Reworks apply pipeline to “narrow” conflict trees and re-merge, supporting partial resolution. |
| apps/desktop/src/lib/stacks/stackService.svelte.ts | Adds client helpers for commitConflicts query + per-hunk resolve mutation. |
| apps/desktop/src/lib/stacks/stackEndpoints.ts | Defines the new backend endpoints and caching/invalidations. |
| apps/desktop/src/lib/stacks/stackController.svelte.ts | Adds diff jumping by path and a safe maybeGetStackContext(). |
| apps/desktop/src/components/views/StackDetails.svelte | Fetches commit conflicts and passes them into MultiDiffView for inline rendering. |
| apps/desktop/src/components/views/BranchCommitList.svelte | Stops hiding conflicted files from “Changed files” list; wires conflicted-file click to diff jump. |
| apps/desktop/src/components/files/FileListConflicts.svelte | Adds optional click override to open diff instead of prompting edit-mode entry. |
| apps/desktop/src/components/files/ChangedFilesPanel.svelte | Plumbs onConflictedFileClick through to FileListConflicts. |
| apps/desktop/src/components/diff/UnifiedDiffView.svelte | Renders inline conflict cards alongside normal diff hunks with toggle + bucketing by line anchor. |
| apps/desktop/src/components/diff/SelectionView.svelte | Fetches commit conflicts for selected commit file and passes hunks to the unified diff view. |
| apps/desktop/src/components/diff/MultiDiffView.svelte | Appends synthetic conflict-only changes, supports jump-to-path, and passes per-file conflict hunks down. |
| apps/desktop/src/components/diff/ConflictHunkCard.svelte | New UI component to view/resolve a single conflict hunk (ours/theirs/edit/AI) and follow rewritten commit IDs. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38df7c43c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two new APIs on the conflict primitives: but_api::resolve::commit_conflicts returns a conflicted commit's conflicts as per-file ours/base/theirs hunks, and resolve_commit_conflict_hunks applies side picks or custom content to any subset of them. Partial resolutions narrow the conflict: each resolved hunk is written into all three side trees and the re-merge conflicts only at what remains, so the commit stays conflicted with fewer conflicts and work can proceed incrementally. Full coverage produces a normal commit via the same path, and the AI resolver now runs on this core. Exposed as `but resolve conflicts` and `but resolve apply <path>[:<hunk>]` (side flags or stdin/file content, defaulting to the oldest conflicted commit), as a read-only conflicts modal on right-click in the desktop app. The agent skill now steers coding agents to this stateless flow, with edit mode reserved for when the commit must be checked out.
Clicking a conflicted commit now renders a real diff for every conflicted file. commit_conflicts returns a synthetic TreeChange per file diffing the current base's version (ours) against the commit's own (theirs) — regular commit diffs are computed against the auto-resolution, which hides exactly the conflicted content. The diff view marks such files with a banner and a Show/Hide toggle that interleaves ours/base/theirs conflict cards at the right position via a new ConflictHunk.line anchor. The file list no longer filters conflicted files, and clicking an unrepresented one jumps to its entry in the diff pane. The right-click 'Show conflicts' modal is superseded and removed.
38df7c4 to
c629c58
Compare
c629c58 to
1b94f32
Compare
1b94f32 to
c6b1b24
Compare
Each inline conflict card now offers Use current base, Use this commit, Edit (a prefilled textarea for a mixed resolution), and Resolve with AI. Every action applies immediately through resolve_commit_conflict_hunks and narrows the conflict; the selection follows the rewritten commit so the remaining conflicts stay in view until none are left. Per-conflict AI is a new HunkResolution::Ai variant on the same API: AI specs are sent to the model as a request narrowed to just those hunks and applied like content resolutions, so sides, content, and AI can mix in one call and AI configuration is only required when actually used. Also exposed as 'but resolve apply <path>:<N> --ai'.
c6b1b24 to
d1c0f7d
Compare
Resolve a conflicted commit's conflicts without ever entering edit mode. Everything works from the committed conflict trees in memory, so agents and the UI can inspect and resolve conflicts one at a time without checking anything out.
API (
but-api::resolve)commit_conflicts(commit)— read-only: returns each conflicted file's ours/base/theirs hunks, plus a syntheticTreeChange(the base's version vs the commit's own) so a fully-conflicted file still renders a real diff. Returns no files for a non-conflicted commit rather than erroring, so callers can race a just-resolved commit safely.resolve_commit_conflict_hunks(commit, specs)— applyOurs/Theirs/Content/Aito any subset of hunks. Resolving a subset narrows the conflict and leaves the commit conflicted with the rest; resolving all of them normalizes it. Each apply rewrites the commit and rebases descendants under one oplog snapshot, so a bad resolution is onebut undoaway.Resolutions are applied by narrowing: the resolved content goes into the ours and theirs side trees only (the base is left untouched, so a later re-pick replays the resolution instead of silently dropping it), then a force-ours re-merge produces the auto-resolution — never marker text in a tree. Per-hunk AI (
HunkResolution::Ai) sends only the addressed hunks to the model and splices the result back likeContent.CLI
but resolve conflicts [<commit-or-branch>]— list the conflicts of a conflicted commit.but resolve apply <path>[:<N>] [--ours|--theirs|--ai|--file <f>|stdin] [--commit <commit-or-branch>]— resolve one conflict (or a whole file with a side). Targets are polymorphic: a branch means its oldest conflicted commit, which stays a stable address across the id churn every apply causes.Desktop
Clicking a conflicted commit now shows a normal diff for every file. Files whose conflict is otherwise hidden by the auto-resolution get their real base-vs-commit diff, with a banner that toggles inline ours/base/theirs conflict cards interleaved at the right position. Each card can resolve that one conflict — use current base, use this commit, edit the merged text, or resolve with AI — and the view follows the rewritten commit so the remaining conflicts stay in front of you until none are left. The old right-click "Show conflicts" modal is removed in favor of the inline view.