Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| use imara_diff::intern::InternedInput; |
| 6 | use imara_diff::sources::byte_lines_with_terminator; |
| 7 | use imara_diff::{Algorithm, diff as imara}; |
| 8 |
| 9 | /// Files bigger than this aren't diffed or merged line by line. |
| 10 | pub const TEXT_LIMIT: usize = 16 << 20; |
| 11 |
| 12 | pub fn is_binary(bytes: &[u8]) -> bool { |
| 13 | bytes.len() > TEXT_LIMIT || bytes[..bytes.len().min(8000)].contains(&0) |
| 14 | } |
| 15 |
| 16 | /// Lines including their terminators. |
| 17 | pub fn lines(bytes: &[u8]) -> Vec<&[u8]> { |
| 18 | byte_lines_with_terminator(bytes).collect() |
| 19 | } |
| 20 |
| 21 | /// A changed region: lines `before` of the old text became lines `after`. |
| 22 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 23 | pub struct Hunk { |
| 24 | pub before: Range<u32>, |
| 25 | pub after: Range<u32>, |
| 26 | } |
| 27 |
| 28 | pub fn hunks(old: &[u8], new: &[u8]) -> Vec<Hunk> { |
| 29 | let input = InternedInput::new(byte_lines_with_terminator(old), byte_lines_with_terminator(new)); |
| 30 | let mut out = Vec::new(); |
| 31 | imara(Algorithm::Histogram, &input, |before: Range<u32>, after: Range<u32>| { |
| 32 | out.push(Hunk { before, after }); |
| 33 | }); |
| 34 | out |
| 35 | } |
| 36 |
| 37 | /// Lines added and removed. |
| 38 | pub fn counts(old: &[u8], new: &[u8]) -> (usize, usize) { |
| 39 | hunks(old, new).iter().fold((0, 0), |(a, r), h| (a + h.after.len(), r + h.before.len())) |
| 40 | } |
| 41 |
| 42 | /// A unified diff body (the `@@` hunks) with `context` lines around changes. |
| 43 | pub fn unified(old: &[u8], new: &[u8], context: u32) -> String { |
| 44 | let a = lines(old); |
| 45 | let b = lines(new); |
| 46 | let hs = hunks(old, new); |
| 47 | if hs.is_empty() { |
| 48 | return String::new(); |
| 49 | } |
| 50 | // Group hunks whose context overlaps. |
| 51 | let mut groups: Vec<Vec<&Hunk>> = Vec::new(); |
| 52 | for h in &hs { |
| 53 | match groups.last_mut() { |
| 54 | Some(g) if h.before.start <= g.last().unwrap().before.end + 2 * context => g.push(h), |
| 55 | _ => groups.push(vec![h]), |
| 56 | } |
| 57 | } |
| 58 | let mut out = String::new(); |
| 59 | for g in groups { |
| 60 | let first = g[0]; |
| 61 | let last = g[g.len() - 1]; |
| 62 | let a_start = first.before.start.saturating_sub(context); |
| 63 | let a_end = (last.before.end + context).min(a.len() as u32); |
| 64 | let lead = first.before.start - a_start; |
| 65 | let b_start = first.after.start - lead; |
| 66 | let trail = a_end - last.before.end; |
| 67 | let b_end = last.after.end + trail; |
| 68 | out.push_str(&format!( |
| 69 | "@@ -{} +{} @@\n", |
| 70 | range_text(a_start, a_end - a_start), |
| 71 | range_text(b_start, b_end - b_start) |
| 72 | )); |
| 73 | let mut pos = a_start; |
| 74 | for h in &g { |
| 75 | for line in &a[pos as usize..h.before.start as usize] { |
| 76 | push_line(&mut out, ' ', line); |
| 77 | } |
| 78 | for line in &a[h.before.start as usize..h.before.end as usize] { |
| 79 | push_line(&mut out, '-', line); |
| 80 | } |
| 81 | for line in &b[h.after.start as usize..h.after.end as usize] { |
| 82 | push_line(&mut out, '+', line); |
| 83 | } |
| 84 | pos = h.before.end; |
| 85 | } |
| 86 | for line in &a[pos as usize..a_end as usize] { |
| 87 | push_line(&mut out, ' ', line); |
| 88 | } |
| 89 | } |
| 90 | out |
| 91 | } |
| 92 |
| 93 | fn range_text(start: u32, len: u32) -> String { |
| 94 | match len { |
| 95 | 0 => format!("{start},0"), |
| 96 | 1 => format!("{}", start + 1), |
| 97 | _ => format!("{},{len}", start + 1), |
| 98 | } |
| 99 | } |
| 100 |
| 101 | fn push_line(out: &mut String, sign: char, line: &[u8]) { |
| 102 | out.push(sign); |
| 103 | let text = String::from_utf8_lossy(line); |
| 104 | out.push_str(&text); |
| 105 | if !text.ends_with('\n') { |
| 106 | out.push_str("\n\\ No newline at end of file\n"); |
| 107 | } |
| 108 | } |
| 109 |
| 110 | #[cfg(test)] |
| 111 | mod tests { |
| 112 | use super::*; |
| 113 |
| 114 | #[test] |
| 115 | fn unified_diff() { |
| 116 | let old = b"a\nb\nc\nd\ne\nf\ng\n"; |
| 117 | let new = b"a\nb\nC\nd\ne\nf\ng\nh\n"; |
| 118 | let text = unified(old, new, 1); |
| 119 | assert!(text.contains("-c\n+C\n")); |
| 120 | assert!(text.contains("+h\n")); |
| 121 | assert_eq!(counts(old, new), (2, 1)); |
| 122 | } |
| 123 |
| 124 | #[test] |
| 125 | fn binary() { |
| 126 | assert!(is_binary(b"a\0b")); |
| 127 | assert!(!is_binary(b"plain text\n")); |
| 128 | } |
| 129 | } |
| 130 |