use crate::index_byte_search::is_slash;
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 is_space_or_dot(byte: &u8) -> bool {
return *byte == b' ' || *byte == b'.';
}
fn only_spaces_and_dots(rest: &[u8]) -> bool {
return rest.iter().all(is_space_or_dot);
}
fn byte_at(name: &[u8], index: usize) -> u8 {
return name.get(index).copied().unwrap_or(0);
}
fn spaces_and_periods(name: &[u8], start: usize) -> bool {
for index in start.. {
let byte: u8 = byte_at(name, index);
if byte == 0 || byte == b':' {
return true;
}
if byte != b' ' && byte != b'.' {
return false;
}
}
return false;
}
fn is_ignorable_pair(pair: &[u8]) -> bool {
return pair == [0xe2, 0x80] || pair == [0xe2, 0x81];
}
fn is_ignorable_triple(triple: &[u8]) -> bool {
return triple == [0xef, 0xbb, 0xbf];
}
fn is_ntfs_dotgitmodules(name: &[u8]) -> bool {
if byte_at(name, 0) == b'.' && name.len() > 10 && name[1..11].eq_ignore_ascii_case(b"gitmodules") {
return spaces_and_periods(name, 11);
}
if name.len() > 5 && name[..6].eq_ignore_ascii_case(b"gitmod") && byte_at(name, 6) == b'~' && (b'1'..=b'4').contains(&byte_at(name, 7)) {
return spaces_and_periods(name, 8);
}
let (mut index, mut saw_tilde): (usize, bool) = (0, false);
while index < 8 {
let byte: u8 = byte_at(name, 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(&byte_at(name, 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(name, 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(is_ignorable_pair) || component.windows(3).any(is_ignorable_triple);
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;
}
let is_link: bool = mode & 0o170_000 == MODE_LINK;
for component in path.split(is_slash) {
if !component_is_plain(component, is_link) {
return false;
}
}
return true;
}
fn selected(path: &[u8], names: &[Vec<u8>]) -> bool {
for name in names {
if path == name.as_slice() || (path.starts_with(name.as_slice()) && path.get(name.len()) == Some(&b'/')) {
return true;
}
}
return false;
}
fn first_at(entries: &[FullEntry], path: &[u8]) -> usize {
let (mut low, mut high): (usize, usize) = (0, entries.len());
while low < high {
let middle: usize = low + (high - low) / 2;
if entries[middle].path.as_slice() < path {
low = middle + 1;
} else {
high = middle;
}
}
return low;
}
fn has_path_at(entries: &[FullEntry], position: usize, path: &[u8]) -> bool {
return match entries.get(position) {
Some(entry) => entry.path.as_slice() == path,
None => false,
};
}
fn find_in_tree<'a>(tree: &'a [FlatEntry], path: &[u8]) -> Option<&'a FlatEntry> {
let (mut low, mut high): (usize, usize) = (0, tree.len());
while low < high {
let middle: usize = low + (high - low) / 2;
match tree[middle].path.as_slice().cmp(path) {
std::cmp::Ordering::Less => low = middle + 1,
std::cmp::Ordering::Greater => high = middle,
std::cmp::Ordering::Equal => return Some(&tree[middle]),
}
}
return None;
}
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 let Some(entry) = entries.get(start)
&& entry.path.starts_with(below.as_slice())
{
return true;
}
for (slash, byte) in path.iter().enumerate() {
if *byte != b'/' {
continue;
}
let above: &[u8] = &path[..slash];
let at: usize = first_at(entries, above);
if has_path_at(entries, at, above) {
return true;
}
}
return false;
}
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 has_path_at(self.index.entries.as_slice(), start, 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 mut existing: Option<usize> = None;
if let Some(entry) = self.index.entries.get(start)
&& entry.path.as_slice() == path
&& entry.stage() == 0
{
existing = Some(start);
}
let skip: u16 = match existing {
Some(at) => self.index.entries[at].extended & SKIP_WORKTREE,
None => 0,
};
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();
for position in &dirty {
if *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<()> {
for name in names {
if !path_is_plain(name.as_slice(), 0) || name.last() == Some(&b'/') {
return None;
}
}
prepare_extensions(index, settings)?;
let mut paths: Vec<Vec<u8>> = Vec::new();
for entry in tree {
paths.push(entry.path.clone());
}
for entry in &index.entries {
if selected(entry.path.as_slice(), names) {
paths.push(entry.path.clone());
}
}
paths.sort();
paths.dedup();
let mut edit: Edit<'_> = Edit { index, settings };
for path in paths {
let wanted: Option<&FlatEntry> = find_in_tree(tree, path.as_slice());
let start: usize = first_at(edit.index.entries.as_slice(), path.as_slice());
let mut held: Vec<&FullEntry> = Vec::new();
for entry in &edit.index.entries[start..] {
if entry.path != path {
break;
}
held.push(entry);
}
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);
}
}
}
for entry in &index.entries {
if entry.mode != MODE_GITLINK && timestamp_seconds <= entry.stat.mtime_sec {
return None;
}
}
return Some(());
}
#[cfg(test)]
#[path = "index_reset_tests.rs"]
mod tests;