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