use crate::context::{ChangeType, RecentCommit, StagedFile};
use crate::git::utils::{is_binary_diff, should_exclude_file};
use crate::log_debug;
use anyhow::{Context, Result};
use git2::{DiffOptions, Repository, StatusOptions};
use std::fs;
use std::path::Path;
#[derive(Debug)]
pub struct RepoFilesInfo {
pub branch: String,
pub recent_commits: Vec<RecentCommit>,
pub staged_files: Vec<StagedFile>,
pub file_paths: Vec<String>,
}
pub fn get_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
log_debug!("Getting file statuses");
let mut staged_files = Vec::new();
let mut opts = StatusOptions::new();
opts.include_untracked(true);
let statuses = repo.statuses(Some(&mut opts))?;
for entry in statuses.iter() {
let path = entry.path().context("Could not get path")?;
let status = entry.status();
if status.is_index_new() || status.is_index_modified() || status.is_index_deleted() {
let change_type = if status.is_index_new() {
ChangeType::Added
} else if status.is_index_modified() {
ChangeType::Modified
} else {
ChangeType::Deleted
};
let should_exclude = should_exclude_file(path);
let diff = if should_exclude {
String::from("[Content excluded]")
} else {
get_diff_for_file(repo, path)?
};
let content =
if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
None
} else {
get_index_content_for_file(repo, path)?
};
staged_files.push(StagedFile {
path: path.to_string(),
change_type,
diff,
content,
content_excluded: should_exclude,
});
}
}
log_debug!("Found {} staged files", staged_files.len());
Ok(staged_files)
}
pub fn get_diff_for_file(repo: &Repository, path: &str) -> Result<String> {
log_debug!("Getting diff for file: {}", path);
let mut diff_options = DiffOptions::new();
diff_options.pathspec(path);
let tree = repo.head().ok().and_then(|head| head.peel_to_tree().ok());
let index = repo.index()?;
let diff = repo.diff_tree_to_index(tree.as_ref(), Some(&index), Some(&mut diff_options))?;
let mut diff_string = String::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let origin = match line.origin() {
'+' | '-' | ' ' => line.origin(),
_ => ' ',
};
diff_string.push(origin);
diff_string.push_str(&String::from_utf8_lossy(line.content()));
true
})?;
if is_binary_diff(&diff_string) {
Ok("[Binary file changed]".to_string())
} else {
log_debug!("Generated diff for {} ({} bytes)", path, diff_string.len());
Ok(diff_string)
}
}
fn get_index_content_for_file(repo: &Repository, path: &str) -> Result<Option<String>> {
let index = repo.index()?;
let Some(entry) = index.get_path(Path::new(path), 0) else {
return Ok(None);
};
let blob = repo.find_blob(entry.id)?;
match std::str::from_utf8(blob.content()) {
Ok(content) => Ok(Some(content.to_string())),
Err(_) => Ok(None),
}
}
pub fn get_unstaged_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
log_debug!("Getting unstaged file statuses");
let mut unstaged_files = Vec::new();
let mut opts = StatusOptions::new();
opts.include_untracked(true);
let statuses = repo.statuses(Some(&mut opts))?;
for entry in statuses.iter() {
let path = entry.path().context("Could not get path")?;
let status = entry.status();
if status.is_wt_new() || status.is_wt_modified() || status.is_wt_deleted() {
let change_type = if status.is_wt_new() {
ChangeType::Added
} else if status.is_wt_modified() {
ChangeType::Modified
} else {
ChangeType::Deleted
};
let should_exclude = should_exclude_file(path);
let diff = if should_exclude {
String::from("[Content excluded]")
} else {
get_diff_for_unstaged_file(repo, path)?
};
let content =
if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
None
} else {
let path_obj = Path::new(path);
if path_obj.exists() {
Some(fs::read_to_string(path_obj)?)
} else {
None
}
};
unstaged_files.push(StagedFile {
path: path.to_string(),
change_type,
diff,
content,
content_excluded: should_exclude,
});
}
}
log_debug!("Found {} unstaged files", unstaged_files.len());
Ok(unstaged_files)
}
pub fn get_diff_for_unstaged_file(repo: &Repository, path: &str) -> Result<String> {
log_debug!("Getting unstaged diff for file: {}", path);
let mut diff_options = DiffOptions::new();
diff_options.pathspec(path);
let diff = repo.diff_index_to_workdir(None, Some(&mut diff_options))?;
let mut diff_string = String::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let origin = match line.origin() {
'+' | '-' | ' ' => line.origin(),
_ => ' ',
};
diff_string.push(origin);
diff_string.push_str(&String::from_utf8_lossy(line.content()));
true
})?;
if is_binary_diff(&diff_string) {
Ok("[Binary file changed]".to_string())
} else {
log_debug!(
"Generated unstaged diff for {} ({} bytes)",
path,
diff_string.len()
);
Ok(diff_string)
}
}
pub fn get_untracked_files(repo: &Repository) -> Result<Vec<String>> {
log_debug!("Getting untracked files");
let mut untracked = Vec::new();
let mut opts = StatusOptions::new();
opts.include_untracked(true);
opts.exclude_submodules(true);
let statuses = repo.statuses(Some(&mut opts))?;
for entry in statuses.iter() {
let status = entry.status();
if status.is_wt_new()
&& !status.is_index_new()
&& let Some(path) = entry.path()
{
untracked.push(path.to_string());
}
}
log_debug!("Found {} untracked files", untracked.len());
Ok(untracked)
}
pub fn get_all_tracked_files(repo: &Repository) -> Result<Vec<String>> {
log_debug!("Getting all tracked files");
let mut files = std::collections::HashSet::new();
if let Ok(head) = repo.head()
&& let Ok(tree) = head.peel_to_tree()
{
tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
if entry.kind() == Some(git2::ObjectType::Blob) {
let path = if dir.is_empty() {
entry.name().unwrap_or("").to_string()
} else {
format!("{}{}", dir, entry.name().unwrap_or(""))
};
if !path.is_empty() {
files.insert(path);
}
}
git2::TreeWalkResult::Ok
})?;
}
let index = repo.index()?;
for entry in index.iter() {
let path = String::from_utf8_lossy(&entry.path).to_string();
files.insert(path);
}
let mut result: Vec<_> = files.into_iter().collect();
result.sort();
log_debug!("Found {} tracked files", result.len());
Ok(result)
}
pub fn get_ahead_behind(repo: &Repository) -> (usize, usize) {
log_debug!("Getting ahead/behind counts");
let Ok(head) = repo.head() else {
return (0, 0); };
let Some(branch_name) = head.shorthand() else {
return (0, 0);
};
let Ok(branch) = repo.find_branch(branch_name, git2::BranchType::Local) else {
return (0, 0);
};
let Ok(upstream) = branch.upstream() else {
return (0, 0); };
let Some(local_oid) = head.target() else {
return (0, 0);
};
let Some(upstream_oid) = upstream.get().target() else {
return (0, 0);
};
match repo.graph_ahead_behind(local_oid, upstream_oid) {
Ok((ahead, behind)) => {
log_debug!("Branch is {} ahead, {} behind upstream", ahead, behind);
(ahead, behind)
}
Err(_) => (0, 0),
}
}