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<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(target.to_string_lossy().as_bytes().to_vec());
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}