Skip to content

feat(source-control): ✨ Add git merge conflict detection and diff viewing - #101

Merged
jorben merged 4 commits into
masterfrom
feat/confilict-diff-support
Apr 21, 2026
Merged

feat(source-control): ✨ Add git merge conflict detection and diff viewing#101
jorben merged 4 commits into
masterfrom
feat/confilict-diff-support

Conversation

@jorben

@jorben jorben commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Detect merge conflicts via git2 index conflict iterator and expose them as a dedicated conflictedFiles list in GitSnapshotDto
  • Add a "Conflicts" section in the Git panel UI with warning styling and C badge
  • Support viewing conflict file diffs by reading workdir content (with conflict markers) as all-new content
  • Deduplicate conflicted files from staged/unstaged/untracked lists so they only appear in the Conflicts section
  • Add isConflicted to GitFileStatusDto and conflicted to GitFileState for file tree overlay

Changes

Backend (Rust / git2)

  • git_manager.rs: Add conflicted_files to SnapshotParts, collect_conflicted_files() using index.conflicts() iterator, and get_conflict_diff() using build_added_file_fallback_hunks
  • git_manager.rs: Filter conflicted paths from staged/unstaged/untracked lists via HashSet dedup
  • git_manager.rs: Add Conflicted to GitFileState priority mapping and map_overlay_status
  • git.rs (model): Add conflicted_files to GitSnapshotDto, is_conflicted to GitFileStatusDto, Conflicted variant to GitFileState
  • git.rs (commands): Expose git_get_conflict_diff Tauri command
  • lib.rs: Register git_get_conflict_diff command
  • index_manager.rs: Add Conflicted state priority

Frontend (React / TypeScript)

  • api.ts: Add conflictedFiles to GitSnapshotDto, isConflicted to GitFileStatusDto, "conflicted" to GitFileState
  • source-control-panels.tsx: Add ConflictGroup component with warning styling; route conflict file clicks to gitGetConflictDiff; include isConflict flag in GitDiffSelection
  • branch-selector.tsx / dashboard-workbench.tsx: Wire conflictedFiles through props
  • project-panel.tsx: Display C badge for conflicted files in tree view
  • git-commands.ts: Add gitGetConflictDiff IPC bridge
  • i18n: Add "sourceControl.conflicts" / "sourceControl.statusConflict" keys (en + zh-CN)

Test Plan

  • cargo check passes with zero errors/warnings
  • tsc --noEmit passes with zero errors
  • Manual: create a merge conflict in a test repo and verify Conflicts section appears
  • Manual: click a conflicted file and verify diff preview shows conflict markers
  • Manual: verify conflicted files do NOT appear in staged/unstaged/untracked sections
  • Manual: verify C badge shows in file tree for conflicted paths

🤖 Generated with TiyCode

Add comprehensive support for detecting and displaying git merge/rebase
conflicts in the source control panel:

- Separate conflicted files from staged/unstaged/untracked lists into
  dedicated conflicted_files field in Git snapshot
- Add GitFileState.Conflicted variant and is_conflicted status flag to
  track file conflict state
- Create ConflictGroup component to display conflicting files with "C"
  badge indicator in warning color
- Implement git_get_conflict_diff command to view conflict markers
  (<<<<<<<, =======, >>>>>>>) in diff preview
- Add i18n keys for "Conflicts" section label in English and Chinese

Users can now easily identify files with merge conflicts and view
the full conflict content in the diff preview panel.
@github-actions

github-actions Bot commented Apr 21, 2026

Copy link
Copy Markdown

AI Code Review Summary

PR: #101 (feat(source-control): ✨ Add git merge conflict detection and diff viewing)
Preferred language: English

Overall Assessment

Detected 5 actionable findings, prioritize CRITICAL/HIGH before merge.

Major Findings by Severity

  • HIGH (3)
    • src-tauri/src/commands/git.rs:234 - No integration test for new git_get_conflict_diff command
    • src-tauri/src/core/git_manager.rs:779 - No unit tests for collect_conflicted_files function
    • src/modules/workbench-shell/ui/source-control-panels.tsx:1660 - conflictedFiles accessed without optional chaining — potential runtime crash
  • MEDIUM (2)
    • src-tauri/src/core/git_manager.rs:303 - get_conflict_diff missing test coverage for edge cases
    • src/modules/workbench-shell/ui/source-control-panels.tsx:1660 - Missing optional chaining on snapshot.conflictedFiles in ConflictGroup render

