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(store, root, root, Some(tree), ignore_patterns, &mut status)?;
22
23 status.modified.sort();
24 status.added.sort();
25 status.deleted.sort();
26
27 Ok(status)
28}
29
30fn compare_worktree_recursive<S: ObjectStore + ?Sized>(
31 store: &S,
32 base: &Path,
33 dir: &Path,
34 tree: Option<&Tree>,
35 ignore_patterns: &[String],
36 status: &mut WorktreeStatus,
37) -> Result<()> {
38 let tree_entries: HashMap<&str, &TreeEntry> = tree
39 .map(|t| t.entries().iter().map(|e| (e.name(), e)).collect())
40 .unwrap_or_default();
41
42 let mut seen_entries: std::collections::HashSet<&str> = std::collections::HashSet::new();
43
44 if dir.exists() {
45 for entry in fs::read_dir(dir)? {
46 let entry = entry?;
47 let path = entry.path();
48 let name = match path.file_name().and_then(|n| n.to_str()) {
49 Some(n) => n,
50 None => continue,
51 };
52
53 let rel_path = path.strip_prefix(base).unwrap_or(&path);
54
55 if should_ignore(rel_path, ignore_patterns) {
56 continue;
57 }
58
59 if let Some((tree_name, _)) = tree_entries.get_key_value(name) {
60 seen_entries.insert(*tree_name);
61 }
62
63 if path.is_symlink() {
64 let target = fs::read_link(&path)?;
65 let blob = Blob::new(crate::util::symlink_target_bytes(&target));
66 let hash = blob.hash();
67 match tree_entries.get(&name) {
68 Some(tree_entry) if tree_entry.entry_type() == EntryType::Symlink => {
69 if Some(hash) != tree_entry.symlink_hash() {
70 status.modified.push(rel_path.to_path_buf());
71 }
72 }
73 Some(_) => {
74 status.modified.push(rel_path.to_path_buf());
75 }
76 None => {
77 status.added.push(rel_path.to_path_buf());
78 }
79 }
80 } else {
81 let metadata = path.metadata()?;
82
83 if metadata.is_file() {
84 match tree_entries.get(&name) {
85 Some(tree_entry) if tree_entry.is_blob() => {
86 let content = fs::read(&path)?;
87 let blob = Blob::new(content);
88 let hash = blob.hash();
89
90 if Some(hash) != tree_entry.blob_hash() {
91 status.modified.push(rel_path.to_path_buf());
92 }
93 }
94 _ => {
95 status.added.push(rel_path.to_path_buf());
96 }
97 }
98 } else if metadata.is_dir() {
99 let subtree = match tree_entries.get(&name) {
100 Some(tree_entry) if tree_entry.is_tree() => tree_entry
101 .tree_hash()
102 .map(|hash| store.get_tree(&hash))
103 .transpose()?
104 .flatten(),
105 _ => None,
106 };
107
108 compare_worktree_recursive(
109 store,
110 base,
111 &path,
112 subtree.as_ref(),
113 ignore_patterns,
114 status,
115 )?;
116 }
117 }
118 }
119 }
120
121 for (name, entry) in &tree_entries {
122 if !seen_entries.contains(name) {
123 let rel_path = dir.strip_prefix(base).unwrap_or(dir).join(name);
124
125 if entry.entry_type() == EntryType::Blob || entry.entry_type() == EntryType::Gitlink {
126 status.deleted.push(rel_path);
127 } else if let Some(tree_hash) = entry.tree_hash()
128 && let Some(subtree) = store.get_tree(&tree_hash)?
129 {
130 mark_all_deleted(store, &rel_path, &subtree, status)?;
131 }
132 }
133 }
134
135 Ok(())
136}
137
138fn mark_all_deleted<S: ObjectStore + ?Sized>(
139 store: &S,
140 prefix: &Path,
141 tree: &Tree,
142 status: &mut WorktreeStatus,
143) -> Result<()> {
144 for entry in tree.entries() {
145 let path = prefix.join(entry.name());
146
147 match entry.entry_type() {
148 EntryType::Blob => {
149 status.deleted.push(path);
150 }
151 EntryType::Tree => {
152 if let Some(tree_hash) = entry.tree_hash()
153 && let Some(subtree) = store.get_tree(&tree_hash)?
154 {
155 mark_all_deleted(store, &path, &subtree, status)?;
156 }
157 }
158 EntryType::Symlink | EntryType::Gitlink => {
159 status.deleted.push(path);
160 }
161 }
162 }
163 Ok(())
164}