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(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}