Actionable Suggestions

  • Add a Rust integration test (e.g., m1_6_git_conflicts.rs) that creates a merge conflict, verifies collect_conflicted_files detects it, and confirms deduplication from staged/unstaged lists works correctly
  • Consider adding a guard or documentation to get_conflict_diff clarifying that the caller must ensure the file is actually in a conflicted state
  • Run cargo fmt before committing as per project guidelines
  • Add an integration test in src-tauri/tests/ for the git_get_conflict_diff command using the milestone naming pattern (e.g., m1_6_conflict_diff.rs)
  • Add Rust unit tests for collect_conflicted_files covering: no conflicts, normal conflicts, malformed entries, entries with no path, and paths outside workspace root
  • Add tests for the conflict deduplication logic at lines R531-R546 to verify conflicted files are removed from staged/unstaged/untracked and don't appear twice
  • Add unit tests for map_overlay_status verifying the new Conflicted-before-Ignored priority, especially for files that are both conflicted and ignored
  • Add edge case tests for get_conflict_diff: binary conflict files, missing workdir files, empty conflict files, and large files with truncation
  • Fix the null-safety issue at R1660 by using optional chaining: (snapshot?.conflictedFiles?.length ?? 0) > 0.
  • Add unit tests for the gitGetConflictDiff bridge command.
  • Add component tests for ConflictGroup rendering with empty and populated conflictedFiles.
  • Verify the Rust backend git_get_conflict_diff command is implemented and matches the expected GitDiffDto return type.

Potential Risks

  • If there are other match statements on GitFileState not shown in the diff, adding the Conflicted variant could cause compile errors (though this would be caught by cargo check)
  • Cross-platform path separator differences could affect deduplication reliability on Windows
  • Conflicted files could appear in both the Conflicts section and Staged/Untracked sections if path normalization differs between collection functions
  • Silent data loss in collect_conflicted_files when conflict entries have no ancestor/ours/theirs entries — these are skipped without logging at R826-R828
  • Binary conflict files may produce unexpected diff output (is_binary:true with empty hunks) that the frontend may not handle correctly
  • Files that are both conflicted and ignored will now show as Conflicted instead of Ignored, changing existing user-facing behavior
  • String::from_utf8_lossy at R830 may produce paths that don't match the paths from other collection functions, breaking deduplication
  • Runtime crash if snapshot is null when the ConflictGroup section evaluates snapshot.conflictedFiles.length at R1660.
  • If the Rust backend does not yet implement git_get_conflict_diff, clicking a conflicted file will produce an unhandled error.
  • Conflicted files that are also staged or unstaged may appear in both the ConflictGroup and ChangeGroup if the backend populates both arrays for the same file.
  • Runtime TypeError when snapshot.conflictedFiles is undefined and accessed without optional chaining
  • Backend Tauri command parameter name mismatch going undetected without bridge tests

Test Suggestions

  • Integration test: create repo with merge conflict, call git_get_snapshot, verify conflicted_files is populated and file does not appear in staged/unstaged/untracked
  • Integration test: call git_get_conflict_diff on a conflicted file and verify hunks contain conflict markers
  • Unit test: collect_conflicted_files with empty index (no conflicts) returns empty vec
  • Unit test: collect_conflicted_files with conflict entries where only ancestor path exists
  • Integration test: create a Git repo with a merge conflict, call git_get_conflict_diff, verify GitDiffDto has status Unmerged and hunks contain conflict markers
  • Unit test: collect_conflicted_files with a repo that has no conflicts returns empty Vec
  • Unit test: collect_conflicted_files with a repo containing conflicts returns correct paths with GitChangeKind::Unmerged
  • Unit test: deduplication logic correctly removes conflicted file paths from staged/unstaged/untracked lists
  • Unit test: map_overlay_status returns Conflicted for a status where is_conflicted() is true, even if is_ignored() is also true
  • Unit test: get_conflict_diff with a binary conflict file returns is_binary:true and empty hunks
  • Unit test: state_priority gives Conflicted (5) higher priority than all other states
  • Integration test: git_get_conflict_diff with invalid workspace_id returns appropriate error

