Skip to main content

callisto_graph/
apply.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use callisto_manifests::{open, OpenContext, WorkspaceCargoResolver};
5use callisto_model::{CommandError, CommandRunner, LockfileRefreshResult, ManifestRole};
6
7use crate::cascade::DepWriteTarget;
8use crate::error::GraphError;
9use crate::plan::{VersionPlan, VersionWriteTarget};
10
11#[derive(Clone, Debug, Default)]
12pub struct ApplyOptions {
13    pub refresh_lockfiles: bool,
14    pub transient: bool,
15}
16
17#[derive(Clone, Debug, Default)]
18pub struct ApplyOutcome {
19    pub lockfile_refresh_results: Option<Vec<LockfileRefreshResult>>,
20    pub staged: Vec<PathBuf>,
21}
22
23pub fn apply_version_plan<R: CommandRunner>(
24    root: &Path,
25    plan: &VersionPlan,
26    runner: &R,
27    opts: &ApplyOptions,
28) -> Result<ApplyOutcome, GraphError> {
29    let mut outcome = ApplyOutcome::default();
30    let mut modified_paths = Vec::new();
31
32    let cargo_workspace = if root.join("Cargo.toml").exists() {
33        if let Ok(resolver) = WorkspaceCargoResolver::load(&root.join("Cargo.toml")) {
34            resolver.inheritance().ok().map(std::sync::Arc::new)
35        } else {
36            None
37        }
38    } else {
39        None
40    };
41
42    let npm_workspace_kind = callisto_manifests::detect_npm_workspace_kind(root)
43        .ok()
44        .flatten();
45
46    let ctx = OpenContext {
47        workspace_root: root,
48        cargo_workspace,
49        npm_workspace_kind,
50    };
51
52    if !opts.transient {
53        for bump in &plan.bumps {
54            for write in &bump.writes {
55                match write {
56                    VersionWriteTarget::Manifest(p) => {
57                        let fmt = callisto_model::ManifestFormat::from_path(p)?;
58                        let decl = callisto_model::ManifestDecl::new(
59                            p.clone(),
60                            ManifestRole::Canonical,
61                            fmt,
62                        )?;
63                        let mut handle = open(&decl, &ctx)?;
64                        handle.write_version(&bump.to)?;
65                        modified_paths.push(p.clone());
66                    }
67                    VersionWriteTarget::CargoWorkspacePackage { root_manifest } => {
68                        let mut ws_res = WorkspaceCargoResolver::load(&root.join(root_manifest))?;
69                        ws_res.write_version(&bump.to)?;
70                        modified_paths.push(root_manifest.clone());
71                    }
72                }
73            }
74        }
75
76        for rewrite in &plan.rewrites {
77            match &rewrite.key.target {
78                DepWriteTarget::Manifest(p) => {
79                    let fmt = callisto_model::ManifestFormat::from_path(p)?;
80                    let decl =
81                        callisto_model::ManifestDecl::new(p.clone(), ManifestRole::Canonical, fmt)?;
82                    let mut handle = open(&decl, &ctx)?;
83                    handle.update_dependency_spec(
84                        &rewrite.key.name,
85                        rewrite.key.kind.unwrap_or(callisto_model::DepKind::Runtime),
86                        rewrite.to.clone(),
87                    )?;
88                    modified_paths.push(p.clone());
89                }
90                DepWriteTarget::CargoWorkspaceDependency { root_manifest } => {
91                    let mut ws_res = WorkspaceCargoResolver::load(&root.join(root_manifest))?;
92                    ws_res.write_dependency(&rewrite.key.name, rewrite.to.clone())?;
93                    modified_paths.push(root_manifest.clone());
94                }
95            }
96        }
97
98        for cl in &plan.changelog_writes {
99            let rendered = callisto_changelog::render_section(&cl.input)?;
100            callisto_changelog::prepend(
101                root,
102                &cl.changelog_path,
103                &cl.input.package.display_name(),
104                &rendered,
105            )?;
106            modified_paths.push(cl.changelog_path.clone());
107        }
108
109        for cs_path in &plan.consumed_changesets {
110            let full = root.join(cs_path);
111            if full.exists() {
112                fs::remove_file(&full).map_err(|e| {
113                    GraphError::Command(CommandError::Io {
114                        program: "fs".to_string(),
115                        message: e.to_string(),
116                    })
117                })?;
118                modified_paths.push(cs_path.clone());
119            }
120        }
121
122        if let Some(ref pre_state) = plan.pre_state_update {
123            let default_dir = PathBuf::from(".changeset");
124            let pre_dir = plan
125                .consumed_changesets
126                .first()
127                .and_then(|p| p.parent())
128                .unwrap_or(&default_dir);
129            let rel_pre_path = pre_dir.join("pre.json");
130            let pre_path = root.join(&rel_pre_path);
131            let text = callisto_format::write_pre_json(pre_state);
132            callisto_manifests::atomic::atomic_write(&pre_path, &text).map_err(|e| {
133                GraphError::Command(CommandError::Io {
134                    program: "fs".to_string(),
135                    message: e.to_string(),
136                })
137            })?;
138            modified_paths.push(rel_pre_path);
139        } else if plan.delete_pre_json {
140            let rel_pre_path = PathBuf::from(".changeset/pre.json");
141            let pre_path = root.join(&rel_pre_path);
142            if pre_path.exists() {
143                fs::remove_file(&pre_path).map_err(|e| {
144                    GraphError::Command(CommandError::Io {
145                        program: "fs".to_string(),
146                        message: e.to_string(),
147                    })
148                })?;
149                modified_paths.push(rel_pre_path);
150            }
151        }
152
153        // Include lockfiles if present in workspace root
154        for lockfile in &[
155            "Cargo.lock",
156            "package-lock.json",
157            "pnpm-lock.yaml",
158            "yarn.lock",
159            "bun.lockb",
160        ] {
161            let p = PathBuf::from(lockfile);
162            if root.join(&p).exists() && !modified_paths.contains(&p) {
163                modified_paths.push(p);
164            }
165        }
166    }
167
168    if !opts.transient && !modified_paths.is_empty() {
169        let (existing, deleted): (Vec<_>, Vec<_>) =
170            modified_paths.iter().partition(|p| root.join(p).exists());
171
172        if !existing.is_empty() {
173            let mut args = vec!["add", "--"];
174            let strs: Vec<String> = existing.iter().map(|p| p.display().to_string()).collect();
175            for s in &strs {
176                args.push(s);
177            }
178            drop(runner.run("git", &args, root));
179        }
180
181        if !deleted.is_empty() {
182            let mut args = vec!["rm", "--cached", "--ignore-unmatch", "--"];
183            let strs: Vec<String> = deleted.iter().map(|p| p.display().to_string()).collect();
184            for s in &strs {
185                args.push(s);
186            }
187            drop(runner.run("git", &args, root));
188        }
189
190        outcome.staged = modified_paths;
191    }
192
193    Ok(outcome)
194}