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}