Skip to main content

weavatrix_git/
status.rs

1use std::{collections::BTreeMap, fs, path::Path};
2
3use crate::{EntryKind, GitError, IndexEntry, ObjectKind, Repository, Result, error::invalid};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum StatusKind {
7    Unmodified,
8    Added,
9    Modified,
10    Deleted,
11    TypeChanged,
12    Unmerged,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct StatusEntry {
17    pub path: Vec<u8>,
18    pub index: StatusKind,
19    pub worktree: StatusKind,
20}
21
22#[derive(Clone, Copy)]
23struct HeadEntry {
24    mode: u32,
25    id: crate::ObjectId,
26}
27
28impl Repository {
29    pub fn status(&self) -> Result<Vec<StatusEntry>> {
30        let work_dir = self
31            .work_dir()
32            .ok_or_else(|| GitError::Unsupported("status in a bare repository".to_owned()))?;
33        let index = self.index_shared()?;
34        let mut head = self.head_entries()?;
35        let mut result = Vec::new();
36        for entry in index.entries() {
37            if entry.stage > 0 {
38                result.push(StatusEntry {
39                    path: entry.path.clone(),
40                    index: StatusKind::Unmerged,
41                    worktree: StatusKind::Unmerged,
42                });
43                continue;
44            }
45            let staged = staged(entry, head.remove(&entry.path));
46            let working = working(self, work_dir, entry)?;
47            if staged != StatusKind::Unmodified || working != StatusKind::Unmodified {
48                result.push(StatusEntry {
49                    path: entry.path.clone(),
50                    index: staged,
51                    worktree: working,
52                });
53            }
54        }
55        result.extend(head.into_keys().map(|path| StatusEntry {
56            path,
57            index: StatusKind::Deleted,
58            worktree: StatusKind::Unmodified,
59        }));
60        result.sort_unstable_by(|left, right| left.path.cmp(&right.path));
61        Ok(result)
62    }
63
64    fn head_entries(&self) -> Result<BTreeMap<Vec<u8>, HeadEntry>> {
65        let Some(head) = self.head()?.target else {
66            return Ok(BTreeMap::new());
67        };
68        let root = self.commit(head)?.tree;
69        let mut pending = vec![(Vec::new(), root, 0_usize)];
70        let mut result = BTreeMap::new();
71        while let Some((prefix, tree, depth)) = pending.pop() {
72            if depth > self.limits().max_tree_depth {
73                return Err(GitError::LimitExceeded {
74                    resource: "status tree depth",
75                    limit: self.limits().max_tree_depth,
76                });
77            }
78            for entry in self.tree(tree)?.entries {
79                let mut path = prefix.clone();
80                if !path.is_empty() {
81                    path.push(b'/');
82                }
83                path.extend(&entry.name);
84                if entry.kind == EntryKind::Tree {
85                    pending.push((path, entry.id, depth + 1));
86                } else {
87                    result.insert(
88                        path,
89                        HeadEntry {
90                            mode: entry.mode,
91                            id: entry.id,
92                        },
93                    );
94                }
95            }
96        }
97        Ok(result)
98    }
99}
100
101fn staged(index: &IndexEntry, head: Option<HeadEntry>) -> StatusKind {
102    let Some(head) = head else {
103        return StatusKind::Added;
104    };
105    if mode_kind(index.mode) != mode_kind(head.mode) {
106        StatusKind::TypeChanged
107    } else if index.id != head.id || index.mode != head.mode {
108        StatusKind::Modified
109    } else {
110        StatusKind::Unmodified
111    }
112}
113
114fn working(repository: &Repository, root: &Path, entry: &IndexEntry) -> Result<StatusKind> {
115    if entry.skip_worktree || entry.intent_to_add {
116        return Ok(StatusKind::Unmodified);
117    }
118    let path_text = std::str::from_utf8(&entry.path)
119        .map_err(|_| GitError::Unsupported("non-UTF-8 status path".to_owned()))?;
120    let path = root.join(path_text);
121    let metadata = match fs::symlink_metadata(&path) {
122        Ok(metadata) => metadata,
123        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
124            return Ok(StatusKind::Deleted);
125        }
126        Err(error) => return Err(error.into()),
127    };
128    match mode_kind(entry.mode) {
129        0o100_000 if metadata.is_file() => compare_file(repository, &path, entry),
130        0o120_000 if metadata.file_type().is_symlink() => {
131            let target = fs::read_link(path)?;
132            let target = target
133                .to_str()
134                .ok_or_else(|| GitError::Unsupported("non-UTF-8 symlink target".to_owned()))?;
135            compare_bytes(repository, target.as_bytes(), entry)
136        }
137        0o160_000 if metadata.is_dir() => Err(GitError::Unsupported(
138            "submodule worktree status".to_owned(),
139        )),
140        _ => Ok(StatusKind::TypeChanged),
141    }
142}
143
144fn compare_file(repository: &Repository, path: &Path, entry: &IndexEntry) -> Result<StatusKind> {
145    let length = usize::try_from(fs::metadata(path)?.len())
146        .map_err(|_| invalid("working file length overflow"))?;
147    if length > repository.limits().max_object_bytes {
148        return Err(GitError::LimitExceeded {
149            resource: "status file bytes",
150            limit: repository.limits().max_object_bytes,
151        });
152    }
153    compare_bytes(repository, &fs::read(path)?, entry)
154}
155
156fn compare_bytes(repository: &Repository, actual: &[u8], entry: &IndexEntry) -> Result<StatusKind> {
157    let object = repository.object(entry.id)?;
158    if object.kind != ObjectKind::Blob {
159        return Err(invalid("index entry does not reference a blob"));
160    }
161    Ok(if actual == object.data {
162        StatusKind::Unmodified
163    } else {
164        StatusKind::Modified
165    })
166}
167
168const fn mode_kind(mode: u32) -> u32 {
169    mode & 0o170_000
170}