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 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}