use std::ops::Range;
use std::path::{Path, PathBuf};
use anyhow::Result;
use futures::{StreamExt, TryStreamExt};
use rustc_hash::FxHashMap;
use crate::cli::ExitStatus;
use crate::cli::auto_update::config::write_new_config;
use crate::cli::auto_update::display::{apply_repo_updates, warn_frozen_mismatches};
use crate::cli::auto_update::source::{collect_repo_sources, evaluate_repo_source};
use crate::cli::reporter::AutoUpdateReporter;
use crate::cli::run::Selectors;
use crate::fs::CWD;
use crate::printer::Printer;
use crate::run::CONCURRENCY;
use crate::store::Store;
use crate::workspace::{Project, Workspace};
mod config;
mod display;
mod repository;
mod source;
#[derive(Default, Clone)]
struct Revision {
rev: String,
frozen: Option<String>,
}
struct RepoUsage<'a> {
project: &'a Project,
remote_count: usize,
remote_index: usize,
rev_line_number: usize,
current_frozen: Option<String>,
current_frozen_site: Option<FrozenCommentSite>,
}
struct RepoTarget<'a> {
repo: &'a str,
current_rev: &'a str,
required_hook_ids: Vec<&'a str>,
usages: Vec<RepoUsage<'a>>,
}
struct RepoSource<'a> {
repo: &'a str,
targets: Vec<RepoTarget<'a>>,
}
enum FrozenMismatchAction {
ReplaceWith(String),
Remove,
NoReplacement,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CommitPresence {
Present,
Absent,
Unknown,
}
enum FrozenMismatchReason {
ResolvesToDifferentCommit,
Unresolvable,
}
struct FrozenMismatch<'a> {
project: &'a Project,
remote_size: usize,
remote_index: usize,
rev_line_number: usize,
current_frozen: String,
frozen_site: Option<FrozenCommentSite>,
reason: FrozenMismatchReason,
current_rev_presence: CommitPresence,
action: FrozenMismatchAction,
}
#[derive(Clone)]
struct FrozenCommentSite {
line_number: usize,
source_line: String,
span: Range<usize>,
}
#[derive(Clone)]
struct FrozenRef {
line_number: usize,
current_frozen: Option<String>,
site: Option<FrozenCommentSite>,
}
struct TagTimestamp {
tag: String,
timestamp: u64,
commit: String,
}
struct ResolvedRepoUpdate<'a> {
revision: Revision,
frozen_mismatches: Vec<FrozenMismatch<'a>>,
}
struct RepoUpdate<'a> {
target: &'a RepoTarget<'a>,
result: Result<ResolvedRepoUpdate<'a>>,
}
type ProjectUpdates<'a> = FxHashMap<&'a Project, Vec<Option<Revision>>>;
struct ApplyRepoUpdatesResult {
failure: bool,
has_updates: bool,
}
enum DisplayEventKind {
Update { current: Revision, next: Revision },
FrozenUpdate { current: String, next: String },
FrozenRemove { current: String },
UpToDate { current: Revision },
Failure { error: String },
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum DisplayStream {
Stdout,
Stderr,
}
struct DisplayEvent<'a> {
stream: DisplayStream,
project: &'a Project,
repo: &'a str,
remote_index: usize,
line_number: usize,
kind: DisplayEventKind,
}
struct FrozenWarningEvent<'a> {
project: &'a Project,
repo: &'a str,
current_rev: &'a str,
remote_index: usize,
mismatch: &'a FrozenMismatch<'a>,
}
type RepoOccurrences<'a> = FxHashMap<(&'a Path, &'a str), usize>;
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn auto_update(
store: &Store,
config: Option<PathBuf>,
filter_repos: Vec<String>,
verbose: bool,
bleeding_edge: bool,
freeze: bool,
jobs: usize,
dry_run: bool,
check: bool,
cooldown_days: u8,
printer: Printer,
) -> Result<ExitStatus> {
let workspace_root = Workspace::find_root(config.as_deref(), &CWD)?;
let selectors = Selectors::default();
let workspace = Workspace::discover(store, workspace_root, config, Some(&selectors), true)?;
let jobs = if jobs == 0 { *CONCURRENCY } else { jobs };
let reporter = AutoUpdateReporter::new(printer);
let repo_sources = collect_repo_sources(&workspace)?;
let sources = repo_sources.iter().filter(|repo_source| {
filter_repos.is_empty() || filter_repos.iter().any(|repo| repo == repo_source.repo)
});
let outcomes: Vec<RepoUpdate<'_>> = futures::stream::iter(sources)
.map(async |repo_source| {
let progress = reporter.on_update_start(repo_source.repo);
let result =
evaluate_repo_source(repo_source, bleeding_edge, freeze, cooldown_days).await;
reporter.on_update_complete(progress);
result
})
.buffer_unordered(jobs)
.try_collect::<Vec<_>>()
.await?
.into_iter()
.flatten()
.collect();
reporter.on_complete();
warn_frozen_mismatches(&outcomes, printer)?;
#[expect(clippy::mutable_key_type)]
let mut project_updates: ProjectUpdates<'_> = FxHashMap::default();
let apply_result =
apply_repo_updates(outcomes, verbose, dry_run, printer, &mut project_updates)?;
if !dry_run {
for (project, revisions) in project_updates {
if revisions.iter().any(Option::is_some) {
write_new_config(project.config_file(), &revisions).await?;
}
}
}
if apply_result.failure || (check && apply_result.has_updates) {
return Ok(ExitStatus::Failure);
}
Ok(ExitStatus::Success)
}