use crate::changes::PackageMapper;
use crate::config::PackageToolsConfig;
use crate::error::{ChangesError, ChangesResult};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::{AsyncFileSystem, FileSystemManager};
use sublime_standard_tools::monorepo::{
MonorepoDetector, MonorepoDetectorTrait, MonorepoKind, WorkspacePackage,
};
#[derive(Clone, Debug)]
pub struct ChangesAnalyzer<F = FileSystemManager>
where
F: AsyncFileSystem + Clone + Send + Sync + 'static,
{
workspace_root: PathBuf,
git_repo: Rc<Repo>,
monorepo_detector: MonorepoDetector<F>,
fs: F,
config: PackageToolsConfig,
monorepo_kind: Option<MonorepoKind>,
}
impl ChangesAnalyzer<FileSystemManager> {
pub async fn new(
workspace_root: PathBuf,
git_repo: Repo,
fs: FileSystemManager,
config: PackageToolsConfig,
) -> ChangesResult<Self> {
Self::new_impl(workspace_root, git_repo, fs, config).await
}
}
impl<F> ChangesAnalyzer<F>
where
F: AsyncFileSystem + Clone + Send + Sync + 'static,
{
pub async fn with_filesystem(
workspace_root: PathBuf,
git_repo: Repo,
fs: F,
config: PackageToolsConfig,
) -> ChangesResult<Self> {
Self::new_impl(workspace_root, git_repo, fs, config).await
}
async fn new_impl(
workspace_root: PathBuf,
git_repo: Repo,
fs: F,
config: PackageToolsConfig,
) -> ChangesResult<Self> {
Self::validate_workspace_root(&workspace_root, &fs).await?;
Self::validate_git_repo(&git_repo, &workspace_root)?;
let monorepo_detector = MonorepoDetector::with_filesystem_and_config(
fs.clone(),
config.get_standard_config().monorepo.clone(),
);
let monorepo_kind = Self::detect_monorepo_kind(&monorepo_detector, &workspace_root).await?;
Ok(Self {
workspace_root,
git_repo: Rc::new(git_repo),
monorepo_detector,
fs,
config,
monorepo_kind,
})
}
async fn validate_workspace_root(workspace_root: &Path, fs: &F) -> ChangesResult<()> {
if !fs.exists(workspace_root).await {
return Err(ChangesError::InvalidWorkspaceRoot {
path: workspace_root.to_path_buf(),
reason: "Directory does not exist".to_string(),
});
}
match fs.metadata(workspace_root).await {
Ok(metadata) => {
if !metadata.is_dir() {
return Err(ChangesError::InvalidWorkspaceRoot {
path: workspace_root.to_path_buf(),
reason: "Path is not a directory".to_string(),
});
}
}
Err(e) => {
return Err(ChangesError::InvalidWorkspaceRoot {
path: workspace_root.to_path_buf(),
reason: format!("Cannot read metadata: {}", e),
});
}
}
Ok(())
}
fn validate_git_repo(git_repo: &Repo, workspace_root: &Path) -> ChangesResult<()> {
let repo_path = git_repo.get_repo_path();
if repo_path.as_os_str().is_empty() {
return Err(ChangesError::RepositoryNotFound { path: workspace_root.to_path_buf() });
}
Ok(())
}
async fn detect_monorepo_kind(
monorepo_detector: &MonorepoDetector<F>,
workspace_root: &Path,
) -> ChangesResult<Option<MonorepoKind>> {
match monorepo_detector.is_monorepo_root(workspace_root).await {
Ok(kind) => Ok(kind),
Err(e) => Err(ChangesError::MonorepoDetectionFailed {
reason: format!("Failed to detect monorepo: {}", e),
}),
}
}
#[must_use]
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
}
#[must_use]
pub fn git_repo(&self) -> &Repo {
&self.git_repo
}
#[must_use]
pub fn monorepo_kind(&self) -> Option<&MonorepoKind> {
self.monorepo_kind.as_ref()
}
#[must_use]
pub fn is_monorepo(&self) -> bool {
self.monorepo_kind.is_some()
}
#[must_use]
pub fn monorepo_detector(&self) -> &MonorepoDetector<F> {
&self.monorepo_detector
}
#[must_use]
pub fn filesystem(&self) -> &F {
&self.fs
}
#[must_use]
pub fn config(&self) -> &PackageToolsConfig {
&self.config
}
pub async fn analyze_working_directory(&self) -> ChangesResult<crate::changes::ChangesReport> {
use crate::changes::{
AnalysisMode, ChangesReport, FileChange, FileChangeType, PackageChanges,
};
use std::collections::HashMap;
let status = self.git_repo.get_status_detailed().map_err(|e| ChangesError::GitError {
operation: "get_status_detailed".to_string(),
reason: format!("Failed to get git status: {}", e),
})?;
if status.is_empty() {
let mut report = ChangesReport::new(AnalysisMode::WorkingDirectory, self.is_monorepo());
let packages = self.get_all_packages().await?;
for package_info in packages {
report.add_package(PackageChanges::new(package_info));
}
return Ok(report);
}
let changed_paths: Vec<PathBuf> = status.iter().map(|f| PathBuf::from(&f.path)).collect();
let mut package_mapper = PackageMapper::with_filesystem_and_config(
self.workspace_root.clone(),
self.fs.clone(),
&self.config,
);
let _files_by_package = package_mapper.map_files_to_packages(&changed_paths).await?;
let mut package_file_changes: HashMap<String, Vec<FileChange>> = HashMap::new();
for git_file in &status {
let file_path = PathBuf::from(&git_file.path);
if let Some(package_name) = package_mapper.find_package_for_file(&file_path).await? {
let all_pkgs = self.get_all_packages().await?;
let package_info = all_pkgs.iter().find(|p| p.name == package_name);
let package_relative_path = if let Some(pkg) = package_info {
file_path.strip_prefix(&pkg.location).unwrap_or(&file_path).to_path_buf()
} else {
file_path.clone()
};
let change_type = FileChangeType::from_git_status(&git_file.status);
let mut file_change =
FileChange::new(file_path.clone(), package_relative_path, change_type);
if !matches!(change_type, FileChangeType::Deleted) {
match self.git_repo.get_file_diff_stats(git_file.path.as_str()) {
Ok(diff_stats) => {
file_change.lines_added = Some(diff_stats.lines_added);
file_change.lines_deleted = Some(diff_stats.lines_deleted);
}
Err(_) => {
}
}
}
package_file_changes.entry(package_name).or_default().push(file_change);
}
}
let all_packages = self.get_all_packages().await?;
let mut report = ChangesReport::new(AnalysisMode::WorkingDirectory, self.is_monorepo());
for package_info in all_packages {
let mut package_changes = PackageChanges::new(package_info.clone());
if let Ok(version) = crate::types::Version::parse(&package_info.version) {
package_changes.current_version = Some(version);
}
if let Some(files) = package_file_changes.get(&package_info.name) {
for file in files {
package_changes.add_file(file.clone());
}
}
report.add_package(package_changes);
}
Ok(report)
}
pub async fn analyze_commit_range(
&self,
from_ref: &str,
to_ref: &str,
) -> ChangesResult<crate::changes::ChangesReport> {
use crate::changes::{
ChangesReport, CommitInfo, FileChange, FileChangeType, PackageChanges,
};
use std::collections::{HashMap, HashSet};
let commits = self.git_repo.get_commits_between(from_ref, to_ref, &None).map_err(|e| {
ChangesError::GitError {
operation: "get_commits_between".to_string(),
reason: format!("Failed to get commits between {} and {}: {}", from_ref, to_ref, e),
}
})?;
if commits.is_empty() {
return Err(ChangesError::InvalidCommitRange {
from: from_ref.to_string(),
to: to_ref.to_string(),
reason: "No commits found in range".to_string(),
});
}
let changed_files =
self.git_repo.get_files_changed_between(from_ref, to_ref).map_err(|e| {
ChangesError::GitError {
operation: "get_files_changed_between".to_string(),
reason: format!("Failed to get changed files: {}", e),
}
})?;
if changed_files.is_empty() {
return Err(ChangesError::NoChangesDetected {
scope: format!("commit range {} to {}", from_ref, to_ref),
});
}
let changed_paths: Vec<PathBuf> =
changed_files.iter().map(|f| PathBuf::from(&f.path)).collect();
let mut package_mapper = PackageMapper::with_filesystem_and_config(
self.workspace_root.clone(),
self.fs.clone(),
&self.config,
);
let files_by_package = package_mapper.map_files_to_packages(&changed_paths).await?;
let mut package_file_changes: HashMap<String, Vec<FileChange>> = HashMap::new();
for git_file in &changed_files {
let file_path = PathBuf::from(&git_file.path);
if let Some(package_name) = package_mapper.find_package_for_file(&file_path).await? {
let all_pkgs = self.get_all_packages().await?;
let package_info = all_pkgs.iter().find(|p| p.name == package_name);
let package_relative_path = if let Some(pkg) = package_info {
file_path.strip_prefix(&pkg.location).unwrap_or(&file_path).to_path_buf()
} else {
file_path.clone()
};
let change_type = FileChangeType::from_git_status(&git_file.status);
let mut file_change =
FileChange::new(file_path.clone(), package_relative_path, change_type);
file_change.lines_added = None;
file_change.lines_deleted = None;
package_file_changes.entry(package_name).or_default().push(file_change);
}
}
let mut commits_by_package: HashMap<String, HashSet<String>> = HashMap::new();
for repo_commit in &commits {
for package_name in files_by_package.keys() {
commits_by_package
.entry(package_name.clone())
.or_default()
.insert(repo_commit.hash.clone());
}
}
let mut commit_info_by_package: HashMap<String, Vec<CommitInfo>> = HashMap::new();
for (package_name, commit_hashes) in &commits_by_package {
let mut package_commits = Vec::new();
for repo_commit in &commits {
if commit_hashes.contains(&repo_commit.hash) {
let affected_packages: Vec<String> = commits_by_package
.iter()
.filter(|(_, hashes)| hashes.contains(&repo_commit.hash))
.map(|(name, _)| name.clone())
.collect();
let mut commit_info =
CommitInfo::from_git_commit(repo_commit, affected_packages);
commit_info.files_changed = changed_files.len();
commit_info.lines_added = 0;
commit_info.lines_deleted = 0;
package_commits.push(commit_info);
}
}
commit_info_by_package.insert(package_name.clone(), package_commits);
}
for (package_name, files) in package_file_changes.iter_mut() {
if let Some(commit_hashes) = commits_by_package.get(package_name) {
for file in files {
file.commits = commit_hashes.iter().cloned().collect();
}
}
}
let all_packages = self.get_all_packages().await?;
let mut report = ChangesReport::new_for_range(from_ref, to_ref, self.is_monorepo());
for package_info in all_packages {
let mut package_changes = PackageChanges::new(package_info.clone());
if let Ok(version) = crate::types::Version::parse(&package_info.version) {
package_changes.current_version = Some(version);
}
if let Some(files) = package_file_changes.get(&package_info.name) {
for file in files {
package_changes.add_file(file.clone());
}
}
if let Some(commits) = commit_info_by_package.get(&package_info.name) {
for commit in commits {
package_changes.add_commit(commit.clone());
}
}
report.add_package(package_changes);
}
Ok(report)
}
pub async fn analyze_with_versions(
&self,
from_ref: &str,
to_ref: &str,
changeset: &crate::types::Changeset,
) -> ChangesResult<crate::changes::ChangesReport> {
let mut report = self.analyze_commit_range(from_ref, to_ref).await?;
for package_changes in &mut report.packages {
if changeset.packages.contains(&package_changes.package_name) {
self.add_version_info(package_changes, changeset)?;
}
}
Ok(report)
}
fn calculate_next_version(
&self,
current_version: &crate::types::Version,
bump: crate::types::VersionBump,
) -> ChangesResult<crate::types::Version> {
current_version.bump(bump).map_err(|e| ChangesError::VersionCalculationFailed {
package: String::from("unknown"), current_version: current_version.to_string(),
bump_type: format!("{:?}", bump),
reason: e.to_string(),
})
}
fn add_version_info(
&self,
package_changes: &mut crate::changes::PackageChanges,
changeset: &crate::types::Changeset,
) -> ChangesResult<()> {
if let Some(current) = &package_changes.current_version {
let next = self.calculate_next_version(current, changeset.bump).map_err(|e| {
if let ChangesError::VersionCalculationFailed {
package: _,
current_version,
bump_type,
reason,
} = e
{
ChangesError::VersionCalculationFailed {
package: package_changes.package_name.clone(),
current_version,
bump_type,
reason,
}
} else {
e
}
})?;
package_changes.next_version = Some(next);
package_changes.bump_type = Some(changeset.bump);
} else {
}
Ok(())
}
async fn get_all_packages(&self) -> ChangesResult<Vec<WorkspacePackage>> {
let packages = if let Some(_monorepo_kind) = &self.monorepo_kind {
self.monorepo_detector.detect_packages(&self.workspace_root).await.map_err(|e| {
ChangesError::MonorepoDetectionFailed {
reason: format!("Failed to detect workspace packages: {}", e),
}
})?
} else {
let package_json_path = self.workspace_root.join("package.json");
let content_result = self.fs.read_file_string(&package_json_path).await;
if content_result.is_err() {
return Err(ChangesError::NoPackagesFound {
workspace_root: self.workspace_root.clone(),
});
}
let content = content_result.map_err(|e| ChangesError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to read package.json: {}", e),
})?;
let package_json: serde_json::Value = serde_json::from_str(&content).map_err(|e| {
ChangesError::PackageJsonParseError {
path: package_json_path.clone(),
reason: format!("Failed to parse package.json: {}", e),
}
})?;
let name = package_json["name"].as_str().unwrap_or("root").to_string();
let version = package_json["version"].as_str().unwrap_or("0.0.0").to_string();
let package = WorkspacePackage {
name,
version,
location: PathBuf::from("."),
absolute_path: self.workspace_root.clone(),
workspace_dependencies: Vec::new(),
workspace_dev_dependencies: Vec::new(),
};
vec![package]
};
Ok(packages)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
use tempfile::TempDir;
async fn create_test_git_repo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
Repo::create(repo_path.to_str().unwrap()).unwrap();
(temp_dir, repo_path)
}
#[allow(dead_code)]
async fn create_test_workspace(with_package_json: bool) -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().to_path_buf();
if with_package_json {
let package_json = r#"{
"name": "test-package",
"version": "1.0.0"
}"#;
fs::write(workspace_path.join("package.json"), package_json).unwrap();
}
(temp_dir, workspace_path)
}
async fn create_test_monorepo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().to_path_buf();
let root_package_json = r#"{
"name": "test-monorepo",
"version": "1.0.0",
"workspaces": ["packages/*"]
}"#;
fs::write(workspace_path.join("package.json"), root_package_json).unwrap();
let pnpm_workspace = "packages:\n - 'packages/*'\n";
fs::write(workspace_path.join("pnpm-workspace.yaml"), pnpm_workspace).unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-a")).unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-b")).unwrap();
let pkg_a_json = r#"{
"name": "@test/pkg-a",
"version": "1.0.0"
}"#;
fs::write(workspace_path.join("packages/pkg-a/package.json"), pkg_a_json).unwrap();
let pkg_b_json = r#"{
"name": "@test/pkg-b",
"version": "1.0.0"
}"#;
fs::write(workspace_path.join("packages/pkg-b/package.json"), pkg_b_json).unwrap();
(temp_dir, workspace_path)
}
#[tokio::test]
async fn test_new_with_valid_single_package_repo() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let result = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await;
assert!(result.is_ok());
let analyzer = result.unwrap();
assert_eq!(analyzer.workspace_root(), repo_path.as_path());
assert!(!analyzer.is_monorepo());
assert!(analyzer.monorepo_kind().is_none());
}
#[tokio::test]
async fn test_new_with_valid_monorepo() {
let (_temp, workspace_path) = create_test_monorepo().await;
Repo::create(workspace_path.to_str().unwrap()).unwrap();
let git_repo = Repo::open(workspace_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let result = ChangesAnalyzer::new(workspace_path.clone(), git_repo, fs, config).await;
assert!(result.is_ok());
let analyzer = result.unwrap();
assert_eq!(analyzer.workspace_root(), workspace_path.as_path());
assert!(analyzer.is_monorepo());
assert!(analyzer.monorepo_kind().is_some());
}
#[tokio::test]
async fn test_new_with_nonexistent_workspace() {
let nonexistent_path = PathBuf::from("/nonexistent/path/to/workspace");
let (_temp_git, repo_path) = create_test_git_repo().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let result = ChangesAnalyzer::new(nonexistent_path.clone(), git_repo, fs, config).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ChangesError::InvalidWorkspaceRoot { .. }));
assert!(err.as_ref().contains("invalid workspace root"));
}
#[tokio::test]
async fn test_new_with_file_instead_of_directory() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("file.txt");
fs::write(&file_path, "test content").unwrap();
let (_temp_git, repo_path) = create_test_git_repo().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let result = ChangesAnalyzer::new(file_path.clone(), git_repo, fs, config).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ChangesError::InvalidWorkspaceRoot { .. }));
}
#[tokio::test]
async fn test_workspace_root_accessor() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await.unwrap();
assert_eq!(analyzer.workspace_root(), repo_path.as_path());
}
#[tokio::test]
async fn test_git_repo_accessor() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await.unwrap();
let repo = analyzer.git_repo();
let repo_path_from_accessor = repo.get_repo_path();
assert!(!repo_path_from_accessor.as_os_str().is_empty());
}
#[tokio::test]
async fn test_is_monorepo_single_package() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
assert!(!analyzer.is_monorepo());
}
#[tokio::test]
async fn test_is_monorepo_with_workspaces() {
let (_temp, workspace_path) = create_test_monorepo().await;
Repo::create(workspace_path.to_str().unwrap()).unwrap();
let git_repo = Repo::open(workspace_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(workspace_path, git_repo, fs, config).await.unwrap();
assert!(analyzer.is_monorepo());
}
#[tokio::test]
async fn test_monorepo_kind_accessor() {
let (_temp, workspace_path) = create_test_monorepo().await;
Repo::create(workspace_path.to_str().unwrap()).unwrap();
let git_repo = Repo::open(workspace_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(workspace_path, git_repo, fs, config).await.unwrap();
let kind = analyzer.monorepo_kind();
assert!(kind.is_some());
}
#[tokio::test]
async fn test_monorepo_detector_accessor() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await.unwrap();
let detector = analyzer.monorepo_detector();
let has_multiple = detector.has_multiple_packages(repo_path.as_path()).await;
assert!(!has_multiple);
}
#[tokio::test]
async fn test_filesystem_accessor() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await.unwrap();
let filesystem = analyzer.filesystem();
let package_json_path = repo_path.join("package.json");
assert!(filesystem.exists(&package_json_path).await);
}
#[tokio::test]
async fn test_config_accessor() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let config = analyzer.config();
assert_eq!(config.changeset.path, ".changesets");
}
#[tokio::test]
async fn test_with_filesystem_custom() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let result =
ChangesAnalyzer::with_filesystem(repo_path.clone(), git_repo, fs, config).await;
assert!(result.is_ok());
let analyzer = result.unwrap();
assert_eq!(analyzer.workspace_root(), repo_path.as_path());
}
#[tokio::test]
async fn test_analyzer_clone() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await.unwrap();
let cloned_analyzer = analyzer.clone();
assert_eq!(cloned_analyzer.workspace_root(), analyzer.workspace_root());
assert_eq!(cloned_analyzer.is_monorepo(), analyzer.is_monorepo());
}
#[tokio::test]
async fn test_validate_workspace_root_with_valid_directory() {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().to_path_buf();
let fs = FileSystemManager::new();
let result =
ChangesAnalyzer::<FileSystemManager>::validate_workspace_root(&workspace_path, &fs)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_workspace_root_with_nonexistent_path() {
let nonexistent = PathBuf::from("/this/does/not/exist");
let fs = FileSystemManager::new();
let result =
ChangesAnalyzer::<FileSystemManager>::validate_workspace_root(&nonexistent, &fs).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ChangesError::InvalidWorkspaceRoot { .. }));
}
#[tokio::test]
async fn test_validate_git_repo_with_valid_repo() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let result = ChangesAnalyzer::<FileSystemManager>::validate_git_repo(&git_repo, &repo_path);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_multiple_analyzer_instances() {
let (_temp_git, repo_path) = create_test_git_repo().await;
let package_json = r#"{"name": "test", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
let git_repo1 = Repo::open(repo_path.to_str().unwrap()).unwrap();
let git_repo2 = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs1 = FileSystemManager::new();
let fs2 = FileSystemManager::new();
let config1 = PackageToolsConfig::default();
let config2 = PackageToolsConfig::default();
let analyzer1 =
ChangesAnalyzer::new(repo_path.clone(), git_repo1, fs1, config1).await.unwrap();
let analyzer2 =
ChangesAnalyzer::new(repo_path.clone(), git_repo2, fs2, config2).await.unwrap();
assert_eq!(analyzer1.workspace_root(), analyzer2.workspace_root());
assert_eq!(analyzer1.is_monorepo(), analyzer2.is_monorepo());
}
}