Skip to main content

crate_seq_core/
init.rs

1//! `init` command orchestration: git tags + registry state → per-crate ledgers.
2
3#[cfg(test)]
4#[path = "init_tests.rs"]
5mod tests;
6
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10use crate_seq_ledger::{
11    CrateConfig, CrateSeqLedger, LedgerAuth, LedgerEntry, LedgerSettings, LedgerStatus,
12    VersionSource,
13};
14use crate_seq_registry::CratesIoClient;
15
16use crate::{discover_members, Error, WorkspaceMember};
17
18/// Configuration for the `init` command.
19pub struct InitConfig {
20    /// Root of the repository (for git tag discovery).
21    pub repo_path: PathBuf,
22    /// Root workspace `Cargo.toml` directory.
23    pub workspace_root: PathBuf,
24    /// If `Some`, initialise only the named crate. If `None`, initialise all members.
25    pub crate_filter: Option<String>,
26    /// Version of crate-seq itself, for the crates.io `User-Agent` header.
27    pub crate_seq_version: String,
28}
29
30/// Result of initialising one crate's ledger.
31#[derive(Debug)]
32pub struct InitResult {
33    /// The crate that was initialised.
34    pub crate_name: String,
35    /// Path of the written ledger file.
36    pub ledger_path: PathBuf,
37    /// Number of version entries written (pending + published + yanked).
38    pub version_count: usize,
39}
40
41/// Resolves which members to process, applying the crate filter if set.
42fn select_members(
43    all: Vec<WorkspaceMember>,
44    filter: Option<&str>,
45) -> Result<Vec<WorkspaceMember>, Error> {
46    match filter {
47        None => Ok(all),
48        Some(name) => {
49            let found = all.into_iter().find(|m| m.name == name);
50            found
51                .map(|m| vec![m])
52                .ok_or_else(|| Error::CrateNotFound(name.to_owned()))
53        }
54    }
55}
56
57/// Loads the tag pattern: from existing ledger if present, otherwise auto-detected.
58fn resolve_tag_pattern(member: &WorkspaceMember, is_workspace: bool) -> String {
59    let ledger_path = member.crate_dir.join(".crate-seq.toml");
60    if ledger_path.exists() {
61        if let Ok(existing) = crate_seq_ledger::load(&ledger_path) {
62            if let Some(pattern) = existing.settings.tag_pattern {
63                return pattern;
64            }
65        }
66    }
67    crate_seq_ledger::detect_tag_pattern(&member.name, is_workspace)
68}
69
70/// Builds a map from `semver::Version` to tag name for quick lookup.
71fn tag_name_map(tags: &[crate_seq_git::TagRef]) -> HashMap<semver::Version, String> {
72    tags.iter()
73        .map(|t| (t.semver.clone(), t.name.clone()))
74        .collect()
75}
76
77/// Constructs the fallback tag ref when no git tag exists for a registry version.
78fn fallback_tag_ref(crate_name: &str, version: &semver::Version) -> String {
79    format!("{crate_name}-v{version}")
80}
81
82/// Builds `LedgerEntry` values from registry versions and git tags.
83fn build_entries(
84    member: &WorkspaceMember,
85    tags: &[crate_seq_git::TagRef],
86    registry: Option<&crate_seq_registry::CrateMetadata>,
87) -> Vec<LedgerEntry> {
88    let tag_map = tag_name_map(tags);
89    let mut entries: Vec<LedgerEntry> = Vec::new();
90
91    if let Some(metadata) = registry {
92        for v in &metadata.versions {
93            let status = if v.yanked {
94                LedgerStatus::Yanked
95            } else {
96                LedgerStatus::Published
97            };
98            let ref_ = tag_map
99                .get(&v.version)
100                .cloned()
101                .unwrap_or_else(|| fallback_tag_ref(&member.name, &v.version));
102            entries.push(LedgerEntry {
103                version: v.version.clone(),
104                source: VersionSource::GitTag,
105                ref_,
106                status,
107            });
108        }
109    }
110
111    let registry_versions: std::collections::HashSet<semver::Version> = registry
112        .map(|m| m.versions.iter().map(|v| v.version.clone()).collect())
113        .unwrap_or_default();
114
115    for tag in tags {
116        if !registry_versions.contains(&tag.semver) {
117            entries.push(LedgerEntry {
118                version: tag.semver.clone(),
119                source: VersionSource::GitTag,
120                ref_: tag.name.clone(),
121                status: LedgerStatus::Pending,
122            });
123        }
124    }
125
126    entries.sort_by(|a, b| a.version.cmp(&b.version));
127    entries
128}
129
130/// Initialises the ledger for a single workspace member.
131fn init_member(
132    member: &WorkspaceMember,
133    is_workspace: bool,
134    config: &InitConfig,
135    client: &CratesIoClient,
136) -> Result<InitResult, Error> {
137    let tag_pattern = resolve_tag_pattern(member, is_workspace);
138    let tags = crate_seq_git::discover_tags(&config.repo_path, &tag_pattern)?;
139    let registry = client.fetch_crate_metadata(&member.name)?;
140    let entries = build_entries(member, &tags, registry.as_ref());
141    let version_count = entries.len();
142
143    let ledger = CrateSeqLedger {
144        crate_config: CrateConfig {
145            name: member.name.clone(),
146            registry: None,
147        },
148        settings: LedgerSettings {
149            tag_pattern: Some(tag_pattern),
150            ..Default::default()
151        },
152        auth: LedgerAuth::default(),
153        entries,
154    };
155
156    let ledger_path = member.crate_dir.join(".crate-seq.toml");
157    crate_seq_ledger::save(&ledger_path, &ledger)?;
158
159    Ok(InitResult {
160        crate_name: member.name.clone(),
161        ledger_path,
162        version_count,
163    })
164}
165
166/// Initialises per-crate ledgers for the given workspace.
167///
168/// For each member: detects tag pattern, queries git tags and crates.io,
169/// diffs them to build ledger entries (registry-published → `Published`/`Yanked`,
170/// git-only → `Pending`), and writes the ledger alongside the crate's `Cargo.toml`.
171///
172/// # Errors
173///
174/// Returns [`Error::CrateNotFound`] if `config.crate_filter` names a crate that
175/// is not a workspace member. Propagates [`Error::Git`], [`Error::Registry`], and
176/// [`Error::Ledger`] from downstream operations.
177pub fn init(config: &InitConfig) -> Result<Vec<InitResult>, Error> {
178    let client = CratesIoClient::new(&config.crate_seq_version)?;
179    init_with_client(config, &client)
180}
181
182/// Inner implementation that accepts an injected `CratesIoClient`.
183///
184/// Separated from `init` to allow tests to inject a mock registry client
185/// without hitting the live crates.io API.
186pub(crate) fn init_with_client(
187    config: &InitConfig,
188    client: &CratesIoClient,
189) -> Result<Vec<InitResult>, Error> {
190    let all_members = discover_members(&config.workspace_root)?;
191    let members = select_members(all_members, config.crate_filter.as_deref())?;
192    let is_workspace = members.len() > 1;
193
194    let mut results = Vec::with_capacity(members.len());
195    for member in &members {
196        results.push(init_member(member, is_workspace, config, client)?);
197    }
198
199    Ok(results)
200}