Skip to main content

git_policy_cli/index/
index_resolve_undo.rs

1//! What:
2//!  The `REUC` (resolve-undo) index extension: read, recorded into when an unmerged entry leaves the index, written back
3//!  (`resolve-undo.c`).
4//! Why:
5//!  Git remembers the three sides of a path whose conflict entries it removes so `git checkout -m` can restore them; an
6//!  in-process index update that removes or replaces unmerged entries must record them the way `record_resolve_undo` does.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! const undo = ResolveUndo.read(bytes, hashLength); undo.record(entry); const out = undo.write();
11//! ```
12
13use std::collections::BTreeMap;
14
15/// The three sides of one path: the mode and object name of stage 1, 2 and 3 (mode 0 means the side was absent).
16#[derive(Clone, Debug, Default, Eq, PartialEq)]
17pub struct Sides {
18    /// The mode of each stage, 0 for none.
19    pub mode: [u32; 3],
20    /// The object name of each stage, empty for none.
21    pub id: [Vec<u8>; 3],
22}
23
24/// The recorded paths in byte order of the path.
25#[derive(Clone, Debug, Default, Eq, PartialEq)]
26pub struct ResolveUndo {
27    /// Path to its sides.
28    pub paths: BTreeMap<Vec<u8>, Sides>,
29}
30
31/// The bytes before the first NUL and the bytes after it.
32fn split_nul(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
33    let nul: usize = bytes.iter().position(|byte| { return *byte == 0; })?;
34    return Some((&bytes[..nul], &bytes[nul + 1..]));
35}
36
37impl ResolveUndo {
38    /// The recorded paths of an extension's bytes, or `None` when Git reports invalid resolve-undo information.
39    pub fn read(bytes: &[u8], hash_length: usize) -> Option<ResolveUndo> {
40        let mut paths: BTreeMap<Vec<u8>, Sides> = BTreeMap::new();
41        let mut rest: &[u8] = bytes;
42        while !rest.is_empty() {
43            let (path, after) = split_nul(rest)?;
44            rest = after;
45            let mut sides: Sides = paths.remove(path).unwrap_or_default();
46            for mode in &mut sides.mode {
47                let (text, following) = split_nul(rest)?;
48                let digits: &str = std::str::from_utf8(text).ok()?;
49                // `strtoul` also takes a sign or a space; only the plain octal digits Git writes are read here.
50                if digits.is_empty() || !digits.bytes().all(|byte| { return (b'0'..=b'7').contains(&byte); }) {
51                    return None;
52                }
53                *mode = u32::from_str_radix(digits, 8).ok()?;
54                rest = following;
55            }
56            for (mode, id) in sides.mode.iter().zip(sides.id.iter_mut()) {
57                if *mode != 0 {
58                    *id = rest.get(..hash_length)?.to_vec();
59                    rest = &rest[hash_length..];
60                }
61            }
62            paths.insert(path.to_vec(), sides);
63        }
64        return Some(ResolveUndo { paths });
65    }
66
67    /// Remember one unmerged entry that is leaving the index (`record_resolve_undo`); a resolved entry is ignored.
68    pub fn record(&mut self, path: &[u8], stage: u8, mode: u32, id: &[u8]) {
69        if stage == 0 {
70            return;
71        }
72        let sides: &mut Sides = self.paths.entry(path.to_vec()).or_default();
73        sides.mode[usize::from(stage) - 1] = mode;
74        sides.id[usize::from(stage) - 1] = id.to_vec();
75    }
76
77    /// The extension's bytes (`resolve_undo_write`).
78    pub fn write(&self) -> Vec<u8> {
79        let mut out: Vec<u8> = Vec::new();
80        for (path, sides) in &self.paths {
81            out.extend_from_slice(path.as_slice());
82            out.push(0);
83            for mode in sides.mode {
84                out.extend_from_slice(format!("{mode:o}").as_bytes());
85                out.push(0);
86            }
87            for (mode, id) in sides.mode.iter().zip(sides.id.iter()) {
88                if *mode != 0 {
89                    out.extend_from_slice(id.as_slice());
90                }
91            }
92        }
93        return out;
94    }
95}
96
97#[cfg(test)]
98#[path = "index_resolve_undo_tests.rs"]
99mod tests;