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.require_tree(&hash))
130 .transpose()?,
131 _ => None,
132 };
133
134 compare_worktree_recursive(
135 store,
136 base,
137 &path,
138 subtree.as_ref(),
139 ignore_patterns,
140 stored_entries_only,
141 status,
142 )?;
143 }
144 }
145 }
146 }
147
148 for (name, entry) in &tree_entries {
149 if !seen_entries.contains(name) {
150 let rel_path = dir.strip_prefix(base).unwrap_or(dir).join(name);
151
152 if matches!(
153 entry.entry_type(),
154 EntryType::Blob | EntryType::Gitlink | EntryType::Symlink
155 ) {
156 status.deleted.push(rel_path);
157 } else if let Some(tree_hash) = entry.tree_hash() {
158 let subtree = store.require_tree(&tree_hash)?;
159 mark_all_deleted(store, &rel_path, &subtree, status)?;
160 }
161 }
162 }
163
164 Ok(())
165}
166
167fn mark_all_deleted<S: ObjectStore + ?Sized>(
168 store: &S,
169 prefix: &Path,
170 tree: &Tree,
171 status: &mut WorktreeStatus,
172) -> Result<()> {
173 for entry in tree.entries() {
174 let path = prefix.join(entry.name());
175
176 match entry.entry_type() {
177 EntryType::Blob => {
178 status.deleted.push(path);
179 }
180 EntryType::Tree => {
181 if let Some(tree_hash) = entry.tree_hash() {
182 let subtree = store.require_tree(&tree_hash)?;
183 mark_all_deleted(store, &path, &subtree, status)?;
184 }
185 }
186 EntryType::Symlink | EntryType::Gitlink => {
187 status.deleted.push(path);
188 }
189 EntryType::Spoollink => {}
192 }
193 }
194 Ok(())
195}