use super::repo_read_index::{IndexEntry, parse_index};
use super::repo_read_layout::ObjectFormat;
use super::repo_read_odb::ObjectDatabase;
use super::repo_read_refs::{Refs, RefValue};
use super::repo_read_tree::{FlatEntry, MODE_GITLINK, flatten_tree, parse_commit};
use super::subvolume_index::{StatFields, StoredStat, be32, walk_entries};
use super::subvolume_index_files::{fields_of, matches_stored, repository_hash_length};
use super::transaction_git::{GitContext, GitRequest, run_git};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
const IN_PROGRESS: &[&str] =
&["index.lock", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "BISECT_LOG", "rebase-merge", "rebase-apply", "sequencer"];
const ASSUME_VALID: u16 = 0x8000;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StatEntry {
pub path: Vec<u8>,
pub flags: u16,
pub mode: u32,
pub stored: StoredStat,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CleanSource {
pub head_commit: String,
pub head_tree: Vec<u8>,
pub tracked: usize,
pub index_seconds: u32,
pub hash_length: usize,
pub attribute_blobs: Vec<(Vec<u8>, Vec<u8>)>,
pub index_bytes: Vec<u8>,
}
fn operation_in_progress(git_dir: &Path) -> Option<&'static str> {
return IN_PROGRESS.iter().copied().find(|name: &&str| { return git_dir.join(name).symlink_metadata().is_ok(); });
}
pub fn head_commit(git_dir: &Path, common_dir: &Path, format: ObjectFormat) -> Option<String> {
let refs: Refs = Refs::new(common_dir, format.hex_length());
return match refs.lookup_head(git_dir)? {
RefValue::Direct(object) => Some(object),
RefValue::Symbolic(name) => refs.resolve(name.as_str()),
};
}
pub fn stored_stats(bytes: &[u8], hash_length: usize) -> Option<Vec<StatEntry>> {
let (entries, _end) = walk_entries(bytes, hash_length)?;
let mut found: Vec<StatEntry> = Vec::with_capacity(entries.len());
for entry in entries {
let stored: StoredStat = StoredStat {
ctime_seconds: be32(bytes, entry.start)?,
ctime_nanos: be32(bytes, entry.start + 4)?,
mtime_seconds: be32(bytes, entry.start + 8)?,
mtime_nanos: be32(bytes, entry.start + 12)?,
ino: be32(bytes, entry.start + 20)?,
uid: be32(bytes, entry.start + 28)?,
gid: be32(bytes, entry.start + 32)?,
size: be32(bytes, entry.start + 36)?,
};
found.push(StatEntry { path: entry.path, flags: entry.flags, mode: entry.mode, stored });
}
return Some(found);
}
fn index_differs_from_tree(index: &[IndexEntry], tree: &[FlatEntry]) -> Option<String> {
if index.len() != tree.len() {
return Some(String::from("the index and the tree of HEAD hold different numbers of files"));
}
for (staged, committed) in index.iter().zip(tree) {
if staged.stage != 0 || staged.extended != 0 {
return Some(format!("the index entry {} is unmerged, intent-to-add or skip-worktree", String::from_utf8_lossy(&staged.path)));
}
if staged.mode == MODE_GITLINK {
return Some(format!("{} is a submodule", String::from_utf8_lossy(&staged.path)));
}
if staged.path != committed.path || staged.mode != committed.mode || staged.id != committed.id {
return Some(format!("{} is staged with a change", String::from_utf8_lossy(&staged.path)));
}
}
return None;
}
fn file_differs_from_entry(root: &Path, entry: &StatEntry, index_seconds: u32) -> Option<String> {
let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
let Ok(metadata) = std::fs::symlink_metadata(root.join(OsStr::from_bytes(entry.path.as_slice()))) else {
return Some(format!("{shown} is missing from the worktree"));
};
return metadata_differs_from_entry(entry, &metadata, index_seconds);
}
pub fn metadata_differs_from_entry(entry: &StatEntry, metadata: &std::fs::Metadata, index_seconds: u32) -> Option<String> {
let StatEntry { path, flags, mode, stored }: &StatEntry = entry;
let shown: String = String::from_utf8_lossy(path).into_owned();
if flags & ASSUME_VALID != 0 {
return Some(format!("{shown} is marked assume-unchanged"));
}
if stored.mtime_seconds >= index_seconds {
return Some(format!("{shown} is as new as the index, so its stat fields prove nothing"));
}
let symbolic: bool = *mode & 0o170_000 == 0o120_000;
if symbolic != metadata.file_type().is_symlink() || (!symbolic && !metadata.file_type().is_file()) {
return Some(format!("{shown} changed its type"));
}
if !symbolic && (*mode & 0o100 != 0) != (metadata.mode() & 0o100 != 0) {
return Some(format!("{shown} changed its executable bit"));
}
let now: StatFields = fields_of(metadata);
if !matches_stored(&now, stored) {
return Some(format!("{shown} differs from its index entry by size, times or inode"));
}
return None;
}
pub fn tree_differs_from_index(root: &Path, entries: &[StatEntry], index_seconds: u32) -> Option<String> {
return entries.iter().find_map(|entry: &StatEntry| { return file_differs_from_entry(root, entry, index_seconds); });
}
pub fn tree_differs_from_index_beneath(root: &std::fs::File, entries: &[StatEntry], index_seconds: u32) -> Option<String> {
use super::fs_beneath::{entry_metadata_at, open_directory_beneath, plain_components};
let mut cached: Option<(Vec<std::ffi::OsString>, std::fs::File)> = None;
for entry in entries {
let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
let Ok(names) = plain_components(Path::new(OsStr::from_bytes(entry.path.as_slice()))) else { return Some(format!("{shown} is not a plain path")) };
let Some((name, parents)) = names.split_last() else { return Some(format!("{shown} is not a plain path")) };
if cached.as_ref().is_none_or(|(held, _directory): &(Vec<std::ffi::OsString>, std::fs::File)| { return held.as_slice() != parents; }) {
let Ok(directory) = open_directory_beneath(root, parents) else { return Some(format!("{shown} is missing from the worktree or reached through a link")) };
cached = Some((parents.to_vec(), directory));
}
let Some((_held, directory)) = cached.as_ref() else { return Some(format!("{shown} is missing from the worktree")) };
let Ok(metadata) = entry_metadata_at(directory, name.as_os_str()) else { return Some(format!("{shown} is missing from the worktree")) };
if let Some(reason) = metadata_differs_from_entry(entry, &metadata, index_seconds) {
return Some(reason);
}
}
return None;
}
pub fn stat_fields_beneath(root: &std::fs::File, path: &[u8]) -> Option<StatFields> {
use super::fs_beneath::{entry_metadata_at, open_directory_beneath, plain_components};
let names = plain_components(Path::new(OsStr::from_bytes(path))).ok()?;
let (name, parents) = names.split_last()?;
let directory = open_directory_beneath(root, parents).ok()?;
return Some(fields_of(&entry_metadata_at(&directory, name.as_os_str()).ok()?));
}
fn untracked_file(context: &GitContext, root: &Path) -> Result<Option<String>, String> {
let request: GitRequest = GitRequest {
cwd: root.to_path_buf(),
arguments: ["ls-files", "--others", "--exclude-standard", "--directory", "--no-empty-directory", "-z"].map(Into::into).to_vec(),
without_prefix: true,
..GitRequest::default()
};
let output = run_git(context, &request).map_err(|error| { return format!("Git could not list untracked files ({error})"); })?;
if output.code != Some(0) {
return Err(format!("Git could not list untracked files: {}", String::from_utf8_lossy(output.stderr.as_slice()).trim()));
}
return Ok(output.stdout.split(|byte: &u8| { return *byte == 0; }).find(|name: &&[u8]| { return !name.is_empty(); }).map(|name: &[u8]| { return String::from_utf8_lossy(name).into_owned(); }));
}
pub fn check_clean(context: &GitContext, root: &Path, git_dir: &Path, common_dir: &Path) -> Result<CleanSource, String> {
if let Some(name) = operation_in_progress(git_dir) {
return Err(format!("{name} exists in {}", git_dir.display()));
}
let hash_length: usize = repository_hash_length(common_dir).ok_or_else(|| { return String::from("the repository configuration is unreadable"); })?;
let format: ObjectFormat = if hash_length == 32 { ObjectFormat::Sha256 } else { ObjectFormat::Sha1 };
let index_path = git_dir.join("index");
let mut index_file: std::fs::File = std::fs::File::open(index_path.as_path()).map_err(|error| { return format!("the index is unreadable ({error})"); })?;
let index_seconds: u32 = index_file.metadata().map_err(|error| { return format!("the index is unreadable ({error})"); })?.mtime() as u32;
let mut bytes: Vec<u8> = Vec::new();
std::io::Read::read_to_end(&mut index_file, &mut bytes).map_err(|error| { return format!("the index is unreadable ({error})"); })?;
let entries: Vec<IndexEntry> = parse_index(bytes.as_slice(), format).ok_or_else(|| { return String::from("the index has a layout the reader does not know"); })?;
let stats = stored_stats(bytes.as_slice(), hash_length).ok_or_else(|| { return String::from("the index entries have no plain layout"); })?;
let commit: String = head_commit(git_dir, common_dir, format).ok_or_else(|| { return String::from("HEAD is not a plain ref to a commit"); })?;
let database: ObjectDatabase = ObjectDatabase::open(common_dir, None, format).ok_or_else(|| { return String::from("the object database has a layout the reader does not know"); })?;
let commit_name: Vec<u8> = super::repo_read_object::from_hex(commit.as_bytes(), format).ok_or_else(|| { return String::from("HEAD names no object"); })?;
let commit_fields = parse_commit(database.read(commit_name.as_slice()).ok_or_else(|| { return String::from("the commit of HEAD is unreadable"); })?.bytes.as_slice(), format)
.ok_or_else(|| { return String::from("the commit of HEAD has a layout the reader does not know"); })?;
let tree: Vec<FlatEntry> = flatten_tree(&database, commit_fields.tree.as_slice(), format).ok_or_else(|| { return String::from("the tree of HEAD is unreadable"); })?;
if let Some(reason) = index_differs_from_tree(entries.as_slice(), tree.as_slice()) {
return Err(reason);
}
let attribute_blobs: Vec<(Vec<u8>, Vec<u8>)> = tree
.iter()
.filter(|entry: &&FlatEntry| { return entry.path == b".gitattributes" || entry.path.ends_with(b"/.gitattributes"); })
.map(|entry: &FlatEntry| { return (entry.path.clone(), entry.id.clone()); })
.collect();
if let Some(reason) = tree_differs_from_index(root, stats.as_slice(), index_seconds) {
return Err(reason);
}
if let Some(name) = untracked_file(context, root)? {
return Err(format!("{name} is untracked and not ignored"));
}
return Ok(CleanSource { head_commit: commit, head_tree: commit_fields.tree, tracked: entries.len(), index_seconds, hash_length, attribute_blobs, index_bytes: bytes });
}
#[cfg(test)]
#[path = "worktree_clean_tests.rs"]
mod tests;