Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
796cc7b
Add unified diff parser with tests
joske Mar 12, 2026
43b492a
Add context diff parser with tests
joske Mar 12, 2026
88ae581
Add patch hunk application with tests
joske Mar 12, 2026
a7d0e4c
Add patch file auto-detection
joske Mar 12, 2026
8b34569
Add CompareMode::Patch variant and CLI detection
joske Mar 12, 2026
01e6ce1
Implement patch window for single-file and multi-file patches
joske Mar 12, 2026
70581bb
Add round-trip parse+apply integration tests
joske Mar 12, 2026
630bac1
Multi-file patches use dir comparison with symlinked originals
joske Mar 12, 2026
bf9ae7e
Add apply-all, fix temp paths, conflict coloring, and cleanup
joske Mar 12, 2026
3710ef6
Fix security and robustness issues in patch support
joske Mar 15, 2026
2f8f715
Use line-start marker detection for apply-all conflict skip
joske Mar 15, 2026
0585b4d
Apply fmt and clippy pedantic fixes
joske Mar 15, 2026
d40f5d9
Fix Windows path traversal, single-file patch mismatch, and missing b…
joske Mar 15, 2026
50a49f3
Reject ambiguous single-file patch matches and surface missing delete…
joske Mar 15, 2026
ed8a19d
Add 26 edge case tests for patch parsing, application, and conflict d…
joske Mar 15, 2026
ffea24f
bump
joske Mar 15, 2026
62ebcd9
bump to 0.8.2
joske Mar 15, 2026
9e81029
bump clap
joske Mar 15, 2026
145e1de
Fix bench CI: replace deprecated criterion::black_box with std::hint
joske Mar 15, 2026
05fd494
Add 13 UI integration tests for patch viewing feature
joske Mar 15, 2026
1b45ad7
Add patch viewing and apply-all test cases to TESTING.md
joske Mar 16, 2026
f779688
Mark 13 automated patch UI tests in TESTING.md
joske Mar 16, 2026
92cc52a
Fix Copilot review: apply-all targets, dedup, docs, cleanup
joske Mar 16, 2026
ec40c7e
Fix Copilot review round 2: gate removals, permissions, error dialog
joske Mar 16, 2026
8fe912c
Fix Copilot review round 3: symlink recursion, range validation, tests
joske Mar 16, 2026
e9a482b
Fix Copilot review round 4: Added fallback, LeftOnly filter, registry…
joske Mar 16, 2026
e03acee
Fix Copilot review round 5: registry timing, dir cleanup, conflicts, …
joske Mar 16, 2026
8780dac
Fix Copilot review round 6: tempfile, /dev/null detection, hunk valid…
joske Mar 16, 2026
b97caae
Fix Copilot review round 7: swap safety, tooltip swap, write errors
joske Mar 16, 2026
67e6008
Replace placeholder collect_actionable_files tests with logic tests
joske Mar 16, 2026
2be7a0e
Fix trailing newline on empty output, normalize backslashes, unscope …
joske Mar 16, 2026
2a3a35e
Tighten context diff detection, fix conflict skip, symlink-to-dir safety
joske Mar 16, 2026
f6c5cce
Remove empty-line hunk special case, scope conflict marker to patch m…
joske Mar 16, 2026
3dd9570
fmt
joske Mar 17, 2026
263add4
bump
joske Mar 23, 2026
b598ed2
bump
joske Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 152 additions & 63 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mergers"
version = "0.8.1"
version = "0.8.2"
edition = "2024"
description = "A visual diff and merge tool for files and directories"
license = "GPL-2.0"
Expand All @@ -12,13 +12,14 @@ gtk4 = { version = "0.11", features = ["v4_14"] }
gio = { version = "0.22", features = ["v2_66"] }
chrono = "0.4"
notify = "8.2"
clap = { version = "4.5", features = ["derive"] }
clap = { version = "4.6", features = ["derive"] }
sourceview5 = { version = "0.11", features = ["gtk_v4_14"] }
serde = { version = "1.0", features = ["derive"] }
toml = "1.0"
ctrlc = { version = "3.5.2", features = ["termination"] }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.8", features = ["html_reports"] }
proptest = "1.10"
tempfile = "3.26"

