1use std::collections::HashMap;
6use std::io::Write;
7use std::path::Path;
8
9use clap::{Parser, ValueEnum};
10use mkit_core::hash::Hash;
11use mkit_core::layout::RepoLayout;
12
13use crate::clap_shim;
14use crate::config;
15use crate::exit;
16use crate::format::{self, JsonObject};
17use crate::remote_dispatch;
18
19#[derive(Debug, Clone, Copy, ValueEnum)]
20enum FetchFormat {
21 Default,
22 Json,
23}
24
25#[derive(Debug, Parser)]
26#[command(
27 name = "mkit fetch",
28 about = "Download from the configured remote without merging."
29)]
30struct FetchOpts {
31 remote: Option<String>,
33 #[arg(long = "no-verify-signatures")]
39 no_verify_signatures: bool,
40 #[arg(long, conflicts_with = "remote")]
44 all: bool,
45 #[arg(long, value_enum, default_value = "default")]
50 format: FetchFormat,
51 #[arg(short = 'q', long)]
53 quiet: bool,
54}
55
56#[must_use]
57pub fn run(args: &[String]) -> u8 {
58 let opts = match clap_shim::parse::<FetchOpts>("mkit fetch", args) {
59 Ok(o) => o,
60 Err(code) => return code,
61 };
62 let json = matches!(opts.format, FetchFormat::Json);
63 let cwd = match std::env::current_dir() {
64 Ok(p) => p,
65 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
66 };
67 let layout = match super::resolve_layout(&cwd) {
68 Ok(layout) => layout,
69 Err(code) => return code,
70 };
71 let cfg = match config::read_layered(&layout) {
72 Ok(c) => c,
73 Err(e) => return emit_err_json(&format!("config: {e}"), exit::CONFIG_ERROR, json),
74 };
75 let require_signed = !opts.no_verify_signatures && cfg.merged.pull_require_signed_or_default();
78 if opts.all {
79 let names = config::configured_remote_names(&cfg);
80 if names.is_empty() {
81 return emit_err_json(
82 "no remote configured — use `mkit remote add <url>`",
83 exit::CONFIG_ERROR,
84 json,
85 );
86 }
87 let mut worst = exit::OK;
91 for name in names {
92 let code = fetch_one(&cwd, &layout, &cfg, &name, require_signed, json, opts.quiet);
93 if code != exit::OK {
94 worst = code;
95 }
96 }
97 return worst;
98 }
99 fetch_one(
100 &cwd,
101 &layout,
102 &cfg,
103 opts.remote.as_deref().unwrap_or(""),
104 require_signed,
105 json,
106 opts.quiet,
107 )
108}
109
110fn fetch_one(
114 cwd: &Path,
115 layout: &RepoLayout,
116 cfg: &config::LayeredConfig,
117 remote: &str,
118 require_signed: bool,
119 json: bool,
120 quiet: bool,
121) -> u8 {
122 let Some(resolved) = config::resolve_remote(cfg, remote) else {
123 return emit_err_json(
124 &if remote.is_empty() {
125 "no remote configured — use `mkit remote add <url>`".to_owned()
126 } else {
127 format!("unknown remote '{remote}'")
128 },
129 exit::CONFIG_ERROR,
130 json,
131 );
132 };
133 let endpoint = resolved.endpoint.as_str();
134 let before = tracking_snapshot(layout, &resolved.name);
137 match remote_dispatch::open_trusted(endpoint, &resolved.name, resolved.repo_chosen, cfg, layout)
138 {
139 Ok(tx) => {
140 let fetch_outcome = {
141 let _progress = crate::progress::start(
144 "Unpacking objects",
145 None,
146 crate::progress::should_report(quiet),
147 quiet,
148 );
149 remote_dispatch::fetch_all_with(cwd, tx.as_ref(), &resolved.name, require_signed)
150 };
151 match fetch_outcome {
152 Ok(_) => {
153 let after = tracking_snapshot(layout, &resolved.name);
154 report_fetch(endpoint, &resolved.name, &before, &after);
155 if json {
156 emit_fetch_json(&resolved.name, endpoint, &before, &after);
157 }
158 exit::OK
159 }
160 Err(remote_dispatch::DispatchError::Interrupted) => {
161 emit_err_json("fetch: interrupted", exit::TEMPFAIL, json)
162 }
163 Err(e @ remote_dispatch::DispatchError::UnsignedOrInvalidObject { .. }) => {
164 emit_err_json(&format!("fetch: {e}"), exit::DATAERR, json)
165 }
166 Err(e) => emit_err_json(&format!("fetch: {e}"), exit::GENERAL_ERROR, json),
167 }
168 }
169 Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
170 emit_err_json(&msg, exit::CONFIG_ERROR, json)
171 }
172 Err(e) => emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
173 }
174}
175
176fn emit_fetch_json(
179 remote: &str,
180 endpoint: &str,
181 before: &HashMap<String, Hash>,
182 after: &HashMap<String, Hash>,
183) {
184 let mut changed: Vec<(&String, Option<Hash>, Hash)> = after
185 .iter()
186 .filter(|(name, new)| before.get(*name) != Some(*new))
187 .map(|(name, new)| (name, before.get(name).copied(), *new))
188 .collect();
189 changed.sort_by(|a, b| a.0.cmp(b.0));
190 let entries: Vec<String> = changed
191 .iter()
192 .map(|(name, old, new)| {
193 let mut obj = JsonObject::new();
194 obj.field_str("name", name)
195 .field_opt_hash("old", old.as_ref())
196 .field_hash("new", new);
197 obj.finish()
198 })
199 .collect();
200 let mut top = JsonObject::new();
201 top.field_bool("ok", true)
202 .field_str("remote", remote)
203 .field_str("endpoint", endpoint)
204 .field_raw("updated", &format!("[{}]", entries.join(",")));
205 let mut stdout = std::io::stdout().lock();
206 let _ = writeln!(stdout, "{}", top.finish());
207}
208
209fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
212 if json {
213 let mut obj = JsonObject::new();
214 obj.field_bool("ok", false).field_str("error", msg);
215 let mut stdout = std::io::stdout().lock();
216 let _ = writeln!(stdout, "{}", obj.finish());
217 }
218 emit_err(msg, code)
219}
220
221fn tracking_snapshot(layout: &RepoLayout, remote: &str) -> HashMap<String, Hash> {
224 super::list_remote_refs_parallel(layout, remote)
225 .unwrap_or_default()
226 .into_iter()
227 .filter_map(|r| r.hash.map(|h| (r.name, h)))
228 .collect()
229}
230
231fn report_fetch(
234 endpoint: &str,
235 remote: &str,
236 before: &HashMap<String, Hash>,
237 after: &HashMap<String, Hash>,
238) {
239 let mut changed: Vec<(&String, Option<Hash>, Hash)> = after
240 .iter()
241 .filter(|(name, new)| before.get(*name) != Some(*new))
242 .map(|(name, new)| (name, before.get(name).copied(), *new))
243 .collect();
244 if changed.is_empty() {
245 return;
246 }
247 changed.sort_by(|a, b| a.0.cmp(b.0));
248 let mut stderr = std::io::stderr().lock();
249 let _ = writeln!(stderr, "From {endpoint}");
250 for (name, old, new) in changed {
251 let dst = format!("{remote}/{name}");
255 let _ = writeln!(
256 stderr,
257 "{}",
258 format::ref_update_line(old.as_ref(), &new, name, &dst, false)
259 );
260 }
261}
262
263use super::error as emit_err;