use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use anyhow::Result; use serde::{Deserialize, Serialize}; use crate::git::RemoteRefSnapshot; use crate::state::{load_toml_or_default, remove_file_if_exists, save_toml}; use super::output::format_error; const FAILURE_STATE_FILE: &str = "failed-repos.toml"; const REF_STATE_FILE: &str = "ref-state.toml"; #[derive(Debug)] pub(super) struct SyncFailure { pub(super) scope: String, pub(super) error: String, retry: Option, } impl SyncFailure { pub(super) fn group(scope: String, error: anyhow::Error) -> Self { Self { scope, error: format_error(&error), retry: None, } } pub(super) fn repo(group: String, repo: String, error: anyhow::Error) -> Self { Self { scope: format!("{group}/{repo}"), error: format_error(&error), retry: Some(FailedRepo { group, repo }), } } } #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub(super) struct FailedRepo { pub(super) group: String, pub(super) repo: String, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub(super) struct FailureState { #[serde(default)] pub(super) repos: Vec, } impl FailureState { pub(super) fn from_failures(failures: &[SyncFailure]) -> Self { let repos = failures .iter() .filter_map(|failure| failure.retry.clone()) .collect::>() .into_iter() .collect(); Self { repos } } pub(super) fn repos_by_group(&self) -> BTreeMap> { let mut output = BTreeMap::>::new(); for failure in &self.repos { output .entry(failure.group.clone()) .or_default() .insert(failure.repo.clone()); } output } } pub(super) fn load_failure_state(work_dir: &Path) -> Result { load_toml_or_default(&failure_state_path(work_dir)) } pub(super) fn save_failure_state(work_dir: &Path, state: &FailureState) -> Result<()> { let path = failure_state_path(work_dir); if state.repos.is_empty() { remove_file_if_exists(&path) } else { save_toml(&path, state) } } pub(super) fn failure_state_path(work_dir: &Path) -> PathBuf { work_dir.join(FAILURE_STATE_FILE) } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub(super) struct RemoteRefState { pub(super) hash: String, pub(super) refs: usize, #[serde(default)] pub(super) branches: BTreeMap, #[serde(default)] pub(super) tags: BTreeMap, } impl From for RemoteRefState { fn from(value: RemoteRefSnapshot) -> Self { Self { hash: value.hash, refs: value.refs, branches: value.branches, tags: value.tags, } } } impl From<&RemoteRefState> for RemoteRefSnapshot { fn from(value: &RemoteRefState) -> Self { Self { hash: value.hash.clone(), refs: value.refs, branches: value.branches.clone(), tags: value.tags.clone(), } } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub(super) struct RefState { #[serde(default)] repos: BTreeMap>>, } impl RefState { pub(super) fn repo_matches( &self, group: &str, repo: &str, refs: &BTreeMap, ) -> bool { self.repos.get(group).and_then(|repos| repos.get(repo)) == Some(refs) } pub(super) fn set_repo( &mut self, group: &str, repo: &str, refs: BTreeMap, ) { self.repos .entry(group.to_string()) .or_default() .insert(repo.to_string(), refs); } pub(super) fn repo( &self, group: &str, repo: &str, ) -> Option<&BTreeMap> { self.repos.get(group).and_then(|repos| repos.get(repo)) } } pub(super) fn load_ref_state(work_dir: &Path) -> Result { load_toml_or_default(&ref_state_path(work_dir)) } pub(super) fn save_ref_state(work_dir: &Path, state: &RefState) -> Result<()> { save_toml(&ref_state_path(work_dir), state) } fn ref_state_path(work_dir: &Path) -> PathBuf { work_dir.join(REF_STATE_FILE) }