//! Replays a git repository's merge commits through Onset's merge and //! compares it with what people actually committed. //! //! For every file both sides of a merge changed: where git's line merge is //! clean, Onset must give the same file; where git stopped and a person //! resolved it, Onset either leaves it to them too, settles it the way they //! did, or (the number to keep at zero) settles it some other way. Files git //! merged cleanly that Onset flags as declaring something twice are listed //! for a look. //! //! Usage: cargo run --release -p onset-core --example merge_corpus -- [max merges] use std::collections::HashSet; use std::io::{BufRead, BufReader, Read, Write}; use std::process::{Command, Stdio}; use onset_core::merge; struct Blobs { child: std::process::Child, out: BufReader, } impl Blobs { fn new(repo: &str) -> Blobs { let mut child = Command::new("git").args(["-C", repo, "cat-file", "--batch"]).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().expect("git cat-file"); let out = BufReader::new(child.stdout.take().unwrap()); Blobs { child, out } } fn get(&mut self, rev: &str, path: &str) -> Option> { writeln!(self.child.stdin.as_mut().unwrap(), "{rev}:{path}").ok()?; let mut header = String::new(); self.out.read_line(&mut header).ok()?; let parts: Vec<&str> = header.split_whitespace().collect(); if parts.len() < 3 || parts[1] != "blob" { return None; } let size: usize = parts[2].parse().ok()?; let mut data = vec![0; size + 1]; self.out.read_exact(&mut data).ok()?; data.pop(); Some(data) } } fn git(repo: &str, args: &[&str]) -> String { let out = Command::new("git").arg("-C").arg(repo).args(args).output().expect("git"); String::from_utf8_lossy(&out.stdout).into_owned() } fn main() { let args: Vec = std::env::args().collect(); let repo = args.get(1).expect("usage: merge_corpus [max merges]"); let max: usize = args.get(2).and_then(|n| n.parse().ok()).unwrap_or(usize::MAX); let mut blobs = Blobs::new(repo); let (mut merges, mut files) = (0, 0); let (mut git_clean, mut same_as_git, mut differ_from_git) = (0, 0, 0); let (mut people, mut settled_same, mut settled_other, mut left) = (0, 0, 0, 0); let mut flagged = Vec::new(); for line in git(repo, &["rev-list", "--merges", "--parents", "HEAD"]).lines() { if merges >= max { break; } let ids: Vec<&str> = line.split_whitespace().collect(); if ids.len() != 3 { continue; } let (m, p1, p2) = (ids[0], ids[1], ids[2]); let bases = git(repo, &["merge-base", "--all", p1, p2]); let bases: Vec<&str> = bases.lines().collect(); if bases.len() != 1 { continue; } let b = bases[0]; merges += 1; let changed = |rev: &str| git(repo, &["diff", "--name-only", "--no-renames", b, rev]).lines().map(String::from).collect::>(); let both: Vec = changed(p1).intersection(&changed(p2)).cloned().collect(); for path in both { let (Some(base), Some(ours), Some(theirs), Some(result)) = (blobs.get(b, &path), blobs.get(p1, &path), blobs.get(p2, &path), blobs.get(m, &path)) else { continue }; if [&base, &ours, &theirs].iter().any(|d| d.contains(&0)) || ours == theirs { continue; } files += 1; let plain = merge::text(&base, &ours, &theirs, "main", "change"); let smart = merge::text_for(&path, &base, &ours, &theirs, "main", "change"); if plain.conflicts == 0 { git_clean += 1; if smart.conflicts == 0 && smart.content == plain.content { same_as_git += 1; } else { differ_from_git += 1; println!("DIFFERS FROM GIT {m:.10} {path}"); } let twice = merge::suspect(&path, &ours, &theirs, &plain.content); if !twice.is_empty() { if let Ok(dir) = std::env::var("MERGE_CORPUS_OUT") { let stem = format!("{dir}/flagged-{m:.10}-{}", path.replace('/', "_")); let _ = std::fs::write(format!("{stem}.merged"), &plain.content); let _ = std::fs::write(format!("{stem}.ours"), &ours); let _ = std::fs::write(format!("{stem}.theirs"), &theirs); } let kept = merge::suspect(&path, &ours, &theirs, &result); flagged.push(format!("{m:.10} {path}: {} (the commit {})", twice.join(", "), if kept.is_empty() { "has one" } else { "has both" })); } continue; } people += 1; if smart.conflicts > 0 { left += 1; } else if smart.content == result { settled_same += 1; } else { settled_other += 1; println!("SETTLED OTHERWISE {m:.10} {path}"); // MERGE_CORPUS_OUT= keeps both files to compare. if let Ok(dir) = std::env::var("MERGE_CORPUS_OUT") { let stem = format!("{dir}/{m:.10}-{}", path.replace('/', "_")); let _ = std::fs::write(format!("{stem}.onset"), &smart.content); let _ = std::fs::write(format!("{stem}.person"), &result); } } } } println!(); println!("merges {merges}, files both sides changed {files}"); println!("git clean: {git_clean} (Onset same {same_as_git}, different {differ_from_git})"); println!("git conflicts a person resolved: {people} (Onset settled the same way {settled_same}, otherwise {settled_other}, left to them {left})"); println!("git-clean merges Onset flags as declaring something twice: {}", flagged.len()); for f in &flagged { println!(" {f}"); } }