Skip to main content

crate_seq_core/
init.rs

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