1use std::collections::{HashMap, HashSet};
7use std::fmt;
8use std::str::FromStr;
9
10use thiserror::Error;
11
12use super::structured::{StructuredHunk, StructuredLine, StructuredLineKind};
13use crate::diff::structured::{
14 hash_hunk_body_from_unified_slice, hunk_body_digest_from_unified_slice,
15};
16use crate::diff::unified_hunk_header_counts;
17use crate::hash::ChecksumHasher;
18use crate::objects::HashAlgo;
19
20const HUNK_ID_HEX_LEN: usize = 12;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum DiffSide {
25 Unstaged,
27 Staged,
29 Other,
31}
32
33impl DiffSide {
34 fn tag_byte(self) -> u8 {
35 match self {
36 Self::Unstaged => 0,
37 Self::Staged => 1,
38 Self::Other => 2,
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct HunkId([u8; HUNK_ID_HEX_LEN / 2]);
46
47#[derive(Debug, Error, PartialEq, Eq)]
49pub enum HunkIdError {
50 #[error("hunk id must be {HUNK_ID_HEX_LEN} lowercase hex characters")]
52 Invalid,
53}
54
55impl HunkId {
56 fn from_digest(digest: &[u8]) -> Self {
57 let mut raw = [0u8; HUNK_ID_HEX_LEN / 2];
58 raw.copy_from_slice(&digest[..HUNK_ID_HEX_LEN / 2]);
59 Self(raw)
60 }
61
62 #[must_use]
64 pub fn from_bytes(bytes: [u8; HUNK_ID_HEX_LEN / 2]) -> Self {
65 Self(bytes)
66 }
67
68 #[must_use]
70 pub fn as_str(&self) -> String {
71 self.to_string()
72 }
73}
74
75impl fmt::Display for HunkId {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 for b in self.0 {
78 write!(f, "{b:02x}")?;
79 }
80 Ok(())
81 }
82}
83
84impl FromStr for HunkId {
85 type Err = HunkIdError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 if s.len() != HUNK_ID_HEX_LEN
89 || !s
90 .bytes()
91 .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c))
92 {
93 return Err(HunkIdError::Invalid);
94 }
95 let mut raw = [0u8; HUNK_ID_HEX_LEN / 2];
96 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
97 let hi = hex_nibble(chunk[0]).ok_or(HunkIdError::Invalid)?;
98 let lo = hex_nibble(chunk[1]).ok_or(HunkIdError::Invalid)?;
99 raw[i] = (hi << 4) | lo;
100 }
101 Ok(Self(raw))
102 }
103}
104
105fn hex_nibble(b: u8) -> Option<u8> {
106 match b {
107 b'0'..=b'9' => Some(b - b'0'),
108 b'a'..=b'f' => Some(b - b'a' + 10),
109 _ => None,
110 }
111}
112
113fn hash_hunk_id(
114 side: DiffSide,
115 path: &str,
116 hunk: &StructuredHunk,
117 duplicate_rank: Option<u32>,
118) -> HunkId {
119 let mut hasher = HashAlgo::Sha1.checksum_hasher();
120 hasher.update(&[side.tag_byte()]);
121 hasher.update(path.as_bytes());
122 hasher.update(&[0]);
123 hash_hunk_body(&mut hasher, hunk);
124 if let Some(rank) = duplicate_rank {
125 hasher.update(&rank.to_le_bytes());
126 }
127 let digest = hasher.finalize();
128 HunkId::from_digest(&digest)
129}
130
131#[derive(Eq, PartialEq, Ord, PartialOrd)]
132struct DuplicateSortKey {
133 old_start: usize,
134 new_start: usize,
135 is_peer: bool,
136 index: usize,
137}
138
139fn duplicate_ranks(hunks: &[StructuredHunk], peer_hunks: &[StructuredHunk]) -> Vec<Option<u32>> {
140 let own_digests: Vec<Vec<u8>> = hunks.iter().map(hunk_body_digest).collect();
141 let peer_digests: Vec<Vec<u8>> = peer_hunks.iter().map(hunk_body_digest).collect();
142 let body_counts = body_count_map_from_digests(&own_digests, &peer_digests);
143 own_digests
144 .iter()
145 .enumerate()
146 .map(|(i, body)| {
147 duplicate_rank_for_body(i, body, hunks, peer_hunks, body_counts.get(body).copied())
148 })
149 .collect()
150}
151
152fn duplicate_rank_for_body(
153 index: usize,
154 body: &[u8],
155 hunks: &[StructuredHunk],
156 peer_hunks: &[StructuredHunk],
157 body_count: Option<u32>,
158) -> Option<u32> {
159 if body_count.unwrap_or(0) <= 1 {
160 return None;
161 }
162 let mut keys: Vec<DuplicateSortKey> = Vec::new();
163 for (j, x) in hunks.iter().enumerate() {
164 if hunk_body_digest(x) == body {
165 keys.push(DuplicateSortKey {
166 old_start: x.old_start,
167 new_start: x.new_start,
168 is_peer: false,
169 index: j,
170 });
171 }
172 }
173 for (j, x) in peer_hunks.iter().enumerate() {
174 if hunk_body_digest(x) == body {
175 keys.push(DuplicateSortKey {
176 old_start: x.old_start,
177 new_start: x.new_start,
178 is_peer: true,
179 index: j,
180 });
181 }
182 }
183 keys.sort();
184 keys.iter()
185 .position(|k| !k.is_peer && k.index == index)
186 .map(|p| p as u32)
187}
188
189fn unified_slice_starts(slice: &str) -> (usize, usize) {
190 slice
191 .split('\n')
192 .next()
193 .and_then(unified_hunk_header_counts)
194 .map(|(o, _, n, _)| (o, n))
195 .unwrap_or((0, 0))
196}
197
198#[allow(clippy::too_many_arguments)]
199fn duplicate_ranks_unified_slices(
200 slices: &[&str],
201 before_count: usize,
202 after_count: usize,
203 before_ends_nl: bool,
204 after_ends_nl: bool,
205 peer_hunks: &[StructuredHunk],
206) -> Vec<Option<u32>> {
207 let bodies: Vec<Option<Vec<u8>>> = slices
208 .iter()
209 .map(|slice| {
210 hunk_body_digest_from_unified_slice(
211 slice,
212 before_count,
213 after_count,
214 before_ends_nl,
215 after_ends_nl,
216 )
217 })
218 .collect();
219 let mut body_counts: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
220 for body in bodies.iter().flatten() {
221 *body_counts.entry(body.clone()).or_insert(0) += 1;
222 }
223 for h in peer_hunks {
224 let body = hunk_body_digest(h);
225 *body_counts.entry(body).or_insert(0) += 1;
226 }
227 bodies
228 .iter()
229 .enumerate()
230 .map(|(i, body)| {
231 let body = body.as_ref()?;
232 if body_counts.get(body).copied().unwrap_or(0) <= 1 {
233 return None;
234 }
235 let mut keys: Vec<DuplicateSortKey> = Vec::new();
236 for (j, other) in slices.iter().enumerate() {
237 let Some(other_body) = hunk_body_digest_from_unified_slice(
238 other,
239 before_count,
240 after_count,
241 before_ends_nl,
242 after_ends_nl,
243 ) else {
244 continue;
245 };
246 if &other_body != body {
247 continue;
248 }
249 let (o, n) = unified_slice_starts(other);
250 keys.push(DuplicateSortKey {
251 old_start: o,
252 new_start: n,
253 is_peer: false,
254 index: j,
255 });
256 }
257 for (j, x) in peer_hunks.iter().enumerate() {
258 if hunk_body_digest(x) == *body {
259 keys.push(DuplicateSortKey {
260 old_start: peer_hunks[j].old_start,
261 new_start: peer_hunks[j].new_start,
262 is_peer: true,
263 index: j,
264 });
265 }
266 }
267 keys.sort();
268 keys.iter()
269 .position(|k| !k.is_peer && k.index == i)
270 .map(|p| p as u32)
271 })
272 .collect()
273}
274
275#[allow(clippy::too_many_arguments)]
276fn hash_hunk_id_from_unified_slice(
277 side: DiffSide,
278 path: &str,
279 slice: &str,
280 before_count: usize,
281 after_count: usize,
282 before_ends_nl: bool,
283 after_ends_nl: bool,
284 duplicate_rank: Option<u32>,
285) -> Option<HunkId> {
286 let mut hasher = HashAlgo::Sha1.checksum_hasher();
287 hasher.update(&[side.tag_byte()]);
288 hasher.update(path.as_bytes());
289 hasher.update(&[0]);
290 if !hash_hunk_body_from_unified_slice(
291 &mut hasher,
292 slice,
293 before_count,
294 after_count,
295 before_ends_nl,
296 after_ends_nl,
297 ) {
298 return None;
299 }
300 if let Some(rank) = duplicate_rank {
301 hasher.update(&rank.to_le_bytes());
302 }
303 let digest = hasher.finalize();
304 Some(HunkId::from_digest(&digest))
305}
306
307fn body_count_map_from_digests(own: &[Vec<u8>], peer: &[Vec<u8>]) -> HashMap<Vec<u8>, u32> {
308 let mut counts: HashMap<Vec<u8>, u32> = HashMap::new();
309 for body in own.iter().chain(peer.iter()) {
310 *counts.entry(body.clone()).or_insert(0) += 1;
311 }
312 counts
313}
314
315fn hash_hunk_body(hasher: &mut ChecksumHasher, hunk: &StructuredHunk) {
316 for line in &hunk.lines {
317 hash_line(hasher, line);
318 }
319}
320
321fn hash_line(hasher: &mut ChecksumHasher, line: &StructuredLine) {
322 let kind = match line.kind {
323 StructuredLineKind::Context => b' ',
324 StructuredLineKind::Add => b'+',
325 StructuredLineKind::Del => b'-',
326 };
327 hasher.update(&[kind]);
328 for seg in &line.segments {
329 hasher.update(seg.text.as_bytes());
330 }
331 hasher.update(&[if line.no_newline_at_eof { 1 } else { 0 }]);
332}
333
334fn hunk_body_digest(hunk: &StructuredHunk) -> Vec<u8> {
335 let mut hasher = HashAlgo::Sha1.checksum_hasher();
336 hash_hunk_body(&mut hasher, hunk);
337 hasher.finalize()
338}
339
340#[must_use]
342pub fn hunk_ids(side: DiffSide, path: &str, hunks: &[StructuredHunk]) -> Vec<HunkId> {
343 hunk_ids_with_duplicate_peers(side, path, hunks, &[])
344}
345
346#[must_use]
350pub fn peer_scope_includes_path(
351 display_path: &str,
352 old_path: Option<&str>,
353 primary: &HashSet<&str>,
354) -> bool {
355 if primary.contains(display_path) {
356 return true;
357 }
358 if let Some(old) = old_path {
359 if old != display_path && primary.contains(old) {
360 return true;
361 }
362 }
363 false
364}
365
366#[must_use]
368pub fn peer_hunks_by_primary_path(
369 primary: &HashSet<&str>,
370 peer_by_path: impl IntoIterator<Item = (String, Vec<StructuredHunk>)>,
371) -> HashMap<String, Vec<StructuredHunk>> {
372 peer_by_path
373 .into_iter()
374 .filter(|(path, _)| primary.contains(path.as_str()))
375 .collect()
376}
377
378#[must_use]
382pub fn hunk_ids_with_duplicate_peers(
383 side: DiffSide,
384 path: &str,
385 hunks: &[StructuredHunk],
386 peer_hunks: &[StructuredHunk],
387) -> Vec<HunkId> {
388 let ranks = duplicate_ranks(hunks, peer_hunks);
389 hunks
390 .iter()
391 .zip(ranks)
392 .map(|(h, rank)| hash_hunk_id(side, path, h, rank))
393 .collect()
394}
395
396#[must_use]
398#[allow(clippy::too_many_arguments)]
399pub(crate) fn hunk_ids_for_unified_slices(
400 side: DiffSide,
401 path: &str,
402 slices: &[&str],
403 before_count: usize,
404 after_count: usize,
405 before_ends_nl: bool,
406 after_ends_nl: bool,
407 peer_hunks: &[StructuredHunk],
408) -> Vec<HunkId> {
409 let ranks = duplicate_ranks_unified_slices(
410 slices,
411 before_count,
412 after_count,
413 before_ends_nl,
414 after_ends_nl,
415 peer_hunks,
416 );
417 slices
418 .iter()
419 .zip(ranks)
420 .filter_map(|(slice, rank)| {
421 hash_hunk_id_from_unified_slice(
422 side,
423 path,
424 slice,
425 before_count,
426 after_count,
427 before_ends_nl,
428 after_ends_nl,
429 rank,
430 )
431 })
432 .collect()
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::diff::structured::{StructuredLineKind, StructuredSegment};
439
440 fn line(kind: StructuredLineKind, text: &str) -> StructuredLine {
441 StructuredLine {
442 kind,
443 old: None,
444 new: None,
445 segments: vec![StructuredSegment {
446 text: text.to_owned(),
447 emphasis: false,
448 }],
449 no_newline_at_eof: false,
450 }
451 }
452
453 fn hunk(lines: Vec<StructuredLine>) -> StructuredHunk {
454 StructuredHunk {
455 old_start: 1,
456 new_start: 1,
457 old_lines: lines.len(),
458 new_lines: lines.len(),
459 context: None,
460 lines,
461 }
462 }
463
464 #[test]
465 fn hunk_id_stable_across_line_shift() {
466 let body = vec![
467 line(StructuredLineKind::Context, "ctx"),
468 line(StructuredLineKind::Add, "new"),
469 ];
470 let mut a = hunk(body.clone());
471 a.old_start = 10;
472 a.new_start = 10;
473 let mut b = hunk(body);
474 b.old_start = 20;
475 b.new_start = 22;
476 let ids_a = hunk_ids(DiffSide::Unstaged, "file.txt", &[a]);
477 let ids_b = hunk_ids(DiffSide::Unstaged, "file.txt", &[b]);
478 assert_eq!(ids_a, ids_b);
479 }
480
481 #[test]
482 fn hunk_id_changes_on_content_change() {
483 let a = hunk(vec![line(StructuredLineKind::Add, "one")]);
484 let b = hunk(vec![line(StructuredLineKind::Add, "two")]);
485 let ids_a = hunk_ids(DiffSide::Unstaged, "f", &[a]);
486 let ids_b = hunk_ids(DiffSide::Unstaged, "f", &[b]);
487 assert_ne!(ids_a[0], ids_b[0]);
488 }
489
490 #[test]
491 fn hunk_id_differs_by_side_and_path() {
492 let h = hunk(vec![line(StructuredLineKind::Add, "x")]);
493 let unstaged = hunk_ids(DiffSide::Unstaged, "a", &[h.clone()]);
494 let staged = hunk_ids(DiffSide::Staged, "a", &[h.clone()]);
495 let other_path = hunk_ids(DiffSide::Unstaged, "b", &[h]);
496 assert_ne!(unstaged[0], staged[0]);
497 assert_ne!(unstaged[0], other_path[0]);
498 }
499
500 #[test]
501 fn hunk_id_duplicate_hunks_disambiguated() {
502 let body = vec![line(StructuredLineKind::Add, "dup")];
503 let mut a = hunk(body.clone());
504 a.old_start = 1;
505 a.new_start = 1;
506 let mut b = hunk(body);
507 b.old_start = 5;
508 b.new_start = 5;
509 let ids = hunk_ids(DiffSide::Staged, "x", &[a, b]);
510 assert_ne!(ids[0], ids[1]);
511 }
512
513 #[test]
514 fn hunk_id_fromstr_rejects_uppercase() {
515 assert_eq!(HunkId::from_str("ABCDEF123456"), Err(HunkIdError::Invalid));
516 }
517
518 #[test]
519 fn hunk_id_stable_when_earlier_duplicate_on_peer_side() {
520 let body = vec![
521 line(StructuredLineKind::Context, "ctx"),
522 line(StructuredLineKind::Add, "same"),
523 ];
524 let mut first = hunk(body.clone());
525 first.old_start = 1;
526 first.new_start = 1;
527 let mut second = hunk(body);
528 second.old_start = 10;
529 second.new_start = 10;
530
531 let both = hunk_ids(DiffSide::Unstaged, "f", &[first.clone(), second.clone()]);
532 assert_ne!(both[0], both[1]);
533
534 let with_peer =
535 hunk_ids_with_duplicate_peers(DiffSide::Unstaged, "f", &[second.clone()], &[first]);
536 assert_eq!(with_peer[0], both[1]);
537 }
538
539 #[test]
540 fn hunk_id_stable_when_insertion_coordinates_shift_after_staging() {
541 let body = vec![line(StructuredLineKind::Add, "insert")];
542 let mut first = hunk(body.clone());
543 first.old_start = 19;
544 first.new_start = 19;
545 let mut second = hunk(body);
546 second.old_start = 19;
547 second.new_start = 20;
548
549 let both = hunk_ids(DiffSide::Unstaged, "f", &[first.clone(), second.clone()]);
550 assert_ne!(both[0], both[1]);
551 let id_later = both[1].clone();
552
553 let mut shifted = second;
554 shifted.old_start = 20;
555 shifted.new_start = 20;
556
557 let after = hunk_ids_with_duplicate_peers(DiffSide::Unstaged, "f", &[shifted], &[first]);
558 assert_eq!(after[0], id_later);
559 }
560
561 #[test]
562 fn hunk_ids_unified_slices_match_structured_hunks() {
563 use crate::diff::imara_unified_hunk_slices;
564 use crate::diff::unified_hunk_body_for_diff_options;
565 use crate::diff::DiffOptions;
566
567 let old = "a\nb\nc\n";
568 let new = "a\nB\nc\nd\n";
569 let opts = DiffOptions::default();
570 let body = unified_hunk_body_for_diff_options(old, new, &opts);
571 let slices: Vec<&str> = imara_unified_hunk_slices(&body);
572 let before_count = 3;
573 let after_count = 4;
574 let from_slices = hunk_ids_for_unified_slices(
575 DiffSide::Unstaged,
576 "f.txt",
577 &slices,
578 before_count,
579 after_count,
580 true,
581 true,
582 &[],
583 );
584 let structured: Vec<StructuredHunk> = slices
585 .iter()
586 .filter_map(|slice| {
587 crate::diff::structured::structured_hunk_for_id_from_unified_slice(
588 slice,
589 before_count,
590 after_count,
591 true,
592 true,
593 )
594 })
595 .collect();
596 let from_structured = hunk_ids(DiffSide::Unstaged, "f.txt", &structured);
597 assert_eq!(from_slices, from_structured);
598 }
599
600 #[test]
601 fn hunk_id_roundtrips_display_fromstr() {
602 let id = hunk_ids(
603 DiffSide::Other,
604 "p",
605 &[hunk(vec![line(StructuredLineKind::Del, "z")])],
606 )[0]
607 .clone();
608 let s = id.to_string();
609 assert_eq!(s.len(), HUNK_ID_HEX_LEN);
610 assert!(
611 s.bytes()
612 .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)),
613 "expected lowercase hex: {s}"
614 );
615 assert_eq!(HunkId::from_str(&s).expect("parse"), id);
616 }
617}