1use super::subvolume_index::{IndexOutcome, RewriteContext, StatFields, StoredStat, rewrite_index_stat_with};
18use super::subvolume_lock::is_ours;
19use std::ffi::OsStr;
20use std::io::Write;
21use std::os::unix::ffi::OsStrExt;
22use std::os::unix::fs::MetadataExt;
23use std::path::{Path, PathBuf};
24
25#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct LinkedWorktree {
36 pub admin_directory: PathBuf,
38 pub worktree: PathBuf,
40}
41
42pub fn linked_worktrees_inside(root: &Path) -> Vec<LinkedWorktree> {
53 let admin: PathBuf = root.join(".git").join("worktrees");
54 let Ok(names) = std::fs::read_dir(&admin) else { return Vec::new() };
55 let mut found: Vec<LinkedWorktree> = Vec::new();
56 for name in names.flatten() {
57 let Ok(recorded) = std::fs::read(name.path().join("gitdir")) else { continue };
58 let Some(worktree) = Path::new(OsStr::from_bytes(recorded.trim_ascii())).parent() else { continue };
60 if worktree.starts_with(root) && worktree != root && worktree.is_dir() {
61 found.push(LinkedWorktree { admin_directory: name.path(), worktree: worktree.to_path_buf() });
62 }
63 }
64 found.sort_by(|left: &LinkedWorktree, right: &LinkedWorktree| { return left.worktree.cmp(&right.worktree); });
65 return found;
66}
67
68fn stat_fields(tree: &Path, path: &[u8]) -> Option<StatFields> {
78 let metadata: std::fs::Metadata = std::fs::symlink_metadata(tree.join(OsStr::from_bytes(path))).ok()?;
79 return Some(StatFields {
80 ctime_seconds: metadata.ctime() as u32,
81 ctime_nanos: metadata.ctime_nsec() as u32,
82 mtime_seconds: metadata.mtime() as u32,
83 mtime_nanos: metadata.mtime_nsec() as u32,
84 dev: metadata.dev() as u32,
85 ino: metadata.ino() as u32,
86 uid: metadata.uid(),
87 gid: metadata.gid(),
88 size: metadata.size() as u32,
89 });
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub enum IndexLock<'a> {
106 Acquire,
108 Held(&'a str),
110}
111
112#[derive(Clone, Copy, Debug)]
125pub struct RefreshOptions<'a> {
126 pub lock: IndexLock<'a>,
128 pub references: &'a [PathBuf],
131 pub hash_length: Option<usize>,
133}
134
135fn matches_stored(now: &StatFields, stored: &StoredStat) -> bool {
137 return now.size == stored.size
138 && now.ino == stored.ino
139 && now.uid == stored.uid
140 && now.gid == stored.gid
141 && now.ctime_seconds == stored.ctime_seconds
142 && (stored.ctime_nanos == 0 || now.ctime_nanos == stored.ctime_nanos)
143 && now.mtime_seconds == stored.mtime_seconds
144 && (stored.mtime_nanos == 0 || now.mtime_nanos == stored.mtime_nanos);
145}
146
147pub fn refresh_index_file_with(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>) -> String {
159 let mut lock_name: std::ffi::OsString = index_path.as_os_str().to_os_string();
160 lock_name.push(".lock");
161 let lock_path: PathBuf = PathBuf::from(lock_name);
162 let mut created: Option<std::fs::File> = None;
163 match options.lock {
164 IndexLock::Acquire => match std::fs::OpenOptions::new().write(true).create_new(true).open(&lock_path) {
165 Ok(opened) => created = Some(opened),
166 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
167 return String::from("skipped (index.lock exists: a Git command is running there; its first status will be slow)");
168 }
169 Err(error) => return format!("skipped (cannot create the lock: {error})"),
170 },
171 IndexLock::Held(token) => {
172 if !is_ours(lock_path.as_path(), token) {
173 return String::from("skipped (index.lock is held by another Git command; its first status will be slow)");
174 }
175 }
176 }
177 let outcome: String = rewrite_under_lock(index_path, tree, options, created.as_mut(), &lock_path);
178 if created.is_some() {
179 let _removed = std::fs::remove_file(&lock_path);
180 }
181 return outcome;
182}
183
184fn rewrite_under_lock(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, lock_file: Option<&mut std::fs::File>, lock_path: &Path) -> String {
186 let index_mtime: Option<(u32, u32)> = match std::fs::metadata(index_path) {
187 Ok(metadata) => Some((metadata.mtime() as u32, metadata.mtime_nsec() as u32)),
188 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return String::from("no index file"),
189 Err(error) => return format!("skipped (cannot examine it: {error})"),
190 };
191 let bytes: Vec<u8> = match std::fs::read(index_path) {
192 Ok(found) => found,
193 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return String::from("no index file"),
194 Err(error) => return format!("skipped (cannot read it: {error})"),
195 };
196 let context: RewriteContext = RewriteContext { hash_length: options.hash_length, index_mtime };
197 let outcome: IndexOutcome = rewrite_index_stat_with(
198 bytes.as_slice(),
199 &context,
200 &mut |path: &[u8]| { return stat_fields(tree, path); },
201 &mut |path: &[u8], stored: &StoredStat| {
202 if options.references.is_empty() {
203 return true;
204 }
205 return options.references.iter().any(|reference: &PathBuf| {
206 return stat_fields(reference.as_path(), path).is_some_and(|now: StatFields| { return matches_stored(&now, stored); });
207 });
208 },
209 );
210 let (rewritten, updated, total): (Vec<u8>, usize, usize) = match outcome {
211 IndexOutcome::Rewritten { bytes: new_bytes, updated, total } => (new_bytes, updated, total),
212 IndexOutcome::Skipped(reason) => return format!("skipped ({reason})"),
213 };
214 let installed: std::io::Result<()> = match lock_file {
217 Some(file) => file.write_all(rewritten.as_slice()).and_then(|()| { return file.sync_all(); }).and_then(|()| { return std::fs::rename(lock_path, index_path); }),
218 None => {
219 let mut sibling_name: std::ffi::OsString = index_path.as_os_str().to_os_string();
220 sibling_name.push(".cli-git-new");
221 let sibling: PathBuf = PathBuf::from(sibling_name);
222 let written: std::io::Result<()> = std::fs::write(&sibling, rewritten.as_slice())
223 .and_then(|()| { return std::fs::File::open(&sibling); })
224 .and_then(|file: std::fs::File| { return file.sync_all(); })
225 .and_then(|()| { return std::fs::rename(&sibling, index_path); });
226 if written.is_err() {
227 let _removed = std::fs::remove_file(&sibling);
228 }
229 written
230 }
231 };
232 if let Err(error) = installed {
233 return format!("skipped (cannot install the new index: {error})");
234 }
235 return format!("{updated} of {total} entries refreshed");
236}
237
238pub fn refresh_index_file_locked(index_path: &Path, tree: &Path, lock: IndexLock<'_>) -> String {
248 return refresh_index_file_with(index_path, tree, &RefreshOptions { lock, references: &[], hash_length: None });
249}
250
251pub fn refresh_index_file(index_path: &Path, tree: &Path) -> String {
261 return refresh_index_file_locked(index_path, tree, IndexLock::Acquire);
262}
263
264pub fn repository_hash_length(git_directory: &Path) -> Option<usize> {
276 let text: String = String::from_utf8_lossy(std::fs::read(git_directory.join("config")).ok()?.as_slice()).into_owned();
277 let mut in_extensions: bool = false;
278 for raw in text.lines() {
279 let line: &str = raw.trim();
280 if let Some(header) = line.strip_prefix('[') {
281 in_extensions = header.split(']').next().is_some_and(|name: &str| { return name.trim().eq_ignore_ascii_case("extensions"); });
282 continue;
283 }
284 if !in_extensions {
285 continue;
286 }
287 let Some((key, value)) = line.split_once('=') else { continue };
288 if key.trim().eq_ignore_ascii_case("objectformat") && value.trim().eq_ignore_ascii_case("sha256") {
289 return Some(32);
290 }
291 }
292 return Some(20);
293}
294
295pub fn refresh_all_indexes(root: &Path, lock: IndexLock<'_>, references: &[PathBuf], say: &mut dyn FnMut(&str)) -> usize {
308 let hash_length: Option<usize> = repository_hash_length(root.join(".git").as_path());
309 let main_index: PathBuf = root.join(".git").join("index");
310 let main_options: RefreshOptions<'_> = RefreshOptions { lock, references, hash_length };
311 say(format!("index {}: {}", main_index.display(), refresh_index_file_with(main_index.as_path(), root, &main_options)).as_str());
312 let mut count: usize = 1;
313 for linked in linked_worktrees_inside(root) {
314 let index: PathBuf = linked.admin_directory.join("index");
315 let relative: &Path = linked.worktree.strip_prefix(root).unwrap_or(Path::new(""));
316 let inside: Vec<PathBuf> = references.iter().map(|reference: &PathBuf| { return reference.join(relative); }).collect();
317 let options: RefreshOptions<'_> = RefreshOptions { lock, references: inside.as_slice(), hash_length };
318 say(format!("index {}: {}", index.display(), refresh_index_file_with(index.as_path(), linked.worktree.as_path(), &options)).as_str());
319 count += 1;
320 }
321 return count;
322}
323
324#[cfg(test)]
326#[path = "subvolume_index_files_tests.rs"]
327mod tests;