Expand Down
62 changes: 36 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ A visual diff and merge tool written in Rust with GTK4, inspired by [Meld](https
- **Chunk navigation** with keyboard shortcuts (with optional wrap-around)
- **Synchronized scrolling** — chunk-aware vertical sync keeps corresponding lines aligned
- **Chunk map** — visual overview strips alongside each pane with viewport indicator
- **Patch viewing** — open `.patch`/`.diff` files to interactively review and apply changes, with conflict markers for failed hunks
- **Patch export** (unified diff format)
- **Refresh** (Ctrl+R / F5) — reload files from disk with unsaved-changes confirmation
- **Open externally** (Ctrl+Shift+O) — open focused file in system default app
Expand Down Expand Up @@ -95,6 +96,12 @@ mergers dir1/ dir2/
# Three-way merge (left, merged, right)
mergers local.txt merged.txt remote.txt

# Apply a patch to a file (auto-detected from .patch/.diff extension)
mergers original.txt changes.patch

# Apply a patch to a directory
mergers project/ changes.patch

# View uncommitted git changes
mergers .

Expand All @@ -120,32 +127,35 @@ git mergetool

## Comparison with Similar Tools

| Feature | mergers | [Meld](https://meldmerge.org/) | [Diffuse](https://github.com/MightyCreak/diffuse) |
| --------------------------------- | --------------------- | ------------------------------ | ------------------------------------------------- |
| **File Comparison** | 2-way, 3-way | 2-way, 3-way | N-way (arbitrary) |
| **Directory Comparison** | Yes | Yes | No |
| **VCS Integration** | Git | Git, Hg, Bzr, CVS, SVN | Git, Hg, Bzr, CVS, SVN, Darcs, Monotone, RCS |
| **Syntax Highlighting** | Yes (GtkSourceView 5) | Yes (GtkSourceView) | Yes |
| **Word-level Diffs** | Yes | Yes | No |
| **Inline Editing** | Yes | Yes | Yes |
| **Manual Line Alignment** | No | No | Yes |
| **Undo/Redo** | Yes | Yes | Unlimited undo |
| **Find & Replace** | Yes | Yes | Yes |
| **Go to Line** | Yes | No | No |
| **Patch Export** | Yes | No | No |
| **Ignore Whitespace** | Yes | Yes | No |
| **Ignore Blank Lines** | Yes | Yes | No |
| **Text Wrapping Options** | Yes | Yes | No |
| **File Monitoring / Auto-reload** | Yes | Yes | No |
| **Synchronized Scrolling** | Yes | Yes | No |
| **Chunk Map (minimap)** | Yes | Yes | No |
| **Conflict Marker Detection** | Yes | No | No |
| **Folder Filters** | Yes | Yes | N/A |
| **Git Mergetool Support** | Yes | Yes | Yes |
| **Custom Pane Labels** | Yes | Yes | Yes |
| **Toolkit** | GTK4 | GTK3 | GTK3 |
| **Language** | Rust | Python | Python |
| **License** | GPL-2.0 | GPL-2.0 | GPL-2.0 |
| Feature | mergers | [Meld](https://meldmerge.org/) | [Diffuse](https://github.com/MightyCreak/diffuse) | [Kompare](https://apps.kde.org/kompare/) |
| --------------------------------- | --------------------- | ------------------------------ | ------------------------------------------------- | ---------------------------------------- |
| **File Comparison** | 2-way, 3-way | 2-way, 3-way | N-way (arbitrary) | 2-way |
| **Directory Comparison** | Yes | Yes | No | Yes |
| **VCS Integration** | Git | Git, Hg, Bzr, CVS, SVN | Git, Hg, Bzr, CVS, SVN, Darcs, Monotone, RCS | No |
| **Syntax Highlighting** | Yes (GtkSourceView 5) | Yes (GtkSourceView) | Yes | No |
| **Word-level Diffs** | Yes | Yes | No | No |
| **Inline Editing** | Yes | Yes | Yes | No |
| **Manual Line Alignment** | No | No | Yes | No |
| **Undo/Redo** | Yes | Yes | Unlimited undo | No |
| **Find & Replace** | Yes | Yes | Yes | No |
| **Go to Line** | Yes | No | No | No |
| **Patch Export** | Yes | No | No | Yes |
| **Patch Viewing/Applying** | Yes | No | No | Yes |
| **Ignore Whitespace** | Yes | Yes | No | No |
| **Ignore Blank Lines** | Yes | Yes | No | No |
| **Text Wrapping Options** | Yes | Yes | No | No |
| **File Monitoring / Auto-reload** | Yes | Yes | No | No |
| **Synchronized Scrolling** | Yes | Yes | No | No |
| **Chunk Map (minimap)** | Yes | Yes | No | No |
| **Conflict Marker Detection** | Yes | No | No | No |
| **Diff Statistics** | No | No | No | Yes |
| **Folder Filters** | Yes | Yes | N/A | No |
| **Git Mergetool Support** | Yes | Yes | Yes | No |
| **Custom Pane Labels** | Yes | Yes | Yes | No |
| **Remote File Support** | Via GVFS | Via GVFS | No | Via KIO |
| **Toolkit** | GTK4 | GTK3 | GTK3 | Qt (KDE Frameworks) |
| **Language** | Rust | Python | Python | C++ |
| **License** | GPL-2.0 | GPL-2.0 | GPL-2.0 | GPL-2.0 |

## Building

Expand Down
66 changes: 65 additions & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Items marked ✅ are covered by automated UI integration tests (`tests/ui_integr
- [x] ✅ Single file path: prints error and exits
- [x] ✅ Non-existent path: prints error and exits
- [x] ✅ File + directory mix: prints error
- [x] ✅ File + patch file: opens patch viewing window
- [x] ✅ Directory + patch file: opens multi-file patch dir comparison
- [x] ✅ `-L`/`--label` flags: custom pane labels shown for 2-file and 3-file modes
- [x] ✅ `--version` / `--help`: correct output

Expand Down Expand Up @@ -47,6 +49,10 @@ Items marked ✅ are covered by automated UI integration tests (`tests/ui_integr
- [x] ✅ Go to line (Ctrl+L): entry appears; type number + Enter to jump; Escape to dismiss; focus returns to active pane
- [x] ✅ Blanks toggle: re-diffs ignoring blank lines
- [x] ✅ Spaces toggle: re-diffs ignoring whitespace
- [ ] Apply all (⏪ button): copies all non-conflicting chunks right → left
- [ ] Apply all: single undo operation (one Ctrl+Z reverts everything)
- [ ] Apply all: skips chunks containing conflict markers (`<<<<<<< original` / `>>>>>>> patch`)
- [ ] Apply all on binary files: button is disabled
- [ ] Export patch (Ctrl+Shift+P): save dialog; generates unified diff
- [x] ✅ Swap panes: swaps content, labels, save paths, dirty state; title updates
- [x] ✅ Preferences (Ctrl+,): opens Preferences
Expand Down Expand Up @@ -153,7 +159,7 @@ Items marked ✅ are covered by automated UI integration tests (`tests/ui_integr
- [ ] Directory headers above each pane with full path as tooltip
- [ ] Copy path button next to each header: copies full absolute path to clipboard
- [ ] Copy path after swap: copies the swapped (current) path, not the original
- [ ] Status colors: blue (different), orange (left-only), green (right-only), dim italic (missing side)
- [ ] Status colors: blue (different), orange (left-only), green (right-only), red (conflict), dim italic (missing side)

### Navigation
- [x] ✅ Double-click file row: opens diff in new notebook tab
Expand All @@ -171,6 +177,10 @@ Items marked ✅ are covered by automated UI integration tests (`tests/ui_integr
- [ ] Delete (Delete key): trashes selected file with confirmation
- [x] ✅ Collapse all: collapses every expanded directory row
- [x] ✅ Expand all: expands every directory row
- [ ] Apply all non-conflicting (⏪ button): copies all non-conflicting files right → left
- [ ] Apply all: confirmation dialog shows file count
- [ ] Apply all: handles modified, new, and deleted files
- [ ] Apply all: skips conflicting files
- [ ] Swap panes: rescans, updates headers and window title
- [ ] Preferences (Ctrl+,)

Expand Down Expand Up @@ -325,6 +335,60 @@ Items marked ✅ are covered by automated UI integration tests (`tests/ui_integr
### Security
- [ ] Paths with `..` components refused

## Patch Viewing / Applying

### CLI
- [x] ✅ `mergers file.c patch.diff`: opens single-file patch view (original vs patched)
- [x] ✅ `mergers dir/ patch.diff`: opens multi-file patch view (dir comparison)
- [x] ✅ `.diff` extension auto-detected as patch mode
- [x] ✅ Ctrl+W closes patch window cleanly
- [ ] `-L` labels applied to patch views
- [ ] Invalid patch file: error dialog shown
- [ ] Base path does not exist: error dialog shown

### Single-File Patch
- [x] ✅ Window title contains "patch" and base filename
- [x] ✅ Two text panes shown
- [x] ✅ Left pane: original file content; Right pane: patched result
- [x] ✅ Right pane contains inserted lines from patch
- [x] ✅ Chunk label shows change count
- [x] ✅ Tab label contains "patch"
- [ ] Conflict hunks shown with `<<<<<<< original` / `>>>>>>> patch` markers in right pane
- [ ] Labels show base filename and "filename (patched)" (not temp paths)
- [ ] Window destroy cleans up temp directory

### Multi-File Patch (Dir View)
- [x] ✅ Window opens successfully
- [x] ✅ Shows patched files (e.g. "different.txt")
- [x] ✅ Shows subdirectories from the patch
- [ ] Left header shows base directory name (not temp path)
- [ ] Left header tooltip shows original base path (not temp path)
- [ ] Right header shows "dirname (patched)"
- [ ] Right header tooltip shows temp directory path
- [ ] Conflicted files colored red in tree view
- [ ] Same-status files filtered out of tree view
- [ ] Empty directories filtered out of tree view
- [ ] Double-click file: opens diff tab with resolved paths
- [ ] Diff tab left pane header/tooltip: shows original file path (resolved through symlink)
- [ ] Diff tab right pane: shows temp patched file path
- [ ] Unsaved changes dialog shows original path (not temp path)

### Apply All (Dir Patch View)
- [ ] ⏪ button: confirmation dialog shows file count
- [ ] Modified files (Different): copied right → left through symlink, then removed from view
- [ ] New files (RightOnly): created in original directory, parent dirs created as needed
- [ ] Deleted files (LeftOnly): removed from original directory
- [ ] Conflicted files: skipped (not applied)
- [ ] View reloads after apply; applied files vanish from tree
- [ ] File watcher suppressed during bulk copy (no spurious reloads)
- [ ] Errors during apply: error dialog shown with details

### Temp Directory Cleanup
- [ ] Normal window close: temp dir removed (connect_shutdown)
- [ ] SIGINT (Ctrl+C): temp dir removed (ctrlc handler)
- [ ] SIGTERM (kill): temp dir removed (ctrlc handler)
- [ ] Multiple patch windows: all temp dirs tracked and cleaned up

## Preferences

- [ ] Font: chooser dialog; live-apply
Expand Down
4 changes: 3 additions & 1 deletion benches/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
clippy::format_collect
)]

use criterion::{Criterion, black_box, criterion_group, criterion_main};
use std::hint::black_box;

use criterion::{Criterion, criterion_group, criterion_main};
use mergers::{
_bench::{self, Side},
myers::{self, DiffChunk, DiffTag},
Expand Down
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

pub mod myers;
pub mod patch;
pub mod settings;
pub mod ui;
pub mod vcs;
Expand Down Expand Up @@ -39,6 +40,11 @@ pub enum CompareMode {
right: PathBuf,
labels: Vec<String>,
},
Patch {
base: PathBuf,
patch: PathBuf,
labels: Vec<String>,
},
Vcs {
dir: PathBuf,
},
Expand Down
Loading
Loading