Skip to main content

grit_lib/
resolve_undo.rs

1//! Git index `REUC` (resolve-undo) extension — records unmerged stages when a conflict is resolved.
2//!
3//! Format matches Git's `resolve-undo.c`: for each path, NUL-terminated name, three octal modes with
4//! NUL terminators, then raw SHA-1s for non-zero modes.
5
6use std::collections::BTreeMap;
7
8use crate::error::{Error, Result};
9use crate::index::IndexEntry;
10use crate::objects::ObjectId;
11
12/// Per-path undo data: up to three conflict stages (index 0 = stage 1).
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ResolveUndoRecord {
15    /// File modes for stages 1–3 (`0` means absent).
16    pub modes: [u32; 3],
17    /// Blob OIDs for stages 1–3 (only meaningful when `modes[i] != 0`).
18    pub oids: [ObjectId; 3],
19}
20
21impl Default for ResolveUndoRecord {
22    fn default() -> Self {
23        Self {
24            modes: [0; 3],
25            oids: [ObjectId::zero(); 3],
26        }
27    }
28}
29
30/// Parse the `REUC` extension payload into a path → record map.
31pub fn parse_resolve_undo_payload(data: &[u8]) -> Result<BTreeMap<Vec<u8>, ResolveUndoRecord>> {
32    let mut out: BTreeMap<Vec<u8>, ResolveUndoRecord> = BTreeMap::new();
33    let mut pos = 0usize;
34    while pos < data.len() {
35        let Some(nul) = data[pos..].iter().position(|&b| b == 0) else {
36            return Err(Error::IndexError("truncated resolve-undo path".to_owned()));
37        };
38        let path = data[pos..pos + nul].to_vec();
39        pos += nul + 1;
40        let mut modes = [0u32; 3];
41        for m in &mut modes {
42            let Some(term) = data[pos..].iter().position(|&b| b == 0) else {
43                return Err(Error::IndexError("truncated resolve-undo mode".to_owned()));
44            };
45            let slice = &data[pos..pos + term];
46            let s = std::str::from_utf8(slice)
47                .map_err(|_| Error::IndexError("invalid UTF-8 in resolve-undo mode".to_owned()))?;
48            *m = u32::from_str_radix(s, 8)
49                .map_err(|_| Error::IndexError(format!("invalid resolve-undo mode '{s}'")))?;
50            pos += term + 1;
51        }
52        let mut oids = [ObjectId::zero(); 3];
53        for i in 0..3 {
54            if modes[i] == 0 {
55                continue;
56            }
57            if pos + 20 > data.len() {
58                return Err(Error::IndexError(
59                    "truncated resolve-undo object id".to_owned(),
60                ));
61            }
62            oids[i] = ObjectId::from_bytes(&data[pos..pos + 20])
63                .map_err(|e| Error::IndexError(e.to_string()))?;
64            pos += 20;
65        }
66        out.insert(path, ResolveUndoRecord { modes, oids });
67    }
68    Ok(out)
69}
70
71/// Serialise resolve-undo records to the `REUC` extension body (sorted by path).
72pub fn write_resolve_undo_payload(map: &BTreeMap<Vec<u8>, ResolveUndoRecord>) -> Vec<u8> {
73    let mut sb = Vec::new();
74    for (path, ru) in map {
75        if !ru.modes.iter().any(|m| *m != 0) {
76            continue;
77        }
78        sb.extend_from_slice(path);
79        sb.push(0);
80        for m in &ru.modes {
81            let s = format!("{:o}", m);
82            sb.extend_from_slice(s.as_bytes());
83            sb.push(0);
84        }
85        for i in 0..3 {
86            if ru.modes[i] != 0 {
87                sb.extend_from_slice(ru.oids[i].as_bytes());
88            }
89        }
90    }
91    sb
92}
93
94/// Merge one unmerged index entry into the resolve-undo map for its path.
95pub fn record_resolve_undo_for_entry(
96    map: &mut Option<BTreeMap<Vec<u8>, ResolveUndoRecord>>,
97    entry: &IndexEntry,
98) {
99    let stage = entry.stage();
100    if stage == 0 || stage > 3 {
101        return;
102    }
103    let idx = (stage - 1) as usize;
104    let m = map.get_or_insert_with(BTreeMap::new);
105    let ru = m.entry(entry.path.clone()).or_default();
106    ru.modes[idx] = entry.mode;
107    ru.oids[idx] = entry.oid;
108}