File-Level Coverage Notes

  • src-tauri/src/core/git_manager.rs: high_risk (This file carries the bulk of new logic with 139 additions and zero test additions. The project guidelines explicitly require backend integration tests for command and runtime behavior changes.)
  • src-tauri/src/commands/git.rs: high_risk (The command is a thin wrapper but still needs integration test coverage per project guidelines.)
  • src-tauri/src/model/git.rs: low_risk (Model changes are straightforward but the new enum variant could cause compile errors in exhaustive matches elsewhere. The cargo build/typecheck should catch those, but deserialization of stored/transported data with the new variant should be verified.)
  • src-tauri/src/core/index_manager.rs: low_risk (Minimal change mirroring the same priority addition in git_manager.rs. Low risk but should be covered by the same overlay priority tests.)
  • src-tauri/src/lib.rs: ok (Single-line addition registering the new command. No testing risk beyond ensuring the command registration is correct, which integration tests would cover.)
  • src/services/bridge/git-commands.ts: New gitGetConflictDiff function lacks test coverage. Low complexity but parameter forwarding should be verified. (Thin wrapper; main risk is parameter name divergence with Rust side.)
  • src/shared/types/api.ts: Type additions for conflicted state are straightforward but conflictedFiles should be optional for backwards compatibility. (Consider making conflictedFiles optional to match runtime reality of older backend versions.)
  • src/modules/workbench-shell/ui/source-control-panels.tsx: Largest change; adds ConflictGroup component and conflict diff preview branch. Inconsistent null-safety and no test coverage are the main concerns. (The inconsistent optional chaining (snapshot?.conflictedFiles at R1062 vs snapshot.conflictedFiles at R1660) is the highest-severity testing concern.)
  • src/modules/workbench-shell/ui/branch-selector.tsx: Type Pick updated to include conflictedFiles. Low risk, follows existing pattern. (Straightforward type extension.)
  • src/modules/workbench-shell/ui/dashboard-workbench.tsx: Memo correctly updated with conflictedFiles in both value and dependency array. Low risk. (Follows existing pattern for other snapshot fields.)
  • src/modules/workbench-shell/ui/project-panel.tsx: Added conflicted state to priority map and badge label. Logic is correct but untested. (The strongestGitState function body wasn't in the diff; verify it iterates GIT_STATE_PRIORITY correctly for the new key.)
  • src/i18n/locales/en.ts: Added sourceControl.conflicts translation key. No testing risk. (Straightforward i18n addition.)
  • src/i18n/locales/zh-CN.ts: Added sourceControl.conflicts translation key. No testing risk. (Straightforward i18n addition.)

Inline Downgraded Items (processed but not inline)

  • None

Coverage Status

  • Target files: 13
  • Covered files: 13
  • Uncovered files: 0
  • No-patch/binary covered as file-level: 0
  • Findings with unknown confidence (N/A): 0

Uncovered list:

  • None

No-patch covered list:

  • None

Runtime/Budget

  • Rounds used: 1/4
  • Planned batches: 2
  • Executed batches: 2
  • Sub-agent runs: 4
  • Planner calls: 1
  • Reviewer calls: 5
  • Model calls: 6/64
  • Structured-output summary-only degradation: NO

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 9
  • Findings with unknown confidence: 1
  • Inline comments attempted: 9
  • Target files: 13
  • Covered files: 13
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

}

#[tauri::command]
pub async fn git_get_conflict_diff(

This comment was marked as outdated.

Ok(files)
}

