Skip to main content

claw_store/
tree_diff.rs

1use std::collections::BTreeMap;
2
3use claw_core::id::ObjectId;
4use claw_core::object::Object;
5use claw_core::types::FileMode;
6
7use crate::ClawStore;
8use crate::StoreError;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ChangeKind {
12    Added,
13    Deleted,
14    Modified,
15    TypeChanged,
16}
17
18#[derive(Debug, Clone)]
19pub struct TreeChange {
20    pub path: String,
21    pub kind: ChangeKind,
22    pub old_id: Option<ObjectId>,
23    pub new_id: Option<ObjectId>,
24    pub old_mode: Option<FileMode>,
25    pub new_mode: Option<FileMode>,
26}
27
28pub fn diff_trees(
29    store: &ClawStore,
30    old_tree: Option<&ObjectId>,
31    new_tree: Option<&ObjectId>,
32    prefix: &str,
33) -> Result<Vec<TreeChange>, StoreError> {
34    let old_entries = match old_tree {
35        Some(id) => flatten_tree_entries(store, id)?,
36        None => BTreeMap::new(),
37    };
38    let new_entries = match new_tree {
39        Some(id) => flatten_tree_entries(store, id)?,
40        None => BTreeMap::new(),
41    };
42
43    let mut changes = Vec::new();
44
45    let mut all_names: Vec<&String> = old_entries.keys().chain(new_entries.keys()).collect();
46    all_names.sort();
47    all_names.dedup();
48
49    for name in all_names {
50        let full_path = if prefix.is_empty() {
51            name.clone()
52        } else {
53            format!("{}/{}", prefix, name)
54        };
55
56        match (old_entries.get(name), new_entries.get(name)) {
57            (None, Some((new_id, new_mode))) => {
58                if *new_mode == FileMode::Directory {
59                    let sub_changes = diff_trees(store, None, Some(new_id), &full_path)?;
60                    changes.extend(sub_changes);
61                } else {
62                    changes.push(TreeChange {
63                        path: full_path,
64                        kind: ChangeKind::Added,
65                        old_id: None,
66                        new_id: Some(*new_id),
67                        old_mode: None,
68                        new_mode: Some(*new_mode),
69                    });
70                }
71            }
72            (Some((old_id, old_mode)), None) => {
73                if *old_mode == FileMode::Directory {
74                    let sub_changes = diff_trees(store, Some(old_id), None, &full_path)?;
75                    changes.extend(sub_changes);
76                } else {
77                    changes.push(TreeChange {
78                        path: full_path,
79                        kind: ChangeKind::Deleted,
80                        old_id: Some(*old_id),
81                        new_id: None,
82                        old_mode: Some(*old_mode),
83                        new_mode: None,
84                    });
85                }
86            }
87            (Some((old_id, old_mode)), Some((new_id, new_mode))) => {
88                if old_mode != new_mode
89                    && (*old_mode == FileMode::Directory || *new_mode == FileMode::Directory)
90                {
91                    // Type changed between file and directory
92                    if *old_mode == FileMode::Directory {
93                        let sub_changes = diff_trees(store, Some(old_id), None, &full_path)?;
94                        changes.extend(sub_changes);
95                    } else {
96                        changes.push(TreeChange {
97                            path: full_path.clone(),
98                            kind: ChangeKind::Deleted,
99                            old_id: Some(*old_id),
100                            new_id: None,
101                            old_mode: Some(*old_mode),
102                            new_mode: None,
103                        });
104                    }
105                    if *new_mode == FileMode::Directory {
106                        let sub_changes = diff_trees(store, None, Some(new_id), &full_path)?;
107                        changes.extend(sub_changes);
108                    } else {
109                        changes.push(TreeChange {
110                            path: full_path,
111                            kind: ChangeKind::Added,
112                            old_id: None,
113                            new_id: Some(*new_id),
114                            old_mode: None,
115                            new_mode: Some(*new_mode),
116                        });
117                    }
118                } else if *old_mode == FileMode::Directory && *new_mode == FileMode::Directory {
119                    if old_id != new_id {
120                        let sub_changes =
121                            diff_trees(store, Some(old_id), Some(new_id), &full_path)?;
122                        changes.extend(sub_changes);
123                    }
124                } else if old_id != new_id {
125                    changes.push(TreeChange {
126                        path: full_path,
127                        kind: if old_mode != new_mode {
128                            ChangeKind::TypeChanged
129                        } else {
130                            ChangeKind::Modified
131                        },
132                        old_id: Some(*old_id),
133                        new_id: Some(*new_id),
134                        old_mode: Some(*old_mode),
135                        new_mode: Some(*new_mode),
136                    });
137                } else if old_mode != new_mode {
138                    changes.push(TreeChange {
139                        path: full_path,
140                        kind: ChangeKind::TypeChanged,
141                        old_id: Some(*old_id),
142                        new_id: Some(*new_id),
143                        old_mode: Some(*old_mode),
144                        new_mode: Some(*new_mode),
145                    });
146                }
147            }
148            (None, None) => unreachable!(),
149        }
150    }
151
152    Ok(changes)
153}
154
155fn flatten_tree_entries(
156    store: &ClawStore,
157    tree_id: &ObjectId,
158) -> Result<BTreeMap<String, (ObjectId, FileMode)>, StoreError> {
159    let obj = store.load_object(tree_id)?;
160    let tree = match obj {
161        Object::Tree(t) => t,
162        _ => return Ok(BTreeMap::new()),
163    };
164    let mut map = BTreeMap::new();
165    for entry in tree.entries {
166        map.insert(entry.name, (entry.object_id, entry.mode));
167    }
168    Ok(map)
169}