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<String, &TreeEntry> = tree
39 .map(|t| t.entries().iter().map(|e| (e.name.clone(), e)).collect())
40 .unwrap_or_default();
41
42 let mut seen_entries: std::collections::HashSet<String> = 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.to_string(),
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 seen_entries.insert(name.clone());
60
61 if path.is_symlink() {
62 let target = fs::read_link(&path)?;
63 let blob = Blob::new(crate::util::symlink_target_bytes(&target));
64 let hash = blob.hash();
65 match tree_entries.get(&name) {
66 Some(tree_entry) if tree_entry.entry_type == EntryType::Symlink => {
67 if hash != tree_entry.hash {
68 status.modified.push(rel_path.to_path_buf());
69 }
70 }
71 Some(_) => {
72 status.modified.push(rel_path.to_path_buf());
73 }
74 None => {
75 status.added.push(rel_path.to_path_buf());
76 }
77 }
78 } else {
79 let metadata = path.metadata()?;
80
81 if metadata.is_file() {
82 match tree_entries.get(&name) {
83 Some(tree_entry) if tree_entry.is_blob() => {
84 let content = fs::read(&path)?;
85 let blob = Blob::new(content);
86 let hash = blob.hash();
87
88 if hash != tree_entry.hash {
89 status.modified.push(rel_path.to_path_buf());
90 }
91 }
92 _ => {
93 status.added.push(rel_path.to_path_buf());
94 }
95 }
96 } else if metadata.is_dir() {
97 let subtree = match tree_entries.get(&name) {
98 Some(tree_entry) if tree_entry.is_tree() => {
99 store.get_tree(&tree_entry.hash)?
100 }
101 _ => None,
102 };
103
104 compare_worktree_recursive(
105 store,
106 base,
107 &path,
108 subtree.as_ref(),
109 ignore_patterns,
110 status,
111 )?;
112 }
113 }
114 }
115 }
116
117 for (name, entry) in &tree_entries {
118 if !seen_entries.contains(name) {
119 let rel_path = dir.strip_prefix(base).unwrap_or(dir).join(name);
120
121 if entry.entry_type == EntryType::Blob {
122 status.deleted.push(rel_path);
123 } else if entry.entry_type == EntryType::Tree
124 && let Some(subtree) = store.get_tree(&entry.hash)?
125 {
126 mark_all_deleted(store, &rel_path, &subtree, status)?;
127 }
128 }
129 }
130
131 Ok(())
132}
133
134fn mark_all_deleted<S: ObjectStore + ?Sized>(
135 store: &S,
136 prefix: &Path,
137 tree: &Tree,
138 status: &mut WorktreeStatus,
139) -> Result<()> {
140 for entry in tree.entries() {
141 let path = prefix.join(&entry.name);
142
143 match entry.entry_type {
144 EntryType::Blob => {
145 status.deleted.push(path);
146 }
147 EntryType::Tree => {
148 if let Some(subtree) = store.get_tree(&entry.hash)? {
149 mark_all_deleted(store, &path, &subtree, status)?;
150 }
151 }
152 EntryType::Symlink => {
153 status.deleted.push(path);
154 }
155 }
156 }
157 Ok(())
158}