Skip to main content

dev_prune/commands/
restore.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Handler for the `dev-prune restore` command.
5//
6// Detects package managers in a project and restores dependencies from lockfiles.
7
8use std::collections::BTreeMap;
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12
13use crate::config::Registry;
14use crate::engine;
15use crate::output;
16
17/// Run the `restore` command.
18pub fn run(path_str: &str) -> Result<()> {
19    let path = Path::new(path_str)
20        .canonicalize()
21        .with_context(|| format!("Path not found: {path_str}"))?;
22
23    output::print_header("dev-prune restore");
24    output::print_info(&format!(
25        "Restoring dependencies in {}",
26        output::clean_path(&path)
27    ));
28
29    // Read through the user's configured depth, not the built-in default: a repository
30    // pruned at a deeper setting has to be restored at that same setting.
31    let global_depth = crate::config::Registry::load()
32        .map(|r| r.settings.scan_depth)
33        .unwrap_or(crate::constants::DEFAULT_SCAN_DEPTH);
34    let results = engine::restore_project_to_depth(&path, global_depth)?;
35
36    let mut failed = 0usize;
37
38    for (adapter_name, result) in &results {
39        match result {
40            Ok(()) => {
41                output::print_success(&format!("{adapter_name}: dependencies restored"));
42            }
43            Err(e) => {
44                output::print_error(&format!("{adapter_name}: {e}"));
45                failed += 1;
46            }
47        }
48    }
49
50    // A restore that failed has to exit non-zero. `devp prune && devp restore` in a
51    // script, or a CI step that runs it, otherwise carries on against a project whose
52    // dependencies are half installed — the one situation the exit code exists for.
53    if failed > 0 {
54        anyhow::bail!(
55            "{failed} of {} {} failed to restore — see the errors above",
56            results.len(),
57            output::plural(results.len(), "adapter", "adapters")
58        );
59    }
60
61    output::print_success(&format!(
62        "Restored {} {}.",
63        results.len(),
64        output::plural(results.len(), "adapter", "adapters")
65    ));
66
67    Ok(())
68}
69
70/// Run `restore --last-run`: put back exactly what the most recent prune pass deleted.
71///
72/// The undo the tool did not have. `devp undo` reverses an `init` or a `link`, which are
73/// registry edits; the thing people actually want reversed is the pass that emptied
74/// twelve directories across four repositories a minute ago, and reconstructing that list
75/// by hand means remembering which repositories were even in it.
76///
77/// The record is not cleared afterwards. Restoring twice is harmless — the second pass is
78/// each manager's own no-op — and a `--last-run` that could only be used once would fail
79/// exactly when a partial restore made the user want to re-run it.
80pub fn run_last_run() -> Result<()> {
81    let registry = Registry::load()?;
82
83    let Some(last) = registry.last_prune.as_ref() else {
84        anyhow::bail!(
85            "No prune pass has been recorded yet, so there is nothing to put back.\n  \
86             `devp restore <path>` restores a project you name."
87        );
88    };
89
90    let total: u64 = last.dirs.iter().map(|d| d.size_freed).sum();
91
92    output::print_header("dev-prune restore --last-run");
93    output::print_info(&format!(
94        "Putting back {} {} deleted on {} ({}).",
95        last.dirs.len(),
96        output::plural(last.dirs.len(), "directory", "directories"),
97        last.at.format("%Y-%m-%d %H:%M UTC"),
98        output::format_bytes(total)
99    ));
100
101    // Grouped by repository so each tree is walked once, in a stable order, however the
102    // pass that recorded them happened to interleave.
103    let mut by_repo: BTreeMap<PathBuf, Vec<(String, String)>> = BTreeMap::new();
104    for dir in &last.dirs {
105        by_repo
106            .entry(dir.repo_path.clone())
107            .or_default()
108            .push((dir.bloat_dir.clone(), dir.adapter.clone()));
109    }
110
111    let global_depth = registry.settings.scan_depth;
112    let mut attempted = 0usize;
113    let mut failed = 0usize;
114
115    for (repo_path, deleted) in &by_repo {
116        println!();
117        output::print_info(&output::clean_path(repo_path));
118
119        // A repository that is gone is reported per directory, not skipped, so the count
120        // at the end still adds up to what the prune took.
121        if !repo_path.exists() {
122            for (label, adapter) in deleted {
123                attempted += 1;
124                failed += 1;
125                output::print_error(&format!(
126                    "  {adapter} ({label}): the repository no longer exists at this path"
127                ));
128            }
129            continue;
130        }
131
132        for (label, result) in engine::restore_deleted(repo_path, deleted, global_depth) {
133            attempted += 1;
134            match result {
135                Ok(()) => output::print_success(&format!("  {label}: restored")),
136                Err(e) => {
137                    failed += 1;
138                    output::print_error(&format!("  {label}: {e}"));
139                }
140            }
141        }
142    }
143
144    println!();
145    if failed > 0 {
146        anyhow::bail!(
147            "{failed} of {attempted} {} failed to restore — see the errors above",
148            output::plural(attempted, "directory", "directories")
149        );
150    }
151
152    output::print_success(&format!(
153        "Restored {attempted} {} across {} {}.",
154        output::plural(attempted, "directory", "directories"),
155        by_repo.len(),
156        output::plural(by_repo.len(), "repository", "repositories")
157    ));
158
159    Ok(())
160}