Skip to content

Commit 8649446

Browse files
committed
v0.2.4: diff3 conflict markers for jj, multi-line import merging
Output diff3 format (with ||||||| base section) in standard marker mode so jj can parse partial resolutions. Fixes "did not produce valid conflict markers" error. Thanks @ilyagr for the report. Also adds multi-line import block merging (consolidates specifiers as sets) and README updates with jj config + mergiraf comparison.
1 parent 57ec765 commit 8649446

8 files changed

Lines changed: 680 additions & 47 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ The same scenario above? Weave merges it cleanly with zero conflicts — both fu
6363

6464
The key difference: Git produces false conflicts on **independent changes** because they happen to be in the same file. Weave only conflicts on **actual semantic collisions** when two branches change the same entity incompatibly.
6565

66+
## Weave vs Mergiraf
67+
68+
Tested on 31 real-world merge scenarios across Python, TypeScript, Rust, Go, Java, and C:
69+
70+
| Tool | Clean Merges | Score |
71+
|------|-------------|-------|
72+
| **Weave** | **31/31** | 100% |
73+
| Mergiraf (v0.16.3) | 26/31 | 83% |
74+
| Git | 15/31 | 48% |
75+
76+
Mergiraf fails on both-add-at-end-of-file, insert-between-existing, and decorator conflict scenarios. Weave resolves all of these because it operates at entity granularity (functions, classes, methods) rather than AST node level. Full breakdown at [ataraxy-labs.github.io/weave](https://ataraxy-labs.github.io/weave/).
77+
6678
## Real-World Benchmarks
6779

6880
Tested on real merge commits from major open-source repositories. For each merge commit, we replay the merge with both Git and Weave, then compare against the human-authored result.
@@ -137,6 +149,25 @@ git config merge.weave.driver "weave-driver %O %A %B %L %P"
137149
echo "*.ts *.tsx *.js *.py *.go *.rs *.java *.c *.cpp *.rb *.cs merge=weave" >> .git/info/attributes
138150
```
139151

152+
## Jujutsu (jj)
153+
154+
Add to your jj config (`jj config edit --user`):
155+
156+
```toml
157+
[merge-tools.weave]
158+
program = "weave-driver"
159+
merge-args = ["$base", "$left", "$right", "-o", "$output", "-l", "$marker_length", "-p", "$path"]
160+
merge-conflict-exit-codes = [1]
161+
merge-tool-edits-conflict-markers = true
162+
conflict-marker-style = "git"
163+
```
164+
165+
Resolve conflicts with `jj resolve --tool weave`, or set as default:
166+
167+
```bash
168+
jj config set --user ui.merge-editor "weave"
169+
```
170+
140171
## Preview
141172

142173
Dry-run a merge to see what weave would do:

crates/weave-cli/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "weave-cli"
3-
version = "0.2.3"
3+
version = "0.2.4"
44
edition = "2021"
55
license = "MIT OR Apache-2.0"
66
description = "Entity-level semantic merge CLI. Resolves conflicts at the function/class level instead of lines."
@@ -15,8 +15,8 @@ name = "weave"
1515
path = "src/main.rs"
1616

1717
[dependencies]
18-
weave-core = { path = "../weave-core", version = "0.2.3" }
19-
weave-crdt = { path = "../weave-crdt", version = "0.2.3" }
18+
weave-core = { path = "../weave-core", version = "0.2.4" }
19+
weave-crdt = { path = "../weave-crdt", version = "0.2.4" }
2020
sem-core = "0.3.9"
2121
clap = { version = "4", features = ["derive"] }
2222
colored = "3"

crates/weave-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "weave-core"
3-
version = "0.2.3"
3+
version = "0.2.4"
44
edition = "2021"
55
license = "MIT OR Apache-2.0"
66
description = "Entity-level semantic merge engine. Three-way merge at the function/class/method level instead of lines."

crates/weave-core/src/conflict.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,28 @@ impl EntityConflict {
375375
}
376376
}
377377

378+
// Base section for diff3 format (standard mode only)
379+
if !fmt.enhanced {
380+
let base_marker = "|".repeat(fmt.marker_length);
381+
out.push_str(&format!("{} base\n", base_marker));
382+
let base = self.base_content.as_deref().unwrap_or("");
383+
if has_narrowing {
384+
let base_lines: Vec<&str> = base.lines().collect();
385+
// Use prefix/suffix from ours/theirs narrowing as approximation
386+
let base_prefix = prefix_len.min(base_lines.len());
387+
let base_suffix = suffix_len.min(base_lines.len().saturating_sub(base_prefix));
388+
for line in &base_lines[base_prefix..base_lines.len() - base_suffix] {
389+
out.push_str(line);
390+
out.push('\n');
391+
}
392+
} else {
393+
out.push_str(base);
394+
if !base.is_empty() && !base.ends_with('\n') {
395+
out.push('\n');
396+
}
397+
}
398+
}
399+
378400
out.push_str(&format!("{}\n", sep));
379401

380402
// Theirs content (narrowed or full)
@@ -771,7 +793,7 @@ mod tests {
771793
base_content: Some("return 0;".to_string()),
772794
};
773795
let markers = conflict.to_conflict_markers(&MarkerFormat::standard(7));
774-
assert_eq!(markers, "<<<<<<< ours\nreturn 1;\n=======\nreturn 2;\n>>>>>>> theirs\n");
796+
assert_eq!(markers, "<<<<<<< ours\nreturn 1;\n||||||| base\nreturn 0;\n=======\nreturn 2;\n>>>>>>> theirs\n");
775797
// No em-dash, no hint, no metadata
776798
assert!(!markers.contains('\u{2014}'));
777799
assert!(!markers.contains("hint"));

0 commit comments

Comments
 (0)