Skip to main content

objects/worktree/
worktree_compare.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Worktree comparison logic.
3
4use 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
13/// Compare worktree against a tree.
14pub 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
38/// Compare every stored entry with the evaluated checkout, including entries
39/// now hidden by ignore rules. Unstored files do not affect this comparison.
40pub 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            // Native child-spool edge: not a filesystem leaf, so there is
193            // nothing on disk to report as deleted.
194            EntryType::Spoollink => {}
195        }
196    }
197    Ok(())
198}