use std::path::Path;
#[cfg(test)]
use std::{path::PathBuf, sync::Mutex};
use super::{
CodeIndexError,
git::{git_bytes, validate_git_ref_arg},
};
#[cfg(test)]
static TRACKED_ENTRIES_OBSERVER: Mutex<Option<(PathBuf, usize)>> = Mutex::new(None);
#[cfg(test)]
pub(crate) fn reset_tracked_entries_call_count_for_root(root: PathBuf) {
*TRACKED_ENTRIES_OBSERVER
.lock()
.expect("tracked entries observer should lock") = Some((root, 0));
}
#[cfg(test)]
pub(crate) fn tracked_entries_call_count_for_root(root: &Path) -> usize {
TRACKED_ENTRIES_OBSERVER
.lock()
.expect("tracked entries observer should lock")
.as_ref()
.filter(|(observed_root, _)| observed_root == root)
.map(|(_, count)| *count)
.unwrap_or(0)
}
pub(super) fn tracked_entries(
root: &Path,
commit: &str,
) -> Result<Vec<GitTreeEntry>, CodeIndexError> {
#[cfg(test)]
if let Some((observed_root, count)) = TRACKED_ENTRIES_OBSERVER
.lock()
.expect("tracked entries observer should lock")
.as_mut()
&& observed_root == root
{
*count += 1;
}
let bytes = git_bytes(root, ["ls-tree", "-r", "-l", "-z", commit])?;
let mut entries = Vec::new();
for record in split_nul(&bytes) {
let Some((metadata, path)) = record.split_once('\t') else {
continue;
};
let fields = metadata.split_whitespace().collect::<Vec<_>>();
if fields.get(1) != Some(&"blob") {
continue;
}
let byte_count = fields
.get(3)
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
entries.push(GitTreeEntry {
path: path.to_owned(),
byte_count,
});
}
Ok(entries)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct GitTreeEntry {
pub(super) path: String,
pub(super) byte_count: usize,
}
pub(super) fn diff_changes(
root: &Path,
base_ref: &str,
head_ref: &str,
) -> Result<Vec<GitChange>, CodeIndexError> {
validate_git_ref_arg("base_ref", base_ref)?;
validate_git_ref_arg("head_ref", head_ref)?;
let bytes = git_bytes(
root,
[
"diff",
"--name-status",
"--find-renames",
"-z",
"--end-of-options",
base_ref,
head_ref,
"--",
],
)?;
parse_name_status_z(&bytes)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum GitChange {
AddedOrModified { path: String },
Deleted { path: String },
Renamed { old_path: String, new_path: String },
Copied { old_path: String, new_path: String },
TypeChanged { path: String },
}
pub(super) fn parse_name_status_z(bytes: &[u8]) -> Result<Vec<GitChange>, CodeIndexError> {
let tokens = split_nul(bytes);
let mut changes = Vec::new();
let mut index = 0;
while index < tokens.len() {
let status = &tokens[index];
index += 1;
if status.starts_with('R') || status.starts_with('C') {
let old_path = tokens.get(index).cloned().ok_or_else(|| {
CodeIndexError::InvalidInput("rename old path is missing".to_owned())
})?;
let new_path = tokens.get(index + 1).cloned().ok_or_else(|| {
CodeIndexError::InvalidInput("rename new path is missing".to_owned())
})?;
index += 2;
if status.starts_with('R') {
changes.push(GitChange::Renamed { old_path, new_path });
} else {
changes.push(GitChange::Copied { old_path, new_path });
}
continue;
}
let path = tokens
.get(index)
.cloned()
.ok_or_else(|| CodeIndexError::InvalidInput("changed path is missing".to_owned()))?;
index += 1;
match status.chars().next() {
Some('D') => changes.push(GitChange::Deleted { path }),
Some('T') => changes.push(GitChange::TypeChanged { path }),
Some('A' | 'M') => changes.push(GitChange::AddedOrModified { path }),
_ => changes.push(GitChange::AddedOrModified { path }),
}
}
Ok(changes)
}
pub(super) fn split_nul(bytes: &[u8]) -> Vec<String> {
bytes
.split(|byte| *byte == 0)
.filter(|part| !part.is_empty())
.map(|part| String::from_utf8_lossy(part).to_string())
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct WorktreePathChange {
pub(super) status: String,
pub(super) path: String,
pub(super) deleted_source: Option<String>,
}
impl WorktreePathChange {
pub(super) fn is_untracked(&self) -> bool {
self.status == "??"
}
}
pub(super) fn worktree_changed_paths(status: &[u8]) -> Vec<WorktreePathChange> {
let tokens = split_nul(status);
let mut changes = Vec::new();
let mut index = 0;
while index < tokens.len() {
let token = &tokens[index];
if token.len() < 4 {
index += 1;
continue;
}
let status = &token[..2];
let path = token[3..].to_owned();
if (status.contains('R') || status.contains('C')) && tokens.get(index + 1).is_some() {
let source = tokens[index + 1].clone();
changes.push(WorktreePathChange {
status: status.to_owned(),
path,
deleted_source: status.contains('R').then_some(source),
});
index += 2;
continue;
}
changes.push(WorktreePathChange {
status: status.to_owned(),
path,
deleted_source: None,
});
index += 1;
}
changes
}