Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 | //! resolved it, Onset either leaves it to them too, settles it the way they |
| 7 | //! did, or (the number to keep at zero) settles it some other way. Files git |
| 8 | //! merged cleanly that Onset flags as declaring something twice are listed |
| 9 | //! for a look. |
| 10 | //! |
| 11 | //! Usage: cargo run --release -p onset-core --example merge_corpus -- <git repo> [max merges] |
| 12 |
| 13 | use std::collections::HashSet; |
| 14 | use std::io::{BufRead, BufReader, Read, Write}; |
| 15 | use std::process::{Command, Stdio}; |
| 16 |
| 17 | use onset_core::merge; |
| 18 |
| 19 | struct Blobs { |
| 20 | child: std::process::Child, |
| 21 | out: BufReader<std::process::ChildStdout>, |
| 22 | } |
| 23 |
| 24 | impl Blobs { |
| 25 | fn new(repo: &str) -> Blobs { |
| 26 | let mut child = Command::new("git").args(["-C", repo, "cat-file", "--batch"]).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().expect("git cat-file"); |
| 27 | let out = BufReader::new(child.stdout.take().unwrap()); |
| 28 | Blobs { child, out } |
| 29 | } |
| 30 |
| 31 | fn get(&mut self, rev: &str, path: &str) -> Option<Vec<u8>> { |
| 32 | writeln!(self.child.stdin.as_mut().unwrap(), "{rev}:{path}").ok()?; |
| 33 | let mut header = String::new(); |
| 34 | self.out.read_line(&mut header).ok()?; |
| 35 | let parts: Vec<&str> = header.split_whitespace().collect(); |
| 36 | if parts.len() < 3 || parts[1] != "blob" { |
| 37 | return None; |
| 38 | } |
| 39 | let size: usize = parts[2].parse().ok()?; |
| 40 | let mut data = vec![0; size + 1]; |
| 41 | self.out.read_exact(&mut data).ok()?; |
| 42 | data.pop(); |
| 43 | Some(data) |
| 44 | } |
| 45 | } |
| 46 |
| 47 | fn git(repo: &str, args: &[&str]) -> String { |
| 48 | let out = Command::new("git").arg("-C").arg(repo).args(args).output().expect("git"); |
| 49 | String::from_utf8_lossy(&out.stdout).into_owned() |
| 50 | } |
| 51 |
| 52 | fn main() { |
| 53 | let args: Vec<String> = std::env::args().collect(); |
| 54 | let repo = args.get(1).expect("usage: merge_corpus <git repo> [max merges]"); |
| 55 | let max: usize = args.get(2).and_then(|n| n.parse().ok()).unwrap_or(usize::MAX); |
| 56 | let mut blobs = Blobs::new(repo); |
| 57 | let (mut merges, mut files) = (0, 0); |
| 58 | let (mut git_clean, mut same_as_git, mut differ_from_git) = (0, 0, 0); |
| 59 | let (mut people, mut settled_same, mut settled_other, mut left) = (0, 0, 0, 0); |
| 60 | let mut flagged = Vec::new(); |
| 61 | for line in git(repo, &["rev-list", "--merges", "--parents", "HEAD"]).lines() { |
| 62 | if merges >= max { |
| 63 | break; |
| 64 | } |
| 65 | let ids: Vec<&str> = line.split_whitespace().collect(); |
| 66 | if ids.len() != 3 { |
| 67 | continue; |
| 68 | } |
| 69 | let (m, p1, p2) = (ids[0], ids[1], ids[2]); |
| 70 | let bases = git(repo, &["merge-base", "--all", p1, p2]); |
| 71 | let bases: Vec<&str> = bases.lines().collect(); |
| 72 | if bases.len() != 1 { |
| 73 | continue; |
| 74 | } |
| 75 | let b = bases[0]; |
| 76 | merges += 1; |
| 77 | let changed = |rev: &str| git(repo, &["diff", "--name-only", "--no-renames", b, rev]).lines().map(String::from).collect::<HashSet<_>>(); |
| 78 | let both: Vec<String> = changed(p1).intersection(&changed(p2)).cloned().collect(); |
| 79 | for path in both { |
| 80 | 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 }; |
| 81 | if [&base, &ours, &theirs].iter().any(|d| d.contains(&0)) || ours == theirs { |
| 82 | continue; |
| 83 | } |
| 84 | files += 1; |
| 85 | let plain = merge::text(&base, &ours, &theirs, "main", "change"); |
| 86 | let smart = merge::text_for(&path, &base, &ours, &theirs, "main", "change"); |
| 87 | if plain.conflicts == 0 { |
| 88 | git_clean += 1; |
| 89 | if smart.conflicts == 0 && smart.content == plain.content { |
| 90 | same_as_git += 1; |
| 91 | } else { |
| 92 | differ_from_git += 1; |
| 93 | println!("DIFFERS FROM GIT {m:.10} {path}"); |
| 94 | } |
| 95 | let twice = merge::suspect(&path, &ours, &theirs, &plain.content); |
| 96 | if !twice.is_empty() { |
| 97 | if let Ok(dir) = std::env::var("MERGE_CORPUS_OUT") { |
| 98 | let stem = format!("{dir}/flagged-{m:.10}-{}", path.replace('/', "_")); |
| 99 | let _ = std::fs::write(format!("{stem}.merged"), &plain.content); |
| 100 | let _ = std::fs::write(format!("{stem}.ours"), &ours); |
| 101 | let _ = std::fs::write(format!("{stem}.theirs"), &theirs); |
| 102 | } |
| 103 | let kept = merge::suspect(&path, &ours, &theirs, &result); |
| 104 | flagged.push(format!("{m:.10} {path}: {} (the commit {})", twice.join(", "), if kept.is_empty() { "has one" } else { "has both" })); |
| 105 | } |
| 106 | continue; |
| 107 | } |
| 108 | people += 1; |
| 109 | if smart.conflicts > 0 { |
| 110 | left += 1; |
| 111 | } else if smart.content == result { |
| 112 | settled_same += 1; |
| 113 | } else { |
| 114 | settled_other += 1; |
| 115 | println!("SETTLED OTHERWISE {m:.10} {path}"); |
| 116 | // MERGE_CORPUS_OUT=<dir> keeps both files to compare. |
| 117 | if let Ok(dir) = std::env::var("MERGE_CORPUS_OUT") { |
| 118 | let stem = format!("{dir}/{m:.10}-{}", path.replace('/', "_")); |
| 119 | let _ = std::fs::write(format!("{stem}.onset"), &smart.content); |
| 120 | let _ = std::fs::write(format!("{stem}.person"), &result); |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | println!(); |
| 126 | println!("merges {merges}, files both sides changed {files}"); |
| 127 | println!("git clean: {git_clean} (Onset same {same_as_git}, different {differ_from_git})"); |
| 128 | println!("git conflicts a person resolved: {people} (Onset settled the same way {settled_same}, otherwise {settled_other}, left to them {left})"); |
| 129 | println!("git-clean merges Onset flags as declaring something twice: {}", flagged.len()); |
| 130 | for f in &flagged { |
| 131 | println!(" {f}"); |
| 132 | } |
| 133 | } |
| 134 |