objects/worktree/
worktree_compare.rs1use std::{collections::HashMap, fs, path::Path};
5
6use super::{worktree_ignore::should_ignore, worktree_types::WorktreeStatus};
7use crate::{
8 error::Result,
9 object::{Blob, EntryType, Tree, TreeEntry},
10 store::ObjectStore,
11};
12
13pub fn compare_worktree<S: ObjectStore + ?Sized>(
15 store: &S,
16 root: &Path,
17 tree: &Tree,
18 ignore_patterns: &[String],
19) -> Result<WorktreeStatus> {
20 let mut status = WorktreeStatus::default();
21 compare_worktree_recursive(
22 store,
23 root,
24 root,
25 Some(tree),
26 ignore_patterns,
27 false,
28 &mut status,
29 )?;
30
31 status.modified.sort();
32 status.added.sort();
33 status.deleted.sort();
34
35 Ok(status)
36}
37
38pub fn compare_stored_tree_entries<S: ObjectStore + ?Sized>(
41 store: &S,
42 root: &Path,
43 tree: &Tree,
44) -> Result<WorktreeStatus> {
45 let mut status = WorktreeStatus::default();
46 compare_worktree_recursive(store, root, root, Some(tree), &[], true, &mut status)?;
47 status.modified.sort();
48 status.deleted.sort();
49 Ok(status)
50}
51
52fn compare_worktree_recursive<S: ObjectStore + ?Sized>(
53 store: &S,
54 base: &Path,
55 dir: &Path,
56 tree: Option<&Tree>,
57 ignore_patterns: &[String],
58 stored_entries_only: bool,
59 status: &mut WorktreeStatus,
60) -> Result<()> {
61 let tree_entries: HashMap<&str, &TreeEntry> = tree
62 .map(|t| t.entries().iter().map(|e| (e.name(), e)).collect())
63 .unwrap_or_default();
64
65 let mut seen_entries: std::collections::HashSet<&str> = std::collections::HashSet::new();
66
67 if dir.exists() {
68 for entry in fs::read_dir(dir)? {
69 let entry = entry?;
70 let path = entry.path();
71 let name = match path.file_name().and_then(|n| n.to_str()) {
72 Some(n) => n,
73 None => continue,
74 };
75
76 let rel_path = path.strip_prefix(base).unwrap_or(&path);
77
78 if stored_entries_only {
79 if !tree_entries.contains_key(name) {
80 continue;
81 }
82 } else if should_ignore(rel_path, ignore_patterns) {
83 continue;
84 }
85
86 if let Some((tree_name, _)) = tree_entries.get_key_value(name) {
87 seen_entries.insert(*tree_name);
88 }
89
90 if path.is_symlink() {
91 let target = fs::read_link(&path)?;
92 let blob = Blob::new(crate::util::symlink_target_bytes(&target));
93 let hash = blob.hash();
94 match tree_entries.get(&name) {
95 Some(tree_entry) if tree_entry.entry_type() == EntryType::Symlink => {
96 if Some(hash) != tree_entry.symlink_hash() {
97 status.modified.push(rel_path.to_path_buf());
98 }
99 }
100 Some(_) => {
101 status.modified.push(rel_path.to_path_buf());
102 }
103 None => {
104 status.added.push(rel_path.to_path_buf());
105 }
106 }
107 } else {
108 let metadata = path.metadata()?;
109
110 if metadata.is_file() {
111 match tree_entries.get(&name) {
112 Some(tree_entry) if tree_entry.is_blob() => {
113 let content = fs::read(&path)?;
114 let blob = Blob::new(content);
115 let hash = blob.hash();
116
117 if Some(hash) != tree_entry.blob_hash() {
118 status.modified.push(rel_path.to_path_buf());
119 }
120 }
121 _ => {
122 status.added.push(rel_path.to_path_buf());
123 }
124 }
125 } else if metadata.is_dir() {
126 let subtree = match tree_entries.get(&name) {
127 Some(tree_entry) if tree_entry.is_tree() => tree_entry
128 .tree_hash()
129 .map(|hash| store.get_tree(&hash))
130 .transpose()?
131 .flatten(),
132 _ => None,
133 };
134
135 compare_worktree_recursive(
136 store,
137 base,
138 &path,
139 subtree.as_ref(),
140 ignore_patterns,
141 stored_entries_only,
142 status,
143 )?;
144 }
145 }
146 }
147 }
148
149 for (name, entry) in &tree_entries {
150 if !seen_entries.contains(name) {
151 let rel_path = dir.strip_prefix(base).unwrap_or(dir).join(name);
152
153 if matches!(
154 entry.entry_type(),
155 EntryType::Blob | EntryType::Gitlink | EntryType::Symlink
156 ) {
157 status.deleted.push(rel_path);
158 } else if let Some(tree_hash) = entry.tree_hash()
159 && let Some(subtree) = store.get_tree(&tree_hash)?
160 {
161 mark_all_deleted(store, &rel_path, &subtree, status)?;
162 }
163 }
164 }
165
166 Ok(())
167}
168
169fn mark_all_deleted<S: ObjectStore + ?Sized>(
170 store: &S,
171 prefix: &Path,
172 tree: &Tree,
173 status: &mut WorktreeStatus,
174) -> Result<()> {
175 for entry in tree.entries() {
176 let path = prefix.join(entry.name());
177
178 match entry.entry_type() {
179 EntryType::Blob => {
180 status.deleted.push(path);
181 }
182 EntryType::Tree => {
183 if let Some(tree_hash) = entry.tree_hash()
184 && let Some(subtree) = store.get_tree(&tree_hash)?
185 {
186 mark_all_deleted(store, &path, &subtree, status)?;
187 }
188 }
189 EntryType::Symlink | EntryType::Gitlink => {
190 status.deleted.push(path);
191 }
192 EntryType::Spoollink => {}
195 }
196 }
197 Ok(())
198}