use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct ProjectContext {
pub readme_content: Option<String>,
pub changes_content: Option<String>,
pub file_tree: Option<String>,
pub git_status: Option<String>,
pub git_branch: Option<String>,
}
impl Default for ProjectContext {
fn default() -> Self {
Self::new()
}
}
impl ProjectContext {
pub fn new() -> Self {
Self {
readme_content: None,
changes_content: None,
file_tree: None,
git_status: None,
git_branch: None,
}
}
pub fn collect(project_dir: &Path) -> Self {
let mut context = Self::new();
context.readme_content = Self::read_file_if_exists(project_dir.join("README.md"));
context.changes_content = Self::read_file_if_exists(project_dir.join("CHANGES.md"));
context.file_tree = Self::get_file_tree(project_dir);
context.git_status = Self::get_git_status(project_dir);
context.git_branch = Self::get_git_branch(project_dir);
context
}
fn read_file_if_exists(path: PathBuf) -> Option<String> {
if path.exists() && path.is_file() {
match fs::read_to_string(&path) {
Ok(content) => {
Some(content)
}
Err(e) => {
crate::log_error!("Error reading {}: {}", path.display(), e);
None
}
}
} else {
None
}
}
fn get_file_tree(project_dir: &Path) -> Option<String> {
let files_list = Self::get_files_list(project_dir)?;
Some(Self::build_tree_structure(&files_list))
}
fn get_files_list(project_dir: &Path) -> Option<String> {
let git_check = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(project_dir)
.output();
if let Ok(output) = git_check {
if output.status.success() {
if let Ok(output) = Command::new("git")
.args(["ls-files"])
.current_dir(project_dir)
.output()
{
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).to_string());
}
}
}
}
None
}
fn build_tree_structure(files_list: &str) -> String {
use std::collections::BTreeMap;
#[derive(Debug)]
enum TreeNode {
File,
Directory(BTreeMap<String, TreeNode>),
}
let mut root: BTreeMap<String, TreeNode> = BTreeMap::new();
for line in files_list.lines() {
let path = line.trim();
if path.is_empty() {
continue;
}
let parts: Vec<&str> = path.split('/').collect();
if parts.is_empty() {
continue;
}
let mut current_map = &mut root;
for (i, part) in parts.iter().enumerate() {
let part_owned = part.to_string();
let is_last = i == parts.len() - 1;
if is_last {
current_map.insert(part_owned, TreeNode::File);
break; } else {
current_map
.entry(part_owned.clone())
.or_insert_with(|| TreeNode::Directory(BTreeMap::new()));
if let Some(TreeNode::Directory(ref mut dir_map)) =
current_map.get_mut(&part_owned)
{
current_map = dir_map;
} else {
break; }
}
}
}
fn render_tree(node_map: &BTreeMap<String, TreeNode>, prefix: &str) -> String {
let mut result = String::new();
let entries: Vec<_> = node_map.iter().collect();
for (i, (name, node)) in entries.iter().enumerate() {
let is_last = i == entries.len() - 1;
let current_prefix = if is_last { "└─ " } else { "├─ " };
let next_prefix = if is_last { " " } else { "│ " };
match node {
TreeNode::File => {
result.push_str(&format!("{}{}{}\n", prefix, current_prefix, name));
}
TreeNode::Directory(children) => {
result.push_str(&format!("{}{}{}/\n", prefix, current_prefix, name));
if !children.is_empty() {
result.push_str(&render_tree(
children,
&format!("{}{}", prefix, next_prefix),
));
}
}
}
}
result
}
render_tree(&root, "")
}
fn get_git_status(project_dir: &Path) -> Option<String> {
let output = Command::new("git")
.args(["status", "--short"])
.current_dir(project_dir)
.output();
if let Ok(output) = output {
if output.status.success() {
let status = String::from_utf8_lossy(&output.stdout).to_string();
if !status.trim().is_empty() {
return Some(status);
}
}
}
None
}
fn get_git_branch(project_dir: &Path) -> Option<String> {
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(project_dir)
.output();
if let Ok(output) = output {
if output.status.success() {
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !branch.is_empty() {
return Some(branch);
}
}
}
None
}
pub fn format_for_prompt(&self) -> String {
let mut result = String::new();
if let Some(readme) = &self.readme_content {
result.push_str("# Project README\n\n");
result.push_str(readme);
result.push_str("\n\n");
}
if let Some(changes) = &self.changes_content {
result.push_str("# Project CHANGES\n\n");
result.push_str(changes);
result.push_str("\n\n");
}
if let Some(branch) = &self.git_branch {
result.push_str(&format!("# Git Branch\n\n{}", branch));
result.push_str("\n\n");
}
if let Some(status) = &self.git_status {
result.push_str("# Git Status\n\n");
result.push_str(status);
result.push_str("\n\n");
}
if let Some(tree) = &self.file_tree {
result.push_str("# Project File Structure\n\n");
result.push_str(tree);
}
result
}
}