fn collect_conflicted_files(

This comment was marked as outdated.


void Promise.all([
gitGetDiff(workspaceId, selection.path, selection.staged),
selection.isConflict

This comment was marked as outdated.

Comment thread src-tauri/src/core/git_manager.rs Outdated
while let Some(conflict) = conflict_iter.next() {
let conflict = match conflict {
Ok(c) => c,
Err(_) => continue,

This comment was marked as outdated.

let staged_files = collect_staged_files(&repo, &repo_root, workspace_root)?;
let unstaged_files = collect_unstaged_files(&repo, &repo_root, workspace_root)?;
let untracked_files = collect_untracked_files(&repo, &repo_root, workspace_root)?;
let conflicted_files = collect_conflicted_files(&repo, &repo_root, workspace_root)?;

This comment was marked as outdated.

);
}

function ConflictGroup({

This comment was marked as outdated.

: null,
fileStatus.isUntracked ? "untracked" : null,
fileStatus.isIgnored ? "ignored" : null,
fileStatus.isConflicted ? "conflict" : null,

This comment was marked as outdated.

});
}

export async function gitGetConflictDiff(

This comment was marked as outdated.

// conflict markers) and display it as all-new content.
// This gives users a clear view of the current state of the file
// including conflict markers (<<<<<<<, =======, >>>>>>>).
let hunks = build_added_file_fallback_hunks(&workspace_root, &workspace_relative);

This comment was marked as outdated.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 4
  • Findings with unknown confidence: 0
  • Inline comments attempted: 4
  • Target files: 13
  • Covered files: 6
  • Uncovered files: 7
    See the summary comment for detailed analysis and coverage details.

);
}

function ConflictGroup({

This comment was marked as outdated.


void Promise.all([
gitGetDiff(workspaceId, selection.path, selection.staged),
selection.isConflict

This comment was marked as outdated.

tracked: 2,
modified: 3,
untracked: 4,
conflicted: 5,

This comment was marked as outdated.

const isUntracked = node.gitState === "untracked";
const badgeLabel = isUntracked ? "U" : isModified ? "M" : null;
const isConflicted = node.gitState === "conflicted";
const badgeLabel = isConflicted ? "C" : isUntracked ? "U" : isModified ? "M" : null;

This comment was marked as outdated.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 2
  • Findings with unknown confidence: 0
  • Inline comments attempted: 2
  • Target files: 13
  • Covered files: 13
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

Ok(files)
}

fn collect_conflicted_files(

This comment was marked as outdated.

Comment thread src-tauri/src/core/git_manager.rs Outdated
while let Some(conflict) = conflict_iter.next() {
let conflict = match conflict {
Ok(c) => c,
Err(_) => continue,

This comment was marked as outdated.

… warning log and fix "conflicted" string mismatch

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 5
  • Findings with unknown confidence: 0
  • Inline comments attempted: 5
  • Target files: 13
  • Covered files: 13
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

.await
}

#[tauri::command]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] No integration test for new git_get_conflict_diff command

The new git_get_conflict_diff Tauri command has no integration test coverage, violating the project's testing guidelines for command behavior changes.

Suggestion: Add an integration test in src-tauri/tests/ (following the milestone naming pattern) that sets up a repo with a merge conflict and verifies the command returns the expected GitDiffDto with Unmerged status and conflict-marker hunks.

Risk: Regression in conflict diff rendering could go undetected; the command could fail silently or return malformed data without any automated safety net.

Confidence: 0.95

[From SubAgent: testing]

Ok(files)
}

fn collect_conflicted_files(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] No unit tests for collect_conflicted_files function

The collect_conflicted_files function has multiple branching paths and error-handling logic that are untested, including malformed entries and path resolution failures.

Suggestion: Add Rust unit tests covering: (1) repo with no conflicts returns empty vec, (2) repo with conflicts returns correct paths with Unmerged status, (3) malformed conflict entries are skipped with warning, (4) entries with no ancestor/ours/theirs are skipped, (5) paths outside workspace root are filtered out.

Risk: Silent data loss if conflict entries are unexpectedly skipped due to path resolution or encoding issues; users may not see all conflicted files.

Confidence: 0.92

[From SubAgent: testing]

onToggleStage={handleToggleStage}
onToggleAll={handleToggleAll}
/>
{snapshot.conflictedFiles.length > 0 && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] conflictedFiles accessed without optional chaining — potential runtime crash

snapshot.conflictedFiles is accessed without optional chaining at R1660 and R1663, while R1062 safely uses snapshot?.conflictedFiles. If the backend omits the field, the direct access will crash.

Suggestion: Use snapshot?.conflictedFiles at R1660-R1663 or provide a fallback default (e.g., snapshot.conflictedFiles?.length ?? 0 > 0). Add a unit test that renders GitPanel with a snapshot missing conflictedFiles to verify backwards compatibility.

Risk: Runtime TypeError when receiving a GitSnapshotDto that lacks the conflictedFiles property.

Confidence: 0.88

[From SubAgent: testing]

})?
}

pub async fn get_conflict_diff(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] get_conflict_diff missing test coverage for edge cases

The get_conflict_diff method has no test coverage for binary files, missing files, empty files, or truncation scenarios.

Suggestion: Add tests for: (1) binary conflict file producing is_binary:true with empty hunks, (2) conflict file that doesn't exist on disk (build_added_file_fallback_hunks failure path), (3) large conflict file truncation behavior, (4) empty conflict file.

Risk: Binary conflict files or missing workdir files could produce unexpected diff results that break the UI without any automated detection.

Confidence: 0.85

[From SubAgent: testing]

onToggleStage={handleToggleStage}
onToggleAll={handleToggleAll}
/>
{snapshot.conflictedFiles.length > 0 && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Missing optional chaining on snapshot.conflictedFiles in ConflictGroup render

At R1660, snapshot.conflictedFiles.length is accessed without optional chaining, while the same property at R1062 uses snapshot?.conflictedFiles.length ?? 0. Since the optional chaining at R1062 implies snapshot can be null, the direct access at R1660 may crash.

Suggestion: Change snapshot.conflictedFiles.length > 0 to snapshot?.conflictedFiles.length or add a guard: (snapshot?.conflictedFiles?.length ?? 0) > 0.

Risk: Runtime TypeError crash if snapshot is null when the ConflictGroup section renders.

Confidence: 0.85

[From SubAgent: general]

@jorben
jorben merged commit 5449744 into master Apr 21, 2026
4 checks passed
@jorben
jorben deleted the feat/confilict-diff-support branch April 21, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant