use crate::config::{ChangesetConfig, PackageToolsConfig};
use crate::error::{ChangesetError, ChangesetResult};
use crate::types::{Changeset, UpdateSummary, VersionBump};
use std::path::PathBuf;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
use super::git_integration::PackageDetector;
use super::storage::{ChangesetStorage, FileBasedChangesetStorage};
#[derive(Debug)]
pub struct ChangesetManager<S: ChangesetStorage> {
storage: S,
workspace_root: PathBuf,
git_repo: Option<Repo>,
config: ChangesetConfig,
full_config: PackageToolsConfig,
}
impl ChangesetManager<FileBasedChangesetStorage<FileSystemManager>> {
pub async fn new(
workspace_root: impl Into<PathBuf>,
fs: FileSystemManager,
config: crate::config::PackageToolsConfig,
) -> ChangesetResult<Self> {
let workspace_root = workspace_root.into();
let changeset_config = config.changeset.clone();
let storage = FileBasedChangesetStorage::new(
workspace_root.clone(),
changeset_config.path.clone(),
changeset_config.history_path.clone(),
fs,
);
let git_repo = Repo::open(workspace_root.to_string_lossy().as_ref()).ok();
Ok(Self {
storage,
workspace_root,
git_repo,
config: changeset_config,
full_config: config,
})
}
}
impl<S: ChangesetStorage> ChangesetManager<S> {
#[must_use]
pub fn with_storage(
storage: S,
workspace_root: impl Into<PathBuf>,
git_repo: Option<Repo>,
full_config: PackageToolsConfig,
) -> Self {
let changeset_config = full_config.changeset.clone();
Self {
storage,
workspace_root: workspace_root.into(),
git_repo,
config: changeset_config,
full_config,
}
}
pub async fn create(
&self,
branch: impl Into<String>,
bump: VersionBump,
environments: Vec<String>,
) -> ChangesetResult<Changeset> {
let branch_name = branch.into();
if branch_name.is_empty() {
return Err(ChangesetError::InvalidBranch {
branch: branch_name,
reason: "Branch name cannot be empty".to_string(),
});
}
if self.storage.exists(&branch_name).await? {
let path = std::env::current_dir()
.unwrap_or_default()
.join(&self.config.path)
.join(&branch_name);
return Err(ChangesetError::AlreadyExists { branch: branch_name, path });
}
self.validate_environments(&environments)?;
let changeset = Changeset::new(branch_name, bump, environments);
self.storage.save(&changeset).await?;
Ok(changeset)
}
pub async fn load(&self, branch: &str) -> ChangesetResult<Changeset> {
self.storage.load(branch).await
}
pub async fn update(&self, changeset: &Changeset) -> ChangesetResult<()> {
let available_envs: Vec<&str> =
self.config.available_environments.iter().map(|s| s.as_str()).collect();
changeset.validate(&available_envs)?;
let mut updated_changeset = changeset.clone();
updated_changeset.touch();
self.storage.save(&updated_changeset).await?;
Ok(())
}
pub async fn delete(&self, branch: &str) -> ChangesetResult<()> {
self.storage.delete(branch).await
}
pub async fn list_pending(&self) -> ChangesetResult<Vec<Changeset>> {
self.storage.list_pending().await
}
fn validate_environments(&self, environments: &[String]) -> ChangesetResult<()> {
for env in environments {
if !self.config.available_environments.contains(env) {
return Err(ChangesetError::InvalidEnvironment {
environment: env.clone(),
available: self.config.available_environments.clone(),
});
}
}
Ok(())
}
#[must_use]
pub fn storage(&self) -> &S {
&self.storage
}
#[must_use]
pub fn git_repo(&self) -> Option<&Repo> {
self.git_repo.as_ref()
}
#[must_use]
pub fn workspace_root(&self) -> &std::path::Path {
&self.workspace_root
}
#[must_use]
pub fn config(&self) -> &ChangesetConfig {
&self.config
}
pub async fn add_commits(
&self,
branch: &str,
commit_ids: Vec<String>,
) -> ChangesetResult<UpdateSummary> {
if commit_ids.is_empty() {
return Ok(UpdateSummary::empty());
}
let mut changeset = self.load(branch).await?;
let new_commits: Vec<String> =
commit_ids.into_iter().filter(|id| !changeset.has_commit(id)).collect();
if new_commits.is_empty() {
return Ok(UpdateSummary::new(0, Vec::new(), Vec::new(), changeset.packages.clone()));
}
for commit in &new_commits {
changeset.add_commit(commit);
}
self.update(&changeset).await?;
Ok(UpdateSummary::new(
new_commits.len(),
new_commits,
Vec::new(),
changeset.packages.clone(),
))
}
pub async fn add_commits_from_git(&self, branch: &str) -> ChangesetResult<UpdateSummary> {
let repo = self.git_repo.as_ref().ok_or_else(|| ChangesetError::GitIntegration {
operation: "add commits from git".to_string(),
reason: "Git repository not available".to_string(),
})?;
let mut changeset = self.load(branch).await?;
let since_commit = changeset.changes.last().cloned();
let detector = PackageDetector::new_with_config(
self.workspace_root.clone(),
repo,
FileSystemManager::new(),
&self.full_config,
);
let new_commits = detector.get_commits_since(since_commit)?;
if new_commits.is_empty() {
return Ok(UpdateSummary::empty());
}
let commit_ids: Vec<String> = new_commits.iter().map(|c| c.hash.clone()).collect();
let affected_packages = detector.detect_affected_packages(&commit_ids).await?;
let mut new_packages: Vec<String> = Vec::new();
let mut existing_packages: Vec<String> = Vec::new();
for pkg in affected_packages {
if changeset.has_package(&pkg) {
existing_packages.push(pkg);
} else {
new_packages.push(pkg);
}
}
for package in &new_packages {
changeset.add_package(package);
}
for commit_id in &commit_ids {
changeset.add_commit(commit_id);
}
self.update(&changeset).await?;
Ok(UpdateSummary::new(commit_ids.len(), commit_ids, new_packages, existing_packages))
}
pub async fn archive(
&self,
branch: &str,
release_info: crate::types::ReleaseInfo,
) -> ChangesetResult<crate::types::ArchiveResult> {
let changeset = self.load(branch).await?;
self.storage.archive(&changeset, release_info).await
}
}