use crate::index_full::{FullEntry, FullIndex, SKIP_WORKTREE};
use crate::index_settings::{IndexSettings, UntrackedMode};
use crate::index_stat::StatData;
use crate::repo_read_tree::{FlatEntry, MODE_GITLINK};
const MODE_EXECUTABLE: u32 = 0o100_755;
const MODE_FILE: u32 = 0o100_644;
const MODE_LINK: u32 = 0o120_000;
fn index_mode(mode: u32) -> u32 {
if mode & 0o170_000 == MODE_LINK {
return MODE_LINK;
}
if mode & 0o170_000 == 0o040_000 || mode & 0o170_000 == MODE_GITLINK {
return MODE_GITLINK;
}
return if mode & 0o100 != 0 { MODE_EXECUTABLE } else { MODE_FILE };
}
fn only_spaces_and_dots(rest: &[u8]) -> bool {
return rest.iter().all(|byte| { return *byte == b' ' || *byte == b'.'; });
}
fn is_ntfs_dotgitmodules(name: &[u8]) -> bool {
let at = |index: usize| -> u8 { return name.get(index).copied().unwrap_or(0); };
let spaces_and_periods = |start: usize| -> bool {
for index in start.. {
let byte: u8 = at(index);
if byte == 0 || byte == b':' {
return true;
}
if byte != b' ' && byte != b'.' {
return false;
}
}
return false;
};
if at(0) == b'.' && name.len() > 10 && name[1..11].eq_ignore_ascii_case(b"gitmodules") {
return spaces_and_periods(11);
}
if name.len() > 5 && name[..6].eq_ignore_ascii_case(b"gitmod") && at(6) == b'~' && (b'1'..=b'4').contains(&at(7)) {
return spaces_and_periods(8);
}
let (mut index, mut saw_tilde): (usize, bool) = (0, false);
while index < 8 {
let byte: u8 = at(index);
if byte == 0 {
return false;
} else if saw_tilde {
if !byte.is_ascii_digit() {
return false;
}
} else if byte == b'~' {
index += 1;
if !(b'1'..=b'9').contains(&at(index)) {
return false;
}
saw_tilde = true;
} else if index >= 6 || byte & 0x80 != 0 || byte.to_ascii_lowercase() != b"gi7eba"[index] {
return false;
}
index += 1;
}
return spaces_and_periods(8);
}
fn component_is_plain(component: &[u8], is_link: bool) -> bool {
let lowered: Vec<u8> = component.to_ascii_lowercase();
let dot_git_like: bool = lowered.strip_prefix(b".git").is_some_and(only_spaces_and_dots);
let dot_gitmodules_like: bool = is_link && (is_ntfs_dotgitmodules(component) || lowered.strip_prefix(b".gitmodules").is_some_and(only_spaces_and_dots) || lowered.starts_with(b"gitmod~"));
let ignorable: bool = component.windows(2).any(|pair| { return pair == [0xe2, 0x80] || pair == [0xe2, 0x81]; }) || component.windows(3).any(|triple| { return triple == [0xef, 0xbb, 0xbf]; });
let separators: bool = component.contains(&b'\\') || component.contains(&b':');
return !component.is_empty() && component != b"." && component != b".." && !separators && !dot_git_like && !dot_gitmodules_like && !lowered.starts_with(b"git~") && !ignorable;
}
fn path_is_plain(path: &[u8], mode: u32) -> bool {
if path.is_empty() {
return false;
}
return path.split(|byte| { return *byte == b'/'; }).all(|component| { return component_is_plain(component, mode & 0o170_000 == MODE_LINK); });
}
fn selected(path: &[u8], names: &[Vec<u8>]) -> bool {
return names.iter().any(|name| { return path == name.as_slice() || (path.starts_with(name.as_slice()) && path.get(name.len()) == Some(&b'/')); });
}
fn first_at(entries: &[FullEntry], path: &[u8]) -> usize {
return entries.partition_point(|entry| { return entry.path.as_slice() < path; });
}
fn has_directory_conflict(entries: &[FullEntry], path: &[u8]) -> bool {
let mut below: Vec<u8> = path.to_vec();
below.push(b'/');
let start: usize = first_at(entries, below.as_slice());
if entries.get(start).is_some_and(|entry| { return entry.path.starts_with(below.as_slice()); }) {
return true;
}
return path.iter().enumerate().filter(|(_, byte)| { return **byte == b'/'; }).any(|(slash, _)| {
let above: &[u8] = &path[..slash];
let at: usize = first_at(entries, above);
return entries.get(at).is_some_and(|entry| { return entry.path.as_slice() == above; });
});
}
struct Edit<'a> {
index: &'a mut FullIndex,
settings: IndexSettings,
}
impl Edit<'_> {
fn forget_untracked(&mut self, path: &[u8]) {
if let Some(cache) = self.index.untracked.as_mut() {
cache.invalidate_path(path);
}
}
fn take_entries(&mut self, path: &[u8]) {
let start: usize = first_at(self.index.entries.as_slice(), path);
while self.index.entries.get(start).is_some_and(|entry| { return entry.path.as_slice() == path; }) {
let gone: FullEntry = self.index.entries.remove(start);
if gone.stage() != 0 {
self.index.resolve_undo.get_or_insert_with(Default::default).record(gone.path.as_slice(), gone.stage(), gone.mode, gone.id.as_slice());
}
}
}
fn remove(&mut self, path: &[u8]) {
if let Some(tree) = self.index.cache_tree.as_mut() {
tree.invalidate(path);
}
self.forget_untracked(path);
self.take_entries(path);
}
fn put(&mut self, wanted: &FlatEntry) {
let path: &[u8] = wanted.path.as_slice();
if let Some(tree) = self.index.cache_tree.as_mut() {
tree.invalidate(path);
}
let start: usize = first_at(self.index.entries.as_slice(), path);
let existing: Option<usize> = self.index.entries.get(start).filter(|entry| { return entry.path.as_slice() == path && entry.stage() == 0; }).map(|_| { return start; });
let skip: u16 = existing.map_or(0, |at| { return self.index.entries[at].extended & SKIP_WORKTREE; });
let made: FullEntry = FullEntry {
stat: StatData::default(),
mode: index_mode(wanted.mode),
id: wanted.id.clone(),
flags: 0,
extended: skip,
path: wanted.path.clone(),
monitor_valid: false,
};
if let Some(at) = existing {
self.index.entries[at] = made;
if self.settings.monitor {
self.forget_untracked(path);
}
return;
}
self.forget_untracked(path);
self.take_entries(path);
let at: usize = first_at(self.index.entries.as_slice(), path);
self.index.entries.insert(at, made);
}
}
fn prepare_extensions(index: &mut FullIndex, settings: IndexSettings) -> Option<()> {
if settings.monitor {
let dirty: Vec<usize> = index.fsmonitor.as_ref()?.dirty.clone();
if dirty.iter().any(|position| { return *position >= index.entries.len(); }) {
return None;
}
for entry in &mut index.entries {
entry.monitor_valid = entry.mode != MODE_GITLINK;
}
for position in dirty {
index.entries[position].monitor_valid = false;
}
} else {
index.fsmonitor = None;
}
match settings.untracked {
UntrackedMode::Keep => {}
UntrackedMode::Write => {
index.untracked.as_ref()?;
}
UntrackedMode::Remove => index.untracked = None,
}
return Some(());
}
pub fn reset_paths(index: &mut FullIndex, names: &[Vec<u8>], tree: &[FlatEntry], settings: IndexSettings, timestamp_seconds: u32) -> Option<()> {
if names.iter().any(|name| { return !path_is_plain(name.as_slice(), 0) || name.last() == Some(&b'/'); }) {
return None;
}
prepare_extensions(index, settings)?;
let mut paths: Vec<Vec<u8>> = tree.iter().map(|entry| { return entry.path.clone(); }).collect();
paths.extend(index.entries.iter().filter(|entry| { return selected(entry.path.as_slice(), names); }).map(|entry| { return entry.path.clone(); }));
paths.sort();
paths.dedup();
let mut edit: Edit<'_> = Edit { index, settings };
for path in paths {
let wanted: Option<&FlatEntry> = tree.binary_search_by(|entry| { return entry.path.cmp(&path); }).ok().map(|at| { return &tree[at]; });
let start: usize = first_at(edit.index.entries.as_slice(), path.as_slice());
let held: Vec<&FullEntry> = edit.index.entries[start..].iter().take_while(|entry| { return entry.path == path; }).collect();
match wanted {
None => edit.remove(path.as_slice()),
Some(entry) => {
let same: bool = matches!(held.as_slice(), [only] if only.stage() == 0 && only.mode == index_mode(entry.mode) && only.id == entry.id);
if same {
continue;
}
if !path_is_plain(entry.path.as_slice(), index_mode(entry.mode)) || has_directory_conflict(edit.index.entries.as_slice(), entry.path.as_slice()) {
return None;
}
edit.put(entry);
}
}
}
let racy: bool = index.entries.iter().any(|entry| { return entry.mode != MODE_GITLINK && timestamp_seconds <= entry.stat.mtime_sec; });
if racy {
return None;
}
return Some(());
}
#[cfg(test)]
#[path = "index_reset_tests.rs"]
mod tests;