1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Copyright 2026 VKrishna04
// SPDX-License-Identifier: Apache-2.0
// Handler for the `dev-prune init` command.
//
// Scans provided paths for Git repositories, registers them in the registry,
// auto-configures background daemon & hooks, and self-heals project configuration schemas.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::config::{PerRepoConfig, Registry};
use crate::output;
use crate::{constants, discovery, scanner};
/// Run the `init` command.
///
/// Scans each provided path for Git repositories and adds them to the registry. With
/// `auto`, the paths are worked out rather than given — see [`crate::discovery`].
pub fn run(paths: &[String], dry_run: bool, auto: bool) -> Result<()> {
output::print_banner();
output::print_header("dev-prune init");
let mut registry = Registry::load()?;
let mut total_found = 0;
let mut skipped_throwaway = 0;
let mut skipped_opted_out = 0;
let mut newly_added_repos: Vec<PathBuf> = Vec::new();
// In `--auto` the candidates are already filtered — discovery has dropped the
// registered, the disposable and the opted-out — so the scan loop below is fed a
// single synthetic "path" list and does no scanning of its own.
let auto_found = if auto {
let found = discovery::discover(®istry)?;
output::print_info(&format!(
"Looked for unregistered repositories in {} {}:",
found.roots.len(),
output::plural(found.roots.len(), "location", "locations")
));
for root in &found.roots {
output::print_info(&format!(" {}", output::clean_path(root)));
}
total_found = found.found.len();
skipped_throwaway = found.throwaway;
skipped_opted_out = found.opted_out;
Some(found.found)
} else {
None
};
let scans: Vec<(PathBuf, Vec<PathBuf>)> = match auto_found {
Some(repos) => vec![(PathBuf::new(), repos)],
None => paths
.iter()
.map(|path_str| {
let path = Path::new(path_str)
.canonicalize()
.with_context(|| format!("Path not found: {path_str}"))?;
output::print_info(&format!(
"Scanning {} for Git repositories...",
output::clean_path(&path)
));
let repos = scanner::scan_for_repos(&path)?;
total_found += repos.len();
Ok((path, repos))
})
.collect::<Result<_>>()?,
};
for (path, repos) in scans {
for repo in repos {
// A plugin manager's throwaway clone is not a workspace. Skipped quietly and
// counted, rather than listed: on the scan that motivated this there were
// twenty-eight of them, and twenty-eight lines of explanation would have
// buried the repositories the user actually wanted registered.
if !auto && super::link::is_throwaway_checkout(&path, &repo) {
skipped_throwaway += 1;
continue;
}
// Honoured before registration, not just before deletion. `devp init` is a
// bulk scan of somebody else's directory tree, and a repository carrying the
// opt-out has already said no to being one of dev-prune's. `devp link <path>`
// still registers it, because naming a repository is not a bulk scan.
if !auto && repo.join(constants::DEVPRUNE_IGNORE_FILE).exists() {
skipped_opted_out += 1;
continue;
}
if registry.add_repo(repo.clone()) {
newly_added_repos.push(repo.clone());
let verb = if dry_run {
"Would register"
} else {
"Registered"
};
output::print_success(&format!("{verb}: {}", output::clean_path(&repo)));
let adoption =
registry.adopt_moved_entry(&repo, scanner::git::repo_identity(&repo));
super::link::report_adoption(&adoption);
} else {
output::print_info(&format!(
"Already registered: {}",
output::clean_path(&repo)
));
// Backfill, so one `devp init ~/code` teaches the whole registry to
// recognise a move later. Only when it is missing: this scan visits
// every repository under the path, every time it is run.
if registry.needs_identity(&repo) {
let adoption =
registry.adopt_moved_entry(&repo, scanner::git::repo_identity(&repo));
super::link::report_adoption(&adoption);
}
}
}
}
if !dry_run {
registry.last_added_repos = newly_added_repos.clone();
registry.save()?;
}
output::print_header("Summary");
// The registry was mutated in memory either way; only the save is skipped. Saying
// "added" after a `--dry-run` would describe a file that was never written.
output::print_info(&format!(
"Found {total_found} {}{}, {} {} new",
if auto { "unregistered " } else { "Git " },
output::plural(total_found, "repo", "repos"),
if dry_run { "would add" } else { "added" },
newly_added_repos.len()
));
if skipped_opted_out > 0 {
output::print_info(&format!(
"Skipped {skipped_opted_out} {} carrying `{}`.",
output::plural(skipped_opted_out, "repository", "repositories"),
constants::DEVPRUNE_IGNORE_FILE,
));
}
if skipped_throwaway > 0 {
// Named, not silent. A scan that quietly drops repositories is one the user
// cannot debug when it drops one they wanted — and `devp link` is the way back.
output::print_info(&format!(
"Skipped {skipped_throwaway} disposable {} (plugin-manager clones, temp directories). \
`devp link <path>` registers one anyway.",
output::plural(skipped_throwaway, "checkout", "checkouts"),
));
}
// After a dry run the in-memory count includes repositories that were never
// written; "would be tracked" is the honest phrasing for it.
output::print_info(&format!(
"{}: {}",
if dry_run {
"Would be tracked"
} else {
"Total tracked"
},
registry.repo_count()
));
if !dry_run {
// Install anything missing. Idempotent, and identical to what `devp setup` and
// the post-upgrade pass do, so there is one code path and one set of rules —
// but only on a machine that has said yes to it. `devp init` was typed to
// register repositories, which is not yet a yes to a scheduled task, and
// stamping while the question is open would swallow the question itself.
if crate::setup::consent_state() == crate::setup::SetupConsent::Granted {
if let Some(report) = crate::setup::ensure_integrations_if_enabled(®istry)
&& (report.changed_anything() || report.needs_attention())
{
output::print_header("Integrations");
report.print(false);
}
crate::setup::suppress_next_auto_setup();
}
if registry.settings.auto_config {
for repo in &newly_added_repos {
crate::commands::link::ensure_default_repo_config(repo);
}
}
// Validate (do NOT rewrite) existing per-repo configs.
//
// The previous behaviour re-serialised `unwrap_or_default()` into every repo,
// which silently replaced a malformed `.devprune.json` with defaults and created
// a config plus an exclude entry in repos that never had one. Report instead.
for repo in registry.repositories.keys() {
if let Err(e) = PerRepoConfig::load_with_diagnostics(repo) {
output::print_warning(&format!(
"{}: `.devprune.json` could not be parsed and was left untouched — {e}",
output::clean_path(repo)
));
}
}
}
// Setting a machine up is exactly the moment to find out the binary is a version
// behind, so this asks now rather than waiting for the weekly interval that governs
// `devp run`. `--dry-run` included: the check writes nothing to disk of its own, and
// knowing before you commit to the real run is the point.
output::print_header("Version");
output::print_info(&format!("Installed: v{}", crate::constants::VERSION));
if registry.settings.update_check {
let mut checked = registry;
if crate::commands::update::check_now(&mut checked) && !dry_run {
let _ = checked.save();
}
registry = checked;
} else {
output::print_info(
"The release check is off (`devp config set update_check true` re-enables it).",
);
}
if dry_run {
output::print_header("Dry Run Complete");
output::print_info("Nothing was written. Re-run without `--dry-run` to register.");
return Ok(());
}
output::print_header("Initialization Complete");
output::print_success(&format!(
"dev-prune initialization complete! All {} tracked {} registered & verified.",
registry.repo_count(),
output::plural(registry.repo_count(), "repository", "repositories")
));
output::print_info("Review or undo the integrations with `devp setup --status`.");
Ok(())
}